patchmaster 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/patchmaster ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # usage: patchmaster [-i] [pm_file]
4
+ #
5
+ # Starts PatchMaster and optionally loads pm_file.
6
+ #
7
+ # The -n flag tells PatchMaster to not use MIDI. All MIDI errors such as not
8
+ # being able to connect to the MIDI devices speicified in pm_file are
9
+ # ignored, and no MIDI data is sent/received. That is useful if you want to
10
+ # run PatchMaster without actually talking to any MIDI devices.
11
+
12
+ require 'patchmaster'
13
+
14
+ pm = PM::Main.instance
15
+ if ARGV[0] == '-n'
16
+ pm.no_midi!
17
+ ARGV.shift
18
+ end
19
+ pm.load(ARGV[0]) if ARGV[0]
20
+ pm.run
@@ -0,0 +1,15 @@
1
+ require 'patchmaster/consts'
2
+ require 'patchmaster/predicates'
3
+ require 'patchmaster/song_list'
4
+ require 'patchmaster/song'
5
+ require 'patchmaster/patch'
6
+ require 'patchmaster/connection'
7
+ require 'patchmaster/filter'
8
+ require 'patchmaster/io'
9
+ require 'patchmaster/patchmaster'
10
+ require 'patchmaster/dsl'
11
+ require 'patchmaster/app/main'
12
+
13
+ def message(str)
14
+ PM::Main.instance.message(str)
15
+ end
@@ -0,0 +1,32 @@
1
+ require 'curses'
2
+
3
+ module PM
4
+ class InfoWindow
5
+
6
+ CONTENTS = File.join(File.dirname(__FILE__), 'info_window_contents.txt')
7
+
8
+ include Curses
9
+
10
+ attr_reader :win
11
+
12
+ TITLE = ' PatchMaster '
13
+
14
+ def initialize(rows, cols, row, col)
15
+ @win = Window.new(rows, cols, row, col)
16
+ end
17
+
18
+ def draw
19
+ @win.setpos(0, (@win.maxx() - TITLE.length) / 2)
20
+ @win.attron(A_REVERSE) {
21
+ @win.addstr(TITLE)
22
+ }
23
+ @win.addstr("\n")
24
+ IO.foreach(CONTENTS) { |line| @win.addstr(line) }
25
+ end
26
+
27
+ def refresh
28
+ @win.refresh
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,15 @@
1
+ j, down - next patch
2
+ k, up - prev patch
3
+ n, left - next song
4
+ p, right - prev song
5
+
6
+ g - goto song
7
+ t - goto song list
8
+
9
+ ESC - panic
10
+ F1 - help
11
+
12
+ l - load
13
+ s - save
14
+
15
+ q - quit
@@ -0,0 +1,26 @@
1
+ require 'patchmaster/app/pm_window'
2
+
3
+ module PM
4
+ class ListWindow < PmWindow
5
+
6
+ attr_reader :list
7
+
8
+ def set_contents(title, list)
9
+ @title, @list = title, list
10
+ draw
11
+ end
12
+
13
+ def draw
14
+ super
15
+ return unless @list
16
+
17
+ @list.each_with_index do |thing, i|
18
+ @win.setpos(i+1, 1)
19
+ @win.attron(A_REVERSE) if thing == @list.curr
20
+ @win.addstr(make_fit(" #{thing.name} "))
21
+ @win.attroff(A_REVERSE) if thing == @list.curr
22
+ end
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,161 @@
1
+ require 'curses'
2
+ require 'singleton'
3
+ %w(list patch info prompt).each { |w| require "patchmaster/app/#{w}_window" }
4
+
5
+ module PM
6
+
7
+ class Main
8
+
9
+ DEBUG_FILE = '/tmp/pm_debug.txt'
10
+
11
+ include Singleton
12
+ include Curses
13
+
14
+ def initialize
15
+ @pm = PatchMaster.instance
16
+ end
17
+
18
+ def no_midi!
19
+ @pm.no_midi!
20
+ end
21
+
22
+ # File must be a Ruby file that returns an array of song lists.
23
+ def load(file)
24
+ @pm.load(file)
25
+ end
26
+
27
+ def run
28
+ if $DEBUG
29
+ @debug_file = File.open(DEBUG_FILE, 'a')
30
+ end
31
+ @pm.start
32
+ begin
33
+ config_curses
34
+ create_windows
35
+ message("Welcome to PatchMaster")
36
+
37
+ loop do
38
+ begin
39
+ refresh_all
40
+ ch = getch
41
+ message("ch = #{ch}") if $DEBUG
42
+ case ch
43
+ when ?j, Key::DOWN
44
+ @pm.next_patch
45
+ when ?k, Key::UP
46
+ @pm.prev_patch
47
+ when ?n, Key::LEFT
48
+ @pm.next_song
49
+ when ?p, Key::RIGHT
50
+ @pm.prev_song
51
+ when ?g
52
+ name = PromptWindow.new('Go To Song', 'Go to song:').gets
53
+ @pm.goto_song(name)
54
+ when ?t
55
+ name = PromptWindow.new('Go To Song List', 'Go to Song List:').gets
56
+ @pm.goto_song_list(name)
57
+ when Key::F1
58
+ help
59
+ when 27 # escape
60
+ @pm.panic
61
+ message('Panic sent')
62
+ when ?l
63
+ file = PromptWindow.new('Load', 'Load file:').gets
64
+ begin
65
+ @pm.load(file)
66
+ rescue => ex
67
+ message(ex.to_s)
68
+ end
69
+ when ?s
70
+ file = PromptWindow.new('Save', 'Save into file:').gets
71
+ begin
72
+ @pm.save(file)
73
+ rescue => ex
74
+ message(ex.to_s)
75
+ end
76
+ when ?q
77
+ break
78
+ end
79
+ rescue => ex
80
+ message(ex.to_s)
81
+ if $DEBUG
82
+ @debug_file.puts caller.join("\n")
83
+ end
84
+ end
85
+ end
86
+ ensure
87
+ close_screen
88
+ @pm.stop
89
+ if $DEBUG
90
+ @debug_file.close
91
+ end
92
+ end
93
+ end
94
+
95
+ def config_curses
96
+ init_screen
97
+ cbreak # unbuffered input
98
+ noecho # do not show typed keys
99
+ stdscr.keypad(true) # enable arrow keys
100
+ curs_set(0) # cursor: 0 = invisible, 1 = normal
101
+ end
102
+
103
+ def create_windows
104
+ top_height = (lines() - 1) * 2 / 3
105
+ bot_height = (lines() - 1) - top_height
106
+ top_width = cols() / 3
107
+
108
+ sls_height = top_height / 3
109
+ sl_height = top_height - sls_height
110
+
111
+ @song_lists_win = ListWindow.new(sls_height, top_width, 0, 0, nil)
112
+ @song_lists_win.set_contents('Song Lists', @pm.song_lists)
113
+ @song_list_win = ListWindow.new(sl_height, top_width, sls_height, 0, 'Song List')
114
+ @song_win = ListWindow.new(top_height, top_width, 0, top_width, 'Song')
115
+ @patch_win = PatchWindow.new(bot_height, cols(), top_height, 0, 'Patch')
116
+ @message_win = Window.new(1, cols(), lines()-1, 0)
117
+
118
+ @info_win = InfoWindow.new(top_height, cols() - (top_width * 2) - 1, 0, top_width * 2 + 1)
119
+ @info_win.draw
120
+ end
121
+
122
+ def help
123
+ message("Help: not yet implemented")
124
+ end
125
+
126
+ def message(str)
127
+ if @message_win
128
+ @message_win.clear
129
+ @message_win.addstr(str)
130
+ @message_win.refresh
131
+ else
132
+ $stderr.puts str
133
+ end
134
+ if $DEBUG
135
+ @debug_file.puts "#{Time.now} #{str}"
136
+ @debug_file.flush
137
+ end
138
+ end
139
+
140
+ def refresh_all
141
+ set_window_data
142
+ [@song_lists_win, @song_list_win, @song_win, @patch_win].map(&:draw)
143
+ [stdscr, @song_lists_win, @song_list_win, @song_win, @info_win, @patch_win, @message_win].map(&:refresh)
144
+ end
145
+
146
+ def set_window_data
147
+ song_list = @pm.curr_song_list
148
+ @song_list_win.set_contents(song_list.name, song_list.songs)
149
+ song = song_list.curr_song
150
+ if song
151
+ @song_win.set_contents(song.name, song.patches)
152
+ patch = song.curr_patch
153
+ @patch_win.patch = patch
154
+ else
155
+ @song_win.set_contents(nil, nil)
156
+ @patch_win.patch = nil
157
+ end
158
+ end
159
+
160
+ end
161
+ end
@@ -0,0 +1,61 @@
1
+ require 'patchmaster/app/pm_window'
2
+
3
+ module PM
4
+ class PatchWindow < PmWindow
5
+
6
+ attr_reader :patch
7
+
8
+ def patch=(patch)
9
+ @title = patch ? patch.name : nil
10
+ @patch = patch
11
+ draw
12
+ end
13
+
14
+ def draw
15
+ super
16
+ @win.setpos(1, 1)
17
+ draw_headers
18
+ return unless @patch
19
+
20
+ max_len = @win.maxx - 3 # minus 2 for borders
21
+ @patch.connections.each_with_index do |connection, i|
22
+ @win.setpos(i+2, 1)
23
+ draw_connection(connection, max_len)
24
+ end
25
+ end
26
+
27
+ def draw_headers
28
+ @win.attron(A_REVERSE) {
29
+ str = " Input Chan | Output Chan | Prog | Zone | Xpose | Filter"
30
+ str << ' ' * (@win.maxx - 2 - str.length)
31
+ @win.addstr(str)
32
+ }
33
+ end
34
+
35
+ def draw_connection(connection, max_len)
36
+ str = " #{'%16s' % connection.input.name}"
37
+ str << " #{connection.input_chan ? ('%2d' % (connection.input_chan+1)) : ' '} |"
38
+ str << " #{'%16s' % connection.output.name}"
39
+ str << " #{'%2d' % (connection.output_chan+1)} |"
40
+ str << if connection.pc?
41
+ " #{'%3d' % connection.pc_prog} |"
42
+ else
43
+ " |"
44
+ end
45
+ str << if connection.zone
46
+ " #{'%3s' % connection.note_num_to_name(connection.zone.begin)}" +
47
+ " - #{'%3s' % connection.note_num_to_name(connection.zone.end)} |"
48
+ else
49
+ ' |'
50
+ end
51
+ str << if connection.xpose && connection.xpose > 0
52
+ " #{'%2d' % connection.xpose.to_i} |"
53
+ else
54
+ " |"
55
+ end
56
+ str << " #{connection.filter}"
57
+ @win.addstr(make_fit(str))
58
+ end
59
+
60
+ end
61
+ end
@@ -0,0 +1,41 @@
1
+ require 'curses'
2
+
3
+ module PM
4
+ class PmWindow
5
+
6
+ include Curses
7
+
8
+ attr_reader :win, :title_prefix
9
+ attr_accessor :title
10
+
11
+ # If title is nil then list's name will be used
12
+ def initialize(rows, cols, row, col, title_prefix)
13
+ @win = Window.new(rows, cols, row, col)
14
+ @title_prefix = title_prefix
15
+ @max_contents_len = @win.maxx - 3 # 2 for borders
16
+ end
17
+
18
+ def refresh
19
+ @win.refresh
20
+ end
21
+
22
+ def draw
23
+ @win.clear
24
+ @win.box(?|, ?-)
25
+ return unless @title_prefix || @title
26
+
27
+ @win.setpos(0, 1)
28
+ @win.attron(A_REVERSE) {
29
+ @win.addch(' ')
30
+ @win.addstr("#{@title_prefix}: ") if @title_prefix
31
+ @win.addstr(@title) if @title
32
+ @win.addch(' ')
33
+ }
34
+ end
35
+
36
+ def make_fit(str)
37
+ str = str[0..@max_contents_len] if str.length > @max_contents_len
38
+ str
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,61 @@
1
+ require 'curses'
2
+
3
+ module PM
4
+ class PromptWindow
5
+
6
+ MAX_WIDTH = 30
7
+
8
+ include Curses
9
+
10
+ def initialize(title, prompt)
11
+ @title, @prompt = title, prompt
12
+ width = cols() / 2
13
+ width = MAX_WIDTH if width > MAX_WIDTH
14
+ @win = Window.new(4, width, lines() / 3, (cols() - width) / 2)
15
+ end
16
+
17
+ def gets
18
+ draw
19
+ str = read_string
20
+ cleanup
21
+ str
22
+ end
23
+
24
+ def draw
25
+ @win.box(?|, ?-)
26
+ @win.setpos(0, 1)
27
+ @win.attron(A_REVERSE) {
28
+ @win.addstr(" #@title ")
29
+ }
30
+
31
+ @win.setpos(1, 1)
32
+ @win.addstr(@prompt)
33
+
34
+ @win.setpos(2, 1)
35
+ @win.attron(A_REVERSE) {
36
+ @win.addstr(' ' * (@win.maxx() - 2))
37
+ }
38
+
39
+ @win.setpos(2, 1)
40
+ @win.refresh
41
+ end
42
+
43
+ def read_string
44
+ nocbreak
45
+ echo
46
+ curs_set(1)
47
+ str = nil
48
+ @win.attron(A_REVERSE) {
49
+ str = @win.getstr
50
+ }
51
+ curs_set(0)
52
+ noecho
53
+ cbreak
54
+ str
55
+ end
56
+
57
+ def cleanup
58
+ @win.close
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,90 @@
1
+ require 'patchmaster/consts'
2
+
3
+ module PM
4
+
5
+ # A Connection connects an InputDevice to an OutputDevice. Whenever MIDI
6
+ # data arrives at the InputDevice it is optionally modified or filtered,
7
+ # then the remaining modified data is sent to the OutputDevice.
8
+ class Connection
9
+
10
+ attr_accessor :input, :input_chan, :output, :output_chan,
11
+ :pc_prog, :zone, :xpose, :filter
12
+
13
+ # If input_chan is nil than all messages from input will be sent to
14
+ # output.
15
+ #
16
+ # All channels (input_chan, output_chan, etc.) are 1-based here but are
17
+ # turned into 0-based channels for later use.
18
+ def initialize(input, input_chan, output, output_chan, filter=nil, opts={})
19
+ @input, @input_chan, @output, @output_chan, @filter = input, input_chan, output, output_chan, filter
20
+ @pc_prog, @zone, @xpose = opts[:pc_prog], opts[:zone], opts[:xpose]
21
+
22
+ @input_chan -= 1 if @input_chan
23
+ @output_chan -= 1 if @output_chan
24
+ end
25
+
26
+ def start(start_bytes=nil)
27
+ midi_out(@output, start_bytes) if start_bytes
28
+ midi_out(@output, [PROGRAM_CHANGE + @output_chan, @pc_prog]) if pc?
29
+ @input.add_connection(self)
30
+ end
31
+
32
+ def stop
33
+ @input.remove_connection(self)
34
+ end
35
+
36
+ def accept_from_input?(bytes)
37
+ return true if @input_chan == nil
38
+ return true if bytes[0] >= 0xf0
39
+ (bytes[0] & 0xf0) >= 0x80 && (bytes[0] & 0x0f) == @input_chan
40
+ end
41
+
42
+ # Returns true if the +@zone+ is nil (allowing all notes throught) or if
43
+ # +@zone+ is a Range and +note+ is inside +@zone+.
44
+ def inside_zone?(note)
45
+ @zone == nil || @zone.include?(note)
46
+ end
47
+
48
+ def midi_in(bytes)
49
+ return unless accept_from_input?(bytes)
50
+
51
+ # TODO handle running bytes
52
+ high_nibble = bytes[0] & 0xf0
53
+ case high_nibble
54
+ when NOTE_ON, NOTE_OFF, POLY_PRESSURE
55
+ return unless inside_zone?(bytes[1])
56
+ bytes[0] = high_nibble + @output_chan
57
+ bytes[1] = ((bytes[1] + @xpose) & 0xff) if @xpose
58
+ when CONTROLLER, PROGRAM_CHANGE, CHANNEL_PRESSURE, PITCH_BEND
59
+ bytes[0] = high_nibble + @output_chan
60
+ end
61
+
62
+ bytes = @filter.call(self, bytes) if @filter
63
+ if bytes && bytes.size > 0
64
+ midi_out(@output, bytes)
65
+ end
66
+ end
67
+
68
+ def midi_out(device, bytes)
69
+ device.midi_out(bytes)
70
+ end
71
+
72
+ def pc?
73
+ @pc_prog != nil
74
+ end
75
+
76
+ def note_num_to_name(n)
77
+ oct = (n / 12) - 1
78
+ note = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'][n % 12]
79
+ "#{note}#{oct}"
80
+ end
81
+
82
+ def to_s
83
+ str = "#{@input.name} ch #{@input_chan ? @input_chan+1 : 'all'} -> #{@output.name} ch #{@output_chan+1}"
84
+ str << "; pc #@pc_prog" if pc?
85
+ str << "; xpose #@xpose" if @xpose
86
+ str << "; zone #{note_num_to_name(@zone.begin)}..#{note_num_to_name(@zone.end)}" if @zone
87
+ end
88
+ end
89
+
90
+ end # PM