class PulseAnalysis::Analysis

Constants

MAX_BPM
MINIMUM_PULSES

Attributes

abberations[R]
data[R]
periods[R]
sound[R]

Public Class Methods

new(sound, options = {}) click to toggle source

@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

amplitude_threshold() click to toggle source

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_abberation() click to toggle source

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_period() click to toggle source

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_abberation() click to toggle source

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_period() click to toggle source

Longest number of samples between pulse @return [Integer]

# File lib/pulse-analysis/analysis.rb, line 44
def longest_period
  @longest_period ||= @periods.max
end
num_pulses() click to toggle source

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() click to toggle source

Run the analysis @return [Boolean]

# File lib/pulse-analysis/analysis.rb, line 23
def run
  prepare
  populate_periods
  validate
  true
end
shortest_period() click to toggle source

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_bpm() click to toggle source

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
valid?() click to toggle source

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() click to toggle source

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

calculate_abberations() click to toggle source

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
calculate_amplitude_threshold() click to toggle source

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_average_abberation() click to toggle source

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_tempo_bpm() click to toggle source

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
length_threshold(raw_periods) click to toggle source

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
min_period_length() click to toggle source
# File lib/pulse-analysis/analysis.rb, line 174
def min_period_length
  @sound.sample_rate * 60 / 4 / MAX_BPM
end
populate_periods() click to toggle source

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
populate_sound(sound) click to toggle source

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
prepare() click to toggle source
# File lib/pulse-analysis/analysis.rb, line 106
def prepare
  @data.prepare
end
prune_periods(periods) click to toggle source

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