Ubah MP3/WAV/OGG atau tautan YouTube menjadi MIDI, lalu mainkan di piano visualizer ala Synthesia langsung di browser — lengkap dengan not balok, stem separation, multi-instrumen synth, transpose, dan tautan share.
Semua proses berat (transkripsi AI, pemisahan stem, unduh YouTube) jalan lokal di mesin ini; browser hanya menangani playback, visual, dan notasi.
Audio / YouTube ──> yt-dlp + ffmpeg ──> [Demucs stem AI] ──> basic-pitch (AI audio→MIDI)
│ │
└────────────► preview audio ◄───────────────┘
│
Next.js UI ◄──── MIDI ──────┘
(falling notes, 88-key piano, Tone.js synth)
| Fitur | Status | Catatan |
|---|---|---|
| Upload lokal (MP3/WAV/OGG/FLAC/M4A/Opus/WebM) | ✅ | drag & drop, maks 60 MB |
| Input URL YouTube | ✅ | yt-dlp, batas durasi 15 menit |
| AI Audio → MIDI (polifonik) | ✅ | basic-pitch (ONNX, CPU), 3 preset ketepatan |
| Kualitas hasil | ✅ | pra-proses audio (normalisasi + band-limit + buang perkusi), gabung fragmen not, buang noise |
| Fallback transkripsi melodi | ✅ | librosa pyin (monofonik) bila basic-pitch gagal |
| AI Stem Separation | ✅ | Demucs 2-stem (vokal/instrumental) atau 4-stem |
| Interactive Piano Simulator (88 tuts) | ✅ | tuts menyala mengikuti not yang berbunyi |
| Synthesia-style falling notes | ✅ | canvas, warna per pitch, auto-follow, click-to-seek |
| Play / Pause / Stop / Seek / Volume | ✅ | transport dengan range-request audio + level meter |
| Sustain pedal otomatis | ✅ | pedal dideteksi dari rekaman (CC64), mode Auto/On/Off |
| Tempo & Pitch shifting | ✅ | 25%–200% tanpa ubah pitch; transpose −12…+12 semitone |
| Multi-Instrument Synth | ✅ | 4 instrumen berbasis rekaman asli (sample) + 2 sintetis |
| Export | ✅ | .mid asli (sudah membawa pedal), .mid transpose/tempo |
| Share link | ✅ | slug unik, halaman /share/<slug> read-only + penghitung kunjungan |
| Layout mobile | ✅ | bottom tab bar, kartu, piano roll dipendekkan, tanpa scroll horizontal |
Dua proses, karena bagian AI-nya Python:
| Bagian | Stack | Port |
|---|---|---|
web/ — UI |
Next.js 16 (App Router) · Tailwind v4 · Framer Motion · Tone.js · @tonejs/midi | 8891 |
backend/ — pipeline |
FastAPI · yt-dlp · Demucs · basic-pitch (ONNX) · librosa · pretty_midi · ffmpeg | 8892 |
State disimpan di SQLite (backend/data/midimagic.db) dengan tabel jobs, shares, events
— additive: setiap konversi jadi record baru, tidak pernah menimpa yang lama.
sudo apt install ffmpeg python3-venv
node -v # butuh Node 20+ — juga dipakai yt-dlp sebagai JS runtimeYouTube: sejak yt-dlp 2025.x ekstraksi YouTube butuh JS runtime. Backend memanggilnya dengan
--js-runtimes node(lihatMIDIMAGIC_YT_JS_RUNTIME). Tanpa itu banyak format hilang dan unduhan sering gagal.
cd backend
python3 -m venv .venv
.venv/bin/pip install -U pip wheel setuptools
.venv/bin/pip install -r requirements.txt
# PyTorch CPU (jangan pakai index default, itu CUDA dan besar)
.venv/bin/pip install --index-url https://download.pytorch.org/whl/cpu torch torchaudio
# basic-pitch dari jalur ONNX: paket normal mem-pin numpy<2 dan gagal build di Python 3.13
.venv/bin/pip install --no-deps basic-pitch
.venv/bin/pip install onnxruntime mir_eval resampyCatatan Python 3.13:
pip install basic-pitch(path TensorFlow) akan gagal: resolvernya menurunkan numpy ke versi tanpa wheel cp313 lalu mencoba build dari source. Pakai urutan--no-deps+onnxruntimedi atas — modelnya identik (basic_pitch/saved_models/icassp_2022/nmp.onnx), jauh lebih ringan dan cepat.
Instrumennya memakai rekaman asli, bukan oscillator:
| Instrumen | Sumber | Sample |
|---|---|---|
| Grand Piano | Salamander Grand Piano (Alexander Holm, CC-BY 3.0) | 30 (A0–C8) |
| Electric Piano | MusyngKite electric_piano_1 |
30 |
| Guitar (Nylon) | MusyngKite acoustic_guitar_nylon |
30 |
| String Ensemble | MusyngKite string_ensemble_1 |
30 |
python3 scripts/fetch_samples.py # ~3,5 MB, sekali sajaSkrip mengunduh ke web/public/audio/ dan menulis web/src/lib/sample-manifest.json.
Tone.Sampler melakukan pitch-shift antar sample (±1,5 semitone), jadi 30 sample per instrumen
sudah cukup — tidak ada artefak yang terdengar.
Analog Synth dan 8-bit Chiptune tetap sintetis (memang itu maksudnya).
cd web
npm install
cp .env.local.example .env.local # lalu sesuaikan
npm run build# terminal 1 — backend
cd backend && ./run.sh
# terminal 2 — frontend (production)
cd web && npx next start -p 8891 -H 0.0.0.0Buka http://localhost:8891.
cp deploy/midimagic-api.service deploy/midimagic-web.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now midimagic-api midimagic-webBackend membaca env var berikut (default di backend/app/config.py):
| Variabel | Default | Arti |
|---|---|---|
MIDIMAGIC_PORT |
8892 |
port API |
MIDIMAGIC_PUBLIC_ORIGIN |
http://localhost:8891 |
dipakai menyusun URL share |
MIDIMAGIC_DEMUCS_MODEL |
htdemucs |
model Demucs |
MIDIMAGIC_WORKERS |
1 |
worker paralel (naikkan hanya kalau RAM cukup) |
MIDIMAGIC_YT_MAX_SECONDS |
900 |
batas durasi YouTube |
MIDIMAGIC_YT_JS_RUNTIME |
node |
JS runtime untuk ekstraksi YouTube (kosongkan untuk mematikan) |
MIDIMAGIC_MAX_UPLOAD_MB |
60 |
batas upload |
MIDIMAGIC_ANALYSIS_SR |
22050 |
sample rate analisis (transkripsi + deteksi pedal) |
MIDIMAGIC_TEMPO |
(otomatis) | override tempo; kosongkan supaya dideteksi dari audio |
Frontend: NEXT_PUBLIC_API_BASE di web/.env.local — di-bake saat build, jadi ubah lalu
npm run build ulang.
| Method | Path | Fungsi |
|---|---|---|
GET |
/api/health |
status engine + statistik |
POST |
/api/jobs |
multipart file atau JSON {youtube_url, options} |
GET |
/api/jobs |
daftar job |
GET |
/api/jobs/{id} |
detail + progress + log event |
DELETE |
/api/jobs/{id} |
hapus job + artefaknya |
GET |
/api/jobs/{id}/midi |
.mid (query: download, semitones, tempo) |
GET |
/api/jobs/{id}/audio |
preview mp3, mendukung Range |
POST |
/api/shares |
buat share {job_id, title} |
GET |
/api/shares/{slug} |
metadata share |
GET |
/api/shares/{slug}/midi |
.mid hasil share |
Opsi job: stems (bool), stem_mode (vocals|full), target
(melody|instrumental|mix), engine (basic-pitch|librosa-pyin),
accuracy (fast|balanced|precise), onset_threshold, frame_threshold,
min_note_length, min_frequency, max_frequency, suppress_percussion,
min_velocity, merge_gap, quantize, tempo.
accuracy memilih satu set threshold; mengirim onset_threshold / frame_threshold /
min_note_length secara eksplisit akan menimpainya.
Upload dan hasil unduhan YouTube sangat berbeda levelnya, dan membawa rumble di bawah jangkauan piano. Keduanya membuat model "berhalusinasi" not. Jadi setiap input dinormalisasi puncaknya, di-band-limit ke rentang piano, dan perkusinya dibuang (harmonic/percussive separation) sebelum masuk model. Hit drum adalah sumber not palsu nomor satu; kalau pakai Demucs stem, langkah ini dilewati karena drumnya sudah hilang.
| Preset | Onset | Frame | Min panjang not | Rentang frekuensi |
|---|---|---|---|---|
fast |
0.60 | 0.35 | 90 ms | 55–3520 Hz |
balanced (default) |
0.50 | 0.30 | 58 ms | 41–4186 Hz |
precise |
0.38 | 0.22 | 42 ms | 33–4978 Hz |
Onset lebih rendah = lebih sensitif (not pelan ikut tertangkap, tapi lebih banyak sampah).
Model memecah satu not piano menjadi 2–3 event berdempetan — itu terdengar seperti stutter dan menggelembungkan jumlah not. Post-process menggabungkan kembali fragmen dengan pitch sama yang jaraknya < 55 ms, memangkas tumpang-tindih, dan membuang bintik ber-velocity rendah yang hampir pasti noise. Pada Canon in D (183 s): 1434 → 839 not (380 fragmen digabung, 251 noise dibuang), dan hasilnya jauh lebih mirip aslinya.
Pedal piano itu terdengar, bukan ditebak. Saat pedal ditahan, senar terus berbunyi melewati jeda antar-not alih-alih teredam. Jadi untuk setiap jeda diukur RMS jeda terhadap RMS not sebelumnya:
- jeda yang tetap berbunyi → pedal ditahan
- jeda yang jatuh ke senyap → pedal dilepas
Jeda berbunyi berurutan dirangkai jadi satu region pedal — persis seperti pemain menahan pedal sepanjang frasa. Region yang sangat panjang (> 8 s) dipotong dengan lepas-singkat 120 ms, seperti pemain menekan ulang pedal.
Hasilnya ditulis ke MIDI sebagai CC64, jadi pedalnya ikut di dalam file: pemutar membacanya,
dan siapa pun yang mengunduh atau membuka .mid hasil share juga mendapatkannya.
Hasil pengukuran pada dua rekaman piano nyata:
| Rekaman | Jeda panjang turun ke | Coverage pedal |
|---|---|---|
| Canon in D (Jacob's Piano) — pedal ditahan terus | −4 dB | 99% |
| Rachmaninoff Op. 39 No. 6 — staccato/marcato | −33 dB (benar-benar teredam) | 88% |
Perbedaan −4 dB vs −33 dB itulah yang dipakai algoritma untuk memutuskan.
Mode pedal di UI: Auto (memakai hasil deteksi), On (paksa tahan), Off (paksa lepas). Piano roll menampilkan lampu pedal kecil di atas keyboard saat pedal sedang aktif.
Fitur partitur/not-balok sudah dihapus atas permintaan: komponen SheetMusic.tsx,
penata balok VexFlow (lib/score.ts), deteksi kunci (lib/key.ts), tab Partitur di HP,
dan dependensi vexflow/jspdf/svg2pdf.js — semuanya hilang dari repo, bukan sekadar
disembunyikan. Fokus aplikasi sekarang: transkripsi yang presisi + piano roll + playback.
Catatan historis: tempo tetap penting walau partitur sudah tidak ada. Header tempo di MIDI dipakai pemutar untuk memetakan detik ke birama, jadi tempo yang salah tetap membuat playback dan piano roll meleset. Tempo dideteksi dari audio lalu ditulis ke header MIDI (contoh: Op. 39 No. 6 terdeteksi 136 BPM, bukan 120 bawaan model).
Ini bug paling berpengaruh di seluruh pipeline, dan sifatnya senyap.
prepare() dulu memanggil librosa.effects.trim, yang memotong silence di ujung file:
rekaman 199.9s menjadi 198.1s sebelum model melihatnya. Karena itu, setiap timestamp
hasil transkripsi relatif terhadap file yang sudah dipotong — seluruh MIDI meleset
~1.1s dari rekaman yang sedang kamu bandingkan, tetapi tetap konsisten dengan dirinya
sendiri, jadi tidak ada yang terlihat rusak.
Terukur terhadap rekaman asli:
| metrik | sebelum | sesudah |
|---|---|---|
| chroma similarity | 0.581 | 0.879 |
| jendela 10s dengan kemiripan ≥0.85 | 0/20 | 15/20 |
| lag terbaik | +1.138s | +0.023s |
| not yang didukung spektrum rekaman | 55.2% | 98.6% |
Perbaikannya tiga lapis:
- Jangan pernah mengubah panjang audio. Silence di-gate (dinolkan), bukan dipotong, sehingga timeline tetap persis. Setiap langkah yang mengubah panjang (resample, padding stem, trim) diikuti pemeriksaan panjang terhadap sumber dengan peringatan keras di log kalau berubah.
- Ukur sisa lag lalu koreksi (
pipeline/align.py). Bangun matriks chroma langsung dari event not MIDI (tanpa render audio), bandingkan dengan chroma audio sumber, cari lag terbaik, geser MIDI kalau |lag| > 40 ms. Jalan sebelum deteksi pedal, karena deteksi pedal memasangkan waktu not dengan jeda di audio. - Tampilkan hasilnya di UI sebagai
align_mspada job, supaya regresi langsung kelihatan.
Tidak ada cara jujur menilai transkripsi selain mendengarkannya bersamaan dengan
rekamannya. Tombol Asli di transport memutar rekaman sumber sejajar dengan piano
(disajikan dari /api/jobs/{id}/audio, elemen audio-nya terpisah dari DOM dan jam-nya
disetel dari clock pemutar). Kalau masih terasa maju/mundur, ada kontrol geser ±50 ms.
Kalau tombol ini butuh geseran besar, itu bug pipeline, bukan selera — laporkan.
Tempo itu heuristik, jadi bisa ditimpa lewat input ♪ = BPM di panel pemutar.
Next.js menyajikan halaman yang di-prerender dengan Cache-Control: s-maxage=31536000
— setahun di shared cache — sementara chunk JS-nya ber-hash dan berganti setiap
build. Browser/proxy yang memegang HTML setahun itu lalu meminta nama chunk yang sudah
tidak ada, hidrasi mati, dan aplikasi tampil berantakan beberapa detik setelah deploy
yang tadinya kelihatan baik.
Karena itu next.config.ts memisahkan kebijakannya:
| path | Cache-Control |
|---|---|
/, /share/:slug, halaman lain |
no-cache, no-store, must-revalidate |
/_next/static/** |
public, max-age=31536000, immutable (benar — nama file ber-hash) |
Verifikasi setelah build: curl -sD - -o /dev/null http://127.0.0.1:8891/ | grep -i cache-control
harus menunjukkan no-store. Kalau browser sudah terlanjur memegang HTML lama, hard
refresh (Ctrl/Cmd+Shift+R) sekali sudah cukup.
Roll-nya selalu menggambar 88 tuts (A0=21 → C8=108, 52 tuts putih). Yang berubah
adalah zoom: lebar kanvas = 52 × zoom, jadi pada HP 390 px dengan zoom default 26
hanya 14 dari 52 tuts putih yang masuk layar — dan itu terbaca sebagai "pianonya
kurang". Slider zoom dulu minimum 14, sehingga di HP mustahil melihat semuanya.
Sekarang: slider turun sampai 5, ada tombol "muat 88 tuts" (menghitung
zoom = lebar container / 52), dan ada penanda status di sebelahnya — hijau
88 tuts (A0–C8) kalau semuanya terlihat, kuning 14/52 tuts putih terlihat kalau
tidak, dengan tooltip yang menjelaskan.
Catatan implementasi: pembungkus roll berada di dalam tab yang dirender kondisional,
jadi useRef biasa bisa kosong saat effect jalan dan lebarnya tidak pernah terukur —
di HP ini diam-diam membuat penanda tetap "1/52" walau tombolnya bekerja. Pakai
callback ref (useState + ref={setEl}) supaya pengukuran ikut elemen yang
benar-benar ter-mount.
Dua engine, dan pemilihannya berpengaruh besar pada presisi.
| engine | jenis | untuk | kecepatan (4 vCPU) |
|---|---|---|---|
| transkun (default) | event-based neural semi-CRF, khusus piano | piano solo | ~0.6× durasi audio |
| basic-pitch | frame-based, polifonik umum | campuran vokal/instrumen, stem | ~0.08× durasi audio |
| librosa pyin | pelacak f0 monofonik | satu nada (vokal, seruling) | cepat |
transkun adalah Transkun (Yujia Yan, MIT) —
prediksi interval not langsung via semi-CRF, bukan per-frame, dan memprediksi
pedal sustain sendiri (CC64 + CC67). Dipasang di venv terpisah:
bash scripts/setup_transkun.shSemua metrik diukur terhadap rekaman sumber, bukan prepared.wav:
| engine | not | chroma | jendela ≥0.85 | recall onset | onset luput |
|---|---|---|---|---|---|
| basic-pitch | 1546 | 0.879 | 15/20 | 0.867 | 54 |
| transkun (pipeline) | 2483 | 0.894 | 18/20 | 1.000 | 0 |
Recall 1.000 = transkun menemukan setiap onset yang terdeteksi di rekaman, tanpa
satu pun luput. Precision onset-nya (0.185) persis di plafon teoretis untuk
pembanding ini: librosa.onset_detect hanya menemukan 405 onset di lagu padat ini,
jadi engine mana pun yang mengeluarkan >405 onset tidak mungkin melewati 405/N.
Baca recall dan chroma, jangan precision, saat membandingkan engine di sini.
Integrasi ini awalnya membuat hasil lebih buruk, dan penyebabnya bukan modelnya:
1. Pembersihan MIDI yang tidak sadar-engine. post.clean ditala untuk model
frame-based, di mana event pendek-dan-lemah hampir selalu noise. Transkun melaporkan
durasi tekanan tuts (median 0.054s pada etude cepat) dan sengaja me-restrike nada
yang sama. min_note_length=0.058s saja menghapus 864 dari 2565 not, dan recall
onset jatuh 0.884 → 0.516. Sekarang transkun memakai "profil event": tidak ada not
pendek yang dibuang, tidak ada re-strike yang ditempel.
2. Tanda koreksi alignment terbalik. align.py menggeser MIDI sebesar -lag,
padahal konvensinya lag positif = MIDI terlalu cepat, jadi koreksinya +lag.
Akibatnya offset 23 ms berubah jadi 70 ms — koreksi menggandakan error alih-alih
menghapusnya. Diverifikasi empiris: menggeser MIDI 0.5s lebih lambat melaporkan
lag −464 ms.
Sekarang align juga memverifikasi koreksinya sendiri: setelah menggeser, lag
diukur ulang, dan kalau sisa offset malah membesar, pergeseran dibatalkan. Estimator
chroma itu heuristik dan tandanya mudah terbalik (dan memang terbalik), jadi yang
dipercaya bukan koreksinya, tapi efek terukurnya.
Transkun melaporkan durasi tekanan tuts, bukan durasi bunyi. Di bawah pedal,
not berbunyi selama pedal ditahan. Tanpa koreksi ini playback terdengar dipetik-petik
(nada terpotong pendek di mana-mana). pipeline/pedal_extend.py menahan setiap
not sampai pedal dilepas, dibatasi tiga hal: tidak melewati serangan berikutnya pada
nada yang sama (restrike meredam senar), tidak melewati akhir rekaman, dan tidak lebih
dari MIDIMAGIC_TRANSKUN_MAX_EXTEND (default 6s).
Pada Rachmaninoff: 1213 dari 2483 not diperpanjang, +484s bunyi, median not 0.054s → 0.134s.
Bisa, dan batasnya bukan durasi melainkan waktu. Di atas
MIDIMAGIC_LONG_AUDIO_SECONDS (default 600s) pipeline berubah bentuk:
- Transkun jalan per-window — window 240s dengan tumpang tindih 6s
(
MIDIMAGIC_TRANSKUN_CHUNK/_OVERLAP). Ini soal memori: inference transkun tumbuh seiring panjang input sementara mesin ini cuma punya ~3,6 GB bebas, jadi memproses 98 menit dalam satu panggilan akan OOM. Not yang jatuh di batas window terlihat utuh oleh window tetangganya, lalu duplikatnya dibuang saat merge (nada sama, onset dalam 40ms). - Unduhan jadi mono 22,05 kHz. Dua jam WAV stereo 44,1k ≈ 1 GB di disk; bentuk mono 22,05k ≈ 260 MB, dan jalur analisis memang resample ke bentuk itu juga.
- Progres per-window muncul di log job, dan UI menampilkan perkiraan total
memakai
SPEED_FACTORhasil ukur (bukan tebakan).
| hasil | |
|---|---|
| window | 10 @240s (+6s overlap), 0 gagal |
| wall clock | 1671s = 0.70× durasi audio |
| peak RSS | 2245 MB — datar, tidak tumbuh seiring panjang |
| not | 12937 (238 duplikat tumpang tindih dibuang) |
| pedal | 663 event CC |
| cakupan | not terakhir 2397s dari 2400s — tidak ada yang terpotong |
Angka 0,70× itu pesimistis: run ini berjalan sambil bersaing CPU dengan build Next.js, dan tiap window men-spawn proses transkun baru yang memuat ulang model (~10–15s × 10). Estimasi wajar untuk mesin idle ≈ 0,63×.
Konsekuensi praktis — perkirakan dari durasi video:
| durasi audio | transkun (0,63–0,70×) | basic-pitch (~0,08×) |
|---|---|---|
| 10 menit | 6–7 menit | ~50 detik |
| 40 menit | 25–28 menit | ~3 menit |
| 98 menit | ~1 jam | ~8 menit |
Jadi: transkun untuk presisi, basic-pitch kalau yang penting selesai cepat — pemilih enginenya ada di Pengaturan lanjutan. Yang menentukan pilihan biasanya apakah materinya piano solo (transkun) atau aransemen campuran (basic-pitch).
cd backend && .venv/bin/python tests/smoke_api.py data/test/melody_stereo.wav # cepat
cd backend && .venv/bin/python tests/smoke_api.py data/test/melody_stereo.wav --stems # ikut DemucsSkrip membuat file WAV sintetis sendiri (data/test/), menjalankan job sampai selesai, lalu
memverifikasi MIDI, preview audio, range request, share, dan export transpose.
Alat diagnosa pipeline (memakai audio dari job yang sudah ada):
cd backend
.venv/bin/python tests/pipeline_check.py data/jobs/<id>/source.wav --preset balanced
.venv/bin/python tests/pedal_probe.py /tmp/mm_check/prepared.wav /tmp/mm_check/raw.mid
.venv/bin/python tests/gap_shape.py /tmp/mm_check/prepared.wav /tmp/mm_check/raw.midgap_shape.py mencetak bentuk decay di dalam setiap jeda antar-not — inilah data yang
menentukan ambang RINGING_RATIO: piano yang dipedal turun hanya beberapa dB, yang teredam
jatuh puluhan dB.
Menilai ketepatan terhadap rekaman aslinya:
cd backend
.venv/bin/python tests/accuracy_report.py prepared.wav output.mid
.venv/bin/python tests/key_probe.py output.mid "A minor"accuracy_report.py merender MIDI-nya jadi audio (synth harmonik sederhana), lalu
membandingkan chroma dengan rekaman asli — time-aligned maupun bebas-waktu (DTW) —
plus precision/recall onset terhadap onset detector librosa. key_probe.py mencetak
perbandingan beberapa skema pembobotan kunci terhadap kunci yang kamu tahu benar.
diagnose.py menjawab "di mana persisnya hasilnya meleset": kemiripan chroma per
jendela 10 detik (bagian mana yang salah), lag global terbaik (geseran atau nada salah),
dukungan spektrum per not (berapa persen not benar-benar ada di rekaman), dan sebaran
offset onset. Bandingkan selalu terhadap rekaman sumber, bukan prepared.wav —
prepared.wav sudah satu timeline dengan MIDI, jadi ia menyembunyikan bug geseran.
| Tahap | 4 detik audio | Lagu 3 menit |
|---|---|---|
| Pra-proses (normalisasi + band-limit + HPSS) | ~1 s | ~10–13 s |
| Transkripsi basic-pitch (ONNX) | ~1,5 s | ~4–6 s |
| Bersihkan MIDI | <0,1 s | ~0,1 s |
| Deteksi pedal | <0,1 s | ~0,3 s |
| Stem separation Demucs | ~30 s | ~20–30 menit |
Demucs memang berat di CPU. Kalau hanya butuh MIDI cepat, matikan stem separation — pra-proses sudah membuang perkusi, jadi akurasinya masih bagus.
midimagic/
├── backend/
│ ├── app/
│ │ ├── main.py # FastAPI: jobs, shares, range streaming
│ │ ├── jobs.py # runner + orkestrasi pipeline
│ │ ├── db.py # SQLite (additive)
│ │ ├── config.py
│ │ └── pipeline/
│ │ ├── fetch.py # upload / yt-dlp → WAV
│ │ ├── stems.py # Demucs
│ │ ├── transcribe.py # pra-proses + basic-pitch / librosa-pyin → MIDI
│ │ ├── sustain.py # deteksi pedal dari audio → CC64 di MIDI
│ │ ├── align.py # ukur & koreksi offset MIDI vs rekaman sumber
│ │ └── post.py # gabung fragmen, buang noise, transpose/tempo
│ ├── tests/
│ │ ├── smoke_api.py # uji end-to-end lewat HTTP
│ │ ├── pipeline_check.py # jalankan pipeline pada audio nyata, cetak waktu
│ │ ├── accuracy_report.py # chroma vs rekaman + precision/recall onset
│ │ ├── diagnose.py # di mana persisnya hasilnya meleset
│ │ ├── key_probe.py # banding skema pembobotan deteksi kunci
│ │ ├── gap_shape.py # bentuk decay di jeda antar-not (tuning pedal)
│ │ └── pedal_probe.py # sebaran rasio energi jeda
│ ├── data/ # sqlite, artefak job, share
│ └── run.sh
├── web/
│ ├── src/app/{page,layout,globals.css,share/[slug]/page.tsx}
│ ├── src/components/ # Studio, PianoRoll, Transport, BottomTabs (mobile), dll
│ ├── src/lib/ # api, audio (Tone), midi (+CC64),
│ │ # instruments, useMediaQuery
│ └── public/audio/ # sample instrumen (Salamander + MusyngKite)
└── deploy/ # unit systemd
Breakpoint tunggal di 1023 px (useMediaQuery.ts). Di bawah itu Studio berubah jadi satu
kolom + bottom tab bar (Putar · Sumber · Suara · Pustaka), piano roll
dipendekkan (250 px + keyboard 74 px), dan overflow-x dikunci supaya tidak ada scroll
horizontal. Posisi tab dipertahankan saat memuat hasil baru (otomatis pindah ke Putar).
Halaman share memakai breakpoint yang sama.
basic-pitch (Spotify) · Transkun (MIT, Yujia Yan) · Demucs (Meta) · Tone.js · yt-dlp · Next.js