Merge pull request #7862 from jepler/synthio-envelope

Synthio envelope
This commit is contained in:
Jeff Epler 2023-05-03 12:42:56 -05:00 committed by GitHub
commit bd9aca2526
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1439 additions and 193 deletions

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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) {
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));
}
STATIC mp_obj_t memoryview_itemsize_get(mp_obj_t self_in) {
mp_obj_array_t *self = MP_OBJ_TO_PTR(self_in);
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,

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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) },

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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 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[self->pos++];
delta <<= 7;
delta |= c & 0x7f;
} while ((c & 0x80) && (self->pos < len));
// errors cannot be raised from the background task, so simply end the track.
if (c & 0x80) {
self->pos = self->track.len;
print_midi_stream_error(self);
}
return delta * self->synth.sample_rate / self->tempo;
}
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) {
uint8_t c;
uint32_t delta = 0;
do {
c = buffer[pos++];
delta <<= 7;
delta |= c & 0x7f;
} while ((c & 0x80) && (pos < len));
if (c & 0x80) {
raise_midi_stream_error(pos);
}
// 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);
}
}
terminate_span(self, &dur);
if (self->pos < len) {
self->synth.span.dur = decode_duration(self);
}
} while (self->pos < len && self->synth.span.dur == 0);
}
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,

View File

@ -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;

View File

@ -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);

View File

@ -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 });
parse_common(bufinfo_waveform, waveform_obj, MP_QSTR_waveform);
}
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);
STATIC int find_channel_with_note(synthio_synth_t *synth, uint8_t note) {
for (int i = 0; i < CIRCUITPY_SYNTHIO_MAX_CHANNELS; i++) {
if (synth->span.note[i] == note) {
return i;
}
}
mp_arg_validate_length_range(bufinfo_waveform->len / 2, 2, 1024, MP_QSTR_waveform);
}
void synthio_span_init(synthio_midi_span_t *span) {
span->dur = 0;
for (size_t i = 0; i < CIRCUITPY_SYNTHIO_MAX_CHANNELS; i++) { span->note[i] = SYNTHIO_SILENCE;
}
}
STATIC int find_channel_with_note(const synthio_midi_span_t *span, uint8_t note) {
for (int i = 0; i < CIRCUITPY_SYNTHIO_MAX_CHANNELS; i++) {
if (span->note[i] == note) {
return i;
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;

View File

@ -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);

View File

@ -0,0 +1 @@
*.wav

View File

@ -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.

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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'")

View File

@ -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))

View File

@ -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]

View File

@ -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)))

View File

@ -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)