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
33 changes: 20 additions & 13 deletions src/store/singing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
Transport,
} from "@/sing/audioRendering";
import {
selectPriorPhrase,
getMeasureDuration,
isValidNote,
isValidScore,
Expand Down Expand Up @@ -787,12 +788,6 @@ export const singingStore = createPartialStore<SingingStoreTypes>({
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 @@ export const singingStore = createPartialStore<SingingStoreTypes>({
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" ||
Expand Down Expand Up @@ -1137,10 +1148,6 @@ export const singingStore = createPartialStore<SingingStoreTypes>({
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