
LevelUp Piano는 iPad용으로 App Store에 출시되어 있습니다 — App Store에서 보기.
왜 만들었나
피아노를 혼자 연습하다 보면 늘 같은 벽에 부딪힙니다. "내가 제대로 치고 있는 건가?"
선생님이 없는 시간에는 틀린 음도, 어긋난 박자도 스스로 알아차리기 어렵습니다. 여러 방식을 고민하던 중 마이크로 소리를 듣고 채점하는 방법도 떠올렸지만, 마이크는 주변 소음과 화음에 약하고 "몇 번째 음이 얼마나 늦었는지"까지는 잡아내지 못합니다.
디지털 피아노에는 이미 정답이 있습니다. 바로 MIDI입니다. 어떤 건반을, 몇 밀리초에, 얼마나 세게 눌렀는지가 정확한 숫자로 나옵니다. 이 신호를 제대로 쓰면 마이크로는 불가능한 음표 단위 피드백을 줄 수 있습니다.
그래서 목표를 이렇게 잡았습니다.
- 디지털 피아노를 아이패드에 연결해 연주를 음표 단위로 실시간 판정한다.
- 판정 결과를 점수·경험치·진도로 바꿔 혼자 하는 연습을 게임처럼 만든다.
- 피아노 앞에 매일 다시 앉게 만드는 습관 루프를 붙인다.
마이크는 "틀렸다"까지만 안다. MIDI는 몇 번째 음이 몇 ms 늦었는지까지 안다 — 음표 단위 피드백은 바로 이 차이에서 나온다.
무엇을 만들었나
실시간 연주 판정
핵심은 판정 엔진입니다. 곡의 노트 차트(어떤 음이 언제 나와야 하는지)와 사용자의 MIDI 입력을 시간축에서 맞춰, 각 음이 정확/빠름/느림/놓침인지 판정합니다. 화면에는 노트가 흐르고, 건반을 누르면 즉시 반응이 돌아옵니다.
네 가지 역할
한 앱 안에 성격이 다른 사용자가 공존합니다.
- 독립 학습자(Learner) — 선생님 없이 스스로 커리큘럼을 밟는 일반 사용자. 구독 기반.
- 학생(Student) — 선생님에게 배정받은 곡을 연습하고, 선생님이 진도를 봅니다.
- 선생님(Teacher) — MIDI 파일을 올려 곡을 만들고 학생에게 배정합니다.
- 운영자(Operator) — 모두가 공유하는 공식 곡 카탈로그를 관리합니다.
매일 앉게 만드는 게이미피케이션
연습은 지루합니다. 그래서 게임의 언어를 빌려왔습니다.
- 경험치·레벨: 연습할수록 쌓이는 진행감
- 스트릭 + 스트릭 프리즈: 연속 학습을 지키되, 하루쯤 빠져도 무너지지 않게
- 일일 목표: 오늘 할 약속을 스스로 정하기
- 티어 리그·친구: 주간 순위와 사회적 동기
- 미션·마스코트 "마에스트로": 짧은 목표와 캐릭터의 응원
어디가 약한지 짚어주는 학습 진단
단순히 "80점"이 아니라, 몇 번째 마디에서 어느 손이 자주 틀리는지를 마디·손 단위로 기록합니다. 이 데이터로 "3~7마디만 왼손으로 다시" 같은 타겟 재연습을 제안하고, 시간이 지나면 다시 물어보는 간격 반복으로 약점을 굳힙니다.
피아노가 없어도 둘러보기
MIDI 키보드가 없는 사람도 앱을 경험할 수 있도록, 온보딩에서 화면 터치 건반으로 샘플 곡을 쳐볼 수 있게 했습니다(점수 미반영). 하드웨어 의존 앱의 진입 장벽을 낮추는 장치입니다.
기술 스택과 아키텍처
| 영역 | 기술 |
|---|---|
| Frontend | Flutter (iPad 우선), BLoC 패턴 |
| 네이티브 오디오 | iOS AVAudioEngine + SoundFont (오프라인 렌더링) |
| Backend | Kotlin, Ktor, Exposed ORM |
| Database | PostgreSQL (Flyway 마이그레이션) |
| 파일 저장 | Cloudflare R2 (Presigned URL 업로드) |
| Infrastructure | Fly.io (도쿄 리전), Docker |
| Observability | Grafana Alloy + OpenTelemetry |
| Push / 인증 | Firebase Cloud Messaging, Sign in with Apple |
곡의 노트 차트와 반주 오디오는 R2에 저장하고, 게임 상태·진도·소셜 데이터는 PostgreSQL에서 관리합니다. 앱은 아이패드를 우선으로 설계했습니다 — 넓은 화면이 노트가 흐르는 게임플레이와 건반 배치에 잘 맞기 때문입니다.
왜 이 스택인가. iPad 우선이라 크로스플랫폼 UI는 Flutter로 빠르게 짓되, 실시간 오디오 합성만은 Flutter로 부족해 iOS 네이티브(AVAudioEngine) 로 브리지했습니다. 백엔드는 판정·점수·소셜의 도메인 로직이 무거워 정적 타입의 Kotlin/Ktor를 골랐고, 노트 차트·오디오 같은 큰 파일은 DB가 아니라 R2에, 관계형 게임 상태는 PostgreSQL에 나눠 담았습니다.
핵심 설계 결정
MIDI 하드웨어라는 전제
이 앱의 핵심 경험은 물리 피아노가 있어야 완성됩니다. 그래서 연결 마법사로 기기 선택을 명시적으로 안내하고(같은 피아노가 여러 엔드포인트로 잡히기도 합니다), 키보드가 없으면 터치 체험으로 우회하도록 했습니다. 연결과 판정 자체는 실시간 MIDI 판정 엔진에서 ±50/150/300ms 윈도우까지 풀었습니다.
기기 안에서 MIDI를 오디오로 굽기
선생님이 올린 임의의 MIDI를 연습용 반주 오디오(1x·0.75x·0.5x 세 속도) 로 바꿔야 하는데, 서버에 신디사이저를 두는 대신 아이패드 안에서 렌더링하기로 했습니다. iOS AVAudioEngine의 오프라인 렌더링과 SoundFont를 쓰는 이 파이프라인은 별도 글에서 2패스·서브청크·64프레임 트림까지 다뤘습니다.
하나의 앱, 네 개의 역할
역할마다 홈 화면·권한·데이터 접근이 다르지만, 연습 화면처럼 공유하는 부분도 많습니다. 학생과 독립 학습자가 같은 게임플레이 엔진을 쓰되 곡의 출처(배정 곡 vs 카탈로그)만 다르게 흐르도록, 공용 코어와 역할별 셸을 분리했습니다 — 역할 설계 이야기에서 권한 매트릭스까지 풀었습니다.
한국어에서 세계로
처음엔 한국어 전용이었지만, 다국어를 도입하며 UI 문자열뿐 아니라 곡 제목 같은 콘텐츠 데이터까지 로케일별로 관리하도록 확장했습니다(현재 한국어·영어, 이후 중국어·일본어). 그 세 겹의 로컬라이제이션은 한국어에서 세계로에 정리했습니다.
연습곡을 무한 생산하는 AI 작곡
칠 곡이 부족하다는 가장 큰 리스크를, LLM으로 연습곡을 생성·검증하는 파이프라인으로 풀고 있습니다 — LLM에게 ms 대신 마디·박으로 작곡시키고, 검증 실패를 그대로 되먹여 재생성합니다. 설계는 LLM을 이용한 곡 만들기에 자세히 담았습니다.
마치며
LevelUp Piano는 "정확한 신호(MIDI) + 게임의 습관 설계"라는 단순한 아이디어에서 출발했지만, 실제로는 실시간 판정, 온디바이스 오디오 렌더링, 다중 역할, 게이미피케이션, 다국어까지 여러 결이 맞물린 프로젝트가 되었습니다. 혼자 연습하는 사람이 "오늘도 한 번 더" 앉게 만드는 것 — 그 작은 순간을 위해 만들었습니다.

