mtk 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/README.md +52 -0
  2. data/Rakefile +31 -0
  3. data/lib/mtk/chord.rb +47 -0
  4. data/lib/mtk/constants/dynamics.rb +56 -0
  5. data/lib/mtk/constants/intervals.rb +76 -0
  6. data/lib/mtk/constants/pitch_classes.rb +18 -0
  7. data/lib/mtk/constants/pitches.rb +24 -0
  8. data/lib/mtk/constants/pseudo_constants.rb +25 -0
  9. data/lib/mtk/event.rb +61 -0
  10. data/lib/mtk/midi/file.rb +179 -0
  11. data/lib/mtk/note.rb +44 -0
  12. data/lib/mtk/numeric_extensions.rb +61 -0
  13. data/lib/mtk/pattern/choice.rb +21 -0
  14. data/lib/mtk/pattern/note_sequence.rb +60 -0
  15. data/lib/mtk/pattern/pitch_sequence.rb +22 -0
  16. data/lib/mtk/pattern/sequence.rb +65 -0
  17. data/lib/mtk/patterns.rb +4 -0
  18. data/lib/mtk/pitch.rb +112 -0
  19. data/lib/mtk/pitch_class.rb +113 -0
  20. data/lib/mtk/pitch_class_set.rb +106 -0
  21. data/lib/mtk/pitch_set.rb +95 -0
  22. data/lib/mtk/timeline.rb +160 -0
  23. data/lib/mtk/util/mappable.rb +14 -0
  24. data/lib/mtk.rb +36 -0
  25. data/spec/mtk/chord_spec.rb +74 -0
  26. data/spec/mtk/constants/dynamics_spec.rb +94 -0
  27. data/spec/mtk/constants/intervals_spec.rb +140 -0
  28. data/spec/mtk/constants/pitch_classes_spec.rb +35 -0
  29. data/spec/mtk/constants/pitches_spec.rb +23 -0
  30. data/spec/mtk/event_spec.rb +120 -0
  31. data/spec/mtk/midi/file_spec.rb +208 -0
  32. data/spec/mtk/note_spec.rb +65 -0
  33. data/spec/mtk/numeric_extensions_spec.rb +102 -0
  34. data/spec/mtk/pattern/choice_spec.rb +21 -0
  35. data/spec/mtk/pattern/note_sequence_spec.rb +121 -0
  36. data/spec/mtk/pattern/pitch_sequence_spec.rb +47 -0
  37. data/spec/mtk/pattern/sequence_spec.rb +54 -0
  38. data/spec/mtk/pitch_class_set_spec.rb +103 -0
  39. data/spec/mtk/pitch_class_spec.rb +165 -0
  40. data/spec/mtk/pitch_set_spec.rb +163 -0
  41. data/spec/mtk/pitch_spec.rb +217 -0
  42. data/spec/mtk/timeline_spec.rb +234 -0
  43. data/spec/spec_helper.rb +7 -0
  44. data/spec/test.mid +0 -0
  45. metadata +97 -0
