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/lib/patchmaster/dsl.rb
CHANGED
@@ -14,27 +14,39 @@ class DSL
|
|
14
14
|
|
15
15
|
def load(file)
|
16
16
|
contents = IO.read(file)
|
17
|
+
@triggers = []
|
17
18
|
@filters = []
|
19
|
+
@songs = {} # key = name, value = song
|
18
20
|
instance_eval(contents)
|
21
|
+
read_triggers(contents)
|
19
22
|
read_filters(contents)
|
20
23
|
end
|
21
24
|
|
22
25
|
def input(port_num, sym, name=nil)
|
23
|
-
@pm.inputs[sym] =
|
26
|
+
@pm.inputs[sym] = InputInstrument.new(name || sym.to_s, port_num, @no_midi)
|
24
27
|
rescue => ex
|
25
|
-
raise "error creating input
|
28
|
+
raise "input: error creating input instrument \"#{name}\" on input port #{port_num}: #{ex}"
|
26
29
|
end
|
27
30
|
alias_method :in, :input
|
28
31
|
|
29
32
|
def output(port_num, sym, name=nil)
|
30
|
-
@pm.outputs[sym] =
|
33
|
+
@pm.outputs[sym] = OutputInstrument.new(name || sym.to_s, port_num, @no_midi)
|
31
34
|
rescue => ex
|
32
|
-
raise "error creating output
|
35
|
+
raise "output: error creating output instrument \"#{name}\" on output port #{port_num}: #{ex}"
|
33
36
|
end
|
34
37
|
alias_method :out, :output
|
35
38
|
|
39
|
+
def trigger(instrument_sym, bytes, &block)
|
40
|
+
instrument = @pm.inputs[instrument_sym]
|
41
|
+
raise "trigger: error finding instrument #{instrument_sym}" unless instrument
|
42
|
+
t = Trigger.new(bytes, block)
|
43
|
+
instrument.triggers << t
|
44
|
+
@triggers << t
|
45
|
+
end
|
46
|
+
|
36
47
|
def song(name)
|
37
48
|
@song = Song.new(name) # ctor saves into @pm.all_songs
|
49
|
+
@songs[name] = @song
|
38
50
|
yield @song if block_given?
|
39
51
|
end
|
40
52
|
|
@@ -50,6 +62,7 @@ class DSL
|
|
50
62
|
|
51
63
|
def connection(in_sym, in_chan, out_sym, out_chan)
|
52
64
|
input = @pm.inputs[in_sym]
|
65
|
+
in_chan = nil if in_chan == :all || in_chan == :any
|
53
66
|
raise "can't find input instrument #{in_sym}" unless input
|
54
67
|
output = @pm.outputs[out_sym]
|
55
68
|
raise "can't find outputput instrument #{out_sym}" unless output
|
@@ -95,7 +108,7 @@ class DSL
|
|
95
108
|
sl = SongList.new(name)
|
96
109
|
@pm.song_lists << sl
|
97
110
|
song_names.each do |sn|
|
98
|
-
song = @
|
111
|
+
song = @songs[sn]
|
99
112
|
raise "song \"#{sn}\" not found (song list \"#{name}\")" unless song
|
100
113
|
sl << song
|
101
114
|
end
|
@@ -106,18 +119,29 @@ class DSL
|
|
106
119
|
def save(file)
|
107
120
|
File.open(file, 'w') { |f|
|
108
121
|
save_instruments(f)
|
122
|
+
save_triggers(f)
|
109
123
|
save_songs(f)
|
110
124
|
save_song_lists(f)
|
111
125
|
}
|
112
126
|
end
|
113
127
|
|
114
128
|
def save_instruments(f)
|
115
|
-
@pm.inputs.each
|
129
|
+
@pm.inputs.each do |sym, instr|
|
116
130
|
f.puts "input #{instr.port_num}, :#{sym}, #{quoted(instr.name)}"
|
117
|
-
|
118
|
-
@pm.outputs.each
|
131
|
+
end
|
132
|
+
@pm.outputs.each do |sym, instr|
|
119
133
|
f.puts "output #{instr.port_num}, :#{sym}, #{quoted(instr.name)}"
|
120
|
-
|
134
|
+
end
|
135
|
+
f.puts
|
136
|
+
end
|
137
|
+
|
138
|
+
def save_triggers(f)
|
139
|
+
@pm.inputs.each do |sym, instrument|
|
140
|
+
instrument.triggers.each do |trigger|
|
141
|
+
str = "trigger :#{sym}, #{trigger.bytes.inspect} #{trigger.text}"
|
142
|
+
f.puts str
|
143
|
+
end
|
144
|
+
end
|
121
145
|
f.puts
|
122
146
|
end
|
123
147
|
|
@@ -138,11 +162,11 @@ class DSL
|
|
138
162
|
end
|
139
163
|
|
140
164
|
def save_connection(f, conn)
|
141
|
-
in_sym = @pm.inputs.
|
165
|
+
in_sym = @pm.inputs.key(conn.input)
|
142
166
|
in_chan = conn.input_chan ? conn.input_chan + 1 : 'nil'
|
143
|
-
out_sym = @pm.outputs.
|
167
|
+
out_sym = @pm.outputs.key(conn.output)
|
144
168
|
out_chan = conn.output_chan + 1
|
145
|
-
f.puts " conn :#{in_sym}, #{in_chan},
|
169
|
+
f.puts " conn :#{in_sym}, #{in_chan}, :#{out_sym}, #{out_chan} do"
|
146
170
|
f.puts " prog_chg #{conn.pc_prog}" if conn.pc?
|
147
171
|
f.puts " zone #{conn.note_num_to_name(conn.zone.begin)}, #{conn.note_num_to_name(conn.zone.end)}" if conn.zone
|
148
172
|
f.puts " xpose #{conn.xpose}" if conn.xpose
|
@@ -185,42 +209,51 @@ class DSL
|
|
185
209
|
end
|
186
210
|
end
|
187
211
|
|
188
|
-
|
189
|
-
|
212
|
+
def read_triggers(contents)
|
213
|
+
read_block_text('trigger', @triggers, contents)
|
214
|
+
end
|
215
|
+
|
190
216
|
def read_filters(contents)
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
217
|
+
read_block_text('filter', @filters, contents)
|
218
|
+
end
|
219
|
+
|
220
|
+
# Extremely simple block text reader. Relies on indentation to detect end
|
221
|
+
# of code block.
|
222
|
+
def read_block_text(name, containers, contents)
|
223
|
+
i = -1
|
224
|
+
in_block = false
|
225
|
+
block_indentation = nil
|
226
|
+
block_end_token = nil
|
195
227
|
contents.each_line do |line|
|
196
|
-
if line =~ /^(\s*)
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
228
|
+
if line =~ /^(\s*)#{name}\s*.*?(({|do|->\s*{|lambda\s*{)(.*))/
|
229
|
+
block_indentation, text = $1, $2
|
230
|
+
i += 1
|
231
|
+
containers[i].text = text + "\n"
|
232
|
+
in_block = true
|
233
|
+
block_end_token = case text
|
201
234
|
when /^{/
|
202
235
|
"}"
|
203
236
|
when /^do\b/
|
204
237
|
"end"
|
205
|
-
when /^lambda\s*({|do)/
|
206
|
-
$
|
238
|
+
when /^(->|lambda)\s*({|do)/
|
239
|
+
$2 == "{" ? "}" : "end"
|
207
240
|
else
|
208
241
|
"}|end" # regex
|
209
242
|
end
|
210
|
-
elsif
|
243
|
+
elsif in_block
|
211
244
|
line =~ /^(\s*)(.*)/
|
212
245
|
indentation, text = $1, $2
|
213
|
-
if indentation.length <=
|
214
|
-
if text =~ /^#{
|
215
|
-
|
246
|
+
if indentation.length <= block_indentation.length
|
247
|
+
if text =~ /^#{block_end_token}/
|
248
|
+
containers[i].text << line
|
216
249
|
end
|
217
|
-
|
218
|
-
in_filter = false
|
250
|
+
in_block = false
|
219
251
|
else
|
220
|
-
|
252
|
+
containers[i].text << line
|
221
253
|
end
|
222
254
|
end
|
223
255
|
end
|
256
|
+
containers.each { |thing| thing.text.strip! }
|
224
257
|
end
|
225
258
|
|
226
259
|
end
|
data/lib/patchmaster/filter.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
+
require 'midi-eye'
|
2
|
+
|
1
3
|
# Ports are UniMIDI inputs or outputs.
|
2
4
|
module PM
|
3
5
|
|
4
|
-
class
|
6
|
+
class Instrument
|
5
7
|
|
6
|
-
attr_reader :name, :port_num, :port
|
8
|
+
attr_reader :name, :port_num, :port, :listener
|
7
9
|
|
8
10
|
def initialize(name, port_num, port)
|
9
11
|
@name, @port_num, @port = name, port_num, port
|
@@ -11,16 +13,17 @@ class MIDIDevice
|
|
11
13
|
|
12
14
|
end
|
13
15
|
|
14
|
-
# When a connection is started, it adds itself to this
|
16
|
+
# When a connection is started, it adds itself to this InputInstrument's
|
15
17
|
# +@connections+. When it ends, it removes itself.
|
16
|
-
class
|
18
|
+
class InputInstrument < Instrument
|
17
19
|
|
18
|
-
attr_accessor :connections
|
20
|
+
attr_accessor :connections, :triggers
|
19
21
|
|
20
22
|
# If +port+ is nil (the normal case), creates either a real or a mock port
|
21
23
|
def initialize(name, port_num, no_midi=false)
|
22
24
|
super(name, port_num, input_port(port_num, no_midi))
|
23
25
|
@connections = []
|
26
|
+
@triggers = []
|
24
27
|
end
|
25
28
|
|
26
29
|
def add_connection(conn)
|
@@ -32,12 +35,22 @@ class InputDevice < MIDIDevice
|
|
32
35
|
end
|
33
36
|
|
34
37
|
# Poll for more MIDI input and process it.
|
35
|
-
def
|
36
|
-
|
38
|
+
def start
|
39
|
+
PatchMaster.instance.debug("instrument #{name} start")
|
40
|
+
@port.clear_buffer
|
41
|
+
@listener = MIDIEye::Listener.new(@port).listen_for { |event| midi_in(event[:message].to_bytes) }
|
42
|
+
@listener.run(:background => true)
|
43
|
+
end
|
44
|
+
|
45
|
+
def stop
|
46
|
+
PatchMaster.instance.debug("instrument #{name} stop")
|
47
|
+
@port.clear_buffer
|
48
|
+
@listener.close
|
37
49
|
end
|
38
50
|
|
39
|
-
# Passes MIDI bytes on to each output connection
|
51
|
+
# Passes MIDI bytes on to triggers and to each output connection.
|
40
52
|
def midi_in(bytes)
|
53
|
+
@triggers.each { |trigger| trigger.signal(bytes) }
|
41
54
|
@connections.each { |conn| conn.midi_in(bytes) }
|
42
55
|
end
|
43
56
|
|
@@ -53,7 +66,7 @@ class InputDevice < MIDIDevice
|
|
53
66
|
|
54
67
|
end
|
55
68
|
|
56
|
-
class
|
69
|
+
class OutputInstrument < Instrument
|
57
70
|
|
58
71
|
def initialize(name, port_num, no_midi=false)
|
59
72
|
super(name, port_num, output_port(port_num, no_midi))
|
@@ -75,8 +88,8 @@ class OutputDevice < MIDIDevice
|
|
75
88
|
end
|
76
89
|
|
77
90
|
class MockInputPort
|
78
|
-
def
|
79
|
-
[]
|
91
|
+
def gets
|
92
|
+
[{:data => [], :timestamp => 0}]
|
80
93
|
end
|
81
94
|
end
|
82
95
|
|
data/lib/patchmaster/patch.rb
CHANGED
@@ -2,12 +2,11 @@ module PM
|
|
2
2
|
|
3
3
|
class Patch
|
4
4
|
|
5
|
-
attr_accessor :name, :connections, :start_bytes
|
5
|
+
attr_accessor :name, :connections, :start_bytes, :stop_bytes
|
6
6
|
|
7
|
-
def initialize(name, start_bytes=nil)
|
8
|
-
@name = name
|
7
|
+
def initialize(name, start_bytes=nil, stop_bytes=nil)
|
8
|
+
@name, @start_bytes, @stop_bytes = name, start_bytes, stop_bytes
|
9
9
|
@connections = []
|
10
|
-
@start_bytes = start_bytes
|
11
10
|
@running = false
|
12
11
|
end
|
13
12
|
|
@@ -19,17 +18,11 @@ class Patch
|
|
19
18
|
@connections.map(&:input).uniq
|
20
19
|
end
|
21
20
|
|
22
|
-
# Send start_bytes to each connection
|
23
|
-
# receives input and passes it on to each connection.
|
21
|
+
# Send start_bytes to each connection.
|
24
22
|
def start
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
Thread.new do
|
29
|
-
loop do
|
30
|
-
break unless @running
|
31
|
-
input_devices.map(&:gets_data)
|
32
|
-
end
|
23
|
+
unless @running
|
24
|
+
@connections.each { |conn| conn.start(@start_bytes) }
|
25
|
+
@running = true
|
33
26
|
end
|
34
27
|
end
|
35
28
|
|
@@ -37,12 +30,12 @@ class Patch
|
|
37
30
|
@running
|
38
31
|
end
|
39
32
|
|
33
|
+
# Send stop_bytes to each connection, then call #stop on each connection.
|
40
34
|
def stop
|
41
35
|
if @running
|
42
36
|
@running = false
|
43
|
-
@connections.
|
37
|
+
@connections.each { |conn| conn.stop(@stop_bytes) }
|
44
38
|
end
|
45
39
|
end
|
46
40
|
end
|
47
|
-
|
48
|
-
end # PM
|
41
|
+
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
require 'singleton'
|
2
|
+
require 'delegate'
|
2
3
|
require 'patchmaster/sorted_song_list'
|
4
|
+
require 'patchmaster/cursor'
|
3
5
|
|
4
6
|
module PM
|
5
7
|
|
@@ -12,157 +14,139 @@ module PM
|
|
12
14
|
# PatchMaster.instance.start
|
13
15
|
# # ...when you're done
|
14
16
|
# PatchMaster.instance.stop
|
15
|
-
class PatchMaster
|
17
|
+
class PatchMaster < SimpleDelegator
|
18
|
+
|
19
|
+
DEBUG_FILE = '/tmp/pm_debug.txt'
|
16
20
|
|
17
21
|
include Singleton
|
18
22
|
|
19
23
|
attr_reader :inputs, :outputs, :all_songs, :song_lists, :no_midi
|
20
|
-
|
24
|
+
|
25
|
+
# A Cursor to which we delegate incoming position methods (#song_list,
|
26
|
+
# #song, #patch, #next_song, #prev_patch, etc.)
|
27
|
+
attr_reader :cursor
|
21
28
|
|
22
29
|
def initialize
|
23
|
-
|
30
|
+
@cursor = Cursor.new(self)
|
31
|
+
super(@cursor)
|
32
|
+
|
33
|
+
if $DEBUG
|
34
|
+
@debug_file = File.open(DEBUG_FILE, 'a')
|
35
|
+
end
|
24
36
|
@no_midi = false
|
25
|
-
|
37
|
+
|
38
|
+
init_data
|
26
39
|
end
|
27
40
|
|
28
41
|
def no_midi!
|
29
42
|
@no_midi = true
|
30
43
|
end
|
31
44
|
|
45
|
+
# Loads +file+. Does its best to restore the current song list, song, and
|
46
|
+
# patch after loading.
|
32
47
|
def load(file)
|
48
|
+
restart = running?
|
33
49
|
stop
|
50
|
+
|
51
|
+
@cursor.mark
|
34
52
|
init_data
|
35
53
|
DSL.new(@no_midi).load(file)
|
54
|
+
@loaded_file = file
|
55
|
+
@cursor.restore
|
56
|
+
|
57
|
+
if restart
|
58
|
+
start(false)
|
59
|
+
elsif @cursor.patch
|
60
|
+
@cursor.patch.start
|
61
|
+
end
|
36
62
|
rescue => ex
|
37
63
|
raise("error loading #{file}: #{ex}\n" + caller.join("\n"))
|
38
64
|
end
|
39
65
|
|
40
66
|
def save(file)
|
41
67
|
DSL.new(@no_midi).save(file)
|
42
|
-
|
68
|
+
@loaded_file = file
|
43
69
|
rescue => ex
|
44
|
-
raise("error saving #{file}: #{ex}")
|
70
|
+
raise("error saving #{file}: #{ex}" + caller.join("\n"))
|
45
71
|
end
|
46
72
|
|
73
|
+
# Initializes the cursor and all data.
|
47
74
|
def init_data
|
48
|
-
@
|
75
|
+
@cursor.clear
|
49
76
|
@inputs = {}
|
50
77
|
@outputs = {}
|
51
|
-
@song_lists =
|
78
|
+
@song_lists = []
|
52
79
|
@all_songs = SortedSongList.new('All Songs')
|
53
80
|
@song_lists << @all_songs
|
54
81
|
end
|
55
82
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
if
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
@curr_patch = nil
|
64
|
-
end
|
83
|
+
# If +init_cursor+ is +true+ (the default), initializes current song list,
|
84
|
+
# song, and patch.
|
85
|
+
def start(init_cursor = true)
|
86
|
+
@cursor.init if init_cursor
|
87
|
+
@cursor.patch.start if @cursor.patch
|
88
|
+
@running = true
|
89
|
+
@inputs.values.map(&:start)
|
65
90
|
end
|
66
91
|
|
67
92
|
def stop
|
68
|
-
@
|
69
|
-
@
|
93
|
+
@cursor.patch.stop if @cursor.patch
|
94
|
+
@inputs.values.map(&:stop)
|
95
|
+
@running = false
|
70
96
|
end
|
71
97
|
|
72
|
-
def
|
73
|
-
|
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
|
98
|
+
def running?
|
99
|
+
@running
|
80
100
|
end
|
81
101
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
@
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
102
|
+
# Sends the +CM_ALL_NOTES_OFF+ controller message to all output
|
103
|
+
# instruments on all 16 MIDI channels. If +individual_notes+ is +true+
|
104
|
+
# send individual +NOTE_OFF+ messages to all notes as well.
|
105
|
+
def panic(individual_notes=false)
|
106
|
+
debug("panic(#{individual_notes})")
|
107
|
+
@outputs.values.each do |out|
|
108
|
+
MIDI_CHANNELS.times do |chan|
|
109
|
+
out.midi_out([CONTROLLER + chan, CM_ALL_NOTES_OFF, 0])
|
110
|
+
if individual_notes
|
111
|
+
128.times { |note| out.midi_out([NOTE_OFF + chan, note, 0]) }
|
112
|
+
end
|
113
|
+
end
|
100
114
|
end
|
101
115
|
end
|
102
116
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
@curr_patch.start
|
117
|
+
# Opens the most recently loaded/saved file name in an editor. After
|
118
|
+
# editing, the file is re-loaded.
|
119
|
+
def edit
|
120
|
+
editor_command = find_editor
|
121
|
+
unless editor_command
|
122
|
+
message("Can not find $VISUAL, $EDITOR, vim, or vi on your path")
|
123
|
+
return
|
111
124
|
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
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
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
|
126
|
+
cmd = "#{editor_command} #{@loaded_file}"
|
127
|
+
debug(cmd)
|
128
|
+
system(cmd)
|
129
|
+
load(@loaded_file)
|
138
130
|
end
|
139
131
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
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
|
132
|
+
# Return the first legit command from $VISUAL, $EDITOR, vim, vi, and
|
133
|
+
# notepad.exe.
|
134
|
+
def find_editor
|
135
|
+
@editor ||= [ENV['VISUAL'], ENV['EDITOR'], 'vim', 'vi', 'notepad.exe'].compact.detect do |cmd|
|
136
|
+
system('which', cmd) || File.exist?(cmd)
|
154
137
|
end
|
155
|
-
@curr_song = new_song
|
156
|
-
@curr_patch = new_patch
|
157
138
|
end
|
158
139
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
140
|
+
# Output +str+ to @debug_file or $stderr.
|
141
|
+
def debug(str)
|
142
|
+
return unless $DEBUG
|
143
|
+
f = @debug_file || $stderr
|
144
|
+
f.puts str
|
145
|
+
f.flush
|
165
146
|
end
|
166
147
|
|
148
|
+
def close_debug_file
|
149
|
+
@debug_file.close if @debug_file
|
150
|
+
end
|
167
151
|
end
|
168
152
|
end
|