--------------------------------------------------------------------------------
-- |
-- Module      :  Sound.OpenAL.ALC.Device
-- Copyright   :  (c) Sven Panne 2003-2009
-- License     :  BSD-style (see the file LICENSE)
-- 
-- Maintainer  :  sven.panne@aedion.de
-- Stability   :  stable
-- Portability :  portable
--
-- This module corresponds to section 6.1 (Managing Devices) of the OpenAL
-- Specification and Reference (version 1.1).
--
-- ALC introduces the notion of a device. A device can be, depending on the
-- implementation, a hardware device, or a daemon\/OS service\/actual
-- server. This mechanism also permits different drivers (and hardware) to
-- coexist within the same system, as well as allowing several applications to
-- share system resources for audio, including a single hardware output
-- device. The details are left to the implementation, which has to map the
-- available backends to unique device specifiers.
--
--------------------------------------------------------------------------------

module Sound.OpenAL.ALC.Device (
   Device, openDevice, closeDevice,
   defaultDeviceSpecifier, deviceSpecifier, allDeviceSpecifiers
) where

import Data.StateVar
import Foreign.Ptr ( Ptr, nullPtr )
import Foreign.Marshal.Utils ( maybePeek )
import Sound.OpenAL.ALC.BasicTypes ( ALCchar )
import Sound.OpenAL.ALC.QueryUtils (
   StringQuery(..), getString, getStringRaw, alcIsExtensionPresent )
import Sound.OpenAL.ALC.String ( withALCString, peekALCString, peekALCStrings )
import Sound.OpenAL.Config (
   Device, ALCdevice(..), unmarshalDevice, closeDevice )

--------------------------------------------------------------------------------

