From 102d8a9178c453cdf07ace89a84f29028a680f04 Mon Sep 17 00:00:00 2001 From: Nanashi Date: Mon, 18 Mar 2024 19:47:02 +0900 Subject: [PATCH] =?UTF-8?q?=E3=82=BD=E3=83=B3=E3=82=B0=EF=BC=9A=E3=83=AC?= =?UTF-8?q?=E3=83=B3=E3=83=80=E3=83=AA=E3=83=B3=E3=82=B0=E9=A0=86=E3=82=92?= =?UTF-8?q?=E6=94=B9=E5=96=84=20(#1909)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Improve: レンダリング順を改善 * Fix: 値を修正 * Fix: toEqualを使う * 音域補正用のパラメーターを増やしつつ、開発時のみの機能に (#1902) * 音高補正にしつつ開発時のみ機能に * VoiceKey→GuidePitch * とりあえず実装としては完成 * keyRangeAdjustmentに * (note|guide)KeyShiftを消し、補正→調整にする * keyShiftを全てKeyRangeAdjustmentへ * Update: mainに追従 * Improve: レビューを反映 Co-Authored-By: Hiroshiba * Change: findPriorPhrases -> selectPriorPhrases * Delete: 不要な型アサーションを削除 Co-Authored-By: sigprogramming * Add: 型アサーションを追加 * Update src/sing/domain.ts * Update src/sing/domain.ts --------- Co-authored-by: Hiroshiba Co-authored-by: Hiroshiba Co-authored-by: sigprogramming --- src/sing/domain.ts | 42 ++++++++++++++- src/store/singing.ts | 33 +++++++----- tests/unit/lib/selectPriorPhrase.spec.ts | 66 ++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 14 deletions(-) create mode 100644 tests/unit/lib/selectPriorPhrase.spec.ts diff --git a/src/sing/domain.ts b/src/sing/domain.ts index 57c1f9b129..146115834e 100644 --- a/src/sing/domain.ts +++ b/src/sing/domain.ts @@ -1,4 +1,4 @@ -import { Note, Score, Tempo, TimeSignature } from "@/store/type"; +import { Note, Phrase, Score, Tempo, TimeSignature } from "@/store/type"; const BEAT_TYPES = [2, 4, 8, 16]; const MIN_BPM = 40; @@ -294,3 +294,43 @@ export function isValidvolumeRangeAdjustment(volumeRangeAdjustment: number) { volumeRangeAdjustment >= -20 ); } + +export function toSortedPhrases(phrases: Map) { + return [...phrases.entries()].sort((a, b) => { + return a[1].startTicks - b[1].startTicks; + }); +} + +/** + * 次にレンダリングするべきPhraseを探す。 + * phrasesが空の場合はエラー + * 優先順: + * - 再生位置が含まれるPhrase + * - 再生位置より後のPhrase + * - 再生位置より前のPhrase + */ +export function selectPriorPhrase( + phrases: Map, + position: number +): [string, Phrase] { + if (phrases.size === 0) { + throw new Error("Received empty phrases"); + } + // 再生位置が含まれるPhrase + for (const [phraseKey, phrase] of phrases) { + if (phrase.startTicks <= position && position <= phrase.endTicks) { + return [phraseKey, phrase]; + } + } + + const sortedPhrases = toSortedPhrases(phrases); + // 再生位置より後のPhrase + for (const [phraseKey, phrase] of sortedPhrases) { + if (phrase.startTicks > position) { + return [phraseKey, phrase]; + } + } + + // 再生位置より前のPhrase + return sortedPhrases[0]; +} diff --git a/src/store/singing.ts b/src/store/singing.ts index 680d590647..4b83b6c13e 100644 --- a/src/store/singing.ts +++ b/src/store/singing.ts @@ -38,6 +38,7 @@ import { Transport, } from "@/sing/audioRendering"; import { + selectPriorPhrase, getMeasureDuration, isValidNote, isValidScore, @@ -795,12 +796,6 @@ export const singingStore = createPartialStore({ return foundPhrases; }; - const getSortedPhrasesEntries = (phrases: Map) => { - return [...phrases.entries()].sort((a, b) => { - return a[1].startTicks - b[1].startTicks; - }); - }; - const fetchQuery = async ( engineId: EngineId, notes: Note[], @@ -1022,12 +1017,28 @@ export const singingStore = createPartialStore({ return; } + const phrasesToBeRendered = new Map( + [...state.phrases.entries()].filter(([, phrase]) => { + return ( + (phrase.state === "WAITING_TO_BE_RENDERED" || + phrase.state === "COULD_NOT_RENDER") && + phrase.singer + ); + }) + ); // 各フレーズのレンダリングを行う - const sortedPhrasesEntries = getSortedPhrasesEntries(state.phrases); - for (const [phraseKey, phrase] of sortedPhrasesEntries) { + while ( + !(startRenderingRequested() || stopRenderingRequested()) && + phrasesToBeRendered.size > 0 + ) { + const [phraseKey, phrase] = selectPriorPhrase( + phrasesToBeRendered, + playheadPosition.value + ); if (!phrase.singer) { - continue; + throw new Error("assert: phrase.singer != undefined"); } + phrasesToBeRendered.delete(phraseKey); if ( phrase.state === "WAITING_TO_BE_RENDERED" || @@ -1157,10 +1168,6 @@ export const singingStore = createPartialStore({ phraseState: "PLAYABLE", }); } - - if (startRenderingRequested() || stopRenderingRequested()) { - return; - } } }; diff --git a/tests/unit/lib/selectPriorPhrase.spec.ts b/tests/unit/lib/selectPriorPhrase.spec.ts new file mode 100644 index 0000000000..dcdc46d272 --- /dev/null +++ b/tests/unit/lib/selectPriorPhrase.spec.ts @@ -0,0 +1,66 @@ +import { it, expect } from "vitest"; +import { Phrase, PhraseState } from "@/store/type"; +import { DEFAULT_TPQN } from "@/sing/storeHelper"; +import { selectPriorPhrase } from "@/sing/domain"; +import { EngineId, StyleId } from "@/type/preload"; + +const tempos = [ + { + position: 0, + bpm: 60, + }, +]; +const createPhrase = ( + start: number, + end: number, + state: PhraseState +): Phrase => { + return { + notes: [], + startTicks: start * DEFAULT_TPQN, + endTicks: end * DEFAULT_TPQN, + keyRangeAdjustment: 0, + state, + tempos, + tpqn: DEFAULT_TPQN, + singer: { + engineId: EngineId("00000000-0000-0000-0000-000000000000"), + styleId: StyleId(0), + }, + }; +}; +const basePhrases = new Map([ + ["1", createPhrase(0, 1, "WAITING_TO_BE_RENDERED")], + ["2", createPhrase(1, 2, "WAITING_TO_BE_RENDERED")], + ["3", createPhrase(2, 3, "WAITING_TO_BE_RENDERED")], + ["4", createPhrase(3, 4, "WAITING_TO_BE_RENDERED")], + ["5", createPhrase(4, 5, "WAITING_TO_BE_RENDERED")], +]); + +it("しっかり優先順位に従って探している", () => { + const phrases = structuredClone(basePhrases); + const position = 2.5 * DEFAULT_TPQN; + for (const expectation of [ + // 再生位置が含まれるPhrase + "3", + // 再生位置より後のPhrase + "4", // 早い方 + "5", // 遅い方 + // 再生位置より前のPhrase + "1", // 早い方 + "2", // 遅い方 + ]) { + const [key] = selectPriorPhrase(phrases, position); + expect(key).toEqual(expectation); + if (key == undefined) { + // 型アサーションのためにthrowを使う + throw new Error("key is undefined"); + } + phrases.delete(key); + } + + // もう再生可能なPhraseがないのでthrow + expect(() => { + selectPriorPhrase(phrases, position); + }).toThrow("Received empty phrases"); +});