inevitable_cacophony 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 23972ffb2207ab73ee426746749fbef9f92b0a4481deef7201de95b3b8522a56
4
+ data.tar.gz: c8735fccc00b5815308a13b95c6be437ae2fbc0782df3beeaf7a05874b2b9c88
5
+ SHA512:
6
+ metadata.gz: 9905f4be065544add64a03acd8b5cb4777204fc087736a8945fd21822a77cb6e48c200084e46e5aa15001e9d1f82eb617de779581eca1087f8b7b0f331edef8d
7
+ data.tar.gz: 922a493d81a05e53a66ae086a2ad32dfa8fcd73376aeec6b703f679006f72fb64e0b7433076ced6d3bf7405a2c494f8c38bac3e233927db8a437cdbfb0428661
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env/ruby
2
+
3
+ require 'optparse'
4
+ require 'stringio'
5
+
6
+ require 'inevitable_cacophony/parser/rhythms'
7
+ require 'inevitable_cacophony/octave_structure'
8
+ require 'inevitable_cacophony/midi_generator'
9
+ require 'inevitable_cacophony/tone_generator'
10
+ require 'inevitable_cacophony/phrase'
11
+ require 'inevitable_cacophony/version'
12
+
13
+ command = -> {
14
+ octave = InevitableCacophony::OctaveStructure.new(input)
15
+
16
+ 3.times.map do
17
+ chord_notes = []
18
+ rhythm.each_beat.map do |beat|
19
+ chords = octave.chords.values
20
+ if chord_notes.empty?
21
+ chord_notes = chords.sample(random: rng).note_scalings.dup
22
+ end
23
+
24
+ note = InevitableCacophony::Note.new(chord_notes.shift, beat)
25
+ InevitableCacophony::Phrase.new(note, tempo: options[:tempo])
26
+ end
27
+ end.flatten
28
+ }
29
+
30
+ render = -> (phrases) {
31
+ tone = InevitableCacophony::ToneGenerator.new(options[:tonic])
32
+ phrases.each { |phrase| tone.add_phrase(phrase) }
33
+
34
+ # Have to buffer output so wavefile can seek back to the beginning to write format info
35
+ output_buffer = StringIO.new
36
+ tone.write(output_buffer)
37
+ $stdout.write(output_buffer.string)
38
+ }
39
+
40
+ def input
41
+ @input ||= $stdin.read
42
+ end
43
+
44
+ def options
45
+ @options ||= {
46
+ tempo: 120, # beats per minute
47
+ tonic: 440 # Hz; middle A
48
+ }
49
+ end
50
+
51
+ def rng
52
+ @rng ||= Random.new(options[:seed] || Random.new_seed)
53
+ end
54
+
55
+ def midi_generator
56
+ @midi_generator ||= begin
57
+ octave_structure = InevitableCacophony::OctaveStructure.new(input)
58
+ InevitableCacophony::MidiGenerator.new(octave_structure, options[:tonic])
59
+ end
60
+ end
61
+
62
+ def rhythm
63
+ @rhythm ||= begin
64
+ all_rhythms = InevitableCacophony::Parser::Rhythms.new.parse(input)
65
+
66
+ # InevitableCacophony::Pick the first rhythm mentioned in the file, which should be the one
67
+ # used by the first section of the piece.
68
+ rhythm_name = all_rhythms.keys.sort_by { |name| input.index(name.to_s) }.first
69
+
70
+ if all_rhythms[rhythm_name]
71
+ all_rhythms[rhythm_name]
72
+ else
73
+
74
+ # InevitableCacophony::If no rhythms are mentioned, parse any rhythm string we can find in the input.
75
+ rhythm_score = input.match(/(\|( |\`)((-|x|X|!)( |\`|\'))+)+\|/).to_s
76
+ InevitableCacophony::Parser::RhythmLine.new.parse(rhythm_score)
77
+ end
78
+ end
79
+ end
80
+
81
+ OptionParser.new do |opts|
82
+
83
+ opts.banner = 'Usage: ruby -Ilib cacophony.rb [options]'
84
+
85
+ opts.on('-b', '--beat', 'Play a beat in the given rhythm') do
86
+ command = -> {
87
+ notes = 3.times.map do
88
+ rhythm.each_beat.map do |beat|
89
+ InevitableCacophony::Note.new(1, beat)
90
+ end
91
+ end.flatten
92
+
93
+ [InevitableCacophony::Phrase.new(*notes, tempo: options[:tempo])]
94
+ }
95
+ end
96
+
97
+ opts.on('-s', '--scale', 'Play a scale in the given style') do
98
+ command = -> {
99
+ octave = InevitableCacophony::OctaveStructure.new(input)
100
+
101
+ scale = if options[:chromatic]
102
+ octave.chromatic_scale
103
+ else
104
+ octave.scales.values.first
105
+ end
106
+
107
+ rising_and_falling = scale.open.note_scalings + scale.note_scalings.reverse
108
+ notes = rising_and_falling.map do |factor|
109
+ InevitableCacophony::Note.new(factor, InevitableCacophony::Rhythm::Beat.new(1, 1, 0))
110
+ end
111
+
112
+ [InevitableCacophony::Phrase.new(*notes, tempo: options[:tempo])]
113
+ }
114
+ end
115
+
116
+ opts.on('-p', '--polyrhythm RATIO', "Rather than loading rhythm normally, use a polyrhythm in the given ratio (e.g 7:11, 2:3:4).") do |ratio|
117
+ components = ratio.split(':').map do |length|
118
+ InevitableCacophony::Rhythm.new([InevitableCacophony::Rhythm::Beat.new(1, 1, 0)] * length.to_i)
119
+ end
120
+
121
+ primary, *secondaries = components
122
+ @rhythm = InevitableCacophony::Polyrhythm.new(primary, secondaries)
123
+ end
124
+
125
+ opts.on('-e', '--eval FORM', 'Parse FORM rather than reading a form description from stdin') do |form|
126
+ @input = form
127
+ end
128
+
129
+ opts.on('--chromatic', 'Use "chromatic" scales (all notes in the form) rather than the named scales typical of the form') do
130
+ options[:chromatic] = true
131
+ end
132
+
133
+ opts.on('-h', '--help', 'Prints this help') do
134
+ puts opts
135
+ exit
136
+ end
137
+
138
+ opts.on('-v', '--version', 'Show version and exit') do
139
+ puts "Inevitable Cacophony version #{InevitableCacophony::VERSION}"
140
+ exit
141
+ end
142
+
143
+ opts.on('-t', '--tempo TEMPO', "Play at the given tempo in beats per minute (default #{options[:tempo]}). For polyrhythms, this applies to whatever number is given first: at the same tempo, a 2:3 rhythm will play faster than a 3:2.") do |tempo|
144
+ options[:tempo] = tempo.to_i
145
+ end
146
+
147
+ opts.on('-S', '--seed SEED', 'Generate random melodies with the given seed, for repeatable results.') do |seed|
148
+ int_seed = seed.to_i
149
+ raise "Expected seed to be a number" unless seed == int_seed.to_s
150
+
151
+ options[:seed] = int_seed
152
+ end
153
+
154
+ opts.on('-m', '--midi', 'Generate output in MIDI rather than WAV format (needs file from -M to play in tune)') do
155
+ render = -> (phrases) {
156
+ phrases.each do |phrase|
157
+ midi_generator.add_phrase(phrase)
158
+ end
159
+ midi_generator.write($stdout)
160
+ }
161
+ end
162
+
163
+ opts.on('-M', '--midi-tuning', 'Instead of music, generate a Scala (Timidity-compatible) tuning file for use with MIDI output from --midi') do
164
+ command = -> {
165
+ midi_generator.frequency_table
166
+ }
167
+ render = -> (frequencies) {
168
+ frequencies.table.each do |frequency|
169
+ $stdout.puts (frequency * 1000).round
170
+ end
171
+ }
172
+ end
173
+ end.parse!
174
+
175
+ render.call(command.call)
@@ -0,0 +1,2 @@
1
+ module InevitableCacophony
2
+ end
@@ -0,0 +1,118 @@
1
+ # Converts Inevitable Cacophony internal note representation
2
+ # into MIDI messages usable by an external synthesizer.
3
+ # Based on examples in the `midilib` gem.
4
+
5
+ require 'midilib/sequence'
6
+ require 'midilib/consts'
7
+
8
+ require 'inevitable_cacophony/midi_generator/frequency_table'
9
+
10
+ module InevitableCacophony
11
+ class MidiGenerator
12
+
13
+ # Set up a MIDI generator for a specific octave structure and tonic
14
+ # We need to know the octave structure because it determines
15
+ # how we allocate MIDI note indices to frequencies.
16
+ def initialize(octave_structure, tonic)
17
+ @frequency_table = FrequencyTable.new(octave_structure, tonic)
18
+ end
19
+
20
+ attr_reader :frequency_table
21
+
22
+ # Add a phrase to the MIDI output we will generate.
23
+ def add_phrase(phrase)
24
+ @phrases ||= []
25
+ @phrases << phrase
26
+ end
27
+
28
+ # @return [Midi::Track] Notes to be output to MIDI; mainly for testing.
29
+ def notes_track(sequence=build_sequence)
30
+ build_notes_track(sequence, @phrases)
31
+ end
32
+
33
+ # Write MIDI output to the given stream.
34
+ def write(io)
35
+ sequence = build_sequence
36
+ sequence.tracks << notes_track(sequence)
37
+
38
+ # Buffer output so this method can be called on stdout.
39
+ buffer = StringIO.new
40
+ sequence.write(buffer)
41
+
42
+ io.write(buffer.string)
43
+ end
44
+
45
+ private
46
+
47
+ def build_sequence
48
+ seq = MIDI::Sequence.new
49
+ seq.tracks << meta_track(seq)
50
+ seq
51
+ end
52
+
53
+ # TODO: why do I have to pass `seq` in,
54
+ # when I'm then later adding the track back to seq.tracks?
55
+ def meta_track(seq)
56
+ track = MIDI::Track.new(seq)
57
+
58
+ # TODO: handle tempo changes (how?)
59
+ track.events << MIDI::Tempo.new(
60
+ MIDI::Tempo.bpm_to_mpq(@phrases.first.tempo)
61
+ )
62
+ track.events << MIDI::MetaEvent.new(
63
+ MIDI::META_SEQ_NAME,
64
+ 'TODO: name sequence'
65
+ )
66
+
67
+ track
68
+ end
69
+
70
+ # TODO: multiple instruments?
71
+ def build_notes_track(seq, phrases)
72
+ track = MIDI::Track.new(seq)
73
+ track.name = 'Cacophony'
74
+
75
+ # TODO: why this particular instrument.
76
+ track.instrument = MIDI::GM_PATCH_NAMES[0]
77
+
78
+ # TODO: what's this for?
79
+ track.events << MIDI::ProgramChange.new(0, 1, 0)
80
+
81
+ # Inter-note delay from the end of the previous beat.
82
+ leftover_delay = 0
83
+
84
+ phrases.each do |phrase|
85
+ phrase.notes.each do |note|
86
+ track.events += midi_events_for_note(leftover_delay, note, seq)
87
+ leftover_delay = seq.length_to_delta(note.beat.after_delay)
88
+ end
89
+ end
90
+
91
+ track
92
+ end
93
+
94
+ # TODO: code smell to pass in seq
95
+ def midi_events_for_note(delay_before, note, seq)
96
+ midi_note = @frequency_table.index_for_ratio(note.ratio)
97
+ beat = note.beat
98
+
99
+ [
100
+ MIDI::NoteOn.new(
101
+ 0,
102
+ midi_note,
103
+ (beat.amplitude * 127).ceil,
104
+ # TODO: can notes be out of order?
105
+ # Beat duration 1 conveniently matches
106
+ # midilib's quarter-note = 1.
107
+ seq.length_to_delta(beat.start_delay) + delay_before
108
+ ),
109
+ MIDI::NoteOff.new(
110
+ 0,
111
+ midi_note,
112
+ 127,
113
+ seq.length_to_delta(beat.sounding_time)
114
+ )
115
+ ]
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,126 @@
1
+ # A frequency table maps Dwarf fortress notes (specific frequencies) to
2
+ # MIDI indices for use in MIDI indexes.
3
+ #
4
+ # Where possible we use the standard MIDI values for DF notes; where that
5
+ # won't work, we try to keep as close to the MIDI structure as the DF scale
6
+ # system will allow.
7
+
8
+ # Using for OctaveStructure::OCTAVE_RATIO; may be better to just use +2+.
9
+ require 'inevitable_cacophony/octave_structure'
10
+
11
+ module InevitableCacophony
12
+ class MidiGenerator
13
+ class FrequencyTable
14
+
15
+ # Raised when there is no MIDI index available for
16
+ # a note we're trying to output
17
+ class OutOfRange < StandardError
18
+ def initialize(frequency, table)
19
+ super("Not enough MIDI indices to represent #{frequency} Hz. "\
20
+ "Available range is #{table.inspect}")
21
+ end
22
+ end
23
+
24
+ # Range of allowed MIDI 1 indices.
25
+ MIDI_RANGE = 0..127
26
+
27
+ # Middle A in MIDI
28
+ MIDI_TONIC = 69
29
+
30
+ # Standard western notes per octave assumed by MIDI
31
+ MIDI_OCTAVE_NOTES = 12
32
+
33
+ # 12TET values of those notes.
34
+ STANDARD_MIDI_FREQUENCIES = MIDI_OCTAVE_NOTES.times.map do |index|
35
+ OctaveStructure::OCTAVE_RATIO ** (index / MIDI_OCTAVE_NOTES.to_f)
36
+ end
37
+
38
+ # Maximum increase/decrease between two frequencies we still treat as
39
+ # "equal". Approximately 1/30th of human Just Noticeable Difference
40
+ # for pitch.
41
+ FREQUENCY_FUDGE_FACTOR = (1.0/10_000)
42
+
43
+ # Create a frequency table with a given structure and tonic.
44
+ #
45
+ # @param octave_structure [OctaveStructure]
46
+ # @param tonic [Integer] The tonic frequency in Hertz.
47
+ # This will correspond to Cacophony frequency 1,
48
+ # and MIDI pitch 69
49
+ def initialize(octave_structure, tonic)
50
+ @tonic = tonic
51
+ @table = build_table(octave_structure, tonic)
52
+ end
53
+
54
+ attr_reader :table
55
+
56
+ # @param ratio [Float] The given note as a ratio to the tonic
57
+ # (e.g. A above middle A = 2.0)
58
+ def index_for_ratio(ratio)
59
+ # TODO: not reliable for approximate matching
60
+ frequency = @tonic * ratio
61
+
62
+ if (match = table.index(frequency))
63
+ match
64
+ else
65
+ raise OutOfRange.new(frequency, table)
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def build_table(octave_structure, tonic)
72
+ chromatic = octave_structure.chromatic_scale.open.note_scalings
73
+ octave_breakdown = best_match_ratios(chromatic)
74
+
75
+ MIDI_RANGE.map do |index|
76
+ tonic_offset = index - MIDI_TONIC
77
+ octave_offset, note = tonic_offset.divmod(octave_breakdown.length)
78
+
79
+ bottom_of_octave = tonic * OctaveStructure::OCTAVE_RATIO**octave_offset
80
+ bottom_of_octave * octave_breakdown[note]
81
+ end
82
+ end
83
+
84
+ # Pick a MIDI index within the octave for each given frequency.
85
+ #
86
+ # If there are few enough (<12) frequencies in the generated scale,
87
+ # we try to keep as much of the normal MIDI tuning as possible, and
88
+ # only re-tune what we need. If the DF scale is a subset of 12TET,
89
+ # this should return the standard MIDI tuning.
90
+ #
91
+ # Other than that it isn't guaranteed to be optimal; currently it's
92
+ # a fairly naieve greedy algorithm.
93
+ #
94
+ # @return [Array] Re-tuned ratios for each position in the MIDI octave.
95
+ def best_match_ratios(frequencies_to_cover)
96
+ standard_octave = STANDARD_MIDI_FREQUENCIES.dup
97
+ ratios = []
98
+
99
+ while (next_frequency = frequencies_to_cover.shift)
100
+
101
+ # Skip ahead (padding slots with 12TET frequencies from low to high) until:
102
+ #
103
+ # * the next 12TET frequency would be sharper, or
104
+ # * any more padding will leave us without enough space.
105
+ while (standard = standard_octave.shift) &&
106
+ sounds_flatter?(standard, next_frequency) &&
107
+ standard_octave.length > frequencies_to_cover.length
108
+
109
+ ratios << standard
110
+ end
111
+
112
+ # Use this frequency in this slot.
113
+ ratios << next_frequency
114
+ end
115
+
116
+ ratios
117
+ end
118
+
119
+ # Like < but considers values within FREQUENCY_FUDGE_FACTOR equal
120
+ def sounds_flatter?(a, b)
121
+ threshold = b * (1 - FREQUENCY_FUDGE_FACTOR)
122
+ a < threshold
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,39 @@
1
+ # Represents a single note of a tune
2
+
3
+ # TODO: only for Beat class
4
+ require 'inevitable_cacophony/rhythm'
5
+
6
+ module InevitableCacophony
7
+ class Note < Struct.new(:ratio, :beat)
8
+
9
+ # @param ratio [Numeric] Note frequency, as a multiple of the tonic.
10
+ # @param amplitude [Rhythm::Beat] A Beat object defining amplitude and timing
11
+ def initialize(ratio, beat)
12
+ super(ratio, beat)
13
+ end
14
+
15
+ # Create a rest for the duration of the given beat.
16
+ # @param beat [Beat]
17
+ def self.rest(beat)
18
+
19
+ # Can't set ratio to 0 as it causes divide-by-zero errors
20
+ new(1, Rhythm::Beat.new(0, beat.duration, beat.timing))
21
+ end
22
+
23
+ def start_delay
24
+ beat.start_delay
25
+ end
26
+
27
+ def after_delay
28
+ beat.after_delay
29
+ end
30
+
31
+ def duration
32
+ beat.duration
33
+ end
34
+
35
+ def frequency
36
+ ratio
37
+ end
38
+ end
39
+ end