기본 콘텐츠로 건너뛰기

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


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




댓글

이 블로그의 인기 게시물

세상 간단한 https(+secured websocket): Caddy

nginx, apache2 같은 걸로 매번 certbot 연동을 통해 https 하는게 지겨워서 알아보니 Caddy라는게 있더라. 설치법은 강하게 크기 위해 알아서 해본다. # cat /etc/caddy/Caddyfile your.shitty.site:8123 {   proxy / localhost:9123 {     websocket     transparent   } } 이건 외부에서 8123으로 들어오는 걸 내부적으로 9123로 맞춰서 https 맞춰주는 reverse proxy. 만일 port를 생략하면 # cat /etc/caddy/Caddyfile your.shitty.site {   proxy / localhost:9123 {     websocket     transparent   } } 기본포트인 443으로 되어 https://your.shitty.site 로 접속이 된다. 요새 말썽이던 cloudflare 버리고 netlify domain이랑 caddy를 쓰니까 앓던 이가 빠진 느낌. 이 조합은 당분간 계속 써야겠다 싶다.

LG G Pro 2 Custom Rom +Rooting 기록

선 루팅 https://forum.xda-developers.com/lg-g3/general/guide-root-lg-firmwares-kitkat-lollipop-t3056951 1.2버전 스크립트 모드로. echo Looking for LG serial port.. echo. adb.exe wait-for-device 여기에서 정체 상태여서 강제로 adb kill-server 했더니 알아서 재부팅하심. 폰에선 "펌웨어 업데이트"화면이 나오고 Phone found at COM5! Rooting phone.. If you don't see the SuperSu installer script runs within about a minute, then the root failed. Author : blog.lvu.kr SPECIAL COMMAND : ENTER, LEAVE ## 이런 상황이 됨. #이 느린 속도로 하나씩 증가하고 있음. 한 시간 기다려도 성과없음. 뭐 몇번해봐도 안되고 결국 불안하긴 하지만 kingroot.net  로 설치 롤리팝이니까  https://drive.google.com/file/d/0B4kfGU1z_PClQ3AyM2lKOGR1S3M/view  이걸 받아서 압축 풀기. d838recovery.bat 만 있어서 당황스럽긴한데 실행하고 1번 하니까 리커버리 모드 진입까진 잘 되는 듯. adb sideload 라는게 있어서 해봄. Advance에서 adb sideload 선택하고 USB연결한 PC에서 adb sideload RR-N-v5.8.5-20171010-d838-Final.zip 하니까 착착 잘 진행됨. 이제 흉측한 U+LTE 가 없어짐. ㅇㅋ 군더더기 없고 깔끔한데 남은 건 gapps 를 설치해야함. http://opengapps.org/  를 권해줘서 갔는데 지옥 느림. ARM + 7.1 + stock 이 기본이...