commit
bd9aca2526
|
@ -48,7 +48,7 @@ CIRCUITPY_KEYPAD_SHIFTREGISTERKEYS = 0
|
|||
CIRCUITPY_KEYPAD_KEYMATRIX = 0
|
||||
CIRCUITPY_MATH = 1
|
||||
CIRCUITPY_STAGE = 1
|
||||
CIRCUITPY_SYNTHIO = 1
|
||||
CIRCUITPY_SYNTHIO = 0
|
||||
CIRCUITPY_ZLIB = 1
|
||||
|
||||
FROZEN_MPY_DIRS += $(TOP)/frozen/circuitpython-stage/pewpew_m4
|
||||
|
|
|
@ -19,6 +19,7 @@ LD_FILE = boards/STM32F411_nvm.ld
|
|||
CIRCUITPY_AESIO = 0
|
||||
CIRCUITPY_BITMAPTOOLS = 0
|
||||
CIRCUITPY_BLEIO_HCI = 0
|
||||
CIRCUITPY_SYNTHIO = 0
|
||||
CIRCUITPY_VECTORIO = 0
|
||||
CIRCUITPY_ULAB = 0
|
||||
CIRCUITPY_ZLIB = 0
|
||||
|
|
|
@ -186,11 +186,26 @@ mp_int_t mp_arg_validate_int_range(mp_int_t i, mp_int_t min, mp_int_t max, qstr
|
|||
return i;
|
||||
}
|
||||
|
||||
mp_float_t mp_arg_validate_type_float(mp_obj_t obj, qstr arg_name) {
|
||||
mp_float_t a_float;
|
||||
if (!mp_obj_get_float_maybe(obj, &a_float)) {
|
||||
mp_raise_TypeError_varg(translate("%q must be of type %q, not %q"), arg_name, MP_QSTR_float, mp_obj_get_type(obj)->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) {
|
||||
const mp_float_t f = mp_arg_validate_type_float(float_in, 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);
|
||||
}
|
||||
}
|
||||
|
||||
mp_float_t mp_arg_validate_obj_float_non_negative(mp_obj_t float_in, mp_float_t default_for_null, qstr arg_name) {
|
||||
const mp_float_t f = (float_in == MP_OBJ_NULL)
|
||||
? default_for_null
|
||||
: mp_obj_get_float(float_in);
|
||||
if (f <= (mp_float_t)0.0) {
|
||||
: mp_arg_validate_type_float(float_in, arg_name);
|
||||
if (f < (mp_float_t)0.0) {
|
||||
mp_raise_ValueError_varg(translate("%q must be >= %d"), arg_name, 0);
|
||||
}
|
||||
return f;
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
|
||||
#include "py/runtime.h"
|
||||
#include "py/binary.h"
|
||||
#include "py/objproperty.h"
|
||||
#include "py/objstr.h"
|
||||
#include "py/objarray.h"
|
||||
|
||||
|
@ -270,15 +271,13 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_2(memoryview_cast_obj, memoryview_cast);
|
|||
#endif
|
||||
|
||||
#if MICROPY_PY_BUILTINS_MEMORYVIEW_ITEMSIZE
|
||||
STATIC void memoryview_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
|
||||
if (dest[0] != MP_OBJ_NULL) {
|
||||
return;
|
||||
}
|
||||
if (attr == MP_QSTR_itemsize) {
|
||||
STATIC mp_obj_t memoryview_itemsize_get(mp_obj_t self_in) {
|
||||
mp_obj_array_t *self = MP_OBJ_TO_PTR(self_in);
|
||||
dest[0] = MP_OBJ_NEW_SMALL_INT(mp_binary_get_size('@', self->typecode & TYPECODE_MASK, NULL));
|
||||
}
|
||||
return MP_OBJ_NEW_SMALL_INT(mp_binary_get_size('@', self->typecode & TYPECODE_MASK, NULL));
|
||||
}
|
||||
MP_DEFINE_CONST_FUN_OBJ_1(memoryview_itemsize_get_obj, memoryview_itemsize_get);
|
||||
|
||||
MP_PROPERTY_GETTER(memoryview_itemsize_obj, (mp_obj_t)&memoryview_itemsize_get_obj);
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
@ -785,9 +784,14 @@ const mp_obj_type_t mp_type_bytearray = {
|
|||
|
||||
#if MICROPY_PY_BUILTINS_MEMORYVIEW
|
||||
|
||||
#if MICROPY_CPYTHON_COMPAT
|
||||
#if MICROPY_CPYTHON_COMPAT || MICROPY_PY_BUILTINS_MEMORYVIEW_ITEMSIZE
|
||||
STATIC const mp_rom_map_elem_t memoryview_locals_dict_table[] = {
|
||||
#if MICROPY_CPYTHON_COMPAT
|
||||
{ MP_ROM_QSTR(MP_QSTR_cast), MP_ROM_PTR(&memoryview_cast_obj) },
|
||||
#endif
|
||||
#if MICROPY_PY_BUILTINS_MEMORYVIEW_ITEMSIZE
|
||||
{ MP_ROM_QSTR(MP_QSTR_itemsize), MP_ROM_PTR(&memoryview_itemsize_obj) },
|
||||
#endif
|
||||
};
|
||||
|
||||
STATIC MP_DEFINE_CONST_DICT(memoryview_locals_dict, memoryview_locals_dict_table);
|
||||
|
@ -798,12 +802,9 @@ const mp_obj_type_t mp_type_memoryview = {
|
|||
.flags = MP_TYPE_FLAG_EQ_CHECKS_OTHER_TYPE | MP_TYPE_FLAG_EXTENDED,
|
||||
.name = MP_QSTR_memoryview,
|
||||
.make_new = memoryview_make_new,
|
||||
#if MICROPY_CPYTHON_COMPAT
|
||||
#if MICROPY_CPYTHON_COMPAT || MICROPY_PY_BUILTINS_MEMORYVIEW_ITEMSIZE
|
||||
.locals_dict = (mp_obj_dict_t *)&memoryview_locals_dict,
|
||||
#endif
|
||||
#if MICROPY_PY_BUILTINS_MEMORYVIEW_ITEMSIZE
|
||||
.attr = memoryview_attr,
|
||||
#endif
|
||||
MP_TYPE_EXTENDED_FIELDS(
|
||||
.getiter = array_iterator_new,
|
||||
.unary_op = array_unary_op,
|
||||
|
|
|
@ -103,6 +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_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);
|
||||
mp_uint_t mp_arg_validate_length_max(mp_uint_t length, mp_uint_t max, qstr arg_name);
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
#include <stdint.h>
|
||||
|
||||
#include "py/obj.h"
|
||||
#include "py/gc.h"
|
||||
#include "py/runtime.h"
|
||||
|
||||
#include "shared-bindings/audiocore/__init__.h"
|
||||
|
@ -46,8 +47,23 @@ STATIC mp_obj_t audiocore_get_buffer(mp_obj_t sample_in) {
|
|||
mp_obj_t result[2] = {mp_obj_new_int_from_uint(gbr), mp_const_none};
|
||||
|
||||
if (gbr != GET_BUFFER_ERROR) {
|
||||
bool single_buffer, samples_signed;
|
||||
uint32_t max_buffer_length;
|
||||
uint8_t spacing;
|
||||
|
||||
uint8_t bits_per_sample = audiosample_bits_per_sample(sample_in);
|
||||
audiosample_get_buffer_structure(sample_in, false, &single_buffer, &samples_signed, &max_buffer_length, &spacing);
|
||||
// copies the data because the gc semantics of get_buffer are unclear
|
||||
result[1] = mp_obj_new_bytes(buffer, buffer_length);
|
||||
void *result_buf = gc_alloc(buffer_length, 0, false);
|
||||
memcpy(result_buf, buffer, buffer_length);
|
||||
char typecode =
|
||||
(bits_per_sample == 8 && samples_signed) ? 'b' :
|
||||
(bits_per_sample == 8 && !samples_signed) ? 'B' :
|
||||
(bits_per_sample == 16 && samples_signed) ? 'h' :
|
||||
(bits_per_sample == 16 && !samples_signed) ? 'H' :
|
||||
'b';
|
||||
size_t nitems = buffer_length / (bits_per_sample / 8);
|
||||
result[1] = mp_obj_new_memoryview(typecode, nitems, result_buf);
|
||||
}
|
||||
|
||||
return mp_obj_new_tuple(2, result);
|
||||
|
|
|
@ -44,7 +44,8 @@
|
|||
//| tempo: int,
|
||||
//| *,
|
||||
//| sample_rate: int = 11025,
|
||||
//| waveform: ReadableBuffer = None
|
||||
//| waveform: Optional[ReadableBuffer] = None,
|
||||
//| envelope: Optional[Envelope] = None,
|
||||
//| ) -> None:
|
||||
//| """Create a MidiTrack from the given stream of MIDI events. Only "Note On" and "Note Off" events
|
||||
//| are supported; channel numbers and key velocities are ignored. Up to two notes may be on at the
|
||||
|
@ -54,6 +55,7 @@
|
|||
//| :param int tempo: Tempo of the streamed events, in MIDI ticks per second
|
||||
//| :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)
|
||||
//| :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.
|
||||
//|
|
||||
//| Simple melody::
|
||||
//|
|
||||
|
@ -72,12 +74,13 @@
|
|||
//| print("stopped")"""
|
||||
//| ...
|
||||
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) {
|
||||
enum { ARG_buffer, ARG_tempo, ARG_sample_rate, ARG_waveform };
|
||||
enum { ARG_buffer, ARG_tempo, ARG_sample_rate, ARG_waveform, ARG_envelope };
|
||||
static const mp_arg_t allowed_args[] = {
|
||||
{ MP_QSTR_buffer, MP_ARG_OBJ | MP_ARG_REQUIRED },
|
||||
{ MP_QSTR_tempo, MP_ARG_INT | MP_ARG_REQUIRED },
|
||||
{ MP_QSTR_sample_rate, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 11025} },
|
||||
{ MP_QSTR_waveform, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none } },
|
||||
{ MP_QSTR_envelope, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none } },
|
||||
};
|
||||
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
|
||||
mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
|
||||
|
@ -96,7 +99,9 @@ STATIC mp_obj_t synthio_miditrack_make_new(const mp_obj_type_t *type, size_t n_a
|
|||
args[ARG_tempo].u_int,
|
||||
args[ARG_sample_rate].u_int,
|
||||
bufinfo_waveform.buf,
|
||||
bufinfo_waveform.len / 2);
|
||||
bufinfo_waveform.len / 2,
|
||||
args[ARG_envelope].u_obj
|
||||
);
|
||||
|
||||
return MP_OBJ_FROM_PTR(self);
|
||||
}
|
||||
|
@ -133,7 +138,7 @@ STATIC mp_obj_t synthio_miditrack_obj___exit__(size_t n_args, const mp_obj_t *ar
|
|||
}
|
||||
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(synthio_miditrack___exit___obj, 4, 4, synthio_miditrack_obj___exit__);
|
||||
|
||||
//| sample_rate: Optional[int]
|
||||
//| sample_rate: int
|
||||
//| """32 bit value that tells how quickly samples are played in Hertz (cycles per second)."""
|
||||
//|
|
||||
STATIC mp_obj_t synthio_miditrack_obj_get_sample_rate(mp_obj_t self_in) {
|
||||
|
@ -146,6 +151,23 @@ MP_DEFINE_CONST_FUN_OBJ_1(synthio_miditrack_get_sample_rate_obj, synthio_miditra
|
|||
MP_PROPERTY_GETTER(synthio_miditrack_sample_rate_obj,
|
||||
(mp_obj_t)&synthio_miditrack_get_sample_rate_obj);
|
||||
|
||||
//| error_location: Optional[int]
|
||||
//| """Offset, in bytes within the midi data, of a decoding error"""
|
||||
//|
|
||||
STATIC mp_obj_t synthio_miditrack_obj_get_error_location(mp_obj_t self_in) {
|
||||
synthio_miditrack_obj_t *self = MP_OBJ_TO_PTR(self_in);
|
||||
check_for_deinit(self);
|
||||
mp_int_t location = common_hal_synthio_miditrack_get_error_location(self);
|
||||
if (location >= 0) {
|
||||
return MP_OBJ_NEW_SMALL_INT(location);
|
||||
}
|
||||
return mp_const_none;
|
||||
}
|
||||
MP_DEFINE_CONST_FUN_OBJ_1(synthio_miditrack_get_error_location_obj, synthio_miditrack_obj_get_error_location);
|
||||
|
||||
MP_PROPERTY_GETTER(synthio_miditrack_error_location_obj,
|
||||
(mp_obj_t)&synthio_miditrack_get_error_location_obj);
|
||||
|
||||
STATIC const mp_rom_map_elem_t synthio_miditrack_locals_dict_table[] = {
|
||||
// Methods
|
||||
{ MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&synthio_miditrack_deinit_obj) },
|
||||
|
@ -154,6 +176,7 @@ STATIC const mp_rom_map_elem_t synthio_miditrack_locals_dict_table[] = {
|
|||
|
||||
// Properties
|
||||
{ MP_ROM_QSTR(MP_QSTR_sample_rate), MP_ROM_PTR(&synthio_miditrack_sample_rate_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_error_location), MP_ROM_PTR(&synthio_miditrack_error_location_obj) },
|
||||
};
|
||||
STATIC MP_DEFINE_CONST_DICT(synthio_miditrack_locals_dict, synthio_miditrack_locals_dict_table);
|
||||
|
||||
|
|
|
@ -24,20 +24,19 @@
|
|||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef MICROPY_INCLUDED_SHARED_BINDINGS_SYNTHIO_MIDITRACK_H
|
||||
#define MICROPY_INCLUDED_SHARED_BINDINGS_SYNTHIO_MIDITRACK_H
|
||||
#pragma once
|
||||
|
||||
#include "shared-module/synthio/MidiTrack.h"
|
||||
|
||||
extern const mp_obj_type_t synthio_miditrack_type;
|
||||
|
||||
void common_hal_synthio_miditrack_construct(synthio_miditrack_obj_t *self,
|
||||
const uint8_t *buffer, uint32_t len, uint32_t tempo, uint32_t sample_rate, const int16_t *waveform, uint16_t waveform_len);
|
||||
const uint8_t *buffer, uint32_t len, uint32_t tempo, uint32_t sample_rate, const int16_t *waveform, uint16_t waveform_len,
|
||||
mp_obj_t envelope);
|
||||
|
||||
void common_hal_synthio_miditrack_deinit(synthio_miditrack_obj_t *self);
|
||||
bool common_hal_synthio_miditrack_deinited(synthio_miditrack_obj_t *self);
|
||||
uint32_t common_hal_synthio_miditrack_get_sample_rate(synthio_miditrack_obj_t *self);
|
||||
uint8_t common_hal_synthio_miditrack_get_bits_per_sample(synthio_miditrack_obj_t *self);
|
||||
uint8_t common_hal_synthio_miditrack_get_channel_count(synthio_miditrack_obj_t *self);
|
||||
|
||||
#endif // MICROPY_INCLUDED_SHARED_BINDINGS_SYNTHIO_MIDITRACK_H
|
||||
mp_int_t common_hal_synthio_miditrack_get_error_location(synthio_miditrack_obj_t *self);
|
||||
|
|
|
@ -37,24 +37,29 @@
|
|||
#include "supervisor/shared/translate/translate.h"
|
||||
|
||||
//| class Synthesizer:
|
||||
//| def __init__(self, *, sample_rate: int = 11025, waveform: ReadableBuffer = None) -> None:
|
||||
//| def __init__(
|
||||
//| self,
|
||||
//| *,
|
||||
//| sample_rate: int = 11025,
|
||||
//| waveform: Optional[ReadableBuffer] = None,
|
||||
//| envelope: Optional[Envelope] = None,
|
||||
//| ) -> None:
|
||||
//| """Create a synthesizer object.
|
||||
//|
|
||||
//| This API is experimental.
|
||||
//|
|
||||
//| At least 2 simultaneous notes are supported. mimxrt10xx and rp2040 platforms support up to
|
||||
//| 12 notes.
|
||||
//|
|
||||
//| Notes use MIDI note numbering, with 60 being C4 or Middle C, approximately 262Hz.
|
||||
//|
|
||||
//| :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). 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.
|
||||
//| :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)
|
||||
//| :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.
|
||||
//| """
|
||||
STATIC mp_obj_t synthio_synthesizer_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) {
|
||||
enum { ARG_sample_rate, ARG_waveform };
|
||||
enum { ARG_sample_rate, ARG_waveform, ARG_envelope };
|
||||
static const mp_arg_t allowed_args[] = {
|
||||
{ MP_QSTR_sample_rate, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 11025} },
|
||||
{ MP_QSTR_waveform, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none } },
|
||||
{ MP_QSTR_envelope, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none } },
|
||||
};
|
||||
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
|
||||
mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
|
||||
|
@ -68,7 +73,9 @@ STATIC mp_obj_t synthio_synthesizer_make_new(const mp_obj_type_t *type, size_t n
|
|||
common_hal_synthio_synthesizer_construct(self,
|
||||
args[ARG_sample_rate].u_int,
|
||||
bufinfo_waveform.buf,
|
||||
bufinfo_waveform.len / 2);
|
||||
bufinfo_waveform.len / 2,
|
||||
args[ARG_envelope].u_obj);
|
||||
|
||||
|
||||
return MP_OBJ_FROM_PTR(self);
|
||||
}
|
||||
|
@ -92,7 +99,20 @@ 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.
|
||||
//|
|
||||
//| Releasing a note that was already released has no effect.
|
||||
//|
|
||||
//| :param Sequence[int] release: Any sequence of integer 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);
|
||||
common_hal_synthio_synthesizer_release(self, release);
|
||||
return mp_const_none;
|
||||
}
|
||||
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:
|
||||
|
@ -178,6 +198,28 @@ STATIC mp_obj_t synthio_synthesizer_obj___exit__(size_t n_args, const mp_obj_t *
|
|||
return mp_const_none;
|
||||
}
|
||||
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(synthio_synthesizer___exit___obj, 4, 4, synthio_synthesizer_obj___exit__);
|
||||
|
||||
//| envelope: Optional[Envelope]
|
||||
//| """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)"""
|
||||
STATIC mp_obj_t synthio_synthesizer_obj_get_envelope(mp_obj_t self_in) {
|
||||
synthio_synthesizer_obj_t *self = MP_OBJ_TO_PTR(self_in);
|
||||
check_for_deinit(self);
|
||||
return synthio_synth_envelope_get(&self->synth);
|
||||
}
|
||||
MP_DEFINE_CONST_FUN_OBJ_1(synthio_synthesizer_get_envelope_obj, synthio_synthesizer_obj_get_envelope);
|
||||
|
||||
STATIC mp_obj_t synthio_synthesizer_obj_set_envelope(mp_obj_t self_in, mp_obj_t envelope) {
|
||||
synthio_synthesizer_obj_t *self = MP_OBJ_TO_PTR(self_in);
|
||||
check_for_deinit(self);
|
||||
synthio_synth_envelope_set(&self->synth, envelope);
|
||||
return mp_const_none;
|
||||
}
|
||||
MP_DEFINE_CONST_FUN_OBJ_2(synthio_synthesizer_set_envelope_obj, synthio_synthesizer_obj_set_envelope);
|
||||
|
||||
MP_PROPERTY_GETSET(synthio_synthesizer_envelope_obj,
|
||||
(mp_obj_t)&synthio_synthesizer_get_envelope_obj,
|
||||
(mp_obj_t)&synthio_synthesizer_set_envelope_obj);
|
||||
|
||||
//| sample_rate: int
|
||||
//| """32 bit value that tells how quickly samples are played in Hertz (cycles per second)."""
|
||||
STATIC mp_obj_t synthio_synthesizer_obj_get_sample_rate(mp_obj_t self_in) {
|
||||
|
@ -210,6 +252,7 @@ MP_PROPERTY_GETTER(synthio_synthesizer_pressed_obj,
|
|||
STATIC const mp_rom_map_elem_t synthio_synthesizer_locals_dict_table[] = {
|
||||
// Methods
|
||||
{ MP_ROM_QSTR(MP_QSTR_press), MP_ROM_PTR(&synthio_synthesizer_press_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_release), MP_ROM_PTR(&synthio_synthesizer_release_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_release_all), MP_ROM_PTR(&synthio_synthesizer_release_all_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_release_then_press), MP_ROM_PTR(&synthio_synthesizer_release_then_press_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_release_all_then_press), MP_ROM_PTR(&synthio_synthesizer_release_all_then_press_obj) },
|
||||
|
@ -218,6 +261,7 @@ STATIC const mp_rom_map_elem_t synthio_synthesizer_locals_dict_table[] = {
|
|||
{ MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&synthio_synthesizer___exit___obj) },
|
||||
|
||||
// Properties
|
||||
{ MP_ROM_QSTR(MP_QSTR_envelope), MP_ROM_PTR(&synthio_synthesizer_envelope_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_sample_rate), MP_ROM_PTR(&synthio_synthesizer_sample_rate_obj) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_max_polyphony), MP_ROM_INT(CIRCUITPY_SYNTHIO_MAX_CHANNELS) },
|
||||
{ MP_ROM_QSTR(MP_QSTR_pressed), MP_ROM_PTR(&synthio_synthesizer_pressed_obj) },
|
||||
|
|
|
@ -32,8 +32,8 @@
|
|||
extern const mp_obj_type_t synthio_synthesizer_type;
|
||||
|
||||
void common_hal_synthio_synthesizer_construct(synthio_synthesizer_obj_t *self,
|
||||
uint32_t sample_rate, const int16_t *waveform, uint16_t waveform_len);
|
||||
|
||||
uint32_t sample_rate, const int16_t *waveform, uint16_t waveform_length,
|
||||
mp_obj_t envelope);
|
||||
void common_hal_synthio_synthesizer_deinit(synthio_synthesizer_obj_t *self);
|
||||
bool common_hal_synthio_synthesizer_deinited(synthio_synthesizer_obj_t *self);
|
||||
uint32_t common_hal_synthio_synthesizer_get_sample_rate(synthio_synthesizer_obj_t *self);
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
|
||||
#include "py/mperrno.h"
|
||||
#include "py/obj.h"
|
||||
#include "py/objnamedtuple.h"
|
||||
#include "py/runtime.h"
|
||||
#include "extmod/vfs_fat.h"
|
||||
#include "extmod/vfs_posix.h"
|
||||
|
@ -36,16 +37,141 @@
|
|||
#include "shared-bindings/synthio/MidiTrack.h"
|
||||
#include "shared-bindings/synthio/Synthesizer.h"
|
||||
|
||||
//| """Support for MIDI synthesis"""
|
||||
#define default_attack_time (MICROPY_FLOAT_CONST(0.1))
|
||||
#define default_decay_time (MICROPY_FLOAT_CONST(0.05))
|
||||
#define default_release_time (MICROPY_FLOAT_CONST(0.2))
|
||||
#define default_attack_level (MICROPY_FLOAT_CONST(1.))
|
||||
#define default_sustain_level (MICROPY_FLOAT_CONST(0.8))
|
||||
|
||||
static const mp_arg_t envelope_properties[] = {
|
||||
{ MP_QSTR_attack_time, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_OBJ_NULL } },
|
||||
{ MP_QSTR_decay_time, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_OBJ_NULL } },
|
||||
{ MP_QSTR_release_time, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_OBJ_NULL } },
|
||||
{ MP_QSTR_attack_level, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_OBJ_NULL } },
|
||||
{ MP_QSTR_sustain_level, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_OBJ_NULL } },
|
||||
};
|
||||
|
||||
//| """Support for multi-channel audio synthesis
|
||||
//|
|
||||
//| def from_file(file: typing.BinaryIO, *, sample_rate: int = 11025) -> MidiTrack:
|
||||
//| At least 2 simultaneous notes are supported. samd5x, mimxrt10xx and rp2040 platforms support up to 12 notes.
|
||||
//|
|
||||
//| """
|
||||
//|
|
||||
//| class Envelope:
|
||||
//| def __init__(
|
||||
//| self,
|
||||
//| *,
|
||||
//| attack_time: Optional[float] = 0.1,
|
||||
//| decay_time: Optional[float] = 0.05,
|
||||
//| release_time: Optional[float] = 0.2,
|
||||
//| attack_level: Optional[float] = 1.0,
|
||||
//| sustain_level: Optional[float] = 0.8,
|
||||
//| ) -> None:
|
||||
//| """Construct an Envelope object
|
||||
//|
|
||||
//| 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``.
|
||||
//|
|
||||
//| 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.
|
||||
//|
|
||||
//| 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)
|
||||
//|
|
||||
//| :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
|
||||
//| """
|
||||
//| attack_time: float
|
||||
//| """The time in seconds it takes to ramp from 0 volume to attack_volume"""
|
||||
//|
|
||||
//| decay_time: float
|
||||
//| """The time in seconds it takes to ramp from attack_volume to sustain_volume"""
|
||||
//|
|
||||
//| release_time: float
|
||||
//| """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"""
|
||||
//|
|
||||
//| sustain_level: float
|
||||
//| """The relative level, in the range ``0.0`` to ``1.0`` of the volume of the sustain phase"""
|
||||
//|
|
||||
|
||||
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) {
|
||||
mp_arg_val_t args[MP_ARRAY_SIZE(envelope_properties)];
|
||||
enum { ARG_attack_time, ARG_decay_time, ARG_release_time, ARG_attack_level, ARG_sustain_level };
|
||||
mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(envelope_properties), envelope_properties, args);
|
||||
|
||||
if (args[ARG_attack_time].u_obj == MP_OBJ_NULL) {
|
||||
args[ARG_attack_time].u_obj = mp_obj_new_float(default_attack_time);
|
||||
}
|
||||
if (args[ARG_decay_time].u_obj == MP_OBJ_NULL) {
|
||||
args[ARG_decay_time].u_obj = mp_obj_new_float(default_decay_time);
|
||||
}
|
||||
if (args[ARG_release_time].u_obj == MP_OBJ_NULL) {
|
||||
args[ARG_release_time].u_obj = mp_obj_new_float(default_release_time);
|
||||
}
|
||||
if (args[ARG_attack_level].u_obj == MP_OBJ_NULL) {
|
||||
args[ARG_attack_level].u_obj = mp_obj_new_float(default_attack_level);
|
||||
}
|
||||
if (args[ARG_sustain_level].u_obj == MP_OBJ_NULL) {
|
||||
args[ARG_sustain_level].u_obj = mp_obj_new_float(default_sustain_level);
|
||||
}
|
||||
|
||||
mp_arg_validate_obj_float_non_negative(args[ARG_attack_time].u_obj, 0., MP_QSTR_attack_time);
|
||||
mp_arg_validate_obj_float_non_negative(args[ARG_decay_time].u_obj, 0., MP_QSTR_decay_time);
|
||||
mp_arg_validate_obj_float_non_negative(args[ARG_release_time].u_obj, 0., MP_QSTR_release_time);
|
||||
|
||||
mp_arg_validate_obj_float_range(args[ARG_attack_level].u_obj, 0, 1, MP_QSTR_attack_level);
|
||||
mp_arg_validate_obj_float_range(args[ARG_sustain_level].u_obj, 0, 1, MP_QSTR_sustain_level);
|
||||
|
||||
MP_STATIC_ASSERT(sizeof(mp_arg_val_t) == sizeof(mp_obj_t));
|
||||
return namedtuple_make_new(type_in, MP_ARRAY_SIZE(args), 0, &args[0].u_obj);
|
||||
};
|
||||
|
||||
const mp_obj_namedtuple_type_t synthio_envelope_type_obj = {
|
||||
.base = {
|
||||
.base = {
|
||||
.type = &mp_type_type
|
||||
},
|
||||
.flags = MP_TYPE_FLAG_EXTENDED,
|
||||
.name = MP_QSTR_Envelope,
|
||||
.print = namedtuple_print,
|
||||
.parent = &mp_type_tuple,
|
||||
.make_new = synthio_envelope_make_new,
|
||||
.attr = namedtuple_attr,
|
||||
MP_TYPE_EXTENDED_FIELDS(
|
||||
.unary_op = mp_obj_tuple_unary_op,
|
||||
.binary_op = mp_obj_tuple_binary_op,
|
||||
.subscr = mp_obj_tuple_subscr,
|
||||
.getiter = mp_obj_tuple_getiter,
|
||||
),
|
||||
},
|
||||
.n_fields = 5,
|
||||
.fields = {
|
||||
MP_QSTR_attack_time,
|
||||
MP_QSTR_decay_time,
|
||||
MP_QSTR_release_time,
|
||||
MP_QSTR_attack_level,
|
||||
MP_QSTR_sustain_level,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
//| def from_file(
|
||||
//| file: typing.BinaryIO,
|
||||
//| *,
|
||||
//| sample_rate: int = 11025,
|
||||
//| waveform: Optional[ReadableBuffer] = None,
|
||||
//| envelope: Optional[Envelope] = None,
|
||||
//| ) -> MidiTrack:
|
||||
//| """Create an AudioSample from an already opened MIDI file.
|
||||
//| Currently, only single-track MIDI (type 0) is supported.
|
||||
//|
|
||||
//| :param typing.BinaryIO file: Already opened MIDI file
|
||||
//| :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)
|
||||
//|
|
||||
//| :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.
|
||||
//|
|
||||
//| Playing a MIDI file from flash::
|
||||
//|
|
||||
|
@ -65,11 +191,12 @@
|
|||
//| ...
|
||||
//|
|
||||
STATIC mp_obj_t synthio_from_file(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
|
||||
enum { ARG_file, ARG_sample_rate, ARG_waveform };
|
||||
enum { ARG_file, ARG_sample_rate, ARG_waveform, ARG_envelope };
|
||||
static const mp_arg_t allowed_args[] = {
|
||||
{ MP_QSTR_file, MP_ARG_OBJ | MP_ARG_REQUIRED },
|
||||
{ MP_QSTR_sample_rate, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 11025} },
|
||||
{ MP_QSTR_waveform, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none } },
|
||||
{ MP_QSTR_envelope, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none } },
|
||||
};
|
||||
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
|
||||
mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
|
||||
|
@ -121,7 +248,9 @@ STATIC mp_obj_t synthio_from_file(size_t n_args, const mp_obj_t *pos_args, mp_ma
|
|||
result->base.type = &synthio_miditrack_type;
|
||||
|
||||
common_hal_synthio_miditrack_construct(result, buffer, track_size,
|
||||
tempo, args[ARG_sample_rate].u_int, bufinfo_waveform.buf, bufinfo_waveform.len / 2);
|
||||
tempo, args[ARG_sample_rate].u_int, bufinfo_waveform.buf, bufinfo_waveform.len / 2,
|
||||
args[ARG_envelope].u_obj
|
||||
);
|
||||
|
||||
#if MICROPY_MALLOC_USES_ALLOCATED_SIZE
|
||||
m_free(buffer, track_size);
|
||||
|
@ -133,12 +262,12 @@ 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 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) },
|
||||
};
|
||||
|
||||
STATIC MP_DEFINE_CONST_DICT(synthio_module_globals, synthio_module_globals_table);
|
||||
|
|
|
@ -26,4 +26,10 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include "py/objnamedtuple.h"
|
||||
|
||||
typedef struct synthio_synth synthio_synth_t;
|
||||
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);
|
||||
|
|
|
@ -28,122 +28,118 @@
|
|||
#include "shared-bindings/synthio/MidiTrack.h"
|
||||
|
||||
|
||||
STATIC NORETURN void raise_midi_stream_error(uint32_t pos) {
|
||||
mp_raise_ValueError_varg(translate("Error in MIDI stream at position %d"), pos);
|
||||
STATIC void print_midi_stream_error(synthio_miditrack_obj_t *self) {
|
||||
self->error_location = self->pos;
|
||||
self->pos = self->track.len;
|
||||
}
|
||||
|
||||
STATIC uint8_t parse_note(const uint8_t *buffer, uint32_t len, uint32_t *pos) {
|
||||
if (*pos + 1 >= len) {
|
||||
raise_midi_stream_error(*pos);
|
||||
STATIC uint8_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);
|
||||
}
|
||||
uint8_t note = buffer[(*pos)++];
|
||||
if (note > 127 || buffer[(*pos)++] > 127) {
|
||||
raise_midi_stream_error(*pos);
|
||||
uint8_t note = buffer[(self->pos)++];
|
||||
if (note > 127 || buffer[(self->pos)++] > 127) {
|
||||
print_midi_stream_error(self);
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
STATIC void terminate_span(synthio_miditrack_obj_t *self, uint16_t *dur) {
|
||||
if (*dur) {
|
||||
self->track[self->total_spans - 1].dur = *dur;
|
||||
*dur = 0;
|
||||
} else {
|
||||
self->total_spans--;
|
||||
}
|
||||
}
|
||||
|
||||
STATIC void add_span(synthio_miditrack_obj_t *self, const synthio_midi_span_t *span) {
|
||||
self->track = m_renew(synthio_midi_span_t, self->track, self->total_spans, self->total_spans + 1);
|
||||
self->track[self->total_spans++] = *span;
|
||||
}
|
||||
|
||||
STATIC void change_span_note(synthio_miditrack_obj_t *self, uint8_t old_note, uint8_t new_note, uint16_t *dur) {
|
||||
synthio_midi_span_t span = self->track[self->total_spans - 1];
|
||||
if (synthio_span_change_note(&span, old_note, new_note)) {
|
||||
terminate_span(self, dur);
|
||||
add_span(self, &span);
|
||||
*dur = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void common_hal_synthio_miditrack_construct(synthio_miditrack_obj_t *self,
|
||||
const uint8_t *buffer, uint32_t len, uint32_t tempo, uint32_t sample_rate,
|
||||
const int16_t *waveform, uint16_t waveform_length) {
|
||||
|
||||
self->synth.sample_rate = sample_rate;
|
||||
self->track = m_malloc(sizeof(synthio_midi_span_t), false);
|
||||
synthio_span_init(self->track);
|
||||
self->next_span = 0;
|
||||
self->total_spans = 1;
|
||||
self->synth.waveform = waveform;
|
||||
self->synth.waveform_length = waveform_length;
|
||||
|
||||
uint16_t dur = 0;
|
||||
uint32_t pos = 0;
|
||||
while (pos < len) {
|
||||
static int decode_duration(synthio_miditrack_obj_t *self) {
|
||||
uint8_t *buffer = self->track.buf;
|
||||
size_t len = self->track.len;
|
||||
uint8_t c;
|
||||
uint32_t delta = 0;
|
||||
do {
|
||||
c = buffer[pos++];
|
||||
c = buffer[self->pos++];
|
||||
delta <<= 7;
|
||||
delta |= c & 0x7f;
|
||||
} while ((c & 0x80) && (pos < len));
|
||||
} while ((c & 0x80) && (self->pos < len));
|
||||
|
||||
// errors cannot be raised from the background task, so simply end the track.
|
||||
if (c & 0x80) {
|
||||
raise_midi_stream_error(pos);
|
||||
self->pos = self->track.len;
|
||||
print_midi_stream_error(self);
|
||||
}
|
||||
return delta * self->synth.sample_rate / self->tempo;
|
||||
}
|
||||
|
||||
// dur is carried over here so that if a note on/off message doesn't actually produce a change, the
|
||||
// underlying "span" is extended. Otherwise, it is zeroed out in the call to `terminate_span`.
|
||||
dur += delta * sample_rate / tempo;
|
||||
|
||||
switch (buffer[pos++] >> 4) {
|
||||
// invariant: pointing at a MIDI message
|
||||
static void decode_until_pause(synthio_miditrack_obj_t *self) {
|
||||
uint8_t *buffer = self->track.buf;
|
||||
size_t len = self->track.len;
|
||||
do {
|
||||
switch (buffer[self->pos++] >> 4) {
|
||||
case 8: { // Note Off
|
||||
uint8_t note = parse_note(buffer, len, &pos);
|
||||
change_span_note(self, note, SYNTHIO_SILENCE, &dur);
|
||||
uint8_t note = parse_note(self);
|
||||
synthio_span_change_note(&self->synth, note, SYNTHIO_SILENCE);
|
||||
break;
|
||||
}
|
||||
case 9: { // Note On
|
||||
uint8_t note = parse_note(buffer, len, &pos);
|
||||
change_span_note(self, SYNTHIO_SILENCE, note, &dur);
|
||||
uint8_t note = parse_note(self);
|
||||
synthio_span_change_note(&self->synth, SYNTHIO_SILENCE, note);
|
||||
break;
|
||||
}
|
||||
case 10:
|
||||
case 11:
|
||||
case 14: // two data bytes to ignore
|
||||
parse_note(buffer, len, &pos);
|
||||
parse_note(self);
|
||||
break;
|
||||
case 12:
|
||||
case 13: // one data byte to ignore
|
||||
if (pos >= len || buffer[pos++] > 127) {
|
||||
raise_midi_stream_error(pos);
|
||||
if (self->pos >= len || buffer[self->pos++] > 127) {
|
||||
print_midi_stream_error(self);
|
||||
}
|
||||
break;
|
||||
case 15: // the full syntax is too complicated, just assume it's "End of Track" event
|
||||
pos = len;
|
||||
self->pos = len;
|
||||
break;
|
||||
default: // invalid event
|
||||
raise_midi_stream_error(pos);
|
||||
print_midi_stream_error(self);
|
||||
}
|
||||
if (self->pos < len) {
|
||||
self->synth.span.dur = decode_duration(self);
|
||||
}
|
||||
} while (self->pos < len && self->synth.span.dur == 0);
|
||||
}
|
||||
terminate_span(self, &dur);
|
||||
|
||||
uint16_t max_dur = 0;
|
||||
for (int i = 0; i < self->total_spans; i++) {
|
||||
max_dur = MAX(self->track[i].dur, max_dur);
|
||||
STATIC void start_parse(synthio_miditrack_obj_t *self) {
|
||||
self->pos = 0;
|
||||
self->error_location = -1;
|
||||
self->synth.span.dur = decode_duration(self);
|
||||
if (self->synth.span.dur == 0) {
|
||||
// the usual case: the file starts with some MIDI event, not a delay
|
||||
decode_until_pause(self);
|
||||
}
|
||||
synthio_synth_init(&self->synth, max_dur);
|
||||
}
|
||||
|
||||
void common_hal_synthio_miditrack_construct(synthio_miditrack_obj_t *self,
|
||||
const uint8_t *buffer, uint32_t len, uint32_t tempo, uint32_t sample_rate,
|
||||
const int16_t *waveform, uint16_t waveform_length,
|
||||
mp_obj_t envelope) {
|
||||
|
||||
self->tempo = tempo;
|
||||
self->track.buf = (void *)buffer;
|
||||
self->track.len = len;
|
||||
|
||||
synthio_synth_init(&self->synth, sample_rate, waveform, waveform_length, envelope);
|
||||
|
||||
start_parse(self);
|
||||
}
|
||||
|
||||
void common_hal_synthio_miditrack_deinit(synthio_miditrack_obj_t *self) {
|
||||
synthio_synth_deinit(&self->synth);
|
||||
m_del(synthio_midi_span_t, self->track, self->total_spans + 1);
|
||||
self->track = NULL;
|
||||
}
|
||||
|
||||
bool common_hal_synthio_miditrack_deinited(synthio_miditrack_obj_t *self) {
|
||||
return synthio_synth_deinited(&self->synth);
|
||||
}
|
||||
|
||||
mp_int_t common_hal_synthio_miditrack_get_error_location(synthio_miditrack_obj_t *self) {
|
||||
return self->error_location;
|
||||
}
|
||||
|
||||
uint32_t common_hal_synthio_miditrack_get_sample_rate(synthio_miditrack_obj_t *self) {
|
||||
return self->synth.sample_rate;
|
||||
}
|
||||
|
@ -157,25 +153,21 @@ uint8_t common_hal_synthio_miditrack_get_channel_count(synthio_miditrack_obj_t *
|
|||
void synthio_miditrack_reset_buffer(synthio_miditrack_obj_t *self,
|
||||
bool single_channel_output, uint8_t channel) {
|
||||
synthio_synth_reset_buffer(&self->synth, single_channel_output, channel);
|
||||
self->synth.span.dur = 0;
|
||||
self->next_span = 0;
|
||||
start_parse(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 (self->synth.span.dur == 0) {
|
||||
if (self->next_span >= self->total_spans) {
|
||||
*buffer_length = 0;
|
||||
return GET_BUFFER_DONE;
|
||||
}
|
||||
self->synth.span = self->track[self->next_span++];
|
||||
}
|
||||
|
||||
synthio_synth_synthesize(&self->synth, buffer, buffer_length, single_channel_output ? 0 : channel);
|
||||
|
||||
return (self->synth.span.dur == 0 && self->next_span >= self->total_spans) ?
|
||||
GET_BUFFER_DONE : GET_BUFFER_MORE_DATA;
|
||||
if (self->synth.span.dur == 0) {
|
||||
if (self->pos == self->track.len) {
|
||||
return GET_BUFFER_DONE;
|
||||
} else {
|
||||
decode_until_pause(self);
|
||||
}
|
||||
}
|
||||
return GET_BUFFER_MORE_DATA;
|
||||
}
|
||||
|
||||
void synthio_miditrack_get_buffer_structure(synthio_miditrack_obj_t *self, bool single_channel_output,
|
||||
|
|
|
@ -34,9 +34,11 @@
|
|||
typedef struct {
|
||||
mp_obj_base_t base;
|
||||
synthio_synth_t synth;
|
||||
uint16_t next_span;
|
||||
uint16_t total_spans;
|
||||
synthio_midi_span_t *track;
|
||||
mp_buffer_info_t track;
|
||||
// invariant: after initial startup, pos always points just after an encoded duration, i.e., at a midi message (or at EOF)
|
||||
size_t pos;
|
||||
mp_int_t error_location;
|
||||
uint32_t tempo;
|
||||
} synthio_miditrack_obj_t;
|
||||
|
||||
|
||||
|
|
|
@ -30,13 +30,10 @@
|
|||
|
||||
|
||||
void common_hal_synthio_synthesizer_construct(synthio_synthesizer_obj_t *self,
|
||||
uint32_t sample_rate, const int16_t *waveform, uint16_t waveform_length) {
|
||||
uint32_t sample_rate, const int16_t *waveform, uint16_t waveform_length,
|
||||
mp_obj_t envelope) {
|
||||
|
||||
self->synth.sample_rate = sample_rate;
|
||||
self->synth.waveform = waveform;
|
||||
self->synth.waveform_length = waveform_length;
|
||||
synthio_synth_init(&self->synth, SYNTHIO_MAX_DUR);
|
||||
common_hal_synthio_synthesizer_release_all(self);
|
||||
synthio_synth_init(&self->synth, sample_rate, waveform, waveform_length, envelope);
|
||||
}
|
||||
|
||||
void common_hal_synthio_synthesizer_deinit(synthio_synthesizer_obj_t *self) {
|
||||
|
@ -74,14 +71,18 @@ void synthio_synthesizer_get_buffer_structure(synthio_synthesizer_obj_t *self, b
|
|||
}
|
||||
|
||||
void common_hal_synthio_synthesizer_release_all(synthio_synthesizer_obj_t *self) {
|
||||
synthio_span_init(&self->synth.span);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
void common_hal_synthio_synthesizer_release(synthio_synthesizer_obj_t *self, mp_obj_t to_release) {
|
||||
mp_obj_iter_buf_t iter_buf;
|
||||
mp_obj_t iterable = mp_getiter(to_release, &iter_buf);
|
||||
mp_obj_t item;
|
||||
while ((item = mp_iternext(iterable)) != MP_OBJ_STOP_ITERATION) {
|
||||
synthio_span_change_note(&self->synth.span, mp_arg_validate_int_range(mp_obj_get_int(item), 0, 127, MP_QSTR_note), SYNTHIO_SILENCE);
|
||||
synthio_span_change_note(&self->synth, mp_arg_validate_int_range(mp_obj_get_int(item), 0, 127, MP_QSTR_note), SYNTHIO_SILENCE);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,15 +91,21 @@ 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 item;
|
||||
while ((item = mp_iternext(iterable)) != MP_OBJ_STOP_ITERATION) {
|
||||
synthio_span_change_note(&self->synth.span, SYNTHIO_SILENCE, mp_arg_validate_int_range(mp_obj_get_int(item), 0, 127, MP_QSTR_note));
|
||||
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 common_hal_synthio_synthesizer_get_pressed_notes(synthio_synthesizer_obj_t *self) {
|
||||
mp_obj_tuple_t *result = MP_OBJ_TO_PTR(mp_obj_new_tuple(synthio_span_count_active_channels(&self->synth.span), NULL));
|
||||
for (size_t i = 0, j = 0; i < CIRCUITPY_SYNTHIO_MAX_CHANNELS; i++) {
|
||||
if (self->synth.span.note[i] != SYNTHIO_SILENCE) {
|
||||
result->items[j++] = MP_OBJ_NEW_SMALL_INT(self->synth.span.note[i]);
|
||||
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) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
return MP_OBJ_FROM_PTR(result);
|
||||
|
|
|
@ -26,18 +26,117 @@
|
|||
*/
|
||||
|
||||
#include "shared-module/synthio/__init__.h"
|
||||
#include "shared-bindings/synthio/__init__.h"
|
||||
#include "py/runtime.h"
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
STATIC const int16_t square_wave[] = {-32768, 32767};
|
||||
|
||||
STATIC const uint16_t notes[] = {8372, 8870, 9397, 9956, 10548, 11175, 11840,
|
||||
12544, 13290, 14080, 14917, 15804}; // 9th octave
|
||||
|
||||
int synthio_span_count_active_channels(synthio_midi_span_t *span) {
|
||||
int result = 0;
|
||||
for (int i = 0; i < CIRCUITPY_SYNTHIO_MAX_CHANNELS; i++) {
|
||||
if (span->note[i] != SYNTHIO_SILENCE) {
|
||||
result += 1;
|
||||
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);
|
||||
if (num_samples == 0) {
|
||||
return 0;
|
||||
}
|
||||
int16_t result = MIN(32767, MAX(1, abs(difference * SYNTHIO_MAX_DUR) / num_samples));
|
||||
return (difference < 0) ? -result : result;
|
||||
}
|
||||
|
||||
STATIC 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;
|
||||
envelope->attack_step = 32767;
|
||||
envelope->decay_step = -32767;
|
||||
envelope->release_step = -32767;
|
||||
return;
|
||||
}
|
||||
mp_arg_validate_type(obj, (mp_obj_type_t *)&synthio_envelope_type_obj, MP_QSTR_envelope);
|
||||
|
||||
size_t len;
|
||||
mp_obj_t *fields;
|
||||
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->attack_step = convert_time_to_rate(
|
||||
sample_rate, fields[0], envelope->attack_level);
|
||||
|
||||
envelope->decay_step = -convert_time_to_rate(
|
||||
sample_rate, fields[1], envelope->attack_level - envelope->sustain_level);
|
||||
|
||||
envelope->release_step = -convert_time_to_rate(
|
||||
sample_rate, fields[2],
|
||||
envelope->decay_step
|
||||
? envelope->sustain_level
|
||||
: envelope->attack_level);
|
||||
}
|
||||
|
||||
STATIC void synthio_envelope_state_step(synthio_envelope_state_t *state, synthio_envelope_definition_t *def, size_t n_steps) {
|
||||
state->substep += n_steps;
|
||||
while (state->substep >= SYNTHIO_MAX_DUR) {
|
||||
// max n_steps should be SYNTHIO_MAX_DUR so this loop executes at most
|
||||
// once
|
||||
state->substep -= SYNTHIO_MAX_DUR;
|
||||
switch (state->state) {
|
||||
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;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
STATIC void synthio_envelope_state_init(synthio_envelope_state_t *state, synthio_envelope_definition_t *def) {
|
||||
state->level = 0;
|
||||
state->substep = 0;
|
||||
state->state = SYNTHIO_ENVELOPE_STATE_ATTACK;
|
||||
|
||||
synthio_envelope_state_step(state, def, SYNTHIO_MAX_DUR);
|
||||
}
|
||||
|
||||
STATIC void synthio_envelope_state_release(synthio_envelope_state_t *state, synthio_envelope_definition_t *def) {
|
||||
state->state = SYNTHIO_ENVELOPE_STATE_RELEASE;
|
||||
}
|
||||
|
||||
|
||||
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) {
|
||||
result += synth->envelope_state[chan].level;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
@ -62,16 +161,23 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t
|
|||
memset(out_buffer, 0, synth->buffer_length);
|
||||
|
||||
int32_t sample_rate = synth->sample_rate;
|
||||
int active_channels = synthio_span_count_active_channels(&synth->span);
|
||||
uint32_t total_envelope = synthio_synth_sum_envelope(synth);
|
||||
const int16_t *waveform = synth->waveform;
|
||||
uint32_t waveform_length = synth->waveform_length;
|
||||
if (active_channels) {
|
||||
int16_t loudness = 0xffff / (1 + 2 * active_channels);
|
||||
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) {
|
||||
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
|
||||
synth->span.note[chan] = SYNTHIO_SILENCE;
|
||||
}
|
||||
uint8_t octave = synth->span.note[chan] / 12;
|
||||
uint16_t base_freq = notes[synth->span.note[chan] % 12];
|
||||
uint32_t accum = synth->accum[chan];
|
||||
|
@ -95,6 +201,11 @@ 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);
|
||||
}
|
||||
|
||||
*buffer_length = synth->last_buffer_length = dur * SYNTHIO_BYTES_PER_SAMPLE;
|
||||
*bufptr = (uint8_t *)out_buffer;
|
||||
}
|
||||
|
@ -117,11 +228,28 @@ void synthio_synth_deinit(synthio_synth_t *synth) {
|
|||
synth->buffers[1] = NULL;
|
||||
}
|
||||
|
||||
void synthio_synth_init(synthio_synth_t *synth, uint16_t max_dur) {
|
||||
synth->buffer_length = MIN(SYNTHIO_MAX_DUR, max_dur) * SYNTHIO_BYTES_PER_SAMPLE;
|
||||
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);
|
||||
synth->envelope_obj = envelope_obj;
|
||||
}
|
||||
|
||||
mp_obj_t synthio_synth_envelope_get(synthio_synth_t *synth) {
|
||||
return synth->envelope_obj;
|
||||
}
|
||||
|
||||
void synthio_synth_init(synthio_synth_t *synth, uint32_t sample_rate, const int16_t *waveform, uint16_t waveform_length, mp_obj_t envelope_obj) {
|
||||
synth->buffer_length = SYNTHIO_MAX_DUR * SYNTHIO_BYTES_PER_SAMPLE;
|
||||
synth->buffers[0] = m_malloc(synth->buffer_length, false);
|
||||
synth->buffers[1] = m_malloc(synth->buffer_length, false);
|
||||
synth->other_channel = -1;
|
||||
synth->waveform = waveform;
|
||||
synth->waveform_length = waveform_length;
|
||||
synth->sample_rate = sample_rate;
|
||||
synthio_synth_envelope_set(synth, envelope_obj);
|
||||
|
||||
for (size_t i = 0; i < CIRCUITPY_SYNTHIO_MAX_CHANNELS; i++) {
|
||||
synth->span.note[i] = SYNTHIO_SILENCE;
|
||||
}
|
||||
}
|
||||
|
||||
void synthio_synth_get_buffer_structure(synthio_synth_t *synth, bool single_channel_output,
|
||||
|
@ -132,40 +260,57 @@ 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) {
|
||||
if (o != mp_const_none) {
|
||||
mp_get_buffer_raise(o, bufinfo, MP_BUFFER_READ);
|
||||
if (bufinfo->typecode != 'h') {
|
||||
mp_raise_ValueError_varg(translate("%q must be array of type 'h'"), what);
|
||||
}
|
||||
mp_arg_validate_length_range(bufinfo->len / 2, 2, 1024, what);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void synthio_synth_parse_waveform(mp_buffer_info_t *bufinfo_waveform, mp_obj_t waveform_obj) {
|
||||
*bufinfo_waveform = ((mp_buffer_info_t) { .buf = (void *)square_wave, .len = 4 });
|
||||
|
||||
if (waveform_obj != mp_const_none) {
|
||||
mp_get_buffer_raise(waveform_obj, bufinfo_waveform, MP_BUFFER_READ);
|
||||
if (bufinfo_waveform->typecode != 'h') {
|
||||
mp_raise_ValueError_varg(translate("%q must be array of type 'h'"), MP_QSTR_waveform);
|
||||
}
|
||||
}
|
||||
mp_arg_validate_length_range(bufinfo_waveform->len / 2, 2, 1024, MP_QSTR_waveform);
|
||||
parse_common(bufinfo_waveform, waveform_obj, MP_QSTR_waveform);
|
||||
}
|
||||
|
||||
void synthio_span_init(synthio_midi_span_t *span) {
|
||||
span->dur = 0;
|
||||
for (size_t i = 0; i < CIRCUITPY_SYNTHIO_MAX_CHANNELS; i++) { span->note[i] = SYNTHIO_SILENCE;
|
||||
}
|
||||
}
|
||||
|
||||
STATIC int find_channel_with_note(const synthio_midi_span_t *span, uint8_t note) {
|
||||
STATIC int find_channel_with_note(synthio_synth_t *synth, uint8_t note) {
|
||||
for (int i = 0; i < CIRCUITPY_SYNTHIO_MAX_CHANNELS; i++) {
|
||||
if (span->note[i] == note) {
|
||||
if (synth->span.note[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)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool synthio_span_change_note(synthio_midi_span_t *span, uint8_t old_note, uint8_t new_note) {
|
||||
if (new_note != SYNTHIO_SILENCE && find_channel_with_note(span, new_note) != -1) {
|
||||
return false; // note already pressed, do nothing
|
||||
bool synthio_span_change_note(synthio_synth_t *synth, uint8_t old_note, uint8_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;
|
||||
return true;
|
||||
}
|
||||
int channel = find_channel_with_note(span, old_note);
|
||||
channel = find_channel_with_note(synth, old_note);
|
||||
if (channel != -1) {
|
||||
span->note[channel] = new_note;
|
||||
if (new_note == SYNTHIO_SILENCE) {
|
||||
synthio_envelope_state_release(&synth->envelope_state[channel], &synth->envelope_definition);
|
||||
} else {
|
||||
synth->span.note[channel] = new_note;
|
||||
synthio_envelope_state_init(&synth->envelope_state[channel], &synth->envelope_definition);
|
||||
synth->accum[channel] = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
#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)
|
||||
|
||||
#include "shared-module/audiocore/__init__.h"
|
||||
|
||||
|
@ -39,6 +40,25 @@ typedef struct {
|
|||
} synthio_midi_span_t;
|
||||
|
||||
typedef struct {
|
||||
// the number of attack or decay steps (signed) per sample
|
||||
// therefore the maximum time is 32767 samples or 0.68s at 48kHz
|
||||
// provided the level is maximum (this should be increased!)
|
||||
int16_t attack_step, decay_step, release_step;
|
||||
uint16_t attack_level, sustain_level;
|
||||
} synthio_envelope_definition_t;
|
||||
|
||||
typedef enum {
|
||||
SYNTHIO_ENVELOPE_STATE_ATTACK, SYNTHIO_ENVELOPE_STATE_DECAY,
|
||||
SYNTHIO_ENVELOPE_STATE_SUSTAIN, SYNTHIO_ENVELOPE_STATE_RELEASE
|
||||
} envelope_state_e;
|
||||
|
||||
typedef struct {
|
||||
int16_t level;
|
||||
uint16_t substep;
|
||||
envelope_state_e state;
|
||||
} synthio_envelope_state_t;
|
||||
|
||||
typedef struct synthio_synth {
|
||||
uint32_t sample_rate;
|
||||
int16_t *buffers[2];
|
||||
const int16_t *waveform;
|
||||
|
@ -46,19 +66,24 @@ typedef struct {
|
|||
uint16_t last_buffer_length;
|
||||
uint8_t other_channel, buffer_index, other_buffer_index;
|
||||
uint16_t waveform_length;
|
||||
synthio_envelope_definition_t envelope_definition;
|
||||
mp_obj_t envelope_obj;
|
||||
synthio_midi_span_t span;
|
||||
uint32_t accum[CIRCUITPY_SYNTHIO_MAX_CHANNELS];
|
||||
synthio_envelope_state_t envelope_state[CIRCUITPY_SYNTHIO_MAX_CHANNELS];
|
||||
} synthio_synth_t;
|
||||
|
||||
void synthio_span_init(synthio_midi_span_t *span);
|
||||
void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **buffer, uint32_t *buffer_length, uint8_t channel);
|
||||
void synthio_synth_deinit(synthio_synth_t *synth);
|
||||
bool synthio_synth_deinited(synthio_synth_t *synth);
|
||||
void synthio_synth_init(synthio_synth_t *synth, uint16_t max_dur);
|
||||
void synthio_synth_init(synthio_synth_t *synth, uint32_t sample_rate, const int16_t *waveform, uint16_t waveform_length,
|
||||
mp_obj_t envelope);
|
||||
void synthio_synth_get_buffer_structure(synthio_synth_t *synth, bool single_channel_output,
|
||||
bool *single_buffer, bool *samples_signed, uint32_t *max_buffer_length, uint8_t *spacing);
|
||||
void synthio_synth_reset_buffer(synthio_synth_t *synth, bool single_channel_output, uint8_t channel);
|
||||
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_midi_span_t *span, uint8_t old_note, uint8_t new_note);
|
||||
int synthio_span_count_active_channels(synthio_midi_span_t *span);
|
||||
bool synthio_span_change_note(synthio_synth_t *synth, uint8_t old_note, uint8_t new_note);
|
||||
|
||||
void synthio_envelope_step(synthio_envelope_definition_t *definition, synthio_envelope_state_t *state, int n_samples);
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
*.wav
|
|
@ -0,0 +1,5 @@
|
|||
# Test synthio without hardware
|
||||
|
||||
Build the uninx 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.
|
|
@ -0,0 +1,11 @@
|
|||
# SPDX-FileCopyrightText: 2023 Guido van Rossum <guido@cwi.nl> and others.
|
||||
#
|
||||
# SPDX-License-Identifier: PSF-2.0
|
||||
|
||||
import struct
|
||||
|
||||
|
||||
def byteswap(data, sampwidth):
|
||||
print(data)
|
||||
raise
|
||||
ch = "I" if sampwidth == 16 else "H"
|
|
@ -0,0 +1,173 @@
|
|||
# SPDX-FileCopyrightText: 2023 Guido van Rossum <guido@cwi.nl> and others.
|
||||
#
|
||||
# SPDX-License-Identifier: PSF-2.0
|
||||
|
||||
"""Simple class to read IFF chunks.
|
||||
|
||||
An IFF chunk (used in formats such as AIFF, TIFF, RMFF (RealMedia File
|
||||
Format)) has the following structure:
|
||||
|
||||
+----------------+
|
||||
| ID (4 bytes) |
|
||||
+----------------+
|
||||
| size (4 bytes) |
|
||||
+----------------+
|
||||
| data |
|
||||
| ... |
|
||||
+----------------+
|
||||
|
||||
The ID is a 4-byte string which identifies the type of chunk.
|
||||
|
||||
The size field (a 32-bit value, encoded using big-endian byte order)
|
||||
gives the size of the whole chunk, including the 8-byte header.
|
||||
|
||||
Usually an IFF-type file consists of one or more chunks. The proposed
|
||||
usage of the Chunk class defined here is to instantiate an instance at
|
||||
the start of each chunk and read from the instance until it reaches
|
||||
the end, after which a new instance can be instantiated. At the end
|
||||
of the file, creating a new instance will fail with an EOFError
|
||||
exception.
|
||||
|
||||
Usage:
|
||||
while True:
|
||||
try:
|
||||
chunk = Chunk(file)
|
||||
except EOFError:
|
||||
break
|
||||
chunktype = chunk.getname()
|
||||
while True:
|
||||
data = chunk.read(nbytes)
|
||||
if not data:
|
||||
pass
|
||||
# do something with data
|
||||
|
||||
The interface is file-like. The implemented methods are:
|
||||
read, close, seek, tell, isatty.
|
||||
Extra methods are: skip() (called by close, skips to the end of the chunk),
|
||||
getname() (returns the name (ID) of the chunk)
|
||||
|
||||
The __init__ method has one required argument, a file-like object
|
||||
(including a chunk instance), and one optional argument, a flag which
|
||||
specifies whether or not chunks are aligned on 2-byte boundaries. The
|
||||
default is 1, i.e. aligned.
|
||||
"""
|
||||
|
||||
|
||||
class Chunk:
|
||||
def __init__(self, file, align=True, bigendian=True, inclheader=False):
|
||||
import struct
|
||||
|
||||
self.closed = False
|
||||
self.align = align # whether to align to word (2-byte) boundaries
|
||||
if bigendian:
|
||||
strflag = ">"
|
||||
else:
|
||||
strflag = "<"
|
||||
self.file = file
|
||||
self.chunkname = file.read(4)
|
||||
if len(self.chunkname) < 4:
|
||||
raise EOFError
|
||||
try:
|
||||
self.chunksize = struct.unpack_from(strflag + "L", file.read(4))[0]
|
||||
except struct.error:
|
||||
raise EOFError from None
|
||||
if inclheader:
|
||||
self.chunksize = self.chunksize - 8 # subtract header
|
||||
self.size_read = 0
|
||||
try:
|
||||
self.offset = self.file.tell()
|
||||
except (AttributeError, OSError):
|
||||
self.seekable = False
|
||||
else:
|
||||
self.seekable = True
|
||||
|
||||
def getname(self):
|
||||
"""Return the name (ID) of the current chunk."""
|
||||
return self.chunkname
|
||||
|
||||
def getsize(self):
|
||||
"""Return the size of the current chunk."""
|
||||
return self.chunksize
|
||||
|
||||
def close(self):
|
||||
if not self.closed:
|
||||
try:
|
||||
self.skip()
|
||||
finally:
|
||||
self.closed = True
|
||||
|
||||
def isatty(self):
|
||||
if self.closed:
|
||||
raise ValueError("I/O operation on closed file")
|
||||
return False
|
||||
|
||||
def seek(self, pos, whence=0):
|
||||
"""Seek to specified position into the chunk.
|
||||
Default position is 0 (start of chunk).
|
||||
If the file is not seekable, this will result in an error.
|
||||
"""
|
||||
|
||||
if self.closed:
|
||||
raise ValueError("I/O operation on closed file")
|
||||
if not self.seekable:
|
||||
raise OSError("cannot seek")
|
||||
if whence == 1:
|
||||
pos = pos + self.size_read
|
||||
elif whence == 2:
|
||||
pos = pos + self.chunksize
|
||||
if pos < 0 or pos > self.chunksize:
|
||||
raise RuntimeError
|
||||
self.file.seek(self.offset + pos, 0)
|
||||
self.size_read = pos
|
||||
|
||||
def tell(self):
|
||||
if self.closed:
|
||||
raise ValueError("I/O operation on closed file")
|
||||
return self.size_read
|
||||
|
||||
def read(self, size=-1):
|
||||
"""Read at most size bytes from the chunk.
|
||||
If size is omitted or negative, read until the end
|
||||
of the chunk.
|
||||
"""
|
||||
|
||||
if self.closed:
|
||||
raise ValueError("I/O operation on closed file")
|
||||
if self.size_read >= self.chunksize:
|
||||
return b""
|
||||
if size < 0:
|
||||
size = self.chunksize - self.size_read
|
||||
if size > self.chunksize - self.size_read:
|
||||
size = self.chunksize - self.size_read
|
||||
data = self.file.read(size)
|
||||
self.size_read = self.size_read + len(data)
|
||||
if self.size_read == self.chunksize and self.align and (self.chunksize & 1):
|
||||
dummy = self.file.read(1)
|
||||
self.size_read = self.size_read + len(dummy)
|
||||
return data
|
||||
|
||||
def skip(self):
|
||||
"""Skip the rest of the chunk.
|
||||
If you are not interested in the contents of the chunk,
|
||||
this method should be called so that the file points to
|
||||
the start of the next chunk.
|
||||
"""
|
||||
|
||||
if self.closed:
|
||||
raise ValueError("I/O operation on closed file")
|
||||
if self.seekable:
|
||||
try:
|
||||
n = self.chunksize - self.size_read
|
||||
# maybe fix alignment
|
||||
if self.align and (self.chunksize & 1):
|
||||
n = n + 1
|
||||
self.file.seek(n, 1)
|
||||
self.size_read = self.size_read + n
|
||||
return
|
||||
except OSError:
|
||||
pass
|
||||
while self.size_read < self.chunksize:
|
||||
n = min(8192, self.chunksize - self.size_read)
|
||||
dummy = self.read(n)
|
||||
if not dummy:
|
||||
raise EOFError
|
|
@ -0,0 +1,59 @@
|
|||
import audiocore
|
||||
import synthio
|
||||
from ulab import numpy as np
|
||||
import wave
|
||||
|
||||
SAMPLE_SIZE = 1024
|
||||
VOLUME = 32700
|
||||
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
|
||||
)
|
||||
melody = synthio.MidiTrack(
|
||||
b"\0\x90H\0*\x80H\0\6\x90J\0*\x80J\0\6\x90L\0*\x80L\0\6\x90J\0"
|
||||
+ b"*\x80J\0\6\x90H\0*\x80H\0\6\x90J\0*\x80J\0\6\x90L\0T\x80L\0"
|
||||
+ b"\x0c\x90H\0T\x80H\0\x0c\x90H\0T\x80H\0",
|
||||
tempo=240,
|
||||
sample_rate=48000,
|
||||
waveform=sine,
|
||||
envelope=envelope,
|
||||
)
|
||||
|
||||
# sox -r 48000 -e signed -b 16 -c 1 tune.raw tune.wav
|
||||
with wave.open("tune.wav", "w") as f:
|
||||
f.setnchannels(1)
|
||||
f.setsampwidth(2)
|
||||
f.setframerate(48000)
|
||||
while True:
|
||||
result, data = audiocore.get_buffer(melody)
|
||||
if data is None:
|
||||
break
|
||||
f.writeframes(data)
|
||||
if result != 1:
|
||||
break
|
||||
|
||||
melody = synthio.MidiTrack(
|
||||
b"\0\x90H\0*\x80H\0\6\x90J\0*\x80J\0\6\x90L\0*\x80L\0\6\x90J\0"
|
||||
+ b"*\x80J\0\6\x90H\0*\x80H\0\6\x90J\0*\x80J\0\6\x90L\0T\x80L\0"
|
||||
+ b"\x0c\x90H\0T\x80H\0\x0c\x90H\0T\x80H\0",
|
||||
tempo=240,
|
||||
sample_rate=48000,
|
||||
waveform=sine,
|
||||
)
|
||||
|
||||
# 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)
|
||||
while True:
|
||||
result, data = audiocore.get_buffer(melody)
|
||||
if data is None:
|
||||
break
|
||||
f.writeframes(data)
|
||||
if result != 1:
|
||||
break
|
|
@ -0,0 +1,550 @@
|
|||
# SPDX-FileCopyrightText: 2023 Guido van Rossum <guido@cwi.nl> and others.
|
||||
#
|
||||
# SPDX-License-Identifier: PSF-2.0
|
||||
|
||||
"""Stuff to parse WAVE files.
|
||||
|
||||
Usage.
|
||||
|
||||
Reading WAVE files:
|
||||
f = wave.open(file, 'r')
|
||||
where file is either the name of a file or an open file pointer.
|
||||
The open file pointer must have methods read(), seek(), and close().
|
||||
When the setpos() and rewind() methods are not used, the seek()
|
||||
method is not necessary.
|
||||
|
||||
This returns an instance of a class with the following public methods:
|
||||
getnchannels() -- returns number of audio channels (1 for
|
||||
mono, 2 for stereo)
|
||||
getsampwidth() -- returns sample width in bytes
|
||||
getframerate() -- returns sampling frequency
|
||||
getnframes() -- returns number of audio frames
|
||||
getcomptype() -- returns compression type ('NONE' for linear samples)
|
||||
getcompname() -- returns human-readable version of
|
||||
compression type ('not compressed' linear samples)
|
||||
getparams() -- returns a namedtuple consisting of all of the
|
||||
above in the above order
|
||||
getmarkers() -- returns None (for compatibility with the
|
||||
aifc module)
|
||||
getmark(id) -- raises an error since the mark does not
|
||||
exist (for compatibility with the aifc module)
|
||||
readframes(n) -- returns at most n frames of audio
|
||||
rewind() -- rewind to the beginning of the audio stream
|
||||
setpos(pos) -- seek to the specified position
|
||||
tell() -- return the current position
|
||||
close() -- close the instance (make it unusable)
|
||||
The position returned by tell() and the position given to setpos()
|
||||
are compatible and have nothing to do with the actual position in the
|
||||
file.
|
||||
The close() method is called automatically when the class instance
|
||||
is destroyed.
|
||||
|
||||
Writing WAVE files:
|
||||
f = wave.open(file, 'w')
|
||||
where file is either the name of a file or an open file pointer.
|
||||
The open file pointer must have methods write(), tell(), seek(), and
|
||||
close().
|
||||
|
||||
This returns an instance of a class with the following public methods:
|
||||
setnchannels(n) -- set the number of channels
|
||||
setsampwidth(n) -- set the sample width
|
||||
setframerate(n) -- set the frame rate
|
||||
setnframes(n) -- set the number of frames
|
||||
setcomptype(type, name)
|
||||
-- set the compression type and the
|
||||
human-readable compression type
|
||||
setparams(tuple)
|
||||
-- set all parameters at once
|
||||
tell() -- return current position in output file
|
||||
writeframesraw(data)
|
||||
-- write audio frames without patching up the
|
||||
file header
|
||||
writeframes(data)
|
||||
-- write audio frames and patch up the file header
|
||||
close() -- patch up the file header and close the
|
||||
output file
|
||||
You should set the parameters before the first writeframesraw or
|
||||
writeframes. The total number of frames does not need to be set,
|
||||
but when it is set to the correct value, the header does not have to
|
||||
be patched up.
|
||||
It is best to first set all parameters, perhaps possibly the
|
||||
compression type, and then write audio frames using writeframesraw.
|
||||
When all frames have been written, either call writeframes(b'') or
|
||||
close() to patch up the sizes in the header.
|
||||
The close() method is called automatically when the class instance
|
||||
is destroyed.
|
||||
"""
|
||||
|
||||
from chunk import Chunk
|
||||
from collections import namedtuple
|
||||
import audioop
|
||||
import builtins
|
||||
import struct
|
||||
import sys
|
||||
|
||||
|
||||
__all__ = ["open", "Error", "Wave_read", "Wave_write"]
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
WAVE_FORMAT_PCM = 0x0001
|
||||
|
||||
_array_fmts = None, "b", "h", None, "i"
|
||||
|
||||
_wave_params = namedtuple(
|
||||
"_wave_params", "nchannels sampwidth framerate nframes comptype compname"
|
||||
)
|
||||
|
||||
|
||||
class Wave_read:
|
||||
"""Variables used in this class:
|
||||
|
||||
These variables are available to the user though appropriate
|
||||
methods of this class:
|
||||
_file -- the open file with methods read(), close(), and seek()
|
||||
set through the __init__() method
|
||||
_nchannels -- the number of audio channels
|
||||
available through the getnchannels() method
|
||||
_nframes -- the number of audio frames
|
||||
available through the getnframes() method
|
||||
_sampwidth -- the number of bytes per audio sample
|
||||
available through the getsampwidth() method
|
||||
_framerate -- the sampling frequency
|
||||
available through the getframerate() method
|
||||
_comptype -- the AIFF-C compression type ('NONE' if AIFF)
|
||||
available through the getcomptype() method
|
||||
_compname -- the human-readable AIFF-C compression type
|
||||
available through the getcomptype() method
|
||||
_soundpos -- the position in the audio stream
|
||||
available through the tell() method, set through the
|
||||
setpos() method
|
||||
|
||||
These variables are used internally only:
|
||||
_fmt_chunk_read -- 1 iff the FMT chunk has been read
|
||||
_data_seek_needed -- 1 iff positioned correctly in audio
|
||||
file for readframes()
|
||||
_data_chunk -- instantiation of a chunk class for the DATA chunk
|
||||
_framesize -- size of one frame in the file
|
||||
"""
|
||||
|
||||
def initfp(self, file):
|
||||
self._convert = None
|
||||
self._soundpos = 0
|
||||
self._file = Chunk(file, bigendian=0)
|
||||
if self._file.getname() != b"RIFF":
|
||||
raise Error("file does not start with RIFF id")
|
||||
if self._file.read(4) != b"WAVE":
|
||||
raise Error("not a WAVE file")
|
||||
self._fmt_chunk_read = 0
|
||||
self._data_chunk = None
|
||||
while 1:
|
||||
self._data_seek_needed = 1
|
||||
try:
|
||||
chunk = Chunk(self._file, bigendian=0)
|
||||
except EOFError:
|
||||
break
|
||||
chunkname = chunk.getname()
|
||||
if chunkname == b"fmt ":
|
||||
self._read_fmt_chunk(chunk)
|
||||
self._fmt_chunk_read = 1
|
||||
elif chunkname == b"data":
|
||||
if not self._fmt_chunk_read:
|
||||
raise Error("data chunk before fmt chunk")
|
||||
self._data_chunk = chunk
|
||||
self._nframes = chunk.chunksize // self._framesize
|
||||
self._data_seek_needed = 0
|
||||
break
|
||||
chunk.skip()
|
||||
if not self._fmt_chunk_read or not self._data_chunk:
|
||||
raise Error("fmt chunk and/or data chunk missing")
|
||||
|
||||
def __init__(self, f):
|
||||
self._i_opened_the_file = None
|
||||
if isinstance(f, str):
|
||||
f = builtins.open(f, "rb")
|
||||
self._i_opened_the_file = f
|
||||
# else, assume it is an open file object already
|
||||
try:
|
||||
self.initfp(f)
|
||||
except:
|
||||
if self._i_opened_the_file:
|
||||
f.close()
|
||||
raise
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.close()
|
||||
|
||||
#
|
||||
# User visible methods.
|
||||
#
|
||||
def getfp(self):
|
||||
return self._file
|
||||
|
||||
def rewind(self):
|
||||
self._data_seek_needed = 1
|
||||
self._soundpos = 0
|
||||
|
||||
def close(self):
|
||||
self._file = None
|
||||
file = self._i_opened_the_file
|
||||
if file:
|
||||
self._i_opened_the_file = None
|
||||
file.close()
|
||||
|
||||
def tell(self):
|
||||
return self._soundpos
|
||||
|
||||
def getnchannels(self):
|
||||
return self._nchannels
|
||||
|
||||
def getnframes(self):
|
||||
return self._nframes
|
||||
|
||||
def getsampwidth(self):
|
||||
return self._sampwidth
|
||||
|
||||
def getframerate(self):
|
||||
return self._framerate
|
||||
|
||||
def getcomptype(self):
|
||||
return self._comptype
|
||||
|
||||
def getcompname(self):
|
||||
return self._compname
|
||||
|
||||
def getparams(self):
|
||||
return _wave_params(
|
||||
self.getnchannels(),
|
||||
self.getsampwidth(),
|
||||
self.getframerate(),
|
||||
self.getnframes(),
|
||||
self.getcomptype(),
|
||||
self.getcompname(),
|
||||
)
|
||||
|
||||
def getmarkers(self):
|
||||
return None
|
||||
|
||||
def getmark(self, id):
|
||||
raise Error("no marks")
|
||||
|
||||
def setpos(self, pos):
|
||||
if pos < 0 or pos > self._nframes:
|
||||
raise Error("position not in range")
|
||||
self._soundpos = pos
|
||||
self._data_seek_needed = 1
|
||||
|
||||
def readframes(self, nframes):
|
||||
if self._data_seek_needed:
|
||||
self._data_chunk.seek(0, 0)
|
||||
pos = self._soundpos * self._framesize
|
||||
if pos:
|
||||
self._data_chunk.seek(pos, 0)
|
||||
self._data_seek_needed = 0
|
||||
if nframes == 0:
|
||||
return b""
|
||||
data = self._data_chunk.read(nframes * self._framesize)
|
||||
if self._sampwidth != 1 and sys.byteorder == "big":
|
||||
data = audioop.byteswap(data, self._sampwidth)
|
||||
if self._convert and data:
|
||||
data = self._convert(data)
|
||||
self._soundpos = self._soundpos + len(data) // (self._nchannels * self._sampwidth)
|
||||
return data
|
||||
|
||||
#
|
||||
# Internal methods.
|
||||
#
|
||||
|
||||
def _read_fmt_chunk(self, chunk):
|
||||
try:
|
||||
(
|
||||
wFormatTag,
|
||||
self._nchannels,
|
||||
self._framerate,
|
||||
dwAvgBytesPerSec,
|
||||
wBlockAlign,
|
||||
) = struct.unpack_from("<HHLLH", chunk.read(14))
|
||||
except struct.error:
|
||||
raise EOFError from None
|
||||
if wFormatTag == WAVE_FORMAT_PCM:
|
||||
try:
|
||||
sampwidth = struct.unpack_from("<H", chunk.read(2))[0]
|
||||
except struct.error:
|
||||
raise EOFError from None
|
||||
self._sampwidth = (sampwidth + 7) // 8
|
||||
if not self._sampwidth:
|
||||
raise Error("bad sample width")
|
||||
else:
|
||||
raise Error("unknown format: %r" % (wFormatTag,))
|
||||
if not self._nchannels:
|
||||
raise Error("bad # of channels")
|
||||
self._framesize = self._nchannels * self._sampwidth
|
||||
self._comptype = "NONE"
|
||||
self._compname = "not compressed"
|
||||
|
||||
|
||||
class Wave_write:
|
||||
"""Variables used in this class:
|
||||
|
||||
These variables are user settable through appropriate methods
|
||||
of this class:
|
||||
_file -- the open file with methods write(), close(), tell(), seek()
|
||||
set through the __init__() method
|
||||
_comptype -- the AIFF-C compression type ('NONE' in AIFF)
|
||||
set through the setcomptype() or setparams() method
|
||||
_compname -- the human-readable AIFF-C compression type
|
||||
set through the setcomptype() or setparams() method
|
||||
_nchannels -- the number of audio channels
|
||||
set through the setnchannels() or setparams() method
|
||||
_sampwidth -- the number of bytes per audio sample
|
||||
set through the setsampwidth() or setparams() method
|
||||
_framerate -- the sampling frequency
|
||||
set through the setframerate() or setparams() method
|
||||
_nframes -- the number of audio frames written to the header
|
||||
set through the setnframes() or setparams() method
|
||||
|
||||
These variables are used internally only:
|
||||
_datalength -- the size of the audio samples written to the header
|
||||
_nframeswritten -- the number of frames actually written
|
||||
_datawritten -- the size of the audio samples actually written
|
||||
"""
|
||||
|
||||
def __init__(self, f):
|
||||
self._i_opened_the_file = None
|
||||
if isinstance(f, str):
|
||||
f = builtins.open(f, "wb")
|
||||
self._i_opened_the_file = f
|
||||
try:
|
||||
self.initfp(f)
|
||||
except:
|
||||
if self._i_opened_the_file:
|
||||
f.close()
|
||||
raise
|
||||
|
||||
def initfp(self, file):
|
||||
self._file = file
|
||||
self._convert = None
|
||||
self._nchannels = 0
|
||||
self._sampwidth = 0
|
||||
self._framerate = 0
|
||||
self._nframes = 0
|
||||
self._nframeswritten = 0
|
||||
self._datawritten = 0
|
||||
self._datalength = 0
|
||||
self._headerwritten = False
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.close()
|
||||
|
||||
#
|
||||
# User visible methods.
|
||||
#
|
||||
def setnchannels(self, nchannels):
|
||||
if self._datawritten:
|
||||
raise Error("cannot change parameters after starting to write")
|
||||
if nchannels < 1:
|
||||
raise Error("bad # of channels")
|
||||
self._nchannels = nchannels
|
||||
|
||||
def getnchannels(self):
|
||||
if not self._nchannels:
|
||||
raise Error("number of channels not set")
|
||||
return self._nchannels
|
||||
|
||||
def setsampwidth(self, sampwidth):
|
||||
if self._datawritten:
|
||||
raise Error("cannot change parameters after starting to write")
|
||||
if sampwidth < 1 or sampwidth > 4:
|
||||
raise Error("bad sample width")
|
||||
self._sampwidth = sampwidth
|
||||
|
||||
def getsampwidth(self):
|
||||
if not self._sampwidth:
|
||||
raise Error("sample width not set")
|
||||
return self._sampwidth
|
||||
|
||||
def setframerate(self, framerate):
|
||||
if self._datawritten:
|
||||
raise Error("cannot change parameters after starting to write")
|
||||
if framerate <= 0:
|
||||
raise Error("bad frame rate")
|
||||
self._framerate = int(round(framerate))
|
||||
|
||||
def getframerate(self):
|
||||
if not self._framerate:
|
||||
raise Error("frame rate not set")
|
||||
return self._framerate
|
||||
|
||||
def setnframes(self, nframes):
|
||||
if self._datawritten:
|
||||
raise Error("cannot change parameters after starting to write")
|
||||
self._nframes = nframes
|
||||
|
||||
def getnframes(self):
|
||||
return self._nframeswritten
|
||||
|
||||
def setcomptype(self, comptype, compname):
|
||||
if self._datawritten:
|
||||
raise Error("cannot change parameters after starting to write")
|
||||
if comptype not in ("NONE",):
|
||||
raise Error("unsupported compression type")
|
||||
self._comptype = comptype
|
||||
self._compname = compname
|
||||
|
||||
def getcomptype(self):
|
||||
return self._comptype
|
||||
|
||||
def getcompname(self):
|
||||
return self._compname
|
||||
|
||||
def setparams(self, params):
|
||||
nchannels, sampwidth, framerate, nframes, comptype, compname = params
|
||||
if self._datawritten:
|
||||
raise Error("cannot change parameters after starting to write")
|
||||
self.setnchannels(nchannels)
|
||||
self.setsampwidth(sampwidth)
|
||||
self.setframerate(framerate)
|
||||
self.setnframes(nframes)
|
||||
self.setcomptype(comptype, compname)
|
||||
|
||||
def getparams(self):
|
||||
if not self._nchannels or not self._sampwidth or not self._framerate:
|
||||
raise Error("not all parameters set")
|
||||
return _wave_params(
|
||||
self._nchannels,
|
||||
self._sampwidth,
|
||||
self._framerate,
|
||||
self._nframes,
|
||||
self._comptype,
|
||||
self._compname,
|
||||
)
|
||||
|
||||
def setmark(self, id, pos, name):
|
||||
raise Error("setmark() not supported")
|
||||
|
||||
def getmark(self, id):
|
||||
raise Error("no marks")
|
||||
|
||||
def getmarkers(self):
|
||||
return None
|
||||
|
||||
def tell(self):
|
||||
return self._nframeswritten
|
||||
|
||||
def writeframesraw(self, data):
|
||||
if not isinstance(data, (bytes, bytearray)):
|
||||
data = memoryview(data).cast("B")
|
||||
self._ensure_header_written(len(data))
|
||||
nframes = len(data) // (self._sampwidth * self._nchannels)
|
||||
if self._convert:
|
||||
data = self._convert(data)
|
||||
if self._sampwidth != 1 and sys.byteorder == "big":
|
||||
data = audioop.byteswap(data, self._sampwidth)
|
||||
self._file.write(data)
|
||||
self._datawritten += len(data)
|
||||
self._nframeswritten = self._nframeswritten + nframes
|
||||
|
||||
def writeframes(self, data):
|
||||
self.writeframesraw(data)
|
||||
if self._datalength != self._datawritten:
|
||||
self._patchheader()
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
if self._file:
|
||||
self._ensure_header_written(0)
|
||||
if self._datalength != self._datawritten:
|
||||
self._patchheader()
|
||||
self._file.flush()
|
||||
finally:
|
||||
self._file = None
|
||||
file = self._i_opened_the_file
|
||||
if file:
|
||||
self._i_opened_the_file = None
|
||||
file.close()
|
||||
|
||||
#
|
||||
# Internal methods.
|
||||
#
|
||||
|
||||
def _ensure_header_written(self, datasize):
|
||||
if not self._headerwritten:
|
||||
if not self._nchannels:
|
||||
raise Error("# channels not specified")
|
||||
if not self._sampwidth:
|
||||
raise Error("sample width not specified")
|
||||
if not self._framerate:
|
||||
raise Error("sampling rate not specified")
|
||||
self._write_header(datasize)
|
||||
|
||||
def _write_header(self, initlength):
|
||||
assert not self._headerwritten
|
||||
self._file.write(b"RIFF")
|
||||
if not self._nframes:
|
||||
self._nframes = initlength // (self._nchannels * self._sampwidth)
|
||||
self._datalength = self._nframes * self._nchannels * self._sampwidth
|
||||
try:
|
||||
self._form_length_pos = self._file.tell()
|
||||
except (AttributeError, OSError):
|
||||
self._form_length_pos = None
|
||||
self._file.write(
|
||||
struct.pack(
|
||||
"<L4s4sLHHLLHH4s",
|
||||
36 + self._datalength,
|
||||
b"WAVE",
|
||||
b"fmt ",
|
||||
16,
|
||||
WAVE_FORMAT_PCM,
|
||||
self._nchannels,
|
||||
self._framerate,
|
||||
self._nchannels * self._framerate * self._sampwidth,
|
||||
self._nchannels * self._sampwidth,
|
||||
self._sampwidth * 8,
|
||||
b"data",
|
||||
)
|
||||
)
|
||||
if self._form_length_pos is not None:
|
||||
self._data_length_pos = self._file.tell()
|
||||
self._file.write(struct.pack("<L", self._datalength))
|
||||
self._headerwritten = True
|
||||
|
||||
def _patchheader(self):
|
||||
assert self._headerwritten
|
||||
if self._datawritten == self._datalength:
|
||||
return
|
||||
curpos = self._file.tell()
|
||||
self._file.seek(self._form_length_pos, 0)
|
||||
self._file.write(struct.pack("<L", 36 + self._datawritten))
|
||||
self._file.seek(self._data_length_pos, 0)
|
||||
self._file.write(struct.pack("<L", self._datawritten))
|
||||
self._file.seek(curpos, 0)
|
||||
self._datalength = self._datawritten
|
||||
|
||||
|
||||
def open(f, mode=None):
|
||||
if mode is None:
|
||||
if hasattr(f, "mode"):
|
||||
mode = f.mode
|
||||
else:
|
||||
mode = "rb"
|
||||
if mode in ("r", "rb"):
|
||||
return Wave_read(f)
|
||||
elif mode in ("w", "wb"):
|
||||
return Wave_write(f)
|
||||
else:
|
||||
raise Error("mode must be 'r', 'rb', 'w', or 'wb'")
|
|
@ -11,10 +11,12 @@ SCORE = b"\0\x90@\0\x20\x90b\0\x20\x80@\0\0\x80\b\0"
|
|||
|
||||
with MidiTrack(SCORE, sample_rate=8000, tempo=640) as m:
|
||||
print(get_structure(m))
|
||||
print(get_buffer(m))
|
||||
p, q = get_buffer(m)
|
||||
print(p, list(q))
|
||||
|
||||
with MidiTrack(
|
||||
SCORE, sample_rate=8000, tempo=640, waveform=array.array("h", [0, 32767, 0, -32768])
|
||||
) as m:
|
||||
print(get_structure(m))
|
||||
print(get_buffer(m))
|
||||
p, q = get_buffer(m)
|
||||
print(p, list(q))
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
(0, 1, 512, 1)
|
||||
(1, b'V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\xaa*\xaa*')
|
||||
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]
|
||||
(0, 1, 512, 1)
|
||||
(1, b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\x00\x00\x00\x00')
|
||||
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,10 +1,11 @@
|
|||
import struct
|
||||
import synthio
|
||||
import audiocore
|
||||
import ulab.numpy as np
|
||||
|
||||
|
||||
def dump_samples():
|
||||
print(struct.unpack("12h", audiocore.get_buffer(s)[1][:24]))
|
||||
print([i for i in audiocore.get_buffer(s)[1][:24]])
|
||||
|
||||
|
||||
s = synthio.Synthesizer(sample_rate=8000)
|
||||
|
@ -22,3 +23,16 @@ dump_samples()
|
|||
s.release_then_press((80,))
|
||||
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
|
||||
)
|
||||
s = synthio.Synthesizer(sample_rate=8000, envelope=envelope)
|
||||
s.press((60,))
|
||||
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)))
|
||||
|
|
|
@ -1,8 +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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
(80,)
|
||||
(-10922, -10922, -10922, -10922, 10922, 10922, 10922, 10922, 10922, -10922, -10922, -10922)
|
||||
[-16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383]
|
||||
(80, 91)
|
||||
(0, 0, 13106, 13106, 0, -13106, -13106, 0, 13106, 13106, 0, 0)
|
||||
[0, 0, 16382, 16382, 0, -16382, -16382, 0, 16382, 16382, 0, 0, 16382, 0, 0, -16382, -16382, 0, 16382, 16382, 0, 0, 16382, 0]
|
||||
(91,)
|
||||
(-10922, 10922, 10922, 10922, -10922, -10922, 10922, 10922, 10922, -10922, -10922, 10922)
|
||||
[-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)
|
||||
(0, 0)
|
||||
(0, 0)
|
||||
(0, 0)
|
||||
(0, 0)
|
||||
(0, 0)
|
||||
|
|
Loading…
Reference in New Issue