You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
- Added sequencer from binary 'waveform' and ADSR data.
- Added waveform generic API to create waveforms, and use period instead of frequency in waveform (avoid division on MCUs)
- Introduced 'def' struct in ADSR API for reuse
- Multi-track MML parser and compilation
- Added some samples from https://electronicmusic.fandom.com/wiki/Music_Macro_Language
- Added scale.mml for testing
Copy file name to clipboardexpand all lines: README.md
+128
Original file line number
Diff line number
Diff line change
@@ -204,6 +204,125 @@ help to periodically seed it, perhaps by taking the least-significant
204
204
bits of ADC readings and feeding those into `srand` to give it some true
205
205
randomness.
206
206
207
+
## Sequencer
208
+
209
+
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.
210
+
211
+
This allow polyphonic tunes to be "pre-compiled" and stored in small binary files, or microcontroller EEPROM, and to be accessed in serial fashion.
212
+
213
+
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.
214
+
215
+
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...).
216
+
217
+
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.
218
+
219
+
To save memory for the tiniest 8-bit microcontrollers, the sequencer stream header and the steps are defined in a compact 8-bit binary format:
220
+
221
+
```
222
+
// A frame
223
+
struct seq_frame_t {
224
+
/*! Envelope definition */
225
+
struct adsr_env_def_t adsr_def;
226
+
/*! Waveform definition */
227
+
struct voice_wf_def_t waveform_def;
228
+
};
229
+
```
230
+
231
+
where `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.
232
+
233
+
In 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.
234
+
235
+
This requires the sequencer compiler to known in advance the target sampling rate.
236
+
237
+
For this reason, a stream header contains the information to avoid issues during reproduction:
238
+
239
+
```
240
+
struct seq_stream_header_t {
241
+
/*! Sampling frequency required for correct timing */
242
+
uint16_t synth_frequency;
243
+
/*! Size of a single frame in bytes */
244
+
uint8_t frame_size;
245
+
/*! Number of voices */
246
+
uint8_t voices;
247
+
/*! Total frame count */
248
+
uint16_t frames;
249
+
/*! Follow frames data, as stream of seq_frame_t */
250
+
};
251
+
```
252
+
253
+
The `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...).
254
+
255
+
### Typical usage
256
+
257
+
The sequencer can be fed via a callback, in order to support serial read for example from serial EEPROM or streams.
258
+
259
+
```c
260
+
/*! Requires a new frame. The handler must return 1 if a new frame was acquired, or zero if EOF */
* Plays a stream sequence of frames, in the order requested by the synth.
265
+
* The frames must then be sorted in the same fetch order and not in channel order.
266
+
*/
267
+
int seq_play_stream(const struct seq_stream_header_t* stream_header, uint8_t voice_count, struct poly_synth_t* synth);
268
+
269
+
/*! Use it when `seq_play_stream` is in use, one call per sample */
270
+
void seq_feed_synth(struct poly_synth_t* synth);
271
+
```
272
+
273
+
## MML compiler
274
+
275
+
A 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).
276
+
277
+
The 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.
278
+
279
+
The MML dialect implemented supports multi-voice: each voice can be specified on a different line, prefixed with the voice number (from *A* to *Z*).
280
+
281
+
| command | meaning |
282
+
| ------------- |-------------|
283
+
| `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. |
284
+
| `p` or `r` | A pause or rest. Like the notes, it is possible to specify the length appending a number and/or dots. |
285
+
| `n`\<n> | Plays a *note code*, between 0 and 84. `0` is the C at octave 0, `33` is A at octave 2 (440Hz), etc... |
286
+
| `o`\<n\> | 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).
287
+
| `<`, `>` | Used to step up or down one octave.
288
+
| `l`\<n\> | 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.
289
+
| `v`\<n\> | Sets the volume of the instruments. It will set the current waveform amplitude (127 being the maximum modulation).
290
+
| `t`\<n\> | Sets the tempo in beats per minute.
291
+
| `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.
292
+
| `ws`, `ww`, `wt` (*) | Sets the square waveform, sawtooth waveform or triangle waveform for the current instrument.
293
+
| `\|` | The pipe character, used in music sheet notation to help aligning different channel, is ignored.
294
+
| `#`, `;` | Characters to denote comment lines: it will skip the rest of the line.
295
+
| `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.
296
+
297
+
(*) custom MML dialect.
298
+
299
+
The 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.
300
+
301
+
### Typical usage
302
+
303
+
The MML file should be loaded entirely in memory to be compiled.
304
+
305
+
```c
306
+
// Set the error handler in order to show errors and line/col counts
307
+
mml_set_error_handler(stderr_err_handler);
308
+
struct seq_frame_map_t map;
309
+
// Parse the MML file and produce sequencer frames as stream.
0 commit comments