저는 다양한 방식으로 오디오를 사용하여 여러 프로젝트를 만들었습니다. 미디 파일 , 음성 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-위치 파일을 사용하고 있습니다. 맥스마인드 IP 주소를 위치 및 장소 이름으로 변환합니다. 시간에 따라 데이터(한 달 길이의 로그 파일)를 분할하여 비디오를 만들 수 있습니다. 다음은 하나의 프레임을 보여주는 그래프 빌더입니다. 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의 클러스터링 플랫폼 중 하나는 위도와 경도를 사용하여 점을 5개의 클러스터로 그룹화했습니다. 각 클러스터의 활동은 비디오와 어울리는 음악의 일부 측면을 유도합니다. 음악 생성 알고리즘은 상태 머신 그것은에서 지어졌다 이 아이디어 . 코드 전환은 색상 그룹 중 하나에 의해 구동되고 타악기 섹션은 다른 그룹, 기타 및 베이스 기타에 의해 구동됩니다. 다섯 가지 색상을 모두 사용하지 않았을 수도 있습니다. 나는 좋은 소리가 났을 때 멈췄습니다. 무작위는 좋은 소리가 아닙니다.
악기 음색(합창, 기타, 타악기)은 제가 만든 WAV 파일 샘플에서 로드됩니다. LMMS . 여러 번의 잘못된 시작 후에 저는 JMP에 샘플을 로드하는 가장 쉬운 방법은 한 악기에 대한 완전한 노트 세트를 노트 사이에 약간의 데드 타임을 두고 균일한 간격으로 하나의 파일에 넣는 것이라고 결정했습니다.
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 파일이 구문 분석되는 방법을 설명합니다. 이 코드는 하나의 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의 현재 위치를 추적하는 변수입니다. 위에서 어떻게 "4"가 4개 요소의 행렬에 할당된 다음 블롭으로 변환되고 문자열로 변환되는지 확인하세요. 이는 루프 없이 수행되며 빠르게 실행됩니다. 숫자 버전은 다음과 같습니다.
// 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 );
);
작곡가는 다시 시작하는 방법(새로 만들기)과 마무리하는 방법(저장)도 알고 있습니다.
유튜브 영상에 나오는 오디오입니다. 첨부된 파일은 WAV 파일을 만들기 위해 LMMS 사본을 얻는 경우 대략(무작위 선택으로 인해) 이를 재현합니다. LMMS 프로젝트를 로드하고 내보냅니다.