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
9 changes: 5 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ for source compatibility; only this repo's own name differs.
`audiospeed/`, `audiofreeverb/`, `audiofilters/`, `audiodelays/`,
`audiomp3/`, `audiodynamics/`, `audioroute/`, `audiomath/`, `audioecho/`,
`audioconvolve/`, `audiobiquad/`, `audioshaper/`, `audioladder/`,
`audiooscillator/`,
`audioverb/`, `audiomodal/`, `audiosampler/`, `audiovorbis/`), plus
`src/cp_compat/`
(CircuitPython-only core primitives ported as standalone compat shims, each
Expand All @@ -38,11 +39,11 @@ for source compatibility; only this repo's own name differs.
repository builds, tests, publishes or freezes them.
- `circuitpython.mk` + `src/circuitpython_spike/` — build `audiodynamics`,
`audioroute`, `audiomath`, `audioecho`, `audioshaper`, `audioladder`,
`audioconvolve`, `audiobiquad`, `audioverb`, `audiomodal`, `audiosampler`
and `audiovorbis` into CircuitPython 11 as a user C module
`audioconvolve`, `audiobiquad`, `audioverb`, `audiomodal`, `audiosampler`,
`audiooscillator` and `audiovorbis` into CircuitPython 11 as a user C module
(`micropython.mk` hands a CircuitPython tree to `circuitpython.mk`). None
of the twelve is a CircuitPython port: the first two come from
micropython-vst3's `vstaudio` engine and the last ten are audiodsp's own,
of the thirteen is a CircuitPython port: the first two come from
micropython-vst3's `vstaudio` engine and the last eleven are audiodsp's own,
so CircuitPython gains them
here rather than the other way round.
- `docs/porting-plan.md` — the full phased porting history, architecture,
Expand Down
38 changes: 35 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ python -m pip install pydevices-audiodsp

This gets you `audiocore`, `synthio`, `audiomixer`, `audiofilters`,
`audiodelays`, `audiofreeverb`, `audiospeed`, `audiodynamics`, `audioroute`,
`audiomath`, `audioecho`, `audioshaper`, `audioladder`, `audioconvolve`,
`audiobiquad`, `audioverb`, `audiomodal`, `audiosampler`, `audiometer`,
`audiomath`, `audioecho`, `audioshaper`, `audioladder`, `audiooscillator`,
`audioconvolve`, `audiobiquad`, `audioverb`, `audiomodal`, `audiosampler`, `audiometer`,
`audiopump`, `audiovorbis`, and the `audiorender` package.
`audiomp3` remains firmware-only. The distribution declares no *required*
runtime dependencies and does not itself publish an `audiodsp` import; its
Expand Down Expand Up @@ -156,7 +156,9 @@ a soft-clip and a cross-feed inside its feedback loop), `audioshaper` (a
waveshaper whose curve is data, applied above the sample rate, and a
sample-and-hold at an exact rational ratio),
`audioladder` (a transistor ladder filter — four one-pole stages round a
feedback loop with an odd saturator inside it), `audioconvolve` (apply a
feedback loop with an odd saturator inside it), `audiooscillator` (a
band-limited oscillator whose pitch, FM, pulse width and hard sync are audio
streams, read sample by sample), `audioconvolve` (apply a
measured or synthesized impulse response, by partitioned FFT), `audioverb`
(a reverberation tank whose line lengths and output taps come from Python),
`audiomodal` (a bank of resonators, which is what a struck object is),
Expand Down Expand Up @@ -359,12 +361,42 @@ ladder = audioladder.Ladder(
passband_comp=0.0, # 0..1, how much of the passband to give back
oversample=2, # 1 or 2; 2 folds back fewer of its harmonics
mix=1.0, # 0 a wire, 1 the filter
buffer_size=None, # bytes a block, as on the effects; 256 frames
)
ladder.play(source)
ladder.set(cutoff_hz=1200.0) # any option, mid-stream, once a block
ladder.clear() # empty the integrators; stops a sustained tone
```

`audiooscillator` is the oscillator synthio does not have. synthio's moves its
pitch once per block and reads a wavetable with no band limiting; this one
reads its pitch, linear FM, pulse width and hard sync from audio streams every
sample, so one oscillator can sync to another or be frequency-modulated by it:

```python
import audiooscillator as ao

