audio-playback 0.0.2

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