changeset 175:a91b4defa581

WebAssembly-related fixes and updates
author Chris Cannam <cannam@all-day-breakfast.com>
date Wed, 13 Jun 2018 15:55:39 +0100
parents a007360392e0
children eaf46e7647a0
files Makefile Makefile.inc test/node-load-test.js test/perf-test-node.js test/perf-test-wasm.html test/perf-test.html test/perf-test.js test/quick-test.js
diffstat 8 files changed, 735 insertions(+), 689 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile	Tue Jun 12 14:58:05 2018 +0100
+++ b/Makefile	Wed Jun 13 15:55:39 2018 +0100
@@ -1,5 +1,5 @@
 
-all:	prerequisites generator examples
+all:	prerequisites generator examples-wasm
 
 prerequisites:
 	./bin/check-prerequisites.sh
@@ -13,9 +13,16 @@
 generator:
 	$(MAKE) -C generator
 
-.PHONY: examples
-examples:
+.PHONY: examples-em
+examples-em:
 	$(MAKE) -C examples/vamp-example-plugins em
 	$(MAKE) -C examples/vamp-test-plugin em
-	$(MAKE) -C examples/vamp-example-plugins test
-	$(MAKE) -C examples/vamp-test-plugin test
+	$(MAKE) -C examples/vamp-example-plugins test-em
+	$(MAKE) -C examples/vamp-test-plugin test-em
+
+.PHONY: examples-wasm
+examples-wasm:
+	$(MAKE) -C examples/vamp-example-plugins wasm
+	$(MAKE) -C examples/vamp-test-plugin wasm
+	$(MAKE) -C examples/vamp-example-plugins test-wasm
+	$(MAKE) -C examples/vamp-test-plugin test-wasm
--- a/Makefile.inc	Tue Jun 12 14:58:05 2018 +0100
+++ b/Makefile.inc	Wed Jun 13 15:55:39 2018 +0100
@@ -36,10 +36,14 @@
 		@echo
 		@echo "Supported make targets:"
 		@echo
+		@echo "$$ make wasm"
+		@echo "  - build Javascript/WebAssembly module using Emscripten"
+		@echo "$$ make test-wasm"
+		@echo "  - build and run simple test of Javascript/WebAssembly module using node.js"
 		@echo "$$ make em"
 		@echo "  - build Javascript module using Emscripten"
-		@echo "$$ make test"
-		@echo "  - build and run simple load test of Javascript module using node.js"
+		@echo "$$ make test-em"
+		@echo "  - build and run simple test of Javascript module using node.js"
 		@echo "$$ make linux"
 		@echo "  - build native-code module on Linux (currently this is mostly for testing)"
 		@echo "$$ make clean"
@@ -95,21 +99,25 @@
 SO_MODULE_EXT	:= .so
 SO_MODULE	:= $(MODULE_NAME)$(SO_MODULE_EXT)
 
-EMFLAGS		:= \
-		--memory-init-file 0 \
+EMFLAGS_ANY	:= \
+                --memory-init-file 0 \
 		-s MODULARIZE=1 \
 		-s NO_FILESYSTEM=1 \
+		-s MEM_INIT_METHOD=0 \
 		-s ERROR_ON_UNDEFINED_SYMBOLS=1 \
 	    	-s "EXPORT_NAME='$(EM_MODULE_SYMBOL)'" \
-	    	-s "EXPORTED_FUNCTIONS=['_piperRequestJson','_piperProcessRaw','_piperFreeJson']" \
+	    	-s "EXPORTED_FUNCTIONS=['_piperRequestJson', '_piperProcessRaw', '_piperFreeJson', '_malloc', '_free']" \
                 -s "EXTRA_EXPORTED_RUNTIME_METHODS=['cwrap', 'allocate', 'ALLOC_NORMAL', 'Pointer_stringify', 'intArrayFromString']" \
 		$(EMFLAGS)
 
-WASMFLAGS       := \
-                $(EMFLAGS) \
-                -s WASM=1
+EMFLAGS_ASMJS   := \
+                $(EMFLAGS_ANY) \
+                -s WASM=0
 
-#                -s "BINARYEN_METHOD='native-wasm'"
+EMFLAGS_WASM    := \
+                $(EMFLAGS_ANY) \
+                -s WASM=1 \
+                -s SINGLE_FILE=1
 
 CXX_SOURCES	:= $(MODULE_SOURCE) $(ADAPTER_SOURCES) $(PLUGIN_SOURCES) $(SDK_SOURCES) $(OTHER_SOURCES)
 C_SOURCES	:= $(PLUGIN_C_SOURCES) $(C_SOURCES)
@@ -123,16 +131,16 @@
 em:	$(EM_MODULE_U)
 em:	CXX		:= em++
 em:	CC		:= emcc
-em:	CXXFLAGS	:= -std=c++11 -fPIC -Wall -Wextra $(DEFINES) $(OPTFLAGS) $(EMFLAGS) $(INCLUDES)
-em:	CFLAGS		:= -fPIC -Wall -Wextra $(DEFINES) $(OPTFLAGS) $(EMFLAGS) $(INCLUDES)
-em:	LDFLAGS		:= $(EMFLAGS)
+em:	CXXFLAGS	:= -std=c++11 -fPIC -Wall -Wextra $(DEFINES) $(OPTFLAGS) $(EMFLAGS_ASMJS) $(INCLUDES)
+em:	CFLAGS		:= -fPIC -Wall -Wextra $(DEFINES) $(OPTFLAGS) $(EMFLAGS_ASMJS) $(INCLUDES)
+em:	LDFLAGS		:= $(EMFLAGS_ASMJS)
 
 wasm:   $(WASM_MODULE_U)
 wasm:   CXX		:= em++
 wasm:	CC		:= emcc
-wasm:	CXXFLAGS	:= -std=c++11 -fPIC -Wall -Wextra $(DEFINES) $(OPTFLAGS) $(WASMFLAGS) $(INCLUDES)
-wasm:	CFLAGS		:= -fPIC -Wall -Wextra $(DEFINES) $(OPTFLAGS) $(WASMFLAGS) $(INCLUDES)
-wasm:	LDFLAGS		:= $(WASMFLAGS)
+wasm:	CXXFLAGS	:= -std=c++11 -fPIC -Wall -Wextra $(DEFINES) $(OPTFLAGS) $(EMFLAGS_WASM) $(INCLUDES)
+wasm:	CFLAGS		:= -fPIC -Wall -Wextra $(DEFINES) $(OPTFLAGS) $(EMFLAGS_WASM) $(INCLUDES)
+wasm:	LDFLAGS		:= $(EMFLAGS_WASM)
 
 linux:	$(SO_MODULE)
 linux:	CXXFLAGS	:= -std=c++11 -fPIC -Wall -Wextra $(DEFINES) $(OPTFLAGS) $(INCLUDES)
@@ -171,10 +179,10 @@
 $(SO_MODULE):	$(OBJECTS)
 		$(CXX) -o $@ $^ $(LDFLAGS)
 
-test:		em
+test-em:	em
 		$(NODE) $(MY_DIR)/test/node-load-test.js $(shell pwd)/$(EM_MODULE_U)
 
-test-wasm:		wasm
+test-wasm:	wasm
 		$(NODE) $(MY_DIR)/test/node-load-test.js $(shell pwd)/$(WASM_MODULE_U)
 
 clean:
--- a/test/node-load-test.js	Tue Jun 12 14:58:05 2018 +0100
+++ b/test/node-load-test.js	Wed Jun 13 15:55:39 2018 +0100
@@ -22,239 +22,242 @@
 
 note("Loading library \"" + libraryPath + "\"...");
 var extractor = require(libraryPath);
