diff --git a/packages/cli/.gitignore b/packages/cli/.gitignore index e20d16c..bedc886 100644 --- a/packages/cli/.gitignore +++ b/packages/cli/.gitignore @@ -2,7 +2,13 @@ kabelsalat.js src/clib/dsp src/clib/wav.c src/clib/alsa.md -src/kabelsalat src/kabelsalat.js.c +src/kabelsalat +src/kabelsalat.js-osx.c +src/kabelsalat-osx +src/kabelsalat.js-pa.c +src/kabelsalat-pa src/clib/sine_wave src/clib/sine_wave.c +src/clib/sine_portaudio.c +src/clib/sine_portaudio diff --git a/packages/cli/package.json b/packages/cli/package.json index 9324fd8..f78e62f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -7,10 +7,22 @@ "license": "AGPL-3.0-or-later", "scripts": { "test": "node src/cli.js", + "ks2c": "pnpm ks2c:compile && pnpm ks2c:build && pnpm ks2c:run", "ks2c:compile": "cd src && node ks2c.js", - "ks2c:build": "cd src && gcc -o kabelsalat kabelsalat.js.c -lm", - "ks2c:run": "cd src && ./kabelsalat | sox -traw -r44100 -b32 -e float -c 2 - -tcoreaudio" + "ks2c:build_gcc": "cd src && gcc -o kabelsalat kabelsalat.js.c -lm", + "ks2c:build": "cd src && clang -Os -flto -o kabelsalat kabelsalat.js.c", + "ks2c:run": "cd src && ./kabelsalat | sox -traw -r44100 -b32 -e float -c 2 - -tcoreaudio", + + "ks2c-osx": "pnpm ks2c-osx:compile && pnpm ks2c-osx:build && pnpm ks2c-osx:run", + "ks2c-osx:compile": "cd src && node ks2c-osx.js", + "ks2c-osx:build": "cd src && clang -Os -framework AudioToolbox -flto -o kabelsalat-osx kabelsalat.js-osx.c", + "ks2c-osx:run": "cd src && ./kabelsalat-osx", + + "ks2c-pa": "pnpm ks2c-pa:compile && pnpm ks2c-pa:build && pnpm ks2c-pa:run", + "ks2c-pa:compile": "cd src && node ks2c-pa.js", + "ks2c-pa:build": "cd src && gcc -Os kabelsalat.js-pa.c -flto -o kabelsalat-pa -I/opt/homebrew/include -L/opt/homebrew/lib -lportaudio -lm", + "ks2c-pa:run": "cd src && ./kabelsalat-pa" }, "dependencies": { "@kabelsalat/core": "workspace:*", diff --git a/packages/cli/src/clib/ugens.c b/packages/cli/src/clib/ugens.c index f85ed78..efb3b7e 100644 --- a/packages/cli/src/clib/ugens.c +++ b/packages/cli/src/clib/ugens.c @@ -12,7 +12,9 @@ #define SAMPLE_TIME (1.0 / SAMPLE_RATE) #define MAX(x, y) (((x) > (y)) ? (x) : (y)) #define MIN(x, y) (((x) < (y)) ? (x) : (y)) -#define RANDOM_FLOAT ((float)arc4random() / (float)UINT32_MAX) +#define RANDOM_FLOAT ((float)arc4random() / (float)UINT32_MAX) // libbsd +//#define RANDOM_FLOAT ((float)rand() / (float)RAND_MAX) // stdlib +//#define RANDOM_FLOAT ((float)random() / (float)0x7FFFFFFF) // POSIX // helpers diff --git a/packages/cli/src/ks2c-osx.js b/packages/cli/src/ks2c-osx.js new file mode 100644 index 0000000..50deb9b --- /dev/null +++ b/packages/cli/src/ks2c-osx.js @@ -0,0 +1,187 @@ +#!/usr/bin/env node + +// node ks2c_osx.js > test.c && clang -Os -fomit-frame-pointer -ffunction-sections -fdata-sections -g0 -flto -framework AudioToolbox test.c -o test && strip test && ./test +// pnpm ks2c + +import fs from "node:fs/promises"; +import * as core from "@kabelsalat/core/src/index.js"; +import * as lib from "@kabelsalat/lib/src/index.js"; +// for some reason, node will always take main and not module file... +import { existsSync } from "node:fs"; +import path from "node:path"; + +async function main() { + let file = process.argv[2]; + if (!file) { + // file = "kabelsalat.js"; + file = "./kabelsalat.js"; + console.log(`no input file given -> using "${file}"`); + } + const fileExists = existsSync(file); + + if (!fileExists) { + console.log(`file "${file}" not found.`); + return; + } + + const filePath = path.resolve(process.cwd(), file); + const ksCode = await fs.readFile(filePath, { encoding: "utf8" }); + const ugenPath = path.resolve(process.cwd(), "./clib/ugens.c"); + const ugenCode = await fs.readFile(ugenPath, { encoding: "utf8" }); + Object.assign(globalThis, core); + Object.assign(globalThis, lib); + const node = core.evaluate(ksCode); + const unit = node.compile({ lang: "c" }); + const cCode = ks2c(unit, ugenCode); + const outFileName = file + "-osx.c"; + // console.log(cCode); + try { + await fs.writeFile(outFileName, cCode); + + console.log(`written ${outFileName}`); + } catch (err) { + console.log(`error writing ${outFileName}: ${err.message}`); + } +} + +main(); + +let ks2c = ( + unit, + ugens +) => `// this file has been compiled from kabelsalat, using ks2c-osx +// this file uses AudioToolbox, which will only work on OSX. +// compile and run with: +// clang -Os -framework AudioToolbox -flto -o kabelsalat-osx kabelsalat.js-osx.c && ./kabelsalat-osx +// this will spit out a self-contained ~34kB binary that plays the compiled kabelsalat patch + +#include + +${ugens} + +#define SAMPLE_RATE 44100 +#define CHANNELS 2 +#define SAMPLE_TIME (1.0 / SAMPLE_RATE) + +typedef struct +{ + double time; + float* r; + float* o; + int osize; + float* s; + void** nodes; // wtf am i doing +} CallbackEnv; + +static OSStatus DSPCallback( + void *inRefCon, + AudioUnitRenderActionFlags *ioActionFlags, + const AudioTimeStamp *inTimeStamp, + UInt32 inBusNumber, + UInt32 inNumberFrames, + AudioBufferList *ioData) +{ + + + CallbackEnv *env = (CallbackEnv *)inRefCon; + + float *buffer = (float *)ioData->mBuffers[0].mData; + void **nodes = env->nodes; + + float *o = env->o; + float *r = env->r; + float *s = env->s; + + for (UInt32 j = 0; j < inNumberFrames; j++) + { + double time = env->time; + + // start of autogenerated callback +memset(o, 0, env->osize); // reset outputs +${unit.src} + // end of autogenerated callback + + float left = o[0]; + float right = o[1]; + + buffer[j*CHANNELS] = left*0.3; + buffer[j*CHANNELS+1] = right*0.3; + env->time += SAMPLE_TIME; + + } + + return noErr; +} + +void SetupAudioUnit(CallbackEnv *env) +{ + AudioComponentDescription desc = {0}; + desc.componentType = kAudioUnitType_Output; + desc.componentSubType = kAudioUnitSubType_DefaultOutput; + desc.componentManufacturer = kAudioUnitManufacturer_Apple; + + AudioComponent outputComponent = AudioComponentFindNext(NULL, &desc); + AudioUnit audioUnit; + AudioComponentInstanceNew(outputComponent, &audioUnit); + + AURenderCallbackStruct callbackStruct; + callbackStruct.inputProc = DSPCallback; + callbackStruct.inputProcRefCon = env; + + AudioUnitSetProperty(audioUnit, + kAudioUnitProperty_SetRenderCallback, + kAudioUnitScope_Input, + 0, + &callbackStruct, + sizeof(callbackStruct)); + + AudioStreamBasicDescription streamFormat; + streamFormat.mSampleRate = SAMPLE_RATE; + streamFormat.mFormatID = kAudioFormatLinearPCM; + streamFormat.mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked; + streamFormat.mFramesPerPacket = 1; + streamFormat.mChannelsPerFrame = CHANNELS; + streamFormat.mBitsPerChannel = sizeof(float) * 8; + streamFormat.mBytesPerPacket = sizeof(float) * CHANNELS; + streamFormat.mBytesPerFrame = sizeof(float) * CHANNELS; + + AudioUnitSetProperty(audioUnit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Input, + 0, + &streamFormat, + sizeof(streamFormat)); + + AudioUnitInitialize(audioUnit); + AudioOutputUnitStart(audioUnit); +} + +int main() +{ + +float o[16] = {0}; // output registry +float s[16] = {0}; // source registry + +// start of autogenerated init +float r[${unit.registers}] = {0}; // node registry +void *nodes[${unit.ugens.length}]; +${unit.ugens + .map((ugen, i) => `nodes[${i}] = ${ugen.type}_create();`) + .join("\n")} + +// end of autogenerated init + + CallbackEnv env; + env.nodes = nodes; + env.r = (float *)r; + env.o = (float *)o; + env.osize = sizeof(o); + env.s = (float *)s; + + SetupAudioUnit(&env); + + while(true) {} + + return 0; +} +`; diff --git a/packages/cli/src/ks2c-pa.js b/packages/cli/src/ks2c-pa.js new file mode 100644 index 0000000..8d32e5c --- /dev/null +++ b/packages/cli/src/ks2c-pa.js @@ -0,0 +1,191 @@ +#!/usr/bin/env node + +// this script takes a file as input (defaults to kabelsalat.js) +// the file is expected to contain a kabelsalat patch +// the patch is used to generate a c file that plays the patch +// the c file uses portaudio, so it should work on all supported platforms + +import fs from "node:fs/promises"; +import * as core from "@kabelsalat/core/src/index.js"; +import * as lib from "@kabelsalat/lib/src/index.js"; +import { existsSync } from "node:fs"; +import path from "node:path"; + +async function main() { + let file = process.argv[2]; + if (!file) { + file = "./kabelsalat.js"; + console.log(`no input file given -> using "${file}"`); + } + const fileExists = existsSync(file); + + if (!fileExists) { + console.log(`file "${file}" not found.`); + return; + } + + const filePath = path.resolve(process.cwd(), file); + const ksCode = await fs.readFile(filePath, { encoding: "utf8" }); + const ugenPath = path.resolve(process.cwd(), "./clib/ugens.c"); + const ugenCode = await fs.readFile(ugenPath, { encoding: "utf8" }); + Object.assign(globalThis, core); + Object.assign(globalThis, lib); + const node = core.evaluate(ksCode); + const unit = node.compile({ lang: "c" }); + const cCode = ks2c(unit, ugenCode); + const outFileName = file + "-pa.c"; + // console.log(cCode); + try { + await fs.writeFile(outFileName, cCode); + + console.log(`written ${outFileName}`); + } catch (err) { + console.log(`error writing ${outFileName}: ${err.message}`); + } +} + +main(); + +let ks2c = ( + unit, + ugens +) => `// this file has been compiled from kabelsalat, using ks2c-pa +// to build it, you need portaudio on your system +// compile and run on OSX with: +// gcc kabelsalat.js-pa.c -o kabelsalat-pa -I/opt/homebrew/include -L/opt/homebrew/lib -lportaudio -lm && ./kabelsalat-pa +// this will spit out a self-contained ~34kB binary that plays the compiled kabelsalat patch + +#include +#include +#include +#include + +${ugens} + +#define SAMPLE_RATE 44100 +#define FRAMES_PER_BUFFER 256 +#define SAMPLE_TIME (1.0 / SAMPLE_RATE) + +typedef struct +{ + double time; + float* r; + float* o; + int osize; + float* s; + void** nodes; // wtf am i doing +} CallbackEnv; + +static int DSPCallback(const void *inputBuffer, void *outputBuffer, + unsigned long framesPerBuffer, + const PaStreamCallbackTimeInfo *timeInfo, + PaStreamCallbackFlags statusFlags, + void *userData) +{ + CallbackEnv *env = (CallbackEnv *)userData; + float *out = (float *)outputBuffer; + + void **nodes = env->nodes; + + float *o = env->o; + float *r = env->r; + float *s = env->s; + + + for (unsigned long i = 0; i < framesPerBuffer; i++) + { + + double time = env->time; + memset(o, 0, env->osize); // reset outputs + +// start of autogenerated callback +${unit.src} +// end of autogenerated callback + + float left = o[0]; + float right = o[1]; + + *out++ = left*.3; + *out++ = right*.3; + env->time += SAMPLE_TIME; + } + return paContinue; +} + +int main() +{ + PaError err; + PaStream *stream; + + + float o[16] = {0}; // output registry + float s[16] = {0}; // source registry + + // start of autogenerated init +float r[${unit.registers}] = {0}; // node registry +void *nodes[${unit.ugens.length}]; +${unit.ugens + .map((ugen, i) => `nodes[${i}] = ${ugen.type}_create();`) + .join("\n")} + +// end of autogenerated init + + CallbackEnv env; + env.nodes = nodes; + env.r = (float *)r; + env.o = (float *)o; + env.osize = sizeof(o); + env.s = (float *)s; + env.time = 0; + + err = Pa_Initialize(); + if (err != paNoError) + { + fprintf(stderr, "PortAudio error: %s\\n", Pa_GetErrorText(err)); + return 1; + } + + err = Pa_OpenDefaultStream(&stream, + 0, // input channels + 2, // output channels + paFloat32, + SAMPLE_RATE, + FRAMES_PER_BUFFER, + DSPCallback, + &env); + if (err != paNoError) + { + fprintf(stderr, "PortAudio error: %s\\n", Pa_GetErrorText(err)); + Pa_Terminate(); + return 1; + } + + err = Pa_StartStream(stream); + if (err != paNoError) + { + fprintf(stderr, "PortAudio error: %s\\n", Pa_GetErrorText(err)); + Pa_Terminate(); + return 1; + } + + printf("Playing kabelsalat patch. Press Enter to stop...\\n"); + getchar(); + + err = Pa_StopStream(stream); + if (err != paNoError) + { + fprintf(stderr, "PortAudio error: %s\\n", Pa_GetErrorText(err)); + } + + err = Pa_CloseStream(stream); + if (err != paNoError) + { + fprintf(stderr, "PortAudio error: %s\\n", Pa_GetErrorText(err)); + } + + Pa_Terminate(); + + printf("playback stopped.\\n"); + return 0; +} +`; diff --git a/packages/cli/src/ks2c.js b/packages/cli/src/ks2c.js index 0ae417a..d29009d 100644 --- a/packages/cli/src/ks2c.js +++ b/packages/cli/src/ks2c.js @@ -45,9 +45,6 @@ async function main() { main(); function ks2c(unit, ugens) { - const outputIndices = unit.ugens - .filter((ugen) => ugen.type === "Output") - .map((output) => unit.ugens.indexOf(output)); return `// this file has been compiled from kabelsalat! // save this file as kabelsalat.js.c, then run: @@ -85,7 +82,7 @@ ${unit.ugens float s[16] = {0}; // source registry while (1) { - for (size_t j = 0; j < BUFFER_SIZE; j+=2) + for (int j = 0; j < BUFFER_SIZE; j+=2) { // start of autogenerated callback memset(o, 0, sizeof(o)); // reset outputs