2012년 2월 2일 목요일

Web Audio로 악기를 만들자.

잉여력이 넘치던 꼬꼬마 시절. 10년전쯤이었나. 2000년 이전이었는지 이후였는지 가물가물.
친구 taekie군과 함께 Lazy Bird Cook Book 이란 프로젝트명을 걸고 wb303이라는 TB-303 머신을 만든 적이 있다.
생긴건 대충 이런 식.

구조는 건반을 찍고 나면 Music Macro Language(http://en.wikipedia.org/wiki/Music_Macro_Language) 비슷한 일련의 문자열을 생성하고 wav 파일을 생성하도록 인자를 POST하는 구조였는데
MSX에서 play 명령문으로 음악을 만들어보거나 마비노기로 연주를 만들어본 사람이라면 익숙한 구조일것이다.

C 1 ! _

첫번째는 C D E F G A B (도레미파솔라시도) 로 음계를
두번째는 옥타브를
그 뒤부터 붙는 !와 _ , x는 각각 accent와 slide, mute 여부를 표기하는 방식으로 극히 단순하지만 나름 효율적인 구조여서 게시판이나 메일, 메신저 등등으로 간편하게 주고 받을 수 있었는데 아무튼 나중엔 Propellerhead 사의 rb338 파일 저장형식인 rbs 포멧을 분석. rbs import 와 song pattern 도 가능했지만 당시에 호스팅했던 하나포스가 망해버리는 바람에 소스가 엉망이 되어버리긴했지만서도.

암튼 요즘은 세상이 좋아져서 Native Application에서나 가능했던 영역들이 속속 html5란 이름 아래 모여들고 있는데
Web Audio라는 기술이 생겨 관심있게 지켜보고 있어 뭐라도 안할 수가 없다.

뚜껑을 열어보니 생각보다 진지하게 만든 구조라서 좀 놀랐음.
https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html
풀 스펙은 위의 링크를 참조하면 됨.
low-pass filter, send effect, pan 등을 routing 하는 구조가 잡혀있음. 심지언 impulse response 까지 지원. 뭐야 무섭쟎아!
일단 빨리 뭐라도 소리를 내보고 싶어서 좀 쉬운 라이브러리가 없나 구글링을 해보니
FireFox와 Chrome 간의 구현방법이 달라서 jQuery 같은 표준라이브러리로 쓸만한 놈이 필요했다.
Sink.js(https://github.com/jussi-kalliokoski/sink.js)가 있고
거기에 FFT(고속푸리에변환)용 라이브러리랑, Wav codec 지원, binary data 처리 라이브러를 합쳐놓은 audiolib.js(https://github.com/jussi-kalliokoski/audiolib.js)가 있어 둘 다 써보기로 결정하고.

삐익하고 소리를 내보았다.

http://jsbin.com/uculiz/edit#javascript,html,live
먼저 audiolib.js를 써서



var dev, sampleRate, osc, buf, fx, filter;
var p = {
  onReady : function(){
    $("#play").click(p.onPlay);
  },
  onPlay : function() {
    setTimeout(function(){
      dev = audioLib.AudioDevice(null, 2);
      sampleRate = dev.sampleRate;
      buf = new Float32Array(sampleRate * 8);
      osc = new audioLib.Oscillator(sampleRate);
      osc.waveShape = 1;
      osc.append(buf.subarray(0, sampleRate * 1), 2);
      dev.writeBuffer(buf);
    }, 0);
  }
};

$().ready(p.onReady);

헉 html 파일에 오타있네.
요정도로 일단.
채널이 2개(stereo)인 AudioDevice를 생성하고
square 파형인 1초 분량의 440hz 음인 buffer 를 생성 후 writeBuffer로 소리를 출력하는 구조.

해놓고보니 sink.js 를 포함하고 있어서 그냥 이걸 쓰는게 낫겠다는 생각이 들었음.

waveShape따위를 쓰고 싶은 생각은 없어서 sink를 사용해서 직접 low-level로 버퍼를 핸들링함.
http://jsbin.com/uculiz/3/edit

var p = {
  channelCount : 1,
  preBufferSize : 0,
  sampleRate : 0,
  onReady : function(){
    $("#play").click(p.onPlay);
  },
  onPlay : function() {
    var dev = audioLib.Sink(function(sampleBuffer){
      // make some noise
      for (i=0; i<sampleBuffer.length; i++){
        sampleBuffer[i] = Math.random() - 0.5;
      }
    }, p.channelCount, p.preBufferSize, p.sampleRate);
  }
};

$().ready(p.onReady);


channelCount 를 제외한 나머지 preBufferSize나 sampleRate는 값을 바꿔도 별 효과가 없음.
-0.5~0.5 사이의 값을 무작위로 생성하여 화이트 노이즈를 생성했다.
play를 하면 무한 재생이 되는데 지금은 일단 모르겠고
의미있는 음도 재생해봐야겠다 싶어서 sin파를 생성해본다.

http://jsbin.com/uculiz/4/edit

var p = {
  channelCount : 2,
  frequency : 440,
  preBufferSize : 0,
  sampleRate : 44100,
  onReady : function(){
    $("#play").click(p.onPlay);
  },
  onPlay : function() {
    var dev = audioLib.Sink(function(sampleBuffer){
      // sampleBuffer length:8192
      for (i=0; i<sampleBuffer.length; i++){
        sampleBuffer[i] = Math.sin(i * Math.PI * p.frequency / p.sampleRate);
      }
    }, p.channelCount, p.preBufferSize, p.sampleRate);
  }
};

$().ready(p.onReady);

440hz의 sin파를 생성하려고 했는데 8192 버퍼마다 끊어짐.
음 뭔가 잘못했나 싶어서 여기저기 기웃기웃.

http://jsbin.com/uculiz/5/edit
Gain 을 달아주면 음량을 조절할 수 있다.
최대값이 1이므로 0이면 무음. 1이상이면 overdrive가 걸린다.

http://jsbin.com/uculiz/9/edit
녹음은 잘 되나?
아 record는 현재 장비에서 나는 음을 캡쳐하는 것이구나.
실시간 mixdown 같은 개념으로 보면 되겠네.


var p = {
  channelCount : 2,
  frequency : 440,
  preBufferSize : 0,
  sampleRate : 44100,
  gain:1.0,
  rec:null,
  dev:null,
  audioElement:null,
  onReady : function(){
    $("#rec").click(p.onRecord);
    $("#play").click(p.onPlay);
  },
  onRecord : function() {
    p.dev = audioLib.Sink(function(sampleBuffer){
      // make some noise
      for (i=0; i<sampleBuffer.length; i++){
        sampleBuffer[i] = Math.floor(i*440/this.sampleRate) % 2 ? 1:-1;
      }
    }, p.channelCount, p.preBufferSize, p.sampleRate);
    p.rec = p.dev.record();
    $("#log").text("recording....");
    /* To stop */
    setTimeout(p.recordStop, 4000);
  },
  recordStop: function() {
    p.rec.stop();
    p.dev.kill();
    // To export wav
    p.audioElement = new Audio(
      'data:audio/wav;base64,' + btoa( p.rec.toWav() )
    );
    p.audioElement.play();
    $("#log").text(p.rec.toWav().length);
  },
  onPlay: function() {
    p.audioElement.play();
  }
};

$().ready(p.onReady);


악! 크롬 다운되서 다날렸다;;
다음에 계속(...할 수 있으려나)