thecollabagepatch commited on
Commit
17c0977
·
1 Parent(s): 5ae6d86

another _append_model_chunk_and_spool revision

Browse files
Files changed (1) hide show
  1. jam_worker.py +94 -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,103 @@ 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]
121
+
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
+ Option B: overwrite the last emitted tail (at target SR) with a properly mixed
429
+ overlap once the next head arrives. Then emit the new body+tail as usual.
430
+
431
+ Keeps the externally visible timing identical to the original pipeline while fixing
432
+ the audible boundary.
433
+ """
434
+ import numpy as np
435
+
436
+ # ---------- unpack model-rate samples ----------
437
  s = wav.samples.astype(np.float32, copy=False)
438
  if s.ndim == 1:
439
  s = s[:, None]
440
+ n_samps, n_ch = s.shape
441
+
442
+ sr_model = self._model_sr
443
+ sr_targ = int(self.params.target_sr)
444
+
445
+ # crossfade length (seconds -> model samples)
446
+ try:
447
+ xfade_s = float(self.mrt.config.crossfade_length)
448
+ except Exception:
449
+ xfade_s = 0.0
450
+ xfade_n = int(round(max(0.0, xfade_s) * float(sr_model)))
451
+
452
+ # helper: resample model-rate frames to target SR using your existing resampler
453
+ def _rs_to_target(y: np.ndarray) -> np.ndarray:
454
+ return y if self._rs is None else self._rs.process(y, final=False)
455
+
456
+ # trivial cases
457
+ if n_samps == 0:
458
+ return
459
+ if xfade_n <= 0 or n_samps < (xfade_n + 1):
460
+ # No crossfade or too short to hold a head
461
+ y_all = _rs_to_target(s)
462
+ if y_all.size:
463
+ self._spool = np.concatenate([self._spool, y_all], axis=0) if self._spool.size else y_all
464
+ self._spool_written += y_all.shape[0]
465
+ # keep model stream contiguous for internal consumers
466
+ self._model_stream = s if self._model_stream is None else np.concatenate([self._model_stream, s], axis=0)
467
  return
468
 
469
+ # ---------- split current chunk at model rate ----------
470
+ head = s[:xfade_n, :]
471
+ body = s[xfade_n:-xfade_n, :] if n_samps >= (2 * xfade_n) else None
472
+ tail = s[-xfade_n:, :]
473
+
474
+ # ----- (A) If we had a pending model tail, correct the spool by replacing its target-rate tail with a mixed overlap -----
475
+ if getattr(self, "_pending_tail_model", None) is not None and self._pending_tail_model.shape[0] == xfade_n:
476
+ prev_tail = self._pending_tail_model
477
+
478
+ # equal-power mix at model rate
479
+ t = np.linspace(0.0, np.pi / 2.0, xfade_n, endpoint=False, dtype=np.float32)[:, None]
480
+ cosw = np.cos(t, dtype=np.float32)
481
+ sinw = np.sin(t, dtype=np.float32)
482
+ mixed_model = (prev_tail * cosw) + (head * sinw) # [xfade_n, C]
483
+
484
+ # resample the MIXED overlap to target SR
485
+ y_mixed = _rs_to_target(mixed_model.astype(np.float32))
486
+
487
+ # compute 'target_xfade_n' as the length of y_mixed (robust to resampler latency)
488
+ target_xfade_n = int(y_mixed.shape[0])
489
+
490
+ # pop last target_xfade_n samples from the spool (they currently hold the old tail)
491
+ if target_xfade_n > 0 and self._spool.size and self._spool.shape[0] >= target_xfade_n:
492
+ self._spool = self._spool[:-target_xfade_n, :]
493
+ self._spool_written -= target_xfade_n
494
+
495
+ # append the corrected mixed overlap
496
+ if y_mixed.size:
497
+ self._spool = np.concatenate([self._spool, y_mixed], axis=0) if self._spool.size else y_mixed
498
+ self._spool_written += y_mixed.shape[0]
499
+
500
+ # ----- (B) Emit this chunk's body and tail at target SR (same as the original behavior) -----
501
+ if body is not None and body.size:
502
+ y_body = _rs_to_target(body.astype(np.float32))
503
+ if y_body.size:
504
+ self._spool = np.concatenate([self._spool, y_body], axis=0) if self._spool.size else y_body
505
+ self._spool_written += y_body.shape[0]
506
+
507
+ y_tail = _rs_to_target(tail.astype(np.float32))
508
+ if y_tail.size:
509
+ self._spool = np.concatenate([self._spool, y_tail], axis=0) if self._spool.size else y_tail
510
+ self._spool_written += y_tail.shape[0]
511
+
512
+ # ----- (C) Maintain model-stream continuity like before -----
513
+ # (Drop the model preroll on the very first append; otherwise splice with mixed + new content.)
514
+ if self._model_stream is None or self._model_stream.shape[0] < xfade_n:
515
+ new_part_for_stream = s[xfade_n:] if xfade_n < n_samps else s[:0]
516
+ self._model_stream = new_part_for_stream.copy()
517
  else:
518
+ # emulate your prior continuity update:
519
+ self._model_stream = np.concatenate([self._model_stream[:-xfade_n], mixed_model if 'mixed_model' in locals() else head, s[xfade_n:]], axis=0)
520
+
521
+ # ----- (D) Store this chunk's tail at model rate for the next correction step -----
522
+ self._pending_tail_model = tail.copy()
 
 
 
 
 
 
523
 
524
  def _should_generate_next_chunk(self) -> bool:
525
  # Allow running ahead relative to whichever is larger: last *consumed*