2020-11-11 20:06:52 +00:00
|
|
|
#include "Pomme.h"
|
|
|
|
#include "PommeFiles.h"
|
|
|
|
#include "PommeSound.h"
|
2021-08-15 10:42:31 +00:00
|
|
|
#include "SoundMixer/ChannelImpl.h"
|
|
|
|
#include "SoundMixer/cmixer.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)
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// Internal channel info
|
|
|
|
|
2021-08-15 10:42:31 +00:00
|
|
|
namespace Pomme::Sound
|
2020-11-11 20:06:52 +00:00
|
|
|
{
|
2021-08-15 10:42:31 +00:00
|
|
|
struct ChannelImpl* gHeadChan = nullptr;
|
|
|
|
int gNumManagedChans = 0;
|
|
|
|
}
|
2020-11-11 20:06:52 +00:00
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// Internal utilities
|
|
|
|
|
2021-07-25 15:22:10 +00:00
|
|
|
static inline ChannelImpl& GetChannelImpl(SndChannelPtr chan)
|
2020-11-11 20:06:52 +00:00
|
|
|
{
|
2021-02-28 22:39:54 +00:00
|
|
|
return *(ChannelImpl*) chan->channelImpl;
|
2020-11-11 20:06:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// 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);
|
|
|
|
|
2021-03-17 19:29:58 +00:00
|
|
|
channelImpl->SetInitializationParameters(init);
|
2020-11-11 20:06:52 +00:00
|
|
|
|
|
|
|
//---------------------------
|
|
|
|
// Done
|
|
|
|
|
2021-08-15 10:42:31 +00:00
|
|
|
LOG << "New channel created, init = $" << std::hex << init << std::dec
|
|
|
|
<< ", total managed channels = " << Pomme::Sound::gNumManagedChans << "\n";
|
2020-11-11 20:06:52 +00:00
|
|
|
|
|
|
|
return noErr;
|
|
|
|
}
|
|
|
|
|
|
|
|
// IM:S:2-129
|
|
|
|
OSErr SndDisposeChannel(SndChannelPtr macChanPtr, Boolean quietNow)
|
|
|
|
{
|
|
|
|
if (!quietNow)
|
|
|
|
{
|
|
|
|
TODO2("SndDisposeChannel: quietNow == false is not implemented");
|
|
|
|
}
|
2021-07-25 15:22:10 +00:00
|
|
|
delete &GetChannelImpl(macChanPtr);
|
2020-11-11 20:06:52 +00:00
|
|
|
return noErr;
|
|
|
|
}
|
|
|
|
|
|
|
|
OSErr SndChannelStatus(SndChannelPtr chan, short theLength, SCStatusPtr theStatus)
|
|
|
|
{
|
|
|
|
*theStatus = {};
|
|
|
|
|
2021-07-25 15:22:10 +00:00
|
|
|
auto& source = GetChannelImpl(chan).source;
|
2020-11-11 20:06:52 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
2021-07-25 15:22:10 +00:00
|
|
|
auto& impl = GetChannelImpl(chan);
|
2020-11-11 20:06:52 +00:00
|
|
|
impl.Recycle();
|
|
|
|
|
2021-05-20 06:19:02 +00:00
|
|
|
//---------------------------------
|
|
|
|
// Distill sound info
|
2020-11-11 20:06:52 +00:00
|
|
|
|
2021-07-18 11:28:27 +00:00
|
|
|
Pomme::Sound::SampledSoundInfo info;
|
2021-05-20 06:19:02 +00:00
|
|
|
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
|
|
|
//---------------------------------
|
2021-02-28 22:39:54 +00:00
|
|
|
// Pass Mac channel parameters to cmixer source.
|
2021-05-20 06:19:02 +00:00
|
|
|
|
2021-02-28 22:39:54 +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;
|
2021-02-28 22:39:54 +00:00
|
|
|
|
|
|
|
// Get it going!
|
2020-11-11 20:06:52 +00:00
|
|
|
impl.source.Play();
|
|
|
|
}
|
|
|
|
|
|
|
|
OSErr SndDoImmediate(SndChannelPtr chan, const SndCommand* cmd)
|
|
|
|
{
|
2021-07-25 15:22:10 +00:00
|
|
|
auto& impl = GetChannelImpl(chan);
|
2020-11-11 20:06:52 +00:00
|
|
|
|
|
|
|
// 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);
|
2021-08-15 11:22:25 +00:00
|
|
|
GetChannelImpl(chan).source.Play();
|
2020-11-11 20:06:52 +00:00
|
|
|
break;
|
|
|
|
|
|
|
|
case ampCmd:
|
2021-02-28 22:39:54 +00:00
|
|
|
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;
|
2021-07-18 11:01:17 +00:00
|
|
|
uint32_t volsum = lvol + rvol;
|
2020-11-21 13:22:58 +00:00
|
|
|
|
2021-07-18 11:01:17 +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
|
|
|
|
2021-02-28 22:39:54 +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:
|
2021-08-15 10:42:31 +00:00
|
|
|
LOG << "freqCmd " << cmd->param2 << " "
|
|
|
|
<< Pomme::Sound::GetMidiNoteName(cmd->param2) << " " << Pomme::Sound::GetMidiNoteFrequency(cmd->param2) << "\n";
|
2020-11-11 20:06:52 +00:00
|
|
|
impl.playbackNote = Byte(cmd->param2);
|
2021-02-28 22:39:54 +00:00
|
|
|
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;
|
2021-02-28 22:39:54 +00:00
|
|
|
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;
|
2021-02-28 22:39:54 +00:00
|
|
|
impl.ApplyParametersToSource(kApplyParameters_Pitch);
|
2021-01-11 21:07:53 +00:00
|
|
|
break;
|
|
|
|
|
2021-03-17 19:29:58 +00:00
|
|
|
case reInitCmd:
|
|
|
|
impl.SetInitializationParameters(cmd->param2);
|
|
|
|
break;
|
|
|
|
|
2020-11-11 20:06:52 +00:00
|
|
|
case pommeSetLoopCmd:
|
2021-02-28 22:39:54 +00:00
|
|
|
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.
|
2020-11-16 06:05:38 +00:00
|
|
|
OSErr SndDoCommand(SndChannelPtr chan, const SndCommand* cmd, Boolean noWait)
|
|
|
|
{
|
2021-02-28 22:39:54 +00:00
|
|
|
TODOMINOR2("SndDoCommand isn't implemented yet, but you can probably use SndDoImmediate instead.");
|
2020-11-16 06:05:38 +00:00
|
|
|
return noErr;
|
|
|
|
}
|
|
|
|
|
2020-11-11 20:06:52 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2021-08-15 11:22:25 +00:00
|
|
|
SndListHandle sndListHandle = Pomme_SndLoadFileAsResource(fRefNum);
|
|
|
|
long offset = 0;
|
|
|
|
GetSoundHeaderOffset(sndListHandle, &offset);
|
|
|
|
InstallSoundInChannel(chan, ((Ptr) *sndListHandle) + offset);
|
2020-11-11 20:06:52 +00:00
|
|
|
|
2021-08-15 11:22:25 +00:00
|
|
|
auto& impl = GetChannelImpl(chan);
|
2020-11-11 20:06:52 +00:00
|
|
|
if (theCompletion)
|
|
|
|
{
|
|
|
|
impl.source.onComplete = [=]() { theCompletion(chan); };
|
|
|
|
}
|
|
|
|
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
|
2021-07-25 15:22:10 +00:00
|
|
|
GetChannelImpl(chan).source.TogglePause();
|
2020-11-11 20:06:52 +00:00
|
|
|
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");
|
2021-07-25 15:22:10 +00:00
|
|
|
GetChannelImpl(chan).source.Stop();
|
2020-11-11 20:06:52 +00:00
|
|
|
return noErr;
|
|
|
|
}
|
|
|
|
|
|
|
|
NumVersion SndSoundManagerVersion()
|
|
|
|
{
|
|
|
|
NumVersion v = {};
|
|
|
|
v.majorRev = 3;
|
|
|
|
v.minorAndBugRev = 9;
|
|
|
|
v.stage = 0x80;
|
|
|
|
v.nonRelRev = 0;
|
|
|
|
return v;
|
|
|
|
}
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
2020-11-25 07:16:58 +00:00
|
|
|
// Extension: pause/unpause channels that are currently playing
|
2020-11-11 20:06:52 +00:00
|
|
|
|
2020-11-25 07:16:58 +00:00
|
|
|
void Pomme_PauseAllChannels(Boolean pause)
|
2020-11-11 20:06:52 +00:00
|
|
|
{
|
2021-08-15 10:42:31 +00:00
|
|
|
for (auto* chan = Pomme::Sound::gHeadChan; chan; chan = chan->GetNext())
|
2020-11-11 20:06:52 +00:00
|
|
|
{
|
|
|
|
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
|
|
|
|
|
2021-08-15 10:42:31 +00:00
|
|
|
void Pomme::Sound::InitMixer()
|
2020-11-11 20:06:52 +00:00
|
|
|
{
|
|
|
|
cmixer::InitWithSDL();
|
|
|
|
}
|
|
|
|
|
2021-08-15 10:42:31 +00:00
|
|
|
void Pomme::Sound::ShutdownMixer()
|
2020-11-11 20:06:52 +00:00
|
|
|
{
|
|
|
|
cmixer::ShutdownWithSDL();
|
2021-08-15 10:42:31 +00:00
|
|
|
while (Pomme::Sound::gHeadChan)
|
2020-11-11 20:06:52 +00:00
|
|
|
{
|
2021-08-15 10:42:31 +00:00
|
|
|
SndDisposeChannel(Pomme::Sound::gHeadChan->macChannel, true);
|
2020-11-11 20:06:52 +00:00
|
|
|
}
|
|
|
|
}
|