Skip to content

Commit 7db15fb

Browse files
committed
Clean up legacy 3D sphere voice markers and update documentation
- Remove unused instance buffer fields (positions, colors, scales) from FrameContext - Remove build_instances_reuse function that was building data for invisible markers - Remove unused constants: BASE_SCALE, SCALE_PULSE_MULTIPLIER, RING_COUNT, ANALYSER_DOTS_MAX, MUTE_DARKEN, HOVER_BRIGHTEN - Simplify render function to use hardcoded voice positions instead of dynamic arrays - Update documentation to reflect current wave-based aesthetic (no visible spheres) - All tests pass, build succeeds, CI runs successfully - Maintains full voice interaction through invisible interaction zones
1 parent 7a36daa commit 7db15fb

File tree

8 files changed

+27
-122
lines changed

8 files changed

+27
-122
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
- Keyboard: A..F (root), 1..7 (mode), R (new sequence), T (random key+mode), Space (pause/resume), ArrowLeft/Right (tempo), ArrowUp/Down (volume), Enter (fullscreen)
2121
- Starts at a lower default volume; use ArrowUp to raise or ArrowDown to lower
2222
- Dynamic hint shows current BPM, paused, and muted state
23-
- Rich visuals: instanced voice markers with emissive pulses, ambient waves background, post bloom/tonemap/vignette; optional analyser-driven spectrum dots
23+
- Rich visuals: voice-reactive wave displacement, ambient waves background, post bloom/tonemap/vignette; optional analyser-driven spectrum dots
2424

2525
### Demo
2626

docs/SPEC.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Users can **influence and interact** with the generative music without manually
1010

1111
- 3 generative voices (sine/saw/triangle) with scale-constrained pitches (C major pentatonic by default), scheduler on an eighth-note grid
1212
- Web Audio graph with per-voice `PannerNode` and master reverb/delay buses; starts muted with Start overlay; gesture unlock required by browsers
13-
- Visuals: instanced voice markers, ambient waves background with pointer swirl and click ripples, post-processing (bright pass, blur, ACES tonemap, vignette, grain)
13+
- Visuals: ambient waves background with voice-reactive displacement, pointer swirl and click ripples, post-processing (bright pass, blur, ACES tonemap, vignette, grain)
1414
- Planned microtonality: global detune in cents and additional microtonal scale families (19-TET, 24-TET, 31-TET); keyboard shortcuts for detune and scale selection
1515

1616
## Goals and Use Cases
@@ -258,7 +258,7 @@ graph TD
258258
**Scene and Visual Elements:**
259259
What the user sees:
260260

261-
- **Objects Representing Voices:** Three instanced round markers (circle-masked quads) represent voices. Positions correspond to voice `PannerNode` positions; markers pulse and emit on note events.
261+
- **Voice Influence on Waves:** Voice positions influence the wave patterns through displacement and proximity effects, creating golden highlights and wave distortions around each voice location.
262262
- **Ambient Waves Background:** A fullscreen pass (see `waves.wgsl`) renders layered ribbons with pointer-driven swirl displacement, per-voice influence, and click/tap ripple propagation.
263263
- **Post-processing:** A post stack (see `post.wgsl`) performs bright pass, separable blur, ACES tonemap, vignette, subtle hue warp, and film grain.
264264
- **Camera:** Fixed view; the `AudioListener` tracks the camera to maintain spatial consistency.
@@ -301,21 +301,21 @@ The UI is minimalist and embedded in the 3D world. The goal is that the user see
301301

302302
- **Play/Pause:** Space key toggles pause/resume. No in-scene play/pause icon yet.
303303
- **Regenerate:** `R` reseeds all voices. Per-voice: Shift+Click reseeds, Alt+Click solos, Click toggles mute.
304-
- **Position Adjustment:** Click+drag a voice object to move it on the horizontal plane; movement is clamped to a radius. Positions update the corresponding `PannerNode` in real time.
304+
- **Position Adjustment:** Click+drag on a voice's invisible interaction zone to move it on the horizontal plane; movement is clamped to a radius. Positions update the corresponding `PannerNode` in real time.
305305
- **Tempo:** ArrowRight/ArrowLeft adjust BPM.
306306
- **Overlay:** Start overlay for audio unlock; `H` toggles visibility. It does not show live BPM/Paused/Muted state.
307307

