Wave Library
Python's built-in wave module reads and writes
uncompressed PCM WAV audio. Learn how sound becomes samples,
how samples are grouped into frames, and how Python can inspect, generate,
reverse and change the volume of audio.
Learning Objectives
- 12.6.3.1 Use commands of the Wave library to process sound files
Conceptual Anchor
From Air Pressure to Numbers
Real sound is a continuously changing pressure wave. Digital audio measures that wave at regular moments. Each measurement becomes a sample value. A WAV file stores the audio parameters plus these measurements as binary data.
sound wave
Continuous pressure changes
sample
Measure the wave repeatedly
frame
One moment across all channels
WAV
Header + PCM frame data
wave.open()
Read or write in Python
What Does Digital Sound Look Like?
A waveform graph places time on the horizontal axis and sample amplitude on the vertical axis. Larger positive or negative sample values mean a larger displacement from the centre line.
Amplitude and Loudness
Multiplying sample values by a factor changes the waveform amplitude.
A factor such as 0.5 makes the stored waveform smaller;
a factor above 1.0 can make it larger, but values must remain
inside the valid sample range.
Sample Rate
Sample rate tells us how many audio frames are stored each second.
For example, 44100 Hz means 44,100 frames per second.
More measurements give a more detailed digital representation of the waveform.
Duration
duration = number_of_frames / frame_rate
Example: 88,200 / 44,100 = 2 seconds.
Sample Values vs Audio Frames
This distinction is important. A frame represents one instant in time for all channels. A mono frame contains one sample value. A stereo frame contains two sample values: Left and Right.
Channels
1 = mono, 2 = stereo.
Sample Width
Bytes used for one channel's sample value. Two bytes means 16-bit PCM.
Frame Rate
Number of audio frames per second.
Number of Frames
Total time positions stored in the file.
Frame Size
channels × sample_width bytes per frame.
Duration
nframes / framerate seconds.
WAV Properties and Commands
| Property | Meaning | Typical value |
|---|---|---|
| Channels | Number of audio channels | 1 mono / 2 stereo |
| Sample width | Bytes per channel sample value | 2 bytes = 16-bit |
| Frame rate | Audio frames per second | 44,100 Hz |
| Frames | Total number of audio frames | Depends on duration |
| Frame size | Channels × sample width | 4 bytes for stereo 16-bit |
| Duration | Frames ÷ frame rate | Calculated |
Reading
| Method | Purpose |
|---|---|
wave.open(file, "rb") |
Open a WAV file for reading |
.getnchannels() |
Read number of channels |
.getsampwidth() |
Read sample width in bytes |
.getframerate() |
Read frame rate / sampling frequency |
.getnframes() |
Read total number of audio frames |
.readframes(n) |
Read up to n audio frames as bytes |
.getparams() |
Read all main WAV parameters as one named tuple |
Writing
| Method | Purpose |
|---|---|
wave.open(file, "wb") |
Open a WAV file for writing |
.setnchannels(n) |
Set mono/stereo channel count |
.setsampwidth(n) |
Set sample width in bytes |
.setframerate(n) |
Set frame rate |
.setparams(params) |
Set all main parameters at once |
.writeframes(data) |
Write PCM frame bytes |
Reading WAV File Information
1 Inspect a WAV File
import wave
with wave.open("song.wav", "rb") as wav:
channels = wav.getnchannels()
sample_width = wav.getsampwidth()
frame_rate = wav.getframerate()
n_frames = wav.getnframes()
duration = n_frames / frame_rate
frame_size = channels * sample_width
print(f"Channels: {channels}")
print(f"Sample width: {sample_width} bytes")
print(f"Frame rate: {frame_rate} Hz")
print(f"Frames: {n_frames}")
print(f"Frame size: {frame_size} bytes")
print(f"Duration: {duration:.2f} seconds")Why use with?
with wave.open(...) automatically closes the WAV stream
when the block finishes, so a separate .close() call is usually
unnecessary.
Reading Only Part of a Sound
You do not always need to read the entire WAV file.
readframes(n) reads only the specified number of
audio frames.
Example: First Half of a WAV File
First find the total number of frames, then divide it by two:
half = wav.getnframes() // 2
Reading half frames gives the
first half of the sound.
2 Save the First Half
import wave
with wave.open("in.wav", "rb") as wav_in:
params = wav_in.getparams()
total_frames = wav_in.getnframes()
half = total_frames // 2
first_half = wav_in.readframes(half)
with wave.open("out.wav", "wb") as wav_out:
wav_out.setparams(params)
wav_out.writeframes(first_half)Key Idea
readframes(n) starts reading from the current position.
Immediately after opening the file, the current position is the
beginning of the audio.
Interactive Tone Generator
A pure tone can be generated with a sine function. Change the frequency and amplitude below. The waveform and the playable WAV example are rebuilt in your browser.
Generated PCM WAV
1 second · mono · 16-bit · 44,100 Hz. The audio is generated locally in your browser.
Tone Parameters
440 Hz
16000
Generate a Tone in Python
2 Create A4 = 440 Hz
import wave
import struct
import math
frequency = 440
duration = 2
sample_rate = 44100
amplitude = 16000
audio = bytearray()
for i in range(int(sample_rate * duration)):
t = i / sample_rate
value = int(
amplitude *
math.sin(2 * math.pi * frequency * t)
)
# 16-bit signed little-endian integer
audio.extend(struct.pack("<h", value))
with wave.open("tone_440hz.wav", "wb") as wav:
wav.setnchannels(1)
wav.setsampwidth(2)
wav.setframerate(sample_rate)
wav.writeframes(audio)
print("Created tone_440hz.wav")<
h
Changing Volume
For 16-bit PCM audio, sample values lie between
-32768 and 32767. Multiplying every sample by
0.5 reduces the waveform amplitude to half.
Original amplitude
× 0.5 amplitude
440 Hz tone
Generated at amplitude 20,000.
Same samples × 0.5
Same frequency and duration, smaller sample values.
3 Reduce Volume of 16-bit PCM Audio
import wave
import struct
factor = 0.5
with wave.open("song.wav", "rb") as wav_in:
params = wav_in.getparams()
frames = wav_in.readframes(wav_in.getnframes())
if params.sampwidth != 2:
raise ValueError("This example expects 16-bit PCM audio")
count = len(frames) // 2
fmt = "<" + "h" * count
samples = struct.unpack(fmt, frames)
changed = []
for sample in samples:
value = int(sample * factor)
# Clamp to signed 16-bit range
value = max(-32768, min(32767, value))
changed.append(value)
output = struct.pack(fmt, *changed)
with wave.open("quiet.wav", "wb") as wav_out:
wav_out.setparams(params)
wav_out.writeframes(output)Changing Playback Speed
The frame rate tells the audio player how many frames should be played every second. If we keep the same audio frames but change the frame rate, the playback speed changes.
2× Slower
Divide the original frame rate by 2.
new_rate = old_rate // 2
Original
Example original frame rate.
44100 Hz
2× Faster
Multiply the original frame rate by 2.
new_rate = old_rate * 2
Speed Formula
New frame rate = original frame rate × speed factor
2× faster → frame_rate × 2
2× slower → frame_rate ÷ 2
4 Speed Up Audio 2×
import wave
with wave.open("in.wav", "rb") as wav_in:
params = wav_in.getparams()
frames = wav_in.readframes(wav_in.getnframes())
with wave.open("out.wav", "wb") as wav_out:
wav_out.setnchannels(params.nchannels)
wav_out.setsampwidth(params.sampwidth)
# Play the same frames twice as fast
wav_out.setframerate(params.framerate * 2)
wav_out.writeframes(frames)5 Slow Down Audio 2×
import wave
with wave.open("in.wav", "rb") as wav_in:
params = wav_in.getparams()
frames = wav_in.readframes(wav_in.getnframes())
with wave.open("out.wav", "wb") as wav_out:
wav_out.setnchannels(params.nchannels)
wav_out.setsampwidth(params.sampwidth)
# Play the same frames twice as slowly
wav_out.setframerate(params.framerate // 2)
wav_out.writeframes(frames)Important: Speed Also Changes Pitch
Changing the frame rate changes both playback speed and pitch. Faster playback sounds higher; slower playback sounds lower. This is different from advanced time-stretching algorithms that can change speed while preserving pitch.
Reversing Audio Correctly
To reverse stereo audio safely, reverse the order of frames, not the order of individual channel samples. This keeps Left and Right values together.
Original stereo frame order
Correct reversed frame order
Rising chirp
Frequency rises during the sound.
Same PCM frames reversed
The rising chirp becomes a falling chirp.
4 Reverse WAV Frame-by-Frame
import wave
with wave.open("song.wav", "rb") as wav_in:
params = wav_in.getparams()
audio = wav_in.readframes(wav_in.getnframes())
# One frame contains one sample value per channel
frame_size = params.nchannels * params.sampwidth
frames = [
audio[i:i + frame_size]
for i in range(0, len(audio), frame_size)
]
reversed_data = b"".join(reversed(frames))
with wave.open("reversed.wav", "wb") as wav_out:
wav_out.setparams(params)
wav_out.writeframes(reversed_data)
print("Audio reversed frame-by-frame")Listen and Compare
These examples are generated locally as mono 16-bit PCM WAV data. Listen for the relationship between waveform properties and what you hear.
Lower frequency
A lower pitch.
A4 reference tone
Twice the frequency of 220 Hz.
Higher frequency
A higher pitch.
Smaller amplitude
Frequency remains 440 Hz; only sample amplitude is reduced.
Why Do We Need struct?
wave.readframes() gives you bytes.
Mathematical operations need numbers. The struct module helps
convert between binary byte sequences and Python integer sample values.
Bytes → integers
struct.unpack() interprets binary audio data as numeric values.
Integers → bytes
struct.pack() converts calculated sample values back to binary data.
<h
< = little-endian, h = signed 16-bit short integer.
import struct
value = 1023
# integer → two bytes
packed = struct.pack("<h", value)
# two bytes → integer
unpacked = struct.unpack("<h", packed)[0]
print(unpacked) # 1023Important Limits
1. Not Every .wav File Is Supported
Python's wave module is designed for
uncompressed PCM WAV. A file can have the
.wav extension and still use an unsupported encoding.
2. Frame ≠ Individual Sample Value
In stereo, one frame contains two channel sample values.
getnframes() reports frames, not
left_samples + right_samples.
3. Do Not Reverse Stereo Sample Values Individually
Reversing the flat sequence L0, R0, L1, R1... produces
...R1, L1, R0, L0, swapping channel order inside frames.
Reverse complete frames instead.
4. The 16-bit Range Is Signed
For 16-bit signed PCM used in these examples, values must remain within
-32768 ... 32767.
5. Sample Width Matters
Code using struct.pack("<h", ...) or
struct.unpack(... "h" ...) assumes 2-byte / 16-bit sample values.
Check getsampwidth() before applying such code to unknown files.
Practice Tasks
Name the four methods used to read channels, sample width, frame rate and number of frames from a WAV file.
Explain the difference between an audio frame and an individual channel sample value.
Write a program that reads a WAV file and prints channels, sample width, frame rate, frame count and duration.
Generate a 3-second mono, 16-bit tone at
261 Hz and save it as middle_c.wav.
A stereo 16-bit WAV has sample_width = 2.
How many bytes are in one frame? Explain your calculation.
Explain why reversing individual values in the sequence
L0, R0, L1, R1 is not a correct stereo reverse.
Create two versions of a 16-bit WAV:
one at 50% volume and one at 150%.
Clamp the louder version to the valid signed 16-bit range.
Generate a short sequence of three notes by producing one sine-wave section after another and writing them into a single WAV file.
Self-Check Quiz
Q1: What does getframerate() return?
44100.
Q2: What does getnframes() return?
Q3: How do you calculate duration?
duration = n_frames / frame_rate.
Q4: What is one stereo frame made from?
Q5: What is the frame size of stereo 16-bit PCM?
2 channels × 2 bytes = 4 bytes per frame.
Q6: What does struct.pack("<h", value) mean?
Q7: Why do we clamp volume-adjusted 16-bit samples?
-32768 ... 32767.
Q8: Why should stereo audio be reversed frame-by-frame?
Official Reference
Python wave module documentation
Official reference for wave.open(), Wave_read,
Wave_write, frames, parameters and PCM WAV support.
End of Term 1 — Online Formative Assessment
Review all six Programming topics, then check your understanding across seven learning objectives.
Start Term 1 FA →