OXYGENHUB
◆ main openmsx / share / scripts / _vgmrecorder.tcl
27.3 KiB · Tcl · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
namespace eval vgm {
2
variable active false
3
4
variable psg_register
5
variable opll_register
6
variable y8950_register
7
variable opl4_register_wave
8
variable opl4_register
9
variable active_fm_register -1
10
variable opl3_register
11
variable opl3_port
12
13
variable start_time 0
14
variable tick_time 0
15
variable ticks
16
variable music_data
17
variable file_name
18
variable original_filename
19
variable directory [file normalize $::env(OPENMSX_USER_DATA)/../vgm_recordings]
20
21
variable psg_logged       false
22
variable fm_logged        false
23
variable y2151_logged     false
24
variable y8950_logged     false
25
variable moonsound_logged false
26
variable scc_logged       false
27
variable opl3_logged      false
28
29
variable scc_plus_used
30
31
variable watchpoints [list]
32
33
variable loop_amount 0
34
variable position 0
35
36
variable auto_next false
37
38
variable mbwave_title_hack       false
39
variable mbwave_loop_hack	 false
40
variable mbwave_basic_title_hack false
41
42
variable supported_chips [list MSX-Music PSG MoonSound MSX-Audio SCC SFG OPL3]
43
44
set_help_proc vgm_rec [namespace code vgm_rec_help]
45
proc vgm_rec_help {args} {
46
        switch -- [lindex $args 1] {
47
                "start"    {return {VGM recording will be initialised, specify one or more soundchips to record.
48
49
Syntax: vgm_rec start <MSX-Audio|MSX-Music|Moonsound|PSG|SCC|SFG|OPL3>
50
51
Actual recording will start when audio is detected to avoid silence at the beginning of the recording. This mechanism will only work if the MSX and/or playback routine does not send data to the soundchip when not playing, recording will start immediately in those cases.
52
}}
53
                "stop" {return {Stop recording and save the data to the openMSX user directory in vgm_recordings. By default the filename will be music0001.vgm, when this exists music0002.vgm etc...
54
55
Syntax: vgm_rec stop
56
}}
57
                "abort"  {return {Abort an active recording without saving the data.
58
59
Syntax: vgm_rec abort
60
}}
61
                "next"   {return {Stops current recording and starts recording the next track, remembering the chips to record for.
62
63
Syntax: vgm_rec next
64
}}
65
                "auto_next"   {return {Enables the auto_next recording; if no data is being sent to the chip for more than 2 seconds, the next recording will be started. Optional argument true/false, defaults to true.
66
67
Syntax: vgm_rec auto_next
68
}}
69
                "prefix"   {return {Specify the prefix of the VGM files, instead of the default music.
70
71
Syntax: vgm_rec prefix
72
}}
73
                "enable_hack"   {return {Enables one of three hacks; MBWave_basic_title, MBWave_title or MBWave_loop.
74
75
Syntax: vgm_rec enable_hack <MBWave_basic_title|MBWave_title|MBWave_loop>
76
77
MBWave_basic_title will change the filename written to the track title detected in the openMSX memory when using the MBWave basic driver. MBWave_title will do the same when using MBWave itself. MBWave_loop will insert a vgm pokey command when the tracks loops for the second time. This can be useful when it's hard to find the loop point using the vgmlpfnd tool from vgm_tools. When using vgm_cmp this otherwise useless command will be optimised away.
78
}}
79
                "disable_hacks"   {return {Disable all hacks.
80
81
Syntax: vgm_rec disable_hacks
82
}}
83
                default {return {Record a vgm file from audio playing in openMSX.
84
85
Syntax: vgm_rec <sub-command> [arguments if needed]
86
87
Where sub-command is one of:
88
start, stop, abort, next, auto_next, prefix, enable_hack or disable_hacks.
89
90
Use 'help vgm_rec <sub-command>' to get more help on specific sub-commands.
91
}}
92
        }
93
}
94
95
proc little_endian_32 {value} {
96
	binary format i $value
97
}
98
99
proc zeros {value} {
100
	string repeat "\0" $value
101
}
102
103
set_tabcompletion_proc vgm_rec [namespace code tab_vgmrec]
104
105
proc tab_vgmrec {args} {
106
	variable supported_chips
107
	if {[lsearch -exact $args "enable_hack"] >= 0} {
108
		concat MBWave_title MBWave_loop MBWave_basic_title
109
	} else {
110
		if {[lsearch -exact $args "start"] >= 0} {
111
			concat $supported_chips
112
		} else {
113
			concat start stop abort next auto_next prefix enable_hack disable_hacks
114
		}
115
	}
116
}
117
118
proc set_next_filename {} {
119
	variable original_filename
120
	variable directory
121
	variable file_name [utils::get_next_numbered_filename $directory $original_filename ".vgm"]
122
}
123
124
proc vgm_rec_set_filename {filename} {
125
	variable original_filename
126
127
	if {[file extension $filename] eq ".vgm"} {
128
		set filename [file rootname $filename]
129
	}
130
	set original_filename $filename
131
	set_next_filename
132
}
133
134
vgm_rec_set_filename "music"
135
136
proc vgm_rec {args} {
137
	variable active
138
	variable auto_next
139
140
	variable mbwave_title_hack
141
	variable mbwave_loop_hack
142
	variable mbwave_basic_title_hack
143
144
	variable psg_logged
145
	variable fm_logged
146
	variable y2151_logged
147
	variable y8950_logged
148
	variable moonsound_logged
149
	variable scc_logged
150
	variable opl3_logged
151
152
	set prefix_index [lsearch -exact $args "prefix"]
153
	if {$prefix_index >= 0} {
154
		if {$prefix_index == ([llength $args] - 1)} {
155
			error "Please specify prefix to use, see 'help vgm_rec prefix'."
156
		}
157
		vgm_rec_set_filename [lindex $args $prefix_index+1]
158
		return
159
	}
160
161
	set hack_index [lsearch -exact $args "enable_hack"]
162
	if {$hack_index >= 0} {
163
		if {$hack_index == ([llength $args] - 1)} {
164
			error "Please specify hack to activate, use tab completion."
165
		}
166
		foreach a [lrange $args $hack_index+1 end] {
167
			if     {[string compare -nocase $a "MBWave_title"      ] == 0} {set mbwave_title_hack       true} \
168
			elseif {[string compare -nocase $a "MBWave_loop"       ] == 0} {set mbwave_loop_hack        true} \
169
			elseif {[string compare -nocase $a "MBWave_basic_title"] == 0} {set mbwave_basic_title_hack true} \
170
			else {error "Hack '$a' not recognized, use tab completion."}
171
		}
172
		if {($mbwave_title_hack || $mbwave_loop_hack) && ($mbwave_basic_title_hack)} {
173
			vgm::vgm_disable_hacks
174
			error "Don't combine 'MBWave_basic_title' with other hacks... all hacks disabled to avoid problems."
175
		}
176
		return "Enabled hack(s): $args."
177
	}
178
179
	if {[lsearch -exact $args "disable_hacks"] >= 0} {
180
		vgm::vgm_disable_hacks
181
		return "Disabled all hacks."
182
	}
183
184
	set auto_next_index [lsearch -exact $args "auto_next"]
185
	if {$auto_next_index >= 0} {
186
		set param [lindex $args $auto_next_index+1] ;# empty if past end
187
		set paramBool [expr {($param eq "") ? true : bool($param)}]
188
		if {$active && $paramBool} {
189
			error "Auto_next can't be actived during recording, abort/stop the current recording and try again."
190
		}
191
		set auto_next $paramBool
192
		return "[expr {$auto_next ? "Enabled" : "Disabled"}] auto_next feature."
193
	}
194
195
	if {[lsearch -exact $args "abort"] >= 0} {
196
		return [vgm::vgm_rec_end true]
197
	}
198
199
	if {[lsearch -exact $args "stop"] >= 0} {
200
		return [vgm::vgm_rec_end false]
201
	}
202
203
	if {[lsearch -exact $args "next"] >= 0} {
204
		if {!$active} {
205
			error "Not recording now..."
206
		}
207
		return [vgm::vgm_rec_next]
208
	}
209
210
	set index [lsearch -exact $args "start"]
211
	if {$index >= 0} {
212
		if {$active} {
213
			error "Already recording, please stop it before running start again."
214
		}
215
		if {$index == ([llength $args] - 1)} {
216
			error "Please choose at least one chip to record for, use tab completion."
217
		}
218
		set psg_logged       false
219
		set fm_logged        false
220
		set y2151_logged     false
221
		set y8950_logged     false
222
		set moonsound_logged false
223
		set scc_logged       false
224
		set opl3_logged      false
225
226
		foreach a [lrange $args $index+1 end] {
227
			if     {[string compare -nocase $a "PSG"      ] == 0} {set psg_logged       true} \
228
			elseif {[string compare -nocase $a "MSX-Music"] == 0} {set fm_logged        true} \
229
			elseif {[string compare -nocase $a "SFG"      ] == 0} {set y2151_logged     true} \
230
			elseif {[string compare -nocase $a "MSX-Audio"] == 0} {set y8950_logged     true} \
231
			elseif {[string compare -nocase $a "MoonSound"] == 0} {set moonsound_logged true} \
232
			elseif {[string compare -nocase $a "SCC"      ] == 0} {set scc_logged       true} \
233
			elseif {[string compare -nocase $a "OPL3"     ] == 0} {set opl3_logged      true} \
234
			else {
235
				error "Invalid chip to record for specified, use tab completion"
236
				return
237
			}
238
		}
239
		return [vgm::vgm_rec_start]
240
	}
241
242
	error "Invalid input detected, use tab completion"
243
}
244
245
proc vgm_disable_hacks {} {
246
        variable mbwave_title_hack       false
247
        variable mbwave_loop_hack        false
248
        variable mbwave_basic_title_hack false
249
}
250
251
proc vgm_rec_start {} {
252
	variable active true
253
254
	variable auto_next
255
	if {$auto_next} {
256
		vgm::vgm_check_audio_data_written
257
	}
258
259
	variable mbwave_loop_hack
260
	if {$mbwave_loop_hack} {
261
		vgm::vgm_log_loop_point
262
	}
263
264
	set_next_filename
265
	variable directory
266
	file mkdir $directory
267
268
	variable psg_register       -1
269
	variable fm_register        -1
270
	variable y2151_register     -1
271
	variable y8950_register     -1
272
	variable opl4_register_wave -1
273
	variable opl4_register      -1
274
	variable opl3_register      -1
275
276
	variable ticks 0
277
	variable music_data ""
278
	variable temp_music_data ""
279
280
	variable scc_plus_used false
281
282
	variable watchpoints
283
284
	variable file_name
285
	set recording_text "VGM recording initiated, start playback now, data will be recorded to $file_name for the following sound chips:"
286
287
	variable psg_logged
288
	if {$psg_logged} {
289
		lappend watchpoints [debug set_watchpoint write_io 0xA0 {} {vgm::write_psg_address}] \
290
		                    [debug set_watchpoint write_io 0xA1 {} {vgm::write_psg_data}]
291
		append recording_text " PSG"
292
	}
293
294
	variable fm_logged
295
	if {$fm_logged} {
296
		lappend watchpoints [debug set_watchpoint write_io 0x7C {} {vgm::write_opll_address}] \
297
		                    [debug set_watchpoint write_io 0x7D {} {vgm::write_opll_data}]
298
		append recording_text " MSX-Music"
299
	}
300
301
	variable y2151_logged
302
	if {$y2151_logged} {
303
		foreach {ps ss} [find_all_sfg] {
304
			lappend watchpoints [debug set_watchpoint write_mem 0x3FF0 "\[watch_in_slot $ps $ss\]" {vgm::write_y2151_address}] \
305
			                    [debug set_watchpoint write_mem 0x3FF1 "\[watch_in_slot $ps $ss\]" {vgm::write_y2151_data}]
306
		}
307
		append recording_text " SFG"
308
	}
309
	variable y8950_logged
310
	if {$y8950_logged} {
311
		lappend watchpoints [debug set_watchpoint write_io 0xC0 {} {vgm::write_y8950_address}] \
312
		                    [debug set_watchpoint write_io 0xC1 {} {vgm::write_y8950_data}]
313
314
		# Save the sample RAM as a datablock. If loaded before starting the recording it's fine, if loaded afterward it'll be saved as vgm commands which will be optimised to datablock by the vgmtools
315
		# Note that we only support the first Y8950 device on the I/O port
316
		set y8950_ram [concat [lindex [machine_info output_port 0xC0] 0] RAM]
317
		if {[lsearch -exact [debug list] $y8950_ram] >= 0} {
318
			set y8950_ram_size [debug size $y8950_ram]
319
			if {$y8950_ram_size > 0} {
320
				append temp_music_data [binary format ccc 0x67 0x66 0x88] \
321
				                       [little_endian_32 [expr {$y8950_ram_size + 8}]] \
322
				                       [little_endian_32 $y8950_ram_size] \
323
				                       [zeros 4] \
324
				                       [debug read_block $y8950_ram 0 $y8950_ram_size]
325
			}
326
		}
327
		append recording_text " MSX-Audio"
328
	}
329
330
	# A thing: for wave to work some bits have to be set through FM2. So
331
	# that must be logged. This logs all, but just so you know...
332
	# Another thing: FM data can be used by FM bank 1 and FM bank 2. FM
333
	# data has a mirror however
334
	# So programs can use both ports in different ways; all to FM data,
335
	# FM1->FM-data,FM2->FM-data-mirror, etc. 4 options.
336
	# http://www.msxarchive.nl/pub/msx/docs/programming/opl4tech.txt
337
	variable moonsound_logged
338
	if {$moonsound_logged} {
339
		lappend watchpoints [debug set_watchpoint write_io 0x7E {} {vgm::write_opl4_address_wave}] \
340
		                    [debug set_watchpoint write_io 0x7F {} {vgm::write_opl4_data_wave}] \
341
		                    [debug set_watchpoint write_io 0xC4 {} {vgm::write_opl4_address_1}] \
342
		                    [debug set_watchpoint write_io 0xC5 {} {vgm::write_opl4_data}] \
343
		                    [debug set_watchpoint write_io 0xC6 {} {vgm::write_opl4_address_2}] \
344
		                    [debug set_watchpoint write_io 0xC7 {} {vgm::write_opl4_data}]
345
346
		# Save the sample RAM as a datablock. If loaded before starting the recording it's fine, if loaded afterward it'll be saved as vgm commands which will be optimised to datablock by the vgmtools
347
		# Note that we only support the first MoonSound device on the I/O port
348
		set moonsound_ram [concat [lindex [machine_info output_port 0x7E] 0] {wave RAM}]
349
		if {[lsearch -exact [debug list] $moonsound_ram] >= 0} {
350
			set moonsound_ram_size [debug size $moonsound_ram]
351
			if {$moonsound_ram_size > 0} {
352
				append temp_music_data [binary format ccc 0x67 0x66 0x87] \
353
				                       [little_endian_32 [expr {$moonsound_ram_size + 8}]] \
354
				                       [little_endian_32 $moonsound_ram_size] \
355
				                       [zeros 4] \
356
				                       [debug read_block $moonsound_ram 0 $moonsound_ram_size]
357
358
				# enable OPL4 mode so it's enabled even if recorded vgm data won't do that
359
				append temp_music_data [binary format cccc 0xD0 0x01 0x05 0x03]
360
			}
361
		}
362
		append recording_text " MoonSound"
363
	}
364
	variable opl3_logged
365
	if {$opl3_logged} {
366
		lappend watchpoints [debug set_watchpoint write_io 0xC0 {} {vgm::write_opl3_address_1}] \
367
		                    [debug set_watchpoint write_io 0xC1 {} {vgm::write_opl3_data}] \
368
		                    [debug set_watchpoint write_io 0xC2 {} {vgm::write_opl3_address_2}] \
369
		                    [debug set_watchpoint write_io 0xC3 {} {vgm::write_opl3_data}] \
370
		                    [debug set_watchpoint write_io 0xC4 {} {vgm::write_opl3_address_1}] \
371
		                    [debug set_watchpoint write_io 0xC5 {} {vgm::write_opl3_data}] \
372
		                    [debug set_watchpoint write_io 0xC6 {} {vgm::write_opl3_address_2}] \
373
		                    [debug set_watchpoint write_io 0xC7 {} {vgm::write_opl3_data}]
374
		append recording_text " OPL3"
375
	}
376
377
	variable scc_logged
378
	if {$scc_logged} {
379
		foreach {ps ss plus} [find_all_scc] {
380
			if {$plus} {
381
				lappend watchpoints [debug set_watchpoint write_mem {0xB800 0xB8AF} "\[watch_in_slot $ps $ss\]" {vgm::scc_plus_data}]
382
			} else {
383
				lappend watchpoints [debug set_watchpoint write_mem {0x9800 0x988F} "\[watch_in_slot $ps $ss\]" {vgm::scc_data}]
384
			}
385
		}
386
		append recording_text " SCC"
387
	}
388
389
	message $recording_text
390
	return $recording_text
391
}
392
393
proc find_all_scc {} {
394
	set result [list]
395
	for {set ps 0} {$ps < 4} {incr ps} {
396
		for {set ss 0} {$ss < 4} {incr ss} {
397
			set device_list [machine_info slot $ps $ss 2]
398
			if {[llength $device_list] != 0} {
399
				set device [lindex $device_list 0]
400
				set device_info_dict [machine_info device $device]
401
				set device_type [dict get $device_info_dict "type"]
402
				if {[string match -nocase *scc* $device_type]} {
403
					lappend result $ps $ss 1
404
				} elseif {[dict exists $device_info_dict "mappertype"]} {
405
					set mapper_type [dict get $device_info_dict "mappertype"]
406
					if {[string match -nocase *scc* $mapper_type] ||
407
					    [string match -nocase manbow2 $mapper_type] ||
408
					    [string match -nocase KonamiUltimateCollection $mapper_type]} {
409
						lappend result $ps $ss 0
410
					}
411
				}
412
			}
413
			if {![machine_info issubslotted $ps]} break
414
		}
415
	}
416
	return $result
417
}
418
419
proc find_all_sfg {} {
420
	set result [list]
421
	for {set ps 0} {$ps < 4} {incr ps} {
422
		for {set ss 0} {$ss < 4} {incr ss} {
423
			set device_list [machine_info slot $ps $ss 0]
424
			if {[llength $device_list] != 0} {
425
				set device [lindex $device_list 0]
426
				set device_info_dict [machine_info device $device]
427
				set device_type [dict get $device_info_dict "type"]
428
				# expected string is "YamahaSFG"
429
				if {[string match -nocase *sfg* $device_type]} {
430
					lappend result $ps $ss
431
				}
432
			}
433
			if {![machine_info issubslotted $ps]} break
434
		}
435
	}
436
	return $result
437
}
438
439
proc write_psg_address {} {
440
	variable psg_register $::wp_last_value
441
}
442
443
proc write_psg_data {} {
444
	variable psg_register
445
	if {$psg_register >= 0 && $psg_register < 14} {
446
		update_time
447
		variable music_data
448
		append music_data [binary format ccc 0xA0 $psg_register $::wp_last_value]
449
	}
450
}
451
452
proc write_opll_address {} {
453
	variable opll_register $::wp_last_value
454
}
455
456
proc write_opll_data {} {
457
	variable opll_register
458
	if {$opll_register >= 0} {
459
		update_time
460
		variable music_data
461
		append music_data [binary format ccc 0x51 $opll_register $::wp_last_value]
462
	}
463
}
464
465
proc write_y2151_address {} {
466
	variable y2151_register $::wp_last_value
467
}
468
proc write_y2151_data {} {
469
	variable y2151_register
470
	if {$y2151_register >= 0} { # initialised to -1
471
		update_time
472
		variable music_data
473
		append music_data [binary format ccc 0x54 $y2151_register $::wp_last_value]
474
	}
475
}
476
477
proc write_y8950_address {} {
478
	variable y8950_register $::wp_last_value
479
}
480
481
proc write_y8950_data {} {
482
	variable y8950_register
483
	if {$y8950_register >= 0} {
484
		update_time
485
		variable music_data
486
		append music_data [binary format ccc 0x5C $y8950_register $::wp_last_value]
487
	}
488
}
489
490
proc write_opl4_address_wave {} {
491
	variable opl4_register_wave $::wp_last_value
492
}
493
494
proc write_opl4_data_wave {} {
495
	variable opl4_register_wave
496
	if {$opl4_register_wave >= 0} {
497
		update_time
498
		# VGM spec: Port 0 = FM1, port 1 = FM2, port 2 = Wave. It's
499
		# based on the datasheet A1 & A2 use.
500
		variable music_data
501
		append music_data [binary format cccc 0xD0 0x2 $opl4_register_wave $::wp_last_value]
502
	}
503
}
504
505
proc write_opl4_address_1 {} {
506
	variable opl4_register $::wp_last_value
507
	variable active_fm_register 0
508
}
509
510
proc write_opl4_address_2 {} {
511
	variable opl4_register $::wp_last_value
512
	variable active_fm_register 1
513
}
514
515
proc write_opl4_data {} {
516
	variable opl4_register
517
	variable active_fm_register
518
	if {$opl4_register >= 0} {
519
		update_time
520
		variable music_data
521
		append music_data [binary format cccc 0xD0 $active_fm_register $opl4_register $::wp_last_value]
522
	}
523
}
524
525
proc write_opl3_address_1 {} {
526
	variable opl3_register $::wp_last_value
527
	variable opl3_port 0xC0
528
}
529
530
proc write_opl3_address_2 {} {
531
	variable opl3_register $::wp_last_value
532
	variable opl3_port 0xC2
533
}
534
535
proc write_opl3_data {} {
536
	variable opl3_register
537
	if {$opl3_register >= 0} {
538
		update_time
539
		variable opl3_port
540
		variable music_data
541
		switch $opl3_port {
542
			0xC0 { append music_data [binary format ccc 0x5E $opl3_register $::wp_last_value] }
543
			0xC2 { append music_data [binary format ccc 0x5F $opl3_register $::wp_last_value] }
544
		}
545
	}
546
}
547
548
proc scc_data {} {
549
	# Thanks ValleyBell, BiFi
550
551
	# if 9800h is written, waveform channel 1   is set in 9800h - 981fh, 32 bytes
552
	# if 9820h is written, waveform channel 2   is set in 9820h - 983fh, 32 bytes
553
	# if 9840h is written, waveform channel 3   is set in 9840h - 985fh, 32 bytes
554
	# if 9860h is written, waveform channel 4,5 is set in 9860h - 987fh, 32 bytes
555
	# if 9880h is written, frequency channel 1 is set in 9880h - 9881h, 12 bits
556
	# if 9882h is written, frequency channel 2 is set in 9882h - 9883h, 12 bits
557
	# if 9884h is written, frequency channel 3 is set in 9884h - 9885h, 12 bits
558
	# if 9886h is written, frequency channel 4 is set in 9886h - 9887h, 12 bits
559
	# if 9888h is written, frequency channel 5 is set in 9888h - 9889h, 12 bits
560
	# if 988ah is written, volume channel 1 is set, 4 bits
561
	# if 988bh is written, volume channel 2 is set, 4 bits
562
	# if 988ch is written, volume channel 3 is set, 4 bits
563
	# if 988dh is written, volume channel 4 is set, 4 bits
564
	# if 988eh is written, volume channel 5 is set, 4 bits
565
	# if 988fh is written, channels 1-5 on/off, 1 bit
566
567
	#VGM port format:
568
	#0x00 - waveform
569
	#0x01 - frequency
570
	#0x02 - volume
571
	#0x03 - key on/off
572
	#0x04 - waveform (0x00 used to do SCC access, 0x04 SCC+)
573
	#0x05 - test register
574
575
	update_time
576
577
	variable music_data
578
	if       {0x9800 <= $::wp_last_address && $::wp_last_address < 0x9880} {
579
		append music_data [binary format cccc 0xD2 0x0 [expr {$::wp_last_address - 0x9800}] $::wp_last_value]
580
	} elseif {0x9880 <= $::wp_last_address && $::wp_last_address < 0x988A} {
581
		append music_data [binary format cccc 0xD2 0x1 [expr {$::wp_last_address - 0x9880}] $::wp_last_value]
582
	} elseif {0x988A <= $::wp_last_address && $::wp_last_address < 0x988F} {
583
		append music_data [binary format cccc 0xD2 0x2 [expr {$::wp_last_address - 0x988A}] $::wp_last_value]
584
	} elseif {$::wp_last_address == 0x988F} {
585
		append music_data [binary format cccc 0xD2 0x3 0x0 $::wp_last_value]
586
	}
587
}
588
589
proc scc_plus_data {} {
590
	# if b800h is written, waveform channel 1 is set in b800h - b81fh, 32 bytes
591
	# if b820h is written, waveform channel 2 is set in b820h - b83fh, 32 bytes
592
	# if b840h is written, waveform channel 3 is set in b840h - b85fh, 32 bytes
593
	# if b860h is written, waveform channel 4 is set in b860h - b87fh, 32 bytes
594
	# if b880h is written, waveform channel 5 is set in b880h - b89fh, 32 bytes
595
	# if b8a0h is written, frequency channel 1 is set in b8a0h - b8a1h, 12 bits
596
	# if b8a2h is written, frequency channel 2 is set in b8a2h - b8a3h, 12 bits
597
	# if b8a4h is written, frequency channel 3 is set in b8a4h - b8a5h, 12 bits
598
	# if b8a6h is written, frequency channel 4 is set in b8a6h - b8a7h, 12 bits
599
	# if b8a8h is written, frequency channel 5 is set in b8a8h - b8a9h, 12 bits
600
	# if b8aah is written, volume channel 1 is set, 4 bits
601
	# if b8abh is written, volume channel 2 is set, 4 bits
602
	# if b8ach is written, volume channel 3 is set, 4 bits
603
	# if b8adh is written, volume channel 4 is set, 4 bits
604
	# if b8aeh is written, volume channel 5 is set, 4 bits
605
	# if b8afh is written, channels 1-5 on/off, 1 bit
606
607
	#VGM port format:
608
	#0x00 - waveform
609
	#0x01 - frequency
610
	#0x02 - volume
611
	#0x03 - key on/off
612
	#0x04 - waveform (0x00 used to do SCC access, 0x04 SCC+)
613
	#0x05 - test register
614
615
	update_time
616
617
	variable music_data
618
	if       {0xB800 <= $::wp_last_address && $::wp_last_address < 0xB8A0} {
619
		append music_data [binary format cccc 0xD2 0x4 [expr {$::wp_last_address - 0xB800}] $::wp_last_value]
620
	} elseif {0xB8A0 <= $::wp_last_address && $::wp_last_address < 0xb8aa} {
621
		append music_data [binary format cccc 0xD2 0x1 [expr {$::wp_last_address - 0xB8A0}] $::wp_last_value]
622
	} elseif {0xB8AA <= $::wp_last_address && $::wp_last_address < 0xB8AF} {
623
		append music_data [binary format cccc 0xD2 0x2 [expr {$::wp_last_address - 0xB8AA}] $::wp_last_value]
624
	} elseif {$::wp_last_address == 0xB8AF} {
625
		append music_data [binary format cccc 0xD2 0x3 0x0 $::wp_last_value]
626
	}
627
628
	variable scc_plus_used true
629
}
630
631
proc update_time {} {
632
	variable start_time
633
	if {$start_time == 0} {
634
		set start_time [machine_info time]
635
		message "VGM recording started, data was written to one of the sound chips recording for."
636
	}
637
638
	variable tick_time
639
	set tick_time [machine_info time]
640
	set new_ticks [expr {int(($tick_time - $start_time) * 44100)}]
641
642
	variable ticks
643
	if {$new_ticks < $ticks} {
644
		puts "VGM warning: Tick backstep ($new_ticks -> $ticks)"
645
			set ticks [expr {$new_ticks - 65535}]
646
	}
647
648
	variable music_data
649
	while {$new_ticks > $ticks} {
650
		set difference [expr {$new_ticks - $ticks}]
651
		set step [expr {$difference > 65535 ? 65535 : $difference}]
652
		incr ticks $step
653
		append music_data [binary format cs 0x61 $step]
654
	}
655
}
656
657
proc vgm_rec_end {abort} {
658
	variable active
659
	if {!$active} {
660
		error "Not recording currently..."
661
	}
662
663
	# remove all watchpoints that were created
664
	variable watchpoints
665
	foreach watch $watchpoints {
666
		if {[catch {
667
			debug remove_watchpoint $watch
668
		} errorText]} {
669
			puts "Failed to remove watchpoint $watch... using savestates maybe? Continue anyway."
670
		}
671
	}
672
	set watchpoints [list]
673
674
	if {!$abort} {
675
		update_time
676
677
		variable tick_time 0
678
		variable music_data
679
		variable temp_music_data 
680
681
		append temp_music_data $music_data [binary format c 0x66]
682
683
		set header "Vgm "
684
		# file size
685
		append header [little_endian_32 [expr {[string length $temp_music_data] + 0x100 - 4}]]
686
		# VGM version 1.7
687
		append header [little_endian_32 0x161] [zeros 4]
688
689
		# YM2413 clock
690
		variable fm_logged
691
		if {$fm_logged} {
692
			append header [little_endian_32 3579545]
693
		} else {
694
			append header [zeros 4]
695
		}
696
697
		append header [zeros 4]
698
		# Number of ticks
699
		variable ticks
700
		append header [little_endian_32 $ticks]
701
		set ticks 0
702
		append header [zeros 20]
703
704
		variable y2151_logged
705
		if {$y2151_logged} {
706
			append header [little_endian_32 3579545]
707
		} else {
708
			append header [zeros 4]
709
		}
710
711
		# Data starts at offset 0x100
712
		append header [little_endian_32 [expr {0x100 - 0x34}]] [zeros 32]
713
714
		# Y8950 clock
715
		variable y8950_logged
716
		if {$y8950_logged} {
717
			append header [little_endian_32 3579545]
718
		} else {
719
			append header [zeros 4]
720
		}
721
722
		# YMF262 clock
723
		variable opl3_logged
724
		if {$opl3_logged} {
725
			append header [little_endian_32 14318182]
726
		} else {
727
			append header [zeros 4]
728
		}
729
730
		# YMF278B clock
731
		variable moonsound_logged
732
		if {$moonsound_logged} {
733
			append header [little_endian_32 33868800]
734
		} else {
735
			append header [zeros 4]
736
		}
737
738
		append header [zeros 16]
739
740
		# AY8910 clock
741
		variable psg_logged
742
		if {$psg_logged} {
743
			append header [little_endian_32 1789773]
744
		} else {
745
			append header [zeros 4]
746
		}
747
748
		append header [zeros 36]
749
750
		# SCC clock
751
		variable scc_logged
752
		if {$scc_logged} {
753
			set scc_clock 1789773
754
			variable scc_plus_used
755
			if {$scc_plus_used} {
756
				# enable bit 31 for SCC+ support, that's how it's done
757
				# in VGM I've been told. Thanks Grauw.
758
				set scc_clock [expr {$scc_clock | 1 << 31}]
759
			}
760
			append header [little_endian_32 $scc_clock]
761
		} else {
762
			append header [zeros 4]
763
		}
764
765
		append header [zeros 96]
766
767
		variable file_name
768
		variable directory
769
770
		# Title hacks
771
		variable mbwave_title_hack
772
		variable mbwave_basic_title_hack
773
		if {$mbwave_title_hack || $mbwave_basic_title_hack} {
774
			set title_address [expr {$mbwave_title_hack ? 0xffc6 : 0xc0dc}]
775
			set file_name [string map {/ -} [debug read_block "Main RAM" $title_address 0x32]]
776
			set file_name [string trim $file_name]
777
			set file_name [format %s%s%s%s $directory "/" $file_name ".vgm"]
778
		}
779
780
		set file_handle [open $file_name "w"]
781
		fconfigure $file_handle -encoding binary -translation binary
782
		puts -nonewline $file_handle $header
783
		puts -nonewline $file_handle $temp_music_data
784
		close $file_handle
785
786
		set stop_message "VGM recording stopped, wrote data to $file_name."
787
	} else {
788
		set stop_message "VGM recording aborted, no data written..."
789
	}
790
791
	set active false
792
	variable start_time 0
793
	variable loop_amount 0
794
795
	message $stop_message
796
	return $stop_message
797
}
798
799
proc vgm_rec_next {} {
800
	variable active
801
	if {!$active} {
802
		variable original_filename "music"
803
	} else {
804
		vgm_rec_end false
805
	}
806
	vgm_rec_start
807
}
808
809
# Generic function to check if audio data is still written when recording is active. If not for a second, assume end recording, and start recording next if this is wanted
810
proc vgm_check_audio_data_written {} {
811
	variable active
812
	if {!$active} return
813
814
	variable tick_time
815
	variable auto_next
816
	set now [machine_info time]
817
	if {$tick_time == 0 || $now - $tick_time < 1} {
818
		after time 1 vgm::vgm_check_audio_data_written
819
	} else {
820
		vgm::vgm_rec_end false
821
		if {$auto_next} {
822
			message "auto_next feature active, starting next recording"
823
			vgm::vgm_rec_start
824
		}
825
	}
826
}
827
828
# Loop point logger for MBWave only
829
proc vgm_log_loop_point {} {
830
	variable watchpoints
831
	lappend watchpoints [debug set_watchpoint write_mem 0x51f7 {} {vgm::vgm_check_loop_point}]
832
}
833
834
proc vgm_check_loop_point {} {
835
        variable start_time
836
        if {$start_time == 0} return
837
838
	variable position
839
	set position_new [expr {$::wp_last_value == 255 ? 0 : $::wp_last_value}]
840
	if {$position_new < $position} {
841
		after time 1 vgm::vgm_log_loop_in_music_data
842
	}
843
	set position $position_new
844
}
845
846
proc vgm_log_loop_in_music_data {} {
847
	variable start_time
848
        if {$start_time == 0} return
849
850
	variable loop_amount
851
	incr loop_amount
852
	variable music_data
853
	append music_data [binary format ccc 0xbb 0xbb 0xbb]
854
	if {$loop_amount == 1} {
855
		message "First loop: Track-length in seconds (if not using transposing..): [expr {[machine_info time] - $start_time}]. Marker inserted in VGM file."
856
	}
857
	if {$loop_amount == 2} {
858
		message "Second loop. Marker inserted in VGM file."
859
	}
860
	if {$loop_amount > 2} {
861
		keymatrixdown 7 4
862
		message "Third loop occurred, stop playback."
863
		variable position 0
864
		set loop_amount 0
865
		after time 1 {keymatrixup 7 4}
866
	}
867
}
868
869
namespace export vgm_rec
870
871
}
872
873
namespace import vgm::*