plaything 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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