packages feed

herald-0.1.0.1: src/Herald/Performance.hs

-- | Renders "Herald.Score" voices to sound.
--
-- >>> let short = setMeter (Meter 4 4) >> tellNotes quarter [c4, d4, e4] >> tellBar
-- >>> either displayScoreError (const "ok") (playScoreMono defaultConfig [(defaultVoice, short)])
-- "voice 1, bar 1: the bar has 3.0 beats but the meter has 4.0"
module Herald.Performance
  ( Performance,
    ScoreConfig (..),
    defaultConfig,
    VoiceConfig (..),
    defaultVoice,
    playScoreMono,
    playScoreStereo,
    ScoreError (..),
    ErrorReason (..),
    displayScoreError,
  )
where

import Control.Monad (forM_, when, zipWithM)
import Control.Monad.Except (MonadError (..))
import Control.Monad.Reader
import Control.Monad.State.Strict
import Control.Monad.Writer.Strict
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Vector.Unboxed qualified as V
import Herald.Performance.Instrument
import Herald.Score
import Herald.Score.Duration
import Herald.Score.Dynamic
import Herald.Score.Pitch
import Herald.Sound
import Herald.Tuning

-- | Shared by every voice.
data ScoreConfig = ScoreConfig
  { tempo :: Bpm,
    tuning :: Tuning
  }

-- | 120 bpm in equal temperament at 440 Hz.
defaultConfig :: ScoreConfig
defaultConfig =
  ScoreConfig
    { tempo = 120,
      tuning = a440
    }

-- | How a voice is performed.
data VoiceConfig = VoiceConfig
  { instrument :: Instrument,
    -- | Ignored by 'playScoreMono'.
    pan :: Pan
  }

-- | The additive instrument, centred.
defaultVoice :: VoiceConfig
defaultVoice =
  VoiceConfig
    { instrument = additive,
      pan = 0
    }

-- | Voices and bars count from 1.
data ScoreError = ScoreError
  { errorVoice :: Int,
    errorBar :: Int,
    errorReason :: ErrorReason
  }
  deriving (Show, Eq)

-- | A notation mistake in a voice.
data ErrorReason
  = -- | Actual and expected beats.
    WrongBarLength Beats Beats
  | TieToDifferentPitch
  | UnfinishedTie
  deriving (Show, Eq)

-- | As in @voice 2, bar 49: the bar has 4.25 beats but the meter has 4.0@.
displayScoreError :: ScoreError -> String
displayScoreError (ScoreError v b reason) =
  "voice " ++ show v ++ ", bar " ++ show b ++ ": " ++ case reason of
    WrongBarLength actual expected ->
      "the bar has " ++ show actual ++ " beats but the meter has " ++ show expected
    TieToDifferentPitch -> "a tie continues into a different pitch"
    UnfinishedTie -> "a tie is not followed by a note of the same pitch"

-- | Voices can only use the 'Score' operations, not its state or errors.
newtype Performance a = Performance (Perform a)
  deriving newtype (Functor, Applicative, Monad)

