官方文档:https://juce.com/learn/tutorials/
1、构建音频播放器
本教程介绍如何打开和播放声音文件。其中包括一些在 JUCE 中处理声音文件的重要类。
级别:中级
平台: Windows、macOS、Linux
类: AudioFormatManager、AudioFormatReader、AudioFormatReaderSource、AudioTransportSource、FileChooser、ChangeListener、File、FileChooser
1.1、入门
在此处下载本教程的演示项目:PIP | ZIP。解压缩项目并在 Projucer 中打开第一个头文件。
1.2、演示项目
演示项目提供了一个三按钮界面,用于控制声音文件的播放。这三个按钮分别是:
- 一个按钮向用户显示文件选择器,供他们选择声音文件。
- 播放声音的按钮。
- 一个按钮来停止声音。
界面如下图所示:
1.3、有用的课程
1.3.1、AudioSource 类
虽然我们可以在音频应用程序模板的getNextAudioBlock()
函数中逐个生成音频样本,但有一些内置工具可用于生成和处理音频。这些工具允许我们将高级构建块链接在一起以形成强大的音频应用程序,而无需在我们的应用程序代码中处理每个音频样本(JUCE 代表我们执行此操作)。这些构建块基于AudioSource类。事实上,如果您已经遵循了基于AudioAppComponent类的任何教程(例如,教程:构建白噪声生成器),那么您已经在使用AudioSource类了。AudioAppComponent类本身继承自AudioSource类,重要的是,它包含一个AudioSourcePlayer对象,该对象在AudioAppComponent和音频硬件设备之间传输音频。我们可以直接在getNextAudioBlock()
函数中生成音频样本,但我们可以将多个AudioSource对象链接在一起以形成一系列流程。我们在本教程中使用此功能。
1.3.2、音频格式
JUCE 提供了许多用于读取和写入多种格式的声音文件的工具。在本教程中,我们将使用其中的几个,特别是使用以下类:
- AudioFormatManager:此类包含音频格式列表(例如 WAV、AIFF、Ogg Vorbis 等),并可以创建合适的对象来读取这些格式的音频数据。
- AudioFormatReader:此类处理音频文件的低级文件读取操作,并允许我们以一致的格式(通常指
float
值数组)读取音频。当AudioFormatManager对象被要求打开特定文件时,它会创建此类的实例。 - AudioFormatReaderSource :这是AudioSource类的子类。它可以从AudioFormatReader对象读取音频数据并通过其
getNextAudioBlock()
函数渲染音频。 - AudioTransportSource :该类是AudioSource类的另一个子类。它可以控制AudioFormatReaderSource对象的播放。此控制包括启动和停止AudioFormatReaderSource对象的播放。它还可以执行采样率转换,并且可以根据需要提前缓冲音频。
1.4、整合
现在,我们将把这些类与合适的用户界面类组合在一起,以制作我们的声音文件播放应用程序。此时,考虑播放音频文件的各个阶段(或传输状态)很有用。加载音频文件后,我们可以考虑以下四种可能的状态:
- Stopped:音频播放已停止并准备开始。
- Starting:音频播放尚未开始,但已被告知开始。
- Playing:音频正在播放。
- Stopping:音频正在播放,但播放已被告知停止,此后它将返回停止状态。
为了表示这些状态,我们在MainContentComponent
类中创建一个enum
:
enum {
Stopped, // 已停止
Starting, // 正在开始
Playing, // 播放中
Stopping // 停止中
};
1.4.1、初始化接口
在我们MainContentComponent
类的构造函数中,我们配置了三个按钮:
MainComponent::MainComponent():
state(TransportState::Stopped)
{
setSize (800, 600);
// Some platforms require permissions to open input channels so request that here
if (juce::RuntimePermissions::isRequired (juce::RuntimePermissions::recordAudio)
&& ! juce::RuntimePermissions::isGranted (juce::RuntimePermissions::recordAudio)) {
juce::RuntimePermissions::request (juce::RuntimePermissions::recordAudio,
[&] (bool granted) { setAudioChannels (granted ? 2 : 0, 2); });
}
else {
// Specify the number of input and output channels that we want to open
setAudioChannels (2, 2);
}
addAndMakeVisible(&openButton);
openButton.setButtonText("Open...");
openButton.onClick = [this]() {openButtonClicked(); };
addAndMakeVisible(&playButton);
playButton.setButtonText("Play");
playButton.onClick = [this]() {playButtonClicked(); };
playButton.setColour(juce::TextButton::buttonColourId, juce::Colours::green);
playButton.setEnabled(false);
addAndMakeVisible(&stopButton);
stopButton.setButtonText("Stop");
stopButton.onClick = [this]() {stopButtonClicked(); };
stopButton.setColour(juce::TextButton::buttonColourId, juce::Colours::red);
stopButton.setEnabled(false);
}
特别注意,我们最初禁用了播放和停止按钮。加载有效文件后,播放按钮就会启用。我们可以在这里看到,我们为这三个按钮的Button::onClick辅助对象分配了一个 lambda 函数(请参阅教程:监听器和广播器)。我们还在构造函数的初始化列表中初始化了传输状态。
1.4.2、其他初始化
除了三个TextButton对象之外,我们的MainContentComponent
类还有另外四个成员:
juce::AudioFormatManager formatManager; // 音频格式管理
std::unique_ptr<juce::AudioFormatReaderSource> readerSource;
juce::AudioTransportSource transportSource;
TransportState state;
这里我们看到前面提到的AudioFormatManager,AudioFormatReaderSource和AudioTransportSource类。
在MainContentComponent
构造函数中,我们需要初始化AudioFormatManager对象来注册标准格式列表:
// 注册标准格式列表
formatManager.registerBasicFormats();
至少这将使AudioFormatManager对象能够为 WAV 和 AIFF 格式创建读取器。其他格式可能可用,具体取决于平台和 Projucer 项目中模块中启用的选项,juce_audio_formats
如以下屏幕截图所示:
在MainContentComponent
构造函数中,我们还将我们的MainContentComponent
对象作为侦听器添加到AudioTransportSource对象中,以便我们可以响应其状态的变化(例如,当它停止时):
// 继承 ChangeListener 类
class MainComponent : public juce::AudioAppComponent, public juce::ChangeListener
// 在构造函数中添加如下代码
transportSource.addChangeListener(this);
注意:在这种情况下,函数addChangeListener()
名称是这样的,而不是像JUCE 中的许多其他监听器类那样简单地使用addListener()
。
1.4.3、响应 AudioTransportSource 更改
当传输中的变化被报告时,changeListenerCallback()
将被调用。这将在消息线程上异步调用:
void MainComponent::changeListenerCallback(juce::ChangeBroadcaster* source) {
if (source == &transportSource) {
if (transportSource.isPlaying()) {
changeState(TransportState::Playing);
} else {
changeState(TransportState::Stopped);
}
}
}
您可以看到这只是调用一个changeState()
成员函数。
1.4.4、改变状态
传输状态的改变被局限在这个单一的changeState()
函数中。这有助于将此功能的所有逻辑集中在一个地方。此函数更新state
成员并触发在新状态下需要对其他对象进行的任何更改。
void MainComponent::changeState(TransportState newState) {
if (state != newState) {
state = newState;
switch (state) {
case Stopped:
stopButton.setEnabled(false);
playButton.setEnabled(true);
transportSource.setPosition(0.0);
break;
case Starting:
playButton.setEnabled(false);
transportSource.start();
case Playing:
stopButton.setEnabled(true);
break;
case Stopping:
transportSource.stop();
break;
}
}
}
- 当传输返回到停止状态时,它会禁用“停止”按钮,启用“播放”按钮,并将传输位置重置回文件的开头。
- 用户点击播放按钮会触发“开始”状态,这会告诉AudioTransportSource对象开始播放。此时我们也禁用了播放按钮。
- AudioTransportSource对象通过函数报告变化后,会触发播放状态。这里我们启用了停止按钮。
changeListenerCallback()
- 停止状态是由用户点击停止按钮触发的,因此我们告诉AudioTransportSource对象停止。
1.4.5、处理音频
此演示项目中的音频处理非常简单:我们只需将通过AudioAppComponent类传递的AudioSourceChannelInfo结构传递给AudioTransportSource对象即可:
void MainComponent::getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill)
{
if (readerSource.get() == nullptr) {
bufferToFill.clearActiveBufferRegion();
return;
}
transportSource.getNextAudioBlock(bufferToFill);
}
请注意,我们首先检查是否存在有效的AudioFormatReaderSource对象,如果没有,则简单地将输出归零(使用方便的AudioSourceChannelInfo ::clearActiveBufferRegion()函数)。AudioFormatReaderSource成员存储在 std::unique_ptr 对象中,因为我们需要根据用户的操作动态创建这些对象。它还允许我们检查是否存在nullptr
无效对象。
我们还需要记住将prepareToPlay()
回调传递给我们正在使用的任何其他AudioSource对象:
void MainComponent::prepareToPlay (int samplesPerBlockExpected, double sampleRate)
{
// This function will be called when the audio device is started, or when
// its settings (i.e. sample rate, block size, etc) are changed.
// You can use this function to initialise any resources you might need,
// but be careful - it will be called on the audio thread, not the GUI thread.
// For more details, see the help for AudioProcessor::prepareToPlay()
transportSource.prepareToPlay(samplesPerBlockExpected, sampleRate);
}
还有releaseResources()
回调:
void MainComponent::releaseResources()
{
// This will be called when the audio device stops, or when it is being
// restarted due to a setting change.
// For more details, see the help for AudioProcessor::releaseResources()
transportSource.releaseResources();
}
1.4.6、打开文件
要打开文件,我们会弹出一个FileChooser对象来响应单击“打开...”按钮:
void MainComponent::openButtonClicked() {
// 创建带有简短消息的FileChooser对象并允许用户选择.wav文件
chooser = std::make_unique<juce::FileChooser>("Select a Wave file to play...",
juce::File{},
"*.wav");
auto chooserFlags = juce::FileBrowserComponent::openMode
| juce::FileBrowserComponent::canSelectFiles;
// 弹出FileChooser对象
chooser->launchAsync(chooserFlags, [this](const juce::FileChooser& fc) {
auto file = fc.getResult();
// 如果文件不为空(用户实际选择了一个文件)
if (file != juce::File{}) {
// 尝试为特定文件创建读取器,如果失败返回nullptr(比如:该文件不是AudioFormatManager对象可以处理的音频格式)
auto* reader = formatManager.createReaderFor(file);
if (reader != nullptr) {
// 使用reader创建一个新的AudioFormatReaderSource对象,第二个参数为true,表示我们希望AudioFormatReaderSource对象管理AudioFormatReader对象并在不再需要时将其删除。我们将AudioFormatReaderSource对象存储在临时的std::unique_ptr对象中,以避免在后续打开文件的命令中过早删除之前分配的AudioFormatReaderSource。
auto newSource = std::make_unique<juce::AudioFormatReaderSource>(reader, true);
// 将AudioFormatReaderSource对象连接到我们getNextAudioBlock()函数中使用的AudioTransportSource对象。如果文件的采样率与硬件采样率不匹配,我们会将其作为第四个参数传入,该参数是从AudioFormatReader获得的。AudioTransportSource将处理任何必要的采样率转换。
transportSource.setSource(newSource.get(), 0, nullptr, reader->sampleRate);
// 启用播放按钮,以便用户可以点击它
playButton.setEnabled(true);
// 由于AudioTransportSource现在应该使用我们新分配的AudioFormatReaderSource对象,因此我们可以安全地将AudioFormatReaderSource对象存储在我们的成员中。为此,我们必须使用std::unique_ptr::release()从局部变量newSource转移所有权
readerSource.reset(newSource.release());
}
}
});
}
注意:将新分配的AudioFormatReaderSource对象存储在临时 std::unique_ptr 对象中还有一个额外的好处,那就是可以避免异常。在函数调用AudioTransportSource::setSource()期间可能会抛出异常,在这种情况下,std::unique_ptr 对象将删除不再需要的AudioFormatReaderSource对象。如果此时使用原始指针来存储AudioFormatReaderSource对象,那么可能会发生内存泄漏,因为如果抛出异常,指针将处于悬空状态。
1.4.7、播放和停止文件
由于我们已经设置了实际播放文件的代码,因此我们只需使用适当的参数调用我们的changeState()
函数即可播放文件。单击“播放”按钮时,我们将执行以下操作:
void MainComponent::playButtonClicked() {
changeState(TransportState::Starting);
}
当单击“停止”按钮时,停止文件同样简单:
练习:创建FileChooser对象时更改第三个 ( filePatternsAllowed
) 参数,以允许应用程序也加载 AIFF 文件。文件模式可以用分号分隔,因此这应该允许此格式的两个常见文件扩展名"*.wav;*.aif;*.aiff"
。
1.4.8、添加暂停功能
现在,我们将逐步介绍如何为应用程序添加暂停功能。在这里,我们将使播放按钮在文件播放时变为暂停按钮(而不是仅仅禁用它)。我们还将使停止按钮在声音文件暂停时变为返回零按钮。
首先,我们需要向TransportState
枚举中添加两个状态Pausing和Paused:
enum TransportState {
Stopped,
Starting,
Playing,
Pausing,
Paused,
Stopping
};
我们的changeState()
函数需要处理这两个新状态,并且其他状态的代码也需要更新:
void MainComponent::changeState(TransportState newState) {
if (state != newState) {
state = newState;
switch (state) {
case Stopped:
playButton.setButtonText("Play");
stopButton.setButtonText("Stop");
stopButton.setEnabled(false);
// playButton.setEnabled(true);
transportSource.setPosition(0.0);
break;
case Starting:
// playButton.setEnabled(false);
transportSource.start();
case Playing:
playButton.setButtonText("Pause");
stopButton.setButtonText("Stop");
stopButton.setEnabled(true);
break;
case Pausing:
transportSource.stop();
break;
case Paused:
playButton.setButtonText("Resume");
stopButton.setButtonText("Return to Zero");
break;
case Stopping:
transportSource.stop();
break;
}
}
}
我们适当地启用和禁用按钮,并在每个状态下正确更新按钮文本。
请注意,当要求在暂停状态下暂停时,我们实际上会停止传输。在changeListenerCallback()
函数中,我们需要根据是否发出暂停或停止请求来更改逻辑以移动到正确的状态:
void MainComponent::changeListenerCallback(juce::ChangeBroadcaster* source) {
if (source == &transportSource) {
if (transportSource.isPlaying()) {
changeState(TransportState::Playing);
} else if ((state == Stopping) || (state == Playing)) {
changeState(TransportState::Stopped);
} else if (state == Pausing) {
changeState(Paused);
}
}
}
我们需要更改单击播放按钮时的代码:
void MainComponent::playButtonClicked() {
if ((state == Stopped) || (state == Paused))
changeState(Starting);
else if (state == Playing)
changeState(Pausing);
}
当单击“停止”按钮时:
void MainComponent::stopButtonClicked() {
if (state == Paused)
changeState(Stopped);
else
changeState(Stopping);
}
就是这样:您现在应该能够构建并运行该应用程序。
练习:将Label对象添加到显示AudioTransportSource对象当前时间位置的界面。您可以使用AudioTransportSource::getCurrentPosition()函数获取此位置。您还需要让该MainContentComponent
类继承自Timer类,并在timerCallback()
函数中执行定期更新以更新标签。您甚至可以使用RelativeTime类将原始时间(以秒为单位)转换为更有用的格式(以分钟、秒和毫秒为单位)。
1
标签:AudioFormatReaderSource,juce,对象,JUCE,音频,transportSource,按钮 From: https://www.cnblogs.com/aoe1231/p/18450352