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 2b430dcc..406af7d3 100644 Binary files a/source/vcpp/cube.suo and b/source/vcpp/cube.suo differ 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 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +