Skip to content

Commit

Permalink
Update sounds edit and doc (goplus#84)
Browse files Browse the repository at this point in the history
  • Loading branch information
AKother authored Feb 8, 2024
1 parent 916a287 commit d12c793
Show file tree
Hide file tree
Showing 11 changed files with 216 additions and 42 deletions.
59 changes: 59 additions & 0 deletions docs/architecture/SoundsEdit.md
Original file line number Diff line number Diff line change
@@ -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

```
<template>
<n-layout has-sider style="height: calc(100vh - 60px - 54px - 12px)">
<n-layout-sider
:native-scrollbar="false"
content-style="paddingLeft: 130px;"
style="width: 175px"
>
<SoundsEditCard
v-for="asset in assets"
:key="asset.id"
:asset="asset"
:style="{ 'margin-bottom': '26px' }"
@click="handleSelect(asset)"
/>
</n-layout-sider>
<n-layout-content>
<SoundsEdit
:key="componentKey"
:asset="selectedAsset"
style="margin-left: 10px"
/>
</n-layout-content>
</n-layout>
</template>
```

16 changes: 16 additions & 0 deletions spx-gui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions spx-gui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
34 changes: 33 additions & 1 deletion spx-gui/src/api/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* @LastEditors: xuning [email protected]
* @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
Expand Down Expand Up @@ -44,3 +44,35 @@ export function getAsset(id: number, assetType: number): Promise<Asset> {
});
}


