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,
|
mapSizeToGain,
|
||||||
mapWobbleToDetune,
|
mapWobbleToDetune,
|
||||||
} from "@/lib/audio/mapping";
|
} from "@/lib/audio/mapping";
|
||||||
|
import { playPreviewSample } from "@/lib/audio/synth";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import * as Tone from "tone";
|
import * as Tone from "tone";
|
||||||
import Layout from "./Layout";
|
import Layout from "./Layout";
|
||||||
@@ -95,7 +96,26 @@ function Index() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<span className="text-sm font-medium">Note/Colour</span>
|
<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>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<span className="text-sm font-medium">Octave</span>
|
<span className="text-sm font-medium">Octave</span>
|
||||||
|
|||||||
@@ -52,7 +52,10 @@ export default function ColorKeyboard({
|
|||||||
backgroundColor: ["D", "E"].includes(note) ? "#000000" : "#ffffff",
|
backgroundColor: ["D", "E"].includes(note) ? "#000000" : "#ffffff",
|
||||||
color: ["D", "E"].includes(note) ? "#ffffff" : "#000000",
|
color: ["D", "E"].includes(note) ? "#ffffff" : "#000000",
|
||||||
}
|
}
|
||||||
: { backgroundColor: color }
|
: {
|
||||||
|
backgroundColor: color,
|
||||||
|
color: ["D", "E"].includes(note) ? "#000000" : "#ffffff",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{note}
|
{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";
|
import * as Tone from "tone";
|
||||||
|
|
||||||
export type SynthNodes = {
|
export type SynthNodes = {
|
||||||
@@ -45,3 +48,86 @@ export function disposeSynth(nodes: SynthNodes) {
|
|||||||
nodes.noise.dispose();
|
nodes.noise.dispose();
|
||||||
nodes.gain.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