From 3bc974b533d133dfe2d278525240c480dbf75324 Mon Sep 17 00:00:00 2001 From: sevenc-nanashi Date: Sun, 6 Oct 2024 21:35:08 +0900 Subject: [PATCH 01/31] =?UTF-8?q?Add:=20=E3=82=BD=E3=83=B3=E3=82=B0?= =?UTF-8?q?=E3=81=AE=E6=9B=B8=E3=81=8D=E5=87=BA=E3=81=97=E3=83=80=E3=82=A4?= =?UTF-8?q?=E3=82=A2=E3=83=AD=E3=82=B0=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Dialog/AllDialog.vue | 11 ++ .../Dialog/ExportSongAudioDialog/BaseCell.vue | 41 +++++ .../ExportSongAudioDialog/Container.vue | 29 ++++ .../ExportSongAudioDialog/Presentation.vue | 155 ++++++++++++++++++ .../UpdateNotificationDialog/Container.vue | 2 +- src/components/Sing/menuBarData.ts | 8 +- src/sing/domain.ts | 2 + src/store/type.ts | 37 +---- src/store/ui.ts | 19 +-- 9 files changed, 255 insertions(+), 49 deletions(-) create mode 100644 src/components/Dialog/ExportSongAudioDialog/BaseCell.vue create mode 100644 src/components/Dialog/ExportSongAudioDialog/Container.vue create mode 100644 src/components/Dialog/ExportSongAudioDialog/Presentation.vue diff --git a/src/components/Dialog/AllDialog.vue b/src/components/Dialog/AllDialog.vue index 30d538c65b..68bec331a1 100644 --- a/src/components/Dialog/AllDialog.vue +++ b/src/components/Dialog/AllDialog.vue @@ -22,6 +22,7 @@ + @@ -39,6 +40,7 @@ import DictionaryManageDialog from "@/components/Dialog/DictionaryManageDialog.v import EngineManageDialog from "@/components/Dialog/EngineManageDialog.vue"; import UpdateNotificationDialogContainer from "@/components/Dialog/UpdateNotificationDialog/Container.vue"; import ImportSongProjectDialog from "@/components/Dialog/ImportSongProjectDialog.vue"; +import ExportSongAudioDialog from "@/components/Dialog/ExportSongAudioDialog/Container.vue"; import { useStore } from "@/store"; import { filterCharacterInfosByStyleType } from "@/store/utility"; @@ -159,6 +161,15 @@ const canOpenNotificationDialog = computed(() => { ); }); +// ソングのオーディオエクスポート時の設定ダイアログ +const isExportSongAudioDialogOpen = computed({ + get: () => store.state.isExportSongAudioDialogOpen, + set: (val) => + store.dispatch("SET_DIALOG_OPEN", { + isExportSongAudioDialogOpen: val, + }), +}); + // ソングのプロジェクトファイルのインポート時の設定ダイアログ const isImportSongProjectDialogOpenComputed = computed({ get: () => store.state.isImportSongProjectDialogOpen, diff --git a/src/components/Dialog/ExportSongAudioDialog/BaseCell.vue b/src/components/Dialog/ExportSongAudioDialog/BaseCell.vue new file mode 100644 index 0000000000..4de4c7df73 --- /dev/null +++ b/src/components/Dialog/ExportSongAudioDialog/BaseCell.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/src/components/Dialog/ExportSongAudioDialog/Container.vue b/src/components/Dialog/ExportSongAudioDialog/Container.vue new file mode 100644 index 0000000000..2b9c0130df --- /dev/null +++ b/src/components/Dialog/ExportSongAudioDialog/Container.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/src/components/Dialog/ExportSongAudioDialog/Presentation.vue b/src/components/Dialog/ExportSongAudioDialog/Presentation.vue new file mode 100644 index 0000000000..57303cbe16 --- /dev/null +++ b/src/components/Dialog/ExportSongAudioDialog/Presentation.vue @@ -0,0 +1,155 @@ + + + + + diff --git a/src/components/Dialog/UpdateNotificationDialog/Container.vue b/src/components/Dialog/UpdateNotificationDialog/Container.vue index 781e8dd685..f29b515657 100644 --- a/src/components/Dialog/UpdateNotificationDialog/Container.vue +++ b/src/components/Dialog/UpdateNotificationDialog/Container.vue @@ -1,4 +1,4 @@ - diff --git a/src/components/Sing/menuBarData.ts b/src/components/Sing/menuBarData.ts index 8a246be18d..2f53d288be 100644 --- a/src/components/Sing/menuBarData.ts +++ b/src/components/Sing/menuBarData.ts @@ -25,9 +25,11 @@ export const useMenuBarData = () => { }); }; - const exportWaveFile = async () => { + const exportAudioFile = async () => { if (uiLocked.value) return; - await store.dispatch("EXPORT_WAVE_FILE", {}); + await store.dispatch("SET_DIALOG_OPEN", { + isExportSongAudioDialogOpen: true, + }); }; const exportStemWaveFile = async () => { @@ -42,7 +44,7 @@ export const useMenuBarData = () => { type: "button", label: "音声を出力", onClick: () => { - void exportWaveFile(); + void exportAudioFile(); }, disableWhenUiLocked: true, }, diff --git a/src/sing/domain.ts b/src/sing/domain.ts index 9acfef90c6..86757a4738 100644 --- a/src/sing/domain.ts +++ b/src/sing/domain.ts @@ -17,6 +17,8 @@ const BEAT_TYPES = [2, 4, 8, 16]; const MIN_BPM = 40; const MAX_SNAP_TYPE = 32; +export type SupportedAudioFormat = "wav" | "mp3" | "ogg"; + export const isTracksEmpty = (tracks: Track[]) => tracks.length === 0 || (tracks.length === 1 && tracks[0].notes.length === 0); diff --git a/src/store/type.ts b/src/store/type.ts index 4aab3297ca..c967e490dd 100644 --- a/src/store/type.ts +++ b/src/store/type.ts @@ -1875,6 +1875,7 @@ export type UiStoreState = { isDictionaryManageDialogOpen: boolean; isEngineManageDialogOpen: boolean; isUpdateNotificationDialogOpen: boolean; + isExportSongAudioDialogOpen: boolean; isImportSongProjectDialogOpen: boolean; isMaximized: boolean; isPinned: boolean; @@ -1883,6 +1884,12 @@ export type UiStoreState = { isVuexReady: boolean; }; +export type DialogStates = { + [K in keyof UiStoreState as K extends `is${string}DialogOpen` + ? K + : never]: boolean; +}; + export type UiStoreTypes = { SET_OPENED_EDITOR: { mutation: { editor: EditorType }; @@ -1935,34 +1942,8 @@ export type UiStoreTypes = { }; SET_DIALOG_OPEN: { - mutation: { - isDefaultStyleSelectDialogOpen?: boolean; - isAcceptRetrieveTelemetryDialogOpen?: boolean; - isAcceptTermsDialogOpen?: boolean; - isDictionaryManageDialogOpen?: boolean; - isHelpDialogOpen?: boolean; - isSettingDialogOpen?: boolean; - isHotkeySettingDialogOpen?: boolean; - isToolbarSettingDialogOpen?: boolean; - isCharacterOrderDialogOpen?: boolean; - isEngineManageDialogOpen?: boolean; - isUpdateNotificationDialogOpen?: boolean; - isImportExternalProjectDialogOpen?: boolean; - }; - action(payload: { - isDefaultStyleSelectDialogOpen?: boolean; - isAcceptRetrieveTelemetryDialogOpen?: boolean; - isAcceptTermsDialogOpen?: boolean; - isDictionaryManageDialogOpen?: boolean; - isHelpDialogOpen?: boolean; - isSettingDialogOpen?: boolean; - isHotkeySettingDialogOpen?: boolean; - isToolbarSettingDialogOpen?: boolean; - isCharacterOrderDialogOpen?: boolean; - isEngineManageDialogOpen?: boolean; - isUpdateNotificationDialogOpen?: boolean; - isImportSongProjectDialogOpen?: boolean; - }): void; + mutation: Partial; + action(payload: Partial): void; }; SHOW_ALERT_DIALOG: { diff --git a/src/store/ui.ts b/src/store/ui.ts index 9d6bdb5afa..e251f5c7f5 100644 --- a/src/store/ui.ts +++ b/src/store/ui.ts @@ -110,6 +110,7 @@ export const uiStoreState: UiStoreState = { isDictionaryManageDialogOpen: false, isEngineManageDialogOpen: false, isUpdateNotificationDialogOpen: false, + isExportSongAudioDialogOpen: false, isImportSongProjectDialogOpen: false, isMaximized: false, isPinned: false, @@ -216,23 +217,7 @@ export const uiStore = createPartialStore({ }, SET_DIALOG_OPEN: { - mutation( - state, - dialogState: { - isDefaultStyleSelectDialogOpen?: boolean; - isAcceptRetrieveTelemetryDialogOpen?: boolean; - isAcceptTermsDialogOpen?: boolean; - isDictionaryManageDialogOpen?: boolean; - isHelpDialogOpen?: boolean; - isSettingDialogOpen?: boolean; - isHotkeySettingDialogOpen?: boolean; - isToolbarSettingDialogOpen?: boolean; - isCharacterOrderDialogOpen?: boolean; - isEngineManageDialogOpen?: boolean; - isUpdateNotificationDialogOpen?: boolean; - isImportExternalProjectDialogOpen?: boolean; - }, - ) { + mutation(state, dialogState) { for (const [key, value] of Object.entries(dialogState)) { if (!(key in state)) { throw new Error(`Unknown dialog state: ${key}`); From 746e911e7c96d961d9aa58694a4316704711eae4 Mon Sep 17 00:00:00 2001 From: sevenc-nanashi Date: Tue, 8 Oct 2024 18:54:56 +0900 Subject: [PATCH 02/31] =?UTF-8?q?Add:=20=E3=83=80=E3=82=A4=E3=82=A2?= =?UTF-8?q?=E3=83=AD=E3=82=B0=E3=82=92=E5=AE=8C=E6=88=90=E3=81=95=E3=81=9B?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ExportSongAudioDialog/Container.vue | 4 +- .../ExportSongAudioDialog/Presentation.vue | 84 +++++++++++++------ 2 files changed, 62 insertions(+), 26 deletions(-) diff --git a/src/components/Dialog/ExportSongAudioDialog/Container.vue b/src/components/Dialog/ExportSongAudioDialog/Container.vue index 2b9c0130df..5578540949 100644 --- a/src/components/Dialog/ExportSongAudioDialog/Container.vue +++ b/src/components/Dialog/ExportSongAudioDialog/Container.vue @@ -1,5 +1,5 @@ diff --git a/src/components/Dialog/ExportSongAudioDialog/Presentation.vue b/src/components/Dialog/ExportSongAudioDialog/Presentation.vue index 57303cbe16..d678abf785 100644 --- a/src/components/Dialog/ExportSongAudioDialog/Presentation.vue +++ b/src/components/Dialog/ExportSongAudioDialog/Presentation.vue @@ -1,5 +1,5 @@ - - diff --git a/src/components/Dialog/ExportSongAudioDialog/Presentation.vue b/src/components/Dialog/ExportSongAudioDialog/Presentation.vue index d678abf785..71ee1e9bf4 100644 --- a/src/components/Dialog/ExportSongAudioDialog/Presentation.vue +++ b/src/components/Dialog/ExportSongAudioDialog/Presentation.vue @@ -41,6 +41,12 @@ dense /> --> + + + (); const emit = defineEmits<{ /** 音声をエクスポートするときに呼ばれる */ - exportAudio: [setting: ExportAudioSetting]; + exportAudio: [exportTarget: ExportTarget, setting: SongExportSetting]; }>(); // 書き出し対象選択 @@ -132,8 +130,11 @@ const exportTargets = [ ]; const exportTarget = ref("master"); +// ステレオ +const isStereo = ref(true); + // フォーマット選択 -const audioFormat = ref("wav"); +const audioFormat = ref("wav"); // const supportedFormats = [ // { // label: "WAV", @@ -162,8 +163,8 @@ const withTrackParameters = ref(true); const handleExportTrack = () => { onDialogOK(); - emit("exportAudio", { - target: exportTarget.value, + emit("exportAudio", exportTarget.value, { + isStereo: isStereo.value, sampleRate: samplingRate.value, audioFormat: audioFormat.value, withLimiter: withLimiter.value, diff --git a/src/sing/domain.ts b/src/sing/domain.ts index 86757a4738..a52e8c66a2 100644 --- a/src/sing/domain.ts +++ b/src/sing/domain.ts @@ -17,6 +17,7 @@ const BEAT_TYPES = [2, 4, 8, 16]; const MIN_BPM = 40; const MAX_SNAP_TYPE = 32; +export type ExportTarget = "master" | "stem"; export type SupportedAudioFormat = "wav" | "mp3" | "ogg"; export const isTracksEmpty = (tracks: Track[]) => diff --git a/src/store/singing.ts b/src/store/singing.ts index d35e61994f..7556720600 100644 --- a/src/store/singing.ts +++ b/src/store/singing.ts @@ -106,6 +106,7 @@ import { PhraseRenderStageId, createPhraseRenderer, } from "@/sing/phraseRendering"; +import { UnreachableError } from "@/type/utility"; const logger = createLogger("store/singing"); @@ -1951,16 +1952,16 @@ export const singingStore = createPartialStore({ EXPORT_WAVE_FILE: { action: createUILockAction( - async ({ state, mutations, getters, actions }, { filePath }) => { + async ({ state, mutations, getters, actions }, { filePath, setting }) => { const exportWaveFile = async (): Promise => { const fileName = generateDefaultSongFileName( getters.PROJECT_NAME, getters.SELECTED_TRACK, getters.CHARACTER_INFO, ); - const numberOfChannels = 2; - const sampleRate = 48000; // TODO: 設定できるようにする - const withLimiter = true; // TODO: 設定できるようにする + const numberOfChannels = setting.isStereo ? 2 : 1; + const sampleRate = setting.sampleRate; + const withLimiter = setting.withLimiter; const renderDuration = getters.CALC_RENDER_DURATION; @@ -2011,36 +2012,21 @@ export const singingStore = createPartialStore({ phraseSingingVoices, ); - const waveFileData = convertToWavFileData(audioBuffer); - - try { - await window.backend - .writeFile({ - filePath, - buffer: waveFileData, - }) - .then(getValueOrThrow); - } catch (e) { - logger.error("Failed to export the wav file.", e); - if (e instanceof ResultError) { - return { - result: "WRITE_ERROR", - path: filePath, - errorMessage: generateWriteErrorMessage( - e as ResultError, - ), - }; - } - return { - result: "UNKNOWN_ERROR", - path: filePath, - errorMessage: - (e instanceof Error ? e.message : String(e)) || - "不明なエラーが発生しました。", - }; + let fileData; + switch (setting.audioFormat) { + case "wav": + fileData = convertToWavFileData(audioBuffer); + break; + default: + throw new UnreachableError("Not implemented"); } - return { result: "SUCCESS", path: filePath }; + const result = await actions.EXPORT_FILE({ + filePath, + content: fileData, + }); + + return result; }; mutations.SET_NOW_AUDIO_EXPORTING({ nowAudioExporting: true }); @@ -2054,15 +2040,14 @@ export const singingStore = createPartialStore({ ), }, - // TODO: EXPORT_WAVE_FILEとコードが重複しているので、共通化する EXPORT_STEM_WAVE_FILE: { action: createUILockAction( - async ({ state, mutations, getters, actions }, { dirPath }) => { + async ({ state, mutations, getters, actions }, { dirPath, setting }) => { let firstFilePath = ""; const exportWaveFile = async (): Promise => { - const numberOfChannels = 2; - const sampleRate = 48000; // TODO: 設定できるようにする - const withLimiter = true; // TODO: 設定できるようにする + const numberOfChannels = setting.isStereo ? 2 : 1; + const sampleRate = setting.sampleRate; + const withLimiter = setting.withLimiter; const renderDuration = getters.CALC_RENDER_DURATION; @@ -2151,36 +2136,25 @@ export const singingStore = createPartialStore({ singingVoiceCache, ); - const waveFileData = convertToWavFileData(audioBuffer); - if (i === 0) { - firstFilePath = filePath; + let fileData; + switch (setting.audioFormat) { + case "wav": + fileData = convertToWavFileData(audioBuffer); + break; + default: + throw new UnreachableError("Not implemented"); } - try { - await window.backend - .writeFile({ - filePath, - buffer: waveFileData, - }) - .then(getValueOrThrow); - } catch (e) { - logger.error("Failed to export the wav file.", e); - if (e instanceof ResultError) { - return { - result: "WRITE_ERROR", - path: filePath, - errorMessage: generateWriteErrorMessage( - e as ResultError, - ), - }; - } - return { - result: "UNKNOWN_ERROR", - path: filePath, - errorMessage: - (e instanceof Error ? e.message : String(e)) || - "不明なエラーが発生しました。", - }; + const result = await actions.EXPORT_FILE({ + filePath, + content: fileData, + }); + if (result.result !== "SUCCESS") { + return result; + } + + if (i === 0) { + firstFilePath = filePath; } } @@ -2198,6 +2172,37 @@ export const singingStore = createPartialStore({ ), }, + EXPORT_FILE: { + async action(_, { filePath, content }) { + try { + await window.backend + .writeFile({ + filePath, + buffer: content, + }) + .then(getValueOrThrow); + } catch (e) { + logger.error("Failed to export file.", e); + if (e instanceof ResultError) { + return { + result: "WRITE_ERROR", + path: filePath, + errorMessage: generateWriteErrorMessage(e as ResultError), + }; + } + return { + result: "UNKNOWN_ERROR", + path: filePath, + errorMessage: + (e instanceof Error ? e.message : String(e)) || + "不明なエラーが発生しました。", + }; + } + + return { result: "SUCCESS", path: filePath }; + }, + }, + CANCEL_AUDIO_EXPORT: { async action({ state, mutations }) { if (!state.nowAudioExporting) { diff --git a/src/store/type.ts b/src/store/type.ts index c967e490dd..3ae338e9fd 100644 --- a/src/store/type.ts +++ b/src/store/type.ts @@ -71,6 +71,7 @@ import { timeSignatureSchema, trackSchema, } from "@/domain/project/schema"; +import { ExportTarget, SupportedAudioFormat } from "@/sing/domain"; /** * エディタ用のAudioQuery @@ -815,6 +816,15 @@ export const PhraseKey = (id: string): PhraseKey => phraseKeySchema.parse(id); export type SequencerEditTarget = "NOTE" | "PITCH"; +export type SongSupportedAudioFormat = "wav" | "mp3" | "ogg"; +export type SongExportSetting = { + isStereo: boolean; + sampleRate: number; + audioFormat: SupportedAudioFormat; + withLimiter: boolean; + withTrackParameters: boolean; +}; + export type SingingStoreState = { tpqn: number; // Ticks Per Quarter Note tempos: Tempo[]; @@ -1062,11 +1072,24 @@ export type SingingStoreTypes = { }; EXPORT_WAVE_FILE: { - action(payload: { filePath?: string }): SaveResultObject; + action(payload: { + filePath?: string; + setting: SongExportSetting; + }): SaveResultObject; }; EXPORT_STEM_WAVE_FILE: { - action(payload: { dirPath?: string }): SaveResultObject; + action(payload: { + dirPath?: string; + setting: SongExportSetting; + }): SaveResultObject; + }; + + EXPORT_FILE: { + action(payload: { + filePath: string; + content: ArrayBuffer; + }): Promise; }; CANCEL_AUDIO_EXPORT: { From f257ac7ae51e7c0965e7a423bcf4e6c571145b9d Mon Sep 17 00:00:00 2001 From: sevenc-nanashi Date: Tue, 8 Oct 2024 20:39:27 +0900 Subject: [PATCH 04/31] =?UTF-8?q?Add:=20=E6=9B=B8=E3=81=8D=E5=87=BA?= =?UTF-8?q?=E3=81=9B=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dialog/ExportSongAudioDialog/Container.vue | 2 +- src/components/Sing/SingEditor.vue | 3 +-- src/components/Sing/menuBarData.ts | 15 +-------------- src/store/singing.ts | 11 +++++++++-- src/store/type.ts | 3 +-- 5 files changed, 13 insertions(+), 21 deletions(-) diff --git a/src/components/Dialog/ExportSongAudioDialog/Container.vue b/src/components/Dialog/ExportSongAudioDialog/Container.vue index d77e6dfe0c..fe6d013808 100644 --- a/src/components/Dialog/ExportSongAudioDialog/Container.vue +++ b/src/components/Dialog/ExportSongAudioDialog/Container.vue @@ -3,8 +3,8 @@ diff --git a/src/sing/domain.ts b/src/sing/domain.ts index a52e8c66a2..9acfef90c6 100644 --- a/src/sing/domain.ts +++ b/src/sing/domain.ts @@ -17,9 +17,6 @@ const BEAT_TYPES = [2, 4, 8, 16]; const MIN_BPM = 40; const MAX_SNAP_TYPE = 32; -export type ExportTarget = "master" | "stem"; -export type SupportedAudioFormat = "wav" | "mp3" | "ogg"; - export const isTracksEmpty = (tracks: Track[]) => tracks.length === 0 || (tracks.length === 1 && tracks[0].notes.length === 0); diff --git a/src/sing/convertToWavFileData.ts b/src/sing/encodeAudioData.ts similarity index 100% rename from src/sing/convertToWavFileData.ts rename to src/sing/encodeAudioData.ts diff --git a/src/store/singing.ts b/src/store/singing.ts index 8ef2d81493..5280f89151 100644 --- a/src/store/singing.ts +++ b/src/store/singing.ts @@ -100,7 +100,7 @@ import { getOrThrow } from "@/helpers/mapHelper"; import { cloneWithUnwrapProxy } from "@/helpers/cloneWithUnwrapProxy"; import { ufProjectToVoicevox } from "@/sing/utaformatixProject/toVoicevox"; import { uuid4 } from "@/helpers/random"; -import { convertToWavFileData } from "@/sing/convertToWavFileData"; +import { convertToWavFileData } from "@/sing/encodeAudioData"; import { generateWriteErrorMessage } from "@/helpers/fileHelper"; import { PhraseRenderStageId, @@ -1937,10 +1937,10 @@ export const singingStore = createPartialStore({ }, }, - EXPORT_WAVE_FILE: { + EXPORT_AUDIO_FILE: { action: createUILockAction( async ({ state, mutations, getters, actions }, { filePath, setting }) => { - const exportWaveFile = async (): Promise => { + const exportAudioFile = async (): Promise => { const fileName = generateDefaultSongFileName( getters.PROJECT_NAME, getters.SELECTED_TRACK, @@ -2017,7 +2017,7 @@ export const singingStore = createPartialStore({ }; mutations.SET_NOW_AUDIO_EXPORTING({ nowAudioExporting: true }); - return exportWaveFile().finally(() => { + return exportAudioFile().finally(() => { mutations.SET_CANCELLATION_OF_AUDIO_EXPORT_REQUESTED({ cancellationOfAudioExportRequested: false, }); @@ -2027,11 +2027,11 @@ export const singingStore = createPartialStore({ ), }, - EXPORT_STEM_WAVE_FILE: { + EXPORT_STEM_AUDIO_FILE: { action: createUILockAction( async ({ state, mutations, getters, actions }, { dirPath, setting }) => { let firstFilePath = ""; - const exportWaveFile = async (): Promise => { + const exportAudioFile = async (): Promise => { const numberOfChannels = setting.isStereo ? 2 : 1; const sampleRate = setting.sampleRate; const withLimiter = setting.withLimiter; @@ -2156,7 +2156,7 @@ export const singingStore = createPartialStore({ }; mutations.SET_NOW_AUDIO_EXPORTING({ nowAudioExporting: true }); - return exportWaveFile().finally(() => { + return exportAudioFile().finally(() => { mutations.SET_CANCELLATION_OF_AUDIO_EXPORT_REQUESTED({ cancellationOfAudioExportRequested: false, }); diff --git a/src/store/type.ts b/src/store/type.ts index ddb49cf86c..ac04724d2b 100644 --- a/src/store/type.ts +++ b/src/store/type.ts @@ -1070,14 +1070,14 @@ export type SingingStoreTypes = { action(payload: { isDrag: boolean }): void; }; - EXPORT_WAVE_FILE: { + EXPORT_AUDIO_FILE: { action(payload: { filePath?: string; setting: SongExportSetting; }): SaveResultObject; }; - EXPORT_STEM_WAVE_FILE: { + EXPORT_STEM_AUDIO_FILE: { action(payload: { dirPath?: string; setting: SongExportSetting; From 38b34492c3bfbd3cf9bda1a4d8d48e5665c3d15c Mon Sep 17 00:00:00 2001 From: sevenc-nanashi Date: Tue, 8 Oct 2024 21:58:47 +0900 Subject: [PATCH 06/31] =?UTF-8?q?Add:=20wav=E4=BB=A5=E5=A4=96=E3=81=AE?= =?UTF-8?q?=E6=9B=B8=E3=81=8D=E5=87=BA=E3=81=97=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 23 +++++- package.json | 1 + src/backend/common/ConfigManager.ts | 7 ++ src/backend/electron/main.ts | 13 +++- src/backend/electron/preload.ts | 3 +- .../ExportSongAudioDialog/Presentation.vue | 37 ++++----- .../FileNameTemplateDialog.stories.ts | 4 +- .../SettingDialog/FileNameTemplateDialog.vue | 35 ++++----- .../Dialog/SettingDialog/SettingDialog.vue | 10 ++- src/sing/encodeAudioData.ts | 78 ++++++++++++++++++- src/store/singing.ts | 58 +++++++------- src/store/type.ts | 4 +- src/store/utility.ts | 6 +- src/type/ipc.ts | 11 ++- src/type/preload.ts | 4 +- 15 files changed, 204 insertions(+), 90 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4babfcee69..7d314206df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "vue": "3.4.26", "vuedraggable": "4.1.0", "vuex": "4.0.2", + "wasm-media-encoders": "0.7.0", "zod": "3.22.4" }, "devDependencies": { @@ -7106,6 +7107,14 @@ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", "dev": true }, + "node_modules/@swc/helpers": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.13.tgz", + "integrity": "sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@swc/jest": { "version": "0.2.36", "resolved": "https://registry.npmjs.org/@swc/jest/-/jest-0.2.36.tgz", @@ -22273,8 +22282,7 @@ "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -24029,6 +24037,17 @@ "makeerror": "1.0.12" } }, + "node_modules/wasm-media-encoders": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/wasm-media-encoders/-/wasm-media-encoders-0.7.0.tgz", + "integrity": "sha512-Sp4wUasgxOK/IFfNhpon6LQQgYGwtpxyV4isjGIe1rvhnJL3w2KYr4f+CdqDNtGPDcgCDRY+uBanVfSx6Si0WQ==", + "dependencies": { + "@swc/helpers": "^0.5.10" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/watchpack": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", diff --git a/package.json b/package.json index 75e20bc3cc..76a5e44a0c 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "vue": "3.4.26", "vuedraggable": "4.1.0", "vuex": "4.0.2", + "wasm-media-encoders": "0.7.0", "zod": "3.22.4" }, "optionalDependencies": { diff --git a/src/backend/common/ConfigManager.ts b/src/backend/common/ConfigManager.ts index 35b069e03a..75172c8133 100644 --- a/src/backend/common/ConfigManager.ts +++ b/src/backend/common/ConfigManager.ts @@ -255,6 +255,13 @@ const migrations: [string, (store: Record) => unknown][] = [ delete experimentalSetting.shouldApplyDefaultPresetOnVoiceChanged; } + // 書き出しテンプレートから拡張子を削除 + const savingSetting = config.savingSetting as { fileNamePattern: string }; + savingSetting.fileNamePattern = savingSetting.fileNamePattern.replace( + ".wav", + "", + ); + return config; }, ], diff --git a/src/backend/electron/main.ts b/src/backend/electron/main.ts index 723259239b..ccc63433e0 100644 --- a/src/backend/electron/main.ts +++ b/src/backend/electron/main.ts @@ -579,12 +579,16 @@ registerIpcMainHandle({ return engineInfoManager.altPortInfos; }, - SHOW_AUDIO_SAVE_DIALOG: async (_, { title, defaultPath }) => { + SHOW_AUDIO_SAVE_DIALOG: async (_, { title, defaultPath, formats }) => { + formats ??= ["wav"]; const result = await retryShowSaveDialogWhileSafeDir(() => dialog.showSaveDialog(win, { title, defaultPath, - filters: [{ name: "Wave File", extensions: ["wav"] }], + filters: formats.map((format) => ({ + name: `${format}ファイル`, + extensions: [format], + })), properties: ["createDirectory"], }), ); @@ -872,7 +876,10 @@ registerIpcMainHandle({ WRITE_FILE: (_, { filePath, buffer }) => { try { - fs.writeFileSync(filePath, new DataView(buffer)); + fs.writeFileSync( + filePath, + new DataView(buffer instanceof Uint8Array ? buffer.buffer : buffer), + ); return success(undefined); } catch (e) { // throwだと`.code`の情報が消えるのでreturn diff --git a/src/backend/electron/preload.ts b/src/backend/electron/preload.ts index c94d3d1ea5..017b09cfcc 100644 --- a/src/backend/electron/preload.ts +++ b/src/backend/electron/preload.ts @@ -54,10 +54,11 @@ const api: Sandbox = { return await ipcRendererInvokeProxy.GET_ALT_PORT_INFOS(); }, - showAudioSaveDialog: ({ title, defaultPath }) => { + showAudioSaveDialog: ({ title, defaultPath, formats }) => { return ipcRendererInvokeProxy.SHOW_AUDIO_SAVE_DIALOG({ title, defaultPath, + formats, }); }, diff --git a/src/components/Dialog/ExportSongAudioDialog/Presentation.vue b/src/components/Dialog/ExportSongAudioDialog/Presentation.vue index 71ee1e9bf4..790e8e82d8 100644 --- a/src/components/Dialog/ExportSongAudioDialog/Presentation.vue +++ b/src/components/Dialog/ExportSongAudioDialog/Presentation.vue @@ -24,7 +24,6 @@ dense /> - + ("master"); const isStereo = ref(true); // フォーマット選択 -const audioFormat = ref("wav"); -// const supportedFormats = [ -// { -// label: "WAV", -// value: "wav", -// }, -// { -// label: "mp3", -// value: "mp3", -// }, -// { -// label: "ogg", -// value: "ogg", -// }, -// ]; +const audioFormat = ref("wav"); +const supportedFormats = [ + { + label: "WAV", + value: "wav", + }, + { + label: "mp3", + value: "mp3", + }, + { + label: "ogg", + value: "ogg", + }, +] as const satisfies { label: string; value: SupportedAudioFormat }[]; // サンプルレート const samplingRate = ref(48000); diff --git a/src/components/Dialog/SettingDialog/FileNameTemplateDialog.stories.ts b/src/components/Dialog/SettingDialog/FileNameTemplateDialog.stories.ts index b550c01a13..cd00934484 100644 --- a/src/components/Dialog/SettingDialog/FileNameTemplateDialog.stories.ts +++ b/src/components/Dialog/SettingDialog/FileNameTemplateDialog.stories.ts @@ -4,7 +4,7 @@ import { Meta, StoryObj } from "@storybook/vue3"; import FileNameTemplateDialog from "./FileNameTemplateDialog.vue"; import { buildAudioFileNameFromRawData, - DEFAULT_AUDIO_FILE_BASE_NAME_TEMPLATE, + DEFAULT_AUDIO_FILE_NAME_TEMPLATE, } from "@/store/utility"; const meta: Meta = { @@ -18,7 +18,7 @@ const meta: Meta = { "date", "projectName", ], - defaultTemplate: DEFAULT_AUDIO_FILE_BASE_NAME_TEMPLATE, + defaultTemplate: DEFAULT_AUDIO_FILE_NAME_TEMPLATE, savedTemplate: "", fileNameBuilder: buildAudioFileNameFromRawData, }, diff --git a/src/components/Dialog/SettingDialog/FileNameTemplateDialog.vue b/src/components/Dialog/SettingDialog/FileNameTemplateDialog.vue index e2f6259b8a..49f94eaa73 100644 --- a/src/components/Dialog/SettingDialog/FileNameTemplateDialog.vue +++ b/src/components/Dialog/SettingDialog/FileNameTemplateDialog.vue @@ -16,12 +16,12 @@
string; + /** ドットまで含んだ拡張子 */ + extension: string; }>(); const emit = defineEmits<{ @@ -109,27 +111,18 @@ const tagStrings = computed(() => props.availableTags.map((tag) => replaceTagIdToTagString[tag]), ); -const savedTemplateWithoutExt = computed(() => - props.savedTemplate.replace(/\.wav$/, ""), -); -const temporaryTemplateWithoutExt = ref(savedTemplateWithoutExt.value); -const temporaryTemplate = computed( - () => temporaryTemplateWithoutExt.value + ".wav", -); +const temporaryTemplate = ref(props.savedTemplate); const missingIndexTagString = computed( - () => - !temporaryTemplateWithoutExt.value.includes( - replaceTagIdToTagString["index"], - ), + () => !temporaryTemplate.value.includes(replaceTagIdToTagString["index"]), ); const invalidChar = computed(() => { - const current = temporaryTemplateWithoutExt.value; + const current = temporaryTemplate.value; const sanitized = sanitizeFileName(current); return Array.from(current).find((char, i) => char !== sanitized[i]); }); const errorMessage = computed(() => { - if (temporaryTemplateWithoutExt.value === "") { + if (temporaryTemplate.value === "") { return "何か入力してください"; } @@ -148,18 +141,18 @@ const errorMessage = computed(() => { const hasError = computed(() => errorMessage.value !== ""); const previewFileName = computed(() => - props.fileNameBuilder(`${temporaryTemplateWithoutExt.value}.wav`), + props.fileNameBuilder(`${temporaryTemplate.value}${props.extension}`), ); const initializeInput = () => { - temporaryTemplateWithoutExt.value = savedTemplateWithoutExt.value; + temporaryTemplate.value = props.savedTemplate; - if (temporaryTemplateWithoutExt.value === "") { - temporaryTemplateWithoutExt.value = props.defaultTemplate; + if (temporaryTemplate.value === "") { + temporaryTemplate.value = props.defaultTemplate; } }; const resetToDefault = () => { - temporaryTemplateWithoutExt.value = props.defaultTemplate; + temporaryTemplate.value = props.defaultTemplate; patternInput.value?.focus(); }; @@ -175,7 +168,7 @@ const insertTagToCurrentPosition = (tag: string) => { const from = elem.selectionStart ?? 0; const to = elem.selectionEnd ?? 0; const newText = text.substring(0, from) + tag + text.substring(to); - temporaryTemplateWithoutExt.value = newText; + temporaryTemplate.value = newText; // キャレットの位置を挿入した後の位置にずらす void nextTick(() => { diff --git a/src/components/Dialog/SettingDialog/SettingDialog.vue b/src/components/Dialog/SettingDialog/SettingDialog.vue index 81a819356c..413f1ef61c 100644 --- a/src/components/Dialog/SettingDialog/SettingDialog.vue +++ b/src/components/Dialog/SettingDialog/SettingDialog.vue @@ -281,7 +281,7 @@ { +import { + createMp3Encoder, + createOggEncoder, + WasmMediaEncoder, +} from "wasm-media-encoders"; + +export type SupportedAudioFormat = "wav" | "mp3" | "ogg"; + +const convertToWavFileData = (audioBuffer: AudioBuffer) => { const bytesPerSample = 4; // Float32 const formatCode = 3; // WAVE_FORMAT_IEEE_FLOAT @@ -52,5 +60,71 @@ export const convertToWavFileData = (audioBuffer: AudioBuffer) => { } } - return buffer; + return new Uint8Array(buffer); +}; + +export const encodeAudioData = async ( + audioBuffer: AudioBuffer, + encoder: WasmMediaEncoder<"audio/ogg" | "audio/mpeg">, +) => { + let outBuffer = new Uint8Array(1024 * 1024); + let offset = 0; + let moreData = true; + encoder.configure({ + channels: audioBuffer.numberOfChannels as 1 | 2, + sampleRate: audioBuffer.sampleRate, + }); + + while (true) { + const mp3Data = moreData + ? encoder.encode( + audioBuffer.numberOfChannels === 1 + ? [audioBuffer.getChannelData(0)] + : [audioBuffer.getChannelData(0), audioBuffer.getChannelData(1)], + ) + : encoder.finalize(); + + if (mp3Data.length + offset > outBuffer.length) { + const newBuffer = new Uint8Array(mp3Data.length + offset); + newBuffer.set(outBuffer); + outBuffer = newBuffer; + } + + outBuffer.set(mp3Data, offset); + offset += mp3Data.length; + + if (!moreData) { + break; + } + + moreData = false; + } + + return outBuffer.slice(0, offset); +}; + +const convertToMp3Data = async (audioBuffer: AudioBuffer) => { + const encoder = await createMp3Encoder(); + const mp3Data = await encodeAudioData(audioBuffer, encoder); + return mp3Data; +}; + +const convertToOggData = async (audioBuffer: AudioBuffer) => { + const encoder = await createOggEncoder(); + const oggData = await encodeAudioData(audioBuffer, encoder); + return oggData; +}; + +export const convertToSupportedAudioFormat = async ( + audioBuffer: AudioBuffer, + format: SupportedAudioFormat, +) => { + switch (format) { + case "wav": + return convertToWavFileData(audioBuffer); + case "mp3": + return convertToMp3Data(audioBuffer); + case "ogg": + return convertToOggData(audioBuffer); + } }; diff --git a/src/store/singing.ts b/src/store/singing.ts index 5280f89151..5421f60134 100644 --- a/src/store/singing.ts +++ b/src/store/singing.ts @@ -100,13 +100,12 @@ import { getOrThrow } from "@/helpers/mapHelper"; import { cloneWithUnwrapProxy } from "@/helpers/cloneWithUnwrapProxy"; import { ufProjectToVoicevox } from "@/sing/utaformatixProject/toVoicevox"; import { uuid4 } from "@/helpers/random"; -import { convertToWavFileData } from "@/sing/encodeAudioData"; +import { convertToSupportedAudioFormat } from "@/sing/encodeAudioData"; import { generateWriteErrorMessage } from "@/helpers/fileHelper"; import { PhraseRenderStageId, createPhraseRenderer, } from "@/sing/phraseRendering"; -import { UnreachableError } from "@/type/utility"; const logger = createLogger("store/singing"); @@ -132,7 +131,7 @@ const generateNoteEvents = (notes: Note[], tempos: Tempo[], tpqn: number) => { }); }; -const generateDefaultSongFileName = ( +const generateDefaultSongFileBaseName = ( projectName: string | undefined, selectedTrack: Track, getCharacterInfo: ( @@ -141,7 +140,7 @@ const generateDefaultSongFileName = ( ) => CharacterInfo | undefined, ) => { if (projectName) { - return projectName + ".wav"; + return projectName; } const singer = selectedTrack.singer; @@ -151,11 +150,11 @@ const generateDefaultSongFileName = ( if (singerName) { const notes = selectedTrack.notes.slice(0, 5); const beginningPartLyrics = notes.map((note) => note.lyric).join(""); - return sanitizeFileName(`${singerName}_${beginningPartLyrics}.wav`); + return sanitizeFileName(`${singerName}_${beginningPartLyrics}`); } } - return `${DEFAULT_PROJECT_NAME}.wav`; + return DEFAULT_PROJECT_NAME; }; const offlineRenderTracks = async ( @@ -1941,11 +1940,12 @@ export const singingStore = createPartialStore({ action: createUILockAction( async ({ state, mutations, getters, actions }, { filePath, setting }) => { const exportAudioFile = async (): Promise => { - const fileName = generateDefaultSongFileName( + const fileBaseName = generateDefaultSongFileBaseName( getters.PROJECT_NAME, getters.SELECTED_TRACK, getters.CHARACTER_INFO, ); + const fileName = `${fileBaseName}.${setting.audioFormat}`; const numberOfChannels = setting.isStereo ? 2 : 1; const sampleRate = setting.sampleRate; const withLimiter = setting.withLimiter; @@ -1962,6 +1962,7 @@ export const singingStore = createPartialStore({ filePath ??= await window.backend.showAudioSaveDialog({ title: "音声を保存", defaultPath: fileName, + formats: [setting.audioFormat], }); } if (!filePath) { @@ -1970,9 +1971,12 @@ export const singingStore = createPartialStore({ if (state.savingSetting.avoidOverwrite) { let tail = 1; - const name = filePath.slice(0, filePath.length - 4); + const pathWithoutExt = filePath.slice( + 0, + -1 - setting.audioFormat.length, + ); while (await window.backend.checkFileExists(filePath)) { - filePath = name + "[" + tail.toString() + "]" + ".wav"; + filePath = `${pathWithoutExt}[${tail}].${setting.audioFormat}`; tail += 1; } } @@ -1999,14 +2003,10 @@ export const singingStore = createPartialStore({ phraseSingingVoices, ); - let fileData; - switch (setting.audioFormat) { - case "wav": - fileData = convertToWavFileData(audioBuffer); - break; - default: - throw new UnreachableError("Not implemented"); - } + const fileData = await convertToSupportedAudioFormat( + audioBuffer, + setting.audioFormat, + ); const result = await actions.EXPORT_FILE({ filePath, @@ -2105,12 +2105,18 @@ export const singingStore = createPartialStore({ trackName: track.name, }, ); - let filePath = path.join(dirPath, trackFileName); + let filePath = path.join( + dirPath, + `${trackFileName}.${setting.audioFormat}`, + ); if (state.savingSetting.avoidOverwrite) { let tail = 1; - const name = filePath.slice(0, filePath.length - 4); + const pathWithoutExt = filePath.slice( + 0, + -1 - setting.audioFormat.length, + ); while (await window.backend.checkFileExists(filePath)) { - filePath = name + "[" + tail.toString() + "]" + ".wav"; + filePath = `${pathWithoutExt}[${tail}].${setting.audioFormat}`; tail += 1; } } @@ -2130,14 +2136,10 @@ export const singingStore = createPartialStore({ singingVoiceCache, ); - let fileData; - switch (setting.audioFormat) { - case "wav": - fileData = convertToWavFileData(audioBuffer); - break; - default: - throw new UnreachableError("Not implemented"); - } + const fileData = await convertToSupportedAudioFormat( + audioBuffer, + setting.audioFormat, + ); const result = await actions.EXPORT_FILE({ filePath, diff --git a/src/store/type.ts b/src/store/type.ts index ac04724d2b..998606e064 100644 --- a/src/store/type.ts +++ b/src/store/type.ts @@ -71,6 +71,7 @@ import { timeSignatureSchema, trackSchema, } from "@/domain/project/schema"; +import { SupportedAudioFormat as SongSupportedAudioFormat } from "@/sing/encodeAudioData"; /** * エディタ用のAudioQuery @@ -815,7 +816,6 @@ export const PhraseKey = (id: string): PhraseKey => phraseKeySchema.parse(id); export type SequencerEditTarget = "NOTE" | "PITCH"; -export type SongSupportedAudioFormat = "wav" | "mp3" | "ogg"; export type SongExportSetting = { isStereo: boolean; sampleRate: number; @@ -1087,7 +1087,7 @@ export type SingingStoreTypes = { EXPORT_FILE: { action(payload: { filePath: string; - content: ArrayBuffer; + content: Uint8Array; }): Promise; }; diff --git a/src/store/utility.ts b/src/store/utility.ts index 9fc4bdd8fd..405deeec33 100644 --- a/src/store/utility.ts +++ b/src/store/utility.ts @@ -130,9 +130,8 @@ const replaceTagStringToTagId: Record = Object.entries( replaceTagIdToTagString, ).reduce((prev, [k, v]) => ({ ...prev, [v]: k }), {}); -export const DEFAULT_AUDIO_FILE_BASE_NAME_TEMPLATE = +export const DEFAULT_AUDIO_FILE_NAME_TEMPLATE = "$連番$_$キャラ$($スタイル$)_$テキスト$"; -export const DEFAULT_AUDIO_FILE_NAME_TEMPLATE = `${DEFAULT_AUDIO_FILE_BASE_NAME_TEMPLATE}.wav`; const DEFAULT_AUDIO_FILE_NAME_VARIABLES = { index: 0, characterName: "四国めたん", @@ -142,9 +141,8 @@ const DEFAULT_AUDIO_FILE_NAME_VARIABLES = { projectName: "VOICEVOXプロジェクト", }; -export const DEFAULT_SONG_AUDIO_FILE_BASE_NAME_TEMPLATE = +export const DEFAULT_SONG_AUDIO_FILE_NAME_TEMPLATE = "$連番$_$キャラ$($スタイル$)_$トラック名$"; -export const DEFAULT_SONG_AUDIO_FILE_NAME_TEMPLATE = `${DEFAULT_SONG_AUDIO_FILE_BASE_NAME_TEMPLATE}.wav`; const DEFAULT_SONG_AUDIO_FILE_NAME_VARIABLES = { index: 0, characterName: "四国めたん", diff --git a/src/type/ipc.ts b/src/type/ipc.ts index 006aca3532..8217524955 100644 --- a/src/type/ipc.ts +++ b/src/type/ipc.ts @@ -14,6 +14,7 @@ import { } from "@/type/preload"; import { AltPortInfos } from "@/store/type"; import { Result } from "@/type/result"; +import { SupportedAudioFormat } from "@/sing/encodeAudioData"; /** * invoke, handle @@ -70,7 +71,13 @@ export type IpcIHData = { }; SHOW_AUDIO_SAVE_DIALOG: { - args: [obj: { title: string; defaultPath?: string }]; + args: [ + obj: { + title: string; + defaultPath?: string; + formats?: SupportedAudioFormat[]; + }, + ]; return?: string; }; @@ -275,7 +282,7 @@ export type IpcIHData = { }; WRITE_FILE: { - args: [obj: { filePath: string; buffer: ArrayBuffer }]; + args: [obj: { filePath: string; buffer: ArrayBuffer | Uint8Array }]; return: Result; }; diff --git a/src/type/preload.ts b/src/type/preload.ts index fac2c9b288..1df72ff36b 100644 --- a/src/type/preload.ts +++ b/src/type/preload.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { IpcSOData } from "./ipc"; import { AltPortInfos } from "@/store/type"; import { Result } from "@/type/result"; +import { SupportedAudioFormat } from "@/sing/encodeAudioData"; export const isProduction = import.meta.env.MODE === "production"; export const isElectron = import.meta.env.VITE_TARGET === "electron"; @@ -223,6 +224,7 @@ export interface Sandbox { showAudioSaveDialog(obj: { title: string; defaultPath?: string; + formats?: SupportedAudioFormat[]; }): Promise; showTextSaveDialog(obj: { title: string; @@ -259,7 +261,7 @@ export interface Sandbox { }): Promise; writeFile(obj: { filePath: string; - buffer: ArrayBuffer; + buffer: ArrayBuffer | Uint8Array; }): Promise>; readFile(obj: { filePath: string }): Promise>; isAvailableGPUMode(): Promise; From 235c8c01567b8c59c707e6cefb909bfe7048928b Mon Sep 17 00:00:00 2001 From: "Nanashi." Date: Tue, 8 Oct 2024 22:01:24 +0900 Subject: [PATCH 07/31] =?UTF-8?q?Change:=20=E3=83=A1=E3=83=A2=20->=20NOTE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Hiroshiba --- src/components/Dialog/ExportSongAudioDialog/Presentation.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Dialog/ExportSongAudioDialog/Presentation.vue b/src/components/Dialog/ExportSongAudioDialog/Presentation.vue index 790e8e82d8..3bb115262c 100644 --- a/src/components/Dialog/ExportSongAudioDialog/Presentation.vue +++ b/src/components/Dialog/ExportSongAudioDialog/Presentation.vue @@ -102,7 +102,7 @@ diff --git a/src/components/Dialog/SettingDialog/FileNameTemplateDialog.vue b/src/components/Dialog/SettingDialog/FileNameTemplateDialog.vue index 625534d817..08c1de26e9 100644 --- a/src/components/Dialog/SettingDialog/FileNameTemplateDialog.vue +++ b/src/components/Dialog/SettingDialog/FileNameTemplateDialog.vue @@ -39,7 +39,7 @@
- 出力例:{{ previewFileName + props.extension }} + 出力例:{{ previewFileName }}
{ }); const hasError = computed(() => errorMessage.value !== ""); -const previewFileName = computed(() => - props.fileNameBuilder(`${temporaryTemplate.value}${props.extension}`), +const previewFileName = computed( + () => props.fileNameBuilder(temporaryTemplate.value) + props.extension, ); const initializeInput = () => { From 5865b946fe53ea4a94ee403d3bb761ac26dcfebe Mon Sep 17 00:00:00 2001 From: sevenc-nanashi Date: Wed, 9 Oct 2024 19:16:15 +0900 Subject: [PATCH 12/31] =?UTF-8?q?Fix:=20=E3=83=97=E3=83=AC=E3=83=93?= =?UTF-8?q?=E3=83=A5=E3=83=BC=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Dialog/SettingDialog/SettingDialog.vue | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/Dialog/SettingDialog/SettingDialog.vue b/src/components/Dialog/SettingDialog/SettingDialog.vue index 413f1ef61c..69835fa1f0 100644 --- a/src/components/Dialog/SettingDialog/SettingDialog.vue +++ b/src/components/Dialog/SettingDialog/SettingDialog.vue @@ -318,7 +318,7 @@ @@ -362,7 +362,7 @@ @@ -796,6 +796,12 @@ const audioFileNamePattern = computed( const songTrackFileNamePattern = computed( () => store.state.savingSetting.songTrackFileNamePattern, ); +const audioFileNamePatternWithExt = computed(() => + audioFileNamePattern.value ? audioFileNamePattern.value + ".wav" : "", +); +const songTrackFileNamePatternWithExt = computed(() => + songTrackFileNamePattern.value ? songTrackFileNamePattern.value + ".wav" : "", +); const gpuSwitchEnabled = (engineId: EngineId) => { // CPU版でもGPUモードからCPUモードに変更できるようにする From dea09c30f9987ae83e6ef2261122e4a33c0faab3 Mon Sep 17 00:00:00 2001 From: sevenc-nanashi Date: Thu, 10 Oct 2024 19:38:33 +0900 Subject: [PATCH 13/31] =?UTF-8?q?Delete:=20=E3=83=95=E3=82=A9=E3=83=BC?= =?UTF-8?q?=E3=83=9E=E3=83=83=E3=83=88=E9=81=B8=E6=8A=9E=E3=82=92=E5=89=8A?= =?UTF-8?q?=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 18 +-- package.json | 1 - src/backend/electron/preload.ts | 3 +- .../ExportSongAudioDialog/Presentation.vue | 36 ----- src/sing/convertToWavFileData.ts | 56 ++++++++ src/sing/encodeAudioData.ts | 130 ------------------ src/store/singing.ts | 17 +-- src/store/type.ts | 2 - src/type/ipc.ts | 2 - src/type/preload.ts | 1 - 10 files changed, 67 insertions(+), 199 deletions(-) create mode 100644 src/sing/convertToWavFileData.ts delete mode 100644 src/sing/encodeAudioData.ts diff --git a/package-lock.json b/package-lock.json index 7d314206df..fb8de7ec62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,6 @@ "vue": "3.4.26", "vuedraggable": "4.1.0", "vuex": "4.0.2", - "wasm-media-encoders": "0.7.0", "zod": "3.22.4" }, "devDependencies": { @@ -7111,6 +7110,9 @@ "version": "0.5.13", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.13.tgz", "integrity": "sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==", + "dev": true, + "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -22282,7 +22284,8 @@ "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true }, "node_modules/tsutils": { "version": "3.21.0", @@ -24037,17 +24040,6 @@ "makeerror": "1.0.12" } }, - "node_modules/wasm-media-encoders": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/wasm-media-encoders/-/wasm-media-encoders-0.7.0.tgz", - "integrity": "sha512-Sp4wUasgxOK/IFfNhpon6LQQgYGwtpxyV4isjGIe1rvhnJL3w2KYr4f+CdqDNtGPDcgCDRY+uBanVfSx6Si0WQ==", - "dependencies": { - "@swc/helpers": "^0.5.10" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/watchpack": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", diff --git a/package.json b/package.json index 76a5e44a0c..75e20bc3cc 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,6 @@ "vue": "3.4.26", "vuedraggable": "4.1.0", "vuex": "4.0.2", - "wasm-media-encoders": "0.7.0", "zod": "3.22.4" }, "optionalDependencies": { diff --git a/src/backend/electron/preload.ts b/src/backend/electron/preload.ts index 017b09cfcc..c94d3d1ea5 100644 --- a/src/backend/electron/preload.ts +++ b/src/backend/electron/preload.ts @@ -54,11 +54,10 @@ const api: Sandbox = { return await ipcRendererInvokeProxy.GET_ALT_PORT_INFOS(); }, - showAudioSaveDialog: ({ title, defaultPath, formats }) => { + showAudioSaveDialog: ({ title, defaultPath }) => { return ipcRendererInvokeProxy.SHOW_AUDIO_SAVE_DIALOG({ title, defaultPath, - formats, }); }, diff --git a/src/components/Dialog/ExportSongAudioDialog/Presentation.vue b/src/components/Dialog/ExportSongAudioDialog/Presentation.vue index 3bb115262c..c5412d3658 100644 --- a/src/components/Dialog/ExportSongAudioDialog/Presentation.vue +++ b/src/components/Dialog/ExportSongAudioDialog/Presentation.vue @@ -24,23 +24,6 @@ dense /> - - - ("master"); // ステレオ const isStereo = ref(true); -// フォーマット選択 -const audioFormat = ref("wav"); -const supportedFormats = [ - { - label: "WAV", - value: "wav", - }, - { - label: "mp3", - value: "mp3", - }, - { - label: "ogg", - value: "ogg", - }, -] as const satisfies { label: string; value: SupportedAudioFormat }[]; - // サンプルレート const samplingRate = ref(48000); const samplingRateOptions = [24000, 44100, 48000, 88200, 96000]; @@ -167,7 +132,6 @@ const handleExportTrack = () => { emit("exportAudio", exportTarget.value, { isStereo: isStereo.value, sampleRate: samplingRate.value, - audioFormat: audioFormat.value, withLimiter: withLimiter.value, withTrackParameters: withTrackParameters.value, }); diff --git a/src/sing/convertToWavFileData.ts b/src/sing/convertToWavFileData.ts new file mode 100644 index 0000000000..1ddfb637e4 --- /dev/null +++ b/src/sing/convertToWavFileData.ts @@ -0,0 +1,56 @@ +export const convertToWavFileData = (audioBuffer: AudioBuffer) => { + const bytesPerSample = 4; // Float32 + const formatCode = 3; // WAVE_FORMAT_IEEE_FLOAT + + const numberOfChannels = audioBuffer.numberOfChannels; + const numberOfSamples = audioBuffer.length; + const sampleRate = audioBuffer.sampleRate; + const byteRate = sampleRate * numberOfChannels * bytesPerSample; + const blockSize = numberOfChannels * bytesPerSample; + const dataSize = numberOfSamples * numberOfChannels * bytesPerSample; + + const buffer = new ArrayBuffer(44 + dataSize); + const dataView = new DataView(buffer); + + let pos = 0; + const writeString = (value: string) => { + for (let i = 0; i < value.length; i++) { + dataView.setUint8(pos, value.charCodeAt(i)); + pos += 1; + } + }; + const writeUint32 = (value: number) => { + dataView.setUint32(pos, value, true); + pos += 4; + }; + const writeUint16 = (value: number) => { + dataView.setUint16(pos, value, true); + pos += 2; + }; + const writeSample = (offset: number, value: number) => { + dataView.setFloat32(pos + offset * 4, value, true); + }; + + writeString("RIFF"); + writeUint32(36 + dataSize); // RIFFチャンクサイズ + writeString("WAVE"); + writeString("fmt "); + writeUint32(16); // fmtチャンクサイズ + writeUint16(formatCode); + writeUint16(numberOfChannels); + writeUint32(sampleRate); + writeUint32(byteRate); + writeUint16(blockSize); + writeUint16(bytesPerSample * 8); // 1サンプルあたりのビット数 + writeString("data"); + writeUint32(dataSize); + + for (let i = 0; i < numberOfChannels; i++) { + const channelData = audioBuffer.getChannelData(i); + for (let j = 0; j < numberOfSamples; j++) { + writeSample(j * numberOfChannels + i, channelData[j]); + } + } + + return new Uint8Array(buffer); +}; diff --git a/src/sing/encodeAudioData.ts b/src/sing/encodeAudioData.ts deleted file mode 100644 index ce0b9ef57d..0000000000 --- a/src/sing/encodeAudioData.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { - createMp3Encoder, - createOggEncoder, - WasmMediaEncoder, -} from "wasm-media-encoders"; - -export type SupportedAudioFormat = "wav" | "mp3" | "ogg"; - -const convertToWavFileData = (audioBuffer: AudioBuffer) => { - const bytesPerSample = 4; // Float32 - const formatCode = 3; // WAVE_FORMAT_IEEE_FLOAT - - const numberOfChannels = audioBuffer.numberOfChannels; - const numberOfSamples = audioBuffer.length; - const sampleRate = audioBuffer.sampleRate; - const byteRate = sampleRate * numberOfChannels * bytesPerSample; - const blockSize = numberOfChannels * bytesPerSample; - const dataSize = numberOfSamples * numberOfChannels * bytesPerSample; - - const buffer = new ArrayBuffer(44 + dataSize); - const dataView = new DataView(buffer); - - let pos = 0; - const writeString = (value: string) => { - for (let i = 0; i < value.length; i++) { - dataView.setUint8(pos, value.charCodeAt(i)); - pos += 1; - } - }; - const writeUint32 = (value: number) => { - dataView.setUint32(pos, value, true); - pos += 4; - }; - const writeUint16 = (value: number) => { - dataView.setUint16(pos, value, true); - pos += 2; - }; - const writeSample = (offset: number, value: number) => { - dataView.setFloat32(pos + offset * 4, value, true); - }; - - writeString("RIFF"); - writeUint32(36 + dataSize); // RIFFチャンクサイズ - writeString("WAVE"); - writeString("fmt "); - writeUint32(16); // fmtチャンクサイズ - writeUint16(formatCode); - writeUint16(numberOfChannels); - writeUint32(sampleRate); - writeUint32(byteRate); - writeUint16(blockSize); - writeUint16(bytesPerSample * 8); // 1サンプルあたりのビット数 - writeString("data"); - writeUint32(dataSize); - - for (let i = 0; i < numberOfChannels; i++) { - const channelData = audioBuffer.getChannelData(i); - for (let j = 0; j < numberOfSamples; j++) { - writeSample(j * numberOfChannels + i, channelData[j]); - } - } - - return new Uint8Array(buffer); -}; - -export const encodeAudioData = async ( - audioBuffer: AudioBuffer, - encoder: WasmMediaEncoder<"audio/ogg" | "audio/mpeg">, -) => { - let outBuffer = new Uint8Array(1024 * 1024); - let offset = 0; - let moreData = true; - encoder.configure({ - channels: audioBuffer.numberOfChannels as 1 | 2, - sampleRate: audioBuffer.sampleRate, - }); - - while (true) { - const mp3Data = moreData - ? encoder.encode( - audioBuffer.numberOfChannels === 1 - ? [audioBuffer.getChannelData(0)] - : [audioBuffer.getChannelData(0), audioBuffer.getChannelData(1)], - ) - : encoder.finalize(); - - if (mp3Data.length + offset > outBuffer.length) { - const newBuffer = new Uint8Array(mp3Data.length + offset); - newBuffer.set(outBuffer); - outBuffer = newBuffer; - } - - outBuffer.set(mp3Data, offset); - offset += mp3Data.length; - - if (!moreData) { - break; - } - - moreData = false; - } - - return outBuffer.slice(0, offset); -}; - -const convertToMp3Data = async (audioBuffer: AudioBuffer) => { - const encoder = await createMp3Encoder(); - const mp3Data = await encodeAudioData(audioBuffer, encoder); - return mp3Data; -}; - -const convertToOggData = async (audioBuffer: AudioBuffer) => { - const encoder = await createOggEncoder(); - const oggData = await encodeAudioData(audioBuffer, encoder); - return oggData; -}; - -export const convertToSupportedAudioFormat = async ( - audioBuffer: AudioBuffer, - format: SupportedAudioFormat, -) => { - switch (format) { - case "wav": - return convertToWavFileData(audioBuffer); - case "mp3": - return convertToMp3Data(audioBuffer); - case "ogg": - return convertToOggData(audioBuffer); - } -}; diff --git a/src/store/singing.ts b/src/store/singing.ts index 5421f60134..cdfaf38cc6 100644 --- a/src/store/singing.ts +++ b/src/store/singing.ts @@ -100,7 +100,7 @@ import { getOrThrow } from "@/helpers/mapHelper"; import { cloneWithUnwrapProxy } from "@/helpers/cloneWithUnwrapProxy"; import { ufProjectToVoicevox } from "@/sing/utaformatixProject/toVoicevox"; import { uuid4 } from "@/helpers/random"; -import { convertToSupportedAudioFormat } from "@/sing/encodeAudioData"; +import { convertToWavFileData } from "@/sing/convertToWavFileData"; import { generateWriteErrorMessage } from "@/helpers/fileHelper"; import { PhraseRenderStageId, @@ -1945,7 +1945,7 @@ export const singingStore = createPartialStore({ getters.SELECTED_TRACK, getters.CHARACTER_INFO, ); - const fileName = `${fileBaseName}.${setting.audioFormat}`; + const fileName = `${fileBaseName}.wav`; const numberOfChannels = setting.isStereo ? 2 : 1; const sampleRate = setting.sampleRate; const withLimiter = setting.withLimiter; @@ -1962,7 +1962,6 @@ export const singingStore = createPartialStore({ filePath ??= await window.backend.showAudioSaveDialog({ title: "音声を保存", defaultPath: fileName, - formats: [setting.audioFormat], }); } if (!filePath) { @@ -1971,12 +1970,9 @@ export const singingStore = createPartialStore({ if (state.savingSetting.avoidOverwrite) { let tail = 1; - const pathWithoutExt = filePath.slice( - 0, - -1 - setting.audioFormat.length, - ); + const pathWithoutExt = filePath.slice(0, -4); while (await window.backend.checkFileExists(filePath)) { - filePath = `${pathWithoutExt}[${tail}].${setting.audioFormat}`; + filePath = `${pathWithoutExt}[${tail}].wav`; tail += 1; } } @@ -2003,10 +1999,7 @@ export const singingStore = createPartialStore({ phraseSingingVoices, ); - const fileData = await convertToSupportedAudioFormat( - audioBuffer, - setting.audioFormat, - ); + const fileData = convertToWavFileData(audioBuffer); const result = await actions.EXPORT_FILE({ filePath, diff --git a/src/store/type.ts b/src/store/type.ts index f9c9434073..6bf35034db 100644 --- a/src/store/type.ts +++ b/src/store/type.ts @@ -71,7 +71,6 @@ import { timeSignatureSchema, trackSchema, } from "@/domain/project/schema"; -import { SupportedAudioFormat as SongSupportedAudioFormat } from "@/sing/encodeAudioData"; /** * エディタ用のAudioQuery @@ -819,7 +818,6 @@ export type SequencerEditTarget = "NOTE" | "PITCH"; export type SongExportSetting = { isStereo: boolean; sampleRate: number; - audioFormat: SongSupportedAudioFormat; withLimiter: boolean; withTrackParameters: boolean; }; diff --git a/src/type/ipc.ts b/src/type/ipc.ts index 8217524955..0311c5efca 100644 --- a/src/type/ipc.ts +++ b/src/type/ipc.ts @@ -14,7 +14,6 @@ import { } from "@/type/preload"; import { AltPortInfos } from "@/store/type"; import { Result } from "@/type/result"; -import { SupportedAudioFormat } from "@/sing/encodeAudioData"; /** * invoke, handle @@ -75,7 +74,6 @@ export type IpcIHData = { obj: { title: string; defaultPath?: string; - formats?: SupportedAudioFormat[]; }, ]; return?: string; diff --git a/src/type/preload.ts b/src/type/preload.ts index 1df72ff36b..f5d5d52be2 100644 --- a/src/type/preload.ts +++ b/src/type/preload.ts @@ -224,7 +224,6 @@ export interface Sandbox { showAudioSaveDialog(obj: { title: string; defaultPath?: string; - formats?: SupportedAudioFormat[]; }): Promise; showTextSaveDialog(obj: { title: string; From e77378ec545f377ba2e1818d59e6a2acca262e9a Mon Sep 17 00:00:00 2001 From: sevenc-nanashi Date: Thu, 10 Oct 2024 19:39:33 +0900 Subject: [PATCH 14/31] =?UTF-8?q?Change:=20=E3=83=86=E3=82=AD=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=82=92=E5=A4=89=E3=81=88=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: sigprogramming --- src/components/Dialog/ExportSongAudioDialog/Presentation.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Dialog/ExportSongAudioDialog/Presentation.vue b/src/components/Dialog/ExportSongAudioDialog/Presentation.vue index c5412d3658..f3d69a41c9 100644 --- a/src/components/Dialog/ExportSongAudioDialog/Presentation.vue +++ b/src/components/Dialog/ExportSongAudioDialog/Presentation.vue @@ -45,7 +45,7 @@ From c7e81a990a0ac130a65f0b3eba32f59b13abe978 Mon Sep 17 00:00:00 2001 From: sevenc-nanashi Date: Thu, 10 Oct 2024 19:42:19 +0900 Subject: [PATCH 15/31] =?UTF-8?q?Update:=20=E8=89=B2=E3=80=85=E6=9B=B4?= =?UTF-8?q?=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/electron/main.ts | 13 +++++++------ src/store/singing.ts | 17 ++++------------- src/type/preload.ts | 1 - 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/src/backend/electron/main.ts b/src/backend/electron/main.ts index ccc63433e0..4e7d0ed1c9 100644 --- a/src/backend/electron/main.ts +++ b/src/backend/electron/main.ts @@ -579,16 +579,17 @@ registerIpcMainHandle({ return engineInfoManager.altPortInfos; }, - SHOW_AUDIO_SAVE_DIALOG: async (_, { title, defaultPath, formats }) => { - formats ??= ["wav"]; + SHOW_AUDIO_SAVE_DIALOG: async (_, { title, defaultPath }) => { const result = await retryShowSaveDialogWhileSafeDir(() => dialog.showSaveDialog(win, { title, defaultPath, - filters: formats.map((format) => ({ - name: `${format}ファイル`, - extensions: [format], - })), + filters: [ + { + name: "wavファイル", + extensions: ["wav"], + }, + ], properties: ["createDirectory"], }), ); diff --git a/src/store/singing.ts b/src/store/singing.ts index cdfaf38cc6..6f42a8f14b 100644 --- a/src/store/singing.ts +++ b/src/store/singing.ts @@ -2098,18 +2098,12 @@ export const singingStore = createPartialStore({ trackName: track.name, }, ); - let filePath = path.join( - dirPath, - `${trackFileName}.${setting.audioFormat}`, - ); + let filePath = path.join(dirPath, `${trackFileName}.wav`); if (state.savingSetting.avoidOverwrite) { let tail = 1; - const pathWithoutExt = filePath.slice( - 0, - -1 - setting.audioFormat.length, - ); + const pathWithoutExt = filePath.slice(0, -4); while (await window.backend.checkFileExists(filePath)) { - filePath = `${pathWithoutExt}[${tail}].${setting.audioFormat}`; + filePath = `${pathWithoutExt}[${tail}].wav`; tail += 1; } } @@ -2129,10 +2123,7 @@ export const singingStore = createPartialStore({ singingVoiceCache, ); - const fileData = await convertToSupportedAudioFormat( - audioBuffer, - setting.audioFormat, - ); + const fileData = convertToWavFileData(audioBuffer); const result = await actions.EXPORT_FILE({ filePath, diff --git a/src/type/preload.ts b/src/type/preload.ts index f5d5d52be2..e3494eec89 100644 --- a/src/type/preload.ts +++ b/src/type/preload.ts @@ -2,7 +2,6 @@ import { z } from "zod"; import { IpcSOData } from "./ipc"; import { AltPortInfos } from "@/store/type"; import { Result } from "@/type/result"; -import { SupportedAudioFormat } from "@/sing/encodeAudioData"; export const isProduction = import.meta.env.MODE === "production"; export const isElectron = import.meta.env.VITE_TARGET === "electron"; From fe0338ed7d59376a00543b1ab0de0a3586ae6f77 Mon Sep 17 00:00:00 2001 From: sevenc-nanashi Date: Thu, 10 Oct 2024 20:08:35 +0900 Subject: [PATCH 16/31] =?UTF-8?q?Change:=20=E3=83=A2=E3=83=8E=E3=83=A9?= =?UTF-8?q?=E3=83=AB=E6=99=82=E3=81=AFpan=3D0=E3=82=92=E4=BD=BF=E3=81=86?= =?UTF-8?q?=E7=94=A8=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ExportSongAudioDialog/Presentation.vue | 41 ++++++++++++++++--- src/store/singing.ts | 22 +++++++--- src/store/type.ts | 3 +- 3 files changed, 53 insertions(+), 13 deletions(-) diff --git a/src/components/Dialog/ExportSongAudioDialog/Presentation.vue b/src/components/Dialog/ExportSongAudioDialog/Presentation.vue index f3d69a41c9..e8034a0080 100644 --- a/src/components/Dialog/ExportSongAudioDialog/Presentation.vue +++ b/src/components/Dialog/ExportSongAudioDialog/Presentation.vue @@ -50,10 +50,15 @@ - + @@ -86,10 +91,11 @@