Skip to content

Commit 12c1a72

Browse files
committed
synthio: implement envelope
This works for me (tested playing midi to raw files on host computer, as well as a variant of the nunchuk instrument on pygamer) it has to re-factor how/when MIDI reading occurs, because reasons. endorse new test results .. and allow `-1` to specify a note with no sustain (plucked)
1 parent 375a9cd commit 12c1a72

File tree

16 files changed

+529
-170
lines changed

16 files changed

+529
-170
lines changed

shared-bindings/audiocore/__init__.c

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include <stdint.h>
2828

2929
#include "py/obj.h"
30+
#include "py/gc.h"
3031
#include "py/runtime.h"
3132

3233
#include "shared-bindings/audiocore/__init__.h"
@@ -46,8 +47,23 @@ STATIC mp_obj_t audiocore_get_buffer(mp_obj_t sample_in) {
4647
mp_obj_t result[2] = {mp_obj_new_int_from_uint(gbr), mp_const_none};
4748

4849
if (gbr != GET_BUFFER_ERROR) {
50+
bool single_buffer, samples_signed;
51+
uint32_t max_buffer_length;
52+
uint8_t spacing;
53+
54+
uint8_t bits_per_sample = audiosample_bits_per_sample(sample_in);
55+
audiosample_get_buffer_structure(sample_in, false, &single_buffer, &samples_signed, &max_buffer_length, &spacing);
4956
// copies the data because the gc semantics of get_buffer are unclear
50-
result[1] = mp_obj_new_bytes(buffer, buffer_length);
57+
void *result_buf = gc_alloc(buffer_length, 0, false);
58+
memcpy(result_buf, buffer, buffer_length);
59+
char typecode =
60+
(bits_per_sample == 8 && samples_signed) ? 'b' :
61+
(bits_per_sample == 8 && !samples_signed) ? 'B' :
62+
(bits_per_sample == 16 && samples_signed) ? 'h' :
63+
(bits_per_sample == 16 && !samples_signed) ? 'H' :
64+
'b';
65+
size_t nitems = buffer_length / (bits_per_sample / 8);
66+
result[1] = mp_obj_new_memoryview(typecode, nitems, result_buf);
5167
}
5268

5369
return mp_obj_new_tuple(2, result);

shared-bindings/synthio/MidiTrack.c

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@
4444
//| tempo: int,
4545
//| *,
4646
//| sample_rate: int = 11025,
47-
//| waveform: ReadableBuffer = None
47+
//| waveform: Optional[ReadableBuffer] = None,
48+
//| envelope: Optional[Envelope] = None,
4849
//| ) -> None:
4950
//| """Create a MidiTrack from the given stream of MIDI events. Only "Note On" and "Note Off" events
5051
//| are supported; channel numbers and key velocities are ignored. Up to two notes may be on at the
@@ -54,6 +55,7 @@
5455
//| :param int tempo: Tempo of the streamed events, in MIDI ticks per second
5556
//| :param int sample_rate: The desired playback sample rate; higher sample rate requires more memory
5657
//| :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)
58+
//| :param Envelope envelope: An object that defines the loudness of a note over time. The default envelope provides no ramping, voices turn instantly on and off.
5759
//|
5860
//| Simple melody::
5961
//|
@@ -72,12 +74,13 @@
7274
//| print("stopped")"""
7375
//| ...
7476
STATIC mp_obj_t synthio_miditrack_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) {
75-
enum { ARG_buffer, ARG_tempo, ARG_sample_rate, ARG_waveform };
77+
enum { ARG_buffer, ARG_tempo, ARG_sample_rate, ARG_waveform, ARG_envelope };
7678
static const mp_arg_t allowed_args[] = {
7779
{ MP_QSTR_buffer, MP_ARG_OBJ | MP_ARG_REQUIRED },
7880
{ MP_QSTR_tempo, MP_ARG_INT | MP_ARG_REQUIRED },
7981
{ MP_QSTR_sample_rate, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 11025} },
8082
{ MP_QSTR_waveform, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none } },
83+
{ MP_QSTR_envelope, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none } },
8184
};
8285
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
8386
mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
@@ -96,7 +99,9 @@ STATIC mp_obj_t synthio_miditrack_make_new(const mp_obj_type_t *type, size_t n_a
9699
args[ARG_tempo].u_int,
97100
args[ARG_sample_rate].u_int,
98101
bufinfo_waveform.buf,
99-
bufinfo_waveform.len / 2);
102+
bufinfo_waveform.len / 2,
103+
args[ARG_envelope].u_obj
104+
);
100105

