Load AIFF-C files as sound resources

This commit is contained in:
Iliyas Jorio 2021-07-25 17:22:10 +02:00
parent 9c223defc2
commit 77fda7913d
7 changed files with 264 additions and 122 deletions

View File

@ -481,6 +481,9 @@ Boolean Pomme_DecompressSoundResource(SndListHandle* sndHandlePtr, long* offsetT
// Pomme extension
void Pomme_PauseAllChannels(Boolean pause);
// Pomme extension
SndListHandle Pomme_SndLoadFileAsResource(short fRefNum);
#ifdef __cplusplus
}
#endif

View File

@ -14,8 +14,6 @@ namespace Pomme::Sound
void Shutdown();
void ReadAIFF(std::istream& input, cmixer::WavStream& output);
struct SampledSoundInfo
{
int16_t nChannels;
@ -100,5 +98,7 @@ namespace Pomme::Sound
void GetSoundInfoFromSndResource(Handle sndHandle, SampledSoundInfo& info);
std::streampos GetSoundInfoFromAIFF(std::istream& input, SampledSoundInfo& info);
std::unique_ptr<Pomme::Sound::Codec> GetCodec(uint32_t fourCC);
}

View File

@ -1,6 +1,7 @@
#include "PommeSound.h"
#include "Utilities/bigendianstreams.h"
#include <cstdint>
#include <map>
static void AIFFAssert(bool condition, const char* message)
{
@ -10,7 +11,84 @@ static void AIFFAssert(bool condition, const char* message)
}
}
void Pomme::Sound::ReadAIFF(std::istream& input, cmixer::WavStream& output)
static void ParseCOMM(Pomme::BigEndianIStream& f, Pomme::Sound::SampledSoundInfo& info, bool isAIFC)
{
info.nChannels = f.Read<uint16_t>();
info.nPackets = f.Read<uint32_t>();
info.codecBitDepth = f.Read<uint16_t>();
info.sampleRate = f.Read80BitFloat();
if (isAIFC)
{
info.compressionType = f.Read<uint32_t>();
f.ReadPascalString(2); // This is a human-friendly compression name. Skip it.
}
else
{
info.compressionType = 'NONE';
}
switch (info.compressionType)
{
case 'NONE': info.bigEndian = true; info.isCompressed = false; break;
case 'twos': info.bigEndian = true; info.isCompressed = false; break;
case 'sowt': info.bigEndian = false; info.isCompressed = false; break;
case 'raw ': info.bigEndian = true; info.isCompressed = false; break;
case 'MAC3': info.bigEndian = true; info.isCompressed = true; break;
case 'ima4': info.bigEndian = true; info.isCompressed = true; break;
case 'ulaw': info.bigEndian = true; info.isCompressed = true; break;
case 'alaw': info.bigEndian = true; info.isCompressed = true; break;
default:
throw std::runtime_error("unknown AIFF-C compression type");
}
}
static void ParseMARK(Pomme::BigEndianIStream& f, std::map<uint16_t, uint32_t>& markers)
{
int16_t nMarkers = f.Read<int16_t>();
for (int16_t i = 0; i < nMarkers; i++)
{
uint16_t markerID = f.Read<uint16_t>();
uint32_t markerPosition = f.Read<uint32_t>();
f.ReadPascalString(2); // skip name
markers[markerID] = markerPosition;
}
}
static void ParseINST(Pomme::BigEndianIStream& f, Pomme::Sound::SampledSoundInfo& info, std::map<uint16_t, uint32_t>& markers)
{
info.baseNote = f.Read<int8_t>();
f.Skip(1); // detune
f.Skip(1); // lowNote
f.Skip(1); // highNote
f.Skip(1); // lowVelocity
f.Skip(1); // highVelocity
f.Skip(2); // gain
uint16_t playMode = f.Read<uint16_t>();
uint16_t beginLoopMarkerID = f.Read<uint16_t>();
uint16_t endLoopMarkerID = f.Read<uint16_t>();
f.Skip(2);
f.Skip(2);
f.Skip(2);
switch (playMode)
{
case 0:
break;
case 1:
info.loopStart = markers.at(beginLoopMarkerID);
info.loopEnd = markers.at(endLoopMarkerID);
break;
default:
throw std::runtime_error("unsupported AIFF INST playMode");
}
}
std::streampos Pomme::Sound::GetSoundInfoFromAIFF(std::istream& input, SampledSoundInfo& info)
{
BigEndianIStream f(input);
@ -20,14 +98,15 @@ void Pomme::Sound::ReadAIFF(std::istream& input, cmixer::WavStream& output)
auto formType = f.Read<uint32_t>();
AIFFAssert(formType == 'AIFF' || formType == 'AIFC', "AIFF: not an AIFF or AIFC file");
// COMM chunk contents
int nChannels = 0;
int nPackets = 0;
int bitDepth = 0;
int sampleRate = 0;
uint32_t compressionType = 'NONE';
bool gotCOMM = false;
std::streampos sampledSoundDataOffset = 0;
info = {};
info.compressionType = 'NONE';
info.isCompressed = false;
info.baseNote = 60; // Middle C
std::map<uint16_t, uint32_t> markers;
while (f.Tell() != endOfForm)
{
@ -39,52 +118,48 @@ void Pomme::Sound::ReadAIFF(std::istream& input, cmixer::WavStream& output)
{
case 'FVER':
{
auto timestamp = f.Read<uint32_t>();
uint32_t timestamp = f.Read<uint32_t>();
AIFFAssert(timestamp == 0xA2805140u, "AIFF: unrecognized FVER");
break;
}
case 'COMM': // common chunk, 2-85
{
nChannels = f.Read<uint16_t>();
nPackets = f.Read<uint32_t>();
bitDepth = f.Read<uint16_t>();
sampleRate = (int)f.Read80BitFloat();
if (formType == 'AIFC')
{
compressionType = f.Read<uint32_t>();
f.ReadPascalString(); // This is a human-friendly compression name. Skip it.
}
ParseCOMM(f, info, formType=='AIFC');
gotCOMM = true;
break;
}
case 'MARK':
ParseMARK(f, markers);
break;
case 'INST':
ParseINST(f, info, markers);
break;
case 'SSND':
{
AIFFAssert(gotCOMM, "AIFF: reached SSND before COMM");
AIFFAssert(0 == f.Read<uint64_t>(), "AIFF: unexpected offset/blockSize in SSND");
// sampled sound data is here
sampledSoundDataOffset = f.Tell();
const int ssndSize = ckSize - 8;
if (compressionType == 'NONE')
info.compressedLength = ssndSize;
if (!info.isCompressed)
{
// Raw big-endian PCM -- just init the WavStream without decoding
auto spanOut = output.GetBuffer(ssndSize);
f.Read(spanOut.data(), ssndSize);
output.Init(sampleRate, bitDepth, nChannels, true, spanOut);
info.decompressedLength = info.compressedLength;
}
else
{
auto ssnd = std::vector<char>(ssndSize);
f.Read(ssnd.data(), ssndSize);
auto codec = Pomme::Sound::GetCodec(compressionType);
auto spanIn = std::span(ssnd);
auto spanOut = output.GetBuffer(nChannels * nPackets * codec->SamplesPerPacket() * 2);
codec->Decode(nChannels, spanIn, spanOut);
output.Init(sampleRate, 16, nChannels, false, spanOut);
auto codec = Pomme::Sound::GetCodec(info.compressionType);
info.decompressedLength = info.nChannels * info.nPackets * codec->SamplesPerPacket() * 2;
}
f.Skip(ssndSize);
break;
}
@ -94,6 +169,15 @@ void Pomme::Sound::ReadAIFF(std::istream& input, cmixer::WavStream& output)
}
AIFFAssert(f.Tell() == endOfChunk, "AIFF: incorrect end-of-chunk position");
// skip zero pad byte if odd position
if ((f.Tell() & 1) == 1)
{
f.Skip(1);
}
}
f.Goto(sampledSoundDataOffset);
return sampledSoundDataOffset;
}

