diff --git a/src/components/Sing/ToolBar/ToolBar.vue b/src/components/Sing/ToolBar/ToolBar.vue index a9ea7b3a82..1b7112b68c 100644 --- a/src/components/Sing/ToolBar/ToolBar.vue +++ b/src/components/Sing/ToolBar/ToolBar.vue @@ -190,6 +190,7 @@ import { import CharacterMenuButton from "@/components/Sing/CharacterMenuButton/MenuButton.vue"; import { useHotkeyManager } from "@/plugins/hotkeyPlugin"; import { SequencerEditTarget } from "@/store/type"; +import { getNoteDuration } from "@/sing/domain"; const store = useStore(); @@ -431,9 +432,34 @@ const goToZero = () => { void store.dispatch("SET_PLAYHEAD_POSITION", { position: 0 }); }; -const { isLoopEnabled, setLoopEnabled } = useLoopControl(); -const toggleLoop = () => { - void setLoopEnabled(!isLoopEnabled.value); +const { + isLoopEnabled, + setLoopEnabled, + setLoopRange, + loopStartTick, + loopEndTick, +} = useLoopControl(); + +const toggleLoop = async () => { + // ループが存在しない場合は見える範囲の1小節でループを作成 + if (loopStartTick.value === loopEndTick.value) { + const sequencerBodyElement = document.querySelector(".sequencer-body"); + if (!sequencerBodyElement) return; + const currentPosition = store.getters.GET_PLAYHEAD_POSITION(); + const timeSignature = store.state.timeSignatures[0]; + const oneMeasureTicks = + getNoteDuration(timeSignature.beatType, store.state.tpqn) * + timeSignature.beats; + const measureNumber = Math.floor(currentPosition / oneMeasureTicks); + const startTick = measureNumber * oneMeasureTicks; + const endTick = startTick + oneMeasureTicks; + + await setLoopRange(startTick, endTick); + await setLoopEnabled(true); + } else { + // すでにループが存在する場合は単純に有効/無効を切り替え + await setLoopEnabled(!isLoopEnabled.value); + } }; const volume = computed({ diff --git a/src/domain/project/index.ts b/src/domain/project/index.ts index 3f541af269..995d144c8e 100644 --- a/src/domain/project/index.ts +++ b/src/domain/project/index.ts @@ -302,6 +302,17 @@ export const migrateProjectFileObject = async ( projectData.song.trackOrder = Object.keys(newTracks); } + // FIXME: 0.21.0 のマイグレーション + //if (semver.satisfies(projectAppVersion, "<0.21.0", semverSatisfiesOptions)) { + if (!("loop" in projectData.song)) { + projectData.song.loop = { + startTick: 0, + endTick: 0, + isLoopEnabled: false, + }; + } + //} + // Validation check // トークはvalidateTalkProjectで検証する // ソングはSET_SCOREの中の`isValidScore`関数で検証される diff --git a/src/domain/project/schema.ts b/src/domain/project/schema.ts index 86ae436992..a4d499ec39 100644 --- a/src/domain/project/schema.ts +++ b/src/domain/project/schema.ts @@ -119,7 +119,11 @@ export const projectSchema = z.object({ timeSignatures: z.array(timeSignatureSchema), tracks: z.record(trackIdSchema, trackSchema), trackOrder: z.array(trackIdSchema), - loop: loopSchema, + loop: z.object({ + startTick: z.number(), + endTick: z.number(), + isLoopEnabled: z.boolean(), + }), }), }); diff --git a/src/sing/domain.ts b/src/sing/domain.ts index d07cdb046d..e7c008f398 100644 --- a/src/sing/domain.ts +++ b/src/sing/domain.ts @@ -342,20 +342,6 @@ export function createDefaultTrack(): Track { }; } -export function createDefaultLoop(): Loop { - const defaultEndTick = getMeasureDuration( - DEFAULT_BEATS, - DEFAULT_BEAT_TYPE, - DEFAULT_TPQN, - ); - console.log("defaultEndTick", defaultEndTick); - return { - isLoopEnabled: false, - startTick: 0, - endTick: defaultEndTick, - }; -} - export function getSnapTypes(tpqn: number) { return getRepresentableNoteTypes(tpqn).filter((value) => { return value <= MAX_SNAP_TYPE; diff --git a/src/store/project.ts b/src/store/project.ts index e927cb3933..e8103b617f 100755 --- a/src/store/project.ts +++ b/src/store/project.ts @@ -22,7 +22,6 @@ import { createDefaultTempo, createDefaultTimeSignature, createDefaultTrack, - createDefaultLoop, DEFAULT_TPQN, } from "@/sing/domain"; import { EditorType } from "@/type/preload"; @@ -62,7 +61,7 @@ const applySongProjectToStore = async ( actions: DotNotationDispatch, songProject: LatestProjectType["song"], ) => { - const { tpqn, tempos, timeSignatures, tracks, trackOrder } = songProject; + const { tpqn, tempos, timeSignatures, tracks, trackOrder, loop } = songProject; await actions.SET_TPQN({ tpqn }); await actions.SET_TEMPOS({ tempos }); @@ -76,6 +75,11 @@ const applySongProjectToStore = async ( }), ), }); + await actions.SET_LOOP_ENABLED({ isLoopEnabled: loop.isLoopEnabled }); + await actions.SET_LOOP_RANGE({ + loopStartTick: loop.startTick, + loopEndTick: loop.endTick, + }); }; export const projectStore = createPartialStore({ @@ -129,14 +133,6 @@ export const projectStore = createPartialStore({ await context.actions.SET_TIME_SIGNATURES({ timeSignatures: [createDefaultTimeSignature(1)], }); - const defaultLoop = createDefaultLoop(); - await context.actions.SET_LOOP_ENABLED({ - isLoopEnabled: defaultLoop.isLoopEnabled, - }); - await context.actions.SET_LOOP_RANGE({ - loopStartTick: defaultLoop.startTick, - loopEndTick: defaultLoop.endTick, - }); const trackId = TrackId(crypto.randomUUID()); await context.actions.SET_TRACKS({ tracks: new Map([[trackId, createDefaultTrack()]]), @@ -159,6 +155,7 @@ export const projectStore = createPartialStore({ if (characterInfos == undefined) throw new Error("characterInfos == undefined"); + console.log(migrateProjectFileObject); const parsedProjectData = await migrateProjectFileObject(projectData, { fetchMoraData: (payload) => actions.FETCH_MORA_DATA(payload), voices: characterInfos.flatMap((characterInfo) => diff --git a/src/store/singing.ts b/src/store/singing.ts index 30d895bccc..e149356749 100644 --- a/src/store/singing.ts +++ b/src/store/singing.ts @@ -78,7 +78,6 @@ import { createDefaultTrack, createDefaultTempo, createDefaultTimeSignature, - createDefaultLoop, isValidNotes, isValidTrack, SEQUENCER_MIN_NUM_MEASURES, @@ -475,9 +474,9 @@ export const singingStoreState: SingingStoreState = { nowAudioExporting: false, cancellationOfAudioExportRequested: false, isSongSidebarOpen: false, - isLoopEnabled: createDefaultLoop().isLoopEnabled, - loopStartTick: createDefaultLoop().startTick, - loopEndTick: createDefaultLoop().endTick, + isLoopEnabled: false, + loopStartTick: 0, + loopEndTick: 0, }; export const singingStore = createPartialStore({