Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: GlasgowEmbedded/glasgow
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: aed67d3d2e2f
Choose a base ref
...
head repository: GlasgowEmbedded/glasgow
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 465de817212b
Choose a head ref
  • 4 commits
  • 2 files changed
  • 1 contributor

Commits on Sep 30, 2019

  1. Copy the full SHA
    4da5772 View commit details
  2. applet.audio.yamaha_opl: move resampling to client side.

    This gives better results latency-wise, could (in principle; not
    currently implemented) handle context sample rate changing during
    playback (which WebKit can apparently do), and also makes downloaded
    WAV files true to the original PCM data.
    whitequark committed Sep 30, 2019
    Copy the full SHA
    3209982 View commit details
  3. Copy the full SHA
    a5c2c83 View commit details
  4. Copy the full SHA
    465de81 View commit details
Showing with 213 additions and 152 deletions.
  1. +98 −115 software/glasgow/applet/audio/yamaha_opl/__init__.py
  2. +115 −37 software/glasgow/applet/audio/yamaha_opl/index.html
213 changes: 98 additions & 115 deletions software/glasgow/applet/audio/yamaha_opl/__init__.py
Original file line number Diff line number Diff line change
@@ -64,8 +64,13 @@
# 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.
# enable all available advanced features, reset the registers controlling advanced features, and
# then disable them back for compatibility with OPL clients that expect the compatibility mode
# to be on.
#
# Note that not all registers should always be reset to zero. For example, on OPL3, CHA/CHB should
# be reset to 1 with A1=L, or OPL2 compatibility breaks, manifesting as missing percussion. This
# is not documented, and cannot be verified on hardware because these registers cannot be read.
#
# Register latency
# ----------------
@@ -106,6 +111,7 @@
# synchronous digital logic, that doesn't generally affect the output until it breaks.
#
# * YM3812 stops working between 10 MHz (good) and 30 MHz (bad).
# * YMF262 stops working between 24 MHz (good) and 48 MHz (bad).
#
# Test cases
# ----------
@@ -133,11 +139,9 @@
import logging
import argparse
import struct
import array
import asyncio
import aiohttp.web as web
import hashlib
import base64
import gzip
import io
from nmigen.compat import *
@@ -225,7 +229,7 @@ def __init__(self, pads):


