'' '' 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) '' It isn't possible to link both backends into the engine at once. '' '' Part of the OHRRPGCE - See LICENSE.txt for GNU GPL License details and disclaimer of liability '' #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" '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 ' External functions 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) 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) ' Older FB have out of date SDL headers #if __FB_VERSION__ < "1.04" declare function Mix_LoadMUS_RW (byval rw as SDL_RWops ptr) as Mix_Music ptr ' Mix_GetMusicDecoder etc also missing #endif #ifndef MIX_INIT_MID 'Exists in SDL_mixer 2 only (but missing from FB's header). '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 enum MusicStatusEnum musicError = -1 ' Don't try again musicOff = 0 musicOn = 1 end enum dim shared supported_formats as integer dim shared music_status as MusicStatusEnum = musicOff dim shared music_vol as integer '0 to 128 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() 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" #else #define sdlX "sdl" #endif dim ver as const SDL_version ptr dim ret as string = "music_" & sdlX dim libhandle as any ptr libhandle = dylibload(ucase(sdlX) + "_mixer") ' For some reason the SDL 1.2 Android port produces libsdl_mixer.so instead if libhandle = NULL then libhandle = dylibload(sdlX + "_mixer") end if 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") end if 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 ver = Mix_Linked_Version() ret += ", SDL_Mixer " & ver->major & "." & ver->minor & "." & ver->patch if music_status = musicOn then dim freq as int32, format as ushort, channels as int32 Mix_QuerySpec(@freq, @format, @channels) ret += " (" & freq & "Hz" 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 = "MPG123" or form = "MAD" then 'Is linked to libmad or libmpg123, rather than smpeg, 'which is totally broken for non-44.1kHz MP3s, 'so intentionally NOT checking for "MP3" supported_formats or= FORMAT_MP3 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" or form = "MIKMOD" or form = "MODPLUG" 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. 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 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 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 ' We're going to be requesting certain things from our audio ' device, so we set them up beforehand ' MIX_DEFAULT_FREQUENCY is 22050, which slightly worsens sound quality ' than playing at 44100, but using 44100 causes tracks between 22-44 ' to be sped up, which sounds worse. See https://sourceforge.net/p/ohrrpgce/bugs/2026/ audio_rate = MIX_DEFAULT_FREQUENCY audio_format = MIX_DEFAULT_FORMAT audio_channels = 2 '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 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 = 64 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) 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 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 "Could not load song " + songname + " (SDL_RWFromFile failed)" 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 Mix_PlayMusic(music_song, -1) music_paused = NO 'not really working when songs are being faded in. if orig_vol = -1 then orig_vol = Mix_VolumeMusic(-1) end if Mix_VolumeMusic(music_vol) 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) music_vol = bound(vol, 0., 1.) * MIX_MAX_VOLUME if music_status = musicOn then Mix_VolumeMusic(music_vol) end if end sub function music_getvolume() as single 'return Mix_VolumeMusic(-1) / MIX_MAX_VOLUME music_getvolume = music_vol / MIX_MAX_VOLUME 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 showbug "sound_play: Mix_PlayChannel failed" 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 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 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