juce实战(2):音频波形绘图

参考链接:tutorial_audio_thumbnail,官方的教程已经写的很好了,这里仅用来记录学习过程和思考

做音频应用当然少不了显示音频波形,所以这个tutorial着重学习一下。

1. 重要对象

1
2
3
4
5
6
7
8
9
10
11
12
juce::TextButton openButton;
juce::TextButton playButton;
juce::TextButton stopButton;

std::unique_ptr<juce::FileChooser> chooser;

juce::AudioFormatManager formatManager; // [3] 管理音频格式
std::unique_ptr<juce::AudioFormatReaderSource> readerSource;
juce::AudioTransportSource transportSource; // 音频传输源,允许对音频进行播放、暂停、停止等操作
TransportState state;//控制播放暂停
juce::AudioThumbnailCache thumbnailCache; // [1] 用于管理多个AudioThumbnail对象
juce::AudioThumbnail thumbnail; // [2] 快速绘制音频波形的缩放视图

重要的类有:AudioThumbnailCache与AudioThumbnail。
AudioThumbnailCache:不仅管理了音频波形视图的对象,还在内存中保留了一组低分辨率的预览,避免过频繁地重复扫描音频来生成视图

2. 初始化

1
2
3
4
MainContentComponent()
: state (Stopped),
thumbnailCache (5), // [4]
thumbnail (512, formatManager, thumbnailCache) // [5]

[4]:“5”是要使用的缩略图数量,AudioThumbnailCache 对象必须使用要存储的缩略图数量来构造
[5]:“512”是构造缩略图的采样点数,能控制缩略图的分辨率

1
2
3
formatManager.registerBasicFormats();
transportSource.addChangeListener (this);
thumbnail.addChangeListener (this); // [6]

在这里给视图对象添加一个监听,当AudioThumbnail发生更新时,可以重绘波形

3.响应更改

1
2
3
4
5
void changeListenerCallback (juce::ChangeBroadcaster* source) override
{
if (source == &transportSource) transportSourceChanged();
if (source == &thumbnail) thumbnailChanged();
}


注意这里,AudioTransportSource 与 AudioThumbnail都是changeListenerCallback的子类,此回调函数根据传入不同的source来执行transportSourceChanged()或thumbnailChanged()

1
2
3
4
5
6
7
8
9
void thumbnailChanged()
{
repaint();
}

void transportSourceChanged()
{
changeState (transportSource.isPlaying() ? Playing : Stopped);
}

当 AudioThumbnail被改变,调用repaint;
当 音频播放的状态被改变,根据音频是否在播放,调用changeState改变状态;

4.执行绘图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void paint (juce::Graphics& g) override
{
juce::Rectangle<int> thumbnailBounds (10, 100, getWidth() - 20, getHeight() - 120);

if (thumbnail.getNumChannels() == 0)
paintIfNoFileLoaded (g, thumbnailBounds);
else
paintIfFileLoaded (g, thumbnailBounds);
}

void paintIfNoFileLoaded (juce::Graphics& g, const juce::Rectangle<int>& thumbnailBounds)
{
g.setColour (juce::Colours::darkgrey);
g.fillRect (thumbnailBounds);
g.setColour (juce::Colours::white);
g.drawFittedText ("No File Loaded", thumbnailBounds, juce::Justification::centred, 1);
}

void paintIfFileLoaded (juce::Graphics& g, const juce::Rectangle<int>& thumbnailBounds)
{
g.setColour (juce::Colours::white);
g.fillRect (thumbnailBounds);

g.setColour (juce::Colours::red); // [8]

thumbnail.drawChannels (g, // [9]
thumbnailBounds,
0.0, // start time
thumbnail.getTotalLength(), // end time
1.0f); // vertical zoom
}

这段应该挺好懂的,还可以看看drawChannels方法:https://docs.juce.com/master/classAudioThumbnail.html#a8fc354b03e88e66771ef3b9647fdd9fa

