Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save dyfer/f2e889c0f96dac6b35abe9f1baaafc8e to your computer and use it in GitHub Desktop.
Save dyfer/f2e889c0f96dac6b35abe9f1baaafc8e to your computer and use it in GitHub Desktop.
// An ad hoc decoder builder using mode matching (pseudo-inverse)
// FuMa, MaxN normalized
// created by Mike McCrea
// adapted by Marcin Pączkowski
/*
– 0° –
/ front \
90° ^ 270°
\ /
– 180°–
*/
//this fuction returns processing function
//which then need to be called inside a synthdef
// EXAMPLE:
// 3 stacked hexagons, oriented to "face" (first speaker off center), ccw, bottom to top
// NOTE: the order you define these directions/distances
// is the order of the output channels of your decoder
// azims_sat, inclins_sat, distances_sat, azims_sub, distances_sub, pattern_sat, pattern_sub
(
// ~makeDecFunc =
{arg
// satellites
// azimuths
azims_sat = [
30, 90, 150, 210, 270, 330, // lower ring
30, 90, 150, 210, 270, 330, // middle ring
30, 90, 150, 210, 270, 330, // upper ring
].degrad,
// inclinations
inclins_sat = [
-25, -20, -25, -25, -20, -25, // lower ring
0, 0, 0, 0, 0, 0, // middle ring
25, 20, 25, 25, 20, 25, // upper ring
].degrad,
// distances to speakers, in meters
distances_sat = [
12, 12, 12, 12, 12, 12,
12, 12, 12, 12, 12, 12,
12, 12, 12, 12, 12, 12
],
// depending on number of subs,
// order is determined by sub decoder matrix below (mono, stereo, quad, etc)
// (likely starting front left, advancing ccw)
// ~~~subs assumed to be 2D (no elevation)~~~
azims_sub = [
30, 90, 150, 210, 270, 330,
].degrad,
distances_sub = [
12, 12, 12, 12, 12, 12
],
pattern_sat =
// 0.75 // cardioid // NOTE: this differs from "decoder k"
((3-sqrt(3))/2) // energy, preferred for larger environments
// 0.5 // planewave
,
pattern_sub =
// 0.75 // cardioid, for subs. // NOTE: this differs from "decoder k"
((3-sqrt(3))/2) // energy
// 0.5 // planewave
;
// var azims_sat, inclins_sat, distances_sat;
// var azims_sub, distances_sub;
var directions_sat, directions_sub;
var maxDist, delayTimes_sat, delayTimes_sub;
var foaEncoderMatrix_sat, decoderMatrix_sat;
var foaEncoderMatrix_sub, decoderMatrix_sub;
// specification error check
if ([azims_sat, inclins_sat, distances_sat].collect(_.size).every(_ == azims_sat.size).not) {
Error("azimuths, inclinations, and distances arrays have to be of equal size.").errorString.postln; this.halt;
};
// directions in [azimuth, inclination] pairs
directions_sat = [azims_sat, inclins_sat].lace(azims_sat.size + inclins_sat.size).clump(2);
directions_sub = azims_sub; // subs are 2D, no elevation
// delay for distance compensation
maxDist = distances_sat.maxItem;
delayTimes_sat = (maxDist - distances_sat) / 344;
delayTimes_sub = (maxDist - distances_sub) / 344;
foaEncoderMatrix_sat = FoaEncoderMatrix.newDirections(
directions_sat,
pattern_sat
);
// build a decoder Matrix from the encoder matrix
// having made an encoder matrix with *newDirections, this uses pseudo-inverse under the hood,
// so rather than take the pseudoinverse again (which would only give the correct result if the
// array were regular, e.g. a platonic solid), scale w and flop.
decoderMatrix_sat = Matrix.with((foaEncoderMatrix_sat.matrix.asArray * [2, 1, 1, 1]).flop); // flop - instead
// cardioid pattern for sub
foaEncoderMatrix_sub = FoaEncoderMatrix.newDirections(
directions_sub,
pattern_sub,
// 0.75 // cardioid, for subs. // NOTE: this differs from "decoder k"
((3-sqrt(3))/2) // energy
// 0.5 // planewave
);
decoderMatrix_sub = Matrix.with((foaEncoderMatrix_sub.matrix.asArray * [2, 1, 1, 1]).flop); // flop - instead
// specification error check
if (decoderMatrix_sub.rows != distances_sub.size) {
Error(format(
"sub distances array does not equal the number of outputs of your sub decoder matrix. [%, %]",
decoderMatrix_sub.rows.size, distances_sub.size
)).errorString.postln; this.halt;
};
// post results
postf("Decoder built.\nConfirm: loudspeakers should be positioned in the following directions (and in this order):\nSatellites:\n");
foaEncoderMatrix_sat.dirInputs.do{ |azElPairs, i|
postf("chan %: %\n", i, azElPairs.raddeg) };
postf("Subs:\n");
foaEncoderMatrix_sub.dirInputs.do{ |azElPairs, i|
postf("chan %: %\n", foaEncoderMatrix_sat.numChannels+i, azElPairs.raddeg) };
postf("max delay time: %\n", (delayTimes_sat++delayTimes_sub).maxItem);
/* The decoding graph */
// first argument, sig, should be a 4-chan b-format input, classic FOA ambisonics (Fuma, MaxN)
// outputs satellites (in order specified in azimuth list above),
// followed by subs.
{|sig, xover_freq=110, sub_amp=1, sat_amp=1|
var pan, foa, foa_hf, foa_lf, foanfc_sat, foanfc_sub;
var decoder_mtx_sat, decoder_mtx_sub, decode_sat, decode_sub;
var sats_delayed, subs_delayed;
// Input signal
foa = sig;
// Test signal = replace with your source
// pan = LFSaw.kr(10.reciprocal, 1).range(0, 2pi);
// foa = FoaPanB.ar(PinkNoise.ar, pan);
// foa = FoaPanB.ar(WhiteNoise.ar, pan);
// crossover
foa_hf = HPF.ar(HPF.ar(foa, xover_freq), xover_freq, sat_amp);
foa_lf = LPF.ar(LPF.ar(foa.keep(3), xover_freq), xover_freq, sub_amp);
/* NFC decoding section --- */
// generate foa NFC signals for satellites, subs
foanfc_sat = distances_sat.collect{ |dist| FoaNFC.ar(foa_hf, dist) };
foanfc_sub = distances_sub.collect{ |dist| FoaNFC.ar(foa_lf, dist) };
// decode
decode_sat = foanfc_sat.collect{ |satnfc, i|
AtkMatrixMix.ar(
satnfc, // near-field compensated foa for this loudspeaker distance
decoderMatrix_sat.fromRow(i) // decoder matrix row for that loudspeaker
)
};
decode_sub = foanfc_sub.collect{ |subnfc, i|
AtkMatrixMix.ar(subnfc, decoderMatrix_sub.fromRow(i))
};
/* -- end NFC decoding section */
// // docoding with no NFC version
// decode_sat = AtkMatrixMix.ar(foa_hf, decoderMatrix_sat);
// decode_sub = AtkMatrixMix.ar(foa_lf, decoderMatrix_sub);
sats_delayed = decode_sat.collect({ |sig, i| DelayN.ar(sig, delayTimes_sat[i], delayTimes_sat[i]) });
subs_delayed = decode_sub.collect({ |sig, i| DelayN.ar(sig, delayTimes_sub[i], delayTimes_sub[i]) });
sats_delayed ++ subs_delayed
};
}
)
/*
//generate the decoder
//args: azims_sat, inclins_sat, distances_sat, azims_sub, distances_sub, pattern_sat, pattern_sub
~graphFunc = ~makeDecFunc.();
//create a synthdef
(
~decodeDef = CtkSynthDef(\decoder, {|inbus, outbus = 0, xover_freq=110, amp = 1, sub_amp=1, sat_amp=1|
var pan, foa, dec;
// Test signal = replace with your source
pan = LFSaw.kr(10.reciprocal, 1).range(0, 2pi);
foa = FoaPanB.ar(PinkNoise.ar, pan);
// foa = FoaPanB.ar(WhiteNoise.ar, pan);
// Input signal
// foa = In.ar(inbus, 4);
//decode
//|sig, xover_freq=110, sub_amp=1, sat_amp=1|
dec = ~graphFunc.(foa, xover_freq, sub_amp, sat_amp);
Out.ar(outbus, dec * amp);
});
)
x = ~decodeDef.note.play
s.meter
// adjust parameters
x.xover_freq = 110
x.amp = 3.dbamp; // adjust overall level
x.sub_amp = -4.dbamp; // adjust sub level
x.sat_amp = 1.dbamp; // adjust satellite level
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment