Patch title: Release 95 bulk changes
Abstract:
File: /pliant/linux/multimedia/audio.pli
Key:
    Removed line
    Added line
module "/pliant/admin/file.pli"
module "/pliant/language/unsafe.pli"
module "/pliant/language/stream.pli"
module "/pliant/language/stream/pipe.pli"
module "/pliant/language/os.pli"
module "/pliant/admin/execute.pli"

module "/pliant/language/stream/listmode.pli"
module "/pliant/language/stream/openmode.pli"
module "/pliant/language/stream/flushmode.pli"
module "/pliant/language/stream/multi.pli"
module "/pliant/language/stream/filesystembase.pli"

constant alsa false # (file_query "file:/lib/libasound.so.2" standard)=defined or (file_query "file:/usr/lib/libasound.so.2" standard)=defined
constant recover true
constant recover2 true
constant resample true
constant timeout 5


#----------------------------------------------------------------------------
#  OS interface


if alsa
  # http://www.alsa-project.org/
  # http://equalarea.com/paul/alsa-audio.html

  function snd_pcm_hw_params_sizeof -> s
    arg Int s
    external "libasound.so.2" "snd_pcm_hw_params_sizeof"

  function snd_pcm_open pcm name stream mode -> err
    arg_rw Address pcm ; arg CStr name ; arg Int stream mode err
    external "libasound.so.2" "snd_pcm_open"
  constant SND_PCM_STREAM_PLAYBACK 0
  constant SND_PCM_STREAM_CAPTURE 1
  constant SND_PCM_NONBLOCK 1

  function snd_pcm_hw_params_malloc ptr -> err
    arg_rw Address ptr ; arg Int err
    external "libasound.so.2" "snd_pcm_hw_params_malloc"

  function snd_pcm_hw_params_any pcm hw -> err
    arg Address pcm hw ; arg Int err
    external "libasound.so.2" "snd_pcm_hw_params_any"

  function snd_pcm_hw_params_set_access pcm hw access -> err
    arg Address pcm hw ; arg Int access err
    external "libasound.so.2" "snd_pcm_hw_params_set_access"
  constant SND_PCM_ACCESS_RW_INTERLEAVED 3

  function snd_pcm_hw_params_set_format pcm hw format -> err
    arg Address pcm hw ; arg Int format err
    external "libasound.so.2" "snd_pcm_hw_params_set_format"
  constant SND_PCM_FORMAT_S8 0
  constant SND_PCM_FORMAT_S16_LE 2
  constant SND_PCM_FORMAT_S24_3LE 32

  function snd_pcm_hw_params_set_rate pcm hw rate dir -> err
    arg Address pcm hw ; arg Int rate dir err
    external "libasound.so.2" "snd_pcm_hw_params_set_rate"

  function snd_pcm_hw_params_set_rate_near pcm hw rate dir -> err
    arg Address pcm hw ; arg_rw Int rate ; arg Int dir err
    external "libasound.so.2" "snd_pcm_hw_params_set_rate_near"

  function snd_pcm_hw_params_set_channels pcm hw channels -> err
    arg Address pcm hw ; arg Int channels err
    external "libasound.so.2" "snd_pcm_hw_params_set_channels"

  function snd_pcm_hw_params pcm hw -> err
    arg Address pcm hw ; arg Int err
    external "libasound.so.2" "snd_pcm_hw_params"

  function snd_pcm_hw_params_free hw
    arg Address hw
    external "libasound.so.2" "snd_pcm_hw_params_free"

  function snd_pcm_prepare pcm -> err
    arg Address pcm ; arg Int err
    external "libasound.so.2" "snd_pcm_prepare"

  function snd_pcm_readi pcm buffer size -> err
    arg Address pcm ; arg Address buffer ; arg Int size err
    external "libasound.so.2" "snd_pcm_readi"

  function snd_pcm_writei pcm buffer size -> err
    arg Address pcm ; arg Address buffer ; arg Int size err
    external "libasound.so.2" "snd_pcm_writei"

  function snd_pcm_status_get_delay pcm -> frames
    arg Address pcm ; arg Int frames
    external "libasound.so.2" "snd_pcm_status_get_delay"

  function snd_pcm_drain pcm -> err
    arg Address pcm ; arg Int err
    external "libasound.so.2" "snd_pcm_drain"

  function snd_pcm_drop pcm -> err
    arg Address pcm ; arg Int err
    external "libasound.so.2" "snd_pcm_drop"

  function snd_pcm_close pcm -> err
    arg Address pcm ; arg Int err
    external "libasound.so.2" "snd_pcm_close"

