Skip to content

Instantly share code, notes, and snippets.

@coder0107git
Last active February 20, 2024 00:03
Show Gist options
  • Save coder0107git/989b22e5e615338db2399204180d9156 to your computer and use it in GitHub Desktop.
Save coder0107git/989b22e5e615338db2399204180d9156 to your computer and use it in GitHub Desktop.
Supercollider synth def parser

About

A Supercollider synth def (scsyndef) file parser written in javascript.

Preview

The JSFiddle preview is here.

Resources

Sonic Pi has a lot of precompiled scsyndef files that can be found here. The synth sources are in the parent directory.

To-Do/Wish-List

  • Handle UGen inputs of constants better (e.g. replace the constant's referenences with their value).
@import url("https://cdn.jsdelivr.net/gh/highlightjs/highlight.js@main/src/styles/github-dark.css");
.hljs {
overflow-x: scroll;
}
<input type="file" accept=".scsyndef" oninput="window.run(this.files[0])">
<pre class="hljs"></pre>
<script src="https://cdn.jsdelivr.net/gh/gh-canon/stack-snippet-console@master/console.js"></script>
<script type="module">
import { Parser } from "https://esm.sh/binary-parser@2.2.1";
import * as prettier from "https://unpkg.com/prettier@3.1.0/standalone.mjs";
import prettierPluginBabel from "https://unpkg.com/prettier@3.1.0/plugins/babel.mjs";
import prettierPluginEstree from "https://unpkg.com/prettier@3.1.0/plugins/estree.mjs";
import hljs from "https://esm.sh/highlight.js/lib/core";
import javascript from "https://esm.sh/highlight.js/lib/languages/javascript";
hljs.registerLanguage("javascript", javascript);
const highlight = code => hljs.highlight(code, {language: "javascript"}).value;
const $r = name => Symbol.for(name);
console.log($r`:i`.toString())
window.onerror = e => alert(e.message)
window.run = async function(file) {
console.log("Hello?", Parser)
const pstring = new Parser()
.namely("pstring")
.nest(null, {
type: new Parser()
// This isn't edian depedent
.uint8("length", {
assert: val => val <= 255, // Must be 255 charechters or less
})
.string("text", {
length: "length",
}),
formatter: a => a.text
})
const versionDep = new Parser()
.namely("versionDep")
.nest(null, {
type: new Parser()
.choice("val", {
tag: "$root.version",
choices: {
1: new Parser().int16be("value"),
2: new Parser().int32be("value"),
},
}),
formatter: a => a.val.value,
});
//const log = tag => val => (console.log(tag, val), val);
let runs = 0;
// Build a Supercollider synth def Parser
// Based on spec: https://depts.washington.edu/dxscdoc/Help/Reference/Synth-Definition-File-Format.html
const SynthDefParser = new Parser()
.namely("top")
.useContextVars()
.endianness("big")
.string("fileMagicBytes", {
length: 4,
assert: "SCgf",
})
.int32("version", {
assert: x => x == 1 || x == 2,
})
.int16("defsLength") // Number of synths in this file
.array("synthDefs", {
length: "defsLength",
type: new Parser()
.endianness("big") // Set this to parse as big-edian numbers
.nest("name", { type: "pstring", }) // Name of the synthdef
// Constants
.nest("constantsLength", { type: "versionDep", }) // Number of constants
// The values of the constants
.array("constants", {
type: new Parser().floatbe("value"),
length: "constantsLength",
// Extract contents of unnecessary extra object created by the array parser
formatter: arr => arr.map(a => a.value),
})
// Synth params
.nest("numParamInitalValues", { type: "versionDep", })
.array("paramsInitalValues", {
type: new Parser().floatbe("value"),
length: "numParamInitalValues",
formatter: arr => arr.map(a => a.value),
})
// The synth parameter names
.nest("paramNamesLength", { type: "versionDep", })
.array("paramNames", {
length: "paramNamesLength",
type: new Parser()
.nest("name", { type: "pstring" })
.nest("index", { type: "versionDep", }), // Index in param array
// Puts the synth params in the correct order
formatter: arr => {
let a = new Array(arr.length);
arr.forEach(val => a[val.index] = val.name);
return a;
},
})
// UGens
.nest("numUGens", { type: "versionDep" })
.array("uGens", {
length: "numUGens",
type: new Parser()
.endianness("big")
.nest("name", { type: "pstring" })
.int8("calculationRate") // No clue what this does
.nest("inputsLength", { type: "versionDep", })
.nest("outputsLength", { type: "versionDep", })
.int16("specialIndex") // No clue what this does either
.array("inputs", {
length: "inputsLength",
type: new Parser()
// UGen index (or constant if x < 0)
.nest("uGenIndex", { type: "versionDep", })
// uGenIndex < 0
// ? indexOfConstant
// : uGenOutputIndex
.nest("index", { type: "versionDep", }),
})
.array("outputs", {
length: "outputsLength",
type: new Parser()
.int8("calculationRate"),
formatter: arr => arr.map(a => a.calculationRate)
})
})
.int16("numVariants")
.array("variants", {
length: "numVariants",
type: new Parser()
.nest("name", { type: "pstring" })
.array("initalValues", {
length: "name",
formatter: a => a.value,
type: new Parser()
.floatbe("value")
})
})
});
console.log("Yay! No errors in parser syntax!");
alert(SynthDefParser.getCode())
document.querySelector("pre").innerHTML = highlight(
await prettier.format(SynthDefParser.getCode(), {
parser: "babel",
plugins: [
prettierPluginBabel,
prettierPluginEstree,
],
})
);
let res = SynthDefParser.parse(new Uint8Array(await file.arrayBuffer()));
//res.synthDefs.forEach(synthDef => synthDef.uGens = "truncated")
console.log(res);
console.log("Done!")
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment