music-score-1.6: src/Music/Score/Chord.hs
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE ViewPatterns #-}
-------------------------------------------------------------------------------------
-- |
-- Copyright : (c) Hans Hoglund 2012
--
-- License : BSD-style
--
-- Maintainer : hans@hanshoglund.se
-- Stability : experimental
-- Portability : non-portable (TF,GNTD)
--
-- Provides a representation for chords.
--
-------------------------------------------------------------------------------------
module Music.Score.Chord (
-- * Chord representation
HasChord(..),
ChordT(..),
-- * Voice separation
separateVoices,
mergePossible,
-- * Chord transformations
takeNoteInChord,
dropNoteInChord,
takeNotesInChord,
dropNotesInChord,
mapSimultaneous,
simultaneous,
simultaneous',
) where
import Prelude hiding (any, mapM_)
import Control.Lens hiding (perform)
import Control.Monad.Plus hiding (mapM_)
import Data.Foldable
import qualified Data.List as List
import qualified Data.List.NonEmpty as NonEmpty
import Data.Ord
import Data.Semigroup
import Data.Typeable
import Music.Score.Combinators
import Music.Score.Convert
import Music.Score.Note
import Music.Score.Part
import Music.Score.Score
import Music.Score.Voice
import Music.Score.Meta
import Music.Time
class HasChord a where
type ChordNote a :: *
-- TODO use NonEmpty
getChord :: a -> [ChordNote a]
instance HasChord [a] where
type ChordNote [a] = a
getChord = id
instance HasChord (ChordT a) where
type ChordNote (ChordT a) = a
getChord (ChordT as) = as
-- TODO Use NonEmpty
newtype ChordT a = ChordT { getChordT :: [a] }
deriving (Eq, Show, Ord, Monad, Functor, Monoid, Semigroup, Foldable, Typeable)
overlaps :: (HasOnset a, HasOffset a, HasOnset b, HasOffset b) => a -> b -> Bool
overlaps t u = not $ offset t <= onset u || offset u <= onset t
overlapsAny :: (Foldable t, HasOnset a, HasOffset a, HasOnset b, HasOffset b) => a -> t b -> Bool
overlapsAny x = any (overlaps x)
notOverlaps :: (HasOnset a, HasOnset b, HasOffset a, HasOffset b) => a -> b -> Bool
x `notOverlaps` y = not (x `overlaps` y)
hasOverlapping :: Score a -> Bool
hasOverlapping x = let ns = (^. notes) x in not $ null [(x,y) | x <- ns, y <- ns, x `overlaps` y, era x /= era y]
-- | Heuristically merge voices if possible
mergePossible :: [Score a] -> [Score a]
mergePossible [] = []
mergePossible (x:xs) = let
pick = x
(res, rest) = List.foldr mergeMaybe' (x, []) xs
in res : mergePossible rest
mergeMaybe' x (y,rest) = if hasOverlapping (x <> y) then (x, y:rest) else (x <> y, rest)
mergeMaybe x y = if hasOverlapping (x <> y) then (x, Just y) else (x <> y, Nothing)
notOverlapsHead :: (HasOnset a, HasOnset b, HasOffset a, HasOffset b) => a -> [b] -> Bool
x `notOverlapsHead` [] = True
x `notOverlapsHead` xs = x `notOverlaps` head xs
class Null a where
isNull :: a -> Bool
-- > isNull mempty
instance Null [a] where
isNull = null
nonNull = not . isNull
class Divisible a where
divide :: a -> (a, a)
data Tower a = Tower [a] a [a]
deriving (Functor, Eq, Show)
tower x = Tower (repeat mempty) x (repeat mempty)
moveUp (Tower (a:as) x (b:bs)) = Tower (x:a:as) b bs
moveDown (Tower (a:as) x (b:bs)) = Tower as a (x:b:bs)
top :: (Monoid a, Null a) => Tower a -> [a]
top (Tower as x sa) = List.takeWhile nonNull sa
middle :: Tower a -> a
middle (Tower as x sa) = x
bottom :: (Monoid a, Null a) => Tower a -> [a]
bottom (Tower as x sa) = reverse (List.takeWhile nonNull as)
-- semantic function
floors :: (Monoid a, Null a) => Tower a -> ([a], a, [a])
floors t = (bottom t, middle t, top t)
compareHead x [] = EQ
compareHead x (y:_) = x `compare` y
comparingHead p x y = compareHead (p x) (fmap p y)
compareHeadVal = comparingHead getNoteValue
pushNote :: Ord a => Note a -> Tower [Note a] -> Tower [Note a]
pushNote n t = if n `notOverlapsHead` middle t then pushMiddle n t else
case n `compareHeadVal` middle t of
GT -> moveUp $ pushNote n (moveDown t)
_ -> moveDown $ pushNote n (moveUp t)
pushMiddle :: a -> Tower [a] -> Tower [a]
pushMiddle x (Tower as a sa) = Tower as (x:a) sa
separateVoices :: Ord a => Score a -> [Score ( a)]
separateVoices = fmap (^. from notes) . f . (^. notes)
where
f = (\(as,x,bs) -> as++[x]++bs) . floors . List.foldr pushNote (tower []) . List.sortBy (comparing getNoteSpan)
-- Note:
--
-- The HasChord instance (for other transformer types) takes care to transform strucuture *above* the chord representation
-- In particular, getChord will extract the chord from below and transform each note (or only the first etc)
-- as appropriate for the given type.
-- The ChordT instances (of other transformer classes) transforms structure *below* the chord representation
-- For example, it allow us to use functions such as up, down, legato etc on chords.
-- TODO rewrite, generalize?
takeNotesInChord n = mapSimultaneous (fmap $ take n)
dropNotesInChord n = mapSimultaneous (fmap $ drop n)
takeNoteInChord n = mapSimultaneous $ (fmap $ take 1) . (fmap $ drop (n - 1))
dropNoteInChord n = mapSimultaneous $ (fmap $ drop1 n)
drop1 n xs = take (n - 1) xs <> drop n xs
-- |
-- Process all simultaneous events.
--
-- Two events /a/ and /b/ are considered simultaneous if and only if they have the same
-- era, that is if @`era` a == `era` b@
--
mapSimultaneous :: (Score [a] -> Score [b]) -> Score a -> Score b
mapSimultaneous f = mscatter . f . simultaneous'
-- |
-- Merge all simultaneous events using their 'Semigroup' instance.
--
-- Two events /a/ and /b/ are considered simultaneous if and only if they have the same
-- era, that is if @`era` a == `era` b@
--
simultaneous :: Semigroup a => Score a -> Score a
simultaneous = fmap (sconcat . NonEmpty.fromList) . simultaneous'
-- |
-- Group simultaneous events as lists.
--
-- Two events /a/ and /b/ are considered simultaneous if and only if they have the same
-- era, that is if @`era` a == `era` b@
--
-- Note that 'simultaneous' is identical to 'simultaneous' @.@ 'fmap' 'return'
--
simultaneous' :: Score a -> Score [a]
simultaneous' sc = (meta .~) m $ (^. from events) vs
where
m = (view meta) sc
-- es :: [Era]
-- evs :: [[a]]
-- vs :: [(Time, Duration, [a])]
es = List.nub $ eras sc
evs = fmap (`chordEvents` sc) es
vs = zipWith (\(view delta -> (t,d)) a -> (t,d,a)) es evs
-- TODO (re)move these
eras :: Score a -> [Span]
eras sc = fmap getSpan . (^. events) $ sc
chordEvents :: Span -> Score a -> [a]
chordEvents era sc = fmap getValue . filter (\ev -> getSpan ev == era) . (^. events) $ sc
getValue :: (Time, Duration, a) -> a
getValue (t,d,a) = a
getSpan :: (Time, Duration, a) -> Span
getSpan (t,d,a) = t >-> d