mtk 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|