Class: Plaything

Inherits:
Object
  • Object
show all
Defined in:
lib/plaything.rb,
lib/plaything/openal.rb,
lib/plaything/version.rb,
lib/plaything/objects/buffer.rb,
lib/plaything/objects/device.rb,
lib/plaything/objects/source.rb,
lib/plaything/objects/context.rb,
lib/plaything/support/paramable.rb,
lib/plaything/support/type_class.rb,
lib/plaything/support/managed_pointer.rb

Overview

Plaything is tiny API wrapper around OpenAL, and makes it easy to play raw (PCM) streaming audio through your speakers.

API consist of a few key methods available on the Plaything instance.

  • #play, #pause, #stop — controls source playback state. If the source runs out of audio to play, it will forcefully stop playback.

  • #position, can be used to retrieve playback position.

  • #queue_size, #drops, #starved? — status information; should be used by the streaming source to improve playback experience.

  • #format= — allows you to change format, even during playback.

  • #stream, #<< — fills the audio buffers with PCM audio.

Internally, Plaything will queue and unqueue buffers as they are played during streaming. When a sufficient amount of audio has been fed into plaything, the audio will be queued on the source and plaything can accept additional audio.

Plaything is considered thread-safe.

Defined Under Namespace

Modules: OpenAL

Constant Summary collapse

Error =
Class.new(StandardError)
Formats =
{
  [ :int16, 1 ] => :mono16,
  [ :int16, 2 ] => :stereo16,
}
VERSION =
"1.1.1"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(format = { sample_rate: 44100, sample_type: :int16, channels: 2 }) ⇒ Plaything

Open the default output device and prepare it for playback.

Raises:



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/plaything.rb', line 35

def initialize(format = { sample_rate: 44100, sample_type: :int16, 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

  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 = []

  @starved = false
  @total_buffers_processed = 0

  @monitor = Monitor.new

  self.format = format
end

Instance Attribute Details

#sourcePlaything::OpenAL::Source (readonly)

Returns the back-end audio source.

Returns:



67
68
69
# File 'lib/plaything.rb', line 67

def source
  @source
end

Instance Method Details

#<<(frames) ⇒ Integer

Note:

this method is here for backwards-compatibility, and does not support changing format automatically. You should use #stream instead.

Queue audio frames for playback.

Parameters:

  • array (Array<Integer>)

    of interleaved audio samples.

Returns:

  • (Integer)

    number of frames consumed (consumed_samples / channels), a multiple of channels



189
190
191
# File 'lib/plaything.rb', line 189

def <<(frames)
  stream(frames, format)
end

#dropsInteger

If audio is starved, and it has not been previously seen as starved, it will return 1. However, if audio is starved and #drops has already reported it as starved, it will return 0. Finally, if audio is not starved, it always returns 0.

Returns:

  • (Integer)

    number of drops since previous call to #drops.



119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/plaything.rb', line 119

def drops
  if starved?
    if @starved_toggle
      0
    else
      @starved_toggle = true
      1
    end
  else
    @starved_toggle = false
    0
  end
end

#formatHash

Returns current audio format in the queues.

Returns:

  • (Hash)

    current audio format in the queues



139
140
141
142
143
144
145
146
147
# File 'lib/plaything.rb', line 139

def format
  synchronize do
    {
      sample_rate: @sample_rate,
      sample_type: @sample_type,
      channels: @channels,
    }
  end
end

#format=(format) ⇒ Object

Note:

if there is any queued audio it will be cleared, and the playback will be stopped.

Change the format.

Parameters:

  • format (Hash)

Options Hash (format):

  • sample_type (Symbol)

    only :int16 available

  • sample_rate (Integer)
  • channels (Integer)

    1 or 2



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/plaything.rb', line 158

def format=(format)
  synchronize do
    if @source.playing?
      stop # clear audio buffers
      @starved = true
    end

    @sample_type = format.fetch(:sample_type)
    @sample_rate = Integer(format.fetch(:sample_rate))
    @channels    = Integer(format.fetch(:channels))

    @sample_format = Formats.fetch([@sample_type, @channels]) do
      raise TypeError, "unknown sample format for type [#{@sample_type}, #{@channels}]"
    end

    # 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
  end
end

#pauseObject

Pause playback of queued audio. Playback will resume from current position when #play is called.



80
81
82
# File 'lib/plaything.rb', line 80

def pause
  synchronize { @source.pause }
end

#playObject

Note:

You must continue to supply audio, or playback will cease.

Start playback of queued audio.



72
73
74
75
76
77
# File 'lib/plaything.rb', line 72

def play
  synchronize do
    @starved = false
    @source.play
  end
end

#positionRational

Returns how many seconds of audio that has been played.

Returns:

  • (Rational)

    how many seconds of audio that has been played.



99
100
101
102
103
104
# File 'lib/plaything.rb', line 99

def position
  synchronize do
    total_samples_processed = @total_buffers_processed * @buffer_length
    Rational(total_samples_processed + @source.sample_offset, @sample_rate)
  end
end

#queue_sizeInteger

Returns total size of current play queue.

Returns:

  • (Integer)

    total size of current play queue.



107
108
109
110
111
# File 'lib/plaything.rb', line 107

def queue_size
  synchronize do
    @source.buffers_queued * @buffer_length - @source.sample_offset
  end
end

#starved?Boolean

Returns true if audio stream has starved.

Returns:

  • (Boolean)

    true if audio stream has starved



134
135
136
# File 'lib/plaything.rb', line 134

def starved?
  synchronize { @starved or @source.starved? }
end

#stopObject

Note:

All audio queues are completely cleared, and #position is reset.

Stop playback and clear any queued audio.



87
88
89
90
91
92
93
94
95
96
# File 'lib/plaything.rb', line 87

def stop
  synchronize do
    @source.stop
    @source.detach_buffers
    @free_buffers.concat(@queued_buffers)
    @queued_buffers.clear
    @queued_frames.clear
    @total_buffers_processed = 0
  end
end

#stream(frames, frame_format) ⇒ Integer

Queue audio frames for playback.

Parameters:

  • array (Array<Integer>)

    of interleaved audio samples.

  • format (Hash)

Returns:

  • (Integer)

    number of frames consumed (consumed_samples / channels), a multiple of channels



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/plaything.rb', line 201

def stream(frames, frame_format)
  synchronize do
    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

    self.format = frame_format if frame_format != format

    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 / @channels
  end
end