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: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,13 +361,22 @@ 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
cutoff_octaves=10.0, # octaves for a full-scale cutoff input
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.modulate(cutoff=lfo) # the cutoff read from a stream, every sample
ladder.clear() # empty the integrators; stops a sustained tone
```

`set()` moves the cutoff once a block, which is plenty for an envelope or a
knob. Sweep it with an audio-rate signal that way and you hear the staircase,
a buzz at the block rate. The `cutoff` input reads the cutoff every sample
instead, as `cutoff_hz * 2 ** (v * cutoff_octaves)` for an input at `v` of
full scale (first channel only), so filter FM sounds like filter FM. An input
of zero, or none at all, is the filter exactly as `cutoff_hz` sets it.

`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
Expand Down
119 changes: 113 additions & 6 deletions src/audioladder/Ladder.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ static const ladder_option_name_t ladder_option_names[] = {
{ MP_QSTR_passband_comp, AUDIODSP_LADDER_OPT_PASSBAND_COMP },
{ MP_QSTR_oversample, AUDIODSP_LADDER_OPT_OVERSAMPLE },
{ MP_QSTR_mix, AUDIODSP_LADDER_OPT_MIX },
{ MP_QSTR_cutoff_octaves, AUDIODSP_LADDER_OPT_CUTOFF_OCTAVES },
};

// Every option is read and checked into a copy of the config before
Expand All @@ -43,7 +44,8 @@ static void ladder_apply_kwargs(audioladder_ladder_obj_t *self,
}
qstr name = mp_obj_str_get_qstr(kw->table[i].key);
if (name == MP_QSTR_sample_rate || name == MP_QSTR_channel_count ||
name == MP_QSTR_buffer_size) {
name == MP_QSTR_buffer_size ||
(!live && name == MP_QSTR_cutoff)) {
continue;
}
float value = (float)mp_obj_get_float(kw->table[i].value);
Expand Down Expand Up @@ -74,6 +76,51 @@ static void ladder_apply_kwargs(audioladder_ladder_obj_t *self,
}
}

// Connects the cutoff input if `kw` names it. Checked first, stored after
// under the lock, so a bad one leaves the input as it was. `strict` is
// modulate()'s: from the constructor every other keyword is an option.
static void ladder_apply_inputs(audioladder_ladder_obj_t *self,
const mp_map_t *kw, bool strict) {
bool named = false;
mp_obj_t chosen = MP_OBJ_NULL;
uint8_t channels = 1;
for (size_t i = 0; i < kw->alloc; ++i) {
if (!mp_map_slot_is_filled(kw, i)) {
continue;
}
qstr name = mp_obj_str_get_qstr(kw->table[i].key);
if (name != MP_QSTR_cutoff) {
if (strict) {
mp_raise_msg_varg(&mp_type_TypeError,
MP_ERROR_TEXT("unknown Ladder input '%q'"), name);
}
continue;
}
mp_obj_t sample = kw->table[i].value;
named = true;
chosen = MP_OBJ_NULL;
channels = 1;
if (sample != mp_const_none) {
audiosample_base_t *base = audiosample_check(sample);
if (base->channel_count < 1 || base->channel_count > 2) {
mp_raise_ValueError(
MP_ERROR_TEXT("an input must be mono or stereo"));
}
chosen = sample;
channels = base->channel_count;
}
}
if (!named) {
return;
}
audiodsp_pump_lock_acquire();
self->cutoff = chosen;
self->cutoff_channels = channels;
self->cutoff_pending = NULL;
self->cutoff_pending_frames = 0;
audiodsp_pump_lock_release();
}

