vue中使用Canvas繪制波形圖和頻譜圖(支持.pcm)

實現方式一:
vue中使用wavesurfer.js繪制波形圖和頻譜圖

安裝colorMap:

npm install --save colormap

1、單個頻譜圖

效果:

在這里插入圖片描述

源碼:

<template><div class="spectrogram-container"><canvas ref="canvas" width="1140" height="150" style="background: #000"></canvas><div class="audio-controls"><audio ref="audioPlayer" controls @play="startPlay" controlsList="nodownload noplaybackrate"></audio></div></div>
</template><script>
import axios from 'axios'
import Vue from 'vue'
import { ACCESS_TOKEN } from '@/store/mutation-types'
import colormap from 'colormap'// 簡易 FFT 實現(Hamming 窗 + 蝶形運算)
// 簡易 FFT 實現(Hamming 窗 + 蝶形運算)
function createFFT(size) {const table = new Float32Array(size)for (let i = 0; i < size; i++) {table[i] = 0.54 - 0.46 * Math.cos((2 * Math.PI * i) / (size - 1)) // Hamming Window}return (input) => {const n = input.lengthconst logN = Math.log2(n)if (n !== 1 << logN) throw new Error('FFT length must be power of 2')const re = new Float32Array(n)const im = new Float32Array(n)for (let i = 0; i < n; i++) {re[i] = input[i] * table[i]im[i] = 0}// 位逆序置換for (let i = 1, j = 0; i < n - 1; i++) {let k = n >> 1while (j >= k) {j -= kk >>= 1}j += kif (i < j) {;[re[i], re[j]] = [re[j], re[i]];[im[i], im[j]] = [im[j], im[i]]}}// 蝶形計算for (let size = 2; size <= n; size <<= 1) {const half = size >> 1const angle = (-2 * Math.PI) / sizeconst w = [1, 0]const wStep = [Math.cos(angle), Math.sin(angle)]for (let i = 0; i < half; i++) {for (let j = i; j < n; j += size) {const l = j + halfconst tRe = w[0] * re[l] - w[1] * im[l]const tIm = w[0] * im[l] + w[1] * re[l]re[l] = re[j] - tReim[l] = im[j] - tImre[j] += tReim[j] += tIm}const tmp = w[0] * wStep[0] - w[1] * wStep[1]w[1] = w[0] * wStep[1] + w[1] * wStep[0]w[0] = tmp}}// ? 修復:使用 n/2,而不是外部不存在的 halfconst spectrum = new Float32Array(n / 2)for (let i = 0; i < n / 2; i++) {const mag = Math.sqrt(re[i] ** 2 + im[i] ** 2)spectrum[i] = Math.log10(mag + 1) * 100}return spectrum}
}export default {name: 'AudioWaveform',props: ['audioUrl'],data() {return {fileData: new Int8Array(0),isPlaying: false,sampleRate: 8000, // ? 改為你的實際采樣率interval: 100,index: 0,mWidth: 0,mHeight: 0,audio: null,animationId: null,// 頻譜相關spectrogram: [], // 頻譜數據:每一列是一個時間幀的頻譜colorMap: [], // colormap 生成的顏色fftSize: 1024, // FFT 大小(必須是 2 的冪)xSize: 300, // 頻譜圖最大列數(由 canvas 寬度決定)barWidth: 1, // 每一列的寬度binHeight: 1, // 每個頻率 bin 的高度}},watch: {audioUrl(newVal) {this.handleAudioUrl(newVal)},},mounted() {this.mWidth = this.$refs.canvas.widththis.mHeight = this.$refs.canvas.height// this.xSize = Math.ceil(this.mWidth / 2) // 最大列數this.xSize = Math.max(130, this.mWidth / 2) // 確保至少能顯示13秒的數據this.barWidth = Math.max(1, Math.floor(this.mWidth / this.xSize))this.binHeight = this.mHeight / (this.fftSize / 2)// 初始化 colormapthis.colorMap = colormap({colormap: 'magma', // 替換為上述任意名稱nshades: 256, // 顏色分段數format: 'rgbaString', // 輸出格式alpha: 1, // 透明度})this.audio = this.$refs.audioPlayerthis.handleAudioUrl(this.audioUrl)},methods: {// 計算使3.2秒音頻剛好撐滿屏幕的xSizecalculateXSizeFor3_2Seconds() {const targetDuration = 3.2 // 3.2秒const totalFrames = (this.sampleRate * targetDuration) / this.fftSize// 兩種計算方式確保精度:// 方式1:基于總樣本數this.xSize = Math.floor((this.sampleRate * targetDuration) / (this.fftSize / 2))// 方式2:基于畫布寬度和期望的時間分辨率// const timePerPixel = (targetDuration * 1000) / this.mWidth; // ms/px// this.xSize = Math.floor((targetDuration * 1000) / timePerPixel);// 限制最小和最大值this.xSize = Math.max(10, Math.min(this.xSize, this.mWidth))// 重新計算每列寬度this.barWidth = this.mWidth / this.xSizeconsole.log(`3.2秒顯示優化: 采樣率=${this.sampleRate}Hz, FFT大小=${this.fftSize}, 畫布寬度=${this.mWidth}px,最終xSize=${this.xSize},每列寬度=${this.barWidth}px`)},handleAudioUrl(audioUrl) {console.log('加載音頻:', audioUrl)if (!audioUrl) return// 停止當前播放并重置狀態this.resetComponent()if (audioUrl.endsWith('.pcm')) {this.loadPcmAudio(audioUrl)} else {this.downloadAudio(audioUrl)}},resetComponent() {this.stopPlayback()this.fileData = new Float32Array(0)this.index = 0this.spectrogram = []this.clearCanvas()// 釋放之前的音頻URLif (this.audio.src) {URL.revokeObjectURL(this.audio.src)this.audio.src = ''}},loadPcmAudio(url) {fetch(url, {method: 'GET',headers: { 'X-Mintti-Web-Token': Vue.ls.get(ACCESS_TOKEN) },}).then((res) => res.arrayBuffer()).then((buffer) => this.initAudioPlayer(buffer)).catch((err) => {console.error('PCM 加載失敗:', err)this.$message.warning('音頻加載失敗')})},downloadAudio(url) {axios.get(url, { responseType: 'arraybuffer' }).then((res) => this.initAudioPlayer(res.data)).catch((err) => {console.error('下載失敗:', err)this.$message.warning('音頻下載失敗')})},initAudioPlayer(arraybuffer) {const uint8 = new Uint8Array(arraybuffer)const isWav = uint8[0] === 82 && uint8[1] === 73 && uint8[2] === 70 // 'RIFF'const dataStart = isWav ? 44 : 0// 處理16位PCM數據const pcmData = new Int16Array(arraybuffer.slice(dataStart))this.fileData = new Float32Array(pcmData.length)// 16位PCM轉Float32 (-32768~32767 -> -1~1)for (let i = 0; i < pcmData.length; i++) {this.fileData[i] = pcmData[i] / 32768.0}// 創建WAV文件用于播放const wavHeader = this.createWavHeader(pcmData.length)const wavData = new Uint8Array(wavHeader.byteLength + arraybuffer.byteLength - dataStart)wavData.set(new Uint8Array(wavHeader), 0)wavData.set(uint8.subarray(dataStart), wavHeader.byteLength)const blob = new Blob([wavData], { type: 'audio/wav' })const url = URL.createObjectURL(blob)this.audio.src = urlthis.audio.load()// 監聽事件this.audio.addEventListener('play', () => (this.isPlaying = true))this.audio.addEventListener('pause', () => (this.isPlaying = false))this.audio.addEventListener('ended', () => this.stopPlayback())this.audio.addEventListener('seeked', () => {this.index = Math.floor((this.audio.currentTime * 1000) / this.interval)this.spectrogram = []})// 加載完成后計算優化參數this.calculateXSizeFor3_2Seconds()},// 創建WAV文件頭createWavHeader(dataLength) {const buffer = new ArrayBuffer(44)const view = new DataView(buffer)// RIFF標識this.writeString(view, 0, 'RIFF')// 文件長度view.setUint32(4, 36 + dataLength * 2, true)// WAVE標識this.writeString(view, 8, 'WAVE')// fmt子塊this.writeString(view, 12, 'fmt ')// fmt長度view.setUint32(16, 16, true)// 編碼方式: 1表示PCMview.setUint16(20, 1, true)// 聲道數view.setUint16(22, 1, true)// 采樣率view.setUint32(24, this.sampleRate, true)// 字節率view.setUint32(28, this.sampleRate * 2, true)// 塊對齊view.setUint16(32, 2, true)// 位深度view.setUint16(34, 16, true)// data標識this.writeString(view, 36, 'data')// data長度view.setUint32(40, dataLength * 2, true)return buffer},writeString(view, offset, string) {for (let i = 0; i < string.length; i++) {view.setUint8(offset + i, string.charCodeAt(i))}},startPlay() {if (this.audio && this.fileData.length > 0) {// 重置狀態if (this.audio.ended || this.index * this.interval >= this.fileData.length / (this.sampleRate / 1000)) {this.stopPlayback()}// 同步起始時間this.lastUpdateTime = performance.now()this.audio.play()this.isPlaying = truethis.timer()}},timer() {if (!this.isPlaying) return// 計算基于音頻時間的理想幀數const targetFrame = Math.floor((this.audio.currentTime * 1000) / this.interval)const maxCatchUpFrames = 5 // 最大追趕幀數,避免卡頓// 適度追趕,避免一次性處理太多幀導致卡頓let framesToUpdate = Math.min(targetFrame - this.index, maxCatchUpFrames)while (framesToUpdate > 0) {this.refreshData()framesToUpdate--}// 正常情況下一幀一幀更新if (framesToUpdate === 0 && this.index < targetFrame) {this.refreshData()}this.animationId = requestAnimationFrame(() => this.timer())},refreshData() {// 計算每幀推進的樣本數,使3.2秒剛好撐滿const samplesPerFrame = Math.floor((this.sampleRate * 3.2) / this.xSize)const start = this.index * samplesPerFrameconst end = start + this.fftSizeif (start >= this.fileData.length) {this.stopPlayback()return}let segmentif (end > this.fileData.length) {segment = new Float32Array(this.fftSize)segment.set(this.fileData.slice(start))} else {segment = this.fileData.slice(start, end)}// 執行FFTconst fft = createFFT(this.fftSize)const spectrum = fft(segment)// 歸一化const maxDB = 0,minDB = -80const normalized = spectrum.map((v) => {const dbValue = 20 * Math.log10(v + 1e-6)return Math.max(0, Math.min(255, Math.floor(((dbValue - minDB) / (maxDB - minDB)) * 255)))})// 更新頻譜數據if (this.spectrogram.length >= this.xSize) {this.spectrogram.shift()}this.spectrogram.push(normalized)this.drawSpectrogram()this.index += 1},drawSpectrogram() {const ctx = this.$refs.canvas.getContext('2d')const { width, height } = ctx.canvasctx.clearRect(0, 0, width, height)const dx = width / Math.max(this.xSize, this.spectrogram.length)for (let x = 0; x < this.spectrogram.length; x++) {const spec = this.spectrogram[x]const canvasX = width - (this.spectrogram.length - x) * dxfor (let y = 0; y < spec.length; y++) {// 使用對數縮放增強低頻顯示const freqIndex = Math.floor(Math.pow(y / spec.length, 0.7) * spec.length)const colorIdx = Math.max(0, Math.min(255, spec[freqIndex]))ctx.fillStyle = this.colorMap[colorIdx]// 低頻在底部,高頻在頂部const pixelY = height - y * (height / spec.length)ctx.fillRect(canvasX, pixelY, dx, height / spec.length)}}},stopPlayback() {this.isPlaying = falsethis.audio.pause()this.audio.currentTime = 0 // 重置播放位置this.index = 0 // 重置頻譜索引this.spectrogram = [] // 清空頻譜數據this.clearAnimation()this.clearCanvas()},clearAnimation() {if (this.animationId) {cancelAnimationFrame(this.animationId)this.animationId = null}},clearCanvas() {const ctx = this.$refs.canvas.getContext('2d')ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)},},beforeDestroy() {this.stopPlayback()if (this.audio.src) {URL.revokeObjectURL(this.audio.src)}},
}
</script><style scoped>
.spectrogram-container {display: flex;flex-direction: column;align-items: center;padding: 10px;font-family: Arial, sans-serif;
}
canvas {border: 1px solid #333;border-radius: 4px;
}
.audio-controls {margin-top: 10px;
}
</style>

2、波形圖+ 頻譜圖

波形圖參考:心電波形圖EcgView

效果圖:

在這里插入圖片描述

源碼:

<template><div class="audio-visualizer"><div class="visualization-container"><div class="waveform-container"><canvas ref="waveformCanvas" width="1140" height="150"></canvas></div><div class="spectrogram-container"><canvas ref="spectrogramCanvas" width="1140" height="150" style="background: #000"></canvas></div></div><div class="audio-controls"><audio ref="audioPlayer" controls @play="startPlay" controlsList="nodownload noplaybackrate"></audio></div></div>
</template><script>
import axios from 'axios'
import Vue from 'vue'
import { ACCESS_TOKEN } from '@/store/mutation-types'
import colormap from 'colormap'export default {name: 'AudioWaveform',props: ['audioUrl'],data() {return {// 音頻數據相關fileData: new Int8Array(0),pcmData: new Float32Array(0),isPlaying: false,sampleRate: 8000,interval: 100, // 統一的時間間隔index: 0,// 波形圖相關waveformData: [],waveformCtx: null,waveformWidth: 1140,waveformHeight: 150,zoom: 0,gapX: 0.2,xSize: 0,maxMillimeter: 5 * 5,STEP_SIZE: 50,gain: 5,maxMidScopeY: 0,// 頻譜圖相關spectrogramCtx: null,spectrogramWidth: 1140,spectrogramHeight: 150,spectrogram: [],colorMap: [],fftSize: 1024,barWidth: 1,binHeight: 1,// 播放控制audio: null,animationId: null,lastTime: 0,}},watch: {audioUrl(newVal) {this.handleAudioUrl(newVal)},},mounted() {// 初始化波形圖this.waveformCtx = this.$refs.waveformCanvas.getContext('2d')this.waveformWidth = this.$refs.waveformCanvas.widththis.waveformHeight = this.$refs.waveformCanvas.heightthis.drawBg(this.waveformCtx)this.initWaveformParams()// 初始化頻譜圖this.spectrogramCtx = this.$refs.spectrogramCanvas.getContext('2d')this.spectrogramWidth = this.$refs.spectrogramCanvas.widththis.spectrogramHeight = this.$refs.spectrogramCanvas.heightthis.xSize = Math.max(130, this.spectrogramWidth / 2)this.barWidth = Math.max(1, Math.floor(this.spectrogramWidth / this.xSize))this.binHeight = this.spectrogramHeight / (this.fftSize / 2)// 初始化 colormapthis.colorMap = colormap({colormap: 'magma',nshades: 256,format: 'rgbaString',alpha: 1,})this.audio = this.$refs.audioPlayerif (this.audioUrl) {this.handleAudioUrl(this.audioUrl)}},methods: {// 初始化波形圖參數initWaveformParams() {this.zoom = this.waveformHeight / this.maxMillimeter// 計算每像素對應的秒數const secondsPerPixel = 0.04 / this.zoom// 計算gapX確保波形填滿畫布const samplesPerPixel = this.sampleRate * secondsPerPixelthis.gapX = Math.max(1, samplesPerPixel / this.STEP_SIZE)this.xSize = Math.ceil(this.waveformWidth / this.gapX)console.log(`波形參數:gapX=${this.gapX}, xSize=${this.xSize}`)},// 處理音頻URLhandleAudioUrl(audioUrl) {if (!audioUrl) returnthis.resetComponent()if (audioUrl.endsWith('.pcm')) {this.loadPcmAudio(audioUrl)} else {this.downloadAudio(audioUrl)}},// 重置組件狀態resetComponent() {this.stopPlayback()this.fileData = new Int8Array(0)this.pcmData = new Float32Array(0)this.index = 0this.waveformData = []this.spectrogram = []this.clearCanvas()if (this.audio && this.audio.src) {URL.revokeObjectURL(this.audio.src)this.audio.src = ''}},// 加載PCM音頻loadPcmAudio(url) {fetch(url, {method: 'GET',headers: { 'X-Mintti-Web-Token': Vue.ls.get(ACCESS_TOKEN) },}).then((res) => res.arrayBuffer()).then((buffer) => this.initAudioPlayer(buffer)).catch((err) => {console.error('PCM 加載失敗:', err)this.$message.warning('音頻加載失敗')})},// 下載音頻downloadAudio(url) {axios.get(url, { responseType: 'arraybuffer' }).then((res) => this.initAudioPlayer(res.data)).catch((err) => {console.error('下載失敗:', err)this.$message.warning('音頻下載失敗')})},// 初始化音頻播放器initAudioPlayer(arraybuffer) {const uint8 = new Uint8Array(arraybuffer)const isWav = uint8[0] === 82 && uint8[1] === 73 && uint8[2] === 70 // 'RIFF'const dataStart = isWav ? 44 : 0// 存儲原始文件數據this.fileData = new Int8Array(arraybuffer.slice(dataStart))// 處理16位PCM數據const pcmData = new Int16Array(arraybuffer.slice(dataStart))this.pcmData = new Float32Array(pcmData.length)// 16位PCM轉Float32 (-32768~32767 -> -1~1)for (let i = 0; i < pcmData.length; i++) {this.pcmData[i] = pcmData[i] / 32768.0}// 創建WAV文件用于播放const wavHeader = this.createWavHeader(pcmData.length)const wavData = new Uint8Array(wavHeader.byteLength + arraybuffer.byteLength - dataStart)wavData.set(new Uint8Array(wavHeader), 0)wavData.set(uint8.subarray(dataStart), wavHeader.byteLength)const blob = new Blob([wavData], { type: 'audio/wav' })const url = URL.createObjectURL(blob)this.audio.src = urlthis.audio.load()// 監聽事件this.audio.addEventListener('play', () => (this.isPlaying = true))this.audio.addEventListener('pause', () => (this.isPlaying = false))this.audio.addEventListener('ended', () => this.stopPlayback())this.audio.addEventListener('seeked', () => {this.index = Math.floor((this.audio.currentTime * 1000) / this.interval)this.waveformData = []this.spectrogram = []})},// 創建WAV文件頭createWavHeader(dataLength) {const buffer = new ArrayBuffer(44)const view = new DataView(buffer)// RIFF標識this.writeString(view, 0, 'RIFF')// 文件長度view.setUint32(4, 36 + dataLength * 2, true)// WAVE標識this.writeString(view, 8, 'WAVE')// fmt子塊this.writeString(view, 12, 'fmt ')// fmt長度view.setUint32(16, 16, true)// 編碼方式: 1表示PCMview.setUint16(20, 1, true)// 聲道數view.setUint16(22, 1, true)// 采樣率view.setUint32(24, this.sampleRate, true)// 字節率view.setUint32(28, this.sampleRate * 2, true)// 塊對齊view.setUint16(32, 2, true)// 位深度view.setUint16(34, 16, true)// data標識this.writeString(view, 36, 'data')// data長度view.setUint32(40, dataLength * 2, true)return buffer},writeString(view, offset, string) {for (let i = 0; i < string.length; i++) {view.setUint8(offset + i, string.charCodeAt(i))}},// 開始播放startPlay() {if (this.audio && this.fileData.length > 0) {// 重置狀態if (this.audio.ended || this.index * this.interval >= this.pcmData.length / (this.sampleRate / 1000)) {this.stopPlayback()}// 同步起始時間this.lastTime = performance.now()this.audio.play()this.isPlaying = truethis.timer()}},// 定時器timer() {if (!this.isPlaying) return// 計算基于音頻時間的理想幀數const targetFrame = Math.floor((this.audio.currentTime * 1000) / this.interval)const maxCatchUpFrames = 5 // 最大追趕幀數,避免卡頓// 適度追趕,避免一次性處理太多幀導致卡頓let framesToUpdate = Math.min(targetFrame - this.index, maxCatchUpFrames)while (framesToUpdate > 0) {this.refreshData()framesToUpdate--}// 正常情況下一幀一幀更新if (framesToUpdate === 0 && this.index < targetFrame) {this.refreshData()}this.animationId = requestAnimationFrame(() => this.timer())},// 刷新數據refreshData() {// 處理波形圖數據 - 每次更新1600字節數據const start = this.index * 1600const end = start + 1600if (start >= this.fileData.length) {this.stopPlayback()return}const byteArray = this.fileData.slice(start, end)const shortArray = new Int16Array(byteArray.length / 2)//遍歷 byteArray,將每兩個字節合并成一個短整型for (let i = 0; i < byteArray.length; i += 2) {shortArray[i / 2] = (byteArray[i] & 0xff) | ((byteArray[i + 1] & 0xff) << 8)}// 修改波形數據處理部分for (let i = 0; i < shortArray.length; i += this.STEP_SIZE) {// 限制波形數據長度,避免內存增長if (this.waveformData.length >= this.xSize * 2) {// 適當增加緩沖區this.waveformData.shift()}this.waveformData.push(shortArray[i])}// 處理頻譜圖數據 - 每次更新fftSize/2個樣本const fftStart = this.index * (this.fftSize / 2)const fftEnd = fftStart + this.fftSizelet segmentif (fftEnd > this.pcmData.length) {segment = new Float32Array(this.fftSize)segment.set(this.pcmData.slice(fftStart))} else {segment = this.pcmData.slice(fftStart, fftEnd)}// 執行FFTconst spectrum = this.fft(segment)// 歸一化const maxDB = 0,minDB = -80const normalized = spectrum.map((v) => {const dbValue = 20 * Math.log10(v + 1e-6)return Math.max(0, Math.min(255, Math.floor(((dbValue - minDB) / (maxDB - minDB)) * 255)))})// 更新頻譜數據if (this.spectrogram.length >= this.xSize) {this.spectrogram.shift()}this.spectrogram.push(normalized)// 繪制this.drawWaveform()this.drawSpectrogram()this.index += 1},// FFT實現fft(input) {const n = input.lengthconst logN = Math.log2(n)if (n !== 1 << logN) throw new Error('FFT length must be power of 2')// 應用Hamming窗const windowed = new Float32Array(n)for (let i = 0; i < n; i++) {windowed[i] = input[i] * (0.54 - 0.46 * Math.cos((2 * Math.PI * i) / (n - 1)))}const re = new Float32Array(n)const im = new Float32Array(n)for (let i = 0; i < n; i++) {re[i] = windowed[i]im[i] = 0}// 位逆序置換for (let i = 1, j = 0; i < n - 1; i++) {let k = n >> 1while (j >= k) {j -= kk >>= 1}j += kif (i < j) {;[re[i], re[j]] = [re[j], re[i]];[im[i], im[j]] = [im[j], im[i]]}}// 蝶形計算for (let size = 2; size <= n; size <<= 1) {const half = size >> 1const angle = (-2 * Math.PI) / sizeconst w = [1, 0]const wStep = [Math.cos(angle), Math.sin(angle)]for (let i = 0; i < half; i++) {for (let j = i; j < n; j += size) {const l = j + halfconst tRe = w[0] * re[l] - w[1] * im[l]const tIm = w[0] * im[l] + w[1] * re[l]re[l] = re[j] - tReim[l] = im[j] - tImre[j] += tReim[j] += tIm}const tmp = w[0] * wStep[0] - w[1] * wStep[1]w[1] = w[0] * wStep[1] + w[1] * wStep[0]w[0] = tmp}}// 計算幅度譜const spectrum = new Float32Array(n / 2)for (let i = 0; i < n / 2; i++) {const mag = Math.sqrt(re[i] ** 2 + im[i] ** 2)spectrum[i] = Math.log10(mag + 1) * 100}return spectrum},// 繪制波形圖drawWaveform() {const ctx = this.waveformCtxctx.clearRect(0, 0, this.waveformWidth, this.waveformHeight)this.drawBg(ctx)ctx.beginPath()ctx.lineWidth = 1ctx.strokeStyle = '#48a1e0'const len = this.waveformData.lengthconst mCenterY = this.waveformHeight / 2const maxPoints = Math.ceil(this.waveformWidth / this.gapX)// 計算可見數據點的起始索引const startIndex = Math.max(0, len - maxPoints)// 從右向左繪制所有可見點for (let i = startIndex; i < len; i++) {const y = Math.floor(this.calcRealMv(this.maxMidScopeY - this.waveformData[i]) * this.gain * this.zoom + mCenterY)// 關鍵修改:計算x坐標,確保波形可以從最右移動到最左const x = this.waveformWidth - (len - i) * this.gapXif (i === startIndex) {ctx.moveTo(x, y)} else {ctx.lineTo(x, y)}}ctx.stroke()},// 繪制頻譜圖drawSpectrogram() {const ctx = this.spectrogramCtxconst { width, height } = ctx.canvasctx.clearRect(0, 0, width, height)const dx = width / Math.max(this.xSize, this.spectrogram.length)for (let x = 0; x < this.spectrogram.length; x++) {const spec = this.spectrogram[x]const canvasX = width - (this.spectrogram.length - x) * dxfor (let y = 0; y < spec.length; y++) {// 使用對數縮放增強低頻顯示const freqIndex = Math.floor(Math.pow(y / spec.length, 0.7) * spec.length)const colorIdx = Math.max(0, Math.min(255, spec[freqIndex]))ctx.fillStyle = this.colorMap[colorIdx]// 低頻在底部,高頻在頂部const pixelY = height - y * (height / spec.length)ctx.fillRect(canvasX, pixelY, dx, height / spec.length)}}},// 繪制背景網格drawBg(ctx) {ctx.lineWidth = 1this.drawGrid(ctx, this.maxMillimeter)ctx.lineWidth = 2this.drawGrid(ctx, this.maxMillimeter / 5)},// 繪制網格drawGrid(ctx, cols) {const { width, height } = ctx.canvasctx.strokeStyle = '#ccc'const rowSpace = height / cols// 畫豎線for (let i = 0; i * rowSpace <= width; i++) {ctx.beginPath()ctx.moveTo(i * rowSpace, 0)ctx.lineTo(i * rowSpace, height)ctx.stroke()}// 畫橫線for (let i = 0; i <= cols; i++) {ctx.beginPath()ctx.moveTo(0, i * rowSpace)ctx.lineTo(width, i * rowSpace)ctx.stroke()}},// 計算實際電壓值calcRealMv(point) {return (point * 3.3) / 32767},// 停止播放stopPlayback() {this.isPlaying = falseif (this.audio) {this.audio.pause()this.audio.currentTime = 0}this.index = 0this.waveformData = []this.spectrogram = []this.clearAnimation()this.clearCanvas()},// 清除動畫clearAnimation() {if (this.animationId) {cancelAnimationFrame(this.animationId)this.animationId = null}},// 清除畫布clearCanvas() {this.waveformCtx.clearRect(0, 0, this.waveformWidth, this.waveformHeight)this.drawBg(this.waveformCtx)this.spectrogramCtx.clearRect(0, 0, this.spectrogramWidth, this.spectrogramHeight)},},beforeDestroy() {this.stopPlayback()if (this.audio && this.audio.src) {URL.revokeObjectURL(this.audio.src)}},
}
</script><style scoped>
.audio-visualizer {display: flex;flex-direction: column;align-items: center;padding: 2px;font-family: Arial, sans-serif;
}.visualization-container {width: 100%;display: flex;flex-direction: column;gap: 2px;
}.waveform-container canvas {border: 1px solid #333;border-radius: 4px;
}.spectrogram-container canvas {border: 1px solid #333;border-radius: 4px;
}.audio-controls {margin-top: 5px;width: 100%;
}audio {width: 100%;
}
</style>

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/93779.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/93779.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/93779.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【Python系列】Flask 應用中的主動垃圾回收

博客目錄一、Python 內存管理基礎二、Flask 中手動觸發 GC 的基本方法三、高級 GC 策略實現1. 使用裝飾器進行請求級別的 GC2. 定期 GC 的實現四、Flask 特有的 GC 集成方式1. 使用 teardown_request 鉤子2. 結合應用上下文管理五、智能 GC 策略六、注意事項與最佳實踐七、替代…

Linux和shell

最快入門的方式是使用蘋果系統。此外&#xff0c;累計補充學習&#xff1a;一、目錄結構/bin&#xff0c;二進制文件 /boot&#xff0c;啟動文件 /dev&#xff0c;設備文件 /home&#xff0c;主目錄&#xff0c;一般外接包、安裝包放在這里 /lib&#xff0c;庫文件 /opt&#x…

告別內存泄漏:你的Rust語言30天征服計劃

歡迎踏上Rust學習之旅&#xff01;第一周&#xff1a;奠定基礎 (Week 1: Laying the Foundation)第1天&#xff1a;環境搭建與 “Hello, World!”核心概念: 安裝Rust工具鏈 (rustup)&#xff0c;它包含了編譯器rustc和包管理器Cargo。Cargo是你的好朋友&#xff0c;用于創建項目…

亂刪文件,電腦不能開機,怎么辦

相信不少朋友在清理電腦、釋放空間時&#xff0c;都做過一件“后悔一整年”的事——亂刪系統文件。本來只是想讓電腦快點、干凈點&#xff0c;結果第二天一開機&#xff1a;黑屏了、藍屏了、無限重啟了&#xff0c;甚至連桌面都見不到了&#xff01;很多用戶在刪文件時&#xf…

ICODE SLIX2有密鑰保護的物流跟蹤、圖書館管理ISO15693標簽讀寫Delphi源碼

本示例使用設備&#xff1a;https://item.taobao.com/item.htm?spma21dvs.23580594.0.0.6781645eXF3tm5&ftt&id959258149468 一、密鑰認證 procedure TForm1.Button21Click(Sender: TObject); varctrlword:byte;passwordid:byte; //密鑰類型status:byte; //存…

核環境特種機器人設備的抗輻照芯片選型方案

摘要&#xff1a;核能作為國家能源安全的重要組成部分&#xff0c;對工業自動化設備的穩定性和可靠性提出了極高的要求。機器人設備在涉核環境下的日常巡檢、設備維護、應急響應等任務中發揮著不可替代的作用。然而&#xff0c;涉核環境&#xff0c;尤其是高能粒子的輻照效應&a…

Linux權限系統完全指南:從本質到安全實踐

一、權限的本質&#xff1a;Linux安全的核心邏輯在Linux的多用戶環境中&#xff0c;權限系統通過三個關鍵維度實現資源隔離&#xff1a;用戶標識 (UID)&#xff1a;系統通過數字ID識別用戶&#xff0c;root用戶的UID固定為0組標識 (GID)&#xff1a;用戶組機制實現批量權限管理…

養老院跌倒漏報率↓78%!陌訊多模態算法在智慧照護中的邊緣計算優化

?摘要??&#xff1a; 針對養老場景中復雜光照與遮擋導致的跌倒漏報問題&#xff0c;陌訊視覺算法通過多模態融合與邊緣計算優化&#xff0c;實測顯示在RK3588 NPU硬件上實現??mAP0.5達89.3%??&#xff0c;較基線模型提升28.5%&#xff0c;功耗降低至7.2W。本文解析其動態…

老年護理實訓室建設方案:打造安全、規范、高效的實踐教學核心平臺

在老齡化社會加速發展的背景下&#xff0c;培養高素質、技能過硬的老年護理專業人才迫在眉睫。一個設計科學、功能完備的老年護理實訓室&#xff0c;正是院校提升實踐教學質量&#xff0c;對接行業需求的核心平臺。本方案旨在構建一個安全、規范、高效的現代化實訓環境。點擊獲…

OpenCv中的 KNN 算法實現手寫數字的識別

目錄 一.案例&#xff1a;手寫數字的識別 1.安裝opencv-python庫 2.將大圖分割成10050個小圖&#xff0c;每份對應一個手寫數字樣品 3.訓練集和測試集 4.為訓練集和測試集準備結果標簽 5.模型訓練與預測 6.計算準確率 7.完整代碼實現 一.案例&#xff1a;手寫數字的識別…

TCP/IP 傳輸層詳解

TCP/IP 傳輸層詳解 傳輸層&#xff08;Transport Layer&#xff09;是 TCP/IP 模型的第四層&#xff08;對應 OSI 模型的傳輸層&#xff09;&#xff0c;核心功能是實現 端到端&#xff08;進程到進程&#xff09;的可靠通信。主要協議包括&#xff1a; TCP&#xff08;傳輸控制…

深度學習筆記:Overview

本文根據吳恩達老師的深度學習課程整理而來&#xff0c;在此表示感知。 文章目錄1.課程筆記2.編程作業1.課程筆記 1&#xff09;深度學習筆記&#xff08;1&#xff09;&#xff1a;神經網絡基礎 2&#xff09;深度學習筆記&#xff08;2&#xff09;&#xff1a;淺層神經網絡…

LLM之RAG理論(十八)| ChatGPT DeepResearch 深度研究功能全面技術分析報告

一、背景與行業環境1.1 DeepResearch 的誕生與戰略意義ChatGPT DeepResearch&#xff08;深度研究&#xff09;是 OpenAI 于 2025 年 2 月 3 日正式發布的全新 AI 智能體產品&#xff0c;是繼 o3-mini 模型發布后&#xff0c;OpenAI 在 AI 研究領域的又一重大突破。這一功能的推…

數據庫學習--------數據庫日志類型及其與事務特性的關系

在數據庫系統中&#xff0c;日志是保證數據可靠性和一致性的重要組成部分&#xff0c;尤其與事務的特性緊密相連。無論是事務的原子性、一致性&#xff0c;還是持久性&#xff0c;都離不開日志的支持。數據庫日志&#xff08;Database Log&#xff09;是數據庫系統記錄自身操作…

如何在 Ubuntu 24.04 或 22.04 LTS 上安裝 OpenShot 視頻編輯器

OpenShot 視頻編輯器是一款輕量級工具,不需要高性能硬件即可編輯視頻。它最初是一個愛好項目,后來成為一款擁有簡單干凈用戶界面的流行免費編輯工具。這款直觀的視頻編輯器可以剪輯影片,并添加額外的視頻和音頻素材。最終,您可以將作品導出為您選擇的格式。本教程將向您展示…

SpringMVC核心原理與實戰指南

什么是MVC&#xff1f; MVC英文是Model View Controller&#xff0c;是模型(model)&#xff0d;視圖(view)&#xff0d;控制器(controller)的縮寫&#xff0c;一種軟件設計規范。 MVC是用一種業務邏輯、數據、界面顯示分離的方法&#xff0c;將業務邏輯聚集到一個部件里面&am…

【JavaEE】(7) 網絡原理 TCP/IP 協議

一、應用層 應用層是程序員最關心的一層&#xff0c;需要自定義數據傳輸的格式&#xff0c;即前&#xff08;客戶端&#xff09;后&#xff08;服務器&#xff09;端交互的接口&#xff0c;然后調用傳輸層的 socket api 來實現網絡通信。 自定義數據傳輸的協議&#xff0c;主要…

深入理解 Slab / Buddy 分配器與 MMU 映射機制

&#x1f4d6; 推薦閱讀&#xff1a;《Yocto項目實戰教程:高效定制嵌入式Linux系統》 &#x1f3a5; 更多學習視頻請關注 B 站&#xff1a;嵌入式Jerry 深入理解 Slab / Buddy 分配器與 MMU 映射機制 在現代 Linux 內核中&#xff0c;物理內存的管理和虛擬地址的映射是系統性能…

Layui核心語法快速入門指南

Layui 基本語法學習指南 Layui 是一個經典的模塊化前端框架&#xff0c;以其輕量易用、組件豐富著稱。以下是 Layui 的核心語法結構和使用方法&#xff1a; 一、模塊加載機制&#xff08;核心基礎&#xff09; // 標準模塊加載語法 layui.use([module1, module2], function()…