LevelUp Piano is shipping on the App Store for iPad — view it on the App Store.
Why I built it
Practicing piano alone, you always hit the same wall: "Am I even playing this right?"
Without a teacher in the room, it's hard to catch your own wrong notes or off beats. One approach I weighed was grading sound through the microphone, but a mic struggles with ambient noise and chords, and it can't tell you which note was late and by how much.
A digital piano already knows the answer — through MIDI. Exactly which key, at exactly which millisecond, with exactly how much velocity, all as precise numbers. Use that signal well and you can give note-by-note feedback a microphone never could.
So I set the goals like this:
- Connect a digital piano to an iPad and judge playing note by note, in real time.
- Turn that judgment into score, XP, and progression — so solo practice feels like a game.
- Add a habit loop that brings people back to the keys every day.
A microphone only knows that you were wrong. MIDI knows which note was late, and by how many milliseconds — note-by-note feedback comes straight from that gap.
What I built
Real-time performance judging
The heart of it is the judgment engine. It aligns the song's note chart (which note should sound, and when) with the user's MIDI input on a shared timeline, and marks each note as perfect / early / late / missed. Notes flow down the screen, and pressing a key gives instant feedback.
Four roles
Very different users live inside one app:
- Independent learner — a general user following a self-paced curriculum without a teacher. Subscription-based.
- Student — practices songs assigned by a teacher, who follows their progress.
- Teacher — uploads MIDI files to create songs and assign them to students.
- Operator — manages the official song catalog everyone shares.
Gamification that keeps you coming back
Practice is boring. So I borrowed the language of games:
- XP and levels: a sense of progress that accrues as you practice
- Streaks + streak freeze: protect your run, but don't let one missed day collapse it
- Daily goals: let users promise themselves what to do today
- Tier leagues and friends: weekly ranking and social motivation
- Missions and the mascot "Maestro": short goals and a character cheering you on
Diagnosis that points to your weak spots
Instead of just "80 points," the app records which measures and which hand you miss most, at the measure and hand level. From that data it suggests targeted re-practice — "just measures 3–7, left hand" — and uses spaced repetition to lock in weak spots over time.
Look around even without a piano
So people without a MIDI keyboard can still experience the app, onboarding lets you play a sample song on an on-screen touch keyboard (no scoring). It lowers the entry barrier of a hardware-dependent app.
Tech stack and architecture
| Area | Technology |
|---|---|
| Frontend | Flutter (iPad-first), BLoC pattern |
| Native audio | iOS AVAudioEngine + SoundFont (offline rendering) |
| Backend | Kotlin, Ktor, Exposed ORM |
| Database | PostgreSQL (Flyway migrations) |
| File storage | Cloudflare R2 (presigned-URL uploads) |
| Infrastructure | Fly.io (Tokyo region), Docker |
| Observability | Grafana Alloy + OpenTelemetry |
| Push / Auth | Firebase Cloud Messaging, Sign in with Apple |
Note charts and backing audio live in R2; game state, progress, and social data live in PostgreSQL. The app is designed iPad-first — the larger screen fits the falling-note gameplay and the keyboard layout much better.
Why this stack. Being iPad-first, the cross-platform UI is built fast in Flutter — but real-time audio synthesis is beyond Flutter, so it bridges to iOS native (AVAudioEngine). The backend carries heavy domain logic for judging, scoring, and social, so it's statically-typed Kotlin/Ktor; large files like note charts and audio go to R2 rather than the DB, while relational game state lives in PostgreSQL.
Key design decisions
MIDI hardware as a premise
The core experience only completes with a physical piano. So a connection wizard explicitly guides device selection (one piano often enumerates as several endpoints), and if there's no keyboard, a touch trial routes around it. Connection and judging themselves are covered in the real-time MIDI judgment engine, down to the ±50/150/300ms windows.
Baking MIDI into audio, on the device
Arbitrary teacher-uploaded MIDI has to become practice backing audio at three tempos (1x / 0.75x / 0.5x). Instead of hosting a synthesizer on the server, I render it on the iPad itself, using iOS AVAudioEngine's offline rendering and a SoundFont. The separate post covers that pipeline down to the two-pass, sub-chunking, and 64-frame trim.
One app, four roles
Each role has a different home screen, permissions, and data access, yet shares a lot — like the practice screen. Students and independent learners run the same gameplay engine while only the song source (assigned songs vs. catalog) differs — a shared core separated from per-role shells. The role-design post goes all the way to the permission matrix.
From Korean to the world
It started Korean-only, but adding internationalization meant handling not just UI strings but content data such as song titles per locale (Korean and English today, then Chinese and Japanese). Those three layers of localization are written up in From Korean to the World.
Generating endless practice songs with AI
The biggest risk — running out of songs to play — is being solved with a pipeline that generates and validates practice songs via an LLM: it composes in measures and beats instead of milliseconds, and feeds validation failures straight back for regeneration. The design is detailed in Making Songs with an LLM.
Closing
LevelUp Piano began from a simple idea — "a precise signal (MIDI) plus a game's habit design" — but became a project where real-time judging, on-device audio rendering, multiple roles, gamification, and localization all interlock. Getting a solo learner to sit down for "one more round today" — that small moment is what it's all for.