piutranq-blog
piutranq-blog
Piu Tranquillo
27 posts
n00b developer
Don't wanna be here? Send us removal request.
piutranq-blog · 7 years ago
Text
[devlog:aoetracker] 2018/04/11
aoec에서 제너레이터의 볼륨값이랑 파형 신호값을 곱해 실제 신호값을 결정하는 amplitude 모듈은 이전까지는 이 실제 신호값을 16비트 양자화된 Hex값으로 보내었다.
왜냐하면 게임보이에서 볼륨을 디지털 단에서 처리, 16비트로 양자화시켜서 출력하는 줄 알고 있었기에 그렇게 처리했는데, 사실 지극히 당연하게도 그렇게 처리해버리면 실수값을 Hex로 변환하는 반올림을 거치므로 볼륨이 15(최대치)가 아닐 때 노이즈나 웨이브 파형이 왜곡되는 현상이 발생한다.
하지만 반올림을 거치지 않고 그냥 신호값 볼륨값의 곱으로 표현을 하게 되면, 이게 더이상 4비트 양자화가 아니라 8비트 양자화가 되어버리는게 아니냐. 는 생각에 반올림 과정을 포기하지 못하고 있었는데, 사실 다시 생각해보니 게임보이에선 볼륨이 다르다고 해서 파형이 왜곡되는 현상이 발생하지 않는다.
그래서 ggdev를 뒤적여본 결과... 게임보이의 볼륨 모듈은 아날로그 단에서 작동한다고. 그럼 볼륨을 적용한 신호값이 4비트 값일 필요가 없다.
결국 나도 amplitude 모듈을 아날로그 단으로 옮겨서 해결을 봄.
1 note · View note
piutranq-blog · 7 years ago
Text
.profile이 갑자기 안먹어서
검색을 해봤는데, zsh를 쓸 때는 .zprofile을 로드하지 .profile은 로드하지 않는다고 하더라.
그래서 .zprofile에 한줄만 써서 해결.
source .profile
0 notes
piutranq-blog · 7 years ago
Text
tmux 이거 간지 쩌는데?
갑자기 CLI뽕이 차올라서 tmux를 깔아보았다. 터미널을 여러 화면으로 분할할 수 있는 프로그램이라고. terminator 같은 GUI 기반의 분할 가능 터미널은 써봤는데 이 녀석은 완전히 CLI 상에서 돌아감.
vim은 아직 익숙하지 않지만 GUI가 싹 다 안될 때 최후의 보루로 쓸 수 있을 정돈 익혔으니 이제 내 코딩 환경을 CLI로 바꿀 시도를 해볼 수 있지 않을까 하는 생각. 성공할지는 모르겠다.
0 notes
piutranq-blog · 7 years ago
Text
[devlog:aoetracker] 2018/03/27
지금 프로젝트가 두 부분으로 나뉘어져있음.
사운드만을 담당하는 aoec와 나머지 대부분 기능을 담당하는 aoetracker
피치와 볼륨을 지정해 노트를 연주하고, 최소 시간 단위인 frame에 대해 엔빌로프, 세미톤/센트 단위의 피치 변화, 음색 변화 등을 명령어로 지정하여 반복하는 기능을 aoetracker 프로젝트에 만들었는데, 사실 이거 aoec로 가야하는거 아닌가 하는 생각에 이르름. 어디까지나 한 채널의 상태를 바꿔서 특정한 음높이, 볼륨, 음색의 소리를 내는거지 이걸로 연속적인 음악을 만드는건 아니게 되니까...
그와는 별개로, 이제 이렇게 커맨드로 컨트롤하는 기능을 만들었으니, 여러개의 커맨드를 묶어 한 마디(4박)나 그보다 긴 시간 단위를 나타낼 프레이즈를 만들어야 하는데, 이것도 막상 만들려니 생각보다 어려워서 막히게 된게 2주째인 것 같음. 후 어디 물어보기라도 할 수 있으면 좋겠는데.
0 notes
piutranq-blog · 7 years ago
Text
텀블러 불편한 점.
포스트를 작성한 뒤 수정하려고 하면 반이 넘게 잘려있음. 그래서 수정하려면 읽기 모드에서 싹 긁어서 (물론 서식이랑 그림, 미디어 등은 싹 다 날아간다) 붙여넣고 수정해야함.
마크다운 지원이 되어서 고른건데 표는 또 안써짐.
0 notes
piutranq-blog · 7 years ago
Text
외부 클럭에 의존하는 임의 속도의 내부 클럭 구현
다음과 같은 상황을 가정해보자.
그래픽에서 흔히 쓰이는 프레임 레이트는 60Hz이고, 우리는 초당 7px의 속도로 스프라이트를 이동시키고자 한다. 그럼 우리는 1f당 몇px만큼 이동해야 하는가? 0.1167이라는 대답은 단순히 px/f 값을 계산한 것 뿐이고, 옳은 대답은 아니다. 이유는 당연하다. 픽셀 값은 항상 정수이기 때문이다.
그렇다면 반대로 1px를 움직이기 위해 몇f를 기다려야 하는가? 같은 이유로 8.5714f라고 말할 수 없다. 하지만, 8f 또는 9f라고 대답할 수는 있을 것이다. 8f 또는 9f의 주기를 적절히 번갈아가면서 적용하면, 8.5714f/px에 근사한 주기로 픽셀을 움직일 수 있을 것이다. 여기서 우리에게 필요한 알고리즘은, 8.5714f/px에 근사한 주기를 얻기 위해 언제 8f 주기를, 언제 9f 주기를 적용할 것인가를 결정하는 것이다.
예를 들어, 아까의 가정인 7px/s가 아닌 6px/s의 속도로 움직인다고 가정해보자. 이때, 1px 이동하는 주기는 정확히 10f가 될 것이다. 이때 10f 주기마다 1px 움직이도록 하는 알고리즘은 알기 쉽다.
/* 이 코드 블록이 프레임 레이트 60Hz의 속도로 반복된다고 가정하자. */ if (frameCount % 10 === 0) { sprite.posX++ }
그렇지만, 8.5714f, 아니, 정확히 (60/7)f의 주기마다 움직이도록 하려면 어떻게 될까?
if (frameCount % (60/7) === 0) { sprite.posX++ }
이 if문 안의 코드는 의도했던 (60/7)f가 아닌, 60f의 빈도로 실행될 것이다. 왜냐하면 0에서 59까지, 60/7로 나눠떨어지는 정수는 0밖에 없고, 60에서 다시 0으로 나눠떨어지기 때문이다. 그런데, 0에서 59까지 60/7로 나눈 나머지 값을 잘 살펴보면, 60프레임이 반복될 동안 0, 9, 18, 26, 35, 43, 52번째의 총 7개 프레임에서 60/7로 나눈 나머지의 정수부가 0이 됨을 확인할 수 있다. 그리고 이들 프레임간의 간격은 8f 또는 9f.
Tumblr media
이는 즉, 60/7로 나눈 나머지의 정수부가 0인, 즉 나머지가 1보다 작은 프레임이 돌아올 때 마다 스프라이트를 움직이면 (60/7)f 주기에 근사하게 된다는 이야기이다.
if (frameCount % (60/7) < 1) { sprite.posX++ }
이 얘기를 좀 더 일반화해보자. 여기서 프레임 레이트는 60Hz라는 정해진 속도로 움직이는 외부 클럭이라고 부를 수 있지 않나 싶다. 그리고 7px/s의 속도로 스프라이트를 움직이는 함수는 외부 클럭에 의존하지만, 임의의 클럭 속도에 근사한 속도로 동작하는 내부 클럭이라고 부를 수 있지 않을까.
예시는 그래픽으로 들었지만, 필자의 경우는 Web Audio API를 통한 사운드 프로그래밍을 하면서 이 문제를 접했다.
AudioContext.ScriptProcessorNode는 사용자가 직접 작성한 스크립트를 통해 오디오 버퍼에 샘플을 기록하는데, 이렇게 샘플이 기록된 버퍼는 초당 44100Hz의 속도로 재생된다. 버퍼 쓰기 함수의 실제 호출 빈도라던가, 쓰기 함수 1회 호출에 몇개의 버퍼를 쓴다던가와는 무관하게, 오로지 사운드라는 측면에서만 바라본다면 1개의 버퍼에 샘플값을 쓰는 행위는 44100Hz 속도의 외부 클럭이라 볼 수 있다.
여기에 필자는 임의 주파수의 화이트 노이즈를 만들기 위해, 난수 생성기 클럭을 어느정도의 빈도로 작동시킬 것인가. 박자의 최소 단위인 tick를 어느정도의 빈도로 작동시킬 것인가 등을 구현해야 한다. 이것들은 모두 임의 속도의 내부 클럭이라고 볼 수 있을 것이다.
위의 예시에선 내부 클럭인 스프라이트 이동이 외부 클럭인 프레임 레이트보다 느렸지만, 내부 클럭이 더 빠른 경우도 있을 것이다. 다양한 경우에서 내부 클럭을 구현하는 방법을 알아보자.
내부 클럭이 더 느린 경우
아까의 경우와 같다. 내부 클럭이 작동할 때 까지 외부 클럭이 몇 클럭 작동해야 하는지 주기를 구해, 이 주기 값으로 외부 클럭 작동 횟수를 나눠, 그 나머지 값이 1 이하가 될 때 내부 클럭을 작동시키면 된다.
/* 역시, 블럭 안은 external.freq의 속도로 반복된다고 가정하자. */ internal.period = external.freq / internal.freq // 내부 클럭의 실행 주기 if (external.count % internal.period < 1) { internal.clock() }
내부 클럭이 더 빠른 경우
외부 클럭과 비교한 내부 클럭 속도 배율을 구한다. 예를 들어 그 배율이 2.37이 나왔다고 가정해보자. 이는 외부 클럭이 100회 작동할 동안, 내부 클럭이 237회 작동함을 의미한다.
외부 클럭 100회 중 내부 클럭 237회가 균일한 빈도로 작동하려면, 외부 클럭 1회에 내부 클럭이 최소 2회 작동해야 하고, 추가로 100회 중 37회가 균일한 빈도로 작동해야 한다.
이는 매번의 외부 클럭마다 속도 배율 2.37의 정수부 2만큼 내부 클럭이 반복 작동한 뒤, 소수부 0.37의 역수인 100/37 주기로 내부 클럭이 추가 작동한다는 이야기이다. 이를 코드로 나타내면 다음과 같다.
internal.freqRatio = internal.freq / external.freq // 외부 클럭과 비교한 내부 클럭의 속도 배율 internal.repeat = Math.floor(internal.freqRatio) // 반복 횟수는 속도 배율의 정수부 internal.period = Math.pow(internal.freqRatio - internal.repeat, -1) // 추가 실행 주기는 속도 배율의 소수부의 역수 for (let i = 0; i < internal.repeat; i++) { internal.clock() } if (external.count % internal.period < 1) { internal.clock() }
매 클럭마다 speedRatio, repeat, period를 계산할 필요가 없을 것이다. 내부 클럭의 속도가 바뀔 때 계산하도록 바꿔보자.
internal.setFreq = function (freq) { this.freq = freq this.freqRatio = freq / external.freq this.repeat = Math.floor(this.speedRatio) this.period = Math.pow(this.speedRatio - this.repeat, -1) } /* 이 메소드만 external.freq의 속도로 작동한다고 가정 */ external.clock = function () { for (let i = 0; i < internal.repeat; i++) { internal.clock() } if (external.count % internal.period < 1) { internal.clock() } }
이 방식은 내부 클럭이 더 느린 경우도 커버할 수 있다. 왜냐하면, 내부 클럭이 더 느리다면 internal.freqRatio가 1보다 작아지므로, 정수부는 0, 소수부는 internal.freqRatio와 같아지고, internal.period는 소수부의 역수이므로 external.freq / internal.freq와 같아지기 때문이다.
이는 내부 클럭이 0.37 배속일때, 외부 클럭 1회당 내부 클럭이 0회 반복 작동한 뒤, 그리고 추가로 외부 클럭 100회당 내부 클럭 37회 꼴로 추가로 반복하는 것을 의미한다.
0 notes
piutranq-blog · 7 years ago
Text
[devlog:aoetracker] 2018/3/16
aoetracker의 사운드 엔진인 aoec에서, 내장 파형, 커스텀 파형 제너레이터의 작동 방식은 샘플러가 작동한 횟수를 패러미터로 받아, 주파수와 비교해서 1주기의 파형 중 어느 부분을 읽을것이냐. 즉 위상각을 결정한 뒤 그 위상각에 해당하는 파형값을 읽는 것이다. aoec에선 파형 1주기가 32개 파형값으로 이뤄져있으므로 위상각이 1/32 단위로 계산된다. 하지만 후술할 내용에서 설명의 편의를 위해 위상각은 아날로그값이라고 가정하겠다.
일반적인 경우 제너레이터의 위상각은 당연히 순차적으로 증가한다. 440Hz 주파수라면 샘플러가 약 100회 진행할 때 마다 1주기만큼의 파형을 읽어야 하므로, 샘플러가 1회 진행할 때 마다 약 0.01만큼의 위상각이 증가할 것이다.
하지만 주파수가 바뀐다면, 주파수가 바뀐 뒤에 계산된 위상각은 주파수가 바뀌기 이전의 위상각과는 전혀 무관하다. 방금전까지 위상각이 0.04였는데, 주파수가 바뀌고 위상각을 새로 계산했더니 0.54로 건너뛰었다고 가정해보자. 이것은 샘플러가 한번 작동했는데 위상각이 0.5 증가, 즉 파형이 0.5주기만큼 진행되었단 이야기이고, 즉 주파수가 순간적으로 22050Hz가 되었단 얘기이다.
그렇기 때문에 실제로 aoec에선 주파수를 바꿀 때 마다 아주 잠깐의 고주파음이 들린다. 이게 한두번이면 큰 문제는 아니겠지만, 이를테면 칩튠에서 매우 자주 사용되는 아르페지오 주법을 사용한다면 매 프레임마다 주파수가 바뀌니 이 고주파음이 엄청 불편할 것이다.
aoec의 노이즈 제너레이터는 이 위상각 계산 방식을 사용하지 않는다. 매 클럭마다 랜덤값을 반환하는 LFSR을 사용해 파형값을 읽으므로, 파형의 어느부분을 읽을 지 결정할 수 없고 항상 다음 파형값만을 읽어올 뿐이다. 건너뛰지 않고 항상 다음 파형값을 읽어들이기 때문에, 이 방식은 주파수가 바뀌어도 고주파음이 발생하지 않는다. 그렇기 때문에 나는 내장, 커스텀 파형 제너레이터에도 이 방식을 적용하기로 결정했다.
0 notes
piutranq-blog · 7 years ago
Text
git의 사용법에 대해 고민 (1)
나는 git과 github를 조별 과제 등 여러명이 함께 코드를 짜는 환경에서 사용해본 적이 없다. 개인 프로젝트, 또는 조별 과제에서 내가 담당한 코드'만' 관리하는 용도로 사용하고 있어서, git을 잘 사용하는 방법을 모른다. 지금까지는 그래도 괜찮았다. 아무때나 커밋을 하고 아무때나 푸시를 해도 어차피 내 프로젝트의 기여자는 나 밖에 없으니까.
하지만 지금 만드는 중인 개인 프로젝트(aoec, aoetracker)가 점점 진행되면서, 이대로는 안된다 하는 생각이 들기 시작한다. 다른 포스트에도 얘기했듯, 나 혼자 하는 개발이란 있을 수 없고, 모두 다른 시간대의 나와 같이 하는 개발이다. 한달동안 코드를 짰다면, 오늘의 나는 지난 한달간 코드를 짠 30명의 나, 그리고 프로젝트가 완성될때까지 매일 코드를 짤 몇백명의 나와 같이 작업하는 것이다. 미래의 내가 '트랑새기 코드 개같이 짰네' 라고 말하지 않을 코드를 짜야 한단 것이다. 그리고 그건 git 사용에서도 결코 예외가 될 수 없다.
내가 git을 처음 쓰면서는, 커밋을 매우 불규칙하게 하고, 커밋을 할 때 마다 푸쉬를 했다. 그냥 이때쯤 저장해야겠다 싶을 때 Ctrl+S 누르듯 github에 올린 것이다. 그렇기 때문에, 만들다 만 함수나 클래스, 모듈 등을 커밋하는가 하면, 이전 커밋에선 멀쩡히 돌아가던 부분이 안돌아가는 문제가 발생했는데도 그걸 방치하고 커밋을 하는가 하면... 총체적 난국이었다.
그러다가, 커밋의 단위는 의미 단위가 되어야 한단 얘길 들었다. 그 뒤로는 적어도 작동하지 않는 코드는 커밋하지 않았다. 하지만 이는 뭔가 강박관념 비슷한게 되었다고 할 까, 이제 작업을 끝내야만 하는데, 지금까지 짠 코드가 하나의 의미 덩어리가 되질 못하면 커밋을 하지 못하고, 그냥 Ctrl+S만 누르고 나오기엔 깔끔하지 않아 불안하고, 결국엔 다 포기하고 git checkout 명령으로 변경사항을 싹 날린 적도 있고...
그 와중에 rebase와 merge라는걸 접하게 되었는데, 여러개의 커밋을 합칠 수 있다고 한다. 하지만 이는 이미 push를 해버린 코드에 대해선 이런저런 충돌때문에 쓸 수 없다 해서 안쓰고 있었다. 내 원격 저장소는 데스크탑 / 노트북간에 로컬 저장소를 공유하는 일종의 클라우드 스토리지 용도로 사용되고 있었기 때문이다.
그리고 꽤 최근에는 그냥 아무때나 커밋/푸시할 용도로, 이력을 지저분하게 써도 되는 저장소와, 외부에 공개할 목적으로 이력을 깔끔하게 써야 하는 저장소로 구분하면 어떨까? 하는 생각에 이르렀고, 그제서야 잊고있더던 무언가가 떠올라버렸다. 지금까지, 나는 브랜치를 master 하나만 쓰고 있었던 것이다.
근데 브랜치를 새로 따서 구분해서 쓴다고 해도, 내가 지금 겪고있는 고민을 해결할 수 있을까? 그것을 장담할 수 없다는건 내가 git의 기능에 대해서 제대로 모르고 있다는 얘기이다. git을 따로 공부해보던가 해야겠다...
0 notes
piutranq-blog · 7 years ago
Text
유닛 테스트를 처음 써보다
지금 만들고 있는 칩튠 트래커의 코드를 짜면서 뭔가 막히는 듯 한 기분이 있는데, 사운드 엔진을 만들 때와는 달리 이 기능을 완성하려면 저 기능을 완성해야는 식으로 물고 물린게 너무 많으니 테스트가 자꾸 미뤄지고, 그러면 지금 짜고 있는 코드가 제대로 짜여지는게 맞는건가 불안해지기 시작한단 것이다.
더군다나 예전엔 웹팩으로 묶은 번들을 한꺼번에 브라우저에 불러와, API를 하나하나 브라우저 콘솔에 쳐가면서 수동으로 테스트를 돌렸는데, 아직까지 브라우저에선 모듈 문법을 지원하지 않다보니 특정 모듈만 개별적으로 불러서 테스트하려는데 거기 딸린 모듈이 또 있다면 골치아픈 것이다.
그래서 노드에서 테스트를 돌리고자 하는데, 노드의 REPL 환경을 쓰자니 상당히 불편해서 테스트를 자동화하기로 했음. 이 시점에서, 학교에서 얼핏 들어보기만 한 (분명 학교에서 배웠을텐데 그때 수업시간에 잤을거다.) 유닛 테스트란 말이 떠올라, 구글링으로 좀 찾아본 뒤 mocha를 깔아서 써보았다.
사용법은 매우 쉽고, 단지 테스트 시나리오를 짜는게 익숙치 않아 좀 헤맸다. 굳이 모든 함수에 테스트를 만들 필요는 없는 것 같고, 패러미터 타입 체크가 제대로 이뤄졌는지 여부나, 단순한 get/setter 메소드에 대해선 테스트 케이스를 작성하지 않음. 다만 조금이라도 확신할 수 없는 로직이 들어가있으면 테스트 케이스를 만들기로 했다.
0 notes
piutranq-blog · 7 years ago
Text
[devlog:aoetracker] 2018/2/27
사운드 엔진인 aoec의 v0.1.0을 배포했다. (npm, github)
나 혼자 쓰려고 만드는거긴 한데 사운드 엔진과 트래커를 프로젝트를 분리할 필요가 생기다보니 아예 배포를 염두에 두고 만들어버린 것.
그러다보니 신경써야 할 게 많은데, 누군가가 이 라이브러리를 쓴다고 가정하면 어렵지 않게 사용할 수 있도록 API를 깔끔하게 잘 정리하고, 문서화도 해놔야 하고... 여간 귀찮은게 아니지만, 내가 쓰려고 만든거니까 미래의 내가 쓴다고 생각하자.
얼마 전부터 느끼던건데, 혼자 하는 코딩같은건 없다. 혼자 하는 것 같아도, 항상 미래의 나와 같이 짜고 있는거라 생각하는게 옳은거임.
하여튼 그래서 문서화는 천천히 하기로 하고, 릴리즈 전에 API를 모두 정리해놨다. 숨길건 싹 숨기고 필요한것만 꺼내놓고. index.js가 캡쳐 처럼 깔끔하게 정리되니 뿌듯함.
Tumblr media
0 notes
piutranq-blog · 7 years ago
Text
[devlog:aoetracker] 2018/02/26
업로드 날짜는 27일 01시이지만, 26일에 작성한 코드 얘길 26일 25시에 업로드하는 것이니 제목엔 26일로 적었다.
4비트 양자화된 칩튠 사운드의 신호값은 0에서 F까지의 16진 정수이다.
내가 만든 제너레이터는 1차적으로 진폭이 F인 파형값을 반환하며, 여기에 제너레이터의 볼륨을 적용하면 0~F 사이의 진폭을 갖는다. 여기서 진폭 적용은 잘 되고 볼륨이 F일땐 멀쩡한데, 볼륨이 낮아지면 골은 0인 채 그대로이고 F였던 마루가 볼륨값 만큼 낮아져 신호가 아랫방향으로 편향되는 문제가 생겼다.
이렇게 되면, 여러 채널에서 합성된 신호값의 편향이 누적되어, 편향이 없었다면 클리핑이 일어나지 않을 믹싱이 -1.0 밑으로 내려가는 클리핑이 발생하게 된다. 특히 채널이 조금만 더 많아져도 그 누적이 심해져, 내 경우는 아날로그 값이 -1.0 ~ -2.0을 찍어 아예 들리지 않는 경우도 겪었다.
이 문제를 해결하려면 볼륨을 적용할 때 다음 식을 합산해 하향 편향된 16진 신호를 다시 위로 들어올려줘야 한다.
Math.floor((16-vol)/2)
vol은 볼륨 값, Math.floor는 자바스크립트에서 소숫점 아래를 자르는 함수이다. 이렇게 하면, 볼륨 값이 홀수일때 7.5, 짝수일때 8을 중심으로 고르게 분포된 파형이 될 것이다.
마지막으로, 볼륨이 0일 때의 처리를 해줘야 하는데, 왜냐하면 0을 -1.0, F를 +1.0에 대응시키면 중앙인 0.0에 해당하는 값이 7.5로, 16진 정수로는 표현할 수 없기 때문이다.
그래서 위의 식에 볼륨 0을 대입하면 8이 계속 이어지는 플랫라인이 되는데, 이정도의 사소한 상향 편향은 별 거 아닐 수 있지만, 누적되면 클리핑의 원인이 되기 때문에 볼륨이 0인 트랙에 대해선 아날로그 값 역시 0.0으로 처리해줘야 한다.
16진 값을 7.5를 반환할 수 없는 실제 하드웨어 또는 에뮬레이터 등의 로우레벨 코드에선 꺼져있는 제너레이터의 16진 신호를 처리하지 않는 방식으로 하겠지만, 나는 코드 작성의 편의를 위해 볼륨이 0이거나, 꺼져있는 제너레이터가 7.5를 반환하도록 했다. 어디까지나 타입 구분을 하지 않는 자바스크립트이기에 가능한 일이다.
0 notes
piutranq-blog · 7 years ago
Text
[devlog:aoetracker] 2018/02/24
youtube
프레임 시퀀서를 만들었다. 무슨 역할이냐면, 여기에다가 여러 프레임들을 묶은 리스트를 지정하면 반복재생 해주는 것이다. 음표 하나에 해당하는 프레임들을 재생할 목적으로 만든건데, 영상에선 뭔가 좀 본격적으로 만들어진 걸 보여주고 싶어서 1마디 통째로 찍는 노가다를 했음.
Tumblr media
주파수 값, 파형 번호, 볼륨 값을 하나하나 지정해줘야 한다. 이걸로 실 사용은 다메 다메
0 notes
piutranq-blog · 7 years ago
Text
같은 모듈을 다른 상수에 두번 할당하면
이를테면 이런 상황 말이다.
// mdl.js let value = 'THIS IS SPARTA!!' function write (str) { value = str } function read (str) { return value } module.exports = { write: write, read: read }
// test.js const mdl1 = require('./mdl') const mdl2 = require('./mdl') mdl1.write('emmmm... value is changed') console.log(mdl1.read()) console.log(mdl2.read())
두 모듈이 서로 다른 스코프에 선언되어 mdl1의 value만 바뀌고 mdl2는 그대로이진 않을까? 하고 생각해서 실험해보았지만, 결과는 아래 그림 처럼 두개 다 바뀜.
Tumblr media
결국 모듈도 객체라서 두번 할당해도 하나의 모듈 객체만 참조한단 얘기이다. 혹시 deep copy를 할 수 있다면 서로 다른 스코프에 할당이 되지 않을까? 이건 나중에 한번 더 해봐야겠다.
0 notes
piutranq-blog · 7 years ago
Text
[devlog:aoetracker] 2018/2/23 (2)
먼저 올린 포스팅에서 발생한 문제를 해결한 방법은 이러하다.
onaudioprocess 함수의 호출은 불규칙적이지만, 그것이 작성한 파형이 재생되는건 당연히 샘플링 레이트 44100Hz에 맞춰 정확한 타이밍으로 돌아간다.
그렇기 때문에, 파형값을 하나하나 읽을 때 마다 44100Hz의 클럭이 하나씩 작동했다고 가정, 프레임 간격을 클럭 단위로 표현. 그리고 클럭 횟수가 프레임 간격에 도달할 때 마다 다음 프레임을 읽는 방식이다.
이걸 코드로 나타내면 이러하다.
/* ...... */ /* 파형값을 읽은 횟수. 초당 44100회 */ let clock = 0 /* 프레임 간격. 1/44100초 단위로, 459는 약 104ms */ let frameInterval = 459 /* this.processor는 ScriptProcessorNode */ this.processor.onaudioprocess = function (e) { /* ...... */ /* 이 루프에서 파형값을 읽어와 아웃풋 버퍼에 작성 */ for (let i = 0; i < buffSize; i++) { /* ...... */ clock++ if (clock % frameInterval === 0) { /* 다음 프레임을 읽는 함수를 여기에 둔다 */ self.getNextFrame() } } }
clock 값을 적당한 곳에서 초기화시키지 않으면 무한정 늘어나는데, 사실은 무한정 늘어나도 상관없다. 왜냐하면, 1시간동안 초기화시키지 않아도 고작 158760000인데, 이정도면 충분히 안전한 정수값이기 때문이다.
A4 C#5 E5 A5의 4화음을 프레임마다 차례로 반복하도록 지정하고, 프레임 간격을 50 (약 1.114ms) 까지 내려가며 확인해봤는데, 제대로 소리가 나는걸 보면 이정도면 충분히 정밀한 시간 간격이라고 판단된다.
0 notes
piutranq-blog · 7 years ago
Text
[devlog:aoetracker] 2018/2/23
내장된 파형, 노이즈, 그리고 커스텀 파형의 구현이 끝났고, 샘플러만 구현하면 사운드 엔진의 제작은 끝인 상황에서 한가지 고민에 빠졌었다. 그것이 뭐냐 하면, ScriptProcessorNode가 파형을 처리하는 방식 때문이었다.
지금 내 프로젝트에서 파형이 생성되서 스피커로 나가는 과정은 이러하다.
WaveformGenerator.getPhaseValue() -> ScriptProcessorNode.onaudioevent() -> AudioProcessingEvent.outputBuffer -> AudioContext.destination -> Play!
대충 이런 식으로 동작한다고 보면 된다. 저기서 WaveformGenerator만 내가 작성한 코드이고 나머지는 Web Audio API.
WaveformGenerator에서 만들어진 파형값들이 저 그림을 거쳐 destination에 넘어간 이후는 어차피 웹 오디오가 담당하는 영역을 넘어섰으므로 관심 밖. 그래서 바로 재생된다고 써놨음.
하여튼, 주목해야 할 부분은 onaudioprocess() 함수와 outputBuffer.
onaudioprocess가 불규칙한 시간 간격으로 호출되어, 호출된 즉시 파형을 읽어 아웃풋 버퍼를 가득 채우기 때문에 문제가 발생한다.
이게 왜 문제냐면, 아웃풋 버퍼를 쓰는 간격 중간에 제너레이터의 특성 (주파수, 볼륨, 파형 등)이 바뀌면, 이를 반영하지 않기 때문이다.
이는 즉 한번 만들어진 아웃풋 버퍼 안에선 같은 소리만이 존재한다는 얘기인데, 아웃풋 버퍼의 크기가 너무 크다면, 예를들면 4096의 경우 약 93 밀리초(ms) 동안은 소리를 바꿀 수가 없는 것이다.
칩튠은 16분음표 박자의 1/6 ~ 1/12 정도의 시간 간격인 '프레임'을 최소 단위로 삼고 있는데, 이를 밀리초 단위로 나타내면 BPM이 느릴 땐 수십, 빠를 땐 4.9ms 정도의 시간 간격을 가지는데, 이래서는 도저히 프레임을 표현할 수가 없다.
만약에 아웃풋 버퍼의 크기를 줄여, 이를테면 256일때의 약 5.8ms 간격으로 하고 BPM을 내려 프레임을 좀 길게 몇십 ms 수준으로 잡는다면 적어도 프레임을 건너뛰는 문제는 발생하지 않는다.
하지만 BPM에 따라 고작 수 ms, 아니면 마이크로초 단위로 바뀌는 다양한 프레임 간격을 표현할 수 없어, 5.8ms의 정수배로 표현하고, 누적된 오차는 같은 프레임을 한번 더 읽는 식이 될텐데, 칩튠에선 같은 프레임을 한번 더 읽는게 음색을 바꿔버릴 수 있는 심각한 오차이기 때문에 도무지 쓸 수 없는 것이다.
이 문제를 어제 오늘 내내 고민하고, 결국엔 아예 처음부터 다시 만들어야 하나 하는 생각에까지 이르렀지만, 가까스로 해결책을 떠올렸기에 한번 시도해보고자 한다.
웹 브라우저의 오디오 구현이 바뀌지 않는 이상 길고 불규칙한 간격으로 버퍼를 쓰는 해결할 수는 없겠지만, 최소한 칩튠 사운드만큼은 똑바로 낼 수 있는 방법이라고 생각하는데, 성공한다면 다시 포스팅을 올릴 것이다.
0 notes
piutranq-blog · 7 years ago
Text
Array.fill()에 새로 만든 객체를 집어넣었을 때
this.__memory = new Array(1024).fill(new Waveform(), 0, 1024)
제목이 뭔 말이냐면, 위 코드 처럼 배열을 초기화하면, 1번 객체의 프로퍼티를 바꿨더니 1024개가 싹 다 바뀌는 문제가 발생.
다시 생각해보니, 저기 저 fill() 함수에 패러미터로 들어간 new Waveform()은 이미 만들어진 객체이고 그거 한개로 0부터 1023까지 다 채워넣을게 당연한 것이었다. 매번 new Waveform()을 호출해 객체를 만드는게 아니라.
이걸 for문으로 표현하면 대충 이런 꼴.
this.__memory = new Array(1024) let instance = new Waveform() for (let i = 0; i < 1024; i ++) { this.__memory[i] = instance }
이러면 결국 0번부터 1023번까지 전부 instance가 가리키는 객체 하나만을 가리킬 게 뻔하지......
0 notes
piutranq-blog · 7 years ago
Text
[devlog:aoetracker] 2018/02/21
LFSR 기반의 노이즈 제너레이터에서 주파수 구현은 완료했다. 실제로 노이즈 제너레이터가 지정된 주파수만큼의 속도로 클럭이 돌아가는 것 처럼 보이게 하는 방법은 별도의 포스트로 쓰겠다.
이번에 알게 된 사실인데, ScriptProcessorNode의 onaudioprocessor에 지정된 함수는 44100Hz나 특정한 클럭으로 작동하는게 아니라, 그저 브라우저에 내장된 샘플러가 버퍼를 44100Hz의 속도로 읽을 수 있도록 미리 다음 버퍼에 파형 데이터를 써주는 일 밖에 하지않는다는 것이다.
아마도 샘플러는 지금 읽는 중인 버퍼를 다 읽고 다음 버퍼로 넘어갈 때 마다 onaudioprocessor를 호출해서 미리 새 버퍼를 채우도록 하고 있을 것이다.
하여튼 이제 노이즈 제너레이터를 완성하고, 사운드 엔진을 제어할 커맨드도 만들었겠다. 마지막으로 커스텀 파형을 재생하는 제너레이터를 작성하고 있다.
단순히 현재 지정된 파형을 재생할 뿐인 것은 매우 간단하다. 위상각이 0 이상 1 이하의 실수값으로 계산되는데, 여기에 32를 곱하고 소숫점 아래를 자르면 0~31까지의 위상 인덱스값이 나온다.
그럼 16진 위상값 32개로 이뤄진 파형 배열에 위상 인덱스값을 넣어 저장된 위상값을 꺼내오기만 하면 그만이다. 이렇게.
calcPhaseValue (phase) { let phaseIndex = Math.floor(this.getPhaseAngle(phase) * 32) return this.__current[phaseIndex] }
약간의 눈치만 있으면 this.__current가 파형 배열이라는걸 알 수 있을 것이다. 파형을 바꾸고 싶다면 this.__current를 바꾸면 될 뿐이다. 정말 간단함.
이제 만들어야 할 것은, 파형을 직접 입력하지 않고, 노이즈나 내장 파형 제너레이터에서도 쓰는 파형 번호를 지정해 거기에 해당하는 파형을 꺼내오도록 하는 일이다.
이것도 할려면 간단하긴 한데... 좀 고민할 것이 있다. 어쩌면 프로젝트 구조를 좀 뜯어고쳐야 할 지도 모르는...
0 notes