UNPKG

@oletizi/audio-tools

Version:

Monorepo for hardware sampler utilities and format parsers

179 lines (164 loc) 7.18 kB
import {parse} from 'yaml' import * as fs from 'fs/promises' const DEBUG = "false" interface Spec { name: string className: string headerOffset: number fields: { n: string, // name f?: string, // function name root l?: string, // label d: string, // description s?: number, // size in bytes; 1 if undefined t?: string // type; number if undefined }[] } const HEADER_START = 7; export async function readSpecs(file: string) { return parse((await fs.readFile(file)).toString()) } export function genImports() { return `// // GENERATED ${new Date()}. DO NOT EDIT. // import {byte2nibblesLE, bytes2numberLE, nibbles2byte, newClientOutput} from "@oletizi/sampler-lib" import {nextByte, akaiByte2String, string2AkaiBytes} from "@/utils/akai-utils.js" ` } export async function genInterface(spec: Spec) { let rv = `export interface ${spec.name} {\n` for (const field of spec.fields) { rv += ` ${field.n}: ${field.t ? field.t : 'number'} // ${field.d}\n` rv += ` ${field.n}Label: string\n` // rv += ` set${field.n}(header: ${spec.name}, v: ${field.t ? field.t : 'number'})\n` rv += `\n` } rv += ' raw: number[] // Raw sysex message data\n' rv += '}\n' return rv } export async function genClass(spec: Spec) { console.log(`Generating ${spec.className}...`) let rv = `export class ${spec.className} {\n` rv += ` private readonly device: Device\n` rv += ` private readonly header: ${spec.name}\n` rv += `\n` rv += ` constructor(device: Device, header: ${spec.name}) {\n` rv += ` this.device = device\n` rv += ` this.header = header\n` rv += ` }\n` rv += `\n` rv += ` getHeader(): ${spec.name} {\n` rv += ` return this.header\n` rv += ` }\n` rv += `\n` // rv += ` copy(): ${spec.className} {\n` // rv += ` const h = {} as ${spec.name}\n` // rv += ` parse${spec.name}(this.header.raw.map(i => i), ${spec.headerOffset}, h)\n` // rv += ` return new ${spec.className}(this.device, h)\n` // rv += ` }\n` // rv += `\n` rv += ' async save() {\n' rv += ` return this.device.sendRaw(this.header.raw)\n` rv += ' }\n' rv += `\n` for (const field of spec.fields) { if (field.f) { // const parseOffset = HEADER_START const fu = String(field.f).charAt(0).toUpperCase() + String(field.f).slice(1) const type = field.t ? field.t : 'number' rv += ` get${fu}(): ${type} { \n` rv += ` return this.header.${field.n}\n` rv += ` }\n` rv += ` set${fu}(v: ${type}) {\n` rv += ` const out = newClientOutput(${DEBUG}, 'set${fu}')\n` rv += ` ${writeFunctionName(spec, field)}(this.header, v)\n` rv += ` // this is dumb. parse should be able to read the raw data; but, it doesn't. You should change that.\n` rv += ` out.log('Parsing header from ${HEADER_START} with header offset: ${spec.headerOffset}')\n` rv += ` const tmp = this.header.raw.slice(${HEADER_START}, this.header.raw.length - 1)\n` rv += ` parse${spec.name}(tmp, ${spec.headerOffset}, this.header)\n` rv += ` }\n` rv += `\n` } } rv += '}\n\n' return rv } function writeFunctionName(spec: Spec, field: any) { return `${spec.name}_write${field.n}` } export async function genSetters(spec: Spec) { let rv = '' let offset = HEADER_START + spec.headerOffset * 2 for (const field of spec.fields) { const fname = writeFunctionName(spec, field) rv += `export function ${fname}(header: ${spec.name}, v: ${field.t ? field.t : 'number'}) {\n` rv += ` const out = newClientOutput(${DEBUG}, '${fname}')\n` rv += ` out.log('Offset: ' + ${offset})\n` if (field.t) { if (field.t === 'string') { // const data = string2AkaiBytes(name) // for (let i = offset, j = 0; i < offset + 12 * 2; i += 2, j++) { // const nibbles = byte2nibblesLE(data[j]) // header.raw[i] = nibbles[0] // header.raw[i + 1] = nibbles[1] // } rv += ` const data = string2AkaiBytes(v)\n` rv += ` for (let i = ${offset}, j = 0; i < ${offset} + 12 * 2; i += 2, j++) {\n` rv += ` const nibbles = byte2nibblesLE(data[j])\n` rv += ` header.raw[i] = nibbles[0]\n` rv += ` header.raw[i + 1] = nibbles[1]` rv += ` }\n` } else { rv += ` // IMPLEMENT ME for field: ${field.t}` } } else { // const d = byte2nibblesLE(polyphony) // header.raw[offset] = d[0] // header.raw[offset + 1] = d[1] rv += ` const d = byte2nibblesLE(v)\n` rv += ` header.raw[${offset}] = d[0]\n` rv += ` header.raw[${offset} + 1] = d[1]\n` } rv += `}\n\n` offset += field.s ? field.s * 2 : 2 } return rv } export async function genParser(spec: Spec) { let rv = `export function parse${spec.name}(data: number[], offset: number, o: ${spec.name}) {\n` rv += ` const out = newClientOutput(${DEBUG}, 'parse${spec.name}')\n` rv += ` const v = {value: 0, offset: offset * 2}\n\n` rv += ' let b: number[]\n' rv += ' function reloff() {\n' + ' // This calculates the current offset into the header data so it will match with the Akai sysex docs for sanity checking. See https://lakai.sourceforge.net/docs/s2800_sysex.html\n' + ' // As such, The math here is weird: \n' + ' // * Each offset "byte" in the docs is actually two little-endian nibbles, each of which take up a slot in the midi data array--hence v.offset /2 \n' + ' return (v.offset / 2)\n' + ' }\n\n' for (const field of spec.fields) { rv += ` // ${field.d}\n` rv += ` out.log('${field.n}: offset: ' + reloff())\n` if (field.l) { rv += ` o["${field.n}Label"] = "${field.l}"\n` } if (field.t) { rv += ` o.${field.n} = ''\n` + ' for (let i = 0; i < 12; i++) {\n' + ' nextByte(data, v)\n' + ` o.${field.n} += akaiByte2String([v.value])\n` + ` out.log('${field.n} at ' + i + ': ' + o.${field.n})` + ' }\n' } else { rv += ` b = []\n` rv += ` for (let i=0; i<${field.s ? field.s : 1}; i++) {\n` rv += ' b.push(nextByte(data, v).value)\n' rv += ` }\n` rv += ` o.${field.n} = bytes2numberLE(b)\n` } rv += '\n' } rv += '}' return rv }