Commit
·
2081536
1
Parent(s):
0103ac5
still attempting to fix the occasional dip into silence
Browse files- jam_worker.py +99 -39
jam_worker.py
CHANGED
|
@@ -117,6 +117,8 @@ 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 +422,106 @@ class JamWorker(threading.Thread):
|
|
| 420 |
|
| 421 |
# ---------- core streaming helpers ----------
|
| 422 |
|
| 423 |
-
def _append_model_chunk_and_spool(self,
|
| 424 |
-
"""
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 443 |
return
|
| 444 |
|
| 445 |
-
#
|
| 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_overlap_model = None
|
| 121 |
+
|
| 122 |
# bar clock: start with offset 0; if you have a downbeat estimator, set base later
|
| 123 |
self._bar_clock = BarClock(self.params.target_sr, self.params.bpm, self.params.beats_per_bar, base_offset_samples=0)
|
| 124 |
|
|
|
|
| 422 |
|
| 423 |
# ---------- core streaming helpers ----------
|
| 424 |
|
| 425 |
+
def _append_model_chunk_and_spool(self, s: np.ndarray) -> None:
|
| 426 |
+
"""
|
| 427 |
+
Append a newly-generated *model-rate* chunk `s` into the output spool, ensuring
|
| 428 |
+
the equal-power crossfade *overlap* is actually included in emitted audio.
|
| 429 |
+
|
| 430 |
+
Strategy (Option A):
|
| 431 |
+
- Keep the last `xfade_n` samples from the previous chunk in `self._pending_overlap_model`.
|
| 432 |
+
- On each new chunk, equal-power mix: mixed = tail(prev) ⨉ cos + head(curr) ⨉ sin
|
| 433 |
+
- Resample+append `mixed` to the target-SR spool, then append the new non-overlapped body.
|
| 434 |
+
- Save the new tail (last `xfade_n`) as `self._pending_overlap_model` for the next call.
|
| 435 |
+
- On the *very first* call (no pending tail yet), DO NOT emit the tail; only emit the body and hold the tail.
|
| 436 |
+
|
| 437 |
+
Notes:
|
| 438 |
+
- This function only manages the *emitted* audio content. It does not change model state.
|
| 439 |
+
- Works with mono or multi-channel arrays shaped [samples] or [samples, channels].
|
| 440 |
+
"""
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
if s is None or s.size == 0:
|
| 444 |
+
return
|
| 445 |
+
|
| 446 |
+
# ---------- Helpers ----------
|
| 447 |
+
def _ensure_2d(x: np.ndarray) -> np.ndarray:
|
| 448 |
+
return x if x.ndim == 2 else x[:, None]
|
| 449 |
+
|
| 450 |
+
def _to_target_sr(y_model: np.ndarray) -> np.ndarray:
|
| 451 |
+
# Reuse your existing resampler here if you have one already.
|
| 452 |
+
# If you use a different helper, swap this call accordingly.
|
| 453 |
+
from utils import resample_audio # adjust if your resampler lives elsewhere
|
| 454 |
+
return resample_audio(y_model, self.mrt.sr, self.params.target_sr)
|
| 455 |
+
|
| 456 |
+
# Compute xfade length in *model samples*
|
| 457 |
+
# Prefer explicit "samples" if present; else derive from seconds.
|
| 458 |
+
try:
|
| 459 |
+
xfade_n = int(getattr(self.mrt.config, "crossfade_samples"))
|
| 460 |
+
except Exception:
|
| 461 |
+
xfade_sec = float(getattr(self.mrt.config, "crossfade_length"))
|
| 462 |
+
xfade_n = int(round(xfade_sec * float(self.mrt.sr)))
|
| 463 |
+
|
| 464 |
+
if xfade_n <= 0:
|
| 465 |
+
# No crossfade configured -> just resample whole thing and append
|
| 466 |
+
y = _to_target_sr(_ensure_2d(s))
|
| 467 |
+
self._spool = np.concatenate([self._spool, y], axis=0) if self._spool.size else y
|
| 468 |
+
self._spool_written += y.shape[0]
|
| 469 |
+
return
|
| 470 |
+
|
| 471 |
+
# Normalize shapes
|
| 472 |
+
s = _ensure_2d(s)
|
| 473 |
+
n_samps = s.shape[0]
|
| 474 |
+
if n_samps <= xfade_n:
|
| 475 |
+
# Too short to meaningfully process: accumulate into pending tail and wait
|
| 476 |
+
tail = s
|
| 477 |
+
self._pending_overlap_model = tail if self._pending_overlap_model is None \
|
| 478 |
+
else np.concatenate([self._pending_overlap_model, tail], axis=0)[-xfade_n:]
|
| 479 |
return
|
| 480 |
|
| 481 |
+
# Split current chunk into head/body/tail at model rate
|
| 482 |
+
head = s[:xfade_n, :]
|
| 483 |
+
body = s[xfade_n:-xfade_n, :] if n_samps >= (2 * xfade_n) else None
|
| 484 |
+
tail = s[-xfade_n:, :]
|
| 485 |
+
|
| 486 |
+
# ---------- If we have a pending tail, mix it with the current head and EMIT the mix ----------
|
| 487 |
+
if self._pending_overlap_model is not None and self._pending_overlap_model.shape[0] == xfade_n:
|
| 488 |
+
prev_tail = self._pending_overlap_model
|
| 489 |
+
|
| 490 |
+
# Equal-power crossfade: tail(prev) * cos + head(curr) * sin
|
| 491 |
+
# Shapes: [xfade_n, C]
|
| 492 |
+
t = np.linspace(0.0, np.pi / 2.0, xfade_n, endpoint=False, dtype=np.float32)[:, None]
|
| 493 |
+
cosw = np.cos(t, dtype=np.float32)
|
| 494 |
+
sinw = np.sin(t, dtype=np.float32)
|
| 495 |
+
mixed = (prev_tail * cosw) + (head * sinw) # still model-rate
|
| 496 |
+
|
| 497 |
+
y_mixed = _to_target_sr(mixed.astype(np.float32))
|
| 498 |
+
# Append the mixed overlap FIRST at target rate
|
| 499 |
+
if self._spool.size:
|
| 500 |
+
self._spool = np.concatenate([self._spool, y_mixed], axis=0)
|
| 501 |
+
else:
|
| 502 |
+
self._spool = y_mixed
|
| 503 |
+
self._spool_written += y_mixed.shape[0]
|
| 504 |
+
|
| 505 |
+
# After mixing, we've consumed head; the "new body" to emit is whatever remains (if any)
|
| 506 |
+
if body is not None and body.size:
|
| 507 |
+
y_body = _to_target_sr(body.astype(np.float32))
|
| 508 |
+
self._spool = np.concatenate([self._spool, y_body], axis=0)
|
| 509 |
+
self._spool_written += y_body.shape[0]
|
| 510 |
+
|
| 511 |
else:
|
| 512 |
+
# FIRST CHUNK: no pending overlap yet
|
| 513 |
+
# Emit only the body; DO NOT emit the tail (we keep it to mix with the next head)
|
| 514 |
+
if body is not None and body.size:
|
| 515 |
+
y_body = _to_target_sr(body.astype(np.float32))
|
| 516 |
+
if self._spool.size:
|
| 517 |
+
self._spool = np.concatenate([self._spool, y_body], axis=0)
|
| 518 |
+
else:
|
| 519 |
+
self._spool = y_body
|
| 520 |
+
self._spool_written += y_body.shape[0]
|
| 521 |
+
# (If there is no body because the chunk is tiny, we emit nothing yet.)
|
| 522 |
+
|
| 523 |
+
# ---------- Store the new pending tail to mix with the next head ----------
|
| 524 |
+
self._pending_overlap_model = tail.copy()
|
| 525 |
|
| 526 |
def _should_generate_next_chunk(self) -> bool:
|
| 527 |
# Allow running ahead relative to whichever is larger: last *consumed*
|