mirror of
https://github.com/hex248/tsos.git
synced 2026-02-07 18:23:05 +00:00
note preview on click
This commit is contained in:
@@ -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() {
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-sm font-medium">Note/Colour</span>
|
||||
<ColorKeyboard value={state.color} onChange={(color) => setState({ ...state, color })} />
|
||||
<ColorKeyboard
|
||||
value={state.color}
|
||||
onChange={(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 });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-sm font-medium">Octave</span>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user