instance Score Performance where
  tellNote b p = Performance (noteP b p)
  tellTie b p = Performance (tieP b p)
  tellChord b ps = Performance (chordP b ps)
  tellTrill b k p q = Performance (trillP b k p q)
  tellRest b = Performance (soundP b silence)
  tellBar = Performance barP
  setKey k = Performance (modify' (\st -> st {stateKey = k}))
  setMeter m = Performance (modify' (\st -> st {stateMeter = Just m}))
  setDynamic d = Performance (modify' (\st -> st {stateDynamic = d}))

type Perform =
  ReaderT (ScoreConfig, VoiceConfig) (StateT PerformState (WriterT Track (Either ScoreError)))

data PerformState = PerformState
  { stateKey :: Key,
    stateMeter :: Maybe Meter,
    stateDynamic :: Dynamic,
    stateBarAccidentals :: Map (Letter, Octave) Accidental,
    stateBar :: Int,
    stateBarBeats :: Beats,
    stateTie :: Maybe Tie
  }

data Tie = Tie
  { tieWritten :: (Letter, Octave),
    tieSemitones :: Semitones,
    tieDynamic :: Dynamic,
    tieBeats :: Beats
  }

-- | Chunks joined once at the end: appending vectors note by note would be
-- quadratic.
newtype Track = Track [Sound]
  deriving newtype (Semigroup, Monoid)

-- | Perform the voices in mono.
playScoreMono :: ScoreConfig -> [(VoiceConfig, Performance ())] -> Either ScoreError Sound
playScoreMono config voices = balanceSound . map snd <$> performVoices config voices

-- | Perform the voices in stereo.
playScoreStereo :: ScoreConfig -> [(VoiceConfig, Performance ())] -> Either ScoreError Stereo
playScoreStereo config voices =
  balanceStereo . map (\(v, s) -> panSound (pan v) s) <$> performVoices config voices

performVoices :: ScoreConfig -> [(VoiceConfig, Performance ())] -> Either ScoreError [(VoiceConfig, Sound)]
performVoices config = zipWithM perform [1 ..]
  where
    perform k (v, Performance voice) =
      case runWriterT (execStateT (runReaderT (voice >> finishP) (config, v)) start) of
        Left e -> Left e {errorVoice = k}
        Right (_, Track chunks) -> Right (v, V.concat chunks)
    start =
      PerformState
        { stateKey = cMajor,
          stateMeter = Nothing,
          stateDynamic = mezzoForte,
          stateBarAccidentals = Map.empty,
          stateBar = 1,
          stateBarBeats = 0,
          stateTie = Nothing
        }

finishP :: Perform ()
finishP = do
  requireNoTie
  beats <- gets stateBarBeats
  when (beats > 0) checkBar

failWith :: ErrorReason -> Perform a
failWith reason = do
  b <- gets stateBar
  throwError (ScoreError 0 b reason)

barP :: Perform ()
barP = do
  checkBar
  modify' $ \st ->
    st
      { stateBar = stateBar st + 1,
        stateBarBeats = 0,
        stateBarAccidentals = Map.empty
      }

checkBar :: Perform ()
checkBar = do
  meter <- gets stateMeter
  beats <- gets stateBarBeats
  forM_ meter $ \m ->
    when (abs (beats - meterBeats m) > 1e-9) $
      failWith (WrongBarLength beats (meterBeats m))

-- | A written accidental lasts until the end of the bar for that letter and
-- octave; an unwritten one comes from earlier in the bar, else the key.
resolve :: Pitch -> Perform Semitones
resolve p = do
  k <- gets stateKey
  case accidental p of
    Just a -> do
      modify' $ \st -> st {stateBarAccidentals = Map.insert here a (stateBarAccidentals st)}
      pure (pitchSemitones k p)
    Nothing -> do
      written <- gets (Map.lookup here . stateBarAccidentals)
      pure (pitchSemitones k p {accidental = written})
  where
    here = (letter p, octave p)

-- | Like 'resolve' without recording the accidental, for trill auxiliaries.
resolveAux :: Pitch -> Perform Semitones
resolveAux p = do
  k <- gets stateKey
  written <- gets (Map.lookup (letter p, octave p) . stateBarAccidentals)
  pure (pitchSemitones k p {accidental = maybe written Just (accidental p)})

advance :: Beats -> Perform ()
advance b = modify' (\st -> st {stateBarBeats = stateBarBeats st + b})

seconds :: Beats -> Perform Seconds
seconds b = asks (\(config, _) -> b * (60.0 / tempo config))

frequency :: Semitones -> Perform Hz
frequency n = asks (\(config, _) -> tune (tuning config) n)

voiceInstrument :: Perform Instrument
voiceInstrument = asks (instrument . snd)

emit :: Dynamic -> Sound -> Perform ()
emit d s = tell $ Track [gain (dynamicGain d) s]

soundP :: Beats -> (Seconds -> Sound) -> Perform ()
soundP b sound = do
  requireNoTie
  t <- seconds b
  d <- gets stateDynamic
  emit d (sound t)
  advance b

requireNoTie :: Perform ()
requireNoTie = do
  t <- gets stateTie
  forM_ t $ \_ -> failWith UnfinishedTie

continueTie :: Beats -> Pitch -> Tie -> Perform Tie
continueTie b p t = do
  when ((letter p, octave p) /= tieWritten t) $ failWith TieToDifferentPitch
  advance b
  pure t {tieBeats = tieBeats t + b}

noteP :: Beats -> Pitch -> Perform ()
noteP b p =
  gets stateTie >>= \case
    Nothing -> do
      f <- resolve p >>= frequency
      i <- voiceInstrument
      soundP b (renderNote i f)
    Just tie -> do
      tie' <- continueTie b p tie
      modify' (\st -> st {stateTie = Nothing})
      f <- frequency (tieSemitones tie')
      t <- seconds (tieBeats tie')
      i <- voiceInstrument
      emit (tieDynamic tie') (renderNote i f t)

tieP :: Beats -> Pitch -> Perform ()
tieP b p =
  gets stateTie >>= \case
    Nothing -> do
      n <- resolve p
      d <- gets stateDynamic
      advance b
      modify' (\st -> st {stateTie = Just (Tie (letter p, octave p) n d b)})
    Just tie -> do
      tie' <- continueTie b p tie
      modify' (\st -> st {stateTie = Just tie'})

chordP :: Beats -> [Pitch] -> Perform ()
chordP b ps = do
  fs <- mapM (\p -> resolve p >>= frequency) ps
  i <- voiceInstrument
  soundP b (chord i fs)

trillP :: Beats -> Int -> Pitch -> Pitch -> Perform ()
trillP b k p q = do
  f <- resolve p >>= frequency
  g <- resolveAux q >>= frequency
  i <- voiceInstrument
  soundP b (trill i k f g)