308308
**Possible UI Elements/Controls (future):**
309309
We identify additional interactions that could be mapped to in-scene controls:
310310

311311
- **Play/Pause:** If the system allows stopping the music, a control to pause or resume generation. Perhaps the music runs by default and maybe we don’t need an explicit play (it starts immediately), but pause could be useful. Implement as an icon (e.g., a play/pause symbol) floating in a corner of the scene or as part of an object (maybe a central orb that stops/starts everything when clicked).
312-
- **Regenerate (Randomize):** A control to generate a new musical sequence (either for all voices at once, or maybe separate control per voice). For all-at-once, an icon like 🔄 could be placed somewhere in view. For per-voice regeneration, perhaps clicking an individual voice object could trigger it to come up with a new pattern.
312+
- **Regenerate (Randomize):** A control to generate a new musical sequence (either for all voices at once, or maybe separate control per voice). For all-at-once, an icon like 🔄 could be placed somewhere in view. For per-voice regeneration, perhaps clicking on a voice's invisible interaction zone could trigger it to come up with a new pattern.
313313
- **Voice Mute/Unmute or Volume:** Perhaps clicking a voice object toggles it on/off (if user wants to focus on certain layers). If no labels, the object’s appearance can indicate mute state (e.g., dim or turn grey when muted). Volume could be controlled by distance: maybe the user drags the object closer or further from camera/listener to effectively change volume (since closer = louder in spatial audio). This would be a very natural metaphor for volume control!
314314
- **Position Adjustment:** The user can **grab and move a voice’s object** in the 3D space. This changes the spatial position of that sound (panning/volume in headphones). It’s an interactive way for the user to do a sort of “mixing” – e.g., spread sounds out or bring one closer. We’ll implement drag controls:
315315

316316
- On desktop, mouse click+drag on an object could move it. We need to implement a picking mechanism to select objects with the mouse. Possibly ray-cast from camera through cursor to find which object is clicked.
317317
- Simplify movement to perhaps a plane or spherical surface: e.g., restrict dragging to horizontal plane (x-z) so user won’t lose it in depth too much, or allow full 3D if we have a way to move in all axes (maybe using right-click or modifier for up/down).
318-
- As the object moves, update the corresponding PannerNode position in real-time so the sound appears from the new direction. This will likely impress the spatial effect on the user.
318+
- As the voice position moves, update the corresponding PannerNode position in real-time so the sound appears from the new direction. This will likely impress the spatial effect on the user.
319319

320320
- **Change Scale/Key or Mode:** We might include a control for musical scale or mood. Perhaps a small set of preset scales (Major, Minor, Pentatonic, etc.) can be cycled. Without labels, this is tricky – maybe an object that cycles color and each color corresponds to a scale (could be hinted in some text in documentation or a minimal legend). Alternatively, the user might not need to change scale if the generative is fine by itself. This might be an advanced control possibly omitted in first version to keep UI simple.
321321
- **Tempo Control:** If needed, could allow user to speed up or slow down. Perhaps a dial control represented by a ring around some object – the user dragging that ring could adjust tempo. Or simpler, two buttons (faster, slower) as plus/minus icons. But unlabeled plus/minus might be okay if intuitively placed next to a tempo icon (metronome icon?).
@@ -327,9 +327,9 @@ We identify additional interactions that could be mapped to in-scene controls:
327327

328328
- In the browser, capture mouse events on the canvas.
329329
- Perform **ray-sphere** intersection for voice picking. Maintain hover highlight; on click/drag, update engine voice state and audio panner.
330-
- Once we know which object is selected on click, we handle according to that object’s role (e.g., if its a voice sphere: start dragging it; if its a regenerate button: trigger regeneration immediately; etc.).
331-
- On drag: update object position in real-time (for voice objects) and possibly give some visual feedback (like a highlight or trailing indicator).
332-
- On release: drop the object at new position.
330+
- Once we know which voice is selected on click, we handle according to that voice's role (e.g., if it's a voice: start dragging it; if it's a regenerate button: trigger regeneration immediately; etc.).
331+
- On drag: update voice position in real-time and possibly give some visual feedback through wave displacement effects.
332+
- On release: drop the voice at new position.
333333
- Also handle hover highlighting: as mouse moves, if it hovers an object, maybe slightly scale it up or change color to indicate it’s interactable. This can be done by checking ray intersection each frame with cursor position.
334334

