Ich habe mehrere Projekte mit Audio auf unterschiedliche Weise erstellt: Midi-Dateien , Sprach-API , Aufrufen einer Windows-DLL und Lesen/Schreiben von WAV-Dateien .
Dieses Mal werde ich wieder mit WAV-Dateien arbeiten. Die Funktionen BlobToMatrix und MatrixToBlob von JMP dienen zur Bearbeitung von Blobs (Binary Large OBjects). LoadTextFile und SaveTextFile wissen, wie Blobs gelesen und geschrieben werden. Eine WAV-Datei ist ein binäres Objekt (das Öffnen einer WAV-Datei in einem Texteditor wird Ihnen nicht viel Freude bereiten). Die speicherinterne Darstellung eines Binärbytes 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, stellt dies kein Problem dar.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 um die IP-Adressen in Standorte und Ortsnamen umzuwandeln. Durch Aufteilen der Daten über einen bestimmten Zeitraum (die einmonatige Protokolldatei) kann ich ein Video erstellen. Hier ist der Graph Builder, der einen Frame zeigt. Ich mache 1800 (30FPS * 60 Sekunden).
Color indicates five lat-lon clustersColor indicates five lat-lon clustersColor indicates five lat-lon clustersColor indicates five lat-lon clustersDie Farbe zeigt fünf Lat-Lon-Cluster an
Eine der Clustering-Plattformen von JMP nutzte Breiten- und Längengrade, um die Punkte in fünf Cluster zu gruppieren. Durch die Aktivität in jedem Cluster wird ein bestimmter Aspekt der Musik passend zum Video gesteuert. Der Musikerzeugungsalgorithmus verwendet a Zustandsmaschine das wurde daraus gebaut diese Idee . Die Akkordübergänge werden von einer der Farbgruppen gesteuert, die Percussion-Sektion von einer anderen sowie von der Gitarre und der Bassgitarre. Möglicherweise habe ich nicht alle fünf Farben verwendet. Ich habe aufgehört, als ich etwas hatte, das gut klang. Zufällig ist kein guter Klang.
Die Instrumentenstimmen (Chor, Gitarre, Percussion) werden aus WAV-Dateibeispielen 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 Noten für ein Instrument in einer Datei zusammenzufassen, in gleichmäßigen Abständen und mit etwas Totzeit zwischen den Noten.
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 fileWirklich tiefe Gitarrennoten am Anfang der WAV-Datei
Der Chor ist derselbe und die Percussion ist ähnlich. Speichern Sie diese Dateien und laden Sie sie dann wie folgt in JMP:
// RIFF loader
RiffUtil = New Namespace(
"RiffUtil"
);
Da es sich um ein großes Projekt handelt, beginnen Sie mit einem Namensraum, um die Details von anderen Teilen des Projekts zu isolieren. RIFF ist der Name des Dateiformats hinter einer WAV-Datei. In diesem Namensraum gibt es mehrere Hilfsfunktionen. Als nächstes fügen Sie eine Funktion zum Laden einer WAV-Datei hinzu.
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 Website, auf die verwiesen wird, und beschreiben, wie die WAV-Datei analysiert wird. Dieser Code verarbeitet ein WAV-Format und löst aus, wenn er das falsche Format erhält. (16-Bit-44,1-kHz-Stereo ist alles, was ich brauche.) Natürlich ruft diese Funktion eine andere Ladefunktion auf, bevor sie etwas 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, indem sie den Blob in den Speicher lädt, das RIFF-Tag und dann das WAVE-Tag überprüft und dann einen Blick in die WAVE-Daten wirft, um die benannten Unterblöcke aufzuteilen. Die frühere Funktion erledigt dann eine umfangreichere Arbeit mit den FMT- und Datenblöcken.
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 verfolgt, damit das nächste Byte verarbeitet werden kann. Beachten Sie oben, wie „vier“ eine Matrix aus vier Elementen zugewiesen, dann in einen Blob und dann in eine Zeichenfolge umgewandelt wird. Dies geschieht ohne Schleife und läuft schnell. 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];
);
Das Zweierkomplement ist ein Thema für ein anderes Mal. Wir werden später einen wavWriter brauchen; 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;
);
Möglicherweise sind Ihnen die Anweisungen riff<<
Jetzt ist es an der Zeit, die Notizen zu laden. Ich werde einen anderen Namensraum für die Notizlade-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, was ich 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 abzurufen, verwendet dann den Samples-Parameter, um die Daten schrittweise zu durchlaufen und ein assoziatives Array kürzerer Matrizen von Audiodaten für einzelne Noten zu erstellen. Firstnote ist die (von mir) der aufsteigenden Notenfolge zugewiesene Nummer.
Es gibt auch einen Komponisten-Namensraum; 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 endet (speichert).
Hier ist der Ton in einem YouTube-Video. Die angehängten Dateien reproduzieren es ungefähr (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.