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