1 /* 2 ============================================================================== 3 4 This file is part of the JUCE examples. 5 Copyright (c) 2020 - Raw Material Software Limited 6 7 The code included in this file is provided under the terms of the ISC license 8 http://www.isc.org/downloads/software-support-policy/isc-license. Permission 9 To use, copy, modify, and/or distribute this software for any purpose with or 10 without fee is hereby granted provided that the above copyright notice and 11 this permission notice appear in all copies. 12 13 THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, 14 WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR 15 PURPOSE, ARE DISCLAIMED. 16 17 ============================================================================== 18 */ 19 20 /******************************************************************************* 21 The block below describes the properties of this PIP. A PIP is a short snippet 22 of code that can be read by the Projucer and used to generate a JUCE project. 23 24 BEGIN_JUCE_PIP_METADATA 25 26 name: AudioRecordingDemo 27 version: 1.0.0 28 vendor: JUCE 29 website: http://juce.com 30 description: Records audio to a file. 31 32 dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats, 33 juce_audio_processors, juce_audio_utils, juce_core, 34 juce_data_structures, juce_events, juce_graphics, 35 juce_gui_basics, juce_gui_extra 36 exporters: xcode_mac, vs2019, linux_make, androidstudio, xcode_iphone 37 38 moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1 39 40 type: Component 41 mainClass: AudioRecordingDemo 42 43 useLocalCopy: 1 44 45 END_JUCE_PIP_METADATA 46 47 *******************************************************************************/ 48 49 #pragma once 50 51 #include "../Assets/DemoUtilities.h" 52 #include "../Assets/AudioLiveScrollingDisplay.h" 53 54 //============================================================================== 55 /** A simple class that acts as an AudioIODeviceCallback and writes the 56 incoming audio data to a WAV file. 57 */ 58 class AudioRecorder : public AudioIODeviceCallback 59 { 60 public: AudioRecorder(AudioThumbnail & thumbnailToUpdate)61 AudioRecorder (AudioThumbnail& thumbnailToUpdate) 62 : thumbnail (thumbnailToUpdate) 63 { 64 backgroundThread.startThread(); 65 } 66 ~AudioRecorder()67 ~AudioRecorder() override 68 { 69 stop(); 70 } 71 72 //============================================================================== startRecording(const File & file)73 void startRecording (const File& file) 74 { 75 stop(); 76 77 if (sampleRate > 0) 78 { 79 // Create an OutputStream to write to our destination file... 80 file.deleteFile(); 81 82 if (auto fileStream = std::unique_ptr<FileOutputStream> (file.createOutputStream())) 83 { 84 // Now create a WAV writer object that writes to our output stream... 85 WavAudioFormat wavFormat; 86 87 if (auto writer = wavFormat.createWriterFor (fileStream.get(), sampleRate, 1, 16, {}, 0)) 88 { 89 fileStream.release(); // (passes responsibility for deleting the stream to the writer object that is now using it) 90 91 // Now we'll create one of these helper objects which will act as a FIFO buffer, and will 92 // write the data to disk on our background thread. 93 threadedWriter.reset (new AudioFormatWriter::ThreadedWriter (writer, backgroundThread, 32768)); 94 95 // Reset our recording thumbnail 96 thumbnail.reset (writer->getNumChannels(), writer->getSampleRate()); 97 nextSampleNum = 0; 98 99 // And now, swap over our active writer pointer so that the audio callback will start using it.. 100 const ScopedLock sl (writerLock); 101 activeWriter = threadedWriter.get(); 102 } 103 } 104 } 105 } 106 stop()107 void stop() 108 { 109 // First, clear this pointer to stop the audio callback from using our writer object.. 110 { 111 const ScopedLock sl (writerLock); 112 activeWriter = nullptr; 113 } 114 115 // Now we can delete the writer object. It's done in this order because the deletion could 116 // take a little time while remaining data gets flushed to disk, so it's best to avoid blocking 117 // the audio callback while this happens. 118 threadedWriter.reset(); 119 } 120 isRecording()121 bool isRecording() const 122 { 123 return activeWriter.load() != nullptr; 124 } 125 126 //============================================================================== audioDeviceAboutToStart(AudioIODevice * device)127 void audioDeviceAboutToStart (AudioIODevice* device) override 128 { 129 sampleRate = device->getCurrentSampleRate(); 130 } 131 audioDeviceStopped()132 void audioDeviceStopped() override 133 { 134 sampleRate = 0; 135 } 136 audioDeviceIOCallback(const float ** inputChannelData,int numInputChannels,float ** outputChannelData,int numOutputChannels,int numSamples)137 void audioDeviceIOCallback (const float** inputChannelData, int numInputChannels, 138 float** outputChannelData, int numOutputChannels, 139 int numSamples) override 140 { 141 const ScopedLock sl (writerLock); 142 143 if (activeWriter.load() != nullptr && numInputChannels >= thumbnail.getNumChannels()) 144 { 145 activeWriter.load()->write (inputChannelData, numSamples); 146 147 // Create an AudioBuffer to wrap our incoming data, note that this does no allocations or copies, it simply references our input data 148 AudioBuffer<float> buffer (const_cast<float**> (inputChannelData), thumbnail.getNumChannels(), numSamples); 149 thumbnail.addBlock (nextSampleNum, buffer, 0, numSamples); 150 nextSampleNum += numSamples; 151 } 152 153 // We need to clear the output buffers, in case they're full of junk.. 154 for (int i = 0; i < numOutputChannels; ++i) 155 if (outputChannelData[i] != nullptr) 156 FloatVectorOperations::clear (outputChannelData[i], numSamples); 157 } 158 159 private: 160 AudioThumbnail& thumbnail; 161 TimeSliceThread backgroundThread { "Audio Recorder Thread" }; // the thread that will write our audio data to disk 162 std::unique_ptr<AudioFormatWriter::ThreadedWriter> threadedWriter; // the FIFO used to buffer the incoming data 163 double sampleRate = 0.0; 164 int64 nextSampleNum = 0; 165 166 CriticalSection writerLock; 167 std::atomic<AudioFormatWriter::ThreadedWriter*> activeWriter { nullptr }; 168 }; 169 170 //============================================================================== 171 class RecordingThumbnail : public Component, 172 private ChangeListener 173 { 174 public: RecordingThumbnail()175 RecordingThumbnail() 176 { 177 formatManager.registerBasicFormats(); 178 thumbnail.addChangeListener (this); 179 } 180 ~RecordingThumbnail()181 ~RecordingThumbnail() override 182 { 183 thumbnail.removeChangeListener (this); 184 } 185 getAudioThumbnail()186 AudioThumbnail& getAudioThumbnail() { return thumbnail; } 187 setDisplayFullThumbnail(bool displayFull)188 void setDisplayFullThumbnail (bool displayFull) 189 { 190 displayFullThumb = displayFull; 191 repaint(); 192 } 193 paint(Graphics & g)194 void paint (Graphics& g) override 195 { 196 g.fillAll (Colours::darkgrey); 197 g.setColour (Colours::lightgrey); 198 199 if (thumbnail.getTotalLength() > 0.0) 200 { 201 auto endTime = displayFullThumb ? thumbnail.getTotalLength() 202 : jmax (30.0, thumbnail.getTotalLength()); 203 204 auto thumbArea = getLocalBounds(); 205 thumbnail.drawChannels (g, thumbArea.reduced (2), 0.0, endTime, 1.0f); 206 } 207 else 208 { 209 g.setFont (14.0f); 210 g.drawFittedText ("(No file recorded)", getLocalBounds(), Justification::centred, 2); 211 } 212 } 213 214 private: 215 AudioFormatManager formatManager; 216 AudioThumbnailCache thumbnailCache { 10 }; 217 AudioThumbnail thumbnail { 512, formatManager, thumbnailCache }; 218 219 bool displayFullThumb = false; 220 changeListenerCallback(ChangeBroadcaster * source)221 void changeListenerCallback (ChangeBroadcaster* source) override 222 { 223 if (source == &thumbnail) 224 repaint(); 225 } 226 227 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RecordingThumbnail) 228 }; 229 230 //============================================================================== 231 class AudioRecordingDemo : public Component 232 { 233 public: AudioRecordingDemo()234 AudioRecordingDemo() 235 { 236 setOpaque (true); 237 addAndMakeVisible (liveAudioScroller); 238 239 addAndMakeVisible (explanationLabel); 240 explanationLabel.setFont (Font (15.0f, Font::plain)); 241 explanationLabel.setJustificationType (Justification::topLeft); 242 explanationLabel.setEditable (false, false, false); 243 explanationLabel.setColour (TextEditor::textColourId, Colours::black); 244 explanationLabel.setColour (TextEditor::backgroundColourId, Colour (0x00000000)); 245 246 addAndMakeVisible (recordButton); 247 recordButton.setColour (TextButton::buttonColourId, Colour (0xffff5c5c)); 248 recordButton.setColour (TextButton::textColourOnId, Colours::black); 249 250 recordButton.onClick = [this] 251 { 252 if (recorder.isRecording()) 253 stopRecording(); 254 else 255 startRecording(); 256 }; 257 258 addAndMakeVisible (recordingThumbnail); 259 260 #ifndef JUCE_DEMO_RUNNER 261 RuntimePermissions::request (RuntimePermissions::recordAudio, 262 [this] (bool granted) 263 { 264 int numInputChannels = granted ? 2 : 0; 265 audioDeviceManager.initialise (numInputChannels, 2, nullptr, true, {}, nullptr); 266 }); 267 #endif 268 269 audioDeviceManager.addAudioCallback (&liveAudioScroller); 270 audioDeviceManager.addAudioCallback (&recorder); 271 272 setSize (500, 500); 273 } 274 ~AudioRecordingDemo()275 ~AudioRecordingDemo() override 276 { 277 audioDeviceManager.removeAudioCallback (&recorder); 278 audioDeviceManager.removeAudioCallback (&liveAudioScroller); 279 } 280 paint(Graphics & g)281 void paint (Graphics& g) override 282 { 283 g.fillAll (getUIColourIfAvailable (LookAndFeel_V4::ColourScheme::UIColour::windowBackground)); 284 } 285 resized()286 void resized() override 287 { 288 auto area = getLocalBounds(); 289 290 liveAudioScroller .setBounds (area.removeFromTop (80).reduced (8)); 291 recordingThumbnail.setBounds (area.removeFromTop (80).reduced (8)); 292 recordButton .setBounds (area.removeFromTop (36).removeFromLeft (140).reduced (8)); 293 explanationLabel .setBounds (area.reduced (8)); 294 } 295 296 private: 297 // if this PIP is running inside the demo runner, we'll use the shared device manager instead 298 #ifndef JUCE_DEMO_RUNNER 299 AudioDeviceManager audioDeviceManager; 300 #else 301 AudioDeviceManager& audioDeviceManager { getSharedAudioDeviceManager (1, 0) }; 302 #endif 303 304 LiveScrollingAudioDisplay liveAudioScroller; 305 RecordingThumbnail recordingThumbnail; 306 AudioRecorder recorder { recordingThumbnail.getAudioThumbnail() }; 307 308 Label explanationLabel { {}, "This page demonstrates how to record a wave file from the live audio input..\n\n" 309 #if (JUCE_ANDROID || JUCE_IOS) 310 "After you are done with your recording you can share with other apps." 311 #else 312 "Pressing record will start recording a file in your \"Documents\" folder." 313 #endif 314 }; 315 TextButton recordButton { "Record" }; 316 File lastRecording; 317 startRecording()318 void startRecording() 319 { 320 if (! RuntimePermissions::isGranted (RuntimePermissions::writeExternalStorage)) 321 { 322 SafePointer<AudioRecordingDemo> safeThis (this); 323 324 RuntimePermissions::request (RuntimePermissions::writeExternalStorage, 325 [safeThis] (bool granted) mutable 326 { 327 if (granted) 328 safeThis->startRecording(); 329 }); 330 return; 331 } 332 333 #if (JUCE_ANDROID || JUCE_IOS) 334 auto parentDir = File::getSpecialLocation (File::tempDirectory); 335 #else 336 auto parentDir = File::getSpecialLocation (File::userDocumentsDirectory); 337 #endif 338 339 lastRecording = parentDir.getNonexistentChildFile ("JUCE Demo Audio Recording", ".wav"); 340 341 recorder.startRecording (lastRecording); 342 343 recordButton.setButtonText ("Stop"); 344 recordingThumbnail.setDisplayFullThumbnail (false); 345 } 346 stopRecording()347 void stopRecording() 348 { 349 recorder.stop(); 350 351 #if JUCE_CONTENT_SHARING 352 SafePointer<AudioRecordingDemo> safeThis (this); 353 File fileToShare = lastRecording; 354 355 ContentSharer::getInstance()->shareFiles (Array<URL> ({URL (fileToShare)}), 356 [safeThis, fileToShare] (bool success, const String& error) 357 { 358 if (fileToShare.existsAsFile()) 359 fileToShare.deleteFile(); 360 361 if (! success && error.isNotEmpty()) 362 { 363 NativeMessageBox::showMessageBoxAsync (AlertWindow::WarningIcon, 364 "Sharing Error", 365 error); 366 } 367 }); 368 #endif 369 370 lastRecording = File(); 371 recordButton.setButtonText ("Record"); 372 recordingThumbnail.setDisplayFullThumbnail (true); 373 } 374 375 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioRecordingDemo) 376 }; 377