bool OggSpeexMetadata::ReadMetadata(CFErrorRef *error)
{
	// Start from scratch
	CFDictionaryRemoveAllValues(mMetadata);
	CFDictionaryRemoveAllValues(mChangedMetadata);
	
	UInt8 buf [PATH_MAX];
	if(!CFURLGetFileSystemRepresentation(mURL, false, buf, PATH_MAX))
		return false;
	
	// TODO: Use unique_ptr once the switch to C++11 STL is made
	std::auto_ptr<TagLib::FileStream> stream(new TagLib::FileStream(reinterpret_cast<const char *>(buf), true));
	if(!stream->isOpen()) {
		if(error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” could not be opened for reading."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Input/output error"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file may have been renamed, moved, deleted, or you may not have appropriate permissions."), "");

			*error = CreateErrorForURL(AudioMetadataErrorDomain, AudioMetadataInputOutputError, description, mURL, failureReason, recoverySuggestion);

			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
		}

		return false;
	}

	TagLib::Ogg::Speex::File file(stream.get());
	if(!file.isValid()) {
		if(nullptr != error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid Ogg Speex file."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Not an Ogg Speex file"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(AudioMetadataErrorDomain, AudioMetadataInputOutputError, description, mURL, failureReason, recoverySuggestion);
			
			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
		}
		
		return false;
	}
	
	CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("Ogg Speex"));
	
	if(file.audioProperties())
		AddAudioPropertiesToDictionary(mMetadata, file.audioProperties());
	
	if(file.tag()) {
		std::vector<AttachedPicture *> pictures;
		AddXiphCommentToDictionary(mMetadata, pictures, file.tag());
		for(auto picture : pictures)
			AddSavedPicture(picture);
	}
	
	return true;
}
bool SFB::Audio::MP3Metadata::_WriteMetadata(CFErrorRef *error)
{
	UInt8 buf [PATH_MAX];
	if(!CFURLGetFileSystemRepresentation(mURL, false, buf, PATH_MAX))
		return false;

	std::unique_ptr<TagLib::FileStream> stream(new TagLib::FileStream((const char *)buf));
	if(!stream->isOpen()) {
		if(error) {
			SFB::CFString description(CFCopyLocalizedString(CFSTR("The file “%@” could not be opened for writing."), ""));
			SFB::CFString failureReason(CFCopyLocalizedString(CFSTR("Input/output error"), ""));
			SFB::CFString recoverySuggestion(CFCopyLocalizedString(CFSTR("The file may have been renamed, moved, deleted, or you may not have appropriate permissions."), ""));

			*error = CreateErrorForURL(Metadata::ErrorDomain, Metadata::InputOutputError, description, mURL, failureReason, recoverySuggestion);
		}

		return false;
	}

	TagLib::MPEG::File file(stream.get(), TagLib::ID3v2::FrameFactory::instance(), false);
	if(!file.isValid()) {
		if(error) {
			SFB::CFString description(CFCopyLocalizedString(CFSTR("The file “%@” is not a valid MPEG file."), ""));
			SFB::CFString failureReason(CFCopyLocalizedString(CFSTR("Not an MPEG file"), ""));
			SFB::CFString recoverySuggestion(CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), ""));
			
			*error = CreateErrorForURL(Metadata::ErrorDomain, Metadata::InputOutputError, description, mURL, failureReason, recoverySuggestion);
		}
		
		return false;
	}

	// APE and ID3v1 tags are only written if present, but ID3v2 tags are always written

	auto APETag = file.APETag();
	if(APETag && !APETag->isEmpty())
		SetAPETagFromMetadata(*this, APETag);

	auto ID3v1Tag = file.ID3v1Tag();
	if(ID3v1Tag && !ID3v1Tag->isEmpty())
		SetID3v1TagFromMetadata(*this, ID3v1Tag);

	SetID3v2TagFromMetadata(*this, file.ID3v2Tag(true));

	if(!file.save()) {
		if(error) {
			SFB::CFString description(CFCopyLocalizedString(CFSTR("The file “%@” is not a valid MPEG file."), ""));
			SFB::CFString failureReason(CFCopyLocalizedString(CFSTR("Unable to write metadata"), ""));
			SFB::CFString recoverySuggestion(CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), ""));
			
			*error = CreateErrorForURL(Metadata::ErrorDomain, Metadata::InputOutputError, description, mURL, failureReason, recoverySuggestion);
		}
		
		return false;
	}

	return true;
}
bool MP3Metadata::WriteMetadata(CFErrorRef *error)
{
	UInt8 buf [PATH_MAX];
	if(!CFURLGetFileSystemRepresentation(mURL, false, buf, PATH_MAX))
		return false;

	auto stream = new TagLib::FileStream(reinterpret_cast<const char *>(buf));
	TagLib::MPEG::File file(stream, TagLib::ID3v2::FrameFactory::instance(), false);
	
	if(!file.isValid()) {
		if(error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid MPEG file."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Not an MPEG file"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(AudioMetadataErrorDomain, AudioMetadataInputOutputError, description, mURL, failureReason, recoverySuggestion);
			
			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
		}
		
		return false;
	}

	// APE and ID3v1 tags are only written if present, but ID3v2 tags are always written

	auto APETag = file.APETag();
	if(APETag && !APETag->isEmpty())
		SetAPETagFromMetadata(*this, APETag);

	auto ID3v1Tag = file.ID3v1Tag();
	if(ID3v1Tag && !ID3v1Tag->isEmpty())
		SetID3v1TagFromMetadata(*this, ID3v1Tag);

	SetID3v2TagFromMetadata(*this, file.ID3v2Tag(true));

	if(!file.save()) {
		if(error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid MPEG file."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Unable to write metadata"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(AudioMetadataErrorDomain, AudioMetadataInputOutputError, description, mURL, failureReason, recoverySuggestion);
			
			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
		}
		
		return false;
	}
	
	MergeChangedMetadataIntoMetadata();
	
	return true;
}
bool SFB::Audio::WAVEMetadata::_WriteMetadata(CFErrorRef *error)
{
	UInt8 buf [PATH_MAX];
	if(!CFURLGetFileSystemRepresentation(mURL, false, buf, PATH_MAX))
		return false;

	std::unique_ptr<TagLib::FileStream> stream(new TagLib::FileStream((const char *)buf));
	if(!stream->isOpen()) {
		if(error) {
			SFB::CFString description = CFCopyLocalizedString(CFSTR("The file “%@” could not be opened for writing."), "");
			SFB::CFString failureReason = CFCopyLocalizedString(CFSTR("Input/output error"), "");
			SFB::CFString recoverySuggestion = CFCopyLocalizedString(CFSTR("The file may have been renamed, moved, deleted, or you may not have appropriate permissions."), "");

			*error = CreateErrorForURL(Metadata::ErrorDomain, Metadata::InputOutputError, description, mURL, failureReason, recoverySuggestion);
		}

		return false;
	}

	TagLib::RIFF::WAV::File file(stream.get(), false);
	if(!file.isValid()) {
		if(error) {
			SFB::CFString description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid WAVE file."), "");
			SFB::CFString failureReason = CFCopyLocalizedString(CFSTR("Not a WAVE file"), "");
			SFB::CFString recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(Metadata::ErrorDomain, Metadata::InputOutputError, description, mURL, failureReason, recoverySuggestion);
		}
		
		return false;
	}

	// An Info tag is only written if present, but ID3v2 tags are always written

	// TODO: Should other field names from the Info tag be handled?
	if(file.InfoTag())
		SetTagFromMetadata(*this, file.InfoTag());

	SetID3v2TagFromMetadata(*this, file.ID3v2Tag());

	if(!file.save()) {
		if(error) {
			SFB::CFString description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid WAVE file."), "");
			SFB::CFString failureReason = CFCopyLocalizedString(CFSTR("Unable to write metadata"), "");
			SFB::CFString recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(Metadata::ErrorDomain, Metadata::InputOutputError, description, mURL, failureReason, recoverySuggestion);
		}
		
		return false;
	}

	return true;
}
bool SFB::Audio::TrueAudioMetadata::_ReadMetadata(CFErrorRef *error)
{
    UInt8 buf [PATH_MAX];
    if(!CFURLGetFileSystemRepresentation(mURL, FALSE, buf, PATH_MAX))
        return false;

    std::unique_ptr<TagLib::FileStream> stream(new TagLib::FileStream((const char *)buf, true));
    if(!stream->isOpen()) {
        if(error) {
            SFB::CFString description = CFCopyLocalizedString(CFSTR("The file “%@” could not be opened for reading."), "");
            SFB::CFString failureReason = CFCopyLocalizedString(CFSTR("Input/output error"), "");
            SFB::CFString recoverySuggestion = CFCopyLocalizedString(CFSTR("The file may have been renamed, moved, deleted, or you may not have appropriate permissions."), "");

            *error = CreateErrorForURL(Metadata::ErrorDomain, Metadata::InputOutputError, description, mURL, failureReason, recoverySuggestion);
        }

        return false;
    }

    TagLib::TrueAudio::File file(stream.get());
    if(!file.isValid()) {
        if(nullptr != error) {
            SFB::CFString description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid True Audio file."), "");
            SFB::CFString failureReason = CFCopyLocalizedString(CFSTR("Not a True Audio file"), "");
            SFB::CFString recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");

            *error = CreateErrorForURL(Metadata::ErrorDomain, Metadata::InputOutputError, description, mURL, failureReason, recoverySuggestion);
        }

        return false;
    }

    CFDictionarySetValue(mMetadata, kFormatNameKey, CFSTR("True Audio"));

    if(file.audioProperties()) {
        auto properties = file.audioProperties();
        AddAudioPropertiesToDictionary(mMetadata, properties);

        if(properties->bitsPerSample())
            AddIntToDictionary(mMetadata, kBitsPerChannelKey, properties->bitsPerSample());
        if(properties->sampleFrames())
            AddIntToDictionary(mMetadata, kTotalFramesKey, (int)properties->sampleFrames());
    }

    // Add all tags that are present
    if(file.ID3v1Tag())
        AddID3v1TagToDictionary(mMetadata, file.ID3v1Tag());

    if(file.ID3v2Tag())
        AddID3v2TagToDictionary(mMetadata, mPictures, file.ID3v2Tag());

    return true;
}
bool MonkeysAudioMetadata::WriteMetadata(CFErrorRef *error)
{
	UInt8 buf [PATH_MAX];
	if(!CFURLGetFileSystemRepresentation(mURL, false, buf, PATH_MAX))
		return false;
	
	auto stream = new TagLib::FileStream(reinterpret_cast<const char *>(buf));
	TagLib::APE::File file(stream, false);
	
	if(!file.isValid()) {
		if(error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid Monkey's Audio file."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Not a Monkey's Audio file"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(AudioMetadataErrorDomain, AudioMetadataInputOutputError, description, mURL, failureReason, recoverySuggestion);
			
			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
		}
		
		return false;
	}

	// Although both ID3v1 and APE tags are read, only APE tags are written
	if(file.APETag())
		SetAPETagFromMetadata(*this, file.APETag());

	if(!file.save()) {
		if(error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid Monkey's Audio file."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Unable to write metadata"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(AudioMetadataErrorDomain, AudioMetadataInputOutputError, description, mURL, failureReason, recoverySuggestion);
			
			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
		}
		
		return false;
	}
	
	MergeChangedMetadataIntoMetadata();
	
	return true;
}
bool SFB::Audio::OggFLACMetadata::_WriteMetadata(CFErrorRef *error)
{
	UInt8 buf [PATH_MAX];
	if(!CFURLGetFileSystemRepresentation(mURL, false, buf, PATH_MAX))
		return false;

	std::unique_ptr<TagLib::FileStream> stream(new TagLib::FileStream((const char *)buf));
	if(!stream->isOpen()) {
		if(error) {
			SFB::CFString description = CFCopyLocalizedString(CFSTR("The file “%@” could not be opened for writing."), "");
			SFB::CFString failureReason = CFCopyLocalizedString(CFSTR("Input/output error"), "");
			SFB::CFString recoverySuggestion = CFCopyLocalizedString(CFSTR("The file may have been renamed, moved, deleted, or you may not have appropriate permissions."), "");

			*error = CreateErrorForURL(Metadata::ErrorDomain, Metadata::InputOutputError, description, mURL, failureReason, recoverySuggestion);
		}

		return false;
	}

	TagLib::Ogg::FLAC::File file(stream.get(), false);
	if(!file.isValid()) {
		if(error) {
			SFB::CFString description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid Ogg file."), "");
			SFB::CFString failureReason = CFCopyLocalizedString(CFSTR("Not an Ogg file"), "");
			SFB::CFString recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(Metadata::ErrorDomain, Metadata::InputOutputError, description, mURL, failureReason, recoverySuggestion);
		}

		return false;
	}

	SetXiphCommentFromMetadata(*this, file.tag());

	if(!file.save()) {
		if(error) {
			SFB::CFString description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid Ogg file."), "");
			SFB::CFString failureReason = CFCopyLocalizedString(CFSTR("Unable to write metadata"), "");
			SFB::CFString recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(Metadata::ErrorDomain, Metadata::InputOutputError, description, mURL, failureReason, recoverySuggestion);
		}
		
		return false;
	}

	return true;
}
bool OggVorbisMetadata::WriteMetadata(CFErrorRef *error)
{
	UInt8 buf [PATH_MAX];
	if(!CFURLGetFileSystemRepresentation(mURL, false, buf, PATH_MAX))
		return false;

	auto stream = new TagLib::FileStream(reinterpret_cast<const char *>(buf));
	TagLib::Ogg::Vorbis::File file(stream, false);
	
	if(!file.isValid()) {
		if(error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid Ogg Vorbis file."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Not an Ogg Vorbis file"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(AudioMetadataErrorDomain, AudioMetadataInputOutputError, description, mURL, failureReason, recoverySuggestion);
			
			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
		}

		return false;
	}

	SetXiphCommentFromMetadata(*this, file.tag());

	if(!file.save()) {
		if(error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid Ogg Vorbis file."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Unable to write metadata"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(AudioMetadataErrorDomain, AudioMetadataInputOutputError, description, mURL, failureReason, recoverySuggestion);
			
			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
		}
		
		return false;
	}

	MergeChangedMetadataIntoMetadata();

	return true;
}
bool SFB::Audio::OggVorbisMetadata::_ReadMetadata(CFErrorRef *error)
{
	UInt8 buf [PATH_MAX];
	if(!CFURLGetFileSystemRepresentation(mURL, false, buf, PATH_MAX))
		return false;
	
	std::unique_ptr<TagLib::FileStream> stream(new TagLib::FileStream((const char *)buf, true));
	if(!stream->isOpen()) {
		if(error) {
			SFB::CFString description = CFCopyLocalizedString(CFSTR("The file “%@” could not be opened for reading."), "");
			SFB::CFString failureReason = CFCopyLocalizedString(CFSTR("Input/output error"), "");
			SFB::CFString recoverySuggestion = CFCopyLocalizedString(CFSTR("The file may have been renamed, moved, deleted, or you may not have appropriate permissions."), "");

			*error = CreateErrorForURL(Metadata::ErrorDomain, Metadata::InputOutputError, description, mURL, failureReason, recoverySuggestion);
		}

		return false;
	}

	TagLib::Ogg::Vorbis::File file(stream.get());
	if(!file.isValid()) {
		if(nullptr != error) {
			SFB::CFString description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid Ogg Vorbis file."), "");
			SFB::CFString failureReason = CFCopyLocalizedString(CFSTR("Not an Ogg Vorbis file"), "");
			SFB::CFString recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(Metadata::ErrorDomain, Metadata::InputOutputError, description, mURL, failureReason, recoverySuggestion);
		}
		
		return false;
	}

	CFDictionarySetValue(mMetadata, kFormatNameKey, CFSTR("Ogg Vorbis"));

	if(file.audioProperties())
		AddAudioPropertiesToDictionary(mMetadata, file.audioProperties());
	
	if(file.tag())
		AddXiphCommentToDictionary(mMetadata, mPictures, file.tag());

	return true;
}
SFB::Audio::Metadata::unique_ptr SFB::Audio::Metadata::CreateMetadataForURL(CFURLRef url, CFErrorRef *error)
{
	if(nullptr == url)
		return nullptr;

	// If this is a file URL, use the extension-based resolvers
	SFB::CFString scheme = CFURLCopyScheme(url);

	// If there is no scheme the URL is invalid
	if(!scheme) {
		if(error)
			*error = CFErrorCreate(kCFAllocatorDefault, kCFErrorDomainPOSIX, EINVAL, nullptr);
		return nullptr;
	}

	if(kCFCompareEqualTo == CFStringCompare(CFSTR("file"), scheme, kCFCompareCaseInsensitive)) {
		// Verify the file exists
		SInt32 errorCode = noErr;
		SFB::CFBoolean fileExists = (CFBooleanRef)CFURLCreatePropertyFromResource(kCFAllocatorDefault, url, kCFURLFileExists, &errorCode);
		
		if(fileExists) {
			if(CFBooleanGetValue(fileExists)) {
				SFB::CFString pathExtension = CFURLCopyPathExtension(url);
				if(pathExtension) {
					
					// Some extensions (.oga for example) support multiple audio codecs (Vorbis, FLAC, Speex)

					for(auto subclassInfo : sRegisteredSubclasses) {
						if(subclassInfo.mHandlesFilesWithExtension(pathExtension)) {
							unique_ptr metadata(subclassInfo.mCreateMetadata(url));
							if(metadata->ReadMetadata(error))
								return metadata;
						}
					}
				}
			}
			else {
				LOGGER_WARNING("org.sbooth.AudioEngine.Metadata", "The requested URL doesn't exist");
				
				if(error) {
					SFB::CFString description = CFCopyLocalizedString(CFSTR("The file “%@” does not exist."), "");
					SFB::CFString failureReason = CFCopyLocalizedString(CFSTR("File not found"), "");
					SFB::CFString recoverySuggestion = CFCopyLocalizedString(CFSTR("The file may exist on removable media or may have been deleted."), "");
					
					*error = CreateErrorForURL(Metadata::ErrorDomain, Metadata::InputOutputError, description, url, failureReason, recoverySuggestion);
				}
			}
		}
		else
			LOGGER_WARNING("org.sbooth.AudioEngine.Metadata", "CFURLCreatePropertyFromResource failed: " << errorCode);		
	}

	return nullptr;
}
bool WavPackMetadata::ReadMetadata(CFErrorRef *error)
{
	// Start from scratch
	CFDictionaryRemoveAllValues(mMetadata);
	CFDictionaryRemoveAllValues(mChangedMetadata);

	UInt8 buf [PATH_MAX];
	if(!CFURLGetFileSystemRepresentation(mURL, FALSE, buf, PATH_MAX))
		return false;

	auto stream = new TagLib::FileStream(reinterpret_cast<const char *>(buf), true);
	TagLib::WavPack::File file(stream);

	if(!file.isValid()) {
		if(nullptr != error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid WavPack file."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Not a WavPack file"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(AudioMetadataErrorDomain, AudioMetadataInputOutputError, description, mURL, failureReason, recoverySuggestion);
			
			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
		}

		return false;
	}
	
	CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("WavPack"));
	
	if(file.audioProperties()) {
		auto properties = file.audioProperties();
		AddAudioPropertiesToDictionary(mMetadata, properties);
		
		if(properties->bitsPerSample())
			AddIntToDictionary(mMetadata, kPropertiesBitsPerChannelKey, properties->bitsPerSample());
		if(properties->sampleFrames())
			AddIntToDictionary(mMetadata, kPropertiesTotalFramesKey, properties->sampleFrames());
	}
	
	if(file.ID3v1Tag())
		AddID3v1TagToDictionary(mMetadata, file.ID3v1Tag());

	if(file.APETag()) {
		std::vector<AttachedPicture *> pictures;
		AddAPETagToDictionary(mMetadata, pictures, file.APETag());
		for(auto picture : pictures)
			AddSavedPicture(picture);
	}

	return true;
}
bool SFB::Audio::MonkeysAudioDecoder::_Open(CFErrorRef *error)
{
	auto ioInterface = 	std::unique_ptr<APEIOInterface>(new APEIOInterface(GetInputSource()));

	auto decompressor = std::unique_ptr<APE::IAPEDecompress>(CreateIAPEDecompressEx(ioInterface.get(), nullptr));
	if(!decompressor) {
		if(error) {
			SFB::CFString description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid Monkey's Audio file."), "");
			SFB::CFString failureReason = CFCopyLocalizedString(CFSTR("Not a Monkey's Audio file"), "");
			SFB::CFString recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(Decoder::ErrorDomain, Decoder::InputOutputError, description, mInputSource->GetURL(), failureReason, recoverySuggestion);
		}
		
		return false;
	}

	mDecompressor = std::move(decompressor);
	mIOInterface = std::move(ioInterface);

	// The file format
	mFormat.mFormatID			= kAudioFormatLinearPCM;
	mFormat.mFormatFlags		= kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
	
	mFormat.mBitsPerChannel		= (UInt32)mDecompressor->GetInfo(APE::APE_INFO_BITS_PER_SAMPLE);
	mFormat.mSampleRate			= mDecompressor->GetInfo(APE::APE_INFO_SAMPLE_RATE);
	mFormat.mChannelsPerFrame	= (UInt32)mDecompressor->GetInfo(APE::APE_INFO_CHANNELS);
	
	mFormat.mBytesPerPacket		= (mFormat.mBitsPerChannel / 8) * mFormat.mChannelsPerFrame;
	mFormat.mFramesPerPacket	= 1;
	mFormat.mBytesPerFrame		= mFormat.mBytesPerPacket * mFormat.mFramesPerPacket;
	
	mFormat.mReserved			= 0;
	
	// Set up the source format
	mSourceFormat.mFormatID				= 'APE ';
	
	mSourceFormat.mSampleRate			= mFormat.mSampleRate;
	mSourceFormat.mChannelsPerFrame		= mFormat.mChannelsPerFrame;
	mSourceFormat.mBitsPerChannel		= mFormat.mBitsPerChannel;

	switch(mFormat.mChannelsPerFrame) {
		case 1:		mChannelLayout = ChannelLayout::ChannelLayoutWithTag(kAudioChannelLayoutTag_Mono);			break;
		case 2:		mChannelLayout = ChannelLayout::ChannelLayoutWithTag(kAudioChannelLayoutTag_Stereo);		break;
		case 4:		mChannelLayout = ChannelLayout::ChannelLayoutWithTag(kAudioChannelLayoutTag_Quadraphonic);	break;
	}

	return true;
}
bool SFB::Audio::MODDecoder::_Open(CFErrorRef *error)
{
	dfs.open = nullptr;
	dfs.skip = skip_callback;
	dfs.getc = getc_callback;
	dfs.getnc = getnc_callback;
	dfs.close = close_callback;

	df = unique_DUMBFILE_ptr(dumbfile_open_ex(this, &dfs), dumbfile_close);
	if(!df) {
		return false;
	}

	SFB::CFString pathExtension = CFURLCopyPathExtension(GetURL());
	if(nullptr == pathExtension)
		return false;

	// Attempt to create the appropriate decoder based on the file's extension
	if(kCFCompareEqualTo == CFStringCompare(pathExtension, CFSTR("it"), kCFCompareCaseInsensitive))
		duh = unique_DUH_ptr(dumb_read_it(df.get()), unload_duh);
	else if(kCFCompareEqualTo == CFStringCompare(pathExtension, CFSTR("xm"), kCFCompareCaseInsensitive))
		duh = unique_DUH_ptr(dumb_read_xm(df.get()), unload_duh);
	else if(kCFCompareEqualTo == CFStringCompare(pathExtension, CFSTR("s3m"), kCFCompareCaseInsensitive))
		duh = unique_DUH_ptr(dumb_read_s3m(df.get()), unload_duh);
	else if(kCFCompareEqualTo == CFStringCompare(pathExtension, CFSTR("mod"), kCFCompareCaseInsensitive))
		duh = unique_DUH_ptr(dumb_read_mod(df.get()), unload_duh);
	
	if(!duh) {
		if(error) {
			SFB::CFString description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid MOD file."), "");
			SFB::CFString failureReason = CFCopyLocalizedString(CFSTR("Not a MOD file"), "");
			SFB::CFString recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(Decoder::ErrorDomain, Decoder::InputOutputError, description, mInputSource->GetURL(), failureReason, recoverySuggestion);
		}
		
		return false;
	}

	// NB: This must change if the sample rate changes because it is based on 65536 Hz
	mTotalFrames = duh_get_length(duh.get());

	dsr = unique_DUH_SIGRENDERER_ptr(duh_start_sigrenderer(duh.get(), 0, 2, 0), duh_end_sigrenderer);
	if(!dsr) {
		if(error) {
			SFB::CFString description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid MOD file."), "");
			SFB::CFString failureReason = CFCopyLocalizedString(CFSTR("Not a MOD file"), "");
			SFB::CFString recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(Decoder::ErrorDomain, Decoder::InputOutputError, description, mInputSource->GetURL(), failureReason, recoverySuggestion);
		}

		return false;
	}
	
	// Generate interleaved 2 channel 44.1 16-bit output
	mFormat.mFormatID			= kAudioFormatLinearPCM;
	mFormat.mFormatFlags		= kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
	
	mFormat.mSampleRate			= DUMB_SAMPLE_RATE;
	mFormat.mChannelsPerFrame	= DUMB_CHANNELS;
	mFormat.mBitsPerChannel		= DUMB_BIT_DEPTH;
	
	mFormat.mBytesPerPacket		= (mFormat.mBitsPerChannel / 8) * mFormat.mChannelsPerFrame;
	mFormat.mFramesPerPacket	= 1;
	mFormat.mBytesPerFrame		= mFormat.mBytesPerPacket * mFormat.mFramesPerPacket;
	
	mFormat.mReserved			= 0;
	
	// Set up the source format
	mSourceFormat.mFormatID				= 'MOD ';
	
	mSourceFormat.mSampleRate			= DUMB_SAMPLE_RATE;
	mSourceFormat.mChannelsPerFrame		= DUMB_CHANNELS;
	
	// Setup the channel layout
	mChannelLayout = ChannelLayout::ChannelLayoutWithTag(kAudioChannelLayoutTag_Stereo);

	return true;
}
bool SFB::Audio::TrueAudioDecoder::_Open(CFErrorRef *error)
{
	mCallbacks				= unique_callback_wrapper_ptr(new TTA_io_callback_wrapper);
	mCallbacks->iocb.read	= read_callback;
	mCallbacks->iocb.write	= nullptr;
	mCallbacks->iocb.seek	= seek_callback;
	mCallbacks->decoder		= this;

	TTA_info streamInfo;

	try {
		mDecoder = unique_tta_ptr(new tta::tta_decoder((TTA_io_callback *)mCallbacks.get()));
		mDecoder->init_get_info(&streamInfo, 0);
	}
	catch(tta::tta_exception e) {
		LOGGER_CRIT("org.sbooth.AudioEngine.Decoder.TrueAudio", "Error creating True Audio decoder: " << e.code());
	}

	if(!mDecoder) {
		if(error) {
			SFB::CFString description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid True Audio file."), "");
			SFB::CFString failureReason = CFCopyLocalizedString(CFSTR("Not a True Audio file"), "");
			SFB::CFString recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(Decoder::ErrorDomain, Decoder::InputOutputError, description, mInputSource->GetURL(), failureReason, recoverySuggestion);
		}

		return false;
	}
	
	mFormat.mFormatID			= kAudioFormatLinearPCM;
	mFormat.mFormatFlags		= kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsSignedInteger;
	
	mFormat.mSampleRate			= streamInfo.sps;
	mFormat.mChannelsPerFrame	= streamInfo.nch;
	mFormat.mBitsPerChannel		= streamInfo.bps;
	
	mFormat.mBytesPerPacket		= ((streamInfo.bps + 7) / 8) * mFormat.mChannelsPerFrame;
	mFormat.mFramesPerPacket	= 1;
	mFormat.mBytesPerFrame		= mFormat.mBytesPerPacket * mFormat.mFramesPerPacket;
	
	mFormat.mReserved			= 0;
	
	// Support 4 to 32 bits per sample (True Audio may support more or less, but the documentation didn't say)
	switch(mFormat.mBitsPerChannel) {
		case 8:
		case 16:
		case 24:
		case 32:
			mFormat.mFormatFlags |= kAudioFormatFlagIsPacked;
			break;

		case 4 ... 7:
		case 9 ... 15:
		case 17 ... 23:
		case 25 ... 31:
			// Align high because Apple's AudioConverter doesn't handle low alignment
			mFormat.mFormatFlags |= kAudioFormatFlagIsAlignedHigh;
			break;

		default:
		{
			LOGGER_CRIT("org.sbooth.AudioEngine.Decoder.TrueAudio", "Unsupported bit depth: " << mFormat.mBitsPerChannel)

			if(error) {
				SFB::CFString description = CFCopyLocalizedString(CFSTR("The file “%@” is not a supported True Audio file."), "");
				SFB::CFString failureReason = CFCopyLocalizedString(CFSTR("Bit depth not supported"), "");
				SFB::CFString recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's bit depth is not supported."), "");
				
				*error = CreateErrorForURL(Decoder::ErrorDomain, Decoder::FileFormatNotSupportedError, description, mInputSource->GetURL(), failureReason, recoverySuggestion);
			}

			return false;
		}
	}

	// Set up the source format
	mSourceFormat.mFormatID				= 'TTA ';

	mSourceFormat.mSampleRate			= streamInfo.sps;
	mSourceFormat.mChannelsPerFrame		= streamInfo.nch;
	mSourceFormat.mBitsPerChannel		= streamInfo.bps;

	// Setup the channel layout
	switch(streamInfo.nch) {
		case 1:		mChannelLayout = ChannelLayout::ChannelLayoutWithTag(kAudioChannelLayoutTag_Mono);			break;
		case 2:		mChannelLayout = ChannelLayout::ChannelLayoutWithTag(kAudioChannelLayoutTag_Stereo);		break;
		case 4:		mChannelLayout = ChannelLayout::ChannelLayoutWithTag(kAudioChannelLayoutTag_Quadraphonic);	break;
	}

	mTotalFrames = streamInfo.samples;

	return true;
}
bool FLACMetadata::ReadMetadata(CFErrorRef *error)
{
	// Start from scratch
	CFDictionaryRemoveAllValues(mMetadata);
	CFDictionaryRemoveAllValues(mChangedMetadata);

	UInt8 buf [PATH_MAX];
	if(!CFURLGetFileSystemRepresentation(mURL, false, buf, PATH_MAX))
		return false;

	auto stream = new TagLib::FileStream(reinterpret_cast<const char *>(buf), true);
	TagLib::FLAC::File file(stream, TagLib::ID3v2::FrameFactory::instance());

	if(!file.isValid()) {
		if(nullptr != error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid FLAC file."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Not a FLAC file"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(AudioMetadataErrorDomain, AudioMetadataInputOutputError, description, mURL, failureReason, recoverySuggestion);
			
			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
		}

		return false;
	}

	CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("FLAC"));

	if(file.audioProperties()) {
		auto properties = file.audioProperties();
		AddAudioPropertiesToDictionary(mMetadata, properties);

		if(properties->sampleWidth())
			AddIntToDictionary(mMetadata, kPropertiesBitsPerChannelKey, properties->sampleWidth());
		if(properties->sampleFrames())
			AddLongLongToDictionary(mMetadata, kPropertiesTotalFramesKey, properties->sampleFrames());
	}

	// Add all tags that are present
	if(file.ID3v1Tag())
		AddID3v1TagToDictionary(mMetadata, file.ID3v1Tag());

	if(file.ID3v2Tag()) {
		std::vector<AttachedPicture *> pictures;
		AddID3v2TagToDictionary(mMetadata, pictures, file.ID3v2Tag());
		for(auto picture : pictures)
			AddSavedPicture(picture);
	}

	if(file.xiphComment()) {
		std::vector<AttachedPicture *> pictures;
		AddXiphCommentToDictionary(mMetadata, pictures, file.xiphComment());
		for(auto picture : pictures)
			AddSavedPicture(picture);
	}

	// Add album art
	for(auto iter : file.pictureList()) {
		CFDataRef data = CFDataCreate(kCFAllocatorDefault, reinterpret_cast<const UInt8 *>(iter->data().data()), iter->data().size());

		CFStringRef description = nullptr;
		if(!iter->description().isNull())
			description = CFStringCreateWithCString(kCFAllocatorDefault, iter->description().toCString(true), kCFStringEncodingUTF8);

		AttachedPicture *picture = new AttachedPicture(data, static_cast<AttachedPicture::Type>(iter->type()), description);
		AddSavedPicture(picture);

		if(data)
			CFRelease(data), data = nullptr;

		if(description)
			CFRelease(description), description = nullptr;
	}

	return true;
}
bool FLACMetadata::WriteMetadata(CFErrorRef *error)
{
	UInt8 buf [PATH_MAX];
	if(!CFURLGetFileSystemRepresentation(mURL, false, buf, PATH_MAX))
		return false;

	auto stream = new TagLib::FileStream(reinterpret_cast<const char *>(buf));
	TagLib::FLAC::File file(stream, TagLib::ID3v2::FrameFactory::instance(), false);

	if(!file.isValid()) {
		if(error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid FLAC file."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Not a FLAC file"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(AudioMetadataErrorDomain, AudioMetadataInputOutputError, description, mURL, failureReason, recoverySuggestion);
			
			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
		}

		return false;
	}

	SetXiphCommentFromMetadata(*this, file.xiphComment(), false);

	// Remove existing cover art
	file.removePictures();

	// Add album art
	for(auto attachedPicture : GetAttachedPictures()) {
	
		CGImageSourceRef imageSource = CGImageSourceCreateWithData(attachedPicture->GetData(), nullptr);
		if(nullptr == imageSource) {
			LOGGER_ERR("org.sbooth.AudioEngine.AudioMetadata.FLAC", "Skipping album art (unable to create image)");
			continue;
		}

		TagLib::FLAC::Picture *picture = new TagLib::FLAC::Picture;
		picture->setData(TagLib::ByteVector((const char *)CFDataGetBytePtr(attachedPicture->GetData()), (TagLib::uint)CFDataGetLength(attachedPicture->GetData())));
		picture->setType(static_cast<TagLib::FLAC::Picture::Type>(attachedPicture->GetType()));
		if(attachedPicture->GetDescription())
			picture->setDescription(TagLib::StringFromCFString(attachedPicture->GetDescription()));

		// Convert the image's UTI into a MIME type
		CFStringRef mimeType = UTTypeCopyPreferredTagWithClass(CGImageSourceGetType(imageSource), kUTTagClassMIMEType);
		if(mimeType) {
			picture->setMimeType(TagLib::StringFromCFString(mimeType));
			CFRelease(mimeType), mimeType = nullptr;
		}

		// Flesh out the height, width, and depth
		CFDictionaryRef imagePropertiesDictionary = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nullptr);
		if(imagePropertiesDictionary) {
			CFNumberRef imageWidth = (CFNumberRef)CFDictionaryGetValue(imagePropertiesDictionary, kCGImagePropertyPixelWidth);
			CFNumberRef imageHeight = (CFNumberRef)CFDictionaryGetValue(imagePropertiesDictionary, kCGImagePropertyPixelHeight);
			CFNumberRef imageDepth = (CFNumberRef)CFDictionaryGetValue(imagePropertiesDictionary, kCGImagePropertyDepth);

			int height, width, depth;

			// Ignore numeric conversion errors
			CFNumberGetValue(imageWidth, kCFNumberIntType, &width);
			CFNumberGetValue(imageHeight, kCFNumberIntType, &height);
			CFNumberGetValue(imageDepth, kCFNumberIntType, &depth);

			picture->setHeight(height);
			picture->setWidth(width);
			picture->setColorDepth(depth);

			CFRelease(imagePropertiesDictionary), imagePropertiesDictionary = nullptr;
		}

		file.addPicture(picture);

		CFRelease(imageSource), imageSource = nullptr;
	}

	if(!file.save()) {
		if(error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid FLAC file."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Unable to write metadata"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(AudioMetadataErrorDomain, AudioMetadataInputOutputError, description, mURL, failureReason, recoverySuggestion);
			
			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
		}

		return false;
	}

	MergeChangedMetadataIntoMetadata();

	return true;
}
SFB::Audio::Decoder::unique_ptr SFB::Audio::Decoder::CreateDecoderForInputSource(InputSource::unique_ptr inputSource, CFStringRef mimeType, CFErrorRef *error)
{
	if(!inputSource)
		return nullptr;

	// Open the input source if it isn't already
	if(AutomaticallyOpenDecoders() && !inputSource->IsOpen() && !inputSource->Open(error))
		return nullptr;

#if 0
	// If the input is an instance of HTTPInputSource, use the MIME type from the server
	// This code is disabled because most HTTP servers don't send the correct MIME types
	HTTPInputSource *httpInputSource = dynamic_cast<HTTPInputSource *>(inputSource);
	bool releaseMIMEType = false;
	if(!mimeType && httpInputSource && httpInputSource->IsOpen()) {
		mimeType = httpInputSource->CopyContentMIMEType();
		if(mimeType)
			releaseMIMEType = true;
	}
#endif

	// The MIME type takes precedence over the file extension
	if(mimeType) {
		for(auto subclassInfo : sRegisteredSubclasses) {
			if(subclassInfo.mHandlesMIMEType(mimeType)) {
				unique_ptr decoder(subclassInfo.mCreateDecoder(std::move(inputSource)));
				if(!AutomaticallyOpenDecoders())
					return decoder;
				else {
					 if(decoder->Open(error))
						 return decoder;
					// Take back the input source for reuse if opening fails
					else
						 inputSource = std::move(decoder->mInputSource);
				}
			}
		}

#if 0
		if(releaseMIMEType)
			CFRelease(mimeType), mimeType = nullptr;
#endif
	}

	// If no MIME type was specified, use the extension-based resolvers

	CFURLRef inputURL = inputSource->GetURL();
	if(!inputURL)
		return nullptr;

	SFB::CFString pathExtension = CFURLCopyPathExtension(inputURL);
	if(!pathExtension) {
		if(error) {
			SFB::CFString description = CFCopyLocalizedString(CFSTR("The type of the file “%@” could not be determined."), "");
			SFB::CFString failureReason = CFCopyLocalizedString(CFSTR("Unknown file type"), "");
			SFB::CFString recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may be missing or may not match the file's type."), "");
			
			*error = CreateErrorForURL(InputSource::ErrorDomain, InputSource::FileNotFoundError, description, inputURL, failureReason, recoverySuggestion);
		}

		return nullptr;
	}

	// TODO: Some extensions (.oga for example) support multiple audio codecs (Vorbis, FLAC, Speex)
	// and if openDecoder is false the wrong decoder type may be returned, since the file isn't analyzed
	// until Open() is called

	for(auto subclassInfo : sRegisteredSubclasses) {
		if(subclassInfo.mHandlesFilesWithExtension(pathExtension)) {
			unique_ptr decoder(subclassInfo.mCreateDecoder(std::move(inputSource)));
			if(!AutomaticallyOpenDecoders())
				return decoder;
			else {
				if(decoder->Open(error))
					return decoder;
				// Take back the input source for reuse if opening fails
				else
					inputSource = std::move(decoder->mInputSource);
			}
		}
	}

	return nullptr;
}
bool MP3Metadata::ReadMetadata(CFErrorRef *error)
{
	// Start from scratch
	CFDictionaryRemoveAllValues(mMetadata);
	CFDictionaryRemoveAllValues(mChangedMetadata);
	
	UInt8 buf [PATH_MAX];
	if(!CFURLGetFileSystemRepresentation(mURL, false, buf, PATH_MAX))
		return false;
	
	auto stream = new TagLib::FileStream(reinterpret_cast<const char *>(buf), true);
	TagLib::MPEG::File file(stream, TagLib::ID3v2::FrameFactory::instance());
	
	if(!file.isValid()) {
		if(nullptr != error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid MPEG file."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Not an MPEG file"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(AudioMetadataErrorDomain, AudioMetadataInputOutputError, description, mURL, failureReason, recoverySuggestion);
			
			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
		}
		
		return false;
	}
	
	CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("MP3"));

	if(file.audioProperties()) {
		auto properties = file.audioProperties();
		AddAudioPropertiesToDictionary(mMetadata, properties);

		// TODO: Is this too much information?
#if 0
		switch(properties->version()) {
			case TagLib::MPEG::Header::Version1:
				switch(properties->layer()) {
					case 1:		CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("MPEG-1 Layer I"));		break;
					case 2:		CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("MPEG-1 Layer II"));	break;
					case 3:		CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("MPEG-1 Layer III"));	break;
				}
				break;
			case TagLib::MPEG::Header::Version2:
				switch(properties->layer()) {
					case 1:		CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("MPEG-2 Layer I"));		break;
					case 2:		CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("MPEG-2 Layer II"));	break;
					case 3:		CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("MPEG-2 Layer III"));	break;
				}
				break;
			case TagLib::MPEG::Header::Version2_5:
				switch(properties->layer()) {
					case 1:		CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("MPEG-2.5 Layer I"));	break;
					case 2:		CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("MPEG-2.5 Layer II"));	break;
					case 3:		CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("MPEG-2.5 Layer III"));	break;
				}
				break;
		}