else

  constant os_SNDCTL_DSP_RESET 0C0045000h
  constant os_SNDCTL_DSP_SYNC 0C0045001h
  constant os_SNDCTL_DSP_SPEED 0C0045002h
  constant os_SNDCTL_DSP_STEREO 0C0045003h
  constant os_SNDCTL_DSP_SETFMT 0C0045005h
  constant os_SNDCTL_DSP_CHANNELS 0C0045006h
  constant os_SNDCTL_DSP_GETODELAY 80045017h

  type os_count_info
    field Int bytes
    field Int blocs
    field Int ptr


constant os_SOUND_MIXER_READ_VOLUME 080044D00h
constant os_SOUND_MIXER_WRITE_VOLUME 0C0044D00h
constant os_SOUND_MIXER_READ_PCM 080044D04h
constant os_SOUND_MIXER_WRITE_PCM 0C0044D04h
constant os_SOUND_MIXER_READ_SPEAKER 080044D05h
constant os_SOUND_MIXER_WRITE_SPEAKER 0C0044D05h
constant os_SOUND_MIXER_READ_LINE 080044D06h
constant os_SOUND_MIXER_WRITE_LINE 0C0044D06h
constant os_SOUND_MIXER_READ_HEADPHONE 080044D0Ah
constant os_SOUND_MIXER_WRITE_HEADPHONE 0C0044D0Ah
constant os_SOUND_MIXER_READ_MIC 080044D07h
constant os_SOUND_MIXER_WRITE_MIC 0C0044D07h
  

#----------------------------------------------------------------------------
#  WAV encoding


type WavSignature
  field (Array Char 4) riff
  field uInt32_li length
  field (Array Char 4) wave
 
type WavFormat
  field (Array Char 4) fmt_
  field uInt32_li length
  field uInt16_li version
  field uInt16_li channel
  field uInt32_li rate
  field uInt32_li bytes_per_second
  field uInt16_li bytes_per_sample
  field uInt16_li bits_per_sample
 
type WavChunk
  field (Array Char 4) data
  field uInt32_li length

type WavHeader
  field WavSignature signature
  field WavFormat format
  field WavChunk chunk


#----------------------------------------------------------------------------
#  Pliant Stream emulation


type AudioDriver
  if alsa
    field Address pcm
    field (Array Byte 256) buffer
    field Int garbadge
  else
    field Stream dsp
  field Str title
  field Int channels bits rate unit
  if resample
    field Int record_bits record_rate record_unit
    field CBool record_resample ; field Array:Float record_sum ; field Float record_slice
    field Int record_mini record_maxi
  field DateTime time
  field CBool wav
  field WavHeader wav_header
  field Int wav_offset
  field uInt wav_remain

StreamDriver maybe AudioDriver

