From 522c5a9a114c616d1daf6a9c495039b3c8e99bb4 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Fri, 12 May 2023 11:49:08 -0500 Subject: [PATCH 1/4] synthio: fix crash on synthesizer.press((float,)) --- shared-module/synthio/Synthesizer.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shared-module/synthio/Synthesizer.c b/shared-module/synthio/Synthesizer.c index c18c067976373..54bc283148aea 100644 --- a/shared-module/synthio/Synthesizer.c +++ b/shared-module/synthio/Synthesizer.c @@ -110,11 +110,12 @@ void common_hal_synthio_synthesizer_press(synthio_synthesizer_obj_t *self, mp_ob mp_obj_t iterable = mp_getiter(to_press, &iter_buf); mp_obj_t note_obj; while ((note_obj = mp_iternext(iterable)) != MP_OBJ_STOP_ITERATION) { + note_obj = validate_note(note_obj); if (!mp_obj_is_small_int(note_obj)) { synthio_note_obj_t *note = MP_OBJ_TO_PTR(note_obj); synthio_note_start(note, self->synth.sample_rate); } - synthio_span_change_note(&self->synth, SYNTHIO_SILENCE, validate_note(note_obj)); + synthio_span_change_note(&self->synth, SYNTHIO_SILENCE, note_obj); } } From 03abc623efc34910d4e430e4bbd995cf30a93b25 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Fri, 12 May 2023 13:44:27 -0500 Subject: [PATCH 2/4] synthio: note: fix assigning bend & tremolo coefficients dynamically --- shared-module/synthio/Note.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/shared-module/synthio/Note.c b/shared-module/synthio/Note.c index 16727b2152a93..680c1c1a1d2b5 100644 --- a/shared-module/synthio/Note.c +++ b/shared-module/synthio/Note.c @@ -85,7 +85,7 @@ mp_float_t common_hal_synthio_note_get_tremolo_depth(synthio_note_obj_t *self) { void common_hal_synthio_note_set_tremolo_depth(synthio_note_obj_t *self, mp_float_t value_in) { mp_float_t val = mp_arg_validate_float_range(value_in, 0, 1, MP_QSTR_tremolo_depth); self->tremolo_descr.amplitude = val; - self->tremolo_state.amplitude_scaled = round_float_to_int(val * 32767); + self->tremolo_state.amplitude_scaled = round_float_to_int(val * 32768); } mp_float_t common_hal_synthio_note_get_tremolo_rate(synthio_note_obj_t *self) { @@ -96,7 +96,7 @@ void common_hal_synthio_note_set_tremolo_rate(synthio_note_obj_t *self, mp_float mp_float_t val = mp_arg_validate_float_range(value_in, 0, 60, MP_QSTR_tremolo_rate); self->tremolo_descr.frequency = val; if (self->sample_rate != 0) { - self->tremolo_state.dds = synthio_frequency_convert_float_to_dds(val, self->sample_rate); + self->tremolo_state.dds = synthio_frequency_convert_float_to_dds(val * 65536, self->sample_rate); } } @@ -107,7 +107,7 @@ mp_float_t common_hal_synthio_note_get_bend_depth(synthio_note_obj_t *self) { void common_hal_synthio_note_set_bend_depth(synthio_note_obj_t *self, mp_float_t value_in) { mp_float_t val = mp_arg_validate_float_range(value_in, -1, 1, MP_QSTR_bend_depth); self->bend_descr.amplitude = val; - self->bend_state.amplitude_scaled = round_float_to_int(val * 32767); + self->bend_state.amplitude_scaled = round_float_to_int(val * 32768); } mp_float_t common_hal_synthio_note_get_bend_rate(synthio_note_obj_t *self) { @@ -127,7 +127,7 @@ void common_hal_synthio_note_set_bend_rate(synthio_note_obj_t *self, mp_float_t mp_float_t val = mp_arg_validate_float_range(value_in, 0, 60, MP_QSTR_bend_rate); self->bend_descr.frequency = val; if (self->sample_rate != 0) { - self->bend_state.dds = synthio_frequency_convert_float_to_dds(val, self->sample_rate); + self->bend_state.dds = synthio_frequency_convert_float_to_dds(val * 65536, self->sample_rate); } } From f68ab9c5c2d81fd91af163fc868c53acdd99b907 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Fri, 12 May 2023 13:44:49 -0500 Subject: [PATCH 3/4] synthio: Fix FIR filtering when audio is stereo --- shared-module/synthio/__init__.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/shared-module/synthio/__init__.c b/shared-module/synthio/__init__.c index 2fed9a51229c9..6b6a45f4465d2 100644 --- a/shared-module/synthio/__init__.c +++ b/shared-module/synthio/__init__.c @@ -314,19 +314,21 @@ STATIC void run_fir(synthio_synth_t *synth, int32_t *out_buffer32, uint16_t dur) size_t fir_len = synth->filter_bufinfo.len / sizeof(int16_t); int32_t *in_buf = synth->filter_buffer; + + int synth_chan = synth->channel_count; // FIR and copy values to output buffer - for (int16_t i = 0; i < dur; i++) { + for (int16_t i = 0; i < dur * synth_chan; i++) { int32_t acc = 0; for (size_t j = 0; j < fir_len; j++) { // shift 5 here is good for up to 32 filtered voices, else might wrap - acc = acc + (in_buf[j] * (coeff[j] >> 5)); + acc = acc + (in_buf[j * synth_chan] * (coeff[j] >> 5)); } *out_buffer32++ = acc >> 10; in_buf++; } // Move values down so that they get filtered next time - memmove(synth->filter_buffer, &synth->filter_buffer[dur], fir_len * sizeof(int32_t)); + memmove(synth->filter_buffer, &synth->filter_buffer[dur * synth_chan], fir_len * sizeof(int32_t) * synth_chan); } STATIC bool synthio_synth_get_note_filtered(mp_obj_t note_obj) { @@ -359,7 +361,7 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t if (synth->filter_buffer) { int32_t *filter_start = &synth->filter_buffer[synth->filter_bufinfo.len * synth->channel_count / sizeof(int16_t)]; - memset(filter_start, 0, dur * sizeof(int32_t)); + memset(filter_start, 0, dur * synth->channel_count * sizeof(int32_t)); for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) { mp_obj_t note_obj = synth->span.note_obj[chan]; From 91a51039108e4f8f3c390ed39b5d90f7d3ef6e0f Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Fri, 12 May 2023 13:57:33 -0500 Subject: [PATCH 4/4] synthio: add a host demo of all major features --- .../circuitpython-manual/synthio/note/demo.py | 282 ++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 tests/circuitpython-manual/synthio/note/demo.py diff --git a/tests/circuitpython-manual/synthio/note/demo.py b/tests/circuitpython-manual/synthio/note/demo.py new file mode 100644 index 0000000000000..49a811fd43e0d --- /dev/null +++ b/tests/circuitpython-manual/synthio/note/demo.py @@ -0,0 +1,282 @@ +import random +import audiocore +import synthio +from ulab import numpy as np +import adafruit_wave as wave + +h = np.array( + [ + -0.001229734800309099, + -0.008235561806605458, + -0.015082497016061390, + -0.020940136918319988, + -0.024981800822463429, + -0.026464233332370746, + -0.024803890156806906, + -0.019642276775473012, + -0.010893620860173042, + 0.001230341899766145, + 0.016221637398855598, + 0.033304135659230648, + 0.051486665261155681, + 0.069636961761409016, + 0.086570197432542767, + 0.101144354207918147, + 0.112353938422488253, + 0.119413577288191297, + 0.121823886314051028, + 0.119413577288191297, + 0.112353938422488253, + 0.101144354207918147, + 0.086570197432542767, + 0.069636961761409016, + 0.051486665261155681, + 0.033304135659230648, + 0.016221637398855598, + 0.001230341899766145, + -0.010893620860173042, + -0.019642276775473012, + -0.024803890156806906, + -0.026464233332370746, + -0.024981800822463429, + -0.020940136918319988, + -0.015082497016061390, + -0.008235561806605458, + -0.001229734800309099, + ] +) + +filter_coeffs = np.array(h[::-1] * 32768, dtype=np.int16) + + +def randf(lo, hi): + return random.random() * (hi - lo) + lo + + +SAMPLE_SIZE = 1024 +VOLUME = 14700 +sine = np.array( + np.sin(np.linspace(0, 2 * np.pi, SAMPLE_SIZE, endpoint=False)) * VOLUME, dtype=np.int16 +) +square = np.array([24000] * (SAMPLE_SIZE // 2) + [-24000] * (SAMPLE_SIZE // 2), dtype=np.int16) + +noise = np.array( + [random.randint(-32768, 32767) for i in range(SAMPLE_SIZE)], + 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 +) + +instant = synthio.Envelope( + attack_time=0, decay_time=0, release_time=0, attack_level=1, sustain_level=1 +) +synth = synthio.Synthesizer( + sample_rate=48000, envelope=None, filter=filter_coeffs, channel_count=2 +) + + +def synthesize(synth): + print( + """you can use arbitrary waveforms, including ones calculated on the fly or read from wave files (up to 1024 points)""" + ) + + waveform = np.zeros(SAMPLE_SIZE, dtype=np.int16) + chord = [ + synthio.Note(synthio.midi_to_hz(n), waveform=waveform, envelope=envelope) + for n in (60, 64, 67, 70) + ] + synth.press(chord) + + for i in range(256): + ctrl = i / 255 + waveform[:] = np.array(square * (1 - ctrl) + sine * ctrl, dtype=np.int16) + yield 4 + + +def synthesize2(synth): + print("""Envelope controls how notes fade in or out""") + + chord = [ + synthio.Note(synthio.midi_to_hz(n), waveform=sine, envelope=instant) + for n in (60, 64, 67, 70) + ] + + for i in range(4): + for c in chord: + synth.release_all_then_press((c,)) + yield 24 + synth.release_all() + + chord = [ + synthio.Note(synthio.midi_to_hz(n), waveform=sine, envelope=envelope) + for n in (60, 64, 67, 70) + ] + + for i in range(4): + for c in chord: + old = (c,) + synth.release_all_then_press((c,)) + yield 24 + + +def synthesize3(synth): + print("""A noise waveform creates percussive sounds""") + + env = synthio.Envelope( + attack_time=0, + decay_time=0.2, + sustain_level=0, + ) + + notes = [ + synthio.Note( + frequency=synthio.midi_to_hz(1 + i), + waveform=noise, + envelope=env, + ) + for i in range(12) + ] + + random.seed(9) + for _ in range(16): + n = random.choice(notes) + d = random.randint(30, 60) + synth.press((n,)) + yield d + + +def synthesize4(synth): + print("""Tremolo varies the note volume within a range at a low frequency""") + + chord = [ + synthio.Note(synthio.midi_to_hz(n), waveform=sine, envelope=None) for n in (60, 64, 67, 70) + ] + + synth.press(chord) + for i in range(16): + for c in chord: + c.tremolo_depth = i / 50 + c.tremolo_rate = (i + 1) / 4 + yield 48 + yield 36 + + +def synthesize5(synth): + print("""You can add vibrato or frequency sweep to notes""") + + chord = [synthio.Note(synthio.midi_to_hz(n), waveform=sine) for n in (60, 64, 67, 70)] + + synth.press(chord) + for i in range(16): + for c in chord: + c.bend_depth = 1 / 24 + c.bend_rate = (i + 1) / 2 + yield 24 + synth.release_all() + yield 100 + + for c in chord: + synth.release_all() + c.bend_mode = synthio.BendMode.SWEEP_IN + c.bend_depth = randf(-1, 1) + c.bend_rate = 1 / 2 + synth.press(chord) + yield 320 + + +def synthesize6(synth): + print("""Ring modulation multiplies two waveforms together to create rich sounds""") + + chord = [ + synthio.Note(synthio.midi_to_hz(n), waveform=square, ring_waveform=sine, envelope=envelope) + for n in (60,) + ] + + synth.press(chord) + yield 200 + + random.seed(75) + + for _ in range(3): + synth.release_all() + yield 36 + for note in chord: + note.ring_frequency = note.frequency * (random.random() * 35 / 1200 + 8) + synth.press(chord) + yield 200 + + +def synthesize7(synth): + print("""FIR filtering can reproduce low, high, notch and band filters""") + chord = [ + synthio.Note(synthio.midi_to_hz(n), waveform=square, filter=False) + for n in (60, 64, 67, 70) + ] + + for i in range(4): + for c in chord: + synth.release_all_then_press((c,)) + yield 24 + synth.release_all() + + for note in chord: + note.filter = True + + for i in range(4): + for c in chord: + synth.release_all_then_press((c,)) + yield 24 + + +def synthesize8(synth): + print("""Notes can be panned between channels""") + chord = [ + synthio.Note(synthio.midi_to_hz(n), waveform=square, envelope=envelope) + for n in (60, 64, 67, 70) + ] + synth.press(chord) + for p in range(-10, 11, 1): + for note in chord: + note.panning = p / 10 + yield 36 + + +def delay(synth): + synth.release_all() + yield 200 + + +def chain(*args): + for a in args: + yield from a + + +# sox -r 48000 -e signed -b 16 -c 1 tune.raw tune.wav +with wave.open("demo.wav", "w") as f: + f.setnchannels(2) + f.setsampwidth(2) + f.setframerate(48000) + import array + + for n in chain( + synthesize(synth), + delay(synth), + synthesize2(synth), + delay(synth), + synthesize3(synth), + delay(synth), + synthesize4(synth), + delay(synth), + synthesize5(synth), + delay(synth), + synthesize6(synth), + delay(synth), + synthesize7(synth), + delay(synth), + synthesize8(synth), + ): + for i in range(n): + result, data = audiocore.get_buffer(synth) + f.writeframes(data)