Skip to content

Commit 9171338

Browse files
committed
Refactor host-testable modules and surface audio init errors
1 parent 4fc968c commit 9171338

File tree

7 files changed

+337
-319
lines changed

7 files changed

+337
-319
lines changed

src/events/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
#[cfg(target_arch = "wasm32")]
12
pub mod keyboard;
23
pub mod keymap;
4+
#[cfg(target_arch = "wasm32")]
35
pub mod pointer;
46

7+
#[cfg(target_arch = "wasm32")]
58
pub use keyboard::{wire_global_keydown, wire_overlay_toggle_h};
9+
#[cfg(target_arch = "wasm32")]
610
pub use pointer::{wire_input_handlers, InputWiring};

src/input.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
use glam::{Vec2, Vec3};
1+
#[cfg(target_arch = "wasm32")]
2+
use glam::Vec2;
3+
use glam::Vec3;
4+
#[cfg(target_arch = "wasm32")]
25
use wasm_bindgen::JsCast;
6+
#[cfg(target_arch = "wasm32")]
37
use web_sys as web;
48

59
#[derive(Default, Clone, Copy)]
@@ -33,6 +37,7 @@ pub fn ray_sphere(ray_origin: Vec3, ray_dir: Vec3, center: Vec3, radius: f32) ->
3337
}
3438

