/**
    audio handling, including sound playing and interface to speex codec and portaudio library

    Copyright (c) 2020-2022 The Creators of Simphone

    See the file COPYING.LESSER.txt for copying permission.
**/

#ifndef _XOPEN_SOURCE
#define _XOPEN_SOURCE 500 /* putenv */
#endif

#include "config.h"
#include "spth.h"

#include "const.h"
#include "logger.h"
#include "table.h"
#include "error.h"
#include "utils.h"
#include "system.h"
#include "crypto.h"
#include "socket.h"
#include "keygen.h"
#include "network.h"
#include "contact.h"
#include "param.h"
#include "proto.h"
#include "limit.h"
#include "proxy.h"
#include "nat.h"
#include "server.h"
#include "client.h"
#include "msg.h"
#include "audio.h"
#include "api.h"

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include <time.h>
#include <errno.h>

#ifndef SIM_AUDIO_DEVICE_SKIP
#define SIM_AUDIO_DEVICE_SKIP 1000
#endif

#define AUDIO_SAMPLE_MIN 6000   /* minimal supported sample rate */
#define AUDIO_SAMPLE_MAX 48000  /* maximal supported sample rate */
#define AUDIO_SAMPLE_CALL 16000 /* default sample rate for audio calls */

#define AUDIO_SAMPLE_PROXY 8000   /* maximal sample rate for relayed audio calls */
#define AUDIO_MAX_PROXY_QUALITY 2 /* maximal speex quality level for relayed audio calls */

#define AUDIO_MAX_FRAME_SAMPLES 640 /* maximal number of samples per frame */
#define AUDIO_MAX_CHANNELS 2        /* maximal number of audio channels */
#define AUDIO_MAX_SOUND_SAMPLES 960 /* maximal sound file frequency = 48 KHz */

const char *audio_state_names[] = {
  CONTACT_AUDIO_HANGUP, CONTACT_AUDIO_OUTGOING, CONTACT_AUDIO_INCOMING, CONTACT_AUDIO_TALKING
};

struct _audio_status audio_status;

#if HAVE_LIBSPEEX

static const char *audio_type_names[] = {
  SIM_EVENT_NET_DEVICE_INPUT, SIM_EVENT_NET_DEVICE_OUTPUT, SIM_EVENT_NET_DEVICE_RING
};
static simbool audio_invalid_flags[SIM_ARRAY_SIZE (audio_type_names)] = { false, false, false };

#define SIM_MODULE "audio"

#include <portaudio.h>

#include <speex/speex.h>
#include <speex/speex_stereo.h>
#include <speex/speex_callbacks.h>
#include <speex/speex_preprocess.h>
#include <speex/speex_resampler.h>
#include <speex/speex_echo.h>
#include <speex/speex_jitter.h>

static struct audio_speex {
  int rate;                      /* audio sample rate (number of samples per second) */
  int samples;                   /* speex frame size (number of samples per frame) */
  int frames;                    /* number of speex frames per audio data packet */
  int quality;                   /* set encoder quality level */
  void *encoder;                 /* speex encoder */
  void *decoder;                 /* speex decoder */
  SpeexPreprocessState *preproc; /* speex denoiser */
  SpeexEchoState *noecho;        /* echo canceller */
  JitterBuffer *dejitter;        /* speex jitter buffer */
  SpeexStereoState stereo;       /* decoder stereo state */
  simunsigned cputime;           /* number of nanoseconds of cpu time that has been used before cpu load measurement */
  simunsigned cpucycles;         /* number of milliseconds of cpu cycles that has been used before cpu load measurement */
  simunsigned cputick;           /* time when cpu load measurement has to finish or zero if already finished */
  int cpuload;                   /* cpu load of audio call in tenths thousands of per cent (0 - no call, < 0 - not yet known) */
  unsigned speed;                /* audio bytes per second times four or zero if none */
  int oldrate;                   /* previous sample rate or zero if none */
} audio_speex;

static simnumber audio_test_id = 0;
static simbool audio_reinit_flag = false;
static simbool audio_reset_start_flag, audio_reset_stop_flag;
static int audio_channels_count[2] = { AUDIO_MAX_CHANNELS, AUDIO_MAX_CHANNELS }; /* [false] = output, [true] = input */
static simnumber audio_tick;
static simunsigned audio_udp_tick;
static int audio_udp_count = 0;
static int audio_hangup_error = SIM_OK;
static simunsigned audio_device_tick = 0;
static pth_t tid_audio = NULL;
static PaStream *audio_stream = NULL, *sound_stream = NULL;
static simtype sound_count_table;  /* keyed by sound name, value is number of times to play (0 = infinite) */
static simtype sound_thread_table; /* keyed by sound name, value is tid */
static simtype event_speech_table; /* preallocated SIM_EVENT_SPEECH */

struct sound_arg {
  pth_t tid;
  char *name;
};

#define AUDIO_GET_CLIENT_FD(client) ((client) == AUDIO_CLIENT_TEST ? -2 : (client) ? (client)->sock->fd : -1)

#define AUDIO_CASE_SPEEX_MODE(rate) \
  ((rate) > 25000 ? &speex_uwb_mode : (rate) > 12500 ? &speex_wb_mode : &speex_nb_mode)
#define AUDIO_CASE_SPEEX_QUALITY(rate) ((rate) > 25000 ? 10 : (rate) > 12500 ? 8 : 6)

static int audio_set_error_ (simclient client, int error) {
  int err = SIM_OK;
  const char *hangup = client && audio_status.client == client && error != SIM_OK ? audio_status.hangup : NULL;
  audio_hangup_error = error;
  if (hangup) {
    simnumber start = client->call.time;
    simcontact contact = client->contact;
    event_test_error_audio (contact->id, err = audio_hangup_ (client, error));
    if (err != SIM_CALL_HANGUP)
      event_send_history_system (contact, "CALL ", hangup, 0, start, error, true);
    audio_status.hangup = NULL;
  }
  return err;
}

static int audio_set_status (simclient client, simnumber id, const char *hangup) {
  audio_status.client = client;
  audio_status.hangup = hangup;
  audio_status.id = id;
  audio_status.udport = audio_udp_count = 0;
  audio_reset_stop_flag = audio_reset_start_flag = false;
  return AUDIO_CALL_TALKING;
}

static void audio_set_params (simtype table, int sample) {
  table_set_pointer (table, SIM_CMD_CALL_CODEC, SIM_CMD_CALL_CODEC_SPEEX);
  table_set_number (table, SIM_CMD_CALL_SAMPLE, sample);
  table_set_number (table, SIM_CMD_CALL_QUALITY, param_get_number ("speex.quality.min"));
  table_set_number (table, SIM_CMD_CALL_FRAMES, param_get_number ("audio.frames.latency"));
  table_set_number (table, SIM_CMD_CALL_MTU, param_get_number ("audio.frames.mtu"));
  table_set_number (table, SIM_CMD_CALL_ECHO, param_get_number ("speex.echo"));
}

static simtype audio_get_params (simclient client, const simtype table, simnumber *sample) {
  client->call.quality = table_get_number (table, SIM_CMD_CALL_QUALITY);
  client->call.frames = table_get_number (table, SIM_CMD_CALL_FRAMES);
  client->call.mtu = table_get_number (table, SIM_CMD_CALL_MTU);
  *sample = table_get_number (table, SIM_CMD_CALL_SAMPLE);
  return table_get_string (table, SIM_CMD_CALL_CODEC);
}

int audio_get_param (int param) {
  if (param == AUDIO_PARAM_SPEED) {
    simclient client = audio_status.client;
    if (! client || client == AUDIO_CLIENT_TEST || ! socket_check_server (client->sock))
      return -1;
    return (int) limit_get_param (LIMIT_PARAM_RECV) >= (int) audio_speex.speed ? audio_speex.speed : 0;
  }
  if (param == AUDIO_PARAM_OLDRATE)
    return audio_speex.oldrate == audio_speex.rate ? 0 : audio_speex.oldrate;
  return param == AUDIO_PARAM_RATE ? audio_speex.rate : param == AUDIO_PARAM_CPU ? audio_speex.cpuload / 1000 : 0;
}

simtype audio_get_codecs (void) {
  return pointer_new (SIM_CMD_CALL_CODEC_SPEEX);
}

static int audio_get_tail (simclient client) {
  int tail = 1;
  if (client) {
    int echo = tail = param_get_number ("speex.echo");
    simnumber flags = client->contact->flags;
    if ((tail <= 0 || (flags & CONTACT_FLAG_ECHO_CANCEL_N)) && (! tail || ! (flags & CONTACT_FLAG_ECHO_CANCEL_Y)))
      tail = 0;
    LOG_DEBUG_ ("speex echo %d%s%s %s\n", echo < 0 ? -1 : ! ! echo, flags & CONTACT_FLAG_ECHO_CANCEL_Y ? "y" : "",
                flags & CONTACT_FLAG_ECHO_CANCEL_N ? "n" : "", tail ? "ON" : "OFF");
  }
  return abs (tail);
}

int audio_device_count (void) {
  int count = audio_test_id ? Pa_GetDeviceCount () : 0;
  return count > 0 ? count : 0;
}

static simtype audio_device_get_name (int *types, const PaDeviceInfo *info) {
  simtype name = nil ();
  if (info) {
#if HAVE_ALSA
    int len = (name = string_copy (info->name)).len, type = AUDIO_TYPE_VIRTUAL;
    if (! SIM_STRING_CHECK_DIFF_CONST (name.ptr, "/dev/")) {
      type = 0;
    } else if (len && name.str[--len] == ')') {
      while (len && isdigit (name.str[len - 1]))
        --len;
      if (len && name.str[--len] == ',') {
        const int l = SIM_STRING_GET_LENGTH_CONST ("hw:");
        while (len && isdigit (name.str[len - 1]))
          --len;
        if (len > l && ! SIM_STRING_CHECK_DIFF_CONST ((char *) &name.str[len - l], "hw:")) {
          char *s = strrchr (name.ptr, ' ');
          type = name.str[s ? s - (char *) name.str : len - l] = 0;
        }
      }
    }
    if (types)
      *types |= type;
#else
#ifdef _WIN32 /* WMME uses CP_ACP */
    unsigned len = strlen (info->name);
    wchar_t *wname = sim_new (sizeof (*wname) * len);
    if (MultiByteToWideChar (CP_ACP, 0, info->name, len, wname, len))
      name = sim_convert_ucs_to_utf (wname, &len);
    sim_free (wname, sizeof (*wname) * strlen (info->name));
    if (name.typ == SIMNIL)
#endif
      name = string_copy (info->name);
#endif /* HAVE_ALSA */
  }
  return name;
}

simtype audio_device_get (int device, int *inchannels, int *outchannels, int *types) {
  const PaDeviceInfo *info = audio_test_id ? Pa_GetDeviceInfo (device) : NULL;
  *types = 0;
  if (info) {
    *inchannels = info->maxInputChannels;
    *outchannels = info->maxOutputChannels;
    if (Pa_GetDefaultInputDevice () == device)
      *types |= AUDIO_TYPE_INPUT;
    if (Pa_GetDefaultOutputDevice () == device)
      *types |= AUDIO_TYPE_OUTPUT;
  }
  return audio_device_get_name (types, info);
}

static const PaDeviceInfo *audio_device_find (const simtype name, unsigned outputidx, int *device) {
  int devidx, devcount = Pa_GetDeviceCount ();
  const PaDeviceInfo *info = NULL;
  simtype names = table_new (61), fullname;
  for (devidx = 0; devidx < devcount && ! info; devidx++) {
    simtype devname = audio_device_get_name (NULL, info = Pa_GetDeviceInfo (*device = devidx));
    if (devname.typ != SIMNIL && (outputidx ? info->maxOutputChannels : info->maxInputChannels)) {
      int count = (int) table_get_number (names, devname.ptr) + 1;
      char idx[13];
      sprintf (idx, "%d:", count);
      table_set_key_number (names, string_copy (devname.ptr), count);
      if (string_check_diff_len (fullname = string_cat (idx, devname.ptr), name.str, name.len))
        info = NULL;
      string_free (fullname);
    } else
      info = NULL;
    string_free (devname);
  }
  table_free (names);
  return info;
}

