Переглянути джерело

节拍器 音频改为前端合成的方式

黄琪勇 3 місяців тому
батько
коміт
ab9bc39edb

BIN
src/assets/tick.wav


BIN
src/assets/tock.wav


+ 10 - 7
src/page-instrument/header-top/index.tsx

@@ -34,6 +34,7 @@ import { isMusicList, musicListShow } from "../component/the-music-list";
 import { EvaluatingDriver, FollowDriver, PractiseDriver } from "../custom-plugins/guide-driver";
 import { fingerRef } from "/src/page-instrument/view-detail/index"
 import WorkHomePop from "./workHomePop";
+import { handleLoadBeatMusic } from "/src/view/audio-list"
 
 const ModeView = defineAsyncComponent(() =>
   import('./modeView')
@@ -125,7 +126,7 @@ let isClickMode = false;
  * @param oldPlaySource  没改变之前的播放类型
  * @param isforceReset   是否强制刷新播放状态 模式times时值改变时候也刷新
  */
-export function handlerModeChange(oldPlayType: "play" | "sing", oldPlaySource: IPlayState, isforceReset?: boolean) {
+export async function handlerModeChange(oldPlayType: "play" | "sing", oldPlaySource: IPlayState, isforceReset?: boolean) {
   const isModeChange = modeChangeHandleTimes(oldPlayType, oldPlaySource);
   // 没有切换的时候 不处理下面的
   if (isModeChange) {
@@ -140,6 +141,8 @@ export function handlerModeChange(oldPlayType: "play" | "sing", oldPlaySource: I
     // 隐藏重播按钮
     resetBtn && (resetBtn.value.display = false);
   }
+  // 节拍器音频加载
+  await handleLoadBeatMusic()
   // 当模式改变的时候  放在这里是因为需要等谱面加载完成之后再提示(点击按钮模式切换才提示)
   if (isClickMode) {
     showToast({
@@ -377,14 +380,14 @@ export default defineComponent({
       // 评测开始 禁用
       if (state.modeType === "evaluating") return { display: false, disabled: true };
       if (!state.isAppPlay) {
+        // 播放过程中不能切换
+        if (state.playState === "play") {
+          return { display: true, disabled: true };
+        }
         if (state.playType === "play") {
           // 原声, 伴奏 少一个,就不能切换
           if (state.music && state.accompany) return { display: true, disabled: false };
         } else {
-          // 播放过程中不能切换
-          if (state.playState === "play") {
-            return { display: true, disabled: true };
-          }
           // 范唱
           let index = 0;
           state.fanSong && index++;
@@ -859,7 +862,7 @@ export default defineComponent({
               id={state.platform === IPlatform.PC ? "teacherTop-1" : "studnetT-1"}
               style={{ display: originBtn.value.display ? "" : "none" }}
               class={["driver-3", styles.btn, originBtn.value.disabled && styles.disabled, state.playType === "play" ? styles.playSource : styles.songSource]}
-              onClick={() => {
+              onClick={async () => {
                 const oldPlayType = state.playType;
                 const oldPlaySource = state.playSource;
                 if (state.playType === "play") {
@@ -873,7 +876,7 @@ export default defineComponent({
                     state.playSource = state.fanSong ? "music" : "background";
                   }
                 }
-                handlerModeChange(oldPlayType, oldPlaySource);
+                await handlerModeChange(oldPlayType, oldPlaySource);
                 showToast({
                   message: state.playType === "play" ? (state.playSource === "music" ? "已切换为原声" : "已切换为伴奏") : state.playSource === "music" ? "已切换为范唱" : state.playSource === "background" ? "已切换为伴唱" : "已切换为唱名",
                   position: "top",

+ 4 - 1
src/page-instrument/header-top/settting/index.tsx

@@ -16,6 +16,7 @@ import useDrag from "/src/view/plugins/useDrag/index";
 import Dragbom from "/src/view/plugins/useDrag/dragbom";
 import { storeData } from "/src/store";
 import { getGuidance, setGuidance } from "../../custom-plugins/guide-page/api";
+import { handleLoadBeatMusic } from "/src/view/audio-list"
 
 export default defineComponent({
 	name: "settting",
@@ -100,7 +101,7 @@ export default defineComponent({
                                 </div>   
                         }
                         {
-                            state.modeType === 'practise' && state.playType === "sing" && state.mingSong && state.mingSongGirl &&
+                            state.modeType === 'practise' && state.playSource === "mingSong" && state.mingSong && state.mingSongGirl &&
                             <div class={styles.cellBox}>
                                 <div class={styles.tit}>唱名类型</div>
                                 <div class={styles.radioBox}>
@@ -111,6 +112,8 @@ export default defineComponent({
                                                     return
                                                 }
                                                 audioData.mingSongType = item.value as 0|1
+                                                // 加载节拍器音频
+                                                handleLoadBeatMusic()
                                                 changeMingSongType()
                                             } }>{item.name}</div>
                                         })

+ 6 - 0
src/page-instrument/header-top/speed/index.tsx

@@ -9,6 +9,7 @@ import { getQuery } from "/src/utils/queryString";
 import { api_createMusicPlayer, api_updateMusicPlayer, api_checkSocketStatus } from "/src/helpers/communication";
 import { storeData } from "/src/store";
 import { data as workData } from "/src/page-instrument/custom-plugins/work-index";
+import { handleLoadBeatMusic } from "/src/view/audio-list"
 
 export default defineComponent({
 	name: "speed",
@@ -62,6 +63,11 @@ export default defineComponent({
 		// 切换节拍器
 		const toggleSwitch = async (res: any) => {
 			switchLoading.value = true;
+			metronomeDisable.value = res;
+			await handleLoadBeatMusic()
+			switchLoading.value = false;
+			return
+			switchLoading.value = true;
 			try {
 			  if (storeData.isApp && state.enableEvaluation) {
 				// 加载弹窗的开始时间

+ 15 - 18
src/utils/crunker.ts

@@ -3,7 +3,7 @@ interface CrunkerConstructorOptions {
    concurrentNetworkRequests: number
 }
 
-type CrunkerInputTypes = string | File | Blob
+type CrunkerInputTypes = string | File | Blob | undefined
 
 export default class Crunker {
    private readonly _sampleRate: number
@@ -16,15 +16,15 @@ export default class Crunker {
       this._sampleRate = sampleRate
       this._concurrentNetworkRequests = concurrentNetworkRequests
    }
-   private _createContext(sampleRate = 44_100): AudioContext {
+   private _createContext(sampleRate = 22050): AudioContext {
       window.AudioContext = window.AudioContext || (window as any).webkitAudioContext || (window as any).mozAudioContext
       return new AudioContext({ sampleRate })
    }
    /**
     *转换url等类型为buffer
     */
-   async fetchAudio(...filepaths: CrunkerInputTypes[]): Promise<AudioBuffer[]> {
-      const buffers: AudioBuffer[] = []
+   async fetchAudio(...filepaths: CrunkerInputTypes[]): Promise<(AudioBuffer | undefined)[]> {
+      const buffers: (AudioBuffer | undefined)[] = []
       const groups = Math.ceil(filepaths.length / this._concurrentNetworkRequests)
       for (let i = 0; i < groups; i++) {
          const group = filepaths.slice(i * this._concurrentNetworkRequests, (i + 1) * this._concurrentNetworkRequests)
@@ -32,9 +32,12 @@ export default class Crunker {
       }
       return buffers
    }
-   private async _fetchAudio(...filepaths: CrunkerInputTypes[]): Promise<AudioBuffer[]> {
+   private async _fetchAudio(...filepaths: CrunkerInputTypes[]): Promise<(AudioBuffer | undefined)[]> {
       return await Promise.all(
          filepaths.map(async filepath => {
+            if (!filepath) {
+               return Promise.resolve(undefined)
+            }
             let buffer: ArrayBuffer
             if (filepath instanceof File || filepath instanceof Blob) {
                buffer = await filepath.arrayBuffer()
@@ -74,24 +77,18 @@ export default class Crunker {
       }
       const output = this._context.createBuffer(this._maxNumberOfChannels(buffers), this._sampleRate * this._maxDuration(buffers), this._sampleRate)
       buffers.forEach((buffer, index) => {
-         for (let channelNumber = 0; channelNumber < buffer.numberOfChannels; channelNumber++) {
+         const offsetNum = Math.round(times[index] * this._sampleRate) //时间偏差
+         for (let channelNumber = 0; channelNumber < output.numberOfChannels; channelNumber++) {
             const outputData = output.getChannelData(channelNumber)
-            const bufferData = buffer.getChannelData(channelNumber)
-            const offsetNum = Math.round(times[index] * this._sampleRate) //时间偏差
-            for (let i = buffer.getChannelData(channelNumber).length - 1; i >= 0; i--) {
-               outputData[i + offsetNum] += bufferData[i]
+            // buffers 有可能是单声道,当单声道的时候 取第一个声道的值
+            const bufferData = buffer.getChannelData(buffer.numberOfChannels < 2 ? 0 : channelNumber)
+            for (let i = bufferData.length - 1; i >= 0; i--) {
                // 当合并大于1或者小于-1的时候可能会爆音  所以这里取最大值和最小值
-               if (outputData[i + offsetNum] > 1) {
-                  outputData[i + offsetNum] = 1
-               }
-               if (outputData[i + offsetNum] < -1) {
-                  outputData[i + offsetNum] = -1
-               }
+               const combinedValue = outputData[i + offsetNum] + bufferData[i]
+               outputData[i + offsetNum] = Math.max(-1, Math.min(1, combinedValue))
             }
-            output.getChannelData(channelNumber).set(outputData)
          }
       })
-
       return output
    }
    /**

+ 118 - 2
src/view/audio-list/index.tsx

@@ -14,6 +14,10 @@ import { evaluatingData } from "/src/view/evaluating";
 import { cloudToggleState } from "/src/helpers/midiPlay"
 import { storeData } from "/src/store";
 import { handleStartTick } from "../tick";
+import Crunker from "/src/utils/crunker"
+import tickMp3 from "/src/assets/tick.wav"
+import tockMp3 from "/src/assets/tock.wav"
+import { metronomeData } from "/src/helpers/metronome";
 
 export const audioData = reactive({
 	songEle: null as HTMLAudioElement | null, // 原生
@@ -200,6 +204,118 @@ export const changeMingSongType = () =>{
 		audioData.songCollection.beatMingSongEle = mingSongType === 1 ? beatMingSongEle : beatMingSongGirlEle
 	}
 }
+
+// 处理加载节拍器音频
+let CrunkerInstance: Crunker
+export const handleLoadBeatMusic = async () => {
+	if(metronomeData.disable) {
+		return
+	}
+	const playType = state.playType
+	const playSource = state.playSource
+	const mingSongType = audioData.mingSongType
+	// 当前模式下 如果已经有合成音频了,就不走合成逻辑了
+	let isBeatMusic = false
+	let currentMusic  //当前的音频
+	const beatPlayObj = {
+		"play_music":"beatSongEle",
+		"play_background":"beatBackgroundEle",
+		"sing_music":"beatFanSongEle",
+		"sing_background":"beatBanSongEle"
+	}
+	const playObj = {
+		"play_music":"music",
+		"play_background":"accompany",
+		"sing_music":"fanSong",
+		"sing_background":"banSong",
+	}
+	if(playSource === "mingSong") {
+		// 当男声女声都有的时候 才区分
+		if(state.mingSong && state.mingSongGirl){
+			isBeatMusic = mingSongType === 1 ? !!audioData.mingSongTypeCollection.beatMingSongEle : !!audioData.mingSongTypeCollection.beatMingSongGirlEle
+			currentMusic = mingSongType === 1 ? state.mingSong : state.mingSongGirl
+		}else{
+			isBeatMusic = !!audioData.mingSongTypeCollection.beatMingSongEle
+			currentMusic = state.mingSong
+		}
+	}else{
+		// @ts-ignore
+		isBeatMusic = !!audioData.songCollection[beatPlayObj[`${playType}_${playSource}`]]
+		// @ts-ignore
+		currentMusic = state[playObj[`${playType}_${playSource}`]]
+	}
+	if(isBeatMusic || !currentMusic){
+		return
+	}
+	state.loadingText = "音频资源加载中,请稍后…"
+	state.isLoading = true
+	/* 音频合成 */
+	if(!CrunkerInstance){
+		CrunkerInstance = new Crunker()
+	}
+	console.time("音频加载时间")
+	const [audioBuffer, tickBuff, tockBuff] = await CrunkerInstance.fetchAudio(`${currentMusic}?v=${Date.now()}`, tickMp3, tockMp3)
+	console.timeEnd("音频加载时间")
+	// 计算音频空白时间
+	const silenceDuration = audioBuffer&&!state.isEvxml ? CrunkerInstance.calculateSilenceDuration(audioBuffer) : 0
+	console.log(`音频空白时间:${silenceDuration}`)
+	const beats:AudioBuffer[] = []
+    const beatsTime:number[] = []
+	metronomeData.metroMeasure.map(measures=>{
+		measures.map((item:any)=>{
+			beats.push(item.index===0?tickBuff!:tockBuff!)
+			beatsTime.push(item.time+silenceDuration) //不是妙极客的曲子才加上空白
+		})
+	})
+	console.time("音频合并时间")
+	const musicBuffMeg = audioBuffer && CrunkerInstance.mergeAudioBuffers([audioBuffer!,...beats],[0,...beatsTime])
+	console.timeEnd("音频合并时间")
+	console.time("音频audioDom生成时间")
+	const musicAudio = musicBuffMeg && CrunkerInstance.exportAudioElement(musicBuffMeg) as any
+	console.timeEnd("音频audioDom生成时间")
+	const playEleObj = {
+		"play_music":"beatSongEle",
+		"play_background":"beatBackgroundEle",
+		"sing_music":"beatFanSongEle",
+		"sing_background":"beatBanSongEle"
+	}
+	// 给音频赋值
+	if(playSource === "mingSong"){
+		// 当男声女声都有的时候 才区分
+		if(state.mingSong && state.mingSongGirl){
+			if(mingSongType === 1) {
+				audioData.mingSongTypeCollection.beatMingSongEle = musicAudio
+			}else {
+				audioData.mingSongTypeCollection.beatMingSongGirlEle = musicAudio
+			}
+		}else{
+			audioData.songCollection.beatMingSongEle = musicAudio
+			audioData.mingSongTypeCollection.beatMingSongEle = musicAudio
+		}
+		if(musicAudio){
+			musicAudio.addEventListener("play", onPlay);
+			musicAudio.addEventListener("ended", onEnded);
+		}
+		changeMingSongType()
+	}else{
+		if(playType === "play" && !audioData.songCollection.beatSongEle && !audioData.songCollection.beatBackgroundEle){
+			if(musicAudio){
+				musicAudio.addEventListener("play", onPlay);
+				musicAudio.addEventListener("ended", onEnded);
+			}
+		}		
+		if(playType === "sing" && !audioData.songCollection.beatFanSongEle && !audioData.songCollection.beatBanSongEle){
+			if(musicAudio){
+				musicAudio.addEventListener("play", onPlay);
+				musicAudio.addEventListener("ended", onEnded);
+			}
+		}
+		// @ts-ignore
+		audioData.songCollection[playEleObj[`${playType}_${playSource}`]] = musicAudio
+	}
+	state.isLoading = false
+}
+
 export default defineComponent({
 	name: "audio-list",
 	setup() {
@@ -383,7 +499,7 @@ export default defineComponent({
 					mingSongGirl.addEventListener("play", onPlay);
 					mingSongGirl.addEventListener("ended", onEnded);
 				}
-				// 处理带节拍器的音源
+				/* // 处理带节拍器的音源
 				const [beatMusic, beatAccompany, beatFanSong, beatBanSong, beatMingSong, beatMingSongGirl] = await loadBeatAudio()
 				Object.assign(audioData.songCollection, {
 					beatSongEle:beatMusic,
@@ -421,7 +537,7 @@ export default defineComponent({
 				if(beatMingSongGirl){
 					beatMingSongGirl.addEventListener("play", onPlay);
 					beatMingSongGirl.addEventListener("ended", onEnded);
-				}
+				}*/
 				// 给男声女声赋值
 				const userGender = storeData.user.gender
 				// 当不为null 和undefined的时候 取userGender的值