私はオーディオをさまざまな方法で使用していくつかのプロジェクトを作成しました。 MIDIファイル、音声API 、Windows DLLの呼び出し、およびWAVファイルの読み書き。
今回は再び WAV ファイルを扱います。 JMP の BlobToMatrix 関数と MatrixToBlob 関数は、BLOB (Binary Large OBject) を操作します。 LoadTextFile と SaveTextFile は、BLOB の読み取りおよび書き込み方法を認識します。 WAV ファイルはバイナリ オブジェクトです (WAV ファイルをテキスト エディタで開いてもあまり楽しくありません)。 JSL マトリックスのバイナリ バイトのメモリ内表現は 8 バイト長です。つまり、1GB (1.5 時間の 16 ビット 44.1KHz ステレオ) WAV ファイルには 8GB のメモリが必要です。 私が必要とする 60 秒のファイルではこれは問題になりません。WAV だけでなく、中程度のサイズのバイナリ ファイルにも同じ考え方を使用できますが、ご覧のとおり、バイナリ ファイルのデコードは複雑です。
以前の役職私はウェブログからデータを収集しました。次の IP-to-location ファイルを使用しています。マックスマインドIP アドレスを場所や地名に変換します。 データ (1 か月の長さのログ ファイル) を一定期間にわたってスライスすることで、ビデオを作成できます。 これは 1 つのフレームを表示するグラフ ビルダーです。 1800 (30FPS * 60 秒) にします。
Color indicates five lat-lon clustersColor indicates five lat-lon clustersColor indicates five lat-lon clustersColor indicates five lat-lon clusters色は 5 つの緯度経度クラスターを示します
JMP のクラスタリング プラットフォームの 1 つは、緯度と経度を使用してポイントを 5 つのクラスターにグループ化しました。各クラスターのアクティビティによって、音楽の一部の側面がビデオに合わせて動かされます。 音楽生成アルゴリズムはステートマシンから構築されたものこのアイデア。 コードの移行はカラー グループの 1 つによって制御され、パーカッション セクションは別のカラー グループ、ギター、ベース ギターによって制御されます。 5 色すべてを使用しなかった可能性もあります。良さそうなものがあったのでやめました。 ランダムは良い音ではありません。
楽器の声 (合唱団、ギター、パーカッション) は、私が作成した WAV ファイルのサンプルから読み込まれます。 LMMS 。 何度か失敗を繰り返した結果、サンプルを JMP にロードする最も簡単な方法は、1 つの楽器の完全なノート セットを 1 つのファイルに均等に配置し、ノート間にデッド タイムを設けて配置することだと判断しました。
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 fileWAV ファイルの先頭にある非常に低いギターの音
合唱団も同じで、パーカッションも似ています。 これらのファイルを保存し、次のように JMP にロードします。
// RIFF loader
RiffUtil = New Namespace(
"RiffUtil"
);
これは大規模なプロジェクトになるため、プロジェクトの他の部分から詳細を分離しやすくするために、名前空間から始めます。 RIFF は、WAV ファイルの背後にあるファイル形式の名前であることが判明しました。 この名前空間にはいくつかのユーティリティ関数があります。 次に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
);
コメントのほとんどは参照サイトから取得したもので、WAV ファイルがどのように解析されるかを説明しています。 このコードは 1 つの WAV 形式を処理し、間違った形式を取得した場合にスローします。 (必要なのは 16 ビット 44.1KHz ステレオだけです。) もちろん、この関数は他の処理を行う前に別のロード関数を呼び出します。
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
);
この関数は、重い作業の最初の部分を実行し、BLOB をメモリにロードし、RIFF タグ、次に WAVE タグを検証し、次に WAVE データ内を覗いて名前付きサブチャンクを分割します。 その後、前の関数は fmt とデータ チャンクを使用してより重い作業を実行します。
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 は 4 バイトを JSL 文字列として取得します。他の関数は 1、2、または 4 バイトの数値を取得します。 riff:offset は、次に処理するバイトの BLOB 内の現在位置を追跡する変数です。 上記で、「four」に 4 つの要素の行列が割り当てられ、blob に変換され、さらに文字列に変換される様子に注目してください。 これはループなしで実行され、高速に実行されます。 ここでは数値バージョンを示します。
// 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];
);
2 の補数については別の機会に説明します。 後で wavWriter が必要になります。ここにあります。 文字列と数値に対してはまったく逆の機能があります。
// 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;
);
riff<<
今度はノートをロードします。 ノートローダー JSL を分離しておくために、別の名前空間を作成します。
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」は、私がこのプロジェクトで決めたことを実行します。プロジェクトが進むにつれて「負荷」は脇に落ちていきました。 load1 は、wav ローダーを使用してオーディオ データのマトリックスを取得し、次に、samples パラメーターを使用してデータをステップスルーし、単一ノートのオーディオ データの短いマトリックスの連想配列を作成します。 Firstnote は、一連の音符の昇順に (私が) 割り当てた番号です。
作曲家の名前空間もあります。作曲家の JSL は、作品に音符を追加する方法を知っています。
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 );
);
作曲家は、最初からやり直す (新規) 方法と終了する (保存する) 方法も知っています。
こちらはYouTubeビデオの音声です。 WAV ファイルを作成するための LMMS のコピーを入手すると、添付ファイルはほぼそれを再現します (ランダムな選択のため)。 LMMS プロジェクトをロードしてエクスポートします。