note
	description: "chocolate doom i_sdlsound.c"
	license: "[
				Copyright (C) 1993-1996 by id Software, Inc.
				Copyright (C) 2005-2014 Simon Howard
				Copyright (C) 2008 David Flater
				Copyright (C) 2021 Ilgiz Mustafin
		
				This program 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.
		
				This program is distributed in the hope that it will be useful,
				but WITHOUT ANY WARRANTY; without even the implied warranty of
				MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
				GNU General Public License for more details.
		
				You should have received a copy of the GNU General Public License along
				with this program; if not, write to the Free Software Foundation, Inc.,
				51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
	]"

class 
	SOUND_SDL_MODULE

inherit
	SOUND_MODULE_T

create 
	make

feature {NONE} 

	i_main: I_MAIN

	make (a_i_main: like i_main)
		do
			i_main := a_i_main
			create channels_playing.make_filled (Void, 0, Num_channels - 1)
		end
	
feature 

	sound_initialized: BOOLEAN

	Use_libsamplerate: BOOLEAN = False

	use_sfx_prefix: BOOLEAN

	mixer_freq: INTEGER_32

	mixer_format: INTEGER_16

	mixer_channels: INTEGER_32

	Num_channels: INTEGER_32 = 16

	channels_playing: ARRAY [detachable ALLOCATED_SOUND_T]
	
feature 

	allocated_sounds_head: detachable ALLOCATED_SOUND_T

	allocated_sounds_tail: detachable ALLOCATED_SOUND_T

	allocated_sounds_size: INTEGER_32
	
feature 

	release_sound_on_channel (channel: INTEGER_32)
		local
			snd: ALLOCATED_SOUND_T
		do
			snd := channels_playing [channel]
			{SDL_MIXER_FUNCTIONS_API}.mix_halt_channel (channel).do_nothing
			if attached snd then
				channels_playing [channel] := Void
				unlock_allocated_sound (snd)
				if snd.pitch /= {I_SOUND}.norm_pitch and snd.use_count <= 0 then
					free_allocated_sound (snd)
				end
			end
		end

	free_allocated_sound (snd: ALLOCATED_SOUND_T)
		do
			allocated_sound_unlink (snd)
			allocated_sounds_size := allocated_sounds_size - snd.chunk.alen.to_integer_32
		end

	lock_sound (sfxinfo: SFXINFO_T): BOOLEAN
		do
			Result := True
			if get_allocated_sound_by_sfx_info_and_pitch (sfxinfo, {I_SOUND}.norm_pitch) = Void then
				if not cache_sfx (sfxinfo) then
					Result := False
				end
			end
			if Result then
				check
						attached get_allocated_sound_by_sfx_info_and_pitch (sfxinfo, {I_SOUND}.norm_pitch) as snd
				then
					lock_allocated_sound (snd)
				end
			end
		end

	get_allocated_sound_by_sfx_info_and_pitch (sfxinfo: SFXINFO_T; pitch: INTEGER_32): detachable ALLOCATED_SOUND_T
		do
			from
				Result := allocated_sounds_head
			until
				Result = Void or else (Result.sfxinfo = sfxinfo and Result.pitch = pitch)
			loop
				Result := Result.next
			end
		end

	pitch_shift (insnd: ALLOCATED_SOUND_T; pitch: INTEGER_32): ALLOCATED_SOUND_T
		local
			inp: INTEGER_32
			outp: INTEGER_32
			srcbuf: MANAGED_POINTER
			dstbuf: MANAGED_POINTER
			srclen, dstlen: NATURAL_32
		do
			srclen := insnd.chunk.alen
			srcbuf := insnd.chunk.abuf_managed
			dstlen := ((1.to_double + (1.to_double - pitch / {I_SOUND}.norm_pitch)) * srclen.to_real_64).floor.to_natural_32
			if dstlen \\ 2 = 0 then
				dstlen := dstlen + 1
			end
			check
					attached insnd.sfxinfo as insnd_sfxinfo
			then
				Result := allocate_sound (insnd_sfxinfo, dstlen)
			end
			if attached Result then
				Result.pitch := pitch
				dstbuf := Result.chunk.abuf_managed
				check
						attached dstbuf and then attached srcbuf
				then
					from
						outp := 0
					until
						outp >= dstlen.to_integer_32 // 2
					loop
						inp := (outp.to_real / dstlen.to_real_32 * srclen.to_real_32).floor.to_integer_32
						dstbuf.put_integer_16 (srcbuf.read_integer_16 (inp * 2), outp * 2)
						outp := outp + 1
					end
				end
			end
		end

	lock_allocated_sound (snd: ALLOCATED_SOUND_T)
			-- Lock a sound, to indicate that it may not be freed.
		do
			snd.use_count := snd.use_count + 1
			allocated_sound_unlink (snd)
			allocated_sound_link (snd)
		end

	allocated_sound_unlink (snd: ALLOCATED_SOUND_T)
			-- Unlink a sound from the linked list
		do
			if snd.prev = Void then
				allocated_sounds_head := snd.next
			else
				check
						attached snd.prev as prev
				then
					prev.next := snd.next
				end
			end
			if snd.next = Void then
				allocated_sounds_tail := snd.prev
			else
				check
						attached snd.next as next
				then
					next.prev := snd.prev
				end
			end
		end

	unlock_allocated_sound (snd: ALLOCATED_SOUND_T)
			-- Unlock a sound to indicate that it may now be freed.
		do
			if snd.use_count <= 0 then
				{I_MAIN}.i_error ("Sound effect released more times than it was locked...")
			end
			snd.use_count := snd.use_count - 1
		end
	
