다양한 방식으로 오디오를 사용하여 여러 프로젝트를 만들었습니다. 미디 파일 , 스피치 API , Windows DLL 호출 및 WAV 파일 읽기/쓰기 .
이번에는 WAV 파일로 다시 작업하겠습니다. JMP의 BlobToMatrix 및 MatrixToBlob 함수는 BLOB(Binary Large OBjects)에서 작동합니다. LoadTextFile 및 SaveTextFile은 블롭을 읽고 쓰는 방법을 알고 있습니다. WAV 파일은 바이너리 개체입니다(텍스트 편집기에서 WAV 파일을 여는 즐거움을 얻지 못할 것입니다). JSL 매트릭스에서 바이너리 바이트의 메모리 내 표현은 길이가 8바이트이므로 1GB(1.5시간 16비트 44.1KHz 스테레오) WAV 파일에는 8GB의 메모리가 필요합니다. 필요한 60초 파일에는 문제가 되지 않습니다.동일한 아이디어를 WAV뿐만 아니라 적당한 크기의 바이너리 파일에도 사용할 수 있지만 보시다시피 바이너리 파일을 디코딩하는 것은 복잡합니다.
이전에 우편 웹 로그에서 데이터를 수집했습니다. 에서 IP-to-location 파일을 사용하고 있습니다. 맥마인드 IP 주소를 위치 및 장소 이름으로 변환합니다. 시간에 따라 데이터를 분할하여(한 달 분량의 로그 파일) 비디오를 만들 수 있습니다. 다음은 하나의 프레임을 보여주는 그래프 빌더입니다. 1800(30FPS * 60초)으로 하겠습니다.
Color indicates five lat-lon clusters색상은 5개의 lat-lon 클러스터를 나타냅니다.
JMP의 클러스터링 플랫폼 중 하나는 위도와 경도를 사용하여 포인트를 5개의 클러스터로 그룹화했습니다. 각 클러스터의 활동은 비디오와 함께 음악의 일부 측면을 구동합니다. 음악 생성 알고리즘은 상태 머신 에서 지어진 이 아이디어 . 코드 전환은 색상 그룹 중 하나에 의해 구동되고 타악기 섹션은 다른 그룹과 기타 및 베이스 기타에 의해 구동됩니다. 다섯 가지 색상을 모두 사용하지 않았을 가능성이 있습니다. 좋은 소리가 났을 때 나는 멈췄다. 무작위는 좋은 소리가 아닙니다.
악기 보이스(합창, 기타, 타악기)는 내가 만든 WAV 파일 샘플에서 로드됩니다. LMMS . 몇 번의 잘못된 시작 후에 저는 샘플을 JMP에 로드하는 가장 쉬운 방법은 하나의 악기에 대한 완전한 음표 세트를 음표 사이에 약간의 데드 타임을 두고 균일한 간격으로 하나의 파일에 넣는 것이라고 결정했습니다.
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 파일이 어떻게 구문 분석되는지 설명합니다. 이 코드는 하나의 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 로더를 사용하여 오디오 데이터의 매트릭스를 가져온 다음 샘플 매개변수를 사용하여 데이터를 단계별로 실행하고 단일 음표에 대한 오디오 데이터의 더 짧은 매트릭스의 연관 배열을 만듭니다. 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 프로젝트를 로드하고 내보냅니다.