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