audio-playback 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|