/* Copyright 2022-2026 Kurt Nienhaus
 *
 * This file is part of VgaGames.
 * VgaGames 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.
 * VgaGames 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 VgaGames.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include "audio.h"

void init_audio(void);
void dest_audio(void);

int vg4_audio_load_nolist(const char *, int, int);
int vg4_audio_clone(int);

static VG_BOOL audio_open(int, VG_BOOL);
static VG_BOOL audio_is_open(void);
static void audio_close(void);
static int audio_load(const char *, int, int);
static int audio_load_doit(const char *, int, int, VG_BOOL);
static void audio_unload(int);
static void audio_unload_group(int);
static const char * audio_getname(int);
static void audio_play(int, VG_BOOL, VG_BOOL);
static void audio_play_after(int, VG_BOOL, int);
static VG_BOOL audio_is_playing(int, VG_BOOL *);
static void audio_stop(int, VG_BOOL);
static void audio_stop_group(int, VG_BOOL);
static void audio_pause(int, VG_BOOL);
static void audio_pause_group(int, VG_BOOL);
static int audio_volume(int, int);
static VG_BOOL audio_mute(VG_BOOL);
static VG_BOOL audio_is_mute(void);
static VG_BOOL audio_suspend(VG_BOOL);
static void audio_dump(FILE *);

static int cmp_wvdata(const void *, const void *);
static struct wv_data * sortfind_wvdata(struct wv_data *, const char *);
static struct wv_data * adddel_wvdata(struct wv_data *, const char *, char *, int);
static double audiocr(int *, int *, VG_BOOL);
static void audiomix(int, unsigned char *, int, double);
static void audiocbf(void *, unsigned char *, int);


/* set functions */
void
init_audio(void)
{
  vg4data.lists.audio.s = SML3_calloc(1, sizeof(*vg4data.lists.audio.s));
  vg4data.lists.audio.s->cbf = audiocbf;
  vg4->audio->open = audio_open;
  vg4->audio->is_open = audio_is_open;
  vg4->audio->close = audio_close;
  vg4->audio->load = audio_load;
  vg4->audio->unload = audio_unload;
  vg4->audio->unload_group = audio_unload_group;
  vg4->audio->getname = audio_getname;
  vg4->audio->play = audio_play;
  vg4->audio->play_after = audio_play_after;
  vg4->audio->is_playing = audio_is_playing;
  vg4->audio->stop = audio_stop;
  vg4->audio->stop_group = audio_stop_group;
  vg4->audio->pause = audio_pause;
  vg4->audio->pause_group = audio_pause_group;
  vg4->audio->volume = audio_volume;
  vg4->audio->mute = audio_mute;
  vg4->audio->is_mute = audio_is_mute;
  vg4->audio->suspend = audio_suspend;
  vg4->audio->dump = audio_dump;
} /* Ende init_audio */

void
dest_audio(void)
{
  vg4->audio->close();
  free(vg4data.lists.audio.s);
  vg4data.lists.audio.s = NULL;
} /* Ende dest_audio */


/* vg4_audio_load_nolist: like audio_load(), but with hidden-inserting into list */
int
vg4_audio_load_nolist(const char *filename, int voladj, int vgidx)
{
  return audio_load_doit(filename, voladj, vgidx, VG_FALSE);
} /* Ende vg4_audio_load_nolist */


/* vg4_audio_clone:
 * clone audio
 * @param audc   audio-descriptor to clone
 * @return  audio-descriptor (positive integer) or 0 = not cloned
 */
int
vg4_audio_clone(int audc)
{
  int audcc;
  struct vgi_audio *sptr;

  audc--;
  if (audc < 0 || audc >= MAX_AUDIO) { return 0; }

  sptr = vg4data.lists.audio.s;

  if (!sptr->d[audc].isext) {
    audcc = vg4->audio->load(sptr->d[audc].u.i.key, sptr->d[audc].voladj, sptr->d[audc].vgidx + 1);
  } else {
    audcc = vg4->audio->load(sptr->d[audc].u.e.key, sptr->d[audc].voladj, sptr->d[audc].vgidx + 1);
  }

  return audcc;
} /* Ende vg4_audio_clone */


/* audio_open:
 * open audio
 * @param freq      one of VG_AUDIO_FREQS
 * @param stereo    VG_FALSE = mono or VG_TRUE = stereo
 * @return  VG_TRUE = OK or VG_FALSE = error
 */
static VG_BOOL
audio_open(int freq, VG_BOOL stereo)
{
  /* open with VG_AUDIO_BITSIZE_16 because of external programs */
  return vg4data.iolib.f.audio_open(freq, stereo, VG_AUDIO_BITSIZE_16);
} /* Ende audio_open */


/* audio_is_open:
 * return whether audio has been opened
 * @return  VG_TRUE = open, VG_FALSE = not open
 */
static VG_BOOL
audio_is_open(void)
{
  return vg4data.iolib.f.audio_is_open();
} /* Ende audio_is_open */


/* audio_close:
 * close audio
 */
static void
audio_close(void)
{
  int audidx;
  struct vgi_audio *sptr;

  if (!vg4data.iolib.f.audio_is_open()) { return; }
  vg4data.iolib.f.audio_close();

  sptr = vg4data.lists.audio.s;

  for (audidx = 0; audidx < MAX_AUDIO; audidx++) {
    if (sptr->d[audidx].isext) {
      if (sptr->d[audidx].u.e.ffp != NULL) { pclose(sptr->d[audidx].u.e.ffp); }
      if (sptr->d[audidx].u.e.file[0] != NULL) { free(sptr->d[audidx].u.e.file[0]); }
      if (sptr->d[audidx].u.e.file[1] != NULL) { free(sptr->d[audidx].u.e.file[1]); }
      if (sptr->d[audidx].u.e.key != NULL) { free(sptr->d[audidx].u.e.key); }
    }
  }
  if (sptr->mixbuf != NULL) { free(sptr->mixbuf); }
  if (sptr->ebuf != NULL) { free(sptr->ebuf); }

  for (audidx = 0; audidx < MAX_AUDIO; audidx++) {
    if (sptr->wvdata[audidx].key != NULL) { free(sptr->wvdata[audidx].key); }
    if (sptr->wvdata[audidx].data != NULL) { free(sptr->wvdata[audidx].data); }
  }

  memset(sptr, 0, sizeof(*sptr));
} /* Ende audio_close */


