From b01146825175c5738de17fb8387124150eaba768 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Sun, 2 Apr 2023 20:37:23 -0500 Subject: [PATCH] Add synthio.Synthesizer In contrast to MidiTrack, this can be controlled from Python code, turning notes on/off as desired. Not tested on real HW yet, just the acceptance test based on checking which notes it thinks are held internally. --- .../unix/variants/coverage/mpconfigvariant.mk | 2 + py/circuitpy_defns.mk | 1 + shared-bindings/synthio/MidiTrack.c | 13 +- shared-bindings/synthio/Synthesizer.c | 211 ++++++++++++++++++ shared-bindings/synthio/Synthesizer.h | 45 ++++ shared-bindings/synthio/__init__.c | 16 +- shared-module/synthio/MidiTrack.c | 16 +- shared-module/synthio/Synthesizer.c | 104 +++++++++ shared-module/synthio/Synthesizer.h | 53 +++++ shared-module/synthio/__init__.c | 70 +++++- shared-module/synthio/__init__.h | 4 + tests/circuitpython/synthesizer.py | 24 ++ tests/circuitpython/synthesizer.py.exp | 8 + 13 files changed, 528 insertions(+), 39 deletions(-) create mode 100644 shared-bindings/synthio/Synthesizer.c create mode 100644 shared-bindings/synthio/Synthesizer.h create mode 100644 shared-module/synthio/Synthesizer.c create mode 100644 shared-module/synthio/Synthesizer.h create mode 100644 tests/circuitpython/synthesizer.py create mode 100644 tests/circuitpython/synthesizer.py.exp diff --git a/ports/unix/variants/coverage/mpconfigvariant.mk b/ports/unix/variants/coverage/mpconfigvariant.mk index fca9a99518..8b25ddb705 100644 --- a/ports/unix/variants/coverage/mpconfigvariant.mk +++ b/ports/unix/variants/coverage/mpconfigvariant.mk @@ -39,6 +39,7 @@ SRC_BITMAP := \ shared-bindings/struct/__init__.c \ shared-bindings/synthio/__init__.c \ shared-bindings/synthio/MidiTrack.c \ + shared-bindings/synthio/Synthesizer.c \ shared-bindings/traceback/__init__.c \ shared-bindings/util.c \ shared-bindings/zlib/__init__.c \ @@ -57,6 +58,7 @@ SRC_BITMAP := \ shared-module/struct/__init__.c \ shared-module/synthio/__init__.c \ shared-module/synthio/MidiTrack.c \ + shared-module/synthio/Synthesizer.c \ shared-module/traceback/__init__.c \ shared-module/zlib/__init__.c \ diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk index 94377238a0..fb5d745ebb 100644 --- a/py/circuitpy_defns.mk +++ b/py/circuitpy_defns.mk @@ -648,6 +648,7 @@ SRC_SHARED_MODULE_ALL = \ supervisor/__init__.c \ supervisor/StatusBar.c \ synthio/MidiTrack.c \ + synthio/Synthesizer.c \ synthio/__init__.c \ terminalio/Terminal.c \ terminalio/__init__.c \ diff --git a/shared-bindings/synthio/MidiTrack.c b/shared-bindings/synthio/MidiTrack.c index 727b9ba335..20a203ce9c 100644 --- a/shared-bindings/synthio/MidiTrack.c +++ b/shared-bindings/synthio/MidiTrack.c @@ -85,17 +85,8 @@ STATIC mp_obj_t synthio_miditrack_make_new(const mp_obj_type_t *type, size_t n_a mp_buffer_info_t bufinfo; mp_get_buffer_raise(args[ARG_buffer].u_obj, &bufinfo, MP_BUFFER_READ); - mp_buffer_info_t bufinfo_waveform = { - .buf = shared_bindings_synthio_square_wave, - .len = 4 - }; - - if (args[ARG_waveform].u_obj != mp_const_none) { - mp_get_buffer_raise(args[ARG_waveform].u_obj, &bufinfo_waveform, MP_BUFFER_READ); - if (bufinfo_waveform.typecode != 'h') { - mp_raise_ValueError_varg(translate("%q must be array of type 'h'"), MP_QSTR_waveform); - } - } + mp_buffer_info_t bufinfo_waveform; + synthio_synth_parse_waveform(&bufinfo_waveform, args[ARG_waveform].u_obj); synthio_miditrack_obj_t *self = m_new_obj(synthio_miditrack_obj_t); self->base.type = &synthio_miditrack_type; diff --git a/shared-bindings/synthio/Synthesizer.c b/shared-bindings/synthio/Synthesizer.c new file mode 100644 index 0000000000..2ad5cbcd42 --- /dev/null +++ b/shared-bindings/synthio/Synthesizer.c @@ -0,0 +1,211 @@ +/* + * This file is part of the Micro Python project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2021 Artyom Skrobov + * Copyright (c) 2023 Jeff Epler for Adafruit Industries + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include + +#include "shared/runtime/context_manager_helpers.h" +#include "py/binary.h" +#include "py/objproperty.h" +#include "py/runtime.h" +#include "shared-bindings/util.h" +#include "shared-bindings/synthio/Synthesizer.h" +#include "shared-bindings/synthio/__init__.h" +#include "supervisor/shared/translate/translate.h" + +//| class Synth: +//| def __init__(self, *, sample_rate: int = 11025, waveform: ReadableBuffer = None) -> None: +//| """Create a synthesizer object.""" +STATIC mp_obj_t synthio_synthesizer_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + enum { ARG_sample_rate, ARG_waveform }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_sample_rate, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 11025} }, + { MP_QSTR_waveform, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none } }, + }; + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + mp_buffer_info_t bufinfo_waveform; + synthio_synth_parse_waveform(&bufinfo_waveform, args[ARG_waveform].u_obj); + + synthio_synthesizer_obj_t *self = m_new_obj(synthio_synthesizer_obj_t); + self->base.type = &synthio_synthesizer_type; + + common_hal_synthio_synthesizer_construct(self, + args[ARG_sample_rate].u_int, + bufinfo_waveform.buf, + bufinfo_waveform.len / 2); + + return MP_OBJ_FROM_PTR(self); +} + +STATIC void check_for_deinit(synthio_synthesizer_obj_t *self) { + if (common_hal_synthio_synthesizer_deinited(self)) { + raise_deinited_error(); + } +} + +//| def press(self, /, press: Sequence[int] = ()) -> None: +//| """Turn some notes on. Notes use MIDI numbering, with 60 being middle C.""" +STATIC mp_obj_t synthio_synthesizer_press(mp_obj_t self_in, mp_obj_t press) { + synthio_synthesizer_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + common_hal_synthio_synthesizer_press(self, press); + return mp_const_none; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_2(synthio_synthesizer_press_obj, synthio_synthesizer_press); +// +//| def release_then_press( +//| self, release: Sequence[int] = (), press: Sequence[int] = () +//| ) -> None: +//| """Turn some notes on and/or off. Notes use MIDI numbering, with 60 being middle C.""" +STATIC mp_obj_t synthio_synthesizer_release_then_press(mp_uint_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_release, ARG_press }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_release, MP_ARG_OBJ, {.u_obj = mp_const_empty_tuple } }, + { MP_QSTR_press, MP_ARG_OBJ, {.u_obj = mp_const_empty_tuple } }, + }; + + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + synthio_synthesizer_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + check_for_deinit(self); + common_hal_synthio_synthesizer_release(self, args[ARG_release].u_obj); + common_hal_synthio_synthesizer_press(self, args[ARG_press].u_obj); + return mp_const_none; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_KW(synthio_synthesizer_release_then_press_obj, 1, synthio_synthesizer_release_then_press); + +// +//| def release_all_then_press(self, /, press: Sequence[int]) -> None: +//| """Turn any currently-playing notes off, then turn on the given notes""" +STATIC mp_obj_t synthio_synthesizer_release_all_then_press(mp_obj_t self_in, mp_obj_t press) { + synthio_synthesizer_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + common_hal_synthio_synthesizer_release_all(self); + common_hal_synthio_synthesizer_press(self, press); + return mp_const_none; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_2(synthio_synthesizer_release_all_then_press_obj, synthio_synthesizer_release_all_then_press); + +// +//| def release_all(self) -> None: +//| """Turn any currently-playing notes off""" +STATIC mp_obj_t synthio_synthesizer_release_all(mp_obj_t self_in) { + synthio_synthesizer_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + common_hal_synthio_synthesizer_release_all(self); + return mp_const_none; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(synthio_synthesizer_release_all_obj, synthio_synthesizer_release_all); + +//| def deinit(self) -> None: +//| """Deinitialises the object and releases any memory resources for reuse.""" +//| ... +STATIC mp_obj_t synthio_synthesizer_deinit(mp_obj_t self_in) { + synthio_synthesizer_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_synthio_synthesizer_deinit(self); + return mp_const_none; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(synthio_synthesizer_deinit_obj, synthio_synthesizer_deinit); + +//| def __enter__(self) -> MidiTrack: +//| """No-op used by Context Managers.""" +//| ... +// Provided by context manager helper. +//| +//| def __exit__(self) -> None: +//| """Automatically deinitializes the hardware when exiting a context. See +//| :ref:`lifetime-and-contextmanagers` for more info.""" +//| ... +STATIC mp_obj_t synthio_synthesizer_obj___exit__(size_t n_args, const mp_obj_t *args) { + (void)n_args; + common_hal_synthio_synthesizer_deinit(args[0]); + return mp_const_none; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(synthio_synthesizer___exit___obj, 4, 4, synthio_synthesizer_obj___exit__); +//| sample_rate: int +//| """32 bit value that tells how quickly samples are played in Hertz (cycles per second).""" +STATIC mp_obj_t synthio_synthesizer_obj_get_sample_rate(mp_obj_t self_in) { + synthio_synthesizer_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return MP_OBJ_NEW_SMALL_INT(common_hal_synthio_synthesizer_get_sample_rate(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(synthio_synthesizer_get_sample_rate_obj, synthio_synthesizer_obj_get_sample_rate); + +MP_PROPERTY_GETTER(synthio_synthesizer_sample_rate_obj, + (mp_obj_t)&synthio_synthesizer_get_sample_rate_obj); + +//| pressed: Tuple[int] +//| """A sequence of the currently pressed notes (read-only property)""" +//| +STATIC mp_obj_t synthio_synthesizer_obj_get_pressed(mp_obj_t self_in) { + synthio_synthesizer_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return common_hal_synthio_synthesizer_get_pressed_notes(self); +} +MP_DEFINE_CONST_FUN_OBJ_1(synthio_synthesizer_get_pressed_obj, synthio_synthesizer_obj_get_pressed); + +MP_PROPERTY_GETTER(synthio_synthesizer_pressed_obj, + (mp_obj_t)&synthio_synthesizer_get_pressed_obj); + +STATIC const mp_rom_map_elem_t synthio_synthesizer_locals_dict_table[] = { + // Methods + { MP_ROM_QSTR(MP_QSTR_press), MP_ROM_PTR(&synthio_synthesizer_press_obj) }, + { MP_ROM_QSTR(MP_QSTR_release_all), MP_ROM_PTR(&synthio_synthesizer_release_all_obj) }, + { MP_ROM_QSTR(MP_QSTR_release_then_press), MP_ROM_PTR(&synthio_synthesizer_release_then_press_obj) }, + { MP_ROM_QSTR(MP_QSTR_release_all_then_press), MP_ROM_PTR(&synthio_synthesizer_release_all_then_press_obj) }, + { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&synthio_synthesizer_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&default___enter___obj) }, + { MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&synthio_synthesizer___exit___obj) }, + + // Properties + { MP_ROM_QSTR(MP_QSTR_sample_rate), MP_ROM_PTR(&synthio_synthesizer_sample_rate_obj) }, + { MP_ROM_QSTR(MP_QSTR_pressed), MP_ROM_PTR(&synthio_synthesizer_pressed_obj) }, +}; +STATIC MP_DEFINE_CONST_DICT(synthio_synthesizer_locals_dict, synthio_synthesizer_locals_dict_table); + +STATIC const audiosample_p_t synthio_synthesizer_proto = { + MP_PROTO_IMPLEMENT(MP_QSTR_protocol_audiosample) + .sample_rate = (audiosample_sample_rate_fun)common_hal_synthio_synthesizer_get_sample_rate, + .bits_per_sample = (audiosample_bits_per_sample_fun)common_hal_synthio_synthesizer_get_bits_per_sample, + .channel_count = (audiosample_channel_count_fun)common_hal_synthio_synthesizer_get_channel_count, + .reset_buffer = (audiosample_reset_buffer_fun)synthio_synthesizer_reset_buffer, + .get_buffer = (audiosample_get_buffer_fun)synthio_synthesizer_get_buffer, + .get_buffer_structure = (audiosample_get_buffer_structure_fun)synthio_synthesizer_get_buffer_structure, +}; + +const mp_obj_type_t synthio_synthesizer_type = { + { &mp_type_type }, + .name = MP_QSTR_Synthesizer, + .flags = MP_TYPE_FLAG_EXTENDED, + .make_new = synthio_synthesizer_make_new, + .locals_dict = (mp_obj_dict_t *)&synthio_synthesizer_locals_dict, + MP_TYPE_EXTENDED_FIELDS( + .protocol = &synthio_synthesizer_proto, + ), +}; diff --git a/shared-bindings/synthio/Synthesizer.h b/shared-bindings/synthio/Synthesizer.h new file mode 100644 index 0000000000..92e75a1c23 --- /dev/null +++ b/shared-bindings/synthio/Synthesizer.h @@ -0,0 +1,45 @@ +/* + * This file is part of the Micro Python project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2021 Artyom Skrobov + * Copyright (c) 2023 Jeff Epler for Adafruit Industries + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#pragma once + +#include "shared-module/synthio/Synthesizer.h" + +extern const mp_obj_type_t synthio_synthesizer_type; + +void common_hal_synthio_synthesizer_construct(synthio_synthesizer_obj_t *self, + uint32_t sample_rate, const int16_t *waveform, uint16_t waveform_len); + +void common_hal_synthio_synthesizer_deinit(synthio_synthesizer_obj_t *self); +bool common_hal_synthio_synthesizer_deinited(synthio_synthesizer_obj_t *self); +uint32_t common_hal_synthio_synthesizer_get_sample_rate(synthio_synthesizer_obj_t *self); +uint8_t common_hal_synthio_synthesizer_get_bits_per_sample(synthio_synthesizer_obj_t *self); +uint8_t common_hal_synthio_synthesizer_get_channel_count(synthio_synthesizer_obj_t *self); +void common_hal_synthio_synthesizer_release(synthio_synthesizer_obj_t *self, mp_obj_t to_release); +void common_hal_synthio_synthesizer_press(synthio_synthesizer_obj_t *self, mp_obj_t to_press); +void common_hal_synthio_synthesizer_release_all(synthio_synthesizer_obj_t *self); +mp_obj_t common_hal_synthio_synthesizer_get_pressed_notes(synthio_synthesizer_obj_t *self); diff --git a/shared-bindings/synthio/__init__.c b/shared-bindings/synthio/__init__.c index dc627adb36..3c50377ff8 100644 --- a/shared-bindings/synthio/__init__.c +++ b/shared-bindings/synthio/__init__.c @@ -34,8 +34,7 @@ #include "shared-bindings/synthio/__init__.h" #include "shared-bindings/synthio/MidiTrack.h" - -int16_t shared_bindings_synthio_square_wave[] = {-32768, 32767}; +#include "shared-bindings/synthio/Synthesizer.h" //| """Support for MIDI synthesis""" //| @@ -79,17 +78,9 @@ STATIC mp_obj_t synthio_from_file(size_t n_args, const mp_obj_t *pos_args, mp_ma } pyb_file_obj_t *file = MP_OBJ_TO_PTR(args[ARG_file].u_obj); - mp_buffer_info_t bufinfo_waveform = { - .buf = shared_bindings_synthio_square_wave, - .len = 4 - }; - if (args[ARG_waveform].u_obj != mp_const_none) { - mp_get_buffer_raise(args[ARG_waveform].u_obj, &bufinfo_waveform, MP_BUFFER_READ); - if (bufinfo_waveform.typecode != 'h') { - mp_raise_ValueError_varg(translate("%q must be array of type 'h'"), MP_QSTR_waveform); - } - } + mp_buffer_info_t bufinfo_waveform; + synthio_synth_parse_waveform(&bufinfo_waveform, args[ARG_waveform].u_obj); uint8_t chunk_header[14]; f_rewind(&file->fp); @@ -146,6 +137,7 @@ MP_DEFINE_CONST_FUN_OBJ_KW(synthio_from_file_obj, 1, synthio_from_file); STATIC const mp_rom_map_elem_t synthio_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_synthio) }, { MP_ROM_QSTR(MP_QSTR_MidiTrack), MP_ROM_PTR(&synthio_miditrack_type) }, + { MP_ROM_QSTR(MP_QSTR_Synthesizer), MP_ROM_PTR(&synthio_synthesizer_type) }, { MP_ROM_QSTR(MP_QSTR_from_file), MP_ROM_PTR(&synthio_from_file_obj) }, }; diff --git a/shared-module/synthio/MidiTrack.c b/shared-module/synthio/MidiTrack.c index 9cc224d96b..0d91ee6982 100644 --- a/shared-module/synthio/MidiTrack.c +++ b/shared-module/synthio/MidiTrack.c @@ -57,21 +57,10 @@ STATIC void add_span(synthio_miditrack_obj_t *self, const synthio_midi_span_t *s self->track[self->total_spans++] = *span; } -STATIC int find_channel_with_note(const synthio_midi_span_t *span, uint8_t note) { - for (int i = 0; i < CIRCUITPY_SYNTHIO_MAX_CHANNELS; i++) { - if (span->note[i] == note) { - return i; - } - } - return -1; -} - STATIC void change_span_note(synthio_miditrack_obj_t *self, uint8_t old_note, uint8_t new_note, uint16_t *dur) { synthio_midi_span_t span = self->track[self->total_spans - 1]; - int channel = find_channel_with_note(&span, old_note); - if (channel != -1) { + if (synthio_span_change_note(&span, old_note, new_note)) { terminate_span(self, dur); - span.note[channel] = new_note; add_span(self, &span); *dur = 0; } @@ -83,12 +72,11 @@ void common_hal_synthio_miditrack_construct(synthio_miditrack_obj_t *self, self->synth.sample_rate = sample_rate; self->track = m_malloc(sizeof(synthio_midi_span_t), false); - *self->track = ((synthio_midi_span_t) { 0, {[0 ... (CIRCUITPY_SYNTHIO_MAX_CHANNELS - 1)] = SYNTHIO_SILENCE} }); + synthio_span_init(self->track); self->next_span = 0; self->total_spans = 1; self->synth.waveform = waveform; self->synth.waveform_length = waveform_length; - mp_arg_validate_length_range(waveform_length, 2, 1024, MP_QSTR_waveform); uint16_t dur = 0; uint32_t pos = 0; diff --git a/shared-module/synthio/Synthesizer.c b/shared-module/synthio/Synthesizer.c new file mode 100644 index 0000000000..8cfc73a980 --- /dev/null +++ b/shared-module/synthio/Synthesizer.c @@ -0,0 +1,104 @@ +/* + * This file is part of the Micro Python project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2021 Artyom Skrobov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "py/runtime.h" +#include "shared-bindings/synthio/Synthesizer.h" + + + +void common_hal_synthio_synthesizer_construct(synthio_synthesizer_obj_t *self, + uint32_t sample_rate, const int16_t *waveform, uint16_t waveform_length) { + + self->synth.sample_rate = sample_rate; + self->synth.waveform = waveform; + self->synth.waveform_length = waveform_length; + synthio_synth_init(&self->synth, SYNTHIO_MAX_DUR); + common_hal_synthio_synthesizer_release_all(self); +} + +void common_hal_synthio_synthesizer_deinit(synthio_synthesizer_obj_t *self) { + synthio_synth_deinit(&self->synth); +} +bool common_hal_synthio_synthesizer_deinited(synthio_synthesizer_obj_t *self) { + return synthio_synth_deinited(&self->synth); +} + +uint32_t common_hal_synthio_synthesizer_get_sample_rate(synthio_synthesizer_obj_t *self) { + return self->synth.sample_rate; +} +uint8_t common_hal_synthio_synthesizer_get_bits_per_sample(synthio_synthesizer_obj_t *self) { + return SYNTHIO_BITS_PER_SAMPLE; +} +uint8_t common_hal_synthio_synthesizer_get_channel_count(synthio_synthesizer_obj_t *self) { + return 1; +} + +void synthio_synthesizer_reset_buffer(synthio_synthesizer_obj_t *self, + bool single_channel_output, uint8_t channel) { +} + +audioio_get_buffer_result_t synthio_synthesizer_get_buffer(synthio_synthesizer_obj_t *self, + bool single_channel_output, uint8_t channel, uint8_t **buffer, uint32_t *buffer_length) { + self->synth.span.dur = SYNTHIO_MAX_DUR; + synthio_synth_synthesize(&self->synth, buffer, buffer_length); + return GET_BUFFER_MORE_DATA; +} + +void synthio_synthesizer_get_buffer_structure(synthio_synthesizer_obj_t *self, bool single_channel_output, + bool *single_buffer, bool *samples_signed, uint32_t *max_buffer_length, uint8_t *spacing) { + return synthio_synth_get_buffer_structure(&self->synth, single_channel_output, single_buffer, samples_signed, max_buffer_length, spacing); +} + +void common_hal_synthio_synthesizer_release_all(synthio_synthesizer_obj_t *self) { + synthio_span_init(&self->synth.span); +} +void common_hal_synthio_synthesizer_release(synthio_synthesizer_obj_t *self, mp_obj_t to_release) { + mp_obj_iter_buf_t iter_buf; + mp_obj_t iterable = mp_getiter(to_release, &iter_buf); + mp_obj_t item; + while ((item = mp_iternext(iterable)) != MP_OBJ_STOP_ITERATION) { + synthio_span_change_note(&self->synth.span, mp_arg_validate_int_range(mp_obj_get_int(item), 0, 127, MP_QSTR_note), SYNTHIO_SILENCE); + } +} + +void common_hal_synthio_synthesizer_press(synthio_synthesizer_obj_t *self, mp_obj_t to_press) { + mp_obj_iter_buf_t iter_buf; + mp_obj_t iterable = mp_getiter(to_press, &iter_buf); + mp_obj_t item; + while ((item = mp_iternext(iterable)) != MP_OBJ_STOP_ITERATION) { + synthio_span_change_note(&self->synth.span, SYNTHIO_SILENCE, mp_arg_validate_int_range(mp_obj_get_int(item), 0, 127, MP_QSTR_note)); + } +} + +mp_obj_t common_hal_synthio_synthesizer_get_pressed_notes(synthio_synthesizer_obj_t *self) { + mp_obj_tuple_t *result = MP_OBJ_TO_PTR(mp_obj_new_tuple(synthio_span_count_active_channels(&self->synth.span), NULL)); + for (size_t i = 0, j = 0; i < CIRCUITPY_SYNTHIO_MAX_CHANNELS; i++) { + if (self->synth.span.note[i] != SYNTHIO_SILENCE) { + result->items[j++] = MP_OBJ_NEW_SMALL_INT(self->synth.span.note[i]); + } + } + return MP_OBJ_FROM_PTR(result); +} diff --git a/shared-module/synthio/Synthesizer.h b/shared-module/synthio/Synthesizer.h new file mode 100644 index 0000000000..256b6137f2 --- /dev/null +++ b/shared-module/synthio/Synthesizer.h @@ -0,0 +1,53 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2021 Artyom Skrobov + * Copyright (c) 2023 Jeff Epler for Adafruit Industries + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#pragma once + +#include "py/obj.h" + +#include "shared-module/synthio/__init__.h" + +typedef struct { + mp_obj_base_t base; + synthio_synth_t synth; +} synthio_synthesizer_obj_t; + + +// These are not available from Python because it may be called in an interrupt. +void synthio_synthesizer_reset_buffer(synthio_synthesizer_obj_t *self, + bool single_channel_output, + uint8_t channel); + +audioio_get_buffer_result_t synthio_synthesizer_get_buffer(synthio_synthesizer_obj_t *self, + bool single_channel_output, + uint8_t channel, + uint8_t **buffer, + uint32_t *buffer_length); // length in bytes + +void synthio_synthesizer_get_buffer_structure(synthio_synthesizer_obj_t *self, bool single_channel_output, + bool *single_buffer, bool *samples_signed, + uint32_t *max_buffer_length, uint8_t *spacing); diff --git a/shared-module/synthio/__init__.c b/shared-module/synthio/__init__.c index 8c58d5b2da..0dee75deb7 100644 --- a/shared-module/synthio/__init__.c +++ b/shared-module/synthio/__init__.c @@ -1,9 +1,39 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2021 Artyom Skrobov + * Copyright (c) 2023 Jeff Epler for Adafruit Industries + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + #include "shared-module/synthio/__init__.h" +#include "py/runtime.h" + +STATIC const int16_t square_wave[] = {-32768, 32767}; STATIC const uint16_t notes[] = {8372, 8870, 9397, 9956, 10548, 11175, 11840, 12544, 13290, 14080, 14917, 15804}; // 9th octave -STATIC int count_active_channels(synthio_midi_span_t *span) { +int synthio_span_count_active_channels(synthio_midi_span_t *span) { int result = 0; for (int i = 0; i < CIRCUITPY_SYNTHIO_MAX_CHANNELS; i++) { if (span->note[i] != SYNTHIO_SILENCE) { @@ -20,7 +50,7 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **buffer, uint32_t memset(synth->buffer, 0, synth->buffer_length); int32_t sample_rate = synth->sample_rate; - int active_channels = count_active_channels(&synth->span); + int active_channels = synthio_span_count_active_channels(&synth->span); const int16_t *waveform = synth->waveform; uint32_t waveform_length = synth->waveform_length; int16_t *out_buffer = synth->buffer; @@ -79,3 +109,39 @@ void synthio_synth_get_buffer_structure(synthio_synth_t *synth, bool single_chan *max_buffer_length = synth->buffer_length; *spacing = 1; } + +void synthio_synth_parse_waveform(mp_buffer_info_t *bufinfo_waveform, mp_obj_t waveform_obj) { + *bufinfo_waveform = ((mp_buffer_info_t) { .buf = (void *)square_wave, .len = 4 }); + + if (waveform_obj != mp_const_none) { + mp_get_buffer_raise(waveform_obj, bufinfo_waveform, MP_BUFFER_READ); + if (bufinfo_waveform->typecode != 'h') { + mp_raise_ValueError_varg(translate("%q must be array of type 'h'"), MP_QSTR_waveform); + } + } + mp_arg_validate_length_range(bufinfo_waveform->len / 2, 2, 1024, MP_QSTR_waveform); +} + +void synthio_span_init(synthio_midi_span_t *span) { + span->dur = 0; + for (size_t i = 0; i < CIRCUITPY_SYNTHIO_MAX_CHANNELS; i++) { span->note[i] = SYNTHIO_SILENCE; + } +} + +STATIC int find_channel_with_note(const synthio_midi_span_t *span, uint8_t note) { + for (int i = 0; i < CIRCUITPY_SYNTHIO_MAX_CHANNELS; i++) { + if (span->note[i] == note) { + return i; + } + } + return -1; +} + +bool synthio_span_change_note(synthio_midi_span_t *span, uint8_t old_note, uint8_t new_note) { + int channel = find_channel_with_note(span, old_note); + if (channel != -1) { + span->note[channel] = new_note; + return true; + } + return false; +} diff --git a/shared-module/synthio/__init__.h b/shared-module/synthio/__init__.h index 88be7e689e..9397495f98 100644 --- a/shared-module/synthio/__init__.h +++ b/shared-module/synthio/__init__.h @@ -48,9 +48,13 @@ typedef struct { uint32_t accum[CIRCUITPY_SYNTHIO_MAX_CHANNELS]; } synthio_synth_t; +void synthio_span_init(synthio_midi_span_t *span); void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **buffer, uint32_t *buffer_length); void synthio_synth_deinit(synthio_synth_t *synth); bool synthio_synth_deinited(synthio_synth_t *synth); void synthio_synth_init(synthio_synth_t *synth, uint16_t max_dur); void synthio_synth_get_buffer_structure(synthio_synth_t *synth, bool single_channel_output, bool *single_buffer, bool *samples_signed, uint32_t *max_buffer_length, uint8_t *spacing); +void synthio_synth_parse_waveform(mp_buffer_info_t *bufinfo_waveform, mp_obj_t waveform_obj); +bool synthio_span_change_note(synthio_midi_span_t *span, uint8_t old_note, uint8_t new_note); +int synthio_span_count_active_channels(synthio_midi_span_t *span); diff --git a/tests/circuitpython/synthesizer.py b/tests/circuitpython/synthesizer.py new file mode 100644 index 0000000000..bdb273f861 --- /dev/null +++ b/tests/circuitpython/synthesizer.py @@ -0,0 +1,24 @@ +import struct +import synthio +import audiocore + + +def dump_samples(): + print(struct.unpack("12h", audiocore.get_buffer(s)[1][:24])) + + +s = synthio.Synthesizer(sample_rate=8000) +print(s.pressed) +dump_samples() + +s.press((80,)) +print(s.pressed) +dump_samples() + +s.press((91,)) +print(s.pressed) +dump_samples() + +s.release_then_press((80,)) +print(s.pressed) +dump_samples() diff --git a/tests/circuitpython/synthesizer.py.exp b/tests/circuitpython/synthesizer.py.exp new file mode 100644 index 0000000000..aa2567641c --- /dev/null +++ b/tests/circuitpython/synthesizer.py.exp @@ -0,0 +1,8 @@ +() +(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) +(80,) +(-4095, -4095, -4095, -4095, 4095, 4095, 4095, 4095, 4095, -4095, -4095, -4095) +(80, 91) +(-5460, -5460, 0, 5460, 5460, 0, 0, 5460, 0, 0, -5460, -5460) +(91,) +(4095, 4095, 4095, -4095, -4095, 4095, 4095, 4095, -4095, -4095, 4095, 4095)