lead = ao.Oscillator(
sample_rate=48000, # and channel_count=1 or 2
frequency=220.0, # Hz, before the pitch input
waveform=ao.SAW, # any sum of SAW, PULSE, TRIANGLE and SINE
waveform_right=ao.PULSE, # the right channel's, from the same phase
pulse_width=0.5, # 0.05..0.95
level=0.5, # output for a waveform's full swing
pitch_octaves=10.0, # octaves for a full-scale pitch input
fm_depth=300.0, # Hz for a full-scale FM input, linear
pwm_depth=0.5, # pulse width for a full-scale PWM input
buffer_size=None, # bytes a block; 256 frames
)
lead.modulate(sync=ao.Oscillator(frequency=110.0, waveform=ao.PULSE))
lead.set(frequency=330.0) # any option, mid-stream
lead.clear() # phase back to zero
```

Steps are band-limited with PolyBLEP and the triangle's corners with
PolyBLAMP. The output trails the phase by one sample, which is what lets a
sync edge, which nothing can see coming, still correct the sample before it.

See [docs/upstream-diff.md](docs/upstream-diff.md) for why its feedback loop
is solved rather than delayed, and what that was measured to be worth.

Expand Down
5 changes: 3 additions & 2 deletions circuitpython.mk
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ AUDIODSP_CP_SPIKE := $(AUDIODSP_CP_SRC)/circuitpython_spike
CFLAGS_USERMOD += -I$(AUDIODSP_CP_SPIKE) -I$(AUDIODSP_CP_SRC)

AUDIODSP_CP_MODULES := audiodynamics audioroute audiomath audioecho audioshaper \
audioladder audioconvolve audiobiquad audioverb audiomodal audiosampler \
audiovorbis
audioladder audiooscillator audioconvolve audiobiquad audioverb audiomodal \
audiosampler audiovorbis

AUDIODSP_CP_SOURCES := \
$(foreach m,$(AUDIODSP_CP_MODULES),$(sort $(wildcard $(AUDIODSP_CP_SPIKE)/shared-bindings/$(m)/*.c))) \
Expand All @@ -48,6 +48,7 @@ AUDIODSP_CP_SOURCES += $(addprefix $(AUDIODSP_CP_SRC)/shared/, \
audiodsp_shaper.c \
audiodsp_samplehold.c \
audiodsp_ladder.c \
audiodsp_oscillator.c \
audiodsp_trig.c \
audiodsp_fft.c \
audiodsp_convolve.c \
Expand Down
3 changes: 3 additions & 0 deletions micropython.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ target_sources(usermod_mpaudio INTERFACE
${MPAUDIO_SRC_DIR}/shared/audiodsp_convolve.c
${MPAUDIO_SRC_DIR}/shared/audiodsp_tank.c
${MPAUDIO_SRC_DIR}/shared/audiodsp_modal.c
${MPAUDIO_SRC_DIR}/shared/audiodsp_oscillator.c
${MPAUDIO_SRC_DIR}/shared/audiodsp_sampler.c
${MPAUDIO_SRC_DIR}/shared/audiodsp_meter.c
${MPAUDIO_SRC_DIR}/audiometer/module.c
Expand Down Expand Up @@ -111,6 +112,8 @@ target_sources(usermod_mpaudio INTERFACE
${MPAUDIO_SRC_DIR}/audioshaper/module.c
${MPAUDIO_SRC_DIR}/audioladder/Ladder.c
${MPAUDIO_SRC_DIR}/audioladder/module.c
${MPAUDIO_SRC_DIR}/audiooscillator/Oscillator.c
${MPAUDIO_SRC_DIR}/audiooscillator/module.c
${MPAUDIO_SRC_DIR}/audioconvolve/Convolver.c
${MPAUDIO_SRC_DIR}/audioconvolve/module.c
${MPAUDIO_SRC_DIR}/audiobiquad/Biquad.c
Expand Down
10 changes: 8 additions & 2 deletions micropython.mk
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ SRC_USERMOD_C += $(MPAUDIO_SRC_DIR)/shared/audiodsp_fft.c
SRC_USERMOD_C += $(MPAUDIO_SRC_DIR)/shared/audiodsp_convolve.c
SRC_USERMOD_C += $(MPAUDIO_SRC_DIR)/shared/audiodsp_tank.c
SRC_USERMOD_C += $(MPAUDIO_SRC_DIR)/shared/audiodsp_modal.c
SRC_USERMOD_C += $(MPAUDIO_SRC_DIR)/shared/audiodsp_oscillator.c
SRC_USERMOD_C += $(MPAUDIO_SRC_DIR)/shared/audiodsp_sampler.c

# --- the pump engine: the portable half of the live audio path --------------
Expand Down Expand Up @@ -210,7 +211,8 @@ SRC_USERMOD_C += \

# --- tier 6: audiodynamics (Dynamics, DYN_*), audioroute (Splitter,
# MidSide), audiomath (Multiply, SubOctave), audioecho
# (FeedbackDelay), audioladder (Ladder), audioconvolve (Convolver),
# (FeedbackDelay), audioladder (Ladder), audiooscillator (Oscillator),
# audioconvolve (Convolver),
# audiobiquad (Biquad, AllPass), audioverb (Tank),
# audiomodal (Bank) and audiosampler (Sampler, SamplerVoice) ---
#
Expand All @@ -219,7 +221,8 @@ SRC_USERMOD_C += \
# library; the rest are audiodsp's own -- audiomath is the only way to modulate
# a stream at audio rate and the only way to divide one down in frequency,
# audioecho puts a filter inside a delay's feedback loop, audioladder puts
# four one-poles and a saturator inside a filter's, audioconvolve applies a
# four one-poles and a saturator inside a filter's, audiooscillator reads its
# pitch, FM, pulse width and sync per sample, audioconvolve applies a
# measured impulse response rather than imitating one, audiobiquad
# filters in float so a tail reaches exact zero, which neither ported
# kernel can (audiodsp#23, #36), and audioverb is a reverberation tank whose
Expand Down Expand Up @@ -259,6 +262,9 @@ SRC_USERMOD_C += \
SRC_USERMOD_C += \
$(MPAUDIO_SRC_DIR)/audioladder/Ladder.c \
$(MPAUDIO_SRC_DIR)/audioladder/module.c
SRC_USERMOD_C += \
$(MPAUDIO_SRC_DIR)/audiooscillator/Oscillator.c \
$(MPAUDIO_SRC_DIR)/audiooscillator/module.c
SRC_USERMOD_C += \
$(MPAUDIO_SRC_DIR)/audioconvolve/Convolver.c \
$(MPAUDIO_SRC_DIR)/audioconvolve/module.c
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Repository = "https://github.com/PyDevices/audiodsp"
Issues = "https://github.com/PyDevices/audiodsp/issues"

[tool.setuptools]
py-modules = ["audiocore", "synthio", "audiomixer", "audiofilters", "audiodelays", "audiofreeverb", "audiospeed", "audiodynamics", "audioroute", "audiomath", "audioecho", "audioconvolve", "audiobiquad", "audioshaper", "audioladder", "audioverb", "audiomodal", "audiosampler", "audiometer", "audiopump", "audiovorbis"]
py-modules = ["audiocore", "synthio", "audiomixer", "audiofilters", "audiodelays", "audiofreeverb", "audiospeed", "audiodynamics", "audioroute", "audiomath", "audioecho", "audioconvolve", "audiobiquad", "audioshaper", "audioladder", "audiooscillator", "audioverb", "audiomodal", "audiosampler", "audiometer", "audiopump", "audiovorbis"]
# Keep py-modules explicit: src/cpython is the root package directory now, and
# an automatic scan there would also pick up anything else that lands in it.
packages = ["audiorender", "audiodsp_util"]
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"src/shared/audiodsp_shaper.c",
"src/shared/audiodsp_samplehold.c",
"src/shared/audiodsp_ladder.c",
"src/shared/audiodsp_oscillator.c",
"src/shared/audiodsp_trig.c",
"src/shared/audiodsp_fft.c",
"src/shared/audiodsp_convolve.c",
Expand Down
29 changes: 24 additions & 5 deletions src/audioladder/Ladder.c
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ static void ladder_apply_kwargs(audioladder_ladder_obj_t *self,
continue;
}
qstr name = mp_obj_str_get_qstr(kw->table[i].key);
if (name == MP_QSTR_sample_rate || name == MP_QSTR_channel_count) {
if (name == MP_QSTR_sample_rate || name == MP_QSTR_channel_count ||
name == MP_QSTR_buffer_size) {
continue;
}
float value = (float)mp_obj_get_float(kw->table[i].value);
Expand Down Expand Up @@ -81,6 +82,7 @@ static mp_obj_t audioladder_ladder_make_new(const mp_obj_type_t *type,

uint32_t sample_rate = 48000;
uint32_t channel_count = 2;
mp_obj_t buffer_size = mp_const_none;
for (size_t i = 0; i < kw_map.alloc; ++i) {
if (!mp_map_slot_is_filled(&kw_map, i)) {
continue;
Expand All @@ -90,6 +92,8 @@ static mp_obj_t audioladder_ladder_make_new(const mp_obj_type_t *type,
sample_rate = (uint32_t)mp_obj_get_int(kw_map.table[i].value);
} else if (name == MP_QSTR_channel_count) {
channel_count = (uint32_t)mp_obj_get_int(kw_map.table[i].value);
} else if (name == MP_QSTR_buffer_size) {
buffer_size = kw_map.table[i].value;
}
}
if (sample_rate < 1u) {
Expand All @@ -98,11 +102,26 @@ static mp_obj_t audioladder_ladder_make_new(const mp_obj_type_t *type,
if (channel_count < 1u || channel_count > 2u) {
mp_raise_ValueError(MP_ERROR_TEXT("channel_count must be 1 or 2"));
}
// `buffer_size` is bytes per block, as on the CircuitPython effects, so a
// host running small blocks gets them without the latency of gathering
// 256 frames. The filter is per sample, so it is the same at any size.
uint32_t frames = AUDIODSP_LADDER_FRAMES;
if (buffer_size != mp_const_none) {
const mp_int_t bytes = mp_obj_get_int(buffer_size);
const mp_int_t width = 2 * (mp_int_t)channel_count;
if (bytes < width || bytes % width != 0 ||
bytes > (mp_int_t)AUDIODSP_LADDER_FRAMES * width) {
mp_raise_ValueError(MP_ERROR_TEXT(
"buffer_size must be whole frames, from one to 256"));
}
frames = (uint32_t)(bytes / width);
}

audioladder_ladder_obj_t *self =
mp_obj_malloc(audioladder_ladder_obj_t, type);
self->base.sample_rate = sample_rate;
self->base.max_buffer_length = sizeof(self->buffer);
self->base.max_buffer_length = frames * 2u * channel_count;
self->frames = frames;
self->base.bits_per_sample = 16;
self->base.channel_count = (uint8_t)channel_count;
self->base.samples_signed = 1;
Expand Down Expand Up @@ -163,7 +182,7 @@ static audioio_get_buffer_result_t audioladder_ladder_get_buffer(
(void)channel;
audioladder_ladder_obj_t *self = MP_OBJ_TO_PTR(self_in);
uint32_t produced = 0;
while (produced < AUDIODSP_LADDER_FRAMES) {
while (produced < self->frames) {
if (self->pending_frames == 0) {
if (self->source == MP_OBJ_NULL) {
break;
Expand All @@ -179,7 +198,7 @@ static audioio_get_buffer_result_t audioladder_ladder_get_buffer(
self->pending = (const int16_t *)raw;
self->pending_frames = raw_bytes / width;
}
uint32_t run = AUDIODSP_LADDER_FRAMES - produced;
uint32_t run = self->frames - produced;
if (run > self->pending_frames) {
run = self->pending_frames;
}
Expand All @@ -196,7 +215,7 @@ static audioio_get_buffer_result_t audioladder_ladder_get_buffer(
// frames that arrive.
if (produced == 0) {
memset(self->buffer, 0, sizeof(self->buffer));
produced = AUDIODSP_LADDER_FRAMES;
produced = self->frames;
}
*buffer = (uint8_t *)self->buffer;
*buffer_length = produced * 2u * self->base.channel_count;
Expand Down
2 changes: 2 additions & 0 deletions src/audioladder/Ladder.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ typedef struct {
// blocks.
const int16_t *pending;
uint32_t pending_frames;
// Frames per output block: buffer_size's, at most AUDIODSP_LADDER_FRAMES.
uint32_t frames;
} audioladder_ladder_obj_t;

extern const mp_obj_type_t audioladder_ladder_type;
Loading
Loading