@@ -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
281290unsafe 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).
0 commit comments