Pomme/src/SoundFormats/SoundFormats.cpp

383 lines
11 KiB
C++

#include "Pomme.h"
#include "PommeFiles.h"
#include "PommeSound.h"
#include "Utilities/memstream.h"
#include "Utilities/bigendianstreams.h"
#include <cstring>
using namespace Pomme::Sound;
enum SoundResourceType
{
kSoundResourceType_Standard = 0x0001,
kSoundResourceType_HyperCard = 0x0002,
kSoundResourceType_Pomme = 'po', // Pomme extension: only sampled data, no command list
};
//-----------------------------------------------------------------------------
// 'snd ' resource header
struct SampledSoundHeader
{
UInt32 zero;
union // the meaning of union this is decided by the encoding type
{
SInt32 stdSH_nBytes;
SInt32 cmpSH_nChannels;
SInt32 extSH_nChannels;
};
UnsignedFixed fixedSampleRate;
UInt32 loopStart;
UInt32 loopEnd;
Byte encoding;
Byte baseFrequency; // 0-127, see Table 2-2, IM:S:2-43
};
static_assert(sizeof(SampledSoundHeader) >= 22 && sizeof(SampledSoundHeader) <= 24,
"unexpected SampledSoundHeader size");
constexpr int kSampledSoundHeaderLength = 22;
constexpr const char* kSampledSoundHeaderPackFormat = "IiIIIbb";
enum SampledSoundEncoding
{
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));
Pomme::BigEndianIStream f(sndStream);
// Read header
SInt16 format = f.Read<SInt16>();
switch (format)
{
case kSoundResourceType_Standard:
{
SInt16 modifierCount = f.Read<SInt16>();
SInt16 synthType = f.Read<SInt16>();
UInt32 initBits = f.Read<UInt32>();
if (1 != modifierCount)
TODOFATAL2("only 1 modifier per 'snd ' is supported");
if (5 != synthType)
TODOFATAL2("only sampledSynth 'snd ' is supported");
if (initBits & initMACE6)
TODOFATAL2("MACE-6 not supported yet");
break;
}
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;
}
// Now read sound commands
SInt16 nCmds = f.Read<SInt16>();
//LOG << nCmds << " commands\n";
for (; nCmds >= 1; nCmds--)
{
UInt16 cmd = f.Read<UInt16>();
f.Skip(2); // SInt16 param1
SInt32 param2 = f.Read<SInt32>();
cmd &= 0x7FFF; // See IM:S:2-75
// When a sound command contained in an 'snd ' resource has associated sound data,
// the high bit of the command is set. This changes the meaning of the param2 field of the
// command from a pointer to a location in RAM to an offset value that specifies the offset
// in bytes from the resource's beginning to the location of the associated sound data (such
// as a sampled sound header).
if (cmd == bufferCmd || cmd == soundCmd)
{
*offset = param2;
return noErr;
}
}
TODOMINOR2("didn't find offset in snd resource");
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);
// Read in SampledSoundHeader and unpack it.
SampledSoundHeader header;
f.Read(reinterpret_cast<char*>(&header), kSampledSoundHeaderLength);
ByteswapStructs(kSampledSoundHeaderPackFormat, kSampledSoundHeaderLength, 1, reinterpret_cast<char*>(&header));
if (header.zero != 0)
{
// The first field can be a pointer to the sampled-sound data.
// In practice it's always gonna be 0.
TODOFATAL2("expected 0 at the beginning of an snd");
}
memset(&info, 0, sizeof(info));
info.sampleRate = static_cast<uint32_t>(header.fixedSampleRate) / 65536.0;
info.baseNote = header.baseFrequency;
info.loopStart = header.loopStart;
info.loopEnd = header.loopEnd;
switch (header.encoding)
{
case kSampledSoundEncoding_stdSH:
info.compressionType = 'raw '; // unsigned (in AIFF-C files, 'NONE' means signed!)
info.isCompressed = false;
info.bigEndian = false;
info.codecBitDepth = 8;
info.nChannels = 1;
info.nPackets = header.stdSH_nBytes;
info.dataStart = sndhdr + f.Tell();
info.compressedLength = header.stdSH_nBytes;
info.decompressedLength = info.compressedLength;
break;
case kSampledSoundEncoding_cmpSH:
{
info.nPackets = f.Read<int32_t>();
f.Skip(14); // skip AIFFSampleRate(10), markerChunk(4)
info.compressionType = f.Read<uint32_t>();
f.Skip(20); // skip futureUse2(4), stateVars(4), leftOverSamples(4), compressionID(2), packetSize(2), snthID(2)
if (info.compressionType == 0) // Assume MACE-3
{
// Assume MACE-3. It should've been set in the init options in the snd pre-header,
// but Nanosaur doesn't actually init the sound channels for MACE-3. So I guess the Mac
// assumes by default that any unspecified compression is MACE-3.
// If it wasn't MACE-3, it would've been caught by GetSoundHeaderOffset.
info.compressionType = 'MAC3';
}
std::unique_ptr<Pomme::Sound::Codec> codec = Pomme::Sound::GetCodec(info.compressionType);
info.isCompressed = true;
info.bigEndian = false;
info.nChannels = header.cmpSH_nChannels;
info.dataStart = sndhdr + f.Tell();
info.codecBitDepth = codec->AIFFBitDepth();
info.compressedLength = info.nChannels * info.nPackets * codec->BytesPerPacket();
info.decompressedLength = info.nChannels * info.nPackets * codec->SamplesPerPacket() * 2;
break;
}
case kSampledSoundEncoding_extSH:
{
info.nPackets = f.Read<int32_t>();
f.Skip(22); // skip AIFFSampleRate(10), markerChunk(4), instrumentChunks(10), AESRecording(4)
info.codecBitDepth = f.Read<int16_t>();
f.Skip(14); // skip futureUse1(2), futureUse2(4), futureUse3(4), futureUse4(4)
info.isCompressed = false;
info.bigEndian = true;
info.compressionType = info.codecBitDepth == 8 ? 'raw ' : 'twos'; // unsigned if 8-bit, signed if 16-bit!
info.nChannels = header.extSH_nChannels;
info.dataStart = sndhdr + f.Tell();
info.compressedLength = header.extSH_nChannels * info.nPackets * info.codecBitDepth / 8;
info.decompressedLength = info.compressedLength;
break;
}
default:
TODOFATAL2("unsupported snd header encoding " << (int)header.encoding);
}
}
//-----------------------------------------------------------------------------
void Pomme::Sound::GetSoundInfoFromSndResource(Handle sndHandle, SampledSoundInfo& info)
{
long offsetToHeader;
GetSoundHeaderOffset((SndListHandle) sndHandle, &offsetToHeader);
Ptr sndhdr = (Ptr) (*sndHandle) + offsetToHeader;
GetSoundInfo(sndhdr, info);
}
SndListHandle Pomme::Sound::SampledSoundInfo::MakeStandaloneResource(char** dataOffsetOut) const
{
const char* data = dataStart;
SampledSoundInfo info = *this;
Handle h = NewHandleClear(2 + 4 + sizeof(info) + compressedLength);
Ptr p = *h;
memcpy(p, "poPOMM", 6); // "po": see kSoundResourceType_Pomme; "POMM": see GetSoundInfo
p += 6;
info.dataStart = p + sizeof(info);
memcpy(p, &info, sizeof(info));
p += sizeof(info);
if (data != nullptr)
{
memcpy(p, data, info.compressedLength);
}
if (dataOffsetOut != nullptr)
{
*dataOffsetOut = p;
}
return (SndListHandle) h;
}
//-----------------------------------------------------------------------------
// Extension: load AIFF file as resource
SndListHandle Pomme_SndLoadFileAsResource(short fRefNum)
{
auto& spec = Pomme::Files::GetSpec(fRefNum);
auto& stream = Pomme::Files::GetStream(fRefNum);
std::string fileName(spec.cName);
std::transform(fileName.begin(), fileName.end(), fileName.begin(), tolower);
// Guess media container from extension
if (fileName.ends_with(".aiff")
|| fileName.ends_with(".aifc")
|| fileName.ends_with(".aif"))
{
return LoadAIFFAsResource(stream);
}
else if (fileName.ends_with(".mp3"))
{
#ifndef POMME_NO_MP3
return LoadMP3AsResource(stream);
#endif
}
return nullptr;
}
//-----------------------------------------------------------------------------
// Extension: decompress
Boolean Pomme_DecompressSoundResource(SndListHandle* sndHandlePtr, long* offsetToHeader)
{
SampledSoundInfo inInfo;
GetSoundInfoFromSndResource((Handle) *sndHandlePtr, inInfo);
if (!inInfo.dataStart)
{
throw std::runtime_error("cannot decompress snd resource without dataStart");
}
Handle h = NewHandleClear(2 + 4 + sizeof(SampledSoundInfo) + inInfo.decompressedLength);
const char* inDataStart = inInfo.dataStart;
char* outDataStart = *h+6+sizeof(SampledSoundInfo);
SampledSoundInfo outInfo = inInfo;
outInfo.dataStart = nullptr;
outInfo.isCompressed = false;
outInfo.compressedLength = outInfo.decompressedLength;
if (!inInfo.isCompressed)
{
// Raw PCM
if (inInfo.decompressedLength != inInfo.compressedLength)
throw std::runtime_error("decompressedLength != compressedLength???");
memcpy(outDataStart, inDataStart, inInfo.decompressedLength);
// If it's big endian, swap the bytes
int bytesPerSample = inInfo.codecBitDepth / 8;
if (inInfo.bigEndian && bytesPerSample > 1)
{
int nIntegers = inInfo.decompressedLength / bytesPerSample;
if (inInfo.decompressedLength != nIntegers * bytesPerSample)
throw std::runtime_error("unexpected big-endian raw PCM decompressed length");
ByteswapInts(bytesPerSample, nIntegers, outDataStart);
outInfo.bigEndian = false;
}
}
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);
outInfo.compressionType = 'swot';
outInfo.bigEndian = false;
outInfo.codecBitDepth = 16;
outInfo.nPackets = codec->SamplesPerPacket() * inInfo.nPackets;
}
// 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 = (SndListHandle) h;
*offsetToHeader = 2;
// Check offset
long offsetCheck = 0;
OSErr err = GetSoundHeaderOffset((SndListHandle) h, &offsetCheck);
if (err != noErr || offsetCheck != 2)
{
throw std::runtime_error("Incorrect decompressed sound header offset");
}
return true;
}
//-----------------------------------------------------------------------------
std::unique_ptr<Pomme::Sound::Codec> Pomme::Sound::GetCodec(uint32_t fourCC)
{
switch (fourCC)
{
case 0: // Assume MACE-3 by default.
case 'MAC3':
return std::make_unique<Pomme::Sound::MACE>();
case 'ima4':
return std::make_unique<Pomme::Sound::IMA4>();
case 'alaw':
case 'ulaw':
return std::make_unique<Pomme::Sound::xlaw>(fourCC);
default:
throw std::runtime_error("Unknown audio codec: " + Pomme::FourCCString(fourCC));
}
}