Skip to content

Commit adefd3b

Browse files
committed
Add record feature, show latency/tail controls in quick audio settings in DAW, add MIDI settings shortcut in MIDI history dialog
1 parent 29bfbe6 commit adefd3b

19 files changed

Lines changed: 994 additions & 417 deletions

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ set(JUCE_COMPILE_DEFINITIONS
303303
JUCE_USE_COREIMAGE_LOADER=0
304304
JUCE_SILENCE_XCODE_15_LINKER_WARNING=1
305305
JUCE_USE_XRENDER=1
306+
JUCE_USE_MP3AUDIOFORMAT=1
306307
JUCE_COREGRAPHICS_RENDER_WITH_MULTIPLE_PAINT_CALLS=0
307308
JUCE_USE_DIRECTWRITE=0
308309
JUCE_JACK=1

Resources/Fonts/IconFont.ttf

424 Bytes
Binary file not shown.

Source/Components/PropertiesPanel.cpp

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -785,19 +785,11 @@ PropertiesPanelProperty* PropertiesPanel::FilePathComponent::createCopy()
785785
return new FilePathComponent(getName(), property);
786786
}
787787

788-
void PropertiesPanel::FilePathComponent::paint(Graphics& g)
789-
{
790-
PropertiesPanelProperty::paint(g);
791-
792-
g.setColour(PlugDataColours::panelBackgroundColour);
793-
g.fillRect(getLocalBounds().removeFromRight(getHeight()));
794-
}
795-
796788
void PropertiesPanel::FilePathComponent::resized()
797789
{
798790
auto labelBounds = getLocalBounds().removeFromRight(getWidth() / 2);
799-
label.setBounds(labelBounds);
800791
browseButton.setBounds(labelBounds.removeFromRight(getHeight()));
792+
label.setBounds(labelBounds);
801793
}
802794

803795
PropertiesPanel::DirectoryPathComponent::DirectoryPathComponent(String const& propertyName, Value const& value)
@@ -907,7 +899,7 @@ void PropertiesPanel::ActionComponent::mouseUp(MouseEvent const& e)
907899

