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: [],
},
],