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