Skip to content

Commit

Permalink
WIP: applet.audio.yamaha_opl: YMF262 support.
Browse files Browse the repository at this point in the history
  • Loading branch information
whitequark committed Mar 18, 2019
1 parent 2f1743a commit 0635d2b
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 36 deletions.
169 changes: 134 additions & 35 deletions software/glasgow/applet/audio/yamaha_opl/__init__.py
Expand Up @@ -45,16 +45,6 @@
# is as follows, in a Verilog-like syntax:
# assign V = {S, {{7{~S}}, M, 7'b0000000}[E+:15]};
#
# Compatibility modes
# -------------------
#
# Yamaha chips that have compatibility features implement them in a somewhat broken way. When
# the compatibility feature is disabled (e.g. bit 5 of 0x01 TEST for YM3812), its registers are
# masked off. However, the actual feature is still (partially) enabled and it will result in
# broken playback if this is not accounted for. Therefore, for example, the reset sequence has to
# enable all available advanced features, zero out the registers, and then disable them back for
# compatibility with OPL clients that expect the compatibility mode to be on.
#
# Bus cycles
# ----------
#
Expand All @@ -63,6 +53,20 @@
# wait states. Reads are referenced to ~RD falling edge, and always read exactly one register,
# although there might be undocumented registers elsewhere.
#
# On some chips (e.g OPL4) the register that can be read has a busy bit, which seems to be always
# the LSB. On many others (e.g. OPL3) there is no busy bit. Whether the busy bit is available
# on any given silicon seems pretty arbitrary.
#
# Register compatibility
# ----------------------
#
# Yamaha chips that have compatibility features implement them in a somewhat broken way. When
# the compatibility feature is disabled (e.g. bit 5 of 0x01 TEST for YM3812), its registers are
# masked off. However, the actual feature is still (partially) enabled and it will result in
# broken playback if this is not accounted for. Therefore, for example, the reset sequence has to
# enable all available advanced features, zero out the registers, and then disable them back for
# compatibility with OPL clients that expect the compatibility mode to be on.
#
# Register latency
# ----------------
#
Expand All @@ -74,9 +78,18 @@
# On YM3812, the latch latency is 12 cycles and the sample takes 72 clocks, therefore each
# address/data write cycle takes 12+12+72 clocks.
#
# On some chips (e.g OPL4) the register that can be read has a busy bit, which seems to be always
# the LSB. On many others (e.g. OPL3) there is no busy bit. Whether the busy bit is available
# on any given silicon seems pretty arbitrary.
# Timing compatibility
# --------------------
#
# When OPL3, functions in the OPL3 mode (NEW=1), the address and data latency are the declared
# values, i.e. 32 and 32 master clock cycles. However, in OPL/OPL2 mode, OPL3 uses completely
# different timings. It is not clear what they are, but 32*4/32*4 is not enough (and lead to missed
# writes), whereas 36*4/36*4 seems to work fine. This is never mentioned in any documentation.
#
# Although it is not mentioned anywhere, it is generally understood that OPL3 in compatibility
# mode (NEW=0) is attempting to emulate two independent OPL2's present on the first release
# of Sound Blaster PRO, which could be the cause of the bizarre timings. See the following link:
# https://www.msx.org/forum/msx-talk/software/vgmplay-msx?page=29
#
# VGM timeline
# ------------
Expand All @@ -93,6 +106,27 @@
# synchronous digital logic, that doesn't generally affect the output until it breaks.
#
# * YM3812 stops working between 15 MHz (good) and 30 MHz (bad).
#
# Test cases
# ----------
#
# Good test cases that stress the various timings and interfaces are:
# * (YM3526) https://vgmrips.net/packs/pack/chelnov-atomic-runner-karnov track 03
# Good general-purpose OPL test.
# * (YM3812) https://vgmrips.net/packs/pack/ultima-vi-the-false-prohpet-ibm-pc-xt-at track 01
# This track makes missing notes (due to timing mismatches) extremely noticeable.
# * (YM3812) https://vgmrips.net/packs/pack/lemmings-dos
# This pack does very few commands at a time and doesn't have software vibrato, so if commands
# go missing, notes go out of tune.
# * (YM3812) https://vgmrips.net/packs/pack/zero-wing-toaplan-1 track 02
# Good general-purpose OPL2 test, exhibits serious glitches if the OPL2 isn't reset correctly
# or if the LSI TEST register handling is broken.
# * (YM3812) https://vgmrips.net/packs/pack/vimana-toaplan-1 track 02
# This is an OPL2 track but the music is written for OPL and in fact the VGM file disables
# WAVE SELECT as one of the first commands. Implementation bugs tend to silence drums,
# which is easily noticeable but only if you listen to the reference first.
# * (YMF262) https://vgmrips.net/packs/pack/touhou-eiyashou-imperishable-night-ibm-pc-at track 18
# Good general-purpose OPL3 test.

