From 91978522c533e65d5e9c7d0fcd85942b6c0b3e68 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Wed, 10 May 2023 07:11:26 -0500 Subject: [PATCH 01/20] synthio: Add adafruit_wave and use it in the manual test --- .gitmodules | 3 + frozen/Adafruit_CircuitPython_Wave | 1 + .../synthio/note/audioop.py | 1 - .../synthio/note/chunk.py | 1 - .../circuitpython-manual/synthio/note/code.py | 8 +- .../circuitpython-manual/synthio/note/wave.py | 1 - .../synthio/wave/audioop.py | 11 - .../synthio/wave/chunk.py | 173 ------ .../synthio/wave/midi2wav.py | 8 +- .../circuitpython-manual/synthio/wave/wave.py | 550 ------------------ 10 files changed, 18 insertions(+), 739 deletions(-) create mode 160000 frozen/Adafruit_CircuitPython_Wave delete mode 120000 tests/circuitpython-manual/synthio/note/audioop.py delete mode 120000 tests/circuitpython-manual/synthio/note/chunk.py delete mode 120000 tests/circuitpython-manual/synthio/note/wave.py delete mode 100644 tests/circuitpython-manual/synthio/wave/audioop.py delete mode 100644 tests/circuitpython-manual/synthio/wave/chunk.py delete mode 100644 tests/circuitpython-manual/synthio/wave/wave.py diff --git a/.gitmodules b/.gitmodules index c690e714e8..91afffe6a6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -338,3 +338,6 @@ [submodule "frozen/circuitpython-pcf85063a"] path = frozen/circuitpython-pcf85063a url = https://github.com/bablokb/circuitpython-pcf85063a +[submodule "frozen/Adafruit_CircuitPython_Wave"] + path = frozen/Adafruit_CircuitPython_Wave + url = http://github.com/adafruit/Adafruit_CircuitPython_Wave.git diff --git a/frozen/Adafruit_CircuitPython_Wave b/frozen/Adafruit_CircuitPython_Wave new file mode 160000 index 0000000000..02b748f2e6 --- /dev/null +++ b/frozen/Adafruit_CircuitPython_Wave @@ -0,0 +1 @@ +Subproject commit 02b748f2e6826dc442c842885e58b07ad10d9287 diff --git a/tests/circuitpython-manual/synthio/note/audioop.py b/tests/circuitpython-manual/synthio/note/audioop.py deleted file mode 120000 index 31896fe265..0000000000 --- a/tests/circuitpython-manual/synthio/note/audioop.py +++ /dev/null @@ -1 +0,0 @@ -../wave/audioop.py \ No newline at end of file diff --git a/tests/circuitpython-manual/synthio/note/chunk.py b/tests/circuitpython-manual/synthio/note/chunk.py deleted file mode 120000 index 00983c1f72..0000000000 --- a/tests/circuitpython-manual/synthio/note/chunk.py +++ /dev/null @@ -1 +0,0 @@ -../wave/chunk.py \ No newline at end of file diff --git a/tests/circuitpython-manual/synthio/note/code.py b/tests/circuitpython-manual/synthio/note/code.py index 3814c57279..d088ede50e 100644 --- a/tests/circuitpython-manual/synthio/note/code.py +++ b/tests/circuitpython-manual/synthio/note/code.py @@ -1,8 +1,14 @@ +import sys + +sys.path.insert( + 0, f"{__file__.rpartition('/')[0] or '.'}/../../../../frozen/Adafruit_CircuitPython_Wave" +) + import random import audiocore import synthio from ulab import numpy as np -import wave +import adafruit_wave as wave SAMPLE_SIZE = 1024 VOLUME = 14700 diff --git a/tests/circuitpython-manual/synthio/note/wave.py b/tests/circuitpython-manual/synthio/note/wave.py deleted file mode 120000 index 45884bd29a..0000000000 --- a/tests/circuitpython-manual/synthio/note/wave.py +++ /dev/null @@ -1 +0,0 @@ -../wave/wave.py \ No newline at end of file diff --git a/tests/circuitpython-manual/synthio/wave/audioop.py b/tests/circuitpython-manual/synthio/wave/audioop.py deleted file mode 100644 index cab2fb4a7a..0000000000 --- a/tests/circuitpython-manual/synthio/wave/audioop.py +++ /dev/null @@ -1,11 +0,0 @@ -# SPDX-FileCopyrightText: 2023 Guido van Rossum and others. -# -# SPDX-License-Identifier: PSF-2.0 - -import struct - - -def byteswap(data, sampwidth): - print(data) - raise - ch = "I" if sampwidth == 16 else "H" diff --git a/tests/circuitpython-manual/synthio/wave/chunk.py b/tests/circuitpython-manual/synthio/wave/chunk.py deleted file mode 100644 index c1e6364b0f..0000000000 --- a/tests/circuitpython-manual/synthio/wave/chunk.py +++ /dev/null @@ -1,173 +0,0 @@ -# SPDX-FileCopyrightText: 2023 Guido van Rossum 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 diff --git a/tests/circuitpython-manual/synthio/wave/midi2wav.py b/tests/circuitpython-manual/synthio/wave/midi2wav.py index daa46a28c0..5d626e7baa 100644 --- a/tests/circuitpython-manual/synthio/wave/midi2wav.py +++ b/tests/circuitpython-manual/synthio/wave/midi2wav.py @@ -1,7 +1,13 @@ +import sys + +sys.path.insert( + 0, f"{__file__.rpartition('/')[0] or '.'}/../../../../frozen/Adafruit_CircuitPython_Wave" +) + import audiocore import synthio from ulab import numpy as np -import wave +import adafruit_wave as wave SAMPLE_SIZE = 1024 VOLUME = 32700 diff --git a/tests/circuitpython-manual/synthio/wave/wave.py b/tests/circuitpython-manual/synthio/wave/wave.py deleted file mode 100644 index dc6d5407a7..0000000000 --- a/tests/circuitpython-manual/synthio/wave/wave.py +++ /dev/null @@ -1,550 +0,0 @@ -# SPDX-FileCopyrightText: 2023 Guido van Rossum 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(" 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( - " Date: Wed, 10 May 2023 07:31:25 -0500 Subject: [PATCH 02/20] synthio: Add `synthio.BendType` --- shared-bindings/synthio/__init__.c | 32 ++++++++++++++++++++++++++++++ shared-bindings/synthio/__init__.h | 5 +++++ 2 files changed, 37 insertions(+) diff --git a/shared-bindings/synthio/__init__.c b/shared-bindings/synthio/__init__.c index 2e4fd7b8a4..61c4e9a8be 100644 --- a/shared-bindings/synthio/__init__.c +++ b/shared-bindings/synthio/__init__.c @@ -26,6 +26,7 @@ #include +#include "py/enum.h" #include "py/mperrno.h" #include "py/obj.h" #include "py/objnamedtuple.h" @@ -52,6 +53,7 @@ static const mp_arg_t envelope_properties[] = { { MP_QSTR_sustain_level, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_OBJ_NULL } }, }; +//| //| """Support for multi-channel audio synthesis //| //| At least 2 simultaneous notes are supported. samd5x, mimxrt10xx and rp2040 platforms support up to 12 notes. @@ -284,9 +286,39 @@ STATIC mp_obj_t onevo_to_hz(mp_obj_t arg) { } MP_DEFINE_CONST_FUN_OBJ_1(synthio_onevo_to_hz_obj, onevo_to_hz); +MAKE_ENUM_VALUE(synthio_bend_mode_type, bend_mode, STATIC, SYNTHIO_BEND_MODE_STATIC); +MAKE_ENUM_VALUE(synthio_bend_mode_type, bend_mode, TREMOLO, SYNTHIO_BEND_MODE_TREMOLO); +MAKE_ENUM_VALUE(synthio_bend_mode_type, bend_mode, SWEEP, SYNTHIO_BEND_MODE_SWEEP); + +//| +//| class BendType: +//| """Controls the way the ``Note.pitch_bend_depth`` and ``Note.pitch_bend_rate`` properties are interpreted.""" +//| +//| STATIC: object +//| """The Note's pitch is modified by its ``pitch_bend_depth``. ``pitch_bend_rate`` is ignored.""" +//| +//| TREMOLO: object +//| """The Note's pitch varies by ``±pitch_bend_depth` at a rate of ``pitch_bend_rate``Hz.""" +//| +//| SWEEP: object +//| """The Note's pitch starts at ``Note.frequency`` then sweeps up or down by ``pitch_bend_depth`` over ``1/pitch_bend_rate`` seconds.""" +//| +MAKE_ENUM_MAP(synthio_bend_mode) { + MAKE_ENUM_MAP_ENTRY(bend_mode, STATIC), + MAKE_ENUM_MAP_ENTRY(bend_mode, TREMOLO), + MAKE_ENUM_MAP_ENTRY(bend_mode, SWEEP), +}; + +STATIC MP_DEFINE_CONST_DICT(synthio_bend_mode_locals_dict, synthio_bend_mode_locals_table); + +MAKE_PRINTER(synthio, synthio_bend_mode); + +MAKE_ENUM_TYPE(synthio, BendType, synthio_bend_mode); + 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_BendType), MP_ROM_PTR(&synthio_bend_mode_type) }, { MP_ROM_QSTR(MP_QSTR_MidiTrack), MP_ROM_PTR(&synthio_miditrack_type) }, { MP_ROM_QSTR(MP_QSTR_Note), MP_ROM_PTR(&synthio_note_type) }, { MP_ROM_QSTR(MP_QSTR_Synthesizer), MP_ROM_PTR(&synthio_synthesizer_type) }, diff --git a/shared-bindings/synthio/__init__.h b/shared-bindings/synthio/__init__.h index 37a5c77b34..e6be579c58 100644 --- a/shared-bindings/synthio/__init__.h +++ b/shared-bindings/synthio/__init__.h @@ -28,6 +28,11 @@ #include "py/objnamedtuple.h" +typedef enum { + SYNTHIO_BEND_MODE_STATIC, SYNTHIO_BEND_MODE_TREMOLO, SYNTHIO_BEND_MODE_SWEEP +} synthio_bend_mode_t; + +extern const mp_obj_type_t synthio_bend_mode_type; 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; From 1d1907b98bca84aa784846d24a408db1fde12d33 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Wed, 10 May 2023 08:40:09 -0500 Subject: [PATCH 03/20] synthio: Generalize vibrato into bend bend can be static, sweep, or vibrato --- shared-bindings/synthio/Note.c | 87 ++++++++++++------- shared-bindings/synthio/Note.h | 12 ++- shared-bindings/synthio/__init__.c | 6 +- shared-bindings/synthio/__init__.h | 6 +- shared-module/synthio/Note.c | 54 ++++++++---- shared-module/synthio/Note.h | 6 +- shared-module/synthio/__init__.c | 12 +++ shared-module/synthio/__init__.h | 4 +- .../circuitpython-manual/synthio/note/code.py | 29 +++++-- tests/circuitpython/synthesizer_note.py.exp | 6 +- 10 files changed, 153 insertions(+), 69 deletions(-) diff --git a/shared-bindings/synthio/Note.c b/shared-bindings/synthio/Note.c index 4cd0abe0d4..905243b4bf 100644 --- a/shared-bindings/synthio/Note.c +++ b/shared-bindings/synthio/Note.c @@ -30,6 +30,7 @@ #include "py/objproperty.h" #include "py/runtime.h" #include "shared-bindings/util.h" +#include "shared-bindings/synthio/__init__.h" #include "shared-bindings/synthio/Note.h" #include "shared-module/synthio/Note.h" @@ -38,8 +39,9 @@ static const mp_arg_t note_properties[] = { { MP_QSTR_amplitude, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_INT(1) } }, { MP_QSTR_tremolo_rate, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = NULL } }, { MP_QSTR_tremolo_depth, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = NULL } }, - { MP_QSTR_vibrato_rate, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = NULL } }, - { MP_QSTR_vibrato_depth, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = NULL } }, + { MP_QSTR_bend_rate, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = NULL } }, + { MP_QSTR_bend_depth, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = NULL } }, + { MP_QSTR_bend_mode, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = (mp_obj_t)MP_ROM_PTR(&bend_mode_VIBRATO_obj) } }, { MP_QSTR_waveform, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_NONE } }, { MP_QSTR_envelope, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_NONE } }, }; @@ -53,17 +55,16 @@ static const mp_arg_t note_properties[] = { //| envelope: Optional[Envelope] = None, //| tremolo_depth: float = 0.0, //| tremolo_rate: float = 0.0, -//| vibrato_depth: float = 0.0, -//| vibrato_rate: float = 0.0, +//| bend_depth: float = 0.0, +//| bend_rate: float = 0.0, //| ) -> None: -//| """Construct a Note object, with a frequency in Hz, and optional amplitude (volume), waveform, envelope, tremolo (volume change) and vibrato (frequency change). +//| """Construct a Note object, with a frequency in Hz, and optional amplitude (volume), waveform, envelope, tremolo (volume change) and bend (frequency change). //| //| If waveform or envelope are `None` the synthesizer object's default waveform or envelope are used. //| //| If the same Note object is played on multiple Synthesizer objects, the result is undefined. //| """ STATIC mp_obj_t synthio_note_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { - enum { ARG_frequency, ARG_amplitude, ARG_waveform, ARG_envelope, ARG_tremolo_rate, ARG_tremolo_depth, ARG_vibrato_rate, ARG_vibrato_depth }; mp_arg_val_t args[MP_ARRAY_SIZE(note_properties)]; mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(note_properties), note_properties, args); @@ -158,46 +159,67 @@ MP_PROPERTY_GETSET(synthio_note_tremolo_rate_obj, (mp_obj_t)&synthio_note_get_tremolo_rate_obj, (mp_obj_t)&synthio_note_set_tremolo_rate_obj); -//| vibrato_depth: float -//| """The vibrato depth of the note, from 0 to 1 //| -//| A depth of 0 disables vibrato. A depth of 1 corresponds to a vibrato of ±1 -//| octave. A depth of (1/12) = 0.833 corresponds to a vibrato of ±1 semitone, +//| bend_mode: BendMode +//| """The type of bend operation""" +STATIC mp_obj_t synthio_note_get_bend_mode(mp_obj_t self_in) { + synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); + return cp_enum_find(&synthio_bend_mode_type, common_hal_synthio_note_get_bend_mode(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(synthio_note_get_bend_mode_obj, synthio_note_get_bend_mode); + +STATIC mp_obj_t synthio_note_set_bend_mode(mp_obj_t self_in, mp_obj_t arg) { + synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_synthio_note_set_bend_mode(self, cp_enum_value(&synthio_bend_mode_type, arg, MP_QSTR_bend_mode)); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(synthio_note_set_bend_mode_obj, synthio_note_set_bend_mode); +MP_PROPERTY_GETSET(synthio_note_bend_mode_obj, + (mp_obj_t)&synthio_note_get_bend_mode_obj, + (mp_obj_t)&synthio_note_set_bend_mode_obj); + +// +//| +//| bend_depth: float +//| """The bend depth of the note, from -1 to +1 +//| +//| A depth of 0 disables bend. A depth of 1 corresponds to a bend of 1 +//| octave. A depth of (1/12) = 0.833 corresponds to a bend of 1 semitone, //| and a depth of .00833 corresponds to one musical cent. //| """ -STATIC mp_obj_t synthio_note_get_vibrato_depth(mp_obj_t self_in) { +STATIC mp_obj_t synthio_note_get_bend_depth(mp_obj_t self_in) { synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); - return mp_obj_new_float(common_hal_synthio_note_get_vibrato_depth(self)); + return mp_obj_new_float(common_hal_synthio_note_get_bend_depth(self)); } -MP_DEFINE_CONST_FUN_OBJ_1(synthio_note_get_vibrato_depth_obj, synthio_note_get_vibrato_depth); +MP_DEFINE_CONST_FUN_OBJ_1(synthio_note_get_bend_depth_obj, synthio_note_get_bend_depth); -STATIC mp_obj_t synthio_note_set_vibrato_depth(mp_obj_t self_in, mp_obj_t arg) { +STATIC mp_obj_t synthio_note_set_bend_depth(mp_obj_t self_in, mp_obj_t arg) { synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); - common_hal_synthio_note_set_vibrato_depth(self, mp_obj_get_float(arg)); + common_hal_synthio_note_set_bend_depth(self, mp_obj_get_float(arg)); return mp_const_none; } -MP_DEFINE_CONST_FUN_OBJ_2(synthio_note_set_vibrato_depth_obj, synthio_note_set_vibrato_depth); -MP_PROPERTY_GETSET(synthio_note_vibrato_depth_obj, - (mp_obj_t)&synthio_note_get_vibrato_depth_obj, - (mp_obj_t)&synthio_note_set_vibrato_depth_obj); +MP_DEFINE_CONST_FUN_OBJ_2(synthio_note_set_bend_depth_obj, synthio_note_set_bend_depth); +MP_PROPERTY_GETSET(synthio_note_bend_depth_obj, + (mp_obj_t)&synthio_note_get_bend_depth_obj, + (mp_obj_t)&synthio_note_set_bend_depth_obj); -//| vibrato_rate: float -//| """The vibrato rate of the note, in Hz.""" -STATIC mp_obj_t synthio_note_get_vibrato_rate(mp_obj_t self_in) { +//| bend_rate: float +//| """The bend rate of the note, in Hz.""" +STATIC mp_obj_t synthio_note_get_bend_rate(mp_obj_t self_in) { synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); - return mp_obj_new_float(common_hal_synthio_note_get_vibrato_rate(self)); + return mp_obj_new_float(common_hal_synthio_note_get_bend_rate(self)); } -MP_DEFINE_CONST_FUN_OBJ_1(synthio_note_get_vibrato_rate_obj, synthio_note_get_vibrato_rate); +MP_DEFINE_CONST_FUN_OBJ_1(synthio_note_get_bend_rate_obj, synthio_note_get_bend_rate); -STATIC mp_obj_t synthio_note_set_vibrato_rate(mp_obj_t self_in, mp_obj_t arg) { +STATIC mp_obj_t synthio_note_set_bend_rate(mp_obj_t self_in, mp_obj_t arg) { synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); - common_hal_synthio_note_set_vibrato_rate(self, mp_obj_get_float(arg)); + common_hal_synthio_note_set_bend_rate(self, mp_obj_get_float(arg)); return mp_const_none; } -MP_DEFINE_CONST_FUN_OBJ_2(synthio_note_set_vibrato_rate_obj, synthio_note_set_vibrato_rate); -MP_PROPERTY_GETSET(synthio_note_vibrato_rate_obj, - (mp_obj_t)&synthio_note_get_vibrato_rate_obj, - (mp_obj_t)&synthio_note_set_vibrato_rate_obj); +MP_DEFINE_CONST_FUN_OBJ_2(synthio_note_set_bend_rate_obj, synthio_note_set_bend_rate); +MP_PROPERTY_GETSET(synthio_note_bend_rate_obj, + (mp_obj_t)&synthio_note_get_bend_rate_obj, + (mp_obj_t)&synthio_note_set_bend_rate_obj); //| waveform: Optional[ReadableBuffer] //| """The waveform of this note. Setting the waveform to a buffer of a different size resets the note's phase.""" @@ -249,8 +271,9 @@ STATIC const mp_rom_map_elem_t synthio_note_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR_envelope), MP_ROM_PTR(&synthio_note_envelope_obj) }, { MP_ROM_QSTR(MP_QSTR_tremolo_depth), MP_ROM_PTR(&synthio_note_tremolo_depth_obj) }, { MP_ROM_QSTR(MP_QSTR_tremolo_rate), MP_ROM_PTR(&synthio_note_tremolo_rate_obj) }, - { MP_ROM_QSTR(MP_QSTR_vibrato_depth), MP_ROM_PTR(&synthio_note_vibrato_depth_obj) }, - { MP_ROM_QSTR(MP_QSTR_vibrato_rate), MP_ROM_PTR(&synthio_note_vibrato_rate_obj) }, + { MP_ROM_QSTR(MP_QSTR_bend_depth), MP_ROM_PTR(&synthio_note_bend_depth_obj) }, + { MP_ROM_QSTR(MP_QSTR_bend_rate), MP_ROM_PTR(&synthio_note_bend_rate_obj) }, + { MP_ROM_QSTR(MP_QSTR_bend_mode), MP_ROM_PTR(&synthio_note_bend_mode_obj) }, }; STATIC MP_DEFINE_CONST_DICT(synthio_note_locals_dict, synthio_note_locals_dict_table); diff --git a/shared-bindings/synthio/Note.h b/shared-bindings/synthio/Note.h index ab9d925f03..90d933fd3f 100644 --- a/shared-bindings/synthio/Note.h +++ b/shared-bindings/synthio/Note.h @@ -4,6 +4,7 @@ typedef struct synthio_note_obj synthio_note_obj_t; extern const mp_obj_type_t synthio_note_type; +typedef enum synthio_bend_mode_e synthio_bend_mode_t; mp_float_t common_hal_synthio_note_get_frequency(synthio_note_obj_t *self); void common_hal_synthio_note_set_frequency(synthio_note_obj_t *self, mp_float_t value); @@ -17,11 +18,14 @@ void common_hal_synthio_note_set_tremolo_rate(synthio_note_obj_t *self, mp_float mp_float_t common_hal_synthio_note_get_tremolo_depth(synthio_note_obj_t *self); void common_hal_synthio_note_set_tremolo_depth(synthio_note_obj_t *self, mp_float_t value); -mp_float_t common_hal_synthio_note_get_vibrato_rate(synthio_note_obj_t *self); -void common_hal_synthio_note_set_vibrato_rate(synthio_note_obj_t *self, mp_float_t value); +synthio_bend_mode_t common_hal_synthio_note_get_bend_mode(synthio_note_obj_t *self); +void common_hal_synthio_note_set_bend_mode(synthio_note_obj_t *self, synthio_bend_mode_t value); -mp_float_t common_hal_synthio_note_get_vibrato_depth(synthio_note_obj_t *self); -void common_hal_synthio_note_set_vibrato_depth(synthio_note_obj_t *self, mp_float_t value); +mp_float_t common_hal_synthio_note_get_bend_rate(synthio_note_obj_t *self); +void common_hal_synthio_note_set_bend_rate(synthio_note_obj_t *self, mp_float_t value); + +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); mp_obj_t common_hal_synthio_note_get_waveform_obj(synthio_note_obj_t *self); void common_hal_synthio_note_set_waveform(synthio_note_obj_t *self, mp_obj_t value); diff --git a/shared-bindings/synthio/__init__.c b/shared-bindings/synthio/__init__.c index 61c4e9a8be..2739d81e88 100644 --- a/shared-bindings/synthio/__init__.c +++ b/shared-bindings/synthio/__init__.c @@ -287,7 +287,7 @@ STATIC mp_obj_t onevo_to_hz(mp_obj_t arg) { MP_DEFINE_CONST_FUN_OBJ_1(synthio_onevo_to_hz_obj, onevo_to_hz); MAKE_ENUM_VALUE(synthio_bend_mode_type, bend_mode, STATIC, SYNTHIO_BEND_MODE_STATIC); -MAKE_ENUM_VALUE(synthio_bend_mode_type, bend_mode, TREMOLO, SYNTHIO_BEND_MODE_TREMOLO); +MAKE_ENUM_VALUE(synthio_bend_mode_type, bend_mode, VIBRATO, SYNTHIO_BEND_MODE_VIBRATO); MAKE_ENUM_VALUE(synthio_bend_mode_type, bend_mode, SWEEP, SYNTHIO_BEND_MODE_SWEEP); //| @@ -297,7 +297,7 @@ MAKE_ENUM_VALUE(synthio_bend_mode_type, bend_mode, SWEEP, SYNTHIO_BEND_MODE_SWEE //| STATIC: object //| """The Note's pitch is modified by its ``pitch_bend_depth``. ``pitch_bend_rate`` is ignored.""" //| -//| TREMOLO: object +//| VIBRATO: object //| """The Note's pitch varies by ``±pitch_bend_depth` at a rate of ``pitch_bend_rate``Hz.""" //| //| SWEEP: object @@ -305,7 +305,7 @@ MAKE_ENUM_VALUE(synthio_bend_mode_type, bend_mode, SWEEP, SYNTHIO_BEND_MODE_SWEE //| MAKE_ENUM_MAP(synthio_bend_mode) { MAKE_ENUM_MAP_ENTRY(bend_mode, STATIC), - MAKE_ENUM_MAP_ENTRY(bend_mode, TREMOLO), + MAKE_ENUM_MAP_ENTRY(bend_mode, VIBRATO), MAKE_ENUM_MAP_ENTRY(bend_mode, SWEEP), }; diff --git a/shared-bindings/synthio/__init__.h b/shared-bindings/synthio/__init__.h index e6be579c58..d09fab815e 100644 --- a/shared-bindings/synthio/__init__.h +++ b/shared-bindings/synthio/__init__.h @@ -27,11 +27,13 @@ #pragma once #include "py/objnamedtuple.h" +#include "py/enum.h" -typedef enum { - SYNTHIO_BEND_MODE_STATIC, SYNTHIO_BEND_MODE_TREMOLO, SYNTHIO_BEND_MODE_SWEEP +typedef enum synthio_bend_mode_e { + SYNTHIO_BEND_MODE_STATIC, SYNTHIO_BEND_MODE_VIBRATO, SYNTHIO_BEND_MODE_SWEEP } synthio_bend_mode_t; +extern const cp_enum_obj_t bend_mode_VIBRATO_obj; extern const mp_obj_type_t synthio_bend_mode_type; typedef struct synthio_synth synthio_synth_t; extern int16_t shared_bindings_synthio_square_wave[]; diff --git a/shared-module/synthio/Note.c b/shared-module/synthio/Note.c index 84949163a7..65c20024a7 100644 --- a/shared-module/synthio/Note.c +++ b/shared-module/synthio/Note.c @@ -76,25 +76,34 @@ void common_hal_synthio_note_set_tremolo_rate(synthio_note_obj_t *self, mp_float } } -mp_float_t common_hal_synthio_note_get_vibrato_depth(synthio_note_obj_t *self) { - return self->vibrato_descr.amplitude; +mp_float_t common_hal_synthio_note_get_bend_depth(synthio_note_obj_t *self) { + return self->bend_descr.amplitude; } -void common_hal_synthio_note_set_vibrato_depth(synthio_note_obj_t *self, mp_float_t value_in) { - mp_float_t val = mp_arg_validate_float_range(value_in, 0, 1, MP_QSTR_vibrato_depth); - self->vibrato_descr.amplitude = val; - self->vibrato_state.amplitude_scaled = round_float_to_int(val * 32767); +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); } -mp_float_t common_hal_synthio_note_get_vibrato_rate(synthio_note_obj_t *self) { - return self->vibrato_descr.frequency; +mp_float_t common_hal_synthio_note_get_bend_rate(synthio_note_obj_t *self) { + return self->bend_descr.frequency; } -void common_hal_synthio_note_set_vibrato_rate(synthio_note_obj_t *self, mp_float_t value_in) { - mp_float_t val = mp_arg_validate_float_range(value_in, 0, 60, MP_QSTR_vibrato_rate); - self->vibrato_descr.frequency = val; +synthio_bend_mode_t common_hal_synthio_note_get_bend_mode(synthio_note_obj_t *self) { + return self->bend_mode; +} + +void common_hal_synthio_note_set_bend_mode(synthio_note_obj_t *self, synthio_bend_mode_t value) { + self->bend_mode = value; +} + + +void common_hal_synthio_note_set_bend_rate(synthio_note_obj_t *self, mp_float_t value_in) { + mp_float_t val = mp_arg_validate_float_range(value_in, 0, 60, MP_QSTR_bend_rate); + self->bend_descr.frequency = val; if (self->sample_rate != 0) { - self->vibrato_state.dds = synthio_frequency_convert_float_to_dds(val, self->sample_rate); + self->bend_state.dds = synthio_frequency_convert_float_to_dds(val, self->sample_rate); } } @@ -139,8 +148,8 @@ void synthio_note_recalculate(synthio_note_obj_t *self, int32_t sample_rate) { synthio_lfo_set(&self->tremolo_state, &self->tremolo_descr, sample_rate); self->tremolo_state.offset_scaled = 32768 - self->tremolo_state.amplitude_scaled; - synthio_lfo_set(&self->vibrato_state, &self->vibrato_descr, sample_rate); - self->vibrato_state.offset_scaled = 32768; + synthio_lfo_set(&self->bend_state, &self->bend_descr, sample_rate); + self->bend_state.offset_scaled = 32768; } void synthio_note_start(synthio_note_obj_t *self, int32_t sample_rate) { @@ -176,10 +185,23 @@ STATIC uint32_t pitch_bend(uint32_t frequency_scaled, uint16_t bend_value) { return (frequency_scaled * (uint64_t)f) >> (15 + down); } +STATIC int synthio_bend_value(synthio_note_obj_t *self, int16_t dur) { + switch (self->bend_mode) { + case SYNTHIO_BEND_MODE_STATIC: + return self->bend_state.amplitude_scaled + self->bend_state.offset_scaled; + case SYNTHIO_BEND_MODE_VIBRATO: + return synthio_lfo_step(&self->bend_state, dur); + case SYNTHIO_BEND_MODE_SWEEP: + return synthio_sweep_step(&self->bend_state, dur); + default: + return 32768; + } +} + uint32_t synthio_note_step(synthio_note_obj_t *self, int32_t sample_rate, int16_t dur, uint16_t *loudness) { int tremolo_value = synthio_lfo_step(&self->tremolo_state, dur); - int vibrato_value = synthio_lfo_step(&self->vibrato_state, dur); *loudness = (*loudness * tremolo_value) >> 15; - uint32_t frequency_scaled = pitch_bend(self->frequency_scaled, vibrato_value); + int bend_value = synthio_bend_value(self, dur); + uint32_t frequency_scaled = pitch_bend(self->frequency_scaled, bend_value); return frequency_scaled; } diff --git a/shared-module/synthio/Note.h b/shared-module/synthio/Note.h index 4efb197c07..ffaf7295fa 100644 --- a/shared-module/synthio/Note.h +++ b/shared-module/synthio/Note.h @@ -27,6 +27,7 @@ #pragma once #include "shared-module/synthio/__init__.h" +#include "shared-bindings/synthio/__init__.h" typedef struct synthio_note_obj { mp_obj_base_t base; @@ -39,8 +40,9 @@ typedef struct synthio_note_obj { int32_t frequency_scaled; int32_t amplitude_scaled; - synthio_lfo_descr_t tremolo_descr, vibrato_descr; - synthio_lfo_state_t tremolo_state, vibrato_state; + synthio_bend_mode_t bend_mode; + synthio_lfo_descr_t tremolo_descr, bend_descr; + synthio_lfo_state_t tremolo_state, bend_state; mp_buffer_info_t waveform_buf; synthio_envelope_definition_t envelope_def; diff --git a/shared-module/synthio/__init__.c b/shared-module/synthio/__init__.c index 5ee72aeb79..0bd02a3436 100644 --- a/shared-module/synthio/__init__.c +++ b/shared-module/synthio/__init__.c @@ -402,6 +402,18 @@ void synthio_lfo_set(synthio_lfo_state_t *state, const synthio_lfo_descr_t *desc state->dds = synthio_frequency_convert_float_to_dds(descr->frequency * 65536, sample_rate); } +int synthio_sweep_step(synthio_lfo_state_t *state, uint16_t dur) { + uint32_t phase = state->phase; + uint16_t whole_phase = phase >> 16; + + // advance the phase accumulator + state->phase = phase + state->dds * dur; + if (state->phase < phase) { + state->phase = 0xffffffff; + } + return (state->amplitude_scaled * whole_phase) / 65536 + state->offset_scaled; +} + int synthio_lfo_step(synthio_lfo_state_t *state, uint16_t dur) { uint32_t phase = state->phase; uint16_t whole_phase = phase >> 16; diff --git a/shared-module/synthio/__init__.h b/shared-module/synthio/__init__.h index 4ea04a09e3..53706f0e6c 100644 --- a/shared-module/synthio/__init__.h +++ b/shared-module/synthio/__init__.h @@ -81,7 +81,8 @@ typedef struct { } synthio_lfo_descr_t; typedef struct { - uint32_t amplitude_scaled, offset_scaled, dds, phase; + int32_t amplitude_scaled; + uint32_t offset_scaled, dds, phase; } synthio_lfo_state_t; @@ -107,3 +108,4 @@ uint32_t synthio_frequency_convert_scaled_to_dds(uint64_t frequency_scaled, int3 void synthio_lfo_set(synthio_lfo_state_t *state, const synthio_lfo_descr_t *descr, uint32_t sample_rate); int synthio_lfo_step(synthio_lfo_state_t *state, uint16_t dur); +int synthio_sweep_step(synthio_lfo_state_t *state, uint16_t dur); diff --git a/tests/circuitpython-manual/synthio/note/code.py b/tests/circuitpython-manual/synthio/note/code.py index d088ede50e..6a6a9556dc 100644 --- a/tests/circuitpython-manual/synthio/note/code.py +++ b/tests/circuitpython-manual/synthio/note/code.py @@ -18,7 +18,7 @@ sine = np.array( ) envelope = synthio.Envelope( - attack_time=0.1, decay_time=0.05, release_time=0.2, attack_level=1, sustain_level=0.8 + attack_time=0.1, decay_time=0.05, release_time=0.2, attack_level=0.8, sustain_level=0.8 ) synth = synthio.Synthesizer(sample_rate=48000) @@ -63,8 +63,8 @@ def synthesize2(synth): def synthesize3(synth): n = synthio.Note( frequency=synthio.midi_to_hz(60), - vibrato_depth=0.1, - vibrato_rate=8, + bend_depth=0.1, + bend_rate=8, waveform=sine, envelope=envelope, ) @@ -79,8 +79,8 @@ def synthesize4(synth): frequency=synthio.midi_to_hz(60), tremolo_depth=0.1, tremolo_rate=1.5, - vibrato_depth=0.1, - vibrato_rate=3, + bend_depth=0.1, + bend_rate=3, waveform=sine, envelope=envelope, ) @@ -109,6 +109,23 @@ def synthesize5(synth): yield 36 +def synthesize6(synth): + n = synthio.Note( + frequency=synthio.midi_to_hz(60), + tremolo_depth=0.1, + tremolo_rate=1.5, + bend_depth=-5 / 12, + bend_rate=1 / 2, + bend_mode=synthio.BendType.SWEEP, + waveform=sine, + envelope=envelope, + ) + synth.press((n,)) + yield 720 + synth.release_all() + yield 36 + + def chain(*args): for a in args: yield from a @@ -119,7 +136,7 @@ with wave.open("tune-noenv.wav", "w") as f: f.setnchannels(1) f.setsampwidth(2) f.setframerate(48000) - for n in chain(synthesize5(synth)): + for n in chain(synthesize6(synth)): for i in range(n): result, data = audiocore.get_buffer(synth) f.writeframes(data) diff --git a/tests/circuitpython/synthesizer_note.py.exp b/tests/circuitpython/synthesizer_note.py.exp index 787243e2a5..bb34c0a6c3 100644 --- a/tests/circuitpython/synthesizer_note.py.exp +++ b/tests/circuitpython/synthesizer_note.py.exp @@ -1,10 +1,10 @@ () [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] -(Note(frequency=830.6076004423605, amplitude=1.0, tremolo_rate=0.0, tremolo_depth=0.0, vibrato_rate=0.0, vibrato_depth=0.0, waveform=None, envelope=None),) +(Note(frequency=830.6076004423605, amplitude=1.0, tremolo_rate=0.0, tremolo_depth=0.0, bend_rate=0.0, bend_depth=0.0, bend_mode=synthio.BendType.VIBRATO, waveform=None, envelope=None),) [-16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383] -(Note(frequency=830.6076004423605, amplitude=1.0, tremolo_rate=0.0, tremolo_depth=0.0, vibrato_rate=0.0, vibrato_depth=0.0, waveform=None, envelope=None), Note(frequency=830.6076004423605, amplitude=1.0, tremolo_rate=0.0, tremolo_depth=0.0, vibrato_rate=0.0, vibrato_depth=0.0, waveform=None, envelope=None)) +(Note(frequency=830.6076004423605, amplitude=1.0, tremolo_rate=0.0, tremolo_depth=0.0, bend_rate=0.0, bend_depth=0.0, bend_mode=synthio.BendType.VIBRATO, waveform=None, envelope=None), Note(frequency=830.6076004423605, amplitude=1.0, tremolo_rate=0.0, tremolo_depth=0.0, bend_rate=0.0, bend_depth=0.0, bend_mode=synthio.BendType.VIBRATO, waveform=None, envelope=None)) [0, 0, 0, 0, 0, 0, 0, 0, 28045, 0, 0, 0, 0, -28046, 0, 0, 0, 0, 28045, 0, 0, 0, 0, -28046] -(Note(frequency=830.6076004423605, amplitude=1.0, tremolo_rate=0.0, tremolo_depth=0.0, vibrato_rate=0.0, vibrato_depth=0.0, waveform=None, envelope=None),) +(Note(frequency=830.6076004423605, amplitude=1.0, tremolo_rate=0.0, tremolo_depth=0.0, bend_rate=0.0, bend_depth=0.0, bend_mode=synthio.BendType.VIBRATO, waveform=None, envelope=None),) [0, 0, 0, 28045, 0, 0, 0, 0, 0, 0, 0, 0, 28045, 0, 0, 0, 0, -28046, 0, 0, 0, 0, 28045, 0] (-5242, 5242) (-10485, 10484) From 2062b2bfb766709935c0c9b7b383802bf1bf67a7 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Wed, 10 May 2023 09:27:47 -0500 Subject: [PATCH 04/20] synthio: add a noise program to the manual tests --- .../synthio/note/noise.py | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 tests/circuitpython-manual/synthio/note/noise.py diff --git a/tests/circuitpython-manual/synthio/note/noise.py b/tests/circuitpython-manual/synthio/note/noise.py new file mode 100644 index 0000000000..53257966cb --- /dev/null +++ b/tests/circuitpython-manual/synthio/note/noise.py @@ -0,0 +1,71 @@ +import sys + +sys.path.insert( + 0, f"{__file__.rpartition('/')[0] or '.'}/../../../../frozen/Adafruit_CircuitPython_Wave" +) + +import random +import audiocore +import synthio +from ulab import numpy as np +import adafruit_wave as wave + +random.seed(9) +SAMPLE_SIZE = 1024 +VOLUME = 14700 +noise = np.array( + [random.randint(-32768, 32767) for i in range(SAMPLE_SIZE)], + dtype=np.int16, +) + +envelope = synthio.Envelope( + attack_time=0, + decay_time=0.2, + sustain_level=0, +) + +synth = synthio.Synthesizer(sample_rate=48000) + + +def randf(lo, hi): + return random.random() * (hi - lo) + lo + + +def synthesize(synth): + notes = [ + synthio.Note( + frequency=synthio.midi_to_hz(1 + i), + waveform=noise, + envelope=envelope, + bend_mode=synthio.BendType.SWEEP, + bend_depth=random.choice((-1, 1)), + bend_rate=randf(4, 12), + ) + for i in range(12) + ] + + random.seed(9) + for _ in range(16): + n = random.choice(notes) + d = random.randint(30, 60) + print(n, d) + synth.press((n,)) + yield d + synth.release_all() + yield 36 + + +def chain(*args): + for a in args: + yield from a + + +# sox -r 48000 -e signed -b 16 -c 1 tune.raw tune.wav +with wave.open("noise.wav", "w") as f: + f.setnchannels(1) + f.setsampwidth(2) + f.setframerate(48000) + for n in chain(synthesize(synth)): + for i in range(n): + result, data = audiocore.get_buffer(synth) + f.writeframes(data) From e87e7ee54f667e114058ccfba109a425eaf990ae Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Wed, 10 May 2023 11:16:22 -0500 Subject: [PATCH 05/20] synthio: add stereo & Note.panning A note can be placed in the center (panning=0) or moved to just the left (panning=1) or right (panning=-1) channels. Fractional panning values place it partially in both channels. --- shared-bindings/synthio/Note.c | 36 ++++++++++++++++----------- shared-bindings/synthio/Note.h | 4 +-- shared-bindings/synthio/Synthesizer.c | 6 ++++- shared-bindings/synthio/Synthesizer.h | 2 +- shared-module/synthio/MidiTrack.c | 2 +- shared-module/synthio/Note.c | 21 ++++++++++------ shared-module/synthio/Note.h | 5 ++-- shared-module/synthio/Synthesizer.c | 6 ++--- shared-module/synthio/__init__.c | 31 +++++++++++++++-------- shared-module/synthio/__init__.h | 3 ++- 10 files changed, 73 insertions(+), 43 deletions(-) diff --git a/shared-bindings/synthio/Note.c b/shared-bindings/synthio/Note.c index 905243b4bf..5912ab30c9 100644 --- a/shared-bindings/synthio/Note.c +++ b/shared-bindings/synthio/Note.c @@ -36,7 +36,7 @@ static const mp_arg_t note_properties[] = { { MP_QSTR_frequency, MP_ARG_OBJ | MP_ARG_REQUIRED, {.u_obj = NULL } }, - { MP_QSTR_amplitude, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_INT(1) } }, + { MP_QSTR_panning, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_INT(0) } }, { MP_QSTR_tremolo_rate, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = NULL } }, { MP_QSTR_tremolo_depth, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = NULL } }, { MP_QSTR_bend_rate, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = NULL } }, @@ -50,15 +50,16 @@ static const mp_arg_t note_properties[] = { //| self, //| *, //| frequency: float, -//| amplitude: float = 1.0, +//| panning: float = 0.0, //| waveform: Optional[ReadableBuffer] = None, //| envelope: Optional[Envelope] = None, //| tremolo_depth: float = 0.0, //| tremolo_rate: float = 0.0, //| bend_depth: float = 0.0, //| bend_rate: float = 0.0, +//| bend_mode: BendMode = BendMode.VIBRATO, //| ) -> None: -//| """Construct a Note object, with a frequency in Hz, and optional amplitude (volume), waveform, envelope, tremolo (volume change) and bend (frequency change). +//| """Construct a Note object, with a frequency in Hz, and optional panning, waveform, envelope, tremolo (volume change) and bend (frequency change). //| //| If waveform or envelope are `None` the synthesizer object's default waveform or envelope are used. //| @@ -99,23 +100,28 @@ MP_PROPERTY_GETSET(synthio_note_frequency_obj, (mp_obj_t)&synthio_note_get_frequency_obj, (mp_obj_t)&synthio_note_set_frequency_obj); -//| amplitude: float -//| """The base amplitude of the note, from 0 to 1""" -STATIC mp_obj_t synthio_note_get_amplitude(mp_obj_t self_in) { +//| panning: float +//| """Defines the channel(s) in which the note appears. +//| +//| -1 is left channel only, 0 is both channels, and 1 is right channel. +//| For fractional values, the note plays at full amplitude in one channel +//| and partial amplitude in the other channel. For instance -.5 plays at full +//| amplitude in the left channel and 1/2 amplitude in the right channel.""" +STATIC mp_obj_t synthio_note_get_panning(mp_obj_t self_in) { synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); - return mp_obj_new_float(common_hal_synthio_note_get_amplitude(self)); + return mp_obj_new_float(common_hal_synthio_note_get_panning(self)); } -MP_DEFINE_CONST_FUN_OBJ_1(synthio_note_get_amplitude_obj, synthio_note_get_amplitude); +MP_DEFINE_CONST_FUN_OBJ_1(synthio_note_get_panning_obj, synthio_note_get_panning); -STATIC mp_obj_t synthio_note_set_amplitude(mp_obj_t self_in, mp_obj_t arg) { +STATIC mp_obj_t synthio_note_set_panning(mp_obj_t self_in, mp_obj_t arg) { synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); - common_hal_synthio_note_set_amplitude(self, mp_obj_get_float(arg)); + common_hal_synthio_note_set_panning(self, mp_obj_get_float(arg)); return mp_const_none; } -MP_DEFINE_CONST_FUN_OBJ_2(synthio_note_set_amplitude_obj, synthio_note_set_amplitude); -MP_PROPERTY_GETSET(synthio_note_amplitude_obj, - (mp_obj_t)&synthio_note_get_amplitude_obj, - (mp_obj_t)&synthio_note_set_amplitude_obj); +MP_DEFINE_CONST_FUN_OBJ_2(synthio_note_set_panning_obj, synthio_note_set_panning); +MP_PROPERTY_GETSET(synthio_note_panning_obj, + (mp_obj_t)&synthio_note_get_panning_obj, + (mp_obj_t)&synthio_note_set_panning_obj); //| tremolo_depth: float @@ -266,7 +272,7 @@ static void note_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_ STATIC const mp_rom_map_elem_t synthio_note_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR_frequency), MP_ROM_PTR(&synthio_note_frequency_obj) }, - { MP_ROM_QSTR(MP_QSTR_amplitude), MP_ROM_PTR(&synthio_note_amplitude_obj) }, + { MP_ROM_QSTR(MP_QSTR_panning), MP_ROM_PTR(&synthio_note_panning_obj) }, { MP_ROM_QSTR(MP_QSTR_waveform), MP_ROM_PTR(&synthio_note_waveform_obj) }, { MP_ROM_QSTR(MP_QSTR_envelope), MP_ROM_PTR(&synthio_note_envelope_obj) }, { MP_ROM_QSTR(MP_QSTR_tremolo_depth), MP_ROM_PTR(&synthio_note_tremolo_depth_obj) }, diff --git a/shared-bindings/synthio/Note.h b/shared-bindings/synthio/Note.h index 90d933fd3f..f441c9ae43 100644 --- a/shared-bindings/synthio/Note.h +++ b/shared-bindings/synthio/Note.h @@ -9,8 +9,8 @@ typedef enum synthio_bend_mode_e synthio_bend_mode_t; mp_float_t common_hal_synthio_note_get_frequency(synthio_note_obj_t *self); void common_hal_synthio_note_set_frequency(synthio_note_obj_t *self, mp_float_t value); -mp_float_t common_hal_synthio_note_get_amplitude(synthio_note_obj_t *self); -void common_hal_synthio_note_set_amplitude(synthio_note_obj_t *self, mp_float_t value); +mp_float_t common_hal_synthio_note_get_panning(synthio_note_obj_t *self); +void common_hal_synthio_note_set_panning(synthio_note_obj_t *self, mp_float_t value); mp_float_t common_hal_synthio_note_get_tremolo_rate(synthio_note_obj_t *self); void common_hal_synthio_note_set_tremolo_rate(synthio_note_obj_t *self, mp_float_t value); diff --git a/shared-bindings/synthio/Synthesizer.c b/shared-bindings/synthio/Synthesizer.c index 5d9be09f70..e7e442d90f 100644 --- a/shared-bindings/synthio/Synthesizer.c +++ b/shared-bindings/synthio/Synthesizer.c @@ -44,6 +44,7 @@ //| self, //| *, //| sample_rate: int = 11025, +//| channel_count: int = 1, //| waveform: Optional[ReadableBuffer] = None, //| envelope: Optional[Envelope] = None, //| ) -> None: @@ -56,13 +57,15 @@ //| and do not support advanced features like tremolo or vibrato. //| //| :param int sample_rate: The desired playback sample rate; higher sample rate requires more memory +//| :param int channel_count: The number of output channels (1=mono, 2=stereo) //| :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, ARG_envelope }; + enum { ARG_sample_rate, ARG_channel_count, 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_channel_count, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 1} }, { 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 } }, }; @@ -77,6 +80,7 @@ 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, + args[ARG_channel_count].u_int, bufinfo_waveform.buf, bufinfo_waveform.len / 2, args[ARG_envelope].u_obj); diff --git a/shared-bindings/synthio/Synthesizer.h b/shared-bindings/synthio/Synthesizer.h index ba6c3b99e3..4568dafdaf 100644 --- a/shared-bindings/synthio/Synthesizer.h +++ b/shared-bindings/synthio/Synthesizer.h @@ -32,7 +32,7 @@ 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_length, + uint32_t sample_rate, int channel_count, 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); diff --git a/shared-module/synthio/MidiTrack.c b/shared-module/synthio/MidiTrack.c index e22753cb26..a24071800b 100644 --- a/shared-module/synthio/MidiTrack.c +++ b/shared-module/synthio/MidiTrack.c @@ -123,7 +123,7 @@ void common_hal_synthio_miditrack_construct(synthio_miditrack_obj_t *self, self->track.buf = (void *)buffer; self->track.len = len; - synthio_synth_init(&self->synth, sample_rate, waveform, waveform_length, envelope); + synthio_synth_init(&self->synth, sample_rate, 1, waveform, waveform_length, envelope); start_parse(self); } diff --git a/shared-module/synthio/Note.c b/shared-module/synthio/Note.c index 65c20024a7..bbd84c729f 100644 --- a/shared-module/synthio/Note.c +++ b/shared-module/synthio/Note.c @@ -44,14 +44,20 @@ void common_hal_synthio_note_set_frequency(synthio_note_obj_t *self, mp_float_t self->frequency_scaled = synthio_frequency_convert_float_to_scaled(val); } -mp_float_t common_hal_synthio_note_get_amplitude(synthio_note_obj_t *self) { - return self->amplitude; +mp_float_t common_hal_synthio_note_get_panning(synthio_note_obj_t *self) { + return self->panning; } -void common_hal_synthio_note_set_amplitude(synthio_note_obj_t *self, mp_float_t value_in) { - mp_float_t val = mp_arg_validate_float_range(value_in, 0, 1, MP_QSTR_amplitude); - self->amplitude = val; - self->amplitude_scaled = round_float_to_int(val * 32767); +void common_hal_synthio_note_set_panning(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_panning); + self->panning = val; + if (val >= 0) { + self->left_panning_scaled = 32768; + self->right_panning_scaled = 32768 - round_float_to_int(val * 32768); + } else { + self->right_panning_scaled = 32768; + self->left_panning_scaled = 32768 + round_float_to_int(val * 32768); + } } mp_float_t common_hal_synthio_note_get_tremolo_depth(synthio_note_obj_t *self) { @@ -200,7 +206,8 @@ STATIC int synthio_bend_value(synthio_note_obj_t *self, int16_t dur) { uint32_t synthio_note_step(synthio_note_obj_t *self, int32_t sample_rate, int16_t dur, uint16_t *loudness) { int tremolo_value = synthio_lfo_step(&self->tremolo_state, dur); - *loudness = (*loudness * tremolo_value) >> 15; + loudness[0] = (((loudness[0] * tremolo_value) >> 15) * self->left_panning_scaled) >> 15; + loudness[1] = (((loudness[1] * tremolo_value) >> 15) * self->right_panning_scaled) >> 15; int bend_value = synthio_bend_value(self, dur); uint32_t frequency_scaled = pitch_bend(self->frequency_scaled, bend_value); return frequency_scaled; diff --git a/shared-module/synthio/Note.h b/shared-module/synthio/Note.h index ffaf7295fa..ec1f262dc5 100644 --- a/shared-module/synthio/Note.h +++ b/shared-module/synthio/Note.h @@ -33,13 +33,14 @@ typedef struct synthio_note_obj { mp_obj_base_t base; mp_float_t frequency; - mp_float_t amplitude; + mp_float_t panning; mp_obj_t waveform_obj, envelope_obj; int32_t sample_rate; int32_t frequency_scaled; int32_t amplitude_scaled; + int32_t left_panning_scaled, right_panning_scaled; synthio_bend_mode_t bend_mode; synthio_lfo_descr_t tremolo_descr, bend_descr; synthio_lfo_state_t tremolo_state, bend_state; @@ -49,7 +50,7 @@ typedef struct synthio_note_obj { } synthio_note_obj_t; void synthio_note_recalculate(synthio_note_obj_t *self, int32_t sample_rate); -uint32_t synthio_note_step(synthio_note_obj_t *self, int32_t sample_rate, int16_t dur, uint16_t *loudness); +uint32_t synthio_note_step(synthio_note_obj_t *self, int32_t sample_rate, int16_t dur, uint16_t loudness[2]); void synthio_note_start(synthio_note_obj_t *self, int32_t sample_rate); bool synthio_note_playing(synthio_note_obj_t *self); uint32_t synthio_note_envelope(synthio_note_obj_t *self); diff --git a/shared-module/synthio/Synthesizer.c b/shared-module/synthio/Synthesizer.c index ad37cccce5..42e234fef1 100644 --- a/shared-module/synthio/Synthesizer.c +++ b/shared-module/synthio/Synthesizer.c @@ -32,10 +32,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, int channel_count, const int16_t *waveform, uint16_t waveform_length, mp_obj_t envelope) { - synthio_synth_init(&self->synth, sample_rate, waveform, waveform_length, envelope); + synthio_synth_init(&self->synth, sample_rate, channel_count, waveform, waveform_length, envelope); } void common_hal_synthio_synthesizer_deinit(synthio_synthesizer_obj_t *self) { @@ -52,7 +52,7 @@ uint8_t common_hal_synthio_synthesizer_get_bits_per_sample(synthio_synthesizer_o return SYNTHIO_BITS_PER_SAMPLE; } uint8_t common_hal_synthio_synthesizer_get_channel_count(synthio_synthesizer_obj_t *self) { - return 1; + return self->synth.channel_count; } void synthio_synthesizer_reset_buffer(synthio_synthesizer_obj_t *self, diff --git a/shared-module/synthio/__init__.c b/shared-module/synthio/__init__.c index 0bd02a3436..051fb522f7 100644 --- a/shared-module/synthio/__init__.c +++ b/shared-module/synthio/__init__.c @@ -187,7 +187,7 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t synth->span.dur -= dur; int32_t sample_rate = synth->sample_rate; - int32_t out_buffer32[dur]; + int32_t out_buffer32[dur * synth->channel_count]; memset(out_buffer32, 0, sizeof(out_buffer32)); for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) { @@ -204,7 +204,7 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t } // adjust loudness by envelope - uint16_t loudness = synth->envelope_state[chan].level; + uint16_t loudness[2] = {synth->envelope_state[chan].level,synth->envelope_state[chan].level}; uint32_t dds_rate; const int16_t *waveform = synth->waveform; @@ -221,7 +221,7 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t dds_rate = (sample_rate / 2 + ((uint64_t)(base_freq * waveform_length) << (SYNTHIO_FREQUENCY_SHIFT - 10 + octave))) / sample_rate; } else { synthio_note_obj_t *note = MP_OBJ_TO_PTR(note_obj); - int32_t frequency_scaled = synthio_note_step(note, sample_rate, dur, &loudness); + int32_t frequency_scaled = synthio_note_step(note, sample_rate, dur, loudness); if (note->waveform_buf.buf) { waveform = note->waveform_buf.buf; waveform_length = note->waveform_buf.len / 2; @@ -241,14 +241,19 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t accum %= lim; } - for (uint16_t i = 0; i < dur; i++) { + int synth_chan = synth->channel_count; + for (uint16_t i = 0, j = 0; i < dur; i++) { accum += dds_rate; // because dds_rate is low enough, the subtraction is guaranteed to go back into range, no expensive modulo needed if (accum > lim) { accum -= lim; } int16_t idx = accum >> SYNTHIO_FREQUENCY_SHIFT; - out_buffer32[i] += (waveform[idx] * loudness) / 65536; + int16_t wi = waveform[idx]; + for (int c = 0; c < synth_chan; c++) { + out_buffer32[j] += (wi * loudness[c]) / 65536; + j++; + } } synth->accum[chan] = accum; } @@ -256,7 +261,7 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t int16_t *out_buffer16 = (int16_t *)(void *)synth->buffers[synth->buffer_index]; // mix down audio - for (size_t i = 0; i < dur; i++) { + for (size_t i = 0; i < MP_ARRAY_SIZE(out_buffer32); i++) { int32_t sample = out_buffer32[i]; out_buffer16[i] = mix_down_sample(sample); } @@ -270,7 +275,7 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t synthio_envelope_state_step(&synth->envelope_state[chan], synthio_synth_get_note_envelope(synth, note_obj), dur); } - *buffer_length = synth->last_buffer_length = dur * SYNTHIO_BYTES_PER_SAMPLE; + *buffer_length = synth->last_buffer_length = dur * SYNTHIO_BYTES_PER_SAMPLE * synth->channel_count; *bufptr = (uint8_t *)out_buffer16; } @@ -301,10 +306,12 @@ 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; +void synthio_synth_init(synthio_synth_t *synth, uint32_t sample_rate, int channel_count, const int16_t *waveform, uint16_t waveform_length, mp_obj_t envelope_obj) { + mp_arg_validate_int_range(channel_count, 1, 2, MP_QSTR_channel_count); + synth->buffer_length = SYNTHIO_MAX_DUR * SYNTHIO_BYTES_PER_SAMPLE * channel_count; synth->buffers[0] = m_malloc(synth->buffer_length, false); synth->buffers[1] = m_malloc(synth->buffer_length, false); + synth->channel_count = channel_count; synth->other_channel = -1; synth->waveform = waveform; synth->waveform_length = waveform_length; @@ -321,7 +328,11 @@ void synthio_synth_get_buffer_structure(synthio_synth_t *synth, bool single_chan *single_buffer = false; *samples_signed = true; *max_buffer_length = synth->buffer_length; - *spacing = 1; + if (single_channel_output) { + *spacing = synth->channel_count; + } else { + *spacing = 1; + } } STATIC bool parse_common(mp_buffer_info_t *bufinfo, mp_obj_t o, int16_t what) { diff --git a/shared-module/synthio/__init__.h b/shared-module/synthio/__init__.h index 53706f0e6c..f4b90a9cac 100644 --- a/shared-module/synthio/__init__.h +++ b/shared-module/synthio/__init__.h @@ -65,6 +65,7 @@ typedef struct synthio_synth { uint32_t total_envelope; int16_t *buffers[2]; const int16_t *waveform; + uint8_t channel_count; uint16_t buffer_length; uint16_t last_buffer_length; uint8_t other_channel, buffer_index, other_buffer_index; @@ -89,7 +90,7 @@ typedef struct { 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, uint32_t sample_rate, const int16_t *waveform, uint16_t waveform_length, +void synthio_synth_init(synthio_synth_t *synth, uint32_t sample_rate, int channel_count, 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); From 095e0208095a5d26d3b884d9e34a930b8a98536a Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Wed, 10 May 2023 12:07:01 -0500 Subject: [PATCH 06/20] synthio: Add ring modulation --- shared-bindings/synthio/Note.c | 47 +++++++++++++ shared-bindings/synthio/Note.h | 6 ++ shared-module/synthio/Note.c | 25 +++++++ shared-module/synthio/Note.h | 6 +- shared-module/synthio/__init__.c | 111 +++++++++++++++++++++++++------ shared-module/synthio/__init__.h | 1 + 6 files changed, 172 insertions(+), 24 deletions(-) diff --git a/shared-bindings/synthio/Note.c b/shared-bindings/synthio/Note.c index 5912ab30c9..b12e015378 100644 --- a/shared-bindings/synthio/Note.c +++ b/shared-bindings/synthio/Note.c @@ -44,6 +44,8 @@ static const mp_arg_t note_properties[] = { { MP_QSTR_bend_mode, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = (mp_obj_t)MP_ROM_PTR(&bend_mode_VIBRATO_obj) } }, { MP_QSTR_waveform, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_NONE } }, { MP_QSTR_envelope, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_NONE } }, + { MP_QSTR_ring_frequency, MP_ARG_OBJ, {.u_obj = NULL } }, + { MP_QSTR_ring_waveform, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_NONE } }, }; //| class Note: //| def __init__( @@ -265,6 +267,49 @@ MP_PROPERTY_GETSET(synthio_note_envelope_obj, (mp_obj_t)&synthio_note_get_envelope_obj, (mp_obj_t)&synthio_note_set_envelope_obj); +//| ring_frequency: float +//| """The ring frequency of the note, in Hz. Zero disables. +//| +//| For ring to take effect, both ring_frequency and ring_wavefor must be set.""" +STATIC mp_obj_t synthio_note_get_ring_frequency(mp_obj_t self_in) { + synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); + return mp_obj_new_float(common_hal_synthio_note_get_ring_frequency(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(synthio_note_get_ring_frequency_obj, synthio_note_get_ring_frequency); + +STATIC mp_obj_t synthio_note_set_ring_frequency(mp_obj_t self_in, mp_obj_t arg) { + synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_synthio_note_set_ring_frequency(self, mp_obj_get_float(arg)); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(synthio_note_set_ring_frequency_obj, synthio_note_set_ring_frequency); +MP_PROPERTY_GETSET(synthio_note_ring_frequency_obj, + (mp_obj_t)&synthio_note_get_ring_frequency_obj, + (mp_obj_t)&synthio_note_set_ring_frequency_obj); + +//| ring_waveform: Optional[ReadableBuffer] +//| """The ring waveform of this note. Setting the ring_waveform to a buffer of a different size resets the note's phase. +//| +//| For ring to take effect, both ring_frequency and ring_wavefor must be set.""" +//| +STATIC mp_obj_t synthio_note_get_ring_waveform(mp_obj_t self_in) { + synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); + return common_hal_synthio_note_get_ring_waveform_obj(self); +} +MP_DEFINE_CONST_FUN_OBJ_1(synthio_note_get_ring_waveform_obj, synthio_note_get_ring_waveform); + +STATIC mp_obj_t synthio_note_set_ring_waveform(mp_obj_t self_in, mp_obj_t arg) { + synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_synthio_note_set_ring_waveform(self, arg); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(synthio_note_set_ring_waveform_obj, synthio_note_set_ring_waveform); +MP_PROPERTY_GETSET(synthio_note_ring_waveform_obj, + (mp_obj_t)&synthio_note_get_ring_waveform_obj, + (mp_obj_t)&synthio_note_set_ring_waveform_obj); + + + static void note_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { (void)kind; properties_print_helper(print, self_in, note_properties, MP_ARRAY_SIZE(note_properties)); @@ -280,6 +325,8 @@ STATIC const mp_rom_map_elem_t synthio_note_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR_bend_depth), MP_ROM_PTR(&synthio_note_bend_depth_obj) }, { MP_ROM_QSTR(MP_QSTR_bend_rate), MP_ROM_PTR(&synthio_note_bend_rate_obj) }, { MP_ROM_QSTR(MP_QSTR_bend_mode), MP_ROM_PTR(&synthio_note_bend_mode_obj) }, + { MP_ROM_QSTR(MP_QSTR_ring_frequency), MP_ROM_PTR(&synthio_note_ring_frequency_obj) }, + { MP_ROM_QSTR(MP_QSTR_ring_waveform), MP_ROM_PTR(&synthio_note_ring_waveform_obj) }, }; STATIC MP_DEFINE_CONST_DICT(synthio_note_locals_dict, synthio_note_locals_dict_table); diff --git a/shared-bindings/synthio/Note.h b/shared-bindings/synthio/Note.h index f441c9ae43..e548f9b5a7 100644 --- a/shared-bindings/synthio/Note.h +++ b/shared-bindings/synthio/Note.h @@ -9,6 +9,9 @@ typedef enum synthio_bend_mode_e synthio_bend_mode_t; mp_float_t common_hal_synthio_note_get_frequency(synthio_note_obj_t *self); void common_hal_synthio_note_set_frequency(synthio_note_obj_t *self, mp_float_t value); +mp_float_t common_hal_synthio_note_get_ring_frequency(synthio_note_obj_t *self); +void common_hal_synthio_note_set_ring_frequency(synthio_note_obj_t *self, mp_float_t value); + mp_float_t common_hal_synthio_note_get_panning(synthio_note_obj_t *self); void common_hal_synthio_note_set_panning(synthio_note_obj_t *self, mp_float_t value); @@ -30,5 +33,8 @@ void common_hal_synthio_note_set_bend_depth(synthio_note_obj_t *self, mp_float_t mp_obj_t common_hal_synthio_note_get_waveform_obj(synthio_note_obj_t *self); void common_hal_synthio_note_set_waveform(synthio_note_obj_t *self, mp_obj_t value); +mp_obj_t common_hal_synthio_note_get_ring_waveform_obj(synthio_note_obj_t *self); +void common_hal_synthio_note_set_ring_waveform(synthio_note_obj_t *self, mp_obj_t value); + mp_obj_t common_hal_synthio_note_get_envelope_obj(synthio_note_obj_t *self); void common_hal_synthio_note_set_envelope(synthio_note_obj_t *self, mp_obj_t value); diff --git a/shared-module/synthio/Note.c b/shared-module/synthio/Note.c index bbd84c729f..87d32da031 100644 --- a/shared-module/synthio/Note.c +++ b/shared-module/synthio/Note.c @@ -44,6 +44,16 @@ void common_hal_synthio_note_set_frequency(synthio_note_obj_t *self, mp_float_t self->frequency_scaled = synthio_frequency_convert_float_to_scaled(val); } +mp_float_t common_hal_synthio_note_get_ring_frequency(synthio_note_obj_t *self) { + return self->ring_frequency; +} + +void common_hal_synthio_note_set_ring_frequency(synthio_note_obj_t *self, mp_float_t value_in) { + mp_float_t val = mp_arg_validate_float_range(value_in, 0, 32767, MP_QSTR_ring_frequency); + self->ring_frequency = val; + self->ring_frequency_scaled = synthio_frequency_convert_float_to_scaled(val); +} + mp_float_t common_hal_synthio_note_get_panning(synthio_note_obj_t *self) { return self->panning; } @@ -142,6 +152,21 @@ void common_hal_synthio_note_set_waveform(synthio_note_obj_t *self, mp_obj_t wav self->waveform_obj = waveform_in; } +mp_obj_t common_hal_synthio_note_get_ring_waveform_obj(synthio_note_obj_t *self) { + return self->ring_waveform_obj; +} + +void common_hal_synthio_note_set_ring_waveform(synthio_note_obj_t *self, mp_obj_t ring_waveform_in) { + if (ring_waveform_in == mp_const_none) { + memset(&self->ring_waveform_buf, 0, sizeof(self->ring_waveform_buf)); + } else { + mp_buffer_info_t bufinfo_ring_waveform; + synthio_synth_parse_waveform(&bufinfo_ring_waveform, ring_waveform_in); + self->ring_waveform_buf = bufinfo_ring_waveform; + } + self->ring_waveform_obj = ring_waveform_in; +} + void synthio_note_recalculate(synthio_note_obj_t *self, int32_t sample_rate) { if (sample_rate == self->sample_rate) { return; diff --git a/shared-module/synthio/Note.h b/shared-module/synthio/Note.h index ec1f262dc5..db3495f8b3 100644 --- a/shared-module/synthio/Note.h +++ b/shared-module/synthio/Note.h @@ -32,13 +32,14 @@ typedef struct synthio_note_obj { mp_obj_base_t base; - mp_float_t frequency; + mp_float_t frequency, ring_frequency; mp_float_t panning; - mp_obj_t waveform_obj, envelope_obj; + mp_obj_t waveform_obj, envelope_obj, ring_waveform_obj; int32_t sample_rate; int32_t frequency_scaled; + int32_t ring_frequency_scaled; int32_t amplitude_scaled; int32_t left_panning_scaled, right_panning_scaled; synthio_bend_mode_t bend_mode; @@ -46,6 +47,7 @@ typedef struct synthio_note_obj { synthio_lfo_state_t tremolo_state, bend_state; mp_buffer_info_t waveform_buf; + mp_buffer_info_t ring_waveform_buf; synthio_envelope_definition_t envelope_def; } synthio_note_obj_t; diff --git a/shared-module/synthio/__init__.c b/shared-module/synthio/__init__.c index 051fb522f7..a7b2e0c25c 100644 --- a/shared-module/synthio/__init__.c +++ b/shared-module/synthio/__init__.c @@ -209,6 +209,11 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t uint32_t dds_rate; const int16_t *waveform = synth->waveform; uint32_t waveform_length = synth->waveform_length; + + uint32_t ring_dds_rate = 0; + const int16_t *ring_waveform = NULL; + uint32_t ring_waveform_length = 0; + if (mp_obj_is_small_int(note_obj)) { uint8_t note = mp_obj_get_int(note_obj); uint8_t octave = note / 12; @@ -227,35 +232,97 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t waveform_length = note->waveform_buf.len / 2; } dds_rate = synthio_frequency_convert_scaled_to_dds((uint64_t)frequency_scaled * waveform_length, sample_rate); - } - - uint32_t accum = synth->accum[chan]; - uint32_t lim = waveform_length << SYNTHIO_FREQUENCY_SHIFT; - if (dds_rate > lim / 2) { - // beyond nyquist, can't play note - continue; - } - - // can happen if note waveform gets set mid-note, but the expensive modulo is usually avoided - if (accum > lim) { - accum %= lim; + if (note->ring_frequency_scaled != 0 && note->ring_waveform_buf.buf) { + ring_waveform = note->ring_waveform_buf.buf; + ring_waveform_length = note->ring_waveform_buf.len / 2; + ring_dds_rate = synthio_frequency_convert_scaled_to_dds((uint64_t)note->ring_frequency_scaled * ring_waveform_length, sample_rate); + uint32_t lim = ring_waveform_length << SYNTHIO_FREQUENCY_SHIFT; + if (ring_dds_rate > lim / 2) { + ring_dds_rate = 0; // can't ring at that frequency + } + } } int synth_chan = synth->channel_count; - for (uint16_t i = 0, j = 0; i < dur; i++) { - accum += dds_rate; - // because dds_rate is low enough, the subtraction is guaranteed to go back into range, no expensive modulo needed + if (ring_dds_rate) { + uint32_t lim = waveform_length << SYNTHIO_FREQUENCY_SHIFT; + uint32_t accum = synth->accum[chan]; + + if (dds_rate > lim / 2) { + // beyond nyquist, can't play note + continue; + } + + // can happen if note waveform gets set mid-note, but the expensive modulo is usually avoided if (accum > lim) { - accum -= lim; + accum %= lim; } - int16_t idx = accum >> SYNTHIO_FREQUENCY_SHIFT; - int16_t wi = waveform[idx]; - for (int c = 0; c < synth_chan; c++) { - out_buffer32[j] += (wi * loudness[c]) / 65536; - j++; + + int32_t ring_buffer[dur]; + // first, fill with waveform + for (uint16_t i = 0; i < dur; i++) { + accum += dds_rate; + // because dds_rate is low enough, the subtraction is guaranteed to go back into range, no expensive modulo needed + if (accum > lim) { + accum -= lim; + } + int16_t idx = accum >> SYNTHIO_FREQUENCY_SHIFT; + ring_buffer[i] = waveform[idx]; } + synth->accum[chan] = accum; + + // now modulate by ring and accumulate + accum = synth->ring_accum[chan]; + lim = ring_waveform_length << SYNTHIO_FREQUENCY_SHIFT; + + // can happen if note waveform gets set mid-note, but the expensive modulo is usually avoided + if (accum > lim) { + accum %= lim; + } + + for (uint16_t i = 0, j = 0; i < dur; i++) { + accum += ring_dds_rate; + // because dds_rate is low enough, the subtraction is guaranteed to go back into range, no expensive modulo needed + if (accum > lim) { + accum -= lim; + } + int16_t idx = accum >> SYNTHIO_FREQUENCY_SHIFT; + int16_t wi = (ring_waveform[idx] * ring_buffer[i]) / 32768; + for (int c = 0; c < synth_chan; c++) { + out_buffer32[j] += (wi * loudness[c]) / 32768; + j++; + } + } + synth->ring_accum[chan] = accum; + } else { + uint32_t lim = waveform_length << SYNTHIO_FREQUENCY_SHIFT; + uint32_t accum = synth->accum[chan]; + + if (dds_rate > lim / 2) { + // beyond nyquist, can't play note + continue; + } + + // can happen if note waveform gets set mid-note, but the expensive modulo is usually avoided + if (accum > lim) { + accum %= lim; + } + + for (uint16_t i = 0, j = 0; i < dur; i++) { + accum += dds_rate; + // because dds_rate is low enough, the subtraction is guaranteed to go back into range, no expensive modulo needed + if (accum > lim) { + accum -= lim; + } + int16_t idx = accum >> SYNTHIO_FREQUENCY_SHIFT; + int16_t wi = waveform[idx]; + for (int c = 0; c < synth_chan; c++) { + out_buffer32[j] += (wi * loudness[c]) / 65536; + j++; + } + } + synth->accum[chan] = accum; } - synth->accum[chan] = accum; } int16_t *out_buffer16 = (int16_t *)(void *)synth->buffers[synth->buffer_index]; diff --git a/shared-module/synthio/__init__.h b/shared-module/synthio/__init__.h index f4b90a9cac..895b0cae5b 100644 --- a/shared-module/synthio/__init__.h +++ b/shared-module/synthio/__init__.h @@ -74,6 +74,7 @@ typedef struct synthio_synth { mp_obj_t envelope_obj; synthio_midi_span_t span; uint32_t accum[CIRCUITPY_SYNTHIO_MAX_CHANNELS]; + uint32_t ring_accum[CIRCUITPY_SYNTHIO_MAX_CHANNELS]; synthio_envelope_state_t envelope_state[CIRCUITPY_SYNTHIO_MAX_CHANNELS]; } synthio_synth_t; From 89080564b4c47296fba4fd33471da2fe5d768b3a Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 11 May 2023 10:19:48 -0500 Subject: [PATCH 07/20] synthio: Fix release time of zero-sustain envelopes When there's no sustain, the release step needs to be calculated from the attack level, not the sustain level. Otherwise, contrary to intent, this leads to the actual release taking a loooonnngg time. --- shared-bindings/synthio/__init__.c | 2 +- shared-module/synthio/__init__.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shared-bindings/synthio/__init__.c b/shared-bindings/synthio/__init__.c index 2739d81e88..de1366717c 100644 --- a/shared-bindings/synthio/__init__.c +++ b/shared-bindings/synthio/__init__.c @@ -80,7 +80,7 @@ static const mp_arg_t envelope_properties[] = { //| //| :param float attack_time: The time in seconds it takes to ramp from 0 volume to attack_volume //| :param float decay_time: The time in seconds it takes to ramp from attack_volume to sustain_volume -//| :param float release_time: The time in seconds it takes to ramp from sustain_volume to release_volume. When a note is released before it has reached the sustain phase, the release is done with the same slope indicated by ``release_time`` and ``sustain_level`` +//| :param float 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``. If the ``sustain_level`` is ``0.0`` then the release slope calculations use the ``attack_level`` instead. //| :param float attack_level: The level, in the range ``0.0`` to ``1.0`` of the peak volume of the attack phase //| :param float sustain_level: The level, in the range ``0.0`` to ``1.0`` of the volume of the sustain phase relative to the attack level //| """ diff --git a/shared-module/synthio/__init__.c b/shared-module/synthio/__init__.c index a7b2e0c25c..2bca3db439 100644 --- a/shared-module/synthio/__init__.c +++ b/shared-module/synthio/__init__.c @@ -89,7 +89,7 @@ void synthio_envelope_definition_set(synthio_envelope_definition_t *envelope, mp envelope->release_step = -convert_time_to_rate( sample_rate, fields[2], - envelope->decay_step + envelope->sustain_level ? envelope->sustain_level : envelope->attack_level); } From ec3096373109ad556ab4885c77fcbaa377a2e760 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 11 May 2023 10:21:56 -0500 Subject: [PATCH 08/20] synthio: fix a -Warray-parameter diagnostic --- shared-module/synthio/Note.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared-module/synthio/Note.c b/shared-module/synthio/Note.c index 87d32da031..9b72129834 100644 --- a/shared-module/synthio/Note.c +++ b/shared-module/synthio/Note.c @@ -229,7 +229,7 @@ STATIC int synthio_bend_value(synthio_note_obj_t *self, int16_t dur) { } } -uint32_t synthio_note_step(synthio_note_obj_t *self, int32_t sample_rate, int16_t dur, uint16_t *loudness) { +uint32_t synthio_note_step(synthio_note_obj_t *self, int32_t sample_rate, int16_t dur, uint16_t loudness[2]) { int tremolo_value = synthio_lfo_step(&self->tremolo_state, dur); loudness[0] = (((loudness[0] * tremolo_value) >> 15) * self->left_panning_scaled) >> 15; loudness[1] = (((loudness[1] * tremolo_value) >> 15) * self->right_panning_scaled) >> 15; From cbd02b74df786d60dd5e7d2e1f2a59a176c3bcba Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 11 May 2023 10:22:51 -0500 Subject: [PATCH 09/20] synthio: endorse updated test result --- tests/circuitpython/synthesizer_note.py.exp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/circuitpython/synthesizer_note.py.exp b/tests/circuitpython/synthesizer_note.py.exp index bb34c0a6c3..e8a1f1e020 100644 --- a/tests/circuitpython/synthesizer_note.py.exp +++ b/tests/circuitpython/synthesizer_note.py.exp @@ -1,10 +1,10 @@ () [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] -(Note(frequency=830.6076004423605, amplitude=1.0, tremolo_rate=0.0, tremolo_depth=0.0, bend_rate=0.0, bend_depth=0.0, bend_mode=synthio.BendType.VIBRATO, waveform=None, envelope=None),) +(Note(frequency=830.6076004423605, panning=0.0, tremolo_rate=0.0, tremolo_depth=0.0, bend_rate=0.0, bend_depth=0.0, bend_mode=synthio.BendType.VIBRATO, waveform=None, envelope=None, ring_frequency=0.0, ring_waveform=None),) [-16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383] -(Note(frequency=830.6076004423605, amplitude=1.0, tremolo_rate=0.0, tremolo_depth=0.0, bend_rate=0.0, bend_depth=0.0, bend_mode=synthio.BendType.VIBRATO, waveform=None, envelope=None), Note(frequency=830.6076004423605, amplitude=1.0, tremolo_rate=0.0, tremolo_depth=0.0, bend_rate=0.0, bend_depth=0.0, bend_mode=synthio.BendType.VIBRATO, waveform=None, envelope=None)) +(Note(frequency=830.6076004423605, panning=0.0, tremolo_rate=0.0, tremolo_depth=0.0, bend_rate=0.0, bend_depth=0.0, bend_mode=synthio.BendType.VIBRATO, waveform=None, envelope=None, ring_frequency=0.0, ring_waveform=None), Note(frequency=830.6076004423605, panning=0.0, tremolo_rate=0.0, tremolo_depth=0.0, bend_rate=0.0, bend_depth=0.0, bend_mode=synthio.BendType.VIBRATO, waveform=None, envelope=None, ring_frequency=0.0, ring_waveform=None)) [0, 0, 0, 0, 0, 0, 0, 0, 28045, 0, 0, 0, 0, -28046, 0, 0, 0, 0, 28045, 0, 0, 0, 0, -28046] -(Note(frequency=830.6076004423605, amplitude=1.0, tremolo_rate=0.0, tremolo_depth=0.0, bend_rate=0.0, bend_depth=0.0, bend_mode=synthio.BendType.VIBRATO, waveform=None, envelope=None),) +(Note(frequency=830.6076004423605, panning=0.0, tremolo_rate=0.0, tremolo_depth=0.0, bend_rate=0.0, bend_depth=0.0, bend_mode=synthio.BendType.VIBRATO, waveform=None, envelope=None, ring_frequency=0.0, ring_waveform=None),) [0, 0, 0, 28045, 0, 0, 0, 0, 0, 0, 0, 0, 28045, 0, 0, 0, 0, -28046, 0, 0, 0, 0, 28045, 0] (-5242, 5242) (-10485, 10484) From 23baf02399f02f9d4a9fecab2ec9cdc974aacee5 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 11 May 2023 15:22:48 -0500 Subject: [PATCH 10/20] synthio: disable on a few boards where it doesn't fit --- ports/atmel-samd/boards/feather_m4_express/mpconfigboard.mk | 1 + ports/atmel-samd/boards/metro_m4_airlift_lite/mpconfigboard.mk | 1 + ports/atmel-samd/boards/metro_m4_express/mpconfigboard.mk | 1 + 3 files changed, 3 insertions(+) diff --git a/ports/atmel-samd/boards/feather_m4_express/mpconfigboard.mk b/ports/atmel-samd/boards/feather_m4_express/mpconfigboard.mk index 2606572d8c..9a5008be85 100644 --- a/ports/atmel-samd/boards/feather_m4_express/mpconfigboard.mk +++ b/ports/atmel-samd/boards/feather_m4_express/mpconfigboard.mk @@ -11,3 +11,4 @@ EXTERNAL_FLASH_DEVICES = GD25Q16C LONGINT_IMPL = MPZ CIRCUITPY__EVE = 1 +CIRCUITPY_SYNTHIO = 0 diff --git a/ports/atmel-samd/boards/metro_m4_airlift_lite/mpconfigboard.mk b/ports/atmel-samd/boards/metro_m4_airlift_lite/mpconfigboard.mk index 1e63476596..ed1cd2455c 100644 --- a/ports/atmel-samd/boards/metro_m4_airlift_lite/mpconfigboard.mk +++ b/ports/atmel-samd/boards/metro_m4_airlift_lite/mpconfigboard.mk @@ -11,3 +11,4 @@ EXTERNAL_FLASH_DEVICES = "S25FL116K, S25FL216K, GD25Q16C" LONGINT_IMPL = MPZ CIRCUITPY__EVE = 1 +CIRCUITPY_SYNTHIO = 0 diff --git a/ports/atmel-samd/boards/metro_m4_express/mpconfigboard.mk b/ports/atmel-samd/boards/metro_m4_express/mpconfigboard.mk index 553bf14f2e..50780cff07 100644 --- a/ports/atmel-samd/boards/metro_m4_express/mpconfigboard.mk +++ b/ports/atmel-samd/boards/metro_m4_express/mpconfigboard.mk @@ -11,3 +11,4 @@ EXTERNAL_FLASH_DEVICES = "S25FL116K, S25FL216K, GD25Q16C" LONGINT_IMPL = MPZ CIRCUITPY__EVE = 1 +CIRCUITPY_SYNTHIO = 0 From 17df23814579c1557f4c00b58923d2fe2d906b2b Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 11 May 2023 15:23:54 -0500 Subject: [PATCH 11/20] synthio: doc fixes, rename BendType to BendMode --- shared-bindings/synthio/Note.c | 6 +++--- shared-bindings/synthio/__init__.c | 13 +++++++++---- tests/circuitpython-manual/synthio/note/code.py | 2 +- tests/circuitpython-manual/synthio/note/noise.py | 2 +- tests/circuitpython/synthesizer_note.py.exp | 6 +++--- 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/shared-bindings/synthio/Note.c b/shared-bindings/synthio/Note.c index b12e015378..dbf0f44495 100644 --- a/shared-bindings/synthio/Note.c +++ b/shared-bindings/synthio/Note.c @@ -59,7 +59,7 @@ static const mp_arg_t note_properties[] = { //| tremolo_rate: float = 0.0, //| bend_depth: float = 0.0, //| bend_rate: float = 0.0, -//| bend_mode: BendMode = BendMode.VIBRATO, +//| bend_mode: "BendMode" = BendMode.VIBRATO, //| ) -> None: //| """Construct a Note object, with a frequency in Hz, and optional panning, waveform, envelope, tremolo (volume change) and bend (frequency change). //| @@ -270,7 +270,7 @@ MP_PROPERTY_GETSET(synthio_note_envelope_obj, //| ring_frequency: float //| """The ring frequency of the note, in Hz. Zero disables. //| -//| For ring to take effect, both ring_frequency and ring_wavefor must be set.""" +//| For ring to take effect, both ``ring_frequency`` and ``ring_waveform`` must be set.""" STATIC mp_obj_t synthio_note_get_ring_frequency(mp_obj_t self_in) { synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); return mp_obj_new_float(common_hal_synthio_note_get_ring_frequency(self)); @@ -290,7 +290,7 @@ MP_PROPERTY_GETSET(synthio_note_ring_frequency_obj, //| ring_waveform: Optional[ReadableBuffer] //| """The ring waveform of this note. Setting the ring_waveform to a buffer of a different size resets the note's phase. //| -//| For ring to take effect, both ring_frequency and ring_wavefor must be set.""" +//| For ring to take effect, both ``ring_frequency`` and ``ring_waveform`` must be set.""" //| STATIC mp_obj_t synthio_note_get_ring_waveform(mp_obj_t self_in) { synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); diff --git a/shared-bindings/synthio/__init__.c b/shared-bindings/synthio/__init__.c index de1366717c..87591c564e 100644 --- a/shared-bindings/synthio/__init__.c +++ b/shared-bindings/synthio/__init__.c @@ -289,36 +289,41 @@ MP_DEFINE_CONST_FUN_OBJ_1(synthio_onevo_to_hz_obj, onevo_to_hz); MAKE_ENUM_VALUE(synthio_bend_mode_type, bend_mode, STATIC, SYNTHIO_BEND_MODE_STATIC); MAKE_ENUM_VALUE(synthio_bend_mode_type, bend_mode, VIBRATO, SYNTHIO_BEND_MODE_VIBRATO); MAKE_ENUM_VALUE(synthio_bend_mode_type, bend_mode, SWEEP, SYNTHIO_BEND_MODE_SWEEP); +MAKE_ENUM_VALUE(synthio_bend_mode_type, bend_mode, SWEEP_IN, SYNTHIO_BEND_MODE_SWEEP_IN); //| -//| class BendType: +//| class BendMode: //| """Controls the way the ``Note.pitch_bend_depth`` and ``Note.pitch_bend_rate`` properties are interpreted.""" //| //| STATIC: object //| """The Note's pitch is modified by its ``pitch_bend_depth``. ``pitch_bend_rate`` is ignored.""" //| //| VIBRATO: object -//| """The Note's pitch varies by ``±pitch_bend_depth` at a rate of ``pitch_bend_rate``Hz.""" +//| """The Note's pitch varies by ``±pitch_bend_depth`` at a rate of ``pitch_bend_rate`` Hz.""" //| //| SWEEP: object //| """The Note's pitch starts at ``Note.frequency`` then sweeps up or down by ``pitch_bend_depth`` over ``1/pitch_bend_rate`` seconds.""" //| +//| SWEEP_IN: object +//| """The Note's pitch sweep is the reverse of ``SWEEP`` mode, starting at the bent pitch and arriving at the tuned pitch.""" +//| MAKE_ENUM_MAP(synthio_bend_mode) { MAKE_ENUM_MAP_ENTRY(bend_mode, STATIC), MAKE_ENUM_MAP_ENTRY(bend_mode, VIBRATO), MAKE_ENUM_MAP_ENTRY(bend_mode, SWEEP), + MAKE_ENUM_MAP_ENTRY(bend_mode, SWEEP_IN), }; STATIC MP_DEFINE_CONST_DICT(synthio_bend_mode_locals_dict, synthio_bend_mode_locals_table); MAKE_PRINTER(synthio, synthio_bend_mode); -MAKE_ENUM_TYPE(synthio, BendType, synthio_bend_mode); +MAKE_ENUM_TYPE(synthio, BendMode, synthio_bend_mode); 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_BendType), MP_ROM_PTR(&synthio_bend_mode_type) }, + { MP_ROM_QSTR(MP_QSTR_BendMode), MP_ROM_PTR(&synthio_bend_mode_type) }, { MP_ROM_QSTR(MP_QSTR_MidiTrack), MP_ROM_PTR(&synthio_miditrack_type) }, { MP_ROM_QSTR(MP_QSTR_Note), MP_ROM_PTR(&synthio_note_type) }, { MP_ROM_QSTR(MP_QSTR_Synthesizer), MP_ROM_PTR(&synthio_synthesizer_type) }, diff --git a/tests/circuitpython-manual/synthio/note/code.py b/tests/circuitpython-manual/synthio/note/code.py index 6a6a9556dc..43f5dfb8a5 100644 --- a/tests/circuitpython-manual/synthio/note/code.py +++ b/tests/circuitpython-manual/synthio/note/code.py @@ -116,7 +116,7 @@ def synthesize6(synth): tremolo_rate=1.5, bend_depth=-5 / 12, bend_rate=1 / 2, - bend_mode=synthio.BendType.SWEEP, + bend_mode=synthio.BendMode.SWEEP, waveform=sine, envelope=envelope, ) diff --git a/tests/circuitpython-manual/synthio/note/noise.py b/tests/circuitpython-manual/synthio/note/noise.py index 53257966cb..b31658b818 100644 --- a/tests/circuitpython-manual/synthio/note/noise.py +++ b/tests/circuitpython-manual/synthio/note/noise.py @@ -37,7 +37,7 @@ def synthesize(synth): frequency=synthio.midi_to_hz(1 + i), waveform=noise, envelope=envelope, - bend_mode=synthio.BendType.SWEEP, + bend_mode=synthio.BendMode.SWEEP, bend_depth=random.choice((-1, 1)), bend_rate=randf(4, 12), ) diff --git a/tests/circuitpython/synthesizer_note.py.exp b/tests/circuitpython/synthesizer_note.py.exp index e8a1f1e020..8c224982a2 100644 --- a/tests/circuitpython/synthesizer_note.py.exp +++ b/tests/circuitpython/synthesizer_note.py.exp @@ -1,10 +1,10 @@ () [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] -(Note(frequency=830.6076004423605, panning=0.0, tremolo_rate=0.0, tremolo_depth=0.0, bend_rate=0.0, bend_depth=0.0, bend_mode=synthio.BendType.VIBRATO, waveform=None, envelope=None, ring_frequency=0.0, ring_waveform=None),) +(Note(frequency=830.6076004423605, panning=0.0, tremolo_rate=0.0, tremolo_depth=0.0, bend_rate=0.0, bend_depth=0.0, bend_mode=synthio.BendMode.VIBRATO, waveform=None, envelope=None, ring_frequency=0.0, ring_waveform=None),) [-16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383] -(Note(frequency=830.6076004423605, panning=0.0, tremolo_rate=0.0, tremolo_depth=0.0, bend_rate=0.0, bend_depth=0.0, bend_mode=synthio.BendType.VIBRATO, waveform=None, envelope=None, ring_frequency=0.0, ring_waveform=None), Note(frequency=830.6076004423605, panning=0.0, tremolo_rate=0.0, tremolo_depth=0.0, bend_rate=0.0, bend_depth=0.0, bend_mode=synthio.BendType.VIBRATO, waveform=None, envelope=None, ring_frequency=0.0, ring_waveform=None)) +(Note(frequency=830.6076004423605, panning=0.0, tremolo_rate=0.0, tremolo_depth=0.0, bend_rate=0.0, bend_depth=0.0, bend_mode=synthio.BendMode.VIBRATO, waveform=None, envelope=None, ring_frequency=0.0, ring_waveform=None), Note(frequency=830.6076004423605, panning=0.0, tremolo_rate=0.0, tremolo_depth=0.0, bend_rate=0.0, bend_depth=0.0, bend_mode=synthio.BendMode.VIBRATO, waveform=None, envelope=None, ring_frequency=0.0, ring_waveform=None)) [0, 0, 0, 0, 0, 0, 0, 0, 28045, 0, 0, 0, 0, -28046, 0, 0, 0, 0, 28045, 0, 0, 0, 0, -28046] -(Note(frequency=830.6076004423605, panning=0.0, tremolo_rate=0.0, tremolo_depth=0.0, bend_rate=0.0, bend_depth=0.0, bend_mode=synthio.BendType.VIBRATO, waveform=None, envelope=None, ring_frequency=0.0, ring_waveform=None),) +(Note(frequency=830.6076004423605, panning=0.0, tremolo_rate=0.0, tremolo_depth=0.0, bend_rate=0.0, bend_depth=0.0, bend_mode=synthio.BendMode.VIBRATO, waveform=None, envelope=None, ring_frequency=0.0, ring_waveform=None),) [0, 0, 0, 28045, 0, 0, 0, 0, 0, 0, 0, 0, 28045, 0, 0, 0, 0, -28046, 0, 0, 0, 0, 28045, 0] (-5242, 5242) (-10485, 10484) From 53e13f15a3f4a0466be84dbffabdf82bd1ee37ac Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 11 May 2023 16:53:40 -0500 Subject: [PATCH 12/20] synthio: Finish ading SWEEP_IN --- shared-bindings/synthio/__init__.h | 2 +- shared-module/synthio/Note.c | 2 ++ shared-module/synthio/__init__.c | 26 ++++++++++++++++++-------- shared-module/synthio/__init__.h | 1 + 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/shared-bindings/synthio/__init__.h b/shared-bindings/synthio/__init__.h index d09fab815e..6f3aed0124 100644 --- a/shared-bindings/synthio/__init__.h +++ b/shared-bindings/synthio/__init__.h @@ -30,7 +30,7 @@ #include "py/enum.h" typedef enum synthio_bend_mode_e { - SYNTHIO_BEND_MODE_STATIC, SYNTHIO_BEND_MODE_VIBRATO, SYNTHIO_BEND_MODE_SWEEP + SYNTHIO_BEND_MODE_STATIC, SYNTHIO_BEND_MODE_VIBRATO, SYNTHIO_BEND_MODE_SWEEP, SYNTHIO_BEND_MODE_SWEEP_IN } synthio_bend_mode_t; extern const cp_enum_obj_t bend_mode_VIBRATO_obj; diff --git a/shared-module/synthio/Note.c b/shared-module/synthio/Note.c index 9b72129834..89de8bce16 100644 --- a/shared-module/synthio/Note.c +++ b/shared-module/synthio/Note.c @@ -224,6 +224,8 @@ STATIC int synthio_bend_value(synthio_note_obj_t *self, int16_t dur) { return synthio_lfo_step(&self->bend_state, dur); case SYNTHIO_BEND_MODE_SWEEP: return synthio_sweep_step(&self->bend_state, dur); + case SYNTHIO_BEND_MODE_SWEEP_IN: + return synthio_sweep_in_step(&self->bend_state, dur); default: return 32768; } diff --git a/shared-module/synthio/__init__.c b/shared-module/synthio/__init__.c index 2bca3db439..36ca709541 100644 --- a/shared-module/synthio/__init__.c +++ b/shared-module/synthio/__init__.c @@ -480,25 +480,35 @@ void synthio_lfo_set(synthio_lfo_state_t *state, const synthio_lfo_descr_t *desc state->dds = synthio_frequency_convert_float_to_dds(descr->frequency * 65536, sample_rate); } -int synthio_sweep_step(synthio_lfo_state_t *state, uint16_t dur) { +STATIC int synthio_lfo_step_common(synthio_lfo_state_t *state, uint16_t dur) { uint32_t phase = state->phase; uint16_t whole_phase = phase >> 16; // advance the phase accumulator state->phase = phase + state->dds * dur; - if (state->phase < phase) { + + return whole_phase; +} +STATIC int synthio_lfo_sweep_common(synthio_lfo_state_t *state, uint16_t dur) { + uint16_t whole_phase = synthio_lfo_step_common(state, dur); + if (state->phase < state->dds) { state->phase = 0xffffffff; } + return whole_phase; +} + +int synthio_sweep_step(synthio_lfo_state_t *state, uint16_t dur) { + uint16_t whole_phase = synthio_lfo_sweep_common(state, dur); + return (state->amplitude_scaled * whole_phase) / 65536 + state->offset_scaled; +} + +int synthio_sweep_in_step(synthio_lfo_state_t *state, uint16_t dur) { + uint16_t whole_phase = 65535 - synthio_lfo_sweep_common(state, dur); return (state->amplitude_scaled * whole_phase) / 65536 + state->offset_scaled; } int synthio_lfo_step(synthio_lfo_state_t *state, uint16_t dur) { - uint32_t phase = state->phase; - uint16_t whole_phase = phase >> 16; - - // advance the phase accumulator - state->phase = phase + state->dds * dur; - + uint16_t whole_phase = synthio_lfo_step_common(state, dur); // create a triangle wave, it's quick and easy int v; if (whole_phase < 16384) { // ramp from 0 to amplitude diff --git a/shared-module/synthio/__init__.h b/shared-module/synthio/__init__.h index 895b0cae5b..c68fb00f97 100644 --- a/shared-module/synthio/__init__.h +++ b/shared-module/synthio/__init__.h @@ -111,3 +111,4 @@ uint32_t synthio_frequency_convert_scaled_to_dds(uint64_t frequency_scaled, int3 void synthio_lfo_set(synthio_lfo_state_t *state, const synthio_lfo_descr_t *descr, uint32_t sample_rate); int synthio_lfo_step(synthio_lfo_state_t *state, uint16_t dur); int synthio_sweep_step(synthio_lfo_state_t *state, uint16_t dur); +int synthio_sweep_in_step(synthio_lfo_state_t *state, uint16_t dur); From d3eda0ad5299c4f26beafd2c683cba13f1e34d1a Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 11 May 2023 17:32:42 -0500 Subject: [PATCH 13/20] synthio: fix a typing error --- shared-bindings/synthio/__init__.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/shared-bindings/synthio/__init__.c b/shared-bindings/synthio/__init__.c index 87591c564e..f776dab251 100644 --- a/shared-bindings/synthio/__init__.c +++ b/shared-bindings/synthio/__init__.c @@ -295,16 +295,16 @@ MAKE_ENUM_VALUE(synthio_bend_mode_type, bend_mode, SWEEP_IN, SYNTHIO_BEND_MODE_S //| class BendMode: //| """Controls the way the ``Note.pitch_bend_depth`` and ``Note.pitch_bend_rate`` properties are interpreted.""" //| -//| STATIC: object +//| STATIC: "BendMode" //| """The Note's pitch is modified by its ``pitch_bend_depth``. ``pitch_bend_rate`` is ignored.""" //| -//| VIBRATO: object +//| VIBRATO: "BendMode" //| """The Note's pitch varies by ``±pitch_bend_depth`` at a rate of ``pitch_bend_rate`` Hz.""" //| -//| SWEEP: object +//| SWEEP: "BendMode" //| """The Note's pitch starts at ``Note.frequency`` then sweeps up or down by ``pitch_bend_depth`` over ``1/pitch_bend_rate`` seconds.""" //| -//| SWEEP_IN: object +//| SWEEP_IN: "BendMode" //| """The Note's pitch sweep is the reverse of ``SWEEP`` mode, starting at the bent pitch and arriving at the tuned pitch.""" //| MAKE_ENUM_MAP(synthio_bend_mode) { From 33fb771b76bfbc2897fb0677d3c6271446329d64 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 11 May 2023 17:56:11 -0500 Subject: [PATCH 14/20] synthio: Add `filter` argument to Synthesizer constructor as step 1/n of adding FIR filtering --- shared-bindings/synthio/MidiTrack.c | 7 ++----- shared-bindings/synthio/MidiTrack.h | 5 ++--- shared-bindings/synthio/Synthesizer.c | 11 +++++------ shared-bindings/synthio/Synthesizer.h | 4 ++-- shared-bindings/synthio/__init__.c | 7 ++----- shared-module/synthio/MidiTrack.c | 5 ++--- shared-module/synthio/Synthesizer.c | 6 +++--- shared-module/synthio/__init__.c | 20 ++++++++++++-------- shared-module/synthio/__init__.h | 7 ++++--- 9 files changed, 34 insertions(+), 38 deletions(-) diff --git a/shared-bindings/synthio/MidiTrack.c b/shared-bindings/synthio/MidiTrack.c index 31d8cbc7f0..2d0d2ab69f 100644 --- a/shared-bindings/synthio/MidiTrack.c +++ b/shared-bindings/synthio/MidiTrack.c @@ -88,9 +88,6 @@ STATIC mp_obj_t synthio_miditrack_make_new(const mp_obj_type_t *type, size_t n_a mp_buffer_info_t bufinfo; mp_get_buffer_raise(args[ARG_buffer].u_obj, &bufinfo, MP_BUFFER_READ); - mp_buffer_info_t bufinfo_waveform; - synthio_synth_parse_waveform(&bufinfo_waveform, args[ARG_waveform].u_obj); - synthio_miditrack_obj_t *self = m_new_obj(synthio_miditrack_obj_t); self->base.type = &synthio_miditrack_type; @@ -98,8 +95,8 @@ STATIC mp_obj_t synthio_miditrack_make_new(const mp_obj_type_t *type, size_t n_a (uint8_t *)bufinfo.buf, bufinfo.len, args[ARG_tempo].u_int, args[ARG_sample_rate].u_int, - bufinfo_waveform.buf, - bufinfo_waveform.len / 2, + args[ARG_waveform].u_obj, + mp_const_none, args[ARG_envelope].u_obj ); diff --git a/shared-bindings/synthio/MidiTrack.h b/shared-bindings/synthio/MidiTrack.h index 1a76ed36f4..5c6bce46cd 100644 --- a/shared-bindings/synthio/MidiTrack.h +++ b/shared-bindings/synthio/MidiTrack.h @@ -27,12 +27,11 @@ #pragma once #include "shared-module/synthio/MidiTrack.h" +#include "py/obj.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, - mp_obj_t envelope); +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, mp_obj_t waveform_obj, mp_obj_t filter_obj, mp_obj_t envelope_obj); void common_hal_synthio_miditrack_deinit(synthio_miditrack_obj_t *self); bool common_hal_synthio_miditrack_deinited(synthio_miditrack_obj_t *self); diff --git a/shared-bindings/synthio/Synthesizer.c b/shared-bindings/synthio/Synthesizer.c index e7e442d90f..67707a763d 100644 --- a/shared-bindings/synthio/Synthesizer.c +++ b/shared-bindings/synthio/Synthesizer.c @@ -59,30 +59,29 @@ //| :param int sample_rate: The desired playback sample rate; higher sample rate requires more memory //| :param int channel_count: The number of output channels (1=mono, 2=stereo) //| :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 ReadableBuffer filter: Coefficients of an FIR filter to apply to notes with ``filter=True``. 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_channel_count, ARG_waveform, ARG_envelope }; + enum { ARG_sample_rate, ARG_channel_count, ARG_waveform, ARG_envelope, ARG_filter }; static const mp_arg_t allowed_args[] = { { MP_QSTR_sample_rate, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 11025} }, { MP_QSTR_channel_count, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 1} }, { 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_QSTR_filter, 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); - mp_buffer_info_t bufinfo_waveform; - synthio_synth_parse_waveform(&bufinfo_waveform, args[ARG_waveform].u_obj); - synthio_synthesizer_obj_t *self = m_new_obj(synthio_synthesizer_obj_t); self->base.type = &synthio_synthesizer_type; common_hal_synthio_synthesizer_construct(self, args[ARG_sample_rate].u_int, args[ARG_channel_count].u_int, - bufinfo_waveform.buf, - bufinfo_waveform.len / 2, + args[ARG_waveform].u_obj, + args[ARG_filter].u_obj, args[ARG_envelope].u_obj); return MP_OBJ_FROM_PTR(self); diff --git a/shared-bindings/synthio/Synthesizer.h b/shared-bindings/synthio/Synthesizer.h index 4568dafdaf..d8000681b8 100644 --- a/shared-bindings/synthio/Synthesizer.h +++ b/shared-bindings/synthio/Synthesizer.h @@ -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, int channel_count, const int16_t *waveform, uint16_t waveform_length, - mp_obj_t envelope); + uint32_t sample_rate, int channel_count, mp_obj_t waveform_obj, mp_obj_t filter_obj, + mp_obj_t envelope_obj); 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); diff --git a/shared-bindings/synthio/__init__.c b/shared-bindings/synthio/__init__.c index f776dab251..132b254942 100644 --- a/shared-bindings/synthio/__init__.c +++ b/shared-bindings/synthio/__init__.c @@ -207,10 +207,6 @@ STATIC mp_obj_t synthio_from_file(size_t n_args, const mp_obj_t *pos_args, mp_ma } pyb_file_obj_t *file = MP_OBJ_TO_PTR(args[ARG_file].u_obj); - - mp_buffer_info_t bufinfo_waveform; - synthio_synth_parse_waveform(&bufinfo_waveform, args[ARG_waveform].u_obj); - uint8_t chunk_header[14]; f_rewind(&file->fp); UINT bytes_read; @@ -250,7 +246,8 @@ 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, args[ARG_waveform].u_obj, + mp_const_none, args[ARG_envelope].u_obj ); diff --git a/shared-module/synthio/MidiTrack.c b/shared-module/synthio/MidiTrack.c index a24071800b..50c7151811 100644 --- a/shared-module/synthio/MidiTrack.c +++ b/shared-module/synthio/MidiTrack.c @@ -116,14 +116,13 @@ STATIC void start_parse(synthio_miditrack_obj_t *self) { 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) { + mp_obj_t waveform_obj, mp_obj_t filter_obj, mp_obj_t envelope_obj) { self->tempo = tempo; self->track.buf = (void *)buffer; self->track.len = len; - synthio_synth_init(&self->synth, sample_rate, 1, waveform, waveform_length, envelope); + synthio_synth_init(&self->synth, sample_rate, 1, waveform_obj, mp_const_none, envelope_obj); start_parse(self); } diff --git a/shared-module/synthio/Synthesizer.c b/shared-module/synthio/Synthesizer.c index 42e234fef1..c18c067976 100644 --- a/shared-module/synthio/Synthesizer.c +++ b/shared-module/synthio/Synthesizer.c @@ -32,10 +32,10 @@ void common_hal_synthio_synthesizer_construct(synthio_synthesizer_obj_t *self, - uint32_t sample_rate, int channel_count, const int16_t *waveform, uint16_t waveform_length, - mp_obj_t envelope) { + uint32_t sample_rate, int channel_count, mp_obj_t waveform_obj, mp_obj_t filter_obj, + mp_obj_t envelope_obj) { - synthio_synth_init(&self->synth, sample_rate, channel_count, waveform, waveform_length, envelope); + synthio_synth_init(&self->synth, sample_rate, channel_count, waveform_obj, filter_obj, envelope_obj); } void common_hal_synthio_synthesizer_deinit(synthio_synthesizer_obj_t *self) { diff --git a/shared-module/synthio/__init__.c b/shared-module/synthio/__init__.c index 36ca709541..71b8250034 100644 --- a/shared-module/synthio/__init__.c +++ b/shared-module/synthio/__init__.c @@ -373,15 +373,16 @@ 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, int channel_count, const int16_t *waveform, uint16_t waveform_length, mp_obj_t envelope_obj) { +void synthio_synth_init(synthio_synth_t *synth, uint32_t sample_rate, int channel_count, mp_obj_t waveform_obj, mp_obj_t filter_obj, mp_obj_t envelope_obj) { mp_arg_validate_int_range(channel_count, 1, 2, MP_QSTR_channel_count); synth->buffer_length = SYNTHIO_MAX_DUR * SYNTHIO_BYTES_PER_SAMPLE * channel_count; synth->buffers[0] = m_malloc(synth->buffer_length, false); synth->buffers[1] = m_malloc(synth->buffer_length, false); synth->channel_count = channel_count; synth->other_channel = -1; - synth->waveform = waveform; - synth->waveform_length = waveform_length; + synth->waveform_obj = waveform_obj; + synthio_synth_parse_waveform(&synth->waveform_bufinfo, waveform_obj); + synthio_synth_parse_filter(&synth->filter_bufinfo, filter_obj); synth->sample_rate = sample_rate; synthio_synth_envelope_set(synth, envelope_obj); @@ -402,21 +403,24 @@ void synthio_synth_get_buffer_structure(synthio_synth_t *synth, bool single_chan } } -STATIC bool parse_common(mp_buffer_info_t *bufinfo, mp_obj_t o, int16_t what) { +STATIC void parse_common(mp_buffer_info_t *bufinfo, mp_obj_t o, int16_t what, mp_int_t max_len) { 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; + mp_arg_validate_length_range(bufinfo->len / 2, 2, max_len, what); } - 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); + parse_common(bufinfo_waveform, waveform_obj, MP_QSTR_waveform, 16384); +} + +void synthio_synth_parse_filter(mp_buffer_info_t *bufinfo_filter, mp_obj_t filter_obj) { + *bufinfo_filter = ((mp_buffer_info_t) { .buf = NULL, .len = 0 }); + parse_common(bufinfo_filter, filter_obj, MP_QSTR_filter, 128); } STATIC int find_channel_with_note(synthio_synth_t *synth, mp_obj_t note) { diff --git a/shared-module/synthio/__init__.h b/shared-module/synthio/__init__.h index c68fb00f97..6241771d33 100644 --- a/shared-module/synthio/__init__.h +++ b/shared-module/synthio/__init__.h @@ -70,8 +70,9 @@ typedef struct synthio_synth { uint16_t last_buffer_length; uint8_t other_channel, buffer_index, other_buffer_index; uint16_t waveform_length; + mp_buffer_info_t waveform_bufinfo, filter_bufinfo; synthio_envelope_definition_t global_envelope_definition; - mp_obj_t envelope_obj; + mp_obj_t waveform_obj, filter_obj, envelope_obj; synthio_midi_span_t span; uint32_t accum[CIRCUITPY_SYNTHIO_MAX_CHANNELS]; uint32_t ring_accum[CIRCUITPY_SYNTHIO_MAX_CHANNELS]; @@ -91,12 +92,12 @@ typedef struct { 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, uint32_t sample_rate, int channel_count, const int16_t *waveform, uint16_t waveform_length, - mp_obj_t envelope); +void synthio_synth_init(synthio_synth_t *synth, uint32_t sample_rate, int channel_count, mp_obj_t waveform_obj, mp_obj_t filter_obj, 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_filter(mp_buffer_info_t *bufinfo_filter, mp_obj_t filter_obj); void synthio_synth_parse_envelope(uint16_t *envelope_sustain_index, mp_buffer_info_t *bufinfo_envelope, mp_obj_t envelope_obj, mp_obj_t envelope_hold_obj); bool synthio_span_change_note(synthio_synth_t *synth, mp_obj_t old_note, mp_obj_t new_note); From 7845a1b13b21b0e2f0f8b61df48ddccfa7f3b9d8 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 11 May 2023 17:58:09 -0500 Subject: [PATCH 15/20] synthio: Add `filter` boolean property to Note objects --- shared-bindings/synthio/Note.c | 20 ++++++++++++++++++++ shared-bindings/synthio/Note.h | 3 +++ shared-module/synthio/Note.c | 8 ++++++++ shared-module/synthio/Note.h | 1 + 4 files changed, 32 insertions(+) diff --git a/shared-bindings/synthio/Note.c b/shared-bindings/synthio/Note.c index dbf0f44495..f058b16d2e 100644 --- a/shared-bindings/synthio/Note.c +++ b/shared-bindings/synthio/Note.c @@ -44,6 +44,7 @@ static const mp_arg_t note_properties[] = { { MP_QSTR_bend_mode, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = (mp_obj_t)MP_ROM_PTR(&bend_mode_VIBRATO_obj) } }, { MP_QSTR_waveform, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_NONE } }, { MP_QSTR_envelope, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_NONE } }, + { MP_QSTR_filter, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_INT(1) } }, { MP_QSTR_ring_frequency, MP_ARG_OBJ, {.u_obj = NULL } }, { MP_QSTR_ring_waveform, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = MP_ROM_NONE } }, }; @@ -102,6 +103,24 @@ MP_PROPERTY_GETSET(synthio_note_frequency_obj, (mp_obj_t)&synthio_note_get_frequency_obj, (mp_obj_t)&synthio_note_set_frequency_obj); +//| filter: bool +//| """True if the note should be processed via the synthesizer's FIR filter.""" +STATIC mp_obj_t synthio_note_get_filter(mp_obj_t self_in) { + synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); + return mp_obj_new_bool(common_hal_synthio_note_get_filter(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(synthio_note_get_filter_obj, synthio_note_get_filter); + +STATIC mp_obj_t synthio_note_set_filter(mp_obj_t self_in, mp_obj_t arg) { + synthio_note_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_synthio_note_set_filter(self, mp_obj_is_true(arg)); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(synthio_note_set_filter_obj, synthio_note_set_filter); +MP_PROPERTY_GETSET(synthio_note_filter_obj, + (mp_obj_t)&synthio_note_get_filter_obj, + (mp_obj_t)&synthio_note_set_filter_obj); + //| panning: float //| """Defines the channel(s) in which the note appears. //| @@ -317,6 +336,7 @@ static void note_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_ STATIC const mp_rom_map_elem_t synthio_note_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR_frequency), MP_ROM_PTR(&synthio_note_frequency_obj) }, + { MP_ROM_QSTR(MP_QSTR_filter), MP_ROM_PTR(&synthio_note_filter_obj) }, { MP_ROM_QSTR(MP_QSTR_panning), MP_ROM_PTR(&synthio_note_panning_obj) }, { MP_ROM_QSTR(MP_QSTR_waveform), MP_ROM_PTR(&synthio_note_waveform_obj) }, { MP_ROM_QSTR(MP_QSTR_envelope), MP_ROM_PTR(&synthio_note_envelope_obj) }, diff --git a/shared-bindings/synthio/Note.h b/shared-bindings/synthio/Note.h index e548f9b5a7..b87310576a 100644 --- a/shared-bindings/synthio/Note.h +++ b/shared-bindings/synthio/Note.h @@ -9,6 +9,9 @@ typedef enum synthio_bend_mode_e synthio_bend_mode_t; mp_float_t common_hal_synthio_note_get_frequency(synthio_note_obj_t *self); void common_hal_synthio_note_set_frequency(synthio_note_obj_t *self, mp_float_t value); +bool common_hal_synthio_note_get_filter(synthio_note_obj_t *self); +void common_hal_synthio_note_set_filter(synthio_note_obj_t *self, bool value); + mp_float_t common_hal_synthio_note_get_ring_frequency(synthio_note_obj_t *self); void common_hal_synthio_note_set_ring_frequency(synthio_note_obj_t *self, mp_float_t value); diff --git a/shared-module/synthio/Note.c b/shared-module/synthio/Note.c index 89de8bce16..e6269eaf2e 100644 --- a/shared-module/synthio/Note.c +++ b/shared-module/synthio/Note.c @@ -44,6 +44,14 @@ void common_hal_synthio_note_set_frequency(synthio_note_obj_t *self, mp_float_t self->frequency_scaled = synthio_frequency_convert_float_to_scaled(val); } +bool common_hal_synthio_note_get_filter(synthio_note_obj_t *self) { + return self->filter; +} + +void common_hal_synthio_note_set_filter(synthio_note_obj_t *self, bool value_in) { + self->filter = value_in; +} + mp_float_t common_hal_synthio_note_get_ring_frequency(synthio_note_obj_t *self) { return self->ring_frequency; } diff --git a/shared-module/synthio/Note.h b/shared-module/synthio/Note.h index db3495f8b3..d7b9a6f67f 100644 --- a/shared-module/synthio/Note.h +++ b/shared-module/synthio/Note.h @@ -42,6 +42,7 @@ typedef struct synthio_note_obj { int32_t ring_frequency_scaled; int32_t amplitude_scaled; int32_t left_panning_scaled, right_panning_scaled; + bool filter; synthio_bend_mode_t bend_mode; synthio_lfo_descr_t tremolo_descr, bend_descr; synthio_lfo_state_t tremolo_state, bend_state; From 9d8dcf7d33b39a00156a675f3cb246b6b26e2ac2 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 11 May 2023 18:11:52 -0500 Subject: [PATCH 16/20] synthio: endorse new test result --- tests/circuitpython/synthesizer_note.py.exp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/circuitpython/synthesizer_note.py.exp b/tests/circuitpython/synthesizer_note.py.exp index 8c224982a2..801f97131e 100644 --- a/tests/circuitpython/synthesizer_note.py.exp +++ b/tests/circuitpython/synthesizer_note.py.exp @@ -1,10 +1,10 @@ () [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] -(Note(frequency=830.6076004423605, panning=0.0, tremolo_rate=0.0, tremolo_depth=0.0, bend_rate=0.0, bend_depth=0.0, bend_mode=synthio.BendMode.VIBRATO, waveform=None, envelope=None, ring_frequency=0.0, ring_waveform=None),) +(Note(frequency=830.6076004423605, panning=0.0, tremolo_rate=0.0, tremolo_depth=0.0, bend_rate=0.0, bend_depth=0.0, bend_mode=synthio.BendMode.VIBRATO, waveform=None, envelope=None, filter=True, ring_frequency=0.0, ring_waveform=None),) [-16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383] -(Note(frequency=830.6076004423605, panning=0.0, tremolo_rate=0.0, tremolo_depth=0.0, bend_rate=0.0, bend_depth=0.0, bend_mode=synthio.BendMode.VIBRATO, waveform=None, envelope=None, ring_frequency=0.0, ring_waveform=None), Note(frequency=830.6076004423605, panning=0.0, tremolo_rate=0.0, tremolo_depth=0.0, bend_rate=0.0, bend_depth=0.0, bend_mode=synthio.BendMode.VIBRATO, waveform=None, envelope=None, ring_frequency=0.0, ring_waveform=None)) +(Note(frequency=830.6076004423605, panning=0.0, tremolo_rate=0.0, tremolo_depth=0.0, bend_rate=0.0, bend_depth=0.0, bend_mode=synthio.BendMode.VIBRATO, waveform=None, envelope=None, filter=True, ring_frequency=0.0, ring_waveform=None), Note(frequency=830.6076004423605, panning=0.0, tremolo_rate=0.0, tremolo_depth=0.0, bend_rate=0.0, bend_depth=0.0, bend_mode=synthio.BendMode.VIBRATO, waveform=None, envelope=None, filter=True, ring_frequency=0.0, ring_waveform=None)) [0, 0, 0, 0, 0, 0, 0, 0, 28045, 0, 0, 0, 0, -28046, 0, 0, 0, 0, 28045, 0, 0, 0, 0, -28046] -(Note(frequency=830.6076004423605, panning=0.0, tremolo_rate=0.0, tremolo_depth=0.0, bend_rate=0.0, bend_depth=0.0, bend_mode=synthio.BendMode.VIBRATO, waveform=None, envelope=None, ring_frequency=0.0, ring_waveform=None),) +(Note(frequency=830.6076004423605, panning=0.0, tremolo_rate=0.0, tremolo_depth=0.0, bend_rate=0.0, bend_depth=0.0, bend_mode=synthio.BendMode.VIBRATO, waveform=None, envelope=None, filter=True, ring_frequency=0.0, ring_waveform=None),) [0, 0, 0, 28045, 0, 0, 0, 0, 0, 0, 0, 0, 28045, 0, 0, 0, 0, -28046, 0, 0, 0, 0, 28045, 0] (-5242, 5242) (-10485, 10484) From 62e6de8ed5fe836b0d7be4ffee8cb4fd7b081dd8 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 11 May 2023 18:13:20 -0500 Subject: [PATCH 17/20] synthio: Factor out `synth_note_into_buffer`, start adding filter buffer --- shared-module/synthio/__init__.c | 284 ++++++++++++++++--------------- shared-module/synthio/__init__.h | 5 +- 2 files changed, 149 insertions(+), 140 deletions(-) diff --git a/shared-module/synthio/__init__.c b/shared-module/synthio/__init__.c index 71b8250034..ac7f6edde7 100644 --- a/shared-module/synthio/__init__.c +++ b/shared-module/synthio/__init__.c @@ -171,6 +171,144 @@ int16_t mix_down_sample(int32_t sample) { return sample; } +static void synth_note_into_buffer(synthio_synth_t *synth, int chan, int32_t *out_buffer32, int16_t dur) { + mp_obj_t note_obj = synth->span.note_obj[chan]; + + if (note_obj == SYNTHIO_SILENCE) { + synth->accum[chan] = 0; + return; + } + + if (synth->envelope_state[chan].level == 0) { + // note is truly finished, but we only just noticed + synth->span.note_obj[chan] = SYNTHIO_SILENCE; + return; + } + + int32_t sample_rate = synth->sample_rate; + + // adjust loudness by envelope + uint16_t loudness[2] = {synth->envelope_state[chan].level,synth->envelope_state[chan].level}; + + uint32_t dds_rate; + const int16_t *waveform = synth->waveform_bufinfo.buf; + uint32_t waveform_length = synth->waveform_bufinfo.len / 2; + + uint32_t ring_dds_rate = 0; + const int16_t *ring_waveform = NULL; + uint32_t ring_waveform_length = 0; + + if (mp_obj_is_small_int(note_obj)) { + uint8_t note = mp_obj_get_int(note_obj); + uint8_t octave = note / 12; + uint16_t base_freq = notes[note % 12]; + // rate = base_freq * waveform_length + // den = sample_rate * 2 ^ (10 - octave) + // den = sample_rate * 2 ^ 10 / 2^octave + // dds_rate = 2^SHIFT * rate / den + // dds_rate = 2^(SHIFT-10+octave) * base_freq * waveform_length / sample_rate + dds_rate = (sample_rate / 2 + ((uint64_t)(base_freq * waveform_length) << (SYNTHIO_FREQUENCY_SHIFT - 10 + octave))) / sample_rate; + } else { + synthio_note_obj_t *note = MP_OBJ_TO_PTR(note_obj); + int32_t frequency_scaled = synthio_note_step(note, sample_rate, dur, loudness); + if (note->waveform_buf.buf) { + waveform = note->waveform_buf.buf; + waveform_length = note->waveform_buf.len / 2; + } + dds_rate = synthio_frequency_convert_scaled_to_dds((uint64_t)frequency_scaled * waveform_length, sample_rate); + if (note->ring_frequency_scaled != 0 && note->ring_waveform_buf.buf) { + ring_waveform = note->ring_waveform_buf.buf; + ring_waveform_length = note->ring_waveform_buf.len / 2; + ring_dds_rate = synthio_frequency_convert_scaled_to_dds((uint64_t)note->ring_frequency_scaled * ring_waveform_length, sample_rate); + uint32_t lim = ring_waveform_length << SYNTHIO_FREQUENCY_SHIFT; + if (ring_dds_rate > lim / 2) { + ring_dds_rate = 0; // can't ring at that frequency + } + } + } + + int synth_chan = synth->channel_count; + if (ring_dds_rate) { + uint32_t lim = waveform_length << SYNTHIO_FREQUENCY_SHIFT; + uint32_t accum = synth->accum[chan]; + + if (dds_rate > lim / 2) { + // beyond nyquist, can't play note + return; + } + + // can happen if note waveform gets set mid-note, but the expensive modulo is usually avoided + if (accum > lim) { + accum %= lim; + } + + int32_t ring_buffer[dur]; + // first, fill with waveform + for (uint16_t i = 0; i < dur; i++) { + accum += dds_rate; + // because dds_rate is low enough, the subtraction is guaranteed to go back into range, no expensive modulo needed + if (accum > lim) { + accum -= lim; + } + int16_t idx = accum >> SYNTHIO_FREQUENCY_SHIFT; + ring_buffer[i] = waveform[idx]; + } + synth->accum[chan] = accum; + + // now modulate by ring and accumulate + accum = synth->ring_accum[chan]; + lim = ring_waveform_length << SYNTHIO_FREQUENCY_SHIFT; + + // can happen if note waveform gets set mid-note, but the expensive modulo is usually avoided + if (accum > lim) { + accum %= lim; + } + + for (uint16_t i = 0, j = 0; i < dur; i++) { + accum += ring_dds_rate; + // because dds_rate is low enough, the subtraction is guaranteed to go back into range, no expensive modulo needed + if (accum > lim) { + accum -= lim; + } + int16_t idx = accum >> SYNTHIO_FREQUENCY_SHIFT; + int16_t wi = (ring_waveform[idx] * ring_buffer[i]) / 32768; + for (int c = 0; c < synth_chan; c++) { + out_buffer32[j] += (wi * loudness[c]) / 32768; + j++; + } + } + synth->ring_accum[chan] = accum; + } else { + uint32_t lim = waveform_length << SYNTHIO_FREQUENCY_SHIFT; + uint32_t accum = synth->accum[chan]; + + if (dds_rate > lim / 2) { + // beyond nyquist, can't play note + return; + } + + // can happen if note waveform gets set mid-note, but the expensive modulo is usually avoided + if (accum > lim) { + accum %= lim; + } + + for (uint16_t i = 0, j = 0; i < dur; i++) { + accum += dds_rate; + // because dds_rate is low enough, the subtraction is guaranteed to go back into range, no expensive modulo needed + if (accum > lim) { + accum -= lim; + } + int16_t idx = accum >> SYNTHIO_FREQUENCY_SHIFT; + int16_t wi = waveform[idx]; + for (int c = 0; c < synth_chan; c++) { + out_buffer32[j] += (wi * loudness[c]) / 65536; + j++; + } + } + synth->accum[chan] = accum; + } +} + void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t *buffer_length, uint8_t channel) { if (channel == synth->other_channel) { @@ -186,145 +324,14 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t uint16_t dur = MIN(SYNTHIO_MAX_DUR, synth->span.dur); synth->span.dur -= dur; - int32_t sample_rate = synth->sample_rate; int32_t out_buffer32[dur * synth->channel_count]; memset(out_buffer32, 0, sizeof(out_buffer32)); for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) { - mp_obj_t note_obj = synth->span.note_obj[chan]; - if (note_obj == SYNTHIO_SILENCE) { - synth->accum[chan] = 0; - continue; - } - - if (synth->envelope_state[chan].level == 0) { - // note is truly finished, but we only just noticed - synth->span.note_obj[chan] = SYNTHIO_SILENCE; - continue; - } - - // adjust loudness by envelope - uint16_t loudness[2] = {synth->envelope_state[chan].level,synth->envelope_state[chan].level}; - - uint32_t dds_rate; - const int16_t *waveform = synth->waveform; - uint32_t waveform_length = synth->waveform_length; - - uint32_t ring_dds_rate = 0; - const int16_t *ring_waveform = NULL; - uint32_t ring_waveform_length = 0; - - if (mp_obj_is_small_int(note_obj)) { - uint8_t note = mp_obj_get_int(note_obj); - uint8_t octave = note / 12; - uint16_t base_freq = notes[note % 12]; - // rate = base_freq * waveform_length - // den = sample_rate * 2 ^ (10 - octave) - // den = sample_rate * 2 ^ 10 / 2^octave - // dds_rate = 2^SHIFT * rate / den - // dds_rate = 2^(SHIFT-10+octave) * base_freq * waveform_length / sample_rate - dds_rate = (sample_rate / 2 + ((uint64_t)(base_freq * waveform_length) << (SYNTHIO_FREQUENCY_SHIFT - 10 + octave))) / sample_rate; - } else { - synthio_note_obj_t *note = MP_OBJ_TO_PTR(note_obj); - int32_t frequency_scaled = synthio_note_step(note, sample_rate, dur, loudness); - if (note->waveform_buf.buf) { - waveform = note->waveform_buf.buf; - waveform_length = note->waveform_buf.len / 2; - } - dds_rate = synthio_frequency_convert_scaled_to_dds((uint64_t)frequency_scaled * waveform_length, sample_rate); - if (note->ring_frequency_scaled != 0 && note->ring_waveform_buf.buf) { - ring_waveform = note->ring_waveform_buf.buf; - ring_waveform_length = note->ring_waveform_buf.len / 2; - ring_dds_rate = synthio_frequency_convert_scaled_to_dds((uint64_t)note->ring_frequency_scaled * ring_waveform_length, sample_rate); - uint32_t lim = ring_waveform_length << SYNTHIO_FREQUENCY_SHIFT; - if (ring_dds_rate > lim / 2) { - ring_dds_rate = 0; // can't ring at that frequency - } - } - } - - int synth_chan = synth->channel_count; - if (ring_dds_rate) { - uint32_t lim = waveform_length << SYNTHIO_FREQUENCY_SHIFT; - uint32_t accum = synth->accum[chan]; - - if (dds_rate > lim / 2) { - // beyond nyquist, can't play note - continue; - } - - // can happen if note waveform gets set mid-note, but the expensive modulo is usually avoided - if (accum > lim) { - accum %= lim; - } - - int32_t ring_buffer[dur]; - // first, fill with waveform - for (uint16_t i = 0; i < dur; i++) { - accum += dds_rate; - // because dds_rate is low enough, the subtraction is guaranteed to go back into range, no expensive modulo needed - if (accum > lim) { - accum -= lim; - } - int16_t idx = accum >> SYNTHIO_FREQUENCY_SHIFT; - ring_buffer[i] = waveform[idx]; - } - synth->accum[chan] = accum; - - // now modulate by ring and accumulate - accum = synth->ring_accum[chan]; - lim = ring_waveform_length << SYNTHIO_FREQUENCY_SHIFT; - - // can happen if note waveform gets set mid-note, but the expensive modulo is usually avoided - if (accum > lim) { - accum %= lim; - } - - for (uint16_t i = 0, j = 0; i < dur; i++) { - accum += ring_dds_rate; - // because dds_rate is low enough, the subtraction is guaranteed to go back into range, no expensive modulo needed - if (accum > lim) { - accum -= lim; - } - int16_t idx = accum >> SYNTHIO_FREQUENCY_SHIFT; - int16_t wi = (ring_waveform[idx] * ring_buffer[i]) / 32768; - for (int c = 0; c < synth_chan; c++) { - out_buffer32[j] += (wi * loudness[c]) / 32768; - j++; - } - } - synth->ring_accum[chan] = accum; - } else { - uint32_t lim = waveform_length << SYNTHIO_FREQUENCY_SHIFT; - uint32_t accum = synth->accum[chan]; - - if (dds_rate > lim / 2) { - // beyond nyquist, can't play note - continue; - } - - // can happen if note waveform gets set mid-note, but the expensive modulo is usually avoided - if (accum > lim) { - accum %= lim; - } - - for (uint16_t i = 0, j = 0; i < dur; i++) { - accum += dds_rate; - // because dds_rate is low enough, the subtraction is guaranteed to go back into range, no expensive modulo needed - if (accum > lim) { - accum -= lim; - } - int16_t idx = accum >> SYNTHIO_FREQUENCY_SHIFT; - int16_t wi = waveform[idx]; - for (int c = 0; c < synth_chan; c++) { - out_buffer32[j] += (wi * loudness[c]) / 65536; - j++; - } - } - synth->accum[chan] = accum; - } + synth_note_into_buffer(synth, chan, out_buffer32, dur); } + int16_t *out_buffer16 = (int16_t *)(void *)synth->buffers[synth->buffer_index]; // mix down audio @@ -358,8 +365,7 @@ bool synthio_synth_deinited(synthio_synth_t *synth) { } void synthio_synth_deinit(synthio_synth_t *synth) { - m_del(uint8_t, synth->buffers[0], synth->buffer_length); - m_del(uint8_t, synth->buffers[1], synth->buffer_length); + synth->filter_buffer = NULL; synth->buffers[0] = NULL; synth->buffers[1] = NULL; } @@ -374,15 +380,19 @@ mp_obj_t synthio_synth_envelope_get(synthio_synth_t *synth) { } void synthio_synth_init(synthio_synth_t *synth, uint32_t sample_rate, int channel_count, mp_obj_t waveform_obj, mp_obj_t filter_obj, mp_obj_t envelope_obj) { + synthio_synth_parse_waveform(&synth->waveform_bufinfo, waveform_obj); + synthio_synth_parse_filter(&synth->filter_bufinfo, filter_obj); mp_arg_validate_int_range(channel_count, 1, 2, MP_QSTR_channel_count); synth->buffer_length = SYNTHIO_MAX_DUR * SYNTHIO_BYTES_PER_SAMPLE * channel_count; synth->buffers[0] = m_malloc(synth->buffer_length, false); synth->buffers[1] = m_malloc(synth->buffer_length, false); + if (synth->filter_bufinfo.len) { + synth->filter_buffer_length = (synth->filter_bufinfo.len + SYNTHIO_MAX_DUR) * channel_count * sizeof(int32_t); + synth->filter_buffer = m_malloc(synth->filter_buffer_length, false); + } synth->channel_count = channel_count; synth->other_channel = -1; synth->waveform_obj = waveform_obj; - synthio_synth_parse_waveform(&synth->waveform_bufinfo, waveform_obj); - synthio_synth_parse_filter(&synth->filter_bufinfo, filter_obj); synth->sample_rate = sample_rate; synthio_synth_envelope_set(synth, envelope_obj); diff --git a/shared-module/synthio/__init__.h b/shared-module/synthio/__init__.h index 6241771d33..a3d0ca57fd 100644 --- a/shared-module/synthio/__init__.h +++ b/shared-module/synthio/__init__.h @@ -64,12 +64,11 @@ typedef struct synthio_synth { uint32_t sample_rate; uint32_t total_envelope; int16_t *buffers[2]; - const int16_t *waveform; + int32_t *filter_buffer; uint8_t channel_count; - uint16_t buffer_length; + uint16_t buffer_length, filter_buffer_length; uint16_t last_buffer_length; uint8_t other_channel, buffer_index, other_buffer_index; - uint16_t waveform_length; mp_buffer_info_t waveform_bufinfo, filter_bufinfo; synthio_envelope_definition_t global_envelope_definition; mp_obj_t waveform_obj, filter_obj, envelope_obj; From f52bb65da6e81299de2e3618e50dde01b906e061 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 11 May 2023 18:41:52 -0500 Subject: [PATCH 18/20] synthio: add some new manual tests --- .../synthio/note/envelope.py | 58 ++++++++++ .../circuitpython-manual/synthio/note/fir.py | 104 ++++++++++++++++++ .../synthio/note/panning.py | 57 ++++++++++ .../circuitpython-manual/synthio/note/ring.py | 60 ++++++++++ 4 files changed, 279 insertions(+) create mode 100644 tests/circuitpython-manual/synthio/note/envelope.py create mode 100644 tests/circuitpython-manual/synthio/note/fir.py create mode 100644 tests/circuitpython-manual/synthio/note/panning.py create mode 100644 tests/circuitpython-manual/synthio/note/ring.py diff --git a/tests/circuitpython-manual/synthio/note/envelope.py b/tests/circuitpython-manual/synthio/note/envelope.py new file mode 100644 index 0000000000..9421cf8c1c --- /dev/null +++ b/tests/circuitpython-manual/synthio/note/envelope.py @@ -0,0 +1,58 @@ +import sys + +sys.path.insert( + 0, f"{__file__.rpartition('/')[0] or '.'}/../../../../frozen/Adafruit_CircuitPython_Wave" +) + +import random +import audiocore +import synthio +from ulab import numpy as np +import adafruit_wave as wave + +SAMPLE_SIZE = 1024 +VOLUME = 32767 +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.05, decay_time=8, release_time=0.25, sustain_level=0) +fast_decay_envelope = synthio.Envelope( + attack_time=0.05, decay_time=0.25, release_time=0.25, sustain_level=0 +) + +synth = synthio.Synthesizer(sample_rate=48000) + + +def synthesize(synth): + notes = (synthio.Note(frequency=440, waveform=sine, envelope=envelope),) + synth.press(notes) + yield 360 + notes[0].envelope = fast_decay_envelope + yield 180 + synth.release_all() + + +def synthesize2(synth): + notes = (synthio.Note(frequency=440, waveform=sine, envelope=envelope),) + synth.press(notes) + yield 360 + synth.release_all() + yield 180 + + +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("envelope.wav", "w") as f: + f.setnchannels(1) + f.setsampwidth(2) + f.setframerate(48000) + for n in chain(synthesize(synth), synthesize2(synth)): + for i in range(n): + result, data = audiocore.get_buffer(synth) + f.writeframes(data) diff --git a/tests/circuitpython-manual/synthio/note/fir.py b/tests/circuitpython-manual/synthio/note/fir.py new file mode 100644 index 0000000000..9d40b584be --- /dev/null +++ b/tests/circuitpython-manual/synthio/note/fir.py @@ -0,0 +1,104 @@ +import sys + +sys.path.insert( + 0, f"{__file__.rpartition('/')[0] or '.'}/../../../../frozen/Adafruit_CircuitPython_Wave" +) + +import random +import audiocore +import synthio +from ulab import numpy as np +import adafruit_wave as wave + +random.seed(9) + +envelope = synthio.Envelope( + attack_time=0.1, decay_time=0.05, release_time=0.2, attack_level=0.8, sustain_level=0.8 +) + +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) + +synth = synthio.Synthesizer(sample_rate=48000, filter=filter_coeffs) + + +def synthesize(synth): + n = synthio.Note( + frequency=120, + envelope=envelope, + filter=False, + ) + + print(synth, n) + synth.press((n,)) + for _ in range(20): + n.frequency *= 1.0595 + yield 36 + synth.release_all() + yield 36 + + n.filter = True + n.frequency = 120 + synth.press((n,)) + for _ in range(20): + n.frequency *= 1.0595 + yield 36 + synth.release_all() + yield 36 + + +def chain(*args): + for a in args: + yield from a + + +# sox -r 48000 -e signed -b 16 -c 1 tune.raw tune.wav +with wave.open("fir.wav", "w") as f: + f.setnchannels(1) + f.setsampwidth(2) + f.setframerate(48000) + for n in chain(synthesize(synth)): + for i in range(n): + result, data = audiocore.get_buffer(synth) + f.writeframes(data) diff --git a/tests/circuitpython-manual/synthio/note/panning.py b/tests/circuitpython-manual/synthio/note/panning.py new file mode 100644 index 0000000000..0b7fb1e2a6 --- /dev/null +++ b/tests/circuitpython-manual/synthio/note/panning.py @@ -0,0 +1,57 @@ +import sys + +sys.path.insert( + 0, f"{__file__.rpartition('/')[0] or '.'}/../../../../frozen/Adafruit_CircuitPython_Wave" +) + +import random +import audiocore +import synthio +from ulab import numpy as np +import adafruit_wave as wave + +random.seed(9) +SAMPLE_SIZE = 1024 +VOLUME = 14700 +sine = np.array( + np.sin(np.linspace(0, 2 * np.pi, SAMPLE_SIZE, endpoint=False)) * VOLUME, + dtype=np.int16, +) + +envelope = synthio.Envelope( + attack_time=0.1, decay_time=0.05, release_time=0.2, attack_level=0.8, sustain_level=0.8 +) + +synth = synthio.Synthesizer(sample_rate=48000, channel_count=2) + + +def synthesize(synth): + n = synthio.Note( + frequency=440, + waveform=sine, + envelope=envelope, + ) + + print(synth, n) + synth.press((n,)) + for p in range(-10, 11, 1): + n.panning = p / 10 + yield 36 + synth.release_all() + yield 36 + + +def chain(*args): + for a in args: + yield from a + + +# sox -r 48000 -e signed -b 16 -c 1 tune.raw tune.wav +with wave.open("panning.wav", "w") as f: + f.setnchannels(2) + f.setsampwidth(2) + f.setframerate(48000) + for n in chain(synthesize(synth)): + for i in range(n): + result, data = audiocore.get_buffer(synth) + f.writeframes(data) diff --git a/tests/circuitpython-manual/synthio/note/ring.py b/tests/circuitpython-manual/synthio/note/ring.py new file mode 100644 index 0000000000..7a418da3af --- /dev/null +++ b/tests/circuitpython-manual/synthio/note/ring.py @@ -0,0 +1,60 @@ +import sys + +sys.path.insert( + 0, f"{__file__.rpartition('/')[0] or '.'}/../../../../frozen/Adafruit_CircuitPython_Wave" +) + +import random +import audiocore +import synthio +from ulab import numpy as np +import adafruit_wave as wave + +random.seed(9) +SAMPLE_SIZE = 1024 +VOLUME = 32767 +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=0.8, sustain_level=0.8 +) + +synth = synthio.Synthesizer(sample_rate=48000) + + +def synthesize(synth): + n = synthio.Note( + frequency=120, + waveform=sine, + ring_waveform=sine, + ring_frequency=769, + envelope=envelope, + bend_mode=synthio.BendType.VIBRATO, + bend_depth=50 / 1200, + bend_rate=7, + ) + + print(synth, n) + synth.press((n,)) + yield 720 + synth.release_all() + yield 36 + + +def chain(*args): + for a in args: + yield from a + + +# sox -r 48000 -e signed -b 16 -c 1 tune.raw tune.wav +with wave.open("ring.wav", "w") as f: + f.setnchannels(1) + f.setsampwidth(2) + f.setframerate(48000) + for n in chain(synthesize(synth)): + for i in range(n): + result, data = audiocore.get_buffer(synth) + f.writeframes(data) From bc7feb30a512fef8927061a724fc0caa46c88a58 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 11 May 2023 18:42:18 -0500 Subject: [PATCH 19/20] synthio: implement FIR filtering A 37-tap filter worked fine on RP2040 (prop maker) at 48kHz, so you can probably pretty much go nuts on M7. --- shared-module/synthio/__init__.c | 68 +++++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 9 deletions(-) diff --git a/shared-module/synthio/__init__.c b/shared-module/synthio/__init__.c index ac7f6edde7..0175629c18 100644 --- a/shared-module/synthio/__init__.c +++ b/shared-module/synthio/__init__.c @@ -192,7 +192,7 @@ static void synth_note_into_buffer(synthio_synth_t *synth, int chan, int32_t *ou uint32_t dds_rate; const int16_t *waveform = synth->waveform_bufinfo.buf; - uint32_t waveform_length = synth->waveform_bufinfo.len / 2; + uint32_t waveform_length = synth->waveform_bufinfo.len / sizeof(int16_t); uint32_t ring_dds_rate = 0; const int16_t *ring_waveform = NULL; @@ -213,15 +213,15 @@ static void synth_note_into_buffer(synthio_synth_t *synth, int chan, int32_t *ou int32_t frequency_scaled = synthio_note_step(note, sample_rate, dur, loudness); if (note->waveform_buf.buf) { waveform = note->waveform_buf.buf; - waveform_length = note->waveform_buf.len / 2; + waveform_length = note->waveform_buf.len / sizeof(int16_t); } dds_rate = synthio_frequency_convert_scaled_to_dds((uint64_t)frequency_scaled * waveform_length, sample_rate); if (note->ring_frequency_scaled != 0 && note->ring_waveform_buf.buf) { ring_waveform = note->ring_waveform_buf.buf; - ring_waveform_length = note->ring_waveform_buf.len / 2; + ring_waveform_length = note->ring_waveform_buf.len / sizeof(int16_t); ring_dds_rate = synthio_frequency_convert_scaled_to_dds((uint64_t)note->ring_frequency_scaled * ring_waveform_length, sample_rate); uint32_t lim = ring_waveform_length << SYNTHIO_FREQUENCY_SHIFT; - if (ring_dds_rate > lim / 2) { + if (ring_dds_rate > lim / sizeof(int16_t)) { ring_dds_rate = 0; // can't ring at that frequency } } @@ -309,6 +309,37 @@ static void synth_note_into_buffer(synthio_synth_t *synth, int chan, int32_t *ou } } +STATIC void run_fir(synthio_synth_t *synth, int32_t *out_buffer32, uint16_t dur) { + int16_t *coeff = (int16_t *)synth->filter_bufinfo.buf; + size_t fir_len = synth->filter_bufinfo.len / sizeof(int16_t); + int32_t *in_buf = synth->filter_buffer; + + // FIR and copy values to output buffer + for (int16_t i = 0; i < dur; 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)); + } + *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)); +} + +STATIC bool synthio_synth_get_note_filtered(mp_obj_t note_obj) { + if (note_obj == mp_const_none) { + return false; + } + if (!mp_obj_is_small_int(note_obj)) { + synthio_note_obj_t *note = MP_OBJ_TO_PTR(note_obj); + return note->filter; + } + return true; +} + void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t *buffer_length, uint8_t channel) { if (channel == synth->other_channel) { @@ -326,11 +357,30 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t int32_t out_buffer32[dur * synth->channel_count]; - memset(out_buffer32, 0, sizeof(out_buffer32)); - for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) { - synth_note_into_buffer(synth, chan, out_buffer32, dur); + 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)); + + for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) { + mp_obj_t note_obj = synth->span.note_obj[chan]; + if (!synthio_synth_get_note_filtered(note_obj)) { + continue; + } + synth_note_into_buffer(synth, chan, filter_start, dur); + } + + run_fir(synth, out_buffer32, dur); + } else { + memset(out_buffer32, 0, sizeof(out_buffer32)); } + for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) { + mp_obj_t note_obj = synth->span.note_obj[chan]; + if (synth->filter_buffer && synthio_synth_get_note_filtered(note_obj)) { + continue; + } + synth_note_into_buffer(synth, chan, out_buffer32, dur); + } int16_t *out_buffer16 = (int16_t *)(void *)synth->buffers[synth->buffer_index]; @@ -387,7 +437,7 @@ void synthio_synth_init(synthio_synth_t *synth, uint32_t sample_rate, int channe synth->buffers[0] = m_malloc(synth->buffer_length, false); synth->buffers[1] = m_malloc(synth->buffer_length, false); if (synth->filter_bufinfo.len) { - synth->filter_buffer_length = (synth->filter_bufinfo.len + SYNTHIO_MAX_DUR) * channel_count * sizeof(int32_t); + synth->filter_buffer_length = (synth->filter_bufinfo.len / 2 + SYNTHIO_MAX_DUR) * channel_count * sizeof(int32_t); synth->filter_buffer = m_malloc(synth->filter_buffer_length, false); } synth->channel_count = channel_count; @@ -419,7 +469,7 @@ STATIC void parse_common(mp_buffer_info_t *bufinfo, mp_obj_t o, int16_t what, mp 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, max_len, what); + mp_arg_validate_length_range(bufinfo->len / sizeof(int16_t), 2, max_len, what); } } From 585b1c23b5b2b9b0eb94b2c8834fbfe7760be45b Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Thu, 11 May 2023 19:13:11 -0500 Subject: [PATCH 20/20] synthio: Fix sweep-type pitch bends The accumulator saturate logic was wrong, and the sweep was never restarted either --- shared-module/synthio/Note.c | 3 +++ shared-module/synthio/__init__.c | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/shared-module/synthio/Note.c b/shared-module/synthio/Note.c index e6269eaf2e..16727b2152 100644 --- a/shared-module/synthio/Note.c +++ b/shared-module/synthio/Note.c @@ -193,6 +193,9 @@ void synthio_note_recalculate(synthio_note_obj_t *self, int32_t sample_rate) { void synthio_note_start(synthio_note_obj_t *self, int32_t sample_rate) { synthio_note_recalculate(self, sample_rate); + if (self->bend_mode != SYNTHIO_BEND_MODE_VIBRATO) { + self->bend_state.phase = 0; + } } uint32_t synthio_note_envelope(synthio_note_obj_t *self) { diff --git a/shared-module/synthio/__init__.c b/shared-module/synthio/__init__.c index 0175629c18..2fed9a5122 100644 --- a/shared-module/synthio/__init__.c +++ b/shared-module/synthio/__init__.c @@ -554,8 +554,9 @@ STATIC int synthio_lfo_step_common(synthio_lfo_state_t *state, uint16_t dur) { return whole_phase; } STATIC int synthio_lfo_sweep_common(synthio_lfo_state_t *state, uint16_t dur) { + uint32_t old_phase = state->phase; uint16_t whole_phase = synthio_lfo_step_common(state, dur); - if (state->phase < state->dds) { + if (state->phase < old_phase) { state->phase = 0xffffffff; } return whole_phase;