Commit
·
82c8e97
1
Parent(s):
579e8a2
one more attempt at _append_model_chunk_and_spool
Browse files- 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 |
-
"""
|
425 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
426 |
s = wav.samples.astype(np.float32, copy=False)
|
427 |
if s.ndim == 1:
|
428 |
s = s[:, None]
|
429 |
-
|
430 |
-
|
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
|
446 |
-
|
447 |
-
|
448 |
-
|
449 |
-
|
450 |
-
|
451 |
-
|
452 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
453 |
else:
|
454 |
-
|
455 |
-
|
456 |
-
|
457 |
-
|
458 |
-
|
459 |
-
|
460 |
-
|
461 |
-
|
462 |
-
|
463 |
-
|
464 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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*
|