static int audio_device_get_param (unsigned outputidx, PaStreamParameters *params, double *sample) {
  extern PaError PaMacCore_GetBufferSizeRange (PaDeviceIndex, long *, long *);
  const PaDeviceInfo *info;
  int latency = sample ? param_get_number ("audio.latency") : param_get_number ("sound.latency");
  simtype namedev = string_cat ("audio.device.", audio_type_names[outputidx]), name = param_get_string (namedev.ptr);
  simtype namechan = string_cat ("audio.channels.", audio_type_names[outputidx]);
  int device = 0, channels = param_get_number (namechan.ptr);
  string_free (namechan);
  string_free (namedev);
  if (! audio_test_id || Pa_GetDeviceCount () <= 0)
    return SIM_CALL_NO_AUDIO;
  if ((info = audio_device_find (name, outputidx, &device)) == NULL) {
    if (name.len)
      LOG_WARN_ ("device %s '%s' not found, using default\n", audio_type_names[outputidx], name.str);
    device = outputidx ? Pa_GetDefaultOutputDevice () : Pa_GetDefaultInputDevice ();
    if ((info = Pa_GetDeviceInfo (device)) == NULL)
      return SIM_CALL_BAD_AUDIO;
  }
  if (! channels) {
    if ((channels = outputidx ? info->maxOutputChannels : info->maxInputChannels) > AUDIO_MAX_CHANNELS) {
      channels = AUDIO_MAX_CHANNELS;
    } else if (channels <= 0)
      channels = 1;
  }
  params->device = device;
  params->channelCount = channels;
  params->sampleFormat = paInt16;
  if (latency < 0)
    latency = param_get_default_number ("audio.latency", 0);
  if (latency > 0) {
    params->suggestedLatency = (PaTime) latency / 1000;
  } else if (sample) {
    params->suggestedLatency = outputidx ? info->defaultLowOutputLatency : info->defaultLowInputLatency;
  } else
    params->suggestedLatency = outputidx ? info->defaultHighOutputLatency : info->defaultHighInputLatency;
  params->hostApiSpecificStreamInfo = NULL;
  if (sample)
    *sample = info->defaultSampleRate;
  if (LOG_PROTECT_ (SIM_MODULE, SIM_LOG_DEBUG)) {
    simtype devname = audio_device_get_name (NULL, info);
    long minsamples = 0, maxsamples = 0;
#ifdef __APPLE__
    PaMacCore_GetBufferSizeRange (device, &minsamples, &maxsamples);
#endif
    LOG_DEBUG_ ("device %s %d %s: sample %g*%d, latency %d (%g:%g ms), samples %ld..%ld\n", audio_type_names[outputidx],
                device, devname.str, info->defaultSampleRate, params->channelCount, latency,
                (outputidx ? info->defaultLowOutputLatency : info->defaultLowInputLatency) * 1000,
                (outputidx ? info->defaultHighOutputLatency : info->defaultHighInputLatency) * 1000,
                minsamples, maxsamples);
    string_free (devname);
    LOG_UNPROTECT_ ();
  }
  return SIM_OK;
}

static int audio_device_get_params (PaStreamParameters *inparams, PaStreamParameters *outparams, int *sample) {
  double insample, outsample;
  int err = audio_device_get_param (0, inparams, &insample);
  if (err == SIM_OK && (err = audio_device_get_param (1, outparams, &outsample)) == SIM_OK) {
    audio_channels_count[false] = outparams->channelCount;
    audio_channels_count[true] = inparams->channelCount;
    if (sample)
      *sample = (int) (insample < outsample ? insample : outsample);
  }
  return err;
}

#define AUDIO_CHECK_PLAYING(sound) \
  (sound_count_table.typ != SIMNIL && table_get (sound_count_table, sound).typ != SIMNIL)

static unsigned audio_play_sound_ (PaStream **stream, const short *input, unsigned samples,
                                   const char *sound, int size, int sample, int channels) {
  simbool test = ! sound || ! strcmp (sound, "hello") || ! strcmp (sound, "ding") || ! strcmp (sound, "bye");
  int maxcount = 2 * sample / size + 1;
  const short *buf = input;
  while (samples && (test ? audio_status.client == AUDIO_CLIENT_TEST : AUDIO_CHECK_PLAYING (sound)) && *stream) {
    long count = Pa_GetStreamWriteAvailable (*stream);
    if (count >= size) {
      int err, i;
      unsigned tmp[AUDIO_MAX_SOUND_SAMPLES * AUDIO_MAX_CHANNELS * sizeof (unsigned short) / sizeof (unsigned)];
      if (channels == AUDIO_MAX_CHANNELS) {
        const unsigned short *src = (const unsigned short *) input;
        unsigned *dst = tmp;
        for (i = 0; i < size; i++) {
          *dst++ = *src << 16 | *src;
          src++;
        }
      }
      err = Pa_WriteStream (*stream, channels == AUDIO_MAX_CHANNELS ? (const void *) tmp : (const void *) input, size);
      if (err != SIM_OK) {
        audio_status.stats[CONTACT_FRAMES_WRITE] += test;
        LOG_NOTE_ ("playback %s error %d\n", sound, err);
      } else
        input += size;
      samples = samples >= (unsigned) size ? samples - size : 0;
      maxcount = 2 * sample / size + 1;
    } else if (count < 0) {
      audio_status.stats[CONTACT_FRAMES_WRITE] += test;
      LOG_WARN_ ("playback %s error %ld\n", sound, count);
    }
    audio_status.stats[CONTACT_FRAMES_ALL] += test;
    pth_usleep_ (size * 500000 / sample);
    if (--maxcount < 0) {
      audio_status.stats[CONTACT_FRAMES_WRITE] += (samples / size) * test;
      audio_status.stats[CONTACT_FRAMES_ALL] += (samples / size) * test;
      LOG_WARN_ ("playback %s stuck\n", sound);
      break;
    }
  }
  LOG_XTRA_ ("played %s (%d samples)\n", sound, (int) (input - buf));
  return input - buf;
}

static simtype sim_sound_load (const char *sound, int *sample, unsigned *samples) {
  const simbyte ***sounds = sound_data, **snd, **s;
  simtype buf = nil (), tmp;
  while (*sounds)
    if (! strcmp (*(const char **) (snd = *sounds++), sound)) {
      if (sample) {
        short *output;
        int newrate = *sample, rate = abs (*sample = *(const int *) *++snd), count = 0;
        SpeexResamplerState *resampler = NULL;
        void *speex = speex_decoder_init (AUDIO_CASE_SPEEX_MODE (rate));
        *sample = newrate ? newrate : *sample;
        if (! speex)
          break;
        if (! newrate || rate == newrate || (resampler = speex_resampler_init (1, rate, newrate, 10, NULL)) != NULL) {
          speex_decoder_ctl (speex, SPEEX_SET_SAMPLING_RATE, &rate);
          speex_decoder_ctl (speex, SPEEX_SET_ENH, &count);
          speex_decoder_ctl (speex, SPEEX_GET_FRAME_SIZE, samples);
          for (s = snd; *++s; count++) {}
          tmp = string_new ((*samples * count + AUDIO_MAX_SOUND_SAMPLES) * sizeof (*output));
          for (output = tmp.ptr; *++snd; output += *samples) {
            SpeexBits bits;
            speex_bits_set_bit_buffer (&bits, (simbyte *) *snd + 1, **snd);
            speex_decode_int (speex, &bits, output);
          }
          memset (output, 0, tmp.str + tmp.len - (simbyte *) output);
          *samples = output - (short *) tmp.ptr;
          if (resampler) {
            unsigned len = (unsigned) ((simunsigned) *samples * newrate / rate);
            buf = string_new ((len + AUDIO_MAX_SOUND_SAMPLES) * sizeof (*output));
            memset (buf.str + len * sizeof (*output), 0, AUDIO_MAX_SOUND_SAMPLES * sizeof (*output));
            if (speex_resampler_process_int (resampler, 0, tmp.ptr, samples, buf.ptr, &len))
              string_free (buf), buf = nil ();
            string_free (tmp);
            *samples = len;
          } else
            buf = tmp;
          if (resampler)
            speex_resampler_destroy (resampler);
        }
        speex_decoder_destroy (speex);
      } else
        buf = pointer_new ("");
      break;
    }
  if (buf.typ == SIMNIL)
    LOG_WARN_ ("sound %s load error\n", sound);
  return buf;
}

static void *thread_sound_ (void *arg) {
  pth_t tid = ((struct sound_arg *) arg)->tid;
  char *name = ((struct sound_arg *) arg)->name;
  int err, rate = 0;
  simnumber count = table_get_number (sound_count_table, name);
  PaStream *local = NULL, **playback;
  simtype buf, sound, repeat;
  unsigned samples = 0;
  _pth_thread_join_ (&tid, NULL, name, -1);
  LOG_API_DEBUG_ ("%s * %lld\n", name, count);
  playback = &local;
  if (! param_get_number ("sound.threads")) {
    while (*(playback = &sound_stream)) {
      simwalker ctx;
      for (table_walk_first (&ctx, sound_count_table); (repeat = table_walk_next_number (&ctx, &sound)).typ != SIMNIL;)
        if (string_check_diff (sound, name) && ! repeat.num) {
          if (count)
            goto quit;
          LOG_DEBUG_ ("sound %s aborting %s\n", name, sound.str);
          table_delete (sound_count_table, sound.ptr);
          table_walk_first (&ctx, sound_count_table);
        }
      while (*playback) {
        if (audio_stream || ! AUDIO_CHECK_PLAYING (name))
          goto quit;
        pth_usleep_ (AUDIO_MAX_SOUND_SAMPLES * 500000 / AUDIO_SAMPLE_MAX);
      }
    }
    if (audio_stream)
      goto quit;
  }
  if (AUDIO_CHECK_PLAYING (name) && (buf = sim_sound_load (name, &rate, &samples)).typ != SIMNIL) {
    PaStreamParameters params;
    LOG_DEBUG_ ("sound %s %dHz (%d samples)\n", name, rate, samples);
    if ((err = audio_device_get_param (rate < 0 ? 1 : 2, &params, NULL)) == SIM_OK)
      err = Pa_OpenStream (playback, NULL, &params, abs (rate), paFramesPerBufferUnspecified, paClipOff, NULL, NULL);
    if (err == SIM_OK && (err = Pa_StartStream (*playback)) != SIM_OK) {
      int err2 = Pa_CloseStream (*playback);
      LOG_DEBUG_ ("close %d\n", err2);
      *playback = NULL;
    }
    if (err == SIM_OK) {
      while (AUDIO_CHECK_PLAYING (name) && *playback) {
        audio_play_sound_ (playback, buf.ptr, samples, name, AUDIO_MAX_SOUND_SAMPLES, abs (rate), params.channelCount);
        if (count && AUDIO_CHECK_PLAYING (name)) {
          if ((count = table_get_number (sound_count_table, name)) == 1)
            break;
          table_set_number (sound_count_table, name, count ? count - 1 : 0);
        }
      }
    } else
      LOG_WARN_ ("open %s error %d\n", name, err);
    if (*playback) {
      if ((err = AUDIO_CHECK_PLAYING (name) ? Pa_StopStream (*playback) : Pa_AbortStream (*playback)) != SIM_OK)
        LOG_WARN_ ("stop %s error %d\n", name, err);
      if ((err = Pa_CloseStream (*playback)) != SIM_OK) {
        LOG_WARN_ ("close %s error %d\n", name, err);
      } else
        LOG_DEBUG_ ("close %s\n", name);
      *playback = NULL;
    }
    string_free (buf);
  }
quit:
  if ((sound_count_table.typ == SIMNIL || table_delete (sound_count_table, name).typ == SIMNIL) && count)
    LOG_WARN_ ("sound %s not playing\n", name);
  LOG_API_DEBUG_ ("%s\n", name);
  sim_free_string (name);
  sim_free (arg, sizeof (struct sound_arg));
  return pth_thread_exit_ (false);
}

int audio_start_sound (const char *sound, int repeat) {
  int err = SIM_OK;
  simtype name, count;
  if (sound && sim_sound_load (sound, NULL, NULL).typ == SIMNIL) {
    err = SIM_CALL_BAD_SOUND;
  } else if (! audio_test_id || sound_count_table.typ == SIMNIL) {
    err = SIM_CALL_NO_AUDIO;
  } else if (sound) {
    if ((count = table_get_key_type (sound_count_table, name = string_copy (sound), SIMNUMBER)).typ == SIMNIL) {
      pth_t tid;
      struct sound_arg *arg = sim_new (sizeof (*arg));
      arg->tid = (pth_t) (long) table_get_number (sound_thread_table, sound);
      if ((err = _pth_thread_spawn (thread_sound_, arg, &tid, name, -1)) != SIM_OK) {
        table_delete (sound_thread_table, sound);
        table_delete (sound_count_table, sound);
        sim_free (arg, sizeof (*arg));
      } else {
        arg->name = string_copy (sound).ptr;
        table_set_key_number (sound_count_table, string_copy (sound), repeat);
        table_set_key_number (sound_thread_table, string_copy (sound), (long) tid);
      }
    } else
      table_set_key_number (sound_count_table, name, count.num && repeat ? count.num + repeat : 0);
  }
  return err;
}

