An open API service indexing awesome lists of open source software.

https://github.com/ul/chez-soundio

libsoundio bindings for Chez Scheme
https://github.com/ul/chez-soundio

chez-scheme ffi ffi-bindings ffi-wrapper library libsoundio literate-programming scheme

Last synced: 2 months ago
JSON representation

libsoundio bindings for Chez Scheme

Awesome Lists containing this project

README

        

* SoundIo

[[https://github.com/andrewrk/libsoundio][libsoundio]] [[https://github.com/cisco/ChezScheme][Chez Scheme]] wrapper.

Status: FFI is complete, high-level wrappers are alpha.

Dependency versions: libsoundio 2.0.0 and Chez Scheme 9.5.5.5

NOTE: Threaded Chez Scheme crashes with SIGILL on MacOS and with SIGSEGV on
Linux if =write_callback= calls Scheme code. Please use provided ring buffer
based bridge.

* TODO Usage

See [[Example]] for now.

* FFI

** Load Library

libsoundio shared library should be installed somewhere in the PATH, let's
load it depending on platform:

#+NAME: load-library
#+BEGIN_SRC scheme
;;
(define init-ffi
(case (machine-type)
[(i3nt ti3nt a6nt ta6nt) (load-shared-object "libsoundio.dll")]
[(i3osx ti3osx a6osx ta6osx tarm64osx) (load-shared-object "libsoundio.dylib")]
[(i3le ti3le a6le ta6le) (load-shared-object "libsoundio.so")]
[else (error "soundio"
"don't know how libsoundio shared library file is called on this machine-type"
(machine-type))]))
;;
#+END_SRC

Machine type correspondence to platform could be found in [[https://cisco.github.io/ChezScheme/release_notes/v9.4/release_notes.html][release notes]].

** Data Structures

Defining foreign types (ftypes) for interaction with C code gives runtime checks and
more clarity. To have mutually recursive ftypes we will describe them one by
one and then put into the single =define-ftype=.

*** TODO Auto-generate most of this from =soundio.h=

*** Enums

Chez Scheme FFI has no(ftype-ref SoundIoOutStream (layout channel_count) out-stream) representation for enums, we are going to make them
just =int= aliases.

#+NAME: ftype-enums
#+BEGIN_SRC scheme
;;
[SoundIoBackend int]
[SoundIoChannelId int]
[SoundIoFormat int]
[SoundIoDeviceAim int]
;;
#+END_SRC

*** Callbacks

libsoundio has plenty of ones, defining ftypes for them instead of just
using =void*= would give us runtime safety and convenience.

#+NAME: ftype-callbacks
#+BEGIN_SRC scheme
;;
[OnDeviceChangeCallback (function ((* SoundIo)) void)]
[OnBackendDisconnectCallback (function ((* SoundIo) int) void)]
[OnEventsSignalCallback (function ((* SoundIo)) void)]
[EmitRtprioWarningCallback (function () void)]
[JackInfoCallback (function ((* char)) void)]
[JackErrorCallback (function ((* char)) void)]
[WriteCallback (function ((* SoundIoOutStream) int int) void)]
[UnderflowCallback (function ((* SoundIoOutStream)) void)]
[ReadCallback (function ((* SoundIoInStream) int int) void)]
[OverflowCallback (function ((* SoundIoInStream)) void)]
[ErrorCallback (function ((* SoundIoOutStream) int) void)]
;;
#+END_SRC

*** Structs

#+NAME: SoundIo
#+BEGIN_SRC scheme
[SoundIo
(struct
[userdata void*] ; Optional. Put whatever you want here. Defaults to NULL.
[on_devices_change (* OnDeviceChangeCallback)] ; Optional callback.
[on_backend_disconnect (* OnBackendDisconnectCallback)] ; Optional callback.
[on_events_signal (* OnEventsSignalCallback)] ; Optional callback.
[current_backend SoundIoBackend] ; Read-only.
[app_name (* char)] ; Optional: Application name.
[emit_rtprio_warning (* EmitRtprioWarningCallback)] ; Optional: Real time priority warning.
[jack_info_callback (* JackInfoCallback)] ; Optional: JACK info callback.
[jack_error_callback (* JackErrorCallback)] ; Optional: JACK error callback.
)]
#+END_SRC

#+NAME: SoundIoChannelArea
#+BEGIN_SRC scheme
[SoundIoChannelArea
(struct
[ptr (* char)]
[step int])]
;; Useful for defining **SoundIoChannelArea in function ftype as (* *SoundIoChannelArea)
;; nested * or its alias doesn't work:
;; Exception: invalid (non-base) foreign-procedure argument ftype **SoundIoChannelArea
[*SoundIoChannelArea (* SoundIoChannelArea)]
#+END_SRC

#+NAME: SoundIoChannelLayout
#+BEGIN_SRC scheme
[SoundIoChannelLayout
(struct
[name (* char)]
[channel_count int]
;; #define SOUNDIO_MAX_CHANNELS 24
;; http://libsound.io/doc-1.1.0/soundio_8h.html#a1bf1282c5d903085916f8ed6af174bdd
[channels (array 24 SoundIoChannelId)])]
#+END_SRC

#+NAME: SoundIoDevice
#+BEGIN_SRC scheme
[SoundIoDevice
(struct
[soundio (* SoundIo)]
[id (* char)]
[name (* char)]
[aim SoundIoDeviceAim]
[layouts (* SoundIoChannelLayout)]
[layout_count int]
[current_layout SoundIoChannelLayout]
[formats (* SoundIoFormat)]
[format_count int]
[current_format SoundIoFormat]
[sample_rates (* SoundIoSampleRateRange)]
[sample_rate_count int]
[sample_rate_current int]
[software_latency_min double]
[software_latency_max double]
[software_latency_current double]
[is_raw boolean]
[ref_count int]
[probe_error int])]
#+END_SRC

#+NAME: SoundIoInStream
#+BEGIN_SRC scheme
[SoundIoInStream
(struct
[device (* SoundIoDevice)]
[format SoundIoFormat]
[sample_rate int]
[layout SoundIoChannelLayout]
[software_latency double]
[userdata void*]
[read_callback (* ReadCallback)]
[overflow_callback (* OverflowCallback)]
[error_callback (* ErrorCallback)]
[name (* char)]
[non_terminal_hint boolean]
[bytes_per_frame int]
[bytes_per_sample int]
[layout_error int])]
#+END_SRC

#+NAME: SoundIoOutStream
#+BEGIN_SRC scheme
[SoundIoOutStream
(struct
[device (* SoundIoDevice)]
[format SoundIoFormat]
[sample_rate int]
[layout SoundIoChannelLayout]
[software_latency double]
[volume float]
[userdata void*]
[write_callback (* WriteCallback)]
[underflow_callback (* UnderflowCallback)]
[error_callback (* ErrorCallback)]
[name (* char)]
[non_terminal_hint boolean]
[bytes_per_frame int]
[bytes_per_sample int]
[layout_error int])]
#+END_SRC

#+NAME: SoundIoSampleRateRange
#+BEGIN_SRC scheme
[SoundIoSampleRateRange
(struct
[min int]
[max int])]
#+END_SRC

#+NAME: SoundIoRingBuffer
#+BEGIN_SRC scheme
[SoundIoRingBuffer
(struct
[mem SoundIoOsMirroredMemory]
[write_offset SoundIoAtomicLong]
[read_offset SoundIoAtomicLong]
[capacity int])]
#+END_SRC

#+NAME: SoundIoOsMirroredMemory
#+BEGIN_SRC scheme
[SoundIoOsMirroredMemory
(struct
[capacity size_t]
[address (* char)]
[priv void*])]
#+END_SRC

#+NAME: SoundIoAtomicLong
#+BEGIN_SRC scheme
[SoundIoAtomicLong long]
#+END_SRC

#+NAME: ftype-structs
#+BEGIN_SRC scheme
;;
<>
<>
<>
<>
<>
<>
<>
<>
<>
<>
;;
#+END_SRC

*** Summa

#+NAME: ftypes
#+BEGIN_SRC scheme
;;
(define-ftype
<>
<>
<>
)
;;
#+END_SRC

** Procedures

We are going to keep original names while defining foreign procedures, thus
let's write a macro to save few keystrokes:

#+NAME: define-foreign-procedure
#+BEGIN_SRC scheme
(define-syntax (define-foreign-procedure stx)
(syntax-case stx ()
[(_ [name args result])
#`(define name
(foreign-procedure
#,(symbol->string (syntax->datum #'name))
args
result))]
[(_ e ...)
#'(begin
(define-foreign-procedure e)
...)]))
#+END_SRC

#+NAME: foreign-procedures
#+BEGIN_SRC scheme
(define-foreign-procedure
[soundio_backend_count ((* SoundIo)) int]
[soundio_backend_name (SoundIoBackend) int]
[soundio_best_matching_channel_layout
((* SoundIoChannelLayout) ; preferred_layouts
int ; preferred_layout_count
(* SoundIoChannelLayout) ; available_layouts
int ; available_layout_count
)
(* SoundIoChannelLayout)]
[soundio_channel_layout_builtin_count () int]
[soundio_channel_layout_detect_builtin ((* SoundIoChannelLayout)) boolean]
[soundio_channel_layout_equal ((* SoundIoChannelLayout) (* SoundIoChannelLayout)) boolean]
[soundio_channel_layout_find_channel ((* SoundIoChannelLayout) SoundIoChannelId) int]
[soundio_channel_layout_get_builtin (int) (* SoundIoChannelLayout)]
[soundio_channel_layout_get_default (#|channel_count|# int) (* SoundIoChannelLayout)]
[soundio_connect ((* SoundIo)) int]
[soundio_connect_backend ((* SoundIo) (* SoundIoBackend)) int]
[soundio_create () (* SoundIo)]
[soundio_default_input_device_index ((* SoundIo)) int]
[soundio_default_output_device_index ((* SoundIo)) int]
[soundio_destroy ((* SoundIo)) void]
[soundio_device_equal ((* SoundIoDevice) (* SoundIoDevice)) boolean]
[soundio_device_nearest_sample_rate ((* SoundIoDevice) int) int]
[soundio_device_ref ((* SoundIoDevice)) void]
[soundio_device_sort_channel_layouts ((* SoundIoDevice)) void]
[soundio_device_supports_format ((* SoundIoDevice) SoundIoFormat) boolean]
[soundio_device_supports_layout ((* SoundIoDevice) (* SoundIoChannelLayout)) boolean]
[soundio_device_supports_sample_rate ((* SoundIoDevice) int) boolean]
[soundio_device_unref ((* SoundIoDevice)) void]
[soundio_disconnect ((* SoundIo)) void]
[soundio_flush_events ((* SoundIo)) void]
[soundio_force_device_scan ((* SoundIo)) void]
[soundio_format_string (SoundIoFormat) string]
[soundio_get_backend ((* SoundIo) int) SoundIoBackend]
;; [soundio_get_bytes_per_frame (SoundIoFormat #|channel_count|# int) int]
;; [soundio_get_bytes_per_sample (SoundIoFormat) int]
;; [soundio_get_bytes_per_second (SoundIoFormat #|channel_count|# int #|sample_rate|# int) int]
[soundio_get_channel_name (SoundIoChannelId) string]
[soundio_get_input_device ((* SoundIo) int) (* SoundIoDevice)]
[soundio_get_output_device ((* SoundIo) int) (* SoundIoDevice)]
[soundio_have_backend (SoundIoBackend) boolean]
[soundio_input_device_count ((* SoundIo)) int]
[soundio_instream_begin_read ((* SoundIoInStream) (* *SoundIoChannelArea) (* int)) int]
[soundio_instream_create ((* SoundIoDevice)) (* SoundIoInStream)]
[soundio_instream_destroy ((* SoundIoInStream)) void]
[soundio_instream_end_read ((* SoundIoInStream)) int]
[soundio_instream_get_latency ((* SoundIoInStream) (* double)) int]
[soundio_instream_open ((* SoundIoInStream)) int]
[soundio_instream_pause ((* SoundIoInStream) boolean) int]
[soundio_instream_start ((* SoundIoInStream)) int]
[soundio_output_device_count ((* SoundIo)) int]
[soundio_outstream_begin_write ((* SoundIoOutStream) (* *SoundIoChannelArea) (* int)) int]
[soundio_outstream_clear_buffer ((* SoundIoOutStream)) int]
[soundio_outstream_create ((* SoundIoDevice)) (* SoundIoOutStream)]
[soundio_outstream_destroy ((* SoundIoOutStream)) void]
[soundio_outstream_end_write ((* SoundIoOutStream)) int]
[soundio_outstream_get_latency ((* SoundIoOutStream) (* double)) int]
[soundio_outstream_open ((* SoundIoOutStream)) int]
[soundio_outstream_pause ((* SoundIoOutStream) boolean) int]
[soundio_outstream_start ((* SoundIoOutStream)) int]
[soundio_parse_channel_id ((* char) int) SoundIoChannelId]
[soundio_ring_buffer_advance_read_ptr ((* SoundIoRingBuffer) int) void]
[soundio_ring_buffer_advance_write_ptr ((* SoundIoRingBuffer) int) void]
[soundio_ring_buffer_capacity ((* SoundIoRingBuffer)) int]
[soundio_ring_buffer_clear ((* SoundIoRingBuffer)) void]
[soundio_ring_buffer_create ((* SoundIo) int) (* SoundIoRingBuffer)]
[soundio_ring_buffer_destroy ((* SoundIoRingBuffer)) void]
[soundio_ring_buffer_fill_count ((* SoundIoRingBuffer)) int]
[soundio_ring_buffer_free_count ((* SoundIoRingBuffer)) int]
[soundio_ring_buffer_read_ptr ((* SoundIoRingBuffer)) (* char)]
[soundio_ring_buffer_write_ptr ((* SoundIoRingBuffer)) (* char)]
[soundio_sort_channel_layouts ((* SoundIoChannelLayout) int) void]
[soundio_strerror (int) string]
[soundio_version_major () int]
[soundio_version_minor () int]
[soundio_version_patch () int]
[soundio_version_string () string]
[soundio_wait_events ((* SoundIo)) void]
[soundio_wakeup ((* SoundIo)) void])
#+END_SRC

** Summa

#+NAME: ffi
#+BEGIN_SRC scheme
;;
<>
<>
<>
<>
;;
#+END_SRC

* Higher-level wrapping

Though library is already usable for producing sound via Scheme there is still
plenty of boilerplate to abstract away. It's quite hard to cover all use
cases, the plan is to add features one by one based on real usage feedback.

Known limitations of current wrapper:

- it designed for threaded version and uses threads; though we could imagine
use case for libsoundio in non-threaded Chez (non-interactive sound
generation), we are interested in live-coding application and lean towards
it
- at the moment only =float= sample type is supported

** C Bridge

To make library work in threaded version we need to build and load our
=bridge.c= helper.

First, we need to define how our file is called and where Scheme's headers
located.

#+NAME: bridge-paths
#+BEGIN_SRC scheme
;;
(define bridge-source-filename "bridge.c")
(define bridge-library-filename "libbridge.so")
(define scheme-headers-path (format "/usr/local/lib/csv9.5.5.5/~a" (machine-type)))
;;
#+END_SRC

In case library doesn't exist try to build it automatically.

#+NAME: build-bridge
#+BEGIN_SRC scheme
;;
(case (machine-type)
[(i3nt ti3nt a6nt ta6nt)
(begin
(error "init-bridge"
"don't know how to build for Windows, look at the source for template to adjust")
(system (format "cl -c -DWIN32 ~a"
bridge-source-filename))
(system (format "link -dll -out:~a ~a.obj"
bridge-library-filename
bridge-source-filename)))]
[(i3osx ti3osx a6osx ta6osx tarm64osx)
(system (format "cc -O3 -dynamiclib -Wl,-undefined -Wl,dynamic_lookup -I~a -lsoundio -o ~a ~a"
scheme-headers-path
bridge-library-filename
bridge-source-filename))]
[(i3le ti3le a6le ta6le)
(system (format "cc -O3 -fPIC -shared -Wl,-undefined -Wl,dynamic_lookup -I~a -lsoundio -o ~a ~a.c"
scheme-headers-path
bridge-library-filename
bridge-source-filename))]
[else (error "init-bridge"
"don't know how to build bridge shared library on this machine-type"
(machine-type))])
;;
#+END_SRC

Machine type correspondence to platform could be found in [[https://cisco.github.io/ChezScheme/release_notes/v9.4/release_notes.html][release notes]].

We need to wrap loading shared library into define to make it work inside
R6RS =library= construct.

#+NAME: init-bridge
#+BEGIN_SRC scheme
;;
<>
(define init-bridge
(begin
(unless (file-exists? bridge-library-filename)
<>
)
(load-shared-object bridge-library-filename)))
;;
#+END_SRC

*** write_callback

Heart of the bridge is custom =write_callback= which draws samples from ring
buffer passed to it via stream's =userdata= field. To avoid underflows we
fill stream with zeros if buffer has not enough data.

#+NAME: write_callback
#+BEGIN_SRC c
//
static void write_callback(struct SoundIoOutStream *outstream, int frame_count_min, int frame_count_max) {
struct SoundIoRingBuffer *ring_buffer = outstream->userdata;
struct SoundIoChannelArea *areas;
int frame_count;
int frames_left;
int err;

char *read_ptr = soundio_ring_buffer_read_ptr(ring_buffer);
int fill_bytes = soundio_ring_buffer_fill_count(ring_buffer);
int fill_count = fill_bytes / outstream->bytes_per_frame;

if (frame_count_min > fill_count) {
<>
}

<>

soundio_ring_buffer_advance_read_ptr(ring_buffer, read_count * outstream->bytes_per_frame);
}
//
#+END_SRC

libsoundio examples suggest to guard actual write to stream with checks.

#+NAME: begin-write
#+BEGIN_SRC c
//
if ((err = soundio_outstream_begin_write(outstream, &areas, &frame_count))) {
fprintf(stderr, "begin_write: %s\n", soundio_strerror(err));
exit(1);
}
//
#+END_SRC

#+NAME: end-write
#+BEGIN_SRC c
//
if ((err = soundio_outstream_end_write(outstream))) {
fprintf(stderr, "end_write: %s\n", soundio_strerror(err));
// REVIEW pthread_exit?
exit(1);
}
//
#+END_SRC

#+NAME: copy-samples-from-buffer
#+BEGIN_SRC c
//
int read_count = frame_count_max < fill_count ? frame_count_max : fill_count;
frames_left = read_count;

while (frames_left > 0) {
int frame_count = frames_left;

<>

if (frame_count <= 0)
break;

for (int frame = 0; frame < frame_count; frame += 1) {
for (int ch = 0; ch < outstream->layout.channel_count; ch += 1) {
memcpy(areas[ch].ptr, read_ptr, outstream->bytes_per_sample);
areas[ch].ptr += areas[ch].step;
read_ptr += outstream->bytes_per_sample;
}
}

<>

frames_left -= frame_count;
}
//
#+END_SRC

#+NAME: fill-stream-with-zeros
#+BEGIN_SRC c
//
frames_left = frame_count_min;
for (;;) {
frame_count = frames_left;
if (!frame_count)
return;

<>

if (!frame_count)
return;
for (int frame = 0; frame < frame_count; frame += 1) {
for (int ch = 0; ch < outstream->layout.channel_count; ch += 1) {
memset(areas[ch].ptr, 0, outstream->bytes_per_sample);
areas[ch].ptr += areas[ch].step;
}
}

<>

frames_left -= frame_count;
}
//
#+END_SRC

*** bridge_outstream_attach_ring_buffer

It accepts =outstream= and =buffer= and sets =buffer= and our
=write_callback= to =outstream=.

#+NAME: bridge_outstream_attach_ring_buffer
#+BEGIN_SRC c
//
EXPORT void bridge_outstream_attach_ring_buffer
(struct SoundIoOutStream *outstream, struct SoundIoRingBuffer *buffer) {
outstream->format = SoundIoFormatFloat32NE;
outstream->userdata = buffer;
outstream->write_callback = write_callback;
}
//
#+END_SRC

*** usleep

It's a microsecond resolution sleep based on calling =select= with timeout.
It accepts =seconds= and =microseconds= to sleep as integers. It is used to
wait a little when buffer is full. It is also useful if you want to
implement high-resolution scheduler. I found out that using Scheme's =sleep=
which calls =nanosleep= under the hood is quite expensive and imprecise.

I'm not sure why it's needed to wrap =select= into Scheme thread
deactivation, but without it attempts to call =usleep= from different
threads leads to stops in sound.

#+NAME: usleep
#+BEGIN_SRC c
//
EXPORT void usleep (long seconds, long microseconds) {
struct timeval timeout;
timeout.tv_sec = seconds;
timeout.tv_usec = microseconds;
Sdeactivate_thread();
select(0, NULL, NULL, NULL, &timeout);
Sactivate_thread();
}
//
#+END_SRC

*** Define foreign procedures in Scheme

#+NAME: bridge-ffi
#+BEGIN_SRC scheme
;;
(define-foreign-procedure
[bridge_outstream_attach_ring_buffer ((* SoundIoOutStream) (* SoundIoRingBuffer)) void]
[usleep (long #|seconds|# long #|microseconds|#) void])
;;
#+END_SRC

** Scheme

Most of the time I want just fire up default output device and provide
per-sample-per-channel dsp callback to make noise, and eventually stop doing
it. It would be good to have dedicated DS which will hold a bunch of pointers
created on the way.

#+NAME: sound-out-record
#+BEGIN_SRC scheme
;;
(define-record-type sound-out
(fields stream
ring-buffer
(mutable write-callback)
(mutable write-thread)))
;;
#+END_SRC

Next step is to encapsulate all initialization routines.

As an experiment, let's go from the end to the beginning. Ultimate goal of
initialization is to have open output audio stream on default device. The
stream should have =write_callback= assigned but to be not started. We want to
ignit sound as a separate action. Also we want to return a bunch of pointers
packed into =sound-out= record to have access to them later: to start and stop
stream and to properly close and destroy stream.

=define-record-type= produced record constructor for us, just pass fields to
it:

#+NAME: make-sound-out
#+BEGIN_SRC scheme
;;
(printf "Channels:\t~s\r\n" channel-count)
(printf "Sample rate:\t~s\r\n" sample-rate)
(printf "Latency:\t~s\r\n" latency)
(printf "Buffer:\t\t~s\r\n" buffer-size)
(make-sound-out out-stream ring-buffer write-callback #f)
;;
#+END_SRC

Callbacks are set before stream start. We don't want user to bother with
pointer arithmetic and stuff, thus we wrap callbacks. Even more, threaded Chez
Scheme crashes when =write_callback= calls Scheme code. Thus we are going to
use ring buffer to build a bridge between systems. User's =write-callback=
will receive =timestamp= and =channel= and should return sample value.
=underflow-callback= is still to be implemented, because we moved to ring
buffer from direct callbacks which corrupted Scheme runtime.

#+NAME: attach-buffer-to-stream
#+BEGIN_SRC scheme
;;
(let* ([frame-size (ftype-sizeof float)]
[channel-count (ftype-ref SoundIoOutStream (layout channel_count) out-stream)]
[sample-rate (ftype-ref SoundIoOutStream (sample_rate) out-stream)]
[latency (ftype-ref SoundIoOutStream (software_latency) out-stream)]
[buffer-size (exact (ceiling (* latency sample-rate)))] ; in samples
[buffer-capacity (* buffer-size frame-size channel-count)] ; in bytes
[ring-buffer (soundio_ring_buffer_create sio buffer-capacity)])
(when (ftype-pointer-null? ring-buffer)
(error "soundio_ring_buffer_create" "out of memory"))
(bridge_outstream_attach_ring_buffer out-stream ring-buffer)
<>
)
;;
#+END_SRC

It makes sense to attach buffer and return =sound-out= record if opening
stream was successful:

#+NAME: try-open-stream
#+BEGIN_SRC scheme
;;
(let ([err (soundio_outstream_open out-stream)])
(when (not (zero? err))
(error "soundio_outstream_open" (soundio_strerror err)))
(let ([err (ftype-ref SoundIoOutStream (layout_error) out-stream)])
(when (not (zero? err))
(error "soundio_outstream_open" (soundio_strerror err))))
<>
)
;;
#+END_SRC

Let's create stream before setting its callbacks:

#+NAME: try-create-stream
#+BEGIN_SRC scheme
;;
(let ([out-stream (soundio_outstream_create device)])
(when (ftype-pointer-null? out-stream)
(error "soundio_outstream_create" "out of memory"))
<>
)
;;
#+END_SRC

The same story with device, we need to obtain it before use:

#+NAME: try-create-device
#+BEGIN_SRC scheme
;;
(let ([idx (soundio_default_output_device_index sio)])
(when (< idx 0)
(error "soundio_default_output_device_index" "no output device found"))
(let ([device (soundio_get_output_device sio idx)])
(when (ftype-pointer-null? device)
(error "soundio_get_output_device" "out of memory"))
<>
))
;;
#+END_SRC

And sio instance is to be created and connected before device access. Note
flushing events.

#+NAME: try-create-connect-sio
#+BEGIN_SRC scheme
;;
(let ([sio (soundio_create)])
(when (ftype-pointer-null? sio)
(error "soundio_create" "out of memory"))
(let ([err (soundio_connect sio)])
(when (not (zero? err))
(error "soundio_connect" (soundio_strerror err)))
(soundio_flush_events sio)
<>
))
;;
#+END_SRC

Now just give it a name =)

#+NAME: open-default-out-stream
#+BEGIN_SRC scheme
;;
(define (open-default-out-stream write-callback)
<>
)
;;
#+END_SRC

Now we need to be able start stream, stop stream and teardown our audio
subsytem. Starting and stopping stream require managing thread responsible
for calling our dsp function and filling ring buffer.

#+NAME: start-out-stream
#+BEGIN_SRC scheme
;;
(define (start-out-stream sound-out)
(let* ([frame-size (ftype-sizeof float)]
[out-stream (sound-out-stream sound-out)]
[channel-count (ftype-ref SoundIoOutStream (layout channel_count) out-stream)]
[sample-rate (ftype-ref SoundIoOutStream (sample_rate) out-stream)]
[seconds-per-sample (inexact (/ sample-rate))]
[ring-buffer (sound-out-ring-buffer sound-out)]
[polling-microseconds 1000]
[sample-number 0])
(sound-out-write-thread-set! sound-out (get-thread-id))
(fork-thread
(lambda ()
(let loop ()
(let ([write-callback (sound-out-write-callback sound-out)])
(when (sound-out-write-thread sound-out)
(let ([free-count (soundio_ring_buffer_free_count ring-buffer)])
(if (zero? free-count)
(begin
(usleep 0 polling-microseconds)
(loop))
(let ([free-frames (/ free-count frame-size channel-count)]
[write-ptr (ftype-pointer-address (soundio_ring_buffer_write_ptr ring-buffer))])
(do ([frame 0 (+ frame 1)])
((= frame free-frames) 0)
(let* ([sample-number (+ sample-number frame)]
[time (fl* (fixnum->flonum sample-number) seconds-per-sample)])
(do ([channel 0 (+ channel 1)])
((= channel channel-count) 0)
(foreign-set!
'float
write-ptr
(* (+ (* frame channel-count) channel) frame-size)
(write-callback time channel))
)))
(soundio_ring_buffer_advance_write_ptr ring-buffer free-count)
(set! sample-number (+ sample-number free-frames))
(loop))
)))))))
(soundio_outstream_start out-stream)))
;;
#+END_SRC

#+NAME: stop-out-stream
#+BEGIN_SRC scheme
;;
(define (stop-out-stream sound-out)
(sound-out-write-thread-set! sound-out #f)
(soundio_outstream_pause (sound-out-stream sound-out) #t))
;;
#+END_SRC

Unmounting entire system require more actions. We are to destroy stream, unref
device, destroy sio and ring buffer.

#+NAME: teardown-out-stream
#+BEGIN_SRC scheme
;;
(define (teardown-out-stream sound-out)
(let* ([stream (sound-out-stream sound-out)]
[ring-buffer (sound-out-ring-buffer sound-out)]
[device (ftype-ref SoundIoOutStream (device) stream)]
[soundio (ftype-ref SoundIoDevice (soundio) device)])
(soundio_outstream_destroy stream)
(soundio_ring_buffer_destroy ring-buffer)
(soundio_device_unref device)
(soundio_destroy soundio)))
;;
#+END_SRC

#+NAME: channel-count
#+BEGIN_SRC scheme
;;
(define (channel-count sound-out)
(ftype-ref SoundIoOutStream
(layout channel_count)
(sound-out-stream sound-out)))
;;
#+END_SRC

#+NAME: sample-rate
#+BEGIN_SRC scheme
;;
(define (sample-rate sound-out)
(ftype-ref SoundIoOutStream
(sample_rate)
(sound-out-stream sound-out)))
;;
#+END_SRC

** Summa

#+NAME: high-level-wrapper
#+BEGIN_SRC scheme
;;
<>
<>
<>
<>
<>
<>
<>
<>
<>
;;
#+END_SRC

* Helpers

=make-ftype-pointer= locks object as pointed [[https://cisco.github.io/ChezScheme/csug9.4/foreign.html][here]], and its manual unlocking is
required to prevent memory leaks. It's done by 3 levels deep call of core
functions, thus we are going to define a dedicated function for it.

#+NAME: unlock-ftype-pointer
#+BEGIN_SRC scheme
;;
(define (unlock-ftype-pointer fptr)
(unlock-object
(foreign-callable-code-object
(ftype-pointer-address fptr))))
;;
#+END_SRC

** Summa

#+NAME: helpers
#+BEGIN_SRC scheme
;;
<>
;;
#+END_SRC

* Example

Let's play a bunch of sine waves (and test performance on the way).

#+NAME: sine-example.ss
#+BEGIN_SRC scheme :tangle sine-example.ss :noweb yes :mkdirp yes :paddle no
(import (prefix (soundio) soundio:))

(define pi 3.1415926535)

(define two-pi (* 2 pi))

(define sine (lambda (time freq)
(sin (* two-pi freq time))))

(define square (lambda (time freq)
(let ([ft (* two-pi freq time)])
(+ (- (* 2 (floor ft))
(floor (* 2 ft)))
1))))

(define write-callback (lambda (time channel)
(let ([k 100]
[sample 0.0])
(do ([i 0 (+ i 1)]
[sample 0.0 (+ sample (sine time (+ 440.0 i)))])
((= i k) (/ sample k))))))

(define square-callback (lambda (time channel)
(let ([k 20]
[sample 0.0])
(do ([i 0 (+ i 1)]
[sample 0.0 (+ sample (square time (+ 440.0 i)))])
((= i k) (/ sample k 2))))))

(define my-out (soundio:open-default-out-stream write-callback))

(soundio:start-out-stream my-out)
#+END_SRC

* License and Contribution

Contribution is more than welcome in any form. If you don't want to bother
youself dealing with org-mode (though it worth trying!), just patch generated
files included in repo and make PR. I'll incorporate changes into org file
then.

#+BEGIN_SRC text :tangle LICENSE
ISC License

Copyright (c) 2017, Ruslan Prokopchuk

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
#+END_SRC

* Files :noexport:
#+BEGIN_SRC scheme :tangle soundio-ffi.ss :noweb yes :mkdirp yes :paddle no
<>
<>
#+END_SRC

#+BEGIN_SRC scheme :tangle soundio.ss :noweb yes :mkdirp yes :paddle no
(library (soundio (1))
(export open-default-out-stream
start-out-stream
stop-out-stream
teardown-out-stream
sample-rate
channel-count
usleep)
(import (chezscheme))
(include "soundio-ffi.ss")
<>
)
#+END_SRC

#+NAME: bridge.c
#+BEGIN_SRC C :tangle bridge.c :noweb yes :mkdirp yes :paddle no
#ifdef WIN32
#define EXPORT extern __declspec (dllexport)
#else
#define EXPORT extern
#endif

#include
#include
#include
#include
#include
#include

#include "scheme.h"

<>
<>
<>
#+END_SRC