-
Notifications
You must be signed in to change notification settings - Fork 3
/
ym2sn.py
2641 lines (2060 loc) · 110 KB
/
ym2sn.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
# ym2sn.py
# .YM files (YM2149 sound chip) to SN76489 .VGM music file format conversion utility
# was originated based on code from https://github.com/FlorentFlament/ym2149-streamer
# Almost completely rewritten by https://github.com/simondotm/
#
# Copyright (c) 2019 Simon Morris. All rights reserved.
#
# "MIT License":
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the Software
# is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
# PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import functools
import itertools
import struct
import sys
import time
import binascii
import math
import os
from os.path import basename
PYTHON_VERSION = sys.version_info[0] # returns 2 or >=3
# Command line can override these defaults
SN_CLOCK = 4000000 # set this to the target SN chip clock speed
LFSR_BIT = 15 # set this to either 15 or 16 depending on which bit of the LFSR is tapped in the SN chip
ENABLE_ENVELOPES = True # enable this to simulate envelopes in the output
ENABLE_ATTENUATION = False # enables conversion of YM to SN attenuation. In theory a better matching of volume in the output.
FILTER_CHANNEL_A = False
FILTER_CHANNEL_B = False
FILTER_CHANNEL_C = False
FILTER_CHANNEL_N = False # Noise channel
ENABLE_DEBUG = False # enable this to have ALL the info spitting out. This is more than ENABLE_VERBOSE
ENABLE_VERBOSE = False
ARDUINO_BIN = False
ENABLE_TUNED_NOISE = False # enable this to tune white noise rather than use the nearest fixed frequency white noise
# white noise is generated every counter cycle, so unlike square wave scale is 1.0 rather than 2.0
SN_NOISE_DC = 2.0 # 2.0
# Percussive Noise has a dedicated channel and attenuator on the SN
# whereas on the YM, the noise waveform is logically OR'd with the squarewave
# Therefore each YM channel contributes 1/3 of the overall noise volume
# As a result SN percussion adds 25% extra volume in the mix unless we compensate for it.
# This scaler allows that to be balanced
# This can be tweaked if necessary
NOISE_MIX_SCALE = 1.0 / 3.0
# Runtime options (not command line options)
ENABLE_NOISE = True # enables noises to be processed
ENABLE_NOISE_PITCH = True # enables 'nearest match' fixed white noise frequency selection rather than fixed single frequency
ENABLE_ENVELOPE_MIX_HACK = True # wierd oddity fix where tone mix is disabled, but envelopes are enabled - EXPERIMENTAL
OPTIMIZE_VGM = True # outputs delta register updates in the vgm rather than 1:1 register dumps
SAMPLE_RATE = 1 # number of volume frames to process per YM frame (1=50Hz, 2=100Hz, 63=700Hz, 126=6300Hz (GOOD!) 147=7350Hz, 294=14700Hz, 441=22050Hz, 882=44100Hz)
# legacy/non working debug flags
SIM_ENVELOPES = True # set to true to use full volume for envelepe controlled sounds
# For testing only
ENABLE_BIN = False # enable output of a test 'bin' file (ie. the raw SN data file)
#--------------------------------------------------------------------
# Bass frequency processing settings
#--------------------------------------------------------------------
# Since the hardware SN76489 chip is limited to a frequency range determined by its clock rate, and also 10-bit precision,
# so some low-end frequencies cannot be reproduced to match the 12-bit precision YM chip.
#
# To remedy this we have two techniques available:
# 1. Use the periodic noise feature of the SN76489 to 'simulate' these lower frequencies (at the cost of interleaving percussion and some approximation because we can only have one channel playing PN)
# 2. Simulate the low-end frequencies in software by implementing a low frequency square wave using software timers to manipulate attenuation on the tone channels
# Periodic noise based bass settings (default)
ENABLE_BASS_TONES = True # enables low frequency tones to be simulated with periodic noise
ENABLE_BASS_BIAS = True # enables bias to the most active bass channel when more than one low frequency tone is playing at once.
FORCE_BASS_CHANNEL = -1 #-1 # set this to 0,1 or 2 (A/B/C) or -1, to make a specific channel always take the bass frequency. Not an elegant or useful approach.
# Software bass settings (overrides periodic noise bass)
# Enabling this setting will create output register data that is not hardware compliant, so any decoder must interpret the data correctly to synthesize bass frequencies.
# The output VGM file is VGM compatible, but it will not sound correct when played due to the data modifications.
#
# The approach is as follows:
# For any frequency on a tone channel that is below the SN76489 hardware tone frequency range (ie. value > 10-bits)
# We divide the tone register value by 4, store that in the 10-bit output, but set bit 6 in the high byte DATA register.
# The decoder must check for this bit being set and interpet the tone register value as the duty cycle time for a software generated squarewave.
ENABLE_SOFTWARE_BASS = False
if ENABLE_SOFTWARE_BASS:
TONE_RANGE = 4095
ENABLE_BASS_TONES = False
else:
TONE_RANGE = 1023
# R00 = Channel A Pitch LO (8 bits)
# R01 = Channel A Pitch HI (4 bits)
# R02 = Channel B Pitch LO (8 bits)
# R03 = Channel B Pitch HI (4 bits)
# R04 = Channel C Pitch LO (8 bits)
# R05 = Channel C Pitch HI (4 bits)
# R06 = Noise Frequency (5 bits)
# R07 = I/O & Mixer (IOB|IOA|NoiseC|NoiseB|NoiseA|ToneC|ToneB|ToneA)
# R08 = Channel A Level (M | 4 bits) (where M is mode)
# R09 = Channel B Level (M | 4 bits)
# R10 = Channel C Level (M | 4 bits)
# R11 = Envelope Freq LO (8 bits)
# R12 = Envelope Freq HI (8 bits)
# R13 = Envelope Shape (CONT|ATT|ALT|HOLD)
# Chip specs:
# 3 x Squarewave tone oscillators and 1 x Noise generator
# 1 x Envelope driver
# 1 x Mixer
# Pitch oscillation frequency is (Clock / 16 x TP) [TP is tone pitch]
# Noise frequency is (Clock / 16 x NP) [NP is noise pitch R6]
# Noise and/or Tone is output when Mixer flag is set to 0 for a channel
# Mode [M] is 1, then envelope drives volume, when 0, the 4 bit value drives attenuation
# Envelope repetition frequency (fE) is (Clock / 256 x EP) [EP is envelope frequency]
# Envelope shape has 10 valid settings - see data sheet for details
# Envelope Generator
# The envelope generator is a simple 5-bit counter, that can be incremented, decremented, reset or stopped
# Control of it's behaviour is via R13
# The output of the counter drives the attenuation of the output signal (in 5 bit precision rather than 4 bit normally)
# The counter increments once every fE/32
# By calculating the envelope frequency we can determine how fast any software simulation of the waveform would need to be
# Writing to register 13 resets the envelope clock
# r13 has a particular status. If the value stored in the file is 0xff, YM emulator will not reset the waveform position.
# To get envelopes working on an SN chip we'd have to simulate the envelopes
# by reprogramming attenuation registers at the correct frequency
# Note that since the SN only has four bit of precision for volume,
# it is already half the required update frequency
# Effects & Digidrums
# Digidrums are 4-bit samples played on one of the 3 voices
# Information for playback is encoded into the spare bits of the YM register data
# Plus 2 'virtual' registers (14+15)
# See ftp://ftp.modland.com/pub/documents/format_documentation/Atari%20ST%20Sound%20Chip%20Emulator%20YM1-6%20(.ay,%20.ym).txt
# r1 free bits are used to code TS:
# r1 bits b5-b4 is a 2bits code wich means:
#
# 00: No TS.
# 01: TS running on voice A
# 10: TS running on voice B
# 11: TS running on voice C
# r1 bit b6 is only used if there is a TS running. If b6 is set, YM emulator must restart
# the TIMER to first position (you must be VERY sound-chip specialist to hear the difference).
#
#
# r3 free bits are used to code a DD start.
# r3 b5-b4 is a 2bits code wich means:
#
# 00: No DD
# 01: DD starts on voice A
# 10: DD starts on voice B
# 11: DD starts on voice C
# Setup 6522 VIA to tick over a counter at the freq we need
# Then setup an interrupt at whatever period we can afford
# Load VIA counter, look up in envelope table, set volume on channels with envelopes enabled
# Could possibly do the digidrums this way too.
# Setup attenuation mapping tables
# SN attenuates in steps of 2dB, whereas YM steps in 1.5dB steps, so find the nearest volume in this table.
# Not sure this is ideal, because without envelopes, some tunes only use the 4 bit levels, and they dont use all 16-levels of the output SN
# but technically is a better representation? hmm.... revist
# from https://www.smspower.org/Development/SN76489
sn_volume_table= [ 0, 1304, 1642, 2067, 2603, 3277, 4125, 5193, 6568, 8231, 10362, 13045, 16422, 20675, 26028, 32767 ]
# When we average volumes (eg. for noise mixing or envelope sampling) we cant just average the linear value because they represent logarithmic attenuation
# So we use tables to convert between the logarithmic attenuation and the linear output
# YM Attentuation at normalized 1V amplitude according to the datasheet is -0.75 dB per step for 5-bit envelopes and -1.5 dB per step for the 4-bit fixed levels
# map 4 or 5-bit logarithmic volume to 8-bit linear volume
#ym_amplitude_table = [ 0x00, 0x01, 0x02, 0x02, 0x03, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0B, 0x0D, 0x10, 0x13, 0x16, 0x1A, 0x1F, 0x25, 0x2C, 0x34, 0x3D, 0x48, 0x54, 0x63, 0x74, 0x88, 0x9F, 0xBA, 0xD9, 0xFF ]
#sn_amplitude_table = [ 4096, 3254, 2584, 2053, 1631, 1295, 1029, 817, 649, 516, 410, 325, 258, 205, 163, 0 ]
# Taken from: https://github.com/true-grue/ayumi/blob/master/ayumi.c
# However, it doesn't marry with the YM2149 spec sheet, nor with the anecdotal reports that the YM attentuation steps in -1.5dB increments.
# There are also other measurements here (for AY) https://github.com/mamedev/mame/blob/master/src/devices/sound/ay8910.cpp
# It looks like it might just be a difference in output levels between AY and YM
# So, I'm gonna run with the YM datasheet version.
# This may need further consideration if we're importing a YM file originally from a CPC/AY
YM_AMPLITUDE_TABLE_EMU = [
0.0, 0.0,
0.00465400167849, 0.00772106507973,
0.0109559777218, 0.0139620050355,
0.0169985503929, 0.0200198367285,
0.024368657969, 0.029694056611,
0.0350652323186, 0.0403906309606,
0.0485389486534, 0.0583352407111,
0.0680552376593, 0.0777752346075,
0.0925154497597, 0.111085679408,
0.129747463188, 0.148485542077,
0.17666895552, 0.211551079576,
0.246387426566, 0.281101701381,
0.333730067903, 0.400427252613,
0.467383840696, 0.53443198291,
0.635172045472, 0.75800717174,
0.879926756695, 1.0 ]
# calculate datasheet spec YM amplitude table from logs
YM_AMPLITUDE_TABLE_DS = []
for v in range(32):
if v == 0:
a = 0.0
else:
a = math.pow(10, ((-0.75*(31-v))/10) )
#print " Amplitude of volume " + str(v) + " is " + str(a)
a = min(1.0, a)
a = max(0.0, a)
YM_AMPLITUDE_TABLE_DS.append( a )
# use the datasheet table for YM amplitudes
ym_amplitude_table = YM_AMPLITUDE_TABLE_DS
# Define if we'll use table based mapping for YM amplitude calcs, or Logarithmic mapping
USE_YM_AMPLITUDE_TABLE = True
# get the normalized linear (float) amplitude for a 5 bit level
def get_ym_amplitude(v):
if USE_YM_AMPLITUDE_TABLE:
return ym_amplitude_table[v]
else:
if v == 0:
a = 0.0
else:
a = math.pow(10, ((-0.75*(31-v))/10) )
#print " Amplitude of volume " + str(v) + " is " + str(a)
a = min(1.0, a)
a = max(0.0, a)
return a
# given an amplitude, return the nearest 5-bit volume level
def get_ym_volume(amplitude):
if USE_YM_AMPLITUDE_TABLE:
best_dist = 1<<31
index = 0
for n in range(32):
ym_amplitude = ym_amplitude_table[n]
p = amplitude - ym_amplitude
dist = p * p
# we always round to the nearest louder level (so we are never quieter than target level)
if dist < best_dist and ym_amplitude >= amplitude:
best_dist = dist
index = n
return index
else:
if (a == 0.0):
v = 0
else:
v = int( 31 - ( (10*math.log(a, 10)) / -0.75 ) )
#print " Volume of amplitude " + str(a) + " is " + str(v)
if v > 31:
print("RANGE ERROR > 5 bits in get_ym_volume()")
v = min(31, v)
v = max(0, v)
return v
# table to map ym volumes from 5-bit to 4-bit SN volumes
ym_sn_volume_table = []
for n in range(32):
a = int(get_ym_amplitude(n) * 32767.0)
dist = 1<<31
index = 0
for i in range(16):
l = sn_volume_table[i]
p = a - l
d = p * p
# we always round to the nearest louder level (so we are never quieter than target level)
if d < dist and l >= a:
dist = d
index = i
ym_sn_volume_table.append(index)
# get the nearest 4-bit logarithmic volume to the given 5-bit ym_volume
def get_sn_volume(ym_volume):
if ym_volume > 31:
print("RANGE ERROR > 5 bits in get_sn_volume()")
if ENABLE_ATTENUATION:
# use lookup table
return ym_sn_volume_table[ym_volume]
else:
# simple attenuation map, with offset
#print("volA=" + str((min(ym_volume+1, 31) >> 1) & 15) + ", volB=" + str((ym_volume >> 1) & 15))
return (min(ym_volume+1, 31) >> 1) & 15
#return (ym_volume >> 1) & 15
# print the tables
if ENABLE_DEBUG:
print("ym_sn_volume_table:")
print(ym_sn_volume_table)
#def get_ym_amplitude2(v):
# if v == 0:
# a = 0.0
# else:
# a = math.pow(10, ((-0.75*(31-v))/10) )
# #print " Amplitude of volume " + str(v) + " is " + str(a)
# a = min(1.0, a)
# a = max(0.0, a)
# return a
#ym_amplitude_table2 = []
#for n in range(32):
# ym_amplitude_table2.append( get_ym_amplitude2(n) )
print("attenuation tables:")
print("{: >2} {: >20} {: >20} {: >20}".format("level", "ymtable_emu", "logtable_datasheet", "sntable"))
for n in range(32):
print("{: >2} {: >20} {: >20} {: >20}".format(n, YM_AMPLITUDE_TABLE_EMU[n], YM_AMPLITUDE_TABLE_DS[n], sn_volume_table[n>>1]/32767.0))
#---- FAST VERSION
# Class to emulate the YM2149 HW envelope generator
# see http://www.cpcwiki.eu/index.php/Ym2149 for the FPGA logic
class YmEnvelope():
ENV_MASTER_CLOCK = 2000000
ENV_CLOCK_DIVIDER = 8
ENV_CONT = (1<<3)
ENV_ATT = (1<<2)
ENV_ALT = (1<<1)
ENV_HOLD = (1<<0)
#-- envelope shapes
#-- CONT|ATT|ALT|HOLD
#-- 0 0 x x \___
#-- 0 1 x x /___
#-- 1 0 0 0 \\\\
#-- 1 0 0 1 \___
#-- 1 0 1 0 \/\/
#-- ___
#-- 1 0 1 1 \
#-- 1 1 0 0 ////
#-- ___
#-- 1 1 0 1 /
#-- 1 1 1 0 /\/\
#-- 1 1 1 1 /___
# Envelopes can be implemented much faster as lookup tables.
# We use a 64 entry tables set based on the envelope shape (there are 16 possible envelope mode settings)
# with variants as:
# Ramp Up / Hold High
# Ramp Up / Hold Low
# Ramp Down / Hold Low
# Ramp Down / Hold High
# Ramp Up / Ramp Down (Loop)
# Ramp Up / Ramp Up (Loop)
# Ramp Down / Ramp Down (Loop)
#
# Pre-create the 16 shapes as an array of 64 values from 0-31 (output V)
# Select appropriate active table as ETABLE when env shape is set
# ELOOP on, if bit 0 (HOLD) is 1 OR bit 3 (CONT) is 0
# Each update, add N envelope cycles to envelope counter
# if ELOOP: ECNT &= 63 else ECNT = MAX(ECNT,63)
# V = ETABLE[ECNT]
# This approach also opens up the possibility for digi drums, since they are effectively just attentuation tables too (just longer ones!)
def __init__(self):
self.__clock_cnt = 0 # clock cycle counter
# Initialise the envelope shape tables
# YM envelopes are very basic and comprise 4 basic types of waveform ramp(up/down) hold(high/low)
ramp_up = []
ramp_dn = []
hold_hi = []
hold_lo = []
# populate these waveforms as 32 5-bit volume levels, where 0 is silent, 31 is full volume
for x in range(0,32):
ramp_up.append(x)
ramp_dn.append(31-x)
hold_hi.append(31)
hold_lo.append(0)
# Now create each shape by copying two combinations of the above 4 shapes into 16 different 64-value arrays.
self.__envelope_shapes = []
def createShape(phase1, phase2):
temp = []
temp.extend(phase1)
temp.extend(phase2)
self.__envelope_shapes.append( temp )
# shapes 0-3
createShape(ramp_dn, hold_lo)
createShape(ramp_dn, hold_lo)
createShape(ramp_dn, hold_lo)
createShape(ramp_dn, hold_lo)
# shapes 4-7
createShape(ramp_up, hold_lo)
createShape(ramp_up, hold_lo)
createShape(ramp_up, hold_lo)
createShape(ramp_up, hold_lo)
# shapes 8-15
createShape(ramp_dn, ramp_dn)
createShape(ramp_dn, hold_lo)
createShape(ramp_dn, ramp_up)
createShape(ramp_dn, hold_hi)
createShape(ramp_up, ramp_up)
createShape(ramp_up, hold_hi)
createShape(ramp_up, ramp_dn)
createShape(ramp_up, hold_lo)
self.reset()
# reset the chip logic state
def reset(self):
self.__rb = 0 # Envelope frequency, 8-bit fine adjustment
self.__rc = 0 # Envelope frequency, 8-bit rough adjustment
self.__rd = 0 # shape of envelope (CONT|ATT|ALT|HOLD)
self.__env_cnt = 0 # envelope period counter (index into the table)
self.__env_table = [] # current envelope shape table
# initialise shape
self.set_envelope_shape(self.__rd)
# set envelope shape register 13
def set_envelope_shape(self, r):
# stash register setting
self.__rd = r
self.__env_cnt = 0
# load initial shape table
self.__env_table = self.__envelope_shapes[r & 15]
# determine if it is a looped shape / mode
if (r & self.ENV_HOLD) == self.ENV_HOLD or (r & self.ENV_CONT) == 0:
self.__env_hold = True
else:
self.__env_hold = False
# set the current envelope volume
self.__env_volume = self.__env_table[0]
if ENABLE_DEBUG:
print(" ENV: set shape " + str(r) + " hold=" + str(self.__env_hold) + " " + str(self.__env_table) )
# set the YM chip envelope frequency registers
def set_envelope_freq(self, hi, lo):
self.__rb = lo
self.__rc = hi
# get the current YM chip envelope frequency as a 16-bit interval/counter value
def get_envelope_period(self):
return self.__rc * 256 + self.__rb
# get the current 5-bit envelope volume, 0-31
def get_envelope_volume(self):
return self.__env_volume
#return (self.__env_table[self.__env_cnt])# & 31)
# advance the envelope emulator by provided number of clock cycles
def tick(self, clocks):
#print "tick(" + str(clocks) + ")"
# advance the clock cycles
self.__clock_cnt += clocks
# get the currently set envelope frequency and scale up by the clock divider, since we're working in cpu clocks rather than envelope clocks
f = self.get_envelope_period() * self.ENV_CLOCK_DIVIDER
#print " f=" + str(f)
a = get_ym_amplitude( self.__env_table[self.__env_cnt] )
n = 1
if (f > 0):
# the envelope logic runs every ENV_CLOCK_DIVIDER clock cycles
# iterate correct number of envelope periods based on the current envelope frequency
# we average the outputs processed, as a simple low pass filter to compensate for larger values of clocks and create a sampled output volume
# TODO: a better resampling filter
while (self.__clock_cnt >= f):
self.__env_cnt += 1
self.__clock_cnt -= f
# if looping, mask the bottom 6 bits, otherwise clamp at 63
if self.__env_hold:
self.__env_cnt = min(63, self.__env_cnt)
else:
self.__env_cnt &= 63
# increase number of envelope samples
n += 1
# add the envelope volume to the sampled volume
a += get_ym_amplitude( self.__env_table[self.__env_cnt] )
#print " __env_cnt=" + str(self.__env_cnt) + ", __clock_cnt=" + str(self.__clock_cnt)
# output volume is the average volume for the elapsed number of clocks
self.__env_volume = get_ym_volume( a / n )
#print " Finished Tick __env_cnt=" + str(self.__env_cnt)
# perform a check that the logic is working correctly
def test(self):
self.reset()
print('default volume - ' + str(self.get_envelope_volume()))
for m in range(16):
self.reset()
self.set_envelope_shape(m)
self.set_envelope_freq(0,1) # interval of 1
vs = ''
for n in range(128):
v = self.get_envelope_volume() >> 1
vs += format(v, 'x')
self.tick(8)
print('output volume M=' + str(format(m, 'x')) + ' - ' + vs)
#stop
class YmReader(object):
# these are set only when outputting looped files
OUTPUT_LOOP_INTRO = False
OUTPUT_LOOP_SECTION = False
def __init__(self, fd):
# create instance of YM envelope generator
self.__ymenv = YmEnvelope()
#self.__ymenv.test()
print("Parsing YM file...")
self.__fd = fd
self.__filename = fd.name
self.__filesize = os.path.getsize(fd.name)
self.__parse_header()
self.__data = []
if not self.__data:
self.__read_data()
self.__check_eof()
def __parse_extra_infos(self):
if self.__header['id'] == 'YM2!' or self.__header['id'] == 'YM3!' or self.__header['id'] == 'YM3b':
self.__header['song_name'] = self.__filename
self.__header['author_name'] = ''
self.__header['song_comment'] = ''
else:
#print("here")
# YM6!
# Thanks http://stackoverflow.com/questions/32774910/clean-way-to-read-a-null-terminated-c-style-string-from-a-file
#toeof = iter(functools.partial(self.__fd.read, 1), '')
#print("here2")
#def readcstr():
# return ''.join(itertools.takewhile('\0'.__ne__, toeof))
def readcstr():
chars = []
while True:
c = self.__fd.read(1)
if c == b'\x00': #chr(0):
return "".join(chars)
#print(c)
chars.append(c.decode("utf-8") )
#print("here3")
self.__header['song_name'] = readcstr()
#print("here4")
self.__header['author_name'] = readcstr()
#print("here5")
self.__header['song_comment'] = readcstr()
#print("here6")
def __parse_header(self):
# See:
# http://leonard.oxg.free.fr/ymformat.html
# ftp://ftp.modland.com/pub/documents/format_documentation/Atari%20ST%20Sound%20Chip%20Emulator%20YM1-6%20(.ay,%20.ym).txt
# Parse the YM file format identifier first
ym_format = self.__fd.read(4)
ym_format = ym_format.decode("utf-8")
print("YM Format: " + ym_format)
# we support YM2, YM3, YM5 and YM6
d = {}
if ym_format == 'YM2!' or ym_format == 'YM3!' or ym_format == 'YM3b':
print("Version 2")
d['id'] = ym_format
d['check_string'] = 'LeOnArD!'
d['nb_frames'] = int( (self.__filesize-4)/14 )
d['song_attributes'] = 1 # interleaved
d['nb_digidrums'] = 0
d['chip_clock'] = 2000000
d['frames_rate'] = 50
d['loop_frame'] = 0
d['extra_data'] = 0
d['nb_registers'] = 14
else:
if ym_format == 'YM6!' or ym_format == 'YM5!':
# Then parse the rest based on version
ym_header = '> 8s I I H I H I H'
s = self.__fd.read(struct.calcsize(ym_header))
(d['check_string'],
d['nb_frames'],
d['song_attributes'],
d['nb_digidrums'],
d['chip_clock'],
d['frames_rate'],
d['loop_frame'],
d['extra_data'],
) = struct.unpack(ym_header, s)
d['id'] = ym_format
d['nb_registers'] = 16
else:
if ym_format == '!C-l':
print('This is an LHA compressed YM file. Please extract the inner YM file first using 7zip or similar.')
sys.exit()
else:
raise Exception('Unknown or Unsupported file format: ' + ym_format)
# ok, carry on.
#b0: Set if Interleaved data block.
#b1: Set if the digi-drum samples are signed data.
#b2: Set if the digidrum is already in ST 4 bits format.
d['interleaved'] = d['song_attributes'] & 0x01 != 0
d['dd_signed'] = d['song_attributes'] & 0x02 != 0
d['dd_stformat'] = d['song_attributes'] & 0x04 != 0
self.__header = d
if d['interleaved']:
print("YM File is Interleaved format")
# read any DD samples
num_dd = self.__header['nb_digidrums']
if num_dd != 0:
print("Music contains " + str(num_dd) + " digi drum samples")
# info
if d['dd_stformat']:
print(" Samples are 4-bit ST format") # TODO: what does this mean?!
else:
print(" Samples are UNKNOWN FORMAT") # TODO:so what format is it exactly?!
if d['dd_signed']:
print(" Samples are SIGNED")
else:
print(" Samples are UNSIGNED")
for i in range(num_dd):
# skip over the digidrums sample file data section for now
#print self.__fd.tell()
sample_size = struct.unpack('>I', self.__fd.read(4))[0] # get sample size
print("Found DigiDrums sample " + str(i) + ", " + str(sample_size) + " bytes, loading data...")
#print sample_size
#print self.__fd.tell()
#print "sample " + str(i) + " size="+str(sample_size)
self.__fd.seek(sample_size, 1) # skip the sample data (for now)
#print self.__fd.tell()
#raise Exception('Unsupported file format: Digidrums are not supported')
self.__parse_extra_infos()
#print "file offset="
#print self.__fd.tell()
# self.dump_header()
def __read_data_interleaved(self):
#print "__read_data_interleaved"
#print "file offset=" + str(self.__fd.tell())
cnt = self.__header['nb_frames']
regs = []
for i in range( self.__header['nb_registers']):
buf = self.__fd.read(cnt) # bytearray
regs.append(buf)
# support output of just the intro (for tunes with looping sections)
loop_frame = self.__header['loop_frame']
if self.OUTPUT_LOOP_INTRO and loop_frame != 0:
self.__header['nb_frames'] = loop_frame
for i in range(self.__header['nb_registers']):
regs[i] = regs[i][0:loop_frame-1]
if self.OUTPUT_LOOP_SECTION and loop_frame != 0:
self.__header['nb_frames'] = cnt - loop_frame
for i in range(self.__header['nb_registers']):
regs[i] = regs[i][loop_frame:]
if ENABLE_DEBUG:
print(" Loaded " + str(len(regs)) + " register data chunks")
for r in range( self.__header['nb_registers']):
print(" Register " + str(r) + " entries = " + str(len(regs[r])))
#self.__data=[''.join(f) for f in zip(*regs)]
self.__data = regs
#print self.__data
def __read_data(self):
if not self.__header['interleaved']:
raise Exception(
'Unsupported file format: Only interleaved data are supported')
#print "file offset=" + str(self.__fd.tell())
self.__read_data_interleaved()
#print "file offset=" + str(self.__fd.tell())
def __check_eof(self):
if self.__fd.read(4) != 'End!':
print('*Warning* End! marker not found after frames')
def dump_header(self):
for k in ('id','check_string', 'nb_frames', 'nb_registers', 'song_attributes',
'nb_digidrums', 'chip_clock', 'frames_rate', 'loop_frame',
'extra_data', 'song_name', 'author_name', 'song_comment'):
print("{}: {}".format(k, self.__header[k]))
def get_header(self):
return self.__header
def get_data(self):
return self.__data
def write_vgm(self, vgm_filename):
# prepare the YM file parser
clock = self.__header['chip_clock']
cnt = self.__header['nb_frames']
frame_rate = self.__header['frames_rate']
regs = self.__data
digi_drums = self.__header['nb_digidrums']
print("Analysing & Converting YM file...")
# prepare the vgm output
#vgm_filename = "test.vgm"
print(" VGM Processing : Writing output VGM file '" + vgm_filename + "'")
print("---")
vgm_stream = bytearray()
vgm_time = 0
vgm_clock = SN_CLOCK # SN clock speed
# prepare the raw output
raw_stream = bytearray()
# YM has 12 bits of precision
# Lower values correspond to higher frequencies - see http://poi.ribbon.free.fr/tmp/freq2regs.htm
ym_freq_hi = (float(clock) / 16.0) / float(1)
ym_freq_lo = (float(clock) / 16.0) / float(4095)
# SN has 10 bits of precision vs YM's 12 bits
sn_freq_hi = float(vgm_clock) / (2.0 * float(1) * 16.0)
sn_freq_lo = float(vgm_clock) / (2.0 * float(TONE_RANGE) * 16.0)
# SN can generate periodic noise in the lower Hz range
sn_pfreq_hi = float(vgm_clock) / (2.0 * float(1) * 16.0 * float(LFSR_BIT))
sn_pfreq_lo = float(vgm_clock) / (2.0 * float(TONE_RANGE) * 16.0 * float(LFSR_BIT))
print(" YM clock is " + str(clock))
print(" SN clock is " + str(vgm_clock))
print(" YM Tone Frequency range from " + str(ym_freq_lo) + "Hz to " + str(ym_freq_hi) + "Hz")
print(" SN Tone Frequency range from " + str(sn_freq_lo) + "Hz to " + str(sn_freq_hi) + "Hz")
print(" SN Bass Frequency range from " + str(sn_pfreq_lo) + "Hz to " + str(sn_pfreq_hi) + "Hz")
if ENABLE_SOFTWARE_BASS:
print(" + Software Bass is ENABLED")
if ENABLE_BASS_TONES:
print(" + Periodic Noise Bass is ENABLED")
if ENABLE_ENVELOPES:
print(" + Envelope Emulation is ENABLED")
if ENABLE_ATTENUATION:
print(" + Volume Attenuation is ENABLED")
if ENABLE_TUNED_NOISE:
print(" + Tuned White Noise is ENABLED [EXPERIMENTAL]")
def get_register_data(register, frame):
return int(binascii.hexlify(regs[register][frame]), 16)
print("---")
#print get_register_data(0,0)
#print get_register_data(1,0)
# set default volumes at the start of the tune for all channels
if not self.OUTPUT_LOOP_SECTION:
dv = 15 # default volume is 15 (silent)
vgm_stream.extend( struct.pack('B', 0x50) ) # COMMAND
vgm_stream.extend( struct.pack('B', 128+(0<<5)+16+dv) ) # LATCH VOLUME C0
vgm_stream.extend( struct.pack('B', 0x50) ) # COMMAND
vgm_stream.extend( struct.pack('B', 128+(1<<5)+16+dv) ) # LATCH VOLUME C1
vgm_stream.extend( struct.pack('B', 0x50) ) # COMMAND
vgm_stream.extend( struct.pack('B', 128+(2<<5)+16+dv) ) # LATCH VOLUME C2
vgm_stream.extend( struct.pack('B', 0x50) ) # COMMAND
vgm_stream.extend( struct.pack('B', 128+(3<<5)+16+dv) ) # LATCH VOLUME C3 to SILENT
# set periodic noise on channel 3
vgm_stream.extend( struct.pack('B', 0x50) ) # COMMAND
vgm_stream.extend( struct.pack('B', 128 + (3 << 5) + 3) ) # LATCH PERIODIC TONE on channel 3
# stats for tracking frequency ranges within music
ym_tone_a_max = 0
ym_tone_b_max = 0
ym_tone_c_max = 0
ym_tone_a_min = 65536
ym_tone_b_min = 65536
ym_tone_c_min = 65536
# range of noise frequencies
ym_noise_max = 0
ym_noise_min = 63356
# range of digidrum playback frequencies
ym_dd_freq_min = 65536
ym_dd_freq_max = 0
# range of envelope frequencies
ym_env_freq_min = 65536
ym_env_freq_max = 0
# number of frames using envelopes
ym_env_count = 0
# number of notes "lost" to tuned white noise (if ENABLE_TUNED_NOISE flag enabled)
ym_lost_notes = 0
# latch data for optimizing the output vgm to only output changes in register state
# make sure the initial values are guaranteed to get an output on first run
sn_attn_latch = [ -1, -1, -1, -1 ]
sn_tone_latch = [ -1, -1, -1, -1 ]
# Helper functions
def get_register_byte(r):
# some tunes have incorrect data stream lengths, handle that here.
if r < len(regs) and i < self.__header['nb_frames'] and i < len(regs[r]) :
n = regs[r][i]
# handle python2.7/3.x bullshit
if PYTHON_VERSION > 2:
return int(n)
else:
return int(binascii.hexlify(n), 16) # seems to work with Python 2
# can't figure out how to parse these bytes in a way that runs on both 2.7 and 3.x
#try:
# v = int(n)
#except:
# # some conversion shit happened.
# v = int( struct.unpack('B', n)[0] ) #n = n # woteva
#finally:
# return int(binascii.hexlify(n), 16) # seems to work with Python 2
# # return v
# # return int( struct.unpack('B', n)[0] )
else:
print("ERROR: Register out of range - bad sample ID or corrupt file?")
return 0
def get_register_word(r):
return get_register_byte(r) + get_register_byte(r+1)*256
def getregisterflag(data,bit,key0,key1):
if data & (1<<bit):
return key1
else:
return key0
#--------------------------------------------------------------
# return frequency in hz of a given YM tone/noise pitch
#--------------------------------------------------------------
def get_ym_frequency(v):
if v < 1:
v = 1
return clock / (16 * v)
#--------------------------------------------------------------
# given a YM tone period, return the equivalent SN tone register period
#--------------------------------------------------------------
def ym_to_sn(ym_tone, is_periodic = False):
transposed = 0
# Adjust freq scale & baseline range if periodic noise selected
baseline_freq = sn_freq_lo
sn_freq_scale = 1.0
if is_periodic:
sn_freq_scale = float(LFSR_BIT)
baseline_freq = sn_pfreq_lo
# tones should never exceed 12-bit range
# but some YM files encode extra info
# into the top 4 bits
if ym_tone > 4095:
print(" ERROR: tone data ("+str(ym_tone)+") is out of range (0-4095)")
ym_tone = ym_tone & 4095
# If the tone is 0, it's probably because
# there's a digidrum being played on this voice
if ym_tone == 0:
if ENABLE_VERBOSE:
print(" ERROR: ym tone is 0")
ym_freq = 0
target_freq = 0
else:
ym_freq = (float(clock) / 16.0) / float(ym_tone)
# if the frequency goes below the range
# of the SN capabilities, add an octave
target_freq = ym_freq
while target_freq < baseline_freq:
#if ENABLE_DEBUG: