diff --git a/src/Index.tsx b/src/Index.tsx
index 279721f..c2fd096 100644
--- a/src/Index.tsx
+++ b/src/Index.tsx
@@ -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 = (
+
+ Audio
+
+ {isMuted ? "Unmute" : "Mute"}
+
+
Shape
setState({ ...state, preset })} />
@@ -79,6 +105,15 @@ function Index() {
onValueChange={([v]) => setState({ ...state, wobbleRandomness: v })}
/>
+
+ Noise
+ setState({ ...state, grain: v })}
+ />
+
);
diff --git a/src/hooks/useAudioContext.ts b/src/hooks/useAudioContext.ts
new file mode 100644
index 0000000..538e4fc
--- /dev/null
+++ b/src/hooks/useAudioContext.ts
@@ -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,
+ };
+}
diff --git a/src/hooks/useSynth.ts b/src/hooks/useSynth.ts
new file mode 100644
index 0000000..c061c3c
--- /dev/null
+++ b/src/hooks/useSynth.ts
@@ -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(null);
+
+ useEffect(() => {
+ synthRef.current = createSynth();
+
+ return () => {
+ if (synthRef.current) {
+ disposeSynth(synthRef.current);
+ synthRef.current = null;
+ }
+ };
+ }, []);
+
+ return synthRef;
+}
diff --git a/src/lib/audio/mapping.ts b/src/lib/audio/mapping.ts
new file mode 100644
index 0000000..718546a
--- /dev/null
+++ b/src/lib/audio/mapping.ts
@@ -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));
+}