/* compare function for qsort() and bsearch() */
static int
cmp_wvdata(const void *v1, const void *v2)
{
  struct wv_data *wd1 = (struct wv_data *)v1;
  struct wv_data *wd2 = (struct wv_data *)v2;
  char *key1, *key2;
  int erg;

  if (wd1 == NULL || wd2 == NULL) { return 0; }

  key1 = wd1->key;
  if (key1 == NULL) { key1 = ""; }
  key2 = wd2->key;
  if (key2 == NULL) { key2 = ""; }

  erg = strcmp(key1, key2);
  if (erg < 0) { return -1; }
  if (erg > 0) { return 1; }
  return 0;
} /* Ende cmp_wvdata */


/* sort resp. search WAVE-data, return struct-element or NULL */
static struct wv_data *
sortfind_wvdata(struct wv_data *wvdata, const char *key)
{
  struct wv_data wvd, *wvdp;

  if (wvdata == NULL) { return NULL; }

  if (key == NULL || *key == '\0') {
    qsort(wvdata, MAX_AUDIO, sizeof(*wvdata), cmp_wvdata);
    return NULL;
  }

  wvd.key = (char *)key;
  wvdp = bsearch(&wvd, wvdata, MAX_AUDIO, sizeof(*wvdata), cmp_wvdata);

  return wvdp;
} /* Ende sortfind_wvdata */


/* add resp. remove WAVE-data, return (new) struct-element or NULL */
static struct wv_data *
adddel_wvdata(struct wv_data *wvdata, const char *key, char *data, int dlen)
{
  struct wv_data *wvdp;

  if (wvdata == NULL || key == NULL || *key == '\0') { outerr("parameter NULL"); return NULL; }

  wvdp = sortfind_wvdata(wvdata, key);

  if (data == NULL || dlen <= 0) {  /* remove */
    if (wvdp != NULL) {
      if (--wvdp->anzref == 0) {
        free(wvdp->key); wvdp->key = NULL;
        free(wvdp->data); wvdp->data = NULL;
        wvdp->dsize = 0;
      }
    }

  } else {  /* add */
    if (wvdp == NULL) {
      int audidx;
      for (audidx = 0; audidx < MAX_AUDIO; audidx++) {
        if (wvdata[audidx].key == NULL) {
          wvdp = &wvdata[audidx];
          wvdp->key = SML3_strdup(key);
          wvdp->data = data;
          wvdp->dsize = dlen;
          wvdp->anzref = 1;
          break;
        }
      }
    } else {
      wvdp->anzref++;
    }
  }

  sortfind_wvdata(wvdata, NULL);

  if (data == NULL || dlen <= 0) {  /* remove */
    return NULL;
  } else {  /* add */
    wvdp = sortfind_wvdata(wvdata, key);
    if (wvdp == NULL) { outerr("no space left in audio array"); return NULL; }
  }

  return wvdp;
} /* Ende adddel_wvdata */


/* calculate (de-)crescendo multiplicator */
static double
audiocr(int *crmom, int *crmax, VG_BOOL isstopping)
{
  double volm;

  if (!isstopping) { volm = 1.; } else { volm = 0.; }

  if (crmom == NULL || crmax == NULL) { return volm; }

  if (*crmax > 0) {
    if (*crmom < *crmax) {
      if (!isstopping) {
        volm = (double)*crmom / (double)*crmax;
      } else {
        volm = (1. - ((double)*crmom / (double)*crmax));
      }
      (*crmom)++;
    }
    if (*crmom >= *crmax) { *crmax = 0; }
  }

  return volm;
} /* Ende audiocr */


