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.
- mp3peaks-0.1.0/Cargo.lock +243 -0
- mp3peaks-0.1.0/Cargo.toml +41 -0
- mp3peaks-0.1.0/LICENSE +21 -0
- mp3peaks-0.1.0/PKG-INFO +189 -0
- mp3peaks-0.1.0/README.md +158 -0
- mp3peaks-0.1.0/THIRD_PARTY_NOTICES.md +577 -0
- mp3peaks-0.1.0/mp3peaks.pyi +28 -0
- mp3peaks-0.1.0/pyproject.toml +52 -0
- mp3peaks-0.1.0/src/lib.rs +459 -0
|
@@ -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.
|
mp3peaks-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
|
mp3peaks-0.1.0/README.md
ADDED
|
@@ -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.
|