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

Commits on Sep 29, 2019

  1. applet.audio.yamaha_opl: rewrite player to use WebSockets.

    This simplifies the code considerably and reduces overhead, as well
    as allowing easier expansion of control/data in the future.
    whitequark committed Sep 29, 2019
    Copy the full SHA
    f8ed49b View commit details
  2. applet.audio.yamaha_opl: set CHA/CHB=1 in $C0..C8 in OPL3 during reset.

    Otherwise OPL3 in OPL2 mode does something really confusing. Although
    it looks as if it should not be generating any sound, it generates
    everything except percussion.
    whitequark committed Sep 29, 2019
    Copy the full SHA
    a04b4d8 View commit details
Showing with 85 additions and 99 deletions.
  1. +34 −38 software/glasgow/applet/audio/yamaha_opl/__init__.py
  2. +51 −61 software/glasgow/applet/audio/yamaha_opl/index.html
72 changes: 34 additions & 38 deletions software/glasgow/applet/audio/yamaha_opl/__init__.py
Original file line number Diff line number Diff line change
@@ -589,6 +589,8 @@ async def _use_highest_level(self):

async def _use_lowest_level(self):
await self.write_register(0x105, 0x00, check_feature=False)
for address in range(0xC0, 0xC9):
await self.write_register(address, 0x30, check_feature=False)
await super()._use_lowest_level()

async def _check_enable_features(self, address, data):
@@ -624,7 +626,7 @@ async def play(self):
await self._opx_iface.wait_clocks(self.clock_rate)
await self._opx_iface.disable()

async def record(self, queue, chunk_count=8192):
async def record(self, queue, chunk_count=2048):
total_count = int(self._reader.total_seconds / self.sample_time)
done_count = 0
while done_count < total_count:
@@ -699,7 +701,12 @@ async def resample(input_queue, output_queue):
return resample, preferred

async def serve_vgm(self, request):
vgm_data = await request.read()
sock = web.WebSocketResponse()
await sock.prepare(request)

headers = await sock.receive_json()
vgm_data = await sock.receive_bytes()

digest = hashlib.sha256(vgm_data).hexdigest()[:16]
self._logger.info("web: %s: submitted by %s",
digest, request.remote)
@@ -742,15 +749,20 @@ async def serve_vgm(self, request):
return web.Response(status=400, text=str(e), content_type="text/plain")

input_rate = 1 / vgm_player.sample_time
preferred_rate = int(request.headers["X-Preferred-Sample-Rate"])
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)

async with self._lock:
voltage = float(request.headers["X-Voltage"])
self._logger.info("setting voltage to %.2f V", voltage)
await self._set_voltage(voltage)
try:
voltage = float(headers["Voltage"])
self._logger.info("setting voltage to %.2f V", voltage)
await self._set_voltage(voltage)

except Exception as error:
await sock.close(code=2000, message=str(error))
return sock

self._logger.info("web: %s: start streaming", digest)

@@ -763,48 +775,32 @@ async def serve_vgm(self, request):
play_fut = asyncio.ensure_future(vgm_player.play())

try:
response = web.StreamResponse()
response.content_type = "text/plain"
response.headers["X-Chip"] = vgm_reader.chips()[0]
response.headers["X-Sample-Rate"] = str(output_rate)
total_samples = int(vgm_reader.total_seconds * output_rate)
response.headers["X-Total-Samples"] = str(total_samples)
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)
response.headers["X-Loop-Skip-To"] = str(loop_skip_to)
response.enable_chunked_encoding()
await response.prepare(request)

TRANSPORT_SIZE = 3072
output_buffer = bytearray()
loop_skip_to = int((vgm_reader.total_seconds -
vgm_reader.loop_seconds) * output_rate)
await sock.send_json({
"Chip": vgm_reader.chips()[0],
"Sample-Rate": output_rate,
"Total-Samples": total_samples,
"Loop-Skip-To": loop_skip_to,
})

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

if not resample_fut.done() or not resample_queue.empty():
while len(output_buffer) < TRANSPORT_SIZE:
output_chunk = await resample_queue.get()
output_buffer += output_chunk
if not output_chunk:
break

transport_chunk = output_buffer[:TRANSPORT_SIZE]
while len(transport_chunk) < TRANSPORT_SIZE:
# Pad last transport chunk with silence
transport_chunk += struct.pack("<H", 32768)
output_buffer = output_buffer[TRANSPORT_SIZE:]
await response.write(base64.b64encode(transport_chunk))
if resample_fut.done() and not output_buffer:
elif resample_fut.done():
break
else:
await sock.send_bytes(await resample_queue.get())

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

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

@@ -817,13 +813,13 @@ async def serve_vgm(self, request):
fut.cancel()
raise

return response
return sock

