packages feed

hmt-0.20: Music/Theory/Tuning/Scala/Cli.hs

-- | Command line interface to hmt/scala.
module Music.Theory.Tuning.Scala.Cli where

import Data.Char {- base -}
import Data.List {- base -}
import System.Environment {- base -}
import Text.Printf {- base -}

import qualified Music.Theory.Array.Text as T {- hmt-base -}
import qualified Music.Theory.Function as T {- hmt-base -}
import qualified Music.Theory.List as T {- hmt-base -}
import qualified Music.Theory.Read as T {- hmt-base -}
import qualified Music.Theory.Show as T {- hmt-base -}

import qualified Music.Theory.Array.Csv.Midi.Mnd as T {- hmt -}
import qualified Music.Theory.Pitch as T {- hmt -}
import qualified Music.Theory.Pitch.Spelling.Table as T {- hmt -}
import qualified Music.Theory.Time.Seq as T {- hmt -}
import qualified Music.Theory.Tuning as T {- hmt -}
import qualified Music.Theory.Tuning.Et as T {- hmt -}
import qualified Music.Theory.Tuning.Midi as T {- hmt -}
import qualified Music.Theory.Tuning.Scala as Scala {- hmt -}
import qualified Music.Theory.Tuning.Scala.Kbm as Kbm {- hmt -}
import qualified Music.Theory.Tuning.Scala.Functions as Functions {- hmt -}
import qualified Music.Theory.Tuning.Scala.Interval as Interval {- hmt -}
import qualified Music.Theory.Tuning.Scala.Mode as Mode {- hmt -}
import qualified Music.Theory.Tuning.Type as T {- hmt -}

type R = Double

db_stat :: IO ()
db_stat = do
  db <- Scala.scl_load_db
  let po = filter (== Just (Right 2)) (map Scala.scale_octave db)
      uf = filter Scala.is_scale_uniform db
      r = ["# entries        : " ++ show (length db)
          ,"# perfect-octave : " ++ show (length po)
          ,"# scale-uniform  : " ++ show (length uf)]
  putStrLn (unlines r)

-- > db_summarise (Just 15) (Just 65)
db_summarise :: Maybe Int -> Maybe Int -> IO ()
db_summarise nm_lim dsc_lim = do
  db <- Scala.scl_load_db
  let nm_seq = map Scala.scale_name db
      nm_max = maybe (maximum (map length nm_seq)) id nm_lim
      dsc_seq = map Scala.scale_description db
      fmt (nm,dsc) = printf "%-*s : %s" nm_max (take nm_max nm) (maybe dsc (flip take dsc) dsc_lim)
      tbl = map fmt (zip nm_seq dsc_seq)
  putStrLn (unlines tbl)

env :: IO ()
env = do
  scl_dir <- Scala.scl_get_dir
  dist_dir <- getEnv "SCALA_DIST_DIR"
  putStrLn ("SCALA_SCL_DIR = " ++ if null scl_dir then "NOT SET" else intercalate ":" scl_dir)
  putStrLn ("SCALA_DIST_DIR = " ++ if null dist_dir then "NOT SET" else dist_dir)

cut :: Maybe Int -> [a] -> [a]
cut lm s = maybe s (\n -> take n s) lm

search :: (IO [a], a -> String, a -> [String]) -> (Bool, Maybe Int) -> [String] -> IO ()
search (load_f,descr_f,stat_f) (ci,lm) txt = do
  db <- load_f
  let modify = if ci then map toLower else id
      txt' = map modify txt
      db' = filter (T.predicate_all (map isInfixOf txt') . modify . descr_f) db
  mapM_ (putStrLn . unlines . map (cut lm) . stat_f) db'

-- > search_scale (True,Nothing) ["xenakis"]
-- > search_scale (True,Just 75) ["lamonte","young"]
search_scale :: (Bool,Maybe Int) -> [String] -> IO ()
search_scale = search (Scala.scl_load_db,Scala.scale_description,Scala.scale_stat)

-- > search_mode (True,Nothing) ["xenakis"]
search_mode :: (Bool,Maybe Int) -> [String] -> IO ()
search_mode = search (fmap Mode.modenam_modes Mode.load_modenam,Mode.mode_description,Mode.mode_stat)

-- > stat_all Nothing
stat_all :: Maybe Int -> IO ()
stat_all character_limit = do
  db <- Scala.scl_load_db
  mapM_ (putStrLn . unlines . map (cut character_limit) . Scala.scale_stat) db

-- > stat_by_name Nothing "young-lm_piano"
stat_by_name :: Maybe Int -> FilePath -> IO ()
stat_by_name lm nm = do
  sc <- Scala.scl_load nm
  putStrLn (unlines (map (cut lm) (Scala.scale_stat sc)))

-- > rng_enum (60,72) == [60 .. 72]
rng_enum :: Enum t => (t,t) -> [t]
rng_enum (l,r) = [l .. r]

cps_tbl :: String -> T.Mnn_Cps_Table -> (T.Midi,T.Midi) -> IO ()
cps_tbl fmt tbl mnn_rng = do
  let cps_pp = T.double_pp 2
      cents_pp = T.double_pp 1
      gen_t i = (i,T.midi_to_pitch_ks i,T.lookup_err i tbl)
      t_pp (i,p,cps) =
          let ref = T.midi_to_cps i
              (_,nr,nr_cps,_,_) = T.nearest_12et_tone_k0 (69,440) cps
          in [show i
             ,cps_pp cps,T.pitch_pp_iso nr,cents_pp (T.cps_difference_cents nr_cps cps)
             ,cps_pp ref,T.pitch_pp_iso p,cents_pp (T.cps_difference_cents ref cps)]
      hdr = ["MNN"
            ,"CPS","ET12","CENTS-/+"
            ,"REF CPS","REF ET12","CENTS-/+"]
      dat = map (t_pp . gen_t) (rng_enum mnn_rng)
      ln = case fmt of
             "md" -> T.table_pp T.table_opt_simple (hdr : dat)
             "csv" -> map (intercalate ",") dat
             _ -> error "cps_tbl: fmt?"
  putStr (unlines ln)

-- > cps_tbl_d12 "md" ("young-lm_piano",-74.7,-3) (60,72)
cps_tbl_d12 :: String -> (String,T.Cents,T.Midi) -> (T.Midi,T.Midi) -> IO ()
cps_tbl_d12 fmt (nm,c,k) mnn_rng = do
  t <- Scala.scl_load_tuning nm :: IO T.Tuning
  let tbl = T.gen_cps_tuning_tbl (T.lift_tuning_f (T.d12_midi_tuning_f (t,c,k)))
  cps_tbl fmt tbl mnn_rng

-- > cps_tbl_cps "md" ("cet111",27.5,9,127-9) (69,69+25)
cps_tbl_cps :: String -> (String,R,T.Midi,Int) -> (T.Midi,T.Midi) -> IO ()
cps_tbl_cps fmt (nm,f0,k,n) mnn_rng = do
  t <- Scala.scl_load_tuning nm
  let tbl = T.gen_cps_tuning_tbl (T.cps_midi_tuning_f (t,f0,k,n))
  cps_tbl fmt tbl mnn_rng

csv_mnd_retune_d12 :: (String,T.Cents,T.Midi) -> FilePath -> FilePath -> IO ()
csv_mnd_retune_d12 (nm,c,k) in_fn out_fn = do
  t <- Scala.scl_load_tuning nm
  let retune_f = T.midi_detune_to_fmidi . T.d12_midi_tuning_f (t,c,k)
  m <- T.csv_midi_read_wseq in_fn :: IO (T.Wseq R (R,R,T.Channel,T.Param))
  let f (tm,(mnn,vel,ch,pm)) = (tm,(retune_f (floor mnn),vel,ch,pm))
  T.csv_mndd_write_wseq 4 out_fn (map f m)

-- > fluidsynth_tuning_d12 ("young-lm_piano",0,0) ("young-lm_piano",-74.7,-3)
fluidsynth_tuning_d12 :: (String,Int,Int) -> (String,T.Cents,T.Midi) -> IO ()
fluidsynth_tuning_d12 (fs_name,fs_bank,fs_prog) (nm,c,k) = do
  t <- Scala.scl_load_tuning nm :: IO T.Tuning
  let tun_f = T.d12_midi_tuning_f (t,c,k)
      pp_f n = let (mnn,dt) = tun_f n
                   cents = fromIntegral mnn * 100 + dt
                   cents_non_neg = if cents < 0 then 0 else cents
               in printf "tune %d %d %d %.2f" fs_bank fs_prog n cents_non_neg
      l = printf "tuning \"%s\" %d %d" fs_name fs_bank fs_prog : map pp_f [0 .. 127]
  putStrLn (unlines l)

{-
import Data.Int {- base -}
import Data.Word {- base -}

int_to_int8 :: Int -> Int8
int_to_int8 = fromIntegral

int8_to_word8 :: Int8 -> Word8
int8_to_word8 = fromIntegral

midi_tbl_binary_mnn_cents_tuning_d12 :: FilePath -> (String,T.Cents,Int) -> IO ()
midi_tbl_binary_mnn_cents_tuning_d12 fn (nm,c,k) = do
  t <- Scala.scl_load_tuning nm :: IO T.Tuning
  let tun_f = T.d12_midi_tuning_f (t,c,k)
      pp_f n = let (mnn,dt) = T.midi_detune_normalise (tun_f n)
               in [int_to_int8 mnn,int_to_int8 (round dt)]
  B.writeFile fn (B.pack (map int8_to_word8 (concatMap pp_f [0 .. 127])))
-}

{-
> midi_tbl_tuning_d12 "freq" ("meanquar",0,0)
> midi_tbl_tuning_d12 "fmidi" ("meanquar",0,0)
> midi_tbl_tuning_d12 "mts" ("young-lm_piano",-74.7,-3)
-}
midi_tbl_tuning_d12 :: String -> (String,T.Cents,T.Midi) -> IO ()
midi_tbl_tuning_d12 typ (nm,c,k) = do
  t <- Scala.scl_load_tuning nm :: IO T.Tuning
  let tun_f = T.d12_midi_tuning_f (t,c,k)
      pp_f n =
        case typ of
          "fmidi" -> printf "%3d,%10.6f" n (T.midi_detune_to_fmidi (tun_f n))
          "freq" -> printf "%3d,%10.4f" n (T.midi_detune_to_cps (tun_f n))
          "mts" ->
            let (mnn,dt) = T.midi_detune_normalise_positive (tun_f n)
            in printf "%3d,%3d,%7.4f" n (mnn `mod` 0x80) dt
          _ -> error "midi_tbl_tuning_d12"
  putStr (unlines (map pp_f [0 .. 127]))

ratio_cents_pp :: Rational -> String
ratio_cents_pp = show . (round :: Double -> Int) . T.ratio_to_cents

-- > intnam_lookup [7/4,7/6,9/8,13/8]
intnam_lookup :: [Rational] -> IO ()
intnam_lookup r_sq = do
  let f db r = let nm = maybe "*Unknown*" snd (Interval.intnam_search_ratio db r)
               in concat [T.ratio_pp r," = ",nm," = ",ratio_cents_pp r]
  db <- Interval.load_intnam
  mapM_ (putStrLn . f db) r_sq

-- > intnam_search "didymus"
intnam_search :: String -> IO ()
intnam_search txt = do
  db <- Interval.load_intnam
  let f (r,nm) = concat [T.ratio_pp r," = ",nm," = ",ratio_cents_pp r]
  mapM_ (putStrLn . f) (Interval.intnam_search_description_ci db txt)

