Better-defined voices, to prepare for polyphony

This commit is contained in:
Neale Pickett 2020-10-24 20:15:18 -06:00
parent 5ad56d8e92
commit 706a00b28f
8 changed files with 390 additions and 154 deletions

8
Makefile Normal file
View File

@ -0,0 +1,8 @@
BOARD = --board adafruit:samd:adafruit_trellis_m4
verify: uilleann.ino
arduino --verify $(BOARD) $<
install: uilleann.ino
arduino --upload $(BOARD) $<

91
dx9.h Normal file
View File

@ -0,0 +1,91 @@
// FM Algorithms used by the DX9
// Excellent write-up:
// https://gist.github.com/bryc/e997954473940ad97a825da4e7a496fa
#pragma once
// Each operator has 4 input gains and one output gain:
// { 1→, 2→, 3→, 4→, →out}
// ⮎4→3→2→1→
#define DX9_ALG_1(feedback) \
{ \
{0, 1, 0, 0, 1}, \
{0, 0, 1, 0, 0}, \
{0, 0, 0, 1, 0}, \
{0, 0, 0, feedback, 0}, \
}
// ⮎4⬎
// 3→2→1→
#define DX9_ALG_2(feedback) \
{ \
{0, 1, 0, 0, 1}, \
{0, 0, 1, 1, 0}, \
{0, 0, 0, 0, 0}, \
{0, 0, 0, feedback, 0}, \
}
// ⮎4⬎
// 3→2→1→
#define DX9_ALG_3(feedback) \
{ \
{0, 1, 0, 1, 1}, \
{0, 0, 1, 0, 0}, \
{0, 0, 0, 0, 0}, \
{0, 0, 0, feedback, 0}, \
}
// ⮎4→3⬎
// 2→1→
#define DX9_ALG_4(feedback) \
{ \
{0, 1, 0, 1, 1}, \
{0, 0, 1, 0, 0}, \
{0, 0, 0, 0, 0}, \
{0, 0, 0, feedback, 0}, \
}
// ⮎4→3→
// 2→1→
#define DX9_ALG_5(feedback) \
{ \
{0, 1, 0, 0, 1}, \
{0, 0, 0, 0, 0}, \
{0, 0, 0, 1, 1}, \
{0, 0, 0, feedback, 0}, \
}
// 1→
// ⮎4→2→
// 3→
#define DX9_ALG_6(feedback) \
{ \
{0, 0, 0, 0, 1}, \
{0, 0, 0, 1, 1}, \
{0, 0, 0, 0, 1}, \
{0, 0, 0, feedback, 0}, \
}
// 1→
// 2→
// ⮎4→3→
#define DX9_ALG_7(feedback) \
{ \
{0, 0, 0, 0, 1}, \
{0, 0, 0, 0, 1}, \
{0, 0, 1, 0, 1}, \
{0, 0, 0, feedback, 0}, \
}
// 1→
// 2→
// 3→
// ⮎4→
#define DX9_ALG_8(feedback) \
{ \
{0, 0, 0, 0, 1}, \
{0, 0, 0, 0, 1}, \
{0, 0, 0, 0, 1}, \
{0, 0, 0, feedback, 1}, \
}

44
notes.cpp Normal file
View File