class YamahaOPxSubtarget(Module):
def __init__(self, pads, in_fifo, out_fifo, format,
def __init__(self, pads, in_fifo, out_fifo, sample_decoder_cls, channel_count,
master_cyc, read_pulse_cyc, write_pulse_cyc,
address_clocks, data_clocks):
self.submodules.cpu_bus = cpu_bus = YamahaCPUBus(pads, master_cyc)
@@ -310,54 +314,48 @@ def __init__(self, pads, in_fifo, out_fifo, format,

# Audio

data_r = Signal(16)
self.sync += If(dac_bus.stb_sy, data_r.eq(Cat(data_r[1:], dac_bus.mo)))

xfer_o = Signal(16)
if format == "F-3Z-9M-1S-3E":
xfer_i = Record([("z", 3), ("m", 9), ("s", 1), ("e", 3),])
# FIXME: this is uglier than necessary because of Migen bugs. Rewrite nicer in nMigen.
self.comb += xfer_o.eq(Cat((Cat(xfer_i.m, Replicate(~xfer_i.s, 7)) << xfer_i.e)[1:16],
xfer_i.s))
elif format == "U-16":
xfer_i = Record([("d", 16)])
self.comb += xfer_o.eq(xfer_i.d)
else:
assert False
self.submodules.decoder = decoder = sample_decoder_cls()

xfrm_o = Signal(16)
xfrm_i = Signal(16)
self.comb += xfrm_o.eq(xfrm_i + 0x8000)
shreg = Signal(len(decoder.i.raw_bits()) * channel_count)
sample = Signal.like(shreg)
self.sync += [
If(dac_bus.stb_sy,
shreg.eq(Cat(shreg[1:], dac_bus.mo))
)
]

self.submodules.data_fsm = FSM()
channel = Signal(1)
self.data_fsm.act("WAIT-SH",
NextValue(in_fifo.flush, ~enabled),
If(dac_bus.stb_sh & enabled,
NextState("SAMPLE")
)
)
self.data_fsm.act("SAMPLE",
NextValue(xfer_i.raw_bits(), data_r),
NextState("TRANSFORM")
)
self.data_fsm.act("TRANSFORM",
NextValue(xfrm_i, xfer_o),
NextState("SEND-L-BYTE")
NextValue(sample, shreg),
NextValue(channel, 0),
NextState("SEND-CHANNEL")
)
self.data_fsm.act("SEND-L-BYTE",
in_fifo.din.eq(xfrm_o[0:8]),
If(in_fifo.writable,
in_fifo.we.eq(1),
NextState("SEND-H-BYTE")
).Elif(dac_bus.stb_sh,
NextState("OVERFLOW")
)
self.data_fsm.act("SEND-CHANNEL",
NextValue(decoder.i.raw_bits(),
sample.word_select(channel, len(decoder.i.raw_bits()))),
NextState("SEND-BYTE")
)
self.data_fsm.act("SEND-H-BYTE",
in_fifo.din.eq(xfrm_o[8:16]),
byteno = Signal(1)
self.data_fsm.act("SEND-BYTE",
in_fifo.din.eq(decoder.o.word_select(byteno, 8)),
If(in_fifo.writable,
in_fifo.we.eq(1),
NextState("WAIT-SH")
NextValue(byteno, byteno + 1),
If(byteno == 1,
NextValue(channel, channel + 1),
If(channel == channel_count - 1,
NextState("WAIT-SH")
).Else(
NextState("SEND-CHANNEL")
)
)
).Elif(dac_bus.stb_sh,
NextState("OVERFLOW")
)
@@ -388,7 +386,8 @@ def get_vgm_clock_rate(self, vgm_reader):
pass

max_master_hz = abstractproperty()
sample_format = abstractproperty()
sample_decoder = abstractproperty()
channel_count = 1

address_clocks = abstractproperty()
data_clocks = abstractproperty()
@@ -499,7 +498,17 @@ async def wait_clocks(self, count):

async def read_samples(self, count):
self._log("read %d samples", count)
return await self.lower.read(count * 2, flush=False)
return await self.lower.read(count * self.channel_count * 2, flush=False)


class YamahaOPLSampleDecoder(Module):
def __init__(self):
self.i = Record([("z", 3), ("m", 9), ("s", 1), ("e", 3)])
self.o = Signal(16)

self.comb += [
self.o.eq(Cat((Cat(self.i.m, Replicate(~self.i.s, 7)) << self.i.e)[1:16], ~self.i.s))
]


class YamahaOPLInterface(YamahaOPxInterface):
@@ -509,7 +518,8 @@ def get_vgm_clock_rate(self, vgm_reader):
return vgm_reader.ym3526_clk, 1

max_master_hz = 4.0e6 # 2.0/3.58/4.0
sample_format = "F-3Z-9M-1S-3E"
sample_decoder = YamahaOPLSampleDecoder
channel_count = 1

address_clocks = 12
data_clocks = 84
@@ -557,6 +567,17 @@ async def _check_enable_features(self, address, data):
await super()._check_enable_features(address, data)


class YamahaOPL3SampleDecoder(Module):
def __init__(self):
# There are 2 dummy clocks between each sample. The DAC doesn't rely on it (it uses two
# phases for two channels per DAC, and a clever arrangement to provide four channels
# without requiring four phases), but we want to save pins and so we do.
self.i = Record([("z", 2), ("d", 16)])
self.o = Signal(16)

self.comb += self.o.eq(self.i.d + 0x8000)


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

@@ -568,7 +589,8 @@ def get_vgm_clock_rate(self, vgm_reader):
return ym3812_clk, 4

max_master_hz = 16.0e6 # 10.0/14.32/16.0
sample_format = "U-16"
sample_decoder = YamahaOPL3SampleDecoder
channel_count = 2 # OPL3 has 4 channels, but we support only 2

# 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
@@ -665,41 +687,6 @@ async def serve_index(self, request):
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):
import numpy

try:
import samplerate
except ImportError as e:
self._logger.warning("samplerate not installed; expect glitches during playback")
async def resample(input_queue, output_queue):
while True:
input_data = await input_queue.get()
await output_queue.put(input_data)
if not input_data:
break
return resample, actual

resampler = samplerate.Resampler()
def resample_worker(input_data, end):
input_array = numpy.frombuffer(input_data, dtype="<i2")
input_array = input_array.astype(numpy.float32) / 32768
output_array = resampler.process(
input_array, ratio=preferred / actual, end_of_input=end)
output_array = (output_array * 32768).astype(numpy.int16)
return output_array.tobytes()
async def resample(input_queue, output_queue):
while True:
input_data = await input_queue.get()
output_data = await asyncio.get_running_loop().run_in_executor(None,
resample_worker, input_data, not input_data)
if output_data:
await output_queue.put(output_data)
if not input_data:
await output_queue.put(b"")
break
return resample, preferred

async def serve_vgm(self, request):
sock = web.WebSocketResponse()
await sock.prepare(request)
@@ -746,13 +733,11 @@ async def serve_vgm(self, request):
except ValueError as e:
self._logger.warning("web: %s: broken upload: %s",
digest, str(e))
return web.Response(status=400, text=str(e), content_type="text/plain")
await sock.close(code=1001, message=str(e))
return sock

input_rate = 1 / vgm_player.sample_time
preferred_rate = int(headers["Preferred-Sample-Rate"])
resample, output_rate = self._make_resampler(input_rate, preferred_rate)
self._logger.info("web: %s: sample rate: input %d, preferred %d, output %d",
digest, input_rate, preferred_rate, output_rate)
sample_rate = 1 / vgm_player.sample_time
self._logger.info("web: %s: sample rate %d", digest, sample_rate)

async with self._lock:
try:
@@ -767,48 +752,51 @@ async def serve_vgm(self, request):
self._logger.info("web: %s: start streaming", digest)

await self._opx_iface.reset()
# Soft reset does not clear all the state immediately, so wait a bit to make sure
# all notes decay, etc.
await vgm_player.wait_seconds(1)

input_queue = asyncio.Queue()
resample_queue = asyncio.Queue()
resample_fut = asyncio.ensure_future(resample(input_queue, resample_queue))
record_fut = asyncio.ensure_future(vgm_player.record(input_queue))
play_fut = asyncio.ensure_future(vgm_player.play())
sample_queue = asyncio.Queue()
record_fut = asyncio.ensure_future(vgm_player.record(sample_queue))
play_fut = asyncio.ensure_future(vgm_player.play())

try:
total_samples = int(vgm_reader.total_seconds * output_rate)
total_samples = int(vgm_reader.total_seconds * sample_rate)
if vgm_reader.loop_samples in (0, vgm_reader.total_samples):
# Either 0 or the entire VGM here means we'll loop the complete track.
loop_skip_to = 0
else:
loop_skip_to = int((vgm_reader.total_seconds -
vgm_reader.loop_seconds) * output_rate)
loop_skip_to = int((vgm_reader.total_seconds - vgm_reader.loop_seconds)
* sample_rate)
await sock.send_json({
"Chip": vgm_reader.chips()[0],
"Sample-Rate": output_rate,
"Channel-Count": self._opx_iface.channel_count,
"Sample-Rate": sample_rate,
"Total-Samples": total_samples,
"Loop-Skip-To": loop_skip_to,
})

while True:
if play_fut.done() and play_fut.exception():
break
elif resample_fut.done():

samples = await sample_queue.get()
if not samples:
break
else:
await sock.send_bytes(await resample_queue.get())
await sock.send_bytes(samples)

for fut in [play_fut, record_fut, resample_fut]:
for fut in [play_fut, record_fut]:
await fut

await sock.close()
self._logger.info("web: %s: done streaming",
digest)
await sock.close()

except asyncio.CancelledError:
self._logger.info("web: %s: cancel streaming",
digest)

for fut in [play_fut, record_fut, resample_fut]:
for fut in [play_fut, record_fut]:
if not fut.done():
fut.cancel()
raise
@@ -857,13 +845,6 @@ class AudioYamahaOPLApplet(GlasgowApplet, name="audio-yamaha-opl"):
the master clock frequency specified in the input file. E.g. using SoX:
$ play -r 49715 output.u16
For the web interface, the browser dictates the sample rate. Streaming at the sample rate other
than the one requested by the browser is possible, but degrades quality. This interface also
has additional Python dependencies:
* numpy (mandatory)
* samplerate (optional, required for best possible quality)
* aiohttp_remotes (optional, required for improved logging)
"""

__pin_sets = ("d", "a")
@@ -908,7 +889,8 @@ def build(self, target, args):
pads=iface.get_pads(args, pins=self.__pins, pin_sets=self.__pin_sets),
out_fifo=iface.get_out_fifo(),
in_fifo=iface.get_in_fifo(auto_flush=False),
format=device_iface_cls.sample_format,
sample_decoder_cls=device_iface_cls.sample_decoder,
channel_count=device_iface_cls.channel_count,
master_cyc=self.derive_clock(
input_hz=target.sys_clk_freq,
output_hz=device_iface_cls.max_master_hz * args.overclock),
@@ -964,19 +946,20 @@ async def interact(self, device, args, opx_iface):
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)
self.logger.info("recording %d channels at sample rate %d Hz",
opx_iface.channel_count, 1 / vgm_player.sample_time)

async def write_pcm(input_queue):
async def write_pcm(sample_queue):
while True:
input_chunk = await input_queue.get()
if not input_chunk:
samples = await sample_queue.get()
if not samples:
break
args.pcm_file.write(input_chunk)
args.pcm_file.write(samples)

input_queue = asyncio.Queue()
sample_queue = asyncio.Queue()
play_fut = asyncio.ensure_future(vgm_player.play())
record_fut = asyncio.ensure_future(vgm_player.record(input_queue))
write_fut = asyncio.ensure_future(write_pcm(input_queue))
record_fut = asyncio.ensure_future(vgm_player.record(sample_queue))
write_fut = asyncio.ensure_future(write_pcm(sample_queue))
done, pending = await asyncio.wait([play_fut, record_fut, write_fut],
return_when=asyncio.FIRST_EXCEPTION)
for fut in done:
Loading