60 lines
2.2 KiB
Python
60 lines
2.2 KiB
Python
"""Create peak-normalized listening copies of PCM16 or float32 capture WAVs.
|
|
|
|
Usage: py tools/audio_preview.py out/audio/single-450-baseline
|
|
Original files retain calibration; preview levels must not be compared by source.
|
|
"""
|
|
import argparse
|
|
import math
|
|
from pathlib import Path
|
|
import struct
|
|
import wave
|
|
|
|
|
|
def preview(path, destination):
|
|
data = path.read_bytes()
|
|
if data[:4] != b"RIFF" or data[8:12] != b"WAVE":
|
|
raise ValueError(f"Not a WAV: {path}")
|
|
offset = 12
|
|
fmt = payload = None
|
|
while offset + 8 <= len(data):
|
|
tag, size = struct.unpack_from("<4sI", data, offset)
|
|
chunk = data[offset + 8:offset + 8 + size]
|
|
if tag == b"fmt ":
|
|
fmt = struct.unpack_from("<HHIIHH", chunk)
|
|
elif tag == b"data":
|
|
payload = chunk
|
|
offset += 8 + size + size % 2
|
|
if fmt is None or payload is None:
|
|
raise ValueError(f"Missing WAV chunks: {path}")
|
|
encoding, channels, rate, _, _, bits = fmt
|
|
if (encoding, bits) == (3, 32):
|
|
values = [x[0] for x in struct.iter_unpack("<f", payload)]
|
|
elif (encoding, bits) == (1, 16):
|
|
values = [x[0] / 32768 for x in struct.iter_unpack("<h", payload)]
|
|
else:
|
|
raise ValueError(f"Unsupported WAV format: {path}")
|
|
if not all(math.isfinite(x) for x in values):
|
|
raise ValueError(f"Nonfinite audio: {path}")
|
|
peak = max(map(abs, values), default=0)
|
|
if peak == 0:
|
|
print(f"{path.name}: silent; no preview")
|
|
return
|
|
gain = 0.8 / peak
|
|
destination.mkdir(parents=True, exist_ok=True)
|
|
target = destination / (path.stem + "-preview.wav")
|
|
with wave.open(str(target), "wb") as out:
|
|
out.setparams((channels, 2, rate, 0, "NONE", "not compressed"))
|
|
out.writeframes(b"".join(struct.pack("<h", round(x * gain * 32767)) for x in values))
|
|
print(f"{target.name}: gain {20 * math.log10(gain):+.1f} dB")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("prefix", type=Path)
|
|
args = parser.parse_args()
|
|
paths = sorted(args.prefix.parent.glob(args.prefix.name + "-*.wav"))
|
|
if not paths:
|
|
parser.error("no matching capture WAVs")
|
|
for path in paths:
|
|
preview(path, args.prefix.parent / "previews")
|