/* audiomixer: mix into audio stream */
static void
audiomix(int audidx, unsigned char *dbuf, int dlen, double volx)
{
  struct vgi_audio *sptr;
  int ipos;
  union {
    float f;
    int i;
    short h;
    unsigned char c[4];
  } u_konv;
  double vcres;

  sptr = vg4data.lists.audio.s;

  if (dbuf == NULL || dlen <= 0) { return; }

  if (audidx >= 0) {  /* mix into audio-data */
    if (volx < .001) { volx = 0.; }

    ipos = 0;
    if (sptr->bitsize == VG_AUDIO_BITSIZE_16) {  /* 16bit integer */
      if (!sptr->stereo) {  /* mono */
        while (dlen >= 2) {
          memmove(u_konv.c, dbuf, 2);
          vcres = audiocr(&sptr->d[audidx].crmom, &sptr->d[audidx].crmax, sptr->d[audidx].isstopping);
          u_konv.h = (short)((double)u_konv.h * volx * vcres);
          sptr->mixbuf[ipos++].l += (long)u_konv.h;
          dlen -= 2; dbuf += 2;
        }
      } else {  /* stereo */
        while (dlen >= 4) {
          memmove(u_konv.c, dbuf, 2);
          vcres = audiocr(&sptr->d[audidx].crmom, &sptr->d[audidx].crmax, sptr->d[audidx].isstopping);
          u_konv.h = (short)((double)u_konv.h * volx * vcres);
          sptr->mixbuf[ipos++].l += (long)u_konv.h;
          dlen -= 2; dbuf += 2;
          memmove(u_konv.c, dbuf, 2);
          vcres = audiocr(&sptr->d[audidx].crmom, &sptr->d[audidx].crmax, sptr->d[audidx].isstopping);
          u_konv.h = (short)((double)u_konv.h * volx * vcres);
          sptr->mixbuf[ipos++].l += (long)u_konv.h;
          dlen -= 2; dbuf += 2;
        }
      }

    } else if (sptr->bitsize == VG_AUDIO_BITSIZE_32 || sptr->bitsize == VG_AUDIO_BITSIZE_32FLOAT) {  /* 32bit */
      if (!sptr->stereo) {  /* mono */
        while (dlen >= 4) {
          if (sptr->bitsize == VG_AUDIO_BITSIZE_32FLOAT) {  /* floating point */
            memmove(u_konv.c, dbuf, 4);
            vcres = audiocr(&sptr->d[audidx].crmom, &sptr->d[audidx].crmax, sptr->d[audidx].isstopping);
            u_konv.f = (float)((double)u_konv.f * volx * vcres);
            sptr->mixbuf[ipos++].d += (double)u_konv.f;
          } else {  /* integer */
            memmove(u_konv.c, dbuf, 4);
            vcres = audiocr(&sptr->d[audidx].crmom, &sptr->d[audidx].crmax, sptr->d[audidx].isstopping);
            u_konv.f = (float)((double)u_konv.f * volx * vcres);
            sptr->mixbuf[ipos++].l += (long)u_konv.i;
          }
          dlen -= 4; dbuf += 4;
        }
      } else {  /* stereo */
        while (dlen >= 8) {
          if (sptr->bitsize == VG_AUDIO_BITSIZE_32FLOAT) {  /* floating point */
            memmove(u_konv.c, dbuf, 4);
            vcres = audiocr(&sptr->d[audidx].crmom, &sptr->d[audidx].crmax, sptr->d[audidx].isstopping);
            u_konv.f = (float)((double)u_konv.f * volx * vcres);
            sptr->mixbuf[ipos++].d += (double)u_konv.f;
            dlen -= 4; dbuf += 4;
            memmove(u_konv.c, dbuf, 4);
            vcres = audiocr(&sptr->d[audidx].crmom, &sptr->d[audidx].crmax, sptr->d[audidx].isstopping);
            u_konv.f = (float)((double)u_konv.f * volx * vcres);
            sptr->mixbuf[ipos++].d += (double)u_konv.f;
          } else {  /* Integer */
            memmove(u_konv.c, dbuf, 4);
            vcres = audiocr(&sptr->d[audidx].crmom, &sptr->d[audidx].crmax, sptr->d[audidx].isstopping);
            u_konv.i = (int)((double)u_konv.i * volx * vcres);
            sptr->mixbuf[ipos++].l += (long)u_konv.i;
            dlen -= 4; dbuf += 4;
            memmove(u_konv.c, dbuf, 4);
            vcres = audiocr(&sptr->d[audidx].crmom, &sptr->d[audidx].crmax, sptr->d[audidx].isstopping);
            u_konv.i = (int)((double)u_konv.i * volx * vcres);
            sptr->mixbuf[ipos++].l += (long)u_konv.i;
          }
          dlen -= 4; dbuf += 4;
        }
      }
    }

  } else {  /* transfer audio data */
    if (sptr->bitsize == VG_AUDIO_BITSIZE_16) {  /* 16bit integer */
      union {
        short h;
        unsigned char c[2];
      } *u_kptr = (void *)dbuf;
      for (ipos = 0; ipos < dlen / 2; ipos++) {
        if (sptr->mixbuf[ipos].l < -32767) {
          sptr->mixbuf[ipos].l = -32767;
        } else if (sptr->mixbuf[ipos].l > 32767) {
          sptr->mixbuf[ipos].l = 32767;
        }
        u_kptr[ipos].h = (short)sptr->mixbuf[ipos].l;
      }
    } else if (sptr->bitsize == VG_AUDIO_BITSIZE_32 || sptr->bitsize == VG_AUDIO_BITSIZE_32FLOAT) {  /* 32bit */
      union {
        float f;
        int i;
        unsigned char c[4];
      } *u_kptr = (void *)dbuf;
      if (sptr->bitsize == VG_AUDIO_BITSIZE_32FLOAT) {  /* floating point */
        for (ipos = 0; ipos < dlen / 4; ipos++) {
          if (sptr->mixbuf[ipos].d < -1.) {
            sptr->mixbuf[ipos].d = -1.;
          } else if (sptr->mixbuf[ipos].d > 1.) {
            sptr->mixbuf[ipos].d = 1.;
          }
          u_kptr[ipos].f = (float)sptr->mixbuf[ipos].d;
        }
      } else {  /* integer */
        for (ipos = 0; ipos < dlen / 4; ipos++) {
          if (sptr->mixbuf[ipos].l < -2147483647) {
            sptr->mixbuf[ipos].l = -2147483647;
          } else if (sptr->mixbuf[ipos].l > 2147483647) {
            sptr->mixbuf[ipos].l = 2147483647;
          }
          u_kptr[ipos].i = (int)sptr->mixbuf[ipos].l;
        }
      }
    }
  }
} /* Ende audiomix */


