-- -- This module describes how to create MIDI file from MIDA environment. -- -- Copyright © 2014–2016 Mark Karpov -- -- MIDA is free software: you can redistribute it and/or modify it under the -- terms of the GNU General Public License as published by the Free Software -- Foundation, either version 3 of the License, or (at your option) any -- later version. -- -- MIDA is distributed in the hope that it will be useful, but WITHOUT ANY -- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -- FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -- details. -- -- You should have received a copy of the GNU General Public License along -- with this program. If not, see . {-# LANGUAGE BangPatterns #-} {-# LANGUAGE RecordWildCards #-} module Mida.Midi ( genMidi , topDefs ) where import Control.Monad.State.Strict import Data.Foldable (foldl') import Data.List (zipWith7) import Mida.Language (HasEnv, setRandGen, evalDef) import Numeric.Natural import Prelude hiding (mod) import qualified Codec.Midi as Midi -- | 'Batch' is collection of all parameters needed to generate complete -- MIDI track. data Batch = Batch { btDur :: [Int] -- ^ Duration stream , btVel :: [Int] -- ^ Velocity stream , btPch :: [Int] -- ^ Pitch stream , _btMod :: Maybe [Int] -- ^ Optional modulation stream , _btBth :: Maybe [Int] -- ^ Optional breath stream , _btAft :: Maybe [Int] -- ^ Optional aftertouch stream , _btBnd :: Maybe [Int] -- ^ Optional pitch bend stream } -- | Apply transformation on all streams in 'Batch'. infixl 4 () :: ([Int] -> [Int]) -> Batch -> Batch f (Batch d v p m t a b) = Batch (f d) (f v) (f p) (f <$> m) (f <$> t) (f <$> a) (f <$> b) -- | Modulation parameters. This defines how sound will be modulated (if at -- all). data ModParams = ModParams { mpValue :: !Int -- ^ Value or amplitude of modulation , mpFigure :: !(Maybe Figure) -- ^ Figure or shape of modulation , mpDuration :: !Int -- ^ Duration in ticks , mpChannel :: !Int -- ^ Channel index , mpProducer :: !(Int -> Int -> Midi.Message) -- ^ Producer function , mpUpBounds :: !(Int, Int) -- ^ Boundaries for ascending modulation , mpDnBounds :: !(Int, Int) -- ^ Boundaries for descending modulation } -- | Modulation figures. data Figure = FigStatic -- ^ Static: no modulation | FigUpDown -- ^ First ascending and then descending | FigDownUp -- ^ First descending and then ascending | FigUp -- ^ Ascending for the whole duration | FigDown -- ^ Descending for the whole duration deriving (Eq, Show, Bounded, Enum) -- | Default modulation parameters. modP :: ModParams modP = ModParams { mpValue = 0 , mpFigure = Just FigStatic , mpDuration = 0 , mpChannel = 0 , mpProducer = flip Midi.ControlChange 1 , mpUpBounds = (0x0000, 0x007f) , mpDnBounds = (0x007f, 0x0000) } -- | Default modulation parameters for breath modulation. bthP :: ModParams bthP = modP { mpProducer = flip Midi.ControlChange 2 } -- | Default modulation parameters for aftertouch modulation. aftP :: ModParams aftP = modP { mpProducer = Midi.ChannelPressure } -- | Default modulation parameters for pitch bend modulation. bndP :: ModParams bndP = modP { mpProducer = Midi.PitchWheel , mpUpBounds = (0x2000, 0x3fff) , mpDnBounds = (0x2000, 0x0000) } -- | A synonym for what we use as track definition. type Track = Midi.Track Int -- | Generate MIDI file from MIDA environment. genMidi :: HasEnv m => Natural -- ^ Seed for random generator -> Natural -- ^ Q value: number of ticks per quarter note -> Natural -- ^ Duration in number of quarter notes -> m Midi.Midi -- ^ MIDI file genMidi seed q b = do setRandGen seed voices <- filter defined <$> mapM request [0..mvIndex] return Midi.Midi { Midi.fileType = Midi.MultiTrack , Midi.timeDiv = Midi.TicksPerBeat (fromIntegral q) , Midi.tracks = zipWith (toTrack . slice (b * q)) voices [0..] } -- | Generate MIDI track given its index. request :: HasEnv m => Natural -- ^ Track index -> m Batch -- ^ 'Batch' for this track request n = do dur <- eval' defDur vel <- eval' defVel pch <- eval' defPch mod <- eval' defMod bth <- eval' defBth aft <- eval' defAft bnd <- eval' defBnd return $ Batch dur vel pch (f mod) (f bth) (f aft) (f bnd) where eval' name = fmap fromIntegral <$> evalDef (name ++ show n) f x = if null x then Nothing else Just x -- | Check if all necessary components in a 'Batch' are defined. defined :: Batch -> Bool defined Batch {..} = all (not . null) [btDur, btVel, btPch] -- | Take some part (determined by number of ticks) of every -- infinite stream in 'Batch' making it finite. slice :: Natural -- ^ Requested duration in ticks -> Batch -- ^ 'Batch' of infinite streams -> Batch -- ^ 'Batch' of finite streams slice t' batch@Batch {..} = take (f 0 0 btDur) batch where t = fromIntegral t' f !i _ [] = i f !i !a (x:xs) = if x + a >= t then succ i else f (succ i) (x + a) xs -- | Generate MIDI 'Track' from 'Batch'. toTrack :: Batch -- ^ Batch -> Int -- ^ Channel number -> Track -- ^ Result track toTrack (Batch d v p m t a b) i = concat (zipWith7 f d v p (r m) (r t) (r a) (r b)) ++ [(0, Midi.TrackEnd)] where r = maybe (repeat Nothing) (fmap Just) f d' v' p' m' t' a' b' = mixEvents [ figure m' d' i modP , figure t' d' i bthP , figure a' d' i aftP , figure b' d' i bndP , [ (0, Midi.NoteOn i p' v') , (d', Midi.NoteOn i p' 0)] ] -- | Merge several tracks together. There is 'Codec.Midi.merge' thing, but -- I'm not sure it does it right, moreover it's not documented. mixEvents :: [Track] -> Track mixEvents = foldl' mixPair mempty -- | Merge just two tracks. mixPair :: Track -> Track -> Track mixPair [] xs = xs mixPair xs [] = xs mixPair (x:xs) (y:ys) = r : mixPair xs' ys' where (r, xs', ys') | fst x <= fst y = (x, xs, f y (fst x) : ys) | otherwise = (y, f x (fst y) : xs, ys) f (i, msg) c = (i - c, msg) -- | Generate track fragment representing modulation of one parameter. figure :: Maybe Int -- ^ Raw modulation value, if present -> Int -- ^ Duration in ticks of entire fragment -> Int -- ^ Channel index -> ModParams -- ^ Default modulation parameters -> Track -- ^ Result fragment figure Nothing _ _ _ = [] figure (Just raw) d ch p = fig p { mpValue = v , mpFigure = f , mpDuration = d , mpChannel = ch } where (fi, v) = quotRem raw 128 f | fi <= fromEnum (maxBound :: Figure) = Just (toEnum fi) | otherwise = Nothing -- | Transform modulation parameters into fragment of 'Track'. fig :: ModParams -> Track fig ModParams { mpDuration = 0 } = [] fig (ModParams v f d ch p ub db) = let getGen x = case x of FigStatic -> figStc ub FigUpDown -> figRtn ub d FigDownUp -> figRtn db d FigUp -> figLin ub d FigDown -> figLin db d in maybe [] (zip (0 : repeat 1) . fmap (p ch)) (getGen <$> f <*> pure v) -- | Generate static stream of values. figStc :: (Int, Int) -- ^ Beginning and end values -> Int -- ^ Amplitude (from 0 to 127) -> [Int] -- ^ Resulting stream figStc be x = [draw be x 1] -- | Generate “up-down” or “down-up” stream of values (i.e. “returning -- stream”). figRtn :: (Int, Int) -- ^ Beginning and end values -> Int -- ^ Total number of elements in stream -> Int -- ^ Amplitude (from 0 to 127) -> [Int] -- ^ Resulting stream figRtn be q x = f <$> [0..l] ++ reverse [0..(q - l - 1)] where f c = draw be (x * c) l l = q `div` 2 -- | Generate linear stream of values. figLin :: (Int, Int) -- ^ Beginning and end values -> Int -- ^ Total number of elements in stream -> Int -- ^ Amplitude (from 0 to 127) -> [Int] -- ^ Resulting stream figLin be q x = f <$> [0..q] where f c = draw be (x * c) q -- | Calculate one point from stream of integer values. draw :: (Int, Int) -- ^ Beginning and end values -> Int -- ^ Numerator -> Int -- ^ Denominator -> Int -- ^ Value of this point draw (b, e) n d = b + (n * (e - b)) `gdiv` (127 * d) where x `gdiv` y = round (fromIntegral x / fromIntegral y :: Double) -- | Collection of “top-level” definitions. topDefs :: [String] topDefs = [ x ++ show n | x <- [defDur,defVel,defPch,defMod,defBth,defAft,defBnd] , n <- [0..mvIndex] ] -- | Maximal voice index. @15@ means that we can have 16 voices total. mvIndex :: Natural mvIndex = 15 defDur :: String defDur = "dur" defVel :: String defVel = "vel" defPch :: String defPch = "pch" defMod :: String defMod = "mod" defBth :: String defBth = "bth" defAft :: String defAft = "aft" defBnd :: String defBnd = "bnd"