From 7a6dac3633345eaeccc237ba02046f2bd8d6ea2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=92=E7=8C=AB=E5=A4=A7=E7=A6=8F?= <93469977+rokujyushi@users.noreply.github.com> Date: Sat, 7 Sep 2024 12:42:18 +0900 Subject: [PATCH 01/10] =?UTF-8?q?=E3=83=94=E3=83=83=E3=83=81=E5=87=A6?= =?UTF-8?q?=E7=90=86=E3=81=AE=E3=82=B3=E3=83=94=E3=83=BC=E3=81=AB=E3=82=88?= =?UTF-8?q?=E3=82=8B=E9=9F=B3=E9=87=8F=E3=81=AE=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Sing/ScoreSequencer.vue | 194 ++++++- src/components/Sing/SequencerNote.vue | 5 +- src/components/Sing/SequencerVolume.vue | 481 ++++++++++++++++++ .../Sing/ToolBar/EditTargetSwicher.vue | 10 + src/domain/project/schema.ts | 1 + src/sing/domain.ts | 50 ++ src/sing/viewHelper.ts | 14 + src/store/singing.ts | 104 ++++ src/store/type.ts | 38 +- 9 files changed, 893 insertions(+), 4 deletions(-) create mode 100644 src/components/Sing/SequencerVolume.vue diff --git a/src/components/Sing/ScoreSequencer.vue b/src/components/Sing/ScoreSequencer.vue index 700c8354b3..3388495e55 100644 --- a/src/components/Sing/ScoreSequencer.vue +++ b/src/components/Sing/ScoreSequencer.vue @@ -83,6 +83,17 @@ :offsetY="scrollY" :previewPitchEdit /> +
{ @@ -407,6 +421,11 @@ const previewPitchEdit = ref< | { type: "erase"; startFrame: number; frameLength: number } | undefined >(undefined); +const previewVolumeEdit = ref< + | { type: "draw"; data: number[]; startFrame: number } + | { type: "erase"; startFrame: number; frameLength: number } + | undefined +>(undefined); const prevCursorPos = { frame: 0, frequency: 0 }; // 前のカーソル位置 // 歌詞を編集中のノート @@ -667,6 +686,79 @@ const previewDrawPitch = () => { prevCursorPos.frequency = cursorFrequency; }; +// ピッチを描く処理を行う +const previewDrawVolume = () => { + if (previewVolumeEdit.value == undefined) { + throw new Error("previewVolumeEdit.value is undefined."); + } + if (previewVolumeEdit.value.type !== "draw") { + throw new Error("previewVolumeEdit.value.type is not draw."); + } + const frameRate = editFrameRate.value; + const cursorBaseX = (scrollX.value + cursorX.value) / zoomX.value; + const cursorBaseY = (scrollY.value + cursorY.value) / zoomY.value; + const cursorTicks = baseXToTick(cursorBaseX, tpqn.value); + const cursorSeconds = tickToSecond(cursorTicks, tempos.value, tpqn.value); + const cursorFrame = Math.round(cursorSeconds * frameRate); + const cursorNoteNumber = baseYToNoteNumber(cursorBaseY, false); + const cursorFrequency = noteNumberToFrequency(cursorNoteNumber); + if (cursorFrame < 0) { + return; + } + const tempVolumeEdit = { + ...previewVolumeEdit.value, + data: [...previewVolumeEdit.value.data], + }; + + if (cursorFrame < tempVolumeEdit.startFrame) { + const numOfFramesToUnshift = tempVolumeEdit.startFrame - cursorFrame; + tempVolumeEdit.data = new Array(numOfFramesToUnshift) + .fill(0) + .concat(tempVolumeEdit.data); + tempVolumeEdit.startFrame = cursorFrame; + } + + const lastFrame = tempVolumeEdit.startFrame + tempVolumeEdit.data.length - 1; + if (cursorFrame > lastFrame) { + const numOfFramesToPush = cursorFrame - lastFrame; + tempVolumeEdit.data = tempVolumeEdit.data.concat( + new Array(numOfFramesToPush).fill(0), + ); + } + + if (cursorFrame === prevCursorPos.frame) { + const i = cursorFrame - tempVolumeEdit.startFrame; + tempVolumeEdit.data[i] = cursorFrequency; + } else if (cursorFrame < prevCursorPos.frame) { + for (let i = cursorFrame; i <= prevCursorPos.frame; i++) { + tempVolumeEdit.data[i - tempVolumeEdit.startFrame] = Math.exp( + linearInterpolation( + cursorFrame, + Math.log(cursorFrequency), + prevCursorPos.frame, + Math.log(prevCursorPos.frequency), + i, + ), + ); + } + } else { + for (let i = prevCursorPos.frame; i <= cursorFrame; i++) { + tempVolumeEdit.data[i - tempVolumeEdit.startFrame] = Math.exp( + linearInterpolation( + prevCursorPos.frame, + Math.log(prevCursorPos.frequency), + cursorFrame, + Math.log(cursorFrequency), + i, + ), + ); + } + } + + previewVolumeEdit.value = tempVolumeEdit; + prevCursorPos.frame = cursorFrame; + prevCursorPos.frequency = cursorFrequency; +}; // ドラッグした範囲のピッチ編集データを消去する処理を行う const previewErasePitch = () => { if (previewPitchEdit.value == undefined) { @@ -699,6 +791,37 @@ const previewErasePitch = () => { prevCursorPos.frame = cursorFrame; }; +const previewEraseVolume = () => { + if (previewVolumeEdit.value == undefined) { + throw new Error("previewVolumeEdit.value is undefined."); + } + if (previewVolumeEdit.value.type !== "erase") { + throw new Error("previewVolumeEdit.value.type is not erase."); + } + const frameRate = editFrameRate.value; + const cursorBaseX = (scrollX.value + cursorX.value) / zoomX.value; + const cursorTicks = baseXToTick(cursorBaseX, tpqn.value); + const cursorSeconds = tickToSecond(cursorTicks, tempos.value, tpqn.value); + const cursorFrame = Math.round(cursorSeconds * frameRate); + if (cursorFrame < 0) { + return; + } + const tempVolumeEdit = { ...previewVolumeEdit.value }; + + if (tempVolumeEdit.startFrame > cursorFrame) { + tempVolumeEdit.frameLength += tempVolumeEdit.startFrame - cursorFrame; + tempVolumeEdit.startFrame = cursorFrame; + } + + const lastFrame = tempVolumeEdit.startFrame + tempVolumeEdit.frameLength - 1; + if (lastFrame < cursorFrame) { + tempVolumeEdit.frameLength += cursorFrame - lastFrame; + } + + previewVolumeEdit.value = tempVolumeEdit; + prevCursorPos.frame = cursorFrame; +}; + const preview = () => { if (executePreviewProcess) { if (previewMode === "ADD_NOTE") { @@ -719,6 +842,12 @@ const preview = () => { if (previewMode === "ERASE_PITCH") { previewErasePitch(); } + if (previewMode === "DRAW_VOLUME") { + previewDrawVolume(); + } + if (previewMode === "ERASE_VOLUME") { + previewEraseVolume(); + } executePreviewProcess = false; } previewRequestId = requestAnimationFrame(preview); @@ -850,6 +979,28 @@ const startPreview = (event: MouseEvent, mode: PreviewMode, note?: Note) => { } prevCursorPos.frame = cursorFrame; prevCursorPos.frequency = cursorFrequency; + } else if (editTarget.value === "VOLUME") { + const frameRate = editFrameRate.value; + const cursorTicks = baseXToTick(cursorBaseX, tpqn.value); + const cursorSeconds = tickToSecond(cursorTicks, tempos.value, tpqn.value); + const cursorFrame = Math.round(cursorSeconds * frameRate); + const cursorNoteNumber = baseYToNoteNumber(cursorBaseY, false); + const cursorFrequency = noteNumberToFrequency(cursorNoteNumber); + if (mode === "DRAW_VOLUME") { + previewVolumeEdit.value = { + type: "draw", + data: [cursorFrequency], + startFrame: cursorFrame, + }; + } else if (mode === "ERASE_VOLUME") { + previewVolumeEdit.value = { + type: "erase", + startFrame: cursorFrame, + frameLength: 1, + }; + } else { + throw new Error("Unknown preview mode."); + } } else { throw new ExhaustiveError(editTarget.value); } @@ -920,6 +1071,37 @@ const endPreview = () => { throw new ExhaustiveError(previewPitchEditType); } previewPitchEdit.value = undefined; + } else if (previewStartEditTarget === "VOLUME") { + if (previewVolumeEdit.value == undefined) { + throw new Error("previewVolumeEdit.value is undefined."); + } + const previewVolumeEditType = previewVolumeEdit.value.type; + if (previewVolumeEditType === "draw") { + // カーソルを動かさずにマウスのボタンを離したときに1フレームのみの変更になり、 + // 1フレームの変更はピッチ編集ラインとして表示されないので、無視する + if (previewVolumeEdit.value.data.length >= 2) { + // 平滑化を行う + let data = previewVolumeEdit.value.data; + data = data.map((value) => Math.log(value)); + applyGaussianFilter(data, 0.7); + data = data.map((value) => Math.exp(value)); + + store.dispatch("COMMAND_SET_VOLUME_EDIT_DATA", { + volumeArray: data, + startFrame: previewVolumeEdit.value.startFrame, + trackId: selectedTrackId.value, + }); + } + } else if (previewVolumeEditType === "erase") { + store.dispatch("COMMAND_ERASE_VOLUME_EDIT_DATA", { + startFrame: previewVolumeEdit.value.startFrame, + frameLength: previewVolumeEdit.value.frameLength, + trackId: selectedTrackId.value, + }); + } else { + throw new ExhaustiveError(previewVolumeEditType); + } + previewVolumeEdit.value = undefined; } else { throw new ExhaustiveError(previewStartEditTarget); } @@ -998,6 +1180,14 @@ const onMouseDown = (event: MouseEvent) => { startPreview(event, "DRAW_PITCH"); } } + } else if (editTarget.value === "VOLUME") { + if (mouseButton === "LEFT_BUTTON") { + if (isOnCommandOrCtrlKeyDown(event)) { + startPreview(event, "ERASE_VOLUME"); + } else { + startPreview(event, "DRAW_VOLUME"); + } + } } else { throw new ExhaustiveError(editTarget.value); } @@ -1566,7 +1756,7 @@ const contextMenuData = computed(() => { pointer-events: none; } -.sequencer-pitch { +.sequencer-curve { grid-row: 2; grid-column: 2; } diff --git a/src/components/Sing/SequencerNote.vue b/src/components/Sing/SequencerNote.vue index df41b34e48..884cd9068e 100644 --- a/src/components/Sing/SequencerNote.vue +++ b/src/components/Sing/SequencerNote.vue @@ -6,7 +6,7 @@ 'preview-lyric': previewLyric != undefined, overlapping: hasOverlappingError, 'invalid-phrase': hasPhraseError, - 'below-pitch': editTargetIsPitch, + 'below-pitch': editTargetIsPitch || editTargetIsVolume, }" :style="{ width: `${width}px`, @@ -129,6 +129,9 @@ const editTargetIsNote = computed(() => { const editTargetIsPitch = computed(() => { return state.sequencerEditTarget === "PITCH"; }); +const editTargetIsVolume = computed(() => { + return state.sequencerEditTarget === "VOLUME"; +}); const hasOverlappingError = computed(() => { return props.isOverlapping && !props.isPreview; }); diff --git a/src/components/Sing/SequencerVolume.vue b/src/components/Sing/SequencerVolume.vue new file mode 100644 index 0000000000..b94980bf46 --- /dev/null +++ b/src/components/Sing/SequencerVolume.vue @@ -0,0 +1,481 @@ + + + + + diff --git a/src/components/Sing/ToolBar/EditTargetSwicher.vue b/src/components/Sing/ToolBar/EditTargetSwicher.vue index f6c74f9579..34e79a161a 100644 --- a/src/components/Sing/ToolBar/EditTargetSwicher.vue +++ b/src/components/Sing/ToolBar/EditTargetSwicher.vue @@ -21,6 +21,11 @@ value: 'PITCH', slot: 'PITCH', }, + { + icon: 'show_chart', + value: 'VOLUME', + slot: 'VOLUME', + }, ]" @update:modelValue="changeEditTarget" > + diff --git a/src/domain/project/schema.ts b/src/domain/project/schema.ts index c23a6812f1..8947192d05 100644 --- a/src/domain/project/schema.ts +++ b/src/domain/project/schema.ts @@ -91,6 +91,7 @@ export const trackSchema = z.object({ volumeRangeAdjustment: z.number(), // 声量調整量 notes: z.array(noteSchema), pitchEditData: z.array(z.number()), // 値の単位はHzで、データが無いところはVALUE_INDICATING_NO_DATAの値 + volumeEditData: z.array(z.number()), // 値の単位はdbで、データが無いところはVALUE_INDICATING_NO_DATAの値 solo: z.boolean(), mute: z.boolean(), diff --git a/src/sing/domain.ts b/src/sing/domain.ts index 6d84530b73..d3dc3b1311 100644 --- a/src/sing/domain.ts +++ b/src/sing/domain.ts @@ -337,6 +337,7 @@ export function createDefaultTrack(): Track { volumeRangeAdjustment: 0, notes: [], pitchEditData: [], + volumeEditData: [], solo: false, mute: false, @@ -379,6 +380,14 @@ export function isValidPitchEditData(pitchEditData: number[]) { ); } +export function isValidVolumeEditData(volumeEditData: number[]) { + return volumeEditData.every( + (value) => + Number.isFinite(value) && + (value > 0 || value === VALUE_INDICATING_NO_DATA), + ); +} + export const calculatePhraseSourceHash = async (phraseSource: PhraseSource) => { const hash = await calculateHash(phraseSource); return phraseSourceHashSchema.parse(hash); @@ -509,6 +518,47 @@ export function applyPitchEdit( } } +export function applyVolumeEdit( + singingGuide: SingingGuide, + volumeEditData: number[], + editFrameRate: number, +) { + // 歌い方のフレームレートと編集フレームレートが一致しない場合はエラー + // TODO: 補間するようにする + if (singingGuide.frameRate !== editFrameRate) { + throw new Error( + "The frame rate between the singing guide and the edit data does not match.", + ); + } + const unvoicedPhonemes = UNVOICED_PHONEMES; + const volume = singingGuide.query.volume; + const phonemes = singingGuide.query.phonemes; + + // 各フレームの音素の配列を生成する + const framePhonemes = convertToFramePhonemes(phonemes); + if (volume.length !== framePhonemes.length) { + throw new Error("volume.length and framePhonemes.length do not match."); + } + + // 歌い方の開始フレームと終了フレームを計算する + const singingGuideFrameLength = volume.length; + const singingGuideStartFrame = Math.round( + singingGuide.startTime * singingGuide.frameRate, + ); + const singingGuideEndFrame = singingGuideStartFrame + singingGuideFrameLength; + + // ピッチ編集をf0に適用する + const startFrame = Math.max(0, singingGuideStartFrame); + const endFrame = Math.min(volumeEditData.length, singingGuideEndFrame); + for (let i = startFrame; i < endFrame; i++) { + const phoneme = framePhonemes[i - singingGuideStartFrame]; + const voiced = !unvoicedPhonemes.includes(phoneme); + if (voiced && volumeEditData[i] !== VALUE_INDICATING_NO_DATA) { + volume[i - singingGuideStartFrame] = volumeEditData[i]; + } + } +} + // 参考:https://github.com/VOICEVOX/voicevox_core/blob/0848630d81ae3e917c6ff2038f0b15bbd4270702/crates/voicevox_core/src/user_dict/word.rs#L83-L90 export const moraPattern = new RegExp( "(?:" + diff --git a/src/sing/viewHelper.ts b/src/sing/viewHelper.ts index b20366eaa9..122a52f8ae 100644 --- a/src/sing/viewHelper.ts +++ b/src/sing/viewHelper.ts @@ -134,6 +134,20 @@ export async function calculatePitchDataHash(pitchData: PitchData) { return pitchDataHashSchema.parse(hash); } +export type VolumeData = { + readonly ticksArray: number[]; + readonly data: number[]; +}; + +const VolumeDataHashSchema = z.string().brand<"VolumeDataHash">(); + +export type VolumeDataHash = z.infer; + +export async function calculateVolumeDataHash(volumeData: VolumeData) { + const hash = await calculateHash(volumeData); + return VolumeDataHashSchema.parse(hash); +} + export type MouseButton = "LEFT_BUTTON" | "RIGHT_BUTTON" | "OTHER_BUTTON"; export function getButton(event: MouseEvent): MouseButton { diff --git a/src/store/singing.ts b/src/store/singing.ts index 187620a989..e444ca9741 100644 --- a/src/store/singing.ts +++ b/src/store/singing.ts @@ -61,8 +61,10 @@ import { calculateSingingVoiceSourceHash, decibelToLinear, applyPitchEdit, + applyVolumeEdit, VALUE_INDICATING_NO_DATA, isValidPitchEditData, + isValidVolumeEditData, calculatePhraseSourceHash, isValidTempos, isValidTimeSignatures, @@ -746,6 +748,59 @@ export const singingStore = createPartialStore({ dispatch("RENDER"); }, }, + SET_VOLUME_EDIT_DATA: { + // ピッチ編集データをセットする。 + // track.pitchEditDataの長さが足りない場合は、伸長も行う。 + mutation(state, { volumeArray, startFrame, trackId }) { + const track = getOrThrow(state.tracks, trackId); + const pitchEditData = track.volumeEditData; + const tempData = [...pitchEditData]; + const endFrame = startFrame + volumeArray.length; + if (tempData.length < endFrame) { + const valuesToPush = new Array(endFrame - tempData.length).fill( + VALUE_INDICATING_NO_DATA, + ); + tempData.push(...valuesToPush); + } + tempData.splice(startFrame, volumeArray.length, ...volumeArray); + track.volumeEditData = tempData; + }, + async action({ dispatch, commit }, { volumeArray, startFrame, trackId }) { + if (startFrame < 0) { + throw new Error("startFrame must be greater than or equal to 0."); + } + if (!isValidVolumeEditData(volumeArray)) { + throw new Error("The pitch edit data is invalid."); + } + commit("SET_VOLUME_EDIT_DATA", { volumeArray, startFrame, trackId }); + + dispatch("RENDER"); + }, + }, + + ERASE_VOLUME_EDIT_DATA: { + mutation(state, { startFrame, frameLength, trackId }) { + const track = getOrThrow(state.tracks, trackId); + const pitchEditData = track.pitchEditData; + const tempData = [...pitchEditData]; + const endFrame = Math.min(startFrame + frameLength, tempData.length); + tempData.fill(VALUE_INDICATING_NO_DATA, startFrame, endFrame); + track.pitchEditData = tempData; + }, + }, + + CLEAR_VOLUME_EDIT_DATA: { + // ピッチ編集データを失くす。 + mutation(state, { trackId }) { + const track = getOrThrow(state.tracks, trackId); + track.pitchEditData = []; + }, + async action({ dispatch, commit }, { trackId }) { + commit("CLEAR_VOLUME_EDIT_DATA", { trackId }); + + dispatch("RENDER"); + }, + }, SET_PHRASES: { mutation(state, { phrases }) { @@ -1632,6 +1687,7 @@ export const singingStore = createPartialStore({ // 歌い方をコピーして、ピッチ編集を適用する singingGuide = structuredClone(toRaw(singingGuide)); applyPitchEdit(singingGuide, track.pitchEditData, editFrameRate); + applyVolumeEdit(singingGuide, track.pitchEditData, editFrameRate); const calculatedHash = await calculateSingingVoiceSourceHash({ singer: singerAndFrameRate.singer, @@ -2634,6 +2690,54 @@ export const singingCommandStore = transformCommandStore( dispatch("RENDER"); }, }, + COMMAND_SET_VOLUME_EDIT_DATA: { + mutation(draft, { volumeArray, startFrame, trackId }) { + singingStore.mutations.SET_VOLUME_EDIT_DATA(draft, { + volumeArray, + startFrame, + trackId, + }); + }, + action({ commit, dispatch }, { volumeArray, startFrame, trackId }) { + if (startFrame < 0) { + throw new Error("startFrame must be greater than or equal to 0."); + } + if (!isValidVolumeEditData(volumeArray)) { + throw new Error("The volume edit data is invalid."); + } + commit("COMMAND_SET_VOLUME_EDIT_DATA", { + volumeArray, + startFrame, + trackId, + }); + + dispatch("RENDER"); + }, + }, + COMMAND_ERASE_VOLUME_EDIT_DATA: { + mutation(draft, { startFrame, frameLength, trackId }) { + singingStore.mutations.ERASE_VOLUME_EDIT_DATA(draft, { + startFrame, + frameLength, + trackId, + }); + }, + action({ commit, dispatch }, { startFrame, frameLength, trackId }) { + if (startFrame < 0) { + throw new Error("startFrame must be greater than or equal to 0."); + } + if (frameLength < 1) { + throw new Error("frameLength must be at least 1."); + } + commit("COMMAND_ERASE_VOLUME_EDIT_DATA", { + startFrame, + frameLength, + trackId, + }); + + dispatch("RENDER"); + }, + }, COMMAND_INSERT_EMPTY_TRACK: { mutation(draft, { trackId, track, prevTrackId }) { diff --git a/src/store/type.ts b/src/store/type.ts index b5963db96b..80b073fb21 100644 --- a/src/store/type.ts +++ b/src/store/type.ts @@ -820,7 +820,7 @@ export type PhraseSource = { export const phraseSourceHashSchema = z.string().brand<"PhraseSourceHash">(); export type PhraseSourceHash = z.infer; -export type SequencerEditTarget = "NOTE" | "PITCH"; +export type SequencerEditTarget = "NOTE" | "PITCH" | "VOLUME"; export type SingingStoreState = { tpqn: number; // Ticks Per Quarter Note @@ -980,6 +980,24 @@ export type SingingStoreTypes = { action(payload: { trackId: TrackId }): void; }; + SET_VOLUME_EDIT_DATA: { + mutation: { volumeArray: number[]; startFrame: number; trackId: TrackId }; + action(payload: { + volumeArray: number[]; + startFrame: number; + trackId: TrackId; + }): void; + }; + + ERASE_VOLUME_EDIT_DATA: { + mutation: { startFrame: number; frameLength: number; trackId: TrackId }; + }; + + CLEAR_VOLUME_EDIT_DATA: { + mutation: { trackId: TrackId }; + action(payload: { trackId: TrackId }): void; + }; + SET_PHRASES: { mutation: { phrases: Map }; }; @@ -1327,6 +1345,24 @@ export type SingingCommandStoreTypes = { }): void; }; + COMMAND_SET_VOLUME_EDIT_DATA: { + mutation: { volumeArray: number[]; startFrame: number; trackId: TrackId }; + action(payload: { + volumeArray: number[]; + startFrame: number; + trackId: TrackId; + }): void; + }; + + COMMAND_ERASE_VOLUME_EDIT_DATA: { + mutation: { startFrame: number; frameLength: number; trackId: TrackId }; + action(payload: { + startFrame: number; + frameLength: number; + trackId: TrackId; + }): void; + }; + COMMAND_INSERT_EMPTY_TRACK: { mutation: { trackId: TrackId; From 75849d23579ee5aa928d29127518d4797e5e9d5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=92=E7=8C=AB=E5=A4=A7=E7=A6=8F?= <93469977+rokujyushi@users.noreply.github.com> Date: Sat, 12 Oct 2024 11:50:07 +0900 Subject: [PATCH 02/10] =?UTF-8?q?VOLUME=E3=82=B5=E3=83=9D=E3=83=BC?= =?UTF-8?q?=E3=83=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Sing/ScoreSequencer.vue | 4 ++-- src/components/Sing/SequencerVolume.vue | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/components/Sing/ScoreSequencer.vue b/src/components/Sing/ScoreSequencer.vue index 3388495e55..59aa31faac 100644 --- a/src/components/Sing/ScoreSequencer.vue +++ b/src/components/Sing/ScoreSequencer.vue @@ -74,7 +74,7 @@
(undefined); -const prevCursorPos = { frame: 0, frequency: 0 }; // 前のカーソル位置 + // 前のカーソル位置 +// const prevCursorPos = ref< +// | { prevCursorFrame: number, prevCursorFrequency: number,prevCursorVolume: number } +// | undefined +// >(undefined); +let prevCursorFrame: number = 0; +let prevCursorFrequency: number = 0; +let prevCursorVolume: number = 0; // 歌詞を編集中のノート const editingLyricNote = computed(() => { @@ -652,27 +662,27 @@ const previewDrawPitch = () => { ); } - if (cursorFrame === prevCursorPos.frame) { + if (cursorFrame === prevCursorFrame) { const i = cursorFrame - tempPitchEdit.startFrame; tempPitchEdit.data[i] = cursorFrequency; - } else if (cursorFrame < prevCursorPos.frame) { - for (let i = cursorFrame; i <= prevCursorPos.frame; i++) { + } else if (cursorFrame < prevCursorFrame) { + for (let i = cursorFrame; i <= prevCursorFrame; i++) { tempPitchEdit.data[i - tempPitchEdit.startFrame] = Math.exp( linearInterpolation( cursorFrame, Math.log(cursorFrequency), - prevCursorPos.frame, - Math.log(prevCursorPos.frequency), + prevCursorFrame, + Math.log(prevCursorFrequency), i, ), ); } } else { - for (let i = prevCursorPos.frame; i <= cursorFrame; i++) { + for (let i = prevCursorFrame; i <= cursorFrame; i++) { tempPitchEdit.data[i - tempPitchEdit.startFrame] = Math.exp( linearInterpolation( - prevCursorPos.frame, - Math.log(prevCursorPos.frequency), + prevCursorFrame, + Math.log(prevCursorFrequency), cursorFrame, Math.log(cursorFrequency), i, @@ -682,11 +692,44 @@ const previewDrawPitch = () => { } previewPitchEdit.value = tempPitchEdit; - prevCursorPos.frame = cursorFrame; - prevCursorPos.frequency = cursorFrequency; + prevCursorFrame = cursorFrame; + prevCursorFrequency = cursorFrequency; }; -// ピッチを描く処理を行う +// ドラッグした範囲のピッチ編集データを消去する処理を行う +const previewErasePitch = () => { + if (previewPitchEdit.value == undefined) { + throw new Error("previewPitchEdit.value is undefined."); + } + if (previewPitchEdit.value.type !== "erase") { + throw new Error("previewPitchEdit.value.type is not erase."); + } + const frameRate = editFrameRate.value; + const cursorBaseX = (scrollX.value + cursorX.value) / zoomX.value; + const cursorTicks = baseXToTick(cursorBaseX, tpqn.value); + const cursorSeconds = tickToSecond(cursorTicks, tempos.value, tpqn.value); + const cursorFrame = Math.round(cursorSeconds * frameRate); + if (cursorFrame < 0) { + return; + } + const tempPitchEdit = { ...previewPitchEdit.value }; + + if (tempPitchEdit.startFrame > cursorFrame) { + tempPitchEdit.frameLength += tempPitchEdit.startFrame - cursorFrame; + tempPitchEdit.startFrame = cursorFrame; + } + + const lastFrame = tempPitchEdit.startFrame + tempPitchEdit.frameLength - 1; + if (lastFrame < cursorFrame) { + tempPitchEdit.frameLength += cursorFrame - lastFrame; + } + + previewPitchEdit.value = tempPitchEdit; + prevCursorFrame = cursorFrame; +}; + + +// ボリュームを描く処理を行う const previewDrawVolume = () => { if (previewVolumeEdit.value == undefined) { throw new Error("previewVolumeEdit.value is undefined."); @@ -700,8 +743,9 @@ const previewDrawVolume = () => { const cursorTicks = baseXToTick(cursorBaseX, tpqn.value); const cursorSeconds = tickToSecond(cursorTicks, tempos.value, tpqn.value); const cursorFrame = Math.round(cursorSeconds * frameRate); - const cursorNoteNumber = baseYToNoteNumber(cursorBaseY, false); - const cursorFrequency = noteNumberToFrequency(cursorNoteNumber); + //const cursorNoteNumber = baseYToNoteNumber(cursorBaseY, false); + const cursorDecibel = viewYToDecibel(cursorY.value); + const cursorVolume = decibelToLinear(cursorDecibel); if (cursorFrame < 0) { return; } @@ -726,29 +770,29 @@ const previewDrawVolume = () => { ); } - if (cursorFrame === prevCursorPos.frame) { + if (cursorFrame === prevCursorFrame) { const i = cursorFrame - tempVolumeEdit.startFrame; - tempVolumeEdit.data[i] = cursorFrequency; - } else if (cursorFrame < prevCursorPos.frame) { - for (let i = cursorFrame; i <= prevCursorPos.frame; i++) { + tempVolumeEdit.data[i] = cursorVolume; + } else if (cursorFrame < prevCursorFrame) { + for (let i = cursorFrame; i <= prevCursorFrame; i++) { tempVolumeEdit.data[i - tempVolumeEdit.startFrame] = Math.exp( linearInterpolation( cursorFrame, - Math.log(cursorFrequency), - prevCursorPos.frame, - Math.log(prevCursorPos.frequency), + Math.log(cursorVolume), + prevCursorFrame, + Math.log(prevCursorVolume), i, ), ); } } else { - for (let i = prevCursorPos.frame; i <= cursorFrame; i++) { + for (let i = prevCursorFrame; i <= cursorFrame; i++) { tempVolumeEdit.data[i - tempVolumeEdit.startFrame] = Math.exp( linearInterpolation( - prevCursorPos.frame, - Math.log(prevCursorPos.frequency), + prevCursorFrame, + Math.log(prevCursorVolume), cursorFrame, - Math.log(cursorFrequency), + Math.log(cursorVolume), i, ), ); @@ -756,39 +800,8 @@ const previewDrawVolume = () => { } previewVolumeEdit.value = tempVolumeEdit; - prevCursorPos.frame = cursorFrame; - prevCursorPos.frequency = cursorFrequency; -}; -// ドラッグした範囲のピッチ編集データを消去する処理を行う -const previewErasePitch = () => { - if (previewPitchEdit.value == undefined) { - throw new Error("previewPitchEdit.value is undefined."); - } - if (previewPitchEdit.value.type !== "erase") { - throw new Error("previewPitchEdit.value.type is not erase."); - } - const frameRate = editFrameRate.value; - const cursorBaseX = (scrollX.value + cursorX.value) / zoomX.value; - const cursorTicks = baseXToTick(cursorBaseX, tpqn.value); - const cursorSeconds = tickToSecond(cursorTicks, tempos.value, tpqn.value); - const cursorFrame = Math.round(cursorSeconds * frameRate); - if (cursorFrame < 0) { - return; - } - const tempPitchEdit = { ...previewPitchEdit.value }; - - if (tempPitchEdit.startFrame > cursorFrame) { - tempPitchEdit.frameLength += tempPitchEdit.startFrame - cursorFrame; - tempPitchEdit.startFrame = cursorFrame; - } - - const lastFrame = tempPitchEdit.startFrame + tempPitchEdit.frameLength - 1; - if (lastFrame < cursorFrame) { - tempPitchEdit.frameLength += cursorFrame - lastFrame; - } - - previewPitchEdit.value = tempPitchEdit; - prevCursorPos.frame = cursorFrame; + prevCursorFrame = cursorFrame; + prevCursorVolume = cursorFrequency; }; const previewEraseVolume = () => { @@ -819,7 +832,7 @@ const previewEraseVolume = () => { } previewVolumeEdit.value = tempVolumeEdit; - prevCursorPos.frame = cursorFrame; + prevCursorFrame = cursorFrame; }; const preview = () => { @@ -977,8 +990,8 @@ const startPreview = (event: MouseEvent, mode: PreviewMode, note?: Note) => { } else { throw new Error("Unknown preview mode."); } - prevCursorPos.frame = cursorFrame; - prevCursorPos.frequency = cursorFrequency; + prevCursorFrame = cursorFrame; + prevCursorFrequency = cursorFrequency; } else if (editTarget.value === "VOLUME") { const frameRate = editFrameRate.value; const cursorTicks = baseXToTick(cursorBaseX, tpqn.value); @@ -1001,6 +1014,8 @@ const startPreview = (event: MouseEvent, mode: PreviewMode, note?: Note) => { } else { throw new Error("Unknown preview mode."); } + prevCursorFrame = cursorFrame; + prevCursorVolume = cursorFrequency; } else { throw new ExhaustiveError(editTarget.value); } diff --git a/src/sing/domain.ts b/src/sing/domain.ts index d3dc3b1311..3925e73748 100644 --- a/src/sing/domain.ts +++ b/src/sing/domain.ts @@ -552,8 +552,7 @@ export function applyVolumeEdit( const endFrame = Math.min(volumeEditData.length, singingGuideEndFrame); for (let i = startFrame; i < endFrame; i++) { const phoneme = framePhonemes[i - singingGuideStartFrame]; - const voiced = !unvoicedPhonemes.includes(phoneme); - if (voiced && volumeEditData[i] !== VALUE_INDICATING_NO_DATA) { + if (volumeEditData[i] !== VALUE_INDICATING_NO_DATA) { volume[i - singingGuideStartFrame] = volumeEditData[i]; } } diff --git a/src/sing/viewHelper.ts b/src/sing/viewHelper.ts index 122a52f8ae..dc6334e874 100644 --- a/src/sing/viewHelper.ts +++ b/src/sing/viewHelper.ts @@ -5,6 +5,9 @@ import { calculateHash } from "@/sing/utility"; const BASE_X_PER_QUARTER_NOTE = 120; const BASE_Y_PER_SEMITONE = 30; +const PIXELS_PER_DECIBEL = 6; +const DECIBEL_VIEW_OFFSET = 100; + export const ZOOM_X_MIN = 0.2; export const ZOOM_X_MAX = 1; export const ZOOM_X_STEP = 0.05; @@ -39,6 +42,14 @@ export function baseYToNoteNumber(baseY: number, integer = true) { : 127.5 - baseY / BASE_Y_PER_SEMITONE; } +export function viewYToDecibel(viewY: number) { + return -((viewY - DECIBEL_VIEW_OFFSET) / PIXELS_PER_DECIBEL); +} + +export function decibelToViewY(decibel: number) { + return -decibel * PIXELS_PER_DECIBEL + DECIBEL_VIEW_OFFSET; +} + export function getPitchFromNoteNumber(noteNumber: number) { const mapPitches = [ "C", diff --git a/src/store/singing.ts b/src/store/singing.ts index e444ca9741..f7f44bd1bd 100644 --- a/src/store/singing.ts +++ b/src/store/singing.ts @@ -749,12 +749,12 @@ export const singingStore = createPartialStore({ }, }, SET_VOLUME_EDIT_DATA: { - // ピッチ編集データをセットする。 - // track.pitchEditDataの長さが足りない場合は、伸長も行う。 + // ボリューム編集データをセットする。 + // track.volumeEditDataの長さが足りない場合は、伸長も行う。 mutation(state, { volumeArray, startFrame, trackId }) { const track = getOrThrow(state.tracks, trackId); - const pitchEditData = track.volumeEditData; - const tempData = [...pitchEditData]; + const volumeEditData = track.volumeEditData; + const tempData = [...volumeEditData]; const endFrame = startFrame + volumeArray.length; if (tempData.length < endFrame) { const valuesToPush = new Array(endFrame - tempData.length).fill( @@ -781,11 +781,11 @@ export const singingStore = createPartialStore({ ERASE_VOLUME_EDIT_DATA: { mutation(state, { startFrame, frameLength, trackId }) { const track = getOrThrow(state.tracks, trackId); - const pitchEditData = track.pitchEditData; - const tempData = [...pitchEditData]; + const volumeEditData = track.volumeEditData; + const tempData = [...volumeEditData]; const endFrame = Math.min(startFrame + frameLength, tempData.length); tempData.fill(VALUE_INDICATING_NO_DATA, startFrame, endFrame); - track.pitchEditData = tempData; + track.volumeEditData = tempData; }, }, @@ -1687,7 +1687,7 @@ export const singingStore = createPartialStore({ // 歌い方をコピーして、ピッチ編集を適用する singingGuide = structuredClone(toRaw(singingGuide)); applyPitchEdit(singingGuide, track.pitchEditData, editFrameRate); - applyVolumeEdit(singingGuide, track.pitchEditData, editFrameRate); + applyVolumeEdit(singingGuide, track.volumeEditData, editFrameRate); const calculatedHash = await calculateSingingVoiceSourceHash({ singer: singerAndFrameRate.singer, From 321f7ed9b3fe6ce23685c35b642766bfabb4ceba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=92=E7=8C=AB=E5=A4=A7=E7=A6=8F?= <93469977+rokujyushi@users.noreply.github.com> Date: Sat, 12 Oct 2024 13:11:32 +0900 Subject: [PATCH 04/10] =?UTF-8?q?=E7=A2=BA=E8=AA=8D=E6=BC=8F=E3=82=8C?= =?UTF-8?q?=E3=81=9F=E6=8C=87=E6=91=98=E9=83=A8=E5=88=86=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Sing/ScoreSequencer.vue | 8 ++++---- src/components/Sing/SequencerNote.vue | 8 ++++---- src/components/Sing/SequencerVolume.vue | 8 -------- 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/components/Sing/ScoreSequencer.vue b/src/components/Sing/ScoreSequencer.vue index 4b63f7a0bc..97888251f3 100644 --- a/src/components/Sing/ScoreSequencer.vue +++ b/src/components/Sing/ScoreSequencer.vue @@ -74,7 +74,7 @@ { } const volume = singingGuide.query.volume; - // 各フレームの音素の配列を生成する - const framePhonemes = convertToFramePhonemes(phonemes); - if (volume.length !== framePhonemes.length) { - throw new Error("volume.length and framePhonemes.length do not match."); - } - // 歌い方の開始フレームと終了フレームを計算する const singingGuideFrameLength = volume.length; const singingGuideStartFrame = Math.round( From 900b2084a68516dc5c1ba2cff5186032a034cf5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=92=E7=8C=AB=E5=A4=A7=E7=A6=8F?= <93469977+rokujyushi@users.noreply.github.com> Date: Sat, 12 Oct 2024 15:52:26 +0900 Subject: [PATCH 05/10] =?UTF-8?q?=E6=8C=87=E6=91=98=E9=83=A8=E5=88=86?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3=5F3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Sing/ScoreSequencer.vue | 6 ------ src/components/Sing/SequencerVolume.vue | 8 ++++---- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/components/Sing/ScoreSequencer.vue b/src/components/Sing/ScoreSequencer.vue index 97888251f3..1214dfb0e7 100644 --- a/src/components/Sing/ScoreSequencer.vue +++ b/src/components/Sing/ScoreSequencer.vue @@ -739,11 +739,9 @@ const previewDrawVolume = () => { } const frameRate = editFrameRate.value; const cursorBaseX = (scrollX.value + cursorX.value) / zoomX.value; - const cursorBaseY = (scrollY.value + cursorY.value) / zoomY.value; const cursorTicks = baseXToTick(cursorBaseX, tpqn.value); const cursorSeconds = tickToSecond(cursorTicks, tempos.value, tpqn.value); const cursorFrame = Math.round(cursorSeconds * frameRate); - //const cursorNoteNumber = baseYToNoteNumber(cursorBaseY, false); const cursorDecibel = viewYToDecibel(cursorY.value); const cursorVolume = decibelToLinear(cursorDecibel); if (cursorFrame < 0) { @@ -1097,10 +1095,6 @@ const endPreview = () => { if (previewVolumeEdit.value.data.length >= 2) { // 平滑化を行う let data = previewVolumeEdit.value.data; - data = data.map((value) => Math.log(value)); - applyGaussianFilter(data, 0.7); - data = data.map((value) => Math.exp(value)); - store.dispatch("COMMAND_SET_VOLUME_EDIT_DATA", { volumeArray: data, startFrame: previewVolumeEdit.value.startFrame, diff --git a/src/components/Sing/SequencerVolume.vue b/src/components/Sing/SequencerVolume.vue index c03dfa00aa..f5481b687a 100644 --- a/src/components/Sing/SequencerVolume.vue +++ b/src/components/Sing/SequencerVolume.vue @@ -9,11 +9,13 @@ import AsyncLock from "async-lock"; import { useStore } from "@/store"; import { VALUE_INDICATING_NO_DATA, - convertToFramePhonemes, linearToDecibel, secondToTick, } from "@/sing/domain"; import { + DECIBEL_VIEW_OFFSET, + PIXELS_PER_DECIBEL, + decibelToViewY, VolumeData, VolumeDataHash, calculateVolumeDataHash, @@ -105,9 +107,7 @@ const updateLineStrips = (volumeLine: VolumeLine) => { const tpqn = store.state.tpqn; const canvasWidthValue = canvasWidth; const zoomX = store.state.sequencerZoomX; - const zoomY = store.state.sequencerZoomY; const offsetX = props.offsetX; - const offsetY = props.offsetY; const removedLineStrips: LineStrip[] = []; @@ -179,7 +179,7 @@ const updateLineStrips = (volumeLine: VolumeLine) => { continue; } const db = linearToDecibel(linear); - const y = db * -10 * zoomY + 250;//- ; + const y = decibelToViewY(db);//- ; lineStrip.setPoint(i, x, y); } lineStrip.update(); From 48e3da621bbe8c22ae8f82d3ee10c3f91aeaf719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=92=E7=8C=AB=E5=A4=A7=E7=A6=8F?= <93469977+rokujyushi@users.noreply.github.com> Date: Sun, 10 Nov 2024 01:04:57 +0900 Subject: [PATCH 06/10] =?UTF-8?q?=E3=83=AC=E3=83=93=E3=83=A5=E3=83=BC?= =?UTF-8?q?=E6=8C=87=E6=91=98=E7=AE=87=E6=89=80=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Sing/ScoreSequencer.vue | 20 ++++------- src/components/Sing/SequencerVolume.vue | 36 ++++++++----------- .../Sing/ToolBar/EditTargetSwicher.vue | 4 ++- src/domain/project/schema.ts | 2 +- src/sing/domain.ts | 2 -- src/store/singing.ts | 15 +++++--- 6 files changed, 35 insertions(+), 44 deletions(-) diff --git a/src/components/Sing/ScoreSequencer.vue b/src/components/Sing/ScoreSequencer.vue index 1214dfb0e7..a3a020d079 100644 --- a/src/components/Sing/ScoreSequencer.vue +++ b/src/components/Sing/ScoreSequencer.vue @@ -198,7 +198,6 @@ import { noteNumberToBaseY, baseYToNoteNumber, viewYToDecibel, - decibelToViewY, keyInfos, getDoremiFromNoteNumber, ZOOM_X_MIN, @@ -429,14 +428,10 @@ const previewVolumeEdit = ref< | { type: "erase"; startFrame: number; frameLength: number } | undefined >(undefined); - // 前のカーソル位置 -// const prevCursorPos = ref< -// | { prevCursorFrame: number, prevCursorFrequency: number,prevCursorVolume: number } -// | undefined -// >(undefined); -let prevCursorFrame: number = 0; -let prevCursorFrequency: number = 0; -let prevCursorVolume: number = 0; +// 前のカーソル位置 +let prevCursorFrame = 0; +let prevCursorFrequency = 0; +let prevCursorVolume = 0; // 歌詞を編集中のノート const editingLyricNote = computed(() => { @@ -728,7 +723,6 @@ const previewErasePitch = () => { prevCursorFrame = cursorFrame; }; - // ボリュームを描く処理を行う const previewDrawVolume = () => { if (previewVolumeEdit.value == undefined) { @@ -757,7 +751,7 @@ const previewDrawVolume = () => { tempVolumeEdit.data = new Array(numOfFramesToUnshift) .fill(0) .concat(tempVolumeEdit.data); - tempVolumeEdit.startFrame = cursorFrame; + tempVolumeEdit.startFrame = cursorFrame; } const lastFrame = tempVolumeEdit.startFrame + tempVolumeEdit.data.length - 1; @@ -1094,7 +1088,7 @@ const endPreview = () => { // 1フレームの変更はピッチ編集ラインとして表示されないので、無視する if (previewVolumeEdit.value.data.length >= 2) { // 平滑化を行う - let data = previewVolumeEdit.value.data; + const data = [...previewVolumeEdit.value.data]; store.dispatch("COMMAND_SET_VOLUME_EDIT_DATA", { volumeArray: data, startFrame: previewVolumeEdit.value.startFrame, @@ -1765,7 +1759,7 @@ const contextMenuData = computed(() => { pointer-events: none; } -.sequencer-volume,.sequencer-pitch { +.sequencer-volume .sequencer-pitch { grid-row: 2; grid-column: 2; } diff --git a/src/components/Sing/SequencerVolume.vue b/src/components/Sing/SequencerVolume.vue index f5481b687a..15dd85f17e 100644 --- a/src/components/Sing/SequencerVolume.vue +++ b/src/components/Sing/SequencerVolume.vue @@ -13,8 +13,6 @@ import { secondToTick, } from "@/sing/domain"; import { - DECIBEL_VIEW_OFFSET, - PIXELS_PER_DECIBEL, decibelToViewY, VolumeData, VolumeDataHash, @@ -40,7 +38,6 @@ type VolumeLine = { const props = defineProps<{ offsetX: number; - offsetY: number; previewVolumeEdit?: | { type: "draw"; data: number[]; startFrame: number } | { type: "erase"; startFrame: number; frameLength: number }; @@ -111,7 +108,7 @@ const updateLineStrips = (volumeLine: VolumeLine) => { const removedLineStrips: LineStrip[] = []; - // 無くなったピッチデータを調べて、そのピッチデータに対応するLineStripを削除する + // 無くなったボリュームデータを調べて、そのボリュームデータに対応するLineStripを削除する for (const [key, lineStrip] of volumeLine.lineStripMap) { if (!volumeLine.volumeDataMap.has(key)) { stage.removeChild(lineStrip.displayObject); @@ -120,7 +117,7 @@ const updateLineStrips = (volumeLine: VolumeLine) => { } } - // ピッチデータに対応するLineStripが無かったら作成する + // ボリュームデータに対応するLineStripが無かったら作成する for (const [key, volumeData] of volumeLine.volumeDataMap) { if (volumeLine.lineStripMap.has(key)) { continue; @@ -175,11 +172,8 @@ const updateLineStrips = (volumeLine: VolumeLine) => { const baseX = tickToBaseX(ticks, tpqn); const x = baseX * zoomX - offsetX; const linear = volumeData.data[i]; - if(Number.isNaN(linear)||linear === undefined){ - continue; - } const db = linearToDecibel(linear); - const y = decibelToViewY(db);//- ; + const y = decibelToViewY(db); lineStrip.setPoint(i, x, y); } lineStrip.update(); @@ -194,7 +188,7 @@ const render = () => { throw new Error("stage is undefined."); } - // シンガーが未設定の場合はピッチラインをすべて非表示にして終了 + // シンガーが未設定の場合はボリュームラインをすべて非表示にして終了 const singer = store.getters.SELECTED_TRACK.singer; if (!singer) { for (const lineStrip of originalVolumeLine.lineStripMap.values()) { @@ -207,14 +201,17 @@ const render = () => { return; } - // ピッチラインのLineStripを更新する + // ボリュームラインのLineStripを更新する updateLineStrips(originalVolumeLine); updateLineStrips(volumeEditLine); renderer.render(stage); }; -const toVolumeData = (framewiseData: number[], frameRate: number): VolumeData => { +const toVolumeData = ( + framewiseData: number[], + frameRate: number, +): VolumeData => { const data = framewiseData; const ticksArray: number[] = []; for (let i = 0; i < data.length; i++) { @@ -259,9 +256,9 @@ const setVolumeDataToVolumeLine = async ( const generateOriginalVolumeData = () => { //const unvoicedPhonemes = UNVOICED_PHONEMES; - const frameRate = editFrameRate.value; // f0(元のピッチ)は編集フレームレートで表示する + const frameRate = editFrameRate.value; // volumeは編集フレームレートで表示する - // 選択中のトラックで使われている歌い方のf0を結合してピッチデータを生成する + // 選択中のトラックで使われている歌い方のvolumeを結合してボリュームデータを生成する const tempData = []; for (const singingGuide of singingGuidesInSelectedTrack.value) { // TODO: 補間を行うようにする @@ -305,7 +302,7 @@ const generateVolumeEditData = () => { const frameRate = editFrameRate.value; const tempData = [...volumeEditData.value]; - // プレビュー中のピッチ編集があれば、適用する + // プレビュー中のボリューム編集があれば、適用する if (previewVolumeEdit.value != undefined) { const previewVolumeEditType = previewVolumeEdit.value.type; if (previewVolumeEditType === "draw") { @@ -380,12 +377,7 @@ watch( ); watch( - () => [ - store.state.sequencerZoomX, - store.state.sequencerZoomY, - props.offsetX, - props.offsetY, - ], + () => [store.state.sequencerZoomX, store.state.sequencerZoomY, props.offsetX], () => { renderInNextFrame = true; }, @@ -415,7 +407,7 @@ onMountedOrActivated(() => { stage = new PIXI.Container(); // webGLVersionをチェックする - // 2未満の場合、ピッチの表示ができないのでエラーとしてロギングする + // 2未満の場合、ボリュームの表示ができないのでエラーとしてロギングする const webGLVersion = renderer.context.webGLVersion; if (webGLVersion < 2) { error(`webGLVersion is less than 2. webGLVersion: ${webGLVersion}`); diff --git a/src/components/Sing/ToolBar/EditTargetSwicher.vue b/src/components/Sing/ToolBar/EditTargetSwicher.vue index 34e79a161a..5da40cddef 100644 --- a/src/components/Sing/ToolBar/EditTargetSwicher.vue +++ b/src/components/Sing/ToolBar/EditTargetSwicher.vue @@ -38,7 +38,9 @@ diff --git a/src/domain/project/schema.ts b/src/domain/project/schema.ts index 8947192d05..e4a218d7ff 100644 --- a/src/domain/project/schema.ts +++ b/src/domain/project/schema.ts @@ -91,7 +91,7 @@ export const trackSchema = z.object({ volumeRangeAdjustment: z.number(), // 声量調整量 notes: z.array(noteSchema), pitchEditData: z.array(z.number()), // 値の単位はHzで、データが無いところはVALUE_INDICATING_NO_DATAの値 - volumeEditData: z.array(z.number()), // 値の単位はdbで、データが無いところはVALUE_INDICATING_NO_DATAの値 + volumeEditData: z.array(z.number()), // 値の単位はdBで、データが無いところはVALUE_INDICATING_NO_DATAの値 solo: z.boolean(), mute: z.boolean(), diff --git a/src/sing/domain.ts b/src/sing/domain.ts index 3925e73748..0b9588b7e0 100644 --- a/src/sing/domain.ts +++ b/src/sing/domain.ts @@ -530,7 +530,6 @@ export function applyVolumeEdit( "The frame rate between the singing guide and the edit data does not match.", ); } - const unvoicedPhonemes = UNVOICED_PHONEMES; const volume = singingGuide.query.volume; const phonemes = singingGuide.query.phonemes; @@ -551,7 +550,6 @@ export function applyVolumeEdit( const startFrame = Math.max(0, singingGuideStartFrame); const endFrame = Math.min(volumeEditData.length, singingGuideEndFrame); for (let i = startFrame; i < endFrame; i++) { - const phoneme = framePhonemes[i - singingGuideStartFrame]; if (volumeEditData[i] !== VALUE_INDICATING_NO_DATA) { volume[i - singingGuideStartFrame] = volumeEditData[i]; } diff --git a/src/store/singing.ts b/src/store/singing.ts index f7f44bd1bd..c6769165c8 100644 --- a/src/store/singing.ts +++ b/src/store/singing.ts @@ -770,7 +770,7 @@ export const singingStore = createPartialStore({ throw new Error("startFrame must be greater than or equal to 0."); } if (!isValidVolumeEditData(volumeArray)) { - throw new Error("The pitch edit data is invalid."); + throw new Error("The volume edit data is invalid."); } commit("SET_VOLUME_EDIT_DATA", { volumeArray, startFrame, trackId }); @@ -790,10 +790,10 @@ export const singingStore = createPartialStore({ }, CLEAR_VOLUME_EDIT_DATA: { - // ピッチ編集データを失くす。 + // ボリューム編集データを失くす。 mutation(state, { trackId }) { const track = getOrThrow(state.tracks, trackId); - track.pitchEditData = []; + track.volumeEditData = []; }, async action({ dispatch, commit }, { trackId }) { commit("CLEAR_VOLUME_EDIT_DATA", { trackId }); @@ -1684,10 +1684,14 @@ export const singingStore = createPartialStore({ phrase.singingGuideKey, ); - // 歌い方をコピーして、ピッチ編集を適用する + // 歌い方をコピーして、ピッチ、ボリューム編集編集を適用する singingGuide = structuredClone(toRaw(singingGuide)); applyPitchEdit(singingGuide, track.pitchEditData, editFrameRate); - applyVolumeEdit(singingGuide, track.volumeEditData, editFrameRate); + applyVolumeEdit( + singingGuide, + track.volumeEditData, + editFrameRate, + ); const calculatedHash = await calculateSingingVoiceSourceHash({ singer: singerAndFrameRate.singer, @@ -1898,6 +1902,7 @@ export const singingStore = createPartialStore({ // ピッチ編集を適用する applyPitchEdit(singingGuide, track.pitchEditData, editFrameRate); + applyVolumeEdit(singingGuide, track.volumeEditData, editFrameRate); // 歌声のキャッシュがあれば取得し、なければ音声合成を行う From 5b88f7574d7054d7f006a49e8d458a3cfc34a208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=92=E7=8C=AB=E5=A4=A7=E7=A6=8F?= <93469977+rokujyushi@users.noreply.github.com> Date: Sun, 10 Nov 2024 20:10:27 +0900 Subject: [PATCH 07/10] =?UTF-8?q?PreviewMode=E3=81=AB=E9=9F=B3=E9=87=8F?= =?UTF-8?q?=E7=B7=A8=E9=9B=86=E3=83=A2=E3=83=BC=E3=83=89=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/sing/viewHelper.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/sing/viewHelper.ts b/src/sing/viewHelper.ts index 0f572a6372..7282488374 100644 --- a/src/sing/viewHelper.ts +++ b/src/sing/viewHelper.ts @@ -166,7 +166,9 @@ export type PreviewMode = | "RESIZE_NOTE_RIGHT" | "RESIZE_NOTE_LEFT" | "DRAW_PITCH" - | "ERASE_PITCH"; + | "ERASE_PITCH" + | "DRAW_VOLUME" + | "ERASE_VOLUME"; export function getButton(event: MouseEvent): MouseButton { // macOSの場合、Ctrl+クリックは右クリック From 0ab2db171da1bb578012bd09ea79623f35e43a4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=92=E7=8C=AB=E5=A4=A7=E7=A6=8F?= <93469977+rokujyushi@users.noreply.github.com> Date: Mon, 11 Nov 2024 02:05:10 +0900 Subject: [PATCH 08/10] =?UTF-8?q?=E3=83=A1=E3=82=A4=E3=83=B3=E3=81=AE?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E3=81=AB=E5=90=88=E3=82=8F=E3=81=9B=E3=81=A6?= =?UTF-8?q?=E9=9F=B3=E5=A3=B0=E7=B7=A8=E9=9B=86=E3=82=92=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Sing/ScoreSequencer.vue | 6 +- src/components/Sing/SequencerNote.vue | 6 +- src/components/Sing/SequencerVolume.vue | 64 +++++++++++++------ .../Sing/ToolBar/EditTargetSwicher.vue | 34 +++++----- src/store/singing.ts | 20 ++++-- 5 files changed, 86 insertions(+), 44 deletions(-) diff --git a/src/components/Sing/ScoreSequencer.vue b/src/components/Sing/ScoreSequencer.vue index 01f312f91f..2acca90770 100644 --- a/src/components/Sing/ScoreSequencer.vue +++ b/src/components/Sing/ScoreSequencer.vue @@ -10,6 +10,7 @@
{ prevCursorPos.frame = cursorFrame; prevCursorPos.volume = cursorVolume; setCursorState(CursorState.DRAW); -} +}; + const previewEraseVolume = () => { if (previewVolumeEdit.value == undefined) { throw new Error("previewVolumeEdit.value is undefined."); @@ -735,7 +737,7 @@ const previewEraseVolume = () => { if (previewVolumeEdit.value.type !== "erase") { throw new Error("previewVolumeEdit.value.type is not erase."); } - const frameRate = editorFrameRate.value;; + const frameRate = editorFrameRate.value; const cursorBaseX = (scrollX.value + cursorX.value) / zoomX.value; const cursorTicks = baseXToTick(cursorBaseX, tpqn.value); const cursorSeconds = tickToSecond(cursorTicks, tempos.value, tpqn.value); diff --git a/src/components/Sing/SequencerNote.vue b/src/components/Sing/SequencerNote.vue index 0b111cb19a..5f82a03b33 100644 --- a/src/components/Sing/SequencerNote.vue +++ b/src/components/Sing/SequencerNote.vue @@ -177,12 +177,14 @@ const classes = computed(() => { return { "edit-note": editTargetIsNote.value, // ノート編集モード "edit-pitch": editTargetIsPitch.value, // ピッチ編集モード + "edit-volume": editTargetIsVolume.value, // ボリューム編集モード selected: props.isSelected, // このノートが選択中 preview: props.isPreview, // なんらかのプレビュー中 "preview-lyric": props.previewLyric != undefined, // 歌詞プレビュー中 overlapping: hasOverlappingError.value, // ノートが重なっている "invalid-phrase": hasPhraseError.value, // フレーズ生成エラー "below-pitch": editTargetIsPitch.value, // ピッチ編集中 + "below-volume": editTargetIsVolume.value, // ボリューム編集中 adding: props.isPreview && props.previewMode === "ADD_NOTE", // ノート追加中 "resizing-right": props.isPreview && props.previewMode === "RESIZE_NOTE_RIGHT", // 右リサイズ中 @@ -449,7 +451,7 @@ const onLeftEdgeMouseDown = (event: MouseEvent) => { } /* ピッチ編集モード */ -.note.edit-pitch { +.note.edit-pitch.edit-volume { // ノートバー .note-bar { background-color: var(--scheme-color-sing-note-bar-below-pitch-container); @@ -526,7 +528,7 @@ const onLeftEdgeMouseDown = (event: MouseEvent) => { } // ピッチ編集モード -.note-lyric.edit-pitch { +.note-lyric.edit-pitch.edit-volume { color: oklch(from var(--scheme-color-on-surface-variant) l c h / 0.8); z-index: vars.$z-index-sing-note-lyric; @include text-outline(var(--scheme-color-surface-variant)); diff --git a/src/components/Sing/SequencerVolume.vue b/src/components/Sing/SequencerVolume.vue index 15dd85f17e..180b90bd8b 100644 --- a/src/components/Sing/SequencerVolume.vue +++ b/src/components/Sing/SequencerVolume.vue @@ -3,7 +3,7 @@