Skip to content
Open
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
25 changes: 25 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"
}
35 changes: 35 additions & 0 deletions src/components/PresetControls.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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)
);
};

return (
<div className="preset-controls" style={{ marginTop: "1rem" }}>
<button onClick={() => savePreset(synthState)}>💾 Save Preset</button>
<button onClick={handleLoadClick}>📂 Load Preset</button>
<input
type="file"
accept="application/json"
ref={fileInputRef}
onChange={handleFileChange}
style={{ display: "none" }}
/>
</div>
);
}
32 changes: 30 additions & 2 deletions src/pages/Sonara.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from 'react';
import EQ from '../components/Equalizers';
import Keys from '../components/Keys';
import ADSR from '../components/Adsr';
import PresetControls from '../components/PresetControls';
import { Voice } from '../audio/Voice';

import { applyEnvelope } from '../utils/applyEnvelope';
Expand All @@ -10,12 +11,28 @@ function Sonara() {
const displayWidth = 800;
const displayHeight = 350;

const [adsr, setAdsr] = useState({ attack: 0.1, decay: 0.2, sustain: 0.7, release: 0.5 });
const DEFAULT_ADSR = { attack: 0.1, decay: 0.2, sustain: 0.7, release: 0.5 };
const DEFAULT_EQ = { nodes: [], curves: [] };
const DEFAULT_WAVEFORM = 'sine';
const DEFAULT_OCTAVE = 4;

const [adsr, setAdsr] = useState(DEFAULT_ADSR);
const [eq, setEq] = useState(DEFAULT_EQ);
const [waveform, setWaveform] = useState(DEFAULT_WAVEFORM);
const [octave, setOctave] = useState(DEFAULT_OCTAVE);
const [wasmModule, setWasmModule] = useState(null);
const [rawwave, setRawwave] = useState([]);

const audioContextRef = useRef(null);
const voicesRef = useRef({});
const [eq, setEq] = useState({ nodes: [], curves: [] });

const synthState = {
adsr,
eq,
waveform,
octave,
rawwave,
};

useEffect(() => {
async function initWasmModule() {
Expand Down Expand Up @@ -66,9 +83,20 @@ function Sonara() {
}
};

const handlePresetLoad = (preset) => {
if (preset.adsr) setAdsr(preset.adsr);
if (preset.eq) setEq(preset.eq);
if (preset.waveform) setWaveform(preset.waveform);
if (preset.octave) setOctave(preset.octave);
if (preset.rawwave) setRawwave(preset.rawwave);
};

return (
<div className="App">
<h1>Sonara</h1>

<PresetControls synthState={synthState} onPresetLoad={handlePresetLoad} />

<Keys onNoteDown={handleNoteDown} onNoteUp={handleNoteUp} />
<ADSR adsr={adsr} setAdsr={setAdsr} />
<EQ
Expand Down
17 changes: 16 additions & 1 deletion src/styles/display.css
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,19 @@
.display-canvas {
border-width: 1px !important;
}
}
}

// new style adding
.preset-controls button {
background-color: #1f1f1f;
color: #fff;
border: none;
padding: 8px 14px;
border-radius: 6px;
cursor: pointer;
transition: 0.2s ease;
}

.preset-controls button:hover {
background-color: #3c3c3c;
}
34 changes: 34 additions & 0 deletions src/utils/presetManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// handles JSON export/import logic safely
export function savePreset(synthState) {
try {
const json = JSON.stringify(synthState, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const a = document.createElement('a');
a.href = url;
a.download = `preset_${timestamp}.json`;
a.click();
URL.revokeObjectURL(url);
alert("✅ Preset saved successfully!");
} catch (error) {
console.error("Error saving preset:", error);
alert("❌ Failed to save preset.");
}
}

export function loadPreset(file, onLoad, onError) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const preset = JSON.parse(e.target.result);
// Basic validation
if (!preset.adsr || !preset.eq) throw new Error("Invalid preset format");
onLoad(preset);
} catch (err) {
console.error("Error loading preset:", err);
onError?.(err.message);
}
};
reader.readAsText(file);
}