#endif

		if(properties->xingHeader() && properties->xingHeader()->totalFrames())
			AddIntToDictionary(mMetadata, kPropertiesTotalFramesKey, properties->xingHeader()->totalFrames());
	}

	if(file.APETag()) {
		std::vector<AttachedPicture *> pictures;
		AddAPETagToDictionary(mMetadata, pictures, file.APETag());
		for(auto picture : pictures)
			AddSavedPicture(picture);
	}

	if(file.ID3v1Tag())
		AddID3v1TagToDictionary(mMetadata, file.ID3v1Tag());

	if(file.ID3v2Tag()) {
		std::vector<AttachedPicture *> pictures;
		AddID3v2TagToDictionary(mMetadata, pictures, file.ID3v2Tag());
		for(auto picture : pictures)
			AddSavedPicture(picture);
	}

	return true;
}
bool SFB::Audio::LibsndfileDecoder::_Open(CFErrorRef *error)
{
	// Set up the virtual IO function pointers
	SF_VIRTUAL_IO virtualIO;
	virtualIO.get_filelen	= my_sf_vio_get_filelen;
	virtualIO.seek			= my_sf_vio_seek;
	virtualIO.read			= my_sf_vio_read;
	virtualIO.write			= nullptr;
	virtualIO.tell			= my_sf_vio_tell;

	// Open the input file
	mFile = unique_SNDFILE_ptr(sf_open_virtual(&virtualIO, SFM_READ, &mFileInfo, this), sf_close);

	if(!mFile) {
		LOGGER_ERR("org.sbooth.AudioEngine.Decoder.Libsndfile", "sf_open_virtual failed: " << sf_error_number(sf_error(nullptr)));

		if(nullptr != error) {
			SFB::CFString description(CFCopyLocalizedString(CFSTR("The format of the file “%@” was not recognized."), ""));
			SFB::CFString failureReason(CFCopyLocalizedString(CFSTR("File Format Not Recognized"), ""));
			SFB::CFString recoverySuggestion(CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), ""));

			*error = CreateErrorForURL(Decoder::ErrorDomain, Decoder::InputOutputError, description, mInputSource->GetURL(), failureReason, recoverySuggestion);
		}

		return false;
	}

	// Generate interleaved PCM output
	mFormat.mFormatID			= kAudioFormatLinearPCM;

	mFormat.mSampleRate			= mFileInfo.samplerate;
	mFormat.mChannelsPerFrame	= (UInt32)mFileInfo.channels;

	int subFormat = SF_FORMAT_SUBMASK & mFileInfo.format;

	// 8-bit PCM will be high-aligned in shorts
	if(SF_FORMAT_PCM_U8 == subFormat) {
		mFormat.mFormatFlags		= kAudioFormatFlagIsAlignedHigh;

		mFormat.mBitsPerChannel		= 8;

		mFormat.mBytesPerPacket		= sizeof(short) * mFormat.mChannelsPerFrame;
		mFormat.mFramesPerPacket	= 1;
		mFormat.mBytesPerFrame		= mFormat.mBytesPerPacket * mFormat.mFramesPerPacket;

		mReadMethod					= ReadMethod::Short;
	}
	else if(SF_FORMAT_PCM_S8 == subFormat) {
		mFormat.mFormatFlags		= kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsAlignedHigh;

		mFormat.mBitsPerChannel		= 8;

		mFormat.mBytesPerPacket		= sizeof(short) * mFormat.mChannelsPerFrame;
		mFormat.mFramesPerPacket	= 1;
		mFormat.mBytesPerFrame		= mFormat.mBytesPerPacket * mFormat.mFramesPerPacket;

		mReadMethod					= ReadMethod::Short;
	}
	// 16-bit PCM
	else if(SF_FORMAT_PCM_16 == subFormat) {
		mFormat.mFormatFlags		= kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;

		mFormat.mBitsPerChannel		= 16;

		mFormat.mBytesPerPacket		= (mFormat.mBitsPerChannel / 8) * mFormat.mChannelsPerFrame;
		mFormat.mFramesPerPacket	= 1;
		mFormat.mBytesPerFrame		= mFormat.mBytesPerPacket * mFormat.mFramesPerPacket;

		mReadMethod					= ReadMethod::Short;
	}
	// 24-bit PCM will be high-aligned in ints
	else if(SF_FORMAT_PCM_24 == subFormat) {
		mFormat.mFormatFlags		= kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsAlignedHigh;

		mFormat.mBitsPerChannel		= 24;

		mFormat.mBytesPerPacket		= sizeof(int) * mFormat.mChannelsPerFrame;
		mFormat.mFramesPerPacket	= 1;
		mFormat.mBytesPerFrame		= mFormat.mBytesPerPacket * mFormat.mFramesPerPacket;

		mReadMethod					= ReadMethod::Int;
	}
	// 32-bit PCM
	else if(SF_FORMAT_PCM_32 == subFormat) {
		mFormat.mFormatFlags		= kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;

		mFormat.mBitsPerChannel		= 32;

		mFormat.mBytesPerPacket		= (mFormat.mBitsPerChannel / 8) * mFormat.mChannelsPerFrame;
		mFormat.mFramesPerPacket	= 1;
		mFormat.mBytesPerFrame		= mFormat.mBytesPerPacket * mFormat.mFramesPerPacket;

		mReadMethod					= ReadMethod::Int;
	}
	// Floating point formats
	else if(SF_FORMAT_FLOAT == subFormat) {
		mFormat.mFormatFlags		= kAudioFormatFlagsNativeFloatPacked;

		mFormat.mBitsPerChannel		= 8 * sizeof(float);

		mFormat.mBytesPerPacket		= (mFormat.mBitsPerChannel / 8) * mFormat.mChannelsPerFrame;
		mFormat.mFramesPerPacket	= 1;
		mFormat.mBytesPerFrame		= mFormat.mBytesPerPacket * mFormat.mFramesPerPacket;

		mReadMethod					= ReadMethod::Float;
	}
	else if(SF_FORMAT_DOUBLE == subFormat) {
		mFormat.mFormatFlags		= kAudioFormatFlagsNativeFloatPacked;

		mFormat.mBitsPerChannel		= 8 * sizeof(double);

		mFormat.mBytesPerPacket		= (mFormat.mBitsPerChannel / 8) * mFormat.mChannelsPerFrame;
		mFormat.mFramesPerPacket	= 1;
		mFormat.mBytesPerFrame		= mFormat.mBytesPerPacket * mFormat.mFramesPerPacket;

		mReadMethod					= ReadMethod::Double;
	}
	// Everything else will be converted to 32-bit float
	else {
		mFormat.mFormatFlags		= kAudioFormatFlagsNativeFloatPacked;

		mFormat.mBitsPerChannel		= 8 * sizeof(float);

		mFormat.mBytesPerPacket		= (mFormat.mBitsPerChannel / 8) * mFormat.mChannelsPerFrame;
		mFormat.mFramesPerPacket	= 1;
		mFormat.mBytesPerFrame		= mFormat.mBytesPerPacket * mFormat.mFramesPerPacket;

		mReadMethod					= ReadMethod::Float;
	}

	mFormat.mReserved			= 0;

	// Set up the source format
	mSourceFormat.mFormatID				= 'SNDF';

	mSourceFormat.mSampleRate			= mFileInfo.samplerate;
	mSourceFormat.mChannelsPerFrame		= (UInt32)mFileInfo.channels;

	switch(subFormat) {
		case SF_FORMAT_PCM_U8:
			mSourceFormat.mBitsPerChannel = 8;
			break;

		case SF_FORMAT_PCM_S8:
			mSourceFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger;
			mSourceFormat.mBitsPerChannel = 8;
			break;

		case SF_FORMAT_PCM_16:
			mSourceFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger;
			mSourceFormat.mBitsPerChannel = 16;
			break;

		case SF_FORMAT_PCM_24:
			mSourceFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger;
			mSourceFormat.mBitsPerChannel = 24;
			break;

		case SF_FORMAT_PCM_32:
			mSourceFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger;
			mSourceFormat.mBitsPerChannel = 32;
			break;

		case SF_FORMAT_FLOAT:
			mSourceFormat.mFormatFlags = kAudioFormatFlagIsFloat;
			mSourceFormat.mBitsPerChannel = 32;
			break;

		case SF_FORMAT_DOUBLE:
			mSourceFormat.mFormatFlags = kAudioFormatFlagIsFloat;
			mSourceFormat.mBitsPerChannel = 64;
			break;
	}

	return true;
}
bool SFB::Audio::MODMetadata::_ReadMetadata(CFErrorRef *error)
{
	UInt8 buf [PATH_MAX];
	if(!CFURLGetFileSystemRepresentation(mURL, false, buf, PATH_MAX))
		return false;

	SFB::CFString pathExtension = CFURLCopyPathExtension(mURL);
	if(!pathExtension)
		return false;

	bool fileIsValid = false;
	if(kCFCompareEqualTo == CFStringCompare(pathExtension, CFSTR("it"), kCFCompareCaseInsensitive)) {
		std::unique_ptr<TagLib::FileStream> stream(new TagLib::FileStream((const char *)buf, true));
		if(!stream->isOpen()) {
			if(error) {
				SFB::CFString description = CFCopyLocalizedString(CFSTR("The file “%@” could not be opened for reading."), "");
				SFB::CFString failureReason = CFCopyLocalizedString(CFSTR("Input/output error"), "");
				SFB::CFString recoverySuggestion = CFCopyLocalizedString(CFSTR("The file may have been renamed, moved, deleted, or you may not have appropriate permissions."), "");

				*error = CreateErrorForURL(Metadata::ErrorDomain, Metadata::InputOutputError, description, mURL, failureReason, recoverySuggestion);
			}

			return false;
		}

		TagLib::IT::File file(stream.get());
		if(file.isValid()) {
			fileIsValid = true;
			CFDictionarySetValue(mMetadata, kFormatNameKey, CFSTR("MOD (Impulse Tracker)"));

			if(file.audioProperties())
				AddAudioPropertiesToDictionary(mMetadata, file.audioProperties());

			if(file.tag())
				AddTagToDictionary(mMetadata, file.tag());
		}
	}
	else if(kCFCompareEqualTo == CFStringCompare(pathExtension, CFSTR("xm"), kCFCompareCaseInsensitive)) {
		std::unique_ptr<TagLib::FileStream> stream(new TagLib::FileStream((const char *)buf, true));
		if(!stream->isOpen()) {
			if(error) {
				SFB::CFString description = CFCopyLocalizedString(CFSTR("The file “%@” could not be opened for reading."), "");
				SFB::CFString failureReason = CFCopyLocalizedString(CFSTR("Input/output error"), "");
				SFB::CFString recoverySuggestion = CFCopyLocalizedString(CFSTR("The file may have been renamed, moved, deleted, or you may not have appropriate permissions."), "");

				*error = CreateErrorForURL(Metadata::ErrorDomain, Metadata::InputOutputError, description, mURL, failureReason, recoverySuggestion);
			}

			return false;
		}

		TagLib::XM::File file(stream.get());
		if(file.isValid()) {
			fileIsValid = true;
			CFDictionarySetValue(mMetadata, kFormatNameKey, CFSTR("MOD (Extended Module)"));

			if(file.audioProperties())
				AddAudioPropertiesToDictionary(mMetadata, file.audioProperties());

			if(file.tag())
				AddTagToDictionary(mMetadata, file.tag());
		}
	}
	else if(kCFCompareEqualTo == CFStringCompare(pathExtension, CFSTR("s3m"), kCFCompareCaseInsensitive)) {
		std::unique_ptr<TagLib::FileStream> stream(new TagLib::FileStream((const char *)buf, true));
		if(!stream->isOpen()) {
			if(error) {
				SFB::CFString description = CFCopyLocalizedString(CFSTR("The file “%@” could not be opened for reading."), "");
				SFB::CFString failureReason = CFCopyLocalizedString(CFSTR("Input/output error"), "");
				SFB::CFString recoverySuggestion = CFCopyLocalizedString(CFSTR("The file may have been renamed, moved, deleted, or you may not have appropriate permissions."), "");

				*error = CreateErrorForURL(Metadata::ErrorDomain, Metadata::InputOutputError, description, mURL, failureReason, recoverySuggestion);
			}

			return false;
		}

		TagLib::S3M::File file(stream.get());
		if(file.isValid()) {
			fileIsValid = true;
			CFDictionarySetValue(mMetadata, kFormatNameKey, CFSTR("MOD (ScreamTracker III)"));

			if(file.audioProperties())
				AddAudioPropertiesToDictionary(mMetadata, file.audioProperties());

			if(file.tag())
				AddTagToDictionary(mMetadata, file.tag());
		}
	}
	else if(kCFCompareEqualTo == CFStringCompare(pathExtension, CFSTR("mod"), kCFCompareCaseInsensitive)) {
		std::unique_ptr<TagLib::FileStream> stream(new TagLib::FileStream((const char *)buf, true));
		if(!stream->isOpen()) {
			if(error) {
				SFB::CFString description = CFCopyLocalizedString(CFSTR("The file “%@” could not be opened for reading."), "");
				SFB::CFString failureReason = CFCopyLocalizedString(CFSTR("Input/output error"), "");
				SFB::CFString recoverySuggestion = CFCopyLocalizedString(CFSTR("The file may have been renamed, moved, deleted, or you may not have appropriate permissions."), "");

				*error = CreateErrorForURL(Metadata::ErrorDomain, Metadata::InputOutputError, description, mURL, failureReason, recoverySuggestion);
			}

			return false;
		}

		TagLib::Mod::File file(stream.get());
		if(file.isValid()) {
			fileIsValid = true;
			CFDictionarySetValue(mMetadata, kFormatNameKey, CFSTR("MOD (Protracker)"));

			if(file.audioProperties())
				AddAudioPropertiesToDictionary(mMetadata, file.audioProperties());

			if(file.tag())
				AddTagToDictionary(mMetadata, file.tag());
		}
	}

	if(!fileIsValid) {
		if(error) {
			SFB::CFString description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid MOD file."), "");
			SFB::CFString failureReason = CFCopyLocalizedString(CFSTR("Not a MOD file"), "");
			SFB::CFString recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(Metadata::ErrorDomain, Metadata::InputOutputError, description, mURL, failureReason, recoverySuggestion);
		}
		
		return false;
	}

	return true;
}
bool TrueAudioDecoder::Open(CFErrorRef *error)
{
	if(IsOpen()) {
		LOGGER_WARNING("org.sbooth.AudioEngine.AudioDecoder.TrueAudio", "Open() called on an AudioDecoder that is already open");		
		return true;
	}
	
	// Ensure the input source is open
	if(!mInputSource->IsOpen() && !mInputSource->Open(error))
		return false;

	mCallbacks				= new TTA_io_callback_wrapper;
	mCallbacks->iocb.read	= read_callback;
	mCallbacks->iocb.write	= nullptr;
	mCallbacks->iocb.seek	= seek_callback;
	mCallbacks->decoder		= this;

	TTA_info streamInfo;

	try {
		mDecoder = new tta::tta_decoder((TTA_io_callback *)mCallbacks);
		mDecoder->init_get_info(&streamInfo, 0);
	}
	catch(tta::tta_exception e) {
		LOGGER_WARNING("org.sbooth.AudioEngine.AudioDecoder.TrueAudio", "Error creating True Audio decoder: " << e.code());
		if(mDecoder)
			delete mDecoder, mDecoder = nullptr;
	}

	if(nullptr == mDecoder) {
		if(error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid True Audio file."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Not a True Audio file"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(AudioDecoderErrorDomain, AudioDecoderInputOutputError, description, mInputSource->GetURL(), failureReason, recoverySuggestion);
			
			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
		}

		return false;
	}

	
	mFormat.mFormatID			= kAudioFormatLinearPCM;
	mFormat.mFormatFlags		= kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsSignedInteger;
	
	mFormat.mSampleRate			= streamInfo.sps;
	mFormat.mChannelsPerFrame	= streamInfo.nch;
	mFormat.mBitsPerChannel		= streamInfo.bps;
	
	mFormat.mBytesPerPacket		= ((streamInfo.bps + 7) / 8) * mFormat.mChannelsPerFrame;
	mFormat.mFramesPerPacket	= 1;
	mFormat.mBytesPerFrame		= mFormat.mBytesPerPacket * mFormat.mFramesPerPacket;
	
	mFormat.mReserved			= 0;
	
	// Support 4 to 32 bits per sample (True Audio may support more or less, but the documentation didn't say)
	switch(mFormat.mBitsPerChannel) {
		case 8:
		case 16:
		case 24:
		case 32:
			mFormat.mFormatFlags |= kAudioFormatFlagIsPacked;
			break;

		case 4 ... 7:
		case 9 ... 15:
		case 17 ... 23:
		case 25 ... 31:
			// Align high because Apple's AudioConverter doesn't handle low alignment
			mFormat.mFormatFlags |= kAudioFormatFlagIsAlignedHigh;
			break;

		default:
		{
			LOGGER_ERR("org.sbooth.AudioEngine.AudioDecoder.TrueAudio", "Unsupported bit depth: " << mFormat.mBitsPerChannel)

			if(error) {
				CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a supported True Audio file."), "");
				CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Bit depth not supported"), "");
				CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's bit depth is not supported."), "");
				
				*error = CreateErrorForURL(AudioDecoderErrorDomain, AudioDecoderFileFormatNotSupportedError, description, mInputSource->GetURL(), failureReason, recoverySuggestion);
				
				CFRelease(description), description = nullptr;
				CFRelease(failureReason), failureReason = nullptr;
				CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
			}

			delete mDecoder, mDecoder = nullptr;
			delete mCallbacks, mCallbacks = nullptr;

			return false;
		}
	}

	// Set up the source format
	mSourceFormat.mFormatID				= 'TTA ';

	mSourceFormat.mSampleRate			= streamInfo.sps;
	mSourceFormat.mChannelsPerFrame		= streamInfo.nch;
	mSourceFormat.mBitsPerChannel		= streamInfo.bps;

	// Setup the channel layout
	mChannelLayout = CreateChannelLayoutWithTag(streamInfo.nch);

	mTotalFrames = streamInfo.samples;

	mIsOpen = true;
	return true;
}
bool OggSpeexDecoder::Open(CFErrorRef *error)
{
	if(IsOpen()) {
		LOGGER_WARNING("org.sbooth.AudioEngine.AudioDecoder.OggSpeex", "Open() called on an AudioDecoder that is already open");		
		return true;
	}

	// Ensure the input source is open
	if(!mInputSource->IsOpen() && !mInputSource->Open(error))
		return false;

	// Initialize Ogg data struct
	ogg_sync_init(&mOggSyncState);

	// Get the ogg buffer for writing
	char *data = ogg_sync_buffer(&mOggSyncState, READ_SIZE_BYTES);
	
	// Read bitstream from input file
	ssize_t bytesRead = GetInputSource()->Read(data, READ_SIZE_BYTES);
	if(-1 == bytesRead) {
		if(error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” could not be read."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Read error"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("Unable to read from the input file."), "");
			
			*error = CreateErrorForURL(AudioDecoderErrorDomain, AudioDecoderInputOutputError, description, mInputSource->GetURL(), failureReason, recoverySuggestion);
			
			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
		}
		
		ogg_sync_destroy(&mOggSyncState);
		return false;
	}
	
	// Tell the sync layer how many bytes were written to its internal buffer
	int result = ogg_sync_wrote(&mOggSyncState, bytesRead);
	if(-1 == result) {
		if(error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid Ogg file."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Not an Ogg file"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(AudioDecoderErrorDomain, AudioDecoderInputOutputError, description, mInputSource->GetURL(), failureReason, recoverySuggestion);
			
			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
		}
		
		ogg_sync_destroy(&mOggSyncState);
		return false;
	}
	
	// Turn the data we wrote into an ogg page
	result = ogg_sync_pageout(&mOggSyncState, &mOggPage);
	if(1 != result) {
		if(error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid Ogg file."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Not an Ogg file"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(AudioDecoderErrorDomain, AudioDecoderInputOutputError, description, mInputSource->GetURL(), failureReason, recoverySuggestion);
			
			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;			
		}
		
		ogg_sync_destroy(&mOggSyncState);
		return false;
	}

	// Initialize the stream and grab the serial number
	ogg_stream_init(&mOggStreamState, ogg_page_serialno(&mOggPage));
	
	// Get the first Ogg page
	result = ogg_stream_pagein(&mOggStreamState, &mOggPage);
	if(0 != result) {
		if(error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid Ogg file."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Not an Ogg file"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(AudioDecoderErrorDomain, AudioDecoderInputOutputError, description, mInputSource->GetURL(), failureReason, recoverySuggestion);
			
			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
		}

		ogg_sync_destroy(&mOggSyncState);
		return false;
	}
	
	// Get the first packet (should be the header) from the page
	ogg_packet op;
	result = ogg_stream_packetout(&mOggStreamState, &op);
	if(1 != result) {
		if(error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid Ogg file."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Not an Ogg file"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(AudioDecoderErrorDomain, AudioDecoderInputOutputError, description, mInputSource->GetURL(), failureReason, recoverySuggestion);
			
			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;			
		}
		
		ogg_sync_destroy(&mOggSyncState);
		return false;
	}

	if(op.bytes >= 5 && !memcmp(op.packet, "Speex", 5))
		mSpeexSerialNumber = mOggStreamState.serialno;

	++mOggPacketCount;
	
	// Convert the packet to the Speex header
	SpeexHeader *header = speex_packet_to_header((char *)op.packet, static_cast<int>(op.bytes));
	if(nullptr == header) {
		if(error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid Ogg Speex file."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Not an Ogg Speex file"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(AudioDecoderErrorDomain, AudioDecoderFileFormatNotRecognizedError, description, mInputSource->GetURL(), failureReason, recoverySuggestion);
			
			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
		}

		ogg_sync_destroy(&mOggSyncState);
		return false;
	}
	else if(SPEEX_NB_MODES <= header->mode) {
		if(error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The Speex mode in the file “%@” is not supported."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Unsupported Ogg Speex file mode"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("This file may have been encoded with a newer version of Speex."), "");
			
			*error = CreateErrorForURL(AudioDecoderErrorDomain, AudioDecoderFileFormatNotSupportedError, description, mInputSource->GetURL(), failureReason, recoverySuggestion);
			
			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
		}
		
		speex_header_free(header), header = nullptr;
		ogg_sync_destroy(&mOggSyncState);
		return false;
	}
	
	const SpeexMode *mode = speex_lib_get_mode(header->mode);
	if(mode->bitstream_version != header->mode_bitstream_version) {
		if(error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The Speex version in the file “%@” is not supported."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Unsupported Ogg Speex file version"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("This file was encoded with a different version of Speex."), "");
			
			*error = CreateErrorForURL(AudioDecoderErrorDomain, AudioDecoderFileFormatNotSupportedError, description, mInputSource->GetURL(), failureReason, recoverySuggestion);
			
			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
		}
		
		speex_header_free(header), header = nullptr;
		ogg_sync_destroy(&mOggSyncState);
		return false;
	}
	
	// Initialize the decoder
	mSpeexDecoder = speex_decoder_init(mode);
	if(nullptr== mSpeexDecoder) {
		if(error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("Unable to initialize the Speex decoder."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Error initializing Speex decoder"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("An unknown error occurred."), "");
			
			*error = CreateErrorForURL(AudioDecoderErrorDomain, AudioDecoderInputOutputError, description, mInputSource->GetURL(), failureReason, recoverySuggestion);
			
			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
		}
		
		speex_header_free(header), header = nullptr;
		ogg_sync_destroy(&mOggSyncState);
		return false;
	}
	
	speex_decoder_ctl(mSpeexDecoder, SPEEX_SET_SAMPLING_RATE, &header->rate);
	
	mSpeexFramesPerOggPacket = (0 == header->frames_per_packet ? 1 : header->frames_per_packet);
	mExtraSpeexHeaderCount = header->extra_headers;

	// Initialize the speex bit-packing data structure
	speex_bits_init(&mSpeexBits);
	
	// Initialize the stereo mode
	mSpeexStereoState = speex_stereo_state_init();
	
	if(2 == header->nb_channels) {
		SpeexCallback callback;
		callback.callback_id = SPEEX_INBAND_STEREO;
		callback.func = speex_std_stereo_request_handler;
		callback.data = mSpeexStereoState;
		speex_decoder_ctl(mSpeexDecoder, SPEEX_SET_HANDLER, &callback);
	}
	
	// Canonical Core Audio format
	mFormat.mFormatID			= kAudioFormatLinearPCM;
	mFormat.mFormatFlags		= kAudioFormatFlagsNativeFloatPacked | kAudioFormatFlagIsNonInterleaved;
	
	mFormat.mBitsPerChannel		= 8 * sizeof(float);
	mFormat.mSampleRate			= header->rate;
	mFormat.mChannelsPerFrame	= header->nb_channels;
	
	mFormat.mBytesPerPacket		= (mFormat.mBitsPerChannel / 8);
	mFormat.mFramesPerPacket	= 1;
	mFormat.mBytesPerFrame		= mFormat.mBytesPerPacket * mFormat.mFramesPerPacket;
	
	mFormat.mReserved			= 0;
	
	// Set up the source format
	mSourceFormat.mFormatID				= 'SPEE';
	
	mSourceFormat.mSampleRate			= header->rate;
	mSourceFormat.mChannelsPerFrame		= header->nb_channels;
	
	switch(header->nb_channels) {
		case 1:		mChannelLayout = CreateChannelLayoutWithTag(kAudioChannelLayoutTag_Mono);			break;
		case 2:		mChannelLayout = CreateChannelLayoutWithTag(kAudioChannelLayoutTag_Stereo);			break;
	}
	
	speex_header_free(header), header = nullptr;

	// Allocate the buffer list
	spx_int32_t speexFrameSize = 0;
	speex_decoder_ctl(mSpeexDecoder, SPEEX_GET_FRAME_SIZE, &speexFrameSize);
	
	mBufferList = AllocateABL(mFormat, speexFrameSize);
	if(nullptr == mBufferList) {
		if(error)
			*error = CFErrorCreate(kCFAllocatorDefault, kCFErrorDomainPOSIX, ENOMEM, nullptr);

		speex_header_free(header), header = nullptr;
		speex_stereo_state_destroy(mSpeexStereoState), mSpeexStereoState = nullptr;
		speex_decoder_destroy(mSpeexDecoder), mSpeexDecoder = nullptr;
		speex_bits_destroy(&mSpeexBits);

		ogg_sync_destroy(&mOggSyncState);
		return false;
	}
	
	for(UInt32 i = 0; i < mBufferList->mNumberBuffers; ++i)
		mBufferList->mBuffers[i].mDataByteSize = 0;

	mIsOpen = true;
	return true;
}
bool MODMetadata::ReadMetadata(CFErrorRef *error)
{
	// Start from scratch
	CFDictionaryRemoveAllValues(mMetadata);
	CFDictionaryRemoveAllValues(mChangedMetadata);
	
	UInt8 buf [PATH_MAX];
	if(!CFURLGetFileSystemRepresentation(mURL, false, buf, PATH_MAX))
		return false;

	CFStringRef pathExtension = CFURLCopyPathExtension(mURL);
	if(nullptr == pathExtension)
		return false;

	bool fileIsValid = false;
	if(kCFCompareEqualTo == CFStringCompare(pathExtension, CFSTR("it"), kCFCompareCaseInsensitive)) {
		auto stream = new TagLib::FileStream(reinterpret_cast<const char *>(buf), true);
		TagLib::IT::File file(stream);

		if(file.isValid()) {
			fileIsValid = true;
			CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("MOD (Impulse Tracker)"));

			if(file.audioProperties())
				AddAudioPropertiesToDictionary(mMetadata, file.audioProperties());

			if(file.tag())
				AddTagToDictionary(mMetadata, file.tag());
		}
	}
	else if(kCFCompareEqualTo == CFStringCompare(pathExtension, CFSTR("xm"), kCFCompareCaseInsensitive)) {
		auto stream = new TagLib::FileStream(reinterpret_cast<const char *>(buf), true);
		TagLib::XM::File file(stream);

		if(file.isValid()) {
			fileIsValid = true;
			CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("MOD (Extended Module)"));

			if(file.audioProperties())
				AddAudioPropertiesToDictionary(mMetadata, file.audioProperties());

			if(file.tag())
				AddTagToDictionary(mMetadata, file.tag());
		}
	}
	else if(kCFCompareEqualTo == CFStringCompare(pathExtension, CFSTR("s3m"), kCFCompareCaseInsensitive)) {
		auto stream = new TagLib::FileStream(reinterpret_cast<const char *>(buf), true);
		TagLib::S3M::File file(stream);

		if(file.isValid()) {
			fileIsValid = true;
			CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("MOD (ScreamTracker III)"));

			if(file.audioProperties())
				AddAudioPropertiesToDictionary(mMetadata, file.audioProperties());

			if(file.tag())
				AddTagToDictionary(mMetadata, file.tag());
		}
	}
	else if(kCFCompareEqualTo == CFStringCompare(pathExtension, CFSTR("mod"), kCFCompareCaseInsensitive)) {
		auto stream = new TagLib::FileStream(reinterpret_cast<const char *>(buf), true);
		TagLib::Mod::File file(stream);

		if(file.isValid()) {
			fileIsValid = true;
			CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("MOD (Protracker)"));

			if(file.audioProperties())
				AddAudioPropertiesToDictionary(mMetadata, file.audioProperties());

			if(file.tag())
				AddTagToDictionary(mMetadata, file.tag());
		}
	}

	CFRelease(pathExtension), pathExtension = nullptr;

	if(!fileIsValid) {
		if(error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid MOD file."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Not a MOD file"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(AudioMetadataErrorDomain, AudioMetadataInputOutputError, description, mURL, failureReason, recoverySuggestion);
			
			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
		}
		
		return false;
	}

	return true;
}
AudioDecoder * AudioDecoder::CreateDecoderForInputSource(InputSource *inputSource, CFStringRef mimeType, CFErrorRef *error)
{
	if(nullptr == inputSource)
		return nullptr;

	AudioDecoder *decoder = nullptr;

	// Open the input source if it isn't already
	if(AutomaticallyOpenDecoders() && !inputSource->IsOpen() && !inputSource->Open(error))
		return nullptr;

	// As a factory this class has knowledge of its subclasses
	// It would be possible (and perhaps preferable) to switch to a generic
	// plugin interface at a later date

#if 0
	// If the input is an instance of HTTPInputSource, use the MIME type from the server
	// This code is disabled because most HTTP servers don't send the correct MIME types
	HTTPInputSource *httpInputSource = dynamic_cast<HTTPInputSource *>(inputSource);
	bool releaseMIMEType = false;
	if(!mimeType && httpInputSource && httpInputSource->IsOpen()) {
		mimeType = httpInputSource->CopyContentMIMEType();
		if(mimeType)
			releaseMIMEType = true;
	}
#endif

	// The MIME type takes precedence over the file extension
	if(mimeType) {
		if(FLACDecoder::HandlesMIMEType(mimeType)) {
			decoder = new FLACDecoder(inputSource);
			if(AutomaticallyOpenDecoders() && !decoder->Open(error)) {
				decoder->mInputSource = nullptr;
				delete decoder, decoder = nullptr;
			}
		}
		if(nullptr == decoder && WavPackDecoder::HandlesMIMEType(mimeType)) {
			decoder = new WavPackDecoder(inputSource);
			if(AutomaticallyOpenDecoders() && !decoder->Open(error)) {
				decoder->mInputSource = nullptr;
				delete decoder, decoder = nullptr;
			}
		}
		if(nullptr == decoder && MPEGDecoder::HandlesMIMEType(mimeType)) {
			decoder = new MPEGDecoder(inputSource);
			if(AutomaticallyOpenDecoders() && !decoder->Open(error)) {
				decoder->mInputSource = nullptr;
				delete decoder, decoder = nullptr;
			}
		}
		if(nullptr == decoder && OggVorbisDecoder::HandlesMIMEType(mimeType)) {
			decoder = new OggVorbisDecoder(inputSource);
			if(AutomaticallyOpenDecoders() && !decoder->Open(error)) {
				decoder->mInputSource = nullptr;
				delete decoder, decoder = nullptr;
			}
		}
		if(nullptr == decoder && OggSpeexDecoder::HandlesMIMEType(mimeType)) {
			decoder = new OggSpeexDecoder(inputSource);
			if(AutomaticallyOpenDecoders() && !decoder->Open(error)) {
				decoder->mInputSource = nullptr;
				delete decoder, decoder = nullptr;
			}
		}
#if !TARGET_OS_IPHONE
		if(nullptr == decoder && MusepackDecoder::HandlesMIMEType(mimeType)) {
			decoder = new MusepackDecoder(inputSource);
			if(AutomaticallyOpenDecoders() && !decoder->Open(error)) {
				decoder->mInputSource = nullptr;
				delete decoder, decoder = nullptr;
			}
		}
		if(nullptr == decoder && MonkeysAudioDecoder::HandlesMIMEType(mimeType)) {
			decoder = new MonkeysAudioDecoder(inputSource);
			if(AutomaticallyOpenDecoders() && !decoder->Open(error)) {
				decoder->mInputSource = nullptr;
				delete decoder, decoder = nullptr;
			}
		}
		if(nullptr == decoder && MODDecoder::HandlesMIMEType(mimeType)) {
			decoder = new MODDecoder(inputSource);
			if(AutomaticallyOpenDecoders() && !decoder->Open(error)) {
				decoder->mInputSource = nullptr;
				delete decoder, decoder = nullptr;
			}
		}
		if(nullptr == decoder && TrueAudioDecoder::HandlesMIMEType(mimeType)) {
			decoder = new TrueAudioDecoder(inputSource);
			if(AutomaticallyOpenDecoders() && !decoder->Open(error)) {
				decoder->mInputSource = nullptr;
				delete decoder, decoder = nullptr;
			}
		}
		if(nullptr == decoder && LibsndfileDecoder::HandlesMIMEType(mimeType)) {
			decoder = new LibsndfileDecoder(inputSource);
			if(AutomaticallyOpenDecoders() && !decoder->Open(error)) {
				decoder->mInputSource = nullptr;
				delete decoder, decoder = nullptr;
			}
		}
#endif
		if(nullptr == decoder && CoreAudioDecoder::HandlesMIMEType(mimeType)) {
			decoder = new CoreAudioDecoder(inputSource);
			if(AutomaticallyOpenDecoders() && !decoder->Open(error)) {
				decoder->mInputSource = nullptr;
				delete decoder, decoder = nullptr;
			}
		}

#if 0
		if(releaseMIMEType)
			CFRelease(mimeType), mimeType = nullptr;
#endif

		if(decoder)
			return decoder;
	}

	// If no MIME type was specified, use the extension-based resolvers

	CFURLRef inputURL = inputSource->GetURL();
	if(!inputURL)
		return nullptr;

	// Determining the extension isn't as simple as using CFURLCopyPathExtension (wouldn't that be nice?),
	// because although the behavior on Lion works like one would expect, on Snow Leopard it returns
	// a number that I believe is part of the inode number, but is definitely NOT the extension
	CFStringRef pathExtension = nullptr;
#if !TARGET_OS_IPHONE
	CFURLRef filePathURL = CFURLCreateFilePathURL(kCFAllocatorDefault, inputURL, nullptr);
	if(filePathURL) {
		pathExtension = CFURLCopyPathExtension(filePathURL);
		CFRelease(filePathURL), filePathURL = nullptr;
	}
	else
#endif
		pathExtension = CFURLCopyPathExtension(inputURL);

	if(!pathExtension) {
		if(error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The type of the file “%@” could not be determined."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Unknown file type"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may be missing or may not match the file's type."), "");
			
			*error = CreateErrorForURL(InputSourceErrorDomain, InputSourceFileNotFoundError, description, inputURL, failureReason, recoverySuggestion);
			
			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
		}

		return nullptr;
	}

	// TODO: Some extensions (.oga for example) support multiple audio codecs (Vorbis, FLAC, Speex)
	// and if openDecoder is false the wrong decoder type may be returned, since the file isn't analyzed
	// until Open() is called
	
	if(FLACDecoder::HandlesFilesWithExtension(pathExtension)) {
		decoder = new FLACDecoder(inputSource);
		if(AutomaticallyOpenDecoders() && !decoder->Open(error)) {
			decoder->mInputSource = nullptr;
			delete decoder, decoder = nullptr;
		}
	}
	if(nullptr == decoder && WavPackDecoder::HandlesFilesWithExtension(pathExtension)) {
		decoder = new WavPackDecoder(inputSource);
		if(AutomaticallyOpenDecoders() && !decoder->Open(error)) {
			decoder->mInputSource = nullptr;
			delete decoder, decoder = nullptr;
		}
	}
	if(nullptr == decoder && MPEGDecoder::HandlesFilesWithExtension(pathExtension)) {
		decoder = new MPEGDecoder(inputSource);
		if(AutomaticallyOpenDecoders() && !decoder->Open(error)) {
			decoder->mInputSource = nullptr;
			delete decoder, decoder = nullptr;
		}
	}
	if(nullptr == decoder && OggVorbisDecoder::HandlesFilesWithExtension(pathExtension)) {
		decoder = new OggVorbisDecoder(inputSource);
		if(AutomaticallyOpenDecoders() && !decoder->Open(error)) {
			decoder->mInputSource = nullptr;
			delete decoder, decoder = nullptr;
		}
	}
	if(nullptr == decoder && OggSpeexDecoder::HandlesFilesWithExtension(pathExtension)) {
		decoder = new OggSpeexDecoder(inputSource);
		if(AutomaticallyOpenDecoders() && !decoder->Open(error)) {
			decoder->mInputSource = nullptr;
			delete decoder, decoder = nullptr;
		}
	}
#if !TARGET_OS_IPHONE
	if(nullptr == decoder && MusepackDecoder::HandlesFilesWithExtension(pathExtension)) {
		decoder = new MusepackDecoder(inputSource);
		if(AutomaticallyOpenDecoders() && !decoder->Open(error)) {
			decoder->mInputSource = nullptr;
			delete decoder, decoder = nullptr;
		}
	}
	if(nullptr == decoder && MonkeysAudioDecoder::HandlesFilesWithExtension(pathExtension)) {
		decoder = new MonkeysAudioDecoder(inputSource);
		if(AutomaticallyOpenDecoders() && !decoder->Open(error)) {
			decoder->mInputSource = nullptr;
			delete decoder, decoder = nullptr;
		}
	}
	if(nullptr == decoder && MODDecoder::HandlesFilesWithExtension(pathExtension)) {
		decoder = new MODDecoder(inputSource);
		if(AutomaticallyOpenDecoders() && !decoder->Open(error)) {
			decoder->mInputSource = nullptr;
			delete decoder, decoder = nullptr;
		}
	}
	if(nullptr == decoder && TrueAudioDecoder::HandlesFilesWithExtension(pathExtension)) {
		decoder = new TrueAudioDecoder(inputSource);
		if(AutomaticallyOpenDecoders() && !decoder->Open(error)) {
			decoder->mInputSource = nullptr;
			delete decoder, decoder = nullptr;
		}
	}
	if(nullptr == decoder && LibsndfileDecoder::HandlesFilesWithExtension(pathExtension)) {
		decoder = new LibsndfileDecoder(inputSource);
		if(AutomaticallyOpenDecoders() && !decoder->Open(error)) {
			decoder->mInputSource = nullptr;
			delete decoder, decoder = nullptr;
		}
	}
#endif
	if(nullptr == decoder && CoreAudioDecoder::HandlesFilesWithExtension(pathExtension)) {
		decoder = new CoreAudioDecoder(inputSource);
		if(AutomaticallyOpenDecoders() && !decoder->Open(error)) {
			decoder->mInputSource = nullptr;
			delete decoder, decoder = nullptr;
		}
	}

	CFRelease(pathExtension), pathExtension = nullptr;

	return decoder;
}
Exemple #25
0
bool SFB::Audio::MP3Metadata::_ReadMetadata(CFErrorRef *error)
{
	UInt8 buf [PATH_MAX];
	if(!CFURLGetFileSystemRepresentation(mURL, false, buf, PATH_MAX))
		return false;
	
	std::unique_ptr<TagLib::FileStream> stream(new TagLib::FileStream((const char *)buf, true));
	if(!stream->isOpen()) {
		if(error) {
			SFB::CFString description(CFCopyLocalizedString(CFSTR("The file “%@” could not be opened for reading."), ""));
			SFB::CFString failureReason(CFCopyLocalizedString(CFSTR("Input/output error"), ""));
			SFB::CFString recoverySuggestion(CFCopyLocalizedString(CFSTR("The file may have been renamed, moved, deleted, or you may not have appropriate permissions."), ""));

			*error = CreateErrorForURL(Metadata::ErrorDomain, Metadata::InputOutputError, description, mURL, failureReason, recoverySuggestion);
		}

		return false;
	}

	TagLib::MPEG::File file(stream.get(), TagLib::ID3v2::FrameFactory::instance());
	if(!file.isValid()) {
		if(nullptr != error) {
			SFB::CFString description(CFCopyLocalizedString(CFSTR("The file “%@” is not a valid MPEG file."), ""));
			SFB::CFString failureReason(CFCopyLocalizedString(CFSTR("Not an MPEG file"), ""));
			SFB::CFString recoverySuggestion(CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), ""));
			
			*error = CreateErrorForURL(Metadata::ErrorDomain, Metadata::InputOutputError, description, mURL, failureReason, recoverySuggestion);
		}
		
		return false;
	}
	
	CFDictionarySetValue(mMetadata, kFormatNameKey, CFSTR("MP3"));

	if(file.audioProperties()) {
		auto properties = file.audioProperties();
		AddAudioPropertiesToDictionary(mMetadata, properties);

		// TODO: Is this too much information?
#if 0
		switch(properties->version()) {
			case TagLib::MPEG::Header::Version1:
				switch(properties->layer()) {
					case 1:		CFDictionarySetValue(mMetadata, kFormatNameKey, CFSTR("MPEG-1 Layer I"));		break;
					case 2:		CFDictionarySetValue(mMetadata, kFormatNameKey, CFSTR("MPEG-1 Layer II"));	break;
					case 3:		CFDictionarySetValue(mMetadata, kFormatNameKey, CFSTR("MPEG-1 Layer III"));	break;
				}
				break;
			case TagLib::MPEG::Header::Version2:
				switch(properties->layer()) {
					case 1:		CFDictionarySetValue(mMetadata, kFormatNameKey, CFSTR("MPEG-2 Layer I"));		break;
					case 2:		CFDictionarySetValue(mMetadata, kFormatNameKey, CFSTR("MPEG-2 Layer II"));	break;
					case 3:		CFDictionarySetValue(mMetadata, kFormatNameKey, CFSTR("MPEG-2 Layer III"));	break;
				}
				break;
			case TagLib::MPEG::Header::Version2_5:
				switch(properties->layer()) {
					case 1:		CFDictionarySetValue(mMetadata, kFormatNameKey, CFSTR("MPEG-2.5 Layer I"));	break;
					case 2:		CFDictionarySetValue(mMetadata, kFormatNameKey, CFSTR("MPEG-2.5 Layer II"));	break;
					case 3:		CFDictionarySetValue(mMetadata, kFormatNameKey, CFSTR("MPEG-2.5 Layer III"));	break;
				}
				break;
		}
#endif

		if(properties->xingHeader() && properties->xingHeader()->totalFrames())
			AddIntToDictionary(mMetadata, kTotalFramesKey, (int)properties->xingHeader()->totalFrames());
	}

	if(file.APETag())
		AddAPETagToDictionary(mMetadata, mPictures, file.APETag());

	if(file.ID3v1Tag())
		AddID3v1TagToDictionary(mMetadata, file.ID3v1Tag());

	if(file.ID3v2Tag())
		AddID3v2TagToDictionary(mMetadata, mPictures, file.ID3v2Tag());

	return true;
}
bool SFB::Audio::ReplayGainAnalyzer::AnalyzeURL(CFURLRef url, CFErrorRef *error)
{
	if(nullptr == url)
		return false;

	auto decoder = Decoder::CreateDecoderForURL(url, error);
	if(!decoder || !decoder->Open(error))
		return false;
	
	AudioStreamBasicDescription inputFormat = decoder->GetFormat();

	// Higher sampling rates aren't natively supported but are handled via resampling
	int32_t decoderSampleRate = (int32_t)inputFormat.mSampleRate;

	bool validSampleRate = EvenMultipleSampleRateIsSupported(decoderSampleRate);
	if(!validSampleRate) {
		if(error) {
			SFB::CFString description = CFCopyLocalizedString(CFSTR("The file “%@” does not contain audio at a supported sample rate."), "");
			SFB::CFString failureReason = CFCopyLocalizedString(CFSTR("Only sample rates of 8.0 KHz, 11.025 KHz, 12.0 KHz, 16.0 KHz, 22.05 KHz, 24.0 KHz, 32.0 KHz, 44.1 KHz, 48 KHz and multiples are supported."), "");
			SFB::CFString recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");

			*error = CreateErrorForURL(ReplayGainAnalyzer::ErrorDomain, ReplayGainAnalyzer::FileFormatNotSupportedError, description, url, failureReason, recoverySuggestion);
		}

		return false;
	}

	Float64 replayGainSampleRate = GetBestReplayGainSampleRateForSampleRate(decoderSampleRate);

	if(!(1 == inputFormat.mChannelsPerFrame || 2 == inputFormat.mChannelsPerFrame)) {
		if(error) {
			SFB::CFString description = CFCopyLocalizedString(CFSTR("The file “%@” does not contain mono or stereo audio."), "");
			SFB::CFString failureReason = CFCopyLocalizedString(CFSTR("Only mono or stereo files supported"), "");
			SFB::CFString recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");

			*error = CreateErrorForURL(ReplayGainAnalyzer::ErrorDomain, ReplayGainAnalyzer::FileFormatNotSupportedError, description, url, failureReason, recoverySuggestion);
		}

		return false;
	}

	AudioStreamBasicDescription outputFormat = {
		.mFormatID				= kAudioFormatLinearPCM,
		.mFormatFlags			= kAudioFormatFlagsNativeFloatPacked | kAudioFormatFlagIsNonInterleaved,
		.mReserved				= 0,
		.mSampleRate			= replayGainSampleRate,
		.mChannelsPerFrame		= inputFormat.mChannelsPerFrame,
		.mBitsPerChannel		= 32,
		.mBytesPerPacket		= 4,
		.mBytesPerFrame			= 4,
		.mFramesPerPacket		= 1
	};
	
	if(!SetSampleRate((int32_t)outputFormat.mSampleRate)) {
		if(error) {
			SFB::CFString description = CFCopyLocalizedString(CFSTR("The file “%@” does not contain audio at a supported sample rate."), "");
			SFB::CFString failureReason = CFCopyLocalizedString(CFSTR("Only sample rates of 8.0 KHz, 11.025 KHz, 12.0 KHz, 16.0 KHz, 22.05 KHz, 24.0 KHz, 32.0 KHz, 44.1 KHz, 48 KHz and multiples are supported."), "");
			SFB::CFString recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");

			*error = CreateErrorForURL(ReplayGainAnalyzer::ErrorDomain, ReplayGainAnalyzer::FileFormatNotSupportedError, description, url, failureReason, recoverySuggestion);
		}

		return false;
	}

	// Converter takes ownership of decoder
	Converter converter(std::move(decoder), outputFormat);
	if(!converter.Open(error))
		return false;
	
	const UInt32 bufferSizeFrames = 512;
	BufferList outputBuffer(outputFormat, bufferSizeFrames);

	bool isStereo = (2 == outputFormat.mChannelsPerFrame);

	for(;;) {
		UInt32 frameCount = converter.ConvertAudio(outputBuffer, bufferSizeFrames);
		if(0 == frameCount)
			break;

		// Find the peak sample magnitude
		float lpeak, rpeak;
		vDSP_maxmgv((const float *)outputBuffer->mBuffers[0].mData, 1, &lpeak, frameCount);
		if(isStereo) {
			vDSP_maxmgv((const float *)outputBuffer->mBuffers[1].mData, 1, &rpeak, frameCount);
			priv->trackPeak = std::max(priv->trackPeak, std::max(lpeak, rpeak));
		}
		else
			priv->trackPeak = std::max(priv->trackPeak, lpeak);

		// The replay gain analyzer expects 16-bit sample size passed as floats
		const float scale = 1u << 15;
		vDSP_vsmul((const float *)outputBuffer->mBuffers[0].mData, 1, &scale, (float *)outputBuffer->mBuffers[0].mData, 1, frameCount);
		if(isStereo) {
			vDSP_vsmul((const float *)outputBuffer->mBuffers[1].mData, 1, &scale, (float *)outputBuffer->mBuffers[1].mData, 1, frameCount);
			AnalyzeSamples((const float *)outputBuffer->mBuffers[0].mData, (const float *)outputBuffer->mBuffers[1].mData, frameCount, true);
		}
		else
			AnalyzeSamples((const float *)outputBuffer->mBuffers[0].mData, nullptr, frameCount, false);
	}

	priv->albumPeak = std::max(priv->albumPeak, priv->trackPeak);

	return true;
}

