libsoundio bindings for Chez Scheme
Switch branches/tags
Nothing to show
Clone or download
Latest commit 98b8113 Oct 21, 2017

README.org

SoundIo

libsoundio Chez Scheme wrapper.

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

Dependency versions: libsoundio 1.1.0 and Chez Scheme 9.5.1

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.

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:

;; <load-library>
(define init-ffi
  (case (machine-type)
    [(i3nt ti3nt a6nt ta6nt) (load-shared-object "libsoundio.dll")]
    [(i3osx ti3osx a6osx ta6osx) (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))]))
;; </load-library>

Machine type correspondence to platform could be found in 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.

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.

;; <ftype-enums>
[SoundIoBackend int]
[SoundIoChannelId int]
[SoundIoFormat int]
[SoundIoDeviceAim int]
;; </ftype-enums>

Callbacks

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

;; <ftype-callbacks>
[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)]
;; </ftype-callbacks>

Structs

[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.
  )]
[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)]
[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)])]
[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])]
[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])]
[SoundIoOutStream
 (struct
   [device (* SoundIoDevice)]
   [format SoundIoFormat]
   [sample_rate int]
   [layout SoundIoChannelLayout]
   [software_latency double]
   [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])]
[SoundIoSampleRateRange
 (struct
  [min int]
  [max int])]
[SoundIoRingBuffer
 (struct
  [mem SoundIoOsMirroredMemory]
  [write_offset SoundIoAtomicLong]
  [read_offset SoundIoAtomicLong]
  [capacity int])]
[SoundIoOsMirroredMemory
 (struct
  [capacity size_t]
  [address (* char)]
  [priv void*])]
[SoundIoAtomicLong long]
;; <ftype-structs>
<<SoundIo>>
<<SoundIoChannelArea>>
<<SoundIoChannelLayout>>
<<SoundIoDevice>>
<<SoundIoInStream>>
<<SoundIoOutStream>>
<<SoundIoSampleRateRange>>
<<SoundIoOsMirroredMemory>>
<<SoundIoAtomicLong>>
<<SoundIoRingBuffer>>
;; </ftype-structs>

Summa

;; <ftypes>
(define-ftype
  <<ftype-enums>>
  <<ftype-callbacks>>
  <<ftype-structs>>
)
;; </ftypes>

Procedures

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

(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)
         ...)]))
(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])

Summa

;; <ffi>
<<load-library>>
<<ftypes>>
<<define-foreign-procedure>>
<<foreign-procedures>>
;; </ffi>

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.

;; <bridge-paths>
(define bridge-source-filename "bridge.c")
(define bridge-library-filename "libbridge.so")
(define scheme-headers-path (format "/usr/local/lib/csv9.5.1/~a" (machine-type)))
;; </bridge-paths>

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

;; <build-bridge>
(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)
   (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))])
;; </build-bridge>

Machine type correspondence to platform could be found in release notes.

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

;; <build-bridge>
<<bridge-paths>>
(define init-bridge
  (begin
    (unless (file-exists? bridge-library-filename)
      <<build-bridge>>
      )
    (load-shared-object bridge-library-filename)))
;; </build-bridge>

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.

// <write_callback>
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) {
    <<fill-stream-with-zeros>>
  }

  <<copy-samples-from-buffer>>

  soundio_ring_buffer_advance_read_ptr(ring_buffer, read_count * outstream->bytes_per_frame);
}
// </write_callback>

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

// <begin-write>
if ((err = soundio_outstream_begin_write(outstream, &areas, &frame_count))) {
  fprintf(stderr, "begin_write: %s\n", soundio_strerror(err));
  exit(1);
}
// </begin-write>
// <end-write>
if ((err = soundio_outstream_end_write(outstream))) {
  fprintf(stderr, "end_write: %s\n", soundio_strerror(err));
  // REVIEW pthread_exit?
  exit(1);
}
// </end-write>
// <copy-samples-from-buffer>
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;

  <<begin-write>>

  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;
    }
  }

  <<end-write>>

  frames_left -= frame_count;
}
// </copy-samples-from-buffer>
// <fill-stream-with-zeros>
frames_left = frame_count_min;
for (;;) {
  frame_count = frames_left;
  if (!frame_count)
    return;

  <<begin-write>>

  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;
    }
  }

  <<end-write>>

  frames_left -= frame_count;
}
// </fill-stream-with-zeros>

bridge_outstream_attach_ring_buffer

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

// <bridge_outstream_attach_ring_buffer>
EXPORT void bridge_outstream_attach_ring_buffer
(struct SoundIoOutStream *outstream, struct SoundIoRingBuffer *buffer) {
  outstream->format = SoundIoFormatFloat32NE;
  outstream->userdata = buffer;
  outstream->write_callback = write_callback;
}
// </bridge_outstream_attach_ring_buffer>

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.

// <usleep>
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();
}
// </usleep>

Define foreign procedures in Scheme

;; <bridge-ffi>
(define-foreign-procedure
  [bridge_outstream_attach_ring_buffer ((* SoundIoOutStream) (* SoundIoRingBuffer)) void]
  [usleep (long #|seconds|# long #|microseconds|#) void])
;; </bridge-ffi>

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.

;; <sound-out-record>
(define-record-type sound-out
  (fields stream
          ring-buffer
          (mutable write-callback)
          (mutable write-thread)))
;; </sound-out-record>

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:

;; <make-sound-out>
(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)
;; </make-sound-out>

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.

;; <attach-buffer-to-stream>
(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)
  <<make-sound-out>>
  )
;; </attach-buffer-to-stream>

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

;; <try-open-stream>
(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))))
  <<attach-buffer-to-stream>>
  )
;; </try-open-stream>

Let’s create stream before setting its callbacks:

;; <try-create-stream>
(let ([out-stream (soundio_outstream_create device)])
  (when (ftype-pointer-null? out-stream)
    (error "soundio_outstream_create" "out of memory"))
  <<try-open-stream>>
  )
;; </try-create-stream>

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

;; <try-create-device>
(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"))
    <<try-create-stream>>
    ))
;; </try-create-device>

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

;; <try-create-connect-sio>
(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)
    <<try-create-device>>
    ))
;; </try-create-connect-sio>

Now just give it a name =)

;; <open-default-out-stream>
(define (open-default-out-stream write-callback)
  <<try-create-connect-sio>>
  )
;; </open-default-out-stream>

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.

;; <start-out-stream>
(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)))
;; </start-out-stream>
;; <stop-out-stream>
(define (stop-out-stream sound-out)
  (sound-out-write-thread-set! sound-out #f)
  (soundio_outstream_pause (sound-out-stream sound-out) #t))
;; </stop-out-stream>

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

;; <teardown-out-stream>
(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)))
;; </teardown-out-stream>
;; <channel-count>
(define (channel-count sound-out)
  (ftype-ref SoundIoOutStream
             (layout channel_count)
             (sound-out-stream sound-out)))
;; </channel-count>
;; <sample-rate>
(define (sample-rate sound-out)
  (ftype-ref SoundIoOutStream
             (sample_rate)
             (sound-out-stream sound-out)))
;; </sample-rate>

Summa

;; <high-level-wrapper>
<<init-bridge>>
<<bridge-ffi>>
<<sound-out-record>>
<<open-default-out-stream>>
<<start-out-stream>>
<<stop-out-stream>>
<<teardown-out-stream>>
<<channel-count>>
<<sample-rate>>
;; </high-level-wrapper>

Helpers

make-ftype-pointer locks object as pointed 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.

;; <unlock-ftype-pointer>
(define (unlock-ftype-pointer fptr)
  (unlock-object
   (foreign-callable-code-object
    (ftype-pointer-address fptr))))
;; </unlock-ftype-pointer>

Summa

;; <helpers>
<<unlock-ftype-pointer>>
;; </helpers>

Example

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

(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)

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.

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.

Files

<<helpers>>
<<ffi>>
(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")
  <<high-level-wrapper>>
)
#ifdef WIN32
#define EXPORT extern __declspec (dllexport)
#else
#define EXPORT extern
#endif

#include <sys/select.h>
#include <soundio/soundio.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

#include "scheme.h"

<<write_callback>>
<<bridge_outstream_attach_ring_buffer>>
<<usleep>>