整理 Song 合成中容易困惑的 Score 与 Note 的规范。
实现依据来自 Revolution\Voicevox\Song\Note / Score / SongAudioQuery 的源码。
Note 类
Note 表示 1 个音符(或休止符)。
use Revolution\Voicevox\Song\Note;
$note = Note::make(
length: 94,
lyric: 'ド',
key: 60,
id: 'intro-1',
);
构造函数参数
| 参数 | 类型 | 说明 |
|---|
length | int | 帧长。将 秒数 × 93.75 取整所得的值 |
lyric | string | 歌词。休止符为空字符串 '' |
key | int|null | MIDI 音符编号。休止符为 null |
id | string|null | 生成 FrameAudioQuery 时会拷贝到 note_id 的辅助 ID |
帧长计算
length 的单位是帧。从秒数换算如下。
由于该值并不直观,如果你熟悉 MIDI,使用 Note::len() 更实用。
Note::len(int $ticks, int $bpm = 125): int
- 前提是将四分音符视为
480 ticks
- 默认
125 BPM
- 内部通过
round() 取整,因此长曲子中会逐渐累积舍入误差
Note::make(length: Note::len(480, 120), lyric: 'ド', key: 60); // 四分音符
Note::make(length: Note::len(960, 120), lyric: 'ミ', key: 64); // 二分音符
Note::len() 虽然方便,但在长曲子中请注意累积误差。必要时可以按段落重新调整长度。
休止符的表示
休止符通过以下组合表示。
Note::make(length: 15, lyric: '', key: null);
MIDI 音符编号参考
| Note | MIDI |
|---|
| C4 | 60 |
| D4 | 62 |
| E4 | 64 |
| F4 | 65 |
| G4 | 67 |
| A4 | 69 |
| B4 | 71 |
| C5 | 72 |
Score 类
Score 是持有 Note 数组的乐谱对象。
use Revolution\Voicevox\Song\Score;
$score = Score::make([
Note::make(length: 15, lyric: '', key: null),
Note::make(length: Note::len(480, 120), lyric: 'ド', key: 60),
]);
重要规则
第一个 note 必须是休止符。
主要 API
- 通过
Score::make(array $notes) 生成
- 通过
add(Note $note) 追加
- 通过
toJson() 转换为 JSON 字符串
$score = Score::make([
Note::make(length: 15, lyric: '', key: null),
]);
$score->add(Note::make(length: Note::len(480, 120), lyric: 'ド', key: 60));
$json = $score->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
Score 构造函数在数组输入时会吸收 lyric 中的空字符串/null,因此即便是经过 ConvertEmptyStringsToNull 中间件处理的表单输入也易于处理。
实用模式:JSON 文件管理(推荐)
在实际产品中,与其把乐谱直接写在 PHP 里,用 JSON 管理会更利于运维。
- 保存自定义格式的 JSON
- 读入并转换为
Note / Score
- 传给
Voicevox::song() 生成
JSON 示例
{
"teacher": 6000,
"speaker": 3001,
"bpm": 120,
"notes": [
{ "ticks": 120, "lyric": "", "key": null, "id": "r0" },
{ "ticks": 480, "lyric": "ド", "key": 60, "id": "n1" },
{ "ticks": 480, "lyric": "レ", "key": 62, "id": "n2" },
{ "ticks": 960, "lyric": "ミ", "key": 64, "id": "n3" },
{ "ticks": 120, "lyric": "", "key": null, "id": "r1" }
]
}
读入并合成
use Illuminate\Support\Facades\Storage;
use Revolution\Voicevox\Song\Note;
use Revolution\Voicevox\Song\Score;
use Revolution\Voicevox\Voicevox;
$chart = json_decode(
Storage::disk('local')->get('songs/sample.json'),
true,
flags: JSON_THROW_ON_ERROR,
);
$score = Score::make(
collect($chart['notes'])
->map(fn (array $note) => Note::make(
length: Note::len(ticks: $note['ticks'], bpm: $chart['bpm']),
lyric: $note['lyric'] ?? '',
key: $note['key'],
id: $note['id'] ?? null,
))
->all(),
);
$response = Voicevox::song($score, teacher: $chart['teacher'])
->generate(id: $chart['speaker']);
$response->storeAs('songs', 'sample.wav');
相关页面