-var extractorModule = extractor();
 
-var piperRequestJson = extractorModule.cwrap(
-    'piperRequestJson', 'number', ['number']
-);
+extractor().then(function(extractorModule) {
 
-var piperProcessRaw = extractorModule.cwrap(
-    "piperProcessRaw", "number", ["number", "number", "number", "number"]
-);
+    var piperRequestJson = extractorModule.cwrap(
+        'piperRequestJson', 'number', ['number']
+    );
 
-var piperFreeJson = extractorModule.cwrap(
-    'piperFreeJson', 'void', ['number']
-);
+    var piperProcessRaw = extractorModule.cwrap(
+        "piperProcessRaw", "number", ["number", "number", "number", "number"]
+    );
 
-function processRaw(request) {
-    
-    const nChannels = request.processInput.inputBuffers.length;
-    const nFrames = request.processInput.inputBuffers[0].length;
+    var piperFreeJson = extractorModule.cwrap(
+        'piperFreeJson', 'void', ['number']
+    );
 
-    const buffersPtr = extractorModule._malloc(nChannels * 4);
-    const buffers = new Uint32Array(
-        extractorModule.HEAPU8.buffer, buffersPtr, nChannels);
+    function processRaw(request) {
+        
+        const nChannels = request.processInput.inputBuffers.length;
+        const nFrames = request.processInput.inputBuffers[0].length;
 
-    for (let i = 0; i < nChannels; ++i) {
-        const framesPtr = extractorModule._malloc(nFrames * 4);
-        const frames = new Float32Array(
-            extractorModule.HEAPU8.buffer, framesPtr, nFrames);
-        frames.set(request.processInput.inputBuffers[i]);
-        buffers[i] = framesPtr;
+        const buffersPtr = extractorModule._malloc(nChannels * 4);
+        const buffers = new Uint32Array(
+            extractorModule.HEAPU8.buffer, buffersPtr, nChannels);
+
+        for (let i = 0; i < nChannels; ++i) {
+            const framesPtr = extractorModule._malloc(nFrames * 4);
+            const frames = new Float32Array(
+                extractorModule.HEAPU8.buffer, framesPtr, nFrames);
+            frames.set(request.processInput.inputBuffers[i]);
+            buffers[i] = framesPtr;
+        }
+        
+        const responseJson = piperProcessRaw(
+            request.handle,
+            buffersPtr,
+            request.processInput.timestamp.s,
+            request.processInput.timestamp.n);
+        
+        for (let i = 0; i < nChannels; ++i) {
+            extractorModule._free(buffers[i]);
+        }
+        extractorModule._free(buffersPtr);
+
+        const responseJstr = extractorModule.Pointer_stringify(responseJson);
+        const response = JSON.parse(responseJstr);
+        
+        piperFreeJson(responseJson);
+        
+        return response;
     }
-    
-    const responseJson = piperProcessRaw(
-        request.handle,
-        buffersPtr,
-        request.processInput.timestamp.s,
-        request.processInput.timestamp.n);
-    
-    for (let i = 0; i < nChannels; ++i) {
-        extractorModule._free(buffers[i]);
-    }
-    extractorModule._free(buffersPtr);
 
-    const responseJstr = extractorModule.Pointer_stringify(responseJson);
-    const response = JSON.parse(responseJstr);
-    
-    piperFreeJson(responseJson);
-    
-    return response;
-}
-
-function makeTimestamp(seconds) {
-    if (seconds >= 0.0) {
-        return {
-            s: Math.floor(seconds),
-            n: Math.floor((seconds - Math.floor(seconds)) * 1e9 + 0.5)
-        };
-    } else {
-        const { s, n } = makeTimestamp(-seconds);
-        return { s: -s, n: -n };
-    }
-}
-
-function frame2timestamp(frame, rate) {
-    return makeTimestamp(frame / rate);
-}
-
-function request(req) {
-    var jsonStr = JSON.stringify(req);
-    var m = extractorModule;
-    // Inspection reveals that intArrayFromString converts the string
-    // from utf16 to utf8, which is what we want (though the docs
-    // don't mention this). Note the *Cstr values are Emscripten heap
-    // pointers
-    let inCstr = m.allocate(m.intArrayFromString(jsonStr), 'i8', m.ALLOC_NORMAL);
-    let outCstr = piperRequestJson(inCstr);
-    m._free(inCstr);
-    const responseJstr = m.Pointer_stringify(outCstr);
-    const response = JSON.parse(responseJstr);
-    piperFreeJson(outCstr);
-    return response;
-}
-
-function myFromBase64(b64) {
-    while (b64.length % 4 > 0) { b64 += "="; }
-    let conv = new Float32Array(base64.toByteArray(b64).buffer);
-    return conv;
-}
-
-function convertWireFeature(wfeature) {
-    let out = {};
-    if (wfeature.timestamp != null) {
-        out.timestamp = wfeature.timestamp;
-    }
-    if (wfeature.duration != null) {
-        out.duration = wfeature.duration;
-    }
-    if (wfeature.label != null) {
-        out.label = wfeature.label;
-    }
-    const vv = wfeature.featureValues;
-    if (vv != null) {
-        if (typeof vv === "string") {
-            out.featureValues = myFromBase64(vv);
+    function makeTimestamp(seconds) {
+        if (seconds >= 0.0) {
+            return {
+                s: Math.floor(seconds),
+                n: Math.floor((seconds - Math.floor(seconds)) * 1e9 + 0.5)
+            };
         } else {
-            out.featureValues = new Float32Array(vv);
+            const { s, n } = makeTimestamp(-seconds);
+            return { s: -s, n: -n };
         }
     }
-    return out;
-}
 
-function convertWireFeatureList(wfeatures) {
-    return wfeatures.map(convertWireFeature);
-}
+    function frame2timestamp(frame, rate) {
+        return makeTimestamp(frame / rate);
+    }
 
-function responseToFeatureSet(response) {
-    const features = new Map();
-    const processResponse = response.result;
-    const wireFeatures = processResponse.features;
-    Object.keys(wireFeatures).forEach(key => {
-        return features.set(key, convertWireFeatureList(wireFeatures[key]));
-    });
-    return features;
-}
+    function request(req) {
+        var jsonStr = JSON.stringify(req);
+        var m = extractorModule;
+        // Inspection reveals that intArrayFromString converts the string
+        // from utf16 to utf8, which is what we want (though the docs
+        // don't mention this). Note the *Cstr values are Emscripten heap
+        // pointers
+        let inCstr = m.allocate(m.intArrayFromString(jsonStr), 'i8', m.ALLOC_NORMAL);
+        let outCstr = piperRequestJson(inCstr);
+        m._free(inCstr);
+        const responseJstr = m.Pointer_stringify(outCstr);
+        const response = JSON.parse(responseJstr);
+        piperFreeJson(outCstr);
+        return response;
+    }
 
-function checkSuccess(response) {
-    if (response.error) {
-	console.log("Request type " + response.method + " failed: " +
-		    response.error.message);
-	throw response.error.message;
+    function myFromBase64(b64) {
+        while (b64.length % 4 > 0) { b64 += "="; }
+        let conv = new Float32Array(base64.toByteArray(b64).buffer);
+        return conv;
     }
-}
 
-function test() {
-
-    let start = (new Date()).getTime();
-
-    const rate = 44100;
-
-    note("Listing plugins...");
-    let response = request({
-	method: "list"
-    });
-    checkSuccess(response);
-
-    if (pluginKey === "") {
-	pluginKey = response.result.available[0].key;
-	note("Loading first plugin \"" + pluginKey + "\"...");
-    } else {
-	note("Loading requested plugin \"" + pluginKey + "\"...");
-    }
-    
-    response = request({
-        method: "load",
-        params: {
-            key: pluginKey,
-            inputSampleRate: rate,
-            adapterFlags: ["AdaptAllSafe"]
+    function convertWireFeature(wfeature) {
+        let out = {};
+        if (wfeature.timestamp != null) {
+            out.timestamp = wfeature.timestamp;
         }
-    });
-
-//    note("Load request returned: " + JSON.stringify(response) + "\n");
-    
-    checkSuccess(response);
-
-    var blockSize = response.result.defaultConfiguration.framing.blockSize;
-    var stepSize = response.result.defaultConfiguration.framing.stepSize;
-
-    response = request({
-        method: "configure",
-        params: {
-            handle: 1,
-            configuration: {
-		framing: {
-                    blockSize: blockSize,
-                    stepSize: stepSize,
-		},
-                channelCount: 1
+        if (wfeature.duration != null) {
+            out.duration = wfeature.duration;
+        }
+        if (wfeature.label != null) {
+            out.label = wfeature.label;
+        }
+        const vv = wfeature.featureValues;
+        if (vv != null) {
+            if (typeof vv === "string") {
+                out.featureValues = myFromBase64(vv);
+            } else {
+                out.featureValues = new Float32Array(vv);
             }
         }
-    });
-    checkSuccess(response);
+        return out;
+    }
 
-    blockSize = response.result.framing.blockSize;
-    stepSize = response.result.framing.stepSize;
-    
-    const nblocks = 1000;
+    function convertWireFeatureList(wfeatures) {
+        return wfeatures.map(convertWireFeature);
+    }
 
-    const makeBlock = (n => { 
-        return {
-            timestamp : frame2timestamp(n * blockSize, rate),
-            inputBuffers : [
-                new Float32Array(Array.from(Array(blockSize).keys(),
-                                            n => n / blockSize))
-            ],
+    function responseToFeatureSet(response) {
+        const features = new Map();
+        const processResponse = response.result;
+        const wireFeatures = processResponse.features;
+        Object.keys(wireFeatures).forEach(key => {
+            return features.set(key, convertWireFeatureList(wireFeatures[key]));
+        });
+        return features;
+    }
+
+    function checkSuccess(response) {
+        if (response.error) {
+	    console.log("Request type " + response.method + " failed: " +
+		        response.error.message);
+	    throw response.error.message;
         }
-    });
-    
-    const blocks = Array.from(Array(nblocks).keys(), makeBlock);
-    
-    note("Now processing " + nblocks + " blocks of 1024 samples each...");
+    }
 
-    let featureCount = 0;
-    
-    for (let i = 0; i < nblocks; ++i) {
-	response = processRaw({
-	    "handle": 1,
-	    "processInput": blocks[i]
-	});
+    function test() {
+
+        let start = (new Date()).getTime();
+
+        const rate = 44100;
+
+        note("Listing plugins...");
+        let response = request({
+	    method: "list"
+        });
+        checkSuccess(response);
+
+        if (pluginKey === "") {
+	    pluginKey = response.result.available[0].key;
+	    note("Loading first plugin \"" + pluginKey + "\"...");
+        } else {
+	    note("Loading requested plugin \"" + pluginKey + "\"...");
+        }
+        
+        response = request({
+            method: "load",
+            params: {
+                key: pluginKey,
+                inputSampleRate: rate,
+                adapterFlags: ["AdaptAllSafe"]
+            }
+        });
+
+        //    note("Load request returned: " + JSON.stringify(response) + "\n");
+        
+        checkSuccess(response);
+
+        var blockSize = response.result.defaultConfiguration.framing.blockSize;
+        var stepSize = response.result.defaultConfiguration.framing.stepSize;
+
+        response = request({
+            method: "configure",
+            params: {
+                handle: 1,
+                configuration: {
+		    framing: {
+                        blockSize: blockSize,
+                        stepSize: stepSize,
+		    },
+                    channelCount: 1
+                }
+            }
+        });
+        checkSuccess(response);
+
+        blockSize = response.result.framing.blockSize;
+        stepSize = response.result.framing.stepSize;
+        
+        const nblocks = 1000;
+
+        const makeBlock = (n => { 
+            return {
+                timestamp : frame2timestamp(n * blockSize, rate),
+                inputBuffers : [
+                    new Float32Array(Array.from(Array(blockSize).keys(),
+                                                n => n / blockSize))
+                ],
+            }
+        });
+        
+        const blocks = Array.from(Array(nblocks).keys(), makeBlock);
+        
+        note("Now processing " + nblocks + " blocks of 1024 samples each...");
+
+        let featureCount = 0;
+        
+        for (let i = 0; i < nblocks; ++i) {
+	    response = processRaw({
+	        "handle": 1,
+	        "processInput": blocks[i]
+	    });
+            let features = responseToFeatureSet(response);
+            for (let featureList of features.values()) {
+                featureCount += featureList.length;
+            }
+        }
+
+        note("Cleaning up the plugin and getting any remaining features...");
+        response = request({
+            method: "finish",
+            params: {
+                handle: 1
+            }
+        });
+        checkSuccess(response);
+
         let features = responseToFeatureSet(response);
         for (let featureList of features.values()) {
             featureCount += featureList.length;
         }
+        
+        note("Done, total number of features across all outputs = " + featureCount);
+
+        let finish = (new Date()).getTime();
+        note("Total time taken " + (finish - start) + " ms");
     }
 
-    note("Cleaning up the plugin and getting any remaining features...");
-    response = request({
-        method: "finish",
-        params: {
-            handle: 1
-        }
-    });
-    checkSuccess(response);
+    test();
+});
 
-    let features = responseToFeatureSet(response);
-    for (let featureList of features.values()) {
-        featureCount += featureList.length;
-    }
-    
-    note("Done, total number of features across all outputs = " + featureCount);
 
-    let finish = (new Date()).getTime();
-    note("Total time taken " + (finish - start) + " ms");
-}
-
-test();
-
--- a/test/perf-test-node.js	Tue Jun 12 14:58:05 2018 +0100
+++ b/test/perf-test-node.js	Wed Jun 13 15:55:39 2018 +0100
@@ -2,224 +2,225 @@
 
 var extractorName = "VampExamplePlugins";
 var pluginKey = "vamp-example-plugins:zerocrossing";
-var extractor = require("../examples/" + extractorName);
+var extractor = require("../examples/vamp-example-plugins/" + extractorName + "_w.umd");
 var base64 = require("./base64");
-var extractorModule = extractor();
 
-// It is possible to declare both parameters and return values as
-// "string", in which case Emscripten will take care of
-// conversions. But it's not clear how one would manage memory for
-// newly-constructed returned C strings -- the returned pointer from
-// piperRequestJson would appear (?) to be thrown away by the
-// Emscripten string converter if we declare it as returning a string,
-// so we have no opportunity to pass it to piperFreeJson, which
-// suggests this would leak memory if the string isn't static. Not
-// wholly sure though. Anyway, passing and returning pointers (as
-// numbers) means we can manage the Emscripten heap memory however we
-// want in our request wrapper function below.
+extractor().then(function (extractorModule) {
 
-var piperRequestJson = extractorModule.cwrap(
-    'piperRequestJson', 'number', ['number']
-);
+    // It is possible to declare both parameters and return values as
+    // "string", in which case Emscripten will take care of
+    // conversions. But it's not clear how one would manage memory for
+    // newly-constructed returned C strings -- the returned pointer from
+    // piperRequestJson would appear (?) to be thrown away by the
+    // Emscripten string converter if we declare it as returning a string,
+    // so we have no opportunity to pass it to piperFreeJson, which
+    // suggests this would leak memory if the string isn't static. Not
+    // wholly sure though. Anyway, passing and returning pointers (as
+    // numbers) means we can manage the Emscripten heap memory however we
+    // want in our request wrapper function below.
 
-var piperProcessRaw = extractorModule.cwrap(
-    "piperProcessRaw", "number", ["number", "number", "number", "number"]
-);
+    var piperRequestJson = extractorModule.cwrap(
+        'piperRequestJson', 'number', ['number']
+    );
 
-var piperFreeJson = extractorModule.cwrap(
-    'piperFreeJson', 'void', ['number']
-);
+    var piperProcessRaw = extractorModule.cwrap(
+        "piperProcessRaw", "number", ["number", "number", "number", "number"]
+    );
 
-function note(blah) {
-    console.log(blah);
-}
+    var piperFreeJson = extractorModule.cwrap(
+        'piperFreeJson', 'void', ['number']
+    );
 
-function comment(blah) {
-    console.log(blah);
-}
+    function note(blah) {
+        console.log(blah);
+    }
 
-function processRaw(request) {
-    
-    const nChannels = request.processInput.inputBuffers.length;
-    const nFrames = request.processInput.inputBuffers[0].length;
+    function comment(blah) {
+        console.log(blah);
+    }
 
-    const buffersPtr = extractorModule._malloc(nChannels * 4);
-    const buffers = new Uint32Array(
-        extractorModule.HEAPU8.buffer, buffersPtr, nChannels);
+    function processRaw(request) {
+        
+        const nChannels = request.processInput.inputBuffers.length;
+        const nFrames = request.processInput.inputBuffers[0].length;
 
-    for (let i = 0; i < nChannels; ++i) {
-        const framesPtr = extractorModule._malloc(nFrames * 4);
-        const frames = new Float32Array(
-            extractorModule.HEAPU8.buffer, framesPtr, nFrames);
-        frames.set(request.processInput.inputBuffers[i]);
-        buffers[i] = framesPtr;
+        const buffersPtr = extractorModule._malloc(nChannels * 4);
+        const buffers = new Uint32Array(
+            extractorModule.HEAPU8.buffer, buffersPtr, nChannels);
+
+        for (let i = 0; i < nChannels; ++i) {
+            const framesPtr = extractorModule._malloc(nFrames * 4);
+            const frames = new Float32Array(
+                extractorModule.HEAPU8.buffer, framesPtr, nFrames);
+            frames.set(request.processInput.inputBuffers[i]);
+            buffers[i] = framesPtr;
+        }
+        
+        const responseJson = piperProcessRaw(
+            request.handle,
+            buffersPtr,
+            request.processInput.timestamp.s,
+            request.processInput.timestamp.n);
+        
+        for (let i = 0; i < nChannels; ++i) {
+            extractorModule._free(buffers[i]);
+        }
+        extractorModule._free(buffersPtr);
+
+        const responseJstr = extractorModule.Pointer_stringify(responseJson);
+        const response = JSON.parse(responseJstr);
+        
+        piperFreeJson(responseJson);
+        
+        return response;
     }
-    
-    const responseJson = piperProcessRaw(
-        request.handle,
-        buffersPtr,
-        request.processInput.timestamp.s,
-        request.processInput.timestamp.n);
-    
-    for (let i = 0; i < nChannels; ++i) {
-        extractorModule._free(buffers[i]);
-    }
-    extractorModule._free(buffersPtr);
 
-    const responseJstr = extractorModule.Pointer_stringify(responseJson);
-    const response = JSON.parse(responseJstr);
-    
-    piperFreeJson(responseJson);
-    
-    return response;
-}
-
-function makeTimestamp(seconds) {
-    if (seconds >= 0.0) {
-        return {
-            s: Math.floor(seconds),
-            n: Math.floor((seconds - Math.floor(seconds)) * 1e9 + 0.5)
-        };
-    } else {
-        const { s, n } = makeTimestamp(-seconds);
-        return { s: -s, n: -n };
-    }
-}
-
-function frame2timestamp(frame, rate) {
-    return makeTimestamp(frame / rate);
-}
-
-function request(jsonStr) {
-    note("Request JSON = " + jsonStr);
-    var m = extractorModule;
-    // Inspection reveals that intArrayFromString converts the string
-    // from utf16 to utf8, which is what we want (though the docs
-    // don't mention this). Note the *Cstr values are Emscripten heap
-    // pointers
-    var inCstr = m.allocate(m.intArrayFromString(jsonStr), 'i8', m.ALLOC_NORMAL);
-    var outCstr = piperRequestJson(inCstr);
-    m._free(inCstr);
-    var result = m.Pointer_stringify(outCstr);
-    piperFreeJson(outCstr);
-    note("Returned JSON = " + result);
-    return result;
-}
-
-function myFromBase64(b64) {
-    while (b64.length % 4 > 0) { b64 += "="; }
-    let conv = new Float32Array(base64.toByteArray(b64).buffer);
-    return conv;
-}
-
-function convertWireFeature(wfeature) {
-    let out = {};
-    if (wfeature.timestamp != null) {
-        out.timestamp = wfeature.timestamp;
-    }
-    if (wfeature.duration != null) {
-        out.duration = wfeature.duration;
-    }
-    if (wfeature.label != null) {
-        out.label = wfeature.label;
-    }
-    const vv = wfeature.featureValues;
-    if (vv != null) {
-        if (typeof vv === "string") {
-            out.featureValues = myFromBase64(vv);
+    function makeTimestamp(seconds) {
+        if (seconds >= 0.0) {
+            return {
+                s: Math.floor(seconds),
+                n: Math.floor((seconds - Math.floor(seconds)) * 1e9 + 0.5)
+            };
         } else {
-            out.featureValues = new Float32Array(vv);
+            const { s, n } = makeTimestamp(-seconds);
+            return { s: -s, n: -n };
         }
     }
-    return out;
-}
 
-function convertWireFeatureList(wfeatures) {
-    return wfeatures.map(convertWireFeature);
-}
-
-function responseToFeatureSet(response) {
-    const features = new Map();
-    const processResponse = response.result;
-    const wireFeatures = processResponse.features;
-    Object.keys(wireFeatures).forEach(key => {
-        return features.set(key, convertWireFeatureList(wireFeatures[key]));
-    });
-    return features;
-}
-
-function test() {
-
-    const rate = 44100;
-    
-    comment("Loading zero crossings plugin...");
-    let result = request('{"method":"load","params": {"key":"' + pluginKey + '","inputSampleRate":' + rate + ',"adapterFlags":["AdaptAllSafe"]}}');
-
-    const blockSize = 1024;
-
-    result = request('{"method":"configure","params":{"handle":1,"configuration":{"blockSize": ' + blockSize + ', "channelCount": 1, "stepSize": ' + blockSize + '}}}');
-
-    const nblocks = 1000;
-
-    const makeBlock = (n => { 
-        return {
-            timestamp : frame2timestamp(n * blockSize, rate),
-            inputBuffers : [
-                new Float32Array(Array.from(Array(blockSize).keys(),
-                                            n => n / blockSize))
-            ],
-        }
-    });
-    
-    const blocks = Array.from(Array(nblocks).keys(), makeBlock);
-    
-    comment("Now processing " + nblocks + " blocks of 1024 samples each...");
-
-    let total = 0;
-    
-    let start = (new Date()).getTime();
-    comment("Start at " + start);
-    
-    for (let i = 0; i < nblocks; ++i) {
-	result = processRaw({
-	    "handle": 1,
-	    "processInput": blocks[i]
-	});
-        let features = responseToFeatureSet(result);
-        let count = features.get("counts")[0].featureValues[0];
-        total += count;
+    function frame2timestamp(frame, rate) {
+        return makeTimestamp(frame / rate);
     }
 
-    let finish = (new Date()).getTime();
-    comment("Finish at " + finish + " for a time of " + (finish - start) + " ms");
-
-    comment("Total = " + total);
-
-    comment("Again...");
-
-    total = 0;
-    
-    start = (new Date()).getTime();
-    comment("Start at " + start);
-    
-    for (let i = 0; i < nblocks; ++i) {
-	result = processRaw({
-	    "handle": 1,
-	    "processInput": blocks[i]
-	});
-        let features = responseToFeatureSet(result);
-        let count = features.get("counts")[0].featureValues[0];
-        total += count;
+    function request(jsonStr) {
+        note("Request JSON = " + jsonStr);
+        var m = extractorModule;
+        // Inspection reveals that intArrayFromString converts the string
+        // from utf16 to utf8, which is what we want (though the docs
+        // don't mention this). Note the *Cstr values are Emscripten heap
+        // pointers
+        var inCstr = m.allocate(m.intArrayFromString(jsonStr), 'i8', m.ALLOC_NORMAL);
+        var outCstr = piperRequestJson(inCstr);
+        m._free(inCstr);
+        var result = m.Pointer_stringify(outCstr);
+        piperFreeJson(outCstr);
+        note("Returned JSON = " + result);
+        return result;
     }
 
-    finish = (new Date()).getTime();
-    comment("Finish at " + finish + " for a time of " + (finish - start) + " ms");
-    
-    comment("Total = " + total);
-    
-    comment("Cleaning up the plugin and getting any remaining features...");
-    result = request('{"method":"finish","params":{"handle":1}}');
-}
+    function myFromBase64(b64) {
+        while (b64.length % 4 > 0) { b64 += "="; }
+        let conv = new Float32Array(base64.toByteArray(b64).buffer);
+        return conv;
+    }
 
-test();
+    function convertWireFeature(wfeature) {
+        let out = {};
+        if (wfeature.timestamp != null) {
+            out.timestamp = wfeature.timestamp;
+        }
+        if (wfeature.duration != null) {
+            out.duration = wfeature.duration;
+        }
+        if (wfeature.label != null) {
+            out.label = wfeature.label;
+        }
+        const vv = wfeature.featureValues;
+        if (vv != null) {
+            if (typeof vv === "string") {
+                out.featureValues = myFromBase64(vv);
+            } else {
+                out.featureValues = new Float32Array(vv);
+            }
+        }
+        return out;
+    }
 
+    function convertWireFeatureList(wfeatures) {
+        return wfeatures.map(convertWireFeature);
+    }
+
+    function responseToFeatureSet(response) {
+        const features = new Map();
+        const processResponse = response.result;
+        const wireFeatures = processResponse.features;
+        Object.keys(wireFeatures).forEach(key => {
+            return features.set(key, convertWireFeatureList(wireFeatures[key]));
+        });
+        return features;
+    }
+
+    function test(extractorModule) {
+
+        const rate = 44100;
+        
+        comment("Loading zero crossings plugin...");
+        let result = request('{"method":"load","params": {"key":"' + pluginKey + '","inputSampleRate":' + rate + ',"adapterFlags":["AdaptAllSafe"]}}');
+
+        const blockSize = 1024;
+
+        result = request('{"method":"configure","params":{"handle":1,"configuration":{"framing": { "blockSize": ' + blockSize + ', "stepSize": ' + blockSize + '}, "channelCount": 1 }}}');
+
+        const nblocks = 1000;
+
+        const makeBlock = (n => { 
+            return {
+                timestamp : frame2timestamp(n * blockSize, rate),
+                inputBuffers : [
+                    new Float32Array(Array.from(Array(blockSize).keys(),
+                                                n => n / blockSize))
+                ],
+            }
+        });
+        
+        const blocks = Array.from(Array(nblocks).keys(), makeBlock);
+        
+        comment("Now processing " + nblocks + " blocks of 1024 samples each...");
+
+        let total = 0;
+        
+        let start = (new Date()).getTime();
+        comment("Start at " + start);
+        
+        for (let i = 0; i < nblocks; ++i) {
+	    result = processRaw({
+	        "handle": 1,
+	        "processInput": blocks[i]
+	    });
+            let features = responseToFeatureSet(result);
+            let count = features.get("counts")[0].featureValues[0];
+            total += count;
+        }
+
+        let finish = (new Date()).getTime();
+        comment("Finish at " + finish + " for a time of " + (finish - start) + " ms");
+
+        comment("Total = " + total);
+
+        comment("Again...");
+
+        total = 0;
+        
+        start = (new Date()).getTime();
+        comment("Start at " + start);
+        
+        for (let i = 0; i < nblocks; ++i) {
+	    result = processRaw({
+	        "handle": 1,
+	        "processInput": blocks[i]
+	    });
+            let features = responseToFeatureSet(result);
+            let count = features.get("counts")[0].featureValues[0];
+            total += count;
+        }
+
+        finish = (new Date()).getTime();
+        comment("Finish at " + finish + " for a time of " + (finish - start) + " ms");
+        
+        comment("Total = " + total);
+        
+        comment("Cleaning up the plugin and getting any remaining features...");
+        result = request('{"method":"finish","params":{"handle":1}}');
+    }
+
+    test();
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/perf-test-wasm.html	Wed Jun 13 15:55:39 2018 +0100
@@ -0,0 +1,22 @@
+<html>
+  <head>
+    <meta charset="UTF-8">
+    <title>Piper Adapter Performance Test</title>
+
+    <style type="text/css">
+      body { margin: 5%; }
+      table, td, th { border: 0.1em solid #e0e0e0; border-collapse: collapse }
+      td, th { padding: 0.5em }
+    </style>
+
+    <script src="../examples/vamp-example-plugins/VampExamplePlugins_w.umd.js"></script>
+    <script src="base64.js"></script>
+    <script src="perf-test.js"></script>
+  </head>
+  <body>
+    <h3>Piper Adapter Performance Test</h3>
+
+    <p id="test-result"></p>
+    
+  </body>
+</html>
--- a/test/perf-test.html	Tue Jun 12 14:58:05 2018 +0100
+++ b/test/perf-test.html	Wed Jun 13 15:55:39 2018 +0100
@@ -1,7 +1,7 @@
 <html>
   <head>
     <meta charset="UTF-8">
-    <title>VamPipe Adapter Test</title>
+    <title>Piper Adapter Performance Test</title>
 
     <style type="text/css">
       body { margin: 5%; }
@@ -9,12 +9,12 @@
       td, th { padding: 0.5em }
     </style>
 
-    <script src="VampExamplePlugins.js"></script>
+    <script src="../examples/vamp-example-plugins/VampExamplePlugins.umd.js"></script>
     <script src="base64.js"></script>
     <script src="perf-test.js"></script>
   </head>
   <body>
-    <h3>VamPipe Adapter Test</h3>
+    <h3>Piper Adapter Performance Test</h3>
 
     <p id="test-result"></p>
     
--- a/test/perf-test.js	Tue Jun 12 14:58:05 2018 +0100
+++ b/test/perf-test.js	Wed Jun 13 15:55:39 2018 +0100
@@ -1,220 +1,223 @@
 "use strict";
 
-var exampleModule = VampExamplePluginsModule();
-
-// It is possible to declare both parameters and return values as
-// "string", in which case Emscripten will take care of
-// conversions. But it's not clear how one would manage memory for
-// newly-constructed returned C strings -- the returned pointer from
-// piperRequestJson would appear (?) to be thrown away by the
-// Emscripten string converter if we declare it as returning a string,
-// so we have no opportunity to pass it to piperFreeJson, which
-// suggests this would leak memory if the string isn't static. Not
-// wholly sure though. Anyway, passing and returning pointers (as
-// numbers) means we can manage the Emscripten heap memory however we
-// want in our request wrapper function below.
-
-var piperRequestJson = exampleModule.cwrap(
-    'piperRequestJson', 'number', ['number']
-);
-
-var piperProcessRaw = exampleModule.cwrap(
-    "piperProcessRaw", "number", ["number", "number", "number", "number"]
-);
-
-var piperFreeJson = exampleModule.cwrap(
-    'piperFreeJson', 'void', ['number']
-);
-
-function note(blah) {
-    document.getElementById("test-result").innerHTML += blah + "<br>";
-}
-
-function comment(blah) {
-    note("<br><i>" + blah + "</i>");
-}
-
-function processRaw(request) {
-    
-    const nChannels = request.processInput.inputBuffers.length;
-    const nFrames = request.processInput.inputBuffers[0].length;
-
-    const buffersPtr = exampleModule._malloc(nChannels * 4);
-    const buffers = new Uint32Array(
-        exampleModule.HEAPU8.buffer, buffersPtr, nChannels);
-
-    for (let i = 0; i < nChannels; ++i) {
-        const framesPtr = exampleModule._malloc(nFrames * 4);
-        const frames = new Float32Array(
-            exampleModule.HEAPU8.buffer, framesPtr, nFrames);
-        frames.set(request.processInput.inputBuffers[i]);
-        buffers[i] = framesPtr;
-    }
-    
-    const responseJson = piperProcessRaw(
-        request.handle,
-        buffersPtr,
-        request.processInput.timestamp.s,
-        request.processInput.timestamp.n);
-    
-    for (let i = 0; i < nChannels; ++i) {
-        exampleModule._free(buffers[i]);
-    }
-    exampleModule._free(buffersPtr);
-
-    const responseJstr = exampleModule.Pointer_stringify(responseJson);
-    const response = JSON.parse(responseJstr);
-    
-    piperFreeJson(responseJson);
-    
-    return response;
-}
-
-function makeTimestamp(seconds) {
-    if (seconds >= 0.0) {
-        return {
-            s: Math.floor(seconds),
-            n: Math.floor((seconds - Math.floor(seconds)) * 1e9 + 0.5)
-        };
-    } else {
-        const { s, n } = makeTimestamp(-seconds);
-        return { s: -s, n: -n };
-    }
-}
-
-function frame2timestamp(frame, rate) {
-    return makeTimestamp(frame / rate);
-}
-
-function request(jsonStr) {
-    note("Request JSON = " + jsonStr);
-    var m = exampleModule;
-    // Inspection reveals that intArrayFromString converts the string
-    // from utf16 to utf8, which is what we want (though the docs
-    // don't mention this). Note the *Cstr values are Emscripten heap
-    // pointers
-    var inCstr = m.allocate(m.intArrayFromString(jsonStr), 'i8', m.ALLOC_NORMAL);
-    var outCstr = piperRequestJson(inCstr);
-    m._free(inCstr);
-    var result = m.Pointer_stringify(outCstr);
-    piperFreeJson(outCstr);
-    note("Returned JSON = " + result);
-    return result;
-}
-
-function myFromBase64(b64) {
-    while (b64.length % 4 > 0) { b64 += "="; }
-    let conv = new Float32Array(toByteArray(b64).buffer);
-    return conv;
-}
-
-function convertWireFeature(wfeature) {
-    let out = {};
-    if (wfeature.timestamp != null) {
-        out.timestamp = wfeature.timestamp;
-    }
-    if (wfeature.duration != null) {
-        out.duration = wfeature.duration;
-    }
-    if (wfeature.label != null) {
-        out.label = wfeature.label;
-    }
-    const vv = wfeature.featureValues;
-    if (vv != null) {
-        if (typeof vv === "string") {
-            out.featureValues = myFromBase64(vv);
-        } else {
-            out.featureValues = new Float32Array(vv);
-        }
-    }
-    return out;
-}
-
-function convertWireFeatureList(wfeatures) {
-    return wfeatures.map(convertWireFeature);
-}
-
-function responseToFeatureSet(response) {
-    const features = new Map();
-    const processResponse = response.result;
-    const wireFeatures = processResponse.features;
-    Object.keys(wireFeatures).forEach(key => {
-        return features.set(key, convertWireFeatureList(wireFeatures[key]));
-    });
-    return features;
-}
-
 function test() {
 
-    const rate = 44100;
-    
-    comment("Loading zero crossings plugin...");
-    let result = request('{"method":"load","params": {"key":"vamp-example-plugins:zerocrossing","inputSampleRate":' + rate + ',"adapterFlags":["AdaptAllSafe"]}}');
+    VampExamplePluginsModule().then(function(exampleModule) {
+        
+        // It is possible to declare both parameters and return values
+        // as "string", in which case Emscripten will take care of
+        // conversions. But it's not clear how one would manage memory
+        // for newly-constructed returned C strings -- the returned
+        // pointer from piperRequestJson would appear (?) to be thrown
+        // away by the Emscripten string converter if we declare it as
+        // returning a string, so we have no opportunity to pass it to
+        // piperFreeJson, which suggests this would leak memory if the
+        // string isn't static. Not wholly sure though. Anyway,
+        // passing and returning pointers (as numbers) means we can
+        // manage the Emscripten heap memory however we want in our
+        // request wrapper function below.
 
-    const blockSize = 1024;
+        var piperRequestJson = exampleModule.cwrap(
+            'piperRequestJson', 'number', ['number']
+        );
 
-    result = request('{"method":"configure","params":{"handle":1,"configuration":{"blockSize": ' + blockSize + ', "channelCount": 1, "stepSize": ' + blockSize + '}}}');
+        var piperProcessRaw = exampleModule.cwrap(
+            "piperProcessRaw", "number", ["number", "number", "number", "number"]
+        );
 
-    const nblocks = 1000;
+        var piperFreeJson = exampleModule.cwrap(
+            'piperFreeJson', 'void', ['number']
+        );
 
-    const makeBlock = (n => { 
-        return {
-            timestamp : frame2timestamp(n * blockSize, rate),
-            inputBuffers : [
-                new Float32Array(Array.from(Array(blockSize).keys(),
-                                            n => n / blockSize))
-            ],
+        function note(blah) {
+            document.getElementById("test-result").innerHTML += blah + "<br>";
         }
+
+        function comment(blah) {
+            note("<br><i>" + blah + "</i>");
+        }
+
+        function processRaw(request) {
+            
+            const nChannels = request.processInput.inputBuffers.length;
+            const nFrames = request.processInput.inputBuffers[0].length;
+
+            const buffersPtr = exampleModule._malloc(nChannels * 4);
+            const buffers = new Uint32Array(
+                exampleModule.HEAPU8.buffer, buffersPtr, nChannels);
+
+            for (let i = 0; i < nChannels; ++i) {
+                const framesPtr = exampleModule._malloc(nFrames * 4);
+                const frames = new Float32Array(
+                    exampleModule.HEAPU8.buffer, framesPtr, nFrames);
+                frames.set(request.processInput.inputBuffers[i]);
+                buffers[i] = framesPtr;
+            }
+            
+            const responseJson = piperProcessRaw(
+                request.handle,
+                buffersPtr,
+                request.processInput.timestamp.s,
+                request.processInput.timestamp.n);
+            
+            for (let i = 0; i < nChannels; ++i) {
+                exampleModule._free(buffers[i]);
+            }
+            exampleModule._free(buffersPtr);
+
+            const responseJstr = exampleModule.Pointer_stringify(responseJson);
+            const response = JSON.parse(responseJstr);
+            
+            piperFreeJson(responseJson);
+            
+            return response;
+        }
+
+        function makeTimestamp(seconds) {
+            if (seconds >= 0.0) {
+                return {
+                    s: Math.floor(seconds),
+                    n: Math.floor((seconds - Math.floor(seconds)) * 1e9 + 0.5)
+                };
+            } else {
+                const { s, n } = makeTimestamp(-seconds);
+                return { s: -s, n: -n };
+            }
+        }
+
+        function frame2timestamp(frame, rate) {
+            return makeTimestamp(frame / rate);
+        }
+
+        function request(jsonStr) {
+            note("Request JSON = " + jsonStr);
+            var m = exampleModule;
+            // Inspection reveals that intArrayFromString converts the string
+            // from utf16 to utf8, which is what we want (though the docs
+            // don't mention this). Note the *Cstr values are Emscripten heap
+            // pointers
+            var inCstr = m.allocate(m.intArrayFromString(jsonStr), 'i8', m.ALLOC_NORMAL);
+            var outCstr = piperRequestJson(inCstr);
+            m._free(inCstr);
+            var result = m.Pointer_stringify(outCstr);
+            piperFreeJson(outCstr);
+            note("Returned JSON = " + result);
+            return result;
+        }
+
+        function myFromBase64(b64) {
+            while (b64.length % 4 > 0) { b64 += "="; }
+            let conv = new Float32Array(toByteArray(b64).buffer);
+            return conv;
+        }
+
+        function convertWireFeature(wfeature) {
+            let out = {};
+            if (wfeature.timestamp != null) {
+                out.timestamp = wfeature.timestamp;
+            }
+            if (wfeature.duration != null) {
+                out.duration = wfeature.duration;
+            }
+            if (wfeature.label != null) {
+                out.label = wfeature.label;
+            }
+            const vv = wfeature.featureValues;
+            if (vv != null) {
+                if (typeof vv === "string") {
+                    out.featureValues = myFromBase64(vv);
+                } else {
+                    out.featureValues = new Float32Array(vv);
+                }
+            }
+            return out;
+        }
+
+        function convertWireFeatureList(wfeatures) {
+            return wfeatures.map(convertWireFeature);
+        }
+
+        function responseToFeatureSet(response) {
+            const features = new Map();
+            const processResponse = response.result;
+            const wireFeatures = processResponse.features;
+            Object.keys(wireFeatures).forEach(key => {
+                return features.set(key, convertWireFeatureList(wireFeatures[key]));
+            });
+            return features;
+        }
+
+        const rate = 44100;
+        
+        comment("Loading zero crossings plugin...");
+        let result = request('{"method":"load","params": {"key":"vamp-example-plugins:zerocrossing","inputSampleRate":' + rate + ',"adapterFlags":["AdaptAllSafe"]}}');
+
+        const blockSize = 1024;
+
+        comment("Configuring plugin...");
+        result = request('{"method":"configure","params":{"handle":1,"configuration":{"framing": { "blockSize": ' + blockSize + ', "stepSize": ' + blockSize + '}, "channelCount": 1}}}');
+
+        const nblocks = 1000;
+
+        const makeBlock = (n => { 
+            return {
+                timestamp : frame2timestamp(n * blockSize, rate),
+                inputBuffers : [
+                    new Float32Array(Array.from(Array(blockSize).keys(),
+                                                n => n / blockSize))
+                ],
+            }
+        });
+        
+        const blocks = Array.from(Array(nblocks).keys(), makeBlock);
+        
+        comment("Now processing " + nblocks + " blocks of 1024 samples each...");
+
+        let total = 0;
+        
+        let start = (new Date()).getTime();
+        comment("Start at " + start);
+        
+        for (let i = 0; i < nblocks; ++i) {
+	    result = processRaw({
+	        "handle": 1,
+	        "processInput": blocks[i]
+	    });
+            let features = responseToFeatureSet(result);
+            let count = features.get("counts")[0].featureValues[0];
+            total += count;
+        }
+
+        let finish = (new Date()).getTime();
+        comment("Finish at " + finish + " for a time of " + (finish - start) + " ms");
+
+        comment("Total = " + total);
+
+        comment("Again...");
+
+        total = 0;
+        
+        start = (new Date()).getTime();
+        comment("Start at " + start);
+        
+        for (let i = 0; i < nblocks; ++i) {
+	    result = processRaw({
+	        "handle": 1,
+	        "processInput": blocks[i]
+	    });
+            let features = responseToFeatureSet(result);
+            let count = features.get("counts")[0].featureValues[0];
+            total += count;
+        }
+
+        finish = (new Date()).getTime();
+        comment("Finish at " + finish + " for a time of " + (finish - start) + " ms");
+        
+        comment("Total = " + total);
+        
+        comment("Cleaning up the plugin and getting any remaining features...");
+        result = request('{"method":"finish","params":{"handle":1}}');
     });
-    
-    const blocks = Array.from(Array(nblocks).keys(), makeBlock);
-    
-    comment("Now processing " + nblocks + " blocks of 1024 samples each...");
-
-    let total = 0;
-    
-    let start = (new Date()).getTime();
-    comment("Start at " + start);
-    
-    for (let i = 0; i < nblocks; ++i) {
-	result = processRaw({
-	    "handle": 1,
-	    "processInput": blocks[i]
-	});
-        let features = responseToFeatureSet(result);
-        let count = features.get("counts")[0].featureValues[0];
-        total += count;
-    }
-
-    let finish = (new Date()).getTime();
-    comment("Finish at " + finish + " for a time of " + (finish - start) + " ms");
-
-    comment("Total = " + total);
-
-    comment("Again...");
-
-    total = 0;
-    
-    start = (new Date()).getTime();
-    comment("Start at " + start);
-    
-    for (let i = 0; i < nblocks; ++i) {
-	result = processRaw({
-	    "handle": 1,
-	    "processInput": blocks[i]
-	});
-        let features = responseToFeatureSet(result);
-        let count = features.get("counts")[0].featureValues[0];
-        total += count;
-    }
-
-    finish = (new Date()).getTime();
-    comment("Finish at " + finish + " for a time of " + (finish - start) + " ms");
-    
-    comment("Total = " + total);
-    
-    comment("Cleaning up the plugin and getting any remaining features...");
-    result = request('{"method":"finish","params":{"handle":1}}');
 }
 
 window.onload = function() {
--- a/test/quick-test.js	Tue Jun 12 14:58:05 2018 +0100
+++ b/test/quick-test.js	Wed Jun 13 15:55:39 2018 +0100
@@ -1,27 +1,5 @@
 "use strict";
 
-var exampleModule = VampExamplePluginsModule();
-
-// It is possible to declare both parameters and return values as
-// "string", in which case Emscripten will take care of
-// conversions. But it's not clear how one would manage memory for
-// newly-constructed returned C strings -- the returned pointer from
-// piperRequestJson would appear (?) to be thrown away by the
-// Emscripten string converter if we declare it as returning a string,
-// so we have no opportunity to pass it to piperFreeJson, which
-// suggests this would leak memory if the string isn't static. Not
-// wholly sure though. Anyway, passing and returning pointers (as
-// numbers) means we can manage the Emscripten heap memory however we
-// want in our request wrapper function below.
-
-var piperRequestJson = exampleModule.cwrap(
-    'piperRequestJson', 'number', ['number']
-);
-
-var piperFreeJson = exampleModule.cwrap(
-    'piperFreeJson', 'void', ['number']
-);
-
 function note(blah) {
     document.getElementById("test-result").innerHTML += blah + "<br>";
 }
@@ -30,45 +8,69 @@
     note("<br><i>" + blah + "</i>");
 }
 
-function request(jsonStr) {
-    note("Request JSON = " + jsonStr);
-    var m = exampleModule;
-    // Inspection reveals that intArrayFromString converts the string
-    // from utf16 to utf8, which is what we want (though the docs
-    // don't mention this). Note the *Cstr values are Emscripten heap
-    // pointers
-    var inCstr = m.allocate(m.intArrayFromString(jsonStr), 'i8', m.ALLOC_NORMAL);
-    var outCstr = piperRequestJson(inCstr);
-    m._free(inCstr);
-    var result = m.Pointer_stringify(outCstr);
-    piperFreeJson(outCstr);
-    note("Returned JSON = " + result);
-    return result;
-}
-
 function test() {
 
-    comment("Querying plugin list...");
-    var result = request('{"method": "list"}');
+    VampExamplePluginsModule().then(function(exampleModule) {
+        
+        // It is possible to declare both parameters and return values
+        // as "string", in which case Emscripten will take care of
+        // conversions. But it's not clear how one would manage memory
+        // for newly-constructed returned C strings -- the returned
+        // pointer from piperRequestJson would appear (?) to be thrown
+        // away by the Emscripten string converter if we declare it as
+        // returning a string, so we have no opportunity to pass it to
+        // piperFreeJson, which suggests this would leak memory if the
+        // string isn't static. Not wholly sure though. Anyway,
+        // passing and returning pointers (as numbers) means we can
+        // manage the Emscripten heap memory however we want in our
+        // request wrapper function below.
 
-    comment("Loading zero crossings plugin...");
-    result = request('{"method":"load","params": {"key":"vamp-example-plugins:powerspectrum","inputSampleRate":16,"adapterFlags":["AdaptAllSafe"]}}');
+        var piperRequestJson = exampleModule.cwrap(
+            'piperRequestJson', 'number', ['number']
+        );
+        
+        var piperFreeJson = exampleModule.cwrap(
+            'piperFreeJson', 'void', ['number']
+        );
 
-    comment("I'm now assuming that the load succeeded and the returned handle was 1. I haven't bothered to parse the JSON. If those assumptions are wrong, this obviously isn't going to work. Configuring the plugin...");
-    result = request('{"method":"configure","params":{"handle":1,"configuration":{"blockSize": 8, "channelCount": 1, "stepSize": 8}}}');
+        function request(jsonStr) {
+            note("Request JSON = " + jsonStr);
+            var m = exampleModule;
+            // Inspection reveals that intArrayFromString converts the
+            // string from utf16 to utf8, which is what we want
+            // (though the docs don't mention this). Note the *Cstr
+            // values are Emscripten heap pointers
+            var inCstr = m.allocate(m.intArrayFromString(jsonStr), 'i8', m.ALLOC_NORMAL);
+            var outCstr = piperRequestJson(inCstr);
+            m._free(inCstr);
+            var result = m.Pointer_stringify(outCstr);
+            piperFreeJson(outCstr);
+            note("Returned JSON = " + result);
+            return result;
+        }
 
-    comment("If I try to configure it again, it should fail because it's already configured... but this doesn't change anything, and subsequent processing should work fine. Just an example of a failure call. NB this only works if Emscripten has exception catching enabled -- it's off by default in opt builds, which would just end the script here. Wonder what the performance penalty is like.");
-    result = request('{"method":"configure","params":{"handle":1,"configuration":{"blockSize": 8, "channelCount": 1, "stepSize": 8}}}');
+        comment("Querying plugin list...");
+        var result = request('{"method": "list"}');
 
-    comment("Now processing a couple of blocks of data, on the same assumptions...");
-    result = request('{"method":"process","params":{"handle":1,"processInput":{"timestamp":{"s":0,"n":0},"inputBuffers":[[0,1,-1,0,1,-1,0,1]]}}}');
-    result = request('{"method":"process","params":{"handle":1,"processInput":{"timestamp":{"s":0,"n":500000000},"inputBuffers":[[0,1,-1,0,1,-1,0,1]]}}}');
+        comment("Loading zero crossings plugin...");
+        result = request('{"method":"load","params": {"key":"vamp-example-plugins:powerspectrum","inputSampleRate":16,"adapterFlags":["AdaptAllSafe"]}}');
 
-    comment("Cleaning up the plugin and getting any remaining features...");
-    result = request('{"method":"finish","params":{"handle":1}}');
+        comment("I'm now assuming that the load succeeded and the returned handle was 1. I haven't bothered to parse the JSON. If those assumptions are wrong, this obviously isn't going to work. Configuring the plugin...");
+        result = request('{"method":"configure","params":{"handle":1,"configuration":{"framing": { "blockSize": 8, "stepSize": 8 }, "channelCount": 1 }}}');
 
-    comment("A process call should now fail, as the plugin has been cleaned up.");
-    result = request('{"method":"process","params":{"handle":1,"processInput":{"timestamp":{"s":0,"n":1000000000},"inputBuffers":[{"values":[0,1,-1,0,1,-1,0,1]}]}}}');
+        comment("If I try to configure it again, it should fail because it's already configured... but this doesn't change anything, and subsequent processing should work fine. Just an example of a failure call. NB this only works if Emscripten has exception catching enabled -- it's off by default in opt builds, which would just end the script here. Wonder what the performance penalty is like.");
+        result = request('{"method":"configure","params":{"handle":1,"configuration":{"framing": { "blockSize": 8, "stepSize": 8 }, "channelCount": 1 }}}');
+
+        comment("Now processing a couple of blocks of data, on the same assumptions...");
+        result = request('{"method":"process","params":{"handle":1,"processInput":{"timestamp":{"s":0,"n":0},"inputBuffers":[[0,1,-1,0,1,-1,0,1]]}}}');
+        result = request('{"method":"process","params":{"handle":1,"processInput":{"timestamp":{"s":0,"n":500000000},"inputBuffers":[[0,1,-1,0,1,-1,0,1]]}}}');
+
+        comment("Cleaning up the plugin and getting any remaining features...");
+        result = request('{"method":"finish","params":{"handle":1}}');
+
+        comment("A process call should now fail, as the plugin has been cleaned up.");
+        result = request('{"method":"process","params":{"handle":1,"processInput":{"timestamp":{"s":0,"n":1000000000},"inputBuffers":[{"values":[0,1,-1,0,1,-1,0,1]}]}}}');
+    });
 }
 
 window.onload = function() {