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.

@@ -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.

-

- 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 {