Descent3/music/sequencer.cpp

749 lines
19 KiB
C++
Raw Normal View History

/*
* Descent 3
* Copyright (C) 2024 Parallax Software
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
--- HISTORICAL COMMENTS FOLLOW ---
2024-04-16 20:21:35 +00:00
* $Logfile: /DescentIII/Main/music/sequencer.cpp $
* $Revision: 35 $
* $Date: 4/09/99 12:04p $
* $Author: Samir $
*
* Event driven music system
*
* $Log: /DescentIII/Main/music/sequencer.cpp $
*
* 35 4/09/99 12:04p Samir
* take out error messages (mprintfs) for stream load failures.
*
* 34 4/01/99 4:50p Matt
* Took out Warning() function, chaning those calls to mprintf()
*
* 33 3/18/99 10:13a Samir
* msuic update.
*
* 32 3/03/99 5:08p Samir
* added slew of debug code: hopefully the library will be revamped after
* OEM.
*
* 31 3/03/99 3:43a Samir
* moved mprintfs for debugging.
*
* 30 3/02/99 1:18p Samir
* fixes to prevent bad filenames from crashing system.
*
* 29 3/01/99 5:01a Samir
* fixed bug when no pending or active song, it would still try to start a
* pending song.
*
* 28 2/28/99 6:35p Samir
* fixed streaming bugs for very small samples.
*
* 27 2/27/99 8:23p Samir
* fixes to music system to act nicely to sudden and frequent region
* changes.
*
* 26 2/27/99 6:51p Samir
* added code for music tester to display current stream and loop/region.
*
* 25 2/27/99 5:13p Nate
* Samir: fixes
*
* 24 2/27/99 4:37p Samir
* return name of loop currently playing.
*
* 23 2/26/99 5:26p Samir
* fixes to streaming audio to reflect fix in direct sound mixer.
*
* 22 2/22/99 3:23p Luke
* upped size of music buffers
*
* 21 2/16/99 12:09p Samir
* immediate switches open new stream object.
*
* 20 1/28/99 2:22p Samir
* simplified music system for D3.
*
* 19 12/13/98 6:51p Samir
* enhancements.
*
* 18 12/11/98 6:05p Samir
* fixed looping bug (didn't set loop count!)
*
* 17 12/11/98 4:02p Samir
* clean theme changes.
*
* 16 12/10/98 10:12a Samir
* uses newer streaming audio library
*
* 15 12/07/98 11:45a Samir
* hacks to get music system mostly working in DS_8. Will change this
* system so it's a lot cleaner after music demo.
*
* 14 12/03/98 12:48p Samir
* speed up sequencer so that gap between samples isn't too big.
*
* 13 11/20/98 5:20p Samir
* added a bunch of high level scripting commands.
*
* 12 11/16/98 4:15p Samir
* fadeouts.
*
* 11 11/13/98 2:27p Samir
* new music system.
*
* 10 8/10/98 5:53p Samir
* improved switching between streams and basic background/combat music.
*
* 9 7/30/98 7:33p Jeff
* fixed bug passing NULL to reset.
*
* 8 7/28/98 5:43p Samir
* reorg of music system.
*
* 7 7/24/98 5:20p Samir
* first real music checkin.
*
* 6 7/08/98 6:26p Samir
* took out obsolete code.
*
* 5 2/15/98 7:12p Chris
* More improvements to the sound lib
*
* 4 1/05/98 3:54p Chris
* Added ambient and explosion sounds
*
* 3 12/31/97 2:58a Chris
* Another major revision to the SoundLib. This cleaned up the code after
* removing the direct sound secondary buffers.
*
* 2 12/10/97 4:47p Chris
* Revision 1.0 of new sound library (no hardware -- uses primary buffer
* streaming)
*
* 3 5/29/97 3:58p Samir
* Works with some very small pauses.
*
* 2 5/23/97 4:34p Samir
*
* $NoKeywords: $
*/
#include <cstring>
#include <cstdlib>
#include "ddio.h"
#include "inffile.h"
#include "log.h"
#include "mem.h"
2024-04-16 20:21:35 +00:00
#include "music.h"
#include "musiclib.h"
#include "pserror.h"
#include "streamaudio.h"
OutrageMusicSeq::OutrageMusicSeq() {
m_sequencer_run = false;
m_sequencer_init = false;
m_str_buffer = NULL;
m_ins_buffer = NULL;
m_curregion = -1;
m_pending_song = m_active_song = m_playing_song = NULL;
}
OutrageMusicSeq::~OutrageMusicSeq() { OutrageMusicSeq::Shutdown(); }
// takes a OMS theme file.
bool OutrageMusicSeq::Init(const char *theme_file) {
// reset streams
m_dominant_strm = 0;
m_timer = 0.0f;
if (theme_file[0] == 0) {
return false;
}
Stop();
// initialize memory buffers
m_ins_buffer = mem_rmalloc<music_ins>(MAX_MUSIC_INSTRUCTIONS);
m_str_buffer = mem_rmalloc<char>(MAX_MUSIC_STRLEN);
2024-04-16 20:21:35 +00:00
m_ins_curptr = m_ins_buffer;
m_str_curptr = m_str_buffer;
m_str_curptr[0] = 0;
// initialize music lists
m_music_list.free();
m_tracklist.init(64);
m_pending_song = m_active_song = m_playing_song = NULL;
m_output_q.flush();
m_curregion = -1;
OutrageMusicSeq::SetVolume(1.0f);
m_sequencer_init = true;
return LoadTheme(theme_file);
}
void OutrageMusicSeq::Shutdown() {
if (!m_sequencer_init)
return;
Stop();
// free music lists.
FreeList();
m_tracklist.free();
// free allocated buffers
mem_free(m_ins_buffer);
mem_free(m_str_buffer);
// free streams
for (int i = 0; i < OMS_NUM_STRM; i++)
m_strm[i].Reset(this);
m_playing_song = NULL;
m_sequencer_init = false;
}
// starts the sequencer (usually at the beginning of a level)
void OutrageMusicSeq::Start() {
if (IsSequencerRunning() || !m_sequencer_init)
return;
int i;
m_sequencer_run = true;
m_dominant_strm = 0;
m_timer = 0.0f;
for (i = 0; i < N_MUSIC_REGS; i++) {
m_registers[i] = 0;
}
LOG_INFO << "Music system on.";
2024-04-16 20:21:35 +00:00
}
// stops the sequencer, flushes events
void OutrageMusicSeq::Stop() {
if (!IsSequencerRunning())
return;
int i;
LOG_INFO << "Music system off.";
2024-04-16 20:21:35 +00:00
m_sequencer_run = false;
for (i = 0; i < OMS_NUM_STRM; i++) {
m_strm[i].Reset(this);
}
}
// set volume of sequencer
void OutrageMusicSeq::SetVolume(float vol) {
int i;
ASSERT(vol >= 0.0f && vol <= 1.0f);
m_mastervol = vol;
for (i = 0; i < OMS_NUM_STRM; i++)
m_strm[i].SetVolume(m_mastervol);
}
#define START_PENDING_SONG() \
do { \
ASSERT(m_pending_song); \
if (m_active_song) { \
m_pending_song->strm = m_active_song->strm; \
} \
m_active_song = m_pending_song; \
if (m_active_song) { \
m_active_song->stream_idle = false; \
m_active_song->request_stop = false; \
} \
m_pending_song = NULL; \
} while (0)
// runs a frame of input.
void OutrageMusicSeq::Frame(float frame_time) {
bool start_pending_song = false;
int i;
// run check.
if (!m_sequencer_run)
return;
// close all streams that aren't the dominant one and have stopped playing
for (i = 0; i < OMS_NUM_STRM; i++) {
if ((&m_strm[i]) != (&m_strm[m_dominant_strm])) {
if (m_strm[i].m_stream.State() == STRM_STOPPED) {
m_strm[i].m_stream.Close();
}
}
}
// determine when to start song if a song is pending.
if (!m_active_song && m_pending_song) {
start_pending_song = true;
}
if (m_pending_song) {
if (m_pending_song->immediate_switch == true) {
start_pending_song = true;
} else if (m_active_song) {
if (m_active_song->stream_idle) {
// this will transfer the current song's steam to the pending one, so the
// ExecScript function can manage these streams.
start_pending_song = true;
} else if (!m_active_song->request_stop) {
m_active_song->request_stop = true;
}
}
}
if (start_pending_song) {
LOG_INFO << "Starting pending song.";
2024-04-16 20:21:35 +00:00
START_PENDING_SONG();
}
// execute song code
ExecScript(m_active_song);
// process streams, starting at dominant stream.
// this is vital to ensure that the dominant stream's commands are executed before any
// other streams.
// i = m_dominant_strm;
// do
// {
// m_strm[i].Process(frame_time);
// i++;
// if (i == OMS_NUM_STRM) i = 0;
// }
// while (i != m_dominant_strm);
m_timer += frame_time;
}
// music kernal.
void OutrageMusicSeq::ExecScript(music_stream *strm) {
if (!strm)
return;
// get command
next_ins:
music_ins *cur_ins = &strm->ins[strm->ip];
2024-05-24 03:07:26 +00:00
uint8_t cmd = cur_ins->cmd;
2024-04-16 20:21:35 +00:00
tMusicVal val = cur_ins->opr.num;
char *str = cur_ins->opr.str;
const char *name;
// goto next instruction.
strm->ip++;
2024-05-24 15:46:07 +00:00
// mprintf(0, "%d ", cmd);
2024-04-16 20:21:35 +00:00
AudioStream *stream = &strm->strm->m_stream;
// execute opcodes.
// if (strm->last_ip != strm->ip) {
// LOGFILE((_logfp, "ins=%d\n", cmd));
// }
strm->last_ip = strm->ip;
switch (cmd) {
case OMFCMD_PLAY:
name = m_tracklist.get(strm->ln_reg);
if (name) {
// close this song's stream.
bool err = false;
strm->old_strm = strm->strm;
strm->error = false;
if (strm->immediate_switch) {
// we start playing the current stream immediately.
stream->Close();
m_dominant_strm = DOMINANT_STRM_ADJUST();
2024-05-24 15:46:07 +00:00
// mprintf(0, "MUSIC: Starting stream with %s on channel %d.\n", name, m_dominant_strm);
LOG_INFO.printf("Starting stream with %s on channel %d.", name, m_dominant_strm);
2024-04-16 20:21:35 +00:00
strm->strm = &m_strm[m_dominant_strm];
stream = &strm->strm->m_stream;
err = stream->Open(name);
} else {
m_dominant_strm = DOMINANT_STRM_ADJUST();
2024-05-24 15:46:07 +00:00
// mprintf(0, "MUSIC: Preparing stream with %s on channel %d.\n", name, m_dominant_strm);
LOG_INFO.printf("Preparing stream with %s on channel %d.", name, m_dominant_strm);
2024-04-16 20:21:35 +00:00
strm->strm = &m_strm[m_dominant_strm];
stream = &strm->strm->m_stream;
err = stream->Open(name);
// stream->Open(name, STRM_OPNF_GRADUAL);
}
// skip instructions until error is cleared.
if (!err) {
LOG_WARNING.printf("Error opening stream %s on channel %d.", name, m_dominant_strm);
2024-04-16 20:21:35 +00:00
strm->error = true;
}
stream->SetLoopCount(strm->p_reg);
stream->SetVolume(m_mastervol);
strm->stream_idle = false;
} else {
LOG_WARNING << "OMS: Stream was not found in track list.";
2024-04-16 20:21:35 +00:00
}
strm->pending_loop_name = name;
break;
case OMFCMD_MPLAY:
if (strm->immediate_switch || strm->error) {
strm->immediate_switch = false;
} else if (!stream->IsReady()) {
strm->ip--;
break;
} else if (strm->old_strm) {
if (strm->old_strm->m_stream.State() != STRM_STOPPED && strm->old_strm->m_stream.State() != STRM_INVALID) {
strm->ip--;
break;
}
}
strm->loop_name = strm->pending_loop_name;
m_playing_song = strm;
if (!strm->error) {
strm->error = !(stream->Play());
}
if (!strm->error) {
LOG_INFO.printf("Playing %s.", strm->loop_name);
LOG_INFO.printf("State of played stream is %d.", strm->loop_name, stream->State());
2024-04-16 20:21:35 +00:00
} else {
LOG_WARNING.printf("Error playing %s.", strm->loop_name);
2024-04-16 20:21:35 +00:00
}
break;
case OMFCMD_WAIT:
Int3(); // flaky, shouldn't be used anymore.
strm->ip--;
if (val == 1) {
if (strm->strm->m_valid_result && strm->strm->m_result.cmd == OMS_STRM_STOP) {
// wait till stream has finished to continue.
strm->ip++;
}
}
break;
case OMFCMD_LCMP:
strm->c_reg = val;
break;
case OMFCMD_LLPT:
strm->ln_reg = str;
break;
case OMFCMD_LPLC:
strm->p_reg = val;
break;
case OMFCMD_SETI:
strm->i_reg = val;
break;
case OMFCMD_INCI:
strm->i_reg++;
break;
case OMFCMD_IFI:
strm->ifi_block = (strm->i_reg == val) ? true : false;
if (!strm->ifi_block) {
// skip ifi block.
while (strm->ins[strm->ip].cmd != OMFCMD_ENDIFI)
strm->ip++;
strm->ip++;
}
break;
case OMFCMD_ENDIFI:
strm->ifi_block = false;
break;
case OMFCMD_COMPARE: {
tMusicVal reg1 = strm->c_reg;
tMusicVal reg2 = m_registers[val];
strm->b_reg = (reg2 - reg1);
break;
}
case OMFCMD_GOTO:
// simply set IP.
strm->ip = val;
break;
case OMFCMD_BLT:
if (strm->b_reg < 0) {
strm->ip = val;
}
break;
case OMFCMD_BGT:
if (strm->b_reg > 0) {
strm->ip = val;
}
break;
case OMFCMD_BEQ:
if (strm->b_reg == 0) {
strm->ip = val;
}
break;
case OMFCMD_BNIF:
if (stream->ReadAheadFinishedLoop() || strm->error) {
oms_q_evt evt;
evt.cmd = OMS_EVT_LOOPENDING;
evt.parm.i = strm->type;
m_output_q.send(evt);
if (!strm->error) {
strm->ip--; // repeat this instruction again
}
} else if (!strm->request_stop) {
if (stream->ReadAhead()) {
strm->ip = val;
} else if (stream->State() != STRM_STOPPING && stream->State() != STRM_STOPPED) {
strm->ip = val;
}
} else {
stream->Stop(true); // soft stop on end of stream.
if (strm->request_stop) {
strm->stream_idle = true;
strm->request_stop = false;
LOG_INFO << "Processed stop request.";
2024-04-16 20:21:35 +00:00
}
}
break;
case OMFCMD_ENDSECTION: {
strm->ip--;
if (stream->State() == STRM_STOPPED) {
oms_q_evt evt;
strm->ip++;
evt.cmd = OMS_EVT_SONGENDED;
evt.parm.i = strm->type;
m_output_q.send(evt);
strm->stream_idle = true;
m_active_song = NULL;
m_playing_song = NULL;
}
break;
}
}
// okay, ifi_block is only really useful to determine if we're going to execute consecutive instructions
// note that even if we turn off ifi_block, this doesn't stop instructions from running inside the ifi_block
// but it does stop consecutive instructions.
// the actual decision to run the instructions in an ifi_block depends on the initial check.
if (cmd != OMFCMD_ENDSECTION && cmd != OMFCMD_PLAY && cmd != OMFCMD_MPLAY && cmd < OMFCMD_BRANCHING) {
goto next_ins;
} else {
strm->ifi_block = false;
}
}
// start a song, stopping the old either cleanly (on measure) or abruptly.
void OutrageMusicSeq::StartSong(int song, bool clean_switch) {
if (m_curregion >= 0) {
m_pending_song = GetSong(m_curregion, (int16_t)song);
2024-04-16 20:21:35 +00:00
} else {
m_pending_song = NULL;
}
if (m_pending_song) {
m_pending_song->request_stop = false;
m_pending_song->immediate_switch = !clean_switch;
m_pending_song->strm = &m_strm[m_dominant_strm];
}
}
// stops song, cold.
void OutrageMusicSeq::StopSong() {
if (m_active_song) {
m_active_song = NULL;
m_playing_song = NULL;
}
m_strm[m_dominant_strm].m_stream.Close();
}
// pauses
void OutrageMusicSeq::Pause() {
int i;
for (i = 0; i < OMS_NUM_STRM; i++) {
m_strm[i].m_stream.Pause();
}
}
void OutrageMusicSeq::Resume() {
int i;
for (i = 0; i < OMS_NUM_STRM; i++) {
m_strm[i].m_stream.Resume();
}
}
// get and set user defined (theme file) parameters
void OutrageMusicSeq::SetRegister(int parm, tMusicVal val) {
ASSERT(parm >= 0 && parm < N_MUSIC_REGS);
m_registers[parm] = val;
}
tMusicVal OutrageMusicSeq::GetRegister(int parm) {
ASSERT(parm >= 0 && parm < N_MUSIC_REGS);
return m_registers[parm];
}
// memory management
OutrageMusicSeq::music_ins *OutrageMusicSeq::alloc_and_copy_ins(int len, const music_ins *ins) {
// copy instructions to static buffer and return a pointer to it
if ((m_ins_curptr + len) > (m_ins_buffer + MAX_MUSIC_INSTRUCTIONS)) {
Int3();
return NULL;
} else {
memcpy(m_ins_curptr, ins, len * sizeof(music_ins));
m_ins_curptr += len;
return (m_ins_curptr - len);
}
}
char *OutrageMusicSeq::alloc_and_copy_str(const char *str) {
// copy string to static buffer and return a pointer to it
int slen = strlen(str) + 1;
if ((m_str_curptr + slen) > (m_str_buffer + MAX_MUSIC_STRLEN)) {
Int3();
return NULL;
} else {
strcpy(m_str_curptr, str);
m_str_curptr += slen;
return (m_str_curptr - slen);
}
}
// Music list management
OutrageMusicSeq::music_stream *OutrageMusicSeq::AddToList(int16_t region, int16_t theme_type, int len,
2024-04-16 20:21:35 +00:00
const music_ins *ins) {
tListNode<music_stream> *node = new tListNode<OutrageMusicSeq::music_stream>;
// create list item
node->t.region = region;
node->t.type = theme_type;
node->t.ins = alloc_and_copy_ins(len, ins);
if (!node->t.ins) {
delete node;
return NULL;
}
// link it in.
m_music_list.link(node);
return &node->t;
}
void OutrageMusicSeq::FreeList() {
tListNode<music_stream> *node;
2024-04-20 13:29:12 +00:00
while ((node = m_music_list.start())) {
2024-04-16 20:21:35 +00:00
node = m_music_list.unlink();
Resolve alloc-dealloc-mismatch in OutrageMusicSeq::FreeList Once a level is loaded/started, the program runs into: ==17675==ERROR: AddressSanitizer: alloc-dealloc-mismatch (operator new vs operator delete []) on 0x50800031be20 f0 operator delete[](void*) (/lib64/libasan.so.8+0xfde68) (BuildId: 5615a04b8d0a5a8eafa661f7235e2d98fa2b9137) f1 OutrageMusicSeq::FreeList() $GIT/music/sequencer.cpp:675 f2 OutrageMusicSeq::Shutdown() $GIT/music/sequencer.cpp:208 f3 D3MusicStop() $GIT/Descent3/d3music.cpp:118 f4 mmInterface::OnDestroy() $GIT/Descent3/mmItem.cpp:400 f5 UIWindow::Destroy() $GIT/ui/UIWindow.cpp:203 f6 MainMenu() $GIT/Descent3/menu.cpp:911 f7 MainLoop() $GIT/Descent3/descent.cpp:546 f8 Descent3() $GIT/Descent3/descent.cpp:508 f9 oeD3LnxApp::run() $GIT/Descent3/sdlmain.cpp:151 0x50800031be20 is located 0 bytes inside of 88-byte region [0x50800031be20,0x50800031be78) allocated by thread T0 here: f0 operator new(unsigned long) (/lib64/libasan.so.8+0xfd2f8) (BuildId: 5615a04b8d0a5a8eafa661f7235e2d98fa2b9137) f1 OutrageMusicSeq::AddToList(short, short, int, OutrageMusicSeq::music_ins const*) $GIT/music/sequencer.cpp:653 f2 OutrageMusicSeq::LoadTheme(char const*) $GIT/music/omflex.cpp:310 f3 OutrageMusicSeq::Init(char const*) $GIT/music/sequencer.cpp:198 f4 D3MusicStart(char const*) $GIT/Descent3/d3music.cpp:80 f5 mmInterface::Create() $GIT/Descent3/mmItem.cpp:353 f6 MainMenu() $GIT/Descent3/menu.cpp:732 f7 MainLoop() $GIT/Descent3/descent.cpp:546 f8 Descent3() $GIT/Descent3/descent.cpp:508 f9 oeD3LnxApp::run() $GIT/Descent3/sdlmain.cpp:151
2024-08-29 21:44:18 +00:00
delete node;
2024-04-16 20:21:35 +00:00
}
m_music_list.free();
}
OutrageMusicSeq::music_stream *OutrageMusicSeq::GetSong(int16_t region, int16_t theme_type) {
2024-04-16 20:21:35 +00:00
tListNode<music_stream> *node = m_music_list.start();
music_stream *strm = NULL;
int i;
for (i = 0; i < m_music_list.length(); node = m_music_list.next(), i++) {
if (region == node->t.region) {
if (theme_type == node->t.type) {
strm = &node->t;
}
if (strm) {
strm->request_stop = false;
strm->immediate_switch = false;
strm->ip = 0;
strm->b_reg = 0;
strm->c_reg = 0;
strm->ln_reg = 0;
strm->p_reg = 0;
strm->i_reg = 0;
strm->ifi_block = false;
strm->stream_idle = true;
strm->strm = NULL;
strm->old_strm = NULL;
strm->loop_name = NULL;
strm->pending_loop_name = NULL;
strm->last_ip = -1;
strm->error = false;
break;
}
}
}
return strm;
}
//@@void OutrageMusicSeq::seqSilence()
//@@{
//@@// silence all streams.
//@@ int init_strm = m_dominant_strm;
//@@ do
//@@ {
//@@ // request switch to dominant stream when measure is complete.
//@@ m_strm[m_dominant_strm].SEND_STRM_FADEOUT(4.0f);
//@@ m_strm[m_dominant_strm].SEND_STRM_STOP();
//@@ m_strm[m_dominant_strm].SEND_STRM_FREE();
//@@ m_dominant_strm = DOMINANT_STRM_ADJUST();
//@@ }
//@@ while (init_strm != m_dominant_strm);
//@@}
void OutrageMusicSeq::SetCurrentRegion(int16_t region) { m_curregion = region; }
2024-04-16 20:21:35 +00:00
int16_t OutrageMusicSeq::GetCurrentRegion() const { return m_curregion; }
2024-04-16 20:21:35 +00:00
// get current loop playing
const char *OutrageMusicSeq::GetCurrentLoopName(int *count) {
if (m_playing_song) {
*count = m_playing_song->strm->m_stream.GetLoopCount();
return m_playing_song->loop_name;
}
*count = 0;
return NULL;
}
// gets current region PLAYING, not PENDING like above.
int16_t OutrageMusicSeq::GetPlayingRegion() const {
2024-04-16 20:21:35 +00:00
if (m_playing_song) {
return m_playing_song->region;
}
return -1;
}