mirror of
https://github.com/jorio/Pomme.git
synced 2024-11-27 05:49:18 +00:00
Separate sound playback from sound utils
This commit is contained in:
parent
664c738817
commit
ebea9d03cd
@ -44,17 +44,25 @@ set(POMME_SOURCES
|
||||
if (NOT(POMME_NO_SOUND))
|
||||
list(APPEND POMME_SOURCES
|
||||
${POMME_SRCDIR}/Sound/AIFF.cpp
|
||||
${POMME_SRCDIR}/Sound/cmixer.cpp
|
||||
${POMME_SRCDIR}/Sound/cmixer.h
|
||||
${POMME_SRCDIR}/Sound/IMA4.cpp
|
||||
${POMME_SRCDIR}/Sound/MACE.cpp
|
||||
${POMME_SRCDIR}/Sound/SoundManager.cpp
|
||||
${POMME_SRCDIR}/Sound/SoundUtilities.cpp
|
||||
${POMME_SRCDIR}/Sound/xlaw.cpp
|
||||
)
|
||||
else()
|
||||
add_compile_definitions(POMME_NO_SOUND)
|
||||
endif()
|
||||
|
||||
if (NOT(POMME_NO_SOUND_PLAYBACK))
|
||||
list(APPEND POMME_SOURCES
|
||||
${POMME_SRCDIR}/Sound/cmixer.cpp
|
||||
${POMME_SRCDIR}/Sound/cmixer.h
|
||||
${POMME_SRCDIR}/Sound/SoundManager.cpp
|
||||
)
|
||||
else()
|
||||
add_compile_definitions(POMME_NO_SOUND_PLAYBACK)
|
||||
endif()
|
||||
|
||||
if (NOT(POMME_NO_GRAPHICS))
|
||||
list(APPEND POMME_SOURCES
|
||||
${POMME_SRCDIR}/Graphics/ARGBPixmap.cpp
|
||||
|
@ -53,7 +53,7 @@ void Pomme::Init()
|
||||
Pomme::Graphics::Init();
|
||||
#endif
|
||||
|
||||
#ifndef POMME_NO_SOUND
|
||||
#ifndef POMME_NO_SOUND_PLAYBACK
|
||||
Pomme::Sound::Init();
|
||||
#endif
|
||||
|
||||
@ -64,7 +64,7 @@ void Pomme::Init()
|
||||
|
||||
void Pomme::Shutdown()
|
||||
{
|
||||
#ifndef POMME_NO_SOUND
|
||||
#ifndef POMME_NO_SOUND_PLAYBACK
|
||||
Pomme::Sound::Shutdown();
|
||||
#endif
|
||||
}
|
||||
|
@ -16,6 +16,23 @@ namespace Pomme::Sound
|
||||
|
||||
void ReadAIFF(std::istream& input, cmixer::WavStream& output);
|
||||
|
||||
struct SampledSoundInfo
|
||||
{
|
||||
int16_t nChannels;
|
||||
uint32_t nPackets;
|
||||
int16_t codecBitDepth;
|
||||
bool bigEndian;
|
||||
double sampleRate;
|
||||
bool isCompressed;
|
||||
uint32_t compressionType;
|
||||
char* dataStart;
|
||||
int compressedLength;
|
||||
int decompressedLength;
|
||||
int8_t baseNote;
|
||||
uint32_t loopStart;
|
||||
uint32_t loopEnd;
|
||||
};
|
||||
|
||||
class Codec
|
||||
{
|
||||
public:
|
||||
@ -79,5 +96,9 @@ namespace Pomme::Sound
|
||||
void Decode(const int nChannels, const std::span<const char> input, const std::span<char> output) override;
|
||||
};
|
||||
|
||||
void GetSoundInfo(const Ptr sndhdr, SampledSoundInfo& info);
|
||||
|
||||
void GetSoundInfoFromSndResource(Handle sndHandle, SampledSoundInfo& info);
|
||||
|
||||
std::unique_ptr<Pomme::Sound::Codec> GetCodec(uint32_t fourCC);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
#include "Pomme.h"
|
||||
#include "PommeFiles.h"
|
||||
#include "Sound/cmixer.h"
|
||||
#include "cmixer.h"
|
||||
#include "PommeSound.h"
|
||||
#include "Utilities/bigendianstreams.h"
|
||||
#include "Utilities/IEEEExtended.h"
|
||||
@ -19,76 +19,6 @@ static struct ChannelImpl* headChan = nullptr;
|
||||
static int nManagedChans = 0;
|
||||
static double midiNoteFrequencies[128];
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Cookie-cutter sound command list.
|
||||
// Used to generate 'snd ' resources.
|
||||
|
||||
static const uint8_t kSampledSoundCommandList[20] = {
|
||||
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
|
||||
};
|
||||
|
||||
constexpr int kSampledSoundCommandListLength = sizeof(kSampledSoundCommandList);
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// '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;
|
||||
SInt32 nativeSH_nBytes;
|
||||
};
|
||||
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
|
||||
{
|
||||
stdSH = 0x00,
|
||||
nativeSH_mono16 = 0x10, // pomme extension
|
||||
nativeSH_stereo16 = 0x11, // pomme extension
|
||||
cmpSH = 0xFE,
|
||||
extSH = 0xFF,
|
||||
};
|
||||
|
||||
struct SampledSoundInfo
|
||||
{
|
||||
int16_t nChannels;
|
||||
uint32_t nPackets;
|
||||
int16_t codecBitDepth;
|
||||
bool bigEndian;
|
||||
double sampleRate;
|
||||
bool isCompressed;
|
||||
uint32_t compressionType;
|
||||
char* dataStart;
|
||||
int compressedLength;
|
||||
int decompressedLength;
|
||||
int8_t baseNote;
|
||||
uint32_t loopStart;
|
||||
uint32_t loopEnd;
|
||||
};
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Internal channel info
|
||||
|
||||
@ -272,123 +202,6 @@ static inline ChannelImpl& GetImpl(SndChannelPtr chan)
|
||||
return *(ChannelImpl*) chan->channelImpl;
|
||||
}
|
||||
|
||||
static void GetSoundInfo(const Ptr sndhdr, SampledSoundInfo& info)
|
||||
{
|
||||
// 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 0x00: // stdSH - standard sound header (noncompressed 8-bit mono sample data)
|
||||
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 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
|
||||
{
|
||||
info.nPackets = f.Read<int32_t>();
|
||||
f.Skip(14);
|
||||
info.compressionType = f.Read<uint32_t>();
|
||||
f.Skip(20);
|
||||
|
||||
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 0xFF: // extSH - extended sound header (noncompressed 8/16-bit mono or stereo)
|
||||
{
|
||||
info.nPackets = f.Read<int32_t>();
|
||||
f.Skip(22);
|
||||
info.codecBitDepth = f.Read<int16_t>();
|
||||
f.Skip(14);
|
||||
|
||||
info.isCompressed = false;
|
||||
info.bigEndian = true;
|
||||
info.compressionType = 'twos'; // TODO: if 16-bit, should we use 'raw ' or 'NONE'/'twos'?
|
||||
info.nChannels = header.extSH_nChannels;
|
||||
info.dataStart = sndhdr + f.Tell();
|
||||
info.compressedLength = header.extSH_nChannels * info.nPackets * info.codecBitDepth / 8;
|
||||
info.decompressedLength = info.compressedLength;
|
||||
|
||||
if (info.codecBitDepth == 8)
|
||||
TODO2("should an 8-bit extSH be 'twos' or 'raw '?");
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
TODOFATAL2("unsupported snd header encoding " << (int)header.encoding);
|
||||
}
|
||||
}
|
||||
|
||||
static void GetSoundInfoFromSndResource(Handle sndHandle, SampledSoundInfo& info)
|
||||
{
|
||||
long offsetToHeader;
|
||||
|
||||
GetSoundHeaderOffset((SndListHandle) sndHandle, &offsetToHeader);
|
||||
|
||||
Ptr sndhdr = (Ptr) (*sndHandle) + offsetToHeader;
|
||||
|
||||
GetSoundInfo(sndhdr, info);
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// MIDI note utilities
|
||||
|
||||
@ -524,7 +337,7 @@ static void InstallSoundInChannel(SndChannelPtr chan, const Ptr sampledSoundHead
|
||||
//---------------------------------
|
||||
// Distill sound info
|
||||
|
||||
SampledSoundInfo info;
|
||||
Pomme::Sound::SampledSoundInfo info;
|
||||
GetSoundInfo(sampledSoundHeader, info);
|
||||
|
||||
//---------------------------------
|
||||
@ -666,66 +479,6 @@ OSErr SndDoCommand(SndChannelPtr chan, const SndCommand* cmd, Boolean noWait)
|
||||
return noErr;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static void Expect(const T a, const T b, const char* msg)
|
||||
{
|
||||
if (a != b)
|
||||
throw std::runtime_error(msg);
|
||||
}
|
||||
|
||||
// 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 1: // Standard 'snd ' resource
|
||||
{
|
||||
Expect<SInt16>(1, f.Read<SInt16>(), "'snd ' modifier count");
|
||||
Expect<SInt16>(5, f.Read<SInt16>(), "'snd ' sampledSynth");
|
||||
UInt32 initBits = f.Read<UInt32>();
|
||||
if (initBits & initMACE6)
|
||||
TODOFATAL2("MACE-6 not supported yet");
|
||||
break;
|
||||
}
|
||||
|
||||
case 2: // HyperCard sampled-sound format
|
||||
f.Skip(2); // Skip reference count (for application use)
|
||||
break;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
LOG << "didn't find offset in snd resource\n";
|
||||
return badFormat;
|
||||
}
|
||||
|
||||
OSErr SndStartFilePlay(
|
||||
SndChannelPtr chan,
|
||||
short fRefNum,
|
||||
@ -811,63 +564,6 @@ NumVersion SndSoundManagerVersion()
|
||||
return v;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Extension: decompress
|
||||
|
||||
Boolean Pomme_DecompressSoundResource(SndListHandle* sndHandlePtr, long* offsetToHeader)
|
||||
{
|
||||
SampledSoundInfo info;
|
||||
GetSoundInfoFromSndResource((Handle) *sndHandlePtr, info);
|
||||
|
||||
// We only handle cmpSH (compressed) 'snd ' resources.
|
||||
if (!info.isCompressed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int outInitialSize = kSampledSoundCommandListLength + kSampledSoundHeaderLength;
|
||||
|
||||
std::unique_ptr<Pomme::Sound::Codec> codec = Pomme::Sound::GetCodec(info.compressionType);
|
||||
|
||||
// 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);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Now we have the PCM data.
|
||||
// Put the output 'snd ' resource together.
|
||||
|
||||
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;
|
||||
|
||||
ByteswapStructs(kSampledSoundHeaderPackFormat, kSampledSoundHeaderLength, 1, reinterpret_cast<char*>(&shOut));
|
||||
|
||||
memcpy(*outHandle, kSampledSoundCommandList, kSampledSoundCommandListLength);
|
||||
memcpy((char*) *outHandle + kSampledSoundCommandListLength, &shOut, kSampledSoundHeaderLength);
|
||||
|
||||
// Nuke compressed sound handle, replace it with the decopmressed one we've just created
|
||||
DisposeHandle((Handle) *sndHandlePtr);
|
||||
*sndHandlePtr = outHandle;
|
||||
*offsetToHeader = kSampledSoundCommandListLength;
|
||||
|
||||
long offsetCheck = 0;
|
||||
OSErr err = GetSoundHeaderOffset(outHandle, &offsetCheck);
|
||||
if (err != noErr || offsetCheck != kSampledSoundCommandListLength)
|
||||
{
|
||||
throw std::runtime_error("Incorrect decompressed sound header offset");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Extension: pause/unpause channels that are currently playing
|
||||
|
||||
@ -906,21 +602,3 @@ void Pomme::Sound::Shutdown()
|
||||
SndDisposeChannel(headChan->macChannel, 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));
|
||||
}
|
||||
}
|
||||
|
315
src/Sound/SoundUtilities.cpp
Normal file
315
src/Sound/SoundUtilities.cpp
Normal file
@ -0,0 +1,315 @@
|
||||
#include "Pomme.h"
|
||||
#include "PommeFiles.h"
|
||||
#include "PommeSound.h"
|
||||
#include "Utilities/memstream.h"
|
||||
#include "Utilities/bigendianstreams.h"
|
||||
#include <cstring>
|
||||
|
||||
using namespace Pomme::Sound;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Cookie-cutter sound command list.
|
||||
// Used to generate 'snd ' resources.
|
||||
|
||||
static const uint8_t kSampledSoundCommandList[20] =
|
||||
{
|
||||
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
|
||||
};
|
||||
|
||||
constexpr int kSampledSoundCommandListLength = sizeof(kSampledSoundCommandList);
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// '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;
|
||||
SInt32 nativeSH_nBytes;
|
||||
};
|
||||
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
|
||||
{
|
||||
stdSH = 0x00,
|
||||
nativeSH_mono16 = 0x10, // pomme extension
|
||||
nativeSH_stereo16 = 0x11, // pomme extension
|
||||
cmpSH = 0xFE,
|
||||
extSH = 0xFF,
|
||||
};
|
||||
|
||||
// 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 1: // Standard 'snd ' resource
|
||||
{
|
||||
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 2: // HyperCard sampled-sound format
|
||||
f.Skip(2); // Skip reference count (for application use)
|
||||
break;
|
||||
|
||||
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)
|
||||
{
|
||||
// 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 0x00: // stdSH - standard sound header (noncompressed 8-bit mono sample data)
|
||||
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 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
|
||||
{
|
||||
info.nPackets = f.Read<int32_t>();
|
||||
f.Skip(14);
|
||||
info.compressionType = f.Read<uint32_t>();
|
||||
f.Skip(20);
|
||||
|
||||
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 0xFF: // extSH - extended sound header (noncompressed 8/16-bit mono or stereo)
|
||||
{
|
||||
info.nPackets = f.Read<int32_t>();
|
||||
f.Skip(22);
|
||||
info.codecBitDepth = f.Read<int16_t>();
|
||||
f.Skip(14);
|
||||
|
||||
info.isCompressed = false;
|
||||
info.bigEndian = true;
|
||||
info.compressionType = 'twos'; // TODO: if 16-bit, should we use 'raw ' or 'NONE'/'twos'?
|
||||
info.nChannels = header.extSH_nChannels;
|
||||
info.dataStart = sndhdr + f.Tell();
|
||||
info.compressedLength = header.extSH_nChannels * info.nPackets * info.codecBitDepth / 8;
|
||||
info.decompressedLength = info.compressedLength;
|
||||
|
||||
if (info.codecBitDepth == 8)
|
||||
TODO2("should an 8-bit extSH be 'twos' or 'raw '?");
|
||||
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);
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Extension: decompress
|
||||
|
||||
Boolean Pomme_DecompressSoundResource(SndListHandle* sndHandlePtr, long* offsetToHeader)
|
||||
{
|
||||
SampledSoundInfo info;
|
||||
GetSoundInfoFromSndResource((Handle) *sndHandlePtr, info);
|
||||
|
||||
// We only handle cmpSH (compressed) 'snd ' resources.
|
||||
if (!info.isCompressed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int outInitialSize = kSampledSoundCommandListLength + kSampledSoundHeaderLength;
|
||||
|
||||
std::unique_ptr<Pomme::Sound::Codec> codec = Pomme::Sound::GetCodec(info.compressionType);
|
||||
|
||||
// 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);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Now we have the PCM data.
|
||||
// Put the output 'snd ' resource together.
|
||||
|
||||
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;
|
||||
|
||||
ByteswapStructs(kSampledSoundHeaderPackFormat, kSampledSoundHeaderLength, 1, reinterpret_cast<char*>(&shOut));
|
||||
|
||||
memcpy(*outHandle, kSampledSoundCommandList, kSampledSoundCommandListLength);
|
||||
memcpy((char*) *outHandle + kSampledSoundCommandListLength, &shOut, kSampledSoundHeaderLength);
|
||||
|
||||
// Nuke compressed sound handle, replace it with the decopmressed one we've just created
|
||||
DisposeHandle((Handle) *sndHandlePtr);
|
||||
*sndHandlePtr = outHandle;
|
||||
*offsetToHeader = kSampledSoundCommandListLength;
|
||||
|
||||
long offsetCheck = 0;
|
||||
OSErr err = GetSoundHeaderOffset(outHandle, &offsetCheck);
|
||||
if (err != noErr || offsetCheck != kSampledSoundCommandListLength)
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
@ -22,7 +22,7 @@
|
||||
** IN THE SOFTWARE.
|
||||
**/
|
||||
|
||||
#include "Sound/cmixer.h"
|
||||
#include "cmixer.h"
|
||||
#include "Utilities/structpack.h"
|
||||
#include <SDL.h>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user