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/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
|