Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
</head>
<body>
<div id="root"></div>
<script src="./dsp.js"></script>
<script src="/dsp.js"></script>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
29 changes: 29 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file added public/favicon.ico
Binary file not shown.
8 changes: 8 additions & 0 deletions public/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "Sonara Synth",
"short_name": "Sonara",
"start_url": "/",
"display": "standalone",
"background_color": "#000",
"theme_color": "#00ff88"
}
5 changes: 4 additions & 1 deletion src/audio/Voice.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
* from note-on to note-off, including the ADSR envelope.
*/
export class Voice {
constructor(audioContext, wasmModule, frequencies, adsr) {
constructor(audioContext, wasmModule, frequencies, adsr, waveform = 'sine', octave = 4) {
this.audioContext = audioContext;
this.wasmModule = wasmModule;
this.frequencies = frequencies;
this.adsr = adsr;
this.waveform = waveform;
this.octave = octave;

// Create the GainNode for ADSR volume control
this.gainNode = this.audioContext.createGain();
Expand All @@ -26,6 +28,7 @@ export class Voice {
const freqAmpPairs = new this.wasmModule.VectorVectorDouble();
this.frequencies.forEach(([freq, amp]) => {
const pair = new this.wasmModule.VectorDouble();
const adjustedFreq = freq * Math.pow(2, this.octave - 4);
pair.push_back(freq);
pair.push_back(amp);
freqAmpPairs.push_back(pair);
Expand Down
95 changes: 84 additions & 11 deletions src/components/Equalizers.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@

import { useState, useEffect, useRef } from 'react';
import { useState, useMemo } from 'react';
import Display from './Display';

Expand All @@ -10,21 +12,92 @@ import Display from './Display';
* width: number,
* height: number}}
*/
function EQ({ wasmModule, width, height, freqs: liveFreqs }) {
function EQ({ wasmModule, width, height, freqs: liveFreqs, eq, setEq }) {
const xRange = [20, 20000];

// Initialize nodes and curves for the EQ
const logXMin = Math.log(xRange[0]);
const logXMax = Math.log(xRange[1]);
const initialNodes = Array.from({ length: 5 }, (_, i) => ({
x: Math.exp(logXMin + (i / 4) * (logXMax - logXMin)),
y: 0.8
}));
const initialCurves = Array(initialNodes.length - 1).fill(0);
const initialNodes = useRef(
eq?.nodes?.length
? eq.nodes
: Array.from({ length: 5 }, (_, i) => ({
x: Math.exp(Math.log(xRange[0]) + (i / 4) * (Math.log(xRange[1]) - Math.log(xRange[0]))),
y: 0.8,
}))
).current;

const initialCurves = useRef(eq?.curves?.length ? eq.curves : Array(initialNodes.length - 1).fill(0)).current;

const [nodes, setNodesLocal] = useState(initialNodes);
const [curves, setCurvesLocal] = useState(initialCurves);

const lastParentEqRef = useRef(JSON.stringify(eq || {}));

// When local nodes/curves change (user edits), push to parent only if different
useEffect(() => {
const parentSerialized = lastParentEqRef.current;
const localSerialized = JSON.stringify({ nodes, curves });
if (parentSerialized !== localSerialized) {
setEq({ nodes, curves });
lastParentEqRef.current = localSerialized;
}
}, [nodes, curves, setEq]);

// When parent eq prop changes (preset load), update local nodes/curves but only if really different
useEffect(() => {
if (!eq) return;
const parentSerialized = JSON.stringify(eq);
const localSerialized = JSON.stringify({ nodes, curves });
if (parentSerialized !== localSerialized) {
if (eq.nodes) setNodesLocal(eq.nodes);
if (eq.curves) setCurvesLocal(eq.curves);
lastParentEqRef.current = parentSerialized;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [eq]);

const processedFreqs = wasmModule
? (() => {
const nodesVec = new wasmModule.VectorNode();
nodes.forEach((node) => nodesVec.push_back(node));

const curvesVec = new wasmModule.VectorDouble();
curves.forEach((curve) => curvesVec.push_back(curve));

const freqsVec = new wasmModule.VectorVectorDouble();
(liveFreqs || []).forEach(([f, a]) => {
const pair = new wasmModule.VectorDouble();
pair.push_back(f);
pair.push_back(a);
freqsVec.push_back(pair);
pair.delete();
});

const result = wasmModule.applyEnvelope(nodesVec, curvesVec, freqsVec);

nodesVec.delete();
curvesVec.delete();
freqsVec.delete();

const [nodes, setNodes] = useState(initialNodes);
const [curves, setCurves] = useState(initialCurves);
return result;
})()
: [];

return (
<div className="EQ">
<h3>Frequency EQ</h3>
<Display
width={width}
height={height}
nodes={nodes}
xRange={xRange}
curves={curves}
onNodesChange={setNodesLocal}
onCurvesChange={setCurvesLocal}
freqs={processedFreqs}
isLogarithmic={true}
wasmModule={wasmModule}
/>
</div>
);
const processedFreqs = useMemo(() => {
if (!wasmModule || !liveFreqs) return [];

Expand Down
5 changes: 2 additions & 3 deletions src/components/Keys.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import { createWaveform } from '../utils/createWaveform';
import { noteFrequencies, keyMap, notes } from '../constants/keys';
import { waveimages } from '../constants/path';

function Keys({ onNoteDown, onNoteUp }) {
const [waveform, setWaveform] = useState('sawtooth');
const [octave, setOctave] = useState(4);
function Keys({ onNoteDown, onNoteUp, waveform, setWaveform, octave, setOctave }) {

const [activeKeys, setActiveKeys] = useState(new Set());

// Handle key press to play a note
Expand Down
99 changes: 99 additions & 0 deletions src/components/PresetControls.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React, { useRef } from "react";
import { savePreset, loadPreset } from "../utils/presetManager";

export default function PresetControls({ synthState, onPresetLoad }) {
const fileInputRef = useRef();

const handleLoadClick = () => fileInputRef.current.click();

const handleFileChange = (e) => {
const file = e.target.files[0];
if (!file) return;
loadPreset(
file,
(preset) => {
onPresetLoad(preset);
alert("✅ Preset loaded successfully!");
},
(error) => alert("❌ Error loading preset: " + error)
);
};

const handleReset = () => {
const defaultPreset = {
waveform: "sine",
octave: 4,
adsr: { attack: 0.1, decay: 0.2, sustain: 0.7, release: 0.3 },
eq: {
nodes: [
{ x: 20, y: 0.8 },
{ x: 200, y: 0.8 },
{ x: 1000, y: 0.8 },
{ x: 5000, y: 0.8 },
{ x: 20000, y: 0.8 }
],
curves: [0, 0, 0, 0]
}
};
onPresetLoad(defaultPreset);
alert("🔄 Reset to default settings!");
};

return (
<div
className="preset-controls"
style={{ marginTop: "1rem", display: "flex", gap: "10px", flexWrap: "wrap" }}
>
<button
onClick={() => savePreset(synthState)}
style={{
padding: "8px 12px",
borderRadius: "6px",
border: "1px solid #444",
background: "#1e1e1e",
color: "#fff",
cursor: "pointer",
}}
>
💾 Save Preset
</button>

<button
onClick={handleLoadClick}
style={{
padding: "8px 12px",
borderRadius: "6px",
border: "1px solid #444",
background: "#1e1e1e",
color: "#fff",
cursor: "pointer",
}}
>
📂 Load Preset
</button>

<button
onClick={handleReset}
style={{
padding: "8px 12px",
borderRadius: "6px",
border: "1px solid #444",
background: "#1e1e1e",
color: "#fff",
cursor: "pointer",
}}
>
🔄 Reset
</button>

<input
type="file"
accept="application/json"
ref={fileInputRef}
onChange={handleFileChange}
style={{ display: "none" }}
/>
</div>
);

}
Loading