Ich habe mehrere Projekte mit Audio auf unterschiedliche Weise erstellt: Midi-Dateien , Sprach-API , Aufruf einer Windows-DLL und Lesen/Schreiben von WAV-Dateien .
Dieses Mal werde ich wieder mit WAV-Dateien arbeiten. BlobToMatrix- und MatrixToBlob-Funktionen von JMP zum Bearbeiten von Blobs (Binary Large OBjects). LoadTextFile und SaveTextFile können Blobs lesen und schreiben. Eine WAV-Datei ist ein binäres Objekt (Sie werden nicht viel Freude daran haben, eine WAV-Datei in einem Texteditor zu öffnen). Die In-Memory-Darstellung eines binären Bytes in einer JSL-Matrix ist acht Bytes lang, was bedeutet, dass eine 1 GB (1,5 Stunden 16-Bit 44,1 kHz Stereo) WAV-Datei 8 GB Speicher benötigt. Für die 60-Sekunden-Datei, die ich benötige, ist dies kein Problem.Die gleichen Ideen können mit allen mittelgroßen Binärdateien verwendet werden, nicht nur mit WAVs, aber wie Sie sehen werden, ist das Dekodieren einer Binärdatei kompliziert.
In einem früheren Post Ich habe Daten aus Webprotokollen gesammelt; Ich verwende eine IP-to-Location-Datei von MAXMIND die IP-Adressen in Standorte und Ortsnamen umzuwandeln. Durch Aufteilen der Daten über die Zeit (die einen Monat lange Protokolldatei) kann ich ein Video erstellen. Hier ist der Graph Builder, der einen Frame zeigt. Ich mache 1800 (30 FPS * 60 Sekunden).
Color indicates five lat-lon clustersDie Farbe zeigt fünf Lat-Lon-Cluster an
Eine der Clustering-Plattformen von JMP verwendete Längen- und Breitengrad, um die Punkte in fünf Cluster zu gruppieren; Aktivität in jedem Cluster wird einen Aspekt der Musik dazu bringen, mit dem Video zu gehen. Der Musikerzeugungsalgorithmus verwendet a Zustandsmaschine daraus wurde gebaut diese Idee . Die Akkordübergänge werden von einer der Farbgruppen gesteuert und die Perkussionssektion von einer anderen sowie der Gitarre und der Bassgitarre. Es ist möglich, dass ich nicht alle fünf Farben verwendet habe; Ich hörte auf, wenn ich etwas hatte, das sich gut anhörte. Random ist kein guter Sound.
Die Instrumentenstimmen (Chor, Gitarre, Schlagzeug) werden aus WAV-Datei-Samples geladen, mit denen ich erstellt habe LMMS . Nach mehreren Fehlstarts entschied ich, dass der einfachste Weg, die Samples in JMP zu laden, darin bestand, einen vollständigen Satz von Noten für ein Instrument in einer Datei abzulegen, gleichmäßig verteilt, mit etwas Totzeit zwischen den Noten.
Really low guitar notes at the start of the WAV fileWirklich tiefe Gitarrennoten am Anfang der WAV-Datei
Der Chor ist derselbe und das Schlagzeug ist ähnlich. Speichern Sie diese Dateien und laden Sie sie dann wie folgt in JMP:
// RIFF loader
RiffUtil = New Namespace(
"RiffUtil"
);
Dies wird ein großes Projekt. Beginnen Sie also mit einem Namensraum, um die Details von anderen Teilen des Projekts zu isolieren. RIFF entpuppt sich als Name des Dateiformats hinter einer WAV-Datei. In diesem Namensraum gibt es mehrere Hilfsfunktionen. Als nächstes fügen Sie eine Funktion hinzu, um ein WAV zu laden.
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
);
Die meisten Kommentare stammen von der referenzierten Seite und beschreiben, wie die WAV-Datei geparst wird. Dieser Code verarbeitet ein WAV-Format und wirft, wenn es das falsche Format erhält. (16 Bit 44,1 kHz Stereo ist alles, was ich brauchte.) Natürlich ruft diese Funktion eine andere Ladefunktion auf, bevor sie irgendetwas anderes tut.
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
);
Diese Funktion erledigt den ersten Teil der schweren Arbeit, lädt den Blob in den Speicher, überprüft das RIFF-Tag, dann das WAVE-Tag und wirft dann einen Blick in die WAVE-Daten, um die benannten Unterblöcke aufzuteilen. Die frühere Funktion erledigt dann mehr Schwerstarbeit mit den fmt- und Daten-Chunks.
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 ruft 4 Bytes als JSL-String ab; andere Funktionen erhalten 1-, 2- oder 4-Byte-Zahlen. riff:offset ist eine Variable, die die aktuelle Position im Blob für das nächste zu verarbeitende Byte verfolgt. Beachten Sie oben, wie "vier" einer Matrix aus vier Elementen zugewiesen, dann in einen Blob konvertiert und dann in eine Zeichenfolge konvertiert wird. Dies geschieht ohne Schleife und läuft schnell ab. Hier sind die numerischen Versionen.
// 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];
);
Zweierkomplement ist ein Thema für ein anderes Mal. Wir brauchen später einen wavWriter; Hier ist es. Es hat die genau entgegengesetzten Funktionen für Zeichenfolgen und Zahlen.
// 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;
);
Vielleicht sind Ihnen die Anweisungen riff<<
Jetzt ist es an der Zeit, die Notizen zu laden. Ich werde einen anderen Namensraum für die Note Loader JSL erstellen, um sie isoliert zu halten.
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" macht das, worauf ich mich für dieses Projekt festgelegt habe; „Load“ blieb im Laufe des Projekts auf der Strecke. load1 verwendet den wav-Loader, um eine Matrix der Audiodaten zu erhalten, verwendet dann den Samples-Parameter, um die Daten schrittweise zu durchlaufen und ein assoziatives Array aus kürzeren Matrizen von Audiodaten für einzelne Noten zu erstellen. Firstnote ist die (von mir) der aufsteigenden Tonfolge zugeordnete Nummer.
Es gibt auch einen Namensraum für Komponisten; Der Komponist JSL weiß, wie man einer Komposition Noten hinzufügt.
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 );
);
Der Komponist weiß auch, wie man neu beginnt (neu) und beendet (speichert).
Hier ist der Ton in einem YouTube-Video. Die angehängten Dateien werden es ungefähr reproduzieren (aufgrund der zufälligen Auswahl), wenn Sie eine Kopie von LMMS erhalten, um die WAV-Dateien zu erstellen. Laden Sie die LMMS-Projekte und exportieren Sie sie.