K

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

index.ts
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.