Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 0635d2b

Browse files
committedMar 18, 2019
WIP: applet.audio.yamaha_opl: YMF262 support.
1 parent 2f1743a commit 0635d2b

File tree

2 files changed

+137
-36
lines changed

2 files changed

+137
-36
lines changed
 

‎software/glasgow/applet/audio/yamaha_opl/__init__.py

+134-35
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,6 @@
4545
# is as follows, in a Verilog-like syntax:
4646
# assign V = {S, {{7{~S}}, M, 7'b0000000}[E+:15]};
4747
#
48-
# Compatibility modes
49-
# -------------------
50-
#
51-
# Yamaha chips that have compatibility features implement them in a somewhat broken way. When
52-
# the compatibility feature is disabled (e.g. bit 5 of 0x01 TEST for YM3812), its registers are
53-
# masked off. However, the actual feature is still (partially) enabled and it will result in
54-
# broken playback if this is not accounted for. Therefore, for example, the reset sequence has to
55-
# enable all available advanced features, zero out the registers, and then disable them back for
56-
# compatibility with OPL clients that expect the compatibility mode to be on.
57-
#
5848
# Bus cycles
5949
# ----------
6050
#
@@ -63,6 +53,20 @@
6353
# wait states. Reads are referenced to ~RD falling edge, and always read exactly one register,
6454
# although there might be undocumented registers elsewhere.
6555
#
56+
# On some chips (e.g OPL4) the register that can be read has a busy bit, which seems to be always
57+
# the LSB. On many others (e.g. OPL3) there is no busy bit. Whether the busy bit is available
58+
# on any given silicon seems pretty arbitrary.
59+
#
60+
# Register compatibility
61+
# ----------------------
62+
#
63+
# Yamaha chips that have compatibility features implement them in a somewhat broken way. When
64+
# the compatibility feature is disabled (e.g. bit 5 of 0x01 TEST for YM3812), its registers are
65+
# masked off. However, the actual feature is still (partially) enabled and it will result in
66+
# broken playback if this is not accounted for. Therefore, for example, the reset sequence has to
67+
# enable all available advanced features, zero out the registers, and then disable them back for
68+
# compatibility with OPL clients that expect the compatibility mode to be on.
69+
#
6670
# Register latency
6771
# ----------------
6872
#
@@ -74,9 +78,18 @@
7478
# On YM3812, the latch latency is 12 cycles and the sample takes 72 clocks, therefore each
7579
# address/data write cycle takes 12+12+72 clocks.
7680
#
77-
# On some chips (e.g OPL4) the register that can be read has a busy bit, which seems to be always
78-
# the LSB. On many others (e.g. OPL3) there is no busy bit. Whether the busy bit is available
79-
# on any given silicon seems pretty arbitrary.
81+
# Timing compatibility
82+
# --------------------
83+
#
84+
# When OPL3, functions in the OPL3 mode (NEW=1), the address and data latency are the declared
85+
# values, i.e. 32 and 32 master clock cycles. However, in OPL/OPL2 mode, OPL3 uses completely
86+
# different timings. It is not clear what they are, but 32*4/32*4 is not enough (and lead to missed
87+
# writes), whereas 36*4/36*4 seems to work fine. This is never mentioned in any documentation.
88+
#
89+
# Although it is not mentioned anywhere, it is generally understood that OPL3 in compatibility
90+
# mode (NEW=0) is attempting to emulate two independent OPL2's present on the first release
91+
# of Sound Blaster PRO, which could be the cause of the bizarre timings. See the following link:
92+
# https://www.msx.org/forum/msx-talk/software/vgmplay-msx?page=29
8093
#
8194
# VGM timeline
8295
# ------------
@@ -93,6 +106,27 @@
93106
# synchronous digital logic, that doesn't generally affect the output until it breaks.
94107
#
95108
# * YM3812 stops working between 15 MHz (good) and 30 MHz (bad).
109+
#
110+
# Test cases
111+
# ----------
112+
#
113+
# Good test cases that stress the various timings and interfaces are:
114+
# * (YM3526) https://vgmrips.net/packs/pack/chelnov-atomic-runner-karnov track 03
115+
# Good general-purpose OPL test.
116+
# * (YM3812) https://vgmrips.net/packs/pack/ultima-vi-the-false-prohpet-ibm-pc-xt-at track 01
117+
# This track makes missing notes (due to timing mismatches) extremely noticeable.
118+
# * (YM3812) https://vgmrips.net/packs/pack/lemmings-dos
119+
# This pack does very few commands at a time and doesn't have software vibrato, so if commands
120+
# go missing, notes go out of tune.
121+
# * (YM3812) https://vgmrips.net/packs/pack/zero-wing-toaplan-1 track 02
122+
# Good general-purpose OPL2 test, exhibits serious glitches if the OPL2 isn't reset correctly
123+
# or if the LSI TEST register handling is broken.
124+
# * (YM3812) https://vgmrips.net/packs/pack/vimana-toaplan-1 track 02
125+
# This is an OPL2 track but the music is written for OPL and in fact the VGM file disables
126+
# WAVE SELECT as one of the first commands. Implementation bugs tend to silence drums,
127+
# which is easily noticeable but only if you listen to the reference first.
128+
# * (YMF262) https://vgmrips.net/packs/pack/touhou-eiyashou-imperishable-night-ibm-pc-at track 18
129+
# Good general-purpose OPL3 test.
96130

