J'ai réalisé plusieurs projets utilisant l'audio de différentes manières : fichiers midi , API vocale , appelant une DLL Windows, et lire/écrire des fichiers WAV .
Cette fois, je travaillerai à nouveau avec des fichiers WAV. Fonctions BlobToMatrix et MatrixToBlob de JMP pour opérer sur des blobs (Binary Large OBjects). LoadTextFile et SaveTextFile savent lire et écrire des blobs. Un fichier WAV est un objet binaire (vous n'aurez pas beaucoup de plaisir à ouvrir un fichier WAV dans un éditeur de texte). La représentation en mémoire d'un octet binaire dans une matrice JSL fait huit octets, ce qui signifie qu'un fichier WAV de 1 Go (1,5 heure 16 bits 44,1 kHz stéréo) a besoin de 8 Go de mémoire. Ce ne sera pas un problème pour le fichier de 60 secondes dont j'ai besoin.Les mêmes idées peuvent être utilisées avec n'importe quel fichier binaire de taille moyenne, pas seulement avec les WAV, mais comme vous le verrez, le décodage d'un fichier binaire est compliqué.
Dans un précédent poste J'ai collecté des données à partir de journaux Web ; J'utilise un fichier IP vers emplacement de MAXMIND pour convertir les adresses IP en emplacements et noms de lieux. En découpant les données dans le temps (le fichier journal d'un mois), je peux créer une vidéo. Voici un générateur de graphiques montrant une image. Je ferai 1800 (30FPS * 60 secondes).
Color indicates five lat-lon clustersColor indicates five lat-lon clustersColor indicates five lat-lon clustersColor indicates five lat-lon clustersLa couleur indique cinq groupes lat-lon
L'une des plates-formes de clustering de JMP utilisait la latitude et la longitude pour regrouper les points en cinq groupes ; l'activité dans chaque cluster pilotera certains aspects de la musique pour accompagner la vidéo. L'algorithme de génération de musique utilise un machine à états qui a été construit à partir de cette idée . Les transitions d'accords sont pilotées par l'un des groupes de couleurs, et la section de percussions par un autre, ainsi que la guitare et la guitare basse. Il est possible que je n'aie pas utilisé les cinq couleurs ; J'ai arrêté quand j'avais quelque chose qui sonnait bien. Le hasard n’est pas un bon son.
Les voix d'instruments (choeur, guitare, percussions) sont chargées à partir d'échantillons de fichiers WAV que j'ai réalisés avec LMMS . Après plusieurs faux départs, j'ai décidé que le moyen le plus simple de charger les échantillons dans JMP était de mettre un ensemble complet de notes pour un instrument dans un seul fichier, régulièrement espacées, avec un certain temps mort entre les notes.
Really low guitar notes at the start of the WAV fileReally low guitar notes at the start of the WAV fileReally low guitar notes at the start of the WAV fileReally low guitar notes at the start of the WAV fileNotes de guitare très basses au début du fichier WAV
Le chœur est le même et les percussions sont similaires. Enregistrez ces fichiers, puis chargez-les dans JMP comme ceci :
// RIFF loader
RiffUtil = New Namespace(
"RiffUtil"
);
Il s'agira d'un projet de grande envergure, alors commencez par un espace de noms pour aider à isoler les détails des autres parties du projet. RIFF s'avère être le nom du format de fichier derrière un fichier WAV. Il y aura plusieurs fonctions utilitaires dans cet espace de noms. Ensuite, ajoutez une fonction pour charger 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 plupart des commentaires proviennent du site référencé et décrivent comment le fichier WAV est analysé. Ce code gère un format WAV et est lancé s'il obtient le mauvais format. (La stéréo 16 bits 44,1 kHz est tout ce dont j'avais besoin.) Bien sûr, cette fonction appelle une autre fonction de chargement avant de faire autre chose.
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
);
Cette fonction fait la première partie du gros du travail, chargeant le blob en mémoire, vérifiant la balise RIFF, puis la balise WAVE, puis jetant un coup d'œil à l'intérieur des données WAVE pour diviser les sous-morceaux nommés. La fonction précédente effectue alors un travail plus lourd avec les morceaux fmt et data.
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 récupère 4 octets sous forme de chaîne JSL ; d'autres fonctions obtiennent des nombres de 1, 2 ou 4 octets. riff:offset est une variable qui suit la position actuelle dans le blob pour le prochain octet à traiter. Notez ci-dessus comment « quatre » se voit attribuer une matrice de quatre éléments, puis convertie en blob, puis convertie en chaîne. Cela se fait sans boucle et s'exécute rapidement. Voici les versions numériques.
// 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];
);
Le complément à deux est un sujet pour une autre fois. Nous aurons besoin d'un wavWriter plus tard ; C'est ici. Il a les fonctions exactement opposées pour les chaînes et les nombres.
// 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;
);
Vous avez peut-être remarqué les instructions riff<<
Il est maintenant temps de charger les notes. Je vais créer un autre espace de nom pour le chargeur de notes JSL afin de le garder isolé.
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" fait ce que j'ai choisi pour ce projet ; La « charge » a été laissée de côté à mesure que le projet avançait. load1 utilise le chargeur wav pour obtenir une matrice des données audio, puis utilise le paramètre samples pour parcourir les données et créer un tableau associatif de matrices plus courtes de données audio pour des notes simples. Firstnote est le numéro attribué (par moi) à la séquence ascendante de notes.
Il existe également un espace de noms de compositeur ; le compositeur JSL sait ajouter des notes à une 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; // normalize to +/- 20,000 out of +/- 32767
RiffUtil:wavWriter( filename, composer:mat );
);
Le compositeur sait aussi recommencer (nouveau) et terminer (sauvegarder).
Voici l'audio dans une vidéo YouTube. Les fichiers joints le reproduiront, approximativement (en raison des choix aléatoires), si vous obtenez une copie de LMMS pour créer les fichiers WAV. Chargez les projets LMMS et exportez.