mirror of
https://github.com/jorio/Pomme.git
synced 2024-12-27 06:29:18 +00:00
Dump 'snd ' resources to AIFF-C
This commit is contained in:
parent
b4516298a3
commit
1495c647fd
@ -7,6 +7,10 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include "CompilerSupport/filesystem.h"
|
#include "CompilerSupport/filesystem.h"
|
||||||
|
|
||||||
|
#if _DEBUG
|
||||||
|
#include "PommeSound.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#define LOG POMME_GENLOG(POMME_DEBUG_RESOURCES, "RSRC")
|
#define LOG POMME_GENLOG(POMME_DEBUG_RESOURCES, "RSRC")
|
||||||
|
|
||||||
using namespace Pomme;
|
using namespace Pomme;
|
||||||
@ -79,7 +83,7 @@ static void DumpResource(const ResourceMetadata& meta)
|
|||||||
outPath /= ss.str();
|
outPath /= ss.str();
|
||||||
outPath += "." + Pomme::FourCCString(meta.type, '_');
|
outPath += "." + Pomme::FourCCString(meta.type, '_');
|
||||||
|
|
||||||
std::ofstream dump(outPath, std::ofstream::binary);
|
std::ofstream dump(outPath, std::ios::binary);
|
||||||
|
|
||||||
// Add a 512-byte blank header to PICTs so tools such as ImageMagick or Preview.app will display them
|
// Add a 512-byte blank header to PICTs so tools such as ImageMagick or Preview.app will display them
|
||||||
if (meta.type == 'PICT')
|
if (meta.type == 'PICT')
|
||||||
@ -88,11 +92,24 @@ static void DumpResource(const ResourceMetadata& meta)
|
|||||||
dump.put(0);
|
dump.put(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
dump.write(*handle, meta.size);
|
dump.write(*handle, meta.size);
|
||||||
dump.close();
|
dump.close();
|
||||||
|
|
||||||
std::cout << "wrote " << outPath << "\n";
|
std::cout << "wrote " << outPath << "\n";
|
||||||
|
|
||||||
|
#if _DEBUG
|
||||||
|
// Dump sounds as AIFF as well
|
||||||
|
if (meta.type == 'snd ')
|
||||||
|
{
|
||||||
|
outPath.replace_extension(".aiff");
|
||||||
|
std::ofstream aiff(outPath, std::ios::binary);
|
||||||
|
Pomme::Sound::DumpSoundResourceToAIFF(handle, aiff, meta.name);
|
||||||
|
aiff.close();
|
||||||
|
|
||||||
|
std::cout << "wrote " << outPath << "\n";
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
DisposeHandle(handle);
|
DisposeHandle(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ std::ostream& operator<<(std::ostream& s, const Rect& r)
|
|||||||
|
|
||||||
void Pomme::Graphics::DumpTGA(const char* path, short width, short height, const char* argbData)
|
void Pomme::Graphics::DumpTGA(const char* path, short width, short height, const char* argbData)
|
||||||
{
|
{
|
||||||
std::ofstream tga(path);
|
std::ofstream tga(path, std::ios::binary);
|
||||||
uint16_t tgaHdr[] = {0, 2, 0, 0, 0, 0, (uint16_t) width, (uint16_t) height, 0x2820};
|
uint16_t tgaHdr[] = {0, 2, 0, 0, 0, 0, (uint16_t) width, (uint16_t) height, 0x2820};
|
||||||
tga.write((const char*) tgaHdr, sizeof(tgaHdr));
|
tga.write((const char*) tgaHdr, sizeof(tgaHdr));
|
||||||
for (int i = 0; i < 4 * width * height; i += 4)
|
for (int i = 0; i < 4 * width * height; i += 4)
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "CompilerSupport/span.h"
|
#include "CompilerSupport/span.h"
|
||||||
|
#include "PommeTypes.h"
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <istream>
|
#include <istream>
|
||||||
|
#include <ostream>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include "Sound/cmixer.h"
|
#include "Sound/cmixer.h"
|
||||||
|
|
||||||
@ -14,6 +16,8 @@ namespace Pomme::Sound
|
|||||||
|
|
||||||
void ReadAIFF(std::istream& input, cmixer::WavStream& output);
|
void ReadAIFF(std::istream& input, cmixer::WavStream& output);
|
||||||
|
|
||||||
|
void DumpSoundResourceToAIFF(Handle input, std::ostream& output, const std::string& resourceName);
|
||||||
|
|
||||||
class Codec
|
class Codec
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@ -24,6 +28,8 @@ namespace Pomme::Sound
|
|||||||
|
|
||||||
virtual int BytesPerPacket() = 0;
|
virtual int BytesPerPacket() = 0;
|
||||||
|
|
||||||
|
virtual int AIFFBitDepth() = 0;
|
||||||
|
|
||||||
virtual void Decode(const int nChannels, const std::span<const char> input, const std::span<char> output) = 0;
|
virtual void Decode(const int nChannels, const std::span<const char> input, const std::span<char> output) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -36,6 +42,9 @@ namespace Pomme::Sound
|
|||||||
int BytesPerPacket() override
|
int BytesPerPacket() override
|
||||||
{ return 2; }
|
{ return 2; }
|
||||||
|
|
||||||
|
int AIFFBitDepth() override
|
||||||
|
{ return 8; }
|
||||||
|
|
||||||
void Decode(const int nChannels, const std::span<const char> input, const std::span<char> output) override;
|
void Decode(const int nChannels, const std::span<const char> input, const std::span<char> output) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -48,6 +57,9 @@ namespace Pomme::Sound
|
|||||||
int BytesPerPacket() override
|
int BytesPerPacket() override
|
||||||
{ return 34; }
|
{ return 34; }
|
||||||
|
|
||||||
|
int AIFFBitDepth() override
|
||||||
|
{ return 16; }
|
||||||
|
|
||||||
void Decode(const int nChannels, const std::span<const char> input, const std::span<char> output) override;
|
void Decode(const int nChannels, const std::span<const char> input, const std::span<char> output) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -63,6 +75,9 @@ namespace Pomme::Sound
|
|||||||
int BytesPerPacket() override
|
int BytesPerPacket() override
|
||||||
{ return 1; }
|
{ return 1; }
|
||||||
|
|
||||||
|
int AIFFBitDepth() override
|
||||||
|
{ return 8; }
|
||||||
|
|
||||||
void Decode(const int nChannels, const std::span<const char> input, const std::span<char> output) override;
|
void Decode(const int nChannels, const std::span<const char> input, const std::span<char> output) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
#include "Sound/cmixer.h"
|
#include "Sound/cmixer.h"
|
||||||
#include "PommeSound.h"
|
#include "PommeSound.h"
|
||||||
#include "Utilities/bigendianstreams.h"
|
#include "Utilities/bigendianstreams.h"
|
||||||
|
#include "Utilities/IEEEExtended.h"
|
||||||
#include "Utilities/memstream.h"
|
#include "Utilities/memstream.h"
|
||||||
|
|
||||||
#include <thread>
|
#include <thread>
|
||||||
@ -49,16 +50,11 @@ struct SampledSoundHeader
|
|||||||
SInt32 extSH_nChannels;
|
SInt32 extSH_nChannels;
|
||||||
SInt32 nativeSH_nBytes;
|
SInt32 nativeSH_nBytes;
|
||||||
};
|
};
|
||||||
Fixed fixedSampleRate;
|
UnsignedFixed fixedSampleRate;
|
||||||
UInt32 loopStart;
|
UInt32 loopStart;
|
||||||
UInt32 loopEnd;
|
UInt32 loopEnd;
|
||||||
Byte encoding;
|
Byte encoding;
|
||||||
Byte baseFrequency; // 0-127, see Table 2-2, IM:S:2-43
|
Byte baseFrequency; // 0-127, see Table 2-2, IM:S:2-43
|
||||||
|
|
||||||
unsigned int sampleRate() const
|
|
||||||
{
|
|
||||||
return (static_cast<unsigned int>(fixedSampleRate) >> 16) & 0xFFFF;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static_assert(sizeof(SampledSoundHeader) >= 22 && sizeof(SampledSoundHeader) <= 24,
|
static_assert(sizeof(SampledSoundHeader) >= 22 && sizeof(SampledSoundHeader) <= 24,
|
||||||
@ -76,6 +72,23 @@ enum SampledSoundEncoding
|
|||||||
extSH = 0xFF,
|
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
|
// Internal channel info
|
||||||
|
|
||||||
@ -259,6 +272,123 @@ static inline ChannelImpl& GetImpl(SndChannelPtr chan)
|
|||||||
return *(ChannelImpl*) chan->channelImpl;
|
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
|
// MIDI note utilities
|
||||||
|
|
||||||
@ -382,135 +512,58 @@ OSErr SndChannelStatus(SndChannelPtr chan, short theLength, SCStatusPtr theStatu
|
|||||||
return noErr;
|
return noErr;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ProcessSoundCmd(SndChannelPtr chan, const Ptr sndhdr)
|
// Install a sampled sound as a voice in a channel.
|
||||||
|
static void InstallSoundInChannel(SndChannelPtr chan, const Ptr sampledSoundHeader)
|
||||||
{
|
{
|
||||||
// Install a sampled sound as a voice in a channel. If the high bit of the
|
//---------------------------------
|
||||||
// command is set, param2 is interpreted as an offset from the beginning of
|
// Get internal channel
|
||||||
// the 'snd ' resource containing the command to the sound header. If the
|
|
||||||
// high bit is not set, param2 is interpreted as a pointer to the sound
|
|
||||||
// header.
|
|
||||||
|
|
||||||
auto& impl = GetImpl(chan);
|
auto& impl = GetImpl(chan);
|
||||||
|
|
||||||
impl.Recycle();
|
impl.Recycle();
|
||||||
|
|
||||||
// PACKED RECORD
|
//---------------------------------
|
||||||
memstream headerInput(sndhdr, kSampledSoundHeaderLength + 42);
|
// Distill sound info
|
||||||
Pomme::BigEndianIStream f(headerInput);
|
|
||||||
|
|
||||||
SampledSoundHeader sh;
|
SampledSoundInfo info;
|
||||||
f.Read(reinterpret_cast<char*>(&sh), kSampledSoundHeaderLength);
|
GetSoundInfo(sampledSoundHeader, info);
|
||||||
ByteswapStructs(kSampledSoundHeaderPackFormat, kSampledSoundHeaderLength, 1, reinterpret_cast<char*>(&sh));
|
|
||||||
|
|
||||||
if (sh.zero != 0)
|
//---------------------------------
|
||||||
|
// Set cmixer source data
|
||||||
|
|
||||||
|
auto spanIn = std::span(info.dataStart, info.compressedLength);
|
||||||
|
|
||||||
|
if (info.isCompressed)
|
||||||
{
|
{
|
||||||
// The first field can be a pointer to the sampled-sound data.
|
auto spanOut = impl.source.GetBuffer(info.decompressedLength);
|
||||||
// In practice it's always gonna be 0.
|
|
||||||
TODOFATAL2("expected 0 at the beginning of an snd");
|
|
||||||
}
|
|
||||||
|
|
||||||
int sampleRate = sh.sampleRate();
|
std::unique_ptr<Pomme::Sound::Codec> codec = Pomme::Sound::GetCodec(info.compressionType);
|
||||||
impl.baseNote = sh.baseFrequency;
|
codec->Decode(info.nChannels, spanIn, spanOut);
|
||||||
|
impl.source.Init(info.sampleRate, 16, info.nChannels, false, spanOut);
|
||||||
LOG << sampleRate << "Hz, " << GetMidiNoteName(sh.baseFrequency) << ", loop " << sh.loopStart << "->" << sh.loopEnd << ", ";
|
|
||||||
|
|
||||||
switch (sh.encoding)
|
|
||||||
{
|
|
||||||
case 0x00: // stdSH - standard sound header - IM:S:2-104
|
|
||||||
{
|
|
||||||
// noncompressed sample data (8-bit mono) from this point on
|
|
||||||
char* here = sndhdr + f.Tell();
|
|
||||||
impl.source.Init(sampleRate, 8, 1, false, std::span<char>(here, sh.stdSH_nBytes));
|
|
||||||
LOG_NOPREFIX << "stdSH: 8-bit mono, " << sh.stdSH_nBytes << " frames\n";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case nativeSH_mono16:
|
|
||||||
case nativeSH_stereo16:
|
|
||||||
{
|
|
||||||
int nChannels = sh.encoding == nativeSH_mono16 ? 1 : 2;
|
|
||||||
char* here = sndhdr + f.Tell();
|
|
||||||
auto span = std::span(here, sh.nativeSH_nBytes);
|
|
||||||
impl.source.Init(sampleRate, 16, nChannels, false, span);
|
|
||||||
LOG_NOPREFIX << "nativeSH\n";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 0xFF: // extSH - extended sound header - IM:S:2-106
|
|
||||||
{
|
|
||||||
// fields that follow baseFrequency
|
|
||||||
SInt32 nFrames = f.Read<SInt32>();
|
|
||||||
f.Skip(22);
|
|
||||||
SInt16 bitDepth = f.Read<SInt16>();
|
|
||||||
f.Skip(14);
|
|
||||||
|
|
||||||
int nBytes = sh.extSH_nChannels * nFrames * bitDepth / 8;
|
|
||||||
|
|
||||||
// noncompressed sample data (big endian) from this point on
|
|
||||||
char* here = sndhdr + f.Tell();
|
|
||||||
|
|
||||||
LOG_NOPREFIX << "extSH: " << bitDepth << "-bit " << (sh.extSH_nChannels == 1? "mono": "stereo") << ", " << nFrames << " frames\n";
|
|
||||||
|
|
||||||
impl.source.Init(sampleRate, bitDepth, sh.extSH_nChannels, true, std::span<char>(here, nBytes));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 0xFE: // cmpSH - compressed sound header - IM:S:2-108
|
|
||||||
{
|
|
||||||
// fields that follow baseFrequency
|
|
||||||
SInt32 nCompressedChunks = f.Read<SInt32>();
|
|
||||||
f.Skip(14);
|
|
||||||
OSType format = f.Read<OSType>();
|
|
||||||
f.Skip(20);
|
|
||||||
|
|
||||||
if (format == 0)
|
|
||||||
{
|
|
||||||
// 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.
|
|
||||||
format = 'MAC3';
|
|
||||||
}
|
|
||||||
|
|
||||||
// compressed sample data from this point on
|
|
||||||
char* here = sndhdr + f.Tell();
|
|
||||||
|
|
||||||
std::cout << "cmpSH: " << Pomme::FourCCString(format) << " " << (sh.cmpSH_nChannels == 1 ? "mono" : "stereo") << ", " << nCompressedChunks << " ck\n";
|
|
||||||
|
|
||||||
std::unique_ptr<Pomme::Sound::Codec> codec = Pomme::Sound::GetCodec(format);
|
|
||||||
|
|
||||||
// Decompress
|
|
||||||
int nBytesIn = sh.cmpSH_nChannels * nCompressedChunks * codec->BytesPerPacket();
|
|
||||||
int nBytesOut = sh.cmpSH_nChannels * nCompressedChunks * codec->SamplesPerPacket() * 2;
|
|
||||||
|
|
||||||
auto spanIn = std::span(here, nBytesIn);
|
|
||||||
auto spanOut = impl.source.GetBuffer(nBytesOut);
|
|
||||||
|
|
||||||
codec->Decode(sh.cmpSH_nChannels, spanIn, spanOut);
|
|
||||||
impl.source.Init(sampleRate, 16, sh.cmpSH_nChannels, false, spanOut);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
TODOFATAL2("unsupported snd header encoding " << (int)sh.encoding);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sh.loopEnd - sh.loopStart <= 1)
|
|
||||||
{
|
|
||||||
// don't loop
|
|
||||||
}
|
|
||||||
else if (sh.loopStart == 0)
|
|
||||||
{
|
|
||||||
impl.source.SetLoop(true);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
TODO2("looping on a portion of the snd isn't supported yet");
|
impl.source.Init(info.sampleRate, info.codecBitDepth, info.nChannels, info.bigEndian, spanIn);
|
||||||
impl.source.SetLoop(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//---------------------------------
|
||||||
|
// Base note
|
||||||
|
|
||||||
|
impl.baseNote = info.baseNote;
|
||||||
|
|
||||||
|
//---------------------------------
|
||||||
|
// Loop
|
||||||
|
|
||||||
|
if (info.loopEnd - info.loopStart >= 2)
|
||||||
|
{
|
||||||
|
impl.source.SetLoop(true);
|
||||||
|
|
||||||
|
if (info.loopStart != 0)
|
||||||
|
TODO2("Warning: looping on a portion of the snd isn't supported yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
//---------------------------------
|
||||||
// Pass Mac channel parameters to cmixer source.
|
// Pass Mac channel parameters to cmixer source.
|
||||||
|
|
||||||
// The loop param is a special case -- we're detecting it automatically according
|
// The loop param is a special case -- we're detecting it automatically according
|
||||||
// to the sound header. If your application needs to force set the loop, it must
|
// to the sound header. If your application needs to force set the loop, it must
|
||||||
// issue pommeSetLoopCmd *after* bufferCmd/soundCmd.
|
// issue pommeSetLoopCmd *after* bufferCmd/soundCmd.
|
||||||
@ -544,7 +597,7 @@ OSErr SndDoImmediate(SndChannelPtr chan, const SndCommand* cmd)
|
|||||||
|
|
||||||
case bufferCmd:
|
case bufferCmd:
|
||||||
case soundCmd:
|
case soundCmd:
|
||||||
ProcessSoundCmd(chan, cmd->ptr);
|
InstallSoundInChannel(chan, cmd->ptr);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ampCmd:
|
case ampCmd:
|
||||||
@ -601,6 +654,7 @@ OSErr SndDoImmediate(SndChannelPtr chan, const SndCommand* cmd)
|
|||||||
return noErr;
|
return noErr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Not implemented yet, but you can probably use SndDoImmediateInstead.
|
||||||
OSErr SndDoCommand(SndChannelPtr chan, const SndCommand* cmd, Boolean noWait)
|
OSErr SndDoCommand(SndChannelPtr chan, const SndCommand* cmd, Boolean noWait)
|
||||||
{
|
{
|
||||||
TODOMINOR2("SndDoCommand isn't implemented yet, but you can probably use SndDoImmediate instead.");
|
TODOMINOR2("SndDoCommand isn't implemented yet, but you can probably use SndDoImmediate instead.");
|
||||||
@ -757,52 +811,38 @@ NumVersion SndSoundManagerVersion()
|
|||||||
|
|
||||||
Boolean Pomme_DecompressSoundResource(SndListHandle* sndHandlePtr, long* offsetToHeader)
|
Boolean Pomme_DecompressSoundResource(SndListHandle* sndHandlePtr, long* offsetToHeader)
|
||||||
{
|
{
|
||||||
// Prep the BE reader on the header.
|
SampledSoundInfo info;
|
||||||
Ptr sndhdr = (Ptr) (**sndHandlePtr) + *offsetToHeader;
|
GetSoundInfoFromSndResource((Handle) *sndHandlePtr, info);
|
||||||
memstream headerInput(sndhdr, kSampledSoundHeaderLength + 42);
|
|
||||||
Pomme::BigEndianIStream f(headerInput);
|
|
||||||
|
|
||||||
// Read in SampledSoundHeader and unpack it.
|
|
||||||
SampledSoundHeader sh;
|
|
||||||
f.Read(reinterpret_cast<char*>(&sh), kSampledSoundHeaderLength);
|
|
||||||
ByteswapStructs(kSampledSoundHeaderPackFormat, kSampledSoundHeaderLength, 1, reinterpret_cast<char*>(&sh));
|
|
||||||
|
|
||||||
// We only handle cmpSH (compressed) 'snd ' resources.
|
// We only handle cmpSH (compressed) 'snd ' resources.
|
||||||
if (sh.encoding != cmpSH)
|
if (!info.isCompressed)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fields that follow SampledSoundHeader when the encoding is cmpSH.
|
|
||||||
const auto nCompressedChunks = f.Read<SInt32>();
|
|
||||||
f.Skip(14);
|
|
||||||
const auto format = f.Read<OSType>();
|
|
||||||
f.Skip(20);
|
|
||||||
|
|
||||||
// Compressed sample data in the input stream from this point on.
|
|
||||||
|
|
||||||
const char* here = sndhdr + f.Tell();
|
|
||||||
|
|
||||||
int outInitialSize = kSampledSoundCommandListLength + kSampledSoundHeaderLength;
|
int outInitialSize = kSampledSoundCommandListLength + kSampledSoundHeaderLength;
|
||||||
|
|
||||||
std::unique_ptr<Pomme::Sound::Codec> codec = Pomme::Sound::GetCodec(format);
|
std::unique_ptr<Pomme::Sound::Codec> codec = Pomme::Sound::GetCodec(info.compressionType);
|
||||||
|
|
||||||
// Decompress
|
// Decompress
|
||||||
const int nBytesIn = sh.cmpSH_nChannels * nCompressedChunks * codec->BytesPerPacket();
|
SndListHandle outHandle = (SndListHandle) NewHandle(outInitialSize + info.decompressedLength);
|
||||||
const int nBytesOut = sh.cmpSH_nChannels * nCompressedChunks * codec->SamplesPerPacket() * 2;
|
auto spanIn = std::span(info.dataStart, info.compressedLength);
|
||||||
SndListHandle outHandle = (SndListHandle) NewHandle(outInitialSize + nBytesOut);
|
auto spanOut = std::span((char*) *outHandle + outInitialSize, info.decompressedLength);
|
||||||
auto spanIn = std::span(here, nBytesIn);
|
codec->Decode(info.nChannels, spanIn, spanOut);
|
||||||
auto spanOut = std::span((char*) *outHandle + outInitialSize, nBytesOut);
|
|
||||||
codec->Decode(sh.cmpSH_nChannels, spanIn, spanOut);
|
|
||||||
|
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
// Now we have the PCM data.
|
// Now we have the PCM data.
|
||||||
// Put the output 'snd ' resource together.
|
// Put the output 'snd ' resource together.
|
||||||
|
|
||||||
SampledSoundHeader shOut = sh;
|
SampledSoundHeader shOut = {};
|
||||||
shOut.zero = 0;
|
shOut.zero = 0;
|
||||||
shOut.encoding = sh.cmpSH_nChannels == 2 ? nativeSH_stereo16 : nativeSH_mono16;
|
shOut.nativeSH_nBytes = info.decompressedLength;
|
||||||
shOut.nativeSH_nBytes = nBytesOut;
|
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));
|
ByteswapStructs(kSampledSoundHeaderPackFormat, kSampledSoundHeaderLength, 1, reinterpret_cast<char*>(&shOut));
|
||||||
|
|
||||||
memcpy(*outHandle, kSampledSoundCommandList, kSampledSoundCommandListLength);
|
memcpy(*outHandle, kSampledSoundCommandList, kSampledSoundCommandListLength);
|
||||||
@ -878,4 +918,140 @@ std::unique_ptr<Pomme::Sound::Codec> Pomme::Sound::GetCodec(uint32_t fourCC)
|
|||||||
default:
|
default:
|
||||||
throw std::runtime_error("Unknown audio codec: " + Pomme::FourCCString(fourCC));
|
throw std::runtime_error("Unknown audio codec: " + Pomme::FourCCString(fourCC));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// Dump 'snd ' resource to AIFF
|
||||||
|
|
||||||
|
void Pomme::Sound::DumpSoundResourceToAIFF(Handle sndHandle, std::ostream& output, const std::string& resourceName)
|
||||||
|
{
|
||||||
|
class AIFFChunkGuard
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AIFFChunkGuard(Pomme::BigEndianOStream& theOutput, uint32_t chunkID)
|
||||||
|
: output(theOutput)
|
||||||
|
{
|
||||||
|
output.Write<uint32_t>(chunkID);
|
||||||
|
lengthFieldPosition = output.Tell();
|
||||||
|
output.Write<uint32_t>('#LEN'); // placeholder
|
||||||
|
}
|
||||||
|
|
||||||
|
~AIFFChunkGuard()
|
||||||
|
{
|
||||||
|
std::streampos endOfChunk = output.Tell();
|
||||||
|
std::streamoff chunkLength = endOfChunk - lengthFieldPosition - static_cast<std::streamoff>(4);
|
||||||
|
|
||||||
|
// Add zero pad byte if chunk length is odd
|
||||||
|
if (0 != (chunkLength & 1))
|
||||||
|
{
|
||||||
|
output.Write<uint8_t>(0);
|
||||||
|
endOfChunk += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
output.Goto(lengthFieldPosition);
|
||||||
|
output.Write<int32_t>(chunkLength);
|
||||||
|
output.Goto(endOfChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Pomme::BigEndianOStream& output;
|
||||||
|
std::streampos lengthFieldPosition;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
SampledSoundInfo info;
|
||||||
|
GetSoundInfoFromSndResource(sndHandle, info);
|
||||||
|
|
||||||
|
char sampleRate80bit[10];
|
||||||
|
ConvertToIeeeExtended(info.sampleRate, sampleRate80bit);
|
||||||
|
|
||||||
|
Pomme::BigEndianOStream of(output);
|
||||||
|
|
||||||
|
bool hasLoop = info.loopEnd - info.loopStart > 1;
|
||||||
|
|
||||||
|
{
|
||||||
|
AIFFChunkGuard form(of, 'FORM');
|
||||||
|
of.Write<uint32_t>('AIFC');
|
||||||
|
|
||||||
|
{
|
||||||
|
AIFFChunkGuard chunk(of, 'FVER');
|
||||||
|
of.Write<uint32_t>(0xA2805140u);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
AIFFChunkGuard chunk(of, 'COMM');
|
||||||
|
of.Write<int16_t>(info.nChannels);
|
||||||
|
of.Write<uint32_t>(info.nPackets);
|
||||||
|
of.Write<int16_t>(info.codecBitDepth);
|
||||||
|
of.Write(sampleRate80bit, sizeof(sampleRate80bit));
|
||||||
|
of.Write<uint32_t>(info.compressionType);
|
||||||
|
|
||||||
|
std::string compressionName;
|
||||||
|
switch (info.compressionType)
|
||||||
|
{
|
||||||
|
case 'MAC3': compressionName = "MACE 3-to-1"; break;
|
||||||
|
case 'ima4': compressionName = "IMA 16 bit 4-to-1"; break;
|
||||||
|
case 'NONE': compressionName = "Signed PCM"; break;
|
||||||
|
case 'twos': compressionName = "Signed big-endian PCM"; break;
|
||||||
|
case 'sowt': compressionName = "Signed little-endian PCM"; break;
|
||||||
|
case 'raw ': compressionName = "Unsigned PCM"; break;
|
||||||
|
case 'ulaw': compressionName = "mu-law"; break;
|
||||||
|
case 'alaw': compressionName = "A-law"; break;
|
||||||
|
default: compressionName = "";
|
||||||
|
}
|
||||||
|
of.WritePascalString(compressionName, 2); // human-readable compression type pascal string
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasLoop)
|
||||||
|
{
|
||||||
|
AIFFChunkGuard chunk(of, 'MARK');
|
||||||
|
of.Write<int16_t>(2); // 2 markers
|
||||||
|
of.Write<int16_t>(101); // marker ID
|
||||||
|
of.Write<uint32_t>(info.loopStart);
|
||||||
|
of.WritePascalString("beg loop", 2);
|
||||||
|
of.Write<int16_t>(102); // marker ID
|
||||||
|
of.Write<uint32_t>(info.loopEnd);
|
||||||
|
of.WritePascalString("end loop", 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.baseNote != kMiddleC || hasLoop)
|
||||||
|
{
|
||||||
|
AIFFChunkGuard chunk(of, 'INST');
|
||||||
|
of.Write<int8_t>(info.baseNote);
|
||||||
|
of.Write<int8_t>(0); // detune
|
||||||
|
of.Write<int8_t>(0x00); // lowNote
|
||||||
|
of.Write<int8_t>(0x7F); // highNote
|
||||||
|
of.Write<int8_t>(0x00); // lowVelocity
|
||||||
|
of.Write<int8_t>(0x7F); // highVelocity
|
||||||
|
of.Write<int16_t>(0); // gain
|
||||||
|
of.Write<int16_t>(hasLoop? 1: 0); // sustainLoop.playMode
|
||||||
|
of.Write<int16_t>(hasLoop? 101: 0); // sustainLoop.beginLoop
|
||||||
|
of.Write<int16_t>(hasLoop? 102: 0); // sustainLoop.endLoop
|
||||||
|
of.Write<int16_t>(0);
|
||||||
|
of.Write<int16_t>(0);
|
||||||
|
of.Write<int16_t>(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resourceName.empty())
|
||||||
|
{
|
||||||
|
AIFFChunkGuard chunk(of, 'NAME');
|
||||||
|
of.WriteRawString(resourceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
AIFFChunkGuard chunk(of, 'ANNO');
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "Verbatim copy of data stream from 'snd ' resource.\n"
|
||||||
|
<< "MIDI base note: " << int(info.baseNote)
|
||||||
|
<< ", sustain loop: " << info.loopStart << "-" << info.loopEnd;
|
||||||
|
of.WriteRawString(ss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
AIFFChunkGuard chunk(of, 'SSND');
|
||||||
|
of.Write<int32_t>(0); // offset; don't care
|
||||||
|
of.Write<int32_t>(0); // blockSize; don't care
|
||||||
|
of.Write(info.dataStart, info.compressedLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user