Import existing source tree; original VCS history is no longer available. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
712 lines
15 KiB
C
712 lines
15 KiB
C
/*(LGPL)
|
|
---------------------------------------------------------------------------
|
|
a_voice.c - Audio Engine low level mixer voices
|
|
---------------------------------------------------------------------------
|
|
* Copyright (C) 2001-2003, David Olofson
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation; either version 2.1 of the License, or (at
|
|
* your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with this program; if not, write to the Free Software Foundation,
|
|
* Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
*/
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <math.h>
|
|
|
|
#include "kobolog.h"
|
|
#include "a_voice.h"
|
|
#include "a_struct.h"
|
|
#include "a_globals.h"
|
|
#include "a_tools.h"
|
|
#include "a_control.h"
|
|
|
|
#define LDBG(x)
|
|
#define EVDBG(x)
|
|
#define CHECKPOINTS
|
|
|
|
|
|
/* Random number generator state for randtrig etc */
|
|
static int rnd = 16576;
|
|
#define UPDATE_RND rnd *= 1566083941UL; rnd++; rnd &= 0x7fffffffUL;
|
|
|
|
/* Last allocated voice (good starting point!) */
|
|
static int last_voice = 0;
|
|
|
|
|
|
void voice_kill(audio_voice_t *v)
|
|
{
|
|
v->vu = 65535; /* Newly allocated voices are harder to steal */
|
|
aev_flush(&v->port);
|
|
if(v->channel)
|
|
{
|
|
--v->channel->playing;
|
|
chan_unlink_voice(v);
|
|
}
|
|
v->state = VS_STOPPED;
|
|
}
|
|
|
|
|
|
int voice_alloc(audio_channel_t *c)
|
|
{
|
|
int lv = 0;
|
|
int i, pri, v, vol;
|
|
|
|
/* Pass 1: Look for an unused voice. */
|
|
for(v = 0; v < AUDIO_MAX_VOICES; ++v)
|
|
{
|
|
if(voicetab[v].state != VS_STOPPED)
|
|
continue; /* Not interesting here... */
|
|
last_voice = v;
|
|
chan_link_voice(c, &voicetab[v]);
|
|
voicetab[v].priority = c->ctl[ACC_PRIORITY];
|
|
voicetab[v].state = VS_RESERVED;
|
|
return v;
|
|
}
|
|
|
|
/*
|
|
* Pass 2: Look for the most silent voice with
|
|
* same or lower priority.
|
|
*/
|
|
lv = last_voice;
|
|
vol = 2000000000;
|
|
v = -1;
|
|
for(i = 0; i < AUDIO_MAX_VOICES; ++i)
|
|
{
|
|
int vu;
|
|
if(++lv >= AUDIO_MAX_VOICES)
|
|
lv = 0;
|
|
if(voicetab[lv].priority < c->ctl[ACC_PRIORITY])
|
|
continue;
|
|
#ifdef AUDIO_USE_VU
|
|
vu = voicetab[lv].vu;
|
|
vu *= (voicetab[lv].ic[VIC_LVOL].v +
|
|
voicetab[lv].ic[VIC_RVOL].v +
|
|
voicetab[lv].ic[VIC_LSEND].v +
|
|
voicetab[lv].ic[VIC_RSEND].v) >> (RAMP_BITS + 2);
|
|
vu >>= VOL_BITS;
|
|
#else
|
|
vu = (voicetab[lv].ic[VIC_LVOL].v +
|
|
voicetab[lv].ic[VIC_RVOL].v +
|
|
voicetab[lv].ic[VIC_LSEND].v +
|
|
voicetab[lv].ic[VIC_RSEND].v) >> (RAMP_BITS + 2);
|
|
#endif
|
|
if(vu < vol)
|
|
{
|
|
vol = vu;
|
|
v = lv;
|
|
}
|
|
}
|
|
if(v >= 0)
|
|
{
|
|
voice_kill(&voicetab[v]);
|
|
chan_link_voice(c, &voicetab[v]);
|
|
last_voice = v;
|
|
voicetab[v].priority = c->ctl[ACC_PRIORITY];
|
|
voicetab[v].state = VS_RESERVED;
|
|
return v;
|
|
}
|
|
|
|
/* Pass 3: Grab voice with lowest priority. */
|
|
lv = last_voice;
|
|
pri = c->ctl[ACC_PRIORITY];
|
|
v = -1;
|
|
for(i = 0; i < AUDIO_MAX_VOICES; ++i)
|
|
{
|
|
if(++lv >= AUDIO_MAX_VOICES)
|
|
lv = 0;
|
|
if(voicetab[lv].priority > pri)
|
|
{
|
|
pri = voicetab[lv].priority;
|
|
v = lv;
|
|
}
|
|
}
|
|
if(v >= 0)
|
|
{
|
|
voice_kill(&voicetab[v]);
|
|
chan_link_voice(c, &voicetab[v]);
|
|
last_voice = v;
|
|
voicetab[v].priority = c->ctl[ACC_PRIORITY];
|
|
voicetab[v].state = VS_RESERVED;
|
|
return v;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
|
|
void voice_start(audio_voice_t *v, int wid)
|
|
{
|
|
int retrig, randtrig;
|
|
|
|
v->wave = v->c[VC_WAVE] = wid;
|
|
v->c[VC_LOOP] = wavetab[wid].looped;
|
|
|
|
v->position = 0;
|
|
v->position_frac = 0;
|
|
if(!wavetab[wid].data.si8)
|
|
{
|
|
voice_kill(v);
|
|
return;
|
|
}
|
|
|
|
/* Set up retrig and looping */
|
|
randtrig = (int)v->c[VC_RANDTRIG];
|
|
retrig = (int)v->c[VC_RETRIG];
|
|
if(randtrig)
|
|
{
|
|
UPDATE_RND
|
|
randtrig = rnd % (randtrig<<1) - randtrig;
|
|
randtrig = retrig * randtrig >> 16;
|
|
retrig += randtrig;
|
|
}
|
|
|
|
if(retrig > 0)
|
|
{
|
|
if((unsigned)retrig > wavetab[wid].play_samples)
|
|
v->section_end = wavetab[wid].play_samples;
|
|
else
|
|
v->section_end = (unsigned)retrig;
|
|
}
|
|
else
|
|
v->section_end = wavetab[wid].play_samples;
|
|
|
|
/* Start voice! */
|
|
v->state = VS_PLAYING;
|
|
}
|
|
|
|
|
|
static inline int __handle_looping(audio_voice_t *v)
|
|
{
|
|
unsigned int samples = wavetab[v->c[VC_WAVE]].play_samples;
|
|
int randtrig = v->c[VC_RANDTRIG];
|
|
int retrig = v->c[VC_RETRIG];
|
|
|
|
/* Latch (new) waveform index */
|
|
v->wave = v->c[VC_WAVE];
|
|
|
|
if(randtrig)
|
|
{
|
|
UPDATE_RND
|
|
randtrig = rnd % (randtrig<<1) - randtrig;
|
|
randtrig = retrig * randtrig >> 16;
|
|
retrig += randtrig;
|
|
}
|
|
|
|
if(retrig > 0)
|
|
{
|
|
unsigned int old_se = v->section_end;
|
|
|
|
if((unsigned)retrig > samples)
|
|
v->section_end = samples;
|
|
else
|
|
v->section_end = (unsigned)retrig;
|
|
|
|
if(old_se > v->position)
|
|
{
|
|
/* Force instant initial retrig */
|
|
v->position = 0;
|
|
v->position_frac = 0;
|
|
}
|
|
else
|
|
{
|
|
/* Wrap loop */
|
|
if(v->position >= old_se)
|
|
v->position = 0;
|
|
else
|
|
v->position -= old_se;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if(v->c[VC_LOOP])
|
|
{
|
|
v->position -= v->section_end;
|
|
v->section_end = samples;
|
|
}
|
|
else
|
|
return 0; /* Stop playing! */
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
|
|
void voice_check_retrig(audio_voice_t *v)
|
|
{
|
|
if(wavetab[v->wave].data.si8)
|
|
{
|
|
int retrig_max = v->c[VC_RETRIG] * v->c[VC_RANDTRIG] >> 16;
|
|
retrig_max += v->c[VC_RETRIG];
|
|
if(v->position > (unsigned)retrig_max)
|
|
__handle_looping(v);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Macro Mayhem! Create all the mixer variants...
|
|
*/
|
|
|
|
static inline void __mix_m8(audio_voice_t *v, int *out, unsigned frames)
|
|
{
|
|
#undef __SEND
|
|
#undef __STEREO
|
|
#undef __16BIT
|
|
#include "a_mixers.h"
|
|
}
|
|
|
|
static inline void __mix_s8(audio_voice_t *v, int *out, unsigned frames)
|
|
{
|
|
#undef __SEND
|
|
#define __STEREO
|
|
#undef __16BIT
|
|
#include "a_mixers.h"
|
|
}
|
|
|
|
static inline void __mix_m16(audio_voice_t *v, int *out, unsigned frames)
|
|
{
|
|
#undef __SEND
|
|
#undef __STEREO
|
|
#define __16BIT
|
|
#include "a_mixers.h"
|
|
}
|
|
|
|
static inline void __mix_s16(audio_voice_t *v, int *out, unsigned frames)
|
|
{
|
|
#undef __SEND
|
|
#define __STEREO
|
|
#define __16BIT
|
|
#include "a_mixers.h"
|
|
}
|
|
|
|
|
|
static inline void __mix_m8d(audio_voice_t *v, int *out, int *sout, unsigned frames)
|
|
{
|
|
#define __SEND
|
|
#undef __STEREO
|
|
#undef __16BIT
|
|
#include "a_mixers.h"
|
|
}
|
|
|
|
static inline void __mix_s8d(audio_voice_t *v, int *out, int *sout, unsigned frames)
|
|
{
|
|
#define __SEND
|
|
#define __STEREO
|
|
#undef __16BIT
|
|
#include "a_mixers.h"
|
|
}
|
|
|
|
static inline void __mix_m16d(audio_voice_t *v, int *out, int *sout, unsigned frames)
|
|
{
|
|
#define __SEND
|
|
#undef __STEREO
|
|
#define __16BIT
|
|
#include "a_mixers.h"
|
|
}
|
|
|
|
static inline void __mix_s16d(audio_voice_t *v, int *out, int *sout, unsigned frames)
|
|
{
|
|
#define __SEND
|
|
#define __STEREO
|
|
#define __16BIT
|
|
#include "a_mixers.h"
|
|
}
|
|
|
|
#undef __SEND
|
|
#undef __STEREO
|
|
#undef __16BIT
|
|
|
|
|
|
/*
|
|
* Calculates resampling input "step", and selects resampling mode.
|
|
*/
|
|
static inline unsigned int __calc_step(audio_voice_t *v)
|
|
{
|
|
audio_resample_t mode = AR_LINEAR;
|
|
/* Resampling factor */
|
|
int pitch = v->c[VC_PITCH];
|
|
unsigned step = (unsigned)fixmul(ptab_convert(pitch),
|
|
wavetab[v->wave].speed);
|
|
#if (FREQ_BITS < 16)
|
|
step >>= 16 - FREQ_BITS;
|
|
#elif (FREQ_BITS > 16)
|
|
step <<= FREQ_BITS - 16;
|
|
#endif
|
|
|
|
/*
|
|
* We must prevent high pithes from locking the mixer in
|
|
* an infinite loop with short looped waveforms...
|
|
*/
|
|
if(step > MAX_STEP)
|
|
{
|
|
#ifdef DEBUG
|
|
log_printf(ELOG, "Too high pitch!\n");
|
|
#endif
|
|
while(step > MAX_STEP)
|
|
step >>= 1;
|
|
}
|
|
|
|
switch(a_settings.quality)
|
|
{
|
|
case AQ_VERY_LOW:
|
|
mode = AR_NEAREST;
|
|
break;
|
|
case AQ_LOW:
|
|
mode = AR_NEAREST_4X;
|
|
break;
|
|
case AQ_NORMAL:
|
|
mode = AR_LINEAR;
|
|
break;
|
|
case AQ_HIGH:
|
|
/* Select resampling method based on in/out ratio */
|
|
if(step > (unsigned)(6 << FREQ_BITS))
|
|
mode = AR_LINEAR_8X_R; /* Above 6:1 */
|
|
else if(step > (unsigned)(3 << FREQ_BITS))
|
|
mode = AR_LINEAR_4X_R; /* Above 3:1 */
|
|
else
|
|
mode = AR_LINEAR_2X_R; /* Below 2:1 */
|
|
break;
|
|
case AQ_VERY_HIGH:
|
|
/* Select resampling method based on in/out ratio */
|
|
if(step > (unsigned)(4 << FREQ_BITS))
|
|
mode = AR_LINEAR_16X_R; /* Above 4:1 */
|
|
else if(step > (unsigned)(3 << FREQ_BITS))
|
|
mode = AR_LINEAR_8X_R; /* Above 3:1 */
|
|
else if(step > (unsigned)(2 << FREQ_BITS))
|
|
mode = AR_LINEAR_4X_R; /* Above 2:1 */
|
|
else if(step > (unsigned)(3 << (FREQ_BITS-1)))
|
|
mode = AR_LINEAR_2X_R; /* Above 1.5:1 */
|
|
else
|
|
mode = AR_CUBIC_R; /* Below 1.5:1 */
|
|
break;
|
|
}
|
|
v->c[VC_RESAMPLE] = mode;
|
|
return step;
|
|
}
|
|
|
|
|
|
/*
|
|
* Calculates # of output frames to the nearest of 'frames',
|
|
* end of segment and the "fragment span limit".
|
|
*/
|
|
static inline unsigned int __endframes(audio_voice_t *v, unsigned int frames)
|
|
{
|
|
#ifdef A_USE_INT64
|
|
Uint64 n, n2;
|
|
#else
|
|
double n, n2;
|
|
#endif
|
|
if(!v->step)
|
|
return frames;
|
|
|
|
#ifdef A_USE_INT64
|
|
n = ((Uint64)(v->position) << 32) | (Uint64)(v->position_frac);
|
|
n >>= 32 - FREQ_BITS;
|
|
n = ((Uint64)(v->section_end) << FREQ_BITS) - n + v->step - 1;
|
|
n /= v->step;
|
|
#else
|
|
n = (double)(v->position) + (double)(v->position_frac) / 4294967296.0;
|
|
n = (double)(v->section_end) - n;
|
|
n /= (double)v->step / (double)(1<<FREQ_BITS);
|
|
n = ceil(n);
|
|
#endif
|
|
if(n > 0xffffffff)
|
|
n = 0xffffffff;
|
|
#ifdef A_USE_INT64
|
|
if(n > (Uint64)frames)
|
|
n = (Uint64)frames;
|
|
#else
|
|
if(n > (double)frames)
|
|
n = (double)frames;
|
|
#endif
|
|
/*
|
|
* Restrict fragment size to prevent read position overflows.
|
|
*
|
|
* (In order to maximize pitch accuracy, voice mixers can only
|
|
* handle a very limited number of input samples without
|
|
* recalculating their "base pointers".)
|
|
*/
|
|
#ifdef A_USE_INT64
|
|
n2 = (Uint64)MAX_FRAGMENT_SPAN << FREQ_BITS;
|
|
n2 /= (Uint64)v->step * (Uint64)frames;
|
|
#else
|
|
n2 = (double)MAX_FRAGMENT_SPAN * (1 << FREQ_BITS);
|
|
n2 /= (double)v->step * (double)frames;
|
|
#endif
|
|
if(n > n2)
|
|
n = n2;
|
|
|
|
#ifdef CHECKPOINTS
|
|
if(!frames)
|
|
{
|
|
voice_kill(v);
|
|
log_printf(ELOG, "Voice locked up! (Too high pitch "
|
|
"resulted in zero fragment size.)\n");
|
|
}
|
|
#endif
|
|
return (unsigned int)n;
|
|
}
|
|
|
|
|
|
static inline void __fragment_single(audio_voice_t *v, int *out,
|
|
unsigned int frames)
|
|
{
|
|
switch(wavetab[v->wave].format)
|
|
{
|
|
case AF_MONO8:
|
|
__mix_m8(v, out, frames);
|
|
break;
|
|
case AF_STEREO8:
|
|
__mix_s8(v, out, frames);
|
|
break;
|
|
case AF_MONO16:
|
|
__mix_m16(v, out, frames);
|
|
break;
|
|
case AF_STEREO16:
|
|
__mix_s16(v, out, frames);
|
|
break;
|
|
case AF_MONO32:
|
|
/*__mix_m32(v, out, frames);*/
|
|
break;
|
|
case AF_STEREO32:
|
|
/*__mix_s32(v, out, frames);*/
|
|
case AF_MIDI: /* warning eliminator */
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
static inline void __fragment_double(audio_voice_t * v, int *out, int *sout,
|
|
unsigned int frames)
|
|
{
|
|
switch(wavetab[v->wave].format)
|
|
{
|
|
case AF_MONO8:
|
|
__mix_m8d(v, out, sout, frames);
|
|
break;
|
|
case AF_STEREO8:
|
|
__mix_s8d(v, out, sout, frames);
|
|
break;
|
|
case AF_MONO16:
|
|
__mix_m16d(v, out, sout, frames);
|
|
break;
|
|
case AF_STEREO16:
|
|
__mix_s16d(v, out, sout, frames);
|
|
break;
|
|
case AF_MONO32:
|
|
/*__mix_m32d(v, out, sout, frames);*/
|
|
break;
|
|
case AF_STEREO32:
|
|
/*__mix_s32d(v, out, sout, frames);*/
|
|
case AF_MIDI: /* warning eliminator */
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Figure out if we should use the "double output" mixers,
|
|
* and where to connect the output(s).
|
|
*/
|
|
static inline int __setup_output(audio_voice_t *v)
|
|
{
|
|
int prim, send;
|
|
/*
|
|
FIXME: This "automatic routing optimization" isn't needed,
|
|
FIXME: and causes trouble elsewhere. Simplify.
|
|
*/
|
|
v->fx1 = v->c[VC_PRIM_BUS];
|
|
v->fx2 = v->c[VC_SEND_BUS];
|
|
prim = (v->fx1 >= 0) && (v->fx1 < AUDIO_MAX_BUSSES);
|
|
send = (v->fx2 >= 0) && (v->fx2 < AUDIO_MAX_BUSSES);
|
|
if(!prim && !send)
|
|
return -1; /* No busses selected! --> */
|
|
|
|
if(prim && send)
|
|
v->use_double = (v->fx1 != v->fx2);
|
|
else
|
|
{
|
|
if(send)
|
|
v->fx1 = v->fx2;
|
|
else
|
|
v->fx2 = v->fx1;
|
|
v->use_double = 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
void voice_process_mix(audio_voice_t *v, int *busses[], unsigned frames)
|
|
{
|
|
unsigned s, frag_s;
|
|
|
|
if((VS_STOPPED == v->state) && (aev_next(&v->port, 0) > frames))
|
|
return; /* Stopped, and no events for this buffer --> */
|
|
|
|
/* Loop until buffer is full, or the voice is "dead". */
|
|
s = 0;
|
|
while(frames)
|
|
{
|
|
unsigned frag_frames;
|
|
while( !(frag_frames = aev_next(&v->port, s)) )
|
|
{
|
|
aev_event_t *ev = aev_read(&v->port);
|
|
switch(ev->type)
|
|
{
|
|
case VE_START:
|
|
voice_start(v, ev->arg1);
|
|
if(VS_STOPPED == v->state)
|
|
{
|
|
aev_free(ev);
|
|
return; /* Error! --> */
|
|
}
|
|
/*
|
|
* NOTE:
|
|
* This being checked here means that
|
|
* it's not possible to change routing
|
|
* during playback. Who would, anyway?
|
|
*/
|
|
if(__setup_output(v) < 0)
|
|
{
|
|
voice_kill(v);
|
|
aev_free(ev);
|
|
return; /* No sends! --> */
|
|
}
|
|
break;
|
|
case VE_STOP:
|
|
voice_kill(v);
|
|
aev_free(ev);
|
|
return; /* Back in the voice pool! --> */
|
|
case VE_SET:
|
|
#ifdef CHECKPOINTS
|
|
if(ev->index >= VC_COUNT)
|
|
{
|
|
log_printf(ELOG, "BUG! VC index out of range!");
|
|
break;
|
|
}
|
|
#endif
|
|
v->c[ev->index] = ev->arg1;
|
|
if(VC_PITCH == ev->index)
|
|
v->step = __calc_step(v);
|
|
break;
|
|
case VE_IRAMP:
|
|
#ifdef CHECKPOINTS
|
|
if(ev->index >= VIC_COUNT)
|
|
{
|
|
log_printf(ELOG, "BUG! VIC index out of range!");
|
|
break;
|
|
}
|
|
#endif
|
|
if(ev->arg2)
|
|
{
|
|
v->ic[ev->index].dv = ev->arg1 << RAMP_BITS;
|
|
v->ic[ev->index].dv -= v->ic[ev->index].v;
|
|
v->ic[ev->index].dv /= ev->arg2 + 1;
|
|
}
|
|
else
|
|
v->ic[ev->index].v = ev->arg1 << RAMP_BITS;
|
|
break;
|
|
}
|
|
aev_free(ev);
|
|
}
|
|
|
|
if(frag_frames > frames)
|
|
frag_frames = frames;
|
|
|
|
/* Handle fragmentation, end-of-waveform and looping */
|
|
frag_s = (VS_PLAYING == v->state) ? 0 : frag_frames;
|
|
while(frag_s < frag_frames)
|
|
{
|
|
unsigned offs = (s + frag_s) << 1;
|
|
unsigned do_frames = __endframes(v, frag_frames - frag_s);
|
|
if(do_frames)
|
|
{
|
|
#ifdef CHECKPOINTS
|
|
if(v->position >= v->section_end)
|
|
{
|
|
log_printf(ELOG, "BUG! position = %u while section_end = %u.",
|
|
v->position, v->section_end);
|
|
log_printf(ELOG, " (step = %u)\n", v->step >> FREQ_BITS);
|
|
v->position = 0;
|
|
}
|
|
#endif
|
|
bustab[v->fx1].in_use = 1;
|
|
if(v->use_double)
|
|
{
|
|
bustab[v->fx2].in_use = 1;
|
|
__fragment_double(v, busses[v->fx1] + offs,
|
|
busses[v->fx2] + offs,
|
|
do_frames);
|
|
}
|
|
else
|
|
__fragment_single(v, busses[v->fx1] + offs,
|
|
do_frames);
|
|
|
|
frag_s += do_frames;
|
|
|
|
// This is just for that damn oversampling...
|
|
if(v->position >= v->section_end)
|
|
do_frames = 0;
|
|
}
|
|
if(!do_frames && !__handle_looping(v))
|
|
{
|
|
voice_kill(v);
|
|
return;
|
|
}
|
|
}
|
|
s += frag_frames;
|
|
frames -= frag_frames;
|
|
}
|
|
}
|
|
|
|
|
|
void voice_process_all(int *bufs[], unsigned frames)
|
|
{
|
|
int i;
|
|
for(i = 0; i < AUDIO_MAX_VOICES; ++i)
|
|
voice_process_mix(voicetab + i, bufs, frames);
|
|
}
|
|
|
|
|
|
static int _is_open = 0;
|
|
|
|
void audio_voice_open(void)
|
|
{
|
|
int i;
|
|
if(_is_open)
|
|
return;
|
|
|
|
memset(voicetab, 0, sizeof(voicetab));
|
|
for(i = 0; i < AUDIO_MAX_VOICES; ++i)
|
|
{
|
|
char *buf = malloc(64);
|
|
snprintf(buf, 64, "Audio Voice %d", i);
|
|
aev_port_init(&voicetab[i].port, buf);
|
|
}
|
|
_is_open = 1;
|
|
}
|
|
|
|
|
|
void audio_voice_close(void)
|
|
{
|
|
int i;
|
|
if(!_is_open)
|
|
return;
|
|
|
|
for(i = 0; i < AUDIO_MAX_VOICES; ++i)
|
|
{
|
|
aev_flush(&voicetab[i].port);
|
|
free((char *)voicetab[i].port.name);
|
|
}
|
|
memset(voicetab, 0, sizeof(voicetab));
|
|
_is_open = 0;
|
|
}
|