diff --git a/src/Index.tsx b/src/Index.tsx index 2318b24..652211d 100644 --- a/src/Index.tsx +++ b/src/Index.tsx @@ -16,6 +16,7 @@ import { mapSizeToGain, mapWobbleToDetune, } from "@/lib/audio/mapping"; +import { playPreviewSample } from "@/lib/audio/synth"; import { useEffect, useState } from "react"; import * as Tone from "tone"; import Layout from "./Layout"; @@ -95,7 +96,26 @@ function Index() {
Note/Colour - setState({ ...state, color })} /> + { + const note = + colorScale.find((entry) => entry.color.toLowerCase() === color.toLowerCase()) + ?.note ?? colorScale[0].note; + + void playPreviewSample({ + preset: state.preset, + roundness: state.roundness, + size: state.size, + grain: state.grain, + note, + octave: state.octave, + synthNodes: synthRef.current, + }); + + setState({ ...state, color }); + }} + />
Octave diff --git a/src/components/controls/ColorKeyboard.tsx b/src/components/controls/ColorKeyboard.tsx index 34eaa3c..c669663 100644 --- a/src/components/controls/ColorKeyboard.tsx +++ b/src/components/controls/ColorKeyboard.tsx @@ -52,7 +52,10 @@ export default function ColorKeyboard({ backgroundColor: ["D", "E"].includes(note) ? "#000000" : "#ffffff", color: ["D", "E"].includes(note) ? "#ffffff" : "#000000", } - : { backgroundColor: color } + : { + backgroundColor: color, + color: ["D", "E"].includes(note) ? "#000000" : "#ffffff", + } } > {note} diff --git a/src/lib/audio/synth.ts b/src/lib/audio/synth.ts index 954b8e2..778cb5a 100644 --- a/src/lib/audio/synth.ts +++ b/src/lib/audio/synth.ts @@ -1,3 +1,6 @@ +import { noteToFrequency } from "@/constants/colorScale"; +import { mapGrainToNoise, mapPresetToOscType, mapRoundnessToFade, mapSizeToGain } from "@/lib/audio/mapping"; +import type { Preset } from "@/types/shape"; import * as Tone from "tone"; export type SynthNodes = { @@ -45,3 +48,86 @@ export function disposeSynth(nodes: SynthNodes) { nodes.noise.dispose(); nodes.gain.dispose(); } + +type PreviewOptions = { + preset: Preset; + roundness: number; + size: number; + grain: number; + note: string; + octave: number; + synthNodes: SynthNodes | null; +}; + +const PREVIEW_ATTACK = 0.02; +const PREVIEW_DURATION = 0.2; +const PREVIEW_CLEANUP = 0.05; + +export async function playPreviewSample(options: PreviewOptions) { + const destination = Tone.getDestination(); + const wasMuted = destination.mute; + + if (wasMuted) { + await Tone.start(); + destination.mute = false; + } + + const previousGain = wasMuted && options.synthNodes ? options.synthNodes.gain.gain.value : null; + if (wasMuted && options.synthNodes) { + options.synthNodes.gain.gain.value = 0; + } + + const previewGain = new Tone.Gain(0); + const crossFade = new Tone.CrossFade(mapRoundnessToFade(options.roundness)); + const oscillatorA = new Tone.Oscillator({ type: mapPresetToOscType(options.preset) }); + const oscillatorB = new Tone.Oscillator({ type: "sine" }); + const noise = new Tone.Noise({ type: "white" }); + + oscillatorA.connect(crossFade.a); + oscillatorB.connect(crossFade.b); + crossFade.connect(previewGain); + noise.connect(previewGain); + previewGain.toDestination(); + + const frequency = noteToFrequency(options.note, options.octave); + oscillatorA.frequency.value = frequency; + oscillatorB.frequency.value = frequency; + + const grain = mapGrainToNoise(options.grain); + const noiseDb = grain === 0 ? Number.NEGATIVE_INFINITY : -40 + (-12 - -40) * grain; + noise.volume.value = noiseDb; + + const now = Tone.now(); + const peak = Tone.dbToGain(mapSizeToGain(options.size)); + const stopAt = now + PREVIEW_DURATION + PREVIEW_CLEANUP; + + previewGain.gain.setValueAtTime(0, now); + previewGain.gain.linearRampToValueAtTime(peak, now + PREVIEW_ATTACK); + previewGain.gain.linearRampToValueAtTime(0, now + PREVIEW_DURATION); + + oscillatorA.start(now); + oscillatorB.start(now); + noise.start(now); + oscillatorA.stop(stopAt); + oscillatorB.stop(stopAt); + noise.stop(stopAt); + + window.setTimeout( + () => { + oscillatorA.dispose(); + oscillatorB.dispose(); + crossFade.dispose(); + noise.dispose(); + previewGain.dispose(); + + if (wasMuted) { + destination.mute = true; + } + + if (previousGain !== null && options.synthNodes) { + options.synthNodes.gain.gain.value = previousGain; + } + }, + (stopAt - now) * 1000, + ); +}