int audio_stop_sound_ (const char *sound) {
  int err = SIM_OK;
  simtype key, name;
  simwalker ctx;
  if (sound) {
    err = table_delete (sound_count_table, sound).typ == SIMNIL ? SIM_CALL_BAD_SOUND : SIM_OK;
  } else if (sound_thread_table.ptr && table_walk_first (&ctx, sound_thread_table)) {
    while (table_walk_next_number (&ctx, &key).typ != SIMNIL)
      table_delete (sound_count_table, key.ptr);
    while (sound_thread_table.ptr && table_walk_first (&ctx, sound_thread_table) &&
           table_walk_next_number (&ctx, &key).typ != SIMNIL) {
      pth_t tid = (pth_t) (long) table_detach_key_number (sound_thread_table, name = string_copy_string (key));
      _pth_thread_join_ (&tid, NULL, name.ptr, -1);
      string_free (name);
    }
  }
  return err;
}

static simtype *event_new_speech (int probablity, int progress, int error) {
  table_set_number (event_speech_table, SIM_EVENT_SPEECH, error);
  table_set_number (event_speech_table, SIM_EVENT_SPEECH_PROBABILITY, probablity);
  table_set_number (event_speech_table, SIM_EVENT_SPEECH_PROGRESS, progress);
  return &event_speech_table;
}

void event_test_net_device (int status) {
  int device;
  unsigned i;
  simtype event = table_new_name (2, SIM_EVENT_NET);
  table_add_pointer (event, SIM_EVENT_NET, SIM_EVENT_NET_DEVICE);
  for (i = 0; i < SIM_ARRAY_SIZE (audio_type_names); i++) {
    simtype str = string_cat ("audio.device.", audio_type_names[i]), name = param_get_string (str.ptr);
    string_free (str);
    audio_invalid_flags[i] = name.len ? ! audio_device_find (name, i, &device) : ! audio_device_count ();
    if (audio_invalid_flags[i]) {
      if (status == SIM_STATUS_BUSY && name.len)
        status = SIM_STATUS_AWAY;
      table_add (event, audio_type_names[i], string_copy_string (name));
    }
  }
  if (status != SIM_STATUS_BUSY && status != SIM_STATUS_IDLE) {
    audio_device_tick = status == SIM_STATUS_ON ? sim_get_tick () : system_get_tick ();
    event_send (NULL, event);
  } else
    table_free (event);
}

void event_test_error_audio (simnumber id, int error) {
  if (error != SIM_OK && error != SIM_CALL_HANGUP) {
    simtype event = table_new_name (2, SIM_EVENT_ERROR);
    table_add_pointer (event, SIM_EVENT_ERROR, SIM_EVENT_ERROR_AUDIO);
    table_add_number (event, SIM_EVENT_ERROR_AUDIO, error);
    event_send_id (id, event);
    table_free (event);
  }
}

static void *thread_hangup_ (void *client) {
  int err = SIM_OK, fd = AUDIO_GET_CLIENT_FD (audio_status.client);
  simnumber id = audio_status.id;
  LOG_API_DEBUG_ ("@%020llu $%d (error %d)\n", id, audio_status.client != client ? -3 : fd, audio_hangup_error);
  if (client != AUDIO_CLIENT_TEST) {
    simbool reinit = audio_reinit_flag, cancel = sim_system_pnp_cancel ();
    LOG_CODE_ANY_ (SIM_MODULE_SYSTEM, SIM_LOG_DEBUG, "hangup%d %s\n", reinit, cancel ? "stop timer" : "no timer");
    err = audio_set_error_ (client, audio_hangup_error);
    if (! reinit)
      audio_init_ (SIM_STATUS_AWAY);
  } else if (audio_status.client == AUDIO_CLIENT_TEST)
    event_test_error_audio (id, err = audio_close_ (false));
  LOG_API_DEBUG_ ("@%020llu $%d error %d\n", id, fd, err);
  return pth_thread_exit_ (false);
}

static void audio_convert_to_mono (const int *input, int samples, short *output) {
  for (; samples--; input++)
    *output++ = (short) (((*input >> 16) + (short) *input) >> 1);
}

static void *audio_test_ (void) {
  int rate = audio_speex.rate, error = PTH_UNPROTECT (SIM_OK);
  unsigned hellosamples = 0, byesamples = 0, dingsamples = 0;
  simtype hello = sim_sound_load ("hello", &rate, &hellosamples), bye = sim_sound_load ("bye", &rate, &byesamples);
  simtype ding = sim_sound_load ("ding", &rate, &dingsamples);
  simbool speaking = PTH_PROTECT_ (false), keygen = audio_status.id == CONTACT_ID_KEYGEN;
  int err, maxcount = 2 * rate / audio_speex.samples + 1, percent = 0, prob = SIM_EVENT_SPEECH_END;
  int samples = param_get_number ("audio.test") * rate, entropy = param_get_number ("crypto.entropy");
  unsigned parts = ((entropy < 0 ? 384 : entropy ? entropy : 1) + RANDOM_SIZE_BLOCK * 8 - 1) / (RANDOM_SIZE_BLOCK * 8);
  int delay = samples - param_get_number ("speex.speech.delay") * rate / 1000, chans = audio_channels_count[false];
  int probstart = param_get_number ("speex.speech.start"), probcont = param_get_number ("speex.speech.continue");
  void **rng = ! keygen && contact_list.me ? random_session : random_private;
  unsigned bytes, len = 0, prog = 0;
  short *buf;
  simtype buffer = string_buffer_new ((samples + audio_speex.samples) * sizeof (*buf) * AUDIO_MAX_CHANNELS);
  limit_reset (sim_system_cpu_get (SYSTEM_CPU_TIME_PROCESS, NULL) / 1000000, system_get_tick ());
  audio_speex.cpuload = 0;
  if (keygen) {
    unsigned bits = random_buffer_add_bits (rng, 0, &bytes);
    if (random_buffer_open (rng, 0, parts)) {
      LOG_DEBUG_ ("speech done: %u/%u bytes\n", bits / 8, bytes);
      keygen = false;
      event_send_id (audio_status.id, *event_new_speech (SIM_EVENT_SPEECH_END, 100, SIM_OK));
    } else if ((bits = bits * 100 / (RANDOM_SIZE_ENTROPY * 8 * RANDOM_ENTROPY_FACTOR * parts)) != 0)
      if ((bytes = bytes * 100 / (RANDOM_SIZE_ENTROPY * RANDOM_ENTROPY_FACTOR * parts)) != 0) {
        prog = bits > bytes ? bytes : bits;
        event_send_fast (audio_status.id, *event_new_speech (SIM_EVENT_SPEECH_MIN, prog, SIM_OK));
      }
  }
  if (audio_status.client == AUDIO_CLIENT_TEST)
    memset (audio_status.stats, 0, sizeof (audio_status.stats));
  audio_play_sound_ (&audio_stream, hello.ptr, hellosamples, "hello", audio_speex.samples, rate, chans);
  audio_play_sound_ (&audio_stream, ding.ptr, dingsamples, "ding", audio_speex.samples, rate, chans);
  LOG_DEBUG_ ("test start: %d samples\n", samples);
  /* clear the recording buffer if the audio driver was so lame as to fill it up (pulseaudio) */
  if (audio_status.client == AUDIO_CLIENT_TEST) {
    if ((err = Pa_StopStream (audio_stream)) != SIM_OK)
      LOG_WARN_ ("test stop error %d\n", err);
    event_send_fast (audio_status.id, *event_new_speech (SIM_EVENT_SPEECH_MIN, prog, err));
    if ((error = Pa_StartStream (audio_stream)) != SIM_OK) {
      LOG_WARN_ ("test start error %d\n", error);
      if (keygen)
        event_send_id (audio_status.id, *event_new_speech (SIM_EVENT_SPEECH_END, prog, error));
      samples = 0;
      keygen = false;
    }
  }
  while (samples > 0 && audio_status.client == AUDIO_CLIENT_TEST) {
    long count = Pa_GetStreamReadAvailable (audio_stream);
    if (count >= audio_speex.samples) {
      string_buffer_append (&buffer, len, audio_speex.samples * sizeof (*buf) * audio_channels_count[true]);
      if ((err = Pa_ReadStream (audio_stream, buf = (short *) (buffer.str + len), audio_speex.samples)) == SIM_OK) {
        if (audio_channels_count[true] == AUDIO_MAX_CHANNELS)
          audio_convert_to_mono ((void *) buf, audio_speex.samples, buf);
        random_buffer_add (rng, buf, audio_speex.samples * sizeof (*buf), samples * sizeof (*buf));
        if (audio_speex.preproc) {
          speex_preprocess_run (audio_speex.preproc, buf);
          speex_preprocess_ctl (audio_speex.preproc, SPEEX_PREPROCESS_GET_PROB, &percent);
          if (samples >= delay)
            percent = SIM_EVENT_SPEECH_END;
          if (percent != prob && percent != SIM_EVENT_SPEECH_END)
            event_send_fast (audio_status.id, *event_new_speech (prob = percent, prog, SIM_OK));
          if (percent > (speaking ? probcont : probstart)) {
            unsigned bits = random_buffer_add_bits (rng, audio_speex.samples / RANDOM_ENTROPY_SAMPLES, &bytes); /* side effect */
            if (keygen) {
              if (random_buffer_open (rng, 0, parts)) {
                LOG_DEBUG_ ("speech done: %u/%u bytes\n", bits / 8, bytes);
                samples = 0;
                keygen = false;
                event_send_id (audio_status.id, *event_new_speech (SIM_EVENT_SPEECH_END, 100, SIM_OK));
              } else {
                unsigned bytespercent = bytes * 100 / (RANDOM_SIZE_ENTROPY * RANDOM_ENTROPY_FACTOR * parts);
                unsigned bitspercent = bits * 100 / (RANDOM_SIZE_ENTROPY * 8 * RANDOM_ENTROPY_FACTOR * parts);
                if (bitspercent > bytespercent)
                  bitspercent = bytespercent;
                if (prog != bitspercent)
                  event_send_fast (audio_status.id, *event_new_speech (prob, prog = bitspercent, SIM_OK));
              }
              samples += audio_speex.samples;
              if (! speaking)
                LOG_DEBUG_ ("speech %d start: %u/%u bytes = %d%%\n", percent, bits / 8, bytes, prog);
            }
            speaking = true;
          } else if (speaking) {
            if (keygen) {
              LOG_DEBUG_ ("speech %d stop %d%%\n", percent, prog);
              samples = param_get_number ("audio.test") * rate;
            }
            speaking = false;
          }
        }
        len += audio_speex.samples * sizeof (*buf);
      }
      samples -= audio_speex.samples;
      maxcount = 2 * rate / audio_speex.samples + 1;
      audio_status.stats[CONTACT_FRAMES_ALL]++;
    } else
      err = count < 0 ? (int) count : SIM_OK;
    if (err != SIM_OK) {
      event_new_speech (prob == SIM_EVENT_SPEECH_END ? SIM_EVENT_SPEECH_MIN : prob, prog, error = err);
      event_send_fast (audio_status.id, event_speech_table);
      audio_status.stats[CONTACT_FRAMES_READ]++;
      LOG_ANY_ (count < 0 ? SIM_LOG_WARN : SIM_LOG_NOTE, "record error %d\n", err);
    }
    pth_usleep_ (audio_speex.samples * 500000 / rate);
    if (--maxcount < 0)
      samples = 0;
  }
  if (rng == random_session) {
    if (! contact_list.me || ! (param_get_number ("crypto.reseed") & 2)) {
      random_buffer_close (random_session);
    } else if (random_buffer_open (random_session, 1, 1))
      LOG_DEBUG_ ("test done: reseeded\n");
  }
  if (keygen) {
    event_new_speech (SIM_EVENT_SPEECH_END, prog, maxcount < 0 ? SIM_CALL_BAD_AUDIO : SIM_CALL_HANGUP);
    event_send_id (audio_status.id, event_speech_table);
  } else if (audio_status.id == CONTACT_ID_TEST)
    event_send_id (audio_status.id, *event_new_speech (SIM_EVENT_SPEECH_END, 100, SIM_OK));
  if (len) {
    audio_play_sound_ (&audio_stream, ding.ptr, dingsamples, "ding", audio_speex.samples, rate, chans);
    LOG_DEBUG_ ("test replay: %d samples\n", len / (unsigned) sizeof (*buf));
  }
  len = audio_play_sound_ (&audio_stream, buffer.ptr, len / sizeof (*buf), NULL, audio_speex.samples, rate, chans);
  string_buffer_free (buffer);
  if (maxcount >= 0) {
    unsigned bits = random_buffer_add_bits (rng, 0, &bytes);
    LOG_DEBUG_ ("test done: %d samples (%u/%u bytes = %d%%)\n", len, bits / 8, bytes, prog);
    error = SIM_OK;
  } else {
    if (error == SIM_OK)
      error = SIM_CALL_NO_AUDIO_INPUT;
    LOG_WARN_ ("test done: %d samples (recording failed)\n", len);
  }
  audio_play_sound_ (&audio_stream, ding.ptr, dingsamples, "ding", audio_speex.samples, rate, chans);
  audio_play_sound_ (&audio_stream, bye.ptr, byesamples, "bye", audio_speex.samples, rate, chans);
  /* flush playback buffer */
  if (audio_status.client == AUDIO_CLIENT_TEST && (err = Pa_StopStream (audio_stream)) != SIM_OK)
    LOG_WARN_ ("test stop error %d\n", err);
  string_free (bye);
  string_free (ding);
  string_free (hello);
  if (audio_status.client == AUDIO_CLIENT_TEST)
    pth_thread_spawn (thread_hangup_, AUDIO_CLIENT_TEST, NULL, -2);
  LOG_API_DEBUG_ ("@%020llu $%d error %d\n", audio_status.id, AUDIO_GET_CLIENT_FD (audio_status.client), error);
  pth_thread_exit_ (false);
  return (void *) (long) error;
}

