diff --git a/src/components/Sing/ScoreSequencer.vue b/src/components/Sing/ScoreSequencer.vue index f814b1549b..32ca183f20 100644 --- a/src/components/Sing/ScoreSequencer.vue +++ b/src/components/Sing/ScoreSequencer.vue @@ -17,6 +17,7 @@ :class="{ 'edit-note': editTarget === 'NOTE', 'edit-pitch': editTarget === 'PITCH', + 'edit-volume': editTarget === 'VOLUME', previewing: nowPreviewing, [cursorClass]: true, }" @@ -84,6 +85,17 @@ :offsetY="scrollY" :previewPitchEdit /> +
(undefined); -const prevCursorPos = { frame: 0, frequency: 0 }; // 前のカーソル位置 +// ボリューム編集のプレビュー +const previewVolumeEdit = ref< + | { type: "draw"; data: number[]; startFrame: number } + | { type: "erase"; startFrame: number; frameLength: number } + | undefined +>(undefined); +const prevCursorPos = { frame: 0, frequency: 0, volume: 0 }; // 前のカーソル位置 // 歌詞を編集中のノート const editingLyricNote = computed(() => { @@ -729,6 +750,112 @@ const previewErasePitch = () => { setCursorState(CursorState.ERASE); }; +// ボリュームを描く処理を行う +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 = editorFrameRate.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); + const cursorDecibel = viewYToDecibel(cursorY.value); + const cursorVolume = decibelToLinear(cursorDecibel); + 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] = cursorVolume; + } else if (cursorFrame < prevCursorPos.frame) { + for (let i = cursorFrame; i <= prevCursorPos.frame; i++) { + tempVolumeEdit.data[i - tempVolumeEdit.startFrame] = Math.exp( + linearInterpolation( + cursorFrame, + Math.log(cursorVolume), + prevCursorPos.frame, + Math.log(prevCursorPos.volume), + i, + ), + ); + } + } else { + for (let i = prevCursorPos.frame; i <= cursorFrame; i++) { + tempVolumeEdit.data[i - tempVolumeEdit.startFrame] = Math.exp( + linearInterpolation( + prevCursorPos.frame, + Math.log(prevCursorPos.volume), + cursorFrame, + Math.log(cursorVolume), + i, + ), + ); + } + } + + previewVolumeEdit.value = tempVolumeEdit; + prevCursorPos.frame = cursorFrame; + prevCursorPos.volume = cursorVolume; + setCursorState(CursorState.DRAW); +}; + +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 = editorFrameRate.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; + setCursorState(CursorState.ERASE); +}; + const preview = () => { if (executePreviewProcess.value) { if (previewMode.value === "ADD_NOTE") { @@ -749,6 +876,12 @@ const preview = () => { if (previewMode.value === "ERASE_PITCH") { previewErasePitch(); } + if (previewMode.value === "DRAW_VOLUME") { + previewDrawVolume(); + } + if (previewMode.value === "ERASE_VOLUME") { + previewEraseVolume(); + } executePreviewProcess.value = false; } previewRequestId = requestAnimationFrame(preview); @@ -880,6 +1013,30 @@ const startPreview = (event: MouseEvent, mode: PreviewMode, note?: Note) => { } prevCursorPos.frame = cursorFrame; prevCursorPos.frequency = cursorFrequency; + } else if (editTarget.value === "VOLUME") { + const frameRate = editorFrameRate.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."); + } + prevCursorPos.frame = cursorFrame; + prevCursorPos.frequency = cursorFrequency; } else { throw new ExhaustiveError(editTarget.value); } @@ -948,6 +1105,33 @@ 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) { + // 平滑化を行う + const data = [...previewVolumeEdit.value.data]; + void store.actions.COMMAND_SET_VOLUME_EDIT_DATA({ + volumeArray: data, + startFrame: previewVolumeEdit.value.startFrame, + trackId: selectedTrackId.value, + }); + } + } else if (previewVolumeEditType === "erase") { + void store.actions.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); } @@ -1027,6 +1211,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); } @@ -1589,6 +1781,7 @@ const contextMenuData = computed(() => { pointer-events: none; } +.sequencer-volume, .sequencer-pitch { grid-row: 2; grid-column: 2; diff --git a/src/components/Sing/SequencerNote.vue b/src/components/Sing/SequencerNote.vue index 849559b0c3..f7a0dff36e 100644 --- a/src/components/Sing/SequencerNote.vue +++ b/src/components/Sing/SequencerNote.vue @@ -149,6 +149,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; }); @@ -174,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", // 右リサイズ中 @@ -381,6 +386,29 @@ const onLeftEdgeMouseDown = (event: MouseEvent) => { } } + // 右リサイズ中 + &.resizing-right { + .note-edge.right { + background-color: var(--scheme-color-sing-note-bar-selected-border); + } + } + + // 左リサイズ中 + &.resizing-left { + .note-edge.left { + background-color: var(--scheme-color-sing-note-bar-selected-border); + } + } + + // 歌詞プレビュー中 + &.preview-lyric { + .note-bar { + background-color: var(--scheme-color-sing-note-bar-preview-container); + border-color: var(--scheme-color-sing-note-bar-preview-border); + outline-color: var(--scheme-color-sing-note-bar-preview-outline); + } + } + // エラー状態 &.overlapping, &.invalid-phrase { @@ -423,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); @@ -500,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 new file mode 100644 index 0000000000..180b90bd8b --- /dev/null +++ b/src/components/Sing/SequencerVolume.vue @@ -0,0 +1,494 @@ + + + + + diff --git a/src/components/Sing/ToolBar/EditTargetSwicher.vue b/src/components/Sing/ToolBar/EditTargetSwicher.vue index 99b0371975..82225185cd 100644 --- a/src/components/Sing/ToolBar/EditTargetSwicher.vue +++ b/src/components/Sing/ToolBar/EditTargetSwicher.vue @@ -37,6 +37,25 @@ ピッチ編集
{{ !isMac ? "Ctrl" : "Cmd" }}+クリックで消去 + + + + + + ボリューム倍率編集
{{ !isMac ? "Ctrl" : "Cmd" }}+クリックで消去 +
+
diff --git a/src/domain/project/schema.ts b/src/domain/project/schema.ts index eba32d5989..b8933e76ee 100644 --- a/src/domain/project/schema.ts +++ b/src/domain/project/schema.ts @@ -92,6 +92,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 faf7230042..82973b88c8 100644 --- a/src/sing/domain.ts +++ b/src/sing/domain.ts @@ -375,6 +375,7 @@ export function createDefaultTrack(): Track { volumeRangeAdjustment: 0, notes: [], pitchEditData: [], + volumeEditData: [], solo: false, mute: false, @@ -417,6 +418,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 calculatePhraseKey = async (phraseSource: PhraseSource) => { const hash = await calculateHash(phraseSource); return PhraseKey(hash); @@ -534,6 +543,45 @@ export function applyPitchEdit( } } +export function applyVolumeEdit( + phraseQuery: EditorFrameAudioQuery, + phraseStartTime: number, + volumeEditData: number[], + editorFrameRate: number, +) { + // フレーズのクエリのフレームレートとエディターのフレームレートが一致しない場合はエラー + // TODO: 補間するようにする + if (phraseQuery.frameRate !== editorFrameRate) { + throw new Error( + "The frame rate between the phrase query and the editor does not match.", + ); + } + const volume = phraseQuery.volume; + const phonemes = phraseQuery.phonemes; + + // 各フレームの音素の配列を生成する + const framePhonemes = convertToFramePhonemes(phonemes); + if (volume.length !== framePhonemes.length) { + throw new Error("volume.length and framePhonemes.length do not match."); + } + + // 歌い方の開始フレームと終了フレームを計算する + const phraseQueryFrameLength = volume.length; + const phraseQueryStartFrame = Math.round( + phraseStartTime * phraseQuery.frameRate, + ); + const phraseQueryEndFrame = phraseQueryStartFrame + phraseQueryFrameLength; + + // ボリューム編集をvolumeに適用する + const startFrame = Math.max(0, phraseQueryStartFrame); + const endFrame = Math.min(volumeEditData.length, phraseQueryEndFrame); + for (let i = startFrame; i < endFrame; i++) { + if (volumeEditData[i] !== VALUE_INDICATING_NO_DATA) { + volume[i - phraseQueryStartFrame] = volumeEditData[i]; + } + } +} + /** * 文字列をモーラと非モーラに分割する。長音は展開される。連続する非モーラはまとめる。 * 例:"カナー漢字" -> ["カ", "ナ", "ア", "漢字"] diff --git a/src/sing/viewHelper.ts b/src/sing/viewHelper.ts index 0f86b0d1ed..e007aa7522 100644 --- a/src/sing/viewHelper.ts +++ b/src/sing/viewHelper.ts @@ -6,6 +6,9 @@ import { isMac } from "@/helpers/platform"; 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.15; export const ZOOM_X_MAX = 2; export const ZOOM_X_STEP = 0.05; @@ -40,6 +43,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", @@ -133,6 +144,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 type PreviewMode = @@ -142,7 +167,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+クリックは右クリック diff --git a/src/store/singing.ts b/src/store/singing.ts index 611fe2ce22..28f9632b73 100644 --- a/src/store/singing.ts +++ b/src/store/singing.ts @@ -70,6 +70,7 @@ import { tickToSecond, VALUE_INDICATING_NO_DATA, isValidPitchEditData, + isValidVolumeEditData, calculatePhraseKey, isValidTempos, isValidTimeSignatures, @@ -87,6 +88,7 @@ import { shouldPlayTracks, decibelToLinear, applyPitchEdit, + applyVolumeEdit, } from "@/sing/domain"; import { getOverlappingNoteIds } from "@/sing/storeHelper"; import { @@ -1214,6 +1216,60 @@ export const singingStore = createPartialStore({ }, }, + SET_VOLUME_EDIT_DATA: { + // ボリューム編集データをセットする。 + // track.volumeEditDataの長さが足りない場合は、伸長も行う。 + mutation(state, { volumeArray, startFrame, trackId }) { + const track = getOrThrow(state.tracks, trackId); + const volumeEditData = track.volumeEditData; + const tempData = [...volumeEditData]; + 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({ actions, mutations }, { 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."); + } + mutations.SET_VOLUME_EDIT_DATA({ volumeArray, startFrame, trackId }); + + void actions.RENDER(); + }, + }, + + ERASE_VOLUME_EDIT_DATA: { + mutation(state, { startFrame, frameLength, trackId }) { + const track = getOrThrow(state.tracks, trackId); + const volumeEditData = track.volumeEditData; + const tempData = [...volumeEditData]; + const endFrame = Math.min(startFrame + frameLength, tempData.length); + tempData.fill(VALUE_INDICATING_NO_DATA, startFrame, endFrame); + track.volumeEditData = tempData; + }, + }, + + CLEAR_VOLUME_EDIT_DATA: { + // ボリューム編集データを失くす。 + mutation(state, { trackId }) { + const track = getOrThrow(state.tracks, trackId); + track.volumeEditData = []; + }, + async action({ actions, mutations }, { trackId }) { + mutations.CLEAR_VOLUME_EDIT_DATA({ trackId }); + + void actions.RENDER(); + }, + }, + SET_PHRASES: { mutation(state, { phrases }) { state.phrases = phrases; @@ -2058,6 +2114,12 @@ export const singingStore = createPartialStore({ track.pitchEditData, context.snapshot.editorFrameRate, ); + applyVolumeEdit( + clonedQuery, + phrase.startTime, + track.volumeEditData, + context.snapshot.editorFrameRate, + ); return { engineId: track.singer.engineId, engineFrameRate: query.frameRate, @@ -2203,6 +2265,12 @@ export const singingStore = createPartialStore({ context.snapshot.editorFrameRate, ); clonedQuery.volume = clonedSingingVolume; + applyVolumeEdit( + clonedQuery, + phrase.startTime, + track.volumeEditData, + context.snapshot.editorFrameRate, + ); return { singer: track.singer, queryForSingingVoiceSynthesis: clonedQuery, @@ -3414,6 +3482,54 @@ export const singingCommandStore = transformCommandStore( void actions.RENDER(); }, }, + COMMAND_SET_VOLUME_EDIT_DATA: { + mutation(draft, { volumeArray, startFrame, trackId }) { + singingStore.mutations.SET_VOLUME_EDIT_DATA(draft, { + volumeArray, + startFrame, + trackId, + }); + }, + action({ mutations, actions }, { 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."); + } + mutations.COMMAND_SET_VOLUME_EDIT_DATA({ + volumeArray, + startFrame, + trackId, + }); + + void actions.RENDER(); + }, + }, + COMMAND_ERASE_VOLUME_EDIT_DATA: { + mutation(draft, { startFrame, frameLength, trackId }) { + singingStore.mutations.ERASE_VOLUME_EDIT_DATA(draft, { + startFrame, + frameLength, + trackId, + }); + }, + action({ mutations, actions }, { 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."); + } + mutations.COMMAND_ERASE_VOLUME_EDIT_DATA({ + startFrame, + frameLength, + trackId, + }); + + void actions.RENDER(); + }, + }, COMMAND_INSERT_EMPTY_TRACK: { mutation(draft, { trackId, track, prevTrackId }) { diff --git a/src/store/type.ts b/src/store/type.ts index eebfd77fcc..8fd7976510 100644 --- a/src/store/type.ts +++ b/src/store/type.ts @@ -838,7 +838,7 @@ const phraseKeySchema = z.string().brand<"PhraseKey">(); export type PhraseKey = z.infer; export const PhraseKey = (id: string): PhraseKey => phraseKeySchema.parse(id); -export type SequencerEditTarget = "NOTE" | "PITCH"; +export type SequencerEditTarget = "NOTE" | "PITCH" | "VOLUME"; export type TrackParameters = { gain: boolean; @@ -1006,6 +1006,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 }; }; @@ -1409,6 +1427,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;