class EL_AUDIO_PLAYER_THREAD

(source code)

Client examples: MEDIA_PLAYER_DUMMY_APP

description

Audio player thread

note
	description: "Audio player thread"

	author: "Finnian Reilly"
	copyright: "Copyright (c) 2001-2022 Finnian Reilly"
	contact: "finnian at eiffel hyphen loop dot com"

	license: "MIT license (See: en.wikipedia.org/wiki/MIT_License)"
	date: "2022-11-15 19:56:06 GMT (Tuesday 15th November 2022)"
	revision: "5"

class
	EL_AUDIO_PLAYER_THREAD [SAMPLE_TYPE -> EL_AUDIO_PCM_SAMPLE create make end]

inherit
	EL_DORMANT_ACTION_LOOP_THREAD
		rename
			resume as play,
			suspend as stop_play,
			is_suspended as is_play_stopped,
			on_suspension as on_finished_play,
			on_resumption as on_start_play,
			do_action as cycle_play_buffers
		redefine
			on_exit, on_finished_play, on_start_play, stop_play, on_start
		end

	EL_AUDIO_PLAYER_CONSTANTS
		undefine
			default_create, is_equal, copy
		end

	EL_MODULE_LOG

	EL_MODULE_LOG_MANAGER

create
	make

feature {NONE} -- Initialization

	make
			--
		do
			default_create
			create play_buffer.make
			create source_producer.make (create {EL_AUDIO_SOURCE_PRODUCER [SAMPLE_TYPE]}.make)
			set_event_listener (create {EL_AUDIO_PLAYER_DO_NOTHING_EVENT_LISTENER})
			create channel_volume_settings.make (<< 50, 50 >>)
		end

feature -- Access

	buffer_duration_secs: REAL

	play_duration: REAL

feature -- Element change

	set_source (a_source: EL_AUDIO_SAMPLE_SOURCE [SAMPLE_TYPE])
			--
		do
			source_header := a_source.header
			set_samples_played_maximum (source_header.sample_count)
			source_producer.set_source (a_source)
		end

	set_event_listener (an_event_listener: like event_listener)
			-- Set `event_listener' to `an_event_listener'.
		do
			event_listener := an_event_listener
		ensure
			event_listener_assigned: event_listener = an_event_listener
		end

	set_samples_played_maximum (a_samples_played_maximum: like samples_played_maximum)
			-- Set `samples_played_maximum' to `a_samples_played_maximum'.
		do
			samples_played_maximum := a_samples_played_maximum
		ensure
			samples_played_maximum_assigned: samples_played_maximum = a_samples_played_maximum
		end

	set_play_duration (duration: REAL)
			--
		do
			play_duration := duration
			samples_played_maximum := (play_duration * source_header.samples_per_sec).rounded
		end

	set_relative_start_position (unit_relative_pos: REAL)
			--
		require
			is_unit: unit_relative_pos.sign >= 0 and unit_relative_pos <= 1.0
		do
			relative_start_position := unit_relative_pos
		end

feature -- Basic operations

	stop_play
			--
		do
			Precursor
			source_producer.stop
		end

	pause
			--
		do
			if audio_device.is_open and then not audio_device.is_paused then
				audio_device.pause
			end
		end

	resume_play
			--
		do
			if audio_device.is_open and then audio_device.is_paused then
				audio_device.restart
			end
		end

	set_channel_volume_settings (volume_settings: ARRAY [INTEGER])
			--
		do
			channel_volume_settings.lock
--			synchronized
				channel_volume_settings.set_item (volume_settings)
				if not is_play_stopped then
					audio_device.set_volume_as_percentage (channel_volume_settings.item)
				end
--			end
			channel_volume_settings.unlock
		end

feature -- Status query

	all_samples_played: BOOLEAN
			--
		do
			Result := samples_played_count >= samples_played_maximum
		end

	is_play_paused: BOOLEAN
			--
		do
			Result := audio_device.is_paused
		end

	is_last_buffer_played: BOOLEAN

feature {NONE} -- Event handlers

	on_exit
			--
		do
			log.enter ("on_exit")
			source_producer.exit
			log.exit
		end

	on_start
		do
			Log_manager.add_thread (Current)
		end

	on_start_play
			--
		local
			volume_settings: ARRAY [INTEGER]
			ch: INTEGER
		do
			log.enter ("on_start_play")

			buffers_played_count := 0
			buffered_sample_count := 0
			samples_played_count := 0
			is_last_buffer_played := false
			last_event_time := 0

			channel_volume_settings.lock
--			synchronized
				if channel_volume_settings.item = Void
					or else channel_volume_settings.item.count /= source_header.num_channels
				then
					create volume_settings.make (1, source_header.num_channels)
					from ch := 1 until ch > volume_settings.count loop
						volume_settings.put (50, ch)
						ch := ch + 1
					end
					channel_volume_settings.set_item (volume_settings)
				end
				create audio_device.make_open (source_header)
				audio_device.save_volume_setting
				audio_device.set_volume_as_percentage (channel_volume_settings.item)
