# week 4
contributed by < `robertlin0401` >
###### tags: `數位音樂訊號分析`
> [GitHub](https://github.com/robertlin0401/Digital-Music-Signal-week4)
---
## 課堂範例
* [程式範例](https://github.com/datuiji/SineWaveSynthesizer/commit/1df1356a7914d647a6b9718b992c084bc509e009)
* [說明文件](https://hackmd.io/@datuiji/JUCE_Framwork_GUI)
---
## 修改 slider
### 簡述
* 此階段的目標
1. 將 slider 改成旋轉式(Rotary),以便後續客製化成旋鈕的樣式
2. 引入 FlexBox 協助進行排版
* 原本的介面如下

### 實作
* 定義兩個 class:`MySlider` 與 `SliderContainer`
* 在 `MySlider` 中主要會設定個別的 slider 其本身的格式,以下為此 class 的 constructor
```cpp
MySlider::MySlider(MyAudioProcessor &p, juce::String labelName)
: audioProcessor(p)
{
setLookAndFeel(&lnf);
slider.setSliderStyle(juce::Slider::SliderStyle::Rotary);
slider.setTextBoxStyle(
juce::Slider::TextBoxBelow, true,
slider.getTextBoxWidth(),
slider.getTextBoxHeight()
);
label.setText(labelName, juce::dontSendNotification);
label.setJustificationType(juce::Justification::centred);
attachment.reset(new juce::AudioProcessorValueTreeState::SliderAttachment(
audioProcessor.tree, juce::String{labelName}.toLowerCase(), slider
));
addAndMakeVisible(slider);
addAndMakeVisible(label);
}
```
* `setLookAndFeel(&lnf)` 用於設定自定義的樣式
* 為此,在 `Slider` 的 destructor 需要執行以下操作
```cpp
MySlider::~MySlider()
{
setLookAndFeel(nullptr);
}
```
* 其中,`lnf` 變數為 `MyLookAndFeel` class 的物件,自定義的樣式將在此 class 中實作
* `juce::Slider::SliderStyle::Rotary` 表示使用旋轉式的 slider,其操作方式為沿著圓周拖動,類似於轉動真實旋鈕的動作
> [SliderStyle docs](https://docs.juce.com/master/classSlider.html#af1caee82552143dd9ff0fc9f0cdc0888)
* `juce::Slider::TextBoxBelow` 表示將顯示數值的文字方塊放置在 slider 下方
> [TextEntryBoxPosition docs](https://docs.juce.com/master/classSlider.html#ab6d7dff67151c029b9cb53fc40b4412f)
* 在 `SliderContainer` 中,透過實作 `resize()` 函式來調整多個 sliders 之間的排版方式,其中,使用了 FlexBox 來輔助排版
```cpp
void SliderContainer::resized()
{
juce::FlexBox flexBox;
for (auto &slider : sliders) {
juce::FlexItem item {*slider};
flexBox.items.add(item.withFlex(1.0));
}
flexBox.performLayout(getLocalBounds());
for (auto &slider : sliders) {
slider->setBounds(slider->getBounds().reduced(5));
}
}
```
:::warning
:notebook_with_decorative_cover:此處的 for 迴圈中,出現了我沒看過的語法,以下筆記
1. `slider : sliders`
* 此為自 C++11 起所提供的迭代方法,稱為 range-based for loop,此處用於遍歷 vector `sliders`,且每次迭代可用變數名稱 `slider` 來存取該次的迭代對象
> [cppreference 說明](https://en.cppreference.com/w/cpp/language/range-for)
2. `auto &`
* 用於表示變數的存取方法,此處表示使用 access by reference 的方式來存取 `slider`
* 另外還有 `auto` 與 `auto &&` 的用法,分別表示使用 access by value 與 access by forwarding reference 的存取方式
> [stackoverflow 說明](https://stackoverflow.com/questions/29859796/c-auto-vs-auto#answer-29860056)
> [cppreference 範例](https://en.cppreference.com/w/cpp/language/range-for#Example)
:::
### 成果

* 由於尚未實作 `MyLookAndFeel` 的部分,因此 slider 的樣式為預設的樣式
## 自定義 slider 樣式
### 簡述
* 此階段的目標:將 slider 改成自定義的樣式
* 實作方法:由於是要設計旋轉式 slider,因此僅需實作 `MyLookAndFeel` class 中的 `drawRotarySlider()` 函式即可
* 參考樣式

### 實作
```cpp
void drawRotarySlider(juce::Graphics &g, int x, int y, int width, int height, float sliderPos,
const float rotaryStartAngle, const float rotaryEndAngle, juce::Slider& slider) override
{
auto bound = juce::Rectangle<int>(x, y, width, height).toFloat().reduced(10);
auto radius = juce::jmin(bound.getWidth(), bound.getHeight()) / 2.0f;
auto centreX = bound.getCentreX();
auto centreY = bound.getCentreY();
auto angle = rotaryStartAngle + sliderPos * (rotaryEndAngle - rotaryStartAngle);
// fill
auto rx = centreX - radius;
auto ry = centreY - radius;
auto rw = radius * 2.0f;
g.setColour(juce::Colours::black);
g.fillEllipse(rx + 1.0f, ry + 1.0f, rw + 2.5f, rw + 2.5f);
g.setColour(juce::Colour::fromString("FF3C3C3C"));
g.fillEllipse(rx, ry, rw, rw);
// outline
g.setColour(juce::Colours::grey);
g.drawEllipse(rx, ry, rw, rw, 3.0f);
// pointer
juce::Path p;
auto pointerLength = radius * 0.33f;
auto pointerThickness = 2.0f;
p.addRectangle(-pointerThickness * 0.5f, -radius, pointerThickness, pointerLength);
p.applyTransform(juce::AffineTransform::rotation(angle).translated(centreX, centreY));
g.setColour(juce::Colours::white);
g.fillPath(p);
}
```
> [Slider customisation tutorial](https://docs.juce.com/master/tutorial_look_and_feel_customisation.html#tutorial_look_and_feel_customisation_sliders)
### 成果

* 在參考圖中,旋鈕表面的反光形成的立體感,原本打算以漸層來模擬,因此研究了 JUCE 中的 `ColourGradient` class,但研究後發現如參考圖中的漸層較難實現,因此改為使用堆疊層次的方式來呈現立體感
> [ColourGradient docs](https://docs.juce.com/master/classColourGradient.html)
## 實作示波器
### 簡述
* 定義一個 class:`Oscilloscope`
* 其中,需要實作的功能有二
1. 生成波形圖
2. 隨時間更新畫面
### 生成波形圖
```cpp=
void Oscilloscope::generateWaveform()
{
juce::AudioBuffer<float> temp;
while (waveform.getNumCompleteBuffersAvailable() > 0) {
if (waveform.getAudioBuffer(temp)) {
auto size = temp.getNumSamples();
juce::FloatVectorOperations::copy(
pathBuffer.getWritePointer(0, 0),
pathBuffer.getReadPointer(0, size),
pathBuffer.getNumSamples() - size
);
juce::FloatVectorOperations::copy(
pathBuffer.getWritePointer(0, pathBuffer.getNumSamples() - size),
temp.getReadPointer(0, 0),
size
);
pathProducer.generatePath(pathBuffer, getLocalBounds().toFloat());
}
}
while (pathProducer.getNumPathsAvailable() > 0) {
pathProducer.getPath(waveformPath);
}
}
```
* `waveform` 為一 FIFO buffer,當此 buffer 可用時,便會開始讀取 audio buffer 的內容,暫存至 `temp` 中,而最終目標是將內容存入 `pathBuffer` 中
* 第一個 `while` 迴圈中,有兩次的 `juce::FloatVectorOperations::copy()` 呼叫
* 第一次呼叫,會將 `pathBuffer` 的內容向前平移,以在後方空出所需的空間
* 第二次呼叫,會將 `temp` 中的內容放入前一步所空出的空間
* 至此便將 `pathBuffer` 的內容更新完畢,接下來便可藉由 `pathProducer` 生成波形
* 第二個 `while` 迴圈中,會將 `pathProducer` 生成的波形取出,暫存至 `waveformPath` 變數中
### 隨時間更新畫面
* 為了讓示波器隨時間更新當前波形,`Oscilloscope` 需要繼承 `juce::Timer` class,並覆寫 `timerCallback()` 函式
```cpp
void Oscilloscope::timerCallback()
{
generateWaveform();
repaint();
}
```
* 在生成波形後呼叫 `repaint()` 函式,會將示波器標示為需要重新繪製,隨後 `paint()` 函式便會被呼叫
* `paint()` 函式實作如下
```cpp
void Oscilloscope::paint(juce::Graphics &g)
{
g.fillAll(juce::Colour(50, 50, 50));
g.setColour(juce::Colours::grey);
g.drawRoundedRectangle(getLocalBounds().toFloat(), 3, 3);
g.setColour(juce::Colours::white);
g.strokePath(waveformPath, juce::PathStrokeType(1.f));
}
```
* 在生成波形時,使用了 `waveformPath` 變數來儲存當前的波形,因此,每次呼叫 `paint()` 函式時,當前的波形便會更新到畫面上
* 在 `Oscilloscope` 的 constructor 中需要啟動計時器,並設定呼叫 `timerCallback()` 的時間間隔
```cpp
Oscilloscope::Oscilloscope(MyAudioProcessor &p):
audioProcessor(p),
waveform(p.getSingleChannelSampleFifo())
{
pathBuffer.setSize(1, 1024);
startTimerHz(30);
}
```
* 此處的 `startTimerHz(30)`,表示每秒有 30 個 cycles,也就是 $1/30$ 秒會呼叫一次 `timerCallback()`
### 成果

## 實作頻譜圖
### 簡述
* 定義一個 class:`FrequencySpectrum`
* 其中,需要實作的功能有三
1. 繪製網格
2. 生成頻譜
3. 隨時間更新畫面
### 繪製網格
* 首先,我們看到 `paint()` 函式
```cpp=
void FrequencySpectrum::paint(juce::Graphics &g)
{
g.fillAll(juce::Colour(50, 50, 50));
g.setColour(juce::Colours::grey);
g.drawRoundedRectangle(getLocalBounds().toFloat(), 3, 3);
drawBackgroundGrid(g);
g.setColour(juce::Colours::white);
g.strokePath(spectrumPath, juce::PathStrokeType(1.f));
drawTextLabels(g);
}
```
* 與示波器相較之下,在 `paint()` 函式中,額外呼叫了兩個函式:`drawBackgroundGrid()` 與 `drawTextLabels()`,以下將分別說明
* 由於後繪製的圖形會將先繪製的圖形覆蓋,因此第 7-10 行的程式碼順序不可更動,以形成網格在頻譜之下、頻譜在標籤之下的效果,如下圖

* `drawBackgroundGrid()` 負責繪製網格,實作如下
```cpp
void FrequencySpectrum::drawBackgroundGrid(juce::Graphics &g)
{
auto freqs = getFrequencies();
auto gains = getGains();
auto area = getRenderArea();
auto left = area.getX();
auto top = area.getY();
auto right = area.getRight();
auto bottom = area.getBottom();
auto xs = getXs(freqs, getAnalysisArea().getX(), getAnalysisArea().getWidth());
g.setColour(juce::Colours::dimgrey);
for (auto x : xs) {
g.drawVerticalLine(x, top, bottom);
}
g.setColour(juce::Colours::darkgrey);
for (auto gain : gains) {
auto y = juce::jmap(
gain, gains.front(), gains.back(),
float(getAnalysisArea().getBottom()),
float(getAnalysisArea().getY())
);
g.drawHorizontalLine(y, left, right);
}
}
```
* 透過呼叫 `getRenderArea()`,便能夠取得需要繪製網格的區域
```cpp
juce::Rectangle<int> getRenderArea()
{
auto bounds = getLocalBounds();
bounds.removeFromTop(3);
bounds.removeFromBottom(3);
bounds.removeFromLeft(3);
bounds.removeFromRight(3);
return bounds;
}
```
* 四個方位皆減去 3,是為了避免與頻譜圖的外框重疊
* `getAnalysisArea()` 會取得實際繪製頻譜的範圍,也就是 x 座標的 20Hz 到 20kHz,以及 y 座標的 -36dB 到 +12dB 的之間範圍
```cpp
juce::Rectangle<int> getAnalysisArea()
{
auto bounds = getLocalBounds();
bounds.removeFromTop(14);
bounds.removeFromBottom(18);
bounds.removeFromLeft(18);
bounds.removeFromRight(16);
return bounds;
}
```
* 網格的線條會繪製在 `getRenderArea()` 所取得的區域上,但線條之間的間隔是以 `getAnalysisArea()` 所取得的區域大小作計算的
* `drawTextLabels()` 負責繪製文字標籤,實作如下
```cpp
void FrequencySpectrum::drawTextLabels(juce::Graphics &g)
{
const int fontHeight = 10;
g.setFont(fontHeight);
g.setColour(juce::Colours::lightgrey);
auto freqs = getFrequencies();
auto gains = getGains();
auto area = getAnalysisArea();
auto left = area.getX();
auto top = area.getY();
auto bottom = area.getBottom();
auto width = area.getWidth();
auto xs = getXs(freqs, left, width);
for (int i = 0; i < freqs.size(); ++i) {
auto f = freqs[i];
auto x = xs[i];
bool isThousand = false;
if (f > 999.f) {
isThousand = true;
f /= 1000.f;
}
juce::String str;
str << f;
if (isThousand)
str << "k";
str << "Hz";
auto textWidth = g.getCurrentFont().getStringWidth(str);
juce::Rectangle<int> r;
r.setSize(textWidth, fontHeight);
r.setCentre(x, 0);
r.setY(getRenderArea().getBottom() - fontHeight);
g.drawFittedText(str, r, juce::Justification::centred, 1);
}
for (auto gain : gains) {
auto y = juce::jmap(gain, gains.front(), gains.back(), float(bottom), float(top));
juce::String str;
if (gain > 0)
str << "+";
str << gain << "dB";
auto textWidth = g.getCurrentFont().getStringWidth(str);
juce::Rectangle<int> r;
r.setSize(textWidth, fontHeight);
r.setCentre(0, y - fontHeight / 2.f);
r.setX(5);
g.drawFittedText(str, r, juce::Justification::centred, 1);
}
}
```
### 生成頻譜
```cpp
void FrequencySpectrum::generateSpectrum()
{
juce::AudioBuffer<float> temp;
while (spectrum.getNumCompleteBuffersAvailable() > 0) {
if (spectrum.getAudioBuffer(temp)) {
auto size = temp.getNumSamples();
juce::FloatVectorOperations::copy(
pathBuffer.getWritePointer(0, 0),
pathBuffer.getReadPointer(0, size),
pathBuffer.getNumSamples() - size
);
juce::FloatVectorOperations::copy(
pathBuffer.getWritePointer(0, pathBuffer.getNumSamples() - size),
temp.getReadPointer(0, 0),
size
);
fftDataGenerator.produceFFTDataForRendering(pathBuffer, getGains().front());
}
}
const auto fftsize = fftDataGenerator.getFFTSize();
const auto binWidth = audioProcessor.getSampleRate() / double(fftsize);
while (fftDataGenerator.getNumAvailableFFTDataBlocks() > 0) {
std::vector<float> fftData;
if(fftDataGenerator.getFFTData(fftData)) {
pathProducer.generatePath(fftData, getAnalysisArea().toFloat(), fftsize, binWidth, getGains().front());
}
}
while (pathProducer.getNumPathsAvailable() > 0) {
pathProducer.getPath(spectrumPath);
}
}
```
* 經由觀察可以發現,此實作與示波器的實作大同小異,相同的部分可參考[該段落](#生成波形圖)的說明
* 不同點在於,`pathBuffer` 更新完畢後,是先透過 `fftDataGenerator` 產生 `fftData` 的,生成的 `fftData` 後續才會交由 `pathProducer` 來生成頻譜
### 隨時間更新畫面
此功能與前述示波器實作的段落中,概念與做法皆相同,詳見[這裡](#隨時間更新畫面)
### 成果

---
## 問題與解法
### `dsp` module
* `FIFO.h` 中使用到 `dsp` module 的部分在建置時會報錯,如下圖

* 由錯誤訊息可知專案沒有讀取到該 module
* 檢查後發現該 module 原本並不包含在專案的 module 列表中,只要自行手動加入即可,如下圖
