inevitable_cacophony 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
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