diff --git a/src/components/Sing/ToolBar.vue b/src/components/Sing/ToolBar.vue index 72e3f0fb78..b548367df9 100644 --- a/src/components/Sing/ToolBar.vue +++ b/src/components/Sing/ToolBar.vue @@ -26,6 +26,16 @@ /> + { })?.iconPath; }); -const bpmInputBuffer = ref(0); -const beatsInputBuffer = ref(0); -const beatTypeInputBuffer = ref(0); +const tempos = computed(() => store.state.tempos); +const timeSignatures = computed(() => store.state.timeSignatures); +const keyShift = computed(() => store.getters.SELECTED_TRACK.voiceKeyShift); + +const bpmInputBuffer = ref(120); +const beatsInputBuffer = ref(4); +const beatTypeInputBuffer = ref(4); +const keyShiftInputBuffer = ref(0); + +watch( + tempos, + () => { + bpmInputBuffer.value = tempos.value[0].bpm; + }, + { deep: true } +); + +watch( + timeSignatures, + () => { + beatsInputBuffer.value = timeSignatures.value[0].beats; + beatTypeInputBuffer.value = timeSignatures.value[0].beatType; + }, + { deep: true } +); + +watch(keyShift, () => { + keyShiftInputBuffer.value = keyShift.value; +}); const setBpmInputBuffer = (bpmStr: string | number | null) => { - const bpm = Number(bpmStr); - if (!isValidBpm(bpm)) { + const bpmValue = Number(bpmStr); + if (!isValidBpm(bpmValue)) { return; } - bpmInputBuffer.value = bpm; + bpmInputBuffer.value = bpmValue; }; + const setBeatsInputBuffer = (beatsStr: string | number | null) => { - const beats = Number(beatsStr); - if (!isValidBeats(beats)) { + const beatsValue = Number(beatsStr); + if (!isValidBeats(beatsValue)) { return; } - beatsInputBuffer.value = beats; + beatsInputBuffer.value = beatsValue; }; + const setBeatTypeInputBuffer = (beatTypeStr: string | number | null) => { - const beatType = Number(beatTypeStr); - if (!isValidBeatType(beatType)) { + const beatTypeValue = Number(beatTypeStr); + if (!isValidBeatType(beatTypeValue)) { + return; + } + beatTypeInputBuffer.value = beatTypeValue; +}; + +const setKeyShiftInputBuffer = (keyShiftStr: string | number | null) => { + const keyShiftValue = Number(keyShiftStr); + if (!isValidVoiceKeyShift(keyShiftValue)) { return; } - beatTypeInputBuffer.value = beatType; + keyShiftInputBuffer.value = keyShiftValue; +}; + +const setTempo = () => { + const bpm = bpmInputBuffer.value; + store.dispatch("SET_TEMPO", { + tempo: { + position: 0, + bpm, + }, + }); +}; + +const setTimeSignature = () => { + const beats = beatsInputBuffer.value; + const beatType = beatTypeInputBuffer.value; + store.dispatch("SET_TIME_SIGNATURE", { + timeSignature: { + measureNumber: 1, + beats, + beatType, + }, + }); +}; + +const setKeyShift = () => { + const voiceKeyShift = keyShiftInputBuffer.value; + store.dispatch("SET_VOICE_KEY_SHIFT", { voiceKeyShift }); }; const playheadTicks = ref(0); @@ -213,50 +287,8 @@ const playHeadPositionMilliSecStr = computed(() => { return milliSecStr; }); -const tempos = computed(() => store.state.tempos); -const timeSignatures = computed(() => store.state.timeSignatures); const nowPlaying = computed(() => store.state.nowPlaying); -watch( - tempos, - () => { - bpmInputBuffer.value = tempos.value[0].bpm; - }, - { deep: true } -); -watch( - timeSignatures, - () => { - beatsInputBuffer.value = timeSignatures.value[0].beats; - beatTypeInputBuffer.value = timeSignatures.value[0].beatType; - }, - { deep: true } -); - -const setTempo = async () => { - const bpm = bpmInputBuffer.value; - if (bpm === 0) return; - await store.dispatch("SET_TEMPO", { - tempo: { - position: 0, - bpm, - }, - }); -}; - -const setTimeSignature = async () => { - const beats = beatsInputBuffer.value; - const beatType = beatTypeInputBuffer.value; - if (beats === 0 || beatType === 0) return; - await store.dispatch("SET_TIME_SIGNATURE", { - timeSignature: { - measureNumber: 1, - beats, - beatType, - }, - }); -}; - const play = () => { store.dispatch("SING_PLAY_AUDIO"); }; @@ -408,9 +440,15 @@ onUnmounted(() => { flex: 1; } -.sing-tempo { +.key-shift { margin-left: 16px; margin-right: 4px; + width: 50px; +} + +.sing-tempo { + margin-left: 8px; + margin-right: 4px; width: 72px; } diff --git a/src/sing/domain.ts b/src/sing/domain.ts index aa4a0f9916..30722a40f6 100644 --- a/src/sing/domain.ts +++ b/src/sing/domain.ts @@ -274,3 +274,11 @@ export function getSnapTypes(tpqn: number) { export function isValidSnapType(snapType: number, tpqn: number) { return getSnapTypes(tpqn).some((value) => value === snapType); } + +export function isValidVoiceKeyShift(voiceKeyShift: number) { + return ( + Number.isInteger(voiceKeyShift) && + voiceKeyShift <= 24 && + voiceKeyShift >= -24 + ); +} diff --git a/src/sing/storeHelper.ts b/src/sing/storeHelper.ts index 9ce1628fa2..ad5ce659b6 100644 --- a/src/sing/storeHelper.ts +++ b/src/sing/storeHelper.ts @@ -8,6 +8,8 @@ export const DEFAULT_BEAT_TYPE = 4; export const generatePhraseHash = async (obj: { singer: Singer | undefined; + notesKeyShift: number; + voiceKeyShift: number; tpqn: number; tempos: Tempo[]; notes: Note[]; diff --git a/src/store/singing.ts b/src/store/singing.ts index 9863f7c356..7e3c2b32d7 100644 --- a/src/store/singing.ts +++ b/src/store/singing.ts @@ -40,6 +40,7 @@ import { isValidSnapType, isValidTempo, isValidTimeSignature, + isValidVoiceKeyShift, secondToTick, tickToSecond, } from "@/sing/domain"; @@ -136,6 +137,8 @@ export const singingStoreState: SingingStoreState = { tracks: [ { singer: undefined, + notesKeyShift: 0, + voiceKeyShift: 0, notes: [], }, ], @@ -214,6 +217,23 @@ export const singingStore = createPartialStore({ }, }, + SET_VOICE_KEY_SHIFT: { + mutation(state, { voiceKeyShift }: { voiceKeyShift: number }) { + state.tracks[selectedTrackIndex].voiceKeyShift = voiceKeyShift; + }, + async action( + { dispatch, commit }, + { voiceKeyShift }: { voiceKeyShift: number } + ) { + if (!isValidVoiceKeyShift(voiceKeyShift)) { + throw new Error("The voiceKeyShift is invalid."); + } + commit("SET_VOICE_KEY_SHIFT", { voiceKeyShift }); + + dispatch("RENDER"); + }, + }, + SET_SCORE: { mutation(state, { score }: { score: Score }) { overlappingNotesDetector.clear(); @@ -821,6 +841,8 @@ export const singingStore = createPartialStore({ async action({ state, getters, commit, dispatch }) { const searchPhrases = async ( singer: Singer | undefined, + notesKeyShift: number, + voiceKeyShift: number, tpqn: number, tempos: Tempo[], notes: Note[] @@ -840,12 +862,16 @@ export const singingStore = createPartialStore({ const phraseLastNote = phraseNotes[phraseNotes.length - 1]; const hash = await generatePhraseHash({ singer, + notesKeyShift, + voiceKeyShift, tpqn, tempos, notes: phraseNotes, }); foundPhrases.set(hash, { singer, + notesKeyShift, + voiceKeyShift, tpqn, tempos, notes: phraseNotes, @@ -871,6 +897,7 @@ export const singingStore = createPartialStore({ notes: Note[], tempos: Tempo[], tpqn: number, + notesKeyShift: number, frameRate: number, restDurationSeconds: number ) => { @@ -903,7 +930,7 @@ export const singingStore = createPartialStore({ .replace("は", "ハ") .replace("へ", "ヘ"); notesForRequestToEngine.push({ - key: note.noteNumber, + key: note.noteNumber + notesKeyShift, frameLength: noteFrameLength, lyric, }); @@ -945,6 +972,15 @@ export const singingStore = createPartialStore({ return frameAudioQuery.phonemes.map((value) => value.phoneme).join(" "); }; + const shiftVoiceKey = ( + voiceKeyShift: number, + frameAudioQuery: FrameAudioQuery + ) => { + frameAudioQuery.f0 = frameAudioQuery.f0.map((value) => { + return value * Math.pow(2, voiceKeyShift / 12); + }); + }; + const calcStartTime = ( notes: Note[], tempos: Tempo[], @@ -1000,13 +1036,22 @@ export const singingStore = createPartialStore({ const tpqn = state.tpqn; const tempos = state.tempos.map((value) => ({ ...value })); const track = getters.SELECTED_TRACK; + const singer = track.singer ? { ...track.singer } : undefined; + const notesKeyShift = track.notesKeyShift; + const voiceKeyShift = track.voiceKeyShift; const notes = track.notes .map((value) => ({ ...value })) .filter((value) => !state.overlappingNoteIds.has(value.id)); - const singer = track.singer ? { ...track.singer } : undefined; // フレーズを更新する - const foundPhrases = await searchPhrases(singer, tpqn, tempos, notes); + const foundPhrases = await searchPhrases( + singer, + notesKeyShift, + voiceKeyShift, + tpqn, + tempos, + notes + ); for (const [hash, phrase] of foundPhrases) { const phraseKey = hash; if (!state.phrases.has(phraseKey)) { @@ -1074,7 +1119,7 @@ export const singingStore = createPartialStore({ }); } - // 推論(クエリのフェッチ)とフレーズの開始時刻の算出を行う + // 推論(クエリのフェッチ)、キーシフト、フレーズの開始時刻の計算を行う if (!phrase.query) { const engineId = phrase.singer.engineId; @@ -1086,6 +1131,7 @@ export const singingStore = createPartialStore({ phrase.notes, phrase.tempos, phrase.tpqn, + phrase.notesKeyShift, frameRate, restDurationSeconds ).catch((error) => { @@ -1095,12 +1141,14 @@ export const singingStore = createPartialStore({ }); throw error; }); - const phonemes = getPhonemes(frameAudioQuery); + const phonemes = getPhonemes(frameAudioQuery); window.electron.logInfo( `Fetched frame audio query. Phonemes are "${phonemes}".` ); + shiftVoiceKey(phrase.voiceKeyShift, frameAudioQuery); + const startTime = calcStartTime( phrase.notes, phrase.tempos, diff --git a/src/store/type.ts b/src/store/type.ts index 558f2eed31..3194fdb929 100644 --- a/src/store/type.ts +++ b/src/store/type.ts @@ -749,6 +749,8 @@ export type Singer = { export type Track = { singer?: Singer; + notesKeyShift: number; + voiceKeyShift: number; notes: Note[]; }; @@ -760,6 +762,8 @@ export type PhraseState = export type Phrase = { singer?: Singer; + notesKeyShift: number; + voiceKeyShift: number; tpqn: number; tempos: Tempo[]; notes: Note[]; @@ -806,6 +810,11 @@ export type SingingStoreTypes = { action(payload: { singer?: Singer }): void; }; + SET_VOICE_KEY_SHIFT: { + mutation: { voiceKeyShift: number }; + action(payload: { voiceKeyShift: number }): void; + }; + SET_SCORE: { mutation: { score: Score }; action(payload: { score: Score }): void; diff --git a/tests/unit/store/Vuex.spec.ts b/tests/unit/store/Vuex.spec.ts index d0f1a6ba17..3070b2a567 100644 --- a/tests/unit/store/Vuex.spec.ts +++ b/tests/unit/store/Vuex.spec.ts @@ -168,6 +168,8 @@ describe("store/vuex.js test", () => { ], tracks: [ { + notesKeyShift: 0, + voiceKeyShift: 0, notes: [], }, ],