from abc import ABCMeta, abstractmethod
import os.path
Expand Down Expand Up @@ -167,7 +201,7 @@ def __init__(self, pads):
clk_sy_r = Signal()
self.sync += [
clk_sy_r.eq(clk_sy_s),
self.stb_sy.eq(~clk_sy_r & clk_sy_s)
self.stb_sy.eq(clk_sy_r & ~clk_sy_s)
]

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

data_r = Signal(16)
data_l = Signal(16)
data_r = Signal(17)
data_l = Signal(17)
self.sync += If(dac_bus.stb_sy, data_r.eq(Cat(data_r[1:], dac_bus.mo)))
self.comb += xfer_i.raw_bits().eq(data_l)
self.comb += xfer_i.raw_bits().eq(data_l[0:])

self.submodules.data_fsm = FSM()
self.data_fsm.act("WAIT-SH",
NextValue(in_fifo.flush, ~enabled),
If(dac_bus.stb_sh & enabled,
NextState("WAIT-SY")
)
)
self.data_fsm.act("WAIT-SY",
If(dac_bus.stb_sy,
NextState("SAMPLE")
)
)
Expand Down Expand Up @@ -406,8 +446,8 @@ def _check_level(self, feature, feature_level):

async def _check_enable_features(self, address, data):
if address not in self._registers:
self._log("client uses undefined feature [%#04x]",
address,
self._log("client uses undefined feature [%#04x]=%#04x",
address, data,
level=logging.WARN)

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


class YamahaOPLInterface(YamahaOPxInterface):
chips = ["YM3526 (OPL)"]
chips = ["YM3526/OPL"]

def get_vgm_clock_rate(self, vgm_reader):
return vgm_reader.ym3526_clk
return vgm_reader.ym3526_clk, 1

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


class YamahaOPL2Interface(YamahaOPLInterface):
chips = YamahaOPLInterface.chips + ["YM3812 (OPL2)"]
chips = YamahaOPLInterface.chips + ["YM3812/OPL2"]

def get_vgm_clock_rate(self, vgm_reader):
return vgm_reader.ym3812_clk or YamahaOPLInterface.get_vgm_clock_rate(self, vgm_reader)
if vgm_reader.ym3812_clk:
return vgm_reader.ym3812_clk, 1
else:
return YamahaOPLInterface.get_vgm_clock_rate(self, vgm_reader)

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

async def _check_enable_features(self, address, data):
if address == 0x01 and data & 0x20:
self._enable_level(2)
if address == 0x01 and data in (0x00, 0x20):
if data & 0x20:
self._enable_level(2)
elif address in range(0xE0, 0xF6):
if self._check_level(address, 2):
await self.write_register(0x01, 0x20)
else:
await super()._check_enable_features(address, data)


class YamahaOPL3Interface(YamahaOPL2Interface):
chips = [chip + " (no CSM)" for chip in YamahaOPL2Interface.chips] + ["YMF262/OPL3"]

def get_vgm_clock_rate(self, vgm_reader):
if vgm_reader.ymf262_clk:
return vgm_reader.ymf262_clk, 1
else:
ym3812_clk, _ = YamahaOPL2Interface.get_vgm_clock_rate(self, vgm_reader)
return ym3812_clk, 4

# The datasheet says use 32 master clock cycle latency. That's a lie, there's a /4 pre-divisor.
# So you'd think 32 * 4 master clock cycles would work. But 32 is also a lie, that doesn't
# result in robust playback. It appears that 36 is the real latency number.
address_clocks = 36 * 4
data_clocks = 36 * 4
sample_clocks = 72 * 4

_registers = YamahaOPL2Interface._registers + [
0x104, *range(0x120, 0x136), *range(0x140, 0x156), *range(0x160, 0x176),
*range(0x180, 0x196), *range(0x1A0, 0x1A9), *range(0x1B0, 0x1B9), *range(0x1C0, 0x1C9),
*range(0x1E0, 0x1F6)
]

async def _use_highest_level(self):
await super()._use_highest_level()
await self.write_register(0x105, 0x01, check_feature=False)

async def _use_lowest_level(self):
await self.write_register(0x105, 0x00, check_feature=False)
await super()._use_lowest_level()

async def _check_enable_features(self, address, data):
if address == 0x08 and data & 0x80:
self._log("client uses deprecated and removed feature [0x08]|0x80",
level=logging.WARN)
elif address == 0x105 and data in (0x00, 0x01):
if data & 0x01:
self._enable_level(3)
elif address in range(0x100, 0x200) and address in self._registers:
if self._check_level(address, 3):
await self.write_register(0x105, 0x01)
else:
await super()._check_enable_features(address, data)


class YamahaVGMStreamPlayer(VGMStreamPlayer):
def __init__(self, reader, opx_iface, clock_rate):
self._reader = reader
Expand Down Expand Up @@ -542,6 +631,9 @@ async def ym3526_write(self, address, data):
async def ym3812_write(self, address, data):
await self._opx_iface.write_register(address, data)

async def ymf262_write(self, address, data):
await self._opx_iface.write_register(address, data)

async def wait_seconds(self, delay):
await self._opx_iface.wait_clocks(int(delay * self.clock_rate))

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

def _make_resampler(self, actual, preferred):
Expand Down Expand Up @@ -613,14 +706,18 @@ async def serve_vgm(self, request):

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

clock_rate = self._opx_iface.get_vgm_clock_rate(vgm_reader)
clock_rate, clock_prescaler = self._opx_iface.get_vgm_clock_rate(vgm_reader)
if clock_rate == 0:
raise ValueError("VGM file does not contain commands for any supported chip")
raise ValueError("VGM file contains commands for {}, which is not a supported chip"
.format(", ".join(vgm_reader.chips())))
if clock_rate & 0xc0000000:
raise ValueError("VGM file uses unsupported chip configuration")
if len(vgm_reader.chips()) != 1:
raise ValueError("VGM file contains commands for {}, but only playback of exactly "
"one chip is supported"
.format(", ".join(vgm_reader.chips())))
clock_rate *= clock_prescaler

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

parser.add_argument(
"-d", "--device", metavar="DEVICE", choices=["OPL", "OPL2"], required=True,
help="Synthesizer family")
"-d", "--device", metavar="DEVICE", choices=["OPL", "OPL2", "OPL3"], required=True,
help="synthesizer family")

