--------------------------------------------------------------------------------
-- |
-- Module      :  Sound.OpenAL.AL.Attenuation
-- Copyright   :  (c) Sven Panne 2005
-- License     :  BSD-style (see the file libraries/OpenAL/LICENSE)
-- 
-- Maintainer  :  sven.panne@aedion.de
-- Stability   :  provisional
-- Portability :  portable
--
-- This module corresponds to section 3.4 (Attenuation By Distance) of the
-- OpenAL Specification and Reference (version 1.1).
-- 
--------------------------------------------------------------------------------

module Sound.OpenAL.AL.Attenuation (
   -- * Introduction
   -- $Introduction

   -- * Handling the Distance Model
   DistanceModel(..), distanceModel

   -- * Evaluation of Gain\/Attenuation Related State
   -- $EvaluationOfGainAttenuationRelatedState

   -- * No Culling By Distance
   -- $NoCullingByDistance
) where

import Foreign.Marshal.Alloc ( alloca )
import Foreign.Ptr ( Ptr )
import Graphics.Rendering.OpenGL.GL.StateVar ( StateVar, makeStateVar )
import Sound.OpenAL.AL.BasicTypes ( ALint, ALenum )
import Sound.OpenAL.AL.PeekPoke ( peek1 )
import Sound.OpenAL.AL.QueryUtils (
   GetPName(GetDistanceModel), marshalGetPName )
import Sound.OpenAL.Constants (
   al_NONE, al_INVERSE_DISTANCE, al_INVERSE_DISTANCE_CLAMPED,
   al_LINEAR_DISTANCE, al_LINEAR_DISTANCE_CLAMPED, al_EXPONENT_DISTANCE,
   al_EXPONENT_DISTANCE_CLAMPED )

#ifdef __HADDOCK__
import Sound.OpenAL.AL.Source (
   sourceGain, gainBounds, coneAngles, coneOuterGain, referenceDistance,
   rolloffFactor, maxDistance )
#endif

--------------------------------------------------------------------------------
-- $Introduction
-- Samples usually use the entire dynamic range of the chosen format\/encoding,
-- independent of their real world intensity. In other words, a jet engine and a
-- clockwork both will have samples with full amplitude. The application will
-- then have to adjust source gain accordingly to account for relative
-- differences.
-- 
-- Source gain is then attenuated by distance. The effective attenuation of a
-- source depends on many factors, among which distance attenuation and source
-- and listener gain are only some of the contributing factors. Even if the
-- source and listener gain exceed 1 (amplification beyond the guaranteed
-- dynamic range), distance and other attenuation might ultimately limit the
-- overall gain to a value below 1.

--------------------------------------------------------------------------------
-- $EvaluationOfGainAttenuationRelatedState
-- While amplification\/attenuation commute (multiplication of scaling factors),
-- clamping operations do not. The order in which various gain related
-- operations are applied is:
-- 
-- 1. Distance attenuation is calculated first, including minimum
-- ('referenceDistance') and maximum ('maxDistance') thresholds.
-- 
-- 2. The result is then multiplied by source gain.
-- 
-- 3. If the source is directional (the inner cone angle is less than the outer
-- cone angle, see 'coneAngles'), an angle-dependent attenuation is calculated
-- depending on 'coneOuterGain', and multiplied with the distance-dependent
-- attenuation. The resulting attenuation factor for the given angle and
-- distance between listener and source is multiplied with 'sourceGain'.
-- 
-- 4. The effective gain computed this way is compared against 'gainBounds'.
-- 
-- 5. The result is guaranteed to be clamped to 'gainBounds', and subsequently
-- multiplied by listener gain which serves as an overall volume control.
-- 
-- The implementation is free to clamp listener gain if necessary due to
-- hardware or implementation constraints.

--------------------------------------------------------------------------------
-- $NoCullingByDistance
-- With the DS3D compatible inverse clamped distance model, OpenAL provides a
-- per-source 'maxDistance' attribute that can be used to define a distance
-- beyond which the source will not be further attenuated by distance. The DS3D
-- distance attenuation model and its clamping of volume is also extended by a
-- mechanism to cull (mute) sources from processing, based on distance. However,
-- the OpenAL does not support culling a source from processing based on a
-- distance threshold.
-- 
-- At this time OpenAL is not meant to support culling at all. Culling based on
-- distance, or bounding volumes, or other criteria, is best left to the
-- application. For example, the application might employ sophisticated
-- techniques to determine whether sources are audible that are beyond the scope
-- of OpenAL. In particular, rule based culling inevitably introduces acoustic
-- artifacts. E.g. if the listener-source distance is nearly equal to the culling
-- threshold distance, but varies above and below, there will be popping
-- artifacts in the absence of hysteresis.

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