5.分离组件

如果想要增加功能(例如给音频图添加一个游标,下文会提到做法),最好要将主控件和子控件分开,方便管理;如果用qt、java等写过图形化界面的话,应该很清楚:要将各个模块还有功能尽量分开,增强代码的易读性,减少程序的冗余性和耦合性(好像讲的有点太过术语化了hhh);

(这里和官方教程不同,官方教程先添加了游标,建议以官方教程为主,如果要接着往下走,建议根据官方代码的AudioThumbnailTutorial_01.h进行修改,仅仅提供一种思路!)

因为还没有做新功能,则先将主组件与音频绘图组件分开,即仿照MainContentComponent新建一个SimpleThumbnailComponent类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class SimpleThumbnailComponent : public juce::Component,
private juce::ChangeListener
{
public:
SimpleThumbnailComponent(int sourceSamplesPerThumbnailSample,
juce::AudioFormatManager& formatManager,
juce::AudioThumbnailCache& cache)
: thumbnail(sourceSamplesPerThumbnailSample, formatManager, cache)
{
thumbnail.addChangeListener(this);
}

private:

juce::AudioThumbnail thumbnail;

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SimpleThumbnailComponent)

};

接着把关于Thumbnail与paint相关的函数与变量从MainContentComponent搬到SimpleThumbnailComponent。

运行一下:
为什么音频波形图位置和之前的不同?因为没有分离好,例如:子组件的paint函数中有
juce::Rectangle thumbnailBounds(10, 100, getWidth() - 20, getHeight() - 120);官方代码中,主控件中的resize函数中调用thumbnailBounds,设置了波形图的位置,而初步分离的代码直接将thumbnailBounds复制到resize函数中,没有对子控件的其他函数进行修改,可以对比一下官方代码和初步分离的代码,也可以调整一下thumbnailBounds的值,观察波形图在界面的移动。

总之…这里只是提供分离控件的思想,故省略这部分的详细思考过程(懒。

添加时间标

有了波形,当然也要有edison同款时间标

转到AudioThumbnailTutorial_04.h,绘制时间标记的代码在SimplePositionOverlay类中。
首先先做一个定时器,按照指定的时间间隔重绘这一条时间标记线,故SimplePositionOverlay要继承juce的Timer类

1
2
class SimplePositionOverlay : public juce::Component,
private juce::Timer

设置定时器的时间间隔为40ms,

1
2
3
4
5
6
public:
SimplePositionOverlay (const juce::AudioTransportSource& transportSourceToUse)
: transportSource (transportSourceToUse)
{
startTimer (40);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
void paint (juce::Graphics& g) override
{
auto duration = (float) transportSource.getLengthInSeconds();

if (duration > 0.0)
{
auto audioPosition = (float) transportSource.getCurrentPosition();
auto drawPosition = (audioPosition / duration) * (float) getWidth();

g.setColour (juce::Colours::green);
g.drawLine (drawPosition, 0.0f, drawPosition, (float) getHeight(), 2.0f);
}
}

paint函数中,audioPosition变量获取音频播放的时间位置(即音频播放到多少分多少秒);drawPosition找到绘制的位置

1
2
3
4
5
6
7
8
9
10
11
12
void mouseDown(const juce::MouseEvent& event) override
{
auto duration = transportSource.getLengthInSeconds();

if (duration > 0.0)
{
auto clickPosition = event.position.x;
auto audioPosition = (clickPosition / (float)getWidth()) * duration;

transportSource.setPosition(audioPosition);
}
}

mouseDown函数中,获取鼠标x轴位置的值,并用这个值通过setPosition设置音频播放的当前时间。

效果:


juce实战(2):音频波形绘图
https://skylarshadow.github.io/2023/11/29/juce-draw-audio-waveforms/
作者
SKYlarS
发布于
2023年11月29日
许可协议