1 /*
2   ==============================================================================
3 
4    This file is part of the JUCE library.
5    Copyright (c) 2020 - Raw Material Software Limited
6 
7    JUCE is an open source library subject to commercial or open-source
8    licensing.
9 
10    By using JUCE, you agree to the terms of both the JUCE 6 End-User License
11    Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
12 
13    End User License Agreement: www.juce.com/juce-6-licence
14    Privacy Policy: www.juce.com/juce-privacy-policy
15 
16    Or: You may also use this code under the terms of the GPL v3 (see
17    www.gnu.org/licenses).
18 
19    JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
20    EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
21    DISCLAIMED.
22 
23   ==============================================================================
24 */
25 
26 namespace juce
27 {
28 
29 namespace CDBurnerHelpers
30 {
enumCDBurners(StringArray * list,int indexToOpen,IDiscMaster ** master)31     IDiscRecorder* enumCDBurners (StringArray* list, int indexToOpen, IDiscMaster** master)
32     {
33         CoInitialize (0);
34 
35         IDiscMaster* dm;
36         IDiscRecorder* result = nullptr;
37 
38         if (SUCCEEDED (CoCreateInstance (CLSID_MSDiscMasterObj, 0,
39                                          CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER,
40                                          IID_IDiscMaster,
41                                          (void**) &dm)))
42         {
43             if (SUCCEEDED (dm->Open()))
44             {
45                 IEnumDiscRecorders* drEnum = nullptr;
46 
47                 if (SUCCEEDED (dm->EnumDiscRecorders (&drEnum)))
48                 {
49                     IDiscRecorder* dr = nullptr;
50                     DWORD dummy;
51                     int index = 0;
52 
53                     while (drEnum->Next (1, &dr, &dummy) == S_OK)
54                     {
55                         if (indexToOpen == index)
56                         {
57                             result = dr;
58                             break;
59                         }
60                         else if (list != nullptr)
61                         {
62                             BSTR path;
63 
64                             if (SUCCEEDED (dr->GetPath (&path)))
65                                 list->add ((const WCHAR*) path);
66                         }
67 
68                         ++index;
69                         dr->Release();
70                     }
71 
72                     drEnum->Release();
73                 }
74 
75                 if (master == 0)
76                     dm->Close();
77             }
78 
79             if (master != nullptr)
80                 *master = dm;
81             else
82                 dm->Release();
83         }
84 
85         return result;
86     }
87 }
88 
89 //==============================================================================
90 class AudioCDBurner::Pimpl  : public ComBaseClassHelper <IDiscMasterProgressEvents>,
91                               public Timer
92 {
93 public:
Pimpl(AudioCDBurner & owner_,IDiscMaster * discMaster_,IDiscRecorder * discRecorder_)94     Pimpl (AudioCDBurner& owner_, IDiscMaster* discMaster_, IDiscRecorder* discRecorder_)
95       : owner (owner_), discMaster (discMaster_), discRecorder (discRecorder_), redbook (0),
96         listener (0), progress (0), shouldCancel (false)
97     {
98         HRESULT hr = discMaster->SetActiveDiscMasterFormat (IID_IRedbookDiscMaster, (void**) &redbook);
99         jassert (SUCCEEDED (hr));
100         hr = discMaster->SetActiveDiscRecorder (discRecorder);
101         //jassert (SUCCEEDED (hr));
102 
103         lastState = getDiskState();
104         startTimer (2000);
105     }
106 
~Pimpl()107     ~Pimpl()  {}
108 
releaseObjects()109     void releaseObjects()
110     {
111         discRecorder->Close();
112         if (redbook != nullptr)
113             redbook->Release();
114         discRecorder->Release();
115         discMaster->Release();
116         Release();
117     }
118 
QueryCancel(boolean * pbCancel)119     JUCE_COMRESULT QueryCancel (boolean* pbCancel)
120     {
121         if (listener != nullptr && ! shouldCancel)
122             shouldCancel = listener->audioCDBurnProgress (progress);
123 
124         *pbCancel = shouldCancel;
125 
126         return S_OK;
127     }
128 
NotifyBlockProgress(long nCompleted,long nTotal)129     JUCE_COMRESULT NotifyBlockProgress (long nCompleted, long nTotal)
130     {
131         progress = nCompleted / (float) nTotal;
132         shouldCancel = listener != nullptr && listener->audioCDBurnProgress (progress);
133 
134         return E_NOTIMPL;
135     }
136 
NotifyPnPActivity(void)137     JUCE_COMRESULT NotifyPnPActivity (void)                              { return E_NOTIMPL; }
NotifyAddProgress(long,long)138     JUCE_COMRESULT NotifyAddProgress (long /*nCompletedSteps*/, long /*nTotalSteps*/)    { return E_NOTIMPL; }
NotifyTrackProgress(long,long)139     JUCE_COMRESULT NotifyTrackProgress (long /*nCurrentTrack*/, long /*nTotalTracks*/)   { return E_NOTIMPL; }
NotifyPreparingBurn(long)140     JUCE_COMRESULT NotifyPreparingBurn (long /*nEstimatedSeconds*/)      { return E_NOTIMPL; }
NotifyClosingDisc(long)141     JUCE_COMRESULT NotifyClosingDisc (long /*nEstimatedSeconds*/)        { return E_NOTIMPL; }
NotifyBurnComplete(HRESULT)142     JUCE_COMRESULT NotifyBurnComplete (HRESULT /*status*/)               { return E_NOTIMPL; }
NotifyEraseComplete(HRESULT)143     JUCE_COMRESULT NotifyEraseComplete (HRESULT /*status*/)              { return E_NOTIMPL; }
144 
145     class ScopedDiscOpener
146     {
147     public:
ScopedDiscOpener(Pimpl & p)148         ScopedDiscOpener (Pimpl& p) : pimpl (p) { pimpl.discRecorder->OpenExclusive(); }
~ScopedDiscOpener()149         ~ScopedDiscOpener()                     { pimpl.discRecorder->Close(); }
150 
151     private:
152         Pimpl& pimpl;
153 
154         JUCE_DECLARE_NON_COPYABLE (ScopedDiscOpener)
155     };
156 
getDiskState()157     DiskState getDiskState()
158     {
159         const ScopedDiscOpener opener (*this);
160 
161         long type, flags;
162         HRESULT hr = discRecorder->QueryMediaType (&type, &flags);
163 
164         if (FAILED (hr))
165             return unknown;
166 
167         if (type != 0 && (flags & MEDIA_WRITABLE) != 0)
168             return writableDiskPresent;
169 
170         if (type == 0)
171             return noDisc;
172 
173         return readOnlyDiskPresent;
174     }
175 
getIntProperty(const LPOLESTR name,const int defaultReturn) const176     int getIntProperty (const LPOLESTR name, const int defaultReturn) const
177     {
178         ComSmartPtr<IPropertyStorage> prop;
179         if (FAILED (discRecorder->GetRecorderProperties (prop.resetAndGetPointerAddress())))
180             return defaultReturn;
181 
182         PROPSPEC iPropSpec;
183         iPropSpec.ulKind = PRSPEC_LPWSTR;
184         iPropSpec.lpwstr = name;
185 
186         PROPVARIANT iPropVariant;
187         return FAILED (prop->ReadMultiple (1, &iPropSpec, &iPropVariant))
188                    ? defaultReturn : (int) iPropVariant.lVal;
189     }
190 
setIntProperty(const LPOLESTR name,const int value) const191     bool setIntProperty (const LPOLESTR name, const int value) const
192     {
193         ComSmartPtr<IPropertyStorage> prop;
194         if (FAILED (discRecorder->GetRecorderProperties (prop.resetAndGetPointerAddress())))
195             return false;
196 
197         PROPSPEC iPropSpec;
198         iPropSpec.ulKind = PRSPEC_LPWSTR;
199         iPropSpec.lpwstr = name;
200 
201         PROPVARIANT iPropVariant;
202         if (FAILED (prop->ReadMultiple (1, &iPropSpec, &iPropVariant)))
203             return false;
204 
205         iPropVariant.lVal = (long) value;
206         return SUCCEEDED (prop->WriteMultiple (1, &iPropSpec, &iPropVariant, iPropVariant.vt))
207                 && SUCCEEDED (discRecorder->SetRecorderProperties (prop));
208     }
209 
timerCallback()210     void timerCallback() override
211     {
212         const DiskState state = getDiskState();
213 
214         if (state != lastState)
215         {
216             lastState = state;
217             owner.sendChangeMessage();
218         }
219     }
220 
221     AudioCDBurner& owner;
222     DiskState lastState;
223     IDiscMaster* discMaster;
224     IDiscRecorder* discRecorder;
225     IRedbookDiscMaster* redbook;
226     AudioCDBurner::BurnProgressListener* listener;
227     float progress;
228     bool shouldCancel;
229 };
230 
231 //==============================================================================
AudioCDBurner(const int deviceIndex)232 AudioCDBurner::AudioCDBurner (const int deviceIndex)
233 {
234     IDiscMaster* discMaster = nullptr;
235     IDiscRecorder* discRecorder = CDBurnerHelpers::enumCDBurners (0, deviceIndex, &discMaster);
236 
237     if (discRecorder != nullptr)
238         pimpl.reset (new Pimpl (*this, discMaster, discRecorder));
239 }
240 
~AudioCDBurner()241 AudioCDBurner::~AudioCDBurner()
242 {
243     if (pimpl != nullptr)
244         pimpl.release()->releaseObjects();
245 }
246 
findAvailableDevices()247 StringArray AudioCDBurner::findAvailableDevices()
248 {
249     StringArray devs;
250     CDBurnerHelpers::enumCDBurners (&devs, -1, 0);
251     return devs;
252 }
253 
openDevice(const int deviceIndex)254 AudioCDBurner* AudioCDBurner::openDevice (const int deviceIndex)
255 {
256     std::unique_ptr<AudioCDBurner> b (new AudioCDBurner (deviceIndex));
257 
258     if (b->pimpl == 0)
259         b = nullptr;
260 
261     return b.release();
262 }
263 
getDiskState() const264 AudioCDBurner::DiskState AudioCDBurner::getDiskState() const
265 {
266     return pimpl->getDiskState();
267 }
268 
isDiskPresent() const269 bool AudioCDBurner::isDiskPresent() const
270 {
271     return getDiskState() == writableDiskPresent;
272 }
273 
openTray()274 bool AudioCDBurner::openTray()
275 {
276     const Pimpl::ScopedDiscOpener opener (*pimpl);
277     return SUCCEEDED (pimpl->discRecorder->Eject());
278 }
279 
waitUntilStateChange(int timeOutMilliseconds)280 AudioCDBurner::DiskState AudioCDBurner::waitUntilStateChange (int timeOutMilliseconds)
281 {
282     const int64 timeout = Time::currentTimeMillis() + timeOutMilliseconds;
283     DiskState oldState = getDiskState();
284     DiskState newState = oldState;
285 
286     while (newState == oldState && Time::currentTimeMillis() < timeout)
287     {
288         newState = getDiskState();
289         Thread::sleep (jmin (250, (int) (timeout - Time::currentTimeMillis())));
290     }
291 
292     return newState;
293 }
294 
getAvailableWriteSpeeds() const295 Array<int> AudioCDBurner::getAvailableWriteSpeeds() const
296 {
297     Array<int> results;
298     const int maxSpeed = pimpl->getIntProperty (L"MaxWriteSpeed", 1);
299     const int speeds[] = { 1, 2, 4, 8, 12, 16, 20, 24, 32, 40, 64, 80 };
300 
301     for (int i = 0; i < numElementsInArray (speeds); ++i)
302         if (speeds[i] <= maxSpeed)
303             results.add (speeds[i]);
304 
305     results.addIfNotAlreadyThere (maxSpeed);
306     return results;
307 }
308 
setBufferUnderrunProtection(const bool shouldBeEnabled)309 bool AudioCDBurner::setBufferUnderrunProtection (const bool shouldBeEnabled)
310 {
311     if (pimpl->getIntProperty (L"BufferUnderrunFreeCapable", 0) == 0)
312         return false;
313 
314     pimpl->setIntProperty (L"EnableBufferUnderrunFree", shouldBeEnabled ? -1 : 0);
315     return pimpl->getIntProperty (L"EnableBufferUnderrunFree", 0) != 0;
316 }
317 
getNumAvailableAudioBlocks() const318 int AudioCDBurner::getNumAvailableAudioBlocks() const
319 {
320     long blocksFree = 0;
321     pimpl->redbook->GetAvailableAudioTrackBlocks (&blocksFree);
322     return blocksFree;
323 }
324 
burn(AudioCDBurner::BurnProgressListener * listener,bool ejectDiscAfterwards,bool performFakeBurnForTesting,int writeSpeed)325 String AudioCDBurner::burn (AudioCDBurner::BurnProgressListener* listener, bool ejectDiscAfterwards,
326                             bool performFakeBurnForTesting, int writeSpeed)
327 {
328     pimpl->setIntProperty (L"WriteSpeed", writeSpeed > 0 ? writeSpeed : -1);
329 
330     pimpl->listener = listener;
331     pimpl->progress = 0;
332     pimpl->shouldCancel = false;
333 
334     UINT_PTR cookie;
335     HRESULT hr = pimpl->discMaster->ProgressAdvise ((AudioCDBurner::Pimpl*) pimpl.get(), &cookie);
336 
337     hr = pimpl->discMaster->RecordDisc (performFakeBurnForTesting,
338                                         ejectDiscAfterwards);
339 
340     String error;
341     if (hr != S_OK)
342     {
343         const char* e = "Couldn't open or write to the CD device";
344 
345         if (hr == IMAPI_E_USERABORT)
346             e = "User cancelled the write operation";
347         else if (hr == IMAPI_E_MEDIUM_NOTPRESENT || hr == IMAPI_E_TRACKOPEN)
348             e = "No Disk present";
349 
350         error = e;
351     }
352 
353     pimpl->discMaster->ProgressUnadvise (cookie);
354     pimpl->listener = 0;
355 
356     return error;
357 }
358 
addAudioTrack(AudioSource * audioSource,int numSamples)359 bool AudioCDBurner::addAudioTrack (AudioSource* audioSource, int numSamples)
360 {
361     if (audioSource == 0)
362         return false;
363 
364     std::unique_ptr<AudioSource> source (audioSource);
365 
366     long bytesPerBlock;
367     HRESULT hr = pimpl->redbook->GetAudioBlockSize (&bytesPerBlock);
368 
369     const int samplesPerBlock = bytesPerBlock / 4;
370     bool ok = true;
371 
372     hr = pimpl->redbook->CreateAudioTrack ((long) numSamples / (bytesPerBlock * 4));
373 
374     HeapBlock<byte> buffer (bytesPerBlock);
375     AudioBuffer<float> sourceBuffer (2, samplesPerBlock);
376     int samplesDone = 0;
377 
378     source->prepareToPlay (samplesPerBlock, 44100.0);
379 
380     while (ok)
381     {
382         {
383             AudioSourceChannelInfo info (&sourceBuffer, 0, samplesPerBlock);
384             sourceBuffer.clear();
385 
386             source->getNextAudioBlock (info);
387         }
388 
389         buffer.clear (bytesPerBlock);
390 
391         typedef AudioData::Pointer <AudioData::Int16, AudioData::LittleEndian,
392                                     AudioData::Interleaved, AudioData::NonConst> CDSampleFormat;
393 
394         typedef AudioData::Pointer <AudioData::Float32, AudioData::NativeEndian,
395                                     AudioData::NonInterleaved, AudioData::Const> SourceSampleFormat;
396 
397         CDSampleFormat left (buffer, 2);
398         left.convertSamples (SourceSampleFormat (sourceBuffer.getReadPointer (0)), samplesPerBlock);
399         CDSampleFormat right (buffer + 2, 2);
400         right.convertSamples (SourceSampleFormat (sourceBuffer.getReadPointer (1)), samplesPerBlock);
401 
402         hr = pimpl->redbook->AddAudioTrackBlocks (buffer, bytesPerBlock);
403 
404         if (FAILED (hr))
405             ok = false;
406 
407         samplesDone += samplesPerBlock;
408 
409         if (samplesDone >= numSamples)
410             break;
411     }
412 
413     hr = pimpl->redbook->CloseAudioTrack();
414     return ok && hr == S_OK;
415 }
416 
417 } // namespace juce
418