Skip to content

Commit e47c911

Browse files
committed
Move transport into viz panel, fix playhead disappearing on restart
Play/Stop buttons now sit between the canvas and the Presets row in the visualisation panel, where they're closest to what they control. Bug fix: pressing Play while a note was sweeping caused the playhead to vanish. The previous oscillator's onended handler was unconditionally clearing the shared playStart/playDuration state — even after the new note had already populated it. Guard the clear so it only fires when the ended oscillator is still the active one.
1 parent 99a276d commit e47c911

5 files changed

Lines changed: 170 additions & 5 deletions

File tree

index.html

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,19 @@ <h2>Sound parameters</h2>
3636
<input type="text" class="basic-line" id="sound-line" spellcheck="false" autocomplete="off" />
3737
<button class="copy-btn" data-copy="sound-line" title="Copy SOUND statement" aria-label="Copy SOUND statement"></button>
3838
</div>
39-
<button id="play">Play</button>
40-
<button id="stop">Stop</button>
4139
</section>
4240
</div>
4341

4442
<div class="viz-col">
4543
<section class="panel viz-panel">
4644
<h2>Visualisation</h2>
4745
<canvas id="viz" width="900" height="320"></canvas>
46+
<div class="transport">
47+
<button id="play">Play</button>
48+
<button id="stop">Stop</button>
49+
</div>
50+
<h3>Presets</h3>
51+
<div class="presets" id="presets"></div>
4852
</section>
4953
</div>
5054
</div>

src/audio.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,14 @@ export function play(samples: Sample[], basePitch: number): void {
4949
playStart = t0;
5050
playDuration = samples.length * CS;
5151
osc.onended = () => {
52-
if (activeNodes && activeNodes.osc === osc) activeNodes = null;
53-
playStart = null;
54-
playDuration = null;
52+
// Only clear shared playback state if this osc is still the active one;
53+
// otherwise a freshly-started note would have its timing wiped when the
54+
// previously-stopped osc's onended fired late.
55+
if (activeNodes && activeNodes.osc === osc) {
56+
activeNodes = null;
57+
playStart = null;
58+
playDuration = null;
59+
}
5560
};
5661
}
5762

src/main.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from "./envelope";
1111
import { render } from "./visualizer";
1212
import { play, playheadFraction, stop } from "./audio";
13+
import { PRESETS, type Preset } from "./presets";
1314

