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.
@@ -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
@@ -0,0 +1,3 @@
1
+ module InevitableCacophony
2
+ VERSION = '0.0.0'
3
+ end