First Commit

This commit is contained in:
Legaeli
2025-10-22 09:20:23 +02:00
commit a36f2ca925
4393 changed files with 1570219 additions and 0 deletions
@@ -0,0 +1,470 @@
/*
==============================================================================
This file is part of the JUCE framework.
Copyright (c) Raw Material Software Limited
JUCE is an open source framework subject to commercial or open source
licensing.
By downloading, installing, or using the JUCE framework, or combining the
JUCE framework with any other source code, object code, content or any other
copyrightable work, you agree to the terms of the JUCE End User Licence
Agreement, and all incorporated terms including the JUCE Privacy Policy and
the JUCE Website Terms of Service, as applicable, which will bind you. If you
do not agree to the terms of these agreements, we will not license the JUCE
framework to you, and you must discontinue the installation or download
process and cease use of the JUCE framework.
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
Or:
You may also use this code under the terms of the AGPLv3:
https://www.gnu.org/licenses/agpl-3.0.en.html
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
==============================================================================
*/
namespace juce
{
const int kilobytesPerSecond1x = 176;
struct AudioTrackProducerClass final : public ObjCClass<NSObject>
{
AudioTrackProducerClass() : ObjCClass<NSObject> ("JUCEAudioTrackProducer_")
{
addIvar<AudioSourceHolder*> ("source");
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
addMethod (@selector (initWithAudioSourceHolder:), initWithAudioSourceHolder);
addMethod (@selector (verifyDataForTrack:intoBuffer:length:atAddress:blockSize:ioFlags:), produceDataForTrack);
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
addMethod (@selector (cleanupTrackAfterBurn:), cleanupTrackAfterBurn);
addMethod (@selector (cleanupTrackAfterVerification:), cleanupTrackAfterVerification);
addMethod (@selector (estimateLengthOfTrack:), estimateLengthOfTrack);
addMethod (@selector (prepareTrack:forBurn:toMedia:), prepareTrack);
addMethod (@selector (prepareTrackForVerification:), prepareTrackForVerification);
addMethod (@selector (produceDataForTrack:intoBuffer:length:atAddress:blockSize:ioFlags:),
produceDataForTrack);
addMethod (@selector (producePreGapForTrack:intoBuffer:length:atAddress:blockSize:ioFlags:),
produceDataForTrack);
registerClass();
}
struct AudioSourceHolder
{
AudioSourceHolder (AudioSource* s, int numFrames)
: source (s), readPosition (0), lengthInFrames (numFrames)
{
}
~AudioSourceHolder()
{
if (source != nullptr)
source->releaseResources();
}
std::unique_ptr<AudioSource> source;
int readPosition, lengthInFrames;
};
private:
static id initWithAudioSourceHolder (id self, SEL, AudioSourceHolder* source)
{
self = sendSuperclassMessage<id> (self, @selector (init));
object_setInstanceVariable (self, "source", source);
return self;
}
static AudioSourceHolder* getSource (id self)
{
return getIvar<AudioSourceHolder*> (self, "source");
}
static void dealloc (id self, SEL)
{
delete getSource (self);
sendSuperclassMessage<void> (self, @selector (dealloc));
}
static void cleanupTrackAfterBurn (id, SEL, DRTrack*) {}
static BOOL cleanupTrackAfterVerification (id, SEL, DRTrack*) { return true; }
static uint64_t estimateLengthOfTrack (id self, SEL, DRTrack*)
{
return static_cast<uint64_t> (getSource (self)->lengthInFrames);
}
static BOOL prepareTrack (id self, SEL, DRTrack*, DRBurn*, NSDictionary*)
{
if (AudioSourceHolder* const source = getSource (self))
{
source->source->prepareToPlay (44100 / 75, 44100);
source->readPosition = 0;
}
return true;
}
static BOOL prepareTrackForVerification (id self, SEL, DRTrack*)
{
if (AudioSourceHolder* const source = getSource (self))
source->source->prepareToPlay (44100 / 75, 44100);
return true;
}
static uint32_t produceDataForTrack (id self, SEL, DRTrack*, char* buffer,
uint32_t bufferLength, uint64_t /*address*/,
uint32_t /*blockSize*/, uint32_t* /*flags*/)
{
if (AudioSourceHolder* const source = getSource (self))
{
const int numSamples = jmin ((int) bufferLength / 4,
(source->lengthInFrames * (44100 / 75)) - source->readPosition);
if (numSamples > 0)
{
AudioBuffer<float> tempBuffer (2, numSamples);
AudioSourceChannelInfo info (tempBuffer);
source->source->getNextAudioBlock (info);
AudioData::interleaveSamples (AudioData::NonInterleavedSource<AudioData::Float32, AudioData::NativeEndian> { tempBuffer.getArrayOfReadPointers(), 2 },
AudioData::InterleavedDest<AudioData::Int16, AudioData::LittleEndian> { reinterpret_cast<uint16*> (buffer), 2 },
numSamples);
source->readPosition += numSamples;
}
return static_cast<uint32_t> (numSamples * 4);
}
return 0;
}
static uint32_t producePreGapForTrack (id, SEL, DRTrack*, char* buffer,
uint32_t bufferLength, uint64_t /*address*/,
uint32_t /*blockSize*/, uint32_t* /*flags*/)
{
zeromem (buffer, bufferLength);
return bufferLength;
}
static BOOL verifyDataForTrack (id, SEL, DRTrack*, const char*,
uint32_t /*bufferLength*/, uint64_t /*address*/,
uint32_t /*blockSize*/, uint32_t* /*flags*/)
{
return true;
}
};
struct OpenDiskDevice
{
OpenDiskDevice (DRDevice* d)
: device (d),
tracks ([[NSMutableArray alloc] init]),
underrunProtection (true)
{
}
~OpenDiskDevice()
{
[tracks release];
}
void addSourceTrack (AudioSource* source, int numSamples)
{
if (source != nullptr)
{
const int numFrames = (numSamples + 587) / 588;
static AudioTrackProducerClass cls;
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
NSObject* producer = [cls.createInstance() performSelector: @selector (initWithAudioSourceHolder:)
withObject: (id) new AudioTrackProducerClass::AudioSourceHolder (source, numFrames)];
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
DRTrack* track = [[DRTrack alloc] initWithProducer: producer];
{
NSMutableDictionary* p = [[track properties] mutableCopy];
[p setObject: [DRMSF msfWithFrames: static_cast<UInt32> (numFrames)] forKey: DRTrackLengthKey];
[p setObject: [NSNumber numberWithUnsignedShort: 2352] forKey: DRBlockSizeKey];
[p setObject: [NSNumber numberWithInt: 0] forKey: DRDataFormKey];
[p setObject: [NSNumber numberWithInt: 0] forKey: DRBlockTypeKey];
[p setObject: [NSNumber numberWithInt: 0] forKey: DRTrackModeKey];
[p setObject: [NSNumber numberWithInt: 0] forKey: DRSessionFormatKey];
[track setProperties: p];
[p release];
}
[tracks addObject: track];
[track release];
[producer release];
}
}
String burn (AudioCDBurner::BurnProgressListener* listener,
bool shouldEject, bool peformFakeBurnForTesting, int burnSpeed)
{
DRBurn* burn = [DRBurn burnForDevice: device];
if (! [device acquireExclusiveAccess])
return "Couldn't open or write to the CD device";
[device acquireMediaReservation];
NSMutableDictionary* d = [[burn properties] mutableCopy];
[d autorelease];
[d setObject: [NSNumber numberWithBool: peformFakeBurnForTesting] forKey: DRBurnTestingKey];
[d setObject: [NSNumber numberWithBool: false] forKey: DRBurnVerifyDiscKey];
[d setObject: (shouldEject ? DRBurnCompletionActionEject : DRBurnCompletionActionMount) forKey: DRBurnCompletionActionKey];
if (burnSpeed > 0)
[d setObject: [NSNumber numberWithFloat: burnSpeed * kilobytesPerSecond1x] forKey: DRBurnRequestedSpeedKey];
if (! underrunProtection)
[d setObject: [NSNumber numberWithBool: false] forKey: DRBurnUnderrunProtectionKey];
[burn setProperties: d];
[burn writeLayout: tracks];
for (;;)
{
Thread::sleep (300);
float progress = [[[burn status] objectForKey: DRStatusPercentCompleteKey] floatValue];
if (listener != nullptr && listener->audioCDBurnProgress (progress))
{
[burn abort];
return "User cancelled the write operation";
}
if ([[[burn status] objectForKey: DRStatusStateKey] isEqualTo: DRStatusStateFailed])
return "Write operation failed";
if ([[[burn status] objectForKey: DRStatusStateKey] isEqualTo: DRStatusStateDone])
break;
NSString* err = (NSString*) [[[burn status] objectForKey: DRErrorStatusKey]
objectForKey: DRErrorStatusErrorStringKey];
if ([err length] > 0)
return nsStringToJuce (err);
}
[device releaseMediaReservation];
[device releaseExclusiveAccess];
return {};
}
DRDevice* device;
NSMutableArray* tracks;
bool underrunProtection;
};
//==============================================================================
class AudioCDBurner::Pimpl : private Timer
{
public:
Pimpl (AudioCDBurner& b, int deviceIndex) : owner (b)
{
if (DRDevice* dev = [[DRDevice devices] objectAtIndex: static_cast<NSUInteger> (deviceIndex)])
{
device.reset (new OpenDiskDevice (dev));
lastState = getDiskState();
startTimer (1000);
}
}
~Pimpl() override
{
stopTimer();
}
DiskState getDiskState() const
{
if ([device->device isValid])
{
NSDictionary* status = [device->device status];
NSString* state = [status objectForKey: DRDeviceMediaStateKey];
if ([state isEqualTo: DRDeviceMediaStateNone])
{
if ([[status objectForKey: DRDeviceIsTrayOpenKey] boolValue])
return trayOpen;
return noDisc;
}
if ([state isEqualTo: DRDeviceMediaStateMediaPresent])
{
if ([[[status objectForKey: DRDeviceMediaInfoKey] objectForKey: DRDeviceMediaBlocksFreeKey] intValue] > 0)
return writableDiskPresent;
return readOnlyDiskPresent;
}
}
return unknown;
}
bool openTray() { return [device->device isValid] && [device->device ejectMedia]; }
Array<int> getAvailableWriteSpeeds() const
{
Array<int> results;
if ([device->device isValid])
for (id kbPerSec in [[[device->device status] objectForKey: DRDeviceMediaInfoKey] objectForKey: DRDeviceBurnSpeedsKey])
results.add ([kbPerSec intValue] / kilobytesPerSecond1x);
return results;
}
bool setBufferUnderrunProtection (const bool shouldBeEnabled)
{
if ([device->device isValid])
{
device->underrunProtection = shouldBeEnabled;
return shouldBeEnabled && [[[device->device status] objectForKey: DRDeviceCanUnderrunProtectCDKey] boolValue];
}
return false;
}
int getNumAvailableAudioBlocks() const
{
return [[[[device->device status] objectForKey: DRDeviceMediaInfoKey]
objectForKey: DRDeviceMediaBlocksFreeKey] intValue];
}
std::unique_ptr<OpenDiskDevice> device;
private:
void timerCallback() override
{
const DiskState state = getDiskState();
if (state != lastState)
{
lastState = state;
owner.sendChangeMessage();
}
}
DiskState lastState;
AudioCDBurner& owner;
};
//==============================================================================
AudioCDBurner::AudioCDBurner (const int deviceIndex)
{
pimpl.reset (new Pimpl (*this, deviceIndex));
}
AudioCDBurner::~AudioCDBurner()
{
}
AudioCDBurner* AudioCDBurner::openDevice (const int deviceIndex)
{
std::unique_ptr<AudioCDBurner> b (new AudioCDBurner (deviceIndex));
if (b->pimpl->device == nil)
b = nullptr;
return b.release();
}
StringArray AudioCDBurner::findAvailableDevices()
{
StringArray s;
for (NSDictionary* dic in [DRDevice devices])
if (NSString* name = [dic valueForKey: DRDeviceProductNameKey])
s.add (nsStringToJuce (name));
return s;
}
AudioCDBurner::DiskState AudioCDBurner::getDiskState() const
{
return pimpl->getDiskState();
}
bool AudioCDBurner::isDiskPresent() const
{
return getDiskState() == writableDiskPresent;
}
bool AudioCDBurner::openTray()
{
return pimpl->openTray();
}
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 (100);
}
return newState;
}
Array<int> AudioCDBurner::getAvailableWriteSpeeds() const
{
return pimpl->getAvailableWriteSpeeds();
}
bool AudioCDBurner::setBufferUnderrunProtection (const bool shouldBeEnabled)
{
return pimpl->setBufferUnderrunProtection (shouldBeEnabled);
}
int AudioCDBurner::getNumAvailableAudioBlocks() const
{
return pimpl->getNumAvailableAudioBlocks();
}
bool AudioCDBurner::addAudioTrack (AudioSource* source, int numSamps)
{
if ([pimpl->device->device isValid])
{
pimpl->device->addSourceTrack (source, numSamps);
return true;
}
return false;
}
String AudioCDBurner::burn (AudioCDBurner::BurnProgressListener* listener,
bool ejectDiscAfterwards,
bool performFakeBurnForTesting,
int writeSpeed)
{
if ([pimpl->device->device isValid])
return pimpl->device->burn (listener, ejectDiscAfterwards, performFakeBurnForTesting, writeSpeed);
return "Couldn't open or write to the CD device";
}
}
@@ -0,0 +1,421 @@
/*
==============================================================================
This file is part of the JUCE framework.
Copyright (c) Raw Material Software Limited
JUCE is an open source framework subject to commercial or open source
licensing.
By downloading, installing, or using the JUCE framework, or combining the
JUCE framework with any other source code, object code, content or any other
copyrightable work, you agree to the terms of the JUCE End User Licence
Agreement, and all incorporated terms including the JUCE Privacy Policy and
the JUCE Website Terms of Service, as applicable, which will bind you. If you
do not agree to the terms of these agreements, we will not license the JUCE
framework to you, and you must discontinue the installation or download
process and cease use of the JUCE framework.
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
Or:
You may also use this code under the terms of the AGPLv3:
https://www.gnu.org/licenses/agpl-3.0.en.html
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
MERCHANTABILITY OR FITNESS FOR A PARTICULAR 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);
}
void releaseObjects()
{
discRecorder->Close();
if (redbook != nullptr)
redbook->Release();
discRecorder->Release();
discMaster->Release();
Release();
}
JUCE_COMRESULT QueryCancel (boolean* pbCancel) override
{
if (listener != nullptr && ! shouldCancel)
shouldCancel = listener->audioCDBurnProgress (progress);
*pbCancel = shouldCancel;
return S_OK;
}
JUCE_COMRESULT NotifyBlockProgress (long nCompleted, long nTotal) override
{
progress = nCompleted / (float) nTotal;
shouldCancel = listener != nullptr && listener->audioCDBurnProgress (progress);
return E_NOTIMPL;
}
JUCE_COMRESULT NotifyPnPActivity (void) override { return E_NOTIMPL; }
JUCE_COMRESULT NotifyAddProgress (long /*nCompletedSteps*/, long /*nTotalSteps*/) override { return E_NOTIMPL; }
JUCE_COMRESULT NotifyTrackProgress (long /*nCurrentTrack*/, long /*nTotalTracks*/) override { return E_NOTIMPL; }
JUCE_COMRESULT NotifyPreparingBurn (long /*nEstimatedSeconds*/) override { return E_NOTIMPL; }
JUCE_COMRESULT NotifyClosingDisc (long /*nEstimatedSeconds*/) override { return E_NOTIMPL; }
JUCE_COMRESULT NotifyBurnComplete (HRESULT /*status*/) override { return E_NOTIMPL; }
JUCE_COMRESULT NotifyEraseComplete (HRESULT /*status*/) override { 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 wchar_t* name, const int defaultReturn) const
{
std::wstring copy { name };
ComSmartPtr<IPropertyStorage> prop;
if (FAILED (discRecorder->GetRecorderProperties (prop.resetAndGetPointerAddress())))
return defaultReturn;
PROPSPEC iPropSpec;
iPropSpec.ulKind = PRSPEC_LPWSTR;
iPropSpec.lpwstr = copy.data();
PROPVARIANT iPropVariant;
return FAILED (prop->ReadMultiple (1, &iPropSpec, &iPropVariant))
? defaultReturn : (int) iPropVariant.lVal;
}
bool setIntProperty (const wchar_t* name, const int value) const
{
std::wstring copy { name };
ComSmartPtr<IPropertyStorage> prop;
if (FAILED (discRecorder->GetRecorderProperties (prop.resetAndGetPointerAddress())))
return false;
PROPSPEC iPropSpec;
iPropSpec.ulKind = PRSPEC_LPWSTR;
iPropSpec.lpwstr = copy.data();
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);
AudioData::interleaveSamples (AudioData::NonInterleavedSource<AudioData::Float32, AudioData::NativeEndian> { sourceBuffer.getArrayOfReadPointers(), 2 },
AudioData::InterleavedDest<AudioData::Int16, AudioData::LittleEndian> { reinterpret_cast<uint16*> (buffer.get()), 2 },
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
@@ -0,0 +1,92 @@
/*
==============================================================================
This file is part of the JUCE framework.
Copyright (c) Raw Material Software Limited
JUCE is an open source framework subject to commercial or open source
licensing.
By downloading, installing, or using the JUCE framework, or combining the
JUCE framework with any other source code, object code, content or any other
copyrightable work, you agree to the terms of the JUCE End User Licence
Agreement, and all incorporated terms including the JUCE Privacy Policy and
the JUCE Website Terms of Service, as applicable, which will bind you. If you
do not agree to the terms of these agreements, we will not license the JUCE
framework to you, and you must discontinue the installation or download
process and cease use of the JUCE framework.
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
Or:
You may also use this code under the terms of the AGPLv3:
https://www.gnu.org/licenses/agpl-3.0.en.html
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
==============================================================================
*/
namespace juce
{
AudioCDReader::AudioCDReader()
: AudioFormatReader (0, "CD Audio")
{
}
StringArray AudioCDReader::getAvailableCDNames()
{
StringArray names;
return names;
}
AudioCDReader* AudioCDReader::createReaderForCD (const int)
{
return nullptr;
}
AudioCDReader::~AudioCDReader()
{
}
void AudioCDReader::refreshTrackLengths()
{
}
bool AudioCDReader::readSamples (int* const*, int, int,
int64, int)
{
return false;
}
bool AudioCDReader::isCDStillPresent() const
{
return false;
}
bool AudioCDReader::isTrackAudio (int) const
{
return false;
}
void AudioCDReader::enableIndexScanning (bool)
{
}
int AudioCDReader::getLastIndex() const
{
return 0;
}
Array<int> AudioCDReader::findIndexesInTrack (const int)
{
return {};
}
} // namespace juce
@@ -0,0 +1,274 @@
/*
==============================================================================
This file is part of the JUCE framework.
Copyright (c) Raw Material Software Limited
JUCE is an open source framework subject to commercial or open source
licensing.
By downloading, installing, or using the JUCE framework, or combining the
JUCE framework with any other source code, object code, content or any other
copyrightable work, you agree to the terms of the JUCE End User Licence
Agreement, and all incorporated terms including the JUCE Privacy Policy and
the JUCE Website Terms of Service, as applicable, which will bind you. If you
do not agree to the terms of these agreements, we will not license the JUCE
framework to you, and you must discontinue the installation or download
process and cease use of the JUCE framework.
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
Or:
You may also use this code under the terms of the AGPLv3:
https://www.gnu.org/licenses/agpl-3.0.en.html
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
==============================================================================
*/
namespace juce
{
namespace CDReaderHelpers
{
inline const XmlElement* getElementForKey (const XmlElement& xml, const String& key)
{
for (auto* child : xml.getChildWithTagNameIterator ("key"))
if (child->getAllSubText().trim() == key)
return child->getNextElement();
return nullptr;
}
static int getIntValueForKey (const XmlElement& xml, const String& key, int defaultValue = -1)
{
const XmlElement* const block = getElementForKey (xml, key);
return block != nullptr ? block->getAllSubText().trim().getIntValue() : defaultValue;
}
// Get the track offsets for a CD given an XmlElement representing its TOC.Plist.
// Returns NULL on success, otherwise a const char* representing an error.
static const char* getTrackOffsets (XmlDocument& xmlDocument, Array<int>& offsets)
{
const std::unique_ptr<XmlElement> xml (xmlDocument.getDocumentElement());
if (xml == nullptr)
return "Couldn't parse XML in file";
const XmlElement* const dict = xml->getChildByName ("dict");
if (dict == nullptr)
return "Couldn't get top level dictionary";
const XmlElement* const sessions = getElementForKey (*dict, "Sessions");
if (sessions == nullptr)
return "Couldn't find sessions key";
const XmlElement* const session = sessions->getFirstChildElement();
if (session == nullptr)
return "Couldn't find first session";
const int leadOut = getIntValueForKey (*session, "Leadout Block");
if (leadOut < 0)
return "Couldn't find Leadout Block";
const XmlElement* const trackArray = getElementForKey (*session, "Track Array");
if (trackArray == nullptr)
return "Couldn't find Track Array";
for (auto* track : trackArray->getChildIterator())
{
const int trackValue = getIntValueForKey (*track, "Start Block");
if (trackValue < 0)
return "Couldn't find Start Block in the track";
offsets.add (trackValue * AudioCDReader::samplesPerFrame - 88200);
}
offsets.add (leadOut * AudioCDReader::samplesPerFrame - 88200);
return nullptr;
}
static void findDevices (Array<File>& cds)
{
File volumes ("/Volumes");
volumes.findChildFiles (cds, File::findDirectories, false);
for (int i = cds.size(); --i >= 0;)
if (! cds.getReference (i).getChildFile (".TOC.plist").exists())
cds.remove (i);
}
struct TrackSorter
{
static int getCDTrackNumber (const File& file)
{
return file.getFileName().initialSectionContainingOnly ("0123456789").getIntValue();
}
static int compareElements (const File& first, const File& second)
{
const int firstTrack = getCDTrackNumber (first);
const int secondTrack = getCDTrackNumber (second);
jassert (firstTrack > 0 && secondTrack > 0);
return firstTrack - secondTrack;
}
};
}
//==============================================================================
StringArray AudioCDReader::getAvailableCDNames()
{
Array<File> cds;
CDReaderHelpers::findDevices (cds);
StringArray names;
for (int i = 0; i < cds.size(); ++i)
names.add (cds.getReference (i).getFileName());
return names;
}
AudioCDReader* AudioCDReader::createReaderForCD (const int index)
{
Array<File> cds;
CDReaderHelpers::findDevices (cds);
if (cds[index].exists())
return new AudioCDReader (cds[index]);
return nullptr;
}
AudioCDReader::AudioCDReader (const File& volume)
: AudioFormatReader (nullptr, "CD Audio"),
volumeDir (volume),
currentReaderTrack (-1)
{
sampleRate = 44100.0;
bitsPerSample = 16;
numChannels = 2;
usesFloatingPointData = false;
refreshTrackLengths();
}
AudioCDReader::~AudioCDReader()
{
}
void AudioCDReader::refreshTrackLengths()
{
tracks.clear();
trackStartSamples.clear();
lengthInSamples = 0;
volumeDir.findChildFiles (tracks, File::findFiles | File::ignoreHiddenFiles, false, "*.aiff");
CDReaderHelpers::TrackSorter sorter;
tracks.sort (sorter);
const File toc (volumeDir.getChildFile (".TOC.plist"));
if (toc.exists())
{
XmlDocument doc (toc);
[[maybe_unused]] const char* error = CDReaderHelpers::getTrackOffsets (doc, trackStartSamples);
lengthInSamples = trackStartSamples.getLast() - trackStartSamples.getFirst();
}
}
bool AudioCDReader::readSamples (int* const* destSamples, int numDestChannels, int startOffsetInDestBuffer,
int64 startSampleInFile, int numSamples)
{
while (numSamples > 0)
{
int track = -1;
for (int i = 0; i < trackStartSamples.size() - 1; ++i)
{
if (startSampleInFile < trackStartSamples.getUnchecked (i + 1))
{
track = i;
break;
}
}
if (track < 0)
return false;
if (track != currentReaderTrack)
{
reader = nullptr;
if (auto in = tracks [track].createInputStream())
{
BufferedInputStream* const bin = new BufferedInputStream (in.release(), 65536, true);
AiffAudioFormat format;
reader.reset (format.createReaderFor (bin, true));
if (reader == nullptr)
currentReaderTrack = -1;
else
currentReaderTrack = track;
}
}
if (reader == nullptr)
return false;
const int startPos = (int) (startSampleInFile - trackStartSamples.getUnchecked (track));
const int numAvailable = (int) jmin ((int64) numSamples, reader->lengthInSamples - startPos);
reader->readSamples (destSamples, numDestChannels, startOffsetInDestBuffer, startPos, numAvailable);
numSamples -= numAvailable;
startSampleInFile += numAvailable;
}
return true;
}
bool AudioCDReader::isCDStillPresent() const
{
return volumeDir.exists();
}
void AudioCDReader::ejectDisk()
{
JUCE_AUTORELEASEPOOL
{
[[NSWorkspace sharedWorkspace] unmountAndEjectDeviceAtPath: juceStringToNS (volumeDir.getFullPathName())];
}
}
bool AudioCDReader::isTrackAudio (int trackNum) const
{
return tracks [trackNum].hasFileExtension (".aiff");
}
void AudioCDReader::enableIndexScanning (bool)
{
// any way to do this on a Mac??
}
int AudioCDReader::getLastIndex() const
{
return 0;
}
Array<int> AudioCDReader::findIndexesInTrack (const int /*trackNumber*/)
{
return {};
}
} // namespace juce
@@ -0,0 +1,527 @@
/*
==============================================================================
This file is part of the JUCE framework.
Copyright (c) Raw Material Software Limited
JUCE is an open source framework subject to commercial or open source
licensing.
By downloading, installing, or using the JUCE framework, or combining the
JUCE framework with any other source code, object code, content or any other
copyrightable work, you agree to the terms of the JUCE End User Licence
Agreement, and all incorporated terms including the JUCE Privacy Policy and
the JUCE Website Terms of Service, as applicable, which will bind you. If you
do not agree to the terms of these agreements, we will not license the JUCE
framework to you, and you must discontinue the installation or download
process and cease use of the JUCE framework.
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
Or:
You may also use this code under the terms of the AGPLv3:
https://www.gnu.org/licenses/agpl-3.0.en.html
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
==============================================================================
*/
namespace juce
{
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
STATICMETHOD (getAndroidBluetoothManager, "getAndroidBluetoothManager", "(Landroid/content/Context;)Lcom/rmsl/juce/JuceMidiSupport$BluetoothMidiManager;")
DECLARE_JNI_CLASS (AndroidJuceMidiSupport, "com/rmsl/juce/JuceMidiSupport")
#undef JNI_CLASS_MEMBERS
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
METHOD (getMidiBluetoothAddresses, "getMidiBluetoothAddresses", "()[Ljava/lang/String;") \
METHOD (pairBluetoothMidiDevice, "pairBluetoothMidiDevice", "(Ljava/lang/String;)Z") \
METHOD (unpairBluetoothMidiDevice, "unpairBluetoothMidiDevice", "(Ljava/lang/String;)V") \
METHOD (getHumanReadableStringForBluetoothAddress, "getHumanReadableStringForBluetoothAddress", "(Ljava/lang/String;)Ljava/lang/String;") \
METHOD (getBluetoothDeviceStatus, "getBluetoothDeviceStatus", "(Ljava/lang/String;)I") \
METHOD (startStopScan, "startStopScan", "(Z)V")
DECLARE_JNI_CLASS (AndroidBluetoothManager, "com/rmsl/juce/JuceMidiSupport$BluetoothMidiManager")
#undef JNI_CLASS_MEMBERS
//==============================================================================
struct AndroidBluetoothMidiInterface
{
static void startStopScan (bool startScanning)
{
JNIEnv* env = getEnv();
LocalRef<jobject> btManager (env->CallStaticObjectMethod (AndroidJuceMidiSupport, AndroidJuceMidiSupport.getAndroidBluetoothManager, getAppContext().get()));
if (btManager.get() != nullptr)
env->CallVoidMethod (btManager.get(), AndroidBluetoothManager.startStopScan, (jboolean) (startScanning ? 1 : 0));
}
static StringArray getBluetoothMidiDevicesNearby()
{
StringArray retval;
JNIEnv* env = getEnv();
LocalRef<jobject> btManager (env->CallStaticObjectMethod (AndroidJuceMidiSupport, AndroidJuceMidiSupport.getAndroidBluetoothManager, getAppContext().get()));
// if this is null then bluetooth is not enabled
if (btManager.get() == nullptr)
return {};
jobjectArray jDevices = (jobjectArray) env->CallObjectMethod (btManager.get(),
AndroidBluetoothManager.getMidiBluetoothAddresses);
LocalRef<jobjectArray> devices (jDevices);
const int count = env->GetArrayLength (devices.get());
for (int i = 0; i < count; ++i)
{
LocalRef<jstring> string ((jstring) env->GetObjectArrayElement (devices.get(), i));
retval.add (juceString (string));
}
return retval;
}
//==============================================================================
static bool pairBluetoothMidiDevice (const String& bluetoothAddress)
{
JNIEnv* env = getEnv();
LocalRef<jobject> btManager (env->CallStaticObjectMethod (AndroidJuceMidiSupport, AndroidJuceMidiSupport.getAndroidBluetoothManager, getAppContext().get()));
if (btManager.get() == nullptr)
return false;
jboolean result = env->CallBooleanMethod (btManager.get(), AndroidBluetoothManager.pairBluetoothMidiDevice,
javaString (bluetoothAddress).get());
return result;
}
static void unpairBluetoothMidiDevice (const String& bluetoothAddress)
{
JNIEnv* env = getEnv();
LocalRef<jobject> btManager (env->CallStaticObjectMethod (AndroidJuceMidiSupport, AndroidJuceMidiSupport.getAndroidBluetoothManager, getAppContext().get()));
if (btManager.get() != nullptr)
env->CallVoidMethod (btManager.get(), AndroidBluetoothManager.unpairBluetoothMidiDevice,
javaString (bluetoothAddress).get());
}
//==============================================================================
static String getHumanReadableStringForBluetoothAddress (const String& address)
{
JNIEnv* env = getEnv();
LocalRef<jobject> btManager (env->CallStaticObjectMethod (AndroidJuceMidiSupport, AndroidJuceMidiSupport.getAndroidBluetoothManager, getAppContext().get()));
if (btManager.get() == nullptr)
return address;
LocalRef<jstring> string ((jstring) env->CallObjectMethod (btManager.get(),
AndroidBluetoothManager.getHumanReadableStringForBluetoothAddress,
javaString (address).get()));
if (string.get() == nullptr)
return address;
return juceString (string);
}
//==============================================================================
enum PairStatus
{
unpaired = 0,
paired = 1,
pairing = 2
};
static PairStatus isBluetoothDevicePaired (const String& address)
{
JNIEnv* env = getEnv();
LocalRef<jobject> btManager (env->CallStaticObjectMethod (AndroidJuceMidiSupport, AndroidJuceMidiSupport.getAndroidBluetoothManager, getAppContext().get()));
if (btManager.get() == nullptr)
return unpaired;
return static_cast<PairStatus> (env->CallIntMethod (btManager.get(), AndroidBluetoothManager.getBluetoothDeviceStatus,
javaString (address).get()));
}
};
//==============================================================================
struct AndroidBluetoothMidiDevice
{
enum ConnectionStatus
{
offline,
connected,
disconnected,
connecting,
disconnecting
};
AndroidBluetoothMidiDevice (String deviceName, String address, ConnectionStatus status)
: name (deviceName), bluetoothAddress (address), connectionStatus (status)
{
// can't create a device without a valid name and bluetooth address!
jassert (! name.isEmpty());
jassert (! bluetoothAddress.isEmpty());
}
bool operator== (const AndroidBluetoothMidiDevice& other) const noexcept
{
return bluetoothAddress == other.bluetoothAddress;
}
bool operator!= (const AndroidBluetoothMidiDevice& other) const noexcept
{
return ! operator== (other);
}
const String name, bluetoothAddress;
ConnectionStatus connectionStatus;
};
//==============================================================================
class AndroidBluetoothMidiDevicesListBox final : public ListBox,
private ListBoxModel,
private Timer
{
public:
//==============================================================================
AndroidBluetoothMidiDevicesListBox()
: timerPeriodInMs (1000)
{
setRowHeight (40);
setModel (this);
setOutlineThickness (1);
startTimer (timerPeriodInMs);
}
void pairDeviceThreadFinished() // callback from PairDeviceThread
{
updateDeviceList();
startTimer (timerPeriodInMs);
}
private:
//==============================================================================
typedef AndroidBluetoothMidiDevice::ConnectionStatus DeviceStatus;
int getNumRows() override
{
return devices.size();
}
void paintListBoxItem (int rowNumber, Graphics& g,
int width, int height, bool) override
{
if (isPositiveAndBelow (rowNumber, devices.size()))
{
const AndroidBluetoothMidiDevice& device = devices.getReference (rowNumber);
const String statusString (getDeviceStatusString (device.connectionStatus));
g.fillAll (Colours::white);
const float xmargin = 3.0f;
const float ymargin = 3.0f;
const float fontHeight = 0.4f * (float) height;
const float deviceNameWidth = 0.6f * (float) width;
g.setFont (fontHeight);
g.setColour (getDeviceNameFontColour (device.connectionStatus));
g.drawText (device.name,
Rectangle<float> (xmargin, ymargin, deviceNameWidth - (2.0f * xmargin), (float) height - (2.0f * ymargin)),
Justification::topLeft, true);
g.setColour (getDeviceStatusFontColour (device.connectionStatus));
g.drawText (statusString,
Rectangle<float> (deviceNameWidth + xmargin, ymargin,
(float) width - deviceNameWidth - (2.0f * xmargin), (float) height - (2.0f * ymargin)),
Justification::topRight, true);
g.setColour (Colours::grey);
g.drawHorizontalLine (height - 1, xmargin, (float) width);
}
}
//==============================================================================
static Colour getDeviceNameFontColour (DeviceStatus deviceStatus) noexcept
{
if (deviceStatus == AndroidBluetoothMidiDevice::offline)
return Colours::grey;
return Colours::black;
}
static Colour getDeviceStatusFontColour (DeviceStatus deviceStatus) noexcept
{
if (deviceStatus == AndroidBluetoothMidiDevice::offline
|| deviceStatus == AndroidBluetoothMidiDevice::connecting
|| deviceStatus == AndroidBluetoothMidiDevice::disconnecting)
return Colours::grey;
if (deviceStatus == AndroidBluetoothMidiDevice::connected)
return Colours::green;
return Colours::black;
}
static String getDeviceStatusString (DeviceStatus deviceStatus) noexcept
{
if (deviceStatus == AndroidBluetoothMidiDevice::offline) return "Offline";
if (deviceStatus == AndroidBluetoothMidiDevice::connected) return "Connected";
if (deviceStatus == AndroidBluetoothMidiDevice::disconnected) return "Not connected";
if (deviceStatus == AndroidBluetoothMidiDevice::connecting) return "Connecting...";
if (deviceStatus == AndroidBluetoothMidiDevice::disconnecting) return "Disconnecting...";
// unknown device state!
jassertfalse;
return "Status unknown";
}
//==============================================================================
void listBoxItemClicked (int row, const MouseEvent&) override
{
const AndroidBluetoothMidiDevice& device = devices.getReference (row);
if (device.connectionStatus == AndroidBluetoothMidiDevice::disconnected)
disconnectedDeviceClicked (row);
else if (device.connectionStatus == AndroidBluetoothMidiDevice::connected)
connectedDeviceClicked (row);
}
void timerCallback() override
{
updateDeviceList();
}
//==============================================================================
struct PairDeviceThread final : public Thread,
private AsyncUpdater
{
PairDeviceThread (const String& bluetoothAddressOfDeviceToPair,
AndroidBluetoothMidiDevicesListBox& ownerListBox)
: Thread (SystemStats::getJUCEVersion() + ": Bluetooth MIDI Device Pairing Thread"),
bluetoothAddress (bluetoothAddressOfDeviceToPair),
owner (&ownerListBox)
{
startThread();
}
void run() override
{
AndroidBluetoothMidiInterface::pairBluetoothMidiDevice (bluetoothAddress);
triggerAsyncUpdate();
}
void handleAsyncUpdate() override
{
if (owner != nullptr)
owner->pairDeviceThreadFinished();
delete this;
}
private:
String bluetoothAddress;
Component::SafePointer<AndroidBluetoothMidiDevicesListBox> owner;
};
//==============================================================================
void disconnectedDeviceClicked (int row)
{
stopTimer();
AndroidBluetoothMidiDevice& device = devices.getReference (row);
device.connectionStatus = AndroidBluetoothMidiDevice::connecting;
updateContent();
repaint();
new PairDeviceThread (device.bluetoothAddress, *this);
}
void connectedDeviceClicked (int row)
{
AndroidBluetoothMidiDevice& device = devices.getReference (row);
device.connectionStatus = AndroidBluetoothMidiDevice::disconnecting;
updateContent();
repaint();
AndroidBluetoothMidiInterface::unpairBluetoothMidiDevice (device.bluetoothAddress);
}
//==============================================================================
void updateDeviceList()
{
StringArray bluetoothAddresses = AndroidBluetoothMidiInterface::getBluetoothMidiDevicesNearby();
Array<AndroidBluetoothMidiDevice> newDevices;
for (String* address = bluetoothAddresses.begin();
address != bluetoothAddresses.end(); ++address)
{
String name = AndroidBluetoothMidiInterface::getHumanReadableStringForBluetoothAddress (*address);
DeviceStatus status;
switch (AndroidBluetoothMidiInterface::isBluetoothDevicePaired (*address))
{
case AndroidBluetoothMidiInterface::pairing:
status = AndroidBluetoothMidiDevice::connecting;
break;
case AndroidBluetoothMidiInterface::paired:
status = AndroidBluetoothMidiDevice::connected;
break;
case AndroidBluetoothMidiInterface::unpaired:
default:
status = AndroidBluetoothMidiDevice::disconnected;
}
newDevices.add (AndroidBluetoothMidiDevice (name, *address, status));
}
devices.swapWith (newDevices);
updateContent();
repaint();
}
Array<AndroidBluetoothMidiDevice> devices;
const int timerPeriodInMs;
};
//==============================================================================
class BluetoothMidiSelectorOverlay final : public Component
{
public:
BluetoothMidiSelectorOverlay (ModalComponentManager::Callback* exitCallbackToUse,
const Rectangle<int>& boundsToUse)
: bounds (boundsToUse)
{
std::unique_ptr<ModalComponentManager::Callback> exitCallback (exitCallbackToUse);
AndroidBluetoothMidiInterface::startStopScan (true);
setAlwaysOnTop (true);
setVisible (true);
addToDesktop (ComponentPeer::windowHasDropShadow);
if (bounds.isEmpty())
setBounds (0, 0, getParentWidth(), getParentHeight());
else
setBounds (bounds);
toFront (true);
setOpaque (! bounds.isEmpty());
addAndMakeVisible (bluetoothDevicesList);
enterModalState (true, exitCallback.release(), true);
}
~BluetoothMidiSelectorOverlay() override
{
AndroidBluetoothMidiInterface::startStopScan (false);
}
void paint (Graphics& g) override
{
g.fillAll (bounds.isEmpty() ? Colours::black.withAlpha (0.6f) : Colours::black);
g.setColour (Colour (0xffdfdfdf));
Rectangle<int> overlayBounds = getOverlayBounds();
g.fillRect (overlayBounds);
g.setColour (Colours::black);
g.setFont (16);
g.drawText ("Bluetooth MIDI Devices",
overlayBounds.removeFromTop (20).reduced (3, 3),
Justification::topLeft, true);
overlayBounds.removeFromTop (2);
g.setFont (12);
g.drawText ("tap to connect/disconnect",
overlayBounds.removeFromTop (18).reduced (3, 3),
Justification::topLeft, true);
}
void inputAttemptWhenModal() override { exitModalState (0); }
void mouseDrag (const MouseEvent&) override {}
void mouseDown (const MouseEvent&) override { exitModalState (0); }
void resized() override { update(); }
void parentSizeChanged() override { update(); }
private:
Rectangle<int> bounds;
void update()
{
if (bounds.isEmpty())
setBounds (0, 0, getParentWidth(), getParentHeight());
else
setBounds (bounds);
bluetoothDevicesList.setBounds (getOverlayBounds().withTrimmedTop (40));
}
Rectangle<int> getOverlayBounds() const noexcept
{
if (bounds.isEmpty())
{
const int pw = getParentWidth();
const int ph = getParentHeight();
return Rectangle<int> (pw, ph).withSizeKeepingCentre (jmin (400, pw - 14),
jmin (300, ph - 40));
}
return bounds.withZeroOrigin();
}
AndroidBluetoothMidiDevicesListBox bluetoothDevicesList;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BluetoothMidiSelectorOverlay)
};
//==============================================================================
bool BluetoothMidiDevicePairingDialogue::open (ModalComponentManager::Callback* exitCallbackPtr,
Rectangle<int>* btBounds)
{
std::unique_ptr<ModalComponentManager::Callback> exitCallback (exitCallbackPtr);
auto boundsToUse = (btBounds != nullptr ? *btBounds : Rectangle<int> {});
if (! RuntimePermissions::isGranted (RuntimePermissions::bluetoothMidi))
{
// If you hit this assert, you probably forgot to get RuntimePermissions::bluetoothMidi.
// This is not going to work, boo! The pairing dialogue won't be able to scan for or
// find any devices, it will just display an empty list, so don't bother opening it.
jassertfalse;
return false;
}
new BluetoothMidiSelectorOverlay (exitCallback.release(), boundsToUse);
return true;
}
bool BluetoothMidiDevicePairingDialogue::isAvailable()
{
auto* env = getEnv();
LocalRef<jobject> btManager (env->CallStaticObjectMethod (AndroidJuceMidiSupport, AndroidJuceMidiSupport.getAndroidBluetoothManager, getAppContext().get()));
return btManager != nullptr;
}
} // namespace juce
@@ -0,0 +1,155 @@
/*
==============================================================================
This file is part of the JUCE framework.
Copyright (c) Raw Material Software Limited
JUCE is an open source framework subject to commercial or open source
licensing.
By downloading, installing, or using the JUCE framework, or combining the
JUCE framework with any other source code, object code, content or any other
copyrightable work, you agree to the terms of the JUCE End User Licence
Agreement, and all incorporated terms including the JUCE Privacy Policy and
the JUCE Website Terms of Service, as applicable, which will bind you. If you
do not agree to the terms of these agreements, we will not license the JUCE
framework to you, and you must discontinue the installation or download
process and cease use of the JUCE framework.
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
Or:
You may also use this code under the terms of the AGPLv3:
https://www.gnu.org/licenses/agpl-3.0.en.html
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
==============================================================================
*/
#if ! TARGET_IPHONE_SIMULATOR
namespace juce
{
//==============================================================================
class BluetoothMidiSelectorOverlay final : public Component
{
public:
BluetoothMidiSelectorOverlay (ModalComponentManager::Callback* exitCallbackToUse,
const Rectangle<int>& boundsToUse)
: bounds (boundsToUse)
{
std::unique_ptr<ModalComponentManager::Callback> exitCallback (exitCallbackToUse);
setAlwaysOnTop (true);
setVisible (true);
addToDesktop (ComponentPeer::windowHasDropShadow);
if (bounds.isEmpty())
setBounds (0, 0, getParentWidth(), getParentHeight());
else
setBounds (bounds);
toFront (true);
setOpaque (true);
controller = [[CABTMIDICentralViewController alloc] init];
nativeSelectorComponent.setView ([controller view]);
addAndMakeVisible (nativeSelectorComponent);
enterModalState (true, exitCallback.release(), true);
}
~BluetoothMidiSelectorOverlay() override
{
nativeSelectorComponent.setView (nullptr);
[controller release];
}
void paint (Graphics& g) override
{
g.fillAll (bounds.isEmpty() ? Colours::black.withAlpha (0.5f) : Colours::black);
}
void inputAttemptWhenModal() override { close(); }
void mouseDrag (const MouseEvent&) override {}
void mouseDown (const MouseEvent&) override { close(); }
void resized() override { update(); }
void parentSizeChanged() override { update(); }
private:
void update()
{
if (bounds.isEmpty())
{
const int pw = getParentWidth();
const int ph = getParentHeight();
nativeSelectorComponent.setBounds (Rectangle<int> (pw, ph)
.withSizeKeepingCentre (jmin (400, pw),
jmin (450, ph - 40)));
}
else
{
nativeSelectorComponent.setBounds (bounds.withZeroOrigin());
}
}
void close()
{
exitModalState (0);
setVisible (false);
}
CABTMIDICentralViewController* controller;
UIViewComponent nativeSelectorComponent;
Rectangle<int> bounds;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BluetoothMidiSelectorOverlay)
};
bool BluetoothMidiDevicePairingDialogue::open (ModalComponentManager::Callback* exitCallback,
Rectangle<int>* btBounds)
{
std::unique_ptr<ModalComponentManager::Callback> cb (exitCallback);
auto boundsToUse = (btBounds != nullptr ? *btBounds : Rectangle<int> {});
if (isAvailable())
{
new BluetoothMidiSelectorOverlay (cb.release(), boundsToUse);
return true;
}
return false;
}
bool BluetoothMidiDevicePairingDialogue::isAvailable()
{
return NSClassFromString (@"CABTMIDICentralViewController") != nil;
}
} // namespace juce
//==============================================================================
#else
namespace juce
{
bool BluetoothMidiDevicePairingDialogue::open (ModalComponentManager::Callback* exitCallback,
Rectangle<int>*)
{
std::unique_ptr<ModalComponentManager::Callback> cb (exitCallback);
return false;
}
bool BluetoothMidiDevicePairingDialogue::isAvailable() { return false; }
}
#endif
@@ -0,0 +1,54 @@
/*
==============================================================================
This file is part of the JUCE framework.
Copyright (c) Raw Material Software Limited
JUCE is an open source framework subject to commercial or open source
licensing.
By downloading, installing, or using the JUCE framework, or combining the
JUCE framework with any other source code, object code, content or any other
copyrightable work, you agree to the terms of the JUCE End User Licence
Agreement, and all incorporated terms including the JUCE Privacy Policy and
the JUCE Website Terms of Service, as applicable, which will bind you. If you
do not agree to the terms of these agreements, we will not license the JUCE
framework to you, and you must discontinue the installation or download
process and cease use of the JUCE framework.
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
Or:
You may also use this code under the terms of the AGPLv3:
https://www.gnu.org/licenses/agpl-3.0.en.html
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
==============================================================================
*/
namespace juce
{
bool BluetoothMidiDevicePairingDialogue::open (ModalComponentManager::Callback* exitCallback,
Rectangle<int>*)
{
std::unique_ptr<ModalComponentManager::Callback> cb (exitCallback);
// not implemented on Linux yet!
// You should check whether the dialogue is available on your system
// using isAvailable() before calling open().
jassertfalse;
return false;
}
bool BluetoothMidiDevicePairingDialogue::isAvailable()
{
return false;
}
} // namespace juce
@@ -0,0 +1,178 @@
/*
==============================================================================
This file is part of the JUCE framework.
Copyright (c) Raw Material Software Limited
JUCE is an open source framework subject to commercial or open source
licensing.
By downloading, installing, or using the JUCE framework, or combining the
JUCE framework with any other source code, object code, content or any other
copyrightable work, you agree to the terms of the JUCE End User Licence
Agreement, and all incorporated terms including the JUCE Privacy Policy and
the JUCE Website Terms of Service, as applicable, which will bind you. If you
do not agree to the terms of these agreements, we will not license the JUCE
framework to you, and you must discontinue the installation or download
process and cease use of the JUCE framework.
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
Or:
You may also use this code under the terms of the AGPLv3:
https://www.gnu.org/licenses/agpl-3.0.en.html
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
class BluetoothMidiPairingWindowClass final : public ObjCClass<NSObject>
{
public:
struct Callbacks
{
std::unique_ptr<ModalComponentManager::Callback> modalExit;
std::function<void()> windowClosed;
};
BluetoothMidiPairingWindowClass() : ObjCClass<NSObject> ("JUCEBluetoothMidiPairingWindowClass_")
{
addIvar<Callbacks*> ("callbacks");
addIvar<CABTLEMIDIWindowController*> ("controller");
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
addMethod (@selector (initWithCallbacks:), initWithCallbacks);
addMethod (@selector (show:), show);
addMethod (@selector (receivedWindowWillClose:), receivedWindowWillClose);
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
addMethod (@selector (dealloc), dealloc);
registerClass();
}
private:
static CABTLEMIDIWindowController* getController (id self)
{
return getIvar<CABTLEMIDIWindowController*> (self, "controller");
}
static id initWithCallbacks (id self, SEL, Callbacks* cbs)
{
self = sendSuperclassMessage<id> (self, @selector (init));
object_setInstanceVariable (self, "callbacks", cbs);
object_setInstanceVariable (self, "controller", [CABTLEMIDIWindowController new]);
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
[[NSNotificationCenter defaultCenter] addObserver: self
selector: @selector (receivedWindowWillClose:)
name: @"NSWindowWillCloseNotification"
object: [getController (self) window]];
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
return self;
}
static void dealloc (id self, SEL)
{
[getController (self) release];
sendSuperclassMessage<void> (self, @selector (dealloc));
}
static void show (id self, SEL, Rectangle<int>* bounds)
{
if (bounds != nullptr)
{
auto nsBounds = makeCGRect (*bounds);
auto mainScreenHeight = []
{
if ([[NSScreen screens] count] == 0)
return (CGFloat) 0.0f;
return [[[NSScreen screens] objectAtIndex: 0] frame].size.height;
}();
nsBounds.origin.y = mainScreenHeight - (nsBounds.origin.y + nsBounds.size.height);
[getController (self).window setFrame: nsBounds
display: YES];
}
[getController (self) showWindow: nil];
}
static void receivedWindowWillClose (id self, SEL, NSNotification*)
{
[[NSNotificationCenter defaultCenter] removeObserver: self];
auto* cbs = getIvar<Callbacks*> (self, "callbacks");
if (cbs->modalExit != nullptr)
cbs->modalExit->modalStateFinished (0);
cbs->windowClosed();
}
};
class BluetoothMidiSelectorWindowHelper final : public DeletedAtShutdown
{
public:
BluetoothMidiSelectorWindowHelper (ModalComponentManager::Callback* exitCallback,
Rectangle<int>* bounds)
{
std::unique_ptr<ModalComponentManager::Callback> exitCB (exitCallback);
static BluetoothMidiPairingWindowClass cls;
window.reset (cls.createInstance());
auto deletionCB = [safeThis = WeakReference<BluetoothMidiSelectorWindowHelper> { this }]
{
if (safeThis != nullptr)
delete safeThis.get();
};
callbacks.reset (new BluetoothMidiPairingWindowClass::Callbacks { std::move (exitCB),
std::move (deletionCB) });
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
[window.get() performSelector: @selector (initWithCallbacks:)
withObject: (id) callbacks.get()];
[window.get() performSelector: @selector (show:)
withObject: (id) bounds];
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
}
private:
std::unique_ptr<NSObject, NSObjectDeleter> window;
std::unique_ptr<BluetoothMidiPairingWindowClass::Callbacks> callbacks;
JUCE_DECLARE_WEAK_REFERENCEABLE (BluetoothMidiSelectorWindowHelper)
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BluetoothMidiSelectorWindowHelper)
};
//==============================================================================
bool BluetoothMidiDevicePairingDialogue::open (ModalComponentManager::Callback* exitCallback,
Rectangle<int>* bounds)
{
new BluetoothMidiSelectorWindowHelper (exitCallback, bounds);
return true;
}
bool BluetoothMidiDevicePairingDialogue::isAvailable()
{
return true;
}
} // namespace juce
@@ -0,0 +1,54 @@
/*
==============================================================================
This file is part of the JUCE framework.
Copyright (c) Raw Material Software Limited
JUCE is an open source framework subject to commercial or open source
licensing.
By downloading, installing, or using the JUCE framework, or combining the
JUCE framework with any other source code, object code, content or any other
copyrightable work, you agree to the terms of the JUCE End User Licence
Agreement, and all incorporated terms including the JUCE Privacy Policy and
the JUCE Website Terms of Service, as applicable, which will bind you. If you
do not agree to the terms of these agreements, we will not license the JUCE
framework to you, and you must discontinue the installation or download
process and cease use of the JUCE framework.
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
Or:
You may also use this code under the terms of the AGPLv3:
https://www.gnu.org/licenses/agpl-3.0.en.html
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
==============================================================================
*/
namespace juce
{
bool BluetoothMidiDevicePairingDialogue::open (ModalComponentManager::Callback* exitCallback,
Rectangle<int>*)
{
std::unique_ptr<ModalComponentManager::Callback> cb (exitCallback);
// not implemented on Windows yet!
// You should check whether the dialogue is available on your system
// using isAvailable() before calling open().
jassertfalse;
return false;
}
bool BluetoothMidiDevicePairingDialogue::isAvailable()
{
return false;
}
} // namespace juce