mirror of
https://github.com/cookiengineer/audacity
synced 2025-04-30 15:49:41 +02:00
537 lines
14 KiB
C
537 lines
14 KiB
C
/*
|
|
* PortBurn
|
|
* Mac OS X implementation
|
|
*
|
|
* Authors:
|
|
* Dominic Mazzoni
|
|
*
|
|
* With assistance from Apple's sample code:
|
|
* /Developer/Examples/DiscRecording/C/
|
|
*
|
|
* License: LGPL
|
|
*/
|
|
|
|
#include "portburn.h"
|
|
#include "portburn_staging.h"
|
|
|
|
#include <string.h>
|
|
|
|
#include <DiscRecording/DiscRecording.h>
|
|
|
|
typedef struct {
|
|
OSStatus err;
|
|
CFArrayRef deviceList;
|
|
DRDeviceRef device;
|
|
CFMutableArrayRef trackArray;
|
|
DRBurnRef burn;
|
|
CFArrayRef trackLayout;
|
|
void *staging;
|
|
float frac;
|
|
} PBHandle;
|
|
|
|
char *PortBurn_CStringFromCFString(CFStringRef cfname) {
|
|
char *name;
|
|
CFIndex len;
|
|
|
|
len = CFStringGetMaximumSizeForEncoding(CFStringGetLength(cfname),
|
|
kCFStringEncodingASCII);
|
|
name = (char *)malloc(len + 1);
|
|
name[0] = 0;
|
|
CFStringGetCString(cfname, name, len + 1, kCFStringEncodingASCII);
|
|
return name;
|
|
}
|
|
|
|
void *PortBurn_Open()
|
|
{
|
|
PBHandle *h;
|
|
|
|
h = (PBHandle *)malloc(sizeof(PBHandle));
|
|
memset(h, 0, sizeof(PBHandle));
|
|
|
|
return h;
|
|
}
|
|
|
|
/* Cleanup */
|
|
void PortBurn_Close(void *handle) {
|
|
PBHandle *h = (PBHandle *)handle;
|
|
if (!h)
|
|
return;
|
|
|
|
if (h->deviceList)
|
|
CFRelease(h->deviceList);
|
|
|
|
free(h);
|
|
}
|
|
|
|
|
|
/* Return a human-readable error string for the last operating system
|
|
specific error (NOT human readable strings for the PortBurn error
|
|
codes). Caller should dispose of the returned string using free(). */
|
|
const char *PortBurn_LastError(void *handle)
|
|
{
|
|
PBHandle *h = (PBHandle *)handle;
|
|
CFStringRef cferr;
|
|
|
|
if (!h)
|
|
return NULL;
|
|
|
|
cferr = DRCopyLocalizedStringForDiscRecordingError(h->err);
|
|
if (cferr) {
|
|
char *result = PortBurn_CStringFromCFString(cferr);
|
|
CFRelease(cferr);
|
|
return result;
|
|
}
|
|
else
|
|
return NULL;
|
|
}
|
|
|
|
/* Get the number of devices capable of burning audio CDs.
|
|
If the result is N, then calls to GetDeviceName and OpenDevice
|
|
are guaranteed to be valid for indices from 0 up to N-1, until
|
|
the next time you call GetNumDevices. At that point, the list of
|
|
devices will be rescanned, and may be different. */
|
|
int PortBurn_GetNumDevices(void *handle)
|
|
{
|
|
PBHandle *h = (PBHandle *)handle;
|
|
|
|
if (!h)
|
|
return 0;
|
|
|
|
if (h->deviceList) {
|
|
CFRelease(h->deviceList);
|
|
h->deviceList = NULL;
|
|
}
|
|
|
|
h->deviceList = DRCopyDeviceArray();
|
|
if (!h->deviceList)
|
|
return 0;
|
|
|
|
return (int)CFArrayGetCount(h->deviceList);
|
|
}
|
|
|
|
/* Get the name of the device with a given index. Only valid
|
|
after a call to GetNumDevices. */
|
|
char *PortBurn_GetDeviceName(void *handle, int index)
|
|
{
|
|
PBHandle *h = (PBHandle *)handle;
|
|
CFDictionaryRef deviceInfo;
|
|
CFStringRef bus, vendor, product;
|
|
CFStringRef cfname;
|
|
char *result;
|
|
|
|
if (!h)
|
|
return NULL;
|
|
|
|
if (!h->deviceList)
|
|
return NULL;
|
|
|
|
DRDeviceRef device;
|
|
device = (DRDeviceRef)CFArrayGetValueAtIndex(h->deviceList, index);
|
|
|
|
deviceInfo = DRDeviceCopyInfo(device);
|
|
bus = (CFStringRef)CFDictionaryGetValue(deviceInfo,
|
|
kDRDevicePhysicalInterconnectKey);
|
|
if (CFEqual(bus, kDRDevicePhysicalInterconnectFireWire))
|
|
bus = CFSTR("FireWire: ");
|
|
else if (CFEqual(bus, kDRDevicePhysicalInterconnectUSB))
|
|
bus = CFSTR("USB: ");
|
|
else if (CFEqual(bus, kDRDevicePhysicalInterconnectATAPI))
|
|
bus = CFSTR("ATAPI: ");
|
|
else if (CFEqual(bus, kDRDevicePhysicalInterconnectSCSI))
|
|
bus = CFSTR("SCSI: ");
|
|
else
|
|
bus = CFSTR("");
|
|
|
|
vendor = CFDictionaryGetValue(deviceInfo, kDRDeviceVendorNameKey);
|
|
product = CFDictionaryGetValue(deviceInfo, kDRDeviceProductNameKey);
|
|
|
|
cfname = CFStringCreateWithFormat(NULL, NULL,
|
|
CFSTR("%@%@ %@"),
|
|
bus, vendor, product);
|
|
|
|
result = PortBurn_CStringFromCFString(cfname);
|
|
|
|
CFRelease(cfname);
|
|
CFRelease(deviceInfo);
|
|
|
|
return result;
|
|
}
|
|
|
|
/* Open a particular device by index number. Returns 0 on success;
|
|
any nonzero value indicates an error, for example if the device is
|
|
already open by some other program. */
|
|
int PortBurn_OpenDevice(void *handle, int index)
|
|
{
|
|
PBHandle *h = (PBHandle *)handle;
|
|
if (!h)
|
|
return pbErrNoHandle;
|
|
|
|
if (!h->deviceList)
|
|
return pbErrMustCallGetNumDevices;
|
|
|
|
h->device = (DRDeviceRef)CFArrayGetValueAtIndex(h->deviceList, index);
|
|
|
|
/* This just indicates interest; it doesn't return an error. */
|
|
DRDeviceAcquireMediaReservation(h->device);
|
|
|
|
return pbSuccess;
|
|
}
|
|
|
|
/* Close a device */
|
|
int PortBurn_CloseDevice(void *handle)
|
|
{
|
|
PBHandle *h = (PBHandle *)handle;
|
|
|
|
if (!h)
|
|
return pbErrNoHandle;
|
|
|
|
if (!h->device)
|
|
return pbErrDeviceNotOpen;
|
|
|
|
if (h->burn != NULL) {
|
|
CFRelease(h->burn);
|
|
h->burn = NULL;
|
|
}
|
|
|
|
if (h->trackArray) {
|
|
CFRelease(h->trackArray);
|
|
h->trackArray = NULL;
|
|
}
|
|
|
|
DRDeviceReleaseMediaReservation(h->device);
|
|
CFRelease(h->device);
|
|
h->device = NULL;
|
|
|
|
if (h->staging) {
|
|
PortBurn_CleanupStaging(h->staging);
|
|
h->staging = NULL;
|
|
}
|
|
|
|
return pbSuccess;
|
|
}
|
|
|
|
/* Eject the media in the currently opened device */
|
|
int PortBurn_EjectDevice(void *handle)
|
|
{
|
|
PBHandle *h = (PBHandle *)handle;
|
|
|
|
if (!h)
|
|
return pbErrNoHandle;
|
|
|
|
if (!h->device)
|
|
return pbErrDeviceNotOpen;
|
|
|
|
h->err = DRDeviceOpenTray(h->device);
|
|
if (noErr == h->err)
|
|
return pbSuccess; /* success */
|
|
|
|
h->err = DRDeviceEjectMedia(h->device);
|
|
if (noErr == h->err)
|
|
return pbSuccess; /* success */
|
|
|
|
return pbErrCannotEject;
|
|
}
|
|
|
|
/* This indicates you're ready to start staging audio data for the
|
|
currently opened device. At this point you are committing to
|
|
exclusive access to the CD burner, and this is the function that
|
|
will fail if another program is using the device, or if there is
|
|
no writable CD media in the device at this point.
|
|
You should pass in the path to a temporary directory that has at
|
|
least 700 MB of free space, to stage the audio, although note that
|
|
not all implementations will make use of this directory. */
|
|
int PortBurn_StartStaging(void *handle, const char *tmpdir)
|
|
|
|
{
|
|
PBHandle *h = (PBHandle *)handle;
|
|
CFDictionaryRef deviceStatus;
|
|
CFStringRef mediaState;
|
|
CFDictionaryRef mediaInfo;
|
|
CFBooleanRef blank;
|
|
CFBooleanRef writable;
|
|
CFBooleanRef reserved;
|
|
|
|
if (!h)
|
|
return pbErrNoHandle;
|
|
|
|
if (!h->device)
|
|
return pbErrDeviceNotOpen;
|
|
|
|
if (h->staging)
|
|
return pbErrAlreadyStagingOrBurning;
|
|
|
|
/* First we check to see that we have blank media in the drive. */
|
|
deviceStatus = DRDeviceCopyStatus(h->device);
|
|
mediaState = (CFStringRef)CFDictionaryGetValue(deviceStatus,
|
|
kDRDeviceMediaStateKey);
|
|
if (mediaState == NULL) {
|
|
CFRelease(deviceStatus);
|
|
return pbErrCannotAccessDevice;
|
|
}
|
|
|
|
if (!CFEqual(mediaState, kDRDeviceMediaStateMediaPresent)) {
|
|
CFRelease(deviceStatus);
|
|
return pbErrNoMediaInDrive;
|
|
}
|
|
|
|
mediaInfo = (CFDictionaryRef)CFDictionaryGetValue(deviceStatus,
|
|
kDRDeviceMediaInfoKey);
|
|
blank = (CFBooleanRef)CFDictionaryGetValue(mediaInfo,
|
|
kDRDeviceMediaIsBlankKey);
|
|
|
|
if (blank == NULL || !CFBooleanGetValue(blank)) {
|
|
CFRelease(deviceStatus);
|
|
return pbErrMediaIsNotBlankAndWritable;
|
|
}
|
|
|
|
writable = (CFBooleanRef)CFDictionaryGetValue(mediaInfo,
|
|
kDRDeviceMediaIsAppendableKey);
|
|
|
|
if (writable == NULL || !CFBooleanGetValue(writable)) {
|
|
CFRelease(deviceStatus);
|
|
return pbErrMediaIsNotBlankAndWritable;
|
|
}
|
|
|
|
reserved = (CFBooleanRef)CFDictionaryGetValue(mediaInfo,
|
|
kDRDeviceMediaIsReservedKey);
|
|
if (reserved == NULL || !CFBooleanGetValue(reserved)) {
|
|
CFRelease(deviceStatus);
|
|
return pbErrCannotReserveDevice;
|
|
}
|
|
|
|
CFRelease(deviceStatus);
|
|
|
|
h->staging = PortBurn_TempDirStaging(tmpdir);
|
|
|
|
if (!h->staging) {
|
|
return pbErrCannotCreateStagingDirectory;
|
|
}
|
|
|
|
h->trackArray = CFArrayCreateMutable(kCFAllocatorDefault, 0,
|
|
&kCFTypeArrayCallBacks);
|
|
|
|
return pbSuccess;
|
|
}
|
|
|
|
/* Start a new audio track. Pass the name of the track, and the
|
|
length in CD Audio frames (each frame is 1/75.0 of a second, exactly). */
|
|
int PortBurn_StartTrack(void *handle, const char *name, int frames)
|
|
{
|
|
PBHandle *h = (PBHandle *)handle;
|
|
if (!h)
|
|
return pbErrNoHandle;
|
|
|
|
if (!h->staging)
|
|
return pbErrMustCallStartStaging;
|
|
|
|
if (0 == PortBurn_StartStagingTrack(h->staging, name, frames))
|
|
return pbSuccess;
|
|
else
|
|
return pbErrCannotCreateStagingFile;
|
|
}
|
|
|
|
/* Add one frame of audio to the current track. The buffer must be exactly
|
|
1176 elements long, containing interleaved left and right audio samples.
|
|
The values should be signed 16-bit numbers in the native endianness of
|
|
this computer. */
|
|
int PortBurn_AddFrame(void *handle, short *buffer)
|
|
{
|
|
PBHandle *h = (PBHandle *)handle;
|
|
|
|
if (!h)
|
|
return pbErrNoHandle;
|
|
|
|
if (!h->staging)
|
|
return pbErrMustCallStartTrack;
|
|
|
|
if (0 == PortBurn_AddStagingFrame(h->staging, buffer))
|
|
return pbSuccess;
|
|
else
|
|
return pbErrCannotWriteToStagingFile;
|
|
}
|
|
|
|
/* Finish the current audio track. */
|
|
int PortBurn_EndTrack(void *handle)
|
|
{
|
|
PBHandle *h = (PBHandle *)handle;
|
|
DRAudioTrackRef track;
|
|
Boolean isDirectory;
|
|
const char *filename;
|
|
FSRef fsref;
|
|
int index;
|
|
|
|
if (!h)
|
|
return pbErrNoHandle;
|
|
|
|
if (!h->staging)
|
|
return pbErrMustCallStartStaging;
|
|
|
|
if (0 != PortBurn_EndStagingTrack(h->staging))
|
|
return pbErrCannotStageTrack;
|
|
|
|
index = PortBurn_GetNumStagedTracks(h->staging);
|
|
if (index <= 0)
|
|
return pbErrCannotStageTrack;
|
|
|
|
filename = PortBurn_GetStagedFilename(h->staging, index - 1);
|
|
printf("Filename: '%s'\n", filename);
|
|
h->err = FSPathMakeRef((const UInt8*)filename, &fsref, &isDirectory);
|
|
if (h->err != noErr)
|
|
return pbErrCannotAccessStagedFile;
|
|
if (isDirectory)
|
|
return pbErrCannotAccessStagedFile;
|
|
|
|
track = DRAudioTrackCreate(&fsref);
|
|
|
|
CFArrayAppendValue(h->trackArray, track);
|
|
CFRelease(track);
|
|
|
|
if (track)
|
|
return pbSuccess;
|
|
else
|
|
return pbErrCannotUseStagedFileForBurning;
|
|
}
|
|
|
|
/* Begin burning the disc. */
|
|
int PortBurn_StartBurning(void *handle)
|
|
{
|
|
PBHandle *h = (PBHandle *)handle;
|
|
|
|
if (!h)
|
|
return pbErrNoHandle;
|
|
|
|
if (!h->device)
|
|
return pbErrDeviceNotOpen;
|
|
|
|
h->burn = DRBurnCreate(h->device);
|
|
if (!h->burn)
|
|
return pbErrCannotPrepareToBurn;
|
|
|
|
h->err = DRBurnWriteLayout(h->burn, h->trackArray);
|
|
if (h->err != noErr) {
|
|
DRBurnAbort(h->burn);
|
|
CFRelease(h->burn);
|
|
h->burn = NULL;
|
|
return pbErrCannotStartBurning;
|
|
}
|
|
|
|
h->frac = 0.0;
|
|
|
|
return pbSuccess;
|
|
}
|
|
|
|
/* Cancel if burning was in progress. It might take a while for
|
|
this to take effect; wait until GetStatus says 1.0 to close
|
|
the device. */
|
|
int PortBurn_CancelBurning(void *handle)
|
|
{
|
|
PBHandle *h = (PBHandle *)handle;
|
|
|
|
if (!h)
|
|
return pbErrNoHandle;
|
|
|
|
if (!h->burn)
|
|
return pbErrNotCurrentlyBurning;
|
|
|
|
DRBurnAbort(h->burn);
|
|
|
|
return pbSuccess;
|
|
}
|
|
|
|
/* During burning, returns the fraction complete in the given
|
|
float pointer, from 0.0 to 1.0. If this function returns
|
|
nonzero, the disc burning has failed and should be aborted.
|
|
If *out_fraction_complete is equal to 1.0, the burning is done;
|
|
you can call PortBurn_CloseDevice.
|
|
*/
|
|
|
|
int PortBurn_GetStatus(void *handle, float *out_fraction_complete)
|
|
{
|
|
PBHandle *h = (PBHandle *)handle;
|
|
CFDictionaryRef status;
|
|
CFStringRef stateRef;
|
|
CFNumberRef fracRef;
|
|
CFNumberRef trackNumRef;
|
|
float frac = 0.0;
|
|
int trackNum = 0;
|
|
|
|
*out_fraction_complete = h->frac;
|
|
|
|
if (!h)
|
|
return pbErrNoHandle;
|
|
|
|
if (!h->burn)
|
|
return pbErrNotCurrentlyBurning;
|
|
|
|
status = DRBurnCopyStatus(h->burn);
|
|
if (!status)
|
|
return pbErrCannotGetBurnStatus;
|
|
|
|
trackNumRef = (CFNumberRef)
|
|
CFDictionaryGetValue(status, kDRStatusCurrentTrackKey);
|
|
if (trackNumRef != NULL) {
|
|
CFNumberGetValue(trackNumRef, kCFNumberIntType, &trackNum);
|
|
}
|
|
|
|
fracRef = (CFNumberRef)
|
|
CFDictionaryGetValue(status, kDRStatusPercentCompleteKey);
|
|
if (fracRef != NULL) {
|
|
CFNumberGetValue(fracRef, kCFNumberFloatType, &frac);
|
|
}
|
|
|
|
stateRef = (CFStringRef)
|
|
CFDictionaryGetValue(status, kDRStatusStateKey);
|
|
if (stateRef == NULL) {
|
|
/* Stick with the last percentage */
|
|
return pbSuccess;
|
|
}
|
|
|
|
if (CFEqual(stateRef, kDRStatusStateNone)) {
|
|
/* Stick with the last percentage */
|
|
return pbSuccess;
|
|
}
|
|
|
|
if (CFEqual(stateRef, kDRStatusStatePreparing)) {
|
|
/* This takes about 1 second */
|
|
h->frac = 0.01;
|
|
}
|
|
|
|
if (CFEqual(stateRef, kDRStatusStateSessionOpen)) {
|
|
/* This takes about 3 seconds */
|
|
h->frac = 0.02;
|
|
}
|
|
|
|
if (CFEqual(stateRef, kDRStatusStateTrackOpen) ||
|
|
CFEqual(stateRef, kDRStatusStateTrackWrite) ||
|
|
CFEqual(stateRef, kDRStatusStateTrackClose) ||
|
|
CFEqual(stateRef, kDRStatusStateSessionClose)) {
|
|
/* The fraction ("percentage") complete will be valid during
|
|
the majority of this range */
|
|
float newFrac = 0.0;
|
|
if (frac > 0.0 && frac <= 1.0)
|
|
newFrac = frac;
|
|
|
|
/* Scale it to the range 0.05 - 0.99 */
|
|
newFrac = 0.05 + 0.94 * newFrac;
|
|
|
|
/* Only use that value if it's larger than the previous value */
|
|
if (newFrac > h->frac)
|
|
h->frac = newFrac;
|
|
}
|
|
|
|
if (CFEqual(stateRef, kDRStatusStateDone)) {
|
|
/* Returning a fraction complete of 1.0 means we're done! */
|
|
h->frac = 1.0;
|
|
}
|
|
|
|
if (CFEqual(stateRef, kDRStatusStateFailed)) {
|
|
CFRelease(status);
|
|
return pbErrBurnFailed;
|
|
}
|
|
|
|
*out_fraction_complete = h->frac;
|
|
|
|
CFRelease(status);
|
|
return pbSuccess;
|
|
}
|