diff --git a/Button.cpp b/Button.cpp index 0636888b..f87ebd90 100644 --- a/Button.cpp +++ b/Button.cpp @@ -85,7 +85,7 @@ TButton::Load_mapping( cParser &Input ) { // token can be a key or block end std::string const key { Input.getToken( true, "\n\r\t ,;" ) }; - if( key == "}" ) { return false; } + if( ( true == key.empty() ) || ( key == "}" ) ) { return false; } // if not block end then the key is followed by assigned value or sub-block if( key == "soundinc:" ) { m_soundfxincrease.deserialize( Input, sound_type::single ); } else if( key == "sounddec:" ) { m_soundfxdecrease.deserialize( Input, sound_type::single ); } diff --git a/Driver.cpp b/Driver.cpp index d66073a7..07342b5a 100644 --- a/Driver.cpp +++ b/Driver.cpp @@ -2368,19 +2368,25 @@ bool TController::ReleaseEngine() else OK = true; } - else if (mvOccupied->ActiveDir == 0) - OK = mvControlling->Mains; // tylko to testujemy dla pojazdu człowieka - if (AIControllFlag) - if (!mvOccupied->DecBrakeLevel()) // tu moze zmieniać na -2, ale to bez znaczenia - if (!mvOccupied->IncLocalBrakeLevel(1)) - { - while (DecSpeed(true)) + else if( mvOccupied->ActiveDir == 0 ) { + // tylko to testujemy dla pojazdu człowieka + OK = mvControlling->Mains; + } + + if( AIControllFlag ) { + mvOccupied->BrakeReleaser( 0 ); + if( !mvOccupied->DecBrakeLevel() ) { + // tu moze zmieniać na -2, ale to bez znaczenia + if( !mvOccupied->IncLocalBrakeLevel( 1 ) ) { + while( DecSpeed( true ) ) ; // zerowanie nastawników - while (mvOccupied->ActiveDir > 0) + while( mvOccupied->ActiveDir > 0 ) mvOccupied->DirectionBackward(); - while (mvOccupied->ActiveDir < 0) + while( mvOccupied->ActiveDir < 0 ) mvOccupied->DirectionForward(); } + } + } OK = OK && (mvOccupied->Vel < 0.01); if (OK) { // jeśli się zatrzymał @@ -3108,7 +3114,7 @@ bool TController::PutCommand( std::string NewCommand, double NewValue1, double N iGuardRadio = 0; // nie przez radio } else { - NewCommand = NewCommand.insert(NewCommand.find_last_of("."),"radio"); // wstawienie przed kropkč + NewCommand = NewCommand.insert(NewCommand.rfind('.'),"radio"); // wstawienie przed kropkč if (FileExists(NewCommand)) { // wczytanie dźwięku odjazdu w wersji radiowej (słychać tylko w kabinie) #ifdef EU07_USE_OLD_SOUNDCODE diff --git a/DynObj.cpp b/DynObj.cpp index 076dd4c2..5a0d5017 100644 --- a/DynObj.cpp +++ b/DynObj.cpp @@ -4116,7 +4116,6 @@ void TDynamicObject::RenderSounds() { else { sHorn2.stop(); } - // szum w czasie jazdy if( ( GetVelocity() > 0.5 ) && ( // compound test whether the vehicle belongs to user-driven consist (as these don't emit outer noise in cab view) @@ -4183,15 +4182,17 @@ void TDynamicObject::RenderSounds() { interpolate( 0.0, 1.0, clamp( - MoverParameters->Vel / 60.0, + MoverParameters->Vel / 40.0, 0.0, 1.0 ) ); + rsOuterNoise - .pitch( clamp( frequency, 0.5, 1.15 ) ) // arbitrary limits to prevent the pitch going out of whack + .pitch( frequency ) // arbitrary limits to prevent the pitch going out of whack .gain( volume ) .play( sound_flags::exclusive | sound_flags::looping ); } else { - rsOuterNoise.stop(); + // don't play the optional ending sound if the listener switches views + rsOuterNoise.stop( false == FreeFlyModeFlag ); } // youBy: dzwiek ostrych lukow i ciasnych zwrotek @@ -4312,7 +4313,6 @@ void TDynamicObject::RenderSounds() { MoverParameters->EventFlag = false; } - } // McZapkie-250202 @@ -5062,14 +5062,13 @@ void TDynamicObject::LoadMMediaFile( std::string BaseDir, std::string TypeName, else if( ( token == "transmission:" ) && ( MoverParameters->EngineType == ElectricSeriesMotor ) ) { // plik z dzwiekiem, mnozniki i ofsety amp. i czest. - rsPrzekladnia.deserialize( parser, sound_type::single, sound_parameters::range ); - - // NOTE, TODO: fixed parameters, put them into simulation-side calculations + // NOTE, fixed default parameters, legacy system leftover rsPrzekladnia.m_amplitudefactor = 0.029; rsPrzekladnia.m_amplitudeoffset = 0.1; rsPrzekladnia.m_frequencyfactor = 0.005; rsPrzekladnia.m_frequencyoffset = 1.0; + rsPrzekladnia.deserialize( parser, sound_type::single, sound_parameters::range ); rsPrzekladnia.owner( this ); } diff --git a/Globals.cpp b/Globals.cpp index 9293298c..c011d925 100644 --- a/Globals.cpp +++ b/Globals.cpp @@ -134,7 +134,7 @@ bool Global::bWireFrame = false; // sound renderer bool Global::bSoundEnabled = true; -float Global::AudioVolume = 2.0f; +float Global::AudioVolume = 1.5f; std::string Global::AudioRenderer; int Global::iWriteLogEnabled = 3; // maska bitowa: 1-zapis do pliku, 2-okienko, 4-nazwy torów diff --git a/Train.cpp b/Train.cpp index dd43dbf1..cbcb6666 100644 --- a/Train.cpp +++ b/Train.cpp @@ -5139,14 +5139,15 @@ TTrain::update_sounds( double const Deltatime ) { // Winger-160404 - syczenie pomocniczego (luzowanie) if( m_lastlocalbrakepressure != -1.f ) { // calculate rate of pressure drop in local brake cylinder, once it's been initialized - auto const brakepressuredifference{ m_lastlocalbrakepressure - mvOccupied->LocBrakePress }; + auto const brakepressuredifference { mvOccupied->LocBrakePress - m_lastlocalbrakepressure }; m_localbrakepressurechange = interpolate( m_localbrakepressurechange, 10 * ( brakepressuredifference / Deltatime ), 0.1f ); } m_lastlocalbrakepressure = mvOccupied->LocBrakePress; - if( ( m_localbrakepressurechange > 0.05f ) + // local brake, release + if( ( m_localbrakepressurechange < -0.05f ) && ( mvOccupied->LocBrakePress > mvOccupied->BrakePress - 0.05 ) ) { rsSBHiss - .gain( clamp( 0.05 * m_localbrakepressurechange, 0.0, 1.5 ) ) + .gain( clamp( rsSBHiss.m_amplitudeoffset + rsSBHiss.m_amplitudefactor * -m_localbrakepressurechange * 0.05, 0.0, 1.5 ) ) .play( sound_flags::exclusive | sound_flags::looping ); } else { @@ -5157,6 +5158,20 @@ TTrain::update_sounds( double const Deltatime ) { rsSBHiss.stop(); } } + // local brake, engage + if( m_localbrakepressurechange > 0.05f ) { + rsSBHissU + .gain( clamp( rsSBHissU.m_amplitudeoffset + rsSBHissU.m_amplitudefactor * m_localbrakepressurechange * 0.05, 0.0, 1.5 ) ) + .play( sound_flags::exclusive | sound_flags::looping ); + } + else { + // don't stop the sound too abruptly + volume = std::max( 0.0, rsSBHissU.gain() - 0.1 * Deltatime ); + rsSBHissU.gain( volume ); + if( volume < 0.05 ) { + rsSBHissU.stop(); + } + } // McZapkie-280302 - syczenie // TODO: softer volume reduction than plain abrupt stop, perhaps as reusable wrapper? @@ -5290,12 +5305,13 @@ TTrain::update_sounds( double const Deltatime ) { mvOccupied->Vel / 40.0, 0.0, 1.0 ) ); rsRunningNoise - .pitch( clamp( frequency, 0.5, 1.15 ) ) // arbitrary limits to prevent the pitch going out of whack + .pitch( frequency ) .gain( volume ) .play( sound_flags::exclusive | sound_flags::looping ); } else { - rsRunningNoise.stop(); + // don't play the optional ending sound if the listener switches views + rsRunningNoise.stop( true == FreeFlyModeFlag ); } // McZapkie-141102: SHP i czuwak, TODO: sygnalizacja kabinowa @@ -5448,6 +5464,10 @@ bool TTrain::LoadMMediaFile(std::string const &asFileName) // syk: rsHissU.deserialize( parser, sound_type::single, sound_parameters::amplitude ); rsHissU.owner( DynamicObject ); + if( true == rsSBHissU.empty() ) { + // fallback for vehicles without defined local brake hiss sound + rsSBHissU = rsHissU; + } } else if (token == "airsound3:") { @@ -5473,6 +5493,11 @@ bool TTrain::LoadMMediaFile(std::string const &asFileName) rsSBHiss.deserialize( parser, sound_type::single, sound_parameters::amplitude ); rsSBHiss.owner( DynamicObject ); } + else if( token == "localbrakesound2:" ) { + // syk: + rsSBHissU.deserialize( parser, sound_type::single, sound_parameters::amplitude ); + rsSBHissU.owner( DynamicObject ); + } else if (token == "fadesound:") { // ambient sound: @@ -5537,7 +5562,7 @@ bool TTrain::InitializeCab(int NewCabNo, std::string const &asFileName) std::vector sounds = { &dsbReverserKey, &dsbNastawnikJazdy, &dsbNastawnikBocz, &dsbSwitch, &dsbPneumaticSwitch, - &rsHiss, &rsHissU, &rsHissE, &rsHissX, &rsHissT, &rsSBHiss, + &rsHiss, &rsHissU, &rsHissE, &rsHissX, &rsHissT, &rsSBHiss, &rsSBHissU, &rsFadeSound, &rsRunningNoise, &dsbHasler, &dsbBuzzer, &dsbSlipAlarm }; @@ -5723,9 +5748,19 @@ bool TTrain::InitializeCab(int NewCabNo, std::string const &asFileName) if( dsbBuzzer.offset() == nullvector ) { dsbBuzzer.offset( btLampkaCzuwaka.model_offset() ); } + auto const localbrakeoffset { ggLocalBrake.model_offset() }; + std::vector localbrakesounds = { + &rsSBHiss, &rsSBHissU + }; + for( auto sound : localbrakesounds ) { + if( sound->offset() == nullvector ) { + sound->offset( localbrakeoffset ); + } + } + // NOTE: if the local brake model can't be located the emitter will also be assigned location of main brake auto const brakeoffset { ggBrakeCtrl.model_offset() }; std::vector brakesounds = { - &rsHiss, &rsHissU, &rsHissE, &rsHissX, &rsHissT, &rsSBHiss, + &rsHiss, &rsHissU, &rsHissE, &rsHissX, &rsHissT, &rsSBHiss, &rsSBHissU, }; for( auto sound : brakesounds ) { if( sound->offset() == nullvector ) { diff --git a/Train.h b/Train.h index 80a950b3..57a33920 100644 --- a/Train.h +++ b/Train.h @@ -410,11 +410,12 @@ public: // reszta może by?publiczna sound_source rsHissX { sound_placement::internal, EU07_SOUND_CABCONTROLSCUTOFFRANGE }; // fala sound_source rsHissT { sound_placement::internal, EU07_SOUND_CABCONTROLSCUTOFFRANGE }; // czasowy sound_source rsSBHiss { sound_placement::internal, EU07_SOUND_CABCONTROLSCUTOFFRANGE }; // local + sound_source rsSBHissU { sound_placement::internal, EU07_SOUND_CABCONTROLSCUTOFFRANGE }; // local, engage brakes float m_lastlocalbrakepressure { -1.f }; // helper, cached level of pressure in local brake cylinder float m_localbrakepressurechange { 0.f }; // recent change of pressure in local brake cylinder sound_source rsFadeSound { sound_placement::internal, EU07_SOUND_CABCONTROLSCUTOFFRANGE }; - sound_source rsRunningNoise{ sound_placement::internal, 2.0 * EU07_SOUND_CABCONTROLSCUTOFFRANGE }; + sound_source rsRunningNoise{ sound_placement::internal, 2 * EU07_SOUND_CABCONTROLSCUTOFFRANGE }; sound_source dsbHasler { sound_placement::internal, EU07_SOUND_CABCONTROLSCUTOFFRANGE }; sound_source dsbBuzzer { sound_placement::internal, EU07_SOUND_CABCONTROLSCUTOFFRANGE }; diff --git a/audiorenderer.cpp b/audiorenderer.cpp index c25390cb..2a7dcbdb 100644 --- a/audiorenderer.cpp +++ b/audiorenderer.cpp @@ -59,14 +59,14 @@ openal_source::update( double const Deltatime ) { if( id != audio::null_resource ) { - ::alGetSourcei( id, AL_BUFFERS_PROCESSED, &buffer_index ); + ::alGetSourcei( id, AL_BUFFERS_PROCESSED, &sound_index ); // for multipart sounds trim away processed sources until only one remains, the last one may be set to looping by the controller ALuint bufferid; - while( ( buffer_index > 0 ) - && ( buffers.size() > 1 ) ) { + while( ( sound_index > 0 ) + && ( sounds.size() > 1 ) ) { ::alSourceUnqueueBuffers( id, 1, &bufferid ); - buffers.erase( std::begin( buffers ) ); - --buffer_index; + sounds.erase( std::begin( sounds ) ); + --sound_index; } int state; @@ -84,7 +84,7 @@ openal_source::sync_with( sound_properties const &State ) { if( id == audio::null_resource ) { // no implementation-side source to match, return sync error so the controller can clean up on its end - is_synced = false; + sync = sync_state::bad_resource; return; } /* @@ -100,10 +100,10 @@ openal_source::sync_with( sound_properties const &State ) { auto const cutoffrange = ( is_multipart ? EU07_SOUND_CUTOFFRANGE : // we keep multi-part sounds around longer, to minimize restarts as the sounds get out and back in range - sound_range * 5.0f ); + sound_range * 7.5f ); if( glm::length2( sound_distance ) > std::min( ( cutoffrange * cutoffrange ), ( EU07_SOUND_CUTOFFRANGE * EU07_SOUND_CUTOFFRANGE ) ) ) { stop(); - is_synced = false; // flag sync failure for the controller + sync = sync_state::bad_distance; // flag sync failure for the controller return; } } @@ -147,9 +147,9 @@ openal_source::sync_with( sound_properties const &State ) { // pitch value has changed properties.pitch = State.pitch; - ::alSourcef( id, AL_PITCH, properties.pitch * pitch_variation ); + ::alSourcef( id, AL_PITCH, clamp( properties.pitch * pitch_variation, 0.1f, 10.f ) ); } - is_synced = true; + sync = sync_state::good; } // sets max audible distance for sounds emitted by the source @@ -205,7 +205,7 @@ openal_source::clear() { stop(); // ...prepare space for returned ids of unqueued buffers (not that we need that info)... std::vector bufferids; - bufferids.resize( buffers.size() ); + bufferids.resize( sounds.size() ); // ...release the buffers... ::alSourceUnqueueBuffers( id, bufferids.size(), bufferids.data() ); } @@ -260,17 +260,6 @@ openal_renderer::init() { return true; } -// schedules playback of specified sample, under control of the specified emitter -void -openal_renderer::insert( sound_source *Controller, audio::buffer_handle const Sound ) { - - audio::openal_source::buffer_sequence buffers { Sound }; - return - insert( - Controller, - std::begin( buffers ), std::end( buffers ) ); -} - // removes from the queue all sounds controlled by the specified sound emitter void openal_renderer::erase( sound_source const *Controller ) { diff --git a/audiorenderer.h b/audiorenderer.h index efed232f..a6864987 100644 --- a/audiorenderer.h +++ b/audiorenderer.h @@ -14,6 +14,8 @@ http://mozilla.org/MPL/2.0/. class sound_source; +using uint32_sequence = std::vector; + // sound emitter state sync item struct sound_properties { glm::dvec3 location; @@ -23,6 +25,12 @@ struct sound_properties { float pitch { 1.f }; }; +enum class sync_state { + good, + bad_distance, + bad_resource +}; + namespace audio { // implementation part of the sound emitter @@ -37,27 +45,30 @@ struct openal_source { // members ALuint id { audio::null_resource }; // associated AL resource sound_source *controller { nullptr }; // source controller - buffer_sequence buffers; // sequence of samples the source will emit - int buffer_index { 0 }; // currently queued sample from the buffer sequence + uint32_sequence sounds; // +// buffer_sequence buffers; // sequence of samples the source will emit + int sound_index { 0 }; // currently queued sample from the buffer sequence bool is_playing { false }; bool is_looping { false }; - bool is_synced { true }; // set to false only if a sync attempt fails sound_properties properties; + sync_state sync { sync_state::good }; // methods template openal_source & - bind( sound_source *Controller, Iterator_ First, Iterator_ Last ) { + bind( sound_source *Controller, uint32_sequence Sounds, Iterator_ First, Iterator_ Last ) { controller = Controller; - buffers.insert( std::end( buffers ), First, Last ); - is_multipart = ( buffers.size() > 1 ); + sounds = Sounds; // look up and queue assigned buffers - std::vector bufferids; - for( auto const buffer : buffers ) { - bufferids.emplace_back( audio::renderer.buffer( buffer ).id ); } + std::vector buffers; + std::for_each( + First, Last, + [&]( audio::buffer_handle const &buffer ) { + buffers.emplace_back( audio::renderer.buffer( buffer ).id ); } ); if( id != audio::null_resource ) { - ::alSourceQueueBuffers( id, static_cast( bufferids.size() ), bufferids.data() ); + ::alSourceQueueBuffers( id, static_cast( buffers.size() ), buffers.data() ); ::alSourceRewind( id ); } + is_multipart = ( buffers.size() > 1 ); return *this; } // starts playback of queued buffers void @@ -119,11 +130,8 @@ public: // schedules playback of provided range of samples, under control of the specified sound emitter template void - insert( sound_source *Controller, Iterator_ First, Iterator_ Last ) { - m_sources.emplace_back( fetch_source().bind( Controller, First, Last ) ); } - // schedules playback of specified sample, under control of the specified sound emitter - void - insert( sound_source *Controller, audio::buffer_handle const Sound ); + insert( Iterator_ First, Iterator_ Last, sound_source *Controller, uint32_sequence Sounds ) { + m_sources.emplace_back( fetch_source().bind( Controller, Sounds, First, Last ) ); } // removes from the queue all sounds controlled by the specified sound emitter void erase( sound_source const *Controller ); diff --git a/parser.cpp b/parser.cpp index 52cde35b..d8ccec70 100644 --- a/parser.cpp +++ b/parser.cpp @@ -109,9 +109,21 @@ cParser::getToken( bool const ToLower, const char *Break ) { } // methods +cParser & +cParser::autoclear( bool const Autoclear ) { + + m_autoclear = Autoclear; + if( mIncludeParser ) { mIncludeParser->autoclear( Autoclear ); } + + return *this; +} + bool cParser::getTokens(unsigned int Count, bool ToLower, const char *Break) { - tokens.clear(); // emulates old parser behaviour. TODO, TBD: allow manual reset? + if( true == m_autoclear ) { + // legacy parser behaviour + tokens.clear(); + } /* if (LoadTraction==true) trtest="niemaproblema"; //wczytywać @@ -123,7 +135,7 @@ bool cParser::getTokens(unsigned int Count, bool ToLower, const char *Break) this->str(""); this->clear(); */ - for (unsigned int i = 0; i < Count; ++i) + for (unsigned int i = tokens.size(); i < Count; ++i) { std::string token = readToken(ToLower, Break); if( true == token.empty() ) { @@ -215,6 +227,7 @@ std::string cParser::readToken( bool ToLower, const char *Break ) { parameter = readToken( false ); } mIncludeParser = std::make_shared( includefile, buffer_FILE, mPath, LoadTraction, includeparameters ); + mIncludeParser->autoclear( m_autoclear ); if( mIncludeParser->mSize <= 0 ) { ErrorLog( "Bad include: can't open file \"" + includefile + "\"" ); } diff --git a/parser.h b/parser.h index 4799a7d2..3c3ee511 100644 --- a/parser.h +++ b/parser.h @@ -61,6 +61,11 @@ class cParser //: public std::stringstream bool ok() { return !mStream->fail(); }; + cParser & + autoclear( bool const Autoclear ); + bool + autoclear() const { + return m_autoclear; } bool getTokens( unsigned int Count = 1, bool ToLower = true, char const *Break = "\n\r\t ;" ); // returns next incoming token, if any, without removing it from the set @@ -91,6 +96,7 @@ class cParser //: public std::stringstream bool trimComments( std::string &String ); std::size_t count(); // members: + bool m_autoclear { true }; // not retrieved tokens are discarded when another read command is issued (legacy behaviour) bool LoadTraction; // load traction? std::shared_ptr mStream; // relevant kind of buffer is attached on creation. std::string mFile; // name of the open file, if any diff --git a/sound.cpp b/sound.cpp index fb8cfcfb..58cbdad3 100644 --- a/sound.cpp +++ b/sound.cpp @@ -37,41 +37,85 @@ sound_source::deserialize( std::string const &Input, sound_type const Legacytype sound_source & sound_source::deserialize( cParser &Input, sound_type const Legacytype, int const Legacyparameters ) { - // TODO: implement block type config parsing - switch( Legacytype ) { - case sound_type::single: { - // single sample only - m_soundmain.buffer = audio::renderer.fetch_buffer( deserialize_filename( Input ) ); - break; - } - case sound_type::multipart: { - // three samples: start, middle, stop - m_soundbegin.buffer = audio::renderer.fetch_buffer( deserialize_filename( Input ) ); - m_soundmain.buffer = audio::renderer.fetch_buffer( deserialize_filename( Input ) ); - m_soundend.buffer = audio::renderer.fetch_buffer( deserialize_filename( Input ) ); - break; - } - default: { - break; - } - } + // cache parser config, as it may change during deserialization + auto const inputautoclear { Input.autoclear() }; - if( Legacyparameters & sound_parameters::range ) { - Input.getTokens( 1, false ); - Input >> m_range; + Input.getTokens( 1, true, "\n\r\t ,;" ); + if( Input.peek() == "{" ) { + // block type config + while( true == deserialize_mapping( Input ) ) { + ; // all work done by while() + } + + if( false == m_soundchunks.empty() ) { + // arrange loaded sound chunks in requested order + std::sort( + std::begin( m_soundchunks ), std::end( m_soundchunks ), + []( soundchunk_pair const &Left, soundchunk_pair const &Right ) { + return ( Left.second.threshold < Right.second.threshold ); } ); + // calculate and cache full range points for each chunk, including crossfade sections: + // on the far end the crossfade section extends to the threshold point of the next chunk... + for( std::size_t idx = 0; idx < m_soundchunks.size() - 1; ++idx ) { + m_soundchunks[ idx ].second.fadeout = m_soundchunks[ idx + 1 ].second.threshold; + } + // ...and on the other end from the threshold point back into the range of previous chunk + for( std::size_t idx = 1; idx < m_soundchunks.size(); ++idx ) { + auto const previouschunkwidth { m_soundchunks[ idx ].second.threshold - m_soundchunks[ idx - 1 ].second.threshold }; + m_soundchunks[ idx ].second.fadein = m_soundchunks[ idx ].second.threshold - 0.01f * m_crossfaderange * previouschunkwidth; + } + m_soundchunks.back().second.fadeout = std::max( m_soundchunks.back().second.threshold, 100 ); + // test if the chunk table contains any actual samples while at it + for( auto &soundchunk : m_soundchunks ) { + if( soundchunk.first.buffer != null_handle ) { + m_soundchunksempty = false; + break; + } + } + } } - if( Legacyparameters & sound_parameters::amplitude ) { - Input.getTokens( 2, false ); - Input - >> m_amplitudefactor - >> m_amplitudeoffset; - } - if( Legacyparameters & sound_parameters::frequency ) { - Input.getTokens( 2, false ); - Input - >> m_frequencyfactor - >> m_frequencyoffset; + else { + // legacy type config + + // set the parser to preserve retrieved tokens, so we don't need to mess with separately passing the initial read + Input.autoclear( false ); + + switch( Legacytype ) { + case sound_type::single: { + // single sample only + m_sounds[ main ].buffer = audio::renderer.fetch_buffer( deserialize_filename( Input ) ); + break; + } + case sound_type::multipart: { + // three samples: start, middle, stop + for( auto &sound : m_sounds ) { + sound.buffer = audio::renderer.fetch_buffer( deserialize_filename( Input ) ); + } + break; + } + default: { + break; + } + } + + if( Legacyparameters & sound_parameters::range ) { + Input.getTokens( 1, false ); + Input >> m_range; + } + if( Legacyparameters & sound_parameters::amplitude ) { + Input.getTokens( 2, false ); + Input + >> m_amplitudefactor + >> m_amplitudeoffset; + } + if( Legacyparameters & sound_parameters::frequency ) { + Input.getTokens( 2, false ); + Input + >> m_frequencyfactor + >> m_frequencyoffset; + } } + // restore parser behaviour + Input.autoclear( inputautoclear ); return *this; } @@ -85,11 +129,11 @@ sound_source::deserialize_filename( cParser &Input ) { // simple case, single file return token; } - // if instead of filename we've encountered '[' this marks beginning of random sound set - // we retrieve all filenames from the set, then return a random one + // if instead of filename we've encountered '[' this marks a beginning of random sounds + // we retrieve all entries, then return a random one std::vector filenames; while( ( ( token = Input.getToken( true, "\n\r\t ,;" ) ) != "" ) - && ( token != "]" ) ) { + && ( token != "]" ) ) { filenames.emplace_back( token ); } if( false == filenames.empty() ) { @@ -102,6 +146,127 @@ sound_source::deserialize_filename( cParser &Input ) { } } +// imports member data pair from the config file +bool +sound_source::deserialize_mapping( cParser &Input ) { + // token can be a key or block end + std::string const key { Input.getToken( true, "\n\r\t ,;[]" ) }; + + if( ( true == key.empty() ) || ( key == "}" ) ) { return false; } + + // if not block end then the key is followed by assigned value or sub-block + if( key == "soundmain:" ) { + sound( sound_id::main ).buffer = audio::renderer.fetch_buffer( deserialize_filename( Input ) ); + } + else if( key == "soundset:" ) { + deserialize_soundset( Input ); + } + else if( key == "soundbegin:" ) { + sound( sound_id::begin ).buffer = audio::renderer.fetch_buffer( deserialize_filename( Input ) ); + } + else if( key == "soundend:" ) { + sound( sound_id::end ).buffer = audio::renderer.fetch_buffer( deserialize_filename( Input ) ); + } + else if( key.compare( 0, std::min( key.size(), 5 ), "sound" ) == 0 ) { + // sound chunks, defined with key soundX where X = activation threshold + auto const indexstart { key.find_first_of( "1234567890" ) }; + auto const indexend { key.find_first_not_of( "1234567890", indexstart ) }; + if( indexstart != std::string::npos ) { + // NOTE: we'll sort the chunks at the end of deserialization + m_soundchunks.emplace_back( + soundchunk_pair { + // sound data + { audio::renderer.fetch_buffer( deserialize_filename( Input ) ), 0 }, + // chunk data + { std::stoi( key.substr( indexstart, indexend - indexstart ) ), 0, 0, 1.f } } ); + } + } + else if( key.compare( 0, std::min( key.size(), 5 ), "pitch" ) == 0 ) { + // sound chunk pitch, defined with key pitchX where X = activation threshold + auto const indexstart { key.find_first_of( "1234567890" ) }; + auto const indexend { key.find_first_not_of( "1234567890", indexstart ) }; + if( indexstart != std::string::npos ) { + auto const index { std::stoi( key.substr( indexstart, indexend - indexstart ) ) }; + auto const pitch { Input.getToken( false, "\n\r\t ,;" ) }; + for( auto &chunk : m_soundchunks ) { + if( chunk.second.threshold == index ) { + chunk.second.pitch = pitch; + break; + } + } + } + } + else if( key == "crossfade:" ) { + // for combined sounds, percentage of assigned range allocated to crossfade sections + Input.getTokens( 1, "\n\r\t ,;" ); + Input >> m_crossfaderange; + m_crossfaderange = clamp( m_crossfaderange, 0, 100 ); + } + else if( key == "placement:" ) { + auto const value { Input.getToken( true, "\n\r\t ,;" ) }; + std::map const placements { + { "internal", sound_placement::internal }, + { "engine", sound_placement::engine }, + { "external", sound_placement::external }, + { "general", sound_placement::general } }; + auto lookup{ placements.find( value ) }; + if( lookup != placements.end() ) { + m_placement = lookup->second; + } + } + else if( key == "offset:" ) { + // point in 3d space, in format [ x, y, z ] + Input.getTokens( 3, false, "\n\r\t ,;[]" ); + Input + >> m_offset.x + >> m_offset.y + >> m_offset.z; + } + else { + // floating point properties + std::map const properties { + { "frequencyfactor:", m_frequencyfactor }, + { "frequencyoffset:", m_frequencyoffset }, + { "amplitudefactor:", m_amplitudefactor }, + { "amplitudeoffset:", m_amplitudeoffset }, + { "range:", m_range } }; + + auto lookup { properties.find( key ) }; + if( lookup != properties.end() ) { + Input.getTokens( 1, false, "\n\r\t ,;" ); + Input >> lookup->second; + } + } + + return true; // return value marks a [ key: value ] pair was extracted, nothing about whether it's recognized +} + +// imports values for initial, main and ending sounds from provided data stream +void +sound_source::deserialize_soundset( cParser &Input ) { + + auto token { Input.getToken( true, "\n\r\t ,;|" ) }; + if( token != "[" ) { + // simple case, basic set of three filenames separated with | + // three samples: start, middle, stop + sound( sound_id::begin ).buffer = audio::renderer.fetch_buffer( token ); + sound( sound_id::main ).buffer = audio::renderer.fetch_buffer( Input.getToken( true, "\n\r\t ,;|" ) ); + sound( sound_id::end ).buffer = audio::renderer.fetch_buffer( Input.getToken( true, "\n\r\t ,;|" ) ); + return; + } + // if instead of filename we've encountered '[' this marks a beginning of random sets + // we retrieve all entries, then process a random one + std::vector soundsets; + while( ( ( token = Input.getToken( true, "\n\r\t ,;" ) ) != "" ) + && ( token != "]" ) ) { + soundsets.emplace_back( token ); + } + if( false == soundsets.empty() ) { + std::shuffle( std::begin( soundsets ), std::end( soundsets ), Global::random_engine ); + return deserialize_soundset( cParser( soundsets.front() ) ); + } +} + // issues contextual play commands for the audio renderer void sound_source::play( int const Flags ) { @@ -112,9 +277,11 @@ sound_source::play( int const Flags ) { return; } if( m_range > 0 ) { - auto const cutoffrange{ m_range * 5 }; - if( glm::length2( location() - glm::dvec3{ Global::pCameraPosition } ) > std::min( 2750.f * 2750.f, cutoffrange * cutoffrange ) ) { - // drop sounds from beyond sensible and/or audible range + auto const cutoffrange { m_range * 5 }; + if( glm::length2( location() - glm::dvec3 { Global::pCameraPosition } ) > std::min( 2750.f * 2750.f, cutoffrange * cutoffrange ) ) { + // while we drop sounds from beyond sensible and/or audible range + // we act as if it was activated normally, meaning no need to include the opening bookend in subsequent calls + m_playbeginning = false; return; } } @@ -126,40 +293,106 @@ sound_source::play( int const Flags ) { m_flags = Flags; + if( sound( sound_id::main ).buffer != null_handle ) { + // basic variant: single main sound, with optional bookends + play_basic(); + } + else { + // combined variant, main sound consists of multiple chunks, with optional bookends + play_combined(); + } +} + +void +sound_source::play_basic() { + if( false == is_playing() ) { // dispatch appropriate sound - // TODO: support for parameter-driven sound table - if( m_soundbegin.buffer != null_handle ) { - std::vector bufferlist { m_soundbegin.buffer, m_soundmain.buffer }; - insert( std::begin( bufferlist ), std::end( bufferlist ) ); + if( ( true == m_playbeginning ) + && ( sound( sound_id::begin ).buffer != null_handle ) ) { + std::vector sounds { sound_id::begin, sound_id::main }; + insert( std::begin( sounds ), std::end( sounds ) ); + m_playbeginning = false; } else { - insert( m_soundmain.buffer ); + insert( sound_id::main ); } } else { - - if( ( m_soundbegin.buffer == null_handle ) + // for single part non-looping samples we allow spawning multiple instances, if not prevented by set flags + if( ( sound( sound_id::begin ).buffer == null_handle ) && ( ( m_flags & ( sound_flags::exclusive | sound_flags::looping ) ) == 0 ) ) { - // for single part non-looping samples we allow spawning multiple instances, if not prevented by set flags - insert( m_soundmain.buffer ); + insert( sound_id::main ); + } + } +} + +void +sound_source::play_combined() { + // combined sound consists of table od samples, each sample associated with certain range of values of controlling variable + // current value of the controlling variable is passed to the source with pitch() call + auto const soundpoint { clamp( m_properties.pitch * 100.f, 0.f, 100.f ) }; + for( std::uint32_t idx = 0; idx < m_soundchunks.size(); ++idx ) { + + auto const &soundchunk { m_soundchunks[ idx ] }; + // a chunk covers range from fade in point, where it starts rising in volume over crossfade distance, + // lasts until fadeout - crossfade distance point, past which it grows quiet until fade out point where it ends + if( soundpoint < soundchunk.second.fadein ) { break; } + if( soundpoint > soundchunk.second.fadeout ) { continue; } + + if( ( soundchunk.first.playing > 0 ) + || ( soundchunk.first.buffer == null_handle ) ) { + // combined sounds only play looped, single copy of each activated chunk + continue; + } + + if( idx > 0 ) { + insert( sound_id::chunk | idx ); + } + else { + // initial chunk requires some safety checks if the optional bookend is present, + // so we don't queue another instance while the bookend is still playing + if( sound( sound_id::begin ).buffer == null_handle ) { + // no bookend, safe to play the chunk + insert( sound_id::chunk | idx ); + } + else { + // branches: + // beginning requested, not playing; queue beginning and chunk + // beginning not requested, not playing; queue chunk + // otherwise skip, one instance is already in the audio queue + if( sound( sound_id::begin ).playing == 0 ) { + if( true == m_playbeginning ) { + std::vector sounds{ sound_id::begin, sound_id::chunk | idx }; + insert( std::begin( sounds ), std::end( sounds ) ); + m_playbeginning = false; + } + else { + insert( sound_id::chunk | idx ); + } + } + } } } } // stops currently active play commands controlled by this emitter void -sound_source::stop() { +sound_source::stop( bool const Skipend ) { + + // if the source was stopped on simulation side, we should play the opening bookend next time it's activated + m_playbeginning = true; if( false == is_playing() ) { return; } m_stop = true; - if( ( m_soundend.buffer != null_handle ) - && ( m_soundend.buffer != m_soundmain.buffer ) // end == main can happen in malformed legacy cases - && ( m_soundend.playing == 0 ) ) { + if( ( false == Skipend ) + && ( sound( sound_id::end ).buffer != null_handle ) + && ( sound( sound_id::end ).buffer != sound( sound_id::main ).buffer ) // end == main can happen in malformed legacy cases + && ( sound( sound_id::end ).playing == 0 ) ) { // spawn potentially defined sound end sample, if the emitter is currently active - insert( m_soundend.buffer ); + insert( sound_id::end ); } } @@ -167,48 +400,64 @@ sound_source::stop() { void sound_source::update( audio::openal_source &Source ) { + if( sound( sound_id::main ).buffer != null_handle ) { + // basic variant: single main sound, with optional bookends + update_basic( Source ); + return; + } + if( false == m_soundchunksempty ) { + // combined variant, main sound consists of multiple chunks, with optional bookends + update_combined( Source ); + return; + } +} + +void +sound_source::update_basic( audio::openal_source &Source ) { + if( true == Source.is_playing ) { if( ( true == m_stop ) - && ( Source.buffers[ Source.buffer_index ] != m_soundend.buffer ) ) { + && ( Source.sounds[ Source.sound_index ] != sound_id::end ) ) { // kill the sound if stop was requested, unless it's sound bookend sample Source.stop(); - update_counter( Source.buffers[ Source.buffer_index ], -1 ); + update_counter( Source.sounds[ Source.sound_index ], -1 ); if( false == is_playing() ) { m_stop = false; } return; } - if( m_soundbegin.buffer != null_handle ) { + if( sound( sound_id::begin ).buffer != null_handle ) { // potentially a multipart sound // detect the moment when the sound moves from startup sample to the main + auto const soundhandle { Source.sounds[ Source.sound_index ] }; if( ( false == Source.is_looping ) - && ( Source.buffers[ Source.buffer_index ] == m_soundmain.buffer ) ) { - // when it happens update active sample flags, and activate the looping + && ( soundhandle == sound_id::main ) ) { + // when it happens update active sample counters, and activate the looping + update_counter( sound_id::begin, -1 ); + update_counter( soundhandle, 1 ); Source.loop( true ); - --( m_soundbegin.playing ); - ++( m_soundmain.playing ); } } - // check and update if needed current sound properties update_location(); update_soundproofing(); Source.sync_with( m_properties ); - if( false == Source.is_synced ) { + if( Source.sync != sync_state::good ) { // if the sync went wrong we let the renderer kill its part of the emitter, and update our playcounter(s) to match - update_counter( Source.buffers[ Source.buffer_index ], -1 ); + update_counter( Source.sounds[ Source.sound_index ], -1 ); } } else { // if the emitter isn't playing it's either done or wasn't yet started // we can determine this from number of processed buffers - if( Source.buffer_index != Source.buffers.size() ) { - auto const buffer { Source.buffers[ Source.buffer_index ] }; + if( Source.sound_index != Source.sounds.size() ) { + // the emitter wasn't yet started + auto const soundhandle { Source.sounds[ Source.sound_index ] }; // emitter initialization - if( ( buffer == m_soundmain.buffer ) + if( ( soundhandle == sound_id::main ) && ( true == TestFlag( m_flags, sound_flags::looping ) ) ) { // main sample can be optionally set to loop Source.loop( true ); @@ -218,22 +467,208 @@ sound_source::update( audio::openal_source &Source ) { update_location(); update_soundproofing(); Source.sync_with( m_properties ); - if( true == Source.is_synced ) { + if( Source.sync == sync_state::good ) { // all set, start playback Source.play(); if( false == Source.is_playing ) { // if the playback didn't start update the state counter - update_counter( buffer, -1 ); + update_counter( soundhandle, -1 ); } } else { // if the initial sync went wrong we skip the activation so the renderer can clean the emitter on its end - update_counter( buffer, -1 ); + update_counter( soundhandle, -1 ); } } else { - auto const buffer { Source.buffers[ Source.buffer_index - 1 ] }; - update_counter( buffer, -1 ); + // the emitter is either all done or was terminated early + update_counter( Source.sounds[ Source.sound_index - 1 ], -1 ); + } + } +} + +void +sound_source::update_combined( audio::openal_source &Source ) { + + if( true == Source.is_playing ) { + + auto const soundhandle { Source.sounds[ Source.sound_index ] }; + + if( ( true == m_stop ) + && ( soundhandle != sound_id::end ) ) { + // kill the sound if stop was requested, unless it's sound bookend sample + Source.stop(); + update_counter( soundhandle, -1 ); + if( false == is_playing() ) { + m_stop = false; + } + return; + } + + if( sound( sound_id::begin ).buffer != null_handle ) { + // potentially a multipart sound + // detect the moment when the sound moves from startup sample to the main + auto const soundhandle { Source.sounds[ Source.sound_index ] }; + if( ( false == Source.is_looping ) + && ( soundhandle == ( sound_id::chunk | 0 ) ) ) { + // when it happens update active sample counters, and activate the looping + update_counter( sound_id::begin, -1 ); + update_counter( soundhandle, 1 ); + Source.loop( true ); + } + } + + if( ( soundhandle & sound_id::chunk ) != 0 ) { + // for sound chunks, test whether the chunk should still be active given current value of the controlling variable + auto const soundpoint { clamp( m_properties.pitch * 100.f, 0.f, 100.f ) }; + auto const &soundchunk { m_soundchunks[ soundhandle ^ sound_id::chunk ] }; + if( ( soundpoint < soundchunk.second.fadein ) + || ( soundpoint > soundchunk.second.fadeout ) ) { + Source.stop(); + update_counter( soundhandle, -1 ); + return; + } + } + + // check and update if needed current sound properties + update_location(); + update_soundproofing(); + // pitch and volume are adjusted on per-chunk basis + // since they're relative to base values, backup these... + auto const baseproperties = m_properties; + // ...adjust per-chunk parameters... + update_crossfade( soundhandle ); + // ... pass the parameters to the audio renderer... + Source.sync_with( m_properties ); + if( Source.sync != sync_state::good ) { + // if the sync went wrong we let the renderer kill its part of the emitter, and update our playcounter(s) to match + update_counter( Source.sounds[ Source.sound_index ], -1 ); + } + // ...and restore base properties + m_properties = baseproperties; + } + else { + // if the emitter isn't playing it's either done or wasn't yet started + // we can determine this from number of processed buffers + if( Source.sound_index != Source.sounds.size() ) { + // the emitter wasn't yet started + auto const soundhandle { Source.sounds[ Source.sound_index ] }; + // emitter initialization + if( ( soundhandle != sound_id::begin ) + && ( soundhandle != sound_id::end ) + && ( true == TestFlag( m_flags, sound_flags::looping ) ) ) { + // main sample can be optionally set to loop + Source.loop( true ); + } + Source.range( m_range ); + Source.pitch( m_pitchvariation ); + update_location(); + update_soundproofing(); + // pitch and volume are adjusted on per-chunk basis + auto const baseproperties = m_properties; + update_crossfade( soundhandle ); + Source.sync_with( m_properties ); + if( Source.sync == sync_state::good ) { + // all set, start playback + Source.play(); + if( false == Source.is_playing ) { + // if the playback didn't start update the state counter + update_counter( soundhandle, -1 ); + } + } + else { + // if the initial sync went wrong we skip the activation so the renderer can clean the emitter on its end + update_counter( soundhandle, -1 ); + } + m_properties = baseproperties; + } + else { + // the emitter is either all done or was terminated early + update_counter( Source.sounds[ Source.sound_index - 1 ], -1 ); + } + } +} + +void +sound_source::update_crossfade( sound_handle const Chunk ) { + + if( ( Chunk & sound_id::chunk ) == 0 ) { + // bookend sounds are played at their base pitch + m_properties.pitch = 1.f; + return; + } + + auto const soundpoint { clamp( m_properties.pitch * 100.f, 0.f, 100.f ) }; + + // NOTE: direct access to implementation details ahead, kinda fugly + auto const chunkindex { Chunk ^ sound_id::chunk }; + auto const &chunkdata { m_soundchunks[ chunkindex ].second }; + + // relative pitch adjustment + // pitch of each chunk is modified based on ratio of the chunk's pitch to that of its neighbour + if( soundpoint < chunkdata.threshold ) { + // interpolate between the pitch of previous chunk and this chunk's base pitch, + // based on how far the current soundpoint is in the range of previous chunk + auto const &previouschunkdata{ m_soundchunks[ chunkindex - 1 ].second }; + m_properties.pitch = + interpolate( + previouschunkdata.pitch / chunkdata.pitch, + 1.f, + clamp( + ( soundpoint - previouschunkdata.threshold ) / ( chunkdata.threshold - previouschunkdata.threshold ), + 0.f, 1.f ) ); + } + else { + + if( chunkindex < ( m_soundchunks.size() - 1 ) ) { + // interpolate between this chunk's base pitch and the pitch of next chunk + // based on how far the current soundpoint is in the range of this chunk + auto const &nextchunkdata { m_soundchunks[ chunkindex + 1 ].second }; + m_properties.pitch = + interpolate( + 1.f, + nextchunkdata.pitch / chunkdata.pitch, + clamp( + ( soundpoint - chunkdata.threshold ) / ( nextchunkdata.threshold - chunkdata.threshold ), + 0.f, 1.f ) ); + } + else { + // pitch of the last (or the only) chunk remains fixed throughout + m_properties.pitch = 1.f; + } + } + + // if there's no crossfade sections, our work is done + if( m_crossfaderange == 0 ) { return; } + + if( chunkindex > 0 ) { + // chunks other than the first can have fadein + auto const fadeinwidth { chunkdata.threshold - chunkdata.fadein }; + if( soundpoint < chunkdata.threshold ) { + m_properties.gain *= + interpolate( + 0.f, 1.f, + clamp( + ( soundpoint - chunkdata.fadein ) / fadeinwidth, + 0.f, 1.f ) ); + return; + } + } + if( chunkindex < ( m_soundchunks.size() - 1 ) ) { + // chunks other than the last can have fadeout + // TODO: cache widths in the chunk data struct? + // fadeout point of this chunk and activation threshold of the next are the same + // fadein range of the next chunk and the fadeout of the processed one are the same + auto const fadeoutwidth { chunkdata.fadeout - m_soundchunks[ chunkindex + 1 ].second.fadein }; + auto const fadeoutstart { chunkdata.fadeout - fadeoutwidth }; + if( soundpoint > fadeoutstart ) { + m_properties.gain *= + interpolate( + 1.f, 0.f, + clamp( + ( soundpoint - fadeoutstart ) / fadeoutwidth, + 0.f, 1.f ) ); + return; } } } @@ -257,7 +692,7 @@ sound_source::gain() const { sound_source & sound_source::pitch( float const Pitch ) { - m_properties.pitch = clamp( Pitch, 0.1f, 10.f ); + m_properties.pitch = Pitch; return *this; } @@ -265,15 +700,25 @@ bool sound_source::empty() const { // NOTE: we test only the main sound, won't bother playing potential bookends if this is missing - // TODO: take into account presence of sample table, for combined sounds - return ( m_soundmain.buffer == null_handle ); + return ( ( sound( sound_id::main ).buffer == null_handle ) && ( m_soundchunksempty ) ); } // returns true if the source is emitting any sound bool sound_source::is_playing( bool const Includesoundends ) const { - return ( ( m_soundbegin.playing + m_soundmain.playing ) > 0 ); + auto isplaying { ( sound( sound_id::begin ).playing + sound( sound_id::main ).playing ) > 0 }; + if( ( false == isplaying ) + && ( false == m_soundchunks.empty() ) ) { + // for emitters with sample tables check also if any of the chunks is active + for( auto const &soundchunk : m_soundchunks ) { + if( soundchunk.first.playing > 0 ) { + isplaying = true; + break; // one will do + } + } + } + return isplaying; } // returns location of the sound source in simulation region space @@ -293,12 +738,10 @@ sound_source::location() const { } void -sound_source::update_counter( audio::buffer_handle const Buffer, int const Value ) { +sound_source::update_counter( sound_handle const Sound, int const Value ) { - if( Buffer == m_soundbegin.buffer ) { m_soundbegin.playing += Value; } - // TODO: take ito accound sample table for combined sounds - else if( Buffer == m_soundmain.buffer ) { m_soundmain.playing += Value; } - else if( Buffer == m_soundend.buffer ) { m_soundend.playing += Value; } + sound( Sound ).playing += Value; + assert( sound( Sound ).playing >= 0 ); } void @@ -380,10 +823,28 @@ sound_source::update_soundproofing() { } void -sound_source::insert( audio::buffer_handle Buffer ) { +sound_source::insert( sound_handle const Sound ) { - std::vector buffers { Buffer }; - return insert( std::begin( buffers ), std::end( buffers ) ); + std::vector sounds { Sound }; + return insert( std::begin( sounds ), std::end( sounds ) ); +} + +sound_source::sound_data & +sound_source::sound( sound_handle const Sound ) { + + return ( + ( Sound & sound_id::chunk ) == sound_id::chunk ? + m_soundchunks[ Sound ^ sound_id::chunk ].first : + m_sounds[ Sound ] ); +} + +sound_source::sound_data const & +sound_source::sound( sound_handle const Sound ) const { + + return ( + ( Sound & sound_id::chunk ) == sound_id::chunk ? + m_soundchunks[ Sound ^ sound_id::chunk ].first : + m_sounds[ Sound ] ); } //--------------------------------------------------------------------------- diff --git a/sound.h b/sound.h index b19c3e7d..cc870889 100644 --- a/sound.h +++ b/sound.h @@ -63,7 +63,7 @@ public: play( int const Flags = 0 ); // stops currently active play commands controlled by this emitter void - stop(); + stop( bool const Skipend = false ); // adjusts parameters of provided implementation-side sound source void update( audio::openal_source &Source ); @@ -110,30 +110,74 @@ public: private: // types struct sound_data { - audio::buffer_handle buffer { null_handle }; - int playing { 0 }; // number of currently active sample instances + audio::buffer_handle buffer; + int playing; // number of currently active sample instances + }; + + struct chunk_data { + int threshold; // nominal point of activation for the given chunk + float fadein; // actual activation point for the given chunk + float fadeout; // actual end point of activation range for the given chunk + float pitch; // base pitch of the chunk + }; + + using soundchunk_pair = std::pair; + + using sound_handle = std::uint32_t; + enum sound_id : std::uint32_t { + begin, + main, + end, + // 31 bits for index into relevant array, msb selects between the sample table and the basic array + chunk = ( 1u << 31 ) }; // methods // extracts name of the sound file from provided data stream std::string deserialize_filename( cParser &Input ); + // imports member data pair from the provided data stream + bool + deserialize_mapping( cParser &Input ); + // imports values for initial, main and ending sounds from provided data stream void - update_counter( audio::buffer_handle const Buffer, int const Value ); + deserialize_soundset( cParser &Input ); + // issues contextual play commands for the audio renderer + void + play_basic(); + void + play_combined(); + void + update_basic( audio::openal_source &Source ); + void + update_combined( audio::openal_source &Source ); + void + update_crossfade( sound_handle const Chunk ); + void + update_counter( sound_handle const Sound, int const Value ); void update_location(); // potentially updates area-based gain factor of the source. returns: true if location has changed bool update_soundproofing(); void - insert( audio::buffer_handle Buffer ); + insert( sound_handle const Sound ); template void insert( Iterator_ First, Iterator_ Last ) { - - audio::renderer.insert( this, First, Last ); - update_counter( *First, 1 ); - } + uint32_sequence sounds; + std::vector buffers; + std::for_each( + First, Last, + [&]( sound_handle const &soundhandle ) { + sounds.emplace_back( soundhandle ); + buffers.emplace_back( sound( soundhandle ).buffer ); } ); + audio::renderer.insert( std::begin( buffers ), std::end( buffers ), this, sounds ); + update_counter( *First, 1 ); } + sound_data & + sound( sound_handle const Sound ); + sound_data const & + sound( sound_handle const Sound ) const; // members TDynamicObject const * m_owner { nullptr }; // optional, the vehicle carrying this sound source @@ -145,10 +189,11 @@ private: sound_properties m_properties; // current properties of the emitted sounds float m_pitchvariation { 0.f }; // emitter-specific shift in base pitch bool m_stop { false }; // indicates active sample instances should be terminated - sound_data m_soundmain; // main sound emitted by the source - sound_data m_soundbegin; // optional, sound emitted before the main sound - sound_data m_soundend; // optional, sound emitted after the main sound - // TODO: table of samples with associated values, activated when controlling variable matches the value + bool m_playbeginning { true }; // indicates started sounds should be preceeded by opening bookend if there's one + std::array m_sounds { {} }; // basic sounds emitted by the source, main and optional bookends + std::vector m_soundchunks; // table of samples activated when associated variable is within certain range + bool m_soundchunksempty { true }; // helper, cached check whether sample table is linked with any actual samples + int m_crossfaderange {}; // range of transition from one chunk to another }; // owner setter/getter