async def serve(self, endpoint):
app = web.Application()
app.add_routes([
web.get ("/", self.serve_index),
web.post("/vgm", self.serve_vgm),
web.get("/", self.serve_index),
web.get("/vgm", self.serve_vgm),
])

try:
112 changes: 51 additions & 61 deletions software/glasgow/applet/audio/yamaha_opl/index.html
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ <h1>Yamaha OP* Web Gateway</h1>
<p>Playback support for: {{compat}}.</p>
<p><b>Glitching</b>: undervolt to <input type="number" min="1.65" max="5.50" value="5.00" step="0.01" id="voltage"></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>Export PCM: <input type="button" id="exportFull" value="Export full"> <input type="button" id="exportLoop" value="Export loop"></p>
<p>Export PCM: <input type="button" id="exportFull" value="Export full" disabled> <input type="button" id="exportLoop" value="Export loop" disabled></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>

@@ -20,7 +20,7 @@ <h1>Yamaha OP* Web Gateway</h1>
And yes, I do transfer raw PCM samples over the network encoded as base64. Deal with it.
-->
<script type="text/javascript">
var BUFFER_AT_LEAST = 100;
var BUFFER_AT_LEAST = 30;

function PCMPlayer() {
this.complete = false;
@@ -293,81 +293,71 @@ <h1>Yamaha OP* Web Gateway</h1>
player.loop = loopCheckbox.checked;
}

