Skip to content

Commit

Permalink
feat: Upload sound from local, edit between multiple audios (#95)
Browse files Browse the repository at this point in the history
* Update sounds edit and doc

* feat:multiple audio editing

* feat:multiple audio editing

* feat:type format && microphone record

* feat:fix type error

* feat:fix type error 2

* fix: address PR comments

* fix: address PR comments

* fix: audioDataService
  • Loading branch information
AKother authored Feb 21, 2024
1 parent 3a9fb0e commit 6cb8df3
Show file tree
Hide file tree
Showing 15 changed files with 675 additions and 215 deletions.
16 changes: 8 additions & 8 deletions spx-gui/src/class/sound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,39 +13,39 @@ import type { SoundConfig } from '@/interface/file';

/**
* @class Sound
*
*
* @author tgb
* @createDate 2024-01-11
*
*
* @example
* // create a sound just with name
* const snd1 = new Sound("1")
* // create a sound with all params
* const snd2 = new Sound("2", [file1, file2], { rate: 1 })
* // create a sound from raw data
* const snd3 = Sound.fromRawData({ name: "3", _files: [file1, file2], config: { rate: 2 } })
*
*
* // change any params
* snd2.name = "3"
* snd2.config.rate = 2
*
*
* // provide addFile and removeFile method
* const file = fileDom.target.files[0] // typeof file ==> File
* snd1.addFile(file)
* snd1.removeFile(file)
*
*
* // check if an obj is an instance of a sound
* Sound.isInstance(snd1) // true
* Sound.isInstance([snd1, snd2]) // true
* Sound.isInstance("hello") // false
*
*
* // conputed path (depend on the name of the sound)
* snd1.path // assets/sounds/1
* snd2.path // assets/sounds/3
*
*
* // computed dir
* snd2.dir // { "assets/sounds/3/index.json": { rate: 2 }, "assets/sounds/3/[file1.name]": file1, "assets/sounds/3/[file2.name]": file2 }
*
*
* // config
* snd1.config = snd1.genDefualtConfig()
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
<div class="sound-icon-text">{{ $t('sounds.download') }}</div>
</div>
<div class="sound-icon-container">
<button @click="saveSound()">
<button @click="updateSound()">
<img class="sound-icon-with-text" src="@/assets/icon/sound/save.svg"/>
</button>
<div class="sound-icon-text">{{ $t('sounds.save') }}</div>
Expand Down Expand Up @@ -144,26 +144,31 @@
<script setup lang="ts">
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, computed, watch } from 'vue'
import TimelinePlugin from 'wavesurfer.js/src/plugin/timeline';
import RegionsPlugin from 'wavesurfer.js/src/plugin/regions';
import CursorPlugin from 'wavesurfer.js/src/plugin/cursor';
import { ref, onMounted, type Ref, watch } from 'vue'
import { nextTick } from "vue";
import { WavesurferEdit } from "@/util/wavesurfer-edit";
import { type SimpleWavesurferBackend, 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'
import { Sound } from '@/class/sound'
import { audioDataService } from '@/util/wavesurfer-edit-data'
const props = defineProps({
asset: {
type: Object as () => Asset,
required: true,
type: Sound,
required: false,
}
})
const emits = defineEmits(['update-sound-file'])
interface WaveSurferInstance {
destroy(): void;
}
const waveSurfer = ref<WaveSurferInstance | null>(null);
const message: MessageApi = useMessage();
let wavesurfer: WaveSurfer = ref(null);
let wavesurfer: WaveSurfer;
let isPlaying: Ref<boolean> = ref(false);
let currentSpeedIndex: number = 1;
const playbackSpeeds: number[] = [0.5, 1.0, 1.25, 1.5, 2.0, 4.0];
Expand All @@ -183,11 +188,26 @@ const isOperateDisabled: Ref<{ [key: string]: boolean }> = ref({
});
onMounted(() => {
initWaveSurfer();
initSoundEdit();
});
const initSoundEdit = () => {
// Make sure that wavesurfer does not stack after each update
if (waveSurfer.value) {
waveSurfer.value.destroy();
waveSurfer.value = null;
}
const waveformContainer = document.querySelector('#waveform');
if (waveformContainer) {
waveformContainer.innerHTML = '';
}
initWaveSurfer();
isRegionOptionDisabled();
}
/* init WaveSurfer */
const initWaveSurfer = () => {
// nextTick : Make sure waveform dom is loaded
nextTick(() => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
Expand Down Expand Up @@ -218,7 +238,7 @@ const initWaveSurfer = () => {
primaryFontColor: '#ffffff',
secondaryFontColor: '#f79e9e',
labelInterval: 10,
timeInterval: 10,
timeInterval: () => 10,
formatTimeCallback: function(seconds: number) {
return new Date(seconds * 1000).toISOString().substr(14, 5);
}
Expand All @@ -241,7 +261,7 @@ const initWaveSurfer = () => {
}),
CursorPlugin.create({
showTime: true,
opacity: 1,
opacity: "1",
customShowTimeStyle: {
borderRadius: '5px',
background: '#fff',
Expand All @@ -254,41 +274,41 @@ const initWaveSurfer = () => {
})
],
});
// 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);
// load sound
if (props.asset) {
console.log(props.asset?.files[0])
wavesurfer.loadBlob(props.asset?.files[0]);
} else {
return
}
wavesurfer.on('ready', () => {
buffer = wavesurfer.backend.buffer!;
soundEditor = new WavesurferEdit({
buffer,
ac: wavesurfer.backend.ac,
maxCount: 20,
});
const backend = wavesurfer.backend as SimpleWavesurferBackend;
if (backend.buffer && backend.ac) {
soundEditor = new WavesurferEdit({
buffer: backend.buffer,
ac: backend.ac,
maxCount: 20,
});
}
});
wavesurfer.on('play', () => {
isPlaying.value = true;
});
wavesurfer.on('pause', () => {
isPlaying.value = false;
});
wavesurfer.on('region-click', (region: any, e: Event) => {
region.play();
e.stopPropagation();
currentRegion = region;
});
wavesurfer.on('region-updated', (region: any, e: Event) => {
wavesurfer.on('region-updated', (region: any) => {
currentRegion = region;
removeRegion(region);
});
wavesurfer.on('region-update-end', (region: any, e: Event) => {
wavesurfer.on('region-update-end', (region: any) => {
currentRegion = region;
isRegionOptionDisabled();
});
Expand All @@ -298,13 +318,11 @@ const initWaveSurfer = () => {
function handleOperate(e: string) : void {
let val = null;
let timeArr = ['paste', 'insert'];
if (timeArr.includes(e)) val = wavesurfer.getCurrentTime();
if (timeArr.includes(e)) val = wavesurfer!.getCurrentTime();
else val = currentRegion;
let res = soundEditor[e](val);
let res = (soundEditor as any)[e](val);
if (!res) return;
showMessage(e);
isOperateDisabled.value.paste = !res.copyData;
isOperateDisabled.value.insert = !res.copyData;
isOperateDisabled.value.backout = res.curIndex <= 0;
isOperateDisabled.value.renewal = res.curIndex >= res.maxIndex - 1;
if (e !== 'copy') {
Expand All @@ -329,7 +347,7 @@ function showMessage(e: string):void {
/* Purpose: only Retain one region */
function removeRegion(region: any = {}): void {
if (!Object.keys(region).length) currentRegion = null;
let regions = wavesurfer.regions.list;
let regions = wavesurfer!.regions.list;
for (const key in regions) {
if (region.id === regions[key].id) continue;
regions[key].remove();
Expand All @@ -342,12 +360,14 @@ function isRegionOptionDisabled(): void {
isOperateDisabled.value.copy = !currentRegion;
isOperateDisabled.value.cut = !currentRegion;
isOperateDisabled.value.remove = !currentRegion;
isOperateDisabled.value.paste = !audioDataService.getCopyData().data;
isOperateDisabled.value.insert = !audioDataService.getCopyData().data;
}
/* Render wavesurfer */
function renderWavesurfer(): void {
wavesurfer.backend.load(buffer);
wavesurfer.drawBuffer();
(wavesurfer!.backend as any).load(buffer);
wavesurfer!.drawBuffer();
}
/* Toggle play and pause */
Expand All @@ -369,19 +389,19 @@ function togglePlaybackSpeed(): void {
/* Increase volume 10% */
function increaseVolume(): void {
var currentVolume = wavesurfer.getVolume();
const currentVolume = wavesurfer.getVolume()
if (currentVolume < 1) {
var newVolume = currentVolume + 0.1;
let newVolume = currentVolume + 0.1
if (newVolume > 1) newVolume = 1;
wavesurfer.setVolume(newVolume);
}
}
/* Decrease volume 10% */
function decreaseVolume(): void {
var currentVolume = wavesurfer.getVolume();
const currentVolume = wavesurfer.getVolume()
if (currentVolume > 0) {
var newVolume = currentVolume - 0.1;
let newVolume = currentVolume - 0.1
if (newVolume < 0) newVolume = 0;
wavesurfer.setVolume(newVolume);
}
Expand Down Expand Up @@ -412,12 +432,14 @@ 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")
/* Update sound */
async function updateSound(): Promise<void> {
const backend = wavesurfer.backend as SimpleWavesurferBackend;
if (backend && backend.buffer) {
const wavBlob = audioBufferToWavBlob(backend.buffer);
const wavFile = wavBlobToFile(wavBlob, props.asset?.name + ".wav");
emits('update-sound-file', wavFile);
}
}
/* Convert WAV Blob to File */
Expand All @@ -427,10 +449,12 @@ function wavBlobToFile(wavBlob: Blob, fileName: string): File {
/* Download sound file */
function downloadSound(): void {
downloadAudioBuffer(wavesurfer.backend.buffer, props.asset.name + ".wav");
const backend = wavesurfer.backend as SimpleWavesurferBackend;
if (backend && backend.buffer) {
downloadAudioBuffer(backend.buffer, props.asset!.name + ".wav");
}
}
/* Transfer AudioBuffer to WAV Blob */
function audioBufferToWavBlob(audioBuffer: AudioBuffer): Blob {
const numOfChan = audioBuffer.numberOfChannels;
Expand Down Expand Up @@ -497,6 +521,9 @@ function downloadAudioBuffer(audioBuffer: AudioBuffer, filename: string): void {
document.body.removeChild(a);
}
watch(() => props.asset, () => {
initSoundEdit();
});
</script>
Expand Down Expand Up @@ -554,6 +581,8 @@ function downloadAudioBuffer(audioBuffer: AudioBuffer, filename: string): void {
}
.waveform-content {
position: relative;
z-index: 0;
margin-top: 30px;
margin-bottom: 30px;
}
Expand Down
Loading

0 comments on commit 6cb8df3

Please sign in to comment.