/**
* 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<Asset> {
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'
},
});
}
1 change: 1 addition & 0 deletions spx-gui/src/assets/icon/sound/save.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions spx-gui/src/components/code-editor/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -79,7 +79,7 @@ export const register=()=>{
});

monaco.languages.setLanguageConfiguration('spx', LanguageConfig)

// Match token and highlight
monaco.languages.setMonarchTokensProvider('spx', MonarchTokensProviderConfig);
// Code hint
Expand Down
91 changes: 64 additions & 27 deletions spx-gui/src/components/sounds/SoundsEdit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,36 +10,42 @@
<n-input
size="small"
round
placeholder="Meow Sound"
:placeholder="props.asset?.name || ''"
class="sound-edit-content-top-input-sound-name"
/>
<!-- Speed Change -->
<div @click="togglePlaybackSpeed()" class="speed-change-container">
<div class="speed-change-container" @click="togglePlaybackSpeed()">
<div class="speed-change-container-text">{{ currentSpeed }}x</div>
</div>
<!-- Undo && ReUndo -->
<div class="sound-icon-container" :class="{ 'disabled': isOperateDisabled.backout }">
<button :disabled="isOperateDisabled.backout" @click="handleOperate('backout')">
<img class="sound-icon-with-text" v-if="!isOperateDisabled.backout" src="@/assets/icon/sound/undo.svg"/>
<img class="sound-icon-with-text" v-else src="@/assets/icon/sound/undo-unable.svg"/>
<img v-if="!isOperateDisabled.backout" class="sound-icon-with-text" src="@/assets/icon/sound/undo.svg"/>
<img v-else class="sound-icon-with-text" src="@/assets/icon/sound/undo-unable.svg"/>
</button>
<div class="sound-icon-text">{{ $t('sounds.undo') }}</div>
</div>
<div class="sound-icon-container" :class="{ 'disabled': isOperateDisabled.renewal }">
<button :disabled="isOperateDisabled.renewal" @click="handleOperate('renewal')">
<img class="sound-icon-with-text" v-if="!isOperateDisabled.renewal" src="@/assets/icon/sound/reUndo.svg"/>
<img class="sound-icon-with-text" v-else src="@/assets/icon/sound/reUndo-unable.svg"/>
<img v-if="!isOperateDisabled.renewal" class="sound-icon-with-text" src="@/assets/icon/sound/reUndo.svg"/>
<img v-else class="sound-icon-with-text" src="@/assets/icon/sound/reUndo-unable.svg"/>
</button>
<div class="sound-icon-text">{{ $t('sounds.reUndo') }}</div>
</div>
<div class="vertical-dashed-line-short"></div>
<!-- Save -->
<!-- Download And Save -->
<div class="sound-icon-container">
<button @click="downloadSound()">
<img class="sound-icon-with-text" src="@/assets/icon/sound/download.svg"/>
</button>
<div class="sound-icon-text">{{ $t('sounds.download') }}</div>
</div>
<div class="sound-icon-container">
<button @click="saveSound()">
<img class="sound-icon-with-text" src="@/assets/icon/sound/save.svg"/>
</button>
<div class="sound-icon-text">{{ $t('sounds.save') }}</div>
</div>
</div>
</div>
<!-- WaveSurfer Part -->
Expand All @@ -52,8 +58,8 @@
<!-- play -->
<div>
<button @click="togglePlayPause()">
<img class="sound-icon" v-if="!isPlaying" src="@/assets/icon/sound/play.svg"/>
<img class="sound-icon" v-else src="@/assets/icon/sound/pause.svg" />
<img v-if="!isPlaying" class="sound-icon" src="@/assets/icon/sound/play.svg"/>
<img v-else class="sound-icon" src="@/assets/icon/sound/pause.svg" />
</button>
</div>
<div class="vertical-dashed-line-long"></div>
Expand Down Expand Up @@ -100,36 +106,36 @@
<!-- Edit -->
<div class="sound-icon-container" :class="{ 'disabled': isOperateDisabled.remove }">
<button :disabled="isOperateDisabled.remove" @click="handleOperate('remove')">
<img class="sound-icon-with-text" v-if="!isOperateDisabled.remove" src="@/assets/icon/sound/delete.svg"/>
<img class="sound-icon-with-text" v-else src="@/assets/icon/sound/delete-unable.svg"/>
<img v-if="!isOperateDisabled.remove" class="sound-icon-with-text" src="@/assets/icon/sound/delete.svg"/>
<img v-else class="sound-icon-with-text" src="@/assets/icon/sound/delete-unable.svg"/>
</button>
<div class="sound-icon-text">{{ $t('sounds.delete') }}</div>
</div>
<div class="sound-icon-container" :class="{ 'disabled': isOperateDisabled.cut }">
<button :disabled="isOperateDisabled.cut" @click="handleOperate('cut')">
<img class="sound-icon-with-text" v-if="!isOperateDisabled.cut" src="@/assets/icon/sound/cut.svg"/>
<img class="sound-icon-with-text" v-else src="@/assets/icon/sound/cut-unable.svg"/>
<img v-if="!isOperateDisabled.cut" class="sound-icon-with-text" src="@/assets/icon/sound/cut.svg"/>
<img v-else class="sound-icon-with-text" src="@/assets/icon/sound/cut-unable.svg"/>
</button>
<div class="sound-icon-text">{{ $t('sounds.cut') }}</div>
</div>
<div class="sound-icon-container" :class="{ 'disabled': isOperateDisabled.copy }">
<button :disabled="isOperateDisabled.copy" @click="handleOperate('copy')">
<img class="sound-icon-with-text" v-if="!isOperateDisabled.copy" src="@/assets/icon/sound/copy.svg"/>
<img class="sound-icon-with-text" v-else src="@/assets/icon/sound/copy-unable.svg"/>
<img v-if="!isOperateDisabled.copy" class="sound-icon-with-text" src="@/assets/icon/sound/copy.svg"/>
<img v-else class="sound-icon-with-text" src="@/assets/icon/sound/copy-unable.svg"/>
</button>
<div class="sound-icon-text">{{ $t('sounds.copy') }}</div>
</div>
<div class="sound-icon-container" :class="{ 'disabled': isOperateDisabled.paste }">
<button :disabled="isOperateDisabled.paste" @click="handleOperate('paste')">
<img class="sound-icon-with-text" v-if="!isOperateDisabled.paste" src="@/assets/icon/sound/paste.svg"/>
<img class="sound-icon-with-text" v-else src="@/assets/icon/sound/paste-unable.svg"/>
<img v-if="!isOperateDisabled.paste" class="sound-icon-with-text" src="@/assets/icon/sound/paste.svg"/>
<img v-else class="sound-icon-with-text" src="@/assets/icon/sound/paste-unable.svg"/>
</button>
<div class="sound-icon-text">{{ $t('sounds.paste') }}</div>
</div>
<div class="sound-icon-container" :class="{ 'disabled': isOperateDisabled.insert }">
<button :disabled="isOperateDisabled.insert" @click="handleOperate('insert')">
<img class="sound-icon-with-text" v-if="!isOperateDisabled.insert" src="@/assets/icon/sound/insert.svg"/>
<img class="sound-icon-with-text" v-else src="@/assets/icon/sound/insert-unable.svg"/>
<img v-if="!isOperateDisabled.insert" class="sound-icon-with-text" src="@/assets/icon/sound/insert.svg"/>
<img v-else class="sound-icon-with-text" src="@/assets/icon/sound/insert-unable.svg"/>
</button>
<div class="sound-icon-text">{{ $t('sounds.insert') }}</div>
</div>
Expand All @@ -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);
Expand Down Expand Up @@ -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!;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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] {
Expand Down
5 changes: 2 additions & 3 deletions spx-gui/src/components/sounds/SoundsEditCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
{{ props.asset.name }}
</div>
<div class="sounds-card-subtitle">
0.85s
</div>
</div>
</div>
Expand All @@ -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);
Expand Down
Loading

0 comments on commit d12c793

Please sign in to comment.