osci-render/Source/components/AudioRecordingComponent.h

325 wiersze
12 KiB
C++

/*
==============================================================================
This file is part of the JUCE examples.
Copyright (c) 2022 - Raw Material Software Limited
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
To use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
PURPOSE, ARE DISCLAIMED.
==============================================================================
*/
/*******************************************************************************
The block below describes the properties of this PIP. A PIP is a short snippet
of code that can be read by the Projucer and used to generate a JUCE project.
BEGIN_JUCE_PIP_METADATA
name: AudioRecordingComponent
version: 1.0.0
vendor: JUCE
website: http://juce.com
description: Records audio to a file.
dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
juce_audio_processors, juce_audio_utils, juce_core,
juce_data_structures, juce_events, juce_graphics,
juce_gui_basics, juce_gui_extra
exporters: xcode_mac, vs2022, linux_make, androidstudio, xcode_iphone
moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
type: Component
mainClass: AudioRecordingComponent
useLocalCopy: 1
END_JUCE_PIP_METADATA
*******************************************************************************/
#pragma once
#include "DoubleTextBox.h"
//==============================================================================
class AudioRecorder final {
public:
AudioRecorder(OscirenderAudioProcessor& p, juce::AudioThumbnail& thumbnailToUpdate)
: audioProcessor(p), thumbnail(thumbnailToUpdate) {
backgroundThread.startThread();
audioProcessor.setAudioThreadCallback([this](const juce::AudioBuffer<float>& buffer) { audioThreadCallback(buffer); });
}
~AudioRecorder() {
audioProcessor.setAudioThreadCallback(nullptr);
stop();
}
//==============================================================================
void startRecording(const juce::File& file) {
stop();
if (audioProcessor.currentSampleRate > 0) {
// Create an OutputStream to write to our destination file...
file.deleteFile();
if (auto fileStream = std::unique_ptr<juce::FileOutputStream>(file.createOutputStream())) {
// Now create a WAV writer object that writes to our output stream...
juce::WavAudioFormat wavFormat;
if (auto writer = wavFormat.createWriterFor(fileStream.get(), audioProcessor.currentSampleRate, 2, 32, {}, 0)) {
fileStream.release(); // (passes responsibility for deleting the stream to the writer object that is now using it)
// Now we'll create one of these helper objects which will act as a FIFO buffer, and will
// write the data to disk on our background thread.
threadedWriter.reset(new juce::AudioFormatWriter::ThreadedWriter(writer, backgroundThread, 32768));
// Reset our recording thumbnail
thumbnail.reset(writer->getNumChannels(), writer->getSampleRate());
nextSampleNum = 0;
// And now, swap over our active writer pointer so that the audio callback will start using it..
const juce::ScopedLock sl(writerLock);
activeWriter = threadedWriter.get();
}
}
}
}
void stop() {
// First, clear this pointer to stop the audio callback from using our writer object..
{
const juce::ScopedLock sl(writerLock);
activeWriter = nullptr;
}
// Now we can delete the writer object. It's done in this order because the deletion could
// take a little time while remaining data gets flushed to disk, so it's best to avoid blocking
// the audio callback while this happens.
threadedWriter.reset();
}
bool isRecording() const {
return activeWriter.load() != nullptr;
}
void audioThreadCallback(const juce::AudioBuffer<float>& buffer) {
if (nextSampleNum >= recordingLength * audioProcessor.currentSampleRate) {
stop();
stopCallback();
return;
}
const juce::ScopedLock sl(writerLock);
int numSamples = buffer.getNumSamples();
if (activeWriter.load() != nullptr) {
activeWriter.load()->write(buffer.getArrayOfReadPointers(), numSamples);
thumbnail.addBlock(nextSampleNum, buffer, 0, numSamples);
nextSampleNum += numSamples;
}
}
void setRecordLength(double recordLength) {
recordingLength = recordLength;
}
std::function<void()> stopCallback;
private:
OscirenderAudioProcessor& audioProcessor;
juce::AudioThumbnail& thumbnail;
juce::TimeSliceThread backgroundThread { "Audio Recorder Thread" }; // the thread that will write our audio data to disk
std::unique_ptr<juce::AudioFormatWriter::ThreadedWriter> threadedWriter; // the FIFO used to buffer the incoming data
juce::int64 nextSampleNum = 0;
double recordingLength = 99999999999.0;
juce::CriticalSection writerLock;
std::atomic<juce::AudioFormatWriter::ThreadedWriter*> activeWriter { nullptr };
};
//==============================================================================
class RecordingThumbnail final : public juce::Component,
private juce::ChangeListener {
public:
RecordingThumbnail() {
formatManager.registerBasicFormats();
thumbnail.addChangeListener(this);
}
~RecordingThumbnail() override {
thumbnail.removeChangeListener(this);
}
juce::AudioThumbnail& getAudioThumbnail() { return thumbnail; }
void setDisplayFullThumbnail(bool displayFull) {
displayFullThumb = displayFull;
repaint();
}
void paint(juce::Graphics& g) override {
g.setColour(juce::Colours::white);
if (thumbnail.getTotalLength() > 0.0) {
auto endTime = displayFullThumb ? thumbnail.getTotalLength()
: juce::jmax(30.0, thumbnail.getTotalLength());
auto thumbArea = getLocalBounds();
thumbnail.drawChannels(g, thumbArea.reduced(2), 0.0, endTime, 1.0f);
}
}
private:
juce::AudioFormatManager formatManager;
juce::AudioThumbnailCache thumbnailCache { 10 };
juce::AudioThumbnail thumbnail { 128, formatManager, thumbnailCache };
bool displayFullThumb = false;
void changeListenerCallback(juce::ChangeBroadcaster* source) override {
if (source == &thumbnail)
repaint();
}
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(RecordingThumbnail)
};
//==============================================================================
class AudioRecordingComponent final : public juce::Component {
public:
AudioRecordingComponent(OscirenderAudioProcessor& p) : audioProcessor(p) {
addAndMakeVisible(recordButton);
addAndMakeVisible(timedRecord);
addAndMakeVisible(recordLength);
recordButton.setTooltip("Start recording audio to a WAV file. Press again to stop and save the recording.");
timedRecord.setTooltip("Record for a set amount of time in seconds. When enabled, the recording will automatically stop once the time is reached.");
recordLength.setValue(1);
recordButton.onClick = [this] {
if (recordButton.getToggleState()) {
startRecording();
} else {
stopRecording();
}
};
timedRecord.onClick = [this] {
if (timedRecord.getToggleState()) {
addAndMakeVisible(recordLength);
} else {
removeChildComponent(&recordLength);
}
resized();
};
recorder.stopCallback = [this] {
juce::MessageManager::callAsync([this] {
recordButton.setToggleState(false, juce::sendNotification);
});
};
addAndMakeVisible(recordingThumbnail);
recordingThumbnail.setDisplayFullThumbnail(true);
}
void resized() override {
double iconSize = 25;
auto area = getLocalBounds();
recordButton.setBounds(area.removeFromLeft(iconSize).withSizeKeepingCentre(iconSize, iconSize));
area.removeFromLeft(5);
timedRecord.setBounds(area.removeFromLeft(iconSize).withSizeKeepingCentre(iconSize, iconSize));
if (timedRecord.getToggleState()) {
recordLength.setBounds(area.removeFromLeft(80).withSizeKeepingCentre(60, 25));
}
area.removeFromLeft(5);
recordingThumbnail.setBounds(area);
}
private:
OscirenderAudioProcessor& audioProcessor;
RecordingThumbnail recordingThumbnail;
AudioRecorder recorder{ audioProcessor, recordingThumbnail.getAudioThumbnail() };
SvgButton recordButton{ "record", BinaryData::record_svg, juce::Colours::white, juce::Colours::red };
juce::File lastRecording;
juce::FileChooser chooser { "Output file...", juce::File::getCurrentWorkingDirectory().getChildFile("recording.wav"), "*.wav" };
SvgButton timedRecord{ "timedRecord", BinaryData::timer_svg, juce::Colours::white, juce::Colours::red };
DoubleTextBox recordLength{ 0, 60 * 60 * 24 };
void startRecording() {
auto parentDir = juce::File::getSpecialLocation(juce::File::tempDirectory);
lastRecording = parentDir.getNonexistentChildFile("osci-render-recording", ".wav");
if (timedRecord.getToggleState()) {
recorder.setRecordLength(recordLength.getValue());
} else {
recorder.setRecordLength(99999999999.0);
}
recorder.startRecording(lastRecording);
recordButton.setColour(juce::TextButton::buttonColourId, juce::Colours::red);
recordButton.setColour(juce::TextButton::textColourOnId, juce::Colours::black);
}
void stopRecording() {
recorder.stop();
recordButton.setColour(juce::TextButton::buttonColourId, findColour(juce::TextButton::buttonColourId));
recordButton.setColour(juce::TextButton::textColourOnId, findColour(juce::TextButton::textColourOnId));
chooser.launchAsync(juce::FileBrowserComponent::saveMode
| juce::FileBrowserComponent::canSelectFiles
| juce::FileBrowserComponent::warnAboutOverwriting,
[this](const juce::FileChooser& c) {
if (juce::FileInputStream inputStream(lastRecording); inputStream.openedOk()) {
juce::URL url = c.getURLResult();
if (url.isLocalFile()) {
if (const auto outputStream = url.getLocalFile().createOutputStream()) {
outputStream->setPosition(0);
outputStream->truncate();
outputStream->writeFromInputStream(inputStream, -1);
}
}
}
lastRecording.deleteFile();
});
}
inline juce::Colour getUIColourIfAvailable(juce::LookAndFeel_V4::ColourScheme::UIColour uiColour, juce::Colour fallback = juce::Colour(0xff4d4d4d)) noexcept {
if (auto* v4 = dynamic_cast<juce::LookAndFeel_V4*> (&juce::LookAndFeel::getDefaultLookAndFeel()))
return v4->getCurrentColourScheme().getUIColour(uiColour);
return fallback;
}
inline std::unique_ptr<juce::OutputStream> makeOutputStream(const juce::URL& url) {
if (const auto doc = juce::AndroidDocument::fromDocument(url))
return doc.createOutputStream();
#if ! JUCE_IOS
if (url.isLocalFile())
return url.getLocalFile().createOutputStream();
#endif
return url.createOutputStream();
}
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AudioRecordingComponent)
};