Synthesis

Contents

Synthesis#

The synth subsystem turns libYSE from a sample player into a polyphonic instrument host. A YSE::synth owns a pool of voices, note allocation, voice stealing and full keyboard state (pedals, controllers, pitch wheel, aftertouch); a voice — a YSE::SYNTH::dspVoice subclass — owns only what a single note sounds like. The synth is rendered behind an ordinary positioned YSE::sound, so everything the spatial engine already does (3D panning, channels, reverb) applies to it.

See the tutorials for worked, compilable walk-throughs: Your first synth, Writing a custom dspVoice, Loading SFZ instruments and DX7 banks, and Per-note 3D: position handlers and swarms.

Note

The instrument DSP below — the SFZ sampler voice and the DX7-class FM voice — is always compiled into libyse; there is no YSE_ENABLE_FM / YSE_ENABLE_SFZ switch. The factory sounds these voices load (SFZ instruments and samples, and the DX7/FM .SYX banks) are not part of the library. They ship as an opt-in content pack fetched only when libYSE is configured with -DYSE_FETCH_CONTENT_PACK=ON (or python yse.py build --content-pack). The DX7 factory-style banks are tolerated, not legally cleared content — see the repository’s CONTENT-LICENSES.md and content/fm/dx7-factory/README.md.

The synth#

namespace YSE

Public API of libYSE — sound playback, mixing, and 3D positional audio.

Entry points: YSE::System (lifecycle and audio device), YSE::Listener (3D origin), YSE::sound (a playable source), YSE::channel (mixing tree), YSE::reverb (positioned reverb zone), YSE::patcher (modular DSP graph), YSE::player (note sequencer). Sub-namespaces group domain-specific types: YSE::DSP for signal processing, YSE::MIDI for MIDI I/O, YSE::MUSIC for note / chord / motif primitives.

Note

Apart from their constructors, the oscillator and vcf classes must only be invoked from inside a DSP callback / process body.

namespace SYNTH#

Every subSystem consists out of several classes which are meant to work together: an interface, implementation, manager, message and a message enumeration.

The synth subsystem mirrors the sound subsystem one-for-one.

Enums

enum MESSAGE#

The control events the interface can push onto an implementation’s lock-free inbox.

#153 wires up the note ops (NOTE_ON / NOTE_OFF / ALL_NOTES_OFF); the pedal / controller / wheel / aftertouch ops are declared here as the fixed contract but handled by #154 (keyboard state). There is deliberately no CALLBACK op — onNoteEvent is a direct atomic function pointer (#154), never a queued message.

Values:

enumerator NOTE_ON#
enumerator NOTE_OFF#
enumerator ALL_NOTES_OFF#
enumerator PITCH_WHEEL#
enumerator CONTROLLER#
enumerator AFTERTOUCH#
enumerator SUSTAIN#
enumerator SOSTENUTO#
enumerator SOFTPEDAL#
enumerator HANDLER_PARAM#
enumerator NOTE_POSITION#
namespace YSE

Public API of libYSE — sound playback, mixing, and 3D positional audio.

Entry points: YSE::System (lifecycle and audio device), YSE::Listener (3D origin), YSE::sound (a playable source), YSE::channel (mixing tree), YSE::reverb (positioned reverb zone), YSE::patcher (modular DSP graph), YSE::player (note sequencer). Sub-namespaces group domain-specific types: YSE::DSP for signal processing, YSE::MIDI for MIDI I/O, YSE::MUSIC for note / chord / motif primitives.

Note

Apart from their constructors, the oscillator and vcf classes must only be invoked from inside a DSP callback / process body.

namespace SYNTH

Every subSystem consists out of several classes which are meant to work together: an interface, implementation, manager, message and a message enumeration.

The synth subsystem mirrors the sound subsystem one-for-one.

class interfaceObject#

A polyphonic synthesiser voice pool rendered behind one YSE::sound.

Write YSE::synth (a typedef for this class). Build the pool from a prototype voice with addVoices, attach it behind a positioned YSE::sound with sound::create(synth&, ...), then drive it with noteOn / noteOff. The engine owns polyphony, allocation, voice stealing and lifecycle; a SYNTH::dspVoice subclass owns only what a single voice sounds like.

Like YSE::sound the interface is non-copyable: the implementation holds this object’s address, so the address must stay stable.

See also

YSE::SYNTH::dspVoice For user-subclassable voices.

See also

YSE::SYNTH::sineVoice The reference sine + ADSR voice.

Public Functions

interfaceObject()#
~interfaceObject()#
interfaceObject(const interfaceObject&) = delete#

Synths are non-copyable (the implementation holds our address).

interfaceObject &operator=(const interfaceObject&) = delete#
interfaceObject &create()#

Register this synth with the engine.

Must be called before addVoices / noteOn. Idempotent-safe to omit: sound::create(synth&, ...) calls it for you if you have not.

interfaceObject &name(const std::string &n)#

Assign a bus-addressable name to this synth.