/* callback-function: put next audio data into buffer */
static void
audiocbf(void *vdata, unsigned char *abuf, int abufsize)
{
  struct vgi_audio *sptr;
  int audidx, dlen;
  double volx;

  (void)vdata;
  sptr = vg4data.lists.audio.s;

  if (abuf == NULL || abufsize <= 0) { return; }

  memset(abuf, 0, (size_t)abufsize);

  /* initialize audio-mixer-buffer */
  if (sptr->mixbuf == NULL || sptr->ebuf == NULL) {
    if (sptr->mixbuf == NULL) {
      sptr->mixbuf = SML3_malloc(sizeof(*sptr->mixbuf) * abufsize);
    }
    if (sptr->ebuf == NULL) {
      sptr->ebuf = SML3_malloc(sizeof(*sptr->ebuf) * abufsize);
    }
    sptr->mixsize = abufsize;
  } else if (sptr->mixsize < abufsize) {
    sptr->mixbuf = SML3_realloc(sptr->mixbuf, sizeof(*sptr->mixbuf) * abufsize);
    sptr->ebuf = SML3_realloc(sptr->ebuf, sizeof(*sptr->ebuf) * abufsize);
    sptr->mixsize = abufsize;
  }
  memset(sptr->mixbuf, 0, sizeof(*sptr->mixbuf) * abufsize);

  /* mix in for each audio file */
  for (audidx = 0; audidx < MAX_AUDIO; audidx++) {
    if (!sptr->d[audidx].isext) {
      if (sptr->d[audidx].u.i.dbuf == NULL) { continue; }  /* not used */
    } else {
      if (sptr->d[audidx].u.e.ffp == NULL) { continue; }  /* not used */
    }
    if (sptr->d[audidx].ispaused) { continue; }
    if (sptr->issuspend && !sptr->d[audidx].issys) { continue; }  /* commonly suspended */

    /* wait until sptr->d[audidx].waitaudc has been ended */
    if (sptr->d[audidx].waitaudc > 0 && sptr->d[audidx].waitaudc <= MAX_AUDIO) {
      if ((!sptr->d[sptr->d[audidx].waitaudc - 1].isext && sptr->d[sptr->d[audidx].waitaudc - 1].u.i.dbuf != NULL)
          || (sptr->d[sptr->d[audidx].waitaudc - 1].isext && sptr->d[sptr->d[audidx].waitaudc - 1].u.e.ffp != NULL)
         ) {
        continue;
      } else {
        sptr->d[audidx].waitaudc = 0;
      }
    }

    /* volume for this audio stream */
    volx = (double)sptr->mainvol * (double)sptr->volg[sptr->d[audidx].vgidx] * (double)sptr->d[audidx].voladj / 1000000.;
    if (sptr->ismute) { volx = 0.; }

    /* mix in audio-data */
    if (!sptr->d[audidx].isext) {  /* internal (WAVE) */

      /* calculate number of audio-data */
      if (sptr->d[audidx].u.i.dlen < abufsize) {
        dlen = sptr->d[audidx].u.i.dlen;
      } else {
        dlen = abufsize;
      }

      /* mix in */
      audiomix(audidx, sptr->d[audidx].u.i.dbuf, dlen, volx);

      sptr->d[audidx].u.i.dbuf += (size_t)dlen;
      sptr->d[audidx].u.i.dlen -= dlen;

      if (dlen < abufsize) {  /* end of audio-data */
        sptr->d[audidx].u.i.dbuf = NULL;
        sptr->d[audidx].u.i.dlen = 0;
        if (!sptr->d[audidx].isstopping && sptr->d[audidx].doloop) {  /* start new */
          sptr->d[audidx].crmax = 0;
          sptr->d[audidx].crmom = 0;
          sptr->d[audidx].u.i.dbuf = sptr->d[audidx].u.i.start;
          sptr->d[audidx].u.i.dlen = sptr->d[audidx].u.i.size;
        }
      }

      if (sptr->d[audidx].isstopping && sptr->d[audidx].crmax == 0) {  /* ended */
        sptr->d[audidx].u.i.dbuf = NULL;
        sptr->d[audidx].u.i.dlen = 0;
        sptr->d[audidx].isstopping = VG_FALSE;
      }

    } else {  /* external program */
      memset(sptr->ebuf, 0, sizeof(*sptr->ebuf) * abufsize);

      /* read in audio-data */
      dlen = (int)fread(sptr->ebuf, 1, abufsize, sptr->d[audidx].u.e.ffp);

      /* mix in */
      audiomix(audidx, sptr->ebuf, dlen, volx);

      if (dlen < abufsize) {  /* end of audio-data */
        pclose(sptr->d[audidx].u.e.ffp);
        sptr->d[audidx].u.e.ffp = NULL;
        if (!sptr->d[audidx].isstopping && sptr->d[audidx].doloop) {  /* start new */
          sptr->d[audidx].crmax = 0;
          sptr->d[audidx].crmom = 0;
          sptr->d[audidx].u.e.ffp = popen(sptr->d[audidx].u.e.file[0], "r");
        }
      }

      if (sptr->d[audidx].isstopping && sptr->d[audidx].crmax == 0) {  /* ended */
        pclose(sptr->d[audidx].u.e.ffp);
        sptr->d[audidx].u.e.ffp = NULL;
        sptr->d[audidx].isstopping = VG_FALSE;
      }
    }
  }

  /* transfer audio-data */
  audiomix(-1, abuf, abufsize, 0.);
} /* audiocbf */


/* audio_load:
 * load audio file (WAVE, FLAC, MP3, OGG or MIDI)
 * @param filename  audio file to load
 * @param voladj    volume adjustment (0 - 255), default = 100
 * @param vgidx     one of VG_AUDIO_VOLUMES: volume-group this audio shall belong to
 *                  (may be or'ed with VG_AUDIO_SYSTEM)
 * @return  audio-descriptor (positive integer) or 0 = not loaded
 */
static int
audio_load(const char *filename, int voladj, int vgidx)
{
  return audio_load_doit(filename, voladj, vgidx, VG_TRUE);
} /* Ende audio_load */


