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,
+ );
+}