method drv setup options flags -> status
  arg_rw AudioDriver drv ; arg Str options ; arg Int flags ; arg ExtendedStatus status
  drv title := options option "title" Str
  drv channels := shunt (options option "mono") 1 (options option "stereo") 2 (options option "channels" Int 2)
  drv bits := options option "bits" Int 16
  drv rate := options option "rate" Int 44100
  drv unit := drv:channels*drv:bits\8
  var Int bits := drv bits ; var Int rate := drv rate
  if resample
    drv record_bits := options option "record_bits" Int drv:bits
    drv record_rate := options option "record_rate" Int drv:rate
    drv record_unit := drv:channels*drv:record_bits\8
    drv record_resample := (flags .and. in)<>0 and drv:record_bits>=drv:bits and drv:record_rate>=drv:rate and (drv:record_bits<>drv:bits or drv:record_rate<>drv:rate)
    if drv:record_resample
      drv:record_sum size := drv channels
      for (var Int i) 0 drv:channels-1
        drv:record_sum i := 0
      drv record_slice := 0
      drv record_mini := 0 ; drv record_maxi := 0
      bits := drv record_bits
      rate := drv record_rate
  status := success
  if alsa
    var Int err := snd_pcm_hw_params_malloc (var Address hw)
    if err<>0
      return (failure "snd_pcm_hw_params_malloc "+string:err)
    err := snd_pcm_hw_params_any drv:pcm hw
    if err<>0
      status := failure "snd_pcm_hw_params_any "+string:err
    err := snd_pcm_hw_params_set_access drv:pcm hw SND_PCM_ACCESS_RW_INTERLEAVED
    if err<>0
      status := failure "snd_pcm_hw_params_set_access "+string:err
  if alsa
    var Int err := snd_pcm_hw_params_set_channels drv:pcm hw drv:channels
    if err<>0
      status := failure "snd_pcm_hw_params_set_channels error "+string:err
  else
    if (os_ioctl drv:dsp:stream_handle os_SNDCTL_DSP_CHANNELS (addressof drv:channels))<>0
      status := failure "failed to set audio channels"
    # var Int stereo := channels-1
    # if (os_ioctl drv:dsp:stream_handle os_SNDCTL_DSP_STEREO addressof:stereo)<>0
    #   status := failure "failed to set audio mono/stereo"
  if alsa
    var Int err := snd_pcm_hw_params_set_format drv:pcm hw (shunt bits=8 SND_PCM_FORMAT_S8 bits=16 SND_PCM_FORMAT_S16_LE bits=24 SND_PCM_FORMAT_S24_3LE -1)
    if err<>0
      status := failure "snd_pcm_hw_params_set_format error "+string:err
  else
    if (os_ioctl drv:dsp:stream_handle os_SNDCTL_DSP_SETFMT addressof:bits)<>0
      status := failure "failed to set audio sampling resolution"
  if alsa
    var Int err := snd_pcm_hw_params_set_rate drv:pcm hw rate 0
    if err<>0
      status := failure "snd_pcm_hw_params_set_rate error "+string:err
  else
    if (os_ioctl drv:dsp:stream_handle os_SNDCTL_DSP_SPEED addressof:rate)<>0
      status := failure "failed to set audio sampling rate"
  if alsa
    err := snd_pcm_hw_params drv:pcm hw
    if err<>0
      status := failure "snd_pcm_hw_params "+string:err
    snd_pcm_hw_params_free hw


if resample

  function read_bits a bits channel -> v
    arg Address a ; arg Int bits channel v
    if bits=8
      v := a map Int8 channel
    eif bits=16
      v := a map Int16 channel
    eif bits=24
      memory_copy (a translate Byte 3*channel) addressof:v 3
      if (v .and. 800000h)<>0
        v := v .or. -1000000h
      else
        v := v .and. 0FFFFFFh
  
  function write_bits a bits channel v
    arg Address a ; arg Int bits channel v
    if bits=8
      a map Int8 channel := v
    eif bits=16
      a map Int16 channel := v
    eif bits=24
      memory_copy addressof:v (a translate Byte 3*channel) 3
    
method drv read buf mini maxi -> red
  arg_rw AudioDriver drv ; arg Address buf ; arg Int mini maxi red
  if alsa
    var Int frames := maxi\drv:unit
    if resample and drv:record_resample
      var Address record_buf := memory_allocate frames*drv:record_unit null
      var Int err := snd_pcm_readi drv:pcm record_buf frames
      if recover and err<0
        snd_pcm_prepare drv:pcm
        err := snd_pcm_readi drv:pcm record_buf frames
      var Address src := record_buf ; var Float src_period := 1/drv:record_rate
      var Address dest := buf ; var Float dest_period := 1/drv:rate
      for (var Int i) 0 err-1
        var CBool full := drv:record_slice+src_period>=dest_period
        if full
          var Float f := dest_period-drv:record_slice
          for (var Int j) 0 drv:channels-1
            drv:record_sum j += (read_bits src drv:record_bits j)*f
          for (var Int j) 0 drv:channels-1
            var Int u := cast drv:record_sum:j/dest_period/2^(drv:record_bits-drv:bits) Int
            drv record_mini := min drv:record_mini u
            drv record_maxi := max drv:record_maxi u
            u := min (max u -(2^(drv:bits-1)-1)) 2^(drv:bits-1)-1
            write_bits dest drv:bits j u
            drv:record_sum j -= u*2^(drv:record_bits-drv:bits)*dest_period
          dest := dest translate Byte drv:unit
          var Float f :=  drv:record_slice+src_period-dest_period
          for (var Int j) 0 drv:channels-1
            drv:record_sum j += (read_bits src drv:record_bits j)*f
          drv record_slice := f
        else
          for (var Int j) 0 drv:channels-1
            drv:record_sum j += (read_bits src drv:record_bits j)*src_period
          drv record_slice += src_period
        src := src translate Byte drv:record_unit
      red := (cast dest Int) .-. (cast buf Int)
      memory_free record_buf
    else
      var Int err := snd_pcm_readi drv:pcm buf frames
      if recover and err<0
        snd_pcm_prepare drv:pcm
        err := snd_pcm_readi drv:pcm buf frames
      red := shunt err>=0 err*drv:unit 0
  else
    red := drv:dsp:stream_driver read buf mini maxi
  drv:time seconds += red/drv:unit/drv:rate

