{- | Scala "keyboard mapping" files (.kbm) and related data structure. -} module Music.Theory.Tuning.Scala.Kbm where import Data.List {- base -} import Data.Maybe {- base -} import System.FilePath {- filepath -} import Text.Printf {- base -} import qualified Music.Theory.Directory as Directory {- hmt -} import qualified Music.Theory.List as List {- hmt -} import qualified Music.Theory.Pitch as Pitch {- hmt -} import qualified Music.Theory.Tuning as Tuning {- hmt -} import qualified Music.Theory.Tuning.Scala as Scala {- hmt -} {- | Scala keyboard mapping (sz,(m0,mN),mC,(mF,f),o,m) - sz = size of map, the pattern repeats every so many keys - (m0,mN) = the first and last midi note numbers to retune - mC = the middle note where the first entry of the mapping is mapped to - (mF,f) = the reference midi-note for which a frequency is given, ie. (69,440) - o = scale degree to consider as formal octave - m = mapping, numbers represent scale degrees mapped to keys, Nothing indicates no mapping -} type Kbm = (Int,(Int,Int),Int,(Int,Double),Int,[Maybe Int]) -- | Pretty-printer for scala .kbm file. kbm_pp :: Kbm -> String kbm_pp (sz,(m0,mN),mC,(mF,f),o,m) = unlines [printf "size = %d" sz ,printf "note-range = (%d,%d)" m0 mN ,printf "note-center = %d" mC ,printf "note-reference = (%d,%f)" mF f ,printf "formal-octave = %d" o ,printf "map = [%s] #%d" (intercalate "," (map (maybe "x" show) m)) (length m)] -- | Is /mnn/ in range? kbm_in_rng :: Kbm -> Int -> Bool kbm_in_rng (_,(m0,mN),_,_,_,_) mnn = mnn >= m0 && mnn <= mN -- | Is /kbm/ linear?, ie. is size zero? (formal-octave may or may not be zero) kbm_is_linear :: Kbm -> Bool kbm_is_linear (sz,_,_,_,_o,_) = sz == 0 -- && o == 0 {- | Given kbm and midi-note-number lookup (octave,scale-degree). > k <- kbm_load_dist "example.kbm" -- 12-tone scale > k <- kbm_load_dist "a440.kbm" -- linear > k <- kbm_load_dist "white.kbm" -- 7-tone scale on white notes > k <- kbm_load_dist "black.kbm" -- 5-tone scale on black notes > k <- kbm_load_dist "128.kbm" > map (kbm_lookup k) [48 .. 72] -} kbm_lookup :: Kbm -> Int -> Maybe (Int,Int) kbm_lookup kbm mnn = if not (kbm_in_rng kbm mnn) then Nothing else if kbm_is_linear kbm then Just (0,mnn) else let (sz,(_m0,_mN),mC,(_mF,_f),_o,m) = kbm (oct,ix) = ((mnn - mC) `divMod` sz) in fmap (\dgr -> (oct,dgr)) (m !! ix) -- | Return the triple (mF,kbm_lookup k mF,f). The lookup for mF is not-nil by definition. -- -- > kbm_lookup_mF k kbm_lookup_mF :: Kbm -> (Int,(Int,Int),Double) kbm_lookup_mF k@(_,_,_,(mF,f),_,_) = case kbm_lookup k mF of Nothing -> error "kbm_lookup_mF?" Just r -> (mF,r,f) -- | Parser for scala .kbm file. kbm_parse :: String -> Kbm kbm_parse s = let f x = case x of "x" -> Nothing _ -> Just (read x) to_m sz = List.pad_right_no_truncate Nothing sz . map f -- _err -- some scala .kbm have |m| > sz? in case Scala.filter_comments (lines s) of i1:i2:i3:i4:i5:d1:i6:m -> let sz = read i1 in (sz,(read i2,read i3),read i4,(read i5,read d1),read i6,to_m sz m) _ -> error "kbm_parse?" -- | 'kbm_parse' of 'readFile' kbm_load_file :: FilePath -> IO Kbm kbm_load_file = fmap kbm_parse . readFile {- | 'kbm_parse' of 'Scala.load_dist_file' > pp nm = kbm_load_dist nm >>= \x -> putStrLn (kbm_pp x) > pp "example" > pp "bp" > pp "7" -- error -- 12/#13 > pp "8" -- error -- 12/#13 > pp "white" -- error -- 12/#13 > pp "black" -- error -- 12/#13 > pp "128" > pp "a440" > pp "61" -} kbm_load_dist :: String -> IO Kbm kbm_load_dist nm = fmap kbm_parse (Scala.load_dist_file (nm <.> "kbm")) -- | If /nm/ is a file name (has a .kbm) extension run 'kbm_load_file' else run 'kbm_load_dist'. kbm_load :: String -> IO Kbm kbm_load nm = if hasExtension nm then kbm_load_file nm else kbm_load_dist nm -- | Load all .kbm files at directory. kbm_load_dir_fn :: FilePath -> IO [(FilePath, Kbm)] kbm_load_dir_fn d = do fn <- Directory.dir_subset [".kbm"] d kbm <- mapM kbm_load fn return (zip fn kbm) {- | Load all .kbm files at scala dist dir. > db <- kbm_load_dist_dir_fn > length db == 41 > x = map (\(fn,(sz,_,_,_,o,m)) -> (System.FilePath.takeFileName fn,sz,length m,o)) db > filter (\(_,i,j,_) -> i < j) x -- size < map-length > filter (\(_,i,_,k) -> i == 0 && k == 0) x -- size and formal octave both zero > map (\(fn,k) -> (System.FilePath.takeFileName fn,kbm_lookup_mF k)) db -} kbm_load_dist_dir_fn :: IO [(FilePath, Kbm)] kbm_load_dist_dir_fn = Scala.dist_get_dir >>= kbm_load_dir_fn {- | Pretty-printer for scala .kbm file. > m <- kbm_load_dist "7.kbm" > kbm_parse (kbm_format m) == m > putStrLn $ kbm_pp m -} kbm_format :: Kbm -> String kbm_format (i1,(i2,i3),i4,(i5,d1),i6,m) = let from_m = map (maybe "x" show) in unlines ([show i1,show i2,show i3,show i4,show i5,show d1,show i6] ++ from_m m) -- | 'writeFile' of 'kbm_format' kbm_wr :: FilePath -> Kbm -> IO () kbm_wr fn = writeFile fn . kbm_format {- | Standard 12-tone mapping with A=440hz (ie. example.kbm) > fmap (== kbm_d12_a440) (kbm_load_dist "example.kbm") > putStrLn $ kbm_pp kbm_d12_a440 -} kbm_d12_a440 :: Kbm kbm_d12_a440 = (12,(0,127),60,(69,440.0),12,map Just [0 .. 11]) kbm_d12_c256 :: Kbm kbm_d12_c256 = (12,(0,127),60,(60,256.0),12,map Just [0 .. 11]) -- | Given size and note-center calculate relative octave and key -- number (not scale degree) of the zero entry. -- -- > map (kbm_k0 12) [59,60,61] == [(-4,1),(-5,0),(-5,11)] kbm_k0 :: Int -> Int -> (Int,Int) kbm_k0 sz mC = let (o,r) = mC `quotRem` sz in (negate o,negate r `mod` sz) -- | Given size and note-center calculate complete octave and key -- number sequence (ie. for entries 0 - 127). -- -- > map (zip [0..] . kbm_oct_key_seq 12) [59,60,61] kbm_oct_key_seq :: Kbm -> [(Int,(Int,Int))] kbm_oct_key_seq (sz,(m0,mN),mC,(_mF,_f),_o,_m) = let (o0,k0) = kbm_k0 sz mC dgr = map (`mod` sz) (take 128 [k0 ..]) upd o j = if j == 0 then (o + 1,(o + 1,j)) else (o,(o,j)) key_seq = snd (mapAccumL upd (o0 - 1) dgr) in zip [m0 .. ] (take (mN - m0 + 1) (drop m0 key_seq)) -- | Given Kbm and SCL calculate frequency of note-center. kbm_mC_freq :: Kbm -> Scala.Scale -> Double kbm_mC_freq (sz,(_m0,_mN),mC,(mF,f),_o,m) scl = let dist_k = (mF - mC) `mod` sz dgr = fromMaybe (error "kbm_mC_freq") (m !! dist_k) c = Scala.scale_cents scl !! dgr in Tuning.cps_shift_cents f (- c) -- | Given Kbm and SCL calculate fractional midi note-numbers for each key. kbm_fmidi_tbl :: Kbm -> Scala.Scale -> [(Int, Double)] kbm_fmidi_tbl kbm scl = let (_sz,(_m0,_mN),_mC,(_mF,_f),o,m) = kbm mC_freq = kbm_mC_freq kbm scl mC_fmidi = Pitch.cps_to_fmidi mC_freq key_seq = kbm_oct_key_seq kbm c = Scala.scale_cents scl oct_cents = c !! o oct_key_to_cents (oct,key) = maybe 0 (c !!) (m !! key) + (fromIntegral oct * oct_cents) in map (\(mnn,oct_key) -> (mnn,mC_fmidi + (oct_key_to_cents oct_key / 100.0))) key_seq -- | Given Kbm and SCL calculate frequencies for each key. kbm_cps_tbl :: Kbm -> Scala.Scale -> [(Int, Double)] kbm_cps_tbl kbm = let f (k,n) = (k,Tuning.fmidi_to_cps n) in map f . kbm_fmidi_tbl kbm {- scl <- Scala.scl_load "young-lm_piano" scl <- Scala.scl_load "meanquar" scl <- Scala.scl_load "et12" kbm <- kbm_load "example" -- d12_a440 -- kbm_d12_a440 kbm_d12_c256 kbm_fmidi_tbl kbm scl kbm_cps_tbl kbm scl -}