Blog Metlivi

Come dare a ogni nota suonata un decadimento del riverbero in base alla durata della pressione

Per assegnare a ogni nota suonata un riverbero con durata propria, misura il tempo tra l’evento note-on e l’evento note-off, mappa tale durata su un tempo di decadimento e applica il risultato a un riverbero dedicato a quella nota. Lascia attivo il riverbero della nota anche dopo che la sorgente sonora si è spenta.

Questo tutorial realizza questo comportamento in SuperCollider, includendo l’ingresso MIDI e un test con note sovrapposte. Il risultato atteso è semplice: una nota tenuta brevemente lascia una coda corta, mentre una nota tenuta più a lungo lascia una coda più lunga, anche quando i rispettivi riverberi si sovrappongono.

29 ottobre 20268 min di letturaDi Metlivi Editorial Team
Sezione 1

Perché ogni nota ha bisogno di un riverbero separato?

Considera due note sovrapposte. Tieni premuta la prima per due secondi e sfiora la seconda per un quarto di secondo. Se entrambe alimentano un unico riverbero, modificare il decadimento per la seconda nota cambia anche l’elaborazione della riverberazione residua della prima. Un controllo condiviso del decadimento non può mantenere due impostazioni indipendenti all’interno dello stesso stato del riverbero.

Il routing deve quindi essere il seguente: la sorgente sonora di ogni nota alimenta il proprio riverbero e le uscite delle singole voci, una volta elaborate, vengono mixate insieme. È una deduzione architetturale: code indipendenti richiedono stati di elaborazione indipendenti, a meno che lo strumento o l’effetto non offrano già una separazione interna equivalente. Cycling ’74: polifonia

Una patch polifonica offre una struttura utile per questo scopo. Cycling ’74 documenta come poly~ di Max crei istanze delle voci e usi thispoly~ per contrassegnare ciascuna istanza come occupata o disponibile. In questo caso, una voce deve restare occupata per tutta la durata della coda del riverbero, invece di tornare disponibile subito al note-off.

C’è anche un vincolo temporale: durante un’esecuzione dal vivo, la durata finale della pressione non è nota finché la nota non termina. L’implementazione qui sotto usa un decadimento provvisorio mentre il tasto è premuto e assegna quello misurato al rilascio. Se vuoi applicare il decadimento finale fin dall’inizio, usa durate delle note già disponibili in una sequenza oppure renderizza le singole note in seguito.

Sezione 2

Che cosa dovrebbe controllare la durata della pressione?

Tieni separati tre controlli: il rilascio dell’ampiezza dello strumento, il decadimento del riverbero e il livello dell’uscita wet. Un rilascio più lungo dello strumento continua ad alimentare il riverbero con il suono. Un livello wet più alto rende l’effetto più forte. Nessuna delle due operazioni, da sola, prolunga il decadimento del riverbero.

In genere, il decadimento non indica un tempo di arresto esatto. La documentazione di Reverb di Ableton definisce il controllo Decay come il tempo necessario affinché la coda scenda a un millesimo dell’ampiezza iniziale, ovvero −60 dB. Questa distinzione è importante quando si confronta un’impostazione del decadimento con il momento in cui la coda diventa impercettibile. Ableton: manuale di riferimento degli effetti audio di Live, Reverb

Per una prima implementazione, usa questa mappatura a puro titolo illustrativo, con tutti i valori espressi in secondi:

decay = min(6.0, max(0.25, 0.25 + 1.5 × heldDuration))

Durata della pressione Decadimento assegnato
0,10 secondi 0,40 secondi
0,25 secondi 0,625 secondi
1,00 secondo 1,75 secondi
2,00 secondi 3,25 secondi
4,00 secondi 6,00 secondi, valore massimo

Questi valori sono scelte progettuali, non risultati acustici misurati. Il valore minimo garantisce alle note brevi una coda udibile; il moltiplicatore determina quanto la durata della pressione modifica l’effetto; il valore massimo limita il decadimento richiesto. Parti da questa mappatura, poi regola questi tre parametri mantenendo costanti la velocity delle note e il livello wet.

Sezione 3

Crea una voce che sopravviva al note-off

L’esempio usa GVerb, integrato in SuperCollider, che accetta un ingresso mono, produce due canali di uscita ed espone revtime in secondi. La documentazione segnala anche un picco di utilizzo della CPU all’istanziazione e artefatti quando si modifica la dimensione della stanza. Per questo l’esempio mantiene fissa la dimensione della stanza ed è preferibile iniziare con poche note simultanee. SuperCollider: GVerb

Ogni voce contiene una sorgente sonora con due oscillatori, un breve rilascio dell’ampiezza e un riverbero. Un inviluppo di durata separato libera la voce solo dopo che è trascorso il tempo previsto per la coda. EnvGen di SuperCollider supporta inviluppi sostenuti che entrano nella fase di rilascio quando il gate raggiunge zero, oltre a un’azione di completamento che libera il synth che li contiene. SuperCollider: EnvGen

Avvia una nuova sessione di SuperCollider con un’uscita audio stereo funzionante. Valuta il seguente blocco di configurazione completo, poi attendi il messaggio Held-note reverb ready nella finestra Post. Non servono campioni né librerie aggiuntive. Il codice è stato confrontato con la documentazione, ma non è stato eseguito qui perché l’interprete di SuperCollider non è disponibile.

(
s.waitForBoot {
    SynthDef(\heldNoteVerb, {
        |out = 0, freq = 440, amp = 0.035,
        gate = 1, decay = 1, alive = 1|
        var sourceEnv, lifetime, source, wet;

        sourceEnv = EnvGen.kr(
            Env.asr(0.01, 1, 0.02), gate
        );
        lifetime = EnvGen.kr(
            Env.asr(0.01, 1, 0.03), alive,
            doneAction: 2
        );
        source = (
            SinOsc.ar(freq) + (0.25 * SinOsc.ar(freq * 2))
        ) * amp * sourceEnv;
        wet = GVerb.ar(
            source,
            roomsize: 20,
            revtime: Lag.kr(decay, 0.03),
            damping: 0.5,
            drylevel: 0,
            earlyreflevel: 0.2,
            taillevel: 0.5
        );
        Out.ar(out, ((source ! 2) + wet) * lifetime);
    }).add;
    s.sync;

    ~heldVoices = Dictionary.new;
    ~tailJobs = List.new;
    ~voiceGroup = Group.head(s);
    ~voiceKey = { |num, chan, src|
        src.asString ++ ":" ++ chan.asString ++ ":" ++ num.asString
    };
    ~decayFor = { |held|
        (0.25 + (1.5 * held)).clip(0.25, 6.0)
    };

    ~releaseHeld = { |num, chan = 0, src = 0|
        var key, queue, entry, held, decay, node;
        key = ~voiceKey.value(num, chan, src);
        queue = ~heldVoices[key];
        if(queue.notNil) {
            if(queue.notEmpty) {
                entry = queue.removeAt(0);
                node = entry[0];
                held = (Main.elapsedTime - entry[1]).max(0);
                decay = ~decayFor.value(held);
                node.set(\decay, decay, \gate, 0);
                [\note, num, \held, held, \decay, decay].postln;
                ~tailJobs.add(Routine {
                    ((2 * decay) + 0.1).wait;
                    node.set(\alive, 0);
                }.play(SystemClock));
            };
            if(queue.isEmpty) { ~heldVoices.removeAt(key) };
        };
    };

    ~startHeld = { |vel, num, chan = 0, src = 0|
        var key, node;
        if(vel <= 0) {
            ~releaseHeld.value(num, chan, src);
        } {
            key = ~voiceKey.value(num, chan, src);
            if(~heldVoices[key].isNil) {
                ~heldVoices[key] = List.new;
            };
            node = Synth(\heldNoteVerb, [
                \freq, num.midicps,
                \amp, (vel / 127) * 0.035
            ], ~voiceGroup);
            ~heldVoices[key].add([node, Main.elapsedTime]);
        };
    };
    "Held-note reverb ready".postln;
};
)

Al rilascio, l’ordine è importante: la voce riceve contemporaneamente l’impostazione del decadimento e la modifica del gate della sorgente. Il riverbero continua a elaborare il segnale mentre la sorgente sfuma in 20 millisecondi. Dopo circa il doppio del decadimento assegnato, più un piccolo margine, l’intera voce sfuma e si libera.

Quel margine finale è una scelta di gestione delle risorse, non una misura del silenzio. Aumentalo se la coda residua è ancora udibile al momento della pulizia. Per uno strumento più grande, sostituisci il margine fisso con il monitoraggio del livello d’uscita e una durata massima. L’esempio conserva anche i riferimenti ai processi completati fino alla pulizia, quindi è una dimostrazione e non uno strumento progettato per funzionare a tempo indeterminato.

Sezione 4

Collega le note MIDI suonate alla voce corretta

La misurazione della durata appartiene a una specifica istanza di nota. Tenere traccia solo del timestamp dell’ultimo note-on non funziona non appena le note si sovrappongono. La configurazione memorizza invece ogni voce e il relativo orario di inizio sotto una chiave che comprende sorgente, canale e altezza.

La guida MIDI di SuperCollider mostra come inizializzare il MIDI, collegare gli ingressi e salvare i riferimenti ai synth affinché il note-off possa rilasciare il suono appropriato. Il riferimento di MIDIdef specifica gli argomenti della callback nell’ordine: valore, numero di nota, canale e sorgente. SuperCollider: uso del MIDI, SuperCollider: MIDIdef

Dopo che la configurazione indica che è pronta, valuta questo blocco per suonare lo strumento dagli ingressi MIDI collegati:

(
MIDIClient.init;
MIDIIn.connectAll;

MIDIdef.noteOn(\heldVerbOn, { |vel, num, chan, src|
    ~startHeld.value(vel, num, chan, src);
});
MIDIdef.noteOff(\heldVerbOff, { |vel, num, chan, src|
    ~releaseHeld.value(num, chan, src);
});
)

L’esempio definisce la durata della pressione come l’intervallo tra gli eventi di nota ricevuti. Non implementa il comportamento del pedale di sustain. Se vuoi una durata estesa dal pedale, rimanda le operazioni di rilascio interessate finché il pedale non viene sollevato, poi misura l’intervallo più lungo.

Le pressioni ripetute della stessa altezza vengono memorizzate in una coda e rilasciate a partire dalla più vecchia. Questa è una politica di abbinamento esplicita per eventi sovrapposti della stessa altezza, provenienti dalla stessa sorgente e dallo stesso canale. I note-off senza corrispondenza non fanno nulla; un note-on senza corrispondenza resta attivo fino alla pulizia. Verifica l’abbinamento degli eventi prima di estendere lo strumento.

Sezione 5

Verifica le code sovrapposte prima di modificare il suono

Per prima cosa, verifica il comportamento senza tastiera. La sequenza seguente usa le stesse funzioni degli eventi dell’ingresso MIDI: la nota 60 dura due secondi, mentre la nota 67 inizia a metà della prima e dura un quarto di secondo. I decadimenti assegnati previsti sono circa 3,25 e 0,625 secondi.

(
~heldTest = Routine {
    ~startHeld.value(100, 60);
    1.0.wait;
    ~startHeld.value(100, 67);
    0.25.wait;
    ~releaseHeld.value(67);
    0.75.wait;
    ~releaseHeld.value(60);
}.play(SystemClock);
)

SystemClock pianifica gli eventi in secondi, mantenendo questo test indipendente dall’impostazione del tempo musicale. Durante l’esecuzione effettiva possono verificarsi piccole differenze tra le durate richieste e quelle stampate. SuperCollider: SystemClock

Ascolta la coda breve della nota più alta mentre quella più bassa continua, seguita dalla coda più lunga della nota più bassa. Poi inverti le durate. Questo secondo passaggio aiuta a verificare che il risultato dipenda dalla durata e non dall’altezza. Confronta le coppie di durata della pressione e decadimento stampate con l’audio.

Per arrestare e rimuovere questo esempio, valuta il seguente blocco di pulizia dopo aver installato i gestori MIDI. Annulla anche le operazioni di coda pianificate prima di rimuovere le rispettive voci:

(
MIDIdef(\heldVerbOn).free;
MIDIdef(\heldVerbOff).free;
if(~heldTest.notNil) { ~heldTest.stop };
~tailJobs.do { |job| job.stop };
~tailJobs.clear;
~voiceGroup.free;
~heldVoices.clear;
)
Sezione 6

Come si diagnosticano le code che si comportano in modo errato?

Se ogni coda cambia quando viene rilasciata una nota, controlla il routing audio. Anche un timestamp per nota produce un comportamento condiviso se il suo risultato controlla un unico riverbero posto dopo il mix delle voci. Sposta il riverbero all’interno di ogni voce oppure instrada l’audio isolato di ciascuna nota verso istanze di elaborazione separate.

Se la coda scompare esattamente al note-off, controlla la durata della voce. L’inviluppo di ampiezza dovrebbe fermare la sorgente sonora senza liberare il riverbero che la contiene. In un’implementazione Max, mantieni occupato lo stato della voce finché l’uscita wet non è terminata, seguendo il modello documentato di allocazione di poly~. Cycling ’74: polifonia

Se i valori di decadimento sono errati, stampa la chiave dell’evento e i timestamp prima di modificare il riverbero. In Max, il blocco di base corrispondente per la temporizzazione è timer, che misura il tempo trascorso tra gli eventi e usa i millisecondi come unità predefinita. Converti esplicitamente le unità quando la destinazione richiede secondi. Cycling ’74: timer

Se le prestazioni diventano irregolari, riduci il numero di voci simultanee e valuta la scelta del riverbero prima di ampliare la patch. Questa dimostrazione non impone un limite alle voci e ogni nuova nota crea un altro riverbero. Uno strumento pronto per la produzione richiede una politica di allocazione esplicita che tenga conto sia delle note tenute sia delle code ancora in fase di riverberazione.

Quando i test di temporizzazione e sovrapposizione funzionano, sostituisci l’oscillatore con la sorgente sonora scelta per ogni voce. Mantieni la stessa sequenza: alloca una voce, registra l’orario di inizio, misura al rilascio, assegna il decadimento e conserva il riverbero finché la coda non è terminata. Questa sequenza rende la durata della pressione un controllo esecutivo ripetibile.

Sezione 7

Fonti

Letture correlate

Continua a esplorare il tema