3539
#[inline]
40+
#[cfg(target_arch = "wasm32")]
3641
pub fn pointer_canvas_px(ev: &web::PointerEvent, canvas: &web::HtmlCanvasElement) -> Vec2 {
3742
let el: web::Element = canvas.clone().unchecked_into();
3843
let rect = el.get_bounding_client_rect();
@@ -44,6 +49,7 @@ pub fn pointer_canvas_px(ev: &web::PointerEvent, canvas: &web::HtmlCanvasElement
4449
}
4550

4651
#[inline]
52+
#[cfg(target_arch = "wasm32")]
4753
pub fn pointer_canvas_uv(ev: &web::PointerEvent, canvas: &web::HtmlCanvasElement) -> [f32; 2] {
4854
let el: web::Element = canvas.clone().unchecked_into();
4955
let rect = el.get_bounding_client_rect();
@@ -61,6 +67,7 @@ pub fn pointer_canvas_uv(ev: &web::PointerEvent, canvas: &web::HtmlCanvasElement
6167
}
6268

6369
#[inline]
70+
#[cfg(target_arch = "wasm32")]
6471
pub fn mouse_uv(canvas: &web::HtmlCanvasElement, mouse: &MouseState) -> [f32; 2] {
6572
let w = canvas.width().max(1) as f32;
6673
let h = canvas.height().max(1) as f32;

src/lib.rs

Lines changed: 14 additions & 286 deletions
Original file line numberDiff line numberDiff line change
@@ -1,295 +1,23 @@
1-
#![cfg(target_arch = "wasm32")]
2-
use crate::core::{EngineParams, MusicEngine, VoiceConfig, Waveform, C_MAJOR_PENTATONIC};
3-
use glam::Vec3;
4-
use instant::Instant;
5-
use std::cell::RefCell;
6-
use std::rc::Rc;
7-
use std::sync::atomic::{AtomicBool, Ordering};
8-
use wasm_bindgen::prelude::*;
9-
use wasm_bindgen::JsCast;
10-
use wasm_bindgen_futures::spawn_local;
11-
use web_sys as web;
1+
pub mod core;
2+
pub mod events;
3+
pub mod input;
124

5+
#[cfg(target_arch = "wasm32")]
136
mod audio;
7+
#[cfg(target_arch = "wasm32")]
148
mod camera;
9+
#[cfg(target_arch = "wasm32")]
1510
mod constants;
16-
mod core;
11+
#[cfg(target_arch = "wasm32")]
1712
mod dom;
18-
mod events;
13+
#[cfg(target_arch = "wasm32")]
1914
mod frame;
20-
mod input;
15+
#[cfg(target_arch = "wasm32")]
2116
mod overlay;
17+
#[cfg(target_arch = "wasm32")]
2218
mod render;
19+
#[cfg(target_arch = "wasm32")]
20+
mod wasm_app;
2321

24-
fn wire_canvas_resize(canvas: &web::HtmlCanvasElement) {
25-
dom::sync_canvas_backing_size(canvas);
26-
let canvas_resize = canvas.clone();
27-
let resize_closure = Closure::wrap(Box::new(move || {
28-
dom::sync_canvas_backing_size(&canvas_resize);
29-
}) as Box<dyn FnMut()>);
30-
if let Some(window) = web::window() {
31-
_ = window
32-
.add_event_listener_with_callback("resize", resize_closure.as_ref().unchecked_ref());
33-
}
34-
resize_closure.forget();
35-
}
36-
37-
struct InitParts {
38-
audio_ctx: web::AudioContext,
39-
listener_for_tick: web::AudioListener,
40-
engine: Rc<RefCell<MusicEngine>>,
41-
paused: Rc<RefCell<bool>>,
42-
}
43-
44-
async fn build_audio_and_engine(_document: web::Document) -> anyhow::Result<InitParts> {
45-
let audio_ctx = web::AudioContext::new().map_err(|e| anyhow::anyhow!("{:?}", e))?;
46-
_ = audio_ctx.resume();
47-
let listener = audio_ctx.listener();
48-
listener.set_position(0.0, 0.0, 1.5);
49-
50-
let voice_configs = vec![
51-
VoiceConfig {
52-
waveform: Waveform::Sine,
53-
base_position: Vec3::new(-1.0, 0.0, 0.0),
54-
trigger_probability: 0.4,
55-
octave_offset: -1,
56-
base_duration: 0.4,
57-
},
58-
VoiceConfig {
59-
waveform: Waveform::Saw,
60-
base_position: Vec3::new(1.0, 0.0, 0.0),
61-
trigger_probability: 0.6,
62-
octave_offset: 0,
63-
base_duration: 0.25,
64-
},
65-
VoiceConfig {
66-
waveform: Waveform::Triangle,
67-
base_position: Vec3::new(0.0, 0.0, -1.0),
68-
trigger_probability: 0.3,
69-
octave_offset: 1,
70-
base_duration: 0.6,
71-
},
72-
];
73-
let engine = Rc::new(RefCell::new(MusicEngine::new(
74-
voice_configs,
75-
EngineParams {
76-
bpm: 110.0,
77-
scale: C_MAJOR_PENTATONIC,
78-
root_midi: 60,
79-
detune_cents: 0.0,
80-
},
81-
42,
82-
)));
83-
{
84-
let e = engine.borrow();
85-
log::info!(
86-
"[engine] voices={} pos0=({:.2},{:.2},{:.2}) pos1=({:.2},{:.2},{:.2}) pos2=({:.2},{:.2},{:.2})",
87-
e.voices.len(),
88-
e.voices[0].position.x, e.voices[0].position.y, e.voices[0].position.z,
89-
e.voices[1].position.x, e.voices[1].position.y, e.voices[1].position.z,
90-
e.voices[2].position.x, e.voices[2].position.y, e.voices[2].position.z
91-
);
92-
}
93-
let paused = Rc::new(RefCell::new(true));
94-
Ok(InitParts {
95-
audio_ctx,
96-
listener_for_tick: listener,
97-
engine,
98-
paused,
99-
})
100-
}
101-
102-
fn wire_overlay_buttons(audio_ctx: &web::AudioContext, paused: &Rc<RefCell<bool>>) {
103-
if let Some(doc2) = dom::window_document() {
104-
let paused_ok = paused.clone();
105-
let audio_ok = audio_ctx.clone();
106-
dom::add_click_listener(&doc2, "overlay-ok", move || {
107-
*paused_ok.borrow_mut() = false;
108-
_ = audio_ok.resume();
109-
if let Some(w2) = web::window() {
110-
if let Some(d2) = w2.document() {
111-
overlay::hide(&d2);
112-
}
113-
}
114-
});
115-
116-
let paused_close = paused.clone();
117-
let audio_close = audio_ctx.clone();
118-
dom::add_click_listener(&doc2, "overlay-close", move || {
119-
*paused_close.borrow_mut() = false;
120-
_ = audio_close.resume();
121-
if let Some(w2) = web::window() {
122-
if let Some(d2) = w2.document() {
123-
overlay::hide(&d2);
124-
}
125-
}
126-
});
127-
}
128-
}
129-
130-
#[wasm_bindgen(start)]
131-
pub fn start() -> Result<(), JsValue> {
132-
console_error_panic_hook::set_once();
133-
console_log::init_with_level(log::Level::Info).ok();
134-
log::info!("app-web starting");
135-
136-
spawn_local(async move {
137-
if let Err(e) = init().await {
138-
log::error!("init error: {:?}", e);
139-
}
140-
});
141-
Ok(())
142-
}
143-
144-
async fn init() -> anyhow::Result<()> {
145-
let window = web::window().ok_or_else(|| anyhow::anyhow!("no window"))?;
146-
let document = window
147-
.document()
148-
.ok_or_else(|| anyhow::anyhow!("no document"))?;
149-
150-
let canvas_el = document
151-
.get_element_by_id("app-canvas")
152-
.ok_or_else(|| anyhow::anyhow!("missing #app-canvas"))?;
153-
let canvas: web::HtmlCanvasElement = canvas_el
154-
.dyn_into::<web::HtmlCanvasElement>()
155-
.map_err(|e| anyhow::anyhow!(format!("{:?}", e)))?;
156-
157-
// Note: start overlay is handled below (toggle with 'h') once audio is initialized.
158-
159-
// Avoid grabbing a 2D context here to allow WebGPU to acquire the canvas
160-
161-
// Maintain canvas internal pixel size to match CSS size * devicePixelRatio
162-
wire_canvas_resize(&canvas);
163-
164-
// Prepare a clone for use inside the click closure
165-
let canvas_for_click = canvas.clone();
166-
167-
// Start audio graph and scheduling + WebGPU renderer immediately; show overlay until OK/close
168-
static STARTED: AtomicBool = AtomicBool::new(false);
169-
{
170-
if STARTED.swap(true, Ordering::SeqCst) == false {
171-
let canvas_for_click_inner = canvas_for_click.clone();
172-
spawn_local(async move {
173-
let InitParts {
174-
audio_ctx,
175-
listener_for_tick,
176-
engine,
177-
paused,
178-
} = match build_audio_and_engine(document.clone()).await {
179-
Ok(p) => p,
180-
Err(_) => return,
181-
};
182-
183-
wire_overlay_buttons(&audio_ctx, &paused);
184-
events::wire_overlay_toggle_h(&document);
185-
186-
// FX buses
187-
let fx = match audio::build_fx_buses(&audio_ctx) {
188-
Ok(f) => f,
189-
Err(_) => return,
190-
};
191-
let master_gain = fx.master_gain.clone();
192-
let sat_pre = fx.sat_pre.clone();
193-
let sat_wet = fx.sat_wet.clone();
194-
let sat_dry = fx.sat_dry.clone();
195-
let reverb_in = fx.reverb_in.clone();
196-
let reverb_wet = fx.reverb_wet.clone();
197-
let delay_in = fx.delay_in.clone();
198-
let delay_feedback = fx.delay_feedback.clone();
199-
let delay_wet = fx.delay_wet.clone();
200-
201-
// Per-voice master gains -> master bus, plus effect sends
202-
let initial_positions: Vec<Vec3> =
203-
engine.borrow().voices.iter().map(|v| v.position).collect();
204-
let routing = match audio::wire_voices(
205-
&audio_ctx,
206-
&initial_positions,
207-
&master_gain,
208-
&delay_in,
209-
&reverb_in,
210-
) {
211-
Ok(r) => r,
212-
Err(_) => return,
213-
};
214-
let delay_sends = Rc::new(routing.delay_sends);
215-
let reverb_sends = Rc::new(routing.reverb_sends);
216-
let voice_panners = routing.voice_panners;
217-
let voice_gains = Rc::new(routing.voice_gains);
218-
219-
// Initialize WebGPU
220-
let gpu: Option<render::GpuState> = frame::init_gpu(&canvas_for_click_inner).await;
221-
222-
// Visual pulses per voice and optional analyser for ambient effects
223-
let pulses = Rc::new(RefCell::new(vec![0.0_f32; engine.borrow().voices.len()]));
224-
let (analyser, analyser_buf) = audio::create_analyser(&audio_ctx);
225-
226-
// Queued ripple UV from pointer taps (read by render tick)
227-
let queued_ripple_uv: Rc<RefCell<Option<[f32; 2]>>> = Rc::new(RefCell::new(None));
228-
229-
// ---------------- Interaction state ----------------
230-
let mouse_state = Rc::new(RefCell::new(input::MouseState::default()));
231-
let hover_index = Rc::new(RefCell::new(None::<usize>));
232-
let drag_state = Rc::new(RefCell::new(input::DragState::default()));
233-
234-
// Keyboard controls
235-
events::wire_global_keydown(
236-
engine.clone(),
237-
paused.clone(),
238-
master_gain.clone(),
239-
canvas_for_click_inner.clone(),
240-
);
241-
242-
// Pointer handlers (move/down/up)
243-
events::wire_input_handlers(events::InputWiring {
244-
canvas: canvas_for_click_inner.clone(),
245-
engine: engine.clone(),
246-
mouse_state: mouse_state.clone(),
247-
hover_index: hover_index.clone(),
248-
drag_state: drag_state.clone(),
249-
voice_gains: voice_gains.clone(),
250-
delay_sends: delay_sends.clone(),
251-
reverb_sends: reverb_sends.clone(),
252-
audio_ctx: audio_ctx.clone(),
253-
queued_ripple_uv: queued_ripple_uv.clone(),
254-
});
255-
256-
// Scheduler + renderer loop driven by requestAnimationFrame
257-
let frame_ctx = Rc::new(RefCell::new(frame::FrameContext {
258-
engine: engine.clone(),
259-
paused: paused.clone(),
260-
pulses: pulses.clone(),
261-
hover_index: hover_index.clone(),
262-
canvas: canvas_for_click_inner.clone(),
263-
mouse: mouse_state.clone(),
264-
audio_ctx: audio_ctx.clone(),
265-
listener: listener_for_tick.clone(),
266-
voice_gains: voice_gains.clone(),
267-
delay_sends: delay_sends.clone(),
268-
reverb_sends: reverb_sends.clone(),
269-
voice_panners,
270-
reverb_wet: reverb_wet.clone(),
271-
delay_wet: delay_wet.clone(),
272-
delay_feedback: delay_feedback.clone(),
273-
sat_pre: sat_pre.clone(),
274-
sat_wet: sat_wet.clone(),
275-
sat_dry: sat_dry.clone(),
276-
analyser: analyser.clone(),
277-
analyser_buf: analyser_buf.clone(),
278-
gpu,
279-
queued_ripple_uv: queued_ripple_uv.clone(),
280-
last_instant: Instant::now(),
281-
prev_uv: [0.5, 0.5],
282-
swirl_energy: 0.0,
283-
swirl_pos: [0.5, 0.5],
284-
swirl_vel: [0.0, 0.0],
285-
swirl_initialized: false,
286-
pulse_energy: [0.0, 0.0, 0.0],
287-
}));
288-
// Start RAF loop
289-
frame::start_loop(frame_ctx);
290-
});
291-
}
292-
}
293-
294-
Ok(())
295-
}
22+
#[cfg(target_arch = "wasm32")]
23+
pub use wasm_app::start;

0 commit comments

Comments
 (0)