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