feature 

	init (a_use_sfx_prefix: BOOLEAN): BOOLEAN
			-- Initialise sound module
			-- Returns True if successfully initialised
		local
			i: INTEGER_32
		do
			use_sfx_prefix := a_use_sfx_prefix
			from
				i := 0
			until
				i >= Num_channels
			loop
				channels_playing [i] := Void
				i := i + 1
			end
			if {SDL_FUNCTIONS_API}.sdl_init ({SDL_CONSTANT_API}.sdl_init_audio.to_natural_32) < 0 then
				print ("Unable to set up sound" + {SDL_ERROR}.sdl_get_error)
				Result := False
			else
				if {SDL_MIXER_FUNCTIONS_API}.mix_open_audio_device ({I_SOUND}.snd_samplerate, {SDL_AUDIO}.audio_s16sys.to_natural_32, 2, get_slice_size, default_pointer, {SDL_CONSTANT_API}.sdl_audio_allow_frequency_change) < 0 then
					print ("Error initialising SDL_mixer: " + {MIX_ERROR}.get_error)
					Result := False
				else
					expand_sound_data_sdl_mode := True
					if {SDL_MIXER_FUNCTIONS_API}.mix_query_spec ($mixer_freq, $mixer_format.to_pointer, $mixer_channels) = 0 then
						{I_MAIN}.i_error ("Error Mix_QuerySpec" + {MIX_ERROR}.get_error)
					end
					if Use_libsamplerate then
						print ("use_libsamplerate=" + Use_libsamplerate.out + " but libsamplerate not supported")
					end
					if {SDL_MIXER_FUNCTIONS_API}.mix_allocate_channels (Num_channels) /= Num_channels then
						{I_MAIN}.i_error ("Should not happen! Mix_AllocateChannels(" + Num_channels.out + ") returned other value")
					end
					{SDL_AUDIO_FUNCTIONS_API}.sdl_pause_audio (0)
					sound_initialized := True
					Result := True
				end
			end
		end

	expand_sound_data_sdl_mode: BOOLEAN
			-- Use ExpandSoudData_SDL? If not, then ExpandSoundData_SRC

	expand_sound_data (sfxinfo: SFXINFO_T; data: POINTER; samplerate, length: INTEGER_32): BOOLEAN
		do
			check
					expand_sound_data_sdl_mode
			then
				Result := expand_sound_data_sdl (sfxinfo, data, samplerate, length)
			end
		end

	expand_sound_data_sdl (sfxinfo: SFXINFO_T; data: POINTER; samplerate: INTEGER_32; length: INTEGER_32): BOOLEAN
			-- Generic sound expansion function for any sample rate.
			-- Returns number of clipped samples (always 0).
		local
			convertor: SDL_AUDIO_CVT
			snd_detachable: ALLOCATED_SOUND_T
			chunk: MIX_CHUNK
			expanded_length: NATURAL_32
		do
			create convertor.make
			expanded_length := ((length.to_natural_64) * mixer_freq.to_natural_64 // samplerate.to_natural_64).to_natural_32
			expanded_length := expanded_length * 4
			snd_detachable := allocate_sound (sfxinfo, expanded_length)
			if attached snd_detachable as snd then
				chunk := snd.chunk
				if samplerate <= mixer_freq and then convertible_ratio (samplerate, mixer_freq) and then {SDL_AUDIO_FUNCTIONS_API}.sdl_build_audio_cvt (convertor, {SDL_AUDIO}.audio_u8.to_natural_32, (1).to_character_8, samplerate, mixer_format.to_natural_32, mixer_channels.to_character_8, mixer_freq) /= 0 then
					convertor.set_len (length)
					convertor.allocate_buf (convertor.len * convertor.len_mult)
					check
							attached convertor.buf_managed as buf
					then
						buf.item.memory_copy (data.item, length)
						if {SDL_AUDIO_FUNCTIONS_API}.sdl_convert_audio (convertor) < 0 then
							{I_MAIN}.i_error ("Couldn't convert audio " + {SDL_ERROR}.sdl_get_error)
						end
						check
								attached chunk.abuf_managed as abuf
						then
							abuf.item.memory_copy (buf.item, chunk.alen.to_integer_32)
						end
						Result := True
					end
				else
					{I_MAIN}.i_error ("Couldn't convert with SDL")
				end
			else
				Result := False
			end
		end

	convertible_ratio (freq1, freq2: INTEGER_32): BOOLEAN
		local
			ratio: INTEGER_32
		do
			if freq1 > freq2 then
				Result := convertible_ratio (freq2, freq1)
			elseif freq2 \\ freq1 /= 0 then
				Result := False
			else
				ratio := freq2 // freq1
				from
				until
					ratio.bit_and (1) /= 0
				loop
					ratio := ratio |>> 1
				end
				Result := ratio = 1
			end
		end

	reserve_cache_space (len: NATURAL_32)
			-- Enforce SFX cache size limit. We are just about to allocate "len"
			-- bytes on the heap for a new sound effect, so free up some space
			-- so that we keep allocated_sounds_size < snd_cachesize
		do
			{NOT_IMPLEMENTED}.not_implemented ("SOUND_SDL reserve_cache_space", False)
		end

	allocate_sound (sfxinfo: SFXINFO_T; len: NATURAL_32): ALLOCATED_SOUND_T
		local
			snd: ALLOCATED_SOUND_T
		do
			reserve_cache_space (len)
			create snd.make
			snd.chunk.allocate_abuf (len.to_integer_32)
			snd.chunk.set_alen (len)
			snd.chunk.set_allocated (1)
			snd.chunk.set_volume ({MIX}.mix_max_volume.to_character_8)
			snd.pitch := {I_SOUND}.norm_pitch
			snd.use_count := 0
			snd.sfxinfo := sfxinfo
			allocated_sounds_size := allocated_sounds_size + len.to_integer_32
			allocated_sound_link (snd)
			Result := snd
		end

	allocated_sound_link (snd: ALLOCATED_SOUND_T)
			-- Hook a sound into the linked list at the head
		do
			snd.prev := Void
			snd.next := allocated_sounds_head
			allocated_sounds_head := snd
			if allocated_sounds_tail = Void then
				allocated_sounds_tail := snd
			else
				check
						attached snd.next as next
				then
					next.prev := snd
				end
			end
		end

	get_slice_size: INTEGER_32
		local
			limit: INTEGER_32
		do
			limit := ({I_SOUND}.snd_samplerate * {I_SOUND}.snd_maxslicetime_ms) // 1000
			from
				Result := 0
			until
				(1 |<< (Result + 1)) > limit
			loop
				Result := Result + 1
			end
			Result := 1 |<< Result
		ensure
				Result <= ({I_SOUND}.snd_samplerate * {I_SOUND}.snd_maxslicetime_ms) // 1000
				({I_SOUND}.snd_samplerate * {I_SOUND}.snd_maxslicetime_ms) // 1000 < (Result * 2)
		end

	Sound_devices: ARRAY [INTEGER_32]
			-- List of sound devices that this sound module is used for.
		once
			Result := <<{I_SOUND}.snddevice_sb, {I_SOUND}.snddevice_pas, {I_SOUND}.snddevice_gus, {I_SOUND}.snddevice_waveblaster, {I_SOUND}.snddevice_soundcanvas, {I_SOUND}.snddevice_awe32>>
		end
	
feature 

	shutdown
			-- Shutdown sound module
		do
			if sound_initialized then
				{SDL_MIXER_FUNCTIONS_API}.mix_close_audio
				{SDL_FUNCTIONS_API}.sdl_quit_sub_system ({SDL_CONSTANT_API}.sdl_init_audio.to_natural_32)
				sound_initialized := False
			end
		end

	get_sfx_lump_name (sfxinfo: SFXINFO_T): STRING_8
		local
			sfx: SFXINFO_T
		do
			if attached sfxinfo.link as link then
				sfx := link
			else
				sfx := sfxinfo
			end
			check
					attached sfx.name as name
			then
				if use_sfx_prefix then
					Result := "ds" + name
				else
					Result := name
				end
			end
		end

	get_sfx_lump_num (sfxinfo: SFXINFO_T): INTEGER_32
			-- Returns the lump index of the given sound.
		do
			Result := i_main.W_wad.w_getnumforname (get_sfx_lump_name (sfxinfo))
		end

	update
			-- Called periodically to update the subsystem.
		local
			i: INTEGER_32
		do
			from
				i := channels_playing.lower
			until
				i > channels_playing.upper
			loop
				if attached channels_playing [i] and not sound_is_playing (i) then
					release_sound_on_channel (i)
				end
				i := i + 1
			end
		end

	sound_is_playing (handle: INTEGER_32): BOOLEAN
			-- Query if a sound is playing on the given channel
		do
			if sound_initialized and handle >= 0 and handle < Num_channels then
				Result := {SDL_MIXER_FUNCTIONS_API}.mix_playing (handle) /= 0
			end
		end

	update_sound_params (handle, vol, sep: INTEGER_32)
			-- Update the sound settings on the given channel.
		local
			left: INTEGER_32
			right: INTEGER_32
		do
			if sound_initialized and handle >= 0 and handle < Num_channels then
				left := ((254 - sep) * vol) // 127
				right := (sep * vol) // 127
				left := left.max (0).min (255)
				right := right.max (0).min (255)
				{SDL_MIXER_FUNCTIONS_API}.mix_set_panning (handle, left.to_character_8, right.to_character_8).do_nothing
			end
		end

	start_sound (sfxinfo: SFXINFO_T; channel, vol, sep, pitch: INTEGER_32): INTEGER_32
			-- Start a sound on a given channel. Returns the channel id
			-- or -1 on failure.
		local
			snd: ALLOCATED_SOUND_T
			newsnd: ALLOCATED_SOUND_T
		do
			if not sound_initialized or channel < 0 or channel >= Num_channels then
				Result := -1
			else
				release_sound_on_channel (channel)
				if not lock_sound (sfxinfo) then
					Result := -1
				else
					snd := get_allocated_sound_by_sfx_info_and_pitch (sfxinfo, pitch)
					if not attached snd then
						snd := get_allocated_sound_by_sfx_info_and_pitch (sfxinfo, {I_SOUND}.norm_pitch)
						if not attached snd then
							Result := -1
						else
							if {I_SOUND}.snd_pitchshift /= 0 then
								newsnd := pitch_shift (snd, pitch)
								if newsnd /= Void then
									lock_allocated_sound (newsnd)
									unlock_allocated_sound (snd)
									snd := newsnd
								end
							end
						end
					else
						lock_allocated_sound (snd)
					end
					if Result /= -1 then
						check
								attached snd as attached_snd
						then
							{MIX}.mix_play_channel (channel, attached_snd.chunk, 0).do_nothing
							channels_playing [channel] := attached_snd
						end
						update_sound_params (channel, vol, sep)
						Result := channel
					end
				end
			end
		end

	stop_sound (handle: INTEGER_32)
			-- Stop the sound playing on the given channel
		do
			if sound_initialized and handle >= 0 and handle < Num_channels then
				release_sound_on_channel (handle)
			end
		end

	cache_sounds (sounds: ARRAY [SFXINFO_T])
			-- Called on startup to precache sound effects (if necessary)
		do
			if not Use_libsamplerate then
				cache_sounds_inner (sounds)
			end
		end

	cache_sounds_inner (sounds: ARRAY [SFXINFO_T])
		local
			i: INTEGER_32
			name: STRING_8
		do
			print ("Precaching all sound effects...")
			from
				i := sounds.lower
			until
				i > sounds.upper
			loop
				if i \\ 6 = 0 then
					print (".")
				end
				name := get_sfx_lump_name (sounds [i])
				sounds [i].lumpnum := i_main.W_wad.w_checknumforname (name)
				if sounds [i].lumpnum /= -1 then
					if not cache_sfx (sounds [i]) then
						print ("Could not cache sound " + i.out + "%N")
					end
				end
				i := i + 1
			end
		end

	cache_sfx (sfxinfo: SFXINFO_T): BOOLEAN
		local
			lumpnum: INTEGER_32
			lumplen: NATURAL_32
			samplerate: INTEGER_32
			length: NATURAL_32
			data: MANAGED_POINTER
		do
			lumpnum := sfxinfo.lumpnum
			data := i_main.W_wad.w_cachelumpnum (lumpnum)
			lumplen := i_main.W_wad.w_lumplength (lumpnum).to_natural_32
			if lumplen < 8 or data.read_natural_8_le (0) /= 3 or data.read_natural_8_le (1) /= 0 then
				Result := False
			else
				samplerate := data.read_integer_16_le (2).to_integer_32
				length := data.read_natural_32_le (4)
				if length > lumplen - 8 or length <= 48 then
					Result := False
				else
					length := length - 32
					create data.share_from_pointer (data.item + 16, length.to_integer_32)
					if not expand_sound_data (sfxinfo, data.item + 8, samplerate, length.to_integer_32) then
						Result := False
					else
						i_main.W_wad.w_releaselumpnum (lumpnum)
						Result := True
					end
				end
			end
		end
	
feature 

	Sound_sdl_module (a_i_main: like i_main): SOUND_SDL_MODULE
		once
			create Result.make (a_i_main)
		ensure
			instance_free: class
		end
	
end -- class SOUND_SDL_MODULE

Generated by ISE EiffelStudio