mp3peaks 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mp3peaks might be problematic. Click here for more details.

@@ -0,0 +1,243 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "autocfg"
7
+ version = "1.5.1"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
10
+
11
+ [[package]]
12
+ name = "bitflags"
13
+ version = "2.13.2"
14
+ source = "registry+https://github.com/rust-lang/crates.io-index"
15
+ checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
16
+
17
+ [[package]]
18
+ name = "bytemuck"
19
+ version = "1.25.2"
20
+ source = "registry+https://github.com/rust-lang/crates.io-index"
21
+ checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
22
+
23
+ [[package]]
24
+ name = "heck"
25
+ version = "0.5.0"
26
+ source = "registry+https://github.com/rust-lang/crates.io-index"
27
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
28
+
29
+ [[package]]
30
+ name = "lazy_static"
31
+ version = "1.5.1"
32
+ source = "registry+https://github.com/rust-lang/crates.io-index"
33
+ checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
34
+
35
+ [[package]]
36
+ name = "libc"
37
+ version = "0.2.191"
38
+ source = "registry+https://github.com/rust-lang/crates.io-index"
39
+ checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
40
+
41
+ [[package]]
42
+ name = "log"
43
+ version = "0.4.35"
44
+ source = "registry+https://github.com/rust-lang/crates.io-index"
45
+ checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c"
46
+
47
+ [[package]]
48
+ name = "mp3peaks"
49
+ version = "0.1.0"
50
+ dependencies = [
51
+ "pyo3",
52
+ "symphonia",
53
+ ]
54
+
55
+ [[package]]
56
+ name = "num-complex"
57
+ version = "0.4.6"
58
+ source = "registry+https://github.com/rust-lang/crates.io-index"
59
+ checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
60
+ dependencies = [
61
+ "num-traits",
62
+ ]
63
+
64
+ [[package]]
65
+ name = "num-traits"
66
+ version = "0.2.19"
67
+ source = "registry+https://github.com/rust-lang/crates.io-index"
68
+ checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
69
+ dependencies = [
70
+ "autocfg",
71
+ ]
72
+
73
+ [[package]]
74
+ name = "once_cell"
75
+ version = "1.21.4"
76
+ source = "registry+https://github.com/rust-lang/crates.io-index"
77
+ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
78
+
79
+ [[package]]
80
+ name = "portable-atomic"
81
+ version = "1.15.0"
82
+ source = "registry+https://github.com/rust-lang/crates.io-index"
83
+ checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
84
+
85
+ [[package]]
86
+ name = "proc-macro2"
87
+ version = "1.0.107"
88
+ source = "registry+https://github.com/rust-lang/crates.io-index"
89
+ checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
90
+ dependencies = [
91
+ "unicode-ident",
92
+ ]
93
+
94
+ [[package]]
95
+ name = "pyo3"
96
+ version = "0.30.1"
97
+ source = "registry+https://github.com/rust-lang/crates.io-index"
98
+ checksum = "229ca13b8a15bb0d1487b81ab975bfcd97513416499a8312011b6036e42e0de4"
99
+ dependencies = [
100
+ "libc",
101
+ "once_cell",
102
+ "portable-atomic",
103
+ "pyo3-build-config",
104
+ "pyo3-ffi",
105
+ "pyo3-macros",
106
+ ]
107
+
108
+ [[package]]
109
+ name = "pyo3-build-config"
110
+ version = "0.30.1"
111
+ source = "registry+https://github.com/rust-lang/crates.io-index"
112
+ checksum = "70130e2b6dd1ee589a8788026a429561ca5c40fa7a2fa7914c2282eddae24700"
113
+ dependencies = [
114
+ "target-lexicon",
115
+ ]
116
+
117
+ [[package]]
118
+ name = "pyo3-ffi"
119
+ version = "0.30.1"
120
+ source = "registry+https://github.com/rust-lang/crates.io-index"
121
+ checksum = "5e7ff73c555c286d212d4ce7e5fbda845a973c4f61bd5f160f5f2c3c2a04d1c4"
122
+ dependencies = [
123
+ "libc",
124
+ "pyo3-build-config",
125
+ ]
126
+
127
+ [[package]]
128
+ name = "pyo3-macros"
129
+ version = "0.30.1"
130
+ source = "registry+https://github.com/rust-lang/crates.io-index"
131
+ checksum = "c46d2e5862c7bb8207e3b639f713dda3c499933444784154c11d013c5a691b60"
132
+ dependencies = [
133
+ "proc-macro2",
134
+ "pyo3-macros-backend",
135
+ "quote",
136
+ "syn",
137
+ ]
138
+
139
+ [[package]]
140
+ name = "pyo3-macros-backend"
141
+ version = "0.30.1"
142
+ source = "registry+https://github.com/rust-lang/crates.io-index"
143
+ checksum = "38b89f066f2ae72a2aef52d1537ca2fcd104440ebfadee06e97aa90738a25b9b"
144
+ dependencies = [
145
+ "heck",
146
+ "proc-macro2",
147
+ "quote",
148
+ "syn",
149
+ ]
150
+
151
+ [[package]]
152
+ name = "quote"
153
+ version = "1.0.47"
154
+ source = "registry+https://github.com/rust-lang/crates.io-index"
155
+ checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
156
+ dependencies = [
157
+ "proc-macro2",
158
+ ]
159
+
160
+ [[package]]
161
+ name = "regex-lite"
162
+ version = "0.1.9"
163
+ source = "registry+https://github.com/rust-lang/crates.io-index"
164
+ checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973"
165
+
166
+ [[package]]
167
+ name = "smallvec"
168
+ version = "1.16.4"
169
+ source = "registry+https://github.com/rust-lang/crates.io-index"
170
+ checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8"
171
+
172
+ [[package]]
173
+ name = "symphonia"
174
+ version = "0.6.1"
175
+ source = "registry+https://github.com/rust-lang/crates.io-index"
176
+ checksum = "a7edef6a96b696d4e0cab5ee9ebb7ca155ed95f30a6b45bbb8b97d2727f02424"
177
+ dependencies = [
178
+ "lazy_static",
179
+ "symphonia-bundle-mp3",
180
+ "symphonia-core",
181
+ "symphonia-metadata",
182
+ ]
183
+
184
+ [[package]]
185
+ name = "symphonia-bundle-mp3"
186
+ version = "0.6.1"
187
+ source = "registry+https://github.com/rust-lang/crates.io-index"
188
+ checksum = "98ea5ffc8716bff677dfb3b01b420c7b758de901a72b8c330bf2040ab74b4add"
189
+ dependencies = [
190
+ "lazy_static",
191
+ "log",
192
+ "symphonia-core",
193
+ ]
194
+
195
+ [[package]]
196
+ name = "symphonia-core"
197
+ version = "0.6.1"
198
+ source = "registry+https://github.com/rust-lang/crates.io-index"
199
+ checksum = "01c412864d599d4750d0c3d684d7e093ec05e5309681ef5252cc1096a437f6e0"
200
+ dependencies = [
201
+ "bitflags",
202
+ "bytemuck",
203
+ "lazy_static",
204
+ "log",
205
+ "num-complex",
206
+ "smallvec",
207
+ ]
208
+
209
+ [[package]]
210
+ name = "symphonia-metadata"
211
+ version = "0.6.1"
212
+ source = "registry+https://github.com/rust-lang/crates.io-index"
213
+ checksum = "83713a97705d77bdef7cdbc0768fd6e5a54e4cd7e48d60a806ae85639e2c87c6"
214
+ dependencies = [
215
+ "lazy_static",
216
+ "log",
217
+ "regex-lite",
218
+ "smallvec",
219
+ "symphonia-core",
220
+ ]
221
+
222
+ [[package]]
223
+ name = "syn"
224
+ version = "3.0.9"
225
+ source = "registry+https://github.com/rust-lang/crates.io-index"
226
+ checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
227
+ dependencies = [
228
+ "proc-macro2",
229
+ "quote",
230
+ "unicode-ident",
231
+ ]
232
+
233
+ [[package]]
234
+ name = "target-lexicon"
235
+ version = "0.13.5"
236
+ source = "registry+https://github.com/rust-lang/crates.io-index"
237
+ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
238
+
239
+ [[package]]
240
+ name = "unicode-ident"
241
+ version = "1.0.27"
242
+ source = "registry+https://github.com/rust-lang/crates.io-index"
243
+ checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
@@ -0,0 +1,41 @@
1
+ [package]
2
+ name = "mp3peaks"
3
+ # The single source of the version: pyproject.toml reads it from here.
4
+ version = "0.1.0"
5
+ edition = "2024"
6
+ rust-version = "1.99"
7
+ description = "audiowaveform-compatible peak waveforms for MP3s, decoded in memory-safe Rust, for Python"
8
+ license = "MIT"
9
+ repository = "https://github.com/monty5811/mp3peaks"
10
+ readme = "README.md"
11
+ keywords = ["audio", "waveform", "mp3", "peaks", "python"]
12
+ categories = ["multimedia::audio"]
13
+ # Only the Python package is released. The crate is a cdylib with nothing to
14
+ # offer Rust users yet, so it stays off crates.io.
15
+ publish = false
16
+ # What goes in the sdist (maturin uses cargo's package list). Leaves out the
17
+ # tests, dev scripts and CI config.
18
+ include = [
19
+ "/Cargo.toml",
20
+ "/Cargo.lock",
21
+ "/LICENSE",
22
+ "/README.md",
23
+ "/THIRD_PARTY_NOTICES.md",
24
+ "/pyproject.toml",
25
+ "/src/**",
26
+ "/mp3peaks.pyi",
27
+ ]
28
+
29
+ [lib]
30
+ name = "mp3peaks"
31
+ crate-type = ["cdylib"]
32
+
33
+ [profile.release]
34
+ lto = true
35
+ codegen-units = 1
36
+
37
+ [dependencies]
38
+ # extension-module is switched on by maturin (pyproject.toml) rather than here,
39
+ # so `cargo test` can link a test binary.
40
+ pyo3 = { version = "0.30.1", features = ["abi3-py311"] }
41
+ symphonia = { version = "0.6.1", default-features = false, features = ["mp3"] }
mp3peaks-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The mp3peaks authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,189 @@
1
+ Metadata-Version: 2.4
2
+ Name: mp3peaks
3
+ Version: 0.1.0
4
+ Classifier: Development Status :: 4 - Beta
5
+ Classifier: Intended Audience :: Developers
6
+ Classifier: Operating System :: MacOS
7
+ Classifier: Operating System :: Microsoft :: Windows
8
+ Classifier: Operating System :: POSIX :: Linux
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3 :: Only
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Programming Language :: Python :: Implementation :: CPython
16
+ Classifier: Programming Language :: Rust
17
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
18
+ Classifier: Typing :: Typed
19
+ License-File: LICENSE
20
+ License-File: THIRD_PARTY_NOTICES.md
21
+ Summary: audiowaveform-compatible peak waveforms for MP3s, decoded in memory-safe Rust (symphonia)
22
+ Keywords: audio,waveform,mp3,peaks,audiowaveform,peaks.js
23
+ License-Expression: MIT
24
+ Requires-Python: >=3.11
25
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
26
+ Project-URL: Changelog, https://github.com/monty5811/mp3peaks/blob/main/CHANGELOG.md
27
+ Project-URL: Homepage, https://github.com/monty5811/mp3peaks
28
+ Project-URL: Issues, https://github.com/monty5811/mp3peaks/issues
29
+ Project-URL: Source, https://github.com/monty5811/mp3peaks
30
+
31
+ # mp3peaks
32
+
33
+ [`audiowaveform`](https://github.com/bbc/audiowaveform)-compatible peak
34
+ waveforms for MP3 files, from a `pip install`. The MP3 is decoded in
35
+ memory-safe Rust by [symphonia](https://github.com/pdeljanov/Symphonia), and
36
+ the result is the JSON that `audiowaveform --output-format json` writes, as
37
+ read by [peaks.js](https://github.com/bbc/peaks.js) and
38
+ [waveform-data.js](https://github.com/bbc/waveform-data.js).
39
+
40
+ ```python
41
+ from mp3peaks import compute_waveform
42
+
43
+ with open("talk.mp3", "rb") as f:
44
+ waveform = compute_waveform(f.read(), number_of_points=256, bits=8)
45
+
46
+ # {"version": 2, "channels": 1, "sample_rate": 44100, "samples_per_pixel": 31556,
47
+ # "bits": 8, "length": 256, "data": [-3, 4, -41, 38, ...]}
48
+ ```
49
+
50
+ ```
51
+ pip install mp3peaks
52
+ ```
53
+
54
+ ## Why
55
+
56
+ - **No system packages.** Wheels are self-contained and have no Python
57
+ dependencies. There's no binary to install and no subprocess or temp files.
58
+ - **Memory-safe decoding.** If you build waveforms from files people upload,
59
+ those bytes never reach a C decoder (libmad in `audiowaveform`, or FFmpeg).
60
+ - **Fast.** On 50 real recordings it took a median of 1.83s per file, against
61
+ 2.70s for `audiowaveform` (see below). The GIL is released while it decodes,
62
+ so threads run in parallel.
63
+
64
+ It was built for [Yet Another Sermon Host](https://yetanothersermon.host) to
65
+ replace calls to the `audiowaveform` binary, and is published in case it's
66
+ useful to anyone else. It's an independent project, not affiliated with the
67
+ BBC or the `audiowaveform` authors.
68
+
69
+ ## Scope
70
+
71
+ It does one thing, deliberately:
72
+
73
+ - **Input:** MP3 only, as bytes in memory.
74
+ - **Output:** `audiowaveform` JSON, format version 2, as a Python dict. Always
75
+ one channel: stereo is mixed down to mono the way `audiowaveform` does it.
76
+ - **Resolution:** 8 or 16 bits. You choose the number of points, not a zoom
77
+ level or samples per pixel.
78
+
79
+ There's no binary `.dat` output, no image rendering, no other input formats and
80
+ no multi-channel output. If you need any of those, use `audiowaveform`.
81
+
82
+ ## API
83
+
84
+ ```python
85
+ compute_waveform(
86
+ data: bytes,
87
+ *,
88
+ number_of_points: int = 256,
89
+ bits: int = 8, # 8 or 16
90
+ duration: float | None = None,
91
+ ) -> dict
92
+ ```
93
+
94
+ - `number_of_points` is between 1 and 16,777,216 (2^24).
95
+ - `duration` (in seconds) sizes the buckets:
96
+ `samples_per_pixel = ceil(sample_rate * duration / number_of_points)`,
97
+ rounded up so the buckets always cover the whole duration. Without it, the
98
+ stream's own duration is used. If given, it must be positive and finite.
99
+ Passing a known duration gives the exact number of points even when the
100
+ MP3's header is wrong.
101
+ - Every decoded sample lands in a bucket, so a few points more than
102
+ `number_of_points` usually come back: the decoded audio runs past the
103
+ duration by the encoder delay and padding. If it runs past by much more
104
+ (a `duration` far too short), neighbouring buckets are merged in pairs,
105
+ doubling `samples_per_pixel`, so at most `2 * (number_of_points + 1)`
106
+ points come back.
107
+ - Encoder delay and padding are decoded rather than trimmed, as
108
+ `audiowaveform` (libmad) does, so the peaks line up with its output.
109
+ - A truncated file gives a waveform of the part that decodes. Corrupt frames
110
+ are skipped. A stream that changes channel count partway (mono to stereo,
111
+ say) decodes to the end. One that changes sample rate raises
112
+ `WaveformError`.
113
+ - Raises `mp3peaks.WaveformError` if the bytes can't be decoded or hold
114
+ no audio, and `ValueError` for bad arguments.
115
+
116
+ Type hints ship with the package.
117
+
118
+ ## Compared with audiowaveform
119
+
120
+ The values follow `audiowaveform`'s rules (see `src/lib.rs`): each channel is
121
+ rounded to 16 bits, channels are averaged with truncation, and 8-bit values
122
+ are divided by 256 rounding toward zero.
123
+
124
+ On 50 real speech recordings from 2008-2026, covering 7 channel and sample-rate
125
+ combinations (41 of them stereo):
126
+
127
+ | | Decoder | Median per file | vs audiowaveform 1.10.1 |
128
+ |---|---|---|---|
129
+ | audiowaveform 1.10.1 (C++) | libmad | 2.70s | - |
130
+ | mp3peaks | symphonia | 1.83s | 45/50 within ±1 (median mean difference 0.002) |
131
+
132
+ The other 5 files are MPEG-2 files that use intensity stereo. libmad decodes
133
+ those about 20% too quietly. symphonia, FFmpeg and mpg123 all agree with each
134
+ other on them, so this package's output is the correct one there.
135
+
136
+ `scripts/compare_with_audiowaveform.py` runs that comparison. It isn't part of
137
+ the package, and needs the `audiowaveform` binary and some MP3s of your own:
138
+
139
+ make compare AUDIOWAVEFORM=/path/to/audiowaveform ARGS="path/to/mp3s"
140
+
141
+ ## Platforms
142
+
143
+ Wheels are built for CPython's stable ABI, so one wheel per platform covers
144
+ Python 3.11 and later:
145
+
146
+ - Linux x86_64 and aarch64 (`manylinux_2_17`, glibc 2.17+)
147
+ - macOS arm64 and x86_64
148
+ - Windows x86_64
149
+
150
+ Anywhere else, pip builds it from the sdist, which needs a Rust toolchain
151
+ (1.99+).
152
+
153
+ ## Support
154
+
155
+ This is maintained for its original use. Bug reports are
156
+ welcome, especially files it decodes differently from other decoders, but
157
+ there's no roadmap and no promise of support. Requests for more formats or
158
+ options will probably be declined to keep the scope small.
159
+
160
+ ## Development
161
+
162
+ Needs cargo (Rust 1.99+) and [uv](https://docs.astral.sh/uv/).
163
+
164
+ make check # rustfmt, clippy, cargo test, ruff, pytest, notices up to date
165
+ make develop # rebuild into .venv after changing src/
166
+ make build # a manylinux wheel for this machine, via zig
167
+ make sdist # the source distribution
168
+ make notices # regenerate THIRD_PARTY_NOTICES.md after changing dependencies
169
+
170
+ `requirements-dev.txt` is only for the tests and scripts. PyAV is there to
171
+ encode test MP3s and to read stream info in the comparison script, not to
172
+ build waveforms.
173
+
174
+ ### Releasing
175
+
176
+ 1. Bump `version` in `Cargo.toml` (the only place it's set) and add a
177
+ `CHANGELOG.md` entry.
178
+ 2. Commit, then tag the commit `v<version>` (for example `v0.1.0`) and push
179
+ the tag.
180
+ 3. `.github/workflows/release.yml` checks the tag matches the version, builds
181
+ the wheels and sdist, smoke-tests each wheel, and publishes to PyPI with
182
+ trusted publishing. No API token is stored anywhere.
183
+
184
+ ## License
185
+
186
+ MIT, see `LICENSE`. The wheels include symphonia, which is MPL-2.0, and PyO3
187
+ and other crates under MIT or Apache-2.0. Their licenses are in
188
+ `THIRD_PARTY_NOTICES.md`, which ships in every wheel and the sdist.
189
+
@@ -0,0 +1,158 @@
1
+ # mp3peaks
2
+
3
+ [`audiowaveform`](https://github.com/bbc/audiowaveform)-compatible peak
4
+ waveforms for MP3 files, from a `pip install`. The MP3 is decoded in
5
+ memory-safe Rust by [symphonia](https://github.com/pdeljanov/Symphonia), and
6
+ the result is the JSON that `audiowaveform --output-format json` writes, as
7
+ read by [peaks.js](https://github.com/bbc/peaks.js) and
8
+ [waveform-data.js](https://github.com/bbc/waveform-data.js).
9
+
10
+ ```python
11
+ from mp3peaks import compute_waveform
12
+
13
+ with open("talk.mp3", "rb") as f:
14
+ waveform = compute_waveform(f.read(), number_of_points=256, bits=8)
15
+
16
+ # {"version": 2, "channels": 1, "sample_rate": 44100, "samples_per_pixel": 31556,
17
+ # "bits": 8, "length": 256, "data": [-3, 4, -41, 38, ...]}
18
+ ```
19
+
20
+ ```
21
+ pip install mp3peaks
22
+ ```
23
+
24
+ ## Why
25
+
26
+ - **No system packages.** Wheels are self-contained and have no Python
27
+ dependencies. There's no binary to install and no subprocess or temp files.
28
+ - **Memory-safe decoding.** If you build waveforms from files people upload,
29
+ those bytes never reach a C decoder (libmad in `audiowaveform`, or FFmpeg).
30
+ - **Fast.** On 50 real recordings it took a median of 1.83s per file, against
31
+ 2.70s for `audiowaveform` (see below). The GIL is released while it decodes,
32
+ so threads run in parallel.
33
+
34
+ It was built for [Yet Another Sermon Host](https://yetanothersermon.host) to
35
+ replace calls to the `audiowaveform` binary, and is published in case it's
36
+ useful to anyone else. It's an independent project, not affiliated with the
37
+ BBC or the `audiowaveform` authors.
38
+
39
+ ## Scope
40
+
41
+ It does one thing, deliberately:
42
+
43
+ - **Input:** MP3 only, as bytes in memory.
44
+ - **Output:** `audiowaveform` JSON, format version 2, as a Python dict. Always
45
+ one channel: stereo is mixed down to mono the way `audiowaveform` does it.
46
+ - **Resolution:** 8 or 16 bits. You choose the number of points, not a zoom
47
+ level or samples per pixel.
48
+
49
+ There's no binary `.dat` output, no image rendering, no other input formats and
50
+ no multi-channel output. If you need any of those, use `audiowaveform`.
51
+
52
+ ## API
53
+
54
+ ```python
55
+ compute_waveform(
56
+ data: bytes,
57
+ *,
58
+ number_of_points: int = 256,
59
+ bits: int = 8, # 8 or 16
60
+ duration: float | None = None,
61
+ ) -> dict
62
+ ```
63
+
64
+ - `number_of_points` is between 1 and 16,777,216 (2^24).
65
+ - `duration` (in seconds) sizes the buckets:
66
+ `samples_per_pixel = ceil(sample_rate * duration / number_of_points)`,
67
+ rounded up so the buckets always cover the whole duration. Without it, the
68
+ stream's own duration is used. If given, it must be positive and finite.
69
+ Passing a known duration gives the exact number of points even when the
70
+ MP3's header is wrong.
71
+ - Every decoded sample lands in a bucket, so a few points more than
72
+ `number_of_points` usually come back: the decoded audio runs past the
73
+ duration by the encoder delay and padding. If it runs past by much more
74
+ (a `duration` far too short), neighbouring buckets are merged in pairs,
75
+ doubling `samples_per_pixel`, so at most `2 * (number_of_points + 1)`
76
+ points come back.
77
+ - Encoder delay and padding are decoded rather than trimmed, as
78
+ `audiowaveform` (libmad) does, so the peaks line up with its output.
79
+ - A truncated file gives a waveform of the part that decodes. Corrupt frames
80
+ are skipped. A stream that changes channel count partway (mono to stereo,
81
+ say) decodes to the end. One that changes sample rate raises
82
+ `WaveformError`.
83
+ - Raises `mp3peaks.WaveformError` if the bytes can't be decoded or hold
84
+ no audio, and `ValueError` for bad arguments.
85
+
86
+ Type hints ship with the package.
87
+
88
+ ## Compared with audiowaveform
89
+
90
+ The values follow `audiowaveform`'s rules (see `src/lib.rs`): each channel is
91
+ rounded to 16 bits, channels are averaged with truncation, and 8-bit values
92
+ are divided by 256 rounding toward zero.
93
+
94
+ On 50 real speech recordings from 2008-2026, covering 7 channel and sample-rate
95
+ combinations (41 of them stereo):
96
+
97
+ | | Decoder | Median per file | vs audiowaveform 1.10.1 |
98
+ |---|---|---|---|
99
+ | audiowaveform 1.10.1 (C++) | libmad | 2.70s | - |
100
+ | mp3peaks | symphonia | 1.83s | 45/50 within ±1 (median mean difference 0.002) |
101
+
102
+ The other 5 files are MPEG-2 files that use intensity stereo. libmad decodes
103
+ those about 20% too quietly. symphonia, FFmpeg and mpg123 all agree with each
104
+ other on them, so this package's output is the correct one there.
105
+
106
+ `scripts/compare_with_audiowaveform.py` runs that comparison. It isn't part of
107
+ the package, and needs the `audiowaveform` binary and some MP3s of your own:
108
+
109
+ make compare AUDIOWAVEFORM=/path/to/audiowaveform ARGS="path/to/mp3s"
110
+
111
+ ## Platforms
112
+
113
+ Wheels are built for CPython's stable ABI, so one wheel per platform covers
114
+ Python 3.11 and later:
115
+
116
+ - Linux x86_64 and aarch64 (`manylinux_2_17`, glibc 2.17+)
117
+ - macOS arm64 and x86_64
118
+ - Windows x86_64
119
+
120
+ Anywhere else, pip builds it from the sdist, which needs a Rust toolchain
121
+ (1.99+).
122
+
123
+ ## Support
124
+
125
+ This is maintained for its original use. Bug reports are
126
+ welcome, especially files it decodes differently from other decoders, but
127
+ there's no roadmap and no promise of support. Requests for more formats or
128
+ options will probably be declined to keep the scope small.
129
+
130
+ ## Development
131
+
132
+ Needs cargo (Rust 1.99+) and [uv](https://docs.astral.sh/uv/).
133
+
134
+ make check # rustfmt, clippy, cargo test, ruff, pytest, notices up to date
135
+ make develop # rebuild into .venv after changing src/
136
+ make build # a manylinux wheel for this machine, via zig
137
+ make sdist # the source distribution
138
+ make notices # regenerate THIRD_PARTY_NOTICES.md after changing dependencies
139
+
140
+ `requirements-dev.txt` is only for the tests and scripts. PyAV is there to
141
+ encode test MP3s and to read stream info in the comparison script, not to
142
+ build waveforms.
143
+
144
+ ### Releasing
145
+
146
+ 1. Bump `version` in `Cargo.toml` (the only place it's set) and add a
147
+ `CHANGELOG.md` entry.
148
+ 2. Commit, then tag the commit `v<version>` (for example `v0.1.0`) and push
149
+ the tag.
150
+ 3. `.github/workflows/release.yml` checks the tag matches the version, builds
151
+ the wheels and sdist, smoke-tests each wheel, and publishes to PyPI with
152
+ trusted publishing. No API token is stored anywhere.
153
+
154
+ ## License
155
+
156
+ MIT, see `LICENSE`. The wheels include symphonia, which is MPL-2.0, and PyO3
157
+ and other crates under MIT or Apache-2.0. Their licenses are in
158
+ `THIRD_PARTY_NOTICES.md`, which ships in every wheel and the sdist.