/* audio_load_doit: do action */
static int
audio_load_doit(const char *filename, int voladj, int vgidx, VG_BOOL intolist)
{
  VG_BOOL issys;
  int audidx;
  const char *endg;
  struct vgi_audio *sptr;

  if (filename == NULL || *filename == '\0') { outerr("no filename given"); return 0; }

  sptr = vg4data.lists.audio.s;

  if (voladj < 0) { voladj = 0; } else if (voladj > 255) { voladj = 255; }
  issys = !!(vgidx & VG_AUDIO_SYSTEM);
  vgidx &= ~VG_AUDIO_SYSTEM;
  if (vgidx < 0 || vgidx == VG_AUDIO_VOLUME_ALL || vgidx >= VG_AUDIO_VOLUME_MAXENUM) { vgidx = VG_AUDIO_VOLUME_SOUND; }

  /* search for free element-index */
  for (audidx = 0; audidx < MAX_AUDIO; audidx++) {
    if (!sptr->d[audidx].isext && sptr->d[audidx].u.i.key == NULL) { break; }
  }
  if (audidx == MAX_AUDIO) { outerr("space exhausted for a new audio file"); return 0; }

  /* get file extension */
  endg = strrchr(filename, '.');
  if (endg == NULL) { endg = ""; }

  /* open file */

  if (strcasecmp(endg, ".wav") == 0 || strcasecmp(endg, ".wave") == 0) {  /* WAVE */
    struct wv_data *wvdp = sortfind_wvdata(sptr->wvdata, filename);
    if (wvdp == NULL) {  /* load file */
      char *memptr = NULL;
      size_t memlen = 0;
      if (!vg4data.iolib.f.audio_load(filename, &memptr, &memlen)) { return 0; }
      wvdp = adddel_wvdata(sptr->wvdata, filename, memptr, (int)memlen);
      if (wvdp == NULL) { return 0; }
    } else {  /* increment reference count */
      wvdp->anzref++;
    }

    vg4data.iolib.f.audio_lock(VG_TRUE);
    memset(&sptr->d[audidx], 0, sizeof(sptr->d[0]));
    sptr->d[audidx].vgidx = vgidx - 1;
    sptr->d[audidx].voladj = voladj;
    sptr->d[audidx].issys = issys;
    sptr->d[audidx].nolist = (intolist ? VG_FALSE : VG_TRUE);
    sptr->d[audidx].isext = VG_FALSE;
    sptr->d[audidx].u.i.key = wvdp->key;
    sptr->d[audidx].u.i.start = (unsigned char *)wvdp->data;
    sptr->d[audidx].u.i.size = wvdp->dsize;
    sptr->d[audidx].u.i.dbuf = NULL;
    sptr->d[audidx].u.i.dlen = 0;
    vg4data.iolib.f.audio_lock(VG_FALSE);

  } else if (strcasecmp(endg, ".fla") == 0 || strcasecmp(endg, ".flac") == 0) {  /* FLAC */
    struct wv_data *wvdp = sortfind_wvdata(sptr->wvdata, filename);
    if (wvdp == NULL) {  /* load file */
      char *memptr;
      size_t memlen;
      if (!flac2wav(filename, &memptr, &memlen)) { return 0; }
      if (!vg4data.iolib.f.audio_load(filename, &memptr, &memlen)) {
        if (memptr != NULL) { free(memptr); }
        return 0;
      }
      wvdp = adddel_wvdata(sptr->wvdata, filename, memptr, (int)memlen);
      if (wvdp == NULL) { return 0; }
    } else {  /* increment reference count */
      wvdp->anzref++;
    }

    vg4data.iolib.f.audio_lock(VG_TRUE);
    memset(&sptr->d[audidx], 0, sizeof(sptr->d[0]));
    sptr->d[audidx].vgidx = vgidx - 1;
    sptr->d[audidx].voladj = voladj;
    sptr->d[audidx].issys = issys;
    sptr->d[audidx].nolist = (intolist ? VG_FALSE : VG_TRUE);
    sptr->d[audidx].isext = VG_FALSE;
    sptr->d[audidx].u.i.key = wvdp->key;
    sptr->d[audidx].u.i.start = (unsigned char *)wvdp->data;
    sptr->d[audidx].u.i.size = wvdp->dsize;
    sptr->d[audidx].u.i.dbuf = NULL;
    sptr->d[audidx].u.i.dlen = 0;
    vg4data.iolib.f.audio_lock(VG_FALSE);

  } else if (strcasecmp(endg, ".mp3") == 0 || strcasecmp(endg, ".mpeg3") == 0) {  /* MP3 */
    /* use child process to read in audio-data */
    char *cprog;

    if (vgidx != VG_AUDIO_VOLUME_MUSIC) {
      outerr("\"%s\" can only played in volume-group VG_AUDIO_VOLUME_MUSIC", filename);
      return 0;
    }

    if (access(filename, R_OK) < 0) {
      outerr("error reading file \"%s\": %s", filename, strerror(errno));
      return 0;
    }

    cprog = mp3exec(16, (sptr->stereo ? 2 : 1), sptr->freq, filename);
    if (cprog == NULL) { return 0; }
    cprog = SML3_strdup(cprog);

    vg4data.iolib.f.audio_lock(VG_TRUE);
    memset(&sptr->d[audidx], 0, sizeof(sptr->d[0]));
    sptr->d[audidx].vgidx = vgidx - 1;
    sptr->d[audidx].voladj = voladj;
    sptr->d[audidx].issys = issys;
    sptr->d[audidx].nolist = (intolist ? VG_FALSE : VG_TRUE);
    sptr->d[audidx].isext = VG_TRUE;
    sptr->d[audidx].u.e.key = SML3_strdup(filename);
    sptr->d[audidx].u.e.file[0] = cprog;
    sptr->d[audidx].u.e.file[1] = NULL;
    sptr->d[audidx].u.e.ffp = NULL;
    vg4data.iolib.f.audio_lock(VG_FALSE);

  } else if (strcasecmp(endg, ".ogg") == 0) {  /* OGG */
    /* use child process to read in audio-data */
    char *cprog;

    if (vgidx != VG_AUDIO_VOLUME_MUSIC) {
      outerr("\"%s\" can only played in volume-group VG_AUDIO_VOLUME_MUSIC", filename);
      return 0;
    }

    if (access(filename, R_OK) < 0) {
      outerr("error reading file \"%s\": %s", filename, strerror(errno));
      return 0;
    }

    cprog = oggexec(filename);
    if (cprog == NULL) { return 0; }
    cprog = SML3_strdup(cprog);

    vg4data.iolib.f.audio_lock(VG_TRUE);
    memset(&sptr->d[audidx], 0, sizeof(sptr->d[0]));
    sptr->d[audidx].vgidx = vgidx - 1;
    sptr->d[audidx].voladj = voladj;
    sptr->d[audidx].issys = issys;
    sptr->d[audidx].nolist = (intolist ? VG_FALSE : VG_TRUE);
    sptr->d[audidx].isext = VG_TRUE;
    sptr->d[audidx].u.e.key = SML3_strdup(filename);
    sptr->d[audidx].u.e.file[0] = cprog;
    sptr->d[audidx].u.e.file[1] = NULL;
    sptr->d[audidx].u.e.ffp = NULL;
    vg4data.iolib.f.audio_lock(VG_FALSE);

  } else if (strcasecmp(endg, ".mid") == 0 || strcasecmp(endg, ".midi") == 0) {  /* MIDI */
    /* use child process to read in audio-data */
    char *cprog1, *cprog2;

    if (vgidx != VG_AUDIO_VOLUME_MUSIC) {
      outerr("\"%s\" can only played in volume-group VG_AUDIO_VOLUME_MUSIC", filename);
      return 0;
    }

    if (access(filename, R_OK) < 0) {
      outerr("error reading file \"%s\": %s", filename, strerror(errno));
      return 0;
    }

    cprog1 = midiexec(16, (sptr->stereo ? 2 : 1), sptr->freq, filename, 0);
    if (cprog1 == NULL) { return 0; }
    cprog1 = SML3_strdup(cprog1);
    cprog2 = midiexec(16, (sptr->stereo ? 2 : 1), sptr->freq, filename, 2);
    if (cprog2 == NULL) { return 0; }
    cprog2 = SML3_strdup(cprog2);

    vg4data.iolib.f.audio_lock(VG_TRUE);
    memset(&sptr->d[audidx], 0, sizeof(sptr->d[0]));
    sptr->d[audidx].vgidx = vgidx - 1;
    sptr->d[audidx].voladj = voladj;
    sptr->d[audidx].issys = issys;
    sptr->d[audidx].nolist = (intolist ? VG_FALSE : VG_TRUE);
    sptr->d[audidx].isext = VG_TRUE;
    sptr->d[audidx].u.e.key = SML3_strdup(filename);
    sptr->d[audidx].u.e.file[0] = cprog1;
    sptr->d[audidx].u.e.file[1] = cprog2;
    sptr->d[audidx].u.e.ffp = NULL;
    vg4data.iolib.f.audio_lock(VG_FALSE);

  } else {
    outerr("Cannot load \"%s\": unknown format", filename);
    return 0;
  }

  return audidx + 1;
} /* Ende audio_load_doit */


