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
Public Class Methods
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
@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
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
# 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
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