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:
Application Support
ATK
kernels
(ATK default)matrices
(ATK default)sounds
(ATK default)extensions
(your custom additions)kernels
matrices
FOA
decoders
encoders
xformers
HOA1
HOA2
...
HOAN
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.
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:
set | type | op | kind |
'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:
.txt
: the most basic text file, writing the raw matrix only..yml
: store the matrix along with metadata in a human readable format..mosl.txt
: a text file formatted for use with ATK-Reaper JSFX-plugins.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');
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 ); )
~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 ) )
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); )
/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');
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 ) )
~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;
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)};
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) }
-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;