diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..10d7849 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +main: clean wasm_exec + tinygo build -o ./public/wasm/wasm.wasm -target wasm -no-debug ./wasm/src/autoCorrelate.go + # cp ./main/index.html ./html/ + cp ./wasm/src/wasm.js ./public/wasm/ + +wasm_exec: + cp `tinygo env TINYGOROOT`/targets/wasm_exec.js ./public/wasm + +clean: + rm -rf ./public/wasm + mkdir ./public/wasm diff --git a/index.html b/index.html index d2f6839..a98bbce 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,8 @@ + + Svelte + TS + Vite App diff --git a/public/wasm/wasm.js b/public/wasm/wasm.js new file mode 100644 index 0000000..76e4ead --- /dev/null +++ b/public/wasm/wasm.js @@ -0,0 +1,28 @@ +"use strict"; + +const WASM_URL = "wasm/wasm.wasm"; + +var wasm; + +function init() { + const go = new Go(); + if ("instantiateStreaming" in WebAssembly) { + WebAssembly.instantiateStreaming(fetch(WASM_URL), go.importObject).then( + function (obj) { + wasm = obj.instance; + go.run(wasm); + } + ); + } else { + fetch(WASM_URL) + .then((resp) => resp.arrayBuffer()) + .then((bytes) => + WebAssembly.instantiate(bytes, go.importObject).then(function (obj) { + wasm = obj.instance; + go.run(wasm); + }) + ); + } +} + +init(); diff --git a/public/wasm/wasm.wasm b/public/wasm/wasm.wasm new file mode 100755 index 0000000..f89678c Binary files /dev/null and b/public/wasm/wasm.wasm differ diff --git a/public/wasm/wasm_exec.js b/public/wasm/wasm_exec.js new file mode 100644 index 0000000..8021b44 --- /dev/null +++ b/public/wasm/wasm_exec.js @@ -0,0 +1,535 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// +// This file has been modified for use by the TinyGo compiler. + +(() => { + // Map multiple JavaScript environments to a single common API, + // preferring web standards over Node.js API. + // + // Environments considered: + // - Browsers + // - Node.js + // - Electron + // - Parcel + + if (typeof global !== "undefined") { + // global already exists + } else if (typeof window !== "undefined") { + window.global = window; + } else if (typeof self !== "undefined") { + self.global = self; + } else { + throw new Error("cannot export Go (neither global, window nor self is defined)"); + } + + if (!global.require && typeof require !== "undefined") { + global.require = require; + } + + if (!global.fs && global.require) { + global.fs = require("fs"); + } + + const enosys = () => { + const err = new Error("not implemented"); + err.code = "ENOSYS"; + return err; + }; + + if (!global.fs) { + let outputBuf = ""; + global.fs = { + constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused + writeSync(fd, buf) { + outputBuf += decoder.decode(buf); + const nl = outputBuf.lastIndexOf("\n"); + if (nl != -1) { + console.log(outputBuf.substr(0, nl)); + outputBuf = outputBuf.substr(nl + 1); + } + return buf.length; + }, + write(fd, buf, offset, length, position, callback) { + if (offset !== 0 || length !== buf.length || position !== null) { + callback(enosys()); + return; + } + const n = this.writeSync(fd, buf); + callback(null, n); + }, + chmod(path, mode, callback) { callback(enosys()); }, + chown(path, uid, gid, callback) { callback(enosys()); }, + close(fd, callback) { callback(enosys()); }, + fchmod(fd, mode, callback) { callback(enosys()); }, + fchown(fd, uid, gid, callback) { callback(enosys()); }, + fstat(fd, callback) { callback(enosys()); }, + fsync(fd, callback) { callback(null); }, + ftruncate(fd, length, callback) { callback(enosys()); }, + lchown(path, uid, gid, callback) { callback(enosys()); }, + link(path, link, callback) { callback(enosys()); }, + lstat(path, callback) { callback(enosys()); }, + mkdir(path, perm, callback) { callback(enosys()); }, + open(path, flags, mode, callback) { callback(enosys()); }, + read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, + readdir(path, callback) { callback(enosys()); }, + readlink(path, callback) { callback(enosys()); }, + rename(from, to, callback) { callback(enosys()); }, + rmdir(path, callback) { callback(enosys()); }, + stat(path, callback) { callback(enosys()); }, + symlink(path, link, callback) { callback(enosys()); }, + truncate(path, length, callback) { callback(enosys()); }, + unlink(path, callback) { callback(enosys()); }, + utimes(path, atime, mtime, callback) { callback(enosys()); }, + }; + } + + if (!global.process) { + global.process = { + getuid() { return -1; }, + getgid() { return -1; }, + geteuid() { return -1; }, + getegid() { return -1; }, + getgroups() { throw enosys(); }, + pid: -1, + ppid: -1, + umask() { throw enosys(); }, + cwd() { throw enosys(); }, + chdir() { throw enosys(); }, + } + } + + if (!global.crypto) { + const nodeCrypto = require("crypto"); + global.crypto = { + getRandomValues(b) { + nodeCrypto.randomFillSync(b); + }, + }; + } + + if (!global.performance) { + global.performance = { + now() { + const [sec, nsec] = process.hrtime(); + return sec * 1000 + nsec / 1000000; + }, + }; + } + + if (!global.TextEncoder) { + global.TextEncoder = require("util").TextEncoder; + } + + if (!global.TextDecoder) { + global.TextDecoder = require("util").TextDecoder; + } + + // End of polyfills for common API. + + const encoder = new TextEncoder("utf-8"); + const decoder = new TextDecoder("utf-8"); + var logLine = []; + + global.Go = class { + constructor() { + this._callbackTimeouts = new Map(); + this._nextCallbackTimeoutID = 1; + + const mem = () => { + // The buffer may change when requesting more memory. + return new DataView(this._inst.exports.memory.buffer); + } + + const setInt64 = (addr, v) => { + mem().setUint32(addr + 0, v, true); + mem().setUint32(addr + 4, Math.floor(v / 4294967296), true); + } + + const getInt64 = (addr) => { + const low = mem().getUint32(addr + 0, true); + const high = mem().getInt32(addr + 4, true); + return low + high * 4294967296; + } + + const loadValue = (addr) => { + const f = mem().getFloat64(addr, true); + if (f === 0) { + return undefined; + } + if (!isNaN(f)) { + return f; + } + + const id = mem().getUint32(addr, true); + return this._values[id]; + } + + const storeValue = (addr, v) => { + const nanHead = 0x7FF80000; + + if (typeof v === "number") { + if (isNaN(v)) { + mem().setUint32(addr + 4, nanHead, true); + mem().setUint32(addr, 0, true); + return; + } + if (v === 0) { + mem().setUint32(addr + 4, nanHead, true); + mem().setUint32(addr, 1, true); + return; + } + mem().setFloat64(addr, v, true); + return; + } + + switch (v) { + case undefined: + mem().setFloat64(addr, 0, true); + return; + case null: + mem().setUint32(addr + 4, nanHead, true); + mem().setUint32(addr, 2, true); + return; + case true: + mem().setUint32(addr + 4, nanHead, true); + mem().setUint32(addr, 3, true); + return; + case false: + mem().setUint32(addr + 4, nanHead, true); + mem().setUint32(addr, 4, true); + return; + } + + let id = this._ids.get(v); + if (id === undefined) { + id = this._idPool.pop(); + if (id === undefined) { + id = this._values.length; + } + this._values[id] = v; + this._goRefCounts[id] = 0; + this._ids.set(v, id); + } + this._goRefCounts[id]++; + let typeFlag = 1; + switch (typeof v) { + case "string": + typeFlag = 2; + break; + case "symbol": + typeFlag = 3; + break; + case "function": + typeFlag = 4; + break; + } + mem().setUint32(addr + 4, nanHead | typeFlag, true); + mem().setUint32(addr, id, true); + } + + const loadSlice = (array, len, cap) => { + return new Uint8Array(this._inst.exports.memory.buffer, array, len); + } + + const loadSliceOfValues = (array, len, cap) => { + const a = new Array(len); + for (let i = 0; i < len; i++) { + a[i] = loadValue(array + i * 8); + } + return a; + } + + const loadString = (ptr, len) => { + return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len)); + } + + const timeOrigin = Date.now() - performance.now(); + this.importObject = { + wasi_snapshot_preview1: { + // https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_write + fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) { + let nwritten = 0; + if (fd == 1) { + for (let iovs_i=0; iovs_i 0, // dummy + fd_fdstat_get: () => 0, // dummy + fd_seek: () => 0, // dummy + "proc_exit": (code) => { + if (global.process) { + // Node.js + process.exit(code); + } else { + // Can't exit in a browser. + throw 'trying to exit with code ' + code; + } + }, + random_get: (bufPtr, bufLen) => { + crypto.getRandomValues(loadSlice(bufPtr, bufLen)); + return 0; + }, + }, + env: { + // func ticks() float64 + "runtime.ticks": () => { + return timeOrigin + performance.now(); + }, + + // func sleepTicks(timeout float64) + "runtime.sleepTicks": (timeout) => { + // Do not sleep, only reactivate scheduler after the given timeout. + setTimeout(this._inst.exports.go_scheduler, timeout); + }, + + // func finalizeRef(v ref) + "syscall/js.finalizeRef": (sp) => { + // Note: TinyGo does not support finalizers so this should never be + // called. + console.error('syscall/js.finalizeRef not implemented'); + }, + + // func stringVal(value string) ref + "syscall/js.stringVal": (ret_ptr, value_ptr, value_len) => { + const s = loadString(value_ptr, value_len); + storeValue(ret_ptr, s); + }, + + // func valueGet(v ref, p string) ref + "syscall/js.valueGet": (retval, v_addr, p_ptr, p_len) => { + let prop = loadString(p_ptr, p_len); + let value = loadValue(v_addr); + let result = Reflect.get(value, prop); + storeValue(retval, result); + }, + + // func valueSet(v ref, p string, x ref) + "syscall/js.valueSet": (v_addr, p_ptr, p_len, x_addr) => { + const v = loadValue(v_addr); + const p = loadString(p_ptr, p_len); + const x = loadValue(x_addr); + Reflect.set(v, p, x); + }, + + // func valueDelete(v ref, p string) + "syscall/js.valueDelete": (v_addr, p_ptr, p_len) => { + const v = loadValue(v_addr); + const p = loadString(p_ptr, p_len); + Reflect.deleteProperty(v, p); + }, + + // func valueIndex(v ref, i int) ref + "syscall/js.valueIndex": (ret_addr, v_addr, i) => { + storeValue(ret_addr, Reflect.get(loadValue(v_addr), i)); + }, + + // valueSetIndex(v ref, i int, x ref) + "syscall/js.valueSetIndex": (v_addr, i, x_addr) => { + Reflect.set(loadValue(v_addr), i, loadValue(x_addr)); + }, + + // func valueCall(v ref, m string, args []ref) (ref, bool) + "syscall/js.valueCall": (ret_addr, v_addr, m_ptr, m_len, args_ptr, args_len, args_cap) => { + const v = loadValue(v_addr); + const name = loadString(m_ptr, m_len); + const args = loadSliceOfValues(args_ptr, args_len, args_cap); + try { + const m = Reflect.get(v, name); + storeValue(ret_addr, Reflect.apply(m, v, args)); + mem().setUint8(ret_addr + 8, 1); + } catch (err) { + storeValue(ret_addr, err); + mem().setUint8(ret_addr + 8, 0); + } + }, + + // func valueInvoke(v ref, args []ref) (ref, bool) + "syscall/js.valueInvoke": (ret_addr, v_addr, args_ptr, args_len, args_cap) => { + try { + const v = loadValue(v_addr); + const args = loadSliceOfValues(args_ptr, args_len, args_cap); + storeValue(ret_addr, Reflect.apply(v, undefined, args)); + mem().setUint8(ret_addr + 8, 1); + } catch (err) { + storeValue(ret_addr, err); + mem().setUint8(ret_addr + 8, 0); + } + }, + + // func valueNew(v ref, args []ref) (ref, bool) + "syscall/js.valueNew": (ret_addr, v_addr, args_ptr, args_len, args_cap) => { + const v = loadValue(v_addr); + const args = loadSliceOfValues(args_ptr, args_len, args_cap); + try { + storeValue(ret_addr, Reflect.construct(v, args)); + mem().setUint8(ret_addr + 8, 1); + } catch (err) { + storeValue(ret_addr, err); + mem().setUint8(ret_addr+ 8, 0); + } + }, + + // func valueLength(v ref) int + "syscall/js.valueLength": (v_addr) => { + return loadValue(v_addr).length; + }, + + // valuePrepareString(v ref) (ref, int) + "syscall/js.valuePrepareString": (ret_addr, v_addr) => { + const s = String(loadValue(v_addr)); + const str = encoder.encode(s); + storeValue(ret_addr, str); + setInt64(ret_addr + 8, str.length); + }, + + // valueLoadString(v ref, b []byte) + "syscall/js.valueLoadString": (v_addr, slice_ptr, slice_len, slice_cap) => { + const str = loadValue(v_addr); + loadSlice(slice_ptr, slice_len, slice_cap).set(str); + }, + + // func valueInstanceOf(v ref, t ref) bool + "syscall/js.valueInstanceOf": (v_addr, t_addr) => { + return loadValue(v_addr) instanceof loadValue(t_addr); + }, + + // func copyBytesToGo(dst []byte, src ref) (int, bool) + "syscall/js.copyBytesToGo": (ret_addr, dest_addr, dest_len, dest_cap, source_addr) => { + let num_bytes_copied_addr = ret_addr; + let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable + + const dst = loadSlice(dest_addr, dest_len); + const src = loadValue(source_addr); + if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { + mem().setUint8(returned_status_addr, 0); // Return "not ok" status + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + setInt64(num_bytes_copied_addr, toCopy.length); + mem().setUint8(returned_status_addr, 1); // Return "ok" status + }, + + // copyBytesToJS(dst ref, src []byte) (int, bool) + // Originally copied from upstream Go project, then modified: + // https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416 + "syscall/js.copyBytesToJS": (ret_addr, dest_addr, source_addr, source_len, source_cap) => { + let num_bytes_copied_addr = ret_addr; + let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable + + const dst = loadValue(dest_addr); + const src = loadSlice(source_addr, source_len); + if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { + mem().setUint8(returned_status_addr, 0); // Return "not ok" status + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + setInt64(num_bytes_copied_addr, toCopy.length); + mem().setUint8(returned_status_addr, 1); // Return "ok" status + }, + } + }; + } + + async run(instance) { + this._inst = instance; + this._values = [ // JS values that Go currently has references to, indexed by reference id + NaN, + 0, + null, + true, + false, + global, + this, + ]; + this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id + this._ids = new Map(); // mapping from JS values to reference ids + this._idPool = []; // unused ids that have been garbage collected + this.exited = false; // whether the Go program has exited + + const mem = new DataView(this._inst.exports.memory.buffer) + + while (true) { + const callbackPromise = new Promise((resolve) => { + this._resolveCallbackPromise = () => { + if (this.exited) { + throw new Error("bad callback: Go program has already exited"); + } + setTimeout(resolve, 0); // make sure it is asynchronous + }; + }); + this._inst.exports._start(); + if (this.exited) { + break; + } + await callbackPromise; + } + } + + _resume() { + if (this.exited) { + throw new Error("Go program has already exited"); + } + this._inst.exports.resume(); + if (this.exited) { + this._resolveExitPromise(); + } + } + + _makeFuncWrapper(id) { + const go = this; + return function () { + const event = { id: id, this: this, args: arguments }; + go._pendingEvent = event; + go._resume(); + return event.result; + }; + } + } + + if ( + global.require && + global.require.main === module && + global.process && + global.process.versions && + !global.process.versions.electron + ) { + if (process.argv.length != 3) { + console.error("usage: go_js_wasm_exec [wasm binary] [arguments]"); + process.exit(1); + } + + const go = new Go(); + WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { + return go.run(result.instance); + }).catch((err) => { + console.error(err); + process.exit(1); + }); + } +})(); diff --git a/src/lib/Tuner.svelte b/src/lib/Tuner.svelte index cd3c272..99c4fd0 100644 --- a/src/lib/Tuner.svelte +++ b/src/lib/Tuner.svelte @@ -202,11 +202,19 @@ // array for received buffer of audio let buflen = 2048; let buf = new Float32Array(buflen); - // updates the note using requestAnimationFrame function updatePitch() { + const wasmMemory = new Float32Array(wasm.exports.memory.buffer); + const wasmMemoryPtr = wasm.exports.getBufferPointer(); + + // Get a slice of the wasm memory that points to the buffer created in Go. + // Since the buffer is a Float32Array, we need to divide the byte offset by 4. (4 bytes per float) + wasmMemory.set(buf, wasmMemoryPtr / wasmMemory.BYTES_PER_ELEMENT); + + // Copy the current audio to buf. Since we set the buffer location to the wasm memory, it's accessible from Go. analyser.getFloatTimeDomainData(buf); - let ac = autoCorrelate(buf, audioContext.sampleRate); + // It may be more clear to pass a pointer to autoCorrelate, instead of using closures in Go. + let ac = wasm.exports.autoCorrelate(audioContext.sampleRate, sensitivity); if (ac == -1) { // note was ignored diff --git a/wasm/src/autoCorrelate.go b/wasm/src/autoCorrelate.go new file mode 100644 index 0000000..ec0f3df --- /dev/null +++ b/wasm/src/autoCorrelate.go @@ -0,0 +1,106 @@ +package main + +import ( + "fmt" + "math" +) + +// Create a byte (float32, not Go byte) buffer, which will be available in Wasm Memory. +// We can then share this buffer with JS and Wasm. +const BUFFER_SIZE int = 2048 + +var buf [BUFFER_SIZE]float32 + +func main() { + fmt.Println("call wasm.exports.autoCorrelate() from JS in the browser:") +} + +// GetBufferPointer returns a pointer (index) to our buffer in wasm memory. +// +//export getBufferPointer +func getBufferPointer() *[BUFFER_SIZE]float32 { + return &buf +} + +// GetBufferSize returns the size of our buffer in wasm memory. +// +//export getBufferSize +func getBufferSize() int { + return BUFFER_SIZE +} + +type Pitch float32 + +var octaveLen = 7.0 + +//export autoCorrelate +func autoCorrelate(sampleRate, thres float64) Pitch { + size := len(buf) + rms := float64(0.0) + for _, v := range buf { + val := float64(v) + rms += val * val + } + rms = math.Sqrt(rms / float64(size)) + if rms < thres { + return -1 + } + + r1 := 0 + r2 := size - 1 + + for i := 0; i < size/2; i++ { + if math.Abs(float64(buf[i])) < thres { + r1 = i + break + } + } + + for i := 1; i < size/2; i++ { + if math.Abs(float64(buf[size-i])) < thres { + r2 = size - i + break + } + } + + bufSample := buf[r1:r2] + size = len(bufSample) + + c := make([]float32, size) + for i := 0; i < size; i++ { + for j := 0; j < size-i; j++ { + c[i] = c[i] + bufSample[j]*bufSample[j+i] + } + } + + d := 0 + for d < size-2 && c[d] > c[d+1] { + d++ + } + + maxVal := -1.0 + maxPos := -1 + + for i := d; i < size; i++ { + if float64(c[i]) > maxVal { + maxVal = float64(c[i]) + maxPos = i + } + } + + t0 := maxPos + + // Handle case when d is already out-of-bounds of c + if maxPos < 1 { + return -1 + } + + x1 := c[t0-1] + x2 := c[t0] + x3 := c[t0+1] + + a := float64((x1 + x3 - 2*x2) / 2) + b := float64((x3 - x1) / 2) + + return Pitch(sampleRate / (float64(t0) - b/(2*a))) +} diff --git a/wasm/src/wasm.js b/wasm/src/wasm.js new file mode 100644 index 0000000..76e4ead --- /dev/null +++ b/wasm/src/wasm.js @@ -0,0 +1,28 @@ +"use strict"; + +const WASM_URL = "wasm/wasm.wasm"; + +var wasm; + +function init() { + const go = new Go(); + if ("instantiateStreaming" in WebAssembly) { + WebAssembly.instantiateStreaming(fetch(WASM_URL), go.importObject).then( + function (obj) { + wasm = obj.instance; + go.run(wasm); + } + ); + } else { + fetch(WASM_URL) + .then((resp) => resp.arrayBuffer()) + .then((bytes) => + WebAssembly.instantiate(bytes, go.importObject).then(function (obj) { + wasm = obj.instance; + go.run(wasm); + }) + ); + } +} + +init();