Better-defined voices, to prepare for polyphony
This commit is contained in:
parent
5ad56d8e92
commit
706a00b28f
|
@ -0,0 +1,8 @@
|
||||||
|
BOARD = --board adafruit:samd:adafruit_trellis_m4
|
||||||
|
|
||||||
|
verify: uilleann.ino
|
||||||
|
arduino --verify $(BOARD) $<
|
||||||
|
|
||||||
|
install: uilleann.ino
|
||||||
|
arduino --upload $(BOARD) $<
|
||||||
|
|
|
@ -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}, \
|
||||||
|
}
|
|
@ -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
11
notes.h
|
@ -1,3 +1,7 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#define PITCH_D4 293.66
|
||||||
|
|
||||||
enum Notes {
|
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_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,
|
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 uint8_t MaxNote = NOTE_B8;
|
||||||
|
|
||||||
const char *NoteNames[] {
|
extern const char *NoteNames[];
|
||||||
"C ", "C#", "D ", "Eb", "E ", "F ", "F#", "G ", "Ab", "A ", "Bb", "B ",
|
extern float JustPitches[MaxNote + 1];
|
||||||
};
|
|
||||||
|
|
||||||
#define PITCH_D4 293.66
|
void setupJustPitches(uint8_t baseNote, float basePitch);
|
||||||
|
|
53
patches.h
53
patches.h
|
@ -1,50 +1,39 @@
|
||||||
typedef struct Operator {
|
// "Factory" patches
|
||||||
float gain;
|
|
||||||
float delay;
|
|
||||||
float attack;
|
|
||||||
float hold;
|
|
||||||
float decay;
|
|
||||||
float sustain;
|
|
||||||
float release;
|
|
||||||
float baseFrequency;
|
|
||||||
float multiplier;
|
|
||||||
} Operator;
|
|
||||||
|
|
||||||
typedef struct Patch {
|
#pragma once
|
||||||
char *name;
|
#include "dx9.h"
|
||||||
Operator operators[4];
|
|
||||||
float feedback;
|
|
||||||
} Patch;
|
|
||||||
|
|
||||||
Patch Bank[] = {
|
// Waveform, offset, multiplier, delay, attack, holdAmp, hold, decay, sustainAmp, release
|
||||||
|
FMPatch Bank[] = {
|
||||||
{
|
{
|
||||||
"Venus Oboe",
|
"Venus Oboe",
|
||||||
|
DX9_ALG_5(0),
|
||||||
{
|
{
|
||||||
{1.0, 0, 10.5, 0, 5000, 0.75, 5.0, 0, 1.00},
|
// Waveform off mult del att hldA hld dec susA rel
|
||||||
{1.0, 0, 10.5, 0, 2000, 0.80, 5.0, 0, 4.00},
|
{WAVEFORM_SINE, 0, 1.00, 0, 10.5, 1.0, 10.5, 0, 0.75, 5},
|
||||||
{0.0, 0, 10.5, 0, 2000, 0.50, 5.0, 0, 8.00},
|
{WAVEFORM_SINE, 0, 4.00, 0, 10.5, 1.0, 10.5, 0, 0.80, 5},
|
||||||
{0.0, 0, 50.0, 0, 800, 0.75, 5.0, 0, 16.00},
|
{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",
|
"IWantPizza",
|
||||||
|
DX9_ALG_1(0),
|
||||||
{
|
{
|
||||||
{1.0, 0, 10.5, 0, 5000, 0.35, 100, 0, 4.00},
|
{WAVEFORM_SINE, 0, 4.00, 0, 10.5, 1.0, 10.5, 0, 0.35, 20},
|
||||||
{1.0, 0, 10.5, 0, 2000, 0.30, 100, 0, 1.00},
|
{WAVEFORM_SINE, 0, 1.00, 0, 10.5, 1.0, 10.5, 0, 0.30, 20},
|
||||||
{1.0, 0, 10.5, 0, 2000, 0.50, 100, 0, 8.00},
|
{WAVEFORM_SINE, 0, 8.00, 0, 10.5, 1.0, 10.5, 0, 0.50, 20},
|
||||||
{1.0, 0, 200, 0, 800, 0.25, 100, 0, 16.00},
|
{WAVEFORM_SINE, 0, 16.00, 0, 10.5, 1.0, 50, 0, 0.25, 20},
|
||||||
},
|
},
|
||||||
0.0,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Ray Gun",
|
"Ray Gun",
|
||||||
|
DX9_ALG_1(0),
|
||||||
{
|
{
|
||||||
{1.0, 0, 10.5, 0, 5000, 0.35, 2000, 0, 1.00},
|
{WAVEFORM_SINE, 0, 1.00, 0, 10.5, 1.0, 10.5, 0, 0.35, 20},
|
||||||
{1.0, 0, 10.5, 0, 2000, 0.30, 2000, 0, 1.00},
|
{WAVEFORM_SINE, 0, 1.00, 0, 10.5, 1.0, 10.5, 0, 0.30, 20},
|
||||||
{1.0, 0, 10.5, 0, 2000, 0.00, 2000, 0, 9.00},
|
{WAVEFORM_SINE, 0, 9.00, 0, 10.5, 1.0, 10.5, 0, 0.00, 20},
|
||||||
{1.0, 0, 200, 0, 800, 0.25, 800, 0, 1.00},
|
{WAVEFORM_SINE, 0, 1.00, 0, 10.5, 1.0, 50, 0, 0.25, 8},
|
||||||
},
|
},
|
||||||
0.0,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
163
synth.h
|
@ -1,38 +1,135 @@
|
||||||
|
#pragma once
|
||||||
#include <Audio.h>
|
#include <Audio.h>
|
||||||
#include <Wire.h>
|
#include <Wire.h>
|
||||||
#include <SPI.h>
|
#include <SPI.h>
|
||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
|
|
||||||
// GUItool: begin automatically generated code
|
#define NUM_OPERATORS 4
|
||||||
AudioMixer4 feedback; //xy=110,37
|
|
||||||
AudioSynthWaveformSineModulated osc4; //xy=112,98
|
/** FMOperator defines all settable paramaters to an operator.
|
||||||
AudioSynthWaveformSineModulated osc2; //xy=112,194
|
*
|
||||||
AudioSynthWaveformSineModulated osc1; //xy=112,245
|
* An FM operator consists of:
|
||||||
AudioSynthWaveformSineModulated osc3; //xy=113,146
|
* - An input
|
||||||
AudioMixer4 mixOp; //xy=114,418
|
* - An oscillator
|
||||||
AudioEffectEnvelope env4; //xy=251,97
|
* - An envelope generator
|
||||||
AudioEffectEnvelope env3; //xy=251,146
|
*
|
||||||
AudioEffectEnvelope env2; //xy=252,194
|
* Frequency Modulation happens by chaining oscillators together,
|
||||||
AudioEffectEnvelope env1; //xy=252,245
|
* using the output of one to modulate the frequency of the next.
|
||||||
AudioFilterBiquad biquad1; //xy=257,418
|
*
|
||||||
AudioMixer4 mixL; //xy=472,402
|
* Oscillators generate waveforms in a shape defined by
|
||||||
AudioMixer4 mixR; //xy=473,498
|
* `synth_waveform.h`. WAVEFORM_SINE is a good one to start with.
|
||||||
AudioOutputAnalogStereo dacs1; //xy=724,452
|
* Other sensible options are SAWTOOTH, SQUARE, and TRIANGLE.
|
||||||
AudioConnection patchCord1(feedback, osc4);
|
*
|
||||||
AudioConnection patchCord2(osc4, env4);
|
* Frequency for an oscillator is calculated with:
|
||||||
AudioConnection patchCord3(osc4, 0, feedback, 0);
|
* offset + (voiceFrequency × multiplier)
|
||||||
AudioConnection patchCord4(osc2, env2);
|
*
|
||||||
AudioConnection patchCord5(osc1, env1);
|
* Oscillator frequency is then modulated by the level obtained
|
||||||
AudioConnection patchCord6(osc3, env3);
|
* by the input mixer: level of 1.0 shifts frequency up by
|
||||||
AudioConnection patchCord7(mixOp, biquad1);
|
* 8 octaves, level of -1.0 shifts frequency down by 8 octaves.
|
||||||
AudioConnection patchCord8(env4, osc3);
|
*
|
||||||
AudioConnection patchCord9(env4, 0, mixOp, 3);
|
* The envelope modifies amplitude of the oscillator output,
|
||||||
AudioConnection patchCord10(env3, 0, mixOp, 2);
|
* using the following rules:
|
||||||
AudioConnection patchCord11(env2, osc1);
|
* - stay at 0 until Note On
|
||||||
AudioConnection patchCord12(env2, 0, mixOp, 1);
|
* - stay at 0 for `delayTime` milliseconds
|
||||||
AudioConnection patchCord13(env1, 0, mixOp, 0);
|
* - linear increase to `holdAmplitude` for `attackTime` milliseconds
|
||||||
AudioConnection patchCord14(biquad1, 0, mixL, 0);
|
* - stay at `holdAmplitude` for `holdTime` milliseconds
|
||||||
AudioConnection patchCord15(biquad1, 0, mixR, 0);
|
* - linear decrease to `sustainAmplitude` for `decayTime` milliseconds
|
||||||
AudioConnection patchCord17(mixL, 0, dacs1, 0);
|
* - stay at `sustainAmplitude` until Note Off
|
||||||
AudioConnection patchCord18(mixR, 0, dacs1, 1);
|
* - linear decrease to 0 for `releaseTime` milliseconds
|
||||||
// GUItool: end automatically generated code
|
*/
|
||||||
|
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);
|
||||||
|
|
|
@ -12,11 +12,47 @@
|
||||||
#define KNEE_OFFSET 0
|
#define KNEE_OFFSET 0
|
||||||
#define KEY_OFFSET 2
|
#define KEY_OFFSET 2
|
||||||
|
|
||||||
float cmaj_low[8] = { 130.81, 146.83, 164.81, 174.61, 196.00, 220.00, 246.94, 261.63 };
|
FMVoice Chanter;
|
||||||
float cmaj_high[8] = { 261.6, 293.7, 329.6, 349.2, 392.0, 440.0, 493.9, 523.3 };
|
FMVoice Drones[3];
|
||||||
|
FMVoice Regulators[3];
|
||||||
|
|
||||||
AudioEffectEnvelope *envs[] = {&env1, &env2, &env3, &env4};
|
AudioFilterBiquad biquad1;
|
||||||
AudioSynthWaveformSineModulated *oscs[] = {&osc1, &osc2, &osc3, &osc4};
|
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;
|
int currentPatch = 0;
|
||||||
|
|
||||||
|
@ -25,55 +61,6 @@ Adafruit_NeoTrellisM4 trellis = Adafruit_NeoTrellisM4();
|
||||||
MicroOLED oled(9, 1);
|
MicroOLED oled(9, 1);
|
||||||
QwiicButton bag;
|
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);
|
setupJustPitches(NOTE_D4, PITCH_D4);
|
||||||
|
@ -117,31 +104,6 @@ void setup(){
|
||||||
AudioMemoryUsageMaxReset();
|
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_UP 0
|
||||||
#define BUTTON_DOWN 8
|
#define BUTTON_DOWN 8
|
||||||
#define BUTTON_PITCH 24
|
#define BUTTON_PITCH 24
|
||||||
|
@ -207,13 +169,13 @@ void updateTunables(uint8_t buttons, int note) {
|
||||||
int bankSize = sizeof(Bank) / sizeof(Bank[0]);
|
int bankSize = sizeof(Bank) / sizeof(Bank[0]);
|
||||||
patch = (patch + bankSize) % bankSize;
|
patch = (patch + bankSize) % bankSize;
|
||||||
|
|
||||||
Patch p = Bank[patch];
|
FMPatch *p = &Bank[patch];
|
||||||
loadPatch(p);
|
FMVoiceLoadPatch(&Chanter, p);
|
||||||
|
|
||||||
oled.clear(PAGE);
|
oled.clear(PAGE);
|
||||||
oled.setFontType(0);
|
oled.setFontType(0);
|
||||||
oled.setCursor(0, 0);
|
oled.setCursor(0, 0);
|
||||||
oled.print(p.name);
|
oled.print(p->name);
|
||||||
oled.setCursor(0, 10);
|
oled.setCursor(0, 10);
|
||||||
oled.print("Patch ");
|
oled.print("Patch ");
|
||||||
oled.print(patch);
|
oled.print(patch);
|
||||||
|
@ -290,7 +252,7 @@ void loop() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (silent) {
|
if (silent) {
|
||||||
noteOff();
|
FMVoiceNoteOff(&Chanter);
|
||||||
playing = false;
|
playing = false;
|
||||||
} else {
|
} else {
|
||||||
// Calculate pitch, and glissando pitch
|
// Calculate pitch, and glissando pitch
|
||||||
|
@ -310,9 +272,9 @@ void loop() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playing) {
|
if (playing) {
|
||||||
setPitch(pitch);
|
FMVoiceSetPitch(&Chanter, pitch);
|
||||||
} else {
|
} else {
|
||||||
noteOn(pitch);
|
FMVoiceNoteOn(&Chanter, pitch);
|
||||||
}
|
}
|
||||||
playing = true;
|
playing = true;
|
||||||
}
|
}
|
Loading…
Reference in New Issue