1. 程式人生 > 其它 >SMFL 教程&個人筆記(2)

SMFL 教程&個人筆記(2)

本文大部分來自官方教程的Google翻譯 但是加了一點點個人的理解和其他相關知識

轉載請註明 原文連結 :https://www.cnblogs.com/Multya/p/16317401.html

官方教程: https://www.sfml-dev.org/tutorials/2.5/

播放聲音和音樂

聲音還是音樂?

SFML 提供了兩個用於播放音訊的類:sf::Soundsf::Music. 它們都提供或多或少相同的功能,主要區別在於它們的工作方式。(記得#include <SFML/Audio.hpp> 喂)

sf::Sound是一個輕量級物件,用於播放從sf::SoundBuffer. 它應該用於可以儲存在記憶體中的小聲音,並且在播放時應該沒有延遲。例如槍聲、腳步聲等。

sf::Music不會將所有音訊資料載入到記憶體中,而是從原始檔動態流式傳輸。它通常用於播放持續幾分鐘的壓縮音樂,否則將需要幾秒鐘才能載入並佔用數百 MB 的記憶體。

載入和播放聲音

如上所述,聲音資料不是直接儲存sf::Sound在一個名為sf::SoundBuffer. 這個類封裝了音訊資料,它基本上是一個 16 位有符號整數陣列(稱為“音訊樣本”)。樣本是聲音訊號在給定時間點的幅度,因此樣本陣列表示完整的聲音。

事實上,sf::Sound/sf::SoundBuffer類的工作方式 與圖形模組中的sf::Sprite/sf::Texture相同。因此,如果您瞭解精靈和紋理如何協同工作,您可以將相同的概念應用於聲音和聲音緩衝區。

您可以使用其loadFromFile功能從磁碟上的檔案載入聲音緩衝區:

#include <SFML/Audio.hpp>

int main()
{
    sf::SoundBuffer buffer;
    if (!buffer.loadFromFile("sound.wav"))
        return -1;

    ...

    return 0;
}

loadFromMemory與其他所有內容一樣,您也可以從記憶體(loadFromMemory) 或自定義輸入流(loadFromStream)載入音訊檔案。

SFML 支援音訊檔案格式 WAV, OGG/Vorbis 和 FLAC。由於許可問題,

支援 MP3。

您還可以直接從樣本陣列載入聲音緩衝區,以防它們來自其他來源:

std::vector<sf::Int16> samples = ...;
buffer.loadFromSamples(&samples[0], samples.size(), 2, 44100);

由於loadFromSamples載入的是樣本的原始陣列而不是音訊檔案,因此它需要額外的引數才能獲得對聲音的完整描述。第一個(第三個引數)是通道數;1 聲道定義單聲道,2 聲道定義立體聲,等等。第二個附加屬性(第四個引數)是取樣率;它定義了每秒必須播放多少個樣本才能重建原始聲音。

關於音訊的引數可以參考這個博文: https://blog.csdn.net/fuhanghang/article/details/123730503

可以看出SFML預設的取樣位數也是16bits 所以只用附加通道數和取樣率就能算出所有東西了

現在已經載入了音訊資料,我們可以用一個sf::Sound例項來播放它。

sf::SoundBuffer buffer;
// load something into the sound buffer...

sf::Sound sound;
sound.setBuffer(buffer);
sound.play();

很酷的是,如果您願意,您可以將相同的聲音緩衝區分配給多個聲音。您甚至可以毫無問題地一起玩它們。

聲音(和音樂)在單獨的執行緒中播放。這意味著你可以在呼叫後自由地做任何你想做的事情play()(當然除了破壞聲音或其資料),聲音將繼續播放直到它完成或明確停止。

播放音樂

sf::Sound不同的是,sf::Music它不預載入音訊資料,而是直接從源流式傳輸資料。音樂的初始化因此更直接:

sf::Music music;
if (!music.openFromFile("music.ogg"))
    return -1; // error
music.play();

需要注意的是,與所有其他 SFML 資源不同,載入函式loadFromFile被命名openFromFile. 這是因為音樂並沒有真正載入,這個功能只是開啟它。資料僅在稍後播放音樂時載入。它還有助於記住,只要播放音訊檔案,它就必須保持可用。
的其他載入函式sf::Music遵循相同的約定:如 openFromMemory, openFromStream.

下一步是什麼?

現在您可以載入和播放聲音或音樂,讓我們看看您可以用它做什麼。

要控制播放,可以使用以下功能:

  • play 開始或恢復播放
  • pause 暫停播放
  • stop 停止播放和倒帶
  • setPlayingOffset 改變當前播放位置

例子:

// start playback
sound.play();

// advance to 2 seconds
sound.setPlayingOffset(sf::seconds(2.f));

// pause playback
sound.pause();

// resume playback
sound.play();

// stop playback and rewind
sound.stop();

getStatus函式返回聲音或音樂的當前狀態,您可以使用它來知道它是停止、播放還是暫停。

聲音和音樂播放也由一些屬性控制,這些屬性可以隨時更改。

音高(pitch) 是改變聲音感知頻率的一個因素:大於 1 以較高的音高播放聲音,小於 1 以較低的音高播放聲音,1 保持不變。改變音高有一個副作用:它會影響播放速度。

sound.setPitch(1.2f);

音量(volume) 是……音量 。值範圍從 0(靜音)到 100(全音量)。預設值為 100,這意味著您不能發出比其初始音量更大的聲音。

sound.setVolume(50.f);

迴圈(loop) 屬性控制聲音/音樂是否自動迴圈播放 。如果它迴圈播放,它將在完成後從頭開始播放,一次又一次,直到您明確呼叫stop. 如果不設定迴圈,它會在完成後自動停止。

sound.setLoop(true);

更多屬性可用,但它們與3D化相關,並在相應教程中進行了說明。

常見錯誤

損壞的聲音緩衝區

最常見的錯誤是讓聲音緩衝區超出作用域(因此被破壞),而聲音仍在使用它。

sf::Sound loadSound(std::string filename)
{
    sf::SoundBuffer buffer; // this buffer is local to the function, it will be destroyed...
    buffer.loadFromFile(filename);
    return sf::Sound(buffer);
} // ... here

sf::Sound sound = loadSound("s.wav");
sound.play(); // ERROR: the sound's buffer no longer exists, the behavior is undefined

請記住,聲音只保留指向 您提供給它的聲音緩衝區的指標,它不包含自己的副本。您必須正確管理聲音緩衝區的生命週期,以便它們在被聲音使用時保持活動狀態。

太多的聲音

另一個錯誤來源是當您嘗試建立大量聲音時。SFML 內部有限制;它可能因作業系統而異,但不應超過 256。此限制是可以同時存在的sf::Sound例項數和sf::Music例項數之和。保持低於限制的一個好方法是在不再需要時銷燬(或回收)未使用的聲音。當然,這隻適用於您必須管理大量聲音和音樂的情況。

在播放時破壞音樂源

請記住,只要播放音樂,它就需要它的來源。當您的應用程式播放它時,磁碟上的音樂檔案可能不會被刪除或移動,但是當您從記憶體中的檔案或自定義輸入流中播放音樂時,事情會變得更加複雜:

// we start with a music file in memory (imagine that we extracted it from a zip archive)
std::vector<char> fileData = ...;

// we play it
sf::Music music;
music.openFromMemory(&fileData[0], fileData.size());
music.play();

// "ok, it seems that we don't need the source file any longer"
fileData.clear();

// ERROR: the music was still streaming the contents of fileData! The behavior is now undefined

sf::Music 不可複製

最後的“錯誤”是一個提醒:sf::Music類是不可複製的,所以你不會被允許這樣做:

sf::Music music;
sf::Music anotherMusic = music; // ERROR

void doSomething(sf::Music music)
{
    ...
}
sf::Music music;
doSomething(music); // ERROR (the function should take its argument by reference, not by value)

錄製音訊

錄製到聲音緩衝區

捕獲的音訊資料最常見的用途是將其儲存到聲音緩衝區 ( sf::SoundBuffer) 中,以便可以播放或儲存到檔案中。

這可以通過sf::SoundBufferRecorder類的非常簡單的介面來實現:

// first check if an input audio device is available on the system
if (!sf::SoundBufferRecorder::isAvailable())
{
    // error: audio capture is not available on this system
    ...
}

// create the recorder
sf::SoundBufferRecorder recorder;

// start the capture
recorder.start();

// wait...

// stop the capture
recorder.stop();

// retrieve the buffer that contains the captured audio data
const sf::SoundBuffer& buffer = recorder.getBuffer();

靜態函式檢查系統SoundBufferRecorder::isAvailable是否支援錄音。如果返回false,您將根本無法使用sf::SoundBufferRecorder該類。

和函式是不言自明的startstop捕獲在其自己的執行緒中執行,這意味著您可以在開始和停止之間做任何您想做的事情。捕獲結束後,錄製的音訊資料可在聲音緩衝區中使用,您可以使用該 getBuffer函式獲取該緩衝區。

使用記錄的資料,您可以:

  • 將其儲存到檔案

    buffer.saveToFile("my_record.ogg");
    
  • 直接播放

    sf::Sound sound(buffer);
    sound.play();
    
  • 訪問原始音訊資料並對其進行分析、轉換等。

    const sf::Int16* samples = buffer.getSamples();
    std::size_t count = buffer.getSampleCount();
    doSomething(samples, count);
    

如果您想在錄音機銷燬或重新啟動後使用捕獲的音訊資料,請不要忘記製作緩衝區的副本

選擇輸入裝置

如果您有多個聲音輸入裝置連線到您的計算機(例如麥克風、聲音介面(外部音效卡)或網路攝像頭麥克風),您可以指定用於錄製的裝置。聲音輸入裝置由其名稱標識。可通過靜態函式SoundBufferRecorder::getAvailableDevices()獲得包含所有連線裝置名稱的std::vector<std::string>。然後,您可以通過將所選裝置名稱傳遞給setDevice()方法,從列表中選擇一個裝置進行錄製。甚至可以即時更改裝置(即在錄製時)。

當前使用的裝置名稱可以通過呼叫獲取getDevice()。如果您不自己選擇裝置,則將使用預設裝置。它的名字可以通過靜態SoundBufferRecorder::getDefaultDevice()函式獲得。

下面是一個如何設定輸入裝置的小例子:

// get the available sound input device names
std::vector<std::string> availableDevices = sf::SoundRecorder::getAvailableDevices();

// choose a device
std::string inputDevice = availableDevices[0];

// create the recorder
sf::SoundBufferRecorder recorder;

// set the device
if (!recorder.setDevice(inputDevice))
{
    // error: device selection failed
    ...
}

// use recorder as usual

自定義錄製

如果您不希望將捕獲的資料儲存在聲音緩衝區中,您可以編寫自己的錄音機。這樣做將允許您在捕獲音訊資料時(幾乎)直接從錄音裝置進行處理。例如,通過這種方式,您可以通過網路傳輸捕獲的音訊,對其執行實時分析等。

要編寫自己的記錄器,您必須從sf::SoundRecorder抽象基類繼承。實際上, sf::SoundBufferRecorder只是這個類的一個內建特化。

您只有一個虛擬函式可以在派生類中覆蓋:onProcessSamples. 每次捕獲新的音訊樣本塊時都會呼叫它,因此這是您實現特定內容的地方。

預設情況下,音訊樣本每 100 毫秒提供給onProcessSamples方法一次。您可以使用該 setProcessingInterval方法更改間隔。例如,如果您想要實時處理記錄的資料,您可能想要使用更小的間隔。請注意,這只是一個提示,實際週期可能會有所不同,因此不要依賴它來實現精確的計時。

還有兩個額外的虛擬函式可以選擇覆蓋:onStartonStop. 它們分別在捕獲開始/停止時呼叫。它們對於初始化/清理任務很有用。

這是一個完整的派生類的骨架:

class MyRecorder : public sf::SoundRecorder
{
    virtual bool onStart() // optional
    {
        // initialize whatever has to be done before the capture starts
        ...

        // return true to start the capture, or false to cancel it
        return true;
    }

    virtual bool onProcessSamples(const sf::Int16* samples, std::size_t sampleCount)
    {
        // do something useful with the new chunk of samples
        ...

        // return true to continue the capture, or false to stop it
        return true;
    }

    virtual void onStop() // optional
    {
        // clean up whatever has to be done after the capture is finished
        ...
    }
}

isAvailable/start/stop函式在基類sf::SoundRecorder中定義 ,start因此在每個派生類中都被繼承。這意味著您可以使用與sf::SoundBufferRecorder類完全相同的任何記錄器類方法 。

if (!MyRecorder::isAvailable())
{
    // error...
}

MyRecorder recorder;
recorder.start();
...
recorder.stop();

執行緒問題

由於記錄是在一個單獨的執行緒中完成的,因此瞭解究竟發生了什麼以及在哪裡發生是很重要的。

onStart將由start函式直接呼叫,因此它在呼叫它的同一執行緒中執行。但是, onProcessSampleonStop始終將從 SFML 建立的內部記錄執行緒呼叫。

如果您的記錄器使用可能在呼叫者執行緒和記錄執行緒中同時訪問的資料您必須保護它(例如使用互斥鎖)以避免併發訪問,這可能導致未定義的行為——損壞的資料被記錄、崩潰等

如果您對執行緒不夠熟悉,可以參考相應的教程瞭解更多資訊。

自定義音訊流

音訊流?那是什麼?

音訊流類似於音樂(還記得sf::Music嗎?)。它具有幾乎相同的功能和行為。唯一的區別是音訊流不播放音訊檔案:而是播放您直接提供的自定義音訊源。換句話說,定義自己的音訊流不僅可以播放檔案,還可以播放:通過網路傳輸的聲音、程式生成的音樂、SFML 不支援的音訊格式等。

實際上,sf::Music該類只是一個特化的音訊流,它從檔案中獲取其音訊樣本。

由於我們正在討論流式傳輸,我們將處理無法完全載入到記憶體中的音訊資料,而是在播放時以小塊的形式載入。如果您的聲音可以完全載入並且可以放入記憶體,那麼音訊流對您沒有幫助:只需將音訊資料載入到 sf::SoundBuffer並使用常規sf::Sound播放它就可以了。

sf::SoundStream

為了定義自己的音訊流,您需要從sf::SoundStream抽象基類繼承。在派生類中有兩個虛擬函式需要重寫:onGetDataonSeek.

class MyAudioStream : public sf::SoundStream
{
    bool onGetData(Chunk &data) override ;

    void onSeek(sf::Time timeOffset) override ;
};

onGetData每當音訊樣本用完並需要更多音訊樣本時,基類都會呼叫它。data您必須通過填寫引數來提供新的音訊樣本:

bool MyAudioStream::onGetData(Chunk& data) override
{
    data.samples = /* put the pointer to the new audio samples */;
    data.sampleCount = /* put the number of audio samples available in the new chunk */;
    //將可用音訊取樣個數放入新區塊(trunk)
    return true;
}

當一切正常時, 您必須返回true,如果必須停止播放,要麼是因為發生了錯誤,要麼是因為沒有更多的音訊資料可以播放,那就返回false

SFML 在返回後立即製作音訊樣本的內部副本onGetData,因此如果您不想保留原始資料,則不必保留。

呼叫公共函式setPlayingOffset時呼叫onSeek函式。其目的是改變源資料中的當前播放位置。該引數是表示新位置的時間值,從聲音的開頭(不是從當前位置開始)。這個功能有時是不可能實現的。在這些情況下,將其留空,並告訴使用自定義類的使用者不支援更改播放位置。

void onSeek(sf::Time timeOffset) override
{	// sometimes it may use like this:
    // compute the corresponding sample index according to the sample rate and channel count
    m_currentSample = static_cast<std::size_t>(timeOffset.asSeconds() * getSampleRate() * getChannelCount());
}

// or like this..

void onSeek(sf::Time timeOffset) override
{
    // nothing happen 
    // or maybe some warmings
}

現在你的類幾乎可以開始工作了。現在唯一sf::SoundStream需要知道的是流的通道數取樣率,以便可以按預期播放。要讓基類知道這些引數,您必須在流類中知道它們後立即呼叫受保護(protected)的函式initialize (這很可能是在載入/初始化流時)。

// where this is done totally depends on how your stream class is designed
unsigned int channelCount = ...;
unsigned int sampleRate = ...;
initialize(channelCount, sampleRate);

執行緒問題

音訊流總是在一個單獨的執行緒中播放,因此瞭解究竟發生了什麼以及在哪裡發生是很重要的。

onSeeksetPlayingOffset函式直接呼叫,所以總是在呼叫者執行緒中執行。但是, onGetData只要正在播放流,就會在 SFML 建立的單獨執行緒中重複呼叫該函式。如果您的流使用可能在呼叫者執行緒和播放執行緒中同時訪問的資料您必須保護它(例如使用互斥鎖)以避免併發訪問,這可能導致未定義的行為——損壞的資料被播放、崩潰等

如果您對執行緒不夠熟悉,可以參考相應的教程瞭解更多資訊。

使用您的音訊流

現在您已經定義了自己的音訊流類,讓我們看看如何使用它。事實上,事情與教程中關於sf::Music的內容非常相似。您可以使用playstopsetPlayingOffsetpause功能控制播放。 您還可以播放聲音的屬性,例如音量或音高。您可以參考 API 文件或其他音訊教程以獲取更多詳細資訊。

一個簡單的例子

這是一個非常簡單的自定義音訊流類示例,它播放聲音緩衝區的資料。這樣的類可能看起來完全沒用,但這裡的重點是關注資料是如何被類流式傳輸的,不管它來自哪裡。

#include <SFML/Audio.hpp>
#include <vector>

// custom audio stream that plays a loaded buffer
class MyStream : public sf::SoundStream
{
public:

    void load(const sf::SoundBuffer& buffer)
    {
        // extract the audio samples from the sound buffer to our own container
        m_samples.assign(buffer.getSamples(), buffer.getSamples() + buffer.getSampleCount());

        // reset the current playing position 
        m_currentSample = 0;

        // initialize the base class
        initialize(buffer.getChannelCount(), buffer.getSampleRate());
    }

private:

    bool onGetData(Chunk& data) override
    {
        // number of samples to stream every time the function is called;
        // in a more robust implementation, it should be a fixed
        // amount of time rather than an arbitrary number of samples
        const int samplesToStream = 50000;

        // set the pointer to the next audio samples to be played
        data.samples = &m_samples[m_currentSample];

        // have we reached the end of the sound?
        if (m_currentSample + samplesToStream <= m_samples.size())
        {
            // end not reached: stream the samples and continue
            data.sampleCount = samplesToStream;
            m_currentSample += samplesToStream;
            return true;
        }
        else
        {
            // end of stream reached: stream the remaining samples and stop playback
            data.sampleCount = m_samples.size() - m_currentSample;
            m_currentSample = m_samples.size();
            return false;
        }
    }

    void onSeek(sf::Time timeOffset) override
    {
        // compute the corresponding sample index according to the sample rate and channel count
        m_currentSample = static_cast<std::size_t>(timeOffset.asSeconds() * getSampleRate() * getChannelCount());
    }

    std::vector<sf::Int16> m_samples;
    std::size_t m_currentSample;
};

int main()
{
    // load an audio buffer from a sound file
    sf::SoundBuffer buffer;
    buffer.loadFromFile("sound.wav");

    // initialize and play our custom stream
    MyStream stream;
    stream.load(buffer);
    stream.play();

    // let it play until it is finished
    while (stream.getStatus() == MyStream::Playing)
        sf::sleep(sf::seconds(0.1f));

    return 0;
}

空間化:3D 聲音

介紹

預設情況下,每個揚聲器都會以最大音量播放聲音和音樂;它們沒有空間化

如果聲音是由螢幕右側的實體發出的,您可能希望從右側的揚聲器中聽到它。如果在播放器後面播放音樂,您可能希望從杜比 5.1 音響系統的後置揚聲器聽到它。

如何做到這一點?

空間化的聲音是單聲道的

一個聲音只有當它有一個單一的通道時才能被空間化,即如果它是一個單聲道的聲音。
對於具有更多通道的聲音,空間化被禁用,因為它們已經明確決定如何使用揚聲器。記住這一點非常重要。

偵聽器

音訊環境中的所有聲音和音樂都將被偵聽器(listener)聽到。揚聲器的輸出取決於偵聽器聽到的內容。

定義偵聽器屬性的類是sf::Listener. 由於偵聽器在環境中是唯一的,因此此類僅包含靜態函式,並不打算例項化。

首先,您可以設定偵聽器在場景中的位置:

sf::Listener::setPosition(10.f, 0.f, 5.f);

如果你有一個 2D 世界,你可以在任何地方使用相同的 Y 值,通常是 0。

除了它的位置,您還可以定義偵聽器的方向:

sf::Listener::setDirection(1.f, 0.f, 0.f);

在這裡,偵聽器沿 +X 軸定向。這意味著,例如,在 (15, 0, 5) 處發出的聲音將從右揚聲器聽到。

偵聽器的“向上”向量預設設定為 (0, 1, 0),換句話說,偵聽器的頭頂指向 +Y。如果需要,您可以更改“向上”向量。但很少有必要。

sf::Listener::setUpVector(1.f, 1.f, 0.f);

這對應於偵聽器將頭向右傾斜 (+X)。

最後,偵聽器可以調整場景的全域性音量:

sf::Listener::setGlobalVolume(50.f);

音量的值在 [0 .. 100] 範圍內,因此將其設定為 50 會將其減少到原始音量的一半。

當然,所有這些屬性都可以通過相應的get函式來讀取。

音訊源

SFML 提供的每個音訊源(聲音、音樂、流)都為空間化定義了相同的屬性。

主要屬性是音訊源的位置。

sound.setPosition(2.f, 0.f, -5.f);

預設情況下,此位置是絕對的,但如果需要,它可以相對於偵聽器。

sound.setRelativeToListener(true);

這對於偵聽器自身發出的聲音(如槍聲或腳步聲)很有用。如果將音訊源的位置設定為 (0, 0, 0),它還具有禁用空間化的有趣副作用。在各種情況下都可能需要非空間化的聲音:GUI 聲音(點選)、環境音樂等。

您還可以根據音訊源與聽眾的距離設定衰減因子。

sound.setMinDistance(5.f);
sound.setAttenuation(10.f);

最小距離(minimum 是以最大音量聽到聲音的距離 。例如,較大的聲音(例如爆炸聲)應該具有較高的最小距離,以確保從很遠的地方就能聽到它們。請注意,最小距離為 0(聲音在偵聽器的頭部內部!)會導致不正確的空間化並導致聲音不衰減。0 是一個無效值,永遠不要使用它。

衰減(attenuation 是一個乘法因子 。衰減越大,當聲音遠離偵聽器時聽到的越少。要獲得不衰減的聲音,您可以使用 0。另一方面,使用 100 之類的值會高度衰減聲音,這意味著只有在非常靠近偵聽器時才能聽到聲音。

這是確切的衰減公式,以防您需要準確的值:

MinDistance   is the sound's minimum distance, set with setMinDistance
Attenuation   is the sound's attenuation, set with setAttenuation
Distance      is the distance between the sound and the listener
Volume factor is the calculated factor, in range [0 .. 1], that will be applied to the sound's volume

Volume factor = MinDistance / (MinDistance + Attenuation * (max(Distance, MinDistance) - MinDistance))
  • MinDistance 是聲音的最小距離,用setMinDistance設定
  • Attenuation 是聲音的衰減,用SetAttention設定
  • Distance 是聲音與聽者之間的距離 即主變數
  • Volume factor 是計算出的音量係數,範圍為[0..1],將應用於聲音的音量大小

\(\Large Volume factor = \frac{MinDistance}{ MinDistance + Attenuation(\max{(Distance, MinDistance)} - MinDistance)}\)

可以看出 這是一個從1開始衰減的反比例函式

網路。。?溜了

一般自己用SFML也很少會用到網路 而是選擇用其他更好的庫 所以就不翻網路那塊了

有興趣可以直接去官網找教程~

下次更新實戰了 bb