101106
return MP_OBJ_FROM_PTR(self);
102107
}

shared-bindings/synthio/MidiTrack.h

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,18 @@
2424
* THE SOFTWARE.
2525
*/
2626

27-
#ifndef MICROPY_INCLUDED_SHARED_BINDINGS_SYNTHIO_MIDITRACK_H
28-
#define MICROPY_INCLUDED_SHARED_BINDINGS_SYNTHIO_MIDITRACK_H
27+
#pragma once
2928

3029
#include "shared-module/synthio/MidiTrack.h"
3130

3231
extern const mp_obj_type_t synthio_miditrack_type;
3332

3433
void common_hal_synthio_miditrack_construct(synthio_miditrack_obj_t *self,
35-
const uint8_t *buffer, uint32_t len, uint32_t tempo, uint32_t sample_rate, const int16_t *waveform, uint16_t waveform_len);
34+
const uint8_t *buffer, uint32_t len, uint32_t tempo, uint32_t sample_rate, const int16_t *waveform, uint16_t waveform_len,
35+
mp_obj_t envelope);
3636

3737
void common_hal_synthio_miditrack_deinit(synthio_miditrack_obj_t *self);
3838
bool common_hal_synthio_miditrack_deinited(synthio_miditrack_obj_t *self);
3939
uint32_t common_hal_synthio_miditrack_get_sample_rate(synthio_miditrack_obj_t *self);
4040
uint8_t common_hal_synthio_miditrack_get_bits_per_sample(synthio_miditrack_obj_t *self);
4141
uint8_t common_hal_synthio_miditrack_get_channel_count(synthio_miditrack_obj_t *self);
42-
43-
#endif // MICROPY_INCLUDED_SHARED_BINDINGS_SYNTHIO_MIDITRACK_H

shared-bindings/synthio/Synthesizer.c

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,24 +37,29 @@
3737
#include "supervisor/shared/translate/translate.h"
3838

3939
//| class Synthesizer:
40-
//| def __init__(self, *, sample_rate: int = 11025, waveform: ReadableBuffer = None) -> None:
40+
//| def __init__(
41+
//| self,
42+
//| *,
43+
//| sample_rate: int = 11025,
44+
//| waveform: Optional[ReadableBuffer] = None,
45+
//| envelope: Optional[Envelope] = None,
46+
//| ) -> None:
4147
//| """Create a synthesizer object.
4248
//|
4349
//| This API is experimental.
4450
//|
45-
//| At least 2 simultaneous notes are supported. mimxrt10xx and rp2040 platforms support up to
46-
//| 12 notes.
47-
//|
4851
//| Notes use MIDI note numbering, with 60 being C4 or Middle C, approximately 262Hz.
4952
//|
5053
//| :param int sample_rate: The desired playback sample rate; higher sample rate requires more memory
51-
//| :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). It is permitted to modify this buffer during synthesis. This can be used, for instance, to control the overall volume or timbre of the notes.
54+
//| :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)
55+
//| :param Optional[Envelope] envelope: An object that defines the loudness of a note over time. The default envelope, `None` provides no ramping, voices turn instantly on and off.
5256
//| """
5357
STATIC mp_obj_t synthio_synthesizer_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) {
54-
enum { ARG_sample_rate, ARG_waveform };
58+
enum { ARG_sample_rate, ARG_waveform, ARG_envelope };
5559
static const mp_arg_t allowed_args[] = {
5660
{ MP_QSTR_sample_rate, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 11025} },
5761
{ MP_QSTR_waveform, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none } },
62+
{ MP_QSTR_envelope, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none } },
5863
};
5964
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
6065
mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
@@ -68,7 +73,9 @@ STATIC mp_obj_t synthio_synthesizer_make_new(const mp_obj_type_t *type, size_t n
6873
common_hal_synthio_synthesizer_construct(self,
6974
args[ARG_sample_rate].u_int,
7075
bufinfo_waveform.buf,
71-
bufinfo_waveform.len / 2);
76+
bufinfo_waveform.len / 2,
77+
args[ARG_envelope].u_obj);
78+
7279

