1
0
mirror of https://github.com/sjlongland/atinysynth.git synced 2025-09-13 10:03:15 +10:00

- 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
This commit is contained in:
Luciano Martorella 2021-05-11 18:05:06 +02:00
parent 1ed955f9b7
commit c19c6dddcb
19 changed files with 1297 additions and 85 deletions

1
.gitignore vendored
View File

@ -7,3 +7,4 @@ sine.c
obj
bin
out.wav
sequencer.bin

View File

@ -24,7 +24,7 @@ $(BINDIR)/synth: $(OBJECTS) $(OBJDIR)/poly.a
@[ -d $(BINDIR) ] || mkdir -p $(BINDIR)
$(CC) -g -o $@ $(LDFLAGS) $(LIBS) $^
$(OBJDIR)/poly.a: $(OBJDIR)/adsr.o $(OBJDIR)/waveform.o
$(OBJDIR)/poly.a: $(OBJDIR)/adsr.o $(OBJDIR)/waveform.o $(OBJDIR)/mml.o $(OBJDIR)/sequencer.o
$(AR) rcs $@ $^
$(OBJDIR)/%.o: $(SRCDIR)/%.c

131
README.md
View File

@ -200,6 +200,125 @@ 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 compact 8-bit binary format:
```
// A frame
struct seq_frame_t {
/*! Envelope definition */
struct adsr_env_def_t adsr_def;
/*! Waveform definition */
struct voice_wf_def_t waveform_def;
};
```
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.
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.
This requires the sequencer compiler to known in advance the target sampling rate.
For this reason, a stream header contains the information to avoid issues during reproduction:
```
struct seq_stream_header_t {
/*! Sampling frequency required for correct timing */
uint16_t synth_frequency;
/*! Size of a single frame in bytes */
uint8_t frame_size;
/*! Number of voices */
uint8_t voices;
/*! Total frame count */
uint16_t frames;
/*! Follow frames data, as stream of seq_frame_t */
};
```
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...).
### Typical usage
The sequencer can be fed via a callback, in order to support serial read for example from serial EEPROM or streams.
```c
/*! Requires a new frame. The handler must return 1 if a new frame was acquired, or zero if EOF */
void seq_set_stream_require_handler(uint8_t (*handler)(struct seq_frame_t* frame));
/*!
* Plays a stream sequence of frames, in the order requested by the synth.
* The frames must then be sorted in the same fetch order and not in channel order.
*/
int seq_play_stream(const struct seq_stream_header_t* stream_header, uint8_t voice_count, struct poly_synth_t* synth);
/*! Use it when `seq_play_stream` is in use, one call per sample */
void seq_feed_synth(struct poly_synth_t* synth);
```
## MML compiler
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).
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.
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*).
| command | meaning |
| ------------- |-------------|
| `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. |
| `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> | Plays a *note code*, between 0 and 84. `0` is the C at octave 0, `33` is A at octave 2 (440Hz), etc... |
| `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).
| `<`, `>` | Used to step up or down one octave.
| `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.
| `v`\<n\> | Sets the volume of the instruments. It will set the current waveform amplitude (127 being the maximum modulation).
| `t`\<n\> | Sets the tempo in beats per minute.
| `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.
| `ws`, `ww`, `wt` (*) | Sets the square waveform, sawtooth waveform or triangle waveform for the current instrument.
| `\|` | The pipe character, used in music sheet notation to help aligning different channel, is ignored.
| `#`, `;` | Characters to denote comment lines: it will skip the rest of the line.
| `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.
(*) custom MML dialect.
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.
### Typical usage
The MML file should be loaded entirely in memory to be compiled.
```c
// Set the error handler in order to show errors and line/col counts
mml_set_error_handler(stderr_err_handler);
struct seq_frame_map_t map;
// Parse the MML file and produce sequencer frames as stream.
if (mml_compile(mml_content, &map)) {
// Error
}
// Compile the channel data map in a stream
struct seq_frame_t* frame_stream;
int frame_count;
int voice_count;
seq_compile(&map, &frame_stream, &frame_count, &voice_count);
// Save the frame stream...
// Free memory
mml_free(map);
seq_free(frame_stream);
```
Ports
-----
@ -322,3 +441,15 @@ in the `enable` bit-mask are cleared by the ADSR state machines.
When the program runs out of command line arguments, or the script ends, it
exits.
In addition, the PC port can be used to compile MML tunes to the sequencer
binary format:
* `compile-mml FILE.mml` compiles the .mml file and produces a `sequencer.bin`
output
and to play sequencer files as well:
* `sequencer FILE.bin` loads and plays the sequencer binary file passed as
input.

56
adsr.c
View File

@ -95,23 +95,23 @@ uint8_t adsr_next(struct adsr_env_gen_t* const adsr) {
adsr->sustain_amp);
/* Are registers set up? */
if (!adsr->time_scale)
if (!adsr->def.time_scale)
return 0;
_DPRINTF("adsr=%p time scale set\n", adsr);
if (!(adsr->delay_time || adsr->attack_time
|| adsr->decay_time
|| adsr->sustain_time
|| adsr->release_time))
if (!(adsr->def.delay_time || adsr->def.attack_time
|| adsr->def.decay_time
|| adsr->def.sustain_time
|| adsr->def.release_time))
return 0;
_DPRINTF("adsr=%p envelope timings set\n", adsr);
if (!(adsr->peak_amp || adsr->sustain_amp))
if (!(adsr->def.peak_amp || adsr->def.sustain_amp))
return 0;
_DPRINTF("adsr=%p envelope amplitudes set\n", adsr);
/* All good */
if (adsr->delay_time)
if (adsr->def.delay_time)
adsr->state = ADSR_STATE_DELAY_INIT;
else
adsr->state = ADSR_STATE_DELAY_EXPIRE;
@ -123,7 +123,7 @@ uint8_t adsr_next(struct adsr_env_gen_t* const adsr) {
/* Setting up a delay */
adsr->amplitude = 0;
adsr->next_event = adsr_num_samples(
adsr->time_scale, adsr->delay_time);
adsr->def.time_scale, adsr->def.delay_time);
adsr->state = ADSR_STATE_DELAY_EXPIRE;
/* Wait for delay */
return adsr->amplitude;
@ -133,7 +133,7 @@ uint8_t adsr_next(struct adsr_env_gen_t* const adsr) {
_DPRINTF("adsr=%p DELAY EXPIRE\n", adsr);
/* Delay has expired */
if (adsr->attack_time)
if (adsr->def.attack_time)
adsr->state = ADSR_STATE_ATTACK_INIT;
else
adsr->state = ADSR_STATE_ATTACK_EXPIRE;
@ -141,8 +141,8 @@ uint8_t adsr_next(struct adsr_env_gen_t* const adsr) {
if (adsr->state == ADSR_STATE_ATTACK_INIT) {
/* Attack is divided into 16 segments */
adsr->time_step = (uint16_t)((adsr->attack_time
* adsr->time_scale) >> 4);
adsr->time_step = (uint16_t)((adsr->def.attack_time
* adsr->def.time_scale) >> 4);
adsr->counter = 16;
adsr->next_event = adsr->time_step;
adsr->state = ADSR_STATE_ATTACK;
@ -158,9 +158,9 @@ uint8_t adsr_next(struct adsr_env_gen_t* const adsr) {
if (adsr->counter) {
/* Change of amplitude */
uint16_t lin_amp = (16-adsr->counter)
* adsr->peak_amp;
* adsr->def.peak_amp;
uint16_t exp_amp = adsr_attack_amp(
adsr->peak_amp, adsr->counter);
adsr->def.peak_amp, adsr->counter);
lin_amp >>= ADSR_LIN_AMP_FACTOR;
_DPRINTF("adsr=%p ATTACK lin=%d exp=%d\n",
adsr, lin_amp, exp_amp);
@ -177,7 +177,7 @@ uint8_t adsr_next(struct adsr_env_gen_t* const adsr) {
if (adsr->state == ADSR_STATE_ATTACK_EXPIRE) {
_DPRINTF("adsr=%p ATTACK EXPIRE\n", adsr);
if (adsr->decay_time)
if (adsr->def.decay_time)
adsr->state = ADSR_STATE_DECAY_INIT;
else
adsr->state = ADSR_STATE_DECAY_EXPIRE;
@ -187,10 +187,10 @@ uint8_t adsr_next(struct adsr_env_gen_t* const adsr) {
_DPRINTF("adsr=%p DECAY INIT\n", adsr);
/* We should be at full amplitude */
adsr->amplitude = adsr->peak_amp;
adsr->amplitude = adsr->def.peak_amp;
adsr->time_step = (uint16_t)((adsr->decay_time
* adsr->time_scale) >> 4);
adsr->time_step = (uint16_t)((adsr->def.decay_time
* adsr->def.time_scale) >> 4);
adsr->counter = 16;
adsr->next_event = adsr->time_step;
adsr->state = ADSR_STATE_DECAY;
@ -201,12 +201,12 @@ uint8_t adsr_next(struct adsr_env_gen_t* const adsr) {
if (adsr->counter) {
/* Linear decrease in amplitude */
uint16_t delta = adsr->peak_amp
- adsr->sustain_amp;
uint16_t delta = adsr->def.peak_amp
- adsr->def.sustain_amp;
delta *= adsr->counter;
delta >>= 4;
adsr->amplitude = adsr->sustain_amp + delta;
adsr->amplitude = adsr->def.sustain_amp + delta;
adsr->next_event = adsr->time_step;
adsr->counter--;
} else {
@ -217,7 +217,7 @@ uint8_t adsr_next(struct adsr_env_gen_t* const adsr) {
if (adsr->state == ADSR_STATE_DECAY_EXPIRE) {
_DPRINTF("adsr=%p DECAY EXPIRE\n", adsr);
if (adsr->sustain_time)
if (adsr->def.sustain_time)
adsr->state = ADSR_STATE_SUSTAIN_INIT;
else
adsr->state = ADSR_STATE_SUSTAIN_EXPIRE;
@ -226,9 +226,9 @@ uint8_t adsr_next(struct adsr_env_gen_t* const adsr) {
if (adsr->state == ADSR_STATE_SUSTAIN_INIT) {
_DPRINTF("adsr=%p SUSTAIN INIT\n", adsr);
adsr->amplitude = adsr->sustain_amp;
adsr->amplitude = adsr->def.sustain_amp;
adsr->next_event = adsr_num_samples(
adsr->time_scale, adsr->sustain_time);
adsr->def.time_scale, adsr->def.sustain_time);
adsr->state = ADSR_STATE_SUSTAIN_EXPIRE;
/* Wait for delay */
return adsr->amplitude;
@ -237,7 +237,7 @@ uint8_t adsr_next(struct adsr_env_gen_t* const adsr) {
if (adsr->state == ADSR_STATE_SUSTAIN_EXPIRE) {
_DPRINTF("adsr=%p SUSTAIN EXPIRE\n", adsr);
if (adsr->release_time)
if (adsr->def.release_time)
adsr->state = ADSR_STATE_RELEASE_INIT;
else
adsr->state = ADSR_STATE_RELEASE_EXPIRE;
@ -246,8 +246,8 @@ uint8_t adsr_next(struct adsr_env_gen_t* const adsr) {
if (adsr->state == ADSR_STATE_RELEASE_INIT) {
_DPRINTF("adsr=%p RELEASE INIT\n", adsr);
adsr->time_step = (uint16_t)((adsr->release_time
* adsr->time_scale) >> 4);
adsr->time_step = (uint16_t)((adsr->def.release_time
* adsr->def.time_scale) >> 4);
adsr->counter = 16;
adsr->next_event = adsr->time_step;
adsr->state = ADSR_STATE_RELEASE;
@ -259,9 +259,9 @@ uint8_t adsr_next(struct adsr_env_gen_t* const adsr) {
if (adsr->counter) {
/* Change of amplitude */
uint16_t lin_amp = adsr->counter
* adsr->sustain_amp;
* adsr->def.sustain_amp;
uint16_t exp_amp = adsr_release_amp(
adsr->sustain_amp, adsr->counter);
adsr->def.sustain_amp, adsr->counter);
lin_amp >>= ADSR_LIN_AMP_FACTOR;
_DPRINTF("adsr=%p RELEASE lin=%d exp=%d\n",
adsr, lin_amp, exp_amp);

41
adsr.h
View File

@ -47,13 +47,9 @@
#define ADSR_INFINITE UINT8_MAX
/*!
* ADSR Envelope Generator data. 20 bytes.
* ADSR Envelope Generator definition. 11 bytes.
*/
struct adsr_env_gen_t {
/*! Time to next event, samples. UINT32_MAX = infinite */
uint32_t next_event;
/*! Time step, samples */
uint16_t time_step;
struct adsr_env_def_t {
/*! Time scale, samples per unit */
uint32_t time_scale;
/*! Delay period, time units. UINT8_MAX = infinite */
@ -70,12 +66,24 @@ struct adsr_env_gen_t {
uint8_t peak_amp;
/*! Sustain amplitude */
uint8_t sustain_amp;
/*! Present amplitude */
uint8_t amplitude;
};
/*!
* ADSR Envelope Generator data. 20 bytes.
*/
struct adsr_env_gen_t {
/*! Definition */
struct adsr_env_def_t def;
/*! Time to next event, samples. UINT32_MAX = infinite */
uint32_t next_event;
/*! Time step, samples */
uint16_t time_step;
/*! ADSR state */
uint8_t state;
/*! ADSR counter */
uint8_t counter;
/*! Present amplitude */
uint8_t amplitude;
};
/*!
@ -105,21 +113,8 @@ static inline void adsr_reset(struct adsr_env_gen_t* const adsr) {
/*!
* Configure the ADSR.
*/
static inline void adsr_config(struct adsr_env_gen_t* const adsr,
uint32_t time_scale, uint8_t delay_time,
uint8_t attack_time, uint8_t decay_time,
uint8_t sustain_time, uint8_t release_time,
uint8_t peak_amp, uint8_t sustain_amp) {
adsr->time_scale = time_scale;
adsr->delay_time = delay_time;
adsr->attack_time = attack_time;
adsr->decay_time = decay_time;
adsr->sustain_time = sustain_time;
adsr->release_time = release_time;
adsr->peak_amp = peak_amp;
adsr->sustain_amp = sustain_amp;
static inline void adsr_config(struct adsr_env_gen_t* const adsr, struct adsr_env_def_t* const def) {
adsr->def = *def;
adsr_reset(adsr);
}

471
mml.c Normal file
View File

@ -0,0 +1,471 @@
/*!
* Polyphonic synthesizer for microcontrollers. MML compiler.
* (C) 2021 Luciano Martorella
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
#include "mml.h"
#include "synth.h"
#include "sequencer.h"
#include "debug.h"
#include <stdlib.h>
#include <string.h>
#include <math.h>
/*!
* Not optimized for microcontroller usage.
* Requires dynamic memory allocation support (heap), especially `malloc` and `realloc`.
*/
/*! Manage parser errors */
static void (*error_handler)(const char* err, int line, int column);
static int line = 1;
static int pos = 1;
#define ARTICULATION_STACCATO (3.0 / 4.0)
#define ARTICULATION_NORMAL (7.0 / 8.0)
#define ARTICULATION_LEGATO (1.0)
/*! Temporary list of sequencer stream frames, per channel */
static struct seq_frame_map_t frame_map;
static void init_stream_channel(int channel) {
// Init new channels
frame_map.channels[channel].count = 0;
frame_map.channels[channel].frames = malloc(sizeof(struct seq_frame_t) * 16);
}
static void add_channel_frame(int channel, int frequency, int duration, int volume, double articulation, int waveform) {
if (channel >= frame_map.channel_count) {
int old_count = frame_map.channel_count;
frame_map.channel_count = channel + 1;
frame_map.channels = realloc(frame_map.channels, sizeof(struct seq_frame_list_t) * frame_map.channel_count);
for (int i = old_count; i < frame_map.channel_count; i++) {
// Init new channels
init_stream_channel(i);
}
}
if (frame_map.channels[channel].count > 0 && (frame_map.channels[channel].count % 16) == 0) {
frame_map.channels[channel].frames = realloc(frame_map.channels[channel].frames, sizeof(struct seq_frame_t) * (frame_map.channels[channel].count + 16));
}
struct seq_frame_t* p = &frame_map.channels[channel].frames[frame_map.channels[channel].count++];
if (!frequency) {
p->waveform_def.mode = VOICE_MODE_DC;
} else {
p->waveform_def.mode = VOICE_MODE_SQUARE;
p->waveform_def.period = voice_wf_freq_to_period(frequency);
p->waveform_def.amplitude = volume;
p->waveform_def.mode = waveform;
}
// Init voice, simple square without envelope
p->adsr_def.delay_time = 0;
p->adsr_def.attack_time = 12;
p->adsr_def.decay_time = 12;
p->adsr_def.peak_amp = 63;
p->adsr_def.sustain_amp = 40;
// Calc duration and scale: TODO better scale algo
int scale = duration / 128;
p->adsr_def.time_scale = scale;
p->adsr_def.release_time = 128 * (1.0 - articulation);
p->adsr_def.sustain_time = 128 - (p->adsr_def.delay_time + p->adsr_def.attack_time + p->adsr_def.decay_time + p->adsr_def.release_time);
}
void mml_set_error_handler(void (*handler)(const char* err, int line, int column)) {
error_handler = handler;
}
/*! Read a single digit from the stream and advance */
static uint8_t read_digit(const char** str, int* pos) {
const char code = **str;
*str += 1;
*pos += 1;
if (code < '0' || code > '9') {
return 255;
} else {
return code - '0';
}
}
/*! Read a number from the stream and advance */
static int read_number(const char** str, int* pos) {
char* end;
int ret = strtol(*str, &end, 10);
if (!ret || end == *str) {
return -1;
}
*pos += (end - *str);
*str = end;
return ret;
}
/*! Convert a node 0-84 to frequency. 0 is "C" at octave 0, so octave 2 (fourth-octave in scientific pitch) c2 = note 24, and a2 (Helmholtz 440Hz) = note 33 */
static int get_freq_from_code(int noteCode) {
return (int)(440.0 * pow(2, ((noteCode - 33) / 12.0)));
}
/*! Convert a a-g code chromatic scale to frequency. Octave 2 is the fourth-octave in scientific pitch */
static int get_freq_from_note(char note, int sharp, int octave) {
int semitone = ((note - 'a' + 5) % 7) * 2;
if (semitone > 4) {
semitone--;
}
if (sharp) {
semitone++;
}
// semitone is 0 for c
return get_freq_from_code(semitone + octave * 12);
}
/*! Get duration in samples. Tempo is in numbers of quartes per minute. Length is fraction of whole note. Dots are number of dots (1 dot = 3/2, 2 dots = 9/4, etc..) */
static int get_duration(int tempo, int length, int dots) {
double l = length;
for (; dots > 0; dots--) {
l /= 1.5;
}
return (int)(synth_freq * 60.0 * 4 / tempo / l);
}
/*! Parser state, per channel */
struct mml_channel_state_t {
uint8_t octave;
int defaultLength;
int defaultLengthDot;
int tempo;
int volume;
double articulation;
int waveform;
// Active in current MML parsing line
int isActive;
};
static struct mml_channel_state_t* mml_channel_states;
static int mml_channel_count;
static void enable_channel(int channel) {
if (channel >= mml_channel_count) {
mml_channel_count = channel + 1;
mml_channel_states = realloc(mml_channel_states, sizeof(struct mml_channel_state_t) * mml_channel_count);
// Init new channel
mml_channel_states[channel].octave = 4;
mml_channel_states[channel].defaultLength = 4;
mml_channel_states[channel].defaultLengthDot = 0;
mml_channel_states[channel].tempo = 120;
mml_channel_states[channel].volume = 63;
mml_channel_states[channel].articulation = ARTICULATION_NORMAL;
mml_channel_states[channel].waveform = VOICE_MODE_SQUARE;
}
mml_channel_states[channel].isActive = 1;
}
// By default, if no channel identifier at the beginning of a MML line, it is referring to A channel only
static void reset_active_state() {
for (int i = 1; i < mml_channel_count; i++) {
mml_channel_states[i].isActive = 0;
}
enable_channel(0);
}
/*!
* Parse the MML file and produce sequencer stream of frames in `stream_channel` array.
*/
static int mml_parse(const char* content) {
line = 1;
pos = 0;
// Starts with 1 voice
mml_channel_states = malloc(0);
frame_map.channels = malloc(0);
frame_map.channel_count = 0;
// Read the string until end
reset_active_state();
while(1) {
pos++;
char code = content[0];
content++;
if (!code) {
break;
}
if (code <= 32 || code == '|') {
// Skip blanks and partitures
if (code == '\n') {
line++;
reset_active_state();
pos = 0;
}
if (code == '\r') {
pos--;
}
continue;
}
if (code == '#' || code == ';') {
// Skip line comment
while (*content != '\n') {
content++;
}
content++;
line++;
reset_active_state();
pos = 0;
continue;
}
if (code >= 'A' && code <= 'Z') {
if (pos == 1) {
// Decode active channels
mml_channel_states[0].isActive = 0;
enable_channel(code - 'A');
while (*content >= 'A' && *content <= 'Z') {
enable_channel(*content - 'A');
content++;
pos++;
}
continue;
} else {
error_handler("Misplaced channel selector", line, pos);
}
}
int isPause;
int isNoteCode;
if (code == 'o') {
int octave = read_digit(&content, &pos);
if (octave == 255 || octave > 6) {
error_handler("Invalid octave", line, pos);
return 1;
}
for (int i = 0; i < mml_channel_count; i++) {
if (mml_channel_states[i].isActive) {
mml_channel_states[i].octave = octave;
}
}
} else if (code == 'l') {
int length = read_number(&content, &pos);
if (length < 0) {
error_handler("Invalid length", line, pos);
return 1;
}
int dot = 0;
while (*content == '.') {
dot++;
content++;
pos++;
}
for (int i = 0; i < mml_channel_count; i++) {
if (mml_channel_states[i].isActive) {
mml_channel_states[i].defaultLength = length;
mml_channel_states[i].defaultLengthDot = dot;
}
}
} else if (code == 't') {
int tempo = read_number(&content, &pos);
if (tempo < 0) {
error_handler("Invalid tempo", line, pos);
return 1;
}
for (int i = 0; i < mml_channel_count; i++) {
if (mml_channel_states[i].isActive) {
mml_channel_states[i].tempo = tempo;
}
}
} else if (code == 'v') {
int volume = read_number(&content, &pos);
if (volume < 0 || volume > 128) {
error_handler("Invalid volume", line, pos);
return 1;
}
for (int i = 0; i < mml_channel_count; i++) {
if (mml_channel_states[i].isActive) {
mml_channel_states[i].volume = volume;
}
}
} else if (code == '<') {
for (int i = 0; i < mml_channel_count; i++) {
if (mml_channel_states[i].isActive) {
if (mml_channel_states[i].octave == 0) {
error_handler("Invalid octave step down", line, pos);
return 1;
}
mml_channel_states[i].octave--;
}
}
} else if (code == '>') {
for (int i = 0; i < mml_channel_count; i++) {
if (mml_channel_states[i].isActive) {
if (mml_channel_states[i].octave == 9) {
error_handler("Invalid octave step up", line, pos);
return 1;
}
mml_channel_states[i].octave++;
}
}
} else if (code == 'm') {
// Music articulation
double articulation;
switch (*content) {
case 'l':
articulation = ARTICULATION_LEGATO;
break;
case 'n':
articulation = ARTICULATION_NORMAL;
break;
case 's':
articulation = ARTICULATION_STACCATO;
break;
default:
error_handler("Invalid music articulation", line, pos);
return 1;
}
for (int i = 0; i < mml_channel_count; i++) {
if (mml_channel_states[i].isActive) {
mml_channel_states[i].articulation = articulation;
}
}
pos++;
content++;
} else if (code == 'w') {
// Waveform
int waveform;
switch (*content) {
case 's':
waveform = VOICE_MODE_SQUARE;
break;
case 'w':
waveform = VOICE_MODE_SAWTOOTH;
break;
case 't':
waveform = VOICE_MODE_TRIANGLE;
break;
default:
error_handler("Invalid waveform", line, pos);
return 1;
}
for (int i = 0; i < mml_channel_count; i++) {
if (mml_channel_states[i].isActive) {
mml_channel_states[i].waveform = waveform;
}
}
pos++;
content++;
} else if ((isPause = (code == 'p' || code == 'r')) || (isNoteCode = code == 'n') || (code >= 'a' && code <= 'g')) {
// Note or pause
int length = -1;
int dot = 0;
int sharp = 0;
int customLength = 0;
int noteCode = -1;
while (1) {
char next = content[0];
if (!isPause && !isNoteCode) {
// Sharp/flat?
if (next == '-' || next == '+' || next == '#') {
// variation
if (next == '-') {
code--;
}
if (code == 'e' || code == 'b') {
error_handler("Invalid sharp", line, pos);
return 1;
}
sharp = 1;
content++;
pos++;
continue;
}
}
if (next >= '0' && next <= '9') {
if (isNoteCode) {
if (noteCode != -1) {
error_handler("Invalid note code", line, pos);
return 1;
}
noteCode = read_number(&content, &pos);
if (noteCode < 0 || noteCode > 84) {
error_handler("Invalid note code", line, pos);
return 1;
}
} else {
if (customLength) {
error_handler("Invalid length", line, pos);
return 1;
}
// Length
length = read_number(&content, &pos);
if (length < 0) {
error_handler("Invalid length", line, pos);
return 1;
}
customLength = 1;
}
continue;
}
if (next == '.') {
// Half length
dot++;
content++;
pos++;
continue;
}
break;
}
// Set note
for (int i = 0; i < mml_channel_count; i++) {
if (mml_channel_states[i].isActive) {
if (isNoteCode && noteCode == 0) {
isPause = 1;
}
int frequency = isPause ? 0 : (isNoteCode ? get_freq_from_code(noteCode) : get_freq_from_note(code, sharp, mml_channel_states[i].octave));
int duration = get_duration(mml_channel_states[i].tempo, length < 0 ? mml_channel_states[i].defaultLength : length, (length < 0 && !dot) ? mml_channel_states[i].defaultLengthDot : dot);
add_channel_frame(i, frequency, duration, mml_channel_states[i].volume, mml_channel_states[i].articulation, mml_channel_states[i].waveform);
}
}
} else {
error_handler("Unknown command", line, pos);
return 1;
}
}
free(mml_channel_states);
}
/*!
* Parse the MML file and produce sequencer frames map.
*/
int mml_compile(const char* content, struct seq_frame_map_t* map) {
int ret = mml_parse(content);
if (ret) {
return ret;
}
*map = frame_map;
return 0;
}
void mml_free(struct seq_frame_map_t* map) {
for (int i = 0; i < map->channel_count; i++) {
free(map->channels[i].frames);
}
free(map->channels);
}

44
mml.h Normal file
View File

@ -0,0 +1,44 @@
/*!
* Polyphonic synthesizer for microcontrollers. MML parser.
* (C) 2021 Luciano Martorella
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
#ifndef _MML_H
#define _MML_H
#include "voice.h"
#include "synth.h"
#include "sequencer.h"
/*! Manage parser errors, used to display it in pc ports */
void mml_set_error_handler(void (*handler)(const char* err, int line, int column));
/*!
* Parse the MML file (entirely read and passed to `content`) and produce
* an offline set of frames by channel (frame map).
* The returned set can be transformed in a sequential stream
* by `seq_compile`.
* Returns non-zero in case of parse error.
*/
int mml_compile(const char* content, struct seq_frame_map_t* map);
/*!
* Free the map allocated by `mml_compile`.
*/
void mml_free(struct seq_frame_map_t* map);
#endif

View File

@ -2,8 +2,8 @@ CROSS_COMPILE ?=
CFLAGS ?= -g -Werror -Woverflow
CPPFLAGS ?= -I$(SRCDIR) -I$(PORTDIR)
LDFLAGS ?= -g -lao -Wl,--as-needed
LIBS += -lao
LDFLAGS ?= -g -lao -lm -Wl,--as-needed
LIBS += -lao -lm
INCLUDES += -I$(SRCDIR) -I$(PORTDIR)
OBJECTS += $(OBJDIR)/main.o

View File

@ -20,13 +20,20 @@
#include "synth.h"
#include "debug.h"
#include "sequencer.h"
#include "mml.h"
#include <stdio.h>
#include <string.h>
#include <ao/ao.h>
const uint16_t synth_freq = 32000;
struct voice_ch_t poly_voice[16];
struct poly_synth_t synth;
static struct voice_ch_t poly_voice[16];
static struct poly_synth_t synth;
static int16_t samples[8192];
static uint16_t samples_sz = 0;
static void (*feed_channels)(struct poly_synth_t* synth) = NULL;
static FILE* seq_stream;
static struct seq_stream_header_t seq_stream_header;
/*! Read a script instead of command-line tokens */
static int read_script(const char* name, int* argc, char*** argv) {
@ -52,11 +59,80 @@ static int read_script(const char* name, int* argc, char*** argv) {
return 1;
}
/* Read and play a MML file */
static void mml_error(const char* err, int line, int column) {
fprintf(stderr, "Error reading MML file: %s at line %d, pos %d\n", err, line, column);
}
static int open_mml(const char* name) {
FILE *fp = fopen(name, "r");
if (!fp) {
fprintf(stderr, "Error reading MML file: %s", name);
return 1;
}
fseek(fp, 0, SEEK_END);
long size = ftell(fp);
char* content = malloc(size + 1);
fseek(fp, 0, SEEK_SET);
fread(content, 1, size, fp);
content[size] = 0;
mml_set_error_handler(mml_error);
int err;
struct seq_frame_map_t map;
err = mml_compile(content, &map);
if (err) {
return err;
}
free(content);
// Sort frames in stream
struct seq_frame_t* frame_stream;
int frame_count;
int voice_count;
seq_compile(&map, &frame_stream, &frame_count, &voice_count);
mml_free(&map);
// Save the compiled output to out.seq
FILE *out = fopen("sequencer.bin", "wb");
if (!out) {
fprintf(stderr, "Cannot write the sequencer.bin file\n");
return 1;
}
seq_stream_header.synth_frequency = synth_freq;
seq_stream_header.frames = frame_count;
seq_stream_header.voices = voice_count;
fwrite(&seq_stream_header, 1, sizeof(struct seq_stream_header_t), out);
fwrite(frame_stream, frame_count, sizeof(struct seq_frame_t), out);
_DPRINTF("File sequencer.bin written\n");
fclose(out);
seq_free(frame_stream);
return err;
}
static uint8_t seq_read_frame(struct seq_frame_t* frame) {
return fread(frame, 1, sizeof(struct seq_frame_t), seq_stream) == sizeof(struct seq_frame_t);
}
static int open_seq(const char* name) {
seq_stream = fopen(name, "rb");
if (!seq_stream) {
fprintf(stderr, "Error reading sequencer file: %s", name);
return 1;
}
fread(&seq_stream_header, 1, sizeof(struct seq_stream_header_t), seq_stream);
int err = seq_play_stream(&seq_stream_header, sizeof(poly_voice) / sizeof(struct voice_ch_t), &synth);
feed_channels = seq_feed_synth;
seq_set_stream_require_handler(seq_read_frame);
return err;
}
int main(int argc, char** argv) {
int voice = 0;
int16_t samples[8192];
uint16_t samples_sz = 0;
synth.voice = poly_voice;
synth.enable = 0;
@ -106,6 +182,26 @@ int main(int argc, char** argv) {
argc++;
argv--;
/* Check for MML compilation only */
} else if (!strcmp(argv[0], "compile-mml")) {
const char* name = argv[1];
_DPRINTF("compiling MML %s\n", name);
return open_mml(name);
/* Check for sequencer file play */
} else if (!strcmp(argv[0], "sequencer")) {
const char* name = argv[1];
_DPRINTF("playing sequencer file %s\n", name);
int err = open_seq(name);
if (err) {
return err;
}
argv++;
argc--;
/* Voice selection */
} else if (!strcmp(argv[0], "voice")) {
voice = atoi(argv[1]);
@ -177,56 +273,56 @@ int main(int argc, char** argv) {
int scale = atoi(argv[1]);
_DPRINTF("channel %d ADSR scale %d samples\n",
voice, scale);
poly_voice[voice].adsr.time_scale = scale;
poly_voice[voice].adsr.def.time_scale = scale;
argv++;
argc--;
} else if (!strcmp(argv[0], "delay")) {
int time = atoi(argv[1]);
_DPRINTF("channel %d ADSR delay %d units\n",
voice, time);
poly_voice[voice].adsr.delay_time = time;
poly_voice[voice].adsr.def.delay_time = time;
argv++;
argc--;
} else if (!strcmp(argv[0], "attack")) {
int time = atoi(argv[1]);
_DPRINTF("channel %d ADSR attack %d units\n",
voice, time);
poly_voice[voice].adsr.attack_time = time;
poly_voice[voice].adsr.def.attack_time = time;
argv++;
argc--;
} else if (!strcmp(argv[0], "decay")) {
int time = atoi(argv[1]);
_DPRINTF("channel %d ADSR decay %d units\n",
voice, time);
poly_voice[voice].adsr.decay_time = time;
poly_voice[voice].adsr.def.decay_time = time;
argv++;
argc--;
} else if (!strcmp(argv[0], "sustain")) {
int time = atoi(argv[1]);
_DPRINTF("channel %d ADSR sustain %d units\n",
voice, time);
poly_voice[voice].adsr.sustain_time = time;
poly_voice[voice].adsr.def.sustain_time = time;
argv++;
argc--;
} else if (!strcmp(argv[0], "release")) {
int time = atoi(argv[1]);
_DPRINTF("channel %d ADSR release %d units\n",
voice, time);
poly_voice[voice].adsr.release_time = time;
poly_voice[voice].adsr.def.release_time = time;
argv++;
argc--;
} else if (!strcmp(argv[0], "peak")) {
int amp = atoi(argv[1]);
_DPRINTF("channel %d ADSR peak amplitude %d\n",
voice, amp);
poly_voice[voice].adsr.peak_amp = amp;
poly_voice[voice].adsr.def.peak_amp = amp;
argv++;
argc--;
} else if (!strcmp(argv[0], "samp")) {
int amp = atoi(argv[1]);
_DPRINTF("channel %d ADSR sustain amplitude %d\n",
voice, amp);
poly_voice[voice].adsr.sustain_amp = amp;
poly_voice[voice].adsr.def.sustain_amp = amp;
argv++;
argc--;
} else if (!strcmp(argv[0], "reset")) {
@ -238,6 +334,10 @@ int main(int argc, char** argv) {
argc--;
/* Play out any remaining samples */
if (feed_channels) {
feed_channels(&synth);
}
if (synth.enable)
_DPRINTF("----- Start playback (0x%lx) -----\n",
synth.enable);
@ -255,6 +355,9 @@ int main(int argc, char** argv) {
sample_ptr++;
samples_sz++;
samples_remain--;
if (feed_channels) {
feed_channels(&synth);
}
}
ao_play(wav_device, (char*)samples, 2*samples_sz);

View File

@ -0,0 +1,4 @@
# “Alle meine Entchen”, a popular German beginner/children's tune.
# https://electronicmusic.fandom.com/wiki/Music_Macro_Language
o2l4t120 cdefg2g2 aaaag2 aaaag2 ffffe2e2 ddddc1

58
resources/bottakuri.mml Normal file
View File

@ -0,0 +1,58 @@
#title "bottakuri-shouten(ORIGINAL)PLAY3->PMD->MIDI"
#copyright "Music Composed by Kenkichi Motoi 1997 Wikimedia version 2012"
#timebase 48
GHI t150
GHI r8 r8 o3 l8 ; Melody 1
G v110
H v60
I v60
H <
I >
G rrrr rrrr rrrr rrrr
HI cgcg cgcg cgcg cgcg
G rrrr rrrr rrrr rrrr
HI cgcg cgcg cgcg >c<ceg
G >e<rrr rr>ef< rrrr >d<rrr
HI >e<gcg cg>ef< dada >d<g+c+g+
G >e<rrr rrrr rrrr rrrr >e<rrr
HI >e<gcg cgcg cgcg >c<gcg >e<gcg
G rr>ef< rrrr >d<rrr >e<rrr
HI cg>ef< dada >d<g+c+g+ >e<gcg
G rrrr rrrr rrrr brrr
HI cgcg cgcg >c<gcg b<b>d<b>
G rrbg+ rarb r>crd erfr
HI d<b>bg+ <d>a<d+>b <f+>>c<<g>>d e<e>f<f>
G erd+
HI e<e>d+
G <ab>c c+<rrr
HI <ab>c c+<c+c+c+
G rb>cc+ d<rrr r>def g
HI c+b>cc+ d<ddd d>def g
H <
I >
GHI <<gb>c
GHI gfed fedc edc<b
GHI l16abababab abababab gagagaga gagagaga
GHI g2
GHI r8
H >
I <
GHI l8>gab
G rrrr rrrr rrrr rrrr
HI cgcg cgcg cgcg cgcg
G rrrr rrrr rrrr rrrr
HI cgcg cgcg cgcg >c<ceg

13
resources/gakkoKouka.mml Normal file
View File

@ -0,0 +1,13 @@
# timebase 480
# title "The M.GAKKOU KOUKA"
# copyright "Music Composed by Kenkichi Motoi 2009 Wikimedia version 2012"
# https://electronicmusic.fandom.com/wiki/Music_Macro_Language
A t160
A o3l4
A ed8ce8 gg8er8 aa8>c<a8 g2r
A aa8ga8 >cc8d<r8 ee8de8 c2r
A dd8dd8 dd8dr8 ed8ef8 g2r
A aa8ga8 >cc8<ar8 >dc8de8 d2<r
A >ee8dc8< ab8>cc8< gg8ea8 g2r
A >cc8<ge8 cd8ea8 gg8de8 c2r

73
resources/loreley.mml Normal file
View File

@ -0,0 +1,73 @@
# Title: Loreley
# Composer: Ph. Friedrich Silcher (17891860), 1837
# Lyrics: Heinrich Heine (17971856), 1824
# Arranger: Klavier: August Linder; MML: mirabilos
# Encoder: mirabilos, 2016
# Copyright: MML encoding & arrangement © 2016 mirabilos, published under The MirOS Licence; copyright for text, music, and piano arrangement has expired
# Source: Linder, August (Hrsg.): Deutsche Weisen : Die beliebtesten Volks- und geistlichen Lieder. Stuttgart: Albert Auers Musikverlag, n.d., c. 1900.
# Instruments: Piano, Voice
# MML Tracks: 4 (5)
# Verses: 3
# Language: German
# Tempo: Andante
# Time Signature: 6/8
# Key Signature: C Major
# Pickup Measure: 1/8
# Measures: 16
## Structure:
# Track 1: voice track
# Text embedded here, as comments (not exported) roughly note-aligned
# Track 2: usually lowest voice double, except in bar 12
# Track 3: occasional helper
# Track 4: bass background
A o2t76l8 g | mlg. mna16 g ml>c< mnb a | g4. f4 f |
# 1. Ich weiß nicht, was soll es be- deu- ten, daß
# 2. Die schön-______ ste Jung-___frau si- tzet dort
# 3. Den Schif-______ fer im klei-nen Schif- fe er-
B o2t76l8 p | mle. mnf16 e mlf mnf f | e4. d4 d |
C o1t76l8 p | p2. | p2. |
D o1t76l8 p | mlc16g16>c<mng mlc16a16>d<mnb | mlc16g16>c<mng ml<f16>f16amnf |
A e. e16 e mldmnc d | mle4. mnep g | mlg. mna16 g ml>c< mnb a |
# ich______ so trau-_rig bin?_____ Ein Mähr- chen aus al-____ ten
# o-_______ ben wun-_der- bar,_____ ihr gold- nes Ge- schmei-_ de
# greift es mit wil-_dem Weh,_____ er schaut nicht die Fel-___ sen-
B c. c16 c<mlbmna b> | mlc4. mncp p | mle. mnf16 e mlf mnf f |
C p4. g4 g | mlg ppmnp4 p | p2. |
D ml<g16>e16gmne <g4 g> | mlc<gemnc4>p | mlc16g16>c<mng mlc16a16>d<mnb |
A g4. f4 f | e. e16 e g f d | mlc4. mncp e |
# Zei- ten, das kommt_____ mir nicht aus dem Sinn._____ Die
# bli- tzet, sie kämmt_____ ihr gol- de- nes Haar._____ Sie
# rif- fe, er schaut nur hin-auf in die Höh._____ Ich
B e4. d4 d | c. c16 c <b b b | p2. |
C p2. | p p p d d f | mle ppmnp4 p |
D mlc16g16>c<mng ml<f16>f16amnf |<g> g c <g g g> | mlc<egmnc4 p |
A mld. mne16 d g d d | b4. a4 a | g4 g mlf#mng a |
# Luft_____ ist kühl und es dun- kelt und ru-hig fließt___ der
# kämmt es mit gol- de- nem Kam- me und singt ein Lied_____ da-
# glau- be, die Wel- len ver- schlin- gen am En- de Schif-fer und
B mlb.> mnc16< b b b b | >d4. c4 c | <b4 p mlamn b> c<< |
C p p p >d p p | g4. e4 e | d4 d<d4 d |
D mlg> d mng<mlg> d mng< | mlg>dmng<mlc>emna | ml<d>dmnb p4 f# |
# The next measure missed an o1g as last eighth, to fit on four tracks
# (but its doubled by the o2g in the vocals, so its okay musically)
A mlg4.mng4 g | g. a16 g>mlc<mn b a |
# Rhein;__ der Gi- pfel des Ber-____ ges
# bei;____ das hat ei-ne wun-_____ der-
# Kahn,___ und das hat mit ih-_____ rem
B mlgb>cdc<mnb | >>e. f16 e mlfmn f f |
C mld4.mnd4> f< | p2. |
D mlb>defe mnd< | mlc16g16>c<mng mlc16a16>d<mnb | l16
A mlg4mn> e d4 d | c. c16c ml<b mna b | ml>c4.mncp ||
# fun-______ kelt im A-_______ bend-son-_ nen-schein.__
# sa-_______ me, ge- wal- ti-ge Me-__ lo- dei._____
# Sin-______ gen die Lo-______ re-_ ley__ ge- than.____
B mle4mn g f4 f | e. e16e ml f mnf f | ml e4.mnep ||
C p2. | p p p g g g | ml gppmnpp ||
D mlcg>mnc<pp8 ml<f>a>mnd<pp8 | ml<g>g>mncpl8p d c d | ml<cegmncp ||

9
resources/scale.mml Normal file
View File

@ -0,0 +1,9 @@
o2l8 ml
v120
ws o2 cc#dd#eff#gg#aa#b>c
p2
wt o2 cc#dd#eff#gg#aa#b>c
p2
ww o2 cc#dd#eff#gg#aa#b>c
p2

160
sequencer.c Normal file
View File

@ -0,0 +1,160 @@
/*!
* Polyphonic synthesizer for microcontrollers. Sequencer code.
* (C) 2021 Luciano Martorella
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
#include "sequencer.h"
#include "debug.h"
#include <stdlib.h>
#include <string.h>
/*! State used between `seq_play_stream` and `seq_feed_synth` */
static int frame_count;
static uint8_t voice_count;
static uint8_t (*new_frame_require)(struct seq_frame_t* frame);
void seq_set_stream_require_handler(uint8_t (*handler)(struct seq_frame_t* frame)) {
new_frame_require = handler;
}
/*! State of the sequencer compiler */
struct compiler_state_t {
/*! The synth used for simulation */
struct poly_synth_t* synth;
/*! The input channel map */
struct seq_frame_map_t* input_map;
/*! The output frame stream */
struct seq_frame_t* out_stream;
/*! The position of writing frame in the output stream */
int stream_position;
/*! The positions of every channel in the input channel map */
int* channel_positions;
};
/*! Feed the first free channel and copy the selected frame in the output stream */
static void seq_feed_channels(struct compiler_state_t* state) {
intptr_t mask = 1;
int voice_idx = 0;
for (int map_idx = 0; map_idx < state->input_map->channel_count; map_idx++) {
// Skip empty channels
const struct seq_frame_list_t* channel = &state->input_map->channels[map_idx];
if (channel->count > 0) {
if (state->channel_positions[voice_idx] < channel->count && (state->synth->enable & mask) == 0) {
// Feed data
struct seq_frame_t* frame = channel->frames + (state->channel_positions[voice_idx]++);
voice_wf_set(&state->synth->voice[voice_idx].wf, &frame->waveform_def);
adsr_config(&state->synth->voice[voice_idx].adsr, &frame->adsr_def);
state->synth->enable |= mask;
state->out_stream[state->stream_position++] = *frame;
// Don't overload the CPU with multiple frames per sample
// This will create minimum phase errors (of 1 sample period) but will keep the process real-time on slower CPUs
break;
}
mask <<= 1;
voice_idx++;
}
}
}
void seq_compile(struct seq_frame_map_t* map, struct seq_frame_t** frame_stream, int* frame_count, int* voice_count) {
int total_frame_count = 0;
// Skip empty channels
int valid_channel_count = 0;
for (int i = 0; i < map->channel_count; i++) {
if (map->channels[i].count > 0) {
valid_channel_count++;
total_frame_count += map->channels[i].count;
}
}
// Prepare output buffer, with total frame count
*frame_count = total_frame_count;
*voice_count = valid_channel_count;
*frame_stream = malloc(sizeof(struct seq_frame_t) * total_frame_count);
// Now play sequencer data, currently by channel, simulating the timing of the synth.
struct poly_synth_t synth;
struct voice_ch_t* poly_voice = malloc(sizeof(struct voice_ch_t) * valid_channel_count);
synth.voice = poly_voice;
synth.mute = 0;
synth.enable = 0;
struct compiler_state_t state;
state.channel_positions = malloc(sizeof(int) * valid_channel_count);
memset(state.channel_positions, 0, sizeof(int) * valid_channel_count);
state.input_map = map;
state.out_stream = *frame_stream;
state.stream_position = 0;
state.synth = &synth;
seq_feed_channels(&state);
while (synth.enable) {
poly_synth_next(&synth);
seq_feed_channels(&state);
}
free(poly_voice);
free(state.channel_positions);
}
int seq_play_stream(const struct seq_stream_header_t* stream_header, uint8_t _voice_count, struct poly_synth_t* synth) {
if (stream_header->voices > _voice_count) {
_DPRINTF("Not enough voices");
return 1;
}
if (stream_header->synth_frequency != synth_freq) {
_DPRINTF("Mismatching sampling frequency");
return 1;
}
frame_count = stream_header->frames;
voice_count = stream_header->voices;
// Disable all channels
synth->enable = 0;
return 0;
}
void seq_feed_synth(struct poly_synth_t* synth) {
uintptr_t mask = 1;
for (uint8_t i = 0; i < voice_count; i++, mask <<= 1) {
if ((synth->enable & mask) == 0) {
// Feed data
struct seq_frame_t frame;
if (!new_frame_require(&frame)) {
// End-of-stream
return;
}
voice_wf_set(&synth->voice[i].wf, &frame.waveform_def);
adsr_config(&synth->voice[i].adsr, &frame.adsr_def);
synth->enable |= mask;
// Don't overload the CPU with multiple frames per sample
// This will create minimum phase errors (of 1 sample period) but will keep the process real-time on slower CPUs
break;
}
}
}
void seq_free(struct seq_frame_t* frame_stream) {
free(frame_stream);
}

88
sequencer.h Normal file
View File

@ -0,0 +1,88 @@
/*!
* Polyphonic synthesizer for microcontrollers.
* (C) 2021 Luciano Martorella
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
#ifndef _SEQUENCER_H
#define _SEQUENCER_H
#include "adsr.h"
#include "waveform.h"
#include "synth.h"
/*!
* Define a single step/frame of the sequencer. It applies to the active channel.
* Contains the definition of the next waveform and envelope.
* 16 bytes
*/
struct seq_frame_t {
/*! Envelope definition */
struct adsr_env_def_t adsr_def;
/*! Waveform definition */
struct voice_wf_def_t waveform_def;
};
struct seq_stream_header_t {
/*! Sampling frequency required for correct timing */
uint16_t synth_frequency;
/*! Number of voices. They will all be enabled */
uint8_t voices;
/*! Total frame count */
uint16_t frames;
/*! Follow frames data, as stream of seq_frame_t */
};
/*!
* Plays a stream sequence of frames, in the order requested by the synth.
* The frames must then be sorted in the same fetch order and not in channel order.
* Frames will be fed using the handler passed by `seq_set_stream_require_handler`.
*/
int seq_play_stream(const struct seq_stream_header_t* stream_header, uint8_t voice_count, struct poly_synth_t* synth);
/*! Requires a new frame. The handler must return 1 if a new frame was acquired, or zero if EOF */
void seq_set_stream_require_handler(uint8_t (*handler)(struct seq_frame_t* frame));
/*! Use it when `seq_play_stream` is in use, must be called at every sample */
void seq_feed_synth(struct poly_synth_t* synth);
/*! List of frames, used by `seq_frame_map_t` */
struct seq_frame_list_t {
/*! Frame count */
int count;
/*! List of frames */
struct seq_frame_t* frames;
};
/*!
* A frame map is an intermediate data in which frames are organized by channel.
* Typically requires dynamic memory allocation (heap) to allocate the whole tune
* upfront.
*/
struct seq_frame_map_t {
/*! Channel count */
int channel_count;
/*! Array of frame lists */
struct seq_frame_list_t* channels;
};
/*! Compile/reorder a frame-map (by channel) to a sequential stream */
void seq_compile(struct seq_frame_map_t* map, struct seq_frame_t** frame_stream, int* frame_count, int* voice_count);
/*! Free the stream allocated by `seq_compile`. */
void seq_free(struct seq_frame_t* frame_stream);
#endif

View File

@ -17,6 +17,8 @@
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
#ifndef _VOICE_H
#define _VOICE_H
#include "waveform.h"
#include "adsr.h"
@ -71,3 +73,5 @@ inline static int8_t voice_ch_next(struct voice_ch_t* const voice) {
/*
* vim: set sw=8 ts=8 noet si tw=72
*/
#endif

View File

@ -23,13 +23,6 @@
#include "debug.h"
#include <stdlib.h>
/* Waveform generation modes */
#define VOICE_MODE_DC (0)
#define VOICE_MODE_SQUARE (1)
#define VOICE_MODE_SAWTOOTH (2)
#define VOICE_MODE_TRIANGLE (3)
#define VOICE_MODE_NOISE (4)
/* Amplitude scaling */
#define VOICE_WF_AMP_SCALE (8)
@ -109,28 +102,27 @@ int8_t voice_wf_next(struct voice_wf_gen_t* const wf_gen) {
}
/* Compute frequency period (sawtooth wave) */
static uint16_t voice_wf_calc_sawtooth_period(uint16_t freq) {
uint16_t voice_wf_freq_to_period(uint16_t freq) {
/* Use 16-bit 12.4 fixed point */
return (((uint32_t)(synth_freq << PERIOD_FP_SCALE)) / freq);
}
/* Compute frequency period (square/triangle wave) */
static uint16_t voice_wf_calc_square_period(uint16_t freq) {
return voice_wf_calc_sawtooth_period(freq) >> 1;
static uint16_t voice_wf_to_square_period(uint16_t period) {
return period >> 1;
}
void voice_wf_set_dc(struct voice_wf_gen_t* const wf_gen,
int8_t amplitude) {
wf_gen->mode = VOICE_MODE_DC;
wf_gen->amplitude = amplitude;
}
void voice_wf_set_square(struct voice_wf_gen_t* const wf_gen,
uint16_t freq, int8_t amplitude) {
static void voice_wf_set_square_p(struct voice_wf_gen_t* const wf_gen,
uint16_t period, int8_t amplitude) {
wf_gen->mode = VOICE_MODE_SQUARE;
wf_gen->amplitude = (int16_t)amplitude << VOICE_WF_AMP_SCALE;
wf_gen->period = voice_wf_calc_square_period(freq);
wf_gen->period = voice_wf_to_square_period(period);
wf_gen->period_remain = wf_gen->period;
wf_gen->sample = wf_gen->amplitude;
_DPRINTF("wf=%p INIT mode=SQUARE amp=%d per=%d rem=%d "
@ -140,11 +132,17 @@ void voice_wf_set_square(struct voice_wf_gen_t* const wf_gen,
wf_gen->sample);
}
void voice_wf_set_sawtooth(struct voice_wf_gen_t* const wf_gen,
void voice_wf_set_square(struct voice_wf_gen_t* const wf_gen,
uint16_t freq, int8_t amplitude) {
uint16_t period = voice_wf_freq_to_period(freq);
voice_wf_set_square_p(wf_gen, period, amplitude);
}
static void voice_wf_set_sawtooth_p(struct voice_wf_gen_t* const wf_gen,
uint16_t period, int8_t amplitude) {
wf_gen->mode = VOICE_MODE_SAWTOOTH;
wf_gen->sample = -(int16_t)amplitude << VOICE_WF_AMP_SCALE;
wf_gen->period = voice_wf_calc_sawtooth_period(freq);
wf_gen->period = period;
wf_gen->period_remain = wf_gen->period;
wf_gen->amplitude = -wf_gen->sample;
wf_gen->step = (wf_gen->amplitude / (wf_gen->period >> PERIOD_FP_SCALE)) << 1;
@ -155,11 +153,17 @@ void voice_wf_set_sawtooth(struct voice_wf_gen_t* const wf_gen,
wf_gen->sample);
}
void voice_wf_set_triangle(struct voice_wf_gen_t* const wf_gen,
void voice_wf_set_sawtooth(struct voice_wf_gen_t* const wf_gen,
uint16_t freq, int8_t amplitude) {
uint16_t period = voice_wf_freq_to_period(freq);
voice_wf_set_sawtooth_p(wf_gen, period, amplitude);
}
static void voice_wf_set_triangle_p(struct voice_wf_gen_t* const wf_gen,
uint16_t period, int8_t amplitude) {
wf_gen->mode = VOICE_MODE_TRIANGLE;
wf_gen->sample = -(int16_t)amplitude << VOICE_WF_AMP_SCALE;
wf_gen->period = voice_wf_calc_square_period(freq);
wf_gen->period = voice_wf_to_square_period(period);
wf_gen->period_remain = wf_gen->period;
wf_gen->amplitude = -wf_gen->sample;
wf_gen->step = (wf_gen->amplitude / (wf_gen->period >> PERIOD_FP_SCALE)) << 1;
@ -170,11 +174,38 @@ void voice_wf_set_triangle(struct voice_wf_gen_t* const wf_gen,
wf_gen->sample);
}
void voice_wf_set_triangle(struct voice_wf_gen_t* const wf_gen,
uint16_t freq, int8_t amplitude) {
uint16_t period = voice_wf_freq_to_period(freq);
voice_wf_set_triangle_p(wf_gen, period, amplitude);
}
void voice_wf_set_noise(struct voice_wf_gen_t* const wf_gen,
int8_t amplitude) {
wf_gen->mode = VOICE_MODE_NOISE;
wf_gen->amplitude = amplitude;
}
void voice_wf_set(struct voice_wf_gen_t* const wf_gen, struct voice_wf_def_t* const wf_def) {
switch (wf_def->mode) {
case VOICE_MODE_DC:
voice_wf_set_dc(wf_gen, wf_def->amplitude);
break;
case VOICE_MODE_SQUARE:
voice_wf_set_square_p(wf_gen, wf_def->period, wf_def->amplitude);
break;
case VOICE_MODE_SAWTOOTH:
voice_wf_set_sawtooth_p(wf_gen, wf_def->period, wf_def->amplitude);
break;
case VOICE_MODE_TRIANGLE:
voice_wf_set_triangle_p(wf_gen, wf_def->period, wf_def->amplitude);
break;
case VOICE_MODE_NOISE:
voice_wf_set_noise(wf_gen, wf_def->amplitude);
break;
}
}
/*
* vim: set sw=8 ts=8 noet si tw=72
*/

View File

@ -43,6 +43,28 @@ struct voice_wf_gen_t {
uint8_t mode;
};
/* Waveform generation modes */
#define VOICE_MODE_DC (0)
#define VOICE_MODE_SQUARE (1)
#define VOICE_MODE_SAWTOOTH (2)
#define VOICE_MODE_TRIANGLE (3)
#define VOICE_MODE_NOISE (4)
/**
* The Waveform definition. 4 bytes.
*/
struct voice_wf_def_t {
/*! Waveform generation mode, see VOICE_MODE_ enumerated values */
uint8_t mode;
/*! Waveform amplitude */
int8_t amplitude;
/*! Waveform full period as `sample_freq / frequency` (if applicable) */
uint16_t period;
};
/* Compute frequency period of a generic wave */
uint16_t voice_wf_freq_to_period(uint16_t frequency);
/*!
* Configure the generator for a DC offset synthesis.
*/
@ -73,6 +95,11 @@ void voice_wf_set_triangle(struct voice_wf_gen_t* const wf_gen,
void voice_wf_set_noise(struct voice_wf_gen_t* const wf_gen,
int8_t amplitude);
/*!
* Configure the generator using waveform type and common parameters
*/
void voice_wf_set(struct voice_wf_gen_t* const wf_gen, struct voice_wf_def_t* const wf_def);
/*!
* Retrieve the next sample from the generator.
*/