kbm_tbl :: String -> String -> String -> IO ()
kbm_tbl ty scl_nm kbm_nm = do
  scl <- Scala.scl_load scl_nm
  kbm <- Kbm.kbm_load kbm_nm
  let tbl = case ty of
        "cps" -> Kbm.kbm_cps_tbl kbm scl
        "fmidi" -> Kbm.kbm_fmidi_tbl kbm scl
        _ -> error "kbm_tbl: unknown type"
      fmt (i,j) = printf "%d,%.4f" i j
      txt = unlines (map fmt tbl)
  putStrLn txt

-- * Main

help :: [String]
help =
    ["cps-tbl md|csv cps name:string f0:real mnn0:int gamut:int mnn-l:int mnn-r:int"
    ,"cps-tbl md|csv d12 name:string cents:real mnn:int mnn-l:int mnn-r:int"
    ,"csv-mnd-retune d12 name:string cents:real mnn:int input-file output-file"
    ,"db stat"
    ,"db summarise nm-lm|nil dsc-lm|nil"
    ,"env"
    ,"fluidsynth d12 scl-name:string cents:real mnn:int fs-name:string fs-bank:int fs-prog:int"
    ,"intervals {half-matrix|list|matrix} {cents|ratios} scale-name:string"
    ,"intname lookup interval:rational..."
    ,"intname search text:string"
    ,"kbm table {cps | fmidi} scala-name:string kbm-name:string"
    ,"midi-table fmidi|freq|mts d12 name:string cents:real mnn:int"
    ,"search scale|mode ci|cs lm|nil text:string..."
    ,"stat all lm|nil"
    ,"stat scale lm|nil name:string|file-path"
    ,""
    ,"  lm:int = line character limit"]

nil_or_read :: Read a => String -> Maybe a
nil_or_read s = if s == "nil" then Nothing else Just (T.read_err s)

scala_cli :: [String] -> IO ()
scala_cli arg = do
  let usage = putStrLn (unlines help)
  case arg of
    ["cps-tbl",fmt,"cps",nm,f0,k,n,l,r] -> cps_tbl_cps fmt (nm,read f0,read k,read n) (read l,read r)
    ["cps-tbl",fmt,"d12",nm,c,k,l,r] -> cps_tbl_d12 fmt (nm,read c,read k) (read l,read r)
    ["csv-mnd-retune","d12",nm,c,k,in_fn,out_fn] -> csv_mnd_retune_d12 (nm,read c,read k) in_fn out_fn
    ["db","stat"] -> db_stat
    ["db","summarise",nm_lim,dsc_lim] -> db_summarise (nil_or_read nm_lim) (nil_or_read dsc_lim)
    ["env"] -> env
    ["fluidsynth","d12",scl_nm,c,k,fs_nm,fs_bank,fs_prog] ->
        fluidsynth_tuning_d12 (fs_nm,read fs_bank,read fs_prog) (scl_nm,read c,read k)
    ["intervals","half-matrix",'c':_,k,nm] -> Functions.intervals_half_matrix_cents (read k) nm
    ["intervals","half-matrix",'r':_,nm] -> Functions.intervals_half_matrix_ratios nm
    ["intervals","list",'r':_,nm] -> Functions.intervals_list_ratios nm
    ["intervals","matrix",'c':_,k,nm] -> Functions.intervals_matrix_cents (read k) nm
    ["intervals","matrix",'r':_,nm] -> Functions.intervals_matrix_ratios nm
    "intnam":"lookup":r_sq -> intnam_lookup (map T.read_ratio_with_div_err r_sq)
    ["intnam","search",txt] -> intnam_search txt
    ["kbm","table",ty,scl_nm,kbm_nm] -> kbm_tbl ty scl_nm kbm_nm
    ["midi-table",typ,"d12",scl_nm,c,k] -> midi_tbl_tuning_d12 typ (scl_nm,read c,read k)
    "search":ty:ci:lm:txt ->
        case ty of
          "scale" -> search_scale (ci == "ci",nil_or_read lm) txt
          "mode" -> search_mode (ci == "ci",nil_or_read lm) txt
          _ -> usage
    ["stat","all",lm] -> stat_all (nil_or_read lm)
    ["stat","scale",lm,nm] -> stat_by_name (nil_or_read lm) nm
    _ -> usage