View File

@ -197,7 +197,7 @@ public:
//-----------------------------------------------------------------------------
// Internal utilities
static inline ChannelImpl& GetImpl(SndChannelPtr chan)
static inline ChannelImpl& GetChannelImpl(SndChannelPtr chan)
{
return *(ChannelImpl*) chan->channelImpl;
}
@ -309,7 +309,7 @@ OSErr SndDisposeChannel(SndChannelPtr macChanPtr, Boolean quietNow)
{
TODO2("SndDisposeChannel: quietNow == false is not implemented");
}
delete &GetImpl(macChanPtr);
delete &GetChannelImpl(macChanPtr);
return noErr;
}
@ -317,7 +317,7 @@ OSErr SndChannelStatus(SndChannelPtr chan, short theLength, SCStatusPtr theStatu
{
*theStatus = {};
auto& source = GetImpl(chan).source;
auto& source = GetChannelImpl(chan).source;
theStatus->scChannelPaused = source.GetState() == cmixer::CM_STATE_PAUSED;
theStatus->scChannelBusy = source.GetState() == cmixer::CM_STATE_PLAYING;
@ -331,7 +331,8 @@ static void InstallSoundInChannel(SndChannelPtr chan, const Ptr sampledSoundHead
//---------------------------------
// Get internal channel
auto& impl = GetImpl(chan);
auto& impl = GetChannelImpl(chan);
impl.Recycle();
//---------------------------------
@ -391,7 +392,7 @@ static void InstallSoundInChannel(SndChannelPtr chan, const Ptr sampledSoundHead
OSErr SndDoImmediate(SndChannelPtr chan, const SndCommand* cmd)
{
auto& impl = GetImpl(chan);
auto& impl = GetChannelImpl(chan);
// Discard the high bit of the command (it indicates whether an 'snd ' resource has associated data).
switch (cmd->cmd & 0x7FFF)
@ -509,13 +510,41 @@ OSErr SndStartFilePlay(
return unimpErr;
}
auto& impl = GetImpl(chan);
auto& impl = GetChannelImpl(chan);
impl.Recycle();
auto& stream = Pomme::Files::GetStream(fRefNum);
auto& fileStream = Pomme::Files::GetStream(fRefNum);
// Rewind -- the file might've been fully played already and we might just be trying to loop it
stream.seekg(0, std::ios::beg);
Pomme::Sound::ReadAIFF(stream, impl.source);
fileStream.seekg(0, std::ios::beg);
// Get metadata from AIFF
Pomme::Sound::SampledSoundInfo info = {};
std::streampos sampledSoundDataOffset = GetSoundInfoFromAIFF(fileStream, info);
// Have file stream seek to start of SSND sampled sound data
if (sampledSoundDataOffset <= 0)
throw std::runtime_error("dubious offset to SSND data");
fileStream.seekg(sampledSoundDataOffset, std::ios::beg);
// Read samples into WavStream
if (!info.isCompressed)
{
// Raw big-endian PCM -- just init the WavStream without decoding
auto spanOut = impl.source.GetBuffer(info.decompressedLength);
fileStream.read(spanOut.data(), info.decompressedLength);
impl.source.Init(info.sampleRate, info.codecBitDepth, info.nChannels, info.bigEndian, spanOut);
}
else
{
auto ssnd = std::vector<char>(info.compressedLength);
fileStream.read(ssnd.data(), info.compressedLength);
auto codec = Pomme::Sound::GetCodec(info.compressionType);
auto spanIn = std::span(ssnd);
auto spanOut = impl.source.GetBuffer(info.decompressedLength);
codec->Decode(info.nChannels, spanIn, spanOut);
impl.source.Init(info.sampleRate, 16, info.nChannels, false, spanOut);
}
if (theCompletion)
{
@ -541,7 +570,7 @@ OSErr SndStartFilePlay(
OSErr SndPauseFilePlay(SndChannelPtr chan)
{
// TODO: check that chan is being used for play from disk
GetImpl(chan).source.TogglePause();
GetChannelImpl(chan).source.TogglePause();
return noErr;
}
@ -550,7 +579,7 @@ OSErr SndStopFilePlay(SndChannelPtr chan, Boolean quietNow)
// TODO: check that chan is being used for play from disk
if (!quietNow)
TODO2("quietNow==false not supported yet, sound will be cut off immediately instead");
GetImpl(chan).source.Stop();
GetChannelImpl(chan).source.Stop();
return noErr;
}

View File

@ -7,25 +7,13 @@
using namespace Pomme::Sound;
//-----------------------------------------------------------------------------
// Cookie-cutter sound command list.
// Used to generate 'snd ' resources.
static const uint8_t kSampledSoundCommandList[20] =
enum SoundResourceType
{
0,1, // format
0,1, // modifier count
0,5, // modifier "sampled synth"
0,0,0,0, // init bits
0,1, // command count
0x80,soundCmd, // command soundCmd (high bit set)
0,0, // param1
0,0,0,20, // param2 (offset)
// Sample data follows
kSoundResourceType_Standard = 0x0001,
kSoundResourceType_HyperCard = 0x0002,
kSoundResourceType_Pomme = 'po', // Pomme extension: only sampled data, no command list
};
constexpr int kSampledSoundCommandListLength = sizeof(kSampledSoundCommandList);
//-----------------------------------------------------------------------------
// 'snd ' resource header
@ -37,7 +25,6 @@ struct SampledSoundHeader
SInt32 stdSH_nBytes;
SInt32 cmpSH_nChannels;
SInt32 extSH_nChannels;
SInt32 nativeSH_nBytes;
};
UnsignedFixed fixedSampleRate;
UInt32 loopStart;
@ -54,14 +41,14 @@ constexpr const char* kSampledSoundHeaderPackFormat = "IiIIIbb";
enum SampledSoundEncoding
{
stdSH = 0x00,
nativeSH_mono16 = 0x10, // pomme extension
nativeSH_stereo16 = 0x11, // pomme extension
cmpSH = 0xFE,
extSH = 0xFF,
kSampledSoundEncoding_stdSH = 0x00, // standard sound header (noncompressed 8-bit mono sample data)
kSampledSoundEncoding_cmpSH = 0xFE, // compressed sound header
kSampledSoundEncoding_extSH = 0xFF, // extended sound header (noncompressed 8/16-bit mono or stereo)
};
//-----------------------------------------------------------------------------
// IM:S:2-58 "MyGetSoundHeaderOffset"
OSErr GetSoundHeaderOffset(SndListHandle sndHandle, long* offset)
{
memstream sndStream((Ptr) *sndHandle, GetHandleSize((Handle) sndHandle));
@ -71,7 +58,7 @@ OSErr GetSoundHeaderOffset(SndListHandle sndHandle, long* offset)
SInt16 format = f.Read<SInt16>();
switch (format)
{
case 1: // Standard 'snd ' resource
case kSoundResourceType_Standard:
{
SInt16 modifierCount = f.Read<SInt16>();
SInt16 synthType = f.Read<SInt16>();
@ -89,10 +76,15 @@ OSErr GetSoundHeaderOffset(SndListHandle sndHandle, long* offset)
break;
}
case 2: // HyperCard sampled-sound format
case kSoundResourceType_HyperCard:
f.Skip(2); // Skip reference count (for application use)
break;
case kSoundResourceType_Pomme:
*offset = 2;
// return now - our own sound resources just have sampled data, no commands
return noErr;
default:
return badFormat;
}
@ -122,8 +114,22 @@ OSErr GetSoundHeaderOffset(SndListHandle sndHandle, long* offset)
return badFormat;
}
//-----------------------------------------------------------------------------
void Pomme::Sound::GetSoundInfo(const Ptr sndhdr, SampledSoundInfo& info)
{
// Check if this snd resource is in Pomme's internal format.
// If so, the resource's header is a raw SampledSoundInfo record,
// which lets us bypass the parsing of a real Mac snd resource.
if (0 == memcmp("POMM", sndhdr, 4))
{
memcpy(&info, sndhdr+4, sizeof(SampledSoundInfo));
info.dataStart = sndhdr+4+sizeof(SampledSoundInfo);
return;
}
// It's a real Mac snd resource. Parse it.
// Prep the BE reader on the header.
memstream headerInput(sndhdr, kSampledSoundHeaderLength + 42);
Pomme::BigEndianIStream f(headerInput);
@ -149,7 +155,7 @@ void Pomme::Sound::GetSoundInfo(const Ptr sndhdr, SampledSoundInfo& info)
switch (header.encoding)
{
case 0x00: // stdSH - standard sound header (noncompressed 8-bit mono sample data)
case kSampledSoundEncoding_stdSH:
info.compressionType = 'raw '; // unsigned (in AIFF-C files, 'NONE' means signed!)
info.isCompressed = false;
info.bigEndian = false;
@ -161,25 +167,12 @@ void Pomme::Sound::GetSoundInfo(const Ptr sndhdr, SampledSoundInfo& info)
info.decompressedLength = info.compressedLength;
break;
case nativeSH_mono16: // pomme extension for little-endian PCM data
case nativeSH_stereo16:
info.compressionType = 'sowt';
info.isCompressed = false;
info.bigEndian = false;
info.codecBitDepth = 16;
info.nChannels = header.encoding == nativeSH_mono16 ? 1 : 2;
info.nPackets = header.nativeSH_nBytes / (2 * info.nChannels);
info.dataStart = sndhdr + f.Tell();
info.compressedLength = header.nativeSH_nBytes;
info.decompressedLength = info.compressedLength;
break;
case 0xFE: // cmpSH - compressed sound header
case kSampledSoundEncoding_cmpSH:
{
info.nPackets = f.Read<int32_t>();
f.Skip(14);
f.Skip(14); // skip AIFFSampleRate(10), markerChunk(4)
info.compressionType = f.Read<uint32_t>();
f.Skip(20);
f.Skip(20); // skip futureUse2(4), stateVars(4), leftOverSamples(4), compressionID(2), packetSize(2), snthID(2)
if (info.compressionType == 0) // Assume MACE-3
{
@ -202,12 +195,12 @@ void Pomme::Sound::GetSoundInfo(const Ptr sndhdr, SampledSoundInfo& info)
break;
}
case 0xFF: // extSH - extended sound header (noncompressed 8/16-bit mono or stereo)
case kSampledSoundEncoding_extSH:
{
info.nPackets = f.Read<int32_t>();
f.Skip(22);
f.Skip(22); // skip AIFFSampleRate(10), markerChunk(4), instrumentChunks(10), AESRecording(4)
info.codecBitDepth = f.Read<int16_t>();
f.Skip(14);
f.Skip(14); // skip futureUse1(2), futureUse2(4), futureUse3(4), futureUse4(4)
info.isCompressed = false;
info.bigEndian = true;
@ -227,6 +220,8 @@ void Pomme::Sound::GetSoundInfo(const Ptr sndhdr, SampledSoundInfo& info)
}
}
//-----------------------------------------------------------------------------
void Pomme::Sound::GetSoundInfoFromSndResource(Handle sndHandle, SampledSoundInfo& info)
{
long offsetToHeader;
@ -238,56 +233,82 @@ void Pomme::Sound::GetSoundInfoFromSndResource(Handle sndHandle, SampledSoundInf
GetSoundInfo(sndhdr, info);
}
//-----------------------------------------------------------------------------
// Extension: load AIFF file as resource
SndListHandle Pomme_SndLoadFileAsResource(short fRefNum)
{
auto& stream = Pomme::Files::GetStream(fRefNum);
Pomme::Sound::SampledSoundInfo info = {};
std::streampos ssndStart = Pomme::Sound::GetSoundInfoFromAIFF(stream, info);
stream.seekg(ssndStart, std::ios::beg);
Handle h = NewHandleClear(2 + 4 + sizeof(SampledSoundInfo) + info.compressedLength);
memcpy(*h, "poPOMM", 6);
memcpy(*h+6, &info, sizeof(SampledSoundInfo));
stream.read(*h+6+sizeof(SampledSoundInfo), info.compressedLength);
return (SndListHandle) h;
}
//-----------------------------------------------------------------------------
// Extension: decompress
Boolean Pomme_DecompressSoundResource(SndListHandle* sndHandlePtr, long* offsetToHeader)
{
SampledSoundInfo info;
GetSoundInfoFromSndResource((Handle) *sndHandlePtr, info);
SampledSoundInfo inInfo;
GetSoundInfoFromSndResource((Handle) *sndHandlePtr, inInfo);
// We only handle cmpSH (compressed) 'snd ' resources.
if (!info.isCompressed)
if (!inInfo.dataStart)
{
return false;
throw std::runtime_error("cannot decompress snd resource without dataStart");
}
int outInitialSize = kSampledSoundCommandListLength + kSampledSoundHeaderLength;
Handle h = NewHandleClear(2 + 4 + sizeof(SampledSoundInfo) + inInfo.decompressedLength);
const char* inDataStart = inInfo.dataStart;
char* outDataStart = *h+6+sizeof(SampledSoundInfo);
std::unique_ptr<Pomme::Sound::Codec> codec = Pomme::Sound::GetCodec(info.compressionType);
SampledSoundInfo outInfo = inInfo;
outInfo.dataStart = nullptr;
outInfo.isCompressed = false;
outInfo.compressedLength = outInfo.decompressedLength;
// Decompress
SndListHandle outHandle = (SndListHandle) NewHandle(outInitialSize + info.decompressedLength);
auto spanIn = std::span(info.dataStart, info.compressedLength);
auto spanOut = std::span((char*) *outHandle + outInitialSize, info.decompressedLength);
codec->Decode(info.nChannels, spanIn, spanOut);
if (!inInfo.isCompressed)
{
// Raw big-endian PCM
if (inInfo.decompressedLength != inInfo.compressedLength)
throw std::runtime_error("decompressedLength != compressedLength???");
// ------------------------------------------------------
// Now we have the PCM data.
// Put the output 'snd ' resource together.
memcpy(outDataStart, inDataStart, inInfo.decompressedLength);
}
else
{
auto codec = Pomme::Sound::GetCodec(inInfo.compressionType);
auto spanIn = std::span(inDataStart, inInfo.compressedLength);
auto spanOut = std::span(outDataStart, inInfo.decompressedLength);
codec->Decode(inInfo.nChannels, spanIn, spanOut);
SampledSoundHeader shOut = {};
shOut.zero = 0;
shOut.nativeSH_nBytes = info.decompressedLength;
shOut.fixedSampleRate = static_cast<UnsignedFixed>(info.sampleRate * 65536.0);
shOut.loopStart = info.loopStart;
shOut.loopEnd = info.loopEnd;
shOut.encoding = info.nChannels == 2 ? nativeSH_stereo16 : nativeSH_mono16;
shOut.baseFrequency = info.baseNote;
outInfo.compressionType = 'swot';
outInfo.bigEndian = false;
outInfo.codecBitDepth = 16;
outInfo.nPackets = codec->SamplesPerPacket() * inInfo.nPackets;
}
ByteswapStructs(kSampledSoundHeaderPackFormat, kSampledSoundHeaderLength, 1, reinterpret_cast<char*>(&shOut));
memcpy(*outHandle, kSampledSoundCommandList, kSampledSoundCommandListLength);
memcpy((char*) *outHandle + kSampledSoundCommandListLength, &shOut, kSampledSoundHeaderLength);
// Write header
memcpy(*h, "poPOMM", 6);
memcpy(*h+6, &outInfo, sizeof(SampledSoundInfo));
// Nuke compressed sound handle, replace it with the decopmressed one we've just created
DisposeHandle((Handle) *sndHandlePtr);
*sndHandlePtr = outHandle;
*offsetToHeader = kSampledSoundCommandListLength;
*sndHandlePtr = (SndListHandle) h;
*offsetToHeader = 2;
// Check offset
long offsetCheck = 0;
OSErr err = GetSoundHeaderOffset(outHandle, &offsetCheck);
if (err != noErr || offsetCheck != kSampledSoundCommandListLength)
OSErr err = GetSoundHeaderOffset((SndListHandle) h, &offsetCheck);
if (err != noErr || offsetCheck != 2)
{
throw std::runtime_error("Incorrect decompressed sound header offset");
}

View File

@ -44,11 +44,16 @@ std::vector<unsigned char> Pomme::BigEndianIStream::ReadBytes(size_t n)
return buf;
}
std::string Pomme::BigEndianIStream::ReadPascalString()
std::string Pomme::BigEndianIStream::ReadPascalString(int padToAlignment)
{
int length = Read<uint8_t>();
auto bytes = ReadBytes(length);
bytes.push_back('\0');
int padding = (length + 1) % padToAlignment;
if (padding != 0)
Skip(padding);
return std::string((const char*) &bytes.data()[0]);
}

View File

@ -37,7 +37,7 @@ namespace Pomme
std::vector<unsigned char> ReadBytes(size_t n);
std::string ReadPascalString();
std::string ReadPascalString(int padToAlignment = 1);
std::string ReadPascalString_FixedLengthRecord(const int maxChars);