연주한 음마다 누르고 있던 시간에 따라 리버브 감쇠 시간을 설정하는 방법
연주한 음마다 지속 시간에 따른 고유한 리버브를 적용하려면 노트 온과 노트 오프 사이의 시간을 측정하고, 그 지속 시간을 감쇠 시간으로 변환한 다음 해당 음 전용 리버브에 적용하세요. 음원의 소리가 끝난 뒤에도 해당 음의 리버브는 계속 작동하도록 하세요.
이 튜토리얼에서는 MIDI 입력과 겹치는 음 테스트를 포함해 SuperCollider에서 이 동작을 구현합니다. 의도한 결과는 간단합니다. 짧게 누른 음은 짧은 잔향을 남기고, 오래 누른 음은 더 긴 잔향을 남깁니다. 두 음의 리버브가 겹쳐도 마찬가지입니다.
왜 음마다 별도의 리버브가 필요한가요?
겹치는 두 음을 생각해 보세요. 첫 번째 음은 2초 동안 누르고, 두 번째 음은 0.25초 동안 짧게 누릅니다. 둘 다 하나의 리버브로 보내면 두 번째 음에 맞춰 감쇠 시간을 바꿀 때 첫 번째 음의 남은 잔향 처리도 바뀝니다. 공유된 감쇠 컨트롤로는 동일한 리버브 상태 안에서 두 개의 독립적인 설정을 유지할 수 없습니다.
따라서 라우팅은 각 음의 음원이 각자 리버브로 들어가고, 처리가 끝난 각 보이스의 출력이 함께 믹스되도록 해야 합니다. 이는 구조에 따른 결론입니다. 독립적인 잔향을 얻으려면 악기나 이펙트가 내부적으로 이에 상응하는 분리를 이미 제공하지 않는 한 처리 상태도 독립적이어야 합니다.
폴리포닉 패치는 이를 구현하기에 유용한 틀입니다. Cycling ’74 문서에서는 Max의 poly~가 보이스 인스턴스를 만들고 thispoly~를 사용해 각 인스턴스의 사용 중 또는 사용 가능 상태를 표시하는 방식을 설명합니다. 이 작업에서는 노트 오프 직후 보이스를 사용 가능 상태로 바꾸는 대신 리버브 잔향이 끝날 때까지 계속 점유해야 합니다. Cycling ’74: 폴리포니
또한 타이밍 제약이 있습니다. 실시간 연주 중에는 음을 놓기 전까지 최종적으로 누르고 있던 시간을 알 수 없습니다. 아래 구현은 건반을 누르는 동안 임시 감쇠 시간을 사용하고, 놓는 순간 측정한 감쇠 시간을 설정합니다. 최종 감쇠 시간을 처음부터 적용해야 한다면 시퀀스에 이미 있는 노트 지속 시간을 사용하거나 나중에 음을 개별적으로 렌더링하세요.
누르고 있던 시간으로 무엇을 제어해야 하나요?
악기의 진폭 릴리스, 리버브 감쇠 시간, 웻 출력 레벨을 서로 구분하세요. 악기의 릴리스가 길수록 리버브로 들어가는 소리가 더 오래 이어집니다. 웻 레벨이 높으면 이펙트 소리가 더 커집니다. 둘 중 어느 것도 그 자체로 리버브 감쇠 시간을 늘리지는 않습니다.
감쇠 시간은 보통 잔향이 정확히 멈추는 시점을 뜻하지 않습니다. Ableton의 Reverb 문서에서는 Decay 컨트롤을 잔향이 처음 진폭의 1,000분의 1, 즉 −60 dB까지 낮아지는 데 걸리는 시간으로 정의합니다. 감쇠 설정과 잔향이 들리지 않게 되는 시점을 비교할 때 이 차이가 중요합니다. Ableton: Live 오디오 이펙트 레퍼런스, Reverb
첫 구현에는 다음의 예시용 매핑을 사용하세요. 모든 값의 단위는 초입니다.
decay = min(6.0, max(0.25, 0.25 + 1.5 × heldDuration))
| 누르고 있던 시간 | 설정된 감쇠 시간 |
|---|---|
| 0.10초 | 0.40초 |
| 0.25초 | 0.625초 |
| 1.00초 | 1.75초 |
| 2.00초 | 3.25초 |
| 4.00초 | 6.00초, 상한 적용 |
이 값은 설계 선택이며 측정된 음향 결과가 아닙니다. 최소값은 짧게 누른 음에도 들리는 잔향을 주고, 배율은 누르는 시간이 이펙트에 미치는 정도를 정하며, 최댓값은 요청되는 감쇠 시간을 제한합니다. 먼저 이 매핑을 사용한 뒤 노트 벨로시티와 웻 레벨을 일정하게 유지하면서 이 세 가지 값을 조정하세요.
노트 오프 뒤에도 유지되는 보이스 만들기
이 예제에서는 SuperCollider에 내장된 GVerb를 사용합니다. 이 이펙트는 모노 입력을 받아 두 채널을 출력하며, revtime를 초 단위로 받습니다. 문서에는 인스턴스를 만들 때 CPU 사용량이 급증할 수 있고 룸 크기를 바꾸면 아티팩트가 생길 수 있다고도 나와 있습니다. 따라서 이 예제에서는 룸 크기를 고정하며, 처음에는 동시에 몇 개의 음만 연주하는 것이 좋습니다. SuperCollider: GVerb
각 보이스에는 두 개의 오실레이터로 구성된 간단한 음원, 짧은 진폭 릴리스, 리버브 하나가 들어 있습니다. 별도의 수명 엔벌로프는 잔향을 위한 시간이 지난 뒤에만 보이스를 해제합니다. SuperCollider의 EnvGen은 게이트가 0이 되면 릴리스되는 서스테인 엔벌로프와, 포함된 신스를 해제하는 완료 동작을 지원합니다. SuperCollider: EnvGen
정상적으로 작동하는 스테레오 오디오 출력이 연결된 새 SuperCollider 세션을 사용하세요. 아래 전체 설정 블록을 평가한 다음 Post 창에 Held-note reverb ready가 표시될 때까지 기다리세요. 샘플이나 추가 라이브러리는 필요하지 않습니다. 코드는 문서를 참고해 검토했으며, SuperCollider 인터프리터를 사용할 수 없어 여기서 실행하지는 않았습니다.
(
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;
};
)
릴리스 시점의 순서가 중요합니다. 보이스에 감쇠 설정과 음원 게이트 변경을 동시에 전달합니다. 음원은 20밀리초 동안 페이드아웃되는 동안에도 리버브는 계속 처리됩니다. 설정된 감쇠 시간의 약 두 배에 약간의 여유 시간을 더한 뒤 전체 보이스가 페이드아웃되고 스스로 해제됩니다.
마지막 여유 시간은 무음 측정값이 아니라 리소스 관리 선택입니다. 정리 시점에도 잔향이 들리면 시간을 늘리세요. 더 큰 악기에서는 고정된 여유 시간 대신 출력 레벨 모니터링과 최대 수명을 함께 사용하세요. 또한 이 예제는 완료된 작업 참조를 정리할 때까지 유지하므로, 계속 실행하는 악기라기보다는 동작을 보여 주는 예제입니다.
연주한 MIDI 음을 올바른 보이스에 연결하기
지속 시간 측정값은 특정 노트 인스턴스에 속합니다. 가장 최근 노트 온의 타임스탬프만 추적하면 음이 겹치는 순간부터 제대로 작동하지 않습니다. 대신 이 설정에서는 소스, 채널, 피치를 포함하는 키 아래에 각 보이스와 시작 시간을 저장합니다.
SuperCollider의 MIDI 가이드에서는 MIDI 초기화, 입력 연결, 노트 오프 시 해당 음을 해제할 수 있도록 신스 참조를 저장하는 방법을 설명합니다. MIDIdef 레퍼런스에는 콜백 인수가 값, 노트 번호, 채널, 소스 순서로 지정되어 있습니다. SuperCollider: MIDI 사용하기, SuperCollider: MIDIdef
설정이 준비되었다는 메시지가 표시되면 연결된 MIDI 입력으로 악기를 연주할 수 있도록 다음 블록을 평가하세요.
(
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);
});
)
이 예제에서는 수신된 노트 이벤트 사이의 간격을 “누르고 있던 시간”으로 정의합니다. 서스테인 페달 동작은 구현하지 않습니다. 페달로 늘어난 시간까지 반영하려면 해당 릴리스 작업을 페달에서 발을 뗄 때까지 미룬 다음, 늘어난 간격을 측정하세요.
같은 피치를 반복해서 누르면 큐에 저장되고 가장 오래된 항목부터 해제됩니다. 이는 한 소스와 채널에서 같은 피치 이벤트가 겹칠 때 적용하는 명시적인 매칭 정책입니다. 짝이 없는 노트 오프는 아무 동작도 하지 않으며, 짝이 없는 노트 온은 정리할 때까지 활성 상태로 남습니다. 악기를 확장하기 전에 이벤트가 올바르게 짝을 이루는지 확인하세요.
사운드를 바꾸기 전에 겹치는 잔향 테스트하기
먼저 키보드 없이 동작을 테스트하세요. 다음 시퀀스는 MIDI 입력과 같은 이벤트 함수를 사용합니다. 노트 60은 2초 동안 지속되고, 노트 67은 그 중간에 시작해 0.25초 동안 지속됩니다. 설정될 감쇠 시간은 각각 약 3.25초와 0.625초입니다.
(
~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은 초 단위로 작업을 예약하므로 이 테스트는 음악적 템포 설정과 무관합니다. 실제 실행에서는 출력된 지속 시간과 요청한 지속 시간 사이에 작은 차이가 생길 수 있습니다. SuperCollider: SystemClock
높은 음의 짧은 잔향이 낮은 음이 계속되는 동안 들린 뒤, 낮은 음의 긴 잔향이 이어지는지 확인하세요. 그런 다음 두 음의 지속 시간을 서로 바꿔 보세요. 두 번째 테스트를 통해 결과를 결정하는 것이 피치가 아니라 지속 시간임을 확인할 수 있습니다. 출력된 누르고 있던 시간과 감쇠 시간의 쌍을 오디오와 함께 살펴보세요.
MIDI 핸들러를 설치한 뒤 이 예제를 중지하고 제거하려면 다음 정리 블록을 평가하세요. 이 블록은 보이스를 제거하기 전에 예약된 잔향 작업도 취소합니다.
(
MIDIdef(\heldVerbOn).free;
MIDIdef(\heldVerbOff).free;
if(~heldTest.notNil) { ~heldTest.stop };
~tailJobs.do { |job| job.stop };
~tailJobs.clear;
~voiceGroup.free;
~heldVoices.clear;
)
잔향이 잘못 동작할 때 어떻게 진단하나요?
한 음을 놓을 때마다 모든 잔향이 바뀐다면 오디오 라우팅을 확인하세요. 노트마다 타임스탬프를 기록해도 그 결과가 보이스 믹스 뒤에 있는 하나의 리버브를 제어하면 동작은 여전히 공유됩니다. 리버브를 각 보이스 안으로 옮기거나 음별 오디오를 분리해 별도의 처리 인스턴스로 보내세요.
노트 오프와 동시에 잔향이 사라진다면 보이스 수명을 확인하세요. 진폭 엔벌로프는 음원을 멈추되 해당 음원을 포함한 리버브까지 해제해서는 안 됩니다. Max 구현에서는 문서에 나온 poly~ 할당 모델에 따라 웻 출력이 끝날 때까지 보이스를 사용 중 상태로 유지하세요. Cycling ’74: 폴리포니
감쇠 수치가 잘못되었다면 리버브를 바꾸기 전에 이벤트 키와 타임스탬프를 출력하세요. Max에서 이에 해당하는 시간 측정 요소는 timer이며, 이벤트 사이의 경과 시간을 측정하고 기본 단위는 밀리초입니다. 대상이 초를 요구하면 단위를 명시적으로 변환하세요. Cycling ’74: timer
성능이 불안정해지면 동시 보이스 수를 줄이고 패치를 확장하기 전에 리버브 선택을 확인하세요. 이 시연에서는 보이스 수를 제한하지 않으며 새 노트마다 리버브가 하나씩 생성됩니다. 실제 악기에는 누르고 있는 음과 계속 울리는 잔향을 모두 고려한 명시적인 할당 정책이 필요합니다.
타이밍과 겹침 테스트가 통과하면 오실레이터를 원하는 보이스별 음원으로 교체하세요. 보이스 할당, 시작 시간 기록, 릴리스 시 측정, 감쇠 시간 설정, 잔향이 끝날 때까지 리버브 유지라는 동일한 순서를 지키세요. 이 순서를 따르면 누르고 있던 시간을 반복 가능한 연주 컨트롤로 사용할 수 있습니다.
