| 1 | /* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ |
| 2 | /* If you are missing that file, acquire a complete release at teeworlds.com. */ |
| 3 | |
| 4 | #include "sounds.h" |
| 5 | |
| 6 | #include <base/time.h> |
| 7 | |
| 8 | #include <engine/engine.h> |
| 9 | #include <engine/shared/config.h> |
| 10 | #include <engine/sound.h> |
| 11 | |
| 12 | #include <generated/client_data.h> |
| 13 | |
| 14 | #include <game/client/components/camera.h> |
| 15 | #include <game/client/components/menus.h> |
| 16 | #include <game/client/gameclient.h> |
| 17 | #include <game/localization.h> |
| 18 | |
| 19 | CSoundLoading::CSoundLoading(CGameClient *pGameClient, bool Render) : |
| 20 | m_pGameClient(pGameClient), |
| 21 | m_Render(Render) |
| 22 | { |
| 23 | Abortable(Abortable: true); |
| 24 | } |
| 25 | |
| 26 | void CSoundLoading::Run() |
| 27 | { |
| 28 | for(int s = 0; s < g_pData->m_NumSounds; s++) |
| 29 | { |
| 30 | const char *pLoadingCaption = Localize(pStr: "Loading DDNet Client" ); |
| 31 | const char *pLoadingContent = Localize(pStr: "Loading sound files" ); |
| 32 | |
| 33 | for(int i = 0; i < g_pData->m_aSounds[s].m_NumSounds; i++) |
| 34 | { |
| 35 | if(State() == IJob::STATE_ABORTED) |
| 36 | return; |
| 37 | |
| 38 | int Id = m_pGameClient->Sound()->LoadWV(pFilename: g_pData->m_aSounds[s].m_aSounds[i].m_pFilename); |
| 39 | g_pData->m_aSounds[s].m_aSounds[i].m_Id = Id; |
| 40 | // try to render a frame |
| 41 | if(m_Render) |
| 42 | m_pGameClient->m_Menus.RenderLoading(pCaption: pLoadingCaption, pContent: pLoadingContent, IncreaseCounter: 0); |
| 43 | } |
| 44 | |
| 45 | if(m_Render) |
| 46 | m_pGameClient->m_Menus.RenderLoading(pCaption: pLoadingCaption, pContent: pLoadingContent, IncreaseCounter: 1); |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | void CSounds::UpdateChannels() |
| 51 | { |
| 52 | const float NewGuiSoundVolume = g_Config.m_SndChatVolume / 100.0f; |
| 53 | if(NewGuiSoundVolume != m_GuiSoundVolume) |
| 54 | { |
| 55 | m_GuiSoundVolume = NewGuiSoundVolume; |
| 56 | Sound()->SetChannel(ChannelId: CSounds::CHN_GUI, Volume: m_GuiSoundVolume, Panning: 0.0f); |
| 57 | } |
| 58 | |
| 59 | const float NewGameSoundVolume = g_Config.m_SndGameVolume / 100.0f; |
| 60 | if(NewGameSoundVolume != m_GameSoundVolume) |
| 61 | { |
| 62 | m_GameSoundVolume = NewGameSoundVolume; |
| 63 | Sound()->SetChannel(ChannelId: CSounds::CHN_WORLD, Volume: 0.9f * m_GameSoundVolume, Panning: 1.0f); |
| 64 | Sound()->SetChannel(ChannelId: CSounds::CHN_GLOBAL, Volume: m_GameSoundVolume, Panning: 0.0f); |
| 65 | } |
| 66 | |
| 67 | const float NewMapSoundVolume = g_Config.m_SndMapVolume / 100.0f; |
| 68 | if(NewMapSoundVolume != m_MapSoundVolume) |
| 69 | { |
| 70 | m_MapSoundVolume = NewMapSoundVolume; |
| 71 | Sound()->SetChannel(ChannelId: CSounds::CHN_MAPSOUND, Volume: m_MapSoundVolume, Panning: 1.0f); |
| 72 | } |
| 73 | |
| 74 | const float NewBackgroundMusicVolume = g_Config.m_SndBackgroundMusicVolume / 100.0f; |
| 75 | if(NewBackgroundMusicVolume != m_BackgroundMusicVolume) |
| 76 | { |
| 77 | m_BackgroundMusicVolume = NewBackgroundMusicVolume; |
| 78 | Sound()->SetChannel(ChannelId: CSounds::CHN_MUSIC, Volume: m_BackgroundMusicVolume, Panning: 1.0f); |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | int CSounds::GetSampleId(int SetId) |
| 83 | { |
| 84 | if(!g_Config.m_SndEnable || !Sound()->IsSoundEnabled() || m_WaitForSoundJob || SetId < 0 || SetId >= g_pData->m_NumSounds) |
| 85 | return -1; |
| 86 | |
| 87 | CDataSoundset *pSet = &g_pData->m_aSounds[SetId]; |
| 88 | if(!pSet->m_NumSounds) |
| 89 | return -1; |
| 90 | |
| 91 | if(pSet->m_NumSounds == 1) |
| 92 | return pSet->m_aSounds[0].m_Id; |
| 93 | |
| 94 | // return random one |
| 95 | int Id; |
| 96 | do |
| 97 | { |
| 98 | Id = rand() % pSet->m_NumSounds; |
| 99 | } while(Id == pSet->m_Last); |
| 100 | pSet->m_Last = Id; |
| 101 | return pSet->m_aSounds[Id].m_Id; |
| 102 | } |
| 103 | |
| 104 | void CSounds::OnInit() |
| 105 | { |
| 106 | UpdateChannels(); |
| 107 | ClearQueue(); |
| 108 | |
| 109 | // load sounds |
| 110 | if(g_Config.m_ClThreadsoundloading) |
| 111 | { |
| 112 | m_pSoundJob = std::make_shared<CSoundLoading>(args: GameClient(), args: false); |
| 113 | GameClient()->Engine()->AddJob(pJob: m_pSoundJob); |
| 114 | m_WaitForSoundJob = true; |
| 115 | GameClient()->m_Menus.RenderLoading(pCaption: Localize(pStr: "Loading DDNet Client" ), pContent: Localize(pStr: "Loading sound files" ), IncreaseCounter: 0); |
| 116 | } |
| 117 | else |
| 118 | { |
| 119 | CSoundLoading(GameClient(), true).Run(); |
| 120 | m_WaitForSoundJob = false; |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | void CSounds::OnReset() |
| 125 | { |
| 126 | if(Client()->State() >= IClient::STATE_ONLINE) |
| 127 | { |
| 128 | Sound()->StopAll(); |
| 129 | ClearQueue(); |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | void CSounds::OnStateChange(int NewState, int OldState) |
| 134 | { |
| 135 | if(NewState == IClient::STATE_ONLINE || NewState == IClient::STATE_DEMOPLAYBACK) |
| 136 | OnReset(); |
| 137 | } |
| 138 | |
| 139 | void CSounds::OnRender() |
| 140 | { |
| 141 | // check for sound initialisation |
| 142 | if(m_WaitForSoundJob) |
| 143 | { |
| 144 | if(m_pSoundJob->State() == IJob::STATE_DONE) |
| 145 | m_WaitForSoundJob = false; |
| 146 | else |
| 147 | return; |
| 148 | } |
| 149 | |
| 150 | Sound()->SetListenerPosition(GameClient()->m_Camera.m_Center); |
| 151 | UpdateChannels(); |
| 152 | |
| 153 | // play sound from queue |
| 154 | if(m_QueuePos > 0) |
| 155 | { |
| 156 | int64_t Now = time(); |
| 157 | if(m_QueueWaitTime <= Now) |
| 158 | { |
| 159 | Play(Channel: m_aQueue[0].m_Channel, SetId: m_aQueue[0].m_SetId, Volume: 1.0f); |
| 160 | m_QueueWaitTime = Now + time_freq() * 3 / 10; // wait 300ms before playing the next one |
| 161 | if(--m_QueuePos > 0) |
| 162 | mem_move(dest: m_aQueue, source: m_aQueue + 1, size: m_QueuePos * sizeof(CQueueEntry)); |
| 163 | } |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | void CSounds::ClearQueue() |
| 168 | { |
| 169 | mem_zero(block: m_aQueue, size: sizeof(m_aQueue)); |
| 170 | m_QueuePos = 0; |
| 171 | m_QueueWaitTime = time(); |
| 172 | } |
| 173 | |
| 174 | void CSounds::Enqueue(int Channel, int SetId) |
| 175 | { |
| 176 | if(GameClient()->m_SuppressEvents) |
| 177 | return; |
| 178 | if(m_QueuePos >= QUEUE_SIZE) |
| 179 | return; |
| 180 | if(Channel != CHN_MUSIC && g_Config.m_ClEditor) |
| 181 | return; |
| 182 | |
| 183 | m_aQueue[m_QueuePos].m_Channel = Channel; |
| 184 | m_aQueue[m_QueuePos++].m_SetId = SetId; |
| 185 | } |
| 186 | |
| 187 | void CSounds::PlayAndRecord(int Channel, int SetId, float Volume, vec2 Position) |
| 188 | { |
| 189 | // TODO: Volume and position are currently not recorded for sounds played with this function |
| 190 | // TODO: This also causes desync sounds during demo playback of demos recorded on high ping servers: |
| 191 | // https://github.com/ddnet/ddnet/issues/1282 |
| 192 | CNetMsg_Sv_SoundGlobal Msg; |
| 193 | Msg.m_SoundId = SetId; |
| 194 | Client()->SendPackMsgActive(pMsg: &Msg, Flags: MSGFLAG_NOSEND | MSGFLAG_RECORD); |
| 195 | |
| 196 | PlayAt(Channel, SetId, Volume, Position); |
| 197 | } |
| 198 | |
| 199 | void CSounds::Play(int Channel, int SetId, float Volume) |
| 200 | { |
| 201 | PlaySample(Channel, SampleId: GetSampleId(SetId), Flags: 0, Volume); |
| 202 | } |
| 203 | |
| 204 | void CSounds::PlayAt(int Channel, int SetId, float Volume, vec2 Position) |
| 205 | { |
| 206 | PlaySampleAt(Channel, SampleId: GetSampleId(SetId), Flags: 0, Volume, Position); |
| 207 | } |
| 208 | |
| 209 | void CSounds::Stop(int SetId) |
| 210 | { |
| 211 | if(m_WaitForSoundJob || SetId < 0 || SetId >= g_pData->m_NumSounds) |
| 212 | return; |
| 213 | |
| 214 | const CDataSoundset *pSet = &g_pData->m_aSounds[SetId]; |
| 215 | for(int i = 0; i < pSet->m_NumSounds; i++) |
| 216 | if(pSet->m_aSounds[i].m_Id != -1) |
| 217 | Sound()->Stop(SampleId: pSet->m_aSounds[i].m_Id); |
| 218 | } |
| 219 | |
| 220 | bool CSounds::IsPlaying(int SetId) |
| 221 | { |
| 222 | if(m_WaitForSoundJob || SetId < 0 || SetId >= g_pData->m_NumSounds) |
| 223 | return false; |
| 224 | |
| 225 | const CDataSoundset *pSet = &g_pData->m_aSounds[SetId]; |
| 226 | for(int i = 0; i < pSet->m_NumSounds; i++) |
| 227 | if(pSet->m_aSounds[i].m_Id != -1 && Sound()->IsPlaying(SampleId: pSet->m_aSounds[i].m_Id)) |
| 228 | return true; |
| 229 | return false; |
| 230 | } |
| 231 | |
| 232 | ISound::CVoiceHandle CSounds::PlaySample(int Channel, int SampleId, int Flags, float Volume) |
| 233 | { |
| 234 | if(GameClient()->m_SuppressEvents || (Channel == CHN_MUSIC && !g_Config.m_SndMusic) || SampleId == -1) |
| 235 | return ISound::CVoiceHandle(); |
| 236 | |
| 237 | if(Channel == CHN_MUSIC) |
| 238 | Flags |= ISound::FLAG_LOOP; |
| 239 | |
| 240 | return Sound()->Play(ChannelId: Channel, SampleId, Flags, Volume); |
| 241 | } |
| 242 | |
| 243 | ISound::CVoiceHandle CSounds::PlaySampleAt(int Channel, int SampleId, int Flags, float Volume, vec2 Position) |
| 244 | { |
| 245 | if(GameClient()->m_SuppressEvents || (Channel == CHN_MUSIC && !g_Config.m_SndMusic) || SampleId == -1) |
| 246 | return ISound::CVoiceHandle(); |
| 247 | |
| 248 | if(Channel == CHN_MUSIC) |
| 249 | Flags |= ISound::FLAG_LOOP; |
| 250 | |
| 251 | return Sound()->PlayAt(ChannelId: Channel, SampleId, Flags, Volume, Position); |
| 252 | } |
| 253 | |