7380
return MP_OBJ_FROM_PTR(self);
7481
}
@@ -191,6 +198,28 @@ STATIC mp_obj_t synthio_synthesizer_obj___exit__(size_t n_args, const mp_obj_t *
191198
return mp_const_none;
192199
}
193200
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(synthio_synthesizer___exit___obj, 4, 4, synthio_synthesizer_obj___exit__);
201+
202+
//| envelope: Optional[Envelope]
203+
//| """The envelope to apply to all notes. `None`, the default envelope, instantly turns notes on and off. The envelope may be changed dynamically, but it affects all notes (even currently playing notes)"""
204+
STATIC mp_obj_t synthio_synthesizer_obj_get_envelope(mp_obj_t self_in) {
205+
synthio_synthesizer_obj_t *self = MP_OBJ_TO_PTR(self_in);
206+
check_for_deinit(self);
207+
return synthio_synth_envelope_get(&self->synth);
208+
}
209+
MP_DEFINE_CONST_FUN_OBJ_1(synthio_synthesizer_get_envelope_obj, synthio_synthesizer_obj_get_envelope);
210+
211+
STATIC mp_obj_t synthio_synthesizer_obj_set_envelope(mp_obj_t self_in, mp_obj_t envelope) {
212+
synthio_synthesizer_obj_t *self = MP_OBJ_TO_PTR(self_in);
213+
check_for_deinit(self);
214+
synthio_synth_envelope_set(&self->synth, envelope);
215+
return mp_const_none;
216+
}
217+
MP_DEFINE_CONST_FUN_OBJ_2(synthio_synthesizer_set_envelope_obj, synthio_synthesizer_obj_set_envelope);
218+
219+
MP_PROPERTY_GETSET(synthio_synthesizer_envelope_obj,
220+
(mp_obj_t)&synthio_synthesizer_get_envelope_obj,
221+
(mp_obj_t)&synthio_synthesizer_set_envelope_obj);
222+
194223
//| sample_rate: int
195224
//| """32 bit value that tells how quickly samples are played in Hertz (cycles per second)."""
196225
STATIC mp_obj_t synthio_synthesizer_obj_get_sample_rate(mp_obj_t self_in) {
@@ -232,6 +261,7 @@ STATIC const mp_rom_map_elem_t synthio_synthesizer_locals_dict_table[] = {
232261
{ MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&synthio_synthesizer___exit___obj) },
233262

234263
// Properties
264+
{ MP_ROM_QSTR(MP_QSTR_envelope), MP_ROM_PTR(&synthio_synthesizer_envelope_obj) },
235265
{ MP_ROM_QSTR(MP_QSTR_sample_rate), MP_ROM_PTR(&synthio_synthesizer_sample_rate_obj) },
236266
{ MP_ROM_QSTR(MP_QSTR_max_polyphony), MP_ROM_INT(CIRCUITPY_SYNTHIO_MAX_CHANNELS) },
237267
{ MP_ROM_QSTR(MP_QSTR_pressed), MP_ROM_PTR(&synthio_synthesizer_pressed_obj) },

shared-bindings/synthio/Synthesizer.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@
3232
extern const mp_obj_type_t synthio_synthesizer_type;
3333

3434
void common_hal_synthio_synthesizer_construct(synthio_synthesizer_obj_t *self,
35-
uint32_t sample_rate, const int16_t *waveform, uint16_t waveform_len);
36-
35+
uint32_t sample_rate, const int16_t *waveform, uint16_t waveform_length,
36+
mp_obj_t envelope);
3737
void common_hal_synthio_synthesizer_deinit(synthio_synthesizer_obj_t *self);
3838
bool common_hal_synthio_synthesizer_deinited(synthio_synthesizer_obj_t *self);
3939
uint32_t common_hal_synthio_synthesizer_get_sample_rate(synthio_synthesizer_obj_t *self);

shared-bindings/synthio/__init__.c

Lines changed: 105 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
#include "py/mperrno.h"
3030
#include "py/obj.h"
31+
#include "py/objnamedtuple.h"
3132
#include "py/runtime.h"
3233
#include "extmod/vfs_fat.h"
3334
#include "extmod/vfs_posix.h"
@@ -36,16 +37,111 @@
3637
#include "shared-bindings/synthio/MidiTrack.h"
3738
#include "shared-bindings/synthio/Synthesizer.h"
3839