--			end
			channel_volume_settings.unlock

			source_producer.initialize (relative_start_position)
			log.put_integer_field ("samples_played_maximum", samples_played_maximum)
			log.put_new_line

			log.exit
		end

	on_finished_play
			--
		do
			audio_device.reset
			remove_played_buffers (false)
			audio_device.restore_volume_setting
			audio_device.close
			event_listener.on_finished
		end

feature {NONE} -- Implemenation

	cycle_play_buffers
			--
		do
			log.enter ("cycle_play_buffers")
			if play_buffer.is_empty then
				log.put_line ("play_buffer.is_empty: true")
				source_producer.buffer_audio_from_source (event_listener, Current)

				suspend_thread  -- Suspended until source producing thread finishes
				Previous_call_is_blocking_thread
-- THREAD WAITING

			end

			if not is_play_stopped then
				fill_play_buffer
			end

			if not play_buffer.is_empty then
				wait_for_playing_buffer_to_finish
				remove_played_buffers (true)
			end

			if is_last_buffer_played or else samples_played_count >= samples_played_maximum then
				stop_play
			end

			log.put_integer_field ("buffered_sample_count", buffered_sample_count)
			log.put_new_line
			log.exit
		end

	wait_for_playing_buffer_to_finish
			--
		local
			wait_millisecs: INTEGER
			count: INTEGER
		do
			log.enter ("wait_for_playing_buffer_to_finish")
			from
				log.put_line ("Sleeping..")
				wait_millisecs := (play_buffer.item.duration * 1000).ceiling + 3
			until
				is_play_stopped or else audio_device.buffers_played_count > buffers_played_count
			loop
				sleep (wait_millisecs)
				wait_millisecs := 4
				count := count + 1
			end
			log.put_integer_field ("sleep_count", count)
			log.put_new_line

			log.put_integer_field (
				"audio_device.buffers_played_count - buffers_played_count",
				audio_device.buffers_played_count - buffers_played_count
			)
			log.put_new_line
			buffers_played_count := audio_device.buffers_played_count
			log.exit
		end

	remove_played_buffers (add_more: BOOLEAN)
			--
		do
			log.enter ("remove_played_buffers")
			from until play_buffer.is_empty or else not play_buffer.item.is_done loop
				buffered_sample_count := buffered_sample_count - play_buffer.item.sample_count
				samples_played_count := samples_played_count + play_buffer.item.sample_count
				log.put_integer_field ("samples_played_count", samples_played_count)
				log.put_integer_field (" samples_played_maximum", samples_played_maximum)
				log.put_new_line
				play_buffer.item.prepare_disposal
				if play_buffer.item.is_last then
					is_last_buffer_played := true
				end
				play_buffer.remove
				if add_more then
					source_producer.queue_next_buffer
				end
			end
			send_buffer_played_event
			log.put_integer_field ("play_buffer.count", play_buffer.count)
			log.put_new_line
			log.exit
		end

	fill_play_buffer
			--
		local
			source_buffer: LINKED_LIST [MANAGED_POINTER]
			last_source_buffer: EL_AUDIO_WAVE_BUFFER
		do
			log.enter ("fill_play_buffer")
			create source_buffer.make
			source_producer.merge_buffer_list_into_other (source_buffer)
			from source_buffer.start until source_buffer.after loop
				if source_buffer.item = Last_buffer_marker then
					last_source_buffer.set_last
				else
					last_source_buffer := create_wave_buffer (source_buffer.item)
					play_buffer.extend (last_source_buffer)
					last_source_buffer.queue_to_play
					buffered_sample_count := buffered_sample_count + last_source_buffer.sample_count
				end
				source_buffer.forth
			end
			log.put_integer_field ("buffered_sample_count", buffered_sample_count)
			log.put_new_line
			log.exit
		end

	send_buffer_played_event
			--
		local
			elapsed_play_time: REAL
		do
			elapsed_play_time := (samples_played_count / source_header.samples_per_sec).truncated_to_real
			if elapsed_play_time - last_event_time >= Minimum_time_between_buffer_played_events then
				last_event_time := elapsed_play_time
				event_listener.on_buffer_played ((samples_played_count / samples_played_maximum).truncated_to_real.min (1.0))
			end
		end

	create_wave_buffer (audio_data: MANAGED_POINTER): EL_AUDIO_WAVE_BUFFER
			--
		do
			create Result.make (
				audio_device.device_handle, audio_data, source_header.block_align, source_header.samples_per_sec
			)
		end

feature {NONE} -- Implementation: components

	play_buffer: LINKED_QUEUE [EL_AUDIO_WAVE_BUFFER]

	audio_device: EL_AUDIO_OUTPUT_DEVICE

	event_listener: EL_AUDIO_PLAYER_EVENT_LISTENER

	source_producer: EL_AUDIO_SOURCE_PRODUCER_I [SAMPLE_TYPE]

feature {NONE} -- Implementation: variables

	channel_volume_settings: EL_MUTEX_REFERENCE [ARRAY [INTEGER]]

	source_header: EL_AUDIO_WAVE_HEADER

	buffered_sample_count: INTEGER

	buffers_played_count: INTEGER

	samples_played_count: INTEGER

	samples_played_maximum: INTEGER

	relative_start_position: REAL

	last_event_time: REAL
		-- Time when last buffer played event generated measured as secs of play


end