plaything 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3b370e69436137efbd0bb9964231b74bae9863a7
4
+ data.tar.gz: e8d93ef8632e8191f2be628a58618f04723e5cf6
5
+ SHA512:
6
+ metadata.gz: 5026505b6b2650bbac2e3a644b10bdbbb78ff675a8c8b9d3c86ec9d33ab81ac5176b5596da5d117837546a99b6aadf4b455ff2630cdd53682c8a2e60cb4fb1bc
7
+ data.tar.gz: d5590baa1b903ea691266b1c38d9834ead39c9d669b01113f929055e730918afefbb4847ebbd73fd973b6e82016c4824662d9bd869dd4001107de3f11be488e0
data/README.md CHANGED
@@ -1,6 +1,18 @@
1
1
  # Plaything
2
2
 
3
- Blast raw PCM audio through your speakers using OpenAL.
3
+ > OpenAL is a cross-platform 3D audio API appropriate for use with gaming applications and many other types of audio applications.
4
+
5
+ Plaything is tiny API wrapper around OpenAL, and makes it easy to play raw (PCM) streaming audio through your speakers.
6
+
7
+ Plaything was initially written to support audio playback from the [spotify gem](http://rubygems.org/gems/spotify).
8
+
9
+
10
+
11
+ ## Note about the OpenAL bindings
12
+
13
+ Plaything contains bindings to a subset of the OpenAL API, just enough to cover the necessary streaming functionality. With little further work, the OpenAL bindings could be extracted and further developed indepdendently of Plaything.
14
+
15
+ Additionally, the OpenAL streaming source is retrievable from Plaything, and allows you to modify parameters on the playback source, such as pitch, gain, and anything else OpenAL allows you to change.
4
16
 
5
17
  ## License
6
18
 
@@ -1,3 +1,4 @@
1
+ require "monitor"
1
2
  require "ffi"
2
3
  require "plaything/version"
3
4
  require "plaything/monkey_patches/ffi"
@@ -5,16 +6,33 @@ require "plaything/support"
5
6
  require "plaything/objects"
6
7
  require "plaything/openal"
7
8
 
9
+ # Plaything is tiny API wrapper around OpenAL, and makes it easy to play raw
10
+ # (PCM) streaming audio through your speakers.
11
+ #
12
+ # API consist of a few key methods available on the Plaything instance.
13
+ #
14
+ # - {#play}, {#pause}, {#stop} — controls source playback state. If the source
15
+ # runs out of audio to play, it will forcefully stop playback.
16
+ # - {#position}, can be used to retrieve playback position.
17
+ # - {#queue_size}, {#drops} — status information; should be used by the streaming
18
+ # source to improve playback experience.
19
+ # - {#format=} — allows you to change format, even during playback.
20
+ # - {#stream}, {#<<} — fills the audio buffers with PCM audio.
21
+ #
22
+ # Internally, Plaything will queue and unqueue buffers as they are played during
23
+ # streaming. When a sufficient amount of audio has been fed into plaything, the
24
+ # audio will be queued on the source and plaything can accept additional audio.
25
+ #
26
+ # Plaything is considered thread-safe.
8
27
  class Plaything
9
28
  Error = Class.new(StandardError)
29
+ Formats = {
30
+ [ :int16, 1 ] => :mono16,
31
+ [ :int16, 2 ] => :stereo16,
32
+ }
10
33
 
11
34
  # Open the default output device and prepare it for playback.
12
- #
13
- # @param [Hash] options
14
- # @option options [Symbol] sample_type (:int16)
15
- # @option options [Integer] sample_rate (44100)
16
- # @option options [Integer] channels (2)
17
- def initialize(options = { sample_type: :int16, sample_rate: 44100, channels: 2 })
35
+ def initialize(format = { sample_rate: 44100, sample_type: :int16, channels: 2 })
18
36
  @device = OpenAL.open_device(nil)
19
37
  raise Error, "Failed to open device" if @device.null?
20
38
 
@@ -28,14 +46,6 @@ class Plaything
28
46
  @source = OpenAL::Source.new(ptr.read_uint)
29
47
  end
30
48
 
31
- @sample_type = options.fetch(:sample_type)
32
- @sample_rate = Integer(options.fetch(:sample_rate))
33
- @channels = Integer(options.fetch(:channels))
34
-
35
- @sample_format = { [ :int16, 2 ] => :stereo16, }.fetch([@sample_type, @channels]) do
36
- raise TypeError, "unknown sample format for type [#{@sample_type}, #{@channels}]"
37
- end
38
-
39
49
  FFI::MemoryPointer.new(OpenAL::Buffer, 3) do |ptr|
40
50
  OpenAL.gen_buffers(ptr.count, ptr)
41
51
  @buffers = OpenAL::Buffer.extract(ptr, ptr.count)
@@ -45,103 +55,170 @@ class Plaything
45
55
  @queued_buffers = []
46
56
  @queued_frames = []
47
57
 
48
- # 44100 int16s = 22050 frames = 0.5s (1 frame * 2 channels = 2 int16 = 1 sample = 1/44100 s)
49
- @buffer_size = @sample_rate * @channels * 1.0
50
- # how many samples there are in each buffer, irrespective of channels
51
- @buffer_length = @buffer_size / @channels
52
- # buffer_duration = buffer_length / sample_rate
53
-
58
+ @drops = 0
54
59
  @total_buffers_processed = 0
60
+
61
+ @monitor = Monitor.new
62
+
63
+ self.format = format
55
64
  end
56
65
 
66
+ # @return [Plaything::OpenAL::Source] the back-end audio source.
67
+ attr_reader :source
68
+
57
69
  # Start playback of queued audio.
58
70
  #
59
71
  # @note You must continue to supply audio, or playback will cease.
60
72
  def play
61
- OpenAL.source_play(@source)
73
+ synchronize { @source.play }
62
74
  end
63
75
 
64
76
  # Pause playback of queued audio. Playback will resume from current position when {#play} is called.
65
77
  def pause
66
- OpenAL.source_pause(@source)
78
+ synchronize { @source.pause }
67
79
  end
68
80
 
69
81
  # Stop playback and clear any queued audio.
70
82
  #
71
83
  # @note All audio queues are completely cleared, and {#position} is reset.
72
84
  def stop
73
- OpenAL.source_stop(@source)
74
- @source.detach_buffers
75
- @free_buffers.concat(@queued_buffers)
76
- @queued_buffers.clear
77
- @queued_frames.clear
78
- @total_buffers_processed = 0
85
+ synchronize do
86
+ @source.stop
87
+ @source.detach_buffers
88
+ @free_buffers.concat(@queued_buffers)
89
+ @queued_buffers.clear
90
+ @queued_frames.clear
91
+ @total_buffers_processed = 0
92
+ end
79
93
  end
80
94
 
81
95
  # @return [Rational] how many seconds of audio that has been played.
82
96
  def position
83
- Rational(@total_buffers_processed * @buffer_length + sample_offset, @sample_rate)
97
+ synchronize do
98
+ total_samples_processed = @total_buffers_processed * @buffer_length
99
+ Rational(total_samples_processed + @source.sample_offset, @sample_rate)
100
+ end
84
101
  end
85
102
 
86
103
  # @return [Integer] total size of current play queue.
87
104
  def queue_size
88
- @source.get(:buffers_queued, Integer) * @buffer_length - sample_offset
105
+ synchronize do
106
+ @source.buffers_queued * @buffer_length - @source.sample_offset
107
+ end
89
108
  end
90
109
 
91
110
  # @return [Integer] how many audio drops since last call to drops.
92
111
  def drops
93
- 0
112
+ synchronize do
113
+ @drops.tap { @drops = 0 }
114
+ end
115
+ end
116
+
117
+ # @return [Hash] current audio format in the queues
118
+ def format
119
+ synchronize do
120
+ {
121
+ sample_rate: @sample_rate,
122
+ sample_type: @sample_type,
123
+ channels: @channels,
124
+ }
125
+ end
126
+ end
127
+
128
+ # Change the format.
129
+ #
130
+ # @note if there is any queued audio it will be cleared,
131
+ # and the playback will be stopped.
132
+ #
133
+ # @param [Hash] format
134
+ # @option format [Symbol] sample_type only :int16 available
135
+ # @option format [Integer] sample_rate
136
+ # @option format [Integer] channels 1 or 2
137
+ def format=(format)
138
+ synchronize do
139
+ if @source.playing?
140
+ stop
141
+ @drops += 1
142
+ end
143
+
144
+ @sample_type = format.fetch(:sample_type)
145
+ @sample_rate = Integer(format.fetch(:sample_rate))
146
+ @channels = Integer(format.fetch(:channels))
147
+
148
+ @sample_format = Formats.fetch([@sample_type, @channels]) do
149
+ raise TypeError, "unknown sample format for type [#{@sample_type}, #{@channels}]"
150
+ end
151
+
152
+ # 44100 int16s = 22050 frames = 0.5s (1 frame * 2 channels = 2 int16 = 1 sample = 1/44100 s)
153
+ @buffer_size = @sample_rate * @channels * 1.0
154
+ # how many samples there are in each buffer, irrespective of channels
155
+ @buffer_length = @buffer_size / @channels
156
+ # buffer_duration = buffer_length / sample_rate
157
+ end
94
158
  end
95
159
 
96
160
  # Queue audio frames for playback.
97
161
  #
98
- # @param [Array<[ Channels… ]>] frames array of N-sized arrays of integers.
162
+ # @note this method is here for backwards-compatibility,
163
+ # and does not support changing format automatically.
164
+ # You should use {#stream} instead.
165
+ #
166
+ # @param [Array<Integer>] array of interleaved audio samples.
167
+ # @return (see #stream)
99
168
  def <<(frames)
100
- if buffers_processed > 0
101
- FFI::MemoryPointer.new(OpenAL::Buffer, buffers_processed) do |ptr|
102
- OpenAL.source_unqueue_buffers(@source, ptr.count, ptr)
103
- @total_buffers_processed += ptr.count
104
- @free_buffers.concat OpenAL::Buffer.extract(ptr, ptr.count)
105
- @queued_buffers.delete_if { |buffer| @free_buffers.include?(buffer) }
169
+ stream(frames, format)
170
+ end
171
+
172
+ # Queue audio frames for playback.
173
+ #
174
+ # @param [Array<Integer>] array of interleaved audio samples.
175
+ # @param [Hash] format
176
+ # @option format [Symbol] :sample_type should be :int16
177
+ # @option format [Integer] :sample_rate
178
+ # @option format [Integer] :channels
179
+ # @return [Integer] number of frames consumed (consumed_samples / channels), a multiple of channels
180
+ def stream(frames, frame_format)
181
+ synchronize do
182
+ if @source.playing? and @source.buffers_processed > 0
183
+ FFI::MemoryPointer.new(OpenAL::Buffer, @source.buffers_processed) do |ptr|
184
+ OpenAL.source_unqueue_buffers(@source, ptr.count, ptr)
185
+ @total_buffers_processed += ptr.count
186
+ @free_buffers.concat OpenAL::Buffer.extract(ptr, ptr.count)
187
+ @queued_buffers.delete_if { |buffer| @free_buffers.include?(buffer) }
188
+ end
106
189
  end
107
- end
108
190
 
109
- wanted_size = (@buffer_size - @queued_frames.length).div(@channels) * @channels
110
- consumed_frames = frames.take(wanted_size)
111
- @queued_frames.concat(consumed_frames)
191
+ self.format = frame_format if frame_format != format
112
192
 
113
- if @queued_frames.length >= @buffer_size and @free_buffers.any?
114
- current_buffer = @free_buffers.shift
193
+ wanted_size = (@buffer_size - @queued_frames.length).div(@channels) * @channels
194
+ consumed_frames = frames.take(wanted_size)
195
+ @queued_frames.concat(consumed_frames)
115
196
 
116
- FFI::MemoryPointer.new(@sample_type, @queued_frames.length) do |frames|
117
- frames.public_send(:"write_array_of_#{@sample_type}", @queued_frames)
118
- # stereo16 = 2 int16s (1 frame) = 1 sample
119
- OpenAL.buffer_data(current_buffer, @sample_format, frames, frames.size, @sample_rate)
120
- @queued_frames.clear
121
- end
197
+ if @queued_frames.length >= @buffer_size and @free_buffers.any?
198
+ current_buffer = @free_buffers.shift
199
+
200
+ FFI::MemoryPointer.new(@sample_type, @queued_frames.length) do |frames|
201
+ frames.public_send(:"write_array_of_#{@sample_type}", @queued_frames)
202
+ # stereo16 = 2 int16s (1 frame) = 1 sample
203
+ OpenAL.buffer_data(current_buffer, @sample_format, frames, frames.size, @sample_rate)
204
+ @queued_frames.clear
205
+ end
206
+
207
+ FFI::MemoryPointer.new(OpenAL::Buffer, 1) do |buffers|
208
+ buffers.write_uint(current_buffer.to_native)
209
+ OpenAL.source_queue_buffers(@source, buffers.count, buffers)
210
+ end
122
211
 
123
- FFI::MemoryPointer.new(OpenAL::Buffer, 1) do |buffers|
124
- buffers.write_uint(current_buffer.to_native)
125
- OpenAL.source_queue_buffers(@source, buffers.count, buffers)
212
+ @queued_buffers.push(current_buffer)
126
213
  end
127
214
 
128
- @queued_buffers.push(current_buffer)
215
+ consumed_frames.length / @channels
129
216
  end
130
-
131
- consumed_frames.length
132
217
  end
133
218
 
134
219
  protected
135
220
 
136
- def sample_offset
137
- @source.get(:sample_offset, Integer)
138
- end
139
-
140
- def buffers_processed
141
- if not @source.stopped?
142
- @source.get(:buffers_processed, Integer)
143
- else
144
- 0
145
- end
221
+ def synchronize
222
+ @monitor.synchronize { return yield }
146
223
  end
147
224
  end
@@ -2,6 +2,10 @@ class Plaything
2
2
  module OpenAL
3
3
  class Buffer < TypeClass(FFI::Type::UINT)
4
4
  include OpenAL::Paramable(:buffer)
5
+
6
+ def self.extract(pointer, count)
7
+ count.times.map { |index| new pointer.get_uint(type.size * index) }
8
+ end
5
9
  end
6
10
  end
7
11
  end
@@ -3,6 +3,37 @@ class Plaything
3
3
  class Source < TypeClass(FFI::Type::UINT)
4
4
  include OpenAL::Paramable(:source)
5
5
 
6
+ # Start playback.
7
+ def play
8
+ OpenAL.source_play(self)
9
+ end
10
+
11
+ # Pause playback.
12
+ def pause
13
+ OpenAL.source_pause(self)
14
+ end
15
+
16
+ # Stop playback and rewind the source.
17
+ def stop
18
+ OpenAL.source_stop(self)
19
+ end
20
+
21
+ # @return [Integer] how many samples (/ channels) that have been played from the queued buffers
22
+ def sample_offset
23
+ get(:sample_offset, Integer)
24
+ end
25
+
26
+ # @return [Integer] number of queued buffers.
27
+ def buffers_queued
28
+ get(:buffers_queued, Integer)
29
+ end
30
+
31
+ # @note returns {#buffers_queued} if source is not playing!
32
+ # @return [Integer] number of processed buffers.
33
+ def buffers_processed
34
+ get(:buffers_processed, Integer)
35
+ end
36
+
6
37
  # Detach all queued or attached buffers.
7
38
  #
8
39
  # @note all buffers must be processed for this operation to succeed.
@@ -5,6 +5,8 @@ class Plaything
5
5
  ffi_lib ["openal", "/System/Library/Frameworks/OpenAL.framework/Versions/Current/OpenAL"]
6
6
 
7
7
  typedef :pointer, :attributes
8
+ typedef :pointer, :source_array
9
+ typedef :pointer, :buffer_array
8
10
  typedef :int, :sizei
9
11
 
10
12
  # Errors
@@ -61,15 +63,15 @@ class Plaything
61
63
  attach_function :alcMakeContextCurrent, [ Context ], :bool
62
64
 
63
65
  # Sources
64
- attach_function :alGenSources, [ :sizei, :pointer ], :void
65
- attach_function :alDeleteSources, [ :sizei, :pointer ], :void
66
+ attach_function :alGenSources, [ :sizei, :source_array ], :void
67
+ attach_function :alDeleteSources, [ :sizei, :source_array ], :void
66
68
 
67
69
  attach_function :alSourcePlay, [ Source ], :void
68
70
  attach_function :alSourcePause, [ Source ], :void
69
71
  attach_function :alSourceStop, [ Source ], :void
70
72
 
71
- attach_function :alSourceQueueBuffers, [ Source, :sizei, :pointer ], :void
72
- attach_function :alSourceUnqueueBuffers, [ Source, :sizei, :pointer ], :void
73
+ attach_function :alSourceQueueBuffers, [ Source, :sizei, :buffer_array ], :void
74
+ attach_function :alSourceUnqueueBuffers, [ Source, :sizei, :buffer_array ], :void
73
75
 
74
76
  # Buffers
75
77
  enum :format, [
@@ -78,8 +80,8 @@ class Plaything
78
80
  :stereo8, 0x1102,
79
81
  :stereo16, 0x1103,
80
82
  ]
81
- attach_function :alGenBuffers, [ :sizei, :pointer ], :void
82
- attach_function :alDeleteBuffers, [ :sizei, :pointer ], :void
83
+ attach_function :alGenBuffers, [ :sizei, :buffer_array ], :void
84
+ attach_function :alDeleteBuffers, [ :sizei, :buffer_array ], :void
83
85
 
84
86
  attach_function :alBufferData, [ Buffer, :format, :pointer, :sizei, :sizei ], :void
85
87
 
@@ -13,13 +13,6 @@ class Plaything
13
13
  rescue => e
14
14
  warn "release for #{name} failed: #{e.message}."
15
15
  end
16
-
17
- def allocate(*args, &block)
18
- pointer = FFI::MemoryPointer.new(*args)
19
- yield pointer
20
- pointer.autorelease = false
21
- new(FFI::Pointer.new(pointer))
22
- end
23
16
  end
24
17
  end
25
18
  end
@@ -3,17 +3,16 @@ class Plaything
3
3
  def self.TypeClass(type)
4
4
  Class.new do
5
5
  extend FFI::DataConverter
6
- @@type = type
6
+
7
+ define_singleton_method(:type) do
8
+ type
9
+ end
7
10
 
8
11
  class << self
9
12
  def inherited(other)
10
13
  other.native_type(type)
11
14
  end
12
15
 
13
- def type
14
- @@type
15
- end
16
-
17
16
  def to_native(source, ctx)
18
17
  source.value
19
18
  end
@@ -25,12 +24,6 @@ class Plaything
25
24
  def size
26
25
  type.size
27
26
  end
28
-
29
- def extract(pointer, count)
30
- pointer.read_array_of_type(self, :read_uint, count).map do |uint|
31
- new(uint)
32
- end
33
- end
34
27
  end
35
28
 
36
29
  def initialize(value)
@@ -1,3 +1,3 @@
1
1
  class Plaything
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0"
3
3
  end
metadata CHANGED
@@ -1,64 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plaything
3
3
  version: !ruby/object:Gem::Version
4
- prerelease:
5
- version: 1.0.0
4
+ version: 1.1.0
6
5
  platform: ruby
7
6
  authors:
8
7
  - Kim Burgestrand
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2013-03-26 00:00:00.000000000 Z
11
+ date: 2013-04-09 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
- version_requirements: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ~>
18
- - !ruby/object:Gem::Version
19
- version: '1.1'
20
- none: false
21
- prerelease: false
22
14
  name: ffi
23
15
  requirement: !ruby/object:Gem::Requirement
24
16
  requirements:
25
17
  - - ~>
26
18
  - !ruby/object:Gem::Version
27
19
  version: '1.1'
28
- none: false
29
20
  type: :runtime
30
- - !ruby/object:Gem::Dependency
21
+ prerelease: false
31
22
  version_requirements: !ruby/object:Gem::Requirement
32
23
  requirements:
33
- - - ! '>='
24
+ - - ~>
34
25
  - !ruby/object:Gem::Version
35
- version: '0'
36
- none: false
37
- prerelease: false
26
+ version: '1.1'
27
+ - !ruby/object:Gem::Dependency
38
28
  name: rspec
39
29
  requirement: !ruby/object:Gem::Requirement
40
30
  requirements:
41
- - - ! '>='
31
+ - - '>='
42
32
  - !ruby/object:Gem::Version
43
33
  version: '0'
44
- none: false
45
34
  type: :development
46
- - !ruby/object:Gem::Dependency
35
+ prerelease: false
47
36
  version_requirements: !ruby/object:Gem::Requirement
48
37
  requirements:
49
- - - ! '>='
38
+ - - '>='
50
39
  - !ruby/object:Gem::Version
51
40
  version: '0'
52
- none: false
53
- prerelease: false
41
+ - !ruby/object:Gem::Dependency
54
42
  name: rake
55
43
  requirement: !ruby/object:Gem::Requirement
56
44
  requirements:
57
- - - ! '>='
45
+ - - '>='
58
46
  - !ruby/object:Gem::Version
59
47
  version: '0'
60
- none: false
61
48
  type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
62
55
  description:
63
56
  email:
64
57
  - kim@burgestrand.se
@@ -90,30 +83,26 @@ files:
90
83
  homepage: https://github.com/Burgestrand/plaything
91
84
  licenses:
92
85
  - MIT
86
+ metadata: {}
93
87
  post_install_message:
94
88
  rdoc_options: []
95
89
  require_paths:
96
90
  - lib
97
91
  required_ruby_version: !ruby/object:Gem::Requirement
98
92
  requirements:
99
- - - ! '>='
93
+ - - '>='
100
94
  - !ruby/object:Gem::Version
101
95
  version: '1.9'
102
- none: false
103
96
  required_rubygems_version: !ruby/object:Gem::Requirement
104
97
  requirements:
105
- - - ! '>='
98
+ - - '>='
106
99
  - !ruby/object:Gem::Version
107
100
  version: '0'
108
- segments:
109
- - 0
110
- hash: 2964046639458012733
111
- none: false
112
101
  requirements: []
113
102
  rubyforge_project:
114
- rubygems_version: 1.8.24
103
+ rubygems_version: 2.0.3
115
104
  signing_key:
116
- specification_version: 3
105
+ specification_version: 4
117
106
  summary: Blast raw PCM audio through your speakers using OpenAL.
118
107
  test_files:
119
108
  - spec/plaything_spec.rb