audio-playback 0.0.2
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 +7 -0
- data/LICENSE +13 -0
- data/README.md +81 -0
- data/bin/playback +66 -0
- data/lib/audio-playback.rb +55 -0
- data/lib/audio-playback/commandline.rb +50 -0
- data/lib/audio-playback/device.rb +61 -0
- data/lib/audio-playback/device/output.rb +113 -0
- data/lib/audio-playback/device/stream.rb +134 -0
- data/lib/audio-playback/file.rb +37 -0
- data/lib/audio-playback/playback.rb +152 -0
- data/lib/audio-playback/playback/frame.rb +72 -0
- data/lib/audio-playback/playback/frame_set.rb +117 -0
- data/lib/audio-playback/playback/stream_data.rb +52 -0
- data/lib/audio-playback/sound.rb +54 -0
- data/test/device/output_test.rb +141 -0
- data/test/device/stream_test.rb +54 -0
- data/test/device_test.rb +75 -0
- data/test/file_test.rb +105 -0
- data/test/helper.rb +59 -0
- data/test/media/1-mono-44100.aiff +0 -0
- data/test/media/1-mono-44100.wav +0 -0
- data/test/media/1-stereo-44100.aiff +0 -0
- data/test/media/1-stereo-44100.wav +0 -0
- data/test/playback/frame_set_test.rb +115 -0
- data/test/playback/frame_test.rb +46 -0
- data/test/playback/stream_data_test.rb +27 -0
- data/test/playback_test.rb +111 -0
- data/test/sound_test.rb +80 -0
- metadata +213 -0
@@ -0,0 +1,134 @@
|
|
1
|
+
module AudioPlayback
|
2
|
+
|
3
|
+
module Device
|
4
|
+
|
5
|
+
class Stream < FFI::PortAudio::Stream
|
6
|
+
|
7
|
+
# @param [Output] output
|
8
|
+
# @param [Hash] options
|
9
|
+
# @option options [IO] logger
|
10
|
+
def initialize(output, options = {})
|
11
|
+
@is_muted = false
|
12
|
+
@gain = 1.0
|
13
|
+
@input = nil
|
14
|
+
@output = output.resource
|
15
|
+
initialize_exit_callback(:logger => options[:logger])
|
16
|
+
end
|
17
|
+
|
18
|
+
# Perform the given playback
|
19
|
+
# @param [Playback] playback
|
20
|
+
# @param [Hash] options
|
21
|
+
# @option options [IO] logger
|
22
|
+
# @return [Stream]
|
23
|
+
def play(playback, options = {})
|
24
|
+
report(playback, options[:logger]) if options[:logger]
|
25
|
+
open_playback(playback)
|
26
|
+
start
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
# Is the stream active?
|
31
|
+
# @return [Boolean]
|
32
|
+
def active?
|
33
|
+
FFI::PortAudio::API.Pa_IsStreamActive(@stream.read_pointer) == 1
|
34
|
+
end
|
35
|
+
|
36
|
+
# Block process until the current playback finishes
|
37
|
+
# @return [Boolean]
|
38
|
+
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)
|
44
|
+
end
|
45
|
+
exit
|
46
|
+
true
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
# Initialize the callback that's fired when the stream exits
|
52
|
+
# @return [Stream]
|
53
|
+
def initialize_exit_callback(options = {})
|
54
|
+
at_exit { exit_callback(options) }
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
# Callback that's fired when the stream exits
|
59
|
+
# @return [Boolean]
|
60
|
+
def exit_callback(options = {})
|
61
|
+
logger = options[:logger]
|
62
|
+
logger.puts("Exit") if logger
|
63
|
+
unless @stream.nil?
|
64
|
+
close
|
65
|
+
FFI::PortAudio::API.Pa_Terminate
|
66
|
+
end
|
67
|
+
true
|
68
|
+
end
|
69
|
+
|
70
|
+
# Initialize the stream for playback
|
71
|
+
# @param [Playback] playback
|
72
|
+
# @return [Boolean]
|
73
|
+
def open_playback(playback)
|
74
|
+
open(@input, @output, playback.sample_rate.to_i, playback.buffer_size, FFI::PortAudio::API::NoFlag, playback.data)
|
75
|
+
true
|
76
|
+
end
|
77
|
+
|
78
|
+
# Report about the stream
|
79
|
+
# @param [Playback] playback
|
80
|
+
# @param [IO] logger
|
81
|
+
# @return [Stream]
|
82
|
+
def report(playback, logger)
|
83
|
+
self
|
84
|
+
end
|
85
|
+
|
86
|
+
# Portaudio stream callback
|
87
|
+
def process(input, output, frames_per_buffer, time_info, status_flags, user_data)
|
88
|
+
#puts "--"
|
89
|
+
#puts "Entering callback at #{Time.now.to_f}"
|
90
|
+
counter = user_data.get_float32(Playback::METADATA.index(:pointer) * Playback::FRAME_SIZE).to_i
|
91
|
+
#puts "Frame: #{counter}"
|
92
|
+
sample_size = user_data.get_float32(Playback::METADATA.index(:size) * Playback::FRAME_SIZE).to_i
|
93
|
+
#puts "Sample size: #{sample_size}"
|
94
|
+
num_channels = user_data.get_float32(Playback::METADATA.index(:num_channels) * Playback::FRAME_SIZE).to_i
|
95
|
+
#puts "Num Channels: #{num_channels}"
|
96
|
+
is_eof = false
|
97
|
+
if counter >= sample_size - frames_per_buffer
|
98
|
+
if counter < sample_size
|
99
|
+
buffer_size = sample_size.divmod(frames_per_buffer).last
|
100
|
+
#puts "Truncated buffer size: #{buffer_size}"
|
101
|
+
difference = frames_per_buffer - buffer_size
|
102
|
+
#puts "Adding #{difference} frames of null audio"
|
103
|
+
extra_data = [0] * difference * num_channels
|
104
|
+
is_eof = true
|
105
|
+
else
|
106
|
+
return :paAbort
|
107
|
+
end
|
108
|
+
end
|
109
|
+
buffer_size ||= frames_per_buffer
|
110
|
+
#puts "Size per buffer per channel: #{frames_per_buffer}"
|
111
|
+
offset = ((counter * num_channels) + Playback::METADATA.count) * Playback::FRAME_SIZE
|
112
|
+
#puts "Starting at location: #{offset}"
|
113
|
+
data = user_data.get_array_of_float32(offset, buffer_size * num_channels)
|
114
|
+
data += extra_data unless extra_data.nil?
|
115
|
+
#puts "This buffer size: #{data.size}"
|
116
|
+
#puts "Writing to output"
|
117
|
+
output.write_array_of_float(data)
|
118
|
+
counter += frames_per_buffer
|
119
|
+
user_data.put_float32(Playback::METADATA.index(:pointer) * Playback::FRAME_SIZE, counter.to_f) # update counter
|
120
|
+
if is_eof
|
121
|
+
#puts "Marking eof"
|
122
|
+
user_data.put_float32(Playback::METADATA.index(:is_eof) * Playback::FRAME_SIZE, 1.0) # mark eof
|
123
|
+
:paComplete
|
124
|
+
else
|
125
|
+
:paContinue
|
126
|
+
end
|
127
|
+
#puts "Exiting callback at #{Time.now.to_f}"
|
128
|
+
result
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module AudioPlayback
|
2
|
+
|
3
|
+
# An audio file
|
4
|
+
class File
|
5
|
+
|
6
|
+
attr_reader :num_channels, :path, :sample_rate, :size
|
7
|
+
|
8
|
+
# @param [::File, String] file_or_path
|
9
|
+
def initialize(file_or_path)
|
10
|
+
@path = file_or_path.kind_of?(::File) ? file_or_path.path : file_or_path
|
11
|
+
@file = RubyAudio::Sound.open(@path)
|
12
|
+
@size = ::File.size(@path)
|
13
|
+
@num_channels = @file.info.channels
|
14
|
+
@sample_rate = @file.info.samplerate
|
15
|
+
end
|
16
|
+
|
17
|
+
# @param [Hash] options
|
18
|
+
# @option options [IO] :logger
|
19
|
+
# @return [Array<Array<Float>>, Array<Float>] File data
|
20
|
+
def read(options = {})
|
21
|
+
if logger = options[:logger]
|
22
|
+
logger.puts("Reading audio file #{@path}")
|
23
|
+
end
|
24
|
+
buffer = RubyAudio::Buffer.float(@size, @num_channels)
|
25
|
+
begin
|
26
|
+
@file.seek(0)
|
27
|
+
@file.read(buffer, @size)
|
28
|
+
data = buffer.to_a
|
29
|
+
rescue RubyAudio::Error
|
30
|
+
end
|
31
|
+
logger.puts("Finished reading audio file #{@path}") if logger
|
32
|
+
data
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require "audio-playback/playback/frame"
|
2
|
+
require "audio-playback/playback/frame_set"
|
3
|
+
require "audio-playback/playback/stream_data"
|
4
|
+
|
5
|
+
module AudioPlayback
|
6
|
+
|
7
|
+
module Playback
|
8
|
+
|
9
|
+
DEFAULT = {
|
10
|
+
:buffer_size => 2**12
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
FRAME_SIZE = FFI::TYPE_FLOAT32.size
|
14
|
+
|
15
|
+
METADATA = [:size, :num_channels, :pointer, :is_eof].freeze
|
16
|
+
|
17
|
+
# Action of playing back an audio file
|
18
|
+
class Action
|
19
|
+
|
20
|
+
extend Forwardable
|
21
|
+
|
22
|
+
attr_reader :buffer_size, :channels, :data, :output, :num_channels, :sound, :stream
|
23
|
+
def_delegators :@sound, :audio_file, :sample_rate, :size
|
24
|
+
|
25
|
+
# @param [Sound] sound
|
26
|
+
# @param [Output] output
|
27
|
+
# @param [Hash] options
|
28
|
+
# @option options [Fixnum] :buffer_size
|
29
|
+
# @option options [Array<Fixnum>, Fixnum] :channels (or: :channel)
|
30
|
+
# @option options [IO] :logger
|
31
|
+
# @option options [Stream] :stream
|
32
|
+
def initialize(sound, output, options = {})
|
33
|
+
@sound = sound
|
34
|
+
@buffer_size = options[:buffer_size] || DEFAULT[:buffer_size]
|
35
|
+
@output = output
|
36
|
+
@stream = options[:stream] || Device::Stream.new(@output, options)
|
37
|
+
populate(options)
|
38
|
+
report(options[:logger]) if options[:logger]
|
39
|
+
end
|
40
|
+
|
41
|
+
# Start playback
|
42
|
+
# @return [Playback]
|
43
|
+
def start
|
44
|
+
@stream.play(self)
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
# Block process until playback finishes
|
49
|
+
# @return [Stream]
|
50
|
+
def block
|
51
|
+
@stream.block
|
52
|
+
end
|
53
|
+
|
54
|
+
# Log a report about playback
|
55
|
+
# @param [IO] logger
|
56
|
+
# @return [Boolean]
|
57
|
+
def report(logger)
|
58
|
+
logger.puts("Playback report for #{@sound.audio_file.path}")
|
59
|
+
logger.puts(" Number of channels: #{@num_channels}")
|
60
|
+
logger.puts(" Direct audio to channels #{@channels.to_s}") unless @channels.nil?
|
61
|
+
logger.puts(" Buffer size: #{@buffer_size}")
|
62
|
+
logger.puts(" Latency: #{@output.latency}")
|
63
|
+
true
|
64
|
+
end
|
65
|
+
|
66
|
+
# Total size of the playback's sound frames in bytes
|
67
|
+
# @return [Fixnum]
|
68
|
+
def data_size
|
69
|
+
frames = (@sound.size * @num_channels) + METADATA.count
|
70
|
+
frames * FRAME_SIZE.size
|
71
|
+
end
|
72
|
+
|
73
|
+
def channels_requested?
|
74
|
+
!@channels.nil?
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# Are the requested channels available in the current environment?
|
80
|
+
# @param [Array<Fixnum>] channels
|
81
|
+
# @return [Boolean]
|
82
|
+
def validate_requested_channels(channels)
|
83
|
+
if channels.count > @output.num_channels
|
84
|
+
raise "Only #{@output.num_channels} channels available on #{@output.name} output"
|
85
|
+
false
|
86
|
+
end
|
87
|
+
true
|
88
|
+
end
|
89
|
+
|
90
|
+
# Validate and populate the variables containing information about the requested channels
|
91
|
+
# @param [Fixnum, Array<Fixnum>] request Channel(s)
|
92
|
+
# @return [Boolean]
|
93
|
+
def populate_requested_channels(request)
|
94
|
+
request = Array(request)
|
95
|
+
requested_channels = request.map(&:to_i).uniq
|
96
|
+
if validate_requested_channels(requested_channels)
|
97
|
+
@num_channels = requested_channels.count
|
98
|
+
@channels = requested_channels
|
99
|
+
true
|
100
|
+
else
|
101
|
+
false
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Populate the playback channels
|
106
|
+
# @param [Hash] options
|
107
|
+
# @option options [Fixnum, Array<Fixnum>] :channels (or: :channel)
|
108
|
+
# @return [Boolean]
|
109
|
+
def populate_channels(options = {})
|
110
|
+
request = options[:channels] || options[:channel]
|
111
|
+
if request.nil?
|
112
|
+
@num_channels = @output.num_channels
|
113
|
+
true
|
114
|
+
else
|
115
|
+
populate_requested_channels(request)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Populate the playback action
|
120
|
+
# @param [Hash] options
|
121
|
+
# @option options [Fixnum, Array<Fixnum>] :channels (or: :channel)
|
122
|
+
# @return [Playback::Action]
|
123
|
+
def populate(options = {})
|
124
|
+
populate_channels(options)
|
125
|
+
@data = StreamData.to_pointer(self)
|
126
|
+
self
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
# Shortcut to Action.new
|
132
|
+
# @return [Playback::Action]
|
133
|
+
def self.new(*args)
|
134
|
+
Action.new(*args)
|
135
|
+
end
|
136
|
+
|
137
|
+
# @param [Sound] sound
|
138
|
+
# @param [Output] output
|
139
|
+
# @param [Hash] options
|
140
|
+
# @option options [Fixnum] :buffer_size
|
141
|
+
# @option options [Array<Fixnum>, Fixnum] :channels (or: :channel)
|
142
|
+
# @option options [IO] :logger
|
143
|
+
# @option options [Stream] :stream
|
144
|
+
# @return [Playback]
|
145
|
+
def self.play(sound, output, options = {})
|
146
|
+
playback = Action.new(sound, output, options)
|
147
|
+
playback.start
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module AudioPlayback
|
2
|
+
|
3
|
+
module Playback
|
4
|
+
|
5
|
+
# A single frame of audio data in the FrameSet
|
6
|
+
class Frame
|
7
|
+
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
attr_reader :frame
|
11
|
+
def_delegators :@frame, :[], :all?, :any?, :count, :each, :flatten, :map, :size, :to_ary
|
12
|
+
|
13
|
+
# @param [Array<Float>, Frame] frame
|
14
|
+
def initialize(frame)
|
15
|
+
@frame = frame.frame if frame.kind_of?(Frame)
|
16
|
+
@frame ||= frame
|
17
|
+
end
|
18
|
+
|
19
|
+
# Truncate the frame to the given size
|
20
|
+
# @param [Fixnum] num
|
21
|
+
# @return [Frame]
|
22
|
+
def truncate(num)
|
23
|
+
@frame.slice!(num..-1)
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
# Fill up the given number of channels at the end of the frame with duplicate data from the last
|
28
|
+
# existing channel
|
29
|
+
# @param [Fixnum] num
|
30
|
+
# @param [Hash] options
|
31
|
+
# @option options [Array<Fixnum>] :channels (required if :num_channels is provided)
|
32
|
+
# @option options [Fixnum] :num_channels (required if :channels is provided)
|
33
|
+
# @return [Boolean]
|
34
|
+
def fill(num, options = {})
|
35
|
+
if (channels = options[:channels]).nil?
|
36
|
+
@frame.fill(@frame.last, @frame.size, num)
|
37
|
+
else
|
38
|
+
fill_for_channels(options[:num_channels], channels)
|
39
|
+
end
|
40
|
+
true
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# Zero out the given number of channels in the frame starting with the given index
|
46
|
+
# @param [Fixnum] index
|
47
|
+
# @param [Fixnum] num_channels
|
48
|
+
# @return [Frame]
|
49
|
+
def silence_channels(index, num_channels)
|
50
|
+
@frame.fill(0, index, num_channels)
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
# Fill the entire frame for the given channels
|
55
|
+
# @param [Fixnum] num_channels
|
56
|
+
# @param [Array<Fixnum>] channels
|
57
|
+
# @return [Boolean]
|
58
|
+
def fill_for_channels(num_channels, channels)
|
59
|
+
values = @frame.dup
|
60
|
+
silence_channels(0, num_channels)
|
61
|
+
channels.each do |channel|
|
62
|
+
value = values[channel] || values.first
|
63
|
+
@frame[channel] = value
|
64
|
+
end
|
65
|
+
true
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module AudioPlayback
|
2
|
+
|
3
|
+
module Playback
|
4
|
+
|
5
|
+
# Container for playback data
|
6
|
+
class FrameSet
|
7
|
+
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
def_delegators :@data, :flatten, :slice, :to_ary, :unshift
|
11
|
+
|
12
|
+
# @param [Playback::Action] playback
|
13
|
+
def initialize(playback)
|
14
|
+
populate(playback)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
# Populate the Container
|
20
|
+
# @param [Playback::Action] playback
|
21
|
+
# @return [Array<Array<Float>>]
|
22
|
+
def populate(playback)
|
23
|
+
data = playback.sound.data.dup
|
24
|
+
data = ensure_array_frames(data)
|
25
|
+
data = to_frame_objects(data)
|
26
|
+
|
27
|
+
@data = if channels_match?(playback)
|
28
|
+
data
|
29
|
+
else
|
30
|
+
build_channels(data, playback)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Does the channel structure of the playback action match the channel structure of the sound?
|
35
|
+
# @param [Playback::Action] playback
|
36
|
+
# @return [Boolean]
|
37
|
+
def channels_match?(playback)
|
38
|
+
playback.sound.num_channels == playback.num_channels && playback.channels.nil?
|
39
|
+
end
|
40
|
+
|
41
|
+
# (Re-)build the channel structure of the frame set
|
42
|
+
# @param [Array<Frame>] data
|
43
|
+
# @param [Playback::Action] playback
|
44
|
+
# @return [Array<Frame>]
|
45
|
+
def build_channels(data, playback)
|
46
|
+
ensure_num_channels(data, playback.num_channels)
|
47
|
+
|
48
|
+
if playback.channels_requested?
|
49
|
+
ensure_requested_channels(data, playback)
|
50
|
+
else
|
51
|
+
ensure_output_channels(data, playback)
|
52
|
+
end
|
53
|
+
data
|
54
|
+
end
|
55
|
+
|
56
|
+
# Build the channel structure of the frame set to what was requested of playback
|
57
|
+
# @param [Array<Frame>] data
|
58
|
+
# @param [Playback::Action] playback
|
59
|
+
# @return [Array<Frame>]
|
60
|
+
def ensure_requested_channels(data, playback)
|
61
|
+
ensure_num_channels(data, playback.output.num_channels, :channels => playback.channels)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Build the channel structure of the frameset to that of the playback output device
|
65
|
+
# @param [Array<Frame>] data
|
66
|
+
# @param [Playback::Action] playback
|
67
|
+
# @return [Array<Frame>]
|
68
|
+
def ensure_output_channels(data, playback)
|
69
|
+
if playback.num_channels != playback.output.num_channels
|
70
|
+
ensure_num_channels(data, playback.output.num_channels)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Ensure that the channel structure of the frameset is according to the given number of channels
|
75
|
+
# and to the given particular channels when provided
|
76
|
+
# @param [Array<Frame>] data
|
77
|
+
# @param [Fixnum] num_channels
|
78
|
+
# @param [Hash] options
|
79
|
+
# @option options [Array<Fixnum>] :channels
|
80
|
+
# @return [Array<Frame>]
|
81
|
+
def ensure_num_channels(data, num_channels, options = {})
|
82
|
+
data.each do |frame|
|
83
|
+
difference = num_channels - frame.size
|
84
|
+
if difference > 0
|
85
|
+
frame.fill(difference, :channels => options[:channels], :num_channels => num_channels)
|
86
|
+
else
|
87
|
+
frame.truncate(num_channels)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
data
|
91
|
+
end
|
92
|
+
|
93
|
+
# Ensure that the input data is Array<Array<Float>>. Single channel audio will be provided as
|
94
|
+
# Array<Float> and is converted here so that the frame set data structure can be built in a
|
95
|
+
# uniform way
|
96
|
+
# @param [Array<Float>, Array<Array<Float>>] data
|
97
|
+
# @return [Array<Array<Float>>]
|
98
|
+
def ensure_array_frames(data)
|
99
|
+
if data.sample.kind_of?(Array)
|
100
|
+
data
|
101
|
+
else
|
102
|
+
data.map { |frame| Array(frame) }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Convert the raw sound data to Frame objects
|
107
|
+
# @param [Array<Array<Float>>] data
|
108
|
+
# @return [Array<Frame>]
|
109
|
+
def to_frame_objects(data)
|
110
|
+
data.map { |frame| Frame.new(frame) }
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|