Skip to content

Commit

Permalink
Flatten sfz includes before parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
kmturley committed Feb 18, 2024
1 parent e462c49 commit 7d6ccbc
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 104 deletions.
8 changes: 4 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ async function run() {
const sfzYaml = file.fileReadString(`${sfzDir}${sfzFile}.yaml`);

// Api testing
// const sfzDir = 'https://raw.githubusercontent.com/kmturley/karoryfer.black-and-green-guitars/main/Programs/';
// const sfzFile = '01-green_keyswitch';
// const sfzJs = await api.apiJson(`${sfzDir}${sfzFile}.json`);
// const sfzDir = 'https://raw.githubusercontent.com/studiorack/salamander-grand-piano/compact/';
// const sfzFile = 'salamander-grand-piano';
// const sfzJs = await api.apiJson(`${sfzDir}${sfzFile}.sfz.json`);
// const sfzText = await api.apiText(`${sfzDir}${sfzFile}.sfz`);
// const sfzXml = await api.apiText(`${sfzDir}${sfzFile}.xml`);
// const sfzXml = await api.apiText(`${sfzDir}${sfzFile}.sfz.xml`);

// const parseSfz = await parse.parseSfz(sfzText, sfzDir);
// console.log('parseSfz', parseSfz);
Expand Down
91 changes: 40 additions & 51 deletions src/parse.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { apiText } from './api';
import {
ParseAttribute,
ParseHeader,
ParseHeaderNames,
ParseOpcode,
Expand All @@ -9,21 +8,18 @@ import {
} from './types/parse';
import { log, pathJoin } from './utils';

const skipCharacters: string[] = [' ', '\t', '\r', '\n'];
const endCharacters: string[] = [' ', '\r', '\n'];
const variables: any = {};
let fileReadString: any = apiText;

function parseDirective(input: string) {
return input.match(/(?<=")[^#"]+(?=")|[^ \r\n"]+/g) || [];
}

function parseEnd(contents: string, startAt: number) {
for (let index: number = startAt; index < contents.length; index++) {
const char: string = contents.charAt(index);
if (endCharacters.includes(char)) return index;
function parseDefines(contents: string) {
const defines: string[] | null = contents.match(/(?<=#define ).+(?=\r|\n)/g);
if (!defines) return contents;
for (const define of defines) {
log(define);
const val: string[] = define.split(' ');
variables[val[0]] = val[1];
}
return contents.length;
return contents;
}

function parseHeader(input: string) {
Expand Down Expand Up @@ -63,27 +59,28 @@ function parseHeaders(headers: ParseHeader[], prefix?: string) {
return regions;
}

async function parseIncludes(contents: string, prefix = '') {
contents = parseDefines(contents);
const includes: string[] | null = contents.match(/#include "(.+?)"/g);
if (!includes) return contents;
for (const include of includes) {
const includePaths: string[] | null = include.match(/(?<=")(.*?)(?=")/g);
if (!includePaths) continue;
if (includePaths[0].includes('$')) includePaths[0] = parseVariables(includePaths[0], variables);
const subcontent: any = await parseLoad(includePaths[0], prefix);
const subcontentFlat: string = await parseIncludes(subcontent, prefix);
contents = contents.replace(include, subcontentFlat);
}
return contents;
}

async function parseLoad(includePath: string, prefix: string) {
const pathJoined: string = pathJoin(prefix, includePath);
let file: string = '';
if (pathJoined.startsWith('http')) file = await apiText(pathJoined);
else if (fileReadString) file = fileReadString(pathJoined);
else file = await apiText(pathJoined);
return await parseSfz(file, prefix);
}

function parseOpcode(input: string) {
const output: ParseAttribute[] = [];
const labels: string[] = input.match(/\w+(?==)/g) || [];
const values: string[] = input.split(/\w+(?==)/g) || [];
values.forEach((val: string) => {
if (!val.length) return;
output.push({
name: labels[output.length],
value: val.trim().replace(/[='"]/g, ''),
});
});
return output;
return file;
}

function parseOpcodeObject(opcodes: ParseOpcode[]) {
Expand All @@ -98,10 +95,6 @@ function parseOpcodeObject(opcodes: ParseOpcode[]) {
return properties;
}

function parseSetLoader(func: any) {
fileReadString = func;
}

function parseSanitize(contents: string) {
// Remove comments.
contents = contents.replace(/\/\*[\s\S]*?\*\/|(?<=[^:])\/\/.*|^\/\/.*/g, '');
Expand All @@ -117,30 +110,25 @@ function parseSanitize(contents: string) {

function parseSegment(segment: string) {
if (segment.includes('"')) segment = segment.replace(/"/g, '');
if (segment.includes('$')) segment = parseVariables(segment, variables);
if (segment.includes('$')) console.log(segment);
segment = parseVariables(segment, variables);
return segment;
}

function parseSetLoader(func: any) {
fileReadString = func;
}

async function parseSfz(contents: string, prefix = '') {
let element: any = {};
let elements: ParseHeader[] = [];
const santized: string = parseSanitize(contents);
const elements: ParseHeader[] = [];
const contentsFlat: string = await parseIncludes(contents, prefix);
const santized: string = parseSanitize(contentsFlat);
const segments: string[] = santized.split(' ');
log(santized);
for (let i: number = 0; i < segments.length; i++) {
const segment: string = parseSegment(segments[i]);
if (segment.charAt(0) === '/') {
log('comment:', segment);
} else if (segment === '#include') {
const key: string = parseSegment(segments[i + 1]);
log('include:', key);
const val: any = await parseLoad(key, prefix);
if (element.elements && val.elements) {
element.elements = element.elements.concat(val.elements);
} else {
elements = elements.concat(val);
}
i += 1;
} else if (segment === '#define') {
const key: string = segments[i + 1];
const val: string = segments[i + 2];
Expand Down Expand Up @@ -178,22 +166,23 @@ async function parseSfz(contents: string, prefix = '') {
return element;
}

function parseVariables(input: string, variables: ParseVariables) {
for (let key in variables) {
function parseVariables(input: string, vars: ParseVariables) {
for (const key in vars) {
const regEx: RegExp = new RegExp('\\' + key, 'g');
input = input.replace(regEx, variables[key]);
input = input.replace(regEx, vars[key]);
}
return input;
}

export {
parseDirective,
parseEnd,
parseDefines,
parseHeader,
parseHeaders,
parseIncludes,
parseLoad,
parseOpcode,
parseOpcodeObject,
parseSanitize,
parseSegment,
parseSetLoader,
parseSfz,
parseVariables,
Expand Down
53 changes: 4 additions & 49 deletions tests/parse.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import {
parseDirective,
parseEnd,
parseDefines,
parseHeader,
parseHeaders,
parseIncludes,
parseLoad,
parseOpcode,
parseOpcodeObject,
parseSanitize,
parseSegment,
parseSetLoader,
parseSfz,
parseVariables,
Expand Down Expand Up @@ -84,15 +85,6 @@ test('parseSfz Hang-D-minor-20220330.sfz', async () => {
expect(convertToXml(await parseSfz(sfzText, sfzPath))).toEqual(normalizeLineEnds(sfzXml));
});

test('parseDirective', () => {
expect(parseDirective('#include "green/stac_tp.sfz"')).toEqual(['#include', 'green/stac_tp.sfz']);
expect(parseDirective('#include "Individual Patchs/In.sfz"')).toEqual(['#include', 'Individual Patchs/In.sfz']);
expect(parseDirective('#include "$directory/$filename.sfz"')).toEqual(['#include', '$directory/$filename.sfz']);
expect(parseDirective('#define $KICKKEY 36')).toEqual(['#define', '$KICKKEY', '36']);
expect(parseDirective('#define $filename region')).toEqual(['#define', '$filename', 'region']);
expect(parseDirective('#define $RETUNED C#0')).toEqual(['#define', '$RETUNED', 'C#0']);
});

test('parseHeader', () => {
expect(parseHeader('<region')).toEqual('region');
expect(parseHeader('< region')).toEqual('region');
Expand Down Expand Up @@ -180,31 +172,6 @@ test('parseHeaders', async () => {
});
});

test('parseOpcode', () => {
expect(parseOpcode('seq_position=3')).toEqual([{ name: 'seq_position', value: '3' }]);
expect(parseOpcode('seq_position=3 pitch_keycenter=50')).toEqual([
{ name: 'seq_position', value: '3' },
{ name: 'pitch_keycenter', value: '50' },
]);
expect(parseOpcode('region_label=01 sample=harmLA0.$EXT')).toEqual([
{ name: 'region_label', value: '01' },
{ name: 'sample', value: 'harmLA0.$EXT' },
]);
expect(parseOpcode('label_cc27="Release vol"')).toEqual([{ name: 'label_cc27', value: 'Release vol' }]);
expect(parseOpcode('label_cc27=Release vol')).toEqual([{ name: 'label_cc27', value: 'Release vol' }]);
expect(parseOpcode('apple=An Apple banana=\'A Banana\' carrot="A Carrot"')).toEqual([
{ name: 'apple', value: 'An Apple' },
{ name: 'banana', value: 'A Banana' },
{ name: 'carrot', value: 'A Carrot' },
]);
expect(parseOpcode('lokey=c5 hikey=c#5')).toEqual([
{ name: 'lokey', value: 'c5' },
{ name: 'hikey', value: 'c#5' },
]);
expect(parseOpcode('ampeg_hold=0.3')).toEqual([{ name: 'ampeg_hold', value: '0.3' }]);
expect(parseOpcode('ampeg_decay_oncc70=-1.2')).toEqual([{ name: 'ampeg_decay_oncc70', value: '-1.2' }]);
});

test('parseOpcodeObject', () => {
expect(
parseOpcodeObject([
Expand All @@ -226,15 +193,3 @@ test('parseVariables', () => {
expect(parseVariables('sample=harmLA0.$EXT', { $OTHER: 'other' })).toEqual('sample=harmLA0.$EXT');
expect(parseVariables('sample=harmLA0.$EXT', { $EXT: 'flac', $OTHER: 'other' })).toEqual('sample=harmLA0.flac');
});

// test('parseEnd', () => {
// const sfzHeader: string = `//----
// //
// // The <group> header
// //`;
// expect(parseEnd(sfzHeader, 0)).toEqual(6);
// expect(parseEnd(sfzHeader, 9)).toEqual(11);
// expect(parseEnd(sfzHeader, 14)).toEqual(35);
// expect(parseEnd('sample=example.wav key=c4 // will play', 0)).toEqual(26);
// expect(parseEnd('/// long release group, cc1 < 64', 0)).toEqual(32);
// });

0 comments on commit 7d6ccbc

Please sign in to comment.