audio-playback 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6610c16cd941a9fac1b38c72500b6d7cabb6e065
4
- data.tar.gz: c6e6902a5c28a811f05a0e5bbc1cd425cef60fb1
3
+ metadata.gz: 6d31788d41c35211e2136b9e47ec91313124292c
4
+ data.tar.gz: b4266924f87e9120e0eef56427cca84f1c7a83da
5
5
  SHA512:
6
- metadata.gz: e05488f906b9f0783b9b5fca1665bef683a37c0dfbf7c432bb5df866b34da7a11f32e57ba91bf5a3d3cdd6648ceab83d504c78934ff2be5ef5abf58315274ed4
7
- data.tar.gz: 01dca1cea7cab6165770841b57ee11e7a7d136add803cd6a0ca6ac608325a43f4aa9e2ffad191a98ca880a85e59bd93d109b21a1ae6300f07c83faf28733f02b
6
+ metadata.gz: dd437d403ba74b706e8d28f62f9b9d65724bfde9fa84cb2ab0ca9ae5a9ed41939480b740e58b1f3bc24d36e743d97a83f058788fb674bf5d1849a6f09eeafb98
7
+ data.tar.gz: fa358efb5773f25ee46dfd88c9b97d2093f5b4b9b17442a68389f50d711a9d725b04185fedbe09490ce84af7a858bb2e7141d8ab5dadc679204c3cb612cfdbb9
data/README.md CHANGED
@@ -1,15 +1,19 @@
1
1
  # Audio Playback
2
2
 
3
- Play audio files at the command line or using Ruby
3
+ A command line and Ruby tool for playing audio files
4
+
5
+ Under the hood the *portaudio* and *libsndfile* libraries are used, enabling the gem to be cross-platform on any systems where these libraries are available
4
6
 
5
7
  ## Installation
6
8
 
7
9
  These packages must be installed first:
8
10
 
