Skip to content

Commit

Permalink
Merge branch 'main' into custom-oscillator
Browse files Browse the repository at this point in the history
  • Loading branch information
bwhitman authored Feb 15, 2024
2 parents 528b573 + dfd4d1d commit 72a9995
Show file tree
Hide file tree
Showing 68 changed files with 2,529 additions and 10,366 deletions.
17 changes: 17 additions & 0 deletions .github/workflows/c-cpp.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: C/C++ CI

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- name: make test
run: make test
13 changes: 10 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ all: default

SOURCES = src/algorithms.c src/amy.c src/envelope.c src/examples.c \
src/filters.c src/oscillators.c src/pcm.c src/partials.c src/custom.c \
src/delay.c src/log2_exp2.c
src/delay.c src/log2_exp2.c src/patches.c

OBJECTS = $(patsubst %.c, %.o, src/algorithms.c src/amy.c src/envelope.c \
src/delay.c src/partials.c src/custom.c \
src/delay.c src/partials.c src/custom.c src/patches.c \
src/examples.c src/filters.c src/oscillators.c src/pcm.c src/log2_exp2.c \
src/libminiaudio-audio.c)

Expand All @@ -56,11 +56,18 @@ amy-message: $(OBJECTS) src/amy-message.o
$(CC) $(OBJECTS) src/amy-message.o -Wall $(LIBS) -o $@

amy-module: amy-example
touch src/amy.c; cd src; ${PYTHON} -m pip install . --force-reinstall; cd ..
${PYTHON} -m pip install -r requirements.txt; touch src/amy.c; cd src; ${PYTHON} -m pip install . --force-reinstall; cd ..

test: amy-module
${PYTHON} test.py

# Report the median FILTER_PROCESS timing over 50 runs.
timing: amy-module
for a in 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40; do ${PYTHON} timing.py 2>&1 ; done > /tmp/timings.txt
cat /tmp/timings.txt | grep AMY_RENDER: | sed -e 's/us//' | sort -n | awk ' { a[i++]=$$4; } END { print a[int(i/2)]; }'
cat /tmp/timings.txt | grep FILTER_PROCESS: | sed -e 's/us//' | sort -n | awk ' { a[i++]=$$4; } END { print a[int(i/2)]; }'


web: $(TARGET)
emcc $(SOURCES) $(EMSCRIPTEN_OPTIONS) -O3 -o src/www/amy.js

Expand Down
88 changes: 47 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# AMY - the Additive Music synthesizer librarY

AMY is a fast, small and accurate music synthesizer library written in C with Python and Arduino bindings that deals with combinations of many oscillators very well. It can easily be embedded into almost any program, architecture or microcontroller. It uses fixed point operations, so you don't even need an FPU. We've run AMY on Mac, Linux, ESP32 and ESP32S3, Teensy 3.6, Teensy 4.1, the Raspberry Pi, the Pi Pico RP2040, iOS devices, and more to come.
AMY is a fast, small and accurate music synthesizer library written in C with Python and Arduino bindings that deals with combinations of many oscillators very well. It can easily be embedded into almost any program, architecture or microcontroller. We've run AMY on Mac, Linux, ESP32 and ESP32S3, Teensy 3.6, Teensy 4.1, the Raspberry Pi, the Pi Pico RP2040, iOS devices, and more to come. It is highly optimized for polyphony and poly-timbral operation on even the lowest power and constrained RAM microcontroller but can scale to as many cores as you want.

It can be used as a very good analog-type synthesizer (Juno-6 style) a FM synthesizer (DX7 style), a partial breakpoint synthesizer (Alles machine or Atari AMY), a drum machine (PCM samples included), or as a lower level toolkit to make your own combinations of oscillators, filters, LFOs and effects.

