/* ============================================================================== This file is part of the JUCE framework examples. Copyright (c) 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" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ============================================================================== */ /******************************************************************************* 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: AudioRecordingDemo 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, juce_audio_processors_headless exporters: xcode_mac, vs2022, vs2026, linux_make, androidstudio, xcode_iphone moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1 type: Component mainClass: AudioRecordingDemo useLocalCopy: 1 END_JUCE_PIP_METADATA *******************************************************************************/ #pragma once #include "../Assets/DemoUtilities.h" #include "../Assets/AudioLiveScrollingDisplay.h" //============================================================================== /** A simple class that acts as an AudioIODeviceCallback and writes the incoming audio data to a WAV file. */ class AudioRecorder final : public AudioIODeviceCallback { public: AudioRecorder (AudioThumbnail& thumbnailToUpdate) : thumbnail (thumbnailToUpdate) { backgroundThread.startThread(); } ~AudioRecorder() override { stop(); } //============================================================================== void startRecording (const File& file) { stop(); if (sampleRate > 0) { // Create an OutputStream to write to our destination file... file.deleteFile(); if (std::unique_ptr fileStream { file.createOutputStream() }) { // Now create a WAV writer object that writes to our output stream... WavAudioFormat wavFormat; using Opts = AudioFormatWriterOptions; if (auto writer = wavFormat.createWriterFor (fileStream, Opts{}.withSampleRate (sampleRate) .withNumChannels (1) .withBitsPerSample (16))) { auto* writerPtr = writer.get(); // 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 AudioFormatWriter::ThreadedWriter (writer.release(), backgroundThread, 32768)); // Reset our recording thumbnail thumbnail.reset (writerPtr->getNumChannels(), writerPtr->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.get(); } } } } 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.load() != nullptr; } //============================================================================== void audioDeviceAboutToStart (AudioIODevice* device) override { sampleRate = device->getCurrentSampleRate(); } void audioDeviceStopped() override { sampleRate = 0; } void audioDeviceIOCallbackWithContext (const float* const* inputChannelData, int numInputChannels, float* const* outputChannelData, int numOutputChannels, int numSamples, const AudioIODeviceCallbackContext& context) override { ignoreUnused (context); const ScopedLock sl (writerLock); if (activeWriter.load() != nullptr && numInputChannels >= thumbnail.getNumChannels()) { activeWriter.load()->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 buffer (const_cast (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 { "Audio Recorder Thread" }; // the thread that will write our audio data to disk std::unique_ptr threadedWriter; // the FIFO used to buffer the incoming data double sampleRate = 0.0; int64 nextSampleNum = 0; CriticalSection writerLock; std::atomic activeWriter { nullptr }; }; //============================================================================== class RecordingThumbnail final : public Component, private ChangeListener { public: RecordingThumbnail() { formatManager.registerBasicFormats(); thumbnail.addChangeListener (this); } ~RecordingThumbnail() override { 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) { auto endTime = displayFullThumb ? thumbnail.getTotalLength() : jmax (30.0, thumbnail.getTotalLength()); auto 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 { 10 }; AudioThumbnail thumbnail { 512, formatManager, thumbnailCache }; bool displayFullThumb = false; void changeListenerCallback (ChangeBroadcaster* source) override { if (source == &thumbnail) repaint(); } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RecordingThumbnail) }; //============================================================================== class AudioRecordingDemo final : public Component { public: AudioRecordingDemo() { setOpaque (true); addAndMakeVisible (liveAudioScroller); addAndMakeVisible (explanationLabel); explanationLabel.setFont (FontOptions (15.0f, 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.setColour (TextButton::buttonColourId, Colour (0xffff5c5c)); recordButton.setColour (TextButton::textColourOnId, Colours::black); recordButton.onClick = [this] { if (recorder.isRecording()) stopRecording(); else startRecording(); }; addAndMakeVisible (recordingThumbnail); #ifndef JUCE_DEMO_RUNNER RuntimePermissions::request (RuntimePermissions::recordAudio, [this] (bool granted) { int numInputChannels = granted ? 2 : 0; audioDeviceManager.initialise (numInputChannels, 2, nullptr, true, {}, nullptr); }); #endif audioDeviceManager.addAudioCallback (&liveAudioScroller); audioDeviceManager.addAudioCallback (&recorder); setSize (500, 500); } ~AudioRecordingDemo() override { audioDeviceManager.removeAudioCallback (&recorder); audioDeviceManager.removeAudioCallback (&liveAudioScroller); } void paint (Graphics& g) override { g.fillAll (getUIColourIfAvailable (LookAndFeel_V4::ColourScheme::UIColour::windowBackground)); } void resized() override { auto 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: // if this PIP is running inside the demo runner, we'll use the shared device manager instead #ifndef JUCE_DEMO_RUNNER AudioDeviceManager audioDeviceManager; #else AudioDeviceManager& audioDeviceManager { getSharedAudioDeviceManager (1, 0) }; #endif LiveScrollingAudioDisplay liveAudioScroller; RecordingThumbnail recordingThumbnail; AudioRecorder recorder { recordingThumbnail.getAudioThumbnail() }; Label explanationLabel { {}, "This page demonstrates how to record a wave file from the live audio input.\n\n" "After you are done with your recording you can choose where to save it." }; TextButton recordButton { "Record" }; File lastRecording; FileChooser chooser { "Output file...", File::getCurrentWorkingDirectory().getChildFile ("recording.wav"), "*.wav" }; void startRecording() { if (! RuntimePermissions::isGranted (RuntimePermissions::writeExternalStorage)) { SafePointer safeThis (this); RuntimePermissions::request (RuntimePermissions::writeExternalStorage, [safeThis] (bool granted) mutable { if (granted) safeThis->startRecording(); }); return; } #if (JUCE_ANDROID || JUCE_IOS) auto parentDir = File::getSpecialLocation (File::tempDirectory); #else auto parentDir = File::getSpecialLocation (File::userDocumentsDirectory); #endif lastRecording = parentDir.getNonexistentChildFile ("JUCE Demo Audio Recording", ".wav"); recorder.startRecording (lastRecording); recordButton.setButtonText ("Stop"); recordingThumbnail.setDisplayFullThumbnail (false); } void stopRecording() { recorder.stop(); chooser.launchAsync ( FileBrowserComponent::saveMode | FileBrowserComponent::canSelectFiles | FileBrowserComponent::warnAboutOverwriting, [this] (const FileChooser& c) { if (FileInputStream inputStream (lastRecording); inputStream.openedOk()) if (const auto outputStream = makeOutputStream (c.getURLResult())) outputStream->writeFromInputStream (inputStream, -1); recordButton.setButtonText ("Record"); recordingThumbnail.setDisplayFullThumbnail (true); }); } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioRecordingDemo) };