Ho realizzato diversi progetti utilizzando l'audio in diversi modi: file midi , discorso api , chiamando una DLL di Windows e lettura/scrittura di file WAV .
Questa volta lavorerò di nuovo con i file WAV. Le funzioni BlobToMatrix e MatrixToBlob di JMP per operare sui BLOB (Binary Large OBjects). LoadTextFile e SaveTextFile sanno come leggere e scrivere BLOB. Un file WAV è un oggetto binario (non ti divertirai molto ad aprire un file WAV in un editor di testo). La rappresentazione in memoria di un byte binario in una matrice JSL è lunga otto byte, il che significa che un file WAV da 1 GB (1,5 ore, 16 bit 44,1 KHz stereo) richiede 8 GB di memoria. Questo non sarà un problema per il file di 60 secondi di cui ho bisogno.Le stesse idee possono essere utilizzate con qualsiasi file binario di dimensioni moderate, non solo WAV, ma come vedrai, la decodifica di un file binario è complicata.
In un precedente inviare Ho raccolto dati da web log; Sto usando un file da IP a posizione da MENTE MASSIMA per convertire gli indirizzi IP in posizioni e nomi di luoghi. Sezionando i dati nel tempo (il file di registro lungo un mese) posso realizzare un video. Ecco il generatore di grafici che mostra un frame. Farò 1800 (30FPS * 60 secondi).
Color indicates five lat-lon clustersIl colore indica cinque grappoli lat-lon
Una delle piattaforme di clustering di JMP utilizzava latitudine e longitudine per raggruppare i punti in cinque cluster; l'attività in ogni cluster guiderà alcuni aspetti della musica da accompagnare al video. L'algoritmo di generazione della musica utilizza a macchina a stati da cui è stato costruito questa idea . Le transizioni degli accordi sono guidate da uno dei gruppi di colori, la sezione delle percussioni da un altro, la chitarra e il basso. È possibile che non abbia usato tutti e cinque i colori; Ho smesso quando ho avuto qualcosa che suonava bene. Casuale non è un buon suono.
Le voci degli strumenti (coro, chitarra, percussioni) vengono caricate da campioni di file WAV con cui ho realizzato LMMS . Dopo diverse false partenze, ho deciso che il modo più semplice per caricare i campioni in JMP era inserire un set completo di note per uno strumento in un file, equidistanti, con un po' di tempo morto tra le note.
Really low guitar notes at the start of the WAV fileNote di chitarra molto basse all'inizio del file WAV
Il coro è lo stesso e le percussioni sono simili. Salva questi file, quindi caricali in JMP in questo modo:
// RIFF loader
RiffUtil = New Namespace(
"RiffUtil"
);
Questo sarà un grande progetto, quindi inizia con uno spazio dei nomi per aiutare a isolare i dettagli dalle altre parti del progetto. RIFF risulta essere il nome del formato file dietro un file WAV. Ci saranno diverse funzioni di utilità in questo spazio dei nomi. Quindi, aggiungi una funzione per caricare un WAV.
RiffUtil:loadWav = Function( {filename}, {Default Local},
riff = RiffUtil:load( filename );
{fmtOffset, fmtLength} = riff:chunks["fmt "];
riff:offset = fmtOffset;
// http://soundfile.sapp.org/doc/WaveFormat/
//JMP offset is 1-based, these are 0-based
//20 2 AudioFormat PCM = 1 (i.e. Linear quantization)
// Values other than 1 indicate some
// form of compression.
q = RiffUtil:getShort( riff, 1 );
riff:AudioFormat = q[1];
If( riff:AudioFormat != 1, Throw( "not PCM audio" ) );
//22 2 NumChannels Mono = 1, Stereo = 2, etc.
q = RiffUtil:getShort( riff, 1 );
riff:NumChannels = q[1];
If( riff:NumChannels != 2, Throw( "not 2-channel stereo" ) );
//24 4 SampleRate 8000, 44100, etc.
q = RiffUtil:getLong( riff, 1 );
riff:SampleRate = q[1];
If( riff:SampleRate != 44100, Throw( "not 44.1 kHz" ) );
//28 4 ByteRate == SampleRate * NumChannels * BitsPerSample/8
q = RiffUtil:getLong( riff, 1 );
riff:ByteRate = q[1];
If( riff:ByteRate != 44100 * 2 * 2, Throw( "not expected byte rate" ) );
//32 2 BlockAlign == NumChannels * BitsPerSample/8
// The number of bytes for one sample including
// all channels. I wonder what happens when
// this number isn't an integer?
q = RiffUtil:getShort( riff, 1 );
riff:BlockAlign = q[1];
If( riff:BlockAlign != 2 * 2, Throw( "not expected block align" ) );
//34 2 BitsPerSample 8 bits = 8, 16 bits = 16, etc.
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;// the namespace
result; // return integers or... /32768;// return normalized +/- 1.0
);
La maggior parte dei commenti sono presi dal sito di riferimento e descrivono come viene analizzato il file WAV. Questo codice gestisce un formato WAV e genera se ottiene il formato sbagliato. (Lo stereo a 16 bit a 44,1 KHz è tutto ciò di cui avevo bisogno.) Ovviamente questa funzione chiama un'altra funzione di caricamento prima di fare qualsiasi altra cosa.
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)" ) );
// walk the chunks and record their offsets
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;//return
);
Questa funzione esegue la prima parte del sollevamento di carichi pesanti, caricando il blob in memoria, verificando il tag RIFF, quindi il tag WAVE, quindi sbirciando all'interno dei dati WAVE per suddividere i sottoblocchi denominati. La funzione precedente quindi fa un lavoro più pesante con i blocchi fmt e dati.
RiffUtil:getString = Function( {riff},
{Default Local}, // return a 4-character id
four = riff:data[riff:offset + 0 :: riff:offset + 3];
riff:offset += 4;
Blob To Char( Matrix To Blob( four, "uint", 1, "big" ) );
);
getString recupera 4 byte come stringa JSL; altre funzioni ottengono numeri da 1-2 o 4 byte. riff:offset è una variabile che tiene traccia della posizione corrente nel BLOB per l'elaborazione del byte successivo. Nota sopra come a "quattro" viene assegnata una matrice di quattro elementi, quindi convertita in un blob, quindi convertita in una stringa. Questo viene fatto senza un ciclo e funziona velocemente. Ecco le versioni numeriche.
// these two transforms assist in the two's complement conversion; if the MSB is on
// ("negative") then also subtract maximum value
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];
);
Il complemento a due è un argomento per un'altra volta. Avremo bisogno di un wavWriter più tardi; Ecco qui. Ha le funzioni esattamente opposte per stringhe e numeri.
// wav writer...44.1 kHz 16 bit stereo
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 ); // pre-allocate buffer
wav:offset = 1; // 1-based
// http://soundfile.sapp.org/doc/WaveFormat/
//JMP offset is 1-based, these are 0-based
//0 4 ChunkID Contains the letters "RIFF" in ASCII form
// (0x52494646 big-endian form).
RiffUtil:putString( wav, "RIFF" );
//4 4 ChunkSize 36 + SubChunk2Size, or more precisely:
// 4 + (8 + SubChunk1Size) + (8 + SubChunk2Size)
// This is the size of the rest of the chunk
// following this number. This is the size of the
// entire file in bytes minus 8 bytes for the
// two fields not included in this count:
// ChunkID and ChunkSize.
RiffUtil:putLong( wav, J( 1, 1, 36 + 2 * uints ) );
//8 4 Format Contains the letters "WAVE"
// (0x57415645 big-endian form).
RiffUtil:putString( wav, "WAVE" );
//
//The "WAVE" format consists of two subchunks: "fmt " and "data":
//The "fmt " subchunk describes the sound data's format:
//
//12 4 Subchunk1ID Contains the letters "fmt "
// (0x666d7420 big-endian form).
RiffUtil:putString( wav, "fmt " );
//16 4 Subchunk1Size 16 for PCM. This is the size of the
// rest of the Subchunk which follows this number.
RiffUtil:putLong( wav, [16] );
//20 2 AudioFormat PCM = 1 (i.e. Linear quantization)
// Values other than 1 indicate some
// form of compression.
RiffUtil:putShort( wav, [1] );
//22 2 NumChannels Mono = 1, Stereo = 2, etc.
RiffUtil:putShort( wav, [2] );
//24 4 SampleRate 8000, 44100, etc.
RiffUtil:putLong( wav, [44100] );
//28 4 ByteRate == SampleRate * NumChannels * BitsPerSample/8
RiffUtil:putLong( wav, [176400] );
//32 2 BlockAlign == NumChannels * BitsPerSample/8
// The number of bytes for one sample including
// all channels. I wonder what happens when
// this number isn't an integer?
RiffUtil:putShort( wav, [4] );
//34 2 BitsPerSample 8 bits = 8, 16 bits = 16, etc.
RiffUtil:putShort( wav, [16] );
// 2 ExtraParamSize if PCM, then doesn't exist
// X ExtraParams space for extra parameters
//
//The "data" subchunk contains the size of the data and the actual sound:
//
//36 4 Subchunk2ID Contains the letters "data"
// (0x64617461 big-endian form).
RiffUtil:putString( wav, "data" );
//40 4 Subchunk2Size == NumSamples * NumChannels * BitsPerSample/8
// This is the number of bytes in the data.
// You can also think of this as the size
// of the read of the subchunk following this
// number.
RiffUtil:putLong( wav, J( 1, 1, 2 * uints ) );
//44 * Data The actual sound data.
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;//return
);
// should longs in wav files ever go negative? what about > 2GB data blocks?
RiffUtil:putLong = Function( {wav, mat},
{Default Local},
//show(mat);
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 has two stereo columns of -32k...+32k ints
mat[Loc( mat < 0 )] += 256 ^ 2; // two's complement: -1==65535 ... -32768 == 32768
len = N Rows( mat ) * N Cols( mat ); // len unsigned (or two's complement) 16-bit values (L+R)
mat = Shape( mat, len, 1 ); // now mat is a list of 16 bit samples, one per row
big = Floor( mat / 256 );
mat -= big * 256; // high bytes
little = mat; // low bytes
//show(nrows(mat),ncols(mat),wav:offset,len,nrows(wav:mat),ncols(wav:mat),nrows(little),ncols(little));
wav:mat[wav:offset + 0 :: wav:offset + 2 * len - 1] = Shape( little || big, 1, 2 * len );
wav:offset += 2 * len;
);
Potresti aver notato le istruzioni riff<<
Ora è il momento di caricare le note. Creerò un altro spazio dei nomi per il caricatore di note JSL per mantenerlo isolato.
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,
//show(firstnote,pos,pos+samples-1,nrows(temp),ncols(temp));
result:notes[firstnote] = temp[pos :: pos + samples - 1, 0];
firstnote += 1;
);
result; // return
);
"load1" fa quello che ho deciso per questo progetto; Il "carico" è caduto nel dimenticatoio man mano che il progetto andava avanti. load1 utilizza il caricatore wav per ottenere una matrice dei dati audio, quindi utilizza il parametro samples per scorrere i dati e creare un array associativo di matrici più brevi di dati audio per singole note. Firstnote è il numero assegnato (da me) alla sequenza ascendente di note.
C'è anche uno spazio per i nomi del compositore; il compositore JSL sa come aggiungere note a una composizione.
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; // normalize to +/- 20,000 out of +/- 32767
RiffUtil:wavWriter( filename, composer:mat );
);
Il compositore sa anche ricominciare (nuovo) e finire (salvare).
Ecco l'audio in un video di YouTube. I file allegati lo riprodurranno, approssimativamente (a causa delle scelte casuali), se ottieni una copia di LMMS per creare i file WAV. Carica i progetti LMMS ed esporta.