-- | OpenAL currently supports six modes of operation with respect to distance
-- attenuation, including one that is similar to the IASIG I3DL2 model. The
-- application chooses one of these models (or chooses to disable
-- distance-dependent attenuation) on a per-context basis.
--
-- The distance used in the formulas for the \"clamped\" modes below is clamped
-- to be in the range between 'referenceDistance' and 'maxDistance':
--
-- /clamped distance/ =
--    max('referenceDistance', min(/distance/, 'maxDistance'))
--
-- The linear models are not physically realistic, but do allow full attenuation
-- of a source beyond a specified distance. The OpenAL implementation is still
-- free to apply any range clamping as necessary.
-- 
-- With all the distance models, if the formula can not be evaluated then the
-- source will not be attenuated. For example, if a linear model is being used
-- with 'referenceDistance' equal to 'maxDistance', then the gain equation will
-- have a divide-by-zero error in it. In this case, there is no attenuation for
-- that source.

data DistanceModel =
     NoAttenuation
   -- ^ Bypass all distance attenuation calculation for all sources. The
   --   implementation is expected to optimize this situation.
   | InverseDistance
   -- ^ Inverse distance rolloff model, which is equivalent to the IASIG I3DL2
   --   model with the exception that 'referenceDistance' does not imply any
   --   clamping.
   --
   --   /gain/ = 'referenceDistance' \/ ('referenceDistance' +
   --             'rolloffFactor' \* (/distance/ - 'referenceDistance'))
   --
   --   The 'referenceDistance' parameter used here is a per-source attribute
   --   which is the distance at which the listener will experience gain
   --   (unless the implementation had to clamp effective gain to the available
   --   dynamic range). 'rolloffFactor' is per-source parameter the application
   --   can use to increase or decrease the range of a source by decreasing or
   --   increasing the attenuation, respectively. The default value is 1. The
   --   implementation is free to optimize for a 'rolloffFactor' value of 0,
   --   which indicates that the application does not wish any distance
   --   attenuation on the respective source.
   | InverseDistanceClamped
   -- ^ Inverse Distance clamped model, which is essentially the inverse
   --   distance rolloff model, extended to guarantee that for distances below
   --   'referenceDistance', gain is clamped. This mode is equivalent to the
   --   IASIG I3DL2 distance model.
   | LinearDistance
   -- ^ Linear distance rolloff model, modeling a linear dropoff in gain as
   -- distance increases between the source and listener.
   --
   --   /gain/ = (1 - 'rolloffFactor' \* (/distance/ - 'referenceDistance') \/
   --            ('maxDistance' - 'referenceDistance'))
   | LinearDistanceClamped
   -- ^ Linear Distance clamped model, which is the linear model, extended to
   --   guarantee that for distances below 'referenceDistance', gain is clamped.
   | ExponentDistance
   -- ^ Exponential distance rolloff model, modeling an exponential dropoff in
   --   gain as distance increases between the source and listener.
   --
   --   /gain/ = (/distance/ \/ 'referenceDistance') \*\* (- 'rolloffFactor')
   | ExponentDistanceClamped
   -- ^ Exponential Distance clamped model, which is the exponential model,
   --   extended to guarantee that for distances below 'referenceDistance',
   --   gain is clamped.
   deriving ( Eq, Ord, Show )

marshalDistanceModel :: DistanceModel -> ALenum
marshalDistanceModel x = case x of
   NoAttenuation -> al_NONE
   InverseDistance -> al_INVERSE_DISTANCE
   InverseDistanceClamped -> al_INVERSE_DISTANCE_CLAMPED
   LinearDistance -> al_LINEAR_DISTANCE
   LinearDistanceClamped -> al_LINEAR_DISTANCE_CLAMPED
   ExponentDistance -> al_EXPONENT_DISTANCE
   ExponentDistanceClamped -> al_EXPONENT_DISTANCE_CLAMPED

unmarshalDistanceModel :: ALenum -> DistanceModel
unmarshalDistanceModel x
   | x == al_NONE = NoAttenuation
   | x == al_INVERSE_DISTANCE = InverseDistance
   | x == al_INVERSE_DISTANCE_CLAMPED = InverseDistanceClamped
   | x == al_LINEAR_DISTANCE = LinearDistance
   | x == al_LINEAR_DISTANCE_CLAMPED = LinearDistanceClamped
   | x == al_EXPONENT_DISTANCE = ExponentDistance
   | x == al_EXPONENT_DISTANCE_CLAMPED = ExponentDistanceClamped
   | otherwise = error ("unmarshalDistanceModel: illegal value " ++ show x)

-- | Contains the current per-context distance model.

distanceModel :: StateVar DistanceModel
distanceModel =
   makeStateVar
      (alloca $ \buf -> do
          alGetIntegerv (marshalGetPName GetDistanceModel) buf
          peek1 (unmarshalDistanceModel . fromIntegral) buf)
      (alDistanceModel . marshalDistanceModel)

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

foreign import CALLCONV unsafe "alDistanceModel"
   alDistanceModel :: ALenum -> IO ()