Skip to content

Commit

Permalink
improved export dialogs for sointu and protracker (#98)
Browse files Browse the repository at this point in the history
* improved export dialogs for sointu and protracker

* wasm-git 0.0.11

* sointu editorcontroller test

* v0.0.39
  • Loading branch information
petersalomonsen authored Aug 21, 2023
1 parent 7d2255f commit d9aa076
Show file tree
Hide file tree
Showing 8 changed files with 822 additions and 41 deletions.
100 changes: 70 additions & 30 deletions wasmaudioworklet/editorcontroller.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@ import { compileWebAssemblySynth } from './synth1/browsersynthcompiler.js';
import { exportVideo, setupWebGL } from './visualizer/fragmentshader.js';
import { triggerDownload } from './common/filedownload.js';
import { decodeBufferFromPNG, encodeBufferAsPNG } from './common/png.js';
import { isSointuSong, getSointuWasm } from './sointu/playsointu.js';
import { isSointuSong, getSointuWasm, getSointuYaml } from './sointu/playsointu.js';

export let songsourceeditor;
export let synthsourceeditor;
export let shadersourceeditor;
let gitrepoconfig = null;

const SONG_MODE_WASM = 'WASM';
const SONG_MODE_SOINTU = 'sointu';
const SONG_MODE_PROTRACKER = 'protracker';

async function loadCodeMirror() {
await loadScript('https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.55.0/codemirror.min.js');
await loadScript('https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.55.0/mode/javascript/javascript.js');
Expand Down Expand Up @@ -166,11 +170,11 @@ export async function initEditor(componentRoot) {
let songmode = 'midi';
if (songsource.indexOf('SONGMODE=PROTRACKER') >= 0) {
// special mode: we are building an amiga protracker module
songmode = 'protracker';
songmode = SONG_MODE_PROTRACKER;
} else if (
songsource.indexOf('global.pattern_size_shift') > -1 ||
songsource.indexOf('global.bpm') > -1) {
songmode = 'WASM';
songmode = SONG_MODE_WASM;
window.insertRecording = () => insertRecording4klang(insertStringIntoEditor);
} else {
const synthSourceIsXML = synthsource.startsWith('<?xml');
Expand Down Expand Up @@ -250,8 +254,8 @@ export async function initEditor(componentRoot) {
);
triggerDownload(URL.createObjectURL(new Blob([wasmbytes], { type: "octet/stream" })), 'song.wasm');
} else if (
exportProject === EXPORT_MODE_MIDISYNTH_MULTIPART_WASM_LIB ||
exportProject === EXPORT_MODE_MIDISYNTH_MULTIPART_WASM_LIB_PNG) {
exportProject === EXPORT_MODE_MIDISYNTH_MULTIPART_WASM_LIB ||
exportProject === EXPORT_MODE_MIDISYNTH_MULTIPART_WASM_LIB_PNG) {
const multipartsequence = createMultipatternSequence();
const wasmbytes = await compileWebAssemblySynth(synthsource,
multipartsequence,
Expand All @@ -277,9 +281,9 @@ export async function initEditor(componentRoot) {
} else if (exportProject === 'video') {
await exportVideo(shadersource, eventlist);
} else if (exportProject === EXPORT_MODE_MIDIPARTS_JSON) {
const songParts = getSongParts();
const songParts = getSongParts();
triggerDownload(URL.createObjectURL(new Blob([JSON.stringify(songParts)],
{ type: "application/json" })), 'songmidiparts.json');
{ type: "application/json" })), 'songmidiparts.json');
}
}
toggleSpinner(false);
Expand All @@ -299,7 +303,7 @@ export async function initEditor(componentRoot) {
const patternToolsGlobal = createPatternToolsGlobal();
try {
window.WASM_SYNTH_LOCATION = null;
if (songmode === 'WASM') {
if (songmode == SONG_MODE_WASM) {
const songfunc = new Function(
['global'].concat(Object.keys(patternToolsGlobal)),
songsource);
Expand Down Expand Up @@ -333,40 +337,76 @@ export async function initEditor(componentRoot) {
// if not a precompiled wasm file available in WASM_SYNTH_LOCATION
toggleSpinner(true);

if (isSointuSong(song)) {
songmode = SONG_MODE_SOINTU;
toggleEditors('assemblyscripteditor', false);
}

if (exportProject) {
toggleSpinner(false);
exportProject = await modal(`
<h3>Select WASM module type to export</h3>
<p>
<form>
<label><input type="radio" name="exporttype" value="wasimain" checked="checked">Self executable WASI module</label><br />
<label><input type="radio" name="exporttype" value="libmodule">Library module</label><br />
</form>
</p>
<button onclick="getRootNode().result(null)">Cancel</button>
<button onclick="getRootNode().result(new FormData(getRootNode().querySelector('form')).get('exporttype'))">
Generate WASM module
</button>
`);
if (songmode == SONG_MODE_WASM) {
exportProject = await modal(`
<h3>Select WASM module type to export</h3>
<p>
<form>
<label><input type="radio" name="exporttype" value="wasimain" checked="checked">Self executable WASI module</label><br />
<label><input type="radio" name="exporttype" value="libmodule">Library module</label><br />
</form>
</p>
<button onclick="getRootNode().result(null)">Cancel</button>
<button onclick="getRootNode().result(new FormData(getRootNode().querySelector('form')).get('exporttype'))">
Generate WASM module
</button>
`);
} else if (songmode == SONG_MODE_SOINTU) {
exportProject = await modal(`
<h3>Select export type</h3>
<p>
<form>
<label><input type="radio" name="exporttype" value="wasmmodule" checked="checked">Wasm module</label><br />
<label><input type="radio" name="exporttype" value="sointuyaml">Sointu YAML</label><br />
</form>
</p>
<button onclick="getRootNode().result(null)">Cancel</button>
<button onclick="getRootNode().result(new FormData(getRootNode().querySelector('form')).get('exporttype'))">
Export
</button>
`);
} else if (songmode == SONG_MODE_PROTRACKER) {
exportProject = await modal(`
<h3>Select export type</h3>
<p>
<form>
<label><input type="radio" name="exporttype" value="protrackermodule" checked="checked">Protracker module</label><br />
</form>
</p>
<button onclick="getRootNode().result(null)">Cancel</button>
<button onclick="getRootNode().result(new FormData(getRootNode().querySelector('form')).get('exporttype'))">
Export
</button>
`);
}
toggleSpinner(true);
}

let synthwasm;
if (isSointuSong(song)) {

if (songmode == SONG_MODE_SOINTU) {
synthwasm = await getSointuWasm(song);
} else {
synthwasm = await compileWebAssemblySynth(synthsource,
exportProject && songmode === 'WASM' ? song : undefined,
songmode === 'protracker' ? 55856 :
exportProject && songmode === SONG_MODE_WASM ? song : undefined,
songmode === SONG_MODE_PROTRACKER ? 55856 :
new AudioContext().sampleRate,
exportProject
);
}

if (synthwasm) {
if (exportProject) {
triggerDownload(URL.createObjectURL(new Blob([synthwasm], {type: 'application/octet-stream'})), 'song.wasm');
if (exportProject == 'sointuyaml') {
triggerDownload(URL.createObjectURL(new Blob([await getSointuYaml(song)], { type: 'application/yaml' })), 'song.yaml');
} if (exportProject) {
triggerDownload(URL.createObjectURL(new Blob([synthwasm], { type: 'application/octet-stream' })), 'song.wasm');
} else {
window.WASM_SYNTH_BYTES = synthwasm;
webassemblySynthUpdated = true;
Expand All @@ -382,7 +422,7 @@ export async function initEditor(componentRoot) {
toggleSpinner(false);
console.log('song mode', songmode);

if (songmode === 'protracker') {
if (songmode === SONG_MODE_PROTRACKER) {
const songworker = new Worker(
URL.createObjectURL(new Blob([
songsource.split("from './lib/").join(`from '${location.origin}${location.pathname === '/' ? '' : location.pathname}/synth1/modformat/lib/`)
Expand All @@ -397,7 +437,7 @@ export async function initEditor(componentRoot) {
const song = await modreciever;
if (exportProject) {
const linkElement = document.createElement('a');
linkElement.href = URL.createObjectURL(new Blob([song.modbytes], {type: 'application/octet-stream'}));
linkElement.href = URL.createObjectURL(new Blob([song.modbytes], { type: 'application/octet-stream' }));
linkElement.download = `${song.name.replace(/[^A-Za-z0-9]+/g, '_').toLowerCase()}.mod`;
linkElement.click();
}
Expand Down
112 changes: 112 additions & 0 deletions wasmaudioworklet/editorcontroller.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { waitForAppReady } from './app.js';
import { songsourceeditor, synthsourceeditor } from './editorcontroller.js';
import { commitAndSyncRemote, log } from './wasmgit/wasmgitclient.js';
import { songsource as sointutestsong, expectedYaml } from './sointu/sointutestsong.js';

describe('editorcontroller', async function () {
this.timeout(30000);
Expand Down Expand Up @@ -229,6 +230,117 @@ export function mixernext(leftSampleBufferPtr: usize, rightSampleBufferPtr: usiz
});
});


describe('editorcontroller with sointu source', async function () {
this.timeout(30000);
this.beforeAll(async () => {
document.documentElement.appendChild(document.createElement('app-javascriptmusic'));
await waitForAppReady();
});
this.afterAll(async () => {
window.stopaudio();
window.audioworkletnode = undefined;
document.documentElement.removeChild(document.querySelector('app-javascriptmusic'));
});

const songsource = sointutestsong;

it('should compile sointu test song', async () => {
songsourceeditor.doc.setValue(songsource);
synthsourceeditor.doc.setValue('');
const appElement = document.getElementsByTagName('app-javascriptmusic')[0].shadowRoot;
let audioWorkletMessage;
window.audioworkletnode = {
port: {
postMessage: msg => audioWorkletMessage = msg
},
context: {
sampleRate: 44100
}
};
appElement.querySelector('#savesongbutton').click();
while (!audioWorkletMessage) {
await new Promise((resolve) => setTimeout(() => resolve(), 1000));
}
assert.equal(localStorage.getItem('storedsynthcode'), '');
assert.equal(localStorage.getItem('storedsongcode'), songsource);
assert.equal(audioWorkletMessage.song.instrumentPatternLists.length, 5);
assert.equal(audioWorkletMessage.song.patterns.length, 4);
assert.equal(appElement.getElementById('assemblyscripteditor').style.display, 'none');
});

it('should compile and export song to sointu yaml', async () => {
songsourceeditor.doc.setValue(songsource);
synthsourceeditor.doc.setValue('');
const appElement = document.getElementsByTagName('app-javascriptmusic')[0].shadowRoot;
let audioWorkletMessage;
window.audioworkletnode = {
port: {
postMessage: msg => audioWorkletMessage = msg
},
context: {
sampleRate: 44100
}
};
const downloadPromise = new Promise(resolve => {
document._createElement = document.createElement;
document.createElement = function (elementName, options) {
const elm = this._createElement(elementName, options);
if (elementName === 'a') {
elm.click = () => resolve(elm.href);
} else if (elementName === 'common-modal') {
elm.shadowRoot.result('sointuyaml');
}
return elm;
}
});

appElement.querySelector('#exportbutton').click();
const url = await downloadPromise;

const sointuyaml = await fetch(url).then(r => r.text());

assert.equal(sointuyaml, expectedYaml);
document.createElement = document._createElement;
});
it('should compile and export song to wasm with lib functions exported', async () => {
songsourceeditor.doc.setValue(songsource);
synthsourceeditor.doc.setValue('');
const appElement = document.getElementsByTagName('app-javascriptmusic')[0].shadowRoot;
let audioWorkletMessage;
window.audioworkletnode = {
port: {
postMessage: msg => audioWorkletMessage = msg
},
context: {
sampleRate: 44100
}
};
const downloadPromise = new Promise(resolve => {
document._createElement = document.createElement;
document.createElement = function (elementName, options) {
const elm = this._createElement(elementName, options);

if (elementName === 'a') {
elm.click = () => resolve(elm.href);
} else if (elementName === 'common-modal') {
elm.shadowRoot.result('wasmmodule');
}
return elm;
}
});

appElement.querySelector('#exportbutton').click();
const url = await downloadPromise;

const wasmbinary = await fetch(url).then(r => r.arrayBuffer());

assert.isAbove(wasmbinary.byteLength, 1000);
assert.isDefined((await WebAssembly.instantiate(wasmbinary)).instance.exports.render_128_samples);
document.createElement = document._createElement;
});
});

describe('editorcontroller with git', async function () {
this.timeout(60000);
const gitrepo = 'test5512331';
Expand Down
4 changes: 2 additions & 2 deletions wasmaudioworklet/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion wasmaudioworklet/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "wasm-music",
"description": "Javascript/WebAssembly live coding environment for music and synthesis",
"version": "0.0.38",
"version": "0.0.39",
"repository": {
"url": "https://github.com/petersalomonsen/javascriptmusic"
},
Expand Down
7 changes: 5 additions & 2 deletions wasmaudioworklet/sointu/playsointu.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export function isSointuSong(song) {
return song.instruments.findIndex(instr => instr.sointu) > -1;
}

export async function getSointuWasm(song) {
export async function getSointuYaml(song) {
if (!scriptspromise) {
globalThis.exports = {};
scriptspromise = loadScript('https://cdn.jsdelivr.net/npm/[email protected]/index.js');
Expand Down Expand Up @@ -66,13 +66,16 @@ export async function getSointuWasm(song) {
},
patch: song.instruments.map(instr => instr.sointu)
};
return jsYaml.dump(sointusong);
}

export async function getSointuWasm(song) {
const wat = await fetch('https://sointu-server-c6w7hd53ia-uc.a.run.app/process', {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({ content: jsYaml.dump(sointusong) })
body: JSON.stringify({ content: await getSointuYaml(song) })
}).then(r => r.text());

const wabt = await exports.WabtModule();
Expand Down
5 changes: 0 additions & 5 deletions wasmaudioworklet/sointu/sointuaudioworkletprocessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,6 @@ class SointuAudioWorkletProcessor extends AudioWorkletProcessor {
}
const shouldUpdateVoices = this.wasmInstance.render_128_samples();
if (this.playing && shouldUpdateVoices) {
/*console.log(this.wasmInstance.tick.value,
this.wasmInstance.row.value,
this.wasmInstance.pattern.value,
this.wasmInstance.sample.value,
this.wasmInstance.outputBufPtr.value);*/
this.wasmInstance.update_voices();
}

Expand Down
Loading

0 comments on commit d9aa076

Please sign in to comment.