mirror of
https://github.com/Pecusx/libretro-atari800.git
synced 2026-05-21 21:33:56 +02:00
544 lines
16 KiB
C
544 lines
16 KiB
C
/*
|
|
* sound.c - platform-independent interface for platform-specific sound output.
|
|
*
|
|
* Copyright (C) 2013 Tomasz Krasuski
|
|
* Copyright (C) 2013-2014 Atari800 development team (see DOC/CREDITS)
|
|
*
|
|
* This file is part of the Atari800 emulator project which emulates
|
|
* the Atari 400, 800, 800XL, 130XE, and 5200 8-bit computers.
|
|
|
|
* Atari800 is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
|
|
* Atari800 is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with Atari800; if not, write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*/
|
|
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
|
|
#include "sound.h"
|
|
|
|
#include "atari.h"
|
|
#include "log.h"
|
|
#include "platform.h"
|
|
#include "pokeysnd.h"
|
|
#include "util.h"
|
|
|
|
#define DEBUG 0
|
|
|
|
int Sound_enabled = 1;
|
|
|
|
Sound_setup_t Sound_desired = {
|
|
44100,
|
|
2,
|
|
1,
|
|
0
|
|
};
|
|
|
|
Sound_setup_t Sound_out;
|
|
|
|
static int paused = TRUE;
|
|
|
|
#ifndef SOUND_CALLBACK
|
|
static UBYTE *process_buffer = NULL;
|
|
static unsigned int process_buffer_size;
|
|
#endif /* !SOUND_CALLBACK */
|
|
|
|
#ifdef SYNCHRONIZED_SOUND
|
|
static UBYTE *sync_buffer = NULL;
|
|
static unsigned int sync_buffer_size;
|
|
/* Two invariants are held:
|
|
a) 0 <= sync_read_pos < sync_buffer_size
|
|
b) sync_read_pos <= sync_write_pos <= sync_buffer_size + sync_read_pos
|
|
sync_write_pos may be >= sync_buffer_size. In such case the actual write
|
|
position is sync_write_pos % sync_buffer_size. */
|
|
static unsigned int sync_write_pos;
|
|
static unsigned int sync_read_pos;
|
|
|
|
unsigned int Sound_latency = 20;
|
|
/* Cumulative audio difference. */
|
|
static double avg_fill;
|
|
/* Estimated fill of sync_buffer */
|
|
static unsigned int sync_est_fill;
|
|
/* If sync_est_fill goes outside this bounds, emulation speed is adjusted. */
|
|
static unsigned int sync_min_fill;
|
|
static unsigned int sync_max_fill;
|
|
#ifdef SOUND_CALLBACK
|
|
#endif /* SOUND_CALLBACK */
|
|
/* Time of last write of sudio to output device (either by Sound_Callback or
|
|
WriteOut). */
|
|
double last_audio_write_time;
|
|
#endif /* SYNCHRONIZED_SOUND */
|
|
|
|
enum { MAX_SAMPLE_SIZE = 2, /* for 16-bit */
|
|
#ifdef STEREO_SOUND
|
|
MAX_CHANNELS = 2,
|
|
#else /* !STEREO_SOUND */
|
|
MAX_CHANNELS = 1,
|
|
#endif /* !STEREO_SOUND */
|
|
MAX_FRAME_SIZE = MAX_SAMPLE_SIZE * MAX_CHANNELS
|
|
};
|
|
|
|
int Sound_ReadConfig(char *option, char *ptr)
|
|
{
|
|
if (strcmp(option, "SOUND_ENABLED") == 0)
|
|
return (Sound_enabled = Util_sscanbool(ptr)) != -1;
|
|
else if (strcmp(option, "SOUND_RATE") == 0)
|
|
return (Sound_desired.freq = Util_sscandec(ptr)) != -1;
|
|
else if (strcmp(option, "SOUND_BITS") == 0) {
|
|
int bits = Util_sscandec(ptr);
|
|
if (bits != 8 && bits != 16)
|
|
return FALSE;
|
|
Sound_desired.sample_size = bits / 8;
|
|
}
|
|
else if (strcmp(option, "SOUND_BUFFER_MS") == 0) {
|
|
int val = Util_sscandec(ptr);
|
|
if (val == -1)
|
|
return FALSE;
|
|
Sound_desired.buffer_ms = val;
|
|
}
|
|
#ifdef SYNCHRONIZED_SOUND
|
|
else if (strcmp(option, "SOUND_LATENCY") == 0)
|
|
return (Sound_latency = Util_sscandec(ptr)) != -1;
|
|
#endif /* SYNCHRONIZED_SOUND */
|
|
else
|
|
return FALSE;
|
|
return TRUE;
|
|
}
|
|
|
|
void Sound_WriteConfig(FILE *fp)
|
|
{
|
|
fprintf(fp, "SOUND_ENABLED=%u\n", Sound_enabled);
|
|
fprintf(fp, "SOUND_RATE=%u\n", Sound_desired.freq);
|
|
fprintf(fp, "SOUND_BITS=%u\n", Sound_desired.sample_size * 8);
|
|
fprintf(fp, "SOUND_BUFFER_MS=%u\n", Sound_desired.buffer_ms);
|
|
#ifdef SYNCHRONIZED_SOUND
|
|
fprintf(fp, "SOUND_LATENCY=%u\n", Sound_latency);
|
|
#endif /* SYNCHRONIZED_SOUND */
|
|
}
|
|
|
|
int Sound_Initialise(int *argc, char *argv[])
|
|
{
|
|
int i, j;
|
|
int help_only = FALSE;
|
|
|
|
for (i = j = 1; i < *argc; i++) {
|
|
int i_a = (i + 1 < *argc); /* is argument available? */
|
|
int a_m = FALSE; /* error, argument missing! */
|
|
int a_i = FALSE; /* error, argument invalid! */
|
|
|
|
if (strcmp(argv[i], "-sound") == 0)
|
|
Sound_enabled = 1;
|
|
else if (strcmp(argv[i], "-nosound") == 0)
|
|
Sound_enabled = 0;
|
|
else if (strcmp(argv[i], "-dsprate") == 0) {
|
|
if (i_a)
|
|
a_i = (Sound_desired.freq = Util_sscandec(argv[++i])) == -1;
|
|
else a_m = TRUE;
|
|
}
|
|
else if (strcmp(argv[i], "-audio16") == 0)
|
|
Sound_desired.sample_size = 2;
|
|
else if (strcmp(argv[i], "-audio8") == 0)
|
|
Sound_desired.sample_size = 1;
|
|
else if (strcmp(argv[i], "snd-buflen") == 0) {
|
|
if (i_a) {
|
|
int val = Util_sscandec(argv[++i]);
|
|
if (val == -1)
|
|
a_i = TRUE;
|
|
else
|
|
Sound_desired.buffer_ms = val;
|
|
}
|
|
else a_m = TRUE;
|
|
}
|
|
#ifdef SYNCHRONIZED_SOUND
|
|
else if (strcmp(argv[i], "-snddelay") == 0)
|
|
if (i_a)
|
|
Sound_latency = Util_sscandec(argv[++i]);
|
|
else a_m = TRUE;
|
|
#endif /* SYNCHRONIZED_SOUND */
|
|
else {
|
|
if (strcmp(argv[i], "-help") == 0) {
|
|
help_only = TRUE;
|
|
Log_print("\t-sound Enable sound");
|
|
Log_print("\t-nosound Disable sound");
|
|
Log_print("\t-dsprate <rate> Set sound output frequency in Hz");
|
|
Log_print("\t-audio16 Set sound output format to 16-bit");
|
|
Log_print("\t-audio8 Set sound output format to 8-bit");
|
|
Log_print("\t-snd-buflen <ms> Set length of the hardware sound buffer in milliseconds");
|
|
#ifdef SYNCHRONIZED_SOUND
|
|
Log_print("\t-snddelay <ms> Set sound latency in milliseconds");
|
|
#endif /* SYNCHRONIZED_SOUND */
|
|
}
|
|
argv[j++] = argv[i];
|
|
}
|
|
|
|
if (a_m) {
|
|
Log_print("Missing argument for '%s'", argv[i]);
|
|
return FALSE;
|
|
} else if (a_i) {
|
|
Log_print("Invalid argument for '%s'", argv[--i]);
|
|
return FALSE;
|
|
}
|
|
}
|
|
*argc = j;
|
|
|
|
if (help_only)
|
|
Sound_enabled = FALSE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
int Sound_Setup(void)
|
|
{
|
|
/* Sanitize freq. */
|
|
if (POKEYSND_enable_new_pokey && Sound_desired.freq < 8192)
|
|
/* MZ POKEY seems to segfault or remain silent with rate < 8009 Hz. */
|
|
Sound_desired.freq = 8192;
|
|
else if (Sound_desired.freq < 1000)
|
|
/* Such low value is impractical. */
|
|
Sound_desired.freq = 1000;
|
|
else if (Sound_desired.freq > 65535)
|
|
/* POKEY emulation doesn't support rate > 65535 Hz. */
|
|
Sound_desired.freq = 65535;
|
|
|
|
Sound_desired.buffer_frames = Sound_desired.freq * Sound_desired.buffer_ms / 1000;
|
|
|
|
Sound_out = Sound_desired;
|
|
if (!(Sound_enabled = PLATFORM_SoundSetup(&Sound_out)))
|
|
return FALSE;
|
|
|
|
Sound_out.buffer_ms = Sound_out.buffer_frames * 1000 / Sound_out.freq;
|
|
|
|
/* Now setup contains actual audio output settings. */
|
|
if ((POKEYSND_enable_new_pokey && Sound_out.freq < 8192)
|
|
|| Sound_out.freq < 1000 || Sound_out.freq > 65535) {
|
|
Log_print("%d frequency not supported", Sound_out.freq);
|
|
Sound_Exit();
|
|
return FALSE;
|
|
}
|
|
if (Sound_out.channels > MAX_CHANNELS) {
|
|
Log_print("%d channels not supported", Sound_out.channels);
|
|
Sound_Exit();
|
|
return FALSE;
|
|
}
|
|
|
|
POKEYSND_stereo_enabled = Sound_out.channels == 2;
|
|
#ifndef SOUND_CALLBACK
|
|
free(process_buffer);
|
|
process_buffer_size = Sound_out.buffer_frames * Sound_out.channels * Sound_out.sample_size;
|
|
process_buffer = Util_malloc(process_buffer_size);
|
|
#endif /* !SOUND_CALLBACK */
|
|
|
|
POKEYSND_Init(POKEYSND_FREQ_17_EXACT, Sound_out.freq, Sound_out.channels, Sound_out.sample_size == 2 ? POKEYSND_BIT16 : 0);
|
|
|
|
#ifdef SYNCHRONIZED_SOUND
|
|
Sound_SetLatency(Sound_latency);
|
|
#endif /* SYNCHRONIZED_SOUND */
|
|
|
|
Sound_desired.freq = Sound_out.freq;
|
|
Sound_desired.sample_size = Sound_out.sample_size;
|
|
Sound_desired.channels = Sound_out.channels;
|
|
/* buffer_ms and buffer_frames are not copied from Sound_out back to
|
|
Sound_desired. The reason is, for some backends (e.g. SDL on PulseAudio)
|
|
opening audio always results in buffer size smaller than desired. If
|
|
the obtained value was copied from Sound_out to Sound_desired, repeated
|
|
calls to Sound_Setup would quickly decrease buffer_ms to 0. */
|
|
|
|
paused = TRUE;
|
|
return TRUE;
|
|
}
|
|
|
|
void Sound_Exit(void)
|
|
{
|
|
if (Sound_enabled) {
|
|
PLATFORM_SoundExit();
|
|
Sound_enabled = FALSE;
|
|
#ifndef SOUND_CALLBACK
|
|
free(process_buffer);
|
|
process_buffer = NULL;
|
|
#endif /* !SOUND_CALLBACK */
|
|
#ifdef SYNCHRONIZED_SOUND
|
|
free(sync_buffer);
|
|
sync_buffer = NULL;
|
|
#endif /* SYNCHRONIZED_SOUND */
|
|
}
|
|
}
|
|
|
|
void Sound_Pause(void)
|
|
{
|
|
if (Sound_enabled && !paused) {
|
|
/* stop audio output */
|
|
PLATFORM_SoundPause();
|
|
paused = TRUE;
|
|
}
|
|
}
|
|
|
|
void Sound_Continue(void)
|
|
{
|
|
if (Sound_enabled && paused) {
|
|
/* start audio output */
|
|
#ifdef SYNCHRONIZED_SOUND
|
|
/* sync_write_pos = sync_read_pos + sync_min_fill;
|
|
avg_fill = sync_min_fill;*/
|
|
last_audio_write_time = Util_time();
|
|
#endif /* SYNCHRONIZED_SOUND */
|
|
PLATFORM_SoundContinue();
|
|
paused = FALSE;
|
|
}
|
|
}
|
|
|
|
/* Fills buffer BUFFER with SIZE bytes of audio samples. */
|
|
static void FillBuffer(UBYTE *buffer, unsigned int size)
|
|
{
|
|
#ifdef SYNCHRONIZED_SOUND
|
|
unsigned int new_read_pos;
|
|
static UBYTE last_frame[MAX_FRAME_SIZE];
|
|
unsigned int bytes_per_frame = Sound_out.channels * Sound_out.sample_size;
|
|
unsigned int to_write = sync_write_pos - sync_read_pos;
|
|
|
|
if (to_write > 0) {
|
|
if (to_write > size)
|
|
to_write = size;
|
|
|
|
new_read_pos = sync_read_pos + to_write;
|
|
|
|
if (new_read_pos <= sync_buffer_size)
|
|
/* no wrap */
|
|
memcpy(buffer, sync_buffer + sync_read_pos, to_write);
|
|
else {
|
|
/* wraps */
|
|
unsigned int first_part_size = sync_buffer_size - sync_read_pos;
|
|
memcpy(buffer, sync_buffer + sync_read_pos, first_part_size);
|
|
memcpy(buffer + first_part_size, sync_buffer, to_write - first_part_size);
|
|
}
|
|
|
|
sync_read_pos = new_read_pos;
|
|
if (sync_read_pos > sync_buffer_size) {
|
|
sync_read_pos -= sync_buffer_size;
|
|
sync_write_pos -= sync_buffer_size;
|
|
}
|
|
/* Save the last frame as we may need it to fill underflow. */
|
|
memcpy(last_frame, buffer + to_write - bytes_per_frame, bytes_per_frame);
|
|
}
|
|
|
|
|
|
/* Just repeat the last good frame if underflow. */
|
|
if (to_write < size) {
|
|
#if DEBUG
|
|
Log_print("Sound buffer underflow: fill %d, needed %d",
|
|
to_write/Sound_out.channels/Sound_out.sample_size,
|
|
size/Sound_out.channels/Sound_out.sample_size);
|
|
#endif
|
|
do {
|
|
memcpy(buffer + to_write, last_frame, bytes_per_frame);
|
|
to_write += bytes_per_frame;
|
|
} while (to_write < size);
|
|
}
|
|
#else /* !SYNCHRONIZED_SOUND */
|
|
POKEYSND_Process(buffer, size / Sound_out.sample_size);
|
|
#endif /* !SYNCHRONIZED_SOUND */
|
|
}
|
|
|
|
#ifdef SOUND_CALLBACK
|
|
void Sound_Callback(UBYTE *buffer, unsigned int size)
|
|
{
|
|
#if DEBUG >= 2
|
|
Log_print("Callback: fill %u, needed %u",
|
|
(sync_write_pos - sync_read_pos) / Sound_out.channels / Sound_out.sample_size,
|
|
size / Sound_out.channels / Sound_out.sample_size);
|
|
#endif
|
|
FillBuffer(buffer, size);
|
|
#ifdef SYNCHRONIZED_SOUND
|
|
last_audio_write_time = Util_time();
|
|
#endif /* SYNCHRONIZED_SOUND */
|
|
}
|
|
#else /* !SOUND_CALLBACK */
|
|
/* Write audio to output device. */
|
|
static void WriteOut(void)
|
|
{
|
|
unsigned int avail = PLATFORM_SoundAvailable();
|
|
|
|
if (avail > 0) {
|
|
#if DEBUG >= 2
|
|
Log_print("WriteOut: fill %u, needed %u",
|
|
(sync_write_pos - sync_read_pos) / Sound_out.channels / Sound_out.sample_size,
|
|
avail / Sound_out.channels / Sound_out.sample_size);
|
|
#endif
|
|
/* On some platforms (eg. NestedVM) avail may be larger than process_buffer_size. */
|
|
do {
|
|
unsigned int len = avail > process_buffer_size ? process_buffer_size : avail;
|
|
FillBuffer(process_buffer, len);
|
|
PLATFORM_SoundWrite(process_buffer, len);
|
|
avail -= len;
|
|
} while (avail > 0);
|
|
#ifdef SYNCHRONIZED_SOUND
|
|
last_audio_write_time = Util_time();
|
|
#endif /* SYNCHRONIZED_SOUND */
|
|
}
|
|
}
|
|
#endif /* !SOUND_CALLBACK */
|
|
|
|
#ifdef SYNCHRONIZED_SOUND
|
|
static void UpdateSyncBuffer(void)
|
|
{
|
|
unsigned int bytes_written;
|
|
unsigned int samples_written;
|
|
unsigned int fill;
|
|
unsigned int new_write_pos;
|
|
|
|
PLATFORM_SoundLock();
|
|
/* Current fill of the audio buffer. */
|
|
fill = sync_write_pos - sync_read_pos;
|
|
|
|
/* Update sync_est_fill. */
|
|
{
|
|
unsigned int est_gap;
|
|
est_gap = (Util_time() - last_audio_write_time)*Sound_out.freq*Sound_out.channels*Sound_out.sample_size;
|
|
if (fill < est_gap)
|
|
sync_est_fill = 0;
|
|
else
|
|
sync_est_fill = fill - est_gap;
|
|
}
|
|
|
|
if (Atari800_turbo && sync_est_fill > sync_max_fill) {
|
|
PLATFORM_SoundUnlock();
|
|
return;
|
|
}
|
|
|
|
/* produce samples from the sound emulation */
|
|
samples_written = POKEYSND_UpdateProcessBuffer();
|
|
bytes_written = Sound_out.sample_size * samples_written;
|
|
|
|
/* if there isn't enough room... */
|
|
if (bytes_written > sync_buffer_size - fill) {
|
|
/* Overflow of sync_buffer. */
|
|
#if DEBUG
|
|
Log_print("Sound buffer overflow: free %d, needed %d",
|
|
(sync_buffer_size - fill)/Sound_out.channels/Sound_out.sample_size,
|
|
bytes_written/Sound_out.channels/Sound_out.sample_size);
|
|
#endif
|
|
/* Wait until hardware buffer can be filled, or wait until callback
|
|
makes place in the buffer. */
|
|
do {
|
|
PLATFORM_SoundUnlock();
|
|
/* Sleep for the duration of one full HW buffer. */
|
|
Util_sleep((double)Sound_out.buffer_frames / Sound_out.freq);
|
|
PLATFORM_SoundLock();
|
|
#ifndef SOUND_CALLBACK
|
|
WriteOut(); /* Write to audio buffer as much as possible. */
|
|
#endif /* SOUND_CALLBACK */
|
|
fill = sync_write_pos - sync_read_pos;
|
|
} while (bytes_written > sync_buffer_size - fill);
|
|
}
|
|
/* Now bytes_written <= audio_buffer_size + dsp_read_pos - dsp_write_pos) */
|
|
|
|
#if DEBUG >= 2
|
|
Log_print("UpdateSyncBuffer: est_gap: %f, fill %u, write %u",
|
|
(Util_time() - last_audio_write_time)*Sound_out.freq,
|
|
fill / Sound_out.channels/Sound_out.sample_size,
|
|
bytes_written / Sound_out.channels/Sound_out.sample_size);
|
|
#endif
|
|
/* now we copy the data into the buffer and adjust the positions */
|
|
new_write_pos = sync_write_pos + bytes_written;
|
|
if (new_write_pos/sync_buffer_size == sync_write_pos/sync_buffer_size)
|
|
/* no wrap */
|
|
memcpy(sync_buffer + sync_write_pos%sync_buffer_size, POKEYSND_process_buffer, bytes_written);
|
|
else {
|
|
/* wraps */
|
|
int first_part_size = sync_buffer_size - sync_write_pos%sync_buffer_size;
|
|
memcpy(sync_buffer + sync_write_pos%sync_buffer_size, POKEYSND_process_buffer, first_part_size);
|
|
memcpy(sync_buffer, POKEYSND_process_buffer + first_part_size, bytes_written - first_part_size);
|
|
}
|
|
|
|
sync_write_pos = new_write_pos;
|
|
if (sync_write_pos > sync_read_pos + sync_buffer_size)
|
|
sync_write_pos -= sync_buffer_size;
|
|
PLATFORM_SoundUnlock();
|
|
}
|
|
#endif /* SYNCHRONIZED_SOUND */
|
|
|
|
void Sound_Update(void)
|
|
{
|
|
if (!Sound_enabled || paused)
|
|
return;
|
|
#ifdef SYNCHRONIZED_SOUND
|
|
UpdateSyncBuffer();
|
|
#endif /* SYNCHRONIZED_SOUND */
|
|
#ifndef SOUND_CALLBACK
|
|
WriteOut();
|
|
#endif /* !SOUND_CALLBACK */
|
|
}
|
|
|
|
#ifdef SYNCHRONIZED_SOUND
|
|
void Sound_SetLatency(unsigned int latency)
|
|
{
|
|
Sound_latency = latency;
|
|
if (Sound_enabled) {
|
|
/* how many fragments in the audio buffer */
|
|
enum { SYNC_BUFFER_FRAGS = 5 };
|
|
unsigned int bytes_per_frame = Sound_out.channels * Sound_out.sample_size;
|
|
unsigned int latency_frames = Sound_out.freq*Sound_latency/1000;
|
|
PLATFORM_SoundLock();
|
|
sync_buffer_size = (latency_frames + SYNC_BUFFER_FRAGS*Sound_out.buffer_frames) * bytes_per_frame;
|
|
sync_min_fill = latency_frames * bytes_per_frame;
|
|
sync_max_fill = sync_min_fill + Sound_out.buffer_frames * bytes_per_frame;
|
|
avg_fill = sync_min_fill;
|
|
sync_read_pos = 0;
|
|
sync_write_pos = sync_min_fill;
|
|
free(sync_buffer);
|
|
sync_buffer = Util_malloc(sync_buffer_size);
|
|
memset(sync_buffer, 0, sync_buffer_size);
|
|
PLATFORM_SoundUnlock();
|
|
}
|
|
}
|
|
|
|
double Sound_AdjustSpeed(void)
|
|
{
|
|
double delay_mult = 1.0;
|
|
static double const alpha = 2.0/(1.0+40.0);
|
|
|
|
if (Sound_enabled && !paused) {
|
|
#if 1
|
|
avg_fill = avg_fill + alpha * (sync_est_fill - avg_fill);
|
|
if (avg_fill < sync_min_fill)
|
|
delay_mult = 0.95;
|
|
else if (avg_fill > sync_max_fill)
|
|
delay_mult = 1.05;
|
|
#endif
|
|
#if 0
|
|
if (sync_est_fill < sync_min_fill)
|
|
delay_mult = 0.95;
|
|
else if (sync_est_fill > sync_max_fill)
|
|
delay_mult = 1.05;
|
|
#endif
|
|
#if DEBUG >= 2
|
|
Log_print("delay_mult: %f, est_fill: %u, avg_fill: %f, buf_size: %u, min_fill: %u, max_fill: %u",
|
|
delay_mult,
|
|
sync_est_fill / Sound_out.channels / Sound_out.sample_size,
|
|
avg_fill / Sound_out.channels / Sound_out.sample_size,
|
|
sync_buffer_size / Sound_out.channels / Sound_out.sample_size,
|
|
sync_min_fill / Sound_out.channels / Sound_out.sample_size,
|
|
sync_max_fill / Sound_out.channels / Sound_out.sample_size);
|
|
#endif
|
|
}
|
|
return delay_mult;
|
|
}
|
|
#endif /* SYNCHRONIZED_SOUND */
|
|
|
|
unsigned int Sound_NextPow2(unsigned int num)
|
|
{
|
|
unsigned int result = 1;
|
|
while (result <= num)
|
|
result <<= 1;
|
|
return result;
|
|
}
|