如果你彈完一句旋律,短暫停手,iPhone 上的模型就依照這段 MIDI prompt 嘗試接下一句,這會比「輸入文字、等 AI 生成整首歌」更像真正的共同創作。這篇要做的,就是把AI 鋼琴 Copilot拆成兩條可走的路:先用現成 App 完成第一次 MIDI 接句,再用一個縮小版 notebook 看懂模型怎麼猜下一顆音。
本文專為第一次碰 MIDI 與音樂模型的讀者寫。你不必先會樂理或機器學習;我會給出器材檢查、NOTE 表示法、tiny decoder 骨架與盲式 A/B 流程。先把界線說清楚:RollTab 作者自述產品模型約 125M 參數;下面使用公開授權 MAESTRO 資料的教學 notebook 是縮小版,兩者不能拿結果互相比。
先說結論:先玩,再拆模型
- 只想感受人機接句:準備 iPhone/iPad、相容的 MIDI 鍵盤與 RollTab,依快速起步檢查清單完成第一輪「你彈、它接」。
- 想知道模型怎麼做:把每顆音壓成
NOTE(pitch, delta, duration, velocity),用少量 MAESTRO MIDI 訓練一個兩層 tiny decoder。 - 想知道有沒有變好:不要只挑最好聽的 demo;用同一首曲、同一切點的 8/16/32-note 巢狀提示,隨機左右順序做盲式 A/B。
本文的錨點公式:AI 鋼琴 Copilot = MIDI 音符資料(音高+間隔+時值+力度)+只預測下一顆 NOTE 的小型自回歸模型。

AI 鋼琴 Copilot 不是「邊彈邊伴奏」
這裡的 Copilot 比較像寫程式時的自動完成:你先彈一個短句,停頓或按下觸發鍵,模型再接續。RollTab 的 App Store 說明把操作分成 Auto 與 Explicit;它不是在每一拍持續聽你、同步配伴奏。這個差別很重要,因為「接句」容許模型在停頓後生成,「即時伴奏」則還要同時處理拍點預測、演奏延遲與雙方搶拍。
作者的技術文描述一個約 125M 參數、decoder-only 的 MIDI 模型,並回報在 iPhone 15 上約 108 notes/s。這是作者在未完整公開硬體 SKU 與測試 protocol 下回報的單機吞吐量,不是首音延遲,也不能證明整段互動沒有卡頓。本文因此只把它當成「小模型可以放到手機做接續」的產品案例,不把數字外推成所有 iPhone 的效能。
路線 A:依快速起步清單完成 iPhone MIDI 接句
步驟 1:先確認三樣東西
- 裝置:截至 2026 年 8 月 21 日,RollTab 1.0 的官方商店頁列出 iPhone/iPad、最低 iOS/iPadOS 18.0。
- 鍵盤:使用 class-compliant USB MIDI 鍵盤,或鍵盤明確提供的 Bluetooth MIDI 模式;一般藍牙音訊配對不等於藍牙 MIDI。
- 連接:相容且供電足夠的 USB-C MIDI 裝置可直接接或經供電 hub;Lightning iPhone 通常要相機轉接器。若鍵盤吃電太多,改用有外接電源的 hub。
Apple 的iPad MIDI 連接指南同時列出 USB、介面與 Bluetooth MIDI,並提醒部分裝置需要供電;相機轉接器說明則涵蓋 Lightning iPhone 的 MIDI 配件情境。連線時先解鎖 iPhone;若有「允許配件連線」提示,先允許,再回 App 看是否收到按鍵。
步驟 2:用最小變因做第一輪
痛點通常不是「AI 不會作曲」,而是你一次調太多旋鈕。解法是先把流程鎖死:在 RollTab 確認目標 MIDI 裝置已被辨識並有輸入,使用中間音域、固定速度,彈 8~16 顆單音後停手;先讓 temperature 保持預設,只生成一小段。成功標準不是「像名曲」,而是畫面同時能區分你的輸入與模型輸出,而且接續音確實發聲。
第一次無聲時,依序檢查四件事:App 有沒有收到 MIDI、音量與靜音模式、是否選到 Explicit 卻沒按生成、鍵盤是否只送到另一個 App。不要先把責任推給模型;先用系統或獨立 MIDI monitor 確認鍵盤有送出事件,再確認 RollTab 收到輸入,最後檢查 App 與系統音訊輸出。
路線 B:把一顆音變成 compound NOTE
MIDI 不是錄音波形,而是一串「何時按哪個鍵、按多久、用多大力度」的事件。對新手最直覺的做法,是讓一顆音就是一筆資料:
NOTE(
pitch=60, # C4
delta=12, # 距前一顆音的 onset 間隔
duration=24, # 按住多久
velocity=10 # 量化後的力度格
)
pitch 告訴你哪個鍵,delta 保留節奏,duration 保留連斷,velocity 保留強弱。如果數顆音同時開始,先按 pitch 排序,第一顆保留時間差,其餘設 delta=0,和弦就不會被誤認成快速琶音。

RollTab 作者採 24 steps per quarter note,並為較長的間隔與時值保留額外 bins;本文 notebook 也用 24 格,但為了教學把欄位改成連續截斷的較小字典。這不是作者模型的同一套 tokenizer。更早的 Compound Word Transformer也已展示把多個音樂屬性組成複合 token,因此這裡應把 compound NOTE 視為成熟設計方向,而不是「第一次有人想到」。
縮小版 notebook:用 32 首 MIDI 跑 tiny decoder
截至 2026 年 8 月 21 日,作者技術文提供架構、資料量級與轉換方向,但頁面沒有附可直接跟跑的訓練 repo、權重、精確 corpus manifest 或轉檔 recipe。所以下面改用 Google Magenta 公開的 MAESTRO v3 MIDI-only,只取 train split 前 32 首,目標是「看懂資料如何進模型、模型如何吐出 MIDI」,不代表 125M 產品結果。
MAESTRO v3 完整 MIDI 約 56 MB、採 CC BY-NC-SA 4.0;示範碼下載完整壓縮檔,但只讀少量檔案。若你的網路、儲存空間或使用情境不適合該授權,就跳過下載,改用自己有權處理的 MIDI。不要把「曲子進入公版」誤解成每個 MIDI 編曲檔都沒有權利問題。
步驟 3:建立 Colab 環境
%pip install -q "pretty_midi==0.2.11.post0"
!wget -q -O maestro-v3.0.0-midi.zip "https://storage.googleapis.com/magentadata/datasets/maestro/v3.0.0/maestro-v3.0.0-midi.zip"
!echo "70470ee253295c8d2c71e6d9d4a815189e35c89624b76d22fce5a019d5dde12c maestro-v3.0.0-midi.zip" | sha256sum -c -
!unzip -qo maestro-v3.0.0-midi.zip
Colab 已附 PyTorch,就不要為了追最新版重裝整個 runtime;先印出 torch.__version__ 並記在實驗結果。套件版本、隨機種子與選了哪些檔案都要一起保存,否則下次聽到差異,你無法判斷是模型還是環境改了。
步驟 4:量化與切序列
from pathlib import Path
import csv, importlib.metadata
import pretty_midi, torch
print(torch.__version__, importlib.metadata.version("pretty_midi"))
PPQ = 24
def encode_midi(path):
pm = pretty_midi.PrettyMIDI(str(path))
notes = sorted(
[n for inst in pm.instruments if not inst.is_drum
for n in inst.notes if n.end > n.start],
key=lambda n: (n.start, n.pitch)
)
rows, prev_onset = [], 0
for n in notes:
onset = round(pm.time_to_tick(n.start) * PPQ / pm.resolution)
end = round(pm.time_to_tick(n.end) * PPQ / pm.resolution)
rows.append([
n.pitch,
min(max(onset - prev_onset, 0), 192),
min(max(end - onset, 1), 384),
min(max(n.velocity * 16 // 128, 0), 15),
])
prev_onset = onset
return torch.tensor(rows, dtype=torch.long).reshape(-1, 4)
root = Path("maestro-v3.0.0")
with (root / "maestro-v3.0.0.csv").open() as f:
rows = list(csv.DictReader(f))
train_paths = [root / r["midi_filename"] for r in rows if r["split"] == "train"][:32]
valid_paths = [root / r["midi_filename"] for r in rows if r["split"] == "validation"][:8]
train_pieces = [encode_midi(p) for p in train_paths]
valid_pieces = [encode_midi(p) for p in valid_paths]
這段會把音高限制在 128 類、delta 193 類、duration 385 類、velocity 16 類,也沿用 MAESTRO 官方 train/validation split。真正的資料管線還應處理踏板、移除空曲與過短序列、持久記錄曲目 ID,並另外檢查近似版本是否洩漏。tiny 練習省略這些,只適合學結構。
步驟 5:四組 embedding 相加,四個 head 分別猜
import torch.nn as nn
import torch.nn.functional as F
VOCABS = (128, 193, 385, 16)
class TinyMidiDecoder(nn.Module):
def __init__(self, d=128, layers=2, heads=4, max_len=128):
super().__init__()
self.emb = nn.ModuleList([nn.Embedding(v, d) for v in VOCABS])
self.pos = nn.Embedding(max_len, d)
block = nn.TransformerEncoderLayer(
d, heads, dim_feedforward=4*d, dropout=0.1,
batch_first=True, norm_first=True
)
self.backbone = nn.TransformerEncoder(block, layers)
self.out = nn.ModuleList([nn.Linear(d, v) for v in VOCABS])
def forward(self, x):
length = x.size(1)
if length > self.pos.num_embeddings:
raise ValueError("sequence exceeds max_len")
h = sum(e(x[..., i]) for i, e in enumerate(self.emb))
h = h + self.pos(torch.arange(length, device=x.device))[None]
causal = torch.triu(torch.ones(
length, length, dtype=torch.bool, device=x.device
), diagonal=1)
h = self.backbone(h, mask=causal)
return [head(h) for head in self.out]
model = TinyMidiDecoder()
batch = train_pieces[0][:129].unsqueeze(0)
logits = model(batch[:, :-1])
target = batch[:, 1:]
loss = torch.stack([
F.cross_entropy(y.flatten(0, 1), target[..., i].flatten())
for i, y in enumerate(logits)
]).mean()
print(loss.item())
模型先把四個欄位的 embedding 相加,再用 causal mask 保證第 t 顆音看不到未來,最後由四個分類 head 預測下一顆 NOTE。這個版本刻意讓四個 head 獨立;作者案例另有欄位內的 nested decoder,會讓 pitch 的選擇影響後續 duration/velocity,複雜度更高。

接著把每首曲切成 129-note 片段:前 128 個位置各自預測緊接著的下一顆音,因此共有 128 個 target。未滿 129 顆的尾段直接捨棄,所以 tiny 版不使用 PAD 或 ignore_index。先用少量片段確認 loss 會下降,再增加資料。下面這格限制每首最多 8 段;執行時間仍會隨 Colab runtime 而變。
from torch.utils.data import DataLoader
def chunks(pieces, length=129, max_each=8):
out = []
for piece in pieces:
added = 0
for start in range(0, max(len(piece) - length + 1, 0), length - 1):
out.append(piece[start:start + length])
added += 1
if added == max_each:
break
if not out:
raise ValueError("No full 129-note chunks")
return torch.stack(out)
def next_note_loss(model, batch):
pred, target = model(batch[:, :-1]), batch[:, 1:]
return torch.stack([
F.cross_entropy(y.flatten(0, 1), target[..., i].flatten())
for i, y in enumerate(pred)
]).mean()
@torch.inference_mode()
def evaluate(model, batches):
model.eval()
values = [next_note_loss(model, x.to(device)).item() for x in batches]
return sum(values) / len(values)
torch.manual_seed(42)
device = "cuda" if torch.cuda.is_available() else "cpu"
train_data = chunks(train_pieces)
valid_data = chunks(valid_pieces, max_each=2)
loader = DataLoader(train_data, batch_size=16, shuffle=True)
valid_loader = DataLoader(valid_data, batch_size=16, shuffle=False)
model = TinyMidiDecoder().to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)
for epoch in range(3):
model.train()
total = 0.0
for batch in loader:
batch = batch.to(device)
loss = next_note_loss(model, batch)
optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
total += loss.item()
print(epoch, total / len(loader), evaluate(model, valid_loader))
最後從 validation 曲目拿 prompt,對四個 head 使用固定的教學用 sampling policy,連續抽樣 64 顆,再固定 120 BPM 寫回 MIDI。四個字典大小不同,top-k 不必設成同一數字;正式實驗要先固定每欄策略。這只是 audition 檔,不能拿同一批 validation 反覆調參後又當 final test。
@torch.no_grad()
def pick(logits, temperature=0.9, top_k=12):
if temperature <= 0 or top_k < 1:
raise ValueError("temperature and top_k must be positive")
values, ids = torch.topk(logits, min(top_k, logits.numel()))
probs = torch.softmax(values / temperature, dim=-1)
return ids[torch.multinomial(probs, 1)].item()
@torch.no_grad()
def continue_notes(prompt, count=64):
seq = prompt.clone().to(device)
model.eval()
field_top_k = (12, 12, 24, 8)
for _ in range(count):
window = seq[-model.pos.num_embeddings:]
last = [head[0, -1] for head in model(window[None])]
last[2] = last[2].clone()
last[2][0] = float("-inf")
note = torch.tensor([
pick(x, top_k=k) for x, k in zip(last, field_top_k)
], device=device)
seq = torch.cat([seq, note[None]], dim=0)
return seq.cpu()
piece, end = valid_pieces[0], len(valid_pieces[0])
while end >= 32 and piece[end - 32, 1].item() == 0:
end -= 1
if end < 32:
raise ValueError("No chord-safe 32-note prompt")
prompt = piece[end - 32:end].clone()
prompt[0, 1] = 0
torch.manual_seed(20260821)
generated = continue_notes(prompt)
tempo, onset = 120, 0
seconds_per_step = (60 / tempo) / PPQ
out = pretty_midi.PrettyMIDI(initial_tempo=tempo)
piano = pretty_midi.Instrument(program=0)
last_by_pitch = {}
for pitch, delta, duration, velocity_bin in generated.tolist():
onset += delta
start = onset * seconds_per_step
if pitch in last_by_pitch:
previous = last_by_pitch[pitch]
if start <= previous.start:
continue
previous.end = min(previous.end, start)
note = pretty_midi.Note(
velocity=4 + 8 * velocity_bin, pitch=pitch,
start=start,
end=(onset + max(duration, 1)) * seconds_per_step,
)
piano.notes.append(note)
last_by_pitch[pitch] = note
out.instruments.append(piano)
out.write("tiny-midi-continuation.mid")
from IPython.display import Audio, display
display(Audio(out.synthesize(fs=16000), rate=16000)) # 正弦波預聽,不是鋼琴音色
如果結果只會重複一個和弦,先查資料、delta 分布與取樣,不要急著加大模型。先固定 seed,再分別向上、向下 sweep temperature,檢查 pitch/delta 分布與重複 onset groups;降低 temperature 不能預設會改善迴圈。每次只改一項,再比較 validation loss 與輸出。
8/16/32-note prompt:怎麼做不自欺的盲式 A/B?
三段不同歌曲的 demo 不能證明「32 顆一定比 8 顆好」,因為曲風、和弦密度與切點都換了。公平做法是從同一首曲、同一目標切點取巢狀 suffix:8-note 是最短尾巴,16-note 包住它,32-note 再包住前兩者。不要從和弦中間切;除了 note 數,也記 onset groups、拍數或小節數。
- 固定生成條件:continuation 的生成 horizon 優先固定拍數或小節,並同時回報 note 數;temperature、各欄 sampling policy、seed 數、音色、響度與候選預算也全部相同。
- 讓聽眾聽到同一段人類前導:模型私下只拿到 8/16/32 顆,避免較長 prompt 因為播放內容較多而先天占便宜。
- 每題重新隨機 A/B:允許「平手」,分開問「接得順不順」與「單獨好不好聽」,不要合成一個模糊總分。
- 檢查順序偏誤:把左右鏡像版本隨機分派給不同聽眾;若同一人重測,只使用隔開的小子集,分析時納入重複量測。回報一致率與分歧,不要只留下偏好百分比。

你可以用欄位 prompt_id, context_len, seed, side, continuity, musicality, tie, listener_id 收集結果。先做小規模 pilot 看題目是否聽得懂,再依變異與預期差異做 power calculation;不要先決定「至少幾人就夠」。若資料曾進入訓練、scheduled sampling 選型或 DPO,最終測試就要換成 composition-disjoint 的保留集。
scheduled sampling 何時用?先別急著加 DPO
一般 teacher forcing 在訓練時永遠餵正確答案,生成時卻只能接自己的預測;這叫 exposure bias。scheduled sampling 原始論文的想法,是逐步讓模型在訓練時也看到自己的輸出。但它是 heuristic,後續理論分析指出目標可能不一致;不是打開開關就保證音樂變好。
RollTab 技術文描述的做法更窄:在同一顆 compound NOTE 的欄位內,偶爾把模型自己預測的 pitch 餵給 duration/velocity,比例由 0 慢慢提高。它不等於把整段序列的上一顆錯音都餵回去,也不能解決所有 note-to-note exposure bias。你的 tiny decoder 先把 baseline、資料切分與盲測做穩,再把這個欄位內 ablation 加進來。
DPO(Direct Preference Optimization)是選配中的選配。只有當你有同一 prompt 的 chosen/rejected continuation、凍結的 reference model、完整四欄 sequence log-probability、composition-disjoint split 與獨立人類終測,才值得做。若偏好標籤與最後評審都來自同一個 AI judge,最多能說模型更貼近那位 judge,不能說人類更喜歡。
從 notebook 到 iPhone,還差哪幾關?
- 模型邊界:先固定最長 context、輸入 shape 與四個輸出 head;動態控制流程常比純矩陣更難轉。
- 數值對齊:先在同一 fixture 比對四個最終 logits 並設定容許誤差,再用 greedy 或預先固定的抽樣值比對 NOTE 與 MIDI;不一致時,才另建能輸出指定中間張量的 diagnostic model。
- 量化:coremltools 最佳化文件支援權重量化,但 INT8 檔案較小不代表所有裝置都用 INT8 計算,也不保證延遲等比例縮短。
- 量測:分開記 cold load、prompt ingestion、first-note latency、持續生成 p50/p95、記憶體與熱降頻;notes/s 只回答其中一小部分。
- App 層:處理 MIDI 裝置插拔、背景中斷、音訊 session、取消生成、匯出與錯誤提示,這些都不在 notebook 裡。
換句話說,Core ML conversion 只是中間站,不是部署完成。想先補足本機小模型的硬體觀念,可以讀 本機 LLM 顯存指南;想理解模型為什麼會從資料學到規律,則接著看 AI 模型如何學習。
最常踩的 6 個坑
- 把接句寫成即時伴奏:產品是停頓/觸發後生成,就用「call-and-response」描述,別偷換成持續同步演奏。
- 用 note 數假裝音樂時間:密集和弦的 32 notes 可能只是一小段;同時回報 onset groups、拍或小節。
- 資料洩漏:同一首作品的不同演奏、轉調或近似編曲若跨 split,模型可能只是記住旋律。
- 忽略踏板:把 sustain control change 丟掉,實際聽感的時值會和 token 不同;至少在限制中說明。
- 只看 validation loss:loss 適合抓訓練壞掉,不能替你回答「接得像不像同一句音樂」。搭配盲聽與重複率。
- 拿 AI judge 當人類:它可以協助篩選,但最終偏好應由目標使用者盲評,且允許平手。
如果你想系統化建立評測,而不是只聽幾段 sample,AI Evals 新手指南會補上資料集、rubric 與回歸測試的觀念;如果未來要把大模型能力壓進小模型,可再讀 模型蒸餾教學。
FAQ:AI 鋼琴 Copilot 新手最常問的 8 題
1. 一定要買 MIDI 鍵盤嗎?
RollTab 的 live input 路線要;notebook 練習不用。App Store 說明列出 MIDI keyboard 為 live input 所需硬體。只學模型時,直接讀 MIDI 檔即可。
2. RollTab 一定要連網嗎?
官方商店文字稱生成在裝置上、無需帳號或雲端;本文沒有做獨立網路稽核。因此可把它當產品標示,不把它寫成已驗證的整個 App 零連線或隱私保證。
3. Android 或 Mac 可以照做嗎?
本文只驗證 iPhone/iPad 的官方上架資訊。模型概念可搬到其他平台,但連線、runtime 與 App 介面都要另查,不能從 iOS listing 推論。
4. notebook 能做出同一個 125M 模型嗎?
不能。本文用不同資料、較小架構與獨立 head;截至 2026 年 8 月 21 日,本文查閱的作者技術文與官方頁面也未附精確 corpus、checkpoint 與轉換 fixture。這裡只重做學習步驟,不等同產品結果。
5. prompt 用 8、16 還是 32 notes 最好?
不能從作者公開的不同曲目 demo 得出固定答案。較短 context 可能留下更多生成自由,較長 context 可能提供更多線索,也可能帶入無關資訊;用同曲同切點的巢狀測試決定。
6. 108 notes/s 就代表沒有延遲嗎?
不代表。作者未公開 benchmark protocol,因此 108 notes/s 無法告訴你是否或如何計入 cold load、prompt ingestion、first-note latency 與持續熱度;要分開量 p50/p95 才知道互動手感。
7. scheduled sampling 是必做嗎?
不是。先做乾淨 baseline,再把它當 ablation;同時看 likelihood、重複率與盲聽,避免只挑支持假設的指標。
8. 新手需要做 DPO 嗎?
通常不需要。沒有高品質成對偏好、凍結 reference 與獨立人類終測時,先把資料、取樣與 A/B 做好,回報會更可信。
給新手的 5 個重點
- 先用 RollTab 理解「你彈、停頓、AI 接」的產品互動,再進模型。
- 把 pitch、delta、duration、velocity 綁成一顆 NOTE,資料會比音訊波形容易檢查。
- tiny decoder 的成功是流程跑通,不是小資料就能複製 125M 模型品質。
- 8/16/32 必須同曲同切點、固定生成條件,才接近公平比較。
- scheduled sampling 與 DPO 都是後段工具;資料權利、split、baseline 與人類盲測排在前面。
接著閱讀
左右滑動查看更多推薦
