inevitable_cacophony 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/inevitable_cacophony +175 -0
- data/lib/inevitable_cacophony.rb +2 -0
- data/lib/inevitable_cacophony/midi_generator.rb +118 -0
- data/lib/inevitable_cacophony/midi_generator/frequency_table.rb +126 -0
- data/lib/inevitable_cacophony/note.rb +39 -0
- data/lib/inevitable_cacophony/octave_structure.rb +233 -0
- data/lib/inevitable_cacophony/parser/rhythm_line.rb +79 -0
- data/lib/inevitable_cacophony/parser/rhythms.rb +138 -0
- data/lib/inevitable_cacophony/parser/sectioned_text.rb +60 -0
- data/lib/inevitable_cacophony/phrase.rb +16 -0
- data/lib/inevitable_cacophony/polyrhythm.rb +169 -0
- data/lib/inevitable_cacophony/rhythm.rb +127 -0
- data/lib/inevitable_cacophony/tone_generator.rb +68 -0
- data/lib/inevitable_cacophony/version.rb +3 -0
- metadata +119 -0
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,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
|