AMY powers the multi-speaker mesh synthesizer [Alles](https://github.com/bwhitman/alles), as well as the [Tulip Creative Computer](https://github.com/bwhitman/tulipcc). Let us know if you use AMY for your own projects and we'll add it here!

Expand Down Expand Up @@ -28,13 +30,17 @@ It supports
* Each oscillator has 3 breakpoint generators, which can modify any combination of amplitude, frequency, duty, filter cutoff, feedback or resonance over time
* Each oscillator can also act as an modulator to modify any combination of parameters of another oscillator, for example, a bass drum can be indicated via a half phase sine wave at 0.25Hz modulating the frequency of another sine wave.
* Control of overall gain and 3-band parametric EQ
* Built in patches for PCM, FM and partials
* Built in patches for PCM, DX7, Juno and partials
* A front end for Juno-6 patches and conversion setup commands
* Built-in clock for short term sequencing of events
* Can use multi-core (including microcontrollers) for rendering if available

The FM synthesizer in AMY is especially well-loved and as close to a real DX7 as you can get. We provide a Python library, `fm.py` that can convert any DX7 patch into AMY setup commands, and also a pure-Python implementation of the AMY FM synthesizer in `dx7_simulator.py`.
The FM synth provides a Python library, [`fm.py`](https://github.com/bwhitman/amy/blob/main/fm.py) that can convert any DX7 patch into AMY setup commands, and also a pure-Python implementation of the AMY FM synthesizer in [`dx7_simulator.py`](https://github.com/bwhitman/amy/blob/main/dx7_simulator.py).

The partial tone synthesizer provides [`partials.py`](https://github.com/bwhitman/amy/blob/main/partials.py), where you can model the partials of any arbitrary audio into AMY setup commands for live partial playback of hundreds of oscillators.

The Juno-6 emulation is in [`juno.py`](https://github.com/bwhitman/amy/blob/main/juno.py) and can read in Juno-6 SYSEX patches and convert them into AMY commands and generate patches.

The partial tone synthesizer also provides `partials.py`, where you can model the partials of any arbitrary audio into AMY setup commands for live partial playback of hundreds of oscillators.

## Using AMY in Arduino

Expand All @@ -43,29 +49,10 @@ Copy this repository to your `Arduino/libraries` folder as `Arduino/libraries/am
The examples rely on the following board packages and libraries:
* RP2040 / Pi Pico: [`arduino-pico`](https://arduino-pico.readthedocs.io/en/latest/install.html#installing-via-arduino-boards-manager)
* Teensy: [`teensyduino`](https://www.pjrc.com/teensy/td_download.html)
* ESP32/ESP32-S3/etc: [`arduino-esp32`](https://espressif-docs.readthedocs-hosted.com/projects/arduino-esp32/en/latest/installing.html) - use a 3.X version when installing
* ESP32/ESP32-S3/etc: [`arduino-esp32`](https://espressif-docs.readthedocs-hosted.com/projects/arduino-esp32/en/latest/installing.html) - use a 2.0.14+ version when installing
* The USB MIDI example requires the [MIDI Library](https://www.arduino.cc/reference/en/libraries/midi-library/)

We provide examples for the Pi Pico and ESP32 that also render in multicore, taking full advantage of the chips for more simultaneous oscillators. If you really want to push the chips to the limit, we recommend using native C code using the `pico-sdk` or `ESP-IDF`.


## Using AMY on bare-metal RP2040 or ESP-IDF (without Arduino)

To build an example without Arduino, supporting multicore, for the Pi Pico / RP2040:

```
gh repo clone raspberrypi/pico-extras
gh repo clone raspberrypi/pico-sdk
# Do whatever installs you need for the pico-sdk
gh repo clone bwhitman/amy
cd amy/src; mkdir build; cd build
export PICO_SDK_PATH=../../../pico-sdk
export PICO_EXTRAS_PATH=../../../pico-extras
cmake ..
make && picotool load amy_example.elf && picotool reboot
```

To build an example of AMY using ESP-IDF for ESP32 variants (without Arduino, supports multi-core), follow the steps in [Alles Flashing](https://github.com/bwhitman/alles/blob/main/alles-flashing.md#set-up-esp-idf) to set up your system with ESP-IDF 5.1-rc2 and building [Alles](https://github.com/bwhitman/alles) for your board.
You can use both cores of supported chips (RP2040 or ESP32) for more oscillators and voices. We provide Arduino examples for the Arduino ESP32 in multicore, and a `pico-sdk` example for the RP2040 that renders in multicore. If you really want to push the chips to the limit, we recommend using native C code using the `pico-sdk` or `ESP-IDF`. You can use [Alles](https://github.com/bwhitman/alles) for a `ESP-IDF` example.

## Using AMY in Python on any platform

Expand All @@ -79,10 +66,9 @@ To run a simple C example on many platforms:

```
make
./amy-example # you should hear FM tones out your default speaker, use ./amy-example -h for options
./amy-example # you should hear tones out your default speaker, use ./amy-example -h for options
```


# Using AMY

AMY can be controlled using its wire protocol or by fillng its data structures directly. It's up to what's easier for you and your application.
Expand All @@ -91,7 +77,7 @@ In Python, rendering to a buffer of samples, using the high level API:

```python
>>> import amy
>>> m = amy.message(osc=0,wave=amy.ALGO,patch=30,note=50,vel=1)
>>> m = amy.message(voices="0",load_patch=130,note=50,vel=1)
>>> print(m) # Show the wire protocol message
't76555951v0w8n50p30l1Z'
>>> amy.send_raw(m)
Expand All @@ -103,7 +89,7 @@ You can also start a thread playing live audio:
```python
>>> import amy
>>> amy.live() # can optinally pass in audio device ID, amy.live(2)
>>> amy.send(osc=0,wave=amy.ALGO,patch=30,note=50,vel=1)
>>> m = amy.send(voices="0",load_patch=130,note=50,vel=1)
>>> amy.stop()
```

Expand All @@ -117,16 +103,16 @@ void bleep() {
int32_t sysclock = amy_sysclock();
e.time = sysclock;
e.wave = SINE;
e.freq = 220;
e.freq_coefs[COEF_CONST] = 220;
e.velocity = 1;
amy_add_event(e);
e.time = sysclock + 150;
e.freq = 440;
e.freq_coefs[COEF_CONST] = 440;
amy_add_event(e);
e.time = sysclock + 300;
e.velocity = 0;
e.amp = 0;
e.freq = 0;
e.freq_coefs[COEF_CONST]=0;
amy_add_event(e);
}

Expand Down Expand Up @@ -177,6 +163,24 @@ On storage connstrained devices, you may want to limit the amount of PCM samples
// or, #include "pcm_small.h"
```

## Voices and patches (DX7 and Juno-6) support

With AMY, you can control the low level oscillators that make up a synthesizer "voice", or you can control voices directly and load in groups of oscillators by sending AMY a patch. A patch is a list of AMY commands that setup one or more oscillators.

A voice in AMY is a collection of oscillators. You can assign patches to any voice number, or set up mulitple voices to have the same patch (for example, a polyphonic synth), and AMY will allocate the oscillators it needs under the hood. You can then play those patches (and modify them) by their voice number. For example, a multitimbral Juno/DX7 synth can be set up like:

```python
amy.send(voices="0,1,2,3", load_patch=1) # juno patch #1 on voice 0-3
amy.send(voices="4,5,6,7", load_patch=129) # DX7 patch #2 on voices 4-7
amy.send(voices="0", note=60, vel=1) # Play note 60 on voice 0
amy.send(voices="0", osc=1, filter_freq="440,0,0,0,5") # adjust the filter on the juno voice (its second oscillator)
```

Our code in `amy_headers.py` generates and bakes in these patches into AMY so they're ready for playback on any device. You can add your own patches by "recording" AMY setup commands and adding them to `patches.h`.

**Note on patches and AMY timing**: If you're using AMY's time scheduler (see below) note that unlike all other AMY commands, allocating new voices from patches (using `load_patch`) will happen once AMY receives the message, not using any advance clock (`time`) you may have set. This default is the right decision for almost all use cases of AMY, but if you do need to be able to "schedule" voice allocations within the short term scheduling window, you can load patches by sending the patch string directly to AMY using the timer, and managing your own oscillator mapping in your code.


# Wire protocol

AMY's wire protocol is a series of numbers delimited by ascii characters that define all possible parameters of an oscillator. This is a design decision intended to make using AMY from any sort of environment as easy as possible, with no data structure or parsing overhead on the client. It's also readable and compact, far more expressive than MIDI and can be sent over network links, UARTs, or as arguments to functions or commands. We've used AMY over multicast UDP, over Javascript, in Max/MSP, in Python, C, Micropython and many more!
Expand Down Expand Up @@ -209,17 +213,21 @@ Here's the full list:
| j | reverb_damping | float 0-1 | Reverb extra decay of high frequencies, default = 0.5. |
| J | reverb_xover_hz | float | Crossover frequency (in Hz) for damping decay, default = 3000. |
| k | chorus_level | float 0-1 | Gain applied to chorus when mixing into output. Set to 0 to turn off chorus. |
| K | load_patch | uint 0-X | Apply a saved patch to start at the selected oscillator |
| L | mod_source | 0 to OSCS-1 | Which oscillator is used as an modulation/LFO source for this oscillator. Source oscillator will be silent. |
| l | vel | float 0-1+ | velocity - >0 to trigger note on, 0 to trigger note off. sets amplitude |
| M | chorus_freq | float | LFO freq of chorus |
| m | chorus_delay | uint 1-512 | Maximum delay in chorus delay lines, in samples. Default 320. |
| N | latency_ms | uint | sets latency in ms. default 0 (see LATENCY) |
| n | note | uint 0-127 | midi note, sets frequency |
| o | algorithm | uint 1-32 | DX7 algorith to use for ALGO type |
| O | algo_source | string | which oscillators to use for the algorithm. list of six, use -1 for not used, e.g 0,1,2,-1,-1-1 |
| p | patch | uint | choose a preloaded PCM sample, partial patch or FM patch number for ALGO waveforms. |
| p | P-patch | uint | choose a preloaded PCM sample or partial patch. Not for DX7 or Juno, use load_patch for those |
| P | phase | float 0-1 | where in the oscillator's cycle to start sampling from (also works on the PCM buffer). default 0 |
| Q | pan | float 0-1 | panning index (for stereo output), 0.0=left, 1.0=right. default 0.5. |
| q | chorus_depth | float | chorus depth |
| R | resonance | float | q factor of biquad filter. in practice, 0-10.0. default 0.7 |
| r | voices | int[,int] | String comma separated list of voices to send message to, or load patch into |
| S | reset | uint | resets given oscillator. set to > OSCS to reset all oscillators, gain and EQ |
| T | bp0_target | uint mask | Which parameter bp0 controls. 1=amp, 2=duty, 4=freq, 8=filter freq, 16=resonance, 32=feedback (can be added together). Can add 64 for linear ramp, otherwise exponential. **Deprecated** for setting targets, subsumbed by ControlCoefs. |
| t | timestamp | uint | ms of expected playback since some fixed start point on your host. you should always give this if you can. |
Expand Down Expand Up @@ -390,22 +398,20 @@ You can set a completely separate breakpoints using the second and third breakpo

## FM & ALGO type

Try default DX7 patches:
Try default DX7 patches, from 128 to 256:

```python
amy.send(wave=amy.ALGO,osc=0,patch=0,note=50,vel=1)
amy.send(wave=amy.ALGO,osc=0,patch=1,note=50,vel=1)
amy.send(voices="0", load_patch=128)
amy.send(voices="0", note=50,vel=1)
```

The `patch` lets you set which preset. It can be from 0 to 1024. Another fun parameter is `ratio`, which for ALGO patch types indicates how slow / fast to play the patch's envelopes. Really cool to slow them down!
The `load_patch` lets you set which preset. Another fun parameter is `ratio`, which for ALGO patch types indicates how slow / fast to play the patch's envelopes. Really cool to slow them down!

```python
amy.send(wave=amy.ALGO,osc=0,note=40,vel=1,ratio=0.5,patch=8) # half speed
amy.send(wave=amy.ALGO,osc=0,note=40,vel=1,ratio=0.05,patch=8) # reaaall sloooow
amy.send(wave=amy.ALGO,osc=0,note=30,vel=1,ratio=0.1,patch=590)
amy.send(voices="0", note=50,vel=1, ratio=0.5) # real slow
```

Let's make the classic FM bell tone ourselves, without a preset. We'll just be using two operators (two sine waves), one modulating the other.
Let's make the classic FM bell tone ourselves, without a patch. We'll just be using two operators (two sine waves), one modulating the other.

![DX7 Algorithms](https://raw.githubusercontent.com/bwhitman/alles/main/pics/dx7_algorithms.jpg)

Expand Down
Loading

0 comments on commit 72a9995

Please sign in to comment.