#define AUDIO_GET_TIMESTAMP() (jitter_buffer_get_pointer_timestamp (audio_speex.dejitter) - (unsigned) audio_tick)

static int audio_count_packets (void) {
  int count = 0;
  return jitter_buffer_ctl (audio_speex.dejitter, JITTER_BUFFER_GET_AVALIABLE_COUNT, &count) ? 0 : count;
}

static void *thread_audio_ (void *arg) {
  simbool speaking = false;
  int err = audio_hangup_error = SIM_OK, err0 = SIM_OK, err1 = SIM_OK, percent = 0, prob = SIM_EVENT_SPEECH_END;
  int old = audio_speex.frames, frames = 0, reseed = 0, timeout = 0, update = 0, activity = 0, opt, i;
  int delay = param_get_number ("speex.speech.delay");
  int probstart = param_get_number ("speex.speech.start"), probcont = param_get_number ("speex.speech.continue");
  simunsigned base = system_get_tick (), tick = 0, tickbase = 0, tickupdate = 0, tickinput = base, tickoutput = base;
  short input[AUDIO_MAX_FRAME_SAMPLES * AUDIO_MAX_CHANNELS], output[AUDIO_MAX_FRAME_SAMPLES * AUDIO_MAX_CHANNELS];
  short tmp[AUDIO_MAX_FRAME_SAMPLES];
  SpeexBits buf;
  LOG_API_DEBUG_ ("@%020llu $%d\n", audio_status.id, AUDIO_GET_CLIENT_FD (audio_status.client));
  pth_thread_set_priority ();
  if (audio_status.client == AUDIO_CLIENT_TEST)
    return audio_test_ ();
  speex_bits_init (&buf);
  if (audio_status.client) {
    reseed = param_get_number ("crypto.reseed");
    timeout = param_get_number_min ("audio.timeout", param_get_number ("audio.pnp") + 1000);
    update = param_get_number ("speex.jitter.update") * audio_speex.rate;
    activity = param_get_number ("speex.jitter.activity");
    for (i = CONTACT_STATS_MINE; i <= CONTACT_STATS_PEER; i++)
      memset (audio_status.client->contact->stats[i], 0, sizeof (audio_status.client->contact->stats[i]));
    tickbase = tickupdate = tick = audio_status.client->sndtick / audio_speex.samples * audio_speex.samples;
    server_set_qos (audio_status.client->sock, param_get_number ("socket.qos"));
  }
  server_ping (NULL);
  while (audio_status.client && audio_status.client->sock->err == SIM_OK) {
    long count = Pa_GetStreamReadAvailable (audio_stream), avail;
    simunsigned now = system_get_tick ();
    if (audio_speex.cputick) {
      if (! audio_speex.cputime) {
        audio_speex.cputime = sim_system_cpu_get (SYSTEM_CPU_TIME_PROCESS, NULL);
        audio_speex.cpucycles = limit_get_cpu ();
        audio_speex.cputick = now + 1000;
      } else if (now >= audio_speex.cputick) {
        simunsigned cputime = sim_system_cpu_get (SYSTEM_CPU_TIME_PROCESS, NULL);
        simnumber cpu = cputime - audio_speex.cputime - (limit_get_cpu () - audio_speex.cpucycles) * 1000000;
        audio_speex.cpuload = (int) ((cpu > 0 ? cpu : 0) / (now - audio_speex.cputick + 1000));
        limit_reset (cputime / 1000000, now);
        audio_speex.cputick = 0;
        LOG_DEBUG_ ("cpu load = %d.%03d%%\n", audio_speex.cpuload / 10000, audio_speex.cpuload % 10000 / 10);
      }
    }
    if (count >= audio_speex.samples) {
      JitterBufferPacket packet;
      packet.data = (char *) input;
      packet.len = sizeof (input);
      if ((err = jitter_buffer_get (audio_speex.dejitter, &packet, audio_speex.samples, NULL)) == SIM_OK) {
        SpeexBits bits;
        if (packet.span != (unsigned) audio_speex.samples)
          LOG_ANY_ (packet.span < (unsigned) audio_speex.samples ? SIM_LOG_INFO : SIM_LOG_NOTE,
                    "frame #%u buffer error %d\n", AUDIO_GET_TIMESTAMP (), packet.span);
        speex_bits_set_bit_buffer (&bits, packet.data, packet.len);
        if ((err = speex_decode_int (audio_speex.decoder, &bits, output)) != SIM_OK) {
          LOG_WARN_ ("frame #%u decode error %d\n", AUDIO_GET_TIMESTAMP (), err);
        } else if (audio_channels_count[false] == AUDIO_MAX_CHANNELS)
          speex_decode_stereo_int (output, audio_speex.samples, &audio_speex.stereo);
      } else if (audio_tick != -1) {
        LOG_DEBUG_ ("frame #%u buffer error %d\n", AUDIO_GET_TIMESTAMP (), err);
        audio_status.client->contact->stats[CONTACT_STATS_MINE][CONTACT_FRAMES_LOST]++;
      }
      if (err != SIM_OK) {
        speex_decode_int (audio_speex.decoder, NULL, output); /* cannot fail */
        if (audio_channels_count[false] == AUDIO_MAX_CHANNELS)
          speex_decode_stereo_int (output, audio_speex.samples, &audio_speex.stereo);
      }
      if (activity >= 0 && tick - tickupdate >= (unsigned) update)
        if (speex_decoder_ctl (audio_speex.decoder, SPEEX_GET_ACTIVITY, &i) >= 0 && i <= activity) {
          if ((opt = jitter_buffer_update_delay (audio_speex.dejitter, NULL, NULL)) != 0) {
            if (opt < 0 && audio_status.client->contact->stats[CONTACT_STATS_MINE][CONTACT_FRAMES_LOST])
              audio_status.client->contact->stats[CONTACT_STATS_MINE][CONTACT_FRAMES_LOST]--;
            LOG_XTRA_ ("frame #%d level %d update %s%d (%d)\n", AUDIO_GET_TIMESTAMP (), i,
                       opt > 0 ? "+" : "", opt / audio_speex.samples, audio_count_packets ());
          }
          tickupdate = tick;
        }
      jitter_buffer_tick (audio_speex.dejitter);
      err = SIM_CALL_NO_AUDIO_OUTPUT;
      if ((avail = Pa_GetStreamWriteAvailable (audio_stream)) < audio_speex.samples) {
        if (avail < 0) {
          if (audio_speex.noecho)
            speex_echo_state_reset (audio_speex.noecho);
          err = err1 = (int) avail;
        }
        LOG_ANY_ (avail > 0 ? SIM_LOG_DEBUG : SIM_LOG_WARN, "frame #%u write error %ld\n",
                  AUDIO_GET_TIMESTAMP (), avail);
      } else
        avail = audio_speex.samples;
      if (avail > 0 && (err = err1 = Pa_WriteStream (audio_stream, output, avail)) != SIM_OK) {
        const PaStreamInfo *info = Pa_GetStreamInfo (audio_stream);
        LOG_NOTE_ ("frame #%u write error %d (%g:%g ms) %gHz\n", AUDIO_GET_TIMESTAMP (), err,
                   info->inputLatency * 1000, info->outputLatency * 1000, info->sampleRate);
        if (audio_speex.noecho)
          speex_echo_state_reset (audio_speex.noecho);
      } else if (avail > 0)
        tickoutput = now;
      if (avail < audio_speex.samples || err != SIM_OK)
        audio_status.client->contact->stats[CONTACT_STATS_MINE][CONTACT_FRAMES_WRITE]++;
      audio_status.client->contact->stats[CONTACT_STATS_MINE][CONTACT_FRAMES_ALL]++;
      if (timeout && (simnumber) (now - tickoutput) >= timeout) {
        LOG_WARN_ ("frame #%u write timeout (%lld ms)\n", AUDIO_GET_TIMESTAMP (), now - tickoutput);
        break;
      }
      if (err != SIM_OK && err != SIM_CALL_NO_AUDIO_OUTPUT)
        goto skip;
      if ((err = err0 = Pa_ReadStream (audio_stream, input, audio_speex.samples)) == SIM_OK) {
        short *frame = input;
        tickinput = now;
        if (audio_reset_stop_flag || audio_speex.frames != old)
          frames = 0;
        old = audio_speex.frames;
        audio_reset_stop_flag = false;
        if (! frames) {
          speex_bits_reset (&buf);
          speex_bits_pack (&buf, audio_speex.frames, 8);
        }
        if (reseed & 1) {
          unsigned len = audio_speex.samples * sizeof (*frame) * audio_channels_count[true];
          random_buffer_add (random_session, frame, len, len);
        }
        if (audio_channels_count[true] == AUDIO_MAX_CHANNELS) {
          if (audio_speex.noecho) {
            audio_convert_to_mono ((void *) output, audio_speex.samples, output);
            audio_convert_to_mono ((void *) frame, audio_speex.samples, frame);
          } else if (! audio_status.client->sock->udport && SOCKET_CHECK_PROXY (audio_status.client->sock)) {
            audio_convert_to_mono ((void *) frame, audio_speex.samples, frame);
          } else if (! audio_reset_start_flag)
            speex_encode_stereo_int (frame, audio_speex.samples, &buf);
        }
        if (audio_speex.noecho)
          speex_echo_cancellation (audio_speex.noecho, input, output, frame = tmp);
        if (audio_speex.preproc) {
          speex_preprocess_run (audio_speex.preproc, frame);
          speex_preprocess_ctl (audio_speex.preproc, SPEEX_PREPROCESS_GET_PROB, &percent);
          if (! delay || (simnumber) (now - base) >= delay) {
            delay = 0;
          } else
            percent = SIM_EVENT_SPEECH_END;
          if (percent != prob && percent != SIM_EVENT_SPEECH_END)
            event_send_fast (audio_status.id, *event_new_speech (prob = percent, -1, SIM_OK));
          if (reseed & 1 && (speaking = percent > (speaking ? probcont : probstart)) == true) {
            random_buffer_add_bits (random_session, audio_speex.samples / RANDOM_ENTROPY_SAMPLES, NULL);
            if (random_buffer_open (random_session, 1, 1))                           /* help in case CryptGenRandom is fucked */
              param_set_number ("crypto.reseed", reseed &= ~1, SIM_PARAM_TEMPORARY); /* stop wasting cpu time on reseeding */
          }
        }
        if (! audio_reset_start_flag)
          speex_encode_int (audio_speex.encoder, frame, &buf);
        speex_bits_insert_terminator (&buf);
        if (++frames >= audio_speex.frames) {
          simtype arrays = array_new_arrays (SIM_CMD_AUDIO_NUMBERS);
          simtype data = pointer_new_len (buf.chars, audio_reset_start_flag ? 0 : speex_bits_nbytes (&buf));
          arrays.arr[SIM_CMD_AUDIO_DATA] = data;
          arrays.arr[SIM_CMD_AUDIO_NUMBERS] = array_new_numbers (SIM_CMD_AUDIO_NUMBERS_LATENCY);
          tick = tick / audio_speex.samples * audio_speex.samples;
          arrays.arr[SIM_CMD_AUDIO_NUMBERS].len = SIM_CMD_AUDIO_NUMBERS_TIME;
          arrays.arr[SIM_CMD_AUDIO_NUMBERS].arr[SIM_CMD_AUDIO_NUMBERS_TIME] = number_new (tick);
          audio_status.client->sndtick = tick += frames * audio_speex.samples;
          if (audio_udp_count && audio_udp_tick && now >= audio_udp_tick) {
            audio_udp_tick = now + param_get_number ("nat.udp");
            audio_udp_count--;
            nat_reverse_send_udp (audio_status.client);
          }
          if (server_send_packet_ (audio_status.client, arrays, audio_speex.rate) != SIM_OK)
            break;
          frames = 0;
        }
      }
    } else {
      if ((avail = Pa_GetStreamWriteAvailable (audio_stream)) < 0) {
        audio_status.client->contact->stats[CONTACT_STATS_MINE][CONTACT_FRAMES_WRITE]++;
        err1 = (int) avail;
        LOG_NOTE_ ("frame #%u write error %ld\n", AUDIO_GET_TIMESTAMP (), avail);
      }
      audio_status.client->contact->stats[CONTACT_STATS_MINE][CONTACT_FRAMES_ALL] += count < 0 ? frames + 1 : avail < 0;
      err = count < 0 ? (err0 = (int) count) : SIM_CALL_NO_AUDIO_INPUT;
    }
    if (err != SIM_OK) {
      if (err != SIM_CALL_NO_AUDIO_INPUT) {
        audio_status.client->contact->stats[CONTACT_STATS_MINE][CONTACT_FRAMES_READ] += frames + 1;
        LOG_ANY_ (count < 0 ? SIM_LOG_WARN : SIM_LOG_NOTE,
                  "frame #%lld read error %d (dropped %d frames)\n", tick - tickbase, err, frames + 1);
        frames = 0;
      }
      if (timeout && (simnumber) (now - tickinput) >= timeout) {
        LOG_WARN_ ("frame #%lld read timeout (%lld ms)\n", tick - tickbase, now - tickinput);
        break;
      }
    }
  skip:
    pth_usleep_ (1000);
    err = SIM_OK;
  }
  audio_hangup_error = err == SIM_OK ? err : err0 != SIM_OK ? err0 : err1 != SIM_OK ? err1 : err;
  if (audio_status.client)
    pth_thread_spawn (thread_hangup_, audio_status.client, NULL, AUDIO_GET_CLIENT_FD (audio_status.client));
  LOG_API_DEBUG_ ("@%020llu $%d error %d (input error %d, output error %d)\n",
                  audio_status.id, AUDIO_GET_CLIENT_FD (audio_status.client), err, err0, err1);
  if (buf.chars)
    memset (buf.chars, 0, buf.buf_size);
  speex_bits_destroy (&buf);
  memset (tmp, 0, sizeof (tmp));
  memset (output, 0, sizeof (output));
  memset (input, 0, sizeof (input));
  limit_reset (sim_system_cpu_get (SYSTEM_CPU_TIME_PROCESS, NULL) / 1000000, system_get_tick ());
  audio_speex.cpuload = 0;
  return pth_thread_exit_ (false);
}

static int audio_send_param (simclient client, int sample) {
  int latency = (audio_speex.frames * audio_speex.samples * 1000 + audio_speex.rate - 1) / audio_speex.rate;
  simtype table = client_new_cmd (SIM_CMD_RING, SIM_CMD_RING_QUALITY, number_new (audio_speex.quality),
                                  SIM_CMD_RING_FRAMES, number_new (latency));
  table_add_number (table, SIM_CMD_RING_SAMPLE, sample);
  if (! sample) {
    latency = param_get_number ("audio.latency");
    if (latency > 0 && param_get_default_number ("audio.latency", 0) != latency) {
      int samples = param_get_number ("audio.samples") * audio_speex.samples / 160;
      table_add_number (table, SIM_CMD_RING_LATENCY, samples ? latency * samples : latency);
    }
    if ((latency = param_get_number ("speex.jitter.latency")) != 0)
      table_add_number (table, SIM_CMD_RING_LATENCY_ADD, latency);
  }
  return client_send (client, table);
}

void audio_recv_packet (simtype data, simnumber timestamp) {
  simtype str = data;
  if (audio_tick == -1)
    audio_tick = timestamp;
  if (audio_speex.dejitter && str.len > 1 && *str.str && ! (--str.len % *str.str)) {
    unsigned i, len = str.len++ / *str.str++;
    for (i = 1; i < str.len; i += len) {
      JitterBufferPacket packet;
      packet.data = str.ptr;
      str.str += packet.len = len;
      packet.timestamp = (spx_uint32_t) timestamp;
      timestamp += packet.span = audio_speex.samples;
      jitter_buffer_put (audio_speex.dejitter, &packet);
    }
  } else if (str.len)
    LOG_NOTE_ (audio_speex.dejitter ? "frame #%u BAD\n" : "frame #%u unexpected\n", (int) timestamp - (int) audio_tick);
  string_free (data);
}

void audio_free_packet (simtype arrays) {
#if SIM_TYPE_CHECKS || ! defined(SIM_TYPE_CHECKS)
  if (arrays.typ == SIMARRAY_ARRAY) {
    if (arrays.len == SIM_CMD_AUDIO_NUMBERS) {
      simtype numbers = arrays.arr[SIM_CMD_AUDIO_NUMBERS];
      arrays.arr[SIM_CMD_AUDIO_DATA] = array_new_type (arrays.arr[SIM_CMD_AUDIO_DATA]);
      if (numbers.typ == SIMNUMBER)
        arrays.arr[SIM_CMD_AUDIO_NUMBERS] = array_new_type (numbers);
    }
    array_free (arrays);
  } else
    table_free (arrays);
#else
  type_free (arrays);
#endif
}

static simtype audio_speex_new_packet (const struct audio_speex *speex, simbool proxy, simbool ping) {
  simtype arrays = array_new_arrays (SIM_CMD_AUDIO_NUMBERS);
  SpeexBits bits;
  short frame[AUDIO_MAX_FRAME_SAMPLES * AUDIO_MAX_CHANNELS];
  memset (frame, 0, sizeof (frame));
  speex_bits_init (&bits);
  if ((audio_channels_count[true] == AUDIO_MAX_CHANNELS || speex != &audio_speex) && ! speex->noecho && ! proxy)
    speex_encode_stereo_int (frame, speex->samples, &bits);
  speex_encode_int (speex->encoder, frame, &bits);
  arrays.arr[SIM_CMD_AUDIO_DATA] = string_new (speex_bits_nbytes (&bits) * speex->frames + 1);
  speex_bits_destroy (&bits);
  if (ping) {
    arrays.arr[SIM_CMD_AUDIO_NUMBERS] = array_new_numbers (SIM_CMD_AUDIO_NUMBERS_LATENCY);
    arrays.arr[SIM_CMD_AUDIO_NUMBERS].arr[SIM_CMD_AUDIO_NUMBERS_TIME] = number_new (0x10000000000LL);
    arrays.arr[SIM_CMD_AUDIO_NUMBERS].arr[SIM_CMD_AUDIO_NUMBERS_PING] = number_new (0x3FFFFFFFFFLL);
    arrays.arr[SIM_CMD_AUDIO_NUMBERS].arr[SIM_CMD_AUDIO_NUMBERS_PONG] = number_new (0x3FFFFFFFFFLL);
    arrays.arr[SIM_CMD_AUDIO_NUMBERS].arr[SIM_CMD_AUDIO_NUMBERS_PROXY] = number_new (0x3FFFFFFF);
    arrays.arr[SIM_CMD_AUDIO_NUMBERS].arr[SIM_CMD_AUDIO_NUMBERS_LATENCY] = number_new (0x3FFFFFFF);
  } else
    arrays.arr[SIM_CMD_AUDIO_NUMBERS] = number_new (0x10000000000LL);
  return arrays;
}

static int audio_speex_size_packet (const struct audio_speex *speex, simsocket sock, simbool proxy, simbool ping) {
  unsigned size = socket_size_max (sock, proxy ? SOCKET_SEND_SSL : SOCKET_SEND_SSL | SOCKET_SEND_PROXY);
  simtype arrays = audio_speex_new_packet (speex, proxy, ping);
  size = SIM_MAX_PACKET_SIZE - size + SOCKET_OVERHEAD_SSL + type_size (arrays);
  audio_free_packet (arrays);
  LOG_DEBUG_ ("speex %d frames (%d+2+%d bytes)\n", speex->frames, size - 2, SOCKET_CALC_OVERHEAD_TCP (size));
  return size + SOCKET_CALC_OVERHEAD_TCP (size);
}

static void audio_speex_reopen (struct audio_speex *speex, simclient client, simbool proxy) {
  int minframes = param_get_number ("audio.frames.min"), maxframes = param_get_number ("audio.frames.max"), bitrate;
  int quality = param_get_number ("speex.quality.max"), callquality = (int) client->call.quality, min = proxy ? -1 : 1;
  if (! quality)
    quality = AUDIO_CASE_SPEEX_QUALITY (speex->rate);
  if (! callquality || callquality != (int) client->call.quality)
    callquality = AUDIO_CASE_SPEEX_QUALITY (speex->rate);
  if (quality > callquality && (quality = callquality) < 0) {
    quality = 0;
  } else if (proxy && quality > AUDIO_MAX_PROXY_QUALITY)
    quality = AUDIO_MAX_PROXY_QUALITY;
  speex_encoder_ctl (speex->encoder, SPEEX_SET_QUALITY, &quality);
  speex_encoder_ctl (speex->encoder, SPEEX_GET_BITRATE, &bitrate);
  LOG_DEBUG_ ("speex %d bits/sec (quality = %d)%s\n", bitrate, quality, proxy ? " PROXY" : "");
  if (client->call.frames < minframes) {
    speex->frames = minframes;
  } else
    speex->frames = client->call.frames > maxframes ? maxframes : (int) client->call.frames;
  if ((speex->frames = speex->frames * speex->rate / (speex->samples * 1000)) == 0)
    speex->frames = 1;
  for (speex->quality = quality; speex->frames > min; speex->frames -= min) {
    int size = audio_speex_size_packet (speex, client->sock, proxy, false);
    if (proxy) {
      int d = (audio_speex_size_packet (speex, client->sock, proxy, true) - size) * 4 + 1;
      size = (unsigned) speex->rate * (size + SOCKET_HEADER_TCP) * 4 / ((unsigned) speex->samples * speex->frames) + d;
      if ((speex->speed = size) + (unsigned) size / 3 <= PROXY_SPEED_AUDIO)
        break;
    } else if (size <= param_get_number ("audio.frames.mtu") && (! client->call.mtu || size <= client->call.mtu))
      break;
  }
  speex->cputime = 0, speex->cputick = 1, speex->cpuload = -1;
}

static int audio_speex_open (struct audio_speex *speex, simclient client, int sample, simbool proxy) {
  int denoise = param_get_number ("speex.denoise"), agc = param_get_number ("speex.agc"), latency;
  int rate, tail = audio_get_tail (client);
  const SpeexMode *mode;
  if (proxy && (! sample || abs (sample) > AUDIO_SAMPLE_PROXY)) {
    speex->rate = rate = AUDIO_SAMPLE_PROXY;
  } else
    speex->rate = rate = sample ? abs (sample) : AUDIO_SAMPLE_CALL;
  mode = AUDIO_CASE_SPEEX_MODE (rate);
  speex_stereo_state_reset (&speex->stereo);
  if ((speex->encoder = speex_encoder_init (mode)) == NULL || (speex->decoder = speex_decoder_init (mode)) == NULL)
    return ENOMEM;
  speex_encoder_ctl (speex->encoder, SPEEX_GET_FRAME_SIZE, &speex->samples);
  if (denoise || agc || tail > 0 || param_get_number ("crypto.reseed") & 1 || param_get_number ("ui.call.speech")) {
    if ((speex->preproc = speex_preprocess_state_init (speex->samples, rate)) == NULL)
      return ENOMEM;
    speex_preprocess_ctl (speex->preproc, SPEEX_PREPROCESS_SET_DENOISE, &denoise);
    speex_preprocess_ctl (speex->preproc, SPEEX_PREPROCESS_SET_AGC, &agc);
  }
  if (client) {
    int complexity = param_get_number ("speex.complexity"), plctuning = param_get_number ("speex.plctuning");
    int enhancer = param_get_number ("speex.enhance"), step = param_get_number ("speex.jitter.step");
    speex_encoder_ctl (speex->encoder, SPEEX_SET_SAMPLING_RATE, &speex->rate);
    speex_decoder_ctl (speex->decoder, SPEEX_SET_SAMPLING_RATE, &speex->rate);
    speex_decoder_ctl (speex->decoder, SPEEX_SET_ENH, &enhancer);
    if (complexity >= 0)
      speex_encoder_ctl (speex->encoder, SPEEX_SET_COMPLEXITY, &complexity);
    if (plctuning)
      speex_encoder_ctl (speex->encoder, SPEEX_SET_PLC_TUNING, &plctuning);
    if (audio_channels_count[false] == AUDIO_MAX_CHANNELS) {
      SpeexCallback callback;
      callback.callback_id = SPEEX_INBAND_STEREO;
      callback.func = speex_std_stereo_request_handler;
      callback.data = &speex->stereo;
      speex_decoder_ctl (speex->decoder, SPEEX_SET_HANDLER, &callback);
    }
    if (tail > 0 && (speex->noecho = speex_echo_state_init (speex->samples, tail * rate / 1000)) == NULL)
      return ENOMEM;
    if ((speex->dejitter = jitter_buffer_init (step *= speex->samples)) == NULL)
      return ENOMEM;
    jitter_buffer_update_delay (speex->dejitter, NULL, NULL);
    latency = param_get_number ("speex.jitter.latency") * rate / (speex->samples * 1000) * speex->samples;
    jitter_buffer_ctl (speex->dejitter, JITTER_BUFFER_SET_MARGIN, &latency);
    LOG_DEBUG_ ("speex %d+%d samples\n", latency, step);
    if ((latency = param_get_number ("speex.jitter.late")) != 0) {
      int late = latency > 0 ? JITTER_BUFFER_SET_LATE_COST : JITTER_BUFFER_SET_MAX_LATE_RATE;
      jitter_buffer_ctl (speex->dejitter, late, &latency);
    }
    audio_speex_reopen (speex, client, proxy);
  }
  return rate == speex->rate ? SIM_OK : EINVAL;
}

