/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * 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 . * */ #include "ags/shared/ac/common.h" #include "ags/engine/ac/game.h" #include "ags/engine/ac/game_setup.h" #include "ags/shared/ac/game_setup_struct.h" #include "ags/engine/ac/game_state.h" #include "ags/engine/ac/global_audio.h" #include "ags/engine/ac/lip_sync.h" #include "ags/engine/ac/path_helper.h" #include "ags/engine/debugging/debug_log.h" #include "ags/engine/debugging/debugger.h" #include "ags/shared/game/room_struct.h" #include "ags/engine/main/engine.h" #include "ags/engine/media/audio/audio_system.h" #include "ags/engine/ac/timer.h" #include "ags/shared/util/string_compat.h" namespace AGS3 { using namespace AGS::Shared; void StopAmbientSound(int channel) { if ((channel < NUM_SPEECH_CHANS) || (channel >= _GP(game).numGameChannels)) quitprintf("!StopAmbientSound: invalid channel %d, supported %d - %d", channel, NUM_SPEECH_CHANS, _GP(game).numGameChannels - 1); if (_GP(ambient)[channel].channel == 0) return; stop_and_destroy_channel(channel); _GP(ambient)[channel].channel = 0; } void PlayAmbientSound(int channel, int sndnum, int vol, int x, int y) { // the channel parameter is to allow multiple ambient sounds in future if ((channel < 1) || (channel == SCHAN_SPEECH) || (channel >= _GP(game).numGameChannels)) quit("!PlayAmbientSound: invalid channel number"); if ((vol < 1) || (vol > 255)) quit("!PlayAmbientSound: volume must be 1 to 255"); ScriptAudioClip *aclip = GetAudioClipForOldStyleNumber(_GP(game), false, sndnum); if (aclip && !is_audiotype_allowed_to_play((AudioFileType)aclip->fileType)) return; // only play the sound if it's not already playing if ((_GP(ambient)[channel].channel < 1) || (!AudioChans::ChannelIsPlaying(_GP(ambient)[channel].channel)) || (_GP(ambient)[channel].num != sndnum)) { StopAmbientSound(channel); // in case a normal non-ambient sound was playing, stop it too stop_and_destroy_channel(channel); SOUNDCLIP *asound = aclip ? load_sound_and_play(aclip, true) : nullptr; if (asound == nullptr) { debug_script_warn("Cannot load ambient sound %d", sndnum); debug_script_log("FAILED to load ambient sound %d", sndnum); return; } debug_script_log("Playing ambient sound %d on channel %d", sndnum, channel); _GP(ambient)[channel].channel = channel; asound->_priority = 15; // ambient sound higher priority than normal sfx AudioChans::SetChannel(channel, asound); } // calculate the maximum distance away the player can be, using X // only (since X centred is still more-or-less total Y) _GP(ambient)[channel].maxdist = ((x > _GP(thisroom).Width / 2) ? x : (_GP(thisroom).Width - x)) - AMBIENCE_FULL_DIST; _GP(ambient)[channel].num = sndnum; _GP(ambient)[channel].x = x; _GP(ambient)[channel].y = y; _GP(ambient)[channel].vol = vol; update_ambient_sound_vol(); } int IsChannelPlaying(int chan) { if (_GP(play).fast_forward) return 0; if ((chan < 0) || (chan >= _GP(game).numGameChannels)) quit("!IsChannelPlaying: invalid sound channel"); if (AudioChans::ChannelIsPlaying(chan)) return 1; return 0; } int IsSoundPlaying() { if (_GP(play).fast_forward) return 0; // find if there's a sound playing for (int i = SCHAN_NORMAL; i < _GP(game).numGameChannels; i++) { if (AudioChans::GetChannelIfPlaying(i)) return 1; } return 0; } // returns -1 on failure, channel number on success int PlaySoundEx(int val1, int channel) { if (_G(debug_flags) & DBG_NOSFX) return -1; ScriptAudioClip *aclip = GetAudioClipForOldStyleNumber(_GP(game), false, val1); if (aclip && !is_audiotype_allowed_to_play((AudioFileType)aclip->fileType)) return -1; // if sound is off, ignore it if ((channel < SCHAN_NORMAL) || (channel >= _GP(game).numGameChannels)) quitprintf("!PlaySoundEx: invalid channel specified, must be %d-%d", SCHAN_NORMAL, _GP(game).numGameChannels - 1); // if an ambient sound is playing on this channel, abort it StopAmbientSound(channel); if (val1 < 0) { stop_and_destroy_channel(channel); return -1; } // if skipping a cutscene, don't try and play the sound if (_GP(play).fast_forward) return -1; // free the old sound stop_and_destroy_channel(channel); debug_script_log("Playing sound %d on channel %d", val1, channel); SOUNDCLIP *soundfx = aclip ? load_sound_and_play(aclip, false) : nullptr; if (soundfx == nullptr) { debug_script_warn("Sound sample load failure: cannot load sound %d", val1); debug_script_log("FAILED to load sound %d", val1); return -1; } soundfx->_priority = 10; soundfx->set_volume255(_GP(play).sound_volume); AudioChans::SetChannel(channel, soundfx); return channel; } void StopAllSounds(int evenAmbient) { // backwards-compatible hack -- stop Type 3 (default Sound Type) Game_StopAudio(3); if (evenAmbient) Game_StopAudio(1); } void PlayMusicResetQueue(int newmus) { _GP(play).music_queue_size = 0; newmusic(newmus); } void SeekMIDIPosition(int position) { if (_GP(play).silent_midi == 0 && _G(current_music_type) != MUS_MIDI) return; auto *ch = AudioChans::GetChannel(SCHAN_MUSIC); ch->seek(position); debug_script_log("Seek MIDI position to %d", position); } int GetMIDIPosition() { if (_GP(play).fast_forward) return 99999; if (_GP(play).silent_midi == 0 && _G(current_music_type) != MUS_MIDI) return -1; // returns -1 on failure according to old manuals auto *ch = AudioChans::GetChannelIfPlaying(SCHAN_MUSIC); if (ch) { return ch->get_pos(); } return -1; } int IsMusicPlaying() { // in case they have a "while (IsMusicPlaying())" loop if ((_GP(play).fast_forward) && (_GP(play).skip_until_char_stops < 0)) return 0; // This only returns positive if there was a music started by old audio API if (_G(current_music_type) == 0) return 0; auto *ch = AudioChans::GetChannel(SCHAN_MUSIC); if (ch == nullptr) { // This was probably a hacky fix in case it was not reset by game update; TODO: find out if needed _G(current_music_type) = 0; return 0; } bool result = (ch->is_playing()) || (_G(crossFading) > 0 && (AudioChans::GetChannelIfPlaying(_G(crossFading)) != nullptr)); return result ? 1 : 0; } int PlayMusicQueued(int musnum) { // Just get the queue size if (musnum < 0) return _GP(play).music_queue_size; if ((IsMusicPlaying() == 0) && (_GP(play).music_queue_size == 0)) { newmusic(musnum); return 0; } if (_GP(play).music_queue_size >= MAX_QUEUED_MUSIC) { debug_script_log("Too many queued music, cannot add %d", musnum); return 0; } if ((_GP(play).music_queue_size > 0) && (_GP(play).music_queue[_GP(play).music_queue_size - 1] >= QUEUED_MUSIC_REPEAT)) { debug_script_warn("PlayMusicQueued: cannot queue music after a repeating tune has been queued"); return 0; } if (_GP(play).music_repeat) { debug_script_log("Queuing music %d to loop", musnum); musnum += QUEUED_MUSIC_REPEAT; } else { debug_script_log("Queuing music %d", musnum); } _GP(play).music_queue[_GP(play).music_queue_size] = musnum; _GP(play).music_queue_size++; if (_GP(play).music_queue_size == 1) { clear_music_cache(); _G(cachedQueuedMusic) = load_music_from_disk(musnum, (_GP(play).music_repeat > 0)); } return _GP(play).music_queue_size; } void scr_StopMusic() { _GP(play).music_queue_size = 0; stopmusic(); } void SeekMODPattern(int patnum) { if (_G(current_music_type) != MUS_MOD) return; auto *ch = AudioChans::GetChannelIfPlaying(SCHAN_MUSIC); if (ch) { ch->seek(patnum); debug_script_log("Seek MOD/XM to pattern %d", patnum); } } void SeekMP3PosMillis(int posn) { if (_G(current_music_type) != MUS_MP3 && _G(current_music_type) != MUS_OGG) return; auto *mus_ch = AudioChans::GetChannel(SCHAN_MUSIC); auto *cf_ch = (_G(crossFading) > 0) ? AudioChans::GetChannel(_G(crossFading)) : nullptr; if (cf_ch) cf_ch->seek(posn); else if (mus_ch) mus_ch->seek(posn); } int GetMP3PosMillis() { // in case they have "while (GetMP3PosMillis() < 5000) " if (_GP(play).fast_forward) return 999999; if (_G(current_music_type) != MUS_MP3 && _G(current_music_type) != MUS_OGG) return 0; // returns 0 on failure according to old manuals auto *ch = AudioChans::GetChannelIfPlaying(SCHAN_MUSIC); if (ch) { int result = ch->get_pos_ms(); if (result >= 0) return result; return ch->get_pos(); } return 0; } void SetMusicVolume(int newvol) { if ((newvol < kRoomVolumeMin) || (newvol > kRoomVolumeMax)) quitprintf("!SetMusicVolume: invalid volume number. Must be from %d to %d.", kRoomVolumeMin, kRoomVolumeMax); _GP(thisroom).Options.MusicVolume = (RoomVolumeMod)newvol; update_music_volume(); } void SetMusicMasterVolume(int newvol) { const int min_volume = _G(loaded_game_file_version) < kGameVersion_330 ? 0 : -LegacyMusicMasterVolumeAdjustment - (kRoomVolumeMax * LegacyRoomVolumeFactor); if ((newvol < min_volume) || (newvol > 100)) quitprintf("!SetMusicMasterVolume: invalid volume - must be from %d to %d", min_volume, 100); _GP(play).music_master_volume = newvol + LegacyMusicMasterVolumeAdjustment; update_music_volume(); } void SetSoundVolume(int newvol) { if ((newvol < 0) || (newvol > 255)) quit("!SetSoundVolume: invalid volume - must be from 0-255"); _GP(play).sound_volume = newvol; Game_SetAudioTypeVolume(AUDIOTYPE_LEGACY_AMBIENT_SOUND, (newvol * 100) / 255, VOL_BOTH); Game_SetAudioTypeVolume(AUDIOTYPE_LEGACY_SOUND, (newvol * 100) / 255, VOL_BOTH); update_ambient_sound_vol(); } void SetChannelVolume(int chan, int newvol) { if ((newvol < 0) || (newvol > 255)) quit("!SetChannelVolume: invalid volume - must be from 0-255"); if ((chan < 0) || (chan >= _GP(game).numGameChannels)) quit("!SetChannelVolume: invalid channel id"); auto *ch = AudioChans::GetChannelIfPlaying(chan); if (ch) { if (chan == _GP(ambient)[chan].channel) { _GP(ambient)[chan].vol = newvol; update_ambient_sound_vol(); } else ch->set_volume255(newvol); } } void SetDigitalMasterVolume(int newvol) { if ((newvol < 0) || (newvol > 100)) quit("!SetDigitalMasterVolume: invalid volume - must be from 0-100"); _GP(play).digital_master_volume = newvol; #if !AGS_PLATFORM_SCUMMVM auto newvol_f = static_cast(newvol) / 100.0; audio_core_set_master_volume(newvol_f); #endif } int GetCurrentMusic() { return _GP(play).cur_music_number; } void SetMusicRepeat(int loopflag) { _GP(play).music_repeat = loopflag; } void PlayMP3File(const char *filename) { debug_script_log("PlayMP3File %s", filename); AssetPath asset_name(filename, "audio"); const bool doLoop = (_GP(play).music_repeat > 0); SOUNDCLIP *clip = my_load_ogg(asset_name, doLoop); int sound_type = 0; if (clip) sound_type = MUS_OGG; if (!clip) { clip = my_load_mp3(asset_name, doLoop); sound_type = MUS_MP3; } if (!clip) { debug_script_warn("PlayMP3File: music file '%s' not found or be read", filename); return; } const int use_chan = prepare_for_new_music(); _G(current_music_type) = sound_type; _GP(play).cur_music_number = 1000; _GP(play).playmp3file_name = filename; clip->set_volume255(150); AudioChans::SetChannel(use_chan, clip); post_new_music_check(); update_music_volume(); } void PlaySilentMIDI(int mnum) { if (_G(current_music_type) == MUS_MIDI) quit("!PlaySilentMIDI: proper midi music is in progress"); _GP(play).silent_midi = mnum; _GP(play).silent_midi_channel = SCHAN_SPEECH; stop_and_destroy_channel(_GP(play).silent_midi_channel); // No idea why it uses speech voice channel, but since it does (and until this is changed) // we have to correctly reset speech voice in case there was a nonblocking speech if (_GP(play).IsNonBlockingVoiceSpeech()) stop_voice_nonblocking(); SOUNDCLIP *clip = load_sound_clip_from_old_style_number(true, mnum, false); if (clip == nullptr) { quitprintf("!PlaySilentMIDI: failed to load aMusic%d", mnum); } AudioChans::SetChannel(_GP(play).silent_midi_channel, clip); if (!clip->play()) { delete clip; clip = nullptr; quitprintf("!PlaySilentMIDI: failed to play aMusic%d", mnum); } clip->set_volume100(0); } void SetSpeechVolume(int newvol) { if ((newvol < 0) || (newvol > 255)) quit("!SetSpeechVolume: invalid volume - must be from 0-255"); auto *ch = AudioChans::GetChannel(SCHAN_SPEECH); if (ch) ch->set_volume255(newvol); _GP(play).speech_volume = newvol; } void SetVoiceMode(int newmod) { if ((newmod < kSpeech_First) || (newmod > kSpeech_Last)) quitprintf("!SetVoiceMode: invalid mode number %d", newmod); _GP(play).speech_mode = (SpeechMode)newmod; } int GetVoiceMode() { return (int)_GP(play).speech_mode; } int IsVoxAvailable() { return _GP(play).voice_avail ? 1 : 0; } int IsMusicVoxAvailable() { return _GP(play).separate_music_lib ? 1 : 0; } ScriptAudioChannel *PlayVoiceClip(CharacterInfo *ch, int sndid, bool as_speech) { if (!play_voice_nonblocking(ch->index_id, sndid, as_speech)) return nullptr; return &_G(scrAudioChannel)[SCHAN_SPEECH]; } // Construct an asset name for the voice-over clip for the given character and cue id String get_cue_filename(int charid, int sndid) { String asset_path = get_voice_assetpath(); String script_name; if (charid >= 0) { // append the first 4 characters of the script name to the filename if (_GP(game).chars2[charid].scrname_new.GetAt(0) == 'c') script_name.SetString(_GP(game).chars2[charid].scrname_new.GetCStr() + 1, 4); else script_name.SetString(_GP(game).chars2[charid].scrname_new.GetCStr(), 4); } else { script_name = "NARR"; } return String::FromFormat("%s%s%d", asset_path.GetCStr(), script_name.GetCStr(), sndid); } // Play voice-over clip on the common channel; // voice_name should be bare clip name without extension static bool play_voice_clip_on_channel(const String &voice_name) { stop_and_destroy_channel(SCHAN_SPEECH); String asset_name = voice_name; asset_name.Append(".wav"); SOUNDCLIP *speechmp3 = my_load_wave(get_voice_over_assetpath(asset_name), false); if (speechmp3 == nullptr) { asset_name.ReplaceMid(asset_name.GetLength() - 3, 3, "ogg"); speechmp3 = my_load_ogg(get_voice_over_assetpath(asset_name), false); } if (speechmp3 == nullptr) { asset_name.ReplaceMid(asset_name.GetLength() - 3, 3, "mp3"); speechmp3 = my_load_mp3(get_voice_over_assetpath(asset_name), false); } if (speechmp3 != nullptr) { speechmp3->set_volume255(_GP(play).speech_volume); if (!speechmp3->play()) { // not assigned to a channel, so clean up manually. delete speechmp3; speechmp3 = nullptr; } } if (speechmp3 == nullptr) { debug_script_warn("Speech load failure: '%s'", voice_name.GetCStr()); return false; } AudioChans::SetChannel(SCHAN_SPEECH, speechmp3); return true; } // Play voice-over clip and adjust audio volumes; // voice_name should be bare clip name without extension static bool play_voice_clip_impl(const String &voice_name, bool as_speech, bool is_blocking) { if (!play_voice_clip_on_channel(voice_name)) return false; if (!as_speech) return true; _GP(play).speech_has_voice = true; _GP(play).speech_voice_blocking = is_blocking; cancel_scheduled_music_update(); _GP(play).music_vol_was = _GP(play).music_master_volume; // Negative value means set exactly; positive means drop that amount if (_GP(play).speech_music_drop < 0) _GP(play).music_master_volume = -_GP(play).speech_music_drop; else _GP(play).music_master_volume -= _GP(play).speech_music_drop; apply_volume_drop_modifier(true); update_music_volume(); update_ambient_sound_vol(); return true; } // Stop voice-over clip and schedule audio volume reset static void stop_voice_clip_impl() { _GP(play).music_master_volume = _GP(play).music_vol_was; // update the music in a bit (fixes two speeches follow each other // and music going up-then-down) schedule_music_update_at(AGS_Clock::now() + std::chrono::milliseconds(500)); stop_and_destroy_channel(SCHAN_SPEECH); } bool play_voice_speech(int charid, int sndid) { // don't play speech if we're skipping a cutscene if (!_GP(play).ShouldPlayVoiceSpeech()) return false; String voice_file = get_cue_filename(charid, sndid); if (!play_voice_clip_impl(voice_file, true, true)) return false; int ii; // Compare the base file name to the .pam file name _G(curLipLine) = -1; // See if we have voice lip sync for this line _G(curLipLinePhoneme) = -1; for (ii = 0; ii < _G(numLipLines); ii++) { if (voice_file.CompareNoCase(_GP(splipsync)[ii].filename) == 0) { _G(curLipLine) = ii; break; } } // if the lip-sync is being used for voice sync, disable // the text-related lipsync if (_G(numLipLines) > 0) _GP(game).options[OPT_LIPSYNCTEXT] = 0; // change Sierra w/bgrnd to Sierra without background when voice // is available (for Tierra) if ((_GP(game).options[OPT_SPEECHTYPE] == 2) && (_GP(play).no_textbg_when_voice > 0)) { _GP(game).options[OPT_SPEECHTYPE] = 1; _GP(play).no_textbg_when_voice = 2; } return true; } bool play_voice_nonblocking(int charid, int sndid, bool as_speech) { // don't play voice if we're skipping a cutscene if (!_GP(play).ShouldPlayVoiceSpeech()) return false; // don't play voice if there's a blocking speech with voice-over already if (_GP(play).IsBlockingVoiceSpeech()) return false; String voice_file = get_cue_filename(charid, sndid); return play_voice_clip_impl(voice_file, as_speech, false); } void stop_voice_speech() { if (!_GP(play).speech_has_voice) return; stop_voice_clip_impl(); // Reset lipsync _G(curLipLine) = -1; // Set back to Sierra w/bgrnd if (_GP(play).no_textbg_when_voice == 2) { _GP(play).no_textbg_when_voice = 1; _GP(game).options[OPT_SPEECHTYPE] = 2; } _GP(play).speech_has_voice = false; _GP(play).speech_voice_blocking = false; } void stop_voice_nonblocking() { if (!_GP(play).speech_has_voice) return; stop_voice_clip_impl(); // Only reset speech flags if we are truly playing a non-blocking voice; // otherwise we might be inside blocking speech function and should let // it keep these flags to be able to finalize properly. // This is an imperfection of current speech implementation. if (!_GP(play).speech_voice_blocking) { _GP(play).speech_has_voice = false; _GP(play).speech_voice_blocking = false; } } } // namespace AGS3