Python Basics: Round-Trip PCM Samples with wave
WAV metadata describes how to interpret sample bytes. A tiny in-memory example is enough to check the relationship between channels, sample width, frames, and signed values without producing or playing an audio file.
Prepare a scratch directory
Tested on Linux with CPython 3.12.14. You need Bash, Python, a text editor, and the standard mktemp, rm, and rmdir utilities. No external packages are required. Run without -O so assertions remain enabled.
Keep the same terminal open. Stop if setup fails, before saving the source:
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1Run the example
Save this as wave-pcm-demo.py:
import io
import struct
import wave
samples = (0, 1000, -1000, 32767, -32768)
payload = struct.pack("<5h", *samples)
with io.BytesIO() as buffer:
with wave.open(buffer, "wb") as writer:
writer.setnchannels(1)
writer.setsampwidth(2)
writer.setframerate(8000)
writer.writeframes(payload)
buffer.seek(0)
with wave.open(buffer, "rb") as reader:
print("Channels:", reader.getnchannels())
print("Sample bytes:", reader.getsampwidth())
print("Frames:", reader.getnframes())
print("Rate:", reader.getframerate())
first = reader.readframes(2)
rest = reader.readframes(10)
print("First samples:", struct.unpack("<2h", first))
print("Remaining samples:", struct.unpack("<3h", rest))
assert first + rest == payload
print("End reached:", reader.readframes(1) == b"")Run it:
python3 -B wave-pcm-demo.pyObserved output:
Channels: 1
Sample bytes: 2
Frames: 5
Rate: 8000
First samples: (0, 1000)
Remaining samples: (-1000, 32767, -32768)
End reached: TrueRead the result
Each mono frame contains one two-byte sample, so five frames occupy ten payload bytes. The < prefix makes the signed 16-bit values explicitly little-endian. Asking for ten remaining frames returns only the three available frames; a subsequent read returns empty bytes.
Apply it carefully
For stereo, one frame contains one sample per channel, so frame count and total sample count differ. wave handles uncompressed PCM rather than decoding arbitrary audio formats. For incoming files, check channels, sample width, compression, and actual returned byte lengths before unpacking. Read large audio in bounded chunks. No sound device is used here.
The official wave reference documents the APIs used here.
Clean up
Remove the saved source, return to the starting directory, and remove the empty practice directory:
rm -- wave-pcm-demo.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirIf the directory is not empty, inspect its remaining contents before removing anything else.
Acknowledgements
This post was created with assistance from AI.