mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-10 23:44:24 +00:00
305 lines
11 KiB
C++
305 lines
11 KiB
C++
/*
|
|
==============================================================================
|
|
|
|
This file is part of the JUCE library.
|
|
Copyright (c) 2017 - ROLI Ltd.
|
|
|
|
JUCE is an open source library subject to commercial or open-source
|
|
licensing.
|
|
|
|
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
|
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
|
27th April 2017).
|
|
|
|
End User License Agreement: www.juce.com/juce-5-licence
|
|
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
|
|
|
Or: You may also use this code under the terms of the GPL v3 (see
|
|
www.gnu.org/licenses).
|
|
|
|
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
|
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
|
DISCLAIMED.
|
|
|
|
==============================================================================
|
|
*/
|
|
|
|
#include "../JuceDemoHeader.h"
|
|
#include "AudioLiveScrollingDisplay.h"
|
|
|
|
//==============================================================================
|
|
/** A simple class that acts as an AudioIODeviceCallback and writes the
|
|
incoming audio data to a WAV file.
|
|
*/
|
|
class AudioRecorder : public AudioIODeviceCallback
|
|
{
|
|
public:
|
|
AudioRecorder (AudioThumbnail& thumbnailToUpdate)
|
|
: thumbnail (thumbnailToUpdate),
|
|
backgroundThread ("Audio Recorder Thread"),
|
|
sampleRate (0), nextSampleNum (0), activeWriter (nullptr)
|
|
{
|
|
backgroundThread.startThread();
|
|
}
|
|
|
|
~AudioRecorder()
|
|
{
|
|
stop();
|
|
}
|
|
|
|
//==============================================================================
|
|
void startRecording (const File& file)
|
|
{
|
|
stop();
|
|
|
|
if (sampleRate > 0)
|
|
{
|
|
// Create an OutputStream to write to our destination file...
|
|
file.deleteFile();
|
|
ScopedPointer<FileOutputStream> fileStream (file.createOutputStream());
|
|
|
|
if (fileStream != nullptr)
|
|
{
|
|
// Now create a WAV writer object that writes to our output stream...
|
|
WavAudioFormat wavFormat;
|
|
AudioFormatWriter* writer = wavFormat.createWriterFor (fileStream, sampleRate, 1, 16, StringPairArray(), 0);
|
|
|
|
if (writer != nullptr)
|
|
{
|
|
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 = new 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 ScopedLock sl (writerLock);
|
|
activeWriter = threadedWriter;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void stop()
|
|
{
|
|
// First, clear this pointer to stop the audio callback from using our writer object..
|
|
{
|
|
const 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 != nullptr;
|
|
}
|
|
|
|
//==============================================================================
|
|
void audioDeviceAboutToStart (AudioIODevice* device) override
|
|
{
|
|
sampleRate = device->getCurrentSampleRate();
|
|
}
|
|
|
|
void audioDeviceStopped() override
|
|
{
|
|
sampleRate = 0;
|
|
}
|
|
|
|
void audioDeviceIOCallback (const float** inputChannelData, int /*numInputChannels*/,
|
|
float** outputChannelData, int numOutputChannels,
|
|
int numSamples) override
|
|
{
|
|
const ScopedLock sl (writerLock);
|
|
|
|
if (activeWriter != nullptr)
|
|
{
|
|
activeWriter->write (inputChannelData, numSamples);
|
|
|
|
// Create an AudioBuffer to wrap our incoming data, note that this does no allocations or copies, it simply references our input data
|
|
AudioBuffer<float> buffer (const_cast<float**> (inputChannelData), thumbnail.getNumChannels(), numSamples);
|
|
thumbnail.addBlock (nextSampleNum, buffer, 0, numSamples);
|
|
nextSampleNum += numSamples;
|
|
}
|
|
|
|
// We need to clear the output buffers, in case they're full of junk..
|
|
for (int i = 0; i < numOutputChannels; ++i)
|
|
if (outputChannelData[i] != nullptr)
|
|
FloatVectorOperations::clear (outputChannelData[i], numSamples);
|
|
}
|
|
|
|
private:
|
|
AudioThumbnail& thumbnail;
|
|
TimeSliceThread backgroundThread; // the thread that will write our audio data to disk
|
|
ScopedPointer<AudioFormatWriter::ThreadedWriter> threadedWriter; // the FIFO used to buffer the incoming data
|
|
double sampleRate;
|
|
int64 nextSampleNum;
|
|
|
|
CriticalSection writerLock;
|
|
AudioFormatWriter::ThreadedWriter* volatile activeWriter;
|
|
};
|
|
|
|
//==============================================================================
|
|
class RecordingThumbnail : public Component,
|
|
private ChangeListener
|
|
{
|
|
public:
|
|
RecordingThumbnail()
|
|
: thumbnailCache (10),
|
|
thumbnail (512, formatManager, thumbnailCache),
|
|
displayFullThumb (false)
|
|
{
|
|
formatManager.registerBasicFormats();
|
|
thumbnail.addChangeListener (this);
|
|
}
|
|
|
|
~RecordingThumbnail()
|
|
{
|
|
thumbnail.removeChangeListener (this);
|
|
}
|
|
|
|
AudioThumbnail& getAudioThumbnail() { return thumbnail; }
|
|
|
|
void setDisplayFullThumbnail (bool displayFull)
|
|
{
|
|
displayFullThumb = displayFull;
|
|
repaint();
|
|
}
|
|
|
|
void paint (Graphics& g) override
|
|
{
|
|
g.fillAll (Colours::darkgrey);
|
|
g.setColour (Colours::lightgrey);
|
|
|
|
if (thumbnail.getTotalLength() > 0.0)
|
|
{
|
|
const double endTime = displayFullThumb ? thumbnail.getTotalLength()
|
|
: jmax (30.0, thumbnail.getTotalLength());
|
|
|
|
Rectangle<int> thumbArea (getLocalBounds());
|
|
thumbnail.drawChannels (g, thumbArea.reduced (2), 0.0, endTime, 1.0f);
|
|
}
|
|
else
|
|
{
|
|
g.setFont (14.0f);
|
|
g.drawFittedText ("(No file recorded)", getLocalBounds(), Justification::centred, 2);
|
|
}
|
|
}
|
|
|
|
private:
|
|
AudioFormatManager formatManager;
|
|
AudioThumbnailCache thumbnailCache;
|
|
AudioThumbnail thumbnail;
|
|
bool displayFullThumb;
|
|
|
|
void changeListenerCallback (ChangeBroadcaster* source) override
|
|
{
|
|
if (source == &thumbnail)
|
|
repaint();
|
|
}
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RecordingThumbnail)
|
|
};
|
|
|
|
//==============================================================================
|
|
class AudioRecordingDemo : public Component,
|
|
private Button::Listener
|
|
{
|
|
public:
|
|
AudioRecordingDemo()
|
|
: deviceManager (MainAppWindow::getSharedAudioDeviceManager()),
|
|
recorder (recordingThumbnail.getAudioThumbnail())
|
|
{
|
|
setOpaque (true);
|
|
addAndMakeVisible (liveAudioScroller);
|
|
|
|
addAndMakeVisible (explanationLabel);
|
|
explanationLabel.setText ("This page demonstrates how to record a wave file from the live audio input..\n\nPressing record will start recording a file in your \"Documents\" folder.", dontSendNotification);
|
|
explanationLabel.setFont (Font (15.00f, Font::plain));
|
|
explanationLabel.setJustificationType (Justification::topLeft);
|
|
explanationLabel.setEditable (false, false, false);
|
|
explanationLabel.setColour (TextEditor::textColourId, Colours::black);
|
|
explanationLabel.setColour (TextEditor::backgroundColourId, Colour (0x00000000));
|
|
|
|
addAndMakeVisible (recordButton);
|
|
recordButton.setButtonText ("Record");
|
|
recordButton.addListener (this);
|
|
recordButton.setColour (TextButton::buttonColourId, Colour (0xffff5c5c));
|
|
recordButton.setColour (TextButton::textColourOnId, Colours::black);
|
|
|
|
addAndMakeVisible (recordingThumbnail);
|
|
|
|
deviceManager.addAudioCallback (&liveAudioScroller);
|
|
deviceManager.addAudioCallback (&recorder);
|
|
}
|
|
|
|
~AudioRecordingDemo()
|
|
{
|
|
deviceManager.removeAudioCallback (&recorder);
|
|
deviceManager.removeAudioCallback (&liveAudioScroller);
|
|
}
|
|
|
|
void paint (Graphics& g) override
|
|
{
|
|
g.fillAll (getUIColourIfAvailable (LookAndFeel_V4::ColourScheme::UIColour::windowBackground));
|
|
}
|
|
|
|
void resized() override
|
|
{
|
|
Rectangle<int> area (getLocalBounds());
|
|
liveAudioScroller.setBounds (area.removeFromTop (80).reduced (8));
|
|
recordingThumbnail.setBounds (area.removeFromTop (80).reduced (8));
|
|
recordButton.setBounds (area.removeFromTop (36).removeFromLeft (140).reduced (8));
|
|
explanationLabel.setBounds (area.reduced (8));
|
|
}
|
|
|
|
private:
|
|
AudioDeviceManager& deviceManager;
|
|
LiveScrollingAudioDisplay liveAudioScroller;
|
|
RecordingThumbnail recordingThumbnail;
|
|
AudioRecorder recorder;
|
|
Label explanationLabel;
|
|
TextButton recordButton;
|
|
|
|
void startRecording()
|
|
{
|
|
const File file (File::getSpecialLocation (File::userDocumentsDirectory)
|
|
.getNonexistentChildFile ("JUCE Demo Audio Recording", ".wav"));
|
|
recorder.startRecording (file);
|
|
|
|
recordButton.setButtonText ("Stop");
|
|
recordingThumbnail.setDisplayFullThumbnail (false);
|
|
}
|
|
|
|
void stopRecording()
|
|
{
|
|
recorder.stop();
|
|
recordButton.setButtonText ("Record");
|
|
recordingThumbnail.setDisplayFullThumbnail (true);
|
|
}
|
|
|
|
void buttonClicked (Button* button) override
|
|
{
|
|
if (button == &recordButton)
|
|
{
|
|
if (recorder.isRecording())
|
|
stopRecording();
|
|
else
|
|
startRecording();
|
|
}
|
|
}
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioRecordingDemo)
|
|
};
|
|
|
|
|
|
// This static object will register this demo type in a global list of demos..
|
|
static JuceDemoType<AudioRecordingDemo> demo ("31 Audio: Recording");
|