-- | 'openDevice' allows the application (i.e. the client program) to connect to
-- a device (i.e. the server). If the function returns 'Nothing', then no sound
-- driver\/device has been found. The argument to 'openDevice' specifies a
-- certain device or device configuration. If it is 'Nothing', the
-- implementation will provide an implementation specific default, see
-- 'defaultDeviceSpecifier'. Otherwise it is 'Just' an implementation-dependent
-- string. You can use 'allDeviceSpecifiers' to get a list of the known OpenAL
-- devices.
--
-- /Notes for Windows:/ There are 3 possible device strings, each having a
-- deprecated equivalent for legacy applications only:
--
-- * @\"Generic Hardware\"@ (legacy string: @\"DirectSound3D\"@)
--
-- * @\"Generic Software\"@ (legacy string: @\"DirectSound\"@)
--
-- * @\"Generic Software Fallback\"@ (legacy string: @\"MMSYSTEM\"@)
--
-- /Notes for Linux\/\*nix:/ If an @.openalrc@ file is present in the user\'s
-- home directory, it is loaded first, otherwise @\/etc\/openalrc@ is tried.
-- The bindings (if any) of @devices@, @direction@, @sampling-rate@, and
-- @speaker-num@ (see below) after loading one of these files take precedence
-- over any bindings done via the argument to 'openDevice'.
--
-- The syntax of these files is lisp-based and a sequence of expressions, where
-- an expression is one the following:
--
-- * A boolean value of the form @\#f@ or @\#t@, which evaluate to /false/ and
-- /true/, respectively.
--
-- * An non-negative integer value, i.e. a sequence of decimal digits,
-- evaluating to itself.
--
-- * A (signed) floating point value, as recoginzed by C\'s @atof()@, evaluating
-- to itself.
--
-- * A pointer value of the form @\#p/num/@, where /num/ can be an octal,
-- decimal or hexadecimal value, as recoginzed by C\`s @strtol()@, evaluating
-- to itself. This kind of expression is currently unused.
--
-- * A string, i.e. a sequence of printable\/whitespace characters between
-- double quotes, evaluating to itself.
--
-- * A symbol, i.e. a sequence of almost all characters which don\'t form a
-- simple expression like the ones mentioned below, e.g. @foo@, @bar1@, @3baz@,
-- ... The symbol evaluates to the value currently bound to it.
--
-- * A function application of the form @(/symbol/ /expression/...)@. The
-- function bound to the symbol is applied to the evaluated arguments.
-- 
-- * A quotation of the form @(quote /expression/)@ or @\'/expression/@,
-- evaluating to the unevaluated /expression/ itself.
--
-- * A definition of the form @(define /symbol/ /expression/)@, binding /symbol/
-- to the value of /expression/. The whole expression evaluates to the value of
-- /expression/, too.
--
-- * A conjunction of boolean expressions of the form @(and /expression/...)@.
-- Each /expression/ is evaluated in turn, and if one of them evaluates to
-- /false/, the value of the whole expression is /false/. Otherwise the value is
-- /true/.
--
-- * An extension loading mechanism of the form @(load-extension
-- /libraryName/)@, where /libraryName/ has to evaluate to a string. This tries
-- to load the dynamic library with up to 3 special entry points:
-- @alExtension_03282000@ (pointing to a mandatory NULL-terminated sequence of
-- pairs of pointers to names and extension functions), @alExtInit_03282000@ (an
-- optional initialization function), and @alExtFini_03282000@ (an optional
-- cleanup function). If the extension could be loaded successfully, the whole
-- expression evaluates to /true/, otherwise to /false/.
--
-- Some symbols have a special meaning for OpenAL:
--
-- [@devices@] Has the form @(/devspec/...)@, where /devspec/ is either a
-- symbol\/string specifying a device or @(/device/ /device-param/...)@,
-- specifying a device with additional parameters. These optional device
-- parameters are stored in a variable @device-params@, but are currently
-- unused. The first device which can successfully opened is used.
--
-- [@direction@] Type string or symbol: @\"read\"@ specifies that the device
-- should be an input device, everything else means output device (default).
--
-- [@sampling-rate@] Type integer or float: Specifies the internal mixing
-- frequency, default is 44.1kHz.
--
-- [@speaker-num@] Type integer or float: Specifies the number of speakers,
-- which can be 1, 2 (default), or 4.
--
-- [@alsa-device@] Type string, @alsa@ backend only: Specifies both
-- @alsa-out-device@ and @alsa-in-device@, default @\"plughw:0,0\"@.
-- 
-- [@alsa-out-device@] Type string, @alsa@ backend only: Specifies the ALSA
-- output device, defaults to the value of @alsa-device@.
--
-- [@alsa-in-device@] Type string, @alsa@ backend only: Specifies the ALSA
-- input device, defaults to the value of @alsa-device@.
--
-- [@native-in-device@] Type string, @native@ backend on IRIX only.
--
-- [@native-out-device@] Type string, @native@ backend on IRIX only.
--
-- [@native-rear-out-device@] Type string, @native@ backend on IRIX only.
--
-- [@native-use-select@] Type boolean, @native@ backend on Linux only: If @\#t@,
-- wait up to 0.8sec for the device to become ready for writing. If @\#f@, just
-- try to write and hope it won\'t hang forever. The latter might be necessary
-- for some drivers which don\'t implement @select()@ , like some Aureal
-- drivers.
--
-- [@lin-dsp-path@] Type string, @native@ backend on Linux only: Path to DSP
-- device for writing, tried before @\/dev\/sound\/dsp@ and @\/dev\/sound@ if
-- set.
--
-- [@lin-dsp-read-path@] Type string, @native@ backend on Linux only: Path to
-- DSP device for reading, tried before @\/dev\/sound\/dsp@ and @\/dev\/sound@
-- if set. Defaults to the value of @lin-dsp-path@.
--
-- [@native-backend-debug@] Type boolean, @native@ backend on Darwin only: If
-- set to @\#f@, be a bit verbose on stderr about what\'s going on in the
-- backend.
--
-- [@source-rolloff-factor@] Type integer or float: Value of the initial rolloff
-- factor for sources, default is @1.0@.
--
-- [@listener-position@] List of 3 integers or floats: Value of the initial
-- listener position, default is @(0 0 0)@.
--
-- [@listener-velocity@] List of 3 integers or floats: Value of the initial
-- listener velocity, default is @(0 0 0)@.
--
-- [@listener-orientation@] List of 6 integers or floats: Value of the initial
-- listener orientation (at\/up), default is @(0 0 -1 0 1 0)@.
--
-- The string given to 'openDevice' has to be of the form @\'((/symbol/
-- /expression/) ...)@, which means basically a sequence of @define@
-- expressions. Example:
--
-- @
-- \"\'((sampling-rate 8000) (native-backend-debug \#f))\"
-- @
--
-- /Note:/ The information above has been reverse-engineered from the OpenAL SI
-- and could be inaccurate. Any corrections and\/or additions are highly
-- welcome.

openDevice :: Maybe String -> IO (Maybe Device)
openDevice maybeDeviceSpec =
   fmap unmarshalDevice $
      maybe (alcOpenDevice nullPtr)   -- use preferred device
            (flip withALCString alcOpenDevice)
            maybeDeviceSpec

foreign import CALLCONV unsafe "alcOpenDevice"
   alcOpenDevice :: Ptr ALCchar -> IO ALCdevice

--------------------------------------------------------------------------------

-- | Contains 'Just' the specifier string for the default device or 'Nothing' if
-- there is no sound support at all.

defaultDeviceSpecifier :: GettableStateVar (Maybe String)
defaultDeviceSpecifier = makeGettableStateVar $
   getStringRaw Nothing DefaultDeviceSpecifier >>= maybePeek peekALCString

--------------------------------------------------------------------------------

-- | Contains the specifier string for the given device.

deviceSpecifier :: Device -> GettableStateVar String
deviceSpecifier device = makeGettableStateVar $
   getString (Just device) DeviceSpecifier

--------------------------------------------------------------------------------

-- | Contains a list of specifiers for all available devices.

allDeviceSpecifiers :: GettableStateVar [String]
allDeviceSpecifiers = makeGettableStateVar $ do
   enumExtPresent <- get (alcIsExtensionPresent Nothing "ALC_ENUMERATION_EXT")
   if enumExtPresent
      then getStringRaw Nothing DeviceSpecifier >>= peekALCStrings
      else get defaultDeviceSpecifier >>= (return . maybe [] (\s -> [s]))