adafruit-circuitpython-flac 1.0.2__py3-none-any.whl

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.
@@ -0,0 +1,176 @@
1
+ Metadata-Version: 2.4
2
+ Name: adafruit-circuitpython-flac
3
+ Version: 1.0.2
4
+ Summary: FLAC decoder and player for CircuitPython, with a turbo (viper) kernel
5
+ Author-email: Adafruit Industries <circuitpython@adafruit.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/adafruit/Adafruit_CircuitPython_FLAC
8
+ Keywords: adafruit,blinka,circuitpython,micropython,flac,audio,decoder,lossless,music,player
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Topic :: Software Development :: Libraries
11
+ Classifier: Topic :: Software Development :: Embedded Systems
12
+ Classifier: Topic :: System :: Hardware
13
+ Classifier: Programming Language :: Python :: 3
14
+ Description-Content-Type: text/x-rst
15
+ License-File: LICENSE
16
+ Requires-Dist: Adafruit-Blinka
17
+ Provides-Extra: optional
18
+ Dynamic: license-file
19
+
20
+ Introduction
21
+ ============
22
+
23
+
24
+ .. image:: https://readthedocs.org/projects/adafruit-circuitpython-flac/badge/?version=latest
25
+ :target: https://docs.circuitpython.org/projects/flac/en/latest/
26
+ :alt: Documentation Status
27
+
28
+
29
+ .. image:: https://raw.githubusercontent.com/adafruit/Adafruit_CircuitPython_Bundle/main/badges/adafruit_discord.svg
30
+ :target: https://adafru.it/discord
31
+ :alt: Discord
32
+
33
+
34
+ .. image:: https://github.com/adafruit/Adafruit_CircuitPython_FLAC/workflows/Build%20CI/badge.svg
35
+ :target: https://github.com/adafruit/Adafruit_CircuitPython_FLAC/actions
36
+ :alt: Build Status
37
+
38
+
39
+ .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
40
+ :target: https://github.com/astral-sh/ruff
41
+ :alt: Code Style: Ruff
42
+
43
+ Play FLAC files on CircuitPython. ``FLACDecoder`` decodes FLAC (lossless) audio to 16-bit PCM,
44
+ and ``FLACPlayer`` streams it to an ``I2SOut``, ``PWMAudioOut`` or ``AudioOut`` while your code
45
+ keeps running. On an RP2350 it plays 16-bit/44.1 kHz and 24-bit/96 kHz files from a microSD
46
+ card at the stock 150 MHz.
47
+
48
+ The decoder is written from RFC 9639 and decodes bit-exact. Its hot loops run in a turbo
49
+ (``@micropython.viper``) kernel, prebuilt for each chip architecture.
50
+
51
+
52
+ Dependencies
53
+ =============
54
+ This library depends on:
55
+
56
+ * `Adafruit CircuitPython <https://github.com/adafruit/circuitpython>`_ 11 or later, on a board
57
+ that can load native ``.mpy`` files (``microcontroller.cpu.architecture`` is not ``None``)
58
+
59
+ The decoding loops are a precompiled kernel, one file per architecture:
60
+
61
+ ====================== =============================================
62
+ ``flac_kernel`` file Boards
63
+ ====================== =============================================
64
+ ``armv6m`` RP2040
65
+ ``armv7emsp`` SAMD51 (M4), nRF52840, RP2350, STM32F4
66
+ ``xtensawin`` ESP32-S2, ESP32-S3
67
+ ``rv32imc`` ESP32-C3, C5, C6
68
+ ====================== =============================================
69
+
70
+ Without a kernel the decoder falls back to plain Python, which is correct but far slower than
71
+ real time for music. Only the RP2350 has been tested for playback.
72
+
73
+ Please ensure all dependencies are available on the CircuitPython filesystem.
74
+ This is easily achieved by downloading
75
+ `the Adafruit library and driver bundle <https://circuitpython.org/libraries>`_
76
+ or individual libraries can be installed using
77
+ `circup <https://github.com/adafruit/circup>`_.
78
+
79
+ Installing to a Connected CircuitPython Device with Circup
80
+ ==========================================================
81
+
82
+ Make sure that you have ``circup`` installed in your Python environment.
83
+ Install it with the following command if necessary:
84
+
85
+ .. code-block:: shell
86
+
87
+ pip3 install circup
88
+
89
+ With ``circup`` installed and your CircuitPython device connected use the
90
+ following command to install:
91
+
92
+ .. code-block:: shell
93
+
94
+ circup install adafruit_flac
95
+
96
+ Or the following command to update an existing version:
97
+
98
+ .. code-block:: shell
99
+
100
+ circup update
101
+
102
+ Usage Example
103
+ =============
104
+
105
+ .. code-block:: python
106
+
107
+ import adafruit_flac
108
+ from adafruit_fruitjam.peripherals import Peripherals
109
+
110
+ decoder = adafruit_flac.FLACDecoder("song.flac")
111
+ fruit_jam = Peripherals(audio_output="headphone", sample_rate=decoder.sample_rate)
112
+
113
+ player = adafruit_flac.FLACPlayer(fruit_jam.audio)
114
+ player.play(decoder)
115
+ while player.playing:
116
+ player.update() # decode the next piece; call this often while playing
117
+
118
+ Speed and memory
119
+ ================
120
+
121
+ Measured on an Adafruit Fruit Jam (RP2350B). Load is decode time divided by audio time; under
122
+ 100% keeps up.
123
+
124
+ ========================= =========== ===========
125
+ Format 150 MHz 300 MHz
126
+ ========================= =========== ===========
127
+ 16-bit, 44.1 kHz stereo 30% 15%
128
+ 24-bit, 96 kHz stereo 89% 46%
129
+ ========================= =========== ===========
130
+
131
+ - Keep the decoder and the audio buffer in internal SRAM. On the RP2350, decoding into PSRAM
132
+ nearly doubled the load at 96 kHz. Create them first in ``code.py``, before other large
133
+ allocations, or pass a preallocated ``array("h")`` as ``FLACPlayer(audio, buffer=...)``.
134
+ - ``FLACPlayer`` decodes straight into the half of its buffer that the audio DMA has already
135
+ copied, so it does not work with ``audiomixer``, which reads the buffer in place.
136
+ - At 24/96 the decoder uses about 83 KB and a 64 ms double buffer 48 KB (plus the output's own
137
+ DMA buffers).
138
+ - ``examples/flac_fruitjam_sd_hires.py`` plays 24/96 from the Fruit Jam's microSD card over
139
+ 4-bit SDIO.
140
+
141
+ Building the kernel
142
+ ===================
143
+
144
+ ``src/flac_turbo.py`` is the kernel source, turbo (``@micropython.viper``) functions.
145
+ ``tools/gen_turbo_lpc.py`` writes the unrolled predictors into it. ``src/Makefile`` compiles it
146
+ with ``mpy-cross -march`` for each architecture into ``adafruit_flac/flac_kernel.<arch>.mpy``:
147
+
148
+ .. code-block:: shell
149
+
150
+ cd src
151
+ make MPY_CROSS=path/to/circuitpython/mpy-cross/build/mpy-cross
152
+
153
+ Use an ``mpy-cross`` from the CircuitPython version the files will run on.
154
+
155
+ Testing
156
+ =======
157
+
158
+ ``tools/make_test_media.py`` builds test clips (it downloads a CC0 recording and needs ``flac``
159
+ and ``ffmpeg``). ``tests/test_decoder.py`` checks the plain Python decoder against libFLAC under
160
+ CPython, ``tests/test_kernel_model.py`` runs the same with a CPython model of the kernel that
161
+ fails on any 32-bit overflow, and ``tools/turbo_check.py`` compares the kernel with the plain
162
+ Python path on a board.
163
+
164
+ Documentation
165
+ =============
166
+ API documentation for this library can be found on `Read the Docs <https://docs.circuitpython.org/projects/flac/en/latest/>`_.
167
+
168
+ For information on building library documentation, please check out
169
+ `this guide <https://learn.adafruit.com/creating-and-sharing-a-circuitpython-library/sharing-our-docs-on-readthedocs#sphinx-5-1>`_.
170
+
171
+ Contributing
172
+ ============
173
+
174
+ Contributions are welcome! Please read our `Code of Conduct
175
+ <https://github.com/adafruit/Adafruit_CircuitPython_FLAC/blob/HEAD/CODE_OF_CONDUCT.md>`_
176
+ before contributing to help this project stay welcoming.
@@ -0,0 +1,14 @@
1
+ adafruit_circuitpython_flac-1.0.2.dist-info/licenses/LICENSE,sha256=bQxQlJZdQnI0BAF8SlSIVnkOuhemp1xqC5_RPHvHKIo,1102
2
+ adafruit_flac/__init__.py,sha256=rwd3Jw-iNsKxvUcyM1_yCgq39ftKaRzQtMbMFYvzbgs,36930
3
+ adafruit_flac/flac_kernel.armv6m.mpy,sha256=9xEO-h85uOh3uZtbYS5gjlBK0iVjq3rm7wEaJr0nbm0,27192
4
+ adafruit_flac/flac_kernel.armv6m.mpy.license,sha256=x0CNJztITfJuEPiEy7vRw-7YoXKz7ozBkRLjFbm995Q,114
5
+ adafruit_flac/flac_kernel.armv7emsp.mpy,sha256=w6smDCyCNmPOTKqzlSRt8YPg64LLv6CiZOECJr9FJmA,25052
6
+ adafruit_flac/flac_kernel.armv7emsp.mpy.license,sha256=x0CNJztITfJuEPiEy7vRw-7YoXKz7ozBkRLjFbm995Q,114
7
+ adafruit_flac/flac_kernel.rv32imc.mpy,sha256=Ce8mC4kediLlOZgjNevbHphsZCR12Bjrlznl3MvFTBw,28893
8
+ adafruit_flac/flac_kernel.rv32imc.mpy.license,sha256=x0CNJztITfJuEPiEy7vRw-7YoXKz7ozBkRLjFbm995Q,114
9
+ adafruit_flac/flac_kernel.xtensawin.mpy,sha256=2Q0aupipVwsXfJ99-JEGNMtmo--BxRza1zaaR5u-B7A,31616
10
+ adafruit_flac/flac_kernel.xtensawin.mpy.license,sha256=x0CNJztITfJuEPiEy7vRw-7YoXKz7ozBkRLjFbm995Q,114
11
+ adafruit_circuitpython_flac-1.0.2.dist-info/METADATA,sha256=3VO5GC7e1D2uVmFFgtb81nj7evVWz-FpkIfSzT95mHc,6893
12
+ adafruit_circuitpython_flac-1.0.2.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
13
+ adafruit_circuitpython_flac-1.0.2.dist-info/top_level.txt,sha256=jCHJ5quA3TzgTSSG7zbfNmG0-agOLMH_GxhHc1_yDRY,14
14
+ adafruit_circuitpython_flac-1.0.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Mikey Sklar for Adafruit Industries
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 @@
1
+ adafruit_flac
@@ -0,0 +1,1069 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 Mikey Sklar for Adafruit Industries
2
+ # SPDX-License-Identifier: MIT
3
+ """
4
+ `adafruit_flac`
5
+ ================================================================================
6
+
7
+ FLAC decoding and playback for CircuitPython.
8
+
9
+ `FLACDecoder` reads a FLAC file and returns 16-bit signed PCM. `FLACPlayer` streams it to an
10
+ audio output (``I2SOut``, ``PWMAudioOut`` or ``AudioOut``) through a double-buffered
11
+ ``audiocore.RawSample``; call `FLACPlayer.update` often from your main loop.
12
+
13
+ The hot loops (Rice decoding, the predictors, stereo decorrelation and PCM conversion) run in
14
+ a turbo kernel, ``flac_kernel.<arch>.mpy``, prebuilt from ``src/flac_turbo.py`` with
15
+ ``mpy-cross -march``. Without a kernel for the board, plain Python loops run instead
16
+ (correct, but several times slower than real time for music).
17
+
18
+ Written from RFC 9639, "Free Lossless Audio Codec (FLAC)" (section numbers in comments refer
19
+ to it). No third-party decoder code is included. Decoding is bit-exact. 20, 24 and 32-bit
20
+ sources are reduced to their top 16 bits.
21
+
22
+ * Author(s): Mikey Sklar
23
+
24
+ Implementation Notes
25
+ --------------------
26
+
27
+ **Hardware:**
28
+
29
+ * Tested on the Adafruit Fruit Jam (RP2350B): 16-bit/44.1 kHz and 24-bit/96 kHz at the
30
+ stock 150 MHz.
31
+
32
+ **Software and Dependencies:**
33
+
34
+ * Adafruit CircuitPython firmware 11 or later, with native ``.mpy`` loading:
35
+ https://circuitpython.org/downloads
36
+ """
37
+
38
+ import struct
39
+ from array import array
40
+
41
+ try:
42
+ from time import monotonic_ns as _now
43
+ except ImportError: # unix port
44
+ from time import time_ns as _now
45
+
46
+ try:
47
+ from ulab import numpy as _np
48
+ except ImportError:
49
+ try:
50
+ import numpy as _np # desktop testing with CPython
51
+ except ImportError:
52
+ _np = None
53
+
54
+ # Turbo (viper) kernel, prebuilt per chip as flac_kernel.<arch>.mpy from src/flac_turbo.py.
55
+ # Without it (CPython, or a chip with no build) the plain Python loops run.
56
+ try:
57
+ from adafruit_flac import flac_kernel as _kernel
58
+ except (ImportError, ValueError):
59
+ _kernel = None
60
+
61
+ # Largest sum the 32-bit turbo predictor can hold.
62
+ _INT32 = (1 << 31) - 1
63
+
64
+ __version__ = "1.0.2"
65
+ __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_FLAC.git"
66
+
67
+ # Largest value a small int holds on a 32-bit MicroPython build. Anything
68
+ # bigger becomes a heap-allocated long, which is several times slower.
69
+ _SMALL = (1 << 30) - 1
70
+
71
+
72
+ # Leading zero bits in a byte; 8 for zero.
73
+ def _lz_table():
74
+ t = bytearray(256)
75
+ t[0] = 8
76
+ for i in range(1, 256):
77
+ n = 0
78
+ while not (i << n) & 0x80:
79
+ n += 1
80
+ t[i] = n
81
+ return bytes(t)
82
+
83
+
84
+ _LZ = _lz_table()
85
+
86
+ # Section 9.1.1 / 9.1.2 lookup tables.
87
+ _BLOCK_SIZES = (
88
+ 0,
89
+ 192,
90
+ 576,
91
+ 1152,
92
+ 2304,
93
+ 4608,
94
+ -1,
95
+ -2,
96
+ 256,
97
+ 512,
98
+ 1024,
99
+ 2048,
100
+ 4096,
101
+ 8192,
102
+ 16384,
103
+ 32768,
104
+ )
105
+ _RATES = (
106
+ 0,
107
+ 88200,
108
+ 176400,
109
+ 192000,
110
+ 8000,
111
+ 16000,
112
+ 22050,
113
+ 24000,
114
+ 32000,
115
+ 44100,
116
+ 48000,
117
+ 96000,
118
+ -1,
119
+ -2,
120
+ -3,
121
+ 0,
122
+ )
123
+ _DEPTHS = (0, 8, 12, 0, 16, 20, 24, 32)
124
+
125
+ # Fixed predictor coefficients, section 9.2.5.
126
+ _FIXED = ((), (1,), (2, -1), (3, -3, 1), (4, -6, 4, -1))
127
+
128
+
129
+ class FLACError(ValueError):
130
+ """Raised for files that are not FLAC or use unsupported features."""
131
+
132
+
133
+ class _Bits:
134
+ """MSB-first bit reader over a bytearray. Used for headers; the hot
135
+ residual loop reads the same buffer with its own inlined code."""
136
+
137
+ def __init__(self, data, pos=0):
138
+ self.data = data
139
+ self.pos = pos
140
+
141
+ def read(self, n):
142
+ if n == 0:
143
+ return 0
144
+ pos = self.pos
145
+ d = self.data
146
+ i = pos >> 3
147
+ need = (pos & 7) + n
148
+ v = 0
149
+ for j in range((need + 7) >> 3):
150
+ v = (v << 8) | d[i + j]
151
+ self.pos = pos + n
152
+ return (v >> (((need + 7) & ~7) - need)) & ((1 << n) - 1)
153
+
154
+ def signed(self, n):
155
+ v = self.read(n)
156
+ if v >> (n - 1):
157
+ v -= 1 << n
158
+ return v
159
+
160
+ def unary(self):
161
+ # count zero bits before the next 1
162
+ d = self.data
163
+ pos = self.pos
164
+ q = 0
165
+ while not (d[pos >> 3] << (pos & 7)) & 0x80:
166
+ q += 1
167
+ pos += 1
168
+ self.pos = pos + 1
169
+ return q
170
+
171
+ def align(self):
172
+ self.pos = (self.pos + 7) & ~7
173
+
174
+
175
+ def _rice(d, pos, out, o, end, k):
176
+ """Decode Rice codes with parameter ``k`` into ``out[o:end]`` from bit
177
+ ``pos`` of ``d`` (section 9.2.7.2). Returns the new bit position.
178
+
179
+ This is the hottest loop. ``for ... in range`` is MicroPython's fastest
180
+ loop, and the common cases get their own copy so the loop body has no
181
+ extra branches."""
182
+ lz = _LZ
183
+ if k == 0:
184
+ for o in range(o, end): # noqa: PLR1704, reuses the argument for speed
185
+ i = pos >> 3
186
+ b = (d[i] << (pos & 7)) & 0xFF
187
+ if b:
188
+ q = lz[b]
189
+ else:
190
+ q = 8 - (pos & 7)
191
+ i += 1
192
+ while not d[i]:
193
+ q += 8
194
+ i += 1
195
+ q += lz[d[i]]
196
+ pos += q + 1
197
+ out[o] = (q >> 1) ^ -(q & 1)
198
+ return pos
199
+ mask = (1 << k) - 1
200
+ sh = 24 - k
201
+ if k <= 17:
202
+ # the k remainder bits always fit in one 3-byte window
203
+ for o in range(o, end): # noqa: PLR1704, reuses the argument for speed
204
+ i = pos >> 3
205
+ b = (d[i] << (pos & 7)) & 0xFF
206
+ if b:
207
+ q = lz[b]
208
+ else:
209
+ q = 8 - (pos & 7)
210
+ i += 1
211
+ while not d[i]:
212
+ q += 8
213
+ i += 1
214
+ q += lz[d[i]]
215
+ pos += q + 1
216
+ i = pos >> 3
217
+ v = (q << k) | (((d[i] << 16 | d[i + 1] << 8 | d[i + 2]) >> (sh - (pos & 7))) & mask)
218
+ pos += k
219
+ out[o] = (v >> 1) ^ -(v & 1)
220
+ return pos
221
+ for o in range(o, end):
222
+ i = pos >> 3
223
+ b = (d[i] << (pos & 7)) & 0xFF
224
+ if b:
225
+ q = lz[b]
226
+ else:
227
+ q = 8 - (pos & 7)
228
+ i += 1
229
+ while not d[i]:
230
+ q += 8
231
+ i += 1
232
+ q += lz[d[i]]
233
+ pos += q + 1
234
+ i = pos >> 3
235
+ s = pos & 7
236
+ if s <= sh:
237
+ v = ((d[i] << 16 | d[i + 1] << 8 | d[i + 2]) >> (sh - s)) & mask
238
+ else:
239
+ # past 30 bits this is a long int on a 32-bit build; rare
240
+ v = ((d[i] << 24 | d[i + 1] << 16 | d[i + 2] << 8 | d[i + 3]) >> (32 - s - k)) & mask
241
+ pos += k
242
+ v |= q << k
243
+ out[o] = (v >> 1) ^ -(v & 1)
244
+ return pos
245
+
246
+
247
+ _LPC_CACHE = {}
248
+
249
+ # Predictor variants, picked per subframe by `FLACDecoder._predict`.
250
+ _PLAIN = 0 # one dot product
251
+ _CSPLIT = 1 # coefficients split into 8-bit halves: two dot products
252
+ _XSPLIT = 2 # samples split into two limbs: two dot products
253
+ _TURBO = 3 # flac_kernel.lpc_restore, when the sum fits in 32 bits
254
+ _TURBO_SPLIT = 4 # flac_kernel.lpc_restore_split, sample limbs for 24-bit
255
+
256
+
257
+ # Orders up to this use the ring-unrolled loop; longer ones rotate locals.
258
+ _RING_MAX = 16
259
+
260
+
261
+ def _lpc_func(order, mode):
262
+ """Build an unrolled predictor for one order (section 9.2.6).
263
+
264
+ ``out`` holds warm-up samples followed by residuals, and is restored in
265
+ place. The previous ``order`` samples live in local variables, which is
266
+ much faster than indexing ``out`` on MicroPython. For orders up to
267
+ `_RING_MAX` the loop body is unrolled ``order`` times and each step
268
+ overwrites the variable holding the oldest sample, so no variables are
269
+ shuffled between samples.
270
+
271
+ On a 32-bit build a product or sum past 30 bits becomes a heap-allocated
272
+ long int. The split modes keep every value small and stay exact:
273
+
274
+ * ``_CSPLIT``: ``c = a * 256 + b`` with ``0 <= b < 256``. For a shift
275
+ ``s >= 8``, ``(A * 256 + B) >> s == (A + (B >> 8)) >> (s - 8)``.
276
+ * ``_XSPLIT``: ``x = h * 2 ** b + l`` with ``0 <= l < 2 ** b``. The
277
+ prediction ``(H * 2 ** b + L) >> s`` equals
278
+ ``(H + (L >> b)) >> (s - b)`` when ``s >= b`` and
279
+ ``(H << (b - s)) + (L >> s)`` when ``s < b``. Both are
280
+ ``((H << u) + (L >> v)) >> w``.
281
+ """
282
+ key = (order, mode)
283
+ f = _LPC_CACHE.get(key)
284
+ if f is not None:
285
+ return f
286
+ n = order
287
+ r = range(n)
288
+ tup = "," if n == 1 else ""
289
+ lines = ["def f(out, end, coefs, shift, b):"]
290
+ if mode == _CSPLIT:
291
+ names_a = ", ".join("a%d" % j for j in r)
292
+ names_b = ", ".join("b%d" % j for j in r)
293
+ lines.append(" %s%s = [c >> 8 for c in coefs]" % (names_a, tup))
294
+ lines.append(" %s%s = [c & 255 for c in coefs]" % (names_b, tup))
295
+ else:
296
+ lines.append(" %s%s = coefs" % (", ".join("c%d" % j for j in r), tup))
297
+ if mode == _CSPLIT:
298
+ lines.append(" s8 = shift - 8")
299
+ elif mode == _XSPLIT:
300
+ lines.append(" u = b - shift if b > shift else 0")
301
+ lines.append(" v = shift if shift < b else b")
302
+ lines.append(" w = shift - b if shift > b else 0")
303
+ lines.append(" lm = (1 << b) - 1")
304
+
305
+ def predict(var):
306
+ # var(j) names the sample j + 1 back
307
+ if mode == _PLAIN:
308
+ p = " + ".join("c%d * %s" % (j, var("x", j)) for j in r)
309
+ return "((%s) >> shift)" % p
310
+ if mode == _CSPLIT:
311
+ a = " + ".join("a%d * %s" % (j, var("x", j)) for j in r)
312
+ b = " + ".join("b%d * %s" % (j, var("x", j)) for j in r)
313
+ return "(((%s) + ((%s) >> 8)) >> s8)" % (a, b)
314
+ h = " + ".join("c%d * %s" % (j, var("h", j)) for j in r)
315
+ l = " + ".join("c%d * %s" % (j, var("l", j)) for j in r)
316
+ return "((((%s) << u) + ((%s) >> v)) >> w)" % (h, l)
317
+
318
+ def store(ind, name):
319
+ # x is the new sample; keep it (or its limbs) in variable slot `name`
320
+ if mode == _XSPLIT:
321
+ return [ind + "h%s = x >> b" % name, ind + "l%s = x & lm" % name]
322
+ return [ind + "x%s = x" % name]
323
+
324
+ if n <= _RING_MAX:
325
+ # slot t holds the sample whose index is t mod n
326
+ for t in r:
327
+ lines.append(" x = out[%d]" % t)
328
+ lines.extend(store(" ", t))
329
+ lines.append(" stop = end - (end - %d) %% %d" % (n, n))
330
+ lines.append(" for i in range(%d, stop, %d):" % (n, n))
331
+ for t in r:
332
+ idx = "i + %d" % t if t else "i"
333
+ lines.append(
334
+ " x = out[%s] + %s"
335
+ % (idx, predict(lambda v, j, t=t: "%s%d" % (v, (t - 1 - j) % n)))
336
+ )
337
+ lines.append(" out[%s] = x" % idx)
338
+ lines.extend(store(" ", t))
339
+ # the last few samples, exactly and slowly
340
+ lines.append(" for i in range(stop, end):")
341
+ lines.append(" s = 0")
342
+ lines.append(" for j in range(%d):" % n)
343
+ lines.append(" s += coefs[j] * out[i - 1 - j]")
344
+ lines.append(" out[i] += s >> shift")
345
+ else:
346
+ # slot j + 1 holds the sample j + 1 back; shuffle after each sample
347
+ for j in r:
348
+ lines.append(" x = out[%d]" % (n - 1 - j))
349
+ lines.extend(store(" ", j + 1))
350
+ lines.append(" for i in range(%d, end):" % n)
351
+ lines.append(" x = out[i] + %s" % predict(lambda v, j: "%s%d" % (v, j + 1)))
352
+ lines.append(" out[i] = x")
353
+ names = ("h", "l") if mode == _XSPLIT else ("x",)
354
+ for j in range(n, 1, -1):
355
+ for v in names:
356
+ lines.append(" %s%d = %s%d" % (v, j, v, j - 1))
357
+ lines.extend(store(" ", 1))
358
+ ns = {}
359
+ exec("\n".join(lines), ns)
360
+ f = ns["f"]
361
+ _LPC_CACHE[key] = f
362
+ return f
363
+
364
+
365
+ class FLACDecoder:
366
+ """Decode a FLAC file to 16-bit signed PCM.
367
+
368
+ :param file: path string or a file opened in ``"rb"`` mode
369
+ :param bool accel: use ``ulab`` for interleaving when it applies
370
+ :param bool turbo: use the prebuilt ``flac_kernel`` for the hot loops when
371
+ this chip has one
372
+
373
+ Playing a FLAC file::
374
+
375
+ import audiobusio
376
+ import board
377
+ import adafruit_flac
378
+
379
+ audio = audiobusio.I2SOut(board.I2S_BCLK, board.I2S_WS, board.I2S_DIN)
380
+ decoder = adafruit_flac.FLACDecoder("song.flac")
381
+ player = adafruit_flac.FLACPlayer(audio)
382
+ player.play(decoder)
383
+ while player.update():
384
+ pass
385
+ """
386
+
387
+ def __init__(self, file, *, accel=True, turbo=True):
388
+ self._owns = False
389
+ self._file = None
390
+ self._accel = bool(accel and _np is not None)
391
+ self._kernel = _kernel if turbo else None
392
+ self._buf = self._chan = self._pcm = None
393
+ self._coefs = array("i", [0]) * 32 # scratch for flac_kernel.subframe
394
+ self.lpc_modes = array("i", [0]) * 5
395
+ """Subframes predicted with the plain, coefficient-split,
396
+ sample-split, turbo and turbo-split loops (for benchmarking)."""
397
+ self._open(file)
398
+
399
+ # -- public API --
400
+
401
+ @property
402
+ def file(self):
403
+ """The open file object. Assign a new file or path to switch songs."""
404
+ return self._file
405
+
406
+ @file.setter
407
+ def file(self, file):
408
+ self._open(file)
409
+
410
+ def open(self, file):
411
+ """Switch to a different file (same as assigning `file`)."""
412
+ self._open(file)
413
+
414
+ @property
415
+ def sample_rate(self):
416
+ """Sample rate in Hz."""
417
+ return self._rate
418
+
419
+ @property
420
+ def channel_count(self):
421
+ """Number of channels (output is limited to 2)."""
422
+ return self._channels
423
+
424
+ @property
425
+ def bits_per_sample(self):
426
+ """Always 16."""
427
+ return 16
428
+
429
+ @property
430
+ def source_bits_per_sample(self):
431
+ """Bit depth stored in the file (8 to 32)."""
432
+ return self._bps
433
+
434
+ @property
435
+ def samples(self):
436
+ """Total frames in the stream (0 if the encoder did not say)."""
437
+ return self._total
438
+
439
+ @property
440
+ def duration(self):
441
+ """Length in seconds (0 if unknown)."""
442
+ return self._total / self._rate if self._rate else 0
443
+
444
+ @property
445
+ def samples_decoded(self):
446
+ """Frames decoded since the file was opened or rewound."""
447
+ return self._decoded
448
+
449
+ def rewind(self):
450
+ """Go back to the first audio frame."""
451
+ self._file.seek(self._audio_start)
452
+ self._len = 0
453
+ self._off = 0
454
+ self._eof = False
455
+ self._decoded = 0
456
+ self._pcm_len = 0
457
+ self._pcm_pos = 0
458
+
459
+ def readinto(self, buf):
460
+ """Fill ``buf`` (an ``array('h')`` or memoryview of one) with
461
+ interleaved samples. Returns the number of values written; less than
462
+ ``len(buf)`` only at the end of the stream."""
463
+ mv = memoryview(buf)
464
+ total = len(mv)
465
+ got = 0
466
+ while got < total:
467
+ n = self.fill(mv[got:])
468
+ if n == 0:
469
+ break
470
+ got += n
471
+ return got
472
+
473
+ def fill(self, mv):
474
+ """Copy decoded samples into memoryview ``mv``, decoding at most one
475
+ frame. Returns the number of values written, 0 at end of stream."""
476
+ if self._pcm_pos >= self._pcm_len:
477
+ if not self._decode_pcm():
478
+ return 0
479
+ p = self._pcm_pos
480
+ n = self._pcm_len - p
481
+ n = min(n, len(mv))
482
+ mv[:n] = self._pcm_mv[p : p + n]
483
+ self._pcm_pos = p + n
484
+ return n
485
+
486
+ def decode_frame(self):
487
+ """Decode the next frame and return its channels as a list of
488
+ ``array('i')`` at the source bit depth (each may be longer than the
489
+ block; use `block_size`). Returns None at the end of the stream."""
490
+ if not self._decode():
491
+ return None
492
+ return self._chan
493
+
494
+ @property
495
+ def block_size(self):
496
+ """Frames in the block most recently returned by `decode_frame`."""
497
+ return self._bs
498
+
499
+ def deinit(self):
500
+ """Release the file (if this decoder opened it) and buffers."""
501
+ if self._owns and self._file is not None:
502
+ self._file.close()
503
+ self._file = None
504
+ self._buf = None
505
+ self._chan = None
506
+ self._pcm = None
507
+ self._pcm_mv = None
508
+
509
+ def __enter__(self):
510
+ return self
511
+
512
+ def __exit__(self, *exc):
513
+ self.deinit()
514
+
515
+ # -- internals --
516
+
517
+ def _open(self, file):
518
+ if self._owns and self._file is not None:
519
+ self._file.close()
520
+ if isinstance(file, str):
521
+ file = open(file, "rb")
522
+ self._owns = True
523
+ else:
524
+ self._owns = False
525
+ self._file = file
526
+ self._read_metadata()
527
+ maxblock = self._maxblock or 4096
528
+ # Worst case frame is a verbatim block plus headers (section 10.1).
529
+ maxframe = self._maxframe or (maxblock * self._channels_coded * (self._bps + 1) // 8 + 64)
530
+ self._chunk = max(maxframe, 4096)
531
+ # Keep the buffers from the previous file when they are big enough: where they
532
+ # landed (internal SRAM or PSRAM) matters several times over for decode speed,
533
+ # and the first allocation is the one most likely to get SRAM.
534
+ # Hottest first: the channel arrays are read and written many times per sample,
535
+ # the PCM frame twice, the input buffer once. If internal RAM runs short, the
536
+ # input buffer is the one that should spill.
537
+ chan = self._chan or []
538
+ if len(chan) < self._channels_coded or len(chan[0]) < maxblock:
539
+ chan = [array("i", [0]) * maxblock for _ in range(self._channels_coded)]
540
+ self._chan = chan
541
+ if self._pcm is None or len(self._pcm) < maxblock * self._channels:
542
+ self._pcm = array("h", [0]) * (maxblock * self._channels)
543
+ self._pcm_mv = memoryview(self._pcm)
544
+ if self._buf is None or len(self._buf) < 2 * self._chunk + 8:
545
+ self._buf = bytearray(2 * self._chunk + 8) # +8: guard bytes for the bit reader
546
+ self._audio_start = file.tell()
547
+ self.rewind()
548
+
549
+ def _read_metadata(self):
550
+ # Section 8: "fLaC" then metadata blocks, the first being STREAMINFO.
551
+ f = self._file
552
+ head = f.read(4)
553
+ if head == b"ID3\x03" or head[:3] == b"ID3":
554
+ # skip an ID3v2 tag some taggers prepend
555
+ rest = f.read(6)
556
+ size = (rest[2] << 21) | (rest[3] << 14) | (rest[4] << 7) | rest[5]
557
+ f.seek(10 + size)
558
+ head = f.read(4)
559
+ if head != b"fLaC":
560
+ raise FLACError("not a FLAC file")
561
+ self._rate = 0
562
+ last = False
563
+ while not last:
564
+ h = f.read(4)
565
+ if len(h) < 4:
566
+ raise FLACError("truncated metadata")
567
+ last = h[0] & 0x80
568
+ kind = h[0] & 0x7F
569
+ size = (h[1] << 16) | (h[2] << 8) | h[3]
570
+ if kind == 0: # STREAMINFO, section 8.2
571
+ si = f.read(size)
572
+ self._minblock, self._maxblock = struct.unpack(">HH", si[0:4])
573
+ self._maxframe = (si[7] << 16) | (si[8] << 8) | si[9]
574
+ b = _Bits(si, 80)
575
+ self._rate = b.read(20)
576
+ self._channels_coded = b.read(3) + 1
577
+ self._bps = b.read(5) + 1
578
+ hi = b.read(4)
579
+ self._total = (hi << 32) | struct.unpack(">I", si[14:18])[0]
580
+ else:
581
+ f.seek(size, 1)
582
+ if not self._rate:
583
+ raise FLACError("no STREAMINFO")
584
+ if self._channels_coded > 2:
585
+ # only the first two channels are played
586
+ self._channels = 2
587
+ else:
588
+ self._channels = self._channels_coded
589
+
590
+ def _refill(self):
591
+ # Keep at least one maximum frame of data after self._off.
592
+ buf = self._buf
593
+ rem = self._len - self._off
594
+ if rem >= self._chunk or self._eof:
595
+ return
596
+ buf[:rem] = buf[self._off : self._len]
597
+ mv = memoryview(buf)
598
+ want = 2 * self._chunk - rem
599
+ got = self._file.readinto(mv[rem : rem + want]) or 0
600
+ if got < want:
601
+ self._eof = True
602
+ self._len = rem + got
603
+ self._off = 0
604
+ # zero guard bytes so the bit reader never sees stale data
605
+ for i in range(self._len, self._len + 8):
606
+ buf[i] = 0
607
+
608
+ def _decode(self):
609
+ self._refill()
610
+ buf = self._buf
611
+ # Section 9.1: find the frame sync code.
612
+ off = self._off
613
+ end = self._len - 1
614
+ while off < end and not (buf[off] == 0xFF and (buf[off + 1] & 0xFE) == 0xF8):
615
+ off += 1
616
+ if off >= end or self._len - off < 6:
617
+ return False
618
+ b = _Bits(buf, off * 8 + 16)
619
+ bs_code = b.read(4)
620
+ rate_code = b.read(4)
621
+ chan_code = b.read(4)
622
+ depth_code = b.read(3)
623
+ b.read(1)
624
+ # coded frame/sample number, UTF-8 style (section 9.1.5)
625
+ first = b.read(8)
626
+ extra = 0
627
+ while first & (0x80 >> extra) and extra < 7:
628
+ extra += 1
629
+ if extra:
630
+ b.read(8 * (extra - 1))
631
+ bs = _BLOCK_SIZES[bs_code]
632
+ if bs == -1:
633
+ bs = b.read(8) + 1
634
+ elif bs == -2:
635
+ bs = b.read(16) + 1
636
+ elif bs == 0:
637
+ raise FLACError("reserved block size")
638
+ rate = _RATES[rate_code]
639
+ if rate == -1:
640
+ b.read(8)
641
+ elif rate < -1:
642
+ b.read(16)
643
+ b.read(8) # CRC-8, not checked
644
+ bps = _DEPTHS[depth_code] or self._bps
645
+ if bs > len(self._chan[0]):
646
+ self._chan = [array("i", [0]) * bs for _ in self._chan]
647
+ self._pcm = array("h", [0]) * (bs * self._channels)
648
+ self._pcm_mv = memoryview(self._pcm)
649
+ chan = self._chan
650
+ if chan_code < 8:
651
+ nch = chan_code + 1
652
+ for c in range(nch):
653
+ self._subframe(b, chan[c], bs, bps)
654
+ elif chan_code <= 10:
655
+ # section 4.2: the side channel carries one extra bit
656
+ self._subframe(b, chan[0], bs, bps + (chan_code == 9))
657
+ self._subframe(b, chan[1], bs, bps + (chan_code != 9))
658
+ self._decorrelate(chan_code, bs, bps)
659
+ else:
660
+ raise FLACError("reserved channel assignment")
661
+ b.align()
662
+ if (b.pos >> 3) + 2 > self._len:
663
+ return False # truncated last frame
664
+ self._off = (b.pos >> 3) + 2 # skip CRC-16
665
+ self._bs = bs
666
+ self._frame_bps = bps
667
+ self._decoded += bs
668
+ return True
669
+
670
+ def _subframe(self, b, out, bs, bps):
671
+ # Section 9.2.
672
+ kernel = self._kernel
673
+ if kernel is not None:
674
+ pos = kernel.subframe(b.data, _LZ, out, b.pos, bs, bps, self._coefs, self.lpc_modes)
675
+ if pos >= 0:
676
+ b.pos = pos
677
+ return
678
+ if pos == -1:
679
+ raise FLACError("bad subframe")
680
+ # -2: past what 32-bit viper math holds; decode this one in Python
681
+ h = b.read(8)
682
+ kind = (h >> 1) & 0x3F
683
+ wasted = 0
684
+ if h & 1:
685
+ wasted = b.unary() + 1
686
+ bps -= wasted
687
+ if kind == 0: # constant
688
+ v = b.signed(bps)
689
+ for i in range(bs):
690
+ out[i] = v
691
+ elif kind == 1: # verbatim
692
+ for i in range(bs):
693
+ out[i] = b.signed(bps)
694
+ elif 8 <= kind <= 12: # fixed
695
+ order = kind - 8
696
+ for i in range(order):
697
+ out[i] = b.signed(bps)
698
+ self._residual(b, out, bs, order)
699
+ if order:
700
+ self._predict(out, bs, _FIXED[order], 0, bps)
701
+ elif kind >= 32: # LPC
702
+ order = kind - 31
703
+ for i in range(order):
704
+ out[i] = b.signed(bps)
705
+ prec = b.read(4) + 1
706
+ if prec == 16:
707
+ raise FLACError("bad LPC precision")
708
+ shift = b.signed(5)
709
+ if shift < 0:
710
+ raise FLACError("negative LPC shift")
711
+ coefs = tuple(b.signed(prec) for _ in range(order))
712
+ self._residual(b, out, bs, order)
713
+ self._predict(out, bs, coefs, shift, bps)
714
+ else:
715
+ raise FLACError("reserved subframe type")
716
+ if wasted:
717
+ for i in range(bs):
718
+ out[i] <<= wasted
719
+
720
+ def _predict(self, out, bs, coefs, shift, bps):
721
+ # Pick the cheapest predictor whose products and sums provably stay
722
+ # in small ints: |x| < 2 ** (bps - 1), so |sum(c * x)| is at most
723
+ # sum(|c|) << (bps - 1).
724
+ order = len(coefs)
725
+ mode = _PLAIN
726
+ b = bps >> 1 # limb split point for _XSPLIT
727
+ ca = 0
728
+ for c in coefs:
729
+ ca += c if c >= 0 else -c
730
+ k = self._kernel
731
+ if k is not None:
732
+ if ca << (bps - 1) <= _INT32:
733
+ self.lpc_modes[_TURBO] += 1
734
+ k.lpc_restore(out, array("i", coefs), order, shift, bs)
735
+ return
736
+ # 24-bit: split samples into two limbs so each dot product fits 32 bits
737
+ if ca << max(b, bps - 1 - b) <= _INT32 and (
738
+ shift >= b or ca << (bps - 1 - shift) <= _INT32
739
+ ):
740
+ self.lpc_modes[_TURBO_SPLIT] += 1
741
+ k.lpc_restore_split(out, array("i", coefs), order, shift, bs, b)
742
+ return
743
+ if ca << (bps - 1) > _SMALL:
744
+ hi = 0
745
+ for c in coefs:
746
+ ch = c >> 8
747
+ hi += ch if ch >= 0 else -ch
748
+ if shift >= 8 and (hi << (bps - 1)) <= _SMALL and (255 * order) << (bps - 1) <= _SMALL:
749
+ mode = _CSPLIT
750
+ elif ca << max(b, bps - 1 - b) <= _SMALL:
751
+ mode = _XSPLIT
752
+ # else: nothing keeps it small; plain with long ints is still exact
753
+ self.lpc_modes[mode] += 1
754
+ _lpc_func(order, mode)(out, bs, coefs, shift, b)
755
+
756
+ def _residual(self, b, out, bs, order):
757
+ # Section 9.2.7.
758
+ kernel = self._kernel
759
+ if kernel is not None:
760
+ pos = kernel.residual(b.data, _LZ, out, b.pos, bs, order)
761
+ if pos < 0:
762
+ raise FLACError("reserved residual coding method")
763
+ b.pos = pos
764
+ return
765
+ self._residual_py(b, out, bs, order)
766
+
767
+ def _residual_py(self, b, out, bs, order):
768
+ method = b.read(2)
769
+ if method > 1:
770
+ raise FLACError("reserved residual coding method")
771
+ pbits = 4 + method
772
+ escape = (1 << pbits) - 1
773
+ porder = b.read(4)
774
+ psize = bs >> porder
775
+ o = order
776
+ d = b.data
777
+ kernel = self._kernel
778
+ for p in range(1 << porder):
779
+ end = psize * (p + 1)
780
+ k = b.read(pbits)
781
+ if k == escape:
782
+ n = b.read(5)
783
+ if n:
784
+ for i in range(o, end):
785
+ out[i] = b.signed(n)
786
+ else:
787
+ for i in range(o, end):
788
+ out[i] = 0
789
+ elif kernel is not None:
790
+ b.pos = kernel.rice_decode(d, _LZ, out, b.pos, o, end, k)
791
+ else:
792
+ b.pos = _rice(d, b.pos, out, o, end, k)
793
+ o = end
794
+
795
+ def _decorrelate(self, code, bs, bps):
796
+ a, s = self._chan[0], self._chan[1]
797
+ # mid/side holds up to bps + 1 bits; << 1 stays in 32 bits up to 24-bit
798
+ if self._kernel is not None and bps <= 24:
799
+ self._kernel.decorrelate(a, s, bs, code)
800
+ return
801
+ if code == 8: # left/side: right = left - side
802
+ for i in range(bs):
803
+ s[i] = a[i] - s[i]
804
+ elif code == 9: # side/right: left = side + right
805
+ for i in range(bs):
806
+ a[i] += s[i]
807
+ else: # mid/side
808
+ for i in range(bs):
809
+ side = s[i]
810
+ mid = (a[i] << 1) | (side & 1)
811
+ a[i] = (mid + side) >> 1
812
+ s[i] = (mid - side) >> 1
813
+
814
+ def _decode_pcm(self):
815
+ if not self._decode():
816
+ return False
817
+ bs = self._bs
818
+ bps = self._frame_bps
819
+ nch = self._channels
820
+ pcm = self._pcm
821
+ chan = self._chan
822
+ kernel = self._kernel
823
+ if kernel is not None:
824
+ for c in range(nch):
825
+ kernel.to_pcm16(pcm, chan[c], bs, nch, c, bps - 16)
826
+ elif self._accel and bps in (16, 24, 32):
827
+ # Bytes 0-1 (16-bit), 1-2 (24-bit) or 2-3 (32-bit) of each
828
+ # little-endian int32 are exactly the 16-bit output. Let ulab copy
829
+ # them byte by byte into place; it has no int32 type.
830
+ off = (bps - 16) >> 3
831
+ step = 2 * nch
832
+ dst = _np.frombuffer(self._pcm_mv[: bs * nch], dtype=_np.uint8)
833
+ for c in range(nch):
834
+ src = _np.frombuffer(chan[c], dtype=_np.uint8, count=4 * bs)
835
+ dst[2 * c :: step] = src[off::4]
836
+ dst[2 * c + 1 :: step] = src[off + 1 :: 4]
837
+ elif nch == 2:
838
+ l, r = chan[0], chan[1]
839
+ if bps > 16:
840
+ sh = bps - 16
841
+ j = 0
842
+ for i in range(bs):
843
+ pcm[j] = l[i] >> sh
844
+ pcm[j + 1] = r[i] >> sh
845
+ j += 2
846
+ else:
847
+ sh = 16 - bps
848
+ j = 0
849
+ for i in range(bs):
850
+ pcm[j] = l[i] << sh
851
+ pcm[j + 1] = r[i] << sh
852
+ j += 2
853
+ else:
854
+ m = chan[0]
855
+ if bps > 16:
856
+ sh = bps - 16
857
+ for i in range(bs):
858
+ pcm[i] = m[i] >> sh
859
+ else:
860
+ sh = 16 - bps
861
+ for i in range(bs):
862
+ pcm[i] = m[i] << sh
863
+ self._pcm_len = bs * nch
864
+ self._pcm_pos = 0
865
+ return True
866
+
867
+
868
+ def _gain(pcm, n, q):
869
+ # Scale n samples of pcm in place by q / 32768.
870
+ if _kernel is not None:
871
+ _kernel.gain(pcm, n, q)
872
+ return
873
+ for i in range(n):
874
+ pcm[i] = (pcm[i] * q) >> 15
875
+
876
+
877
+ class FLACPlayer:
878
+ """Stream a `FLACDecoder` to an audio output from Python.
879
+
880
+ Decoded PCM goes straight into a double-buffered `audiocore.RawSample` that
881
+ loops on the output, so playback keeps going while your code runs. Call
882
+ `update` often (several times per ``buffer_ms``); each call does at most one
883
+ FLAC frame of decoding.
884
+
885
+ The output must copy each half when it starts playing it, as ``I2SOut``,
886
+ ``PWMAudioOut`` and ``AudioOut`` do (into their DMA buffers): a half is
887
+ rewritten as soon as the output has taken it. Not for ``audiomixer``, which
888
+ reads the sample in place.
889
+
890
+ Keep the buffer in internal RAM where there is a choice: on an RP2350 with
891
+ PSRAM, writing decoded audio to PSRAM nearly doubled the decode time at
892
+ 96 kHz. Pass ``buffer`` to allocate it early, before other code takes the
893
+ internal RAM.
894
+
895
+ :param audio: an ``audiobusio.I2SOut``, ``audiopwmio.PWMAudioOut`` or
896
+ ``audioio.AudioOut``-like object with ``play(sample, loop=True)``,
897
+ ``stop()`` and optionally ``pause()``
898
+ :param int buffer_ms: length of each half of the output buffer. The output's DMA
899
+ needs internal RAM for twice the whole buffer; on a Fruit Jam with the display
900
+ running, 120 ms is about the most that fits at 48 kHz stereo
901
+ :param buffer: optional ``array('h')`` to use for both halves (its length
902
+ sets the half size; ``buffer_ms`` is then ignored)
903
+ :param float level: output level from 0.0 (silent) to 1.0 (unchanged), see `level`
904
+ """
905
+
906
+ def __init__(self, audio, *, buffer_ms=64, buffer=None, level=1.0):
907
+ self._audio = audio
908
+ self.level = level
909
+ self._buffer_ms = buffer_ms
910
+ self._fixed = buffer is not None
911
+ self._buf = buffer
912
+ self._mv = memoryview(buffer) if buffer is not None else None
913
+ self._zero = array("h", [0]) * 256
914
+ self._decoder = None
915
+ self._playing = False
916
+ self._paused = False
917
+ self.underruns = 0
918
+ """Count of buffer halves that were not ready in time."""
919
+
920
+ @property
921
+ def playing(self):
922
+ """True while a file is playing (including while paused)."""
923
+ return self._playing
924
+
925
+ @property
926
+ def paused(self):
927
+ """True while paused."""
928
+ return self._paused
929
+
930
+ @property
931
+ def decoder(self):
932
+ """The `FLACDecoder` being played."""
933
+ return self._decoder
934
+
935
+ @property
936
+ def level(self):
937
+ """Output level from 0.0 (silent) to 1.0 (unchanged, the default), like
938
+ ``audiomixer.MixerVoice.level``. Samples are scaled as they are decoded, so a
939
+ change is heard after the buffered audio (up to two ``buffer_ms`` halves)."""
940
+ return self._q / 32768
941
+
942
+ @level.setter
943
+ def level(self, value):
944
+ self._q = min(32768, max(0, int(value * 32768 + 0.5)))
945
+
946
+ def play(self, decoder, *, loop=False):
947
+ """Start playing ``decoder`` from its current position."""
948
+ import audiocore # noqa: PLC0415, so the decoder also imports on desktop Python
949
+
950
+ self.stop()
951
+ self._decoder = decoder
952
+ self._loop = loop
953
+ ch = decoder.channel_count
954
+ if self._fixed:
955
+ half = len(self._buf) // 2 // ch * ch
956
+ else:
957
+ half = max(256, decoder.sample_rate * self._buffer_ms // 1000) * ch
958
+ if self._buf is None or len(self._buf) != 2 * half:
959
+ self._buf = array("h", [0]) * (2 * half)
960
+ self._mv = memoryview(self._buf)
961
+ self._half = half
962
+ self._eof = False
963
+ self._last_real = -1
964
+ # prime both halves before starting the output
965
+ for w in range(2):
966
+ self._w = w
967
+ self._filled = 0
968
+ while self._filled < half:
969
+ self._fill_once()
970
+ if self._eof and self._last_real < 0:
971
+ self._last_real = w
972
+ self._w = 2
973
+ self._filled = 0
974
+ self._half_ns = half // ch * 1_000_000_000 // decoder.sample_rate
975
+ self._sample = audiocore.RawSample(
976
+ self._mv[: 2 * half],
977
+ channel_count=ch,
978
+ sample_rate=decoder.sample_rate,
979
+ single_buffer=False,
980
+ )
981
+ self._audio.play(self._sample, loop=True)
982
+ self._t0 = _now()
983
+ self._playing = True
984
+ self._paused = False
985
+
986
+ def _fill_once(self):
987
+ # Decode up to one frame into the half being written; pad with silence at the end.
988
+ half = self._half
989
+ a = (self._w & 1) * half + self._filled
990
+ room = half - self._filled
991
+ if not self._eof:
992
+ n = self._decoder.fill(self._mv[a : a + room])
993
+ if not n and self._loop:
994
+ self._decoder.rewind()
995
+ n = self._decoder.fill(self._mv[a : a + room])
996
+ if n:
997
+ if self._q < 32768:
998
+ _gain(self._mv[a : a + n], n, self._q)
999
+ self._filled += n
1000
+ return
1001
+ self._eof = True
1002
+ z = self._zero
1003
+ while room > 0:
1004
+ k = room if room < len(z) else len(z)
1005
+ self._mv[a : a + k] = z[:k]
1006
+ a += k
1007
+ room -= k
1008
+ self._filled = half
1009
+
1010
+ def update(self):
1011
+ """Do a slice of decoding work. Returns True while still playing."""
1012
+ if not self._playing:
1013
+ return False
1014
+ if self._paused:
1015
+ return True
1016
+ elapsed = _now() - self._t0
1017
+ hd = self._half_ns
1018
+ # halves the output has taken so far: two at start, then one per half
1019
+ fetched = 2 + elapsed // hd
1020
+ if self._last_real >= 0 and fetched >= self._last_real + 3:
1021
+ # the half after the last real one (silence) is now playing
1022
+ self.stop()
1023
+ return False
1024
+ w = self._w
1025
+ if w < fetched:
1026
+ # fell behind: half w was taken before it was ready; skip ahead
1027
+ self.underruns += 1
1028
+ w = self._w = fetched + 1
1029
+ self._filled = 0
1030
+ # Half w reuses the slot of half w - 2, taken at (w - 3) * hd. Leave a quarter
1031
+ # half of margin for the output's background task to copy it.
1032
+ if elapsed < (w - 3) * hd + (hd >> 2):
1033
+ return True
1034
+ self._fill_once()
1035
+ if self._filled >= self._half:
1036
+ if self._eof and self._last_real < 0:
1037
+ self._last_real = w
1038
+ self._w = w + 1
1039
+ self._filled = 0
1040
+ return True
1041
+
1042
+ def pause(self):
1043
+ """Pause output, if the audio object supports it."""
1044
+ if self._playing and not self._paused:
1045
+ self._audio.pause()
1046
+ self._pause_t = _now()
1047
+ self._paused = True
1048
+
1049
+ def resume(self):
1050
+ """Resume after `pause`."""
1051
+ if self._paused:
1052
+ self._t0 += _now() - self._pause_t
1053
+ self._audio.resume()
1054
+ self._paused = False
1055
+
1056
+ def stop(self):
1057
+ """Stop playback."""
1058
+ if self._playing:
1059
+ self._audio.stop()
1060
+ self._playing = False
1061
+ self._paused = False
1062
+
1063
+ def deinit(self):
1064
+ """Stop and release buffers."""
1065
+ self.stop()
1066
+ self._buf = None
1067
+ self._mv = None
1068
+ self._fixed = False
1069
+ self._sample = None
Binary file
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 Mikey Sklar for Adafruit Industries
2
+ #
3
+ # SPDX-License-Identifier: MIT
Binary file
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 Mikey Sklar for Adafruit Industries
2
+ #
3
+ # SPDX-License-Identifier: MIT
Binary file
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 Mikey Sklar for Adafruit Industries
2
+ #
3
+ # SPDX-License-Identifier: MIT
Binary file
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 Mikey Sklar for Adafruit Industries
2
+ #
3
+ # SPDX-License-Identifier: MIT