mirror of
https://github.com/sjlongland/atinysynth.git
synced 2025-09-13 10:03:15 +10:00
README.md: Add some more detailed notes.
This documents the theory of operation and how the ports work.
This commit is contained in:
parent
f982e0c31f
commit
1cc66ace92
319
README.md
319
README.md
@ -2,8 +2,8 @@ ADSR-based Polyphonic Synthesizer
|
||||
=================================
|
||||
|
||||
This project is intended to be a polyphonic synthesizer for use in
|
||||
embedded microcontrollers. It features multi-voice synthesis for multiple
|
||||
channels.
|
||||
embedded microcontrollers. It features multi-voice synthesis for
|
||||
multiple channels.
|
||||
|
||||
The synthesis is inspired from the highly regarded MOS Technologies 6581
|
||||
"SID" chip, which supported up to 3 voices each producing either a
|
||||
@ -12,4 +12,317 @@ attack/decay/sustain/release envelope generation.
|
||||
|
||||
This tries to achieve the same thing in software.
|
||||
|
||||
Code is presently a work-in-progress.
|
||||
Principle of operation
|
||||
----------------------
|
||||
|
||||
The library runs as a state machine. Synthesis is performed completely
|
||||
using only integer artihmatic operations: specifically addition,
|
||||
subtraction, left and right shifts, and occasional multiplication. This
|
||||
makes it suitable for smaller CPU cores such as Atmel's TinyAVR, ARM's
|
||||
Cortex M0+, lower-end TI MSP430 and other minimalist CPU cores that lack
|
||||
hardware multipliers or floating-point hardware.
|
||||
|
||||
The data types and sizes are optimised for 8-bit microcontroller
|
||||
hardware.
|
||||
|
||||
The state is defined as an array of "voice" objects, all of the type
|
||||
`struct voice_ch_t` and a synthesizer state machine object of type
|
||||
`struct poly_synth_t`.
|
||||
|
||||
These voices combine an ADSR envelope generator and a waveform
|
||||
generator. A voice is configured by setting the waveform type and
|
||||
frequency in the waveform generator. This algorithmically provides a
|
||||
monophonic tone which is then amplitude-modulated using the ADSR
|
||||
envelope generator.
|
||||
|
||||
Under the control of the synthesizer state machine, the voices are
|
||||
selectively computed and summed to produce a final sample value for the
|
||||
output. The bit masks that enable and mute channels are defined by the
|
||||
`uintptr_t` data type, and so in most cases, 16 or 32 channels can be
|
||||
accommodated depending on the underlying microcontroller.
|
||||
|
||||
### ADSR Envelope
|
||||
|
||||
ADSR stands for Attack-Decay-Sustain-Release, and forms a mechanism for
|
||||
modelling typical instrument sounds. The state machine for the ADSR
|
||||
waveform moves through the following states:
|
||||
|
||||
1. Delay phase: This is a programming convenience that allows for the
|
||||
state of multiple voices to be configured at some convenient point in
|
||||
the program in bulk whilst still providing flexibility on when a
|
||||
particular note is played. As there is no amplitude change, an
|
||||
"infinte" time delay may also be specified here, allowing a note to
|
||||
be configured then triggered "on cue".
|
||||
|
||||
2. Attack phase: The amplitude starts at 0, and using an approximated
|
||||
exponential function, rises rapidly up to the peak amplitude. The
|
||||
exponential function is approximated by left-shifting the peak
|
||||
amplitude value by varying amounts.
|
||||
|
||||
3. Decay phase: The amplitude drops from the peak, to the sustain
|
||||
amplitude. The decay is linear with time and is achieved by
|
||||
subtracting a fraction of the difference between peak and sustain
|
||||
amplitudes each cycle.
|
||||
|
||||
4. Sustain phase: The amplitude is held constant at the sustain
|
||||
amplitude. As there is no amplitude change, it is also possible to
|
||||
define this with an "infinite" duration, allowing the note to be
|
||||
released "on cue" (e.g. when the user releases a key).
|
||||
|
||||
5. Release phase: The amplitude dies off with a reverse-exponential
|
||||
function much like the attack phase. Again, it is approximated by
|
||||
left-shifting the sustain amplitude.
|
||||
|
||||
Typical usage
|
||||
-------------
|
||||
|
||||
The typical usage scenario is to statically define an array of `struct
|
||||
voice_ch_t` objects and a `struct poly_synth_t` object. To set the
|
||||
sample rate, create a header file with the content:
|
||||
|
||||
```
|
||||
#define SYNTH_FREQ (16000)
|
||||
```
|
||||
|
||||
… then in your project's `Makefile` or C-preprocessor settings, define
|
||||
`SYNTH_CFG=\"synth-config.h\"` to tell the library where to find its
|
||||
configuration.
|
||||
|
||||
The above example sets the sample rate to 16kHz… you can set any value
|
||||
here appropriate for your microcontroller.
|
||||
|
||||
The `struct poly_synth_t` is initialised by clearing the `enable` and
|
||||
`mute` members, and setting the `voice` member to the address of the
|
||||
array of `struct voice_ch_t` objects.
|
||||
|
||||
Having initialised the data structures, you can then start reading your
|
||||
musical score. To play a note, you select a voice channel, then:
|
||||
|
||||
* Call `adsr_config` with the arguments:
|
||||
* `time_scale`: number of samples per "time unit"
|
||||
* `delay_time`: number of `time_scale` units before the "attack"
|
||||
phase. If set to `ADSR_INFINITE`, the note is delayed until
|
||||
`adsr_continue` is called.
|
||||
* `attack_time`: number of `time_scale` units taken for the note to
|
||||
reach peak amplitude (`peak_amp`)
|
||||
* `decay_time`: number of `time_scale` units taken for the note to
|
||||
decay to the "sustain" amplitude (`sustain_amp`)
|
||||
* `sustain_time`: number of `time_scale` units taken for the note to
|
||||
hold the `sustain_amp` amplitude before the "release" phase.
|
||||
* `release_time`: number of `time_scale` units taken for the note to
|
||||
decay back to silence.
|
||||
* `peak_amp`: the peak amplitude of the note.
|
||||
* `sustain_amp`: the sustained amplitude of the note.
|
||||
* Call one of the waveform generator set-up functions.
|
||||
* `amplitude` sets the base amplitude for the waveform generator.
|
||||
* `freq` sets the frequency for the waveform generator.
|
||||
* Set the corresponding bits in `struct poly_synth_t`:
|
||||
* set the corresponding `enable` bit to compute the output of that
|
||||
synth voice channel
|
||||
* clear the corresponding `mute` bit for the output of that synth
|
||||
channel to be added to the resultant output.
|
||||
|
||||
Then call `poly_synth_next` repeatedly to read off each sample. The
|
||||
samples are returned as signed 8-bit PCM. Each call will advance the
|
||||
state machines and so successive calls will return consecutive samples.
|
||||
|
||||
As each channel finishes, the corresponding bit in the `enable` member
|
||||
of `struct poly_synth_t` is cleared.
|
||||
|
||||
When all the machines have finished, the `poly_synth_next` function will
|
||||
return all zeros and the `enable` field of `struct poly_synth_t` will be
|
||||
zero.
|
||||
|
||||
### Waveform generators
|
||||
|
||||
There are 5 waveform generator algorithms to choose from. The state
|
||||
machines have the following variables:
|
||||
|
||||
* `sample`: The latest waveform generator sample.
|
||||
* `amplitude`: The peak waveform amplitude (from 0 axis, so half.
|
||||
peak-to-peak).
|
||||
* `period`: The period of the internal state machine counter
|
||||
* `period_remain`: The internal state machine counter itself. This gets
|
||||
set to a value then decremented until it reaches zero.
|
||||
* `step`: The amplitude step size.
|
||||
|
||||
#### DC waveform generator (`voice_wf_set_dc`)
|
||||
|
||||
Configures the waveform generator to generate a "DC" waveform (constant
|
||||
amplitude). Not terribly useful at this time but may be handy if you
|
||||
wish to use the ADSR envelope generator only to modulate lights.
|
||||
|
||||
#### Square wave generator (`voice_wf_set_square`)
|
||||
|
||||
Configures the waveform generator to generate a square wave.
|
||||
|
||||
`sample` is initialised as `+amplitude`, and the half-period is
|
||||
computed as `period=SYNTH_FREQ/(2*freq)`. `period_remain` is
|
||||
initialised to `period`.
|
||||
|
||||
Each sample, `period_remain` is decremented. When `period_remain`
|
||||
reaches zero:
|
||||
|
||||
* `sample` is algebraically negated
|
||||
* `period_remain` is reset back to `period`
|
||||
|
||||
#### Sawtooth wave generator (`voice_wf_set_sawtooth`)
|
||||
|
||||
Configures the waveform generator to produce a sawtooth wave.
|
||||
|
||||
`sample` is initialised as `-amplitude`, and the time period is
|
||||
computed as `period=SYNTH_FREQ/freq`. The `step` is computed as
|
||||
`step=(2*amplitude)/T`. `period_remain` is initialised at `period`.
|
||||
|
||||
Every sample, `sample` is incremented by `step` and `period_remain`
|
||||
decremented. When `period_remain` reaches zero:
|
||||
|
||||
* `sample` is reset to `-amplitude`
|
||||
* `period_remain` reset to `period`
|
||||
|
||||
#### Triangle wave generator (`voice_wf_set_triangle`)
|
||||
|
||||
Configures the waveform generator to produce a triangle wave.
|
||||
|
||||
`sample` is initialised as `-amplitude`, and the time period is
|
||||
computed as `period=SYNTH_FREQ/(2*freq)`. The `step` is computed as
|
||||
`step=(2*amplitude)/T`. `period_remain` is initialised at `period`.
|
||||
|
||||
Every sample, `sample` is incremented by `step` and `period_remain`
|
||||
decremented. When `period_remain` reaches zero:
|
||||
|
||||
* if `step` is negative, `sample` is reset to `-amplitude`, otherwise
|
||||
it is reset to `+amplitude`.
|
||||
* `step` is algebraically negated.
|
||||
* `period_remain` is reset to `period`
|
||||
|
||||
#### Pseudorandom noise generator (`wf_voice_set_noise`)
|
||||
|
||||
This generates random samples at a given amplitude. The randomness
|
||||
depends on the C library's random number generator (`rand()`), so it may
|
||||
help to periodically seed it, perhaps by taking the least-significant
|
||||
bits of ADC readings and feeding those into `srand` to give it some true
|
||||
randomness.
|
||||
|
||||
Ports
|
||||
-----
|
||||
|
||||
The code is written with portability in mind. While it has only ever
|
||||
been compiled on GNU/Linux platforms, it has successfully worked on AVR
|
||||
and Linux/x86-64. The code *should* compile and work for other
|
||||
processor architectures too.
|
||||
|
||||
To build a port, run:
|
||||
|
||||
```
|
||||
$ make PORT=port_name
|
||||
```
|
||||
|
||||
### ATTiny85 port (`attiny85`)
|
||||
|
||||
The ATTiny85 port was the first microcontroller port of this synthesizer
|
||||
library. The demonstration port tries to operate as a stand-alone
|
||||
synthesizer. The PWM output is emitted out of `PB4` and `PB3` is used
|
||||
as an ADC input.
|
||||
|
||||
Connected to `PB3` is a voltage divider network, with the segments
|
||||
connected to Vcc via pushbuttons. When a button is pressed, it shorts a
|
||||
section of the resistor divider out, and a higher voltage is seen on the
|
||||
ADC input.
|
||||
|
||||
The voltage is translated to a button press and used to trigger one of
|
||||
the voices, each of which have been configured with a different note.
|
||||
|
||||
The code is a work-in-progress, with some notable bugginess with the
|
||||
ADC-based keyboard implementation.
|
||||
|
||||
The remaining pins are available for other uses such as I²C/SPI or for
|
||||
additional GPIOs.
|
||||
|
||||
### ATTiny861 port (`attiny861`)
|
||||
|
||||
This code is forked from the ATTiny85 port when it was realised that the
|
||||
ATTiny85 with its 5 usable I/O pins would be incapable of driving lots
|
||||
of lights without a lot of I/O expansion hardware.
|
||||
|
||||
Here, four I/O pins on port B are allocated:
|
||||
|
||||
* `PB3`: PWM audio output
|
||||
* `PB4`: audio enable pin
|
||||
* `PB5`: PWM light output
|
||||
* `PB6`: GPIO enable pin
|
||||
|
||||
The audio amplifier used in the prototype is the NJR NJM2113D, and
|
||||
features a "chip disable" pin that powers down the amplifier when pulled
|
||||
high. The audio enable signal drives a N-channel MOSFET (e.g. 2N7000)
|
||||
that pulls the pin low against a pull-up resistor to +12V.
|
||||
|
||||
A logic high on the audio enable signal turns on the amplifier.
|
||||
|
||||
All of the pins on port A (`PA0` through to `PA7`) are used for
|
||||
interfacing to MOSFETs and pushbuttons by way of a multiplexing circuit
|
||||
driven by the GPIO enable pin. The multiplexing circuit consists of two
|
||||
4066-style analogue switches (I am using Motorola MC14066Bs here because
|
||||
I have dozens of them with 8241 date codes) and a 74374 D-latch.
|
||||
|
||||
The GPIO enable line connects the clock input of the 74374 and the
|
||||
switch enable pins on all switches in both 4066s. The 4066 and 74374
|
||||
inputs are paralleled.
|
||||
|
||||
When GPIO enable is driven low, this turns off the 4066s allowing us to
|
||||
assert signals for the 74374. On the rising edge of GPIO enable, the
|
||||
74374 latches those signals and the 4066s re-connect port A to the
|
||||
outside world. By doing this, port A is able to be used both for
|
||||
control of digital outputs hanging off the 74374 and for analogue +
|
||||
digital I/O through the 4066s.
|
||||
|
||||
The light PWM signal on `PB5` connects to the 74374's "output enable"
|
||||
line, and thus by using pull-down resistors on the outputs of the 74374,
|
||||
it can drive N-channel MOSFETs to control the brightness of 8 lights.
|
||||
|
||||
Individual control of lights can be achieved with ⅛ maximum duty cycle
|
||||
by choosing a single output then driving that line with the desired PWM
|
||||
amplitude.
|
||||
|
||||
### PC port (`pc`)
|
||||
|
||||
This uses `libao` and a command line interface to simulate the output of
|
||||
the synthesizer. It was used to debug the synthesizer.
|
||||
|
||||
The synthesizer commands are given as command-line arguments:
|
||||
|
||||
* `voice V` selects a given voice channel
|
||||
* `mute M` sets the synthesizer `mute` bit-mask
|
||||
* `en M` sets the synthesizer `enable` bit-mask
|
||||
* `dc A` sets the selected voice channel to produce a DC offset of
|
||||
amplitude `A`.
|
||||
* `noise A` sets the selected voice channel to produce pseudorandom
|
||||
noise at amplitude `A`.
|
||||
* `square F A` sets the selected voice channel to produce a square wave
|
||||
of frequency `F` Hz and amplitude `A`.
|
||||
* `sawtooth F A` sets the selected voice channel to produce a sawtooth
|
||||
wave of frequency `F` Hz and amplitude `A`.
|
||||
* `triangle F A` sets the selected voice channel to produce a triangle
|
||||
wave of frequency `F` Hz and amplitude `A`.
|
||||
* `scale N` sets the ADSR time unit scale for the selected channel to
|
||||
`N` samples per "tick"
|
||||
* `delay N` sets the ADSR delay period for the selected channel to `N`
|
||||
"ticks"
|
||||
* `attack N` sets the ADSR attack period for the selected channel to `N`
|
||||
"ticks"
|
||||
* `decay N` sets the ADSR decay period for the selected channel to `N`
|
||||
"ticks"
|
||||
* `sustain N` sets the ADSR sustain period for the selected channel to `N`
|
||||
"ticks"
|
||||
* `release N` sets the ADSR release period for the selected channel to `N`
|
||||
"ticks"
|
||||
* `peak A` sets the peak ADSR amplitude for the selected channel to `A`
|
||||
* `samp A` sets the sustained ADSR amplitude for the selected channel to
|
||||
`A`
|
||||
* `reset` resets the ADSR state machine for the selected channel.
|
||||
|
||||
Once the `enable` bit-mask is set, the program loops, playing sound via
|
||||
`libao` and writing the samples to `out.raw` for later analysis until
|
||||
all bits in the `enable` bit-mask are cleared by the ADSR state machines.
|
||||
|
||||
When the program runs out of command line arguments, it exits.
|
||||
|
Loading…
Reference in New Issue
Block a user