| « Workspace is up! | Our own place! And, 0 for 3 » |
Reverb for spatialization
Warning: Technical content!
I'd meant to blog this later on, but since the question came up on the sc-users mailing list a second time recently, I'll go ahead and do it now.
For the Kennedy Center project, I wanted to be able to move sounds around the space suggested by the stage. It's not simply a matter of panning left and right and making the sounds louder or softer. Left/right panning is easy but the sound's perceived distance from the listener depends on other auditory cues, mainly the sound reflected from surfaces around the listener and the sound's origin.
Those were new techniques for me at that time. For help, I turned to Charles Dodge and Thomas A. Jerse's Computer Music: Synthesis, Composition and Performance (whose first edition dates back to 1985 but is still an excellent reference). Buried in the middle of Chapter 10 is a summary of John Chowning's approach, briefly:
- as the distance d from the listener increases, the volume of direct sound decreases as 1/d;
- at the same time, the volume of reverberant sound decreases as 1/sqrt(d) -- the further away the sound goes, the more reflected sound you will hear compared to direct sound.
It also distinguishes between global reverberation, which comes from everywhere more or less equally, and local reverberation where the reflections come from close to the sound's perceived location. As the sound moves further away, local reverberation takes over; so, if 1/sqrt(d) is the total amount of reverberation that should be heard, 1/d of that should be global and 1 - (1/d) should be local.
To apply this to lots of sound generators (SynthDefs), I wanted to take the function defining the UGen graph and extract the resulting sound from it, and then apply an extra pair of Out UGens to route the sound to the reverb synths in the right proportion. That is fairly straightforward in itself:
var result = SynthDef.wrap(ugenFunc, rates), out, pan, outctl;
// ... snip ...
distance = distance.clip(distNear, distFar);
result = result * distNear;
Out.ar(outctl, result / distance);
// also, high frequencies are likely to be dulled
// as the sound moves further away
result = BHiShelf.ar(result,
distance.linexp(distNear, distFar, attNearFreq, attFarFreq),
distance.linlin(distNear, distFar, attNearRs, attFarRs),
distance.linlin(distNear, distFar, attNearDb, attFarDb)
);
distance = distance.sqrt;
Out.ar(glrvbout, result * glrvbamt / distance);
Out.ar(lcrvbout, result * lcrvbamt * (1 - distance.reciprocal));
I did need some fancy tap dancing in case the function already includes a main Out unit. In that case, I wanted to remove the original one and replace it by the above that scales 'result' inversely to distance. Let the evil hacks commence...
// this follows the "result =" line above
if((outctl = findCtl.(\outbus)).isNil and: { (outctl = findCtl.(\out)).isNil }) {
outctl = NamedControl(\out, 0);
};
if(result.rate != \audio) {
// not audio rate, look for Out unit
// (Out.ar returns 0.0, which is scalar)
out = UGen.buildSynthDef.children.detect(_.writesToBus);
if(out.notNil) {
result = out.inputs[out.inputs.size - out.numAudioChannels .. ];
UGen.buildSynthDef.children.remove(out);
} {
Error("Result of UGen func is not audio rate and has no Out unit").throw;
};
};
Potentially still breakable -- really I should 'detect' SynthDef children that are audio rate and where 'writesToBus' is true. This would get confused by an Out.kr preceding the audio rate output. It's OK for my usage because generally I write SynthDefs that output either an audio rate or a control rate signal, but not both in the same one.
Putting them together gives me a function to which I can pass a SynthDef name and UGen function, and receive a SynthDef with the sound source and the reverb connections.
~addrvb = { |name, ugenFunc, metadata, rates|
var findCtl = { |cname|
block { |break|
UGen.buildSynthDef.children.do { |unit|
if(unit.isKindOf(Control)) {
unit.channels.do { |out|
if(out.name == cname) { break.(out) }
}
}
};
nil
}
};
SynthDef(name, { |distance = 5, distNear = 5, distFar = 14,
glrvbout, lcrvbout, glrvbamt = 0.075, lcrvbamt = 0.035,
attNearFreq = 9000, attFarFreq = 3000,
attNearDb = -5, attFarDb = -18,
attNearRs = 2, attFarRs = 2|
var result = SynthDef.wrap(ugenFunc, rates), out, outctl;
if((outctl = findCtl.(\outbus)).isNil and: { (outctl = findCtl.(\out)).isNil }) {
outctl = NamedControl(\out, 0);
};
if(result.rate != \audio) {
// not audio rate, look for Out unit
// (Out.ar returns 0.0, which is scalar)
out = UGen.buildSynthDef.children.detect(_.writesToBus);
if(out.notNil) {
result = out.inputs[out.inputs.size - out.numAudioChannels .. ];
UGen.buildSynthDef.children.remove(out);
} {
Error("Result of UGen func is not audio rate and has no Out unit").throw;
};
};
distance = distance.clip(distNear, distFar);
result = result * distNear;
Out.ar(outctl, result / distance);
result = BHiShelf.ar(result,
distance.linexp(distNear, distFar, attNearFreq, attFarFreq),
distance.linlin(distNear, distFar, attNearRs, attFarRs),
distance.linlin(distNear, distFar, attNearDb, attFarDb)
);
distance = distance.sqrt;
Out.ar(glrvbout, result * glrvbamt / distance);
Out.ar(lcrvbout, result * lcrvbamt * (1 - distance.reciprocal));
}, nil, metadata: metadata);
};
Instr("busfx.freeverb2", { |bus, mix = 0.25, room = 0.15, damp = 0.5,
amp = 1|
var in = In.ar(bus, 2);
FreeVerb2.ar(in[0], in[1], mix, room, damp, amp);
}, [\mybus, nil, nil, nil, nil]);
Instr([\busfx, \rvb_allpass2], { arg bus, numChan, maxDelay, preDelay,
decay, numRefl, random;
var sig, new, dlys,
trigrand = Impulse.kr(0);
(random.rate == \control).if({ trigrand = trigrand +
HPZ1.kr(random).abs });
sig = In.ar(bus, numChan);
new = sig;
dlys = Array.fill(numRefl, {
new = AllpassN.ar(new, maxDelay, Array.fill(numChan, {
TRand.kr(0.0, random, trigrand) }) + preDelay, decay);
});
Mix.ar(dlys * Array.series(numRefl, 1,
(numRefl+1).reciprocal.neg))
}, [\audiobus, \numChannels, [0.25, 2], [0.001, 1.5, \exponential, 0,
0.05], [0.01, 10, \exponential, 0, 0.25], [1, 10, \linear, 1, 4],
NoLagControlSpec(0.001, 1, \exponential, 0, 0.03), TrigSpec()]);
~master ?? { ~master = MixerChannel(\master, s, 2, 2, level: 1) };
// global reverb
~glrvbmc ?? {
~glrvbmc = MixerChannel(\rvb, s, 2, 2, level: 1, outbus: ~master);
};
~glrvbmc.doWhenReady {
if(~glrvb.isNil or: { ~glrvb.isPlaying.not }) {
~glrvb = ~glrvbmc.playfx(Instr("busfx.freeverb2"),
[0, 1.0, 0.54166668653488, 0.5, 1.0]);
};
};
// local reverb should be more stereo-separated
~lcrvbmc ?? {
~lcrvbmc = MixerChannel(\rvb, s, 2, 2, level: 1, outbus: ~master);
};
~lcrvbmc.doWhenReady {
if(~lcrvb.isNil or: { ~lcrvb.isPlaying.not }) {
~lcrvb = ~lcrvbmc.playfx(Instr("busfx.rvb_allpass2"),
[20, 2, 0.25, 0.014025612063518, 0.17782792880092,
4, 0.019573417367152]);
};
};
~addrvb.(\test, { |freq = 440, amp = 0.1, time = 0.1, pan|
Pan2.ar(
SinOsc.ar(freq, 0, EnvGen.ar(Env.perc(0.01, time), doneAction: 2)),
pan
)
}).add;
p = Pbind(
\instrument, \test,
\freq, Pexprand(200.0, 800.0, inf),
\amp, 0.5,
\angle, Ptime() * 2pi / 10 - 0.5pi,
\distFar, 25,
\distance, sin(Pkey(\angle)).linlin(-1, 1, 5, 25),
\pan, cos(Pkey(\angle)),
\delta, 0.125
).play(protoEvent: ().proto_((
glrvbout: ~glrvbmc.inbus,
lcrvbout: ~lcrvbmc.inbus,
out: ~master.inbus
)));
p.stop; No feedback yet
Comments are not allowed from anonymous visitors. Please use the Contact link at the top or bottom of this page to email me for a user account. This is just an antispam measure.