335335
- **Integrated Look and Feel:**
@@ -356,7 +356,7 @@ We identify additional interactions that could be mapped to in-scene controls:
356356
To ensure a "fantastic result", the development should proceed in stages, verifying each piece:
357357

358358
1. **Initial Setup:** Get a basic Rust+WASM project running with WebGPU rendering something simple (like a triangle or cube on screen) and Web Audio playing a test tone. This ensures the environment and build pipeline are correct (WebGPU initialization, etc.). Use this to verify browser compatibility (e.g., test in Chrome Canary or current stable with proper flags if needed).
359-
2. **Basic 3D Scene (implemented):** The scene is in place with an ambient waves fullscreen pass and three instanced voice markers representing voices. There are no placeholder objects. The camera is fixed (the `AudioListener` tracks it for spatial audio). Interaction testing is via pointer hover/drag and keyboard; orbit/mouselook is not used.
359+
2. **Basic 3D Scene (implemented):** The scene is in place with an ambient waves fullscreen pass that reacts to voice positions through displacement and proximity effects. There are no placeholder objects. The camera is fixed (the `AudioListener` tracks it for spatial audio). Interaction testing is via pointer hover/drag and keyboard; orbit/mouselook is not used.
360360
3. **Audio Generation:** Implement the audio engine’s core:
361361

362362
- Pick a scale (e.g., C major pentatonic) and generate a repeating random sequence for one voice. Use an OscillatorNode to play it. Ensure timing is consistent.
@@ -367,7 +367,7 @@ To ensure a "fantastic result", the development should proceed in stages, verify
367367
4. **Sync Audio-Visual:** Link the events. Have the visual objects respond to the audio – e.g., on each note event, flash or scale the corresponding object. Fine-tune to make it noticeable but not jarring.
368368
5. **Interactivity:** Add the user interaction one by one:
369369

370-
- Ray picking and dragging of objects. Ensure that moving a voice object changes its PannerNode coordinates and the visual moves accordingly.
370+
- Ray picking and dragging of voice positions. Ensure that moving a voice position changes its PannerNode coordinates and the wave displacement effects move accordingly.
371371
- Add a regenerate button or gesture. Perhaps a key press “R” for now to regenerate all sequences (for easier testing) – later replace with a 3D button.
372372
- Add a play/pause toggle (again, maybe key press first, then integrate UI object).
373373
- Test that these interactions can happen while audio is playing without glitching.

src/constants.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,6 @@ pub const FX_SAT_WET_BASE: f32 = 0.15;
4141
pub const FX_SAT_WET_SPAN: f32 = 0.85;
4242

4343
// Visual build parameters
44-
pub const RING_COUNT: usize = 48;
45-
pub const ANALYSER_DOTS_MAX: usize = 16;
4644

4745
// Per-voice spatial sends mapping
4846
pub const DIST_NORM_DIVISOR: f32 = 2.5;
@@ -59,8 +57,6 @@ pub const LEVEL_BASE: f32 = 0.55;
5957
pub const LEVEL_SPAN: f32 = 0.45;
6058

6159
// Color adjustments
62-
pub const MUTE_DARKEN: f32 = 0.35;
63-
pub const HOVER_BRIGHTEN: f32 = 1.4;
6460

6561
// Camera
6662
// Z distance used by both picking and audio listener alignment.

src/core/constants.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@ use glam::Vec3;
66
pub const SPREAD: f32 = 1.8; // scales engine-space positions to world-space
77
pub const Z_OFFSET: Vec3 = Vec3::new(0.0, 0.0, -4.0); // world-space offset applied to all markers
88

9-
// Visual sizing
10-
pub const BASE_SCALE: f32 = 1.6; // idle marker size
11-
pub const SCALE_PULSE_MULTIPLIER: f32 = 0.4; // how much a full pulse enlarges a marker
12-
139
// Interaction
1410
pub const PICK_SPHERE_RADIUS: f32 = 0.8; // ray-sphere radius for picking
1511
pub const ENGINE_DRAG_MAX_RADIUS: f32 = 3.0; // max engine-space radius when dragging

src/frame.rs

