require "ffi" require "plaything/version" require "plaything/monkey_patches/ffi" require "plaything/support" require "plaything/objects" require "plaything/openal" class Plaything Error = Class.new(StandardError) # Open the default output device and prepare it for playback. # # @param [Hash] options # @option options [Symbol] sample_type (:int16) # @option options [Integer] sample_rate (44100) # @option options [Integer] channels (2) def initialize(options = { sample_type: :int16, sample_rate: 44100, channels: 2 }) @device = OpenAL.open_device(nil) raise Error, "Failed to open device" if @device.null? @context = OpenAL.create_context(@device, nil) OpenAL.make_context_current(@context) OpenAL.distance_model(:none) OpenAL.listenerf(:gain, 1.0) FFI::MemoryPointer.new(OpenAL::Source, 1) do |ptr| OpenAL.gen_sources(ptr.count, ptr) @source = OpenAL::Source.new(ptr.read_uint) end @sample_type = options.fetch(:sample_type) @sample_rate = Integer(options.fetch(:sample_rate)) @channels = Integer(options.fetch(:channels)) @sample_format = { [ :int16, 2 ] => :stereo16, }.fetch([@sample_type, @channels]) do raise TypeError, "unknown sample format for type [#{@sample_type}, #{@channels}]" end FFI::MemoryPointer.new(OpenAL::Buffer, 3) do |ptr| OpenAL.gen_buffers(ptr.count, ptr) @buffers = OpenAL::Buffer.extract(ptr, ptr.count) end @free_buffers = @buffers.clone @queued_buffers = [] @queued_frames = [] # 44100 int16s = 22050 frames = 0.5s (1 frame * 2 channels = 2 int16 = 1 sample = 1/44100 s) @buffer_size = @sample_rate * @channels * 1.0 # how many samples there are in each buffer, irrespective of channels @buffer_length = @buffer_size / @channels # buffer_duration = buffer_length / sample_rate @total_buffers_processed = 0 end # Start playback of queued audio. # # @note You must continue to supply audio, or playback will cease. def play OpenAL.source_play(@source) end # Pause playback of queued audio. Playback will resume from current position when {#play} is called. def pause OpenAL.source_pause(@source) end # Stop playback and clear any queued audio. # # @note All audio queues are completely cleared, and {#position} is reset. def stop OpenAL.source_stop(@source) @source.detach_buffers @free_buffers.concat(@queued_buffers) @queued_buffers.clear @queued_frames.clear @total_buffers_processed = 0 end # @return [Rational] how many seconds of audio that has been played. def position Rational(@total_buffers_processed * @buffer_length + sample_offset, @sample_rate) end # @return [Integer] total size of current play queue. def queue_size @source.get(:buffers_queued, Integer) * @buffer_length - sample_offset end # @return [Integer] how many audio drops since last call to drops. def drops 0 end # Queue audio frames for playback. # # @param [Array<[ Channels… ]>] frames array of N-sized arrays of integers. def <<(frames) if buffers_processed > 0 FFI::MemoryPointer.new(OpenAL::Buffer, buffers_processed) do |ptr| OpenAL.source_unqueue_buffers(@source, ptr.count, ptr) @total_buffers_processed += ptr.count @free_buffers.concat OpenAL::Buffer.extract(ptr, ptr.count) @queued_buffers.delete_if { |buffer| @free_buffers.include?(buffer) } end end wanted_size = (@buffer_size - @queued_frames.length).div(@channels) * @channels consumed_frames = frames.take(wanted_size) @queued_frames.concat(consumed_frames) if @queued_frames.length >= @buffer_size and @free_buffers.any? current_buffer = @free_buffers.shift FFI::MemoryPointer.new(@sample_type, @queued_frames.length) do |frames| frames.public_send(:"write_array_of_#{@sample_type}", @queued_frames) # stereo16 = 2 int16s (1 frame) = 1 sample OpenAL.buffer_data(current_buffer, @sample_format, frames, frames.size, @sample_rate) @queued_frames.clear end FFI::MemoryPointer.new(OpenAL::Buffer, 1) do |buffers| buffers.write_uint(current_buffer.to_native) OpenAL.source_queue_buffers(@source, buffers.count, buffers) end @queued_buffers.push(current_buffer) end consumed_frames.length end protected def sample_offset @source.get(:sample_offset, Integer) end def buffers_processed if not @source.stopped? @source.get(:buffers_processed, Integer) else 0 end end end