diff --git a/package.json b/package.json index c9a6153405..909ad611b0 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,8 @@ "electron:build": "cross-env VITE_TARGET=electron NODE_ENV=production vite build && electron-builder --config electron-builder.config.cjs --publish never", "browser:serve": "cross-env VITE_TARGET=browser vite", "browser:build": "cross-env VITE_TARGET=browser NODE_ENV=production vite build", + "vst:serve": "cross-env VITE_TARGET=vst vite", + "vst:build": "cross-env VITE_TARGET=vst NODE_ENV=production vite build", "storybook": "storybook dev --port 6006", "storybook:build": "storybook build", "// --- lifecycle ---": "", @@ -57,6 +59,7 @@ "@std/path": "npm:@jsr/std__path@1.0.8", "async-lock": "1.4.1", "dayjs": "1.11.13", + "dequal": "2.0.3", "electron-log": "5.3.1", "electron-window-state": "5.0.3", "encoding-japanese": "2.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index af285b6379..b24ac28ee3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: dayjs: specifier: 1.11.13 version: 1.11.13 + dequal: + specifier: 2.0.3 + version: 2.0.3 electron-log: specifier: 5.3.1 version: 5.3.1 diff --git a/src/backend/vst/ipc.ts b/src/backend/vst/ipc.ts new file mode 100644 index 0000000000..1312efd8ac --- /dev/null +++ b/src/backend/vst/ipc.ts @@ -0,0 +1,383 @@ +import { toBase64, toBytes } from "fast-base64"; +import { Routing } from "./type"; +import { Metadata } from "@/backend/common/ConfigManager"; +import { TrackId } from "@/type/preload"; +import { createLogger } from "@/helpers/log"; +import { Brand, UnreachableError } from "@/type/utility"; +import { SingingVoiceKey, Track } from "@/store/type"; + +/* +メモ: +- VSTプラグインとの通信を行うためのファイル。 +- 送信はpostMessage、受信はonIpcResponseとonIpcNotificationを使う。 +- 通信内容はJSONでやり取りする。 +- だいたいの流れ: + - requestIdを振る + - Promiseを作っておく + - postMessageでJSONを送信 + - onIpcResponseで受信 + - requestIdを使ってPromiseをresolveする + + - リクエストなしで通知だけ(再生位置の変更など)の場合はonIpcNotificationを使う + +- ipcの関数はcreateMessageFunctionで作る + - これは直接exportするべきではない。必ず間に関数を挟むこと +- 通知はonReceivedIPCMessageで受け取る + */ + +declare global { + interface Window { + ipc: { + postMessage: (value: string) => void; + }; + onIpcResponse: (value: unknown) => void; + onIpcNotification: (value: unknown) => void; + } +} + +class RustError extends Error { + constructor(message: string) { + super(message); + this.name = "RustError"; + } +} + +type Notifications = { + updatePlayingState: boolean; + engineReady: { port: number }; +}; + +type RequestId = Brand; +const createGetRequestId = (): (() => RequestId) => { + let requestId = 0; + return () => requestId++ as RequestId; +}; +const getRequestId = createGetRequestId(); +let handlerInitialized = false; +const notificationReceivers = new Map void)[]>(); +const messagePromises = new Map< + number, + { + resolve: (value: unknown) => void; + reject: (reason?: unknown) => void; + name: string; + logInfo: boolean; + } +>(); +const initializeMessageHandler = () => { + log.info("Initializing message handler"); + window.onIpcResponse = (value: unknown) => { + const { requestId, payload } = value as { + requestId: number; + payload: { Ok: unknown } | { Err: string }; + }; + const { resolve, reject, name, logInfo } = + messagePromises.get(requestId) ?? {}; + if (!resolve || !reject) { + log.warn(`No promise found for requestId: ${requestId}`); + return; + } + messagePromises.delete(requestId); + if ("Ok" in payload) { + if (logInfo) { + log.info(`From plugin: ${name}(${requestId}), Ok`); + } + resolve(payload.Ok); + } else { + log.error(`From plugin: ${name}(${requestId}), Err: ${payload.Err}`); + reject(new RustError(payload.Err)); + } + }; + window.onIpcNotification = (value: unknown) => { + const message = value as { + type: string; + payload: unknown; + }; + const receivers = notificationReceivers.get(message.type); + if (!receivers) { + log.warn(`No receiver found for notification: ${message.type}`); + return; + } + log.info(`From plugin: ${message.type}`); + for (const receiver of receivers) { + receiver(message.payload); + } + }; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const createMessageFunction = any>( + name: string, + options: Partial<{ logInfo: boolean }> = {}, +) => { + const logInfo = options?.logInfo ?? true; + return (arg?: Parameters[0]) => { + if (!window.ipc?.postMessage) { + throw new UnreachableError( + "This function should not be called outside of the plugin environment", + ); + } + if (!handlerInitialized) { + handlerInitialized = true; + initializeMessageHandler(); + } + const currentNonce = getRequestId(); + if (logInfo) { + log.info(`To plugin: ${name}(${currentNonce})`); + } + window.ipc.postMessage( + JSON.stringify({ + requestId: currentNonce, + inner: { + type: name, + payload: arg, + }, + }), + ); + const { promise, resolve, reject } = Promise.withResolvers(); + messagePromises.set(currentNonce, { + resolve, + reject, + name, + logInfo, + }); + + return promise as Promise>; + }; +}; + +export type VstPhrase = { + start: number; + trackId: TrackId; + voice: string | null; + notes: VstNote[]; +}; +export type VstNote = { + start: number; + end: number; + noteNumber: number; +}; + +const ipcGetConfig = createMessageFunction<() => string | null>("getConfig"); +const ipcSetConfig = + createMessageFunction<(config: string) => void>("setConfig"); +const ipcGetProject = createMessageFunction<() => string>("getProject"); +const ipcSetProject = + createMessageFunction<(project: string) => void>("setProject"); +const ipcShowImportFileDialog = createMessageFunction< + (options: { + name?: string; + extensions?: string[]; + title: string; + }) => string | null +>("showImportFileDialog"); +const ipcShowExportFileDialog = createMessageFunction< + (obj: { + defaultPath?: string; + extensionName: string; + extensions: string[]; + title: string; + }) => string | null +>("showExportFileDialog"); +const ipcShowSaveDirectoryDialog = createMessageFunction< + (obj: { title: string }) => string | null +>("showSaveDirectoryDialog"); +const ipcExportProject = createMessageFunction<() => boolean>("exportProject"); + +const ipcReadFile = createMessageFunction<(path: string) => string>("readFile"); +const ipcWriteFile = + createMessageFunction<(obj: { path: string; data: string }) => void>( + "writeFile", + ); +const ipcCheckFileExists = + createMessageFunction<(path: string) => boolean>("checkFileExists"); + +const ipcSetPhrases = createMessageFunction< + (phrases: VstPhrase[]) => { + missingVoices: SingingVoiceKey[]; + } +>("setPhrases"); +const ipcSetVoices = + createMessageFunction<(voices: Record) => void>( + "setVoices", + ); +const ipcSetTracks = + createMessageFunction<(tracks: Record) => void>("setTracks"); +const ipcGetRouting = createMessageFunction<() => Routing>("getRouting"); +const ipcSetRouting = + createMessageFunction<(routing: Routing) => void>("setRouting"); + +const ipcGetCurrentPosition = createMessageFunction<() => number | null>( + "getCurrentPosition", + { logInfo: false }, +); + +const ipcStartEngine = + createMessageFunction< + (args: { useGpu: boolean; forceRestart: boolean }) => void + >("startEngine"); +const ipcChangeEnginePath = + createMessageFunction<() => void>("changeEnginePath"); + +const ipcOpenLogDirectory = + createMessageFunction<() => void>("openLogDirectory"); +const ipcOpenEngineDirectory = createMessageFunction<() => void>( + "openEngineDirectory", +); + +const ipcZoom = createMessageFunction<(factor: number) => void>("zoom"); + +const ipcLogInfo = createMessageFunction<(message: string) => void>("logInfo", { + logInfo: false, +}); +const ipcLogWarn = createMessageFunction<(message: string) => void>("logWarn", { + logInfo: false, +}); +const ipcLogError = createMessageFunction<(message: string) => void>( + "logError", + { logInfo: false }, +); + +type Config = Record & Metadata; +const log = createLogger("vst/ipc"); + +export async function getConfig(): Promise { + const rawConfig = await ipcGetConfig(); + if (!rawConfig) { + return undefined; + } + return JSON.parse(rawConfig) as Config; +} + +export async function setConfig(config: Config) { + await ipcSetConfig(JSON.stringify(config)); +} + +export async function getProject(): Promise { + return await ipcGetProject(); +} + +export async function setProject(memory: string) { + await ipcSetProject(memory); +} + +export async function setPhrases(phrases: VstPhrase[]) { + const { missingVoices } = await ipcSetPhrases(phrases); + return missingVoices; +} + +export async function setVoices(voices: Record) { + await ipcSetVoices(voices); +} + +export async function showImportFileDialog(options: { + name?: string; + extensions?: string[]; + title: string; +}): Promise { + return await ipcShowImportFileDialog(options).then( + (result) => result || undefined, + ); +} + +export async function readFile(filePath: string): Promise { + const base64 = await ipcReadFile(filePath); + return await toBytes(base64); +} + +export async function writeFile(filePath: string, buffer: Uint8Array) { + const base64 = await toBase64(buffer); + await ipcWriteFile({ path: filePath, data: base64 }); +} + +export async function checkFileExists(filePath: string): Promise { + return await ipcCheckFileExists(filePath); +} + +export async function exportProject(): Promise< + "success" | "cancelled" | "error" +> { + return await ipcExportProject() + .then((result) => (result ? "success" : "cancelled")) + .catch(() => "error"); +} + +export async function setTracks(tracks: Record) { + await ipcSetTracks(tracks); +} + +export async function getRouting(): Promise { + return await ipcGetRouting(); +} + +export async function setRouting(routing: Routing) { + await ipcSetRouting(routing); +} + +export async function getCurrentPosition(): Promise { + return await ipcGetCurrentPosition(); +} + +export async function startEngine(args: { + useGpu: boolean; + forceRestart: boolean; +}) { + await ipcStartEngine(args); +} + +export async function changeEnginePath() { + await ipcChangeEnginePath(); +} + +export async function zoom(factor: number) { + await ipcZoom(factor); +} + +export async function showExportFileDialog(obj: { + defaultPath?: string; + extensionName: string; + extensions: string[]; + title: string; +}): Promise { + return await ipcShowExportFileDialog(obj).then( + (result) => result || undefined, + ); +} + +export async function showSaveDirectoryDialog(obj: { + title: string; +}): Promise { + return await ipcShowSaveDirectoryDialog(obj).then( + (result) => result || undefined, + ); +} + +export async function openLogDirectory() { + await ipcOpenLogDirectory(); +} + +export async function openEngineDirectory() { + await ipcOpenEngineDirectory(); +} + +export function logInfo(message: string) { + void ipcLogInfo(message); +} + +export function logWarn(message: string) { + void ipcLogWarn(message); +} + +export function logError(message: string) { + void ipcLogError(message); +} + +export function onReceivedIPCMessage( + name: T, + callback: (value: Notifications[T]) => void, +) { + if (!notificationReceivers.has(name)) { + notificationReceivers.set(name, []); + } + notificationReceivers.get(name)?.push(callback as (value: unknown) => void); +} diff --git a/src/backend/vst/preload.ts b/src/backend/vst/preload.ts new file mode 100644 index 0000000000..89be272241 --- /dev/null +++ b/src/backend/vst/preload.ts @@ -0,0 +1,6 @@ +import { api } from "./sandbox"; +import { SandboxKey, Sandbox } from "@/type/preload"; + +const sandbox: Sandbox = api; +// @ts-expect-error readonlyになっているが、初期化処理はここで行うので問題ない +window[SandboxKey] = sandbox; diff --git a/src/backend/vst/sandbox.ts b/src/backend/vst/sandbox.ts new file mode 100644 index 0000000000..13877633e9 --- /dev/null +++ b/src/backend/vst/sandbox.ts @@ -0,0 +1,287 @@ +import { getConfigManager } from "./vstConfig"; +import { + getProject, + readFile, + startEngine, + setProject, + showImportFileDialog, + zoom, + writeFile, + showExportFileDialog, + showSaveDirectoryDialog, + checkFileExists, + logInfo, + logWarn, + logError, + onReceivedIPCMessage, + openLogDirectory, + openEngineDirectory, +} from "./ipc"; +import { + EngineId, + EngineInfo, + EngineSettingType, + EngineSettings, + Sandbox, +} from "@/type/preload"; +import { api as browserSandbox } from "@/backend/browser/sandbox"; +import { failure, success } from "@/type/result"; +import { loadEnvEngineInfos } from "@/domain/defaultEngine/envEngineInfo"; +import { UnreachableError } from "@/type/utility"; + +export const internalProjectFilePath = "/dev/vst-project.vvproj"; + +class UnimplementedError extends Error { + constructor() { + super("Function not implemented."); + } +} + +let zoomValue = 1; + +let engineInfoPromise: Promise | undefined; + +/** + * VST版のSandBox実装 + * ブラウザ版のSandBoxを継承している + */ +export const api: Sandbox = { + // ブラウザ版を使い回す + getTextAsset(key) { + return browserSandbox.getTextAsset(key); + }, + + getAltPortInfos() { + return browserSandbox.getAltPortInfos(); + }, + + async getInitialProjectFilePath() { + const projectExists = await getProject(); + + return projectExists ? internalProjectFilePath : undefined; + }, + + async showSaveDirectoryDialog(obj) { + return await showSaveDirectoryDialog(obj); + }, + + showOpenFileDialog(options) { + return showImportFileDialog(options); + }, + + async showSaveFileDialog(obj) { + return await showExportFileDialog({ + extensionName: obj.name, + extensions: obj.extensions, + title: obj.title, + defaultPath: obj.defaultPath, + }); + }, + + async writeFile(options) { + if (options.filePath === internalProjectFilePath) { + await setProject(new TextDecoder().decode(options.buffer)); + return success(undefined); + } + + try { + await writeFile(options.filePath, new Uint8Array(options.buffer)); + return success(undefined); + } catch (e) { + return failure(e as Error); + } + }, + + async readFile(options) { + if (options.filePath === internalProjectFilePath) { + const project = await getProject(); + const buffer = new TextEncoder().encode(project); + return success(buffer); + } else { + try { + return success(await readFile(options.filePath)); + } catch (e) { + return failure(e as Error); + } + } + }, + + isAvailableGPUMode(): Promise { + // TODO: Rust側でちゃんと実装する + return Promise.resolve(true); + }, + + onReceivedIPCMsg(listeners) { + return browserSandbox.onReceivedIPCMsg(listeners); + }, + + async zoomIn() { + zoomValue = Math.min(Math.max(zoomValue + 0.1, 0.5), 3); + await zoom(zoomValue); + }, + + async zoomOut() { + zoomValue = Math.min(Math.max(zoomValue - 0.1, 0.5), 3); + await zoom(zoomValue); + }, + + async zoomReset() { + zoomValue = 1; + await zoom(zoomValue); + }, + + logInfo(...params) { + logInfo(params.map(String).join(" ")); + }, + + logWarn(...params) { + logWarn(params.map(String).join(" ")); + }, + + logError(...params) { + logError(params.map(String).join(" ")); + }, + + openLogDirectory() { + void openLogDirectory(); + }, + + async engineInfos() { + if (!engineInfoPromise) { + const { promise, resolve } = Promise.withResolvers(); + // エンジンが準備完了したときの処理 + onReceivedIPCMessage("engineReady", ({ port }: { port: number }) => { + const baseEngineInfo = loadEnvEngineInfos()[0]; + if (baseEngineInfo.type != "path") { + throw new Error("default engine type must be path"); + } + resolve([ + { + ...baseEngineInfo, + protocol: "http://", + hostname: "localhost", + defaultPort: port.toString(), + pathname: "", + type: "path", + isDefault: true, + }, + ]); + }); + engineInfoPromise = promise; + } + + return engineInfoPromise; + }, + + async restartEngine(engineId) { + const engineInfos = await this.engineInfos(); + if (engineInfos.length === 0) { + throw new Error("No engine info found"); + } + if (engineId !== engineInfos[0].uuid) { + // とりあえずマルチエンジンはサポートしない + throw new Error(`Invalid engineId: ${engineId}`); + } + const engineSettings = await this.getSetting("engineSettings"); + const engineSetting = engineSettings[engineId]; + if (!engineSetting) { + throw new UnreachableError(`unreachable: engineSetting is not found`); + } + await startEngine({ useGpu: engineSetting.useGpu, forceRestart: true }); + }, + + openEngineDirectory() { + void openEngineDirectory(); + }, + + setNativeTheme() { + // なにもしない。 + }, + + vuexReady() { + return browserSandbox.vuexReady(); + }, + + async checkFileExists(file) { + return await checkFileExists(file); + }, + + getDefaultToolbarSetting() { + return browserSandbox.getDefaultToolbarSetting(); + }, + + async getSetting(key) { + const configManager = await getConfigManager(); + return configManager.get(key); + }, + + async setSetting(key, newValue) { + const configManager = await getConfigManager(); + configManager.set(key, newValue); + return newValue; + }, + + async setEngineSetting(engineId: EngineId, engineSetting: EngineSettingType) { + const engineSettings = (await this.getSetting( + "engineSettings", + )) as EngineSettings; + engineSettings[engineId] = engineSetting; + await this.setSetting("engineSettings", engineSettings); + return; + }, + + isMaximizedWindow() { + // 表示だけなのでとりあえずfalseを返す + return Promise.resolve(false); + }, + + hotkeySettings(data) { + return browserSandbox.hotkeySettings.bind(this)(data); + }, + + // 未実装 + showOpenDirectoryDialog() { + // エンジン管理で使っている。VST版では使わないので未実装 + throw new UnimplementedError(); + }, + + closeWindow() { + throw new UnimplementedError(); + }, + + minimizeWindow() { + throw new UnimplementedError(); + }, + + changePinWindow() { + throw new UnimplementedError(); + }, + + toggleMaximizeWindow() { + throw new UnimplementedError(); + }, + + toggleFullScreen() { + throw new UnimplementedError(); + }, + + installVvppEngine() { + throw new UnimplementedError(); + }, + + uninstallVvppEngine() { + throw new UnimplementedError(); + }, + + validateEngineDir() { + throw new UnimplementedError(); + }, + + reloadApp() { + throw new UnimplementedError(); + }, + + getPathForFile() { + throw new UnimplementedError(); + }, +}; diff --git a/src/backend/vst/type.ts b/src/backend/vst/type.ts new file mode 100644 index 0000000000..5dd8a40f16 --- /dev/null +++ b/src/backend/vst/type.ts @@ -0,0 +1,6 @@ +import { TrackId } from "@/type/preload"; + +export type Routing = { + channelMode: "mono" | "stereo"; + channelIndex: Record; +}; diff --git a/src/backend/vst/vstConfig.ts b/src/backend/vst/vstConfig.ts new file mode 100644 index 0000000000..bce1181a1f --- /dev/null +++ b/src/backend/vst/vstConfig.ts @@ -0,0 +1,58 @@ +import AsyncLock from "async-lock"; +import { defaultEngine } from "../browser/contract"; + +import { getConfig, setConfig } from "./ipc"; +import { BaseConfigManager, Metadata } from "@/backend/common/ConfigManager"; +import { ConfigType, EngineId, engineSettingSchema } from "@/type/preload"; +import { UnreachableError } from "@/type/utility"; +import { isMac } from "@/helpers/platform"; + +let configManager: VstConfigManager | undefined; +const configManagerLock = new AsyncLock(); +const defaultEngineId = EngineId(defaultEngine.uuid); + +export async function getConfigManager() { + await configManagerLock.acquire("configManager", async () => { + if (!configManager) { + configManager = new VstConfigManager({ + isMac, + }); + await configManager.initialize(); + } + }); + + if (!configManager) { + throw new Error("configManager is undefined"); + } + + return configManager; +} + +class VstConfigManager extends BaseConfigManager { + protected getAppVersion() { + return import.meta.env.VITE_APP_VERSION; + } + protected async exists() { + const memory = await getConfig(); + return memory != null; + } + protected async load(): Promise & Metadata> { + const memory = await getConfig(); + if (!memory) { + throw new UnreachableError("memory is null"); + } + return memory; + } + + protected async save(data: ConfigType & Metadata) { + await setConfig(data); + } + + protected getDefaultConfig() { + const baseConfig = super.getDefaultConfig(); + baseConfig.engineSettings[defaultEngineId] ??= engineSettingSchema.parse( + {}, + ); + return baseConfig; + } +} diff --git a/src/backend/vst/vstPlugin.ts b/src/backend/vst/vstPlugin.ts new file mode 100644 index 0000000000..2ceca536f0 --- /dev/null +++ b/src/backend/vst/vstPlugin.ts @@ -0,0 +1,251 @@ +import { Plugin, ref, watch } from "vue"; +import AsyncLock from "async-lock"; +import { debounce } from "quasar"; +import { toBase64 } from "fast-base64"; +import { dequal } from "dequal"; +import { + onReceivedIPCMessage, + setPhrases, + setTracks, + setVoices, + getCurrentPosition, + startEngine, + VstPhrase, + getProject, +} from "./ipc"; +import { internalProjectFilePath } from "./sandbox"; +import { Store } from "@/store/vuex"; +import { + AllActions, + AllGetters, + AllMutations, + Phrase, + PhraseKey, + SingingVoiceKey, + State, +} from "@/store/type"; +import { secondToTick, tickToSecond } from "@/sing/domain"; +import { createLogger } from "@/helpers/log"; +import { getOrThrow } from "@/helpers/mapHelper"; +import { UnreachableError } from "@/type/utility"; +import { loadEnvEngineInfos } from "@/domain/defaultEngine/envEngineInfo"; +import onetimeWatch from "@/helpers/onetimeWatch"; + +export type Message = + | { + type: "update:time"; + time: number; + } + | { + type: "update:isPlaying"; + isPlaying: boolean; + }; + +const log = createLogger("vstMessageReceiver"); + +// VSTプラグインからのメッセージを受け取る。 +export const vstPlugin: Plugin = { + install: ( + _, + { + store, + }: { + store: Store; + }, + ) => { + if (import.meta.env.VITE_TARGET !== "vst") { + return; + } + + // 再生状態の更新 + onReceivedIPCMessage("updatePlayingState", (isPlaying: boolean) => { + if (isPlaying) { + void store.dispatch("SING_STOP_AUDIO"); + } + }); + + // エンジンを起動するように指示 + void (async () => { + const baseEngineInfo = loadEnvEngineInfos()[0]; + const engineSettings = await window.backend.getSetting("engineSettings"); + const engineId = baseEngineInfo.uuid; + const engineSetting = engineSettings[engineId]; + if (!engineSetting) { + throw new UnreachableError(`unreachable: engineSetting is not found`); + } + await startEngine({ + useGpu: engineSetting.useGpu, + forceRestart: false, + }); + })(); + + // 再生位置の更新 + const updatePlayheadPosition = async () => { + const maybeCurrentPosition = await getCurrentPosition(); + if (maybeCurrentPosition != undefined) { + void store.dispatch("SET_PLAYHEAD_POSITION", { + position: secondToTick( + maybeCurrentPosition, + store.state.tempos, + store.state.tpqn, + ), + }); + } + requestAnimationFrame(updatePlayheadPosition); + }; + + requestAnimationFrame(updatePlayheadPosition); + + const lock = new AsyncLock(); + + const isReady = ref(false); + // プロジェクトの保存と送信。 + // 操作がない(最後の操作から1秒)場合に自動で保存する。 + watch( + () => ({ + tempos: store.state.tempos, + tpqn: store.state.tpqn, + timeSignatures: store.state.timeSignatures, + tracks: store.state.tracks, + }), + debounce(() => { + if (!isReady.value) { + return; + } + log.info("Saving project file"); + void store.mutations.SET_PROJECT_FILEPATH({ + filePath: internalProjectFilePath, + }); + void store.actions.SAVE_PROJECT_FILE_OVERWRITE(); + }, 1000), + { deep: true }, + ); + + // ソングエディタを強制。 + watch( + () => store.state.openedEditor, + (openedEditor) => { + if (openedEditor !== "song") { + void store.dispatch("SET_ROOT_MISC_SETTING", { + key: "openedEditor", + value: "song", + }); + } + }, + ); + + // プロジェクトの読み込み + const projectPromise = getProject(); + const projectState = ref<"loading" | "exists" | "notExists">("loading"); + void projectPromise.then((project) => { + if (project) { + projectState.value = "exists"; + } else { + projectState.value = "notExists"; + } + }); + + // - プロジェクトが存在する状態でプロジェクトの読み込みが完了した + // - プロジェクトが存在しないことが判った + // のいずれかの状態になったら、プロジェクトなどの保存を開始する。 + onetimeWatch( + () => [store.state.projectFilePath, projectState.value] as const, + async ([projectFilePath, projectState]) => { + if ( + !( + (projectState === "exists" && projectFilePath) || + projectState === "notExists" + ) + ) { + return "continue"; + } + + isReady.value = true; + + return "unwatch"; + }, + { deep: true }, + ); + + // フレーズの送信。100msごとに送信する。 + let lastPhrases: VstPhrase[] = []; + const sendPhrases = debounce(async (phrases: Map) => { + void lock.acquire("phrases", async () => { + log.info("Sending phrases"); + const newPhrases = [...phrases.values()].map((phrase) => ({ + start: phrase.startTime, + trackId: phrase.trackId, + voice: + (phrase.state === "PLAYABLE" && phrase.singingVoiceKey) || null, + notes: phrase.notes.map((note) => ({ + start: tickToSecond( + note.position, + store.state.tempos, + store.state.tpqn, + ), + end: tickToSecond( + note.position + note.duration, + store.state.tempos, + store.state.tpqn, + ), + noteNumber: note.noteNumber, + })), + })); + if (newPhrases.length === 0) { + return; + } + if (dequal(newPhrases, lastPhrases)) { + return; + } + lastPhrases = newPhrases; + const missingVoices = await setPhrases(newPhrases); + + if (missingVoices.length > 0) { + log.info(`Missing ${missingVoices.length} voices`); + const voices: Record = {}; + for (const voice of missingVoices) { + const cachedVoice = await store.actions.GET_PHRASE_SINGING_VOICE({ + key: voice, + }); + if (cachedVoice) { + voices[voice] = await toBase64( + new Uint8Array(await cachedVoice.arrayBuffer()), + ); + } + } + + await setVoices(voices); + log.info("Voices sent"); + } else { + log.info("All voices are available"); + } + }); + }, 100); + + watch( + () => [store.state.phrases, isReady] as const, + ([phrases]) => { + if (!isReady.value) return; + sendPhrases(phrases); + }, + { deep: true }, + ); + + // トラック送信 + watch( + () => [store.state.tracks, store.state.trackOrder, isReady] as const, + ([tracks, trackOrder]) => { + if (!isReady.value) return; + void lock.acquire("tracks", async () => { + log.info("Sending tracks"); + const serializedTracks = Object.fromEntries( + trackOrder.map((trackId) => [trackId, getOrThrow(tracks, trackId)]), + ); + + await setTracks(serializedTracks); + }); + }, + { deep: true }, + ); + }, +}; diff --git a/src/components/Dialog/AllDialog.vue b/src/components/Dialog/AllDialog.vue index c0bf8b1475..57af29085a 100644 --- a/src/components/Dialog/AllDialog.vue +++ b/src/components/Dialog/AllDialog.vue @@ -31,6 +31,7 @@ + diff --git a/src/components/Dialog/Dialog.ts b/src/components/Dialog/Dialog.ts index 02dfadc77f..a15a66bde5 100644 --- a/src/components/Dialog/Dialog.ts +++ b/src/components/Dialog/Dialog.ts @@ -12,6 +12,7 @@ import { } from "@/store/type"; import { DotNotationDispatch } from "@/store/vuex"; import { withProgress } from "@/store/ui"; +import { UnreachableError } from "@/type/utility"; import { errorToMessage } from "@/helpers/errorHelper"; type MediaType = "audio" | "text" | "project" | "label"; @@ -44,7 +45,7 @@ export type QuestionDialogOptions = { title: string; message: string; buttons: (string | { text: string; color: string })[]; - cancel: number; + cancel: number | "noCancel"; default?: number; }; @@ -157,12 +158,18 @@ export const showQuestionDialog = async (options: QuestionDialogOptions) => { title: options.title, message: options.message, buttons: options.buttons, - persistent: options.cancel == undefined, + persistent: options.cancel === "noCancel", default: options.default, }, }) .onOk(({ index }: { index: number }) => resolve(index)) - .onCancel(() => resolve(options.cancel)); + .onCancel(() => { + if (options.cancel === "noCancel") + throw new UnreachableError( + "Unreachable: options.cancel == 'noCancel', but onCancel is called", + ); + resolve(options.cancel); + }); const index = await promise; @@ -372,7 +379,7 @@ export const notifyResult = ( } }; -const NOTIFY_TIMEOUT = 7000; +export const NOTIFY_TIMEOUT = 7000; export const showNotifyAndNotShowAgainButton = ( { diff --git a/src/components/Dialog/VstRoutingDialog/Container.vue b/src/components/Dialog/VstRoutingDialog/Container.vue new file mode 100644 index 0000000000..4278a36c42 --- /dev/null +++ b/src/components/Dialog/VstRoutingDialog/Container.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/src/components/Dialog/VstRoutingDialog/Presentation.vue b/src/components/Dialog/VstRoutingDialog/Presentation.vue new file mode 100644 index 0000000000..a1294202a4 --- /dev/null +++ b/src/components/Dialog/VstRoutingDialog/Presentation.vue @@ -0,0 +1,179 @@ + + + + + diff --git a/src/components/Dialog/VstRoutingDialog/index.stories.ts b/src/components/Dialog/VstRoutingDialog/index.stories.ts new file mode 100644 index 0000000000..f801497eaa --- /dev/null +++ b/src/components/Dialog/VstRoutingDialog/index.stories.ts @@ -0,0 +1,88 @@ +import { Meta, StoryObj } from "@storybook/vue3"; +import Presentation from "./Presentation.vue"; +import { Track } from "@/store/type"; +import { createDefaultTrack } from "@/sing/domain"; +import { TrackId } from "@/type/preload"; +import { Routing } from "@/backend/vst/type"; + +const meta: Meta = { + component: Presentation, + args: {}, + tags: ["!autodocs"], // ダイアログ系はautodocsのプレビューが正しく表示されないので無効化 +}; + +export default meta; +type Story = StoryObj; + +const generateTrack = (index: number): [TrackId, Track] => [ + TrackId(`00000000-0000-0000-0000-${index.toString().padStart(12, "0")}`), + { + ...createDefaultTrack(), + name: `Track ${index}`, + }, +]; +const tracks = Object.fromEntries( + Array(16) + .fill(null) + .map((_, i) => generateTrack(i + 1)), +); +const routingChannelIndex: Routing["channelIndex"] = Object.fromEntries( + Object.keys(tracks).map((trackId, i) => { + return [trackId, i] as const; + }), +); + +const trackOrder = Object.keys(tracks).map((trackId) => TrackId(trackId)); + +export const OpenedLoading: Story = { + name: "開いている:ローディング", + args: { + modelValue: true, + routingInfo: { + status: "loading", + }, + tracks, + trackOrder, + }, +}; + +export const OpenedStereo: Story = { + name: "開いている:ステレオ", + + args: { + modelValue: true, + routingInfo: { + status: "loaded", + data: { + channelMode: "stereo", + channelIndex: routingChannelIndex, + }, + }, + tracks, + trackOrder, + }, +}; +export const OpenedMono: Story = { + name: "開いている:モノラル", + + args: { + modelValue: true, + routingInfo: { + status: "loaded", + data: { + channelMode: "mono", + channelIndex: routingChannelIndex, + }, + }, + tracks, + trackOrder, + }, +}; + +export const Closed: Story = { + name: "閉じている", + tags: ["skip-screenshot"], + args: { + modelValue: false, + }, +}; diff --git a/src/components/Menu/MenuBar/MenuBar.vue b/src/components/Menu/MenuBar/MenuBar.vue index 6e33ada6e1..fe57e9b3cf 100644 --- a/src/components/Menu/MenuBar/MenuBar.vue +++ b/src/components/Menu/MenuBar/MenuBar.vue @@ -1,7 +1,10 @@ @@ -37,6 +40,7 @@ import { MenuBarCategory } from "./menuBarData"; import TitleBarButtons from "./TitleBarButtons.vue"; import TitleBarEditorSwitcher from "./TitleBarEditorSwitcher.vue"; import { useStore } from "@/store"; +import { isElectron, isVst } from "@/helpers/platform"; import { getAppInfos } from "@/domain/appInfo"; const props = defineProps<{ @@ -178,7 +182,10 @@ watch(uiLocked, () => { .q-bar { min-height: vars.$menubar-height; - -webkit-app-region: drag; // Electronのドラッグ領域 + + &.enable-drag { + -webkit-app-region: drag; // Electronのドラッグ領域 + } :deep(.q-btn) { margin-left: 0; -webkit-app-region: no-drag; // Electronのドラッグ領域対象から外す diff --git a/src/components/Sing/menuBarData.ts b/src/components/Sing/menuBarData.ts index 802d53d518..656b0666a9 100644 --- a/src/components/Sing/menuBarData.ts +++ b/src/components/Sing/menuBarData.ts @@ -1,4 +1,6 @@ import { computed } from "vue"; +import { isVst } from "@/helpers/platform"; +import { MenuItemData } from "@/components/Menu/type"; import { Store } from "@/store"; import { useRootMiscSetting } from "@/composables/useRootMiscSetting"; import { ExportSongProjectFileType } from "@/store/type"; @@ -59,17 +61,27 @@ export const useMenuBarData = (store: Store): MaybeComputedMenuBarContent => { ); }; + const saveProjectCopy = async () => { + if (!uiLocked.value) { + await store.actions.SAVE_PROJECT_FILE_AS_COPY(); + } + }; + // 「ファイル」メニュー const fileSubMenuData = computed(() => ({ audioExport: [ - { - type: "button", - label: "音声書き出し", - onClick: () => { - void exportAudioFile(); - }, - disableWhenUiLocked: true, - }, + ...(isVst + ? [] + : ([ + { + type: "button", + label: "音声書き出し", + onClick: () => { + void exportAudioFile(); + }, + disableWhenUiLocked: true, + }, + ] satisfies MenuItemData[])), { type: "button", label: "labファイルを書き出し", @@ -91,22 +103,39 @@ export const useMenuBarData = (store: Store): MaybeComputedMenuBarContent => { { type: "root", label: "プロジェクトをエクスポート", - subMenu: ( - [ - ["smf", "MIDI (SMF)"], - ["musicxml", "MusicXML"], - ["ufdata", "Utaformatix"], - ["ust", "UTAU"], - ] satisfies [fileType: ExportSongProjectFileType, label: string][] - ).map(([fileType, label]) => ({ - type: "button", - label, - onClick: () => { - void exportSongProject(fileType, label); - }, - disableWhenUiLocked: true, - })), disableWhenUiLocked: true, + subMenu: [ + ...(isVst + ? ([ + { + type: "button", + label: "VOICEVOX", + onClick: () => { + void saveProjectCopy(); + }, + disableWhenUiLocked: true, + }, + ] satisfies MenuItemData[]) + : []), + ...( + [ + ["smf", "MIDI (SMF)"], + ["musicxml", "MusicXML"], + ["ufdata", "Utaformatix"], + ["ust", "UTAU"], + ] satisfies [fileType: ExportSongProjectFileType, label: string][] + ).map( + ([fileType, label]) => + ({ + type: "button", + label, + onClick: () => { + void exportSongProject(fileType, label); + }, + disableWhenUiLocked: true, + }) satisfies MenuItemData, + ), + ], }, ], })); diff --git a/src/helpers/platform.ts b/src/helpers/platform.ts index 94b1318363..fe63d99044 100644 --- a/src/helpers/platform.ts +++ b/src/helpers/platform.ts @@ -5,6 +5,7 @@ export const isProduction = import.meta.env.MODE === "production"; export const isElectron = import.meta.env.VITE_TARGET === "electron"; export const isBrowser = import.meta.env.VITE_TARGET === "browser"; +export const isVst = import.meta.env.VITE_TARGET === "vst"; // electronのメイン・レンダラープロセス内、ブラウザ内どこでも使用可能なOS判定 function checkOs(os: "windows" | "mac"): boolean { diff --git a/src/index.html b/src/index.html index 7c0e994707..003dc1cb28 100644 --- a/src/index.html +++ b/src/index.html @@ -13,7 +13,7 @@ JavaScriptが無効化されている環境では動作しません。
- + diff --git a/src/main.ts b/src/main.ts index 838b961b58..4a0e89d45f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,6 +5,7 @@ import iconSet from "quasar/icon-set/material-icons"; import { store, storeKey } from "./store"; import { ipcMessageReceiver } from "./plugins/ipcMessageReceiverPlugin"; import { hotkeyPlugin } from "./plugins/hotkeyPlugin"; +import { vstPlugin } from "@/backend/vst/vstPlugin"; import App from "@/components/App.vue"; import { markdownItPlugin } from "@/plugins/markdownItPlugin"; @@ -43,4 +44,5 @@ createApp(App) .use(hotkeyPlugin) .use(ipcMessageReceiver, { store }) .use(markdownItPlugin) + .use(vstPlugin, { store }) .mount("#app"); diff --git a/src/store/singing.ts b/src/store/singing.ts index f53df2dd23..aa12fa079f 100644 --- a/src/store/singing.ts +++ b/src/store/singing.ts @@ -3195,6 +3195,12 @@ export const singingStore = createPartialStore({ }, ), }, + + GET_PHRASE_SINGING_VOICE: { + action(_, { key }) { + return phraseSingingVoices.get(key); + }, + }, }); export const singingCommandStoreState: SingingCommandStoreState = {}; diff --git a/src/store/type.ts b/src/store/type.ts index 3c73f1977a..7643ca8048 100644 --- a/src/store/type.ts +++ b/src/store/type.ts @@ -1410,6 +1410,10 @@ export type SingingStoreTypes = { fileTypeLabel: string; }): Promise; }; + + GET_PHRASE_SINGING_VOICE: { + action(payload: { key: SingingVoiceKey }): SingingVoice | undefined; + }; }; // eslint-disable-next-line @typescript-eslint/no-empty-object-type @@ -2056,6 +2060,7 @@ export type DialogStates = { isImportSongProjectDialogOpen: boolean; isPresetManageDialogOpen: boolean; isHelpDialogOpen: boolean; + isVstRoutingDialogOpen: boolean; }; export type UiStoreTypes = { diff --git a/src/store/ui.ts b/src/store/ui.ts index 79002e8fbb..5b45de0246 100644 --- a/src/store/ui.ts +++ b/src/store/ui.ts @@ -80,6 +80,7 @@ export const uiStoreState: UiStoreState = { isImportSongProjectDialogOpen: false, isPresetManageDialogOpen: false, isHelpDialogOpen: false, + isVstRoutingDialogOpen: false, isMaximized: false, isPinned: false, isFullscreen: false, diff --git a/src/type/preload.ts b/src/type/preload.ts index a5dafc9201..9698d780a5 100644 --- a/src/type/preload.ts +++ b/src/type/preload.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { IpcSOData } from "./ipc"; -import { AltPortInfos } from "@/store/type"; +import { AltPortInfos, SingingVoice, SingingVoiceKey } from "@/store/type"; import { Result } from "@/type/result"; import { HotkeySettingType, diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 2295c4fd93..a64bd6098f 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -9,7 +9,7 @@ interface ImportMetaEnv { readonly VITE_OFFICIAL_WEBSITE_URL: string; readonly VITE_LATEST_UPDATE_INFOS_URL: string; readonly VITE_GTM_CONTAINER_ID: string; - readonly VITE_TARGET: "electron" | "browser"; + readonly VITE_TARGET: "electron" | "browser" | "vst"; readonly VITE_EXTRA_VERSION_INFO: string | undefined; } diff --git "a/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/\343\202\275\343\203\263\343\202\260\347\224\273\351\235\242-browser-win32.png" "b/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/\343\202\275\343\203\263\343\202\260\347\224\273\351\235\242-browser-win32.png" index b4f31ba32b..6404d75a01 100644 Binary files "a/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/\343\202\275\343\203\263\343\202\260\347\224\273\351\235\242-browser-win32.png" and "b/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/\343\202\275\343\203\263\343\202\260\347\224\273\351\235\242-browser-win32.png" differ diff --git "a/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/\343\203\210\343\203\274\343\202\257\347\224\273\351\235\242-browser-win32.png" "b/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/\343\203\210\343\203\274\343\202\257\347\224\273\351\235\242-browser-win32.png" index dcb994c32f..bbdbc2afef 100644 Binary files "a/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/\343\203\210\343\203\274\343\202\257\347\224\273\351\235\242-browser-win32.png" and "b/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/\343\203\210\343\203\274\343\202\257\347\224\273\351\235\242-browser-win32.png" differ diff --git "a/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-0-browser-win32.png" "b/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-0-browser-win32.png" index a4dc2a7145..bd6950e73c 100644 Binary files "a/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-0-browser-win32.png" and "b/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-0-browser-win32.png" differ diff --git "a/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-1-browser-win32.png" "b/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-1-browser-win32.png" index cb0fe2d43b..dc692dc2a1 100644 Binary files "a/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-1-browser-win32.png" and "b/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-1-browser-win32.png" differ diff --git "a/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-2-browser-win32.png" "b/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-2-browser-win32.png" index 813e0bd32a..db256cb53b 100644 Binary files "a/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-2-browser-win32.png" and "b/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-2-browser-win32.png" differ diff --git "a/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-3-browser-win32.png" "b/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-3-browser-win32.png" index dd653ecec9..d655da5b88 100644 Binary files "a/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-3-browser-win32.png" and "b/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-3-browser-win32.png" differ diff --git "a/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-4-browser-win32.png" "b/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-4-browser-win32.png" index 1b741c3899..747746d20d 100644 Binary files "a/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-4-browser-win32.png" and "b/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-4-browser-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/components-dialog-vstroutingdialog--opened-loading-dark-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/components-dialog-vstroutingdialog--opened-loading-dark-storybook-win32.png" new file mode 100644 index 0000000000..0ddc9724f0 Binary files /dev/null and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/components-dialog-vstroutingdialog--opened-loading-dark-storybook-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/components-dialog-vstroutingdialog--opened-loading-light-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/components-dialog-vstroutingdialog--opened-loading-light-storybook-win32.png" new file mode 100644 index 0000000000..1a1ab01b3a Binary files /dev/null and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/components-dialog-vstroutingdialog--opened-loading-light-storybook-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/components-dialog-vstroutingdialog--opened-mono-dark-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/components-dialog-vstroutingdialog--opened-mono-dark-storybook-win32.png" new file mode 100644 index 0000000000..a5a1d9e132 Binary files /dev/null and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/components-dialog-vstroutingdialog--opened-mono-dark-storybook-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/components-dialog-vstroutingdialog--opened-mono-light-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/components-dialog-vstroutingdialog--opened-mono-light-storybook-win32.png" new file mode 100644 index 0000000000..8bfc19f131 Binary files /dev/null and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/components-dialog-vstroutingdialog--opened-mono-light-storybook-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/components-dialog-vstroutingdialog--opened-stereo-dark-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/components-dialog-vstroutingdialog--opened-stereo-dark-storybook-win32.png" new file mode 100644 index 0000000000..a2e61c5fb7 Binary files /dev/null and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/components-dialog-vstroutingdialog--opened-stereo-dark-storybook-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/components-dialog-vstroutingdialog--opened-stereo-light-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/components-dialog-vstroutingdialog--opened-stereo-light-storybook-win32.png" new file mode 100644 index 0000000000..67bd968fe3 Binary files /dev/null and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/components-dialog-vstroutingdialog--opened-stereo-light-storybook-win32.png" differ diff --git a/vite.config.ts b/vite.config.ts index 01bd8aa599..0eaeabab4a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -17,6 +17,8 @@ import { const isElectron = process.env.VITE_TARGET === "electron"; const isBrowser = process.env.VITE_TARGET === "browser"; +const isVst = process.env.VITE_TARGET === "vst"; + const isProduction = process.env.NODE_ENV === "production"; export default defineConfig((options) => { @@ -145,7 +147,8 @@ export default defineConfig((options) => { }, }), ], - isBrowser && injectBrowserPreloadPlugin(), + isBrowser && injectPreloadPlugin("browser"), + isVst && injectPreloadPlugin("vst"), ], }; }); @@ -163,15 +166,15 @@ const cleanDistPlugin = (): Plugin => { }; }; -const injectBrowserPreloadPlugin = (): Plugin => { +const injectPreloadPlugin = (backendName: "browser" | "vst"): Plugin => { return { name: "inject-browser-preload", transformIndexHtml: { order: "pre", handler: (html: string) => html.replace( - "", - ``, + ``, + ``, ), }, };