method drv write buf mini maxi -> written
  oarg_rw AudioDriver drv ; arg Address buf ; arg Int mini maxi written
  written := 0
  if drv:wav and drv:wav_offset<WavHeader:size
    written := min maxi WavHeader:size-drv:wav_offset
    memory_copy buf ((addressof drv:wav_header) translate Byte drv:wav_offset) written
    drv wav_offset += written
    if drv:wav_offset=WavHeader:size
      if (drv setup "channels "+string:(cast drv:wav_header:format:channel Int)+" rate "+string:(cast drv:wav_header:format:rate Int)+" bits "+string:(cast drv:wav_header:format:bits_per_sample Int) out)=failure
        return 0
      drv wav_remain := drv:wav_header:chunk length
  if written<mini
    var Int base := written
    if alsa
      if drv:garbadge<>0
        var Int step := min maxi-written drv:unit-drv:garbadge
        memory_copy (buf translate Byte written) ((addressof drv:buffer) translate Byte drv:garbadge) step
        drv garbadge += step
        written += step
        if drv:garbadge=drv:unit
          var Int err := snd_pcm_writei drv:pcm (addressof drv:buffer) 1
          drv garbadge := 0
      var Int frames := (maxi-written)\drv:unit
      if frames>0
        var Int err := snd_pcm_writei drv:pcm (buf translate Byte written) frames
        if recover and err<0
          snd_pcm_prepare drv:pcm
          err := snd_pcm_writei drv:pcm (buf translate Byte written) frames
        written += shunt err>=0 err*drv:unit 0
      else
        var Int step := min maxi-written drv:unit-drv:garbadge
        memory_copy (buf translate Byte written) ((addressof drv:buffer) translate Byte drv:garbadge) step
        drv garbadge += step
        written += step
    else
      var Int step := drv:dsp:stream_driver write (buf translate Byte written) mini-written maxi-written
      written += step
    drv:time seconds += (written-base)/drv:unit/drv:rate

method drv configure command stream -> status
  arg_rw AudioDriver drv ; arg Str command ; arg_rw Stream stream ; arg ExtendedStatus status
  if (command option "rate")
    status := drv setup command stream:stream_flags
  else
    status := failure

method drv query command stream answer -> status
  oarg_rw AudioDriver drv ; arg Str command ; arg_rw Stream stream ; arg_w Str answer ; arg ExtendedStatus status
  if command="time"
    var DateTime time := drv time
    if alsa
      time seconds -= (snd_pcm_status_get_delay drv:pcm)/drv:rate
    else
      if (os_ioctl drv:dsp:stream_handle os_SNDCTL_DSP_GETODELAY addressof:(var Int delay))=0
        time seconds -= delay/drv:unit/drv:rate
    answer := string time
    status := success
  eif command="pending"
    if alsa
      answer := string ((cast stream:stream_write_cur Int) .-. (cast stream:stream_write_buf Int))/drv:unit/drv:rate+(snd_pcm_status_get_delay drv:pcm)/drv:rate
      status := success
    else
      if (os_ioctl drv:dsp:stream_handle os_SNDCTL_DSP_GETODELAY addressof:(var Int delay))=0
        answer := string ((cast stream:stream_write_cur Int) .-. (cast stream:stream_write_buf Int))/drv:unit/drv:rate+delay/drv:unit/drv:rate
        status := success
      else
        answer := ""
        status := failure
  eif command="title"
    answer := drv title
    status := success
  eif command="time"
    answer := string drv:time
    status := success
  eif command="channels"
    answer := string drv:channels
    status := success
  eif command="bits"
    answer := string drv:bits
    status := success
  eif command="rate"
    answer := string drv:rate
    status := success
  eif command="unit"
    answer := string drv:unit
    status := success
  eif resample and command="range"
    answer := (string drv:record_mini/(2^(drv:bits-1)-1) "fixed 2")+" "+(string drv:record_maxi/(2^(drv:bits-1)-1) "fixed 2")
    status := shunt drv:record_resample success failure
  else
    status := failure

