/*
Copyright (C) 1997-2001 Id Software, Inc.

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

*/
// snd_dma.c -- main control for any streaming sound output device

#include "client.h"
#include "snd_loc.h"

void S_Play(void);
void S_SoundList(void);
void S_Update_();
void S_StopAllSounds(void);

// Internal sound data & structures

// only begin attenuating sound volumes when outside the FULLVOLUME range
#define SOUND_FULLVOLUME 80

#define SOUND_LOOPATTENUATE 0.003

int s_registration_sequence;

channel_t channels[MAX_CHANNELS];

bool snd_initialized = false;
int sound_started = 0;

dma_t dma;

vec3_t listener_origin;
vec3_t listener_forward;
vec3_t listener_right;
vec3_t listener_up;

bool s_registering;

int soundtime;   // sample PAIRS
int paintedtime; // sample PAIRS

// during registration it is possible to have more sounds
// than could actually be referenced during gameplay,
// because we don't want to free anything until we are
// sure we won't need it.
#define MAX_SFX (MAX_SOUNDS * 2)
sfx_t known_sfx[MAX_SFX];
int num_sfx;

#define MAX_PLAYSOUNDS 128
playsound_t s_playsounds[MAX_PLAYSOUNDS];
playsound_t s_freeplays;
playsound_t s_pendingplays;

int s_beginofs;

cvar_t *s_volume;
cvar_t *s_testsound;
cvar_t *s_loadas8bit;
cvar_t *s_khz;
cvar_t *s_show;
cvar_t *s_mixahead;
cvar_t *s_primary;

int s_rawend;
portable_samplepair_t s_rawsamples[MAX_RAW_SAMPLES];

// User-setable variables

void S_SoundInfo_f(void) {
  if(!sound_started) {
    Com_Printf("sound system not started\n");
    return;
  }

  Com_Printf("%5d stereo\n", dma.channels - 1);
  Com_Printf("%5d samples\n", dma.samples);
  Com_Printf("%5d samplepos\n", dma.samplepos);
  Com_Printf("%5d samplebits\n", dma.samplebits);
  Com_Printf("%5d submission_chunk\n", dma.submission_chunk);
  Com_Printf("%5d speed\n", dma.speed);
  Com_Printf("0x%x dma buffer\n", dma.buffer);
}

/*
================
S_Init
================
*/
void S_Init(void) {
  cvar_t *cv;

  Com_Printf("\n------- sound initialization -------\n");

  cv = Cvar_Get("s_initsound", "1", 0);
  if(!cv->value)
    Com_Printf("not initializing.\n");
  else {
    s_volume = Cvar_Get("s_volume", "0.7", CVAR_ARCHIVE);
    s_khz = Cvar_Get("s_khz", "11", CVAR_ARCHIVE);
    s_loadas8bit = Cvar_Get("s_loadas8bit", "1", CVAR_ARCHIVE);
    s_mixahead = Cvar_Get("s_mixahead", "0.2", CVAR_ARCHIVE);
    s_show = Cvar_Get("s_show", "0", 0);
    s_testsound = Cvar_Get("s_testsound", "0", 0);
    s_primary = Cvar_Get("s_primary", "0", CVAR_ARCHIVE); // win32 specific

    Cmd_AddCommand("play", S_Play);
    Cmd_AddCommand("stopsound", S_StopAllSounds);
    Cmd_AddCommand("soundlist", S_SoundList);
    Cmd_AddCommand("soundinfo", S_SoundInfo_f);

    if(!SNDDMA_Init())
      return;

    S_InitScaletable();

    sound_started = 1;
    num_sfx = 0;

    soundtime = 0;
    paintedtime = 0;

    Com_Printf("sound sampling rate: %i\n", dma.speed);

    S_StopAllSounds();
  }

  Com_Printf("------------------------------------\n");
}

// Shutdown sound engine

void S_Shutdown(void) {
  int i;
  sfx_t *sfx;

  if(!sound_started)
    return;

  SNDDMA_Shutdown();

  sound_started = 0;

  Cmd_RemoveCommand("play");
  Cmd_RemoveCommand("stopsound");
  Cmd_RemoveCommand("soundlist");
  Cmd_RemoveCommand("soundinfo");

  // free all sounds
  for(i = 0, sfx = known_sfx; i < num_sfx; i++, sfx++) {
    if(!sfx->name[0])
      continue;
    if(sfx->cache)
      Z_Free(sfx->cache);
    memset(sfx, 0, sizeof(*sfx));
  }

  num_sfx = 0;
}

// Load a sound

/*
==================
S_FindName

==================
*/
sfx_t *S_FindName(char *name, bool create) {
  int i;
  sfx_t *sfx;

  if(!name)
    Com_Error(ERR_FATAL, "S_FindName: NULL\n");
  if(!name[0])
    Com_Error(ERR_FATAL, "S_FindName: empty name\n");

  if(strlen(name) >= MAX_QPATH)
    Com_Error(ERR_FATAL, "Sound name too long: %s", name);

  // see if already loaded
  for(i = 0; i < num_sfx; i++)
    if(!strcmp(known_sfx[i].name, name)) {
      return &known_sfx[i];
    }

  if(!create)
    return NULL;

  // find a free sfx
  for(i = 0; i < num_sfx; i++)
    if(!known_sfx[i].name[0])
      //			registration_sequence < s_registration_sequence)
      break;

  if(i == num_sfx) {
    if(num_sfx == MAX_SFX)
      Com_Error(ERR_FATAL, "S_FindName: out of sfx_t");
    num_sfx++;
  }

  sfx = &known_sfx[i];
  memset(sfx, 0, sizeof(*sfx));
  strcpy(sfx->name, name);
  sfx->registration_sequence = s_registration_sequence;

  return sfx;
}

/*
==================
S_AliasName

==================
*/
sfx_t *S_AliasName(char *aliasname, char *truename) {
  sfx_t *sfx;
  char *s;
  int i;

  s = Z_Malloc(MAX_QPATH);
  strcpy(s, truename);

  // find a free sfx
  for(i = 0; i < num_sfx; i++)
    if(!known_sfx[i].name[0])
      break;

  if(i == num_sfx) {
    if(num_sfx == MAX_SFX)
      Com_Error(ERR_FATAL, "S_FindName: out of sfx_t");
    num_sfx++;
  }

  sfx = &known_sfx[i];
  memset(sfx, 0, sizeof(*sfx));
  strcpy(sfx->name, aliasname);
  sfx->registration_sequence = s_registration_sequence;
  sfx->truename = s;

  return sfx;
}

/*
=====================
S_BeginRegistration

=====================
*/
void S_BeginRegistration(void) {
  s_registration_sequence++;
  s_registering = true;
}

/*
==================
S_RegisterSound

==================
*/
sfx_t *S_RegisterSound(char *name) {
  sfx_t *sfx;

  if(!sound_started)
    return NULL;

  sfx = S_FindName(name, true);
  sfx->registration_sequence = s_registration_sequence;

  if(!s_registering)
    S_LoadSound(sfx);

  return sfx;
}

/*
=====================
S_EndRegistration

=====================
*/
void S_EndRegistration(void) {
  int i;
  sfx_t *sfx;
  int size;

  // free any sounds not from this registration sequence
  for(i = 0, sfx = known_sfx; i < num_sfx; i++, sfx++) {
    if(!sfx->name[0])
      continue;
    if(sfx->registration_sequence != s_registration_sequence) { // don't need this sound
      if(sfx->cache)                                            // it is possible to have a leftover
        Z_Free(sfx->cache);                                     // from a server that didn't finish loading
      memset(sfx, 0, sizeof(*sfx));
    } else { // make sure it is paged in
      if(sfx->cache) {
        size = sfx->cache->length * sfx->cache->width;
        Com_PageInMemory((byte *)sfx->cache, size);
      }
    }
  }

  // load everything in
  for(i = 0, sfx = known_sfx; i < num_sfx; i++, sfx++) {
    if(!sfx->name[0])
      continue;
    S_LoadSound(sfx);
  }

  s_registering = false;
}

//=============================================================================

/*
=================
S_PickChannel
=================
*/
channel_t *S_PickChannel(int entnum, int entchannel) {
  int ch_idx;
  int first_to_die;
  int life_left;
  channel_t *ch;

  if(entchannel < 0)
    Com_Error(ERR_DROP, "S_PickChannel: entchannel<0");

  // Check for replacement sound, or find the best one to replace
  first_to_die = -1;
  life_left = 0x7fffffff;
  for(ch_idx = 0; ch_idx < MAX_CHANNELS; ch_idx++) {
    if(entchannel != 0 // channel 0 never overrides
       && channels[ch_idx].entnum == entnum &&
       channels[ch_idx].entchannel == entchannel) { // always override sound from same entity
      first_to_die = ch_idx;
      break;
    }

    // don't let monster sounds override player sounds
    if(channels[ch_idx].entnum == cl.playernum + 1 && entnum != cl.playernum + 1 && channels[ch_idx].sfx)
      continue;

    if(channels[ch_idx].end - paintedtime < life_left) {
      life_left = channels[ch_idx].end - paintedtime;
      first_to_die = ch_idx;
    }
  }

  if(first_to_die == -1)
    return NULL;

  ch = &channels[first_to_die];
  memset(ch, 0, sizeof(*ch));

  return ch;
}

/*
=================
S_SpatializeOrigin

Used for spatializing channels and autosounds
=================
*/
void S_SpatializeOrigin(vec3_t origin, float master_vol, float dist_mult, int *left_vol, int *right_vol) {
  vec_t dot;
  vec_t dist;
  vec_t lscale, rscale, scale;
  vec3_t source_vec;

  if(cls.state != ca_active) {
    *left_vol = *right_vol = 255;
    return;
  }

  // calculate stereo seperation and distance attenuation
  VectorSubtract(origin, listener_origin, source_vec);

  dist = VectorNormalize(source_vec);
  dist -= SOUND_FULLVOLUME;
  if(dist < 0)
    dist = 0;        // close enough to be at full volume
  dist *= dist_mult; // different attenuation levels

  dot = DotProduct(listener_right, source_vec);

  if(dma.channels == 1 || !dist_mult) { // no attenuation = no spatialization
    rscale = 1.0;
    lscale = 1.0;
  } else {
    rscale = 0.5 * (1.0 + dot);
    lscale = 0.5 * (1.0 - dot);
  }

  // add in distance effect
  scale = (1.0 - dist) * rscale;
  *right_vol = (int)(master_vol * scale);
  if(*right_vol < 0)
    *right_vol = 0;

  scale = (1.0 - dist) * lscale;
  *left_vol = (int)(master_vol * scale);
  if(*left_vol < 0)
    *left_vol = 0;
}

/*
=================
S_Spatialize
=================
*/
void S_Spatialize(channel_t *ch) {
  vec3_t origin;

  // anything coming from the view entity will always be full volume
  if(ch->entnum == cl.playernum + 1) {
    ch->leftvol = ch->master_vol;
    ch->rightvol = ch->master_vol;
    return;
  }

  if(ch->fixed_origin) {
    VectorCopy(ch->origin, origin);
  } else
    CL_GetEntitySoundOrigin(ch->entnum, origin);

  S_SpatializeOrigin(origin, ch->master_vol, ch->dist_mult, &ch->leftvol, &ch->rightvol);
}

/*
=================
S_AllocPlaysound
=================
*/
playsound_t *S_AllocPlaysound(void) {
  playsound_t *ps;

  ps = s_freeplays.next;
  if(ps == &s_freeplays)
    return NULL; // no free playsounds

  // unlink from freelist
  ps->prev->next = ps->next;
  ps->next->prev = ps->prev;

  return ps;
}

/*
=================
S_FreePlaysound
=================
*/
void S_FreePlaysound(playsound_t *ps) {
  // unlink from channel
  ps->prev->next = ps->next;
  ps->next->prev = ps->prev;

  // add to free list
  ps->next = s_freeplays.next;
  s_freeplays.next->prev = ps;
  ps->prev = &s_freeplays;
  s_freeplays.next = ps;
}

/*
===============
S_IssuePlaysound

Take the next playsound and begin it on the channel
This is never called directly by S_Play*, but only
by the update loop.
===============
*/
void S_IssuePlaysound(playsound_t *ps) {
  channel_t *ch;
  sfxcache_t *sc;

  if(s_show->value)
    Com_Printf("Issue %i\n", ps->begin);
  // pick a channel to play on
  ch = S_PickChannel(ps->entnum, ps->entchannel);
  if(!ch) {
    S_FreePlaysound(ps);
    return;
  }

  // spatialize
  if(ps->attenuation == ATTN_STATIC)
    ch->dist_mult = ps->attenuation * 0.001;
  else
    ch->dist_mult = ps->attenuation * 0.0005;
  ch->master_vol = ps->volume;
  ch->entnum = ps->entnum;
  ch->entchannel = ps->entchannel;
  ch->sfx = ps->sfx;
  VectorCopy(ps->origin, ch->origin);
  ch->fixed_origin = ps->fixed_origin;

  S_Spatialize(ch);

  ch->pos = 0;
  sc = S_LoadSound(ch->sfx);
  ch->end = paintedtime + sc->length;

  // free the playsound
  S_FreePlaysound(ps);
}

struct sfx_s *S_RegisterSexedSound(entity_state_t *ent, char *base) {
  int n;
  char *p;
  struct sfx_s *sfx;
  FILE *f;
  char model[MAX_QPATH];
  char sexedFilename[MAX_QPATH];
  char maleFilename[MAX_QPATH];

  // determine what model the client is using
  model[0] = 0;
  n = CS_PLAYERSKINS + ent->number - 1;
  if(cl.configstrings[n][0]) {
    p = strchr(cl.configstrings[n], '\\');
    if(p) {
      p += 1;
      strcpy(model, p);
      p = strchr(model, '/');
      if(p)
        *p = 0;
    }
  }
  // if we can't figure it out, they're male
  if(!model[0])
    strcpy(model, "male");

  // see if we already know of the model specific sound
  Com_sprintf(sexedFilename, sizeof(sexedFilename), "#players/%s/%s", model, base + 1);
  sfx = S_FindName(sexedFilename, false);

  if(!sfx) {
    // no, so see if it exists
    FS_FOpenFile(&sexedFilename[1], &f);
    if(f) {
      // yes, close the file and register it
      FS_FCloseFile(f);
      sfx = S_RegisterSound(sexedFilename);
    } else {
      // no, revert to the male sound in the pak0.pak
      Com_sprintf(maleFilename, sizeof(maleFilename), "player/%s/%s", "male", base + 1);
      sfx = S_AliasName(sexedFilename, maleFilename);
    }
  }

  return sfx;
}

// Start a sound effect

/*
====================
S_StartSound

Validates the parms and ques the sound up
if pos is NULL, the sound will be dynamically sourced from the entity
Entchannel 0 will never override a playing sound
====================
*/
void S_StartSound(vec3_t origin, int entnum, int entchannel, sfx_t *sfx, float fvol, float attenuation, float timeofs) {
  sfxcache_t *sc;
  int vol;
  playsound_t *ps, *sort;
  int start;

  if(!sound_started)
    return;

  if(!sfx)
    return;

  if(sfx->name[0] == '*')
    sfx = S_RegisterSexedSound(&cl_entities[entnum].current, sfx->name);

  // make sure the sound is loaded
  sc = S_LoadSound(sfx);
  if(!sc)
    return; // couldn't load the sound's data

  vol = fvol * 255;

  // make the playsound_t
  ps = S_AllocPlaysound();
  if(!ps)
    return;

  if(origin) {
    VectorCopy(origin, ps->origin);
    ps->fixed_origin = true;
  } else
    ps->fixed_origin = false;

  ps->entnum = entnum;
  ps->entchannel = entchannel;
  ps->attenuation = attenuation;
  ps->volume = vol;
  ps->sfx = sfx;

  // drift s_beginofs
  start = cl.frame.servertime * 0.001 * dma.speed + s_beginofs;
  if(start < paintedtime) {
    start = paintedtime;
    s_beginofs = start - (cl.frame.servertime * 0.001 * dma.speed);
  } else if(start > paintedtime + 0.3 * dma.speed) {
    start = paintedtime + 0.1 * dma.speed;
    s_beginofs = start - (cl.frame.servertime * 0.001 * dma.speed);
  } else {
    s_beginofs -= 10;
  }

  if(!timeofs)
    ps->begin = paintedtime;
  else
    ps->begin = start + timeofs * dma.speed;

  // sort into the pending sound list
  for(sort = s_pendingplays.next; sort != &s_pendingplays && sort->begin < ps->begin; sort = sort->next)
    ;

  ps->next = sort;
  ps->prev = sort->prev;

  ps->next->prev = ps;
  ps->prev->next = ps;
}

/*
==================
S_StartLocalSound
==================
*/
void S_StartLocalSound(char *sound) {
  sfx_t *sfx;

  if(!sound_started)
    return;

  sfx = S_RegisterSound(sound);
  if(!sfx) {
    Com_Printf("S_StartLocalSound: can't cache %s\n", sound);
    return;
  }
  S_StartSound(NULL, cl.playernum + 1, 0, sfx, 1, 1, 0);
}

/*
==================
S_ClearBuffer
==================
*/
void S_ClearBuffer(void) {
  int clear;

  if(!sound_started)
    return;

  s_rawend = 0;

  if(dma.samplebits == 8)
    clear = 0x80;
  else
    clear = 0;

  SNDDMA_BeginPainting();
  if(dma.buffer)
    memset(dma.buffer, clear, dma.samples * dma.samplebits / 8);
  SNDDMA_Submit();
}

/*
==================
S_StopAllSounds
==================
*/
void S_StopAllSounds(void) {
  int i;

  if(!sound_started)
    return;

  // clear all the playsounds
  memset(s_playsounds, 0, sizeof(s_playsounds));
  s_freeplays.next = s_freeplays.prev = &s_freeplays;
  s_pendingplays.next = s_pendingplays.prev = &s_pendingplays;

  for(i = 0; i < MAX_PLAYSOUNDS; i++) {
    s_playsounds[i].prev = &s_freeplays;
    s_playsounds[i].next = s_freeplays.next;
    s_playsounds[i].prev->next = &s_playsounds[i];
    s_playsounds[i].next->prev = &s_playsounds[i];
  }

  // clear all the channels
  memset(channels, 0, sizeof(channels));

  S_ClearBuffer();
}

/*
==================
S_AddLoopSounds

Entities with a ->sound field will generated looped sounds
that are automatically started, stopped, and merged together
as the entities are sent to the client
==================
*/
void S_AddLoopSounds(void) {
  int i, j;
  int sounds[MAX_EDICTS];
  int left, right, left_total, right_total;
  channel_t *ch;
  sfx_t *sfx;
  sfxcache_t *sc;
  int num;
  entity_state_t *ent;

  if(cl_paused->value)
    return;

  if(cls.state != ca_active)
    return;

  if(!cl.sound_prepped)
    return;

  for(i = 0; i < cl.frame.num_entities; i++) {
    num = (cl.frame.parse_entities + i) & (MAX_PARSE_ENTITIES - 1);
    ent = &cl_parse_entities[num];
    sounds[i] = ent->sound;
  }

  for(i = 0; i < cl.frame.num_entities; i++) {
    if(!sounds[i])
      continue;

    sfx = cl.sound_precache[sounds[i]];
    if(!sfx)
      continue; // bad sound effect
    sc = sfx->cache;
    if(!sc)
      continue;

    num = (cl.frame.parse_entities + i) & (MAX_PARSE_ENTITIES - 1);
    ent = &cl_parse_entities[num];

    // find the total contribution of all sounds of this type
    S_SpatializeOrigin(ent->origin, 255.0, SOUND_LOOPATTENUATE, &left_total, &right_total);
    for(j = i + 1; j < cl.frame.num_entities; j++) {
      if(sounds[j] != sounds[i])
        continue;
      sounds[j] = 0; // don't check this again later

      num = (cl.frame.parse_entities + j) & (MAX_PARSE_ENTITIES - 1);
      ent = &cl_parse_entities[num];

      S_SpatializeOrigin(ent->origin, 255.0, SOUND_LOOPATTENUATE, &left, &right);
      left_total += left;
      right_total += right;
    }

    if(left_total == 0 && right_total == 0)
      continue; // not audible

    // allocate a channel
    ch = S_PickChannel(0, 0);
    if(!ch)
      return;

    if(left_total > 255)
      left_total = 255;
    if(right_total > 255)
      right_total = 255;
    ch->leftvol = left_total;
    ch->rightvol = right_total;
    ch->autosound = true; // remove next frame
    ch->sfx = sfx;
    ch->pos = paintedtime % sc->length;
    ch->end = paintedtime + sc->length - ch->pos;
  }
}

//=============================================================================

/*
============
S_RawSamples

Cinematic streaming and voice over network
============
*/
void S_RawSamples(int samples, int rate, int width, int channels, byte *data) {
  int i;
  int src, dst;
  float scale;

  if(!sound_started)
    return;

  if(s_rawend < paintedtime)
    s_rawend = paintedtime;
  scale = (float)rate / dma.speed;

  // Com_Printf ("%i < %i < %i\n", soundtime, paintedtime, s_rawend);
  if(channels == 2 && width == 2) {
    if(scale == 1.0) { // optimized case
      for(i = 0; i < samples; i++) {
        dst = s_rawend & (MAX_RAW_SAMPLES - 1);
        s_rawend++;
        s_rawsamples[dst].left = LittleShort(((short *)data)[i * 2]) << 8;
        s_rawsamples[dst].right = LittleShort(((short *)data)[i * 2 + 1]) << 8;
      }
    } else {
      for(i = 0;; i++) {
        src = i * scale;
        if(src >= samples)
          break;
        dst = s_rawend & (MAX_RAW_SAMPLES - 1);
        s_rawend++;
        s_rawsamples[dst].left = LittleShort(((short *)data)[src * 2]) << 8;
        s_rawsamples[dst].right = LittleShort(((short *)data)[src * 2 + 1]) << 8;
      }
    }
  } else if(channels == 1 && width == 2) {
    for(i = 0;; i++) {
      src = i * scale;
      if(src >= samples)
        break;
      dst = s_rawend & (MAX_RAW_SAMPLES - 1);
      s_rawend++;
      s_rawsamples[dst].left = LittleShort(((short *)data)[src]) << 8;
      s_rawsamples[dst].right = LittleShort(((short *)data)[src]) << 8;
    }
  } else if(channels == 2 && width == 1) {
    for(i = 0;; i++) {
      src = i * scale;
      if(src >= samples)
        break;
      dst = s_rawend & (MAX_RAW_SAMPLES - 1);
      s_rawend++;
      s_rawsamples[dst].left = ((char *)data)[src * 2] << 16;
      s_rawsamples[dst].right = ((char *)data)[src * 2 + 1] << 16;
    }
  } else if(channels == 1 && width == 1) {
    for(i = 0;; i++) {
      src = i * scale;
      if(src >= samples)
        break;
      dst = s_rawend & (MAX_RAW_SAMPLES - 1);
      s_rawend++;
      s_rawsamples[dst].left = (((byte *)data)[src] - 128) << 16;
      s_rawsamples[dst].right = (((byte *)data)[src] - 128) << 16;
    }
  }
}

//=============================================================================