/* audio_unload:
 * unload audio file
 * @param audc   audio-descriptor
 */
static void
audio_unload(int audc)
{
  struct vgi_audio *sptr;

  audc--;
  if (audc < 0 || audc >= MAX_AUDIO) { return; }

  sptr = vg4data.lists.audio.s;

  /* is loaded? */
  if (!sptr->d[audc].isext && sptr->d[audc].u.i.key == NULL) { return; }

  vg4data.iolib.f.audio_lock(VG_TRUE);
  if (!sptr->d[audc].isext) {  /* internal */
    adddel_wvdata(sptr->wvdata, sptr->d[audc].u.i.key, NULL, 0);
  } else {  /* external program */
    if (sptr->d[audc].u.e.ffp != NULL) { pclose(sptr->d[audc].u.e.ffp); }
    if (sptr->d[audc].u.e.file[0] != NULL) { free(sptr->d[audc].u.e.file[0]); }
    if (sptr->d[audc].u.e.file[1] != NULL) { free(sptr->d[audc].u.e.file[1]); }
    if (sptr->d[audc].u.e.key != NULL) { free(sptr->d[audc].u.e.key); }
  }
  memset(&sptr->d[audc], 0, sizeof(sptr->d[audc]));
  vg4data.iolib.f.audio_lock(VG_FALSE);
} /* Ende audio_unload */


/* audio_unload_group:
 * unload audio files of a volume-group
 * @param vgidx  volume-group: one of VG_AUDIO_VOLUMES
 */
static void
audio_unload_group(int vgidx)
{
  struct vgi_audio *sptr;
  int audidx;

  if (vgidx < 0 || vgidx >= VG_AUDIO_VOLUME_MAXENUM) { return; }

  sptr = vg4data.lists.audio.s;

  for (audidx = 0; audidx < MAX_AUDIO; audidx++) {
    if (!sptr->d[audidx].isext && sptr->d[audidx].u.i.key == NULL) { continue; }
    if (vgidx == VG_AUDIO_VOLUME_ALL || vgidx == sptr->d[audidx].vgidx + 1) {
      if (!sptr->d[audidx].nolist) { vg4->audio->unload(audidx + 1); }
    }
  }
} /* Ende audio_unload_group */


/* audio_getname:
 * get filename of the audio
 * @param audc   audio-descriptor
 * @return  filename
 */
static const char *
audio_getname(int audc)
{
  struct vgi_audio *sptr;

  audc--;
  if (audc < 0 || audc >= MAX_AUDIO) { return ""; }

  sptr = vg4data.lists.audio.s;

  /* is loaded? */
  if (!sptr->d[audc].isext && sptr->d[audc].u.i.key == NULL) { return ""; }

  if (sptr->d[audc].isext) { return sptr->d[audc].u.e.key; }
  return sptr->d[audc].u.i.key;
} /* Ende audio_getname */


/* audio_play:
 * play audio file
 * @param audc   audio-descriptor
 * @param loop   whether play looping
 * @param cresc  whether start first playing with crescendo
 *
 * Note: an already playing audio file will not be touched
 */
static void
audio_play(int audc, VG_BOOL loop, VG_BOOL cresc)
{
  struct vgi_audio *sptr;

  audc--;
  if (audc < 0 || audc >= MAX_AUDIO) { return; }

  sptr = vg4data.lists.audio.s;

  /* is loaded? */
  if (!sptr->d[audc].isext && sptr->d[audc].u.i.key == NULL) { return; }

  /* set to playing */
  if ((!sptr->d[audc].isext && sptr->d[audc].u.i.dbuf == NULL)
      || (sptr->d[audc].isext && sptr->d[audc].u.e.ffp == NULL)
     ) {
    vg4data.iolib.f.audio_lock(VG_TRUE);

    sptr->d[audc].waitaudc = 0;
    sptr->d[audc].doloop = loop;
    sptr->d[audc].isstopping = VG_FALSE;
    sptr->d[audc].ispaused = VG_FALSE;

    if (cresc) {  /* crescendo */
      sptr->d[audc].crmax = sptr->freq * (sptr->stereo ? 2 : 1) * CRESDIM_SEK;
    } else {
      sptr->d[audc].crmax = 0;
    }
    sptr->d[audc].crmom = 0;

    if (!sptr->d[audc].isext) {  /* internal */
      sptr->d[audc].u.i.dbuf = sptr->d[audc].u.i.start;
      sptr->d[audc].u.i.dlen = sptr->d[audc].u.i.size;
    } else {  /* external program */
      if (loop && sptr->d[audc].u.e.file[1] != NULL) {
        sptr->d[audc].u.e.ffp = popen(sptr->d[audc].u.e.file[1], "r");
      } else {
        sptr->d[audc].u.e.ffp = popen(sptr->d[audc].u.e.file[0], "r");
      }
    }

    vg4data.iolib.f.audio_lock(VG_FALSE);
  }
} /* Ende audio_play */


/* audio_play_after:
 * play audio file after another one has been ended
 * @param audc    audio-descriptor
 * @param loop    whether play looping
 * @param audcw   audio-descriptor to wait for
 *
 * Note: an already playing audio file will not be touched
 */
static void
audio_play_after(int audc, VG_BOOL loop, int audcw)
{
  struct vgi_audio *sptr;

  audc--;
  if (audc < 0 || audc >= MAX_AUDIO) { return; }

  if (audcw <= 0 || audcw > MAX_AUDIO || audcw == audc + 1) { audcw = 0; }

  sptr = vg4data.lists.audio.s;

  /* is loaded? */
  if (!sptr->d[audc].isext && sptr->d[audc].u.i.key == NULL) { return; }

  /* set to playing */
  if ((!sptr->d[audc].isext && sptr->d[audc].u.i.dbuf == NULL)
      || (sptr->d[audc].isext && sptr->d[audc].u.e.ffp == NULL)
     ) {
    vg4data.iolib.f.audio_lock(VG_TRUE);

    sptr->d[audc].waitaudc = audcw;
    sptr->d[audc].doloop = loop;
    sptr->d[audc].isstopping = VG_FALSE;
    sptr->d[audc].ispaused = VG_FALSE;

    sptr->d[audc].crmax = sptr->d[audc].crmom = 0;

    if (!sptr->d[audc].isext) {  /* internal */
      sptr->d[audc].u.i.dbuf = sptr->d[audc].u.i.start;
      sptr->d[audc].u.i.dlen = sptr->d[audc].u.i.size;
    } else {  /* external program */
      if (loop && sptr->d[audc].u.e.file[1] != NULL) {
        sptr->d[audc].u.e.ffp = popen(sptr->d[audc].u.e.file[1], "r");
      } else {
        sptr->d[audc].u.e.ffp = popen(sptr->d[audc].u.e.file[0], "r");
      }
    }

    vg4data.iolib.f.audio_lock(VG_FALSE);
  }
} /* Ende audio_play_after */


/* audio_is_playing:
 * return whether an audio file is playing
 * @param audc       audio-descriptor
 * @param islooping  if not NULL, return whether is playing in a loop
 * @return  VG_TRUE = is playing or VG_FALSE = is not playing
 */
static VG_BOOL
audio_is_playing(int audc, VG_BOOL *islooping)
{
  struct vgi_audio *sptr;

  if (islooping != NULL) { *islooping = VG_FALSE; }

  audc--;
  if (audc < 0 || audc >= MAX_AUDIO) { return VG_FALSE; }

  sptr = vg4data.lists.audio.s;

  /* is loaded? */
  if (!sptr->d[audc].isext && sptr->d[audc].u.i.key == NULL) { return VG_FALSE; }

  /* is playing? */
  if ((!sptr->d[audc].isext && sptr->d[audc].u.i.dbuf != NULL)
      || (sptr->d[audc].isext && sptr->d[audc].u.e.ffp != NULL)
     ) {
    if (islooping != NULL && sptr->d[audc].doloop) { *islooping = VG_TRUE; }
    return VG_TRUE;
  }

  return VG_FALSE;
} /* Ende audio_is_playing */


/* audio_stop:
 * stop playing an audio file
 * @param audc     audio-descriptor
 * @param decresc  whether stop playing with decrescendo
 */
static void
audio_stop(int audc, VG_BOOL decresc)
{
  struct vgi_audio *sptr;

  audc--;
  if (audc < 0 || audc >= MAX_AUDIO) { return; }

  sptr = vg4data.lists.audio.s;

  /* is loaded? */
  if (!sptr->d[audc].isext && sptr->d[audc].u.i.key == NULL) { return; }

  /* is playing? */
  if ((!sptr->d[audc].isext && sptr->d[audc].u.i.dbuf == NULL)
      || (sptr->d[audc].isext && sptr->d[audc].u.e.ffp == NULL)) { return; }

  if (!sptr->d[audc].isstopping || !decresc) {
    vg4data.iolib.f.audio_lock(VG_TRUE);
    sptr->d[audc].isstopping = VG_TRUE;
    if (decresc) {  /* decrescendo */
      sptr->d[audc].crmax = sptr->freq * (sptr->stereo ? 2 : 1) * CRESDIM_SEK;
    } else {
      sptr->d[audc].crmax = 0;
    }
    sptr->d[audc].crmom = 0;
    vg4data.iolib.f.audio_lock(VG_FALSE);
  }
} /* Ende audio_stop */


/* audio_stop_group:
 * stop playing audio files of a volume-group
 * @param vgidx    volume-group: one of VG_AUDIO_VOLUMES
 * @param decresc  whether stop playing with decrescendo
 */
static void
audio_stop_group(int vgidx, VG_BOOL decresc)
{
  struct vgi_audio *sptr;
  int audidx;

  if (vgidx < 0 || vgidx >= VG_AUDIO_VOLUME_MAXENUM) { return; }

  sptr = vg4data.lists.audio.s;

  for (audidx = 0; audidx < MAX_AUDIO; audidx++) {
    if (!sptr->d[audidx].isext && sptr->d[audidx].u.i.key == NULL) { continue; }
    if (vgidx == VG_AUDIO_VOLUME_ALL || vgidx == sptr->d[audidx].vgidx + 1) {
      vg4->audio->stop(audidx + 1, decresc);
    }
  }
} /* Ende audio_stop_group */


/* audio_pause:
 * pause or continue playing an audio file
 * @param audc     audio-descriptor
 * @param dopause  VG_TRUE = pause or VG_FALSE = continue
 */
static void
audio_pause(int audc, VG_BOOL dopause)
{
  struct vgi_audio *sptr;

  audc--;
  if (audc < 0 || audc >= MAX_AUDIO) { return; }

  sptr = vg4data.lists.audio.s;

  /* is loaded? */
  if (!sptr->d[audc].isext && sptr->d[audc].u.i.key == NULL) { return; }

  /* is playing? */
  if ((!sptr->d[audc].isext && sptr->d[audc].u.i.dbuf == NULL)
      || (sptr->d[audc].isext && sptr->d[audc].u.e.ffp == NULL)) { return; }

  vg4data.iolib.f.audio_lock(VG_TRUE);
  sptr->d[audc].ispaused = dopause;
  vg4data.iolib.f.audio_lock(VG_FALSE);
} /* Ende audio_pause */


/* audio_pause_group:
 * pause or continue playing audio files of a volume-group
 * @param vgidx    volume-group: one of VG_AUDIO_VOLUMES
 * @param dopause  VG_TRUE = pause or VG_FALSE = continue
 */
