Skip to content

streaming

Block-wise reading and writing for files that should not sit in memory: multi-hour recordings, live encoding, seam-free streaming resample. The reader keeps its decoder (and resampler state) across reads, so blocks concatenate exactly to the full-file decode.

StreamReader

with specux.audio.StreamReader("long.flac", sr=16000, mono=True) as r:
r.samplerate, r.channels, r.length
block = r.read(30 * r.samplerate) # 30 s: (frames,) here, short at EOF
r.seek(600 * r.samplerate) # frame-accurate reposition to 10:00

read_into

read_into(out) decodes the next block directly into a buffer you own (no intermediate allocation) and returns the frames written, 0 once drained. out is writable float32: (frames, channels) C-contiguous interleaved, or 1-D for a mono stream. Allocate the buffer once as pinned torch memory and the loop becomes the fastest disk-to-GPU path: decoder → pinned RAM → async H2D, with no hop in between:

with specux.audio.StreamReader("long.flac", sr=16000) as r:
buf = torch.empty((65536, r.channels), dtype=torch.float32).pin_memory()
view = buf.numpy() # zero-copy view of the pinned RAM
while (got := r.read_into(view)):
gpu = buf[:got].to("cuda", non_blocking=True).T # (channels, got)
...

blocks

The one-liner for the common loop. block_frames sets the window size, hop how far the window advances per iteration (default: block_frames, so windows sit back to back):

sr = 16000
for y in specux.audio.blocks("long.flac", 30 * sr, sr=sr, mono=True):
S = specux.melspectrogram(y, sr=sr) # 30 s at a time

With hop < block_frames the windows overlap; the tail of one block equals the head of the next:

for y in specux.audio.blocks("long.flac", sr, hop=sr // 2, mono=True):
... # 1 s windows, every 0.5 s

Sized so STFT frames tile exactly, this streams a spectrogram of arbitrarily long audio in constant memory. Each window carries the n_fft - hop_length samples of context the next one needs; hop a multiple of hop_length keeps the framing aligned, and center=False stops the transform from padding each block as if it were a whole signal:

n_fft, hop_length = 2048, 512
step = hop_length * 1024 # ~33 s of new samples per window
for y in specux.audio.blocks("long.flac", step + n_fft - hop_length,
hop=step, sr=sr, mono=True):
S = specux.melspectrogram(y, sr=sr, n_fft=n_fft,
hop_length=hop_length, center=False)
... # concatenating S along time == the one-shot spectrogram

StreamWriter

The writing mirror: open once, push blocks, encoder state persists. A constant-memory transcode is one loop over reader and writer:

with specux.audio.StreamReader("in.wav") as r, \
specux.audio.StreamWriter("out.flac", r.samplerate, r.channels) as w:
block = r.read(65536)
while block.shape[-1]:
w.write(block)
block = r.read(65536)

input_sr= resamples on the way in, statefully, so block boundaries leave no seams:

with specux.audio.StreamWriter("out.mp3", 44100, 2, input_sr=48000) as w:
for block in specux.audio.blocks("in_48k.wav", 48000):
w.write(block) # written at 44100