diff --git a/docs/architecture/SoundsEdit.md b/docs/architecture/SoundsEdit.md new file mode 100644 index 000000000..55988cf75 --- /dev/null +++ b/docs/architecture/SoundsEdit.md @@ -0,0 +1,59 @@ +# Sounds Edit Module + +## Module Purpose + +The Sound Editing Module is designed to provide an interface through which users can edit sound files in their project. This includes operations such as play, pause, adjust playback speed, volume control, and audio region editing operations like cut, copy, paste, and delete. + +## Module Scope + +This module allows users to load an audio file and visually edit it on a waveform graph. Users can visually select audio regions on the waveform for different editing operations. Moreover, users can download or save the edited audio file. + +## Module Structure + +Components: + +- `SoundsHome.vue` - Main page for audio editing +- `SoundsEdit.vue` - Main operational part of audio editing +- `SoundsEditCard.vue` - Renders audio cards +- `wavesurfer-edit.ts` - Waveform operations + +## Module Interface + +### Inputs + +| Parameter | Required | Type | Description | +|-----------| -------- |---------|--------------------| +| SoundList | Yes | Asset[] | Project Audio List | + + +### Outputs + +## Example Usage + +``` + +``` + diff --git a/spx-gui/package-lock.json b/spx-gui/package-lock.json index f785ee876..7df5eba1b 100644 --- a/spx-gui/package-lock.json +++ b/spx-gui/package-lock.json @@ -30,6 +30,7 @@ "@types/file-saver": "^2.0.7", "@types/golang-wasm-exec": "^1.15.2", "@types/node": "^20.11.10", + "@types/wavesurfer.js": "^6.0.12", "@vicons/antd": "^0.12.0", "@vicons/ionicons5": "^0.12.0", "@vicons/material": "^0.12.0", @@ -1930,6 +1931,12 @@ "integrity": "sha512-madaWq2k+LYMEhmcp0fs+OGaLFk0OenpHa4gmI4VEmCKX4PJntQ6fnnGADVFrVkBj0wIdAlQnK/MrlYTHsa1gQ==", "dev": true }, + "node_modules/@types/debounce": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@types/debounce/-/debounce-1.2.4.tgz", + "integrity": "sha512-jBqiORIzKDOToaF63Fm//haOCHuwQuLa2202RK4MozpA6lh93eCBc+/8+wZn5OzjJt3ySdc+74SXWXB55Ewtyw==", + "dev": true + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -1989,6 +1996,15 @@ "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, + "node_modules/@types/wavesurfer.js": { + "version": "6.0.12", + "resolved": "https://registry.npmmirror.com/@types/wavesurfer.js/-/wavesurfer.js-6.0.12.tgz", + "integrity": "sha512-oM9hYlPIVms4uwwoaGs9d0qp7Xk7IjSGkdwgmhUymVUIIilRfjtSQvoOgv4dpKiW0UozWRSyXfQqTobi0qWyCw==", + "dev": true, + "dependencies": { + "@types/debounce": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.20.0.tgz", diff --git a/spx-gui/package.json b/spx-gui/package.json index 916d44b96..b3580d607 100644 --- a/spx-gui/package.json +++ b/spx-gui/package.json @@ -33,6 +33,7 @@ "@types/file-saver": "^2.0.7", "@types/golang-wasm-exec": "^1.15.2", "@types/node": "^20.11.10", + "@types/wavesurfer.js": "^6.0.12", "@vicons/antd": "^0.12.0", "@vicons/ionicons5": "^0.12.0", "@vicons/material": "^0.12.0", diff --git a/spx-gui/src/api/asset.ts b/spx-gui/src/api/asset.ts index b2a62b9b5..2626c4bdc 100644 --- a/spx-gui/src/api/asset.ts +++ b/spx-gui/src/api/asset.ts @@ -4,7 +4,7 @@ * @LastEditors: xuning 453594138@qq.com * @LastEditTime: 2024-02-06 13:43:02 * @FilePath: /builder/spx-gui/src/api/asset.ts - * @Description: + * @Description: */ import { service } from "@/axios"; import type { Asset, PageData } from "@/interface/library.ts"; // Adjust the import paths as needed @@ -44,3 +44,35 @@ export function getAsset(id: number, assetType: number): Promise { }); } + +/** + * Save asset + * + * @param id + * @param name + * @param uid + * @param category + * @param isPublic + * @param assetType The type of the asset. See src/constant/constant.ts for details. + * @param file + */ +export async function saveAsset(id: number, name: string, uid: number, category: string, isPublic: number, assetType: number, file: File): Promise { + const url = '/asset/save'; + const formData = new FormData(); + formData.append('id', id.toString()); + formData.append('name', name); + formData.append('uid', uid.toString()); + formData.append('category', category); + formData.append('isPublic', isPublic ? '1' : '0'); + formData.append('assetType', assetType.toString()); + formData.append('file', file); + + return service({ + url: url, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + }, + }); +} diff --git a/spx-gui/src/assets/icon/sound/save.svg b/spx-gui/src/assets/icon/sound/save.svg new file mode 100644 index 000000000..b253edd61 --- /dev/null +++ b/spx-gui/src/assets/icon/sound/save.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/spx-gui/src/components/code-editor/register.ts b/spx-gui/src/components/code-editor/register.ts index f9439b76f..810d3aab0 100644 --- a/spx-gui/src/components/code-editor/register.ts +++ b/spx-gui/src/components/code-editor/register.ts @@ -4,7 +4,7 @@ * @LastEditors: Zhang Zhi Yang * @LastEditTime: 2024-02-02 11:20:28 * @FilePath: /builder/spx-gui/src/components/code-editor/register.ts - * @Description: + * @Description: */ import { keywords, typeKeywords, LanguageConfig, MonarchTokensProviderConfig } from './language' import wasmModuleUrl from '/wasm/format.wasm?url&wasmModule'; @@ -79,7 +79,7 @@ export const register=()=>{ }); monaco.languages.setLanguageConfiguration('spx', LanguageConfig) - + // Match token and highlight monaco.languages.setMonarchTokensProvider('spx', MonarchTokensProviderConfig); // Code hint diff --git a/spx-gui/src/components/sounds/SoundsEdit.vue b/spx-gui/src/components/sounds/SoundsEdit.vue index 7d6061d48..714d3061c 100644 --- a/spx-gui/src/components/sounds/SoundsEdit.vue +++ b/spx-gui/src/components/sounds/SoundsEdit.vue @@ -10,36 +10,42 @@ -
+
{{ currentSpeed }}x
{{ $t('sounds.undo') }}
{{ $t('sounds.reUndo') }}
- +
{{ $t('sounds.download') }}
+
+ +
{{ $t('sounds.save') }}
+
@@ -52,8 +58,8 @@
@@ -100,36 +106,36 @@
{{ $t('sounds.delete') }}
{{ $t('sounds.cut') }}
{{ $t('sounds.copy') }}
{{ $t('sounds.paste') }}
{{ $t('sounds.insert') }}
@@ -141,10 +147,20 @@ import WaveSurfer from 'wavesurfer.js'; import TimelinePlugin from 'wavesurfer.js/dist/plugin/wavesurfer.timeline.js'; import RegionsPlugin from 'wavesurfer.js/dist/plugin/wavesurfer.regions.js'; import CursorPlugin from 'wavesurfer.js/dist/plugin/wavesurfer.cursor.js'; -import { ref, onMounted, type Ref } from 'vue'; -import { nextTick } from "@vue/runtime-dom"; -import { WavesurferEdit } from "@/util/wavesurferEdit"; +import { ref, onMounted, type Ref, computed, watch } from 'vue' +import { nextTick } from "vue"; +import { WavesurferEdit } from "@/util/wavesurfer-edit"; import { NGradientText, NInput, useMessage, type MessageApi } from "naive-ui"; +import type { Asset } from "@/interface/library"; +import { saveAsset } from '@/api/asset' +import { AssetType } from '@/constant/constant' + +const props = defineProps({ + asset: { + type: Object as () => Asset, + required: true, + } +}) const message: MessageApi = useMessage(); let wavesurfer: WaveSurfer = ref(null); @@ -238,8 +254,15 @@ const initWaveSurfer = () => { }) ], }); - // load music - wavesurfer.load("/audio.mp3"); + // load music TODO replace with real url + // wavesurfer.load("/audio.mp3"); + + console.log(props.asset?.address) + const addressObj = JSON.parse(props.asset?.address); + const assets = addressObj.assets; + const url = assets[Object.keys(assets)[0]] + + wavesurfer.load(url); wavesurfer.on('ready', () => { buffer = wavesurfer.backend.buffer!; @@ -389,11 +412,25 @@ function backward(): void { wavesurfer.skip(-5) } +/* Save sound */ +async function saveSound(): void { + const wavBlob = audioBufferToWavBlob(wavesurfer.backend.buffer); + const wavFile = wavBlobToFile(wavBlob, "example.wav"); + await saveAsset(props.asset?.id, props.asset?.name, props.asset?.id, "sound", 1, AssetType.Sounds, wavFile); + console.log("Save successfully") +} + +/* Convert WAV Blob to File */ +function wavBlobToFile(wavBlob: Blob, fileName: string): File { + return new File([wavBlob], fileName, { type: 'audio/wav' }); +} + /* Download sound file */ function downloadSound(): void { - downloadAudioBuffer(wavesurfer.backend.buffer, "sound.wav"); + downloadAudioBuffer(wavesurfer.backend.buffer, props.asset.name + ".wav"); } + /* Transfer AudioBuffer to WAV Blob */ function audioBufferToWavBlob(audioBuffer: AudioBuffer): Blob { const numOfChan = audioBuffer.numberOfChannels; @@ -537,14 +574,14 @@ function downloadAudioBuffer(audioBuffer: AudioBuffer, filename: string): void { .sound-icon-container { font-size: 18px; - color: #474343; /* 默认颜色 */ + color: #474343; display: flex; flex-direction: column; align-items: center; } .sound-icon-container.disabled { - color: grey; /* 当添加了 disabled 类时的颜色 */ + color: grey; } .sound-icon-container button[disabled] { diff --git a/spx-gui/src/components/sounds/SoundsEditCard.vue b/spx-gui/src/components/sounds/SoundsEditCard.vue index f1221b176..da2b4178d 100644 --- a/spx-gui/src/components/sounds/SoundsEditCard.vue +++ b/spx-gui/src/components/sounds/SoundsEditCard.vue @@ -16,7 +16,6 @@ {{ props.asset.name }}
- 0.85s
@@ -42,8 +41,8 @@ const props = defineProps({ .sounds-card { margin-top: 10px; - width: 125px; - height: 125px; + width: 120px; + height: 120px; border-radius: 20px; background: linear-gradient(145deg, $sounds-edit-card-1, $sounds-edit-card-2); box-shadow: 0 0 5px rgb(220, 79, 35); diff --git a/spx-gui/src/components/sounds/SoundsHome.vue b/spx-gui/src/components/sounds/SoundsHome.vue index 5eaab6e54..98e5792cd 100644 --- a/spx-gui/src/components/sounds/SoundsHome.vue +++ b/spx-gui/src/components/sounds/SoundsHome.vue @@ -2,7 +2,7 @@ * @Author: Yao xinyue kother@qq.com * @Date: 2024-01-12 17:27:57 * @LastEditors: Xu Ning - * @LastEditTime: 2024-01-24 18:02:58 + * @LastEditTime: 2024-02-04 13:42:12 * @FilePath: /builder/spx-gui/src/components/sounds/SoundsHome.vue * @Description: Sounds Homepage, includes Edit Part And Card List --> @@ -10,7 +10,7 @@ - + @@ -36,17 +41,39 @@ import { getAssetList } from "@/api/asset"; import { AssetType } from "@/constant/constant"; const assets = ref([]); +const selectedAsset = ref(null); +const componentKey = ref(0); onMounted(async () => { try { - const pageIndex = 1; - const pageSize = 10; - const response = await getAssetList(pageIndex, pageSize, AssetType.Sounds); - assets.value = response.data; + assets.value = await fetchAssets(AssetType.Sounds); + if (assets.value.length > 0) { + selectedAsset.value = assets.value[0]; + } } catch (error) { console.error("Error fetching assets:", error); } }); + +const fetchAssets = async (assetType: number, category?: string) => { + try { + const pageIndex = 1; + const pageSize = 20; + const response = await getAssetList(pageIndex, pageSize, assetType, category); + if (response.data.data.data == null) + return []; + return response.data.data.data; + } catch (error) { + console.error("Error fetching assets:", error); + return []; + } +}; + +const handleSelect = (asset: Asset) => { + selectedAsset.value = asset; + componentKey.value++; // Increment the key to force re-creation of SoundsEdit +}; +