<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          爺青回!用原生 Audio API 實(shí)現(xiàn)一個(gè)千千靜聽(tīng)

          共 7315字,需瀏覽 15分鐘

           ·

          2022-03-15 23:00

          前言

          最近看了一下鐘文澤的 Macbook Pro 測(cè)評(píng)視頻(唉,最近又想買(mǎi)電子產(chǎn)品了),他在測(cè)評(píng)音響的時(shí)候,點(diǎn)播了一首蔡琴的《渡口》。

          當(dāng)聽(tīng)到這首歌的時(shí)候,我真的是情不自禁地感嘆:“爺青回??!”,想當(dāng)年,第一次聽(tīng)這首歌的時(shí)候還是在 Windows XP 系統(tǒng)上的 千千靜聽(tīng) 這個(gè)播放器里聽(tīng)到的,那時(shí)印象最深刻的就是里面的 音頻可視化(頻譜圖) 了。

          當(dāng)我在發(fā)呆、無(wú)聊的時(shí)候,音頻頻譜圖里的小浮塊總能讓我盯上一整天。而如今,在各大音樂(lè)軟件中已很少看到這樣的頻譜圖了。那今天就跟大家一起用原生的 Audio API 來(lái)實(shí)現(xiàn)這個(gè)頻譜圖吧。

          項(xiàng)目已經(jīng)放在 Github[1],也可以在 這里預(yù)覽[2]

          由于原來(lái)的擬物風(fēng)格實(shí)現(xiàn)太難實(shí)現(xiàn)了,只能做個(gè)粗糙的版本 :)

          解決思路

          首先我們要理解頻譜圖里的這些“長(zhǎng)條”是什么意思。實(shí)際上這是音頻里的 頻率 Frequency,我們常說(shuō)的低音炮和美高音就是指在聲音在低頻區(qū)和高頻區(qū)的表現(xiàn)。

          了解了音頻頻率后,我們可以先理清一下這個(gè)小玩具的實(shí)現(xiàn)思路:

          從音頻獲取音頻流 stream,通過(guò)中間的解析器分析出頻率值 freqency,將這些頻率值通過(guò)“長(zhǎng)條”的方式繪制在 上,然后以此不斷循環(huán)就可以實(shí)現(xiàn)這樣的頻譜動(dòng)態(tài)圖了。

          根據(jù)上面的思路,我們首先要準(zhǔn)備好這樣的頁(yè)面結(jié)構(gòu):

          const?Player:?FC?=?()?=>?{
          ??const?{visualize}?=?useAudioVisualization('#canvas',?50);

          ??const?audioRef?=?useRef(null);

          ??const?onPlay?=?async?()?=>?{
          ????if?(audioRef.current)?{
          ??????await?audioRef.current.play();
          ??????const?stream?=?(audioRef.current?as?any).captureStream();
          ??????visualize(stream)
          ????}
          ??}

          ??return?(
          ????<div?className={styles.player}>
          ??????<div?className={styles.canvas}>
          ????????<canvas?id="canvas"?width={500}?height={300}/>
          ??????div>

          ??????<div?className={styles.controls}>
          ????????<audio?ref={audioRef}?src={audioUrl}?onPlay={onPlay}?controls?/>
          ??????div>
          ????div>
          ??)
          }

          useAudioVisualization

          這里使用 React Hook 的方式來(lái)封裝可視化邏輯:

          const?useAudioVisualization?=?(selector:?string,?length?=?50)?=>?{
          ??//?開(kāi)始可視化
          ??const?visualize?=?(stream:?MediaStream)?=>?{
          ??
          ??}

          ??return?{?visualize?};
          }

          visualize

          在拿到音頻的流之后,我們就可以調(diào)用 Audio API 來(lái)創(chuàng)建解析器并分析音頻了。

          //?開(kāi)始可視化
          const?visualize?=?(stream:?MediaStream)?=>?{
          ??const?canvasEl:?HTMLCanvasElement?|?null?=?document.querySelector(selector);
          ??if?(!canvasEl)?{
          ????throw?new?Error('找不到?canvas');
          ??}

          ??//?創(chuàng)建解析器
          ??audioCtxRef.current?=?new?AudioContext()
          ??analyserRef.current?=?audioCtxRef.current.createAnalyser();

          ??//?獲取音頻源
          ??const?source?=?audioCtxRef.current.createMediaStreamSource(stream);
          ??//?將音頻源連接解析器
          ??source.connect(analyserRef.current);

          ??//?準(zhǔn)備數(shù)據(jù)數(shù)組
          ??analyserRef.current.fftSize?=?256;
          ??const?bufferLength?=?analyserRef.current.frequencyBinCount;
          ??const?dataArray?=?new?Uint8Array(bufferLength);

          ??//?開(kāi)始遞歸畫(huà)圖
          ??drawEachFrame(canvasEl,?dataArray);
          }

          這里主要做了幾件事:

          • 通過(guò) AudioContext 創(chuàng)建 analyser
          • 將音頻輸入源連接 analyser,每次播放的時(shí)候,音頻都會(huì)經(jīng)過(guò) analyser 進(jìn)行處理
          • 設(shè)置 fft,從 analyser 獲取音頻頻率數(shù)據(jù) dataArray

          經(jīng)過(guò)上面的操作我們已經(jīng)拿到了音頻的數(shù)據(jù),接下來(lái)就是渲染 的時(shí)候了,開(kāi)始實(shí)現(xiàn) drawEachFrame。

          drawEachFrame

          我們?nèi)粘K吹降膭?dòng)畫(huà)本質(zhì)上都是一個(gè)畫(huà)面一個(gè)畫(huà)面連續(xù)播放的效果。

          只要畫(huà)面足夠快就可以讓畫(huà)面動(dòng)起來(lái),那究竟要多快呢?相信有的同學(xué)已經(jīng)開(kāi)始拿起紙和筆來(lái)算了。

          其實(shí)并不用這么復(fù)雜,這里給大家推薦一個(gè) API requestAnimationFrame。它會(huì)以瀏覽器的顯示頻率來(lái)作為其動(dòng)畫(huà)動(dòng)作的頻率,比如瀏覽器每 10ms 刷新一次,動(dòng)畫(huà)回調(diào)也每 10ms 調(diào)用一次,這樣就不會(huì)存在過(guò)度繪制的問(wèn)題,動(dòng)畫(huà)不會(huì)掉幀,自然流暢。

          只要我們?cè)?requestAnimationFramecallback 里不斷地繪制 就可以獲得一個(gè)流暢的頻譜圖了。

          //?每個(gè)動(dòng)畫(huà)幀都畫(huà)圖
          const?drawEachFrame?=?(canvasEl:?HTMLCanvasElement,?dataArray:?Uint8Array)?=>?{
          ??//?遞歸調(diào)用
          ??requestAnimateFrameIdRef.current?=?requestAnimationFrame(()?=>?drawEachFrame(canvasEl,?dataArray));

          ??if?(analyserRef.current)?{
          ????//?讀取當(dāng)前幀新的數(shù)據(jù)
          ????analyserRef.current.getByteFrequencyData(dataArray);
          ????//?更新長(zhǎng)度
          ????const?bars?=?dataArray.slice(0,?Math.min(length,?dataArray.length));
          ????//?畫(huà)圖
          ????clearCanvas(canvasEl);
          ????//?繪制小浮塊
          ????drawFloats(canvasEl,?bars);
          ????//?繪制條狀圖
          ????drawBars(canvasEl,?bars);
          ??}
          }

          上面的 drawEachFrame 里又調(diào)用了一次 requestAnimationFrame,以此來(lái)實(shí)現(xiàn)遞歸循環(huán)調(diào)用的效果。

          這里我們還會(huì)把 requestAnimateFrameId 給記錄下來(lái),以防之后銷(xiāo)毀時(shí)要調(diào)用 window.cancelAnimationFrame(id) 來(lái)清除。

          clearCanvas

          在繪制 前,我們先把它給清空一下:

          export?const?clearCanvas?=?(canvasEl:?HTMLCanvasElement)?=>?{
          ??const?canvasWidth?=?canvasEl.width;
          ??const?canvasHeight?=?canvasEl.height;
          ??const?canvasCtx?=?canvasEl.getContext("2d");

          ??if?(!canvasCtx)?{
          ????return;
          ??}

          ??//?繪制圖形
          ??canvasCtx.fillStyle?=?'rgb(29,19,62)';
          ??canvasCtx.fillRect(0,?0,?canvasWidth,?canvasHeight);
          }

          這樣就能得到一個(gè)純色的 “白板” 了:

          drawBars

          接下來(lái)實(shí)現(xiàn)條狀圖,圖示:

          代碼實(shí)現(xiàn):

          //?浮動(dòng)的小塊
          let?floats:?any?=?[];
          //?高度
          const?FLOAT_HEIGHT?=?4;
          //?下落高度
          const?DROP_DISTANCE?=?1;
          //?Bar?的?border?寬度
          const?BAR_GAP?=?2;

          export?const?drawBars?=?(canvasEl:?HTMLCanvasElement,?dataArray:?Uint8Array)?=>?{
          ??const?canvasWidth?=?canvasEl.width;
          ??const?canvasHeight?=?canvasEl.height;
          ??const?canvasCtx?=?canvasEl.getContext("2d");

          ??if?(!canvasCtx)?{
          ????return;
          ??}

          ??const?barWidth?=?canvasWidth?/?dataArray.length
          ??let?x?=?0;

          ??dataArray.forEach((dataItem)?=>?{
          ????const?barHeight?=?dataItem;

          ????//?添加漸變色
          ????const?gradient?=?canvasCtx.createLinearGradient(canvasWidth?/?2,?canvasHeight?/?2,?canvasWidth?/?2,?canvasHeight);
          ????gradient.addColorStop(0,?'#68b3ec');
          ????gradient.addColorStop(0.5,?'#4b5fc9');
          ????gradient.addColorStop(1,?'#68b3ec');

          ????//?畫(huà)?bar
          ????canvasCtx.fillStyle?=?gradient;
          ????canvasCtx.fillRect(x,?canvasHeight?-?barHeight,?barWidth,?barHeight);

          ????x?+=?barWidth?+?BAR_GAP;
          ??})
          }

          這里有幾個(gè)點(diǎn)要注意:

          • 畫(huà)長(zhǎng)方形的時(shí)候,原點(diǎn)是在左上角,所以 y 的值為 canvasHeight - barHeight,即 總高度 - 條形高度
          • 畫(huà)下一個(gè) bar 的時(shí)候,需要 + BORDER_WIDTH 來(lái)空出一個(gè)空隙,不然 bar 就都黏在一起了
          • 中畫(huà)漸變,需要用 addColorStop 來(lái)實(shí)現(xiàn)

          最后效果:

          drawFloats

          有了上面畫(huà)條狀 bar 的經(jīng)驗(yàn)后,我們很容易就能想到怎么畫(huà)這些小塊了:

          圖示:

          export?const?drawFloats?=?(canvasEl:?HTMLCanvasElement,?dataArray:?Uint8Array)?=>?{
          ??const?canvasWidth?=?canvasEl.width;
          ??const?canvasHeight?=?canvasEl.height;
          ??const?canvasCtx?=?canvasEl.getContext("2d");

          ??if?(!canvasCtx)?{
          ????return;
          ??}

          ??//?找到最大值,以及初始化高度
          ??dataArray.forEach((item,?index)?=>?{
          ????//?默認(rèn)值
          ????floats[index]?=?floats[index]?||?FLOAT_HEIGHT;
          ????//?處理當(dāng)前值
          ????const?pushHeight?=?item?+?FLOAT_HEIGHT;
          ????const?dropHeight?=?floats[index]?-?DROP_DISTANCE;
          ????//?取最大值
          ????floats[index]?=?Math.max(dropHeight,?pushHeight);
          ??})

          ??const?barWidth?=?canvasWidth?/?dataArray.length;
          ??let?x?=?0;

          ??floats.forEach((floatItem:?number)?=>?{
          ????const?floatHeight?=?floatItem;

          ????canvasCtx.fillStyle?=?'#3e47a0';
          ????canvasCtx.fillRect(x,?canvasHeight?-?floatHeight,?barWidth,?FLOAT_HEIGHT);

          ????x?+=?barWidth?+?BAR_GAP;
          ??})
          }

          這里最關(guān)鍵就是這個(gè)小浮塊的高度,我們直接取浮想塊下降了的高度 dropHeight 以及被 bar 推高的高度 pushHeight 他們兩的最大值就可以了 floats[index] = Math.max(dropHeight, pushHeight)。

          在實(shí)現(xiàn)好了之后,來(lái)一首試音原聲大碟《渡口》,即可享受頻譜圖帶來(lái)的快樂(lè):

          stopVisualize

          有開(kāi)始就有結(jié)束,由于這里動(dòng)用了 , requestAnimationFrame 這些資源,所以當(dāng)組件銷(xiāo)毀時(shí)應(yīng)該清空他們:

          const?useAudioVisualization?=?(selector:?string,?length?=?50)?=>?{
          ??...

          ??//?重置?canvas
          ??const?resetCanvas?=?()?=>?{
          ????const?canvasEl:?HTMLCanvasElement?|?null?=?document.querySelector(selector);
          ????if?(canvasEl)?{
          ??????const?emptyDataArray?=?(new?Uint8Array(length)).map(()?=>?0);
          ??????clearFloats();
          ??????clearCanvas(canvasEl);
          ??????drawFloats(canvasEl,?emptyDataArray);
          ????}
          ??}

          ??//?停止
          ??const?stopVisualize?=?()?=>?{
          ????if?(requestAnimateFrameIdRef.current)?{
          ??????window.cancelAnimationFrame(requestAnimateFrameIdRef.current);
          ??????resetCanvas();
          ????}
          ??};

          ??return?{
          ????visualize,
          ????stopVisualize,
          ????resetCanvas,
          ????requestAnimateFrameId:?requestAnimateFrameIdRef.current
          ??};
          }

          這里我們也把 requestAnimateFrameId 扔出來(lái),可由開(kāi)發(fā)者自己處理。

          完整的使用方式是這樣的:

          const?Player?=?()?=>?{
          ??const?{visualize,?stopVisualize,?resetCanvas}?=?useAudioVisualization('#canvas',?50);
          ??
          ??const?audioRef?=?useRef(null);

          ??const?onPlay?=?async?()?=>?{
          ????if?(audioRef.current)?{
          ??????stopVisualize();
          ??????await?audioRef.current.play();
          ??????const?stream?=?(audioRef.current?as?any).captureStream();
          ??????visualize(stream)
          ????}
          ??}

          ??const?onPause?=?async?()?=>?{
          ????resetCanvas();
          ??}

          ??useEffect(()?=>?{
          ????resetCanvas();
          ????return?()?=>?{
          ??????stopVisualize()
          ????}
          ??},?[]);

          ??return?(
          ????<div?className={styles.player}>
          ??????<div?className={styles.canvas}>
          ????????<canvas?id="canvas"?width={500}?height={300}/>
          ??????div>

          ??????<div?className={styles.controls}>
          ????????<audio?ref={audioRef}?src={audioUrl}?onPlay={onPlay}?onPause={onPause}?controls?/>
          ??????div>
          ????div>
          ??)
          }

          更好看的樣式就交給同學(xué)們自己實(shí)現(xiàn)了 :) 當(dāng)然你也可以在 我的 Github 項(xiàng)目 里直接 Copy 我的丑陋樣式。

          總結(jié)

          最后總結(jié)一下這個(gè)頻譜圖的實(shí)現(xiàn):

          1. 使用 Audio API 創(chuàng)建 analyser,將音頻流 stream 連接到 analyser
          2. 設(shè)置 analyserfft 參數(shù),以此獲取音頻數(shù)據(jù)
          3. 通過(guò)遞歸調(diào)用 requestAnimationFrame 來(lái)實(shí)現(xiàn)動(dòng)畫(huà)效果
          4. 使用 Canvas API 來(lái)繪制條形圖以及小浮塊,將這繪制操作放在 requestAnimationFrame 的回調(diào)中,從而展示動(dòng)態(tài)的頻譜圖

          如果你看完還是做不出自己的千千靜聽(tīng),可以在 我的 Github 項(xiàng)目 里直接看源碼實(shí)現(xiàn)。

          好了,這個(gè)千千靜聽(tīng)的項(xiàng)目就給大家?guī)У竭@里。如果你喜歡我的文章,可以走一波關(guān)注,一鍵三連我也不介意,比心 ??

          參考資料

          [1]

          Github 地址: https://github.com/haixiangyan/ttplayer

          [2]

          預(yù)覽地址: https://github.yanhaixiang.com/ttplayer/

          瀏覽 56
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产18欠欠欠一区二区 | 免费 69视频 | 91麻豆国产福利在线观看 | 另类中文字幕 | 蜜桃视频网站免费观看 |