thecollabagepatch commited on
Commit
82c8e97
·
1 Parent(s): 579e8a2

one more attempt at _append_model_chunk_and_spool

Browse files
Files changed (1) hide show
  1. jam_worker.py +115 -36
jam_worker.py CHANGED
@@ -117,6 +117,9 @@ class JamWorker(threading.Thread):
117
  self._spool = np.zeros((0, 2), dtype=np.float32) # (S,2) target SR
118
  self._spool_written = 0 # absolute frames written into spool
119
 
 
 
 
120
  # bar clock: start with offset 0; if you have a downbeat estimator, set base later
121
  self._bar_clock = BarClock(self.params.target_sr, self.params.bpm, self.params.beats_per_bar, base_offset_samples=0)
122
 
@@ -420,48 +423,124 @@ class JamWorker(threading.Thread):
420
 
421
  # ---------- core streaming helpers ----------
422
 
423
- def _append_model_chunk_and_spool(self, wav: au.Waveform):
424
- """Crossfade into the model-rate stream and write the *non-overlapped*
425
- tail to the target-SR spool."""
 
 
 
 
 
 
 
 
 
 
 
 
426
  s = wav.samples.astype(np.float32, copy=False)
427
  if s.ndim == 1:
428
  s = s[:, None]
429
- sr = self._model_sr
430
- xfade_s = float(self.mrt.config.crossfade_length)
431
- xfade_n = int(round(max(0.0, xfade_s) * sr))
432
-
433
- if self._model_stream is None:
434
- # first chunk: drop the preroll (xfade) then spool
435
- new_part = s[xfade_n:] if xfade_n < s.shape[0] else s[:0]
436
- self._model_stream = new_part.copy()
437
- if new_part.size:
438
- y = (new_part.astype(np.float32, copy=False)
439
- if self._rs is None else
440
- self._rs.process(new_part.astype(np.float32, copy=False), final=False))
441
- self._spool = np.concatenate([self._spool, y], axis=0)
442
- self._spool_written += y.shape[0]
443
  return
444
 
445
- # crossfade into existing stream
446
- if xfade_n > 0 and self._model_stream.shape[0] >= xfade_n and s.shape[0] >= xfade_n:
447
- tail = self._model_stream[-xfade_n:]
448
- head = s[:xfade_n]
449
- t = np.linspace(0, np.pi/2, xfade_n, endpoint=False, dtype=np.float32)[:, None]
450
- mixed = tail * np.cos(t) + head * np.sin(t)
451
- self._model_stream = np.concatenate([self._model_stream[:-xfade_n], mixed, s[xfade_n:]], axis=0)
452
- new_part = s[xfade_n:]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453
  else:
