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"
>ピッチ編集
{{ !isMac ? "Ctrl" : "Cmd" }}+クリックで消去
+
+ ボリューム倍率編集
{{ !isMac ? "Ctrl" : "Cmd" }}+クリックで消去
+
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 @@