module SPCore
class Oscillator
  include Hashmake::HashMakeable
  attr_accessor :wave_type, :amplitude, :dc_offset
  attr_reader :frequency, :sample_rate, :phase_offset
  
  WAVE_SINE = :waveSine
  WAVE_TRIANGLE = :waveTriangle
  WAVE_SAWTOOTH = :waveSawtooth
  WAVE_SQUARE = :waveSquare

  WAVES = [WAVE_SINE, WAVE_TRIANGLE, WAVE_SAWTOOTH, WAVE_SQUARE]
  
  ARG_SPECS = [
    Hashmake::ArgSpec.new(:reqd => true, :key => :sample_rate, :type => Float, :validator => ->(a){ a > 0.0 } ),
    Hashmake::ArgSpec.new(:reqd => false, :key => :wave_type, :type => Symbol, :default => WAVE_SINE, :validator => ->(a){ WAVES.include? a } ),
    Hashmake::ArgSpec.new(:reqd => false, :key => :frequency, :type => Float, :default => 1.0, :validator => ->(a){ a > 0.0 } ),
    Hashmake::ArgSpec.new(:reqd => false, :key => :amplitude, :type => Float, :default => 1.0 ),
    Hashmake::ArgSpec.new(:reqd => false, :key => :phase_offset, :type => Float, :default => 0.0 ),
    Hashmake::ArgSpec.new(:reqd => false, :key => :dc_offset, :type => Float, :default => 0.0 ),
  ]

  def initialize args
    hash_make Oscillator::ARG_SPECS, args
    
    @phase_angle_incr = (@frequency * TWO_PI) / @sample_rate
    @current_phase_angle = @phase_offset
  end
  
  def sample_rate= sample_rate
    @sample_rate = sample_rate
    self.frequency = @frequency
  end
  
  def frequency= frequency
    @frequency = frequency
    @phase_angle_incr = (@frequency * TWO_PI) / @sample_rate
  end
  
  def phase_offset= phase_offset
    @current_phase_angle += (phase_offset - @phase_offset);
    @phase_offset = phase_offset
  end

  def sample
    output = 0.0

    while(@current_phase_angle < NEG_PI)
      @current_phase_angle += TWO_PI
    end

    while(@current_phase_angle > Math::PI)
      @current_phase_angle -= TWO_PI
    end

    case @wave_type
    when WAVE_SINE
      output = @amplitude * sine(@current_phase_angle) + @dc_offset
    when WAVE_TRIANGLE
      output = @amplitude * triangle(@current_phase_angle) + @dc_offset
    when WAVE_SQUARE
      output = @amplitude * square(@current_phase_angle) + @dc_offset
    when WAVE_SAWTOOTH
      output = @amplitude * sawtooth(@current_phase_angle) + @dc_offset
    else
      raise "Encountered unexpected wave type #{@wave_type}"
    end
    
    @current_phase_angle += @phase_angle_incr
    return output
  end

  K_SINE_B = 4.0 / Math::PI
  K_SINE_C = -4.0 / (Math::PI * Math::PI)
  # Q = 0.775
  K_SINE_P = 0.225
  
  # generate a sine wave:
  # input range: -PI to PI
  # ouput range: -1 to 1
  def sine x
    y = K_SINE_B * x + K_SINE_C * x * x.abs
    # for extra precision
    y = K_SINE_P * (y * y.abs - y) + y   # Q * y + P * y * y.abs
    
    # sin normally output outputs -1 to 1, so to adjust
    # it to output 0 to 1, return (y*0.5)+0.5
    return y
  end

  K_TRIANGLE_A = 2.0 / Math::PI;

  # generate a triangle wave:
  # input range: -PI to PI
  # ouput range: -1 to 1
  def triangle x
    (K_TRIANGLE_A * x).abs - 1.0
  end

  # generate a square wave (50% duty cycle):
  # input range: -PI to PI
  # ouput range: 0 to 1
  def square x
    (x >= 0.0) ? 1.0 : -1.0
  end

  K_SAWTOOTH_A = 1.0 / Math::PI

  # generate a sawtooth wave:
  # input range: -PI to PI
  # ouput range: -1 to 1
  def sawtooth x
    K_SAWTOOTH_A * x
  end

end
end