1415
interface FieldSpec {
1516
key: keyof Envelope;
@@ -189,6 +190,33 @@ soundLine.addEventListener("input", () => {
189190
refresh("sound");
190191
});
191192

193+
function loadPreset(p: Preset): void {
194+
Object.assign(env, p.env);
195+
Object.assign(sound, p.sound);
196+
for (const f of ENV_FIELDS) {
197+
const input = envInputs.get(f.key);
198+
if (input) input.value = String(env[f.key]);
199+
}
200+
for (const f of SOUND_FIELDS) {
201+
const input = soundInputs.get(f.key);
202+
if (input) input.value = String(sound[f.key]);
203+
}
204+
envelopeLine.classList.remove("invalid");
205+
soundLine.classList.remove("invalid");
206+
refresh();
207+
}
208+
209+
const presetsContainer = document.getElementById("presets") as HTMLElement;
210+
for (const p of PRESETS) {
211+
const btn = document.createElement("button");
212+
btn.className = "preset-btn";
213+
btn.type = "button";
214+
btn.textContent = p.name;
215+
btn.title = p.description;
216+
btn.addEventListener("click", () => loadPreset(p));
217+
presetsContainer.appendChild(btn);
218+
}
219+
192220
document.getElementById("play")!.addEventListener("click", () => {
193221
play(currentSamples, sound.pitch);
194222
animatePlayhead();

src/presets.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import type { Envelope } from "./envelope";
2+
3+
export interface SoundPreset {
4+
channel: number;
5+
amplitude: number;
6+
pitch: number;
7+
duration: number;
8+
}
9+
10+
export interface Preset {
11+
name: string;
12+
description: string;
13+
env: Envelope;
14+
sound: SoundPreset;
15+
}
16+
17+
// `T` values >= 128 enable auto-repeat of the pitch envelope (bit 7 set).
18+
// 132 = 128 + 4 cs per step, 134 = 128 + 6, etc.
19+
export const PRESETS: Preset[] = [
20+
{
21+
name: "Piano",
22+
description: "Quick attack, gentle decay, slow fade through sustain",
23+
env: { n: 1, t: 1, pi1: 0, pi2: 0, pi3: 0, pn1: 0, pn2: 0, pn3: 0,
24+
aa: 127, ad: -2, as: -1, ar: -10, ala: 126, ald: 80 },
25+
sound: { channel: 1, amplitude: 1, pitch: 100, duration: 40 },
26+
},
27+
{
28+
name: "Pluck",
29+
description: "Snappy string-like pluck",
30+
env: { n: 1, t: 1, pi1: 0, pi2: 0, pi3: 0, pn1: 0, pn2: 0, pn3: 0,
31+
aa: 127, ad: -8, as: -1, ar: -20, ala: 126, ald: 40 },
32+
sound: { channel: 1, amplitude: 1, pitch: 100, duration: 30 },
33+
},
34+
{
35+
name: "Bell",
36+
description: "Sharp attack, long ringing decay",
37+
env: { n: 1, t: 1, pi1: 0, pi2: 0, pi3: 0, pn1: 0, pn2: 0, pn3: 0,
38+
aa: 127, ad: -1, as: -1, ar: -8, ala: 126, ald: 110 },
39+
sound: { channel: 1, amplitude: 1, pitch: 130, duration: 80 },
40+
},
41+
{
42+
name: "Pad",
43+
description: "Slow rising drone",
44+
env: { n: 1, t: 1, pi1: 0, pi2: 0, pi3: 0, pn1: 0, pn2: 0, pn3: 0,
45+
aa: 5, ad: -1, as: 0, ar: -3, ala: 100, ald: 90 },
46+
sound: { channel: 1, amplitude: 1, pitch: 100, duration: 100 },
47+
},
48+
{
49+
name: "Bass",
50+
description: "Punchy low-end thump",
51+
env: { n: 1, t: 1, pi1: 0, pi2: 0, pi3: 0, pn1: 0, pn2: 0, pn3: 0,
52+
aa: 80, ad: -3, as: -1, ar: -15, ala: 110, ald: 70 },
53+
sound: { channel: 1, amplitude: 1, pitch: 60, duration: 30 },
54+
},
55+
{
56+
name: "Drum",
57+
description: "Short percussive hit",
58+
env: { n: 1, t: 1, pi1: 0, pi2: 0, pi3: 0, pn1: 0, pn2: 0, pn3: 0,
59+
aa: 127, ad: -30, as: -20, ar: -30, ala: 126, ald: 40 },
60+
sound: { channel: 1, amplitude: 1, pitch: 30, duration: 5 },
61+
},
62+
{
63+
name: "Vibrato",
64+
description: "Looping ±½-semitone wobble around the note",
65+
env: { n: 1, t: 130, pi1: 1, pi2: -1, pi3: 1, pn1: 2, pn2: 4, pn3: 2,
66+
aa: 127, ad: -2, as: -1, ar: -10, ala: 126, ald: 100 },
67+
sound: { channel: 1, amplitude: 1, pitch: 100, duration: 60 },
68+
},
69+
{
70+
name: "Trill",
71+
description: "Looping alternation between two notes a whole step apart",
72+
env: { n: 1, t: 136, pi1: 8, pi2: -8, pi3: 0, pn1: 1, pn2: 1, pn3: 0,
73+
aa: 127, ad: -2, as: 0, ar: -10, ala: 126, ald: 100 },
74+
sound: { channel: 1, amplitude: 1, pitch: 100, duration: 60 },
75+
},
76+
{
77+
name: "Siren",
78+
description: "Looping octave sweep up and back down",
79+
env: { n: 1, t: 132, pi1: 2, pi2: -2, pi3: 0, pn1: 24, pn2: 24, pn3: 0,
80+
aa: 127, ad: -2, as: 0, ar: -2, ala: 126, ald: 100 },
81+
sound: { channel: 1, amplitude: 1, pitch: 80, duration: 80 },
82+
},
83+
{
84+
name: "Arpeggio",
85+
description: "Looping major triad: root → 3rd → 5th",
86+
env: { n: 1, t: 138, pi1: 16, pi2: 12, pi3: -28, pn1: 1, pn2: 1, pn3: 1,
87+
aa: 127, ad: -2, as: 0, ar: -10, ala: 126, ald: 100 },
88+
sound: { channel: 1, amplitude: 1, pitch: 80, duration: 100 },
89+
},
90+
{
91+
name: "Laser",
92+
description: "Fast downward pitch sweep — classic zap",
93+
env: { n: 1, t: 1, pi1: -12, pi2: -4, pi3: -1, pn1: 8, pn2: 8, pn3: 8,
94+
aa: 127, ad: -1, as: -2, ar: -30, ala: 126, ald: 100 },
95+
sound: { channel: 1, amplitude: 1, pitch: 200, duration: 20 },
96+
},
97+
{
98+
name: "Wobble",
99+
description: "Slow looping pitch wobble, sustained pad timbre",
100+
env: { n: 1, t: 140, pi1: 2, pi2: -2, pi3: 0, pn1: 4, pn2: 4, pn3: 0,
101+
aa: 20, ad: -1, as: 0, ar: -5, ala: 110, ald: 90 },
102+
sound: { channel: 1, amplitude: 1, pitch: 90, duration: 100 },
103+
},
104+
];

src/style.css

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,27 @@ canvas {
158158
display: block;
159159
border-radius: 4px;
160160
}
161+
162+
.transport {
163+
display: flex;
164+
gap: 8px;
165+
margin: 12px 0;
166+
}
167+
168+
.presets {
169+
display: flex;
170+
flex-wrap: wrap;
171+
gap: 6px;
172+
}
173+
.preset-btn {
174+
background: #21262d;
175+
color: #c9d1d9;
176+
border: 1px solid #30363d;
177+
border-radius: 4px;
178+
padding: 4px 10px;
179+
margin: 0;
180+
font: inherit;
181+
font-size: 0.8rem;
182+
cursor: pointer;
183+
}
184+
.preset-btn:hover { background: #30363d; border-color: #58a6ff; }

0 commit comments

Comments
 (0)