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