'OHRRPGCE - music_sdl and music_sdl2 audio backends '(C) Copyright 1997-2020 James Paige, Ralph Versteegen, and the OHRRPGCE Developers 'Dual licensed under the GNU GPL v2+ and MIT Licenses. Read LICENSE.txt for terms and disclaimer of liability. ' ' music_sdl.bas - This compiles to both music_sdl and music_sdl2 audio backends, ' music_sdl: SDL 1.2 + SDL_mixer 1.2 (when SDL_MIXER2 not defined) ' music_sdl2: SDL 2 + SDL_mixer 2 (when SDL_MIXER2 defined, ie when included from music_sdl2.bas) ' It isn't possible to link both backends into the engine at once. #include "config.bi" #ifdef __FB_WIN32__ 'In FB >= 1.04 SDL.bi includes windows.bi; we have to include it first to do the necessary conflict prevention include_windows_bi() #endif #include "music.bi" #include "gfx.bi" #include "util.bi" #include "common.bi" #include "backendinfo.bi" 'warning: due to a FB bug, overloaded functions must be declared before SDL.bi is included #ifdef __FB_UNIX__ 'In FB >= 1.04 SDL.bi includes Xlib.bi; fix a conflict #undef font #endif #ifdef SDL_MIXER2 #include "SDL2\SDL.bi" #include "SDL2\SDL_mixer.bi" #else #include "SDL\SDL.bi" #include "SDL\SDL_mixer.bi" #endif #ifndef Mix_ClearError 'Missing from SDL_mixer 1.2's header #define Mix_ClearError SDL_ClearError #endif ' External functions #ifndef SDL_MIXER2 declare function safe_RWops(byval rw as SDL_RWops ptr) as SDL_RWops ptr declare sub safe_RWops_close (byval rw as SDL_RWops ptr) #endif extern "C" declare function SDL_RWFromLump(byval lump as Lump ptr) as SDL_RWops ptr 'The decoder enum functions are only available in SDL_mixer > 1.2.8 which is the version shipped with 'Debian 6.0 Squeeze and hence older Ubuntu. Squeeze was superceded by 7.0 Wheezy in May 2013. 'So don't depend on these functions. dim shared _Mix_GetNumMusicDecoders as function () as Sint32 dim shared _Mix_GetNumChunkDecoders as function () as Sint32 dim shared _Mix_GetMusicDecoder as function (byval index as Sint32) as zstring ptr dim shared _Mix_GetChunkDecoder as function (byval index as Sint32) as zstring ptr 'We might not actually link to libmodplug, but want the type/enum declarations. 'Warning: does #inclib "modplug", which we don't actually want. 'Luckily as long as not building with "scons linkgcc=0", #inclibs are ignored. #include "modplug.bi" 'These are only available if SDL_mixer has been statically linked with libmodplug and 'exports its symbols (as our builds of SDL_mixer for Windows and Mac do) dim shared _ModPlug_GetSettings as sub (byval settings as ModPlug_Settings ptr) dim shared _ModPlug_SetSettings as sub (byval settings as const ModPlug_Settings ptr) #ifndef MIX_INIT_MID 'Exists in SDL_mixer 2 only (but missing from older FB headers). 'Equal to MIX_INIT_FLUIDSYNTH in SDL_mixer 1.2. #define MIX_INIT_MID &h00000020 #endif end extern ' Local functions declare function next_free_slot() as integer declare function sfx_slot_info (byval slot as integer) as string declare sub enable_modplug_looping() enum MusicStatusEnum musicError = -1 ' Don't try again musicOff = 0 musicOn = 1 end enum dim shared mixer_version as integer 'E.g. 1213 for SDL_mixer 1.2.13 dim shared supported_formats as integer dim shared have_modplug as bool dim shared tried_enabling_modplug_loops as bool dim shared modplug_handle as any ptr dim shared music_status as MusicStatusEnum = musicOff dim shared music_vol as double '0 to 1 nominally; values above 1 useful when other multipliers exist dim shared music_paused as bool 'Always false: we never pause! (see r5406) dim shared music_song as Mix_Music ptr = NULL dim shared music_song_rw as SDL_RWops ptr = NULL dim shared orig_vol as integer = -1 dim shared nonmidi_playing as bool = NO 'The music module needs to manage a list of temporary files to delete when closed dim shared tempfiles() as string dim shared callback_set_up as bool = NO sub quit_sdl_audio() 'Close libmodplug if modplug_handle then dylibfree modplug_handle modplug_handle = NULL end if tried_enabling_modplug_loops = NO if SDL_WasInit(SDL_INIT_AUDIO) then SDL_QuitSubSystem(SDL_INIT_AUDIO) if SDL_WasInit(0) = 0 then SDL_Quit() end if end if end sub function music_get_info() as string #ifdef SDL_MIXER2 #define sdlX "SDL2" ' The .dll/.so name is in globals.bas, generated by verprint #define SONAME libsdl2_mixer_name #else #define sdlX "SDL" #define SONAME libsdl_mixer_name #endif dim ret as string = "music_" & lcase(sdlX) dim libhandle as any ptr #ifdef __FB_DARWIN__ libhandle = dylib_noload(sdlX "_mixer.framework/" sdlX "_mixer") #elseif defined(__FB_WIN32__) or defined(__FB_JS__) 'Shouldn't need to bother with dylib_noload libhandle = dylibload(SONAME) #else ' __FB_UNIX__ 'Especially on Linux must make sure we don't load a different (system) .so 'to the one we're linked to (possibly a library in linux/$arch/) 'Don't really need to bother with dylib_noload on Windows libhandle = dylib_noload(SONAME) if libhandle then _Mix_GetNumMusicDecoders = dylibsymbol(libhandle, "Mix_GetNumMusicDecoders") _Mix_GetNumChunkDecoders = dylibsymbol(libhandle, "Mix_GetNumChunkDecoders") _Mix_GetMusicDecoder = dylibsymbol(libhandle, "Mix_GetMusicDecoder") _Mix_GetChunkDecoder = dylibsymbol(libhandle, "Mix_GetChunkDecoder") _ModPlug_GetSettings = dylibsymbol(libhandle, "ModPlug_GetSettings") _ModPlug_SetSettings = dylibsymbol(libhandle, "ModPlug_SetSettings") else debug "dylib_noload(" & SONAME & ") failed. Continuing" end if #endif dim ver as const SDL_version ptr if gfxbackend <> "sdl" andalso gfxbackend <> "sdl2" then #ifdef SDL_MIXER2 dim ver2 as SDL_version SDL_GetVersion(@ver2) ver = @ver2 #else ver = SDL_Linked_Version() #endif ret += "SDL " & ver->major & "." & ver->minor & "." & ver->patch & ", " end if #if not (defined(__FB_JS__) and not defined(SDL_MIXER2)) 'Emscripten's Mix_Linked_Version() just throws an exception ver = Mix_Linked_Version() ret += "SDL_Mixer " & ver->major & "." & ver->minor & "." & ver->patch mixer_version = ver->major * 1000 + ver->minor * 100 + ver->patch #endif if music_status = musicOn then #if not (defined(__FB_JS__) and not defined(SDL_MIXER2)) 'Emscripten's Mix_QuerySpec() just throws an exception dim freq as int32, format as ushort, channels as int32 Mix_QuerySpec(@freq, @format, @channels) ret += " (" & freq & "Hz" #endif have_modplug = NO if _Mix_GetNumMusicDecoders andalso _Mix_GetMusicDecoder then ret += ", Music decoders:" for i as integer = 0 to _Mix_GetNumMusicDecoders() - 1 if i > 0 then ret += "," dim form as string = *_Mix_GetMusicDecoder(i) ret += form 'SDL2_mixer lists the file formats in the list of decoders, 'SDL_mixer 1.2 may only list the decoders, such as MIKMOD. 'Mix_GetChunkDecoder only lists file formats, not decoders. if form = "MP3" then 'Note: SDL_mixer 1.2 reports "MP3" only regardless of which library it's 'using, 2.x also reports "MPG123", "DRMP3", "SMPEG", or "MAD". 'SDL_mixer 1.2.13 (unreleased) supports only libmad or libmpg123, not smpeg. 'smpeg may crash for non-44.1kHz MP3s (bug #372), so don't support them on 'SDL_mixer <= 1.2.12 Mac/Linux, where it's very probably linked to smpeg. '(Latest SDL_mixer 1.2.13 from git, used by some distros, has finally 'dropped smpeg support) #ifdef __FB_WIN32__ supported_formats or= FORMAT_MP3 #else if mixer_version >= 1213 then supported_formats or= FORMAT_MP3 end if #endif elseif form = "OGG" then supported_formats or= FORMAT_OGG elseif form = "FLAC" then supported_formats or= FORMAT_FLAC elseif form = "WAVE" then supported_formats or= FORMAT_WAV elseif form = "MOD" then supported_formats or= FORMAT_MODULES elseif form = "MODPLUG" then supported_formats or= FORMAT_MODULES have_modplug = YES elseif form = "MIKMOD" then supported_formats or= FORMAT_MODULES elseif form = "MIDI" or form = "TIMIDITY" or form = "FLUIDSYNTH" or form = "NATIVEMIDI" then supported_formats or= FORMAT_MIDI or FORMAT_BAM end if next else 'A very out of date copy of SDL_mixer (1.2). Assume linked to SMPEG supported_formats = FORMAT_BAM or FORMAT_MIDI or FORMAT_MODULES or FORMAT_OGG or FORMAT_WAV end if if _Mix_GetNumChunkDecoders andalso _Mix_GetChunkDecoder then 'BTW, SDL_mixer 1.2 doesn't support playing .mp3 sound effects (chunks)! ret += " Sample decoders:" for i as integer = 0 to _Mix_GetNumChunkDecoders() - 1 if i > 0 then ret += "," ret += *_Mix_GetChunkDecoder(i) next end if ret += ")" end if if libhandle then dylibfree(libhandle) return ret end function 'Note that the backend will still be asked to play files it doesn't report supporting function music_supported_formats() as integer return supported_formats and VALID_MUSIC_FORMAT end function function sound_supported_formats() as integer return supported_formats and VALID_SFX_FORMAT end function sub music_init() if music_status = musicOff then dim audio_rate as integer dim audio_format as Uint16 dim audio_channels as integer dim audio_buffers as integer #if defined(__FB_UNIX__) and defined(SDL_MIXER2) 'SDL2_mixer only looks for soundfonts for its default fluidsynth MIDI backend at '/usr/share/sounds/sf2/FluidR3_GM.sf2 but there are a couple other common places for distros 'to put them; otherwise the user needs to set the SDL_SOUNDFONTS envvar. Mix_SetSoundFonts 'overrides SDL_SOUNDFONTS, so make sure we don't do that, and the built default is used only 'if neither of those is available. dim soundfonts as zstring ptr = SDL_getenv("SDL_SOUNDFONTS") if soundfonts = NULL orelse len(*soundfonts) = 0 then 'This list based on one from OpenTTD dim default_paths(...) as string = { _ _ ' Debian/Ubuntu/OpenSUSE/Slackware "/usr/share/sounds/sf2/FluidR3_GM.sf2", _ "/usr/share/sounds/sf2/TimGM6mb.sf2", _ "/usr/share/sounds/sf2/FluidR3_GS.sf2", _ _ ' RedHat/Fedora/Arch "/usr/share/soundfonts/FluidR3_GM.sf2", _ "/usr/share/soundfonts/FluidR3_GS.sf2" _ } 'Only pass on paths that exist, otherwise errors are printed to stderr dim paths as string for idx as integer = 0 to ubound(default_paths) if isfile(default_paths(idx)) then if len(paths) then paths &= ":;" paths &= default_paths(idx) end if next if len(paths) then Mix_SetSoundFonts(strptr(paths)) else debuginfo "Warning: no soundfonts for MIDI playback using fluidsynth found in common " _ "locations. Set the SDL_SOUNDFONTS environmental variable if you've installed them." end if end if #endif #ifndef SDL_MIXER2 ' MIX_DEFAULT_FREQUENCY is 22050, which slightly worsens sound quality ' than playing at 44100, but using 44100 causes tracks between 22-44kHz ' to be sped up, which sounds worse. See https://github.com/ohrrpgce/ohrrpgce/issues/1085 audio_rate = MIX_DEFAULT_FREQUENCY 'Despite the documentation, non power of 2 buffer size MAY work depending on the driver, and pygame even does it '1024 seems to give much lower delay than 1536 before being played, maybe a non-power of two problem audio_buffers = 1024 '1536 #else ' SDL_mixer 2: the above problem doesn't apply ' TODO: make configurable #ifdef __FB_BLACKBOX__ audio_rate = 48000 #else audio_rate = 44100 #endif audio_buffers = 2048 'Might as well increase to match (effect not investigated) 'SDL_SetHint("SDL_MIXER_DEBUG_MUSIC_INTERFACES", "1") #endif audio_format = MIX_DEFAULT_FORMAT audio_channels = 2 if SDL_WasInit(0) = 0 then if SDL_Init(SDL_INIT_AUDIO) then debug "Can't start SDL (audio): " & *SDL_GetError music_status = musicError exit sub end if elseif SDL_WasInit(SDL_INIT_AUDIO) = 0 then if SDL_InitSubSystem(SDL_INIT_AUDIO) then debug "Can't start SDL audio subsys: " & *SDL_GetError music_status = musicError quit_sdl_audio() exit sub end if end if if (Mix_OpenAudio(audio_rate, audio_format, audio_channels, audio_buffers)) <> 0 then 'if (Mix_OpenAudio(audio_rate, audio_format, audio_channels, 2048)) <> 0 then debug "Can't open audio : " & *Mix_GetError music_status = musicError quit_sdl_audio() exit sub 'end if end if music_vol = 0.5 music_status = musicOn music_paused = NO 'Kludge, just for Mix_GetChunkDecoder/Mix_GetMusicDecoder: these don't tell 'about all supported formats until the dynamic libraries are actually 'loaded. So force loading them. (SDL_mixer 2.0.2 only, earlier versions always 'loaded everything from Mix_OpenAudio). Increasing startup time just to get 'supported_formats is sad, but at least it prevents pauses later. Mix_Init(MIX_INIT_MID or MIX_INIT_OGG or MIX_INIT_MP3 or MIX_INIT_MOD or MIX_INIT_FLAC) 'SDL_mixer 1.2.12 bug: if compiled against libmad Mix_Init sets the error "Mixer not built with MP3 support" 'if Mix_GetError then debug "Mix_Init: " & *Mix_GetError Mix_ClearError end if end sub sub music_close() if music_status = musicOn then if orig_vol > 0 then 'restore original volume Mix_VolumeMusic(orig_vol) else 'arbitrary medium value Mix_VolumeMusic(0.5 * MIX_MAX_VOLUME) end if music_stop() Mix_CloseAudio() quit_sdl_audio() music_status = musicOff callback_set_up = NO ' For SFX for i as integer = 0 to ubound(tempfiles) safekill tempfiles(i) next erase tempfiles end if end sub sub music_play(byval lump as Lump ptr, byval fmt as MusicFormatEnum) end sub sub music_play(filename as string, byval fmt as MusicFormatEnum) if music_status = musicOn then dim songname as string = filename if fmt = FORMAT_BAM then dim midname as string 'use last 3 hex digits of length as a kind of hash, 'to verify that the .bmd does belong to this file '(Note that all instances of Custom currently share tmpdir; 'should fix that) dim as integer fhash = filelen(songname) and &h0fff midname = tmpdir & trimpath(songname) & "-" & lcase(hex(fhash)) & ".bmd" 'check if already converted if isfile(midname) = NO then bam2mid(songname, midname) a_append tempfiles(), midname end if songname = midname fmt = FORMAT_MIDI end if music_stop #ifndef __FB_WIN32__ if getmusictype(songname) and FORMAT_MODULES then 'Hack. Work around SDL_mixer bug 1499: SDL_mixer (before Jan 2021) 'and SDL2_mixer (before 2.6.0) did not enable loop points in modplug '(nor mikmod), although our Windows builds had it enabled. In case 'not using a custom build, try to enable loop points. Must happen 'before playing. enable_modplug_looping end if #endif log_openfile songname 'Versions of SDL_mixer 1.2 before 1.2.12 (the final release) failed to 'close the file when playing MOD or WAV music files using Mix_LoadMUS! '(Bugs 1021, 1168). 'So we use Mix_LoadMUS_RW instead. 'In SDL_mixer 1.2, Mix_LoadMUS_RW does not close the RWops, so we close it 'after stopping the music using the complicated safe_RWops() wrapper logic. 'SDL_mixer 2.0.0 fixes this problem, adding an argument to Mix_LoadMUS_RW 'telling whether to close the RWops. #ifdef SDL_MIXER2 music_song = Mix_LoadMUS(songname) #else music_song_rw = SDL_RWFromFile(songname, @"rb") if music_song_rw = NULL then debug "Couldn't SDL_RWFromFile(" + songname + "): " & *SDL_GetError() exit sub end if music_song_rw = safe_RWops(music_song_rw) music_song = Mix_LoadMUS_RW(music_song_rw) #endif if music_song = 0 then debug "Could not load song " + songname + " : " & *Mix_GetError exit sub end if music_paused = NO if Mix_PlayMusic(music_song, -1) then debug "Could not Mix_PlayMusic " + songname + " : " & *Mix_GetError music_stop exit sub end if 'not really working when songs are being faded in. if orig_vol = -1 then orig_vol = Mix_VolumeMusic(-1) end if dim volume_mult as double = 1. 'SDL_mixer 2.6.0 halves modplug volume (a change first backported to 'our SDL_mixer.dll 1.2.13 build and then later also officially 'backported to SDL_mixer 1.2), so we halve the volume when using 'older versions, for consistency. '(Our pre-2.6.0 build of SDL2_mixer.dll identified as 2.0.5) if (fmt and FORMAT_MODULES) andalso have_modplug then #ifdef SDL_MIXER2 if mixer_version < 2005 then volume_mult = 0.5 #else if mixer_version < 1213 then volume_mult = 0.5 #endif end if Mix_VolumeMusic(music_vol * volume_mult * MIX_MAX_VOLUME) if fmt <> FORMAT_MIDI then nonmidi_playing = YES else nonmidi_playing = NO end if end if end sub sub music_pause() 'Pause is broken in SDL_Mixer, so just stop. 'A look at the source indicates that it won't work for MIDI if music_status = musicOn then if music_song <> 0 then Mix_HaltMusic nonmidi_playing = NO end if end if end sub sub music_resume() if music_status = musicOn then if music_song <> 0 then Mix_ResumeMusic music_paused = NO end if end if end sub sub music_stop() if music_song <> 0 then Mix_FreeMusic(music_song) music_song = 0 music_paused = NO nonmidi_playing = NO end if #ifndef SDL_MIXER2 if music_song_rw <> 0 then 'Is safe even if has already been closed and freed safe_RWops_close(music_song_rw) music_song_rw = NULL end if #endif end sub ' Info on [Bug 843] Sound effects now affected by volume (on Windows) ' ' Note that Mix_VolumeMusic(-1) does not return the system ' MIDI volume level on Windows, it just returns what was set last. ' ' Windows XP: ' In Volume Control is a slider for SW Synth, the MIDI ' synthesizer. Setting the music volume while playing a MIDI ' sends a MIDI event which sets the SW Synth volume level, ' which can be overridden in Volume Control (note that Volume ' Control doesn't update the slider live). ' ' Windows 7+: ' The MIDI volume control is gone. Instead, apparently trying ' to set the MIDI volume (by midiOutSetVolume, which is what ' SDL_mixer does), actually sets the process's volume instead ' (waveOutSetVolume). The process volume is AFAIK not otherwise ' modified by SDL. Meaning sfx can only be quieter than MIDI. ' TODO: does it make sense to reset waveOutSetVolume after ' playing a MIDI? ' Two possible workarounds: ' -Set volume on each MIDI note instead of on the stream ' (would need to patch SDL_mixer/use music_native) ' -Use a separate process to play MIDI, see code from Eternity Engine here: ' https://www.doomworld.com/vb/post/1124981 ' -Maybe use some new Vista+ API: ' http://stackoverflow.com/a/19940489/1185152 ' See https://www.doomworld.com/vb/source-ports/63861-windows-sound-any-general-fixes/ ' for a summary ' ' Also even in old Windows there are problems with the MIDI ' volume if not using the SW Synth. ' http://forums.libsdl.org/viewtopic.php?t=949 ' ' See also http://odamex.net/bugs/show_bug.cgi?id=863 ' about the midiOutSetVolume volume curve being logarithmic, ' unlike SDL_mixer's internal volume. ' Volume fading: see r2283 sub music_setvolume(byval vol as single) 'SDL_mixer (unfortunately) internally clamps to MIX_MAX_VOLUME, so we don't need to music_vol = large(vol, 0.) if music_status = musicOn then Mix_VolumeMusic(music_vol * MIX_MAX_VOLUME) end if end sub function music_getvolume() as single 'return Mix_VolumeMusic(-1) / MIX_MAX_VOLUME return music_vol end function '------------ Sound effects -------------- DECLARE sub SDL_done_playing cdecl(byval channel as int32) ' The SDL_Mixer channel number is equal to the SoundEffectSlot index TYPE SoundEffectSlot EXTENDS SFXCommonData used as bool 'whether this slot is free playing as bool 'Set to false by a callback when the channel finishes buf as Mix_Chunk ptr END TYPE 'music_sdl has an arbitrary limit of 16 sound effects playing at once: dim shared sfx_slots(15) as SoundEffectSlot dim shared sound_inited as bool sub sound_init 'if this were called twice, the world would end. if sound_inited then exit sub 'anything that might be initialized here is done in music_init 'but, I must do it here too music_init Mix_AllocateChannels(ubound(sfx_slots) + 1) if callback_set_up = NO then Mix_channelFinished(@SDL_done_playing) callback_set_up = YES end if sound_inited = YES end sub sub sound_reset 'trying to free something that's already freed... bad! if sound_inited = NO then exit sub for slot as integer = 0 to ubound(sfx_slots) sound_unload(slot) next end sub sub sound_close sound_reset() sound_inited = NO end sub ' Returns -1 if too many sounds already playing/loaded function next_free_slot() as integer static retake_slot as integer = 0 dim i as integer 'Look for empty slots for i = 0 to ubound(sfx_slots) if sfx_slots(i).used = NO then return i end if next 'Look for silent slots for i = 0 to ubound(sfx_slots) retake_slot = (retake_slot + 1) mod (ubound(sfx_slots)+1) with sfx_slots(retake_slot) if .playing = NO then Mix_FreeChunk(.buf) .used = NO return retake_slot end if end with next return -1 ' no slot found end function 'Resumes a sfx if it's paused sub sound_play(slot as integer, loopcount as integer, volume as single = 1.) if slot = -1 then exit sub ' sfx_slots acts like a cache in this backend, since .buf ' remains loaded after the sound effect has stopped. with sfx_slots(slot) if .buf = 0 then showbug "sound_play: not loaded" exit sub end if if .playing = NO then ' Note that the i-th sfx slot is played on the i-th SDL_mixer channel, ' which is just a simplification. if Mix_PlayChannel(slot, .buf, loopcount) = -1 then 'E.g. a corrupt .ogg debug "sfx " & .effectID & " Mix_PlayChannel failed:" & *Mix_GetError() exit sub end if .playing = YES end if if Mix_Paused(slot) then ' Haven't tested, but it looks like SDL_mixer doesn't clear its paused flag ' when a channel stops Mix_Resume(slot) end if ' SDL_mixer has separate channel and chunk volumes and multiples them. ' We do the multiplication ourselves, only using channel volumes. ' Note that the built-in support for fades works by adjust channel ' volumes, not chunk volumes. And volumes are capped to 100%. Mix_Volume(slot, volume * MIX_MAX_VOLUME) end with end sub sub sound_pause(slot as integer) if slot = -1 then exit sub with sfx_slots(slot) if .playing then Mix_Pause(slot) end if end with end sub sub sound_stop(slot as integer) if slot = -1 then exit sub with sfx_slots(slot) if .playing then Mix_HaltChannel(slot) .playing = NO end if end with end sub sub sound_setvolume(slot as integer, volume as single) if slot = -1 then exit sub Mix_Volume(slot, volume * MIX_MAX_VOLUME) end sub function sound_getvolume(slot as integer) as single if slot = -1 then return 0. return Mix_Volume(slot, -1) / MIX_MAX_VOLUME end function sub sound_free(num as integer) for slot as integer = 0 to ubound(sfx_slots) with sfx_slots(slot) if .effectID = num then sound_unload slot end with next end sub function sound_playing(slot as integer) as bool if slot = -1 then return NO if sfx_slots(slot).used = NO then return NO return sfx_slots(slot).playing end function function sound_slotdata(slot as integer) as SFXCommonData ptr if slot < 0 or slot > ubound(sfx_slots) then return NULL if sfx_slots(slot).used = NO then return NULL return @sfx_slots(slot) end function function sound_lastslot() as integer return ubound(sfx_slots) end function ' Returns the first sound slot with the given sound effect ID (num); ' if the sound is not loaded, returns -1. function sound_slot_with_id(num as integer) as integer for slot as integer = 0 to ubound(sfx_slots) with sfx_slots(slot) if .used andalso .effectID = num then return slot end with next return -1 end function 'Loads a sound into a slot, and marks its ID num (equal to OHR sfx number). 'Returns the slot number, or -1 if an error occurs. function sound_load overload(lump as Lump ptr, num as integer = -1) as integer return -1 end function function sound_load overload(filename as string, num as integer = -1) as integer dim slot as integer dim sfx as Mix_Chunk ptr if filename = "" then return -1 if not isfile(filename) then return -1 'File size restriction to stop massive oggs being decompressed 'into memory. '(this check is now only done in browse.bas when importing) 'if filelen(filename) > 500*1024 then ' debug "Sound effect file too large (>500k): " & filename ' return -1 'end if log_openfile filename sfx = Mix_LoadWAV(@filename[0]) if sfx = NULL then debug "Couldn't Mix_LoadWAV " & filename & " : " & *Mix_GetError() return -1 end if slot = next_free_slot() 'debuginfo "sound_load(" & filename & "," & num & ") in slot " & slot if slot = -1 then debuginfo "sound_load(""" & filename & """, " & num & ") no more sound slots available" else with sfx_slots(slot) .used = YES .effectID = num .buf = sfx .playing = NO end with end if return slot end function 'Unloads a sound loaded in a slot. TAKES A CACHE SLOT, NOT AN SFX ID NUMBER! sub sound_unload(slot as integer) with sfx_slots(slot) if .used = NO then exit sub Mix_FreeChunk(.buf) .playing = NO .used = NO .effectID = 0 .buf = 0 end with end sub sub SDL_done_playing cdecl(byval channel as int32) sfx_slots(channel).playing = NO end sub '-- for debugging function sfx_slot_info (byval slot as integer) as string with sfx_slots(slot) return strprintf("slot %d used=%d sfx=%d playing=%d paused=%d buf=%x", _ slot, .used, .effectID, .playing, Mix_Paused(slot), .buf) end with end function '================================================================================ ' ModPlug settings ' ' This is obsolete since we now use libxmp instead of libmodplug whenever possible. type ModplugSettingsMenu extends ModularMenu settings as ModPlug_Settings declare sub update () declare function each_tick () as bool end type sub ModplugSettingsMenu.update () _ModPlug_SetSettings(@settings) redim menu(4) state.last = ubound(menu) menu(0) = "Previous Menu..." menu(1) = "Noise reduction: " & yesorno(settings.mFlags and MODPLUG_ENABLE_NOISE_REDUCTION) menu(2) = "Reverb: " & settings.mReverbDepth & "%" menu(3) = "Surround: " & yesorno(settings.mFlags and MODPLUG_ENABLE_SURROUND) menu(4) = "Megabass: " & settings.mBassAmount & "%" end sub function ModplugSettingsMenu.each_tick () as bool dim changed as bool select case state.pt case 0 if enter_space_click(state) then return YES case 1 changed = bitgrabber(settings.mFlags, MODPLUG_ENABLE_NOISE_REDUCTION, state) case 2 changed = intgrabber(settings.mReverbDepth, 0, 100) setbitmask settings.mFlags, MODPLUG_ENABLE_REVERB, settings.mReverbDepth > 0 case 3 changed = bitgrabber(settings.mFlags, MODPLUG_ENABLE_SURROUND, state) case 4 changed = intgrabber(settings.mBassAmount, 0, 100) setbitmask settings.mFlags, MODPLUG_ENABLE_MEGABASS, settings.mBassAmount > 0 end select state.need_update or= changed end function function modplug_settings_menu () as bool if _ModPlug_GetSettings = NULL or _ModPlug_SetSettings = NULL then return NO dim menu as ModplugSettingsMenu menu.floating = YES menu.tooltip = "ModPlug settings (not saved)" _ModPlug_GetSettings(@menu.settings) menu.run() _ModPlug_SetSettings(@menu.settings) return YES end function function music_settings_menu () as bool return modplug_settings_menu() end function #ifndef __FB_WIN32__ 'Try to override SDL_mixer's disabling of loop points in ModPlug. 'Does not affect any currently playing module. 'Not needed if using our SDL_mixer.dll on Windows. sub enable_modplug_looping () if tried_enabling_modplug_loops then exit sub tried_enabling_modplug_loops = YES 'When, and only when, SDL_Mixer's modplug backend is first loaded it will call 'ModPlug_SetSettings, so ensure that has happened so our changes aren't clobbered '(currently, we already do this in music_init()) if Mix_Init(MIX_INIT_MOD) = 0 then exit sub 'Can't play mods 'Don't go loading modplug if SDL_mixer isn't using it if have_modplug = NO then exit sub if _ModPlug_GetSettings = NULL orelse _ModPlug_SetSettings = NULL then 'Using NULL as the module handle doesn't work, as SDL_mixer doesn't 'load modplug into the global namespace. #ifdef __FB_DARWIN__ modplug_handle = dylibload("modplug.framework/modplug") #endif if modplug_handle = NULL then modplug_handle = dylibload("modplug") end if if modplug_handle then debuginfo "Loaded libmodplug" _ModPlug_GetSettings = dylibsymbol(modplug_handle, "ModPlug_GetSettings") _ModPlug_SetSettings = dylibsymbol(modplug_handle, "ModPlug_SetSettings") if _ModPlug_GetSettings = NULL orelse _ModPlug_SetSettings = NULL then debuginfo "ModPlug_Get/SetSettings missing!" exit sub end if else debuginfo "Couldn't load libmodplug" exit sub end if end if dim settings as ModPlug_Settings _ModPlug_GetSettings(@settings) if settings.mLoopCount = -1 then 'debuginfo "ModPlug looping already enabled" else debuginfo "Enabling ModPlug looping" settings.mLoopCount = -1 _ModPlug_SetSettings(@settings) end if end sub #endif