From 760f6fb1b56aacb27dac17700ea78e2fb14f0ccf Mon Sep 17 00:00:00 2001 From: Sig Date: Sat, 7 Oct 2023 15:09:15 +0900 Subject: [PATCH] =?UTF-8?q?=E3=82=B7=E3=83=BC=E3=82=B1=E3=83=B3=E3=82=B5?= =?UTF-8?q?=E3=83=BC=E3=81=AE=E8=A8=88=E7=AE=97=E3=81=AE=E6=95=B4=E7=90=86?= =?UTF-8?q?=E3=81=A8=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 | 188 +++++++++++++++---------- src/components/Sing/SequencerKeys.vue | 44 +++--- src/components/Sing/SequencerNote.vue | 51 ++++--- src/helpers/singHelper.ts | 95 +++++++++---- src/store/singing.ts | 14 +- src/store/type.ts | 2 +- tests/unit/store/Vuex.spec.ts | 2 +- 7 files changed, 241 insertions(+), 155 deletions(-) diff --git a/src/components/Sing/ScoreSequencer.vue b/src/components/Sing/ScoreSequencer.vue index 969a2ecc37..3e4728d9c8 100644 --- a/src/components/Sing/ScoreSequencer.vue +++ b/src/components/Sing/ScoreSequencer.vue @@ -14,37 +14,36 @@ - - + { - const resolution = state.score?.resolution || 480; - // NOTE: 最低長: 仮32小節...MIDI長さ(曲長さ)が決まっていないため、無限スクロール化する or 最後尾に足した場合は伸びるようにするなど? - const minDuration = resolution * 4 * 32; - const lastNote = state.score?.notes.slice(-1)[0]; - // Score長さ: スコア長もしくは最低長のうち長い方 - const totalDuration = lastNote - ? Math.max(lastNote.position + lastNote.duration, minDuration) - : minDuration; - // グリッド幅1/16 - const gridDuration = resolution / 4; - // NOTE: いったん最後尾に足した場合は伸びるようにする - const gridsMax = Math.ceil(totalDuration / gridDuration) + 16; - return [...Array(gridsMax).keys()].map( - (gridNum) => gridNum * gridDuration - ); - }); + // 分解能(Ticks Per Quarter Note) + const tpqn = computed(() => state.score?.resolution ?? 480); // ノート - const notes = computed(() => state.score?.notes); - // 表紙 - const timeSignatures = computed(() => state.score?.timeSignatures); + const notes = computed(() => state.score?.notes ?? []); + // 拍子 + const defaultTimeSignature: TimeSignature = { + position: 0, + beats: 4, + beatType: 4, + }; + const timeSignature = computed(() => { + return state.score?.timeSignatures?.[0] ?? defaultTimeSignature; + }); // ズーム状態 const zoomX = computed(() => state.sequencerZoomX); const zoomY = computed(() => state.sequencerZoomY); // スナップサイズ - const snapSize = computed(() => state.sequencerSnapSize); - const snapWidth = computed(() => (snapSize.value / 4) * zoomX.value); + const snapSizeTicks = computed(() => { + return getNoteDuration(state.sequencerSnapType, tpqn.value); + }); + const snapSizeBaseX = computed(() => { + return tickToBaseX(snapSizeTicks.value, tpqn.value); + }); + const snapWidth = computed(() => snapSizeBaseX.value * zoomX.value); // グリッドサイズ - const gridWidth = computed(() => sizeX * zoomX.value); - const gridHeight = computed(() => sizeY * zoomY.value); + const gridSizeTicks = snapSizeTicks; + const gridSizeBaseX = snapSizeBaseX; + const gridSizeBaseY = getKeyBaseHeight(); + const gridWidth = computed(() => gridSizeBaseX.value * zoomX.value); + const gridHeight = computed(() => gridSizeBaseY * zoomY.value); + // シーケンサグリッド + const ticksPerMeasure = computed(() => { + return getTicksPerMeasure(timeSignature.value, tpqn.value); + }); + const gridNumXPerMeasure = computed(() => { + // TODO: スナップが3連符のときにおかしくなるので修正する + return Math.round(ticksPerMeasure.value / gridSizeTicks.value); + }); + const gridNumX = computed(() => { + // NOTE: 最低長: 仮32小節...スコア長(曲長さ)が決まっていないため、無限スクロール化する or 最後尾に足した場合は伸びるようにするなど? + const minMeasureNum = 32; + const measureNum = Math.max( + minMeasureNum, + getMeasureNum(notes.value, ticksPerMeasure.value) + ); + // NOTE: いったん最後尾に足した場合は伸びるようにする + return gridNumXPerMeasure.value * (measureNum + 1); + }); // スクロール位置 // const scrollX = computed(() => state.sequencerScrollX); - const scrollY = computed(() => state.sequencerScrollY); + // const scrollY = computed(() => state.sequencerScrollY); const selectedNoteIds = computed(() => state.selectedNoteIds); // ノートの追加 const addNote = (event: MouseEvent) => { - const resolution = state.score?.resolution; - const gridXSize = resolution ? resolution / 4 : snapSize.value; - const position = - gridXSize * Math.floor(event.offsetX / (sizeX * zoomX.value)); - const midi = 127 - Math.floor(event.offsetY / (sizeY * zoomY.value)); - if (0 > midi) { + const eventOffsetBaseX = event.offsetX / zoomX.value; + const eventOffsetBaseY = event.offsetY / zoomY.value; + const positionBaseX = + gridSizeBaseX.value * + Math.floor(eventOffsetBaseX / gridSizeBaseX.value); + const position = baseXToTick(positionBaseX, tpqn.value); + const noteNumber = baseYToNoteNumber(eventOffsetBaseY); + if (noteNumber < 0) { return; } // NOTE: ノートの追加は1/8をベース - const duration = gridXSize * 2; - const lyric = getDoremiFromMidi(midi); + const duration = getNoteDuration(8, tpqn.value); + const lyric = getDoremiFromNoteNumber(noteNumber); // NOTE: 仮ID const id = uuidv4(); store.dispatch("ADD_NOTE", { note: { id, position, - midi, + midi: noteNumber, duration, lyric, }, @@ -258,7 +281,8 @@ export default defineComponent({ // カーソル位置に応じてノート移動量を計算 let amountPositionX = 0; if (gridWidth.value <= Math.abs(distanceX)) { - amountPositionX = 0 < distanceX ? snapSize.value : -snapSize.value; + amountPositionX = + 0 < distanceX ? snapSizeTicks.value : -snapSizeTicks.value; const dragMoveCurrentXNext = dragMoveCurrentX.value + (0 < amountPositionX ? gridWidth.value : -gridWidth.value); @@ -273,7 +297,7 @@ export default defineComponent({ dragMoveCurrentY.value = dragMoveCurrentYNext; } - // 選択中のノートのpositionとmidiを変更 + // 選択中のノートのpositionとnoteNumberを変更 let isNotesChanged = false; const newNotes = [...state.score.notes].map((note) => { if (selectedNoteIds.value.includes(note.id)) { @@ -282,10 +306,10 @@ export default defineComponent({ } isNotesChanged = true; const position = note.position + amountPositionX; - const midi = note.midi + amountPositionY; + const noteNumber = note.midi + amountPositionY; return { ...note, - midi, + midi: noteNumber, position, }; } else { @@ -333,8 +357,11 @@ export default defineComponent({ if (selectedNoteIds.value.includes(note.id)) { const duration = note.duration + - (0 < distanceX ? snapSize.value : -snapSize.value); - if (duration < Math.max(snapSize.value, 0) || note.position < 0) { + (0 < distanceX ? snapSizeTicks.value : -snapSizeTicks.value); + if ( + duration < Math.max(snapSizeTicks.value, 0) || + note.position < 0 + ) { return note; } else { isNotesChanged = true; @@ -385,11 +412,14 @@ export default defineComponent({ if (selectedNoteIds.value.includes(note.id)) { const position = note.position + - (0 < distanceX ? snapSize.value : -snapSize.value); + (0 < distanceX ? snapSizeTicks.value : -snapSizeTicks.value); const duration = note.duration + - (0 > distanceX ? snapSize.value : -snapSize.value); - if (duration < Math.max(snapSize.value, 0) || note.position < 0) { + (0 > distanceX ? snapSizeTicks.value : -snapSizeTicks.value); + if ( + duration < Math.max(snapSizeTicks.value, 0) || + note.position < 0 + ) { return note; } else { isNotesChanged = true; @@ -451,10 +481,10 @@ export default defineComponent({ } const newNotes = state.score.notes.map((note) => { if (selectedNoteIds.value.includes(note.id)) { - const midi = Math.min(note.midi + 1, 127); + const noteNumber = Math.min(note.midi + 1, 127); return { ...note, - midi, + midi: noteNumber, }; } else { return note; @@ -472,10 +502,10 @@ export default defineComponent({ } const newNotes = state.score.notes.map((note) => { if (selectedNoteIds.value.includes(note.id)) { - const midi = Math.max(note.midi - 1, 0); + const noteNumber = Math.max(note.midi - 1, 0); return { ...note, - midi, + midi: noteNumber, }; } else { return note; @@ -493,7 +523,7 @@ export default defineComponent({ } const newNotes = state.score.notes.map((note) => { if (selectedNoteIds.value.includes(note.id)) { - const position = note.position + snapSize.value; + const position = note.position + snapSizeTicks.value; return { ...note, position, @@ -511,7 +541,7 @@ export default defineComponent({ } const newNotes = state.score.notes.map((note) => { if (selectedNoteIds.value.includes(note.id)) { - const position = note.position - snapSize.value; + const position = note.position - snapSizeTicks.value; return { ...note, position, @@ -557,24 +587,26 @@ export default defineComponent({ onMounted(() => { const el = document.querySelector("#score-sequencer"); - // C4あたりにスクロールする + // 上から2/3の位置がC4になるようにスクロールする if (el) { - el.scrollTop = scrollY.value * (sizeY * zoomY.value); + const c4BaseY = noteNumberToBaseY(60); + const clientBaseHeight = el.clientHeight / zoomY.value; + const scrollBaseY = c4BaseY - clientBaseHeight * (2 / 3); + el.scrollTop = scrollBaseY * zoomY.value; } }); return { - timeSignatures, - gridY, - gridX, + gridWidth, + gridHeight, + gridNumXPerMeasure, + gridNumX, + keyInfos, notes, zoomX, zoomY, - sizeX, - sizeY, cursorX, cursorY, - getPitchFromMidi, setZoomX, setZoomY, addNote, diff --git a/src/components/Sing/SequencerKeys.vue b/src/components/Sing/SequencerKeys.vue index cdac074d39..7cc6bb60f2 100644 --- a/src/components/Sing/SequencerKeys.vue +++ b/src/components/Sing/SequencerKeys.vue @@ -1,37 +1,37 @@