--------------------------------------------------------------------------------
-- |
-- Module      :  Sound.OpenAL.AL.Source
-- 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 sections 4.1 (Basic Listener and Source
-- Attributes) and 4.3 (Source Objects) of the OpenAL Specification and
-- Reference (version 1.1).
--
-- Sources specify attributes like position, velocity, and a buffer with sample
-- data. By controlling a source\'s attributes the application can modify and
-- parameterize the static sample data provided by the buffer referenced by the
-- source. Sources define a localized sound, and encapsulate a set of attributes
-- applied to a sound at its origin, i.e. in the very first stage of the
-- processing on the way to the listener. Source related effects have to be
-- applied before listener related effects unless the output is invariant to any
-- collapse or reversal of order. OpenAL also provides additional functions to
-- manipulate and query the execution state of sources: the current playing
-- status of a source, including access to the current sampling position within
-- the associated buffer.
--
--------------------------------------------------------------------------------

module Sound.OpenAL.AL.Source (
   -- * The Source Type
   Source,

   -- * Source Attributes

   -- ** Basic Source Attributes
   sourcePosition, sourceVelocity, sourceGain,

   -- ** Source Positioning
   SourceRelative(..), sourceRelative,

   -- ** Source Type
   SourceType(..), sourceType,

   -- ** Buffer Looping
   LoopingMode(..), loopingMode,

   -- ** Current Buffer
   buffer,

   -- ** Queue State Queries
   buffersQueued, buffersProcessed,

   -- ** Bounds on Gain
   gainBounds,

   -- ** Distance Model Attributes
   referenceDistance, rolloffFactor, maxDistance,

   -- ** Frequency Shift by Pitch
   pitch,

   -- ** Direction and Cone
   -- $DirectionAndCone
   direction, coneAngles, coneOuterGain,

   -- ** Offset
   secOffset, sampleOffset, byteOffset,

   -- * Queuing Buffers with a Source
   -- $QueuingBuffersWithASource
   queueBuffers, unqueueBuffers,

   -- * Managing Source Execution
   -- $ManagingSourceExecution
   SourceState(..), sourceState, play, pause, stop, rewind
) where

import Control.Monad
import Data.ObjectName
import Data.StateVar
import Data.Tensor
import Foreign.Marshal.Array
import Foreign.Marshal.Utils
import Foreign.Ptr
import Foreign.Storable
import Sound.OpenAL.AL.ALboolean
import Sound.OpenAL.AL.BasicTypes
import Sound.OpenAL.AL.BufferInternal
import Sound.OpenAL.AL.Listener
import Sound.OpenAL.AL.PeekPoke
import Sound.OpenAL.AL.QueryUtils
import Sound.OpenAL.AL.SourceState
import Sound.OpenAL.Constants

#ifdef __HADDOCK__
import Sound.OpenAL.AL.Listener ( listenerGain )
import Sound.OpenAL.AL.Errors ( ALErrorCategory(..) )
#endif

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

-- | The abstract buffer type.

newtype Source = Source ALuint
   deriving ( Eq, Ord, Show )

instance Storable Source where
   sizeOf    ~(Source b) = sizeOf b
   alignment ~(Source b) = alignment b
   peek                  = peek1 Source . castPtr
   poke ptr   (Source b) = poke1 (castPtr ptr) b

instance ObjectName Source where
   genObjectNames n =
      allocaArray n $ \buf -> do
         alGenSources (fromIntegral n) buf
         peekArray n buf

   deleteObjectNames = withArraySizei alDeleteSources

   isObjectName = fmap unmarshalALboolean . alIsSource

foreign import CALLCONV unsafe "alGenSources"
   alGenSources :: ALsizei -> Ptr Source -> IO ()

foreign import CALLCONV unsafe "alDeleteSources"
   alDeleteSources :: ALsizei -> Ptr Source -> IO ()

foreign import CALLCONV unsafe "alIsSource"
   alIsSource :: Source -> IO ALboolean

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

