SFML 教程&个人笔记(2)

本文大部分来自官方教程的Google翻译 但是加了一点点个人的理解和其他相关知识

转载请注明 原文链接 :https://www.cnblogs.com/Multya/p/16317401.html

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

本文有什么

这是SFML官方教程的翻译

涉及的模块有

  • Audio module 音乐模块

  • 并没有网络模块 作者太懒了

好 开始吧

播放声音和音乐

声音还是音乐?

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也很少会用到网络 而是选择用其他更好的库 所以就不翻网络那块了

有兴趣可以直接去官网找教程~

posted @ 2022-05-29 16:25  S47ar_oT  阅读(616)  评论(0编辑  收藏  举报