@staticmethod
def _device_iface_cls(args):
if args.device == "OPL":
return YamahaOPLInterface
if args.device == "OPL2":
return YamahaOPL2Interface
if args.device == "OPL3":
return YamahaOPL3Interface

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

clock_rate = opx_iface.get_vgm_clock_rate(vgm_reader)
clock_rate, clock_prescaler = opx_iface.get_vgm_clock_rate(vgm_reader)
if clock_rate == 0:
raise GlasgowAppletError("VGM file does not contain commands for any "
"supported chip")
if clock_rate & 0xc0000000:
raise GlasgowAppletError("VGM file uses unsupported chip configuration")
clock_rate *= clock_prescaler

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

Expand Down
4 changes: 3 additions & 1 deletion software/glasgow/applet/audio/yamaha_opl/index.html
Expand Up @@ -5,7 +5,8 @@
<body style="width:50em">
<h1>Yamaha OP* Web Gateway</h1>
<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>
<p>Supported chips: {{chips}}.</p>
<p>Connected synthesizer: {{chip}}.</p>
<p>Playback support for: {{compat}}.</p>
<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>
<p>Status: <span id="chipStatus">no chip</span>, <span id="netStatus">idle</span>, <span id="playStatus">stopped</span>.</p>
<p id="errorPane" style="color:red; display:none">Error: <span id="error"></span></p>
Expand Down Expand Up @@ -155,6 +156,7 @@ <h1>Yamaha OP* Web Gateway</h1>
playButton.onclick = function(event) {
playButton.disabled = true;
replayButton.disabled = true;
chipStatusSpan.innerText = "no chip";

var player = new PCMPlayer();
window.player = player;
Expand Down

0 comments on commit 0635d2b

Please sign in to comment.