Skip to content

Commit 81db11d

Browse files
Add Terminal node, Text Ops, and patch shift nodes
1 parent 53cb79d commit 81db11d

File tree

15 files changed

+1365
-77
lines changed

15 files changed

+1365
-77
lines changed

src/app/mod.rs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,9 @@ impl PatchworkApp {
499499
NodeType::AudioDistortion { drive } => {
500500
self.audio.engine_write_param(nid, 0, *drive);
501501
}
502+
NodeType::AudioPitchShift { semitones } => {
503+
self.audio.engine_write_param(nid, 0, *semitones);
504+
}
502505
NodeType::AudioReverb { room_size, damping, mix } => {
503506
self.audio.engine_write_param(nid, 0, *room_size);
504507
self.audio.engine_write_param(nid, 1, *damping);
@@ -797,7 +800,7 @@ impl PatchworkApp {
797800
.id(egui::Id::new(("node", node_id, zoom_bucket)))
798801
.current_pos(egui::pos2(egui_x, egui_y))
799802
.default_width(node_width)
800-
.resizable(is_display || (!is_custom_render && !is_comment && !is_monitor && !is_audio_player && !is_math))
803+
.resizable(is_display)
801804
.constrain(false)
802805
.collapsible(!is_custom_render && !no_title && !is_pinned)
803806
.title_bar(!is_custom_render && !no_title);
@@ -1088,6 +1091,34 @@ impl PatchworkApp {
10881091
port_positions.insert((node_id, i, false), pos);
10891092
fg.circle_filled(pos, 3.0, egui::Color32::from_rgb(60, 140, 255));
10901093
}
1094+
1095+
// For WGSL nodes: run the offscreen render pass even when collapsed
1096+
// so downstream multi-pass chains keep receiving image data.
1097+
if let NodeType::WgslViewer {
1098+
wgsl_code,
1099+
uniform_names,
1100+
uniform_types,
1101+
uniform_values,
1102+
canvas_w,
1103+
canvas_h,
1104+
image_a_mode,
1105+
image_b_mode,
1106+
feedback_reset_pending,
1107+
..
1108+
} = &mut node.node_type {
1109+
crate::nodes::wgsl_viewer::render_offscreen_pass(
1110+
ctx, node_id,
1111+
wgsl_code,
1112+
uniform_names,
1113+
uniform_types,
1114+
uniform_values,
1115+
*canvas_w, *canvas_h,
1116+
*image_a_mode, *image_b_mode,
1117+
feedback_reset_pending,
1118+
&connections, values,
1119+
&wgpu_render_state,
1120+
);
1121+
}
10911122
}
10921123

10931124
// Undo snapshot on drag start (coalesced — one per gesture)
@@ -1380,8 +1411,16 @@ impl PatchworkApp {
13801411

13811412
for &(fn_, fp, tn, tp) in &pending_connections {
13821413
let from_name = self.graph.nodes.get(&fn_).map(|n| n.node_type.title()).unwrap_or("?");
1414+
let from_port = self.graph.nodes.get(&fn_)
1415+
.and_then(|n| n.node_type.outputs().get(fp).map(|p| p.name.clone()))
1416+
.unwrap_or_else(|| fp.to_string().into());
13831417
let to_name = self.graph.nodes.get(&tn).map(|n| n.node_type.title()).unwrap_or("?");
1384-
crate::system_log::log(format!("Connected {} (id:{}) → {} (id:{})", from_name, fn_, to_name, tn));
1418+
let to_port = self.graph.nodes.get(&tn)
1419+
.and_then(|n| n.node_type.inputs().get(tp).map(|p| p.name.clone()))
1420+
.unwrap_or_else(|| tp.to_string().into());
1421+
crate::system_log::log(format!(
1422+
"Connected {}.{} → {}.{}", from_name, from_port, to_name, to_port
1423+
));
13851424
self.graph.add_connection(fn_, fp, tn, tp);
13861425
// Only send engine connect/disconnect for audio ports
13871426
// For mixer nodes, skip gain control ports (odd: 1, 3, 5)

src/audio/buffers.rs

Lines changed: 143 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,15 @@ pub struct SamplerBuffer {
276276
pub sample_rate: AtomicU32,
277277
/// Play direction: false = forward, true = reverse.
278278
pub reverse: AtomicBool,
279+
/// Playback rate multiplier × 1000 (1000 = 1.0x, 500 = 0.5x, 2000 = 2.0x).
280+
/// Set by UI/param sync; read by audio thread in play_into.
281+
pub playback_rate_x1000: AtomicU32,
282+
/// Fractional read position accumulator for variable-rate linear interpolation.
283+
/// Only accessed by the audio thread (play_into). UnsafeCell for interior mutability.
284+
frac_read_pos: UnsafeCell<f64>,
285+
/// Pending seek target in samples. usize::MAX = no seek pending.
286+
/// Set by UI thread via seek_to_normalized(); consumed atomically in play_into.
287+
pub pending_seek: AtomicUsize,
279288
}
280289

281290
unsafe impl Send for SamplerBuffer {}
@@ -298,9 +307,25 @@ impl SamplerBuffer {
298307
trim_end: AtomicUsize::new(0),
299308
sample_rate: AtomicU32::new(sample_rate),
300309
reverse: AtomicBool::new(false),
310+
playback_rate_x1000: AtomicU32::new(1000),
311+
frac_read_pos: UnsafeCell::new(0.0),
312+
pending_seek: AtomicUsize::new(usize::MAX),
301313
}
302314
}
303315

316+
/// Schedule a seek to a normalized position within the trim range.
317+
/// 0.0 = trim_start, 1.0 = trim_end. Safe to call from the UI thread.
318+
/// Takes effect at the start of the next play_into call (even while paused,
319+
/// so next play() starts from the seeked position).
320+
pub fn seek_to_normalized(&self, t: f32) {
321+
let trim_start = self.trim_start.load(Ordering::Relaxed);
322+
let trim_end = self.effective_end();
323+
if trim_end <= trim_start { return; }
324+
let t = t.clamp(0.0, 1.0) as f64;
325+
let target = trim_start + ((trim_end - trim_start) as f64 * t) as usize;
326+
self.pending_seek.store(target, Ordering::Release);
327+
}
328+
304329
/// Start recording — resets write position, playhead, trim, and clears
305330
/// previous data so downstream nodes can't keep playing the old tail.
306331
pub fn start_recording(&self) {
@@ -317,6 +342,9 @@ impl SamplerBuffer {
317342
// be heard if play is retriggered before a new recording finishes.
318343
let data = unsafe { &mut *self.data.get() };
319344
for s in data.iter_mut() { *s = 0.0; }
345+
// Clear any stale seek so the new recording starts clean.
346+
self.pending_seek.store(usize::MAX, Ordering::Release);
347+
unsafe { *self.frac_read_pos.get() = 0.0; }
320348
self.recording.store(true, Ordering::Release);
321349
}
322350

@@ -361,6 +389,8 @@ impl SamplerBuffer {
361389
self.read_pos.store(start, Ordering::Release);
362390
}
363391
self.recording.store(false, Ordering::Release);
392+
// Reset fractional position so variable-rate interpolation starts clean.
393+
unsafe { *self.frac_read_pos.get() = 0.0; }
364394
self.playing.store(true, Ordering::Release);
365395
}
366396

@@ -370,10 +400,21 @@ impl SamplerBuffer {
370400
}
371401

372402
/// Read samples for playback (called from audio thread).
373-
/// Respects trim start/end and reverse direction. Returns silence when not playing.
403+
/// Respects trim start/end, reverse direction, and variable playback rate.
404+
/// Returns silence when not playing. Applies any pending seek first.
374405
pub fn play_into(&self, buf: &mut [f32], num_frames: usize) {
406+
let n = num_frames.min(buf.len());
407+
408+
// ── Apply pending seek FIRST — even while paused, so next play starts there ──
409+
let seek_target = self.pending_seek.load(Ordering::Acquire);
410+
if seek_target != usize::MAX {
411+
self.read_pos.store(seek_target, Ordering::Release);
412+
unsafe { *self.frac_read_pos.get() = 0.0; }
413+
self.pending_seek.store(usize::MAX, Ordering::Release);
414+
}
415+
375416
if !self.playing.load(Ordering::Relaxed) {
376-
for i in 0..num_frames.min(buf.len()) { buf[i] = 0.0; }
417+
for i in 0..n { buf[i] = 0.0; }
377418
return;
378419
}
379420

@@ -382,38 +423,116 @@ impl SamplerBuffer {
382423
let trim_start = self.trim_start.load(Ordering::Relaxed);
383424
let looping = self.looping.load(Ordering::Relaxed);
384425
let reverse = self.reverse.load(Ordering::Relaxed);
426+
let rate = self.playback_rate_x1000.load(Ordering::Relaxed) as f64 / 1000.0;
427+
let frac_pos = unsafe { &mut *self.frac_read_pos.get() };
385428
let mut rp = self.read_pos.load(Ordering::Relaxed);
386429

387-
if reverse {
388-
for i in 0..num_frames.min(buf.len()) {
389-
if rp <= trim_start || rp == 0 {
390-
if looping {
391-
rp = if trim_end > 0 { trim_end - 1 } else { 0 };
392-
} else {
393-
for j in i..num_frames.min(buf.len()) { buf[j] = 0.0; }
394-
self.playing.store(false, Ordering::Release);
395-
break;
430+
// Guard against degenerate trim range
431+
if trim_end <= trim_start {
432+
for i in 0..n { buf[i] = 0.0; }
433+
return;
434+
}
435+
let range_len = trim_end - trim_start;
436+
437+
let safe_sample = |pos: usize| -> f32 {
438+
if pos < self.capacity { data[pos] } else { 0.0 }
439+
};
440+
441+
if (rate - 1.0).abs() < 0.001 {
442+
// ── Fast path: 1.0x speed, integer stepping (original logic) ──────
443+
if reverse {
444+
for i in 0..n {
445+
if rp <= trim_start || rp == 0 {
446+
if looping {
447+
rp = if trim_end > 0 { trim_end - 1 } else { 0 };
448+
} else {
449+
for j in i..n { buf[j] = 0.0; }
450+
self.playing.store(false, Ordering::Release);
451+
break;
452+
}
396453
}
454+
buf[i] = safe_sample(rp);
455+
rp = rp.saturating_sub(1);
456+
}
457+
} else {
458+
for i in 0..n {
459+
if rp >= trim_end {
460+
if looping {
461+
rp = trim_start;
462+
} else {
463+
for j in i..n { buf[j] = 0.0; }
464+
self.playing.store(false, Ordering::Release);
465+
break;
466+
}
467+
}
468+
buf[i] = safe_sample(rp);
469+
rp += 1;
397470
}
398-
buf[i] = if rp < self.capacity { data[rp] } else { 0.0 };
399-
rp = rp.saturating_sub(1);
400471
}
472+
self.read_pos.store(rp, Ordering::Release);
401473
} else {
402-
for i in 0..num_frames.min(buf.len()) {
403-
if rp >= trim_end {
404-
if looping {
405-
rp = trim_start;
406-
} else {
407-
for j in i..num_frames.min(buf.len()) { buf[j] = 0.0; }
408-
self.playing.store(false, Ordering::Release);
409-
break;
474+
// ── Variable-rate path: linear interpolation ──────────────────────
475+
if !reverse {
476+
// Forward variable rate
477+
for i in 0..n {
478+
// How many whole samples ahead of rp we are
479+
let int_off = *frac_pos as usize;
480+
let pos0 = rp + int_off;
481+
482+
if pos0 >= trim_end {
483+
if looping {
484+
// Wrap: consume integer part, reset within trim range
485+
let consumed = *frac_pos as usize;
486+
let overshoot = (rp + consumed).saturating_sub(trim_start);
487+
rp = trim_start + (overshoot % range_len.max(1));
488+
*frac_pos -= consumed as f64;
489+
} else {
490+
for j in i..n { buf[j] = 0.0; }
491+
self.playing.store(false, Ordering::Release);
492+
break;
493+
}
494+
continue;
410495
}
496+
497+
let frac = (*frac_pos - int_off as f64) as f32;
498+
let pos1 = (pos0 + 1).min(trim_end - 1);
499+
buf[i] = safe_sample(pos0) + (safe_sample(pos1) - safe_sample(pos0)) * frac;
500+
*frac_pos += rate;
411501
}
412-
buf[i] = if rp < self.capacity { data[rp] } else { 0.0 };
413-
rp += 1;
502+
// Commit integer part of frac_pos into rp
503+
let consumed = *frac_pos as usize;
504+
rp = rp.wrapping_add(consumed);
505+
*frac_pos -= consumed as f64;
506+
} else {
507+
// Reverse variable rate — frac_pos accumulates forward but we walk backward
508+
for i in 0..n {
509+
let int_off = *frac_pos as usize;
510+
511+
if rp < int_off || rp - int_off <= trim_start {
512+
if looping {
513+
rp = trim_end.saturating_sub(1);
514+
*frac_pos = 0.0;
515+
} else {
516+
for j in i..n { buf[j] = 0.0; }
517+
self.playing.store(false, Ordering::Release);
518+
break;
519+
}
520+
continue;
521+
}
522+
523+
let pos0 = rp - int_off;
524+
let frac = (*frac_pos - int_off as f64) as f32;
525+
let pos1 = pos0.saturating_sub(1).max(trim_start);
526+
buf[i] = safe_sample(pos0) + (safe_sample(pos1) - safe_sample(pos0)) * frac;
527+
*frac_pos += rate;
528+
}
529+
// Commit integer part backward
530+
let consumed = *frac_pos as usize;
531+
rp = rp.saturating_sub(consumed);
532+
*frac_pos -= consumed as f64;
414533
}
534+
self.read_pos.store(rp, Ordering::Release);
415535
}
416-
self.read_pos.store(rp, Ordering::Release);
417536
}
418537

419538
/// Effective end position (trim_end if set, else recorded_length).

src/audio/manager.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,9 @@ impl AudioManager {
372372
NodeType::AudioDistortion { drive } => {
373373
Some((Box::new(effects::DistortionProcessor::new(*drive)), 1))
374374
}
375+
NodeType::AudioPitchShift { semitones } => {
376+
Some((Box::new(effects::PitchShiftProcessor::new(*semitones)), 1))
377+
}
375378
NodeType::AudioReverb { room_size, damping, mix } => {
376379
Some((Box::new(effects::ReverbProcessor::new(*room_size, *damping, *mix)), 3))
377380
}
@@ -426,7 +429,7 @@ impl AudioManager {
426429
self.sampler_buffers.insert(nid, buf.clone());
427430
buf
428431
};
429-
Some((Box::new(sampler::SamplerProcessor::new(buf, *volume)), 3))
432+
Some((Box::new(sampler::SamplerProcessor::new(buf, *volume)), 5))
430433
}
431434
_ => None,
432435
}
@@ -483,7 +486,7 @@ impl AudioManager {
483486
// Register processor in the engine (if running)
484487
if self.engine_tx.is_some() && !self.has_processor(node_id) {
485488
let processor = Box::new(super::processors::sampler::SamplerProcessor::new(buf.clone(), 1.0));
486-
self.add_processor(node_id, processor, 3);
489+
self.add_processor(node_id, processor, 5);
487490
}
488491

489492
buf

0 commit comments

Comments
 (0)