さまざまな方法でオーディオを使用していくつかのプロジェクトを作成しました。 midiファイル、音声API 、Windows DLL の呼び出し、およびWAVファイルの読み書き.
今回もWAVファイルで作業します。 JMP の BlobToMatrix および MatrixToBlob 関数は、ブロブ (Binary Large OBject) を操作します。 LoadTextFile と SaveTextFile は、BLOB の読み取りと書き込みの方法を認識しています。 WAV ファイルはバイナリ オブジェクトです (WAV ファイルをテキスト エディターで開いてもあまり楽しくありません)。 JSL マトリックスのバイナリ バイトのメモリ内表現は 8 バイトの長さです。つまり、1 GB (1.5 時間 16 ビット 44.1 KHz ステレオ) の WAV ファイルには 8 GB のメモリが必要です。 これは、必要な 60 秒のファイルでは問題になりません。同じアイデアは、WAV だけでなく、中程度のサイズのバイナリ ファイルにも使用できますが、ご覧のとおり、バイナリ ファイルのデコードは複雑です。
以前の役職Web ログからデータを収集しました。 IP-to-location ファイルを使用していますマックスマインドIP アドレスを場所と地名に変換します。 時間の経過とともにデータ (1 か月分のログ ファイル) をスライスすることで、ビデオを作成できます。 1 つのフレームを表示するグラフ ビルダーを次に示します。 1800 (30FPS * 60 秒) にします。
Color 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 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 は、次のバイトを処理するためにブロブ内の現在の位置を追跡する変数です。 上記の「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 は、昇順の音符に (私が) 割り当てた番号です。
composer 名前空間もあります。コンポーザ 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 );
);
作曲家は、最初からやり直す (新規) 方法と終了する (保存する) 方法も知っています。
こちらはユーチューブ動画の音声です。 WAV ファイルを作成するために LMMS のコピーを取得した場合、添付ファイルはほぼ (ランダムな選択のため) 再現されます。 LMMS プロジェクトをロードしてエクスポートします。