module Sound.RubberBand.Option
( Options(..)

, Process(..)
, Stretch(..)
, Transients(..)
, Detector(..)
, Phase(..)
, Threading(..)
, Window(..)
, Smoothing(..)
, Formant(..)
, Pitch(..)
, Channels(..)

, Option(..)

, setOption
, getOption

, toOptions
, fromOptions

, defaultOptions
, percussiveOptions
) where

import Data.Bits ((.|.), (.&.), complement)
import Data.Maybe (fromMaybe)

{- |
Processing options for the timestretcher. The preferred
options should normally be set when calling 'Sound.RubberBand.Nice.new'.
-}
data Options = Options
  { oProcess    :: Process
  , oStretch    :: Stretch
  , oTransients :: Transients
  , oDetector   :: Detector
  , oPhase      :: Phase
  , oThreading  :: Threading
  , oWindow     :: Window
  , oSmoothing  :: Smoothing
  , oFormant    :: Formant
  , oPitch      :: Pitch
  , oChannels   :: Channels
  } deriving (Eq, Ord, Show, Read)

{- |
'Process' flags determine how the timestretcher
will be invoked. These options may not be changed after
construction.

The 'Process' setting is likely to depend on your architecture:
non-real-time operation on seekable files: 'Offline'; real-time
or streaming operation: 'RealTime'.
-}
data Process
  = Offline
  {- ^
  Run the stretcher in offline mode. In this mode the input data needs to
  be provided twice, once to 'Sound.RubberBand.Nice.study', which calculates
  a stretch profile for the audio, and once to
  'Sound.RubberBand.Nice.process', which stretches it.
  -}
  | RealTime
  {- ^
  Run the stretcher in real-time mode. In this mode only
  'Sound.RubberBand.Nice.process' should be called, and the
  stretcher adjusts dynamically in response to the input audio.
  -}
  deriving (Eq, Ord, Show, Read, Enum, Bounded)

{- |
'Stretch' flags control the profile used for
variable timestretching.  Rubber Band always adjusts the
stretch profile to minimise stretching of busy broadband
transient sounds, but the degree to which it does so is
adjustable.  These options may not be changed after
construction.
-}
data Stretch
  = Elastic
  {- ^
  Only meaningful in offline
  mode, and the default in that mode.  The audio will be
  stretched at a variable rate, aimed at preserving the quality
  of transient sounds as much as possible.  The timings of low
  activity regions between transients may be less exact than
  when the precise flag is set.
  -}
  | Precise
  {- ^
  Although still using a variable
  stretch rate, the audio will be stretched so as to maintain
  as close as possible to a linear stretch ratio throughout.
  Timing may be better than when using 'Elastic', at
  slight cost to the sound quality of transients.  This setting
  is always used when running in real-time mode.
  -}
  deriving (Eq, Ord, Show, Read, Enum, Bounded)

{- |
'Transients' flags control the component
frequency phase-reset mechanism that may be used at transient
points to provide clarity and realism to percussion and other
significant transient sounds.  These options may be changed
after construction when running in real-time mode, but not when
running in offline mode.
-}
data Transients
  = Crisp
  {- ^
  Reset component phases at the
  peak of each transient (the start of a significant note or
  percussive event).  This, the default setting, usually
  results in a clear-sounding output; but it is not always
  consistent, and may cause interruptions in stable sounds
  present at the same time as transient events.  The
  'Detector' flags can be used to tune this to some
  extent.
  -}
  | Mixed
  {- ^
  Reset component phases at the
  peak of each transient, outside a frequency range typical of
  musical fundamental frequencies.  The results may be more
  regular for mixed stable and percussive notes than
  'Crisp', but with a "phasier" sound.  The
  balance may sound very good for certain types of music and
  fairly bad for others.
  -}
  | Smooth
  {- ^
  Do not reset component phases
  at any point.  The results will be smoother and more regular
  but may be less clear than with either of the other
  'Transients' flags.
  -}
  deriving (Eq, Ord, Show, Read, Enum, Bounded)

{- |
'Detector' flags control the type of
transient detector used.  These options may be changed
after construction when running in real-time mode, but not when
running in offline mode.
-}
data Detector
  = Compound
  {- ^
  Use a general-purpose
  transient detector which is likely to be good for most
  situations.  This is the default.
  -}
  | Percussive
  {- ^
  Detect percussive
  transients.  Note that this was the default and only option
  in Rubber Band versions prior to 1.5.
  -}
  | Soft
  {- ^
  Use an onset detector with less
  of a bias toward percussive transients.  This may give better
  results with certain material (e.g. relatively monophonic
  piano music).
  -}
  deriving (Eq, Ord, Show, Read, Enum, Bounded)

