SMFL 教程&个人笔记(2)
本文大部分来自官方教程的Google翻译 但是加了一点点个人的理解和其他相关知识
转载请注明 原文链接 :https://www.cnblogs.com/Multya/p/16317401.html
官方教程: https://www.sfml-dev.org/tutorials/2.5/
播放声音和音乐
声音还是音乐?
SFML 提供了两个用于播放音频的类:sf::Sound
和sf::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 声道定义立体声,等等。第二个附加属性(第四个参数)是采样率;它定义了每秒必须播放多少个样本才能重建原始声音。
现在已经加载了音频数据,我们可以用一个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
该类。
和函数是不言自明的start
。stop
捕获在其自己的线程中运行,这意味着您可以在开始和停止之间做任何您想做的事情。捕获结束后,录制的音频数据可在声音缓冲区中使用,您可以使用该 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
方法更改间隔。例如,如果您想要实时处理记录的数据,您可能想要使用更小的间隔。请注意,这只是一个提示,实际周期可能会有所不同,因此不要依赖它来实现精确的计时。
还有两个额外的虚函数可以选择覆盖:onStart
和onStop
. 它们分别在捕获开始/停止时调用。它们对于初始化/清理任务很有用。
这是一个完整的派生类的骨架:
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
函数直接调用,因此它在调用它的同一线程中执行。但是, onProcessSample
和onStop
始终将从 SFML 创建的内部记录线程调用。
如果您的记录器使用可能在调用者线程和记录线程中同时访问的数据,您必须保护它(例如使用互斥锁)以避免并发访问,这可能导致未定义的行为——损坏的数据被记录、崩溃等
如果您对线程不够熟悉,可以参考相应的教程了解更多信息。
持续更新中。。。