-- | 'sourcePosition' contains the current location of the source in the world
-- coordinate system. Any 3-tuple of valid float values is allowed.
-- Implementation behavior on encountering NaN and infinity is not defined. The
-- initial position is ('Vertex3' 0 0 0).

sourcePosition :: Source -> StateVar (Vertex3 ALfloat)
sourcePosition = makeSourceStateVar dictVertex3ALfloat GetPosition

-- | 'sourceVelocity' contains current velocity (speed and direction) of the
-- source in the world coordinate system. Any 3-tuple of valid float values is
-- allowed, and the initial velocity is ('Vector3' 0 0 0). 'sourceVelocity' does
-- not affect 'sourcePosition'. OpenAL does not calculate the velocity from
-- subsequent position updates, nor does it adjust the position over time based
-- on the specified velocity. Any such calculation is left to the application.
-- For the purposes of sound processing, position and velocity are independent
-- parameters affecting different aspects of the sounds.
--
-- 'sourceVelocity' is taken into account by the driver to synthesize the
-- Doppler effect perceived by the listener for each source, based on the
-- velocity of both source and listener, and the Doppler related parameters.

sourceVelocity :: Source -> StateVar (Vector3 ALfloat)
sourceVelocity = makeSourceStateVar dictVector3ALfloat GetVelocity

-- | 'sourceGain' contains a scalar amplitude multiplier for the given source.
-- The initial value 1 means that the sound is unattenuated. A 'sourceGain'
-- value of 0.5 is equivalent to an attenuation of 6dB. The value zero equals
-- silence (no output). Driver implementations are free to optimize this case
-- and skip mixing and processing stages where applicable. The implementation is
-- in charge of ensuring artifact-free (click-free) changes of gain values and
-- is free to defer actual modification of the sound samples, within the limits
-- of acceptable latencies.
--
-- A 'sourceGain' larger than 1 (amplification) is permitted. However, the
-- implementation is free to clamp the total gain (effective gain per source
-- times listener gain) to 1 to prevent overflow.

sourceGain :: Source -> StateVar Gain
sourceGain = makeSourceStateVar dictALfloat GetGain

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

-- | The entity to which the source attributes 'sourcePosition',
-- 'sourceVelocity' and 'direction' are to be interpreted.

data SourceRelative =
     World
   | Listener
   deriving ( Eq, Ord, Show )

marshalSourceRelative :: SourceRelative -> Bool
marshalSourceRelative = (== Listener)

unmarshalSourceRelative :: Bool -> SourceRelative
unmarshalSourceRelative x = if x then Listener else World

-- | If 'sourceRelative' contains 'Listener', it indicates indicates that the
-- values specified by 'sourcePosition', 'sourceVelocity' and 'direction' are to
-- be interpreted relative to the listener position. The initial value is
-- 'World', indicating that those source attributes are to be interpreted
-- relative to the world, i.e. they are considered absolute.

sourceRelative :: Source -> StateVar SourceRelative
sourceRelative = makeSourceStateVar dictSourceRelative GetSourceRelative

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

-- | When first created, a source will be in the 'Undetermined' state. If a
-- buffer is then attached using 'buffer', then the source will enter the
-- 'Static' state. If the first buffer attached to a source is attached using
-- 'queueBuffers', then the source will enter the 'Streaming' state. A source of
-- either state will be reset to state 'Undetermined' by setting its 'buffer' to
-- 'Nothing', and attaching any buffer to a streaming source will change the
-- state to 'Static'. Attempting to queue a buffer on a static source will
-- result in an 'ALInvalidOperation' error.

data SourceType =
     Undetermined
   | Static
   | Streaming
   deriving ( Eq, Ord, Show )

unmarshalSourceType :: ALint -> SourceType
unmarshalSourceType x
   | x == al_UNDETERMINED = Undetermined
   | x == al_STATIC = Static
   | x == al_STREAMING = Streaming
   | otherwise = error ("unmarshalSourceType: illegal value " ++ show x)

-- | 'sourceType' indicates whether a source is ready to queue buffers, ready to
-- use a static buffer, or is in an undetermined state where it can be used for
-- either streaming or static playback.

