module Music.Score.Export.Midi (
HasMidiProgram(..),
Midi,
HasMidi,
toMidi,
writeMidi,
showMidi,
openMidi,
) where
import Music.Dynamics.Literal
import Music.Pitch.Literal
import qualified Codec.Midi as Midi
import Control.Comonad (Comonad (..), extract)
import Control.Applicative
import Data.Colour.Names as Color
import Data.Foldable (Foldable)
import qualified Data.Foldable
import Data.Functor.Couple
import Data.Maybe
import Data.Ratio
import Data.Traversable (Traversable, sequenceA)
import Music.Score.Internal.Export hiding (MVoice)
import System.Process
import Music.Score.Internal.Quantize
import qualified Text.Pretty as Pretty
import qualified Data.List
import Music.Score.Internal.Util (composed, unRatio, swap, retainUpdates)
import Music.Score.Export.DynamicNotation
import Data.Semigroup.Instances
import Data.Functor.Identity
import Data.Semigroup
import Control.Monad
import Data.VectorSpace hiding (Sum(..))
import Data.AffineSpace
import Control.Lens (over)
import Control.Lens.Operators hiding ((|>))
import qualified Data.List as List
import Music.Time
import Music.Score.Dynamics
import Music.Score.Articulation
import Music.Score.Part
import Music.Score.Tremolo
import Music.Score.Text
import Music.Score.Harmonics
import Music.Score.Slide
import Music.Score.Color
import Music.Score.Ties
import Music.Score.Meta
import Music.Score.Meta.Tempo
import Music.Score.Export.Backend
class HasMidiProgram a where
getMidiChannel :: a -> Midi.Channel
getMidiProgram :: a -> Midi.Preset
getMidiChannel _ = 0
instance HasMidiProgram () where
getMidiProgram _ = 0
instance HasMidiProgram Double where
getMidiProgram = fromIntegral . floor
instance HasMidiProgram Float where
getMidiProgram = fromIntegral . floor
instance HasMidiProgram Int where
getMidiProgram = id
instance HasMidiProgram Integer where
getMidiProgram = fromIntegral
instance (Integral a, HasMidiProgram a) => HasMidiProgram (Ratio a) where
getMidiProgram = fromIntegral . floor
data Midi
type MidiContext = Identity
type MidiEvent = Score Midi.Message
type MidiInstr = (Midi.Channel, Midi.Preset)
data MidiScore a = MidiScore [(MidiInstr, Score a)]
deriving Functor
instance HasBackend Midi where
type BackendScore Midi = MidiScore
type BackendContext Midi = MidiContext
type BackendNote Midi = MidiEvent
type BackendMusic Midi = Midi.Midi
finalizeExport _ (MidiScore trs) = let
controlTrack = [(0, Midi.TempoChange 1000000), (endDelta, Midi.TrackEnd)]
mainTracks = fmap (uncurry translMidiTrack . fmap join) trs
in
Midi.Midi fileType (Midi.TicksPerBeat divisions) (controlTrack : mainTracks)
where
translMidiTrack :: MidiInstr -> Score Midi.Message -> [(Int, Midi.Message)]
translMidiTrack (ch, p) = addTrackEnd
. setProgramChannel ch p
. scoreToMidiTrack
addTrackEnd :: [(Int, Midi.Message)] -> [(Int, Midi.Message)]
addTrackEnd = (<> [(endDelta, Midi.TrackEnd)])
setProgramChannel :: Midi.Channel -> Midi.Preset -> Midi.Track Midi.Ticks -> Midi.Track Midi.Ticks
setProgramChannel ch prg = ([(0, Midi.ProgramChange ch prg)] <>) . fmap (fmap $ setC ch)
scoreToMidiTrack :: Score Midi.Message -> Midi.Track Midi.Ticks
scoreToMidiTrack = fmap (\(t,_,x) -> (round ((t .-. 0) ^* divisions), x)) . toRelative . (^. triples)
fileType = Midi.MultiTrack
divisions = 1024
endDelta = 10000
toRelative :: [(Time, Duration, b)] -> [(Time, Duration, b)]
toRelative = snd . List.mapAccumL g 0
where
g now (t,d,x) = (t, (0 .+^ (t .-. now),d,x))
instance (HasPart' a, HasMidiProgram (Part a)) => HasBackendScore Midi (Voice a) where
type BackendScoreEvent Midi (Voice a) = a
exportScore _ xs = MidiScore [((getMidiChannel (xs^?!parts), getMidiProgram (xs^?!parts)), fmap Identity $ voiceToScore xs)]
where
voiceToScore :: Voice a -> Score a
voiceToScore = renderAlignedVoice . aligned (0 :: Time) (0 :: LocalDuration)
instance (HasPart' a, Ord (Part a), HasMidiProgram (Part a)) => HasBackendScore Midi (Score a) where
type BackendScoreEvent Midi (Score a) = a
exportScore _ xs = MidiScore (map (\(p,sc) -> ((getMidiChannel p, getMidiProgram p), fmap Identity sc))
$ extractPartsWithInfo $ fixTempo $ normalizeScore xs)
where
fixTempo = stretch (tempoToDuration (metaAtStart xs))
instance HasBackendNote Midi a => HasBackendNote Midi [a] where
exportNote b ps = mconcat $ map (exportNote b) $ sequenceA ps
instance HasBackendNote Midi Int where
exportNote _ (Identity pv) = mkMidiNote pv
instance HasBackendNote Midi Integer where
exportNote _ (Identity pv) = mkMidiNote (fromIntegral pv)
instance HasBackendNote Midi Float where
exportNote b = exportNote b . fmap (toInteger . round)
exportChord b = exportChord b . fmap (fmap (toInteger . round))
instance HasBackendNote Midi Double where
exportNote b = exportNote b . fmap (toInteger . round)
exportChord b = exportChord b . fmap (fmap (toInteger . round))
instance HasBackendNote Midi a => HasBackendNote Midi (Behavior a) where
exportNote b = exportNote b . fmap (! 0)
exportChord b = exportChord b . fmap (fmap (! 0))
instance (Real d, HasBackendNote Midi a) => HasBackendNote Midi (DynamicT d a) where
exportNote b (Identity (DynamicT (realToFrac -> d, x))) = setV (dynLevel d) <$> exportNote b (Identity x)
dynLevel :: Double -> Midi.Velocity
dynLevel x = round $ (\x -> x * 58.5 + 64) $ f $inRange (1,1) (x/3.5)
where
f = id
inRange (m,n) x = (m `max` x) `min` n
instance HasBackendNote Midi a => HasBackendNote Midi (ArticulationT b a) where
exportNote b (Identity (ArticulationT (_, x))) = exportNote b (Identity x)
instance HasBackendNote Midi a => HasBackendNote Midi (PartT n a) where
exportNote b = exportNote b . fmap extract
instance HasBackendNote Midi a => HasBackendNote Midi (TremoloT a) where
exportNote b = exportNote b . fmap extract
instance HasBackendNote Midi a => HasBackendNote Midi (TextT a) where
exportNote b = exportNote b . fmap extract
instance HasBackendNote Midi a => HasBackendNote Midi (HarmonicT a) where
exportNote b = exportNote b . fmap extract
instance HasBackendNote Midi a => HasBackendNote Midi (SlideT a) where
exportNote b = exportNote b . fmap extract
instance HasBackendNote Midi a => HasBackendNote Midi (TieT a) where
exportNote b = exportNote b . fmap extract
instance HasBackendNote Midi a => HasBackendNote Midi (ColorT a) where
exportNote b = exportNote b . fmap extract
mkMidiNote :: Int -> Score Midi.Message
mkMidiNote p = mempty
|> pure (Midi.NoteOn 0 (fromIntegral $ p + 60) 64)
|> pure (Midi.NoteOff 0 (fromIntegral $ p + 60) 64)
setV :: Midi.Velocity -> Midi.Message -> Midi.Message
setV v = go
where
go (Midi.NoteOff c k _) = Midi.NoteOff c k v
go (Midi.NoteOn c k _) = Midi.NoteOn c k v
go (Midi.KeyPressure c k _) = Midi.KeyPressure c k v
go (Midi.ControlChange c n v) = Midi.ControlChange c n v
go (Midi.ProgramChange c p) = Midi.ProgramChange c p
go (Midi.ChannelPressure c p) = Midi.ChannelPressure c p
go (Midi.PitchWheel c w) = Midi.PitchWheel c w
go (Midi.ChannelPrefix c) = Midi.ChannelPrefix c
setC :: Midi.Channel -> Midi.Message -> Midi.Message
setC c = go
where
go (Midi.NoteOff _ k v) = Midi.NoteOff c k v
go (Midi.NoteOn _ k v) = Midi.NoteOn c k v
go (Midi.KeyPressure _ k v) = Midi.KeyPressure c k v
go (Midi.ControlChange _ n v) = Midi.ControlChange c n v
go (Midi.ProgramChange _ p) = Midi.ProgramChange c p
go (Midi.ChannelPressure _ p) = Midi.ChannelPressure c p
go (Midi.PitchWheel _ w) = Midi.PitchWheel c w
go (Midi.ChannelPrefix _) = Midi.ChannelPrefix c
type HasMidi a = (HasBackendNote Midi (BackendScoreEvent Midi a), HasBackendScore Midi a)
toMidi :: HasMidi a => a -> Midi.Midi
toMidi = export (undefined::Midi)
writeMidi :: HasMidi a => FilePath -> a -> IO ()
writeMidi path sc = Midi.exportFile path (toMidi sc)
showMidi :: HasMidi a => a -> IO ()
showMidi = print . toMidi
openMidi :: HasMidi a => a -> IO ()
openMidi score = do
writeMidi "test.mid" score
void $ runCommand "timidity test.mid" >>= waitForProcess