static mp_obj_t audioladder_ladder_make_new(const mp_obj_type_t *type,
size_t n_args, size_t n_kw, const mp_obj_t *all_args) {
mp_arg_check_num(n_args, n_kw, 0, 0, true);
Expand Down Expand Up @@ -129,13 +176,18 @@ static mp_obj_t audioladder_ladder_make_new(const mp_obj_type_t *type,
self->source = MP_OBJ_NULL;
self->pending = NULL;
self->pending_frames = 0;
self->cutoff = MP_OBJ_NULL;
self->cutoff_pending = NULL;
self->cutoff_pending_frames = 0;
self->cutoff_channels = 1;

audiodsp_ladder_config_init(&self->config, sample_rate);
audiodsp_ladder_set_channel_count(&self->config, channel_count);
audiodsp_ladder_state_init(&self->state);

ladder_apply_kwargs(self, &kw_map, false);
audiodsp_ladder_config_finish(&self->config);
ladder_apply_inputs(self, &kw_map, false);
return MP_OBJ_FROM_PTR(self);
}

Expand Down Expand Up @@ -164,6 +216,17 @@ static mp_obj_t audioladder_ladder_set(size_t n_args, const mp_obj_t *args,
static MP_DEFINE_CONST_FUN_OBJ_KW(audioladder_ladder_set_obj, 1,
audioladder_ladder_set);

static mp_obj_t audioladder_ladder_modulate(size_t n_args,
const mp_obj_t *args, mp_map_t *kw_args) {
audioladder_ladder_obj_t *self = MP_OBJ_TO_PTR(args[0]);
audiosample_check_for_deinit(&self->base);
(void)n_args;
ladder_apply_inputs(self, kw_args, true);
return mp_const_none;
}
static MP_DEFINE_CONST_FUN_OBJ_KW(audioladder_ladder_modulate_obj, 1,
audioladder_ladder_modulate);

static mp_obj_t audioladder_ladder_clear(mp_obj_t self_in) {
audioladder_ladder_obj_t *self = MP_OBJ_TO_PTR(self_in);
audiosample_check_for_deinit(&self->base);
Expand Down Expand Up @@ -202,9 +265,39 @@ static audioio_get_buffer_result_t audioladder_ladder_get_buffer(
if (run > self->pending_frames) {
run = self->pending_frames;
}
audiodsp_ladder_process_s16(&self->config, &self->state,
&self->buffer[produced * self->base.channel_count],
self->pending, run);
const int16_t *cutoff = NULL;
if (self->cutoff != MP_OBJ_NULL) {
if (self->cutoff_pending_frames == 0) {
uint8_t *raw = NULL;
uint32_t raw_bytes = 0;
audioio_get_buffer_result_t result = audiosample_get_buffer(
self->cutoff, false, 0, &raw, &raw_bytes);
const uint32_t width = 2u * self->cutoff_channels;
// An input that has run dry counts as zero for this run.
if (result != GET_BUFFER_ERROR && raw != NULL &&
raw_bytes >= width) {
self->cutoff_pending = (const int16_t *)raw;
self->cutoff_pending_frames = raw_bytes / width;
}
}
if (self->cutoff_pending_frames != 0) {
if (run > self->cutoff_pending_frames) {
run = self->cutoff_pending_frames;
}
cutoff = self->cutoff_pending;
}
}
if (cutoff == NULL) {
audiodsp_ladder_process_s16(&self->config, &self->state,
&self->buffer[produced * self->base.channel_count],
self->pending, run);
} else {
audiodsp_ladder_process_cutoff_s16(&self->config, &self->state,
&self->buffer[produced * self->base.channel_count],
self->pending, run, cutoff, self->cutoff_channels);
self->cutoff_pending += run * self->cutoff_channels;
self->cutoff_pending_frames -= run;
}
self->pending += run * self->base.channel_count;
self->pending_frames -= run;
produced += run;
Expand Down Expand Up @@ -257,6 +350,9 @@ static mp_obj_t audioladder_ladder_deinit(mp_obj_t self_in) {
self->source = mp_const_none;
self->pending = NULL;
self->pending_frames = 0;
self->cutoff = MP_OBJ_NULL;
self->cutoff_pending = NULL;
self->cutoff_pending_frames = 0;
audiodsp_pump_lock_release();
return mp_const_none;
}
Expand All @@ -268,6 +364,8 @@ static const mp_rom_map_elem_t audioladder_ladder_locals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&default___exit___obj) },
{ MP_ROM_QSTR(MP_QSTR_play), MP_ROM_PTR(&audioladder_ladder_play_obj) },
{ MP_ROM_QSTR(MP_QSTR_set), MP_ROM_PTR(&audioladder_ladder_set_obj) },
{ MP_ROM_QSTR(MP_QSTR_modulate),
MP_ROM_PTR(&audioladder_ladder_modulate_obj) },
{ MP_ROM_QSTR(MP_QSTR_clear), MP_ROM_PTR(&audioladder_ladder_clear_obj) },
AUDIOSAMPLE_FIELDS,
};
Expand All @@ -278,8 +376,17 @@ static MP_DEFINE_CONST_DICT(audioladder_ladder_locals,
// audiodsp#112: the pump refuses a file-backed source at handover,
// and the protocol had no way to look past the tail.
static mp_obj_t audioladder_ladder_sources(mp_obj_t self_in, uint8_t index) {
return index == 0 ? ((audioladder_ladder_obj_t *)MP_OBJ_TO_PTR(self_in))->source
: MP_OBJ_NULL;
audioladder_ladder_obj_t *self = MP_OBJ_TO_PTR(self_in);
// An empty slot is mp_const_none and the walk asks for the next one;
// MP_OBJ_NULL ends it, so a source not yet played must not hide the
// cutoff input behind it.
if (index == 0) {
return self->source == MP_OBJ_NULL ? mp_const_none : self->source;
}
if (index == 1) {
return self->cutoff == MP_OBJ_NULL ? mp_const_none : self->cutoff;
}
return MP_OBJ_NULL;
}

static const audiosample_p_t audioladder_ladder_proto = {
Expand Down
6 changes: 6 additions & 0 deletions src/audioladder/Ladder.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ typedef struct {
uint32_t pending_frames;
// Frames per output block: buffer_size's, at most AUDIODSP_LADDER_FRAMES.
uint32_t frames;
// The cutoff input, MP_OBJ_NULL when not connected, and its frames
// fetched but not yet consumed.
mp_obj_t cutoff;
const int16_t *cutoff_pending;
uint32_t cutoff_pending_frames;
uint8_t cutoff_channels;
} audioladder_ladder_obj_t;

extern const mp_obj_type_t audioladder_ladder_type;
90 changes: 85 additions & 5 deletions src/circuitpython_spike/shared-bindings/audioladder/Ladder.c
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@
//| sample_rate: int = 48000,
//| channel_count: int = 2,
//| buffer_size: Optional[int] = None,
//| cutoff: Optional[circuitpython_typing.AudioSample] = None,
//| cutoff_hz: float = 1000.0,
//| resonance: float = 0.0,
//| drive: float = 1.0,
//| poles: int = 4,
//| passband_comp: float = 0.0,
//| oversample: int = 2,
//| mix: float = 1.0,
//| cutoff_octaves: float = 10.0,
//| ) -> None:
//| """Create a ladder filter.
//|
Expand All @@ -53,7 +55,15 @@
//| back less. ``mix`` is a plain crossfade, 0 a wire and 1 the
//| filter. ``buffer_size`` is the size in bytes of each block handed
//| out, as on the CircuitPython effects, from one frame to the
//| default of 256."""
//| default of 256.
//|
//| ``cutoff`` is an audio input that moves the cutoff every sample,
//| for filter FM at an audio rate. Its first channel is read as a
//| fraction ``v`` of full scale, and the cutoff is
//| ``cutoff_hz * 2 ** (v * cutoff_octaves)``, clamped to the same
//| band ``cutoff_hz`` is. An input that is not connected, or runs
//| dry, counts as zero, and zero is the filter as ``cutoff_hz``
//| sets it."""
//| ...

// The options __init__ and set() accept, paired with the shared DSP's enum.
Expand All @@ -73,17 +83,19 @@ static const ladder_option_name_t ladder_option_names[] = {
{ MP_QSTR_passband_comp, AUDIODSP_LADDER_OPT_PASSBAND_COMP },
{ MP_QSTR_oversample, AUDIODSP_LADDER_OPT_OVERSAMPLE },
{ MP_QSTR_mix, AUDIODSP_LADDER_OPT_MIX },
{ MP_QSTR_cutoff_octaves, AUDIODSP_LADDER_OPT_CUTOFF_OCTAVES },
};

static void ladder_apply_kwargs(audioladder_ladder_obj_t *self,
const mp_map_t *kw) {
const mp_map_t *kw, bool live) {
for (size_t i = 0; i < kw->alloc; ++i) {
if (!mp_map_slot_is_filled(kw, i)) {
continue;
}
qstr name = mp_obj_str_get_qstr(kw->table[i].key);
if (name == MP_QSTR_sample_rate || name == MP_QSTR_channel_count ||
name == MP_QSTR_buffer_size) {
name == MP_QSTR_buffer_size ||
(!live && name == MP_QSTR_cutoff)) {
continue;
}
float value = (float)mp_obj_get_float(kw->table[i].value);
Expand All @@ -104,6 +116,49 @@ static void ladder_apply_kwargs(audioladder_ladder_obj_t *self,
}
}

// Connects the cutoff input if `kw` names it. Checked first, stored after,
// so a bad one leaves the input as it was. `strict` is modulate()'s: from
// the constructor every other keyword is an option.
static void ladder_apply_inputs(audioladder_ladder_obj_t *self,
const mp_map_t *kw, bool strict) {
bool named = false;
mp_obj_t chosen = MP_OBJ_NULL;
uint8_t channels = 1;
for (size_t i = 0; i < kw->alloc; ++i) {
if (!mp_map_slot_is_filled(kw, i)) {
continue;
}
qstr name = mp_obj_str_get_qstr(kw->table[i].key);
if (name != MP_QSTR_cutoff) {
if (strict) {
mp_raise_msg_varg(&mp_type_TypeError,
MP_ERROR_TEXT("unknown Ladder input '%q'"), name);
}
continue;
}
mp_obj_t sample = kw->table[i].value;
named = true;
chosen = MP_OBJ_NULL;
channels = 1;
if (sample != mp_const_none) {
audiosample_base_t *base = audiosample_check(sample);
if (base->channel_count < 1 || base->channel_count > 2) {
mp_raise_ValueError(
MP_ERROR_TEXT("an input must be mono or stereo"));
}
chosen = sample;
channels = base->channel_count;
}
}
if (!named) {
return;
}
self->cutoff = chosen;
self->cutoff_channels = channels;
self->cutoff_pending = NULL;
self->cutoff_pending_frames = 0;
}

static mp_obj_t audioladder_ladder_make_new(const mp_obj_type_t *type,
size_t n_args, size_t n_kw, const mp_obj_t *all_args) {
mp_arg_check_num(n_args, n_kw, 0, 0, true);
Expand Down Expand Up @@ -159,13 +214,18 @@ static mp_obj_t audioladder_ladder_make_new(const mp_obj_type_t *type,
self->source = MP_OBJ_NULL;
self->pending = NULL;
self->pending_frames = 0;
self->cutoff = MP_OBJ_NULL;
self->cutoff_pending = NULL;
self->cutoff_pending_frames = 0;
self->cutoff_channels = 1;

audiodsp_ladder_config_init(&self->config, sample_rate);
audiodsp_ladder_set_channel_count(&self->config, channel_count);
audiodsp_ladder_state_init(&self->state);

ladder_apply_kwargs(self, &kw_map);
ladder_apply_kwargs(self, &kw_map, false);
audiodsp_ladder_config_finish(&self->config);
ladder_apply_inputs(self, &kw_map, false);
return MP_OBJ_FROM_PTR(self);
}

Expand Down Expand Up @@ -194,12 +254,27 @@ static mp_obj_t audioladder_ladder_set(size_t n_args, const mp_obj_t *args,
audioladder_ladder_obj_t *self = MP_OBJ_TO_PTR(args[0]);
audiosample_check_for_deinit(&self->base);
(void)n_args;
ladder_apply_kwargs(self, kw_args);
ladder_apply_kwargs(self, kw_args, true);
return mp_const_none;
}
MP_DEFINE_CONST_FUN_OBJ_KW(audioladder_ladder_set_obj, 1,
audioladder_ladder_set);

//| def modulate(self, **inputs: Optional[circuitpython_typing.AudioSample]) -> None:
//| """Connect inputs by name: ``cutoff``. None disconnects it; an
//| input not named is left as it is."""
//| ...
static mp_obj_t audioladder_ladder_modulate(size_t n_args,
const mp_obj_t *args, mp_map_t *kw_args) {
audioladder_ladder_obj_t *self = MP_OBJ_TO_PTR(args[0]);
audiosample_check_for_deinit(&self->base);
(void)n_args;
ladder_apply_inputs(self, kw_args, true);
return mp_const_none;
}
MP_DEFINE_CONST_FUN_OBJ_KW(audioladder_ladder_modulate_obj, 1,
audioladder_ladder_modulate);

//| def clear(self) -> None:
//| """Empty the integrators and the solver's history, which also
//| stops a self-oscillation."""
Expand All @@ -226,6 +301,9 @@ static mp_obj_t audioladder_ladder_deinit(mp_obj_t self_in) {
self->source = mp_const_none;
self->pending = NULL;
self->pending_frames = 0;
self->cutoff = MP_OBJ_NULL;
self->cutoff_pending = NULL;
self->cutoff_pending_frames = 0;
return mp_const_none;
}
MP_DEFINE_CONST_FUN_OBJ_1(audioladder_ladder_deinit_obj, audioladder_ladder_deinit);
Expand All @@ -236,6 +314,8 @@ static const mp_rom_map_elem_t audioladder_ladder_locals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&default___exit___obj) },
{ MP_ROM_QSTR(MP_QSTR_play), MP_ROM_PTR(&audioladder_ladder_play_obj) },
{ MP_ROM_QSTR(MP_QSTR_set), MP_ROM_PTR(&audioladder_ladder_set_obj) },
{ MP_ROM_QSTR(MP_QSTR_modulate),
MP_ROM_PTR(&audioladder_ladder_modulate_obj) },
{ MP_ROM_QSTR(MP_QSTR_clear), MP_ROM_PTR(&audioladder_ladder_clear_obj) },
AUDIOSAMPLE_FIELDS,
};
Expand Down
Loading
Loading