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: whitequark/glasgow
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 7af5478544d4
Choose a base ref
...
head repository: whitequark/glasgow
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: e99751447a42
Choose a head ref
  • 3 commits
  • 3 files changed
  • 1 contributor

Commits on Feb 25, 2019

  1. applet.yamaha_opl: try to enable missing features.

    This may (or may not) work around some VGM bugs.
    whitequark committed Feb 25, 2019
    Copy the full SHA
    83dbf5e View commit details
  2. Copy the full SHA
    0a8722a View commit details
  3. Copy the full SHA
    e997514 View commit details
Showing with 52 additions and 38 deletions.
  1. +25 −12 software/glasgow/applet/yamaha_opl/__init__.py
  2. +25 −26 software/glasgow/applet/yamaha_opl/index.html
  3. +2 −0 software/glasgow/protocol/vgm.py
37 changes: 25 additions & 12 deletions software/glasgow/applet/yamaha_opl/__init__.py
Original file line number Diff line number Diff line change
@@ -373,15 +373,23 @@ def _check_level(self, feature, feature_level):
self._log("client uses feature [%#04x] with level %d, but only level %d is enabled",
feature, feature_level, self._feature_level,
level=logging.WARN)
self._log("retrying with level %d enabled",
feature_level,
level=logging.WARN)
return True
return False

def _check_enable_features(self, address, data):
async def _check_enable_features(self, address, data):
# YM3812 specific
if address == 0x01 and data & 0x20:
self._enable_level(2)
if address in range(0xe0, 0xf7):
self._check_level(address, 2)
if self._check_level(address, 2):
await self.write_register(0x01, 0x20)

async def write_register(self, address, data, check_feature=True):
if check_feature:
await self._check_enable_features(address, data)
if self._instant_writes:
old_phase_accum = self._phase_accum
self._phase_accum += self.write_clocks
@@ -390,8 +398,6 @@ async def write_register(self, address, data, check_feature=True):
else:
self._log("write [%#04x]=%#04x",
address, data)
if check_feature:
self._check_enable_features(address, data)
await self.lower.write([OP_WRITE|0, address, OP_WRITE|1, data])

async def wait_clocks(self, count):
@@ -420,11 +426,11 @@ async def read_samples(self, count, hint=0):


class YamahaVGMStreamPlayer(VGMStreamPlayer):
def __init__(self, reader, opl_iface):
def __init__(self, reader, opl_iface, clock_rate):
self._reader = reader
self._opl_iface = opl_iface

self.clock_rate = reader.ym3812_clk
self.clock_rate = clock_rate
self.sample_time = opl_iface.sample_clocks / self.clock_rate

async def play(self, disable=True):
@@ -457,6 +463,9 @@ async def queue_samples(count):

await queue.put(b"")

async def ym3526_write(self, address, data):
await self._opl_iface.write_register(address, data)

async def ym3812_write(self, address, data):
await self._opl_iface.write_register(address, data)

@@ -529,15 +538,19 @@ async def serve_vgm(self, request):

self._logger.info("web: %s: VGM has commands for %s",
digest, ", ".join(vgm_reader.chips()))
if vgm_reader.ym3812_clk == 0:
raise ValueError("VGM file does not contain commands for YM3812")
if vgm_reader.ym3812_clk & 0xc0000000:
raise ValueError("VGM file uses unsupported YM3812 configuration")
if len(vgm_reader.chips()) != 1:
raise ValueError("VGM file contains commands for more than one chip")

clock_rate = vgm_reader.ym3526_clk or vgm_reader.ym3812_clk
if clock_rate == 0:
raise ValueError("VGM file does not contain commands for YM3526 or YM3812")
if clock_rate & 0xc0000000:
raise ValueError("VGM file uses unsupported chip configuration")

self._logger.info("web: %s: VGM is looped for %.2f/%.2f s",
digest, vgm_reader.loop_seconds, vgm_reader.total_seconds)

vgm_player = YamahaVGMStreamPlayer(vgm_reader, self._opl_iface)
vgm_player = YamahaVGMStreamPlayer(vgm_reader, self._opl_iface, clock_rate)
except ValueError as e:
self._logger.warning("web: %s: broken upload: %s",
digest, str(e))
@@ -720,7 +733,7 @@ async def interact(self, device, args, opl_iface):
self.logger.warning("VGM file contains commands for %s, which will be ignored"
.format(", ".join(vgm_reader.chips())))

vgm_player = YamahaVGMStreamPlayer(vgm_reader, opl_iface)
vgm_player = YamahaVGMStreamPlayer(vgm_reader, opl_iface, vgm_reader.ym3812_clk)
self.logger.info("recording at sample rate %d Hz", 1 / vgm_player.sample_time)

