Per-note 3D: position handlers and swarms#
Goal: give every note its own position in space, so a single chord becomes a swarm of independently-moving sound sources orbiting the listener.
A synth is rendered behind one positioned YSE::sound, so by
default every voice shares that one position. Attach a
YSE::SYNTH::positionHandler and each voice gets its own 3D
position, updated every block — the basis of the “swarm” effect. libYSE ships
three handlers: static,
random-spread and
orbit (the swarm workhorse), or you
can derive your own.
This tutorial walks through Demo20_Swarm: a chord of sine voices orbiting
the listener, steerable live.
Source: Demo20_Swarm.cpp.
Attaching a handler#
Build a voice prototype and a handler prototype, then chain
positionHandler after addVoices. The synth clones the handler once
per voice slot, so each voice steers independently:
voiceProto_ = std::make_unique<YSE::SYNTH::sineVoice>();
voiceProto_->attack(0.02f).decay(0.1f).sustain(0.7f).release(0.4f);
handlerProto_ = std::make_unique<YSE::SYNTH::orbitHandler>();
handlerProto_->radius(1.0f).velocityRadius(2.0f).aftertouchWiden(1.5f).rate(2.5f).releaseSlow(
0.5f);
synth_ = std::make_unique<YSE::synth>();
synth_->create().addVoices(*voiceProto_, 8).positionHandler(*handlerProto_);
sound_ = std::make_unique<YSE::sound>();
sound_->create(*synth_);
sound_->play();
The orbit handler reads its base
radius from radius, widens with note velocity (velocityRadius) and
aftertouch (aftertouchWiden), spins at rate radians per second, and
slows on release (releaseSlow). Like a voice, a handler prototype must
outlive setup — the engine reads it to clone but neither copies nor owns it.
Note
With no handler attached, positionHandler is simply never called and
every voice uses the synth’s aggregate position — so adding per-note
positioning is purely additive to the Your first synth flow.
Steering the swarm live#
Two live controls, both bounded and allocation-free, so they are safe to call every frame:
void DemoSwarm::CenterLeft() {
centerX_ -= 1.0f;
synth_->handlerParam(YSE::SYNTH::HP_CENTER_X, centerX_);
}
void DemoSwarm::CenterRight() {
centerX_ += 1.0f;
synth_->handlerParam(YSE::SYNTH::HP_CENTER_X, centerX_);
}
void DemoSwarm::MoreAftertouch() {
aftertouch_ = aftertouch_ + 0.2f > 1.0f ? 1.0f : aftertouch_ + 0.2f;
synth_->aftertouch(kMidiChannel, -1, aftertouch_); // channel-wide
}
void DemoSwarm::LessAftertouch() {
aftertouch_ = aftertouch_ - 0.2f < 0.0f ? 0.0f : aftertouch_ - 0.2f;
synth_->aftertouch(kMidiChannel, -1, aftertouch_);
handlerParam(index, value)sets a shared handler parameter every live handler reads next block. Indices 0–2 are the swarm centre (YSE::SYNTH::HP_CENTER_Xand friends), so one call recentres the entire swarm.aftertouch(channel, -1, value)applies channel-wide pressure; the orbit handler widens the radius with it.
To place a single note explicitly instead (app-driven trajectories rather
than a handler), use YSE::synth::notePosition().
Reading a voice back#
getVoicePosition returns a best-effort snapshot of where a sounding voice
currently is — handy for driving visuals off the audio:
void DemoSwarm::ShowStatus() {
if (!playing_) return;
YSE::Pos p = synth_->getVoicePosition(kMidiChannel, kChord[0]);
std::cout << "\rcentreX=" << centerX_ << " aftertouch=" << aftertouch_ << " voice[" << kChord[0]
<< "] pos=(" << p.x << ", " << p.z << ") " << std::flush;
}
What you learned#
Attach a
positionHandlerwithsynth::positionHandler(prototype)to give every voice its own 3D position; the prototype must outlive setup.The
orbitHandlermakes a chord orbit the listener; velocity and aftertouch shape the radius.Steer the whole swarm with
handlerParam(centre at indices 0–2), place one note withnotePosition, and read a voice back withgetVoicePosition.
Next#
Synthesis — position-handler reference.
3D positioning — the listener and 3D basics this builds on.