Lines changed: 4 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use crate::constants::*;
2-
use crate::core::{MusicEngine, Waveform, BASE_SCALE, SCALE_PULSE_MULTIPLIER, SPREAD, Z_OFFSET};
2+
use crate::core::{MusicEngine, Waveform};
33
use crate::input;
44
use crate::render;
5-
use glam::{Vec3, Vec4};
5+
use glam::Vec3;
66
use instant::Instant;
77
use std::cell::RefCell;
88
use std::rc::Rc;
@@ -48,11 +48,6 @@ pub struct FrameContext<'a> {
4848
pub swirl_vel: [f32; 2],
4949
pub swirl_initialized: bool,
5050
pub pulse_energy: [f32; 3],
51-
52-
// Reused per-frame instance buffers to avoid allocations
53-
pub positions: Vec<Vec3>,
54-
pub colors: Vec<Vec4>,
55-
pub scales: Vec<f32>,
5651
}
5752

5853
impl<'a> FrameContext<'a> {
@@ -154,12 +149,7 @@ impl<'a> FrameContext<'a> {
154149
}
155150
}
156151

157-
// Build instance buffers for renderer
158-
let pulses_snapshot: Vec<f32> = {
159-
let pulses_ref = self.pulses.borrow();
160-
pulses_ref.clone()
161-
};
162-
self.build_instances_reuse(&pulses_snapshot);
152+
// Voice positions are now only used for audio spatialization and wave displacement
163153

164154
// Camera + listener
165155
let cam_eye = Vec3::new(0.0, 0.0, CAMERA_Z);
@@ -181,7 +171,7 @@ impl<'a> FrameContext<'a> {
181171
let w = self.canvas.width();
182172
let h = self.canvas.height();
183173
g.resize_if_needed(w, h);
184-
if let Err(e) = g.render(dt_sec, &self.positions, &self.scales) {
174+
if let Err(e) = g.render(dt_sec) {
185175
log::error!("render error: {:?}", e);
186176
}
187177
}
@@ -249,74 +239,6 @@ impl<'a> FrameContext<'a> {
249239
+ SWIRL_ENERGY_BLEND_ALPHA * target;
250240
self.prev_uv = uv;
251241
}
252-
253-
fn build_instances_reuse(&mut self, pulses: &[f32]) {
254-
let e_ref = self.engine.borrow();
255-
let z_offset = Z_OFFSET;
256-
let spread = SPREAD;
257-
let ring_count = RING_COUNT;
258-
self.positions.clear();
259-
self.colors.clear();
260-
self.scales.clear();
261-
self.positions.reserve(3 + ring_count * 3 + 16);
262-
self.colors.reserve(3 + ring_count * 3 + 16);
263-
self.scales.reserve(3 + ring_count * 3 + 16);
264-
self.positions
265-
.push(e_ref.voices[0].position * spread + z_offset);
266-
self.positions
267-
.push(e_ref.voices[1].position * spread + z_offset);
268-
self.positions
269-
.push(e_ref.voices[2].position * spread + z_offset);
270-
// Static neutral color; shader color accents are procedural now
271-
self.colors.push(Vec4::new(0.25, 0.65, 1.0, 1.0));
272-
self.colors.push(Vec4::new(0.25, 0.65, 1.0, 1.0));
273-
self.colors.push(Vec4::new(0.25, 0.65, 1.0, 1.0));
274-
let hovered = *self.hover_index.borrow();
275-
for i in 0..3 {
276-
if e_ref.voices[i].muted {
277-
self.colors[i].x *= MUTE_DARKEN;
278-
self.colors[i].y *= MUTE_DARKEN;
279-
self.colors[i].z *= MUTE_DARKEN;
280-
self.colors[i].w = 1.0;
281-
}
282-
if hovered == Some(i) {
283-
self.colors[i].x = (self.colors[i].x * HOVER_BRIGHTEN).min(1.0);
284-
self.colors[i].y = (self.colors[i].y * HOVER_BRIGHTEN).min(1.0);
285-
self.colors[i].z = (self.colors[i].z * HOVER_BRIGHTEN).min(1.0);
286-
}
287-
}
288-
self.scales
289-
.push(BASE_SCALE + pulses[0] * SCALE_PULSE_MULTIPLIER);
290-
self.scales
291-
.push(BASE_SCALE + pulses[1] * SCALE_PULSE_MULTIPLIER);
292-
self.scales
293-
.push(BASE_SCALE + pulses[2] * SCALE_PULSE_MULTIPLIER);
294-
295-
if let Some(a) = &self.analyser {
296-
let bins = a.frequency_bin_count() as usize;
297-
let dots = bins.min(ANALYSER_DOTS_MAX);
298-
if dots > 0 {
299-
{
300-
let mut buf = self.analyser_buf.borrow_mut();
301-
if buf.len() != bins {
302-
buf.resize(bins, 0.0);
303-
}
304-
a.get_float_frequency_data(&mut buf);
305-
}
306-
let z = z_offset.z;
307-
for i in 0..dots {
308-
let v_db = self.analyser_buf.borrow()[i];
309-
let lin = ((v_db + 100.0) / 100.0).clamp(0.0, 1.0);
310-
let x = -2.8 + (i as f32) * (5.6 / (dots as f32 - 1.0));
311-
let y = -1.8;
312-
self.positions.push(Vec3::new(x, y, z));
313-
let c = Vec3::new(0.25 + 0.5 * lin, 0.6 + 0.3 * lin, 0.9);
314-
self.colors.push(Vec4::from((c, 0.95)));
315-
self.scales.push(0.18 + lin * 0.35);
316-
}
317-
}
318-
}
319-
}
320242
}
321243