static void audio_speex_close (struct audio_speex *speex) {
  if (speex->dejitter)
    jitter_buffer_destroy (speex->dejitter), speex->dejitter = NULL;
  if (speex->preproc)
    speex_preprocess_state_destroy (speex->preproc), speex->preproc = NULL;
  if (speex->noecho)
    speex_echo_state_destroy (speex->noecho), speex->noecho = NULL;
  if (speex->decoder)
    speex_decoder_destroy (speex->decoder), speex->decoder = NULL;
  if (speex->encoder)
    speex_encoder_destroy (speex->encoder), speex->encoder = NULL;
  speex->speed = speex->frames = speex->samples = 0;
}

int audio_close_ (simbool hangup) {
  void *ret = NULL;
  simclient client = audio_status.client;
  int err = SIM_OK, fd = AUDIO_GET_CLIENT_FD (client);
  PaStream **stop = &sound_stream;
  const char *nick = client == AUDIO_CLIENT_TEST ? "TEST" : client ? client->contact->nick.ptr : NULL;
  LOG_XTRA_ ("CLOSE%d '%s'\n", hangup, nick);
  if (client) {
    stop = &audio_stream;
    if (client != AUDIO_CLIENT_TEST) {
      client->call.outparams = client->call.inparams = NULL;
      client->sock->ip = client->sock->udport = 0;
      if (hangup) {
        int e = audio_hangup_error;
        audio_hangup_error = SIM_OK;
        audio_hangup_ (NULL, e);
        if (*stop) {
          LOG_WARN_ ("stop error\n");
        } else
          stop = &sound_stream;
        if (audio_status.client != client) {
          const char *oldnick = nick;
          client = audio_status.client;
          nick = client == AUDIO_CLIENT_TEST ? "TEST" : client ? client->contact->nick.ptr : NULL;
          LOG_XTRA_ ("CLOSE '%s' -> '%s'\n", oldnick, nick);
        }
      }
    } else
      event_send_audio (NULL, CONTACT_AUDIO_TALKING, CONTACT_AUDIO_HANGUP);
    if (! hangup || audio_status.client == AUDIO_CLIENT_TEST) {
      audio_status.client = NULL;
      client_set_param (NULL, 0, CLIENT_PARAM_XFER_NAT);
    }
  }
  if (*stop) {
    LOG_XTRA_ ("close%d '%s'\n", hangup, nick);
    if (client) {
      stop = &audio_stream;
      if (client != AUDIO_CLIENT_TEST && audio_speex.speed)
        limit_send_speed (client, audio_speex.speed = 0);
      if ((err = pth_thread_join_ (&tid_audio, &ret, thread_audio_, fd)) != SIM_OK)
        return err;
      audio_status.id = 0;
      audio_speex_close (&audio_speex);
      if (client != AUDIO_CLIENT_TEST) {
        server_set_qos (client->sock, 0);
        client_release (client);
      }
    }
    if (*stop) {
      const PaStreamInfo *info = Pa_GetStreamInfo (*stop);
      if ((err = Pa_AbortStream (*stop)) != SIM_OK)
        LOG_WARN_ ("stop error %d\n", err);
      if (info)
        LOG_DEBUG_ ("close (%g:%g ms)\n", info->inputLatency * 1000, info->outputLatency * 1000);
      if ((err = Pa_CloseStream (*stop)) != SIM_OK)
        LOG_WARN_ ("close error %d\n", err);
      *stop = NULL;
    } else
      LOG_DEBUG_ ("close%d '%s'\n", hangup, nick);
  }
  if (audio_reinit_flag) {
    LOG_DEBUG_ ("reinit $%d\n", fd);
    audio_reinit_flag = false;
    audio_init_ (SIM_STATUS_INVISIBLE);
  }
  return ret ? (int) (long) ret : err;
}

static int audio_open (simclient client, int sample,
                       const PaStreamParameters *inparams, const PaStreamParameters *outparams) {
  simsocket sock = client ? client->sock : NULL;
  simbool proxy = client && ! (client->flags & CLIENT_FLAG_BUFFERING) && ! sock->udport && SOCKET_CHECK_PROXY (sock);
  const PaStreamParameters *input = inparams ? inparams : client->call.inparams;
  const PaStreamParameters *output = outparams ? outparams : client->call.outparams;
  int err, samples = param_get_number ("audio.samples"), tail;
  if (inparams && outparams && (audio_status.id || audio_status.client || sound_stream)) {
    LOG_WARN_ ("open %dHz error %d (@%020llu $%d %d)\n", audio_speex.rate, SIM_CALL_TALKING,
               audio_status.id, AUDIO_GET_CLIENT_FD (audio_status.client), sound_stream != 0);
    return SIM_CALL_TALKING;
  }
  audio_speex_close (&audio_speex);
  if ((err = audio_speex_open (&audio_speex, client, sample, proxy)) == SIM_OK) {
    samples = samples * audio_speex.samples / 160;
    err = Pa_OpenStream (&audio_stream, input, output, audio_speex.rate, samples, paClipOff, NULL, NULL);
  }
  if (err == SIM_OK) {
    const PaStreamInfo *info = Pa_GetStreamInfo (audio_stream);
    LOG_ANY_ (info->sampleRate == audio_speex.rate ? SIM_LOG_DEBUG : SIM_LOG_WARN, "open %gHz: %d samples (%g:%g ms)\n",
              info->sampleRate, samples, info->inputLatency * 1000, info->outputLatency * 1000);
    if (client && (tail = audio_get_tail (client)) > 0) {
      speex_echo_state_destroy (audio_speex.noecho);
      tail += (int) ((info->inputLatency > info->outputLatency ? info->inputLatency : info->outputLatency) * 1000);
      if ((audio_speex.noecho = speex_echo_state_init (audio_speex.samples, tail * audio_speex.rate / 1000)) != NULL) {
        speex_echo_ctl (audio_speex.noecho, SPEEX_ECHO_SET_SAMPLING_RATE, &audio_speex.rate);
        speex_preprocess_ctl (audio_speex.preproc, SPEEX_PREPROCESS_SET_ECHO_STATE, audio_speex.noecho);
        LOG_DEBUG_ ("speex %d ms = %d samples\n", tail, tail * audio_speex.rate / 1000);
      } else
        err = ENOMEM;
    }
    if (err == SIM_OK && (err = Pa_StartStream (audio_stream)) == SIM_OK && inparams && outparams) {
      audio_tick = -1;
      err = pth_thread_spawn (thread_audio_, NULL, &tid_audio, sock ? sock->fd : -2);
    }
    if (err == SIM_OK) {
      if ((int) info->sampleRate == audio_speex.rate) {
        if (client) {
          audio_send_param (client, inparams && outparams ? 0 : audio_speex.rate);
          client->call.inparams = input;
          client->call.outparams = output;
        }
        return SIM_OK;
      }
      err = SIM_CALL_NO_SAMPLE;
    }
    Pa_CloseStream (audio_stream);
  }
  audio_stream = NULL;
  LOG_WARN_ ("open %dHz error %d\n", audio_speex.rate, err);
  audio_speex_close (&audio_speex);
  return err;
}

int audio_reset (simclient client) {
  int err = SIM_OK, sample = client->call.sample ? abs (client->call.sample) : AUDIO_SAMPLE_CALL;
  if (audio_status.client == client && ! audio_reset_start_flag) {
    int frames = audio_speex.frames, quality = audio_speex.quality;
    if (audio_speex.speed)
      err = limit_send_speed (client, 0);
    audio_speex_reopen (&audio_speex, client, false);
    audio_speex.speed = 0;
    if (err == SIM_OK && (audio_speex.frames != frames || audio_speex.quality != quality))
      err = audio_send_param (client, -audio_speex.rate);
    if (err == SIM_OK && audio_speex.rate != sample && param_get_number ("audio.reopen")) {
      if ((err = client_send_cmd (client, SIM_CMD_REQUEST_CALL, SIM_CMD_REQUEST_CALL_SAMPLE, number_new (sample),
                                  NULL, nil ())) == SIM_OK)
        audio_reset_start_flag = true;
      LOG_XTRA_ ("udp $%d oldrate %d -> %d (reset)\n", client->sock->fd, audio_speex.oldrate, sample);
      audio_speex.oldrate = sample;
    } else if (err == SIM_OK)
      err = SIM_CALL_NO_SAMPLE;
  } else
    err = audio_status.client != client ? SIM_CALL_HANGUP : SIM_CALL_TALKING;
  LOG_INFO_ ("reopen %dHz (error %d)\n", sample, err);
  return err;
}

int audio_reopen_ (simclient client, simnumber sample, simbool reply) {
  int err = SIM_CALL_HANGUP;
  if (audio_status.client == client && param_get_number ("audio.reopen")) {
    int mysample = client->call.sample ? abs (client->call.sample) : AUDIO_SAMPLE_CALL;
    if (mysample > AUDIO_SAMPLE_PROXY)
      if (! (client->flags & CLIENT_FLAG_BUFFERING) && ! client->sock->udport && SOCKET_CHECK_PROXY (client->sock))
        mysample = AUDIO_SAMPLE_PROXY;
    if (sample == audio_speex.rate || sample != mysample) {
      err = sample == audio_speex.rate ? SIM_OK : sample ? SIM_CALL_BAD_SAMPLE : SIM_CALL_BAD_SOUND;
    } else if (! reply || audio_reset_start_flag) {
      if ((err = Pa_AbortStream (audio_stream)) != SIM_OK)
        LOG_WARN_ ("reopen stop error %d\n", err);
      if ((err = Pa_CloseStream (audio_stream)) != SIM_OK)
        LOG_WARN_ ("reopen close error %d\n", err);
      LOG_XTRA_ ("udp $%d oldrate %d -> %d (reopen)\n", client->sock->fd, audio_speex.oldrate, audio_speex.rate);
      audio_speex.oldrate = audio_speex.rate;
      audio_stream = NULL;
      if ((err = audio_open (client, mysample, NULL, NULL)) != SIM_OK) {
        param_set_number ("audio.reopen", 0, SIM_PARAM_TEMPORARY);
        audio_set_error_ (client, err);
      } else
        audio_reset_start_flag = true;
    } else
      err = SIM_CALL_TALKING;
    if (audio_reset_start_flag)
      audio_reset_stop_flag = true;
    audio_reset_start_flag = false;
  }
  LOG_INFO_ ("reopen %s %lldHz (error %d)\n", reply ? "reply" : "request", sample, err);
  if (reply) {
    err = SIM_OK;
  } else if (err == SIM_OK) {
    err = client_send_cmd (client, SIM_CMD_REPLY_CALL, SIM_CMD_REPLY_CALL_SAMPLE, number_new (sample), NULL, nil ());
  } else
    err = client_send_cmd (client, SIM_CMD_REPLY_CALL, NULL, nil (), NULL, nil ());
  return err;
}

