Files
assaultcube-/source/src/audiomanager.cpp
T
ac-stef e318ecd68e integrated automatic embedded map config files
This patch enables an option to keep the map config embedded in the map
file - with autogenerated content. Any changes to map config data will be
written with the map. This includes data by loadnotexture, loadsky,
mapmodel, texture, fog, fogcolour, mapsound and shadowyaw. Once enabled,
all changes with those commands become persistent to the map.

New variables:

mapsoundchanged
  Set to "1" with every command that changes mapsound slots. Should be
  used to trigger a rebuild of mapsound menus.

New/changed commands:

mapsound
  Now returns the number of the created slot.

mapmodel
  Now returns the number of the created slot.

texture
  Now returns the number of the created slot.

loadsky
  Is now a builtin string variable (instead of a command), which means,
  that the current value can easily be retrieved or edited.
  The skymap name in the argument is now required to start with
  "textures/skymaps/" - but that part of the path can be omitted.
  examples:
    loadsky steini/steini2 (loads textures/skymaps/steini/steini2_*)
    loadsky textures/skymaps/steini/steini2 (same)
    loadsky ../foo/bar (loads textures/foo/bar_*)
    echo $loadsky

getnotexture
  Returns the current "notexture" path. (Set by loadnotexture)

dumpmapconfig (available for debug binaries only)
  Writes the current automatically generated map config to
  dumpmapconfig.txt.

DOCREF mapsoundchanged mapsound mapmodel texture loadsky getnotexture
2015-05-18 14:46:03 +02:00

837 lines
21 KiB
C++