sourceType :: Source -> GettableStateVar SourceType
sourceType = makeSourceGettableStateVar dictSourceType GetSourceType

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

-- | Specifies what should happen when the end of a buffer queue is reached.

data LoopingMode =
     OneShot
   | Looping
   deriving ( Eq, Ord, Show )

marshalLoopingMode :: LoopingMode -> Bool
marshalLoopingMode = (== Looping)

unmarshalLoopingMode :: Bool -> LoopingMode
unmarshalLoopingMode x = if x then Looping else OneShot

-- | If 'loopingMode' contains 'Looping', it indicates that the source will not
-- be in the 'Stopped' state once it reaches the end of last buffer in the
-- buffer queue. Instead, the source will immediately promote to 'Initial' and
-- 'Playing'.  The initial value is 'OneShot'. 'loopingMode' can be changed on a
-- source in any execution state. In particular, it can be changed on a
-- 'Playing' source.

loopingMode :: Source -> StateVar LoopingMode
loopingMode = makeSourceStateVar dictLoopingMode GetLooping

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

-- | 'buffer' contains the current buffer object. Setting 'buffer' to 'Just' a
-- buffer object makes it the head entry in the source\'s queue. Setting
-- 'buffer'for a source in the 'Stopped' or 'Initial' state empties the entire
-- queue, then appends the one buffer specified (or none at all if 'Nothing'
-- was specified).
-- 
-- For a source in the 'Playing' or 'Paused' state, setting 'buffer' will result
-- in the 'ALInvalidOperation' error state being set. 'buffer' can be applied only
-- to sources in the 'Initial' and 'Stopped' states. Specifying an invalid
-- buffer name (either because the buffer name doesn\'t exist or because that
-- buffer can\'t be attached to the specified source) will result in an
-- 'ALInvalidValue' error while specifying an invalid source name results in an
-- 'ALInvalidName' error. Setting 'buffer' to 'Nothing' is a legal way to release
-- the current buffer queue on a source in the 'Initial' or 'Stopped' state,
-- whether the source has just one entry (current buffer) or more. Setting
-- 'buffer' to 'Nothing' still causes an 'ALInvalidOperation' for any source in
-- the 'Playing' or 'Paused' state, consequently it cannot be used to mute or
-- stop a source. The initial value is 'Nothing'.

buffer :: Source -> StateVar (Maybe Buffer)
buffer = makeSourceStateVar dictMaybeBuffer GetBuffer

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

-- | 'buffersQueued' contains the number of buffers in the queue of a given
-- source. This includes those not yet played, the one currently playing, and
-- the ones that have been played already. It will contain 0 if 'buffer' has
-- been set to 'Nothing'.

buffersQueued :: Source -> GettableStateVar ALint
buffersQueued = makeSourceGettableStateVar dictALint GetBuffersQueued

-- | 'buffersProcessed' contains the number of buffers that have been played
-- by a given source.  Indirectly, this gives the index of the buffer currently
-- playing. It can be used to determine how much slots are needed for unqueuing
-- them. On a source in the 'Stopped' state, all buffers are processed. On a
-- source in the 'Initial' state, no buffers are processed, all buffers are
-- pending. It will contain 0 if 'buffer' has been set to 'Nothing'.

buffersProcessed :: Source -> GettableStateVar ALint
buffersProcessed = makeSourceGettableStateVar dictALint GetBuffersProcessed

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

-- | 'gainBounds' contains two scalar amplitude thresholds between 0 and 1
-- (included): The minimum guaranteed gain for this source and the maximum gain
-- permitted, with initial values 0 and 1, respectively At the end of the
-- processing of various attenuation factors such as distance based attenuation
-- and 'sourceGain', the effective gain calculated is compared to these values:
--
-- If the effective gain is lower than the minimum gain, the minimum gain is
-- applied. This happens before the 'listenerGain' is applied. If a zero minimum
-- gain is set, then the effective gain will not be corrected.
--
-- If the effective gain is higher than the maximum gain, the maximum gain is
-- applied. This happens before the 'listenerGain' is applied. If the
-- 'listenerGain' times the maximum gain still exceeds the maximum gain the
-- implementation can handle, the implementation is free to clamp. If a zero
-- maximum gain is set, then the source is effectively muted. The implementation
-- is free to optimize for this situation, but no optimization is required or
-- recommended as setting 'sourceGain' to zero is the proper way to mute a
-- source.

gainBounds :: Source -> StateVar (Gain, Gain)
gainBounds source =
   pairStateVars
      (makeSourceStateVar dictALfloat GetMinGain source)
      (makeSourceStateVar dictALfloat GetMaxGain source)

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

-- | 'referenceDistance' is used for distance attenuation calculations based on
-- inverse distance with rolloff. Depending on the distance model it will also
-- act as a distance threshold below which gain is clamped. See
-- "Sound.OpenAL.AL.Attenuation" for details. The initial value is 1.

referenceDistance :: Source -> StateVar ALfloat
referenceDistance = makeSourceStateVar dictALfloat GetReferenceDistance

-- | 'rolloffFactor' is used for distance attenuation calculations based on
-- inverse distance with rolloff. For distances smaller than 'maxDistance' (and,
-- depending on the distance model, larger than 'referenceDistance'), this will
-- scale the distance attenuation over the applicable range. See
-- "Sound.OpenAL.AL.Attenuation" for details how the attenuation is computed as
-- a function of the distance. The initial value is 1.
-- 
-- In particular, 'rolloffFactor' can be set to zero for those sources that are
-- supposed to be exempt from distance attenuation. The implementation is
-- encouraged to optimize this case, bypassing distance attenuation calculation
-- entirely on a persource basis.

rolloffFactor :: Source -> StateVar ALfloat
rolloffFactor = makeSourceStateVar dictALfloat GetRolloffFactor

-- | 'maxDistance' is used for distance attenuation calculations based on
-- inverse distance with rolloff, if the inverse clamped distance model is
-- used. In this case, distances greater than 'maxDistance' will be clamped to
-- 'maxDistance'.  'maxDistance' based clamping is applied before minimum gain
-- clamping (see 'gainBounds'), so if the effective gain at 'maxDistance' is
-- larger than the minimum gain, the minimum gain will have no effect. No
-- culling is supported. The initial value is the largest representable
-- 'ALfloat'.

maxDistance :: Source -> StateVar ALfloat
maxDistance = makeSourceStateVar dictALfloat GetMaxDistance

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

-- | 'pitch' contains the desired pitch shift, where 1 (the initial value)
-- equals identity. Each reduction by 50 percent equals a pitch shift of -12
-- semitones (one octave reduction). Each doubling equals a pitch shift of 12
-- semitones (one octave increase). Zero is not a legal value. Implementations
-- may clamp the actual output pitch range to any values subject to the
-- implementation's own limits.

pitch :: Source -> StateVar ALfloat
pitch = makeSourceStateVar dictALfloat GetPitch

--------------------------------------------------------------------------------
-- $DirectionAndCone
-- Each source can be directional, depending on the settings for 'coneAngles'.
-- There are three zones defined: the inner cone, the outside zone, and the
-- transitional zone in between. The angle-dependent gain for a directional
-- source is constant inside the inner cone, and changes over the transitional
-- zone to the value specified outside the outer cone. 'sourceGain' is applied
-- for the inner cone, with an application selectable 'coneOuterGain' factor to
-- define the gain in the outer zone. In the transitional zone
-- implementation-dependent interpolation between 'sourceGain' and 'sourceGain'
-- times 'coneOuterGain' is applied.

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

-- | If 'direction' does not contain the zero vector ('Vector3' 0 0 0), the
-- source is directional. The sound emission is presumed to be symmetric around
-- the direction vector (cylinder symmetry). Sources are not oriented in full 3
-- degrees of freedom, only two angles are effectively needed.
--
-- The zero vector is the initial value, indicating that a source is not
-- directional. Specifying a non-zero vector will make the source directional.
-- Specifying a zero vector for a directional source will effectively mark it as
-- nondirectional.

direction :: Source -> StateVar (Vector3 ALfloat)
direction = makeSourceStateVar dictVector3ALfloat GetDirection

-- | 'coneAngles' contains the inner and outer angles of the sound cone, in
-- degrees. The default of 360 for the inner cone angle means that it covers the
-- entire world, which is equivalent to an omni-directional source. The default
-- of 360 for the outer cone angle means that it covers the entire world. If the
-- inner angle is also 360, then the zone for angle-dependent attenuation is
-- zero.

coneAngles :: Source -> StateVar (ALfloat, ALfloat)
coneAngles source =
   pairStateVars
      (makeSourceStateVar dictALfloat GetConeInnerAngle source)
      (makeSourceStateVar dictALfloat GetConeOuterAngle source)

-- | 'coneOuterGain' contains the factor with which 'sourceGain' is multiplied
-- to determine the effective gain outside the cone defined by the outer angle.
-- The effective gain applied outside the outer cone is 'sourceGain' times
-- 'coneOuterGain'. Changing 'sourceGain' affects all directions, i.e. the
-- source is attenuated in all directions, for any position of the listener. The
-- application has to change 'coneOuterGain' as well if a different behavior is
-- desired.

coneOuterGain :: Source -> StateVar Gain
coneOuterGain = makeSourceStateVar dictALfloat GetConeOuterGain

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

-- | 'secOffset' contains the playback position, expressed in seconds (the value
-- will loop back to zero for looping sources).
--
-- When setting 'secOffset' on a source which is already playing, the playback
-- will jump to the new offset unless the new offset is out of range, in which
-- case an 'ALInvalidValue' error is set. If the source is not playing, then the
-- offset will be applied on the next 'play' call.
--
-- The position is relative to the beginning of all the queued buffers for the
-- source, and any queued buffers traversed by a set call will be marked as
-- processed.
--
-- This value is based on byte position, so a pitch-shifted source will have an
-- exaggerated playback speed. For example, you can be 0.5 seconds into a buffer
-- having taken only 0.25 seconds to get there if the pitch is set to 2.

secOffset :: Source -> StateVar ALfloat
secOffset = makeSourceStateVar dictALfloat GetSecOffset

-- | 'sampleOffset' contains the playback position, expressed in samples (the
-- value will loop back to zero for looping sources). For a compressed format,
-- this value will represent an exact offset within the uncompressed data.
-- 
-- When setting 'sampleOffset' on a source which is already playing, the
-- playback will jump to the new offset unless the new offset is out of range,
-- in which case an 'ALInvalidValue' error is set. If the source is not playing,
-- then the offset will be applied on the next 'play' call. A 'stop', 'rewind',
-- or a second 'play' call will reset the offset to the beginning of the buffer.
-- 
-- The position is relative to the beginning of all the queued buffers for the
-- source, and any queued buffers traversed by a set call will be marked as
-- processed.

sampleOffset :: Source -> StateVar ALint
sampleOffset = makeSourceStateVar dictALint GetSampleOffset

-- | 'byteOffset' contains the playback position, expressed in bytes (the value
-- will loop back to zero for looping sources). For a compressed format, this
-- value may represent an approximate offset within the compressed data buffer.
-- 
-- When setting 'byteOffset' on a source which is already playing, the playback
-- will jump to the new offset unless the new offset is out of range, in which
-- case an 'ALInvalidValue' error is set. If the source is not playing, then the
-- offset will be applied on the next 'play' call. A 'stop', 'rewind', or a
-- second 'play' call will reset the offset to the beginning of the buffer.
-- 
-- The position is relative to the beginning of all the queued buffers for the
-- source, and any queued buffers traversed by a set call will be marked as
-- processed.

byteOffset :: Source -> StateVar ALint
byteOffset = makeSourceStateVar dictALint GetByteOffset

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

pairStateVars :: StateVar a -> StateVar b -> StateVar (a,b)
pairStateVars var1 var2 =
   makeStateVar
      (liftM2 (,) (get var1) (get var2))
      (\(val1,val2) -> do var1 $= val1; var2 $= val2)

data Dictionary a b c = Dictionary {
   alGetter  :: Source -> ALenum -> Ptr b -> IO (),
   alSetter  :: Source -> ALenum -> Ptr b -> IO (),
   size      :: Int,
   peekSize  :: Ptr b -> IO a,
   marshal   :: a -> c }

dictALint :: Dictionary ALint ALint ALint
dictALint = Dictionary {
   alGetter  = alGetSourceiv,
   alSetter  = alSourceiv,
   size      = 1,
   peekSize  = peek1 id,
   marshal   = id }

dictALfloat :: Dictionary ALfloat ALfloat ALfloat
dictALfloat = Dictionary {
   alGetter  = alGetSourcefv,
   alSetter  = alSourcefv,
   size      = 1,
   peekSize  = peek1 id,
   marshal   = id }

dictSourceRelative :: Dictionary SourceRelative ALint ALint
dictSourceRelative = Dictionary {
   alGetter  = alGetSourceiv,
   alSetter  = alSourceiv,
   size      = 1,
   peekSize  = peek1 (unmarshalSourceRelative . unmarshalALboolean . fromIntegral),
   marshal   = fromIntegral . marshalALboolean . marshalSourceRelative }

dictSourceType :: Dictionary SourceType ALint ALint
dictSourceType = Dictionary {
   alGetter  = alGetSourceiv,
   alSetter  = undefined,
   size      = 1,
   peekSize  = peek1 unmarshalSourceType,
   marshal   = undefined }

dictLoopingMode :: Dictionary LoopingMode ALint ALint
dictLoopingMode = Dictionary {
   alGetter  = alGetSourceiv,
   alSetter  = alSourceiv,
   size      = 1,
   peekSize  = peek1 (unmarshalLoopingMode . unmarshalALboolean . fromIntegral),
   marshal   = fromIntegral . marshalALboolean . marshalLoopingMode }

dictSourceState :: Dictionary SourceState ALint ALint
dictSourceState = Dictionary {
   alGetter  = alGetSourceiv,
   alSetter  = undefined,
   size      = 1,
   peekSize  = peek1 unmarshalSourceState,
   marshal   = undefined }

dictVertex3ALfloat :: Dictionary (Vertex3 ALfloat) ALfloat (Vertex3 ALfloat)
dictVertex3ALfloat = Dictionary {
   alGetter  = alGetSourcefv,
   alSetter  = alSourcefv,
   size      = 3,
   peekSize  = peek3 Vertex3,
   marshal   = id }

dictVector3ALfloat :: Dictionary (Vector3 ALfloat) ALfloat (Vector3 ALfloat)
dictVector3ALfloat = Dictionary {
   alGetter  = alGetSourcefv,
   alSetter  = alSourcefv,
   size      = 3,
   peekSize  = peek3 Vector3,
   marshal   = id }

dictMaybeBuffer :: Dictionary (Maybe Buffer) ALint ALint
dictMaybeBuffer = Dictionary {
   alGetter  = alGetSourceiv,
   alSetter  = alSourceiv,
   size      = 1,
   peekSize  = peek1 (unmarshalBuffer . fromIntegral),
   marshal   = fromIntegral . marshalBuffer }

makeGetter :: Storable b => Dictionary a b c -> GetPName -> Source -> IO a
makeGetter dict name source =
   allocaArray (size dict) $ \buf -> do
      alGetter dict source (marshalGetPName name) buf
      peekSize dict buf

makeSetter :: Storable c => Dictionary a b c -> GetPName -> Source -> a -> IO ()
makeSetter dict name source value =
   with (marshal dict value) $
      alSetter dict source (marshalGetPName name) . castPtr

makeSourceStateVar ::
   (Storable b, Storable c) =>
   Dictionary a b c -> GetPName -> Source -> StateVar a
makeSourceStateVar dict name source =
   makeStateVar
      (makeGetter dict name source)
      (makeSetter dict name source)

makeSourceGettableStateVar ::
   (Storable b, Storable c) =>
   Dictionary a b c -> GetPName -> Source -> GettableStateVar a
makeSourceGettableStateVar dict name source =
   makeGettableStateVar
      (makeGetter dict name source)

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

foreign import CALLCONV unsafe "alGetSourcefv"
   alGetSourcefv :: Source -> ALenum -> Ptr ALfloat -> IO ()

foreign import CALLCONV unsafe "alSourcefv"
   alSourcefv :: Source -> ALenum -> Ptr ALfloat -> IO ()

foreign import CALLCONV unsafe "alGetSourceiv"
   alGetSourceiv :: Source ->  ALenum -> Ptr ALint -> IO ()

-- Note: Older OpenAL implementations have no alSourceiv, so we emulate it here.
alSourceiv :: Source -> ALenum -> Ptr ALint -> IO ()
alSourceiv source n buf =  peek buf >>= alSourcei source n

foreign import CALLCONV unsafe "alSourcei"
   alSourcei :: Source -> ALenum -> ALint -> IO ()

--------------------------------------------------------------------------------
-- $QueuingBuffersWithASource
-- OpenAL does not specify a built-in streaming mechanism. There is no mechanism
-- to stream data into a buffer object. Instead, the API has a more flexible and
-- versatile mechanism to queue buffers for sources. There are many ways to use
-- this feature, with streaming being only one of them.
--
-- Streaming is replaced by queuing static buffers. This effectively moves any
-- multi-buffer caching into the application and allows the application to
-- select how many buffers it wants to use, the size of the buffers, and whether
-- these are re-used in cycle, pooled, or thrown away.
--
-- Looping (over a finite number of repetitions) can be implemented by
-- explicitly repeating buffers in the queue. Infinite loops can (theoretically)
-- be accomplished by sufficiently large repetition counters. If only a single
-- buffer is supposed to be repeated infinitely, using the respective source
-- attribute 'loopingMode' is recommended.
--
-- Loop Points for restricted looping inside a buffer can in many cases be
-- replaced by splitting the sample into several buffers and queuing the sample
-- fragments (including repetitions) accordingly.
--
-- Buffers can be queued, unqueued after they have been used, and either be
-- deleted, or refilled and queued again. Splitting large samples over several
-- buffers maintained in a queue has distinct advantages over approaches that
-- require explicit management of samples and sample indices.

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

-- | The application can queue up one or multiple buffer names using
-- 'queueBuffers'. The buffers will be queued in the sequence in which they
-- appear in the list.
--
-- This command is legal on a source in any playback state (to allow for
-- streaming, queuing has to be possible on a 'Playing' source).
--
-- All buffers in a queue must have the same format and attributes. An attempt
-- to mix formats or other buffer attributes will result in a failure and an
-- 'ALInvalidValue' error will be thrown. If the queue operation fails, the source
-- queue will remain unchanged (even if some of the buffers could have been
-- queued).

queueBuffers :: Source -> [Buffer] -> IO ()
queueBuffers = withArraySizei . alSourceQueueBuffers

withArraySizei :: Storable a => (ALsizei -> Ptr a -> IO ()) -> [a] -> IO ()
withArraySizei f xs = withArrayLen xs $ f . fromIntegral

foreign import CALLCONV unsafe "alSourceQueueBuffers"
   alSourceQueueBuffers :: Source -> ALsizei -> Ptr Buffer -> IO ()

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

-- | Once a queue entry for a buffer has been appended to a queue and is pending
-- processing, it should not be changed. Removal of a given queue entry is not
-- possible unless either the source is stopped (in which case then entire queue
-- is considered processed), or if the queue entry has already been processed
-- ('Playing' or 'Paused' source). A playing source will enter the 'Stopped'
-- state if it completes playback of the last buffer in its queue (the same
-- behavior as when a single buffer has been attached to a source and has
-- finished playback).
--
-- The 'unqueueBuffers' command removes a number of buffers entries that have
-- finished processing, in the order of appearance, from the queue. The
-- operation will fail with an 'ALInvalidValue' error if more buffers are
-- requested than available, leaving the destination arguments unchanged.

unqueueBuffers :: Source -> [Buffer] -> IO ()
unqueueBuffers = withArraySizei . alSourceUnqueueBuffers

foreign import CALLCONV unsafe "alSourceUnqueueBuffers"
   alSourceUnqueueBuffers :: Source -> ALsizei -> Ptr Buffer -> IO ()

--------------------------------------------------------------------------------
-- $ManagingSourceExecution
-- The execution state of a source can be queried. OpenAL provides a set of
-- functions that initiate state transitions causing sources to start and stop
-- execution.

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

-- | Contains the current execution state of the given source. The initial state
-- of any source is 'Initial'.
--
-- Note that the 'Initial' state is not necessarily identical to the initial
-- state in which a source is created, because the other source attributes are
-- not automatically reset to their initial values. 'Initial' merely indicates
-- that the source can be executed using the 'play' command. A 'Stopped' or
-- 'Initial' source can be reset into the default configuration by using a
-- sequence of source commands as necessary. As the application has to specify
-- all relevant state anyway to create a useful source configuration, no reset
-- command is provided.

sourceState :: Source -> GettableStateVar SourceState
sourceState source =
   makeGettableStateVar
      (makeGetter dictSourceState GetSourceState source)

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

-- | 'play' applied to an 'Initial' source will promote the source to 'Playing',
-- thus the data found in the buffer will be fed into the processing, starting
-- at the beginning. 'play' applied to a 'Playing' source will restart the
-- source from the beginning. It will not affect the configuration, and will
-- leave the source in 'Playing' state, but reset the sampling offset to the
-- beginning. 'play' applied to a 'Paused' source will resume processing using
-- the source state as preserved at the 'pause' operation. 'play' applied to a
-- 'Stopped' source will propagate it to 'Initial' then to 'Playing'
-- immediately.

play :: [Source] -> IO ()
play = withArraySizei alSourcePlayv

foreign import CALLCONV unsafe "alSourcePlayv"
   alSourcePlayv :: ALsizei -> Ptr Source -> IO ()

-- | 'pause' applied to an 'Initial' source is a legal NOP. 'pause' applied to a
-- 'Playing' source will change its state to 'Paused'. The source is exempt from
-- processing, its current state is preserved. 'pause' applied to a 'Paused'
-- source is a legal NOP. 'pause' applied to a 'Stopped' source is a legal NOP.

pause :: [Source] -> IO ()
pause = withArraySizei alSourcePausev

foreign import CALLCONV unsafe "alSourcePausev"
   alSourcePausev :: ALsizei -> Ptr Source -> IO ()

-- | 'stop' applied to an 'Initial' source is a legal NOP. 'stop' applied to a
-- 'Playing' source will change its state to 'Stopped'. The source is exempt
-- from processing, its current state is preserved. 'stop' applied to a 'Paused'
-- source will change its state to 'Stopped', with the same consequences as on a
-- 'Playing' source. 'stop' applied to a 'Stopped' source is a legal NOP.

stop :: [Source] -> IO ()
stop = withArraySizei alSourceStopv

foreign import CALLCONV unsafe "alSourceStopv"
   alSourceStopv :: ALsizei -> Ptr Source -> IO ()

-- | 'rewind' applied to an 'Initial' source is a legal NOP. 'rewind' applied to
-- a 'Playing' source will change its state to 'Stopped' then 'Initial'. The
-- source is exempt from processing: its current state is preserved, with the
-- exception of the sampling offset, which is reset to the beginning.  'rewind'
-- applied to a 'Paused' source will change its state to 'Initial', with the
-- same consequences as on a 'Playing' source.  'rewind' applied to an 'Stopped'
-- source promotes the source to 'Initial', resetting the sampling offset to the
-- beginning.

rewind :: [Source] -> IO ()
rewind = withArraySizei alSourceRewindv

foreign import CALLCONV unsafe "alSourceRewindv"
   alSourceRewindv :: ALsizei -> Ptr Source -> IO ()