patchmaster 0.0.0

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.
@@ -0,0 +1,23 @@
1
+ module PM
2
+
3
+ # Filters are blocks of code executed by a Connection to modify incoming
4
+ # MIDI bytes. Since we want to save them to files, we store the text
5
+ # representation as well.
6
+ class Filter
7
+
8
+ attr_accessor :block, :text
9
+
10
+ def initialize(block, text=nil)
11
+ @block, @text = block, text
12
+ end
13
+
14
+ def call(conn, bytes)
15
+ @block.call(conn, bytes)
16
+ end
17
+
18
+ def to_s
19
+ @text.gsub("\n", "; ")
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,87 @@
1
+ # Ports are UniMIDI inputs or outputs.
2
+ module PM
3
+
4
+ class MIDIDevice
5
+
6
+ attr_reader :name, :port_num, :port
7
+
8
+ def initialize(name, port_num, port)
9
+ @name, @port_num, @port = name, port_num, port
10
+ end
11
+
12
+ end
13
+
14
+ # When a connection is started, it adds itself to this InputDevice's
15
+ # +@connections+. When it ends, it removes itself.
16
+ class InputDevice < MIDIDevice
17
+
18
+ attr_accessor :connections
19
+
20
+ # If +port+ is nil (the normal case), creates either a real or a mock port
21
+ def initialize(name, port_num, no_midi=false)
22
+ super(name, port_num, input_port(port_num, no_midi))
23
+ @connections = []
24
+ end
25
+
26
+ def add_connection(conn)
27
+ @connections << conn
28
+ end
29
+
30
+ def remove_connection(conn)
31
+ @connections.delete(conn)
32
+ end
33
+
34
+ # Poll for more MIDI input and process it.
35
+ def gets_data
36
+ @port.gets_data.each { |bytes| midi_in(bytes) }
37
+ end
38
+
39
+ # Passes MIDI bytes on to each output connection
40
+ def midi_in(bytes)
41
+ @connections.each { |conn| conn.midi_in(bytes) }
42
+ end
43
+
44
+ private
45
+
46
+ def input_port(port_num, no_midi=false)
47
+ if no_midi
48
+ MockInputPort.new
49
+ else
50
+ UniMIDI::Input.all[port_num].open
51
+ end
52
+ end
53
+
54
+ end
55
+
56
+ class OutputDevice < MIDIDevice
57
+
58
+ def initialize(name, port_num, no_midi=false)
59
+ super(name, port_num, output_port(port_num, no_midi))
60
+ end
61
+
62
+ def midi_out(bytes)
63
+ @port.puts bytes
64
+ end
65
+
66
+ private
67
+
68
+ def output_port(port_num, no_midi)
69
+ if no_midi
70
+ MockOutputPort.new
71
+ else
72
+ UniMIDI::Output.all[port_num].open
73
+ end
74
+ end
75
+ end
76
+
77
+ class MockInputPort
78
+ def gets_data
79
+ []
80
+ end
81
+ end
82
+
83
+ class MockOutputPort
84
+ def puts(data)
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,121 @@
1
+ module PM
2
+
3
+ # A list (Array) of things with a cursor. +@curr+ is an integer index into
4
+ # an array of data.
5
+ class List
6
+
7
+ include Enumerable
8
+
9
+ # If +enter_sym+ or +exit_sym+ are defined, they will be sent to a data
10
+ # element when it becomes the current element or stops being the current
11
+ # element.
12
+ def initialize
13
+ @data = []
14
+ @curr = nil
15
+ end
16
+
17
+ # Adding data does not modify the cursor.
18
+ def <<(data)
19
+ @data << data
20
+ end
21
+
22
+ # Inserts +data+ before +before_this+. If +before_this+ is not in our
23
+ # list, +data+ is inserted at the beginning of the list.
24
+ def insert_before(before_this, data)
25
+ idx = @data.index(before_this) || 0
26
+ @data[idx, 0] = data
27
+ end
28
+
29
+ # Inserts +data+ after +after_this+. If +after_this+ is not in our list,
30
+ # +data+ is inserted at the end of the list.
31
+ def insert_after(after_this, data)
32
+ idx = @data.index(after_this) || @data.length-1
33
+ @data[idx + 1, 0] = data
34
+ end
35
+
36
+ def size
37
+ @data.size
38
+ end
39
+ alias_method :length, :size
40
+
41
+ def first?
42
+ @curr == 0
43
+ end
44
+
45
+ def first
46
+ if !@data.empty? && @curr != 0
47
+ @curr = 0
48
+ end
49
+ curr
50
+ end
51
+
52
+ def next
53
+ if @curr == nil || @curr >= @data.length - 1
54
+ @curr = nil
55
+ else
56
+ @curr += 1
57
+ end
58
+ curr
59
+ end
60
+
61
+ def curr
62
+ @curr ? @data[@curr] : nil
63
+ end
64
+
65
+ # This does not change what is stored at the current location. Rather,
66
+ # it moves this list's cursor to point to +data+ and returns +data+.
67
+ def curr=(data)
68
+ if curr != data
69
+ @curr = @data.index(data)
70
+ end
71
+ data
72
+ end
73
+
74
+ # Returns the +n+th data element. This method is not normally used to
75
+ # access data because it does not change the cursor. It is used to peek
76
+ # into the list's data array, for example during testing.
77
+ def [](n)
78
+ @data[n]
79
+ end
80
+
81
+ def prev
82
+ if @curr == nil || @curr == 0
83
+ @curr = nil
84
+ else
85
+ @curr -= 1
86
+ end
87
+ curr
88
+ end
89
+
90
+ def last
91
+ if !@data.empty? && !last?
92
+ @curr = @data.length - 1
93
+ end
94
+ curr
95
+ end
96
+
97
+ def last?
98
+ @curr == nil || @curr == @data.length - 1
99
+ end
100
+
101
+ def remove(data)
102
+ return unless @data.include?(data)
103
+ @data[@data.index(data), 1] = []
104
+ if @data.empty?
105
+ @curr = nil
106
+ elsif @curr >= @data.length
107
+ @curr = @data.length - 1
108
+ end
109
+ end
110
+
111
+ def each
112
+ @data.each { |data| yield data }
113
+ end
114
+
115
+ # For debugging
116
+ def to_s
117
+ "List(#{@data.empty? ? 'empty' : @data[0].class.name}), size #{size}, curr index #{@curr}"
118
+ end
119
+
120
+ end
121
+ end
@@ -0,0 +1,36 @@
1
+ module PM
2
+
3
+ # A ListContainer responds to messages that manipulate the cursors for one
4
+ # or more List objects.
5
+ module ListContainer
6
+
7
+ def method_missing(sym, *args)
8
+ case sym.to_s
9
+ when /^curr_(\w+)=$/
10
+ name = $1
11
+ ivar_sym = "@#{pluralize(name)}".to_sym
12
+ raise "no such ivar #{ivar_sym} in #{self.class}" unless instance_variable_defined?(ivar_sym)
13
+ instance_variable_get(ivar_sym).send(:curr=, args[0])
14
+ when /^(first|next|prev|curr|last)_(\w+)(\?)?$/
15
+ method, ivar, qmark = $1, $2, $3
16
+ ivar_sym = "@#{pluralize(ivar)}".to_sym
17
+ raise "no such ivar #{ivar_sym} in #{self.class}" unless instance_variable_defined?(ivar_sym)
18
+ instance_variable_get(ivar_sym).send("#{method}#{qmark}".to_sym)
19
+ else
20
+ super
21
+ end
22
+ end
23
+
24
+ def pluralize(str)
25
+ case str
26
+ when /s$/, /ch$/
27
+ "#{str}es"
28
+ when /y$/
29
+ "#{str[0..-2]}ies}"
30
+ else
31
+ "#{str}s"
32
+ end
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,48 @@
1
+ module PM
2
+
3
+ class Patch
4
+
5
+ attr_accessor :name, :connections, :start_bytes
6
+
7
+ def initialize(name, start_bytes=nil)
8
+ @name = name
9
+ @connections = []
10
+ @start_bytes = start_bytes
11
+ @running = false
12
+ end
13
+
14
+ def <<(conn)
15
+ @connections << conn
16
+ end
17
+
18
+ def inputs
19
+ @connections.map(&:input).uniq
20
+ end
21
+
22
+ # Send start_bytes to each connection, then spawn a new thread that
23
+ # receives input and passes it on to each connection.
24
+ def start
25
+ input_devices = inputs()
26
+ @connections.each { |conn| conn.start(@start_bytes) }
27
+ @running = true
28
+ Thread.new do
29
+ loop do
30
+ break unless @running
31
+ input_devices.map(&:gets_data)
32
+ end
33
+ end
34
+ end
35
+
36
+ def running?
37
+ @running
38
+ end
39
+
40
+ def stop
41
+ if @running
42
+ @running = false
43
+ @connections.map(&:stop)
44
+ end
45
+ end
46
+ end
47
+
48
+ end # PM
@@ -0,0 +1,168 @@
1
+ require 'singleton'
2
+ require 'patchmaster/sorted_song_list'
3
+
4
+ module PM
5
+
6
+ # Global behavior: master list of songs, list of song lists, stuff like
7
+ # that.
8
+ #
9
+ # Typical use:
10
+ #
11
+ # PatchMaster.instance.load("my_pm_dsl_file")
12
+ # PatchMaster.instance.start
13
+ # # ...when you're done
14
+ # PatchMaster.instance.stop
15
+ class PatchMaster
16
+
17
+ include Singleton
18
+
19
+ attr_reader :inputs, :outputs, :all_songs, :song_lists, :no_midi
20
+ attr_reader :curr_song_list, :curr_song, :curr_patch
21
+
22
+ def initialize
23
+ init_data
24
+ @no_midi = false
25
+ @curr_song_list = @curr_song = @curr_patch = nil
26
+ end
27
+
28
+ def no_midi!
29
+ @no_midi = true
30
+ end
31
+
32
+ def load(file)
33
+ stop
34
+ init_data
35
+ DSL.new(@no_midi).load(file)
36
+ rescue => ex
37
+ raise("error loading #{file}: #{ex}\n" + caller.join("\n"))
38
+ end
39
+
40
+ def save(file)
41
+ DSL.new(@no_midi).save(file)
42
+ message("saved #{file}")
43
+ rescue => ex
44
+ raise("error saving #{file}: #{ex}")
45
+ end
46
+
47
+ def init_data
48
+ @curr_song_list = @curr_song = @curr_patch = nil
49
+ @inputs = {}
50
+ @outputs = {}
51
+ @song_lists = List.new
52
+ @all_songs = SortedSongList.new('All Songs')
53
+ @song_lists << @all_songs
54
+ end
55
+
56
+ def start
57
+ @curr_song_list = @song_lists.first # sets cursor in @song_lists
58
+ @curr_song = @curr_song_list.first_song
59
+ if @curr_song
60
+ @curr_patch = @curr_song.first_patch
61
+ @curr_patch.start
62
+ else
63
+ @curr_patch = nil
64
+ end
65
+ end
66
+
67
+ def stop
68
+ @curr_patch.stop if @curr_patch
69
+ @curr_song_list = @curr_song = @curr_patch = nil
70
+ end
71
+
72
+ def next_song
73
+ return unless @curr_song_list
74
+ return if @curr_song_list.last_song?
75
+
76
+ @curr_patch.stop if @curr_patch
77
+ @curr_song = @curr_song_list.next_song
78
+ @curr_patch = @curr_song.first_patch
79
+ @curr_patch.start
80
+ end
81
+
82
+ def prev_song
83
+ return unless @curr_song_list
84
+ return if @curr_song_list.first_song?
85
+
86
+ @curr_patch.stop if @curr_patch
87
+ @curr_song = @curr_song_list.prev_song
88
+ @curr_patch = @curr_song.first_patch
89
+ @curr_patch.start
90
+ end
91
+
92
+ def next_patch
93
+ return unless @curr_song
94
+ if @curr_song.last_patch?
95
+ next_song
96
+ elsif @curr_patch
97
+ @curr_patch.stop
98
+ @curr_patch = @curr_song.next_patch
99
+ @curr_patch.start
100
+ end
101
+ end
102
+
103
+ def prev_patch
104
+ return unless @curr_song
105
+ if @curr_song.first_patch?
106
+ prev_song
107
+ elsif @curr_patch
108
+ @curr_patch.stop
109
+ @curr_patch = @curr_song.prev_patch
110
+ @curr_patch.start
111
+ end
112
+ end
113
+
114
+ def goto_song(name_regex)
115
+ new_song_list = new_song = new_patch = nil
116
+ new_song = @curr_song_list.find(name_regex) if @curr_song_list
117
+ new_song = @all_songs.find(name_regex) unless new_song
118
+ new_patch = new_song ? new_song.first_patch : nil
119
+
120
+ if (new_song && new_song != @curr_song) || # moved to new song
121
+ (new_song == @curr_song && @curr_patch != new_patch) # same song but not at same first patch
122
+
123
+ @curr_patch.stop if @curr_patch
124
+
125
+ if @curr_song_list.songs.include?(new_song)
126
+ new_song_list = @curr_song_list
127
+ else
128
+ # Not found in current song list. Switch to all_songs list.
129
+ new_song_list = @all_songs
130
+ end
131
+ new_song_list.curr_song = new_song # move to that song in selected song list
132
+
133
+ @curr_song_list = new_song_list
134
+ @curr_song = new_song
135
+ @curr_patch = new_patch
136
+ @curr_patch.start
137
+ end
138
+ end
139
+
140
+ def goto_song_list(name_regex)
141
+ name_regex = Regexp.new(name_regex.to_s, true) # make case-insensitive
142
+ new_song_list = @song_lists.detect { |song_list| song_list.name =~ name_regex }
143
+ return unless new_song_list
144
+
145
+ @curr_song_list = new_song_list
146
+ @song_lists.curr = new_song_list # set cursor
147
+
148
+ new_song = @curr_song_list.first_song
149
+ new_patch = new_song ? new_song.first_patch : nil
150
+
151
+ if new_patch != @curr_patch
152
+ @curr_patch.stop if @curr_patch
153
+ new_patch.start if new_patch
154
+ end
155
+ @curr_song = new_song
156
+ @curr_patch = new_patch
157
+ end
158
+
159
+ def panic
160
+ @outputs.values.each do |out|
161
+ MIDI_CHANNELS.times do |chan|
162
+ out.midi_out([CONTROLLER + chan, CM_ALL_NOTES_OFF, 0])
163
+ end
164
+ end
165
+ end
166
+
167
+ end
168
+ end