diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5a974e6fb4..5e6dcfe71d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -135,7 +135,7 @@ VITE_DEFAULT_ENGINE_INFOS=`[ "uuid": "074fc39e-678b-4c13-8916-ffca8d505d1d", "name": "VOICEVOX Engine", "executionEnabled": true, - "executionFilePath": "vv-engine/run.exe", + "executionFilePath": "C:/Users/(ユーザー名)/AppData/Local/Programs/VOICEVOX/vv-engine/run.exe", "executionArgs": [], "host": "http://127.0.0.1:50021" } diff --git a/README.md b/README.md index 30518730d1..ab3172a3fd 100644 --- a/README.md +++ b/README.md @@ -49,21 +49,26 @@ npm ci ## 実行 -`.env.production`をコピーして`.env`を作成し、`VITE_DEFAULT_ENGINE_INFOS`内の`executionFilePath`に`voicevox_engine`のパスを指定します。 +### エンジンの準備 -[製品版 VOICEVOX](https://voicevox.hiroshiba.jp/) のディレクトリのパスを指定すれば動きます。 +`.env.production`をコピーして`.env`を作成し、`VITE_DEFAULT_ENGINE_INFOS`内の`executionFilePath`に +[製品版 VOICEVOX](https://voicevox.hiroshiba.jp/) 内の`vv-engine/run.exe`を指定すれば動きます。 -Windows の場合でもパスの区切り文字は`\`ではなく`/`なのでご注意ください。 +Windows でインストール先を変更していない場合は`C:/Users/(ユーザー名)/AppData/Local/Programs/VOICEVOX/vv-engine/run.exe`を指定してください。 +パスの区切り文字は`\`ではなく`/`なのでご注意ください。 -また、macOS 向けの`VOICEVOX.app`を利用している場合は`/path/to/VOICEVOX.app/Contents/MacOS/vv-engine/run`を指定してください。 +macOS 向けの`VOICEVOX.app`を利用している場合は`/path/to/VOICEVOX.app/Contents/MacOS/vv-engine/run`を指定してください。 -Linux の場合は、[Releases](https://github.com/VOICEVOX/voicevox/releases/)から入手できる tar.gz 版に含まれる`run`コマンドを指定してください。 +Linux の場合は、[Releases](https://github.com/VOICEVOX/voicevox/releases/)から入手できる tar.gz 版に含まれる`vv-engine/run`コマンドを指定してください。 AppImage 版の場合は`$ /path/to/VOICEVOX.AppImage --appimage-mount`でファイルシステムをマウントできます。 -VOICEVOX エディタの実行とは別にエンジン API のサーバを立てている場合は`executionFilePath`を指定する必要はありません。 +VOICEVOX エディタの実行とは別にエンジン API のサーバを立てている場合は`executionFilePath`を指定する必要はありませんが、 +代わりに`executionEnabled`を`false`にしてください。 これは製品版 VOICEVOX を起動している場合もあてはまります。 -また、エンジン API の宛先エンドポイントを変更する場合は`VITE_DEFAULT_ENGINE_INFOS`内の`host`を変更してください。 +エンジン API の宛先エンドポイントを変更する場合は`VITE_DEFAULT_ENGINE_INFOS`内の`host`を変更してください。 + +### Electron の実行 ```bash # 開発しやすい環境で実行 diff --git a/package-lock.json b/package-lock.json index b3f784215b..23581591b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@gtm-support/vue-gtm": "1.2.3", "@quasar/extras": "1.10.10", - "@sevenc-nanashi/utaformatix-ts": "npm:@jsr/sevenc-nanashi__utaformatix-ts@0.3.2", + "@sevenc-nanashi/utaformatix-ts": "npm:@jsr/sevenc-nanashi__utaformatix-ts@0.4.0", "async-lock": "1.4.0", "colorjs.io": "0.5.2", "dayjs": "1.10.7", @@ -5198,10 +5198,11 @@ }, "node_modules/@sevenc-nanashi/utaformatix-ts": { "name": "@jsr/sevenc-nanashi__utaformatix-ts", - "version": "0.3.2", - "resolved": "https://npm.jsr.io/~/11/@jsr/sevenc-nanashi__utaformatix-ts/0.3.2.tgz", - "integrity": "sha512-TdCME7wLGNnv7vcTRXZ4IXZpv/7uVUqy1ACIxuZQNg2M9jYk3aA9CSRNHkrrTIAeH4JuDBpONfn9zb9GkXsscg==", + "version": "0.4.0", + "resolved": "https://npm.jsr.io/~/11/@jsr/sevenc-nanashi__utaformatix-ts/0.4.0.tgz", + "integrity": "sha512-/rI7ZOy51LjOP+OwWclbuJ6K7gRTs9s2DHGrxBgJ1ho4TeE9LpFgNyQReKwsznLAgleAvJWJbW3a2MMAoNHeGA==", "dependencies": { + "defu": "^6.1.4", "jszip": "^3.10.1", "utaformatix-data": "^1.1.0" } @@ -11213,8 +11214,7 @@ "node_modules/defu": { "version": "6.1.4", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", - "dev": true + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==" }, "node_modules/delayed-stream": { "version": "1.0.0", diff --git a/package.json b/package.json index 9755c59f5a..ad15746422 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "dependencies": { "@gtm-support/vue-gtm": "1.2.3", "@quasar/extras": "1.10.10", - "@sevenc-nanashi/utaformatix-ts": "npm:@jsr/sevenc-nanashi__utaformatix-ts@0.3.2", + "@sevenc-nanashi/utaformatix-ts": "npm:@jsr/sevenc-nanashi__utaformatix-ts@0.4.0", "async-lock": "1.4.0", "colorjs.io": "0.5.2", "dayjs": "1.10.7", diff --git a/src/components/Dialog/DictionaryManageDialog.vue b/src/components/Dialog/DictionaryManageDialog.vue index 682a61c8fa..b491b4de7b 100644 --- a/src/components/Dialog/DictionaryManageDialog.vue +++ b/src/components/Dialog/DictionaryManageDialog.vue @@ -90,7 +90,10 @@ dense round icon="edit" - @click.stop="editWord" + @click.stop=" + selectWord(key); + editWord(); + " > 編集 @@ -100,7 +103,10 @@ dense round icon="delete_outline" - @click.stop="deleteWord" + @click.stop=" + selectWord(key); + deleteWord(); + " > 削除 diff --git a/src/components/Sing/SideBar/TrackItem.vue b/src/components/Sing/SideBar/TrackItem.vue index eaeb3e0c32..79da221fb3 100644 --- a/src/components/Sing/SideBar/TrackItem.vue +++ b/src/components/Sing/SideBar/TrackItem.vue @@ -214,13 +214,16 @@ watchEffect(() => { const updateTrackName = () => { if (temporaryTrackName.value === track.value.name) return; + + // 空のトラック名だと空欄のようになってしまうので許容しない if (temporaryTrackName.value === "") { temporaryTrackName.value = track.value.name; return; } + store.dispatch("COMMAND_SET_TRACK_NAME", { trackId: props.trackId, - name: track.value.name, + name: temporaryTrackName.value, }); }; diff --git a/src/components/Sing/SingEditor.vue b/src/components/Sing/SingEditor.vue index 1d7432a579..69c28c5da9 100644 --- a/src/components/Sing/SingEditor.vue +++ b/src/components/Sing/SingEditor.vue @@ -124,6 +124,7 @@ onetimeWatch( await store.dispatch("SET_VOLUME", { volume: 0.6 }); await store.dispatch("SET_PLAYHEAD_POSITION", { position: 0 }); + await store.dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS"); isCompletedInitialStartup.value = true; return "unwatch"; diff --git a/src/store/command.ts b/src/store/command.ts index 8b64796edd..649e415823 100644 --- a/src/store/command.ts +++ b/src/store/command.ts @@ -111,6 +111,7 @@ export const commandStore = createPartialStore({ if (editor === "song") { // TODO: 存在しないノートのみ選択解除、あるいはSELECTED_NOTE_IDS getterを作る mutations.DESELECT_ALL_NOTES(); + actions.SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS(); actions.RENDER(); } }, @@ -129,6 +130,7 @@ export const commandStore = createPartialStore({ if (editor === "song") { // TODO: 存在しないノートのみ選択解除、あるいはSELECTED_NOTE_IDS getterを作る mutations.DESELECT_ALL_NOTES(); + actions.SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS(); actions.RENDER(); } }, diff --git a/src/store/singing.ts b/src/store/singing.ts index 187620a989..6ac6bf7443 100644 --- a/src/store/singing.ts +++ b/src/store/singing.ts @@ -21,6 +21,7 @@ import { SequencerEditTarget, PhraseSourceHash, Track, + SequenceId, } from "./type"; import { DEFAULT_PROJECT_NAME, sanitizeFileName } from "./utility"; import { @@ -40,7 +41,6 @@ import { Clipper, Limiter, NoteEvent, - NoteSequence, OfflineTransport, PolySynth, Sequence, @@ -250,7 +250,7 @@ if (window.AudioContext) { const playheadPosition = new FrequentlyUpdatedState(0); const singingVoices = new Map(); -const sequences = new Map(); +const sequences = new Map(); const animationTimer = new AnimationTimer(); const singingGuideCache = new Map(); @@ -258,6 +258,139 @@ const singingVoiceCache = new Map(); const initialTrackId = TrackId(crypto.randomUUID()); +/** + * シーケンスの音源の出力を取得する。 + * @param sequence シーケンス + * @returns シーケンスの音源の出力 + */ +const getOutputOfAudioSource = (sequence: Sequence) => { + if (sequence.type === "note") { + return sequence.instrument.output; + } else if (sequence.type === "audio") { + return sequence.audioPlayer.output; + } else { + throw new Error("Unknown type of sequence."); + } +}; + +/** + * シーケンスを登録する。 + * ChannelStripが存在する場合は、ChannelStripにシーケンスを接続する。 + * @param sequenceId シーケンスID + * @param sequence トラックIDを持つシーケンス + */ +const registerSequence = ( + sequenceId: SequenceId, + sequence: Sequence & { trackId: TrackId }, +) => { + if (transport == undefined) { + throw new Error("transport is undefined."); + } + if (sequences.has(sequenceId)) { + throw new Error("Sequence already exists."); + } + sequences.set(sequenceId, sequence); + + // Transportに追加する + transport.addSequence(sequence); + + // ChannelStripがある場合は接続する + const channelStrip = trackChannelStrips.get(sequence.trackId); + if (channelStrip != undefined) { + getOutputOfAudioSource(sequence).connect(channelStrip.input); + } +}; + +/** + * シーケンスを削除する。 + * ChannelStripが存在する場合は、ChannelStripとシーケンスの接続を解除する。 + * @param sequenceId シーケンスID + */ +const deleteSequence = (sequenceId: SequenceId) => { + if (transport == undefined) { + throw new Error("transport is undefined."); + } + const sequence = sequences.get(sequenceId); + if (sequence == undefined) { + throw new Error("Sequence does not exist."); + } + sequences.delete(sequenceId); + + // Transportから削除する + transport.removeSequence(sequence); + + // ChannelStripがある場合は接続を解除する + if (trackChannelStrips.has(sequence.trackId)) { + getOutputOfAudioSource(sequence).disconnect(); + } +}; + +/** + * `tracks`と`trackChannelStrips`を同期する。 + * シーケンスが存在する場合は、ChannelStripとシーケンスの接続・接続の解除を行う。 + * @param tracks `state`の`tracks` + * @param enableMultiTrack マルチトラックが有効かどうか + */ +const syncTracksAndTrackChannelStrips = ( + tracks: Map, + enableMultiTrack: boolean, +) => { + if (audioContext == undefined) { + throw new Error("audioContext is undefined."); + } + if (mainChannelStrip == undefined) { + throw new Error("mainChannelStrip is undefined."); + } + + const shouldPlays = shouldPlayTracks(tracks); + for (const [trackId, track] of tracks) { + if (!trackChannelStrips.has(trackId)) { + const channelStrip = new ChannelStrip(audioContext); + channelStrip.output.connect(mainChannelStrip.input); + trackChannelStrips.set(trackId, channelStrip); + + // シーケンスがある場合は、それらを接続する + for (const [sequenceId, sequence] of sequences) { + if (trackId === sequence.trackId) { + const sequence = sequences.get(sequenceId); + if (sequence == undefined) { + throw new Error("Sequence does not exist."); + } + getOutputOfAudioSource(sequence).connect(channelStrip.input); + } + } + } + + const channelStrip = getOrThrow(trackChannelStrips, trackId); + if (enableMultiTrack) { + channelStrip.volume = track.gain; + channelStrip.pan = track.pan; + channelStrip.mute = !shouldPlays.has(trackId); + } else { + channelStrip.volume = 1; + channelStrip.pan = 0; + channelStrip.mute = false; + } + } + for (const [trackId, channelStrip] of trackChannelStrips) { + if (!tracks.has(trackId)) { + channelStrip.output.disconnect(); + trackChannelStrips.delete(trackId); + + // シーケンスがある場合は、それらの接続を解除する + for (const [sequenceId, sequence] of sequences) { + if (trackId === sequence.trackId) { + const sequence = sequences.get(sequenceId); + if (sequence == undefined) { + throw new Error("Sequence does not exist."); + } + getOutputOfAudioSource(sequence).disconnect(); + } + } + } + } +}; + /** トラックを取得する。見付からないときはフォールバックとして最初のトラックを返す。 */ const getSelectedTrackWithFallback = (partialState: { tracks: Map; @@ -520,15 +653,13 @@ export const singingStore = createPartialStore({ state.timeSignatures = timeSignatures; }, async action( - { commit, dispatch }, + { commit }, { timeSignatures }: { timeSignatures: TimeSignature[] }, ) { if (!isValidTimeSignatures(timeSignatures)) { throw new Error("The time signatures are invalid."); } commit("SET_TIME_SIGNATURES", { timeSignatures }); - - dispatch("RENDER"); }, }, @@ -795,6 +926,23 @@ export const singingStore = createPartialStore({ }, }, + SET_SEQUENCE_ID_TO_PHRASE: { + mutation( + state, + { + phraseKey, + sequenceId, + }: { + phraseKey: PhraseSourceHash; + sequenceId: SequenceId | undefined; + }, + ) { + const phrase = getOrThrow(state.phrases, phraseKey); + + phrase.sequenceId = sequenceId; + }, + }, + SET_SINGING_GUIDE: { mutation( state, @@ -1070,6 +1218,7 @@ export const singingStore = createPartialStore({ } commit("INSERT_TRACK", { trackId, track, prevTrackId }); + dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS"); dispatch("RENDER"); }, }, @@ -1085,6 +1234,7 @@ export const singingStore = createPartialStore({ } commit("DELETE_TRACK", { trackId }); + dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS"); dispatch("RENDER"); }, }, @@ -1117,6 +1267,7 @@ export const singingStore = createPartialStore({ commit("SET_TRACK", { trackId, track }); + dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS"); dispatch("RENDER"); }, }, @@ -1132,10 +1283,20 @@ export const singingStore = createPartialStore({ } commit("SET_TRACKS", { tracks }); + dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS"); dispatch("RENDER"); }, }, + SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS: { + async action({ state }) { + syncTracksAndTrackChannelStrips( + state.tracks, + state.experimentalSetting.enableMultiTrack, + ); + }, + }, + /** * レンダリングを行う。レンダリング中だった場合は停止して再レンダリングする。 */ @@ -1451,32 +1612,24 @@ export const singingStore = createPartialStore({ } }; - const getAudioSourceNode = (sequence: Sequence) => { - if (sequence.type === "note") { - return sequence.instrument.output; - } else if (sequence.type === "audio") { - return sequence.audioPlayer.output; - } else { - throw new Error("Unknown type of sequence."); - } - }; - // NOTE: 型推論でawaitの前か後かが考慮されないので、関数を介して取得する(型がbooleanになるようにする) const startRenderingRequested = () => state.startRenderingRequested; const stopRenderingRequested = () => state.stopRenderingRequested; + /** + * フレーズが持つシーケンスのIDを取得する。 + * @param phraseKey フレーズのキー + * @returns シーケンスID + */ + const getPhraseSequenceId = (phraseKey: PhraseSourceHash) => { + return getOrThrow(state.phrases, phraseKey).sequenceId; + }; + const render = async () => { if (!audioContext) { throw new Error("audioContext is undefined."); } - if (!transport) { - throw new Error("transport is undefined."); - } - if (!mainChannelStrip) { - throw new Error("channelStrip is undefined."); - } const audioContextRef = audioContext; - const transportRef = transport; // レンダリング中に変更される可能性のあるデータをコピーする const tracks = cloneWithUnwrapProxy(state.tracks); @@ -1488,40 +1641,6 @@ export const singingStore = createPartialStore({ ]), ); - // trackChannelStripsを同期する。 - // ここで更新されたChannelStripに既存のAudioPlayerなどを繋げる必要がある。 - // そのため、Phraseが変わっていなくてもPhraseの更新=AudioPlayerなどの再接続は毎回行う必要がある。 - // trackChannelStripsを同期した後、フレーズの更新が完了するまではreturnやthrowをしないこと。 - // TODO: 良い設計を考える - // ref: https://github.com/VOICEVOX/voicevox/pull/2176#discussion_r1693991784 - - const shouldPlays = shouldPlayTracks(tracks); - for (const [trackId, track] of tracks) { - if (!trackChannelStrips.has(trackId)) { - const channelStrip = new ChannelStrip(audioContext); - channelStrip.output.connect(mainChannelStrip.input); - trackChannelStrips.set(trackId, channelStrip); - } - - const channelStrip = getOrThrow(trackChannelStrips, trackId); - channelStrip.volume = state.experimentalSetting.enableMultiTrack - ? track.gain - : 1; - channelStrip.pan = state.experimentalSetting.enableMultiTrack - ? track.pan - : 0; - channelStrip.mute = state.experimentalSetting.enableMultiTrack - ? !shouldPlays.has(trackId) - : false; - } - for (const trackId of trackChannelStrips.keys()) { - if (!tracks.has(trackId)) { - const channelStrip = getOrThrow(trackChannelStrips, trackId); - channelStrip.output.disconnect(); - trackChannelStrips.delete(trackId); - } - } - const singerAndFrameRates = new Map( [...tracks].map(([trackId, track]) => [ trackId, @@ -1657,13 +1776,15 @@ export const singingStore = createPartialStore({ } } - // 無くなったフレーズの音源とシーケンスの接続を解除して削除する + // 無くなったフレーズのシーケンスを削除する for (const phraseKey of disappearedPhraseKeys) { - const sequence = sequences.get(phraseKey); - if (sequence) { - getAudioSourceNode(sequence).disconnect(); - transportRef.removeSequence(sequence); - sequences.delete(phraseKey); + const phraseSequenceId = getPhraseSequenceId(phraseKey); + if (phraseSequenceId != undefined) { + deleteSequence(phraseSequenceId); + commit("SET_SEQUENCE_ID_TO_PHRASE", { + phraseKey, + sequenceId: undefined, + }); } } @@ -1705,31 +1826,30 @@ export const singingStore = createPartialStore({ }), ); for (const [phraseKey, phrase] of phrasesToBeRendered) { - // シーケンスが存在する場合、シーケンスの接続を解除して削除する + // シーケンスが存在する場合は、シーケンスを削除する // TODO: ピッチを編集したときは行わないようにする - const sequence = sequences.get(phraseKey); - if (sequence) { - getAudioSourceNode(sequence).disconnect(); - transportRef.removeSequence(sequence); - sequences.delete(phraseKey); + const phraseSequenceId = getPhraseSequenceId(phraseKey); + if (phraseSequenceId != undefined) { + deleteSequence(phraseSequenceId); + commit("SET_SEQUENCE_ID_TO_PHRASE", { + phraseKey, + sequenceId: undefined, + }); } - // シーケンスが存在しない場合、ノートシーケンスを作成してプレビュー音が鳴るようにする + // ノートシーケンスを作成して登録し、プレビュー音が鳴るようにする - if (!sequences.has(phraseKey)) { - const noteEvents = generateNoteEvents(phrase.notes, tempos, tpqn); - const polySynth = new PolySynth(audioContextRef); - const noteSequence: NoteSequence = { - type: "note", - instrument: polySynth, - noteEvents, - }; - const channelStrip = getOrThrow(trackChannelStrips, phrase.trackId); - polySynth.output.connect(channelStrip.input); - transportRef.addSequence(noteSequence); - sequences.set(phraseKey, noteSequence); - } + const noteEvents = generateNoteEvents(phrase.notes, tempos, tpqn); + const polySynth = new PolySynth(audioContextRef); + const sequenceId = SequenceId(uuid4()); + registerSequence(sequenceId, { + type: "note", + instrument: polySynth, + noteEvents, + trackId: phrase.trackId, + }); + commit("SET_SEQUENCE_ID_TO_PHRASE", { phraseKey, sequenceId }); } while (phrasesToBeRendered.size > 0) { if (startRenderingRequested() || stopRenderingRequested()) { @@ -1908,16 +2028,18 @@ export const singingStore = createPartialStore({ singingVoiceKey, }); - // シーケンスが存在する場合、シーケンスの接続を解除して削除する + // シーケンスが存在する場合、シーケンスを削除する - const sequence = sequences.get(phraseKey); - if (sequence) { - getAudioSourceNode(sequence).disconnect(); - transportRef.removeSequence(sequence); - sequences.delete(phraseKey); + const phraseSequenceId = getPhraseSequenceId(phraseKey); + if (phraseSequenceId != undefined) { + deleteSequence(phraseSequenceId); + commit("SET_SEQUENCE_ID_TO_PHRASE", { + phraseKey, + sequenceId: undefined, + }); } - // オーディオシーケンスを作成して接続する + // オーディオシーケンスを作成して登録する const audioEvents = await generateAudioEvents( audioContextRef, @@ -1925,15 +2047,14 @@ export const singingStore = createPartialStore({ singingVoice.blob, ); const audioPlayer = new AudioPlayer(audioContext); - const audioSequence: AudioSequence = { + const sequenceId = SequenceId(uuid4()); + registerSequence(sequenceId, { type: "audio", audioPlayer, audioEvents, - }; - const channelStrip = getOrThrow(trackChannelStrips, phrase.trackId); - audioPlayer.output.connect(channelStrip.input); - transportRef.addSequence(audioSequence); - sequences.set(phraseKey, audioSequence); + trackId: phrase.trackId, + }); + commit("SET_SEQUENCE_ID_TO_PHRASE", { phraseKey, sequenceId }); commit("SET_STATE_TO_PHRASE", { phraseKey, @@ -2301,7 +2422,7 @@ export const singingStore = createPartialStore({ action({ commit, dispatch }, { trackId, mute }) { commit("SET_TRACK_MUTE", { trackId, mute }); - dispatch("RENDER"); + dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS"); }, }, @@ -2313,7 +2434,7 @@ export const singingStore = createPartialStore({ action({ commit, dispatch }, { trackId, solo }) { commit("SET_TRACK_SOLO", { trackId, solo }); - dispatch("RENDER"); + dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS"); }, }, @@ -2325,7 +2446,7 @@ export const singingStore = createPartialStore({ action({ commit, dispatch }, { trackId, gain }) { commit("SET_TRACK_GAIN", { trackId, gain }); - dispatch("RENDER"); + dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS"); }, }, @@ -2337,7 +2458,7 @@ export const singingStore = createPartialStore({ action({ commit, dispatch }, { trackId, pan }) { commit("SET_TRACK_PAN", { trackId, pan }); - dispatch("RENDER"); + dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS"); }, }, @@ -2365,8 +2486,11 @@ export const singingStore = createPartialStore({ track.solo = false; } }, - action({ commit }) { + action({ commit, dispatch }) { commit("UNSOLO_ALL_TRACKS"); + + dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS"); + dispatch("RENDER"); }, }, @@ -2658,6 +2782,9 @@ export const singingCommandStore = transformCommandStore( track: cloneWithUnwrapProxy(track), prevTrackId, }); + + dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS"); + dispatch("RENDER"); }, }, @@ -2668,6 +2795,7 @@ export const singingCommandStore = transformCommandStore( action({ commit, dispatch }, { trackId }) { commit("COMMAND_DELETE_TRACK", { trackId }); + dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS"); dispatch("RENDER"); }, }, @@ -2688,7 +2816,7 @@ export const singingCommandStore = transformCommandStore( action({ commit, dispatch }, { trackId, mute }) { commit("COMMAND_SET_TRACK_MUTE", { trackId, mute }); - dispatch("RENDER"); + dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS"); }, }, @@ -2699,7 +2827,7 @@ export const singingCommandStore = transformCommandStore( action({ commit, dispatch }, { trackId, solo }) { commit("COMMAND_SET_TRACK_SOLO", { trackId, solo }); - dispatch("RENDER"); + dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS"); }, }, @@ -2710,7 +2838,7 @@ export const singingCommandStore = transformCommandStore( action({ commit, dispatch }, { trackId, gain }) { commit("COMMAND_SET_TRACK_GAIN", { trackId, gain }); - dispatch("RENDER"); + dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS"); }, }, @@ -2721,7 +2849,7 @@ export const singingCommandStore = transformCommandStore( action({ commit, dispatch }, { trackId, pan }) { commit("COMMAND_SET_TRACK_PAN", { trackId, pan }); - dispatch("RENDER"); + dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS"); }, }, @@ -2738,8 +2866,11 @@ export const singingCommandStore = transformCommandStore( mutation(draft) { singingStore.mutations.UNSOLO_ALL_TRACKS(draft, undefined); }, - action({ commit }) { + action({ commit, dispatch }) { commit("COMMAND_UNSOLO_ALL_TRACKS"); + + dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS"); + dispatch("RENDER"); }, }, @@ -2807,6 +2938,7 @@ export const singingCommandStore = transformCommandStore( tracks: payload, }); + dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS"); dispatch("RENDER"); }, }, @@ -2857,6 +2989,7 @@ export const singingCommandStore = transformCommandStore( tracks: filteredTracks, }); + dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS"); dispatch("RENDER"); }, ), @@ -2896,6 +3029,7 @@ export const singingCommandStore = transformCommandStore( tracks: filteredTracks, }); + dispatch("SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS"); dispatch("RENDER"); }, ), diff --git a/src/store/type.ts b/src/store/type.ts index 2f1b0ed2d9..6656aded76 100644 --- a/src/store/type.ts +++ b/src/store/type.ts @@ -798,6 +798,11 @@ export type SingingVoiceSourceHash = z.infer< typeof singingVoiceSourceHashSchema >; +export const sequenceIdSchema = z.string().brand<"SequenceId">(); +export type SequenceId = z.infer; +export const SequenceId = (id: string): SequenceId => + sequenceIdSchema.parse(id); + /** * フレーズ(レンダリング区間) */ @@ -808,6 +813,7 @@ export type Phrase = { state: PhraseState; singingGuideKey?: SingingGuideSourceHash; singingVoiceKey?: SingingVoiceSourceHash; + sequenceId?: SequenceId; }; /** @@ -1007,6 +1013,13 @@ export type SingingStoreTypes = { }; }; + SET_SEQUENCE_ID_TO_PHRASE: { + mutation: { + phraseKey: PhraseSourceHash; + sequenceId: SequenceId | undefined; + }; + }; + SET_SINGING_GUIDE: { mutation: { singingGuideKey: SingingGuideSourceHash; @@ -1246,6 +1259,10 @@ export type SingingStoreTypes = { CALC_RENDER_DURATION: { getter: number; }; + + SYNC_TRACKS_AND_TRACK_CHANNEL_STRIPS: { + action(): void; + }; }; export type SingingCommandStoreState = {