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 +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
@@ -0,0 +1,60 @@
|
|
1
|
+
# Splits text into a sequence of delimited sections,
|
2
|
+
# and knows how to find the one you want.
|
3
|
+
#
|
4
|
+
# Used to parse complex paragraph structures without
|
5
|
+
# having to handle every single paragraph type,
|
6
|
+
# and without crashing if an expected type is missing.
|
7
|
+
|
8
|
+
module InevitableCacophony
|
9
|
+
module Parser
|
10
|
+
class SectionedText
|
11
|
+
|
12
|
+
PARAGRAPH_DELIMITER = "\n\n"
|
13
|
+
SENTENCE_DELIMITER = /\.\s+/
|
14
|
+
|
15
|
+
# @param description [String] The description to parse
|
16
|
+
# @param delimiter [String,Regex] The delimiter between string sections. Defaults to splitting by paragraphs.
|
17
|
+
def initialize(description, delimiter=PARAGRAPH_DELIMITER)
|
18
|
+
@sections = description.split(delimiter).map(&:strip)
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_accessor :sections
|
22
|
+
|
23
|
+
# Find a section (paragraph, sentence, etc.) of the description
|
24
|
+
# matching a given regular expression.
|
25
|
+
# @param key [Regex]
|
26
|
+
# @return [String]
|
27
|
+
def find(key)
|
28
|
+
find_all(key).first
|
29
|
+
end
|
30
|
+
|
31
|
+
# Find all sections matching a given key
|
32
|
+
# @param key [Regex]
|
33
|
+
# @return [Array<String>]
|
34
|
+
def find_all(key)
|
35
|
+
@sections.select { |s| key.match?(s) } || raise("No match for #{key.inspect} in #{@sections.inspect}")
|
36
|
+
end
|
37
|
+
|
38
|
+
# Find a paragraph within the description, and break it up into sentences.
|
39
|
+
# @param key [Regex]
|
40
|
+
# @return [SectionedText] The paragraph, split into sentences.
|
41
|
+
def find_paragraph(key)
|
42
|
+
find_all_paragraphs(key).first
|
43
|
+
find_all_paragraphs(key).first
|
44
|
+
end
|
45
|
+
|
46
|
+
# As above but finds all matching paragraphs.
|
47
|
+
# @param key [Regex]
|
48
|
+
# @return [Array<SectionedText>]
|
49
|
+
def find_all_paragraphs(key)
|
50
|
+
find_all(key).map do |string|
|
51
|
+
SectionedText.new(string, SENTENCE_DELIMITER)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def inspect
|
56
|
+
"<SectionedText: #{@sections.inspect}>"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# Represents a "phrase", by which I mean a sequence of notes
|
2
|
+
# with common performance instructions (tempo, volume, etc.)
|
3
|
+
|
4
|
+
module InevitableCacophony
|
5
|
+
class Phrase
|
6
|
+
|
7
|
+
# @param notes [Array<Note>] The notes to play (what you'd write on the bar lines, mostly)
|
8
|
+
# @param tempo [Numeric] Tempo in beats per minute.
|
9
|
+
def initialize(*notes, tempo: raise)
|
10
|
+
@tempo = tempo
|
11
|
+
@notes = notes
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :notes, :tempo
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
# Represents a rhythm that combines two or more simpler rhythms.
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
require 'inevitable_cacophony/rhythm'
|
6
|
+
|
7
|
+
module InevitableCacophony
|
8
|
+
class Polyrhythm < Rhythm
|
9
|
+
|
10
|
+
# Creates a new polyrhythm by combining two simpler component rhythms.
|
11
|
+
# It will have the same duration as the primary rhythm, but include
|
12
|
+
# beats from both it and all the secondaries.
|
13
|
+
#
|
14
|
+
# TODO: do I want to emphasise the primary rhythm more?
|
15
|
+
#
|
16
|
+
# @param primary [Rhythm] The rhythm that will be considered the primary.
|
17
|
+
# @param secondaries [Array<Rhythm>] The other component rhythms.
|
18
|
+
def initialize(primary, secondaries)
|
19
|
+
@primary = primary
|
20
|
+
@secondaries = secondaries
|
21
|
+
|
22
|
+
unscaled_beats = beats_from_canonical(canonical)
|
23
|
+
scaled_beats = scale_beats(unscaled_beats, @primary.duration)
|
24
|
+
super(scaled_beats)
|
25
|
+
end
|
26
|
+
|
27
|
+
attr_accessor :primary, :secondaries
|
28
|
+
|
29
|
+
# @return [Array<Rhythm>] All the component rhythms that make up this polyrhythm
|
30
|
+
def components
|
31
|
+
[primary, *secondaries]
|
32
|
+
end
|
33
|
+
|
34
|
+
# Calculates the canonical form by combining the two component rhythms.
|
35
|
+
# @return [Array<Float>]
|
36
|
+
def canonical
|
37
|
+
|
38
|
+
sounding = Set.new
|
39
|
+
first, *rest = aligned_components
|
40
|
+
unnormalised = first.zip(*rest).map do |beats_at_tick|
|
41
|
+
beat, sounding = update_sounding_beats(sounding, beats_at_tick)
|
42
|
+
beat
|
43
|
+
end
|
44
|
+
|
45
|
+
# Renormalise to a maximum volume of 100%
|
46
|
+
max_amplitude = unnormalised.compact.max.to_f
|
47
|
+
unnormalised.map { |b| b && b / max_amplitude }
|
48
|
+
end
|
49
|
+
|
50
|
+
def == other
|
51
|
+
self.class == other.class &&
|
52
|
+
self.primary == other.primary &&
|
53
|
+
self.secondaries == other.secondaries
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
# Calculate a set of beats with timings from the given canonical rhythm.
|
60
|
+
# TODO: properly account for pre-existing durations.
|
61
|
+
#
|
62
|
+
# @param canonical [Array<Float,NilClass>]
|
63
|
+
# @return [Array<Beat>]
|
64
|
+
def beats_from_canonical(canonical)
|
65
|
+
[].tap do |beats|
|
66
|
+
amplitude = canonical.shift || 0
|
67
|
+
|
68
|
+
duration = 1 # to account for the first timeslot that we just shifted off.
|
69
|
+
canonical.each do |this_beat|
|
70
|
+
if this_beat.nil?
|
71
|
+
duration += 1
|
72
|
+
else
|
73
|
+
beats << Beat.new(amplitude, duration, 0)
|
74
|
+
|
75
|
+
# Now start collecting time for the next beat.
|
76
|
+
duration = 1
|
77
|
+
amplitude = this_beat
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
beats << Beat.new(amplitude, duration, 0)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Returns the "canonical" forms of the component rhythms, but stretched
|
86
|
+
# all to the same length, so corresponding beats in each rhythm have the
|
87
|
+
# same index.
|
88
|
+
# @return [Array<Array<Float, NilClass>>]
|
89
|
+
def aligned_components
|
90
|
+
canon_components = components.map(&:canonical)
|
91
|
+
common_multiple = canon_components.map(&:length).inject(1, &:lcm)
|
92
|
+
|
93
|
+
# Stretch each component rhythm to the right length, and return them.
|
94
|
+
canon_components.map do |component|
|
95
|
+
stretch_factor = common_multiple / component.length
|
96
|
+
|
97
|
+
unless stretch_factor == stretch_factor.to_i
|
98
|
+
raise "Expected dividing LCM of lengths by one length to be an integer."
|
99
|
+
end
|
100
|
+
|
101
|
+
space_between_beats = stretch_factor - 1
|
102
|
+
|
103
|
+
component.map { |beat| [beat] + Array.new(space_between_beats) }.flatten
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Given several channels and the set of beats currently playing,
|
108
|
+
# calculate the beat that should now start/stop/continue playing.
|
109
|
+
#
|
110
|
+
# @param sounding [Set{Integer}] The channels with a beat currently playing.
|
111
|
+
# @param current_channel_state [Array<Float>] The beat each channel has at this tick.
|
112
|
+
# (+nil+) means continuing an earlier beat.
|
113
|
+
# @return [Array<[Float, NilClass],Set{Integer}] A two-element array like +[beat, sounding]+,
|
114
|
+
# where +beat+ is the amplitude to play now (+nil+ to hold last note; 0 to stop),
|
115
|
+
# and +sounding+ is the Set of channels still playing.
|
116
|
+
def update_sounding_beats(sounding, current_channel_states)
|
117
|
+
|
118
|
+
beat = nil
|
119
|
+
|
120
|
+
# If we're starting new beats, they interrupt whatever came before.
|
121
|
+
new_beats = indices_of(current_channel_states) { |b| b && b > 0 }
|
122
|
+
if new_beats.any?
|
123
|
+
sounding = new_beats.to_set
|
124
|
+
beat = current_channel_states.compact.sum
|
125
|
+
else
|
126
|
+
|
127
|
+
# If every beat has now stopped, go silent.
|
128
|
+
# Otherwise, keep playing what's still sounding.
|
129
|
+
finished_beats = indices_of(current_channel_states, 0)
|
130
|
+
sounding.subtract(finished_beats)
|
131
|
+
|
132
|
+
if finished_beats.any? && sounding.empty?
|
133
|
+
beat = 0
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
[beat, sounding]
|
138
|
+
end
|
139
|
+
|
140
|
+
# TODO: should really be in some other class.
|
141
|
+
# Returns all indices of an array where the given value can be found,
|
142
|
+
# or all that match the given block
|
143
|
+
#
|
144
|
+
# @param array An object responding to #each_index and #[<index>]
|
145
|
+
# @param condition The object we're looking for in the array
|
146
|
+
# @return Array<Integer> indices into `array` matching the conditions.
|
147
|
+
#
|
148
|
+
# Source: steenslag at Stack Overflow (https://stackoverflow.com/a/13660352/10955118); CC-BY-SA
|
149
|
+
def indices_of(array, condition=nil, &block)
|
150
|
+
block ||= condition.method(:==)
|
151
|
+
|
152
|
+
array.each_index.select do |index|
|
153
|
+
block.call(array[index])
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Scales each beat in the given list so the list has the given total duration.
|
158
|
+
#
|
159
|
+
# @param beats [Array<Beat>]
|
160
|
+
# @param duration [Float]
|
161
|
+
def scale_beats(beats, total_duration)
|
162
|
+
scale_factor = total_duration.to_f / beats.map(&:duration).sum
|
163
|
+
|
164
|
+
beats.map do |beat|
|
165
|
+
Beat.new(beat.amplitude, beat.duration * scale_factor, beat.timing)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# A rhythm, represented as a sequence of beats of varying length and volume.
|
2
|
+
# Beats may be "early" or "late", but internally this is represented by
|
3
|
+
# adjusting the durations of surrounding beats.
|
4
|
+
|
5
|
+
module InevitableCacophony
|
6
|
+
class Rhythm
|
7
|
+
|
8
|
+
# Amount of silence before a note, as a fraction of the note's duration
|
9
|
+
START_DELAY = (0.3).rationalize
|
10
|
+
|
11
|
+
# Amount of silence after notes, as a fraction of duration.
|
12
|
+
AFTER_DELAY = (0.3).rationalize
|
13
|
+
|
14
|
+
# Amplitude -- how loud the beat is, on a scale from silent to MAX VOLUME.
|
15
|
+
# Duration -- how long it is, in arbitrary beat units (think metronome ticks)
|
16
|
+
# Timing -- how early or late the beat is, relative to the same metaphorical metronome.
|
17
|
+
class Beat < Struct.new(:amplitude, :duration, :timing)
|
18
|
+
|
19
|
+
# How much earlier or later than normal this beat's time slice should start,
|
20
|
+
# accounting for the standard start/end delays, timing, and duration.
|
21
|
+
# Negative numbers start earlier, positive ones later.
|
22
|
+
#
|
23
|
+
# @return [Float]
|
24
|
+
def start_offset
|
25
|
+
standard_start_delay = START_DELAY * duration
|
26
|
+
start_delay - standard_start_delay
|
27
|
+
end
|
28
|
+
|
29
|
+
# How much silence there is before this note starts,
|
30
|
+
# after the previous note has finished its time (like padding in CSS).
|
31
|
+
#
|
32
|
+
# @return [Float]
|
33
|
+
def start_delay
|
34
|
+
start_and_after_delays.first * duration
|
35
|
+
end
|
36
|
+
|
37
|
+
# How much silence there is after this note ends,
|
38
|
+
# before the next note's timeslot.
|
39
|
+
#
|
40
|
+
# @return [Float]
|
41
|
+
def after_delay
|
42
|
+
start_and_after_delays.last * duration
|
43
|
+
end
|
44
|
+
|
45
|
+
# How long this note sounds for,
|
46
|
+
# excluding any start/end delays.
|
47
|
+
def sounding_time
|
48
|
+
duration * (1 - start_and_after_delays.sum)
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
# Calculate the before-note and after-note delays together,
|
54
|
+
# to ensure they add up correctly.
|
55
|
+
def start_and_after_delays
|
56
|
+
@start_and_after_delays ||= begin
|
57
|
+
|
58
|
+
# Positive values from 0 to 1.
|
59
|
+
# Higher numbers mean move more of this offset to the other side of the note
|
60
|
+
# (e.g. start earlier for start offset).
|
61
|
+
start_offset = -[timing, 0].min
|
62
|
+
end_offset = [timing, 0].max
|
63
|
+
|
64
|
+
# This is basically matrix multiplication; multiply [START_DELAY, END_DELAY]
|
65
|
+
# by [
|
66
|
+
# (1 - start_offset) end_offset
|
67
|
+
# start_offset (1 - end_offset)
|
68
|
+
# ]
|
69
|
+
[
|
70
|
+
((1 - start_offset) * START_DELAY) + (end_offset * AFTER_DELAY),
|
71
|
+
(start_offset * START_DELAY) + ((1 - end_offset) * AFTER_DELAY)
|
72
|
+
]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def initialize(beats)
|
78
|
+
@beats = beats
|
79
|
+
end
|
80
|
+
|
81
|
+
attr_reader :beats
|
82
|
+
|
83
|
+
def each_beat(&block)
|
84
|
+
@beats.each(&block)
|
85
|
+
end
|
86
|
+
|
87
|
+
# @return [Integer] Total duration of all beats in this rhythm.
|
88
|
+
def duration
|
89
|
+
each_beat.sum(&:duration)
|
90
|
+
end
|
91
|
+
|
92
|
+
# @return [Array<Numeric,NilClass>] An array where a[i] is the amplitude of the beat at time-step i
|
93
|
+
# (rests are 0), or nil if no beat is played then.
|
94
|
+
# This will be as long as necessary to represent the rhythm accurately,
|
95
|
+
# including early and late beats.
|
96
|
+
def canonical
|
97
|
+
if duration != duration.to_i
|
98
|
+
raise "Cannot yet canonicalise rhythms with non-integer length"
|
99
|
+
end
|
100
|
+
|
101
|
+
# Figure out the timing offset we need to allow for,
|
102
|
+
# and space the beats enough to make it work.
|
103
|
+
timing_offset_denominators = self.beats.map do |beat|
|
104
|
+
beat.start_offset.rationalize.denominator
|
105
|
+
end
|
106
|
+
denominator = timing_offset_denominators.inject(1, &:lcm)
|
107
|
+
|
108
|
+
scaled_duration = duration * denominator
|
109
|
+
Array.new(scaled_duration).tap do |spaced_beats|
|
110
|
+
self.beats.each_with_index do |beat, index|
|
111
|
+
offset_index = index + beat.start_offset
|
112
|
+
scaled_index = offset_index * denominator
|
113
|
+
spaced_beats[scaled_index] = beat.amplitude
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def inspect
|
119
|
+
"<#Rhythm duration=#{duration} @beats=#{beats.inspect}>"
|
120
|
+
end
|
121
|
+
|
122
|
+
def == other
|
123
|
+
self.class == other.class &&
|
124
|
+
self.beats == other.beats
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# Converts note information into raw WAV file data
|
2
|
+
# Based on examples in {http://wavefilegem.com/examples}
|
3
|
+
|
4
|
+
require 'wavefile'
|
5
|
+
|
6
|
+
require 'inevitable_cacophony/note'
|
7
|
+
|
8
|
+
module InevitableCacophony
|
9
|
+
class ToneGenerator
|
10
|
+
|
11
|
+
SAMPLE_RATE = 44100 # Hertz
|
12
|
+
|
13
|
+
# One full revolution of a circle (or one full cycle of a sine wave)
|
14
|
+
TAU = Math::PI * 2
|
15
|
+
|
16
|
+
# Create a buffer representing a given phrase as an audio sample
|
17
|
+
#
|
18
|
+
# @param phrase [Phrase]
|
19
|
+
def phrase_buffer(phrase)
|
20
|
+
samples = phrase.notes.map { |note| note_samples(note, phrase.tempo) }
|
21
|
+
WaveFile::Buffer.new(samples.flatten, WaveFile::Format.new(:mono, :float, SAMPLE_RATE))
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_phrase(phrase)
|
25
|
+
@phrases << phrase_buffer(phrase)
|
26
|
+
end
|
27
|
+
|
28
|
+
def write(io)
|
29
|
+
WaveFile::Writer.new(io, WaveFile::Format.new(:mono, :pcm_16, SAMPLE_RATE)) do |writer|
|
30
|
+
@phrases.each do |phrase|
|
31
|
+
writer.write(phrase)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# @param tonic [Numeric] The tonic frequency, in Hertz
|
37
|
+
def initialize(tonic)
|
38
|
+
@tonic = tonic
|
39
|
+
@phrases = []
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
# Create a array of amplitudes representing a single note as a sample.
|
45
|
+
#
|
46
|
+
# @param note [Note]
|
47
|
+
# @param tempo [Numeric] Tempo in BPM to play the note at
|
48
|
+
# (exact duration will also depend on the beat).
|
49
|
+
def note_samples(note, tempo)
|
50
|
+
samples_per_wave = SAMPLE_RATE / (note.ratio.to_f * @tonic)
|
51
|
+
samples_per_beat = (60.0 / tempo) * SAMPLE_RATE
|
52
|
+
samples = []
|
53
|
+
|
54
|
+
start_delay = note.start_delay * samples_per_beat
|
55
|
+
after_delay = note.after_delay * samples_per_beat
|
56
|
+
note_length = (note.duration * samples_per_beat) - start_delay - after_delay
|
57
|
+
|
58
|
+
samples << ([0.0] * start_delay)
|
59
|
+
|
60
|
+
samples << note_length.to_i.times.map do |index|
|
61
|
+
wave_fraction = index / samples_per_wave.to_f
|
62
|
+
note.beat.amplitude * Math.sin(wave_fraction * TAU)
|
63
|
+
end
|
64
|
+
samples << ([0.0] * after_delay)
|
65
|
+
samples.flatten
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|