- Python 99.7%
- Makefile 0.3%
|
All checks were successful
ci/woodpecker/push/test/1 Pipeline was successful
ci/woodpecker/push/test/2 Pipeline was successful
ci/woodpecker/push/test/3 Pipeline was successful
ci/woodpecker/push/test/4 Pipeline was successful
ci/woodpecker/push/typing Pipeline was successful
ci/woodpecker/push/test/5 Pipeline was successful
Reviewed-on: #2 |
||
|---|---|---|
| .woodpecker | ||
| examples | ||
| specs | ||
| src | ||
| test | ||
| .gitignore | ||
| CHANGES.md | ||
| CUSTOM_CHUNKS.md | ||
| DEVELOPING.md | ||
| header_image.png | ||
| LICENSE.txt | ||
| Makefile | ||
| pyproject.toml | ||
| README.md | ||
| VERSIONING.md | ||
newwave - drop-in replacement for wave.py
"Enjoy the silence"
newwave is a drop-in replacement for Python's standard library wave module,
which is used for reading and writing WAVE audio files.
It backports features from newer Python versions to older ones, so you can use
the latest wave improvements on Python 3.10 and later.
Features
Features only in newwave:
- IEEE float wave file support via
getformat()/setformat()andWaveFormatenum (GH-60729, PR #102574) - WAVEFORMATEXTENSIBLE write support for >2 channels, >16-bit, and custom channel masks
ChannelMaskIntFlag enum for discoverable speaker position configuration- LIST INFO metadata support (
setmetadata(),gettag(), etc.) - Convenience
wave.read()andwave.write()functions for simpler API - Public
wave_paramsnamed tuple withformatfield for typed parameter handling (pending merge in python GH-94992) - Fix for
Wave_writenot writing pad byte for odd-sized data chunks (GH-117716) - Fix for
setframerate()accepting values like0.5that round to invalid framerate0(GH-132445)
Features backported from newer Python versions:
- Support for
bytesand path-like objects inwave.open()(GH-140951, in Python since 3.15) - Fix for
Wave_writeemitting unraisable exception whenopen()raises (GH-136529, in Python since 3.15) - Removal of deprecated
setmark,getmark,getmarkersinterfaces (GH-105098, in Python since 3.15) - Support for reading WAVE_FORMAT_EXTENSIBLE files (GH-96777, in Python since 3.12)
Installation
pip install newwave
or with uv:
uv add newwave
Requires Python 3.10 or later.
Usage
Simply replace your wave import with newwave:
# Before
import wave
# After
import newwave as wave
Reading a WAVE file
import newwave as wave
# Using the convenience function
with wave.read('input.wav') as w:
params = w.getparams()
print(f"{params.nchannels} channels, {params.framerate} Hz, {params.sampwidth * 8}-bit")
data = w.readframes(w.getnframes())
# Or using wave.open() (compatible with stdlib)
with wave.open('input.wav', 'rb') as w:
data = w.readframes(w.getnframes())
Writing a WAVE file
import newwave as wave
# Using the convenience function (all params are keyword-only)
with wave.write('output.wav', channels=2, sampwidth=2, framerate=44100) as w:
w.writeframes(audio_data)
# With IEEE float format
with wave.write('float.wav', channels=1, sampwidth=4, framerate=48000,
format=wave.WaveFormat.IEEE_FLOAT) as w:
w.writeframes(float_audio_data)
# Multichannel (>2 channels or >16-bit auto-uses WAVEFORMATEXTENSIBLE)
with wave.write('surround.wav', channels=6, sampwidth=3, framerate=48000,
channel_mask=wave.ChannelMask.SURROUND_5_1) as w:
w.writeframes(surround_audio_data)
# Or using wave.open() (compatible with stdlib)
with wave.open('output.wav', 'wb') as w:
w.setnchannels(2)
w.setsampwidth(2)
w.setframerate(44100)
w.writeframes(audio_data)
Metadata
newwave supports LIST INFO metadata for tagging audio files using the standard RIFF INFO chunk specification. Only the 13 predefined tags are supported:
title, artist, album, tracknumber, date, genre, comment, copyright, software, engineer, technician, source, subject, keywords
Attempting to use custom tag names will raise a ValueError to prevent silent data loss. For details on each tag, see the RIFF Interchange File Format Specification (Microsoft).
import newwave as wave
# Writing metadata
with wave.write('tagged.wav', channels=2, sampwidth=2, framerate=44100) as w:
w.setmetadata({
'title': 'My Song',
'artist': 'The Artist',
'album': 'The Album',
'date': '2026',
'genre': 'Electronic',
'comment': 'Created with newwave',
})
w.writeframes(audio_data)
# Reading metadata
with wave.read('tagged.wav') as r:
print(r.gettag('title')) # 'My Song'
print(r.getmetadata()) # {'title': 'My Song', 'artist': ...}
Encoding note: Metadata is written as UTF-8. Modern tools (ffprobe, Audacity) handle this correctly, but some legacy tools (mediainfo) may display non-ASCII characters incorrectly. For maximum compatibility with older software, use ASCII-only metadata.
Custom RIFF Chunks
Beyond the 13 standard LIST INFO tags, newwave supports arbitrary custom RIFF chunks for storing application-specific data. Custom chunks can be used to:
- Store encoding parameters and processing history
- Preserve metadata through FLAC encode/decode cycles
- Store binary data (plugin state, MIDI, markers, etc.)
- Attach application-specific information to WAVE files
Custom chunks are written after the audio data and LIST INFO metadata. Each chunk has a 4-byte identifier and arbitrary byte data.
import json
import newwave as wave
# Writing custom chunks
with wave.write('custom.wav', channels=2, sampwidth=2, framerate=44100) as w:
w.writeframes(audio_data)
# Store application metadata as JSON
metadata = {'encoder': 'my-app/1.0', 'quality': 'lossless'}
w.addchunk(b'APP\x00', json.dumps(metadata).encode('utf-8'))
# Store binary data (e.g., processing parameters)
w.addchunk(b'PROC', struct.pack('<ff', 0.5, 1.2))
# Reading custom chunks
with wave.read('custom.wav') as r:
chunks = r.getchunks() # dict of all custom chunks
if b'APP\x00' in chunks:
metadata = json.loads(chunks[b'APP\x00'].decode('utf-8'))
print(f"Encoded by: {metadata['encoder']}")
For detailed examples and FLAC integration patterns, see CUSTOM_CHUNKS.md and examples/custom_chunks.py.
Examples
The examples/ directory contains runnable scripts demonstrating various features:
- sine_wave.py - Basic sine wave generation
- metadata.py - Reading and writing LIST INFO metadata tags
- custom_chunks.py - Custom RIFF chunks for arbitrary metadata
- writing_wave_formats.py - Various audio formats (24-bit, float, multichannel)
- multi_channel_data_logger.py - Using WAVE for non-audio data logging
Run any example with:
PYTHONPATH=src python examples/metadata.py
API Reference
Functions
wave.read(filename) -> Wave_read
Open a WAVE file for reading. Returns a context manager.
wave.write(filename, *, channels=, sampwidth=, framerate=, format=WaveFormat.PCM, channel_mask=None) -> Wave_write
Open a WAVE file for writing. All parameters except filename are keyword-only. Returns a context manager.
- channels: Number of audio channels (1-18 for standard layouts)
- sampwidth: Sample width in bytes (1=8-bit, 2=16-bit, 3=24-bit, 4=32-bit)
- framerate: Sample rate in Hz
- format:
WaveFormat.PCMorWaveFormat.IEEE_FLOAT - channel_mask: Speaker positions (int or
ChannelMask). Use0for data channels without speaker assignment (e.g., lab equipment, sensor data).
Classes
Wave_read
Reader for WAVE files. Key methods:
| Method | Returns | Description |
|---|---|---|
getnchannels() |
int | Number of audio channels |
getsampwidth() |
int | Sample width in bytes |
getframerate() |
int | Sample rate in Hz |
getnframes() |
int | Total number of frames |
getformat() |
WaveFormat | Format type (PCM, IEEE_FLOAT, EXTENSIBLE) |
getparams() |
wave_params | Named tuple with all parameters |
readframes(n) |
bytes | Read up to n frames |
rewind() |
None | Seek to start of audio data |
setpos(pos) |
None | Seek to frame position |
tell() |
int | Current frame position |
getmetadata() |
dict | All LIST INFO tags as a dictionary |
gettag(tag) |
str/None | Single tag value (tag: MetadataTag or str) |
getchunk(chunk_id) |
bytes/None | Custom RIFF chunk data or None if not found |
getchunks() |
dict | All custom chunks as {chunk_id: data} |
Wave_write
Writer for WAVE files. Key methods:
| Method | Description |
|---|---|
writeframes(data) |
Write audio frames and update header |
writeframesraw(data) |
Write frames without updating header |
tell() |
Return number of frames written |
close() |
Finalize header and close file |
setmetadata(dict) |
Set all metadata tags at once (keys: MetadataTag or str) |
settag(tag, value) |
Set a single metadata tag (tag: MetadataTag or str) |
gettag(tag) |
Get a metadata tag value, or None if not set |
addchunk(chunk_id, data) |
Add a custom RIFF chunk (4-byte chunk_id, bytes data) |
getchunk(chunk_id) |
Get custom chunk data or None if not found |
When using wave.open() instead of wave.write(), use these setters before writing:
setnchannels(), setsampwidth(), setframerate(), setformat(), setchannelmask().
Enums
WaveFormat (IntEnum)
| Value | Constant | Description |
|---|---|---|
| 0x0001 | PCM |
Standard integer samples |
| 0x0003 | IEEE_FLOAT |
32/64-bit floating point |
| 0xFFFE | EXTENSIBLE |
Extended format with channel mask |
EXTENSIBLE is automatically used when channels > 2, sampwidth > 2, or channel_mask is set.
ChannelMask (IntFlag)
Individual speaker positions:
| Flag | Bit | Description |
|---|---|---|
FRONT_LEFT |
0x1 | Front left speaker |
FRONT_RIGHT |
0x2 | Front right speaker |
FRONT_CENTER |
0x4 | Front center speaker |
LOW_FREQUENCY |
0x8 | LFE / subwoofer |
BACK_LEFT |
0x10 | Back left speaker |
BACK_RIGHT |
0x20 | Back right speaker |
SIDE_LEFT |
0x200 | Side left speaker |
SIDE_RIGHT |
0x400 | Side right speaker |
Common configurations:
| Preset | Value | Channels | Layout |
|---|---|---|---|
MONO |
0x04 | 1 | Front Center |
STEREO |
0x03 | 2 | FL, FR |
QUAD |
0x33 | 4 | FL, FR, BL, BR |
SURROUND_5_1 |
0x3F | 6 | FL, FR, FC, LFE, BL, BR |
SURROUND_7_1 |
0x63F | 8 | 5.1 + SL, SR |
Use channel_mask=0 for generic data channels without speaker assignment
(e.g., scientific instruments, data loggers).
MetadataTag (Enum)
Standard LIST INFO metadata tags. Use for IDE autocomplete or strings for convenience:
import newwave as wave
# Using enum (recommended for IDE autocomplete)
with wave.write('output.wav', channels=2, sampwidth=2, framerate=44100) as w:
w.settag(wave.MetadataTag.TITLE, 'My Song')
w.settag(wave.MetadataTag.ARTIST, 'The Artist')
# ...
# Using strings (still works)
with wave.write('output.wav', channels=2, sampwidth=2, framerate=44100) as w:
w.settag('title', 'My Song')
w.settag('artist', 'The Artist')
# ...
Available tags in MetadataTag:
| Enum Member | Tag Name | INFO Chunk | Description |
|---|---|---|---|
TITLE |
title |
INAM | Track/song title |
ARTIST |
artist |
IART | Artist/performer |
ALBUM |
album |
IPRD | Album/product name |
TRACKNUMBER |
tracknumber |
ITRK | Track number |
DATE |
date |
ICRD | Creation date (YYYY or YYYY-MM-DD) |
GENRE |
genre |
IGNR | Genre |
COMMENT |
comment |
ICMT | Comments |
COPYRIGHT |
copyright |
ICOP | Copyright notice |
SOFTWARE |
software |
ISFT | Software used to create |
ENGINEER |
engineer |
IENG | Engineer |
TECHNICIAN |
technician |
ITCH | Technician |
SOURCE |
source |
ISRC | Source |
SUBJECT |
subject |
ISBJ | Subject |
KEYWORDS |
keywords |
IKEY | Keywords |
Unknown tag names raise ValueError.
Relationship to CPython
newwave is not a fork in spirit, but a compatibility and incubation layer for
Python’s standard-library wave module.
The goals of this project are to:
- Backport features from newer Python releases to older supported versions.
- Provide fixes for correctness issues in
wave.py, such as malformed headers or incorrect frame padding. - Experiment with and stabilize enhancements (e.g. higher bit depths, extended formats) before proposing them upstream to CPython.
Where practical, fixes and features developed in newwave are intended to be
upstreamed to CPython. Some enhancements may remain in newwave if they are
not suitable for inclusion in the standard library due to
backward-compatibility, policy, or maintenance constraints.
newwave aims to remain a drop-in replacement for wave, preserving the
public API and behavior of the standard library wherever possible.
Authors
Because wave.py and its tests and supporting structure are taken from cpython, there is a long list of authors for this module. I used git-filter-repo to extract this module. This means that in the git repository, you can actually see the 'git blame' history just as you could on cpython. But python is very old at this point and in the first ten years or so many patches were sent in via email lists and so generating a full list of authors for this module from source control is not going to be working. So please consider the list of authors: everyone you find in source control for this repository, plus possibly everyone mentioned in cpython's AUTHORS!
The following contributors have patches in this repository that have not yet been merged to cpython:
- Lionel Koenig - IEEE floating-point wave file support
- Brendan Fahy - Public
wave_paramsnamedtuple API - Michiel W. Beijen - Write support for WAVEFORMATEXTENSIBLE, API improvements, bug fixes, and all other work on this project
License
Because the 'wave' library from Python stdlib is the starting point from this module, the license is the same as Python, meaning: PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2. The full license text from Python is distributed in LICENSE.txt in this release.
Getting help and discussion
For bugs, compatibility issues, feature ideas, or discussions related to this project and its relationship to CPython’s wave module, please open an issue on Codeberg. This is the preferred place so information stays discoverable.
Development
See DEVELOPING.md
Make WAVEs!
