1use bytemuck::{Pod, Zeroable};
17use wgpu::util::DeviceExt;
18
19use crate::config::WaveParams;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26#[repr(u32)]
27pub enum WaveformMode {
28 Circle = 0,
29 XyOscopeOctagonal = 1,
30 XyOscopeQuadrilateral = 2,
31 Blob = 3,
32 DerivativeLine = 4,
33 ExplosiveHash = 5,
34 DoubleLine = 6,
35 DoubleHorizontal = 7,
36}
37
38impl WaveformMode {
39 pub fn from_i32(v: i32) -> Self {
42 match v {
43 0 => Self::Circle,
44 1 => Self::XyOscopeOctagonal,
45 2 => Self::XyOscopeQuadrilateral,
46 3 => Self::Blob,
47 4 => Self::DerivativeLine,
48 5 => Self::ExplosiveHash,
49 6 => Self::DoubleLine,
50 7 => Self::DoubleHorizontal,
51 _ => Self::Circle,
52 }
53 }
54
55 pub fn is_closed(self) -> bool {
57 matches!(self, Self::Circle | Self::Blob)
58 }
59
60 pub fn is_double_pass(self) -> bool {
63 matches!(self, Self::DoubleLine)
64 }
65}
66
67pub const NUM_WAVE_SAMPLES: usize = 512;
71
72const DEFAULT_SMOOTHING_ALPHA: f32 = 0.0;
76
77#[repr(C)]
80#[derive(Debug, Clone, Copy, Pod, Zeroable)]
81pub struct WaveUniforms {
82 pub resolution: [f32; 2],
83 pub time: f32,
84 pub mode: u32,
85
86 pub wave_x: f32,
87 pub wave_y: f32,
88 pub wave_scale: f32,
89 pub wave_alpha: f32,
90
91 pub wave_param: f32,
92 pub aspect: f32,
93 pub thickness: f32,
94 pub smoothing: f32,
95
96 pub wave_color: [f32; 4],
97
98 pub num_samples: u32,
99 pub is_thick: u32,
100 pub is_dots: u32,
101 pub sample_offset: u32,
108}
109
110impl Default for WaveUniforms {
111 fn default() -> Self {
112 Self {
113 resolution: [1.0, 1.0],
114 time: 0.0,
115 mode: 0,
116 wave_x: 0.5,
117 wave_y: 0.5,
118 wave_scale: 0.5,
119 wave_alpha: 1.0,
120 wave_param: 0.0,
121 aspect: 1.0,
122 thickness: 0.004,
123 smoothing: 0.0,
124 wave_color: [1.0, 1.0, 1.0, 1.0],
125 num_samples: NUM_WAVE_SAMPLES as u32,
126 is_thick: 0,
127 is_dots: 0,
128 sample_offset: 0,
129 }
130 }
131}
132
133#[repr(C)]
138#[derive(Debug, Clone, Copy, Pod, Zeroable)]
139pub struct WavePoint {
140 pub position: [f32; 2],
141 pub value: f32,
142 pub _padding: f32,
143}
144
145pub struct WaveformRenderer {
148 pipelines: [wgpu::RenderPipeline; 4],
151
152 bind_group: wgpu::BindGroup,
153 uniform_buffer: wgpu::Buffer,
154 sample_buffer: wgpu::Buffer,
155
156 last_uniforms: WaveUniforms,
158}
159
160#[inline]
161fn pipeline_index(is_dots: bool, is_additive: bool) -> usize {
162 (is_additive as usize) | ((is_dots as usize) << 1)
163}
164
165impl WaveformRenderer {
166 pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
169 let shader = crate::pipeline_helpers::load_wgsl(
170 device,
171 "Waveform Shader",
172 include_str!("../shaders/waveform_advanced.wgsl"),
173 );
174
175 let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
176 label: Some("Waveform Uniforms"),
177 size: std::mem::size_of::<WaveUniforms>() as u64,
178 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
179 mapped_at_creation: false,
180 });
181
182 let zeros = [0.0f32; NUM_WAVE_SAMPLES * 2];
188 let sample_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
189 label: Some("Waveform Samples"),
190 contents: bytemuck::cast_slice(&zeros),
191 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
192 });
193
194 let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
195 label: Some("Waveform BGL"),
196 entries: &[
197 wgpu::BindGroupLayoutEntry {
198 binding: 0,
199 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
200 ty: wgpu::BindingType::Buffer {
201 ty: wgpu::BufferBindingType::Uniform,
202 has_dynamic_offset: false,
203 min_binding_size: None,
204 },
205 count: None,
206 },
207 wgpu::BindGroupLayoutEntry {
208 binding: 1,
209 visibility: wgpu::ShaderStages::VERTEX,
210 ty: wgpu::BindingType::Buffer {
211 ty: wgpu::BufferBindingType::Storage { read_only: true },
212 has_dynamic_offset: false,
213 min_binding_size: None,
214 },
215 count: None,
216 },
217 ],
218 });
219
220 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
221 label: Some("Waveform BG"),
222 layout: &bgl,
223 entries: &[
224 wgpu::BindGroupEntry {
225 binding: 0,
226 resource: uniform_buffer.as_entire_binding(),
227 },
228 wgpu::BindGroupEntry {
229 binding: 1,
230 resource: sample_buffer.as_entire_binding(),
231 },
232 ],
233 });
234
235 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
236 label: Some("Waveform Layout"),
237 bind_group_layouts: &[Some(&bgl)],
238 immediate_size: 0,
239 });
240
241 let make_pipeline = |label: &str, additive: bool| -> wgpu::RenderPipeline {
242 let blend = crate::pipeline_helpers::blend_state_for(additive);
243 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
244 label: Some(label),
245 layout: Some(&layout),
246 vertex: wgpu::VertexState {
247 module: &shader,
248 entry_point: Some("vs_main"),
249 buffers: &[],
250 compilation_options: Default::default(),
251 },
252 fragment: Some(wgpu::FragmentState {
253 module: &shader,
254 entry_point: Some("fs_main"),
255 targets: &[Some(wgpu::ColorTargetState {
256 format,
257 blend: Some(blend),
258 write_mask: wgpu::ColorWrites::ALL,
259 })],
260 compilation_options: Default::default(),
261 }),
262 primitive: wgpu::PrimitiveState {
263 topology: wgpu::PrimitiveTopology::TriangleList,
264 ..Default::default()
265 },
266 depth_stencil: None,
267 multisample: wgpu::MultisampleState::default(),
268 multiview_mask: None,
269 cache: None,
270 })
271 };
272
273 let pipelines = [
274 make_pipeline("Wave Lines Alpha", false),
275 make_pipeline("Wave Lines Additive", true),
276 make_pipeline("Wave Dots Alpha", false),
277 make_pipeline("Wave Dots Additive", true),
278 ];
279
280 Self {
281 pipelines,
282 bind_group,
283 uniform_buffer,
284 sample_buffer,
285 last_uniforms: WaveUniforms::default(),
286 }
287 }
288
289 pub fn update_wave_samples(&self, queue: &wgpu::Queue, samples: &[f32]) {
299 let mut buf = [0.0f32; NUM_WAVE_SAMPLES * 2];
300 let n = samples.len().min(NUM_WAVE_SAMPLES);
301 buf[..n].copy_from_slice(&samples[..n]);
302 buf[NUM_WAVE_SAMPLES..NUM_WAVE_SAMPLES + n].copy_from_slice(&samples[..n]);
303 queue.write_buffer(&self.sample_buffer, 0, bytemuck::cast_slice(&buf));
304 }
305
306 pub fn update_wave_samples_lr(&self, queue: &wgpu::Queue, left: &[f32], right: &[f32]) {
313 let mut buf = [0.0f32; NUM_WAVE_SAMPLES * 2];
314 let nl = left.len().min(NUM_WAVE_SAMPLES);
315 let nr = right.len().min(NUM_WAVE_SAMPLES);
316 buf[..nl].copy_from_slice(&left[..nl]);
317 buf[NUM_WAVE_SAMPLES..NUM_WAVE_SAMPLES + nr].copy_from_slice(&right[..nr]);
318 queue.write_buffer(&self.sample_buffer, 0, bytemuck::cast_slice(&buf));
319 }
320
321 pub fn update_uniforms(&mut self, queue: &wgpu::Queue, u: &WaveUniforms) {
323 self.last_uniforms = *u;
324 queue.write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(u));
325 }
326
327 pub fn render(&self, encoder: &mut wgpu::CommandEncoder, view: &wgpu::TextureView) {
331 let mode = WaveformMode::from_i32(self.last_uniforms.mode as i32);
332 let is_dots = self.last_uniforms.is_dots != 0 || mode == WaveformMode::ExplosiveHash;
333 let is_additive = false; self.render_with_blend(encoder, view, is_dots, is_additive);
338 }
339
340 pub fn render_with_blend(
344 &self,
345 encoder: &mut wgpu::CommandEncoder,
346 view: &wgpu::TextureView,
347 is_dots: bool,
348 is_additive: bool,
349 ) {
350 let mode = WaveformMode::from_i32(self.last_uniforms.mode as i32);
351 let num_samples = self.last_uniforms.num_samples;
352 if num_samples == 0 {
353 return;
354 }
355 let segments = if mode.is_closed() {
358 num_samples
359 } else {
360 num_samples.saturating_sub(1)
361 };
362 let vertex_count = segments * 6;
363
364 let pidx = pipeline_index(is_dots, is_additive);
365 let pipeline = &self.pipelines[pidx];
366
367 let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
368 label: Some("Waveform Pass"),
369 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
370 view,
371 depth_slice: None,
372 resolve_target: None,
373 ops: wgpu::Operations {
374 load: wgpu::LoadOp::Load,
375 store: wgpu::StoreOp::Store,
376 },
377 })],
378 depth_stencil_attachment: None,
379 timestamp_writes: None,
380 occlusion_query_set: None,
381 multiview_mask: None,
382 });
383 rp.set_pipeline(pipeline);
384 rp.set_bind_group(0, &self.bind_group, &[]);
385 rp.draw(0..vertex_count, 0..1);
386 }
387}
388
389pub fn apply_smoothing(samples: &mut [f32], smoothing: f32) {
398 let s = smoothing.clamp(0.0, 1.0);
399 if s <= DEFAULT_SMOOTHING_ALPHA {
400 return;
401 }
402 let alpha = (s * 0.833).clamp(0.0, 0.95);
404 let mut prev = samples.first().copied().unwrap_or(0.0);
405 for s in samples.iter_mut() {
406 let next = prev * alpha + *s * (1.0 - alpha);
407 *s = next;
408 prev = next;
409 }
410}
411
412pub fn build_uniforms(
417 params: WaveParams,
418 instantaneous_vol: f32,
419 width: u32,
420 height: u32,
421 time: f32,
422) -> WaveUniforms {
423 let aspect = width as f32 / height.max(1) as f32;
424 let alpha_mod = if params.mod_alpha_by_volume {
425 let v = instantaneous_vol.clamp(0.0, 1.0);
426 let span = (params.mod_alpha_end - params.mod_alpha_start)
427 .abs()
428 .max(1e-3);
429 ((v - params.mod_alpha_start) / span).clamp(0.0, 1.0)
430 } else {
431 1.0
432 };
433 let mut color = [params.r, params.g, params.b, params.a * alpha_mod];
434 if params.maximize_color {
435 let m = color[0].max(color[1]).max(color[2]).max(1e-3);
438 color[0] /= m;
439 color[1] /= m;
440 color[2] /= m;
441 }
442 let thickness = if params.thick { 0.0075 } else { 0.0035 };
443 WaveUniforms {
444 resolution: [width as f32, height as f32],
445 time,
446 mode: WaveformMode::from_i32(params.mode) as u32,
447 wave_x: params.x,
448 wave_y: params.y,
449 wave_scale: params.scale,
450 wave_alpha: params.a,
451 wave_param: params.param,
452 aspect,
453 thickness,
454 smoothing: params.smoothing,
455 wave_color: color,
456 num_samples: NUM_WAVE_SAMPLES as u32,
457 is_thick: params.thick as u32,
458 is_dots: params.dots as u32,
459 sample_offset: 0,
460 }
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466
467 #[test]
468 fn waveform_mode_from_i32_known() {
469 assert_eq!(WaveformMode::from_i32(0), WaveformMode::Circle);
470 assert_eq!(WaveformMode::from_i32(1), WaveformMode::XyOscopeOctagonal);
471 assert_eq!(WaveformMode::from_i32(7), WaveformMode::DoubleHorizontal);
472 }
473
474 #[test]
475 fn waveform_mode_from_i32_clamps_out_of_range() {
476 assert_eq!(WaveformMode::from_i32(-1), WaveformMode::Circle);
477 assert_eq!(WaveformMode::from_i32(99), WaveformMode::Circle);
478 assert_eq!(WaveformMode::from_i32(8), WaveformMode::Circle);
479 }
480
481 #[test]
482 fn waveform_mode_is_closed_for_circle_and_blob() {
483 assert!(WaveformMode::Circle.is_closed());
484 assert!(WaveformMode::Blob.is_closed());
485 assert!(!WaveformMode::DoubleLine.is_closed());
486 assert!(!WaveformMode::DerivativeLine.is_closed());
487 }
488
489 #[test]
490 fn smoothing_zero_is_passthrough() {
491 let mut s = vec![1.0_f32, -1.0, 1.0, -1.0];
492 let orig = s.clone();
493 apply_smoothing(&mut s, 0.0);
494 assert_eq!(s, orig);
495 }
496
497 #[test]
498 fn smoothing_attenuates_oscillation() {
499 let mut s = vec![1.0_f32, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0];
500 apply_smoothing(&mut s, 0.9);
501 let max = s.iter().cloned().fold(f32::MIN, f32::max);
504 let min = s.iter().cloned().fold(f32::MAX, f32::min);
505 assert!(
506 (max - min) < 1.5,
507 "expected smoothed wave to attenuate (got peak-to-peak {})",
508 max - min
509 );
510 }
511
512 #[test]
513 fn build_uniforms_maps_fields() {
514 let params = WaveParams {
515 r: 1.0,
516 g: 0.5,
517 b: 0.25,
518 a: 0.8,
519 x: 0.4,
520 y: 0.6,
521 mode: 4,
522 scale: 0.7,
523 param: 0.3,
524 smoothing: 0.5,
525 thick: true,
526 dots: false,
527 additive: true,
528 maximize_color: false,
529 mod_alpha_by_volume: false,
530 mod_alpha_start: 0.0,
531 mod_alpha_end: 1.0,
532 split_lr: false,
533 };
534 let u = build_uniforms(params, 0.0, 1280, 720, 1.5);
535 assert_eq!(u.mode, WaveformMode::DerivativeLine as u32);
536 assert!((u.wave_x - 0.4).abs() < 1e-5);
537 assert!((u.aspect - (1280.0 / 720.0)).abs() < 1e-3);
538 assert_eq!(u.is_thick, 1);
539 assert_eq!(u.is_dots, 0);
540 }
541
542 #[test]
543 fn build_uniforms_maximize_color_normalizes_rgb() {
544 let params = WaveParams {
545 r: 0.5,
546 g: 0.25,
547 b: 0.25,
548 a: 1.0,
549 x: 0.5,
550 y: 0.5,
551 mode: 0,
552 scale: 0.5,
553 param: 0.0,
554 smoothing: 0.0,
555 thick: false,
556 dots: false,
557 additive: false,
558 maximize_color: true,
559 mod_alpha_by_volume: false,
560 mod_alpha_start: 0.0,
561 mod_alpha_end: 1.0,
562 split_lr: false,
563 };
564 let u = build_uniforms(params, 0.0, 100, 100, 0.0);
565 assert!((u.wave_color[0] - 1.0).abs() < 1e-5);
566 assert!((u.wave_color[1] - 0.5).abs() < 1e-5);
567 assert!((u.wave_color[2] - 0.5).abs() < 1e-5);
568 }
569
570 #[test]
571 fn build_uniforms_mod_alpha_by_volume_scales_alpha() {
572 let params = WaveParams {
573 r: 1.0,
574 g: 1.0,
575 b: 1.0,
576 a: 1.0,
577 x: 0.5,
578 y: 0.5,
579 mode: 0,
580 scale: 0.5,
581 param: 0.0,
582 smoothing: 0.0,
583 thick: false,
584 dots: false,
585 additive: false,
586 maximize_color: false,
587 mod_alpha_by_volume: true,
588 mod_alpha_start: 0.0,
589 mod_alpha_end: 1.0,
590 split_lr: false,
591 };
592 let low = build_uniforms(params, 0.1, 100, 100, 0.0);
593 let high = build_uniforms(params, 0.9, 100, 100, 0.0);
594 assert!(low.wave_color[3] < high.wave_color[3]);
595 }
596}