기본 콘텐츠로 건너뛰기

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


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




이 블로그의 인기 게시물

ESP32 DevBoard 개봉기

오늘 드디어 손에 넣었다. ESP32 DevBoard!
Adafruit 에서 15개 한정 재입고 트윗을 보고 광속 결제.
그리고 1주일의 기다림. 사랑해요 USPS <3
알리를 이용하다보니 1주일 정도는 광속 배송임.
물론 배송비도 무자비함 -_ㅜ
15개 한정판 adafruit 발 dev board
그놈이 틀림없으렸다.
오오 강려크한 포스
ESP32_Core_board_V2라고 적혀있군요.
ESP32 맞구요. 네네. ESP32-D0WDQ6 라고 써있는데 D → Dual-core 0 → No internal flash W → Wi-Fi D → Dual-mode Bluetooth Q → Quad Flat No-leads (QFN) package 6 → 6 mm × 6 mm package body size 라고 함.
길이는 이정도
모듈크기는 이정도
코어는 6mm밖에 안해! 여기에 전기만 넣으면 BLE+WIFI!
밑에 크고 발 8개 달린 놈은 FM25Q32라고 32Mbit 플래시메모리
ESP8266 DevBoard 동생이랑 비교 크고 아름다운 레귤레이터랑 CP2102 USB Driver가 붙어있음.
ESP8266 DevBoard엔 CH340G 인데 확 작아졌네.
머리를 맞대어 보았음.
모듈크기는 아주 약간 ESP32가 더 큰데 워낙에 핀이 많고 촘촘함. ESP8266인 ESP12는 핀 간격이 2.00mm인데 비해
ESP32는 1.27mm 밖에 안함.
딱봐도 비교가 될 정도.
https://www.sparkfun.com/news/2017 크고 아름다운 Pinouts

ESP8266 보드랑 별로 안달라보인다.
http://www.silabs.com/products/mcu/pages/usbtouartbridgevcpdrivers.aspx#mac
에서 CP2102 드라이버를 설치하고
screen 으로 연결해보자.
내 경우엔 tty.SLAB_USBtoUART 로 잡혔다.
어디서 기본 속도가 115200bps 라고 들은 적이 있어서
screen /dev/tty.SLAB_USBtoUART …

Rinkeby Test Network에 접근하는 간단한 방법.

dApp 개발 시 실제 계정으로 트랜젝션을 보내면 너무나 비싸므로
Rinkeby나 Ropsten 같은 테스트 네트워크에 연결하여 마이닝 없이 faucet을 통해 ether를 받고
그걸로 트랜젝션 테스트를 하면 편리하다.

보통 https://github.com/ethereum/wiki/wiki/Dapp-using-Meteor#create-your-%C3%90app 문서를 보고 시작하는데
geth --rpc --rpccorsdomain "http://localhost:3000" 이렇게 하면 마이닝부터 해야하니 귀찮다.
https://infura.io/#how-to 를 보고 계정을 신청하자. 이런 것도 호스팅이 되다니 좋은 세상이네.
간단한 개인 정보 몇가지를 입력하고 나면 Access Token이 나온다.

가입 후  https://infura.io/register.html 화면

Access Token이 있는 네트워크 주소로 geth를 연결한다.
geth --rpc --rpccorsdomain "https://rinkeby.infura.io/<YOUR_ACCESS_TOKEN>" 이러면 오케이.

meteor project를 만들고
meteor add ethereum:web3 추가한 다음 console에서
web3.eth.getBalance(web3.eth.coinbase, (error,result)=>console.log(
  error, result.toFormat()
)); 자신의 coinbase의 잔액을 구해보자.
6eth가 최소단위인 wei로 보면 6,000,000,000,000,000,000 정도.
https://faucet.rinkeby.io/ 여기에서 받아온 (무료로/마이닝없이) ether가 잘 나온다.
여기서부터 시작하는게 좋아보인다.

meteor로 nw.js 개발하기.

실제로 nw.js 어플리케이션을 개발하다보면 UI구현하기 막막하고 수동으로 리프레쉬 하는 것도 귀찮아서 Meteor 연동을 하려고 했더니 생각보다 간단했다.

디렉토리 구조는 먼저 이렇게 잡았다.
`- app
  `-client
  `-public
`- dist
  `- 배포용 html,css,js
  `- package.json
`- package.json 아이디어는 이렇다. nw.js의 시작페이지를 http://localhost:3000으로 두고 배포시엔 meteor client 배포툴인 meteor-build-client를 사용하여 html,css,js 로 분리하는 계획이다.

가장 중요한 nw.js 용 package.json 파일은 아래와 같이 구성한다.
{
  "main": "http://localhost:3000",
  "node-remote": "http://localhost:3000",
  "name": "<앱이름>"
} 이게 전부. 어떻게 보면 Web과 nw.js를 동시에 개발할 수도 있는 환경이기도 한 것이다.
meteor create app 을 해서 meteor용 앱을 만들고 meteor 를 시작한다.
그리고, 위의 package.json이 있는 경로로 돌아가서 nw . 으로 nwjs를 실행한다.

한번 번쩍하더니 잘 된다.
cordova 등에서 index.html 대신 http://localhost:3000을 하는 것도 비슷한 느낌이다.

즐겁게 개발을 일단 마구 하고 실제로 배포하기 위해서는 개발환경이 아니라 html,css,js로 구성된 배포본을 만들어야한다.
npm install -g 해도 되지만 어짜피 Meteor에서만 쓸거
meteor npm install -g meteor-build-client 해버릴거다.

개발은 문제 없어 보이고 배포판을 한번 만들어보자. meteor app 이 있는 경로(meteor run으로 …