class PulseAnalysis::Analysis
Constants
- MAX_BPM
- MINIMUM_PULSES
Attributes
Public Class Methods
@param [PulseAnalysis::Sound] sound Sound
to analyze @param [Hash] options @option options [Float] :amplitude_threshold Pulses above this amplitude will be analyzed @option options [Integer] :length_threshold Pulse periods longer than this value will be analyzed
# File lib/pulse-analysis/analysis.rb, line 14 def initialize(sound, options = {}) @amplitude_threshold = options[:amplitude_threshold] @length_threshold = options[:length_threshold] populate_sound(sound) @data = AudioData.new(@sound) end
Public Instance Methods
The threshold (0..1) at which pulses will register as high @return [Float]
# File lib/pulse-analysis/analysis.rb, line 82 def amplitude_threshold @amplitude_threshold ||= calculate_amplitude_threshold end
Average sequential abberation between pulses @return [Float]
# File lib/pulse-analysis/analysis.rb, line 70 def average_abberation @average_abberation ||= calculate_average_abberation end
Average number of samples between pulses @return [Float]
# File lib/pulse-analysis/analysis.rb, line 32 def average_period @average_period ||= @periods.inject(&:+).to_f / num_pulses end
Largest sequential abberation between pulses @return [Integer]
# File lib/pulse-analysis/analysis.rb, line 64 def largest_abberation @largest_abberation ||= abberations.max || 0 end
Longest number of samples between pulse @return [Integer]
# File lib/pulse-analysis/analysis.rb, line 44 def longest_period @longest_period ||= @periods.max end
Number of usable pulses in the audio file @return [Integer]
# File lib/pulse-analysis/analysis.rb, line 38 def num_pulses @num_pulses ||= @periods.count end
Run the analysis @return [Boolean]
# File lib/pulse-analysis/analysis.rb, line 23 def run prepare populate_periods validate true end
Shortest number of samples between pulse @return [Integer]
# File lib/pulse-analysis/analysis.rb, line 50 def shortest_period @shortest_period ||= @periods.min end
Tempo of the audio file in beats per minute (BPM) Assumes that the pulse is 16th notes as per the Innerclock Litmus Test @return [Float]
# File lib/pulse-analysis/analysis.rb, line 58 def tempo_bpm @tempo_bpm ||= calculate_tempo_bpm end
Validate that the analysis has produced meaningful results @return [Boolean]
# File lib/pulse-analysis/analysis.rb, line 100 def valid? @periods.count > MINIMUM_PULSES end
Validate that analysis on the given data can produce meaningful results @return [Boolean]
# File lib/pulse-analysis/analysis.rb, line 89 def validate if valid? true else message = "Could not produce a valid analysis." raise(message) end end
Private Instance Methods
Populate the instance with abberations derived from the periods @return [Array<Integer>]
# File lib/pulse-analysis/analysis.rb, line 140 def calculate_abberations i = 0 abberations = [] @periods.each do |period| unless i.zero? last_period = @periods[i - 1] abberations << period - last_period end i += 1 end abberations.pop abberations.shift abberations.map(&:abs) end
Calcuate the threshold at which pulses will register as high This is derived from the audio data @return [Float]
# File lib/pulse-analysis/analysis.rb, line 158 def calculate_amplitude_threshold @data.max * 0.8 end
Calculate the average deviation from timing of one period to the next @return [Float]
# File lib/pulse-analysis/analysis.rb, line 113 def calculate_average_abberation if abberations.empty? 0.0 else abberations.inject(&:+).to_f / abberations.count end end
Calculate the rhythmic tempo of the sound in beats per minute @return [Float]
# File lib/pulse-analysis/analysis.rb, line 123 def calculate_tempo_bpm seconds = average_period / @sound.sample_rate division = 4 # 16th notes 60 / seconds / division end
Threshold at which periods will be disregarded if they are shorter than Defaults to 80% of the average period size @param [Array<Integer>] @return [Integer]
# File lib/pulse-analysis/analysis.rb, line 166 def length_threshold(raw_periods) if @length_threshold.nil? average_period = raw_periods.inject(&:+).to_f / raw_periods.count @length_threshold = (average_period * 0.8).to_i end @length_threshold end
# File lib/pulse-analysis/analysis.rb, line 174 def min_period_length @sound.sample_rate * 60 / 4 / MAX_BPM end
Calculate periods between pulses @return [Array<Integer>]
# File lib/pulse-analysis/analysis.rb, line 180 def populate_periods is_high = true periods = [] period_index = 0 @data.each do |frame| if frame.abs < amplitude_threshold # if pulse is low is_high = false else # pulse is high if !is_high # last frame, the pulse was low # move to next period if length is past minimum if periods[period_index] >= min_period_length is_high = true period_index += 1 end end end # count period length periods[period_index] ||= 0 periods[period_index] += 1 end prune_periods(periods) @periods = periods end
Load the sound file and validate the data @param [PulseAnalysis::Sound] sound Sound
to analyze @return [PulseAnalysis::Sound]
# File lib/pulse-analysis/analysis.rb, line 132 def populate_sound(sound) @sound = sound @sound.validate_for_analysis @sound end
# File lib/pulse-analysis/analysis.rb, line 106 def prepare @data.prepare end
Remove any possibly malformed periods from the calculated set @return [Array<Integer>]
# File lib/pulse-analysis/analysis.rb, line 207 def prune_periods(periods) # remove the first and last periods in case recording wasn't started/ # stopped in sync periods.shift periods.pop # remove periods that are below length threshold length = length_threshold(periods) periods.reject! { |period| period < length } periods end