async def write_pcm(input_queue):
51 changes: 25 additions & 26 deletions software/glasgow/applet/yamaha_opl/index.html
Original file line number Diff line number Diff line change
@@ -5,7 +5,8 @@
<body style="width:50em">
<h1>Yamaha OPL* Web Gateway</h1>
<p>This webpage lets you submit commands to a real Yamaha YM3812 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>Play a <a href="https://vgmrips.net/packs/chip/ym3812">VGM/VGZ</a> file: <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>Supported chips: <a href="https://vgmrips.net/packs/chip/ym3526">YM3526</a>, <a href="https://vgmrips.net/packs/chip/ym3812">YM3812</a>.</p>
<p>Play a VGM/VGZ file: <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="netStatus">idle</span>, <span id="playStatus">stopped</span></p>
<p id="errorPane" style="color:red; display:none">Error: <span id="error"></span></p>

@@ -27,11 +28,10 @@ <h1>Yamaha OPL* Web Gateway</h1>
this._context = new (window.AudioContext || window.webkitAudioContext);
this._buffers = [];

this._buffersDone = 0;
this._buffersTotal = 0;
this._queueStart = 0;
this._queueEnd = 0;
this._queueSize = 0;

this._playing = 0;
this._index = 0;
this._timestamp = this._context.currentTime;

this.preferredSampleRate = function() {
@@ -46,18 +46,17 @@ <h1>Yamaha OPL* Web Gateway</h1>
f32Buffer[i] = (dataView.getUint16(i * 2, /*littleEndian=*/true) - 32768) / 32768;

this._buffers.push(audioBuffer);
this._buffersTotal++;
this._updateStatus();
}

this.scheduleAtLeast = function(atLeast) {
if(this._buffers.length - this._index < atLeast) {
if(this._buffers.length - this._queueEnd < atLeast) {
console.log("need at least", atLeast, "buffers;",
"have ", this._buffers.length - this._index);
"have ", this._buffers.length - this._queueEnd);
return;
}
if(this._playing >= atLeast) return false;
var scheduleCount = atLeast - this._playing;
if(this._queueSize >= atLeast) return false;
var scheduleCount = atLeast - this._queueSize;
console.log("scheduling", scheduleCount, "buffers");
for(var i = 0; i < scheduleCount; i++)
this._scheduleNode();
@@ -67,17 +66,17 @@ <h1>Yamaha OPL* Web Gateway</h1>
this._scheduleNode = function(first) {
var skipTo = 0;
var skipPos = 0;
if(this._index == this._buffers.length) {
if(this._queueEnd == this._buffers.length) {
if(this.complete && this.loop) {
console.log("looping");
this._index = 0;
this._queueEnd = 0;
skipTo = this.loopSkipTo;
} else return;
}

while(true) {
var bufferOffset = 0;
var bufferIndex = this._index++;
var bufferIndex = this._queueEnd++;
var audioBuffer = this._buffers[bufferIndex];
if(skipTo > 0 && skipPos < skipTo) {
if(skipPos + audioBuffer.length < skipTo) {
@@ -96,28 +95,28 @@ <h1>Yamaha OPL* Web Gateway</h1>
var audioBufferSource = this._context.createBufferSource();
audioBufferSource.buffer = audioBuffer;
audioBufferSource.connect(this._context.destination);
this._playing++;
this._queueSize++;
audioBufferSource.start(this._timestamp, bufferOffset);
console.log("scheduled buffer", bufferIndex, "at", this._timestamp, ";",
"now", this._playing);
"now", this._queueSize);

if(this._timestamp < this._context.currentTime)
this._timestamp = this._context.currentTime;
this._timestamp += audioBuffer.duration;

var player = this;
audioBufferSource.onended = function(event) {
player._playing--;
player._buffersDone = bufferIndex;
player._queueSize--;
player._queueStart = bufferIndex;
console.log("finished buffer", bufferIndex, ";",
"now", player._playing);
if(this.complete && bufferIndex == player._buffers.length - 1) {
"now", player._queueSize);
if(player.complete && bufferIndex == player._buffers.length - 1) {
// Chromium appears to not invoke onended for some audio buffer sources for unknown
// reasons. This appears to happen only when the console is closed (?!) and I think
// when the page isn't in focus, and if we don't account for this, the player will
// hang and require a page reload.
if(!player.loop) {
player._playing = 0;
player._queueSize = 0;
}
}
player._updateStatus();
@@ -126,20 +125,20 @@ <h1>Yamaha OPL* Web Gateway</h1>
};

this.rewind = function() {
if(this._playing) return;
if(this._queueSize) return;

this._buffersDone = 0;
this._index = 0;
this._queueStart = 0;
this._queueEnd = 0;
this._timestamp = this._context.currentTime;
}

this.onstatuschange = undefined;
this._updateStatus = function() {
if(this.onstatuschange)
this.onstatuschange({
playing: this._playing,
done: this._buffersDone,
total: this._buffersTotal
playing: !!this._queueSize,
done: this._queueStart,
total: this._buffers.length,
});
}
}
2 changes: 2 additions & 0 deletions software/glasgow/protocol/vgm.py
Original file line number Diff line number Diff line change
@@ -153,6 +153,8 @@ async def parse_data(self, player):
command = self._read0("B")
if command == 0x5A:
await player.ym3812_write(*self._read("BB"))
elif command == 0x5B:
await player.ym3526_write(*self._read("BB"))
elif command == 0x61:
samples = self._read0("<H")
await player.wait_seconds(samples / SAMPLE_RATE)