/* * Portions of this file are copyright Rebirth contributors and licensed as * described in COPYING.txt. * Portions of this file are copyright Parallax Software and licensed * according to the Parallax license below. * See COPYING.txt for license details. THE COMPUTER CODE CONTAINED HEREIN IS THE SOLE PROPERTY OF PARALLAX SOFTWARE CORPORATION ("PARALLAX"). PARALLAX, IN DISTRIBUTING THE CODE TO END-USERS, AND SUBJECT TO ALL OF THE TERMS AND CONDITIONS HEREIN, GRANTS A ROYALTY-FREE, PERPETUAL LICENSE TO SUCH END-USERS FOR USE BY SUCH END-USERS IN USING, DISPLAYING, AND CREATING DERIVATIVE WORKS THEREOF, SO LONG AS SUCH USE, DISPLAY OR CREATION IS FOR NON-COMMERCIAL, ROYALTY OR REVENUE FREE PURPOSES. IN NO EVENT SHALL THE END-USER USE THE COMPUTER CODE CONTAINED HEREIN FOR REVENUE-BEARING PURPOSES. THE END-USER UNDERSTANDS AND AGREES TO THE TERMS HEREIN AND ACCEPTS THE SAME BY USE OF THIS FILE. COPYRIGHT 1993-1999 PARALLAX SOFTWARE CORPORATION. ALL RIGHTS RESERVED. */ #include #include #include #include #include #include #include #include "maths.h" #include "object.h" #include "timer.h" #include "joy.h" #include "digi.h" #include "digi_audio.h" #include "sounds.h" #include "args.h" #include "newdemo.h" #include "gameseg.h" #include "dxxerror.h" #include "wall.h" #include "piggy.h" #include "kconfig.h" #include "config.h" #include "compiler-range_for.h" #include "d_levelstate.h" #include #include using std::max; namespace dcx { namespace { constexpr std::integral_constant SOF_USED{}; // Set if this sample is used constexpr std::integral_constant SOF_LINK_TO_OBJ{}; // Sound is linked to a moving object. If object dies, then finishes play and quits. constexpr std::integral_constant SOF_LINK_TO_POS{}; // Sound is linked to segment, pos constexpr std::integral_constant SOF_PLAY_FOREVER{}; // Play forever (or until level is stopped), otherwise plays once constexpr std::integral_constant SOF_PERMANENT{}; // Part of the level, like a waterfall or fan constexpr std::integral_constant MAX_SOUND_OBJECTS{}; } struct sound_object { short signature; // A unique signature to this sound ubyte flags; // Used to tell if this slot is used and/or currently playing, and how long. ubyte pad; // Keep alignment fix max_volume; // Max volume that this sound is playing at vm_distance max_distance; // The max distance that this sound can be heard at... int volume; // Volume that this sound is playing at sound_pan pan; // Pan value that this sound is playing at int channel; // What channel this is playing on, -1 if not playing short soundnum; // The sound number that is playing int loop_start; // The start point of the loop. -1 means no loop int loop_end; // The end point of the loop union link { constexpr link() : pos{} { } struct { segnum_t segnum; // Used if SOF_LINK_TO_POS field is used sidenum_t sidenum; vms_vector position; } pos; struct { objnum_t objnum; // Used if SOF_LINK_TO_OBJ field is used object_signature_t objsignature; } obj; } link_type; }; namespace { using sound_objects_t = std::array; static sound_objects_t SoundObjects; static short next_signature=0; static int N_active_sound_objects; static void digi_kill_sound(sound_object &s) { s.flags = 0; // Mark as dead, so some other sound can use this sound if (s.channel > -1) { N_active_sound_objects--; digi_stop_sound(std::exchange(s.channel, -1)); } } static std::pair find_sound_object_flags0(sound_objects_t &SoundObjects) { const auto eso = SoundObjects.end(); const auto i = std::find_if(SoundObjects.begin(), eso, [](sound_object &so) { return so.flags == 0; }); return {i, eso}; } static std::pair find_sound_object(sound_objects_t &SoundObjects, const unsigned soundnum, const vcobjidx_t obj, const sound_stack once) { if (once == sound_stack::allow_stacking) return find_sound_object_flags0(SoundObjects); const auto eso = SoundObjects.end(); sound_objects_t::iterator free_sound_object = eso; for (auto i = SoundObjects.begin(); i != eso; ++i) { constexpr uint8_t used_obj = SOF_USED | SOF_LINK_TO_OBJ; auto &so = *i; const auto flags = so.flags; if (flags == 0) free_sound_object = i; else if (so.soundnum == soundnum && (flags & used_obj) == used_obj) { /* No check for a signature match here. If the signature * matches, this sound will be reclaimed to prevent * stacking. If the signature does not match, then the old * sound is tied to a dead object and needs to be removed. */ if (so.link_type.obj.objnum == obj) { digi_kill_sound(so); return {i, eso}; } } } return {free_sound_object, eso}; } } } namespace dsx { /* Find the sound which actually equates to a sound number */ int digi_xlat_sound(int soundno) { if (soundno < 0) return -1; if (CGameArg.SysLowMem) { soundno = AltSounds[soundno]; if (soundno == 255) return -1; } //Assert(Sounds[soundno] != 255); //if hit this, probably using undefined sound if (Sounds[soundno] == 255) return -1; return Sounds[soundno]; } namespace { static int digi_unxlat_sound(int soundno) { if ( soundno < 0 ) return -1; auto &table = (CGameArg.SysLowMem ? AltSounds : Sounds); auto b = std::begin(table); auto e = std::end(table); auto i = std::find(b, e, soundno); if (i != e) return std::distance(b, i); throw std::invalid_argument("sound not loaded"); } static std::pair digi_get_sound_loc(const vms_matrix &listener, const vms_vector &listener_pos, const vcsegptridx_t listener_seg, const vms_vector &sound_pos, const vcsegptridx_t sound_seg, fix max_volume, vm_distance max_distance) { vms_vector vector_to_sound; fix angle_from_ear; max_distance = (max_distance*5)/4; // Make all sounds travel 1.25 times as far. // Warning: Made the vm_vec_normalized_dir be vm_vec_normalized_dir_quick and got illegal values to acos in the fang computation. auto distance = vm_vec_normalized_dir_quick( vector_to_sound, sound_pos, listener_pos ); if (distance < max_distance ) { int num_search_segs = f2i(max_distance/20); if ( num_search_segs < 1 ) num_search_segs = 1; auto path_distance = find_connected_distance(listener_pos, listener_seg, sound_pos, sound_seg, num_search_segs, WALL_IS_DOORWAY_FLAG::rendpast | WALL_IS_DOORWAY_FLAG::fly); if ( path_distance > -1 ) { const int volume = max_volume - fixdiv(path_distance,max_distance); if (volume > 0) { angle_from_ear = vm_vec_delta_ang_norm(listener.rvec,vector_to_sound,listener.uvec); auto cosang = fix_cos(angle_from_ear); if (GameCfg.ReverseStereo) cosang *= -1; return {volume, sound_pan{(cosang + F1_0) / 2}}; } } } return {}; } static void digi_update_sound_loc(const vms_matrix &listener, const vms_vector &listener_pos, const vcsegptridx_t listener_seg, const vms_vector &sound_pos, const vcsegptridx_t sound_seg, sound_object &so) { auto &&[volume, pan] = digi_get_sound_loc(listener, listener_pos, listener_seg, sound_pos, sound_seg, so.max_volume, so.max_distance); so.volume = volume; so.pan = pan; } } void digi_play_sample_once( int soundno, fix max_volume ) { if ( Newdemo_state == ND_STATE_RECORDING ) newdemo_record_sound( soundno ); soundno = digi_xlat_sound(soundno); if (soundno < 0 ) return; // start the sample playing digi_start_sound(soundno, max_volume, sound_pan{0x7fff}, 0, -1, -1, sound_object_none); } void digi_play_sample( int soundno, fix max_volume ) { if ( Newdemo_state == ND_STATE_RECORDING ) newdemo_record_sound( soundno ); soundno = digi_xlat_sound(soundno); if (soundno < 0 ) return; // start the sample playing digi_start_sound(soundno, max_volume, sound_pan{0x7fff}, 0, -1, -1, sound_object_none); } void digi_play_sample_3d(int soundno, const sound_pan angle, int volume) { if ( Newdemo_state == ND_STATE_RECORDING ) { newdemo_record_sound_3d_once( soundno, angle, volume ); } soundno = digi_xlat_sound(soundno); if (soundno < 0 ) return; if (volume < 10 ) return; // start the sample playing digi_start_sound( soundno, volume, angle, 0, -1, -1, sound_object_none); } namespace { static void SoundQ_init(); static void SoundQ_process(); static void SoundQ_pause(); } void digi_init_sounds() { SoundQ_init(); digi_stop_all_channels(); digi_stop_looping_sound(); range_for (auto &i, SoundObjects) { i.channel = -1; i.flags = 0; // Mark as dead, so some other sound can use this sound } N_active_sound_objects = 0; } namespace { // plays a sample that loops forever. // Call digi_stop_channe(channel) to stop it. // Call digi_set_channel_volume(channel, volume) to change volume. // if loop_start is -1, entire sample loops // Returns the channel that sound is playing on, or -1 if can't play. // This could happen because of no sound drivers loaded or not enough channels. static int digi_looping_sound = -1; static int digi_looping_volume; static int digi_looping_start = -1; static int digi_looping_end = -1; static int digi_looping_channel = -1; static void digi_play_sample_looping_sub() { if ( digi_looping_sound > -1 ) digi_looping_channel = digi_start_sound( digi_looping_sound, digi_looping_volume, sound_pan{0x7fff}, 1, digi_looping_start, digi_looping_end, sound_object_none); } } void digi_play_sample_looping( int soundno, fix max_volume,int loop_start, int loop_end ) { soundno = digi_xlat_sound(soundno); if (soundno < 0 ) return; if (digi_looping_channel>-1) digi_stop_sound( digi_looping_channel ); digi_looping_sound = soundno; digi_looping_volume = max_volume; digi_looping_start = loop_start; digi_looping_end = loop_end; digi_play_sample_looping_sub(); } void digi_change_looping_volume( fix volume ) { digi_looping_volume = volume; if ( digi_looping_channel > -1 ) digi_set_channel_volume( digi_looping_channel, volume ); } namespace { static void digi_pause_looping_sound() { const auto c = digi_looping_channel; if (c > -1) { digi_looping_channel = -1; digi_stop_sound(c); } } } void digi_stop_looping_sound() { digi_looping_sound = -1; digi_pause_looping_sound(); } namespace { static void digi_unpause_looping_sound() { digi_play_sample_looping_sub(); } } //hack to not start object when loading level int Dont_start_sound_objects = 0; namespace { static void digi_start_sound_object(sound_object &s) { // start sample structures s.channel = -1; if ( s.volume <= 0 ) return; if ( Dont_start_sound_objects ) return; // only use up to half the sound channels for "permanent" sounts if ((s.flags & SOF_PERMANENT) && (N_active_sound_objects >= max(1, digi_max_channels / 4))) return; // start the sample playing s.channel = digi_start_sound( s.soundnum, s.volume, s.pan, s.flags & SOF_PLAY_FOREVER, s.loop_start, s.loop_end, &s); if (s.channel > -1 ) N_active_sound_objects++; } static void digi_link_sound_common(const object_base &viewer, sound_object &so, const vms_vector &pos, const int forever, const fix max_volume, const vm_distance max_distance, const int soundnum, const vcsegptridx_t segnum) { so.signature=next_signature++; if ( forever ) so.flags |= SOF_PLAY_FOREVER; so.soundnum = soundnum; so.max_volume = max_volume; so.max_distance = max_distance; so.volume = 0; so.pan = {}; if (Dont_start_sound_objects) { //started at level start so.flags |= SOF_PERMANENT; so.channel = -1; } else { digi_update_sound_loc(viewer.orient, viewer.pos, segnum.absolute_sibling(viewer.segnum), pos, segnum, so); digi_start_sound_object(so); // If it's a one-shot sound effect, and it can't start right away, then // just cancel it and be done with it. if ( (so.channel < 0) && (!(so.flags & SOF_PLAY_FOREVER)) ) { so.flags = 0; return; } } } } void digi_link_sound_to_object3(const unsigned org_soundnum, const vcobjptridx_t objnum, const uint8_t forever, const fix max_volume, const sound_stack once, const vm_distance max_distance, const int loop_start, const int loop_end) { auto &viewer = *Viewer; int soundnum; soundnum = digi_xlat_sound(org_soundnum); if (max_volume < 0) return; // if ( max_volume > F1_0 ) max_volume = F1_0; if (soundnum < 0) return; if (GameSounds[soundnum].data==NULL) { Int3(); return; } if ( Newdemo_state == ND_STATE_RECORDING ) { if ( !forever ) { // forever flag is not recorded, use original limited sound objects hack for demo recording auto segnum = vcsegptridx(objnum->segnum); const auto &&[volume, pan] = digi_get_sound_loc(viewer.orient, viewer.pos, segnum.absolute_sibling(viewer.segnum), objnum->pos, segnum, max_volume, max_distance); newdemo_record_sound_3d_once( org_soundnum, pan, volume ); } else newdemo_record_link_sound_to_object3( org_soundnum, objnum, max_volume, max_distance, loop_start, loop_end ); } const auto &&f = find_sound_object(SoundObjects, soundnum, objnum, once); if (f.first == f.second) return; auto &so = *f.first; so.flags = SOF_USED | SOF_LINK_TO_OBJ; so.link_type.obj.objnum = objnum; so.link_type.obj.objsignature = objnum->signature; so.loop_start = loop_start; so.loop_end = loop_end; digi_link_sound_common(viewer, so, objnum->pos, forever, max_volume, max_distance, soundnum, vcsegptridx(objnum->segnum)); } void digi_link_sound_to_object2(const unsigned soundnum, const vcobjptridx_t objnum, const uint8_t forever, const fix max_volume, const sound_stack once, const vm_distance max_distance) { digi_link_sound_to_object3(soundnum, objnum, forever, max_volume, once, max_distance, -1, -1); } void digi_link_sound_to_object(const unsigned soundnum, const vcobjptridx_t objnum, const uint8_t forever, const fix max_volume, const sound_stack once) { digi_link_sound_to_object2(soundnum, objnum, forever, max_volume, once, vm_distance{256*F1_0}); } namespace { static void digi_link_sound_to_pos2(const int org_soundnum, const vcsegptridx_t segnum, const sidenum_t sidenum, const vms_vector &pos, int forever, fix max_volume, const vm_distance max_distance) { auto &viewer = *Viewer; int soundnum; soundnum = digi_xlat_sound(org_soundnum); if (max_volume < 0) return; // if ( max_volume > F1_0 ) max_volume = F1_0; if (soundnum < 0) return; if (GameSounds[soundnum].data==NULL) { Int3(); return; } if (!forever) { // Hack to keep sounds from building up... const auto &&[volume, pan] = digi_get_sound_loc(viewer.orient, viewer.pos, segnum.absolute_sibling(viewer.segnum), pos, segnum, max_volume, max_distance); digi_play_sample_3d(org_soundnum, pan, volume); return; } auto f = find_sound_object_flags0(SoundObjects); if (f.first == f.second) return; auto &so = *f.first; so.flags = SOF_USED | SOF_LINK_TO_POS; so.link_type.pos.segnum = segnum; so.link_type.pos.sidenum = sidenum; so.link_type.pos.position = pos; so.loop_start = so.loop_end = -1; digi_link_sound_common(viewer, so, pos, forever, max_volume, max_distance, soundnum, segnum); } } void digi_link_sound_to_pos(const unsigned soundnum, const vcsegptridx_t segnum, const sidenum_t sidenum, const vms_vector &pos, const int forever, const fix max_volume) { digi_link_sound_to_pos2(soundnum, segnum, sidenum, pos, forever, max_volume, vm_distance{F1_0 * 256}); } //if soundnum==-1, kill any sound void digi_kill_sound_linked_to_segment(const vmsegidx_t segnum, const sidenum_t sidenum, int soundnum) { if (soundnum != -1) soundnum = digi_xlat_sound(soundnum); range_for (auto &i, SoundObjects) { if ( (i.flags & SOF_USED) && (i.flags & SOF_LINK_TO_POS) ) { if ((i.link_type.pos.segnum == segnum) && (i.link_type.pos.sidenum==sidenum) && (soundnum==-1 || i.soundnum==soundnum )) { digi_kill_sound(i); } } } } void digi_kill_sound_linked_to_object(const vcobjptridx_t objnum) { if ( Newdemo_state == ND_STATE_RECORDING ) { newdemo_record_kill_sound_linked_to_object( objnum ); } range_for (auto &i, SoundObjects) { if ( (i.flags & SOF_USED) && (i.flags & SOF_LINK_TO_OBJ ) ) { if (i.link_type.obj.objnum == objnum) { digi_kill_sound(i); } } } } namespace { // John's new function, 2/22/96. static void digi_record_sound_objects() { range_for (auto &s, SoundObjects) { if ((s.flags & SOF_USED) && (s.flags & SOF_LINK_TO_OBJ) && (s.flags & SOF_PLAY_FOREVER)) { newdemo_record_link_sound_to_object3( digi_unxlat_sound(s.soundnum), s.link_type.obj.objnum, s.max_volume, s.max_distance, s.loop_start, s.loop_end ); } } } static int was_recording = 0; } void digi_sync_sounds() { int oldvolume; if ( Newdemo_state == ND_STATE_RECORDING) { if ( !was_recording ) { digi_record_sound_objects(); } was_recording = 1; } else { was_recording = 0; } SoundQ_process(); if (!Viewer) return; auto &Objects = LevelUniqueObjectState.Objects; auto &vcobjptr = Objects.vcptr; const auto viewer = Viewer; range_for (auto &s, SoundObjects) { if (s.flags & SOF_USED) { oldvolume = s.volume; const auto oldpan = s.pan; if ( !(s.flags & SOF_PLAY_FOREVER) ) { // Check if its done. if (s.channel > -1 ) { if ( !digi_is_channel_playing(s.channel) ) { digi_end_sound( s.channel ); s.flags = 0; // Mark as dead, so some other sound can use this sound N_active_sound_objects--; continue; // Go on to next sound... } } } if ( s.flags & SOF_LINK_TO_POS ) { digi_update_sound_loc(viewer->orient, viewer->pos, vcsegptridx(viewer->segnum), s.link_type.pos.position, vcsegptridx(s.link_type.pos.segnum), s); } else if ( s.flags & SOF_LINK_TO_OBJ ) { const object &objp = [&vcobjptr, &s]{ if (Newdemo_state != ND_STATE_PLAYBACK) return vcobjptr(s.link_type.obj.objnum); auto objnum = newdemo_find_object(s.link_type.obj.objsignature); if (objnum != object_none) return static_cast(objnum); return vcobjptr(object_first); }(); if ((objp.type==OBJ_NONE) || (objp.signature!=s.link_type.obj.objsignature)) { // The object that this is linked to is dead, so just end this sound if it is looping. if ( s.channel>-1 ) { if (s.flags & SOF_PLAY_FOREVER) digi_stop_sound( s.channel ); else digi_end_sound( s.channel ); N_active_sound_objects--; } s.flags = 0; // Mark as dead, so some other sound can use this sound continue; // Go on to next sound... } else { digi_update_sound_loc(viewer->orient, viewer->pos, vcsegptridx(viewer->segnum), objp.pos, vcsegptridx(objp.segnum), s); } } if (oldvolume != s.volume) { if ( s.volume < 1 ) { // Sound is too far away, so stop it from playing. const auto c = s.channel; if (c > -1) { s.channel = -1; if (s.flags & SOF_PLAY_FOREVER) digi_stop_sound(c); else digi_end_sound(c); N_active_sound_objects--; } if (! (s.flags & SOF_PLAY_FOREVER)) { s.flags = 0; // Mark as dead, so some other sound can use this sound continue; } } else { if (s.channel<0) { digi_start_sound_object(s); } else { digi_set_channel_volume( s.channel, s.volume ); } } } if (oldpan != s.pan) { if (s.channel>-1) digi_set_channel_pan( s.channel, s.pan ); } } } } void digi_pause_digi_sounds() { digi_pause_looping_sound(); range_for (auto &s, SoundObjects) { if (!(s.flags & SOF_USED)) continue; const auto c = s.channel; if (c > -1) { s.channel = -1; if (! (s.flags & SOF_PLAY_FOREVER)) s.flags = 0; // Mark as dead, so some other sound can use this sound N_active_sound_objects--; digi_stop_sound(c); } } digi_stop_all_channels(); SoundQ_pause(); } void digi_resume_digi_sounds() { digi_sync_sounds(); //don't think we really need to do this, but can't hurt digi_unpause_looping_sound(); } // Called by the code in digi.c when another sound takes this sound object's // slot because the sound was done playing. void digi_end_soundobj(sound_object &s) { Assert(s.flags & SOF_USED); Assert(s.channel > -1); N_active_sound_objects--; s.channel = -1; } void digi_stop_digi_sounds() { digi_stop_looping_sound(); range_for (auto &s, SoundObjects) { if (s.flags & SOF_USED) { if ( s.channel > -1 ) { digi_stop_sound( s.channel ); N_active_sound_objects--; } s.flags = 0; // Mark as dead, so some other sound can use this sound } } digi_stop_all_channels(); SoundQ_init(); } #ifndef NDEBUG int verify_sound_channel_free( int channel ) { const auto predicate = [channel](const sound_object &s) { return (s.flags & SOF_USED) && s.channel == channel; }; if (std::any_of(SoundObjects.begin(), SoundObjects.end(), predicate)) throw std::runtime_error("sound busy"); return 0; } #endif int SoundQ_channel; namespace { struct sound_q { fix64 time_added; int soundnum; }; #define MAX_LIFE F1_0*30 // After being queued for 30 seconds, don't play it static int SoundQ_head, SoundQ_tail, SoundQ_num; static std::array SoundQ; void SoundQ_init() { SoundQ_head = SoundQ_tail = 0; SoundQ_num = 0; SoundQ_channel = -1; } void SoundQ_pause() { SoundQ_channel = -1; } } void SoundQ_end() { // Current playing sound is stopped, so take it off the Queue SoundQ_head = (SoundQ_head+1); if (SoundQ_head >= SoundQ.size()) SoundQ_head = 0; SoundQ_num--; SoundQ_channel = -1; } namespace { void SoundQ_process() { if ( SoundQ_channel > -1 ) { if ( digi_is_channel_playing(SoundQ_channel) ) return; SoundQ_end(); } while ( SoundQ_head != SoundQ_tail ) { sound_q * q = &SoundQ[SoundQ_head]; if ( q->time_added+MAX_LIFE > timer_query() ) { SoundQ_channel = digi_start_sound(q->soundnum, F1_0+1, sound_pan{0x7fff}, 0, -1, -1, sound_object_none); return; } else { // expired; remove from Queue SoundQ_end(); } } } } void digi_start_sound_queued( short soundnum, fix volume ) { int i; soundnum = digi_xlat_sound(soundnum); if (soundnum < 0 ) return; i = SoundQ_tail+1; if (i >= SoundQ.size()) i = 0; // Make sure its loud so it doesn't get cancelled! if ( volume < F1_0+1 ) volume = F1_0 + 1; if ( i != SoundQ_head ) { SoundQ[SoundQ_tail].time_added = timer_query(); SoundQ[SoundQ_tail].soundnum = soundnum; SoundQ_num++; SoundQ_tail = i; } // Try to start it! SoundQ_process(); } }