// audio interface of the engine
#include "cube.h"
#define DEBUGCOND (audiodebug==1)
VARF(audio, 0, 1, 1, initwarning("sound configuration", INIT_RESET, CHANGE_SOUND));
VARP(audiodebug, 0, 0, 1);
char *musicdonecmd = NULL;
VARFP(musicvol, 0, 128, 255, audiomgr.setmusicvol(musicvol));
// audio manager
audiomanager::audiomanager()
{
nosound = true;
currentpitch = 1.0f;
device = NULL;
context = NULL;
}
void audiomanager::initsound()
{
if(!audio)
{
conoutf("audio is disabled");
return;
}
nosound = true;
device = NULL;
context = NULL;
// list available devices
if(alcIsExtensionPresent(NULL, "ALC_ENUMERATION_EXT"))
{
const ALCchar *devices = alcGetString(NULL, ALC_DEVICE_SPECIFIER);
if(devices)
{
string d;
copystring(d, "Audio devices: ");
// null separated device string
for(const ALchar *c = devices; *c; c += strlen(c)+1)
{
if(c!=devices) concatstring(d, ", ");
concatstring(d, c);
}
conoutf("%s", d);
}
}
// open device
const char *devicename = getalias("openaldevice");
device = alcOpenDevice(devicename && devicename[0] ? devicename : NULL);
if(device)
{
context = alcCreateContext(device, NULL);
if(context)
{
alcMakeContextCurrent(context);
alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED);
// backend infos
conoutf("Sound: %s / %s (%s)", alcGetString(device, ALC_DEVICE_SPECIFIER), alGetString(AL_RENDERER), alGetString(AL_VENDOR));
conoutf("Driver: %s", alGetString(AL_VERSION));
// allocate OpenAL resources
sourcescheduler::instance().init(16);
// let the stream get the first source from the scheduler
gamemusic = new oggstream();
if(!gamemusic->valid) DELETEP(gamemusic);
setmusicvol(musicvol);
nosound = false;
}
}
if(nosound)
{
ALERR;
if(context) alcDestroyContext(context);
if(device) alcCloseDevice(device);
conoutf("sound initialization failed!");
}
}
void audiomanager::music(char *name, int millis, char *cmd)
{
if(nosound) return;
stopsound();
if(musicvol && *name)
{
if(cmd[0]) musicdonecmd = newstring(cmd);
if(gamemusic->open(name))
{
// fade
if(millis > 0)
{
const int fadetime = 1000;
gamemusic->fadein(lastmillis, fadetime);
gamemusic->fadeout(lastmillis+millis, fadetime);
}
// play
bool loop = cmd && cmd[0];
if(!gamemusic->playback(loop))
{
conoutf("could not play music: %s", name);
return;
}
setmusicvol(musicvol);
}
else conoutf("could not open music: %s", name);
}
}
void audiomanager::musicpreload(int id)
{
if(nosound) return;
stopsound();
if(musicvol && (id>=M_FLAGGRAB && id<=M_LASTMINUTE2))
{
char *name = musics[id];
conoutf("preloading music #%d : %s", id, name);
if(gamemusic->open(name))
{
defformatstring(whendone)("musicvol %d", musicvol);
musicdonecmd = newstring(whendone);
//conoutf("when done: %s", musicdonecmd);
const int preloadfadetime = 3;
gamemusic->fadein(lastmillis, preloadfadetime);
gamemusic->fadeout(lastmillis+2*preloadfadetime, preloadfadetime);
if(!gamemusic->playback(false))
{
conoutf("could not play music: %s", name);
return;
}
setmusicvol(1); // not 0 !
}
else conoutf("could not open music: %s", name);
}
else setmusicvol(musicvol); // call "musicpreload -1" to ensure musicdonecmd runs - but it should w/o that
}
void audiomanager::musicsuggest(int id, int millis, bool rndofs) // play bg music if nothing else is playing
{
if(nosound || !gamemusic) return;
if(gamemusic->playing()) return;
if(!musicvol) return;
if(!musics.inrange(id))
{
conoutf("\f3music %d not registered", id);
return;
}
char *name = musics[id];
if(gamemusic->open(name))
{
gamemusic->fadein(lastmillis, 1000);
gamemusic->fadeout(millis ? lastmillis+millis : 0, 1000);
if(rndofs) gamemusic->seek(millis ? (double)rnd(millis)/2.0f : (double)lastmillis);
if(!gamemusic->playback(rndofs)) conoutf("could not play music: %s", name);
}
else conoutf("could not open music: %s", name);
}
void audiomanager::musicfadeout(int id)
{
if(nosound || !gamemusic) return;
if(!gamemusic->playing() || !musics.inrange(id)) return;
if(!strcmp(musics[id], gamemusic->name)) gamemusic->fadeout(lastmillis+1000, 1000);
}
void audiomanager::setmusicvol(int musicvol)
{
if(gamemusic) gamemusic->setvolume(musicvol > 0 ? musicvol/255.0f : 0);
}
void audiomanager::setlistenervol(int vol)
{
if(!nosound) alListenerf(AL_GAIN, vol/255.0f);
}
void audiomanager::registermusic(char *name)
{
if(nosound||!musicvol) return;
if(!name || !name[0]) return;
musics.add(newstring(name));
}
int audiomanager::findsound(char *name, int vol, vector<soundconfig> &sounds)
{
loopv(sounds)
{
if(!strcmp(sounds[i].buf->name, name) && (!vol || sounds[i].vol==vol)) return i;
}
return -1;
}
int audiomanager::addsound(char *name, int vol, int maxuses, bool loop, vector<soundconfig> &sounds, bool load, int audibleradius)
{
if(nosound) return -1;
if(!soundvol) return -1;
// check if the sound was already registered
int index = findsound(name, vol, sounds);
if(index > -1) return index;
sbuffer *b = bufferpool.find(name);
if(!b)
{
conoutf("\f3failed to allocate sample %s", name);
return -1;
}
if(load && !b->load()) conoutf("\f3failed to load sample %s", name);
soundconfig s(b, vol, maxuses, loop, audibleradius);
sounds.add(s);
return sounds.length()-1;
}
void audiomanager::preloadmapsound(entity &e, bool trydl)
{
if(e.type!=SOUND || !mapsounds.inrange(e.attr1)) return;
sbuffer *buf = mapsounds[e.attr1].buf;
if(!buf->load(trydl) && !trydl) conoutf("\f3failed to load sample %s", buf->name);
}
void audiomanager::preloadmapsounds(bool trydl)
{
loopv(ents)
{
entity &e = ents[i];
if(e.type==SOUND) preloadmapsound(e, trydl);
}
}
void audiomanager::applymapsoundchanges() // during map editing, drop all mapsounds so they can be re-added
{
loopv(locations)
{
location *l = locations[i];
if(l && l->ref && l->ref->type==worldobjreference::WR_ENTITY) l->drop();
}
}
void audiomanager::setchannels(int num)
{
if(!nosound) sourcescheduler::instance().init(num);
};
// called at game exit
void audiomanager::soundcleanup()
{
if(nosound) return;
// destroy consuming code
stopsound();
DELETEP(gamemusic);
mapsounds.shrink(0);
locations.deletecontents();
gamesounds.shrink(0);
// kill scheduler
sourcescheduler::instance().reset();
bufferpool.clear();
// shutdown openal
alcMakeContextCurrent(NULL);
if(context) alcDestroyContext(context);
if(device) alcCloseDevice(device);
}
// clear world-related sounds, called on mapchange
void audiomanager::clearworldsounds(bool fullclean)
{
stopsound();
if(fullclean) mapsounds.shrink(0);
locations.deleteworldobjsounds();
}
void audiomanager::mapsoundreset()
{
mapsounds.shrink(0);
locations.deleteworldobjsounds();
}
VARP(footsteps, 0, 1, 1);
VARP(localfootsteps, 0, 1, 1);
void audiomanager::updateplayerfootsteps(playerent *p)
{
if(!p) return;
const int footstepradius = 20;
// find existing footstep sounds
physentreference ref(p);
location *locs[] =
{
locations.find(S_FOOTSTEPS, &ref, gamesounds),
locations.find(S_FOOTSTEPSCROUCH, &ref, gamesounds),
locations.find(S_WATERFOOTSTEPS, &ref, gamesounds),
locations.find(S_WATERFOOTSTEPSCROUCH, &ref, gamesounds)
};
bool local = (p == camera1);
bool inrange = footsteps && (local || (camera1->o.dist(p->o) < footstepradius));
if(!footsteps || (local && !localfootsteps) || !inrange || p->state != CS_ALIVE || lastmillis-p->lastpain < 300 || (!p->onfloor && p->timeinair>50) || (!p->move && !p->strafe) || p->inwater)
{
const int minplaytime = 200;
loopi(sizeof(locs)/sizeof(locs[0]))
{
location *l = locs[i];
if(!l) continue;
if(l->playmillis+minplaytime>totalmillis) continue; // tolerate short interruptions by enforcing a minimal playtime
l->drop();
}
}
else
{
// play footsteps
int grounddist;
if( ((int)p->o.x) >= 0 && ((int)p->o.y) >= 0 && ((int)p->o.x) < ssize && ((int)p->o.y) < ssize) { // sam's fix to the sound crash
grounddist = hdr.waterlevel-S((int)p->o.x, (int)p->o.y)->floor;
}else{
grounddist = 0;
}
// int grounddist = hdr.waterlevel-S((int)p->o.x, (int)p->o.y)->floor;
bool water = p->o.z-p->eyeheight+0.25f<hdr.waterlevel;
if(water && grounddist>p->eyeheight) return; // don't play step sound when jumping into water
int stepsound;
if(p->crouching) stepsound = water ? S_WATERFOOTSTEPSCROUCH : S_FOOTSTEPSCROUCH; // crouch
else stepsound = water ? S_WATERFOOTSTEPS : S_FOOTSTEPS; // normal
// proc existing sounds
bool isplaying = false;
loopi(sizeof(locs)/sizeof(locs[0]))
{
location *l = locs[i];
if(!l) continue;
if(i+S_FOOTSTEPS==stepsound) isplaying = true; // already playing
else l->drop(); // different footstep sound, drop it
}
if(!isplaying)
{
// play
float rndoffset = float(rnd(500))/500.0f;
audiomgr._playsound(stepsound, ref, local ? SP_HIGH : SP_LOW, rndoffset);
}
}
}
// manage looping sounds
location *audiomanager::updateloopsound(int sound, bool active, float vol)
{
location *l = locations.find(sound, NULL, gamesounds);
if(!l && active) l = _playsound(sound, camerareference(), SP_HIGH, 0.0f, true);
else if(l && !active) l->drop();
if(l && vol != 1.0f) l->src->gain(vol);
return l;
}
VARP(mapsoundrefresh, 0, 10, 1000);
void audiomanager::mutesound(int n, int off)
{
bool mute = (off == 0);
if(!gamesounds.inrange(n))
{
conoutf("\f3could not %s sound #%d", mute ? "silence" : "unmute", n);
return;
}
gamesounds[n].muted = mute;
}
void audiomanager::unmuteallsounds()
{
loopv(gamesounds) gamesounds[i].muted = false;
}
int audiomanager::soundmuted(int n)
{
return gamesounds.inrange(n) && !gamesounds[n].muted ? 0 : 1;
}
void audiomanager::writesoundconfig(stream *f)
{
bool wrotesound = false;
loopv(gamesounds)
{
if(gamesounds[i].muted)
{
if(!wrotesound)
{
f->printf("// sound settings\n\n");
wrotesound = true;
}
f->printf("mutesound %d\n", i);
}
}
}
void voicecom(char *sound, char *text)
{
if(!sound || !sound[0]) return;
if(!text || !text[0]) return;
static int last = 0;
if(!last || lastmillis-last > 2000)
{
extern int voicecomsounds;
defformatstring(soundpath)("voicecom/%s", sound);
int s = audiomgr.findsound(soundpath, 0, gamesounds);
if(s < 0 || s < S_AFFIRMATIVE || s >= S_NULL) return;
if(voicecomsounds>0) audiomgr.playsound(s, SP_HIGH);
if(s >= S_NICESHOT) // public
{
addmsg(SV_VOICECOM, "ri", s);
toserver(text);
}
else // team
{
addmsg(SV_VOICECOMTEAM, "ri", s);
defformatstring(teamtext)("%c%s", '%', text);
toserver(teamtext);
}
last = lastmillis;
}
}
COMMAND(voicecom, "ss");
void soundtest()
{
loopi(S_NULL) audiomgr.playsound(i, rnd(SP_HIGH+1));
}
COMMAND(soundtest, "");
// sound configuration
soundconfig::soundconfig(sbuffer *b, int vol, int maxuses, bool loop, int audibleradius)
{
buf = b;
this->vol = vol > 0 ? vol : 100;
this->maxuses = maxuses;
this->loop = loop;
this->audibleradius = audibleradius;
this->model = audibleradius > 0 ? DM_LINEAR : DM_DEFAULT; // use linear model when radius is configured
uses = 0;
muted = false;
}
void soundconfig::onattach()
{
uses++;
}
void soundconfig::ondetach()
{
uses--;
}
vector<soundconfig> gamesounds, mapsounds;
void audiomanager::detachsounds(playerent *owner)
{
if(nosound) return;
// make all dependent locations static
locations.replaceworldobjreference(physentreference(owner), staticreference(owner->o));
}
VARP(maxsoundsatonce, 0, 32, 100);
location *audiomanager::_playsound(int n, const worldobjreference &r, int priority, float offset, bool loop)
{
if(nosound || !soundvol) return NULL;
if(soundmuted(n)) return NULL;
DEBUGVAR(n);
DEBUGVAR(priority);
if(r.type!=worldobjreference::WR_ENTITY)
{
// avoid bursts of sounds with heavy packetloss and in sp
static int soundsatonce = 0, lastsoundmillis = 0;
if(totalmillis==lastsoundmillis) soundsatonce++; else soundsatonce = 1;
lastsoundmillis = totalmillis;
if(maxsoundsatonce && soundsatonce>maxsoundsatonce)
{
DEBUGVAR(soundsatonce);
return NULL;
}
}
location *loc = new location(n, r, priority);
locations.add(loc);
if(!loc->stale)
{
if(offset>0) loc->offset(offset);
if(currentpitch!=1.0f) loc->pitch(currentpitch);
loc->play(loop);
}
return loc;
}
void audiomanager::playsound(int n, int priority) { _playsound(n, camerareference(), priority); }
void audiomanager::playsound(int n, physent *p, int priority) { if(p) _playsound(n, physentreference(p), priority); }
void audiomanager::playsound(int n, entity *e, int priority) { if(e) _playsound(n, entityreference(e), priority); }
void audiomanager::playsound(int n, const vec *v, int priority) { if(v) _playsound(n, staticreference(*v), priority); }
void audiomanager::playsoundname(char *s, const vec *loc, int vol)
{
if(!nosound) return;
if(vol <= 0) vol = 100;
int id = findsound(s, vol, gamesounds);
if(id < 0) id = addsound(s, vol, 0, false, gamesounds, true, 0);
playsound(id, loc, SP_NORMAL);
}
void audiomanager::playsoundc(int n, physent *p, int priority)
{
if(p && p!=player1) playsound(n, p, priority);
else
{
addmsg(SV_SOUND, "i", n);
playsound(n, priority);
}
}
void audiomanager::stopsound()
{
if(nosound) return;
DELETEA(musicdonecmd);
if(gamemusic) gamemusic->reset();
}
VARP(heartbeat, 0, 0, 99);
// main audio update routine
void audiomanager::updateaudio()
{
if(nosound) return;
alcSuspendContext(context); // don't process sounds while we mess around
bool alive = player1->state==CS_ALIVE;
bool firstperson = camera1==player1 || (player1->isspectating() && player1->spectatemode==SM_DEATHCAM);
// footsteps
updateplayerfootsteps(player1);
loopv(players)
{
playerent *p = players[i];
if(!p) continue;
updateplayerfootsteps(p);
}
// water
bool underwater = /*alive &&*/ firstperson && hdr.waterlevel>player1->o.z+player1->aboveeye;
updateloopsound(S_UNDERWATER, underwater);
// tinnitus
bool tinnitus = alive && firstperson && player1->eardamagemillis>0 && lastmillis<=player1->eardamagemillis;
location *tinnitusloc = updateloopsound(S_TINNITUS, tinnitus);
// heartbeat
bool heartbeatsound = heartbeat && alive && firstperson && !m_osok && player1->health <= heartbeat;
updateloopsound(S_HEARTBEAT, heartbeatsound);
// pitch fx
const float lowpitch = 0.65f;
bool pitchfx = underwater || tinnitus;
if(pitchfx && currentpitch!=lowpitch)
{
currentpitch = lowpitch;
locations.forcepitch(currentpitch);
if(tinnitusloc) tinnitusloc->pitch(1.9f); // super high pitched tinnitus
}
else if(!pitchfx && currentpitch==lowpitch)
{
currentpitch = 1.0f;
locations.forcepitch(currentpitch);
}
// update map sounds
static int lastmapsound = 0;
if(!lastmapsound || totalmillis-lastmapsound>mapsoundrefresh || !mapsoundrefresh)
{
loopv(ents)
{
entity &e = ents[i];
vec o(e.x, e.y, e.z);
if(e.type!=SOUND) continue;
int sound = e.attr1;
int radius = e.attr2;
bool hearable = (radius==0 || camera1->o.dist(o)<radius);
entityreference entref(&e);
// search existing sound loc
location *loc = locations.find(sound, &entref, mapsounds);
if(hearable && !loc) // play
{
_playsound(sound, entref, SP_HIGH, 0.0f, true);
}
else if(!hearable && loc) // stop
{
loc->drop();
}
}
lastmapsound = totalmillis;
}
// update all sound locations
locations.updatelocations();
// update background music
if(gamemusic)
{
if(!gamemusic->update())
{
// music ended, exec command
if(musicdonecmd)
{
char *cmd = musicdonecmd;
musicdonecmd = NULL;
execute(cmd);
delete[] cmd;
}
}
}
// listener
vec o[2];
o[0].x = (float)(cosf(RAD*(camera1->yaw-90)));
o[0].y = (float)(sinf(RAD*(camera1->yaw-90)));
o[0].z = 0.0f;
o[1].x = o[1].y = 0.0f;
o[1].z = -1.0f;
alListenerfv(AL_ORIENTATION, (ALfloat *) &o);
alListenerfv(AL_POSITION, (ALfloat *) &camera1->o);
alcProcessContext(context);
}
// binding of sounds to the 3D world
// camera
camerareference::camerareference() : worldobjreference(WR_CAMERA) {}
worldobjreference *camerareference::clone() const
{
return new camerareference(*this);
}
const vec &camerareference::currentposition() const
{
return camera1->o;
}
bool camerareference::nodistance()
{
return true;
}
bool camerareference::operator==(const worldobjreference &other)
{
return type==other.type;
}
// physent
physentreference::physentreference(physent *ref) : worldobjreference(WR_PHYSENT)
{
ASSERT(ref);
phys = ref;
}
worldobjreference *physentreference::clone() const
{
return new physentreference(*this);
}
const vec &physentreference::currentposition() const
{
return phys->o;
}
bool physentreference::nodistance()
{
return phys==camera1;
}
bool physentreference::operator==(const worldobjreference &other)
{
return type==other.type && phys==((physentreference &)other).phys;
}
// entity
entityreference::entityreference(entity *ref) : worldobjreference(WR_ENTITY)
{
ASSERT(ref);
ent = ref;
}
worldobjreference *entityreference::clone() const
{
return new entityreference(*this);
}
const vec &entityreference::currentposition() const
{
static vec tmp;
tmp = vec(ent->x, ent->y, ent->z);
return tmp;
}
bool entityreference::nodistance() { return ent->attr3>0; }
bool entityreference::operator==(const worldobjreference &other) { return type==other.type && ent==((entityreference &)other).ent; }
// static
staticreference::staticreference(const vec &ref) : worldobjreference(WR_STATICPOS)
{
pos = ref;
}
worldobjreference *staticreference::clone() const
{
return new staticreference(*this);
}
const vec &staticreference::currentposition() const
{
return pos;
}
bool staticreference::nodistance()
{
return false;
}
bool staticreference::operator==(const worldobjreference &other)
{
return type==other.type && pos==((staticreference &)other).pos;
}
// instance
audiomanager audiomgr;
COMMANDF(sound, "i", (int *n)
{
audiomgr.playsound(*n);
});
COMMANDF(applymapsoundchanges, "", (){
audiomgr.applymapsoundchanges();
});
COMMANDF(unmuteallsounds, "", () {
audiomgr.unmuteallsounds();
});
COMMANDF(mutesound, "ii", (int *n, int *off)
{
audiomgr.mutesound(*n, *off);
});
COMMANDF(soundmuted, "i", (int *n)
{
intret(audiomgr.soundmuted(*n));
});
VAR(mapsoundchanged, 0, 0, 1);
COMMANDF(mapsoundreset, "", ()
{
audiomgr.mapsoundreset();
mapconfigdata.mapsoundlines.shrink(0);
mapsoundchanged = 1;
flagmapconfigchange();
});
VARF(soundchannels, 4, 32, 1024, audiomgr.setchannels(soundchannels); );
VARFP(soundvol, 0, 128, 255, audiomgr.setlistenervol(soundvol); );
COMMANDF(registersound, "siii", (char *name, int *vol, int *loop, int *audibleradius)
{
intret(audiomgr.addsound(name, *vol, -1, *loop != 0, gamesounds, true, *audibleradius));
});
COMMANDF(mapsound, "si", (char *name, int *maxuses)
{
audiomgr.addsound(name, 255, *maxuses, true, mapsounds, false, 0);
intret(mapconfigdata.mapsoundlines.length());
copystring(mapconfigdata.mapsoundlines.add().name, name);
mapconfigdata.mapsoundlines.last().maxuses = *maxuses;
mapsoundchanged = 1;
flagmapconfigchange();
});
COMMANDF(registermusic, "s", (char *name)
{
audiomgr.registermusic(name);
});
COMMANDF(musicpreload, "i", (int *id)
{
audiomgr.musicpreload(*id);
});
COMMANDF(music, "sis", (char *name, int *millis, char *cmd)
{
audiomgr.music(name, *millis, cmd);
});