Pomme/src/Sound/SoundManager.cpp

927 lines
23 KiB
C++
Raw Normal View History

2020-11-11 20:06:52 +00:00
#include "Pomme.h"
#include "PommeFiles.h"
#include "Sound/cmixer.h"
#include "PommeSound.h"
2021-05-19 19:15:35 +00:00
#include "Utilities/bigendianstreams.h"
2021-05-20 06:19:02 +00:00
#include "Utilities/IEEEExtended.h"
2020-11-11 20:06:52 +00:00
#include "Utilities/memstream.h"
#include <thread>
#include <chrono>
#include <iostream>
#include <cassert>
#include <cstring>
#define LOG POMME_GENLOG(POMME_DEBUG_SOUND, "SOUN")
#define LOG_NOPREFIX POMME_GENLOG_NOPREFIX(POMME_DEBUG_SOUND)
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;
};
2021-05-20 06:19:02 +00:00
UnsignedFixed fixedSampleRate;
2020-11-11 20:06:52 +00:00
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,
};
2021-05-20 06:19:02 +00:00
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;
};
2020-11-11 20:06:52 +00:00
//-----------------------------------------------------------------------------
// Internal channel info
enum ApplyParametersMask
{
kApplyParameters_PanAndGain = 1 << 0,
kApplyParameters_Pitch = 1 << 1,
kApplyParameters_Loop = 1 << 2,
2021-02-28 22:42:36 +00:00
kApplyParameters_Interpolation = 1 << 3,
kApplyParameters_All = 0xFFFFFFFF
};
2020-11-11 20:06:52 +00:00
struct ChannelImpl
{
private:
ChannelImpl* prev;
ChannelImpl* next;
public:
// Pointer to application-facing interface
2020-11-11 20:06:52 +00:00
SndChannelPtr macChannel;
bool macChannelStructAllocatedByPomme;
cmixer::WavStream source;
// Parameters coming from Mac sound commands, passed back to cmixer source
double pan;
double gain;
Byte baseNote;
Byte playbackNote;
double pitchMult;
bool loop;
2021-02-28 22:42:36 +00:00
bool interpolate;
2020-11-11 20:06:52 +00:00
bool temporaryPause = false;
ChannelImpl(SndChannelPtr _macChannel, bool transferMacChannelOwnership)
: macChannel(_macChannel)
, macChannelStructAllocatedByPomme(transferMacChannelOwnership)
, source()
, pan(0.0)
, gain(1.0)
2020-11-11 20:06:52 +00:00
, baseNote(kMiddleC)
, playbackNote(kMiddleC)
, pitchMult(1.0)
, loop(false)
2021-02-28 22:42:36 +00:00
, interpolate(false)
2020-11-11 20:06:52 +00:00
{
macChannel->channelImpl = (Ptr) this;
2020-11-11 20:06:52 +00:00
Link(); // Link chan into our list of managed chans
}
~ChannelImpl()
{
Unlink();
macChannel->channelImpl = nullptr;
2020-11-11 20:06:52 +00:00
if (macChannelStructAllocatedByPomme)
{
delete macChannel;
}
}
void Recycle()
{
source.Clear();
}
void SetInitializationParameters(long initBits)
{
interpolate = !(initBits & initNoInterp);
source.SetInterpolation(interpolate);
}
void ApplyParametersToSource(uint32_t mask, bool evenIfInactive = false)
2020-11-11 20:06:52 +00:00
{
if (!evenIfInactive && !source.active)
2020-11-11 20:06:52 +00:00
{
return;
}
// Pitch
if (mask & kApplyParameters_Pitch)
{
double baseFreq = midiNoteFrequencies[baseNote];
double playbackFreq = midiNoteFrequencies[playbackNote];
source.SetPitch(pitchMult * playbackFreq / baseFreq);
}
// Pan and gain
if (mask & kApplyParameters_PanAndGain)
{
source.SetPan(pan);
source.SetGain(gain);
}
// Interpolation
if (mask & kApplyParameters_Interpolation)
{
source.SetInterpolation(interpolate);
}
2021-02-28 22:42:36 +00:00
// Interpolation
if (mask & kApplyParameters_Loop)
{
source.SetLoop(loop);
}
2020-11-11 20:06:52 +00:00
}
ChannelImpl* GetPrev() const
{
return prev;
}
ChannelImpl* GetNext() const
{
return next;
}
void SetPrev(ChannelImpl* newPrev)
{
prev = newPrev;
}
void SetNext(ChannelImpl* newNext)
{
next = newNext;
macChannel->nextChan = newNext ? newNext->macChannel : nullptr;
}
void Link()
{
if (!headChan)
{
SetNext(nullptr);
}
else
{
assert(nullptr == headChan->GetPrev());
headChan->SetPrev(this);
SetNext(headChan);
}
headChan = this;
SetPrev(nullptr);
nManagedChans++;
}
void Unlink()
{
if (headChan == this)
{
headChan = GetNext();
}
if (nullptr != GetPrev())
{
GetPrev()->SetNext(GetNext());
}
if (nullptr != GetNext())
{
GetNext()->SetPrev(GetPrev());
}
SetPrev(nullptr);
SetNext(nullptr);
nManagedChans--;
}
};
//-----------------------------------------------------------------------------
// Internal utilities
static inline ChannelImpl& GetImpl(SndChannelPtr chan)
{
return *(ChannelImpl*) chan->channelImpl;
2020-11-11 20:06:52 +00:00
}
2021-05-20 06:19:02 +00:00
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);
}
2020-11-11 20:06:52 +00:00
//-----------------------------------------------------------------------------
// MIDI note utilities
// Note: these names are according to IM:S:2-43.
// These names won't match real-world names.
// E.g. for note 67 (A 440Hz), this will return "A6", whereas the real-world
// convention for that note is "A4".
static std::string GetMidiNoteName(int i)
{
static const char* gamme[12] = {"A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#"};
int octave = 1 + (i + 3) / 12;
int semitonesFromA = (i + 3) % 12;
std::stringstream ss;
ss << gamme[semitonesFromA] << octave;
return ss.str();
}
static void InitMidiFrequencyTable()
{
// powers of twelfth root of two
double gamme[12];
gamme[0] = 1.0;
for (int i = 1; i < 12; i++)
{
gamme[i] = gamme[i - 1] * 1.059630943592952646;
}
for (int i = 0; i < 128; i++)
{
int octave = 1 + (i + 3) / 12; // A440 and middle C are in octave 7
int semitone = (i + 3) % 12; // halfsteps up from A in current octave
if (octave < 7)
midiNoteFrequencies[i] = gamme[semitone] * 440.0 / (1 << (7 - octave)); // 440/(2**octaveDiff)
else
midiNoteFrequencies[i] = gamme[semitone] * 440.0 * (1 << (octave - 7)); // 440*(2**octaveDiff)
//LOG << i << "\t" << GetMidiNoteName(i) << "\t" << midiNoteFrequencies[i] << "\n";
}
}
//-----------------------------------------------------------------------------
// Sound Manager
OSErr GetDefaultOutputVolume(long* stereoLevel)
{
unsigned short g = (unsigned short) (cmixer::GetMasterGain() * 256.0);
*stereoLevel = (g << 16) | g;
return noErr;
}
// See IM:S:2-139, "Controlling Volume Levels".
OSErr SetDefaultOutputVolume(long stereoLevel)
{
unsigned short left = 0xFFFF & stereoLevel;
unsigned short right = 0xFFFF & (stereoLevel >> 16);
if (right != left)
TODOMINOR2("setting different volumes for left & right is not implemented");
LOG << left / 256.0 << "\n";
cmixer::SetMasterGain(left / 256.0);
return noErr;
}
// IM:S:2-127
OSErr SndNewChannel(SndChannelPtr* macChanPtr, short synth, long init, SndCallBackProcPtr userRoutine)
{
if (synth != sampledSynth)
{
TODO2("unimplemented synth type " << sampledSynth);
return unimpErr;
}
//---------------------------
// Allocate Mac channel record if needed
bool transferMacChannelOwnership = false;
if (!*macChanPtr)
{
*macChanPtr = new SndChannel;
(**macChanPtr) = {};
transferMacChannelOwnership = true;
}
//---------------------------
// Set up
(**macChanPtr).callBack = userRoutine;
2021-02-28 22:42:36 +00:00
auto channelImpl = new ChannelImpl(*macChanPtr, transferMacChannelOwnership);
channelImpl->SetInitializationParameters(init);
2020-11-11 20:06:52 +00:00
//---------------------------
// Done
LOG << "New channel created, init = $" << std::hex << init << std::dec << ", total managed channels = " << nManagedChans << "\n";
return noErr;
}
// IM:S:2-129
OSErr SndDisposeChannel(SndChannelPtr macChanPtr, Boolean quietNow)
{
if (!quietNow)
{
TODO2("SndDisposeChannel: quietNow == false is not implemented");
}
delete &GetImpl(macChanPtr);
return noErr;
}
OSErr SndChannelStatus(SndChannelPtr chan, short theLength, SCStatusPtr theStatus)
{
*theStatus = {};
auto& source = GetImpl(chan).source;
theStatus->scChannelPaused = source.GetState() == cmixer::CM_STATE_PAUSED;
theStatus->scChannelBusy = source.GetState() == cmixer::CM_STATE_PLAYING;
return noErr;
}
2021-05-20 06:19:02 +00:00
// Install a sampled sound as a voice in a channel.
static void InstallSoundInChannel(SndChannelPtr chan, const Ptr sampledSoundHeader)
2020-11-11 20:06:52 +00:00
{
2021-05-20 06:19:02 +00:00
//---------------------------------
// Get internal channel
2020-11-11 20:06:52 +00:00
auto& impl = GetImpl(chan);
impl.Recycle();
2021-05-20 06:19:02 +00:00
//---------------------------------
// Distill sound info
2020-11-11 20:06:52 +00:00
2021-05-20 06:19:02 +00:00
SampledSoundInfo info;
GetSoundInfo(sampledSoundHeader, info);
2020-11-11 20:06:52 +00:00
2021-05-20 06:19:02 +00:00
//---------------------------------
// Set cmixer source data
2020-11-11 20:06:52 +00:00
2021-05-20 06:19:02 +00:00
auto spanIn = std::span(info.dataStart, info.compressedLength);
2020-11-11 20:06:52 +00:00
2021-05-20 06:19:02 +00:00
if (info.isCompressed)
2020-11-11 20:06:52 +00:00
{
2021-05-20 06:19:02 +00:00
auto spanOut = impl.source.GetBuffer(info.decompressedLength);
2020-11-11 20:06:52 +00:00
2021-05-20 06:19:02 +00:00
std::unique_ptr<Pomme::Sound::Codec> codec = Pomme::Sound::GetCodec(info.compressionType);
codec->Decode(info.nChannels, spanIn, spanOut);
impl.source.Init(info.sampleRate, 16, info.nChannels, false, spanOut);
2020-11-11 20:06:52 +00:00
}
2021-05-20 06:19:02 +00:00
else
2020-11-11 20:06:52 +00:00
{
2021-05-20 06:19:02 +00:00
impl.source.Init(info.sampleRate, info.codecBitDepth, info.nChannels, info.bigEndian, spanIn);
2020-11-11 20:06:52 +00:00
}
2021-05-20 06:19:02 +00:00
//---------------------------------
// Base note
2020-11-11 20:06:52 +00:00
2021-05-20 06:19:02 +00:00
impl.baseNote = info.baseNote;
2020-11-11 20:06:52 +00:00
2021-05-20 06:19:02 +00:00
//---------------------------------
// Loop
2020-11-11 20:06:52 +00:00
2021-05-20 06:19:02 +00:00
if (info.loopEnd - info.loopStart >= 2)
2020-11-11 20:06:52 +00:00
{
impl.source.SetLoop(true);
2021-05-20 06:19:02 +00:00
if (info.loopStart != 0)
TODO2("Warning: looping on a portion of the snd isn't supported yet");
2020-11-11 20:06:52 +00:00
}
2021-05-20 06:19:02 +00:00
//---------------------------------
// Pass Mac channel parameters to cmixer source.
2021-05-20 06:19:02 +00:00
// 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
// issue pommeSetLoopCmd *after* bufferCmd/soundCmd.
impl.ApplyParametersToSource(kApplyParameters_All & ~kApplyParameters_Loop, true);
// Override systemwide audio pause.
2020-11-11 20:06:52 +00:00
impl.temporaryPause = false;
// Get it going!
2020-11-11 20:06:52 +00:00
impl.source.Play();
}
OSErr SndDoImmediate(SndChannelPtr chan, const SndCommand* cmd)
{
auto& impl = GetImpl(chan);
// Discard the high bit of the command (it indicates whether an 'snd ' resource has associated data).
switch (cmd->cmd & 0x7FFF)
{
case nullCmd:
break;
case flushCmd:
// flushCmd is a no-op for now because we don't support queuing commands--
// all commands are executed immediately in the current implementation.
break;
case quietCmd:
impl.source.Stop();
break;
2020-11-21 13:23:28 +00:00
case bufferCmd:
2020-11-11 20:06:52 +00:00
case soundCmd:
2021-05-20 06:19:02 +00:00
InstallSoundInChannel(chan, cmd->ptr);
2020-11-11 20:06:52 +00:00
break;
case ampCmd:
impl.gain = cmd->param1 / 256.0;
impl.ApplyParametersToSource(kApplyParameters_PanAndGain);
2020-11-11 20:06:52 +00:00
break;
2020-11-21 13:22:58 +00:00
case volumeCmd:
{
uint16_t lvol = (cmd->param2 ) & 0xFFFF;
uint16_t rvol = (cmd->param2 >> 16) & 0xFFFF;
uint32_t volsum = lvol + rvol;
2020-11-21 13:22:58 +00:00
double pan = 0;
if (volsum != 0) // don't divide by zero
{
pan = (double)rvol / volsum;
pan = 2*pan - 1; // Transpose pan from [0...1] to [-1...+1]
}
2020-11-21 13:22:58 +00:00
impl.pan = pan;
impl.gain = std::max(lvol, rvol) / 256.0;
impl.ApplyParametersToSource(kApplyParameters_PanAndGain);
2020-11-21 13:22:58 +00:00
break;
}
2020-11-11 20:06:52 +00:00
case freqCmd:
LOG << "freqCmd " << cmd->param2 << " " << GetMidiNoteName(cmd->param2) << " " << midiNoteFrequencies[cmd->param2] << "\n";
impl.playbackNote = Byte(cmd->param2);
impl.ApplyParametersToSource(kApplyParameters_Pitch);
2020-11-11 20:06:52 +00:00
break;
case rateCmd:
// IM:S says it's a fixed-point multiplier of 22KHz, but Nanosaur uses rate "1" everywhere,
// even for sounds sampled at 44Khz, so I'm treating it as just a pitch multiplier.
impl.pitchMult = cmd->param2 / 65536.0;
impl.ApplyParametersToSource(kApplyParameters_Pitch);
2020-11-11 20:06:52 +00:00
break;
2021-01-11 21:07:53 +00:00
case rateMultiplierCmd:
impl.pitchMult = cmd->param2 / 65536.0;
impl.ApplyParametersToSource(kApplyParameters_Pitch);
2021-01-11 21:07:53 +00:00
break;
case reInitCmd:
impl.SetInitializationParameters(cmd->param2);
break;
2020-11-11 20:06:52 +00:00
case pommeSetLoopCmd:
impl.loop = cmd->param1;
impl.ApplyParametersToSource(kApplyParameters_Loop);
2020-11-11 20:06:52 +00:00
break;
default:
TODOMINOR2(cmd->cmd << "(" << cmd->param1 << "," << cmd->param2 << ")");
}
return noErr;
}
2021-05-20 06:19:02 +00:00
// Not implemented yet, but you can probably use SndDoImmediateInstead.
OSErr SndDoCommand(SndChannelPtr chan, const SndCommand* cmd, Boolean noWait)
{
TODOMINOR2("SndDoCommand isn't implemented yet, but you can probably use SndDoImmediate instead.");
return noErr;
}
2020-11-11 20:06:52 +00:00
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;
}
2020-11-11 20:06:52 +00:00
case 2: // HyperCard sampled-sound format
f.Skip(2); // Skip reference count (for application use)
break;
default:
return badFormat;
}
2020-11-11 20:06:52 +00:00
// Now read sound commands
2020-11-11 20:06:52 +00:00
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,
short resNum,
long bufferSize,
Ptr theBuffer,
/*AudioSelectionPtr*/ void* theSelection,
FilePlayCompletionUPP theCompletion,
Boolean async)
{
if (resNum != 0)
{
TODO2("playing snd resource not implemented yet, resource " << resNum);
return unimpErr;
}
if (!chan)
{
if (async) // async requires passing in a channel
return badChannel;
TODO2("nullptr chan for sync play, check IM:S:1-37");
return unimpErr;
}
if (theSelection)
{
TODO2("audio selection record not implemented");
return unimpErr;
}
auto& impl = GetImpl(chan);
impl.Recycle();
auto& stream = 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);
if (theCompletion)
{
impl.source.onComplete = [=]() { theCompletion(chan); };
}
impl.temporaryPause = false;
impl.source.Play();
if (!async)
{
while (impl.source.GetState() != cmixer::CM_STATE_STOPPED)
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
impl.Recycle();
return noErr;
}
return noErr;
}
OSErr SndPauseFilePlay(SndChannelPtr chan)
{
// TODO: check that chan is being used for play from disk
GetImpl(chan).source.TogglePause();
return noErr;
}
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();
return noErr;
}
NumVersion SndSoundManagerVersion()
{
NumVersion v = {};
v.majorRev = 3;
v.minorAndBugRev = 9;
v.stage = 0x80;
v.nonRelRev = 0;
return v;
}
//-----------------------------------------------------------------------------
// Extension: decompress
Boolean Pomme_DecompressSoundResource(SndListHandle* sndHandlePtr, long* offsetToHeader)
{
2021-05-20 06:19:02 +00:00
SampledSoundInfo info;
GetSoundInfoFromSndResource((Handle) *sndHandlePtr, info);
2020-11-11 20:06:52 +00:00
// We only handle cmpSH (compressed) 'snd ' resources.
2021-05-20 06:19:02 +00:00
if (!info.isCompressed)
2020-11-11 20:06:52 +00:00
{
return false;
}
int outInitialSize = kSampledSoundCommandListLength + kSampledSoundHeaderLength;
2021-05-20 06:19:02 +00:00
std::unique_ptr<Pomme::Sound::Codec> codec = Pomme::Sound::GetCodec(info.compressionType);
2020-11-11 20:06:52 +00:00
// Decompress
2021-05-20 06:19:02 +00:00
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);
2020-11-11 20:06:52 +00:00
// ------------------------------------------------------
// Now we have the PCM data.
// Put the output 'snd ' resource together.
2021-05-20 06:19:02 +00:00
SampledSoundHeader shOut = {};
2020-11-11 20:06:52 +00:00
shOut.zero = 0;
2021-05-20 06:19:02 +00:00
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;
2020-11-11 20:06:52 +00:00
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
2020-11-11 20:06:52 +00:00
void Pomme_PauseAllChannels(Boolean pause)
2020-11-11 20:06:52 +00:00
{
for (auto* chan = headChan; chan; chan = chan->GetNext())
{
auto& source = chan->source;
if (pause && source.state == cmixer::CM_STATE_PLAYING && !chan->temporaryPause)
{
source.Pause();
chan->temporaryPause = true;
}
else if (!pause && source.state == cmixer::CM_STATE_PAUSED && chan->temporaryPause)
{
source.Play();
chan->temporaryPause = false;
}
}
}
//-----------------------------------------------------------------------------
// Init Sound Manager
void Pomme::Sound::Init()
{
InitMidiFrequencyTable();
cmixer::InitWithSDL();
}
void Pomme::Sound::Shutdown()
{
cmixer::ShutdownWithSDL();
while (headChan)
{
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));
}
2021-05-20 06:19:02 +00:00
}