mtk 0.0.1
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.
- data/README.md +52 -0
- data/Rakefile +31 -0
- data/lib/mtk/chord.rb +47 -0
- data/lib/mtk/constants/dynamics.rb +56 -0
- data/lib/mtk/constants/intervals.rb +76 -0
- data/lib/mtk/constants/pitch_classes.rb +18 -0
- data/lib/mtk/constants/pitches.rb +24 -0
- data/lib/mtk/constants/pseudo_constants.rb +25 -0
- data/lib/mtk/event.rb +61 -0
- data/lib/mtk/midi/file.rb +179 -0
- data/lib/mtk/note.rb +44 -0
- data/lib/mtk/numeric_extensions.rb +61 -0
- data/lib/mtk/pattern/choice.rb +21 -0
- data/lib/mtk/pattern/note_sequence.rb +60 -0
- data/lib/mtk/pattern/pitch_sequence.rb +22 -0
- data/lib/mtk/pattern/sequence.rb +65 -0
- data/lib/mtk/patterns.rb +4 -0
- data/lib/mtk/pitch.rb +112 -0
- data/lib/mtk/pitch_class.rb +113 -0
- data/lib/mtk/pitch_class_set.rb +106 -0
- data/lib/mtk/pitch_set.rb +95 -0
- data/lib/mtk/timeline.rb +160 -0
- data/lib/mtk/util/mappable.rb +14 -0
- data/lib/mtk.rb +36 -0
- data/spec/mtk/chord_spec.rb +74 -0
- data/spec/mtk/constants/dynamics_spec.rb +94 -0
- data/spec/mtk/constants/intervals_spec.rb +140 -0
- data/spec/mtk/constants/pitch_classes_spec.rb +35 -0
- data/spec/mtk/constants/pitches_spec.rb +23 -0
- data/spec/mtk/event_spec.rb +120 -0
- data/spec/mtk/midi/file_spec.rb +208 -0
- data/spec/mtk/note_spec.rb +65 -0
- data/spec/mtk/numeric_extensions_spec.rb +102 -0
- data/spec/mtk/pattern/choice_spec.rb +21 -0
- data/spec/mtk/pattern/note_sequence_spec.rb +121 -0
- data/spec/mtk/pattern/pitch_sequence_spec.rb +47 -0
- data/spec/mtk/pattern/sequence_spec.rb +54 -0
- data/spec/mtk/pitch_class_set_spec.rb +103 -0
- data/spec/mtk/pitch_class_spec.rb +165 -0
- data/spec/mtk/pitch_set_spec.rb +163 -0
- data/spec/mtk/pitch_spec.rb +217 -0
- data/spec/mtk/timeline_spec.rb +234 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/test.mid +0 -0
- 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
|