diff --git a/index.html b/index.html
index 126b34c..1414388 100644
--- a/index.html
+++ b/index.html
@@ -4,14 +4,14 @@
@@ -22,7 +22,7 @@
@@ -35,7 +35,7 @@
@@ -46,7 +46,7 @@
"@type": "WebApplication",
"name": "Fleeting Garden",
"url": "https://schmelczer.dev/fleeting/",
- "description": "Draw colour into a canvas that keeps moving. Your strokes become paths for life that splits, drifts, and redraws the surface over time.",
+ "description": "Plant colour, fold gestures with mirrors, and watch small agents turn each brushstroke into a shifting WebGPU garden.",
"image": "https://schmelczer.dev/fleeting/og-image.jpg",
"applicationCategory": "DesignApplication",
"operatingSystem": "Any",
@@ -91,7 +91,7 @@
Fleeting Garden
- Draw colour into a canvas that keeps moving. Your strokes become paths for life that splits, drifts, and redraws the surface over time.
+ Tend it while you can. The garden returns to weather either way.
Start
@@ -139,19 +139,24 @@
>
-
- Draw into a field of particles and watch the simulation fold your marks back into motion.
+
+ A garden is what we tend; the wild is what we get the moment we look away.
+ Both happen here at once. Your strokes plant colour, small agents follow them,
+ branch off, and slowly rewrite the patch you laid down into something you
+ didn't quite plan.
- Choose one of three colour swatches, then drag to draw.
- Use the eraser to clear space and reshape the field.
- The mirror repeats each gesture across the canvas.
- The arrows switch the current atmosphere.
+ Three swatches plant the line; the eraser carves a clearing.
+ The mirror folds one gesture into many.
+ The arrows change the season.
-
- My implementation of physarum simulation introduces drawing and procedurally generated piano for a more immersive experience. Learn more about my work at schmelczer.dev
+
+ Built with WebGPU, running locally in your browser. More of my work at
+ schmelczer.dev .
diff --git a/src/audio/garden-audio-config.ts b/src/audio/garden-audio-config.ts
index 37e5ace..b5fbc1d 100644
--- a/src/audio/garden-audio-config.ts
+++ b/src/audio/garden-audio-config.ts
@@ -1,7 +1,6 @@
import type { PianoNoteRole } from './garden-audio-types';
-export const DEFAULT_AUDIO_VOLUME = 0.65;
-export const MAX_AUDIO_VOLUME = 1.5;
+export const DEFAULT_AUDIO_VOLUME = 0.5;
export const SILENT_AUDIO_GAIN = 0.0001;
type GardenAudioChordQuality = 'major' | 'minor' | 'sus2' | 'sus4';
@@ -59,33 +58,17 @@ export const createGardenAudioConfig = () => ({
timeRampSeconds: 0.12,
},
piano: {
- maxVoices: 48,
- gain: 0.78,
+ maxVoices: 24,
+ gain: 0.48,
sustainSeconds: 0.42,
sustainLevel: 0.26,
- releaseSeconds: 0.62,
- lowpassHz: 9500,
- gainAttackSeconds: 0.003,
- lowpassMaxHz: 16000,
- lowpassMinHz: 900,
+ releaseSeconds: 0.34,
+ lowpassHz: 7000,
+ gainAttackSeconds: 0.006,
+ lowpassMaxHz: 12000,
+ lowpassMinHz: 1400,
sustainBase: 0.45,
sustainVelocityRange: 0.55,
- releaseSampleGain: 0.035,
- releaseSampleVelocityBase: 0.45,
- releaseSampleVelocityRange: 0.55,
- roomSend: 0.18,
- velocityLayerCurve: 0.72,
- velocityLayerMax: 0.26,
- velocityLayerMin: 0.035,
- voiceDecayEstimateSeconds: 1.9,
- },
- room: {
- decaySeconds: 1.65,
- highPassHz: 120,
- lowPassHz: 8200,
- preDelaySeconds: 0.018,
- sendGain: 1,
- wetGain: 0.11,
},
rhythm: {
idleIntensity: defaultGardenAudioVibeSettings.idleIntensity,
diff --git a/src/audio/garden-audio-graph.ts b/src/audio/garden-audio-graph.ts
index a08ac58..a288465 100644
--- a/src/audio/garden-audio-graph.ts
+++ b/src/audio/garden-audio-graph.ts
@@ -28,11 +28,11 @@ const graphTuning = {
latencyHint: 'interactive',
outputFilterType: 'highpass',
compressor: {
- thresholdDb: -17,
+ thresholdDb: -18,
kneeDb: 18,
- ratio: 2.2,
- attackSeconds: 0.014,
- releaseSeconds: 0.28,
+ ratio: 2.1,
+ attackSeconds: 0.018,
+ releaseSeconds: 0.18,
},
} as const;
const delayFilterTuning = {
@@ -45,7 +45,6 @@ export class GardenAudioGraph {
public context: AudioContext | null = null;
public eventBus: GainNode | null = null;
public delayInput: GainNode | null = null;
- public roomInput: GainNode | null = null;
public noiseBus: GainNode | null = null;
public noiseBuffer: AudioBuffer | null = null;
@@ -88,12 +87,10 @@ export class GardenAudioGraph {
const context = new AudioContextConstructor({
latencyHint: graphTuning.latencyHint,
});
- const outputBus = context.createGain();
const masterGain = context.createGain();
const highPass = context.createBiquadFilter();
const compressor = context.createDynamicsCompressor();
- outputBus.gain.value = 1;
masterGain.gain.value = 0;
highPass.type = graphTuning.outputFilterType;
highPass.frequency.value = outputHighPassFrequencyHz;
@@ -103,18 +100,15 @@ export class GardenAudioGraph {
compressor.attack.value = graphTuning.compressor.attackSeconds;
compressor.release.value = graphTuning.compressor.releaseSeconds;
- // Keep peak control independent from the user's volume slider.
- outputBus.connect(highPass);
+ masterGain.connect(highPass);
highPass.connect(compressor);
- compressor.connect(masterGain);
- masterGain.connect(context.destination);
+ compressor.connect(context.destination);
this.context = context;
this.masterGain = masterGain;
this.noiseBuffer = this.createNoiseBuffer(context);
- this.createDelay(context, outputBus);
- this.createRoom(context, outputBus);
- this.createBuses(context, outputBus);
+ this.createDelay(context, masterGain);
+ this.createBuses(context, masterGain);
return context;
}
@@ -230,7 +224,7 @@ export class GardenAudioGraph {
}
}
- private createDelay(context: AudioContext, outputBus: GainNode): void {
+ private createDelay(context: AudioContext, masterGain: GainNode): void {
const delayInput = context.createGain();
const delayNode = context.createDelay(graphTuning.delayMaxSeconds);
const delayFeedback = context.createGain();
@@ -256,7 +250,7 @@ export class GardenAudioGraph {
delayFeedback.connect(delayNode);
delayNode.connect(returnLowPass);
returnLowPass.connect(delayOutput);
- delayOutput.connect(outputBus);
+ delayOutput.connect(masterGain);
this.delayInput = delayInput;
this.delayNode = delayNode;
@@ -264,37 +258,10 @@ export class GardenAudioGraph {
this.delayOutput = delayOutput;
}
- private createRoom(context: AudioContext, outputBus: GainNode): void {
- const roomInput = context.createGain();
- const preDelay = context.createDelay(0.08);
- const convolver = context.createConvolver();
- const highPass = context.createBiquadFilter();
- const lowPass = context.createBiquadFilter();
- const roomOutput = context.createGain();
-
- roomInput.gain.value = this.config.room.sendGain;
- preDelay.delayTime.value = this.config.room.preDelaySeconds;
- convolver.buffer = this.createRoomImpulse(context);
- highPass.type = 'highpass';
- highPass.frequency.value = this.config.room.highPassHz;
- lowPass.type = 'lowpass';
- lowPass.frequency.value = this.config.room.lowPassHz;
- roomOutput.gain.value = this.config.room.wetGain;
-
- roomInput.connect(preDelay);
- preDelay.connect(convolver);
- convolver.connect(highPass);
- highPass.connect(lowPass);
- lowPass.connect(roomOutput);
- roomOutput.connect(outputBus);
-
- this.roomInput = roomInput;
- }
-
- private createBuses(context: AudioContext, outputBus: GainNode): void {
+ private createBuses(context: AudioContext, masterGain: GainNode): void {
const eventBus = context.createGain();
eventBus.gain.value = graphTuning.eventBusGain;
- eventBus.connect(outputBus);
+ eventBus.connect(masterGain);
this.eventBus = eventBus;
this.pianoBuses.clear();
@@ -361,34 +328,10 @@ export class GardenAudioGraph {
return buffer;
}
- private createRoomImpulse(context: AudioContext): AudioBuffer {
- const sampleCount = Math.max(
- 1,
- Math.floor(context.sampleRate * this.config.room.decaySeconds)
- );
- const impulse = context.createBuffer(2, sampleCount, context.sampleRate);
-
- for (let channel = 0; channel < impulse.numberOfChannels; channel += 1) {
- const data = impulse.getChannelData(channel);
- for (let index = 0; index < sampleCount; index += 1) {
- const position = index / sampleCount;
- const decay = Math.pow(1 - position, 2.35);
- const earlyReflection =
- index % Math.max(1, Math.floor(context.sampleRate * 0.011)) === 0
- ? 0.18 * (1 - position)
- : 0;
- data[index] = (Math.random() * 2 - 1 + earlyReflection) * decay;
- }
- }
-
- return impulse;
- }
-
private clearNodes(): void {
this.context = null;
this.eventBus = null;
this.delayInput = null;
- this.roomInput = null;
this.noiseBus = null;
this.noiseBuffer = null;
this.masterGain = null;
diff --git a/src/audio/garden-audio-types.ts b/src/audio/garden-audio-types.ts
index ade78a4..fecbcf8 100644
--- a/src/audio/garden-audio-types.ts
+++ b/src/audio/garden-audio-types.ts
@@ -14,22 +14,11 @@ export interface GardenAudioStroke {
elapsedSeconds: number;
}
-export interface LoadedPianoStrikeSample {
- midi: number;
- velocityLayer: number;
- buffer: AudioBuffer;
-}
-
-export interface LoadedPianoReleaseSample {
+export interface LoadedPianoSample {
midi: number;
buffer: AudioBuffer;
}
-export interface LoadedPianoSamples {
- releases: Array;
- strikes: Array;
-}
-
export interface PianoNote {
midi: number;
velocity: number;
diff --git a/src/audio/garden-audio.ts b/src/audio/garden-audio.ts
index d2178cc..3aa4c88 100644
--- a/src/audio/garden-audio.ts
+++ b/src/audio/garden-audio.ts
@@ -1,8 +1,7 @@
import { ErrorHandler, Severity } from '../utils/error-handler';
-import { clamp } from '../utils/math';
+import { clamp01 } from '../utils/math';
import type { VibeId, VibePreset } from '../vibes';
import {
- MAX_AUDIO_VOLUME,
SILENT_AUDIO_GAIN,
type GardenAudioConfig,
type GardenAudioVibeProfile,
@@ -50,7 +49,7 @@ export class GardenAudio {
private hasLoadedPiano = false;
public constructor(private readonly config: GardenAudioConfig) {
- this.masterVolume = clamp(config.masterVolume, 0, MAX_AUDIO_VOLUME);
+ this.masterVolume = clamp01(config.masterVolume);
this.graph = new GardenAudioGraph(config);
this.piano = new PianoSampler(config, this.graph);
this.noise = new NoiseBurstPlayer(this.graph);
@@ -229,7 +228,7 @@ export class GardenAudio {
}
public setMasterVolume(masterVolume: number): void {
- this.masterVolume = clamp(masterVolume, 0, MAX_AUDIO_VOLUME);
+ this.masterVolume = clamp01(masterVolume);
if (!this.isMuted) {
this.graph.setMasterGain(this.masterVolume, this.config.updateRampSeconds);
}
@@ -397,7 +396,7 @@ export class GardenAudio {
return;
}
- const distanceActivity = clamp(activity, 0, 1);
+ const distanceActivity = clamp01(activity);
if (distanceActivity <= 0) {
return;
}
diff --git a/src/audio/piano-sampler.ts b/src/audio/piano-sampler.ts
index 5e0b771..1a0c77c 100644
--- a/src/audio/piano-sampler.ts
+++ b/src/audio/piano-sampler.ts
@@ -2,56 +2,32 @@ import { clamp, clamp01 } from '../utils/math';
import type { GardenAudioConfig } from './garden-audio-config';
import type { GardenAudioGraph } from './garden-audio-graph';
import { PITCH_SEMITONES_PER_OCTAVE } from './garden-audio-music';
-import type {
- LoadedPianoReleaseSample,
- LoadedPianoSamples,
- LoadedPianoStrikeSample,
- PianoNote,
-} from './garden-audio-types';
+import type { LoadedPianoSample, PianoNote } from './garden-audio-types';
import { getLoadedPianoSamples, loadPianoSamples } from './piano-samples';
export const PIANO_SCHEDULE_AHEAD_SECONDS = 0.002;
interface ActivePianoVoice {
gain: GainNode;
- peakGain: number;
- releaseAt: number;
- sources: Array;
- startedAt: number;
- stopAt: number;
-}
-
-interface SelectedPianoStrikeSample {
- gainScale: number;
- sample: LoadedPianoStrikeSample;
-}
-
-interface ScheduledReleaseSample {
- gainValue: number;
- source: AudioBufferSourceNode;
- startTime: number;
+ source: AudioScheduledSourceNode;
stopAt: number;
}
const pianoSamplerTuning = {
filterType: 'lowpass',
- filterQ: 0.45,
+ filterQ: 0.7,
minDurationSeconds: 0.08,
minFadeSeconds: 0.08,
minGain: 0.0001,
- releaseSampleAttackSeconds: 0.006,
- releaseSampleDecaySeconds: 0.18,
- releaseTimeConstantCount: 6,
- tailStopExtraSeconds: 0.08,
- voiceStealFadeSeconds: 0.045,
- voiceStealStopSeconds: 0.09,
+ releaseTimeConstantCount: 5,
+ tailStopExtraSeconds: 0.05,
+ voiceStealFadeSeconds: 0.025,
+ voiceStealStopSeconds: 0.05,
} as const;
export class PianoSampler {
+ private samples: Array = [];
private activeVoices: Array = [];
- private releaseSamples: Array = [];
- private strikeSamples: Array = [];
- private velocityLayers: Array = [];
public constructor(
private readonly config: GardenAudioConfig,
@@ -59,7 +35,7 @@ export class PianoSampler {
) {}
public load(context: BaseAudioContext): Promise {
- if (this.strikeSamples.length > 0) {
+ if (this.samples.length > 0) {
return Promise.resolve();
}
@@ -91,9 +67,8 @@ export class PianoSampler {
return;
}
- const noteVelocity = clamp01(velocity);
- const selectedSamples = this.selectStrikeSamples(midi, noteVelocity);
- if (selectedSamples.length === 0) {
+ const sample = this.findNearestSample(midi);
+ if (!sample) {
return;
}
@@ -101,6 +76,7 @@ export class PianoSampler {
context.currentTime + PIANO_SCHEDULE_AHEAD_SECONDS,
startTime
);
+ const noteVelocity = clamp01(velocity);
const noteGainValue = this.computeNoteGain(noteVelocity);
const sustainSeconds =
profileSustainSeconds *
@@ -112,36 +88,45 @@ export class PianoSampler {
const stopAt =
releaseAt +
this.config.piano.releaseSeconds * pianoSamplerTuning.releaseTimeConstantCount;
- const strikeSources = selectedSamples.map(({ gainScale, sample }) => ({
- gainScale,
- source: this.createSource(
- context,
- sample.buffer,
- midi,
- sample.midi,
- scheduledStart
- ),
- }));
- const releaseSample = this.createReleaseSample({
- context,
- midi,
- noteVelocity,
- releaseAt,
- });
+ const source = context.createBufferSource();
+
+ source.buffer = sample.buffer;
+ source.playbackRate.setValueAtTime(
+ Math.pow(2, (midi - sample.midi) / PITCH_SEMITONES_PER_OCTAVE),
+ scheduledStart
+ );
this.scheduleVoice({
+ source,
+ scheduledStart,
+ stopAt,
+ pan,
+ lowpassHz,
delaySend,
eventBus,
- lowpassHz,
- noteGainValue,
- pan,
- releaseAt,
- releaseSample,
- scheduledStart,
- stopAt: releaseSample ? Math.max(stopAt, releaseSample.stopAt) : stopAt,
- strikeSources,
- sustainAt,
- sustainSeconds,
+ configureGainEnvelope: (gain) => {
+ gain.gain.setValueAtTime(pianoSamplerTuning.minGain, scheduledStart);
+ gain.gain.exponentialRampToValueAtTime(
+ noteGainValue,
+ scheduledStart + this.config.piano.gainAttackSeconds
+ );
+ gain.gain.setTargetAtTime(
+ Math.max(
+ pianoSamplerTuning.minGain,
+ noteGainValue * this.config.piano.sustainLevel
+ ),
+ sustainAt,
+ Math.max(
+ pianoSamplerTuning.minFadeSeconds,
+ sustainSeconds * this.config.piano.sustainBase
+ )
+ );
+ gain.gain.setTargetAtTime(
+ pianoSamplerTuning.minGain,
+ releaseAt,
+ this.config.piano.releaseSeconds
+ );
+ },
});
}
@@ -161,40 +146,30 @@ export class PianoSampler {
}
public reset(): void {
- this.releaseSamples = [];
- this.strikeSamples = [];
- this.velocityLayers = [];
+ this.samples = [];
this.activeVoices = [];
}
private scheduleVoice({
- strikeSources,
- releaseSample,
+ source,
scheduledStart,
- sustainAt,
- sustainSeconds,
- releaseAt,
stopAt,
pan,
lowpassHz,
delaySend,
eventBus,
- noteGainValue,
+ configureGainEnvelope,
}: {
- delaySend: number;
- eventBus: GainNode;
- lowpassHz: number;
- noteGainValue: number;
- pan: number;
- releaseAt: number;
- releaseSample: ScheduledReleaseSample | null;
+ source: AudioScheduledSourceNode;
scheduledStart: number;
stopAt: number;
- strikeSources: Array<{ gainScale: number; source: AudioBufferSourceNode }>;
- sustainAt: number;
- sustainSeconds: number;
+ pan: number;
+ lowpassHz: number;
+ delaySend: number;
+ eventBus: GainNode;
+ configureGainEnvelope: (gain: GainNode) => void;
}): void {
- const { context, delayInput, roomInput } = this.graph;
+ const { context, delayInput } = this.graph;
if (!context) {
return;
}
@@ -202,18 +177,15 @@ export class PianoSampler {
const filter = context.createBiquadFilter();
const gain = context.createGain();
const panner = context.createStereoPanner();
- const sourceGains = strikeSources.map(({ gainScale }) => {
- const sourceGain = context.createGain();
- sourceGain.gain.value = gainScale;
- return sourceGain;
- });
- let delaySendGain: GainNode | null = null;
- let roomSendGain: GainNode | null = null;
- let releaseGain: GainNode | null = null;
+ let sendGain: GainNode | null = null;
this.trimActiveVoices(scheduledStart);
while (this.activeVoices.length >= this.config.piano.maxVoices) {
- this.stealQuietestVoice(scheduledStart);
+ const oldest = this.activeVoices.shift();
+ if (!oldest) {
+ break;
+ }
+ this.stopVoice(oldest, scheduledStart);
}
filter.type = pianoSamplerTuning.filterType;
@@ -223,352 +195,48 @@ export class PianoSampler {
);
filter.Q.value = pianoSamplerTuning.filterQ;
panner.pan.setValueAtTime(clamp(pan, -1, 1), scheduledStart);
- this.configureGainEnvelope({
- gain,
- noteGainValue,
- releaseAt,
- scheduledStart,
- sustainAt,
- sustainSeconds,
- });
+ configureGainEnvelope(gain);
- strikeSources.forEach(({ source }, index) => {
- source.connect(sourceGains[index]);
- sourceGains[index].connect(filter);
- });
+ source.connect(filter);
filter.connect(gain);
gain.connect(panner);
-
- if (releaseSample) {
- releaseGain = context.createGain();
- releaseSample.source.connect(releaseGain);
- releaseGain.connect(panner);
- this.configureReleaseEnvelope(releaseGain, releaseSample);
- }
-
panner.connect(eventBus);
if (delayInput && delaySend > 0) {
- delaySendGain = context.createGain();
- delaySendGain.gain.value = delaySend;
- panner.connect(delaySendGain);
- delaySendGain.connect(delayInput);
+ sendGain = context.createGain();
+ sendGain.gain.value = delaySend;
+ panner.connect(sendGain);
+ sendGain.connect(delayInput);
}
- if (roomInput && this.config.piano.roomSend > 0) {
- roomSendGain = context.createGain();
- roomSendGain.gain.value = this.config.piano.roomSend;
- panner.connect(roomSendGain);
- roomSendGain.connect(roomInput);
- }
+ source.start(scheduledStart);
+ source.stop(stopAt + pianoSamplerTuning.tailStopExtraSeconds);
+ this.activeVoices.push({ gain, source, stopAt });
- const sources = [
- ...strikeSources.map(({ source }) => source),
- ...(releaseSample ? [releaseSample.source] : []),
- ];
-
- strikeSources.forEach(({ source }) => {
- source.start(scheduledStart);
- source.stop(stopAt + pianoSamplerTuning.tailStopExtraSeconds);
- });
- if (releaseSample) {
- releaseSample.source.start(releaseSample.startTime);
- releaseSample.source.stop(releaseSample.stopAt);
- }
-
- const voice: ActivePianoVoice = {
- gain,
- peakGain: noteGainValue,
- releaseAt,
- sources,
- startedAt: scheduledStart,
- stopAt,
- };
- this.activeVoices.push(voice);
-
- this.cleanupVoiceWhenSourcesEnd({
- delaySendGain,
- filter,
- gain,
- panner,
- releaseGain,
- roomSendGain,
- sourceGains,
- sources,
- voice,
- });
- }
-
- private configureGainEnvelope({
- gain,
- noteGainValue,
- releaseAt,
- scheduledStart,
- sustainAt,
- sustainSeconds,
- }: {
- gain: GainNode;
- noteGainValue: number;
- releaseAt: number;
- scheduledStart: number;
- sustainAt: number;
- sustainSeconds: number;
- }): void {
- gain.gain.setValueAtTime(pianoSamplerTuning.minGain, scheduledStart);
- gain.gain.exponentialRampToValueAtTime(
- noteGainValue,
- scheduledStart + this.config.piano.gainAttackSeconds
- );
- gain.gain.setTargetAtTime(
- Math.max(
- pianoSamplerTuning.minGain,
- noteGainValue * this.config.piano.sustainLevel
- ),
- sustainAt,
- Math.max(
- pianoSamplerTuning.minFadeSeconds,
- sustainSeconds * this.config.piano.sustainBase
- )
- );
- gain.gain.setTargetAtTime(
- pianoSamplerTuning.minGain,
- releaseAt,
- this.config.piano.releaseSeconds
- );
- }
-
- private configureReleaseEnvelope(
- releaseGain: GainNode,
- releaseSample: ScheduledReleaseSample
- ): void {
- releaseGain.gain.setValueAtTime(pianoSamplerTuning.minGain, releaseSample.startTime);
- releaseGain.gain.exponentialRampToValueAtTime(
- releaseSample.gainValue,
- releaseSample.startTime + pianoSamplerTuning.releaseSampleAttackSeconds
- );
- releaseGain.gain.setTargetAtTime(
- pianoSamplerTuning.minGain,
- releaseSample.startTime + pianoSamplerTuning.releaseSampleAttackSeconds,
- pianoSamplerTuning.releaseSampleDecaySeconds
- );
- }
-
- private createSource(
- context: BaseAudioContext,
- buffer: AudioBuffer,
- midi: number,
- sampleMidi: number,
- scheduledStart: number
- ): AudioBufferSourceNode {
- const source = context.createBufferSource();
- source.buffer = buffer;
- source.playbackRate.setValueAtTime(
- Math.pow(2, (midi - sampleMidi) / PITCH_SEMITONES_PER_OCTAVE),
- scheduledStart
- );
- return source;
- }
-
- private createReleaseSample({
- context,
- midi,
- noteVelocity,
- releaseAt,
- }: {
- context: BaseAudioContext;
- midi: number;
- noteVelocity: number;
- releaseAt: number;
- }): ScheduledReleaseSample | null {
- const sample = this.findNearestReleaseSample(midi);
- if (!sample) {
- return null;
- }
-
- const source = this.createSource(
- context,
- sample.buffer,
- midi,
- sample.midi,
- releaseAt
- );
- const gainValue =
- this.config.piano.releaseSampleGain *
- (this.config.piano.releaseSampleVelocityBase +
- noteVelocity * this.config.piano.releaseSampleVelocityRange);
-
- return {
- gainValue: Math.max(pianoSamplerTuning.minGain, gainValue),
- source,
- startTime: releaseAt,
- stopAt:
- releaseAt + sample.buffer.duration + pianoSamplerTuning.tailStopExtraSeconds,
- };
- }
-
- private cleanupVoiceWhenSourcesEnd({
- sources,
- sourceGains,
- filter,
- gain,
- releaseGain,
- panner,
- delaySendGain,
- roomSendGain,
- voice,
- }: {
- delaySendGain: GainNode | null;
- filter: BiquadFilterNode;
- gain: GainNode;
- panner: StereoPannerNode;
- releaseGain: GainNode | null;
- roomSendGain: GainNode | null;
- sourceGains: Array;
- sources: Array;
- voice: ActivePianoVoice;
- }): void {
- let remainingSources = sources.length;
- const cleanup = (): void => {
- remainingSources -= 1;
- if (remainingSources > 0) {
- return;
- }
-
- sources.forEach((source) => {
+ source.addEventListener(
+ 'ended',
+ () => {
source.disconnect();
- });
- sourceGains.forEach((sourceGain) => {
- sourceGain.disconnect();
- });
- filter.disconnect();
- gain.disconnect();
- releaseGain?.disconnect();
- panner.disconnect();
- delaySendGain?.disconnect();
- roomSendGain?.disconnect();
- this.activeVoices = this.activeVoices.filter(
- (activeVoice) => activeVoice !== voice
- );
- };
-
- sources.forEach((source) => {
- source.addEventListener('ended', cleanup, { once: true });
- });
+ filter.disconnect();
+ gain.disconnect();
+ panner.disconnect();
+ sendGain?.disconnect();
+ this.activeVoices = this.activeVoices.filter((voice) => voice.gain !== gain);
+ },
+ { once: true }
+ );
}
private computeNoteGain(velocity: number): number {
return Math.max(pianoSamplerTuning.minGain, this.config.piano.gain * velocity);
}
- private selectStrikeSamples(
- midi: number,
- noteVelocity: number
- ): Array {
- if (this.strikeSamples.length === 0 || this.velocityLayers.length === 0) {
- return [];
- }
-
- const targetLayer = this.getTargetVelocityLayer(noteVelocity);
- const layerPair = this.getVelocityLayerPair(targetLayer);
- if (!layerPair) {
- return [];
- }
-
- const lowerSample = this.findNearestStrikeSample(midi, layerPair.lower);
- const upperSample =
- layerPair.upper === layerPair.lower
- ? null
- : this.findNearestStrikeSample(midi, layerPair.upper);
-
- if (!lowerSample && !upperSample) {
- return [];
- }
- if (!upperSample || layerPair.blend <= 0) {
- return lowerSample ? [{ gainScale: 1, sample: lowerSample }] : [];
- }
- if (!lowerSample || layerPair.blend >= 1) {
- return [{ gainScale: 1, sample: upperSample }];
- }
-
- return [
- {
- gainScale: Math.sqrt(1 - layerPair.blend),
- sample: lowerSample,
- },
- {
- gainScale: Math.sqrt(layerPair.blend),
- sample: upperSample,
- },
- ];
- }
-
- private getTargetVelocityLayer(noteVelocity: number): number {
- const firstLayer = this.velocityLayers[0];
- const lastLayer = this.velocityLayers[this.velocityLayers.length - 1];
- const velocityRange = Math.max(
- 0.001,
- this.config.piano.velocityLayerMax - this.config.piano.velocityLayerMin
- );
- const normalizedVelocity = clamp01(
- (noteVelocity - this.config.piano.velocityLayerMin) / velocityRange
- );
- const curvedVelocity = Math.pow(
- normalizedVelocity,
- this.config.piano.velocityLayerCurve
- );
-
- return firstLayer + (lastLayer - firstLayer) * curvedVelocity;
- }
-
- private getVelocityLayerPair(
- targetLayer: number
- ): { blend: number; lower: number; upper: number } | null {
- const firstLayer = this.velocityLayers[0];
- if (firstLayer === undefined) {
- return null;
- }
- if (targetLayer <= firstLayer) {
- return { blend: 0, lower: firstLayer, upper: firstLayer };
- }
-
- for (let index = 1; index < this.velocityLayers.length; index += 1) {
- const upper = this.velocityLayers[index];
- const lower = this.velocityLayers[index - 1];
- if (targetLayer <= upper) {
- return {
- blend: (targetLayer - lower) / (upper - lower),
- lower,
- upper,
- };
- }
- }
-
- const lastLayer = this.velocityLayers[this.velocityLayers.length - 1];
- return { blend: 0, lower: lastLayer, upper: lastLayer };
- }
-
- private findNearestStrikeSample(
- midi: number,
- velocityLayer: number
- ): LoadedPianoStrikeSample | null {
- const layerSamples = this.strikeSamples.filter(
- (sample) => sample.velocityLayer === velocityLayer
- );
- if (layerSamples.length === 0) {
+ private findNearestSample(midi: number): LoadedPianoSample | null {
+ if (this.samples.length === 0) {
return null;
}
- return layerSamples.reduce((nearest, sample) =>
- Math.abs(sample.midi - midi) < Math.abs(nearest.midi - midi) ? sample : nearest
- );
- }
-
- private findNearestReleaseSample(midi: number): LoadedPianoReleaseSample | null {
- if (this.releaseSamples.length === 0) {
- return null;
- }
-
- return this.releaseSamples.reduce((nearest, sample) =>
+ return this.samples.reduce((nearest, sample) =>
Math.abs(sample.midi - midi) < Math.abs(nearest.midi - midi) ? sample : nearest
);
}
@@ -577,33 +245,6 @@ export class PianoSampler {
this.activeVoices = this.activeVoices.filter((voice) => voice.stopAt > now);
}
- private stealQuietestVoice(now: number): void {
- const quietestVoice = this.activeVoices.reduce(
- (quietest, voice) =>
- quietest === null ||
- this.getVoiceActivityScore(voice, now) < this.getVoiceActivityScore(quietest, now)
- ? voice
- : quietest,
- null
- );
- if (!quietestVoice) {
- return;
- }
-
- this.stopVoice(quietestVoice, now);
- this.activeVoices = this.activeVoices.filter((voice) => voice !== quietestVoice);
- }
-
- private getVoiceActivityScore(voice: ActivePianoVoice, now: number): number {
- const ageSeconds = Math.max(0, now - voice.startedAt);
- const releasedScale = now >= voice.releaseAt ? 0.28 : 1;
- return (
- voice.peakGain *
- releasedScale *
- Math.exp(-ageSeconds / this.config.piano.voiceDecayEstimateSeconds)
- );
- }
-
private stopVoice(voice: ActivePianoVoice, now: number): void {
const stopAt = now + pianoSamplerTuning.voiceStealStopSeconds;
@@ -613,23 +254,11 @@ export class PianoSampler {
now,
pianoSamplerTuning.voiceStealFadeSeconds
);
- voice.sources.forEach((source) => {
- try {
- source.stop(stopAt);
- } catch {
- // The source may already have ended naturally.
- }
- });
voice.stopAt = stopAt;
+ voice.source.stop(stopAt);
}
- private setSamples(samples: LoadedPianoSamples): void {
- this.releaseSamples = samples.releases.slice().sort((a, b) => a.midi - b.midi);
- this.strikeSamples = samples.strikes
- .slice()
- .sort((a, b) => a.midi - b.midi || a.velocityLayer - b.velocityLayer);
- this.velocityLayers = [
- ...new Set(this.strikeSamples.map((sample) => sample.velocityLayer)),
- ].sort((a, b) => a - b);
+ private setSamples(samples: Array): void {
+ this.samples = samples.slice().sort((a, b) => a.midi - b.midi);
}
}
diff --git a/src/audio/piano-samples.ts b/src/audio/piano-samples.ts
index 2d2bc25..569eca4 100644
--- a/src/audio/piano-samples.ts
+++ b/src/audio/piano-samples.ts
@@ -1,26 +1,40 @@
-import type {
- LoadedPianoReleaseSample,
- LoadedPianoSamples,
- LoadedPianoStrikeSample,
-} from './garden-audio-types';
+import type { LoadedPianoSample } from './garden-audio-types';
+import a0SampleUrl from './samples/A0v12.m4a?url&no-inline';
+import a1SampleUrl from './samples/A1v12.m4a?url&no-inline';
+import a2SampleUrl from './samples/A2v12.m4a?url&no-inline';
+import a3SampleUrl from './samples/A3v12.m4a?url&no-inline';
+import a4SampleUrl from './samples/A4v12.m4a?url&no-inline';
+import a5SampleUrl from './samples/A5v12.m4a?url&no-inline';
+import a6SampleUrl from './samples/A6v12.m4a?url&no-inline';
+import a7SampleUrl from './samples/A7v12.m4a?url&no-inline';
+import c1SampleUrl from './samples/C1v12.m4a?url&no-inline';
+import c2SampleUrl from './samples/C2v12.m4a?url&no-inline';
+import c3SampleUrl from './samples/C3v12.m4a?url&no-inline';
+import c4SampleUrl from './samples/C4v12.m4a?url&no-inline';
+import c5SampleUrl from './samples/C5v12.m4a?url&no-inline';
+import c6SampleUrl from './samples/C6v12.m4a?url&no-inline';
+import c7SampleUrl from './samples/C7v12.m4a?url&no-inline';
+import c8SampleUrl from './samples/C8v12.m4a?url&no-inline';
+import dSharp1SampleUrl from './samples/Dsharp1v12.m4a?url&no-inline';
+import dSharp2SampleUrl from './samples/Dsharp2v12.m4a?url&no-inline';
+import dSharp3SampleUrl from './samples/Dsharp3v12.m4a?url&no-inline';
+import dSharp4SampleUrl from './samples/Dsharp4v12.m4a?url&no-inline';
+import dSharp5SampleUrl from './samples/Dsharp5v12.m4a?url&no-inline';
+import dSharp6SampleUrl from './samples/Dsharp6v12.m4a?url&no-inline';
+import dSharp7SampleUrl from './samples/Dsharp7v12.m4a?url&no-inline';
+import fSharp1SampleUrl from './samples/Fsharp1v12.m4a?url&no-inline';
+import fSharp2SampleUrl from './samples/Fsharp2v12.m4a?url&no-inline';
+import fSharp3SampleUrl from './samples/Fsharp3v12.m4a?url&no-inline';
+import fSharp4SampleUrl from './samples/Fsharp4v12.m4a?url&no-inline';
+import fSharp5SampleUrl from './samples/Fsharp5v12.m4a?url&no-inline';
+import fSharp6SampleUrl from './samples/Fsharp6v12.m4a?url&no-inline';
+import fSharp7SampleUrl from './samples/Fsharp7v12.m4a?url&no-inline';
-interface PianoStrikeSampleDefinition {
- kind: 'strike';
- midi: number;
- path: string;
- url: string;
- velocityLayer: number;
-}
-
-interface PianoReleaseSampleDefinition {
- kind: 'release';
- midi: number;
- path: string;
+interface PianoSampleDefinition {
+ note: string;
url: string;
}
-type PianoSampleDefinition = PianoStrikeSampleDefinition | PianoReleaseSampleDefinition;
-
export interface PianoSampleLoadProgress {
failedCount: number;
loadedCount: number;
@@ -28,28 +42,54 @@ export interface PianoSampleLoadProgress {
totalCount: number;
}
-const pianoSampleModules = import.meta.glob('./samples/*.m4a', {
- eager: true,
- import: 'default',
- query: '?url&no-inline',
-}) as Record;
-const pianoSampleDefinitions = getPianoSampleDefinitions(pianoSampleModules);
+const pianoSampleDefinitions: Array = [
+ { url: a0SampleUrl, note: 'A0' },
+ { url: c1SampleUrl, note: 'C1' },
+ { url: dSharp1SampleUrl, note: 'Dsharp1' },
+ { url: fSharp1SampleUrl, note: 'Fsharp1' },
+ { url: a1SampleUrl, note: 'A1' },
+ { url: c2SampleUrl, note: 'C2' },
+ { url: dSharp2SampleUrl, note: 'Dsharp2' },
+ { url: fSharp2SampleUrl, note: 'Fsharp2' },
+ { url: a2SampleUrl, note: 'A2' },
+ { url: c3SampleUrl, note: 'C3' },
+ { url: dSharp3SampleUrl, note: 'Dsharp3' },
+ { url: fSharp3SampleUrl, note: 'Fsharp3' },
+ { url: a3SampleUrl, note: 'A3' },
+ { url: c4SampleUrl, note: 'C4' },
+ { url: dSharp4SampleUrl, note: 'Dsharp4' },
+ { url: fSharp4SampleUrl, note: 'Fsharp4' },
+ { url: a4SampleUrl, note: 'A4' },
+ { url: c5SampleUrl, note: 'C5' },
+ { url: dSharp5SampleUrl, note: 'Dsharp5' },
+ { url: fSharp5SampleUrl, note: 'Fsharp5' },
+ { url: a5SampleUrl, note: 'A5' },
+ { url: c6SampleUrl, note: 'C6' },
+ { url: dSharp6SampleUrl, note: 'Dsharp6' },
+ { url: fSharp6SampleUrl, note: 'Fsharp6' },
+ { url: a6SampleUrl, note: 'A6' },
+ { url: c7SampleUrl, note: 'C7' },
+ { url: dSharp7SampleUrl, note: 'Dsharp7' },
+ { url: fSharp7SampleUrl, note: 'Fsharp7' },
+ { url: a7SampleUrl, note: 'A7' },
+ { url: c8SampleUrl, note: 'C8' },
+];
-let loadedPianoSamples: LoadedPianoSamples | null = null;
-let pianoSampleLoadPromise: Promise | null = null;
+let loadedPianoSamples: Array | null = null;
+let pianoSampleLoadPromise: Promise> | null = null;
let lastPianoSampleProgress: PianoSampleLoadProgress | null = null;
const pianoSampleProgressListeners = new Set<
(progress: PianoSampleLoadProgress) => void
>();
const sampleLoadTuning = {
- concurrency: 6,
+ concurrency: 4,
sampleTimeoutMs: 15_000,
};
export const preloadPianoSamples = (
onProgress?: (progress: PianoSampleLoadProgress) => void
-): Promise => {
+): Promise> => {
const OfflineAudioContextConstructor = globalThis.OfflineAudioContext;
if (!OfflineAudioContextConstructor) {
@@ -66,19 +106,18 @@ export const preloadPianoSamples = (
export const loadPianoSamples = (
decodeContext: BaseAudioContext,
onProgress?: (progress: PianoSampleLoadProgress) => void
-): Promise => {
+): Promise> => {
const unsubscribeProgress = subscribeToPianoSampleProgress(onProgress);
if (loadedPianoSamples) {
emitPianoSampleProgress({
failedCount: 0,
- loadedCount: loadedPianoSamples.strikes.length + loadedPianoSamples.releases.length,
- settledCount:
- loadedPianoSamples.strikes.length + loadedPianoSamples.releases.length,
+ loadedCount: loadedPianoSamples.length,
+ settledCount: loadedPianoSamples.length,
totalCount: pianoSampleDefinitions.length,
});
unsubscribeProgress();
- return Promise.resolve(cloneLoadedPianoSamples(loadedPianoSamples));
+ return Promise.resolve([...loadedPianoSamples]);
}
if (pianoSampleLoadPromise) {
@@ -112,15 +151,13 @@ export const loadPianoSamples = (
)
.then(
(samples) => {
- loadedPianoSamples = sortLoadedPianoSamples(samples);
- const loadedCount =
- loadedPianoSamples.strikes.length + loadedPianoSamples.releases.length;
- if (loadedCount !== pianoSampleDefinitions.length) {
+ loadedPianoSamples = samples.sort((a, b) => a.midi - b.midi);
+ if (loadedPianoSamples.length !== pianoSampleDefinitions.length) {
throw new Error(
- `Loaded ${loadedCount}/${pianoSampleDefinitions.length} piano samples.`
+ `Loaded ${loadedPianoSamples.length}/${pianoSampleDefinitions.length} piano samples.`
);
}
- return cloneLoadedPianoSamples(loadedPianoSamples);
+ return [...loadedPianoSamples];
},
(error: unknown) => {
pianoSampleLoadPromise = null;
@@ -133,38 +170,29 @@ export const loadPianoSamples = (
return pianoSampleLoadPromise;
};
-export const getLoadedPianoSamples = (): LoadedPianoSamples | null =>
- loadedPianoSamples ? cloneLoadedPianoSamples(loadedPianoSamples) : null;
+export const getLoadedPianoSamples = (): Array | null =>
+ loadedPianoSamples ? [...loadedPianoSamples] : null;
const loadPianoSample = async (
decodeContext: BaseAudioContext,
sample: PianoSampleDefinition,
signal: AbortSignal
-): Promise => {
+): Promise => {
const response = await fetch(sample.url, { signal });
if (!response.ok) {
- throw new Error(`Unable to load piano sample ${sample.path}`);
+ throw new Error(`Unable to load piano sample ${getPianoSamplePath(sample)}`);
}
const audioData = await response.arrayBuffer();
const buffer = await decodeContext.decodeAudioData(audioData);
- if (sample.kind === 'strike') {
- return {
- buffer,
- midi: sample.midi,
- velocityLayer: sample.velocityLayer,
- };
- }
- return { buffer, midi: sample.midi };
+ return { midi: getMidiForPianoSample(sample), buffer };
};
const loadPianoSampleBatch = async (
samples: Array,
- loadSample: (
- sample: PianoSampleDefinition
- ) => Promise
-): Promise> => {
- const results: Array = [];
+ loadSample: (sample: PianoSampleDefinition) => Promise
+): Promise> => {
+ const results: Array = [];
for (let index = 0; index < samples.length; index += sampleLoadTuning.concurrency) {
const batch = samples.slice(index, index + sampleLoadTuning.concurrency);
@@ -219,50 +247,13 @@ const emitPianoSampleProgress = (progress: PianoSampleLoadProgress): void => {
pianoSampleProgressListeners.forEach((listener) => listener(progress));
};
-function getPianoSampleDefinitions(
- modules: Record
-): Array {
- return Object.entries(modules)
- .map(([path, url]) => getPianoSampleDefinition(path, url))
- .sort((a, b) => a.midi - b.midi || getSampleSortValue(a) - getSampleSortValue(b));
-}
+const getPianoSamplePath = (sample: PianoSampleDefinition): string =>
+ `./samples/${sample.note}v12.m4a`;
-function getPianoSampleDefinition(path: string, url: string): PianoSampleDefinition {
- const filename = path.split('/').pop() ?? path;
- const strikeMatch = /^(?[A-G](?:sharp)?\d+)v(?\d+)\.m4a$/.exec(
- filename
- );
- if (strikeMatch?.groups) {
- return {
- kind: 'strike',
- midi: getMidiForPianoSampleNote(strikeMatch.groups.note),
- path,
- url,
- velocityLayer: Number(strikeMatch.groups.velocityLayer),
- };
- }
-
- const releaseMatch = /^rel(?\d+)\.m4a$/.exec(filename);
- if (releaseMatch?.groups) {
- return {
- kind: 'release',
- midi: getMidiForReleaseSample(Number(releaseMatch.groups.releaseIndex)),
- path,
- url,
- };
- }
-
- throw new Error(`Invalid piano sample filename ${path}`);
-}
-
-function getSampleSortValue(sample: PianoSampleDefinition): number {
- return sample.kind === 'strike' ? sample.velocityLayer : Number.MAX_SAFE_INTEGER;
-}
-
-function getMidiForPianoSampleNote(note: string): number {
- const match = /^(?[A-G])(?sharp)?(?\d+)$/.exec(note);
+const getMidiForPianoSample = (sample: PianoSampleDefinition): number => {
+ const match = /^(?[A-G])(?sharp)?(?\d+)$/.exec(sample.note);
if (!match?.groups) {
- throw new Error(`Invalid piano sample note ${note}`);
+ throw new Error(`Invalid piano sample note ${sample.note}`);
}
const semitoneByName: Record = {
@@ -277,25 +268,4 @@ function getMidiForPianoSampleNote(note: string): number {
const octave = Number(match.groups.octave);
const semitone = semitoneByName[match.groups.name] + (match.groups.accidental ? 1 : 0);
return (octave + 1) * 12 + semitone;
-}
-
-function getMidiForReleaseSample(releaseIndex: number): number {
- const pianoLowestMidi = 21;
- return pianoLowestMidi + releaseIndex - 1;
-}
-
-const sortLoadedPianoSamples = (
- samples: Array
-): LoadedPianoSamples => ({
- releases: samples
- .filter((sample): sample is LoadedPianoReleaseSample => !('velocityLayer' in sample))
- .sort((a, b) => a.midi - b.midi),
- strikes: samples
- .filter((sample): sample is LoadedPianoStrikeSample => 'velocityLayer' in sample)
- .sort((a, b) => a.midi - b.midi || a.velocityLayer - b.velocityLayer),
-});
-
-const cloneLoadedPianoSamples = (samples: LoadedPianoSamples): LoadedPianoSamples => ({
- releases: [...samples.releases],
- strikes: [...samples.strikes],
-});
+};
diff --git a/src/audio/samples/A0v12.m4a b/src/audio/samples/A0v12.m4a
index 71c0564..db06fc3 100644
Binary files a/src/audio/samples/A0v12.m4a and b/src/audio/samples/A0v12.m4a differ
diff --git a/src/audio/samples/A0v16.m4a b/src/audio/samples/A0v16.m4a
deleted file mode 100644
index aae9230..0000000
Binary files a/src/audio/samples/A0v16.m4a and /dev/null differ
diff --git a/src/audio/samples/A0v4.m4a b/src/audio/samples/A0v4.m4a
deleted file mode 100644
index 0679ac0..0000000
Binary files a/src/audio/samples/A0v4.m4a and /dev/null differ
diff --git a/src/audio/samples/A0v8.m4a b/src/audio/samples/A0v8.m4a
deleted file mode 100644
index 99195dd..0000000
Binary files a/src/audio/samples/A0v8.m4a and /dev/null differ
diff --git a/src/audio/samples/A1v12.m4a b/src/audio/samples/A1v12.m4a
index 4d47370..f1ed488 100644
Binary files a/src/audio/samples/A1v12.m4a and b/src/audio/samples/A1v12.m4a differ
diff --git a/src/audio/samples/A1v16.m4a b/src/audio/samples/A1v16.m4a
deleted file mode 100644
index a85872d..0000000
Binary files a/src/audio/samples/A1v16.m4a and /dev/null differ
diff --git a/src/audio/samples/A1v4.m4a b/src/audio/samples/A1v4.m4a
deleted file mode 100644
index 8095af9..0000000
Binary files a/src/audio/samples/A1v4.m4a and /dev/null differ
diff --git a/src/audio/samples/A1v8.m4a b/src/audio/samples/A1v8.m4a
deleted file mode 100644
index de6d334..0000000
Binary files a/src/audio/samples/A1v8.m4a and /dev/null differ
diff --git a/src/audio/samples/A2v12.m4a b/src/audio/samples/A2v12.m4a
index 2ac10d3..52df725 100644
Binary files a/src/audio/samples/A2v12.m4a and b/src/audio/samples/A2v12.m4a differ
diff --git a/src/audio/samples/A2v16.m4a b/src/audio/samples/A2v16.m4a
deleted file mode 100644
index 8aa478c..0000000
Binary files a/src/audio/samples/A2v16.m4a and /dev/null differ
diff --git a/src/audio/samples/A2v4.m4a b/src/audio/samples/A2v4.m4a
deleted file mode 100644
index f241c2e..0000000
Binary files a/src/audio/samples/A2v4.m4a and /dev/null differ
diff --git a/src/audio/samples/A2v8.m4a b/src/audio/samples/A2v8.m4a
deleted file mode 100644
index ceaca7f..0000000
Binary files a/src/audio/samples/A2v8.m4a and /dev/null differ
diff --git a/src/audio/samples/A3v12.m4a b/src/audio/samples/A3v12.m4a
index 10cc72c..707a766 100644
Binary files a/src/audio/samples/A3v12.m4a and b/src/audio/samples/A3v12.m4a differ
diff --git a/src/audio/samples/A3v16.m4a b/src/audio/samples/A3v16.m4a
deleted file mode 100644
index a100c90..0000000
Binary files a/src/audio/samples/A3v16.m4a and /dev/null differ
diff --git a/src/audio/samples/A3v4.m4a b/src/audio/samples/A3v4.m4a
deleted file mode 100644
index 0eae496..0000000
Binary files a/src/audio/samples/A3v4.m4a and /dev/null differ
diff --git a/src/audio/samples/A3v8.m4a b/src/audio/samples/A3v8.m4a
deleted file mode 100644
index 2158608..0000000
Binary files a/src/audio/samples/A3v8.m4a and /dev/null differ
diff --git a/src/audio/samples/A4v12.m4a b/src/audio/samples/A4v12.m4a
index 7fe52f4..679bcff 100644
Binary files a/src/audio/samples/A4v12.m4a and b/src/audio/samples/A4v12.m4a differ
diff --git a/src/audio/samples/A4v16.m4a b/src/audio/samples/A4v16.m4a
deleted file mode 100644
index 4d20781..0000000
Binary files a/src/audio/samples/A4v16.m4a and /dev/null differ
diff --git a/src/audio/samples/A4v4.m4a b/src/audio/samples/A4v4.m4a
deleted file mode 100644
index 4e55c94..0000000
Binary files a/src/audio/samples/A4v4.m4a and /dev/null differ
diff --git a/src/audio/samples/A4v8.m4a b/src/audio/samples/A4v8.m4a
deleted file mode 100644
index 335fa93..0000000
Binary files a/src/audio/samples/A4v8.m4a and /dev/null differ
diff --git a/src/audio/samples/A5v12.m4a b/src/audio/samples/A5v12.m4a
index 9d3d280..4a2c896 100644
Binary files a/src/audio/samples/A5v12.m4a and b/src/audio/samples/A5v12.m4a differ
diff --git a/src/audio/samples/A5v16.m4a b/src/audio/samples/A5v16.m4a
deleted file mode 100644
index cd7c62f..0000000
Binary files a/src/audio/samples/A5v16.m4a and /dev/null differ
diff --git a/src/audio/samples/A5v4.m4a b/src/audio/samples/A5v4.m4a
deleted file mode 100644
index c68176e..0000000
Binary files a/src/audio/samples/A5v4.m4a and /dev/null differ
diff --git a/src/audio/samples/A5v8.m4a b/src/audio/samples/A5v8.m4a
deleted file mode 100644
index 0afb0b9..0000000
Binary files a/src/audio/samples/A5v8.m4a and /dev/null differ
diff --git a/src/audio/samples/A6v12.m4a b/src/audio/samples/A6v12.m4a
index 9b8644d..abbd605 100644
Binary files a/src/audio/samples/A6v12.m4a and b/src/audio/samples/A6v12.m4a differ
diff --git a/src/audio/samples/A6v16.m4a b/src/audio/samples/A6v16.m4a
deleted file mode 100644
index 26bcc55..0000000
Binary files a/src/audio/samples/A6v16.m4a and /dev/null differ
diff --git a/src/audio/samples/A6v4.m4a b/src/audio/samples/A6v4.m4a
deleted file mode 100644
index 6d37b98..0000000
Binary files a/src/audio/samples/A6v4.m4a and /dev/null differ
diff --git a/src/audio/samples/A6v8.m4a b/src/audio/samples/A6v8.m4a
deleted file mode 100644
index bd89797..0000000
Binary files a/src/audio/samples/A6v8.m4a and /dev/null differ
diff --git a/src/audio/samples/A7v12.m4a b/src/audio/samples/A7v12.m4a
index 2a4193b..3fd6829 100644
Binary files a/src/audio/samples/A7v12.m4a and b/src/audio/samples/A7v12.m4a differ
diff --git a/src/audio/samples/A7v16.m4a b/src/audio/samples/A7v16.m4a
deleted file mode 100644
index c0f0e52..0000000
Binary files a/src/audio/samples/A7v16.m4a and /dev/null differ
diff --git a/src/audio/samples/A7v4.m4a b/src/audio/samples/A7v4.m4a
deleted file mode 100644
index 2106892..0000000
Binary files a/src/audio/samples/A7v4.m4a and /dev/null differ
diff --git a/src/audio/samples/A7v8.m4a b/src/audio/samples/A7v8.m4a
deleted file mode 100644
index a25a530..0000000
Binary files a/src/audio/samples/A7v8.m4a and /dev/null differ
diff --git a/src/audio/samples/C1v12.m4a b/src/audio/samples/C1v12.m4a
index d94fd2e..59d5f61 100644
Binary files a/src/audio/samples/C1v12.m4a and b/src/audio/samples/C1v12.m4a differ
diff --git a/src/audio/samples/C1v16.m4a b/src/audio/samples/C1v16.m4a
deleted file mode 100644
index f6c995d..0000000
Binary files a/src/audio/samples/C1v16.m4a and /dev/null differ
diff --git a/src/audio/samples/C1v4.m4a b/src/audio/samples/C1v4.m4a
deleted file mode 100644
index 5c2d043..0000000
Binary files a/src/audio/samples/C1v4.m4a and /dev/null differ
diff --git a/src/audio/samples/C1v8.m4a b/src/audio/samples/C1v8.m4a
deleted file mode 100644
index febaf24..0000000
Binary files a/src/audio/samples/C1v8.m4a and /dev/null differ
diff --git a/src/audio/samples/C2v12.m4a b/src/audio/samples/C2v12.m4a
index 44bda1a..9b636f9 100644
Binary files a/src/audio/samples/C2v12.m4a and b/src/audio/samples/C2v12.m4a differ
diff --git a/src/audio/samples/C2v16.m4a b/src/audio/samples/C2v16.m4a
deleted file mode 100644
index bb729d8..0000000
Binary files a/src/audio/samples/C2v16.m4a and /dev/null differ
diff --git a/src/audio/samples/C2v4.m4a b/src/audio/samples/C2v4.m4a
deleted file mode 100644
index 9ce27c9..0000000
Binary files a/src/audio/samples/C2v4.m4a and /dev/null differ
diff --git a/src/audio/samples/C2v8.m4a b/src/audio/samples/C2v8.m4a
deleted file mode 100644
index 3cd4b1c..0000000
Binary files a/src/audio/samples/C2v8.m4a and /dev/null differ
diff --git a/src/audio/samples/C3v12.m4a b/src/audio/samples/C3v12.m4a
index a7c9af0..e891e16 100644
Binary files a/src/audio/samples/C3v12.m4a and b/src/audio/samples/C3v12.m4a differ
diff --git a/src/audio/samples/C3v16.m4a b/src/audio/samples/C3v16.m4a
deleted file mode 100644
index d6ad787..0000000
Binary files a/src/audio/samples/C3v16.m4a and /dev/null differ
diff --git a/src/audio/samples/C3v4.m4a b/src/audio/samples/C3v4.m4a
deleted file mode 100644
index e850712..0000000
Binary files a/src/audio/samples/C3v4.m4a and /dev/null differ
diff --git a/src/audio/samples/C3v8.m4a b/src/audio/samples/C3v8.m4a
deleted file mode 100644
index e781b6b..0000000
Binary files a/src/audio/samples/C3v8.m4a and /dev/null differ
diff --git a/src/audio/samples/C4v12.m4a b/src/audio/samples/C4v12.m4a
index fb37963..6061dc5 100644
Binary files a/src/audio/samples/C4v12.m4a and b/src/audio/samples/C4v12.m4a differ
diff --git a/src/audio/samples/C4v16.m4a b/src/audio/samples/C4v16.m4a
deleted file mode 100644
index eed90fa..0000000
Binary files a/src/audio/samples/C4v16.m4a and /dev/null differ
diff --git a/src/audio/samples/C4v4.m4a b/src/audio/samples/C4v4.m4a
deleted file mode 100644
index 7f1994f..0000000
Binary files a/src/audio/samples/C4v4.m4a and /dev/null differ
diff --git a/src/audio/samples/C4v8.m4a b/src/audio/samples/C4v8.m4a
deleted file mode 100644
index 9a45ff8..0000000
Binary files a/src/audio/samples/C4v8.m4a and /dev/null differ
diff --git a/src/audio/samples/C5v12.m4a b/src/audio/samples/C5v12.m4a
index 3c08107..a6d8898 100644
Binary files a/src/audio/samples/C5v12.m4a and b/src/audio/samples/C5v12.m4a differ
diff --git a/src/audio/samples/C5v16.m4a b/src/audio/samples/C5v16.m4a
deleted file mode 100644
index 24edb5a..0000000
Binary files a/src/audio/samples/C5v16.m4a and /dev/null differ
diff --git a/src/audio/samples/C5v4.m4a b/src/audio/samples/C5v4.m4a
deleted file mode 100644
index 130ea2e..0000000
Binary files a/src/audio/samples/C5v4.m4a and /dev/null differ
diff --git a/src/audio/samples/C5v8.m4a b/src/audio/samples/C5v8.m4a
deleted file mode 100644
index 403b22d..0000000
Binary files a/src/audio/samples/C5v8.m4a and /dev/null differ
diff --git a/src/audio/samples/C6v12.m4a b/src/audio/samples/C6v12.m4a
index d8268ff..745a4d6 100644
Binary files a/src/audio/samples/C6v12.m4a and b/src/audio/samples/C6v12.m4a differ
diff --git a/src/audio/samples/C6v16.m4a b/src/audio/samples/C6v16.m4a
deleted file mode 100644
index 8305844..0000000
Binary files a/src/audio/samples/C6v16.m4a and /dev/null differ
diff --git a/src/audio/samples/C6v4.m4a b/src/audio/samples/C6v4.m4a
deleted file mode 100644
index 8646b5b..0000000
Binary files a/src/audio/samples/C6v4.m4a and /dev/null differ
diff --git a/src/audio/samples/C6v8.m4a b/src/audio/samples/C6v8.m4a
deleted file mode 100644
index f85e6a1..0000000
Binary files a/src/audio/samples/C6v8.m4a and /dev/null differ
diff --git a/src/audio/samples/C7v12.m4a b/src/audio/samples/C7v12.m4a
index d3bcee5..6470854 100644
Binary files a/src/audio/samples/C7v12.m4a and b/src/audio/samples/C7v12.m4a differ
diff --git a/src/audio/samples/C7v16.m4a b/src/audio/samples/C7v16.m4a
deleted file mode 100644
index 0211360..0000000
Binary files a/src/audio/samples/C7v16.m4a and /dev/null differ
diff --git a/src/audio/samples/C7v4.m4a b/src/audio/samples/C7v4.m4a
deleted file mode 100644
index a2e2d2d..0000000
Binary files a/src/audio/samples/C7v4.m4a and /dev/null differ
diff --git a/src/audio/samples/C7v8.m4a b/src/audio/samples/C7v8.m4a
deleted file mode 100644
index 6113888..0000000
Binary files a/src/audio/samples/C7v8.m4a and /dev/null differ
diff --git a/src/audio/samples/C8v12.m4a b/src/audio/samples/C8v12.m4a
index 584f5ff..dfbbfd1 100644
Binary files a/src/audio/samples/C8v12.m4a and b/src/audio/samples/C8v12.m4a differ
diff --git a/src/audio/samples/C8v16.m4a b/src/audio/samples/C8v16.m4a
deleted file mode 100644
index b8d2aa8..0000000
Binary files a/src/audio/samples/C8v16.m4a and /dev/null differ
diff --git a/src/audio/samples/C8v4.m4a b/src/audio/samples/C8v4.m4a
deleted file mode 100644
index ff91df5..0000000
Binary files a/src/audio/samples/C8v4.m4a and /dev/null differ
diff --git a/src/audio/samples/C8v8.m4a b/src/audio/samples/C8v8.m4a
deleted file mode 100644
index 32e8a04..0000000
Binary files a/src/audio/samples/C8v8.m4a and /dev/null differ
diff --git a/src/audio/samples/Dsharp1v12.m4a b/src/audio/samples/Dsharp1v12.m4a
index 0f7d897..22d0924 100644
Binary files a/src/audio/samples/Dsharp1v12.m4a and b/src/audio/samples/Dsharp1v12.m4a differ
diff --git a/src/audio/samples/Dsharp1v16.m4a b/src/audio/samples/Dsharp1v16.m4a
deleted file mode 100644
index fe422e2..0000000
Binary files a/src/audio/samples/Dsharp1v16.m4a and /dev/null differ
diff --git a/src/audio/samples/Dsharp1v4.m4a b/src/audio/samples/Dsharp1v4.m4a
deleted file mode 100644
index 03e7208..0000000
Binary files a/src/audio/samples/Dsharp1v4.m4a and /dev/null differ
diff --git a/src/audio/samples/Dsharp1v8.m4a b/src/audio/samples/Dsharp1v8.m4a
deleted file mode 100644
index b5de786..0000000
Binary files a/src/audio/samples/Dsharp1v8.m4a and /dev/null differ
diff --git a/src/audio/samples/Dsharp2v12.m4a b/src/audio/samples/Dsharp2v12.m4a
index a2392da..f25db22 100644
Binary files a/src/audio/samples/Dsharp2v12.m4a and b/src/audio/samples/Dsharp2v12.m4a differ
diff --git a/src/audio/samples/Dsharp2v16.m4a b/src/audio/samples/Dsharp2v16.m4a
deleted file mode 100644
index 264716e..0000000
Binary files a/src/audio/samples/Dsharp2v16.m4a and /dev/null differ
diff --git a/src/audio/samples/Dsharp2v4.m4a b/src/audio/samples/Dsharp2v4.m4a
deleted file mode 100644
index 6984fbc..0000000
Binary files a/src/audio/samples/Dsharp2v4.m4a and /dev/null differ
diff --git a/src/audio/samples/Dsharp2v8.m4a b/src/audio/samples/Dsharp2v8.m4a
deleted file mode 100644
index 0461578..0000000
Binary files a/src/audio/samples/Dsharp2v8.m4a and /dev/null differ
diff --git a/src/audio/samples/Dsharp3v12.m4a b/src/audio/samples/Dsharp3v12.m4a
index f4d7398..7e09558 100644
Binary files a/src/audio/samples/Dsharp3v12.m4a and b/src/audio/samples/Dsharp3v12.m4a differ
diff --git a/src/audio/samples/Dsharp3v16.m4a b/src/audio/samples/Dsharp3v16.m4a
deleted file mode 100644
index 3a48ed5..0000000
Binary files a/src/audio/samples/Dsharp3v16.m4a and /dev/null differ
diff --git a/src/audio/samples/Dsharp3v4.m4a b/src/audio/samples/Dsharp3v4.m4a
deleted file mode 100644
index a3d205d..0000000
Binary files a/src/audio/samples/Dsharp3v4.m4a and /dev/null differ
diff --git a/src/audio/samples/Dsharp3v8.m4a b/src/audio/samples/Dsharp3v8.m4a
deleted file mode 100644
index 17b16e1..0000000
Binary files a/src/audio/samples/Dsharp3v8.m4a and /dev/null differ
diff --git a/src/audio/samples/Dsharp4v12.m4a b/src/audio/samples/Dsharp4v12.m4a
index 42fbf3a..d670fbb 100644
Binary files a/src/audio/samples/Dsharp4v12.m4a and b/src/audio/samples/Dsharp4v12.m4a differ
diff --git a/src/audio/samples/Dsharp4v16.m4a b/src/audio/samples/Dsharp4v16.m4a
deleted file mode 100644
index 2c2cd57..0000000
Binary files a/src/audio/samples/Dsharp4v16.m4a and /dev/null differ
diff --git a/src/audio/samples/Dsharp4v4.m4a b/src/audio/samples/Dsharp4v4.m4a
deleted file mode 100644
index 9591c46..0000000
Binary files a/src/audio/samples/Dsharp4v4.m4a and /dev/null differ
diff --git a/src/audio/samples/Dsharp4v8.m4a b/src/audio/samples/Dsharp4v8.m4a
deleted file mode 100644
index 734a779..0000000
Binary files a/src/audio/samples/Dsharp4v8.m4a and /dev/null differ
diff --git a/src/audio/samples/Dsharp5v12.m4a b/src/audio/samples/Dsharp5v12.m4a
index 9083dd0..cdbd7b8 100644
Binary files a/src/audio/samples/Dsharp5v12.m4a and b/src/audio/samples/Dsharp5v12.m4a differ
diff --git a/src/audio/samples/Dsharp5v16.m4a b/src/audio/samples/Dsharp5v16.m4a
deleted file mode 100644
index 33bfab4..0000000
Binary files a/src/audio/samples/Dsharp5v16.m4a and /dev/null differ
diff --git a/src/audio/samples/Dsharp5v4.m4a b/src/audio/samples/Dsharp5v4.m4a
deleted file mode 100644
index fa6c930..0000000
Binary files a/src/audio/samples/Dsharp5v4.m4a and /dev/null differ
diff --git a/src/audio/samples/Dsharp5v8.m4a b/src/audio/samples/Dsharp5v8.m4a
deleted file mode 100644
index 21dce22..0000000
Binary files a/src/audio/samples/Dsharp5v8.m4a and /dev/null differ
diff --git a/src/audio/samples/Dsharp6v12.m4a b/src/audio/samples/Dsharp6v12.m4a
index aaf59ee..b5ff787 100644
Binary files a/src/audio/samples/Dsharp6v12.m4a and b/src/audio/samples/Dsharp6v12.m4a differ
diff --git a/src/audio/samples/Dsharp6v16.m4a b/src/audio/samples/Dsharp6v16.m4a
deleted file mode 100644
index 58762f4..0000000
Binary files a/src/audio/samples/Dsharp6v16.m4a and /dev/null differ
diff --git a/src/audio/samples/Dsharp6v4.m4a b/src/audio/samples/Dsharp6v4.m4a
deleted file mode 100644
index b617ba3..0000000
Binary files a/src/audio/samples/Dsharp6v4.m4a and /dev/null differ
diff --git a/src/audio/samples/Dsharp6v8.m4a b/src/audio/samples/Dsharp6v8.m4a
deleted file mode 100644
index a7704ea..0000000
Binary files a/src/audio/samples/Dsharp6v8.m4a and /dev/null differ
diff --git a/src/audio/samples/Dsharp7v12.m4a b/src/audio/samples/Dsharp7v12.m4a
index 950034f..a9b6cda 100644
Binary files a/src/audio/samples/Dsharp7v12.m4a and b/src/audio/samples/Dsharp7v12.m4a differ
diff --git a/src/audio/samples/Dsharp7v16.m4a b/src/audio/samples/Dsharp7v16.m4a
deleted file mode 100644
index 81667cb..0000000
Binary files a/src/audio/samples/Dsharp7v16.m4a and /dev/null differ
diff --git a/src/audio/samples/Dsharp7v4.m4a b/src/audio/samples/Dsharp7v4.m4a
deleted file mode 100644
index d49a972..0000000
Binary files a/src/audio/samples/Dsharp7v4.m4a and /dev/null differ
diff --git a/src/audio/samples/Dsharp7v8.m4a b/src/audio/samples/Dsharp7v8.m4a
deleted file mode 100644
index c57ebae..0000000
Binary files a/src/audio/samples/Dsharp7v8.m4a and /dev/null differ
diff --git a/src/audio/samples/Fsharp1v12.m4a b/src/audio/samples/Fsharp1v12.m4a
index 3b941c8..752590f 100644
Binary files a/src/audio/samples/Fsharp1v12.m4a and b/src/audio/samples/Fsharp1v12.m4a differ
diff --git a/src/audio/samples/Fsharp1v16.m4a b/src/audio/samples/Fsharp1v16.m4a
deleted file mode 100644
index 71c269b..0000000
Binary files a/src/audio/samples/Fsharp1v16.m4a and /dev/null differ
diff --git a/src/audio/samples/Fsharp1v4.m4a b/src/audio/samples/Fsharp1v4.m4a
deleted file mode 100644
index c12ccb1..0000000
Binary files a/src/audio/samples/Fsharp1v4.m4a and /dev/null differ
diff --git a/src/audio/samples/Fsharp1v8.m4a b/src/audio/samples/Fsharp1v8.m4a
deleted file mode 100644
index 799e5ee..0000000
Binary files a/src/audio/samples/Fsharp1v8.m4a and /dev/null differ
diff --git a/src/audio/samples/Fsharp2v12.m4a b/src/audio/samples/Fsharp2v12.m4a
index 2779ee3..3477cb8 100644
Binary files a/src/audio/samples/Fsharp2v12.m4a and b/src/audio/samples/Fsharp2v12.m4a differ
diff --git a/src/audio/samples/Fsharp2v16.m4a b/src/audio/samples/Fsharp2v16.m4a
deleted file mode 100644
index 219ab97..0000000
Binary files a/src/audio/samples/Fsharp2v16.m4a and /dev/null differ
diff --git a/src/audio/samples/Fsharp2v4.m4a b/src/audio/samples/Fsharp2v4.m4a
deleted file mode 100644
index ca98511..0000000
Binary files a/src/audio/samples/Fsharp2v4.m4a and /dev/null differ
diff --git a/src/audio/samples/Fsharp2v8.m4a b/src/audio/samples/Fsharp2v8.m4a
deleted file mode 100644
index e3a9480..0000000
Binary files a/src/audio/samples/Fsharp2v8.m4a and /dev/null differ
diff --git a/src/audio/samples/Fsharp3v12.m4a b/src/audio/samples/Fsharp3v12.m4a
index 85424f5..d36f8dd 100644
Binary files a/src/audio/samples/Fsharp3v12.m4a and b/src/audio/samples/Fsharp3v12.m4a differ
diff --git a/src/audio/samples/Fsharp3v16.m4a b/src/audio/samples/Fsharp3v16.m4a
deleted file mode 100644
index 939721f..0000000
Binary files a/src/audio/samples/Fsharp3v16.m4a and /dev/null differ
diff --git a/src/audio/samples/Fsharp3v4.m4a b/src/audio/samples/Fsharp3v4.m4a
deleted file mode 100644
index d64957d..0000000
Binary files a/src/audio/samples/Fsharp3v4.m4a and /dev/null differ
diff --git a/src/audio/samples/Fsharp3v8.m4a b/src/audio/samples/Fsharp3v8.m4a
deleted file mode 100644
index 5dd56f5..0000000
Binary files a/src/audio/samples/Fsharp3v8.m4a and /dev/null differ
diff --git a/src/audio/samples/Fsharp4v12.m4a b/src/audio/samples/Fsharp4v12.m4a
index 81309f1..21df1e2 100644
Binary files a/src/audio/samples/Fsharp4v12.m4a and b/src/audio/samples/Fsharp4v12.m4a differ
diff --git a/src/audio/samples/Fsharp4v16.m4a b/src/audio/samples/Fsharp4v16.m4a
deleted file mode 100644
index 2b60946..0000000
Binary files a/src/audio/samples/Fsharp4v16.m4a and /dev/null differ
diff --git a/src/audio/samples/Fsharp4v4.m4a b/src/audio/samples/Fsharp4v4.m4a
deleted file mode 100644
index 553e183..0000000
Binary files a/src/audio/samples/Fsharp4v4.m4a and /dev/null differ
diff --git a/src/audio/samples/Fsharp4v8.m4a b/src/audio/samples/Fsharp4v8.m4a
deleted file mode 100644
index 3d9454b..0000000
Binary files a/src/audio/samples/Fsharp4v8.m4a and /dev/null differ
diff --git a/src/audio/samples/Fsharp5v12.m4a b/src/audio/samples/Fsharp5v12.m4a
index d09214c..1105dfb 100644
Binary files a/src/audio/samples/Fsharp5v12.m4a and b/src/audio/samples/Fsharp5v12.m4a differ
diff --git a/src/audio/samples/Fsharp5v16.m4a b/src/audio/samples/Fsharp5v16.m4a
deleted file mode 100644
index b43b919..0000000
Binary files a/src/audio/samples/Fsharp5v16.m4a and /dev/null differ
diff --git a/src/audio/samples/Fsharp5v4.m4a b/src/audio/samples/Fsharp5v4.m4a
deleted file mode 100644
index c3eaac2..0000000
Binary files a/src/audio/samples/Fsharp5v4.m4a and /dev/null differ
diff --git a/src/audio/samples/Fsharp5v8.m4a b/src/audio/samples/Fsharp5v8.m4a
deleted file mode 100644
index 79fb2f6..0000000
Binary files a/src/audio/samples/Fsharp5v8.m4a and /dev/null differ
diff --git a/src/audio/samples/Fsharp6v12.m4a b/src/audio/samples/Fsharp6v12.m4a
index b92adb7..d141d41 100644
Binary files a/src/audio/samples/Fsharp6v12.m4a and b/src/audio/samples/Fsharp6v12.m4a differ
diff --git a/src/audio/samples/Fsharp6v16.m4a b/src/audio/samples/Fsharp6v16.m4a
deleted file mode 100644
index 9b228c5..0000000
Binary files a/src/audio/samples/Fsharp6v16.m4a and /dev/null differ
diff --git a/src/audio/samples/Fsharp6v4.m4a b/src/audio/samples/Fsharp6v4.m4a
deleted file mode 100644
index c9a1691..0000000
Binary files a/src/audio/samples/Fsharp6v4.m4a and /dev/null differ
diff --git a/src/audio/samples/Fsharp6v8.m4a b/src/audio/samples/Fsharp6v8.m4a
deleted file mode 100644
index 7c122bd..0000000
Binary files a/src/audio/samples/Fsharp6v8.m4a and /dev/null differ
diff --git a/src/audio/samples/Fsharp7v12.m4a b/src/audio/samples/Fsharp7v12.m4a
index 2ad18ba..d69ac59 100644
Binary files a/src/audio/samples/Fsharp7v12.m4a and b/src/audio/samples/Fsharp7v12.m4a differ
diff --git a/src/audio/samples/Fsharp7v16.m4a b/src/audio/samples/Fsharp7v16.m4a
deleted file mode 100644
index e4644a2..0000000
Binary files a/src/audio/samples/Fsharp7v16.m4a and /dev/null differ
diff --git a/src/audio/samples/Fsharp7v4.m4a b/src/audio/samples/Fsharp7v4.m4a
deleted file mode 100644
index f3f9787..0000000
Binary files a/src/audio/samples/Fsharp7v4.m4a and /dev/null differ
diff --git a/src/audio/samples/Fsharp7v8.m4a b/src/audio/samples/Fsharp7v8.m4a
deleted file mode 100644
index d9020b6..0000000
Binary files a/src/audio/samples/Fsharp7v8.m4a and /dev/null differ
diff --git a/src/audio/samples/README.md b/src/audio/samples/README.md
index b37247b..bdde746 100644
--- a/src/audio/samples/README.md
+++ b/src/audio/samples/README.md
@@ -2,25 +2,15 @@ Piano samples are Salamander Grand Piano V3 samples by Alexander Holm,
transcoded from OGG Vorbis to AAC M4A for iOS browser playback and distributed
under CC BY 3.0.
-Source packages:
-
-- @audio-samples/piano-velocity4
-- @audio-samples/piano-velocity8
-- @audio-samples/piano-velocity12
-- @audio-samples/piano-velocity16
-- @audio-samples/piano-release
-
+Source package: @audio-samples/piano-velocity12
Source recording: https://archive.org/details/SalamanderGrandPianoV3
License: https://creativecommons.org/licenses/by/3.0/
-Checked-in subset: velocity layers `v4`, `v8`, `v12`, and `v16` at the
-available Salamander strike anchors, plus all 88 release samples. The strike
-anchors are A, C, Dsharp, and Fsharp for octaves 1-7, plus A0, A7, and C8.
-The app derives strike MIDI values and velocity layers from filenames in
-`piano-samples.ts`; release sample `rel1` maps to A0 and `rel88` maps to C8.
+Checked-in subset: velocity layer `v12`, every minor-third anchor from A0
+through C8: A, C, Dsharp, and Fsharp for octaves 1-7, plus A0, A7, and C8.
+The app derives MIDI values from those note names in `piano-samples.ts`.
-Repro notes: start from the matching OGG files in the source packages and
-transcode each selected sample to AAC/M4A at 192 kbps without renaming the
-note/velocity or release stem. Replace `#` with `sharp` in filenames for URL
-compatibility. Expected output filenames include `v.m4a`, for
-example `C4v16.m4a`, and `rel.m4a`, for example `rel40.m4a`.
+Repro notes: start from the matching `v12` OGG files in the source package and
+transcode each selected sample to AAC/M4A without renaming the note/velocity
+stem. The expected output filenames are `v12.m4a`, for example
+`C4v12.m4a`.
diff --git a/src/audio/samples/rel1.m4a b/src/audio/samples/rel1.m4a
deleted file mode 100644
index 8cf2d5b..0000000
Binary files a/src/audio/samples/rel1.m4a and /dev/null differ
diff --git a/src/audio/samples/rel10.m4a b/src/audio/samples/rel10.m4a
deleted file mode 100644
index 6cad512..0000000
Binary files a/src/audio/samples/rel10.m4a and /dev/null differ
diff --git a/src/audio/samples/rel11.m4a b/src/audio/samples/rel11.m4a
deleted file mode 100644
index 0b386b3..0000000
Binary files a/src/audio/samples/rel11.m4a and /dev/null differ
diff --git a/src/audio/samples/rel12.m4a b/src/audio/samples/rel12.m4a
deleted file mode 100644
index c870544..0000000
Binary files a/src/audio/samples/rel12.m4a and /dev/null differ
diff --git a/src/audio/samples/rel13.m4a b/src/audio/samples/rel13.m4a
deleted file mode 100644
index 5849362..0000000
Binary files a/src/audio/samples/rel13.m4a and /dev/null differ
diff --git a/src/audio/samples/rel14.m4a b/src/audio/samples/rel14.m4a
deleted file mode 100644
index 0dfc5a0..0000000
Binary files a/src/audio/samples/rel14.m4a and /dev/null differ
diff --git a/src/audio/samples/rel15.m4a b/src/audio/samples/rel15.m4a
deleted file mode 100644
index 54b6acf..0000000
Binary files a/src/audio/samples/rel15.m4a and /dev/null differ
diff --git a/src/audio/samples/rel16.m4a b/src/audio/samples/rel16.m4a
deleted file mode 100644
index 863f1ff..0000000
Binary files a/src/audio/samples/rel16.m4a and /dev/null differ
diff --git a/src/audio/samples/rel17.m4a b/src/audio/samples/rel17.m4a
deleted file mode 100644
index 591ee71..0000000
Binary files a/src/audio/samples/rel17.m4a and /dev/null differ
diff --git a/src/audio/samples/rel18.m4a b/src/audio/samples/rel18.m4a
deleted file mode 100644
index 77533bf..0000000
Binary files a/src/audio/samples/rel18.m4a and /dev/null differ
diff --git a/src/audio/samples/rel19.m4a b/src/audio/samples/rel19.m4a
deleted file mode 100644
index 89c7b27..0000000
Binary files a/src/audio/samples/rel19.m4a and /dev/null differ
diff --git a/src/audio/samples/rel2.m4a b/src/audio/samples/rel2.m4a
deleted file mode 100644
index dd5ab64..0000000
Binary files a/src/audio/samples/rel2.m4a and /dev/null differ
diff --git a/src/audio/samples/rel20.m4a b/src/audio/samples/rel20.m4a
deleted file mode 100644
index 874cbfe..0000000
Binary files a/src/audio/samples/rel20.m4a and /dev/null differ
diff --git a/src/audio/samples/rel21.m4a b/src/audio/samples/rel21.m4a
deleted file mode 100644
index daafbef..0000000
Binary files a/src/audio/samples/rel21.m4a and /dev/null differ
diff --git a/src/audio/samples/rel22.m4a b/src/audio/samples/rel22.m4a
deleted file mode 100644
index 0dbe681..0000000
Binary files a/src/audio/samples/rel22.m4a and /dev/null differ
diff --git a/src/audio/samples/rel23.m4a b/src/audio/samples/rel23.m4a
deleted file mode 100644
index ff51cbc..0000000
Binary files a/src/audio/samples/rel23.m4a and /dev/null differ
diff --git a/src/audio/samples/rel24.m4a b/src/audio/samples/rel24.m4a
deleted file mode 100644
index b5515e5..0000000
Binary files a/src/audio/samples/rel24.m4a and /dev/null differ
diff --git a/src/audio/samples/rel25.m4a b/src/audio/samples/rel25.m4a
deleted file mode 100644
index 12ccd06..0000000
Binary files a/src/audio/samples/rel25.m4a and /dev/null differ
diff --git a/src/audio/samples/rel26.m4a b/src/audio/samples/rel26.m4a
deleted file mode 100644
index 81e3075..0000000
Binary files a/src/audio/samples/rel26.m4a and /dev/null differ
diff --git a/src/audio/samples/rel27.m4a b/src/audio/samples/rel27.m4a
deleted file mode 100644
index 25ebe1e..0000000
Binary files a/src/audio/samples/rel27.m4a and /dev/null differ
diff --git a/src/audio/samples/rel28.m4a b/src/audio/samples/rel28.m4a
deleted file mode 100644
index d49226d..0000000
Binary files a/src/audio/samples/rel28.m4a and /dev/null differ
diff --git a/src/audio/samples/rel29.m4a b/src/audio/samples/rel29.m4a
deleted file mode 100644
index 6fd7267..0000000
Binary files a/src/audio/samples/rel29.m4a and /dev/null differ
diff --git a/src/audio/samples/rel3.m4a b/src/audio/samples/rel3.m4a
deleted file mode 100644
index 9d67e09..0000000
Binary files a/src/audio/samples/rel3.m4a and /dev/null differ
diff --git a/src/audio/samples/rel30.m4a b/src/audio/samples/rel30.m4a
deleted file mode 100644
index 3312f64..0000000
Binary files a/src/audio/samples/rel30.m4a and /dev/null differ
diff --git a/src/audio/samples/rel31.m4a b/src/audio/samples/rel31.m4a
deleted file mode 100644
index 2543ce7..0000000
Binary files a/src/audio/samples/rel31.m4a and /dev/null differ
diff --git a/src/audio/samples/rel32.m4a b/src/audio/samples/rel32.m4a
deleted file mode 100644
index b06a0f9..0000000
Binary files a/src/audio/samples/rel32.m4a and /dev/null differ
diff --git a/src/audio/samples/rel33.m4a b/src/audio/samples/rel33.m4a
deleted file mode 100644
index 40de0b5..0000000
Binary files a/src/audio/samples/rel33.m4a and /dev/null differ
diff --git a/src/audio/samples/rel34.m4a b/src/audio/samples/rel34.m4a
deleted file mode 100644
index 52761b1..0000000
Binary files a/src/audio/samples/rel34.m4a and /dev/null differ
diff --git a/src/audio/samples/rel35.m4a b/src/audio/samples/rel35.m4a
deleted file mode 100644
index 3603cf2..0000000
Binary files a/src/audio/samples/rel35.m4a and /dev/null differ
diff --git a/src/audio/samples/rel36.m4a b/src/audio/samples/rel36.m4a
deleted file mode 100644
index e16e5f4..0000000
Binary files a/src/audio/samples/rel36.m4a and /dev/null differ
diff --git a/src/audio/samples/rel37.m4a b/src/audio/samples/rel37.m4a
deleted file mode 100644
index c0c316c..0000000
Binary files a/src/audio/samples/rel37.m4a and /dev/null differ
diff --git a/src/audio/samples/rel38.m4a b/src/audio/samples/rel38.m4a
deleted file mode 100644
index 4b1496f..0000000
Binary files a/src/audio/samples/rel38.m4a and /dev/null differ
diff --git a/src/audio/samples/rel39.m4a b/src/audio/samples/rel39.m4a
deleted file mode 100644
index af13d13..0000000
Binary files a/src/audio/samples/rel39.m4a and /dev/null differ
diff --git a/src/audio/samples/rel4.m4a b/src/audio/samples/rel4.m4a
deleted file mode 100644
index addaa1f..0000000
Binary files a/src/audio/samples/rel4.m4a and /dev/null differ
diff --git a/src/audio/samples/rel40.m4a b/src/audio/samples/rel40.m4a
deleted file mode 100644
index 41fb5b8..0000000
Binary files a/src/audio/samples/rel40.m4a and /dev/null differ
diff --git a/src/audio/samples/rel41.m4a b/src/audio/samples/rel41.m4a
deleted file mode 100644
index bfa11c8..0000000
Binary files a/src/audio/samples/rel41.m4a and /dev/null differ
diff --git a/src/audio/samples/rel42.m4a b/src/audio/samples/rel42.m4a
deleted file mode 100644
index d6439fa..0000000
Binary files a/src/audio/samples/rel42.m4a and /dev/null differ
diff --git a/src/audio/samples/rel43.m4a b/src/audio/samples/rel43.m4a
deleted file mode 100644
index 5b94878..0000000
Binary files a/src/audio/samples/rel43.m4a and /dev/null differ
diff --git a/src/audio/samples/rel44.m4a b/src/audio/samples/rel44.m4a
deleted file mode 100644
index ba6b1d3..0000000
Binary files a/src/audio/samples/rel44.m4a and /dev/null differ
diff --git a/src/audio/samples/rel45.m4a b/src/audio/samples/rel45.m4a
deleted file mode 100644
index 4eab6bc..0000000
Binary files a/src/audio/samples/rel45.m4a and /dev/null differ
diff --git a/src/audio/samples/rel46.m4a b/src/audio/samples/rel46.m4a
deleted file mode 100644
index 42a0fb3..0000000
Binary files a/src/audio/samples/rel46.m4a and /dev/null differ
diff --git a/src/audio/samples/rel47.m4a b/src/audio/samples/rel47.m4a
deleted file mode 100644
index b7fa1a7..0000000
Binary files a/src/audio/samples/rel47.m4a and /dev/null differ
diff --git a/src/audio/samples/rel48.m4a b/src/audio/samples/rel48.m4a
deleted file mode 100644
index 3234c08..0000000
Binary files a/src/audio/samples/rel48.m4a and /dev/null differ
diff --git a/src/audio/samples/rel49.m4a b/src/audio/samples/rel49.m4a
deleted file mode 100644
index 6e49637..0000000
Binary files a/src/audio/samples/rel49.m4a and /dev/null differ
diff --git a/src/audio/samples/rel5.m4a b/src/audio/samples/rel5.m4a
deleted file mode 100644
index 17c9856..0000000
Binary files a/src/audio/samples/rel5.m4a and /dev/null differ
diff --git a/src/audio/samples/rel50.m4a b/src/audio/samples/rel50.m4a
deleted file mode 100644
index dd01182..0000000
Binary files a/src/audio/samples/rel50.m4a and /dev/null differ
diff --git a/src/audio/samples/rel51.m4a b/src/audio/samples/rel51.m4a
deleted file mode 100644
index c875276..0000000
Binary files a/src/audio/samples/rel51.m4a and /dev/null differ
diff --git a/src/audio/samples/rel52.m4a b/src/audio/samples/rel52.m4a
deleted file mode 100644
index d49ebb4..0000000
Binary files a/src/audio/samples/rel52.m4a and /dev/null differ
diff --git a/src/audio/samples/rel53.m4a b/src/audio/samples/rel53.m4a
deleted file mode 100644
index d599e0d..0000000
Binary files a/src/audio/samples/rel53.m4a and /dev/null differ
diff --git a/src/audio/samples/rel54.m4a b/src/audio/samples/rel54.m4a
deleted file mode 100644
index 7dc6bd5..0000000
Binary files a/src/audio/samples/rel54.m4a and /dev/null differ
diff --git a/src/audio/samples/rel55.m4a b/src/audio/samples/rel55.m4a
deleted file mode 100644
index 4534017..0000000
Binary files a/src/audio/samples/rel55.m4a and /dev/null differ
diff --git a/src/audio/samples/rel56.m4a b/src/audio/samples/rel56.m4a
deleted file mode 100644
index aac1106..0000000
Binary files a/src/audio/samples/rel56.m4a and /dev/null differ
diff --git a/src/audio/samples/rel57.m4a b/src/audio/samples/rel57.m4a
deleted file mode 100644
index a6b637b..0000000
Binary files a/src/audio/samples/rel57.m4a and /dev/null differ
diff --git a/src/audio/samples/rel58.m4a b/src/audio/samples/rel58.m4a
deleted file mode 100644
index 311e6bc..0000000
Binary files a/src/audio/samples/rel58.m4a and /dev/null differ
diff --git a/src/audio/samples/rel59.m4a b/src/audio/samples/rel59.m4a
deleted file mode 100644
index 6dc7ddc..0000000
Binary files a/src/audio/samples/rel59.m4a and /dev/null differ
diff --git a/src/audio/samples/rel6.m4a b/src/audio/samples/rel6.m4a
deleted file mode 100644
index e805093..0000000
Binary files a/src/audio/samples/rel6.m4a and /dev/null differ
diff --git a/src/audio/samples/rel60.m4a b/src/audio/samples/rel60.m4a
deleted file mode 100644
index 60568da..0000000
Binary files a/src/audio/samples/rel60.m4a and /dev/null differ
diff --git a/src/audio/samples/rel61.m4a b/src/audio/samples/rel61.m4a
deleted file mode 100644
index 42e57d3..0000000
Binary files a/src/audio/samples/rel61.m4a and /dev/null differ
diff --git a/src/audio/samples/rel62.m4a b/src/audio/samples/rel62.m4a
deleted file mode 100644
index 386bc6f..0000000
Binary files a/src/audio/samples/rel62.m4a and /dev/null differ
diff --git a/src/audio/samples/rel63.m4a b/src/audio/samples/rel63.m4a
deleted file mode 100644
index a2a5fc5..0000000
Binary files a/src/audio/samples/rel63.m4a and /dev/null differ
diff --git a/src/audio/samples/rel64.m4a b/src/audio/samples/rel64.m4a
deleted file mode 100644
index b0795b0..0000000
Binary files a/src/audio/samples/rel64.m4a and /dev/null differ
diff --git a/src/audio/samples/rel65.m4a b/src/audio/samples/rel65.m4a
deleted file mode 100644
index c51e877..0000000
Binary files a/src/audio/samples/rel65.m4a and /dev/null differ
diff --git a/src/audio/samples/rel66.m4a b/src/audio/samples/rel66.m4a
deleted file mode 100644
index 7307df4..0000000
Binary files a/src/audio/samples/rel66.m4a and /dev/null differ
diff --git a/src/audio/samples/rel67.m4a b/src/audio/samples/rel67.m4a
deleted file mode 100644
index ede7dd2..0000000
Binary files a/src/audio/samples/rel67.m4a and /dev/null differ
diff --git a/src/audio/samples/rel68.m4a b/src/audio/samples/rel68.m4a
deleted file mode 100644
index 25d123f..0000000
Binary files a/src/audio/samples/rel68.m4a and /dev/null differ
diff --git a/src/audio/samples/rel69.m4a b/src/audio/samples/rel69.m4a
deleted file mode 100644
index 1102c0e..0000000
Binary files a/src/audio/samples/rel69.m4a and /dev/null differ
diff --git a/src/audio/samples/rel7.m4a b/src/audio/samples/rel7.m4a
deleted file mode 100644
index 91f5ef6..0000000
Binary files a/src/audio/samples/rel7.m4a and /dev/null differ
diff --git a/src/audio/samples/rel70.m4a b/src/audio/samples/rel70.m4a
deleted file mode 100644
index 7771d47..0000000
Binary files a/src/audio/samples/rel70.m4a and /dev/null differ
diff --git a/src/audio/samples/rel71.m4a b/src/audio/samples/rel71.m4a
deleted file mode 100644
index 03fcfd4..0000000
Binary files a/src/audio/samples/rel71.m4a and /dev/null differ
diff --git a/src/audio/samples/rel72.m4a b/src/audio/samples/rel72.m4a
deleted file mode 100644
index f945bb9..0000000
Binary files a/src/audio/samples/rel72.m4a and /dev/null differ
diff --git a/src/audio/samples/rel73.m4a b/src/audio/samples/rel73.m4a
deleted file mode 100644
index 104ba8d..0000000
Binary files a/src/audio/samples/rel73.m4a and /dev/null differ
diff --git a/src/audio/samples/rel74.m4a b/src/audio/samples/rel74.m4a
deleted file mode 100644
index 88c6f8d..0000000
Binary files a/src/audio/samples/rel74.m4a and /dev/null differ
diff --git a/src/audio/samples/rel75.m4a b/src/audio/samples/rel75.m4a
deleted file mode 100644
index 8e32dd2..0000000
Binary files a/src/audio/samples/rel75.m4a and /dev/null differ
diff --git a/src/audio/samples/rel76.m4a b/src/audio/samples/rel76.m4a
deleted file mode 100644
index e95f09d..0000000
Binary files a/src/audio/samples/rel76.m4a and /dev/null differ
diff --git a/src/audio/samples/rel77.m4a b/src/audio/samples/rel77.m4a
deleted file mode 100644
index dab0b10..0000000
Binary files a/src/audio/samples/rel77.m4a and /dev/null differ
diff --git a/src/audio/samples/rel78.m4a b/src/audio/samples/rel78.m4a
deleted file mode 100644
index 0c9808c..0000000
Binary files a/src/audio/samples/rel78.m4a and /dev/null differ
diff --git a/src/audio/samples/rel79.m4a b/src/audio/samples/rel79.m4a
deleted file mode 100644
index 75d2f1e..0000000
Binary files a/src/audio/samples/rel79.m4a and /dev/null differ
diff --git a/src/audio/samples/rel8.m4a b/src/audio/samples/rel8.m4a
deleted file mode 100644
index 69c5036..0000000
Binary files a/src/audio/samples/rel8.m4a and /dev/null differ
diff --git a/src/audio/samples/rel80.m4a b/src/audio/samples/rel80.m4a
deleted file mode 100644
index f43ae99..0000000
Binary files a/src/audio/samples/rel80.m4a and /dev/null differ
diff --git a/src/audio/samples/rel81.m4a b/src/audio/samples/rel81.m4a
deleted file mode 100644
index 7a6198f..0000000
Binary files a/src/audio/samples/rel81.m4a and /dev/null differ
diff --git a/src/audio/samples/rel82.m4a b/src/audio/samples/rel82.m4a
deleted file mode 100644
index 19a1c8e..0000000
Binary files a/src/audio/samples/rel82.m4a and /dev/null differ
diff --git a/src/audio/samples/rel83.m4a b/src/audio/samples/rel83.m4a
deleted file mode 100644
index 78ca8a2..0000000
Binary files a/src/audio/samples/rel83.m4a and /dev/null differ
diff --git a/src/audio/samples/rel84.m4a b/src/audio/samples/rel84.m4a
deleted file mode 100644
index 3f4d8c5..0000000
Binary files a/src/audio/samples/rel84.m4a and /dev/null differ
diff --git a/src/audio/samples/rel85.m4a b/src/audio/samples/rel85.m4a
deleted file mode 100644
index afa6e8e..0000000
Binary files a/src/audio/samples/rel85.m4a and /dev/null differ
diff --git a/src/audio/samples/rel86.m4a b/src/audio/samples/rel86.m4a
deleted file mode 100644
index 26977b1..0000000
Binary files a/src/audio/samples/rel86.m4a and /dev/null differ
diff --git a/src/audio/samples/rel87.m4a b/src/audio/samples/rel87.m4a
deleted file mode 100644
index 666d1d8..0000000
Binary files a/src/audio/samples/rel87.m4a and /dev/null differ
diff --git a/src/audio/samples/rel88.m4a b/src/audio/samples/rel88.m4a
deleted file mode 100644
index 6045663..0000000
Binary files a/src/audio/samples/rel88.m4a and /dev/null differ
diff --git a/src/audio/samples/rel9.m4a b/src/audio/samples/rel9.m4a
deleted file mode 100644
index 1d2be34..0000000
Binary files a/src/audio/samples/rel9.m4a and /dev/null differ
diff --git a/src/config/eraser-size.ts b/src/config/eraser-size.ts
deleted file mode 100644
index 008bee7..0000000
--- a/src/config/eraser-size.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-export interface CssPixelSize {
- height: number;
- width: number;
-}
-
-export const ERASER_SIZE_MIN = 24;
-export const ERASER_SIZE_MAX = 480;
-
-const ERASER_MAX_SHORT_SIDE_RATIO = 0.55;
-
-const getNormalizedEraserSizeMax = (maxSize: number): number => {
- const safeMaxSize = Number.isFinite(maxSize) ? Math.floor(maxSize) : ERASER_SIZE_MAX;
- return Math.max(ERASER_SIZE_MIN, Math.min(ERASER_SIZE_MAX, safeMaxSize));
-};
-
-export const getElementCssPixelSize = (element: Element): CssPixelSize => {
- const rect = element.getBoundingClientRect();
- const { clientHeight = 0, clientWidth = 0 } = element as HTMLElement;
- return {
- height: rect.height || clientHeight,
- width: rect.width || clientWidth,
- };
-};
-
-export const getEraserSizeMaxForCssSize = ({ height, width }: CssPixelSize): number => {
- const shortestSide = Math.min(height, width);
- if (!Number.isFinite(shortestSide) || shortestSide <= 0) {
- return ERASER_SIZE_MAX;
- }
-
- return getNormalizedEraserSizeMax(
- Math.floor(shortestSide * ERASER_MAX_SHORT_SIDE_RATIO)
- );
-};
-
-export const clampEraserSize = (
- value: number,
- maxSize = ERASER_SIZE_MAX,
- fallbackSize = ERASER_SIZE_MIN
-): number => {
- const max = getNormalizedEraserSizeMax(maxSize);
- const fallback = Number.isFinite(fallbackSize) ? fallbackSize : ERASER_SIZE_MIN;
- const safeValue = Number.isFinite(value) ? value : fallback;
- return Math.min(max, Math.max(ERASER_SIZE_MIN, Math.round(safeValue)));
-};
-
-export const getEffectiveEraserSize = (
- size: number,
- cssSize: CssPixelSize,
- fallbackSize = ERASER_SIZE_MIN
-): number => clampEraserSize(size, getEraserSizeMaxForCssSize(cssSize), fallbackSize);
-
-export const getEraserSizeRatio = (size: number, maxSize = ERASER_SIZE_MAX): number => {
- const max = getNormalizedEraserSizeMax(maxSize);
- if (max === ERASER_SIZE_MIN) {
- return 0;
- }
-
- return (clampEraserSize(size, max) - ERASER_SIZE_MIN) / (max - ERASER_SIZE_MIN);
-};
-
-const ERASER_SLIDER_MIN = 0;
-const ERASER_SLIDER_MAX = 1;
-
-const clampSliderRatio = (value: number): number => {
- const safeValue = Number.isFinite(value) ? value : ERASER_SLIDER_MIN;
- return Math.min(ERASER_SLIDER_MAX, Math.max(ERASER_SLIDER_MIN, safeValue));
-};
-
-export const getEraserSizeFromSliderRatio = (
- sliderRatio: number,
- maxSize = ERASER_SIZE_MAX
-): number => {
- const max = getNormalizedEraserSizeMax(maxSize);
- return clampEraserSize(
- ERASER_SIZE_MIN + (max - ERASER_SIZE_MIN) * clampSliderRatio(sliderRatio) ** 2,
- max
- );
-};
-
-export const getEraserSliderRatioFromSize = (
- size: number,
- maxSize = ERASER_SIZE_MAX
-): number => Math.sqrt(getEraserSizeRatio(size, maxSize));
diff --git a/src/game-loop/eraser-preview.ts b/src/game-loop/eraser-preview.ts
index 493d497..fefd93c 100644
--- a/src/game-loop/eraser-preview.ts
+++ b/src/game-loop/eraser-preview.ts
@@ -1,4 +1,3 @@
-import { getEffectiveEraserSize } from '../config/eraser-size';
import { settings } from '../settings';
export class EraserPreview {
@@ -50,15 +49,9 @@ export class EraserPreview {
};
}
- const rect = this.canvas.getBoundingClientRect();
- const size = getEffectiveEraserSize(settings.eraserSize, {
- height: rect.height || this.canvas.clientHeight,
- width: rect.width || this.canvas.clientWidth,
- });
-
- if (this.previousSize !== size) {
- this.element.style.setProperty('--eraser-preview-size', `${size}px`);
- this.previousSize = size;
+ if (this.previousSize !== settings.eraserSize) {
+ this.element.style.setProperty('--eraser-preview-size', `${settings.eraserSize}px`);
+ this.previousSize = settings.eraserSize;
}
if (
@@ -70,6 +63,7 @@ export class EraserPreview {
return;
}
+ const rect = this.canvas.getBoundingClientRect();
const left = `${this.previewClientPosition.x - rect.left}px`;
const top = `${this.previewClientPosition.y - rect.top}px`;
if (this.previousLeft !== left) {
diff --git a/src/game-loop/game-loop.ts b/src/game-loop/game-loop.ts
index d53a73c..25d2323 100644
--- a/src/game-loop/game-loop.ts
+++ b/src/game-loop/game-loop.ts
@@ -2,7 +2,6 @@ import { vec2 } from 'gl-matrix';
import { GardenAudio } from '../audio/garden-audio';
import { createGardenAudioConfig } from '../audio/garden-audio-config';
-import { getEffectiveEraserSize, getElementCssPixelSize } from '../config/eraser-size';
import { activeVibe, settings } from '../settings';
import { DeltaTimeCalculator } from '../utils/delta-time-calculator';
import { rgbColorToCss, type RgbColor } from '../utils/rgb-color';
@@ -210,11 +209,7 @@ export default class GameLoop {
const runtimeSettings = { ...settings };
const introProgress = this.introPrompt.progress;
const canvasPixelRatio = this.canvasPixelRatio;
- const eraserCssSize = getEffectiveEraserSize(
- runtimeSettings.eraserSize,
- getElementCssPixelSize(this.canvas)
- );
- const eraserPixelSize = eraserCssSize * canvasPixelRatio;
+ const eraserPixelSize = runtimeSettings.eraserSize * canvasPixelRatio;
const isErasing = this.pointerInput.isEraseMode;
const accentColor =
channelColors[runtimeSettings.selectedColorIndex] ?? channelColors[0];
diff --git a/src/page/audio-control.ts b/src/page/audio-control.ts
index 0918eda..159c6c2 100644
--- a/src/page/audio-control.ts
+++ b/src/page/audio-control.ts
@@ -1,17 +1,18 @@
-import { DEFAULT_AUDIO_VOLUME, MAX_AUDIO_VOLUME } from '../audio/garden-audio-config';
+import { DEFAULT_AUDIO_VOLUME } from '../audio/garden-audio-config';
import type GameLoop from '../game-loop/game-loop';
import { readBrowserStorage, writeBrowserStorage } from '../utils/browser-storage';
import { queryRequiredElement } from '../utils/dom';
+import { clamp01 } from '../utils/math';
const AUDIO_MUTED_STORAGE_KEY = 'fleeting-garden:audio-muted';
const AUDIO_VOLUME_STORAGE_KEY = 'fleeting-garden:audio-volume';
const AUDIO_VOLUME_MIN = 0;
-const AUDIO_VOLUME_MAX = MAX_AUDIO_VOLUME;
+const AUDIO_VOLUME_MAX = 1;
const AUDIO_VOLUME_STEP = 0.01;
const clampAudioVolume = (value: number): number => {
const safeValue = Number.isFinite(value) ? value : DEFAULT_AUDIO_VOLUME;
- return Math.min(AUDIO_VOLUME_MAX, Math.max(AUDIO_VOLUME_MIN, safeValue));
+ return Math.min(AUDIO_VOLUME_MAX, Math.max(AUDIO_VOLUME_MIN, clamp01(safeValue)));
};
const readInitialAudioVolume = (): number => {
@@ -82,7 +83,6 @@ export class AudioControl {
this.audioVolume = clampAudioVolume(this.audioVolume);
const isEffectivelyMuted = this.isMuted;
const volumePercent = Math.round(this.audioVolume * 100);
- const volumeProgressPercent = Math.round((this.audioVolume / AUDIO_VOLUME_MAX) * 100);
this.soundButton.classList.toggle('muted', isEffectivelyMuted);
this.soundButton.setAttribute('aria-pressed', String(isEffectivelyMuted));
@@ -102,10 +102,7 @@ export class AudioControl {
this.volumeControl.title = isEffectivelyMuted
? `Muted, ${volumePercent}% volume`
: `${volumePercent}% volume`;
- this.volumeControl.style.setProperty(
- '--volume-progress',
- `${volumeProgressPercent}%`
- );
+ this.volumeControl.style.setProperty('--volume-progress', `${volumePercent}%`);
const game = this.options.getGame();
game?.setAudioVolume(this.audioVolume);
diff --git a/src/page/eraser-size-control.test.ts b/src/page/eraser-size-control.test.ts
index 372c63e..1d93778 100644
--- a/src/page/eraser-size-control.test.ts
+++ b/src/page/eraser-size-control.test.ts
@@ -3,11 +3,9 @@ import { describe, expect, it } from 'vitest';
import {
ERASER_SIZE_MAX,
ERASER_SIZE_MIN,
- getEffectiveEraserSize,
getEraserSizeFromSliderRatio,
- getEraserSizeMaxForCssSize,
getEraserSliderRatioFromSize,
-} from '../config/eraser-size';
+} from './eraser-size-control';
describe('eraser size slider mapping', () => {
it('maps slider position quadratically to eraser size', () => {
@@ -25,15 +23,4 @@ describe('eraser size slider mapping', () => {
expect(getEraserSliderRatioFromSize(quarterRangeSize)).toBe(0.5);
expect(getEraserSliderRatioFromSize(ERASER_SIZE_MAX)).toBe(1);
});
-
- it('uses a responsive max size on small canvases', () => {
- const mobileMax = getEraserSizeMaxForCssSize({ height: 640, width: 390 });
-
- expect(mobileMax).toBeLessThan(ERASER_SIZE_MAX);
- expect(getEraserSizeFromSliderRatio(1, mobileMax)).toBe(mobileMax);
- expect(getEraserSliderRatioFromSize(ERASER_SIZE_MAX, mobileMax)).toBe(1);
- expect(getEffectiveEraserSize(ERASER_SIZE_MAX, { height: 640, width: 390 })).toBe(
- mobileMax
- );
- });
});
diff --git a/src/page/eraser-size-control.ts b/src/page/eraser-size-control.ts
index beb2d0f..8f93d51 100644
--- a/src/page/eraser-size-control.ts
+++ b/src/page/eraser-size-control.ts
@@ -1,25 +1,40 @@
-import {
- clampEraserSize,
- ERASER_SIZE_MAX,
- getElementCssPixelSize,
- getEraserSizeFromSliderRatio,
- getEraserSizeMaxForCssSize,
- getEraserSizeRatio,
- getEraserSliderRatioFromSize,
-} from '../config/eraser-size';
import type GameLoop from '../game-loop/game-loop';
import { DEFAULT_ERASER_SIZE, settings } from '../settings';
import { queryRequiredElement } from '../utils/dom';
+export const ERASER_SIZE_MIN = 24;
+export const ERASER_SIZE_MAX = 480;
+
const ERASER_CONTROL_SCALE_MIN = 0.74;
const ERASER_CONTROL_SCALE_MAX = 1.34;
+const clampEraserSize = (value: number): number => {
+ const safeValue = Number.isFinite(value) ? value : DEFAULT_ERASER_SIZE;
+ return Math.min(ERASER_SIZE_MAX, Math.max(ERASER_SIZE_MIN, Math.round(safeValue)));
+};
+
const ERASER_SLIDER_MIN = 0;
const ERASER_SLIDER_MAX = 1;
const ERASER_SLIDER_STEP = 0.001;
-const clampStoredEraserSize = (value: number): number =>
- clampEraserSize(value, ERASER_SIZE_MAX, DEFAULT_ERASER_SIZE);
+const clampSliderRatio = (value: number): number => {
+ const safeValue = Number.isFinite(value) ? value : ERASER_SLIDER_MIN;
+ return Math.min(ERASER_SLIDER_MAX, Math.max(ERASER_SLIDER_MIN, safeValue));
+};
+
+const getEraserSizeRatio = (size: number): number => {
+ return (clampEraserSize(size) - ERASER_SIZE_MIN) / (ERASER_SIZE_MAX - ERASER_SIZE_MIN);
+};
+
+export const getEraserSizeFromSliderRatio = (sliderRatio: number): number => {
+ return clampEraserSize(
+ ERASER_SIZE_MIN +
+ (ERASER_SIZE_MAX - ERASER_SIZE_MIN) * clampSliderRatio(sliderRatio) ** 2
+ );
+};
+
+export const getEraserSliderRatioFromSize = (size: number): number =>
+ Math.sqrt(getEraserSizeRatio(size));
interface EraserSizeControlOptions {
getGame: () => GameLoop | null;
@@ -33,7 +48,6 @@ export class EraserSizeControl {
HTMLLabelElement
);
private readonly slider = queryRequiredElement('.eraser-size-slider', HTMLInputElement);
- private readonly canvas = queryRequiredElement('canvas', HTMLCanvasElement);
private isActive = false;
public constructor(private readonly options: EraserSizeControlOptions) {
@@ -41,10 +55,7 @@ export class EraserSizeControl {
this.control.addEventListener('click', this.activate);
this.slider.addEventListener('focus', this.activate);
this.slider.addEventListener('input', () => {
- settings.eraserSize = getEraserSizeFromSliderRatio(
- Number(this.slider.value),
- this.getResponsiveMaxSize()
- );
+ settings.eraserSize = getEraserSizeFromSliderRatio(Number(this.slider.value));
this.activate();
this.render();
this.options.onChange();
@@ -52,21 +63,19 @@ export class EraserSizeControl {
}
public render(): void {
- const maxSize = this.getResponsiveMaxSize();
- const storedSize = clampStoredEraserSize(settings.eraserSize);
- if (settings.eraserSize !== storedSize) {
- settings.eraserSize = storedSize;
+ const size = clampEraserSize(settings.eraserSize);
+ if (settings.eraserSize !== size) {
+ settings.eraserSize = size;
}
- const size = clampEraserSize(storedSize, maxSize, DEFAULT_ERASER_SIZE);
- const sliderRatio = getEraserSliderRatioFromSize(size, maxSize);
+ const sliderRatio = getEraserSliderRatioFromSize(size);
this.slider.min = ERASER_SLIDER_MIN.toString();
this.slider.max = ERASER_SLIDER_MAX.toString();
this.slider.step = ERASER_SLIDER_STEP.toString();
this.slider.value = sliderRatio.toString();
this.slider.setAttribute('aria-valuetext', `${size}px`);
- const sizeRatio = getEraserSizeRatio(size, maxSize);
+ const sizeRatio = getEraserSizeRatio(size);
const scale =
ERASER_CONTROL_SCALE_MIN +
(ERASER_CONTROL_SCALE_MAX - ERASER_CONTROL_SCALE_MIN) * sizeRatio;
@@ -86,10 +95,6 @@ export class EraserSizeControl {
this.options.onActivate();
};
- private getResponsiveMaxSize(): number {
- return getEraserSizeMaxForCssSize(getElementCssPixelSize(this.canvas));
- }
-
private syncActiveState(): void {
this.control.classList.toggle('active', this.isActive);
this.slider.setAttribute(
diff --git a/src/style/_loading.scss b/src/style/_loading.scss
index d9a519b..925f915 100644
--- a/src/style/_loading.scss
+++ b/src/style/_loading.scss
@@ -52,7 +52,7 @@
> .splash-description {
margin: 0;
- max-width: 70ch;
+ max-width: 28ch;
color: rgb(255 255 255 / 80%);
font-size: 15px;
font-weight: 400;
diff --git a/src/style/_panels.scss b/src/style/_panels.scss
index 029a044..3e92b02 100644
--- a/src/style/_panels.scss
+++ b/src/style/_panels.scss
@@ -2,10 +2,13 @@
html > body > aside.control-dock > .info-page {
width: min(100%, 520px);
- max-height: 200vh;
- max-height: 200dvh;
+ max-height: min(62vh, 480px);
+ max-height: min(62dvh, 480px);
margin: 0 auto 10px;
- overflow: hidden;
+ overflow-x: hidden;
+ overflow-y: auto;
+ overscroll-behavior: contain;
+ touch-action: pan-y;
border: 1px solid rgb(255 255 255 / 46%);
border-radius: 8px;
background:
@@ -17,12 +20,26 @@ html > body > aside.control-dock > .info-page {
0 16px 42px rgb(0 0 0 / 30%),
0 2px 10px rgb(0 0 0 / 18%);
backdrop-filter: blur(16px) saturate(118%);
+ scrollbar-width: thin;
+ scrollbar-color: rgb(69 98 88 / 62%) transparent;
+ -webkit-overflow-scrolling: touch;
transition:
max-height var(--transition-time-long),
opacity var(--transition-time-long),
transform var(--transition-time-long),
margin-bottom var(--transition-time-long);
+ &::-webkit-scrollbar-track,
+ &::-webkit-scrollbar {
+ background-color: transparent;
+ width: 6px;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background-color: rgb(69 98 88 / 62%);
+ border-radius: 8px;
+ }
+
&:focus-visible {
outline: 2px solid rgb(17 56 45);
outline-offset: 3px;
@@ -133,8 +150,9 @@ html > body > aside.control-dock > .info-page {
line-height: 1.18;
}
- .info-page__main,
- .info-page__notes {
+ .info-page__lede,
+ .info-page__notes,
+ .info-page__meta {
max-width: 56ch;
overflow-wrap: break-word;
color: rgb(25 35 32);
@@ -142,7 +160,8 @@ html > body > aside.control-dock > .info-page {
line-height: 1.56;
}
- .info-page__main {
+ .info-page__lede,
+ .info-page__meta {
margin-bottom: 0;
hyphens: auto;
}
@@ -164,6 +183,19 @@ html > body > aside.control-dock > .info-page {
}
}
+ .info-page__meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.35rem;
+ align-items: baseline;
+ margin-top: 0.2rem;
+ padding-top: 0.75rem;
+ border-top: 1px solid rgb(43 66 57 / 16%);
+ color: rgb(67 82 77);
+ font-size: 0.8rem;
+ line-height: 1.45;
+ }
+
a {
color: rgb(0 83 105);
font-weight: 700;
@@ -193,16 +225,45 @@ html > body > aside.control-dock > .info-page {
@include on-small-screen {
width: min(100%, 520px);
+ max-height: min(58vh, 500px);
+ max-height: min(58dvh, 500px);
.info-page__content {
gap: 0.75rem;
padding: 14px;
}
- .info-page__main,
+ .info-page__lede,
.info-page__notes {
font-size: 0.95rem;
line-height: 1.52;
}
+
+ .info-page__meta {
+ font-size: 0.875rem;
+ line-height: 1.45;
+ }
+ }
+
+ @media (max-height: 420px) {
+ max-height: min(
+ 58vh,
+ max(
+ 10rem,
+ calc(
+ 100vh - 168px - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px)
+ )
+ )
+ );
+ max-height: min(
+ 58dvh,
+ max(
+ 10rem,
+ calc(
+ 100dvh -
+ 168px - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px)
+ )
+ )
+ );
}
}
diff --git a/src/style/common.scss b/src/style/common.scss
index 9996bc5..23c82aa 100644
--- a/src/style/common.scss
+++ b/src/style/common.scss
@@ -16,24 +16,15 @@
html {
height: 100%;
- overscroll-behavior: none;
touch-action: manipulation;
- user-select: none;
-webkit-font-smoothing: antialiased;
- -webkit-touch-callout: none;
- -webkit-user-select: none;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
body {
font-family: 'Open Sans', sans-serif;
- overscroll-behavior: none;
touch-action: manipulation;
- user-select: none;
- -webkit-tap-highlight-color: transparent;
- -webkit-touch-callout: none;
- -webkit-user-select: none;
}
.visually-hidden {