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 +4 -4
- data/README.md +20 -6
- data/bin/playback +4 -2
- data/lib/audio-playback.rb +8 -7
- data/lib/audio-playback/device/stream.rb +20 -8
- data/lib/audio-playback/playback.rb +20 -7
- data/lib/audio-playback/playback/frame_set.rb +17 -8
- data/lib/audio-playback/playback/mixer.rb +60 -0
- data/lib/audio-playback/playback/stream_data.rb +3 -1
- data/test/media/2.wav +0 -0
- data/test/playback/mixer_test.rb +25 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6d31788d41c35211e2136b9e47ec91313124292c
|
4
|
+
data.tar.gz: b4266924f87e9120e0eef56427cca84f1c7a83da
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dd437d403ba74b706e8d28f62f9b9d65724bfde9fa84cb2ab0ca9ae5a9ed41939480b740e58b1f3bc24d36e743d97a83f058788fb674bf5d1849a6f09eeafb98
|
7
|
+
data.tar.gz: fa358efb5773f25ee46dfd88c9b97d2093f5b4b9b17442a68389f50d711a9d725b04185fedbe09490ce84af7a858bb2e7141d8ab5dadc679204c3cb612cfdbb9
|
data/README.md
CHANGED
@@ -1,15 +1,19 @@
|
|
1
1
|
# Audio Playback
|
2
2
|
|
3
|
-
|
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
|
-
*
|
10
|
-
*
|
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
|
-
|
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
|
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
|
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
|
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
|
|
data/lib/audio-playback.rb
CHANGED
@@ -24,20 +24,21 @@ require "audio-playback/sound"
|
|
24
24
|
# Play audio files
|
25
25
|
module AudioPlayback
|
26
26
|
|
27
|
-
VERSION = "0.0.
|
27
|
+
VERSION = "0.0.3"
|
28
28
|
|
29
29
|
# Convenience method to play an audio file
|
30
|
-
# @param [::File, String]
|
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(
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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, :
|
23
|
-
def_delegators :@
|
23
|
+
attr_reader :buffer_size, :channels, :data, :output, :num_channels, :sounds, :stream
|
24
|
+
def_delegators :@sounds, :audio_files
|
24
25
|
|
25
|
-
# @param [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(
|
33
|
-
@
|
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
|
-
|
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 = (
|
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
|
-
#
|
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
|
23
|
-
data =
|
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
|
-
|
28
|
-
|
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
|
-
|
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
|
-
|
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.
|
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.
|
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-
|
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
|