bool SFB::Audio::ReplayGainAnalyzer::GetTrackGain(float& trackGain)
{
	if(!analyzeResult(priv->A, sizeof(priv->A) / sizeof(*(priv->A)), trackGain))
		return false;

	for(uint32_t i = 0; i < sizeof(priv->A) / sizeof(*(priv->A)); ++i) {
		priv->B[i] += priv->A[i];
		priv->A[i]  = 0;
	}

	priv->Zero();

	priv->totsamp	= 0;
	priv->lsum		= priv->rsum = 0.;

	return true;
}

bool SFB::Audio::ReplayGainAnalyzer::GetTrackPeak(float& trackPeak)
{
	trackPeak = priv->trackPeak;
	priv->trackPeak = 0.;
	return true;
}
bool SFB::Audio::MusepackDecoder::_Open(CFErrorRef *error)
{
	UInt8 buf [PATH_MAX];
	if(!CFURLGetFileSystemRepresentation(mInputSource->GetURL(), FALSE, buf, PATH_MAX))
		return false;

	mReader.read = read_callback;
	mReader.seek = seek_callback;
	mReader.tell = tell_callback;
	mReader.get_size = get_size_callback;
	mReader.canseek = canseek_callback;
	mReader.data = this;
	
	mDemux = mpc_demux_init(&mReader);
	if(nullptr == mDemux) {
		if(error) {
			SFB::CFString description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid Musepack file."), "");
			SFB::CFString failureReason = CFCopyLocalizedString(CFSTR("Not a Musepack file"), "");
			SFB::CFString recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(Decoder::ErrorDomain, Decoder::InputOutputError, description, mInputSource->GetURL(), failureReason, recoverySuggestion);
		}

		mpc_reader_exit_stdio(&mReader);
		
		return false;
	}
	
	// Get input file information
	mpc_streaminfo streaminfo;
	mpc_demux_get_info(mDemux, &streaminfo);
	
	mTotalFrames				= mpc_streaminfo_get_length_samples(&streaminfo);
	
	// Canonical Core Audio format
	mFormat.mFormatID			= kAudioFormatLinearPCM;
	mFormat.mFormatFlags		= kAudioFormatFlagsNativeFloatPacked | kAudioFormatFlagIsNonInterleaved;
	
	mFormat.mSampleRate			= streaminfo.sample_freq;
	mFormat.mChannelsPerFrame	= streaminfo.channels;
	mFormat.mBitsPerChannel		= 8 * sizeof(float);
	
	mFormat.mBytesPerPacket		= (mFormat.mBitsPerChannel / 8);
	mFormat.mFramesPerPacket	= 1;
	mFormat.mBytesPerFrame		= mFormat.mBytesPerPacket * mFormat.mFramesPerPacket;
	
	mFormat.mReserved			= 0;
	
	// Set up the source format
	mSourceFormat.mFormatID				= 'MUSE';
	
	mSourceFormat.mSampleRate			= streaminfo.sample_freq;
	mSourceFormat.mChannelsPerFrame		= streaminfo.channels;
	
	mSourceFormat.mFramesPerPacket		= (1 << streaminfo.block_pwr);
	
	// Setup the channel layout
	switch(streaminfo.channels) {
		case 1:		mChannelLayout = ChannelLayout::ChannelLayoutWithTag(kAudioChannelLayoutTag_Mono);			break;
		case 2:		mChannelLayout = ChannelLayout::ChannelLayoutWithTag(kAudioChannelLayoutTag_Stereo);		break;
		case 4:		mChannelLayout = ChannelLayout::ChannelLayoutWithTag(kAudioChannelLayoutTag_Quadraphonic);	break;
	}
	
	// Allocate the buffer list
	if(!mBufferList.Allocate(mFormat, MPC_FRAME_LENGTH)) {
		if(error)
			*error = CFErrorCreate(kCFAllocatorDefault, kCFErrorDomainPOSIX, ENOMEM, nullptr);

		mpc_demux_exit(mDemux), mDemux = nullptr;
		mpc_reader_exit_stdio(&mReader);
		
		return false;
	}

	for(UInt32 i = 0; i < mBufferList->mNumberBuffers; ++i)
		mBufferList->mBuffers[i].mDataByteSize = 0;

	return true;
}
bool MonkeysAudioDecoder::Open(CFErrorRef *error)
{
	if(IsOpen()) {
		LOGGER_WARNING("org.sbooth.AudioEngine.AudioDecoder.MonkeysAudio", "Open() called on an AudioDecoder that is already open");		
		return true;
	}

	// Ensure the input source is open
	if(!mInputSource->IsOpen() && !mInputSource->Open(error))
		return false;

	mIOInterface = new APEIOInterface(GetInputSource());

	int errorCode;
	mDecompressor = CreateIAPEDecompressEx(mIOInterface, &errorCode);
	
	if(nullptr == mDecompressor) {
		if(error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid Monkey's Audio file."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Not a Monkey's Audio file"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(AudioDecoderErrorDomain, AudioDecoderInputOutputError, description, mInputSource->GetURL(), failureReason, recoverySuggestion);
			
			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
		}
		
		return false;
	}

	// The file format
	mFormat.mFormatID			= kAudioFormatLinearPCM;
	mFormat.mFormatFlags		= kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
	
	mFormat.mBitsPerChannel		= static_cast<UInt32>(mDecompressor->GetInfo(APE_INFO_BITS_PER_SAMPLE));
	mFormat.mSampleRate			= mDecompressor->GetInfo(APE_INFO_SAMPLE_RATE);
	mFormat.mChannelsPerFrame	= static_cast<UInt32>(mDecompressor->GetInfo(APE_INFO_CHANNELS));
	
	mFormat.mBytesPerPacket		= (mFormat.mBitsPerChannel / 8) * mFormat.mChannelsPerFrame;
	mFormat.mFramesPerPacket	= 1;
	mFormat.mBytesPerFrame		= mFormat.mBytesPerPacket * mFormat.mFramesPerPacket;
	
	mFormat.mReserved			= 0;
	
	// Set up the source format
	mSourceFormat.mFormatID				= 'APE ';
	
	mSourceFormat.mSampleRate			= mFormat.mSampleRate;
	mSourceFormat.mChannelsPerFrame		= mFormat.mChannelsPerFrame;
	
	switch(mFormat.mChannelsPerFrame) {
		case 1:		mChannelLayout = CreateChannelLayoutWithTag(kAudioChannelLayoutTag_Mono);			break;
		case 2:		mChannelLayout = CreateChannelLayoutWithTag(kAudioChannelLayoutTag_Stereo);			break;
		case 4:		mChannelLayout = CreateChannelLayoutWithTag(kAudioChannelLayoutTag_Quadraphonic);	break;
	}

	mIsOpen = true;
	return true;
}
bool TrueAudioMetadata::WriteMetadata(CFErrorRef *error)
{
	UInt8 buf [PATH_MAX];
	if(!CFURLGetFileSystemRepresentation(mURL, false, buf, PATH_MAX))
		return false;

	// TODO: Use unique_ptr once the switch to C++11 STL is made
	std::auto_ptr<TagLib::FileStream> stream(new TagLib::FileStream(reinterpret_cast<const char *>(buf)));
	if(!stream->isOpen()) {
		if(error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” could not be opened for writing."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Input/output error"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file may have been renamed, moved, deleted, or you may not have appropriate permissions."), "");

			*error = CreateErrorForURL(AudioMetadataErrorDomain, AudioMetadataInputOutputError, description, mURL, failureReason, recoverySuggestion);

			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
		}

		return false;
	}

	TagLib::TrueAudio::File file(stream.get(), false);
	if(!file.isValid()) {
		if(error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid True Audio file."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Not a True Audio file"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(AudioMetadataErrorDomain, AudioMetadataInputOutputError, description, mURL, failureReason, recoverySuggestion);
			
			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
		}

		return false;
	}

	// ID3v1 tags are only written if present, but ID3v2 tags are always written

	if(file.ID3v1Tag())
		SetID3v1TagFromMetadata(*this, file.ID3v1Tag());

	SetID3v2TagFromMetadata(*this, file.ID3v2Tag(true));

	if(!file.save()) {
		if(error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid True Audio file."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Unable to write metadata"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(AudioMetadataErrorDomain, AudioMetadataInputOutputError, description, mURL, failureReason, recoverySuggestion);
			
			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
		}

		return false;
	}

	MergeChangedMetadataIntoMetadata();

	return true;
}
bool WavPackDecoder::Open(CFErrorRef *error)
{
	if(IsOpen()) {
		LOGGER_WARNING("org.sbooth.AudioEngine.AudioDecoder.WavPack", "Open() called on an AudioDecoder that is already open");		
		return true;
	}

	// Ensure the input source is open
	if(!mInputSource->IsOpen() && !mInputSource->Open(error))
		return false;

	mStreamReader.read_bytes = read_bytes_callback;
	mStreamReader.get_pos = get_pos_callback;
	mStreamReader.set_pos_abs = set_pos_abs_callback;
	mStreamReader.set_pos_rel = set_pos_rel_callback;
	mStreamReader.push_back_byte = push_back_byte_callback;
	mStreamReader.get_length = get_length_callback;
	mStreamReader.can_seek = can_seek_callback;
	
	char errorBuf [80];
	
	// Setup converter
	mWPC = WavpackOpenFileInputEx(&mStreamReader, this, nullptr, errorBuf, OPEN_WVC | OPEN_NORMALIZE, 0);
	if(nullptr == mWPC) {
		if(error) {
			CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid WavPack file."), "");
			CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Not a WavPack file"), "");
			CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "");
			
			*error = CreateErrorForURL(AudioDecoderErrorDomain, AudioDecoderInputOutputError, description, mInputSource->GetURL(), failureReason, recoverySuggestion);
			
			CFRelease(description), description = nullptr;
			CFRelease(failureReason), failureReason = nullptr;
			CFRelease(recoverySuggestion), recoverySuggestion = nullptr;
		}
		
		return false;
	}
	
	// Floating-point and lossy files will be handed off in the canonical Core Audio format
	int mode = WavpackGetMode(mWPC);
	if(MODE_FLOAT & mode || !(MODE_LOSSLESS & mode)) {
		// Canonical Core Audio format
		mFormat.mFormatID			= kAudioFormatLinearPCM;
		mFormat.mFormatFlags		= kAudioFormatFlagsNativeFloatPacked | kAudioFormatFlagIsNonInterleaved;
		
		mFormat.mSampleRate			= WavpackGetSampleRate(mWPC);
		mFormat.mChannelsPerFrame	= WavpackGetNumChannels(mWPC);		
		mFormat.mBitsPerChannel		= 8 * sizeof(float);
		
		mFormat.mBytesPerPacket		= (mFormat.mBitsPerChannel / 8);
		mFormat.mFramesPerPacket	= 1;
		mFormat.mBytesPerFrame		= mFormat.mBytesPerPacket * mFormat.mFramesPerPacket;
		
		mFormat.mReserved			= 0;
	}
	else {
		mFormat.mFormatID			= kAudioFormatLinearPCM;
		mFormat.mFormatFlags		= kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsNonInterleaved;

		// Don't set kAudioFormatFlagIsAlignedHigh for 32-bit integer files
		mFormat.mFormatFlags		|= (32 == WavpackGetBitsPerSample(mWPC) ? kAudioFormatFlagIsPacked : kAudioFormatFlagIsAlignedHigh);

		mFormat.mSampleRate			= WavpackGetSampleRate(mWPC);
		mFormat.mChannelsPerFrame	= WavpackGetNumChannels(mWPC);
		mFormat.mBitsPerChannel		= WavpackGetBitsPerSample(mWPC);
		
		mFormat.mBytesPerPacket		= sizeof(int32_t);
		mFormat.mFramesPerPacket	= 1;
		mFormat.mBytesPerFrame		= mFormat.mBytesPerPacket * mFormat.mFramesPerPacket;
		
		mFormat.mReserved			= 0;
	}
	
	mTotalFrames						= WavpackGetNumSamples(mWPC);
	
	// Set up the source format
	mSourceFormat.mFormatID				= 'WVPK';
	
	mSourceFormat.mSampleRate			= WavpackGetSampleRate(mWPC);
	mSourceFormat.mChannelsPerFrame		= WavpackGetNumChannels(mWPC);
	mSourceFormat.mBitsPerChannel		= WavpackGetBitsPerSample(mWPC);
	
	// Setup the channel layout
	switch(mFormat.mChannelsPerFrame) {
		case 1:		mChannelLayout = CreateChannelLayoutWithTag(kAudioChannelLayoutTag_Mono);			break;
		case 2:		mChannelLayout = CreateChannelLayoutWithTag(kAudioChannelLayoutTag_Stereo);			break;
		case 4:		mChannelLayout = CreateChannelLayoutWithTag(kAudioChannelLayoutTag_Quadraphonic);	break;
	}
	
	mBuffer = static_cast<int32_t *>(calloc(BUFFER_SIZE_FRAMES * mFormat.mChannelsPerFrame, sizeof(int32_t)));

	if(nullptr == mBuffer) {
		if(error)
			*error = CFErrorCreate(kCFAllocatorDefault, kCFErrorDomainPOSIX, ENOMEM, nullptr);
		
		return false;		
	}

	mIsOpen = true;
	return true;
}