{"id":24442601,"url":"https://github.com/sjlongland/atinysynth","last_synced_at":"2025-04-12T21:06:12.235Z","repository":{"id":49957790,"uuid":"87621286","full_name":"sjlongland/atinysynth","owner":"sjlongland","description":"ADSR embedded polyphonic synthesizer for microcontrollers","archived":false,"fork":false,"pushed_at":"2021-06-07T22:27:29.000Z","size":141,"stargazers_count":21,"open_issues_count":0,"forks_count":3,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-12T21:05:55.858Z","etag":null,"topics":["adsr","attiny85","attiny861","microcontroller","music","synthesis","synthesizer"],"latest_commit_sha":null,"homepage":"https://stuartl.longlandclan.id.au/blog/category/projects/toy-synthesizer/","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sjlongland.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-04-08T08:58:21.000Z","updated_at":"2024-06-20T08:34:44.000Z","dependencies_parsed_at":"2022-08-30T05:40:30.673Z","dependency_job_id":null,"html_url":"https://github.com/sjlongland/atinysynth","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sjlongland%2Fatinysynth","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sjlongland%2Fatinysynth/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sjlongland%2Fatinysynth/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sjlongland%2Fatinysynth/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sjlongland","download_url":"https://codeload.github.com/sjlongland/atinysynth/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248631685,"owners_count":21136562,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["adsr","attiny85","attiny861","microcontroller","music","synthesis","synthesizer"],"created_at":"2025-01-20T21:59:27.683Z","updated_at":"2025-04-12T21:06:12.213Z","avatar_url":"https://github.com/sjlongland.png","language":"C","funding_links":[],"categories":[],"sub_categories":[],"readme":"ADSR-based Polyphonic Synthesizer\n=================================\n\nThis project is intended to be a polyphonic synthesizer for use in embedded\nmicrocontrollers.  It features multi-voice synthesis for multiple channels.\n\nThe synthesis is inspired from the highly regarded MOS Technologies 6581 \"SID\"\nchip, which supported up to 3 voices each producing either a square wave,\ntriangle wave or sawtooth wave output and hardware\nattack/decay/sustain/release envelope generation.\n\nThis tries to achieve the same thing in software.\n\nPrinciple of operation\n----------------------\n\nThe library runs as a state machine.  Synthesis is performed completely using\nonly integer arithmetic operations: specifically addition, subtraction, left\nand right shifts, and occasional multiplication.  This makes it suitable for\nsmaller CPU cores such as Atmel's TinyAVR, ARM's Cortex M0+, lower-end TI\nMSP430, [Microchip PIC12](https://github.com/lmartorella/atinysynth) and other\nminimalist CPU cores that lack hardware multipliers or floating-point hardware.\n\nThe data types and sizes are optimised for 8-bit microcontroller hardware.\n\nThe state is defined as an array of \"voice\" objects, all of the type `struct\nvoice_ch_t` and a synthesizer state machine object of type `struct\npoly_synth_t`.\n\nThese voices combine an ADSR envelope generator and a waveform generator.  A\nvoice is configured by setting the waveform type and frequency in the waveform\ngenerator.  This algorithmically provides a monophonic tone which is then\namplitude-modulated using the ADSR envelope generator.\n\nUnder the control of the synthesizer state machine, the voices are selectively\ncomputed and summed to produce a final sample value for the output.  The bit\nmasks that enable and mute channels are defined by the `uintptr_t` data type,\nand so in most cases, 16 or 32 channels can be accommodated depending on the\nunderlying microcontroller.\n\n### ADSR Envelope\n\nADSR stands for Attack-Decay-Sustain-Release, and forms a mechanism for\nmodelling typical instrument sounds.  The state machine for the ADSR waveform\nmoves through the following states:\n\n1. Delay phase: This is a programming convenience that allows for the state of\n   multiple voices to be configured at some convenient point in the program in\n   bulk whilst still providing flexibility on when a particular note is\n   played.  As there is no amplitude change, an \"infinite\" time delay may also\n   be specified here, allowing a note to be configured then triggered \"on\n   cue\".\n\n2. Attack phase: The amplitude starts at 0, and using an approximated\n   exponential function, rises rapidly up to the peak amplitude.  The\n   exponential function is approximated by left-shifting the peak amplitude\n   value by varying amounts.\n\n3. Decay phase: The amplitude drops from the peak, to the sustain amplitude.\n   The decay is linear with time and is achieved by subtracting a fraction of\n   the difference between peak and sustain amplitudes each cycle.\n\n4. Sustain phase: The amplitude is held constant at the sustain amplitude.  As\n   there is no amplitude change, it is also possible to define this with an\n   \"infinite\" duration, allowing the note to be released \"on cue\" (e.g. when\n   the user releases a key).\n\n5. Release phase: The amplitude dies off with a reverse-exponential function\n   much like the attack phase.  Again, it is approximated by left-shifting the\n   sustain amplitude.\n\nTypical usage\n-------------\n\nThe typical usage scenario is to statically define an array of `struct\nvoice_ch_t` objects and a `struct poly_synth_t` object.  To set the sample\nrate, create a header file with the content:\n\n```\n#define SYNTH_FREQ\t\t(16000)\n```\n\n… then in your project's `Makefile` or C-preprocessor settings, define\n`SYNTH_CFG=\\\"synth-config.h\\\"` to tell the library where to find its\nconfiguration.\n\nThe above example sets the sample rate to 16kHz… you can set any value here\nappropriate for your microcontroller.\n\nThe `struct poly_synth_t` is initialised by clearing the `enable` and `mute`\nmembers, and setting the `voice` member to the address of the array of `struct\nvoice_ch_t` objects.\n\nHaving initialised the data structures, you can then start reading your\nmusical score.  To play a note, you select a voice channel, then:\n\n* Call `adsr_config` with the arguments:\n  * `time_scale`: number of samples per \"time unit\"\n  * `delay_time`: number of `time_scale` units before the \"attack\" phase.  If\n    set to `ADSR_INFINITE`, the note is delayed until `adsr_continue` is\n    called.\n  * `attack_time`: number of `time_scale` units taken for the note to reach\n    peak amplitude (`peak_amp`)\n  * `decay_time`: number of `time_scale` units taken for the note to decay to\n    the \"sustain\" amplitude (`sustain_amp`)\n  * `sustain_time`: number of `time_scale` units taken for the note to hold\n    the `sustain_amp` amplitude before the \"release\" phase.\n  * `release_time`: number of `time_scale` units taken for the note to decay\n    back to silence.\n  * `peak_amp`: the peak amplitude of the note.\n  * `sustain_amp`: the sustained amplitude of the note.\n* Call one of the waveform generator set-up functions.\n  * `amplitude` sets the base amplitude for the waveform generator.\n  * `freq` sets the frequency for the waveform generator.\n* Set the corresponding bits in `struct poly_synth_t`:\n  * set the corresponding `enable` bit to compute the output of that synth\n    voice channel\n  * clear the corresponding `mute` bit for the output of that synth channel to\n    be added to the resultant output.\n\nThen call `poly_synth_next` repeatedly to read off each sample.  The samples\nare returned as signed 8-bit PCM.  Each call will advance the state machines\nand so successive calls will return consecutive samples.\n\nAs each channel finishes, the corresponding bit in the `enable` member of\n`struct poly_synth_t` is cleared.\n\nWhen all the machines have finished, the `poly_synth_next` function will\nreturn all zeros and the `enable` field of `struct poly_synth_t` will be zero.\n\n### Waveform generators\n\nThere are 5 waveform generator algorithms to choose from.  The state machines\nhave the following variables:\n\n* `sample`: The latest waveform generator sample.\n* `amplitude`: The peak waveform amplitude (from 0 axis, so half.\n  peak-to-peak).\n* `period`: The period of the internal state machine counter\n* `period_remain`: The internal state machine counter itself.  This gets set\n  to a value then decremented until it reaches zero.\n* `step`: The amplitude step size.\n\n#### DC waveform generator (`voice_wf_set_dc`)\n\nConfigures the waveform generator to generate a \"DC\" waveform (constant\namplitude).  Not terribly useful at this time but may be handy if you wish to\nuse the ADSR envelope generator only to modulate lights.\n\n#### Square wave generator (`voice_wf_set_square`)\n\nConfigures the waveform generator to generate a square wave.\n\n`sample` is initialised as `+amplitude`, and the half-period is computed as\n`period=SYNTH_FREQ/(2*freq)`. `period_remain` is initialised to `period`.\n\nFixed-point `12.4` format (16 bit) is used to store the period counters, to\nallow tuned notes on low sampling rates too.\n\nEach sample, `period_remain` is decremented.  When `period_remain` reaches\nzero:\n\n* `sample` is algebraically negated\n* `period_remain` is reset back to `period`\n\n#### Sawtooth wave generator (`voice_wf_set_sawtooth`)\n\nConfigures the waveform generator to produce a sawtooth wave.\n\n`sample` is initialised as `-amplitude`, and the time period is computed as\n`period=SYNTH_FREQ/freq`.  The `step` is computed as `step=(2*amplitude)/T`.\n`period_remain` is initialised at `period`.\n\nEvery sample, `sample` is incremented by `step` and `period_remain`\ndecremented.  When `period_remain` reaches zero:\n\n* `sample` is reset to `-amplitude`\n* `period_remain` reset to `period`\n\n#### Triangle wave generator (`voice_wf_set_triangle`)\n\nConfigures the waveform generator to produce a triangle wave.\n\n`sample` is initialised as `-amplitude`, and the time period is computed as\n`period=SYNTH_FREQ/(2*freq)`.  The `step` is computed as\n`step=(2*amplitude)/T`.  `period_remain` is initialised at `period`.\n\nEvery sample, `sample` is incremented by `step` and `period_remain`\ndecremented.  When `period_remain` reaches zero:\n\n* if `step` is negative, `sample` is reset to `-amplitude`, otherwise it is\n  reset to `+amplitude`.\n* `step` is algebraically negated.\n* `period_remain` is reset to `period`\n\n#### Pseudorandom noise generator (`wf_voice_set_noise`)\n\nThis generates random samples at a given amplitude.  The randomness depends on\nthe C library's random number generator (`rand()`), so it may help to\nperiodically seed it, perhaps by taking the least-significant bits of ADC\nreadings and feeding those into `srand` to give it some true randomness.\n\n## Sequencer\n\nSince 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. \n\nThis allow polyphonic tunes to be \"pre-compiled\" and stored in small binary files, or microcontroller EEPROM, and to be accessed in serial fashion.\n\nEach 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.\n\nIn 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...).\n\nThis 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.\n\nTo save memory for the tiniest 8-bit microcontrollers, the sequencer stream header and the steps are defined in a compact 8-bit binary format:\n\n```\n// A frame\nstruct seq_frame_t {\n    /*! Envelope definition */\n    struct adsr_env_def_t adsr_def;\n    /*! Waveform definition */\n    struct voice_wf_def_t waveform_def;\n};\n```\n\nwhere `adsr_env_def_t` is the argument for the `adsr_config`, and `voice_wf_def_t` is the minimum set of arguments to initialize a waveform.\n\nIn order to save computational-demanding 16-bit division operations on 8-bit targets, the waveform frequency in the definition is expressed as waveform period instead of frequency in Hz, to allow faster play at runtime.\n\nThis requires the sequencer compiler to known in advance the target sampling rate.\n\nFor this reason, a stream header contains the information to avoid issues during reproduction:\n\n```\nstruct seq_stream_header_t {\n    /*! Sampling frequency required for correct timing */\n    uint16_t synth_frequency;\n    /*! Size of a single frame in bytes */\n    uint8_t frame_size;\n    /*! Number of voices */\n    uint8_t voices;\n    /*! Total frame count */\n    uint16_t frames;\n    /*! Follow frames data, as stream of seq_frame_t */\n};\n```\n\nThe `frame_size` field is useful when the code in the target microcontroller is compiled with different setting (e.g. different time scale, or different set of features that requires less data, like no Attack/Decay, etc...).\n\n### Typical usage\n\nThe sequencer can be fed via a callback, in order to support serial read for example from serial EEPROM or streams.\n\n```c\n/*! Requires a new frame. The handler must return 1 if a new frame was acquired, or zero if EOF */\nvoid seq_set_stream_require_handler(uint8_t (*handler)(struct seq_frame_t* frame));\n\n/*! \n * Plays a stream sequence of frames, in the order requested by the synth.\n * The frames must then be sorted in the same fetch order and not in channel order.\n */\nint seq_play_stream(const struct seq_stream_header_t* stream_header, uint8_t voice_count, struct poly_synth_t* synth);\n\n/*! Use it when `seq_play_stream` is in use, one call per sample */\nvoid seq_feed_synth(struct poly_synth_t* synth);\n```\n\n## MML compiler\n\nA very common language to define tunes in a quasi-human-readable fashion is the [Music Macro Language](https://en.wikipedia.org/wiki/Music_Macro_Language) (MML).\n\nThe project contains an implementation of a MML parser that creates a sequencer stream. In that way, it is possible to 'compile' tunes into binary streams, embed it in the microcontroller and play it from the sequencer stream with the least as computational power as possible.\n\nThe MML dialect implemented supports multi-voice: each voice can be specified on a different line, prefixed with the voice number (from *A* to *Z*).\n\n| command       | meaning  |\n| ------------- |-------------|\n| `cdefgab` | The letters `a` to `g` correspond to the musical pitches and cause the corresponding note to be played. Sharp notes are produced by appending a `+` or `#`, and flat notes by appending a `-`. The length of a note can be specified by appending a number representing its length (see `l` command). One or more dots `.` can be added to increase the length of 3/2. |\n| `p` or `r` | A pause or rest. Like the notes, it is possible to specify the length appending a number and/or dots. | \n| `n`\\\u003cn\u003e | Plays a *note code*, between 0 and 84. `0` is the C at octave 0, `33` is A at octave 2 (440Hz), etc... | \n| `o`\\\u003cn\\\u003e | Specify the octave the instrument will play in (from 0 to 6). The default octave is 2 (corresponding to the fourth-octave in scientific pitch).\n| `\u003c`, `\u003e` | Used to step up or down one octave.\n| `l`\\\u003cn\\\u003e | Specify the default length used by notes or rests which do not explicitly define one. `4` means 1/4, `16` means 1/16 etc... One or more dots `.` can be added to increase the length of 3/2.\n| `v`\\\u003cn\\\u003e | Sets the volume of the instruments. It will set the current waveform amplitude (127 being the maximum modulation).\n| `t`\\\u003cn\\\u003e | Sets the tempo in beats per minute.\n| `mn`, `ml`, `ms` | Sets the articulation for the current instrument. Stands for *music normal* (note plays for 7/8 of the length), *music legato* (note plays full length) and *music staccato* (note plays 3/4 of length). This is implemented using the *decay* of ADSR modulation.\n| `ws`, `ww`, `wt` (*) | Sets the square waveform, sawtooth waveform or triangle waveform for the current instrument.\n| `\\|` | The pipe character, used in music sheet notation to help aligning different channel, is ignored.\n| `#`, `;` | Characters to denote comment lines: it will skip the rest of the line.\n| `A-Z` (*) | Sets the active voice for the current MML line. Multiple characters can be specified: in that case all the selected voices will receive the MML commands until the end of the line.\n\n(*) custom MML dialect.\n\nThe MML 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 the data to create a binary stream for the sequencer. The typical usage is a compiler for PC.\n\n### Typical usage\n\nThe MML file should be loaded entirely in memory to be compiled. \n\n```c\n// Set the error handler in order to show errors and line/col counts\nmml_set_error_handler(stderr_err_handler);\nstruct seq_frame_map_t map;\n// Parse the MML file and produce sequencer frames as stream.\nif (mml_compile(mml_content, \u0026map)) {\n    // Error\n}\n// Compile the channel data map in a stream\nstruct seq_frame_t* frame_stream;\nint frame_count;\nint voice_count;\nseq_compile(\u0026map, \u0026frame_stream, \u0026frame_count, \u0026voice_count);\n\n// Save the frame stream...\n\n// Free memory\nmml_free(map);\nseq_free(frame_stream);\n```\n\nPorts\n-----\n\nThe code is written with portability in mind.  While it has only ever been\ncompiled on GNU/Linux platforms, it has successfully worked on AVR and\nLinux/x86-64.  The code *should* compile and work for other processor\narchitectures too.\n\nTo build a port, run:\n\n```\n$ make PORT=port_name\n```\n\n### ATTiny85 port (`attiny85`)\n\nThe ATTiny85 port was the first microcontroller port of this synthesizer\nlibrary.  The demonstration port tries to operate as a stand-alone\nsynthesizer.  The PWM output is emitted out of `PB4` and `PB3` is used as an\nADC input.\n\nConnected to `PB3` is a voltage divider network, with the segments connected\nto Vcc via pushbuttons.  When a button is pressed, it shorts a section of the\nresistor divider out, and a higher voltage is seen on the ADC input.\n\nThe voltage is translated to a button press and used to trigger one of the\nvoices, each of which have been configured with a different note.\n\nThe code is a work-in-progress, with some notable bugginess with the ADC-based\nkeyboard implementation.\n\nThe remaining pins are available for other uses such as I²C/SPI or for\nadditional GPIOs.\n\n### ATTiny861 port (`attiny861`)\n\nThis code is forked from the ATTiny85 port when it was realised that the\nATTiny85 with its 5 usable I/O pins would be incapable of driving lots of\nlights without a lot of I/O expansion hardware.\n\nHere, four I/O pins on port B are allocated:\n\n* `PB3`: PWM audio output\n* `PB4`: audio enable pin\n* `PB5`: PWM light output\n* `PB6`: GPIO enable pin\n\nThe audio amplifier used in the prototype is the NJR NJM2113D, and features a\n\"chip disable\" pin that powers down the amplifier when pulled high.  The audio\nenable signal drives a N-channel MOSFET (e.g. 2N7000) that pulls the pin low\nagainst a pull-up resistor to +12V.\n\nA logic high on the audio enable signal turns on the amplifier.\n\nAll of the pins on port A (`PA0` through to `PA7`) are used for interfacing to\nMOSFETs and pushbuttons by way of a multiplexing circuit driven by the GPIO\nenable pin.  The multiplexing circuit consists of two 4066-style analogue\nswitches (I am using Motorola MC14066Bs here because I have dozens of them\nwith 8241 date codes) and a 74374 D-latch.\n\nThe GPIO enable line connects the clock input of the 74374 and the switch\nenable pins on all switches in both 4066s.  The 4066 and 74374 inputs are\nparalleled.\n\nWhen GPIO enable is driven low, this turns off the 4066s allowing us to assert\nsignals for the 74374.  On the rising edge of GPIO enable, the 74374 latches\nthose signals and the 4066s re-connect port A to the outside world.  By doing\nthis, port A is able to be used both for control of digital outputs hanging\noff the 74374 and for analogue + digital I/O through the 4066s.\n\nThe light PWM signal on `PB5` connects to the 74374's \"output enable\" line,\nand thus by using pull-down resistors on the outputs of the 74374, it can\ndrive N-channel MOSFETs to control the brightness of 8 lights.\n\nIndividual control of lights can be achieved with ⅛ maximum duty cycle by\nchoosing a single output then driving that line with the desired PWM\namplitude.\n\n### PC port (`pc`)\n\nThis uses `libao` and a command line interface to simulate the output of the\nsynthesizer and to output a `.wav` file.  It was used to debug the\nsynthesizer.\n\nThe synthesizer commands are given as command-line arguments:\n\n* `voice V` selects a given voice channel\n* `mute M` sets the synthesizer `mute` bit-mask\n* `en M` sets the synthesizer `enable` bit-mask\n* `dc A` sets the selected voice channel to produce a DC offset of amplitude\n  `A`.\n* `noise A` sets the selected voice channel to produce pseudorandom noise at\n  amplitude `A`.\n* `square F A` sets the selected voice channel to produce a square wave of\n  frequency `F` Hz and amplitude `A`.\n* `sawtooth F A` sets the selected voice channel to produce a sawtooth wave of\n  frequency `F` Hz and amplitude `A`.\n* `triangle F A` sets the selected voice channel to produce a triangle wave of\n  frequency `F` Hz and amplitude `A`.\n* `scale N` sets the ADSR time unit scale for the selected channel to `N`\n  samples per \"tick\"\n* `delay N` sets the ADSR delay period for the selected channel to `N` \"ticks\"\n* `attack N` sets the ADSR attack period for the selected channel to `N`\n  \"ticks\"\n* `decay N` sets the ADSR decay period for the selected channel to `N` \"ticks\"\n* `sustain N` sets the ADSR sustain period for the selected channel to `N`\n  \"ticks\"\n* `release N` sets the ADSR release period for the selected channel to `N`\n  \"ticks\"\n* `peak A` sets the peak ADSR amplitude for the selected channel to `A`\n* `samp A` sets the sustained ADSR amplitude for the selected channel to `A`\n* `reset` resets the ADSR state machine for the selected channel.\n\nOr, alternatively, you can pass all the above commands stored in a text file:\n* `-- NAME` loads and run the script, and skip all the remaining arguments.\n\nOnce the `enable` bit-mask is set, the program loops, playing sound via\n`libao` and writing the samples to `out.wav` for later analysis until all bits\nin the `enable` bit-mask are cleared by the ADSR state machines.\n\nWhen the program runs out of command line arguments, or the script ends, it\nexits.\n\nIn addition, the PC port can be used to compile MML tunes to the sequencer\nbinary format:\n\n* `compile-mml FILE.mml` compiles the .mml file and produces a `sequencer.bin`\noutput\n\nand to play sequencer files as well:\n\n* `sequencer FILE.bin` loads and plays the sequencer binary file passed as\ninput.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsjlongland%2Fatinysynth","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsjlongland%2Fatinysynth","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsjlongland%2Fatinysynth/lists"}