/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2017 - ROLI Ltd. JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 5 End-User License Agreement and JUCE 5 Privacy Policy (both updated and effective as of the 27th April 2017). End User License Agreement: www.juce.com/juce-5-licence Privacy Policy: www.juce.com/juce-5-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { namespace CDBurnerHelpers { IDiscRecorder* enumCDBurners (StringArray* list, int indexToOpen, IDiscMaster** master) { CoInitialize (0); IDiscMaster* dm; IDiscRecorder* result = nullptr; if (SUCCEEDED (CoCreateInstance (CLSID_MSDiscMasterObj, 0, CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER, IID_IDiscMaster, (void**) &dm))) { if (SUCCEEDED (dm->Open())) { IEnumDiscRecorders* drEnum = nullptr; if (SUCCEEDED (dm->EnumDiscRecorders (&drEnum))) { IDiscRecorder* dr = nullptr; DWORD dummy; int index = 0; while (drEnum->Next (1, &dr, &dummy) == S_OK) { if (indexToOpen == index) { result = dr; break; } else if (list != nullptr) { BSTR path; if (SUCCEEDED (dr->GetPath (&path))) list->add ((const WCHAR*) path); } ++index; dr->Release(); } drEnum->Release(); } if (master == 0) dm->Close(); } if (master != nullptr) *master = dm; else dm->Release(); } return result; } } //============================================================================== class AudioCDBurner::Pimpl : public ComBaseClassHelper , public Timer { public: Pimpl (AudioCDBurner& owner_, IDiscMaster* discMaster_, IDiscRecorder* discRecorder_) : owner (owner_), discMaster (discMaster_), discRecorder (discRecorder_), redbook (0), listener (0), progress (0), shouldCancel (false) { HRESULT hr = discMaster->SetActiveDiscMasterFormat (IID_IRedbookDiscMaster, (void**) &redbook); jassert (SUCCEEDED (hr)); hr = discMaster->SetActiveDiscRecorder (discRecorder); //jassert (SUCCEEDED (hr)); lastState = getDiskState(); startTimer (2000); } ~Pimpl() {} void releaseObjects() { discRecorder->Close(); if (redbook != nullptr) redbook->Release(); discRecorder->Release(); discMaster->Release(); Release(); } JUCE_COMRESULT QueryCancel (boolean* pbCancel) { if (listener != nullptr && ! shouldCancel) shouldCancel = listener->audioCDBurnProgress (progress); *pbCancel = shouldCancel; return S_OK; } JUCE_COMRESULT NotifyBlockProgress (long nCompleted, long nTotal) { progress = nCompleted / (float) nTotal; shouldCancel = listener != nullptr && listener->audioCDBurnProgress (progress); return E_NOTIMPL; } JUCE_COMRESULT NotifyPnPActivity (void) { return E_NOTIMPL; } JUCE_COMRESULT NotifyAddProgress (long /*nCompletedSteps*/, long /*nTotalSteps*/) { return E_NOTIMPL; } JUCE_COMRESULT NotifyTrackProgress (long /*nCurrentTrack*/, long /*nTotalTracks*/) { return E_NOTIMPL; } JUCE_COMRESULT NotifyPreparingBurn (long /*nEstimatedSeconds*/) { return E_NOTIMPL; } JUCE_COMRESULT NotifyClosingDisc (long /*nEstimatedSeconds*/) { return E_NOTIMPL; } JUCE_COMRESULT NotifyBurnComplete (HRESULT /*status*/) { return E_NOTIMPL; } JUCE_COMRESULT NotifyEraseComplete (HRESULT /*status*/) { return E_NOTIMPL; } class ScopedDiscOpener { public: ScopedDiscOpener (Pimpl& p) : pimpl (p) { pimpl.discRecorder->OpenExclusive(); } ~ScopedDiscOpener() { pimpl.discRecorder->Close(); } private: Pimpl& pimpl; JUCE_DECLARE_NON_COPYABLE (ScopedDiscOpener) }; DiskState getDiskState() { const ScopedDiscOpener opener (*this); long type, flags; HRESULT hr = discRecorder->QueryMediaType (&type, &flags); if (FAILED (hr)) return unknown; if (type != 0 && (flags & MEDIA_WRITABLE) != 0) return writableDiskPresent; if (type == 0) return noDisc; return readOnlyDiskPresent; } int getIntProperty (const LPOLESTR name, const int defaultReturn) const { ComSmartPtr prop; if (FAILED (discRecorder->GetRecorderProperties (prop.resetAndGetPointerAddress()))) return defaultReturn; PROPSPEC iPropSpec; iPropSpec.ulKind = PRSPEC_LPWSTR; iPropSpec.lpwstr = name; PROPVARIANT iPropVariant; return FAILED (prop->ReadMultiple (1, &iPropSpec, &iPropVariant)) ? defaultReturn : (int) iPropVariant.lVal; } bool setIntProperty (const LPOLESTR name, const int value) const { ComSmartPtr prop; if (FAILED (discRecorder->GetRecorderProperties (prop.resetAndGetPointerAddress()))) return false; PROPSPEC iPropSpec; iPropSpec.ulKind = PRSPEC_LPWSTR; iPropSpec.lpwstr = name; PROPVARIANT iPropVariant; if (FAILED (prop->ReadMultiple (1, &iPropSpec, &iPropVariant))) return false; iPropVariant.lVal = (long) value; return SUCCEEDED (prop->WriteMultiple (1, &iPropSpec, &iPropVariant, iPropVariant.vt)) && SUCCEEDED (discRecorder->SetRecorderProperties (prop)); } void timerCallback() override { const DiskState state = getDiskState(); if (state != lastState) { lastState = state; owner.sendChangeMessage(); } } AudioCDBurner& owner; DiskState lastState; IDiscMaster* discMaster; IDiscRecorder* discRecorder; IRedbookDiscMaster* redbook; AudioCDBurner::BurnProgressListener* listener; float progress; bool shouldCancel; }; //============================================================================== AudioCDBurner::AudioCDBurner (const int deviceIndex) { IDiscMaster* discMaster = nullptr; IDiscRecorder* discRecorder = CDBurnerHelpers::enumCDBurners (0, deviceIndex, &discMaster); if (discRecorder != nullptr) pimpl.reset (new Pimpl (*this, discMaster, discRecorder)); } AudioCDBurner::~AudioCDBurner() { if (pimpl != nullptr) pimpl.release()->releaseObjects(); } StringArray AudioCDBurner::findAvailableDevices() { StringArray devs; CDBurnerHelpers::enumCDBurners (&devs, -1, 0); return devs; } AudioCDBurner* AudioCDBurner::openDevice (const int deviceIndex) { std::unique_ptr b (new AudioCDBurner (deviceIndex)); if (b->pimpl == 0) b = nullptr; return b.release(); } AudioCDBurner::DiskState AudioCDBurner::getDiskState() const { return pimpl->getDiskState(); } bool AudioCDBurner::isDiskPresent() const { return getDiskState() == writableDiskPresent; } bool AudioCDBurner::openTray() { const Pimpl::ScopedDiscOpener opener (*pimpl); return SUCCEEDED (pimpl->discRecorder->Eject()); } AudioCDBurner::DiskState AudioCDBurner::waitUntilStateChange (int timeOutMilliseconds) { const int64 timeout = Time::currentTimeMillis() + timeOutMilliseconds; DiskState oldState = getDiskState(); DiskState newState = oldState; while (newState == oldState && Time::currentTimeMillis() < timeout) { newState = getDiskState(); Thread::sleep (jmin (250, (int) (timeout - Time::currentTimeMillis()))); } return newState; } Array AudioCDBurner::getAvailableWriteSpeeds() const { Array results; const int maxSpeed = pimpl->getIntProperty (L"MaxWriteSpeed", 1); const int speeds[] = { 1, 2, 4, 8, 12, 16, 20, 24, 32, 40, 64, 80 }; for (int i = 0; i < numElementsInArray (speeds); ++i) if (speeds[i] <= maxSpeed) results.add (speeds[i]); results.addIfNotAlreadyThere (maxSpeed); return results; } bool AudioCDBurner::setBufferUnderrunProtection (const bool shouldBeEnabled) { if (pimpl->getIntProperty (L"BufferUnderrunFreeCapable", 0) == 0) return false; pimpl->setIntProperty (L"EnableBufferUnderrunFree", shouldBeEnabled ? -1 : 0); return pimpl->getIntProperty (L"EnableBufferUnderrunFree", 0) != 0; } int AudioCDBurner::getNumAvailableAudioBlocks() const { long blocksFree = 0; pimpl->redbook->GetAvailableAudioTrackBlocks (&blocksFree); return blocksFree; } String AudioCDBurner::burn (AudioCDBurner::BurnProgressListener* listener, bool ejectDiscAfterwards, bool performFakeBurnForTesting, int writeSpeed) { pimpl->setIntProperty (L"WriteSpeed", writeSpeed > 0 ? writeSpeed : -1); pimpl->listener = listener; pimpl->progress = 0; pimpl->shouldCancel = false; UINT_PTR cookie; HRESULT hr = pimpl->discMaster->ProgressAdvise ((AudioCDBurner::Pimpl*) pimpl.get(), &cookie); hr = pimpl->discMaster->RecordDisc (performFakeBurnForTesting, ejectDiscAfterwards); String error; if (hr != S_OK) { const char* e = "Couldn't open or write to the CD device"; if (hr == IMAPI_E_USERABORT) e = "User cancelled the write operation"; else if (hr == IMAPI_E_MEDIUM_NOTPRESENT || hr == IMAPI_E_TRACKOPEN) e = "No Disk present"; error = e; } pimpl->discMaster->ProgressUnadvise (cookie); pimpl->listener = 0; return error; } bool AudioCDBurner::addAudioTrack (AudioSource* audioSource, int numSamples) { if (audioSource == 0) return false; std::unique_ptr source (audioSource); long bytesPerBlock; HRESULT hr = pimpl->redbook->GetAudioBlockSize (&bytesPerBlock); const int samplesPerBlock = bytesPerBlock / 4; bool ok = true; hr = pimpl->redbook->CreateAudioTrack ((long) numSamples / (bytesPerBlock * 4)); HeapBlock buffer (bytesPerBlock); AudioBuffer sourceBuffer (2, samplesPerBlock); int samplesDone = 0; source->prepareToPlay (samplesPerBlock, 44100.0); while (ok) { { AudioSourceChannelInfo info (&sourceBuffer, 0, samplesPerBlock); sourceBuffer.clear(); source->getNextAudioBlock (info); } buffer.clear (bytesPerBlock); typedef AudioData::Pointer CDSampleFormat; typedef AudioData::Pointer SourceSampleFormat; CDSampleFormat left (buffer, 2); left.convertSamples (SourceSampleFormat (sourceBuffer.getReadPointer (0)), samplesPerBlock); CDSampleFormat right (buffer + 2, 2); right.convertSamples (SourceSampleFormat (sourceBuffer.getReadPointer (1)), samplesPerBlock); hr = pimpl->redbook->AddAudioTrackBlocks (buffer, bytesPerBlock); if (FAILED (hr)) ok = false; samplesDone += samplesPerBlock; if (samplesDone >= numSamples) break; } hr = pimpl->redbook->CloseAudioTrack(); return ok && hr == S_OK; } } // namespace juce