切換語言為:簡體

Tone.js 音訊播放器詳細使用方法,及遇到的bug解決方法

  • 爱糖宝
  • 2024-07-23
  • 2135
  • 0
  • 0

使用 Tone.js 實現一個基本的音訊播放器,該播放器不僅支援基本的播放控制,如播放、暫停、進度顯示等等,還具備倍速播放和音高調整等功能

本文介紹如何使用 Tone.js 構建一個完整的音訊播放器,並實現音高調整功能。

1. 安裝及引入

Tone.js 是一個功能強大且易於使用的 Web Audio 庫,專為創作互動式音樂和聲音設計而設計。它提供了許多抽象層,使得在瀏覽器中製作複雜的音訊應用程式變得更加容易,適合任何希望在網頁上實現高質量音訊和音樂體驗的開發者。

Tone.js 的目標是為音樂家和音訊工程師提供一種熟悉的工具集,類似於傳統的數字音訊工作站(DAW)軟體。

Tone.js 官方文件:tonejs.github.io/docs/15.0.4…

  1. 安裝

# 我的版本是v15.0.4 
npm install tone


  1. 引入

import * as Tone from "tone";


2. 建立播放器

  • Tone.Player: 是一個多功能元件,旨在播放音訊檔案,並具備開始、迴圈和停止播放等功能

    • onload:載入完成回撥。自動載入音訊檔案,載入完成後,才能開始播放

    • onstop:停止播放回調。(注意:暫停/拖動進度條時,也會觸發,無法當作播放結束事件處理)

    • start:開始播放事件

    • stop:停止播放事件

    • dispose:釋放資源,當不再需要播放器時,記得呼叫 .dispose() 方法來釋放資源

    • playbackRate:設定播放速率

    • volume:設定播放音量

    • mute:設定禁音/非禁音狀態

    • seek:設定當前播放位置

    • duration:獲取音訊檔案的總時長(player.current.buffer.duration)

    • loop:設定是否迴圈播放

  • Tone.PitchShift:Tone.js 中的一個效果器,對輸入訊號進行近乎實時的音調轉換。該效果是透過調整 DelayNode 的延遲時間來實現的,具體來說,是使用鋸齒波來週期性地加速或減速延遲時間,從而產生音高變化的效果。

    • dispose:釋放資源

    • pitch:設定音調偏移量。引數可以是正數(升高音高)也可以是負數(降低音高)。例如,引數 0.5 表示升高半音,-1 表示降低一個全音。透過改變這個引數,可以實現實時的音高變換效果。

const player = useRef<Tone.Player | null>(null); // 播放器
const pitchShift = useRef<Tone.PitchShift | null>(null); // 音高效果器
const [playPitch, setPlayPitch] = useState(0); // 音高控制

useEffect(() => {
  if (!audioSrc) return;

  // 1. 建立 Tone.Player 例項,只在首次呼叫時建立
  player.current = new Tone.Player({
    url: audioSrc, // 音訊檔案的 URL
    onload: () => {
      // 當音訊載入完成時執行的回撥函式
      if (!player.current) return;
      // 獲取音訊總時長
      setAllTime(player.current.buffer.duration);
    },
    onstop: (data) => {
      // 當播放停止時執行的回撥函式
      // 注意:暫停/拖動進度條時,也會觸發,無法當作播放結束事件處理
      console.log("Tone onstop:");
    },
    onerror: (error) => {
      // 當載入音訊檔案出錯時執行的回撥函式
      console.error("Tone onerror:", error);
    },
  });
  // 2. 建立 PitchShift 節點,引數為音高偏移量
  // 初始音高偏移量為 0,意味著沒有音高變化
  pitchShift.current = new Tone.PitchShift(0);
  // 3. 將 Player 的輸出連線到 PitchShift 節點
  // 這樣音訊資料會先經過 PitchShift 處理,再輸出
  player.current.connect(pitchShift.current);
  // 4. 將 PitchShift 節點的輸出連線到音訊上下文的目的地
  // 這是音訊輸出的最終目的地,通常是使用者的揚聲器
  pitchShift.current.toDestination();

  return () => {
    // 清理資源,釋放 Player 和 PitchShift 例項佔用的資源
    player.current?.dispose();
    pitchShift.current?.dispose();
  };
}, [audioSrc]);


3. 播放/暫停

3.1 Player 處理

Player提供了startstop方法,分別用於開始和停止播放音訊。

// 播放或者暫停
const pauseOrPlay = () => {
  if (isPlay) {
    player.current.stop();
    setIsPlay(false);
  } else {
    player.current.start();
    setIsPlay(true);
  }
};


3.2 Transport 處理

但是 Player 沒有找到暫停的方法,start()方法每次都是從頭開始播放的。後來查了資料,發現可以用Tone.getTransport()處理

Tone.Transport 通常與 Player 或 Synth 等其他 Tone.js 元件結合使用,以實現更復雜的音訊同步和控制。例如,可以讓多個音軌或音效同步啟動和停止,或者根據節拍和時間簽名來安排音符和效果。

  • Tone.getTransport()方法返回的是 Transport 例項,這個例項可以用來控制整個音訊應用的節奏和同步,包括啟動、停止、暫停、跳轉以及節拍和時間簽名的管理。

    • start([time]) - 開始播放,如果提供了 time 引數,它將從指定的時間開始播放。

    • stop([time]) - 停止播放

    • pause([time]) - 暫停播放

    • clear([eventId]) - 清除事件

    • position:控制當前播放位置

注意:使用前要先同步一下,player.current.sync().start(0)

注意:重新播放前,需要重置position