39-
//| """Support for MIDI synthesis"""
40+
//| """Support for multi-channel audio synthesis
4041
//|
41-
//| def from_file(file: typing.BinaryIO, *, sample_rate: int = 11025) -> MidiTrack:
42+
//| At least 2 simultaneous notes are supported. samd5x, mimxrt10xx and rp2040 platforms support up to 12 notes.
43+
//|
44+
//| I'm a little teapot. I'm not on line 11, but I don't know what is.
45+
//| """
46+
//|
47+
//| class Envelope:
48+
//| def __init__(
49+
//| self,
50+
//| attack_time: float,
51+
//| decay_time: float,
52+
//| release_time: float,
53+
//| attack_level: float,
54+
//| sustain_level: float,
55+
//| ) -> None:
56+
//| """Construct an Envelope object
57+
//|
58+
//| The Envelope defines an ADSR (Attack, Decay, Sustain, Release) envelope with linear amplitude ramping. A note starts at 0 volume, then increases to ``attack_level`` over ``attack_time`` seconds; then it decays to ``sustain_level`` over ``decay_time`` seconds. Finally, when the note is released, it decreases to ``0`` volume over ``release_time``.
59+
//|
60+
//| If the ``sustain_level`` of an envelope is 0, then the decay and sustain phases of the note are always omitted. The note is considered to be released as soon as the envelope reaches the end of the attack phase. The ``decay_time`` is ignored. This is similar to how a plucked or struck instrument behaves.
61+
//|
62+
//| If a note is released before it reaches its sustain phase, it decays with the same slope indicated by ``sustain_level/release_time`` (or ``attack_level/release_time`` for plucked envelopes)
63+
//|
64+
//| :param float attack_time: The time in seconds it takes to ramp from 0 volume to attack_volume
65+
//| :param float decay_time: The time in seconds it takes to ramp from attack_volume to sustain_volume
66+
//| :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``
67+
//| :param float attack_level: The relative level, in the range ``0.0`` to ``1.0`` of the peak volume of the attack phase
68+
//| :param float sustain_level: The relative level, in the range ``0.0`` to ``1.0`` of the volume of the sustain phase
69+
//| """
70+
//| attack_time: float
71+
//| """The time in seconds it takes to ramp from 0 volume to attack_volume"""
72+
//|
73+
//| decay_time: float
74+
//| """The time in seconds it takes to ramp from attack_volume to sustain_volume"""
75+
//|
76+
//| release_time: float
77+
//| """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``"""
78+
//|
79+
//| attack_level: float
80+
//| """The relative level, in the range ``0.0`` to ``1.0`` of the peak volume of the attack phase"""
81+
//|
82+
//| sustain_level: float
83+
//| """The relative level, in the range ``0.0`` to ``1.0`` of the volume of the sustain phase"""
84+
//|
85+
86+
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 *args) {
87+
mp_obj_t new_obj = namedtuple_make_new(type_in, n_args, n_kw, args);
88+
mp_obj_t *fields;
89+
size_t len;
90+
mp_obj_tuple_get(new_obj, &len, &fields);
91+
92+
mp_arg_validate_obj_float_non_negative(fields[0], 0., MP_QSTR_attack_time);
93+
mp_arg_validate_obj_float_non_negative(fields[1], 0., MP_QSTR_decay_time);
94+
mp_arg_validate_obj_float_non_negative(fields[2], 0., MP_QSTR_release_time);
95+
96+
mp_arg_validate_obj_float_range(fields[3], 0, 1, MP_QSTR_attack_level);
97+
mp_arg_validate_obj_float_range(fields[4], 0, 1, MP_QSTR_sustain_level);
98+
99+
return new_obj;
100+
};
101+
102+
const mp_obj_namedtuple_type_t synthio_envelope_type_obj = {
103+
.base = {
104+
.base = {
105+
.type = &mp_type_type
106+
},
107+
.flags = MP_TYPE_FLAG_EXTENDED,
108+
.name = MP_QSTR_Envelope,
109+
.print = namedtuple_print,
110+
.parent = &mp_type_tuple,
111+
.make_new = synthio_envelope_make_new,
112+
.attr = namedtuple_attr,
113+
MP_TYPE_EXTENDED_FIELDS(
114+
.unary_op = mp_obj_tuple_unary_op,
115+
.binary_op = mp_obj_tuple_binary_op,
116+
.subscr = mp_obj_tuple_subscr,
117+
.getiter = mp_obj_tuple_getiter,
118+
),
119+
},
120+
.n_fields = 5,
121+
.fields = {
122+
MP_QSTR_attack_time,
123+
MP_QSTR_decay_time,
124+
MP_QSTR_release_time,
125+
MP_QSTR_attack_level,
126+
MP_QSTR_sustain_level,
127+
},
128+
};
129+
130+
131+
//| def from_file(
132+
//| file: typing.BinaryIO,
133+
//| *,
134+
//| sample_rate: int = 11025,
135+
//| waveform: Optional[ReadableBuffer] = None,
136+
//| envelope: Optional[ReadableBuffer] = None,
137+
//| ) -> MidiTrack:
42138
//| """Create an AudioSample from an already opened MIDI file.
43139
//| Currently, only single-track MIDI (type 0) is supported.
44140
//|
45141
//| :param typing.BinaryIO file: Already opened MIDI file
46142
//| :param int sample_rate: The desired playback sample rate; higher sample rate requires more memory
47143
//| :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)
48-
//|
144+
//| :param Envelope envelope: An object that defines the loudness of a note over time. The default envelope provides no ramping, voices turn instantly on and off.
49145
//|
50146
//| Playing a MIDI file from flash::
51147
//|
@@ -65,11 +161,12 @@
65161
//| ...
66162
//|
67163
STATIC mp_obj_t synthio_from_file(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
68-
enum { ARG_file, ARG_sample_rate, ARG_waveform };
164+
enum { ARG_file, ARG_sample_rate, ARG_waveform, ARG_envelope };
69165
static const mp_arg_t allowed_args[] = {
70166
{ MP_QSTR_file, MP_ARG_OBJ | MP_ARG_REQUIRED },
71167
{ MP_QSTR_sample_rate, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 11025} },
72168
{ MP_QSTR_waveform, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none } },
169+
{ MP_QSTR_envelope, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none } },
73170
};
74171
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
75172
mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
@@ -121,7 +218,9 @@ STATIC mp_obj_t synthio_from_file(size_t n_args, const mp_obj_t *pos_args, mp_ma
121218
result->base.type = &synthio_miditrack_type;
122219

123220
common_hal_synthio_miditrack_construct(result, buffer, track_size,
124-
tempo, args[ARG_sample_rate].u_int, bufinfo_waveform.buf, bufinfo_waveform.len / 2);
221+
tempo, args[ARG_sample_rate].u_int, bufinfo_waveform.buf, bufinfo_waveform.len / 2,
222+
args[ARG_envelope].u_obj
223+
);
125224

