audio-playback 0.0.2 → 0.0.3

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.
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