SAURON's "coolant flush sound glitched and perma" is one instance of a systemic
defect. SetupPatch (L4AUDLVL.cpp) decided looping from the SAMPLE flag alone:
AL_LOOPING = (info.loop != ForceStatic)
That armed every non-ForceStatic sample -- 224 of the 603 authored zones,
including unmistakable one-shots -- as an OpenAL source that never ends on its
own. Any such sound whose note-off never arrives plays forever. Measured 1946
LoopAtWill x Transient arming events in one short session.
The sample flag expresses PERMISSION; the SOURCE decides. Three facts [T1]:
* the enum's own comments (L4AUDLVL.h:14-19) -- LoopAtWill = "will play once
OR LOOP AS DESIRED", ForceStatic = "plays only once EVEN IF LOOPED" (a
veto), LoopAlways = "ramp up and then down";
* LoopAtWill is enum value 0, i.e. the DEFAULT an unauthored sample receives
(WTPresets.cpp:37) -- it cannot mean "loop forever";
* LoopAlways, the real always-loop, is used by ZERO shipped samples.
"As desired" is the source's authored AudioRenderType (Transient=one-shot vs
Sustained=held), streamed from AUDIO*.RES (AUDLVL.cpp:28) and already consulted
by the renderer (L4AUDRND.cpp:567, AUDREND.cpp:184). SetupPatch now takes it,
threaded from all three L4AudioSource call sites (Direct/Dynamic3D/Static3D).
New rule: LoopAlways->1, ForceStatic->0, LoopAtWill->the source's render type.
MEASURED before changing behaviour (BT_LOOP_AUDIT=1, scratchpad/loopaudit.py):
LoopAtWill x Transient -> loop=0 (was 1) 1946 events
LoopAtWill x Sustained -> loop=1 unchanged 65 events
ForceStatic x Transient -> loop=0 unchanged 292 events
The engine loops (EngineAccel07_z0..z2, EngineMotor01, EnginePower01) are all
LoopAtWill/Sustained and PRESERVED -- no regression there. All 24 reclassified
samples are genuine one-shots: laser charge/fire/explosion/loaded, missile
loading, engine shift, coolant pressure inc/dec.
A/B PROOF (scratchpad/loopab.py, same session both arms, only BT_LOOP_LEGACY
differs), asking the engine's own BT_AUDIO_DUMP what is still playing with
loop=1 long after every release:
[legacy] EnginePower01, CoolantPresInc03_z2, CoolantPresDcr03_z2
[fixed] EnginePower01
The two stuck coolant sources are the reported symptom. They were eventually
reclaimed when a later trigger reused the source, which is why the bug was
intermittent -- the "perma" case is when nothing else retriggers it. The fix
removes the mechanism.
BT_LOOP_LEGACY=1 restores the old rule for a field A/B without a rebuild. The
layers to listen to are the beam sustains: LaserA/CSustain* are authored
LoopAtWill on a TRANSIENT source, so they now end with the sample instead of
looping until note-off. The render type is T1 authored data; the interpretation
that LoopAtWill defers to it is a strong reading of the enum + defaults rather
than a disassembled statement, and is flagged as such in the KB.
Plausibly bears on #32 (audio cutting in/out late in a match): a stuck looping
source holds its pool slot for the rest of the round.
Rigs: scratchpad/flushsnd.py (flush start/stop pairing), flushsnd2.py (stuck-
source hunt), loopaudit.py (the classification matrix), loopab.py (the A/B).
KB: context/wintesla-port.md audio section, context/decomp-reference.md env
table (BT_LOOP_AUDIT / BT_LOOP_LEGACY / BT_AUDIO_DUMP / BT_AUD_TAIL),
docs/AUDIO_FIDELITY.md F38. checkctx.py CLEAN.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
555 lines
17 KiB
C++
555 lines
17 KiB
C++
#include <cstdlib>
|
|
#include "mungal4.h"
|
|
#pragma hdrstop
|
|
|
|
#include "l4audlvl.h"
|
|
#include "l4audres.h"
|
|
#include "..\munga\audrend.h"
|
|
#include "..\munga\objstrm.h"
|
|
#include "..\munga\namelist.h"
|
|
#include "openal/al.h"
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Release fades ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
// (task #50, AUDIO_FIDELITY F13) ~20 looping presets author a releaseVolEnv of
|
|
// 1.1-3.9 s the AWE32 applied as a note-off fade while the loop continued;
|
|
// StopNote registers a dB-linear ramp here instead of cutting. Serviced once
|
|
// per frame from AudioHead::Execute; a fade finishing (or being reclaimed by a
|
|
// restart) stops + rewinds the source so the next SetupPatch sees AL_INITIAL.
|
|
//
|
|
|
|
// [aud-tail] (Gitea #5 verdict harness, additive): BT_AUD_TAIL=1 gates
|
|
// timestamped play/stop/fade tracing so the audible tail past beam-off can be
|
|
// measured against the authored releaseSec. clock() is CRT wall-clock ms and
|
|
// matches the [aud-tail] emitter state-transition logs (emitter.cpp).
|
|
#include <ctime>
|
|
static bool AudTailLog()
|
|
{
|
|
static int s_on = -1;
|
|
if (s_on < 0) s_on = (getenv("BT_AUD_TAIL") != 0) ? 1 : 0;
|
|
return s_on != 0;
|
|
}
|
|
|
|
namespace
|
|
{
|
|
struct ReleaseFade
|
|
{
|
|
ALuint source;
|
|
float gain0;
|
|
float duration;
|
|
float elapsed;
|
|
bool active;
|
|
};
|
|
const int MAX_RELEASE_FADES = 64;
|
|
ReleaseFade s_releaseFades[MAX_RELEASE_FADES];
|
|
|
|
void FinishFade(ReleaseFade &fade)
|
|
{
|
|
if (AudTailLog())
|
|
DEBUG_STREAM << "[aud-tail] t=" << (long)clock() << "ms fade-END src="
|
|
<< fade.source << " after " << fade.elapsed << "/" << fade.duration
|
|
<< "s\n" << std::flush;
|
|
alSourceStop(fade.source);
|
|
alSourceRewind(fade.source);
|
|
alSourcef(fade.source, AL_GAIN, fade.gain0);
|
|
fade.active = false;
|
|
}
|
|
|
|
void StartReleaseFade(ALuint source, float duration)
|
|
{
|
|
for (int i = 0; i < MAX_RELEASE_FADES; i++)
|
|
{
|
|
if (s_releaseFades[i].active && s_releaseFades[i].source == source)
|
|
{
|
|
return; // already releasing
|
|
}
|
|
}
|
|
for (int i = 0; i < MAX_RELEASE_FADES; i++)
|
|
{
|
|
if (!s_releaseFades[i].active)
|
|
{
|
|
ReleaseFade &fade = s_releaseFades[i];
|
|
fade.source = source;
|
|
alGetSourcef(source, AL_GAIN, &fade.gain0);
|
|
fade.duration = duration;
|
|
fade.elapsed = 0.0f;
|
|
fade.active = true;
|
|
if (AudTailLog())
|
|
DEBUG_STREAM << "[aud-tail] t=" << (long)clock() << "ms fade-START src="
|
|
<< source << " dur=" << duration << "s gain0=" << fade.gain0
|
|
<< "\n" << std::flush;
|
|
return;
|
|
}
|
|
}
|
|
// table full -- fall back to the instant cut
|
|
alSourceStop(source);
|
|
alSourceRewind(source);
|
|
}
|
|
|
|
// A restart wants this source back NOW: finalize any fade so the caller
|
|
// sees AL_INITIAL with the pre-fade gain restored.
|
|
void ReclaimFadingSource(ALuint source)
|
|
{
|
|
for (int i = 0; i < MAX_RELEASE_FADES; i++)
|
|
{
|
|
if (s_releaseFades[i].active && s_releaseFades[i].source == source)
|
|
{
|
|
FinishFade(s_releaseFades[i]);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void PRESET_serviceReleaseFades(float elapsed_seconds)
|
|
{
|
|
for (int i = 0; i < MAX_RELEASE_FADES; i++)
|
|
{
|
|
ReleaseFade &fade = s_releaseFades[i];
|
|
if (!fade.active)
|
|
{
|
|
continue;
|
|
}
|
|
fade.elapsed += elapsed_seconds;
|
|
if (fade.elapsed >= fade.duration)
|
|
{
|
|
FinishFade(fade);
|
|
}
|
|
else
|
|
{
|
|
// dB-linear ramp to -96 dB over the authored release time
|
|
float attenuation = powf(10.0f, -4.8f * (fade.elapsed / fade.duration));
|
|
alSourcef(fade.source, AL_GAIN, fade.gain0 * attenuation);
|
|
}
|
|
}
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PatchLevelOfDetail ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
#if DEBUG_LEVEL>0
|
|
TableOf<PatchLevelOfDetail*, unsigned int>
|
|
PatchLevelOfDetail::patchTableSocket(NULL, True);
|
|
#endif
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
PatchLevelOfDetail::PatchLevelOfDetail(PlugStream *stream):
|
|
AudioLevelOfDetail(stream)
|
|
{
|
|
MemoryStream_Read(stream, &bankID);
|
|
MemoryStream_Read(stream, &patchID);
|
|
MemoryStream_Read(stream, &maxMIDIFilterCutoff);
|
|
|
|
// (task #50) the AWE 4-voice comment applied to layers SHARING a key range;
|
|
// full-zone presets legitimately carry up to MAX_PRESET_SAMPLES zones
|
|
// (key-splits select at most a few per note).
|
|
Warn(GetVoiceCount() > MAX_PRESET_SAMPLES);
|
|
|
|
#ifdef LAB_ONLY
|
|
setupCount = 0;
|
|
#endif
|
|
|
|
//
|
|
// Keep table of created patchs to verify that duplicates
|
|
// are not created, could move this to tool time
|
|
//
|
|
#if DEBUG_LEVEL>0
|
|
unsigned int index_value = (bankID * 1000) + patchID;
|
|
if (patchTableSocket.Find(index_value) != NULL)
|
|
{
|
|
Dump((int)bankID);
|
|
Dump((int)patchID);
|
|
}
|
|
Verify(patchTableSocket.Find(index_value) == NULL);
|
|
patchTableSocket.AddValue(this, index_value);
|
|
#endif
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
void
|
|
PatchLevelOfDetail::BuildFromPage(
|
|
PlugStream *stream,
|
|
NameList *name_list,
|
|
ClassID class_ID,
|
|
ObjectID object_ID
|
|
)
|
|
{
|
|
AudioLevelOfDetail::BuildFromPage(stream, name_list, class_ID, object_ID);
|
|
|
|
//
|
|
// Store fields
|
|
//
|
|
MEM_STRM_WRITE_ENTRY(*stream, name_list, SBKBankID, bank_ID);
|
|
MEM_STRM_WRITE_ENTRY(*stream, name_list, SBKPatchID, patch_ID);
|
|
MEM_STRM_WRITE_ENTRY(*stream, name_list, MIDINRPNValue, max_filter_cutoff);
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
PatchLevelOfDetail::~PatchLevelOfDetail()
|
|
{
|
|
#ifdef LAB_ONLY
|
|
cout << "PatchLevelOfDetail::~PatchLevelOfDetail\t";
|
|
cout << (int)bankID << ":" << (int)patchID << "\t";
|
|
cout << "setupCount=\t" << setupCount << "\n";
|
|
#endif
|
|
|
|
/*if (m_numSources > 0 && m_sources != NULL)
|
|
{
|
|
for (int i=0; i < m_numSources; i++)
|
|
{
|
|
//Detach buffer
|
|
alSourcei(m_sources[i],AL_BUFFER,0);
|
|
//Stop source
|
|
ALenum state;
|
|
alGetSourcei(m_sources[i],AL_SOURCE_STATE,&state);
|
|
if (state == AL_PLAYING)
|
|
{
|
|
alSourceStop(m_sources[i]);
|
|
}
|
|
}
|
|
|
|
//Destroy all sources
|
|
alDeleteSources(m_numSources,m_sources);
|
|
}*/
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
Logical
|
|
PatchLevelOfDetail::TestInstance() const
|
|
{
|
|
AudioLevelOfDetail::TestInstance();
|
|
return True;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
void
|
|
PatchLevelOfDetail::SetupPatch(SourceSet sourceSet, int note, Logical sustained)
|
|
{
|
|
// Check(this);
|
|
//// Check(channel);
|
|
//
|
|
// #ifdef LAB_ONLY
|
|
// setupCount++;
|
|
// #endif
|
|
SAMPLEINFO info;
|
|
|
|
// (task #50, AUDIO_FIDELITY F1) the authored MIDI note SELECTS zones:
|
|
// attach only samples whose [keyLo,keyHi] contains the note (key-splits
|
|
// pick one band, layers attach together); detach the rest so a rewound
|
|
// source can't replay a stale buffer from a previous note.
|
|
for (int i=0; i < sourceSet.count; i++)
|
|
{
|
|
info = PRESET_getSampleInfo(bankID,patchID,i);
|
|
if (info.bufferIndex >= 0)
|
|
{
|
|
ReclaimFadingSource(sourceSet.sources[i]);
|
|
|
|
ALenum sourceState;
|
|
alGetSourcei(sourceSet.sources[i], AL_SOURCE_STATE, &sourceState);
|
|
|
|
bool zone_matches = (note >= info.keyLo && note <= info.keyHi);
|
|
|
|
if (getenv("BT_AUDIO_SPATIAL")) { static int s_se=0; if (s_se++<200)
|
|
DEBUG_STREAM << "[audio] SetupPatch ENTRY bank=" << (int)bankID << " patch=" << (int)patchID << " src=" << sourceSet.sources[i]
|
|
<< " file=" << (info.file?info.file:"?") << " note=" << note << (zone_matches?"":" ZONESKIP")
|
|
<< " state=" << sourceState << " (INITIAL=" << AL_INITIAL << ")" << "\n" << std::flush; }
|
|
|
|
if (sourceState == AL_INITIAL)
|
|
{
|
|
alSourcei(sourceSet.sources[i],AL_BUFFER,
|
|
zone_matches ? AL_getBuffer(info.bufferIndex) : 0);
|
|
{ static int s_a=0; if (getenv("BT_AUDIO_LOG") && s_a++<24) DEBUG_STREAM << "[audio] SetupPatch src=" << sourceSet.sources[i] << " file=" << (info.file?info.file:"?") << " loop=" << (info.loop==ForceStatic?0:1) << "\n" << std::flush; }
|
|
alSourcei(sourceSet.sources[i],AL_SOURCE_RELATIVE,AL_TRUE);
|
|
|
|
AudioRenderer *render = application->GetAudioRenderer();
|
|
AudioHead *head = render->GetAudioHead();
|
|
|
|
// (F1) authored stereo pairs: pan hard-left/right zones via a
|
|
// listener-relative offset (the distance model is AL_NONE, so
|
|
// this affects direction only, never gain).
|
|
float pan_x =
|
|
(info.chan == CHANNEL_LEFT) ? -0.5f :
|
|
(info.chan == CHANNEL_RIGHT) ? 0.5f : 0.0f;
|
|
alSource3f(sourceSet.sources[i],AL_POSITION,pan_x,0,0);
|
|
alSource3f(sourceSet.sources[i],AL_VELOCITY,0,0,0);
|
|
|
|
//
|
|
// (Gitea #51) THE LOOP DECISION. The old rule was
|
|
// `ForceStatic ? 0 : 1`, which turned every non-ForceStatic
|
|
// sample into an OpenAL source that NEVER ends -- 224 of the
|
|
// 603 authored samples, including obvious one-shots. Any such
|
|
// sound whose note-off never arrives plays forever: SAURON's
|
|
// "coolant flush sound glitched and perma".
|
|
//
|
|
// The enum's own comments (L4AUDLVL.h:14-19) say the sample
|
|
// flag is not the decision:
|
|
// LoopAtWill "Will play once OR LOOP AS DESIRED" <- defers
|
|
// ForceStatic "Plays only once EVEN IF LOOPED" <- veto
|
|
// LoopAlways "Ramp up and then down" <- always
|
|
// and LoopAtWill is enum value 0, i.e. the DEFAULT an
|
|
// unauthored sample gets (WTPresets.cpp:37) -- so it cannot
|
|
// mean "loop forever". LoopAlways, the real always-loop, is
|
|
// used by ZERO shipped samples.
|
|
//
|
|
// "As desired" is the SOURCE's authored AudioRenderType,
|
|
// streamed from AUDIO*.RES (AUDLVL.cpp:28): a sustained source
|
|
// (engine, wind, coolant leak) holds its note and loops; a
|
|
// transient one is a one-shot and must not.
|
|
//
|
|
// BT_LOOP_LEGACY=1 restores the old always-loop rule, so a
|
|
// field A/B can settle any "that sound got cut short" report
|
|
// without a rebuild (the BT_NO_PILOTLIST precedent). The
|
|
// beam-sustain layers are the ones to listen to: LaserA/CSustain
|
|
// are authored LoopAtWill on a TRANSIENT source, so they now end
|
|
// with the sample instead of looping until the note-off.
|
|
static int s_legacy = -1;
|
|
if (s_legacy < 0) s_legacy = (getenv("BT_LOOP_LEGACY") != 0) ? 1 : 0;
|
|
|
|
Logical wants_loop = s_legacy
|
|
? ((info.loop != ForceStatic) ? True : False) // old rule
|
|
: ((info.loop == LoopAlways) ? True :
|
|
(info.loop == ForceStatic) ? False :
|
|
sustained); // LoopAtWill -> the source decides
|
|
alSourcei(sourceSet.sources[i],AL_LOOPING, wants_loop ? 1 : 0);
|
|
|
|
// #51 measurement: tabulate what each sound is authored as, so
|
|
// the reclassification can be audited rather than assumed.
|
|
if (getenv("BT_LOOP_AUDIT"))
|
|
DEBUG_STREAM << "[loop-audit] bank=" << (int)bankID
|
|
<< " patch=" << (int)patchID
|
|
<< " file=" << (info.file ? info.file : "?")
|
|
<< " sample=" << (info.loop == ForceStatic ? "ForceStatic" :
|
|
info.loop == LoopAlways ? "LoopAlways" : "LoopAtWill")
|
|
<< " source=" << (sustained ? "Sustained" : "Transient")
|
|
<< " -> loop=" << (wants_loop ? 1 : 0)
|
|
<< (( info.loop != ForceStatic && !sustained) ? " [WAS 1, NOW 0]" : "")
|
|
<< "\n" << std::flush;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// Set the channel to the bank and program, reset patch controls
|
|
//
|
|
/* channel->SelectBank(bankID);
|
|
channel->SendProgramChange(patchID);
|
|
channel->SendController(MIDI_CONTROL_RESET, MIDI_MAX_CONTROL_VALUE);*/
|
|
|
|
//TODO: Sync up OpenAL source info appropriately
|
|
|
|
/*#if 0
|
|
Tell((int)bankID << ":" << (int)patchID << "\n");
|
|
#endif*/
|
|
}
|
|
|
|
void PatchLevelOfDetail::PlayNote(SourceSet sourceSet, int note)
|
|
{
|
|
if (sourceSet.count > 0 && sourceSet.sources != NULL)
|
|
{
|
|
alGetError();
|
|
//DEBUG_STREAM << "Playing bank " << (int)bankID << ", patch " << (int)patchID << std::endl;
|
|
|
|
for (int i = 0; i < sourceSet.count; i++)
|
|
{
|
|
// (F1) play only the zones SetupPatch attached for this note
|
|
ALint attached = 0;
|
|
alGetSourcei(sourceSet.sources[i], AL_BUFFER, &attached);
|
|
if (attached == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
ALenum state;
|
|
alGetSourcei(sourceSet.sources[i], AL_SOURCE_STATE, &state);
|
|
|
|
if (state != AL_PLAYING)
|
|
{
|
|
alSourcePlay(sourceSet.sources[i]);
|
|
{ static int s_p=0; if (getenv("BT_AUDIO_LOG") && s_p++<30) DEBUG_STREAM << "[audio] PlayNote alSourcePlay src=" << sourceSet.sources[i] << " note=" << note << "\n" << std::flush; }
|
|
if (AudTailLog())
|
|
{
|
|
SAMPLEINFO zinfo = PRESET_getSampleInfo(bankID, patchID, i);
|
|
DEBUG_STREAM << "[aud-tail] t=" << (long)clock() << "ms PLAY src="
|
|
<< sourceSet.sources[i] << " bank=" << (int)bankID << " patch="
|
|
<< (int)patchID << " file=" << (zinfo.file ? zinfo.file : "?")
|
|
<< " loop=" << (int)(zinfo.loop == LoopAtWill)
|
|
<< " rel=" << zinfo.releaseSec << "s\n" << std::flush;
|
|
}
|
|
}
|
|
|
|
ALenum error = alGetError();
|
|
if (error != AL_NO_ERROR)
|
|
{
|
|
DEBUG_STREAM << "Playing hit an error: " << error << std::endl;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void PatchLevelOfDetail::StopNote(SourceSet sourceSet)
|
|
{
|
|
if (getenv("BT_AUDIO_LOG")) { static int s_s=0; if (s_s++<400) { DEBUG_STREAM << "[audio] StopNote"; for (int _i=0;_i<sourceSet.count;_i++) DEBUG_STREAM << " src=" << sourceSet.sources[_i]; DEBUG_STREAM << "\n" << std::flush; } }
|
|
if (sourceSet.count >0 && sourceSet.sources != NULL)
|
|
{
|
|
// (task #50, AUDIO_FIDELITY F13) honor the authored releaseVolEnv:
|
|
// a playing zone with a release time fades out dB-linearly instead of
|
|
// cutting; everything else stops instantly (faithful for one-shots).
|
|
for (int i = 0; i < sourceSet.count; i++)
|
|
{
|
|
SAMPLEINFO info = PRESET_getSampleInfo(bankID,patchID,i);
|
|
ALenum state;
|
|
alGetSourcei(sourceSet.sources[i], AL_SOURCE_STATE, &state);
|
|
if (AudTailLog() && state == AL_PLAYING)
|
|
DEBUG_STREAM << "[aud-tail] t=" << (long)clock() << "ms STOP src="
|
|
<< sourceSet.sources[i] << " bank=" << (int)bankID << " patch="
|
|
<< (int)patchID << " file=" << (info.file ? info.file : "?")
|
|
<< " rel=" << info.releaseSec << "s -> "
|
|
<< ((info.releaseSec > 0.05f) ? "fade" : "cut") << "\n" << std::flush;
|
|
if (state == AL_PLAYING && info.releaseSec > 0.05f)
|
|
{
|
|
StartReleaseFade(sourceSet.sources[i], info.releaseSec);
|
|
}
|
|
else
|
|
{
|
|
alSourceStop(sourceSet.sources[i]);
|
|
alSourceRewind(sourceSet.sources[i]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PatchResource ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
PatchResource::PatchResource(PlugStream *stream):
|
|
AudioResource(stream)
|
|
{
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
void
|
|
PatchResource::BuildFromPage(
|
|
PlugStream *stream,
|
|
NameList *name_list,
|
|
ClassID class_ID,
|
|
ObjectID object_ID
|
|
)
|
|
{
|
|
AudioResource::BuildFromPage(stream, name_list, class_ID, object_ID);
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
PatchResource::~PatchResource()
|
|
{
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
Logical
|
|
PatchResource::TestInstance() const
|
|
{
|
|
AudioResource::TestInstance();
|
|
return True;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
void
|
|
PatchResource::SetupPatch(SourceSet sourceSet, int note, Logical sustained)
|
|
{
|
|
Check(this);
|
|
// Check(channel);
|
|
|
|
PatchLevelOfDetail *patch_level_of_detail =
|
|
Cast_Object(PatchLevelOfDetail*, GetAudioLevelOfDetail());
|
|
|
|
Check(patch_level_of_detail);
|
|
patch_level_of_detail->SetupPatch(sourceSet, note, sustained);
|
|
}
|
|
|
|
void PatchResource::PlayNote(SourceSet sourceSet, int note)
|
|
{
|
|
Check(this);
|
|
// Check(channel);
|
|
|
|
PatchLevelOfDetail *patch_level_of_detail =
|
|
Cast_Object(PatchLevelOfDetail*, GetAudioLevelOfDetail());
|
|
|
|
Check(patch_level_of_detail);
|
|
patch_level_of_detail->PlayNote(sourceSet, note);
|
|
}
|
|
|
|
void PatchResource::StopNote(SourceSet sourceSet)
|
|
{
|
|
Check(this);
|
|
// Check(channel);
|
|
|
|
PatchLevelOfDetail *patch_level_of_detail =
|
|
Cast_Object(PatchLevelOfDetail*, GetAudioLevelOfDetail());
|
|
|
|
Check(patch_level_of_detail);
|
|
patch_level_of_detail->StopNote(sourceSet);
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
int PatchResource::GetBankID()
|
|
{
|
|
PatchLevelOfDetail *patch_level_of_detail =
|
|
Cast_Object(PatchLevelOfDetail*, GetAudioLevelOfDetail());
|
|
return (patch_level_of_detail != 0) ? patch_level_of_detail->GetBankID() : 0;
|
|
}
|
|
|
|
int PatchResource::GetPatchID()
|
|
{
|
|
PatchLevelOfDetail *patch_level_of_detail =
|
|
Cast_Object(PatchLevelOfDetail*, GetAudioLevelOfDetail());
|
|
return (patch_level_of_detail != 0) ? patch_level_of_detail->GetPatchID() : 0;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
MIDINRPNValue
|
|
PatchResource::GetMaxMIDIFilterCutoff()
|
|
{
|
|
Check(this);
|
|
|
|
PatchLevelOfDetail *patch_level_of_detail =
|
|
Cast_Object(PatchLevelOfDetail*, GetAudioLevelOfDetail());
|
|
|
|
Check(patch_level_of_detail);
|
|
return patch_level_of_detail->GetMaxMIDIFilterCutoff();
|
|
}
|