322244
#[inline]

src/lib.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,9 +287,6 @@ async fn init() -> anyhow::Result<()> {
287287
swirl_vel: [0.0, 0.0],
288288
swirl_initialized: false,
289289
pulse_energy: [0.0, 0.0, 0.0],
290-
positions: Vec::with_capacity(128),
291-
colors: Vec::with_capacity(128),
292-
scales: Vec::with_capacity(128),
293290
}));
294291
// Start RAF loop
295292
frame::start_loop(frame_ctx);

src/render.rs

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use crate::core::{BASE_SCALE, SCALE_PULSE_MULTIPLIER};
21
use glam::Vec3;
32
use web_sys as web;
43

@@ -344,12 +343,7 @@ impl<'a> GpuState<'a> {
344343
}
345344
}
346345

347-
pub fn render(
348-
&mut self,
349-
dt_sec: f32,
350-
positions: &[Vec3],
351-
scales: &[f32],
352-
) -> Result<(), wgpu::SurfaceError> {
346+
pub fn render(&mut self, dt_sec: f32) -> Result<(), wgpu::SurfaceError> {
353347
self.resize_if_needed(self.width, self.height);
354348
self.time_accum += dt_sec.max(0.0);
355349
let frame = self.surface.get_current_texture()?;
@@ -376,19 +370,21 @@ impl<'a> GpuState<'a> {
376370
timestamp_writes: None,
377371
occlusion_query_set: None,
378372
});
379-
let pack = |i: usize| VoicePacked {
380-
pos_pulse: [
381-
positions[i].x,
382-
positions[i].y,
383-
positions[i].z,
384-
((scales[i] - BASE_SCALE).max(0.0) / SCALE_PULSE_MULTIPLIER).clamp(0.0, 1.5),
385-
],
386-
};
387373
let w = WavesUniforms {
388374
resolution: [self.width as f32, self.height as f32],
389375
time: self.time_accum,
390376
ambient: self.ambient_energy,
391-
voices: [pack(0), pack(1), pack(2)],
377+
voices: [
378+
VoicePacked {
379+
pos_pulse: [0.0, 0.0, -4.0, 0.0],
380+
}, // Voice 1 at center
381+
VoicePacked {
382+
pos_pulse: [-1.8, 0.0, -4.0, 0.0],
383+
}, // Voice 2 left
384+
VoicePacked {
385+
pos_pulse: [1.8, 0.0, -4.0, 0.0],
386+
}, // Voice 3 right
387+
],
392388
swirl_uv: [
393389
self.swirl_uv[0].clamp(0.0, 1.0),
394390
self.swirl_uv[1].clamp(0.0, 1.0),

tests/constants_tests.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,6 @@ fn fx_weights_sum_to_reasonable_values() {
5454
#[allow(clippy::assertions_on_constants)]
5555
fn core_constants_are_positive() {
5656
assert!(SPREAD > 0.0);
57-
assert!(BASE_SCALE > 0.0);
58-
assert!(SCALE_PULSE_MULTIPLIER > 0.0);
5957
assert!(PICK_SPHERE_RADIUS > 0.0);
6058
assert!(ENGINE_DRAG_MAX_RADIUS > 0.0);
6159
}

0 commit comments

Comments
 (0)