Spaces:
Sleeping
Sleeping
Retune the original Taiko kit for arcade punch
Browse files- README.md +3 -2
- softchart/preview_audio.py +103 -67
README.md
CHANGED
|
@@ -29,8 +29,9 @@ The Space uses Gradio Server mode with a fully custom, responsive interface.
|
|
| 29 |
Upload a song, pick a difficulty, and get a full-song mel/plan preview, a
|
| 30 |
full TJA chart image, a `.tja` playable in TJAPlayer3 / OpenTaiko, and a
|
| 31 |
synchronized WAV mix that lets you hear the generated chart performed over
|
| 32 |
-
the source music immediately. The
|
| 33 |
-
SoftChart and does not contain audio taken from a commercial
|
|
|
|
| 34 |
|
| 35 |
The generation API streams the real processing stages to the interface:
|
| 36 |
audio analysis, beat-grid fitting, song-level planning, note generation,
|
|
|
|
| 29 |
Upload a song, pick a difficulty, and get a full-song mel/plan preview, a
|
| 30 |
full TJA chart image, a `.tja` playable in TJAPlayer3 / OpenTaiko, and a
|
| 31 |
synchronized WAV mix that lets you hear the generated chart performed over
|
| 32 |
+
the source music immediately. The original, arcade-tuned wadaiko kit is
|
| 33 |
+
synthesized by SoftChart and does not contain audio taken from a commercial
|
| 34 |
+
game.
|
| 35 |
|
| 36 |
The generation API streams the real processing stages to the interface:
|
| 37 |
audio analysis, beat-grid fitting, song-level planning, note generation,
|
softchart/preview_audio.py
CHANGED
|
@@ -236,73 +236,85 @@ def _falling_mode(t: np.ndarray, start_hz: float, end_hz: float,
|
|
| 236 |
return np.sin(phase) * np.exp(-t / decay)
|
| 237 |
|
| 238 |
|
| 239 |
-
def
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
|
| 245 |
|
| 246 |
def _make_don(sample_rate: int, variant: int, big: bool = False,
|
| 247 |
tap: bool = False) -> np.ndarray:
|
| 248 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
n = int(round(duration * sample_rate))
|
| 250 |
t = np.arange(n, dtype=np.float64) / sample_rate
|
| 251 |
rng = np.random.default_rng(12_710 + 101 * variant + 1_009 * int(big) + 7_003 * int(tap))
|
| 252 |
-
pitch = (0.
|
| 253 |
-
|
| 254 |
-
pitch *= 1.10
|
| 255 |
-
attack = 1.0 - np.exp(-t / 0.00075)
|
| 256 |
|
| 257 |
modes = np.zeros(n, dtype=np.float64)
|
| 258 |
-
frequencies = (
|
| 259 |
-
weights = (1.00, 0.
|
| 260 |
-
decays = (0.
|
| 261 |
-
decay_scale = 0.
|
| 262 |
for frequency, weight, decay in zip(frequencies, weights, decays):
|
| 263 |
end_hz = frequency * pitch
|
| 264 |
modes += weight * _falling_mode(
|
| 265 |
-
t, end_hz * (1.
|
| 266 |
-
0.
|
| 267 |
)
|
| 268 |
|
| 269 |
-
skin = _filtered_noise(rng, n, sample_rate,
|
| 270 |
-
skin *= np.exp(-t / (0.
|
| 271 |
-
stick = _filtered_noise(rng, n, sample_rate,
|
| 272 |
-
stick *= np.exp(-t / 0.
|
| 273 |
-
signal =
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
signal
|
| 279 |
-
|
|
|
|
|
|
|
| 280 |
signal *= target / max(float(np.max(np.abs(signal))), 1e-12)
|
| 281 |
return signal.astype(np.float32)
|
| 282 |
|
| 283 |
|
| 284 |
def _make_ka(sample_rate: int, variant: int, big: bool = False) -> np.ndarray:
|
| 285 |
-
|
|
|
|
|
|
|
|
|
|
| 286 |
n = int(round(duration * sample_rate))
|
| 287 |
t = np.arange(n, dtype=np.float64) / sample_rate
|
| 288 |
rng = np.random.default_rng(37_901 + 131 * variant + 2_003 * int(big))
|
| 289 |
-
pitch = (0.
|
| 290 |
-
attack = 1.0 - np.exp(-t / 0.
|
| 291 |
|
| 292 |
-
rim = _filtered_noise(rng, n, sample_rate,
|
| 293 |
-
rim *= np.exp(-t /
|
| 294 |
modes = np.zeros(n, dtype=np.float64)
|
| 295 |
for frequency, weight, decay in (
|
| 296 |
-
(
|
| 297 |
-
(
|
|
|
|
| 298 |
):
|
| 299 |
phase = 2.0 * np.pi * frequency * pitch * t + variant * 0.31
|
| 300 |
-
modes += weight * np.sin(phase) * np.exp(-t / (decay * (1.
|
| 301 |
-
signal = 0.
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
signal *= np.exp(-np.maximum(t - duration * 0.
|
| 305 |
-
target = 0.
|
| 306 |
signal *= target / max(float(np.max(np.abs(signal))), 1e-12)
|
| 307 |
return signal.astype(np.float32)
|
| 308 |
|
|
@@ -332,13 +344,13 @@ def _expand_strikes(events: list[TaikoEvent]) -> tuple[list[_Strike], int]:
|
|
| 332 |
ordinary_index = 0
|
| 333 |
for event in events:
|
| 334 |
if event.kind in {"don", "ka", "don_big", "ka_big"}:
|
| 335 |
-
if event.kind
|
| 336 |
-
pan = -0.
|
| 337 |
-
elif event.kind == "
|
| 338 |
-
pan = -0.
|
| 339 |
else:
|
| 340 |
-
pan = -0.
|
| 341 |
-
depth = 0.
|
| 342 |
strikes.append(_Strike(event.time, event.kind, pan, depth, False))
|
| 343 |
ordinary_index += 1
|
| 344 |
continue
|
|
@@ -401,7 +413,7 @@ def _load_stereo_audio(audio_path: str, sample_rate: int) -> np.ndarray:
|
|
| 401 |
|
| 402 |
|
| 403 |
def _place_strike(drums: np.ndarray, sample: np.ndarray, strike: _Strike,
|
| 404 |
-
sample_rate: int) -> bool:
|
| 405 |
start = int(round(strike.time * sample_rate))
|
| 406 |
sample_start = max(0, -start)
|
| 407 |
output_start = max(0, start)
|
|
@@ -415,26 +427,33 @@ def _place_strike(drums: np.ndarray, sample: np.ndarray, strike: _Strike,
|
|
| 415 |
drums[output_start:output_start + count, 0] += segment * left_gain
|
| 416 |
drums[output_start:output_start + count, 1] += segment * right_gain
|
| 417 |
|
| 418 |
-
# A
|
| 419 |
-
#
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
return True
|
| 428 |
|
| 429 |
|
| 430 |
def _duck_envelope(frame_count: int, strikes: list[_Strike], sample_rate: int,
|
| 431 |
drum_gain: float) -> tuple[np.ndarray, float]:
|
| 432 |
reduction = np.zeros(frame_count, dtype=np.float32)
|
| 433 |
-
attack_frames = max(1, int(round(0.
|
| 434 |
-
release_frames = max(1, int(round(0.
|
| 435 |
attack = 0.5 - 0.5 * np.cos(np.linspace(0.0, np.pi, attack_frames, dtype=np.float32))
|
| 436 |
release = np.exp(-np.arange(release_frames, dtype=np.float32) /
|
| 437 |
-
max(1.0, 0.
|
| 438 |
shape = np.concatenate((attack, release))
|
| 439 |
gain_scale = min(drum_gain / 0.8, 1.0) if drum_gain > 0.0 else 0.0
|
| 440 |
|
|
@@ -450,7 +469,7 @@ def _duck_envelope(frame_count: int, strikes: list[_Strike], sample_rate: int,
|
|
| 450 |
target = reduction[output_start:output_start + count]
|
| 451 |
np.maximum(target, envelope, out=target)
|
| 452 |
|
| 453 |
-
np.minimum(reduction, 0.
|
| 454 |
gain = 1.0 - reduction
|
| 455 |
max_duck_db = -20.0 * math.log10(max(float(np.min(gain)), 1e-12))
|
| 456 |
return gain, max_duck_db
|
|
@@ -466,6 +485,16 @@ def _write_pcm16_wav(out_path: str, pcm: np.ndarray, sample_rate: int) -> None:
|
|
| 466 |
wav.writeframes(np.ascontiguousarray(pcm.astype("<i2", copy=False)).tobytes())
|
| 467 |
|
| 468 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 469 |
def synthesize_taiko_preview(
|
| 470 |
audio_path: str,
|
| 471 |
tja_text: str,
|
|
@@ -475,9 +504,9 @@ def synthesize_taiko_preview(
|
|
| 475 |
) -> dict[str, float | int]:
|
| 476 |
"""Mix a synchronized stereo PCM16 WAV preview and return numeric stats.
|
| 477 |
|
| 478 |
-
Music is held at
|
| 479 |
-
transient.
|
| 480 |
-
|
| 481 |
"""
|
| 482 |
|
| 483 |
if not isinstance(sample_rate, int) or sample_rate < 16_000 or sample_rate > 192_000:
|
|
@@ -507,21 +536,28 @@ def synthesize_taiko_preview(
|
|
| 507 |
for index, strike in enumerate(strikes):
|
| 508 |
samples = bank[strike.kind]
|
| 509 |
sample = samples[index % len(samples)]
|
| 510 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 511 |
audible_strikes.append(strike)
|
| 512 |
|
|
|
|
| 513 |
duck_gain, max_duck_db = _duck_envelope(
|
| 514 |
len(music), audible_strikes, sample_rate, drum_gain,
|
| 515 |
)
|
| 516 |
-
music_gain = 10.0 ** (-
|
| 517 |
mixed = music * (music_gain * duck_gain[:, None])
|
| 518 |
mixed += drums * float(drum_gain)
|
| 519 |
|
| 520 |
pre_limit_peak = float(np.max(np.abs(mixed))) if mixed.size else 0.0
|
| 521 |
float_ceiling = _PCM16_CEILING / 32767.0
|
| 522 |
-
|
| 523 |
-
mixed *= limiter_gain
|
| 524 |
np.clip(mixed, -float_ceiling, float_ceiling, out=mixed)
|
|
|
|
|
|
|
| 525 |
pcm = np.rint(mixed * 32767.0).astype(np.int16)
|
| 526 |
_write_pcm16_wav(out_path, pcm, sample_rate)
|
| 527 |
|
|
|
|
| 236 |
return np.sin(phase) * np.exp(-t / decay)
|
| 237 |
|
| 238 |
|
| 239 |
+
def _highpass(signal: np.ndarray, sample_rate: int, cutoff_hz: float) -> np.ndarray:
|
| 240 |
+
sos = butter(2, cutoff_hz, btype="highpass", fs=sample_rate, output="sos")
|
| 241 |
+
return sosfilt(sos, signal).astype(np.float64)
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def _soft_clip(signal: np.ndarray, drive: float) -> np.ndarray:
|
| 245 |
+
"""Compress peaks while keeping the small-signal gain at unity."""
|
| 246 |
+
|
| 247 |
+
return np.tanh(signal * drive) / drive
|
| 248 |
|
| 249 |
|
| 250 |
def _make_don(sample_rate: int, variant: int, big: bool = False,
|
| 251 |
tap: bool = False) -> np.ndarray:
|
| 252 |
+
# Compact arcade feedback needs the mid-bass membrane cluster and bachi
|
| 253 |
+
# attack to survive small speakers. The frequencies are based on measured
|
| 254 |
+
# wadaiko mode relationships, deliberately omitting kick-like sub-bass and
|
| 255 |
+
# the large downward pitch sweep used by the previous voice.
|
| 256 |
+
duration = 0.105 if tap else (0.29 if big else 0.27)
|
| 257 |
n = int(round(duration * sample_rate))
|
| 258 |
t = np.arange(n, dtype=np.float64) / sample_rate
|
| 259 |
rng = np.random.default_rng(12_710 + 101 * variant + 1_009 * int(big) + 7_003 * int(tap))
|
| 260 |
+
pitch = (0.982, 0.994, 1.008, 1.019)[variant % 4]
|
| 261 |
+
attack = 1.0 - np.exp(-t / 0.00024)
|
|
|
|
|
|
|
| 262 |
|
| 263 |
modes = np.zeros(n, dtype=np.float64)
|
| 264 |
+
frequencies = (255.0, 295.0, 560.0, 690.0, 1_015.0)
|
| 265 |
+
weights = (0.82, 1.00, 0.47, 0.32, 0.17)
|
| 266 |
+
decays = (0.185, 0.165, 0.095, 0.068, 0.040)
|
| 267 |
+
decay_scale = 0.31 if tap else (1.06 if big else 1.0)
|
| 268 |
for frequency, weight, decay in zip(frequencies, weights, decays):
|
| 269 |
end_hz = frequency * pitch
|
| 270 |
modes += weight * _falling_mode(
|
| 271 |
+
t, end_hz * (1.012 + 0.002 * variant), end_hz,
|
| 272 |
+
0.009, decay * decay_scale,
|
| 273 |
)
|
| 274 |
|
| 275 |
+
skin = _filtered_noise(rng, n, sample_rate, 380.0, 5_400.0)
|
| 276 |
+
skin *= np.exp(-t / (0.0042 if tap else 0.0075))
|
| 277 |
+
stick = _filtered_noise(rng, n, sample_rate, 1_300.0, 9_500.0)
|
| 278 |
+
stick *= np.exp(-t / 0.0018)
|
| 279 |
+
signal = (
|
| 280 |
+
attack * modes
|
| 281 |
+
+ (0.30 if tap else 0.20) * skin
|
| 282 |
+
+ (0.17 if tap else 0.105) * stick
|
| 283 |
+
)
|
| 284 |
+
signal = _highpass(signal, sample_rate, 115.0 if tap else 62.0)
|
| 285 |
+
signal = _soft_clip(signal, 1.42)
|
| 286 |
+
signal *= np.exp(-np.maximum(t - duration * 0.72, 0.0) / 0.018)
|
| 287 |
+
target = 0.59 if tap else (0.90 if big else 0.84)
|
| 288 |
signal *= target / max(float(np.max(np.abs(signal))), 1e-12)
|
| 289 |
return signal.astype(np.float32)
|
| 290 |
|
| 291 |
|
| 292 |
def _make_ka(sample_rate: int, variant: int, big: bool = False) -> np.ndarray:
|
| 293 |
+
# Ka is a dry wooden rim crack: a millisecond-scale excitation followed by
|
| 294 |
+
# short, irregular high modes. Broadband noise is only the bachi contact,
|
| 295 |
+
# not the body of the sound.
|
| 296 |
+
duration = 0.16 if big else 0.14
|
| 297 |
n = int(round(duration * sample_rate))
|
| 298 |
t = np.arange(n, dtype=np.float64) / sample_rate
|
| 299 |
rng = np.random.default_rng(37_901 + 131 * variant + 2_003 * int(big))
|
| 300 |
+
pitch = (0.981, 0.995, 1.009, 1.021)[variant % 4]
|
| 301 |
+
attack = 1.0 - np.exp(-t / 0.00016)
|
| 302 |
|
| 303 |
+
rim = _filtered_noise(rng, n, sample_rate, 1_200.0, 10_500.0)
|
| 304 |
+
rim *= np.exp(-t / 0.0028)
|
| 305 |
modes = np.zeros(n, dtype=np.float64)
|
| 306 |
for frequency, weight, decay in (
|
| 307 |
+
(650.0, 0.42, 0.064), (1_050.0, 1.00, 0.048),
|
| 308 |
+
(1_700.0, 0.63, 0.034), (2_700.0, 0.35, 0.022),
|
| 309 |
+
(4_200.0, 0.19, 0.012),
|
| 310 |
):
|
| 311 |
phase = 2.0 * np.pi * frequency * pitch * t + variant * 0.31
|
| 312 |
+
modes += weight * np.sin(phase) * np.exp(-t / (decay * (1.04 if big else 1.0)))
|
| 313 |
+
signal = 0.22 * rim + attack * modes
|
| 314 |
+
signal = _highpass(signal, sample_rate, 390.0)
|
| 315 |
+
signal = _soft_clip(signal, 1.28)
|
| 316 |
+
signal *= np.exp(-np.maximum(t - duration * 0.68, 0.0) / 0.012)
|
| 317 |
+
target = 0.91 if big else 0.79
|
| 318 |
signal *= target / max(float(np.max(np.abs(signal))), 1e-12)
|
| 319 |
return signal.astype(np.float32)
|
| 320 |
|
|
|
|
| 344 |
ordinary_index = 0
|
| 345 |
for event in events:
|
| 346 |
if event.kind in {"don", "ka", "don_big", "ka_big"}:
|
| 347 |
+
if event.kind.endswith("_big"):
|
| 348 |
+
pan = -0.35 if ordinary_index % 2 == 0 else 0.35
|
| 349 |
+
elif event.kind == "ka":
|
| 350 |
+
pan = -0.14 if ordinary_index % 2 == 0 else 0.14
|
| 351 |
else:
|
| 352 |
+
pan = -0.02 if ordinary_index % 2 == 0 else 0.02
|
| 353 |
+
depth = 0.30 if event.kind.endswith("_big") else 0.22
|
| 354 |
strikes.append(_Strike(event.time, event.kind, pan, depth, False))
|
| 355 |
ordinary_index += 1
|
| 356 |
continue
|
|
|
|
| 413 |
|
| 414 |
|
| 415 |
def _place_strike(drums: np.ndarray, sample: np.ndarray, strike: _Strike,
|
| 416 |
+
sample_rate: int, layered_sample: np.ndarray | None = None) -> bool:
|
| 417 |
start = int(round(strike.time * sample_rate))
|
| 418 |
sample_start = max(0, -start)
|
| 419 |
output_start = max(0, start)
|
|
|
|
| 427 |
drums[output_start:output_start + count, 0] += segment * left_gain
|
| 428 |
drums[output_start:output_start + count, 1] += segment * right_gain
|
| 429 |
|
| 430 |
+
# A big note represents both hands, not a lower-pitched or longer drum.
|
| 431 |
+
# Layer an independently synthesized variant on the opposite side a few
|
| 432 |
+
# milliseconds later while keeping the first transient exactly on-grid.
|
| 433 |
+
if layered_sample is not None:
|
| 434 |
+
layer_delay = int(round(0.0018 * sample_rate))
|
| 435 |
+
layer_start = output_start + layer_delay
|
| 436 |
+
layer_source_start = sample_start
|
| 437 |
+
layer_count = min(
|
| 438 |
+
len(layered_sample) - layer_source_start,
|
| 439 |
+
len(drums) - layer_start,
|
| 440 |
+
)
|
| 441 |
+
if layer_count > 0:
|
| 442 |
+
layer = layered_sample[layer_source_start:layer_source_start + layer_count]
|
| 443 |
+
layer_gain = 0.72 if strike.kind == "roll_big" else 0.92
|
| 444 |
+
drums[layer_start:layer_start + layer_count, 0] += layer * right_gain * layer_gain
|
| 445 |
+
drums[layer_start:layer_start + layer_count, 1] += layer * left_gain * layer_gain
|
| 446 |
return True
|
| 447 |
|
| 448 |
|
| 449 |
def _duck_envelope(frame_count: int, strikes: list[_Strike], sample_rate: int,
|
| 450 |
drum_gain: float) -> tuple[np.ndarray, float]:
|
| 451 |
reduction = np.zeros(frame_count, dtype=np.float32)
|
| 452 |
+
attack_frames = max(1, int(round(0.0025 * sample_rate)))
|
| 453 |
+
release_frames = max(1, int(round(0.080 * sample_rate)))
|
| 454 |
attack = 0.5 - 0.5 * np.cos(np.linspace(0.0, np.pi, attack_frames, dtype=np.float32))
|
| 455 |
release = np.exp(-np.arange(release_frames, dtype=np.float32) /
|
| 456 |
+
max(1.0, 0.027 * sample_rate))
|
| 457 |
shape = np.concatenate((attack, release))
|
| 458 |
gain_scale = min(drum_gain / 0.8, 1.0) if drum_gain > 0.0 else 0.0
|
| 459 |
|
|
|
|
| 469 |
target = reduction[output_start:output_start + count]
|
| 470 |
np.maximum(target, envelope, out=target)
|
| 471 |
|
| 472 |
+
np.minimum(reduction, 0.38, out=reduction)
|
| 473 |
gain = 1.0 - reduction
|
| 474 |
max_duck_db = -20.0 * math.log10(max(float(np.min(gain)), 1e-12))
|
| 475 |
return gain, max_duck_db
|
|
|
|
| 485 |
wav.writeframes(np.ascontiguousarray(pcm.astype("<i2", copy=False)).tobytes())
|
| 486 |
|
| 487 |
|
| 488 |
+
def _soft_limit(signal: np.ndarray, threshold: float, ceiling: float) -> np.ndarray:
|
| 489 |
+
"""Limit only hot samples instead of turning down the entire preview."""
|
| 490 |
+
|
| 491 |
+
magnitude = np.abs(signal)
|
| 492 |
+
width = ceiling - threshold
|
| 493 |
+
over = np.maximum(magnitude - threshold, 0.0) / width
|
| 494 |
+
shaped = threshold + width * np.tanh(over)
|
| 495 |
+
return np.copysign(np.where(magnitude <= threshold, magnitude, shaped), signal)
|
| 496 |
+
|
| 497 |
+
|
| 498 |
def synthesize_taiko_preview(
|
| 499 |
audio_path: str,
|
| 500 |
tja_text: str,
|
|
|
|
| 504 |
) -> dict[str, float | int]:
|
| 505 |
"""Mix a synchronized stereo PCM16 WAV preview and return numeric stats.
|
| 506 |
|
| 507 |
+
Music is held at 3.5 dB of mix headroom and briefly ducked under each drum
|
| 508 |
+
transient. A local soft limiter controls only hot samples before PCM16
|
| 509 |
+
quantization at a -1 dBFS sample-peak ceiling.
|
| 510 |
"""
|
| 511 |
|
| 512 |
if not isinstance(sample_rate, int) or sample_rate < 16_000 or sample_rate > 192_000:
|
|
|
|
| 536 |
for index, strike in enumerate(strikes):
|
| 537 |
samples = bank[strike.kind]
|
| 538 |
sample = samples[index % len(samples)]
|
| 539 |
+
layered_sample = None
|
| 540 |
+
if strike.kind in {"don_big", "ka_big", "roll_big"}:
|
| 541 |
+
layered_sample = samples[(index + 1) % len(samples)]
|
| 542 |
+
if _place_strike(
|
| 543 |
+
drums, sample, strike, sample_rate, layered_sample=layered_sample,
|
| 544 |
+
):
|
| 545 |
audible_strikes.append(strike)
|
| 546 |
|
| 547 |
+
drums = _soft_clip(drums, 1.10).astype(np.float32)
|
| 548 |
duck_gain, max_duck_db = _duck_envelope(
|
| 549 |
len(music), audible_strikes, sample_rate, drum_gain,
|
| 550 |
)
|
| 551 |
+
music_gain = 10.0 ** (-3.5 / 20.0)
|
| 552 |
mixed = music * (music_gain * duck_gain[:, None])
|
| 553 |
mixed += drums * float(drum_gain)
|
| 554 |
|
| 555 |
pre_limit_peak = float(np.max(np.abs(mixed))) if mixed.size else 0.0
|
| 556 |
float_ceiling = _PCM16_CEILING / 32767.0
|
| 557 |
+
mixed = _soft_limit(mixed, threshold=0.68, ceiling=float_ceiling)
|
|
|
|
| 558 |
np.clip(mixed, -float_ceiling, float_ceiling, out=mixed)
|
| 559 |
+
limited_peak = float(np.max(np.abs(mixed))) if mixed.size else 0.0
|
| 560 |
+
limiter_gain = min(1.0, limited_peak / pre_limit_peak) if pre_limit_peak > 0.0 else 1.0
|
| 561 |
pcm = np.rint(mixed * 32767.0).astype(np.int16)
|
| 562 |
_write_pcm16_wav(out_path, pcm, sample_rate)
|
| 563 |
|