Your first synth#
Goal: build a polyphonic synthesiser, attach it behind a sound, and play notes on it.
Up to now every sound has come from a file or a hand-built DSP source. A
YSE::synth is different: it owns a pool of voices and turns
note-on / note-off events into sound, handling polyphony, voice allocation
and voice stealing for you. This tutorial walks through
Demo18_FMKeyboard — a 16-voice FM synth played from a MIDI keyboard or
the console note keys.
Source: Demo18_FMKeyboard.cpp.
Mental model#
Three objects cooperate:
A voice prototype — any
YSE::SYNTH::dspVoicesubclass (hereYSE::SYNTH::fmVoice). It describes what one note sounds like. The synth clones it once per voice.The synth —
YSE::synth. It owns the cloned voices and the keyboard state, and receives yournoteOn/noteOffcalls.A sound — an ordinary positioned
YSE::sound. The synth is rendered behind it, so 3D panning, channels and reverb all apply exactly as they do to a file-backed sound.
Building the synth#
Create a prototype voice, hand it to synth::addVoices to size the pool,
then attach the synth to a sound and start it:
proto_ = std::make_unique<YSE::SYNTH::fmVoice>();
proto_->setPatch(bank_.voices[patchIndex_]);
synth_ = std::make_unique<YSE::synth>();
synth_->create().addVoices(*proto_, 16);
sound_ = std::make_unique<YSE::sound>();
sound_->create(*synth_);
sound_->play();
create() registers the synth with the engine. addVoices(prototype,
16) clones the prototype 16 times — that is the polyphony. Cloning happens
off the audio thread on the engine’s setup pool, so the synth becomes
playable a moment after addVoices returns, exactly like a file-backed
sound is not playable until its buffer finishes loading. sound::create
calls synth::create for you if you have not, and takes the same optional
channel and volume arguments as any other sound.
Note
The prototype must outlive addVoices — the engine reads it to clone
but neither copies nor owns it. In the demo it is a member
(proto_), so it lives as long as the synth.
Playing notes#
Once the synth is playing, drive it with note events. noteOn /
noteOff take a MIDI channel (1–16, or 0 for omni), a note number, and a
velocity in [0, 1]:
void DemoFMKeyboard::PlayChord() {
if (!available_) return;
for (int n : kChord)
synth_->noteOn(kMidiChannel, n, 0.8f);
chordDown_ = true;
}
void DemoFMKeyboard::ReleaseChord() {
if (!available_) return;
for (int n : kChord)
synth_->noteOff(kMidiChannel, n);
chordDown_ = false;
allNotesOff(channel) releases everything on a channel (0 = all
channels); the voices enter their normal release, they are not cut.
Tearing it down safely#
A synth is attached to a sound, and the sound must stop and be reclaimed before the synth (and then the prototype) go away — the same lifetime discipline a file-backed sound follows. The demo stops the sound, pumps a few engine updates so the slow pool reclaims it, then releases the synth:
if (synth_) synth_->allNotesOff();
if (sound_) sound_->stop();
// Let the sound release and the slow pool reclaim it before the synth (and
// then the prototype) go away, honouring the synth lifetime contract.
for (int i = 0; i < 5; i++) {
YSE::System().update();
YSE::System().sleep(20);
}
sound_.reset();
for (int i = 0; i < 5; i++) {
YSE::System().update();
YSE::System().sleep(20);
}
synth_.reset();
proto_.reset();
What you learned#
A synth is a pool of voices behind an ordinary positioned sound.
create().addVoices(prototype, n)sizes the polyphony; the prototype must outlive the call.Drive it with
noteOn/noteOff/allNotesOff.Destroy the sound before the synth before the prototype.
Next#
Writing a custom dspVoice — write your own voice from scratch.
Loading SFZ instruments and DX7 banks — load SFZ instruments and DX7 banks.
Per-note 3D: position handlers and swarms — give every note its own position.
Synthesis — the full synth API reference.