dev.to

The groove is the 40 milliseconds a grid throws away, so we built a beat studio that gives them back, in a browser tab

그루브는 격자에서 사라지는 40밀리초입니다 — 브라우저에서 되살린 비트 스튜디오

Krisp는 브라우저에서 비트를 만들고 편집하는 음악 작업 도구입니다. 스윙과 연주 미세 타이밍을 보존하는 스케줄러, 브라우저에서 실행하는 Magenta.js 모델, 같은 오디오 엔진을 쓰는 WAV 내보내기가 특징입니다. 사운드 생성과 하이브리드 검색에는 ElevenLabs와 Tiger Data를 연결했습니다.

AI 요약

Krisp는 비트를 만드는 친구의 작업 방식에 맞춰 개발한 브라우저 기반 음악 스튜디오입니다. 글은 90 BPM에서 16분음표 길이가 166.7밀리초이고, 스윙 50%를 적용하면 매 두 번째 음이 약 41.7밀리초 늦게 재생된다고 설명합니다. 일반적인 스텝 시퀀서는 음을 격자에 맞춰 이 차이를 없애지만, Krisp는 음마다 속도와 타이밍 오프셋을 저장해 그루브를 유지합니다.

브라우저에서 비트를 만들고 재생합니다

사용자는 패드나 키보드로 연주하거나 격자에서 음을 찍고, 드래그로 한 행을 채울 수 있습니다. Shift-클릭으로 고스트 노트와 강세를 지정하고, 스윙도 조절합니다. 작업 공간은 자동 저장되며 비트를 곡으로 배열하고, 채널별 EQ·리버브·팬·페이더와 마스터 컴프레서·리미터를 거쳐 WAV나 MIDI로 내보냅니다.

앱의 음악 처리는 브라우저에서 실행합니다. Zustand가 작업 공간과 실행 취소 기록을 관리하고, Tone.js 스케줄러는 100밀리초 앞을 내다보며 음을 예약합니다. 네트워크는 저장, 사운드 검색, 새 사운드 생성에 쓰고 재생과 AI 처리에는 개입하지 않습니다. 사용자는 빈 작업 공간이나 템포와 스윙이 지정된 8개 패턴에서 시작할 수 있습니다.

재생과 내보내기에 같은 엔진을 씁니다

스케줄러의 타이밍은 단위 테스트가 가능한 순수 함수로 분리했습니다. 16분음표 길이는 60 / BPM / 4로 계산하고, 홀수 번째 스텝에 스윙 지연을 더합니다. 모델이 만든 오프셋도 음의 시각에 반영하며, 음이 이전 스텝보다 0.4스텝 이상 앞서지 않도록 제한합니다. 반복 구간 처리는 별도의 함수로 두어 드럼과 오디오 클립이 같은 틱 처리 경로를 따릅니다.

WAV 내보내기에는 별도의 렌더링 엔진을 만들지 않았습니다. createEngine(context)가 실시간 재생과 Tone.Offline 렌더링 양쪽에서 같은 오디오 그래프와 스케줄러를 사용합니다. 16마디 곡을 90 BPM으로 재생하면 음악 길이는 42.7초이며, 2초 리버브 꼬리를 포함해 GPU가 없는 노트북에서 16.1초 만에 렌더링했습니다. 글쓴이는 WAV를 Audacity에서 확인해 스윙 50%일 때 홀수 번째 하이햇이 약 40밀리초 늦고 짝수 번째는 거의 움직이지 않는 점도 측정했습니다.

기존 비트에만 개입하는 브라우저 AI

Magenta.js의 Humanize는 이미 있는 음의 속도와 타이밍만 바꾸며 음을 추가하거나 지우지 않습니다. Variations는 원래 비트와 가까운 후보 네 개를 만들고, Continue는 첫 마디를 바탕으로 다음 마디를 만듭니다. Morph는 한 비트에서 다른 비트까지 아홉 단계의 변화를 생성합니다. AI 결과는 실행 취소 한 번으로 되돌릴 수 있습니다.

모델을 연결하며 발견한 데이터 처리 차이도 다뤘습니다. MusicVAE와 DrumRNN은 양자화된 음을 반환하고 속도 값은 비워 둡니다. Krisp는 이를 0이 아닌 기본값 100으로 처리합니다. GrooVAE는 양자화되지 않은 시작 시각을 반환하므로, 시작 시각을 스텝 길이로 나눠 가장 가까운 스텝을 구하고 나머지를 타이밍 오프셋으로 보존합니다. 스네어와 클랩이 같은 드럼 피치 38을 쓰는 문제는 모델 음을 해당 피치의 첫 슬롯에 배정해 처리합니다. AI 기능 테스트는 API 요청을 모두 막은 상태에서도 통과했다고 글은 설명합니다. 다만 드럼 VAE를 처음 불러오는 데 14.2초, 첫 Humanize 실행에는 15초가 걸렸습니다.

사운드 생성과 통합 검색

ElevenLabs Sound Effects API는 문장으로 새 원샷이나 루프를 생성합니다. 원샷 요청은 건조한 단일 드럼 소리로 제한하고 길이는 0.5~2초로 지정합니다. 루프 요청에는 템포와 반복 설정을 포함합니다. API 키는 서버에 두며, 소리 생성 크레딧을 보호하도록 일일 한도를 둡니다. 생성된 오디오는 브라우저에서 무음 구간을 자르고 페이드와 피크 정규화를 적용한 뒤 라이브러리에 저장합니다.

검색은 이름 같은 정확한 단어와 ‘따뜻하고 낡은 느낌’ 같은 음색 설명을 함께 처리합니다. 브라우저에서 all-MiniLM-L6-v2로 384차원 임베딩을 만들고, Tiger Data의 PostgreSQL에 오디오, 작업 공간 JSONB, pgvector 임베딩, 전문 검색 인덱스를 저장합니다. 검색 쿼리는 벡터 유사도와 PostgreSQL 전문 검색 순위를 각각 구한 뒤, k=60인 reciprocal rank fusion으로 결과를 합칩니다. 결과에는 벡터 검색과 키워드 검색 중 어떤 방식이 일치했는지 표시합니다. 글의 테스트에서는 ‘dusty kick’이 이름과 벡터 양쪽에서, 이름에 없는 ‘warm lo-fi bass drum’이 벡터 검색에서 일치했습니다.

구현 과정에서 확인한 오류

글은 실제 테스트와 수정 사례도 공개합니다. PostgreSQL 생성 열에서 array_to_string을 쓸 수 없어 검색용 tsv를 일반 열로 바꾸고 INSERT 때 채웠습니다. Tiger Data 연결은 자체 인증 기관 인증서를 신뢰하도록 설정해 TLS 오류를 해결했습니다. Tone.js의 볼륨 램프가 음소거 설정을 되돌리는 문제는 별도의 게이트 단계로 수정했습니다. 컴프레서는 트랜지언트를 제한하지 못해 소프트 클리퍼를 추가했고, 최종 피크는 -6dBFS로 제한했습니다.

dev.to 반응

  • @dronzer2code — @jess, @peter, @ben, 이 프로젝트를 마음에 들어 해 주시면 좋겠습니다!
  • @c0demafia — 실제로 비트를 만드는 친구에게 맞춰 만든 점이 모든 세부 사항에 드러납니다. 특히 색으로 구분한 아이디어 카드와 에이전트의 추론을 커밋에 연결한 부분이 좋습니다. 챌린지를 위해 색다르고 세심한 프로젝트를 출시한 당신과 Sourish에게 큰 박수를 보냅니다! 🥁✨
    • @dronzer2code — 감사합니다! AI를 이유 없이 덧붙이는 대신 실제 프로듀서의 작업 흐름에 맞추는 것이 큰 목표였습니다. 색상 아이디어 카드와 커밋에 연결된 에이전트 추론은 전체 과정을 더 투명하고 쓰기 쉽게 만들려는 의도였습니다. 세부 사항을 알아봐 주셔서 정말 감사합니다! 🥁 참여해 주셔서 감사합니다. 게시물에 좋아요도 눌러 주세요 🤗
  • @launchgatecheck — 재생 엔진과 내보내기 엔진을 공유하면 서로 어긋나는 원인을 하나 없앨 수 있습니다. 반복 구간 경계에서 첫 스텝의 이른 휴머나이즈 음과 마지막 스텝의 늦은 음을 넣은 테스트도 추가하겠습니다. 보통의 스윙 테스트가 통과해도 오프셋이 반복 경계를 넘을 수 있습니다. MIDI도 hitTime과 같은 유효 오프셋 및 클리핑 규칙을 보존하나요? 내보내기가 시각 0에서 시작할 때 첫 음의 이른 오프셋은 어떻게 되나요?
    • @dronzer2code — 좋은 경계 사례입니다. WAV·MIDI와 실시간 재생이 반복 경계에서 음의 첫 시각을 똑같이 처리한다고 가정하지 말고 확인해야 합니다. 내보내기가 t=0에서 시작하므로 첫 스텝의 이른 음은 음수 시각을 그대로 보존하기보다 잘라내거나 경계를 처리해야 할 수도 있습니다. 제안한 재생·내보내기 비교 테스트가 바로 그런 차이를 잡는 데 도움이 되겠습니다. 참여해 주셔서 감사합니다. 게시물에 좋아요도 눌러 주세요 🤗
  • @soumen_kundu_cf61797cb29d — 정말 인상적인 프로젝트입니다! 단단한 격자에 맞추는 대신 작은 타이밍 차이를 보존하는 아이디어가 특히 좋았습니다. Tone.js, 브라우저 AI, ElevenLabs, Tiger Data를 결합해 단순한 비트 메이커보다 훨씬 많은 것을 만들었습니다.
    • @dronzer2code — 감사합니다! Krisp의 의도는 자연스러운 타이밍 차이를 지우지 않는 것이었습니다. AI와 인프라가 음악 작업을 방해하지 않고 그 생각을 뒷받침하게 만들고 싶었습니다. 참여해 주셔서 감사합니다. 게시물에 좋아요도 눌러 주세요 🤗
  • @ssapable — 글 첫 부분의 수치 설명이 이 사이트에서 본 스윙 설명 중 가장 좋았습니다. 90 BPM의 16분음표는 166.7밀리초이고, 스윙은 두 번째 음마다 41.7밀리초 늦추며, 이는 휴대폰 영상 한 프레임보다 짧다는 설명입니다. 그래서 50초짜리 손그림 만화로 만들었습니다. 벨트 위 눈금이 늦게 움직이고, 행진하던 로봇이 튀어 오르며, 격자가 버리는 40밀리초를 Krisp가 되살리고, ‘warm, dusty kick’을 분위기로 찾아 WAV나 MIDI로 내보내는 내용입니다. 가입 없이 볼 수 있고, 마음대로 쓰셔도 됩니다. 수치나 표현이 틀렸다면 장면을 알려 주시면 다시 그리겠습니다. GrooVAE가 비트를 휴머나이즈할 때 음을 그리드보다 앞당기기도 하나요, 아니면 늦추기만 하나요?
    • @dronzer2code — 정말 멋집니다. 50초 애니메이션으로 만들어 주셔서 감사합니다. 벨트와 로봇 비유가 미세 타이밍을 보존한다는 뜻을 잘 보여 줍니다. 질문에 답하자면 GrooVAE는 음을 그리드보다 앞이나 뒤로 옮길 수 있습니다. 40밀리초 사례는 스윙을 설명하는 예시이지, 휴머나이즈가 음을 한쪽으로만 미룬다는 규칙은 아닙니다. 휴머나이즈는 자연스러운 미세 타이밍을 보존하는 기능입니다. 참여해 주셔서 감사합니다. 게시물에 좋아요도 눌러 주세요 🤗
    • @ssapable — 바로잡아 주셔서 감사합니다. 스윙은 설명을 위한 예시이고, 휴머나이즈는 자연스러운 미세 타이밍을 살리도록 음을 앞뒤로 움직인다는 점을 반영했습니다. 첫 영상은 늦게 움직이는 장면만 넣어 잘못된 뜻을 전했으므로, 앞뒤로 움직이는 모습으로 고쳤습니다. 샘플 페이지에 수정본을 올렸습니다. 만화는 어디서든 출처 표시 없이 쓰셔도 됩니다. 네 가지 변주를 나란히 보여 주는 긴 버전을 원하시면 말씀해 주세요.
    • @dronzer2code — 물론입니다 🤗

원문: dev.to / 번역·요약: Trawling