Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP:音量編集機能の追加 #2369

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
195 changes: 194 additions & 1 deletion src/components/Sing/ScoreSequencer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
:class="{
'edit-note': editTarget === 'NOTE',
'edit-pitch': editTarget === 'PITCH',
'edit-volume': editTarget === 'VOLUME',
previewing: nowPreviewing,
[cursorClass]: true,
}"
Expand Down Expand Up @@ -84,6 +85,17 @@
:offsetY="scrollY"
:previewPitchEdit
/>
<SequencerVolume
v-if="editTarget === 'VOLUME'"
class="sequencer-volume"
:style="{
marginRight: `${scrollBarWidth}px`,
marginBottom: `${scrollBarWidth}px`,
}"
:offsetX="scrollX"
:offsetY="scrollY"
:previewVolumeEdit
/>
<div
class="sequencer-overlay"
:style="{
Expand Down Expand Up @@ -181,6 +193,7 @@ import {
getNoteDuration,
getStartTicksOfPhrase,
noteNumberToFrequency,
decibelToLinear,
tickToSecond,
} from "@/sing/domain";
import {
Expand All @@ -189,6 +202,7 @@ import {
baseXToTick,
noteNumberToBaseY,
baseYToNoteNumber,
viewYToDecibel,
keyInfos,
getDoremiFromNoteNumber,
ZOOM_X_MIN,
Expand All @@ -209,6 +223,7 @@ import SequencerShadowNote from "@/components/Sing/SequencerShadowNote.vue";
import SequencerPhraseIndicator from "@/components/Sing/SequencerPhraseIndicator.vue";
import CharacterPortrait from "@/components/Sing/CharacterPortrait.vue";
import SequencerPitch from "@/components/Sing/SequencerPitch.vue";
import SequencerVolume from "@/components/Sing/SequencerVolume.vue";
import SequencerLyricInput from "@/components/Sing/SequencerLyricInput.vue";
import { isOnCommandOrCtrlKeyDown } from "@/store/utility";
import { createLogger } from "@/domain/frontend/log";
Expand Down Expand Up @@ -406,7 +421,13 @@ const previewPitchEdit = ref<
| { type: "erase"; startFrame: number; frameLength: number }
| undefined
>(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(() => {
Expand Down Expand Up @@ -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") {
Expand All @@ -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);
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -1589,6 +1781,7 @@ const contextMenuData = computed<ContextMenuItemData[]>(() => {
pointer-events: none;
}

.sequencer-volume,
.sequencer-pitch {
grid-row: 2;
grid-column: 2;
Expand Down
32 changes: 30 additions & 2 deletions src/components/Sing/SequencerNote.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
Expand All @@ -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", // 右リサイズ中
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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));
Expand Down
Loading
Loading