Atinysynth
ADSR embedded polyphonic synthesizer for microcontrollers
Install / Use
/learn @sjlongland/AtinysynthREADME
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.
The synthesis is inspired from the highly regarded MOS Technologies 6581 "SID" chip, which supported up to 3 voices each producing either a square wave, triangle wave or sawtooth wave output and hardware attack/decay/sustain/release envelope generation.
This tries to achieve the same thing in software.
Principle of operation
The library runs as a state machine. Synthesis is performed completely using only integer arithmetic 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, Microchip PIC12 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:
-
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 "infinite" time delay may also be specified here, allowing a note to be configured then triggered "on cue".
-
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.
-
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.
-
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).
-
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_configwith the arguments:time_scale: number of samples per "time unit"delay_time: number oftime_scaleunits before the "attack" phase. If set toADSR_INFINITE, the note is delayed untiladsr_continueis called.attack_time: number oftime_scaleunits taken for the note to reach peak amplitude (peak_amp)decay_time: number oftime_scaleunits taken for the note to decay to the "sustain" amplitude (sustain_amp)sustain_time: number oftime_scaleunits taken for the note to hold thesustain_ampamplitude before the "release" phase.release_time: number oftime_scaleunits 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.
amplitudesets the base amplitude for the waveform generator.freqsets the frequency for the waveform generator.
- Set the corresponding bits in
struct poly_synth_t:- set the corresponding
enablebit to compute the output of that synth voice channel - clear the corresponding
mutebit for the output of that synth channel to be added to the resultant output.
- set the corresponding
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 counterperiod_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.
Fixed-point 12.4 format (16 bit) is used to store the period counters, to
allow tuned notes on low sampling rates too.
Each sample, period_remain is decremented. When period_remain reaches
zero:
sampleis algebraically negatedperiod_remainis reset back toperiod
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:
sampleis reset to-amplitudeperiod_remainreset toperiod
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
stepis negative,sampleis reset to-amplitude, otherwise it is reset to+amplitude. stepis algebraically negated.period_remainis reset toperiod
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.
Sequencer
Since the synthesizer state machine is effective in defining when a "note" envelope is terminated, it is then possible to store all the subsequent "notes" in a stream of consecutive steps. Each step contains a pair of waveform settings and ADSR settings.
This allow polyphonic tunes to be "pre-compiled" and stored in small binary files, or microcontroller EEPROM, and to be accessed in serial fashion.
Each tune are stored in a way that each frame in the stream should feed the next available channel with the enable flag of the struct poly_synth_t structure reset.
In order to arrange the steps of all the channels in the correct sequence, a sequencer compiler has to be run on all the channel steps, and sort it correctly using an instance of the synth configured in the exact way of the target system (e.g. same sampling rate, same number of voices, etc...).
This compiler is not optimized to run on a microcontroller (it requires dynamic memory allocation), but to be run on a PC in order to obtain compact binary files to be played by the sequencer on the host MCU.
To save memory for the tiniest 8-bit microcontrollers, the sequencer stream header and the steps are defined in a compa
Related Skills
node-connect
352.5kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
111.3kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
352.5kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
352.5kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
