-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathvgmconverter.py
2980 lines (2346 loc) · 102 KB
/
vgmconverter.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python
# python script to convert & process VGM files for SN76489 PSG
# by scrubbly 2016
# Released under MIT license
#
# VGM files can be loaded, filtered, transposed for different clock speeds, and quantized to fixed playback rates (lossy)
#
# Created primarily to enable porting of NTSC or PAL versions of SN76489 chip tunes to the BBC Micro, but is generally useful for other platforms.
#
# Based on https://github.com/cdodd/vgmparse
#
# Useful VGM/SN76489 References & Resources:
# http://www.smspower.org/Development/SN76489
# http://vgmrips.net/wiki/VGM_Specification
# http://vgmrips.net/packs/pack/svc-motm
# http://www.wothke.ch/webvgm/
# http://www.stairwaytohell.com/music/index.html?page=vgmarchive
# http://www.zeridajh.org/articles/various/sn76489/index.htm
# http://www.smspower.org/Music/Homebrew
# http://www.tommowalker.co.uk/music.html
# http://battleofthebits.org/arena/Tag/SN76489/
# http://battleofthebits.org/browser/
import gzip
import struct
import sys
import binascii
import math
from os.path import basename
if (sys.version_info > (3, 0)):
from io import BytesIO as ByteBuffer
else:
from StringIO import StringIO as ByteBuffer
#-----------------------------------------------------------------------------
class FatalError(Exception):
pass
class VgmStream:
# VGM commands:
# 0x50 [dd] = PSG SN76489 write value dd
# 0x61 [nnnn] = WAIT n cycles (0-65535)
# 0x62 = WAIT 735 samples (1/60 sec)
# 0x63 = WAIT 882 samples (1/50 sec)
# 0x66 = END
# 0x7n = WAIT n+1 samples (0-15)
#--------------------------------------------------------------------------------------------------------------------------------
# SN76489 register writes
# If bit 7 is 1 then the byte is a LATCH/DATA byte.
# %1cctdddd
# cc - channel (0-3)
# t - type (1 to latch volume, 1 to latch tone/noise)
# dddd - placed into the low 4 bits of the relevant register. For the three-bit noise register, the highest bit is discarded.
#
# If bit 7 is 0 then the byte is a DATA byte.
# %0-DDDDDD
# If the currently latched register is a tone register then the low 6 bits of the byte (DDDDDD)
# are placed into the high 6 bits of the latched register. If the latched register is less than 6 bits wide
# (ie. not one of the tone registers), instead the low bits are placed into the corresponding bits of the
# register, and any extra high bits are discarded.
#
# Tone registers
# DDDDDDdddd = cccccccccc
# DDDDDDdddd gives the 10-bit half-wave counter reset value.
#
# Volume registers
# (DDDDDD)dddd = (--vvvv)vvvv
# dddd gives the 4-bit volume value.
# If a data byte is written, the low 4 bits of DDDDDD update the 4-bit volume value. However, this is unnecessary.
#
# Noise register
# (DDDDDD)dddd = (---trr)-trr
# The low 2 bits of dddd select the shift rate and the next highest bit (bit 2) selects the mode (white (1) or "periodic" (0)).
# If a data byte is written, its low 3 bits update the shift rate and mode in the same way.
#--------------------------------------------------------------------------------------------------------------------------------
# script vars / configs
VGM_FREQUENCY = 44100
# script options
RETUNE_PERIODIC = True # [TO BE REMOVED] if true will attempt to retune any use of the periodic noise effect
VERBOSE = False
STRIP_GD3 = False
LENGTH = 0 # required output length (in seconds)
# VGM file identifier
vgm_magic_number = b'Vgm '
disable_dual_chip = True # [TODO] handle dual PSG a bit better
vgm_source_clock = 0
vgm_target_clock = 0
vgm_filename = ''
vgm_loop_offset = 0
vgm_loop_length = 0
# Supported VGM versions
supported_ver_list = [
0x00000101,
0x00000110,
0x00000150,
0x00000151,
0x00000160,
0x00000161,
]
# VGM metadata offsets
metadata_offsets = {
# SDM Hacked version number 101 too
0x00000101: {
'vgm_ident': {'offset': 0x00, 'size': 4, 'type_format': None},
'eof_offset': {'offset': 0x04, 'size': 4, 'type_format': '<I'},
'version': {'offset': 0x08, 'size': 4, 'type_format': '<I'},
'sn76489_clock': {'offset': 0x0c, 'size': 4, 'type_format': '<I'},
'ym2413_clock': {'offset': 0x10, 'size': 4, 'type_format': '<I'},
'gd3_offset': {'offset': 0x14, 'size': 4, 'type_format': '<I'},
'total_samples': {'offset': 0x18, 'size': 4, 'type_format': '<I'},
'loop_offset': {'offset': 0x1c, 'size': 4, 'type_format': '<I'},
'loop_samples': {'offset': 0x20, 'size': 4, 'type_format': '<I'},
'rate': {'offset': 0x24, 'size': 4, 'type_format': '<I'},
'sn76489_feedback': {
'offset': 0x28,
'size': 2,
'type_format': '<H',
},
'sn76489_shift_register_width': {
'offset': 0x2a,
'size': 1,
'type_format': 'B',
},
'ym2612_clock': {'offset': 0x2c, 'size': 4, 'type_format': '<I'},
'ym2151_clock': {'offset': 0x30, 'size': 4, 'type_format': '<I'},
'vgm_data_offset': {
'offset': 0x34,
'size': 4,
'type_format': '<I',
},
},
# Version 1.10`
0x00000110: {
'vgm_ident': {'offset': 0x00, 'size': 4, 'type_format': None},
'eof_offset': {'offset': 0x04, 'size': 4, 'type_format': '<I'},
'version': {'offset': 0x08, 'size': 4, 'type_format': '<I'},
'sn76489_clock': {'offset': 0x0c, 'size': 4, 'type_format': '<I'},
'ym2413_clock': {'offset': 0x10, 'size': 4, 'type_format': '<I'},
'gd3_offset': {'offset': 0x14, 'size': 4, 'type_format': '<I'},
'total_samples': {'offset': 0x18, 'size': 4, 'type_format': '<I'},
'loop_offset': {'offset': 0x1c, 'size': 4, 'type_format': '<I'},
'loop_samples': {'offset': 0x20, 'size': 4, 'type_format': '<I'},
'rate': {'offset': 0x24, 'size': 4, 'type_format': '<I'},
'sn76489_feedback': {
'offset': 0x28,
'size': 2,
'type_format': '<H',
},
'sn76489_shift_register_width': {
'offset': 0x2a,
'size': 1,
'type_format': 'B',
},
'ym2612_clock': {'offset': 0x2c, 'size': 4, 'type_format': '<I'},
'ym2151_clock': {'offset': 0x30, 'size': 4, 'type_format': '<I'},
'vgm_data_offset': {
'offset': 0x34,
'size': 4,
'type_format': '<I',
},
},
# Version 1.50`
0x00000150: {
'vgm_ident': {'offset': 0x00, 'size': 4, 'type_format': None},
'eof_offset': {'offset': 0x04, 'size': 4, 'type_format': '<I'},
'version': {'offset': 0x08, 'size': 4, 'type_format': '<I'},
'sn76489_clock': {'offset': 0x0c, 'size': 4, 'type_format': '<I'},
'ym2413_clock': {'offset': 0x10, 'size': 4, 'type_format': '<I'},
'gd3_offset': {'offset': 0x14, 'size': 4, 'type_format': '<I'},
'total_samples': {'offset': 0x18, 'size': 4, 'type_format': '<I'},
'loop_offset': {'offset': 0x1c, 'size': 4, 'type_format': '<I'},
'loop_samples': {'offset': 0x20, 'size': 4, 'type_format': '<I'},
'rate': {'offset': 0x24, 'size': 4, 'type_format': '<I'},
'sn76489_feedback': {
'offset': 0x28,
'size': 2,
'type_format': '<H',
},
'sn76489_shift_register_width': {
'offset': 0x2a,
'size': 1,
'type_format': 'B',
},
'ym2612_clock': {'offset': 0x2c, 'size': 4, 'type_format': '<I'},
'ym2151_clock': {'offset': 0x30, 'size': 4, 'type_format': '<I'},
'vgm_data_offset': {
'offset': 0x34,
'size': 4,
'type_format': '<I',
},
},
# SDM Hacked version number, we are happy enough to parse v1.51 as if it were 1.50 since the 1.51 updates dont apply to us anyway
0x00000151: {
'vgm_ident': {'offset': 0x00, 'size': 4, 'type_format': None},
'eof_offset': {'offset': 0x04, 'size': 4, 'type_format': '<I'},
'version': {'offset': 0x08, 'size': 4, 'type_format': '<I'},
'sn76489_clock': {'offset': 0x0c, 'size': 4, 'type_format': '<I'},
'ym2413_clock': {'offset': 0x10, 'size': 4, 'type_format': '<I'},
'gd3_offset': {'offset': 0x14, 'size': 4, 'type_format': '<I'},
'total_samples': {'offset': 0x18, 'size': 4, 'type_format': '<I'},
'loop_offset': {'offset': 0x1c, 'size': 4, 'type_format': '<I'},
'loop_samples': {'offset': 0x20, 'size': 4, 'type_format': '<I'},
'rate': {'offset': 0x24, 'size': 4, 'type_format': '<I'},
'sn76489_feedback': {
'offset': 0x28,
'size': 2,
'type_format': '<H',
},
'sn76489_shift_register_width': {
'offset': 0x2a,
'size': 1,
'type_format': 'B',
},
'ym2612_clock': {'offset': 0x2c, 'size': 4, 'type_format': '<I'},
'ym2151_clock': {'offset': 0x30, 'size': 4, 'type_format': '<I'},
'vgm_data_offset': {
'offset': 0x34,
'size': 4,
'type_format': '<I',
},
},
# SDM Hacked version number, we are happy enough to parse v1.60 as if it were 1.50 since the 1.51 updates dont apply to us anyway
0x00000160: {
'vgm_ident': {'offset': 0x00, 'size': 4, 'type_format': None},
'eof_offset': {'offset': 0x04, 'size': 4, 'type_format': '<I'},
'version': {'offset': 0x08, 'size': 4, 'type_format': '<I'},
'sn76489_clock': {'offset': 0x0c, 'size': 4, 'type_format': '<I'},
'ym2413_clock': {'offset': 0x10, 'size': 4, 'type_format': '<I'},
'gd3_offset': {'offset': 0x14, 'size': 4, 'type_format': '<I'},
'total_samples': {'offset': 0x18, 'size': 4, 'type_format': '<I'},
'loop_offset': {'offset': 0x1c, 'size': 4, 'type_format': '<I'},
'loop_samples': {'offset': 0x20, 'size': 4, 'type_format': '<I'},
'rate': {'offset': 0x24, 'size': 4, 'type_format': '<I'},
'sn76489_feedback': {
'offset': 0x28,
'size': 2,
'type_format': '<H',
},
'sn76489_shift_register_width': {
'offset': 0x2a,
'size': 1,
'type_format': 'B',
},
'ym2612_clock': {'offset': 0x2c, 'size': 4, 'type_format': '<I'},
'ym2151_clock': {'offset': 0x30, 'size': 4, 'type_format': '<I'},
'vgm_data_offset': {
'offset': 0x34,
'size': 4,
'type_format': '<I',
},
},
# SDM Hacked version number, we are happy enough to parse v1.61 as if it were 1.50 since the 1.51 updates dont apply to us anyway
0x00000161: {
'vgm_ident': {'offset': 0x00, 'size': 4, 'type_format': None},
'eof_offset': {'offset': 0x04, 'size': 4, 'type_format': '<I'},
'version': {'offset': 0x08, 'size': 4, 'type_format': '<I'},
'sn76489_clock': {'offset': 0x0c, 'size': 4, 'type_format': '<I'},
'ym2413_clock': {'offset': 0x10, 'size': 4, 'type_format': '<I'},
'gd3_offset': {'offset': 0x14, 'size': 4, 'type_format': '<I'},
'total_samples': {'offset': 0x18, 'size': 4, 'type_format': '<I'},
'loop_offset': {'offset': 0x1c, 'size': 4, 'type_format': '<I'},
'loop_samples': {'offset': 0x20, 'size': 4, 'type_format': '<I'},
'rate': {'offset': 0x24, 'size': 4, 'type_format': '<I'},
'sn76489_feedback': {
'offset': 0x28,
'size': 2,
'type_format': '<H',
},
'sn76489_shift_register_width': {
'offset': 0x2a,
'size': 1,
'type_format': 'B',
},
'ym2612_clock': {'offset': 0x2c, 'size': 4, 'type_format': '<I'},
'ym2151_clock': {'offset': 0x30, 'size': 4, 'type_format': '<I'},
'vgm_data_offset': {
'offset': 0x34,
'size': 4,
'type_format': '<I',
},
}
}
# constructor - pass in the filename of the VGM
def __init__(self, vgm_filename):
self.vgm_filename = vgm_filename
print " VGM file loaded : '" + vgm_filename + "'"
# open the vgm file and parse it
vgm_file = open(vgm_filename, 'rb')
vgm_data = vgm_file.read()
# Store the VGM data and validate it
self.data = ByteBuffer(vgm_data)
vgm_file.close()
# parse
self.validate_vgm_data()
# Set up the variables that will be populated
self.command_list = []
self.data_block = None
self.gd3_data = {}
self.metadata = {}
# Parse the VGM metadata and validate the VGM version
self.parse_metadata()
# Display info about the file
self.vgm_loop_offset = self.metadata['loop_offset']
self.vgm_loop_length = self.metadata['loop_samples']
print " VGM Version : " + "%x" % int(self.metadata['version'])
print "VGM SN76489 clock : " + str(float(self.metadata['sn76489_clock'])/1000000) + " MHz"
print " VGM Rate : " + str(float(self.metadata['rate'])) + " Hz"
print " VGM Samples : " + str(int(self.metadata['total_samples'])) + " (" + str(int(self.metadata['total_samples'])/self.VGM_FREQUENCY) + " seconds)"
print " VGM Loop Offset : " + str(self.vgm_loop_offset)
print " VGM Loop Length : " + str(self.vgm_loop_length)
# Validation to check we can parse it
self.validate_vgm_version()
# Sanity check this VGM is suitable for this script - must be SN76489 only
if self.metadata['sn76489_clock'] == 0 or self.metadata['ym2413_clock'] !=0 or self.metadata['ym2413_clock'] !=0 or self.metadata['ym2413_clock'] !=0:
raise FatalError("This script only supports VGM's for SN76489 PSG")
# see if this VGM uses Dual Chip mode
if (self.metadata['sn76489_clock'] & 0x40000000) == 0x40000000:
self.dual_chip_mode_enabled = True
else:
self.dual_chip_mode_enabled = False
print " VGM Dual Chip : " + str(self.dual_chip_mode_enabled)
# override/disable dual chip commands in the output stream if required
if (self.disable_dual_chip == True) and (self.dual_chip_mode_enabled == True) :
# remove the clock flag that enables dual chip mode
self.metadata['sn76489_clock'] = self.metadata['sn76489_clock'] & 0xbfffffff
self.dual_chip_mode_enabled = False
print "Dual Chip Mode Disabled - DC Commands will be removed"
# take a copy of the clock speed for the VGM processor functions
self.vgm_source_clock = self.metadata['sn76489_clock']
self.vgm_target_clock = self.vgm_source_clock
# Parse GD3 data and the VGM commands
self.parse_gd3()
self.parse_commands()
print " VGM Commands # : " + str(len(self.command_list))
print ""
def validate_vgm_data(self):
# Save the current position of the VGM data
original_pos = self.data.tell()
# Seek to the start of the file
self.data.seek(0)
# Perform basic validation on the given file by checking for the VGM
# magic number ('Vgm ')
if self.data.read(4) != self.vgm_magic_number:
# Could not find the magic number. The file could be gzipped (e.g.
# a vgz file). Try un-gzipping the file and trying again.
self.data.seek(0)
self.data = gzip.GzipFile(fileobj=self.data, mode='rb')
try:
if self.data.read(4) != self.vgm_magic_number:
print "Error: Data does not appear to be a valid VGM file"
raise ValueError('Data does not appear to be a valid VGM file')
except IOError:
print "Error: Data does not appear to be a valid VGM file"
# IOError will be raised if the file is not a valid gzip file
raise ValueError('Data does not appear to be a valid VGM file')
# Seek back to the original position in the VGM data
self.data.seek(original_pos)
def parse_metadata(self):
# Save the current position of the VGM data
original_pos = self.data.tell()
# Create the list to store the VGM metadata
self.metadata = {}
# Iterate over the offsets and parse the metadata
for version, offsets in self.metadata_offsets.items():
for value, offset_data in offsets.items():
# Seek to the data location and read the data
self.data.seek(offset_data['offset'])
data = self.data.read(offset_data['size'])
# Unpack the data if required
if offset_data['type_format'] is not None:
self.metadata[value] = struct.unpack(
offset_data['type_format'],
data,
)[0]
else:
self.metadata[value] = data
# Seek back to the original position in the VGM data
self.data.seek(original_pos)
def validate_vgm_version(self):
if self.metadata['version'] not in self.supported_ver_list:
print "VGM version is not supported"
raise FatalError('VGM version is not supported')
def parse_gd3(self):
# Save the current position of the VGM data
original_pos = self.data.tell()
# Seek to the start of the GD3 data
self.data.seek(
self.metadata['gd3_offset'] +
self.metadata_offsets[self.metadata['version']]['gd3_offset']['offset']
)
# Skip 8 bytes ('Gd3 ' string and 4 byte version identifier)
self.data.seek(8, 1)
# Get the length of the GD3 data, then read it
gd3_length = struct.unpack('<I', self.data.read(4))[0]
gd3_data = ByteBuffer(self.data.read(gd3_length))
# Parse the GD3 data
gd3_fields = []
current_field = b''
while True:
# Read two bytes. All characters (English and Japanese) in the GD3
# data use two byte encoding
char = gd3_data.read(2)
# Break if we are at the end of the GD3 data
if char == b'':
break
# Check if we are at the end of a field, if not then continue to
# append to "current_field"
if char == b'\x00\x00':
gd3_fields.append(current_field)
current_field = b''
else:
current_field += char
# Once all the fields have been parsed, create a dict with the data
# some Gd3 tags dont have notes section
gd3_notes = ''
gd3_title_eng = basename(self.vgm_filename).encode("utf_16")
if len(gd3_fields) > 10:
gd3_notes = gd3_fields[10]
if len(gd3_fields) > 8:
if len(gd3_fields[0]) > 0:
gd3_title_eng = gd3_fields[0]
self.gd3_data = {
'title_eng': gd3_title_eng,
'title_jap': gd3_fields[1],
'game_eng': gd3_fields[2],
'game_jap': gd3_fields[3],
'console_eng': gd3_fields[4],
'console_jap': gd3_fields[5],
'artist_eng': gd3_fields[6],
'artist_jap': gd3_fields[7],
'date': gd3_fields[8],
'vgm_creator': gd3_fields[9],
'notes': gd3_notes
}
else:
print "WARNING: Malformed/missing GD3 tag"
self.gd3_data = {
'title_eng': gd3_title_eng,
'title_jap': '',
'game_eng': '',
'game_jap': '',
'console_eng': '',
'console_jap': '',
'artist_eng': 'Unknown'.encode("utf_16"),
'artist_jap': '',
'date': '',
'vgm_creator': '',
'notes': ''
}
# Seek back to the original position in the VGM data
self.data.seek(original_pos)
#-------------------------------------------------------------------------------------------------
def parse_commands(self):
# Save the current position of the VGM data
original_pos = self.data.tell()
# Seek to the start of the VGM data
self.data.seek(
self.metadata['vgm_data_offset'] +
self.metadata_offsets[self.metadata['version']]['vgm_data_offset']['offset']
)
while True:
# Read a byte, this will be a VGM command, we will then make
# decisions based on the given command
command = self.data.read(1)
# Break if we are at the end of the file
if command == '':
break
# 0x4f dd - Game Gear PSG stereo, write dd to port 0x06
# 0x50 dd - PSG (SN76489/SN76496) write value dd
if command in [b'\x4f', b'\x50']:
self.command_list.append({
'command': command,
'data': self.data.read(1),
})
# 0x51 aa dd - YM2413, write value dd to register aa
# 0x52 aa dd - YM2612 port 0, write value dd to register aa
# 0x53 aa dd - YM2612 port 1, write value dd to register aa
# 0x54 aa dd - YM2151, write value dd to register aa
elif command in [b'\x51', b'\x52', b'\x53', b'\x54']:
self.command_list.append({
'command': command,
'data': self.data.read(2),
})
# 0x61 nn nn - Wait n samples, n can range from 0 to 65535
elif command == b'\x61':
self.command_list.append({
'command': command,
'data': self.data.read(2),
})
# 0x62 - Wait 735 samples (60th of a second)
# 0x63 - Wait 882 samples (50th of a second)
# 0x66 - End of sound data
elif command in [b'\x62', b'\x63', b'\x66']:
self.command_list.append({'command': command, 'data': None})
# Stop processing commands if we are at the end of the music
# data
if command == b'\x66':
break
# 0x67 0x66 tt ss ss ss ss - Data block
elif command == b'\x67':
# Skip the compatibility and type bytes (0x66 tt)
self.data.seek(2, 1)
# Read the size of the data block
data_block_size = struct.unpack('<I', self.data.read(4))[0]
# Store the data block for later use
self.data_block = ByteBuffer(self.data.read(data_block_size))
# 0x7n - Wait n+1 samples, n can range from 0 to 15
# 0x8n - YM2612 port 0 address 2A write from the data bank, then
# wait n samples; n can range from 0 to 15
elif b'\x70' <= command <= b'\x8f':
self.command_list.append({'command': command, 'data': None})
# 0xe0 dddddddd - Seek to offset dddddddd (Intel byte order) in PCM
# data bank
elif command == b'\xe0':
self.command_list.append({
'command': command,
'data': self.data.read(4),
})
# 0x30 dd - dual chip command
elif command == b'\x30':
if self.dual_chip_mode_enabled:
self.command_list.append({
'command': command,
'data': self.data.read(1),
})
# Seek back to the original position in the VGM data
self.data.seek(original_pos)
#-------------------------------------------------------------------------------------------------
def write_vgm(self, filename):
print " VGM Processing : Writing output VGM file '" + filename + "'"
vgm_stream = bytearray()
vgm_time = 0
# convert the VGM command list to a byte array
for elem in self.command_list:
command = elem['command']
data = elem['data']
# track time offset for debug purposes
if b'\x70' <= command <= b'\x7f':
pdata = binascii.hexlify(command)
t = int(pdata, 16)
t &= 15
t += 1
vgm_time += t
scommand = "WAITn"
#if self.VERBOSE: print " WAITN=" + str(t)
else:
pcommand = binascii.hexlify(command)
if pcommand == "61":
scommand = "WAIT"
pdata = binascii.hexlify(data)
t = int(pdata, 16)
# sdm: swap bytes to LSB
lsb = t & 255
msb = (t / 256)
t = (lsb * 256) + msb
vgm_time += t
#if self.VERBOSE: print " WAIT=" + str(t)
else:
if pcommand == "62": #wait60
vgm_time += 735
else:
if pcommand == "63": #wait50
vgm_time += 882
if (data != None):
if self.VERBOSE: print "command=" + str(binascii.hexlify(command)) + ", data=" + str(binascii.hexlify(data)) + ", time=" + str(float(vgm_time)/44100.0) + " secs"
# filter dual chip
if b'\x30' == command:
if self.VERBOSE: print "DUAL CHIP COMMAND"
#continue
#command = b'\x50'
vgm_stream.extend(command)
if (data != None):
vgm_stream.extend(data)
vgm_stream_length = len(vgm_stream)
# build the GD3 data block
gd3_data = bytearray()
gd3_stream = bytearray()
gd3_stream_length = 0
gd3_offset = 0
if self.STRIP_GD3 == False:
gd3_data.extend(self.gd3_data['title_eng'] + b'\x00\x00')
gd3_data.extend(self.gd3_data['title_jap'] + b'\x00\x00')
gd3_data.extend(self.gd3_data['game_eng'] + b'\x00\x00')
gd3_data.extend(self.gd3_data['game_jap'] + b'\x00\x00')
gd3_data.extend(self.gd3_data['console_eng'] + b'\x00\x00')
gd3_data.extend(self.gd3_data['console_jap'] + b'\x00\x00')
gd3_data.extend(self.gd3_data['artist_eng'] + b'\x00\x00')
gd3_data.extend(self.gd3_data['artist_jap'] + b'\x00\x00')
gd3_data.extend(self.gd3_data['date'] + b'\x00\x00')
gd3_data.extend(self.gd3_data['vgm_creator'] + b'\x00\x00')
gd3_data.extend(self.gd3_data['notes'] + b'\x00\x00')
gd3_stream.extend('Gd3 ')
gd3_stream.extend(struct.pack('I', 0x100)) # GD3 version
gd3_stream.extend(struct.pack('I', len(gd3_data))) # GD3 length
gd3_stream.extend(gd3_data)
gd3_offset = (64-20) + vgm_stream_length
gd3_stream_length = len(gd3_stream)
else:
print " VGM Processing : GD3 tag was stripped"
# build the full VGM output stream
vgm_data = bytearray()
vgm_data.extend(self.vgm_magic_number)
vgm_data.extend(struct.pack('I', 64 + vgm_stream_length + gd3_stream_length - 4)) # EoF offset
vgm_data.extend(struct.pack('I', 0x00000151)) # Version
vgm_data.extend(struct.pack('I', self.metadata['sn76489_clock']))
vgm_data.extend(struct.pack('I', self.metadata['ym2413_clock']))
vgm_data.extend(struct.pack('I', gd3_offset)) # GD3 offset
vgm_data.extend(struct.pack('I', self.metadata['total_samples'])) # total samples
vgm_data.extend(struct.pack('I', 0)) #self.metadata['loop_offset'])) # loop offset
vgm_data.extend(struct.pack('I', 0)) #self.metadata['loop_samples'])) # loop # samples
vgm_data.extend(struct.pack('I', self.metadata['rate'])) # rate
vgm_data.extend(struct.pack('H', self.metadata['sn76489_feedback'])) # sn fb
vgm_data.extend(struct.pack('B', self.metadata['sn76489_shift_register_width'])) # SNW
vgm_data.extend(struct.pack('B', 0)) # SN Flags
vgm_data.extend(struct.pack('I', self.metadata['ym2612_clock']))
vgm_data.extend(struct.pack('I', self.metadata['ym2151_clock']))
vgm_data.extend(struct.pack('I', 12)) # VGM data offset
vgm_data.extend(struct.pack('I', 0)) # SEGA PCM clock
vgm_data.extend(struct.pack('I', 0)) # SPCM interface
# attach the vgm data
vgm_data.extend(vgm_stream)
# attach the vgm gd3 tag if required
if self.STRIP_GD3 == False:
vgm_data.extend(gd3_stream)
# write to output file
vgm_file = open(filename, 'wb')
vgm_file.write(vgm_data)
vgm_file.close()
print " VGM Processing : Written " + str(int(len(vgm_data))) + " bytes, GD3 tag used " + str(gd3_stream_length) + " bytes"
print "All done."
#-------------------------------------------------------------------------------------------------
# clock_type can be NTSC, PAL or BBC (case insensitive)
def set_target_clock(self, clock_type):
if clock_type.lower() == 'ntsc':
self.metadata['sn76489_feedback'] = 0x0006 # 0x0006 for SN76494, SN76496
self.metadata['sn76489_clock'] = 3579545 # usually 3.579545 MHz (NTSC) for Sega-based PSG tunes
self.metadata['sn76489_shift_register_width'] = 16 #
self.vgm_target_clock = self.metadata['sn76489_clock']
else:
if clock_type.lower() == 'pal':
self.metadata['sn76489_feedback'] = 0x0006 # 0x0006 for SN76494, SN76496
self.metadata['sn76489_clock'] = 4433619 # 4.43361875 Mz for PAL
self.metadata['sn76489_shift_register_width'] = 16 #
self.vgm_target_clock = self.metadata['sn76489_clock']
else:
if clock_type.lower() == 'bbc':
self.metadata['sn76489_feedback'] = 0x0003 # 0x0003 for BBC configuration of SN76489
self.metadata['sn76489_clock'] = 4000000 # 4.0 Mhz on Beeb,
self.metadata['sn76489_shift_register_width'] = 15 # BBC taps bit 15 on the SR
self.vgm_target_clock = self.metadata['sn76489_clock']
#-------------------------------------------------------------------------------------------------
def set_verbose(self, verbose):
self.VERBOSE = verbose
#-------------------------------------------------------------------------------------------------
def set_length(self, length):
self.LENGTH = int(length)
#-------------------------------------------------------------------------------------------------
# helper function
# given a start offset (default 0) into the command list, find the next index where
# the command matches search_command or return -1 if no more of these commands can be found.
def find_next_command(self, search_command, offset = 0):
for j in range(offset, len(self.command_list)):
c = self.command_list[j]["command"]
# only process write data commands
if c == search_command:
return j
else:
return -1
#-------------------------------------------------------------------------------------------------
# iterate through the command list, removing any write commands that are destined for filter_channel_id
def filter_channel(self, filter_channel_id):
print " VGM Processing : Filtering channel " + str(filter_channel_id)
filtered_command_list = []
j = 0
latched_channel = 0
for q in self.command_list:
# only process write data commands
if q["command"] != struct.pack('B', 0x50):
filtered_command_list.append(q)
else:
# Check if LATCH/DATA write
qdata = q["data"]
qw = int(binascii.hexlify(qdata), 16)
if qw & 128:
# Get channel id and latch it
latched_channel = (qw>>5)&3
if latched_channel != filter_channel_id:
filtered_command_list.append(q)
self.command_list = filtered_command_list
#-------------------------------------------------------------------------------------------------
# iterate through the command list, unpacking any single tone writes on a channel
def unpack_tones(self):
print " VGM Processing : Unpacking tones "
filtered_command_list = []
j = 0
latched_channel = 0
latched_tone_frequencies = [0, 0, 0, 0]
for n in range(len(self.command_list)):
q = self.command_list[n]
# we always output at least the same data stream, but we might inject a new tone write if needed
filtered_command_list.append(q)
# only process write data commands
if q["command"] == struct.pack('B', 0x50):
# Check if LATCH/DATA write
qdata = q["data"]
qw = int(binascii.hexlify(qdata), 16)
if qw & 128:
# Get channel id and latch it
latched_channel = (qw>>5)&3
# Check if TONE update
if (qw & 16) == 0:
# get low 4 bits and merge with latched channel's frequency register
qfreq = (qw & 0b00001111)
latched_tone_frequencies[latched_channel] = (latched_tone_frequencies[latched_channel] & 0b1111110000) | qfreq
# look ahead, and see if the next command is a DATA write as if so, this will be part of the same tone commmand
# so load this into our register as well so that we have the correct tone frequency to work with
multi_write = False
nindex = n
while (nindex < (len(self.command_list)-1)):# check we dont overflow the array, bail if we do, since it means we didn't find any further DATA writes.
nindex += 1
ncommand = self.command_list[nindex]["command"]
# skip any non-VGM-write commands
if ncommand != struct.pack('B', 0x50):
continue
else:
# found the next VGM write command
ndata = self.command_list[nindex]["data"]
# Check if next this is a DATA write, and capture frequency if so
# otherwise, its a LATCH/DATA write, so no additional frequency to process
nw = int(binascii.hexlify(ndata), 16)
if (nw & 128) == 0:
multi_write = True
nfreq = (nw & 0b00111111)
latched_tone_frequencies[latched_channel] = (latched_tone_frequencies[latched_channel] & 0b0000001111) | (nfreq << 4)
break
# if we detected a single register tone write, we need as unpack it, to make sure transposing works correctly
if multi_write == False and latched_channel != 3:
if self.VERBOSE: print " UNPACKING SINGLE REGISTER TONE WRITE on CHANNEL " + str(latched_channel)
# inject additional tone write to prevent any more single register tone writes
# re-program q and re-cycle it
hi_data = (latched_tone_frequencies[latched_channel]>>4) & 0b00111111
new_q = { "command" : q["command"], "data" : struct.pack('B', hi_data) }
filtered_command_list.append(new_q)
self.command_list = filtered_command_list
#-------------------------------------------------------------------------------------------------
# Process the tone frequencies in the VGM for the given clock_type ('ntsc', 'pal' or 'bbc')
# such that the output VGM plays at the same pitch as the original, but using the target clock speeds.
# Tuned periodic and white noise are also transposed.
def transpose(self, clock_type):
self.unpack_tones()
# setup the correct target chip parameters
self.set_target_clock(clock_type)
# total number of commands in the vgm stream
num_commands = len(self.command_list)
# re-tune any tone commands if target clock is different to source clock
# i think it's safe to do this in the quantized packets we've created, as they tend to be completed within a single time slot
# (eg. little or no chance of a multi-tone LATCH+DATA write being split by a wait command)
if (self.vgm_source_clock != self.vgm_target_clock):
print " VGM Processing : Re-tuning VGM to new clock speed"
print " VGM Processing : Original clock " + str(float(self.vgm_source_clock)/1000000.0) + " MHz, Target Clock " + str(float(self.vgm_target_clock)/1000000.0) + " MHz"
# used by the clock retuning code, initialized once at the start of the song, so that latched register states are preserved across the song
latched_tone_frequencies = [0, 0, 0, 0]
latched_volumes = [0, 0, 0, 0]
tone2_offsets = [-1, -1]
latched_channel = 0
vgm_time = 0
# helper function
# calculates a retuned tone frequency based on given frequency & periodic noise indication
# returns retuned frequency.
# does not change any external state
def recalc_frequency(tone_frequency, is_periodic_noise_tone = False):
if self.VERBOSE: print " recalc_frequency(), vgm_time=" + str(vgm_time) + " clock time=" + str(float(vgm_time)/44100.0) + " secs"
# compute the correct frequency
# first check it is not 0 (illegal value)
output_freq = 0
if tone_frequency == 0:
if self.VERBOSE: print "Zero frequency tone detected on channel "# + str(latched_channel)
else:
# compute correct hz frequency of current tone from formula:
#
# hz = Clock Or for periodic noise: hz = Clock where SR is 15 or 16 depending on chip
# ------------- ------------------
# ( 2 x N x 16) ( 2 x N x 16 x SR)
if is_periodic_noise_tone:
if self.VERBOSE: print "Periodic noise tone"
noise_ratio = (15.0 / 16.0) * (float(self.vgm_source_clock) / float(self.vgm_target_clock))
v = float(tone_frequency) / noise_ratio
if self.VERBOSE: print "noise_ratio=" + str(noise_ratio)
if self.VERBOSE: print "original freq=" + str(tone_frequency) + ", new freq=" + str(v)
if self.VERBOSE: print "retuned periodic noise effect on channel 2"
else:
if self.VERBOSE: print "Normal tone"
# compute corrected tone register value for generating the same frequency using the target chip's clock rate
hz = float(self.vgm_source_clock) / ( 2.0 * float(tone_frequency) * 16.0)
if self.VERBOSE: print "hz=" + str(hz)
v = float(self.vgm_target_clock) / (2.0 * hz * 16.0 )
if self.VERBOSE: print "v=" + str(v)
# due to the integer maths, some precision is lost at the lower end
output_freq = int(round(v)) # using round minimizes error margin at lower precision
# clamp range to 10 bits
if output_freq > 1023:
output_freq = 1023
if output_freq < 1:
output_freq = 1
if is_periodic_noise_tone:
hz1 = float(self.vgm_source_clock) / (2.0 * float(tone_frequency) * 16.0 * 15.0) # target frequency
hz2 = float(self.vgm_target_clock) / (2.0 * float(output_freq) * 16.0 * 15.0)
else:
hz1 = float(self.vgm_source_clock) / (2.0 * float(tone_frequency) * 16.0) # target frequency
hz2 = float(self.vgm_target_clock) / (2.0 * float(output_freq) * 16.0)
hz_err = hz2-hz1
if self.VERBOSE: print "channel=" + str(latched_channel) + ", old frequency=" + str(tone_frequency) + ", new frequency=" + str(output_freq) + ", source_clock=" + str(self.vgm_source_clock) + ", target_clock=" + str(self.vgm_target_clock) + ", src_hz=" + str(hz1) + ", tgt_hz=" + str(hz2) + ", hz_err =" + str(hz_err)
if hz_err > 2.0 or hz_err < -2.0:
print " WARNING: Large error transposing tone! [" + str(hz_err) + " Hz ] (channel="+str(latched_channel)+", PN="+str(is_periodic_noise_tone)+")"
#if self.VERBOSE: print ""
return output_freq
TEST_OUTPUT = False # show additional test output in this section
# iterate through write commands looking for tone writes and recalculate their frequencies
## first create a reference copy of the command list (just for a tuning hack below)
#command_list_copy = list(self.command_list)
for n in range(len(self.command_list)):
command = self.command_list[n]["command"]