908900
PropertiesPanel::PropertiesPanel()
909901
{
910-
messageWhenEmpty = "(nothing settable)";
902+
messageWhenEmpty = "(no inspector selection)";
911903

912904
addAndMakeVisible(viewport);
913905
viewport.setViewedComponent(propertyHolderComponent = new PropertyHolderComponent());

Source/Components/PropertiesPanel.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,8 +265,6 @@ class PropertiesPanel : public Component {
265265

266266
PropertiesPanelProperty* createCopy() override;
267267

268-
void paint(Graphics& g) override;
269-
270268
void resized() override;
271269
};
272270

Source/Constants.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ struct Icons {
127127
static inline String const ShowIndex = CharPointer_UTF8("\xc2\xbA");
128128
static inline String const ShowXY = CharPointer_UTF8("\xc2\xbb");
129129

130+
static inline String const Record = CharPointer_UTF8 ("\xc3\x8a");
131+
static inline String const AudioSettings = CharPointer_UTF8("\xc3\x89");
132+
130133
// ================== OBJECT ICONS ==================
131134

132135
// generic

Source/Dialogs/AudioExportDialog.h

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
class AudioExportDialog final : public Component
2+
, public Value::Listener
3+
, private Thread
4+
, private Timer {
5+
public:
6+
AudioExportDialog(Dialog* parentDialog, File const& recordingToExport)
7+
: Thread("Audio Export")
8+
, dialog(parentDialog)
9+
, recording(recordingToExport)
10+
{
11+
formatManager.registerBasicFormats();
12+
13+
for (int i = 0; i < formatManager.getNumKnownFormats(); i++) {
14+
formatNames.add(formatManager.getKnownFormat(i)->getFormatName());
15+
formatExtensions.add(formatManager.getKnownFormat(i)->getFileExtensions()[0]);
16+
}
17+
18+
destinationValue = File::getSpecialLocation(File::userMusicDirectory).getChildFile("recording.wav").getFullPathName();
19+
formatValue = 1;
20+
sampleRateValue = 1;
21+
bitDepthValue = 2;
22+
normaliseValue = false;
23+
24+
auto* destinationComponent = new PropertiesPanel::FilePathComponent("Destination", destinationValue);
25+
auto* formatComponent = new PropertiesPanel::ComboComponent("Format", formatValue, formatNames);
26+
auto* sampleRateComponent = new PropertiesPanel::ComboComponent("Sample rate", sampleRateValue, { "44100", "48000", "88200", "96000", "192000" });
27+
auto* bitDepthComponent = new PropertiesPanel::ComboComponent("Bit depth", bitDepthValue, { "16", "24", "32" });
28+
auto* normaliseComponent = new PropertiesPanel::BoolComponent("Normalize", normaliseValue, { "No", "Yes" });
29+
30+
panel.addSection(" ", { destinationComponent, formatComponent, sampleRateComponent, bitDepthComponent, normaliseComponent });
31+
addAndMakeVisible(panel);
32+
33+
auto const backgroundColour = PlugDataColours::panelBackgroundColour;
34+
cancelButton.setColour(TextButton::buttonColourId, backgroundColour.contrasting(0.05f));
35+
cancelButton.setColour(TextButton::buttonOnColourId, backgroundColour.contrasting(0.1f));
36+
cancelButton.setColour(ComboBox::outlineColourId, Colours::transparentBlack);
37+
cancelButton.setButtonText("Cancel");
38+
cancelButton.onClick = [this] {
39+
if (isThreadRunning()) {
40+
signalThreadShouldExit();
41+
waitForThreadToExit(2000);
42+
}
43+
recording.deleteFile();
44+
dialog->closeDialog();
45+
};
46+
addAndMakeVisible(cancelButton);
47+
48+
exportButton.setColour(TextButton::buttonColourId, backgroundColour.contrasting(0.05f));
49+
exportButton.setColour(TextButton::buttonOnColourId, backgroundColour.contrasting(0.1f));
50+
exportButton.setColour(ComboBox::outlineColourId, Colours::transparentBlack);
51+
exportButton.setButtonText("Export");
52+
exportButton.onClick = [this] { beginExport(); };
53+
addAndMakeVisible(exportButton);
54+
55+
progressBar = std::make_unique<ProgressBar>(progress);
56+
progressBar->setTextToDisplay("Exporting...");
57+
addChildComponent(*progressBar);
58+
59+
formatValue.addListener(this);
60+
61+
setSize(520, 285);
62+
}
63+
64+
~AudioExportDialog() override
65+
{
66+
stopTimer();
67+
if (isThreadRunning()) {
68+
signalThreadShouldExit();
69+
waitForThreadToExit(2000);
70+
}
71+
}
72+
73+
void valueChanged(Value& v) override
74+
{
75+
int const formatIdx = jlimit(0, (int)formatNames.size() - 1, getValue<int>(formatValue) - 1);
76+
auto formatExtension = formatExtensions[formatIdx];
77+
destinationValue = File(getValue<String>(destinationValue)).withFileExtension(formatExtension).getFullPathName();
78+
}
79+
80+
void resized() override
81+
{
82+
auto bounds = getLocalBounds().withTrimmedTop(16);
83+
auto buttonRow = bounds.removeFromBottom(56).reduced(8);
84+
int const buttonWidth = (buttonRow.getWidth() - 8) / 2;
85+
cancelButton.setBounds(buttonRow.removeFromLeft(buttonWidth).withSizeKeepingCentre(84, 26));
86+
buttonRow.removeFromLeft(8);
87+
exportButton.setBounds(buttonRow.withSizeKeepingCentre(84, 26));
88+
89+
panel.setContentWidth(bounds.getWidth() - 32);
90+
panel.setBounds(bounds);
91+
}
92+
93+
private:
94+
StringArray formatNames;
95+
StringArray formatExtensions;
96+
static constexpr std::array sampleRates { 44100, 48000, 88200, 96000, 192000 };
97+
static constexpr std::array bitDepths { 16, 24, 32 };
98+
99+
void beginExport()
100+
{
101+
// Snapshot current settings into members so the background thread can
102+
// read them without touching Value (which isn't thread-safe).
103+
int const formatIdx = jlimit(0, (int)formatNames.size() - 1, getValue<int>(formatValue) - 1);
104+
chosenFormatName = formatNames[formatIdx];
105+
chosenDestination = File(getValue<String>(destinationValue)).withFileExtension(formatExtensions[formatIdx]);
106+
chosenSampleRate = sampleRates[jlimit(0, (int)sampleRates.size() - 1, getValue<int>(sampleRateValue) - 1)];
107+
chosenBitDepth = bitDepths[jlimit(0, (int)bitDepths.size() - 1, getValue<int>(bitDepthValue) - 1)];
108+
chosenNormalise = getValue<bool>(normaliseValue);
109+
110+
if (chosenDestination == File { })
111+
return;
112+
113+
panel.setEnabled(false);
114+
exportButton.setEnabled(false);
115+
progress = 0.0;
116+
progressBar->setVisible(true);
117+
resized();
118+
119+
startThread();
120+
startTimerHz(30);
121+
}
122+
123+
void run() override
124+
{
125+
WavAudioFormat wav;
126+
std::unique_ptr<AudioFormatReader> reader(
127+
wav.createReaderFor(recording.createInputStream().release(), true));
128+
if (reader == nullptr)
129+
return;
130+
131+
progress = 0.05;
132+
133+
AudioBuffer<float> audio(static_cast<int>(reader->numChannels),
134+
static_cast<int>(reader->lengthInSamples));
135+
reader->read(&audio, 0, audio.getNumSamples(), 0, true, true);
136+
if (threadShouldExit())
137+
return;
138+
progress = 0.3;
139+
140+
if (chosenNormalise) {
141+
float peak = 0.0f;
142+
for (int ch = 0; ch < audio.getNumChannels(); ++ch)
143+
peak = jmax(peak, audio.getMagnitude(ch, 0, audio.getNumSamples()));
144+
if (peak > 0.0f)
145+
audio.applyGain(1.0f / peak);
146+
}
147+
if (threadShouldExit())
148+
return;
149+
progress = 0.45;
150+
151+
AudioBuffer<float> const* sourceBuffer = &audio;
152+
AudioBuffer<float> resampled;
153+
if (chosenSampleRate != static_cast<int>(reader->sampleRate)) {
154+
double const ratio = reader->sampleRate / static_cast<double>(chosenSampleRate);
155+
int const outSamples = static_cast<int>(audio.getNumSamples() / ratio);
156+
resampled.setSize(audio.getNumChannels(), outSamples);
157+
158+
for (int ch = 0; ch < audio.getNumChannels(); ++ch) {
159+
LagrangeInterpolator interp;
160+
interp.process(ratio, audio.getReadPointer(ch), resampled.getWritePointer(ch), outSamples);
161+
if (threadShouldExit())
162+
return;
163+
}
164+
sourceBuffer = &resampled;
165+
}
166+
progress = 0.65;
167+
168+
auto* format = formatForName(chosenFormatName);
169+
if (format == nullptr)
170+
return;
171+
172+
chosenDestination.deleteFile();
173+
auto out = std::unique_ptr<OutputStream>(chosenDestination.createOutputStream());
174+
if (out == nullptr)
175+
return;
176+
177+
auto const options = AudioFormatWriterOptions { }
178+
.withSampleRate(static_cast<double>(chosenSampleRate))
179+
.withNumChannels(sourceBuffer->getNumChannels())
180+
.withBitsPerSample(chosenBitDepth);
181+
182+
auto writer = format->createWriterFor(out, options);
183+
if (writer == nullptr)
184+
return;
185+
out.release();
186+
187+
int const totalSamples = sourceBuffer->getNumSamples();
188+
int const chunkSize = 8192;
189+
int samplesWritten = 0;
190+
while (samplesWritten < totalSamples) {
191+
if (threadShouldExit())
192+
return;
193+
int const thisChunk = jmin(chunkSize, totalSamples - samplesWritten);
194+
writer->writeFromAudioSampleBuffer(*sourceBuffer, samplesWritten, thisChunk);
195+
samplesWritten += thisChunk;
196+
progress = 0.65 + 0.35 * (samplesWritten / static_cast<double>(totalSamples));
197+
}
198+
progress = 1.0;
199+
}
200+
201+
void paint(Graphics& g) override
202+
{
203+
g.setColour(PlugDataColours::panelBackgroundColour);
204+
g.fillRoundedRectangle(getLocalBounds().reduced(1).toFloat(), Corners::windowCornerRadius);
205+
206+
auto const titlebarBounds = getLocalBounds().removeFromTop(40).toFloat();
207+
208+
Path p;
209+
p.addRoundedRectangle(titlebarBounds.getX(), titlebarBounds.getY(), titlebarBounds.getWidth(), titlebarBounds.getHeight(), Corners::windowCornerRadius, Corners::windowCornerRadius, true, true, false, false);
210+
211+
g.setColour(PlugDataColours::toolbarBackgroundColour);
212+
g.fillPath(p);
213+
214+
g.setColour(PlugDataColours::toolbarOutlineColour);
215+
g.drawHorizontalLine(40, 0.0f, getWidth());
216+
217+
Fonts::drawStyledText(g, "Export recording", Rectangle<float>(0.0f, 4.0f, getWidth(), 32.0f), PlugDataColours::panelTextColour, Semibold, 15, Justification::centred);
218+
}
219+
220+
void timerCallback() override
221+
{
222+
if (!isThreadRunning()) {
223+
stopTimer();
224+
progressBar->setVisible(false);
225+
recording.deleteFile();
226+
dialog->closeDialog();
227+
}
228+
}
229+
230+
AudioFormat* formatForName(String const& name) const
231+
{
232+
for (int i = 0; i < formatManager.getNumKnownFormats(); ++i) {
233+
auto* f = formatManager.getKnownFormat(i);
234+
if (f->getFormatName().containsIgnoreCase(name))
235+
return f;
236+
}
237+
return nullptr;
238+
}
239+
240+
Dialog* dialog;
241+
File recording;
242+
243+
AudioFormatManager formatManager;
244+
245+
Value destinationValue, formatValue, sampleRateValue, bitDepthValue, normaliseValue;
246+
247+
File chosenDestination;
248+
String chosenFormatName;
249+
int chosenSampleRate = 44100;
250+
int chosenBitDepth = 24;
251+
bool chosenNormalise = false;
252+
253+
PropertiesPanel panel;
254+
TextButton cancelButton;
255+
TextButton exportButton;
256+
257+
std::unique_ptr<ProgressBar> progressBar;
258+
double progress = 0.0;
259+
};

0 commit comments

Comments
 (0)