From 5514e3065da2cdd4e81670d9f6691269eeff6392 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Tue, 25 Apr 2023 16:42:05 -0500 Subject: [PATCH 01/19] synthio: add midi_to_hz to convert notes in the MIDI 1-127 note scale to floating point Hz --- shared-bindings/synthio/__init__.c | 7 +++++++ shared-bindings/synthio/__init__.h | 1 + shared-module/synthio/__init__.c | 6 ++++++ 3 files changed, 14 insertions(+) diff --git a/shared-bindings/synthio/__init__.c b/shared-bindings/synthio/__init__.c index eaf67cc0e7..89929b0563 100644 --- a/shared-bindings/synthio/__init__.c +++ b/shared-bindings/synthio/__init__.c @@ -262,12 +262,19 @@ STATIC mp_obj_t synthio_from_file(size_t n_args, const mp_obj_t *pos_args, mp_ma } MP_DEFINE_CONST_FUN_OBJ_KW(synthio_from_file_obj, 1, synthio_from_file); +STATIC mp_obj_t midi_to_hz(mp_obj_t arg) { + mp_int_t note = mp_arg_validate_int_range(mp_obj_get_int(arg), 1, 127, MP_QSTR_note); + return mp_obj_new_float(common_hal_synthio_midi_to_hz_float(note)); +} +MP_DEFINE_CONST_FUN_OBJ_1(synthio_midi_to_hz_obj, midi_to_hz); + 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) }, { MP_ROM_QSTR(MP_QSTR_Envelope), MP_ROM_PTR(&synthio_envelope_type_obj) }, + { MP_ROM_QSTR(MP_QSTR_midi_to_hz), MP_ROM_PTR(&synthio_midi_to_hz_obj) }, }; STATIC MP_DEFINE_CONST_DICT(synthio_module_globals, synthio_module_globals_table); diff --git a/shared-bindings/synthio/__init__.h b/shared-bindings/synthio/__init__.h index 6ea323905a..d3ccc1ce04 100644 --- a/shared-bindings/synthio/__init__.h +++ b/shared-bindings/synthio/__init__.h @@ -33,3 +33,4 @@ extern int16_t shared_bindings_synthio_square_wave[]; extern const mp_obj_namedtuple_type_t synthio_envelope_type_obj; void synthio_synth_envelope_set(synthio_synth_t *synth, mp_obj_t envelope_obj); mp_obj_t synthio_synth_envelope_get(synthio_synth_t *synth); +mp_float_t common_hal_synthio_midi_to_hz_float(mp_int_t note); diff --git a/shared-module/synthio/__init__.c b/shared-module/synthio/__init__.c index c7e1096632..8c17dfdeed 100644 --- a/shared-module/synthio/__init__.c +++ b/shared-module/synthio/__init__.c @@ -36,6 +36,12 @@ 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 +mp_float_t common_hal_synthio_midi_to_hz_float(mp_int_t arg) { + uint8_t octave = arg / 12; + uint16_t base_freq = notes[arg % 12]; + return MICROPY_FLOAT_C_FUN(ldexp)(base_freq, octave - 10); +} + STATIC int16_t convert_time_to_rate(uint32_t sample_rate, mp_obj_t time_in, int16_t difference) { mp_float_t time = mp_obj_get_float(time_in); int num_samples = (int)MICROPY_FLOAT_C_FUN(round)(time * sample_rate); From c22fd2a18e4732b2b7dab6e58e924615adeffb12 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Mon, 1 May 2023 09:39:40 -0500 Subject: [PATCH 02/19] runtime: Add mp_arg_validate_float_range --- py/argcheck.c | 7 ++++++- py/runtime.h | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/py/argcheck.c b/py/argcheck.c index 265b303901..8a4419f88f 100644 --- a/py/argcheck.c +++ b/py/argcheck.c @@ -194,11 +194,16 @@ mp_float_t mp_arg_validate_type_float(mp_obj_t obj, qstr arg_name) { return a_float; } -void mp_arg_validate_obj_float_range(mp_obj_t float_in, mp_int_t min, mp_int_t max, qstr arg_name) { +mp_float_t mp_arg_validate_obj_float_range(mp_obj_t float_in, mp_int_t min, mp_int_t max, qstr arg_name) { const mp_float_t f = mp_arg_validate_type_float(float_in, arg_name); + return mp_arg_validate_float_range(f, min, max, arg_name); +} + +mp_float_t mp_arg_validate_float_range(mp_float_t f, mp_int_t min, mp_int_t max, qstr arg_name) { if (f < (mp_float_t)min || f > (mp_float_t)max) { mp_raise_ValueError_varg(translate("%q must be %d-%d"), arg_name, min, max); } + return f; } mp_float_t mp_arg_validate_obj_float_non_negative(mp_obj_t float_in, mp_float_t default_for_null, qstr arg_name) { diff --git a/py/runtime.h b/py/runtime.h index f7b96a27ae..196874bff9 100644 --- a/py/runtime.h +++ b/py/runtime.h @@ -103,7 +103,8 @@ mp_int_t mp_arg_validate_int_max(mp_int_t i, mp_int_t j, qstr arg_name); mp_int_t mp_arg_validate_int_range(mp_int_t i, mp_int_t min, mp_int_t max, qstr arg_name); #if MICROPY_PY_BUILTINS_FLOAT mp_float_t mp_arg_validate_obj_float_non_negative(mp_obj_t float_in, mp_float_t default_for_null, qstr arg_name); -void mp_arg_validate_obj_float_range(mp_obj_t float_in, mp_int_t min, mp_int_t max, qstr arg_name); +mp_float_t mp_arg_validate_obj_float_range(mp_obj_t float_in, mp_int_t min, mp_int_t max, qstr arg_name); +mp_float_t mp_arg_validate_float_range(mp_float_t float_in, mp_int_t min, mp_int_t max, qstr arg_name); mp_float_t mp_arg_validate_type_float(mp_obj_t obj, qstr arg_name); #endif mp_uint_t mp_arg_validate_length_min(mp_uint_t length, mp_uint_t min, qstr arg_name); From bc03e03b9e62048de4d6d2b12e75301eda4b1652 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Wed, 3 May 2023 09:11:17 -0500 Subject: [PATCH 03/19] util: Add properties_print_helper --- shared-bindings/util.c | 12 ++++++++++++ shared-bindings/util.h | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/shared-bindings/util.c b/shared-bindings/util.c index 5c5eafad4a..78960a7552 100644 --- a/shared-bindings/util.c +++ b/shared-bindings/util.c @@ -37,5 +37,17 @@ void raise_deinited_error(void) { mp_raise_ValueError(translate("Object has been deinitialized and can no longer be used. Create a new object.")); } +void properties_print_helper(const mp_print_t *print, mp_obj_t self_in, const mp_arg_t *properties, size_t n_properties) { + const mp_obj_type_t *type = mp_obj_get_type(self_in); + mp_printf(print, "%q(", type->name); + for (size_t i = 0; i < n_properties; i++) { + if (i > 0) { + mp_print_str(print, ", "); + } + mp_printf(print, "%q=", properties[i].qst); + mp_obj_print_helper(print, mp_load_attr(self_in, properties[i].qst), PRINT_REPR); + } + mp_print_str(print, ")"); +} #endif // MICROPY_INCLUDED_SHARED_BINDINGS_UTIL_H diff --git a/shared-bindings/util.h b/shared-bindings/util.h index 33454f10ef..b35e903d38 100644 --- a/shared-bindings/util.h +++ b/shared-bindings/util.h @@ -27,7 +27,11 @@ #ifndef MICROPY_INCLUDED_ATMEL_SAMD_COMMON_HAL_UTIL_H #define MICROPY_INCLUDED_ATMEL_SAMD_COMMON_HAL_UTIL_H +#include "py/mpprint.h" +#include "py/runtime.h" + void raise_deinited_error(void); +void properties_print_helper(const mp_print_t *print, mp_obj_t self_in, const mp_arg_t *properties, size_t n_properties); #endif // MICROPY_INCLUDED_ATMEL_SAMD_COMMON_HAL_UTIL_H From a7da245ad062078ab7b7a67111446adc8e9afc5e Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Mon, 1 May 2023 09:41:05 -0500 Subject: [PATCH 04/19] synthio: Add synthio.Note This class allows much more expressive sound synthesis: * tremolo & vibrato * arbitrary frequency * different evelope & waveform per note * all properties dynamically settable from Python code --- .../unix/variants/coverage/mpconfigvariant.mk | 2 + py/circuitpy_defns.mk | 1 + shared-bindings/synthio/Note.c | 252 ++++++++++++++++++ shared-bindings/synthio/Note.h | 30 +++ shared-bindings/synthio/Synthesizer.c | 38 +-- shared-bindings/synthio/__init__.c | 3 +- shared-module/synthio/MidiTrack.c | 8 +- shared-module/synthio/Note.c | 158 +++++++++++ shared-module/synthio/Note.h | 54 ++++ shared-module/synthio/Synthesizer.c | 38 ++- shared-module/synthio/__init__.c | 131 +++++++-- shared-module/synthio/__init__.h | 27 +- .../synthio/note/README.md | 5 + .../synthio/note/audioop.py | 1 + .../synthio/note/chunk.py | 1 + .../circuitpython-manual/synthio/note/code.py | 100 +++++++ .../circuitpython-manual/synthio/note/wave.py | 1 + .../synthio/wave/README.md | 2 +- tests/circuitpython/synthesizer_note.py | 41 +++ tests/circuitpython/synthesizer_note.py.exp | 32 +++ 20 files changed, 863 insertions(+), 62 deletions(-) create mode 100644 shared-bindings/synthio/Note.c create mode 100644 shared-bindings/synthio/Note.h create mode 100644 shared-module/synthio/Note.c create mode 100644 shared-module/synthio/Note.h create mode 100644 tests/circuitpython-manual/synthio/note/README.md create mode 120000 tests/circuitpython-manual/synthio/note/audioop.py create mode 120000 tests/circuitpython-manual/synthio/note/chunk.py create mode 100644 tests/circuitpython-manual/synthio/note/code.py create mode 120000 tests/circuitpython-manual/synthio/note/wave.py create mode 100644 tests/circuitpython/synthesizer_note.py create mode 100644 tests/circuitpython/synthesizer_note.py.exp diff --git a/ports/unix/variants/coverage/mpconfigvariant.mk b/ports/unix/variants/coverage/mpconfigvariant.mk index 854d052c6c..41f44df2b4 100644 --- a/ports/unix/variants/coverage/mpconfigvariant.mk +++ b/ports/unix/variants/coverage/mpconfigvariant.mk @@ -42,6 +42,7 @@ SRC_BITMAP := \ shared-bindings/struct/__init__.c \ shared-bindings/synthio/__init__.c \ shared-bindings/synthio/MidiTrack.c \ + shared-bindings/synthio/Note.c \ shared-bindings/synthio/Synthesizer.c \ shared-bindings/traceback/__init__.c \ shared-bindings/util.c \ @@ -64,6 +65,7 @@ SRC_BITMAP := \ shared-module/struct/__init__.c \ shared-module/synthio/__init__.c \ shared-module/synthio/MidiTrack.c \ + shared-module/synthio/Note.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 d9af56356a..553490d99b 100644 --- a/py/circuitpy_defns.mk +++ b/py/circuitpy_defns.mk @@ -651,6 +651,7 @@ SRC_SHARED_MODULE_ALL = \ supervisor/__init__.c \ supervisor/StatusBar.c \ synthio/MidiTrack.c \ + synthio/Note.c \ synthio/Synthesizer.c \ synthio/__init__.c \ terminalio/Terminal.c \ diff --git a/shared-bindings/synthio/Note.c b/shared-bindings/synthio/Note.c new file mode 100644 index 0000000000..0e2dad40d6 --- /dev/null +++ b/shared-bindings/synthio/Note.c @@ -0,0 +1,252 @@ +/* + * 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 "py/objproperty.h" +#include "py/runtime.h" +#include "shared-bindings/util.h" +#include "shared-bindings/synthio/Note.h" +#include "shared-module/synthio/Note.h" + +static const mp_arg_t note_properties[] = { + { MP_QSTR_frequency, MP_ARG_OBJ | MP_ARG_REQUIRED, {.u_obj = NULL } }, + { MP_QSTR_amplitude, MP_ARG_OBJ, {.u_obj = MP_ROM_INT(1) } }, + { MP_QSTR_tremolo_rate, MP_ARG_OBJ, {.u_obj = NULL } }, + { MP_QSTR_tremolo_depth, MP_ARG_OBJ, {.u_obj = NULL } }, + { MP_QSTR_vibrato_rate, MP_ARG_OBJ, {.u_obj = NULL } }, + { MP_QSTR_vibrato_depth, MP_ARG_OBJ, {.u_obj = NULL } }, + { MP_QSTR_waveform, MP_ARG_OBJ, {.u_obj = MP_ROM_NONE } }, + { MP_QSTR_envelope, MP_ARG_OBJ, {.u_obj = MP_ROM_NONE } }, +}; +//| class Note: +//| def __init__( +//| self, +//| frequency: float, +//| amplitude: float = 1.0, +//| waveform: Optional[ReadableBuffer] = None, +//| envelope: Optional[Envelope] = None, +//| tremolo_depth: float = 0.0, +//| tremolo_rate: float = 0.0, +//| vibrato_depth: float = 0.0, +//| vibrato_rate: float = 0.0, +//| ) -> None: +//| """Construct a Note object, with a frequency in Hz, and optional amplitude (volume), waveform, envelope, tremolo (volume change) and vibrato (frequency change). +//| +//| If waveform or envelope are `None` the synthesizer object's default waveform or envelope are used. +//| +//| If the same Note object is played on multiple Synthesizer objects, the result is undefined. +//| """ +STATIC mp_obj_t synthio_note_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + enum { ARG_frequency, ARG_amplitude, ARG_waveform, ARG_envelope, ARG_tremolo_rate, ARG_tremolo_depth, ARG_vibrato_rate, ARG_vibrato_depth }; + mp_arg_val_t args[MP_ARRAY_SIZE(note_properties)]; + mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(note_properties), note_properties, args); + + synthio_note_obj_t *self = m_new_obj(synthio_note_obj_t); + self->base.type = &synthio_note_type; + + mp_obj_t result = MP_OBJ_FROM_PTR(self); + for (size_t i = 0; i < MP_ARRAY_SIZE(note_properties); i++) { + if (args[i].u_obj != NULL) { + mp_store_attr(result, note_properties[i].qst, args[i].u_obj); + } + } + + return result; +}; + +//| frequency: float +//| """The base frequency of the note, in Hz.""" +STATIC mp_obj_t synthio_note_get_frequency(mp_obj_t self_in) { + synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); + return mp_obj_new_float(common_hal_synthio_note_get_frequency(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(synthio_note_get_frequency_obj, synthio_note_get_frequency); + +STATIC mp_obj_t synthio_note_set_frequency(mp_obj_t self_in, mp_obj_t arg) { + synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_synthio_note_set_frequency(self, mp_obj_get_float(arg)); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(synthio_note_set_frequency_obj, synthio_note_set_frequency); +MP_PROPERTY_GETSET(synthio_note_frequency_obj, + (mp_obj_t)&synthio_note_get_frequency_obj, + (mp_obj_t)&synthio_note_set_frequency_obj); + +//| amplitude: float +//| """The base amplitude of the note, from 0 to 1""" +STATIC mp_obj_t synthio_note_get_amplitude(mp_obj_t self_in) { + synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); + return mp_obj_new_float(common_hal_synthio_note_get_amplitude(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(synthio_note_get_amplitude_obj, synthio_note_get_amplitude); + +STATIC mp_obj_t synthio_note_set_amplitude(mp_obj_t self_in, mp_obj_t arg) { + synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_synthio_note_set_amplitude(self, mp_obj_get_float(arg)); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(synthio_note_set_amplitude_obj, synthio_note_set_amplitude); +MP_PROPERTY_GETSET(synthio_note_amplitude_obj, + (mp_obj_t)&synthio_note_get_amplitude_obj, + (mp_obj_t)&synthio_note_set_amplitude_obj); + + +//| tremolo_depth: float +//| """The tremolo depth of the note, from 0 to 1""" +STATIC mp_obj_t synthio_note_get_tremolo_depth(mp_obj_t self_in) { + synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); + return mp_obj_new_float(common_hal_synthio_note_get_tremolo_depth(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(synthio_note_get_tremolo_depth_obj, synthio_note_get_tremolo_depth); + +STATIC mp_obj_t synthio_note_set_tremolo_depth(mp_obj_t self_in, mp_obj_t arg) { + synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_synthio_note_set_tremolo_depth(self, mp_obj_get_float(arg)); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(synthio_note_set_tremolo_depth_obj, synthio_note_set_tremolo_depth); +MP_PROPERTY_GETSET(synthio_note_tremolo_depth_obj, + (mp_obj_t)&synthio_note_get_tremolo_depth_obj, + (mp_obj_t)&synthio_note_set_tremolo_depth_obj); + +//| tremolo_rate: float +//| """The tremolo rate of the note, in Hz.""" +STATIC mp_obj_t synthio_note_get_tremolo_rate(mp_obj_t self_in) { + synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); + return mp_obj_new_float(common_hal_synthio_note_get_tremolo_rate(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(synthio_note_get_tremolo_rate_obj, synthio_note_get_tremolo_rate); + +STATIC mp_obj_t synthio_note_set_tremolo_rate(mp_obj_t self_in, mp_obj_t arg) { + synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_synthio_note_set_tremolo_rate(self, mp_obj_get_float(arg)); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(synthio_note_set_tremolo_rate_obj, synthio_note_set_tremolo_rate); +MP_PROPERTY_GETSET(synthio_note_tremolo_rate_obj, + (mp_obj_t)&synthio_note_get_tremolo_rate_obj, + (mp_obj_t)&synthio_note_set_tremolo_rate_obj); + +//| vibrato_depth: float +//| """The vibrato depth of the note, from 0 to 1""" +STATIC mp_obj_t synthio_note_get_vibrato_depth(mp_obj_t self_in) { + synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); + return mp_obj_new_float(common_hal_synthio_note_get_vibrato_depth(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(synthio_note_get_vibrato_depth_obj, synthio_note_get_vibrato_depth); + +STATIC mp_obj_t synthio_note_set_vibrato_depth(mp_obj_t self_in, mp_obj_t arg) { + synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_synthio_note_set_vibrato_depth(self, mp_obj_get_float(arg)); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(synthio_note_set_vibrato_depth_obj, synthio_note_set_vibrato_depth); +MP_PROPERTY_GETSET(synthio_note_vibrato_depth_obj, + (mp_obj_t)&synthio_note_get_vibrato_depth_obj, + (mp_obj_t)&synthio_note_set_vibrato_depth_obj); + +//| vibrato_rate: float +//| """The vibrato rate of the note, in Hz.""" +STATIC mp_obj_t synthio_note_get_vibrato_rate(mp_obj_t self_in) { + synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); + return mp_obj_new_float(common_hal_synthio_note_get_vibrato_rate(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(synthio_note_get_vibrato_rate_obj, synthio_note_get_vibrato_rate); + +STATIC mp_obj_t synthio_note_set_vibrato_rate(mp_obj_t self_in, mp_obj_t arg) { + synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_synthio_note_set_vibrato_rate(self, mp_obj_get_float(arg)); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(synthio_note_set_vibrato_rate_obj, synthio_note_set_vibrato_rate); +MP_PROPERTY_GETSET(synthio_note_vibrato_rate_obj, + (mp_obj_t)&synthio_note_get_vibrato_rate_obj, + (mp_obj_t)&synthio_note_set_vibrato_rate_obj); + +//| waveform: Optional[ReadableBuffer] +//| """The waveform of this note. Setting the waveform to a buffer of a different size resets the note's phase.""" +STATIC mp_obj_t synthio_note_get_waveform(mp_obj_t self_in) { + synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); + return common_hal_synthio_note_get_waveform_obj(self); +} +MP_DEFINE_CONST_FUN_OBJ_1(synthio_note_get_waveform_obj, synthio_note_get_waveform); + +STATIC mp_obj_t synthio_note_set_waveform(mp_obj_t self_in, mp_obj_t arg) { + synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_synthio_note_set_waveform(self, arg); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(synthio_note_set_waveform_obj, synthio_note_set_waveform); +MP_PROPERTY_GETSET(synthio_note_waveform_obj, + (mp_obj_t)&synthio_note_get_waveform_obj, + (mp_obj_t)&synthio_note_set_waveform_obj); + + +//| envelope: Envelope +//| """The envelope of this note""" +//| +STATIC mp_obj_t synthio_note_get_envelope(mp_obj_t self_in) { + synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); + return common_hal_synthio_note_get_envelope_obj(self); +} +MP_DEFINE_CONST_FUN_OBJ_1(synthio_note_get_envelope_obj, synthio_note_get_envelope); + +STATIC mp_obj_t synthio_note_set_envelope(mp_obj_t self_in, mp_obj_t arg) { + synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_synthio_note_set_envelope(self, arg); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(synthio_note_set_envelope_obj, synthio_note_set_envelope); +MP_PROPERTY_GETSET(synthio_note_envelope_obj, + (mp_obj_t)&synthio_note_get_envelope_obj, + (mp_obj_t)&synthio_note_set_envelope_obj); + +static void note_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { + (void)kind; + properties_print_helper(print, self_in, note_properties, MP_ARRAY_SIZE(note_properties)); +} + +STATIC const mp_rom_map_elem_t synthio_note_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_frequency), MP_ROM_PTR(&synthio_note_frequency_obj) }, + { MP_ROM_QSTR(MP_QSTR_amplitude), MP_ROM_PTR(&synthio_note_amplitude_obj) }, + { MP_ROM_QSTR(MP_QSTR_waveform), MP_ROM_PTR(&synthio_note_waveform_obj) }, + { MP_ROM_QSTR(MP_QSTR_envelope), MP_ROM_PTR(&synthio_note_envelope_obj) }, + { MP_ROM_QSTR(MP_QSTR_tremolo_depth), MP_ROM_PTR(&synthio_note_tremolo_depth_obj) }, + { MP_ROM_QSTR(MP_QSTR_tremolo_rate), MP_ROM_PTR(&synthio_note_tremolo_rate_obj) }, + { MP_ROM_QSTR(MP_QSTR_vibrato_depth), MP_ROM_PTR(&synthio_note_vibrato_depth_obj) }, + { MP_ROM_QSTR(MP_QSTR_vibrato_rate), MP_ROM_PTR(&synthio_note_vibrato_rate_obj) }, +}; +STATIC MP_DEFINE_CONST_DICT(synthio_note_locals_dict, synthio_note_locals_dict_table); + +const mp_obj_type_t synthio_note_type = { + { &mp_type_type }, + .name = MP_QSTR_Note, + .make_new = synthio_note_make_new, + .locals_dict = (mp_obj_dict_t *)&synthio_note_locals_dict, + .print = note_print, +}; diff --git a/shared-bindings/synthio/Note.h b/shared-bindings/synthio/Note.h new file mode 100644 index 0000000000..ab9d925f03 --- /dev/null +++ b/shared-bindings/synthio/Note.h @@ -0,0 +1,30 @@ +#pragma once + +#include "py/obj.h" + +typedef struct synthio_note_obj synthio_note_obj_t; +extern const mp_obj_type_t synthio_note_type; + +mp_float_t common_hal_synthio_note_get_frequency(synthio_note_obj_t *self); +void common_hal_synthio_note_set_frequency(synthio_note_obj_t *self, mp_float_t value); + +mp_float_t common_hal_synthio_note_get_amplitude(synthio_note_obj_t *self); +void common_hal_synthio_note_set_amplitude(synthio_note_obj_t *self, mp_float_t value); + +mp_float_t common_hal_synthio_note_get_tremolo_rate(synthio_note_obj_t *self); +void common_hal_synthio_note_set_tremolo_rate(synthio_note_obj_t *self, mp_float_t value); + +mp_float_t common_hal_synthio_note_get_tremolo_depth(synthio_note_obj_t *self); +void common_hal_synthio_note_set_tremolo_depth(synthio_note_obj_t *self, mp_float_t value); + +mp_float_t common_hal_synthio_note_get_vibrato_rate(synthio_note_obj_t *self); +void common_hal_synthio_note_set_vibrato_rate(synthio_note_obj_t *self, mp_float_t value); + +mp_float_t common_hal_synthio_note_get_vibrato_depth(synthio_note_obj_t *self); +void common_hal_synthio_note_set_vibrato_depth(synthio_note_obj_t *self, mp_float_t value); + +mp_obj_t common_hal_synthio_note_get_waveform_obj(synthio_note_obj_t *self); +void common_hal_synthio_note_set_waveform(synthio_note_obj_t *self, mp_obj_t value); + +mp_obj_t common_hal_synthio_note_get_envelope_obj(synthio_note_obj_t *self); +void common_hal_synthio_note_set_envelope(synthio_note_obj_t *self, mp_obj_t value); diff --git a/shared-bindings/synthio/Synthesizer.c b/shared-bindings/synthio/Synthesizer.c index 97941b860b..5d9be09f70 100644 --- a/shared-bindings/synthio/Synthesizer.c +++ b/shared-bindings/synthio/Synthesizer.c @@ -36,6 +36,9 @@ #include "shared-bindings/synthio/__init__.h" #include "supervisor/shared/translate/translate.h" +//| NoteSequence = Sequence[Union[int, Note]] +//| """A sequence of notes, which can each be integer MIDI notes or `Note` objects""" +//| //| class Synthesizer: //| def __init__( //| self, @@ -48,7 +51,9 @@ //| //| This API is experimental. //| -//| Notes use MIDI note numbering, with 60 being C4 or Middle C, approximately 262Hz. +//| Integer notes use MIDI note numbering, with 60 being C4 or Middle C, +//| approximately 262Hz. Integer notes use the given waveform & envelope, +//| and do not support advanced features like tremolo or vibrato. //| //| :param int sample_rate: The desired playback sample rate; higher sample rate requires more memory //| :param ReadableBuffer waveform: A single-cycle waveform. Default is a 50% duty cycle square wave. If specified, must be a ReadableBuffer of type 'h' (signed 16 bit) @@ -76,7 +81,6 @@ STATIC mp_obj_t synthio_synthesizer_make_new(const mp_obj_type_t *type, size_t n bufinfo_waveform.len / 2, args[ARG_envelope].u_obj); - return MP_OBJ_FROM_PTR(self); } @@ -86,12 +90,12 @@ STATIC void check_for_deinit(synthio_synthesizer_obj_t *self) { } } -//| def press(self, /, press: Sequence[int] = ()) -> None: -//| """Turn some notes on. Notes use MIDI numbering, with 60 being middle C, approximately 262Hz. +//| def press(self, /, press: NoteSequence = ()) -> None: +//| """Turn some notes on. //| //| Pressing a note that was already pressed has no effect. //| -//| :param Sequence[int] press: Any sequence of integer notes.""" +//| :param NoteSequence press: Any sequence of notes.""" 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); @@ -99,12 +103,12 @@ STATIC mp_obj_t synthio_synthesizer_press(mp_obj_t self_in, mp_obj_t press) { return mp_const_none; } STATIC MP_DEFINE_CONST_FUN_OBJ_2(synthio_synthesizer_press_obj, synthio_synthesizer_press); -//| def release(self, /, release: Sequence[int] = ()) -> None: -//| """Turn some notes off. Notes use MIDI numbering, with 60 being middle C, approximately 262Hz. +//| def release(self, /, release: NoteSequence = ()) -> None: +//| """Turn some notes off. //| //| Releasing a note that was already released has no effect. //| -//| :param Sequence[int] release: Any sequence of integer notes.""" +//| :param NoteSequence release: Any sequence of notes.""" STATIC mp_obj_t synthio_synthesizer_release(mp_obj_t self_in, mp_obj_t release) { synthio_synthesizer_obj_t *self = MP_OBJ_TO_PTR(self_in); check_for_deinit(self); @@ -113,10 +117,8 @@ STATIC mp_obj_t synthio_synthesizer_release(mp_obj_t self_in, mp_obj_t release) } STATIC MP_DEFINE_CONST_FUN_OBJ_2(synthio_synthesizer_release_obj, synthio_synthesizer_release); -//| 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. +//| def release_then_press(self, release: NoteSequence = (), press: NoteSequence = ()) -> None: +//| """Turn some notes on and/or off. //| //| It is OK to release note that was not actually turned on. //| @@ -125,8 +127,8 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_2(synthio_synthesizer_release_obj, synthio_synthe //| Releasing and pressing the note again has little effect, but does reset the phase //| of the note, which may be perceptible as a small glitch. //| -//| :param Sequence[int] release: Any sequence of integer notes. -//| :param Sequence[int] press: Any sequence of integer notes.""" +//| :param NoteSequence release: Any sequence of notes. +//| :param NoteSequence press: Any sequence of notes.""" 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[] = { @@ -146,13 +148,13 @@ STATIC mp_obj_t synthio_synthesizer_release_then_press(mp_uint_t n_args, const m 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: +//| def release_all_then_press(self, /, press: NoteSequence) -> None: //| """Turn any currently-playing notes off, then turn on the given notes //| //| Releasing and pressing the note again has little effect, but does reset the phase //| of the note, which may be perceptible as a small glitch. //| -//| :param Sequence[int] press: Any sequence of integer notes.""" +//| :param NoteSequence press: Any sequence of 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); @@ -183,7 +185,7 @@ STATIC mp_obj_t synthio_synthesizer_deinit(mp_obj_t self_in) { } STATIC MP_DEFINE_CONST_FUN_OBJ_1(synthio_synthesizer_deinit_obj, synthio_synthesizer_deinit); -//| def __enter__(self) -> MidiTrack: +//| def __enter__(self) -> Synthesizer: //| """No-op used by Context Managers.""" //| ... // Provided by context manager helper. @@ -232,7 +234,7 @@ MP_DEFINE_CONST_FUN_OBJ_1(synthio_synthesizer_get_sample_rate_obj, synthio_synth MP_PROPERTY_GETTER(synthio_synthesizer_sample_rate_obj, (mp_obj_t)&synthio_synthesizer_get_sample_rate_obj); -//| pressed: Tuple[int] +//| pressed: NoteSequence //| """A sequence of the currently pressed notes (read-only property)""" //| STATIC mp_obj_t synthio_synthesizer_obj_get_pressed(mp_obj_t self_in) { diff --git a/shared-bindings/synthio/__init__.c b/shared-bindings/synthio/__init__.c index 89929b0563..7478925dcc 100644 --- a/shared-bindings/synthio/__init__.c +++ b/shared-bindings/synthio/__init__.c @@ -35,6 +35,7 @@ #include "shared-bindings/synthio/__init__.h" #include "shared-bindings/synthio/MidiTrack.h" +#include "shared-bindings/synthio/Note.h" #include "shared-bindings/synthio/Synthesizer.h" #define default_attack_time (MICROPY_FLOAT_CONST(0.1)) @@ -157,7 +158,6 @@ const mp_obj_namedtuple_type_t synthio_envelope_type_obj = { }, }; - //| def from_file( //| file: typing.BinaryIO, //| *, @@ -271,6 +271,7 @@ MP_DEFINE_CONST_FUN_OBJ_1(synthio_midi_to_hz_obj, midi_to_hz); 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_Note), MP_ROM_PTR(&synthio_note_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) }, { MP_ROM_QSTR(MP_QSTR_Envelope), MP_ROM_PTR(&synthio_envelope_type_obj) }, diff --git a/shared-module/synthio/MidiTrack.c b/shared-module/synthio/MidiTrack.c index c59fa23bef..343a107db2 100644 --- a/shared-module/synthio/MidiTrack.c +++ b/shared-module/synthio/MidiTrack.c @@ -33,7 +33,7 @@ STATIC void print_midi_stream_error(synthio_miditrack_obj_t *self) { self->pos = self->track.len; } -STATIC uint8_t parse_note(synthio_miditrack_obj_t *self) { +STATIC mp_obj_t parse_note(synthio_miditrack_obj_t *self) { uint8_t *buffer = self->track.buf; size_t len = self->track.len; if (self->pos + 1 >= len) { @@ -43,7 +43,7 @@ STATIC uint8_t parse_note(synthio_miditrack_obj_t *self) { if (note > 127 || buffer[(self->pos)++] > 127) { print_midi_stream_error(self); } - return note; + return MP_OBJ_NEW_SMALL_INT(note); } static int decode_duration(synthio_miditrack_obj_t *self) { @@ -72,12 +72,12 @@ static void decode_until_pause(synthio_miditrack_obj_t *self) { do { switch (buffer[self->pos++] >> 4) { case 8: { // Note Off - uint8_t note = parse_note(self); + mp_obj_t note = parse_note(self); synthio_span_change_note(&self->synth, note, SYNTHIO_SILENCE); break; } case 9: { // Note On - uint8_t note = parse_note(self); + mp_obj_t note = parse_note(self); synthio_span_change_note(&self->synth, SYNTHIO_SILENCE, note); break; } diff --git a/shared-module/synthio/Note.c b/shared-module/synthio/Note.c new file mode 100644 index 0000000000..d31acb31f0 --- /dev/null +++ b/shared-module/synthio/Note.c @@ -0,0 +1,158 @@ +/* + * This file is part of the Micro Python project, http://micropython.org/ + * + * The MIT License (MIT) + * + * 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 "py/runtime.h" +#include "shared-module/synthio/Note.h" +#include "shared-bindings/synthio/Note.h" +#include "shared-bindings/synthio/__init__.h" + +static int32_t round_float_to_int(mp_float_t f) { + return (int32_t)(f + MICROPY_FLOAT_CONST(0.5)); +} + +mp_float_t common_hal_synthio_note_get_frequency(synthio_note_obj_t *self) { + return self->frequency; +} + +void common_hal_synthio_note_set_frequency(synthio_note_obj_t *self, mp_float_t value_in) { + mp_float_t val = mp_arg_validate_float_range(value_in, 0, 32767, MP_QSTR_frequency); + self->frequency = val; + self->frequency_scaled = synthio_frequency_convert_float_to_scaled(val); +} + +mp_float_t common_hal_synthio_note_get_amplitude(synthio_note_obj_t *self) { + return self->amplitude; +} + +void common_hal_synthio_note_set_amplitude(synthio_note_obj_t *self, mp_float_t value_in) { + mp_float_t val = mp_arg_validate_float_range(value_in, 0, 1, MP_QSTR_amplitude); + self->amplitude = val; + self->amplitude_scaled = round_float_to_int(val * 32767); +} + +mp_float_t common_hal_synthio_note_get_tremolo_depth(synthio_note_obj_t *self) { + return self->tremolo_descr.amplitude; +} + +void common_hal_synthio_note_set_tremolo_depth(synthio_note_obj_t *self, mp_float_t value_in) { + mp_float_t val = mp_arg_validate_float_range(value_in, 0, 1, MP_QSTR_tremolo_depth); + self->tremolo_descr.amplitude = val; + self->tremolo_state.amplitude_scaled = round_float_to_int(val * 32767); +} + +mp_float_t common_hal_synthio_note_get_tremolo_rate(synthio_note_obj_t *self) { + return self->tremolo_descr.frequency; +} + +void common_hal_synthio_note_set_tremolo_rate(synthio_note_obj_t *self, mp_float_t value_in) { + mp_float_t val = mp_arg_validate_float_range(value_in, 0, 60, MP_QSTR_tremolo_rate); + self->tremolo_descr.frequency = val; + if (self->sample_rate != 0) { + self->tremolo_state.dds = synthio_frequency_convert_float_to_dds(val, self->sample_rate); + } +} + +mp_float_t common_hal_synthio_note_get_vibrato_depth(synthio_note_obj_t *self) { + return self->vibrato_descr.amplitude; +} + +void common_hal_synthio_note_set_vibrato_depth(synthio_note_obj_t *self, mp_float_t value_in) { + mp_float_t val = mp_arg_validate_float_range(value_in, 0, 1, MP_QSTR_vibrato_depth); + self->vibrato_descr.amplitude = val; + self->vibrato_state.amplitude_scaled = round_float_to_int(val * 32767); +} + +mp_float_t common_hal_synthio_note_get_vibrato_rate(synthio_note_obj_t *self) { + return self->vibrato_descr.frequency; +} + +void common_hal_synthio_note_set_vibrato_rate(synthio_note_obj_t *self, mp_float_t value_in) { + mp_float_t val = mp_arg_validate_float_range(value_in, 0, 60, MP_QSTR_vibrato_rate); + self->vibrato_descr.frequency = val; + if (self->sample_rate != 0) { + self->vibrato_state.dds = synthio_frequency_convert_float_to_dds(val, self->sample_rate); + } +} + +mp_obj_t common_hal_synthio_note_get_envelope_obj(synthio_note_obj_t *self) { + return self->envelope_obj; +} + +void common_hal_synthio_note_set_envelope(synthio_note_obj_t *self, mp_obj_t envelope_in) { + if (envelope_in != mp_const_none) { + mp_arg_validate_type(envelope_in, (mp_obj_type_t *)&synthio_envelope_type_obj, MP_QSTR_envelope); + } + self->envelope_obj = envelope_in; +} + +mp_obj_t common_hal_synthio_note_get_waveform_obj(synthio_note_obj_t *self) { + return self->waveform_obj; +} + +void common_hal_synthio_note_set_waveform(synthio_note_obj_t *self, mp_obj_t waveform_in) { + if (waveform_in == mp_const_none) { + memset(&self->waveform_buf, 0, sizeof(self->waveform_buf)); + } else { + mp_buffer_info_t bufinfo_waveform; + synthio_synth_parse_waveform(&bufinfo_waveform, waveform_in); + self->waveform_buf = bufinfo_waveform; + } + self->waveform_obj = waveform_in; +} + +void synthio_note_recalculate(synthio_note_obj_t *self, int32_t sample_rate) { + if (sample_rate == self->sample_rate) { + return; + } + self->sample_rate = sample_rate; + + if (self->envelope_obj != mp_const_none) { + synthio_envelope_definition_set(&self->envelope_def, self->envelope_obj, sample_rate); + } + + synthio_lfo_set(&self->tremolo_state, &self->tremolo_descr, sample_rate); + self->tremolo_state.offset_scaled = 32768 - self->tremolo_state.amplitude_scaled; + synthio_lfo_set(&self->vibrato_state, &self->vibrato_descr, sample_rate); + self->vibrato_state.offset_scaled = 32768; +} + +void synthio_note_start(synthio_note_obj_t *self, int32_t sample_rate) { + synthio_note_recalculate(self, sample_rate); + self->phase = 0; +} + +uint32_t synthio_note_envelope(synthio_note_obj_t *self) { + return self->amplitude_scaled; +} + +uint32_t synthio_note_step(synthio_note_obj_t *self, int32_t sample_rate, int16_t dur, uint16_t *loudness) { + int tremolo_value = synthio_lfo_step(&self->tremolo_state, dur); + int vibrato_value = synthio_lfo_step(&self->vibrato_state, dur); + *loudness = (*loudness * tremolo_value) >> 15; + uint32_t frequency_scaled = ((uint64_t)self->frequency_scaled * vibrato_value) >> 15; + return frequency_scaled; +} diff --git a/shared-module/synthio/Note.h b/shared-module/synthio/Note.h new file mode 100644 index 0000000000..4b66997996 --- /dev/null +++ b/shared-module/synthio/Note.h @@ -0,0 +1,54 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * 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/__init__.h" + +typedef struct synthio_note_obj { + mp_obj_base_t base; + + mp_float_t frequency; + mp_float_t amplitude; + mp_obj_t waveform_obj, envelope_obj; + + int32_t sample_rate; + + int32_t phase; + int32_t frequency_scaled; + int32_t amplitude_scaled; + synthio_lfo_descr_t tremolo_descr, vibrato_descr; + synthio_lfo_state_t tremolo_state, vibrato_state; + + mp_buffer_info_t waveform_buf; + synthio_envelope_definition_t envelope_def; +} synthio_note_obj_t; + +void synthio_note_recalculate(synthio_note_obj_t *self, int32_t sample_rate); +uint32_t synthio_note_step(synthio_note_obj_t *self, int32_t sample_rate, int16_t dur, uint16_t *loudness); +void synthio_note_start(synthio_note_obj_t *self, int32_t sample_rate); +bool synthio_note_playing(synthio_note_obj_t *self); +uint32_t synthio_note_envelope(synthio_note_obj_t *self); diff --git a/shared-module/synthio/Synthesizer.c b/shared-module/synthio/Synthesizer.c index 42d0468845..02d98a521e 100644 --- a/shared-module/synthio/Synthesizer.c +++ b/shared-module/synthio/Synthesizer.c @@ -26,6 +26,8 @@ #include "py/runtime.h" #include "shared-bindings/synthio/Synthesizer.h" +#include "shared-bindings/synthio/Note.h" +#include "shared-module/synthio/Note.h" @@ -72,40 +74,58 @@ void synthio_synthesizer_get_buffer_structure(synthio_synthesizer_obj_t *self, b void common_hal_synthio_synthesizer_release_all(synthio_synthesizer_obj_t *self) { for (size_t i = 0; i < CIRCUITPY_SYNTHIO_MAX_CHANNELS; i++) { - if (self->synth.span.note[i] != SYNTHIO_SILENCE) { - synthio_span_change_note(&self->synth, self->synth.span.note[i], SYNTHIO_SILENCE); + if (self->synth.span.note_obj[i] != SYNTHIO_SILENCE) { + synthio_span_change_note(&self->synth, self->synth.span.note_obj[i], SYNTHIO_SILENCE); } } } + +STATIC mp_obj_t validate_note(mp_obj_t note_in) { + if (mp_obj_is_small_int(note_in)) { + mp_arg_validate_int_range(mp_obj_get_int(note_in), 0, 127, MP_QSTR_note); + } else { + const mp_obj_type_t *note_type = mp_obj_get_type(note_in); + if (note_type != &synthio_note_type) { + mp_raise_TypeError_varg(translate("%q must be of type %q or %q, not %q"), MP_QSTR_note, MP_QSTR_int, MP_QSTR_Note, note_type->name); + } + } + return note_in; +} + 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, mp_arg_validate_int_range(mp_obj_get_int(item), 0, 127, MP_QSTR_note), SYNTHIO_SILENCE); + synthio_span_change_note(&self->synth, validate_note(item), 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, SYNTHIO_SILENCE, mp_arg_validate_int_range(mp_obj_get_int(item), 0, 127, MP_QSTR_note)); + mp_obj_t note_obj; + while ((note_obj = mp_iternext(iterable)) != MP_OBJ_STOP_ITERATION) { + if (synthio_span_change_note(&self->synth, SYNTHIO_SILENCE, validate_note(note_obj))) { + if (!mp_obj_is_small_int(note_obj)) { + synthio_note_obj_t *note = MP_OBJ_TO_PTR(note_obj); + synthio_note_start(note, self->synth.sample_rate); + } + } } } mp_obj_t common_hal_synthio_synthesizer_get_pressed_notes(synthio_synthesizer_obj_t *self) { int count = 0; for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) { - if (self->synth.span.note[chan] != SYNTHIO_SILENCE && self->synth.envelope_state[chan].state != SYNTHIO_ENVELOPE_STATE_RELEASE) { + if (self->synth.span.note_obj[chan] != SYNTHIO_SILENCE && SYNTHIO_NOTE_IS_PLAYING(&self->synth, chan)) { count += 1; } } mp_obj_tuple_t *result = MP_OBJ_TO_PTR(mp_obj_new_tuple(count, NULL)); for (size_t chan = 0, j = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) { - if (self->synth.span.note[chan] != SYNTHIO_SILENCE && self->synth.envelope_state[chan].state != SYNTHIO_ENVELOPE_STATE_RELEASE) { - result->items[j++] = MP_OBJ_NEW_SMALL_INT(self->synth.span.note[chan]); + if (self->synth.span.note_obj[chan] != SYNTHIO_SILENCE && SYNTHIO_NOTE_IS_PLAYING(&self->synth, chan)) { + result->items[j++] = self->synth.span.note_obj[chan]; } } return MP_OBJ_FROM_PTR(result); diff --git a/shared-module/synthio/__init__.c b/shared-module/synthio/__init__.c index 8c17dfdeed..857f483780 100644 --- a/shared-module/synthio/__init__.c +++ b/shared-module/synthio/__init__.c @@ -27,6 +27,7 @@ #include "shared-module/synthio/__init__.h" #include "shared-bindings/synthio/__init__.h" +#include "shared-module/synthio/Note.h" #include "py/runtime.h" #include #include @@ -36,6 +37,14 @@ 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 int32_t round_float_to_int(mp_float_t f) { + return (int32_t)(f + MICROPY_FLOAT_CONST(0.5)); +} + +static int64_t round_float_to_int64(mp_float_t f) { + return (int64_t)(f + MICROPY_FLOAT_CONST(0.5)); +} + mp_float_t common_hal_synthio_midi_to_hz_float(mp_int_t arg) { uint8_t octave = arg / 12; uint16_t base_freq = notes[arg % 12]; @@ -52,7 +61,7 @@ STATIC int16_t convert_time_to_rate(uint32_t sample_rate, mp_obj_t time_in, int1 return (difference < 0) ? -result : result; } -STATIC void synthio_envelope_definition_set(synthio_envelope_definition_t *envelope, mp_obj_t obj, uint32_t sample_rate) { +void synthio_envelope_definition_set(synthio_envelope_definition_t *envelope, mp_obj_t obj, uint32_t sample_rate) { if (obj == mp_const_none) { envelope->attack_level = 32767; envelope->sustain_level = 32767; @@ -141,7 +150,7 @@ STATIC void synthio_envelope_state_release(synthio_envelope_state_t *state, synt STATIC uint32_t synthio_synth_sum_envelope(synthio_synth_t *synth) { uint32_t result = 0; for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) { - if (synth->span.note[chan] != SYNTHIO_SILENCE) { + if (synth->span.note_obj[chan] != SYNTHIO_SILENCE) { result += synth->envelope_state[chan].level; } } @@ -168,12 +177,10 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t int32_t sample_rate = synth->sample_rate; uint32_t total_envelope = synthio_synth_sum_envelope(synth); - const int16_t *waveform = synth->waveform; - uint32_t waveform_length = synth->waveform_length; if (total_envelope > 0) { uint16_t ovl_loudness = 0x7fffffff / MAX(0x8000, total_envelope); for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) { - if (synth->span.note[chan] == SYNTHIO_SILENCE) { + if (synth->span.note_obj[chan] == SYNTHIO_SILENCE) { synth->accum[chan] = 0; continue; } @@ -182,25 +189,52 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t if (synth->envelope_state[chan].level == 0) { // note is truly finished - synth->span.note[chan] = SYNTHIO_SILENCE; + synth->span.note_obj[chan] = SYNTHIO_SILENCE; } - uint8_t octave = synth->span.note[chan] / 12; - uint16_t base_freq = notes[synth->span.note[chan] % 12]; + + uint32_t dds_rate; + const int16_t *waveform = synth->waveform; + uint32_t waveform_length = synth->waveform_length; + mp_obj_t note_obj = synth->span.note_obj[chan]; + if (mp_obj_is_small_int(note_obj)) { + uint8_t note = mp_obj_get_int(note_obj); + uint8_t octave = note / 12; + uint16_t base_freq = notes[note % 12]; + // rate = base_freq * waveform_length + // den = sample_rate * 2 ^ (10 - octave) + // den = sample_rate * 2 ^ 10 / 2^octave + // dds_rate = 2^SHIFT * rate / den + // dds_rate = 2^(SHIFT-10+octave) * base_freq * waveform_length / sample_rate + dds_rate = (sample_rate / 2 + ((uint64_t)(base_freq * waveform_length) << (SYNTHIO_FREQUENCY_SHIFT - 10 + octave))) / sample_rate; + } else { + synthio_note_obj_t *note = MP_OBJ_TO_PTR(note_obj); + int32_t frequency_scaled = synthio_note_step(note, sample_rate, dur, &loudness); + if (note->waveform_buf.buf) { + waveform = note->waveform_buf.buf; + waveform_length = note->waveform_buf.len / 2; + } + dds_rate = synthio_frequency_convert_scaled_to_dds((uint64_t)frequency_scaled * waveform_length, sample_rate); + } + uint32_t accum = synth->accum[chan]; -#define SHIFT (16) - // rate = base_freq * waveform_length - // den = sample_rate * 2 ^ (10 - octave) - // den = sample_rate * 2 ^ 10 / 2^octave - // dds_rate = 2^SHIFT * rate / den - // dds_rate = 2^(SHIFT-10+octave) * base_freq * waveform_length / sample_rate - uint32_t dds_rate = (sample_rate / 2 + ((uint64_t)(base_freq * waveform_length) << (SHIFT - 10 + octave))) / sample_rate; + uint32_t lim = waveform_length << SYNTHIO_FREQUENCY_SHIFT; + if (dds_rate > lim / 2) { + // beyond nyquist, can't play note + continue; + } + + // can happen if note waveform gets set mid-note, but the expensive modulo is usually avoided + if (accum > lim) { + accum %= lim; + } for (uint16_t i = 0; i < dur; i++) { accum += dds_rate; - if (accum > waveform_length << SHIFT) { - accum -= waveform_length << SHIFT; + // because dds_rate is low enough, the subtraction is guaranteed to go back into range, no expensive modulo needed + if (accum > lim) { + accum -= lim; } - int16_t idx = accum >> SHIFT; + int16_t idx = accum >> SYNTHIO_FREQUENCY_SHIFT; out_buffer[i] += (waveform[idx] * loudness) / 65536; } synth->accum[chan] = accum; @@ -209,7 +243,18 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t // advance envelope states for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) { - synthio_envelope_state_step(&synth->envelope_state[chan], &synth->envelope_definition, dur); + mp_obj_t note_obj = synth->span.note_obj[chan]; + if (note_obj == SYNTHIO_SILENCE) { + continue; + } + synthio_envelope_definition_t *def = &synth->envelope_definition; + if (!mp_obj_is_small_int(note_obj)) { + synthio_note_obj_t *note = MP_OBJ_TO_PTR(note_obj); + if (note->envelope_obj != mp_const_none) { + def = ¬e->envelope_def; + } + } + synthio_envelope_state_step(&synth->envelope_state[chan], def, dur); } *buffer_length = synth->last_buffer_length = dur * SYNTHIO_BYTES_PER_SAMPLE; @@ -254,7 +299,7 @@ void synthio_synth_init(synthio_synth_t *synth, uint32_t sample_rate, const int1 synthio_synth_envelope_set(synth, envelope_obj); for (size_t i = 0; i < CIRCUITPY_SYNTHIO_MAX_CHANNELS; i++) { - synth->span.note[i] = SYNTHIO_SILENCE; + synth->span.note_obj[i] = SYNTHIO_SILENCE; } } @@ -283,16 +328,16 @@ void synthio_synth_parse_waveform(mp_buffer_info_t *bufinfo_waveform, mp_obj_t w parse_common(bufinfo_waveform, waveform_obj, MP_QSTR_waveform); } -STATIC int find_channel_with_note(synthio_synth_t *synth, uint8_t note) { +STATIC int find_channel_with_note(synthio_synth_t *synth, mp_obj_t note) { for (int i = 0; i < CIRCUITPY_SYNTHIO_MAX_CHANNELS; i++) { - if (synth->span.note[i] == note) { + if (synth->span.note_obj[i] == note) { return i; } } if (note == SYNTHIO_SILENCE) { // we need a victim note that is releasing. simple algorithm: lowest numbered slot for (int i = 0; i < CIRCUITPY_SYNTHIO_MAX_CHANNELS; i++) { - if (SYNTHIO_VOICE_IS_RELEASING(synth, i)) { + if (!SYNTHIO_NOTE_IS_PLAYING(synth, i)) { return i; } } @@ -300,7 +345,7 @@ STATIC int find_channel_with_note(synthio_synth_t *synth, uint8_t note) { return -1; } -bool synthio_span_change_note(synthio_synth_t *synth, uint8_t old_note, uint8_t new_note) { +bool synthio_span_change_note(synthio_synth_t *synth, mp_obj_t old_note, mp_obj_t new_note) { int channel; if (new_note != SYNTHIO_SILENCE && (channel = find_channel_with_note(synth, new_note)) != -1) { // note already playing, re-strike @@ -313,7 +358,7 @@ bool synthio_span_change_note(synthio_synth_t *synth, uint8_t old_note, uint8_t if (new_note == SYNTHIO_SILENCE) { synthio_envelope_state_release(&synth->envelope_state[channel], &synth->envelope_definition); } else { - synth->span.note[channel] = new_note; + synth->span.note_obj[channel] = new_note; synthio_envelope_state_init(&synth->envelope_state[channel], &synth->envelope_definition); synth->accum[channel] = 0; } @@ -321,3 +366,39 @@ bool synthio_span_change_note(synthio_synth_t *synth, uint8_t old_note, uint8_t } return false; } + +uint64_t synthio_frequency_convert_float_to_scaled(mp_float_t val) { + return round_float_to_int64(val * (1 << SYNTHIO_FREQUENCY_SHIFT)); +} + +uint32_t synthio_frequency_convert_float_to_dds(mp_float_t frequency_hz, int32_t sample_rate) { + return synthio_frequency_convert_scaled_to_dds(synthio_frequency_convert_float_to_scaled(frequency_hz), sample_rate); +} + +uint32_t synthio_frequency_convert_scaled_to_dds(uint64_t frequency_scaled, int32_t sample_rate) { + return (sample_rate / 2 + frequency_scaled) / sample_rate; +} + +void synthio_lfo_set(synthio_lfo_state_t *state, const synthio_lfo_descr_t *descr, uint32_t sample_rate) { + state->amplitude_scaled = round_float_to_int(descr->amplitude * 32768); + state->dds = synthio_frequency_convert_float_to_dds(descr->frequency * 65536, sample_rate); +} + +int synthio_lfo_step(synthio_lfo_state_t *state, uint16_t dur) { + uint32_t phase = state->phase; + uint16_t whole_phase = phase >> 16; + + // advance the phase accumulator + state->phase = phase + state->dds * dur; + + // create a triangle wave, it's quick and easy + int v; + if (whole_phase < 16384) { // ramp from 0 to amplitude + v = (state->amplitude_scaled * whole_phase); + } else if (whole_phase < 49152) { // ramp from +amplitude to -amplitude + v = (state->amplitude_scaled * (32768 - whole_phase)); + } else { // from -amplitude to 0 + v = (state->amplitude_scaled * (whole_phase - 65536)); + } + return v / 16384 + state->offset_scaled; +} diff --git a/shared-module/synthio/__init__.h b/shared-module/synthio/__init__.h index 56521d3208..42e4ee79d4 100644 --- a/shared-module/synthio/__init__.h +++ b/shared-module/synthio/__init__.h @@ -29,14 +29,16 @@ #define SYNTHIO_BITS_PER_SAMPLE (16) #define SYNTHIO_BYTES_PER_SAMPLE (SYNTHIO_BITS_PER_SAMPLE / 8) #define SYNTHIO_MAX_DUR (256) -#define SYNTHIO_SILENCE (0x80) -#define SYNTHIO_VOICE_IS_RELEASING(synth, i) (synth->envelope_state[i].state == SYNTHIO_ENVELOPE_STATE_RELEASE) +#define SYNTHIO_SILENCE (mp_const_none) +#define SYNTHIO_NOTE_IS_SIMPLE(note) (mp_obj_is_small_int(note)) +#define SYNTHIO_NOTE_IS_PLAYING(synth, i) ((synth)->envelope_state[(i)].state != SYNTHIO_ENVELOPE_STATE_RELEASE) +#define SYNTHIO_FREQUENCY_SHIFT (16) #include "shared-module/audiocore/__init__.h" typedef struct { uint16_t dur; - uint8_t note[CIRCUITPY_SYNTHIO_MAX_CHANNELS]; + mp_obj_t note_obj[CIRCUITPY_SYNTHIO_MAX_CHANNELS]; } synthio_midi_span_t; typedef struct { @@ -73,6 +75,15 @@ typedef struct synthio_synth { synthio_envelope_state_t envelope_state[CIRCUITPY_SYNTHIO_MAX_CHANNELS]; } synthio_synth_t; +typedef struct { + mp_float_t amplitude, frequency; +} synthio_lfo_descr_t; + +typedef struct { + uint32_t amplitude_scaled, offset_scaled, dds, phase; +} synthio_lfo_state_t; + + void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **buffer, uint32_t *buffer_length, uint8_t channel); void synthio_synth_deinit(synthio_synth_t *synth); bool synthio_synth_deinited(synthio_synth_t *synth); @@ -84,6 +95,14 @@ void synthio_synth_reset_buffer(synthio_synth_t *synth, bool single_channel_outp void synthio_synth_parse_waveform(mp_buffer_info_t *bufinfo_waveform, mp_obj_t waveform_obj); void synthio_synth_parse_envelope(uint16_t *envelope_sustain_index, mp_buffer_info_t *bufinfo_envelope, mp_obj_t envelope_obj, mp_obj_t envelope_hold_obj); -bool synthio_span_change_note(synthio_synth_t *synth, uint8_t old_note, uint8_t new_note); +bool synthio_span_change_note(synthio_synth_t *synth, mp_obj_t old_note, mp_obj_t new_note); void synthio_envelope_step(synthio_envelope_definition_t *definition, synthio_envelope_state_t *state, int n_samples); +void synthio_envelope_definition_set(synthio_envelope_definition_t *envelope, mp_obj_t obj, uint32_t sample_rate); + +uint64_t synthio_frequency_convert_float_to_scaled(mp_float_t frequency_hz); +uint32_t synthio_frequency_convert_float_to_dds(mp_float_t frequency_hz, int32_t sample_rate); +uint32_t synthio_frequency_convert_scaled_to_dds(uint64_t frequency_scaled, int32_t sample_rate); + +void synthio_lfo_set(synthio_lfo_state_t *state, const synthio_lfo_descr_t *descr, uint32_t sample_rate); +int synthio_lfo_step(synthio_lfo_state_t *state, uint16_t dur); diff --git a/tests/circuitpython-manual/synthio/note/README.md b/tests/circuitpython-manual/synthio/note/README.md new file mode 100644 index 0000000000..84ac1cbd6f --- /dev/null +++ b/tests/circuitpython-manual/synthio/note/README.md @@ -0,0 +1,5 @@ +# Test synthio without hardware + +Build the unix port then run `....../ports/unix/micropython-coverage code.py`. + +This will create `tune.wav` as output, which you can listen to using any old audio player. diff --git a/tests/circuitpython-manual/synthio/note/audioop.py b/tests/circuitpython-manual/synthio/note/audioop.py new file mode 120000 index 0000000000..31896fe265 --- /dev/null +++ b/tests/circuitpython-manual/synthio/note/audioop.py @@ -0,0 +1 @@ +../wave/audioop.py \ No newline at end of file diff --git a/tests/circuitpython-manual/synthio/note/chunk.py b/tests/circuitpython-manual/synthio/note/chunk.py new file mode 120000 index 0000000000..00983c1f72 --- /dev/null +++ b/tests/circuitpython-manual/synthio/note/chunk.py @@ -0,0 +1 @@ +../wave/chunk.py \ No newline at end of file diff --git a/tests/circuitpython-manual/synthio/note/code.py b/tests/circuitpython-manual/synthio/note/code.py new file mode 100644 index 0000000000..72783d1b55 --- /dev/null +++ b/tests/circuitpython-manual/synthio/note/code.py @@ -0,0 +1,100 @@ +import random +import audiocore +import synthio +from ulab import numpy as np +import wave + +SAMPLE_SIZE = 1024 +VOLUME = 14700 +sine = np.array( + np.sin(np.linspace(0, 2 * np.pi, SAMPLE_SIZE, endpoint=False)) * VOLUME, + dtype=np.int16, +) + +envelope = synthio.Envelope( + attack_time=0.1, decay_time=0.05, release_time=0.2, attack_level=1, sustain_level=0.8 +) + +synth = synthio.Synthesizer(sample_rate=48000) + + +def synthesize(synth): + random.seed(3) + target_notes = [synthio.midi_to_hz(n + o) for n in (60, 64, 67, 70) for o in (-12, 12, 0)] + print(target_notes) + notes = [ + synthio.Note(frequency=random.randint(60, 20000), waveform=sine, envelope=envelope) + for note in target_notes + ] + synth.press(notes) + target = synthio.midi_to_hz(72) + factor = 0.98 + for i in range(600): + yield 1 + for ni, ti in zip(notes, target_notes): + print(ni.frequency, ti) + break + for ni, ti in zip(notes, target_notes): + ni.frequency = (ni.frequency * factor) + (ti * (1 - factor)) + synth.release_all() + yield 36 + + +def synthesize2(synth): + n = synthio.Note( + frequency=synthio.midi_to_hz(60), + tremolo_depth=0.2, + tremolo_rate=2, + waveform=sine, + envelope=envelope, + ) + synth.press((n,)) + yield 360 + synth.release_all() + yield 36 + + +def synthesize3(synth): + n = synthio.Note( + frequency=synthio.midi_to_hz(60), + vibrato_depth=0.1, + vibrato_rate=8, + waveform=sine, + envelope=envelope, + ) + synth.press((n,)) + yield 360 + synth.release_all() + yield 36 + + +def synthesize4(synth): + n = synthio.Note( + frequency=synthio.midi_to_hz(60), + tremolo_depth=0.1, + tremolo_rate=1.5, + vibrato_depth=0.1, + vibrato_rate=3, + waveform=sine, + envelope=envelope, + ) + synth.press((n,)) + yield 720 + synth.release_all() + yield 36 + + +def chain(*args): + for a in args: + yield from a + + +# sox -r 48000 -e signed -b 16 -c 1 tune.raw tune.wav +with wave.open("tune-noenv.wav", "w") as f: + f.setnchannels(1) + f.setsampwidth(2) + f.setframerate(48000) + for n in chain(synthesize2(synth), synthesize3(synth), synthesize4(synth)): + for i in range(n): + result, data = audiocore.get_buffer(synth) + f.writeframes(data) diff --git a/tests/circuitpython-manual/synthio/note/wave.py b/tests/circuitpython-manual/synthio/note/wave.py new file mode 120000 index 0000000000..45884bd29a --- /dev/null +++ b/tests/circuitpython-manual/synthio/note/wave.py @@ -0,0 +1 @@ +../wave/wave.py \ No newline at end of file diff --git a/tests/circuitpython-manual/synthio/wave/README.md b/tests/circuitpython-manual/synthio/wave/README.md index 27868bf9f3..83639b374d 100644 --- a/tests/circuitpython-manual/synthio/wave/README.md +++ b/tests/circuitpython-manual/synthio/wave/README.md @@ -1,5 +1,5 @@ # Test synthio without hardware -Build the uninx port then run `....../ports/unix/micropython-coverage midi2wav.py`. +Build the unix port then run `....../ports/unix/micropython-coverage midi2wav.py`. This will create `tune.wav` as output, which you can listen to using any old audio player. diff --git a/tests/circuitpython/synthesizer_note.py b/tests/circuitpython/synthesizer_note.py new file mode 100644 index 0000000000..77420e66f6 --- /dev/null +++ b/tests/circuitpython/synthesizer_note.py @@ -0,0 +1,41 @@ +import struct +import synthio +import audiocore + + +def dump_samples(): + print([i for i in audiocore.get_buffer(s)[1][:24]]) + + +n80 = synthio.Note(synthio.midi_to_hz(80)) +n91 = synthio.Note(synthio.midi_to_hz(80)) + +s = synthio.Synthesizer(sample_rate=8000) +print(s.pressed) +dump_samples() + +s.press((n80,)) +print(s.pressed) +dump_samples() + +s.press((n91,)) +print(s.pressed) +dump_samples() + +s.release_then_press((n80,)) +print(s.pressed) +dump_samples() + +envelope = synthio.Envelope( + attack_time=0.1, decay_time=0.05, release_time=0.2, attack_level=1, sustain_level=0.8 +) +n60 = synthio.Note(synthio.midi_to_hz(60), envelope=envelope) +s = synthio.Synthesizer(sample_rate=8000, envelope=envelope) +s.press((n60,)) +for _ in range(12): + buf = audiocore.get_buffer(s)[1] + print((min(buf), max(buf))) +s.release_all() +for _ in range(12): + buf = audiocore.get_buffer(s)[1] + print((min(buf), max(buf))) diff --git a/tests/circuitpython/synthesizer_note.py.exp b/tests/circuitpython/synthesizer_note.py.exp new file mode 100644 index 0000000000..82ae685c07 --- /dev/null +++ b/tests/circuitpython/synthesizer_note.py.exp @@ -0,0 +1,32 @@ +() +[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] +(Note(frequency=830.625, amplitude=1.0, tremolo_rate=0.0, tremolo_depth=0.0, vibrato_rate=0.0, vibrato_depth=0.0, waveform=None, envelope=None),) +[-16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383] +(Note(frequency=830.625, amplitude=1.0, tremolo_rate=0.0, tremolo_depth=0.0, vibrato_rate=0.0, vibrato_depth=0.0, waveform=None, envelope=None), Note(frequency=830.625, amplitude=1.0, tremolo_rate=0.0, tremolo_depth=0.0, vibrato_rate=0.0, vibrato_depth=0.0, waveform=None, envelope=None)) +[0, 0, 0, 0, 0, 0, 0, 0, 16382, 0, 0, 0, 0, -16382, 0, 0, 0, 0, 16382, 0, 0, 0, 0, -16382] +(Note(frequency=830.625, amplitude=1.0, tremolo_rate=0.0, tremolo_depth=0.0, vibrato_rate=0.0, vibrato_depth=0.0, waveform=None, envelope=None),) +[0, 0, 0, 16382, 0, 0, 0, 0, 0, 0, 0, 0, 16382, 0, 0, 0, 0, -16382, 0, 0, 0, 0, 16382, 0] +(-5242, 5241) +(-10484, 10484) +(-15727, 15726) +(-16383, 16382) +(-14286, 14285) +(-13106, 13105) +(-13106, 13105) +(-13106, 13105) +(-13106, 13105) +(-13106, 13105) +(-13106, 13105) +(-13106, 13105) +(-13106, 13105) +(-11009, 11008) +(-8912, 8911) +(-6815, 6814) +(-4718, 4717) +(-2621, 2620) +(-524, 523) +(0, 0) +(0, 0) +(0, 0) +(0, 0) +(0, 0) From 4f56b7646e9c7107a4a0e9d1addf58d6df4d5a5d Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Tue, 2 May 2023 14:14:50 -0500 Subject: [PATCH 05/19] synthio: slow ramp overall envelope back up .. and account releasing notes at their sustain level until they're done. this ameliorates the effect where multiple releasing notes don't seem to actually be releasing, but stay at a constant volume. --- shared-module/synthio/__init__.c | 30 ++++++++++++++++++++++++------ shared-module/synthio/__init__.h | 1 + 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/shared-module/synthio/__init__.c b/shared-module/synthio/__init__.c index 857f483780..19e9990b17 100644 --- a/shared-module/synthio/__init__.c +++ b/shared-module/synthio/__init__.c @@ -151,7 +151,12 @@ STATIC uint32_t synthio_synth_sum_envelope(synthio_synth_t *synth) { uint32_t result = 0; for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) { if (synth->span.note_obj[chan] != SYNTHIO_SILENCE) { - result += synth->envelope_state[chan].level; + synthio_envelope_state_t *state = &synth->envelope_state[chan]; + if (state->state == SYNTHIO_ENVELOPE_STATE_RELEASE) { + result += synth->envelope_definition.sustain_level; + } else { + result += state->level; + } } } return result; @@ -177,25 +182,38 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t int32_t sample_rate = synth->sample_rate; uint32_t total_envelope = synthio_synth_sum_envelope(synth); + if (total_envelope < synth->total_envelope) { + // total envelope is decreasing. Slowly let remaining notes get louder + // the time constant is arbitrary, on the order of 1s at 48kHz + total_envelope = synth->total_envelope = ( + total_envelope + synth->total_envelope * 255) / 256; + } else { + // total envelope is steady or increasing, so just store this as + // the high water mark + synth->total_envelope = total_envelope; + } if (total_envelope > 0) { uint16_t ovl_loudness = 0x7fffffff / MAX(0x8000, total_envelope); + for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) { - if (synth->span.note_obj[chan] == SYNTHIO_SILENCE) { + mp_obj_t note_obj = synth->span.note_obj[chan]; + if (note_obj == SYNTHIO_SILENCE) { synth->accum[chan] = 0; continue; } - // adjust loudness by envelope - uint16_t loudness = (ovl_loudness * synth->envelope_state[chan].level) >> 16; if (synth->envelope_state[chan].level == 0) { - // note is truly finished + // note is truly finished, but we only just noticed synth->span.note_obj[chan] = SYNTHIO_SILENCE; + continue; } + // adjust loudness by envelope + uint16_t loudness = (ovl_loudness * synth->envelope_state[chan].level) >> 16; + uint32_t dds_rate; const int16_t *waveform = synth->waveform; uint32_t waveform_length = synth->waveform_length; - mp_obj_t note_obj = synth->span.note_obj[chan]; if (mp_obj_is_small_int(note_obj)) { uint8_t note = mp_obj_get_int(note_obj); uint8_t octave = note / 12; diff --git a/shared-module/synthio/__init__.h b/shared-module/synthio/__init__.h index 42e4ee79d4..29520dfb5b 100644 --- a/shared-module/synthio/__init__.h +++ b/shared-module/synthio/__init__.h @@ -62,6 +62,7 @@ typedef struct { typedef struct synthio_synth { uint32_t sample_rate; + uint32_t total_envelope; int16_t *buffers[2]; const int16_t *waveform; uint16_t buffer_length; From c06597c07aaacf7ae0125467d905e7b169d23fc2 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Tue, 2 May 2023 14:00:31 -0500 Subject: [PATCH 06/19] synthio: replace the quietest releasing note when over-writing --- shared-module/synthio/__init__.c | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/shared-module/synthio/__init__.c b/shared-module/synthio/__init__.c index 19e9990b17..4b32ec3e91 100644 --- a/shared-module/synthio/__init__.c +++ b/shared-module/synthio/__init__.c @@ -352,15 +352,21 @@ STATIC int find_channel_with_note(synthio_synth_t *synth, mp_obj_t note) { return i; } } + int result = -1; if (note == SYNTHIO_SILENCE) { - // we need a victim note that is releasing. simple algorithm: lowest numbered slot - for (int i = 0; i < CIRCUITPY_SYNTHIO_MAX_CHANNELS; i++) { - if (!SYNTHIO_NOTE_IS_PLAYING(synth, i)) { - return i; + // replace the releasing note with lowest volume level + int level = 32768; + for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) { + if (!SYNTHIO_NOTE_IS_PLAYING(synth, chan)) { + synthio_envelope_state_t *state = &synth->envelope_state[chan]; + if (state->level < level) { + result = chan; + level = state->level; + } } } } - return -1; + return result; } bool synthio_span_change_note(synthio_synth_t *synth, mp_obj_t old_note, mp_obj_t new_note) { From 1701552decff4fa50e1f92240d2833f03118339b Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 4 May 2023 07:22:02 -0500 Subject: [PATCH 07/19] synthio: make sustain level relative to attack level and re-vamp overall envelope calculation again. Now, if you set a low overall attack level like 0.2 this avoids the "diminishing volume" effect when many notes sound at once. You need simply choose a maximum attack level that is appropriate for the max number of voices that will actually be played. --- shared-bindings/synthio/__init__.c | 8 ++++---- shared-module/synthio/__init__.c | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/shared-bindings/synthio/__init__.c b/shared-bindings/synthio/__init__.c index 7478925dcc..5af352b404 100644 --- a/shared-bindings/synthio/__init__.c +++ b/shared-bindings/synthio/__init__.c @@ -79,8 +79,8 @@ static const mp_arg_t envelope_properties[] = { //| :param float attack_time: The time in seconds it takes to ramp from 0 volume to attack_volume //| :param float decay_time: The time in seconds it takes to ramp from attack_volume to sustain_volume //| :param float release_time: The time in seconds it takes to ramp from sustain_volume to release_volume. When a note is released before it has reached the sustain phase, the release is done with the same slope indicated by ``release_time`` and ``sustain_level`` -//| :param float attack_level: The relative level, in the range ``0.0`` to ``1.0`` of the peak volume of the attack phase -//| :param float sustain_level: The relative level, in the range ``0.0`` to ``1.0`` of the volume of the sustain phase +//| :param float attack_level: The level, in the range ``0.0`` to ``1.0`` of the peak volume of the attack phase +//| :param float sustain_level: The level, in the range ``0.0`` to ``1.0`` of the volume of the sustain phase relative to the attack level //| """ //| attack_time: float //| """The time in seconds it takes to ramp from 0 volume to attack_volume""" @@ -92,10 +92,10 @@ static const mp_arg_t envelope_properties[] = { //| """The time in seconds it takes to ramp from sustain_volume to release_volume. When a note is released before it has reached the sustain phase, the release is done with the same slope indicated by ``release_time`` and ``sustain_level``""" //| //| attack_level: float -//| """The relative level, in the range ``0.0`` to ``1.0`` of the peak volume of the attack phase""" +//| """The level, in the range ``0.0`` to ``1.0`` of the peak volume of the attack phase""" //| //| sustain_level: float -//| """The relative level, in the range ``0.0`` to ``1.0`` of the volume of the sustain phase""" +//| """The level, in the range ``0.0`` to ``1.0`` of the volume of the sustain phase relative to the attack level""" //| STATIC mp_obj_t synthio_envelope_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { diff --git a/shared-module/synthio/__init__.c b/shared-module/synthio/__init__.c index 4b32ec3e91..fc9d31f029 100644 --- a/shared-module/synthio/__init__.c +++ b/shared-module/synthio/__init__.c @@ -77,7 +77,7 @@ void synthio_envelope_definition_set(synthio_envelope_definition_t *envelope, mp mp_obj_tuple_get(obj, &len, &fields); envelope->attack_level = (int)(32767 * mp_obj_get_float(fields[3])); - envelope->sustain_level = (int)(32767 * mp_obj_get_float(fields[4])); + envelope->sustain_level = (int)(32767 * mp_obj_get_float(fields[4]) * mp_obj_get_float(fields[3])); envelope->attack_step = convert_time_to_rate( sample_rate, fields[0], envelope->attack_level); @@ -152,10 +152,10 @@ STATIC uint32_t synthio_synth_sum_envelope(synthio_synth_t *synth) { for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) { if (synth->span.note_obj[chan] != SYNTHIO_SILENCE) { synthio_envelope_state_t *state = &synth->envelope_state[chan]; - if (state->state == SYNTHIO_ENVELOPE_STATE_RELEASE) { - result += synth->envelope_definition.sustain_level; - } else { + if (state->state == SYNTHIO_ENVELOPE_STATE_ATTACK) { result += state->level; + } else { + result += synth->envelope_definition.attack_level; } } } From 021aaa4599718901bd45c06c440c736a892126d0 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 4 May 2023 07:22:20 -0500 Subject: [PATCH 08/19] synthio: remove unused 'phase' from Note objects --- shared-module/synthio/Note.c | 1 - shared-module/synthio/Note.h | 1 - 2 files changed, 2 deletions(-) diff --git a/shared-module/synthio/Note.c b/shared-module/synthio/Note.c index d31acb31f0..b6357559ee 100644 --- a/shared-module/synthio/Note.c +++ b/shared-module/synthio/Note.c @@ -142,7 +142,6 @@ void synthio_note_recalculate(synthio_note_obj_t *self, int32_t sample_rate) { void synthio_note_start(synthio_note_obj_t *self, int32_t sample_rate) { synthio_note_recalculate(self, sample_rate); - self->phase = 0; } uint32_t synthio_note_envelope(synthio_note_obj_t *self) { diff --git a/shared-module/synthio/Note.h b/shared-module/synthio/Note.h index 4b66997996..4efb197c07 100644 --- a/shared-module/synthio/Note.h +++ b/shared-module/synthio/Note.h @@ -37,7 +37,6 @@ typedef struct synthio_note_obj { int32_t sample_rate; - int32_t phase; int32_t frequency_scaled; int32_t amplitude_scaled; synthio_lfo_descr_t tremolo_descr, vibrato_descr; From 2b0231e9d313a4657a95b4a4a5c364f94c582859 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 4 May 2023 07:23:17 -0500 Subject: [PATCH 09/19] synthio: re-striking a note should re-enter attack .. without changing the current note amplitude --- shared-module/synthio/__init__.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/shared-module/synthio/__init__.c b/shared-module/synthio/__init__.c index fc9d31f029..6a9911aa9f 100644 --- a/shared-module/synthio/__init__.c +++ b/shared-module/synthio/__init__.c @@ -372,9 +372,8 @@ STATIC int find_channel_with_note(synthio_synth_t *synth, mp_obj_t note) { bool synthio_span_change_note(synthio_synth_t *synth, mp_obj_t old_note, mp_obj_t new_note) { int channel; if (new_note != SYNTHIO_SILENCE && (channel = find_channel_with_note(synth, new_note)) != -1) { - // note already playing, re-strike - synthio_envelope_state_init(&synth->envelope_state[channel], &synth->envelope_definition); - synth->accum[channel] = 0; + // note already playing, re-enter attack phase + synth->envelope_state[channel].state = SYNTHIO_ENVELOPE_STATE_ATTACK; return true; } channel = find_channel_with_note(synth, old_note); From e23e7d3b3f7779f2157bbef2e0f447e7706cbdc4 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 4 May 2023 07:45:45 -0500 Subject: [PATCH 10/19] synthio: get_buffer: return error if object deinited this may fix a weird crash during shutdown --- shared-module/synthio/MidiTrack.c | 4 ++++ shared-module/synthio/Synthesizer.c | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/shared-module/synthio/MidiTrack.c b/shared-module/synthio/MidiTrack.c index 343a107db2..66f28409d9 100644 --- a/shared-module/synthio/MidiTrack.c +++ b/shared-module/synthio/MidiTrack.c @@ -158,6 +158,10 @@ void synthio_miditrack_reset_buffer(synthio_miditrack_obj_t *self, audioio_get_buffer_result_t synthio_miditrack_get_buffer(synthio_miditrack_obj_t *self, bool single_channel_output, uint8_t channel, uint8_t **buffer, uint32_t *buffer_length) { + if (common_hal_synthio_miditrack_deinited(self)) { + *buffer_length = 0; + return GET_BUFFER_ERROR; + } synthio_synth_synthesize(&self->synth, buffer, buffer_length, single_channel_output ? 0 : channel); if (self->synth.span.dur == 0) { diff --git a/shared-module/synthio/Synthesizer.c b/shared-module/synthio/Synthesizer.c index 02d98a521e..e7fb324d25 100644 --- a/shared-module/synthio/Synthesizer.c +++ b/shared-module/synthio/Synthesizer.c @@ -62,6 +62,10 @@ void synthio_synthesizer_reset_buffer(synthio_synthesizer_obj_t *self, 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) { + if (common_hal_synthio_synthesizer_deinited(self)) { + *buffer_length = 0; + return GET_BUFFER_ERROR; + } self->synth.span.dur = SYNTHIO_MAX_DUR; synthio_synth_synthesize(&self->synth, buffer, buffer_length, single_channel_output ? channel : 0); return GET_BUFFER_MORE_DATA; From a388a59543550359226d40fbe6b7dc9652f70048 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 4 May 2023 08:41:23 -0500 Subject: [PATCH 11/19] rp2040: fix audio glitch at soft-reload The internal flash cache wasn't being properly used, because `write_blocks` unconditionally performed the flash write. Fixing this so that the write's not done until `internal_flash_flush` fixes the problem in my test program with i2sout & synthio. as a future optimization, `flash_read_blocks` could learn to read out of the cache, but that's probably not super important. --- ports/raspberrypi/supervisor/internal_flash.c | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/ports/raspberrypi/supervisor/internal_flash.c b/ports/raspberrypi/supervisor/internal_flash.c index 010528559d..9216f1ebe2 100644 --- a/ports/raspberrypi/supervisor/internal_flash.c +++ b/ports/raspberrypi/supervisor/internal_flash.c @@ -95,14 +95,16 @@ void port_internal_flash_flush(void) { if (_cache_lba == NO_CACHE) { return; } + // Make sure we don't have an interrupt while we do flash operations. common_hal_mcu_disable_interrupts(); flash_range_erase(CIRCUITPY_CIRCUITPY_DRIVE_START_ADDR + _cache_lba, SECTOR_SIZE); flash_range_program(CIRCUITPY_CIRCUITPY_DRIVE_START_ADDR + _cache_lba, _cache, SECTOR_SIZE); - common_hal_mcu_enable_interrupts(); _cache_lba = NO_CACHE; + common_hal_mcu_enable_interrupts(); } mp_uint_t supervisor_flash_read_blocks(uint8_t *dest, uint32_t block, uint32_t num_blocks) { + port_internal_flash_flush(); // we never read out of the cache, so we have to write it if dirty memcpy(dest, (void *)(XIP_BASE + CIRCUITPY_CIRCUITPY_DRIVE_START_ADDR + block * FILESYSTEM_BLOCK_SIZE), num_blocks * FILESYSTEM_BLOCK_SIZE); @@ -118,6 +120,7 @@ mp_uint_t supervisor_flash_write_blocks(const uint8_t *src, uint32_t lba, uint32 uint8_t block_offset = block_address % blocks_per_sector; if (_cache_lba != block_address) { + port_internal_flash_flush(); memcpy(_cache, (void *)(XIP_BASE + CIRCUITPY_CIRCUITPY_DRIVE_START_ADDR + sector_offset), SECTOR_SIZE); @@ -133,11 +136,6 @@ mp_uint_t supervisor_flash_write_blocks(const uint8_t *src, uint32_t lba, uint32 FILESYSTEM_BLOCK_SIZE); block++; } - // Make sure we don't have an interrupt while we do flash operations. - common_hal_mcu_disable_interrupts(); - flash_range_erase(CIRCUITPY_CIRCUITPY_DRIVE_START_ADDR + sector_offset, SECTOR_SIZE); - flash_range_program(CIRCUITPY_CIRCUITPY_DRIVE_START_ADDR + sector_offset, _cache, SECTOR_SIZE); - common_hal_mcu_enable_interrupts(); } return 0; // success From c839888e28c7064fb7bd6698488f1d66c3481434 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 4 May 2023 08:45:31 -0500 Subject: [PATCH 12/19] synthio: rename function that records error position --- shared-module/synthio/MidiTrack.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/shared-module/synthio/MidiTrack.c b/shared-module/synthio/MidiTrack.c index 66f28409d9..e22753cb26 100644 --- a/shared-module/synthio/MidiTrack.c +++ b/shared-module/synthio/MidiTrack.c @@ -28,7 +28,7 @@ #include "shared-bindings/synthio/MidiTrack.h" -STATIC void print_midi_stream_error(synthio_miditrack_obj_t *self) { +STATIC void record_midi_stream_error(synthio_miditrack_obj_t *self) { self->error_location = self->pos; self->pos = self->track.len; } @@ -37,11 +37,11 @@ STATIC mp_obj_t parse_note(synthio_miditrack_obj_t *self) { uint8_t *buffer = self->track.buf; size_t len = self->track.len; if (self->pos + 1 >= len) { - print_midi_stream_error(self); + record_midi_stream_error(self); } uint8_t note = buffer[(self->pos)++]; if (note > 127 || buffer[(self->pos)++] > 127) { - print_midi_stream_error(self); + record_midi_stream_error(self); } return MP_OBJ_NEW_SMALL_INT(note); } @@ -60,7 +60,7 @@ static int decode_duration(synthio_miditrack_obj_t *self) { // errors cannot be raised from the background task, so simply end the track. if (c & 0x80) { self->pos = self->track.len; - print_midi_stream_error(self); + record_midi_stream_error(self); } return delta * self->synth.sample_rate / self->tempo; } @@ -89,14 +89,14 @@ static void decode_until_pause(synthio_miditrack_obj_t *self) { case 12: case 13: // one data byte to ignore if (self->pos >= len || buffer[self->pos++] > 127) { - print_midi_stream_error(self); + record_midi_stream_error(self); } break; case 15: // the full syntax is too complicated, just assume it's "End of Track" event self->pos = len; break; default: // invalid event - print_midi_stream_error(self); + record_midi_stream_error(self); } if (self->pos < len) { self->synth.span.dur = decode_duration(self); From a94031d094a44fa157a480351225c3da9af428c3 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 4 May 2023 10:16:19 -0500 Subject: [PATCH 13/19] synthio: make most Note constructor args kw-only for similar reasons as Envelope. The mandatory frequency argument can still be given as a positional argument. --- shared-bindings/synthio/Note.c | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/shared-bindings/synthio/Note.c b/shared-bindings/synthio/Note.c index 0e2dad40d6..427f65097b 100644 --- a/shared-bindings/synthio/Note.c +++ b/shared-bindings/synthio/Note.c @@ -35,17 +35,18 @@ static const mp_arg_t note_properties[] = { { MP_QSTR_frequency, MP_ARG_OBJ | MP_ARG_REQUIRED, {.u_obj = NULL } }, - { MP_QSTR_amplitude, MP_ARG_OBJ, {.u_obj = MP_ROM_INT(1) } }, - { MP_QSTR_tremolo_rate, MP_ARG_OBJ, {.u_obj = NULL } }, - { MP_QSTR_tremolo_depth, MP_ARG_OBJ, {.u_obj = NULL } }, - { MP_QSTR_vibrato_rate, MP_ARG_OBJ, {.u_obj = NULL } }, - { MP_QSTR_vibrato_depth, MP_ARG_OBJ, {.u_obj = NULL } }, - { MP_QSTR_waveform, MP_ARG_OBJ, {.u_obj = MP_ROM_NONE } }, - { MP_QSTR_envelope, MP_ARG_OBJ, {.u_obj = MP_ROM_NONE } }, + { MP_QSTR_amplitude, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_INT(1) } }, + { MP_QSTR_tremolo_rate, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = NULL } }, + { MP_QSTR_tremolo_depth, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = NULL } }, + { MP_QSTR_vibrato_rate, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = NULL } }, + { MP_QSTR_vibrato_depth, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = NULL } }, + { MP_QSTR_waveform, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_NONE } }, + { MP_QSTR_envelope, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_NONE } }, }; //| class Note: //| def __init__( //| self, +//| *, //| frequency: float, //| amplitude: float = 1.0, //| waveform: Optional[ReadableBuffer] = None, From eebd4a7f521541d850a6d5aaaed9fd3c1cb04a0d Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 4 May 2023 10:16:58 -0500 Subject: [PATCH 14/19] synthio: no longer fits on feather m4 can --- ports/atmel-samd/boards/feather_m4_can/mpconfigboard.mk | 1 + 1 file changed, 1 insertion(+) diff --git a/ports/atmel-samd/boards/feather_m4_can/mpconfigboard.mk b/ports/atmel-samd/boards/feather_m4_can/mpconfigboard.mk index 1c337d6256..8c9779e5f9 100644 --- a/ports/atmel-samd/boards/feather_m4_can/mpconfigboard.mk +++ b/ports/atmel-samd/boards/feather_m4_can/mpconfigboard.mk @@ -12,5 +12,6 @@ LONGINT_IMPL = MPZ CIRCUITPY__EVE = 1 CIRCUITPY_CANIO = 1 +CIRCUITPY_SYNTHIO = 0 CIRCUITPY_LTO_PARTITION = one From d2aca7eba072710a91d27be95aef1801b58ccf67 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 4 May 2023 12:23:45 -0500 Subject: [PATCH 15/19] synthio: fix per-note envelope & envelope modification .. and simplify the envelope advance logic by handling 'instant' values more intelligently. --- shared-module/synthio/Note.c | 3 ++ shared-module/synthio/Synthesizer.c | 9 ++-- shared-module/synthio/__init__.c | 77 +++++++++++++---------------- shared-module/synthio/__init__.h | 2 +- 4 files changed, 41 insertions(+), 50 deletions(-) diff --git a/shared-module/synthio/Note.c b/shared-module/synthio/Note.c index b6357559ee..e86253cdd1 100644 --- a/shared-module/synthio/Note.c +++ b/shared-module/synthio/Note.c @@ -105,6 +105,9 @@ mp_obj_t common_hal_synthio_note_get_envelope_obj(synthio_note_obj_t *self) { void common_hal_synthio_note_set_envelope(synthio_note_obj_t *self, mp_obj_t envelope_in) { if (envelope_in != mp_const_none) { mp_arg_validate_type(envelope_in, (mp_obj_type_t *)&synthio_envelope_type_obj, MP_QSTR_envelope); + if (self->sample_rate != 0) { + synthio_envelope_definition_set(&self->envelope_def, envelope_in, self->sample_rate); + } } self->envelope_obj = envelope_in; } diff --git a/shared-module/synthio/Synthesizer.c b/shared-module/synthio/Synthesizer.c index e7fb324d25..ad37cccce5 100644 --- a/shared-module/synthio/Synthesizer.c +++ b/shared-module/synthio/Synthesizer.c @@ -110,12 +110,11 @@ void common_hal_synthio_synthesizer_press(synthio_synthesizer_obj_t *self, mp_ob mp_obj_t iterable = mp_getiter(to_press, &iter_buf); mp_obj_t note_obj; while ((note_obj = mp_iternext(iterable)) != MP_OBJ_STOP_ITERATION) { - if (synthio_span_change_note(&self->synth, SYNTHIO_SILENCE, validate_note(note_obj))) { - if (!mp_obj_is_small_int(note_obj)) { - synthio_note_obj_t *note = MP_OBJ_TO_PTR(note_obj); - synthio_note_start(note, self->synth.sample_rate); - } + if (!mp_obj_is_small_int(note_obj)) { + synthio_note_obj_t *note = MP_OBJ_TO_PTR(note_obj); + synthio_note_start(note, self->synth.sample_rate); } + synthio_span_change_note(&self->synth, SYNTHIO_SILENCE, validate_note(note_obj)); } } diff --git a/shared-module/synthio/__init__.c b/shared-module/synthio/__init__.c index 6a9911aa9f..d5efb0f842 100644 --- a/shared-module/synthio/__init__.c +++ b/shared-module/synthio/__init__.c @@ -37,11 +37,11 @@ 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 int32_t round_float_to_int(mp_float_t f) { +STATIC int32_t round_float_to_int(mp_float_t f) { return (int32_t)(f + MICROPY_FLOAT_CONST(0.5)); } -static int64_t round_float_to_int64(mp_float_t f) { +STATIC int64_t round_float_to_int64(mp_float_t f) { return (int64_t)(f + MICROPY_FLOAT_CONST(0.5)); } @@ -55,7 +55,7 @@ STATIC int16_t convert_time_to_rate(uint32_t sample_rate, mp_obj_t time_in, int1 mp_float_t time = mp_obj_get_float(time_in); int num_samples = (int)MICROPY_FLOAT_C_FUN(round)(time * sample_rate); if (num_samples == 0) { - return 0; + return 32767; } int16_t result = MIN(32767, MAX(1, abs(difference * SYNTHIO_MAX_DUR) / num_samples)); return (difference < 0) ? -result : result; @@ -102,34 +102,19 @@ STATIC void synthio_envelope_state_step(synthio_envelope_state_t *state, synthio case SYNTHIO_ENVELOPE_STATE_SUSTAIN: break; case SYNTHIO_ENVELOPE_STATE_ATTACK: - if (def->attack_step != 0) { - state->level = MIN(state->level + def->attack_step, def->attack_level); - if (state->level == def->attack_level) { - state->state = SYNTHIO_ENVELOPE_STATE_DECAY; - } - break; - } - state->state = SYNTHIO_ENVELOPE_STATE_DECAY; - MP_FALLTHROUGH; - case SYNTHIO_ENVELOPE_STATE_DECAY: - if (def->decay_step != 0) { - state->level = MAX(state->level + def->decay_step, def->sustain_level); - assert(state->level >= 0); - if (state->level == def->sustain_level) { - state->state = SYNTHIO_ENVELOPE_STATE_SUSTAIN; - } - break; - } - state->state = SYNTHIO_ENVELOPE_STATE_RELEASE; - MP_FALLTHROUGH; - case SYNTHIO_ENVELOPE_STATE_RELEASE: - if (def->release_step != 0) { - int delta = def->release_step; - state->level = MAX(state->level + delta, 0); - } else { - state->level = 0; + state->level = MIN(state->level + def->attack_step, def->attack_level); + if (state->level == def->attack_level) { + state->state = SYNTHIO_ENVELOPE_STATE_DECAY; } break; + case SYNTHIO_ENVELOPE_STATE_DECAY: + state->level = MAX(state->level + def->decay_step, def->sustain_level); + if (state->level == def->sustain_level) { + state->state = SYNTHIO_ENVELOPE_STATE_SUSTAIN; + } + break; + case SYNTHIO_ENVELOPE_STATE_RELEASE: + state->level = MAX(state->level + def->release_step, 0); } } } @@ -146,23 +131,34 @@ STATIC void synthio_envelope_state_release(synthio_envelope_state_t *state, synt state->state = SYNTHIO_ENVELOPE_STATE_RELEASE; } +STATIC synthio_envelope_definition_t *synthio_synth_get_note_envelope(synthio_synth_t *synth, mp_obj_t note_obj) { + synthio_envelope_definition_t *def = &synth->global_envelope_definition; + if (!mp_obj_is_small_int(note_obj)) { + synthio_note_obj_t *note = MP_OBJ_TO_PTR(note_obj); + if (note->envelope_obj != mp_const_none) { + def = ¬e->envelope_def; + } + } + return def; +} + STATIC uint32_t synthio_synth_sum_envelope(synthio_synth_t *synth) { uint32_t result = 0; for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) { - if (synth->span.note_obj[chan] != SYNTHIO_SILENCE) { + mp_obj_t note_obj = synth->span.note_obj[chan]; + if (note_obj != SYNTHIO_SILENCE) { synthio_envelope_state_t *state = &synth->envelope_state[chan]; if (state->state == SYNTHIO_ENVELOPE_STATE_ATTACK) { result += state->level; } else { - result += synth->envelope_definition.attack_level; + result += synthio_synth_get_note_envelope(synth, note_obj)->attack_level; } } } return result; } - void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t *buffer_length, uint8_t channel) { if (channel == synth->other_channel) { @@ -265,14 +261,7 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t if (note_obj == SYNTHIO_SILENCE) { continue; } - synthio_envelope_definition_t *def = &synth->envelope_definition; - if (!mp_obj_is_small_int(note_obj)) { - synthio_note_obj_t *note = MP_OBJ_TO_PTR(note_obj); - if (note->envelope_obj != mp_const_none) { - def = ¬e->envelope_def; - } - } - synthio_envelope_state_step(&synth->envelope_state[chan], def, dur); + synthio_envelope_state_step(&synth->envelope_state[chan], synthio_synth_get_note_envelope(synth, note_obj), dur); } *buffer_length = synth->last_buffer_length = dur * SYNTHIO_BYTES_PER_SAMPLE; @@ -298,7 +287,7 @@ void synthio_synth_deinit(synthio_synth_t *synth) { } void synthio_synth_envelope_set(synthio_synth_t *synth, mp_obj_t envelope_obj) { - synthio_envelope_definition_set(&synth->envelope_definition, envelope_obj, synth->sample_rate); + synthio_envelope_definition_set(&synth->global_envelope_definition, envelope_obj, synth->sample_rate); synth->envelope_obj = envelope_obj; } @@ -329,7 +318,7 @@ void synthio_synth_get_buffer_structure(synthio_synth_t *synth, bool single_chan *spacing = 1; } -static bool parse_common(mp_buffer_info_t *bufinfo, mp_obj_t o, int16_t what) { +STATIC bool parse_common(mp_buffer_info_t *bufinfo, mp_obj_t o, int16_t what) { if (o != mp_const_none) { mp_get_buffer_raise(o, bufinfo, MP_BUFFER_READ); if (bufinfo->typecode != 'h') { @@ -379,10 +368,10 @@ bool synthio_span_change_note(synthio_synth_t *synth, mp_obj_t old_note, mp_obj_ channel = find_channel_with_note(synth, old_note); if (channel != -1) { if (new_note == SYNTHIO_SILENCE) { - synthio_envelope_state_release(&synth->envelope_state[channel], &synth->envelope_definition); + synthio_envelope_state_release(&synth->envelope_state[channel], synthio_synth_get_note_envelope(synth, old_note)); } else { synth->span.note_obj[channel] = new_note; - synthio_envelope_state_init(&synth->envelope_state[channel], &synth->envelope_definition); + synthio_envelope_state_init(&synth->envelope_state[channel], synthio_synth_get_note_envelope(synth, new_note)); synth->accum[channel] = 0; } return true; diff --git a/shared-module/synthio/__init__.h b/shared-module/synthio/__init__.h index 29520dfb5b..4ea04a09e3 100644 --- a/shared-module/synthio/__init__.h +++ b/shared-module/synthio/__init__.h @@ -69,7 +69,7 @@ typedef struct synthio_synth { uint16_t last_buffer_length; uint8_t other_channel, buffer_index, other_buffer_index; uint16_t waveform_length; - synthio_envelope_definition_t envelope_definition; + synthio_envelope_definition_t global_envelope_definition; mp_obj_t envelope_obj; synthio_midi_span_t span; uint32_t accum[CIRCUITPY_SYNTHIO_MAX_CHANNELS]; From a854e554f4f2492aaa881da659c601afdeef047e Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Fri, 5 May 2023 18:25:51 -0500 Subject: [PATCH 16/19] synthio: add midi_to_hz to docs --- shared-bindings/synthio/__init__.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/shared-bindings/synthio/__init__.c b/shared-bindings/synthio/__init__.c index 5af352b404..b48e279028 100644 --- a/shared-bindings/synthio/__init__.c +++ b/shared-bindings/synthio/__init__.c @@ -262,6 +262,10 @@ STATIC mp_obj_t synthio_from_file(size_t n_args, const mp_obj_t *pos_args, mp_ma } MP_DEFINE_CONST_FUN_OBJ_KW(synthio_from_file_obj, 1, synthio_from_file); +//| def midi_to_hz(midi_note: int) -> float: +//| """Converts the given midi note (60 = middle C, 69 = concert A) to Hz""" +//| +/ STATIC mp_obj_t midi_to_hz(mp_obj_t arg) { mp_int_t note = mp_arg_validate_int_range(mp_obj_get_int(arg), 1, 127, MP_QSTR_note); return mp_obj_new_float(common_hal_synthio_midi_to_hz_float(note)); From a53c0ed0663ce0b15e7ea7089b9b7c54c62d7b7f Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Sat, 6 May 2023 08:35:48 -0500 Subject: [PATCH 17/19] synthio: add onevo_to_hz, implement midi_to_hz in terms of it this has the side effect of making some notes more accurate, the new frequency= value in the test is closer to the true midi frequency of 830.609...Hz. --- shared-bindings/synthio/__init__.c | 20 +++++++++++++++++--- shared-bindings/synthio/__init__.h | 3 ++- shared-module/synthio/__init__.c | 10 ++++++---- tests/circuitpython/synthesizer_note.py.exp | 6 +++--- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/shared-bindings/synthio/__init__.c b/shared-bindings/synthio/__init__.c index b48e279028..2e4fd7b8a4 100644 --- a/shared-bindings/synthio/__init__.c +++ b/shared-bindings/synthio/__init__.c @@ -262,16 +262,29 @@ STATIC mp_obj_t synthio_from_file(size_t n_args, const mp_obj_t *pos_args, mp_ma } MP_DEFINE_CONST_FUN_OBJ_KW(synthio_from_file_obj, 1, synthio_from_file); -//| def midi_to_hz(midi_note: int) -> float: +//| def midi_to_hz(midi_note: float) -> float: //| """Converts the given midi note (60 = middle C, 69 = concert A) to Hz""" //| -/ + STATIC mp_obj_t midi_to_hz(mp_obj_t arg) { - mp_int_t note = mp_arg_validate_int_range(mp_obj_get_int(arg), 1, 127, MP_QSTR_note); + mp_float_t note = mp_arg_validate_obj_float_range(arg, 1, 127, MP_QSTR_note); return mp_obj_new_float(common_hal_synthio_midi_to_hz_float(note)); } MP_DEFINE_CONST_FUN_OBJ_1(synthio_midi_to_hz_obj, midi_to_hz); +//| def onevo_to_hz(ctrl: float) -> float: +//| """Converts a 1v/octave signal to Hz. +//| +//| 60/12 (5.0) corresponds to middle C, 69/12 is concert A.""" +//| + +STATIC mp_obj_t onevo_to_hz(mp_obj_t arg) { + mp_float_t note = mp_arg_validate_obj_float_range(arg, 0, 11, MP_QSTR_ctrl); + return mp_obj_new_float(common_hal_synthio_onevo_to_hz_float(note)); +} +MP_DEFINE_CONST_FUN_OBJ_1(synthio_onevo_to_hz_obj, onevo_to_hz); + + 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) }, @@ -280,6 +293,7 @@ STATIC const mp_rom_map_elem_t synthio_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR_from_file), MP_ROM_PTR(&synthio_from_file_obj) }, { MP_ROM_QSTR(MP_QSTR_Envelope), MP_ROM_PTR(&synthio_envelope_type_obj) }, { MP_ROM_QSTR(MP_QSTR_midi_to_hz), MP_ROM_PTR(&synthio_midi_to_hz_obj) }, + { MP_ROM_QSTR(MP_QSTR_onevo_to_hz), MP_ROM_PTR(&synthio_midi_to_hz_obj) }, }; STATIC MP_DEFINE_CONST_DICT(synthio_module_globals, synthio_module_globals_table); diff --git a/shared-bindings/synthio/__init__.h b/shared-bindings/synthio/__init__.h index d3ccc1ce04..37a5c77b34 100644 --- a/shared-bindings/synthio/__init__.h +++ b/shared-bindings/synthio/__init__.h @@ -33,4 +33,5 @@ extern int16_t shared_bindings_synthio_square_wave[]; extern const mp_obj_namedtuple_type_t synthio_envelope_type_obj; void synthio_synth_envelope_set(synthio_synth_t *synth, mp_obj_t envelope_obj); mp_obj_t synthio_synth_envelope_get(synthio_synth_t *synth); -mp_float_t common_hal_synthio_midi_to_hz_float(mp_int_t note); +mp_float_t common_hal_synthio_midi_to_hz_float(mp_float_t note); +mp_float_t common_hal_synthio_onevo_to_hz_float(mp_float_t note); diff --git a/shared-module/synthio/__init__.c b/shared-module/synthio/__init__.c index d5efb0f842..596f3fb184 100644 --- a/shared-module/synthio/__init__.c +++ b/shared-module/synthio/__init__.c @@ -45,10 +45,12 @@ STATIC int64_t round_float_to_int64(mp_float_t f) { return (int64_t)(f + MICROPY_FLOAT_CONST(0.5)); } -mp_float_t common_hal_synthio_midi_to_hz_float(mp_int_t arg) { - uint8_t octave = arg / 12; - uint16_t base_freq = notes[arg % 12]; - return MICROPY_FLOAT_C_FUN(ldexp)(base_freq, octave - 10); +mp_float_t common_hal_synthio_midi_to_hz_float(mp_float_t arg) { + return common_hal_synthio_onevo_to_hz_float(arg / 12.); +} + +mp_float_t common_hal_synthio_onevo_to_hz_float(mp_float_t octave) { + return notes[0] * MICROPY_FLOAT_C_FUN(pow)(2., octave - 10); } STATIC int16_t convert_time_to_rate(uint32_t sample_rate, mp_obj_t time_in, int16_t difference) { diff --git a/tests/circuitpython/synthesizer_note.py.exp b/tests/circuitpython/synthesizer_note.py.exp index 82ae685c07..e51cea1755 100644 --- a/tests/circuitpython/synthesizer_note.py.exp +++ b/tests/circuitpython/synthesizer_note.py.exp @@ -1,10 +1,10 @@ () [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] -(Note(frequency=830.625, amplitude=1.0, tremolo_rate=0.0, tremolo_depth=0.0, vibrato_rate=0.0, vibrato_depth=0.0, waveform=None, envelope=None),) +(Note(frequency=830.6076004423605, amplitude=1.0, tremolo_rate=0.0, tremolo_depth=0.0, vibrato_rate=0.0, vibrato_depth=0.0, waveform=None, envelope=None),) [-16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383] -(Note(frequency=830.625, amplitude=1.0, tremolo_rate=0.0, tremolo_depth=0.0, vibrato_rate=0.0, vibrato_depth=0.0, waveform=None, envelope=None), Note(frequency=830.625, amplitude=1.0, tremolo_rate=0.0, tremolo_depth=0.0, vibrato_rate=0.0, vibrato_depth=0.0, waveform=None, envelope=None)) +(Note(frequency=830.6076004423605, amplitude=1.0, tremolo_rate=0.0, tremolo_depth=0.0, vibrato_rate=0.0, vibrato_depth=0.0, waveform=None, envelope=None), Note(frequency=830.6076004423605, amplitude=1.0, tremolo_rate=0.0, tremolo_depth=0.0, vibrato_rate=0.0, vibrato_depth=0.0, waveform=None, envelope=None)) [0, 0, 0, 0, 0, 0, 0, 0, 16382, 0, 0, 0, 0, -16382, 0, 0, 0, 0, 16382, 0, 0, 0, 0, -16382] -(Note(frequency=830.625, amplitude=1.0, tremolo_rate=0.0, tremolo_depth=0.0, vibrato_rate=0.0, vibrato_depth=0.0, waveform=None, envelope=None),) +(Note(frequency=830.6076004423605, amplitude=1.0, tremolo_rate=0.0, tremolo_depth=0.0, vibrato_rate=0.0, vibrato_depth=0.0, waveform=None, envelope=None),) [0, 0, 0, 16382, 0, 0, 0, 0, 0, 0, 0, 0, 16382, 0, 0, 0, 0, -16382, 0, 0, 0, 0, 16382, 0] (-5242, 5241) (-10484, 10484) From 9a9f3229fa4559865bdaa9b44995bf3ea5abaac3 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Sat, 6 May 2023 21:34:48 -0500 Subject: [PATCH 18/19] synthio: Perform vibrato in pitch, not as frequency ratio Now the vibrato 'units' are 1.0 = one octave, 1/12 = one semitone, 1/1200 = one cent. Before, the units were somewhat arbitrary and were not perceptually "symmetrical" around the base frequency. For vibrato_depth = 1/12 and base frequency of 440, before: pitch from 403.33 to 476.67Hz, not corresponding to any notes after: pitch from 415.30 to 466.16Hz, corresponding to G# and A# --- shared-bindings/synthio/Note.c | 14 ++++++++++++-- shared-module/synthio/Note.c | 27 ++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/shared-bindings/synthio/Note.c b/shared-bindings/synthio/Note.c index 427f65097b..4cd0abe0d4 100644 --- a/shared-bindings/synthio/Note.c +++ b/shared-bindings/synthio/Note.c @@ -118,7 +118,12 @@ MP_PROPERTY_GETSET(synthio_note_amplitude_obj, //| tremolo_depth: float -//| """The tremolo depth of the note, from 0 to 1""" +//| """The tremolo depth of the note, from 0 to 1 +//| +//| A depth of 0 disables tremolo. A nonzero value enables tremolo, +//| with the maximum decrease in amplitude being equal to the tremolo +//| depth. A note with a tremolo depth of 1 will fade out to nothing, while +//| a tremolo depth of 0.1 will give a minimum amplitude of 0.9.""" STATIC mp_obj_t synthio_note_get_tremolo_depth(mp_obj_t self_in) { synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); return mp_obj_new_float(common_hal_synthio_note_get_tremolo_depth(self)); @@ -154,7 +159,12 @@ MP_PROPERTY_GETSET(synthio_note_tremolo_rate_obj, (mp_obj_t)&synthio_note_set_tremolo_rate_obj); //| vibrato_depth: float -//| """The vibrato depth of the note, from 0 to 1""" +//| """The vibrato depth of the note, from 0 to 1 +//| +//| A depth of 0 disables vibrato. A depth of 1 corresponds to a vibrato of ±1 +//| octave. A depth of (1/12) = 0.833 corresponds to a vibrato of ±1 semitone, +//| and a depth of .00833 corresponds to one musical cent. +//| """ STATIC mp_obj_t synthio_note_get_vibrato_depth(mp_obj_t self_in) { synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); return mp_obj_new_float(common_hal_synthio_note_get_vibrato_depth(self)); diff --git a/shared-module/synthio/Note.c b/shared-module/synthio/Note.c index e86253cdd1..84949163a7 100644 --- a/shared-module/synthio/Note.c +++ b/shared-module/synthio/Note.c @@ -151,10 +151,35 @@ uint32_t synthio_note_envelope(synthio_note_obj_t *self) { return self->amplitude_scaled; } +// Perform a pitch bend operation +// +// bend_value is in the range [0, 65535]. "no change" is 32768. The bend unit is 32768/octave. +// +// compare to (frequency_scaled * pow(2, (bend_value-32768)/32768)) +// a 13-entry pitch table +#define BEND_SCALE (32768) +#define BEND_OFFSET (BEND_SCALE) + +STATIC uint16_t pitch_bend_table[] = { 0, 1948, 4013, 6200, 8517, 10972, 13573, 16329, 19248, 22341, 25618, 29090, 32768 }; + +STATIC uint32_t pitch_bend(uint32_t frequency_scaled, uint16_t bend_value) { + bool down = (bend_value < 32768); + if (!down) { + bend_value -= 32768; + } + uint32_t bend_value_semitone = (uint32_t)bend_value * 24; // 65536/semitone + uint32_t semitone = bend_value_semitone >> 16; + uint32_t fractone = bend_value_semitone & 0xffff; + uint32_t f_lo = pitch_bend_table[semitone]; + uint32_t f_hi = pitch_bend_table[semitone + 1]; // table has 13 entries, indexing with semitone=12 is OK + uint32_t f = ((f_lo * (65535 - fractone) + f_hi * fractone) >> 16) + BEND_OFFSET; + return (frequency_scaled * (uint64_t)f) >> (15 + down); +} + uint32_t synthio_note_step(synthio_note_obj_t *self, int32_t sample_rate, int16_t dur, uint16_t *loudness) { int tremolo_value = synthio_lfo_step(&self->tremolo_state, dur); int vibrato_value = synthio_lfo_step(&self->vibrato_state, dur); *loudness = (*loudness * tremolo_value) >> 15; - uint32_t frequency_scaled = ((uint64_t)self->frequency_scaled * vibrato_value) >> 15; + uint32_t frequency_scaled = pitch_bend(self->frequency_scaled, vibrato_value); return frequency_scaled; } From c031bda5dd78294ba6498d56cfa4ab8c511bdbb3 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Mon, 8 May 2023 09:20:46 -0500 Subject: [PATCH 19/19] synthio: implement a range compressor with hard knee This really improves the loudness of the output with multiple notes while being a nice simple algorithm to implement. --- shared-module/synthio/__init__.c | 184 +++++++++--------- .../circuitpython-manual/synthio/note/code.py | 21 +- tests/circuitpython/miditrack.py.exp | 4 +- tests/circuitpython/synthesizer.py.exp | 44 ++--- tests/circuitpython/synthesizer_note.py.exp | 44 ++--- 5 files changed, 160 insertions(+), 137 deletions(-) diff --git a/shared-module/synthio/__init__.c b/shared-module/synthio/__init__.c index 596f3fb184..5ee72aeb79 100644 --- a/shared-module/synthio/__init__.c +++ b/shared-module/synthio/__init__.c @@ -145,20 +145,30 @@ STATIC synthio_envelope_definition_t *synthio_synth_get_note_envelope(synthio_sy } -STATIC uint32_t synthio_synth_sum_envelope(synthio_synth_t *synth) { - uint32_t result = 0; - for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) { - mp_obj_t note_obj = synth->span.note_obj[chan]; - if (note_obj != SYNTHIO_SILENCE) { - synthio_envelope_state_t *state = &synth->envelope_state[chan]; - if (state->state == SYNTHIO_ENVELOPE_STATE_ATTACK) { - result += state->level; - } else { - result += synthio_synth_get_note_envelope(synth, note_obj)->attack_level; - } - } +#define RANGE_LOW (-28000) +#define RANGE_HIGH (28000) +#define RANGE_SHIFT (16) +#define RANGE_SCALE (0xfffffff / (32768 * CIRCUITPY_SYNTHIO_MAX_CHANNELS - RANGE_HIGH)) + +// dynamic range compression via a downward compressor with hard knee +// +// When the output value is within the range +-28000 (about 85% of full scale), +// it is unchanged. Otherwise, it undergoes a gain reduction so that the +// largest possible values, (+32768,-32767) * CIRCUITPY_SYNTHIO_MAX_CHANNELS, +// still fit within the output range +// +// This produces a much louder overall volume with multiple voices, without +// much additional processing. +// +// https://en.wikipedia.org/wiki/Dynamic_range_compression +STATIC +int16_t mix_down_sample(int32_t sample) { + if (sample < RANGE_LOW) { + sample = (((sample - RANGE_LOW) * RANGE_SCALE) >> RANGE_SHIFT) + RANGE_LOW; + } else if (sample > RANGE_HIGH) { + sample = (((sample - RANGE_HIGH) * RANGE_SCALE) >> RANGE_SHIFT) + RANGE_HIGH; } - return result; + return sample; } void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t *buffer_length, uint8_t channel) { @@ -172,89 +182,83 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t synth->buffer_index = !synth->buffer_index; synth->other_channel = 1 - channel; synth->other_buffer_index = synth->buffer_index; - int16_t *out_buffer = (int16_t *)(void *)synth->buffers[synth->buffer_index]; uint16_t dur = MIN(SYNTHIO_MAX_DUR, synth->span.dur); synth->span.dur -= dur; - memset(out_buffer, 0, synth->buffer_length); int32_t sample_rate = synth->sample_rate; - uint32_t total_envelope = synthio_synth_sum_envelope(synth); - if (total_envelope < synth->total_envelope) { - // total envelope is decreasing. Slowly let remaining notes get louder - // the time constant is arbitrary, on the order of 1s at 48kHz - total_envelope = synth->total_envelope = ( - total_envelope + synth->total_envelope * 255) / 256; - } else { - // total envelope is steady or increasing, so just store this as - // the high water mark - synth->total_envelope = total_envelope; - } - if (total_envelope > 0) { - uint16_t ovl_loudness = 0x7fffffff / MAX(0x8000, total_envelope); + int32_t out_buffer32[dur]; - for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) { - mp_obj_t note_obj = synth->span.note_obj[chan]; - if (note_obj == SYNTHIO_SILENCE) { - synth->accum[chan] = 0; - continue; - } - - if (synth->envelope_state[chan].level == 0) { - // note is truly finished, but we only just noticed - synth->span.note_obj[chan] = SYNTHIO_SILENCE; - continue; - } - - // adjust loudness by envelope - uint16_t loudness = (ovl_loudness * synth->envelope_state[chan].level) >> 16; - - uint32_t dds_rate; - const int16_t *waveform = synth->waveform; - uint32_t waveform_length = synth->waveform_length; - if (mp_obj_is_small_int(note_obj)) { - uint8_t note = mp_obj_get_int(note_obj); - uint8_t octave = note / 12; - uint16_t base_freq = notes[note % 12]; - // rate = base_freq * waveform_length - // den = sample_rate * 2 ^ (10 - octave) - // den = sample_rate * 2 ^ 10 / 2^octave - // dds_rate = 2^SHIFT * rate / den - // dds_rate = 2^(SHIFT-10+octave) * base_freq * waveform_length / sample_rate - dds_rate = (sample_rate / 2 + ((uint64_t)(base_freq * waveform_length) << (SYNTHIO_FREQUENCY_SHIFT - 10 + octave))) / sample_rate; - } else { - synthio_note_obj_t *note = MP_OBJ_TO_PTR(note_obj); - int32_t frequency_scaled = synthio_note_step(note, sample_rate, dur, &loudness); - if (note->waveform_buf.buf) { - waveform = note->waveform_buf.buf; - waveform_length = note->waveform_buf.len / 2; - } - dds_rate = synthio_frequency_convert_scaled_to_dds((uint64_t)frequency_scaled * waveform_length, sample_rate); - } - - uint32_t accum = synth->accum[chan]; - uint32_t lim = waveform_length << SYNTHIO_FREQUENCY_SHIFT; - if (dds_rate > lim / 2) { - // beyond nyquist, can't play note - continue; - } - - // can happen if note waveform gets set mid-note, but the expensive modulo is usually avoided - if (accum > lim) { - accum %= lim; - } - - for (uint16_t i = 0; i < dur; i++) { - accum += dds_rate; - // because dds_rate is low enough, the subtraction is guaranteed to go back into range, no expensive modulo needed - if (accum > lim) { - accum -= lim; - } - int16_t idx = accum >> SYNTHIO_FREQUENCY_SHIFT; - out_buffer[i] += (waveform[idx] * loudness) / 65536; - } - synth->accum[chan] = accum; + memset(out_buffer32, 0, sizeof(out_buffer32)); + for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) { + mp_obj_t note_obj = synth->span.note_obj[chan]; + if (note_obj == SYNTHIO_SILENCE) { + synth->accum[chan] = 0; + continue; } + + if (synth->envelope_state[chan].level == 0) { + // note is truly finished, but we only just noticed + synth->span.note_obj[chan] = SYNTHIO_SILENCE; + continue; + } + + // adjust loudness by envelope + uint16_t loudness = synth->envelope_state[chan].level; + + uint32_t dds_rate; + const int16_t *waveform = synth->waveform; + uint32_t waveform_length = synth->waveform_length; + if (mp_obj_is_small_int(note_obj)) { + uint8_t note = mp_obj_get_int(note_obj); + uint8_t octave = note / 12; + uint16_t base_freq = notes[note % 12]; + // rate = base_freq * waveform_length + // den = sample_rate * 2 ^ (10 - octave) + // den = sample_rate * 2 ^ 10 / 2^octave + // dds_rate = 2^SHIFT * rate / den + // dds_rate = 2^(SHIFT-10+octave) * base_freq * waveform_length / sample_rate + dds_rate = (sample_rate / 2 + ((uint64_t)(base_freq * waveform_length) << (SYNTHIO_FREQUENCY_SHIFT - 10 + octave))) / sample_rate; + } else { + synthio_note_obj_t *note = MP_OBJ_TO_PTR(note_obj); + int32_t frequency_scaled = synthio_note_step(note, sample_rate, dur, &loudness); + if (note->waveform_buf.buf) { + waveform = note->waveform_buf.buf; + waveform_length = note->waveform_buf.len / 2; + } + dds_rate = synthio_frequency_convert_scaled_to_dds((uint64_t)frequency_scaled * waveform_length, sample_rate); + } + + uint32_t accum = synth->accum[chan]; + uint32_t lim = waveform_length << SYNTHIO_FREQUENCY_SHIFT; + if (dds_rate > lim / 2) { + // beyond nyquist, can't play note + continue; + } + + // can happen if note waveform gets set mid-note, but the expensive modulo is usually avoided + if (accum > lim) { + accum %= lim; + } + + for (uint16_t i = 0; i < dur; i++) { + accum += dds_rate; + // because dds_rate is low enough, the subtraction is guaranteed to go back into range, no expensive modulo needed + if (accum > lim) { + accum -= lim; + } + int16_t idx = accum >> SYNTHIO_FREQUENCY_SHIFT; + out_buffer32[i] += (waveform[idx] * loudness) / 65536; + } + synth->accum[chan] = accum; + } + + int16_t *out_buffer16 = (int16_t *)(void *)synth->buffers[synth->buffer_index]; + + // mix down audio + for (size_t i = 0; i < dur; i++) { + int32_t sample = out_buffer32[i]; + out_buffer16[i] = mix_down_sample(sample); } // advance envelope states @@ -267,7 +271,7 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t } *buffer_length = synth->last_buffer_length = dur * SYNTHIO_BYTES_PER_SAMPLE; - *bufptr = (uint8_t *)out_buffer; + *bufptr = (uint8_t *)out_buffer16; } void synthio_synth_reset_buffer(synthio_synth_t *synth, bool single_channel_output, uint8_t channel) { diff --git a/tests/circuitpython-manual/synthio/note/code.py b/tests/circuitpython-manual/synthio/note/code.py index 72783d1b55..3814c57279 100644 --- a/tests/circuitpython-manual/synthio/note/code.py +++ b/tests/circuitpython-manual/synthio/note/code.py @@ -84,6 +84,25 @@ def synthesize4(synth): yield 36 +def synthesize5(synth): + notes = [ + synthio.Note( + frequency=synthio.midi_to_hz(60 + i + o), + waveform=sine, + envelope=envelope, + ) + for i in [0, 4, 7] + for o in [0, -12, 12] + ] + + for n in notes: + print(n) + synth.press((n,)) + yield 120 + synth.release_all() + yield 36 + + def chain(*args): for a in args: yield from a @@ -94,7 +113,7 @@ with wave.open("tune-noenv.wav", "w") as f: f.setnchannels(1) f.setsampwidth(2) f.setframerate(48000) - for n in chain(synthesize2(synth), synthesize3(synth), synthesize4(synth)): + for n in chain(synthesize5(synth)): for i in range(n): result, data = audiocore.get_buffer(synth) f.writeframes(data) diff --git a/tests/circuitpython/miditrack.py.exp b/tests/circuitpython/miditrack.py.exp index 6de9f947c6..22a78e5445 100644 --- a/tests/circuitpython/miditrack.py.exp +++ b/tests/circuitpython/miditrack.py.exp @@ -1,4 +1,4 @@ (0, 1, 512, 1) -1 [-16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16382, 16382] +1 [-16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383] (0, 1, 512, 1) -1 [0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0] +1 [0, 0, 0, 0, 0, 0, 16383, 16383, 16383, 16383, 16383, 16383, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16383, 16383, 16383, 16383, 16383, 16383, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16383, 16383, 16383, 16383, 16383, 16383, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16383, 16383, 16383, 16383, 16383, 16383, 0, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16383, 16383, 16383, 16383, 16383, 16383, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16383, 16383, 16383, 16383, 16383, 16383, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16383, 16383, 16383, 16383, 16383, 16383, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16383, 16383, 16383, 16383, 16383, 16383, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16383, 16383, 16383, 16383, 16383, 16383, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16383, 16383, 16383, 16383, 16383, 16383, 0, 0] diff --git a/tests/circuitpython/synthesizer.py.exp b/tests/circuitpython/synthesizer.py.exp index 826290fe27..98f45f8bf8 100644 --- a/tests/circuitpython/synthesizer.py.exp +++ b/tests/circuitpython/synthesizer.py.exp @@ -1,30 +1,30 @@ () [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] (80,) -[-16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383] +[-16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383] (80, 91) -[0, 0, 16382, 16382, 0, -16382, -16382, 0, 16382, 16382, 0, 0, 16382, 0, 0, -16382, -16382, 0, 16382, 16382, 0, 0, 16382, 0] +[0, 0, 28045, 28045, 0, -28046, -28046, 0, 28045, 28045, 0, 0, 28045, 0, 0, -28046, -28046, 0, 28045, 28045, 0, 0, 28045, 0] (91,) -[-16382, 0, 0, 16382, 0, 0, 16382, 16382, 0, -16382, -16382, 0, 16382, 16382, 0, 0, 16382, 0, 0, -16382, -16382, -16382, 16382, 16382] -(-5242, 5241) -(-10484, 10484) -(-15727, 15726) -(-16383, 16382) -(-14286, 14285) -(-13106, 13105) -(-13106, 13105) -(-13106, 13105) -(-13106, 13105) -(-13106, 13105) -(-13106, 13105) -(-13106, 13105) -(-13106, 13105) -(-11009, 11008) -(-8912, 8911) -(-6815, 6814) -(-4718, 4717) -(-2621, 2620) -(-524, 523) +[-28046, 0, 0, 28045, 0, 0, 28045, 28045, 0, -28046, -28046, 0, 28045, 28045, 0, 0, 28045, 0, 0, -28046, -28046, -28046, 28045, 28045] +(-5242, 5242) +(-10485, 10484) +(-15727, 15727) +(-16383, 16383) +(-14286, 14286) +(-13106, 13106) +(-13106, 13106) +(-13106, 13106) +(-13106, 13106) +(-13106, 13106) +(-13106, 13106) +(-13106, 13106) +(-13106, 13106) +(-11009, 11009) +(-8912, 8912) +(-6815, 6815) +(-4718, 4718) +(-2621, 2621) +(-524, 524) (0, 0) (0, 0) (0, 0) diff --git a/tests/circuitpython/synthesizer_note.py.exp b/tests/circuitpython/synthesizer_note.py.exp index e51cea1755..787243e2a5 100644 --- a/tests/circuitpython/synthesizer_note.py.exp +++ b/tests/circuitpython/synthesizer_note.py.exp @@ -1,30 +1,30 @@ () [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] (Note(frequency=830.6076004423605, amplitude=1.0, tremolo_rate=0.0, tremolo_depth=0.0, vibrato_rate=0.0, vibrato_depth=0.0, waveform=None, envelope=None),) -[-16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383] +[-16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383] (Note(frequency=830.6076004423605, amplitude=1.0, tremolo_rate=0.0, tremolo_depth=0.0, vibrato_rate=0.0, vibrato_depth=0.0, waveform=None, envelope=None), Note(frequency=830.6076004423605, amplitude=1.0, tremolo_rate=0.0, tremolo_depth=0.0, vibrato_rate=0.0, vibrato_depth=0.0, waveform=None, envelope=None)) -[0, 0, 0, 0, 0, 0, 0, 0, 16382, 0, 0, 0, 0, -16382, 0, 0, 0, 0, 16382, 0, 0, 0, 0, -16382] +[0, 0, 0, 0, 0, 0, 0, 0, 28045, 0, 0, 0, 0, -28046, 0, 0, 0, 0, 28045, 0, 0, 0, 0, -28046] (Note(frequency=830.6076004423605, amplitude=1.0, tremolo_rate=0.0, tremolo_depth=0.0, vibrato_rate=0.0, vibrato_depth=0.0, waveform=None, envelope=None),) -[0, 0, 0, 16382, 0, 0, 0, 0, 0, 0, 0, 0, 16382, 0, 0, 0, 0, -16382, 0, 0, 0, 0, 16382, 0] -(-5242, 5241) -(-10484, 10484) -(-15727, 15726) -(-16383, 16382) -(-14286, 14285) -(-13106, 13105) -(-13106, 13105) -(-13106, 13105) -(-13106, 13105) -(-13106, 13105) -(-13106, 13105) -(-13106, 13105) -(-13106, 13105) -(-11009, 11008) -(-8912, 8911) -(-6815, 6814) -(-4718, 4717) -(-2621, 2620) -(-524, 523) +[0, 0, 0, 28045, 0, 0, 0, 0, 0, 0, 0, 0, 28045, 0, 0, 0, 0, -28046, 0, 0, 0, 0, 28045, 0] +(-5242, 5242) +(-10485, 10484) +(-15727, 15727) +(-16383, 16383) +(-14286, 14286) +(-13106, 13106) +(-13106, 13106) +(-13106, 13106) +(-13106, 13106) +(-13106, 13106) +(-13106, 13106) +(-13106, 13106) +(-13106, 13106) +(-11009, 11009) +(-8912, 8912) +(-6815, 6815) +(-4718, 4718) +(-2621, 2621) +(-524, 524) (0, 0) (0, 0) (0, 0)