| """Dependency-light audio placeholder for Day 1 pipeline proof.""" |
|
|
| from __future__ import annotations |
|
|
| import math |
| import wave |
| from pathlib import Path |
|
|
| from .schema import Delivery, Script |
|
|
| SAMPLE_RATE = 24_000 |
|
|
| _DELIVERY_FREQ = { |
| Delivery.NEUTRAL: 330, |
| Delivery.SLOW: 262, |
| Delivery.URGENT: 494, |
| Delivery.WHISPER: 220, |
| Delivery.BOOMING: 165, |
| Delivery.DEADPAN: 294, |
| Delivery.AGITATED: 440, |
| } |
|
|
|
|
| def render_placeholder_wav(script: Script, output_path: Path) -> Path: |
| """Render a rough tone timeline so orchestration has a real file artifact.""" |
| script.validate() |
| output_path.parent.mkdir(parents=True, exist_ok=True) |
| samples: list[int] = [] |
| samples.extend(_tone(196, 0.5, 0.28)) |
| samples.extend(_silence(0.15)) |
| for scene in script.scenes: |
| samples.extend(_noise_burst(0.18)) |
| samples.extend(_silence(0.15)) |
| for line in scene.lines: |
| duration = min(max(len(line.text) / 34.0, 0.65), 2.4) |
| samples.extend(_tone(_DELIVERY_FREQ[line.delivery], duration, 0.22)) |
| samples.extend(_silence(0.18)) |
| samples.extend(_silence(0.35)) |
| samples.extend(_tone(147, 0.8, 0.24)) |
|
|
| with wave.open(str(output_path), "wb") as wav: |
| wav.setnchannels(1) |
| wav.setsampwidth(2) |
| wav.setframerate(SAMPLE_RATE) |
| wav.writeframes(b"".join(sample.to_bytes(2, "little", signed=True) for sample in samples)) |
| return output_path |
|
|
|
|
| def _tone(freq: float, seconds: float, gain: float) -> list[int]: |
| count = int(SAMPLE_RATE * seconds) |
| attack = max(1, int(SAMPLE_RATE * 0.02)) |
| release = max(1, int(SAMPLE_RATE * 0.03)) |
| out = [] |
| for i in range(count): |
| envelope = 1.0 |
| if i < attack: |
| envelope = i / attack |
| elif i > count - release: |
| envelope = max(0.0, (count - i) / release) |
| value = math.sin(2 * math.pi * freq * (i / SAMPLE_RATE)) |
| out.append(int(32767 * gain * envelope * value)) |
| return out |
|
|
|
|
| def _silence(seconds: float) -> list[int]: |
| return [0] * int(SAMPLE_RATE * seconds) |
|
|
|
|
| def _noise_burst(seconds: float) -> list[int]: |
| count = int(SAMPLE_RATE * seconds) |
| out = [] |
| seed = 17 |
| for i in range(count): |
| seed = (1103515245 * seed + 12345) & 0x7FFFFFFF |
| envelope = max(0.0, 1.0 - i / count) |
| value = ((seed / 0x7FFFFFFF) * 2.0) - 1.0 |
| out.append(int(32767 * 0.08 * envelope * value)) |
| return out |
|
|
|
|