├── .gitignore ├── examples ├── one_second_sine.rb ├── record_microphone_delay.rb ├── increase_frequency.rb ├── shepard.rb └── song.rb ├── easy_audio.gemspec ├── Rakefile ├── samus.json ├── LICENSE ├── README.md └── lib └── easy_audio.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .yardoc 2 | doc 3 | *.gem 4 | -------------------------------------------------------------------------------- /examples/one_second_sine.rb: -------------------------------------------------------------------------------- 1 | # Play a one second sine wave at 440hz (A note) 2 | require_relative '../lib/easy_audio' 3 | 4 | EasyAudio.easy_open(&EasyAudio::Waveforms::SINE) 5 | sleep 1 6 | -------------------------------------------------------------------------------- /examples/record_microphone_delay.rb: -------------------------------------------------------------------------------- 1 | # Record data from the microphone and 2 | # play it back on output with a 1 second delay 3 | require_relative '../lib/easy_audio' 4 | 5 | EasyAudio.easy_open(in: true, out: true, latency: 1.0) { current_sample } 6 | sleep 10 # for 10 seconds 7 | -------------------------------------------------------------------------------- /examples/increase_frequency.rb: -------------------------------------------------------------------------------- 1 | # A triangle wave that increases in frequency over 3 seconds 2 | require_relative '../lib/easy_audio' 3 | 4 | stream = EasyAudio.easy_open(freq: 220, &EasyAudio::Waveforms::TRIANGLE) 5 | 6 | Thread.new { loop { stream.frequency += 50; sleep 0.2 } } 7 | sleep 3 8 | -------------------------------------------------------------------------------- /examples/shepard.rb: -------------------------------------------------------------------------------- 1 | require_relative '../lib/easy_audio' 2 | 3 | def render(freq: 220.0, time: 1.0, sample_rate: 44100) 4 | (sample_rate * time).to_i.times.map do |n| 5 | 4.times.to_a.map do |i| 6 | 12.times.to_a.map do |j| 7 | fij = freq * 2 ** i * 2 ** (j / 12.0) 8 | a = Math.exp(-(Math.log2(fij / freq) ** 2) / 0.5) 9 | a * Math.sin(Math::PI * 2 * fij * n / sample_rate) 10 | end.to_a.reduce(&:+) 11 | end.to_a.reduce(&:+) 12 | end 13 | end 14 | 15 | puts "Rendering..." 16 | buffer = render(time: 4) 17 | puts "Playing..." 18 | EasyAudio.easy_open { buffer.shift } 19 | sleep 4 20 | -------------------------------------------------------------------------------- /easy_audio.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = "easy_audio" 3 | s.summary = "EasyAudio is a simplified wrapper for the portaudio library." 4 | s.description = "EasyAudio allows you to play or record from your sound card." 5 | s.version = File.read("lib/easy_audio.rb")[/VERSION = "(.+?)"/, 1] 6 | s.author = "Loren Segal" 7 | s.email = "lsegal@soen.ca" 8 | s.homepage = "http://github.com/lsegal/easy_audio" 9 | s.platform = Gem::Platform::RUBY 10 | s.files = `git ls-files`.split(/\s+/) 11 | s.extensions = ["Rakefile"] 12 | s.license = "BSD" 13 | 14 | s.add_runtime_dependency "ffi-portaudio", "~> 0.0" 15 | end 16 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | task :default => :install 2 | 3 | task :install do 4 | have_portaudio = false 5 | print "Checking for portaudio..." 6 | begin 7 | require "ffi-portaudio" 8 | have_portaudio = true 9 | puts " yes." 10 | rescue LoadError 11 | puts "no." 12 | end 13 | 14 | if !have_portaudio 15 | success = false 16 | puts "Portaudio is missing, attempting to install..." 17 | 18 | if RbConfig::CONFIG['host_os'].match(/darwin/) 19 | puts "Detected Mac OS X, installing with Homebrew..." 20 | begin 21 | sh "brew install portaudio" 22 | success = true if $? == 0 23 | rescue 24 | puts "Could not install portaudio. Do you have Homebrew installed?" 25 | end 26 | else 27 | puts "Only OS X installation currently supported. Install portaudio " + 28 | "from http://portaudio.com and reinstall." 29 | end 30 | 31 | if success 32 | puts "Installed portaudio. Continuing installation of easy_audio..." 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /samus.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [ 3 | { 4 | "action": "fs-sedfiles", 5 | "files": ["lib/easy_audio.rb"], 6 | "arguments": { 7 | "search": "VERSION = ['\"](.+?)['\"]", 8 | "replace": "VERSION = \"$version\"" 9 | } 10 | }, 11 | { 12 | "action": "git-commit", 13 | "files": ["lib/easy_audio.rb"] 14 | }, 15 | { 16 | "action": "git-merge", 17 | "arguments": { 18 | "branch": "master" 19 | } 20 | }, 21 | { 22 | "action": "archive-git-full", 23 | "files": ["git.tgz"], 24 | "publish": [{ 25 | "action": "git-push", 26 | "arguments": { 27 | "remotes": "origin", 28 | "refs": "master v$version" 29 | } 30 | }] 31 | }, 32 | { 33 | "action": "gem-build", 34 | "files": ["*.gemspec"], 35 | "publish": [ 36 | { 37 | "action": "gem-push", 38 | "files": ["*.gem"], 39 | "credentials": "lsegal.rubygems" 40 | } 41 | ] 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Loren Segal 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the copyright holder nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL LOREN SEGAL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EasyAudio 2 | 3 | [![Gem Version](https://badge.fury.io/rb/easy_audio.svg)](http://badge.fury.io/rb/easy_audio) 4 | 5 | EasyAudio is a simplified wrapper for the [portaudio][portaudio] library, which 6 | allows to you play or record audio directly from your sound card. 7 | 8 | ## Installing 9 | 10 | ```sh 11 | $ gem install easy_audio 12 | ``` 13 | 14 | Note: if you are on a Linux or Windows machine you will need to manually 15 | install portaudio to a location in your library paths. The gem will attempt 16 | to install this automatically on OS X through [Homebrew][brew]. 17 | 18 | ## Usage 19 | 20 | Here's how you can easily play a sine wave at 440hz: 21 | 22 | ```ruby 23 | require 'easy_audio' 24 | 25 | EasyAudio.easy_open(&EasyAudio::Waveforms::SINE) 26 | sleep 2 # play for 2 seconds 27 | ``` 28 | 29 | Play a custom waveform (a cosine wave): 30 | 31 | ```ruby 32 | require 'easy_audio' 33 | 34 | EasyAudio.easy_open { Math.cos(Math::PI * 2 * step) } 35 | sleep 1 36 | ``` 37 | 38 | Here's a triangle wave that increases its frequency over 3 seconds: 39 | 40 | ```ruby 41 | require 'easy_audio' 42 | 43 | stream = EasyAudio.easy_open(freq: 220, &EasyAudio::Waveforms::TRIANGLE) 44 | Thread.new { loop { stream.frequency += 50; sleep 0.2 } } 45 | sleep 3 46 | ``` 47 | 48 | Record audio from your microphone and play it back a second later: 49 | 50 | ```ruby 51 | require 'easy_audio' 52 | 53 | EasyAudio.easy_open(in: true, out: true, latency: 1.0) { current_sample } 54 | sleep 10 # for 10 seconds 55 | ``` 56 | 57 | ## Documentation 58 | 59 | See the API documentation on [rubydoc.info][docs]. 60 | 61 | ## License 62 | 63 | EasyAudio is copyright © 2014 by Loren Segal and licensed under the BSD 64 | license. See the LICENSE file for more information. 65 | 66 | [portaudio]: http://portaudio.com 67 | [brew]: http://brew.sh 68 | [docs]: http://rubydoc.info/gems/easy_audio/frames 69 | -------------------------------------------------------------------------------- /examples/song.rb: -------------------------------------------------------------------------------- 1 | require_relative '../lib/easy_audio' 2 | 3 | def freq_for_note(note) 4 | 2.0 ** ((note-49.0)/12.0) * 440.0 5 | end 6 | 7 | class Sound 8 | def initialize(freq: 1, &block) 9 | @freq = freq 10 | @fn = block 11 | @frame = 0 12 | @step = 0 13 | end 14 | 15 | attr_accessor :frame, :step, :freq 16 | 17 | def next_frame(frame, step) 18 | @frame, @step = frame, step 19 | calculate_step 20 | instance_exec(&@fn) 21 | end 22 | 23 | def calculate_step 24 | @step = (step * @freq.to_f) % 1.0 25 | end 26 | 27 | def e(fn = nil, &block) 28 | instance_exec(&(fn || block)) 29 | end 30 | 31 | def f(fn = nil, note, &block) 32 | orig_freq, orig_step = @freq, @step 33 | @freq = freq_for_note(note) 34 | calculate_step 35 | result = e(fn || block) 36 | @freq = orig_freq 37 | @step = orig_step 38 | result 39 | end 40 | 41 | def fr(fn = nil, freq, &block) 42 | orig_freq, orig_step = @freq, @step 43 | @freq = freq 44 | calculate_step 45 | result = e(fn || block) 46 | @freq = orig_freq 47 | @step = orig_step 48 | result 49 | end 50 | end 51 | 52 | class Sequencer 53 | def initialize(stream: EasyAudio::EasyStream.new(amp: 0.8, frame_size: 4096, latency: 12.0), bpm: 120) 54 | srand 55 | @stream = stream 56 | @stream.fn = method(:next_frame) 57 | @scene = nil 58 | @scenes = {} 59 | @rendered_scenes = {} 60 | @keyframes = {} 61 | @tracks = [] 62 | @bpm = bpm.to_f 63 | @sample = 0 64 | @kf = 0 65 | @samples_per_bar = (@stream.sample_rate * 60) / @bpm 66 | end 67 | 68 | def next_frame 69 | if @keyframes[@kf.to_i] 70 | @scene = @keyframes[@kf.to_i] 71 | end 72 | @kf += 1 73 | 74 | if @rendered_scenes[@scene] && @rendered_scenes[@scene][@sample.to_i] 75 | result = @rendered_scenes[@scene][@sample.to_i] 76 | else 77 | result = calculate_frame(@scene, @sample.to_i) 78 | end 79 | 80 | @sample = (@sample + 1) % @samples_per_bar.to_i 81 | result 82 | end 83 | 84 | def add_scene(name = nil, tracks) 85 | @scenes[name.to_s || 'default'] = tracks.map do |track| 86 | track.map {|t| Sound === t ? t : t ? Sound.new(&t) : nil } 87 | end 88 | end 89 | 90 | def calculate_frame(name, i) 91 | @rendered_scenes[name] ||= {} 92 | total = 0.0 93 | @scenes[name].each do |track| 94 | q = @samples_per_bar.to_i / track.length 95 | n = (i / q).to_i 96 | step = (i.to_f / @stream.sample_rate) % 1.0 97 | total += track[n] ? track[n].next_frame(i % q, step) : 0.0 98 | end 99 | @rendered_scenes[name][i] = total 100 | end 101 | 102 | def render_scenes 103 | @rendered_scenes = {} 104 | @keyframes.values.uniq.each do |name| 105 | reader, writer = IO.pipe 106 | fork do 107 | reader.close 108 | data = @samples_per_bar.to_i.times.map do |i| 109 | calculate_frame(name, i) 110 | end 111 | writer.puts(Marshal.dump(data)) 112 | end 113 | 114 | writer.close 115 | data = Marshal.load(reader.read) 116 | @rendered_scenes[name] = data 117 | end 118 | end 119 | 120 | def play(scenes: ['16:default']) 121 | @sample = 0 122 | @kf = 0 123 | @keyframes = {} 124 | 125 | last_frame = 0 126 | scenes.each do |scene| 127 | bars, name = *scene.split(':') 128 | @keyframes[last_frame] = name.to_s 129 | last_frame += (bars.to_i * @samples_per_bar).to_i 130 | end 131 | 132 | puts "Rendering scenes in background..." 133 | Thread.new { render_scenes } 134 | 135 | puts "Starting audio..." 136 | @stream.start 137 | sleep(last_frame.to_f / @stream.sample_rate) 138 | sleep 0.1 while @kf < last_frame 139 | end 140 | end 141 | 142 | def sn(fn, note) 143 | Sound.new(freq: freq_for_note(note), &fn) 144 | end 145 | 146 | # Instruments 147 | 148 | SINE = -> { Math.sin(2 * Math::PI * step) * 0.8 } 149 | SQUARE = -> { step < 0.5 ? -0.8 : 0.8 } 150 | TRIANGLE = -> { (1 - 4 * (step.round - step).abs) * 0.55 } 151 | SAW = -> { 2 * (step - step.round) * 0.8 } 152 | 153 | NOISE = -> { rand - 0.5 } 154 | 155 | EXP_FALLOFF = -> { [(1 / (frame * 0.002)), 1.0].min } 156 | EXP_FALLOFF2 = -> { [(1 / (frame * 0.005)), 1.0].min } 157 | LIN_FALLOFF = -> { (50000.0 - @frame) / 50000.0 } 158 | 159 | SNARE = -> { e(EXP_FALLOFF) * e(NOISE) * 0.8 } 160 | BASSDRUM = -> { [1.0,e(EXP_FALLOFF2) * e(NOISE) * 0.1 + e(SINE) * 2 * e(EXP_FALLOFF)].min } 161 | HIHAT = -> { e(NOISE) * 0.3 * e(EXP_FALLOFF2) + e(SQUARE) * 0.1 * e(EXP_FALLOFF2) } 162 | 163 | LEAD = -> { e(TRIANGLE) * e(EXP_FALLOFF) } 164 | LEAD2 = -> { e(SAW) * Math.sin(step * 4.0) * 0.2 * fr(SINE,2*freq*Math.tan(step*0.005)) * 0.4 + e(NOISE) * 0.05 } 165 | SQUARELEAD = -> { e(SQUARE) * 0.4 * e(EXP_FALLOFF) } 166 | SQUARELEAD2 = -> { e(SQUARE) * 0.4 * e(EXP_FALLOFF2) } 167 | PHASED = -> { fr(SINE, Math.sin(@step / 4.0) * (freq / 2.0)) * 0.01 * [1.0,frame.to_f/50000.0].max } 168 | 169 | # Sequencer and scenes 170 | 171 | s = Sequencer.new bpm: 43 172 | s.add_scene :A, [ 173 | [nil, nil, SNARE, nil], 174 | [sn(BASSDRUM, 20), nil, nil, nil], 175 | [nil, sn(SQUARELEAD, 46)] * 4, 176 | [sn(LEAD, 51), nil, nil, sn(TRIANGLE,49)], 177 | [nil, sn(SINE,20), nil, nil] * 2, 178 | ] 179 | s.add_scene :A2, [ 180 | [nil, nil, SNARE, nil], 181 | [sn(BASSDRUM, 20), nil, nil, nil], 182 | [nil, sn(SQUARELEAD, 46)] * 4, 183 | [sn(LEAD, 51), nil, nil, sn(TRIANGLE,54)], 184 | [nil, sn(SINE,26), nil, nil] * 2, 185 | ] 186 | s.add_scene :B, [ 187 | [sn(HIHAT, 70), nil, nil, nil, sn(HIHAT, 70), nil, sn(HIHAT, 70), nil] * 2, 188 | [nil, nil, SNARE, nil], 189 | [sn(BASSDRUM, 20), nil, nil, nil, SNARE, nil, sn(BASSDRUM, 20), nil], 190 | [nil, sn(SQUARELEAD, 42)] * 4, 191 | [sn(LEAD, 51), nil, nil, sn(TRIANGLE,49)], 192 | [nil, sn(SINE,23), nil, nil] * 2, 193 | ] 194 | s.add_scene :C, [ 195 | [sn(HIHAT, 70), nil, nil, nil, sn(HIHAT, 70), nil, sn(HIHAT, 70), nil] * 2, 196 | [nil, nil, nil, nil, nil, SNARE, nil, nil, nil, nil], 197 | [nil, sn(SQUARELEAD, 49), nil, sn(SQUARELEAD, 49), nil, nil, nil, sn(-> { e(TRIANGLE) * 0.5 }, 54)], 198 | [sn(BASSDRUM, 20), nil, nil, nil, nil, nil, sn(BASSDRUM, 20), nil], 199 | [sn(LEAD, 46), sn(LEAD, 46), nil, nil], 200 | [nil, nil, nil, nil, nil, nil, nil, sn(TRIANGLE,42)], 201 | [nil, nil, nil, nil, sn(LEAD2,59), sn(LEAD2,59), nil, nil], 202 | [sn(SINE,18)], 203 | [sn(PHASED,20)] 204 | ] 205 | s.add_scene :D, [ 206 | [nil, sn(SQUARELEAD, 46)] * 4, 207 | [sn(LEAD, 51), nil, nil, sn(TRIANGLE,49)], 208 | ] 209 | s.add_scene :D2, [ 210 | [nil, sn(SQUARELEAD, 46)] * 4, 211 | [sn(TRIANGLE, 51), nil, nil, sn(TRIANGLE,46)], 212 | ] 213 | s.add_scene :D3, [ 214 | [nil, sn(SQUARELEAD, 46)] * 4, 215 | [sn(TRIANGLE, 42), nil, nil, nil], 216 | ] 217 | s.add_scene :D4, [ 218 | [nil, sn(SQUARELEAD, 46)] * 4, 219 | ] 220 | 221 | s.add_scene :D5, [ 222 | [nil, sn(-> { e(SQUARELEAD) * e(EXP_FALLOFF2) }, 46)] * 4, 223 | [sn(-> { e(SQUARELEAD) * e(EXP_FALLOFF2) }, 46), nil, nil, nil, nil, nil, nil, nil] 224 | ] 225 | 226 | # Play! 227 | s.play scenes: %w( 228 | 2:D5 229 | 1:A 1:A2 2:B 4:C 230 | 1:A 1:A2 2:B 4:C 231 | 1:D 1:D2 1:D3 1:D4 232 | ) 233 | -------------------------------------------------------------------------------- /lib/easy_audio.rb: -------------------------------------------------------------------------------- 1 | require 'ffi-portaudio' 2 | 3 | # Easy Audio is a library to simplify the Portaudio interface 4 | # @see http://portaudio.com 5 | module EasyAudio 6 | VERSION = "0.1.0" 7 | 8 | # Represents a single buffer passed to the {Stream} process block 9 | class StreamPacket < Struct.new(:samples, :num_samples, :time_info, :status_info, :user_data) 10 | end 11 | 12 | # Represents a single audio input/output stream. See {#initialize} for usage 13 | # examples. 14 | class Stream < FFI::PortAudio::Stream 15 | include FFI::PortAudio 16 | 17 | # @!method start 18 | # Starts processing the stream. 19 | 20 | # @!method stop 21 | # Stops processing the stream. 22 | 23 | # Creates a new stream for processing audio. Call {#start} to start 24 | # processing. 25 | # 26 | # @option opts :sample_rate [Fixnum] (44100) the sample rate to play at. 27 | # @option opts :frame_size [Fixnum] (256) the number of frames per buffer. 28 | # @option opts :total_secs [Float] (nil) total number of frames to play. 29 | # @option opts :in [Boolean] whether to use the default input device. 30 | # @option opts :out [Boolean] whether to use the default output device. 31 | # @option opts :in_chans [Fixnum] (2) the number of channels to process from 32 | # the input device. 33 | # @option opts :out_chans [Fixnum] (2) the number of channels to process 34 | # from the output device. 35 | # @option opts :latency [Float] (0.0) the default latency for processing. 36 | # @yield [buffer] runs the provided block against the sample buffer data 37 | # @yieldparam buffer [StreamPacket] the sample data to process 38 | # @yieldreturn [Array] return an array of interlaced floating points 39 | # for each channel in {#output_channels}. 40 | # @example Process audio from input (microphone) and playback on output 41 | # EasyAudio::Stream.new(in: true, out: true) do |buffer| 42 | # buffer.samples # echos the stream to output 43 | # end 44 | # @see #start 45 | def initialize(opts = {}, &block) 46 | pa_start 47 | 48 | @fn = block 49 | @sample_rate = opts[:sample_rate] || 44100 50 | @frame_size = opts[:frame_size] || 256 51 | @input_channels = opts[:in_chans] || 1 52 | @output_channels = opts[:out_chans] || 1 53 | @latency = opts[:latency] || 0.01 54 | @total_frames = opts[:total_secs] ? 55 | (opts[:total_secs].to_f * @sample_rate).floor : nil 56 | @total_num_frames = 0 57 | 58 | input, output = nil, nil 59 | if opts[:in] || opts[:in_chans] 60 | device = API.Pa_GetDefaultInputDevice 61 | input = stream_for(device, @input_channels, @latency) 62 | end 63 | 64 | if opts[:out] || opts[:out_chans] || !input 65 | device = API.Pa_GetDefaultOutputDevice 66 | output = stream_for(device, @output_channels, @latency) 67 | end 68 | 69 | open(input, output, @sample_rate, @frame_size) 70 | end 71 | 72 | attr_accessor :fn, :sample_rate, :frame_size 73 | attr_reader :input_channels, :output_channels, :latency 74 | 75 | private 76 | 77 | # Don't override this function. Pass in a `process` Proc object to 78 | # {#initialize} instead. 79 | def process(input, output, frames, time_info, status, user_data) 80 | if @total_frames && @total_num_frames > @total_frames 81 | return :paAbort 82 | else 83 | @total_num_frames += frames 84 | end 85 | 86 | result = run_process(input, output, frames, time_info, status, user_data) 87 | return result if Symbol === result 88 | unless Array === result 89 | result = Array.new(frames * @output_channels).map {0} 90 | end 91 | 92 | output.write_array_of_float(result) 93 | :paContinue 94 | rescue => e 95 | puts e.message + "\n " + e.backtrace.join("\n ") 96 | :paAbort 97 | end 98 | 99 | def run_process(input, output, frames, time_info, status, user_data) 100 | inbuf = nil 101 | if input.address != 0 102 | inbuf = input.read_array_of_float(frames * @input_channels) 103 | end 104 | 105 | buffer = StreamPacket.new(inbuf, frames, time_info, status, user_data) 106 | @fn ? @fn.call(buffer) : nil 107 | end 108 | 109 | def stream_for(device, channels, latency) 110 | API::PaStreamParameters.new.tap do |stream| 111 | info = API.Pa_GetDeviceInfo(device) 112 | stream[:device] = device 113 | stream[:suggestedLatency] = latency 114 | stream[:hostApiSpecificStreamInfo] = nil 115 | stream[:channelCount] = channels 116 | stream[:sampleFormat] = API::Float32 117 | end 118 | end 119 | 120 | def pa_start 121 | return if @@stream_started 122 | API.Pa_Initialize 123 | at_exit { API.Pa_Terminate } 124 | @@stream_started = true 125 | end 126 | 127 | @@stream_started = false 128 | end 129 | 130 | # A simplified {Stream} class whose processor block only processes a single 131 | # frame at a time. See {Waveforms} for a set of pre-fabricated EasyStream 132 | # processor blocks for examples of how to process a single stream. 133 | # 134 | # Note that instead of passing state information as an argument to the block, 135 | # state is instead stored in the class itself, and the block is instance 136 | # evaluated. This makes it a bit slower to process, but much more convenient 137 | # for creating blocks. 138 | # 139 | # See {#initialize} for usage examples. 140 | class EasyStream < Stream 141 | 142 | # {include:Stream#initialize} 143 | # 144 | # @option (see Stream#initialize) 145 | # @option opts :freq [Float] (440.0) the frequency to generate {#step} 146 | # values at. 147 | # @option opts :amp [Float] (1.0) the amplitude to scale values to. 148 | # @yield a process block that processes one frame at a time. 149 | # @yieldreturn [Array] return an array of interlaced floating points 150 | # for each channel in {#output_channels}. 151 | # @example Process audio from input (microphone) and playback on output 152 | # EasyAudio::EasyStream.new(in: true, out: true) { current_sample }.start 153 | # @example Play a sine wave. 154 | # EasyAudio::EasyStream.new(&EasyAudio::Waveforms::SINE).start 155 | # @example Play a square wave. 156 | # EasyAudio::EasyStream.new(&EasyAudio::Waveforms::SQUARE).start 157 | def initialize(opts = {}, &block) 158 | @frequency = opts[:freq] || 440.0 159 | @amp = opts[:amp] || 1.0 160 | @frame = 0 161 | @channel = 0 162 | 163 | super(opts, &block) 164 | end 165 | 166 | attr_accessor :amp, :frequency, :frame 167 | attr_reader :step, :channel, :samples, :num_frames 168 | attr_reader :time_info, :status_info, :user_data, :i, :current_sample 169 | 170 | private 171 | 172 | def run_process(input, output, frames, time_info, status, user_data) 173 | @samples = nil 174 | if input.address != 0 175 | @samples = input.read_array_of_float(frames * @input_channels) 176 | end 177 | 178 | @current_sample = nil 179 | @num_frames = frames 180 | @time_info = time_info 181 | @status_info = status 182 | @user_data = user_data 183 | 184 | result = Array.new(frames * @output_channels) 185 | if @fn 186 | @i = 0 187 | frames.times do 188 | @step = @frame * (@frequency / @sample_rate.to_f) % 1.0 189 | @output_channels.times do |ch| 190 | @channel = ch 191 | @current_sample = @samples[@i] if @samples 192 | result[@i] = @amp.to_f * (instance_exec(&@fn) || 0.0) 193 | @i += 1 194 | end 195 | @frame += 1 196 | end 197 | else 198 | result = result.map {0} 199 | end 200 | 201 | result 202 | end 203 | 204 | def e(fn = nil, &block) 205 | instance_exec(&(fn || block)) 206 | end 207 | 208 | def fr(fn = nil, freq, &block) 209 | orig_freq, orig_step = @frequency, @step 210 | @frequency = freq 211 | @step = @frame * (@frequency / @sample_rate.to_f) % 1.0 212 | result = e(fn || block) 213 | @frequency = orig_freq 214 | @step = orig_step 215 | result 216 | end 217 | end 218 | 219 | module_function 220 | 221 | # Quickly opens a {Stream} and calls {Stream#start}. 222 | # 223 | # @option (see Stream#initialize) 224 | # @yield (see Stream#initialize) 225 | # @yieldparam (see Stream#initialize) 226 | # @yieldreturn (see Stream#initialize) 227 | def open(opts = {}, &block) 228 | Stream.new(opts, &block).tap {|s| s.start } 229 | end 230 | 231 | # Quickly opens an {EasyStream} and calls {Stream#start}. 232 | # 233 | # @option (see EasyStream#initialize) 234 | # @yield (see EasyStream#initialize) 235 | # @yieldreturn (see EasyStream#initialize) 236 | # @example Process audio from input (microphone) and playback on output 237 | # EasyAudio.easy_open(in: true, out: true) { current_sample } 238 | # @example Play a sine wave. 239 | # EasyAudio.easy_open(&EasyAudio::Waveforms::SINE) 240 | # @example Play a square wave. 241 | # EasyAudio.easy_open(&EasyAudio::Waveforms::SQUARE) 242 | # @see EasyStream#initialize 243 | def easy_open(opts = {}, &block) 244 | EasyStream.new(opts, &block).tap {|s| s.start } 245 | end 246 | 247 | # A collection of pre-fabricated waveforms that can be plugged into 248 | # {EasyStream} or {easy_open}. 249 | module Waveforms 250 | # Generates a sine wave 251 | SINE = -> { Math.sin(2 * Math::PI * step) } 252 | 253 | # Generates a square wave 254 | SQUARE = -> { step < 0.5 ? -1 : 1 } 255 | 256 | # Generates a triangle wave 257 | TRIANGLE = -> { 1 - 4 * (step.round - step).abs } 258 | 259 | # Generates a sawtooth wave 260 | SAW = -> { 2 * (step - step.round) } 261 | end 262 | end 263 | --------------------------------------------------------------------------------