patchmaster 0.0.0 → 0.0.1
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/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
|