From 3821b6be668252d8ca61dc84bd5480498f639658 Mon Sep 17 00:00:00 2001 From: Sig <62321214+sigprogramming@users.noreply.github.com> Date: Sun, 22 Dec 2024 09:31:19 +0900 Subject: [PATCH] =?UTF-8?q?=E3=83=AC=E3=83=B3=E3=83=80=E3=83=AA=E3=83=B3?= =?UTF-8?q?=E3=82=B0=E5=87=A6=E7=90=86=E3=82=92=E3=83=AA=E3=83=95=E3=82=A1?= =?UTF-8?q?=E3=82=AF=E3=82=BF=E3=83=AA=E3=83=B3=E3=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/store/singing.ts | 490 ++++++++++++++++++++++--------------------- 1 file changed, 250 insertions(+), 240 deletions(-) diff --git a/src/store/singing.ts b/src/store/singing.ts index 7c0809621a..71e72067de 100644 --- a/src/store/singing.ts +++ b/src/store/singing.ts @@ -121,15 +121,6 @@ type SnapshotForPhraseRender = Readonly<{ editorFrameRate: number; }>; -/** - * フレーズレンダリングのコンテキスト - */ -type PhraseRenderContext = Readonly<{ - snapshot: SnapshotForPhraseRender; - trackId: TrackId; - phraseKey: PhraseKey; -}>; - type PhraseRenderStageId = | "queryGeneration" | "singingPitchGeneration" @@ -148,19 +139,27 @@ type PhraseRenderStage = Readonly<{ * @param context コンテキスト * @returns 実行が必要かどうかのブール値 */ - shouldBeExecuted: (context: PhraseRenderContext) => Promise; + needsExecution: ( + trackId: TrackId, + phraseKey: PhraseKey, + snapshot: SnapshotForPhraseRender, + ) => Promise; /** * 前回の処理結果を削除する。 * @param context コンテキスト */ - deleteExecutionResult: (context: PhraseRenderContext) => void; + deleteExecutionResult: (phraseKey: PhraseKey) => void; /** * ステージの処理を実行する。 * @param context コンテキスト */ - execute: (context: PhraseRenderContext) => Promise; + execute: ( + trackId: TrackId, + phraseKey: PhraseKey, + snapshot: SnapshotForPhraseRender, + ) => Promise; }>; /** @@ -213,51 +212,6 @@ type SingingVoiceSource = Readonly<{ queryForSingingVoiceSynthesis: EditorFrameAudioQuery; }>; -/** - * フレーズレンダラー。 - * 各フレーズごとに、ステージを進めながらレンダリング処理を行う。 - * レンダリングが必要かどうかの判定やキャッシュの作成も行う。 - */ -type PhraseRenderer = Readonly<{ - /** - * 一番最初のステージのIDを返す。 - * 一度もレンダリングを行っていないフレーズは、 - * この(一番最初の)ステージからレンダリング処理を開始する必要がある。 - * @returns ステージID - */ - getFirstRenderStageId: () => PhraseRenderStageId; - - /** - * レンダリングが必要なフレーズかどうかを判断し、 - * レンダリングが必要であればどのステージから開始されるべきかを判断して、そのステージのIDを返す。 - * レンダリングが必要ない場合、undefinedが返される。 - * @param snapshot スナップショット - * @param trackId トラックID - * @param phraseKey フレーズキー - * @returns ステージID または undefined - */ - determineStartStage: ( - snapshot: SnapshotForPhraseRender, - trackId: TrackId, - phraseKey: PhraseKey, - ) => Promise; - - /** - * 指定されたフレーズのレンダリング処理を、指定されたステージから開始する。 - * レンダリング処理を開始する前に、前回のレンダリング処理結果の削除が行われる。 - * @param snapshot スナップショット - * @param trackId トラックID - * @param phraseKey フレーズキー - * @param startStageId 開始ステージID - */ - render: ( - snapshot: SnapshotForPhraseRender, - trackId: TrackId, - phraseKey: PhraseKey, - startStageId: PhraseRenderStageId, - ) => Promise; -}>; - /** * リクエスト用のノーツ(と休符)を作成する。 */ @@ -1743,6 +1697,32 @@ export const singingStore = createPartialStore({ const lastRestDurationSeconds = 0.5; // TODO: 設定できるようにする const fadeOutDurationSeconds = 0.15; // TODO: 設定できるようにする + /** + * レンダリング中に変更される可能性のあるデータのコピーを作成する。 + */ + const createSnapshot = () => { + return { + tpqn: state.tpqn, + tempos: cloneWithUnwrapProxy(state.tempos), + tracks: cloneWithUnwrapProxy(state.tracks), + trackOverlappingNoteIds: new Map( + [...state.tracks.keys()].map((trackId) => [ + trackId, + getters.OVERLAPPING_NOTE_IDS(trackId), + ]), + ), + engineFrameRates: new Map( + Object.entries(state.engineManifests).map( + ([engineId, engineManifest]) => [ + engineId as EngineId, + engineManifest.frameRate, + ], + ), + ), + editorFrameRate: state.editorFrameRate, + } as const; + }; + const calcPhraseFirstRestDuration = ( prevPhraseLastNote: Note | undefined, phraseFirstNote: Note, @@ -1803,14 +1783,14 @@ export const singingStore = createPartialStore({ ); }; - const searchPhrases = async ( + const generatePhrases = async ( notes: Note[], tempos: Tempo[], tpqn: number, phraseFirstRestMinDurationSeconds: number, trackId: TrackId, ) => { - const foundPhrases = new Map(); + const generatePhrases = new Map(); let phraseNotes: Note[] = []; let prevPhraseLastNote: Note | undefined = undefined; @@ -1847,7 +1827,7 @@ export const singingStore = createPartialStore({ startTime: phraseStartTime, trackId, }); - foundPhrases.set(phraseKey, { + generatePhrases.set(phraseKey, { firstRestDuration: phraseFirstRestDuration, notes: phraseNotes, startTime: phraseStartTime, @@ -1861,26 +1841,28 @@ export const singingStore = createPartialStore({ } } } - return foundPhrases; + return generatePhrases; }; const generateQuerySource = ( - context: PhraseRenderContext, + trackId: TrackId, + phraseKey: PhraseKey, + snapshot: SnapshotForPhraseRender, ): QuerySource => { - const track = getOrThrow(context.snapshot.tracks, context.trackId); + const track = getOrThrow(snapshot.tracks, trackId); if (track.singer == undefined) { throw new Error("track.singer is undefined."); } const engineFrameRate = getOrThrow( - context.snapshot.engineFrameRates, + snapshot.engineFrameRates, track.singer.engineId, ); - const phrase = getOrThrow(state.phrases, context.phraseKey); + const phrase = getOrThrow(state.phrases, phraseKey); return { engineId: track.singer.engineId, engineFrameRate, - tpqn: context.snapshot.tpqn, - tempos: context.snapshot.tempos, + tpqn: snapshot.tpqn, + tempos: snapshot.tempos, firstRestDuration: phrase.firstRestDuration, notes: phrase.notes, keyRangeAdjustment: track.keyRangeAdjustment, @@ -1949,30 +1931,38 @@ export const singingStore = createPartialStore({ const queryGenerationStage: PhraseRenderStage = { id: "queryGeneration", - shouldBeExecuted: async (context: PhraseRenderContext) => { - const track = getOrThrow(context.snapshot.tracks, context.trackId); + needsExecution: async ( + trackId: TrackId, + phraseKey: PhraseKey, + snapshot: SnapshotForPhraseRender, + ) => { + const track = getOrThrow(snapshot.tracks, trackId); if (track.singer == undefined) { return false; } - const phrase = getOrThrow(state.phrases, context.phraseKey); + const phrase = getOrThrow(state.phrases, phraseKey); const phraseQueryKey = phrase.queryKey; - const querySource = generateQuerySource(context); + const querySource = generateQuerySource(trackId, phraseKey, snapshot); const queryKey = await calculateQueryKey(querySource); return phraseQueryKey == undefined || phraseQueryKey !== queryKey; }, - deleteExecutionResult: (context: PhraseRenderContext) => { - const phrase = getOrThrow(state.phrases, context.phraseKey); + deleteExecutionResult: (phraseKey: PhraseKey) => { + const phrase = getOrThrow(state.phrases, phraseKey); const phraseQueryKey = phrase.queryKey; if (phraseQueryKey != undefined) { mutations.DELETE_PHRASE_QUERY({ queryKey: phraseQueryKey }); mutations.SET_QUERY_KEY_TO_PHRASE({ - phraseKey: context.phraseKey, + phraseKey, queryKey: undefined, }); } }, - execute: async (context: PhraseRenderContext) => { - const querySource = generateQuerySource(context); + execute: async ( + trackId: TrackId, + phraseKey: PhraseKey, + snapshot: SnapshotForPhraseRender, + ) => { + const querySource = generateQuerySource(trackId, phraseKey, snapshot); const queryKey = await calculateQueryKey(querySource); let query = queryCache.get(queryKey); @@ -1985,27 +1975,26 @@ export const singingStore = createPartialStore({ queryCache.set(queryKey, query); } - const phrase = getOrThrow(state.phrases, context.phraseKey); + const phrase = getOrThrow(state.phrases, phraseKey); const phraseQueryKey = phrase.queryKey; if (phraseQueryKey != undefined) { throw new Error("The previous query has not been removed."); } mutations.SET_PHRASE_QUERY({ queryKey, query }); - mutations.SET_QUERY_KEY_TO_PHRASE({ - phraseKey: context.phraseKey, - queryKey, - }); + mutations.SET_QUERY_KEY_TO_PHRASE({ phraseKey, queryKey }); }, }; const generateSingingPitchSource = ( - context: PhraseRenderContext, + trackId: TrackId, + phraseKey: PhraseKey, + snapshot: SnapshotForPhraseRender, ): SingingPitchSource => { - const track = getOrThrow(context.snapshot.tracks, context.trackId); + const track = getOrThrow(snapshot.tracks, trackId); if (track.singer == undefined) { throw new Error("track.singer is undefined."); } - const phrase = getOrThrow(state.phrases, context.phraseKey); + const phrase = getOrThrow(state.phrases, phraseKey); const phraseQueryKey = phrase.queryKey; if (phraseQueryKey == undefined) { throw new Error("phraseQueryKey is undefined."); @@ -2016,8 +2005,8 @@ export const singingStore = createPartialStore({ return { engineId: track.singer.engineId, engineFrameRate: query.frameRate, - tpqn: context.snapshot.tpqn, - tempos: context.snapshot.tempos, + tpqn: snapshot.tpqn, + tempos: snapshot.tempos, firstRestDuration: phrase.firstRestDuration, notes: phrase.notes, keyRangeAdjustment: track.keyRangeAdjustment, @@ -2034,14 +2023,22 @@ export const singingStore = createPartialStore({ const singingPitchGenerationStage: PhraseRenderStage = { id: "singingPitchGeneration", - shouldBeExecuted: async (context: PhraseRenderContext) => { - const track = getOrThrow(context.snapshot.tracks, context.trackId); + needsExecution: async ( + trackId: TrackId, + phraseKey: PhraseKey, + snapshot: SnapshotForPhraseRender, + ) => { + const track = getOrThrow(snapshot.tracks, trackId); if (track.singer == undefined) { return false; } - const phrase = getOrThrow(state.phrases, context.phraseKey); + const phrase = getOrThrow(state.phrases, phraseKey); const phraseSingingPitchKey = phrase.singingPitchKey; - const singingPitchSource = generateSingingPitchSource(context); + const singingPitchSource = generateSingingPitchSource( + trackId, + phraseKey, + snapshot, + ); const singingPitchKey = await calculateSingingPitchKey(singingPitchSource); return ( @@ -2049,21 +2046,29 @@ export const singingStore = createPartialStore({ phraseSingingPitchKey !== singingPitchKey ); }, - deleteExecutionResult: (context: PhraseRenderContext) => { - const phrase = getOrThrow(state.phrases, context.phraseKey); + deleteExecutionResult: (phraseKey: PhraseKey) => { + const phrase = getOrThrow(state.phrases, phraseKey); const phraseSingingPitchKey = phrase.singingPitchKey; if (phraseSingingPitchKey != undefined) { mutations.DELETE_PHRASE_SINGING_PITCH({ singingPitchKey: phraseSingingPitchKey, }); mutations.SET_SINGING_PITCH_KEY_TO_PHRASE({ - phraseKey: context.phraseKey, + phraseKey, singingPitchKey: undefined, }); } }, - execute: async (context: PhraseRenderContext) => { - const singingPitchSource = generateSingingPitchSource(context); + execute: async ( + trackId: TrackId, + phraseKey: PhraseKey, + snapshot: SnapshotForPhraseRender, + ) => { + const singingPitchSource = generateSingingPitchSource( + trackId, + phraseKey, + snapshot, + ); const singingPitchKey = await calculateSingingPitchKey(singingPitchSource); @@ -2076,27 +2081,29 @@ export const singingStore = createPartialStore({ singingPitchCache.set(singingPitchKey, singingPitch); } - const phrase = getOrThrow(state.phrases, context.phraseKey); + const phrase = getOrThrow(state.phrases, phraseKey); const phraseSingingPitchKey = phrase.singingPitchKey; if (phraseSingingPitchKey != undefined) { throw new Error("The previous singing pitch has not been removed."); } mutations.SET_PHRASE_SINGING_PITCH({ singingPitchKey, singingPitch }); mutations.SET_SINGING_PITCH_KEY_TO_PHRASE({ - phraseKey: context.phraseKey, + phraseKey, singingPitchKey, }); }, }; const generateSingingVolumeSource = ( - context: PhraseRenderContext, + trackId: TrackId, + phraseKey: PhraseKey, + snapshot: SnapshotForPhraseRender, ): SingingVolumeSource => { - const track = getOrThrow(context.snapshot.tracks, context.trackId); + const track = getOrThrow(snapshot.tracks, trackId); if (track.singer == undefined) { throw new Error("track.singer is undefined."); } - const phrase = getOrThrow(state.phrases, context.phraseKey); + const phrase = getOrThrow(state.phrases, phraseKey); const phraseQueryKey = phrase.queryKey; if (phraseQueryKey == undefined) { throw new Error("phraseQueryKey is undefined."); @@ -2108,13 +2115,13 @@ export const singingStore = createPartialStore({ clonedQuery, phrase.startTime, track.pitchEditData, - context.snapshot.editorFrameRate, + snapshot.editorFrameRate, ); return { engineId: track.singer.engineId, engineFrameRate: query.frameRate, - tpqn: context.snapshot.tpqn, - tempos: context.snapshot.tempos, + tpqn: snapshot.tpqn, + tempos: snapshot.tempos, firstRestDuration: phrase.firstRestDuration, notes: phrase.notes, keyRangeAdjustment: track.keyRangeAdjustment, @@ -2165,36 +2172,52 @@ export const singingStore = createPartialStore({ const singingVolumeGenerationStage: PhraseRenderStage = { id: "singingVolumeGeneration", - shouldBeExecuted: async (context: PhraseRenderContext) => { - const track = getOrThrow(context.snapshot.tracks, context.trackId); + needsExecution: async ( + trackId: TrackId, + phraseKey: PhraseKey, + snapshot: SnapshotForPhraseRender, + ) => { + const track = getOrThrow(snapshot.tracks, trackId); if (track.singer == undefined) { return false; } - const singingVolumeSource = generateSingingVolumeSource(context); + const singingVolumeSource = generateSingingVolumeSource( + trackId, + phraseKey, + snapshot, + ); const singingVolumeKey = await calculateSingingVolumeKey(singingVolumeSource); - const phrase = getOrThrow(state.phrases, context.phraseKey); + const phrase = getOrThrow(state.phrases, phraseKey); const phraseSingingVolumeKey = phrase.singingVolumeKey; return ( phraseSingingVolumeKey == undefined || phraseSingingVolumeKey !== singingVolumeKey ); }, - deleteExecutionResult: (context: PhraseRenderContext) => { - const phrase = getOrThrow(state.phrases, context.phraseKey); + deleteExecutionResult: (phraseKey: PhraseKey) => { + const phrase = getOrThrow(state.phrases, phraseKey); const phraseSingingVolumeKey = phrase.singingVolumeKey; if (phraseSingingVolumeKey != undefined) { mutations.DELETE_PHRASE_SINGING_VOLUME({ singingVolumeKey: phraseSingingVolumeKey, }); mutations.SET_SINGING_VOLUME_KEY_TO_PHRASE({ - phraseKey: context.phraseKey, + phraseKey, singingVolumeKey: undefined, }); } }, - execute: async (context: PhraseRenderContext) => { - const singingVolumeSource = generateSingingVolumeSource(context); + execute: async ( + trackId: TrackId, + phraseKey: PhraseKey, + snapshot: SnapshotForPhraseRender, + ) => { + const singingVolumeSource = generateSingingVolumeSource( + trackId, + phraseKey, + snapshot, + ); const singingVolumeKey = await calculateSingingVolumeKey(singingVolumeSource); @@ -2207,7 +2230,7 @@ export const singingStore = createPartialStore({ singingVolumeCache.set(singingVolumeKey, singingVolume); } - const phrase = getOrThrow(state.phrases, context.phraseKey); + const phrase = getOrThrow(state.phrases, phraseKey); const phraseSingingVolumeKey = phrase.singingVolumeKey; if (phraseSingingVolumeKey != undefined) { throw new Error( @@ -2219,20 +2242,22 @@ export const singingStore = createPartialStore({ singingVolume, }); mutations.SET_SINGING_VOLUME_KEY_TO_PHRASE({ - phraseKey: context.phraseKey, + phraseKey, singingVolumeKey, }); }, }; const generateSingingVoiceSource = ( - context: PhraseRenderContext, + trackId: TrackId, + phraseKey: PhraseKey, + snapshot: SnapshotForPhraseRender, ): SingingVoiceSource => { - const track = getOrThrow(context.snapshot.tracks, context.trackId); + const track = getOrThrow(snapshot.tracks, trackId); if (track.singer == undefined) { throw new Error("track.singer is undefined."); } - const phrase = getOrThrow(state.phrases, context.phraseKey); + const phrase = getOrThrow(state.phrases, phraseKey); const phraseQueryKey = phrase.queryKey; const phraseSingingVolumeKey = phrase.singingVolumeKey; if (phraseQueryKey == undefined) { @@ -2252,7 +2277,7 @@ export const singingStore = createPartialStore({ clonedQuery, phrase.startTime, track.pitchEditData, - context.snapshot.editorFrameRate, + snapshot.editorFrameRate, ); clonedQuery.volume = clonedSingingVolume; return { @@ -2292,34 +2317,50 @@ export const singingStore = createPartialStore({ const singingVoiceSynthesisStage: PhraseRenderStage = { id: "singingVoiceSynthesis", - shouldBeExecuted: async (context: PhraseRenderContext) => { - const track = getOrThrow(context.snapshot.tracks, context.trackId); + needsExecution: async ( + trackId: TrackId, + phraseKey: PhraseKey, + snapshot: SnapshotForPhraseRender, + ) => { + const track = getOrThrow(snapshot.tracks, trackId); if (track.singer == undefined) { return false; } - const singingVoiceSource = generateSingingVoiceSource(context); + const singingVoiceSource = generateSingingVoiceSource( + trackId, + phraseKey, + snapshot, + ); const singingVoiceKey = await calculateSingingVoiceKey(singingVoiceSource); - const phrase = getOrThrow(state.phrases, context.phraseKey); + const phrase = getOrThrow(state.phrases, phraseKey); const phraseSingingVoiceKey = phrase.singingVoiceKey; return ( phraseSingingVoiceKey == undefined || phraseSingingVoiceKey !== singingVoiceKey ); }, - deleteExecutionResult: (context: PhraseRenderContext) => { - const phrase = getOrThrow(state.phrases, context.phraseKey); + deleteExecutionResult: (phraseKey: PhraseKey) => { + const phrase = getOrThrow(state.phrases, phraseKey); const phraseSingingVoiceKey = phrase.singingVoiceKey; if (phraseSingingVoiceKey != undefined) { phraseSingingVoices.delete(phraseSingingVoiceKey); mutations.SET_SINGING_VOICE_KEY_TO_PHRASE({ - phraseKey: context.phraseKey, + phraseKey, singingVoiceKey: undefined, }); } }, - execute: async (context: PhraseRenderContext) => { - const singingVoiceSource = generateSingingVoiceSource(context); + execute: async ( + trackId: TrackId, + phraseKey: PhraseKey, + snapshot: SnapshotForPhraseRender, + ) => { + const singingVoiceSource = generateSingingVoiceSource( + trackId, + phraseKey, + snapshot, + ); const singingVoiceKey = await calculateSingingVoiceKey(singingVoiceSource); @@ -2332,19 +2373,20 @@ export const singingStore = createPartialStore({ singingVoiceCache.set(singingVoiceKey, singingVoice); } - const phrase = getOrThrow(state.phrases, context.phraseKey); + const phrase = getOrThrow(state.phrases, phraseKey); const phraseSingingVoiceKey = phrase.singingVoiceKey; if (phraseSingingVoiceKey != undefined) { throw new Error("The previous singing voice has not been removed."); } phraseSingingVoices.set(singingVoiceKey, singingVoice); mutations.SET_SINGING_VOICE_KEY_TO_PHRASE({ - phraseKey: context.phraseKey, + phraseKey, singingVoiceKey, }); }, }; + // NOTE: ステージは実行順で保持 const stages: readonly PhraseRenderStage[] = [ queryGenerationStage, singingPitchGenerationStage, @@ -2352,53 +2394,6 @@ export const singingStore = createPartialStore({ singingVoiceSynthesisStage, ]; - const phraseRenderer: PhraseRenderer = { - getFirstRenderStageId: () => { - return stages[0].id; - }, - determineStartStage: async ( - snapshot: SnapshotForPhraseRender, - trackId: TrackId, - phraseKey: PhraseKey, - ) => { - const context: PhraseRenderContext = { - snapshot, - trackId, - phraseKey, - }; - for (const stage of stages) { - if (await stage.shouldBeExecuted(context)) { - return stage.id; - } - } - return undefined; - }, - render: async ( - snapshot: SnapshotForPhraseRender, - trackId: TrackId, - phraseKey: PhraseKey, - startStageId: PhraseRenderStageId, - ) => { - const context: PhraseRenderContext = { - snapshot, - trackId, - phraseKey, - }; - const startStageIndex = stages.findIndex((value) => { - return value.id === startStageId; - }); - if (startStageIndex === -1) { - throw new Error("Stage not found."); - } - for (let i = stages.length - 1; i >= startStageIndex; i--) { - stages[i].deleteExecutionResult(context); - } - for (let i = startStageIndex; i < stages.length; i++) { - await stages[i].execute(context); - } - }, - }; - // NOTE: 型推論でawaitの前か後かが考慮されないので、関数を介して取得する(型がbooleanになるようにする) const startRenderingRequested = () => state.startRenderingRequested; const stopRenderingRequested = () => state.stopRenderingRequested; @@ -2423,94 +2418,98 @@ export const singingStore = createPartialStore({ const render = async () => { const firstRestMinDurationSeconds = 0.12; - - // レンダリング中に変更される可能性のあるデータのコピー - const snapshot = { - tpqn: state.tpqn, - tempos: cloneWithUnwrapProxy(state.tempos), - tracks: cloneWithUnwrapProxy(state.tracks), - trackOverlappingNoteIds: new Map( - [...state.tracks.keys()].map((trackId) => [ - trackId, - getters.OVERLAPPING_NOTE_IDS(trackId), - ]), - ), - engineFrameRates: new Map( - Object.entries(state.engineManifests).map( - ([engineId, engineManifest]) => [ - engineId as EngineId, - engineManifest.frameRate, - ], - ), - ), - editorFrameRate: state.editorFrameRate, - } as const; + const snapshot = createSnapshot(); const renderStartStageIds = new Map(); - // フレーズを更新する - - const foundPhrases = new Map(); + // 重なっているノートを削除する + const filteredTrackNotes = new Map(); for (const [trackId, track] of snapshot.tracks) { - // 重なっているノートを削除する const overlappingNoteIds = getOrThrow( snapshot.trackOverlappingNoteIds, trackId, ); - const notes = track.notes.filter( + const filteredNotes = track.notes.filter( (value) => !overlappingNoteIds.has(value.id), ); - const phrases = await searchPhrases( - notes, + filteredTrackNotes.set(trackId, filteredNotes); + } + + // ノーツからフレーズを生成する + const generatedPhrases = new Map(); + for (const trackId of snapshot.tracks.keys()) { + const filteredNotes = getOrThrow(filteredTrackNotes, trackId); + const phrases = await generatePhrases( + filteredNotes, snapshot.tempos, snapshot.tpqn, firstRestMinDurationSeconds, trackId, ); for (const [phraseHash, phrase] of phrases) { - foundPhrases.set(phraseHash, phrase); + generatedPhrases.set(phraseHash, phrase); } } - const phrases = new Map(); + const mergedPhrases = new Map(); + const newlyCreatedPhraseKeys = new Set(); const disappearedPhraseKeys = new Set(); + // 新しく作られたフレーズとstateにある既存のフレーズをマージする + // 新しく作られたフレーズと無くなったフレーズのキーのリスト(Set)も作成する for (const phraseKey of state.phrases.keys()) { - if (!foundPhrases.has(phraseKey)) { - // 無くなったフレーズの場合 + if (!generatedPhrases.has(phraseKey)) { disappearedPhraseKeys.add(phraseKey); } } - for (const [phraseKey, foundPhrase] of foundPhrases) { - // 新しいフレーズまたは既存のフレーズの場合 + for (const [phraseKey, generatedPhrase] of generatedPhrases) { const existingPhrase = state.phrases.get(phraseKey); - const phrase = - existingPhrase == undefined - ? foundPhrase - : cloneWithUnwrapProxy(existingPhrase); + const isNewlyCreated = existingPhrase == undefined; + const phrase = isNewlyCreated + ? generatedPhrase + : cloneWithUnwrapProxy(existingPhrase); + + mergedPhrases.set(phraseKey, phrase); + if (isNewlyCreated) { + newlyCreatedPhraseKeys.add(phraseKey); + } + } + + // 各フレーズのレンダリング開始ステージを決定する + // 新しいフレーズの場合は最初からレンダリングする + // phrase.stateがCOULD_NOT_RENDERだった場合は最初からレンダリングし直す + for (const [phraseKey, phrase] of mergedPhrases) { + const track = getOrThrow(snapshot.tracks, phrase.trackId); + const trackId = phrase.trackId; + const isNewlyCreated = newlyCreatedPhraseKeys.has(phraseKey); + + if (track.singer == undefined) { + continue; + } + if (isNewlyCreated || phrase.state === "COULD_NOT_RENDER") { + renderStartStageIds.set(phraseKey, stages[0].id); + } else { + for (const stage of stages) { + if (await stage.needsExecution(trackId, phraseKey, snapshot)) { + renderStartStageIds.set(phraseKey, stage.id); + break; + } + } + } + } + + // phrase.stateを更新する + for (const [phraseKey, phrase] of mergedPhrases) { const track = getOrThrow(snapshot.tracks, phrase.trackId); if (track.singer == undefined) { phrase.state = "SINGER_IS_NOT_SET"; } else { - // 新しいフレーズの場合は最初からレンダリングする - // phrase.stateがCOULD_NOT_RENDERだった場合は最初からレンダリングし直す - // 既存のフレーズの場合は適切なレンダリング開始ステージを決定する - const renderStartStageId = - existingPhrase == undefined || phrase.state === "COULD_NOT_RENDER" - ? phraseRenderer.getFirstRenderStageId() - : await phraseRenderer.determineStartStage( - snapshot, - foundPhrase.trackId, - phraseKey, - ); - if (renderStartStageId == undefined) { - phrase.state = "PLAYABLE"; - } else { - renderStartStageIds.set(phraseKey, renderStartStageId); + if (renderStartStageIds.has(phraseKey)) { phrase.state = "WAITING_TO_BE_RENDERED"; + } else { + phrase.state = "PLAYABLE"; } } - phrases.set(phraseKey, phrase); } // 無くなったフレーズのシーケンスを削除する @@ -2521,12 +2520,13 @@ export const singingStore = createPartialStore({ } } - mutations.SET_PHRASES({ phrases }); + // state.phrasesを更新する + mutations.SET_PHRASES({ phrases: mergedPhrases }); logger.info("Phrases updated."); - // 各フレーズのレンダリングを行う - + // シンガーが設定されていないフレーズとレンダリング未完了のフレーズが + // プレビュー音で再生されるようにする for (const [phraseKey, phrase] of state.phrases.entries()) { if ( phrase.state === "SINGER_IS_NOT_SET" || @@ -2555,6 +2555,8 @@ export const singingStore = createPartialStore({ mutations.SET_SEQUENCE_ID_TO_PHRASE({ phraseKey, sequenceId }); } } + + // 各フレーズのレンダリングを行い、レンダリングされた音声が再生されるようにする const phrasesToBeRendered = new Map( [...state.phrases.entries()].filter(([, phrase]) => { return phrase.state === "WAITING_TO_BE_RENDERED"; @@ -2577,12 +2579,20 @@ export const singingStore = createPartialStore({ try { // フレーズのレンダリングを行う - await phraseRenderer.render( - snapshot, - phrase.trackId, - phraseKey, - getOrThrow(renderStartStageIds, phraseKey), - ); + const trackId = phrase.trackId; + const startStageId = getOrThrow(renderStartStageIds, phraseKey); + const startStageIndex = stages.findIndex((value) => { + return value.id === startStageId; + }); + if (startStageIndex === -1) { + throw new Error("Stage not found."); + } + for (let i = stages.length - 1; i >= startStageIndex; i--) { + stages[i].deleteExecutionResult(phraseKey); + } + for (let i = startStageIndex; i < stages.length; i++) { + await stages[i].execute(trackId, phraseKey, snapshot); + } // シーケンスが存在する場合、シーケンスを削除する const phraseSequenceId = getPhraseSequenceId(phraseKey);