class InevitableCacophony::MidiGenerator::FrequencyTable

Constants

FREQUENCY_FUDGE_FACTOR

Maximum increase/decrease between two frequencies we still treat as “equal”. Approximately 1/30th of human Just Noticeable Difference for pitch.

MIDI_OCTAVE_NOTES

Standard western notes per octave assumed by MIDI

MIDI_RANGE

Range of allowed MIDI 1 indices.

MIDI_TONIC

Middle A in MIDI

STANDARD_MIDI_FREQUENCIES

12TET values of those notes.

Attributes

table[R]

Public Class Methods

new(octave_structure, tonic) click to toggle source

Create a frequency table with a given structure and tonic.

@param octave_structure [OctaveStructure] @param tonic [Integer] The tonic frequency in Hertz.

This will correspond to Cacophony frequency 1,
and MIDI pitch 69
# File lib/inevitable_cacophony/midi_generator/frequency_table.rb, line 49
def initialize(octave_structure, tonic)
        @tonic = tonic
        @table = build_table(octave_structure, tonic)
end

Public Instance Methods

index_for_ratio(ratio) click to toggle source

@param ratio [Float] The given note as a ratio to the tonic

(e.g. A above middle A = 2.0)
# File lib/inevitable_cacophony/midi_generator/frequency_table.rb, line 58
def index_for_ratio(ratio)
        # TODO: not reliable for approximate matching
        frequency = @tonic * ratio
        
        if (match = table.index(frequency))
                match
        else
                raise OutOfRange.new(frequency, table)
        end
end

Private Instance Methods

best_match_ratios(frequencies_to_cover) click to toggle source

Pick a MIDI index within the octave for each given frequency.

If there are few enough (<12) frequencies in the generated scale, we try to keep as much of the normal MIDI tuning as possible, and only re-tune what we need. If the DF scale is a subset of 12TET, this should return the standard MIDI tuning.

Other than that it isn't guaranteed to be optimal; currently it's a fairly naieve greedy algorithm.

@return [Array] Re-tuned ratios for each position in the MIDI octave.

# File lib/inevitable_cacophony/midi_generator/frequency_table.rb, line 95
def best_match_ratios(frequencies_to_cover)
        standard_octave = STANDARD_MIDI_FREQUENCIES.dup
        ratios = []
        
        while (next_frequency = frequencies_to_cover.shift)
        
                # Skip ahead (padding slots with 12TET frequencies from low to high) until:
                #
                # * the next 12TET frequency would be sharper, or
                # * any more padding will leave us without enough space.
                while (standard = standard_octave.shift) &&
                                sounds_flatter?(standard, next_frequency) &&
                                standard_octave.length > frequencies_to_cover.length
        
                        ratios << standard
                end
        
                # Use this frequency in this slot.
                ratios << next_frequency
        end
        
        ratios
end
build_table(octave_structure, tonic) click to toggle source
# File lib/inevitable_cacophony/midi_generator/frequency_table.rb, line 71
def build_table(octave_structure, tonic)
        chromatic = octave_structure.chromatic_scale.open.note_scalings
        octave_breakdown = best_match_ratios(chromatic)
        
        MIDI_RANGE.map do |index|
                tonic_offset = index - MIDI_TONIC
                octave_offset, note = tonic_offset.divmod(octave_breakdown.length)
        
                bottom_of_octave = tonic * OctaveStructure::OCTAVE_RATIO**octave_offset
                bottom_of_octave * octave_breakdown[note]
        end
end
sounds_flatter?(a, b) click to toggle source

Like < but considers values within FREQUENCY_FUDGE_FACTOR equal

# File lib/inevitable_cacophony/midi_generator/frequency_table.rb, line 120
def sounds_flatter?(a, b)
        threshold = b * (1 - FREQUENCY_FUDGE_FACTOR)
        a < threshold
end