Unit 12.1A · Term 1

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

Lesson Presentation

12.1A-wave-library.pdf · Slides for classroom use

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.

← time → vertical position = amplitude

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.

Low sampling density Few measurements of the wave
Medium sampling density More points describe the wave
High sampling density Much finer time resolution

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.

Mono
Frame 0M0
Frame 1M1
Frame 2M2
Frame 3M3
Stereo
Frame 0L0 · R0
Frame 1L1 · R1
Frame 2L2 · R2
Frame 3L3 · R3

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

Frequency 440 Hz
Amplitude 16000
frequency = 440 amplitude = 16000 value = int( amplitude * math.sin(2 * math.pi * frequency * t) )

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")
<
little
endian
h
signed
short
2 bytes

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

Original

440 Hz tone

Generated at amplitude 20,000.

50%

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

F0L0 · R0
F1L1 · R1
F2L2 · R2
F3L3 · R3

Correct reversed frame order

F3L3 · R3
F2L2 · R2
F1L1 · R1
F0L0 · R0
Original

Rising chirp

Frequency rises during the sound.

Reversed

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.

220 Hz

Lower frequency

A lower pitch.

440 Hz

A4 reference tone

Twice the frequency of 220 Hz.

880 Hz

Higher frequency

A higher pitch.

Same 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) # 1023

Important 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

Remember

Name the four methods used to read channels, sample width, frame rate and number of frames from a WAV file.

Understand

Explain the difference between an audio frame and an individual channel sample value.

Apply

Write a program that reads a WAV file and prints channels, sample width, frame rate, frame count and duration.

Apply

Generate a 3-second mono, 16-bit tone at 261 Hz and save it as middle_c.wav.

Analyze

A stereo 16-bit WAV has sample_width = 2. How many bytes are in one frame? Explain your calculation.

Analyze

Explain why reversing individual values in the sequence L0, R0, L1, R1 is not a correct stereo reverse.

Create

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.

Create

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?

The audio frame rate / sampling frequency in frames per second, for example 44100.

Q2: What does getnframes() return?

The number of audio frames in the WAV stream.

Q3: How do you calculate duration?

duration = n_frames / frame_rate.

Q4: What is one stereo frame made from?

One sample value for each channel — normally one Left value and one Right value.

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?

Pack the integer as a little-endian signed 16-bit short value.

Q7: Why do we clamp volume-adjusted 16-bit samples?

Because the valid signed 16-bit range is -32768 ... 32767.

Q8: Why should stereo audio be reversed frame-by-frame?

So the Left and Right values belonging to the same moment stay together while the time order of frames is reversed.

Official Reference

Python wave module documentation

Official reference for wave.open(), Wave_read, Wave_write, frames, parameters and PCM WAV support.

Open docs

End of Term 1 — Online Formative Assessment

Review all six Programming topics, then check your understanding across seven learning objectives.

Start Term 1 FA →