{- |
'Phase' flags control the adjustment of
component frequency phases from one analysis window to the next
during non-transient segments.  These options may be changed at
any time.
-}
data Phase
  = Laminar
  {- ^
  Adjust phases when stretching in
  such a way as to try to retain the continuity of phase
  relationships between adjacent frequency bins whose phases
  are behaving in similar ways.  This, the default setting,
  should give good results in most situations.
  -}
  | Independent
  {- ^
  Adjust the phase in each
  frequency bin independently from its neighbours.  This
  usually results in a slightly softer, phasier sound.
  -}
  deriving (Eq, Ord, Show, Read, Enum, Bounded)

{- |
'Threading' flags control the threading
model of the stretcher.  These options may not be changed after
construction.
-}
data Threading
  = Auto
  {- ^
  Permit the stretcher to
  determine its own threading model.  Usually this means using
  one processing thread per audio channel in offline mode if
  the stretcher is able to determine that more than one CPU is
  available, and one thread only in realtime mode.  This is the
  defafult.
  -}
  | Never
  {- ^
  Never use more than one thread.
  -}
  | Always
  {- ^
  Use multiple threads in any
  situation where 'Auto' would do so, except omit
  the check for multiple CPUs and instead assume it to be true.
  -}
  deriving (Eq, Ord, Show, Read, Enum, Bounded)

{- |
'Window' flags control the window size for
FFT processing.  The window size actually used will depend on
many factors, but it can be influenced.  These options may not
be changed after construction.
-}
data Window
  = Standard
  {- ^
  Use the default window size.
  The actual size will vary depending on other parameters.
  This option is expected to produce better results than the
  other window options in most situations.
  -}
  | Short
  {- ^
  Use a shorter window.  This may
  result in crisper sound for audio that depends strongly on
  its timing qualities.
  -}
  | Long
  {- ^
  Use a longer window.  This is
  likely to result in a smoother sound at the expense of
  clarity and timing.
  -}
  deriving (Eq, Ord, Show, Read, Enum, Bounded)

{- |
'Smoothing' flags control the use of
window-presum FFT and time-domain smoothing.  These options may
not be changed after construction.
-}
data Smoothing
  = SmoothingOff
  {- ^
  Do not use time-domain smoothing.
  This is the default.
  -}
  | SmoothingOn
  {- ^
  Use time-domain smoothing.  This
  will result in a softer sound with some audible artifacts
  around sharp transients, but it may be appropriate for longer
  stretches of some instruments and can mix well with a 'Window' setting of
  'Short'.
  -}
  deriving (Eq, Ord, Show, Read, Enum, Bounded)

{- |
'Formant' flags control the handling of
formant shape (spectral envelope) when pitch-shifting.  These
options may be changed at any time.
-}
data Formant
  = Shifted
  {- ^
  Apply no special formant
  processing.  The spectral envelope will be pitch shifted as
  normal.  This is the default.
  -}
  | Preserved
  {- ^
  Preserve the spectral
  envelope of the unshifted signal.  This permits shifting the
  note frequency without so substantially affecting the
  perceived pitch profile of the voice or instrument.
  -}
  deriving (Eq, Ord, Show, Read, Enum, Bounded)

{- |
'Pitch' flags control the method used for
pitch shifting.  These options may be changed at any time.
They are only effective in realtime mode; in offline mode, the
pitch-shift method is fixed.
-}
data Pitch
  = HighSpeed
  {- ^
  Use a method with a CPU cost
  that is relatively moderate and predictable.  This may
  sound less clear than 'HighQuality', especially
  for large pitch shifts.  This is the default.
  -}
  | HighQuality
  {- ^
  Use the highest quality
  method for pitch shifting.  This method has a CPU cost
  approximately proportional to the required frequency shift.
  -}
  | HighConsistency
  {- ^
  Use the method that gives
  greatest consistency when used to create small variations in
  pitch around the 1.0-ratio level.  Unlike the previous two
  options, this avoids discontinuities when moving across the
  1.0 pitch scale in real-time; it also consumes more CPU than
  the others in the case where the pitch scale is exactly 1.0.
  -}
  deriving (Eq, Ord, Show, Read, Enum, Bounded)