int audio_send_udp (simclient client, simtype table, const char *cmd, unsigned ip, int port) {
  int err = SIM_OK, delta, sample = client->call.sample, len;
  simnumber sigsample, quality = client->call.quality, frames = client->call.frames, mtu = client->call.mtu;
  struct audio_speex speex;
  if (strcmp (cmd, SIM_CMD_UDP_REQUEST)) {
    if (string_check_diff (audio_get_params (client, table, &sigsample), SIM_CMD_CALL_CODEC_SPEEX)) {
      err = SIM_CALL_BAD_CODEC;
    } else if (sigsample < 0 ? sigsample > -AUDIO_SAMPLE_MIN || sigsample < -AUDIO_SAMPLE_MAX :
                               sigsample > 0 && (sigsample < AUDIO_SAMPLE_MIN || sigsample > AUDIO_SAMPLE_MAX)) {
      err = SIM_CALL_BAD_SAMPLE;
    } else
      client->call.sample = (int) sigsample;
  }
  memset (&speex, 0, sizeof (speex));
  if (err == SIM_OK && (err = audio_speex_open (&speex, client, client->call.sample, false)) == SIM_OK) {
    simnumber seq = client->flags & CLIENT_FLAG_ACCEPTED ? -1 : ((simnumber) 1 << 62) - 1;
    simtype arrays = audio_speex_new_packet (&speex, false, true), str;
    audio_set_params (table, client->call.sample);
    table_set_number (table, SIM_CMD_UDP_TIME, ++client->sndtick);
    table_set_pointer (table, SIM_CMD_UDP, cmd);
    table_set (table, SIM_CMD_UDP_DST_IP, string_copy (network_convert_ip (ip)));
    table_set_number (table, SIM_CMD_UDP_DST_PORT, port);
    table_set_pointer (table, SIM_CMD_UDP_PAD, "");
    random_get (random_public, str = string_new ((delta = type_size (arrays) - table_size (table, 0)) > 0 ? delta : 0));
    table_set (table, SIM_CMD_UDP_PAD, str);
    audio_free_packet (arrays);
    len = socket_size_table (client->sock, table, SOCKET_SEND_UDP | SOCKET_SEND_AUDIO);
    if ((err = client_send_udp (client, table, seq, ip, port)) == SIM_OK && (len -= SOCKET_OVERHEAD_SSL) >= 0) {
      LOG_DEBUG_ ("udp $%d send %dHz '%s' #%lld (%u bytes) to %s:%d '%s'\n", client->sock->fd, client->call.sample, cmd,
                  client->sndtick % speex.samples, len, network_convert_ip (ip), port, client->contact->nick.str);
      len += SOCKET_CALC_OVERHEAD_UDP ((unsigned) len);
      client->contact->stats[CONTACT_STATS_OUTPUT][CONTACT_STAT_SENT] += len;
    }
  } else {
    LOG_WARN_ ("udp %s error %d\n", cmd, err);
    table_free (table);
  }
  audio_speex_close (&speex);
  client->call.sample = sample;
  client->call.quality = quality;
  client->call.frames = frames;
  client->call.mtu = mtu;
  return err;
}

void audio_start_udp (simbool nat) {
  simclient client = audio_status.client;
  audio_udp_tick = 0;
  if (param_get_number ("net.tor.port") > 0 || (audio_udp_count = param_get_number ("client.udp")) == 0)
    return;
  audio_udp_tick = system_get_tick () + param_get_number ("nat.udp");
  if (! nat || nat_get_connected (client)) {
    nat_reverse_send_udp (client);
  } else if (param_get_number ("nat.udp")) {
    const simnumber seq = ((simnumber) 1 << 62) - 1;
    simcustomer proxy = proxy_get_proxy (NULL, NULL, NULL);
    simtype table = table_new (2);
    audio_udp_count++;
    table_add_pointer (table, SIM_CMD_UDP, SIM_CMD_UDP_REQUEST);
    if (socket_check_client (client->sock)) {
      const int bits = SOCKET_SEND_UDP | SOCKET_SEND_PROXY;
      table_add_number (table, SIM_CMD_UDP_TIME, ++client->sndtick);
      if (socket_send_udp (client->sock, table, seq, client->proxyip, client->proxyudport, bits, NULL) == SIM_OK)
        LOG_DEBUG_SIMTYPE_ (table, 0, "udp $%d send (%u bytes) to %s:%d ", client->sock->fd,
                            socket_size_table (client->sock, table, bits) - SOCKET_OVERHEAD_SSL,
                            network_convert_ip (client->proxyip), client->proxyudport);
    } else if (proxy) {
      table_add_number (table, SIM_CMD_UDP_TIME, ++proxy->sndtick);
      if (socket_send_udp (proxy->sock, table, seq, proxy->ip, proxy->proxyport, SOCKET_SEND_UDP, NULL) == SIM_OK)
        LOG_DEBUG_SIMTYPE_ (table, 0, "udp $%d send (%u bytes) to %s:%d ", proxy->sock->fd,
                            socket_size_table (proxy->sock, table, SOCKET_SEND_UDP) - SOCKET_OVERHEAD_SSL,
                            network_convert_ip (proxy->ip), proxy->proxyport);
    } else {
      int port = network_get_port (NULL);
      audio_status.udport = port ? port : abs (param_get_number ("main.port"));
      client_send_cmd (client, SIM_CMD_UDP, SIM_CMD_UDP_SRC_PORT, number_new (audio_status.udport), NULL, nil ());
      if (audio_status.udport && client->call.udport)
        audio_start_udp (false);
    }
    table_free (table);
  } else
    audio_udp_count = 0;
}

int audio_start (simclient client, int sample, int state, simbool call) {
  simnumber start = client->call.time;
  simtype table = nil ();
  client_set_param (client, sample, state);
  if (state == AUDIO_CALL_TALKING) {
    limit_send_speed (client, audio_speex.speed);
    audio_start_udp (true);
    event_send_history_system (client->contact, "CALL ", "START", 0, start, SIM_OK, ! call);
  }
  if (call) {
    audio_set_params (table = client_new_cmd (SIM_CMD_CALL, NULL, nil (), NULL, nil ()), sample);
  } else if (state == AUDIO_CALL_INCOMING)
    table = client_new_cmd (SIM_CMD_RING, NULL, nil (), NULL, nil ());
  return table.typ != SIMNIL ? client_send (client, table) : SIM_OK;
}

static int audio_start_call (simclient client, int sample, int state, simbool call) {
  int err = audio_start (client, sample, state, call);
  if (err == SIM_OK && state != AUDIO_CALL_TALKING) /* AUDIO_CALL_HANGUP not possible here: AUDIO_CALL_INCOMING or AUDIO_CALL_OUTGOING */
    if (socket_check_client (client->sock) && param_get_number ("nat.attempt") & 1)
      nat_traverse_attempt (client);
  return err;
}

int audio_call_ (simnumber id) {
  int err = SIM_CONTACT_UNKNOWN, state = AUDIO_CALL_HANGUP, sample = 0, sigsample, mysample;
  simbool reinit = false;
  simnumber seen = time (NULL), start = seen;
  simclient client;
  simcontact contact = contact_list_find_id (id);
  static PaStreamParameters audio_call_inparams, audio_call_outparams;
  if ((audio_invalid_flags[0] || audio_invalid_flags[1]) && audio_test_id)
    audio_init_ (SIM_STATUS_AWAY);
  if (id == CONTACT_ID_KEYGEN && (audio_invalid_flags[0] || audio_invalid_flags[1]))
    return SIM_CALL_INVALID_AUDIO;
  mysample = abs (sigsample = param_get_number ("audio.sample"));
  if (! contact || contact->auth < CONTACT_AUTH_NEW) {
    int *defsample = mysample && (id != CONTACT_ID_KEYGEN || sigsample < 0) ? NULL : &mysample;
    if (audio_test_id && (id == CONTACT_ID_KEYGEN || id == audio_test_id))
      if ((err = audio_close_ (true)) == SIM_OK)
        if ((err = audio_device_get_params (&audio_call_inparams, &audio_call_outparams, defsample)) == SIM_OK) {
          if ((err = audio_open (NULL, mysample, &audio_call_inparams, &audio_call_outparams)) == SIM_OK) {
            audio_set_status (AUDIO_CLIENT_TEST, id, NULL);
            event_send_audio (NULL, CONTACT_AUDIO_HANGUP, CONTACT_AUDIO_TALKING);
          } else if (err != SIM_CALL_TALKING && err != SIM_CALL_NO_SAMPLE)
            audio_init_ (SIM_STATUS_AWAY);
        }
    return err;
  }
  if (contact->msgs.flags & CONTACT_MSG_OVERFLOW)
    return SIM_MSG_OVERFLOW;
  if (! (contact->flags & CONTACT_FLAG_AUDIO) || ! CONTACT_CHECK_RIGHT_AUDIO (contact))
    return SIM_API_NO_RIGHT;
  if ((client = contact->client) != NULL)
    if ((sample = client->call.sample, start = client->call.time, state = client->call.state) == AUDIO_CALL_TALKING)
      return SIM_CALL_TALKING;
  MSG_UNSET_CALL (contact);
  client_acquire (client);
  LOG_DEBUG_ ("CALL '%s' %dHz %s\n", contact->nick.str, sigsample, audio_state_names[state]);
  err = SIM_OK;
  if (state == AUDIO_CALL_INCOMING && client && (err = audio_close_ (true)) == SIM_OK)
    if ((sample = client->call.sample, start = client->call.time, state = client->call.state) == AUDIO_CALL_TALKING) {
      client_release (client);
      return SIM_CALL_TALKING;
    }
  contact->seen[CONTACT_SEEN_ACTIVITY] = seen;
  if (err == SIM_OK) {
    if (state != AUDIO_CALL_INCOMING || ! client) {
      sample = mysample;
      state = AUDIO_CALL_OUTGOING;
      if (client)
        client->flags &= ~CLIENT_FLAG_RINGING;
    } else if (sample >= 0 || sigsample >= 0 || -sample == mysample) {
      if (sample >= 0 && (! sample || (mysample && (sigsample <= 0 || sample > mysample))))
        sample = mysample ? mysample : AUDIO_SAMPLE_CALL;
      if ((err = audio_device_get_params (&audio_call_inparams, &audio_call_outparams, NULL)) == SIM_OK) {
        if ((err = audio_open (client, sample, &audio_call_inparams, &audio_call_outparams)) == SIM_OK) {
          state = audio_set_status (client_acquire (client), contact->id, "HUNGUP");
        } else if (err != SIM_CALL_TALKING && err != SIM_CALL_NO_SAMPLE)
          reinit = true;
      }
    } else
      err = SIM_CALL_BAD_SAMPLE;
    if (sigsample < 0 && sample > 0)
      sample = -sample;
  }
  if (err != SIM_OK) {
    LOG_WARN_ ("CALL '%s' error %d\n", contact->nick.str, err);
    if (client)
      event_test_error_audio (contact->id, audio_hangup_ (client, err));
    if (reinit)
      audio_init_ (SIM_STATUS_AWAY);
  } else if (client) {
    /*err =*/audio_start_call (client, sample, state, true);
  } else if ((err = msg_connect (contact, true)) == SIM_OK) {
    MSG_SET_CALL (contact, seen, sample);
    event_send_audio (contact, CONTACT_AUDIO_HANGUP, CONTACT_AUDIO_CALLING);
  }
  client_release (client);
  if (err != SIM_OK) {
    const char *text = state == AUDIO_CALL_INCOMING || state == AUDIO_CALL_TALKING ? "BUSY" : "ERROR";
    event_send_history_system (contact, "CALL ", text, 0, start, err, true);
  }
  return err;
}