player.current = new Tone.Player({
  url: audioSrc,
  onload: () => {
    // 設定迴圈播放
    // player.current.loop = true;
    // 在音訊載入完成後,與Transport同步
    // 注意:要加該程式碼,Tone.getTransport().start()才能起作用
    player.current.sync().start(0);
  },
});

const pauseOrPlay = () => {
  if (isPlay) {
    Tone.getTransport().pause();
    setIsPlay(false);
  } else {
    // 注意:確保在開始播放前,position被重置為0,才能開始重新播放
    if (currentTime >= allTime) {
      Tone.getTransport().position = 0;
    }
    Tone.getTransport().start();
    setIsPlay(true);
  }
};


4. 禁音/取消禁音

//禁音/取消禁音
const onMuteAudio = () => {
  if (!player.current) return;
  setIsMuted(!isMuted);
  player.current.mute = !isMuted;
};


5. 音量控制

  • value / 100:將 value(介於 0 到 100 之間的百分比值)轉換為 0 到 1 之間的範圍,這是 Tone.Gain 節點期望的增益值範圍。

  • Tone.gainToDb:這個函式將線性的增益值轉換為分貝值。在內部,它使用以下公式:dB = 20 * log10(gain)

為什麼使用分貝?因為分貝能夠更好地反映人耳對音量變化的感知。例如,將音量增加一倍(線性增益從 1 增加到 2)在分貝中大約相當於增加了 6dB,而將音量增加到原來的十分之一(線性增益從 1 減少到 0.1)則相當於減少了 20dB。這種對數關係使得分貝成為描述音量變化的更直觀的單位。

// 改變音量
const changeVolume = (value: number) => {
  if (!player.current) return;
  // 將百分比音量值轉換為分貝,就可以在Tone.js的音訊處理鏈中使用了
  player.current.volume.value = Tone.gainToDb(value / 100);
  setVolume(value);
  setIsMuted(!value);
};


6. 倍速控制

// 播放倍數
const changePlayRate = (num: number) => {
  if (!player.current) return;
  setPlayRate(num);
  player.current.playbackRate = num;
};


7. 音高控制

// 播放音高
const changePlayPitch = (num: number) => {
  if (pitchShift.current) {
    setPlayPitch(num);
    pitchShift.current.pitch = num;
  }
};


8. 播放進度條

8.1 Transport 處理

Player 播放器沒有找到監聽播放時間的事件,onstop 也無法確認播放結束,所以使用 Tone.Transport 處理

scheduleRepeat:用於按指定的時間間隔重複執行一個回撥函式;interval 引數是 "16n",表示每十六分音符執行一次回撥。

useEffect(() => {
  const eventId = Tone.getTransport().scheduleRepeat((time) => {
    const currentTime = Tone.Time(time).toSeconds();
    console.log("Tone currentTime:", currentTime);
    // setCurrentTime(currentTime);
  }, "16n");

  return () => {
    // 清理資源
    Tone.getTransport().clear(eventId);
  };
}, [audioSrc]);


使用時,發現這個不準,停止時也在變動,無法作為進度條的控制。也沒找到其他方式,就暫時放棄了

8.2 Audio timeupdate 處理

主要是文件太少了,可參考的資料也少,確實沒找到其他方式處理進度條,但是功能還是得實現的呀。。。

最後無奈的處理方式,透過隱藏的 Audio 控制元件處理。使用了 Audio API 的 timeupdate 事件,透過監聽音訊的播放時間,實時更新進度條。

注意:Audio 一直保持 muted 禁音處理,只用作進度條同步

所以其他事件(播放/暫停、禁音、音量、倍速等)中,也需要新增 audioRef.current 的處理邏輯,進行播放進度同步,我就不一一新增了。如下以 changeTime 為例:

// 修改播放時間
const changeTime = (value: number) => {
  if (!player.current) return;
  audioRef.current!.currentTime = value;
  // 控制播放進度
  player.current.seek(value);
  setCurrentTime(value);
  if (
    value === player.current.buffer.duration || // Tone.js播放器總時長
    value === audioRef.current!.duration // 音訊播放器總時長
  ) {
    // 上述兩個總時長不是完全相等的,有些誤差
    setIsPlay(false);
  }
};


9. 播放/暫停 bug 修復

(1)問題:測試時,如果同時初始化多個 MAudio 元件,會出現播放時,其他元件也會同時播放的情況。

(2)原因:

Tone.Transport是 Tone.js 中的全域性控制器,它允許以一種統一的方式控制音訊的播放、暫停、停止以及各種時間相關的操作。

使用player.current.sync().start(0)來同步播放器時,實際上是在告訴播放器與 Tone.Transport 的節奏和時間線保持一致。在同步之後,都是從時間點 0 開始的,按照 Transport 的節奏同時開始播放、暫停。

(3)解決:找了好久,沒找到解決方法。。。,Transport 也沒法用了

皇天不負有心人,最後終於發現了一個解決方案,還是使用 Player 來處理

  • start(time?, offset?, duration?):用於指定何時開始播放音訊緩衝區(buffer),並且可以指定從緩衝區的哪個位置開始播放,以及播放的持續時間。

    • time:表示開始播放的時間

    • offset:從音訊樣本的開始位置偏移多少時間開始播放

    • duration:表示播放的持續時間

// 播放或者暫停
const pauseOrPlay = () => {
  if (!player.current) return;
  if (isPlay) {
    player.current.stop();
    audioRef.current!.pause();
    setIsPlay(false);
  } else {
    if (
      currentTime >= allTime ||
      currentTime >= player.current.buffer.duration
    ) {
      // 重新播放
      player.current.start(0);
    } else {
      // 繼續播放,使用offset控制
      player.current.start(0, currentTime);
    }
    audioRef.current!.play();
    setIsPlay(true);
  }
};

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.