if false
  method drv flush level -> status
    arg_rw AudioDriver drv ; arg Int level ; arg Status status
    if level=sync
      if alsa
        sleep (snd_pcm_status_get_delay drv:pcm)/drv:rate
      else
        if (os_ioctl drv:dsp:stream_handle os_SNDCTL_DSP_GETODELAY addressof:(var Int delay))=0
          sleep delay/drv:unit/drv:rate
    status := success

method drv close -> status
  arg_rw AudioDriver drv ; arg ExtendedStatus status
  status := success
  if alsa
    snd_pcm_drain drv:pcm
    var Int err := snd_pcm_close drv:pcm
    if err<>0
      status := failure "snd_pcm_close error "+string:err

type AudioFileSystem
  void
FileSystem maybe AudioFileSystem

method fs open name options flags stream support -> status
  arg_rw AudioFileSystem fs ; arg Str name options ; arg Int flags ; arg_rw Stream stream support ; arg ExtendedStatus status
  var Int default_volume := options option "volume" Int
  if default_volume=defined
    (var Stream s) open "file:/dev/mixer"+name in+out+safe
    var Int volume := options option "master_volume" Int default_volume ; volume += volume*256
    os_ioctl s:stream_handle os_SOUND_MIXER_WRITE_VOLUME addressof:volume
    var Int volume := options option "pcm_volume" Int default_volume ; volume += volume*256
    os_ioctl s:stream_handle os_SOUND_MIXER_WRITE_PCM addressof:volume
    var Int volume := options option "speaker_volume" Int default_volume ; volume += volume*256
    os_ioctl s:stream_handle os_SOUND_MIXER_WRITE_SPEAKER addressof:volume
    var Int volume := options option "line_volume" Int default_volume ; volume += volume*256
    os_ioctl s:stream_handle os_SOUND_MIXER_WRITE_LINE addressof:volume
    var Int volume := options option "headphone_volume" Int default_volume ; volume += volume*256
    os_ioctl s:stream_handle os_SOUND_MIXER_WRITE_HEADPHONE addressof:volume
    var Int volume := options option "mic_volume" Int default_volume ; volume += volume*256
    os_ioctl s:stream_handle os_SOUND_MIXER_WRITE_MIC addressof:volume
    s close
  var Link:AudioDriver drv :> new AudioDriver
  if alsa
    var Str dev := options option "device" Str "hw:"+(shunt name<>"" name "0")
    var Int err := snd_pcm_open drv:pcm dev (shunt (flags .and. out)<>0 SND_PCM_STREAM_PLAYBACK SND_PCM_STREAM_CAPTURE) 0 # SND_PCM_NONBLOCK
    if err<>0
      return (failure "snd_pcm_open error "+string:err)
  else
    status := drv:dsp open (options option "device" Str "device:/dsp"+name) (flags .or. safe)
    if status=failure
      return (failure "failed to open OSS device "+drv:dsp:name)
  drv wav := options option "wav"
  drv wav_offset := 0
  if alsa
    drv garbadge := 0
  if (flags .and. out)<>0 or not drv:wav
    status := drv setup options flags
  else
    status := success
  if status=success
    stream stream_driver :> drv
    var Str format := options option "audio_format" Str
    if (flags .and. out)<>0 and (format="flac" or format="ogg" or format="mp3")
      drv wav := format<>"mp3"
      stream_pipe (var Str encoded_in) (var Str encoded_out)
      stream_pipe (var Str wav_in) (var Str wav_out)
      var Link:Stream device :> new Stream
      device open encoded_out out+safe
      stream stream_driver :> device stream_driver
      device stream_driver :> drv
      thread
        if format="flac"
          execute "flac --decode --stdout --silent -" input encoded_in output wav_out
        eif format="ogg"
          execute "oggdec --quiet -o - -" input encoded_in output wav_out
        eif format="mp3"
          execute "mpg123 -s -q -" input encoded_in output wav_out
      thread
        (var Stream clear) open wav_in in+safe
        while not clear:atend and device=success
          raw_copy clear device 1 2^24
        if false
          clear raw_read addressof:(var WavHeader header) WavHeader:size
          device raw_write addressof:header WavHeader:size
          while not clear:atend and device=success
            clear raw_read addressof:(var Int16 sample) Int16:size
            device raw_write addressof:sample Int16:size
            clear raw_read addressof:(var Int16 sample) Int16:size
            # sample := 0
            device raw_write addressof:sample Int16:size
        else
          while not clear:atend and device=success
            raw_copy clear device 1 2^24
    eif (flags .and. out)<>0 and format="wav"
      drv wav := true
  eif alsa
    snd_pcm_close drv:pcm
  drv time := options option "time" DateTime datetime