{- |
'Channels' flags control the method used for
processing two-channel audio.  These options may not be changed
after construction.
-}
data Channels
  = Apart
  {- ^
  Each channel is processed
  individually, though timing is synchronised and phases are
  synchronised at transients (depending on the 'Transients'
  setting).  This gives the highest quality for the individual
  channels but a relative lack of stereo focus and unrealistic
  increase in "width".  This is the default.
  -}
  | Together
  {- ^
  The first two channels (where
  two or more are present) are considered to be a stereo pair
  and are processed in mid-side format; mid and side are
  processed individually, with timing synchronised and phases
  synchronised at transients (depending on the 'Transients'
  setting).  This usually leads to better focus in the centre
  but a loss of stereo space and width.  Any channels beyond
  the first two are processed individually.
  -}
  deriving (Eq, Ord, Show, Read, Enum, Bounded)

-- | A more limited enumeration class, without the extra functionality of
-- Haskell's 'Enum'.
class (Enum o, Bounded o) => Option o where
  -- | Note that 'optionEnum' returns different values from 'toEnum'.
  optionEnum :: o -> Int

setOption :: (Option o) => o -> Int -> Int
setOption o i = let
  allOptions = [minBound..maxBound] `asTypeOf` [o]
  mask = foldr (.|.) 0 $ map optionEnum allOptions
  in (i .&. complement mask) .|. optionEnum o
  -- Each option type uses some bits of an 'Int' for its value.
  -- Conveniently only the low 29 bits are used, fitting nicely
  -- into the Haskell Report's 30-bit guaranteed 'Int' size.

getOption :: (Option o) => Int -> o
getOption i = let
  allOptions = [minBound..maxBound] `asTypeOf` [o]
  lookupList = zip (map optionEnum allOptions) allOptions
  mask = foldr (.|.) 0 $ map fst lookupList
  masked = i .&. mask
  o = fromMaybe
    (error $ "getOption: no result for masked value " ++ show masked)
    (lookup masked lookupList)
  in o

toOptions :: Int -> Options
toOptions i = Options
  (getOption i)
  (getOption i)
  (getOption i)
  (getOption i)
  (getOption i)
  (getOption i)
  (getOption i)
  (getOption i)
  (getOption i)
  (getOption i)
  (getOption i)

fromOptions :: Options -> Int
fromOptions (Options a b c d e f g h i j k) = foldr (.|.) 0
  [ optionEnum a
  , optionEnum b
  , optionEnum c
  , optionEnum d
  , optionEnum e
  , optionEnum f
  , optionEnum g
  , optionEnum h
  , optionEnum i
  , optionEnum j
  , optionEnum k
  ]

instance Option Process where
  optionEnum Offline  = 0x00000000
  optionEnum RealTime = 0x00000001

instance Option Stretch where
  optionEnum Elastic = 0x00000000
  optionEnum Precise = 0x00000010

instance Option Transients where
  optionEnum Crisp  = 0x00000000
  optionEnum Mixed  = 0x00000100
  optionEnum Smooth = 0x00000200

instance Option Detector where
  optionEnum Compound   = 0x00000000
  optionEnum Percussive = 0x00000400
  optionEnum Soft       = 0x00000800

instance Option Phase where
  optionEnum Laminar     = 0x00000000
  optionEnum Independent = 0x00002000

instance Option Threading where
  optionEnum Auto   = 0x00000000
  optionEnum Never  = 0x00010000
  optionEnum Always = 0x00020000

instance Option Window where
  optionEnum Standard = 0x00000000
  optionEnum Short    = 0x00100000
  optionEnum Long     = 0x00200000

instance Option Smoothing where
  optionEnum SmoothingOff = 0x00000000
  optionEnum SmoothingOn  = 0x00800000

instance Option Formant where
  optionEnum Shifted   = 0x00000000
  optionEnum Preserved = 0x01000000

instance Option Pitch where
  optionEnum HighSpeed       = 0x00000000
  optionEnum HighQuality     = 0x02000000
  optionEnum HighConsistency = 0x04000000

instance Option Channels where
  optionEnum Apart    = 0x00000000
  optionEnum Together = 0x10000000

-- | Intended to give good results in most situations.
defaultOptions :: Options
defaultOptions = toOptions 0x00000000

percussiveOptions :: Options
percussiveOptions = toOptions 0x00102000