I've made several projects using audio in different ways: midi files, speech api, calling a windows DLL, and reading/writing WAV files.
This time I'll be working with WAV files again. JMP's BlobToMatrix and MatrixToBlob functions to operate on blobs (Binary Large OBjects). LoadTextFile and SaveTextFile know how to read and write blobs. A WAV file is a binary object (you won't get much joy opening a WAV file in a text editor). The in-memory representation of a binary byte in a JSL matrix is eight bytes long, which means a 1GB (1.5 hour 16-bit 44.1KHz stereo) WAV file needs 8GB of memory. This won't be an issue for the 60-second file I need. The same ideas can be used with any moderate sized binary files, not just WAVs, but as you'll see, decoding a binary file is complicated.
In a previous post I collected data from web logs; I'm using an IP-to-location file from MAXMIND to convert the IP addresses to locations and place names. By slicing the data across time (the one month long log file) I can make a video. Here's graph builder showing one frame. I'll make 1800 (30FPS * 60 seconds).
Color indicates five lat-lon clusters
One of JMP's clustering platforms used latitude and longitude to group the points into five clusters; activity in each cluster will drive some aspect of the music to go with the video. The music generating algorithm uses a state machine that was built from this idea. The chord transitions are driven by one of the color groups, and the percussion section by another, and the guitar, and the bass guitar. It's possible I didn't use all five colors; I stopped when I had something that sounded good. Random is not a good sound.
The instrument voices (choir, guitar, percussion) are loaded from WAV file samples that I made with LMMS. After several false starts, I decided the easiest way to get the samples loaded into JMP was to put a complete set of notes for one instrument in one file, evenly spaced, with some dead time between notes.
Really low guitar notes at the start of the WAV file
The choir is the same and the percussion is similar. Save these files, then load them in JMP like this:
RiffUtil = New Namespace(
"RiffUtil"
);
This is going to be a large project, so start with a name space to help isolate the details from other parts of the project. RIFF turns out to be the name of the file format behind a WAV file. There will be several utility functions in this name space. Next, add a function to load a WAV.
RiffUtil:loadWav = Function( {filename}, {Default Local},
riff = RiffUtil:load( filename );
{fmtOffset, fmtLength} = riff:chunks["fmt "];
riff:offset = fmtOffset;
q = RiffUtil:getShort( riff, 1 );
riff:AudioFormat = q[1];
If( riff:AudioFormat != 1, Throw( "not PCM audio" ) );
q = RiffUtil:getShort( riff, 1 );
riff:NumChannels = q[1];
If( riff:NumChannels != 2, Throw( "not 2-channel stereo" ) );
q = RiffUtil:getLong( riff, 1 );
riff:SampleRate = q[1];
If( riff:SampleRate != 44100, Throw( "not 44.1 kHz" ) );
q = RiffUtil:getLong( riff, 1 );
riff:ByteRate = q[1];
If( riff:ByteRate != 44100 * 2 * 2, Throw( "not expected byte rate" ) );
q = RiffUtil:getShort( riff, 1 );
riff:BlockAlign = q[1];
If( riff:BlockAlign != 2 * 2, Throw( "not expected block align" ) );
q = RiffUtil:getShort( riff, 1 );
riff:BitsPerSample = q[1];
If( riff:BitsPerSample != 16, Throw( "not expected bits per sample" ) );
Show( riff:AudioFormat, riff:NumChannels, riff:SampleRate, riff:ByteRate, riff:BlockAlign, riff:BitsPerSample );
{dataOffset, dataLength} = riff:chunks["data"];
riff:offset = dataOffset;
nShorts = dataLength / 2;
result = Shape( RiffUtil:getShort( riff, nShorts ), nShorts / 2, 2 );
riff << delete;
result;
);
Most of the comments are taken from the referenced site and describe how the WAV file is parsed. This code handles one WAV format and throws if it gets the wrong format. (16 bit 44.1KHz stereo is all I needed.) Of course this function calls another load function before it does anything else.
RiffUtil:load = Function( {filename}, {Default Local},
riff = New Namespace();
riff:data = Blob To Matrix( Load Text File( filename, BLOB ), "uint", 1, "big" );
riff:offset = 1;
If( RiffUtil:getString( riff ) != "RIFF", Throw( filename || " is not a valid wav (RIFF missing)" ) );
q = RiffUtil:getLong( riff, 1 );
riff:length = q[1];
If( RiffUtil:getString( riff ) != "WAVE", Throw( filename || " is not a valid wav (WAVE missing)" ) );
riff:chunks = Associative Array();
While( riff:offset < riff:length - 8,
chunkName = RiffUtil:getString( riff );
q = RiffUtil:GetLong( riff, 1 );
chunkLength = q[1];
riff:chunks[chunkName] = Eval List( {riff:offset, chunkLength} );
riff:offset += chunkLength;
);
riff;
);
This function does the first part of the heavy lifting, loading the blob into memory, verify the RIFF tag, then the WAVE tag, then peeking inside the WAVE data to split out the named sub-chunks. The earlier function then does more heavy lifting with the fmt and data chunks.
RiffUtil:getString = Function( {riff},
{Default Local},
four = riff:data[riff:offset + 0 :: riff:offset + 3];
riff:offset += 4;
Blob To Char( Matrix To Blob( four, "uint", 1, "big" ) );
);
getString retrieves 4 bytes as a JSL string; other functions get 1- 2- or 4-byte numbers. riff:offset is a variable that tracks the current position in the blob for the next byte to process. Note above how "four" is assigned a matrix of four elements, then converted to a blob, then converted to a string. This is done without a loop and runs fast. Here are the numeric versions.
RiffUtil:transform2 = ((0 :: 127) * (256 ^ 1)) || ((128 :: 255) * (256 ^ 1) - (256 ^ 2));
RiffUtil:getShort = Function( {riff, n},
{Default Local},
chooser = riff:offset + ((0 :: n - 1) * 2);
little = riff:data[chooser];
biggest = riff:data[chooser + 1];
riff:offset += 2 * n;
little + RiffUtil:transform2[biggest + 1];
);
RiffUtil:transform4 = ((0 :: 127) * (256 ^ 3)) || ((128 :: 255) * (256 ^ 3) - (256 ^ 4));
RiffUtil:getLong = Function( {riff, n},
{Default Local},
chooser = riff:offset + ((0 :: n - 1) * 4);
little = riff:data[chooser];
big = riff:data[chooser + 1];
bigger = riff:data[chooser + 2];
biggest = riff:data[chooser + 3];
riff:offset += 4 * n;
little + big * (256) + bigger * (256 ^ 2) + RiffUtil:transform4[biggest + 1];
);
Twos complement is a subject for another time. We'll need a wavWriter later; here it is. It has the exact opposite functions for strings and numbers.
RiffUtil:WavWriter = Function( {filename, mat},
{Default Local},
uints = N Rows( mat ) * N Cols( mat );
wav = New Namespace();
wav:mat = J( 44 + 2 * uints, 1, 0 );
wav:offset = 1;
RiffUtil:putString( wav, "RIFF" );
RiffUtil:putLong( wav, J( 1, 1, 36 + 2 * uints ) );
RiffUtil:putString( wav, "WAVE" );
RiffUtil:putString( wav, "fmt " );
RiffUtil:putLong( wav, [16] );
RiffUtil:putShort( wav, [1] );
RiffUtil:putShort( wav, [2] );
RiffUtil:putLong( wav, [44100] );
RiffUtil:putLong( wav, [176400] );
RiffUtil:putShort( wav, [4] );
RiffUtil:putShort( wav, [16] );
RiffUtil:putString( wav, "data" );
RiffUtil:putLong( wav, J( 1, 1, 2 * uints ) );
RiffUtil:putShort( wav, mat );
Open( Save Text File( filename, Matrix To Blob( wav:mat, "uint", 1, "big" ) ) );
wav << delete;
);
RiffUtil:putString = Function( {wav, txt},
{Default Local},
mat = Blob To Matrix( Char To Blob( txt ), "uint", 1, "big" );
len = N Rows( mat );
wav:mat[wav:offset + 0 :: wav:offset + len - 1] = mat;
wav:offset += len;
0;
);
RiffUtil:putLong = Function( {wav, mat},
{Default Local},
len = N Rows( mat ) * N Cols( mat );
biggest = Floor( mat / 256 ^ 3 );
mat -= biggest * 256 ^ 3;
bigger = Floor( mat / 256 ^ 2 );
mat -= bigger * 256 ^ 2;
big = Floor( mat / 256 );
mat -= big * 256;
little = mat;
wav:mat[wav:offset + 0 :: wav:offset + 4 * len - 1] = Shape( little || big || bigger || biggest, 1, 4 * len );
wav:offset += 4 * len;
);
RiffUtil:putShort = Function( {wav, mat},
{Default Local},
mat[Loc( mat < 0 )] += 256 ^ 2;
len = N Rows( mat ) * N Cols( mat );
mat = Shape( mat, len, 1 );
big = Floor( mat / 256 );
mat -= big * 256;
little = mat;
wav:mat[wav:offset + 0 :: wav:offset + 2 * len - 1] = Shape( little || big, 1, 2 * len );
wav:offset += 2 * len;
);
You might have noticed the riff<<delete and wav<<delete statements; this code creates temporary name spaces to hold objects; those name spaces should be deleted when no longer needed. In this case, they have done their work (reading data into a matrix, or writing a matrix to a file) and would just take up memory until JMP closes if they were not deleted.
Now it is time to load the notes. I'll make another name space for the note loader JSL to keep it isolated.
noteLoader = New Namespace(
"noteLoader"
);
noteLoader:load1 = Function( {path, samples, firstnote},
{Default Local},
Show( path );
result = New Namespace();
result:notes = Associative Array();
temp = RiffUtil:loadWav( path );
For( pos = 1, pos < N Rows( temp ) - samples, pos += samples,
result:notes[firstnote] = temp[pos :: pos + samples - 1, 0];
firstnote += 1;
);
result;
);
"load1" does what I settled on for this project; "load" fell by the way side as the project progressed. load1 uses the wav loader to get a matrix of the audio data, then uses the samples parameter to step through the data and create an associative array of shorter matrices of audio data for single notes. Firstnote is the number asigned (by me) to the ascending sequence of notes.
There is also a composer name space; the composer JSL knows how to add notes to a composition.
composer = New Namespace(
"composer"
);
composer:create = Function( {seconds},
{Default Local},
composer:mat = J( seconds * 44100, 2, 0 );
composer:choirPad4 = noteLoader:load1( path || "choir1/choirPad4.wav", 44100 * 8, 36);
composer:guitarVarA = noteLoader:load1( path || "Guitar1/varA.wav", 44100 * 2, 12 );
composer:guitarVarB = noteLoader:load1( path || "Guitar1/varB.wav", 44100 * 2, 12 );
composer:guitarVarC = noteLoader:load1( path || "Guitar1/varC.wav", 44100 * 2, 12 );
composer:guitarVarD = noteLoader:load1( path || "Guitar1/varD.wav", 44100 * 2, 12 );
composer:percus = noteLoader:load1( path || "Drum1/percus.wav", 44100 * 2, 1 );
);
composer:compose = Function( {instrument, note, position, volume}, {Default Local},
position = Round( 1 + position * 44100 );
duration = N Rows( instrument:notes[note] );
composer:mat[position :: (position + duration - 1), 0] += (instrument:notes[note]) * volume;
);
composer:new = Function( {}, {Default Local},
composer:mat[0, 0] = 0
);
composer:limit = Function( {}, {},
N Rows( composer:mat )
);
composer:save = Function( {filename}, {Default Local},
x = Max( Abs( composer:mat ) );
composer:mat = (20000 / x) * composer:mat;
RiffUtil:wavWriter( filename, composer:mat );
);
The composer also knows how to start over (new) and finish (save).
Here's the audio in a youtube video. The attached files will reproduce it, approximately (because of the random choices), if you get a copy of LMMS to make the WAV files. Load the LMMS projects and export.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.