@ -0,0 +1,44 @@
#include <Arduino.h>
#include "notes.h"
const char *NoteNames[] {
"C ", "C#", "D ", "Eb", "E ", "F ", "F#", "G ", "Ab", "A ", "Bb", "B ",
};
float JustPitches[MaxNote + 1];
// Hat tip to Kyle Gann
// https://www.kylegann.com/tuning.html
void setupJustPitches(uint8_t baseNote, float basePitch) {
JustPitches[baseNote + 0] = basePitch * 1 / 1; // D
JustPitches[baseNote + 1] = basePitch * 16 / 15; // Eb
JustPitches[baseNote + 2] = basePitch * 9 / 8; // E
JustPitches[baseNote + 3] = basePitch * 6 / 5; // F
JustPitches[baseNote + 4] = basePitch * 5 / 4; // F#
JustPitches[baseNote + 5] = basePitch * 4 / 3; // G
JustPitches[baseNote + 6] = basePitch * 45 / 32; // Ab
JustPitches[baseNote + 7] = basePitch * 3 / 2; // A
JustPitches[baseNote + 8] = basePitch * 8 / 5; // Bb
JustPitches[baseNote + 9] = basePitch * 5 / 3; // B
JustPitches[baseNote + 10] = basePitch * 9 / 5; // C
JustPitches[baseNote + 11] = basePitch * 15 / 8; // C#
// Two fourths up from the base pitch, so G major scale sounds right
JustPitches[baseNote + 11] = basePitch * 8 / 3; // C#
// Octaves
for (int note = baseNote; note < baseNote + 12; note++) {
for (int i = 1; i < 9; i++) {
int multiplier = 1<<i;
int shift = i*12;
int upNote = note + shift;
int dnNote = note - shift;
if (upNote <= MaxNote) {
JustPitches[upNote] = JustPitches[note] * multiplier;
}
if (dnNote >= 0) {
JustPitches[dnNote] = JustPitches[note] / multiplier;
}
}
}
}

11
notes.h
View File

