(*
* Hedgewars, a free turn based strategy game
* Copyright (c) 2004-2015 Andrey Korotaev <unC0Rr@gmail.com>
*
* 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; version 2 of the License
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*)
{$INCLUDE "options.inc"}
unit uSound;
(*
* This unit controls the sounds and music of the game.
* Doesn't really do anything if isSoundEnabled = false and isMusicEnabled = false
*
* There are three basic types of sound controls:
* Music - The background music of the game:
* * will only be played if isMusicEnabled = true
* * can be started, changed, paused and resumed
* Sound - Can be started and stopped
* Looped Sound - Subtype of sound: plays in a loop using a
* "channel", of which the id is returned on start.
* The channel id can be used to stop a specific sound loop.
*)
interface
uses SDLh, uConsts, uTypes;
procedure preInitModule;
procedure initModule;
procedure freeModule;
procedure InitSound; // Initiates sound-system if isSoundEnabled.
procedure ReleaseSound(complete: boolean); // Releases sound-system and used resources.
procedure ResetSound; // Reset sound state to the previous state.
procedure SetSound(enabled: boolean); // Enable/disable sound-system and backup status.
procedure SetAudioDampen(enabled: boolean); // Enable/disable automatic dampening if losing window focus.
// MUSIC
// Obvious music commands for music track
procedure SetMusic(enabled: boolean); // Enable/disable music.
procedure SetMusicName(musicname: shortstring); // Set name of the file to play.
procedure PlayMusic; // Play music from the start.
procedure PauseMusic; // Pause music.
procedure ResumeMusic; // Resume music from pause point.
procedure ChangeMusic(musicname: shortstring); // Replaces music track with musicname and plays it.
procedure StopMusic; // Stops and releases the current track.
// SOUNDS
// Plays the sound snd [from a given voicepack],
// if keepPlaying is given and true,
// then the sound's playback won't be interrupted if asked to play again.
// Returns true if sound was found and is played, false otherwise.
function PlaySound(snd: TSound): boolean;
function PlaySound(snd: TSound; keepPlaying: boolean): boolean;
function PlaySound(snd: TSound; keepPlaying: boolean; ignoreMask: boolean): boolean;
function PlaySound(snd: TSound; keepPlaying, ignoreMask, soundAsMusic: boolean): boolean;
function PlaySoundV(snd: TSound; voicepack: PVoicepack): boolean;
function PlaySoundV(snd: TSound; voicepack: PVoicepack; keepPlaying: boolean): boolean;
function PlaySoundV(snd: TSound; voicepack: PVoicepack; keepPlaying, ignoreMask: boolean): boolean;
function PlaySoundV(snd: TSound; voicepack: PVoicepack; keepPlaying, ignoreMask, soundAsMusic: boolean): boolean;
// Plays/stops a sound to replace the main background music temporarily.
procedure PlayMusicSound(snd: TSound);
procedure StopMusicSound(snd: TSound);
// Plays sound snd [of voicepack] in a loop, but starts with fadems milliseconds of fade-in.
// Returns sound channel of the looped sound.
function LoopSound(snd: TSound): LongInt;
function LoopSound(snd: TSound; fadems: LongInt): LongInt;
function LoopSoundV(snd: TSound; voicepack: PVoicepack): LongInt;
function LoopSoundV(snd: TSound; voicepack: PVoicepack; fadems: LongInt): LongInt;
// Stops the normal/looped sound of the given type/in the given channel
// [with a fade-out effect for fadems milliseconds].
procedure StopSound(snd: TSound);
procedure StopSound(snd: TSound; soundAsMusic: boolean);
procedure StopSoundChan(chn: LongInt);
procedure StopSoundChan(chn, fadems: LongInt);
// Add voice to the voice queue
procedure AddVoice(snd: TSound; voicepack: PVoicepack);
procedure AddVoice(snd: TSound; voicepack: PVoicepack; ignoreMask, isFallback: boolean);
// Actually play next voice
procedure PlayNextVoice;
// GLOBAL FUNCTIONS
// Drastically lower the volume when we lose focus (and restore the previous value).
procedure DampenAudio;
procedure UndampenAudio;
// Mute/Unmute audio
procedure MuteAudio;
// MISC
// Set the initial volume
procedure SetVolume(vol: LongInt);
// Modifies the sound volume of the game by voldelta and returns the new volume level.
function ChangeVolume(voldelta: LongInt): LongInt;
// Returns the current volume in percent. Intended for display on UI.
function GetVolumePercent(): LongInt;
// Returns a pointer to the voicepack with the given name.
function AskForVoicepack(name: shortstring): Pointer;
var MusicFN: shortstring; // music file name
SDMusicFN: shortstring; // SD music file name
FallbackMusicFN: shortstring; // fallback music file name
FallbackSDMusicFN: shortstring; // fallback SD music fille name
var Volume: LongInt;
SoundTimerTicks: Longword;
LastVoiceFailed: boolean;
implementation
uses uVariables, uConsole, uCommands, uDebug, uPhysFSLayer;
const chanTPU = 32;
var cInitVolume: LongInt;
previousVolume: LongInt; // cached volume value
lastChan: array [TSound] of LongInt;
voicepacks: array[0..cMaxTeams] of TVoicepack;
defVoicepack: PVoicepack;
Mus: PMixMusic; // music pointer
isMusicEnabled: boolean;
isSoundEnabled: boolean;
isAutoDampening: boolean;
isSEBackup: boolean;
VoiceList : array[0..7] of TVoice = (
( snd: sndNone; voicepack: nil; isFallback: false),
( snd: sndNone; voicepack: nil; isFallback: false),
( snd: sndNone; voicepack: nil; isFallback: false),
( snd: sndNone; voicepack: nil; isFallback: false),
( snd: sndNone; voicepack: nil; isFallback: false),
( snd: sndNone; voicepack: nil; isFallback: false),
( snd: sndNone; voicepack: nil; isFallback: false),
( snd: sndNone; voicepack: nil; isFallback: false));
Soundz: array[TSound] of record
FileName: string[31];
Path, AltPath : TPathType;
end = (
(FileName: ''; Path: ptNone; AltPath: ptNone),// sndNone
(FileName: 'grenadeimpact.ogg'; Path: ptSounds; AltPath: ptNone),// sndGrenadeImpact
(FileName: 'explosion.ogg'; Path: ptSounds; AltPath: ptNone),// sndExplosion
(FileName: 'throwpowerup.ogg'; Path: ptSounds; AltPath: ptNone),// sndThrowPowerUp
(FileName: 'throwrelease.ogg'; Path: ptSounds; AltPath: ptNone),// sndThrowRelease
(FileName: 'splash.ogg'; Path: ptCurrTheme; AltPath: ptSounds),// sndSplash
(FileName: 'shotgunreload.ogg'; Path: ptSounds; AltPath: ptNone),// sndShotgunReload
(FileName: 'shotgunfire.ogg'; Path: ptSounds; AltPath: ptNone),// sndShotgunFire
(FileName: 'graveimpact.ogg'; Path: ptSounds; AltPath: ptNone),// sndGraveImpact
(FileName: 'mineimpact.ogg'; Path: ptSounds; AltPath: ptNone),// sndMineImpact
(FileName: 'minetick.ogg'; Path: ptSounds; AltPath: ptNone),// sndMineTicks
// TODO: New mudball sound?
(FileName: 'Droplet1.ogg'; Path: ptSounds; AltPath: ptNone),// sndMudballImpact
(FileName: 'pickhammer.ogg'; Path: ptSounds; AltPath: ptNone),// sndPickhammer
(FileName: 'gun.ogg'; Path: ptSounds; AltPath: ptNone),// sndGun
(FileName: 'bee.ogg'; Path: ptSounds; AltPath: ptNone),// sndBee
(FileName: 'Jump1.ogg'; Path: ptVoices; AltPath: ptNone),// sndJump1
(FileName: 'Jump2.ogg'; Path: ptVoices; AltPath: ptNone),// sndJump2
(FileName: 'Jump3.ogg'; Path: ptVoices; AltPath: ptNone),// sndJump3
(FileName: 'Yessir.ogg'; Path: ptVoices; AltPath: ptNone),// sndYesSir
(FileName: 'Laugh.ogg'; Path: ptVoices; AltPath: ptNone),// sndLaugh
(FileName: 'Illgetyou.ogg'; Path: ptVoices; AltPath: ptNone),// sndIllGetYou
(FileName: 'Justyouwait.ogg'; Path: ptVoices; AltPath: ptNone),// sndJustyouwait
(FileName: 'Incoming.ogg'; Path: ptVoices; AltPath: ptNone),// sndIncoming
(FileName: 'Missed.ogg'; Path: ptVoices; AltPath: ptNone),// sndMissed
(FileName: 'Stupid.ogg'; Path: ptVoices; AltPath: ptNone),// sndStupid
(FileName: 'Firstblood.ogg'; Path: ptVoices; AltPath: ptNone),// sndFirstBlood
(FileName: 'Boring.ogg'; Path: ptVoices; AltPath: ptNone),// sndBoring
(FileName: 'Byebye.ogg'; Path: ptVoices; AltPath: ptNone),// sndByeBye
(FileName: 'Sameteam.ogg'; Path: ptVoices; AltPath: ptNone),// sndSameTeam
(FileName: 'Nutter.ogg'; Path: ptVoices; AltPath: ptNone),// sndNutter
(FileName: 'Reinforcements.ogg'; Path: ptVoices; AltPath: ptNone),// sndReinforce
(FileName: 'Traitor.ogg'; Path: ptVoices; AltPath: ptNone),// sndTraitor
(FileName: 'Youllregretthat.ogg'; Path: ptVoices; AltPath: ptNone),// sndRegret
(FileName: 'Enemydown.ogg'; Path: ptVoices; AltPath: ptNone),// sndEnemyDown
(FileName: 'Coward.ogg'; Path: ptVoices; AltPath: ptNone),// sndCoward
(FileName: 'Hurry.ogg'; Path: ptVoices; AltPath: ptNone),// sndHurry
(FileName: 'Watchit.ogg'; Path: ptVoices; AltPath: ptNone),// sndWatchIt
(FileName: 'Kamikaze.ogg'; Path: ptVoices; AltPath: ptNone),// sndKamikaze
(FileName: 'cake2.ogg'; Path: ptSounds; AltPath: ptNone),// sndCake
(FileName: 'Ow1.ogg'; Path: ptVoices; AltPath: ptNone),// sndOw1
(FileName: 'Ow2.ogg'; Path: ptVoices; AltPath: ptNone),// sndOw2
(FileName: 'Ow3.ogg'; Path: ptVoices; AltPath: ptNone),// sndOw3
(FileName: 'Ow4.ogg'; Path: ptVoices; AltPath: ptNone),// sndOw4
(FileName: 'Firepunch1.ogg'; Path: ptVoices; AltPath: ptNone),// sndFirePunch1
(FileName: 'Firepunch2.ogg'; Path: ptVoices; AltPath: ptNone),// sndFirePunch2
(FileName: 'Firepunch3.ogg'; Path: ptVoices; AltPath: ptNone),// sndFirePunch3
(FileName: 'Firepunch4.ogg'; Path: ptVoices; AltPath: ptNone),// sndFirePunch4
(FileName: 'Firepunch5.ogg'; Path: ptVoices; AltPath: ptNone),// sndFirePunch5
(FileName: 'Firepunch6.ogg'; Path: ptVoices; AltPath: ptNone),// sndFirePunch6
(FileName: 'Melon.ogg'; Path: ptVoices; AltPath: ptNone),// sndMelon
(FileName: 'Hellish.ogg'; Path: ptSounds; AltPath: ptNone),// sndHellish
(FileName: 'Yoohoo.ogg'; Path: ptSounds; AltPath: ptNone),// sndYoohoo
(FileName: 'rcplane.ogg'; Path: ptSounds; AltPath: ptNone),// sndRCPlane
(FileName: 'whipcrack.ogg'; Path: ptSounds; AltPath: ptNone),// sndWhipCrack
(FileName:'ride_of_the_valkyries.ogg'; Path: ptSounds; AltPath: ptNone),// sndRideOfTheValkyries
(FileName: 'denied.ogg'; Path: ptSounds; AltPath: ptNone),// sndDenied
(FileName: 'placed.ogg'; Path: ptSounds; AltPath: ptNone),// sndPlaced
(FileName: 'baseballbat.ogg'; Path: ptSounds; AltPath: ptNone),// sndBaseballBat
(FileName: 'steam.ogg'; Path: ptSounds; AltPath: ptNone),// sndVaporize
(FileName: 'warp.ogg'; Path: ptSounds; AltPath: ptNone),// sndWarp
(FileName: 'suddendeath.ogg'; Path: ptSounds; AltPath: ptNone),// sndSuddenDeath
(FileName: 'mortar.ogg'; Path: ptSounds; AltPath: ptNone),// sndMortar
(FileName: 'shutterclick.ogg'; Path: ptSounds; AltPath: ptNone),// sndShutter
(FileName: 'homerun.ogg'; Path: ptSounds; AltPath: ptNone),// sndHomerun
(FileName: 'molotov.ogg'; Path: ptSounds; AltPath: ptNone),// sndMolotov
(FileName: 'Takecover.ogg'; Path: ptVoices; AltPath: ptNone),// sndCover
(FileName: 'Uh-oh.ogg'; Path: ptVoices; AltPath: ptNone),// sndUhOh
(FileName: 'Oops.ogg'; Path: ptVoices; AltPath: ptNone),// sndOops
(FileName: 'Nooo.ogg'; Path: ptVoices; AltPath: ptNone),// sndNooo
(FileName: 'Hello.ogg'; Path: ptVoices; AltPath: ptNone),// sndHello
(FileName: 'ropeshot.ogg'; Path: ptSounds; AltPath: ptNone),// sndRopeShot
(FileName: 'ropeattach.ogg'; Path: ptSounds; AltPath: ptNone),// sndRopeAttach
(FileName: 'roperelease.ogg'; Path: ptSounds; AltPath: ptNone),// sndRopeRelease
(FileName: 'switchhog.ogg'; Path: ptSounds; AltPath: ptNone),// sndSwitchHog
(FileName: 'Victory.ogg'; Path: ptVoices; AltPath: ptNone),// sndVictory
(FileName: 'Flawless.ogg'; Path: ptVoices; AltPath: ptNone),// sndFlawless
(FileName: 'sniperreload.ogg'; Path: ptSounds; AltPath: ptNone),// sndSniperReload
(FileName: 'steps.ogg'; Path: ptSounds; AltPath: ptNone),// sndSteps
(FileName: 'lowgravity.ogg'; Path: ptSounds; AltPath: ptNone),// sndLowGravity
(FileName: 'hell_growl.ogg'; Path: ptSounds; AltPath: ptNone),// sndHellishImpact1
(FileName: 'hell_ooff.ogg'; Path: ptSounds; AltPath: ptNone),// sndHellishImpact2
(FileName: 'hell_ow.ogg'; Path: ptSounds; AltPath: ptNone),// sndHellishImpact3
(FileName: 'hell_ugh.ogg'; Path: ptSounds; AltPath: ptNone),// sndHellishImpact4
(FileName: 'melonimpact.ogg'; Path: ptSounds; AltPath: ptNone),// sndMelonImpact
(FileName: 'Droplet1.ogg'; Path: ptCurrTheme; AltPath: ptSounds),// sndDroplet1
(FileName: 'Droplet2.ogg'; Path: ptCurrTheme; AltPath: ptSounds),// sndDroplet2
(FileName: 'Droplet3.ogg'; Path: ptCurrTheme; AltPath: ptSounds),// sndDroplet3
(FileName: 'egg.ogg'; Path: ptSounds; AltPath: ptNone),// sndEggBreak
(FileName: 'drillgun.ogg'; Path: ptSounds; AltPath: ptNone),// sndDrillRocket
(FileName: 'PoisonCough.ogg'; Path: ptVoices; AltPath: ptDefaultVoice),// sndPoisonCough
(FileName: 'PoisonMoan.ogg'; Path: ptVoices; AltPath: ptDefaultVoice),// sndPoisonMoan
(FileName: 'BirdyLay.ogg'; Path: ptSounds; AltPath: ptNone),// sndBirdyLay
(FileName: 'Whistle.ogg'; Path: ptSounds; AltPath: ptNone),// sndWhistle
(FileName: 'beewater.ogg'; Path: ptSounds; AltPath: ptNone),// sndBeeWater
(FileName: '1C.ogg'; Path: ptSounds; AltPath: ptNone),// sndPiano0
(FileName: '2D.ogg'; Path: ptSounds; AltPath: ptNone),// sndPiano1
(FileName: '3E.ogg'; Path: ptSounds; AltPath: ptNone),// sndPiano2
(FileName: '4F.ogg'; Path: ptSounds; AltPath: ptNone),// sndPiano3
(FileName: '5G.ogg'; Path: ptSounds; AltPath: ptNone),// sndPiano4
(FileName: '6A.ogg'; Path: ptSounds; AltPath: ptNone),// sndPiano5
(FileName: '7B.ogg'; Path: ptSounds; AltPath: ptNone),// sndPiano6
(FileName: '8C.ogg'; Path: ptSounds; AltPath: ptNone),// sndPiano7
(FileName: '9D.ogg'; Path: ptSounds; AltPath: ptNone),// sndPiano8
(FileName: 'skip.ogg'; Path: ptCurrTheme; AltPath: ptSounds),// sndSkip
(FileName: 'sinegun.ogg'; Path: ptSounds; AltPath: ptNone),// sndSineGun
(FileName: 'Ooff1.ogg'; Path: ptVoices; AltPath: ptNone),// sndOoff1
(FileName: 'Ooff2.ogg'; Path: ptVoices; AltPath: ptNone),// sndOoff2
(FileName: 'Ooff3.ogg'; Path: ptVoices; AltPath: ptNone),// sndOoff3
(FileName: 'hammer.ogg'; Path: ptSounds; AltPath: ptNone),// sndWhack
(FileName: 'Comeonthen.ogg'; Path: ptVoices; AltPath: ptNone),// sndComeonthen
(FileName: 'parachute.ogg'; Path: ptSounds; AltPath: ptNone),// sndParachute
(FileName: 'bump.ogg'; Path: ptSounds; AltPath: ptNone),// sndBump
(FileName: 'hogchant3.ogg'; Path: ptSounds; AltPath: ptNone),// sndResurrector
(FileName: 'plane.ogg'; Path: ptSounds; AltPath: ptNone),// sndPlane
(FileName: 'TARDIS.ogg'; Path: ptSounds; AltPath: ptNone),// sndTardis
(FileName: 'frozen_hog_impact.ogg'; Path: ptSounds; AltPath: ptNone),// sndFrozenHogImpact
(FileName: 'ice_beam.ogg'; Path: ptSounds; AltPath: ptNone),// sndIceBeam
(FileName: 'hog_freeze.ogg'; Path: ptSounds; AltPath: ptNone), // sndHogFreeze
(FileName: 'airmine_impact.ogg'; Path: ptSounds; AltPath: ptNone),// sndAirMineImpact
(FileName: 'knife_impact.ogg'; Path: ptSounds; AltPath: ptNone),// sndKnifeImpact
(FileName: 'extratime.ogg'; Path: ptSounds; AltPath: ptNone),// sndExtraTime
(FileName: 'lasersight.ogg'; Path: ptSounds; AltPath: ptNone),// sndLaserSight
(FileName: 'invulnerable.ogg'; Path: ptSounds; AltPath: ptNone),// sndInvulnerable
(FileName: 'ufo.ogg'; Path: ptSounds; AltPath: ptNone),// sndJetpackLaunch
(FileName: 'jetpackboost.ogg'; Path: ptSounds; AltPath: ptNone),// sndJetpackBoost
(FileName: 'portalshot.ogg'; Path: ptSounds; AltPath: ptNone),// sndPortalShot
(FileName: 'portalswitch.ogg'; Path: ptSounds; AltPath: ptNone),// sndPortalSwitch
(FileName: 'portalopen.ogg'; Path: ptSounds; AltPath: ptNone),// sndPortalOpen
(FileName: 'blowtorch.ogg'; Path: ptSounds; AltPath: ptNone),// sndBlowTorch
(FileName: 'countdown1.ogg'; Path: ptSounds; AltPath: ptNone),// sndCountdown1
(FileName: 'countdown2.ogg'; Path: ptSounds; AltPath: ptNone),// sndCountdown2
(FileName: 'countdown3.ogg'; Path: ptSounds; AltPath: ptNone),// sndCountdown3
(FileName: 'countdown4.ogg'; Path: ptSounds; AltPath: ptNone),// sndCountdown4
// TODO: Check which creeper (formerly rubberduck) sounds are needed, maybe rename them
(FileName: 'rubberduck_drop.ogg'; Path: ptSounds; AltPath: ptNone),// sndCreeperDrop
(FileName: 'rubberduck_water.ogg'; Path: ptSounds; AltPath: ptNone),// sndCreeperWater
(FileName: 'rubberduck_die.ogg'; Path: ptSounds; AltPath: ptNone),// sndCreeperDie
(FileName: 'custom1.ogg'; Path: ptSounds; AltPath: ptNone),// sndCustom1
(FileName: 'custom2.ogg'; Path: ptSounds; AltPath: ptNone),// sndCustom2
(FileName: 'custom3.ogg'; Path: ptSounds; AltPath: ptNone),// sndCustom3
(FileName: 'custom4.ogg'; Path: ptSounds; AltPath: ptNone),// sndCustom4
(FileName: 'custom5.ogg'; Path: ptSounds; AltPath: ptNone),// sndCustom5
(FileName: 'custom6.ogg'; Path: ptSounds; AltPath: ptNone),// sndCustom6
(FileName: 'custom7.ogg'; Path: ptSounds; AltPath: ptNone),// sndCustom7
(FileName: 'custom8.ogg'; Path: ptSounds; AltPath: ptNone),// sndCustom8
(FileName: 'minigun.ogg'; Path: ptSounds; AltPath: ptNone),// sndMinigun
(FileName: 'flamethrower.ogg'; Path: ptSounds; AltPath: ptNone),// sndFlamethrower
(FileName: 'ice_beam_idle.ogg'; Path: ptSounds; AltPath: ptNone),// sndIceBeamIdle
(FileName: 'landgun.ogg'; Path: ptSounds; AltPath: ptNone),// sndLandGun
(FileName: 'graveimpact.ogg'; Path: ptSounds; AltPath: ptNone),// sndCaseImpact
// TODO: New Extra Damage sound
(FileName: 'hell_ugh.ogg'; Path: ptSounds; AltPath: ptNone),// sndExtraDamage
(FileName: 'firepunch_hit.ogg'; Path: ptSounds; AltPath: ptNone),// sndFirePunchHit
(FileName: 'Grenade.ogg'; Path: ptVoices; AltPath: ptNone),// sndGrenade
(FileName: 'Thisoneismine.ogg'; Path: ptVoices; AltPath: ptNone),// sndThisOneIsMine
(FileName: 'Whatthe.ogg'; Path: ptVoices; AltPath: ptNone),// sndWhatThe
(FileName: 'Solong.ogg'; Path: ptVoices; AltPath: ptNone),// sndSoLong
(FileName: 'Ohdear.ogg'; Path: ptVoices; AltPath: ptNone),// sndOhDear
(FileName: 'Gonnagetyou.ogg'; Path: ptVoices; AltPath: ptNone),// sndGonnaGetYou
(FileName: 'Drat.ogg'; Path: ptVoices; AltPath: ptNone),// sndDrat
(FileName: 'Bugger.ogg'; Path: ptVoices; AltPath: ptNone),// sndBugger
(FileName: 'Amazing.ogg'; Path: ptVoices; AltPath: ptNone),// sndAmazing
(FileName: 'Brilliant.ogg'; Path: ptVoices; AltPath: ptNone),// sndBrilliant
(FileName: 'Excellent.ogg'; Path: ptVoices; AltPath: ptNone),// sndExcellent
(FileName: 'Fire.ogg'; Path: ptVoices; AltPath: ptNone),// sndFire
(FileName: 'Watchthis.ogg'; Path: ptVoices; AltPath: ptNone),// sndWatchThis
(FileName: 'Runaway.ogg'; Path: ptVoices; AltPath: ptNone),// sndRunAway
(FileName: 'Revenge.ogg'; Path: ptVoices; AltPath: ptNone),// sndRevenge
(FileName: 'Cutitout.ogg'; Path: ptVoices; AltPath: ptNone),// sndCutItOut
(FileName: 'Leavemealone.ogg'; Path: ptVoices; AltPath: ptNone),// sndLeaveMeAlone
(FileName: 'Ouch.ogg'; Path: ptVoices; AltPath: ptNone),// sndOuch
(FileName: 'Hmm.ogg'; Path: ptVoices; AltPath: ptNone),// sndHmm
(FileName: 'Kiss.ogg'; Path: ptSounds; AltPath: ptNone),// sndKiss
(FileName: 'Flyaway.ogg'; Path: ptVoices; AltPath: ptNone),// sndFlyAway
(FileName: 'planewater.ogg'; Path: ptSounds; AltPath: ptNone) // sndPlaneWater
);
function AskForVoicepack(name: shortstring): Pointer;
var i: Longword;
tmp, nameStart, langName, path: shortstring;
begin
nameStart:= name;
i:= 0;
{ Adjust for language suffix: Voicepacks can have an optional language suffix.
It's an underscore followed by an ISO 639-1 or ISO 639-2 language code.
The suffix “_qau” is special, it will enable automatic language selection
of this voicepack. For example, if team has set Default_qau as voicepack,
and the player language is Russian, the actual voicepack will be Default_ru,
provided it can be found on the disk.
“qau” is a valid ISO 639-2 language code reserved for local use. }
tmp:= Copy(name, Length(name) - 3, 4);
if (tmp = '_qau') then
name:= Copy(name, 1, Length(name) - 4);
if (cLanguage <> 'en') and (tmp = '_qau') then
begin
langName:= name+'_'+cLanguage;
path:= cPathz[ptVoices] + '/' + langName;
if pfsExists(path) then
name:= langName
else
if Length(cLanguage) > 3 then
begin
langName:= name+'_'+Copy(cLanguage,1,2);
path:= cPathz[ptVoices] + '/' + langName;
if pfsExists(path) then
name:= langName
end
end;
path:= cPathz[ptVoices] + '/' + name;
// Fallback to localized Default if voicepack can't be found at all
if (nameStart <> 'Default_qau') and (not pfsExists(path)) then
exit(AskForVoicepack('Default_qau'));
while (voicepacks[i].name <> name) and (voicepacks[i].name <> '') and (i < cMaxTeams) do
begin
inc(i);
//TryDo(i <= cMaxTeams, 'Engine bug: AskForVoicepack i > cMaxTeams', true)
end;
voicepacks[i].name:= name;
AskForVoicepack:= @voicepacks[i]
end;
procedure InitSound;
const channels: LongInt = 2;
var success: boolean;
s: shortstring;
begin
if not (isSoundEnabled or isMusicEnabled) then
begin
isAudioMuted:= true;
cInitVolume:= 0;
exit;
end;
WriteToConsole('Init sound...');
success:= SDL_InitSubSystem(SDL_INIT_AUDIO) = 0;
if success then
begin
WriteLnToConsole(msgOK);
WriteToConsole('Open audio...');
success:= Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, channels, 1024) = 0;
end;
if success then
WriteLnToConsole(msgOK)
else
begin
WriteLnToConsole(msgFailed);
isSoundEnabled:= false;
isMusicEnabled:= false;
isAudioMuted:= true;
cInitVolume:= 0;
end;
WriteToConsole('Init SDL_mixer... ');
if (Mix_Init(MIX_INIT_OGG or MIX_INIT_OPUS) and MIX_INIT_OPUS) = 0 then
begin
s:= SDL_GetError();
WriteToConsole('Cannot init OPUS: ' + s);
if SDLCheck(Mix_Init(MIX_INIT_OGG) <> 0, 'Mix_Init', true) then exit;
end;
WriteLnToConsole(msgOK);
// from uVariables to be used by other modules
cIsSoundEnabled:= true;
Mix_AllocateChannels(Succ(chanTPU));
previousVolume:= cInitVolume;
ChangeVolume(cInitVolume);
end;
procedure ResetSound;
begin
isSoundEnabled:= isSEBackup;
end;
procedure SetSound(enabled: boolean);
begin
isSEBackup:= isSoundEnabled;
isSoundEnabled:= enabled;
end;
procedure SetAudioDampen(enabled: boolean);
begin
isAutoDampening:= enabled;
end;
// when complete is false, this procedure just releases some of the chucks on inactive channels
// in this way music is not stopped, nor are chucks currently being played
procedure ReleaseSound(complete: boolean);
var i: TSound;
t: Longword;
begin
// release and nil all sounds
for t:= 0 to cMaxTeams do
if voicepacks[t].name <> '' then
for i:= Low(TSound) to High(TSound) do
if voicepacks[t].chunks[i] <> nil then
if complete or (Mix_Playing(lastChan[i]) = 0) then
begin
Mix_HaltChannel(lastChan[i]);
lastChan[i]:= -1;
Mix_FreeChunk(voicepacks[t].chunks[i]);
voicepacks[t].chunks[i]:= nil;
end;
// stop music
if complete then
begin
if Mus <> nil then
begin
Mix_HaltMusic();
Mix_FreeMusic(Mus);
Mus:= nil;
end;
// make sure all instances of sdl_mixer are unloaded before continuing
while Mix_Init(0) <> 0 do
Mix_Quit();
Mix_CloseAudio();
end;
end;
// Get a fallback voice, assuming that snd is not available. Returns sndNone if none is found.
function GetFallbackV(snd: TSound): TSound;
begin
// Fallback to sndFirePunch1 / sndOw1 / sndOoff1 if a "higher-numbered" sound is missing
if (snd in [sndFirePunch2, sndFirePunch3, sndFirePunch4, sndFirePunch5, sndFirePunch6]) then
GetFallbackV := sndFirePunch1
else if (snd in [sndOw2, sndOw3, sndOw4, sndOuch]) then
GetFallbackV := sndOw1
else if (snd in [sndOoff2, sndOoff3]) then
GetFallbackV := sndOoff1
// Other fallback sounds
else if (snd = sndGrenade) then
if random(2) = 0 then
GetFallbackV := sndNooo
else
GetFallbackV := sndUhOh
else if (snd in [sndDrat, sndBugger]) then
GetFallbackV := sndStupid
else if (snd in [sndGonnaGetYou, sndIllGetYou, sndJustYouWait, sndCutItOut, sndLeaveMeAlone]) then
GetFallbackV := sndRegret
else if (snd in [sndOhDear, sndSoLong]) then
GetFallbackV := sndByeBye
else if (snd in [sndWhatThe, sndUhOh]) then
GetFallbackV := sndNooo
else if (snd = sndRunAway) then
GetFallbackV := sndOops
else if (snd = sndThisOneIsMine) then
GetFallbackV := sndReinforce
else if (snd in [sndAmazing, sndBrilliant, sndExcellent]) then
GetFallbackV := sndEnemyDown
else if (snd = sndPoisonCough) then
GetFallbackV := sndPoisonMoan
else if (snd = sndPoisonMoan) then
GetFallbackV := sndPoisonCough
else if (snd = sndFlawless) then
GetFallbackV := sndVictory
else if (snd = sndSameTeam) then
GetFallbackV := sndTraitor
else if (snd = sndMelon) then
GetFallbackV := sndCover
// sndHmm is used for enemy turn start, so sndHello is an "okay" replacement
else if (snd = sndHmm) then
GetFallbackV := sndHello
else
GetFallbackV := sndNone;
end;
function PlaySound(snd: TSound): boolean;
begin
PlaySound:= PlaySoundV(snd, nil, false, false, false);
end;
function PlaySound(snd: TSound; keepPlaying: boolean): boolean;
begin
PlaySound:= PlaySoundV(snd, nil, keepPlaying, false, false);
end;
function PlaySound(snd: TSound; keepPlaying: boolean; ignoreMask: boolean): boolean;
begin
PlaySound:= PlaySoundV(snd, nil, keepPlaying, ignoreMask, false);
end;
function PlaySound(snd: TSound; keepPlaying: boolean; ignoreMask, soundAsMusic: boolean): boolean;
begin
PlaySound:= PlaySoundV(snd, nil, keepPlaying, ignoreMask, soundAsMusic);
end;
function PlaySoundV(snd: TSound; voicepack: PVoicepack): boolean;
begin
PlaySoundV:= PlaySoundV(snd, voicepack, false, false, false);
end;
function PlaySoundV(snd: TSound; voicepack: PVoicepack; keepPlaying: boolean): boolean;
begin
PlaySoundV:= PlaySoundV(snd, voicepack, keepPlaying, false, false);
end;
function PlaySoundV(snd: TSound; voicepack: PVoicepack; keepPlaying, ignoreMask: boolean): boolean;
begin
PlaySoundV:= PlaySoundV(snd, voicepack, keepPlaying, ignoreMask, false);
end;
function PlaySoundV(snd: TSound; voicepack: PVoicepack; keepPlaying, ignoreMask, soundAsMusic: boolean): boolean;
var s: shortstring;
tempSnd, loadSnd: TSound;
rwops: PSDL_RWops;
begin
PlaySoundV:= false;
if ((not isSoundEnabled) and (not (soundAsMusic and isMusicEnabled))) or fastUntilLag then
exit;
if keepPlaying and (lastChan[snd] <> -1) and (Mix_Playing(lastChan[snd]) <> 0) then
exit;
if (ignoreMask = false) and (MaskedSounds[snd] = true) then
exit;
if (voicepack <> nil) then
begin
if (voicepack^.chunks[snd] = nil) and (Soundz[snd].Path = ptVoices) and (Soundz[snd].FileName <> '') then
begin
loadSnd:= snd;
s:= cPathz[Soundz[loadSnd].Path] + '/' + voicepack^.name + '/' + Soundz[loadSnd].FileName;
// Fallback taunts
if (not pfsExists(s)) then
begin
tempSnd := GetFallbackV(snd);
if tempSnd <> sndNone then
begin
loadSnd := tempSnd;
//LastVoice.snd := tempSnd;
end;
s:= cPathz[Soundz[loadSnd].Path] + '/' + voicepack^.name + '/' + Soundz[loadSnd].FileName;
end;
WriteToConsole(msgLoading + s + ' ... ');
rwops := rwopsOpenRead(s);
if rwops = nil then
begin
s:= cPathz[Soundz[loadSnd].AltPath] + '/' + Soundz[loadSnd].FileName;
WriteToConsole(msgLoading + s + ' ... ');
rwops := rwopsOpenRead(s);
end;
voicepack^.chunks[snd]:= Mix_LoadWAV_RW(rwops, 1);
if voicepack^.chunks[snd] = nil then
WriteLnToConsole(msgFailed)
else
WriteLnToConsole(msgOK)
end;
lastChan[snd]:= Mix_PlayChannelTimed(-1, voicepack^.chunks[snd], 0, -1);
PlaySoundV:= true;
end
else
begin
if (defVoicepack^.chunks[snd] = nil) and (Soundz[snd].Path <> ptVoices) and (Soundz[snd].FileName <> '') then
begin
s:= cPathz[Soundz[snd].Path] + '/' + Soundz[snd].FileName;
WriteToConsole(msgLoading + s + ' ... ');
rwops := rwopsOpenRead(s);
if rwops = nil then
begin
s:= cPathz[Soundz[snd].AltPath] + '/' + Soundz[snd].FileName;
WriteToConsole(msgLoading + s + ' ... ');
rwops := rwopsOpenRead(s);
end;
defVoicepack^.chunks[snd]:= Mix_LoadWAV_RW(rwops, 1);
if SDLCheck(defVoicepack^.chunks[snd] <> nil, 'Mix_LoadWAV_RW', true) then exit;
WriteLnToConsole(msgOK);
end;
lastChan[snd]:= Mix_PlayChannelTimed(-1, defVoicepack^.chunks[snd], 0, -1);
PlaySoundV:= true;
end;
end;
procedure PlayMusicSound(snd: TSound);
begin
PauseMusic;
PlaySound(snd, false, false, true);
end;
procedure StopMusicSound(snd: TSound);
begin
StopSound(snd, true);
ResumeMusic;
end;
procedure AddVoice(snd: TSound; voicepack: PVoicepack);
begin
AddVoice(snd, voicepack, false, false);
end;
{
AddVoice: Add a voice to the voice queue.
* snd: Sound ID
* voicepack: Hedgehog voicepack
* ignoreMask: If true, the sound will be played anyway if masked by Lua
* isFallback: If true, this sound is added as fallback if the sound previously added to the
queue was not found. Useful to make sure a voice is always played, even if
a voicepack is incomplete.
Example:
AddVoice(sndRevenge, voiceAttacker);
AddVoice(sndRegret, voiceVictim, false, true);
--> plays sndRegret if sndRevenge could not be played.
}
procedure AddVoice(snd: TSound; voicepack: PVoicepack; ignoreMask, isFallback: boolean);
var i : LongInt;
begin
if (not isSoundEnabled) or fastUntilLag or ((LastVoice.snd = snd) and (LastVoice.voicepack = voicepack)) then
exit;
if (ignoreMask = false) and (MaskedSounds[snd] = true) then
exit;
if (snd = sndVictory) or (snd = sndFlawless) then
begin
Mix_FadeOutChannel(-1, 800);
for i:= 0 to High(VoiceList) do
VoiceList[i].snd:= sndNone;
LastVoice.snd:= sndNone;
end;
i:= 0;
while (i <= High(VoiceList)) and (VoiceList[i].snd <> sndNone) do
inc(i);
// skip playing same sound for same hog twice
if (i>0) and (VoiceList[i-1].snd = snd) and (VoiceList[i-1].voicepack = voicepack) then
exit;
if(i <= High(VoiceList)) then
begin
VoiceList[i].snd:= snd;
VoiceList[i].voicepack:= voicepack;
VoiceList[i].isFallback:= isFallback;
end
end;
procedure PlayNextVoice;
var i : LongInt;
played : boolean;
begin
if (not isSoundEnabled) or fastUntilLag or ((LastVoice.snd <> sndNone) and (lastChan[LastVoice.snd] <> -1) and (Mix_Playing(lastChan[LastVoice.snd]) <> 0)) then
exit;
i:= 0;
while (i<High(VoiceList)) and (VoiceList[i].snd = sndNone) do
inc(i);
played:= false;
if (VoiceList[i].snd <> sndNone) and ((not VoiceList[i].isFallback) or LastVoiceFailed) then
begin
LastVoice.snd:= VoiceList[i].snd;
LastVoice.voicepack:= VoiceList[i].voicepack;
LastVoice.isFallback:= VoiceList[i].isFallback;
VoiceList[i].snd:= sndNone;
played:= PlaySoundV(LastVoice.snd, LastVoice.voicepack);
// Remember if sound was not played.
LastVoiceFailed:= (not played);
end
else
LastVoice.snd:= sndNone;
end;
function LoopSound(snd: TSound): LongInt;
begin
LoopSound:= LoopSoundV(snd, nil)
end;
function LoopSound(snd: TSound; fadems: LongInt): LongInt;
begin
LoopSound:= LoopSoundV(snd, nil, fadems)
end;
function LoopSoundV(snd: TSound; voicepack: PVoicepack): LongInt;
begin
voicepack:= voicepack; // avoid compiler hint
LoopSoundV:= LoopSoundV(snd, nil, 0)
end;
function LoopSoundV(snd: TSound; voicepack: PVoicepack; fadems: LongInt): LongInt;
var s: shortstring;
rwops: PSDL_RWops;
begin
if (not isSoundEnabled) or fastUntilLag then
begin
LoopSoundV:= -1;
exit
end;
if (voicepack <> nil) then
begin
if (voicepack^.chunks[snd] = nil) and (Soundz[snd].Path = ptVoices) and (Soundz[snd].FileName <> '') then
begin
s:= cPathz[Soundz[snd].Path] + '/' + voicepack^.name + '/' + Soundz[snd].FileName;
WriteToConsole(msgLoading + s + ' ');
rwops:=rwopsOpenRead(s);
if rwops = nil then
begin
s:= cPathz[Soundz[snd].AltPath] + '/' + Soundz[snd].FileName;
WriteToConsole(msgLoading + s + ' ... ');
rwops:=rwopsOpenRead(s);
end;
voicepack^.chunks[snd]:= Mix_LoadWAV_RW(rwops, 1);
if voicepack^.chunks[snd] = nil then
WriteLnToConsole(msgFailed)
else
WriteLnToConsole(msgOK)
end;
LoopSoundV:= Mix_PlayChannelTimed(-1, voicepack^.chunks[snd], -1, -1)
end
else
begin
if (defVoicepack^.chunks[snd] = nil) and (Soundz[snd].Path <> ptVoices) and (Soundz[snd].FileName <> '') then
begin
s:= cPathz[Soundz[snd].Path] + '/' + Soundz[snd].FileName;
WriteToConsole(msgLoading + s + ' ');
defVoicepack^.chunks[snd]:= Mix_LoadWAV_RW(rwopsOpenRead(s), 1);
if SDLCheck(defVoicepack^.chunks[snd] <> nil, 'Mix_LoadWAV_RW', true) then exit(-1);
WriteLnToConsole(msgOK);
end;
if fadems > 0 then
LoopSoundV:= Mix_FadeInChannelTimed(-1, defVoicepack^.chunks[snd], -1, fadems, -1)
else
LoopSoundV:= Mix_PlayChannelTimed(-1, defVoicepack^.chunks[snd], -1, -1);
end;
end;
procedure StopSound(snd: TSound);
begin
StopSound(snd, false);
end;
procedure StopSound(snd: TSound; soundAsMusic: boolean);
begin
if ((not isSoundEnabled) and (not (soundAsMusic and isMusicEnabled))) then
exit;
if (lastChan[snd] <> -1) and (Mix_Playing(lastChan[snd]) <> 0) then
begin
Mix_HaltChannel(lastChan[snd]);
lastChan[snd]:= -1;
end;
end;
procedure StopSoundChan(chn: LongInt);
begin
if not isSoundEnabled then
exit;
if (chn <> -1) and (Mix_Playing(chn) <> 0) then
Mix_HaltChannel(chn);
end;
procedure StopSoundChan(chn, fadems: LongInt);
begin
if not isSoundEnabled then
exit;
if (chn <> -1) and (Mix_Playing(chn) <> 0) then
if isAudioMuted then
Mix_HaltChannel(chn)
else
Mix_FadeOutChannel(chn, fadems);
end;
procedure PlayMusic;
var s: shortstring;
begin
if (MusicFN = '') or (not isMusicEnabled) then
exit;
if SuddenDeath and (SDMusicFN <> '') then
s:= '/Music/' + SDMusicFN
else s:= '/Music/' + MusicFN;
WriteToConsole(msgLoading + s + ' ');
// Load normal music
Mus:= Mix_LoadMUS_RW(rwopsOpenRead(s));
SDLCheck(Mus <> nil, 'Mix_LoadMUS_RW', false);
if Mus <> nil then
WriteLnToConsole(msgOK);
// If normal music failed, try to get fallback music
if Mus = nil then
begin
WriteLnToConsole('Music not found. Trying fallback music.');
if SuddenDeath and (FallbackSDMusicFN <> '') then
s:= '/Music/' + FallbackSDMusicFN
else if (not SuddenDeath) and (FallbackMusicFN <> '') then
s:= '/Music/' + FallbackMusicFN
else
begin
WriteLnToConsole('No fallback music configured!');
s:= ''
end;
if (s <> '') then
begin
WriteLnToConsole(msgLoading + s + ' ');
Mus:= Mix_LoadMUS_RW(rwopsOpenRead(s));
SDLCheck(Mus <> nil, 'Mix_LoadMUS_RW', false);
if Mus <> nil then
WriteLnToConsole(msgOK)
end;
end;
SDLCheck(Mix_FadeInMusic(Mus, -1, 3000) <> -1, 'Mix_FadeInMusic', false)
end;
procedure SetVolume(vol: LongInt);
begin
cInitVolume:= vol;
end;
function GetVolumePercent(): LongInt;
begin
GetVolumePercent:= Volume * 100 div MIX_MAX_VOLUME;
// 0 and 100 will only be displayed when at min/max values
// to avoid confusion.
if ((GetVolumePercent = 0) and (Volume > 0)) then
GetVolumePercent:= 1
else if ((GetVolumePercent = 100) and (Volume < MIX_MAX_VOLUME)) then
GetVolumePercent:= 99;
end;
function ChangeVolume(voldelta: LongInt): LongInt;
begin
ChangeVolume:= 0;
if not (isSoundEnabled or isMusicEnabled) or ((voldelta = 0) and (not (cInitVolume = 0))) then
exit;
inc(Volume, voldelta);
if Volume < 0 then
Volume:= 0;
// apply Volume to all channels
Mix_Volume(-1, Volume);
// get assigned Volume
Volume:= Mix_Volume(-1, -1);
if isMusicEnabled then
Mix_VolumeMusic(Volume * 4 div 8);
ChangeVolume:= GetVolumePercent();
if (isMusicEnabled) then
if (Volume = 0) then
PauseMusic
else
ResumeMusic;
isAudioMuted:= (Volume = 0);
end;
procedure DampenAudio;
begin
if (isAudioMuted or (not isAutoDampening)) then
exit;
previousVolume:= Volume;
ChangeVolume(-Volume * 7 div 9);
end;
procedure UndampenAudio;
begin
if (isAudioMuted or (not isAutoDampening)) then
exit;
ChangeVolume(previousVolume - Volume);
end;
procedure MuteAudio;
begin
if not (isSoundEnabled or isMusicEnabled) then
exit;
if (isAudioMuted) then
begin
ResumeMusic;
ChangeVolume(previousVolume);
end
else
begin
PauseMusic;
previousVolume:= Volume;
ChangeVolume(-Volume);
end;
// isAudioMuted is updated in ChangeVolume
end;
procedure SetMusic(enabled: boolean);
begin
isMusicEnabled:= enabled;
end;
procedure SetMusicName(musicname: shortstring);
begin
MusicFN:= musicname;
end;
procedure PauseMusic;
begin
if (MusicFN = '') or (not isMusicEnabled) then
exit;
if Mus <> nil then
Mix_PauseMusic(Mus);
end;
procedure ResumeMusic;
begin
if (MusicFN = '') or (not isMusicEnabled) then
exit;
if Mus <> nil then
Mix_ResumeMusic(Mus);
end;
procedure ChangeMusic(musicname: shortstring);
begin
MusicFN:= musicname;
if (MusicFN = '') or (not isMusicEnabled) then
exit;
StopMusic;
PlayMusic;
end;
procedure StopMusic;
begin
if (MusicFN = '') or (not isMusicEnabled) then
exit;
if Mus <> nil then
begin
Mix_FreeMusic(Mus);
Mus:= nil;
end
end;
procedure chVoicepack(var s: shortstring);
begin
if CurrentTeam = nil then
OutError(errmsgIncorrectUse + ' "/voicepack"', true);
if s[1]='"' then Delete(s, 1, 1);
if s[byte(s[0])]='"' then
Delete(s, byte(s[0]), 1);
CurrentTeam^.voicepack:= AskForVoicepack(s)
end;
procedure preInitModule;
begin
isMusicEnabled:= true;
isSoundEnabled:= true;
isAutoDampening:= true;
cInitVolume:= 100;
end;
procedure initModule;
var t: LongInt;
i: TSound;
begin
RegisterVariable('voicepack', @chVoicepack, false);
MusicFN:='';
SDMusicFN:= 'sdmusic.ogg';
FallbackMusicFN:='';
FallbackSDMusicFN:= 'sdmusic.ogg';
Mus:= nil;
isAudioMuted:= false;
isSEBackup:= isSoundEnabled;
Volume:= 0;
SoundTimerTicks:= 0;
defVoicepack:= AskForVoicepack('Default_qau');
LastVoiceFailed:= false;
for i:= Low(TSound) to High(TSound) do
lastChan[i]:= -1;
// initialize all voices to nil so that they can be loaded lazily
for t:= 0 to cMaxTeams do
if voicepacks[t].name <> '' then
for i:= Low(TSound) to High(TSound) do
voicepacks[t].chunks[i]:= nil;
(* on MOBILE SDL_mixer has to be compiled against Tremor (USE_OGG_TREMOR)
or sound files bigger than 32k will lockup the game on slow cpu *)
for i:= Low(TSound) to High(TSound) do
defVoicepack^.chunks[i]:= nil;
end;
procedure freeModule;
begin
if isSoundEnabled or isMusicEnabled then
ReleaseSound(true);
end;
end.