Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ソング:レンダリング順を改善 #1909

Merged
merged 13 commits into from
Mar 18, 2024
Merged
43 changes: 42 additions & 1 deletion src/sing/domain.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -286,3 +286,44 @@ export function isValidKeyRangeAdjustment(keyRangeAdjustment: number) {
keyRangeAdjustment >= -24
);
}

export function toSortedPhrases(phrases: Map<string, Phrase>) {
return [...phrases.entries()].sort((a, b) => {
return a[1].startTicks - b[1].startTicks;
});
}

/**
* 次にレンダリングするべきPhraseを探す。
* phrasesが空の場合はエ
* 優先順:
Hiroshiba marked this conversation as resolved.
Show resolved Hide resolved
* - 再生位置が含まれるPhrase
* - 再生位置より後のPhrase
* - 再生位置より前のPhrase
*
*/
export function selectPriorPhrase(
Hiroshiba marked this conversation as resolved.
Show resolved Hide resolved
phrases: Map<string, Phrase>,
position: number
Hiroshiba marked this conversation as resolved.
Show resolved Hide resolved
): [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];
}
Hiroshiba marked this conversation as resolved.
Show resolved Hide resolved
35 changes: 21 additions & 14 deletions src/store/singing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
Transport,
} from "@/sing/audioRendering";
import {
selectPriorPhrase,
getMeasureDuration,
isValidNote,
isValidScore,
Expand Down Expand Up @@ -787,12 +788,6 @@
return foundPhrases;
};

const getSortedPhrasesEntries = (phrases: Map<string, Phrase>) => {
return [...phrases.entries()].sort((a, b) => {
return a[1].startTicks - b[1].startTicks;
});
};

const fetchQuery = async (
engineId: EngineId,
notes: Note[],
Expand Down Expand Up @@ -1003,12 +998,28 @@
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) {
if (!phrase.singer) {
continue;
while (
!(startRenderingRequested() || stopRenderingRequested()) &&
phrasesToBeRendered.size > 0
) {
const [phraseKey, phrase] = selectPriorPhrase(
phrasesToBeRendered,
playheadPosition.value
);
if (!phrase) {
break;
}
sevenc-nanashi marked this conversation as resolved.
Show resolved Hide resolved
phrasesToBeRendered.delete(phraseKey);

if (
phrase.state === "WAITING_TO_BE_RENDERED" ||
Expand All @@ -1023,12 +1034,12 @@
// 推論(クエリのフェッチ)、キーシフト、フレーズの開始時刻の計算を行う

if (!phrase.query) {
const engineId = phrase.singer.engineId;

Check failure on line 1037 in src/store/singing.ts

View workflow job for this annotation

GitHub Actions / build-test

Object is possibly 'undefined'.

Check failure on line 1037 in src/store/singing.ts

View workflow job for this annotation

GitHub Actions / lint

Object is possibly 'undefined'.
const frameRate = state.engineManifests[engineId].frameRate;
const restDurationSeconds = 1; // 前後の休符の長さはとりあえず1秒に設定

const frameAudioQuery = await fetchQuery(
phrase.singer.engineId,

Check failure on line 1042 in src/store/singing.ts

View workflow job for this annotation

GitHub Actions / build-test

Object is possibly 'undefined'.

Check failure on line 1042 in src/store/singing.ts

View workflow job for this annotation

GitHub Actions / lint

Object is possibly 'undefined'.
phrase.notes,
phrase.tempos,
phrase.tpqn,
Expand Down Expand Up @@ -1091,7 +1102,7 @@
if (phraseData.blob) {
window.backend.logInfo(`Loaded audio buffer from cache.`);
} else {
const blob = await synthesize(phrase.singer, phrase.query).catch(

Check failure on line 1105 in src/store/singing.ts

View workflow job for this annotation

GitHub Actions / build-test

Argument of type '{ engineId: string & BRAND<"EngineId">; styleId: number & BRAND<"StyleId">; } | undefined' is not assignable to parameter of type '{ engineId: string & BRAND<"EngineId">; styleId: number & BRAND<"StyleId">; }'.

Check failure on line 1105 in src/store/singing.ts

View workflow job for this annotation

GitHub Actions / lint

Argument of type '{ engineId: string & BRAND<"EngineId">; styleId: number & BRAND<"StyleId">; } | undefined' is not assignable to parameter of type '{ engineId: string & BRAND<"EngineId">; styleId: number & BRAND<"StyleId">; }'.
(error) => {
commit("SET_STATE_TO_PHRASE", {
phraseKey,
Expand Down Expand Up @@ -1137,10 +1148,6 @@
phraseState: "PLAYABLE",
});
}

if (startRenderingRequested() || stopRenderingRequested()) {
return;
}
}
};

Expand Down
66 changes: 66 additions & 0 deletions tests/unit/lib/selectPriorPhrase.spec.ts
Original file line number Diff line number Diff line change
@@ -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<string, Phrase>([
["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");
});
Loading