Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ audio pump holding up under live use such as a stompbox or a stage rig.

- The CircuitPython glue's folder takes a lasting name (`src/circuitpython_spike/`
becomes `src/circuitpython/`).
- `audiocore`, `audiomp3`, `audiopump` and `synthio` report a `__revision__`
like the other modules, so a board can say which source each was built from
(today those four have none on MicroPython).

Bugs, and things you need that don't work yet, go to
[issues](https://github.com/PyDevices/audiodsp/issues).
8 changes: 6 additions & 2 deletions docs/how-it-works/upstream-diff.md
Original file line number Diff line number Diff line change
Expand Up @@ -1570,8 +1570,12 @@ could not do either:
`audioeffects.Tremolo` is exactly this effect inside that ceiling; a ring
modulator wants hundreds of hertz and a carrier of a few kilohertz.

The arithmetic is `shared/audiodsp_multiply.c`, Q15 and stateless: the product
is `(a * b) >> 15`, blended `(dry * a + wet * product) >> 15`, clamped. The
The arithmetic is `shared/audiodsp_multiply.c`, Q15: the product is
`(a * b) >> 15`, blended `(dry * a + wet * product) >> 15`, clamped. The one
thing it carries between runs is a move of `mix`, which walks over 20 ms
(`shared/audiodsp_level_ramp.h`) a frame at a time, rounded to the Q15 grid,
and lands on `mix` exactly; before the first pull and after a reset `mix`
lands at once. The
two negative rails are the one product that lands outside `int16`, which is
what the clamp is for; [`tests/parity/multiply_probe.py`](../../tests/parity/multiply_probe.py) drives it there
deliberately rather than assuming.
Expand Down
5 changes: 3 additions & 2 deletions docs/reference/filters-and-effects.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ The reasons these exist beside CircuitPython's own modules are in

A level moved on a playing node walks to its new value in a straight line
over 20 ms instead of stepping, so turning it in one call doesn't click: the
`mix` of `Biquad`, `AllPass`, `Waveshaper` and `FeedbackDelay`, the
Waveshaper's `post_gain` and the Dynamics node's `makeup_db`. A node that
`mix` of `Biquad`, `AllPass`, `Waveshaper`, `FeedbackDelay` and
`audiomath.Multiply`, the Waveshaper's `post_gain` and the Dynamics node's
`makeup_db`. A node that
hasn't played yet, or has just been reset, takes its levels at once.

## `audiobiquad`
Expand Down
6 changes: 6 additions & 0 deletions src/audiocore/audiocore.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ of nodes produces.

from typing import Self

__version__: str
"""The audiodsp release this module came from."""
__revision__: str
"""Which source this build was made from, from `git describe`: a release tag,
or the nearest one and the commit. "unknown" when the build had no git."""

class RawSample:
"""Plays a buffer of raw PCM samples that you provide.

Expand Down
2 changes: 2 additions & 0 deletions src/audiocore/module.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "audiocore/RawSample.h"
#include "audiocore/WaveFile.h"
#include "audiocore/__init__.h"
#include "cp_compat/audiodsp_build.h"

#include "py/obj.h"
#include "py/runtime.h"
Expand Down Expand Up @@ -84,6 +85,7 @@ static MP_DEFINE_CONST_FUN_OBJ_1(audiocore_reset_buffer_obj, audiocore_reset_buf

static const mp_rom_map_elem_t audiocore_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_audiocore) },
AUDIODSP_BUILD_GLOBALS,
{ MP_ROM_QSTR(MP_QSTR_RawSample), MP_ROM_PTR(&audioio_rawsample_type) },
{ MP_ROM_QSTR(MP_QSTR_WaveFile), MP_ROM_PTR(&audioio_wavefile_type) },
{ MP_ROM_QSTR(MP_QSTR_get_buffer), MP_ROM_PTR(&audiocore_get_buffer_obj) },
Expand Down
18 changes: 11 additions & 7 deletions src/audiomath/Multiply.c
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ static mp_obj_t audiomath_multiply_make_new(const mp_obj_type_t *type,
audiodsp_multiply_config_init(&self->config);
audiodsp_multiply_set_channel_count(&self->config,
(uint32_t)self->base.channel_count);
audiodsp_multiply_set_sample_rate(&self->config, self->base.sample_rate);
audiodsp_multiply_state_clear(&self->state);

if (args[ARG_source].u_obj != mp_const_none) {
audiosample_base_t *sample = audiosample_check(args[ARG_source].u_obj);
Expand Down Expand Up @@ -178,7 +180,7 @@ static audioio_get_buffer_result_t audiomath_multiply_get_buffer(
if (run > self->pending_modulator_frames) {
run = self->pending_modulator_frames;
}
audiodsp_multiply_process_s16(&self->config,
audiodsp_multiply_process_s16(&self->config, &self->state,
&self->buffer[produced * self->base.channel_count],
self->pending_source,
self->pending_modulator, run);
Expand Down Expand Up @@ -208,12 +210,14 @@ static void audiomath_multiply_reset_buffer(mp_obj_t self_in,
bool single_channel_output, uint8_t channel) {
(void)single_channel_output;
(void)channel;
(void)self_in;
// Nothing to clear: the product holds nothing between frames. The frames
// already taken from the source and from the modulator are kept, the rule
// CircuitPython's effects follow for a host reset: neither input was
// reset, so dropping them would skip that much of it and shift the
// modulator against the source.
audiomath_multiply_obj_t *self = MP_OBJ_TO_PTR(self_in);
// The product holds nothing between frames; only a walk of `mix` in
// progress is cleared, so the next block lands at `mix` as it stands.
// The frames already taken from the source and from the modulator are
// kept, the rule CircuitPython's effects follow for a host reset: neither
// input was reset, so dropping them would skip that much of it and shift
// the modulator against the source.
audiodsp_multiply_state_clear(&self->state);
}

// `deinit()` releases what this binding holds and marks the node
Expand Down
2 changes: 2 additions & 0 deletions src/audiomath/Multiply.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ typedef struct {
// failure: no modulator means the signal passes through untouched.
mp_obj_t modulator;
audiodsp_multiply_config_t config;
// `mix` as the output stands, walking to a move (audiodsp_multiply.h).
audiodsp_multiply_state_t state;
int16_t buffer[AUDIODSP_MULTIPLY_FRAMES * 2];
// Frames fetched from each input but not yet consumed, carried across
// output blocks. The two inputs hand out different block lengths, so
Expand Down
4 changes: 4 additions & 0 deletions src/audiomath/audiomath.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ class Multiply:
def set(self, *, mix: float = ...) -> None:
"""Change an option while the node plays.

A move of ``mix`` on a playing node walks to the new value in a
straight line over 20 ms instead of stepping, so it doesn't click.
Before the first pull, or after a reset, it lands at once.

Args:
mix: Wet amount from 0.0 to 1.0, clamped.

Expand Down
6 changes: 6 additions & 0 deletions src/audiomp3/audiomp3.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ sample plays.

from typing import BinaryIO, Self

__version__: str
"""The audiodsp release this module came from."""
__revision__: str
"""Which source this build was made from, from `git describe`: a release tag,
or the nearest one and the commit. "unknown" when the build had no git."""

class MP3Decoder:
"""Decodes an MP3 file as it plays.

Expand Down
2 changes: 2 additions & 0 deletions src/audiomp3/module.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
// SPDX-License-Identifier: MIT

#include "audiomp3/MP3Decoder.h"
#include "cp_compat/audiodsp_build.h"

#include "py/obj.h"

static const mp_rom_map_elem_t audiomp3_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_audiomp3) },
AUDIODSP_BUILD_GLOBALS,
{ MP_ROM_QSTR(MP_QSTR_MP3Decoder), MP_ROM_PTR(&audiomp3_mp3file_type) },
};
static MP_DEFINE_CONST_DICT(audiomp3_module_globals, audiomp3_module_globals_table);
Expand Down
2 changes: 2 additions & 0 deletions src/audiopump/audiopump.c
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
#include "audiopump/audiopump_ring.h"
#include "audiopump/audiopump_tap.h"
#include "audiopump/audiopump_drift.h"
#include "cp_compat/audiodsp_build.h"

// The same acquire/release pair the push ring uses, and here for the same
// reason: `volatile` orders the compiler, not the machine, and a 64-bit store
Expand Down Expand Up @@ -2580,6 +2581,7 @@ static const mp_rom_map_elem_t audiopump_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR_lock_reset), MP_ROM_PTR(&audiopump_lock_reset_obj) },
{ MP_ROM_QSTR(MP_QSTR_fault), MP_ROM_PTR(&audiopump_fault_obj) },
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_audiopump) },
AUDIODSP_BUILD_GLOBALS,
{ MP_ROM_QSTR(MP_QSTR_pull), MP_ROM_PTR(&audiopump_pull_obj) },
{ MP_ROM_QSTR(MP_QSTR_spawn), MP_ROM_PTR(&audiopump_spawn_obj) },
{ MP_ROM_QSTR(MP_QSTR_join), MP_ROM_PTR(&audiopump_join_obj) },
Expand Down
6 changes: 6 additions & 0 deletions src/audiopump/audiopump.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ layer already pulls from its own audio thread.

from typing import Protocol, Self

__version__: str
"""The audiodsp release this module came from."""
__revision__: str
"""Which source this build was made from, from `git describe`: a release tag,
or the nearest one and the commit. "unknown" when the build had no git."""

class _AudioSample(Protocol):
@property
def sample_rate(self) -> int: ...
Expand Down
2 changes: 2 additions & 0 deletions src/circuitpython_spike/shared-bindings/audiomath/Multiply.c
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ static mp_obj_t audiomath_multiply_make_new(const mp_obj_type_t *type,
audiodsp_multiply_config_init(&self->config);
audiodsp_multiply_set_channel_count(&self->config,
(uint32_t)self->base.channel_count);
audiodsp_multiply_set_sample_rate(&self->config, self->base.sample_rate);
audiodsp_multiply_state_clear(&self->state);

if (args[ARG_source].u_obj != mp_const_none) {
audiosample_base_t *sample = audiosample_check(args[ARG_source].u_obj);
Expand Down
15 changes: 8 additions & 7 deletions src/circuitpython_spike/shared-module/audiomath/Multiply.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@

void audiomath_multiply_reset_buffer(audiomath_multiply_obj_t *self,
bool single_channel_output, uint8_t channel) {
(void)self;
(void)single_channel_output;
(void)channel;
// Nothing to clear: the product holds nothing between frames. The frames
// already taken from the source and from the modulator are kept, the rule
// CircuitPython's effects follow for a host reset: neither input was
// reset, so dropping them would skip that much of it and shift the
// modulator against the source.
// The product holds nothing between frames; only a walk of `mix` in
// progress is cleared, so the next block lands at `mix` as it stands.
// The frames already taken from the source and from the modulator are
// kept, the rule CircuitPython's effects follow for a host reset: neither
// input was reset, so dropping them would skip that much of it and shift
// the modulator against the source.
audiodsp_multiply_state_clear(&self->state);
}

audioio_get_buffer_result_t audiomath_multiply_get_buffer(
Expand Down Expand Up @@ -64,7 +65,7 @@ audioio_get_buffer_result_t audiomath_multiply_get_buffer(
if (run > self->pending_modulator_frames) {
run = self->pending_modulator_frames;
}
audiodsp_multiply_process_s16(&self->config,
audiodsp_multiply_process_s16(&self->config, &self->state,
&self->buffer[produced * self->base.channel_count],
self->pending_source,
self->pending_modulator, run);
Expand Down
2 changes: 2 additions & 0 deletions src/circuitpython_spike/shared-module/audiomath/Multiply.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ typedef struct {
// failure: no modulator means the signal passes through untouched.
mp_obj_t modulator;
audiodsp_multiply_config_t config;
// `mix` as the output stands, walking to a move (audiodsp_multiply.h).
audiodsp_multiply_state_t state;
int16_t buffer[AUDIODSP_MULTIPLY_FRAMES * 2];
// Frames fetched from each input but not yet consumed, carried across
// output blocks. The two inputs hand out different block lengths, so
Expand Down
25 changes: 23 additions & 2 deletions src/cpython/_audiodsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -5619,22 +5619,38 @@ static PyObject *audiodsp_meter_db_byte_py(PyObject *module, PyObject *args) {
static PyObject *audiodsp_multiply_s16(PyObject *module, PyObject *args) {
Py_buffer signal = {0};
Py_buffer modulator = {0};
Py_buffer state = {0};
double mix = 1.0;
unsigned int channel_count = 2;
if (!PyArg_ParseTuple(args, "y*y*d|I:multiply_s16", &signal, &modulator,
&mix, &channel_count)) {
unsigned int ramp_frames = 0;
// `state`, when given, is a writable buffer of MULTIPLY_STATE_BYTES the
// caller keeps between calls (zeroed is a cleared walk): a move of `mix`
// then walks over `ramp_frames`, as the MicroPython node's does.
if (!PyArg_ParseTuple(args, "y*y*d|Iw*I:multiply_s16", &signal,
&modulator, &mix, &channel_count, &state, &ramp_frames)) {
return NULL;
}
if (state.buf != NULL &&
state.len != (Py_ssize_t)sizeof(audiodsp_multiply_state_t)) {
PyBuffer_Release(&signal);
PyBuffer_Release(&modulator);
PyBuffer_Release(&state);
PyErr_SetString(PyExc_ValueError,
"state must be MULTIPLY_STATE_BYTES long");
return NULL;
}
if (channel_count < 1 || channel_count > 2) {
PyBuffer_Release(&signal);
PyBuffer_Release(&modulator);
PyBuffer_Release(&state);
PyErr_SetString(PyExc_ValueError, "channel_count must be 1 or 2");
return NULL;
}
const Py_ssize_t width = 2 * (Py_ssize_t)channel_count;
if (signal.len != modulator.len || signal.len % width) {
PyBuffer_Release(&signal);
PyBuffer_Release(&modulator);
PyBuffer_Release(&state);
PyErr_SetString(PyExc_ValueError,
"buffers must be the same whole number of configured frames");
return NULL;
Expand All @@ -5645,12 +5661,15 @@ static PyObject *audiodsp_multiply_s16(PyObject *module, PyObject *args) {
audiodsp_multiply_config_init(&config);
audiodsp_multiply_set_channel_count(&config, channel_count);
audiodsp_multiply_set_mix(&config, (float)mix);
config.ramp_frames = ramp_frames;
audiodsp_multiply_process_s16(&config,
(audiodsp_multiply_state_t *)state.buf,
(int16_t *)PyBytes_AS_STRING(result), (const int16_t *)signal.buf,
(const int16_t *)modulator.buf, (uint32_t)(signal.len / width));
}
PyBuffer_Release(&signal);
PyBuffer_Release(&modulator);
PyBuffer_Release(&state);
return result;
}

Expand Down Expand Up @@ -6591,6 +6610,8 @@ static int audiodsp_exec(PyObject *module) {
AUDIODSP_DYNAMICS_FRAMES) < 0) return -1;
if (PyModule_AddIntConstant(module, "MULTIPLY_FRAMES",
AUDIODSP_MULTIPLY_FRAMES) < 0) return -1;
if (PyModule_AddIntConstant(module, "MULTIPLY_STATE_BYTES",
(long)sizeof(audiodsp_multiply_state_t)) < 0) return -1;
if (PyModule_AddIntConstant(module, "MIDSIDE_FRAMES",
AUDIODSP_MIDSIDE_FRAMES) < 0) return -1;
state->feedback_delay_state_type = PyType_FromModuleAndSpec(module,
Expand Down
5 changes: 4 additions & 1 deletion src/cpython/audiocore.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@

import wave

import _audiodsp # noqa: F401 - verifies that the native runtime is present
import _audiodsp

__version__ = _audiodsp.__version__
__revision__ = _audiodsp.__revision__

GET_BUFFER_DONE = 0
GET_BUFFER_MORE_DATA = 1
Expand Down
18 changes: 12 additions & 6 deletions src/cpython/audiomath.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ def __init__(self, source=None, modulator=None, mix=1.0,
self._source = source
self._modulator = modulator
self._mix = 1.0
# A move of `mix` walks over 20 ms in the shared kernel, which keeps
# where the walk stands here between runs; zeroed is no walk.
self._ramp = bytearray(_audiodsp.MULTIPLY_STATE_BYTES)
self._ramp_frames = (self.sample_rate * 20 + 500) // 1000
self._pending_source = b""
self._pending_modulator = b""
self._apply({"mix": mix})
Expand Down Expand Up @@ -151,11 +155,13 @@ def _release(self):

def _reset_buffer(self, single_channel_output=False, audio_channel=0):
self._check()
# Nothing to clear: the product holds nothing between frames. The
# frames already taken from the source and from the modulator are
# kept, the rule CircuitPython's effects follow for a host reset:
# neither input was reset, so dropping them would skip that much of
# it and shift the modulator against the source.
# The product holds nothing between frames; only a walk of `mix` in
# progress is cleared, so the next block lands at `mix` as it
# stands. The frames already taken from the source and from the
# modulator are kept, the rule CircuitPython's effects follow for a
# host reset: neither input was reset, so dropping them would skip
# that much of it and shift the modulator against the source.
self._ramp[:] = bytes(len(self._ramp))

def _pull(self, sample):
result, data = get_buffer(sample, False, 0)
Expand Down Expand Up @@ -189,7 +195,7 @@ def _get_buffer(self, single_channel_output=False, audio_channel=0):
output += _audiodsp.multiply_s16(
self._pending_source[:run * width],
self._pending_modulator[:run * width], self._mix,
self.channel_count)
self.channel_count, self._ramp, self._ramp_frames)
self._pending_modulator = self._pending_modulator[run * width:]
else:
output += self._pending_source[:run * width]
Expand Down
3 changes: 3 additions & 0 deletions src/cpython/audiopump.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@
GET_BUFFER_ERROR, GET_BUFFER_MORE_DATA, _AudioSample, _borrow,
)

__version__ = _audiodsp.__version__
__revision__ = _audiodsp.__revision__

_fnv = _audiodsp.fnv1a64

STATUS_WORDS = 34
Expand Down
3 changes: 3 additions & 0 deletions src/cpython/synthio.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
import _audiodsp
from audiocore import GET_BUFFER_MORE_DATA, _AudioSample

__version__ = _audiodsp.__version__
__revision__ = _audiodsp.__revision__

waveform_max_length = 16384
_DEFAULT_WAVEFORM = array("h", (-32768, 32767))
_MIDI_BASE_FREQUENCIES = (8372, 8870, 9397, 9956, 10548, 11175, 11840,
Expand Down
Loading
Loading