From 279ab2c50a86108d28f349d284e70e7976fac904 Mon Sep 17 00:00:00 2001 From: RikitoNoto <56541594+RikitoNoto@users.noreply.github.com> Date: Mon, 27 May 2024 13:39:25 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E3=83=95=E3=82=A1?= =?UTF-8?q?=E3=82=A4=E3=83=AB=E3=81=AE=E4=BF=9D=E5=AD=98=E3=82=92=E3=82=A2?= =?UTF-8?q?=E3=83=88=E3=83=9F=E3=83=83=E3=82=AF=E6=93=8D=E4=BD=9C=E3=81=AB?= =?UTF-8?q?=E5=A4=89=E6=9B=B4=20(#2098)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #2082 tempファイル作成後に設定ファイルを書き込むよう修正 --- src/backend/electron/electronConfig.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/backend/electron/electronConfig.ts b/src/backend/electron/electronConfig.ts index 06678ebf93..57219548a4 100644 --- a/src/backend/electron/electronConfig.ts +++ b/src/backend/electron/electronConfig.ts @@ -1,6 +1,7 @@ import { join } from "path"; import fs from "fs"; import { app } from "electron"; +import { moveFile } from "move-file"; import { BaseConfigManager, Metadata } from "@/backend/common/ConfigManager"; import { ConfigType } from "@/type/preload"; @@ -21,10 +22,16 @@ export class ElectronConfigManager extends BaseConfigManager { } protected async save(config: ConfigType & Metadata) { + // ファイル書き込みに失敗したときに設定が消えないように、tempファイル書き込み後上書き移動する + const temp_path = `${this.configPath}.tmp`; await fs.promises.writeFile( - this.configPath, + temp_path, JSON.stringify(config, undefined, 2), ); + + await moveFile(temp_path, this.configPath, { + overwrite: true, + }); } private get configPath(): string { From 1604843a66bd9ee6ba67a433a22b547dcd5c31ae Mon Sep 17 00:00:00 2001 From: Segu <51497552+Segu-g@users.noreply.github.com> Date: Wed, 29 May 2024 01:23:17 +0900 Subject: [PATCH 2/4] =?UTF-8?q?vuex=E3=81=AEstore=E3=81=AE=E5=91=BC?= =?UTF-8?q?=E3=81=B3=E5=87=BA=E3=81=97=E3=82=92=E3=83=AA=E3=83=86=E3=83=A9?= =?UTF-8?q?=E3=83=AB=E5=BC=95=E6=95=B0=E3=81=8B=E3=82=89Dot=E8=A8=98?= =?UTF-8?q?=E6=B3=95=E3=81=B8=20(#2099)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add feature dot notation action * use dot notation store in dictionary.ts * このPRと直接関係ないがvuex.tsのStoreはclassである必要がないためfix * add mutaitons and actions to Store class * import alias `createDotNotationPartialStore` to `createDotPartialStore` * コメント追加とDispatch等をstore内でも見えるように * fix name DotPartStore * 丁寧語になっていたのを修正 * as unknownの文についてFIXMEを追加 * rename import `createPartialStore` * format --- src/store/dictionary.ts | 146 +++++++++++++------------ src/store/vuex.ts | 235 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 297 insertions(+), 84 deletions(-) diff --git a/src/store/dictionary.ts b/src/store/dictionary.ts index 50f07d9b1b..f43d259bb3 100644 --- a/src/store/dictionary.ts +++ b/src/store/dictionary.ts @@ -1,4 +1,4 @@ -import { createPartialStore } from "./vuex"; +import { createDotNotationPartialStore as createPartialStore } from "./vuex"; import { UserDictWord, UserDictWordToJSON } from "@/openapi"; import { DictionaryStoreState, DictionaryStoreTypes } from "@/store/type"; import { EngineId } from "@/type/preload"; @@ -7,10 +7,12 @@ export const dictionaryStoreState: DictionaryStoreState = {}; export const dictionaryStore = createPartialStore({ LOAD_USER_DICT: { - async action({ dispatch }, { engineId }) { - const engineDict = await dispatch("INSTANTIATE_ENGINE_CONNECTOR", { - engineId, - }).then((instance) => instance.invoke("getUserDictWordsUserDictGet")({})); + async action({ actions }, { engineId }) { + const engineDict = await actions + .INSTANTIATE_ENGINE_CONNECTOR({ + engineId, + }) + .then((instance) => instance.invoke("getUserDictWordsUserDictGet")({})); // 50音順にソートするために、一旦arrayにする const dictArray = Object.keys(engineDict).map((k) => { @@ -32,10 +34,10 @@ export const dictionaryStore = createPartialStore({ }, LOAD_ALL_USER_DICT: { - async action({ dispatch, state }) { + async action({ actions, state }) { const allDict = await Promise.all( state.engineIds.map((engineId) => { - return dispatch("LOAD_USER_DICT", { engineId }); + return actions.LOAD_USER_DICT({ engineId }); }), ); const mergedDictMap = new Map(); @@ -61,7 +63,7 @@ export const dictionaryStore = createPartialStore({ ADD_WORD: { async action( - { state, dispatch }, + { state, actions }, { surface, pronunciation, accentType, priority }, ) { // 同じ単語IDで登録するために、1つのエンジンで登録したあと全エンジンに同期する。 @@ -69,90 +71,100 @@ export const dictionaryStore = createPartialStore({ if (engineId == undefined) throw new Error(`No such engine registered: index == 0`); - await dispatch("INSTANTIATE_ENGINE_CONNECTOR", { - engineId, - }).then((instance) => - instance.invoke("addUserDictWordUserDictWordPost")({ - surface, - pronunciation, - accentType, - priority, - }), - ); + await actions + .INSTANTIATE_ENGINE_CONNECTOR({ + engineId, + }) + .then((instance) => + instance.invoke("addUserDictWordUserDictWordPost")({ + surface, + pronunciation, + accentType, + priority, + }), + ); - await dispatch("SYNC_ALL_USER_DICT"); + await actions.SYNC_ALL_USER_DICT(); }, }, REWRITE_WORD: { async action( - { state, dispatch }, + { state, actions }, { wordUuid, surface, pronunciation, accentType, priority }, ) { if (state.engineIds.length === 0) throw new Error(`At least one engine must be registered`); for (const engineId of state.engineIds) { - await dispatch("INSTANTIATE_ENGINE_CONNECTOR", { - engineId, - }).then((instance) => - instance.invoke("rewriteUserDictWordUserDictWordWordUuidPut")({ - wordUuid, - surface, - pronunciation, - accentType, - priority, - }), - ); + await actions + .INSTANTIATE_ENGINE_CONNECTOR({ + engineId, + }) + .then((instance) => + instance.invoke("rewriteUserDictWordUserDictWordWordUuidPut")({ + wordUuid, + surface, + pronunciation, + accentType, + priority, + }), + ); } }, }, DELETE_WORD: { - async action({ state, dispatch }, { wordUuid }) { + async action({ state, actions }, { wordUuid }) { if (state.engineIds.length === 0) throw new Error(`At least one engine must be registered`); for (const engineId of state.engineIds) { - await dispatch("INSTANTIATE_ENGINE_CONNECTOR", { - engineId, - }).then((instance) => - instance.invoke("deleteUserDictWordUserDictWordWordUuidDelete")({ - wordUuid, - }), - ); + await actions + .INSTANTIATE_ENGINE_CONNECTOR({ + engineId, + }) + .then((instance) => + instance.invoke("deleteUserDictWordUserDictWordWordUuidDelete")({ + wordUuid, + }), + ); } }, }, SYNC_ALL_USER_DICT: { - async action({ dispatch, state }) { - const mergedDict = await dispatch("LOAD_ALL_USER_DICT"); + async action({ actions, state }) { + const mergedDict = await actions.LOAD_ALL_USER_DICT(); for (const engineId of state.engineIds) { // エンジンの辞書のIDリストを取得する。 - const dictIdSet = await dispatch("INSTANTIATE_ENGINE_CONNECTOR", { - engineId, - }).then( - async (instance) => - new Set( - Object.keys( - await instance.invoke("getUserDictWordsUserDictGet")({}), - ), - ), - ); - if (Object.keys(mergedDict).some((id) => !dictIdSet.has(id))) { - await dispatch("INSTANTIATE_ENGINE_CONNECTOR", { + const dictIdSet = await actions + .INSTANTIATE_ENGINE_CONNECTOR({ engineId, - }).then((instance) => - // マージした辞書をエンジンにインポートする。 - instance.invoke("importUserDictWordsImportUserDictPost")({ - override: true, - requestBody: Object.fromEntries( - Object.entries(mergedDict).map(([k, v]) => [ - k, - UserDictWordToJSON(v), - ]), + }) + .then( + async (instance) => + new Set( + Object.keys( + await instance.invoke("getUserDictWordsUserDictGet")({}), + ), ), - }), ); + if (Object.keys(mergedDict).some((id) => !dictIdSet.has(id))) { + await actions + .INSTANTIATE_ENGINE_CONNECTOR({ + engineId, + }) + .then((instance) => + // マージした辞書をエンジンにインポートする。 + instance.invoke("importUserDictWordsImportUserDictPost")({ + override: true, + requestBody: Object.fromEntries( + Object.entries(mergedDict).map(([k, v]) => [ + k, + UserDictWordToJSON(v), + ]), + ), + }), + ); } const removedDictIdSet = new Set(dictIdSet); // マージされた辞書にあるIDを削除する。 @@ -163,8 +175,9 @@ export const dictionaryStore = createPartialStore({ } } - await dispatch("INSTANTIATE_ENGINE_CONNECTOR", { engineId }).then( - (instance) => { + await actions + .INSTANTIATE_ENGINE_CONNECTOR({ engineId }) + .then((instance) => { // マージ処理で削除された項目をエンジンから削除する。 Promise.all( [...removedDictIdSet].map((id) => @@ -175,8 +188,7 @@ export const dictionaryStore = createPartialStore({ ), ), ); - }, - ); + }); } }, }, diff --git a/src/store/vuex.ts b/src/store/vuex.ts index d51c1eddfa..c3a5ad7b77 100644 --- a/src/store/vuex.ts +++ b/src/store/vuex.ts @@ -3,7 +3,6 @@ import { InjectionKey } from "vue"; import { Store as BaseStore, - createStore as baseCreateStore, useStore as baseUseStore, ModuleTree, Plugin, @@ -34,20 +33,32 @@ export class Store< A extends ActionsBase, M extends MutationsBase, > extends BaseStore { - constructor(options: OriginalStoreOptions) { - super(options); + constructor(options: StoreOptions) { + super(options as OriginalStoreOptions); + // @ts-expect-error Storeの型を書き換えている影響で未初期化として判定される + this.actions = dotNotationDispatchProxy(this.dispatch.bind(this)); + this.mutations = dotNotationCommitProxy( + // @ts-expect-error Storeの型を書き換えている影響で未初期化として判定される + this.commit.bind(this) as Commit, + ); } readonly getters!: G; - // 既に型がつけられているものを上書きすることになるので、TS2564を吐く、それの回避 - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore + // @ts-expect-error Storeの型を非互換な型で書き換えているためエラー dispatch: Dispatch; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore + // @ts-expect-error Storeの型を非互換な型で書き換えているためエラー commit: Commit; + /** + * ドット記法用のActionを直接呼べる。エラーになる場合はdispatchを使う。 + * 詳細 https://github.com/VOICEVOX/voicevox/issues/2088 + */ + actions: DotNotationDispatch; + /** + * ドット記法用のMutationを直接呼べる。エラーになる場合はcommitを使う。 + * 詳細 https://github.com/VOICEVOX/voicevox/issues/2088 + */ + mutations: DotNotationCommit; } export function createStore< @@ -56,13 +67,7 @@ export function createStore< A extends ActionsBase, M extends MutationsBase, >(options: StoreOptions): Store { - // optionsをOriginalStoreOptionsで型キャストしないとTS2589を吐く - return baseCreateStore(options as OriginalStoreOptions) as Store< - S, - G, - A, - M - >; + return new Store(options); } export function useStore< @@ -71,7 +76,8 @@ export function useStore< A extends ActionsBase, M extends MutationsBase, >(injectKey?: InjectionKey> | string): Store { - return baseUseStore(injectKey) as Store; + // FIXME: dispatchとcommitの型を戻せばsuper typeになるのでunknownを消せる。 + return baseUseStore(injectKey) as unknown as Store; } export interface Dispatch { @@ -103,6 +109,43 @@ export interface Commit { ): void; } +export type DotNotationDispatch = { + [T in keyof A]: ( + ...payload: Parameters + ) => Promise>>; +}; + +const dotNotationDispatchProxy = ( + dispatch: Dispatch, +): DotNotationDispatch => + new Proxy( + { dispatch }, + { + get(target, tag: string) { + return (...payloads: Parameters) => + target.dispatch(tag, ...payloads); + }, + }, + ) as DotNotationDispatch; + +export type DotNotationCommit = { + [T in keyof M]: ( + ...payload: M[T] extends undefined ? void[] : [M[T]] + ) => void; +}; + +const dotNotationCommitProxy = ( + commit: Commit, +): DotNotationCommit => + new Proxy( + { commit }, + { + get(target, tag: string) { + return (...payloads: [M[string]]) => target.commit(tag, ...payloads); + }, + }, + ) as DotNotationCommit; + export interface StoreOptions< S, G extends GettersBase, @@ -161,6 +204,49 @@ export interface ActionObject< handler: ActionHandler; } +export type DotNotationActionContext< + S, + R, + SG extends GettersBase, + SA extends ActionsBase, + SM extends MutationsBase, +> = { + /** + * ドット記法用のActionを直接呼べる。エラーになる場合はdispatchを使う。 + * 詳細 https://github.com/VOICEVOX/voicevox/issues/2088 + */ + actions: DotNotationDispatch; + /** + * ドット記法用のMutationを直接呼べる。エラーになる場合はcommitを使う。 + * 詳細 https://github.com/VOICEVOX/voicevox/issues/2088 + */ + mutations: DotNotationCommit; +} & ActionContext; + +export type DotNotationActionHandler< + S, + R, + SG extends GettersBase, + SA extends ActionsBase, + SM extends MutationsBase, + K extends keyof SA, +> = ( + this: Store, + injectee: DotNotationActionContext, + payload: Parameters[0], +) => ReturnType; +export interface DotNotationActionObject< + S, + R, + SG extends GettersBase, + SA extends ActionsBase, + SM extends MutationsBase, + K extends keyof SA, +> { + root?: boolean; + handler: DotNotationActionHandler; +} + export type Getter = ( state: S, getters: SG, @@ -186,6 +272,63 @@ export type Mutation = ( payload: M[K], ) => void; +export type DotNotationAction< + S, + R, + A extends ActionsBase, + K extends keyof A, + SG extends GettersBase, + SA extends ActionsBase, + SM extends MutationsBase, +> = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + | DotNotationActionHandler + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + | DotNotationActionObject; + +// ドット記法のActionを通常のActionに変換する関数 +const unwrapDotNotationAction = < + S, + R, + A extends ActionsBase, + K extends keyof A, + SG extends GettersBase, + SA extends ActionsBase, + SM extends MutationsBase, +>( + dotNotationAction: DotNotationAction, +): Action => { + const wrappedHandler = + typeof dotNotationAction === "function" + ? dotNotationAction + : dotNotationAction.handler; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const handler: ActionHandler = function ( + injectee, + payload, + ) { + const dotNotationInjectee = { + ...injectee, + actions: dotNotationDispatchProxy(injectee.dispatch), + mutations: dotNotationCommitProxy(injectee.commit), + }; + return wrappedHandler.call(this, dotNotationInjectee, payload); + }; + + if (typeof dotNotationAction === "function") { + return handler; + } else { + return { + ...dotNotationAction, + handler, + }; + } +}; + export type GetterTree< S, R, @@ -268,6 +411,30 @@ type PartialStoreOptions< }; }; +type DotNotationPartialStoreOptions< + S, + T extends StoreTypesBase, + G extends GettersBase, + A extends ActionsBase, + M extends MutationsBase, +> = { + [K in keyof T]: { + [GAM in keyof T[K]]: GAM extends "getter" + ? K extends keyof G + ? Getter + : never + : GAM extends "action" + ? K extends keyof A + ? DotNotationAction + : never + : GAM extends "mutation" + ? K extends keyof M + ? Mutation + : never + : never; + }; +}; + export const createPartialStore = < T extends StoreTypesBase, G extends GettersBase = StoreType, @@ -301,3 +468,37 @@ export const createPartialStore = < return obj; }; + +export const createDotNotationPartialStore = < + T extends StoreTypesBase, + G extends GettersBase = StoreType, + A extends ActionsBase = StoreType, + M extends MutationsBase = StoreType, +>( + options: DotNotationPartialStoreOptions, +): StoreOptions => { + const obj = Object.keys(options).reduce( + (acc, cur) => { + const option = options[cur]; + + if (option.getter) { + acc.getters[cur] = option.getter; + } + if (option.mutation) { + acc.mutations[cur] = option.mutation; + } + if (option.action) { + acc.actions[cur] = unwrapDotNotationAction(option.action); + } + + return acc; + }, + { + getters: Object.create(null), + mutations: Object.create(null), + actions: Object.create(null), + }, + ); + + return obj; +}; From de5a7263c0b97bb826bba852d7bbd098e45ce978 Mon Sep 17 00:00:00 2001 From: Sig Date: Wed, 29 May 2024 01:33:46 +0900 Subject: [PATCH 3/4] =?UTF-8?q?=E3=83=94=E3=83=83=E3=83=81=E7=B7=A8?= =?UTF-8?q?=E9=9B=86=E3=83=A2=E3=83=BC=E3=83=89=E3=81=A7=E3=83=86=E3=83=B3?= =?UTF-8?q?=E3=83=9D=E5=A4=89=E6=9B=B4=E3=82=92=E8=A1=8C=E3=81=86=E3=81=A8?= =?UTF-8?q?=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E3=81=8C=E6=8F=8F=E3=81=84?= =?UTF-8?q?=E3=81=9F=E3=83=94=E3=83=83=E3=83=81=E3=81=8C=E3=81=9A=E3=82=8C?= =?UTF-8?q?=E3=82=8B=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3=20(#2101)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ユーザーが描いたピッチがずれるのを修正、リファクタリング * ピッチ編集モードの時にノートの端や縁の色が変わらないようにした --- src/components/Sing/SequencerNote.vue | 32 ++- src/components/Sing/SequencerPitch.vue | 261 +++++++++++-------------- src/sing/utility.ts | 7 + src/sing/viewHelper.ts | 21 +- 4 files changed, 144 insertions(+), 177 deletions(-) diff --git a/src/components/Sing/SequencerNote.vue b/src/components/Sing/SequencerNote.vue index 56c13bb0ec..20d074779d 100644 --- a/src/components/Sing/SequencerNote.vue +++ b/src/components/Sing/SequencerNote.vue @@ -307,17 +307,23 @@ const onLyricInput = (event: Event) => { } } - &.selected { - // 色は仮 - .note-bar { - background-color: lab(95, -22.953, 14.365); - border-color: lab(65, -22.953, 14.365); - outline: solid 2px lab(70, -22.953, 14.365); + &:not(.below-pitch) { + .note-left-edge:hover { + // FIXME: hoverだとカーソル位置によって適用されないので、プレビュー中に明示的にクラス指定する + background-color: lab(80, -22.953, 14.365); } - &.below-pitch { + .note-right-edge:hover { + // FIXME: hoverだとカーソル位置によって適用されないので、プレビュー中に明示的にクラス指定する + background-color: lab(80, -22.953, 14.365); + } + + &.selected { + // 色は仮 .note-bar { - background-color: rgba(colors.$primary-rgb, 0.18); + background-color: lab(95, -22.953, 14.365); + border-color: lab(65, -22.953, 14.365); + outline: solid 2px lab(70, -22.953, 14.365); } } } @@ -395,11 +401,6 @@ const onLyricInput = (event: Event) => { left: -1px; width: 5px; height: 100%; - - &:hover { - // FIXME: hoverだとカーソル位置によって適用されないので、プレビュー中に明示的にクラス指定する - background-color: lab(80, -22.953, 14.365); - } } .note-right-edge { @@ -408,11 +409,6 @@ const onLyricInput = (event: Event) => { right: -1px; width: 5px; height: 100%; - - &:hover { - // FIXME: hoverだとカーソル位置によって適用されないので、プレビュー中に明示的にクラス指定する - background-color: lab(80, -22.953, 14.365); - } } .note-lyric-input { diff --git a/src/components/Sing/SequencerPitch.vue b/src/components/Sing/SequencerPitch.vue index 9e30597baa..5d475bdc89 100644 --- a/src/components/Sing/SequencerPitch.vue +++ b/src/components/Sing/SequencerPitch.vue @@ -15,9 +15,9 @@ import { secondToTick, } from "@/sing/domain"; import { - FramewiseDataSection, - FramewiseDataSectionHash, - calculateFramewiseDataSectionHash, + PitchData, + PitchDataHash, + calculatePitchDataHash, noteNumberToBaseY, tickToBaseX, } from "@/sing/viewHelper"; @@ -28,17 +28,15 @@ import { } from "@/composables/onMountOrActivate"; import { ExhaustiveError } from "@/type/utility"; import { createLogger } from "@/domain/frontend/log"; +import { getLast } from "@/sing/utility"; type PitchLine = { - readonly frameTicksArray: number[]; - readonly lineStrip: LineStrip; + readonly color: Color; + readonly width: number; + readonly pitchDataMap: Map; + readonly lineStripMap: Map; }; -const originalPitchLineColor = new Color(171, 201, 176, 255); -const originalPitchLineWidth = 1.2; -const pitchEditLineColor = new Color(146, 214, 154, 255); -const pitchEditLineWidth = 2; - const props = defineProps<{ offsetX: number; offsetY: number; @@ -49,6 +47,8 @@ const props = defineProps<{ const { warn, error } = createLogger("SequencerPitch"); const store = useStore(); +const tpqn = computed(() => store.state.tpqn); +const tempos = computed(() => [store.state.tempos[0]]); const singingGuides = computed(() => [...store.state.singingGuides.values()]); const pitchEditData = computed(() => { return store.getters.SELECTED_TRACK.pitchEditData; @@ -56,6 +56,19 @@ const pitchEditData = computed(() => { const previewPitchEdit = computed(() => props.previewPitchEdit); const editFrameRate = computed(() => store.state.editFrameRate); +const originalPitchLine: PitchLine = { + color: new Color(171, 201, 176, 255), + width: 1.2, + pitchDataMap: new Map(), + lineStripMap: new Map(), +}; +const pitchEditLine: PitchLine = { + color: new Color(146, 214, 154, 255), + width: 2, + pitchDataMap: new Map(), + lineStripMap: new Map(), +}; + const canvasContainer = ref(null); let resizeObserver: ResizeObserver | undefined; let canvasWidth: number | undefined; @@ -66,24 +79,7 @@ let stage: PIXI.Container | undefined; let requestId: number | undefined; let renderInNextFrame = false; -let originalPitchDataSectionMap = new Map< - FramewiseDataSectionHash, - FramewiseDataSection ->(); -let pitchEditDataSectionMap = new Map< - FramewiseDataSectionHash, - FramewiseDataSection ->(); - -const originalPitchLineMap = new Map(); -const pitchEditLineMap = new Map(); - -const updatePitchLines = ( - dataSectionMap: Map, - pitchLineMap: Map, - pitchLineColor: Color, - pitchLineWidth: number, -) => { +const updateLineStrips = (pitchLine: PitchLine) => { if (stage == undefined) { throw new Error("stage is undefined."); } @@ -91,7 +87,6 @@ const updatePitchLines = ( throw new Error("canvasWidth is undefined."); } const tpqn = store.state.tpqn; - const tempos = [store.state.tempos[0]]; const canvasWidthValue = canvasWidth; const zoomX = store.state.sequencerZoomX; const zoomY = store.state.sequencerZoomY; @@ -100,48 +95,37 @@ const updatePitchLines = ( const removedLineStrips: LineStrip[] = []; - // 無くなったデータ区間を調べて、そのデータ区間に対応するピッチラインを削除する - for (const [key, pitchLine] of pitchLineMap) { - if (!dataSectionMap.has(key)) { - stage.removeChild(pitchLine.lineStrip.displayObject); - removedLineStrips.push(pitchLine.lineStrip); - pitchLineMap.delete(key); + // 無くなったピッチデータを調べて、そのピッチデータに対応するLineStripを削除する + for (const [key, lineStrip] of pitchLine.lineStripMap) { + if (!pitchLine.pitchDataMap.has(key)) { + stage.removeChild(lineStrip.displayObject); + removedLineStrips.push(lineStrip); + pitchLine.lineStripMap.delete(key); } } - // データ区間に対応するピッチラインが無かったら生成する - for (const [key, dataSection] of dataSectionMap) { - if (pitchLineMap.has(key)) { + // ピッチデータに対応するLineStripが無かったら作成する + for (const [key, pitchData] of pitchLine.pitchDataMap) { + if (pitchLine.lineStripMap.has(key)) { continue; } - const startFrame = dataSection.startFrame; - const frameLength = dataSection.data.length; - const endFrame = startFrame + frameLength; - const frameRate = dataSection.frameRate; - - // 各フレームのticksは前もって計算しておく - const frameTicksArray: number[] = []; - for (let i = startFrame; i < endFrame; i++) { - const ticks = secondToTick(i / frameRate, tempos, tpqn); - frameTicksArray.push(ticks); - } + const dataLength = pitchData.data.length; // 再利用できるLineStripがあれば再利用し、なければLineStripを作成する let lineStrip = removedLineStrips.pop(); if (lineStrip != undefined) { if ( - !lineStrip.color.equals(pitchLineColor) || - lineStrip.width !== pitchLineWidth + !lineStrip.color.equals(pitchLine.color) || + lineStrip.width !== pitchLine.width ) { throw new Error("Color or width does not match."); } - lineStrip.numOfPoints = frameLength; + lineStrip.numOfPoints = dataLength; } else { - lineStrip = new LineStrip(frameLength, pitchLineColor, pitchLineWidth); + lineStrip = new LineStrip(dataLength, pitchLine.color, pitchLine.width); } stage.addChild(lineStrip.displayObject); - - pitchLineMap.set(key, { frameTicksArray, lineStrip }); + pitchLine.lineStripMap.set(key, lineStrip); } // 再利用されなかったLineStripは破棄する @@ -149,44 +133,38 @@ const updatePitchLines = ( lineStrip.destroy(); } - // ピッチラインを更新 - for (const [key, dataSection] of dataSectionMap) { - const pitchLine = pitchLineMap.get(key); - if (pitchLine == undefined) { - throw new Error("pitchLine is undefined."); - } - if (pitchLine.frameTicksArray.length !== dataSection.data.length) { - throw new Error( - "frameTicksArray.length and dataSection.length do not match.", - ); + // LineStripを更新 + for (const [key, pitchData] of pitchLine.pitchDataMap) { + const lineStrip = pitchLine.lineStripMap.get(key); + if (lineStrip == undefined) { + throw new Error("lineStrip is undefined."); } // カリングを行う - const startTicks = pitchLine.frameTicksArray[0]; + const startTicks = pitchData.ticksArray[0]; const startBaseX = tickToBaseX(startTicks, tpqn); const startX = startBaseX * zoomX - offsetX; - const lastIndex = pitchLine.frameTicksArray.length - 1; - const endTicks = pitchLine.frameTicksArray[lastIndex]; - const endBaseX = tickToBaseX(endTicks, tpqn); - const endX = endBaseX * zoomX - offsetX; - if (startX >= canvasWidthValue || endX <= 0) { - pitchLine.lineStrip.renderable = false; + const lastTicks = getLast(pitchData.ticksArray); + const lastBaseX = tickToBaseX(lastTicks, tpqn); + const lastX = lastBaseX * zoomX - offsetX; + if (startX >= canvasWidthValue || lastX <= 0) { + lineStrip.renderable = false; continue; } - pitchLine.lineStrip.renderable = true; + lineStrip.renderable = true; // ポイントを計算してlineStripに設定&更新 - for (let i = 0; i < dataSection.data.length; i++) { - const ticks = pitchLine.frameTicksArray[i]; + for (let i = 0; i < pitchData.data.length; i++) { + const ticks = pitchData.ticksArray[i]; const baseX = tickToBaseX(ticks, tpqn); const x = baseX * zoomX - offsetX; - const freq = dataSection.data[i]; + const freq = pitchData.data[i]; const noteNumber = frequencyToNoteNumber(freq); const baseY = noteNumberToBaseY(noteNumber); const y = baseY * zoomY - offsetY; - pitchLine.lineStrip.setPoint(i, x, y); + lineStrip.setPoint(i, x, y); } - pitchLine.lineStrip.update(); + lineStrip.update(); } }; @@ -201,66 +179,72 @@ const render = () => { // シンガーが未設定の場合はピッチラインをすべて非表示にして終了 const singer = store.getters.SELECTED_TRACK.singer; if (!singer) { - for (const originalPitchLine of originalPitchLineMap.values()) { - originalPitchLine.lineStrip.renderable = false; + for (const lineStrip of originalPitchLine.lineStripMap.values()) { + lineStrip.renderable = false; } - for (const pitchEditLine of pitchEditLineMap.values()) { - pitchEditLine.lineStrip.renderable = false; + for (const lineStrip of pitchEditLine.lineStripMap.values()) { + lineStrip.renderable = false; } renderer.render(stage); return; } - // ピッチラインを更新する - updatePitchLines( - originalPitchDataSectionMap, - originalPitchLineMap, - originalPitchLineColor, - originalPitchLineWidth, - ); - updatePitchLines( - pitchEditDataSectionMap, - pitchEditLineMap, - pitchEditLineColor, - pitchEditLineWidth, - ); + // ピッチラインのLineStripを更新する + updateLineStrips(originalPitchLine); + updateLineStrips(pitchEditLine); renderer.render(stage); }; -const generateDataSectionMap = async (data: number[], frameRate: number) => { - // データ区間(データがある区間)の配列を生成する - let dataSections: FramewiseDataSection[] = []; +const toPitchData = (framewiseData: number[], frameRate: number): PitchData => { + const data = framewiseData; + const ticksArray: number[] = []; + for (let i = 0; i < data.length; i++) { + const ticks = secondToTick(i / frameRate, tempos.value, tpqn.value); + ticksArray.push(ticks); + } + return { ticksArray, data }; +}; + +const splitPitchData = (pitchData: PitchData, delimiter: number) => { + const ticksArray = pitchData.ticksArray; + const data = pitchData.data; + const pitchDataArray: PitchData[] = []; for (let i = 0; i < data.length; i++) { - if (data[i] !== VALUE_INDICATING_NO_DATA) { - if (i === 0 || data[i - 1] === VALUE_INDICATING_NO_DATA) { - dataSections.push({ startFrame: i, frameRate, data: [] }); + if (data[i] !== delimiter) { + if (i === 0 || data[i - 1] === delimiter) { + pitchDataArray.push({ ticksArray: [], data: [] }); } - dataSections[dataSections.length - 1].data.push(data[i]); + const lastPitchData = getLast(pitchDataArray); + lastPitchData.ticksArray.push(ticksArray[i]); + lastPitchData.data.push(data[i]); } } - dataSections = dataSections.filter((value) => value.data.length >= 2); - - // データ区間のハッシュを計算して、ハッシュがキーのマップにする - const dataSectionMap = new Map< - FramewiseDataSectionHash, - FramewiseDataSection - >(); - for (const dataSection of dataSections) { - const hash = await calculateFramewiseDataSectionHash(dataSection); - dataSectionMap.set(hash, dataSection); - } - return dataSectionMap; + return pitchDataArray; }; -const updateOriginalPitchDataSectionMap = async () => { - // 歌い方のf0を結合して1次元のデータにし、 - // 1次元のデータからデータ区間のマップを生成して、originalPitchDataSectionMapに設定する +const setPitchDataToPitchLine = async ( + pitchData: PitchData, + pitchLine: PitchLine, +) => { + const partialPitchDataArray = splitPitchData( + pitchData, + VALUE_INDICATING_NO_DATA, + ).filter((value) => value.data.length >= 2); + + pitchLine.pitchDataMap.clear(); + for (const partialPitchData of partialPitchDataArray) { + const hash = await calculatePitchDataHash(partialPitchData); + pitchLine.pitchDataMap.set(hash, partialPitchData); + } +}; +const generateOriginalPitchData = () => { const unvoicedPhonemes = UNVOICED_PHONEMES; - const frameRate = editFrameRate.value; // f0(元のピッチ)は編集フレームレートで表示する const singingGuidesValue = singingGuides.value; + const frameRate = editFrameRate.value; // f0(元のピッチ)は編集フレームレートで表示する + // 歌い方のf0を結合してピッチデータを生成する const tempData: number[] = []; for (const singingGuide of singingGuidesValue) { // TODO: 補間を行うようにする @@ -307,20 +291,13 @@ const updateOriginalPitchDataSectionMap = async () => { } } } - - // データ区間(ピッチのデータがある区間)のマップを生成する - const dataSectionMap = await generateDataSectionMap(tempData, frameRate); - - originalPitchDataSectionMap = dataSectionMap; + return toPitchData(tempData, frameRate); }; -const updatePitchEditDataSectionMap = async () => { - // ピッチ編集データとプレビュー中のピッチ編集データを結合して1次元のデータにし、 - // 1次元のデータからデータ区間のマップを生成して、pitchEditDataSectionMapに設定する - +const generatePitchEditData = () => { const frameRate = editFrameRate.value; - const tempData = [...pitchEditData.value]; + const tempData = [...pitchEditData.value]; // プレビュー中のピッチ編集があれば、適用する if (previewPitchEdit.value != undefined) { const previewPitchEditType = previewPitchEdit.value.type; @@ -350,22 +327,19 @@ const updatePitchEditDataSectionMap = async () => { throw new ExhaustiveError(previewPitchEditType); } } - - // データ区間(ピッチ編集データがある区間)のマップを生成する - const dataSectionMap = await generateDataSectionMap(tempData, frameRate); - - pitchEditDataSectionMap = dataSectionMap; + return toPitchData(tempData, frameRate); }; const asyncLock = new AsyncLock({ maxPending: 1 }); watch( - singingGuides, + [singingGuides, tempos, tpqn], async () => { asyncLock.acquire( "originalPitch", async () => { - await updateOriginalPitchDataSectionMap(); + const originalPitchData = generateOriginalPitchData(); + await setPitchDataToPitchLine(originalPitchData, originalPitchLine); renderInNextFrame = true; }, (err) => { @@ -379,12 +353,13 @@ watch( ); watch( - [pitchEditData, previewPitchEdit], + [pitchEditData, previewPitchEdit, tempos, tpqn], async () => { asyncLock.acquire( "pitchEdit", async () => { - await updatePitchEditDataSectionMap(); + const pitchEditData = generatePitchEditData(); + await setPitchDataToPitchLine(pitchEditData, pitchEditLine); renderInNextFrame = true; }, (err) => { @@ -471,14 +446,10 @@ onUnmountedOrDeactivated(() => { window.cancelAnimationFrame(requestId); } stage?.destroy(); - originalPitchLineMap.forEach((value) => { - value.lineStrip.destroy(); - }); - originalPitchLineMap.clear(); - pitchEditLineMap.forEach((value) => { - value.lineStrip.destroy(); - }); - pitchEditLineMap.clear(); + originalPitchLine.lineStripMap.forEach((value) => value.destroy()); + originalPitchLine.lineStripMap.clear(); + pitchEditLine.lineStripMap.forEach((value) => value.destroy()); + pitchEditLine.lineStripMap.clear(); renderer?.destroy(true); resizeObserver?.disconnect(); }); diff --git a/src/sing/utility.ts b/src/sing/utility.ts index 91cd4d83cf..2d9f38b2cc 100644 --- a/src/sing/utility.ts +++ b/src/sing/utility.ts @@ -3,6 +3,13 @@ export function round(value: number, digits: number) { return Math.round(value * powerOf10) / powerOf10; } +export function getLast(array: T[]) { + if (array.length === 0) { + throw new Error("array.length is 0."); + } + return array[array.length - 1]; +} + export function linearInterpolation( x1: number, y1: number, diff --git a/src/sing/viewHelper.ts b/src/sing/viewHelper.ts index 781123f3da..d41460ec61 100644 --- a/src/sing/viewHelper.ts +++ b/src/sing/viewHelper.ts @@ -182,25 +182,18 @@ export class GridAreaInfo implements AreaInfo { } } -export type FramewiseDataSection = { - readonly startFrame: number; - readonly frameRate: number; +export type PitchData = { + readonly ticksArray: number[]; readonly data: number[]; }; -const framewiseDataSectionHashSchema = z - .string() - .brand<"FramewiseDataSectionHash">(); +const pitchDataHashSchema = z.string().brand<"PitchDataHash">(); -export type FramewiseDataSectionHash = z.infer< - typeof framewiseDataSectionHashSchema ->; +export type PitchDataHash = z.infer; -export async function calculateFramewiseDataSectionHash( - dataSection: FramewiseDataSection, -) { - const hash = await calculateHash(dataSection); - return framewiseDataSectionHashSchema.parse(hash); +export async function calculatePitchDataHash(pitchData: PitchData) { + const hash = await calculateHash(pitchData); + return pitchDataHashSchema.parse(hash); } export type MouseButton = "LEFT_BUTTON" | "RIGHT_BUTTON" | "OTHER_BUTTON"; From a07c776956987f8a44538089278449cd0469f022 Mon Sep 17 00:00:00 2001 From: Nanashi Date: Wed, 29 May 2024 01:47:02 +0900 Subject: [PATCH 4/4] =?UTF-8?q?Improve:=20=E3=83=87=E3=83=BC=E3=82=BF?= =?UTF-8?q?=E6=BA=96=E5=82=99=E3=82=92=E9=AB=98=E9=80=9F=E5=8C=96=20(#2090?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Change: fast-base64を使う * Refactor: asyncComputedに統一 * Improve: キャッシュを追加 * Change: useEngineIconsにする * Code: コメントを足す * Fix: 代入忘れ * Change: computedにする * Fix: 型を足す * Add: コメントを追加 * Update src/components/Menu/MenuBar/MenuBar.vue --------- Co-authored-by: Hiroshiba --- package-lock.json | 6 +++++ package.json | 1 + src/@types/fast-base64.d.ts | 5 ++++ src/components/CharacterButton.vue | 11 ++------ src/components/Dialog/EngineManageDialog.vue | 12 +++------ src/components/Menu/MenuBar/MenuBar.vue | 5 ++-- .../Sing/CharacterMenuButton/MenuButton.vue | 11 ++------ src/composables/useEngineIcons.ts | 27 +++++++++++++++++++ src/helpers/base64Helper.ts | 21 ++++++++++----- src/store/audio.ts | 21 ++++++++------- 10 files changed, 76 insertions(+), 44 deletions(-) create mode 100644 src/@types/fast-base64.d.ts create mode 100644 src/composables/useEngineIcons.ts diff --git a/package-lock.json b/package-lock.json index b9b4e12bf3..6da39e115c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "electron-window-state": "5.0.3", "encoding-japanese": "1.0.30", "fast-array-diff": "1.1.0", + "fast-base64": "0.1.8", "glob": "8.0.3", "hotkeys-js": "3.13.6", "immer": "9.0.21", @@ -6613,6 +6614,11 @@ "resolved": "https://registry.npmjs.org/fast-array-diff/-/fast-array-diff-1.1.0.tgz", "integrity": "sha512-muSPyZa/yHCoDQhah9th57AmLENB1nekbrUoLAqOpQXdl1Kw8VbH24Syl5XLscaQJlx7KRU95bfTDPvVB5BJvw==" }, + "node_modules/fast-base64": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/fast-base64/-/fast-base64-0.1.8.tgz", + "integrity": "sha512-LICiPjlLyh7/P3gcJYDjKEIX41odzqny1VHSnPsAlBb/zcSJJPYrSNHs54e2TytDRTwHZl7KG5c33IMLdT+9Eg==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", diff --git a/package.json b/package.json index 30d8bbf1ed..10d7069c38 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "electron-window-state": "5.0.3", "encoding-japanese": "1.0.30", "fast-array-diff": "1.1.0", + "fast-base64": "0.1.8", "glob": "8.0.3", "hotkeys-js": "3.13.6", "immer": "9.0.21", diff --git a/src/@types/fast-base64.d.ts b/src/@types/fast-base64.d.ts new file mode 100644 index 0000000000..a52a07fa05 --- /dev/null +++ b/src/@types/fast-base64.d.ts @@ -0,0 +1,5 @@ +// fast-base64の型定義が壊れているので、ここで型定義を追加する。 +declare module "fast-base64" { + export function toBytes(base64: string): Promise; + export function toBase64(bytes: Uint8Array): Promise; +} diff --git a/src/components/CharacterButton.vue b/src/components/CharacterButton.vue index 3e49c5f91a..2ca2304b50 100644 --- a/src/components/CharacterButton.vue +++ b/src/components/CharacterButton.vue @@ -196,11 +196,11 @@