{-# OPTIONS_GHC -fno-warn-unused-imports #-} -------------------------------------------------------------------------------- -- | -- Module : Sound.OpenAL.AL.Source -- Copyright : (c) Sven Panne 2003-2016 -- License : BSD3 -- -- Maintainer : Sven Panne -- 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 -- Make the foreign imports happy. import Foreign.C.Types import Control.Monad ( liftM2 ) import Control.Monad.IO.Class ( MonadIO(..) ) import Data.ObjectName ( ObjectName(..), GeneratableObjectName(..) ) import Data.StateVar ( get, ($=) , GettableStateVar, makeGettableStateVar , StateVar, makeStateVar ) import Foreign.Marshal.Array ( allocaArray, peekArray, withArrayLen ) import Foreign.Marshal.Utils ( with ) import Foreign.Ptr ( Ptr, castPtr ) import Foreign.Storable ( Storable(..) ) import Graphics.Rendering.OpenGL.GL.Tensor ( Vector3(..), Vertex3(..) ) 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 -- For Haddock only. import Sound.OpenAL.AL.Errors -------------------------------------------------------------------------------- -- | 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 deleteObjectNames = liftIO . withArraySizei alDeleteSources isObjectName = liftIO . fmap unmarshalALboolean . alIsSource instance GeneratableObjectName Source where genObjectNames n = liftIO $ allocaArray n $ \buf -> do alGenSources (fromIntegral n) buf peekArray n buf foreign import ccall unsafe "alGenSources" alGenSources :: ALsizei -> Ptr Source -> IO () foreign import ccall unsafe "alDeleteSources" alDeleteSources :: ALsizei -> Ptr Source -> IO () foreign import ccall 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 ccall unsafe "alGetSourcefv" alGetSourcefv :: Source -> ALenum -> Ptr ALfloat -> IO () foreign import ccall unsafe "alSourcefv" alSourcefv :: Source -> ALenum -> Ptr ALfloat -> IO () foreign import ccall 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 ccall 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 :: MonadIO m => Source -> [Buffer] -> m () queueBuffers = withArraySizei . alSourceQueueBuffers withArraySizei :: (MonadIO m, Storable a) => (ALsizei -> Ptr a -> IO ()) -> [a] -> m () withArraySizei f xs = liftIO $ withArrayLen xs $ f . fromIntegral foreign import ccall 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 from the queue, returning the buffers that were unqueued. -- The operation will fail with an 'ALInvalidValue' error if more buffers are -- requested than available, leaving the destination arguments unchanged. unqueueBuffers :: MonadIO m => Source -> ALsizei -> m [Buffer] unqueueBuffers src n = liftIO $ allocaArray (fromIntegral n) $ \p -> do alSourceUnqueueBuffers src n p peekArray (fromIntegral n) p foreign import ccall 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 :: MonadIO m => [Source] -> m () play = withArraySizei alSourcePlayv foreign import ccall 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 :: MonadIO m => [Source] -> m () pause = withArraySizei alSourcePausev foreign import ccall 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 :: MonadIO m => [Source] -> m () stop = withArraySizei alSourceStopv foreign import ccall 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 :: MonadIO m => [Source] -> m () rewind = withArraySizei alSourceRewindv foreign import ccall unsafe "alSourceRewindv" alSourceRewindv :: ALsizei -> Ptr Source -> IO ()