int audio_ring_ (simclient client, const simtype table) {
  simnumber start = client->call.time, sigsample, echo = table_get_number (table, SIM_CMD_CALL_ECHO);
  simtype codec = audio_get_params (client, table, &sigsample);
  int sample, mysample = client->call.sample, state = client->call.state, oldstate = state, err = SIM_OK;
  if (sigsample >= 0) {
    sample = sigsample >= 999999 ? 999999 : (int) sigsample;
  } else
    sample = sigsample <= -999999 ? -999999 : -(int) sigsample;
  LOG_DEBUG_ ("'%s' CALL %s %lldHz (quality = %lld, MTU = %lld, tail = %lld) %lld ms %s\n",
              client->contact->nick.str, codec.str, sigsample, client->call.quality, client->call.mtu,
              echo, client->call.frames, audio_state_names[state]);
  if (string_check_diff (codec, SIM_CMD_CALL_CODEC_SPEEX)) {
    LOG_WARN_ ("codec %s unknown '%s'\n", codec.str, client->contact->nick.str);
    event_test_error_audio (client->contact->id, audio_hangup_ (client, err = SIM_CALL_BAD_CODEC));
  } else if (! sample) {
    if ((sample = abs ((int) (sigsample = param_get_number ("audio.sample")))) == 0)
      sigsample = sample = AUDIO_SAMPLE_CALL;
  } else if (sample < AUDIO_SAMPLE_MIN || sample > AUDIO_SAMPLE_MAX) {
    LOG_WARN_ ("sample rate %d invalid\n", sample);
    event_test_error_audio (client->contact->id, audio_hangup_ (client, err = SIM_CALL_BAD_SAMPLE));
  }
  if (err == SIM_OK) {
    if (state == AUDIO_CALL_OUTGOING && (err = audio_close_ (true)) == SIM_OK)
      mysample = client->call.sample, start = client->call.time, state = client->call.state;
    if (state == AUDIO_CALL_TALKING) {
      LOG_NOTE_ ("CALL '%s' SKIPPED\n", client->contact->nick.str);
      event_test_error_audio (client->contact->id, audio_hangup_ (client, err = SIM_CALL_TALKING));
    }
  }
  client->contact->seen[CONTACT_SEEN_ACTIVITY] = time (NULL);
  if (err == SIM_OK) {
    if (state == AUDIO_CALL_OUTGOING) {
      static PaStreamParameters audio_ring_inparams, audio_ring_outparams;
      if (mysample < 0 && sigsample < 0 && mysample != sigsample) {
        LOG_WARN_ ("sample rate %d invalid (wanted %d)\n", sample, -mysample);
        err = SIM_CALL_BAD_SAMPLE;
      } else if ((err = audio_device_get_params (&audio_ring_inparams, &audio_ring_outparams, NULL)) == SIM_OK) {
        sample = ! mysample || sigsample < 0 || sample < mysample ? sample : (int) (sigsample = abs (mysample));
        err = audio_open (client, sample, &audio_ring_inparams, &audio_ring_outparams);
      }
      if (err != SIM_OK) {
        event_test_error_audio (client->contact->id, audio_hangup_ (client, err));
        if (err != SIM_CALL_TALKING && err != SIM_CALL_NO_SAMPLE && err != SIM_CALL_BAD_SAMPLE)
          audio_init_ (SIM_STATUS_AWAY);
      } else
        state = audio_set_status (client_acquire (client), client->contact->id, "HANGUP");
    } else if (mysample >= 0 || sigsample >= 0 || mysample == sigsample) {
      state = AUDIO_CALL_INCOMING;
    } else
      event_test_error_audio (client->contact->id, audio_hangup_ (client, err = SIM_CALL_BAD_SAMPLE));
  }
  if (err != SIM_OK) {
    const char *text = oldstate == AUDIO_CALL_OUTGOING ? "ERROR" : "BUSY";
    event_send_history_system (client->contact, "CALL ", text, 0, start, err, true);
  } else if (audio_start_call (client, (int) sigsample, state, false) == SIM_OK) {
    if (client->contact->flags & CONTACT_FLAG_ECHO_Y && ! echo)
      event_test_error_audio (client->contact->id, SIM_CALL_NO_ECHO_CANCELLER);
    if (state == AUDIO_CALL_INCOMING && param_get_number ("audio.answer"))
      audio_call_ (client->contact->id);
  }
  return SIM_OK;
}

int audio_stop_ (simclient client, int error, simbool hangup) {
  simnumber start = client->call.time;
  int err, state = client->call.state;
  const char *text = audio_status.hangup;
  if (state == AUDIO_CALL_HANGUP) {
    if (! hangup || error == SIM_OK)
      LOG_NOTE_ ("HANGUP%d '%s' SKIPPED (error %d) %s\n", hangup,
                 client->contact->nick.str, error, audio_state_names[state]);
    return SIM_CALL_HANGUP;
  }
  MSG_UNSET_CALL (client->contact);
  client_set_param (client, 0, AUDIO_CALL_HANGUP);
  LOG_DEBUG_ ("'%s' HANGUP%d (error %d) %s\n", client->contact->nick.str, hangup, error, audio_state_names[state]);
  if (! hangup) {
    if (state == AUDIO_CALL_INCOMING) {
      text = "ABORT";
      error = SIM_OK;
    } else if (state == AUDIO_CALL_OUTGOING) {
      text = "BUSY";
    } else
      audio_status.hangup = NULL;
    if (text)
      client->contact->seen[CONTACT_SEEN_ACTIVITY] = time (NULL);
  } else if (error == SIM_OK) {
    if (state != AUDIO_CALL_TALKING) {
      text = state == AUDIO_CALL_OUTGOING ? "ABORT" : "BUSY";
    } else
      audio_status.hangup = NULL;
  } else
    text = NULL;
  err = state == AUDIO_CALL_TALKING ? audio_close_ (false) : SIM_OK;
  if (text)
    event_send_history_system (client->contact, "CALL ", text, 0, start, error, hangup);
  return err;
}

int audio_hangup_ (simclient client, int error) {
  int err;
  if (! client && ((client = audio_status.client) == NULL || client == AUDIO_CLIENT_TEST)) {
    LOG_NOTE_ ("HANGUP SKIPPED\n");
    return SIM_CALL_HANGUP;
  }
  if ((err = audio_stop_ (client_acquire (client), error, true)) != SIM_CALL_HANGUP || error != SIM_OK) {
    int e = (error != SIM_OK ? error : err) > 0 ? SIM_NO_ERROR : error != SIM_OK ? error : err;
    error = client_send_cmd (client, SIM_CMD_HANGUP, SIM_CMD_HANGUP_ERROR, number_new (e), NULL, nil ());
    if (error == SIM_OK) {
      simtype tmp = table_new (1);
      error = server_exec_ping (client, tmp);
      table_free (tmp);
    }
    if (error == SIM_OK) {
      LOG_DEBUG_ ("HANGUP '%s'\n", client->contact->nick.str);
    } else
      LOG_DEBUG_ ("HANGUP '%s' (error %d)\n", client->contact->nick.str, error);
  }
  client_release (client);
  return err;
}

int audio_init_ (int status) {
  int err = SIM_OK, on = audio_device_count () != 0;
  if (status == SIM_STATUS_IDLE && system_get_tick () - audio_device_tick < SIM_AUDIO_DEVICE_SKIP)
    return SIM_OK;
  if (status != SIM_STATUS_ON) {
    if (audio_status.client || audio_status.id) {
      /*event_test_net_device (SIM_STATUS_AWAY); // makes no sense as portaudio won't re-read the device list from the system */
      LOG_DEBUG_ ("reinit $%d @%020llu\n", AUDIO_GET_CLIENT_FD (audio_status.client), audio_status.id);
      audio_reinit_flag = true;
      return SIM_OK;
    }
    audio_uninit_ (true);
    if (event_speech_table.typ == SIMNIL)
      return SIM_OK;
  } else {
    event_speech_table = table_new_const_name (4, SIM_EVENT_SPEECH, NULL);
    event_new_speech (SIM_EVENT_SPEECH_END, 100, SIM_OK);
    random_buffer_init (random_private);
    random_buffer_init (random_session);
  }
  if (! audio_test_id) {
#if HAVE_ALSA
    extern PaError PaAlsa_SetRetriesBusy (int);
    extern int PaAlsa_SetNumPeriods (int);
    int retries = param_get_number ("audio.alsa.retries"), periods = param_get_number ("audio.alsa.periods");
    int devices = param_get_number ("audio.alsa.devices"), mmaps = param_get_number ("audio.alsa.mmap");
    if (retries >= 0)
      PaAlsa_SetRetriesBusy (retries);
    if (periods)
      PaAlsa_SetNumPeriods (periods);
    if (putenv ((char *) (devices & 4 ? "PA_ALSA_IGNORE_ALL_PLUGINS=1" : "PA_ALSA_IGNORE_ALL_PLUGINS=0")))
      err = errno ? errno : ENOMEM;
    if (err == SIM_OK && putenv ((char *) (devices & 1 ? "PA_ALSA_PLUGHW=1" : "PA_ALSA_PLUGHW=0")))
      err = errno ? errno : ENOMEM;
    if (err == SIM_OK && putenv ((char *) (mmaps ? "PA_ALSA_NO_MMAP=0" : "PA_ALSA_NO_MMAP=1")))
      err = errno ? errno : ENOMEM;
#endif
    if (err == SIM_OK && (err = Pa_Initialize ()) == SIM_OK) {
      audio_test_id = CONTACT_ID_TEST;
      sound_count_table = table_new_const (78, NULL);
      sound_thread_table = table_new_const (78, NULL);
      LOG_DEBUG_ ("init\n");
      event_test_net_device (status);
    } else
      LOG_ERROR_ ("init error %d\n", err);
  }
  if (status == SIM_STATUS_ON) {
    system_pnp_start ();
  } else if ((audio_device_count () != 0) != on) {
    simtype event = table_new_name (2, SIM_EVENT_STATUS);
    table_add_number (event, SIM_EVENT_STATUS, on ? SIM_STATUS_ON : SIM_STATUS_OFF);
    if (audio_device_count ()) {
      status = SIM_STATUS_ON;
    } else if (status == SIM_STATUS_IDLE || status == SIM_STATUS_AWAY)
      status = SIM_STATUS_INVISIBLE;
    table_add_number (event, CONTACT_KEY_STATUS, status);
    event_send_id (CONTACT_ID_TEST, event);
    table_free (event);
  }
  return err;
}

int audio_uninit_ (simbool reinit) {
  int err = SIM_OK;
  LOG_DEBUG_ ("uninit\n");
  if (! reinit)
    system_pnp_stop_ ();
  audio_reinit_flag = false;
  audio_stop_sound_ (NULL);
  audio_close_ (false);
  if (audio_test_id) {
    audio_test_id = 0;
    table_free (sound_count_table), sound_count_table = nil ();
    table_free (sound_thread_table), sound_thread_table = nil ();
    if ((err = Pa_Terminate ()) != SIM_OK)
      LOG_ERROR_ ("uninit error %d\n", err);
  }
  if (! reinit) {
    random_buffer_close (random_session);
    random_buffer_close (random_private);
    if (event_speech_table.ptr)
      table_free (event_speech_table);
    event_speech_table = nil ();
  }
  return err;
}

void audio_log_sounds (const char *module, int level) {
  simtype names = table_copy (sound_count_table, sound_count_table.len);
  const simbyte ***sounds = sound_data;
  while (*sounds)
    table_add_pointer (names, *(const char **) *sounds++, "");
  log_simtype_ (module, level, names, 0, " ");
  table_free (names);
  if (table_count (sound_thread_table))
    log_simtype_ (module, level, sound_thread_table, LOG_BIT_HEX, " ");
}

void audio_log_devices (const char *module, int level) {
  int i = -1, inchans = 0, outchans = 0, types;
  simtype tmp;
  while ((tmp = audio_device_get (++i, &inchans, &outchans, &types)).typ != SIMNIL) {
    log_any_ (module, level, "%c%d (%s%d:%s%d)\t %s\n", types & AUDIO_TYPE_VIRTUAL ? '-' : ' ', i,
              types & AUDIO_TYPE_INPUT ? "+" : "", inchans, types & AUDIO_TYPE_OUTPUT ? "+" : "", outchans, tmp.str);
    string_free (tmp);
  }
}

#else

int audio_get_param (int param) { return param != AUDIO_PARAM_RATE ? 0 : 1 - AUDIO_SAMPLE_MIN; }
simtype audio_get_codecs (void) { return pointer_new (""); }
int audio_device_count (void) { return 0; }
simtype audio_device_get (int device, int *inchannels, int *outchannels, int *types) { return nil (); }
int audio_start_sound (const char *sound, int repeat) { return SIM_CALL_NO_AUDIO; }
int audio_stop_sound_ (const char *sound) { return SIM_CALL_BAD_SOUND; }
void event_test_net_device (int status) {}
void event_test_error_audio (simnumber id, int error) {}
void audio_recv_packet (simtype data, simnumber timestamp) {}
void audio_free_packet (simtype arrays) { type_free (arrays); }
int audio_close_ (simbool hangup) { return SIM_OK; }
int audio_reset (simclient client) { return SIM_OK; }
int audio_reopen_ (simclient client, simnumber sample, simbool reply) { return SIM_OK; }
int audio_send_udp (simclient client, simtype table, const char *cmd, unsigned ip, int port) { return SIM_OK; }
void audio_start_udp (simbool nat) {}
int audio_start (simclient client, int sample, int state, simbool call) { return SIM_OK; }
int audio_call_ (simnumber id) { return SIM_CALL_NO_AUDIO; }
int audio_ring_ (simclient client, const simtype table) { return SIM_OK; }
int audio_stop_ (simclient client, int error, simbool hangup) { return SIM_CALL_HANGUP; }
int audio_hangup_ (simclient client, int error) { return SIM_OK; }
int audio_init_ (int status) { return SIM_OK; }
int audio_uninit_ (simbool reinit) { return SIM_OK; }
void audio_log_sounds (const char *module, int level) {}
void audio_log_devices (const char *module, int level) {}

#endif /* HAVE_LIBSPEEX */
