419 lines
13 KiB
C++
419 lines
13 KiB
C++
|
/*
|
||
|
==============================================================================
|
||
|
|
||
|
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 <IDiscMasterProgressEvents>,
|
||
|
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<IPropertyStorage> 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<IPropertyStorage> 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<AudioCDBurner> 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<int> AudioCDBurner::getAvailableWriteSpeeds() const
|
||
|
{
|
||
|
Array<int> 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<AudioSource> 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<byte> buffer (bytesPerBlock);
|
||
|
AudioBuffer<float> 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 <AudioData::Int16, AudioData::LittleEndian,
|
||
|
AudioData::Interleaved, AudioData::NonConst> CDSampleFormat;
|
||
|
|
||
|
typedef AudioData::Pointer <AudioData::Float32, AudioData::NativeEndian,
|
||
|
AudioData::NonInterleaved, AudioData::Const> 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
|