mirror of
https://github.com/hex248/tsos.git
synced 2026-02-07 18:23:05 +00:00
map shape to audio with tone.js
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
import ShapeCanvas from "@/components/canvas/ShapeCanvas";
|
||||
import PresetSelector from "@/components/controls/PresetSelector";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { Toggle } from "@/components/ui/toggle";
|
||||
import { useAudioContext } from "@/hooks/useAudioContext";
|
||||
import { useShapeState } from "@/hooks/useShapeState";
|
||||
import { useSynth } from "@/hooks/useSynth";
|
||||
import { mapGrainToNoise, mapPresetToOscType, mapRoundnessToFade, mapSizeToGain } from "@/lib/audio/mapping";
|
||||
import { useEffect, useState } from "react";
|
||||
import * as Tone from "tone";
|
||||
import Layout from "./Layout";
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
@@ -28,9 +33,30 @@ function Index() {
|
||||
const centerY = dimensions.height / 2;
|
||||
|
||||
const [state, setState] = useShapeState(centerX, centerY);
|
||||
const { isMuted, toggleMute } = useAudioContext();
|
||||
const synthRef = useSynth();
|
||||
|
||||
useEffect(() => {
|
||||
if (!synthRef.current) return;
|
||||
|
||||
const nodes = synthRef.current;
|
||||
nodes.oscillatorA.type = mapPresetToOscType(state.preset);
|
||||
nodes.crossFade.fade.value = mapRoundnessToFade(state.roundness);
|
||||
nodes.gain.gain.value = Tone.dbToGain(mapSizeToGain(state.size));
|
||||
|
||||
const grain = mapGrainToNoise(state.grain);
|
||||
const noiseDb = grain === 0 ? Number.NEGATIVE_INFINITY : -40 + (-12 - -40) * grain;
|
||||
nodes.noise.volume.value = noiseDb;
|
||||
}, [state.preset, state.roundness, state.size, state.grain, synthRef]);
|
||||
|
||||
const sidebarContent = (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-sm font-medium">Audio</span>
|
||||
<Toggle pressed={!isMuted} onPressedChange={toggleMute} variant="outline">
|
||||
{isMuted ? "Unmute" : "Mute"}
|
||||
</Toggle>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-sm font-medium">Shape</span>
|
||||
<PresetSelector value={state.preset} onChange={(preset) => setState({ ...state, preset })} />
|
||||
@@ -79,6 +105,15 @@ function Index() {
|
||||
onValueChange={([v]) => setState({ ...state, wobbleRandomness: v })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-sm font-medium">Noise</span>
|
||||
<Slider
|
||||
value={[state.grain]}
|
||||
min={0}
|
||||
max={100}
|
||||
onValueChange={([v]) => setState({ ...state, grain: v })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
32
src/hooks/useAudioContext.ts
Normal file
32
src/hooks/useAudioContext.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { getDestination, start } from "tone";
|
||||
|
||||
export function useAudioContext() {
|
||||
const [isMuted, setIsMuted] = useState(true);
|
||||
const destination = getDestination();
|
||||
|
||||
useEffect(() => {
|
||||
destination.mute = true;
|
||||
|
||||
return () => {
|
||||
destination.mute = true;
|
||||
};
|
||||
}, [destination]);
|
||||
|
||||
const toggleMute = useCallback(async () => {
|
||||
if (isMuted) {
|
||||
await start();
|
||||
destination.mute = false;
|
||||
setIsMuted(false);
|
||||
return;
|
||||
}
|
||||
|
||||
destination.mute = true;
|
||||
setIsMuted(true);
|
||||
}, [destination, isMuted]);
|
||||
|
||||
return {
|
||||
isMuted,
|
||||
toggleMute,
|
||||
};
|
||||
}
|
||||
20
src/hooks/useSynth.ts
Normal file
20
src/hooks/useSynth.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { SynthNodes } from "@/lib/audio/synth";
|
||||
import { createSynth, disposeSynth } from "@/lib/audio/synth";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export function useSynth() {
|
||||
const synthRef = useRef<SynthNodes | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
synthRef.current = createSynth();
|
||||
|
||||
return () => {
|
||||
if (synthRef.current) {
|
||||
disposeSynth(synthRef.current);
|
||||
synthRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return synthRef;
|
||||
}
|
||||
31
src/lib/audio/mapping.ts
Normal file
31
src/lib/audio/mapping.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { Preset } from "@/types/shape";
|
||||
|
||||
export function mapPresetToOscType(preset: Preset): "sawtooth" | "square" | "sine" {
|
||||
switch (preset) {
|
||||
case "triangle":
|
||||
return "sawtooth";
|
||||
case "square":
|
||||
return "square";
|
||||
case "circle":
|
||||
return "sine";
|
||||
}
|
||||
}
|
||||
|
||||
export function mapRoundnessToFade(roundness: number): number {
|
||||
return clamp01(roundness / 100);
|
||||
}
|
||||
|
||||
export function mapSizeToGain(size: number): number {
|
||||
const minDb = -30;
|
||||
const maxDb = -6;
|
||||
const t = clamp01(size / 100);
|
||||
return minDb + (maxDb - minDb) * t;
|
||||
}
|
||||
|
||||
export function mapGrainToNoise(grain: number): number {
|
||||
return clamp01(grain / 100);
|
||||
}
|
||||
|
||||
function clamp01(value: number) {
|
||||
return Math.min(1, Math.max(0, value));
|
||||
}
|
||||
Reference in New Issue
Block a user