97131
from abc import ABCMeta, abstractmethod
98132
import os.path
@@ -167,7 +201,7 @@ def __init__(self, pads):
167201
clk_sy_r = Signal()
168202
self.sync += [
169203
clk_sy_r.eq(clk_sy_s),
170-
self.stb_sy.eq(~clk_sy_r & clk_sy_s)
204+
self.stb_sy.eq(clk_sy_r & ~clk_sy_s)
171205
]
172206

173207
sh_r = Signal()
@@ -285,18 +319,24 @@ def __init__(self, pads, in_fifo, out_fifo,
285319
xfer_o = Signal(16)
286320
self.comb += [
287321
# FIXME: this is uglier than necessary because of Migen bugs. Rewrite nicer in nMigen.
288-
xfer_o.eq(Cat((Cat(xfer_i.m, Replicate(~xfer_i.s, 7)) << xfer_i.e)[1:16], xfer_i.s))
322+
# xfer_o.eq(Cat((Cat(xfer_i.m, Replicate(~xfer_i.s, 7)) << xfer_i.e)[1:16], xfer_i.s))
323+
xfer_o.eq(xfer_i.raw_bits())
289324
]
290325

291-
data_r = Signal(16)
292-
data_l = Signal(16)
326+
data_r = Signal(17)
327+
data_l = Signal(17)
293328
self.sync += If(dac_bus.stb_sy, data_r.eq(Cat(data_r[1:], dac_bus.mo)))
294-
self.comb += xfer_i.raw_bits().eq(data_l)
329+
self.comb += xfer_i.raw_bits().eq(data_l[0:])
295330

296331
self.submodules.data_fsm = FSM()
297332
self.data_fsm.act("WAIT-SH",
298333
NextValue(in_fifo.flush, ~enabled),
299334
If(dac_bus.stb_sh & enabled,
335+
NextState("WAIT-SY")
336+
)
337+
)
338+
self.data_fsm.act("WAIT-SY",
339+
If(dac_bus.stb_sy,
300340
NextState("SAMPLE")
301341
)
302342
)
@@ -406,8 +446,8 @@ def _check_level(self, feature, feature_level):
406446

407447
async def _check_enable_features(self, address, data):
408448
if address not in self._registers:
409-
self._log("client uses undefined feature [%#04x]",
410-
address,
449+
self._log("client uses undefined feature [%#04x]=%#04x",
450+
address, data,
411451
level=logging.WARN)
412452

413453
async def write_register(self, address, data, check_feature=True):
@@ -451,10 +491,10 @@ async def read_samples(self, count, hint=0):
451491

452492

453493
class YamahaOPLInterface(YamahaOPxInterface):
454-
chips = ["YM3526 (OPL)"]
494+
chips = ["YM3526/OPL"]
455495

456496
def get_vgm_clock_rate(self, vgm_reader):
457-
return vgm_reader.ym3526_clk
497+
return vgm_reader.ym3526_clk, 1
458498

459499
address_clocks = 12
460500
data_clocks = 84
@@ -473,10 +513,13 @@ async def _check_enable_features(self, address, data):
473513

474514

475515
class YamahaOPL2Interface(YamahaOPLInterface):
476-
chips = YamahaOPLInterface.chips + ["YM3812 (OPL2)"]
516+
chips = YamahaOPLInterface.chips + ["YM3812/OPL2"]
477517

478518
def get_vgm_clock_rate(self, vgm_reader):
479-
return vgm_reader.ym3812_clk or YamahaOPLInterface.get_vgm_clock_rate(self, vgm_reader)
519+
if vgm_reader.ym3812_clk:
520+
return vgm_reader.ym3812_clk, 1
521+
else:
522+
return YamahaOPLInterface.get_vgm_clock_rate(self, vgm_reader)
480523

481524
_registers = YamahaOPLInterface._registers + [
482525
*range(0xE0, 0xF6)
@@ -489,15 +532,61 @@ async def _use_lowest_level(self):
489532
await self.write_register(0x01, 0x00, check_feature=False)
490533

491534
async def _check_enable_features(self, address, data):
492-
if address == 0x01 and data & 0x20:
493-
self._enable_level(2)
535+
if address == 0x01 and data in (0x00, 0x20):
536+
if data & 0x20:
537+
self._enable_level(2)
494538
elif address in range(0xE0, 0xF6):
495539
if self._check_level(address, 2):
496540
await self.write_register(0x01, 0x20)
497541
else:
498542
await super()._check_enable_features(address, data)
499543

500544

545+
class YamahaOPL3Interface(YamahaOPL2Interface):
546+
chips = [chip + " (no CSM)" for chip in YamahaOPL2Interface.chips] + ["YMF262/OPL3"]
547+
548+
def get_vgm_clock_rate(self, vgm_reader):
549+
if vgm_reader.ymf262_clk:
550+
return vgm_reader.ymf262_clk, 1
551+
else:
552+
ym3812_clk, _ = YamahaOPL2Interface.get_vgm_clock_rate(self, vgm_reader)
553+
return ym3812_clk, 4
554+
555+
# The datasheet says use 32 master clock cycle latency. That's a lie, there's a /4 pre-divisor.
556+
# So you'd think 32 * 4 master clock cycles would work. But 32 is also a lie, that doesn't
557+
# result in robust playback. It appears that 36 is the real latency number.
558+
address_clocks = 36 * 4
559+
data_clocks = 36 * 4
560+
sample_clocks = 72 * 4
561+
562+
_registers = YamahaOPL2Interface._registers + [
563+
0x104, *range(0x120, 0x136), *range(0x140, 0x156), *range(0x160, 0x176),
564+
*range(0x180, 0x196), *range(0x1A0, 0x1A9), *range(0x1B0, 0x1B9), *range(0x1C0, 0x1C9),
565+
*range(0x1E0, 0x1F6)
566+
]
567+
568+
async def _use_highest_level(self):
569+
await super()._use_highest_level()
570+
await self.write_register(0x105, 0x01, check_feature=False)
571+
572+
async def _use_lowest_level(self):
573+
await self.write_register(0x105, 0x00, check_feature=False)
574+
await super()._use_lowest_level()
575+
576+
async def _check_enable_features(self, address, data):
577+
if address == 0x08 and data & 0x80:
578+
self._log("client uses deprecated and removed feature [0x08]|0x80",
579+
level=logging.WARN)
580+
elif address == 0x105 and data in (0x00, 0x01):
581+
if data & 0x01:
582+
self._enable_level(3)
583+
elif address in range(0x100, 0x200) and address in self._registers:
584+
if self._check_level(address, 3):
585+
await self.write_register(0x105, 0x01)
586+
else:
587+
await super()._check_enable_features(address, data)
588+
589+
501590
class YamahaVGMStreamPlayer(VGMStreamPlayer):
502591
def __init__(self, reader, opx_iface, clock_rate):
503592
self._reader = reader
@@ -542,6 +631,9 @@ async def ym3526_write(self, address, data):
542631
async def ym3812_write(self, address, data):
543632
await self._opx_iface.write_register(address, data)
544633

634+
async def ymf262_write(self, address, data):
635+
await self._opx_iface.write_register(address, data)
636+
545637
async def wait_seconds(self, delay):
546638
await self._opx_iface.wait_clocks(int(delay * self.clock_rate))
547639

@@ -555,7 +647,8 @@ def __init__(self, logger, opx_iface):
555647
async def serve_index(self, request):
556648
with open(os.path.join(os.path.dirname(__file__), "index.html")) as f:
557649
index_html = f.read()
558-
index_html = index_html.replace("{{chips}}", ", ".join(self._opx_iface.chips))
650+
index_html = index_html.replace("{{chip}}", self._opx_iface.chips[-1])
651+
index_html = index_html.replace("{{compat}}", ", ".join(self._opx_iface.chips))
559652
return web.Response(text=index_html, content_type="text/html")
560653

561654
def _make_resampler(self, actual, preferred):
@@ -613,14 +706,18 @@ async def serve_vgm(self, request):
613706

614707
self._logger.info("web: %s: VGM has commands for %s",
615708
digest, ", ".join(vgm_reader.chips()))
616-
if len(vgm_reader.chips()) != 1:
617-
raise ValueError("VGM file does not contain commands for exactly one chip")
618709

619-
clock_rate = self._opx_iface.get_vgm_clock_rate(vgm_reader)
710+
clock_rate, clock_prescaler = self._opx_iface.get_vgm_clock_rate(vgm_reader)
620711
if clock_rate == 0:
621-
raise ValueError("VGM file does not contain commands for any supported chip")
712+
raise ValueError("VGM file contains commands for {}, which is not a supported chip"
713+
.format(", ".join(vgm_reader.chips())))
622714
if clock_rate & 0xc0000000:
623715
raise ValueError("VGM file uses unsupported chip configuration")
716+
if len(vgm_reader.chips()) != 1:
717+
raise ValueError("VGM file contains commands for {}, but only playback of exactly "
718+
"one chip is supported"
719+
.format(", ".join(vgm_reader.chips())))
720+
clock_rate *= clock_prescaler
624721

625722
self._logger.info("web: %s: VGM is looped for %.2f/%.2f s",
626723
digest, vgm_reader.loop_seconds, vgm_reader.total_seconds)
@@ -768,15 +865,17 @@ def add_build_arguments(cls, parser, access):
768865
access.add_pin_argument(parser, "mo", default=True)
769866

770867
parser.add_argument(
771-
"-d", "--device", metavar="DEVICE", choices=["OPL", "OPL2"], required=True,
772-
help="Synthesizer family")
868+
"-d", "--device", metavar="DEVICE", choices=["OPL", "OPL2", "OPL3"], required=True,
869+
help="synthesizer family")
773870

774871
@staticmethod
775872
def _device_iface_cls(args):
776873
if args.device == "OPL":
777874
return YamahaOPLInterface
778875
if args.device == "OPL2":
779876
return YamahaOPL2Interface
877+
if args.device == "OPL3":
878+
return YamahaOPL3Interface
780879

781880
def build(self, target, args):
782881
device_iface_cls = self._device_iface_cls(args)
@@ -830,12 +929,13 @@ async def interact(self, device, args, opx_iface):
830929
if len(vgm_reader.chips()) != 1:
831930
raise GlasgowAppletError("VGM file does not contain commands for exactly one chip")
832931

833-
clock_rate = opx_iface.get_vgm_clock_rate(vgm_reader)
932+
clock_rate, clock_prescaler = opx_iface.get_vgm_clock_rate(vgm_reader)
834933
if clock_rate == 0:
835934
raise GlasgowAppletError("VGM file does not contain commands for any "
836935
"supported chip")
837936
if clock_rate & 0xc0000000:
838937
raise GlasgowAppletError("VGM file uses unsupported chip configuration")
938+
clock_rate *= clock_prescaler
839939

840940
vgm_player = YamahaVGMStreamPlayer(vgm_reader, opx_iface, clock_rate)
841941
self.logger.info("recording at sample rate %d Hz", 1 / vgm_player.sample_time)
@@ -853,7 +953,6 @@ async def write_pcm(input_queue):
853953
write_fut = asyncio.ensure_future(write_pcm(input_queue))
854954
done, pending = await asyncio.wait([play_fut, record_fut, write_fut],
855955
return_when=asyncio.FIRST_EXCEPTION)
856-
print(done, pending)
857956
for fut in done:
858957
await fut
859958

‎software/glasgow/applet/audio/yamaha_opl/index.html

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
<body style="width:50em">
66
<h1>Yamaha OP* Web Gateway</h1>
77
<p>This webpage lets you submit commands to a real Yamaha synthesizer and listen to the output. The synthesizer is a <b>shared resource</b> (and isn't very fast, although it does not have to run in real time), and everyone who submits a file is in the same queue, so you might have to wait until it's your turn.</p>
8-
<p>Supported chips: {{chips}}.</p>
8+
<p>Connected synthesizer: {{chip}}.</p>
9+
<p>Playback support for: {{compat}}.</p>
910
<p>Play a <a href="https://vgmrips.net/packs/chips">VGM/VGZ file</a>: <input type="file" id="file" accept=".vgm,.vgz"> <input type="button" id="play" value="Play"> <input type="button" id="replay" value="Replay" disabled> <input type="checkbox" id="loop"> <label for="loop">Loop</label></p>
1011
<p>Status: <span id="chipStatus">no chip</span>, <span id="netStatus">idle</span>, <span id="playStatus">stopped</span>.</p>
1112
<p id="errorPane" style="color:red; display:none">Error: <span id="error"></span></p>
@@ -155,6 +156,7 @@ <h1>Yamaha OP* Web Gateway</h1>
155156
playButton.onclick = function(event) {
156157
playButton.disabled = true;
157158
replayButton.disabled = true;
159+
chipStatusSpan.innerText = "no chip";
158160

159161
var player = new PCMPlayer();
160162
window.player = player;

0 commit comments

Comments
 (0)
Please sign in to comment.