From c592212fed4e97ea45114200edae24cc35081772 Mon Sep 17 00:00:00 2001 From: Jacob McGrath <113722146+passyur@users.noreply.github.com> Date: Sun, 14 Jun 2026 19:40:49 -0500 Subject: [PATCH 1/2] lass, partial: defer placeholder spatialization fan-out to sound level for each note, every partial was being expanded into `numChannels` full-length buffers during `spatialize_Track`. for the default spatializer, each of those buffers were identical copies scaled by `1/numChannels`. this change leverages the fact that these buffers are copies of an initial buffer by performing the per-partial spatialization computation outside of the per-sound loop (and during the unification step) --- LASS/src/MultiPan.h | 2 ++ LASS/src/Pan.h | 4 ++- LASS/src/Partial.cpp | 29 ++++++++++++------- LASS/src/Sound.cpp | 66 ++++++++++++++++++++---------------------- LASS/src/Spatializer.h | 37 +++++++++++++---------- 5 files changed, 77 insertions(+), 61 deletions(-) diff --git a/LASS/src/MultiPan.h b/LASS/src/MultiPan.h index 5f6a86f0..0ea804ed 100644 --- a/LASS/src/MultiPan.h +++ b/LASS/src/MultiPan.h @@ -137,6 +137,8 @@ class MultiPan : public Spatializer **/ MultiTrack* spatialize_Track(Track& t, int numTracks); + bool isPlaceholder() const { return false; } + /** * \deprecated **/ diff --git a/LASS/src/Pan.h b/LASS/src/Pan.h index 4f0f6fa7..60752d25 100644 --- a/LASS/src/Pan.h +++ b/LASS/src/Pan.h @@ -82,7 +82,9 @@ class Pan : public Spatializer **/ MultiTrack* spatialize_Track(Track& t, int numTracks); - /** + bool isPlaceholder() const { return false; } + + /** * \deprecated **/ void xml_print( ofstream& xmlOutput ); diff --git a/LASS/src/Partial.cpp b/LASS/src/Partial.cpp index cb773888..6172f278 100644 --- a/LASS/src/Partial.cpp +++ b/LASS/src/Partial.cpp @@ -374,16 +374,24 @@ MultiTrack* Partial::render(int numChannels, _track = &tmp; } - /* ZIYUAN CHEN, July 2023: On the default behavior of spatialization (Partial side) - * If a non-placeholder subclass of Spatializer (Pan/MultiPan) is set in the sound, - * "spatializer_" in the partials will be placeholders that merely averages the - * sound evenly accross all tracks. - * But unlike Sound::render(), the placeholders are NOT IGNORED since they are essential - * for transforming Track to MultiTrack, whose channels uniformly hold scaled versions - * of the original Track. - * Compare Sound::render(). - */ - MultiTrack* returnTrack = spatializer_->spatialize_Track(*_track, numChannels); + /* ZIYUAN CHEN, July 2023 (revised by Jacob McGrath, June 2026) */ + MultiTrack* returnTrack; + if (spatializer_->isPlaceholder()) + { + // The placeholder spatializer would expand into `numChannels` identical + // copies if done here, so instead we return a single-track (mono) + // `MultiTrack` and let `Sound::render()` perform one placeholder expansion after + // all partials are composited (scaled by `1/numChannels`). + returnTrack = new MultiTrack(); + returnTrack->add(_track); // transfer ownership of the Track + } + else + { + // Non-placeholder subclasses (e.g., `Pan`/`MultiPan`) spatialize each partial + // differently, so we must perform the expansion per-partial. + returnTrack = spatializer_->spatialize_Track(*_track, numChannels); + delete _track; // spatialize_Track copies its input; free the original + } //cout << "Partial::render - frequency after detune:" << getParam(FREQ_ENV).getMaxValue() << endl; // cout<< "--------------------------------------------"<< endl; @@ -396,7 +404,6 @@ MultiTrack* Partial::render(int numChannels, delete freqtrans_amp_env; delete freqtrans_rate_env; - return returnTrack; } diff --git a/LASS/src/Sound.cpp b/LASS/src/Sound.cpp index 987585ca..797aa8cd 100644 --- a/LASS/src/Sound.cpp +++ b/LASS/src/Sound.cpp @@ -311,47 +311,45 @@ MultiTrack* Sound::render( cout << "\t Applying Reverb..." << endl; MultiTrack &reverbedTrack = reverbObj->do_reverb_MultiTrack(*composite); delete composite; + composite = &reverbedTrack; + } - //------------------ - // spatialize the sound into a MultiTrack object - //------------------ + //------------------ + // spatialize the sound into a MultiTrack object + //------------------ - /* ZIYUAN CHEN, July 2023: On the default behavior of spatialization (Sound side) - * If a non-placeholder subclass of Spatializer (Pan/MultiPan) is set in the partials, - * "spatializer_" in the sound will be a placeholder and should be IGNORED. - * Otherwise, (indirectly) calling Spatializer::spatialize_Track() will cause each - * track to be averaged across all channels (default placeholder behavior) and - * OVERWRITE any spatialization performed by the partials! + /* Spatialization is applied at exactly one level -- this sound or its + * partials, never both (the other level holds a placeholder). + * If the partials carried the real Pan/MultiPan they already produced + * numChannels tracks; re-running a sound-level spatializer would average + * them together and destroy that per-partial spatialization, so we skip + * it (guarded by spa_modified_ below). + * If the partials used placeholders they returned single (mono) tracks, and + * the deferred fan-out below promotes the composite to numChannels using + * this sound's spatializer -- a real Pan/MultiPan if one was set, else the + * placeholder that averages evenly across channels. * Compare Partial::render(). */ - cout << "\t Spatializing..." << endl; - - if (!spa_modified_) - return &reverbedTrack; - - MultiTrack* mt = spatializer_->spatialize_MultiTrack(reverbedTrack, numChannels, sampleCount, samplingRate); - // delete the temporary track object that held the unspatialized reverbed sound - delete &reverbedTrack; - - return mt; - } - else - { - //------------------ - // spatialize the sound into a MultiTrack object - //------------------ - - /* ZIYUAN CHEN, July 2023 - See above */ - cout << "\t Spatializing..." << endl; + // When the partials used placeholder spatializers, Partial::render() returned + // single-track (mono) MultiTracks, so "composite" has fewer than numChannels + // tracks. Perform the deferred composite spatialization here. + if (composite->size() < numChannels) { + cout << "\t Spatializing..." << endl; + MultiTrack* mt = spatializer_->spatialize_MultiTrack(*composite, numChannels, sampleCount, samplingRate); + delete composite; + return mt; + } - if (!spa_modified_) - return composite; + // Otherwise the partials already produced numChannels tracks (real per-partial + // spatialization), so a sound-level spatializer would only overwrite them. + if (!spa_modified_) + return composite; - MultiTrack* mt = spatializer_->spatialize_MultiTrack(*composite, numChannels, sampleCount, samplingRate); - delete composite; - return mt; - } + cout << "\t Spatializing..." << endl; + MultiTrack* mt = spatializer_->spatialize_MultiTrack(*composite, numChannels, sampleCount, samplingRate); + delete composite; + return mt; } //----------------------------------------------------------------------------// diff --git a/LASS/src/Spatializer.h b/LASS/src/Spatializer.h index fd6b7225..42f185fc 100644 --- a/LASS/src/Spatializer.h +++ b/LASS/src/Spatializer.h @@ -60,29 +60,36 @@ class Spatializer **/ virtual MultiTrack* spatialize_Track(Track& t, int numTracks); - /** ZIYUAN CHEN, July 2023 - * This will take a MultiTrack object, and spatialize it - * to a MultiTrack object with numTracks tracks. - * CASE 1: If spatialization is applied [by sound], this method - * will superimpose numTracks spatialized copies of identical - * scaled tracks generated by the placeholder spatialize_Track - * method when composing "composite." - * CASE 2: If spatialization is applied [by partials], this method - * will effectively do nothing since the partials are already - * spatialized when composing "composite." This logic is NOT - * implemented in the function (it's tracked by spa_modified_). - * This is a shared method and is NOT supposed to be overridden - * by inheriting classes. - * \param t The MultiTrack to spatialize - * \param numTracks The number of MultiTracks *in both input and output* + /** + * \brief Spatialize a MultiTrack into one with numTracks channels + * \details Each input track is fanned out via spatialize_Track and the + * results are superimposed onto the output. + * (When the partials carried the real (e.g., `Pan`/`MultiPan`) spatializer + * they already produced `numTracks` channels and `Sound::render()` skips + * this call, since running it would overwrite that per-partial spatialization.) + * \note This is a shared method and is NOT supposed to be overridden by + * inheriting classes; only `spatialize_Track` is virtual. + * \param t The MultiTrack to spatialize (typically a single mono track) + * \param numTracks The number of channels in the output * \param sampleCount The number of samples to process * \param samplingRate The sampling rate * \return a MultiTrack + * \author Ziyuan Chen **/ virtual MultiTrack* spatialize_MultiTrack(MultiTrack& t, int numTracks, m_sample_count_type sampleCount, m_rate_type samplingRate = DEFAULT_SAMPLING_RATE); + /** + * Reports whether this is the default placeholder spatializer (even + * averaging across channels) as opposed to a `Pan`/`MultiPan`. + * `Partial::render()` uses this to skip the per-partial channel expansion + * when it would only produce identical scaled copies, deferring a + * single expansion to `Sound::render()` and saving `numChannels-1` + * buffers per partial. + **/ + virtual bool isPlaceholder() const { return true; } + /** * This function creates an exact duplicate of this Spatializer. **/ From cc7e9fc36284ccf7940da4a9fb29abc32270e124 Mon Sep 17 00:00:00 2001 From: Jacob McGrath <113722146+passyur@users.noreply.github.com> Date: Mon, 15 Jun 2026 16:42:50 -0500 Subject: [PATCH 2/2] lass, reverb: reuse per-thread scratch buffers in do_reverb_SoundSample instead of repeatedly allocating new vectors, store vectors as members for each Reverb computation and reuse existing vectors (after zeroing) --- LASS/src/Reverb.cpp | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/LASS/src/Reverb.cpp b/LASS/src/Reverb.cpp index a8604c4d..cb73e384 100644 --- a/LASS/src/Reverb.cpp +++ b/LASS/src/Reverb.cpp @@ -403,9 +403,22 @@ SoundSample *Reverb::do_reverb_SoundSample(SoundSample *inWave, Envelope *percen float* inData = &(*inWave)[0]; float* outData = &(*outWave)[0]; - // Accumulate the 6 comb filter outputs. - vector combSum(N, 0.0f); - vector filterBuf(N); + // Reusable per-thread scratch buffers. Reverb is invoked once per partial, + // once per sound, and once for the whole score, so allocating and freeing + // four full-length buffers on every call was a significant source of churn. + // thread_local keeps each render thread's scratch alive (grown to the largest + // call it has seen) and is race-free regardless of whether a Reverb instance + // is shared across the worker threads. resize() reuses existing capacity. + thread_local vector combSum, filterBuf, envVals, diff; + combSum.resize(N); + filterBuf.resize(N); + envVals.resize(N); + diff.resize(N); + + // Accumulate the 6 comb filter outputs. combSum accumulates, so unlike the + // other scratch buffers (each fully overwritten before use) it must be + // re-zeroed on every call. + vDSP_vclr(combSum.data(), 1, (vDSP_Length)N); for (int f = 0; f < REVERB_NUM_COMB_FILTERS; f++) { lpcfilter[f]->do_filter_buffer(inData, filterBuf.data(), N); vDSP_vadd(combSum.data(), 1, filterBuf.data(), 1, @@ -424,12 +437,10 @@ SoundSample *Reverb::do_reverb_SoundSample(SoundSample *inWave, Envelope *percen // = env[i] * (apOut[i] - in[i]) + in[i] float duration = percentReverb->getDuration(); float invN = 1.0f / (float)N; - vector envVals(N); for (long i = 0; i < N; i++) envVals[i] = percentReverb->getValue((float)i * invN, duration); // diff[i] = apOut[i] - in[i] (vDSP_vsub: C = B - A) - vector diff(N); vDSP_vsub(inData, 1, outData, 1, diff.data(), 1, (vDSP_Length)N); // outData[i] = envVals[i] * diff[i] + in[i]