data/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # MTK
2
+ ## Music ToolKit for Ruby
3
+
4
+ Classes for modeling music with a focus on simplicity. Support for reading/writing MIDI files (and soon, realtime MIDI).
5
+
6
+
7
+
8
+ ## Goals
9
+
10
+ * Build musical generators to assist with composing music
11
+ * Re-implement Cosy (http://compusition.com/web/software/cosy) using these models as the "backend"
12
+
13
+
14
+
15
+ ## Status
16
+
17
+ Pre-alpha, API subject to change. Feedback welcome!
18
+
19
+
20
+
21
+ ## Requirements
22
+ ### Ruby Version
23
+
24
+ Ruby 1.8 or 1.9
25
+
26
+ ### Gem Dependencies
27
+
28
+ * rake (tests & docs)
29
+ * rspec (tests)
30
+ * yard (docs)
31
+ * yard-rspec (docs)
32
+ * rdiscount (docs)
33
+ * midilib (MIDI file I/O -- not strictly required by core lib, but currently needed for tests)
34
+
35
+
36
+
37
+ ## Documentation
38
+
39
+ rake yard
40
+
41
+ then open doc/frames.html
42
+
43
+
44
+
45
+ ## Tests
46
+
47
+ rake spec
48
+
49
+ I test with MRI 1.8.7, MRI 1.9.2, JRuby 1.5.6, and JRuby 1.6.1 on OS X via rvm:
50
+
51
+ rvm 1.8.7,1.9.2,jruby-1.5.6,jruby-1.6.1 rake spec:fast
52
+
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+ require 'rspec/core/rake_task'
2
+ require 'rake/clean'
3
+ require 'yard'
4
+
5
+ task :default => :spec
6
+
7
+ CLEAN.include('html','doc') # clean and clobber do the same thing for now
8
+
9
+ desc "Run RSpec tests with full output"
10
+ RSpec::Core::RakeTask.new do |spec|
11
+ spec.rspec_opts = ["--color", "--format", "nested"]
12
+ end
13
+
14
+ namespace :spec do
15
+ desc "Run RSpecs tests with summary output and fast failure"
16
+ RSpec::Core::RakeTask.new(:fast) do |spec|
17
+ spec.rspec_opts = ["--color", "--fail-fast"]
18
+ end
19
+ end
20
+
21
+ YARD::Rake::YardocTask.new do |yard|
22
+ yard.files = ['lib/**/*.rb', 'spec/**/*.rb']
23
+ yard.options = []
24
+ if File.exist? '../yard-spec-plugin/lib/yard-rspec.rb'
25
+ # prefer my local patched copy which can handle my rspec conventions better...
26
+ yard.options.concat ['-e' '../yard-spec-plugin/lib/yard-rspec.rb']
27
+ else
28
+ # use the gem
29
+ yard.options.concat ['-e' 'yard-rspec']
30
+ end
31
+ end
data/lib/mtk/chord.rb ADDED
@@ -0,0 +1,47 @@
1
+ module MTK
2
+
3
+ # A multi-pitch, note-like {Event} defined by a {PitchSet}, intensity, and duration
4
+ class Chord < Event
5
+
6
+ # the {PitchSet} of the chord
7
+ attr_reader :pitch_set
8
+
9
+ def initialize(pitches, intensity, duration)
10
+ @pitch_set = if pitches.is_a? PitchSet
11
+ pitches
12
+ else
13
+ PitchSet.new(pitches)
14
+ end
15
+ super(intensity, duration)
16
+ end
17
+
18
+ def self.from_hash(hash)
19
+ new hash[:pitch_set], hash[:intensity], hash[:duration]
20
+ end
21
+
22
+ def to_hash
23
+ super.merge({ :pitch_set => @pitch_set })
24
+ end
25
+
26
+ def pitches
27
+ @pitch_set.pitches
28
+ end
29
+
30
+ def transpose(interval)
31
+ self.class.new(@pitch_set + interval, @intensity, @duration)
32
+ end
33
+
34
+ def == other
35
+ super and other.respond_to? :pitch_set and @pitch_set == other.pitch_set
36
+ end
37
+
38
+ def to_s
39
+ "Chord(#{pitch_set}, #{super})"
40
+ end
41
+
42
+ def inspect
43
+ "Chord(#{pitch_set}, #{super})"
44
+ end
45
+ end
46
+
47
+ end
@@ -0,0 +1,56 @@
1
+ module MTK
2
+
3
+ # Defines values for standard dynamic symbols.
4
+ #
5
+ # These can be thought of like constants, but in order to distinguish 'f' (forte) from the {PitchClass} 'F'
6
+ # it was necessary to use lower-case names and therefore define them as "pseudo constant" methods.
7
+ # The methods are available either throught the module (MTK::Dynamics::f) or via mixin (include MTK::Dynamics; f)
8
+ #
9
+ # These values are intensities in the range 0.0 - 1.0, so they can be easily scaled (unlike MIDI velocities).
10
+ #
11
+ # @note Including this module shadows Ruby's built-in p() method.
12
+ # If you include this module, you can access the built-in p() method via Kernel.p()
13
+ #
14
+ # @see Note
15
+ module Dynamics
16
+ extend MTK::PseudoConstants
17
+
18
+ # NOTE: the yard doc macros here only fill in [$2] with the actual value when generating docs under Ruby 1.9+
19
+
20
+ # pianississimo
21
+ # @macro [attach] dynamics.define_constant
22
+ # @attribute [r]
23
+ # @return [$2] intensity value for $1
24
+ define_constant 'ppp', 0.125
25
+
26
+ # pianissimo
27
+ define_constant 'pp', 0.25
28
+
29
+ # piano
30
+ # @note Including this module shadows Ruby's built-in p() method.
31
+ # If you include this module, you can access the built-in p() method via Kernel.p()
32
+ define_constant 'p', 0.375
33
+
34
+ # mezzo-piano
35
+ define_constant 'mp', 0.5
36
+
37
+ # mezzo-forte
38
+ define_constant 'mf', 0.625
39
+
40
+ # forte
41
+ define_constant 'f', 0.75
42
+
43
+ # fortissimo
44
+ define_constant 'ff', 0.875
45
+
46
+ # fortississimo
47
+ define_constant 'fff', 1.0
48
+
49
+ def self.[](name)
50
+ send name
51
+ rescue
52
+ nil
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,76 @@
1
+ module MTK
2
+
3
+ # Defines a constant for intervals up to an octave using diatonic naming conventions (see http://en.wikipedia.org/wiki/Interval_(music)#Main_intervals)
4
+ #
5
+ # Naming conventions
6
+ # P#: perfect interval
7
+ # M#: major interval
8
+ # m#: minor interval
9
+ # TT: tritone (AKA augmented 4th or diminshed 5th)
10
+ #
11
+ # These can be thought of like constants, but in order to succintly distinguish 'm2' (minor) from 'M2' (major),
12
+ # it was necessary to use lower-case names for some of the values and therefore define them as "pseudo constant" methods.
13
+ # The methods are available either through the module (MTK::Intervals::m2) or via mixin (include MTK::Intervals; m2)
14
+ module Intervals
15
+ extend PseudoConstants
16
+
17
+ # NOTE: the yard doc macros here only fill in [$2] with the actual value when generating docs under Ruby 1.9+
18
+
19
+ # perfect unison
20
+ # @macro [attach] interval.define_constant
21
+ # @attribute [r]
22
+ # @return [$2] number of semitones in the interval $1
23
+ define_constant 'P1', 0
24
+
25
+ # minor second
26
+ # @macro [attach] interval.define_constant
27
+ # @attribute [r]
28
+ # @return [$2] number of semitones in the interval $1
29
+ define_constant 'm2', 1
30
+
31
+ # major second
32
+ define_constant 'M2', 2
33
+
34
+ # minor third
35
+ define_constant 'm3', 3
36
+
37
+ # major third
38
+ define_constant 'M3', 4
39
+
40
+ # pefect fourth
41
+ define_constant 'P4', 5
42
+
43
+ # tritone (AKA augmented fourth or diminished fifth)
44
+ define_constant 'TT', 6
45
+
46
+ # perfect fifth
47
+ define_constant 'P5', 7
48
+
49
+ # minor sixth
50
+ define_constant 'm6', 8
51
+
52
+ # major sixth
53
+ define_constant 'M6', 9
54
+
55
+ # minor seventh
56
+ define_constant 'm7', 10
57
+
58
+ # major seventh
59
+ define_constant 'M7', 11
60
+
61
+ # pefect octave
62
+ define_constant 'P8', 12
63
+
64
+ def self.[](name)
65
+ send name
66
+ rescue
67
+ begin
68
+ const_get name
69
+ rescue
70
+ nil
71
+ end
72
+ end
73
+
74
+ end
75
+
76
+ end
@@ -0,0 +1,18 @@
1
+ module MTK
2
+
3
+ # Defines a constant for each {PitchClass} in the Western chromatic scale.
4
+
5
+ module PitchClasses
6
+
7
+ # An array of all pitch class constants defined in this module
8
+ PITCH_CLASSES = []
9
+
10
+ for name in PitchClass::NAMES
11
+ pc = PitchClass.from_name name
12
+ PITCH_CLASSES << pc
13
+ const_set name, pc
14
+ end
15
+
16
+ end
17
+
18
+ end
@@ -0,0 +1,24 @@
1
+ module MTK
2
+
3
+ # Defines a constants for each {Pitch} in the standard MIDI range using scientific pitch notation.
4
+ #
5
+ # See http://en.wikipedia.org/wiki/Scientific_pitch_notation
6
+ #
7
+ # Note that because the character '#' cannot be used in the name of a constant,
8
+ # The "black key" pitches are all named as flats with 'b' (for example, Gb3 or Cb4)
9
+
10
+ module Pitches
11
+
12
+ # An array of all the pitch constants defined in this module
13
+ PITCHES = []
14
+
15
+ 128.times do |note_number|
16
+ pitch = Pitch.from_i( note_number )
17
+ PITCHES << pitch
18
+ octave_str = pitch.octave.to_s.sub(/-/,'_') # '_1' for -1
19
+ const_set "#{pitch.pitch_class}#{octave_str}", pitch
20
+ end
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,25 @@
1
+ module MTK
2
+
3
+ # Extension for modules that want to define pseudo-constants (constant-like values with lower-case names)
4
+ module PseudoConstants
5
+
6
+ # Define a "constant" as a module method and module function (available both through the module namespace and as a mixin method),
7
+ # in order to facility defining constant-like values with lower-case names.
8
+ #
9
+ # @param name [Symbol] the name of the pseudo-constant
10
+ # @param value [Object] the value of the pseudo-constant
11
+ # @return [nil]
12
+ def define_constant name, value
13
+ if name[0..0] =~ /[A-Z]/
14
+ const_set name, value # it's just a normal constant
15
+ else
16
+ # the pseudo-constant definition is the combination of a method and module_function:
17
+ define_method(name) { value }
18
+ module_function name
19
+ end
20
+ nil
21
+ end
22
+
23
+ end
24
+
25
+ end
data/lib/mtk/event.rb ADDED
@@ -0,0 +1,61 @@
1
+ module MTK
2
+
3
+ # An abstract musical event that has an intensity and a duration
4
+ # @abstract
5
+ class Event
6
+
7
+ # intensity of the note as a value in the range 0.0 - 1.0
8
+ attr_reader :intensity
9
+
10
+ # duration of the note in beats (e.g. 1.0 is a quarter note in 4/4 time signatures)
11
+ attr_reader :duration
12
+
13
+ def initialize(intensity, duration)
14
+ @intensity, @duration = intensity, duration
15
+ end
16
+
17
+ def self.from_hash(hash)
18
+ new hash[:intensity], hash[:duration]
19
+ end
20
+
21
+ def to_hash
22
+ { :intensity => @intensity, :duration => @duration }
23
+ end
24
+
25
+ def clone_with(hash)
26
+ self.class.from_hash(to_hash.merge hash)
27
+ end
28
+
29
+ def scale_intensity(scaling_factor)
30
+ clone_with :intensity => @intensity * scaling_factor.to_f
31
+ end
32
+
33
+ def scale_duration(scaling_factor)
34
+ clone_with :duration => @duration * scaling_factor.to_f
35
+ end
36
+
37
+ # intensity scaled to the MIDI range 0-127
38
+ def velocity
39
+ (127 * @intensity).round
40
+ end
41
+
42
+ def duration_in_pulses(pulses_per_beat)
43
+ (@duration * pulses_per_beat).round
44
+ end
45
+
46
+ def == other
47
+ other.respond_to? :intensity and @intensity == other.intensity and
48
+ other.respond_to? :duration and @duration == other.duration
49
+ end
50
+
51
+ def to_s
52
+ "#{sprintf '%.2f',@intensity}, #{sprintf '%.2f',@duration}"
53
+ end
54
+
55
+ def inspect
56
+ "#@intensity, #@duration"
57
+ end
58
+
59
+ end
60
+
61
+ end
@@ -0,0 +1,179 @@
1
+ require 'rubygems'
2
+ require 'midilib'
3
+
4
+ module MTK
5
+ module MIDI
6
+
7
+ class File
8
+ def initialize file
9
+ if file.respond_to? :path
10
+ @file = file.path
11
+ else
12
+ @file = file.to_s
13
+ end
14
+ end
15
+
16
+ # Read a MIDI file into an Array of {Timeline}s
17
+ #
18
+ # @param filepath [String, #path] path of the file to be written
19
+ # @return [Timeline]
20
+ #
21
+ def to_timelines
22
+ timelines = []
23
+
24
+ ::File.open(@file, 'rb') do |f|
25
+ sequence = ::MIDI::Sequence.new
26
+ sequence.read(f)
27
+ pulses_per_beat = sequence.ppqn.to_f
28
+ track_idx = -1
29
+
30
+ for track in sequence
31
+ track_idx += 1
32
+ timeline = Timeline.new
33
+ note_ons = {}
34
+ #puts "TRACK #{track_idx}"
35
+
36
+ for event in track
37
+ #puts "#{event.class}: #{event} @#{event.time_from_start}"
38
+ case event
39
+ when ::MIDI::NoteOn
40
+ note_ons[event.note] = event
41
+
42
+ when ::MIDI::NoteOff
43
+ if on_event = note_ons.delete(event.note)
44
+ time = (on_event.time_from_start)/pulses_per_beat
45
+ duration = (event.time_from_start - on_event.time_from_start)/pulses_per_beat
46
+ note = Note.from_midi event.note, on_event.velocity, duration
47
+ timeline.add time, note
48
+ end
49
+ end
50
+ end
51
+ timelines << timeline
52
+ end
53
+ end
54
+ timelines
55
+ end
56
+
57
+ def write(anything)
58
+ case anything
59
+ when Timeline then
60
+ write_timeline(anything)
61
+ when Array then
62
+ write_timelines(anything)
63
+ else
64
+ raise "#{self.class}#write doesn't understand #{anything.class}"
65
+ end
66
+ end
67
+
68
+ def write_timelines(timelines, parent_sequence=nil)
69
+ sequence = parent_sequence || ::MIDI::Sequence.new
70
+ for timeline in timelines
71
+ write_timeline(timeline, sequence)
72
+ end
73
+ write_to_disk sequence unless parent_sequence
74
+ end
75
+
76
+ # Write the Timeline as a MIDI file
77
+ #
78
+ # @param [Timeline]
79
+ def write_timeline(timeline, parent_sequence=nil)
80
+ sequence = parent_sequence || ::MIDI::Sequence.new
81
+ clock_rate = sequence.ppqn
82
+ track = add_track sequence
83
+ channel = 1
84
+
85
+ for time, event in timeline
86
+ time *= clock_rate
87
+ case event
88
+ when Note
89
+ pitch, velocity = event.pitch, event.velocity
90
+ add_event track, time => note_on(channel, pitch, velocity)
91
+ duration = event.duration_in_pulses(clock_rate)
92
+ add_event track, time+duration => note_off(channel, pitch, velocity)
93
+
94
+ when Chord
95
+ velocity = event.velocity
96
+ duration = event.duration_in_pulses(clock_rate)
97
+ for pitch in event.pitches
98
+ pitch = pitch.to_i
99
+ add_event track, time => note_on(channel, pitch, velocity)
100
+ add_event track, time+duration => note_off(channel, pitch, velocity)
101
+ end
102
+
103
+ end
104
+ end
105
+ track.recalc_delta_from_times
106
+
107
+ write_to_disk sequence unless parent_sequence
108
+ end
109
+
110
+
111
+ ########################
112
+ private
113
+
114
+ def write_to_disk(sequence)
115
+ ::File.open(@file, 'wb') { |f| sequence.write f }
116
+ end
117
+
118
+ def print_midi sequence
119
+ for track in sequence
120
+ puts "\n*** track \"#{track.name}\""
121
+ puts "#{track.events.length} events"
122
+ for event in track
123
+ puts "#{event.to_s} (#{event.time_from_start})"
124
+ end
125
+ end
126
+ end
127
+
128
+ # Set tempo in terms of Quarter Notes per Minute (aka BPM)
129
+ def tempo(bpm)
130
+ ms_per_quarter_note = ::MIDI::Tempo.bpm_to_mpq(bpm)
131
+ ::MIDI::Tempo.new(ms_per_quarter_note)
132
+ end
133
+
134
+ def program(program_number)
135
+ ::MIDI::ProgramChange.new(channel, program_number)
136
+ end
137
+
138
+ def note_on(channel, pitch, velocity)
139
+ ::MIDI::NoteOn.new(channel, pitch.to_i, velocity)
140
+ end
141
+
142
+ def note_off(channel, pitch, velocity)
143
+ ::MIDI::NoteOff.new(channel, pitch.to_i, velocity)
144
+ end
145
+
146
+ def cc(channel, controller, value)
147
+ ::MIDI::Controller.new(channel, controller.to_i, value.to_i)
148
+ end
149
+
150
+ def pitch_bend(channel, value)
151
+ ::MIDI::PitchBend.new(channel, value)
152
+ end
153
+
154
+ def add_track sequence, opts={}
155
+ track = ::MIDI::Track.new(sequence)
156
+ track.name = opts.fetch :name, ''
157
+ sequence.tracks << track
158
+ track
159
+ end
160
+
161
+ def add_event track, event_hash
162
+ for time, event in event_hash
163
+ event.time_from_start = time
164
+ track.events << event
165
+ event
166
+ end
167
+ end
168
+
169
+ end
170
+
171
+ end
172
+
173
+ def MIDI_File(f)
174
+ MIDI::File.new(f)
175
+ end
176
+ module_function :MIDI_File
177
+
178
+ end
179
+
data/lib/mtk/note.rb ADDED
@@ -0,0 +1,44 @@
1
+ module MTK
2
+
3
+ # A musical #{Event} defined by a {Pitch}, intensity, and duration
4
+ class Note < Event
5
+
6
+ # frequency of the note as a Pitch
7
+ attr_reader :pitch
8
+
9
+ def initialize(pitch, intensity, duration)
10
+ @pitch = pitch
11
+ super(intensity, duration)
12
+ end
13
+
14
+ def self.from_hash(hash)
15
+ new hash[:pitch], hash[:intensity], hash[:duration]
16
+ end
17
+
18
+ def self.from_midi(pitch, velocity, beats)
19
+ new Pitches::PITCHES[pitch], velocity/127.0, beats
20
+ end
21
+
22
+ def to_hash
23
+ super.merge({ :pitch => @pitch })
24
+ end
25
+
26
+ def transpose(interval)
27
+ self.class.new(@pitch+interval, @intensity, @duration)
28
+ end
29
+
30
+ def == other
31
+ super and other.respond_to? :pitch and @pitch == other.pitch
32
+ end
33
+
34
+ def to_s
35
+ "Note(#{pitch}, #{super})"
36
+ end
37
+
38
+ def inspect
39
+ "Note(#{pitch}, #{super})"
40
+ end
41
+
42
+ end
43
+
44
+ end
@@ -0,0 +1,61 @@
1
+ class Numeric
2
+
3
+ def semitones
4
+ self
5
+ end
6
+
7
+ def cents
8
+ self/100.0
9
+ end
10
+
11
+ def minor_seconds
12
+ self
13
+ end
14
+
15
+ def major_seconds
16
+ self * 2
17
+ end
18
+
19
+ def minor_thirds
20
+ self * 3
21
+ end
22
+
23
+ def major_thirds
24
+ self * 4
25
+ end
26
+
27
+ def perfect_fourths
28
+ self * 5
29
+ end
30
+
31
+ def tritones
32
+ self * 6
33
+ end
34
+ alias augmented_fourths tritones
35
+ alias diminshed_fifths tritones
36
+
37
+ def perfect_fifths
38
+ self * 7
39
+ end
40
+
41
+ def minor_sixths
42
+ self * 8
43
+ end
44
+
45
+ def major_sixths
46
+ self * 9
47
+ end
48
+
49
+ def minor_sevenths
50
+ self * 10
51
+ end
52
+
53
+ def major_sevenths
54
+ self * 11
55
+ end
56
+
57
+ def octaves
58
+ self * 12
59
+ end
60
+
61
+ end
@@ -0,0 +1,21 @@
1
+ module MTK
2
+ module Pattern
3
+
4
+ # An element enumerator that randomly choices from a list of elements
5
+ class Choice
6
+
7
+ # The element choices
8
+ attr_reader :elements
9
+
10
+ def initialize(elements)
11
+ @elements = elements
12
+ end
13
+
14
+ def next
15
+ @elements[rand(@elements.length)] if @elements and not @elements.empty?
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+ end