diff --git a/AGENTS.md b/AGENTS.md index 49b4b42a6..43e901cee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,7 +13,7 @@ lines its run must print, and the gates run on the mini cluster. bend2/main.ts the CLI; imported, the .bend loader for bun and node bend2/base.bend the base library bend2/bend.lean the core, mechanized in Lean - bend2/effs/ one file per IO effect, per backend + bend2/effs/ IO effects per backend; audio shares one C source bend2/pack/ package.json, tsconfig.json, bun.lock bend2/docs/ the papers' Typst sources, the film, gen_pins.ts (the record pins on this Mac), gen_charts.ts (the landing diff --git a/bend2/base.bend b/bend2/base.bend index 35475bf56..327eff37d 100644 --- a/bend2/base.bend +++ b/bend2/base.bend @@ -368,15 +368,15 @@ def Window.close(window: Window) -> IO(Unit): import "./effs/window_close.js" def Audio.open(rate: U32) -> IO(Result<&1, &1, U32 & String, Audio>): - import "./effs/audio_open.c" + import "./effs/audio.c" import "./effs/audio_open.js" def Audio.write(audio: Audio, samples: List<&2, F32>) -> IO(Audio & U32): - import "./effs/audio_write.c" + import "./effs/audio.c" import "./effs/audio_write.js" def Audio.close(audio: Audio) -> IO(Unit): - import "./effs/audio_close.c" + import "./effs/audio.c" import "./effs/audio_close.js" # Equal diff --git a/bend2/effs/audio_write.c b/bend2/effs/audio.c similarity index 85% rename from bend2/effs/audio_write.c rename to bend2/effs/audio.c index 8727c0563..21d2398af 100644 --- a/bend2/effs/audio_write.c +++ b/bend2/effs/audio.c @@ -3,9 +3,8 @@ // The ring: 4096 float32 stereo frames. The effects fill it on the // evaluator's thread; the device's callback drains it and pads the -// rest of its buffer with silence. The same block sits in -// audio_write.c and audio_close.c under this guard, so that any one -// of the three effects compiles alone. +// rest of its buffer with silence. All audio effects share this source; +// each entry is present only when its effect is reachable. #ifndef IO_RING #define IO_RING 4096u @@ -164,6 +163,24 @@ static void io_ring_free(IoRing* p) { #endif #endif +#ifdef CID_AUDIO_OPEN +Term audio_open_run(Env e, Term* f, IoWork* w) { + u32 rate = (u32)f[0]; + IoRing* p = io_mem(calloc(1, sizeof *p)); + u32 code = rate < 8000 || rate > 192000 ? EINVAL : io_ring_start(p, rate); + if (code != 0) { + io_ring_free(p); + return io_fail(e, code, code == EINVAL ? NULL : "Audio.open: no audio output"); + } + return io_done(e, io_hand((intptr_t)p)); +} + +static void __attribute__((constructor)) audio_open_use(void) { + io_eff(CID_AUDIO_OPEN, audio_open_run, 0); +} +#endif + +#ifdef CID_AUDIO_WRITE // The samples (interleaved L R ...) into the ring; the frames queued // after the write. Past the ring's room, the samples are dropped and // the queue answered as it is. @@ -189,3 +206,15 @@ Term audio_write_run(Env e, Term* f, IoWork* w) { static void __attribute__((constructor)) audio_write_use(void) { io_eff(CID_AUDIO_WRITE, audio_write_run, 0); } +#endif + +#ifdef CID_AUDIO_CLOSE +Term audio_close_run(Env e, Term* f, IoWork* w) { + io_ring_free((IoRing*)(uintptr_t)io_hand_v(f[0])); + return term_pak(CID_UNIT, 0); +} + +static void __attribute__((constructor)) audio_close_use(void) { + io_eff(CID_AUDIO_CLOSE, audio_close_run, 0); +} +#endif diff --git a/bend2/effs/audio_close.c b/bend2/effs/audio_close.c deleted file mode 100644 index a3642f1d7..000000000 --- a/bend2/effs/audio_close.c +++ /dev/null @@ -1,174 +0,0 @@ -// Audio -// ===== - -// The ring: 4096 float32 stereo frames. The effects fill it on the -// evaluator's thread; the device's callback drains it and pads the -// rest of its buffer with silence. The same block sits in -// audio_write.c and audio_close.c under this guard, so that any one -// of the three effects compiles alone. -#ifndef IO_RING -#define IO_RING 4096u - -#ifdef __OBJC__ -#import -#elif defined(__linux__) -#include -#endif - -typedef struct { - _Atomic(u64) read, written; - float pcm[IO_RING * 2]; -#ifdef __OBJC__ - AudioUnit unit; -#elif defined(__linux__) - snd_pcm_t* unit; - pthread_t pump; - _Atomic(u32) done; -#endif -} IoRing; - -// n frames queued after the write; a write past the ring's room is -// dropped and the queue answered as it is. -static u64 io_ring_write(IoRing* p, const float* pcm, u32 n) { - u64 w = atomic_load_explicit(&p->written, memory_order_relaxed); - u64 r = atomic_load_explicit(&p->read, memory_order_acquire); - if (w - r + n <= IO_RING) { - u32 at = (u32)(w % IO_RING); - u32 first = n < IO_RING - at ? n : IO_RING - at; - memcpy(p->pcm + at * 2, pcm, first * 8); - memcpy(p->pcm, pcm + first * 2, (n - first) * 8); - atomic_store_explicit(&p->written, w + n, memory_order_release); - w += n; - } - return w - r; -} - -static void io_ring_pull(IoRing* p, float* out, u32 frames) { - u64 r = atomic_load_explicit(&p->read, memory_order_relaxed); - u64 w = atomic_load_explicit(&p->written, memory_order_acquire); - u32 n = (u32)(w - r < frames ? w - r : frames); - u32 at = (u32)(r % IO_RING); - u32 first = n < IO_RING - at ? n : IO_RING - at; - memcpy(out, p->pcm + at * 2, first * 8); - memcpy(out + first * 2, p->pcm, (n - first) * 8); - memset(out + n * 2, 0, (frames - n) * 8); - atomic_store_explicit(&p->read, r + n, memory_order_release); -} - -#ifdef __OBJC__ - -static OSStatus io_ring_pump(void* ctx, AudioUnitRenderActionFlags* flags, - const AudioTimeStamp* when, UInt32 bus, UInt32 frames, - AudioBufferList* bl) { - if (bl->mNumberBuffers != 1 || bl->mBuffers[0].mNumberChannels != 2 - || bl->mBuffers[0].mDataByteSize < frames * 8) { - return kAudio_ParamError; - } - io_ring_pull(ctx, bl->mBuffers[0].mData, frames); - return noErr; -} - -// The default output unit fed float32 stereo at the asked rate (the -// unit converts to the device's own). -static u32 io_ring_start(IoRing* p, u32 rate) { - AudioComponentDescription desc = { kAudioUnitType_Output, - kAudioUnitSubType_DefaultOutput, kAudioUnitManufacturer_Apple, 0, 0 }; - AudioComponent comp = AudioComponentFindNext(NULL, &desc); - if (comp == NULL || AudioComponentInstanceNew(comp, &p->unit) != noErr) { - return ENODEV; - } - AudioStreamBasicDescription fmt = { 0 }; - fmt.mSampleRate = rate; - fmt.mFormatID = kAudioFormatLinearPCM; - fmt.mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked; - fmt.mBytesPerPacket = 8; - fmt.mFramesPerPacket = 1; - fmt.mBytesPerFrame = 8; - fmt.mChannelsPerFrame = 2; - fmt.mBitsPerChannel = 32; - AURenderCallbackStruct cb = { io_ring_pump, p }; - bool ok = AudioUnitSetProperty(p->unit, kAudioUnitProperty_StreamFormat, - kAudioUnitScope_Input, 0, &fmt, sizeof fmt) == noErr - && AudioUnitSetProperty(p->unit, kAudioUnitProperty_SetRenderCallback, - kAudioUnitScope_Input, 0, &cb, sizeof cb) == noErr - && AudioUnitInitialize(p->unit) == noErr - && AudioOutputUnitStart(p->unit) == noErr; - return ok ? 0 : ENODEV; -} - -static void io_ring_free(IoRing* p) { - if (p->unit != NULL) { - AudioOutputUnitStop(p->unit); - AudioUnitUninitialize(p->unit); - AudioComponentInstanceDispose(p->unit); - } - free(p); -} - -#elif defined(__linux__) - -// A thread feeds the default ALSA device 256 frames at a time (a -// write blocks until the device has room, so the ring drains at the -// device's clock). -static void* io_ring_pump(void* ctx) { - IoRing* p = ctx; - float out[256 * 2]; - while (atomic_load_explicit(&p->done, memory_order_relaxed) == 0) { - io_ring_pull(p, out, 256); - snd_pcm_sframes_t n = snd_pcm_writei(p->unit, out, 256); - if (n < 0) { - snd_pcm_recover(p->unit, (int)n, 1); - } - } - return NULL; -} - -static void io_ring_hush(const char* file, int line, const char* fn, int err, - const char* fmt, ...) { -} - -static u32 io_ring_start(IoRing* p, u32 rate) { - snd_lib_error_set_handler(io_ring_hush); - if (snd_pcm_open(&p->unit, "default", SND_PCM_STREAM_PLAYBACK, 0) < 0) { - return ENODEV; - } - if (snd_pcm_set_params(p->unit, SND_PCM_FORMAT_FLOAT_LE, - SND_PCM_ACCESS_RW_INTERLEAVED, 2, rate, 1, 20000) < 0 - || pthread_create(&p->pump, NULL, io_ring_pump, p) != 0) { - return ENODEV; - } - return 0; -} - -static void io_ring_free(IoRing* p) { - if (p->unit != NULL) { - atomic_store_explicit(&p->done, 1, memory_order_relaxed); - if (p->pump != 0) { - pthread_join(p->pump, NULL); - } - snd_pcm_close(p->unit); - } - free(p); -} - -#else - -static u32 io_ring_start(IoRing* p, u32 rate) { - return ENOTSUP; -} - -static void io_ring_free(IoRing* p) { - free(p); -} - -#endif -#endif - -Term audio_close_run(Env e, Term* f, IoWork* w) { - io_ring_free((IoRing*)(uintptr_t)io_hand_v(f[0])); - return term_pak(CID_UNIT, 0); -} - -static void __attribute__((constructor)) audio_close_use(void) { - io_eff(CID_AUDIO_CLOSE, audio_close_run, 0); -} diff --git a/bend2/effs/audio_open.c b/bend2/effs/audio_open.c deleted file mode 100644 index db2806c6b..000000000 --- a/bend2/effs/audio_open.c +++ /dev/null @@ -1,180 +0,0 @@ -// Audio -// ===== - -// The ring: 4096 float32 stereo frames. The effects fill it on the -// evaluator's thread; the device's callback drains it and pads the -// rest of its buffer with silence. The same block sits in -// audio_write.c and audio_close.c under this guard, so that any one -// of the three effects compiles alone. -#ifndef IO_RING -#define IO_RING 4096u - -#ifdef __OBJC__ -#import -#elif defined(__linux__) -#include -#endif - -typedef struct { - _Atomic(u64) read, written; - float pcm[IO_RING * 2]; -#ifdef __OBJC__ - AudioUnit unit; -#elif defined(__linux__) - snd_pcm_t* unit; - pthread_t pump; - _Atomic(u32) done; -#endif -} IoRing; - -// n frames queued after the write; a write past the ring's room is -// dropped and the queue answered as it is. -static u64 io_ring_write(IoRing* p, const float* pcm, u32 n) { - u64 w = atomic_load_explicit(&p->written, memory_order_relaxed); - u64 r = atomic_load_explicit(&p->read, memory_order_acquire); - if (w - r + n <= IO_RING) { - u32 at = (u32)(w % IO_RING); - u32 first = n < IO_RING - at ? n : IO_RING - at; - memcpy(p->pcm + at * 2, pcm, first * 8); - memcpy(p->pcm, pcm + first * 2, (n - first) * 8); - atomic_store_explicit(&p->written, w + n, memory_order_release); - w += n; - } - return w - r; -} - -static void io_ring_pull(IoRing* p, float* out, u32 frames) { - u64 r = atomic_load_explicit(&p->read, memory_order_relaxed); - u64 w = atomic_load_explicit(&p->written, memory_order_acquire); - u32 n = (u32)(w - r < frames ? w - r : frames); - u32 at = (u32)(r % IO_RING); - u32 first = n < IO_RING - at ? n : IO_RING - at; - memcpy(out, p->pcm + at * 2, first * 8); - memcpy(out + first * 2, p->pcm, (n - first) * 8); - memset(out + n * 2, 0, (frames - n) * 8); - atomic_store_explicit(&p->read, r + n, memory_order_release); -} - -#ifdef __OBJC__ - -static OSStatus io_ring_pump(void* ctx, AudioUnitRenderActionFlags* flags, - const AudioTimeStamp* when, UInt32 bus, UInt32 frames, - AudioBufferList* bl) { - if (bl->mNumberBuffers != 1 || bl->mBuffers[0].mNumberChannels != 2 - || bl->mBuffers[0].mDataByteSize < frames * 8) { - return kAudio_ParamError; - } - io_ring_pull(ctx, bl->mBuffers[0].mData, frames); - return noErr; -} - -// The default output unit fed float32 stereo at the asked rate (the -// unit converts to the device's own). -static u32 io_ring_start(IoRing* p, u32 rate) { - AudioComponentDescription desc = { kAudioUnitType_Output, - kAudioUnitSubType_DefaultOutput, kAudioUnitManufacturer_Apple, 0, 0 }; - AudioComponent comp = AudioComponentFindNext(NULL, &desc); - if (comp == NULL || AudioComponentInstanceNew(comp, &p->unit) != noErr) { - return ENODEV; - } - AudioStreamBasicDescription fmt = { 0 }; - fmt.mSampleRate = rate; - fmt.mFormatID = kAudioFormatLinearPCM; - fmt.mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked; - fmt.mBytesPerPacket = 8; - fmt.mFramesPerPacket = 1; - fmt.mBytesPerFrame = 8; - fmt.mChannelsPerFrame = 2; - fmt.mBitsPerChannel = 32; - AURenderCallbackStruct cb = { io_ring_pump, p }; - bool ok = AudioUnitSetProperty(p->unit, kAudioUnitProperty_StreamFormat, - kAudioUnitScope_Input, 0, &fmt, sizeof fmt) == noErr - && AudioUnitSetProperty(p->unit, kAudioUnitProperty_SetRenderCallback, - kAudioUnitScope_Input, 0, &cb, sizeof cb) == noErr - && AudioUnitInitialize(p->unit) == noErr - && AudioOutputUnitStart(p->unit) == noErr; - return ok ? 0 : ENODEV; -} - -static void io_ring_free(IoRing* p) { - if (p->unit != NULL) { - AudioOutputUnitStop(p->unit); - AudioUnitUninitialize(p->unit); - AudioComponentInstanceDispose(p->unit); - } - free(p); -} - -#elif defined(__linux__) - -// A thread feeds the default ALSA device 256 frames at a time (a -// write blocks until the device has room, so the ring drains at the -// device's clock). -static void* io_ring_pump(void* ctx) { - IoRing* p = ctx; - float out[256 * 2]; - while (atomic_load_explicit(&p->done, memory_order_relaxed) == 0) { - io_ring_pull(p, out, 256); - snd_pcm_sframes_t n = snd_pcm_writei(p->unit, out, 256); - if (n < 0) { - snd_pcm_recover(p->unit, (int)n, 1); - } - } - return NULL; -} - -static void io_ring_hush(const char* file, int line, const char* fn, int err, - const char* fmt, ...) { -} - -static u32 io_ring_start(IoRing* p, u32 rate) { - snd_lib_error_set_handler(io_ring_hush); - if (snd_pcm_open(&p->unit, "default", SND_PCM_STREAM_PLAYBACK, 0) < 0) { - return ENODEV; - } - if (snd_pcm_set_params(p->unit, SND_PCM_FORMAT_FLOAT_LE, - SND_PCM_ACCESS_RW_INTERLEAVED, 2, rate, 1, 20000) < 0 - || pthread_create(&p->pump, NULL, io_ring_pump, p) != 0) { - return ENODEV; - } - return 0; -} - -static void io_ring_free(IoRing* p) { - if (p->unit != NULL) { - atomic_store_explicit(&p->done, 1, memory_order_relaxed); - if (p->pump != 0) { - pthread_join(p->pump, NULL); - } - snd_pcm_close(p->unit); - } - free(p); -} - -#else - -static u32 io_ring_start(IoRing* p, u32 rate) { - return ENOTSUP; -} - -static void io_ring_free(IoRing* p) { - free(p); -} - -#endif -#endif - -Term audio_open_run(Env e, Term* f, IoWork* w) { - u32 rate = (u32)f[0]; - IoRing* p = io_mem(calloc(1, sizeof *p)); - u32 code = rate < 8000 || rate > 192000 ? EINVAL : io_ring_start(p, rate); - if (code != 0) { - io_ring_free(p); - return io_fail(e, code, code == EINVAL ? NULL : "Audio.open: no audio output"); - } - return io_done(e, io_hand((intptr_t)p)); -} - -static void __attribute__((constructor)) audio_open_use(void) { - io_eff(CID_AUDIO_OPEN, audio_open_run, 0); -} diff --git a/tests/io/audio_only_close.bend b/tests/io/audio_only_close.bend new file mode 100644 index 000000000..2b5453ec9 --- /dev/null +++ b/tests/io/audio_only_close.bend @@ -0,0 +1,15 @@ +# Only Audio.close is reachable; the unused open and write entries must +# not require their constructor IDs. None needs no audio device. +import Base + +def close(m: Maybe<&1, Audio>) -> IO(Unit): + match m: + case None{}: + IO.print("close") + case Some{a}: + Audio.close(a) + +def main() -> IO(Unit): + close(None{}) + +#|close diff --git a/tests/io/audio_only_open.bend b/tests/io/audio_only_open.bend new file mode 100644 index 000000000..603591c46 --- /dev/null +++ b/tests/io/audio_only_open.bend @@ -0,0 +1,10 @@ +# Only Audio.open is reachable: its shared source must not register +# write or close. Rate 0 avoids opening a native audio device. +import Base + +def main() -> IO(Unit): + do IO: + r : Result<&1, &1, U32 & String, Audio> <- Audio.open(0) + IO.print("open") + +#|open diff --git a/tests/io/audio_only_write.bend b/tests/io/audio_only_write.bend new file mode 100644 index 000000000..0795f9fcc --- /dev/null +++ b/tests/io/audio_only_write.bend @@ -0,0 +1,17 @@ +# Only Audio.write is reachable. None keeps the test independent of an +# audio device while the Some arm requires the write entry to compile. +import Base + +def write(m: Maybe<&1, Audio>) -> IO(Unit): + match m: + case None{}: + IO.print("write") + case Some{a}: + do IO: + r : Audio & U32 <- Audio.write(a, []) + IO.print("write") + +def main() -> IO(Unit): + write(None{}) + +#|write