Make a synth
MIDI notes in, sound out.A device that listens for MIDI notes and plays them. Three pieces: an oscillator (the tone), an envelope (shapes the volume so notes don't click), and reading MIDI.
Do Make a wavefolder first if you
haven't, so the describe and process shape is familiar.
A device that takes notes
Give it a MIDI input and an audio output, plus a few fields to hold state between blocks.
class Node extends Device {
private osc: Oscillator | null = null; // the tone
private env: Envelope | null = null; // the volume shape
private freq: f32 = 440; // the note we're on
describe(): Manifest {
const m = new Manifest("Synth");
m.addPort(midiIn("MIDI"));
m.addPort(audioOut("Out", 2));
return m;
}
}Build them in prepare
The oscillator and envelope get set up before audio starts. Build in prepare, use
in process.
prepare(sampleRate: f64, maxBlock: i32): void {
this.osc = new Oscillator(<f32>sampleRate);
this.env = new Envelope(<f32>sampleRate);
}Read the notes
Walk the MIDI events from this block. Note-on sets the pitch and opens the envelope; note-off releases it.
const count = ctx.midiInCount();
for (let e = 0; e < count; e++) {
const m = ctx.midiEvent(e); // typed; a note-on with velocity 0 reads as note-off
if (m.isNoteOn()) {
this.freq = noteToFreq(<f32>m.note);
env.noteOn(<f32>1.0, <f32>0.01, <f32>0.0, <f32>0.0);
} else if (m.isNoteOff()) {
env.noteOff(<f32>0.4, <f32>0.0);
}
}Make the sound
The envelope gives a volume that opens on note-on and falls on note-off; the oscillator gives the tone. Multiply, send to both speakers.
for (let i = 0; i < ctx.n; i++) {
const amp = env.tick(<f32>0.0, <f32>0.0, <f32>1.0, <f32>0.0); // full while held
const s = osc.process(this.freq) * amp;
ctx.output(0, i, s);
ctx.output(1, i, s);
}Save, route a MIDI clip in, press play.
The finished device
import {
Device,
Manifest,
Ctx,
Oscillator,
Envelope,
noteToFreq,
midiIn,
audioOut,
register,
} from "@msh/device-sdk/device/assembly";
class Node extends Device {
private osc: Oscillator | null = null;
private env: Envelope | null = null;
private freq: f32 = 440;
describe(): Manifest {
const m = new Manifest("Synth");
m.addPort(midiIn("MIDI"));
m.addPort(audioOut("Out", 2));
return m;
}
prepare(sampleRate: f64, maxBlock: i32): void {
this.osc = new Oscillator(<f32>sampleRate);
this.env = new Envelope(<f32>sampleRate);
}
process(ctx: Ctx): void {
const osc = this.osc;
const env = this.env;
if (osc == null || env == null) return;
const count = ctx.midiInCount();
for (let e = 0; e < count; e++) {
const m = ctx.midiEvent(e);
if (m.isNoteOn()) {
this.freq = noteToFreq(<f32>m.note);
env.noteOn(<f32>1.0, <f32>0.01, <f32>0.0, <f32>0.0);
} else if (m.isNoteOff()) {
env.noteOff(<f32>0.4, <f32>0.0);
}
}
for (let i = 0; i < ctx.n; i++) {
const amp = env.tick(<f32>0.0, <f32>0.0, <f32>1.0, <f32>0.0);
const s = osc.process(this.freq) * amp;
ctx.output(0, i, s);
ctx.output(1, i, s);
}
}
}
register(new Node());
export * from "@msh/device-sdk/device/assembly";This plays one note at a time. From here: add a waveform setting
(osc.setWaveform(Waveform.Saw)), run the output through a
filter, or hold chords with a
VoiceAllocator and a bank of oscillators.