From 74e1468ee3d2319dcaadc3e266af378d5d453068 Mon Sep 17 00:00:00 2001 From: driAn Date: Tue, 25 Nov 2008 22:50:35 +0000 Subject: [PATCH] ac audio architecture refactoring git-svn-id: svn://svn.code.sf.net/p/actiongame/code/trunk/ac@3760 e016b1c5-ed68-44aa-a2f7-31cac88cffde --- source/src/audiomanager.cpp | 744 ++++++ source/src/bot/bot.cpp | 2 +- source/src/client.cpp | 2 +- source/src/clientgame.cpp | 36 +- source/src/clients2c.cpp | 8 +- source/src/command.cpp | 2 +- source/src/entities.cpp | 4 +- source/src/entity.h | 3 +- source/src/main.cpp | 6 +- source/src/menus.cpp | 4 +- source/src/oggstream.cpp | 283 ++ source/src/openal.cpp | 398 +++ source/src/pch.h | 10 + source/src/physics.cpp | 10 +- source/src/protos.h | 12 +- source/src/renderparticles.cpp | 4 +- source/src/sound.cpp | 1865 ------------- source/src/sound.h | 303 +++ source/src/soundlocation.cpp | 283 ++ source/src/soundscheduler.cpp | 154 ++ source/src/weapon.cpp | 32 +- source/src/world.cpp | 4 +- source/src/worldio.cpp | 2 +- source/vcpp/cube.suo | Bin 599552 -> 644096 bytes source/vcpp/cube.vcproj | 4532 ++++++++++++++++---------------- 25 files changed, 4519 insertions(+), 4184 deletions(-) create mode 100644 source/src/audiomanager.cpp create mode 100644 source/src/oggstream.cpp create mode 100644 source/src/openal.cpp create mode 100644 source/src/soundlocation.cpp create mode 100644 source/src/soundscheduler.cpp diff --git a/source/src/audiomanager.cpp b/source/src/audiomanager.cpp new file mode 100644 index 00000000..4be3e8f2 --- /dev/null +++ b/source/src/audiomanager.cpp @@ -0,0 +1,744 @@ +// audio interface of the engine + +#include "pch.h" +#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; + +void setmusicvol() +{ + extern int musicvol; + if(audiomgr.gamemusic) audiomgr.gamemusic->setvolume(musicvol > 0 ? musicvol/255.0f : 0); +} + +VARFP(musicvol, 0, 128, 255, setmusicvol()); + +// 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; + s_strcpy(d, "Audio devices: "); + + // null separated device string + for(const ALchar *c = devices; c[strlen(c)+1]; c += strlen(c)+1) + { + if(c!=devices) s_strcat(d, ", "); + s_strcat(d, c); + } + conoutf(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 + scheduler.init(); + + // let the stream get the first source from the scheduler + gamemusic = new oggstream(); + if(!gamemusic->valid) DELETEP(gamemusic); + setmusicvol(); + + nosound = false; + } + } + + if(nosound) + { + ALERR; + if(context) alcDestroyContext(context); + if(device) alcCloseDevice(device); + conoutf("sound initialization failed!"); + } +} + +cvector musics; + +void audiomanager::music(char *name, char *millis, char *cmd) +{ + if(nosound) return; + stopsound(); + if(musicvol && *name) + { + if(cmd[0]) musicdonecmd = newstring(cmd); + + if(gamemusic->open(name)) + { + // fade + if(atoi(millis) > 0) + { + const int fadetime = 1000; + gamemusic->fadein(lastmillis, fadetime); + gamemusic->fadeout(lastmillis+atoi(millis), fadetime); + } + + // play + bool loop = cmd && cmd[0]; + if(!gamemusic->playback(loop)) + { + conoutf("could not play music: %s", name); + return; + } + setmusicvol(); + } + else conoutf("could not open music: %s", name); + } +} + +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(!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::registermusic(char *name) +{ + if(nosound) return; + if(!name || !name[0]) return; + musics.add(newstring(name)); +} + +int findsound(char *name, int vol, vector &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 &sounds, bool load) +{ + if(nosound) return -1; + + 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 > 0 ? vol : 100, maxuses, loop); + sounds.add(s); + return sounds.length()-1; +} + +void audiomanager::preloadmapsound(entity &e) +{ + if(e.type!=SOUND || !mapsounds.inrange(e.attr1)) return; + sbuffer *buf = mapsounds[e.attr1].buf; + if(!buf->load()) conoutf("\f3failed to load sample %s", buf->name); +} + +void audiomanager::preloadmapsounds() +{ + loopv(ents) + { + entity &e = ents[i]; + if(e.type==SOUND) preloadmapsound(e); + } +} + +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(); + } +} + + + +// called at game exit +void audiomanager::soundcleanup() +{ + if(nosound) return; + + // destroy consuming code + stopsound(); + DELETEP(gamemusic); + mapsounds.setsize(0); + locations.deletecontentsp(); + gamesounds.setsize(0); + bufferpool.clear(); + + // kill scheduler + scheduler.reset(); + + // 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.setsize(0); + locations.deleteworldobjsounds(); +} + +void audiomanager::mapsoundreset() +{ + mapsounds.setsize(0); + locations.deleteworldobjsounds(); +} + +VARP(footsteps, 0, 1, 1); +VARP(localfootsteps, 0, 1, 1); + +void audiomanager::updateplayerfootsteps(playerent *p) +{ + if(!p) return; + + const int footstepradius = 6; + static float lastoffset = 0; + + // 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 + lastoffset = l->offset(); // save last offset + DEBUGVAR(lastoffset); + l->drop(); + } + } + else + { + // play footsteps + + int grounddist = hdr.waterlevel-S((int)p->o.x, (int)p->o.y)->floor; + bool water = p->o.z-p->eyeheight+0.25fp->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 unmuteallsounds() +{ + loopv(gamesounds) gamesounds[i].muted = false; +} + +void mutesound(int n, int off) +{ + bool mute = (off == 0); + if(!gamesounds.inrange(n)) + { + conoutf("\f3could not %s sound no %d", mute ? "silence" : "unmute", n); + return; + } + gamesounds[n].muted = mute; +} + +int soundmuted(int n) +{ + return gamesounds.inrange(n) && gamesounds[n].muted ? 1 : 0; +} + +COMMAND(unmuteallsounds, ARG_NONE); +COMMAND(mutesound, ARG_2INT); +COMMAND(soundmuted, ARG_1EXP); + +void audiomanager::writesoundconfig(FILE *f) +{ + loopv(gamesounds) if(gamesounds[i].muted) fprintf(f, "mutesound %d\n", i); +} + + + +void voicecom(char *sound, char *text) +{ + if(!sound || !sound[0]) return; + static int last = 0; + if(!last || lastmillis-last > 2000) + { + s_sprintfd(soundpath)("voicecom/%s", sound); + int s = findsound(soundpath, 0, gamesounds); + if(s < 0 || s < S_AFFIRMATIVE || s > S_NICESHOT) return; + audiomgr.playsound(s, SP_HIGH); + if(s == S_NICESHOT) // public + { + addmsg(SV_VOICECOM, "ri", s); + toserver(text); + } + else // team + { + addmsg(SV_VOICECOMTEAM, "ri", s); + s_sprintfd(teamtext)("%c%s", '%', text); + toserver(teamtext); + } + last = lastmillis; + } +} + +COMMAND(voicecom, ARG_2STR); + + + +void soundtest() +{ + loopi(S_NULL) audiomgr.playsound(i, rnd(SP_HIGH+1)); +} + +COMMAND(soundtest, ARG_NONE); + + + +// sound configuration + +soundconfig::soundconfig(sbuffer *b, int vol, int maxuses, bool loop) +{ + buf = b; + this->vol = vol; + this->maxuses = maxuses; + this->loop = loop; + uses = 0; + muted = false; +} + +void soundconfig::onattach() +{ + uses++; +} + +void soundconfig::ondetach() +{ + uses--; +} + +vector 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, 10, 100); + +location *audiomanager::_playsound(int n, const worldobjreference &r, int priority, float offset, bool loop) +{ + if(nosound || !soundvol) 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); + playsound(id, loc, SP_NORMAL); +} + +void audiomanager::playsoundc(int n, physent *p) +{ + if(p && p!=player1) playsound(n, p); + else + { + addmsg(SV_SOUND, "i", n); + playsound(n); + } +} + +void audiomanager::stopsound() +{ + if(nosound) return; + DELETEA(musicdonecmd); + if(gamemusic) gamemusic->reset(); +} + +// 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_DEAD; + 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); + + // 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; + bool hearable = e.attr2==0 || camera1->o.dist(o)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); +} + +audiomanager audiomgr; + + +// 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; +} + +// cli interface + +void sound(int n) { audiomgr.playsound(n); } +COMMAND(sound, ARG_1INT); + +void applymapsoundchanges() { audiomgr.applymapsoundchanges(); } +COMMANDN(applymapsoundchanges, applymapsoundchanges, ARG_NONE); + +void mapsoundreset() { audiomgr.mapsoundreset(); } +COMMAND(mapsoundreset, ARG_NONE); + +VARF(soundchannels, 4, 32, 1024, { if(!audiomgr.nosound) scheduler.init(); }); + +VARFP(soundvol, 0, 128, 255, +{ + if(!audiomgr.nosound) alListenerf(AL_GAIN, soundvol/255.0f); +}); + +void registersound(char *name, char *vol, char *loop) { audiomgr.addsound(name, atoi(vol), -1, atoi(loop) != 0, gamesounds, true); } +COMMAND(registersound, ARG_4STR); + +void mapsound(char *name, char *maxuses) { audiomgr.addsound(name, 255, atoi(maxuses), true, mapsounds, false); } +COMMAND(mapsound, ARG_3STR); + +void registermusic(char *name) { audiomgr.registermusic(name); } +COMMAND(registermusic, ARG_1STR); + +void music(char *name, char *millis, char *cmd) { audiomgr.music(name, millis, cmd); } +COMMAND(music, ARG_3STR); \ No newline at end of file diff --git a/source/src/bot/bot.cpp b/source/src/bot/bot.cpp index c4f732d1..b46f600d 100644 --- a/source/src/bot/bot.cpp +++ b/source/src/bot/bot.cpp @@ -304,7 +304,7 @@ float CBot::GetDistance(entity *e) bool CBot::SelectGun(int Gun) { if(m_pMyEnt->weaponsel->reloading) return false; - if (m_pMyEnt->gunselect != Gun) playsound(S_GUNCHANGE, m_pMyEnt); + if (m_pMyEnt->gunselect != Gun) audiomgr.playsound(S_GUNCHANGE, m_pMyEnt); m_pMyEnt->gunselect = Gun; return true; } diff --git a/source/src/client.cpp b/source/src/client.cpp index e78122e2..6df09400 100644 --- a/source/src/client.cpp +++ b/source/src/client.cpp @@ -159,7 +159,7 @@ void disconnect(int onlyclean, int async) kickallbots(); loopv(players) zapplayer(players[i]); clearvote(); - clearworldsounds(false); + audiomgr.clearworldsounds(false); localdisconnect(); } #if 0 diff --git a/source/src/clientgame.cpp b/source/src/clientgame.cpp index 82e63a1b..5612c1d5 100644 --- a/source/src/clientgame.cpp +++ b/source/src/clientgame.cpp @@ -519,8 +519,8 @@ void dodamage(int damage, playerent *pl, playerent *actor, bool gib, bool local) damageeffect(damage, pl); if(pl->health<=0) { if(local) dokill(pl, actor, gib); } - else if(pl==player1) playsound(S_PAIN6, SP_HIGH); - else playsound(S_PAIN1+rnd(5), pl); + else if(pl==player1) audiomgr.playsound(S_PAIN6, SP_HIGH); + else audiomgr.playsound(S_PAIN1+rnd(5), pl); } void dokill(playerent *pl, playerent *act, bool gib) @@ -548,7 +548,7 @@ void dokill(playerent *pl, playerent *act, bool gib) if(gib) { - if(pl!=act && act->weaponsel->type == GUN_SNIPER) playsound(S_HEADSHOT, SP_LOW); + if(pl!=act && act->weaponsel->type == GUN_SNIPER) audiomgr.playsound(S_HEADSHOT, SP_LOW); addgib(pl); } @@ -560,7 +560,7 @@ void dokill(playerent *pl, playerent *act, bool gib) deathstate(pl); pl->deaths++; - playsound(S_DIE1+rnd(2), pl); + audiomgr.playsound(S_DIE1+rnd(2), pl); } VAR(minutesremaining, 1, 0, 0); @@ -583,7 +583,7 @@ void timeupdate(int timeremain) conoutf("time remaining: %d minutes", timeremain); if(timeremain==1) { - musicsuggest(M_LASTMINUTE1 + rnd(2), 70*1000, true); + audiomgr.musicsuggest(M_LASTMINUTE1 + rnd(2), 70*1000, true); hudoutf("1 minute left!"); } } @@ -681,7 +681,7 @@ void resetmap() clearminimap(); cleardynlights(); pruneundos(); - clearworldsounds(); + audiomgr.clearworldsounds(); particlereset(); setvar("gamespeed", 100); setvar("paused", 0); @@ -771,11 +771,11 @@ void flagmsg(int flag, int message, int actor, int flagtime) switch(message) { case FM_PICKUP: - playsound(S_FLAGPICKUP, SP_HIGHEST); + audiomgr.playsound(S_FLAGPICKUP, SP_HIGHEST); if(firstperson) { hudoutf("\f2you got the %sflag", m_ctf ? "enemy " : ""); - musicsuggest(M_FLAGGRAB, m_ctf ? 90*1000 : 900*1000, true); + audiomgr.musicsuggest(M_FLAGGRAB, m_ctf ? 90*1000 : 900*1000, true); } else hudoutf("\f2%s%s got %s flag", flagteam, colorname(act), teamstr); break; @@ -783,32 +783,32 @@ void flagmsg(int flag, int message, int actor, int flagtime) case FM_DROP: { const char *droplost = message == FM_LOST ? "lost" : "dropped"; - playsound(S_FLAGDROP, SP_HIGHEST); + audiomgr.playsound(S_FLAGDROP, SP_HIGHEST); if(firstperson) { hudoutf("\f2you %s the flag", droplost); - musicfadeout(M_FLAGGRAB); + audiomgr.musicfadeout(M_FLAGGRAB); } else hudoutf("\f2%s %s %s flag", colorname(act), droplost, teamstr); break; } case FM_RETURN: - playsound(S_FLAGRETURN, SP_HIGHEST); + audiomgr.playsound(S_FLAGRETURN, SP_HIGHEST); if(firstperson) hudoutf("\f2you returned your flag"); else hudoutf("\f2%s returned %s flag", colorname(act), teamstr); break; case FM_SCORE: - playsound(S_FLAGSCORE, SP_HIGHEST); + audiomgr.playsound(S_FLAGSCORE, SP_HIGHEST); if(firstperson) { hudoutf("\f2you scored"); - if(m_ctf) musicfadeout(M_FLAGGRAB); + if(m_ctf) audiomgr.musicfadeout(M_FLAGGRAB); } else hudoutf("\f2%s scored for %s team", colorname(act), teammate ? "your" : "the enemy"); break; case FM_KTFSCORE: { - playsound(S_VOTEPASS, SP_HIGHEST); // need better ktf sound here + audiomgr.playsound(S_VOTEPASS, SP_HIGHEST); // need better ktf sound here const char *ta = firstperson ? "you have" : colorname(act); const char *tb = firstperson ? "" : " has"; const char *tc = teammate && !firstperson ? "your teammate " : ""; @@ -823,9 +823,9 @@ void flagmsg(int flag, int message, int actor, int flagtime) hudoutf("\f2%s failed to score (own team flag not taken)", firstperson ? "you" : colorname(act)); break; case FM_RESET: - playsound(S_FLAGRETURN, SP_HIGHEST); + audiomgr.playsound(S_FLAGRETURN, SP_HIGHEST); hudoutf("the server reset the flag"); - if(firstperson || own) musicfadeout(M_FLAGGRAB); + if(firstperson || own) audiomgr.musicfadeout(M_FLAGGRAB); break; } } @@ -958,7 +958,7 @@ void displayvote(votedisplayinfo *v) DELETEP(curvote); curvote = v; conoutf("%s called a vote: %s", v->owner ? colorname(v->owner) : "", curvote->desc); - playsound(S_CALLVOTE, SP_HIGHEST); + audiomgr.playsound(S_CALLVOTE, SP_HIGHEST); curvote->localplayervoted = false; } @@ -985,7 +985,7 @@ void voteresult(int v) curvote->result = v; curvote->millis = totalmillis + 5000; conoutf("vote %s", v == VOTE_YES ? "passed" : "failed"); - if(multiplayer(false)) playsound(v == VOTE_YES ? S_VOTEPASS : S_VOTEFAIL, SP_HIGH); + if(multiplayer(false)) audiomgr.playsound(v == VOTE_YES ? S_VOTEPASS : S_VOTEFAIL, SP_HIGH); } } diff --git a/source/src/clients2c.cpp b/source/src/clients2c.cpp index f4c0e3ff..88bc32bf 100644 --- a/source/src/clients2c.cpp +++ b/source/src/clients2c.cpp @@ -192,19 +192,19 @@ void parsemessages(int cn, playerent *d, ucharbuf &p) } case SV_SOUND: - playsound(getint(p), d); + audiomgr.playsound(getint(p), d); break; case SV_VOICECOMTEAM: { playerent *d = getclient(getint(p)); if(d) d->lastvoicecom = lastmillis; - playsound(getint(p), SP_HIGH); + audiomgr.playsound(getint(p), SP_HIGH); break; } case SV_VOICECOM: { - playsound(getint(p), SP_HIGH); + audiomgr.playsound(getint(p), SP_HIGH); if(d) d->lastvoicecom = lastmillis; break; } @@ -539,7 +539,7 @@ void parsemessages(int cn, playerent *d, ucharbuf &p) ents[i].attr4 = getint(p); ents[i].spawned = false; if(ents[i].type==LIGHT || to==LIGHT) calclight(); - if(ents[i].type==SOUND) preloadmapsound(ents[i]); + if(ents[i].type==SOUND) audiomgr.preloadmapsound(ents[i]); break; } diff --git a/source/src/command.cpp b/source/src/command.cpp index 1eb8d6db..b5aeafec 100644 --- a/source/src/command.cpp +++ b/source/src/command.cpp @@ -869,7 +869,7 @@ void writecfg() extern int lowfps, highfps; fprintf(f, "fpsrange %d %d\n", lowfps, highfps); fprintf(f, "\n"); - writesoundconfig(f); + audiomgr.writesoundconfig(f); fprintf(f, "\n"); enumerate(*idents, ident, id, if(!id.persist) continue; diff --git a/source/src/entities.cpp b/source/src/entities.cpp index be689d94..af4616b2 100644 --- a/source/src/entities.cpp +++ b/source/src/entities.cpp @@ -144,8 +144,8 @@ void pickupeffects(int n, playerent *d) if(d!=player1 && d->type!=ENT_BOT) return; if(&is) { - if(d==player1) playsoundc(is.sound); - else playsound(is.sound, d); + if(d==player1) audiomgr.playsoundc(is.sound); + else audiomgr.playsound(is.sound, d); } weapon *w = NULL; diff --git a/source/src/entity.h b/source/src/entity.h index 0c5528b8..6631f080 100644 --- a/source/src/entity.h +++ b/source/src/entity.h @@ -376,14 +376,13 @@ struct playerent : dynent, playerstate virtual ~playerent() { extern void removebounceents(playerent *owner); - extern void detachsounds(playerent *owner); extern void removedynlights(physent *owner); extern void zapplayerflags(playerent *owner); extern void cleanplayervotes(playerent *owner); extern physent *camera1; extern void togglespect(); removebounceents(this); - detachsounds(this); + audiomgr.detachsounds(this); removedynlights(this); zapplayerflags(this); cleanplayervotes(this); diff --git a/source/src/main.cpp b/source/src/main.cpp index faa1ee7d..1db30ca0 100644 --- a/source/src/main.cpp +++ b/source/src/main.cpp @@ -8,7 +8,7 @@ void cleanup(char *msg) // single program exit point; if(!msg) { cleanupclient(); - soundcleanup(); + audiomgr.soundcleanup(); cleanupserver(); } SDL_ShowCursor(1); @@ -595,7 +595,7 @@ int main(int argc, char **argv) particleinit(); initlog("sound"); - initsound(); + audiomgr.initsound(); initlog("cfg"); extern void *scoremenu, *teammenu, *ctfmenu, *servmenu, *kickmenu, *banmenu, *forceteammenu, *giveadminmenu, *docmenu, *applymenu; @@ -697,7 +697,7 @@ int main(int argc, char **argv) fps = (1000.0f/elapsed+fps*10)/11; frames++; - updateaudio(); + audiomgr.updateaudio(); computeraytable(camera1->o.x, camera1->o.y, dynfov()); if(frames>3) diff --git a/source/src/menus.cpp b/source/src/menus.cpp index c447a117..92fa67ee 100644 --- a/source/src/menus.cpp +++ b/source/src/menus.cpp @@ -75,7 +75,7 @@ void menuselect(void *menu, int sel) { m.items[oldsel]->focus(false); m.items[sel]->focus(true); - playsound(S_MENUSELECT, SP_HIGHEST); + audiomgr.playsound(S_MENUSELECT, SP_HIGHEST); } } } @@ -725,7 +725,7 @@ bool menukey(int code, bool isdown, int unicode, SDLMod mod) if(code==SDLK_RETURN || code==SDLK_SPACE || code==-1 || code==-2) { m.select(); - playsound(S_MENUENTER, SP_HIGHEST); + audiomgr.playsound(S_MENUENTER, SP_HIGHEST); return true; } return false; diff --git a/source/src/oggstream.cpp b/source/src/oggstream.cpp new file mode 100644 index 00000000..bd078bd3 --- /dev/null +++ b/source/src/oggstream.cpp @@ -0,0 +1,283 @@ +// audio streaming + +#include "pch.h" +#include "cube.h" + +#define DEBUGCOND (audiodebug==1) + +// ogg compat + +static int oggseek(FILE *f, ogg_int64_t off, int whence) +{ + return f ? fseek(f, off, whence) : -1; +} + +ov_callbacks oggcallbacks = +{ + (size_t (*)(void *, size_t, size_t, void *)) fread, + (int (*)(void *, ogg_int64_t, int)) oggseek, + (int (*)(void *)) fclose, + (long (*)(void *)) ftell +}; + + +// ogg audio streaming + +oggstream::oggstream() : valid(false), isopen(false), src(NULL) +{ + reset(); + + // grab a source and keep it during the whole lifetime + src = scheduler.newsource(SP_HIGHEST, camera1->o); + if(src) + { + if(src->valid) + { + src->init(this); + src->sourcerelative(true); + } + else + { + scheduler.releasesource(src); + src = NULL; + } + } + + if(!src) return; + + alclearerr(); + alGenBuffers(2, bufferids); + valid = !ALERR; +} + +oggstream::~oggstream() +{ + reset(); + + if(src) scheduler.releasesource(src); + + if(alIsBuffer(bufferids[0]) || alIsBuffer(bufferids[1])) + { + alclearerr(); + alDeleteBuffers(2, bufferids); + ALERR; + } +} + +void oggstream::reset() +{ + name[0] = '\0'; + + // stop playing + if(src) + { + src->stop(); + src->unqueueallbuffers(); + src->buffer(0); + } + format = AL_NONE; + + // reset file handler + if(isopen) + { + isopen = !ov_clear(&oggfile); + } + info = NULL; + totalseconds = 0.0f; + + // default settings + startmillis = endmillis = startfademillis = endfademillis = 0; + gain = 1.0f; // reset gain but not volume setting + looping = false; +} + +bool oggstream::open(const char *f) +{ + ASSERT(valid); + if(!f) return false; + if(playing() || isopen) reset(); + + const char *exts[] = { "", ".wav", ".ogg" }; + string filepath; + + loopi(sizeof(exts)/sizeof(exts[0])) + { + s_sprintf(filepath)("packages/audio/songs/%s%s", f, exts[i]); + FILE *file = fopen(findfile(path(filepath), "rb"), "rb"); + if(!file) continue; + + isopen = !ov_open_callbacks(file, &oggfile, NULL, 0, oggcallbacks); + if(!isopen) + { + fclose(file); + continue; + } + + info = ov_info(&oggfile, -1); + format = info->channels == 2 ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16; + totalseconds = ov_time_total(&oggfile, -1); + s_strcpy(name, f); + + return true; + } + return false; +} + +void oggstream::onsourcereassign(source *s) +{ + // should NEVER happen because streams do have the highest priority, see constructor + ASSERT(0); + if(src && src==s) + { + reset(); + src = NULL; + } +} + +bool oggstream::stream(ALuint bufid) +{ + ASSERT(valid); + + loopi(2) + { + char pcm[BUFSIZE]; + ALsizei size = 0; + int bitstream; + while(size < BUFSIZE) + { + long bytes = ov_read(&oggfile, pcm + size, BUFSIZE - size, isbigendian(), 2, 1, &bitstream); + if(bytes > 0) size += bytes; + else if (bytes < 0) return false; + else break; // done + } + + if(size==0) + { + if(looping && !ov_pcm_seek(&oggfile, 0)) continue; // try again to replay + else return false; + } + + alclearerr(); + alBufferData(bufid, format, pcm, size, info->rate); + return !ALERR; + } + + return false; +} + +bool oggstream::update() +{ + ASSERT(valid); + if(!isopen || !playing()) return false; + + // update buffer queue + ALint processed; + bool active = true; + alGetSourcei(src->id, AL_BUFFERS_PROCESSED, &processed); + loopi(processed) + { + ALuint buffer; + alSourceUnqueueBuffers(src->id, 1, &buffer); + active = stream(buffer); + if(active) alSourceQueueBuffers(src->id, 1, &buffer); + } + + if(active) + { + // fade in + if(startmillis > 0) + { + const float start = (lastmillis-startmillis)/(float)startfademillis; + if(start>=0.00f && start<=1.00001f) + { + setgain(start); + return true; + } + } + + // fade out + if(endmillis > 0) + { + if(lastmillis<=endmillis) // set gain + { + const float end = (endmillis-lastmillis)/(float)endfademillis; + if(end>=-0.00001f && end<=1.00f) + { + setgain(end); + return true; + } + } + else // stop + { + active = false; + } + } + } + + if(!active) reset(); // reset stream if the end is reached + return active; +} + +bool oggstream::playing() +{ + ASSERT(valid); + return src->playing(); +} + +void oggstream::updategain() +{ + ASSERT(valid); + src->gain(gain*volume); +} + +void oggstream::setgain(float g) +{ + ASSERT(valid); + gain = g; + updategain(); +} + +void oggstream::setvolume(float v) +{ + ASSERT(valid); + volume = v; + updategain(); +} + +void oggstream::fadein(int startmillis, int fademillis) +{ + ASSERT(valid); + setgain(0.01f); + this->startmillis = startmillis; + this->startfademillis = fademillis; +} + +void oggstream::fadeout(int endmillis, int fademillis) +{ + ASSERT(valid); + this->endmillis = (endmillis || totalseconds > 0.0f) ? endmillis : lastmillis+(int)totalseconds; + this->endfademillis = fademillis; +} + +bool oggstream::playback(bool looping) +{ + ASSERT(valid); + if(playing()) return true; + this->looping = looping; + if(!stream(bufferids[0]) || !stream(bufferids[1])) return false; + if(!startmillis && !endmillis && !startfademillis && !endfademillis) setgain(1.0f); + + updategain(); + src->queuebuffers(2, bufferids); + src->play(); + + return true; +} + +void oggstream::seek(double offset) +{ + ASSERT(valid); + if(!totalseconds) return; + ov_time_seek_page(&oggfile, fmod(totalseconds-5.0f, totalseconds)); +} + diff --git a/source/src/openal.cpp b/source/src/openal.cpp new file mode 100644 index 00000000..f20a0853 --- /dev/null +++ b/source/src/openal.cpp @@ -0,0 +1,398 @@ +// simple OpenAL call wrappers + +#include "pch.h" +#include "cube.h" + +#define DEBUGCOND (audiodebug==1) + +VAR(al_referencedistance, 0, 400, 1000000); +VAR(al_rollofffactor, 0, 100, 1000000); + +// represents an OpenAL source, an audio emitter in the 3D world + +source::source() : id(0), owner(NULL), locked(false), valid(false), priority(SP_NORMAL) +{ + valid = generate(); + ASSERT(!valid || alIsSource(id)); +} + +source::~source() +{ + if(valid) delete_(); +} + +void source::lock() +{ + locked = true; +} + +void source::unlock() +{ + locked = false; + owner = NULL; + stop(); + buffer(0); +} + +void source::reset() +{ + ASSERT(alIsSource(id)); + owner = NULL; + locked = false; + priority = SP_NORMAL; + + // restore default settings + + stop(); + buffer(0); + + pitch(1.0f); + gain(1.0f); + position(0.0f, 0.0f, 0.0f); + velocity(0.0f, 0.0f, 0.0f); + + looping(false); + sourcerelative(false); + + // fit into distance model + alSourcef(id, AL_REFERENCE_DISTANCE, al_referencedistance/100.0f); + alSourcef(id, AL_ROLLOFF_FACTOR, al_rollofffactor/100.0f); +} + + +void source::init(sourceowner *o) +{ + ASSERT(o); + owner = o; +} + +void source::onreassign() +{ + if(owner) + { + owner->onsourcereassign(this); + owner = NULL; + } +} + +bool source::generate() +{ + alclearerr(); + alGenSources(1, &id); + + return !alerr(false); +} + +bool source::delete_() +{ + alclearerr(); + alDeleteSources(1, &id); + return !ALERR; +} + +bool source::buffer(ALuint buf_id) +{ + alclearerr(); +#ifdef __APPLE__ // weird bug + if (buf_id) +#endif + alSourcei(id, AL_BUFFER, buf_id); + + return !ALERR; +} + +bool source::looping(bool enable) +{ + alclearerr(); + alSourcei(id, AL_LOOPING, enable ? 1 : 0); + return !ALERR; +} + +bool source::queuebuffers(ALsizei n, const ALuint *buffer_ids) +{ + alclearerr(); + alSourceQueueBuffers(id, n, buffer_ids); + return !ALERR; +} + +bool source::unqueueallbuffers() +{ + alclearerr(); + ALint queued; + alGetSourcei(id, AL_BUFFERS_QUEUED, &queued); + ALERR; + loopi(queued) + { + ALuint buffer; + alSourceUnqueueBuffers(id, 1, &buffer); + } + return !ALERR; +} + +bool source::gain(float g) +{ + alclearerr(); + alSourcef(id, AL_GAIN, g); + return !ALERR; +} + +bool source::pitch(float p) +{ + alclearerr(); + alSourcef(id, AL_PITCH, p); + return !ALERR; +} + +bool source::position(const vec &pos) +{ + alclearerr(); + alSourcefv(id, AL_POSITION, (ALfloat *) &pos); + return !ALERR; +} + +bool source::position(float x, float y, float z) +{ + alclearerr(); + alSource3f(id, AL_POSITION, x, y, z); + return !ALERR; +} + +bool source::velocity(float x, float y, float z) +{ + alclearerr(); + alSource3f(id, AL_VELOCITY, x, y, z); + return !ALERR; +} + +vec source::position() +{ + alclearerr(); + vec p; + alGetSourcefv(id, AL_POSITION, (ALfloat*) &p); + if(ALERR) return vec(0,0,0); + else return p; +} + +bool source::sourcerelative(bool enable) +{ + alclearerr(); + alSourcei(id, AL_SOURCE_RELATIVE, enable ? AL_TRUE : AL_FALSE); + return !ALERR; +} + +int source::state() +{ + ALint s; + alGetSourcei(id, AL_SOURCE_STATE, &s); + return s; +} + +bool source::secoffset(float secs) +{ + alclearerr(); + alSourcef(id, AL_SEC_OFFSET, secs); + // some openal implementations seem to spam invalid enum on this + // return !ALERR; + return !alerr(false); +} + +float source::secoffset() +{ + alclearerr(); + ALfloat s; + alGetSourcef(id, AL_SEC_OFFSET, &s); + // some openal implementations seem to spam invalid enum on this + // ALERR; + alerr(false); + return s; +} + +bool source::playing() +{ + return (state() == AL_PLAYING); +} + +bool source::play() +{ + alclearerr(); + alSourcePlay(id); + return !ALERR; +} + +bool source::stop() +{ + alclearerr(); + alSourceStop(id); + return !ALERR; +} + +bool source::rewind() +{ + alclearerr(); + alSourceRewind(id); + return !ALERR; +} + +void source::printposition() +{ + alclearerr(); + vec v = position(); + ALint s; + alGetSourcei(id, AL_SOURCE_TYPE, &s); + conoutf("sound %d: pos(%f,%f,%f) t(%d) ", id, v.x, v.y, v.z, s); + ALERR; +} + + +// represents an OpenAL sound buffer + +sbuffer::sbuffer() : id(0), name(NULL) +{ +} + +sbuffer::~sbuffer() +{ + unload(); +} + +bool sbuffer::load() +{ + if(!name) return false; + if(id) return true; + alclearerr(); + alGenBuffers(1, &id); + if(!ALERR) + { + const char *exts[] = { "", ".wav", ".ogg" }; + string filepath; + loopi(sizeof(exts)/sizeof(exts[0])) + { + s_sprintf(filepath)("packages/audio/sounds/%s%s", name, exts[i]); + const char *file = findfile(path(filepath), "rb"); + size_t len = strlen(filepath); + + if(len >= 4 && !strcasecmp(filepath + len - 4, ".ogg")) + { + FILE *f = fopen(file, "rb"); + if(!f) continue; + + OggVorbis_File oggfile; + if(!ov_open_callbacks(f, &oggfile, NULL, 0, oggcallbacks)) + { + vorbis_info *info = ov_info(&oggfile, -1); + + const size_t BUFSIZE = 32*1024; + vector buf; + int bitstream; + long bytes; + + do + { + char buffer[BUFSIZE]; + bytes = ov_read(&oggfile, buffer, BUFSIZE, isbigendian(), 2, 1, &bitstream); + loopi(bytes) buf.add(buffer[i]); + } while(bytes > 0); + + alBufferData(id, info->channels == 2 ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16, buf.getbuf(), buf.length(), info->rate); + ov_clear(&oggfile); + } + else + { + fclose(f); + continue; + } + } + else + { + SDL_AudioSpec wavspec; + uint32_t wavlen; + uint8_t *wavbuf; + + if(!SDL_LoadWAV(file, &wavspec, &wavbuf, &wavlen)) + { + SDL_ClearError(); + continue; + } + + ALenum format; + switch(wavspec.format) // map wav header to openal format + { + case AUDIO_U8: + case AUDIO_S8: + format = wavspec.channels==2 ? AL_FORMAT_STEREO8 : AL_FORMAT_MONO8; + break; + case AUDIO_U16: + case AUDIO_S16: + format = wavspec.channels==2 ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16; + break; + default: + SDL_FreeWAV(wavbuf); + unload(); + return false; + } + + alBufferData(id, format, wavbuf, wavlen, wavspec.freq); + SDL_FreeWAV(wavbuf); + + if(ALERR) break; + } + + return true; + } + } + unload(); // loading failed + return false; +} + +void sbuffer::unload() +{ + if(!id) return; + alclearerr(); + if(alIsBuffer(id)) alDeleteBuffers(1, &id); + id = 0; + ALERR; +} + +// buffer collection, find or load data + +bufferhashtable::~bufferhashtable() {} + +sbuffer *bufferhashtable::find(char *name) +{ + sbuffer *b = access(name); + if(!b) + { + name = newstring(name); + b = &(*this)[name]; + b->name = name; + } + return b; +} + + +// OpenAL error handling + +void alclearerr() +{ + alGetError(); +} + +bool alerr(bool msg, int line) +{ + ALenum er = alGetError(); + if(er && msg) + { + const char *desc = "unknown"; + switch(er) + { + case AL_INVALID_NAME: desc = "invalid name"; break; + case AL_INVALID_ENUM: desc = "invalid enum"; break; + case AL_INVALID_VALUE: desc = "invalid value"; break; + case AL_INVALID_OPERATION: desc = "invalid operation"; break; + case AL_OUT_OF_MEMORY: desc = "out of memory"; break; + } + if(line) conoutf("\f3OpenAL Error (%X): %s, line %d", er, desc, line); + else conoutf("\f3OpenAL Error (%X): %s", er, desc); + } + return er > 0; +} \ No newline at end of file diff --git a/source/src/pch.h b/source/src/pch.h index f7266f51..a53e0001 100644 --- a/source/src/pch.h +++ b/source/src/pch.h @@ -53,3 +53,13 @@ #include #include +#ifdef __APPLE__ +#include "OpenAL/al.h" +#include "OpenAL/alc.h" +#include "Vorbis/vorbisfile.h" +#else +#include "AL/al.h" +#include "AL/alc.h" +#include "vorbis/vorbisfile.h" +#endif + diff --git a/source/src/physics.cpp b/source/src/physics.cpp index f3898d47..879f3a89 100644 --- a/source/src/physics.cpp +++ b/source/src/physics.cpp @@ -429,7 +429,7 @@ void moveplayer(physent *pl, int moveres, bool local, int curtime) pl->jumpnext = false; pl->vel.z = 2.0f; // physics impulse upwards if(water) { pl->vel.x /= 8; pl->vel.y /= 8; } // dampen velocity change even harder, gives correct water feel - else if(pl==player1 || pl->type!=ENT_PLAYER) playsoundc(S_JUMP, pl); + else if(pl==player1 || pl->type!=ENT_PLAYER) audiomgr.playsoundc(S_JUMP, pl); } pl->timeinair = 0; } @@ -444,7 +444,7 @@ void moveplayer(physent *pl, int moveres, bool local, int curtime) int sound = timeinair > 800 ? S_HARDLAND : S_SOFTLAND; if(pl->state!=CS_DEAD) { - if(pl==player1 || pl->type!=ENT_PLAYER) playsoundc(sound, pl); + if(pl==player1 || pl->type!=ENT_PLAYER) audiomgr.playsoundc(sound, pl); } } } @@ -585,12 +585,12 @@ void moveplayer(physent *pl, int moveres, bool local, int curtime) { if(!pl->lastsplash || lastmillis-pl->lastsplash>500) { - playsound(S_SPLASH2, pl); + audiomgr.playsound(S_SPLASH2, pl); pl->lastsplash = lastmillis; } if(pl==player1) pl->vel.z = 0; } - else if(pl->inwater && !water) playsound(S_SPLASH1, &pl->o); + else if(pl->inwater && !water) audiomgr.playsound(S_SPLASH1, &pl->o); pl->inwater = water; } @@ -708,7 +708,7 @@ void updatecrouch(playerent *p, bool on) const float crouchspeed = 0.6f; p->crouching = on; p->eyeheightvel = on ? -crouchspeed : crouchspeed; - if(p==player1) playsoundc(on ? S_CROUCH : S_UNCROUCH); + if(p==player1) audiomgr.playsoundc(on ? S_CROUCH : S_UNCROUCH); } void crouch(bool on) diff --git a/source/src/protos.h b/source/src/protos.h index 4ca99419..d3f0070f 100644 --- a/source/src/protos.h +++ b/source/src/protos.h @@ -563,11 +563,12 @@ extern bool objcollide(physent *d, const vec &objpos, float objrad, float objhei extern bool collide(physent *d, bool spawn = false, float drop = 0, float rise = 0); // sound -extern void playsound(int n, int priority = SP_NORMAL); -extern void playsound(int n, physent *p, int priority = SP_NORMAL); -extern void playsound(int n, entity *e, int priority = SP_NORMAL); -extern void playsound(int n, const vec *v, int priority = SP_NORMAL); -extern void playsoundc(int n, physent *p = NULL); +/* +extern void audiomgr.playsound(int n, int priority = SP_NORMAL); +extern void audiomgr.playsound(int n, physent *p, int priority = SP_NORMAL); +extern void audiomgr.playsound(int n, entity *e, int priority = SP_NORMAL); +extern void audiomgr.playsound(int n, const vec *v, int priority = SP_NORMAL); +extern void audiomgr.playsoundc(int n, physent *p = NULL); extern void initsound(); extern void soundcleanup(); extern void musicsuggest(int id, int millis = 0, bool rndofs = false); @@ -578,6 +579,7 @@ extern void updateaudio(); extern void preloadmapsound(entity &e); extern void preloadmapsounds(); extern void writesoundconfig(FILE *f); +*/ // rendermodel extern void rendermodel(const char *mdl, int anim, int tex, float rad, const vec &o, float yaw, float pitch, float speed = 0, int basetime = 0, playerent *d = NULL, modelattach *a = NULL, float scale = 1.0f); diff --git a/source/src/renderparticles.cpp b/source/src/renderparticles.cpp index 0ea9fde8..b1b85c1e 100644 --- a/source/src/renderparticles.cpp +++ b/source/src/renderparticles.cpp @@ -610,7 +610,7 @@ bool addbullethole(dynent *d, const vec &from, const vec &to, float radius, bool newparticle(o, surface, bulletholettl, 7); if(noisy && bulletbouncesound && bulletbouncesoundrad && d!=player1 && o.dist(camera1->o) <= bulletbouncesoundrad) { - playsound(o.zo) > bulletairsoundrad) return; // outside player radius - playsound(S_BULLETAIR1 + rnd(2), &soundpos, SP_LOW); + audiomgr.playsound(S_BULLETAIR1 + rnd(2), &soundpos, SP_LOW); } diff --git a/source/src/sound.cpp b/source/src/sound.cpp index 604f53cb..78b04004 100644 --- a/source/src/sound.cpp +++ b/source/src/sound.cpp @@ -3,1868 +3,3 @@ #include "pch.h" #include "cube.h" -#ifdef __APPLE__ -#include "OpenAL/al.h" -#include "OpenAL/alc.h" -#include "Vorbis/vorbisfile.h" -#else -#include "AL/al.h" -#include "AL/alc.h" -#include "vorbis/vorbisfile.h" -#endif - -VARF(audio, 0, 1, 1, initwarning("sound configuration", INIT_RESET, CHANGE_SOUND)); -VARP(gainscale, 1, 90, 100); - -VARP(audiodebug, 0, 0, 1); -#define DEBUGCOND (audiodebug==1) - -VAR(al_referencedistance, 0, 400, 1000000); -VAR(al_rollofffactor, 0, 100, 1000000); - -static bool nosound = true; -ALCdevice *device = NULL; -ALCcontext *context = NULL; - -struct location *playsound(int n, const struct worldobjreference &r, int priority, float offset = 0.0f, bool loop = false); - -void alclearerr() -{ - alGetError(); -} - -bool alerr(bool msg = true, int line = 0) -{ - ALenum er = alGetError(); - if(er && msg) - { - const char *desc = "unknown"; - switch(er) - { - case AL_INVALID_NAME: desc = "invalid name"; break; - case AL_INVALID_ENUM: desc = "invalid enum"; break; - case AL_INVALID_VALUE: desc = "invalid value"; break; - case AL_INVALID_OPERATION: desc = "invalid operation"; break; - case AL_OUT_OF_MEMORY: desc = "out of memory"; break; - } - if(line) conoutf("\f3OpenAL Error (%X): %s, line %d", er, desc, line); - else conoutf("\f3OpenAL Error (%X): %s", er, desc); - } - return er > 0; -} - -#define ALERR alerr(true, __LINE__) - -// provides an interface for callbacks. - -struct sourceowner -{ - virtual ~sourceowner() {} - virtual void onsourcereassign(struct source *s) = 0; -}; - -// represents an OpenAL source, an audio emitter in the 3D world -// available sources are provided by the sourcescheduler - -struct source -{ - ALuint id; - - sourceowner *owner; - bool locked, valid; - int priority; - - source() : id(0), owner(NULL), locked(false), valid(false), priority(SP_NORMAL) - { - valid = generate(); - ASSERT(!valid || alIsSource(id)); - } - - ~source() - { - if(valid) delete_(); - } - - void lock() { locked = true; } - - void unlock() - { - locked = false; - owner = NULL; - stop(); - buffer(0); - } - - void reset() - { - ASSERT(alIsSource(id)); - owner = NULL; - locked = false; - priority = SP_NORMAL; - - // restore default settings - - stop(); - buffer(0); - - pitch(1.0f); - gain(1.0f); - position(0.0f, 0.0f, 0.0f); - velocity(0.0f, 0.0f, 0.0f); - - looping(false); - sourcerelative(false); - - // fit into distance model - alSourcef(id, AL_REFERENCE_DISTANCE, al_referencedistance/100.0f); - alSourcef(id, AL_ROLLOFF_FACTOR, al_rollofffactor/100.0f); - } - - - void init(sourceowner *o) - { - ASSERT(o); - owner = o; - } - - void onreassign() - { - if(owner) - { - owner->onsourcereassign(this); - owner = NULL; - } - } - - // OpenAL wrapping methods - - bool generate() - { - alclearerr(); - alGenSources(1, &id); - - return !alerr(false); - } - - bool delete_() - { - alclearerr(); - alDeleteSources(1, &id); - return !ALERR; - } - - bool buffer(ALuint buf_id) - { - alclearerr(); -#ifdef __APPLE__ // weird bug - if (buf_id) -#endif - alSourcei(id, AL_BUFFER, buf_id); - - return !ALERR; - } - - bool looping(bool enable) - { - alclearerr(); - alSourcei(id, AL_LOOPING, enable ? 1 : 0); - return !ALERR; - } - - bool queuebuffers(ALsizei n, const ALuint *buffer_ids) - { - alclearerr(); - alSourceQueueBuffers(id, n, buffer_ids); - return !ALERR; - } - - bool unqueueallbuffers() - { - alclearerr(); - ALint queued; - alGetSourcei(id, AL_BUFFERS_QUEUED, &queued); - ALERR; - loopi(queued) - { - ALuint buffer; - alSourceUnqueueBuffers(id, 1, &buffer); - } - return !ALERR; - } - - bool gain(float g) - { - alclearerr(); - alSourcef(id, AL_GAIN, g); - return !ALERR; - } - - bool pitch(float p) - { - alclearerr(); - alSourcef(id, AL_PITCH, p); - return !ALERR; - } - - bool position(const vec &pos) - { - alclearerr(); - alSourcefv(id, AL_POSITION, (ALfloat *) &pos); - return !ALERR; - } - - bool position(float x, float y, float z) - { - alclearerr(); - alSource3f(id, AL_POSITION, x, y, z); - return !ALERR; - } - - bool velocity(float x, float y, float z) - { - alclearerr(); - alSource3f(id, AL_VELOCITY, x, y, z); - return !ALERR; - } - - vec position() - { - alclearerr(); - vec p; - alGetSourcefv(id, AL_POSITION, (ALfloat*) &p); - if(ALERR) return vec(0,0,0); - else return p; - } - - bool sourcerelative(bool enable) - { - alclearerr(); - alSourcei(id, AL_SOURCE_RELATIVE, enable ? AL_TRUE : AL_FALSE); - return !ALERR; - } - - int state() - { - ALint s; - alGetSourcei(id, AL_SOURCE_STATE, &s); - return s; - } - - bool secoffset(float secs) - { - alclearerr(); - alSourcef(id, AL_SEC_OFFSET, secs); - // some openal implementations seem to spam invalid enum on this - // return !ALERR; - return !alerr(false); - } - - float secoffset() - { - alclearerr(); - ALfloat s; - alGetSourcef(id, AL_SEC_OFFSET, &s); - // some openal implementations seem to spam invalid enum on this - // ALERR; - alerr(false); - return s; - } - - bool playing() - { - return (state() == AL_PLAYING); - } - - bool play() - { - alclearerr(); - alSourcePlay(id); - return !ALERR; - } - - bool stop() - { - alclearerr(); - alSourceStop(id); - return !ALERR; - } - - bool rewind() - { - alclearerr(); - alSourceRewind(id); - return !ALERR; - } - - void printposition() - { - alclearerr(); - vec v = position(); - ALint s; - alGetSourcei(id, AL_SOURCE_TYPE, &s); - conoutf("sound %d: pos(%f,%f,%f) t(%d) ", id, v.x, v.y, v.z, s); - ALERR; - } -}; - -VARP(soundschedpriorityscore, 0, 100, 1000); -VARP(soundscheddistancescore, 0, 5, 1000); -VARP(soundschedoldbonus, 0, 100, 1000); -VARP(soundschedreserve, 0, 2, 100); - -// AC sound scheduler, manages available sound sources. -// It keeps a set of reserved sources for each priority level to avoid running out of sources by floods of low-priority sounds. -// Under load it uses priority and distance information to reassign its resources. - -extern int soundchannels; - -struct sourcescheduler -{ - vector sources; - - sourcescheduler() {} - - void init() - { - int newchannels = soundchannels - sources.length(); - if(newchannels < 0) - { - loopv(sources) - { - source *src = sources[i]; - if(src->locked) continue; - sources.remove(i--); - delete src; - if(sources.length() <= soundchannels) break; - } - } - else loopi(newchannels) - { - source *src = new source(); - if(src->valid) sources.add(src); - else - { - DELETEP(src); - break; - } - } - - } - - void reset() - { - loopv(sources) sources[i]->reset(); - sources.deletecontentsp(); - } - - // returns a free sound source (channel) - // consuming code must call sourcescheduler::releasesource() after use - - source *newsource(int priority, const vec &o) - { - if(!sources.length()) - { - DEBUG("empty source collection"); - return NULL; - } - - source *src = NULL; - - // reserve some sources for sounds of higher priority - int reserved = (SP_HIGHEST-priority)*soundschedreserve; - DEBUGVAR(reserved); - - // search unused source - loopv(sources) - { - if(!sources[i]->locked && reserved--<=0) - { - src = sources[i]; - DEBUGVAR(src); - break; - } - } - - if(!src) - { - DEBUG("no empty source found"); - - // low priority sounds can't replace others - if(SP_LOW==priority) - { - DEBUG("low prio sound aborted"); - return NULL; - } - - // try replacing a used source - // score our sound - const float dist = o.iszero() ? 0.0f : camera1->o.dist(o); - const float score = (priority*soundschedpriorityscore) - (dist*soundscheddistancescore); - - // score other sounds - float worstscore = 0.0f; - source *worstsource = NULL; - - loopv(sources) - { - source *s = sources[i]; - if(s->priority==SP_HIGHEST) continue; // highest priority sounds can't be replaced - - vec otherpos = s->position(); - float otherdist = otherpos.iszero() ? 0.0f : camera1->o.dist(otherpos); - float otherscore = (s->priority*soundschedpriorityscore) - (otherdist*soundscheddistancescore) + soundschedoldbonus; - if(!worstsource || otherscore < worstscore) - { - worstsource = s; - worstscore = otherscore; - } - } - - // pick worst source and replace it - if(worstsource && score>worstscore) - { - src = worstsource; - src->onreassign(); // inform previous owner about the take-over - DEBUG("replaced sound of same prio"); - } - } - - if(!src) - { - DEBUG("sound aborted, no channel takeover possible"); - return NULL; - } - - src->reset(); // default settings - src->lock(); // exclusive lock - src->priority = priority; - return src; - } - - // give source back to the pool - void releasesource(source *src) - { - ASSERT(src); - ASSERT(src->locked); // detect double release - - if(!src) return; - src->unlock(); - - if(sources.length() > soundchannels) - { - sources.removeobj(src); - delete src; - } - } -}; - -// scheduler instance -sourcescheduler scheduler; - -VARF(soundchannels, 4, 32, 1024, { if(!nosound) scheduler.init(); }); - -// binding of sounds to the 3D world - -struct worldobjreference -{ - enum worldobjtype { WR_CAMERA, WR_PHYSENT, WR_ENTITY, WR_STATICPOS }; - int type; - - worldobjreference(int t) : type(t) {} - virtual ~worldobjreference() {} - virtual worldobjreference *clone() const = 0; - virtual const vec ¤tposition() const = 0; - virtual bool nodistance() = 0; - virtual bool operator==(const worldobjreference &other) = 0; - virtual bool operator!=(const worldobjreference &other) { return !(*this==other); } - virtual void attach() {} - virtual void detach() {} -}; - -struct camerareference : worldobjreference -{ - camerareference() : worldobjreference(WR_CAMERA) {} - worldobjreference *clone() const { return new camerareference(*this); } - const vec ¤tposition() const { return camera1->o; } - bool nodistance() { return true; } - bool operator==(const worldobjreference &other) { return type==other.type; } -}; - -struct physentreference : worldobjreference -{ - physent *phys; - - physentreference(physent *ref) : worldobjreference(WR_PHYSENT) - { - ASSERT(ref); - phys = ref; - } - - worldobjreference *clone() const { return new physentreference(*this); } - const vec ¤tposition() const { return phys->o; } - bool nodistance() { return phys==camera1; } - bool operator==(const worldobjreference &other) { return type==other.type && phys==((physentreference &)other).phys; } -}; - -struct entityreference : worldobjreference -{ - entity *ent; - - entityreference(entity *ref) : worldobjreference(WR_ENTITY) - { - ASSERT(ref); - ent = ref; - } - - worldobjreference *clone() const { return new entityreference(*this); } - - const vec ¤tposition() const - { - static vec tmp; - tmp = vec(ent->x, ent->y, ent->z); - return tmp; - } - - bool nodistance() { return ent->attr3>0; } - bool operator==(const worldobjreference &other) { return type==other.type && ent==((entityreference &)other).ent; } -}; - -struct staticreference : worldobjreference -{ - vec pos; - - staticreference(const vec &ref) : worldobjreference(WR_STATICPOS) - { - pos = ref; - } - - worldobjreference *clone() const { return new staticreference(*this); } - const vec ¤tposition() const { return pos; } - bool nodistance() { return false; } - bool operator==(const worldobjreference &other) { return type==other.type && pos==((staticreference &)other).pos; } -}; - - -static int oggseek(FILE *f, ogg_int64_t off, int whence) -{ - return f ? fseek(f, off, whence) : -1; -} - -static ov_callbacks oggcallbacks = -{ - (size_t (*)(void *, size_t, size_t, void *)) fread, - (int (*)(void *, ogg_int64_t, int)) oggseek, - (int (*)(void *)) fclose, - (long (*)(void *)) ftell -}; - -// audio streaming for background music - -struct oggstream : sourceowner -{ - string name; - bool valid; - - // file stream - OggVorbis_File oggfile; - bool isopen; - vorbis_info *info; - double totalseconds; - static const int BUFSIZE = 1024 * 512; // 512kb buffer - - // OpenAL resources - ALuint bufferids[2]; - source *src; - ALenum format; - - // settings - float volume, gain; - int startmillis, endmillis, startfademillis, endfademillis; - bool looping; - - oggstream() : valid(false), isopen(false), src(NULL) - { - reset(); - - // grab a source and keep it during the whole lifetime - src = scheduler.newsource(SP_HIGHEST, camera1->o); - if(src) - { - if(src->valid) - { - src->init(this); - src->sourcerelative(true); - } - else - { - scheduler.releasesource(src); - src = NULL; - } - } - - if(!src) return; - - alclearerr(); - alGenBuffers(2, bufferids); - valid = !ALERR; - } - - ~oggstream() - { - reset(); - - if(src) scheduler.releasesource(src); - - if(alIsBuffer(bufferids[0]) || alIsBuffer(bufferids[1])) - { - alclearerr(); - alDeleteBuffers(2, bufferids); - ALERR; - } - } - - void reset() - { - name[0] = '\0'; - - // stop playing - if(src) - { - src->stop(); - src->unqueueallbuffers(); - src->buffer(0); - } - format = AL_NONE; - - // reset file handler - if(isopen) - { - isopen = !ov_clear(&oggfile); - } - info = NULL; - totalseconds = 0.0f; - - // default settings - startmillis = endmillis = startfademillis = endfademillis = 0; - gain = 1.0f; // reset gain but not volume setting - looping = false; - } - - bool open(const char *f) - { - ASSERT(valid); - if(!f) return false; - if(playing() || isopen) reset(); - - const char *exts[] = { "", ".wav", ".ogg" }; - string filepath; - - loopi(sizeof(exts)/sizeof(exts[0])) - { - s_sprintf(filepath)("packages/audio/songs/%s%s", f, exts[i]); - FILE *file = fopen(findfile(path(filepath), "rb"), "rb"); - if(!file) continue; - - isopen = !ov_open_callbacks(file, &oggfile, NULL, 0, oggcallbacks); - if(!isopen) - { - fclose(file); - continue; - } - - info = ov_info(&oggfile, -1); - format = info->channels == 2 ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16; - totalseconds = ov_time_total(&oggfile, -1); - s_strcpy(name, f); - - return true; - } - return false; - } - - void onsourcereassign(source *s) - { - // should NEVER happen because streams do have the highest priority, see constructor - ASSERT(0); - if(src && src==s) - { - reset(); - src = NULL; - } - } - - bool stream(ALuint bufid) - { - ASSERT(valid); - - loopi(2) - { - char pcm[BUFSIZE]; - ALsizei size = 0; - int bitstream; - while(size < BUFSIZE) - { - long bytes = ov_read(&oggfile, pcm + size, BUFSIZE - size, isbigendian(), 2, 1, &bitstream); - if(bytes > 0) size += bytes; - else if (bytes < 0) return false; - else break; // done - } - - if(size==0) - { - if(looping && !ov_pcm_seek(&oggfile, 0)) continue; // try again to replay - else return false; - } - - alclearerr(); - alBufferData(bufid, format, pcm, size, info->rate); - return !ALERR; - } - - return false; - } - - bool update() - { - ASSERT(valid); - if(!isopen || !playing()) return false; - - // update buffer queue - ALint processed; - bool active = true; - alGetSourcei(src->id, AL_BUFFERS_PROCESSED, &processed); - loopi(processed) - { - ALuint buffer; - alSourceUnqueueBuffers(src->id, 1, &buffer); - active = stream(buffer); - if(active) alSourceQueueBuffers(src->id, 1, &buffer); - } - - if(active) - { - // fade in - if(startmillis > 0) - { - const float start = (lastmillis-startmillis)/(float)startfademillis; - if(start>=0.00f && start<=1.00001f) - { - setgain(start); - return true; - } - } - - // fade out - if(endmillis > 0) - { - if(lastmillis<=endmillis) // set gain - { - const float end = (endmillis-lastmillis)/(float)endfademillis; - if(end>=-0.00001f && end<=1.00f) - { - setgain(end); - return true; - } - } - else // stop - { - active = false; - } - } - } - - if(!active) reset(); // reset stream if the end is reached - return active; - } - - bool playing() - { - ASSERT(valid); - return src->playing(); - } - - void updategain() - { - ASSERT(valid); - src->gain(gain*volume); - } - - void setgain(float g) - { - ASSERT(valid); - gain = g; - updategain(); - } - - void setvolume(float v) - { - ASSERT(valid); - volume = v; - updategain(); - } - - void fadein(int startmillis, int fademillis) - { - ASSERT(valid); - setgain(0.01f); - this->startmillis = startmillis; - this->startfademillis = fademillis; - } - - void fadeout(int endmillis, int fademillis) - { - ASSERT(valid); - this->endmillis = (endmillis || totalseconds > 0.0f) ? endmillis : lastmillis+(int)totalseconds; - this->endfademillis = fademillis; - } - - bool playback(bool looping = false) - { - ASSERT(valid); - if(playing()) return true; - this->looping = looping; - if(!stream(bufferids[0]) || !stream(bufferids[1])) return false; - if(!startmillis && !endmillis && !startfademillis && !endfademillis) setgain(1.0f); - - updategain(); - src->queuebuffers(2, bufferids); - src->play(); - - return true; - } - - void seek(double offset) - { - ASSERT(valid); - if(!totalseconds) return; - ov_time_seek_page(&oggfile, fmod(totalseconds-5.0f, totalseconds)); - } -}; - -struct sbuffer -{ - ALuint id; - const char *name; - - sbuffer() : id(0), name(NULL) - { - } - - ~sbuffer() - { - unload(); - } - - bool load() - { - if(!name) return false; - if(id) return true; - alclearerr(); - alGenBuffers(1, &id); - if(!ALERR) - { - const char *exts[] = { "", ".wav", ".ogg" }; - string filepath; - loopi(sizeof(exts)/sizeof(exts[0])) - { - s_sprintf(filepath)("packages/audio/sounds/%s%s", name, exts[i]); - const char *file = findfile(path(filepath), "rb"); - size_t len = strlen(filepath); - - if(len >= 4 && !strcasecmp(filepath + len - 4, ".ogg")) - { - FILE *f = fopen(file, "rb"); - if(!f) continue; - - OggVorbis_File oggfile; - if(!ov_open_callbacks(f, &oggfile, NULL, 0, oggcallbacks)) - { - vorbis_info *info = ov_info(&oggfile, -1); - - const size_t BUFSIZE = 32*1024; - vector buf; - int bitstream; - long bytes; - - do - { - char buffer[BUFSIZE]; - bytes = ov_read(&oggfile, buffer, BUFSIZE, isbigendian(), 2, 1, &bitstream); - loopi(bytes) buf.add(buffer[i]); - } while(bytes > 0); - - alBufferData(id, info->channels == 2 ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16, buf.getbuf(), buf.length(), info->rate); - ov_clear(&oggfile); - } - else - { - fclose(f); - continue; - } - } - else - { - SDL_AudioSpec wavspec; - uint32_t wavlen; - uint8_t *wavbuf; - - if(!SDL_LoadWAV(file, &wavspec, &wavbuf, &wavlen)) - { - SDL_ClearError(); - continue; - } - - ALenum format; - switch(wavspec.format) // map wav header to openal format - { - case AUDIO_U8: - case AUDIO_S8: - format = wavspec.channels==2 ? AL_FORMAT_STEREO8 : AL_FORMAT_MONO8; - break; - case AUDIO_U16: - case AUDIO_S16: - format = wavspec.channels==2 ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16; - break; - default: - SDL_FreeWAV(wavbuf); - unload(); - return false; - } - - alBufferData(id, format, wavbuf, wavlen, wavspec.freq); - SDL_FreeWAV(wavbuf); - - if(ALERR) break; - } - - return true; - } - } - unload(); // loading failed - return false; - } - - void unload() - { - if(!id) return; - alclearerr(); - if(alIsBuffer(id)) alDeleteBuffers(1, &id); - id = 0; - ALERR; - } -}; - -oggstream *gamemusic = NULL; - - -struct soundconfig -{ - sbuffer *buf; - int vol, uses, maxuses; - bool loop; - bool muted; - - soundconfig(sbuffer *b, int vol, int maxuses, bool loop) - { - buf = b; - this->vol = vol; - this->maxuses = maxuses; - this->loop = loop; - uses = 0; - muted = false; - } - - void onattach() { uses++; } - void ondetach() { uses--; } -}; - -vector gamesounds, mapsounds; - - -// short living sound occurrence, dies once the sound stops -struct location : sourceowner -{ - soundconfig *cfg; - source *src; - worldobjreference *ref; - - bool stale; - int playmillis; - - location(int sound, const worldobjreference &r, int priority = SP_NORMAL) : cfg(NULL), src(NULL), ref(NULL), stale(false), playmillis(0) - { - vector &sounds = (r.type==worldobjreference::WR_ENTITY ? mapsounds : gamesounds); - if(!sounds.inrange(sound)) - { - conoutf("unregistered sound: %d", sound); - stale = true; - return; - } - - // get sound config - cfg = &sounds[sound]; - cfg->onattach(); - if((r.type==worldobjreference::WR_ENTITY && cfg->maxuses >= 0 && cfg->uses >= cfg->maxuses) || cfg->muted) // check max-use limits - { - stale = true; - return; - } - - // assign buffer - sbuffer *buf = cfg->buf; - if(!buf || !buf->id) - { - stale = true; - return; - } - - // obtain source - src = scheduler.newsource(priority, r.currentposition()); - // apply configuration - if(!src || !src->valid || !src->buffer(cfg->buf->id) || !src->looping(cfg->loop) || !src->gain(cfg->vol/100.0f*((float)gainscale)/100.0f)) - { - stale = true; - return; - } - src->init(this); - - // set position - attachworldobjreference(r); - } - - ~location() - { - if(src) scheduler.releasesource(src); - if(cfg) cfg->ondetach(); - if(ref) - { - ref->detach(); - DELETEP(ref); - } - } - - // attach a reference to a world object to get the 3D position from - - void attachworldobjreference(const worldobjreference &r) - { - ASSERT(!stale && src && src->valid); - if(stale) return; - if(ref) - { - ref->detach(); - DELETEP(ref); - } - ref = r.clone(); - evaluateworldobjref(); - ref->attach(); - } - - // enable/disable distance calculations - void evaluateworldobjref() - { - src->sourcerelative(ref->nodistance()); - } - - // marks itself for deletion if source got lost - void onsourcereassign(source *s) - { - if(s==src) - { - stale = true; - src = NULL; - } - } - - void updatepos() - { - ASSERT(!stale && ref); - if(stale) return; - - const vec &pos = ref->currentposition(); - - switch(ref->type) - { - case worldobjreference::WR_CAMERA: break; - case worldobjreference::WR_PHYSENT: - { - if(!ref->nodistance()) src->position(pos); - break; - } - case worldobjreference::WR_ENTITY: - { - entityreference &eref = *(entityreference *)ref; - const float vol = eref.ent->attr4<=0.0f ? 1.0f : eref.ent->attr4/255.0f; - - if(ref->nodistance()) - { - // own distance model for entities/mapsounds: linear & clamping - - const float innerradius = float(eref.ent->attr3); // full gain area / size property - const float outerradius = float(eref.ent->attr2); // fading gain area / radius property - float dist = camera1->o.dist(pos); - - if(dist <= innerradius) src->gain(1.0f*vol); // inside full gain area - else if(dist <= outerradius) // inside fading gain area - { - const float fadeoutdistance = outerradius-innerradius; - const float fadeout = dist-innerradius; - src->gain((1.0f - fadeout/fadeoutdistance)*vol); - } - else src->gain(0.0f); // outside entity - } - else - { - // use openal distance model to make the sound appear from a certain direction (non-ambient) - src->position(pos); - src->gain(vol); - } - break; - } - case worldobjreference::WR_STATICPOS: - { - src->position(pos); - break; - } - } - } - - void update() - { - if(stale) return; - - switch(src->state()) - { - case AL_PLAYING: - updatepos(); - break; - case AL_STOPPED: - case AL_PAUSED: - case AL_INITIAL: - stale = true; - break; - } - } - - void play(bool loop = false) - { - if(stale) return; - - updatepos(); - if(loop) src->looping(loop); - if(src->play()) playmillis = totalmillis; - } - - void pitch(float p) - { - if(stale) return; - src->pitch(p); - } - - void offset(float secs) - { - ASSERT(!stale); - if(stale) return; - src->secoffset(secs); - } - - float offset() - { - ASSERT(!stale); - if(stale) return 0.0f; - return src->secoffset(); - } - - void drop() - { - src->stop(); - stale = true; // drop from collection on next update cycle - } -}; - -struct locvector : vector -{ - virtual ~locvector() {} - - location *find(int sound, worldobjreference *ref = NULL, const vector &soundcollection = gamesounds) - { - if(sound<0 || sound>=soundcollection.length()) return NULL; - loopi(ulen) if(buf[i] && !buf[i]->stale) - { - if(buf[i]->cfg != &soundcollection[sound]) continue; // check if its the same sound - if(ref && *buf[i]->ref!=*ref) continue; // optionally check if its the same reference - return buf[i]; // found - } - return NULL; - } - - void delete_(int i) - { - location *loc = buf[i]; - remove(i); - delete loc; - } - - void replaceworldobjreference(const worldobjreference &oldr, const worldobjreference &newr) - { - loopv(*this) - { - location *l = buf[i]; - if(!l || !l->ref) continue; - if(*l->ref==oldr) l->attachworldobjreference(newr); - } - } - - // update stuff, remove stale data - void updatelocations() - { - // check if camera carrier changed - bool camchanged = false; - static physent *lastcamera = NULL; - if(lastcamera!=camera1) - { - if(lastcamera!=NULL) camchanged = true; - lastcamera = camera1; - } - - // update all locations - loopv(*this) - { - location *l = buf[i]; - if(!l) continue; - - l->update(); - if(l->stale) delete_(i--); - else if(camchanged) l->evaluateworldobjref(); // cam changed, evaluate world reference again - } - } - - // force pitch across all locations - void forcepitch(float pitch) - { - loopv(*this) - { - location *l = buf[i]; - if(!l) continue; - if(l->src && l->src->locked) l->src->pitch(pitch); - } - } - - // delete all sounds except world-neutral sounds like GUI/notifacation - void deleteworldobjsounds() - { - loopv(*this) - { - location *l = buf[i]; - if(!l) continue; - // world-neutral sounds - if(l->cfg == &gamesounds[S_MENUENTER] || - l->cfg == &gamesounds[S_MENUSELECT] || - l->cfg == &gamesounds[S_CALLVOTE] || - l->cfg == &gamesounds[S_VOTEPASS] || - l->cfg == &gamesounds[S_VOTEFAIL]) continue; - - delete_(i--); - } - } -}; - -locvector locations; - - -struct bufferhashtable : hashtable -{ - virtual ~bufferhashtable() {} - - // find or load data - virtual sbuffer *find(char *name) - { - sbuffer *b = access(name); - if(!b) - { - name = newstring(name); - b = &(*this)[name]; - b->name = name; - } - return b; - } -}; - -bufferhashtable bufferpool; - - -VARFP(soundvol, 0, 128, 255, -{ - if(!nosound) alListenerf(AL_GAIN, soundvol/255.0f); -}); - -void setmusicvol() -{ - extern int musicvol; - if(gamemusic) gamemusic->setvolume(musicvol > 0 ? musicvol/255.0f : 0); -} -VARFP(musicvol, 0, 128, 255, setmusicvol()); - -char *musicdonecmd = NULL; - -void stopsound() -{ - if(nosound) return; - DELETEA(musicdonecmd); - if(gamemusic) gamemusic->reset(); -} - -void 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; - s_strcpy(d, "Audio devices: "); - - // null separated device string - for(const ALchar *c = devices; c[strlen(c)+1]; c += strlen(c)+1) - { - if(c!=devices) s_strcat(d, ", "); - s_strcat(d, c); - } - conoutf(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 - scheduler.init(); - - // let the stream get the first source from the scheduler - gamemusic = new oggstream(); - if(!gamemusic->valid) DELETEP(gamemusic); - setmusicvol(); - - nosound = false; - } - } - - if(nosound) - { - ALERR; - if(context) alcDestroyContext(context); - if(device) alcCloseDevice(device); - conoutf("sound initialization failed!"); - } -} - -cvector musics; - -void music(char *name, char *millis, char *cmd) -{ - if(nosound) return; - stopsound(); - if(musicvol && *name) - { - if(cmd[0]) musicdonecmd = newstring(cmd); - - if(gamemusic->open(name)) - { - // fade - if(atoi(millis) > 0) - { - const int fadetime = 1000; - gamemusic->fadein(lastmillis, fadetime); - gamemusic->fadeout(lastmillis+atoi(millis), fadetime); - } - - // play - bool loop = cmd && cmd[0]; - if(!gamemusic->playback(loop)) - { - conoutf("could not play music: %s", name); - return; - } - setmusicvol(); - } - else conoutf("could not open music: %s", name); - } -} - -void musicsuggest(int id, int millis, bool rndofs) // play bg music if nothing else is playing -{ - if(nosound || !gamemusic) return; - if(gamemusic->playing()) 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 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 registermusic(char *name) -{ - if(nosound) return; - if(!name || !name[0]) return; - musics.add(newstring(name)); -} - -COMMAND(registermusic, ARG_1STR); -COMMAND(music, ARG_3STR); - -int findsound(char *name, int vol, vector &sounds) -{ - loopv(sounds) - { - if(!strcmp(sounds[i].buf->name, name) && (!vol || sounds[i].vol==vol)) return i; - } - return -1; -} - -int addsound(char *name, int vol, int maxuses, bool loop, vector &sounds, bool load) -{ - if(nosound) return -1; - - 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 > 0 ? vol : 100, maxuses, loop); - sounds.add(s); - return sounds.length()-1; -} - -void registersound(char *name, char *vol, char *loop) { addsound(name, atoi(vol), -1, atoi(loop) != 0, gamesounds, true); } -COMMAND(registersound, ARG_4STR); - -void mapsound(char *name, char *maxuses) { addsound(name, 255, atoi(maxuses), true, mapsounds, false); } -COMMAND(mapsound, ARG_3STR); - -void preloadmapsound(entity &e) -{ - if(e.type!=SOUND || !mapsounds.inrange(e.attr1)) return; - sbuffer *buf = mapsounds[e.attr1].buf; - if(!buf->load()) conoutf("\f3failed to load sample %s", buf->name); -} - -void preloadmapsounds() -{ - loopv(ents) - { - entity &e = ents[i]; - if(e.type==SOUND) preloadmapsound(e); - } -} - -void 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(); - } -} - -COMMAND(applymapsoundchanges, ARG_NONE); - -// called at game exit -void soundcleanup() -{ - if(nosound) return; - - // destroy consuming code - stopsound(); - DELETEP(gamemusic); - mapsounds.setsize(0); - locations.deletecontentsp(); - gamesounds.setsize(0); - bufferpool.clear(); - - // kill scheduler - scheduler.reset(); - - // shutdown openal - alcMakeContextCurrent(NULL); - if(context) alcDestroyContext(context); - if(device) alcCloseDevice(device); -} - -// clear world-related sounds, called on mapchange -void clearworldsounds(bool fullclean) -{ - stopsound(); - if(fullclean) mapsounds.setsize(0); - locations.deleteworldobjsounds(); -} - -void mapsoundreset() -{ - mapsounds.setsize(0); - locations.deleteworldobjsounds(); -} - -COMMAND(mapsoundreset, ARG_NONE); - -VARP(footsteps, 0, 1, 1); -VARP(localfootsteps, 0, 1, 1); - -void updateplayerfootsteps(playerent *p) -{ - if(!p) return; - - const int footstepradius = 6; - static float lastoffset = 0; - - // find existing footstep sounds - physentreference ref(p); - location *locs[] = - { - locations.find(S_FOOTSTEPS, &ref), - locations.find(S_FOOTSTEPSCROUCH, &ref), - locations.find(S_WATERFOOTSTEPS, &ref), - locations.find(S_WATERFOOTSTEPSCROUCH, &ref) - }; - - 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 - lastoffset = l->offset(); // save last offset - DEBUGVAR(lastoffset); - l->drop(); - } - } - else - { - // play footsteps - - int grounddist = hdr.waterlevel-S((int)p->o.x, (int)p->o.y)->floor; - bool water = p->o.z-p->eyeheight+0.25fp->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; - playsound(stepsound, ref, local ? SP_HIGH : SP_LOW, rndoffset); - } - } -} - -// manage looping sounds -location *updateloopsound(int sound, bool active, float vol = 1.0f) -{ - location *l = locations.find(sound); - 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; -} - -float currentpitch = 1.0f; - -VARP(mapsoundrefresh, 0, 10, 1000); - -void updateaudio() -{ - if(nosound) return; - - alcSuspendContext(context); // don't process sounds while we mess around - - //bool alive = player1->state!=CS_DEAD; - 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); - - // 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; - bool hearable = e.attr2==0 || camera1->o.dist(o)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); -} - - -void unmuteallsounds() -{ - loopv(gamesounds) gamesounds[i].muted = false; -} - -void mutesound(int n, int off) -{ - bool mute = (off == 0); - if(!gamesounds.inrange(n)) - { - conoutf("\f3could not %s sound no %d", mute ? "silence" : "unmute", n); - return; - } - gamesounds[n].muted = mute; -} - -int soundmuted(int n) -{ - return gamesounds.inrange(n) && gamesounds[n].muted ? 1 : 0; -} - -COMMAND(unmuteallsounds, ARG_NONE); -COMMAND(mutesound, ARG_2INT); -COMMAND(soundmuted, ARG_1EXP); - -void writesoundconfig(FILE *f) -{ - loopv(gamesounds) if(gamesounds[i].muted) fprintf(f, "mutesound %d\n", i); -} - -VARP(maxsoundsatonce, 0, 10, 100); - -location *playsound(int n, const worldobjreference &r, int priority, float offset, bool loop) -{ - if(nosound || !soundvol) 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 playsound(int n, int priority) { playsound(n, camerareference(), priority); } -void playsound(int n, physent *p, int priority) { if(p) playsound(n, physentreference(p), priority); } -void playsound(int n, entity *e, int priority) { if(e) playsound(n, entityreference(e), priority); } -void playsound(int n, const vec *v, int priority) { if(v) playsound(n, staticreference(*v), priority); } - -void 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); - playsound(id, loc); -} - -void sound(int n) { playsound(n); } -COMMAND(sound, ARG_1INT); - -void playsoundc(int n, physent *p) -{ - if(p && p!=player1) playsound(n, p); - else - { - addmsg(SV_SOUND, "i", n); - playsound(n); - } -} - -void voicecom(char *sound, char *text) -{ - if(!sound || !sound[0]) return; - static int last = 0; - if(!last || lastmillis-last > 2000) - { - s_sprintfd(soundpath)("voicecom/%s", sound); - int s = findsound(soundpath, 0, gamesounds); - if(s < 0 || s < S_AFFIRMATIVE || s > S_NICESHOT) return; - playsound(s, SP_HIGH); - if(s == S_NICESHOT) // public - { - addmsg(SV_VOICECOM, "ri", s); - toserver(text); - } - else // team - { - addmsg(SV_VOICECOMTEAM, "ri", s); - s_sprintfd(teamtext)("%c%s", '%', text); - toserver(teamtext); - } - last = lastmillis; - } -} - -COMMAND(voicecom, ARG_2STR); - -void detachsounds(playerent *owner) -{ - if(nosound) return; - // make all dependent locations static - locations.replaceworldobjreference(physentreference(owner), staticreference(owner->o)); -} - -void soundtest() -{ - loopi(S_NULL) playsound(i, rnd(SP_HIGH+1)); -} - -COMMAND(soundtest, ARG_NONE); - diff --git a/source/src/sound.h b/source/src/sound.h index d8a03cc5..ebe8c087 100644 --- a/source/src/sound.h +++ b/source/src/sound.h @@ -65,6 +65,7 @@ enum SP_HIGHEST }; +// hardcoded music enum { M_FLAGGRAB = 0, @@ -72,3 +73,305 @@ enum M_LASTMINUTE2 }; +// owner of an OpenAL source, used as callback interface +struct sourceowner +{ + virtual ~sourceowner() {} + virtual void onsourcereassign(struct source *s) = 0; +}; + + +// represents an OpenAL source, an audio emitter in the 3D world + +struct source +{ + ALuint id; + + sourceowner *owner; + bool locked, valid; + int priority; + + source(); + ~source(); + + void lock(); + void unlock(); + void reset(); + void init(sourceowner *o); + void onreassign(); + + bool generate(); + bool delete_(); + bool buffer(ALuint buf_id); + bool looping(bool enable); + bool queuebuffers(ALsizei n, const ALuint *buffer_ids); + bool unqueueallbuffers(); + bool gain(float g); + bool pitch(float p); + bool position(const vec &pos); + bool position(float x, float y, float z); + bool velocity(float x, float y, float z); + vec position(); + bool sourcerelative(bool enable); + int state(); + bool secoffset(float secs); + float secoffset(); + bool playing(); + bool play(); + bool stop(); + bool rewind(); + + void printposition(); +}; + + +// represents an OpenAL sound buffer + +struct sbuffer +{ + ALuint id; + const char *name; + + sbuffer(); + ~sbuffer(); + + bool load(); + void unload(); +}; + +// buffer storage + +struct bufferhashtable : hashtable +{ + virtual ~bufferhashtable(); + virtual sbuffer *find(char *name); +}; + +// manages audio channels + +struct sourcescheduler +{ + vector sources; + + sourcescheduler(); + + void init(); + void reset(); + source *newsource(int priority, const vec &o); + void releasesource(source *src); +}; + +// audio streaming + +struct oggstream : sourceowner +{ + string name; + bool valid; + + // file stream + OggVorbis_File oggfile; + bool isopen; + vorbis_info *info; + double totalseconds; + static const int BUFSIZE = 1024 * 512; // 512kb buffer + + // OpenAL resources + ALuint bufferids[2]; + source *src; + ALenum format; + + // settings + float volume, gain; + int startmillis, endmillis, startfademillis, endfademillis; + bool looping; + + oggstream(); + ~oggstream(); + + void reset(); + bool open(const char *f); + void onsourcereassign(source *s); + bool stream(ALuint bufid); + bool update(); + bool playing(); + void updategain(); + void setgain(float g); + void setvolume(float v); + void fadein(int startmillis, int fademillis); + void fadeout(int endmillis, int fademillis); + bool playback(bool looping = false); + void seek(double offset); +}; + + +struct soundconfig +{ + sbuffer *buf; + int vol, uses, maxuses; + bool loop; + bool muted; + + soundconfig(sbuffer *b, int vol, int maxuses, bool loop); + void onattach(); + void ondetach(); +}; + + +struct worldobjreference +{ + enum worldobjtype { WR_CAMERA, WR_PHYSENT, WR_ENTITY, WR_STATICPOS }; + int type; + + worldobjreference(int t) : type(t) {} + virtual ~worldobjreference() {} + virtual worldobjreference *clone() const = 0; + virtual const vec ¤tposition() const = 0; + virtual bool nodistance() = 0; + virtual bool operator==(const worldobjreference &other) = 0; + virtual bool operator!=(const worldobjreference &other) { return !(*this==other); } + virtual void attach() {} + virtual void detach() {} +}; + +struct camerareference : worldobjreference +{ + camerareference(); + worldobjreference *clone() const; + const vec ¤tposition() const; + bool nodistance(); + bool operator==(const worldobjreference &other); +}; + +struct physentreference : worldobjreference +{ + struct physent *phys; + + physentreference(physent *ref); + + worldobjreference *clone() const; + const vec ¤tposition() const; + bool nodistance(); + bool operator==(const worldobjreference &other); +}; + +struct entityreference : worldobjreference +{ + struct entity *ent; + + entityreference(entity *ref); + + worldobjreference *clone() const; + const vec ¤tposition() const; + bool nodistance(); + bool operator==(const worldobjreference &other); +}; + +struct staticreference : worldobjreference +{ + vec pos; + + staticreference(const vec &ref); + + worldobjreference *clone() const; + const vec ¤tposition() const; + bool nodistance(); + bool operator==(const worldobjreference &other); +}; + + +struct location : sourceowner +{ + soundconfig *cfg; + source *src; + worldobjreference *ref; + + bool stale; + int playmillis; + + location(int sound, const worldobjreference &r, int priority = SP_NORMAL); + ~location(); + void attachworldobjreference(const worldobjreference &r); + void evaluateworldobjref(); + void onsourcereassign(source *s); + void updatepos(); + void update(); + void play(bool loop = false); + void pitch(float p); + void offset(float secs); + float offset(); + void drop(); +}; + +struct locvector : vector +{ + virtual ~locvector() {} + + location *find(int sound, worldobjreference *ref/* = NULL*/, const vector &soundcollection /* = gamesounds*/); + void delete_(int i); + void replaceworldobjreference(const worldobjreference &oldr, const worldobjreference &newr); + void updatelocations(); + void forcepitch(float pitch); + void deleteworldobjsounds(); +}; + +// audio interface to the engine + +struct audiomanager +{ + bool nosound; + float currentpitch; + ALCdevice *device; + ALCcontext *context; + + bufferhashtable bufferpool; + locvector locations; + //sourcescheduler scheduler; + oggstream *gamemusic; + + audiomanager(); + + location *_playsound(int n, const worldobjreference &r, int priority, float offset = 0.0f, bool loop = false); + void playsound(int n, int priority = SP_NORMAL); + void playsound(int n, struct physent *p, int priority = SP_NORMAL); + void playsound(int n, struct entity *e, int priority = SP_NORMAL); + void playsound(int n, const vec *v, int priority = SP_NORMAL); + void playsoundname(char *s, const vec *loc, int vol); + void sound(int n); + void playsoundc(int n, physent *p = NULL); + void stopsound(); + + void initsound(); + void soundcleanup(); + void mapsoundreset(); + void clearworldsounds(bool fullclean = true); + + void music(char *name, char *millis, char *cmd); + void musicsuggest(int id, int millis, bool rndofs); + void musicfadeout(int id); + void registermusic(char *name); + int addsound(char *name, int vol, int maxuses, bool loop, vector &sounds, bool load); + + void updateplayerfootsteps(struct playerent *p); + location *updateloopsound(int sound, bool active, float vol = 1.0f); + void updateaudio(); + + void detachsounds(struct playerent *owner); + void preloadmapsound(entity &e); + void preloadmapsounds(); + void applymapsoundchanges(); + void writesoundconfig(FILE *f); +}; + +void alclearerr(); +bool alerr(bool msg = true, int line = 0); +#define ALERR alerr(true, __LINE__) + +extern vector gamesounds, mapsounds; +extern ov_callbacks oggcallbacks; +extern int soundvol; +extern int audiodebug; + +extern sourcescheduler scheduler; // FIXME +extern audiomanager audiomgr; + + diff --git a/source/src/soundlocation.cpp b/source/src/soundlocation.cpp new file mode 100644 index 00000000..df3c816e --- /dev/null +++ b/source/src/soundlocation.cpp @@ -0,0 +1,283 @@ +// short living sound occurrence, dies once the sound stops + +#include "pch.h" +#include "cube.h" + +#define DEBUGCOND (audiodebug==1) + +VARP(gainscale, 1, 90, 100); + +location::location(int sound, const worldobjreference &r, int priority) : cfg(NULL), src(NULL), ref(NULL), stale(false), playmillis(0) +{ + vector &sounds = (r.type==worldobjreference::WR_ENTITY ? mapsounds : gamesounds); + if(!sounds.inrange(sound)) + { + conoutf("unregistered sound: %d", sound); + stale = true; + return; + } + + // get sound config + cfg = &sounds[sound]; + cfg->onattach(); + if((r.type==worldobjreference::WR_ENTITY && cfg->maxuses >= 0 && cfg->uses >= cfg->maxuses) || cfg->muted) // check max-use limits + { + stale = true; + return; + } + + // assign buffer + sbuffer *buf = cfg->buf; + if(!buf || !buf->id) + { + stale = true; + return; + } + + // obtain source + src = scheduler.newsource(priority, r.currentposition()); + // apply configuration + if(!src || !src->valid || !src->buffer(cfg->buf->id) || !src->looping(cfg->loop) || !src->gain(cfg->vol/100.0f*((float)gainscale)/100.0f)) + { + stale = true; + return; + } + src->init(this); + + // set position + attachworldobjreference(r); +} + +location::~location() +{ + if(src) scheduler.releasesource(src); + if(cfg) cfg->ondetach(); + if(ref) + { + ref->detach(); + DELETEP(ref); + } +} + +// attach a reference to a world object to get the 3D position from + +void location::attachworldobjreference(const worldobjreference &r) +{ + ASSERT(!stale && src && src->valid); + if(stale) return; + if(ref) + { + ref->detach(); + DELETEP(ref); + } + ref = r.clone(); + evaluateworldobjref(); + ref->attach(); +} + +// enable/disable distance calculations +void location::evaluateworldobjref() +{ + src->sourcerelative(ref->nodistance()); +} + +// marks itself for deletion if source got lost +void location::onsourcereassign(source *s) +{ + if(s==src) + { + stale = true; + src = NULL; + } +} + +void location::updatepos() +{ + ASSERT(!stale && ref); + if(stale) return; + + const vec &pos = ref->currentposition(); + + switch(ref->type) + { + case worldobjreference::WR_CAMERA: break; + case worldobjreference::WR_PHYSENT: + { + if(!ref->nodistance()) src->position(pos); + break; + } + case worldobjreference::WR_ENTITY: + { + entityreference &eref = *(entityreference *)ref; + const float vol = eref.ent->attr4<=0.0f ? 1.0f : eref.ent->attr4/255.0f; + + if(ref->nodistance()) + { + // own distance model for entities/mapsounds: linear & clamping + + const float innerradius = float(eref.ent->attr3); // full gain area / size property + const float outerradius = float(eref.ent->attr2); // fading gain area / radius property + float dist = camera1->o.dist(pos); + + if(dist <= innerradius) src->gain(1.0f*vol); // inside full gain area + else if(dist <= outerradius) // inside fading gain area + { + const float fadeoutdistance = outerradius-innerradius; + const float fadeout = dist-innerradius; + src->gain((1.0f - fadeout/fadeoutdistance)*vol); + } + else src->gain(0.0f); // outside entity + } + else + { + // use openal distance model to make the sound appear from a certain direction (non-ambient) + src->position(pos); + src->gain(vol); + } + break; + } + case worldobjreference::WR_STATICPOS: + { + src->position(pos); + break; + } + } +} + +void location::update() +{ + if(stale) return; + + switch(src->state()) + { + case AL_PLAYING: + updatepos(); + break; + case AL_STOPPED: + case AL_PAUSED: + case AL_INITIAL: + stale = true; + break; + } +} + +void location::play(bool loop) +{ + if(stale) return; + + updatepos(); + if(loop) src->looping(loop); + if(src->play()) playmillis = totalmillis; +} + +void location::pitch(float p) +{ + if(stale) return; + src->pitch(p); +} + +void location::offset(float secs) +{ + ASSERT(!stale); + if(stale) return; + src->secoffset(secs); +} + +float location::offset() +{ + ASSERT(!stale); + if(stale) return 0.0f; + return src->secoffset(); +} + +void location::drop() +{ + src->stop(); + stale = true; // drop from collection on next update cycle +} + + +// location collection + +location *locvector::find(int sound, worldobjreference *ref, const vector &soundcollection /* = gamesounds*/) +{ + if(sound<0 || sound>=soundcollection.length()) return NULL; + loopi(ulen) if(buf[i] && !buf[i]->stale) + { + if(buf[i]->cfg != &soundcollection[sound]) continue; // check if its the same sound + if(ref && *buf[i]->ref!=*ref) continue; // optionally check if its the same reference + return buf[i]; // found + } + return NULL; +} + +void locvector::delete_(int i) +{ + location *loc = buf[i]; + remove(i); + delete loc; +} + +void locvector::replaceworldobjreference(const worldobjreference &oldr, const worldobjreference &newr) +{ + loopv(*this) + { + location *l = buf[i]; + if(!l || !l->ref) continue; + if(*l->ref==oldr) l->attachworldobjreference(newr); + } +} + +// update stuff, remove stale data +void locvector::updatelocations() +{ + // check if camera carrier changed + bool camchanged = false; + static physent *lastcamera = NULL; + if(lastcamera!=camera1) + { + if(lastcamera!=NULL) camchanged = true; + lastcamera = camera1; + } + + // update all locations + loopv(*this) + { + location *l = buf[i]; + if(!l) continue; + + l->update(); + if(l->stale) delete_(i--); + else if(camchanged) l->evaluateworldobjref(); // cam changed, evaluate world reference again + } +} + +// force pitch across all locations +void locvector::forcepitch(float pitch) +{ + loopv(*this) + { + location *l = buf[i]; + if(!l) continue; + if(l->src && l->src->locked) l->src->pitch(pitch); + } +} + +// delete all sounds except world-neutral sounds like GUI/notifacation +void locvector::deleteworldobjsounds() +{ + loopv(*this) + { + location *l = buf[i]; + if(!l) continue; + // world-neutral sounds + if(l->cfg == &gamesounds[S_MENUENTER] || + l->cfg == &gamesounds[S_MENUSELECT] || + l->cfg == &gamesounds[S_CALLVOTE] || + l->cfg == &gamesounds[S_VOTEPASS] || + l->cfg == &gamesounds[S_VOTEFAIL]) continue; + + delete_(i--); + } +}; + diff --git a/source/src/soundscheduler.cpp b/source/src/soundscheduler.cpp new file mode 100644 index 00000000..6074f6d1 --- /dev/null +++ b/source/src/soundscheduler.cpp @@ -0,0 +1,154 @@ +// AC sound scheduler, manages available sound sources. +// It keeps a set of reserved sources for each priority level to avoid running out of sources by floods of low-priority sounds. +// Under load it uses priority and distance information to reassign its resources. + +#include "pch.h" +#include "cube.h" + +#define DEBUGCOND (audiodebug==1) + +extern int soundchannels; + +VARP(soundschedpriorityscore, 0, 100, 1000); +VARP(soundscheddistancescore, 0, 5, 1000); +VARP(soundschedoldbonus, 0, 100, 1000); +VARP(soundschedreserve, 0, 2, 100); + +sourcescheduler::sourcescheduler() +{ +} + +void sourcescheduler::init() +{ + int newchannels = soundchannels - sources.length(); + if(newchannels < 0) + { + loopv(sources) + { + source *src = sources[i]; + if(src->locked) continue; + sources.remove(i--); + delete src; + if(sources.length() <= soundchannels) break; + } + } + else loopi(newchannels) + { + source *src = new source(); + if(src->valid) sources.add(src); + else + { + DELETEP(src); + break; + } + } +} + +void sourcescheduler::reset() +{ + loopv(sources) sources[i]->reset(); + sources.deletecontentsp(); +} + +// returns a free sound source (channel) +// consuming code must call sourcescheduler::releasesource() after use + +source *sourcescheduler::newsource(int priority, const vec &o) +{ + if(!sources.length()) + { + DEBUG("empty source collection"); + return NULL; + } + + source *src = NULL; + + // reserve some sources for sounds of higher priority + int reserved = (SP_HIGHEST-priority)*soundschedreserve; + DEBUGVAR(reserved); + + // search unused source + loopv(sources) + { + if(!sources[i]->locked && reserved--<=0) + { + src = sources[i]; + DEBUGVAR(src); + break; + } + } + + if(!src) + { + DEBUG("no empty source found"); + + // low priority sounds can't replace others + if(SP_LOW==priority) + { + DEBUG("low prio sound aborted"); + return NULL; + } + + // try replacing a used source + // score our sound + const float dist = o.iszero() ? 0.0f : camera1->o.dist(o); + const float score = (priority*soundschedpriorityscore) - (dist*soundscheddistancescore); + + // score other sounds + float worstscore = 0.0f; + source *worstsource = NULL; + + loopv(sources) + { + source *s = sources[i]; + if(s->priority==SP_HIGHEST) continue; // highest priority sounds can't be replaced + + vec otherpos = s->position(); + float otherdist = otherpos.iszero() ? 0.0f : camera1->o.dist(otherpos); + float otherscore = (s->priority*soundschedpriorityscore) - (otherdist*soundscheddistancescore) + soundschedoldbonus; + if(!worstsource || otherscore < worstscore) + { + worstsource = s; + worstscore = otherscore; + } + } + + // pick worst source and replace it + if(worstsource && score>worstscore) + { + src = worstsource; + src->onreassign(); // inform previous owner about the take-over + DEBUG("replaced sound of same prio"); + } + } + + if(!src) + { + DEBUG("sound aborted, no channel takeover possible"); + return NULL; + } + + src->reset(); // default settings + src->lock(); // exclusive lock + src->priority = priority; + return src; +} + +// give source back to the pool +void sourcescheduler::releasesource(source *src) +{ + ASSERT(src); + ASSERT(src->locked); // detect double release + + if(!src) return; + src->unlock(); + + if(sources.length() > soundchannels) + { + sources.removeobj(src); + delete src; + } +} + +sourcescheduler scheduler; // FIXME, move to audiomanager + diff --git a/source/src/weapon.cpp b/source/src/weapon.cpp index 8b6e8841..fb602f9f 100644 --- a/source/src/weapon.cpp +++ b/source/src/weapon.cpp @@ -327,13 +327,13 @@ void hit(int damage, playerent *d, playerent *at, const vec &vel, int gun, bool updatedmgindicator(at->o); damageblend(damage); damageeffect(damage, d); - playsound(S_PAIN6, SP_HIGH); + audiomgr.playsound(S_PAIN6, SP_HIGH); } else { h.dir = ivec(int(vel.x*DNF), int(vel.y*DNF), int(vel.z*DNF)); damageeffect(damage, d); - playsound(S_PAIN1+rnd(5), d); + audiomgr.playsound(S_PAIN1+rnd(5), d); } } } @@ -441,7 +441,7 @@ VARP(gibspeed, 1, 30, 100); void addgib(playerent *d) { if(!d || !gib || !gibttl) return; - playsound(S_GIB, d); + audiomgr.playsound(S_GIB, d); loopi(gibnum) { @@ -555,7 +555,7 @@ void weapon::attacksound() { if(info.sound == S_NULL) return; bool local = (owner == player1); - playsound(info.sound, owner, local ? SP_HIGH : SP_NORMAL); + audiomgr.playsound(info.sound, owner, local ? SP_HIGH : SP_NORMAL); } bool weapon::reload() @@ -570,8 +570,8 @@ bool weapon::reload() ammo -= numbullets; bool local = (player1 == owner); - if(owner->type==ENT_BOT) playsound(info.reload, owner); - else playsoundc(info.reload); + if(owner->type==ENT_BOT) audiomgr.playsound(info.reload, owner); + else audiomgr.playsoundc(info.reload); if(local) addmsg(SV_RELOAD, "ri2", lastmillis, owner->weaponsel->type); return true; } @@ -641,7 +641,7 @@ void weapon::updatetimers() void weapon::onselecting() { updatelastaction(owner); - playsound(S_GUNCHANGE, owner == player1? SP_HIGH : SP_NORMAL); + audiomgr.playsound(S_GUNCHANGE, owner == player1? SP_HIGH : SP_NORMAL); } void weapon::renderhudmodel() { renderhudmodel(owner->lastaction); } @@ -697,7 +697,7 @@ void grenadeent::explode() if(local) addmsg(SV_EXPLODE, "ri3iv", lastmillis, GUN_GRENADE, millis, // fixme hits.length(), hits.length()*sizeof(hitmsg)/sizeof(int), hits.getbuf()); - playsound(S_FEXPLODE, &o); + audiomgr.playsound(S_FEXPLODE, &o); } void grenadeent::splash() @@ -729,7 +729,7 @@ void grenadeent::activate(const vec &from, const vec &to) (int)(from.x*DMF), (int)(from.y*DMF), (int)(from.z*DMF), (int)(to.x*DMF), (int)(to.y*DMF), (int)(to.z*DMF), 0); - playsound(S_GRENADEPULL, SP_HIGH); + audiomgr.playsound(S_GRENADEPULL, SP_HIGH); } } @@ -745,9 +745,9 @@ void grenadeent::_throw(const vec &from, const vec &vel) if(local) { addmsg(SV_THROWNADE, "ri7", int(o.x*DMF), int(o.y*DMF), int(o.z*DMF), int(vel.x*DMF), int(vel.y*DMF), int(vel.z*DMF), lastmillis-millis); - playsound(S_GRENADETHROW, SP_HIGH); + audiomgr.playsound(S_GRENADETHROW, SP_HIGH); } - else playsound(S_GRENADETHROW, owner); + else audiomgr.playsound(S_GRENADETHROW, owner); } void grenadeent::moveoutsidebbox(const vec &direction, playerent *boundingbox) @@ -766,7 +766,7 @@ bool grenadeent::applyphysics() { return nadestate==NS_THROWED; } void grenadeent::oncollision() { - if(distsincebounce>=1.5f) playsound(S_GRENADEBOUNCE1+rnd(2), &o); + if(distsincebounce>=1.5f) audiomgr.playsound(S_GRENADEBOUNCE1+rnd(2), &o); distsincebounce = 0.0f; } @@ -824,7 +824,7 @@ bool grenades::attack(vec &targ) void grenades::attackfx(const vec &from, const vec &to, int millis) // other player's grenades { throwmillis = lastmillis-millis; - if(millis == 0) playsound(S_GRENADEPULL, owner); // activate + if(millis == 0) audiomgr.playsound(S_GRENADEPULL, owner); // activate else if(millis > 0) // throw { grenadeent *g = new grenadeent(owner, millis); @@ -897,7 +897,7 @@ void grenades::renderstats() bool grenades::selectable() { return weapon::selectable() && state != GST_INHAND && mag; } void grenades::reset() { throwmillis = 0; state = GST_NONE; } -void grenades::onselecting() { reset(); playsound(S_GUNCHANGE); } +void grenades::onselecting() { reset(); audiomgr.playsound(S_GUNCHANGE); } void grenades::onownerdies() { reset(); @@ -925,7 +925,7 @@ bool gun::attack(vec &targ) updatelastaction(owner); if(!mag) { - playsoundc(S_NOAMMO); + audiomgr.playsoundc(S_NOAMMO); gunwait += 250; owner->lastattackweapon = NULL; shots = 0; @@ -1187,7 +1187,7 @@ void checkakimbo() a.mag = 0; a.ammo = 0; if(player1->weaponsel->type==GUN_AKIMBO) player1->weaponswitch(&p); - playsoundc(S_AKIMBOOUT); + audiomgr.playsoundc(S_AKIMBOOUT); } } } diff --git a/source/src/world.cpp b/source/src/world.cpp index e457ee29..423be36c 100644 --- a/source/src/world.cpp +++ b/source/src/world.cpp @@ -152,7 +152,7 @@ void entproperty(int prop, int amount) switch(e.type) { case LIGHT: calclight(); break; - case SOUND: preloadmapsound(e); + case SOUND: audiomgr.preloadmapsound(e); } } @@ -245,7 +245,7 @@ entity *newentity(int index, int x, int y, int z, char *what, int v1, int v2, in switch(type) { case LIGHT: calclight(); break; - case SOUND: preloadmapsound(e); break; + case SOUND: audiomgr.preloadmapsound(e); break; } return index<0 ? &ents.last() : &ents[index]; } diff --git a/source/src/worldio.cpp b/source/src/worldio.cpp index a5036053..520dfa1f 100644 --- a/source/src/worldio.cpp +++ b/source/src/worldio.cpp @@ -459,7 +459,7 @@ bool load_world(char *mname) // still supports all map formats that have c2skeepalive(); preload_mapmodels(); c2skeepalive(); - preloadmapsounds(); + audiomgr.preloadmapsounds(); c2skeepalive(); startmap(mname); diff --git a/source/vcpp/cube.suo b/source/vcpp/cube.suo index 2b430dcc42dbab6b8c050b5d0651578d8a98af05..406af7d35561b50c1f103f06d1d6e51bb722990e 100644 GIT binary patch delta 48313 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 diff --git a/source/vcpp/cube.vcproj b/source/vcpp/cube.vcproj index 63b28f11..4249d813 100644 --- a/source/vcpp/cube.vcproj +++ b/source/vcpp/cube.vcproj @@ -1,2254 +1,2278 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +