SuperCollider GUIDES (extension)

Guide to ATK Matrix Files
ExtensionExtension

A guide to reading, writing, and storing ATK matrices.

Directory Structure

The ATK store assets in its Application Support directory:

Atk.userSupportDir

This includes three default directories:

These folders store the files shipped with the ATK. You can also optionally add your own extensions folder, in which you can store kernels and matrices of your own design. Note this is different from SuperCollider's Extensions folder. If you haven't yet added an extensions directory, you can see where to put it by executing the following method:

Atk.userExtensionsDir         // view it
Atk.userExtensionsDir.openOS  // open it... if it exists!

There's a handy method that will build it for you in the expected structure:

Atk.createExtensionsDir

This will create a directory structure that lives in your next to your default ATK assets. Note this creates both a matrices folder structure, and an identical kernels folder structure for storing your custom kernels. The full structure will look like this:

Each of the folders (FOA>encoders, HOA5>decoders, etc.) are empty and ready to store matrices (and kernels) for use with the ATK-SC3 (this package) and ATK-Reaper (more on that later). When you write a matrix using the ATK, it will store it in this directory structure by default, and will look here by default when asked to read in a matrix from file.

You can view this structure and any files you've stored there using the following method:

Atk.postMyMatrices();                    // All sets, all matrices types
Atk.postMyMatrices('FOA');               // FOA matrices hierarchy
Atk.postMyMatrices('FOA', 'encoders');    // FOA encoders only
Atk.postMyMatrices('FOA', 'decoders');    // FOA decoders only
Atk.postMyMatrices('FOA', 'xformers');    // FOA xformers only

Each of these matrix subdirectories can have further subdirectories at your discretion, e.g. for particular projects or categories of matrices.

Writing Matrices

We'll start by writing a matrix file.

Let's create a first order A-format encoding matrix from a nine-point spherical t-design. For our purposes, we'll use a spherical designs with d = 2, giving a collection of uniformly distributed points on a sphere. The t-design we're using below can be found in Hardin and Sloan's Library of 3-D Designs. 1

(
// Spherical coordinates of the nine-point t-design.
~directions = [
    [ 0, 45 ],  [ 120, 45 ],  [ -120, 45 ],
    [ 0, 0 ],   [ 120, 0 ],   [ -120, 0 ],
    [ 0, -45 ], [ 120, -45 ], [ -120, -45 ]
].degrad;

// Here's our 9-point A-format to B-format (planewave, aka velocity) encoder:
~encoder = FoaEncoderMatrix.newDirections(~directions);
)

This FoaEncoderMatrix is now ready to be used for encoding planewaves arriving from those nine uniformly distributed incidences. Within the ATK's classification hierarchy, ~encoder looks like this:
settypeopkind
'FOA''encoder''matrix''dirs'

For fun, let's inspect:

(
var methodsToInspect = ['class', 'set', 'type', 'op', 'kind' ];

methodsToInspect.do({arg item; (item.asString ++ " : " ++ ~encoder.perform(item)).postln;})
)

After all that hard work (thanks ATK!), we want to store the result to a file for use in the future, and to use in ATK-Reaper plugins! 2

There are three available file formats, each with a special purpose:

Let's write this encoder matrix out in all three formats:

// .txt extension writes the matrix only
~encoder.writeToFile("my9PointEncoder.txt");

// .yml writes metadata as well
~encoder.writeToFile("my9PointEncoder.yml");

// .mosl.txt writes matrix only, single lines for Reaper to read
~encoder.writeToFile("my9PointEncoder.mosl.txt");

Because we only specified a file name, not a full path, the ATK will store the matrix in the default location. As we're writing an FoaEncoderMatrix, ATK can infer that it's an encoder in the FOA set. (We also know, we're dealing with a matrix operation.) Therefore, the ATK knows to put it in: ../extensions/matrices/FOA/encoders.

Had we specified a full path instead, it would have saved to that location.

// Here are our encoders (defaults to showing the FOA set)
Atk.postMyMatrices('FOA', 'encoders');

Writing Metadata

Because this matrix encoder is somewhat unique, it would be helpful to provide a bit more information about it for future reference. This is where the .yml file format comes in.

Note that the AtkMatrix: -writeToFile method has some optional arguments: note and attributeDictionary. A note can be a brief description, while an attributeDictionary is a Dictionary for storing any info you'd like in the form of key:value pairs.

(
// A 'note': a description or note about the matrix.
~note = "This is a nine-point t-design encoder made for a matrix file writing demo.";

// A Dictionary of more metadata to add.
~properties =  (
    author: "Me, the Reader",
    dateCreated: Date.getDate.stamp,
    ordering: 'FuMa',
    normalisation: 'MaxN',
    dirInputs: ~directions
);
)
NOTE: If keys in the attributeDictionary match instance variables of FoaEncoderMatrix, they can be retrieved with getters once loaded from the file. This is the case for dirInputs specified in the ~properties dictionary above. We'll see the effect this has below.

Now write this matrix and metadata to file... Be sure to specify the .yml extension in order to write the metadata. Set overwrite = true to force overwrite the file we wrote before with the same name and extension.

(
~encoder.writeToFile( "my9PointEncoder.yml",
  note: ~note,
  attributeDictionary: ~properties,
  overwrite: true
)
)

Writing Raw Matrices

In the above examples, we've been reading/writing matrices encapsulated in the AtkMatrix subclasses. When writing from these objects, some the information can be inferred from them, such as the set (Ambisonic order, channel ordering, channel normalisation, e.g. 'FOA', 'HOA3', etc.) and type of matrix (e.g. 'encoder', 'decoder', 'xformer'). In the case of a raw matrix, you must cast it to an AtkMatrix, specifying the set and type explicitly, before writing it to a file.

( // Here's a raw A-to-B encoder matrix:
~matrix = Matrix.with([
    [ 0.61237243569579, 0.61237243569579, 0.61237243569579, 0.61237243569579 ],
    [ 0.5, 0.5, -0.5, -0.5 ],
    [ 0.5, -0.5, 0.5, -0.5 ],
    [ 0.5, -0.5, -0.5, 0.5 ]
])
)

Metadata is useful to record more information about the matrix:

(
~note = "A 4-channel A-to-B encoder matrix, in Front-Left-Up orientation.";

// A Dictionary of more metadata to add.
~properties =  (
    author: "Me, the Reader",
    dateCreated: Date.getDate.stamp,
    ordering: 'FuMa',
    normalisation: 'MaxN',
    dirInputs: [ [ 0.78539816339745, 0.61547970867039 ], [ -0.78539816339745, -0.61547970867039 ], [ 2.3561944901923, -0.61547970867039 ], [ -2.3561944901923, 0.61547970867039 ] ]
);
)

Be sure to specify the set and type when creating an AtkMatrix from your Matrix. This is how the ATK will know where to store the file by default (unless a full path is provided to the file name argument).

(
~atkMatrix = ~matrix.asAtkMatrix('FOA', 'encoder'); // set, type
// be sure to use .yml extension for metadata
~atkMatrix.writeToFile("myA2B_flu_Matrix.yml", ~note, ~properties);
)
NOTE: If providing a file path relative to your /ATK/extension/matrices/...directory, set and type arguments are necessary when creating the AtkMatrix from your Matrix in order to locate the proper directory to store your file. If providing an absolute file path, set and type are recommended but not strictly enforced. This allows storing matrices outside the ATK paradigm, e.g. VBAP matrices, etc.

There it is:

Atk.postMyMatrices('FOA', 'encoders');

Subfolders: Organizing your matrices

If you'll be generating many matrices, it's advisable to organize your matrices into subfolders. For example, if you're algorithmically generating hundreds of matrices for a particular project or process, it makes sense to store them in a subfolder.

To do this, you can create subfolders inside your /encoders, /decoders, and /xformers folders.

// Store your encoder matrix with the other encoders, which live here:
Atk.getMatrixExtensionSubPath('FOA', 'encoders');

// You can make subfolder for a group of matrices, say, for a particular project:
(
~projSubFolderName = "myProject";

File.mkdir( Atk.getMatrixExtensionSubPath('FOA', 'encoders').fullPath +/+ ~projSubFolderName )
)

// For convenience, we'll write the 9-point ~encoder matrix,
// which we created above, to a new file in your new project folder.
// (We'll need to reset ~note and ~properties, as we clobbered them above!)
(

// A 'note': a description or note about the matrix.
~note = "This is a nine-point t-design encoder made for a matrix file writing demo.";

// A Dictionary of more metadata to add.
~properties =  (
    author: "Me, the Reader",
    dateCreated: Date.getDate.stamp,
    ordering: 'FuMa',
    normalisation: 'MaxN',
    dirInputs: ~directions
);

~encoder.writeToFile(~projSubFolderName +/+ "projectEncoder1.yml",
  note: ~note,
  attributeDictionary: ~properties
)
)
NOTE: Remember that because ~encoder is an FoaEncoderMatrix, the set ('FOA') and type ('encoder') arguments are inferred.
// There it is, in the 'myProject' subdirectory.
Atk.postMyMatrices('FOA', 'encoders')

Later you'll use FoaEncoderMatrix to read the file back in. ATK will know where to look ( extensions/matrices/enocoders/FOA ) so you can simply specify the relative path of your subfolder/file.yml:

~projectEncoder1 = FoaEncoderMatrix.newFromFile(~projSubFolderName +/+ "projectEncoder1.yml");

~projectEncoder1.info;

Reading Matrices

We wrote three encoder matrix files earlier. Let's now read them in. As when writing, the ATK looks in the extensions/matrices directory by default. Unless the matrix file is somewhere outside the default location, a filename will suffice to read it in. The type ('encoder', 'decoder', 'xformer') is inferred from the object being instantiated.

We can even omit the file extension if we don't expect multiple file formats (.txt, .yml, .mosl.txt) stored under the same name:

~encoder = FoaEncoderMatrix.newFromFile("my9PointEncoder")
// >> ERROR: It sees we have more than one file with that name.

So, we'll need to specify the extension. As mentioned before, each file format determines what kind of information is stored in the file.

Lets have a look at what each file format gives us back:

.txt format:

// Reading the .txt file, we just get a matrix and basic info.
~encoder = FoaEncoderMatrix.newFromFile("my9PointEncoder.txt");

// All the standard instance vars are preserved.
~encoder.matrix;
~encoder.kind;          // Defaults to filename
~encoder.dirOutputs;    // Outputs are inf, becuase the output is b-format, i.e "all directions".
~encoder.dirInputs;     // With no metadata, we can't know input directions, so 'unspecified'
~encoder.dirInputs.size;// ...but knowing how large the array is tells us how many inputs the matrix expects
~encoder.dim;           // We see it's a 3-D matrix

.mosl.txt format:

// reading the mosl.txt file, we just get a matrix and basic info
~encoder = FoaEncoderMatrix.newFromFile("my9PointEncoder.mosl.txt");

// all the standard instance vars are preserved
~encoder.matrix;
~encoder.kind;          // Defaults to filename
~encoder.dirOutputs;    // inf, by nature of encoding to b-format
~encoder.dirInputs;     // With no metadata, we can't know input directions
~encoder.dim;

.yml format:

// reading the .yml file, we get the matrix plus metadata
~encoder = FoaEncoderMatrix.newFromFile("my9PointEncoder.yml");

// all the standard instance vars are preserved
~encoder.matrix;
~encoder.kind;
~encoder.dirOutputs;
~encoder.dim;

// NOTE: because we provided the 'dirInputs' to the attributeDictionary
// when we wrote it to file, we now have that info for reference. Useful!
~encoder.dirInputs;


// Plus the other data written to it:
~encoder.info;           // Formatted post

// Metadata is loaded as an IndentityDictionary, so values
// from the attributeDictionary can be accessed by their
// keys as pseudo-methods.
~encoder.fileParse;       // For direct access to the dictionary of values
~encoder.fileParse.note;  // What was this matrix again?? Oh yea...
~encoder.fileParse.ordering;
~encoder.fileParse.keysValuesDo{|k,v| postf("% : %\n", k, v)};

Test case: Creating a decoder from an FoaEncoderMatrix

We've now instantiated a new ~encoder by reading in the file that stored the matrix that we originally built using the planewave encoder: FoaEncoderMatrix: *newDirections (using the points of a nine-point t-design). As it turns out, a matrix encoder created by FoaEncoderMatrix: *newDirections can be used to build a decoder of the same geometry.3 Doing so just involves performing the Matrix: -pseudoInverse on the encoder's Matrix.4

// Retrieve the Matrix object stored in the FoaEncoderMatrix
// (Should be the 9-point t-design from above!)
~encoderMatrix = ~encoder.matrix;

// Perform the pseudoinverse on that matrix to find decoding coefficients
~decoderMatrix = ~encoderMatrix.pseudoInverse;

Using these coefficients will return a 'velocity' decode (aka "strict soundfield" or "basic"). Loudspeakers should be positioned in the following directions (and in this order):

~encoder.dirInputs.do{|azElPairs, i| postf("chan %: %\n", i, azElPairs.raddeg) }
NOTE: Because we're inverting an encoder to produce a decoder, the original input directions of the encoder will now be the output directions of the decoder. The channel ordering will be in the same order the input directions were specified in the encoder. Therefore, in the above code we query the encoder for its -dirInputs to know where our output channel signals are expected to be sent (in space!).

It's important to note here that the ~decoderMatrix matrix is a Matrix object, not an FoaDecoderMatrix object. As such, we'll need to use care when building the decoding SynthDef. Instead of using the FoaDecode UGen (which expects an FoaDecoderMatrix or FoaDecoderKernel decoder), we'll use the AtkMatrixMix UGen, which will decode our B-format signal by using our newly authored decoding Matrix, ~decoderMatrix.

(
~decoderDef = SynthDef(\pinv_decoder, { arg outbus=0, amp=1;
    var foa, out;

    // Test signal: panning noise
    foa = FoaPanB.ar( PinkNoise.ar, LFSaw.kr( 12.reciprocal, 1 ), 0 );

    // ~decoderMatrix is just a Matrix of decoding coefficients, so we use AtkMatrixMix
    out = AtkMatrixMix.ar(foa, ~decoderMatrix, amp);

    Out.ar(outbus, out);

}).load(Server.default);
)

~decoderSynth = Synth(\pinv_decoder, [\outbus, 0, \amp, -8.dbamp]);

// Scope the 9 channels of the decoded output
s.scope(9, 0);

// Clean up
~decoderSynth.free;

[1] - Hardin, R. H. and Sloane, N. J. A., "Sperical Designs", http://neilsloane.com/sphdesigns/, accessed on July 29, 2016.
[2] - ATK-Reaper will support matrix loading in an upcoming release.
[3] - Recall the ATK's matching A-format encoders and decoders!
[4] - This decoder design method is often described as pseudo-inverse or mode-matching.