1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| """Render the spectrogram band that carries the password.
Usage: uv run --with numpy --with pillow python render_spec.py 2.wav spec.png """ import sys import wave
import numpy as np from PIL import Image
NFFT, HOP = 1024, 64 FMAX = 2000 DYNAMIC_RANGE = 70.0
def load_mono(path): with wave.open(path, "rb") as w: sample_rate = w.getframerate() channels = w.getnchannels() frames = w.readframes(w.getnframes()) pcm = np.frombuffer(frames, dtype=np.int16).astype(np.float64) return sample_rate, pcm.reshape(-1, channels).mean(axis=1)
def render(path): sample_rate, samples = load_mono(path) window = np.hanning(NFFT) freqs = np.fft.rfftfreq(NFFT, 1.0 / sample_rate) keep = int(np.searchsorted(freqs, FMAX)) frame_count = 1 + (len(samples) - NFFT) // HOP magnitude = np.empty((keep, frame_count))
for index in range(frame_count): start = index * HOP frame = samples[start:start + NFFT] * window magnitude[:, index] = np.abs(np.fft.rfft(frame))[:keep]
db = 20.0 * np.log10(magnitude + 1e-9) db -= db.max() image = np.clip((db + DYNAMIC_RANGE) / DYNAMIC_RANGE, 0.0, 1.0) image = (image[::-1] * 255).astype(np.uint8) return image
def main(): source = sys.argv[1] if len(sys.argv) > 1 else "2.wav" target = sys.argv[2] if len(sys.argv) > 2 else "spec.png" image = Image.fromarray(render(source), "L") image = image.resize((image.width, image.height * 4), Image.NEAREST) image.save(target) print("wrote %s (%d x %d, 0-%d Hz)" % (target, image.width, image.height, FMAX))
if __name__ == "__main__": main()
|