기본 콘텐츠로 건너뛰기

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);


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




이 블로그의 인기 게시물

vulcanJS - 10. Posts Update/Delete

마지막으로 수정과 삭제를 구현해보면 목록 조회(List), 상세 조회, 쓰기, 수정, 삭제까지 모든 필요한 요소를 아우를 수 있을 것이다.
감이 좋은 분들은 눈치 챘을지도 모르겠지만 사실 수정이란 건 UI면에서 볼때 이미 양식이 채워져있는 신규 쓰기와 별반 다르지 않다.

먼저 해야할 것은 역시나 Component를 만드는 일이다.
$ vulcan g component
? Package name spectrum-simplebb
? Component name PostsEditComponent
? Component type Class Component
? Register component Yes
   create packages/spectrum-simplebb/lib/components/PostsEditComponent.jsx
 conflict packages/spectrum-simplebb/lib/components/index.js
? Overwrite packages/spectrum-simplebb/lib/components/index.js? overwrite
    force packages/spectrum-simplebb/lib/components/index.js PostsEditComponent를 만들었다.
route도 만들자. /posts/edit/:id 이렇게 경로를 만들면 좋겠다. 그러고보니 이전 글에서 만든 상세보기도 /posts/view/:id 형식으로 만들껄 그랬다.
$ vulcan g route
? Package name spectrum-simplebb
? Route name postsEdit
? Route path /posts/edit/:_id
? Component name PostsEditComponent
? Layout name
 conflict packages/spectrum-simplebb/lib/modules/routes.js
? Overwrite packages/spectrum-simplebb/lib/modules/routes.js? overwrite
   …

vulcanJS - 1. vulcan-cli 설치하고 프로젝트 만들기

vulcanJS를 학습하면서 알아낸 점을 기록하는 의미의 튜토리얼을 써본다.

Telescope도 그랬지만 forum 형태의 예제에서 embedly를 사용하는 URL 요소가 글에 들어가는 점이나 일반인들에게는 익숙하지 않은 Markdown 문법 등을 걷어내고 최소형태에서부터 접근해야할 필요성을 느껴서 단순 post만 목록 열람하고 읽고 쓰는 구현을 해보기로 했다.

먼저, vulcanJS를 github를 통해 clone 하고 meteor create --package를 통해 만들수도 있지만 Vulcan-Cli(https://github.com/VulcanJS/vulcanjs-cli)가 있어서 이걸 일단 사용해보기로 한다.

대충 내용을 읽어보니 Rails 생각이 나기도 하고 재밌어 보인다.
npm install -g vulcanjs-cli 혹은 meteor 에서만 한다면
meteor npm install -g vulcanjs-cli 이런 식으로 설치하고 쓰면 된다.
npm 에 global로 설치한 첫번째 경우 기준으로 진행해보겠다.
설치하고 난 뒤 vulcan 을 실행해보면
$ vulcan
vulcan usage:
Synopsis
  vulcan <action> <object> <...>
    <action>   Operation to perform
    <object>   Asset type (contextual to action)
    <...>      Parameters. If not provided, interactively entered
Project initialisation
  vulcan create <appName>
  vulcan init <appName>
Assets creation
  vulcan (generate|g) package <packageName>
  vulcan (generate|g) model <packageName>…

Troubleshooting - Meteor package가 적용이 되지 않을 때

버전 1.5 기준 package.js에서 Package.onUse 에 새 패키지를 추가했는데 인식하지 못하는 경우가 있다.
Package.onUse((api) => {
  api.use([
    'vulcan:core',
    'vulcan:forms',
    'vulcan:accounts' /* <-- 추가함! */
  ]);
...
} 내부패키지건 원격패키지건 안되는 안된다. 이럴 때 meteor add 후 meteor remove 해도 되지만 더 간단한 방법이 있다. meteor update vulcan:accounts 이렇게 update 해주는 방법이 있다. .meteor/package 파일을 건들지 않아서 좋다. 그래도 역시 좋지 않다. Meteor 스럽지 않다. https://github.com/meteor/meteor/issues/7721 현재 1.5.2에서도 해결이 안되었군요. 해결되어 적용되면 다시 글 올리겠습니다.