newwave 0.31.0
Drop-in replacement for Pythons wave.py
Requires Python: >=3.10
Keywords: audio, pcm, wave

# newwave - drop-in replacement for wave.py

_"Enjoy the silence"_

![newwave header](header_image.png)

`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()` and `WaveFormat` enum ([GH-60729](https://github.com/python/cpython/issues/60729), [PR #102574](https://github.com/python/cpython/pull/102574))
- WAVEFORMATEXTENSIBLE write support for >2 channels, >16-bit, and custom channel masks
- `ChannelMask` IntFlag enum for discoverable speaker position configuration
- LIST INFO metadata support (`setmetadata()`, `gettag()`, etc.)
- Convenience `wave.read()` and `wave.write()` functions for simpler API
- Public `wave_params` named tuple with `format` field for typed parameter handling (pending merge in python [GH-94992](https://github.com/python/cpython/issues/94992))
- Fix for `Wave_write` not writing pad byte for odd-sized data chunks ([GH-117716](https://github.com/python/cpython/issues/117716))
- Fix for `setframerate()` accepting values like `0.5` that round to invalid framerate `0` ([GH-132445](https://github.com/python/cpython/issues/132445))


Features backported from newer Python versions:

- Support for `bytes` and path-like objects in `wave.open()` ([GH-140951](https://github.com/python/cpython/pull/140951), in Python since 3.15)
- Fix for `Wave_write` emitting unraisable exception when `open()` raises ([GH-136529](https://github.com/python/cpython/pull/136529), in Python since 3.15)
- Removal of deprecated `setmark`, `getmark`, `getmarkers` interfaces ([GH-105098](https://github.com/python/cpython/pull/105098), in Python since 3.15)
- Support for reading WAVE_FORMAT_EXTENSIBLE files ([GH-96777](https://github.com/python/cpython/pull/96777), in Python since 3.12)

## Installation

```bash
pip install newwave
```
or with uv:

```bash
uv add newwave
```

Requires Python 3.10 or later.

## Usage

Simply replace your `wave` import with `newwave`:

```python
# Before
import wave

# After
import newwave as wave
```

### Reading a WAVE file

```python
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

```python
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 13 predefined tags are supported** (case-insensitive):

`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](https://www.adobe.io/content/dam/udp/assets/open/audio/riffnewmedia.pdf) (Microsoft).

```python
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.

```python
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](CUSTOM_CHUNKS.md) and [examples/custom_chunks.py](examples/custom_chunks.py).

## Examples

The [examples/](examples/) directory contains runnable scripts demonstrating various features:

- [sine_wave.py](examples/sine_wave.py) - Basic sine wave generation
- [metadata.py](examples/metadata.py) - Reading and writing LIST INFO metadata tags
- [custom_chunks.py](examples/custom_chunks.py) - Custom RIFF chunks for arbitrary metadata
- [writing_wave_formats.py](examples/writing_wave_formats.py) - Various audio formats (24-bit, float, multichannel)
- [multi_channel_data_logger.py](examples/multi_channel_data_logger.py) - Using WAVE for non-audio data logging

Run any example with:
```bash
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.PCM` or `WaveFormat.IEEE_FLOAT`
- **channel_mask**: Speaker positions (int or `ChannelMask`). Use `0` for 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:

```python
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_params` namedtuple 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](https://codeberg.org/michielb/newwave/issues).
This is the preferred place so information stays discoverable.

## Development

See [DEVELOPING.md](DEVELOPING.md)

_Make WAVEs!_