852ee1c3 |
1 | /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * |
2 | * Mupen64plus-sdl-audio - main.c * |
3 | * Mupen64Plus homepage: http://code.google.com/p/mupen64plus/ * |
4 | * Copyright (C) 2007-2009 Richard Goedeken * |
5 | * Copyright (C) 2007-2008 Ebenblues * |
6 | * Copyright (C) 2003 JttL * |
7 | * Copyright (C) 2002 Hacktarux * |
8 | * * |
9 | * This program is free software; you can redistribute it and/or modify * |
10 | * it under the terms of the GNU General Public License as published by * |
11 | * the Free Software Foundation; either version 2 of the License, or * |
12 | * (at your option) any later version. * |
13 | * * |
14 | * This program is distributed in the hope that it will be useful, * |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of * |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * |
17 | * GNU General Public License for more details. * |
18 | * * |
19 | * You should have received a copy of the GNU General Public License * |
20 | * along with this program; if not, write to the * |
21 | * Free Software Foundation, Inc., * |
22 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * |
23 | * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ |
24 | |
25 | #include <stdio.h> |
26 | #include <stdlib.h> |
27 | #include <string.h> |
28 | |
29 | #include <SDL.h> |
30 | #include <SDL_audio.h> |
31 | |
32 | #ifdef USE_SRC |
33 | #include <samplerate.h> |
34 | #endif |
35 | #ifdef USE_SPEEX |
36 | #include <speex/speex_resampler.h> |
37 | #endif |
38 | |
39 | #define M64P_PLUGIN_PROTOTYPES 1 |
40 | #include "m64p_types.h" |
41 | #include "m64p_plugin.h" |
42 | #include "m64p_common.h" |
43 | #include "m64p_config.h" |
44 | |
45 | #include "main.h" |
46 | #include "volume.h" |
47 | #include "osal_dynamiclib.h" |
48 | |
49 | /* Default start-time size of primary buffer (in equivalent output samples). |
50 | This is the buffer where audio is loaded after it's extracted from n64's memory. |
51 | This value must be larger than PRIMARY_BUFFER_TARGET */ |
52 | #define PRIMARY_BUFFER_SIZE 16384 |
53 | |
54 | /* this is the buffer fullness level (in equivalent output samples) which is targeted |
55 | for the primary audio buffer (by inserting delays) each time data is received from |
56 | the running N64 program. This value must be larger than the SECONDARY_BUFFER_SIZE. |
57 | Decreasing this value will reduce audio latency but requires a faster PC to avoid |
58 | choppiness. Increasing this will increase audio latency but reduce the chance of |
59 | drop-outs. The default value 10240 gives a 232ms maximum A/V delay at 44.1khz */ |
60 | #define PRIMARY_BUFFER_TARGET 10240 |
61 | |
62 | /* Size of secondary buffer, in output samples. This is the requested size of SDL's |
63 | hardware buffer, and the size of the mix buffer for doing SDL volume control. The |
64 | SDL documentation states that this should be a power of two between 512 and 8192. */ |
65 | #define SECONDARY_BUFFER_SIZE 8192 |
66 | /*SEB 2048 before*/ |
67 | |
68 | /* This sets default frequency what is used if rom doesn't want to change it. |
69 | Probably only game that needs this is Zelda: Ocarina Of Time Master Quest |
70 | *NOTICE* We should try to find out why Demos' frequencies are always wrong |
71 | They tend to rely on a default frequency, apparently, never the same one ;)*/ |
72 | #define DEFAULT_FREQUENCY 33600 |
73 | |
74 | /* number of bytes per sample */ |
75 | #define N64_SAMPLE_BYTES 4 |
76 | #define SDL_SAMPLE_BYTES 4 |
77 | |
78 | /* volume mixer types */ |
79 | #define VOLUME_TYPE_SDL 1 |
80 | #define VOLUME_TYPE_OSS 2 |
81 | |
82 | /* local variables */ |
83 | static void (*l_DebugCallback)(void *, int, const char *) = NULL; |
84 | static void *l_DebugCallContext = NULL; |
85 | static int l_PluginInit = 0; |
86 | static int l_PausedForSync = 1; /* Audio is started in paused state after SDL initialization */ |
87 | static m64p_handle l_ConfigAudio; |
88 | |
89 | enum resampler_type { |
90 | RESAMPLER_TRIVIAL, |
91 | #ifdef USE_SRC |
92 | RESAMPLER_SRC, |
93 | #endif |
94 | #ifdef USE_SPEEX |
95 | RESAMPLER_SPEEX, |
96 | #endif |
97 | }; |
98 | |
99 | /* Read header for type definition */ |
100 | static AUDIO_INFO AudioInfo; |
101 | /* The hardware specifications we are using */ |
102 | static SDL_AudioSpec *hardware_spec; |
103 | /* Pointer to the primary audio buffer */ |
104 | static unsigned char *primaryBuffer = NULL; |
105 | static unsigned int primaryBufferBytes = 0; |
106 | /* Pointer to the mixing buffer for voume control*/ |
107 | static unsigned char *mixBuffer = NULL; |
108 | /* Position in buffer array where next audio chunk should be placed */ |
109 | static unsigned int buffer_pos = 0; |
110 | /* Audio frequency, this is usually obtained from the game, but for compatibility we set default value */ |
111 | static int GameFreq = DEFAULT_FREQUENCY; |
112 | /* timestamp for the last time that our audio callback was called */ |
113 | static unsigned int last_callback_ticks = 0; |
114 | /* SpeedFactor is used to increase/decrease game playback speed */ |
115 | static unsigned int speed_factor = 100; |
116 | // If this is true then left and right channels are swapped */ |
117 | static int SwapChannels = 0; |
118 | // Size of Primary audio buffer in equivalent output samples |
119 | static unsigned int PrimaryBufferSize = PRIMARY_BUFFER_SIZE; |
120 | // Fullness level target for Primary audio buffer, in equivalent output samples |
121 | static unsigned int PrimaryBufferTarget = PRIMARY_BUFFER_TARGET; |
122 | // Size of Secondary audio buffer in output samples |
123 | static unsigned int SecondaryBufferSize = SECONDARY_BUFFER_SIZE; |
124 | // Resample type |
125 | static enum resampler_type Resample = RESAMPLER_TRIVIAL; |
126 | // Resampler specific quality |
127 | static int ResampleQuality = 3; |
128 | // volume to scale the audio by, range of 0..100 |
129 | // if muted, this holds the volume when not muted |
130 | static int VolPercent = 80; |
131 | // how much percent to increment/decrement volume by |
132 | static int VolDelta = 5; |
133 | // the actual volume passed into SDL, range of 0..SDL_MIX_MAXVOLUME |
134 | static int VolSDL = SDL_MIX_MAXVOLUME; |
135 | // Muted or not |
136 | static int VolIsMuted = 0; |
137 | //which type of volume control to use |
138 | static int VolumeControlType = VOLUME_TYPE_SDL; |
139 | |
140 | static int OutputFreq; |
141 | |
142 | // Prototype of local functions |
143 | static void my_audio_callback(void *userdata, unsigned char *stream, int len); |
144 | static void InitializeAudio(int freq); |
145 | static void ReadConfig(void); |
146 | static void InitializeSDL(void); |
147 | |
148 | static int critical_failure = 0; |
149 | |
150 | /* definitions of pointers to Core config functions */ |
151 | ptr_ConfigOpenSection ConfigOpenSection = NULL; |
152 | ptr_ConfigDeleteSection ConfigDeleteSection = NULL; |
153 | ptr_ConfigSaveSection ConfigSaveSection = NULL; |
154 | ptr_ConfigSetParameter ConfigSetParameter = NULL; |
155 | ptr_ConfigGetParameter ConfigGetParameter = NULL; |
156 | ptr_ConfigGetParameterHelp ConfigGetParameterHelp = NULL; |
157 | ptr_ConfigSetDefaultInt ConfigSetDefaultInt = NULL; |
158 | ptr_ConfigSetDefaultFloat ConfigSetDefaultFloat = NULL; |
159 | ptr_ConfigSetDefaultBool ConfigSetDefaultBool = NULL; |
160 | ptr_ConfigSetDefaultString ConfigSetDefaultString = NULL; |
161 | ptr_ConfigGetParamInt ConfigGetParamInt = NULL; |
162 | ptr_ConfigGetParamFloat ConfigGetParamFloat = NULL; |
163 | ptr_ConfigGetParamBool ConfigGetParamBool = NULL; |
164 | ptr_ConfigGetParamString ConfigGetParamString = NULL; |
165 | |
166 | /* Global functions */ |
167 | static void DebugMessage(int level, const char *message, ...) |
168 | { |
169 | char msgbuf[1024]; |
170 | va_list args; |
171 | |
172 | if (l_DebugCallback == NULL) |
173 | return; |
174 | |
175 | va_start(args, message); |
176 | vsprintf(msgbuf, message, args); |
177 | |
178 | (*l_DebugCallback)(l_DebugCallContext, level, msgbuf); |
179 | |
180 | va_end(args); |
181 | } |
182 | |
183 | /* Mupen64Plus plugin functions */ |
184 | EXPORT m64p_error CALL PluginStartup(m64p_dynlib_handle CoreLibHandle, void *Context, |
185 | void (*DebugCallback)(void *, int, const char *)) |
186 | { |
187 | ptr_CoreGetAPIVersions CoreAPIVersionFunc; |
188 | |
189 | int ConfigAPIVersion, DebugAPIVersion, VidextAPIVersion, bSaveConfig; |
190 | float fConfigParamsVersion = 0.0f; |
191 | |
192 | if (l_PluginInit) |
193 | return M64ERR_ALREADY_INIT; |
194 | |
195 | /* first thing is to set the callback function for debug info */ |
196 | l_DebugCallback = DebugCallback; |
197 | l_DebugCallContext = Context; |
198 | |
199 | /* attach and call the CoreGetAPIVersions function, check Config API version for compatibility */ |
200 | CoreAPIVersionFunc = (ptr_CoreGetAPIVersions) osal_dynlib_getproc(CoreLibHandle, "CoreGetAPIVersions"); |
201 | if (CoreAPIVersionFunc == NULL) |
202 | { |
203 | DebugMessage(M64MSG_ERROR, "Core emulator broken; no CoreAPIVersionFunc() function found."); |
204 | return M64ERR_INCOMPATIBLE; |
205 | } |
206 | |
207 | (*CoreAPIVersionFunc)(&ConfigAPIVersion, &DebugAPIVersion, &VidextAPIVersion, NULL); |
208 | if ((ConfigAPIVersion & 0xffff0000) != (CONFIG_API_VERSION & 0xffff0000)) |
209 | { |
210 | DebugMessage(M64MSG_ERROR, "Emulator core Config API (v%i.%i.%i) incompatible with plugin (v%i.%i.%i)", |
211 | VERSION_PRINTF_SPLIT(ConfigAPIVersion), VERSION_PRINTF_SPLIT(CONFIG_API_VERSION)); |
212 | return M64ERR_INCOMPATIBLE; |
213 | } |
214 | |
215 | /* Get the core config function pointers from the library handle */ |
216 | ConfigOpenSection = (ptr_ConfigOpenSection) osal_dynlib_getproc(CoreLibHandle, "ConfigOpenSection"); |
217 | ConfigDeleteSection = (ptr_ConfigDeleteSection) osal_dynlib_getproc(CoreLibHandle, "ConfigDeleteSection"); |
218 | ConfigSaveSection = (ptr_ConfigSaveSection) osal_dynlib_getproc(CoreLibHandle, "ConfigSaveSection"); |
219 | ConfigSetParameter = (ptr_ConfigSetParameter) osal_dynlib_getproc(CoreLibHandle, "ConfigSetParameter"); |
220 | ConfigGetParameter = (ptr_ConfigGetParameter) osal_dynlib_getproc(CoreLibHandle, "ConfigGetParameter"); |
221 | ConfigSetDefaultInt = (ptr_ConfigSetDefaultInt) osal_dynlib_getproc(CoreLibHandle, "ConfigSetDefaultInt"); |
222 | ConfigSetDefaultFloat = (ptr_ConfigSetDefaultFloat) osal_dynlib_getproc(CoreLibHandle, "ConfigSetDefaultFloat"); |
223 | ConfigSetDefaultBool = (ptr_ConfigSetDefaultBool) osal_dynlib_getproc(CoreLibHandle, "ConfigSetDefaultBool"); |
224 | ConfigSetDefaultString = (ptr_ConfigSetDefaultString) osal_dynlib_getproc(CoreLibHandle, "ConfigSetDefaultString"); |
225 | ConfigGetParamInt = (ptr_ConfigGetParamInt) osal_dynlib_getproc(CoreLibHandle, "ConfigGetParamInt"); |
226 | ConfigGetParamFloat = (ptr_ConfigGetParamFloat) osal_dynlib_getproc(CoreLibHandle, "ConfigGetParamFloat"); |
227 | ConfigGetParamBool = (ptr_ConfigGetParamBool) osal_dynlib_getproc(CoreLibHandle, "ConfigGetParamBool"); |
228 | ConfigGetParamString = (ptr_ConfigGetParamString) osal_dynlib_getproc(CoreLibHandle, "ConfigGetParamString"); |
229 | |
230 | if (!ConfigOpenSection || !ConfigDeleteSection || !ConfigSetParameter || !ConfigGetParameter || |
231 | !ConfigSetDefaultInt || !ConfigSetDefaultFloat || !ConfigSetDefaultBool || !ConfigSetDefaultString || |
232 | !ConfigGetParamInt || !ConfigGetParamFloat || !ConfigGetParamBool || !ConfigGetParamString) |
233 | return M64ERR_INCOMPATIBLE; |
234 | |
235 | /* ConfigSaveSection was added in Config API v2.1.0 */ |
236 | if (ConfigAPIVersion >= 0x020100 && !ConfigSaveSection) |
237 | return M64ERR_INCOMPATIBLE; |
238 | |
239 | /* get a configuration section handle */ |
240 | if (ConfigOpenSection("Audio-SDL", &l_ConfigAudio) != M64ERR_SUCCESS) |
241 | { |
242 | DebugMessage(M64MSG_ERROR, "Couldn't open config section 'Audio-SDL'"); |
243 | return M64ERR_INPUT_NOT_FOUND; |
244 | } |
245 | |
246 | /* check the section version number */ |
247 | bSaveConfig = 0; |
248 | if (ConfigGetParameter(l_ConfigAudio, "Version", M64TYPE_FLOAT, &fConfigParamsVersion, sizeof(float)) != M64ERR_SUCCESS) |
249 | { |
250 | DebugMessage(M64MSG_WARNING, "No version number in 'Audio-SDL' config section. Setting defaults."); |
251 | ConfigDeleteSection("Audio-SDL"); |
252 | ConfigOpenSection("Audio-SDL", &l_ConfigAudio); |
253 | bSaveConfig = 1; |
254 | } |
255 | else if (((int) fConfigParamsVersion) != ((int) CONFIG_PARAM_VERSION)) |
256 | { |
257 | DebugMessage(M64MSG_WARNING, "Incompatible version %.2f in 'Audio-SDL' config section: current is %.2f. Setting defaults.", fConfigParamsVersion, (float) CONFIG_PARAM_VERSION); |
258 | ConfigDeleteSection("Audio-SDL"); |
259 | ConfigOpenSection("Audio-SDL", &l_ConfigAudio); |
260 | bSaveConfig = 1; |
261 | } |
262 | else if ((CONFIG_PARAM_VERSION - fConfigParamsVersion) >= 0.0001f) |
263 | { |
264 | /* handle upgrades */ |
265 | float fVersion = CONFIG_PARAM_VERSION; |
266 | ConfigSetParameter(l_ConfigAudio, "Version", M64TYPE_FLOAT, &fVersion); |
267 | DebugMessage(M64MSG_INFO, "Updating parameter set version in 'Audio-SDL' config section to %.2f", fVersion); |
268 | bSaveConfig = 1; |
269 | } |
270 | |
271 | /* set the default values for this plugin */ |
272 | ConfigSetDefaultFloat(l_ConfigAudio, "Version", CONFIG_PARAM_VERSION, "Mupen64Plus SDL Audio Plugin config parameter version number"); |
273 | ConfigSetDefaultInt(l_ConfigAudio, "DEFAULT_FREQUENCY", DEFAULT_FREQUENCY, "Frequency which is used if rom doesn't want to change it"); |
274 | ConfigSetDefaultBool(l_ConfigAudio, "SWAP_CHANNELS", 0, "Swaps left and right channels"); |
275 | ConfigSetDefaultInt(l_ConfigAudio, "PRIMARY_BUFFER_SIZE", PRIMARY_BUFFER_SIZE, "Size of primary buffer in output samples. This is where audio is loaded after it's extracted from n64's memory."); |
276 | ConfigSetDefaultInt(l_ConfigAudio, "PRIMARY_BUFFER_TARGET", PRIMARY_BUFFER_TARGET, "Fullness level target for Primary audio buffer, in equivalent output samples"); |
277 | ConfigSetDefaultInt(l_ConfigAudio, "SECONDARY_BUFFER_SIZE", SECONDARY_BUFFER_SIZE, "Size of secondary buffer in output samples. This is SDL's hardware buffer."); |
278 | ConfigSetDefaultString(l_ConfigAudio, "RESAMPLE", "trivial", "Audio resampling algorithm. src-sinc-best-quality, src-sinc-medium-quality, src-sinc-fastest, src-zero-order-hold, src-linear, speex-fixed-{10-0}, trivial"); |
279 | ConfigSetDefaultInt(l_ConfigAudio, "VOLUME_CONTROL_TYPE", VOLUME_TYPE_SDL, "Volume control type: 1 = SDL (only affects Mupen64Plus output) 2 = OSS mixer (adjusts master PC volume)"); |
280 | ConfigSetDefaultInt(l_ConfigAudio, "VOLUME_ADJUST", 5, "Percentage change each time the volume is increased or decreased"); |
281 | ConfigSetDefaultInt(l_ConfigAudio, "VOLUME_DEFAULT", 80, "Default volume when a game is started. Only used if VOLUME_CONTROL_TYPE is 1"); |
282 | |
283 | if (bSaveConfig && ConfigAPIVersion >= 0x020100) |
284 | ConfigSaveSection("Audio-SDL"); |
285 | |
286 | l_PluginInit = 1; |
287 | return M64ERR_SUCCESS; |
288 | } |
289 | |
290 | EXPORT m64p_error CALL PluginShutdown(void) |
291 | { |
292 | if (!l_PluginInit) |
293 | return M64ERR_NOT_INIT; |
294 | |
295 | /* reset some local variables */ |
296 | l_DebugCallback = NULL; |
297 | l_DebugCallContext = NULL; |
298 | |
299 | /* make sure our buffer is freed */ |
300 | if (mixBuffer != NULL) |
301 | { |
302 | free(mixBuffer); |
303 | mixBuffer = NULL; |
304 | } |
305 | |
306 | l_PluginInit = 0; |
307 | return M64ERR_SUCCESS; |
308 | } |
309 | |
310 | EXPORT m64p_error CALL PluginGetVersion(m64p_plugin_type *PluginType, int *PluginVersion, int *APIVersion, const char **PluginNamePtr, int *Capabilities) |
311 | { |
312 | /* set version info */ |
313 | if (PluginType != NULL) |
314 | *PluginType = M64PLUGIN_AUDIO; |
315 | |
316 | if (PluginVersion != NULL) |
317 | *PluginVersion = SDL_AUDIO_PLUGIN_VERSION; |
318 | |
319 | if (APIVersion != NULL) |
320 | *APIVersion = AUDIO_PLUGIN_API_VERSION; |
321 | |
322 | if (PluginNamePtr != NULL) |
323 | *PluginNamePtr = "Mupen64Plus SDL Audio Plugin"; |
324 | |
325 | if (Capabilities != NULL) |
326 | { |
327 | *Capabilities = 0; |
328 | } |
329 | |
330 | return M64ERR_SUCCESS; |
331 | } |
332 | |
333 | /* ----------- Audio Functions ------------- */ |
334 | EXPORT void CALL AiDacrateChanged( int SystemType ) |
335 | { |
336 | int f = GameFreq; |
337 | |
338 | if (!l_PluginInit) |
339 | return; |
340 | |
341 | switch (SystemType) |
342 | { |
343 | case SYSTEM_NTSC: |
344 | f = 48681812 / (*AudioInfo.AI_DACRATE_REG + 1); |
345 | break; |
346 | case SYSTEM_PAL: |
347 | f = 49656530 / (*AudioInfo.AI_DACRATE_REG + 1); |
348 | break; |
349 | case SYSTEM_MPAL: |
350 | f = 48628316 / (*AudioInfo.AI_DACRATE_REG + 1); |
351 | break; |
352 | } |
353 | InitializeAudio(f); |
354 | } |
355 | |
356 | |
357 | EXPORT void CALL AiLenChanged( void ) |
358 | { |
359 | unsigned int LenReg; |
360 | unsigned char *p; |
361 | unsigned int CurrLevel, CurrTime, ExpectedLevel, ExpectedTime; |
362 | |
363 | if (critical_failure == 1) |
364 | return; |
365 | if (!l_PluginInit) |
366 | return; |
367 | |
368 | LenReg = *AudioInfo.AI_LEN_REG; |
369 | p = AudioInfo.RDRAM + (*AudioInfo.AI_DRAM_ADDR_REG & 0xFFFFFF); |
370 | |
371 | if (buffer_pos + LenReg < primaryBufferBytes) |
372 | { |
373 | unsigned int i; |
374 | |
375 | SDL_LockAudio(); |
376 | for ( i = 0 ; i < LenReg ; i += 4 ) |
377 | { |
378 | |
379 | if(SwapChannels == 0) |
380 | { |
381 | // Left channel |
382 | primaryBuffer[ buffer_pos + i ] = p[ i + 2 ]; |
383 | primaryBuffer[ buffer_pos + i + 1 ] = p[ i + 3 ]; |
384 | |
385 | // Right channel |
386 | primaryBuffer[ buffer_pos + i + 2 ] = p[ i ]; |
387 | primaryBuffer[ buffer_pos + i + 3 ] = p[ i + 1 ]; |
388 | } else { |
389 | // Left channel |
390 | primaryBuffer[ buffer_pos + i ] = p[ i ]; |
391 | primaryBuffer[ buffer_pos + i + 1 ] = p[ i + 1 ]; |
392 | |
393 | // Right channel |
394 | primaryBuffer[ buffer_pos + i + 2 ] = p[ i + 2]; |
395 | primaryBuffer[ buffer_pos + i + 3 ] = p[ i + 3 ]; |
396 | } |
397 | } |
398 | buffer_pos += i; |
399 | SDL_UnlockAudio(); |
400 | } |
401 | else |
402 | { |
403 | DebugMessage(M64MSG_WARNING, "AiLenChanged(): Audio buffer overflow."); |
404 | } |
405 | |
406 | /* Now we need to handle synchronization, by inserting time delay to keep the emulator running at the correct speed */ |
407 | /* Start by calculating the current Primary buffer fullness in terms of output samples */ |
408 | CurrLevel = (unsigned int) (((long long) (buffer_pos/N64_SAMPLE_BYTES) * OutputFreq * 100) / (GameFreq * speed_factor)); |
409 | /* Next, extrapolate to the buffer level at the expected time of the next audio callback, assuming that the |
410 | buffer is filled at the same rate as the output frequency */ |
411 | CurrTime = SDL_GetTicks(); |
412 | ExpectedTime = last_callback_ticks + ((1000 * SecondaryBufferSize) / OutputFreq); |
413 | ExpectedLevel = CurrLevel; |
414 | if (CurrTime < ExpectedTime) |
415 | ExpectedLevel += (ExpectedTime - CurrTime) * OutputFreq / 1000; |
416 | /* If the expected value of the Primary Buffer Fullness at the time of the next audio callback is more than 10 |
417 | milliseconds ahead of our target buffer fullness level, then insert a delay now */ |
418 | DebugMessage(M64MSG_VERBOSE, "%03i New audio bytes: %i Time to next callback: %i Current/Expected buffer level: %i/%i", |
419 | CurrTime % 1000, LenReg, (int) (ExpectedTime - CurrTime), CurrLevel, ExpectedLevel); |
420 | if (ExpectedLevel >= PrimaryBufferTarget + OutputFreq / 100) |
421 | { |
422 | unsigned int WaitTime = (ExpectedLevel - PrimaryBufferTarget) * 1000 / OutputFreq; |
423 | DebugMessage(M64MSG_VERBOSE, " AiLenChanged(): Waiting %ims", WaitTime); |
424 | if (l_PausedForSync) |
425 | SDL_PauseAudio(0); |
426 | l_PausedForSync = 0; |
427 | SDL_Delay(WaitTime); |
428 | } |
429 | /* Or if the expected level of the primary buffer is less than the secondary buffer size |
430 | (ie, predicting an underflow), then pause the audio to let the emulator catch up to speed */ |
431 | else if (ExpectedLevel < SecondaryBufferSize) |
432 | { |
433 | DebugMessage(M64MSG_VERBOSE, " AiLenChanged(): Possible underflow at next audio callback; pausing playback"); |
434 | if (!l_PausedForSync) |
435 | SDL_PauseAudio(1); |
436 | l_PausedForSync = 1; |
437 | } |
438 | /* otherwise the predicted buffer level is within our tolerance, so everything is okay */ |
439 | else |
440 | { |
441 | if (l_PausedForSync) |
442 | SDL_PauseAudio(0); |
443 | l_PausedForSync = 0; |
444 | } |
445 | } |
446 | |
447 | EXPORT int CALL InitiateAudio( AUDIO_INFO Audio_Info ) |
448 | { |
449 | if (!l_PluginInit) |
450 | return 0; |
451 | |
452 | AudioInfo = Audio_Info; |
453 | return 1; |
454 | } |
455 | |
456 | static int underrun_count = 0; |
457 | |
458 | #ifdef USE_SRC |
459 | static float *_src = NULL; |
460 | static unsigned int _src_len = 0; |
461 | static float *_dest = NULL; |
462 | static unsigned int _dest_len = 0; |
463 | static int error; |
464 | static SRC_STATE *src_state; |
465 | static SRC_DATA src_data; |
466 | #endif |
467 | #ifdef USE_SPEEX |
468 | SpeexResamplerState* spx_state = NULL; |
469 | static int error; |
470 | #endif |
471 | |
472 | static int resample(unsigned char *input, int input_avail, int oldsamplerate, unsigned char *output, int output_needed, int newsamplerate) |
473 | { |
474 | int *psrc = (int*)input; |
475 | int *pdest = (int*)output; |
476 | int i = 0, j = 0; |
477 | |
478 | #ifdef USE_SPEEX |
479 | spx_uint32_t in_len, out_len; |
480 | if(Resample == RESAMPLER_SPEEX) |
481 | { |
482 | if(spx_state == NULL) |
483 | { |
484 | spx_state = speex_resampler_init(2, oldsamplerate, newsamplerate, ResampleQuality, &error); |
485 | if(spx_state == NULL) |
486 | { |
487 | memset(output, 0, output_needed); |
488 | return 0; |
489 | } |
490 | } |
491 | speex_resampler_set_rate(spx_state, oldsamplerate, newsamplerate); |
492 | in_len = input_avail / 4; |
493 | out_len = output_needed / 4; |
494 | |
495 | if ((error = speex_resampler_process_interleaved_int(spx_state, (const spx_int16_t *)input, &in_len, (spx_int16_t *)output, &out_len))) |
496 | { |
497 | memset(output, 0, output_needed); |
498 | return input_avail; // number of bytes consumed |
499 | } |
500 | return in_len * 4; |
501 | } |
502 | #endif |
503 | #ifdef USE_SRC |
504 | if(Resample == RESAMPLER_SRC) |
505 | { |
506 | // the high quality resampler needs more input than the samplerate ratio would indicate to work properly |
507 | if (input_avail > output_needed * 3 / 2) |
508 | input_avail = output_needed * 3 / 2; // just to avoid too much short-float-short conversion time |
509 | if (_src_len < input_avail*2 && input_avail > 0) |
510 | { |
511 | if(_src) free(_src); |
512 | _src_len = input_avail*2; |
513 | _src = malloc(_src_len); |
514 | } |
515 | if (_dest_len < output_needed*2 && output_needed > 0) |
516 | { |
517 | if(_dest) free(_dest); |
518 | _dest_len = output_needed*2; |
519 | _dest = malloc(_dest_len); |
520 | } |
521 | memset(_src,0,_src_len); |
522 | memset(_dest,0,_dest_len); |
523 | if(src_state == NULL) |
524 | { |
525 | src_state = src_new (ResampleQuality, 2, &error); |
526 | if(src_state == NULL) |
527 | { |
528 | memset(output, 0, output_needed); |
529 | return 0; |
530 | } |
531 | } |
532 | src_short_to_float_array ((short *) input, _src, input_avail/2); |
533 | src_data.end_of_input = 0; |
534 | src_data.data_in = _src; |
535 | src_data.input_frames = input_avail/4; |
536 | src_data.src_ratio = (float) newsamplerate / oldsamplerate; |
537 | src_data.data_out = _dest; |
538 | src_data.output_frames = output_needed/4; |
539 | if ((error = src_process (src_state, &src_data))) |
540 | { |
541 | memset(output, 0, output_needed); |
542 | return input_avail; // number of bytes consumed |
543 | } |
544 | src_float_to_short_array (_dest, (short *) output, output_needed/2); |
545 | return src_data.input_frames_used * 4; |
546 | } |
547 | #endif |
548 | // RESAMPLE == TRIVIAL |
549 | if (newsamplerate >= oldsamplerate) |
550 | { |
551 | int sldf = oldsamplerate; |
552 | int const2 = 2*sldf; |
553 | int dldf = newsamplerate; |
554 | int const1 = const2 - 2*dldf; |
555 | int criteria = const2 - dldf; |
556 | for (i = 0; i < output_needed/4; i++) |
557 | { |
558 | pdest[i] = psrc[j]; |
559 | if(criteria >= 0) |
560 | { |
561 | ++j; |
562 | criteria += const1; |
563 | } |
564 | else criteria += const2; |
565 | } |
566 | return j * 4; //number of bytes consumed |
567 | } |
568 | // newsamplerate < oldsamplerate, this only happens when speed_factor > 1 |
569 | for (i = 0; i < output_needed/4; i++) |
570 | { |
571 | j = i * oldsamplerate / newsamplerate; |
572 | pdest[i] = psrc[j]; |
573 | } |
574 | return j * 4; //number of bytes consumed |
575 | } |
576 | |
577 | static void my_audio_callback(void *userdata, unsigned char *stream, int len) |
578 | { |
579 | int oldsamplerate, newsamplerate; |
580 | |
581 | if (!l_PluginInit) |
582 | return; |
583 | |
584 | /* mark the time, for synchronization on the input side */ |
585 | last_callback_ticks = SDL_GetTicks(); |
586 | |
587 | newsamplerate = OutputFreq * 100 / speed_factor; |
588 | oldsamplerate = GameFreq; |
589 | |
590 | if (buffer_pos > (unsigned int) (len * oldsamplerate) / newsamplerate) |
591 | { |
592 | int input_used; |
593 | #if defined(HAS_OSS_SUPPORT) |
594 | if (VolumeControlType == VOLUME_TYPE_OSS) |
595 | { |
596 | input_used = resample(primaryBuffer, buffer_pos, oldsamplerate, stream, len, newsamplerate); |
597 | } |
598 | else |
599 | #endif |
600 | { |
601 | input_used = resample(primaryBuffer, buffer_pos, oldsamplerate, mixBuffer, len, newsamplerate); |
602 | memset(stream, 0, len); |
603 | SDL_MixAudio(stream, mixBuffer, len, VolSDL); |
604 | } |
605 | memmove(primaryBuffer, &primaryBuffer[input_used], buffer_pos - input_used); |
606 | buffer_pos -= input_used; |
607 | DebugMessage(M64MSG_VERBOSE, "%03i my_audio_callback: used %i samples", |
608 | last_callback_ticks % 1000, len / SDL_SAMPLE_BYTES); |
609 | } |
610 | else |
611 | { |
612 | unsigned int SamplesNeeded = (len * oldsamplerate) / (newsamplerate * SDL_SAMPLE_BYTES); |
613 | unsigned int SamplesPresent = buffer_pos / N64_SAMPLE_BYTES; |
614 | underrun_count++; |
615 | DebugMessage(M64MSG_VERBOSE, "%03i Buffer underflow (%i). %i samples present, %i needed", |
616 | last_callback_ticks % 1000, underrun_count, SamplesPresent, SamplesNeeded); |
617 | memset(stream , 0, len); |
618 | } |
619 | } |
620 | EXPORT int CALL RomOpen(void) |
621 | { |
622 | if (!l_PluginInit) |
623 | return 0; |
624 | |
625 | ReadConfig(); |
626 | InitializeAudio(GameFreq); |
627 | return 1; |
628 | } |
629 | |
630 | static void InitializeSDL(void) |
631 | { |
632 | DebugMessage(M64MSG_INFO, "Initializing SDL audio subsystem..."); |
633 | |
634 | if(SDL_Init(SDL_INIT_AUDIO | SDL_INIT_TIMER) < 0) |
635 | { |
636 | DebugMessage(M64MSG_ERROR, "Failed to initialize SDL audio subsystem; forcing exit.\n"); |
637 | critical_failure = 1; |
638 | return; |
639 | } |
640 | critical_failure = 0; |
641 | |
642 | } |
643 | |
644 | static void CreatePrimaryBuffer(void) |
645 | { |
646 | unsigned int newPrimaryBytes = (unsigned int) ((long long) PrimaryBufferSize * GameFreq * speed_factor / |
647 | (OutputFreq * 100)) * N64_SAMPLE_BYTES; |
648 | if (primaryBuffer == NULL) |
649 | { |
650 | DebugMessage(M64MSG_VERBOSE, "Allocating memory for audio buffer: %i bytes.", newPrimaryBytes); |
651 | primaryBuffer = (unsigned char*) malloc(newPrimaryBytes); |
652 | memset(primaryBuffer, 0, newPrimaryBytes); |
653 | primaryBufferBytes = newPrimaryBytes; |
654 | } |
655 | else if (newPrimaryBytes > primaryBufferBytes) /* primary buffer only grows; there's no point in shrinking it */ |
656 | { |
657 | unsigned char *newPrimaryBuffer = (unsigned char*) malloc(newPrimaryBytes); |
658 | unsigned char *oldPrimaryBuffer = primaryBuffer; |
659 | SDL_LockAudio(); |
660 | memcpy(newPrimaryBuffer, oldPrimaryBuffer, primaryBufferBytes); |
661 | memset(newPrimaryBuffer + primaryBufferBytes, 0, newPrimaryBytes - primaryBufferBytes); |
662 | primaryBuffer = newPrimaryBuffer; |
663 | primaryBufferBytes = newPrimaryBytes; |
664 | SDL_UnlockAudio(); |
665 | free(oldPrimaryBuffer); |
666 | } |
667 | } |
668 | |
669 | static void InitializeAudio(int freq) |
670 | { |
671 | SDL_AudioSpec *desired, *obtained; |
672 | |
673 | if(SDL_WasInit(SDL_INIT_AUDIO|SDL_INIT_TIMER) == (SDL_INIT_AUDIO|SDL_INIT_TIMER) ) |
674 | { |
675 | DebugMessage(M64MSG_VERBOSE, "InitializeAudio(): SDL Audio sub-system already initialized."); |
676 | SDL_PauseAudio(1); |
677 | SDL_CloseAudio(); |
678 | } |
679 | else |
680 | { |
681 | DebugMessage(M64MSG_VERBOSE, "InitializeAudio(): Initializing SDL Audio"); |
682 | DebugMessage(M64MSG_VERBOSE, "Primary buffer: %i output samples.", PrimaryBufferSize); |
683 | DebugMessage(M64MSG_VERBOSE, "Primary target fullness: %i output samples.", PrimaryBufferTarget); |
684 | DebugMessage(M64MSG_VERBOSE, "Secondary buffer: %i output samples.", SecondaryBufferSize); |
685 | InitializeSDL(); |
686 | } |
687 | if (critical_failure == 1) |
688 | return; |
689 | GameFreq = freq; // This is important for the sync |
690 | if(hardware_spec != NULL) free(hardware_spec); |
691 | |
692 | // Allocate space for SDL_AudioSpec |
693 | desired = malloc(sizeof(SDL_AudioSpec)); |
694 | obtained = malloc(sizeof(SDL_AudioSpec)); |
695 | |
696 | if(freq < 11025) OutputFreq = 11025; |
697 | else if(freq < 22050) OutputFreq = 22050; |
698 | else OutputFreq = 44100; |
699 | |
700 | desired->freq = OutputFreq; |
701 | |
702 | DebugMessage(M64MSG_VERBOSE, "Requesting frequency: %iHz.", desired->freq); |
703 | /* 16-bit signed audio */ |
704 | desired->format=AUDIO_S16SYS; |
705 | DebugMessage(M64MSG_VERBOSE, "Requesting format: %i.", desired->format); |
706 | /* Stereo */ |
707 | desired->channels=2; |
708 | /* reload these because they gets re-assigned from SDL data below, and InitializeAudio can be called more than once */ |
709 | PrimaryBufferSize = ConfigGetParamInt(l_ConfigAudio, "PRIMARY_BUFFER_SIZE"); |
710 | PrimaryBufferTarget = ConfigGetParamInt(l_ConfigAudio, "PRIMARY_BUFFER_TARGET"); |
711 | SecondaryBufferSize = ConfigGetParamInt(l_ConfigAudio, "SECONDARY_BUFFER_SIZE"); |
712 | desired->samples = SecondaryBufferSize; |
713 | /* Our callback function */ |
714 | desired->callback = my_audio_callback; |
715 | desired->userdata = NULL; |
716 | |
717 | /* Open the audio device */ |
718 | l_PausedForSync = 1; |
719 | if (SDL_OpenAudio(desired, obtained) < 0) |
720 | { |
721 | DebugMessage(M64MSG_ERROR, "Couldn't open audio: %s", SDL_GetError()); |
722 | critical_failure = 1; |
723 | return; |
724 | } |
725 | if (desired->format != obtained->format) |
726 | { |
727 | DebugMessage(M64MSG_WARNING, "Obtained audio format differs from requested."); |
728 | } |
729 | if (desired->freq != obtained->freq) |
730 | { |
731 | DebugMessage(M64MSG_WARNING, "Obtained frequency differs from requested."); |
732 | } |
733 | |
734 | /* desired spec is no longer needed */ |
735 | free(desired); |
736 | hardware_spec=obtained; |
737 | |
738 | /* allocate memory for audio buffers */ |
739 | OutputFreq = hardware_spec->freq; |
740 | SecondaryBufferSize = hardware_spec->samples; |
741 | if (PrimaryBufferTarget < SecondaryBufferSize) |
742 | PrimaryBufferTarget = SecondaryBufferSize; |
743 | if (PrimaryBufferSize < PrimaryBufferTarget) |
744 | PrimaryBufferSize = PrimaryBufferTarget; |
745 | if (PrimaryBufferSize < SecondaryBufferSize * 2) |
746 | PrimaryBufferSize = SecondaryBufferSize * 2; |
747 | CreatePrimaryBuffer(); |
748 | if (mixBuffer != NULL) |
749 | free(mixBuffer); |
750 | mixBuffer = (unsigned char*) malloc(SecondaryBufferSize * SDL_SAMPLE_BYTES); |
751 | |
752 | /* preset the last callback time */ |
753 | if (last_callback_ticks == 0) |
754 | last_callback_ticks = SDL_GetTicks(); |
755 | |
756 | DebugMessage(M64MSG_VERBOSE, "Frequency: %i", hardware_spec->freq); |
757 | DebugMessage(M64MSG_VERBOSE, "Format: %i", hardware_spec->format); |
758 | DebugMessage(M64MSG_VERBOSE, "Channels: %i", hardware_spec->channels); |
759 | DebugMessage(M64MSG_VERBOSE, "Silence: %i", hardware_spec->silence); |
760 | DebugMessage(M64MSG_VERBOSE, "Samples: %i", hardware_spec->samples); |
761 | DebugMessage(M64MSG_VERBOSE, "Size: %i", hardware_spec->size); |
762 | |
763 | /* set playback volume */ |
764 | #if defined(HAS_OSS_SUPPORT) |
765 | if (VolumeControlType == VOLUME_TYPE_OSS) |
766 | { |
767 | VolPercent = volGet(); |
768 | } |
769 | else |
770 | #endif |
771 | { |
772 | VolSDL = SDL_MIX_MAXVOLUME * VolPercent / 100; |
773 | } |
774 | |
775 | } |
776 | EXPORT void CALL RomClosed( void ) |
777 | { |
778 | if (!l_PluginInit) |
779 | return; |
780 | if (critical_failure == 1) |
781 | return; |
782 | DebugMessage(M64MSG_VERBOSE, "Cleaning up SDL sound plugin..."); |
783 | |
784 | // Shut down SDL Audio output |
785 | SDL_PauseAudio(1); |
786 | SDL_CloseAudio(); |
787 | |
788 | // Delete the buffer, as we are done producing sound |
789 | if (primaryBuffer != NULL) |
790 | { |
791 | primaryBufferBytes = 0; |
792 | free(primaryBuffer); |
793 | primaryBuffer = NULL; |
794 | } |
795 | if (mixBuffer != NULL) |
796 | { |
797 | free(mixBuffer); |
798 | mixBuffer = NULL; |
799 | } |
800 | |
801 | // Delete the hardware spec struct |
802 | if(hardware_spec != NULL) free(hardware_spec); |
803 | hardware_spec = NULL; |
804 | |
805 | // Shutdown the respective subsystems |
806 | if(SDL_WasInit(SDL_INIT_AUDIO) != 0) SDL_QuitSubSystem(SDL_INIT_AUDIO); |
807 | if(SDL_WasInit(SDL_INIT_TIMER) != 0) SDL_QuitSubSystem(SDL_INIT_TIMER); |
808 | } |
809 | |
810 | EXPORT void CALL ProcessAList(void) |
811 | { |
812 | } |
813 | |
814 | EXPORT void CALL SetSpeedFactor(int percentage) |
815 | { |
816 | if (!l_PluginInit) |
817 | return; |
818 | if (percentage >= 10 && percentage <= 300) |
819 | speed_factor = percentage; |
820 | // we need a different size primary buffer to store the N64 samples when the speed changes |
821 | CreatePrimaryBuffer(); |
822 | } |
823 | |
824 | static void ReadConfig(void) |
825 | { |
826 | const char *resampler_id; |
827 | |
828 | /* read the configuration values into our static variables */ |
829 | GameFreq = ConfigGetParamInt(l_ConfigAudio, "DEFAULT_FREQUENCY"); |
830 | SwapChannels = ConfigGetParamBool(l_ConfigAudio, "SWAP_CHANNELS"); |
831 | PrimaryBufferSize = ConfigGetParamInt(l_ConfigAudio, "PRIMARY_BUFFER_SIZE"); |
832 | PrimaryBufferTarget = ConfigGetParamInt(l_ConfigAudio, "PRIMARY_BUFFER_TARGET"); |
833 | SecondaryBufferSize = ConfigGetParamInt(l_ConfigAudio, "SECONDARY_BUFFER_SIZE"); |
834 | resampler_id = ConfigGetParamString(l_ConfigAudio, "RESAMPLE"); |
835 | VolumeControlType = ConfigGetParamInt(l_ConfigAudio, "VOLUME_CONTROL_TYPE"); |
836 | VolDelta = ConfigGetParamInt(l_ConfigAudio, "VOLUME_ADJUST"); |
837 | VolPercent = ConfigGetParamInt(l_ConfigAudio, "VOLUME_DEFAULT"); |
838 | |
839 | if (!resampler_id) { |
840 | Resample = RESAMPLER_TRIVIAL; |
841 | DebugMessage(M64MSG_WARNING, "Could not find RESAMPLE configuration; use trivial resampler"); |
842 | return; |
843 | } |
844 | if (strcmp(resampler_id, "trivial") == 0) { |
845 | Resample = RESAMPLER_TRIVIAL; |
846 | return; |
847 | } |
848 | #ifdef USE_SPEEX |
849 | if (strncmp(resampler_id, "speex-fixed-", strlen("speex-fixed-")) == 0) { |
850 | int i; |
851 | static const char *speex_quality[] = { |
852 | "speex-fixed-0", |
853 | "speex-fixed-1", |
854 | "speex-fixed-2", |
855 | "speex-fixed-3", |
856 | "speex-fixed-4", |
857 | "speex-fixed-5", |
858 | "speex-fixed-6", |
859 | "speex-fixed-7", |
860 | "speex-fixed-8", |
861 | "speex-fixed-9", |
862 | "speex-fixed-10", |
863 | }; |
864 | Resample = RESAMPLER_SPEEX; |
865 | for (i = 0; i < sizeof(speex_quality) / sizeof(*speex_quality); i++) { |
866 | if (strcmp(speex_quality[i], resampler_id) == 0) { |
867 | ResampleQuality = i; |
868 | return; |
869 | } |
870 | } |
871 | DebugMessage(M64MSG_WARNING, "Unknown RESAMPLE configuration %s; use speex-fixed-4 resampler", resampler_id); |
872 | ResampleQuality = 4; |
873 | return; |
874 | } |
875 | #endif |
876 | #ifdef USE_SRC |
877 | if (strncmp(resampler_id, "src-", strlen("src-")) == 0) { |
878 | Resample = RESAMPLER_SRC; |
879 | if (strcmp(resampler_id, "src-sinc-best-quality") == 0) { |
880 | ResampleQuality = SRC_SINC_BEST_QUALITY; |
881 | return; |
882 | } |
883 | if (strcmp(resampler_id, "src-sinc-medium-quality") == 0) { |
884 | ResampleQuality = SRC_SINC_MEDIUM_QUALITY; |
885 | return; |
886 | } |
887 | if (strcmp(resampler_id, "src-sinc-fastest") == 0) { |
888 | ResampleQuality = SRC_SINC_FASTEST; |
889 | return; |
890 | } |
891 | if (strcmp(resampler_id, "src-zero-order-hold") == 0) { |
892 | ResampleQuality = SRC_ZERO_ORDER_HOLD; |
893 | return; |
894 | } |
895 | if (strcmp(resampler_id, "src-linear") == 0) { |
896 | ResampleQuality = SRC_LINEAR; |
897 | return; |
898 | } |
899 | DebugMessage(M64MSG_WARNING, "Unknown RESAMPLE configuration %s; use src-sinc-medium-quality resampler", resampler_id); |
900 | ResampleQuality = SRC_SINC_MEDIUM_QUALITY; |
901 | return; |
902 | } |
903 | #endif |
904 | DebugMessage(M64MSG_WARNING, "Unknown RESAMPLE configuration %s; use trivial resampler", resampler_id); |
905 | Resample = RESAMPLER_TRIVIAL; |
906 | } |
907 | |
908 | // Returns the most recent ummuted volume level. |
909 | static int VolumeGetUnmutedLevel(void) |
910 | { |
911 | #if defined(HAS_OSS_SUPPORT) |
912 | // reload volume if we're using OSS |
913 | if (!VolIsMuted && VolumeControlType == VOLUME_TYPE_OSS) |
914 | { |
915 | return volGet(); |
916 | } |
917 | #endif |
918 | |
919 | return VolPercent; |
920 | } |
921 | |
922 | // Sets the volume level based on the contents of VolPercent and VolIsMuted |
923 | static void VolumeCommit(void) |
924 | { |
925 | int levelToCommit = VolIsMuted ? 0 : VolPercent; |
926 | |
927 | #if defined(HAS_OSS_SUPPORT) |
928 | if (VolumeControlType == VOLUME_TYPE_OSS) |
929 | { |
930 | //OSS mixer volume |
931 | volSet(levelToCommit); |
932 | } |
933 | else |
934 | #endif |
935 | { |
936 | VolSDL = SDL_MIX_MAXVOLUME * levelToCommit / 100; |
937 | } |
938 | } |
939 | |
940 | EXPORT void CALL VolumeMute(void) |
941 | { |
942 | if (!l_PluginInit) |
943 | return; |
944 | |
945 | // Store the volume level in order to restore it later |
946 | if (!VolIsMuted) |
947 | VolPercent = VolumeGetUnmutedLevel(); |
948 | |
949 | // Toogle mute |
950 | VolIsMuted = !VolIsMuted; |
951 | VolumeCommit(); |
952 | } |
953 | |
954 | EXPORT void CALL VolumeUp(void) |
955 | { |
956 | if (!l_PluginInit) |
957 | return; |
958 | |
959 | VolumeSetLevel(VolumeGetUnmutedLevel() + VolDelta); |
960 | } |
961 | |
962 | EXPORT void CALL VolumeDown(void) |
963 | { |
964 | if (!l_PluginInit) |
965 | return; |
966 | |
967 | VolumeSetLevel(VolumeGetUnmutedLevel() - VolDelta); |
968 | } |
969 | |
970 | EXPORT int CALL VolumeGetLevel(void) |
971 | { |
972 | return VolIsMuted ? 0 : VolumeGetUnmutedLevel(); |
973 | } |
974 | |
975 | EXPORT void CALL VolumeSetLevel(int level) |
976 | { |
977 | if (!l_PluginInit) |
978 | return; |
979 | |
980 | //if muted, unmute first |
981 | VolIsMuted = 0; |
982 | |
983 | // adjust volume |
984 | VolPercent = level; |
985 | if (VolPercent < 0) |
986 | VolPercent = 0; |
987 | else if (VolPercent > 100) |
988 | VolPercent = 100; |
989 | |
990 | VolumeCommit(); |
991 | } |
992 | |
993 | EXPORT const char * CALL VolumeGetString(void) |
994 | { |
995 | static char VolumeString[32]; |
996 | |
997 | if (VolIsMuted) |
998 | { |
999 | strcpy(VolumeString, "Mute"); |
1000 | } |
1001 | else |
1002 | { |
1003 | sprintf(VolumeString, "%i%%", VolPercent); |
1004 | } |
1005 | |
1006 | return VolumeString; |
1007 | } |
1008 | |