/*
============
S_Update

Called once each time through the main loop
============
*/
void S_Update(vec3_t origin, vec3_t forward, vec3_t right, vec3_t up) {
  int i;
  int total;
  channel_t *ch;

  if(!sound_started)
    return;

  // if the laoding plaque is up, clear everything
  // out to make sure we aren't looping a dirty
  // dma buffer while loading
  if(cls.disable_screen) {
    S_ClearBuffer();
    return;
  }

  // rebuild scale tables if volume is modified
  if(s_volume->modified)
    S_InitScaletable();

  VectorCopy(origin, listener_origin);
  VectorCopy(forward, listener_forward);
  VectorCopy(right, listener_right);
  VectorCopy(up, listener_up);

  // update spatialization for dynamic sounds
  ch = channels;
  for(i = 0; i < MAX_CHANNELS; i++, ch++) {
    if(!ch->sfx)
      continue;
    if(ch->autosound) { // autosounds are regenerated fresh each frame
      memset(ch, 0, sizeof(*ch));
      continue;
    }
    S_Spatialize(ch); // respatialize channel
    if(!ch->leftvol && !ch->rightvol) {
      memset(ch, 0, sizeof(*ch));
      continue;
    }
  }

  // add loopsounds
  S_AddLoopSounds();

  //
  // debugging output
  //
  if(s_show->value) {
    total = 0;
    ch = channels;
    for(i = 0; i < MAX_CHANNELS; i++, ch++)
      if(ch->sfx && (ch->leftvol || ch->rightvol)) {
        Com_Printf("%3i %3i %s\n", ch->leftvol, ch->rightvol, ch->sfx->name);
        total++;
      }

    Com_Printf("----(%i)---- painted: %i\n", total, paintedtime);
  }

  // mix some sound
  S_Update_();
}

void GetSoundtime(void) {
  int samplepos;
  static int buffers;
  static int oldsamplepos;
  int fullsamples;

  fullsamples = dma.samples / dma.channels;

  // it is possible to miscount buffers if it has wrapped twice between
  // calls to S_Update.  Oh well.
  samplepos = SNDDMA_GetDMAPos();

  if(samplepos < oldsamplepos) {
    buffers++; // buffer wrapped

    if(paintedtime > 0x40000000) { // time to chop things off to avoid 32 bit limits
      buffers = 0;
      paintedtime = fullsamples;
      S_StopAllSounds();
    }
  }
  oldsamplepos = samplepos;

  soundtime = buffers * fullsamples + samplepos / dma.channels;
}

void S_Update_(void) {
  unsigned endtime;
  int samps;

  if(!sound_started)
    return;

  SNDDMA_BeginPainting();

  if(!dma.buffer)
    return;

  // Updates DMA time
  GetSoundtime();

  // check to make sure that we haven't overshot
  if(paintedtime < soundtime) {
    Com_DPrintf("S_Update_ : overflow\n");
    paintedtime = soundtime;
  }

  // mix ahead of current position
  endtime = soundtime + s_mixahead->value * dma.speed;
  // endtime = (soundtime + 4096) & ~4095;

  // mix to an even submission block size
  endtime = (endtime + dma.submission_chunk - 1) & ~(dma.submission_chunk - 1);
  samps = dma.samples >> (dma.channels - 1);
  if(endtime - soundtime > samps)
    endtime = soundtime + samps;

  S_PaintChannels(endtime);

  SNDDMA_Submit();
}

/*
===============================================================================

console functions

===============================================================================
*/

void S_Play(void) {
  int i;
  char name[256];
  sfx_t *sfx;

  i = 1;
  while(i < Cmd_Argc()) {
    if(!strrchr(Cmd_Argv(i), '.')) {
      strcpy(name, Cmd_Argv(i));
      strcat(name, ".wav");
    } else
      strcpy(name, Cmd_Argv(i));
    sfx = S_RegisterSound(name);
    S_StartSound(NULL, cl.playernum + 1, 0, sfx, 1.0, 1.0, 0);
    i++;
  }
}

void S_SoundList(void) {
  int i;
  sfx_t *sfx;
  sfxcache_t *sc;
  int size, total;

  total = 0;
  for(sfx = known_sfx, i = 0; i < num_sfx; i++, sfx++) {
    if(!sfx->registration_sequence)
      continue;
    sc = sfx->cache;
    if(sc) {
      size = sc->length * sc->width * (sc->stereo + 1);
      total += size;
      if(sc->loopstart >= 0)
        Com_Printf("L");
      else
        Com_Printf(" ");
      Com_Printf("(%2db) %6i : %s\n", sc->width * 8, size, sfx->name);
    } else {
      if(sfx->name[0] == '*')
        Com_Printf("  placeholder : %s\n", sfx->name);
      else
        Com_Printf("  not loaded  : %s\n", sfx->name);
    }
  }
  Com_Printf("Total resident: %i\n", total);
}