노래를 만들어라: 나의 달리기가 자체 사운드트랙을 만들다 (폰에서 구동되는 Gemma 사용)
요약
이 글은 외부 활동 시간(달리기, 산책 등)을 음악의 레이어로 변환하는 'Grow a Song'이라는 프로젝트를 소개합니다. 이 작곡기는 Gemma 3 1B 모델을 사용하여 사용자의 위치 정보와 일상 기록을 분석하고, 이를 바탕으로 키, 스케일, 코드 진행 등을 결정하여 독특한 사운드트랙을 생성합니다.
핵심 포인트
- 외부 활동 시간을 음악적 레이어로 변환하는 혁신적인 아이디어입니다.
- Gemma 3 1B 모델이 작곡의 핵심 역할을 수행하며, 시간대와 위치 정보를 활용합니다.
- 사용자의 실제 움직임(달리기/산책)을 기록하고 이를 사운드트랙에 반영합니다.
- 오픈 소스 프로젝트로 GitHub에서 코드를 확인할 수 있습니다.
이 글은 Hacktoberfest Open-Source AI Challenge, Week 1: Touch Grass에 제출하는 내용입니다.
제가 만든 것
금요일 아침에 저는 35분 동안 달리기를 했습니다. 시작했을 때 제 노래는 낮은 드론 사운드 하나였습니다. 돌아올 무렵에는 패드, 베이스 라인, 드럼, 칼림바 멜로디, 그리고 실제로 달렸던 거리의 소리가 10초 분량으로 뒤섞여 있었습니다.
Grow a Song은 외부 시간을 음악으로 바꿉니다. 야외에서 보내는 매 분은 오늘 트랙에 한 겹을 추가합니다:
| 외부 시간 (20분 목표) | 레이어 |
|---|---|
| 0분 | 드론 |
| ... |
하루 종일 실내에 머무르면 노래가 빈약하게 유지되며, 밖에 나가야만 채워집니다. 건너뛰기나 레이어 구매 방법이 없으므로, 전체 노래를 들을 수 있는 유일한 방법은 외출하는 것입니다.
작곡가는 iPhone 13 Pro에서 구동되는 Gemma 3 1B입니다. 매번 산책이나 달리기를 하기 전에 이 모델은 하루 시간대, 태양의 고도, 앞으로 할 일, 그리고 이번 주에 얼마나 밖에 있었는지 등을 살펴본 후 키(key), 스케일(scale), 템포(tempo), 코드 진행(chord progression), 악기, 제목, 그리고 한 줄짜리 라이너 노트(liner note)를 선택합니다. 제 달리기가 얻은 결과는 다음과 같습니다:
Ephemeral Echoes. D 도리안(dorian), 160 bpm, 코드 1 7 5 3, 따뜻한 패드, 맥동하는 베이스, 역동적인 드럼, 칼림바.
_
⚠️ [IMG:N] 형식 토큰은 이미지 placeholder 입니다. 번역하지 말고 원래 위치에 그대로 유지하세요.
- 35.1분 동안 야외 활동 (outside)을 했으며, 3.4km를 걸었습니다.
- 총 6개 레이어(layer)가 재생되었습니다 (제가 목표했던 시간은 20분이었습니다).
- 오전 7시 40분에 녹음된 10초 클립이 현재 노래의 최상단 레이어가 되었습니다.
- Gemma의 활동 기록: "당신은 3.2km를 달렸습니다. 'Ephemeral Echoes'가 이제 녹음의 일부입니다." (정신적으로는 정확합니다. 거리는 약 200m 정도 차이가 나는데, 이는 1B 모델이 일기 항목을 작성할 때 나오는 오차입니다.)
코드
GitHub logo ansh-saini / grow-a-song
Grow a Song (노래를 키워라)
야외에서 보내는 시간이 음악이 됩니다.
야외에서 보낸 매 분은 오픈 웨이트 모델에 의해 iPhone으로 작곡되어 오늘 노래의 한 레이어를 추가합니다.
기능 설명 (What it does)
산책이나 달리기를 시작하세요. 당신의 노래는 하나의 낮은 드론(drone) 사운드로 시작합니다. 야외에서 시간을 보낼수록 새로운 레이어들이 점차 추가됩니다:
| 일일 목표 달성률 | 레이어 |
|---|---|
| 0 % | 드론 (Drone) |
| ... |
실내에 머무르면 노래는 여전히 빈약합니다. 모든 사운드를 들을 수 있는 유일한 방법은 밖에 나가는 것입니다.
외출 전에 Gemma 3 1B가 휴대폰에서 구동되어 하루의 시간, 태양의 고도, 무엇 등을 기반으로 곡을 작곡합니다: 키(key), 스케일(scale), 템포(tempo), 코드 진행(chord progression), 악기, 제목 및 한 줄짜리 기록 문구(liner note)가 포함됩니다.
Swift, SwiftUI, AVAudioEngine, CoreLocation, CoreMotion, HealthKit 및 MLX Swift LM. 음악 엔진은 37개의 테스트를 가진 순수 Swift 패키지이므로, 대부분의 기능은 휴대폰 없이 Mac에서 실행하고 테스트할 수 있습니다.
제작 과정 (How I Built It)
Gemma가 작곡하고, Swift가 연주한다
저는 1B 모델이 오디오나 MIDI 음표를 하나하나 생성하는 것을 원하지 않았습니다. 그렇게 작은 모델은 분위기를 파악하는 데는 괜찮지만 수백 개의 음표를 추적하는 데는 매우 취약합니다. 그래서 Gemma는 약 열 가지의 음악적 결정을 내리고 이를 JSON 형태로 반환합니다:
{"title": "Ephemeral Echoes", "key": "D", "scale": "dorian", "bpm": 160,
"progression": [1, 7, 5, 3], "pad": "warm", "bass": "pulse",
"drums": "driving", "melody": "kalimba", "energy": 1.0,
...
결정론적 시퀀서(deterministic sequencer)는 이를 16마디의 음표로 변환합니다 (멜로디가 A A' B A 형태로 구성되어 반복할 때 귀에 잘 박히도록). 그리고 처음부터 작성한 작은 신스(synth)가 이를 연주합니다: 디튠된 SAW 패드, FM 칼림바, 155Hz에서 시작하여 하강하는 사인파 형태의 킥 드럼, 그리고 리버브입니다. 동일한 사양이 항상 동일한 노래를 생성하며, 이는 나중에 중요하게 작용했습니다.
1B 모델에서 신뢰할 수 있는 JSON 추출하기
Mac으로 진행한 첫 번째 평가(eval)는 표면적으로는 괜찮아 보였습니다: 6곡 중 6곡이 연주 가능했습니다. 하지만 그 이면에서는 스키마 제약 디코딩(schema-constrained decoding, MLX의 MLXGuidedGeneration, XGrammar 기반)이 매번 실패했고, 폴백 파서(fallback parser)가 조용히 이를 복구하고 있었습니다.
부분적인 출력 로그를 보니 그 이유를 알 수 있었습니다. Gemma는 줄바꿈을 계속해서 내보냈습니다. JSON은 무제한의 공백(whitespace)을 허용하기 때문에, 탐욕적 디코딩(greedy decoding)은 이 공백이 항상 유효한 다음 토큰이라고 판단하고 360개 토큰 전체를 공백으로 채웠습니다. 라이브러리의 공백 및 종료 토큰 편향(bias)을 추가하여 이를 수정했습니다:
| 이전 | 이후 | |
|---|---|---|
| 스키마 가이드 성공률 | 0 / 6 | 6 / 6 |
| 평균 작곡 시간 (M2 Pro) | 9.5초 | 4.4초 |
두 번째 문제는 취향이었습니다. Gemma는 달마다의 경우에도 D 메이저 펜타토닉, 코드 1 5 6 4를 사용하고 BPM 60을 거의 항상 선택했습니다. 1B 모델은 자신이 먼저 읽는 옵션에 크게 의존하는 경향이 있습니다. 그래서 프롬프트가 이제 모든 옵션 목록의 순서를 현재 분(minute)으로 시드화하여 무작위로 섞고, 작은 가드가 활동에 맞는 범위로 BPM을 조정합니다 (예:
조도 센서가 없는 외부 환경을 감지하는 방법
Apple Watch는 주변광 센서를 이용해 낮 시간의 시간을 측정하며, 앱은 HealthKit에서 이 정보를 읽어옵니다. 하지만 저는 전용 스마트폰만 가진 사람들에게도 작동하는 앱을 원했고, iPhone 앱으로는 휴대폰 자체의 조도 센서 값을 읽을 수 없습니다.
그래서 휴대폰이 이를 추론합니다. 다음 조건들이 충족될 때 1분이 카운트됩니다:
- 태양이 시민 황혼(-6도) 위에 떠 있는 시간 (NOAA 태양 방정식을 이용해 시간과 위치를 기반으로 현지 계산), 그리고
- CoreMotion이 사용자가 걷거나, 뛰거나, 자전거를 타는 중이라고 감지하거나, GPS 고정 정확도가 20m 이내이고(보통 탁 트인 하늘을 의미) 집에서 떨어진 곳에 있을 때.
운전하는 시간은 카운트되지 않습니다. 처음 앱을 실행하면 CoreMotion의 7일 활동 기록 덕분에 일주일 분량의 예상 노래가 바로 제공됩니다. 시계를 사용하는 경우, 측정된 숫자가 그 역할을 이어받습니다.
달리기를 망칠 뻔했던 버그
저는 달리기를 시작하기 전에 두 번째 AI 에이전트에게 코드 검토를 맡겼고, 이 과정에서 실제 충돌(crash)을 발견했습니다. iOS는 백그라운드 앱이 GPU를 사용하는 것을 허용하지 않습니다. 만약 제가 '시작' 버튼을 누르고 Gemma가 여전히 작곡하는 동안 휴대폰 화면을 잠근다면, MLX가 해당 오류에 부딪혀 fatalError를 호출하고, 앱과 음악, 그리고 분 카운터가 모두 한 번에 종료되어 달리기를 시작한 지 몇 초 만에 멈추게 됩니다.
이 문제를 해결하기 위해 세 가지 수정 사항을 적용했습니다. Gemma는 앱 화면이 표시되는 동안에만 실행되며, 백그라운드에서 요청된 작업은 사용자가 앱을 다시 열 때까지 대기합니다. 또한, 작곡하는 데 걸리는 몇 초 동안 화면이 깨어 있도록 유지됩니다. 그리고 모든 MLX 호출은 withError로 감싸져 GPU 실패가 Swift 오류로 처리되고 충돌 대신 내장 노래로 대체되도록 했습니다. 어떤 경우든 음악은 드론(drone)과 함께 즉시 시작되며, Gemma의 노래는 다음 16마디 구절 경계에서 이어받아 중도에 끊기는 일이 없습니다.
같은 검토를 통해 달리기 진행 상황이 '종료' 버튼을 누를 때만 저장된다는 것도 발견했고, 이제는 30분마다 자동으로 저장되도록 수정했습니다.
노래를 휴대폰에서 꺼내는 방법
달리기 후에는 노트북에서 노래를 듣고 싶었지만, 한 번도 내보낸 적이 없었다. 렌더러가 결정론적(deterministic)이기 때문에, 그날의 기록과 휴대폰의 필드 클립을 케이블로 복사하여 동일한 엔진으로 Mac에서 똑같은 노래를 렌더링했다. 이것이 위에 연결된 오디오다.
AI 에이전트로 구축하고, 달리기로 테스트하다
나는 Claude Code와 함께 한 번에 긴 세션을 거쳐 이를 만들었다: 계획(plan), 코딩(code), 테스트(test), 검토(review), 수정(fix). 이 과정은 작은 하네스(make test, make render, make eval, make device)를 설정하여 모든 변경 사항이 단위 테스트(unit tests)와 렌더링된 오디오 레벨, 그리고 내 휴대폰에 도달하기 전 실제 Gemma 출력과 비교 검사되도록 했다. 그다음 진짜 테스트는 나였다: 신발을 신고 '시작' 버튼을 누르는 것.
Open Innovation이 중요한 이유
Song을 성장시키려면 사용자의 위치 정보, 움직임, 때로는 건강 데이터, 그리고 마이크가 필요하다. 이 모든 것을 서버에 맡겨서 코드를 조합하게 하는 것은 엄청난 양이다.
오픈 웨이트(open-weight) 모델을 로컬에서 실행하면, 그 어떤 것도 휴대폰 밖으로 나가지 않는다. GPS, 움직임, 일광 시간 분, 그리고 거리 녹음은 장치 내에 머무르며, 데이터를 유출할 수 있는 백엔드도 없다.
또한 달리기가 실제로 이루어지는 곳에서도 작동한다. 트레일이나 공원은 신호가 없는 경우가 많고, 0.8 GB를 다운로드한 후에는 앱이 신호를 필요로 하지 않는다.
모델을 실행하는 데 비용이 들지 않기 때문에, Gemma는 내가 요청량을 아끼는 대신 매번 산책 전에 작곡할 수 있다. 그리고 모델이 설정(config) 한 줄만으로 이루어져 있기 때문에, 휴대폰에 편안하게 맞는 더 큰 Gemma로 이동할 수 있다.
수상 부문
- Gemma 활용 최우수: Gemma 3 1B (QAT, 4-bit)는 MLX Swift LM을 통해 장치에서 완전히 실행되며 스키마 기반의 JSON 출력으로 모든 노래를 작곡한다.
- ElevenLabs 활용 최우수: 데모 비디오 내레이션. 앱 자체는 네트워크 호출을 하지 않는다.
Claude Code로 구축함. 프로젝트는 내가 지휘했고, 내 달리기로 테스트했으며, 이 게시물을 검토하고 편집했다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기