--------------------------------------------------------------------------------
-- |
-- Module      :  Sound.OpenAL.AL.Source
-- Copyright   :  (c) Sven Panne 2003-2005
-- License     :  BSD-style (see the file libraries/OpenAL/LICENSE)
-- 
-- Maintainer  :  sven.panne@aedion.de
-- Stability   :  provisional
-- 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 ( liftM2 )
import Foreign.Marshal.Array ( withArrayLen, peekArray, allocaArray )
import Foreign.Marshal.Utils ( with )
import Foreign.Ptr ( Ptr, castPtr )
import Foreign.Storable ( Storable(..) )
import Graphics.Rendering.OpenGL.GL.BufferObjects ( ObjectName(..) )
import Graphics.Rendering.OpenGL.GL.CoordTrans ( Vector3(..) )
import Graphics.Rendering.OpenGL.GL.VertexSpec ( Vertex3(..) )
import Graphics.Rendering.OpenGL.GL.StateVar (
   HasGetter(get), HasSetter(($=)), makeStateVar, StateVar, GettableStateVar,
   makeGettableStateVar )
import Sound.OpenAL.AL.ALboolean ( marshalALboolean, unmarshalALboolean )
import Sound.OpenAL.AL.BasicTypes
import Sound.OpenAL.AL.Buffer ( Buffer )
import Sound.OpenAL.AL.BufferInternal ( marshalBuffer, unmarshalBuffer )
import Sound.OpenAL.AL.Listener ( Gain )
import Sound.OpenAL.AL.PeekPoke ( peek1, poke1, peek3 )
import Sound.OpenAL.AL.SourceState ( SourceState(..), unmarshalSourceState )
import Sound.OpenAL.AL.QueryUtils (
   GetPName(GetPosition,GetVelocity,GetGain,GetSourceRelative,GetSourceType,
            GetLooping,GetBuffer,GetBuffersQueued,GetBuffersProcessed,
            GetMinGain,GetMaxGain,GetReferenceDistance,GetRolloffFactor,
            GetMaxDistance,GetPitch,GetDirection,GetConeInnerAngle,
            GetConeOuterAngle,GetConeOuterGain,GetSecOffset,GetSampleOffset,
            GetByteOffset,GetSourceState),
   marshalGetPName )
import Sound.OpenAL.Constants ( al_UNDETERMINED, al_STATIC, al_STREAMING )

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