J'ai réalisé plusieurs projets utilisant l'audio de différentes manières : fichiers midi , API vocale , en appelant une DLL Windows, et lecture/écriture de fichiers WAV .
Cette fois, je vais à nouveau travailler avec des fichiers WAV. Les 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 a une longueur de huit octets, ce qui signifie qu'un fichier WAV de 1 Go (1,5 heure 16 bits 44,1 KHz stéréo) nécessite 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 les WAV, mais comme vous le verrez, le décodage d'un fichier binaire est compliqué.
Dans un précédent Publier 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 vais faire 1800 (30FPS * 60 secondes).
Color indicates five lat-lon clustersLa couleur indique cinq grappes lat-lon
L'une des plates-formes de clustering de JMP utilisait la latitude et la longitude pour regrouper les points en cinq clusters ; l'activité dans chaque groupe conduira certains aspects de la musique à accompagner la vidéo. L'algorithme de génération de musique utilise un machine d'état qui a été construit à partir cette idée . Les transitions d'accords sont pilotées par l'un des groupes de couleurs, et la section de percussion par un autre, et 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. Random n'est pas un bon son.
Les voix des instruments (chœur, guitare, percussion) sont chargées à partir d'échantillons de fichiers WAV que j'ai créé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 fichier, régulièrement espacées, avec un temps mort entre les notes.
Really 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"
);
Cela va être un grand projet, 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 sont tirés du site référencé et décrivent comment le fichier WAV est analysé. Ce code gère un format WAV et le lance 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 quoi que ce soit d'autre.
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 effectue 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 séparer les sous-morceaux nommés. La fonction précédente fait alors plus de travail 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 ; les 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" est affecté à une matrice de quatre éléments, puis converti en blob, puis converti en chaîne. Cela se fait sans boucle et fonctionne 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 noms 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 ; "charge" est tombé au bord du chemin au fur et à 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 les notes individuelles. La première note est le numéro attribué (par moi) à la séquence ascendante de notes.
Il y a aussi un espace de nom 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 finir (sauvegarder).
Voici l'audio dans une vidéo youtube. Les fichiers joints le reproduiront, approximativement (à cause des choix aléatoires), si vous obtenez une copie de LMMS pour faire les fichiers WAV. Chargez les projets LMMS et exportez.