_vgmrecorder.tcl 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771
  1. namespace eval vgm {
  2. variable active false
  3. variable psg_register
  4. variable opll_register
  5. variable y8950_register
  6. variable opl4_register_wave
  7. variable opl4_register
  8. variable active_fm_register -1
  9. variable start_time 0
  10. variable tick_time 0
  11. variable ticks
  12. variable music_data
  13. variable file_name
  14. variable original_filename
  15. variable directory [file normalize $::env(OPENMSX_USER_DATA)/../vgm_recordings]
  16. variable psg_logged false
  17. variable fm_logged false
  18. variable y8950_logged false
  19. variable moonsound_logged false
  20. variable scc_logged false
  21. variable scc_plus_used
  22. variable watchpoints [list]
  23. variable loop_amount 0
  24. variable position 0
  25. variable auto_next false
  26. variable mbwave_title_hack false
  27. variable mbwave_loop_hack false
  28. variable mbwave_basic_title_hack false
  29. variable supported_chips [list MSX-Music PSG Moonsound MSX-Audio SCC]
  30. set_help_proc vgm_rec [namespace code vgm_rec_help]
  31. proc vgm_rec_help {args} {
  32. switch -- [lindex $args 1] {
  33. "start" {return {VGM recording will be initialised, specify one or more soundchips to record.
  34. Syntax: vgm_rec start <MSX-Audio|MSX-Music|Moonsound|PSG|SCC>
  35. 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.
  36. }}
  37. "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...
  38. Syntax: vgm_rec stop
  39. }}
  40. "abort" {return {Abort an active recording without saving the data.
  41. Syntax: vgm_rec abort
  42. }}
  43. "next" {return {Stops current recording and starts recording the next track, remembering the chips to record for.
  44. Syntax: vgm_rec next
  45. }}
  46. "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.
  47. Syntax: vgm_rec auto_next
  48. }}
  49. "prefix" {return {Specify the prefix of the VGM files, instead of the default music.
  50. Syntax: vgm_rec prefix
  51. }}
  52. "enable_hack" {return {Enables one of three hacks; MBWave_basic_title, MBWave_title or MBWave_loop.
  53. Syntax: vgm_rec enable_hack <MBWave_basic_title|MBWave_title|MBWave_loop>
  54. 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.
  55. }}
  56. "disable_hacks" {return {Disable all hacks.
  57. Syntax: vgm_rec disable_hacks
  58. }}
  59. default {return {Record a vgm file from audio playing in openMSX.
  60. Syntax: vgm_rec <sub-command> [arguments if needed]
  61. Where sub-command is one of:
  62. start, stop, abort, next, auto_next, prefix, enable_hack or disable_hacks.
  63. Use 'help vgm_rec <sub-command>' to get more help on specific sub-commands.
  64. }}
  65. }
  66. }
  67. proc little_endian_32 {value} {
  68. binary format i $value
  69. }
  70. proc zeros {value} {
  71. string repeat "\0" $value
  72. }
  73. set_tabcompletion_proc vgm_rec [namespace code tab_vgmrec]
  74. proc tab_vgmrec {args} {
  75. variable supported_chips
  76. if {[lsearch -exact $args "enable_hack"] >= 0} {
  77. concat MBWave_title MBWave_loop MBWave_basic_title
  78. } else {
  79. if {[lsearch -exact $args "start"] >= 0} {
  80. concat $supported_chips
  81. } else {
  82. concat start stop abort next auto_next prefix enable_hack disable_hacks
  83. }
  84. }
  85. }
  86. proc set_next_filename {} {
  87. variable original_filename
  88. variable directory
  89. variable file_name [utils::get_next_numbered_filename $directory $original_filename ".vgm"]
  90. }
  91. proc vgm_rec_set_filename {filename} {
  92. variable original_filename
  93. if {[file extension $filename] eq ".vgm"} {
  94. set filename [file rootname $filename]
  95. }
  96. set original_filename $filename
  97. set_next_filename
  98. }
  99. vgm_rec_set_filename "music"
  100. proc vgm_rec {args} {
  101. variable active
  102. variable auto_next
  103. variable mbwave_title_hack
  104. variable mbwave_loop_hack
  105. variable mbwave_basic_title_hack
  106. variable psg_logged
  107. variable fm_logged
  108. variable y8950_logged
  109. variable moonsound_logged
  110. variable scc_logged
  111. set prefix_index [lsearch -exact $args "prefix"]
  112. if {$prefix_index >= 0} {
  113. if {$prefix_index == ([llength $args] - 1)} {
  114. error "Please specify prefix to use, see 'help vgm_rec prefix'."
  115. }
  116. vgm_rec_set_filename [lindex $args $prefix_index+1]
  117. return
  118. }
  119. set hack_index [lsearch -exact $args "enable_hack"]
  120. if {$hack_index >= 0} {
  121. if {$hack_index == ([llength $args] - 1)} {
  122. error "Please specify hack to activate, use tab completion."
  123. }
  124. foreach a [lrange $args $hack_index+1 end] {
  125. if {[string compare -nocase $a "MBWave_title" ] == 0} {set mbwave_title_hack true} \
  126. elseif {[string compare -nocase $a "MBWave_loop" ] == 0} {set mbwave_loop_hack true} \
  127. elseif {[string compare -nocase $a "MBWave_basic_title"] == 0} {set mbwave_basic_title_hack true} \
  128. else {error "Hack '$a' not recognized, use tab completion."}
  129. }
  130. if {($mbwave_title_hack || $mbwave_loop_hack) && ($mbwave_basic_title_hack)} {
  131. vgm::vgm_disable_hacks
  132. error "Don't combine 'MBWave_basic_title' with other hacks... all hacks disabled to avoid problems."
  133. }
  134. return "Enabled hack(s): $args."
  135. }
  136. if {[lsearch -exact $args "disable_hacks"] >= 0} {
  137. vgm::vgm_disable_hacks
  138. return "Disabled all hacks."
  139. }
  140. set auto_next_index [lsearch -exact $args "auto_next"]
  141. if {$auto_next_index >= 0} {
  142. set param [lindex $args $auto_next_index+1] ;# empty if past end
  143. set paramBool [expr {($param eq "") ? true : bool($param)}]
  144. if {$active && $paramBool} {
  145. error "Auto_next can't be actived during recording, abort/stop the current recording and try again."
  146. }
  147. set auto_next $paramBool
  148. return "[expr {$auto_next ? "Enabled" : "Disabled"}] auto_next feature."
  149. }
  150. if {[lsearch -exact $args "abort"] >= 0} {
  151. return [vgm::vgm_rec_end true]
  152. }
  153. if {[lsearch -exact $args "stop"] >= 0} {
  154. return [vgm::vgm_rec_end false]
  155. }
  156. if {[lsearch -exact $args "next"] >= 0} {
  157. if {!$active} {
  158. error "Not recording now..."
  159. }
  160. return [vgm::vgm_rec_next]
  161. }
  162. set index [lsearch -exact $args "start"]
  163. if {$index >= 0} {
  164. if {$active} {
  165. error "Already recording, please stop it before running start again."
  166. }
  167. if {$index == ([llength $args] - 1)} {
  168. error "Please choose at least one chip to record for, use tab completion."
  169. }
  170. set psg_logged false
  171. set fm_logged false
  172. set y8950_logged false
  173. set moonsound_logged false
  174. set scc_logged false
  175. foreach a [lrange $args $index+1 end] {
  176. if {[string compare -nocase $a "PSG" ] == 0} {set psg_logged true} \
  177. elseif {[string compare -nocase $a "MSX-Music"] == 0} {set fm_logged true} \
  178. elseif {[string compare -nocase $a "MSX-Audio"] == 0} {set y8950_logged true} \
  179. elseif {[string compare -nocase $a "Moonsound"] == 0} {set moonsound_logged true} \
  180. elseif {[string compare -nocase $a "SCC" ] == 0} {set scc_logged true} \
  181. else {
  182. error "Invalid chip to record for specified, use tab completion"
  183. return
  184. }
  185. }
  186. return [vgm::vgm_rec_start]
  187. }
  188. error "Invalid input detected, use tab completion"
  189. }
  190. proc vgm_disable_hacks {} {
  191. variable mbwave_title_hack false
  192. variable mbwave_loop_hack false
  193. variable mbwave_basic_title_hack false
  194. }
  195. proc vgm_rec_start {} {
  196. variable active true
  197. variable auto_next
  198. if {$auto_next} {
  199. vgm::vgm_check_audio_data_written
  200. }
  201. variable mbwave_loop_hack
  202. if {$mbwave_loop_hack} {
  203. vgm::vgm_log_loop_point
  204. }
  205. set_next_filename
  206. variable directory
  207. file mkdir $directory
  208. variable psg_register -1
  209. variable fm_register -1
  210. variable y8950_register -1
  211. variable opl4_register_wave -1
  212. variable opl4_register -1
  213. variable ticks 0
  214. variable music_data ""
  215. variable temp_music_data ""
  216. variable scc_plus_used false
  217. variable watchpoints
  218. variable file_name
  219. set recording_text "VGM recording initiated, start playback now, data will be recorded to $file_name for the following sound chips:"
  220. variable psg_logged
  221. if {$psg_logged} {
  222. lappend watchpoints [debug set_watchpoint write_io 0xA0 {} {vgm::write_psg_address}] \
  223. [debug set_watchpoint write_io 0xA1 {} {vgm::write_psg_data}]
  224. append recording_text " PSG"
  225. }
  226. variable fm_logged
  227. if {$fm_logged} {
  228. lappend watchpoints [debug set_watchpoint write_io 0x7C {} {vgm::write_opll_address}] \
  229. [debug set_watchpoint write_io 0x7D {} {vgm::write_opll_data}]
  230. append recording_text " MSX-Music"
  231. }
  232. variable y8950_logged
  233. if {$y8950_logged} {
  234. lappend watchpoints [debug set_watchpoint write_io 0xC0 {} {vgm::write_y8950_address}] \
  235. [debug set_watchpoint write_io 0xC1 {} {vgm::write_y8950_data}]
  236. # 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
  237. set y8950_ram [concat [machine_info output_port 0xC0] RAM]
  238. if {[lsearch -exact [debug list] $y8950_ram] >= 0} {
  239. set y8950_ram_size [debug size $y8950_ram]
  240. if {$y8950_ram_size > 0} {
  241. append temp_music_data [binary format ccc 0x67 0x66 0x88] \
  242. [little_endian_32 [expr {$y8950_ram_size + 8}]] \
  243. [little_endian_32 $y8950_ram_size] \
  244. [zeros 4] \
  245. [debug read_block $y8950_ram 0 $y8950_ram_size]
  246. }
  247. }
  248. append recording_text " MSX-Audio"
  249. }
  250. # A thing: for wave to work some bits have to be set through FM2. So
  251. # that must be logged. This logs all, but just so you know...
  252. # Another thing: FM data can be used by FM bank 1 and FM bank 2. FM
  253. # data has a mirror however
  254. # So programs can use both ports in different ways; all to FM data,
  255. # FM1->FM-data,FM2->FM-data-mirror, etc. 4 options.
  256. # http://www.msxarchive.nl/pub/msx/docs/programming/opl4tech.txt
  257. variable moonsound_logged
  258. if {$moonsound_logged} {
  259. lappend watchpoints [debug set_watchpoint write_io 0x7E {} {vgm::write_opl4_address_wave}] \
  260. [debug set_watchpoint write_io 0x7F {} {vgm::write_opl4_data_wave}] \
  261. [debug set_watchpoint write_io 0xC4 {} {vgm::write_opl4_address_1}] \
  262. [debug set_watchpoint write_io 0xC5 {} {vgm::write_opl4_data}] \
  263. [debug set_watchpoint write_io 0xC6 {} {vgm::write_opl4_address_2}] \
  264. [debug set_watchpoint write_io 0xC7 {} {vgm::write_opl4_data}]
  265. # 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
  266. set moonsound_ram [concat [machine_info output_port 0x7E] {wave RAM}]
  267. if {[lsearch -exact [debug list] $moonsound_ram] >= 0} {
  268. set moonsound_ram_size [debug size $moonsound_ram]
  269. if {$moonsound_ram_size > 0} {
  270. append temp_music_data [binary format ccc 0x67 0x66 0x87] \
  271. [little_endian_32 [expr {$moonsound_ram_size + 8}]] \
  272. [little_endian_32 $moonsound_ram_size] \
  273. [zeros 4] \
  274. [debug read_block $moonsound_ram 0 $moonsound_ram_size]
  275. # enable OPL4 mode so it's enabled even if recorded vgm data won't do that
  276. append temp_music_data [binary format cccc 0xD0 0x01 0x05 0x03]
  277. }
  278. }
  279. append recording_text " Moondsound"
  280. }
  281. variable scc_logged
  282. if {$scc_logged} {
  283. foreach {ps ss plus} [find_all_scc] {
  284. if {$plus} {
  285. lappend watchpoints [debug set_watchpoint write_mem {0xB800 0xB8AF} "\[watch_in_slot $ps $ss\]" {vgm::scc_plus_data}]
  286. } else {
  287. lappend watchpoints [debug set_watchpoint write_mem {0x9800 0x988F} "\[watch_in_slot $ps $ss\]" {vgm::scc_data}]
  288. }
  289. }
  290. append recording_text " SCC"
  291. }
  292. message $recording_text
  293. return $recording_text
  294. }
  295. proc find_all_scc {} {
  296. set result [list]
  297. for {set ps 0} {$ps < 4} {incr ps} {
  298. for {set ss 0} {$ss < 4} {incr ss} {
  299. set device_list [machine_info slot $ps $ss 2]
  300. if {[llength $device_list] != 0} {
  301. set device [lindex $device_list 0]
  302. set device_info_dict [machine_info device $device]
  303. set device_type [dict get $device_info_dict "type"]
  304. if {[string match -nocase *scc* $device_type]} {
  305. lappend result $ps $ss 1
  306. } elseif {[dict exists $device_info_dict "mappertype"]} {
  307. set mapper_type [dict get $device_info_dict "mappertype"]
  308. if {[string match -nocase *scc* $mapper_type] ||
  309. [string match -nocase manbow2 $mapper_type] ||
  310. [string match -nocase KonamiUltimateCollection $mapper_type]} {
  311. lappend result $ps $ss 0
  312. }
  313. }
  314. }
  315. if {![machine_info issubslotted $ps]} break
  316. }
  317. }
  318. return $result
  319. }
  320. proc write_psg_address {} {
  321. variable psg_register $::wp_last_value
  322. }
  323. proc write_psg_data {} {
  324. variable psg_register
  325. if {$psg_register >= 0 && $psg_register < 14} {
  326. update_time
  327. variable music_data
  328. append music_data [binary format ccc 0xA0 $psg_register $::wp_last_value]
  329. }
  330. }
  331. proc write_opll_address {} {
  332. variable opll_register $::wp_last_value
  333. }
  334. proc write_opll_data {} {
  335. variable opll_register
  336. if {$opll_register >= 0} {
  337. update_time
  338. variable music_data
  339. append music_data [binary format ccc 0x51 $opll_register $::wp_last_value]
  340. }
  341. }
  342. proc write_y8950_address {} {
  343. variable y8950_register $::wp_last_value
  344. }
  345. proc write_y8950_data {} {
  346. variable y8950_register
  347. if {$y8950_register >= 0} {
  348. update_time
  349. variable music_data
  350. append music_data [binary format ccc 0x5C $y8950_register $::wp_last_value]
  351. }
  352. }
  353. proc write_opl4_address_wave {} {
  354. variable opl4_register_wave $::wp_last_value
  355. }
  356. proc write_opl4_data_wave {} {
  357. variable opl4_register_wave
  358. if {$opl4_register_wave >= 0} {
  359. update_time
  360. # VGM spec: Port 0 = FM1, port 1 = FM2, port 2 = Wave. It's
  361. # based on the datasheet A1 & A2 use.
  362. variable music_data
  363. append music_data [binary format cccc 0xD0 0x2 $opl4_register_wave $::wp_last_value]
  364. }
  365. }
  366. proc write_opl4_address_1 {} {
  367. variable opl4_register $::wp_last_value
  368. variable active_fm_register 0
  369. }
  370. proc write_opl4_address_2 {} {
  371. variable opl4_register $::wp_last_value
  372. variable active_fm_register 1
  373. }
  374. proc write_opl4_data {} {
  375. variable opl4_register
  376. variable active_fm_register
  377. if {$opl4_register >= 0} {
  378. update_time
  379. variable music_data
  380. append music_data [binary format cccc 0xD0 $active_fm_register $opl4_register $::wp_last_value]
  381. }
  382. }
  383. proc scc_data {} {
  384. # Thanks ValleyBell, BiFi
  385. # if 9800h is written, waveform channel 1 is set in 9800h - 981fh, 32 bytes
  386. # if 9820h is written, waveform channel 2 is set in 9820h - 983fh, 32 bytes
  387. # if 9840h is written, waveform channel 3 is set in 9840h - 985fh, 32 bytes
  388. # if 9860h is written, waveform channel 4,5 is set in 9860h - 987fh, 32 bytes
  389. # if 9880h is written, frequency channel 1 is set in 9880h - 9881h, 12 bits
  390. # if 9882h is written, frequency channel 2 is set in 9882h - 9883h, 12 bits
  391. # if 9884h is written, frequency channel 3 is set in 9884h - 9885h, 12 bits
  392. # if 9886h is written, frequency channel 4 is set in 9886h - 9887h, 12 bits
  393. # if 9888h is written, frequency channel 5 is set in 9888h - 9889h, 12 bits
  394. # if 988ah is written, volume channel 1 is set, 4 bits
  395. # if 988bh is written, volume channel 2 is set, 4 bits
  396. # if 988ch is written, volume channel 3 is set, 4 bits
  397. # if 988dh is written, volume channel 4 is set, 4 bits
  398. # if 988eh is written, volume channel 5 is set, 4 bits
  399. # if 988fh is written, channels 1-5 on/off, 1 bit
  400. #VGM port format:
  401. #0x00 - waveform
  402. #0x01 - frequency
  403. #0x02 - volume
  404. #0x03 - key on/off
  405. #0x04 - waveform (0x00 used to do SCC access, 0x04 SCC+)
  406. #0x05 - test register
  407. update_time
  408. variable music_data
  409. if {0x9800 <= $::wp_last_address && $::wp_last_address < 0x9880} {
  410. append music_data [binary format cccc 0xD2 0x0 [expr {$::wp_last_address - 0x9800}] $::wp_last_value]
  411. } elseif {0x9880 <= $::wp_last_address && $::wp_last_address < 0x988A} {
  412. append music_data [binary format cccc 0xD2 0x1 [expr {$::wp_last_address - 0x9880}] $::wp_last_value]
  413. } elseif {0x988A <= $::wp_last_address && $::wp_last_address < 0x988F} {
  414. append music_data [binary format cccc 0xD2 0x2 [expr {$::wp_last_address - 0x988A}] $::wp_last_value]
  415. } elseif {$::wp_last_address == 0x988F} {
  416. append music_data [binary format cccc 0xD2 0x3 0x0 $::wp_last_value]
  417. }
  418. }
  419. proc scc_plus_data {} {
  420. # if b800h is written, waveform channel 1 is set in b800h - b81fh, 32 bytes
  421. # if b820h is written, waveform channel 2 is set in b820h - b83fh, 32 bytes
  422. # if b840h is written, waveform channel 3 is set in b840h - b85fh, 32 bytes
  423. # if b860h is written, waveform channel 4 is set in b860h - b87fh, 32 bytes
  424. # if b880h is written, waveform channel 5 is set in b880h - b89fh, 32 bytes
  425. # if b8a0h is written, frequency channel 1 is set in b8a0h - b8a1h, 12 bits
  426. # if b8a2h is written, frequency channel 2 is set in b8a2h - b8a3h, 12 bits
  427. # if b8a4h is written, frequency channel 3 is set in b8a4h - b8a5h, 12 bits
  428. # if b8a6h is written, frequency channel 4 is set in b8a6h - b8a7h, 12 bits
  429. # if b8a8h is written, frequency channel 5 is set in b8a8h - b8a9h, 12 bits
  430. # if b8aah is written, volume channel 1 is set, 4 bits
  431. # if b8abh is written, volume channel 2 is set, 4 bits
  432. # if b8ach is written, volume channel 3 is set, 4 bits
  433. # if b8adh is written, volume channel 4 is set, 4 bits
  434. # if b8aeh is written, volume channel 5 is set, 4 bits
  435. # if b8afh is written, channels 1-5 on/off, 1 bit
  436. #VGM port format:
  437. #0x00 - waveform
  438. #0x01 - frequency
  439. #0x02 - volume
  440. #0x03 - key on/off
  441. #0x04 - waveform (0x00 used to do SCC access, 0x04 SCC+)
  442. #0x05 - test register
  443. update_time
  444. variable music_data
  445. if {0xB800 <= $::wp_last_address && $::wp_last_address < 0xB8A0} {
  446. append music_data [binary format cccc 0xD2 0x4 [expr {$::wp_last_address - 0xB800}] $::wp_last_value]
  447. } elseif {0xB8A0 <= $::wp_last_address && $::wp_last_address < 0xb8aa} {
  448. append music_data [binary format cccc 0xD2 0x1 [expr {$::wp_last_address - 0xB8A0}] $::wp_last_value]
  449. } elseif {0xB8AA <= $::wp_last_address && $::wp_last_address < 0xB8AF} {
  450. append music_data [binary format cccc 0xD2 0x2 [expr {$::wp_last_address - 0xB8AA}] $::wp_last_value]
  451. } elseif {$::wp_last_address == 0xB8AF} {
  452. append music_data [binary format cccc 0xD2 0x3 0x0 $::wp_last_value]
  453. }
  454. variable scc_plus_used true
  455. }
  456. proc update_time {} {
  457. variable start_time
  458. if {$start_time == 0} {
  459. set start_time [machine_info time]
  460. message "VGM recording started, data was written to one of the sound chips recording for."
  461. }
  462. variable tick_time
  463. set tick_time [machine_info time]
  464. set new_ticks [expr {int(($tick_time - $start_time) * 44100)}]
  465. variable ticks
  466. if {$new_ticks < $ticks} {
  467. puts "VGM warning: Tick backstep ($new_ticks -> $ticks)"
  468. set ticks [expr {$new_ticks - 65535}]
  469. }
  470. variable music_data
  471. while {$new_ticks > $ticks} {
  472. set difference [expr {$new_ticks - $ticks}]
  473. set step [expr {$difference > 65535 ? 65535 : $difference}]
  474. incr ticks $step
  475. append music_data [binary format cs 0x61 $step]
  476. }
  477. }
  478. proc vgm_rec_end {abort} {
  479. variable active
  480. if {!$active} {
  481. error "Not recording currently..."
  482. }
  483. # remove all watchpoints that were created
  484. variable watchpoints
  485. foreach watch $watchpoints {
  486. if {[catch {
  487. debug remove_watchpoint $watch
  488. } errorText]} {
  489. puts "Failed to remove watchpoint $watch... using savestates maybe? Continue anyway."
  490. }
  491. }
  492. set watchpoints [list]
  493. if {!$abort} {
  494. update_time
  495. variable tick_time 0
  496. variable music_data
  497. variable temp_music_data
  498. append temp_music_data $music_data [binary format c 0x66]
  499. set header "Vgm "
  500. # file size
  501. append header [little_endian_32 [expr {[string length $temp_music_data] + 0x100 - 4}]]
  502. # VGM version 1.7
  503. append header [little_endian_32 0x161] [zeros 4]
  504. # YM2413 clock
  505. variable fm_logged
  506. if {$fm_logged} {
  507. append header [little_endian_32 3579545]
  508. } else {
  509. append header [zeros 4]
  510. }
  511. append header [zeros 4]
  512. # Number of ticks
  513. variable ticks
  514. append header [little_endian_32 $ticks]
  515. set ticks 0
  516. append header [zeros 24]
  517. # Data starts at offset 0x100
  518. append header [little_endian_32 [expr {0x100 - 0x34}]] [zeros 32]
  519. # Y8950 clock
  520. variable y8950_logged
  521. if {$y8950_logged} {
  522. append header [little_endian_32 3579545]
  523. } else {
  524. append header [zeros 4]
  525. }
  526. append header [zeros 4]
  527. # YMF278B clock
  528. variable moonsound_logged
  529. if {$moonsound_logged} {
  530. append header [little_endian_32 33868800]
  531. } else {
  532. append header [zeros 4]
  533. }
  534. append header [zeros 16]
  535. # AY8910 clock
  536. variable psg_logged
  537. if {$psg_logged} {
  538. append header [little_endian_32 1789773]
  539. } else {
  540. append header [zeros 4]
  541. }
  542. append header [zeros 36]
  543. # SCC clock
  544. variable scc_logged
  545. if {$scc_logged} {
  546. set scc_clock 1789773
  547. variable scc_plus_used
  548. if {$scc_plus_used} {
  549. # enable bit 31 for SCC+ support, that's how it's done
  550. # in VGM I've been told. Thanks Grauw.
  551. set scc_clock [expr {$scc_clock | 1 << 31}]
  552. }
  553. append header [little_endian_32 $scc_clock]
  554. } else {
  555. append header [zeros 4]
  556. }
  557. append header [zeros 96]
  558. variable file_name
  559. variable directory
  560. # Title hacks
  561. variable mbwave_title_hack
  562. variable mbwave_basic_title_hack
  563. if {$mbwave_title_hack || $mbwave_basic_title_hack} {
  564. set title_address [expr {$mbwave_title_hack ? 0xffc6 : 0xc0dc}]
  565. set file_name [string map {/ -} [debug read_block "Main RAM" $title_address 0x32]]
  566. set file_name [string trim $file_name]
  567. set file_name [format %s%s%s%s $directory "/" $file_name ".vgm"]
  568. }
  569. set file_handle [open $file_name "w"]
  570. fconfigure $file_handle -encoding binary -translation binary
  571. puts -nonewline $file_handle $header
  572. puts -nonewline $file_handle $temp_music_data
  573. close $file_handle
  574. set stop_message "VGM recording stopped, wrote data to $file_name."
  575. } else {
  576. set stop_message "VGM recording aborted, no data written..."
  577. }
  578. set active false
  579. variable start_time 0
  580. variable loop_amount 0
  581. message $stop_message
  582. return $stop_message
  583. }
  584. proc vgm_rec_next {} {
  585. variable active
  586. if {!$active} {
  587. variable original_filename "music"
  588. } else {
  589. vgm_rec_end false
  590. }
  591. vgm_rec_start
  592. }
  593. # 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
  594. proc vgm_check_audio_data_written {} {
  595. variable active
  596. if {!$active} return
  597. variable tick_time
  598. variable auto_next
  599. set now [machine_info time]
  600. if {$tick_time == 0 || $now - $tick_time < 1} {
  601. after time 1 vgm::vgm_check_audio_data_written
  602. } else {
  603. vgm::vgm_rec_end false
  604. if {$auto_next} {
  605. message "auto_next feature active, starting next recording"
  606. vgm::vgm_rec_start
  607. }
  608. }
  609. }
  610. # Loop point logger for MBWave only
  611. proc vgm_log_loop_point {} {
  612. variable watchpoints
  613. lappend watchpoints [debug set_watchpoint write_mem 0x51f7 {} {vgm::vgm_check_loop_point}]
  614. }
  615. proc vgm_check_loop_point {} {
  616. variable start_time
  617. if {$start_time == 0} return
  618. variable position
  619. set position_new [expr {$::wp_last_value == 255 ? 0 : $::wp_last_value}]
  620. if {$position_new < $position} {
  621. after time 1 vgm::vgm_log_loop_in_music_data
  622. }
  623. set position $position_new
  624. }
  625. proc vgm_log_loop_in_music_data {} {
  626. variable start_time
  627. if {$start_time == 0} return
  628. variable loop_amount
  629. incr loop_amount
  630. variable music_data
  631. append music_data [binary format ccc 0xbb 0xbb 0xbb]
  632. if {$loop_amount == 1} {
  633. message "First loop: Track-length in seconds (if not using transposing..): [expr {[machine_info time] - $start_time}]. Marker inserted in VGM file."
  634. }
  635. if {$loop_amount == 2} {
  636. message "Second loop. Marker inserted in VGM file."
  637. }
  638. if {$loop_amount > 2} {
  639. keymatrixdown 7 4
  640. message "Third loop occurred, stop playback."
  641. variable position 0
  642. set loop_amount 0
  643. after time 1 {keymatrixup 7 4}
  644. }
  645. }
  646. namespace export vgm_rec
  647. }
  648. namespace import vgm::*