126225
#if MICROPY_MALLOC_USES_ALLOCATED_SIZE
127226
m_free(buffer, track_size);
@@ -133,12 +232,12 @@ STATIC mp_obj_t synthio_from_file(size_t n_args, const mp_obj_t *pos_args, mp_ma
133232
}
134233
MP_DEFINE_CONST_FUN_OBJ_KW(synthio_from_file_obj, 1, synthio_from_file);
135234

136-
137235
STATIC const mp_rom_map_elem_t synthio_module_globals_table[] = {
138236
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_synthio) },
139237
{ MP_ROM_QSTR(MP_QSTR_MidiTrack), MP_ROM_PTR(&synthio_miditrack_type) },
140238
{ MP_ROM_QSTR(MP_QSTR_Synthesizer), MP_ROM_PTR(&synthio_synthesizer_type) },
141239
{ MP_ROM_QSTR(MP_QSTR_from_file), MP_ROM_PTR(&synthio_from_file_obj) },
240+
{ MP_ROM_QSTR(MP_QSTR_Envelope), MP_ROM_PTR(&synthio_envelope_type_obj) },
142241
};
143242

144243
STATIC MP_DEFINE_CONST_DICT(synthio_module_globals, synthio_module_globals_table);

shared-bindings/synthio/__init__.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,10 @@
2626

2727
#pragma once
2828

29+
#include "py/objnamedtuple.h"
30+
31+
typedef struct synthio_synth synthio_synth_t;
2932
extern int16_t shared_bindings_synthio_square_wave[];
33+
extern const mp_obj_namedtuple_type_t synthio_envelope_type_obj;
34+
void synthio_synth_envelope_set(synthio_synth_t *synth, mp_obj_t envelope_obj);
35+
mp_obj_t synthio_synth_envelope_get(synthio_synth_t *synth);

0 commit comments

Comments
 (0)