var xhr = new XMLHttpRequest();
xhr.open("POST", "vgm", /*async=*/true);
xhr.setRequestHeader("X-Preferred-Sample-Rate", player.preferredSampleRate());
xhr.setRequestHeader("X-Voltage", voltage.value);
var socketUrl = new URL("/vgm", window.location.href);
socketUrl.protocol = socketUrl.protocol.replace("http", "ws");
var socket = new WebSocket(socketUrl);
socket.binaryType = "arraybuffer";
netStatusSpan.innerText = "connecting";
var errored = false;
socket.onopen = function(event) {
errored = false;
socket.send(JSON.stringify({
"Preferred-Sample-Rate": player.preferredSampleRate(),
"Voltage": voltage.value,
}));
socket.send(fileInput.files[0]);
netStatusSpan.innerText = "waiting";
}
var sampleRate = 0;
var seenBytes = 0;
var totalSamples = 0;
var seenSamples = 0;
xhr.onreadystatechange = function(event) {
if(xhr.status >= 400) {
errorPane.style.display = "";
errorSpan.innerText = xhr.responseText;
playButton.disabled = false;
}

if(xhr.status == 200 && xhr.readyState > 2) {
errorPane.style.display = "none";

if(totalSamples == 0) {
sampleRate = parseInt(xhr.getResponseHeader("X-Sample-Rate"));
totalSamples = parseInt(xhr.getResponseHeader("X-Total-Samples"));
player.loopSkipTo = parseInt(xhr.getResponseHeader("X-Loop-Skip-To"));
chipStatusSpan.innerText = "chip " + xhr.getResponseHeader("X-Chip");
}

var CHUNK_SIZE = 4096;
while(xhr.responseText.length >= seenBytes + CHUNK_SIZE) {
var chunk = xhr.responseText.substr(seenBytes, CHUNK_SIZE);
var bytes = base64js.toByteArray(chunk);
var viewLength = (totalSamples - seenSamples) * 2;
if(viewLength > bytes.buffer.byteLength)
viewLength = bytes.buffer.byteLength;
var view = new DataView(bytes.buffer, 0, viewLength);
seenSamples += view.byteLength / 2;
player.addSamples(view, sampleRate);
if(!player._playing)
player.scheduleAtLeast(BUFFER_AT_LEAST);
seenBytes += CHUNK_SIZE;
}

if(xhr.readyState == 4) {
player.complete = true;
exportFullButton.disabled = false;
if(player.loopSkipTo > 0)
exportLoopButton.disabled = false;
}
var doneSamples = 0;
socket.onmessage = function(event) {
errorPane.style.display = "none";
if(totalSamples == 0) {
var response = JSON.parse(event.data);
sampleRate = response["Sample-Rate"];
totalSamples = response["Total-Samples"];
player.loopSkipTo = response["Loop-Skip-To"];
chipStatusSpan.innerText = "chip " + response["Chip"];
} else {
var view = new DataView(new Uint8Array(event.data).buffer);
doneSamples += view.byteLength / 2;
player.addSamples(view, sampleRate);
if(!player._playing)
player.scheduleAtLeast(BUFFER_AT_LEAST);
netStatusSpan.innerText = "streaming " + Math.floor(doneSamples / totalSamples * 100) +
"% (" + doneSamples + "/" + totalSamples + " samples)";
}

var status;
switch(xhr.readyState) {
case 3: status = "streaming"; break;
case 4: status = "idle"; break;
}
socket.onerror = function(event) {
errored = true;
}
socket.onclose = function(event) {
playButton.disabled = false;
if(errored || event.code != 1000) {
errorPane.style.display = "";
errorSpan.innerText = event.reason || "WebSocket connection failed";
netStatusSpan.innerText = "error";
} else {
player.complete = true;
exportFullButton.disabled = false;
if(player.loopSkipTo > 0)
exportLoopButton.disabled = false;
netStatusSpan.innerText = "done " + Math.floor(doneSamples / totalSamples * 100) +
"% (" + doneSamples + "/" + totalSamples + " samples)";
}
if(xhr.readyState == 3)
status += " (" + seenBytes + " bytes)";
netStatusSpan.innerText = status;
}
netStatusSpan.innerText = "waiting";
xhr.send(fileInput.files[0]);

exportFullButton.onclick = function(event) {
var fileName = fileInput.files[0].name.replace(/\.vg[mz]$/i, "") + ".wav";
downloadFile(makeWAVFile(fileName, /*numOfChannels=*/1, sampleRate, /*bytesPerSample=*/2,
player.getAllSamples()));
}

exportLoopButton.onclick = function(event) {
var fileName = fileInput.files[0].name.replace(/\.vg[mz]$/i, "") + " (loop).wav";
downloadFile(makeWAVFile(fileName, /*numOfChannels=*/1, sampleRate, /*bytesPerSample=*/2,
player.getLoopSamples()));
}
};
</script>
<script type="text/javascript">
// base64.min.js
(function(r){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=r()}else if(typeof define==="function"&&define.amd){define([],r)}else{var e;if(typeof window!=="undefined"){e=window}else if(typeof global!=="undefined"){e=global}else if(typeof self!=="undefined"){e=self}else{e=this}e.base64js=r()}})(function(){var r,e,n;return function(){function r(e,n,t){function o(f,i){if(!n[f]){if(!e[f]){var u="function"==typeof require&&require;if(!i&&u)return u(f,!0);if(a)return a(f,!0);var v=new Error("Cannot find module '"+f+"'");throw v.code="MODULE_NOT_FOUND",v}var d=n[f]={exports:{}};e[f][0].call(d.exports,function(r){var n=e[f][1][r];return o(n||r)},d,d.exports,r,e,n,t)}return n[f].exports}for(var a="function"==typeof require&&require,f=0;f<t.length;f++)o(t[f]);return o}return r}()({"/":[function(r,e,n){"use strict";n.byteLength=d;n.toByteArray=h;n.fromByteArray=p;var t=[];var o=[];var a=typeof Uint8Array!=="undefined"?Uint8Array:Array;var f="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";for(var i=0,u=f.length;i<u;++i){t[i]=f[i];o[f.charCodeAt(i)]=i}o["-".charCodeAt(0)]=62;o["_".charCodeAt(0)]=63;function v(r){var e=r.length;if(e%4>0){throw new Error("Invalid string. Length must be a multiple of 4")}var n=r.indexOf("=");if(n===-1)n=e;var t=n===e?0:4-n%4;return[n,t]}function d(r){var e=v(r);var n=e[0];var t=e[1];return(n+t)*3/4-t}function c(r,e,n){return(e+n)*3/4-n}function h(r){var e;var n=v(r);var t=n[0];var f=n[1];var i=new a(c(r,t,f));var u=0;var d=f>0?t-4:t;for(var h=0;h<d;h+=4){e=o[r.charCodeAt(h)]<<18|o[r.charCodeAt(h+1)]<<12|o[r.charCodeAt(h+2)]<<6|o[r.charCodeAt(h+3)];i[u++]=e>>16&255;i[u++]=e>>8&255;i[u++]=e&255}if(f===2){e=o[r.charCodeAt(h)]<<2|o[r.charCodeAt(h+1)]>>4;i[u++]=e&255}if(f===1){e=o[r.charCodeAt(h)]<<10|o[r.charCodeAt(h+1)]<<4|o[r.charCodeAt(h+2)]>>2;i[u++]=e>>8&255;i[u++]=e&255}return i}function s(r){return t[r>>18&63]+t[r>>12&63]+t[r>>6&63]+t[r&63]}function l(r,e,n){var t;var o=[];for(var a=e;a<n;a+=3){t=(r[a]<<16&16711680)+(r[a+1]<<8&65280)+(r[a+2]&255);o.push(s(t))}return o.join("")}function p(r){var e;var n=r.length;var o=n%3;var a=[];var f=16383;for(var i=0,u=n-o;i<u;i+=f){a.push(l(r,i,i+f>u?u:i+f))}if(o===1){e=r[n-1];a.push(t[e>>2]+t[e<<4&63]+"==")}else if(o===2){e=(r[n-2]<<8)+r[n-1];a.push(t[e>>10]+t[e>>4&63]+t[e<<2&63]+"=")}return a.join("")}},{}]},{},[])("/")});
</script>
</body>