Repository navigation
audiopump: DriftResampler, a Ring filled on another clock held at its target fill - #339
Merged
Merged
Conversation
… at its target fill A 32-tap windowed-sinc interpolator with a 32.32 read position, steered by a PI loop on the Ring's smoothed fill. The kernel is shared C; MicroPython, CircuitPython and CPython bind it and render it byte for byte. It lives in audiopump because CircuitPython owns audiospeed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
audiopump.DriftResampler. It plays aRingthat something else fills on its own clock, such as a USB microphone, a network stream or a second codec, and steers its ratio so the Ring stays at a target fill. Two devices that both say 48 kHz differ by tens of ppm, so without this a Ring between them slowly runs dry or overflows.How it works
The full write-up is docs/how-it-works/drift-resampler.md.
tools/design_drift_table.py, so every target uses the same numbers. Per stereo output frame this costs about 96 float multiply-adds. At 48 kHz that is about 3% of an ESP32-P4 core. That figure is estimated from the operation count and has not been timed on a board.SpeedChanger's 16.16 rate moves in steps of about 15 ppm.max_ppm. The loop starts at 8 times itsbandwidth(default 0.01 Hz, allowed 0.001 to 0.1 Hz) and narrows with a 15 s time constant. Playback starts at the target, and anything beyond it is dropped on the first block. An underrun plays a block of silence, is counted on the node and on the Ring, and the node then waits for the target again, keeping the rate it has learned.audiopumpbesideRing, not inaudiospeed, because CircuitPython has its ownaudiospeedand our modules never shadow CircuitPython's.The Ring gains consumer-side calls (
readable,read,starve) on MicroPython and CircuitPython so the node can read it at its own pace.Measured
These are desktop runs through the CPython build. A producer writes a Ring of 8 blocks of 256 frames (target 1024) on an offset clock, in 48-frame and 480-frame packets, for 90 to 120 s.
THD+N for a 0.9 FS sine (20 Hz to 20 kHz, fundamental excluded) is −93.7 to −95.2 dB at 1 kHz and 10 kHz in every case. A single 16-bit quantization measures −97.5 dB on the same analysis, and two quantizations (16-bit in, 16-bit out) come to about −94.5 dB.
Tests
tests/test_cpython_drift.pyopens with a trait table and runs in about 1.5 s. It covers lock at ±100 ppm with 48-frame and 480-frame packets, a wandering clock, THD+N below −92 dB at 1 kHz and 10 kHz, exact pass-through of a constant input, starting at the target, a stall,reset(), and every argument bound.tests/parity/drift_probe.py, added toverify_dsp.py, renders five cases three ways: ±3000 ppm, a stall, a retarget, and 44.1 to 48 kHz. MicroPython (unix), CircuitPython (unix coverage) and CPython give byte-identical output.verify_dsp.pyrun was not possible because the local MicroPython build lacked other modules.tests/test_binding_parity.pynow holds the twoDriftResamplerbindings to the same names.Not done