gvar AudioFileSystem audio_file_system
pliant_multi_file_system mount "audio:" "" audio_file_system


#----------------------------------------------------------------------------
#  High level interface


constant soundcache bigcache

gvar Sem play_sem
gvar Stream play_stream
gvar CBool play_stop := false

function audio_play_stop
  play_stop := true
  play_sem request ; play_sem release
  play_stop := false
 
function audio_play_start file options -> status
  arg Str file options ; arg ExtendedStatus status
  var DateTime time := options option "time" DateTime datetime
  var Time skip := options option "skip" Time (time 0 0 0 0)
  time seconds += skip seconds
  audio_play_stop
  play_sem request
  status := play_stream open "audio:" "time "+string:time+" "+options+" audio_format "+string:(file (file search_last "." file:len)+1 file:len) out+safe+soundcache
  if status=success
    thread
      part play "Sound play"
        if file:len>0 and (file file:len-1)="/"
          var Array:FileInfo files := file_list file standard+sorted
          var Int first := (options option "track" Int 1)-1
          for (var Int i) first files:size-1
            var Int lap := 0
            part play_track
              var Str f := files:i name
              if (play_stream open "audio:" "time "+string:time+" "+options+" audio_format "+string:(f (f search_last "." f:len)+1 f:len) out+safe+soundcache)=success
                console "track " i+1 eol
                (var Stream data) open files:i:name in+safe+soundcache
                while not play_stop and (raw_copy data play_stream 1 2^22)>0
                  void
              eif lap<500
                sleep 0.01
                lap += 1
                restart play_track
        else
          (var Stream data) open file in+safe+soundcache
          if skip:seconds>0
            (play_stream query "rate") parse (var Int rate)
            (play_stream query "unit") parse (var Int unit)
            var Int i := cast skip:seconds Int
            if i>skip:seconds
              i -= 1
            var Float r := skip:seconds-i
            data configure "seek "+(string 1n*i*rate*unit+(cast r*rate Int)*unit)
          while not play_stop and pliant_execution_phase=execution_phase_run and (raw_copy data play_stream 1 2^22)>0
            void
        play_stream close
        play_sem release
  else
    play_sem release

function audio_play_title -> title
  arg Str title
  title := play_stream safe_query "title"

function audio_play_time -> dt
  arg DateTime dt
  if not ((play_stream safe_query "time") parse dt)
    dt := undefined


gvar Sem record_sem
gvar Stream record_stream
gvar CBool record_stop := false

function audio_record_stop
  record_stop := true
  record_sem request ; record_sem release
  record_stop := false
 
function audio_record_start file options -> status
  arg Str file options ; arg ExtendedStatus status
  audio_record_stop
  record_sem request
  status := record_stream open "audio:" options in+safe+soundcache
  if status=success
    thread
      part record "Sound recording"
        (var Stream data) open file out+safe+soundcache
        if recover2
          part record2
            while not record_stop and pliant_execution_phase=execution_phase_run and (raw_copy record_stream data 1 2^22)>0
              void
            if not record_stop and record_stream=failure and (record_stream open "audio:" options in+safe+soundcache)=success
              restart record2    
        else
          while not record_stop and pliant_execution_phase=execution_phase_run and (raw_copy record_stream data 1 2^22)>0
            void
        record_stream close
        record_sem release
  else
    record_sem release

function audio_record_title -> title
  arg Str title
  title := record_stream safe_query "title"

function audio_record_time -> dt
  arg DateTime dt
  if not ((record_stream safe_query "time") parse dt)
    dt := undefined

function audio_record_range -> r
  arg Str r
  r := record_stream safe_query "range"

  
export audio_play_start audio_play_title audio_play_time audio_play_stop
export audio_record_start audio_record_title audio_record_time audio_record_range audio_record_stop