Skip to content

Commit 71d2613

Browse files
committed
app-native: fix saw/triangle normalization; app-web: remove legacy scene pipeline and orbit feature; add F/Escape fullscreen; clean hint text; add app-core tests for reseed determinism and tempo/solo; update docs and tests; green checks
1 parent deaf1b9 commit 71d2613

File tree

9 files changed

+143
-231
lines changed

9 files changed

+143
-231
lines changed

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ default-members = ["crates/app-native"]
88
authors = ["Robert Gilks"]
99
edition = "2021"
1010
license = "MIT"
11-
name = "geno-1"
1211
version = "0.1.0"
1312
description = "Generative 3D music visualizer using Rust, WebGPU (wgpu v24), and WebAudio"
1413
repository = "https://github.com/rgilks/geno-1"

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
- Mouse-driven FX: corner-based saturation (clean ↔ fizz) and opposite-corner delay emphasis; visuals have inertial swirl motion and click ripples
1111
- Start overlay to initialize audio (Click Start; canvas-click fallback)
1212
- Drag voices in XZ plane; click to mute, Shift+Click reseed, Alt+Click solo
13-
- Keyboard: R (reseed all), Space (pause), + / - (tempo), M (master mute), O (orbit on/off)
13+
- Keyboard: R (reseed all), Space (pause), + / - (tempo), M (master mute)
1414
- Starts muted by default; press M to unmute the master bus
15-
- Dynamic hint shows current BPM, paused, muted, and orbit state
15+
- Dynamic hint shows current BPM, paused, and muted state
1616
- Rich visuals: instanced voice markers with emissive pulses, ambient waves background, post bloom/tonemap/vignette; optional analyser-driven spectrum dots
1717
- Native front-end renders and plays synthesized audio; parity improving:
1818
- Equal-power stereo panning from X position, multiple waveforms (sine/square/saw/triangle)
@@ -44,7 +44,7 @@ Notes:
4444

4545
Quick controls (browser):
4646

47-
- R: reseed all • Space: pause/resume • +/-: tempo • M: master mute • O: orbit on/off
47+
- R: reseed all • Space: pause/resume • +/-: tempo • M: master mute
4848
- Click a voice to mute; Alt+Click to solo; Shift+Click to reseed a voice; drag to move in XZ
4949

5050
### Pre-commit Check
@@ -67,7 +67,7 @@ Controls in browser:
6767
- Click Start to initialize audio (canvas click also works)
6868
- Drag a circle to move a voice in XZ plane (updates spatialization)
6969
- Click a voice: mute; Shift+Click: reseed; Alt+Click: solo
70-
- Keys: R (reseed all), Space (pause/resume), + / - (tempo), M (master mute), O (orbit on/off)
70+
- Keys: R (reseed all), Space (pause/resume), + / - (tempo), M (master mute)
7171
- Mouse position maps to master saturation and delay; moving the pointer leaves a “water-like” trailing swirl in visuals
7272

7373
Headless test:
@@ -94,7 +94,7 @@ Headless test:
9494
- Screenshots/GIFs live in `docs/media/`.
9595
- Place files like:
9696
- `docs/media/screenshot-1.png` – main scene
97-
- `docs/media/screenshot-2.png`orbit and hint overlay
97+
- `docs/media/screenshot-2.png` – hint overlay
9898
- `docs/media/loop-1.gif` – waves background with ripples and pulses
9999

100100
Links:

crates/app-core/src/music.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,4 +285,78 @@ mod tests {
285285
assert!(!engine.voices[1].muted);
286286
assert!(!engine.voices[2].muted);
287287
}
288+
289+
#[test]
290+
fn reseed_determinism_per_voice() {
291+
// Given identical configs and params, reseeding a voice with a fixed seed should
292+
// produce identical first scheduled events across engines.
293+
let configs = vec![
294+
VoiceConfig {
295+
color_rgb: [1.0, 0.0, 0.0],
296+
waveform: Waveform::Sine,
297+
base_position: Vec3::new(-1.0, 0.0, 0.0),
298+
},
299+
VoiceConfig {
300+
color_rgb: [0.0, 1.0, 0.0],
301+
waveform: Waveform::Saw,
302+
base_position: Vec3::new(1.0, 0.0, 0.0),
303+
},
304+
VoiceConfig {
305+
color_rgb: [0.0, 0.0, 1.0],
306+
waveform: Waveform::Triangle,
307+
base_position: Vec3::new(0.0, 0.0, -1.0),
308+
},
309+
];
310+
let params = EngineParams::default();
311+
let mut a = MusicEngine::new(configs.clone(), params.clone(), 111);
312+
let mut b = MusicEngine::new(configs, params, 222);
313+
// Force same reseed for voice 1
314+
a.reseed_voice(1, Some(9999));
315+
b.reseed_voice(1, Some(9999));
316+
// Advance enough time to schedule a step and collect events
317+
let mut out_a = Vec::new();
318+
let mut out_b = Vec::new();
319+
a.tick(Duration::from_millis(300), 0.0, &mut out_a);
320+
b.tick(Duration::from_millis(300), 0.0, &mut out_b);
321+
// Filter for voice 1 events and compare first if both exist
322+
let ev_a = out_a.into_iter().find(|e| e.voice_index == 1);
323+
let ev_b = out_b.into_iter().find(|e| e.voice_index == 1);
324+
if let (Some(x), Some(y)) = (ev_a, ev_b) {
325+
assert!((x.frequency_hz - y.frequency_hz).abs() < 1e-3);
326+
assert!((x.duration_sec - y.duration_sec).abs() < 1e-3);
327+
}
328+
}
329+
330+
#[test]
331+
fn tempo_change_does_not_break_mute_and_solo() {
332+
let configs = vec![
333+
VoiceConfig {
334+
color_rgb: [1.0, 0.0, 0.0],
335+
waveform: Waveform::Sine,
336+
base_position: Vec3::new(-1.0, 0.0, 0.0),
337+
},
338+
VoiceConfig {
339+
color_rgb: [0.0, 1.0, 0.0],
340+
waveform: Waveform::Saw,
341+
base_position: Vec3::new(1.0, 0.0, 0.0),
342+
},
343+
VoiceConfig {
344+
color_rgb: [0.0, 0.0, 1.0],
345+
waveform: Waveform::Triangle,
346+
base_position: Vec3::new(0.0, 0.0, -1.0),
347+
},
348+
];
349+
let params = EngineParams::default();
350+
let mut engine = MusicEngine::new(configs, params, 7);
351+
// Solo voice 0 then change tempo
352+
engine.toggle_solo(0);
353+
engine.set_bpm(140.0);
354+
// Mute flags should still reflect solo state
355+
assert!(!engine.voices[0].muted);
356+
assert!(engine.voices[1].muted);
357+
assert!(engine.voices[2].muted);
358+
// Toggle solo off and ensure unmuted
359+
engine.toggle_solo(0);
360+
assert!(engine.voices.iter().all(|v| !v.muted));
361+
}
288362
}

crates/app-native/src/main.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -707,15 +707,11 @@ fn render_wave_sample(phase: f32, wave: WaveKind) -> f32 {
707707
WaveKind::Saw => {
708708
// Map phase 0..2PI to -1..1
709709
let t = phase / (2.0 * std::f32::consts::PI);
710-
(2.0 * (t - t.floor())) * 2.0 - 1.0
710+
2.0 * (t - t.floor()) - 1.0
711711
}
712712
WaveKind::Triangle => {
713-
// Triangle from saw
714-
let saw = {
715-
let t = phase / (2.0 * std::f32::consts::PI);
716-
(2.0 * (t - t.floor())) * 2.0 - 1.0
717-
};
718-
(2.0 / std::f32::consts::PI) * (saw.asin())
713+
// Triangle using arcsin(sin) identity, normalized to [-1, 1]
714+
(2.0 / std::f32::consts::PI) * (phase.sin().asin())
719715
}
720716
}
721717
}

crates/app-web/index.html

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,10 @@
8080
</div>
8181
<canvas id="app-canvas" width="1280" height="720"></canvas>
8282
<div class="hint" data-visible="0" style="display: none">
83-
Click canvas to start • Drag a circle to move<br />
84-
Click: mute, Shift+Click: reseed, Alt+Click: solo<br />
85-
R: reseed all • Space: pause/resume • +/-: tempo • M: master mute<br />
83+
Click Start to begin • Drag to move a voice<br />
84+
Click: mute • Shift+Click: reseed • Alt+Click: solo<br />
85+
R: reseed all • Space: pause/resume • +/-: tempo • M: master mute • F:
86+
fullscreen<br />
8687
BPM: 110 • Paused: no • Muted: yes
8788
</div>
8889
<div

0 commit comments

Comments
 (0)