メインコンテンツへスキップ

目的

VOICEVOXエディターの .vvproj をそのまま活用して、Laravel でトーク・ソングを再生成できるようにします。

トップレベル構造

.vvproj は UTF-8 JSON です。トークとソングを同じファイルに保存します。
{
  "appVersion": "0.25.2",
  "talk": {
    "audioKeys": [],
    "audioItems": {}
  },
  "song": {
    "tpqn": 480,
    "tempos": [],
    "timeSignatures": [],
    "tracks": {},
    "trackOrder": []
  }
}
キー内容
appVersion保存時のエディターバージョン
talkトーク用データ (audioKeys + audioItems)
songソング用データ (テンポ・拍子・トラック群)

talk セクション

talk.audioKeys は順序配列、talk.audioItems は ID をキーにした Record です。
{
  "audioKeys": ["audio-item-uuid"],
  "audioItems": {
    "audio-item-uuid": {
      "text": "ずんだもんなのだ",
      "voice": {
        "engineId": "engine-uuid",
        "speakerId": "speaker-uuid",
        "styleId": 3
      },
      "query": {
        "accentPhrases": [],
        "speedScale": 1,
        "pitchScale": 0
      },
      "presetKey": "preset-uuid"
    }
  }
}

TalkAudioItem

キー内容
text入力テキスト
voice.engineIdエンジン ID (/engine_manifest と対応)
voice.speakerId話者 UUID
voice.styleIdスタイル ID。/synthesis?speaker={styleId} にそのまま渡す
queryAudioQuery 相当
presetKeyエディターのプリセット ID。Laravel 側プリセットは プリセット を参照
.vvprojquery が保存済みなら、/audio_query を再実行せずにそのまま合成へ進めます。

accentPhrases

キー内容
morasモーラ配列 (consonant 系は省略される場合あり)
accentアクセント位置 (1始まり)
pauseMora句読点などで無音を入れるときに付くことがある
isInterrogative疑問文フラグ

song セクション

{
  "tpqn": 480,
  "tempos": [{ "position": 0, "bpm": 120 }],
  "timeSignatures": [{ "measureNumber": 1, "beats": 4, "beatType": 4 }],
  "tracks": {
    "track-uuid": {
      "name": "無名トラック",
      "singer": { "engineId": "engine-uuid", "styleId": 3003 },
      "notes": []
    }
  },
  "trackOrder": ["track-uuid"]
}
キー内容
tpqnTicks Per Quarter Note。標準は 480
temposテンポマップ (position は tick)
timeSignatures拍子マップ (measureNumber は 1 始まり)
tracksTrack ID をキーにした Record
trackOrder表示・再生順。tracks のキー集合と一致させる

Track

キー内容
singer.styleId最終的に /frame_synthesis?speaker={styleId} へ渡す ID
notesノート配列
keyRangeAdjustment半音単位のキー調整
volumeRangeAdjustment音量調整
pitchEditData / volumeEditDataフレーム単位の編集データ
phonemeTimingEditDataNote ID ごとの音素タイミング編集
solo / mute再生対象トラック判定に使う

Note

キー内容
idノート ID (一意)
positionノート開始 tick
durationノート長 tick
noteNumberMIDI ノート番号
lyric歌詞

tick・秒・フレーム変換

単一テンポでは次で変換します。
seconds = ticks / tpqn * 60 / bpm
frames = round(seconds * frameRate)
テンポ変更ありでは、temposposition 昇順で区間積算します。
function ticksToSeconds(int $targetTick, int $tpqn, array $tempos): float
{
    $seconds = 0.0;
    $currentTick = 0;

    foreach ($tempos as $index => $tempo) {
        $nextTick = $tempos[$index + 1]['position'] ?? $targetTick;
        $segmentEnd = min($targetTick, $nextTick);

        if ($segmentEnd <= $currentTick) {
            break;
        }

        $bpm = $tempo['bpm'];
        $seconds += (($segmentEnd - $currentTick) / $tpqn) * (60 / $bpm);
        $currentTick = $segmentEnd;
    }

    return $seconds;
}
Note::len() ヘルパーで tick からフレーム長へ変換する実装は Score と Note 詳細 を参照してください。

ソング音声生成の流れ

直接編集時の注意点

  • tracks のキー集合と trackOrder を必ず一致させる
  • talk.audioKeystalk.audioItems も同様に同期する
  • position >= 0duration >= 1noteNumber0..127 を維持する
  • tempos[0].position は通常 0timeSignatures[0].measureNumber は通常 1
  • 未知キーは可能なら保持して再保存し、将来バージョンとの互換性を壊さない

Laravel コード例

.vvproj を読み込み、トークとソングを生成する最小例です。
use Illuminate\Support\Facades\Storage;
use Revolution\Voicevox\Client\TalkAudioQuery;
use Revolution\Voicevox\Song\Note;
use Revolution\Voicevox\Song\Score;
use Revolution\Voicevox\Voicevox;

$project = json_decode(
    Storage::disk('local')->get('voicevox/sample.vvproj'),
    true,
    flags: JSON_THROW_ON_ERROR,
);

// Talk: 保存済み query をそのまま合成
foreach ($project['talk']['audioKeys'] as $audioKey) {
    $item = $project['talk']['audioItems'][$audioKey];

    Voicevox::talk($item['text'], id: $item['voice']['styleId'])
        ->tap(fn (TalkAudioQuery $talk) => $talk->audioQuery = array_replace($talk->audioQuery, $item['query']))
        ->generate(id: $item['voice']['styleId'])
        ->storeAs('vvproj/talk', "{$audioKey}.wav");
}

// Song: ここでは先頭トラックの duration を frame_length に変換して生成
$trackId = $project['song']['trackOrder'][0];
$track = $project['song']['tracks'][$trackId];
$bpm = $project['song']['tempos'][0]['bpm'] ?? 120;

$score = Score::make([
    Note::make(length: 15, lyric: '', key: null),
    ...collect($track['notes'])->map(
        fn (array $note) => Note::make(
            length: Note::len(ticks: $note['duration'], bpm: $bpm),
            lyric: $note['lyric'] ?? 'ら',
            key: $note['noteNumber'],
            id: $note['id'] ?? null,
        ),
    )->all(),
    Note::make(length: 2, lyric: '', key: null),
]);

Voicevox::song($score, teacher: 6000)
    ->generate(id: $track['singer']['styleId'])
    ->storeAs('vvproj/song', "{$trackId}.wav");
最終更新日 2026年5月28日