patchmaster 0.0.0 → 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/patchmaster +18 -10
- data/lib/patchmaster.rb +2 -5
- data/lib/patchmaster/app/info_window.rb +3 -2
- data/lib/patchmaster/app/info_window_contents.txt +5 -4
- data/lib/patchmaster/app/list_window.rb +7 -4
- data/lib/patchmaster/app/main.rb +54 -39
- data/lib/patchmaster/app/trigger_window.rb +27 -0
- data/lib/patchmaster/connection.rb +15 -13
- data/lib/patchmaster/cursor.rb +174 -0
- data/lib/patchmaster/dsl.rb +65 -32
- data/lib/patchmaster/filter.rb +1 -1
- data/lib/patchmaster/{io.rb → instrument.rb} +24 -11
- data/lib/patchmaster/patch.rb +10 -17
- data/lib/patchmaster/patchmaster.rb +84 -100
- data/lib/patchmaster/song.rb +1 -6
- data/lib/patchmaster/song_list.rb +1 -11
- data/lib/patchmaster/sorted_song_list.rb +1 -1
- data/lib/patchmaster/trigger.rb +32 -0
- data/test/test_helper.rb +21 -13
- metadata +7 -6
- data/lib/patchmaster/list.rb +0 -121
- data/lib/patchmaster/list_container.rb +0 -36
data/bin/patchmaster
CHANGED
@@ -1,20 +1,28 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
#
|
3
|
-
# usage: patchmaster [-
|
3
|
+
# usage: patchmaster [-n] [-d] [pm_file]
|
4
4
|
#
|
5
5
|
# Starts PatchMaster and optionally loads pm_file.
|
6
6
|
#
|
7
7
|
# The -n flag tells PatchMaster to not use MIDI. All MIDI errors such as not
|
8
|
-
# being able to connect to the MIDI
|
8
|
+
# being able to connect to the MIDI instruments specified in pm_file are
|
9
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
|
10
|
+
# run PatchMaster without actually talking to any MIDI instruments.
|
11
11
|
|
12
|
+
require 'optparse'
|
13
|
+
|
14
|
+
use_midi = true
|
15
|
+
OptionParser.new do |opts|
|
16
|
+
opts.banner = "usage: patchmaster [options] [pm_file]"
|
17
|
+
opts.on("-d", "--debug", "Turn on debug mode") { $DEBUG = true }
|
18
|
+
opts.on("-n", "--no-midi", "Turn off MIDI processing") { use_midi = false }
|
19
|
+
end.parse!(ARGV)
|
20
|
+
|
21
|
+
# Must require patchmaster here, after handling options, because Singleton
|
22
|
+
# initialze code checks $DEBUG.
|
12
23
|
require 'patchmaster'
|
13
24
|
|
14
|
-
|
15
|
-
if
|
16
|
-
|
17
|
-
|
18
|
-
end
|
19
|
-
pm.load(ARGV[0]) if ARGV[0]
|
20
|
-
pm.run
|
25
|
+
app = PM::Main.instance
|
26
|
+
app.no_midi! if !use_midi
|
27
|
+
app.load(ARGV[0]) if ARGV[0]
|
28
|
+
app.run
|
data/lib/patchmaster.rb
CHANGED
@@ -5,11 +5,8 @@ require 'patchmaster/song'
|
|
5
5
|
require 'patchmaster/patch'
|
6
6
|
require 'patchmaster/connection'
|
7
7
|
require 'patchmaster/filter'
|
8
|
-
require 'patchmaster/
|
8
|
+
require 'patchmaster/instrument'
|
9
9
|
require 'patchmaster/patchmaster'
|
10
|
+
require 'patchmaster/trigger'
|
10
11
|
require 'patchmaster/dsl'
|
11
12
|
require 'patchmaster/app/main'
|
12
|
-
|
13
|
-
def message(str)
|
14
|
-
PM::Main.instance.message(str)
|
15
|
-
end
|
@@ -7,12 +7,13 @@ class InfoWindow
|
|
7
7
|
|
8
8
|
include Curses
|
9
9
|
|
10
|
-
attr_reader :win
|
10
|
+
attr_reader :win, :text
|
11
11
|
|
12
12
|
TITLE = ' PatchMaster '
|
13
13
|
|
14
14
|
def initialize(rows, cols, row, col)
|
15
15
|
@win = Window.new(rows, cols, row, col)
|
16
|
+
@text = IO.read(CONTENTS)
|
16
17
|
end
|
17
18
|
|
18
19
|
def draw
|
@@ -21,7 +22,7 @@ class InfoWindow
|
|
21
22
|
@win.addstr(TITLE)
|
22
23
|
}
|
23
24
|
@win.addstr("\n")
|
24
|
-
|
25
|
+
@text.each_line { |line| @win.addstr(line) }
|
25
26
|
end
|
26
27
|
|
27
28
|
def refresh
|
@@ -1,7 +1,7 @@
|
|
1
|
-
j, down - next patch
|
2
|
-
k, up
|
3
|
-
n, left
|
4
|
-
p, right
|
1
|
+
j, down, space - next patch
|
2
|
+
k, up - prev patch
|
3
|
+
n, left - next song
|
4
|
+
p, right - prev song
|
5
5
|
|
6
6
|
g - goto song
|
7
7
|
t - goto song list
|
@@ -11,5 +11,6 @@ F1 - help
|
|
11
11
|
|
12
12
|
l - load
|
13
13
|
s - save
|
14
|
+
e - edit
|
14
15
|
|
15
16
|
q - quit
|
@@ -5,8 +5,10 @@ class ListWindow < PmWindow
|
|
5
5
|
|
6
6
|
attr_reader :list
|
7
7
|
|
8
|
-
|
9
|
-
|
8
|
+
# +curr_item_method_sym+ is a method symbol that is sent to
|
9
|
+
# PM::PatchMaster to obtain the current item so we can highlight it.
|
10
|
+
def set_contents(title, list, curr_item_method_sym)
|
11
|
+
@title, @list, @curr_item_method_sym = title, list, curr_item_method_sym
|
10
12
|
draw
|
11
13
|
end
|
12
14
|
|
@@ -14,11 +16,12 @@ class ListWindow < PmWindow
|
|
14
16
|
super
|
15
17
|
return unless @list
|
16
18
|
|
19
|
+
curr_item = PM::PatchMaster.instance.send(@curr_item_method_sym)
|
17
20
|
@list.each_with_index do |thing, i|
|
18
21
|
@win.setpos(i+1, 1)
|
19
|
-
@win.attron(A_REVERSE) if thing ==
|
22
|
+
@win.attron(A_REVERSE) if thing == curr_item
|
20
23
|
@win.addstr(make_fit(" #{thing.name} "))
|
21
|
-
@win.attroff(A_REVERSE) if thing ==
|
24
|
+
@win.attroff(A_REVERSE) if thing == curr_item
|
22
25
|
end
|
23
26
|
end
|
24
27
|
|
data/lib/patchmaster/app/main.rb
CHANGED
@@ -1,13 +1,11 @@
|
|
1
1
|
require 'curses'
|
2
2
|
require 'singleton'
|
3
|
-
%w(list patch info prompt).each { |w| require "patchmaster/app/#{w}_window" }
|
3
|
+
%w(list patch info trigger prompt).each { |w| require "patchmaster/app/#{w}_window" }
|
4
4
|
|
5
5
|
module PM
|
6
6
|
|
7
7
|
class Main
|
8
8
|
|
9
|
-
DEBUG_FILE = '/tmp/pm_debug.txt'
|
10
|
-
|
11
9
|
include Singleton
|
12
10
|
include Curses
|
13
11
|
|
@@ -19,20 +17,15 @@ class Main
|
|
19
17
|
@pm.no_midi!
|
20
18
|
end
|
21
19
|
|
22
|
-
# File must be a Ruby file that returns an array of song lists.
|
23
20
|
def load(file)
|
24
21
|
@pm.load(file)
|
25
22
|
end
|
26
23
|
|
27
24
|
def run
|
28
|
-
if $DEBUG
|
29
|
-
@debug_file = File.open(DEBUG_FILE, 'a')
|
30
|
-
end
|
31
25
|
@pm.start
|
32
26
|
begin
|
33
27
|
config_curses
|
34
28
|
create_windows
|
35
|
-
message("Welcome to PatchMaster")
|
36
29
|
|
37
30
|
loop do
|
38
31
|
begin
|
@@ -40,55 +33,68 @@ class Main
|
|
40
33
|
ch = getch
|
41
34
|
message("ch = #{ch}") if $DEBUG
|
42
35
|
case ch
|
43
|
-
when
|
36
|
+
when 'j', Key::DOWN, ' '
|
44
37
|
@pm.next_patch
|
45
|
-
when
|
38
|
+
when 'k', Key::UP
|
46
39
|
@pm.prev_patch
|
47
|
-
when
|
40
|
+
when 'n', Key::LEFT
|
48
41
|
@pm.next_song
|
49
|
-
when
|
42
|
+
when 'p', Key::RIGHT
|
50
43
|
@pm.prev_song
|
51
|
-
when
|
44
|
+
when 'g'
|
52
45
|
name = PromptWindow.new('Go To Song', 'Go to song:').gets
|
53
46
|
@pm.goto_song(name)
|
54
|
-
when
|
47
|
+
when 't'
|
55
48
|
name = PromptWindow.new('Go To Song List', 'Go to Song List:').gets
|
56
49
|
@pm.goto_song_list(name)
|
50
|
+
when 'e'
|
51
|
+
close_screen
|
52
|
+
@pm.edit
|
57
53
|
when Key::F1
|
58
54
|
help
|
59
|
-
when 27
|
60
|
-
|
55
|
+
when 27 # "\e" doesn't work here
|
56
|
+
# Twice in a row sends individual note-off commands
|
57
|
+
message('Sending panic note off messages...')
|
58
|
+
@pm.panic(@prev_cmd == 27)
|
61
59
|
message('Panic sent')
|
62
|
-
when
|
60
|
+
when 'l'
|
63
61
|
file = PromptWindow.new('Load', 'Load file:').gets
|
64
62
|
begin
|
65
63
|
@pm.load(file)
|
64
|
+
message("Loaded #{file}")
|
66
65
|
rescue => ex
|
67
66
|
message(ex.to_s)
|
68
67
|
end
|
69
|
-
when
|
68
|
+
when 's'
|
70
69
|
file = PromptWindow.new('Save', 'Save into file:').gets
|
71
70
|
begin
|
72
71
|
@pm.save(file)
|
72
|
+
message("Saved #{file}")
|
73
73
|
rescue => ex
|
74
74
|
message(ex.to_s)
|
75
75
|
end
|
76
|
-
when ?
|
76
|
+
when '?'
|
77
|
+
if $DEBUG
|
78
|
+
require 'pp'
|
79
|
+
out = ''
|
80
|
+
str = pp(@pm, out)
|
81
|
+
message("pm = #{out}")
|
82
|
+
end
|
83
|
+
when 'q'
|
77
84
|
break
|
78
85
|
end
|
86
|
+
@prev_cmd = ch
|
79
87
|
rescue => ex
|
80
88
|
message(ex.to_s)
|
81
|
-
|
82
|
-
@debug_file.puts caller.join("\n")
|
83
|
-
end
|
89
|
+
@pm.debug caller.join("\n")
|
84
90
|
end
|
85
91
|
end
|
86
92
|
ensure
|
93
|
+
clear
|
94
|
+
refresh
|
87
95
|
close_screen
|
88
96
|
@pm.stop
|
89
|
-
|
90
|
-
@debug_file.close
|
91
|
-
end
|
97
|
+
@pm.close_debug_file
|
92
98
|
end
|
93
99
|
end
|
94
100
|
|
@@ -109,14 +115,22 @@ class Main
|
|
109
115
|
sl_height = top_height - sls_height
|
110
116
|
|
111
117
|
@song_lists_win = ListWindow.new(sls_height, top_width, 0, 0, nil)
|
112
|
-
@song_lists_win.set_contents('Song Lists', @pm.song_lists)
|
113
118
|
@song_list_win = ListWindow.new(sl_height, top_width, sls_height, 0, 'Song List')
|
114
119
|
@song_win = ListWindow.new(top_height, top_width, 0, top_width, 'Song')
|
115
120
|
@patch_win = PatchWindow.new(bot_height, cols(), top_height, 0, 'Patch')
|
116
121
|
@message_win = Window.new(1, cols(), lines()-1, 0)
|
122
|
+
@message_win.scrollok(false)
|
123
|
+
|
124
|
+
third_height = top_height / 3
|
125
|
+
width = cols() - (top_width * 2) - 1
|
126
|
+
left = top_width * 2 + 1
|
117
127
|
|
118
|
-
@
|
128
|
+
@trigger_win = TriggerWindow.new(third_height, width, third_height * 2, left)
|
129
|
+
@trigger_win.draw
|
130
|
+
|
131
|
+
@info_win = InfoWindow.new(third_height * 2, width, 0, left)
|
119
132
|
@info_win.draw
|
133
|
+
|
120
134
|
end
|
121
135
|
|
122
136
|
def help
|
@@ -131,28 +145,29 @@ class Main
|
|
131
145
|
else
|
132
146
|
$stderr.puts str
|
133
147
|
end
|
134
|
-
|
135
|
-
@debug_file.puts "#{Time.now} #{str}"
|
136
|
-
@debug_file.flush
|
137
|
-
end
|
148
|
+
@pm.debug "#{Time.now} #{str}"
|
138
149
|
end
|
139
150
|
|
140
151
|
def refresh_all
|
141
152
|
set_window_data
|
142
|
-
[@song_lists_win, @song_list_win, @song_win, @patch_win]
|
143
|
-
|
153
|
+
wins = [@song_lists_win, @song_list_win, @song_win, @patch_win, @info_win, @trigger_win]
|
154
|
+
wins.map(&:draw)
|
155
|
+
([stdscr] + wins).map(&:refresh)
|
144
156
|
end
|
145
157
|
|
146
158
|
def set_window_data
|
147
|
-
|
148
|
-
|
149
|
-
|
159
|
+
@song_lists_win.set_contents('Song Lists', @pm.song_lists, :song_list)
|
160
|
+
|
161
|
+
song_list = @pm.song_list
|
162
|
+
@song_list_win.set_contents(song_list.name, song_list.songs, :song)
|
163
|
+
|
164
|
+
song = @pm.song
|
150
165
|
if song
|
151
|
-
@song_win.set_contents(song.name, song.patches)
|
152
|
-
patch =
|
166
|
+
@song_win.set_contents(song.name, song.patches, :patch)
|
167
|
+
patch = @pm.patch
|
153
168
|
@patch_win.patch = patch
|
154
169
|
else
|
155
|
-
@song_win.set_contents(nil, nil)
|
170
|
+
@song_win.set_contents(nil, nil, :patch)
|
156
171
|
@patch_win.patch = nil
|
157
172
|
end
|
158
173
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'curses'
|
2
|
+
|
3
|
+
module PM
|
4
|
+
class TriggerWindow < PmWindow
|
5
|
+
|
6
|
+
include Curses
|
7
|
+
|
8
|
+
def initialize(rows, cols, row, col)
|
9
|
+
super(rows, cols, row, col, nil)
|
10
|
+
@title = 'Triggers '
|
11
|
+
end
|
12
|
+
|
13
|
+
def draw
|
14
|
+
super
|
15
|
+
pm = PM::PatchMaster.instance
|
16
|
+
i = 0
|
17
|
+
pm.inputs.each do |sym, instrument|
|
18
|
+
instrument.triggers.each do |trigger|
|
19
|
+
@win.setpos(i+1, 1)
|
20
|
+
@win.addstr(make_fit(":#{sym} #{trigger.to_s}"))
|
21
|
+
i += 1
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
@@ -2,9 +2,10 @@ require 'patchmaster/consts'
|
|
2
2
|
|
3
3
|
module PM
|
4
4
|
|
5
|
-
# A Connection connects an
|
6
|
-
# data arrives at the
|
7
|
-
# then the remaining modified data is sent to the
|
5
|
+
# A Connection connects an InputInstrument to an OutputInstrument. Whenever
|
6
|
+
# MIDI data arrives at the InputInstrument it is optionally modified or
|
7
|
+
# filtered, then the remaining modified data is sent to the
|
8
|
+
# OutputInstrument.
|
8
9
|
class Connection
|
9
10
|
|
10
11
|
attr_accessor :input, :input_chan, :output, :output_chan,
|
@@ -24,19 +25,20 @@ class Connection
|
|
24
25
|
end
|
25
26
|
|
26
27
|
def start(start_bytes=nil)
|
27
|
-
midi_out(
|
28
|
-
midi_out(
|
28
|
+
midi_out(start_bytes) if start_bytes
|
29
|
+
midi_out([PROGRAM_CHANGE + @output_chan, @pc_prog]) if pc?
|
29
30
|
@input.add_connection(self)
|
30
31
|
end
|
31
32
|
|
32
|
-
def stop
|
33
|
+
def stop(stop_bytes=nil)
|
34
|
+
midi_out(stop_bytes) if stop_bytes
|
33
35
|
@input.remove_connection(self)
|
34
36
|
end
|
35
37
|
|
36
38
|
def accept_from_input?(bytes)
|
37
39
|
return true if @input_chan == nil
|
38
|
-
return true
|
39
|
-
|
40
|
+
return true unless bytes.channel?
|
41
|
+
bytes.note? && bytes.channel == @input_chan
|
40
42
|
end
|
41
43
|
|
42
44
|
# Returns true if the +@zone+ is nil (allowing all notes throught) or if
|
@@ -48,8 +50,8 @@ class Connection
|
|
48
50
|
def midi_in(bytes)
|
49
51
|
return unless accept_from_input?(bytes)
|
50
52
|
|
51
|
-
# TODO handle running bytes
|
52
|
-
high_nibble = bytes
|
53
|
+
# TODO handle running bytes if needed
|
54
|
+
high_nibble = bytes.high_nibble
|
53
55
|
case high_nibble
|
54
56
|
when NOTE_ON, NOTE_OFF, POLY_PRESSURE
|
55
57
|
return unless inside_zone?(bytes[1])
|
@@ -61,12 +63,12 @@ class Connection
|
|
61
63
|
|
62
64
|
bytes = @filter.call(self, bytes) if @filter
|
63
65
|
if bytes && bytes.size > 0
|
64
|
-
midi_out(
|
66
|
+
midi_out(bytes)
|
65
67
|
end
|
66
68
|
end
|
67
69
|
|
68
|
-
def midi_out(
|
69
|
-
|
70
|
+
def midi_out(bytes)
|
71
|
+
@output.midi_out(bytes)
|
70
72
|
end
|
71
73
|
|
72
74
|
def pc?
|
@@ -0,0 +1,174 @@
|
|
1
|
+
module PM
|
2
|
+
|
3
|
+
# A PM::Cursor knows the current PM::SongList, PM::Song, and PM::Patch, how
|
4
|
+
# to move between songs and patches, and how to find them given name
|
5
|
+
# regexes. A Cursor does not start/stop patches or manage connections.
|
6
|
+
class Cursor
|
7
|
+
|
8
|
+
attr_reader :song_list, :song, :patch
|
9
|
+
|
10
|
+
def initialize(pm)
|
11
|
+
@pm = pm
|
12
|
+
clear
|
13
|
+
end
|
14
|
+
|
15
|
+
# Set @song_list, @song, and @patch to +nil+.
|
16
|
+
def clear
|
17
|
+
@song_list = @song = @patch = nil
|
18
|
+
# Do not erase names saved by #mark.
|
19
|
+
end
|
20
|
+
|
21
|
+
# Set @song_list to All Songs, @song to first song, and
|
22
|
+
# @patch to song's first patch. Song and patch may be +nil+.
|
23
|
+
def init
|
24
|
+
@song_list = @pm.song_lists.first
|
25
|
+
@song = @song_list.songs.first
|
26
|
+
if @song
|
27
|
+
@patch = @song.patches.first
|
28
|
+
else
|
29
|
+
@patch = nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def next_song
|
34
|
+
return unless @song_list
|
35
|
+
return if @song_list.songs.last == @song
|
36
|
+
|
37
|
+
@patch.stop if @patch
|
38
|
+
@song = @song_list.songs[@song_list.songs.index(@song) + 1]
|
39
|
+
@patch = @song.patches.first
|
40
|
+
@patch.start
|
41
|
+
end
|
42
|
+
|
43
|
+
def prev_song
|
44
|
+
return unless @song_list
|
45
|
+
return if @song_list.songs.first == @song
|
46
|
+
|
47
|
+
@patch.stop if @patch
|
48
|
+
@song = @song_list.songs[@song_list.songs.index(@song) - 1]
|
49
|
+
@patch = @song.patches.first
|
50
|
+
@patch.start
|
51
|
+
end
|
52
|
+
|
53
|
+
def next_patch
|
54
|
+
return unless @song
|
55
|
+
if @song.patches.last == @patch
|
56
|
+
next_song
|
57
|
+
elsif @patch
|
58
|
+
@patch.stop
|
59
|
+
@patch = @song.patches[@song.patches.index(@patch) + 1]
|
60
|
+
@patch.start
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def prev_patch
|
65
|
+
return unless @song
|
66
|
+
if @song.patches.first == @patch
|
67
|
+
prev_song
|
68
|
+
elsif @patch
|
69
|
+
@patch.stop
|
70
|
+
@patch = @song.patches[@song.patches.index(@patch) - 1]
|
71
|
+
@patch.start
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def goto_song(name_regex)
|
76
|
+
new_song_list = new_song = new_patch = nil
|
77
|
+
new_song = @song_list.find(name_regex) if @song_list
|
78
|
+
new_song = @@pm.all_songs.find(name_regex) unless new_song
|
79
|
+
new_patch = new_song ? new_song.patches.first : nil
|
80
|
+
|
81
|
+
if (new_song && new_song != @song) || # moved to new song
|
82
|
+
(new_song == @song && @patch != new_patch) # same song but not at same first patch
|
83
|
+
|
84
|
+
@patch.stop if @patch
|
85
|
+
|
86
|
+
if @song_list.songs.include?(new_song)
|
87
|
+
new_song_list = @song_list
|
88
|
+
else
|
89
|
+
# Not found in current song list. Switch to @pm.all_songs list.
|
90
|
+
new_song_list = @@pm.all_songs
|
91
|
+
end
|
92
|
+
|
93
|
+
@song_list = new_song_list
|
94
|
+
@song = new_song
|
95
|
+
@patch = new_patch
|
96
|
+
@patch.start
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def goto_song_list(name_regex)
|
101
|
+
name_regex = Regexp.new(name_regex.to_s, true) # make case-insensitive
|
102
|
+
new_song_list = @pm.song_lists.detect { |song_list| song_list.name =~ name_regex }
|
103
|
+
return unless new_song_list
|
104
|
+
|
105
|
+
@song_list = new_song_list
|
106
|
+
|
107
|
+
new_song = @song_list.songs.first
|
108
|
+
new_patch = new_song ? new_song.patches.first : nil
|
109
|
+
|
110
|
+
if new_patch != @patch
|
111
|
+
@patch.stop if @patch
|
112
|
+
new_patch.start if new_patch
|
113
|
+
end
|
114
|
+
@song = new_song
|
115
|
+
@patch = new_patch
|
116
|
+
end
|
117
|
+
|
118
|
+
# Remembers the names of the current song list, song, and patch.
|
119
|
+
# Used by #restore.
|
120
|
+
def mark
|
121
|
+
@song_list_name = @song_list ? @song_list.name : nil
|
122
|
+
@song_name = @song ? @song.name : nil
|
123
|
+
@patch_name = @patch ? @patch.name : nil
|
124
|
+
end
|
125
|
+
|
126
|
+
# Using the names saved by #save, try to find them now.
|
127
|
+
#
|
128
|
+
# Since names can change we use Damerau-Levenshtein distance on lowercase
|
129
|
+
# versions of all strings.
|
130
|
+
def restore
|
131
|
+
return unless @song_list_name # will be nil on initial load
|
132
|
+
|
133
|
+
@song_list = find_nearest_match(@pm.song_lists, @song_list_name) || @pm.all_songs
|
134
|
+
@song = find_nearest_match(@song_list.songs, @song_name) || @song_list.songs.first
|
135
|
+
if @song
|
136
|
+
@patch = find_nearest_match(@song.patches, @patch_name) || @song.patches.first
|
137
|
+
else
|
138
|
+
@patch = nil
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# List must contain objects that respond to #name. If +str+ is nil or
|
143
|
+
# +list+ is +nil+ or empty then +nil+ is returned.
|
144
|
+
def find_nearest_match(list, str)
|
145
|
+
return nil unless str && list && !list.empty?
|
146
|
+
|
147
|
+
str = str.downcase
|
148
|
+
distances = list.collect { |item| dameraulevenshtein(str, item.name.downcase) }
|
149
|
+
min_distance = distances.min
|
150
|
+
list[distances.index(distances.min)]
|
151
|
+
end
|
152
|
+
|
153
|
+
# https://gist.github.com/182759 (git://gist.github.com/182759.git)
|
154
|
+
# Referenced from http://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance
|
155
|
+
def dameraulevenshtein(seq1, seq2)
|
156
|
+
oneago = nil
|
157
|
+
thisrow = (1..seq2.size).to_a + [0]
|
158
|
+
seq1.size.times do |x|
|
159
|
+
twoago, oneago, thisrow = oneago, thisrow, [0] * seq2.size + [x + 1]
|
160
|
+
seq2.size.times do |y|
|
161
|
+
delcost = oneago[y] + 1
|
162
|
+
addcost = thisrow[y - 1] + 1
|
163
|
+
subcost = oneago[y - 1] + ((seq1[x] != seq2[y]) ? 1 : 0)
|
164
|
+
thisrow[y] = [delcost, addcost, subcost].min
|
165
|
+
if (x > 0 and y > 0 and seq1[x] == seq2[y-1] and seq1[x-1] == seq2[y] and seq1[x] != seq2[y])
|
166
|
+
thisrow[y] = [thisrow[y], twoago[y-2] + 1].min
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
return thisrow[seq2.size - 1]
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
174
|
+
end
|