Names the synth on the global named bus (issue #388, following the sound/channel pattern of #123) so live coders can drive it by string address rather than by C++ handle. Once named foo, the synth subscribes to (all payloads are list[float] unless noted; the full shape contract lives in docs/design/live_coding_dsl.md):

The subscribers run on the control thread and enqueue through the same RT-safe message inbox as the C++ setters — no new audio-thread surface. Anonymous synths (the default) are not addressable. Passing an empty string clears the name and removes the bus subscriptions. Renaming re-subscribes under the new name.

Two synths cannot share a name: the second name() is rejected and logged via the engine’s error path; the first registration wins. The bus is only live between System::init() and System::close() — naming a synth while the engine is down is a no-op.

Parameters:

n – The name, or "" to make the synth anonymous again.

Returns:

*this for fluent chaining.

interfaceObject &addVoices(dspVoice &prototype, int numVoices, int channel = 0, int lowestNote = 0, int highestNote = 127)#

Clone prototype numVoices times into a voice group.

The group responds to note numbers in [lowestNote, highestNote] on MIDI channel (0 = omni). May be called several times to build layered or split keyboards; a note may sound in more than one matching group. Cloning happens off the audio thread, on the engine’s setup pool, so the synth becomes playable a short moment after this returns (when it reaches OBJECT_READY) — exactly like a file-backed sound is not playable until its buffer finishes loading.

Warning

prototype must outlive the resulting setup — the engine reads it to clone but neither copies nor owns it. Call addVoices before the synth is attached and played.

interfaceObject &noteOn(int channel, int noteNumber, float velocity)#

Start a note.

velocity is normalised to [0, 1].

interfaceObject &noteOff(int channel, int noteNumber, float velocity = 0.f)#

Release a note.

interfaceObject &noteOn(const MUSIC::note &note)#

Start a note from a MUSIC::note (uses its pitch/volume/channel).

interfaceObject &noteOff(const MUSIC::note &note)#

Release the note matching a MUSIC::note (pitch/channel).

interfaceObject &allNotesOff(int channel = 0)#

Release every held note on channel (0 = all channels).

A bulk note-off: voices enter their normal release, they are not cut.

interfaceObject &pitchWheel(int channel, float value)#

Bend every voice on channel.

value is normalised to [-1, 1] (0 = centre). How far a voice bends is the voice’s concern.

interfaceObject &controller(int channel, int number, float value)#

Send a control-change.

value is normalised to [0, 1].

CC 64 / 66 / 67 act as the sustain / sostenuto / soft pedals; every other CC number is stored as the channel’s last controller value.

interfaceObject &aftertouch(int channel, int noteNumber, float value)#

Apply aftertouch pressure, normalised to [0, 1].

noteNumber == -1 is channel-wide (every voice on the channel); otherwise only the voice(s) sounding that note receive it.

interfaceObject &sustain(int channel, bool down)#

Sustain pedal (CC 64).

Down defers NOTE_OFF releases on the channel; up releases every note that was waiting on it.

interfaceObject &sostenuto(int channel, bool down)#

Sostenuto pedal (CC 66).

Down captures the currently-held notes and sustains only those; up releases them.

interfaceObject &softPedal(int channel, bool down)#

Soft pedal (CC 67).

While down, notes that START scale their velocity down; sounding voices are unaffected.

interfaceObject &positionHandler(YSE::SYNTH::positionHandler &prototype)#

Clone prototype once per voice slot as this synth’s position handler, giving every note its own 3D position and movement.

With no handler attached, every voice uses the synth’s aggregate position — so this call is purely additive. Ship-in handlers are SYNTH::staticHandler / SYNTH::randomSpreadHandler / SYNTH::orbitHandler, or derive your own from SYNTH::positionHandler.

Warning

prototype must outlive setup — the engine reads it to clone but neither copies nor owns it. Call before the synth is attached and played (rejected after setup, like addVoices).

interfaceObject &handlerParam(int index, float value)#

Set a shared handler parameter by index (e.g.

the swarm centre at indices 0..2). All of the synth’s live handlers read it next block. A bounded, allocation-free message — safe to call every control tick.

interfaceObject &notePosition(int channel, int noteNumber, const Pos &pos)#

Imperatively place the voice(s) sounding noteNumber on channel at pos (app-driven trajectories).

A bounded, allocation-free message. When a handler is attached it re-steers the voice next block, so this is primarily for the no-handler case.

Pos getVoicePosition(int channel, int noteNumber) const#

Current position of a voice sounding (channel, noteNumber), or the origin if none is.

Best-effort audio-thread snapshot for tests / metering.

interfaceObject &onNoteEvent(void (*func)(bool noteOn, float *noteNumber, float *velocity))#

Install an audio-thread note-rewrite hook, or clear it with nullptr.

func runs on the audio thread for every NOTE_ON / NOTE_OFF, before keyboard bookkeeping and allocation, and may rewrite *noteNumber and *velocity in place — the classic use is transposition, retuning, velocity curves or note filtering. Only a free function or captureless lambda is accepted (it carries no heap closure). It must obey the same real-time rules as a voice process(): no allocation, no locks, no blocking. See docs/design/synth_core.md §7.

int getNumVoices() const#

Total number of allocated voices across all groups.

bool isValid() const#

Whether this interface has a live implementation.

Private Functions

void registerOnBus()#
void unregisterFromBus()#

Private Members

implementationObject *pimpl = nullptr#
std::string _name#
std::uint64_t _busHandles[6] = {0, 0, 0, 0, 0, 0}#
bool _busOwner = {false}#

Friends

friend class YSE::sound
friend class SYNTH::implementationObject

The voice model#

Derive from YSE::SYNTH::dspVoice to define a custom voice, or use one of the built-in voices below.

namespace YSE

Public API of libYSE — sound playback, mixing, and 3D positional audio.

Entry points: YSE::System (lifecycle and audio device), YSE::Listener (3D origin), YSE::sound (a playable source), YSE::channel (mixing tree), YSE::reverb (positioned reverb zone), YSE::patcher (modular DSP graph), YSE::player (note sequencer). Sub-namespaces group domain-specific types: YSE::DSP for signal processing, YSE::MIDI for MIDI I/O, YSE::MUSIC for note / chord / motif primitives.

Note

Apart from their constructors, the oscillator and vcf classes must only be invoked from inside a DSP callback / process body.

namespace SYNTH

Every subSystem consists out of several classes which are meant to work together: an interface, implementation, manager, message and a message enumeration.

The synth subsystem mirrors the sound subsystem one-for-one.

class dspVoice : public YSE::DSP::dspSourceObject#

Base class for user-subclassable synthesiser voices.

Derive from dspVoice to define what a single note sounds like. The base is a DSP::dspSourceObject — it already provides the samples output buffers and the process(SOUND_STATUS&) entry point the sound renderer needs — extended with the pieces the voice allocator relies on: a clone() prototype hook, atomic frequency / velocity / aftertouch inputs, and a default atomic frequency() that stores the note as Hz.

The reference implementation is SYNTH::sineVoice (a sine shaped by an ADSR envelope). See docs/design/synth_core.md §3 for the full contract.

Subclassed by YSE::SYNTH::fmVoice, YSE::SYNTH::samplerVoice, YSE::SYNTH::sineVoice, YSE::SYNTH::vaVoice

Public Functions

inline dspVoice(int outputChannels = 1)#

Construct a voice with outputChannels output buffers.

inline virtual ~dspVoice()#
virtual void process(SOUND_STATUS &intent) override = 0#

Fill samples for one block.

You must implement this.

intent is this voice’s own SOUND_STATUS: SS_WANTSTOPLAY on note start, SS_WANTSTOSTOP on note release, etc. Honour it to drive your amplitude envelope, and settle it to SS_STOPPED once your release tail has finished so the allocator can free the slot.

Runs on the audio thread. Must be allocation-free, lock-free and non-blocking.

virtual dspVoice *clone() = 0#

Return a new, fully-allocated heap instance of your derived type.

You must implement this.

The typical body is return new MyVoice(*this); — a copy-construct. Every buffer, table and filter state the returned voice will touch in process() must already exist, so process() is thereafter allocation-free. Called only on the setup thread (never the audio thread); allocation here is fine.

inline virtual void frequency(Flt midiNote) override#

Set the note frequency from a MIDI note number.

Stored internally as Hz. Called by the allocator on NOTE_ON; a voice reads the result with getFrequency() in process().

inline Flt getFrequency() const#

Current note frequency in Hz.

inline void velocity(Flt v)#

Set the note velocity, normalised to [0, 1].

inline Flt getVelocity() const#

Current note velocity in [0, 1].

inline Flt getAftertouch() const#

Current aftertouch pressure in [0, 1].

inline Flt getPitchWheel() const#

Current pitch-wheel position in [-1, 1] for this voice’s channel.

The keyboard state machine forwards the channel’s pitch-wheel value to every voice sounding on that channel (and primes a new voice with the current value on note start). How far a voice bends for a given wheel position — the bend range in semitones — is the voice’s concern; the core only delivers the normalised position. See docs/design/synth_core.md §5.

Protected Functions

inline dspVoice(const dspVoice &o)#

Copy the note atomics (and base output buffers) into a fresh, independently-owned voice.

Provided so a derived clone() can be a plain copy-construct (new MyVoice(*this)): std::atomic members are not implicitly copyable, so the base supplies this. Each instance keeps its own atomics, so a clone shares no mutable note state with its prototype.

Private Members

aFlt _frequency = {440.f}#
aFlt _velocity = {0.f}#
aFlt _aftertouch = {0.f}#
aFlt _pitchWheel = {0.f}#

Friends

friend class implementationObject
namespace YSE

Public API of libYSE — sound playback, mixing, and 3D positional audio.

Entry points: YSE::System (lifecycle and audio device), YSE::Listener (3D origin), YSE::sound (a playable source), YSE::channel (mixing tree), YSE::reverb (positioned reverb zone), YSE::patcher (modular DSP graph), YSE::player (note sequencer). Sub-namespaces group domain-specific types: YSE::DSP for signal processing, YSE::MIDI for MIDI I/O, YSE::MUSIC for note / chord / motif primitives.

Note

Apart from their constructors, the oscillator and vcf classes must only be invoked from inside a DSP callback / process body.

namespace SYNTH

Every subSystem consists out of several classes which are meant to work together: an interface, implementation, manager, message and a message enumeration.

The synth subsystem mirrors the sound subsystem one-for-one.

class sineVoice : public YSE::SYNTH::dspVoice#

Reference voice — a sine oscillator gated by an ADSR envelope.

Pitched from getFrequency(), scaled by getVelocity() and shaped by a classic attack / decay / sustain / release envelope keyed off the SOUND_STATUS intent:

  • SS_WANTSTOPLAY restarts the oscillator phase and the envelope attack, then settles the intent to SS_PLAYING.

  • SS_PLAYING holds the sustain level.

  • SS_WANTSTOSTOP enters the release tail; when the tail reaches zero the voice settles the intent to SS_STOPPED.

Everything is allocated up front (in the constructor and the envelope setters, both off the audio thread) so process() and clone()’s copy stay allocation-clean on their respective threads.

Public Functions

sineVoice(int outputChannels = 1)#

Construct a mono (or outputChannels-wide) sine voice with a default ADSR.

sineVoice &attack(Flt seconds)#

Set the attack time in seconds.

sineVoice &decay(Flt seconds)#

Set the decay time in seconds.

sineVoice &sustain(Flt level)#

Set the sustain level in [0, 1].

sineVoice &release(Flt seconds)#

Set the release time in seconds.

inline Flt attack() const#

Current attack time in seconds.

inline Flt decay() const#

Current decay time in seconds.

inline Flt sustain() const#

Current sustain level in [0, 1].

inline Flt release() const#

Current release time in seconds.

virtual void process(SOUND_STATUS &intent) override#

Render one block, honouring and settling intent.

Audio-thread only.

virtual dspVoice *clone() override#

Return a new, independently-allocated copy of this voice.

Setup-thread only.

Protected Functions

sineVoice(const sineVoice &other)#

Copy-construct an independent voice (rebuilds its own envelope).

Private Types

enum Phase#

Values:

enumerator IDLE#
enumerator PLAYING#
enumerator RELEASING#

Private Functions

void buildEnvelope()#

Private Members

Flt _attack#
Flt _decay#
Flt _sustainLevel#
Flt _release#
DSP::sine osc#
std::unique_ptr<DSP::ADSRenvelope> env#
Phase phase#

Virtual-analog voice#

A multi-oscillator virtual-analog + wavetable voice with a Moog-style ladder filter and a live-editable shared patch.

namespace YSE

Public API of libYSE — sound playback, mixing, and 3D positional audio.

Entry points: YSE::System (lifecycle and audio device), YSE::Listener (3D origin), YSE::sound (a playable source), YSE::channel (mixing tree), YSE::reverb (positioned reverb zone), YSE::patcher (modular DSP graph), YSE::player (note sequencer). Sub-namespaces group domain-specific types: YSE::DSP for signal processing, YSE::MIDI for MIDI I/O, YSE::MUSIC for note / chord / motif primitives.

Note

Apart from their constructors, the oscillator and vcf classes must only be invoked from inside a DSP callback / process body.

namespace SYNTH

Every subSystem consists out of several classes which are meant to work together: an interface, implementation, manager, message and a message enumeration.

The synth subsystem mirrors the sound subsystem one-for-one.

Enums

enum VA_WAVEFORM#

Oscillator waveform modes for vaVoice.

Values:

enumerator VA_SAW#

Band-limited sawtooth.

enumerator VA_PULSE#

Band-limited pulse with variable width (PWM).

enumerator VA_TRIANGLE#

Band-limited triangle.

enumerator VA_SINE#

Sine.

enumerator VA_NOISE#

White noise.

enumerator VA_WAVETABLE#

Morph across the wavetable bank (see vaParams).

class vaADSR#

Lightweight real-time linear ADSR.

A minimal segment-based envelope used for both the amplitude and filter contours. Unlike DSP::ADSRenvelope (breakpoint list + an allocating generate()), this recomputes its per-sample slopes from the current patch times every block, so envelope times are live-settable with no allocation — exactly what the shared vaParams requires. Internal to vaVoice.

Public Functions

void configure(Flt attack, Flt decay, Flt sustain, Flt release, Flt sampleRate)#

(Re)compute per-sample slopes from times (seconds) + sustain.

void gateOn()#

Begin the attack segment (from the current level — no click).

void gateOff()#

Begin the release segment from the current level.

Flt tick()#

Advance one sample and return the new level.

void advance(int n)#

Advance n samples (block-rate use).

inline Flt level() const#

Current level in [0, 1].

inline bool idle() const#

Whether the envelope has returned to rest after a release.

inline void reset()#

Reset to rest (level 0).

Private Types

enum Stage#

Values:

enumerator IDLE#
enumerator ATTACK#
enumerator DECAY#
enumerator SUSTAIN#
enumerator RELEASE#

Private Members

Stage stage = IDLE#
Flt lvl = 0.f#
Flt sus = 0.7f#
Flt aInc = 1.f#
Flt dInc = 1.f#
Flt rSec = 0.1f#
Flt rInc = 1.f#
Flt sr = 44100.f#
class vaParams#

Live-settable patch shared by every voice of one synth.

A vaParams is the “sound” — all continuous parameters are atomics read on the audio thread, so they can be tweaked from a control thread while voices play, glitch-free (no allocation, no locks). All voices of a synth share one vaParams (the prototype’s, carried across clone() by shared pointer), while each voice keeps its own independent oscillator phase, filter state and envelope state.

Retain the shared pointer (vaVoice::patch()) if you want to keep editing the patch after the prototype has been handed to synth::addVoices and destroyed.

The wavetable bank (used by VA_WAVETABLE mode) is built once and is intended to be populated before playback via loadWavetable — like voice allocation, table (re)building is a setup-thread operation, not an audio-thread one. The morph position is the live control.

Public Functions

vaParams()#

Construct a sensible default patch (osc 1 saw, gentle filter).

vaParams(const vaParams&) = delete#
vaParams &operator=(const vaParams&) = delete#
void loadWavetable(int slot, const std::vector<Flt> &cycle)#

Install a single-cycle waveform into bank slot slot.

cycle is one period of a normalised waveform (any length — AKWF tables are 600 samples). Slots beyond the current bank size are appended. Setup-thread only — this allocates/reshapes table storage and must not be called while the voice is rendering.

inline int wavetableCount() const#

Number of tables currently in the morph bank.

Public Members

aInt oscWave[kNumOsc]#

VA_WAVEFORM per oscillator.

aFlt oscDetune[kNumOsc]#

Detune in semitones.

aFlt oscLevel[kNumOsc]#

Mix level in [0, 1].

aFlt oscPulseWidth[kNumOsc]#

Pulse width in (0, 1) for VA_PULSE.

aFlt wavetablePosition#

Morph position in [0, 1] for VA_WAVETABLE.

aFlt cutoff#

Base cutoff in Hz.

aFlt resonance#

Resonance in [0, 1].

aFlt keyTracking#

Key-follow in [0, 1] (0 = fixed, 1 = full tracking).

aFlt filterEnvAmount#

Filter-envelope depth in octaves (may be negative).

aFlt filterVelAmount#

Velocity → cutoff depth in octaves at full velocity.

aFlt ampAttack#
aFlt ampDecay#
aFlt ampSustain#
aFlt ampRelease#
aFlt ampVelAmount#

Velocity → amplitude amount in [0, 1].

aFlt filterAttack#
aFlt filterDecay#
aFlt filterSustain#
aFlt filterRelease#
aInt lfoType#

DSP::LFO_TYPE shape.

aFlt lfoRate#

LFO rate in Hz.

aFlt lfoToPitch#

LFO → pitch depth in semitones.

aFlt lfoToCutoff#

LFO → cutoff depth in octaves.

aFlt lfoToWavetable#

LFO → wavetable-position depth in [0, 1].

aFlt gain#

Master voice gain in [0, 1].

DSP::wavetable sawTable#
DSP::wavetable triTable#
DSP::wavetable sineTable#
std::vector<DSP::wavetable> wtBank#

Wavetable bank morphed across by VA_WAVETABLE mode.

Defaults to a small set of single-cycle shapes so morph works out of the box. Populate with AKWF-style single-cycle tables via loadWavetable before playback.

Public Static Attributes

static const int kNumOsc = 3#

Number of oscillators per voice.

class vaVoice : public YSE::SYNTH::dspVoice#

Virtual-analog + wavetable synthesiser voice.

Derives SYNTH::dspVoice and consumes the synth-core voice contract (intent-driven lifecycle, clone() prototype, atomic note inputs). All tone parameters live in a shared vaParams patch; construct one prototype, dial in the patch, then hand it to synth::addVoices — the clones share the patch and stay editable through patch().

Everything the audio thread needs is allocated up front (tables in the patch, per-voice buffers/filter/LFO in the constructor), so process and clone’s copy stay allocation-free on their respective threads.

Public Functions

vaVoice(int outputChannels = 1)#

Construct a voice with a fresh default patch.

inline std::shared_ptr<vaParams> patch() const#

The shared, live-settable patch.

Retain to keep editing after the prototype is gone.

inline vaParams &parameters()#

Convenience reference to the shared patch.

virtual void process(SOUND_STATUS &intent) override#

Render one block, honouring and settling intent.

Audio-thread only.

virtual dspVoice *clone() override#

Return a new voice sharing this voice’s patch, with fresh DSP state.

Setup-thread only.

Protected Functions

vaVoice(const vaVoice &other)#

Copy-construct: share the patch, rebuild independent DSP state.

Private Types

enum Phase#

Values:

enumerator IDLE#
enumerator PLAYING#
enumerator RELEASING#

Private Functions

Flt renderOsc(int index, Dbl phase, Flt pulseWidth, Flt wtPos)#

Private Members

std::shared_ptr<vaParams> params#
Dbl oscPhase[vaParams::kNumOsc]#
UInt noiseState#
DSP::lfo lfoOsc#
vaADSR ampEnv#
vaADSR filEnv#
DSP::ladderFilter filter#
Phase phase#

Private Static Functions

static Flt readTable(DSP::wavetable &t, Dbl phase)#

SFZ sampler voice#

Loads an SFZ instrument (region map + resident PCM) and plays it as a multi-layer sampler voice. See Loading SFZ instruments and DX7 banks.

namespace YSE

Public API of libYSE — sound playback, mixing, and 3D positional audio.

Entry points: YSE::System (lifecycle and audio device), YSE::Listener (3D origin), YSE::sound (a playable source), YSE::channel (mixing tree), YSE::reverb (positioned reverb zone), YSE::patcher (modular DSP graph), YSE::player (note sequencer). Sub-namespaces group domain-specific types: YSE::DSP for signal processing, YSE::MIDI for MIDI I/O, YSE::MUSIC for note / chord / motif primitives.

Note

Apart from their constructors, the oscillator and vcf classes must only be invoked from inside a DSP callback / process body.

namespace SYNTH

Every subSystem consists out of several classes which are meant to work together: an interface, implementation, manager, message and a message enumeration.

The synth subsystem mirrors the sound subsystem one-for-one.

struct residentSample#

One fully-resident, de-duplicated sample (all channels in RAM).

Built once on the setup / slow-pool thread by samplerInstrument::load and immutable thereafter, so any number of voices read it concurrently on the audio thread without synchronisation (spec §10). Parallel to sfzInstrument::samples (index == sampleIndex).

Public Members

std::vector<DSP::fileBuffer> channels#

One buffer per source channel.

long frames = 0#

Frame count (per channel).

Flt sampleRateAdjustment = 1.0f#

fileRate / deviceRate (spec §6).

bool silence = false#

sample=*silence — produces silence, no PCM.

bool loaded = false#

PCM decoded (or silence), ready to render.

class samplerConfig#

Chainable single-sample convenience facade (spec §11).

Sugar that builds a one-region instrument without an .sfz file. It emits the same flattened region model the parser produces, so a samplerConfig sampler behaves identically to the equivalent hand-written one-region file.

Public Functions

inline samplerConfig &name(const char *n)#

Instrument label (identification only; not an SFZ opcode).

inline samplerConfig &file(const char *f)#

Absolute path to the sample file (sample=).

inline samplerConfig &channel(U8 c)#

MIDI channel for addVoices (0 = omni).

Not an SFZ opcode.

inline samplerConfig &root(U8 rootNote)#

Root note — the key that plays the sample untransposed.

inline samplerConfig &range(U8 low, U8 high)#

Playable key range (also the addVoices window).

inline samplerConfig &envelope(Flt attack, Flt release, Flt maxLength)#

Amplitude envelope: attack / release (seconds) and a one-shot length cap (seconds; clamps end when the region does not loop).

inline const std::string &name() const#
inline U8 channel() const#
inline U8 low() const#
inline U8 high() const#
bool build(samplerInstrument &inst) const#

Build the one-region model this facade describes into inst and decode its sample.

Setup thread only. Returns true on success.

Private Members

std::string name_#
std::string file_#
U8 channel_ = 0#
U8 root_ = 60#
U8 low_ = 0#
U8 high_ = 127#
Flt attack_ = 0.0f#
Flt release_ = 0.1f#
Flt maxLength_ = 10.0f#
class samplerInstrument#

The shared, playable SFZ instrument: region table + resident PCM.

All voices cloned from one prototype share one samplerInstrument (like vaParams for vaVoice): the immutable region table and PCM are read-only on the audio thread, while the small cross-note round-robin and choke coordination state is touched only inside the audio-thread render pass (single-threaded — plain ints, no atomics, no locks).

Public Functions

bool load()#

Decode every unique sample into RAM.

Setup / slow-pool thread only — never the audio thread. Returns true if the instrument is playable (valid region table + at least one loaded sample).

inline bool valid() const#

Whether this instrument is playable (valid region table + at least one resident sample).

Computed, so a caller may also build the model + samples in place without going through load.

uint32_t bumpChoke(int g)#

Bump the choke generation for group g (a region fired it).

Returns the new generation. Groups outside [1,127] clamp into range.

uint32_t chokeGen(int g) const#

Current choke generation for group g (0 if never fired).

Public Members

DSP::sfzInstrument model#

Flattened region table (immutable after load).

std::vector<residentSample> samples#

Resident PCM, parallel to model.samples.

std::array<int, 128> seqCounter = {}#

Per-key round-robin hit counter (spec §4).

Read+incremented at the NOTE_ON edge inside process(); the region table stays immutable.

Private Members

std::array<uint32_t, kMaxChokeGroups> chokeGenerations = {}#

Private Static Attributes

static constexpr int kMaxChokeGroups = 128#
class samplerVoice : public YSE::SYNTH::dspVoice#

SFZ sampler voice — renders the shared region table for one note.

Load an instrument (loadSFZ / configure) into the prototype, then hand it to synth::addVoices; every clone shares the same immutable region table and resident PCM (instrument()) while keeping its own independent per-layer playback state.

Public Functions

samplerVoice(int outputChannels = 1)#

Construct an empty mono (or wider) sampler voice.

bool loadSFZ(const std::string &path)#

Load and preload an .sfz file into this prototype.

Setup thread only (parses + decodes off the audio thread). Returns true when the instrument is playable.

bool configure(const samplerConfig &cfg)#

Build this prototype from a samplerConfig facade.

Setup thread only. Returns true when the instrument is playable.

inline std::shared_ptr<samplerInstrument> instrument() const#

The shared instrument (region table + resident PCM).

Retain to keep it alive alongside the prototype and its clones.

inline samplerVoice &setInstrument(std::shared_ptr<samplerInstrument> i)#

Attach an already-built instrument.

Clones made afterwards share it (the region table + PCM are never duplicated, spec §10). Setup thread only.

virtual void process(SOUND_STATUS &intent) override#

Render one block, honouring and settling intent.

Audio-thread only.

virtual dspVoice *clone() override#

Return a new voice sharing this voice’s instrument, fresh state.

Setup-thread only.

virtual void frequency(Flt midiNote) override#

Store the note frequency (Hz, base) and the raw MIDI note the region matcher needs.

Called by the allocator on NOTE_ON.

inline int getNote() const#

Current raw MIDI note number for region selection.

int activeLayers() const#

Number of layers currently sounding.

int layerRegion(int i) const#

Region-table index of sounding layer i (-1 if none).

Flt layerGain(int i) const#

Constant NOTE_ON amplitude gain of sounding layer i.

inline int lastHit() const#

The round-robin hit number chosen at the last NOTE_ON.

Protected Functions

samplerVoice(const samplerVoice &other)#

Copy-construct: share the instrument, reset per-voice state.

Private Types

enum Phase#

Values:

enumerator IDLE#
enumerator PLAYING#
enumerator RELEASING#

Private Functions

void startNote()#
void releaseNote()#
void armLayer(Layer &L, int regionIndex, int note, int velocity)#
bool renderLayers(int blockLen)#
void applyChoke()#

Private Members

std::shared_ptr<samplerInstrument> inst#
std::array<Layer, DSP::YSE_MAX_REGION_LAYERS> layers#
Phase phase = IDLE#
aInt note_ = {60}#
int lastHit_ = 0#
bool chokeFading = false#
int chokeFadePos = 0#
int chokeFadeSamps = 1#

Private Static Functions

static Flt cubic(const Flt *d, long n, double pos, long loopStart, long loopEnd, bool looping)#
struct Layer#

Public Members

bool active = false#
bool finished = false#
int regionIndex = -1#
const Flt *ch[2] = {nullptr, nullptr}#
int numCh = 0#
long frames = 0#
double pos = 0.0#
double baseSpeed = 1.0#
long offset = 0#
long endFrame = 0#
int loopMode = DSP::SFZ_NO_LOOP#
bool looping = false#
long loopStart = 0#
long loopEnd = 0#
bool sampleDone = false#
Flt gain = 1.0f#
sfzADSR env#
bool releasing = false#
int chokeGroup = 0#
int offBy = 0#
int offMode = DSP::SFZ_OFF_FAST#
uint32_t offByBaseline = 0#
class sfzADSR#

Amplitude EG for one layer — allocation-free DAHDSR.

The spec (§8) maps ampeg_* onto the engine’s DSP::ADSRenvelope, but that envelope’s generate() allocates, and a sampler voice is reused across notes that resolve to different regions with different ampeg_* values — so rebuilding it at NOTE_ON would allocate on the audio path. This lightweight envelope reconfigures its per-sample slopes with no allocation, exactly the reason vaVoice introduced vaADSR. It adds the delay + hold stages the full DAHDSR set needs.

Public Functions

void configure(Flt delay, Flt attack, Flt hold, Flt decay, Flt sustain, Flt release, Flt sr)#

(Re)configure from ampeg_* times (seconds), sustain [0,1].

void gateOn()#

Begin at the delay/attack stage from silence.

void gateOff()#

Enter the release stage from the current level.

Flt tick()#

Advance one sample; return the new level.

inline bool atEnd() const#

Whether the release has finished (envelope at rest after gateOff).

inline bool active() const#

Whether the envelope is producing sound (past IDLE, before DONE).

inline void reset()#

Reset to rest.

Private Types

enum Stage#

Values:

enumerator IDLE#
enumerator DELAY#
enumerator ATTACK#
enumerator HOLD#
enumerator DECAY#
enumerator SUSTAIN#
enumerator RELEASE#
enumerator DONE#

Private Members

Stage stage = IDLE#
Flt lvl = 0.0f#
Flt sus = 1.0f#
Flt sr = 44100.0f#
long delaySamps = 0#
long holdSamps = 0#
long delayCnt = 0#
long holdCnt = 0#
Flt aInc = 1.0f#
Flt dInc = 1.0f#
Flt rInc = 1.0f#
Flt rSecs = 0.1f#

FM voice and DX7 banks#

A DX7-class 6-operator FM voice driven by an YSE::SYNTH::fmPatch, plus the SysEx importer that fills patches from a vintage DX7 bank dump.

namespace YSE

Public API of libYSE — sound playback, mixing, and 3D positional audio.

Entry points: YSE::System (lifecycle and audio device), YSE::Listener (3D origin), YSE::sound (a playable source), YSE::channel (mixing tree), YSE::reverb (positioned reverb zone), YSE::patcher (modular DSP graph), YSE::player (note sequencer). Sub-namespaces group domain-specific types: YSE::DSP for signal processing, YSE::MIDI for MIDI I/O, YSE::MUSIC for note / chord / motif primitives.

Note

Apart from their constructors, the oscillator and vcf classes must only be invoked from inside a DSP callback / process body.

namespace SYNTH

Every subSystem consists out of several classes which are meant to work together: an interface, implementation, manager, message and a message enumeration.

The synth subsystem mirrors the sound subsystem one-for-one.

class fmVoice : public YSE::SYNTH::dspVoice#

DX7-class 6-operator FM voice.

Construct one, dial in its fmPatch (or load a DX7 voice into it via #177), then hand it to synth::addVoices. Clones share the patch and stay editable through patch(). Patch edits take effect on the next note-on (the FM core bakes operator parameters at key-down, exactly like the hardware reacting to a program change between notes).

Public Functions

fmVoice(int outputChannels = 1)#

Construct a voice with the built-in sine test patch.

~fmVoice() override#
inline std::shared_ptr<fmPatch> patch() const#

The shared, live-editable patch.

Retain to keep editing after the prototype is gone.

inline fmPatch &parameters()#

Convenience reference to the shared patch.

inline fmVoice &setPatch(const fmPatch &p)#

Overwrite the shared patch; applied on the next note-on.

virtual void process(SOUND_STATUS &intent) override#

Render one block, honouring and settling intent.

Audio-thread only.

virtual dspVoice *clone() override#

Return a new voice sharing this voice’s patch, with fresh DSP state.

Setup-thread only.

Protected Functions

fmVoice(const fmVoice &other)#

Copy-construct: share the patch, build fresh independent core state.

Private Functions

void startNote()#

Private Members

std::shared_ptr<fmPatch> params#
std::unique_ptr<fmVoiceState> state#
namespace YSE

Public API of libYSE — sound playback, mixing, and 3D positional audio.

Entry points: YSE::System (lifecycle and audio device), YSE::Listener (3D origin), YSE::sound (a playable source), YSE::channel (mixing tree), YSE::reverb (positioned reverb zone), YSE::patcher (modular DSP graph), YSE::player (note sequencer). Sub-namespaces group domain-specific types: YSE::DSP for signal processing, YSE::MIDI for MIDI I/O, YSE::MUSIC for note / chord / motif primitives.

Note

Apart from their constructors, the oscillator and vcf classes must only be invoked from inside a DSP callback / process body.

namespace SYNTH

Every subSystem consists out of several classes which are meant to work together: an interface, implementation, manager, message and a message enumeration.

The synth subsystem mirrors the sound subsystem one-for-one.

struct fmOperator#

One DX7 operator’s parameters (21 fields).

Maps to bytes [op*21 .. op*21+20] of the 156-byte unpacked voice. Operators are stored in DX7 voice order: op[0] is OP1 … op[5] is OP6, matching the algorithm routing tables in the ported core.

Public Members

uint8_t egRate[4]#

Envelope rates R1..R4 (0..99). [+0..3].

uint8_t egLevel[4]#

Envelope levels L1..L4 (0..99). [+4..7].

uint8_t levelScaleBreakPoint#

Keyboard level-scaling break point. [+8].

uint8_t levelScaleLeftDepth#

Level scaling, left depth (0..99). [+9].

uint8_t levelScaleRightDepth#

Level scaling, right depth (0..99). [+10].

uint8_t levelScaleLeftCurve#

Left curve (0..3: -lin,-exp,+exp,+lin).[+11].

uint8_t levelScaleRightCurve#

Right curve (0..3). [+12].

uint8_t rateScaling#

Keyboard rate scaling (0..7). [+13].

uint8_t ampModSens#

Amplitude-modulation sensitivity (0..3). [+14].

uint8_t keyVelSens#

Key-velocity sensitivity (0..7). [+15].

uint8_t outputLevel#

Operator output level (0..99). [+16].

uint8_t oscMode#

0 = frequency ratio, 1 = fixed frequency. [+17]

uint8_t freqCoarse#

Coarse frequency (0..31). [+18].

uint8_t freqFine#

Fine frequency (0..99). [+19].

uint8_t detune#

Detune (0..14, 7 = centre). [+20].

struct fmPatch#

A complete DX7 6-operator voice.

Plain data — no behaviour, no atomics — so it is trivially copyable and cheap to hand across the setup boundary. fmVoice snapshots one of these at construction and serialises it to the MSFA core on each note-on via toUnpacked; the shared, live-editable copy the voice keeps is the fmVoice patch (see fmVoice.hpp).

Built-in test voices (defined in code; #176 acceptance).

static fmPatch sine()#

A single unmodulated carrier — a pure sine (algorithm 32).

static fmPatch fm2op()#

A textbook 2-operator FM patch: OP1 modulated by OP2 at a 1:1 ratio (algorithm 5), producing a carrier plus FM sidebands.

static fmPatch brass()#

A fuller, brass-like patch exercising all six operators.

Public Functions

void toUnpacked(char dest[156]) const#

Serialise into the 156-byte unpacked voice the MSFA core reads.

dest must point to at least 156 bytes. This is the exact layout msfa::Dx7Note::init consumes; fmVoice calls it on note-on.

Public Members

fmOperator op[6]#

OP1..OP6. [0..125].

uint8_t pitchEgRate[4]#

Pitch envelope rates R1..R4 (0..99). [126..129].

uint8_t pitchEgLevel[4]#

Pitch envelope levels L1..L4 (0..99). [130..133].

uint8_t algorithm#

FM algorithm (0..31; DX7 shows 1..32). [134].

uint8_t feedback#

Feedback amount (0..7). [135].

uint8_t oscKeySync#

Oscillator key sync (0/1). [136].

uint8_t lfoSpeed#

LFO speed (0..99). [137].

uint8_t lfoDelay#

LFO delay (0..99). [138].

uint8_t lfoPitchModDepth#

LFO pitch-mod depth PMD (0..99). [139].

uint8_t lfoAmpModDepth#

LFO amp-mod depth AMD (0..99). [140].

uint8_t lfoSync#

LFO key sync (0/1). [141].

uint8_t lfoWaveform#

LFO waveform (0..5: tri,saw-d,saw-u,sqr,sine,s&h). [142].

uint8_t pitchModSens#

Pitch-mod sensitivity (0..7). [143].

uint8_t transpose#

Transpose in semitones (0..48, 24 = none). [144].

char name[10]#

Voice name, ASCII, space-padded. [145..154].

uint8_t opEnabled#

Operator on/off bitmask (bit n = OPn+1); 0x3f = all on. [155].

namespace YSE

Public API of libYSE — sound playback, mixing, and 3D positional audio.

Entry points: YSE::System (lifecycle and audio device), YSE::Listener (3D origin), YSE::sound (a playable source), YSE::channel (mixing tree), YSE::reverb (positioned reverb zone), YSE::patcher (modular DSP graph), YSE::player (note sequencer). Sub-namespaces group domain-specific types: YSE::DSP for signal processing, YSE::MIDI for MIDI I/O, YSE::MUSIC for note / chord / motif primitives.

Note

Apart from their constructors, the oscillator and vcf classes must only be invoked from inside a DSP callback / process body.

namespace SYNTH

Every subSystem consists out of several classes which are meant to work together: an interface, implementation, manager, message and a message enumeration.

The synth subsystem mirrors the sound subsystem one-for-one.

struct dx7Bank#

A parsed DX7 bank: the voices plus name lookup helpers.

A packed bulk dump yields 32 voices; a single-voice dump yields one. Plain data — safe to copy and hand across the setup boundary.

Public Functions

inline size_t size() const#

Number of voices in the bank.

inline bool empty() const#

True when no voice was parsed.

std::string name(size_t index) const#

Voice name at index, trimmed of trailing spaces.

Returns:

The name, or an empty string if index is out of range.

int indexOf(const char *voiceName) const#

First voice whose (trimmed) name equals voiceName.

Returns:

The voice index, or -1 if no voice matches.

Public Members

std::vector<fmPatch> voices#

Parsed voices, in bank order.

class dx7SysEx#

Stateless DX7 SysEx parser.

Offline / setup-thread only.

Public Static Functions

static bool parse(const uint8_t *data, size_t length, dx7Bank &out)#

Parse a DX7 SysEx image from memory.

Parameters:
  • data – Raw bytes (a .syx file image or an in-memory dump).

  • length – Number of bytes at data.

  • out – Filled with the parsed voices on success; untouched on failure.

Returns:

true on success. On failure logs E_FILE_ERROR and returns false (bad header, wrong length, or checksum mismatch).

static bool loadBank(const char *path, dx7Bank &out)#

Load and parse a DX7 .syx file.

Parameters:
  • path – UTF-8 path to the SysEx file.

  • out – Filled with the parsed voices on success.

Returns:

true on success; on failure logs E_FILE_ERROR (file not found / unreadable, or a parse error) and returns false.

Per-note 3D positioning#

Attach a YSE::SYNTH::positionHandler to give every voice its own 3D position and movement — the basis of the “swarm” effect. Steer the whole swarm live with YSE::synth::handlerParam(), or place a single note imperatively with YSE::synth::notePosition(). See Per-note 3D: position handlers and swarms.

namespace YSE

Public API of libYSE — sound playback, mixing, and 3D positional audio.

Entry points: YSE::System (lifecycle and audio device), YSE::Listener (3D origin), YSE::sound (a playable source), YSE::channel (mixing tree), YSE::reverb (positioned reverb zone), YSE::patcher (modular DSP graph), YSE::player (note sequencer). Sub-namespaces group domain-specific types: YSE::DSP for signal processing, YSE::MIDI for MIDI I/O, YSE::MUSIC for note / chord / motif primitives.

Note

Apart from their constructors, the oscillator and vcf classes must only be invoked from inside a DSP callback / process body.

namespace SYNTH

Every subSystem consists out of several classes which are meant to work together: an interface, implementation, manager, message and a message enumeration.

The synth subsystem mirrors the sound subsystem one-for-one.

class positionHandler#

Base class for user-subclassable per-note position behaviours.

Derive from positionHandler to decide where every note of a synth lives and how it moves — a static offset, a random scatter, a swarm orbiting a moving centre — without writing engine code per note. The returned Pos is the voice’s position in the same coordinate frame as a YSE::sound position; the engine feeds it straight into that voice’s panner (see docs/design/per_note_positioning.md §6/§7).

Lifecycle (mirrors the voice slot, §8/§10/§11). Attach a prototype with synth::positionHandler(proto). The engine clones it once per voice slot on the setup pool (via clone()) and reuses that one instance for every note the slot ever plays. When the allocator lands a note on a slot it calls noteOn(); every audio block until the voice’s release tail ends it calls update(); on the note-off edge it calls onRelease() once. The instance is never freed at note rate — it is re-seeded by the next noteOn().

Stealing (§11). Because one instance is permanently paired with a slot and reused, a stolen slot simply gets a fresh noteOn() for the new note. Therefore **noteOn() must establish the note’s COMPLETE initial state** (reset every phase / counter / RNG draw) — the instance may have just finished another note. Do not assume construction state.

Real-time discipline. noteOn(), update() and onRelease() run on the audio thread. They must not allocate, lock, block on I/O, or log — identical rules to a voice process(). Allocate everything the hooks touch in the constructor / clone() (which run only on the setup pool). Any state shared with another thread must arrive through the handler-param block or be atomic.

The shipped reference implementations are SYNTH::staticHandler, SYNTH::randomSpreadHandler and SYNTH::orbitHandler (the last is the swarm workhorse and the template for a custom handler).

See also

YSE::SYNTH::orbitHandler The swarm reference handler.

See also

YSE::synth::positionHandler To attach a prototype.

Subclassed by YSE::SYNTH::orbitHandler, YSE::SYNTH::randomSpreadHandler, YSE::SYNTH::staticHandler

Public Functions

inline virtual ~positionHandler()#
virtual positionHandler *clone() = 0#

Return a new, fully-allocated heap clone of your derived type.

You must implement this.

The typical body is return new MyHandler(*this);. Everything the hooks will touch must already exist in the returned object, so the hooks stay allocation-free. Called only on the setup pool (never the audio thread); allocation here is fine.

virtual Pos noteOn(const voiceContext &ctx) = 0#

A note begins on this handler’s slot.

Return its initial position. You must implement this.

Runs on the audio thread; allocation-free. Because the instance is reused across notes (and across voice steals), this hook must fully reinitialise every piece of per-note state it keeps.

virtual Pos update(const voiceContext &ctx, Flt delta) = 0#

Control-rate steer, called once per audio block while the note sounds (through its release tail).

Return the new position. You must implement this.

delta is the block’s duration in seconds (buffer length / samplerate), so advancing state by rate * delta is frame-rate independent. Runs on the audio thread; allocation-free.

inline virtual void onRelease(const voiceContext&)#

The note entered its release tail (key up / note-off edge).

Optional — the default is a no-op.

Position control CONTINUES through the tail (update() keeps being called until the voice stops), so a handler that ignores release keeps moving. Override to change behaviour for the fade (e.g. drift outward, slow an orbit). Called exactly once, on the edge. Audio thread; allocation-free.

class voiceContext#

Read-only view of one voice’s live note state, handed to a positionHandler on every hook.

The synth fills a fresh voiceContext on the stack for each hook call — it is never a heap object and the handler never owns it. Everything on it is a value or a same-thread read; nothing here allocates or locks. Read it to modulate position (e.g. velocity -> orbit radius, aftertouch -> swarm width, a control-change -> height); you cannot write note state through it.

Public Functions

inline Flt controller(int number) const#

Live value of control-change number on this voice’s channel, normalised to [0, 1].

Out-of-range numbers read 0. This is the synth forwarding a controller to the handler — same-thread read, no atomics.

inline Flt handlerParam(int index) const#

Shared handler parameter index, written by synth::handlerParam() on the audio thread (§9).

All of a synth’s live handlers read the same block, so it is the natural home for a steerable swarm centre / radius. Out-of-range indices read 0.

Public Members

Flt frequency = 0.f#

The sounding pitch, in Hz.

Flt velocity = 0.f#

Note-on velocity, normalised to [0, 1].

Flt aftertouch = 0.f#

Live aftertouch pressure for this voice, [0, 1].

Flt pitchWheel = 0.f#

Live pitch-wheel position for the channel, [-1, 1].

Int channel = 0#

MIDI channel that triggered the note (1..16).

Int note = 0#

MIDI note number.

Private Members

const Flt *controllers_ = nullptr#
const Flt *handlerParams_ = nullptr#
int numHandlerParams_ = 0#

Friends

friend class implementationObject
namespace YSE

Public API of libYSE — sound playback, mixing, and 3D positional audio.

Entry points: YSE::System (lifecycle and audio device), YSE::Listener (3D origin), YSE::sound (a playable source), YSE::channel (mixing tree), YSE::reverb (positioned reverb zone), YSE::patcher (modular DSP graph), YSE::player (note sequencer). Sub-namespaces group domain-specific types: YSE::DSP for signal processing, YSE::MIDI for MIDI I/O, YSE::MUSIC for note / chord / motif primitives.

Note

Apart from their constructors, the oscillator and vcf classes must only be invoked from inside a DSP callback / process body.

namespace SYNTH

Every subSystem consists out of several classes which are meant to work together: an interface, implementation, manager, message and a message enumeration.

The synth subsystem mirrors the sound subsystem one-for-one.

Enums

enum HandlerParamIndex#

Shared handler-param indices the built-in handlers read for their steerable centre (written with synth::handlerParam(index, value)).

Values:

enumerator HP_CENTER_X#
enumerator HP_CENTER_Y#
enumerator HP_CENTER_Z#
class orbitHandler : public YSE::SYNTH::positionHandler#

The swarm handler — each note orbits a shared, steerable centre.

This is the epic’s showcase and the template for a user handler. Each note gets a distinct starting phase (from its note number), a radius derived from velocity (and widened live by aftertouch), and advances its phase at rate rad/s. The centre is read live from handler-params 0..2, so synth::handlerParam() recentres the whole swarm from the main thread with one bounded message. On release the orbit slows, so a released note keeps moving — audibly correct — through its decay tail.

Public Functions

inline orbitHandler &radius(Flt r)#

Base orbit radius (added to the velocity-scaled term).

inline orbitHandler &velocityRadius(Flt r)#

Extra radius added at full velocity.

inline orbitHandler &aftertouchWiden(Flt frac)#

Fraction of extra radius added at full aftertouch (swarm widening).

inline orbitHandler &rate(Flt radiansPerSecond)#

Orbit angular speed in radians per second.

inline orbitHandler &height(Flt h)#

Vertical offset of the orbit plane from the centre.

inline orbitHandler &releaseSlow(Flt factor)#

Multiplier applied to rate once the note is released.

virtual positionHandler *clone() override#

Return a new, fully-allocated heap clone of your derived type.

You must implement this.

The typical body is return new MyHandler(*this);. Everything the hooks will touch must already exist in the returned object, so the hooks stay allocation-free. Called only on the setup pool (never the audio thread); allocation here is fine.

virtual Pos noteOn(const voiceContext &ctx) override#

A note begins on this handler’s slot.

Return its initial position. You must implement this.

Runs on the audio thread; allocation-free. Because the instance is reused across notes (and across voice steals), this hook must fully reinitialise every piece of per-note state it keeps.

virtual Pos update(const voiceContext &ctx, Flt delta) override#

Control-rate steer, called once per audio block while the note sounds (through its release tail).

Return the new position. You must implement this.

delta is the block’s duration in seconds (buffer length / samplerate), so advancing state by rate * delta is frame-rate independent. Runs on the audio thread; allocation-free.

virtual void onRelease(const voiceContext &ctx) override#

The note entered its release tail (key up / note-off edge).

Optional — the default is a no-op.

Position control CONTINUES through the tail (update() keeps being called until the voice stops), so a handler that ignores release keeps moving. Override to change behaviour for the fade (e.g. drift outward, slow an orbit). Called exactly once, on the edge. Audio thread; allocation-free.

Private Functions

Pos positionAt(const voiceContext &ctx, Flt phase) const#

Private Members

Flt radius_ = 1.f#
Flt velocityRadius_ = 2.f#
Flt aftertouchWiden_ = 1.f#
Flt rate_ = 2.f#
Flt height_ = 0.f#
Flt releaseSlow_ = 0.5f#
Flt phase_ = 0.f#

current orbit angle

Flt speed_ = 2.f#

current angular speed (rate_, halved on release)

Flt noteRadius_ = 1.f#

radius chosen for the current note

class randomSpreadHandler : public YSE::SYNTH::positionHandler#

Scatters each note to a random point within radius of the shared centre, drawn once at note-on and held for the note.

The draw uses a small deterministic PRNG seeded from seed() plus a per-slot offset, so the same seed reproduces the same scatter every run (the basis of the seeded-trajectory tests). A voice steal re-draws in noteOn(), so a stolen slot re-randomises correctly.

Public Functions

inline randomSpreadHandler &radius(Flt r)#

Radius of the spread sphere around the centre.

inline Flt radius() const#
inline randomSpreadHandler &seed(uint32_t s)#

Base RNG seed.

Each cloned slot derives a distinct stream from it, so the whole synth is reproducible for a given seed.

virtual positionHandler *clone() override#

Return a new, fully-allocated heap clone of your derived type.

You must implement this.

The typical body is return new MyHandler(*this);. Everything the hooks will touch must already exist in the returned object, so the hooks stay allocation-free. Called only on the setup pool (never the audio thread); allocation here is fine.

virtual Pos noteOn(const voiceContext &ctx) override#

A note begins on this handler’s slot.

Return its initial position. You must implement this.

Runs on the audio thread; allocation-free. Because the instance is reused across notes (and across voice steals), this hook must fully reinitialise every piece of per-note state it keeps.

virtual Pos update(const voiceContext &ctx, Flt delta) override#

Control-rate steer, called once per audio block while the note sounds (through its release tail).

Return the new position. You must implement this.

delta is the block’s duration in seconds (buffer length / samplerate), so advancing state by rate * delta is frame-rate independent. Runs on the audio thread; allocation-free.

Private Functions

Flt nextRandom()#
Pos center(const voiceContext &ctx) const#

Private Members

Flt radius_ = 1.f#
uint32_t baseSeed_ = 0x9E3779B9u#
uint32_t cloneCounter_ = 0#
uint32_t rngState_ = 0x9E3779B9u#
Pos offset_ = {0.f}#
class staticHandler : public YSE::SYNTH::positionHandler#

Places every note at one fixed position.

The trivial default.

Ignores the shared centre and all live values — a genuinely static source. Use synth::notePosition() for app-driven trajectories when no movement behaviour is wanted.

Public Functions

inline staticHandler &position(const Pos &p)#

Set the fixed position (same frame as a YSE::sound position).

inline Pos position() const#

Current fixed position.

virtual positionHandler *clone() override#

Return a new, fully-allocated heap clone of your derived type.

You must implement this.

The typical body is return new MyHandler(*this);. Everything the hooks will touch must already exist in the returned object, so the hooks stay allocation-free. Called only on the setup pool (never the audio thread); allocation here is fine.

virtual Pos noteOn(const voiceContext &ctx) override#

A note begins on this handler’s slot.

Return its initial position. You must implement this.

Runs on the audio thread; allocation-free. Because the instance is reused across notes (and across voice steals), this hook must fully reinitialise every piece of per-note state it keeps.

virtual Pos update(const voiceContext &ctx, Flt delta) override#

Control-rate steer, called once per audio block while the note sounds (through its release tail).

Return the new position. You must implement this.

delta is the block’s duration in seconds (buffer length / samplerate), so advancing state by rate * delta is frame-rate independent. Runs on the audio thread; allocation-free.

Private Members

Pos position_ = {0.f}#