1
0
mirror of https://github.com/sjlongland/atinysynth.git synced 2025-09-13 18:13:16 +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:
Stuart Longland 2017-06-02 20:29:41 +10:00
parent f982e0c31f
commit 1cc66ace92
Signed by: stuartl
GPG Key ID: F954BBBB7948D546

319
README.md
View File

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