@ -1,3 +1,7 @@
#pragma once
#define PITCH_D4 293.66
enum Notes {
NOTE_C0, NOTE_CS0, NOTE_D0, NOTE_DS0, NOTE_E0, NOTE_F0, NOTE_FS0, NOTE_G0, NOTE_GS0, NOTE_A0, NOTE_AS0, NOTE_B0,
NOTE_C1, NOTE_CS1, NOTE_D1, NOTE_DS1, NOTE_E1, NOTE_F1, NOTE_FS1, NOTE_G1, NOTE_GS1, NOTE_A1, NOTE_AS1, NOTE_B1,
@ -11,8 +15,7 @@ enum Notes {
};
const uint8_t MaxNote = NOTE_B8;
const char *NoteNames[] {
"C ", "C#", "D ", "Eb", "E ", "F ", "F#", "G ", "Ab", "A ", "Bb", "B ",
};
extern const char *NoteNames[];
extern float JustPitches[MaxNote + 1];
#define PITCH_D4 293.66
void setupJustPitches(uint8_t baseNote, float basePitch);

View File

@ -1,50 +1,39 @@
typedef struct Operator {
float gain;
float delay;
float attack;
float hold;
float decay;
float sustain;
float release;
float baseFrequency;
float multiplier;
} Operator;
// "Factory" patches
typedef struct Patch {
char *name;
Operator operators[4];
float feedback;
} Patch;
#pragma once
#include "dx9.h"
Patch Bank[] = {
// Waveform, offset, multiplier, delay, attack, holdAmp, hold, decay, sustainAmp, release
FMPatch Bank[] = {
{
"Venus Oboe",
DX9_ALG_5(0),
{
{1.0, 0, 10.5, 0, 5000, 0.75, 5.0, 0, 1.00},
{1.0, 0, 10.5, 0, 2000, 0.80, 5.0, 0, 4.00},
{0.0, 0, 10.5, 0, 2000, 0.50, 5.0, 0, 8.00},
{0.0, 0, 50.0, 0, 800, 0.75, 5.0, 0, 16.00},
// Waveform off mult del att hldA hld dec susA rel
{WAVEFORM_SINE, 0, 1.00, 0, 10.5, 1.0, 10.5, 0, 0.75, 5},
{WAVEFORM_SINE, 0, 4.00, 0, 10.5, 1.0, 10.5, 0, 0.80, 5},
{WAVEFORM_SINE, 0, 8.00, 0, 10.5, 1.0, 10.5, 0, 0.50, 5},
{WAVEFORM_SINE, 0, 16.00, 0, 10.5, 1.0, 50.0, 0, 0.75, 5},
},
0.0,
},
{
"IWantPizza",
DX9_ALG_1(0),
{
{1.0, 0, 10.5, 0, 5000, 0.35, 100, 0, 4.00},
{1.0, 0, 10.5, 0, 2000, 0.30, 100, 0, 1.00},
{1.0, 0, 10.5, 0, 2000, 0.50, 100, 0, 8.00},
{1.0, 0, 200, 0, 800, 0.25, 100, 0, 16.00},
{WAVEFORM_SINE, 0, 4.00, 0, 10.5, 1.0, 10.5, 0, 0.35, 20},
{WAVEFORM_SINE, 0, 1.00, 0, 10.5, 1.0, 10.5, 0, 0.30, 20},
{WAVEFORM_SINE, 0, 8.00, 0, 10.5, 1.0, 10.5, 0, 0.50, 20},
{WAVEFORM_SINE, 0, 16.00, 0, 10.5, 1.0, 50, 0, 0.25, 20},
},
0.0,
},
{
"Ray Gun",
DX9_ALG_1(0),
{
{1.0, 0, 10.5, 0, 5000, 0.35, 2000, 0, 1.00},
{1.0, 0, 10.5, 0, 2000, 0.30, 2000, 0, 1.00},
{1.0, 0, 10.5, 0, 2000, 0.00, 2000, 0, 9.00},
{1.0, 0, 200, 0, 800, 0.25, 800, 0, 1.00},
{WAVEFORM_SINE, 0, 1.00, 0, 10.5, 1.0, 10.5, 0, 0.35, 20},
{WAVEFORM_SINE, 0, 1.00, 0, 10.5, 1.0, 10.5, 0, 0.30, 20},
{WAVEFORM_SINE, 0, 9.00, 0, 10.5, 1.0, 10.5, 0, 0.00, 20},
{WAVEFORM_SINE, 0, 1.00, 0, 10.5, 1.0, 50, 0, 0.25, 8},
},
0.0,
},
};

42
synth.cpp Normal file
View File

@ -0,0 +1,42 @@
#include "synth.h"
void FMVoiceLoadPatch(FMVoice *v, FMPatch *p) {
for (int i=0; i<NUM_OPERATORS; i++) {
FMOperator op = p->operators[i];
v->oscillators[i].begin(op.waveform);
v->envelopes[i].delay(op.delayTime);
v->envelopes[i].attack(op.attackTime);
v->oscillators[i].amplitude(op.holdAmplitude);
v->envelopes[i].hold(op.holdTime);
v->envelopes[i].decay(op.decayTime);
v->envelopes[i].sustain(op.sustainAmplitude / op.holdAmplitude);
v->envelopes[i].release(op.releaseTime);
// This feels wasteful 🙁
for (int j=0; j<NUM_OPERATORS; j++) {
v->mixers[i].gain(j, p->gains[i][j]);
}
v->outputMixer.gain(i, p->gains[i][NUM_OPERATORS]);
}
}
void FMVoiceSetPitch(FMVoice *v, float freq) {
for (int i=0; i<4; i++) {
FMOperator op = v->patch->operators[i];
v->oscillators[i].frequency(op.offset + (freq * op.multiplier));
}
}
void FMVoiceNoteOn(FMVoice *v, float freq) {
FMVoiceSetPitch(v, freq);
for (int i=0; i<4; i++) {
v->envelopes[i].noteOn();
}
}
void FMVoiceNoteOff(FMVoice *v) {
for (int i=0; i<4; i++) {
v->envelopes[i].noteOff();
}
}

163
synth.h
View File

@ -1,38 +1,135 @@
#pragma once
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
// GUItool: begin automatically generated code
AudioMixer4 feedback; //xy=110,37
AudioSynthWaveformSineModulated osc4; //xy=112,98
AudioSynthWaveformSineModulated osc2; //xy=112,194
AudioSynthWaveformSineModulated osc1; //xy=112,245
AudioSynthWaveformSineModulated osc3; //xy=113,146
AudioMixer4 mixOp; //xy=114,418
AudioEffectEnvelope env4; //xy=251,97
AudioEffectEnvelope env3; //xy=251,146
AudioEffectEnvelope env2; //xy=252,194
AudioEffectEnvelope env1; //xy=252,245
AudioFilterBiquad biquad1; //xy=257,418
AudioMixer4 mixL; //xy=472,402
AudioMixer4 mixR; //xy=473,498
AudioOutputAnalogStereo dacs1; //xy=724,452
AudioConnection patchCord1(feedback, osc4);
AudioConnection patchCord2(osc4, env4);
AudioConnection patchCord3(osc4, 0, feedback, 0);
AudioConnection patchCord4(osc2, env2);
AudioConnection patchCord5(osc1, env1);
AudioConnection patchCord6(osc3, env3);
AudioConnection patchCord7(mixOp, biquad1);
AudioConnection patchCord8(env4, osc3);
AudioConnection patchCord9(env4, 0, mixOp, 3);
AudioConnection patchCord10(env3, 0, mixOp, 2);
AudioConnection patchCord11(env2, osc1);
AudioConnection patchCord12(env2, 0, mixOp, 1);
AudioConnection patchCord13(env1, 0, mixOp, 0);
AudioConnection patchCord14(biquad1, 0, mixL, 0);
AudioConnection patchCord15(biquad1, 0, mixR, 0);
AudioConnection patchCord17(mixL, 0, dacs1, 0);
AudioConnection patchCord18(mixR, 0, dacs1, 1);
// GUItool: end automatically generated code
#define NUM_OPERATORS 4
/** FMOperator defines all settable paramaters to an operator.
*
* An FM operator consists of:
* - An input
* - An oscillator
* - An envelope generator
*
* Frequency Modulation happens by chaining oscillators together,
* using the output of one to modulate the frequency of the next.
*
* Oscillators generate waveforms in a shape defined by
* `synth_waveform.h`. WAVEFORM_SINE is a good one to start with.
* Other sensible options are SAWTOOTH, SQUARE, and TRIANGLE.
*
* Frequency for an oscillator is calculated with:
* offset + (voiceFrequency × multiplier)
*
* Oscillator frequency is then modulated by the level obtained
* by the input mixer: level of 1.0 shifts frequency up by
* 8 octaves, level of -1.0 shifts frequency down by 8 octaves.
*
* The envelope modifies amplitude of the oscillator output,
* using the following rules:
* - stay at 0 until Note On
* - stay at 0 for `delayTime` milliseconds
* - linear increase to `holdAmplitude` for `attackTime` milliseconds
* - stay at `holdAmplitude` for `holdTime` milliseconds
* - linear decrease to `sustainAmplitude` for `decayTime` milliseconds
* - stay at `sustainAmplitude` until Note Off
* - linear decrease to 0 for `releaseTime` milliseconds
*/
typedef struct FMOperator {
// Oscillator
short waveform;
float offset;
float multiplier;
// Envelope
float delayTime;
float attackTime;
float holdAmplitude;
float holdTime;
float decayTime;
float sustainAmplitude;
float releaseTime;
} FMOperator;
/** FMPatch defines all parameters to a voice patch.
*
* This defines the "sound" of an FM voice,
* just like a "Patch" does in a hardware synthesizer.
* I think of a "patch" being the physical cables that
* connect oscillators together, and to the output mixer.
*
* Each operator has NUM_OPERATORS input gains,
* one output gain (to the voice output mixer),
* and NUM_OPERATORS operators.
*
* Historical FM synthisizers,
* such as the DX7 or DX9,
* used "algorithms" to patch operators into one another:
* this is done with 0.0 or 1.0 values to the gains.
* The "feedback" on operator 4 of the DX9
* can be accomplished by patching an operator into itself.
*/
typedef struct FMPatch {
char *name;
float gains[NUM_OPERATORS][NUM_OPERATORS+1];
FMOperator operators[NUM_OPERATORS];
} FMPatch;
/** FMVoice sets up all the Audio objects for a voice.
*/
typedef struct FMVoice {
AudioMixer4 mixers[NUM_OPERATORS];
AudioSynthWaveformModulated oscillators[NUM_OPERATORS];
AudioEffectEnvelope envelopes[NUM_OPERATORS];
AudioMixer4 outputMixer;
FMPatch *patch;
} FMVoice;
/** FMOperatorWiring outputs AudioConnection initializers to wire one FM Operator
*/
#define FMOperatorWiring(name, i) \
{name.mixers[i], 0, name.oscillators[i], 0}, \
{name.oscillators[i], 0, name.envelopes[i], 0}, \
{name.envelopes[i], 0, name.outputMixer, i}, \
{name.envelopes[i], 0, name.mixers[0], i}, \
{name.envelopes[i], 0, name.mixers[1], i}, \
{name.envelopes[i], 0, name.mixers[2], i}, \
{name.envelopes[i], 0, name.mixers[3], i}
/** FMVoiceWiring outputs AudioConnection initializer to wire one FMVoice
*/
#define FMVoiceWiring(name) \
FMOperatorWiring(name, 0), \
FMOperatorWiring(name, 1), \
FMOperatorWiring(name, 2), \
FMOperatorWiring(name, 3)
/** FMVoiceLoadPatch loads a patch into a voice.
*/
void FMVoiceLoadPatch(FMVoice *v, FMPatch *p);
/** FMVoiceSetPitch sets the pitch (Hz) of a voice.
*
* This does not signal the envelope in any way.
* You would use this for a glissando, portamento, or pitch bend.
* In my bagpipe, this prevents "reed noise" when changing notes.
*/
void FMVoiceSetPitch(FMVoice *v, float pitch);
/** FMVoiceNoteOn sets the pitch (Hz) of a voice, and starts in playing.
*
* This tells the envelope generators to begin.
* On a piano, this is what you would use when a key is pressed.
* In my bagpipe, this triggers "reed noise".
*/
void FMVoiceNoteOn(FMVoice *v, float pitch);
/** FMVoiceNoteOff stops a note from playing.
*
* This turns the voice "off" by shutting down all the envelope generators.
* On a piano, this is what you would use when a key is released.
* In my bagpipe, this corresponds to all holes being closed.
*/
void FMVoiceNoteOff(FMVoice *v);

View File

@ -12,11 +12,47 @@
#define KNEE_OFFSET 0
#define KEY_OFFSET 2
float cmaj_low[8] = { 130.81, 146.83, 164.81, 174.61, 196.00, 220.00, 246.94, 261.63 };
float cmaj_high[8] = { 261.6, 293.7, 329.6, 349.2, 392.0, 440.0, 493.9, 523.3 };
FMVoice Chanter;
FMVoice Drones[3];
FMVoice Regulators[3];
AudioEffectEnvelope *envs[] = {&env1, &env2, &env3, &env4};
AudioSynthWaveformSineModulated *oscs[] = {&osc1, &osc2, &osc3, &osc4};
AudioFilterBiquad biquad1;
AudioMixer4 mixDrones;
AudioMixer4 mixRegulators;
AudioMixer4 mixL;
AudioMixer4 mixR;
AudioOutputAnalogStereo dacs1;
AudioConnection FMVoicePatchCords[] = {
{Chanter.outputMixer, 0, biquad1, 0},
{biquad1, 0, mixL, 0},
{biquad1, 0, mixR, 0},
{Drones[0].outputMixer, 0, mixDrones, 0},
{Drones[1].outputMixer, 0, mixDrones, 1},
{Drones[2].outputMixer, 0, mixDrones, 2},
{mixDrones, 0, mixL, 1},
{mixDrones, 0, mixR, 1},
{Regulators[0].outputMixer, 0, mixRegulators, 0},
{Regulators[1].outputMixer, 0, mixRegulators, 1},
{Regulators[2].outputMixer, 0, mixRegulators, 2},
{mixRegulators, 0, mixL, 2},
{mixRegulators, 0, mixR, 2},
{mixL, 0, dacs1, 0},
{mixR, 0, dacs1, 1},
FMVoiceWiring(Chanter),
FMVoiceWiring(Drones[0]),
FMVoiceWiring(Drones[1]),
FMVoiceWiring(Drones[2]),
FMVoiceWiring(Drones[3]),
FMVoiceWiring(Regulators[0]),
FMVoiceWiring(Regulators[1]),
FMVoiceWiring(Regulators[2]),
FMVoiceWiring(Regulators[3]),
};
int currentPatch = 0;
@ -25,57 +61,8 @@ Adafruit_NeoTrellisM4 trellis = Adafruit_NeoTrellisM4();
MicroOLED oled(9, 1);
QwiicButton bag;
// Hat tip to Kyle Gann
// https://www.kylegann.com/tuning.html
float JustPitches[MaxNote + 1];
void setupJustPitches(uint8_t baseNote, float basePitch) {
JustPitches[baseNote + 0] = basePitch * 1 / 1;
JustPitches[baseNote + 1] = basePitch * 16 / 15;
JustPitches[baseNote + 2] = basePitch * 9 / 8;
JustPitches[baseNote + 3] = basePitch * 6 / 5;
JustPitches[baseNote + 4] = basePitch * 5 / 4;
JustPitches[baseNote + 5] = basePitch * 4 / 3;
JustPitches[baseNote + 6] = basePitch * 45 / 32;
JustPitches[baseNote + 7] = basePitch * 3 / 2;
JustPitches[baseNote + 8] = basePitch * 8 / 5;
JustPitches[baseNote + 9] = basePitch * 5 / 3;
JustPitches[baseNote + 10] = basePitch * 9 / 5;
JustPitches[baseNote + 11] = basePitch * 15 / 8;
// Octaves
for (int note = baseNote; note < baseNote + 12; note++) {
for (int i = 1; i < 9; i++) {
int multiplier = 1<<i;
int shift = i*12;
int upNote = note + shift;
int dnNote = note - shift;
if (upNote <= MaxNote) {
JustPitches[upNote] = JustPitches[note] * multiplier;
}
if (dnNote >= 0) {
JustPitches[dnNote] = JustPitches[note] / multiplier;
}
}
}
}
void loadPatch(Patch p) {
for (int i=0; i<4; i++) {
Operator op = p.operators[i];
oscs[i]->amplitude(op.gain);
envs[i]->delay(op.delay);
envs[i]->attack(op.attack);
envs[i]->hold(op.hold);
envs[i]->decay(op.decay);
envs[i]->sustain(op.sustain);
envs[i]->release(op.release);
}
feedback.gain(0, p.feedback);
}
void setup(){
void setup() {
setupJustPitches(NOTE_D4, PITCH_D4);
pinMode(LED_BUILTIN, OUTPUT);
@ -117,31 +104,6 @@ void setup(){
AudioMemoryUsageMaxReset();
}
void setPitch(float freq) {
for (int i=0; i<4; i++) {
Operator op = Bank[currentPatch].operators[i];
oscs[i]->frequency(op.baseFrequency + freq*op.multiplier);
}
}
void noteOn(float freq) {
AudioNoInterrupts();
for (int i=0; i<4; i++) {
Operator op = Bank[currentPatch].operators[i];
oscs[i]->frequency(op.baseFrequency + freq*op.multiplier);
envs[i]->noteOn();
}
AudioInterrupts();
}
void noteOff() {
AudioNoInterrupts();
for (int i=0; i<4; i++) {
envs[i]->noteOff();
}
AudioInterrupts();
}
#define BUTTON_UP 0
#define BUTTON_DOWN 8
#define BUTTON_PITCH 24
@ -207,13 +169,13 @@ void updateTunables(uint8_t buttons, int note) {
int bankSize = sizeof(Bank) / sizeof(Bank[0]);
patch = (patch + bankSize) % bankSize;
Patch p = Bank[patch];
loadPatch(p);
FMPatch *p = &Bank[patch];
FMVoiceLoadPatch(&Chanter, p);
oled.clear(PAGE);
oled.setFontType(0);
oled.setCursor(0, 0);
oled.print(p.name);
oled.print(p->name);
oled.setCursor(0, 10);
oled.print("Patch ");
oled.print(patch);
@ -290,7 +252,7 @@ void loop() {
}
if (silent) {
noteOff();
FMVoiceNoteOff(&Chanter);
playing = false;
} else {
// Calculate pitch, and glissando pitch
@ -310,9 +272,9 @@ void loop() {
}
if (playing) {
setPitch(pitch);
FMVoiceSetPitch(&Chanter, pitch);
} else {
noteOn(pitch);
FMVoiceNoteOn(&Chanter, pitch);
}
playing = true;
}