9
- * portaudio
10
- * libsndfile
11
+ * libsndfile ([link](https://github.com/erikd/libsndfile))
12
+ * portaudio ([link](http://portaudio.com/docs/v19-doxydocs/pages.html))
13
+
14
+ Both libraries are available in *Homebrew*, *APT*, *Yum* as well as many other package managers. For those who wish to compile themselves or need more information about those packages, follow the links above for more information
11
15
 
12
- Install the gem using
16
+ Once those libraries are installed, install the gem itself using
13
17
 
14
18
  gem install audio-playback
15
19
 
@@ -29,7 +33,7 @@ Or if you're using Bundler, add this to your Gemfile
29
33
 
30
34
  * `-b` Buffer size in bytes. Defaults to 4096
31
35
 
32
- * `-c` Output audio to the given channel(s). Eg `-c 0,1` will direct audio to channels 0 and 1. Defaults to use all available channels
36
+ * `-c` Output audio to the given channel(s). Eg `-c 0,1` will direct audio to channels 0 and 1. Defaults to use channels 0 and 1 on the selected device
33
37
 
34
38
  * `-o` Output device id or name. Defaults to the system default
35
39
 
@@ -58,6 +62,7 @@ options = {
58
62
 
59
63
  @playback = AudioPlayback.play("test/media/1-stereo-44100.wav", options)
60
64
 
65
+ # Play in the foreground
61
66
  @playback.block
62
67
 
63
68
  ```
@@ -66,7 +71,7 @@ options = {
66
71
 
67
72
  * `:buffer_size` Buffer size in bytes. Defaults to 4096
68
73
 
69
- * `:channel` or `:channels` Output audio to the given channel(s). Eg `:channels => [0,1]` will direct the audio to channels 0 and 1. Defaults to use all available channels
74
+ * `:channel` or `:channels` Output audio to the given channel(s). Eg `:channels => [0,1]` will direct the audio to channels 0 and 1. Defaults to use channels 0 and 1 on the selected device
70
75
 
71
76
  * `:latency` Latency in seconds. Defaults to use the default latency for the selected output device
72
77
 
@@ -74,6 +79,15 @@ options = {
74
79
 
75
80
  * `:output_device` Output device id or name
76
81
 
82
+ #### More Examples
83
+
84
+ More Ruby code examples:
85
+
86
+ * [List devices](https://github.com/arirusso/audio-playback/blob/master/examples/list_devices.rb)
87
+ * [Select a file and play](https://github.com/arirusso/audio-playback/blob/master/examples/select_and_play.rb)
88
+ * [Play multiple files in one stream](https://github.com/arirusso/audio-playback/blob/master/examples/play_multiple.rb)
89
+ * [Play multiple files in multiple streams](https://github.com/arirusso/audio-playback/blob/master/examples/play_multiple.rb)
90
+
77
91
  ## License
78
92
 
79
93
  Licensed under Apache 2.0, See the file LICENSE
data/bin/playback CHANGED
@@ -12,11 +12,13 @@ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
12
12
  #
13
13
  # * `-b` Buffer size in bytes. Defaults to 4096
14
14
  #
15
- # * `-c` Output audio to the given channel(s). Eg `-c 0,1` will direct audio to channels 0 and 1. Defaults to use all available channels
15
+ # * `-c` Output audio to the given channel(s). Eg `-c 0,1` will direct audio to channels 0 and 1. Defaults to use channels 0 and 1 on the selected device
16
16
  #
17
17
  # * `-o` Output device id or name. Defaults to the system default
18
18
  #
19
- # * `-v` Verbose
19
+ # * `-v` or `--verbose` Verbose
20
+ #
21
+ # * `--list-devices` List the available audio output devices
20
22
  #
21
23
  #
22
24
 
@@ -24,20 +24,21 @@ require "audio-playback/sound"
24
24
  # Play audio files
25
25
  module AudioPlayback
26
26
 
27
- VERSION = "0.0.2"
27
+ VERSION = "0.0.3"
28
28
 
29
29
  # Convenience method to play an audio file
30
- # @param [::File, String] file_path
30
+ # @param [Array<::File>, Array<String>, ::File, String] file_paths
31
31
  # @param [Hash] options
32
32
  # @option options [Fixnum] :buffer_size Buffer size in bytes. Defaults to 4096
33
33
  # @option options [Array<Fixnum>, Fixnum] :channels (or: :channel) Output audio to the given channel(s). Eg `:channels => [0,1]` will direct the audio to channels 0 and 1. Defaults to use all available channels
34
34
  # @option options [Float] :latency Latency in seconds. Defaults to use the default latency for the selected output device
35
35
  # @option options [IO] :logger Logger object
36
- # @option options [Fixnum, String] :output_device Output device id or name
37
- def self.play(file_path, options = {})
38
- sound = Sound.load(file_path, options)
39
- output = Device::Output.by_name(options[:output_device]) || Device::Output.by_id(options[:output_device]) || Device.default_output
40
- Playback.play(sound, output, options)
36
+ # @option options [Fixnum, String] :output_device (or: :output) Output device id or name
37
+ def self.play(file_paths, options = {})
38
+ sounds = Array(file_paths).map { |path| Sound.load(path, options) }
39
+ requested_device = options[:output_device] || options[:output]
40
+ output = Device::Output.by_name(requested_device) || Device::Output.by_id(requested_device) || Device.default_output
41
+ Playback.play(sounds, output, options)
41
42
  end
42
43
 
43
44
  # List the available audio output devices
@@ -4,6 +4,12 @@ module AudioPlayback
4
4
 
5
5
  class Stream < FFI::PortAudio::Stream
6
6
 
7
+ # Keep track of all streams
8
+ # @return [Array<Stream>]
9
+ def self.streams
10
+ @streams ||= []
11
+ end
12
+
7
13
  # @param [Output] output
8
14
  # @param [Hash] options
9
15
  # @option options [IO] logger
@@ -13,6 +19,7 @@ module AudioPlayback
13
19
  @input = nil
14
20
  @output = output.resource
15
21
  initialize_exit_callback(:logger => options[:logger])
22
+ Stream.streams << self
16
23
  end
17
24
 
18
25
  # Perform the given playback
@@ -36,14 +43,19 @@ module AudioPlayback
36
43
  # Block process until the current playback finishes
37
44
  # @return [Boolean]
38
45
  def block
39
- while active?
40
- sleep(0.0001)
41
- end
42
- while FFI::PortAudio::API.Pa_IsStreamActive(@stream.read_pointer) != :paNoError
43
- sleep(1)
46
+ begin
47
+ while active?
48
+ sleep(0.0001)
49
+ end
50
+ while FFI::PortAudio::API.Pa_IsStreamActive(@stream.read_pointer) != :paNoError
51
+ sleep(1)
52
+ end
53
+ rescue SystemExit, Interrupt
54
+ # Control-C
55
+ ensure
56
+ exit
57
+ true
44
58
  end
45
- exit
46
- true
47
59
  end
48
60
 
49
61
  private
@@ -61,7 +73,7 @@ module AudioPlayback
61
73
  logger = options[:logger]
62
74
  logger.puts("Exit") if logger
63
75
  unless @stream.nil?
64
- close
76
+ #close
65
77
  FFI::PortAudio::API.Pa_Terminate
66
78
  end
67
79
  true
@@ -1,5 +1,6 @@
1
1
  require "audio-playback/playback/frame"
2
2
  require "audio-playback/playback/frame_set"
3
+ require "audio-playback/playback/mixer"
3
4
  require "audio-playback/playback/stream_data"
4
5
 
5
6
  module AudioPlayback
@@ -19,18 +20,18 @@ module AudioPlayback
19
20
 
20
21
  extend Forwardable
21
22
 
22
- attr_reader :buffer_size, :channels, :data, :output, :num_channels, :sound, :stream
23
- def_delegators :@sound, :audio_file, :sample_rate, :size
23
+ attr_reader :buffer_size, :channels, :data, :output, :num_channels, :sounds, :stream
24
+ def_delegators :@sounds, :audio_files
24
25
 
25
- # @param [Sound] sound
26
+ # @param [Array<Sound>, Sound] sounds
26
27
  # @param [Output] output
27
28
  # @param [Hash] options
28
29
  # @option options [Fixnum] :buffer_size
29
30
  # @option options [Array<Fixnum>, Fixnum] :channels (or: :channel)
30
31
  # @option options [IO] :logger
31
32
  # @option options [Stream] :stream
32
- def initialize(sound, output, options = {})
33
- @sound = sound
33
+ def initialize(sounds, output, options = {})
34
+ @sounds = Array(sounds)
34
35
  @buffer_size = options[:buffer_size] || DEFAULT[:buffer_size]
35
36
  @output = output
36
37
  @stream = options[:stream] || Device::Stream.new(@output, options)
@@ -38,12 +39,21 @@ module AudioPlayback
38
39
  report(options[:logger]) if options[:logger]
39
40
  end
40
41
 
42
+ def sample_rate
43
+ @sounds.last.sample_rate
44
+ end
45
+
46
+ def size
47
+ @sounds.map(&:size).max
48
+ end
49
+
41
50
  # Start playback
42
51
  # @return [Playback]
43
52
  def start
44
53
  @stream.play(self)
45
54
  self
46
55
  end
56
+ alias_method :play, :start
47
57
 
48
58
  # Block process until playback finishes
49
59
  # @return [Stream]
@@ -55,7 +65,8 @@ module AudioPlayback
55
65
  # @param [IO] logger
56
66
  # @return [Boolean]
57
67
  def report(logger)
58
- logger.puts("Playback report for #{@sound.audio_file.path}")
68
+ paths = @sounds.map(&:audio_file).map(&:path)
69
+ logger.puts("Playback report for #{paths}")
59
70
  logger.puts(" Number of channels: #{@num_channels}")
60
71
  logger.puts(" Direct audio to channels #{@channels.to_s}") unless @channels.nil?
61
72
  logger.puts(" Buffer size: #{@buffer_size}")
@@ -66,10 +77,12 @@ module AudioPlayback
66
77
  # Total size of the playback's sound frames in bytes
67
78
  # @return [Fixnum]
68
79
  def data_size
69
- frames = (@sound.size * @num_channels) + METADATA.count
80
+ frames = (size * @num_channels) + METADATA.count
70
81
  frames * FRAME_SIZE.size
71
82
  end
72
83
 
84
+ # Has a different channel configuration than the default been requested?
85
+ # @return [Boolean]
73
86
  def channels_requested?
74
87
  !@channels.nil?
75
88
  end
@@ -16,26 +16,35 @@ module AudioPlayback
16
16
 
17
17
  private
18
18
 
19
- # Populate the Container
19
+ # Build the frame set data for the given playback action and sound
20
20
  # @param [Playback::Action] playback
21
+ # @param [Sound] sound
21
22
  # @return [Array<Array<Float>>]
22
- def populate(playback)
23
- data = playback.sound.data.dup
23
+ def build_for_sound(playback, sound)
24
+ data = sound.data
24
25
  data = ensure_array_frames(data)
25
26
  data = to_frame_objects(data)
27
+ data = build_channels(data, playback) if !channels_match?(playback, sound)
28
+ data
29
+ end
26
30
 
27
- @data = if channels_match?(playback)
28
- data
31
+ # Populate the Container
32
+ # @param [Playback::Action] playback
33
+ # @return [Array<Array<Float>>]
34
+ def populate(playback)
35
+ data = playback.sounds.map { |sound| build_for_sound(playback, sound) }
36
+ @data = if data.count > 1
37
+ Mixer.mix(data)
29
38
  else
30
- build_channels(data, playback)
39
+ data[0]
31
40
  end
32
41
  end
33
42
 
34
43
  # Does the channel structure of the playback action match the channel structure of the sound?
35
44
  # @param [Playback::Action] playback
36
45
  # @return [Boolean]
37
- def channels_match?(playback)
38
- playback.sound.num_channels == playback.num_channels && playback.channels.nil?
46
+ def channels_match?(playback, sound)
47
+ sound.num_channels == playback.num_channels && playback.channels.nil?
39
48
  end
40
49
 
41
50
  # (Re-)build the channel structure of the frame set
@@ -0,0 +1,60 @@
1
+ module AudioPlayback
2
+
3
+ module Playback
4
+
5
+ # Mix sound data
6
+ class Mixer
7
+
8
+ # Mix multiple sounds at equal amplitude
9
+ # @param [Array<Array<Array<Fixnum>>>] sounds_data
10
+ # @return [Array<Array<Fixnum>>]
11
+ def self.mix(sounds_data)
12
+ mixer = new(sounds_data)
13
+ mixer.mix
14
+ end
15
+
16
+ # @param [Array<Array<Array<Fixnum>>>] sounds_data
17
+ def initialize(sounds_data)
18
+ @data = sounds_data
19
+ populate
20
+ end
21
+
22
+ # Mix multiple sounds at equal amplitude
23
+ # @return [Array<Array<Fixnum>>]
24
+ def mix
25
+ (0..@length-1).to_a.map { |index| mix_frame(index) }
26
+ end
27
+
28
+ private
29
+
30
+ # Populate the mixer metadata
31
+ # @return [Mixer]
32
+ def populate
33
+ @length = @data.map(&:size).max
34
+ @depth = @data.count
35
+ self
36
+ end
37
+
38
+ # Get all of the data frames for the given index
39
+ # For example for index 3, two two channel sounds, frames(3) might give you [[1, 3], [2, 3]]
40
+ # @param [Fixnum] index
41
+ # @return [Array<Array<Fixnum>>]
42
+ def frames(index)
43
+ @data.map { |sound_data| sound_data[index] }
44
+ end
45
+
46
+ # Mix the frame with the given index
47
+ # whereas frames(3) might give you [[1, 3], [2, 3]]
48
+ # mix_frame(3) might give you [1.5, 3]
49
+ # @param [Fixnum] index
50
+ # @return [Array<Fixnum>]
51
+ def mix_frame(index)
52
+ totals = frames(index).compact.transpose.map { |x| x && x.reduce(:+) || 0 }
53
+ totals.map { |frame| frame / @depth }
54
+ end
55
+
56
+ end
57
+
58
+ end
59
+
60
+ end
@@ -6,6 +6,8 @@ module AudioPlayback
6
6
  class StreamData
7
7
 
8
8
  # A C pointer version of the audio data
9
+ # @param [Playback::Action] playback
10
+ # @return [FFI::Pointer]
9
11
  def self.to_pointer(playback)
10
12
  stream_data = new(playback)
11
13
  stream_data.to_pointer
@@ -41,7 +43,7 @@ module AudioPlayback
41
43
  @data.unshift(0.0) # 3. is_eof
42
44
  @data.unshift(0.0) # 2. counter
43
45
  @data.unshift(@playback.output.num_channels.to_f) # 1. num_channels
44
- @data.unshift(@playback.sound.size.to_f) # 0. sample size
46
+ @data.unshift(@playback.size.to_f) # 0. sample size
45
47
  @data
46
48
  end
47
49
 
data/test/media/2.wav ADDED
Binary file
@@ -0,0 +1,25 @@
1
+ require "helper"
2
+
3
+ class AudioPlayback::Playback::MixerTest < Minitest::Test
4
+
5
+ context "Mixer" do
6
+
7
+ context ".mix" do
8
+
9
+ setup do
10
+ @sound1 = [[1,2],[3,4],[5, 6]]
11
+ @sound2 = [[7,8],[9, 10]]
12
+ @sound = [@sound1, @sound2]
13
+ end
14
+
15
+ should "mix channels" do
16
+ @result = AudioPlayback::Playback::Mixer.mix(@sound)
17
+ refute_nil @result
18
+ assert_equal [[4, 5], [6, 7], [2, 3]], @result
19
+ end
20
+
21
+ end
22
+
23
+ end
24
+
25
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: audio-playback
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ari Russo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-10-21 00:00:00.000000000 Z
11
+ date: 2015-11-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -170,6 +170,7 @@ files:
170
170
  - lib/audio-playback/playback.rb
171
171
  - lib/audio-playback/playback/frame.rb
172
172
  - lib/audio-playback/playback/frame_set.rb
173
+ - lib/audio-playback/playback/mixer.rb
173
174
  - lib/audio-playback/playback/stream_data.rb
174
175
  - lib/audio-playback/sound.rb
175
176
  - test/device/output_test.rb
@@ -181,8 +182,10 @@ files:
181
182
  - test/media/1-mono-44100.wav
182
183
  - test/media/1-stereo-44100.aiff
183
184
  - test/media/1-stereo-44100.wav
185
+ - test/media/2.wav
184
186
  - test/playback/frame_set_test.rb
185
187
  - test/playback/frame_test.rb
188
+ - test/playback/mixer_test.rb
186
189
  - test/playback/stream_data_test.rb
187
190
  - test/playback_test.rb
188
191
  - test/sound_test.rb