454
- self._model_stream = np.concatenate([self._model_stream, s], axis=0)
455
- new_part = s
456
-
457
- # spool only the *new* non-overlapped part
458
- if new_part.size:
459
- y = (new_part.astype(np.float32, copy=False)
460
- if self._rs is None else
461
- self._rs.process(new_part.astype(np.float32, copy=False), final=False))
462
- if y.size:
463
- self._spool = np.concatenate([self._spool, y], axis=0)
464
- self._spool_written += y.shape[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
465
 
466
  def _should_generate_next_chunk(self) -> bool:
467
  # Allow running ahead relative to whichever is larger: last *consumed*
 
117
  self._spool = np.zeros((0, 2), dtype=np.float32) # (S,2) target SR
118
  self._spool_written = 0 # absolute frames written into spool
119
 
120
+ self._pending_tail_model = None # type: Optional[np.ndarray] # last tail at model SR
121
+ self._pending_tail_target_len = 0 # number of target-SR samples last tail contributed
122
+
123
  # bar clock: start with offset 0; if you have a downbeat estimator, set base later
124
  self._bar_clock = BarClock(self.params.target_sr, self.params.bpm, self.params.beats_per_bar, base_offset_samples=0)
125
 
 
423
 
424
  # ---------- core streaming helpers ----------
425
 
426
+ def _append_model_chunk_and_spool(self, wav: au.Waveform) -> None:
427
+ """
428
+ Conservative boundary fix:
429
+ - Emit body+tail immediately (target SR), unchanged from your original behavior.
430
+ - On *next* call, compute the mixed overlap (prev tail ⨉ cos + new head ⨉ sin),
431
+ resample it, and overwrite the last `_pending_tail_target_len` samples in the
432
+ target-SR spool with that mixed overlap. Then emit THIS chunk's body+tail and
433
+ remember THIS chunk's tail length at target SR for the next correction.
434
+
435
+ This keeps external timing and bar alignment identical, but removes the audible
436
+ fade-to-zero at chunk ends.
437
+ """
438
+ import numpy as np
439
+
440
+ # ---- unpack model-rate samples ----
441
  s = wav.samples.astype(np.float32, copy=False)
442
  if s.ndim == 1:
443
  s = s[:, None]
444
+ n_samps, _ = s.shape
445
+ if n_samps == 0:
 
 
 
 
 
 
 
 
 
 
 
 
446
  return
447
 
448
+ # crossfade length in model samples
449
+ try:
450
+ xfade_s = float(self.mrt.config.crossfade_length)
451
+ except Exception:
452
+ xfade_s = 0.0
453
+ xfade_n = int(round(max(0.0, xfade_s) * float(self._model_sr)))
454
+
455
+ # helper: resample to target SR via your streaming resampler
456
+ def to_target(y: np.ndarray) -> np.ndarray:
457
+ return y if self._rs is None else self._rs.process(y, final=False)
458
+
459
+ # ------------------------------------------
460
+ # (A) If we have a pending model tail, fix the last emitted tail at target SR
461
+ # ------------------------------------------
462
+ if self._pending_tail_model is not None and self._pending_tail_model.shape[0] == xfade_n and xfade_n > 0 and n_samps >= xfade_n:
463
+ head = s[:xfade_n, :]
464
+ t = np.linspace(0.0, np.pi/2.0, xfade_n, endpoint=False, dtype=np.float32)[:, None]
465
+ cosw = np.cos(t, dtype=np.float32)
466
+ sinw = np.sin(t, dtype=np.float32)
467
+ mixed_model = (self._pending_tail_model * cosw) + (head * sinw) # [xfade_n, C] at model SR
468
+
469
+ y_mixed = to_target(mixed_model.astype(np.float32))
470
+ Lcorr = int(y_mixed.shape[0]) # exact target-SR samples to write
471
+
472
+ # Overwrite the last `_pending_tail_target_len` samples of the spool with `y_mixed`.
473
+ # Use the *smaller* of the two lengths to be safe.
474
+ Lpop = min(self._pending_tail_target_len, self._spool.shape[0], Lcorr)
475
+ if Lpop > 0 and self._spool.size:
476
+ # Trim last Lpop samples
477
+ self._spool = self._spool[:-Lpop, :]
478
+ self._spool_written -= Lpop
479
+ # Append corrected overlap (trim/pad to Lpop to avoid drift)
480
+ if Lcorr != Lpop:
481
+ if Lcorr > Lpop:
482
+ y_m = y_mixed[-Lpop:, :]
483
+ else:
484
+ pad = np.zeros((Lpop - Lcorr, y_mixed.shape[1]), dtype=np.float32)
485
+ y_m = np.concatenate([y_mixed, pad], axis=0)
486
+ else:
487
+ y_m = y_mixed
488
+ self._spool = np.concatenate([self._spool, y_m], axis=0) if self._spool.size else y_m
489
+ self._spool_written += y_m.shape[0]
490
+
491
+ # For internal continuity, update _model_stream like before
492
+ if self._model_stream is None or self._model_stream.shape[0] < xfade_n:
493
+ self._model_stream = s[xfade_n:].copy()
494
+ else:
495
+ self._model_stream = np.concatenate([self._model_stream[:-xfade_n], mixed_model, s[xfade_n:]], axis=0)
496
  else:
497
+ # First-ever call or too-short to mix: maintain _model_stream minimally
498
+ if xfade_n > 0 and n_samps > xfade_n:
499
+ self._model_stream = s[xfade_n:].copy() if self._model_stream is None else np.concatenate([self._model_stream, s[xfade_n:]], axis=0)
500
+ else:
501
+ self._model_stream = s.copy() if self._model_stream is None else np.concatenate([self._model_stream, s], axis=0)
502
+
503
+ # ------------------------------------------
504
+ # (B) Emit THIS chunk's body and tail (same external behavior)
505
+ # ------------------------------------------
506
+ if xfade_n > 0 and n_samps >= (2 * xfade_n):
507
+ body = s[xfade_n:-xfade_n, :]
508
+ if body.size:
509
+ y_body = to_target(body.astype(np.float32))
510
+ if y_body.size:
511
+ self._spool = np.concatenate([self._spool, y_body], axis=0) if self._spool.size else y_body
512
+ self._spool_written += y_body.shape[0]
513
+ else:
514
+ # If chunk too short for head+tail split, treat all (minus preroll) as body
515
+ if xfade_n > 0 and n_samps > xfade_n:
516
+ body = s[xfade_n:, :]
517
+ y_body = to_target(body.astype(np.float32))
518
+ if y_body.size:
519
+ self._spool = np.concatenate([self._spool, y_body], axis=0) if self._spool.size else y_body
520
+ self._spool_written += y_body.shape[0]
521
+ # No tail to remember this round
522
+ self._pending_tail_model = None
523
+ self._pending_tail_target_len = 0
524
+ return
525
+
526
+ # Tail (always remember how many TARGET samples we append)
527
+ if xfade_n > 0 and n_samps >= xfade_n:
528
+ tail = s[-xfade_n:, :]
529
+ y_tail = to_target(tail.astype(np.float32))
530
+ Ltail = int(y_tail.shape[0])
531
+ if Ltail:
532
+ self._spool = np.concatenate([self._spool, y_tail], axis=0) if self._spool.size else y_tail
533
+ self._spool_written += Ltail
534
+ self._pending_tail_model = tail.copy()
535
+ self._pending_tail_target_len = Ltail
536
+ else:
537
+ # Nothing appended (resampler returning nothing yet) — keep model tail but mark zero target len
538
+ self._pending_tail_model = tail.copy()
539
+ self._pending_tail_target_len = 0
540
+ else:
541
+ self._pending_tail_model = None
542
+ self._pending_tail_target_len = 0
543
+
544
 
545
  def _should_generate_next_chunk(self) -> bool:
546
  # Allow running ahead relative to whichever is larger: last *consumed*