static void
audio_pause_group(int vgidx, VG_BOOL dopause)
{
  struct vgi_audio *sptr;
  int audidx;

  if (vgidx < 0 || vgidx >= VG_AUDIO_VOLUME_MAXENUM) { return; }

  sptr = vg4data.lists.audio.s;

  for (audidx = 0; audidx < MAX_AUDIO; audidx++) {
    if (!sptr->d[audidx].isext && sptr->d[audidx].u.i.key == NULL) { continue; }
    if (vgidx == VG_AUDIO_VOLUME_ALL || vgidx == sptr->d[audidx].vgidx + 1) {
      vg4->audio->pause(audidx + 1, dopause);
    }
  }
} /* Ende audio_pause_group */


/* audio_volume:
 * set volume of main volume or of a volume-group
 * @param vgidx    volume-group: one of VG_AUDIO_VOLUMES
 *                 (VG_AUDIO_VOLUME_ALL: main volume)
 * @param volval   new value of the volume (0 to 255)
 *                 or -1 = don't set
 * @return  old value of the volume
 */
static int
audio_volume(int vgidx, int volval)
{
  struct vgi_audio *sptr;
  int oldval;

  if (vgidx < 0 || vgidx >= VG_AUDIO_VOLUME_MAXENUM) { return 0; }

  sptr = vg4data.lists.audio.s;

  if (volval > 255) { volval = 255; }

  oldval = 0;
  if (vgidx == VG_AUDIO_VOLUME_ALL) {
    oldval = sptr->mainvol;
    if (volval >= 0) { sptr->mainvol = volval; }
  } else {
    oldval = sptr->volg[vgidx - 1];
    if (volval >= 0) { sptr->volg[vgidx - 1] = volval; }
  }

  return oldval;
} /* Ende audio_volume */


/* audio_mute:
 * set to mute or unset from mute
 * @param doit  VG_TRUE = set, VG_FALSE = unset
 * @return  old value
 */
static VG_BOOL
audio_mute(VG_BOOL doit)
{
  struct vgi_audio *sptr;
  VG_BOOL oldval;

  sptr = vg4data.lists.audio.s;

  oldval = sptr->ismute;
  sptr->ismute = doit;

  return oldval;
} /* Ende audio_mute */


/* audio_is_mute:
 * return whether audio is set to mute
 * @return  VG_TRUE = mute, VG_FALSE = not mute
 */
static VG_BOOL
audio_is_mute(void)
{
  struct vgi_audio *sptr;

  sptr = vg4data.lists.audio.s;

  return sptr->ismute;
} /* Ende audio_is_mute */


/* audio_suspend:
 * suspend or continue audio output
 * (except audio files loaded with VG_AUDIO_SYSTEM)
 * @param doit  VG_TRUE = suspend, VG_FALSE = continue
 * @return  old value
 */
static VG_BOOL
audio_suspend(VG_BOOL doit)
{
  struct vgi_audio *sptr;
  VG_BOOL oldval;

  sptr = vg4data.lists.audio.s;

  oldval = sptr->issuspend;
  sptr->issuspend = doit;

  return oldval;
} /* Ende audio_suspend */


/* audio_dump:
 * dump audio entries
 * @param outfp  filepointer to dump to, or NULL = stdout
 */
static void
audio_dump(FILE *outfp)
{
  int audidx, i1;
  struct vgi_audio *sptr;

  if (outfp == NULL) { outfp = stdout; }

  sptr = vg4data.lists.audio.s;

  fprintf(outfp, "\nDump of audio entries\n");
  fprintf(outfp, "=====================\n\n");

  if (sptr->freq == 0) { fprintf(outfp, "Audio device not opened\n\n"); return; }

  fprintf(outfp, "Audio device opened with frequency=%d, %s\n", sptr->freq, (sptr->stereo ? "stereo" : "mono"));
  fprintf(outfp, "Playing audio is %smute and is %ssuspended\n", (sptr->ismute ? "" : "not "), (sptr->issuspend ? "" : "not "));
  fprintf(outfp, "Volumes: main=%d, volgroups: ", (int)sptr->mainvol);
  for (i1 = 0; i1 < VG_AUDIO_VOLUME_MAXENUM - 1; i1++) { fprintf(outfp, "[%d]=%d ", i1 + 1, (int)sptr->volg[i1]); }
  fprintf(outfp, "\n\n");

  for (audidx = 0; audidx < MAX_AUDIO; audidx++) {
    if (!sptr->d[audidx].isext && sptr->d[audidx].u.i.key == NULL) { continue; }
    if (sptr->d[audidx].nolist) { continue; }

    fprintf(outfp, "- [%d]\n", audidx + 1);

    if (sptr->d[audidx].isext) {
      fprintf(outfp, "  file=%s, is-playing=%s\n", sptr->d[audidx].u.e.key, (sptr->d[audidx].u.e.ffp != NULL ? "yes" : "no"));
      fprintf(outfp, "  external program: %s\n", sptr->d[audidx].u.e.file[0]);
    } else {
      fprintf(outfp, "  file=%s, is-playing=%s\n", sptr->d[audidx].u.i.key, (sptr->d[audidx].u.i.dbuf != NULL ? "yes" : "no"));
      fprintf(outfp, "  data-size=%d bytes, still-to-play=%d bytes\n", sptr->d[audidx].u.i.size, sptr->d[audidx].u.i.dlen);
    }

    fprintf(outfp, "  volgroup=%d, vol-adjustment=%d, wait-for-audio=%d\n",
            (int)sptr->d[audidx].vgidx + 1,
            (int)sptr->d[audidx].voladj,
            sptr->d[audidx].waitaudc);
    fprintf(outfp, "  is-system-sound=%s, play-looping=%s\n",
            (sptr->d[audidx].issys ? "yes" : "no"),
            (sptr->d[audidx].doloop ? "yes" : "no"));
    fprintf(outfp, "  is-stopping=%s, is-paused=%s\n",
            (sptr->d[audidx].isstopping ? "yes" : "no"),
            (sptr->d[audidx].ispaused ? "yes" : "no"));

    fprintf(outfp, "\n");
  }
} /* Ende audio_dump */
