mtk 0.0.1 → 0.0.2
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/.yardopts +9 -0
- data/INTRO.md +73 -0
- data/LICENSE.txt +27 -0
- data/README.md +93 -18
- data/Rakefile +13 -1
- data/examples/crescendo.rb +20 -0
- data/examples/dynamic_pattern.rb +39 -0
- data/examples/play_midi.rb +19 -0
- data/examples/print_midi.rb +13 -0
- data/examples/random_tone_row.rb +18 -0
- data/examples/tone_row_melody.rb +21 -0
- data/lib/mtk/_constants/durations.rb +80 -0
- data/lib/mtk/_constants/intensities.rb +81 -0
- data/lib/mtk/{constants → _constants}/intervals.rb +10 -1
- data/lib/mtk/_constants/pitch_classes.rb +35 -0
- data/lib/mtk/_constants/pitches.rb +49 -0
- data/lib/mtk/{numeric_extensions.rb → _numeric_extensions.rb} +0 -0
- data/lib/mtk/event.rb +14 -5
- data/lib/mtk/helper/collection.rb +114 -0
- data/lib/mtk/helper/event_builder.rb +85 -0
- data/lib/mtk/{constants → helper}/pseudo_constants.rb +7 -6
- data/lib/mtk/lang/grammar.rb +17 -0
- data/lib/mtk/lang/mtk_grammar.citrus +60 -0
- data/lib/mtk/midi/file.rb +10 -15
- data/lib/mtk/midi/jsound_input.rb +68 -0
- data/lib/mtk/midi/jsound_output.rb +80 -0
- data/lib/mtk/note.rb +22 -3
- data/lib/mtk/pattern/abstract_pattern.rb +132 -0
- data/lib/mtk/pattern/choice.rb +25 -9
- data/lib/mtk/pattern/cycle.rb +51 -0
- data/lib/mtk/pattern/enumerator.rb +26 -0
- data/lib/mtk/pattern/function.rb +46 -0
- data/lib/mtk/pattern/lines.rb +60 -0
- data/lib/mtk/pattern/palindrome.rb +42 -0
- data/lib/mtk/pattern/sequence.rb +15 -50
- data/lib/mtk/pitch.rb +45 -6
- data/lib/mtk/pitch_class.rb +36 -35
- data/lib/mtk/pitch_class_set.rb +46 -14
- data/lib/mtk/pitch_set.rb +20 -31
- data/lib/mtk/sequencer/abstract_sequencer.rb +85 -0
- data/lib/mtk/sequencer/rhythmic_sequencer.rb +29 -0
- data/lib/mtk/sequencer/step_sequencer.rb +26 -0
- data/lib/mtk/timeline.rb +75 -22
- data/lib/mtk/transform/invertible.rb +15 -0
- data/lib/mtk/{util → transform}/mappable.rb +6 -2
- data/lib/mtk/transform/set_theory_operations.rb +34 -0
- data/lib/mtk/transform/transposable.rb +14 -0
- data/lib/mtk.rb +56 -22
- data/spec/mtk/_constants/durations_spec.rb +118 -0
- data/spec/mtk/{constants/dynamics_spec.rb → _constants/intensities_spec.rb} +48 -17
- data/spec/mtk/{constants → _constants}/intervals_spec.rb +21 -0
- data/spec/mtk/_constants/pitch_classes_spec.rb +58 -0
- data/spec/mtk/_constants/pitches_spec.rb +52 -0
- data/spec/mtk/{numeric_extensions_spec.rb → _numeric_extensions_spec.rb} +0 -0
- data/spec/mtk/event_spec.rb +19 -0
- data/spec/mtk/helper/collection_spec.rb +291 -0
- data/spec/mtk/helper/event_builder_spec.rb +92 -0
- data/spec/mtk/helper/pseudo_constants_spec.rb +20 -0
- data/spec/mtk/lang/grammar_spec.rb +100 -0
- data/spec/mtk/midi/file_spec.rb +41 -6
- data/spec/mtk/note_spec.rb +53 -3
- data/spec/mtk/pattern/abstract_pattern_spec.rb +45 -0
- data/spec/mtk/pattern/choice_spec.rb +89 -3
- data/spec/mtk/pattern/cycle_spec.rb +133 -0
- data/spec/mtk/pattern/function_spec.rb +133 -0
- data/spec/mtk/pattern/lines_spec.rb +93 -0
- data/spec/mtk/pattern/note_cycle_spec.rb.bak +116 -0
- data/spec/mtk/pattern/palindrome_spec.rb +124 -0
- data/spec/mtk/pattern/pitch_cycle_spec.rb.bak +47 -0
- data/spec/mtk/pattern/pitch_sequence_spec.rb.bak +37 -0
- data/spec/mtk/pattern/sequence_spec.rb +128 -31
- data/spec/mtk/pitch_class_set_spec.rb +240 -7
- data/spec/mtk/pitch_class_spec.rb +84 -18
- data/spec/mtk/pitch_set_spec.rb +45 -10
- data/spec/mtk/pitch_spec.rb +59 -0
- data/spec/mtk/sequencer/abstract_sequencer_spec.rb +159 -0
- data/spec/mtk/sequencer/rhythmic_sequencer_spec.rb +49 -0
- data/spec/mtk/sequencer/step_sequencer_spec.rb +71 -0
- data/spec/mtk/timeline_spec.rb +118 -15
- data/spec/spec_helper.rb +4 -3
- metadata +59 -22
- data/lib/mtk/chord.rb +0 -47
- data/lib/mtk/constants/dynamics.rb +0 -56
- data/lib/mtk/constants/pitch_classes.rb +0 -18
- data/lib/mtk/constants/pitches.rb +0 -24
- data/lib/mtk/pattern/note_sequence.rb +0 -60
- data/lib/mtk/pattern/pitch_sequence.rb +0 -22
- data/lib/mtk/patterns.rb +0 -4
- data/spec/mtk/chord_spec.rb +0 -74
- data/spec/mtk/constants/pitch_classes_spec.rb +0 -35
- data/spec/mtk/constants/pitches_spec.rb +0 -23
- data/spec/mtk/pattern/note_sequence_spec.rb +0 -121
- data/spec/mtk/pattern/pitch_sequence_spec.rb +0 -47
@@ -0,0 +1,49 @@
|
|
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 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
|
+
# @note Because the character '-' (minus) cannot be used in the name of a constant,
|
10
|
+
# the low pitches use '_' (underscore) in place of '-' (minus) (for example C_1).
|
11
|
+
module Pitches
|
12
|
+
|
13
|
+
# The values of all "psuedo constants" defined in this module
|
14
|
+
PITCHES = []
|
15
|
+
|
16
|
+
# The names of all "psuedo constants" defined in this module
|
17
|
+
PITCH_NAMES = []
|
18
|
+
|
19
|
+
128.times do |note_number|
|
20
|
+
pitch = Pitch.from_i( note_number )
|
21
|
+
PITCHES << pitch
|
22
|
+
|
23
|
+
octave_str = pitch.octave.to_s.sub(/-/,'_') # '_1' for -1
|
24
|
+
name = "#{pitch.pitch_class}#{octave_str}"
|
25
|
+
PITCH_NAMES << name
|
26
|
+
|
27
|
+
const_set name, pitch
|
28
|
+
end
|
29
|
+
|
30
|
+
PITCHES.freeze
|
31
|
+
PITCH_NAMES.freeze
|
32
|
+
|
33
|
+
# Lookup the value of an pitch constant by name.
|
34
|
+
# @example lookup value of 'C3'
|
35
|
+
# MTK::Pitches['C3']
|
36
|
+
# @see Pitch.from_s
|
37
|
+
# @note Unlike {Pitch.from_s} this method will accept either '_' (underscore) or '-' (minus) and treat it like '-' (minus)
|
38
|
+
# @note Unlike {Pitch.from_s} this method only accepts the accidental 'b'
|
39
|
+
def self.[](name)
|
40
|
+
begin
|
41
|
+
const_get name.sub('-','_')
|
42
|
+
rescue
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
File without changes
|
data/lib/mtk/event.rb
CHANGED
@@ -7,9 +7,6 @@ module MTK
|
|
7
7
|
# intensity of the note as a value in the range 0.0 - 1.0
|
8
8
|
attr_reader :intensity
|
9
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
10
|
def initialize(intensity, duration)
|
14
11
|
@intensity, @duration = intensity, duration
|
15
12
|
end
|
@@ -36,11 +33,23 @@ module MTK
|
|
36
33
|
|
37
34
|
# intensity scaled to the MIDI range 0-127
|
38
35
|
def velocity
|
39
|
-
(127 * @intensity).round
|
36
|
+
@velocity ||= (127 * @intensity).round
|
37
|
+
end
|
38
|
+
|
39
|
+
# Duration of the Event in beats (e.g. 1.0 is a quarter note in 4/4 time signatures)
|
40
|
+
# This is the absolute value of the duration attribute used to construct the object.
|
41
|
+
# @see rest?
|
42
|
+
def duration
|
43
|
+
@abs_duration ||= @duration.abs
|
44
|
+
end
|
45
|
+
|
46
|
+
# By convention, any events with negative durations are considered a rest
|
47
|
+
def rest?
|
48
|
+
@duration < 0
|
40
49
|
end
|
41
50
|
|
42
51
|
def duration_in_pulses(pulses_per_beat)
|
43
|
-
(
|
52
|
+
@duration_in_pulses ||= (duration * pulses_per_beat).round
|
44
53
|
end
|
45
54
|
|
46
55
|
def == other
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module MTK::Helper
|
2
|
+
|
3
|
+
# Given a method #elements, which returns an Array of elements in the collection,
|
4
|
+
# including this module will make the class Enumerable and provide various methods you'd expect from an Array.
|
5
|
+
module Collection
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
# A mutable array of elements in this collection
|
9
|
+
def to_a
|
10
|
+
Array.new(elements) # we construct a new array since some including classes make elements be immutable
|
11
|
+
end
|
12
|
+
|
13
|
+
# The number of elements in this collection
|
14
|
+
def size
|
15
|
+
elements.size
|
16
|
+
end
|
17
|
+
alias length size
|
18
|
+
|
19
|
+
def empty?
|
20
|
+
elements.nil? or elements.size == 0
|
21
|
+
end
|
22
|
+
|
23
|
+
# The each iterator for providing Enumerable functionality
|
24
|
+
def each &block
|
25
|
+
elements.each &block
|
26
|
+
end
|
27
|
+
|
28
|
+
# The first element
|
29
|
+
def first(n=nil)
|
30
|
+
n ? elements.first(n) : elements.first
|
31
|
+
end
|
32
|
+
|
33
|
+
# The last element
|
34
|
+
def last(n=nil)
|
35
|
+
n ? elements.last(n) : elements.last
|
36
|
+
end
|
37
|
+
|
38
|
+
# The element with the given index
|
39
|
+
def [](index)
|
40
|
+
elements[index]
|
41
|
+
end
|
42
|
+
|
43
|
+
def repeat(times=2)
|
44
|
+
full_repetitions, fractional_repetitions = times.floor, times%1 # split into int and fractional part
|
45
|
+
repeated = elements * full_repetitions
|
46
|
+
repeated += elements[0...elements.size*fractional_repetitions]
|
47
|
+
clone_with repeated
|
48
|
+
end
|
49
|
+
|
50
|
+
def permute
|
51
|
+
clone_with elements.shuffle
|
52
|
+
end
|
53
|
+
alias shuffle permute
|
54
|
+
|
55
|
+
def rotate(offset=1)
|
56
|
+
clone_with elements.rotate(offset)
|
57
|
+
end
|
58
|
+
|
59
|
+
def concat(other)
|
60
|
+
other_elements = (other.respond_to? :elements) ? other.elements : other
|
61
|
+
clone_with(elements + other_elements)
|
62
|
+
end
|
63
|
+
|
64
|
+
def reverse
|
65
|
+
clone_with elements.reverse
|
66
|
+
end
|
67
|
+
alias retrograde reverse
|
68
|
+
|
69
|
+
def ==(other)
|
70
|
+
if other.respond_to? :elements
|
71
|
+
if other.respond_to? :options
|
72
|
+
elements == other.elements and @options == other.options
|
73
|
+
else
|
74
|
+
elements == other.elements
|
75
|
+
end
|
76
|
+
else
|
77
|
+
elements == other
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Create a copy of the collection.
|
82
|
+
# In order to use this method, the including class must implement .from_a()
|
83
|
+
def clone
|
84
|
+
clone_with to_a
|
85
|
+
end
|
86
|
+
|
87
|
+
#################################
|
88
|
+
private
|
89
|
+
|
90
|
+
# "clones" the object with the given elements, attempting to maintain any @options
|
91
|
+
# This is designed to work with 2 argument constructors: def initialize(elements, options={})
|
92
|
+
def clone_with elements
|
93
|
+
from_a = self.class.method(:from_a)
|
94
|
+
if from_a.arity == -2
|
95
|
+
from_a[elements, (@options || {})]
|
96
|
+
else
|
97
|
+
from_a[elements]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
if not Array.instance_methods.include? :rotate
|
106
|
+
# Array#rotate is only available in Ruby 1.9, so we may have to implement it
|
107
|
+
class Array
|
108
|
+
def rotate(offset=1)
|
109
|
+
return self if empty?
|
110
|
+
offset %= length
|
111
|
+
self[offset..-1]+self[0...offset]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module MTK
|
2
|
+
module Helper
|
3
|
+
|
4
|
+
# A helper class for {Sequencer}s.
|
5
|
+
# Takes a list of patterns and constructs a list of {Event}s from the next elements in each pattern.
|
6
|
+
class EventBuilder
|
7
|
+
|
8
|
+
DEFAULT_PITCH = MTK::Pitches::C4
|
9
|
+
DEFAULT_INTENSITY = MTK::Intensities::f
|
10
|
+
DEFAULT_DURATION = 1
|
11
|
+
|
12
|
+
def initialize(patterns, options={})
|
13
|
+
@patterns = patterns
|
14
|
+
@options = options
|
15
|
+
@max_interval = options.fetch :max_interval, 12
|
16
|
+
rewind
|
17
|
+
end
|
18
|
+
|
19
|
+
# Build a list of events from the next element in each {Pattern}
|
20
|
+
# @return [Array] an array of events
|
21
|
+
def next
|
22
|
+
pitches = []
|
23
|
+
intensity = @default_intensity
|
24
|
+
duration = @default_duration
|
25
|
+
|
26
|
+
for pattern in @patterns
|
27
|
+
element = pattern.next
|
28
|
+
case element
|
29
|
+
when Pitch then pitches << element
|
30
|
+
when PitchSet then pitches += element.pitches
|
31
|
+
when PitchClass then pitches += pitches_for_pitch_classes([element], @previous_pitch || @default_pitch)
|
32
|
+
when PitchClassSet then pitches += pitches_for_pitch_classes(element, @previous_pitch || @default_pitch)
|
33
|
+
else case pattern.type
|
34
|
+
when :pitch
|
35
|
+
if element.is_a? Numeric
|
36
|
+
# pitch interval case
|
37
|
+
if @previous_pitches
|
38
|
+
pitches += @previous_pitches.map{|pitch| pitch + element }
|
39
|
+
else
|
40
|
+
pitches << ((@previous_pitch || @default_pitch) + element)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
when :intensity then intensity = element
|
44
|
+
when :duration then duration = element
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
if not pitches.empty?
|
50
|
+
@previous_pitch = pitches.last
|
51
|
+
@previous_pitches = pitches.length > 1 ? pitches : nil
|
52
|
+
pitches.map{|pitch| Note(pitch,intensity,duration) }
|
53
|
+
else
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Reset the EventBuilder to its initial state
|
59
|
+
def rewind
|
60
|
+
@default_pitch = @options.fetch :default_pitch, DEFAULT_PITCH
|
61
|
+
@default_intensity = @options.fetch :default_intensity, DEFAULT_INTENSITY
|
62
|
+
@default_duration = @options.fetch :default_duration, DEFAULT_DURATION
|
63
|
+
@previous_pitch = nil
|
64
|
+
@previous_pitches = nil
|
65
|
+
@patterns.each{|pattern| pattern.rewind }
|
66
|
+
end
|
67
|
+
|
68
|
+
########################
|
69
|
+
private
|
70
|
+
|
71
|
+
def pitches_for_pitch_classes(pitch_classes, previous_pitch)
|
72
|
+
pitches = []
|
73
|
+
for pitch_class in pitch_classes
|
74
|
+
pitch = previous_pitch.nearest(pitch_class)
|
75
|
+
pitch -= 12 if pitch > @default_pitch+@max_interval # keep within max_distance of start (default is one octave)
|
76
|
+
pitch += 12 if pitch < @default_pitch-@max_interval
|
77
|
+
pitches << pitch
|
78
|
+
end
|
79
|
+
pitches
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
@@ -1,10 +1,11 @@
|
|
1
|
-
module MTK
|
2
|
-
|
1
|
+
module MTK::Helper
|
2
|
+
|
3
|
+
|
3
4
|
# Extension for modules that want to define pseudo-constants (constant-like values with lower-case names)
|
4
5
|
module PseudoConstants
|
5
|
-
|
6
|
-
# Define a
|
7
|
-
#
|
6
|
+
|
7
|
+
# Define a module method and module function (available both through the module namespace and as a mixin method),
|
8
|
+
# to provide a constant with a lower-case name.
|
8
9
|
#
|
9
10
|
# @param name [Symbol] the name of the pseudo-constant
|
10
11
|
# @param value [Object] the value of the pseudo-constant
|
@@ -21,5 +22,5 @@ module MTK
|
|
21
22
|
end
|
22
23
|
|
23
24
|
end
|
24
|
-
|
25
|
+
|
25
26
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'citrus'
|
2
|
+
Citrus.load File.join(File.dirname(__FILE__),'mtk_grammar')
|
3
|
+
|
4
|
+
module MTK
|
5
|
+
module Lang
|
6
|
+
|
7
|
+
# Parser for the {file:lib/mtk/lang/mtk_grammar.citrus MTK grammar}
|
8
|
+
class Grammar
|
9
|
+
|
10
|
+
def self.parse(syntax, root=:pitch)
|
11
|
+
MTK_Grammar.parse(syntax, :root => root).value
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
grammar MTK_Grammar
|
2
|
+
include MTK
|
3
|
+
|
4
|
+
rule pitch_sequence
|
5
|
+
( pitch (space pitch)* ) {
|
6
|
+
Pattern.PitchSequence *captures[:pitch].map{|p| p.value }
|
7
|
+
}
|
8
|
+
end
|
9
|
+
|
10
|
+
rule pitch
|
11
|
+
( pitch_class int ) {
|
12
|
+
Pitch[pitch_class.value, int.value]
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
rule pitch_class
|
17
|
+
( [A-Ga-g] [#b]*2 ) {
|
18
|
+
PitchClass[to_s]
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
rule interval
|
23
|
+
( 'P' [1458] | [Mm] [2367] | 'TT' ) {
|
24
|
+
Intervals[to_s]
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
rule intensity
|
29
|
+
( ('p'1*3 | 'mp' | 'mf' | 'f'1*3) ('+'|'-')? ) {
|
30
|
+
Intensities[to_s]
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
rule duration
|
35
|
+
( [whqesrx] ('.'|'t')* ) {
|
36
|
+
Durations[to_s]
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
rule number
|
41
|
+
float | int
|
42
|
+
end
|
43
|
+
|
44
|
+
rule float
|
45
|
+
( '-'? [0-9]+ '.' [0-9]+ ) {
|
46
|
+
to_f
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
rule int
|
51
|
+
( '-'? [0-9]+ ) {
|
52
|
+
to_i
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
rule space
|
57
|
+
[\s]+ { nil }
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
data/lib/mtk/midi/file.rb
CHANGED
@@ -82,24 +82,18 @@ module MTK
|
|
82
82
|
track = add_track sequence
|
83
83
|
channel = 1
|
84
84
|
|
85
|
-
for time,
|
85
|
+
for time,events in timeline
|
86
86
|
time *= clock_rate
|
87
|
-
|
88
|
-
|
87
|
+
|
88
|
+
for event in events
|
89
|
+
next if event.rest?
|
90
|
+
|
91
|
+
if event.is_a? Note
|
89
92
|
pitch, velocity = event.pitch, event.velocity
|
90
93
|
add_event track, time => note_on(channel, pitch, velocity)
|
91
94
|
duration = event.duration_in_pulses(clock_rate)
|
92
95
|
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
|
-
|
96
|
+
end
|
103
97
|
end
|
104
98
|
end
|
105
99
|
track.recalc_delta_from_times
|
@@ -160,16 +154,17 @@ module MTK
|
|
160
154
|
|
161
155
|
def add_event track, event_hash
|
162
156
|
for time, event in event_hash
|
163
|
-
event.time_from_start = time
|
157
|
+
event.time_from_start = time.round # MIDI file event times must be in whole number pulses (typically 480 or 960 per quarter note)
|
164
158
|
track.events << event
|
165
159
|
event
|
166
160
|
end
|
167
161
|
end
|
168
162
|
|
169
163
|
end
|
170
|
-
|
171
164
|
end
|
172
165
|
|
166
|
+
# Shortcut for MTK::MIDI::File.new
|
167
|
+
# @note Only available if you require 'mtk/midi/file'
|
173
168
|
def MIDI_File(f)
|
174
169
|
MIDI::File.new(f)
|
175
170
|
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'jsound'
|
3
|
+
|
4
|
+
module MTK
|
5
|
+
module MIDI
|
6
|
+
|
7
|
+
# Provides MIDI input for JRuby via the jsound gem
|
8
|
+
class JSoundInput
|
9
|
+
|
10
|
+
def initialize(input_device)
|
11
|
+
if input_device.is_a? ::JSound::Midi::Device
|
12
|
+
@input = input_device
|
13
|
+
else
|
14
|
+
@input = ::JSound::Midi::INPUTS.send input_device
|
15
|
+
end
|
16
|
+
@recorder = ::JSound::Midi::Devices::Recorder.new(false)
|
17
|
+
@input >> @recorder
|
18
|
+
end
|
19
|
+
|
20
|
+
def record
|
21
|
+
@input.open
|
22
|
+
@recorder.clear
|
23
|
+
@recorder.start
|
24
|
+
end
|
25
|
+
|
26
|
+
def stop
|
27
|
+
@recorder.stop
|
28
|
+
@input.close
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_timeline(options={})
|
32
|
+
bpm = options.fetch :bmp, 120
|
33
|
+
beats_per_second = bpm.to_f/60
|
34
|
+
timeline = Timeline.new
|
35
|
+
note_ons = {}
|
36
|
+
start = nil
|
37
|
+
|
38
|
+
for message,time in @recorder.messages_with_timestamps
|
39
|
+
start = time unless start
|
40
|
+
time -= start
|
41
|
+
time /= beats_per_second
|
42
|
+
|
43
|
+
case message.type
|
44
|
+
when :note_on
|
45
|
+
note_ons[message.pitch] = [message,time]
|
46
|
+
|
47
|
+
when :note_off
|
48
|
+
if note_ons.has_key? message.pitch
|
49
|
+
note_on, start_time = note_ons[message.pitch]
|
50
|
+
duration = time - start_time
|
51
|
+
note = Note.from_midi note_on.pitch, note_on.velocity, duration
|
52
|
+
timeline.add time,note
|
53
|
+
end
|
54
|
+
|
55
|
+
else timeline.add time,message
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
timeline.quantize! options[:quantize] if options.key? :quantize
|
60
|
+
timeline.shift_to! options[:shift_to] if options.key? :shift_to
|
61
|
+
|
62
|
+
timeline
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'jsound'
|
3
|
+
require 'gamelan'
|
4
|
+
|
5
|
+
module MTK
|
6
|
+
module MIDI
|
7
|
+
|
8
|
+
# Provides MIDI output for JRuby via the jsound gem
|
9
|
+
class JSoundOutput
|
10
|
+
|
11
|
+
attr_reader :device
|
12
|
+
|
13
|
+
def initialize(output, options={})
|
14
|
+
if output.is_a? ::JSound::Midi::Device
|
15
|
+
@device = output
|
16
|
+
else
|
17
|
+
@device = ::JSound::Midi::OUTPUTS.send output
|
18
|
+
end
|
19
|
+
|
20
|
+
@generator = ::JSound::Midi::Devices::Generator.new
|
21
|
+
if options[:monitor]
|
22
|
+
@monitor = ::JSound::Midi::Devices::Monitor.new
|
23
|
+
@generator >> [@monitor, @device]
|
24
|
+
else
|
25
|
+
@generator >> @device
|
26
|
+
end
|
27
|
+
@device.open
|
28
|
+
end
|
29
|
+
|
30
|
+
def play(timeline, options={})
|
31
|
+
scheduler_rate = options.fetch :scheduler_rate, 500 # default: 500 Hz
|
32
|
+
trailing_buffer = options.fetch :trailing_buffer, 2 # default: continue playing for 2 beats after the end of the timeline
|
33
|
+
in_background = options.fetch :background, false # default: don't run in background Thread
|
34
|
+
bpm = options.fetch :bmp, 120 # default: 120 beats per minute
|
35
|
+
|
36
|
+
@scheduler = Gamelan::Scheduler.new :tempo => bpm, :rate => scheduler_rate
|
37
|
+
|
38
|
+
for time,events in timeline
|
39
|
+
for event in events
|
40
|
+
case event
|
41
|
+
when Note
|
42
|
+
pitch, velocity, duration = event.to_midi
|
43
|
+
at time, note_on(pitch,velocity)
|
44
|
+
time += duration
|
45
|
+
at time, note_off(pitch,velocity)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end_time = timeline.times.last + trailing_buffer
|
51
|
+
@scheduler.at(end_time) { @scheduler.stop }
|
52
|
+
|
53
|
+
thread = @scheduler.run
|
54
|
+
thread.join if not in_background
|
55
|
+
end
|
56
|
+
|
57
|
+
######################
|
58
|
+
private
|
59
|
+
|
60
|
+
# It's necessary to generate the events through methods and lambdas like this to create closures.
|
61
|
+
# Otherwise when the @generator methods are called, they might not be passed the values you expected.
|
62
|
+
# I suspect this may not a problem in MRI ruby, but I'm having trouble in JRuby
|
63
|
+
# (pitch and velocity were always the last scheduled values)
|
64
|
+
|
65
|
+
def note_on(pitch, velocity)
|
66
|
+
lambda { @generator.note_on(pitch, velocity) }
|
67
|
+
end
|
68
|
+
|
69
|
+
def note_off(pitch, velocity)
|
70
|
+
lambda { @generator.note_off(pitch, velocity) }
|
71
|
+
end
|
72
|
+
|
73
|
+
def at time, block
|
74
|
+
@scheduler.at(time) { block.call }
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
data/lib/mtk/note.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module MTK
|
2
2
|
|
3
|
-
# A musical
|
3
|
+
# A musical {Event} defined by a {Pitch}, intensity, and duration
|
4
4
|
class Note < Event
|
5
5
|
|
6
6
|
# frequency of the note as a Pitch
|
@@ -15,18 +15,26 @@ module MTK
|
|
15
15
|
new hash[:pitch], hash[:intensity], hash[:duration]
|
16
16
|
end
|
17
17
|
|
18
|
+
def to_hash
|
19
|
+
super.merge({ :pitch => @pitch })
|
20
|
+
end
|
21
|
+
|
18
22
|
def self.from_midi(pitch, velocity, beats)
|
19
23
|
new Pitches::PITCHES[pitch], velocity/127.0, beats
|
20
24
|
end
|
21
25
|
|
22
|
-
def
|
23
|
-
|
26
|
+
def to_midi
|
27
|
+
[pitch.to_i, velocity, duration]
|
24
28
|
end
|
25
29
|
|
26
30
|
def transpose(interval)
|
27
31
|
self.class.new(@pitch+interval, @intensity, @duration)
|
28
32
|
end
|
29
33
|
|
34
|
+
def invert(around_pitch)
|
35
|
+
self.class.new(@pitch.invert(around_pitch), @intensity, @duration)
|
36
|
+
end
|
37
|
+
|
30
38
|
def == other
|
31
39
|
super and other.respond_to? :pitch and @pitch == other.pitch
|
32
40
|
end
|
@@ -41,4 +49,15 @@ module MTK
|
|
41
49
|
|
42
50
|
end
|
43
51
|
|
52
|
+
# Construct a {Note} from any supported type
|
53
|
+
def Note(*anything)
|
54
|
+
anything = anything.first if anything.size == 1
|
55
|
+
case anything
|
56
|
+
when Array then Note.new(*anything)
|
57
|
+
when Note then anything
|
58
|
+
else raise "Note doesn't understand #{anything.class}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
module_function :Note
|
62
|
+
|
44
63
|
end
|