HarmTrace 2.0 → 2.1
raw patch · 38 files changed
+1378/−1729 lines, 38 filesdep +HCodecsdep +HarmTrace-Basedep +ghc-primdep ~basedep ~binarydep ~cmdargssetup-changed
Dependencies added: HCodecs, HarmTrace-Base, ghc-prim
Dependency ranges changed: base, binary, cmdargs, instant-generics, sox, template-haskell, uu-parsinglib
Files
- HarmTrace.cabal +23/−34
- Setup.hs +1/−1
- src/Constants.hs +30/−10
- src/HarmTrace/Audio/Annotate.hs +139/−106
- src/HarmTrace/Audio/AnnotationParser.hs +18/−41
- src/HarmTrace/Audio/ChordTypes.hs +0/−297
- src/HarmTrace/Audio/ChromaChord.hs +95/−99
- src/HarmTrace/Audio/ChromaKey.hs +31/−18
- src/HarmTrace/Audio/DataParser.hs +9/−7
- src/HarmTrace/Audio/Evaluation.hs +163/−56
- src/HarmTrace/Audio/Key.hs +39/−14
- src/HarmTrace/Audio/Statistical.hs +1/−1
- src/HarmTrace/Base/MusicRep.hs +0/−265
- src/HarmTrace/Base/Parsing.hs +0/−47
- src/HarmTrace/Base/TypeLevel.hs +0/−81
- src/HarmTrace/HAnTree/Binary.hs +0/−59
- src/HarmTrace/HAnTree/HAn.hs +15/−23
- src/HarmTrace/HAnTree/PostProcess.hs +2/−2
- src/HarmTrace/HarmTrace.hs +5/−5
- src/HarmTrace/IO/BasePaths.hs +22/−14
- src/HarmTrace/IO/Common.hs +0/−1
- src/HarmTrace/IO/FeatExtract.hs +39/−22
- src/HarmTrace/IO/Parse.hs +84/−5
- src/HarmTrace/IO/Recognise.hs +325/−221
- src/HarmTrace/IO/Wave.hs +48/−0
- src/HarmTrace/Matching/HChord.hs +3/−2
- src/HarmTrace/Matching/Sim.hs +1/−1
- src/HarmTrace/Matching/SimpleChord.hs +7/−4
- src/HarmTrace/Models/ChordTokens.hs +77/−0
- src/HarmTrace/Models/Jazz/Instances.hs +13/−8
- src/HarmTrace/Models/Jazz/Model.hs +2/−2
- src/HarmTrace/Models/Parser.hs +8/−2
- src/HarmTrace/Models/Pop/Instances.hs +13/−15
- src/HarmTrace/Models/Pop/Model.hs +2/−2
- src/HarmTrace/Models/TypeLevel.hs +81/−0
- src/HarmTrace/Tokenizer/Tokenizer.hs +0/−155
- src/HarmTrace/Tokenizer/Tokens.hs +0/−65
- src/Main.hs +82/−44
HarmTrace.cabal view
@@ -1,5 +1,5 @@ name: HarmTrace -version: 2.0 +version: 2.1 synopsis: Harmony Analysis and Retrieval of Music description: HarmTrace: Harmony Analysis and Retrieval of Music with Type-level Representations of Abstract @@ -15,8 +15,8 @@ 2011. <http://dreixel.net/research/pdf/fmmh.pdf> -copyright: (c) 2010--2012 Universiteit Utrecht, - 2012 University of Oxford +copyright: (c) 2010--2013 Universiteit Utrecht, + 2012--2013 University of Oxford license: GPL-3 license-file: LICENSE author: W. Bas de Haas and Jose Pedro Magalhaes @@ -26,7 +26,7 @@ category: Music build-type: Custom cabal-version: >= 1.6 -tested-with: GHC == 7.0.3, GHC == 7.2.1, GHC == 7.4.1 +tested-with: GHC == 7.0.3, GHC == 7.2.1, GHC == 7.4.1, GHC == 7.6.1 extra-source-files: README @@ -36,7 +36,6 @@ HarmTrace.HarmTrace - HarmTrace.Audio.ChordTypes HarmTrace.Audio.Annotate HarmTrace.Audio.AnnotationParser HarmTrace.Audio.DataParser @@ -46,11 +45,6 @@ HarmTrace.Audio.Evaluation HarmTrace.Audio.Statistical - HarmTrace.Base.MusicRep - HarmTrace.Base.Parsing - HarmTrace.Base.TypeLevel - - HarmTrace.HAnTree.Binary HarmTrace.HAnTree.HAn HarmTrace.HAnTree.HAnParser HarmTrace.HAnTree.PostProcess @@ -67,6 +61,7 @@ HarmTrace.IO.FeatExtract HarmTrace.IO.Downsample HarmTrace.IO.BasePaths + HarmTrace.IO.Wave HarmTrace.Matching.Alignment HarmTrace.Matching.HChord @@ -77,6 +72,8 @@ HarmTrace.Models.Models HarmTrace.Models.Parser + HarmTrace.Models.ChordTokens + HarmTrace.Models.TypeLevel HarmTrace.Models.Jazz.Instances HarmTrace.Models.Jazz.Main @@ -86,17 +83,15 @@ HarmTrace.Models.Pop.Main HarmTrace.Models.Pop.Model - HarmTrace.Tokenizer.Tokenizer - HarmTrace.Tokenizer.Tokens - main-is: Main.hs - build-depends: base >= 4.2 && < 4.6, template-haskell >=2.4 && <2.8, + build-depends: base >= 4.2 && < 4.7, template-haskell >=2.4 && <2.9, mtl, directory, filepath, array, parallel >= 3, - Diff == 0.1.*, cmdargs >= 0.9 && < 0.10, process >= 1.0, - uu-parsinglib == 2.7.1, ListLike >= 3.0.1, - vector >= 0.7, deepseq, sox >= 0.2.2.1, - instant-generics >= 0.3.1 && < 0.4, binary == 0.5.*, - hmatrix >= 0.11.0.3, hmatrix-gsl-stats >= 0.1.2.9 + Diff == 0.1.*, cmdargs >= 0.10.1, process >= 1.0, + uu-parsinglib == 2.7.4.*, ListLike >= 3.0.1, + vector >= 0.7, deepseq, sox >= 0.2.2.2, + instant-generics >= 0.3.6 && < 0.4, binary >= 0.6.4, + hmatrix >= 0.11.0.3, hmatrix-gsl-stats >= 0.1.2.9, + HarmTrace-Base >= 1.0.0.2, ghc-prim >= 0.2 ghc-options: -Wall -O2 -fno-spec-constr-count -funbox-strict-fields @@ -109,13 +104,14 @@ library -- Note the extreme duplication of information -- I don't think there's much we can do about that - build-depends: base >= 4.2 && < 4.6, template-haskell >=2.4 && <2.8, + build-depends: base >= 4.2 && < 4.7, template-haskell >=2.4 && <2.9, mtl, directory, filepath, array, parallel >= 3, Diff == 0.1.*, parseargs >= 0.1.3.2, process >= 1.0, - uu-parsinglib == 2.7.1, ListLike >= 3.0.1, - vector >= 0.7, deepseq, - instant-generics >= 0.3.1 && < 0.4, binary == 0.5.*, - hmatrix >= 0.11.0.3, hmatrix-gsl-stats >= 0.1.2.9 + uu-parsinglib == 2.7.4.*, ListLike >= 3.0.1, + vector >= 0.7, deepseq, HCodecs >= 0.2.2, + instant-generics >= 0.3.6 && < 0.4, binary >= 0.6.4, + hmatrix >= 0.11.0.3, hmatrix-gsl-stats >= 0.1.2.9, + HarmTrace-Base >= 1.0.0.2, ghc-prim >= 0.2 ghc-options: -Wall -O2 -fno-spec-constr-count -funbox-strict-fields @@ -126,8 +122,7 @@ hs-source-dirs: src - exposed-modules: HarmTrace.Audio.ChordTypes - HarmTrace.Audio.Annotate + exposed-modules: HarmTrace.Audio.Annotate HarmTrace.Audio.AnnotationParser HarmTrace.Audio.DataParser HarmTrace.Audio.ChromaChord @@ -136,11 +131,6 @@ HarmTrace.Audio.Evaluation HarmTrace.Audio.Statistical - HarmTrace.Base.MusicRep - HarmTrace.Base.Parsing - HarmTrace.Base.TypeLevel - - HarmTrace.HAnTree.Binary HarmTrace.HAnTree.HAn HarmTrace.HAnTree.HAnParser HarmTrace.HAnTree.PostProcess @@ -156,6 +146,8 @@ HarmTrace.Models.Models HarmTrace.Models.Parser + HarmTrace.Models.ChordTokens + HarmTrace.Models.TypeLevel HarmTrace.Models.Jazz.Instances HarmTrace.Models.Jazz.Main @@ -164,6 +156,3 @@ HarmTrace.Models.Pop.Instances HarmTrace.Models.Pop.Main HarmTrace.Models.Pop.Model - - HarmTrace.Tokenizer.Tokenizer - HarmTrace.Tokenizer.Tokens
Setup.hs view
@@ -9,7 +9,7 @@ , preBuild = preBuild' } -- Runs the testsuite -runTests' _ _ _ _ = system "shelltest tests -c -d" >> return () +runTests' _ _ _ _ = system "shelltest tests -c -d -- --threads=4" >> return () -- Update the VERSION string before building --preBuild' :: Args -> BuildFlags -> IO HookedBuildInfo
src/Constants.hs view
@@ -18,11 +18,12 @@ , displaySampleRate, outputSampleRate, outputBitsPerSample , outputNrOfChannels, keyStr, chromaStr, beatStr , beatVampPath, chromaVampPath, keyVampPath - , deleteDownsampledWav - , defaultVampDir, defaultFeatDir , defaultOutDir - , modulationPenalty, minModulationLength ) where + , deleteDownsampledWav, downSample, defaultLogDir + , defaultVampDir, defaultFeatDir , defaultOutDir, logFileSuffix + , sonicAnnotator, modulationPenalty, minModulationLength + , ChordPrintOpts (..), defaultChordPrinting ) where -import HarmTrace.Audio.ChordTypes (NumData) +import HarmTrace.Base.MusicTime (NumData) -------------------------------------------------------------------------------- -- Constants @@ -33,8 +34,7 @@ vERSION = gitVersion ++ " (master)" gitVersion :: String -gitVersion = "HarmTrace-2.0" - +gitVersion = "HarmTrace-2.0-145-g3a005f7" -------------------------------------------------------------------------------- -- Chord Transcription Parameters @@ -43,7 +43,7 @@ -- | The cutOffProbability is the value that determines the length of the -- probChord list (a sorted list with normalised distances to the chroma vector) cutOffProbability :: NumData -cutOffProbability = 0.90 +cutOffProbability = 0.85 maxProbChordListLength, maxSegmentSize, maxLProductSize :: Int -- | The maximum number if chords (the lenght) of a chord candidate list @@ -95,9 +95,14 @@ -- | Controls whether the downsampled wav file is deleted after feature -- extraction. -deleteDownsampledWav :: Bool +deleteDownsampledWav, downSample :: Bool deleteDownsampledWav = True +-- | Controls whether SoX is used to downsample the audio or whether the +-- audio file is passed to the sonic annotator directly. If 'downSample is +-- set to False, all paramters downsampling parameters will be ignored +downSample = True + -- | The strings that build up a audio feature file name keyStr, chromaStr, beatStr :: String chromaStr = "_vamp_nnls-chroma_nnls-chroma_bothchroma.csv" @@ -113,11 +118,26 @@ -- | specifying the filename of the key-finding chroma VAMP plugin transform file keyVampPath = "tuned-chroma.txt" -defaultVampDir, defaultOutDir, defaultFeatDir :: FilePath +defaultVampDir, defaultLogDir, defaultOutDir, defaultFeatDir :: FilePath -- | The default base directory is the current directory -defaultVampDir = "" +defaultVampDir = "" defaultOutDir = "" +defaultLogDir = "" defaultFeatDir = "" + +-- | By default we assume the sonic-annotator is in the path +sonicAnnotator, logFileSuffix :: String +sonicAnnotator = "sonic-annotator" +logFileSuffix = "extract.log" + +-- | An ennumerator of determining the output format of the chords +data ChordPrintOpts = PrintMajMin | PrintChordClass + +-- | By default we only display major and minor chords to the user (because +-- the transcription quality of these chords is better than that of the +-- chord class). +defaultChordPrinting :: ChordPrintOpts +defaultChordPrinting = PrintMajMin -------------------------------------------------------------------------------- -- Chroma key estimation
src/HarmTrace/Audio/Annotate.hs view
@@ -17,25 +17,24 @@ -- knowledge (the HarmTrace harmony model) -------------------------------------------------------------------------------- module HarmTrace.Audio.Annotate ( mptreeAnnotator, groupAnnotator - , simpleAnnotator , putSegStats, preProcessData - , preProcessKeyData ) where + , simpleAnnotator, mptreeAnnotatorSTG + , putSegStats, preProcessData + ) where -- parameters import Constants ( maxSegmentSize, maxLProductSize) -- Audio Stuff import HarmTrace.Audio.ChromaChord ( createChordRanks, beatSync - , mergeByBeat, addBeatTimeStamp - , mergeAndTimeStamp) --- import HarmTrace.Audio.ChromaKey ( syncWithAnnKey) + , mergeByOneAndThree ) import HarmTrace.Audio.Key (getBeatSyncKeyFromChroma) -import HarmTrace.Audio.ChordTypes +import HarmTrace.Base.MusicTime -- Harmony Model stuff import HarmTrace.Base.MusicRep import HarmTrace.Models.Models import HarmTrace.Models.Jazz.Main import HarmTrace.Models.Pop.Main -import HarmTrace.Tokenizer.Tokens +import HarmTrace.Models.ChordTokens import HarmTrace.IO.Errors import HarmTrace.HAnTree.HAn (HAn) import HarmTrace.HAnTree.Tree (Tree, size, depth) @@ -46,73 +45,51 @@ import Text.ParserCombinators.UU.BasicInstances import System.IO (stdout,hFlush) -import Data.List (sortBy, groupBy, zipWith4) +import Data.List (sortBy, groupBy, intersperse) +import Control.Arrow (first) import Text.Printf (printf) -------------------------------------------------------------------------------- -- From chords with probabilities to a single chord, using harmony -------------------------------------------------------------------------------- +mptreeAnnotatorSTG :: GrammarEx -> Maybe [TimedData Key] -> AudioFeat + -> ChordAnnotation +mptreeAnnotatorSTG gex k = snapToGrid . mptreeAnnotator gex k + -- | MPTrEE (Model Propelled Transcription of Euphonic Enitities): -- a sophisticated, harmony and beat informed chord annotator mptreeAnnotator :: GrammarEx -> Maybe [TimedData Key] -> AudioFeat - -> ChordBeatAnnotation + -> ChordAnnotation mptreeAnnotator (GrammarEx g) k f = concatMap (harmonize g) (preProcessData k f) -- | preprocesses the raw audio data before using chord harmony model based -- chord selection. First, the beats and chroma are synchronised. Second, --- chord candidate lists are created. Third, a beat time-stamp is added. Fourth, --- smart, beat informed grouping of the chord candidates is performed. Fifth, --- the chord candidate lists are grouped in segments based on the key (obtained +-- chord candidate lists are created. Third, smart, beat informed grouping of +-- the chord candidates is performed. Fourth, the chord candidate lists are +-- grouped in segments based on the key (obtained -- as provided by the user or as derived from the audio data). Last, the -- chord candidate lists are further segmented based on the occurrences of -- I and V chords. preProcessData :: Maybe [TimedData Key] -> AudioFeat -> [ProbChordSeg] -preProcessData gtk (AudioFeat chrm beats afk) = - segmentByTonic $ segmentByKey key . mergeByBeat . addBeatTimeStamp bt - . createChordRanks $ beatSync bt chrm where - - -- if a ground-truth key annotations are provided, we use these - -- annotations otherwise the key is derived from audio data - (bt, key) = case gtk of - Nothing -> preProcessKeyData chrm beats afk - (Just k) -> syncWithAnnKey k - - -- syncronises the last beat and key frame to match the last chord frame - syncWithAnnKey :: [TimedData Key] -> ([BeatBar], [TimedData Key]) - syncWithAnnKey keys = - let endTime = BeatBar (time $ last chrm, Four) - beats' = takeWhile (< endTime) beats ++ [endTime] - -- filter the None keys - none (Key r _) = r /= Note Nothing N - noNoneKey = (filter (none . getData) keys) - -- reset key start timestamp to 0.0 and end timestampt to chorma end - resetHead = setOnset (head noNoneKey) 0.0 : tail noNoneKey - (l,[lst]) = splitAt (length resetHead - 1) resetHead - in (beats',l ++[setOffset lst (timeStamp endTime)]) - --- | preprocesses the audio data for automatic key detection. -preProcessKeyData :: [ChordinoLine] -> [BeatBar] -> [ChordinoLine] - -> ([BeatBar], [TimedData Key]) -preProcessKeyData chrm beats afk = - ( b, dumpBeats . mergeAndTimeStamp head b $ getBeatSyncKeyFromChroma b key ) - where -- synchronise the last beat and key to match the last chord - endTime = time $ last chrm - afk' = takeWhile ((< endTime).time) afk - (l,[lst]) = splitAt (length afk' - 1) afk' - end = BeatBar (endTime, Four) - key = l ++ [lst{time = endTime}] - b = takeWhile (< end) beats ++ [end] +preProcessData gtk af@(AudioFeat chrm beats _afk _id) = + segmentByTonic $ segmentByKey key . mergeByOneAndThree + . createChordRanks $ beatSync beats chrm + where key = maybe (getBeatSyncKeyFromChroma af) id gtk --- reminder: ProbChordSeg = Segment Key [BeatTimedData [ProbChord]] -harmonize :: forall g. (GTree g) => Grammar g -> ProbChordSeg -> ChordBeatAnnotation +-- reminder: ProbChordSeg = Segment Key [TimedData [ProbChord]] +-- | Chord selection based on the harmtrace model +harmonize :: forall g. (GTree g) => Grammar g -> ProbChordSeg + -> ChordAnnotation harmonize g (Segment k cands) = let isExpandable :: Bool isExpandable = length (filter ((>1) . length) (map getData cands)) > 0 - myParse :: [ChordToken] -> (Tree HAn,[ChordToken],[Error Int]) - myParse x = - let -- First, parse the tokens + myParse :: [ProbChord] -> (Tree HAn,[ProbChord],Float) + myParse cs = + let x = map probChord cs + + -- First, parse the tokens res :: ([g],[Error Int]) res = case g of Jazz -> parse_h ((,) <$> pJazz k <*> pEnd) (createStr 0 x) @@ -123,52 +100,111 @@ t = pieceTreeHAn (postProc [ RemovePDPT, MergeDelChords ] pr) u :: forall a. a u = error "harmonize: undefined placeholder evaluated" - -- Return the Tree HAn, the input tokens, and the errors - in (t, x, snd res) - - -- To be improved - evaluateParse :: (Tree HAn,[ChordToken],[Error Int]) - -> (Tree HAn,[ChordToken],Float) - evaluateParse (ts,tokens,errors) = (ts,tokens,errorRatio errors tokens) + -- Return the Tree HAn, the input tokens, and the error-ratio + in (t, cs, errorRatio (snd res) x ) -- Generate, parse, and evaluate all possible sequences of chords - parseResults :: [(Tree HAn,[ChordToken],Float)] - parseResults = [ evaluateParse (myParse l) - | l <- lProduct (map (map probChord . getData) cands) ] + parseResults :: [(Tree HAn,[ProbChord],Float)] + parseResults = [ myParse l + | l <- lProduct (map getData cands) ] -- From all possible parse trees, take the best one - select :: [(Tree HAn,[ChordToken],Float)] -> [ChordToken] + select :: [(Tree HAn,[ProbChord],Float)] -> [ProbChord] select = select1 . head . groupBy (\(_,_,a) (_,_,b) -> a `compare` b == EQ) . sortBy (\(_,_,a) (_,_,b) -> a `compare` b) -- These all have the same error ratio, so we sort them first by tree -- size, then depth, and pick the first - select1 :: [(Tree HAn,[ChordToken],Float)] -> [ChordToken] + select1 :: [(Tree HAn,[ProbChord],Float)] -> [ProbChord] select1 = snd3 . head . sortBy cmp where cmp (a,_,_) (b,_,_) = (size a, depth a) `compare` (size b, depth b) - snd3 (_,a,_) = a + snd3 (_,s,_) = s probChord :: ProbChord -> ChordToken - probChord (ProbChord lab@(Chord r sh _add _on _dur) _p) = + probChord (ProbChord lab@(Chord r _sh _add _on _dur) _p) = (ChordToken r' sh' [lab] NotParsed 1 0) where r' = if isNone r then Note Nothing Imp else toScaleDegree k r - sh' = if sh == None then NoClass else toClassType sh + sh' = toClassType lab - -- replaces the candidate list by the selected chord in a - -- Timed datatype (either TimedData or BeatTimedData) - -- setBestChords :: Timed t => [ChordToken] -> [t ChordLabel] - setBestChords :: [ChordToken] -> [BeatTimedData ChordLabel] - setBestChords = zipWith setData cands . map (head . chords) + -- replaces the candidate list by the selected chord in a TimedData + setBestChords :: [ProbChord] -> [TimedData ProbChord] + setBestChords = zipWith setData cands -- if there is nothing to expand, do not parse in if isExpandable then setBestChords $ select parseResults else map pickHead cands -pickHead :: Timed t => t [ProbChord] -> t ChordLabel -pickHead tpc = setData tpc (chordLab . head $ getData tpc) - +pickHead :: TimedData [ProbChord] -> TimedData ProbChord +pickHead = fmap head + -------------------------------------------------------------------------------- +-- post-processing functions +-------------------------------------------------------------------------------- + +-- snapToGrid :: ChordAnnotation -> ChordAnnotation -- = [TimedData ProbChord] +snapToGrid :: [TimedData ProbChord] -> [TimedData ProbChord] +snapToGrid = foldr snap [] . reduceTimedPChords where + + snap :: TimedData a -> [TimedData a] -> [TimedData a] + snap td [] = [td] + snap a (h : tl) = case ( odd (beatLen a) && odd (beatLen h) , getBeat h ) of + (True, Two ) -> shiftFwd a h ++ tl + (True, Four) -> shiftBwd a h ++ tl + _ -> a : h : tl + + beatLen :: TimedData a -> Int + beatLen = pred . length . getTimeStamps + +-- Shifts the second TimedData backwards in time, lengthing the first TimedData +shiftBwd :: TimedData a -> TimedData a -> [TimedData a] +shiftBwd (TimedData a ta) tdb = case getTimeStamps tdb of + [_on, off] -> [TimedData a (ta ++ [off]) ] + (_hb : hhb : tb) -> [TimedData a (ta ++ [hhb]), tdb{getTimeStamps = (hhb:tb)}] + [_] -> error "HarmTrace.Audio.Annotate.shiftBwd: 1 timestamp, onset == offset" + [ ] -> error "HarmTrace.Audio.Annotate.shiftBwd: No timestamps to shift" + +-- Shifts the second TimedData forwards in time at the cost of the first +shiftFwd :: TimedData a -> TimedData a -> [TimedData a] +shiftFwd tda (TimedData b tb) = case getTimeStamps tda of + [ ] -> error "HarmTrace.Audio.Annotate.shiftFwd: No timestamps to shift" + [_] -> error "HarmTrace.Audio.Annotate.shiftFwd: 1 timestamp, onset == offset" + [on, _off] -> [TimedData b (on : tb) ] + ta -> [tda {getTimeStamps = initTa}, TimedData b (oneButLastTa : tb)] + where + (initTa,oneButLastTa) = snocsnoc ta + + -- takes a list of elements and returns, all elements up to the one-but- + -- last element (discarding the last elem), and the one-but last element + snocsnoc :: [a] -> ([a],a) + snocsnoc [] = error "snocsnoc: empty list" + snocsnoc [_] = error "snocsnoc: singleton list" + snocsnoc [x,_lst] = ([x], x) -- = the one-but-last element + snocsnoc (x:xs) = first (x :) (snocsnoc xs) + +-- | Returns the reduced chord sequences, where repeated chords are merged +-- into one 'ProbChord', wrapped in a 'TimedData' type. +reduceTimedPChords :: [TimedData ProbChord] -> [TimedData ProbChord] +reduceTimedPChords = foldr group [] where + + group :: TimedData ProbChord -> [TimedData ProbChord] -> [TimedData ProbChord] + group c [] = [c] + group tc@(TimedData c tsc ) (th@(TimedData h tsh ) : t) + | c `pChordEq` h = concatTimedData c {prob = avgProb} tc th : t + | otherwise = tc : th : t where + + avgProb :: NumData + avgProb = let ltsc = fromIntegral $ length tsc + ltsh = fromIntegral $ length tsh + tot = ltsc + ltsh + in (prob c * ltsc) + (prob h * ltsh) / tot + + pChordEq :: ProbChord -> ProbChord -> Bool + pChordEq (ProbChord cA _pA) (ProbChord cB _pB) = + chordRoot cA == chordRoot cB && + chordShorthand cA == chordShorthand cB + +-------------------------------------------------------------------------------- -- Segmentation functions -------------------------------------------------------------------------------- -- Temporary test values @@ -184,13 +220,12 @@ -- move to segmentations function in Harmonize? -segmentByKey ::Timed t=>[t Key] -> [BeatTimedData [ProbChord]] -> [ProbChordSeg] +segmentByKey :: [TimedData Key] -> [TimedData [ProbChord]] -> [ProbChordSeg] segmentByKey [] _ = error "segmentByKey: empty key list" segmentByKey [k] chds = [Segment (getData k) chds] segmentByKey (k : ks) chds = let (seg,cs) = span ((<= offset k) . offset) chds in Segment (getData k) seg : segmentByKey ks cs --- Reminder: TimedData [ProbChord] == Block segmentByTonic :: [ProbChordSeg] -> [ProbChordSeg] segmentByTonic segs = concatMap emergencySplit $ concatMap split segs where split :: ProbChordSeg -> [ProbChordSeg] @@ -210,8 +245,7 @@ (l,r) = splitAt (blen `div` 2) b -- Break into segments according to the key --- segmentTonic :: Timed t => Key -> [t [ProbChord]] -> [[t [ProbChord]]] -segmentTonic :: Key -> [BeatTimedData [ProbChord]] -> [[BeatTimedData [ProbChord]]] +segmentTonic :: Key -> [TimedData [ProbChord]] -> [[TimedData [ProbChord]]] segmentTonic k cands = segment cands [] where segment [] [] = [] segment [] acc = [reverse acc] @@ -222,7 +256,7 @@ -- Take the first chord (which is the one with the highest probability, since -- the list is sorted) -getFstChord :: Timed t => t [ProbChord] -> ChordLabel +getFstChord :: TimedData [ProbChord] -> ChordLabel getFstChord c = case getData c of [] -> error "getFstChord: empty list" (h:_) -> chordLab h -- only split on chords we are certain of @@ -230,21 +264,16 @@ -- Check if this chord label is the tonic isTonic :: ChordLabel -> Key -> Bool -isTonic c (Key r m) = r == chordRoot c && m `eqMode` chordShorthand c +isTonic (Chord (Note Nothing N) _ _ _ _) _ = False +isTonic c (Key r m) = r == chordRoot c && m == toMode (toTriad c) -- Check if this chord label is the dominant -- JPM: I don't understand why this function looks so different from `isTonic` isDom :: ChordLabel -> Key -> Bool isDom (Chord (Note Nothing N) _ _ _ _) _ = False isDom c key = toScaleDegree key (chordRoot c) == Note Nothing V - && MajMode `eqMode` chordShorthand c + && toTriad c == MajTriad --- It is debatable how to implement this function, musically speaking. This --- is what I came up with, without thinking too much. -eqMode :: Mode -> Shorthand -> Bool -eqMode _ Sus4 = False -eqMode _ Sus2 = False -eqMode m sh = m == toMode sh lProduct :: [[a]] -> [[a]] lProduct [] = [] @@ -260,19 +289,28 @@ putSegStats k af = mapM_ segmentStat $ preProcessData k af segmentStat :: ProbChordSeg -> IO () -segmentStat s@(Segment k bs) = - do putStr ("start: " ++ (printf "%.3f" . onset $ head bs)) +segmentStat (Segment k bs) = + do putStr ("\nstart: " ++ (printf "%.3f" . onset $ head bs)) putStr (", end: " ++ (printf "%.3f" . offset $ last bs)) putStr (", key: " ++ show k) putStr (", probChords: " ++ show (length bs)) let (l, lpr) = lProdStats bs putStr (", lists > 1: " ++ show l) putStrLn (" lProduct: " ++ show lpr) - print s >> hFlush stdout + (putStrLn . concat . intersperse "\n" . map showTimedData $ bs) + >> hFlush stdout where + showTimedData :: TimedData [ProbChord] -> String + showTimedData td = + (concat . intersperse ", " . map showProbChord . getData $ td) + ++ ": " ++ ( show . getTimeStamps $ td ) + + showProbChord :: ProbChord -> String + showProbChord (ProbChord lab p) = show lab ++ '@' : printf "%.3f" p + -- Given a Block list this function returns the number of probChords with a -- list > 1 (fst) and the lProduct size (snd) -lProdStats :: Timed t => [t [a]] -> (Int, Int) +lProdStats :: [TimedData [a]] -> (Int, Int) lProdStats bs = (length l, lpr) where l = filter ((>1) . length ) (map getData bs) lpr = foldr (\a b -> length a * b) 1 l @@ -284,22 +322,17 @@ -- | Creates an annotation out of a Chord candidate list by just picking the -- first chord. This annotator does smart grouping -- (see 'HarmTrace.Audio.ChromaChord.mergeByBeat'). -groupAnnotator :: GrammarEx -> Maybe [TimedData Key] -> AudioFeat -> ChordBeatAnnotation -groupAnnotator _g _keyAnn (AudioFeat chrm beats _key ) = -- ignore key info - let endTime = BeatBar (time $ last chrm, Four) +groupAnnotator :: GrammarEx -> Maybe [TimedData Key] -> AudioFeat -> ChordAnnotation +groupAnnotator _g _keyAnn (AudioFeat chrm beats _key _id) = -- ignore key info + -- TODO: check is this synchronisation still needed??? + let endTime = BarTime (time $ last chrm) Four beats' = takeWhile (< endTime) beats ++ [endTime] - in map pickHead . mergeByBeat . addBeatTimeStamp beats' . createChordRanks - $ beatSync beats' chrm + in map pickHead . mergeByOneAndThree + . createChordRanks $ beatSync beats' chrm -- | The most simple annotator, no grouping, no matching, -- just pick the best matching chord -simpleAnnotator :: GrammarEx -> Maybe [TimedData Key] -> AudioFeat -> ChordBeatAnnotation -simpleAnnotator _g _keyAnn (AudioFeat crm bts _key ) = -- ignore key - addTimeInfo . map (chordLab . head) . createChordRanks $ beatSync bts crm - where -- wraps a data types into 'BeatTimedData' datatype - addTimeInfo :: [a] -> [BeatTimedData a] - addTimeInfo blcks = zipWith4 BeatTimedData blcks bts' (0 : off) off - - (off,bts') = unzip $ map beatBar bts - +simpleAnnotator :: GrammarEx -> Maybe [TimedData Key] -> AudioFeat -> ChordAnnotation +simpleAnnotator _g _keyAnn (AudioFeat crm bts _key _id) = -- ignore key + map pickHead . createChordRanks $ beatSync bts crm
src/HarmTrace/Audio/AnnotationParser.hs view
@@ -17,72 +17,49 @@ module HarmTrace.Audio.AnnotationParser ( parseAnnotationData , parseKeyAnnotationData - , preProcess) where - -import Data.Maybe (isJust,fromJust) + ) where import HarmTrace.Audio.DataParser (pNumData) -import HarmTrace.Audio.ChordTypes +import HarmTrace.Base.MusicTime import HarmTrace.Base.MusicRep import HarmTrace.Base.Parsing -import HarmTrace.Tokenizer.Tokenizer ( parseDegrees, parseDegree - , parseRoot , parseShorthand) +import HarmTrace.Base.ChordTokenizer ( pRoot, pChord) -- perhaps this file should be moved to the tokeniser module, because it is -- is very related to tokenising -------------------------------------------------------------------------------- --- Harmonically analysing Chord Annotations --------------------------------------------------------------------------------- - --- | Preprocesses a chord annotation for analysing their harmonic structure by --- removing the time stamps and the None chords. -preProcess :: [TimedData ChordLabel] -> [ChordLabel] -preProcess = preProcess' 0 -preProcess' :: Int -> [TimedData ChordLabel] -> [ChordLabel] -preProcess' _ [] = [] -preProcess' ix (TimedData c@(Chord r sh ad _loc dur) _on _off : ns) - | isNoneChord c = preProcess' ix ns - | otherwise = Chord r sh ad ix dur : preProcess' (ix+1) ns - --------------------------------------------------------------------------------- --- Chords +-- Parse MIREX style chord annotations -------------------------------------------------------------------------------- -- | Parses a chord annotation. -parseAnnotationData :: Parser ChordAnnotation -parseAnnotationData = pListSep_ng pLineEnd pChordSegment <* pLineEnd +parseAnnotationData :: Parser [TimedData ChordLabel] +parseAnnotationData = pListSep_ng pLineEnd pChordSegment <* pLineEnd + <* (pLineEnd `opt` "\n") +-- | Parses the onset, offset and chordlabel on one line pChordSegment :: Parser (TimedData ChordLabel) -pChordSegment = timedData' <$> pNumData <* pSpaceTab - <*> pNumData <* pSpaceTab <*> pChord - - -pChord :: Parser ChordLabel -pChord = f <$> parseRoot <*> pMaybe (pSym ':' *> parseShorthand) - <*> (parseDegrees `opt` []) - -- there might be a basenote inversion annotation - -- which we currently ignore - <*> pMaybe (pSym '/' *> parseDegree) where - -- if there is no root note, there is no chord type - -- Chord root shorthand additions start duration - f r sh _ _ | r == Note Nothing N = Chord r None [] 0 1 - | isJust sh = Chord r (fromJust sh) [] 0 1 - | otherwise = Chord r Maj [] 0 1 +pChordSegment = timedData' <$> pNumData <* pSpaceTab + <*> pNumData <* pSpaceTab + <*> pChord -------------------------------------------------------------------------------- -- Keys -------------------------------------------------------------------------------- -- | Parses a 'Key' annotation. parseKeyAnnotationData :: Parser [TimedData Key] -parseKeyAnnotationData = pListSep_ng pLineEnd pKeySegment <* pLineEnd +parseKeyAnnotationData = noNoneKey <$> pListSep_ng pLineEnd pKeySegment + <* pLineEnd where + -- filter the None keys + noNoneKey = filter ((/= Note Nothing N) . keyRoot . getData) + pKeySegment :: Parser (TimedData Key) pKeySegment = timedData' <$> pNumData <* pSpaceTab <*> pNumData <* pSpaceTab <*> (pKey <|> pKeyNone) pKey :: Parser Key -pKey = Key <$ pString "Key" <* pSpaceTab <*> parseRoot <*> pMode +pKey = Key <$ pString "Key" <* pSpaceTab <*> pRoot <*> pMode pKeyNone :: Parser Key pKeyNone = Key (Note Nothing N) MajMode <$ pString "Silence" @@ -102,7 +79,7 @@ -------------------------------------------------------------------------------- timedData' :: NumData -> NumData -> a -> TimedData a -timedData' on off chrd = TimedData chrd on off +timedData' on off chrd = TimedData chrd [Time on, Time off] pSpaceTab :: Parser Char pSpaceTab = pSym ' ' <|> pSym '\t'
− src/HarmTrace/Audio/ChordTypes.hs
@@ -1,297 +0,0 @@-{-# LANGUAGE TypeSynonymInstances #-} -{-# LANGUAGE FlexibleInstances #-} - --------------------------------------------------------------------------------- --- | --- Module : HarmTrace.Audio.ChordTypes --- Copyright : (c) 2010-2012 Universiteit Utrecht, 2012 University of Oxford --- License : GPL3 --- --- Maintainer : bash@cs.uu.nl, jpm@cs.ox.ac.uk --- Stability : experimental --- Portability : non-portable --- --- Summary: A set of types and classes for representing musical chords --- recognised from an arbitrary audio source. --------------------------------------------------------------------------------- - -module HarmTrace.Audio.ChordTypes ( - -- * The 'Timed' class - Timed (..) - - -- * Datatypes - -- ** Types for representing Chords and their probabilities - , ChordBeatAnnotation - , ChordAnnotation - , ProbChordSeg (..) - , ProbChord (..) - , ChordCand (..) - - -- ** Representing musical time - , TimedData (..) - , BeatTimedData (..) - -- , beatTimedData - , Beat (..) - , BeatBar (..) - , BeatBarTrackData - , NumData - - -- ** Representing raw audio data - , AudioFeat (..) - , ChordinoData - , ChordinoLine (..) - , KeyStrengthData - , BeatTrackerData - , BeatChroma - , ChordStruct - - -- * Functions - -- ** Type conversion and other utilities - , getBeatTrack - , getBeat - , setBeat - , nextBeat - , prevBeat - , dumpBeats - , dumpBeat - , timeStamp - , beat - - -- ** miscellaneous - , chromaPC -)where - -import HarmTrace.Base.MusicRep -import Text.Printf (printf) -import Control.DeepSeq - --------------------------------------------------------------------------------- --- High-level structure --------------------------------------------------------------------------------- - --- | Represents a chord transcription, similar to 'ChordAnnotation', but --- 'ChordBeatAnnotation' also contains 'Beat' information. -type ChordBeatAnnotation = [BeatTimedData ChordLabel] - --- | A chord annotation consists of a --- list with chords and segment boundaries. -type ChordAnnotation = [TimedData ChordLabel] - -data TimedData a = TimedData a NumData NumData - --- | A datatype that wraps around an arbitrary datatype, adding (in this order) --- a 'Beat', an onset, and an offset. -data BeatTimedData a = BeatTimedData a Beat NumData NumData - --- | An alternative constructor for a BeatTimedData using two BeatBar datatypes --- instead of a 'Beat' and two 'NumData's. --- beatTimedData :: a -> BeatBar -> BeatBar -> BeatTimedData a --- beatTimedData a on off = let (onnum,onbt) = beatBar on - -- in BeatTimedData a onbt onnum (fst $ beatBar off) - --- | Clustering 'ProbChord's in a collection of chords that share a key -data ProbChordSeg = Segment { segKey :: Key - , segChords :: [BeatTimedData [ProbChord]] } - --- | Combines a 'ChordLabel' with a probability. -data ProbChord = ProbChord {chordLab :: ChordLabel, prob :: NumData} - --- | A chord candidate: an intermediate datatype that matches shorthand, --- chord structure and root note (plus inversion) -data ChordCand = ChordCand { originalRootCC :: Root - , inversionRootCC :: Root - , shorthardCC :: Shorthand - , chordStructCC :: ChordStruct } - deriving Show - -type ChordStruct = [NumData] - --- | For now, we fix the number of available beats to four, because this is also --- hard-coded into the bar and beat-tracker. -data Beat = One | Two | Three | Four deriving (Eq, Enum) - --- | An iterable list of Roots -chromaPC ::[Root] -chromaPC = [ Note Nothing C - , Note (Just Fl) D - , Note Nothing D - , Note (Just Fl) E - , Note Nothing E - , Note Nothing F - , Note (Just Sh) F - , Note Nothing G - , Note (Just Fl) A - , Note Nothing A - , Note (Just Fl) B - , Note Nothing B - ] - --- | 'Timed' provides an interface for datatypes that add (musical) time --- information to other datatypes. Hence, it allows for accessing the fields --- of 'TimedData' and 'BeatTimedData' via the same interface. -class Timed t where - -- | Returns the contained datatype - getData :: t a -> a - -- | Returns the onset time stamp - onset :: t a -> NumData - -- | Returns the offset time stamp - offset :: t a -> NumData - -- | wraps a datatype in 't' - setData :: t a -> b -> t b - -- | Sets the onset time stamp - setOnset :: t a -> NumData -> t a - -- | Sets the offset time stamp - setOffset :: t a -> NumData -> t a - -instance Timed TimedData where - getData (TimedData d _ _ ) = d - onset (TimedData _ on _ ) = on - offset (TimedData _ _ off) = off - setData (TimedData _ on off) d = TimedData d on off - setOnset (TimedData d _ off) on = TimedData d on off - setOffset (TimedData d on _ ) off = TimedData d on off - -instance Timed BeatTimedData where - getData (BeatTimedData d _ _ _ ) = d - onset (BeatTimedData _ _ on _ ) = on - offset (BeatTimedData _ _ _ off) = off - setData (BeatTimedData _ b on off) d = BeatTimedData d b on off - setOnset (BeatTimedData d b _ off) on = BeatTimedData d b on off - setOffset (BeatTimedData d b on _ ) off = BeatTimedData d b on off - --------------------------------------------------------------------------------- --- NFData instances --------------------------------------------------------------------------------- - --- Simplified -instance NFData (TimedData ChordLabel) where - rnf (TimedData a b c) = a `seq` rnf b `seq` rnf c - -instance NFData Beat where - rnf One = () - rnf Two = () - rnf Three = () - rnf Four = () - --------------------------------------------------------------------------------- --- Instances of high-level data structure --------------------------------------------------------------------------------- - -instance Eq (ProbChord) where - a == b = chordLab a == chordLab b - --- TODO remove line-endings from show instances - -instance Show (ProbChord) where - show (ProbChord (Chord r sh _ _ _) p) = - show r ++ ':' : show sh ++ ':' : printf "%.2f" p - -instance Show a => Show (TimedData a) where - show (TimedData bk s l) = show bk ++ " (" ++ show s ++ ':' : show l ++ ")\n" - -instance Show ProbChordSeg where - show pc = concatMap (\x -> show (segKey pc) ++ ' ' : show x) (segChords pc) - -instance Show Beat where - show One = "1" - show Two = "2" - show Three = "3" - show Four = "4" - -instance Show BeatBar where - show = show . beatBar - -instance Show a => Show (BeatTimedData a) where - show (BeatTimedData dat bt on off) = - show bt ++ ';' : show dat ++ ';' : show on ++ ';' : show off ++ "\n" - --------------------------------------------------------------------------------- --- numerical data representation --------------------------------------------------------------------------------- - --- | Groups the three types of VAMP plug-in data: 'ChordinoData', --- 'BeatBarTrackData', and 'KeyStrengthData'. See for more information: --- --- * <http://www.vamp-plugins.org> --- --- * <http://isophonics.net/nnls-chroma> --- --- * <http://omras2.org/SonicAnnotator> -data AudioFeat = AudioFeat { getChroma :: ChordinoData - , getBeats :: BeatBarTrackData - , getKeys :: KeyStrengthData } - -type ChordinoData = [ChordinoLine] - --- | Represents two chroma features and a time stamp. -data ChordinoLine = ChordinoLine - { - -- | Returns the time stamp of the chroma features - time :: NumData - -- | Returns the bass chroma feature - , bass :: [NumData] -- each of the lists has always 12 elements - -- | Returns the treble chroma feature - , treb :: [NumData] -- A, Bb, B, C, Db, D, Eb, E, F, F#, G, Ab - } deriving (Eq, Show) -- and is shifted 3 positions to match C, Db, .., B - -type KeyStrengthData = ChordinoData - -type BeatTrackerData = [NumData] - -newtype BeatBar = BeatBar {beatBar :: (NumData, Beat)} deriving Eq - -type BeatBarTrackData = [BeatBar] - --- | A type synonym is defined for our main numerical representation, this --- allows us to easily change the precision. -type NumData = Double - -type BeatChroma = [[ChordinoLine]] -- one list per beat - --- we compare based on the timestamp only -instance Ord BeatBar where - compare (BeatBar (b1,_)) (BeatBar (b2,_)) = compare b1 b2 - --------------------------------------------------------------------------------- --- Some type conversion utilities --------------------------------------------------------------------------------- - --- | Converts 'BeatBarTrackData' into 'BeatTrackerData' -getBeatTrack :: BeatBarTrackData -> BeatTrackerData -getBeatTrack = map (fst . beatBar) - --- | Provides access to the 'Beat' field of a 'BeatTimedData'. The other fields --- should be accessed by the methods of the 'Timed' class. -getBeat :: BeatTimedData a -> Beat -getBeat (BeatTimedData _ b _ _) = b - --- | Adds 'Beat' information to a 'Timed' datatype -setBeat :: Timed t => t a -> Beat -> BeatTimedData a -setBeat tdat bt = BeatTimedData (getData tdat) bt (onset tdat) (offset tdat) - -nextBeat, prevBeat :: Beat -> Beat --- | returns the next beat, e.g. @ nextBeat Two = Three @. --- Following the (current) definition of 'Beat', we still assume 4/4, in the --- future this function should also have the meter as an argument. -nextBeat Four = One -nextBeat b = succ b - --- | returns the previous 'Beat', similar to 'prevBeat'. -prevBeat One = Four -prevBeat b = pred b - --- | Converts a list of 'BeatTimedData's into a list of 'TimedData's -dumpBeats :: [BeatTimedData a] -> [TimedData a] -dumpBeats = map dumpBeat - --- | Converts a 'BeatTimedData' into a 'TimedData' -dumpBeat :: BeatTimedData a -> TimedData a -dumpBeat (BeatTimedData dat _bt on off) = TimedData dat on off - --- | Returns the time stamp of a 'BeatBar' -timeStamp :: BeatBar -> NumData -timeStamp = fst . beatBar - --- | Returns the 'Beat' of a 'BeatBar' -beat :: BeatBar -> Beat -beat = snd . beatBar
src/HarmTrace/Audio/ChromaChord.hs view
@@ -14,8 +14,10 @@ -------------------------------------------------------------------------------- module HarmTrace.Audio.ChromaChord ( createChordRanks, beatSync - , mergeByBeat, addBeatTimeStamp - , mergeAndTimeStamp, meanBeatSyncVectors + -- , mergeByBeat + , meanBeatSyncVectors + , mergeByOneAndThree + -- , addBeatTimeStamp , module Numeric.LinearAlgebra , module Numeric.GSL.Statistics ) where @@ -23,14 +25,14 @@ import Constants (maxProbChordListLength, cutOffProbability) import HarmTrace.Audio.DataParser (shift) -import HarmTrace.Audio.ChordTypes +import HarmTrace.Base.MusicTime import HarmTrace.Base.MusicRep -- import Text.Printf (printf) import Data.List (sortBy,find) -- , elemIndices, minimumBy) -import Data.Maybe (isJust, fromJust, mapMaybe) +import Data.Maybe (mapMaybe) import Data.Ord (comparing) --- import Data.Function (on) +import Data.Functor ((<$>)) -- http://hackage.haskell.org/package/hmatrix import Numeric.LinearAlgebra hiding (find) @@ -40,61 +42,81 @@ -------------------------------------------------------------------------------- -- Matching chords and chroma -------------------------------------------------------------------------------- - --- | Wraps Chord candidate lists into a 'BeatTimedData' structure -addBeatTimeStamp :: [BeatBar] -> [[a]] -> [BeatTimedData [a]] -addBeatTimeStamp = beatTime (BeatBar (0,Four)) where + +-- | Synchronises the 'ChordinoData' with the list of beats +-- by grouping the 'ChordinoLines' of the 'ChordinoData' in separate lists. +beatSync :: BarTimeTrackData -> [ChordinoLine] -> [BeatChroma] -- = [TimedData ChordinoLine] +beatSync _ [] = [] +beatSync [] _ = error "HarmTrace.Audio.ChromaChord: no beat tracker data" +beatSync bt cs = beatTime fstBeat syncbt beatAlignedChroma where + + fstBeat = BarTime 0 (prevBeat . beat . head $ bt) + lstBeat = BarTime (time . last $ cs) (nextBeat . beat . last $ bt) + syncbt = takeWhile (< lstBeat) bt ++ [lstBeat] + beatAlignedChroma = groupChroma (getBeatTrack syncbt) [head cs] cs - beatTime :: BeatBar -> [BeatBar] -> [[a]] -> [BeatTimedData [a]] + -- groups a list of 'ChromaLine's into beat synchronised group: + -- one list per beat + groupChroma :: [NumData] -> [ChordinoLine] -> [ChordinoLine] -> [[ChordinoLine]] + groupChroma _ _ [] = [] + groupChroma [] _ c = [c] -- TODO: prv should be first arg + groupChroma (b:bs) prv c -- we also store the previous group in case beat < time + | null x = prv : groupChroma bs prv c -- Why do we need this? + | otherwise = x : groupChroma bs x xs + where (x, xs) = span ((b >=) . time) c + + -- given beatTracker data and a grouped chroma, the grouped chroma is wrapped + -- into a TimedData type + beatTime :: BarTime -> [BarTime] -> [a] -> [TimedData a] + -- TODO replace last by this case + -- beatTime (BarTime (on, onbt)) [] [c] = TimedData c onbt on (time . last $ c) beatTime _ [] [] = [] - beatTime (BeatBar (on, onbt)) (next@(BeatBar (off, _)) : bs) (x : xs) = - BeatTimedData x onbt on off : beatTime next bs xs - beatTime _ _ _ = error "addBeatTimeStamp:: asynchronous beats and data" - --- | Given a list of beats, a list of grouped data items, and a merging function --- 'mergeAndTimeStamp' returns a list of 'BeatTimedData'. Before wrapping the --- the grouped data items, e.g. chord candidates, the list is reduced by the --- provided merging function. -mergeAndTimeStamp ::([a] -> a)-> [BeatBar] -> [[a]] -> [BeatTimedData a] -mergeAndTimeStamp f = merge (BeatBar (0,Four)) where - -- merge :: BeatBar -> [BeatBar] -> [[a]] -> [BeatTimedData a] - merge _ [] [] = [] - merge (BeatBar (on, bt)) beats (x : xs) = - let (off : rest) = drop (length x -1) beats - in BeatTimedData (f x) bt on (timeStamp off) : merge off rest xs - merge _ _ _ = error "mergeAndTimeStamp: asynchronous beats and data" - + beatTime (BarTime on onbt) (next@(BarTime off _) : bs) (x : xs) = + TimedData x [BarTime on onbt, BarTime off (nextBeat onbt)] : beatTime next bs xs + beatTime _ _ _ = error "beatSync: asynchronous beats and data" + + -- | Merges chord segments, adding a bias toward merging at the first --- and the third 'Beat' (specified by 'canMerge'). -mergeByBeat :: [BeatTimedData [ProbChord]] -> [BeatTimedData [ProbChord]] -mergeByBeat [] = [] -mergeByBeat [a] = [a] -mergeByBeat (x:y:xs) - | canMerge (getBeat x) (getBeat y) && isJust xy = mergeByBeat (fromJust xy:xs) - | otherwise = x : mergeByBeat (y:xs) where - xy = merge x y - -- merges two Timed 'ProbChord's using intersectPC (currently in Utils.hs) - merge :: BeatTimedData [ProbChord] -> BeatTimedData [ProbChord] - -> Maybe (BeatTimedData [ProbChord]) - merge a b - | not $ null m = Just (BeatTimedData m (getBeat a) (onset a) (offset b)) - | otherwise = Nothing - where m = intersectPC (getData a) (getData b) - -- specifies which combinations of beats are allowed to merge - canMerge :: Beat -> Beat -> Bool - canMerge One _ = True - canMerge Three _ = True - canMerge _ _ = False +-- and the third 'Beat' +mergeByOneAndThree :: [TimedData [ProbChord]] -> [TimedData [ProbChord]] +mergeByOneAndThree = mergeByBeat canMerge intersectPC where + -- specifies which combinations of beats are allowed to merge + canMerge :: Beat -> Beat -> Bool + canMerge One _ = True + canMerge Three _ = True + canMerge _ _ = False +-- Conditionally combines lists wrapped in a 'BeatTimeData' in a list. The first +-- argument should determine at which combinations of beats the merging function +-- (the second argument) should be applied. N.B. this function cannot +-- be rewritten with a foldr, because the outcome is dependend of the +-- left-to-right processing. Rewriting the function with foldl makes the +-- whole recognition process about 1/3 slower... +mergeByBeat :: (Beat -> Beat -> Bool) -> ([a] -> [a] -> [a]) + -> [TimedData [a]] -> [TimedData [a]] +mergeByBeat _ _ [] = [] +mergeByBeat _ _ [a] = [a] +mergeByBeat canMerge merge (x:y:xs) + | canMerge (getBeat x) (getBeat y) + && not (null m) = mergeByBeat canMerge merge (xy:xs) + | otherwise = x : mergeByBeat canMerge merge ( y:xs) + where xy = TimedData m (getTimeStamps x ++ (tail . getTimeStamps $ y)) + m = merge (getData x) (getData y) +-- | Calculates the intersection of to ['ProbChord]' N.B. because of the +-- set-based nature of the function the function is rather expensive, luckily +-- the length of the ['ProbChord'] is constant. Also, because the intersection +-- might change the order of the 'ProbChord's we sort the list again +-- descendingly intersectPC :: [ProbChord] -> [ProbChord] -> [ProbChord] -intersectPC a b = let (a',b') = order a b - in mapMaybe (findAndMergePC a') b' where +intersectPC a b = reverse . sortBy (comparing prob) + $ mapMaybe (findAndMerge a') b' where + (a',b') = order a b -- N.B. the probabilities are not divided by their length so > 1, -- due to the addition - findAndMergePC :: [ProbChord] -> ProbChord -> Maybe ProbChord - findAndMergePC pcs pc = case find (== pc) pcs of - (Just pc') -> Just (ProbChord (chordLab pc) (prob pc + prob pc')) + findAndMerge :: [ProbChord] -> ProbChord -> Maybe ProbChord + findAndMerge pcs pc = case find (== pc) pcs of + (Just pc') -> Just (ProbChord (chordLab pc) (prob pc + prob pc')) Nothing -> Nothing -- takes two lists and returns a tuple where the first element is the smallest @@ -104,19 +126,6 @@ | length x <= length y = (x,y) | otherwise = (y,x) --- | Synchronises the 'ChordinoData' with the list of beats --- by grouping the 'ChordinoLines' of the 'ChordinoData' in separate lists. -beatSync :: BeatBarTrackData -> ChordinoData -> BeatChroma -beatSync _ [] = [] -beatSync bt (cd:cs) = beatSync' (getBeatTrack bt) [cd] (cd:cs) where - beatSync' :: [NumData] -> [ChordinoLine] -> [ChordinoLine] -> [[ChordinoLine]] - beatSync' _ _ [] = [] - beatSync' [] _ c = [c] - beatSync' (b:bs) prv c -- we also store the previous group in case beat < time - | null x = prv : beatSync' bs prv xs - | otherwise = x : beatSync' bs x xs - where (x, xs) = span ((>=) b . time) c - -------------------------------------------------------------------------------- -- Matrix Functions for matching chords -------------------------------------------------------------------------------- @@ -130,11 +139,12 @@ -- between a chord candidate and the chroma vector in the range [0,1], -- the distances are normalised by dividing them by distance of the best -- matching chord candidate. -createChordRanks :: BeatChroma -> [[ProbChord]] +createChordRanks :: [BeatChroma] -> [TimedData [ProbChord]] createChordRanks = - map (selectTop . normalize . sortTake . matchCDictionary). meanBeatSyncVectors + map (selectTop . normalize . sortTake . matchCDictionary <$>) . meanBeatSyncVectors where - sortTake, normalize :: [ProbChord] -> [ProbChord] + -- here prob is still (unnormalised) euclidean distance + sortTake, normalize :: [ProbChord] -> [ProbChord] sortTake = take maxProbChordListLength . sortBy (comparing prob) normalize l@(h:_) = let ph = prob h in map (\p -> p{prob = ph / prob p }) l @@ -150,23 +160,17 @@ -- takes the mean of every "beat block" and these Vectors as one Matrix -- Each row of this matrix corresponds to the chroma within one beat -meanBeatSyncVectors :: BeatChroma -> [Vector NumData] -- [[ChordinoLine]] -meanBeatSyncVectors = map mean . beatSyncMatrix - --- takes the median of every "beat block" and these Vectors as one Matrix --- Each row of this matrix corresponds to the chroma within one beat --- N.B. does not perform as well as meanBeatSyncVectors --- medianBeatSyncVectors :: BeatChroma -> [Vector NumData] --- medianBeatSyncVectors = - -- map (fromList . map GSL.median . toColumns) . beatSyncMatrix +-- N.B. BeatChroma = BeatTimeData ChordinoLine +meanBeatSyncVectors :: [BeatChroma] -> [TimedData (Vector NumData)] +meanBeatSyncVectors = map (mean <$>) . beatSyncMatrix -- creates a list of matrices, in which each matrix corresponds to the -- collection of chroma vectors within one beat (drop the time stamp) -beatSyncMatrix :: BeatChroma -> [Matrix NumData] -beatSyncMatrix = map (dropColumns 1 . toChromaMatrix) +beatSyncMatrix :: [BeatChroma] -> [TimedData (Matrix NumData)] +beatSyncMatrix = map (dropColumns 1 . toChromaMatrix <$>) -- converts a ChordinoData into a Matrix -toChromaMatrix :: ChordinoData -> Matrix NumData +toChromaMatrix :: [ChordinoLine] -> Matrix NumData toChromaMatrix = fromLists . map mergeLine where mergeLine :: ChordinoLine -> [NumData] mergeLine (ChordinoLine tm bs tb) = tm : bs ++ tb @@ -181,17 +185,18 @@ matchStruct chroma (ChordCand r _ir None cs) = ProbChord (Chord r None [] 0 1) (pnorm PNorm2 (chroma - fromList (cs ++ cs))) matchStruct chroma (ChordCand r _ir sh cs) = - -- Chord root shorthand degrees description_str repetitions - ProbChord (Chord r sh [] 0 1) (sqrt (bss * bss + treble * treble)) + -- Chord root shorthand degrees location duration + -- ProbChord (Chord r sh [] 0 1) (sqrt (bss * bss + treble * treble)) + ProbChord (Chord r sh [] 0 1) ((bss + treble) * 0.5) where treble = pnorm PNorm2 (subVector 12 12 chroma - fromList cs) - -- (_ir,bss) = minimumBy (compare `on` snd) . map matchInv $ rootInvs cs + (_ir,bss) = matchInv r -- calculates the euclidean distance between the bass chromagram -- and all bass note inversions. matchInv :: Root -> (Root, NumData) matchInv ir = (ir, pnorm PNorm2 (subVector 0 12 chroma - bcs)) where - bcs = fromList (pre ++ [1] ++ tail post) + bcs = fromList (pre ++ [1.0] ++ tail post) (pre, post) = splitAt (toSemitone ir) (shortHandToCS None) -- For a given chord structure, compute all possible @@ -223,28 +228,19 @@ mkChordCands :: Int -> [ChordCand] mkChordCands n = [ ChordCand r r sh shiftedCS] where shiftedCS = shift (12-n) cstruct - r = iThRoot n - --- Get the i-th chord root, for 0 <= i < 12 --- For i >= 12, the result is given modulo 12 --- JPM: I actually think this is not necessary because we know that --- 0 <= i < 12, but it makes things safer anyway -iThRoot :: Int -> Root -iThRoot n | n < 0 = error "iThRoot: negative index" - | n >= 0 && n < 12 = chromaPC !! n - | otherwise = iThRoot (n `mod` 12) + r = toRoot n shortHandToCS :: Shorthand -> ChordStruct shortHandToCS sh = case sh of - -- 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 - -- C, Db, D, Eb, E, F, F#, G, Ab, A, Bb, B - Maj -> [1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0 ] - Min -> [1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0 ] + -- 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 + -- C, Db, D, Eb, E, F, F#, G, Ab, A, Bb, B + Maj -> [1.5, 0, 0.5, 0, 1.0, 0, 0, 1.5, 0, 0.5, 0, 0.5 ] + Min -> [1.5, 0, 0 , 1.0, 0 , 0, 0, 1.5, 0, 0 , 1.0, 0 ] + Sev -> [1.5, 0, 0 , 0, 1.0, 0, 0, 1.5, 0, 0.0, 1.0, 0 ] -- Dim -> [1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0 ] -- HDim7 -> [1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0 ] -- Dim -> [1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0 ] -- Maj6 -> [1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0 ] - Sev -> [1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0 ] -- Dim7 -> [1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0 ] -- Maj7 -> [1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1 ] -- Min7 -> [1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0 ]
src/HarmTrace/Audio/ChromaKey.hs view
@@ -15,47 +15,60 @@ module HarmTrace.Audio.ChromaKey ( beatSyncKeyStrength, keyMap ) where import HarmTrace.Audio.ChromaChord ( Vector, fromList, correlation - , beatSync, meanBeatSyncVectors ) -import HarmTrace.Audio.ChordTypes + , beatSync, meanBeatSyncVectors + ) +import HarmTrace.Base.MusicTime import HarmTrace.Base.MusicRep - -------------------------------------------------------------------------------- -- Chroma key estimation -------------------------------------------------------------------------------- -- | Calculates the beat synchronised key strenght for all -- 24 keys (ordered by 'KeyMap'). -beatSyncKeyStrength :: BeatBarTrackData -> ChordinoData -> [[NumData]] +beatSyncKeyStrength :: BarTimeTrackData -> ChordinoData -> [TimedData [NumData]] beatSyncKeyStrength bts key = - map matchKeyProfiles . meanBeatSyncVectors $ beatSync bts key + map matchKeyProfiles . meanBeatSyncVectors $ beatSync bts key where + + -- canMerge :: Beat -> Beat -> Bool + -- canMerge One Two = True + -- canMerge One Three = True + -- canMerge One Four = True + -- canMerge _ _ = False + -nanToZero :: RealFloat a => a -> a +nanToZero :: Double -> NumData nanToZero n = if isNaN n then 0 else n -matchKeyProfiles :: Vector NumData -> [NumData] +matchKeyProfiles :: TimedData (Vector NumData) -> TimedData [NumData] -- matchKeyProfiles chroma = map (\x -> pnorm PNorm2 (chroma - x)) allKeyProfiles -- matchKeyProfiles crm = map (nanToZero . GSL.correlation crm) allKeyProfiles -matchKeyProfiles crm = map (nanToZero . correlation crm) allKeyProfiles +-- matchKeyProfiles crm = map (nanToZero . correlation crm) allKeyProfiles +matchKeyProfiles = fmap (\x -> map (nanToZero . correlation x) allKeyProfiles) -allKeyProfiles :: [Vector NumData] +allKeyProfiles :: [Vector Double] allKeyProfiles = map (fromList . keyToProfile) keyMap -keyToProfile :: Key -> [NumData] +keyToProfile :: Key -> [Double] keyToProfile (Key root m) = reverseShift (toSemitone root) (selectProfile m) where reverseShift :: Int -> [a] -> [a] reverseShift p l = b ++ a where (a,b) = splitAt (length l - p) l -selectProfile :: Mode -> [NumData] -selectProfile MajMode = krumhanslProfCMaj -selectProfile MinMode = krumhanslProfCMin +selectProfile :: Mode -> [Double] +selectProfile MajMode = temperleyProfCMaj +selectProfile MinMode = temperleyProfCMin -krumhanslProfCMaj, krumhanslProfCMin :: [NumData] -krumhanslProfCMaj = - [6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88] -krumhanslProfCMin = - [6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17] +-- krumhanslProfCMaj, krumhanslProfCMin :: [NumData] +-- krumhanslProfCMaj = + -- [6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88] +-- krumhanslProfCMin = + -- [6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17] +temperleyProfCMaj, temperleyProfCMin :: [Double] +temperleyProfCMaj = + [5.0, 2.0, 3.5, 2.0, 4.5, 4.0, 2.0, 4.5, 2.0, 3.5, 1.5, 4.0 ] +temperleyProfCMin = + [5.0, 2.0, 3.5, 4.5, 2.0, 4.0, 2.0, 4.5, 3.5, 2.0, 1.5, 4.0 ] -------------------------------------------------------------------------------- -- key strength Matrix Computations --------------------------------------------------------------------------------
src/HarmTrace/Audio/DataParser.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE RankNTypes #-} {-# OPTIONS_GHC -Wall #-} -------------------------------------------------------------------------------- @@ -17,7 +18,7 @@ module HarmTrace.Audio.DataParser ( -- * Parsing beat data parseBeatData - , parseBeatBarData + , parseBarTimeData , pBeat -- * Parsing chromagram data , parseChordinoData @@ -32,7 +33,7 @@ , shift ) where -import HarmTrace.Audio.ChordTypes +import HarmTrace.Base.MusicTime import HarmTrace.Base.Parsing hiding (pComma,pQuotedString,pParentheticalString) -------------------------------------------------------------------------------- @@ -44,11 +45,11 @@ parseBeatData = pListSep_ng pLineEnd pLine <* pLineEnd where pLine = opt pLabel "" *> pNumData <* opt (pComma *> pQuotedString) "" --- | Parses 'BeatBar' data. -parseBeatBarData :: Parser BeatBarTrackData -parseBeatBarData = pListSep_ng pLineEnd pLine <* pLineEnd where - pLine = curry BeatBar <$> (opt pLabel "" *> pNumData ) - <*> (pComma *> pBeat) +-- | Parses 'BarTime' data. +parseBarTimeData :: Parser BarTimeTrackData +parseBarTimeData = pListSep_ng pLineEnd pLine <* pLineEnd where + pLine = BarTime <$> (opt pLabel "" *> pNumData ) + <*> (pComma *> pBeat) -- | Parses a 'Beat'. pBeat :: Parser Beat pBeat = toBeat <$> pQuotedString where @@ -94,6 +95,7 @@ -------------------------------------------------------------------------------- pNumData :: Parser NumData +{-# INLINE pNumData #-} pNumData = pDoubleRaw pComma :: Parser Char
src/HarmTrace/Audio/Evaluation.hs view
@@ -8,21 +8,37 @@ -- Stability : experimental -- Portability : non-portable -- --- Summary: Basic parsers for parsing VAMP csv files. +-- Summary: A module for evaluating chord and key annotations -------------------------------------------------------------------------------- + module HarmTrace.Audio.Evaluation ( - relCorrectOverlap, printRelCorrectOverlap + -- * Evaluation functions + relCorrectOverlap + , achievScore + , chordChangeRatio + , avgDistToOne + -- * Chord and key equality functions + , chordTriadEq + , chordClassEq + , majMinEq + -- * Displaying evaluations + , printChordRCO + , printRCO + -- * Sampling + , sample ) where import Constants -import HarmTrace.Audio.ChordTypes -import HarmTrace.Audio.Annotate (preProcessData, preProcessKeyData) +import HarmTrace.Base.MusicTime +import HarmTrace.Audio.Annotate (preProcessData) import HarmTrace.Base.MusicRep -import Data.List (genericLength, zipWith5) +import Data.List (genericLength, zipWith5, foldl') import Text.Printf(printf) import System.IO (stdout,hFlush) +import Data.Foldable (foldrM) +import Control.Monad.State (State, execState, modify) -- TODO this is a parameter and should some how be integrated into Constants.hs -- this functions determines when two chords are considered the same @@ -30,55 +46,148 @@ eqFunc = chordTriadEq -------------------------------------------------------------------------------- --- Evaluation functions +-- Chord and key equality functions -------------------------------------------------------------------------------- --- chordClassEq :: ChordLabel -> ChordLabel -> Bool --- chordClassEq (Chord (Note Nothing N) None _ _ _) - -- (Chord (Note Nothing N) None _ _ _) = True --- chordClassEq (Chord (Note Nothing N) None _ _ _) _ = False --- chordClassEq _ (Chord (Note Nothing N) None _ _ _) = False --- chordClassEq (Chord r1 sh1 _ _ _) (Chord r2 sh2 _ _ _) = - -- (toSemitone r1) == (toSemitone r2) && (toClassType sh1 == toClassType sh2) +-- | Returns True if both 'ChordLabel's are equal at the chord class level: +-- A chord is classified as being major, minor, dominant seventh, or dimished +-- seventh. 'chordClassEq' only returns True if the class of compared chords +-- is the same. "None Chords" match only with other None Chords and +-- with nothing else +chordClassEq :: ChordLabel -> ChordLabel -> Bool +chordClassEq a b = chordRoot a `rootEQ` chordRoot b + && toClassType a == toClassType b +-- | Returns True if both 'ChordLabel's are equal at the triad level: they are +-- either moth major or both minor. "None Chords" match only with other "None +-- Chords" and with nothing else chordTriadEq :: ChordLabel -> ChordLabel -> Bool -chordTriadEq (Chord (Note Nothing N) None _ _ _) - (Chord (Note Nothing N) None _ _ _) = True -chordTriadEq (Chord (Note Nothing N) None _ _ _) _ = False -chordTriadEq _ (Chord (Note Nothing N) None _ _ _) = False -chordTriadEq (Chord r1 sh1 _ _ _) (Chord r2 sh2 _ _ _) = - toSemitone r1 == toSemitone r2 && toMode sh1 == toMode sh2 +chordTriadEq a b = chordRoot a `rootEQ` chordRoot b + && toMajMin (toTriad a) == toMajMin (toTriad b) +-- | Returns True if the 'Root's of the 'Chord's are equal, but the one chord +-- is Major and the other chord is Minor. +majMinEq :: ChordLabel -> ChordLabel -> Bool +majMinEq a b = chordRoot a `rootEQ` chordRoot b + && toTriad a `triadEq` toTriad b where + + -- ingore the NoClass and only return True in case of maj/min and min/maj + triadEq :: Triad -> Triad -> Bool + triadEq x y = case (toMajMin x, toMajMin y) of + (MajClass, MinClass) -> True + (MinClass, MajClass) -> True + _ -> False + + + +-- | enharmonic equality for 'Root' 'Note's, N == N, X == X, and G# == Ab +rootEQ :: Root -> Root -> Bool +rootEQ (Note Nothing X) (Note Nothing X) = True -- two unknown roots +rootEQ (Note Nothing N) (Note Nothing N) = True -- two none roots +rootEQ (Note Nothing X) _ = False -- one unknown root +rootEQ _ (Note Nothing X) = False +rootEQ (Note Nothing N) _ = False -- one none root +rootEQ _ (Note Nothing N) = False +rootEQ a b = toSemitone a == toSemitone b + +-------------------------------------------------------------------------------- +-- Evaluation functions +-------------------------------------------------------------------------------- + -- | Calculates the relative correct overlap, which is the recall -- of matching frames, and defined as the nr of matching frames (sampled at -- an 10 milisecond interval) divided by all frames. -relCorrectOverlap :: ChordAnnotation -> ChordAnnotation -> Double -relCorrectOverlap a b = - foldl countMatch 0 (zipWith eqFunc sama samb) / tot where - sama = sample a - samb = sample b - tot = max (genericLength sama) (genericLength samb) +relCorrectOverlap :: (a -> a -> Bool) -> [TimedData a] -> [TimedData a] + -> Double +relCorrectOverlap eq a b = foldl' countMatch 0 (zipWith eq sama samb) / tot + where sama = sample a + samb = sample b + tot = max (genericLength sama) (genericLength samb) +countMatch :: Double -> Bool -> Double +countMatch x y | y = succ x -- count the number of matching frames + | otherwise = x + +-- | Given a chord annotation sample the chord label at every 10 ms +sample :: [TimedData a]-> [a] +sample = sampleWith evaluationSampleRate + +-- like sample, but takes a sample rate (seconds :: Float) as argument +sampleWith :: NumData -> [TimedData a] -> [a] +sampleWith rate = sampleAt [0.00, rate .. ] + + +-- samples at specific points in time, specified in a list +sampleAt :: [NumData] -> [TimedData a] -> [a] +sampleAt _ [] = [] -- below, will never occur +sampleAt [] _ = error "Harmtrace.Audio.Evaluation: No sampling grid specified" +sampleAt (t:ts) (c:cs) + | t <= offset c = getData c : sampleAt ts (c:cs) + | otherwise = sampleAt (t:ts) cs + +-- | calculates the maximal achievable score given a ground truth annotation +-- and a chord candidate list. +achievScore :: [TimedData ChordLabel] -> [TimedData [ChordLabel]] -> Double +achievScore a b = sum (zipWith eq sama samb) / len + where sama = sample a + samb = sample b + len = min (genericLength sama) (genericLength samb) + eq c cs | foldr (\x -> (chordTriadEq c x ||)) False cs = 1.0 + | otherwise = 0.0 + +-- | calculates the number of chord changes in the ground-truth divided +-- by the number of chord changes in the machine annotation. A number < 1 +-- indicates that the machine annotation misses some chord changes. A number +-- > 1 indicates that the machine annotation finds to many chord sequences. +chordChangeRatio :: (ChordLabel -> ChordLabel -> Bool) + -> [TimedData ChordLabel] -> [TimedData ChordLabel] -> Double +chordChangeRatio eq gt ma = (fromIntegral . countChordChanges $ gt) + / (fromIntegral . countChordChanges $ ma) where + + countChordChanges :: [TimedData ChordLabel] -> Int + countChordChanges cs = execState (foldrM step [] $ dropTimed cs) 0 + + step :: ChordLabel -> [ChordLabel] -> State Int [ChordLabel] + step c [] = do modify succ + return [c] + step a ( b : cs ) + | a `eq` b = return (a : b : cs) + | otherwise = do modify succ + return (a : b : cs) + +-- | The 'chordChangeRatio' is optimal if it is one, but it can be larger or +-- smaller than 1. Therefore, calculating the average blurs the actual result. +-- 'avgDistToOne' takes the absolute difference to 1.0 and averages these for a +-- list of Doubles. +avgDistToOne :: [Double] -> Double +avgDistToOne ds = (sum . map absDistToOne $ ds) / genericLength ds where + + absDistToOne :: Double -> Double + absDistToOne a = abs (1.0 - a) + +-------------------------------------------------------------------------------- +-- Displaying evaluations (all in IO) +-------------------------------------------------------------------------------- + -- | does the same thing as relCorrectOverlap, but it also prints the -- chords and uses a lower sample rate. N.B. the number output by -- 'printRelCorrectOverlap' might differ from the output of -- 'relCorrectOverlap', because a different sample rate might be used (see -- 'Constants'). -printRelCorrectOverlap :: (AudioFeat -> ChordBeatAnnotation) -> AudioFeat - -> ChordAnnotation -> IO Double -printRelCorrectOverlap annotator af@(AudioFeat chrm btbar afk) gt = do - let keys = snd $ preProcessKeyData chrm btbar afk - -- BUG: now alswo when we are evaluating a simple annotator grouping is +printChordRCO :: (AudioFeat -> ChordAnnotation) -> [TimedData Key] + -> AudioFeat -> [TimedData ChordLabel] -> IO Double +printChordRCO annotator key af gt = do + let -- BUG: now alswo when we are evaluating a simple annotator grouping is -- is displayed, this is wrong. printRelCorrectOverlap should -- be independend of the kind of annotator. - blks :: [BeatTimedData [ProbChord]] + blks :: [TimedData [ProbChord]] blks = concatMap segChords $ preProcessData Nothing af -- sample the info for printing and evaluation - samaf = sampleWith displaySampleRate (annotator af) + samaf = sampleWith displaySampleRate (dropProb . annotator $ af) samgt = sampleWith displaySampleRate gt sambk = sampleWith displaySampleRate blks - samk = sampleWith displaySampleRate keys + samk = sampleWith displaySampleRate key tot = max (genericLength samaf) (genericLength samgt) showEq m = if m then "==" else "/=" @@ -91,28 +200,26 @@ return equal where equal = g `eqFunc` a putStrLn "time\tmatch\tGT\t\tMPTREE\tkey\toptional chords" m <- sequence (zipWith5 printEval [0.0,displaySampleRate ..] - samgt samaf samk sambk) + samgt + samaf + samk + sambk) return (foldl countMatch 0 m / tot) - -countMatch :: Double -> Bool -> Double -countMatch x y | y = succ x -- count the number of matching frames - | otherwise = x - --- given a chord annotation sample the chord label at every 10 ms -sample :: Timed t => [t a] -> [a] -sample = sampleWith evaluationSampleRate - --- like sample, but takes a sample rate (seconds :: Float) as argument -sampleWith :: Timed t => NumData -> [t a] -> [a] -sampleWith rate = sampleAt [0.00, rate .. ] - - --- samples at specific points in time, specified in a list -sampleAt :: Timed t => [NumData] -> [t a] -> [a] -sampleAt _ [] = [] -- below, will never occur -sampleAt [] _ = error "Harmtrace.Audio.Evaluation: No sampling grid specified" -sampleAt (t:ts) (c:cs) - | t <= offset c = getData c : sampleAt ts (c:cs) - | otherwise = sampleAt (t:ts) cs + + +-- | Calculates the relative correct overlap, which is the recall +-- of matching frames, and defined as the nr of matching frames (sampled at +-- an interval set in 'HarnTrace.Constants' divided by all frames. +-- This functions difers from 'relCorrectOverlap' in that it uses an +-- equality function that is in IO. +printRCO :: (a -> a -> IO (Bool)) -> [TimedData a] -> [TimedData a] + -> IO (Double) +printRCO ioeq a b = do matches <- sequence (zipWith3 printEq [0,displaySampleRate ..] sama samb) + return (foldl' countMatch 0 matches / tot) + where sama = sampleWith displaySampleRate a + samb = sampleWith displaySampleRate b + tot = max (genericLength sama) (genericLength samb) - + -- printEq :: NumData -> a -> a -> IO (Bool) + printEq ts x y = do putStr (printf "%.2f: " ts) + ioeq x y
src/HarmTrace/Audio/Key.hs view
@@ -16,7 +16,7 @@ module HarmTrace.Audio.Key (getBeatSyncKeyFromChroma) where -import HarmTrace.Audio.ChordTypes +import HarmTrace.Base.MusicTime import HarmTrace.Audio.Statistical (groupMinSize, mode) import HarmTrace.Audio.ChromaKey ( beatSyncKeyStrength, keyMap) import HarmTrace.Base.MusicRep @@ -37,18 +37,41 @@ -------------------------------------------------------------------------------- -- | Returns a 'Key' key assignment, given 'Beat' and chroma information. -getBeatSyncKeyFromChroma :: [BeatBar] -> [ChordinoLine] -> [[Key]] -getBeatSyncKeyFromChroma bts key = - groupKeys . getKeyFromTable $ selectKey bts key +-- N.B. The beats and chroma do not have to be synchronised, they will be +-- sychronised in the process of finding the key +-- (see 'Harmtrace.Audio.ChromaKey' and 'Harmtrace.Audio.ChromaChord.BeatSync') +getBeatSyncKeyFromChroma :: AudioFeat -> [TimedData Key] +getBeatSyncKeyFromChroma (AudioFeat _cs bt key _id) = + mergeAndTimeStamp fstBeat syncbt + . groupKeys . getKeyFromTable . selectKey $ beatSyncKeyStrength bt key where -selectKey :: [BeatBar] -> [ChordinoLine] -> Vector (Vector (Int, NumData)) -selectKey _bts [] = empty -selectKey [] _key = empty -selectKey bts key = k where + -- This synchronisation is ugly, but necessary... + -- keyStr = beatSyncKeyStrength bt key + fstBeat = BarTime 0 (prevBeat . beat . L.head $ bt) + lstBeat = BarTime (time . L.last $ key) (nextBeat . beat . L.last $ bt) + syncbt = L.takeWhile (< lstBeat) bt L.++ [lstBeat] + + -- | Given a list of beats, a list of grouped data items, and a merging function + -- 'mergeAndTimeStamp' returns a list of 'BeatTimedData'. Before wrapping the + -- the grouped data items, e.g. chord candidates, the list is reduced by the + -- provided merging function. + mergeAndTimeStamp :: BarTime -> [BarTime] -> [[a]] -> [TimedData a] + mergeAndTimeStamp _ [] [] = [] + mergeAndTimeStamp on beats (x : xs) = + let (off : rest) = L.drop (L.length x -1) beats + in TimedData (L.head x) [on, off] : mergeAndTimeStamp off rest xs + mergeAndTimeStamp _ _ _ = error "mergeAndTimeStamp: asynchronous beats and data" + + +-- selectKey :: [BarTime] -> [ChordinoLine] -> Vector (Vector (Int, NumData)) +selectKey :: [TimedData [NumData]] -> Vector (Vector (Int, NumData)) +selectKey [] = empty +-- selectKey [] _key = empty +selectKey key = k where -- start by calculating the beat synchronised key strenght for all -- 24 keys (ordered by HarmTrace.Audio.ChromaKey.keyMap) m :: Vector (Vector NumData) - m = fromList . L.map fromList $ beatSyncKeyStrength bts key + m = fromList . L.map fromList $ dropTimed key -- calculate for every beat the maximum key (the index) and the -- profile correlation (snd) @@ -87,24 +110,26 @@ -- key assignments getKeyFromTable :: Vector (Vector (Int, NumData)) -> [Key] getKeyFromTable k = L.map (keyMap !!) (L.reverse yek) where - yek = collectMax (fst $ maximumBy (comparing snd) (last k)) (reverse k) + yek = collectMax (getMax $ last k) (reverse k) -- given the table calulated with selectKey, this function calculates -- the actual key assignment for every beat collectMax :: Int -> Vector (Vector (Int, NumData)) -> [Int] collectMax startj l | null l = [] - | otherwise = fst (head l ! startj) : collectMax m (tail l) where - m = fst $ maximumBy (comparing snd) (head l) + | otherwise = fst (head l ! startj) : collectMax (getMax $ head l) (tail l) + + getMax :: Vector (Int, NumData) -> Int + getMax = fst . maximumBy (comparing snd) -- debugging: --- printKeyTable :: [BeatBar] -> [ChordinoLine] -> IO () +-- printKeyTable :: [BarTime] -> [ChordinoLine] -> IO () -- printKeyTable bts chrm = -- let showLn :: Vector (Int, NumData) -> IO () -- showLn x = do mapM_ (\(i,f) -> putStr (printf "(%d, %.2f)" i f)) x -- putStr "\n" -- in mapM_ showLn $ selectKey bts chrm --- naiveBeatSyncKey :: BeatBarTrackData -> [ChordinoLine] -> [Key] +-- naiveBeatSyncKey :: BarTimeTrackData -> [ChordinoLine] -> [Key] -- naiveBeatSyncKey bts key = -- L.map (((!!) keyMap) . maxListIndex) $ beatSyncKeyStrenth bts key
src/HarmTrace/Audio/Statistical.hs view
@@ -13,7 +13,7 @@ -------------------------------------------------------------------------------- module HarmTrace.Audio.Statistical where -import HarmTrace.Audio.ChordTypes +import HarmTrace.Base.MusicTime import Data.List ( genericLength, tails , inits,maximumBy, sort, group)
− src/HarmTrace/Base/MusicRep.hs
@@ -1,265 +0,0 @@-{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE EmptyDataDecls #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE GADTs #-} - -module HarmTrace.Base.MusicRep where - -import Data.Maybe -import Data.List (elemIndex, intersperse, intercalate) -import Control.DeepSeq -import HarmTrace.HAnTree.Binary -import Generics.Instant.TH -import Data.Binary - --------------------------------------------------------------------------------- --- Representing musical information at the value level --------------------------------------------------------------------------------- - --- Keys (at the value level) -data Key = Key { keyRoot :: Root, keyMode :: Mode } deriving (Eq, Ord) -data Mode = MajMode | MinMode deriving (Eq, Ord) - -instance NFData Mode where - rnf MinMode = () - rnf MajMode = () - -type ChordLabel = Chord Root -type ChordDegree = Chord ScaleDegree - --- the representation for a single tokenized chord -data Chord a = Chord { chordRoot :: a - , chordShorthand :: Shorthand - , chordAdditions :: [Addition] - , getLoc :: Int -- the index of the chord - , duration :: Int -- in the list of tokens - } - -data Class = Class ClassType Shorthand - -data ClassType = MajClass | MinClass | DomClass | DimClass | NoClass - deriving (Eq) - -data Shorthand = -- Triad chords - Maj | Min | Dim | Aug - -- Seventh chords - | Maj7 | Min7 | Sev | Dim7 | HDim7 | MinMaj7 - -- Sixth chords - | Maj6 | Min6 - -- Extended chords - | Nin | Maj9 | Min9 - -- Suspended chords - | Sus4 | Sus2 - -- In some cases there is no chord a certain position - -- This is especially important for the chroma processing - | None - deriving (Show, Eq, Enum, Bounded) - - --- Key relative scale degrees to abstract from the absolute Root notes -type ScaleDegree = Note DiatonicDegree - -data DiatonicDegree = I | II | III | IV | V | VI | VII | Imp - deriving (Show, Eq, Enum, Ord, Bounded) - --- Representing absolute root notes -type Root = Note DiatonicNatural - -data DiatonicNatural = C | D | E | F | G | A | B | N -- N is for no root - deriving (Show, Eq, Enum, Ord, Bounded) - --- Intervals for additonal chord notes -type Addition = Note Interval - -data Interval = I1 | I2 | I3 | I4 | I5 | I6 | I7 | I8 | I9 | I10 - | I11 | I12 | I13 - deriving (Eq, Enum, Ord, Bounded) - -data Note a = Note (Maybe Modifier) a deriving (Eq, Ord) - -data Modifier = Sh | Fl | SS | FF -- Sharp, flat, double sharp, double flat - deriving (Eq, Ord) - --------------------------------------------------------------------------------- --- Instances for the general music datatypes --------------------------------------------------------------------------------- - -instance Show Key where - show (Key r m) = show r ++ show m - -instance Show Mode where - show MajMode = "" - show MinMode = "m" - -instance Eq a => Eq (Chord a) where - (Chord ra sha dega _loc _d) == (Chord rb shb degb _locb _db) - = ra == rb && sha == shb && dega == degb - -instance (Show a) => Show (Chord a) where - show (Chord r sh deg _loc _d) = show r ++ ':' : show sh - ++ (if not (null deg) then showAdds deg else "") - -- ++ '_' : show loc ++ ':' : show d - -showAdds :: Show a => [a] -> String -showAdds x = '(' : intercalate "," (map show x) ++ ")" - -instance Show Class where show (Class ct _) = show ct - -instance Show ClassType where - show MajClass = "" - show MinClass = "m" - show DomClass = "7" - show DimClass = "0" - show NoClass = "N" - -instance (Show a) => Show (Note a) where - show (Note m interval) = show interval ++ maybe "" show m - -instance Show Interval where - show a = show . ((!!) ([1..13]::[Integer])) - . fromJust $ elemIndex a [minBound..] - - -instance Show Modifier where - show Sh = "#" - show Fl = "b" - show SS = "##" - show FF = "bb" - --- for showing additional additions -showAdditions :: [Addition] -> String -showAdditions a - | null a = "" - | otherwise = "(" ++ concat (intersperse "," (map show a)) ++ ")" - --------------------------------------------------------------------------------- --- Utils --------------------------------------------------------------------------------- - -isNone :: Root -> Bool -isNone (Note _ N) = True -isNone _ = False - -noneLabel :: ChordLabel -noneLabel = (Chord (Note Nothing N) None [] 0 0) - -isNoneChord :: ChordLabel -> Bool -isNoneChord (Chord (Note _ N) _ _ _ _) = True -isNoneChord (Chord _ None _ _ _) = True -isNoneChord _ = False - -toClassType :: Shorthand -> ClassType -toClassType sh - | sh `elem` [Maj,Maj7,Maj6,Maj9,MinMaj7,Sus4,Sus2] = MajClass - | sh `elem` [Min,Min7,Min6,Min9,HDim7] = MinClass - | sh `elem` [Sev,Nin,Aug] = DomClass - | sh `elem` [Dim,Dim7] = DimClass - | otherwise = error - ("HarmTrace.Base.MusicRep.toClassType: unknown shorthand: " ++ show sh) - -toMode :: Shorthand -> Mode -toMode Maj = MajMode -toMode Min = MinMode -toMode Dim = MinMode -toMode Aug = MajMode -toMode Maj7 = MajMode -toMode Min7 = MinMode -toMode Sev = MajMode -toMode Dim7 = MinMode -toMode HDim7 = MinMode -toMode MinMaj7 = MinMode -toMode Maj6 = MajMode -toMode Min6 = MinMode -toMode Nin = MajMode -toMode Maj9 = MajMode -toMode Min9 = MinMode -toMode Sus4 = MinMode -- for now -toMode Sus2 = MinMode -- for now -toMode m = error - ("HarmTrace.Base.MusicRep.toMode: unknown shorthand: " ++ show m) - --------------------------------------------------------------------------------- --- Value Level Scale Degree Transposition --------------------------------------------------------------------------------- - --- Chord root shorthand degrees location duration -toChordDegree :: Key -> ChordLabel -> ChordDegree -toChordDegree k (Chord r sh degs loc d) = - Chord (toScaleDegree k r) sh degs loc d - -toScaleDegree :: Key -> Root -> ScaleDegree -toScaleDegree _ n@(Note _ N) = - error ("HarmTrace.Base.MusicRep.toScaleDegree: cannot transpose" ++ show n) -toScaleDegree (Key kr _) cr = -- Note Nothing I - scaleDegrees!!(((toSemitone cr) - (toSemitone kr)) `mod` 12) - --- transposes a degree with sem semitones up -transposeSem :: ScaleDegree -> Int -> ScaleDegree -transposeSem deg sem = scaleDegrees!!((sem + (toSemitone deg)) `mod` 12) where - --- gives the semitone value [0,11] of a Degree, e.g. F# = 6 -toSemitone :: (Show a, Enum a) => Note a -> Int -toSemitone (Note m p) - | ix > 6 = error ("HarmTrace.Base.MusicRep.toSemitone: no semitone for" - ++ show (Note m p)) - | otherwise = ([0,2,4,5,7,9,11] !! ix) + modToSemi m where - ix = fromEnum p - --- transforms type-level modifiers to semitones (Int values) -modToSemi :: Maybe Modifier -> Int -modToSemi Nothing = 0 -modToSemi (Just Sh) = 1 -modToSemi (Just Fl) = -1 -modToSemi (Just SS) = 2 -modToSemi (Just FF) = -2 - -scaleDegrees ::[ScaleDegree] -scaleDegrees = [ Note Nothing I - , Note (Just Fl) II - , Note Nothing II - , Note (Just Fl) III - , Note Nothing III - , Note Nothing IV - , Note (Just Sh) IV - , Note Nothing V - , Note (Just Fl) VI - , Note Nothing VI - , Note (Just Fl) VII - , Note Nothing VII - ] - --------------------------------------------------------------------------------- --- Binary instances --------------------------------------------------------------------------------- - -deriveAllL [''Note, ''DiatonicDegree - , ''Mode, ''Chord, ''DiatonicNatural, ''ClassType - , ''Modifier, ''Shorthand, ''Interval] - -instance (Binary a) => Binary (Note a) where - put = putDefault - get = getDefault -instance Binary DiatonicDegree where - put = putDefault - get = getDefault -instance Binary Mode where - put = putDefault - get = getDefault -instance (Binary a) => Binary (Chord a) where - put = putDefault - get = getDefault -instance Binary DiatonicNatural where - put = putDefault - get = getDefault -instance Binary ClassType where - put = putDefault - get = getDefault -instance Binary Modifier where - put = putDefault - get = getDefault -instance Binary Shorthand where - put = putDefault - get = getDefault -instance Binary Interval where - put = putDefault - get = getDefault
− src/HarmTrace/Base/Parsing.hs
@@ -1,47 +0,0 @@-{-# LANGUAGE FlexibleContexts #-} -{-# OPTIONS_GHC -Wall #-} - -module HarmTrace.Base.Parsing ( parseData, parseDataWithErrors - , pString, pLineEnd, pManyTill - , module Data.ListLike.Base - , module Text.ParserCombinators.UU - , module Text.ParserCombinators.UU.Utils - , module Text.ParserCombinators.UU.BasicInstances - ) where - -import Text.ParserCombinators.UU -import Text.ParserCombinators.UU.Utils hiding (pSpaces) -import Text.ParserCombinators.UU.BasicInstances hiding (IsLocationUpdatedBy) -import Data.ListLike.Base (ListLike) - --------------------------------------------------------------------------------- --- A collection of parsing functions used by parsers throughout the project --------------------------------------------------------------------------------- - --- toplevel parsers -parseData :: (ListLike s a, Show a) => P (Str a s LineColPos) b -> s -> b -parseData p inp = fst ( parseDataWithErrors p inp ) - -parseDataWithErrors :: (ListLike s a, Show a) - => P (Str a s LineColPos) b -> s -> (b, [Error LineColPos]) -parseDataWithErrors p inp = (parse ( (,) <$> p <*> pEnd) - (createStr (LineColPos 0 0 0) inp)) - --- parses specific string -pString :: (ListLike state a, IsLocationUpdatedBy loc a, Show a, Eq a) - => [a] -> P (Str a state loc) [a] -pString s = foldr (\a b -> (:) <$> a <*> b) (pure []) (map pSym s) - --- parses whitespace (@pedro: should probably not contain '\n') --- pSpaces :: Parser Char --- pSpaces = pAnySym [' ','\n','\t'] - --- parses UNIX and DOS/WINDOWS line endings -pLineEnd :: Parser String -pLineEnd = pString "\n" <|> pString "\r\n" <|> pString " " <|> pString "\t" - -pManyTill :: P st a -> P st b -> P st [a] -pManyTill p end = [] <$ end - <<|> - (:) <$> p <*> pManyTill p end -
− src/HarmTrace/Base/TypeLevel.hs
@@ -1,81 +0,0 @@-{-# LANGUAGE EmptyDataDecls #-} -{-# LANGUAGE KindSignatures #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE UndecidableInstances #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE DeriveDataTypeable #-} - -module HarmTrace.Base.TypeLevel ( - Su, Ze - , T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10 - , T11, T12, T13, T14, T15, T16, T17, T18, T19, T20 - , ToNat(..) - ) where - -import Data.Typeable - - --- Type level peano naturals -data Su :: * -> * deriving Typeable -data Ze :: * deriving Typeable - --- Some shorthands -type T0 = Ze -type T1 = Su T0 -type T2 = Su T1 -type T3 = Su T2 -type T4 = Su T3 -type T5 = Su T4 -type T6 = Su T5 -type T7 = Su T6 -type T8 = Su T7 -type T9 = Su T8 -type T10 = Su T9 -type T11 = Su T10 -type T12 = Su T11 -type T13 = Su T12 -type T14 = Su T13 -type T15 = Su T14 -type T16 = Su T15 -type T17 = Su T16 -type T18 = Su T17 -type T19 = Su T18 -type T20 = Su T19 - -class ToNat n where - toNat :: n -> Int - -instance ToNat Ze where toNat _ = 0 -instance (ToNat n) => ToNat (Su n) where toNat _ = 1 + toNat (undefined :: n) -{- --- Below is some experimentation... - --- A degree has a distance to root in semi-tones (n in T0..T11) and a --- class (major or minor) -data Degree n cls - --- Transposing is a bit like addition... -type family Transpose m n --- ... but we normalize at the end to stay within T0..T11 -type instance Transpose m T0 = Norm m -type instance Transpose m (Su n) = Transpose (Su m) n - --- Normalizing is the same as subtracting T12, but only if we can. Else we keep --- the type unchanged. -type Norm m = Sub m T12 m - --- Subtraction with an extra type for failure -type family Sub m n fail --- Inductive case -type instance Sub (Su m) (Su n) fail = Sub m n fail --- Base case, subtraction succeeded -type instance Sub m T0 fail = m --- Base case, subtraction failed -type instance Sub T0 (Su n) fail = fail - --- A secondary dominant is a transposition by 7 semi-tones -type SD deg = Transpose deg T7 - --- A tritone substitution is a transposition by 6 semi-tones -type TS deg = Transpose deg T6 --}
− src/HarmTrace/HAnTree/Binary.hs
@@ -1,59 +0,0 @@-{-# LANGUAGE GADTs #-} -{-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE TypeOperators #-} - --- Generic Binary - -module HarmTrace.HAnTree.Binary where - -import Control.Monad (liftM, liftM2) -import Data.Binary -import Generics.Instant - - -class GBinary a where - gput :: a -> Put - gget :: Get a - - -instance GBinary U where - gput _ = return () - gget = return U - -instance (GBinary a) => GBinary (CEq c p p a) where - gput (C a) = gput a - gget = liftM C gget -{- -instance (GBinary a) => GBinary (CEq c p q a) where - gput _ = return () - gget = error "gget: CEq impossible" --} -instance (GBinary a, GBinary b) => GBinary (a :+: b) where - gput (L a) = put (0 :: Word8) >> gput a - gput (R a) = put (1 :: Word8) >> gput a - gget = do t <- get :: Get Word8 - case t of - 0 -> liftM L gget - 1 -> liftM R gget - _ -> error "gget: :+: impossible" - -instance (GBinary a, GBinary b) => GBinary (a :*: b) where - gput (a :*: b) = gput a >> gput b - gget = liftM2 (:*:) gget gget - -instance (Binary a) => GBinary (Rec a) where - gput (Rec a) = put a - gget = liftM Rec get - -instance (Binary a) => GBinary (Var a) where - gput (Var a) = put a - gget = liftM Var get - - --- Default implementations -getDefault :: (Representable a, GBinary (Rep a)) => Get a -getDefault = fmap to gget - -putDefault :: (Representable a, GBinary (Rep a)) => a -> Put -putDefault = gput . from
src/HarmTrace/HAnTree/HAn.hs view
@@ -2,16 +2,16 @@ {-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE GADTs #-} +{-# LANGUAGE DeriveGeneric #-} module HarmTrace.HAnTree.HAn where import HarmTrace.Base.MusicRep -import HarmTrace.Tokenizer.Tokens -import HarmTrace.HAnTree.Binary +import HarmTrace.Models.ChordTokens -import Generics.Instant.TH import Control.DeepSeq import Data.Binary +import GHC.Generics (Generic) -------------------------------------------------------------------------------- -- Datatypes for representing Harmonic Analyses (at the value level) @@ -22,7 +22,8 @@ | HAnFunc !HFunc | HAnPrep !Prep | HAnTrans !Trans - | HAnChord !ChordToken + | HAnChord !ChordToken + deriving Generic -- duration Mode constructor_ix specials data HFunc = Ton !Int !Mode !Int !(Maybe Spec) @@ -30,44 +31,35 @@ | Sub !Int !Mode !Int !(Maybe Spec) | P | PD - | PT + | PT + deriving Generic data Spec = Blues | MinBorrow | Parallel - deriving Eq + deriving (Eq, Generic) -- Preparations, like secondary dominants etc. that cause a "split" in the tree data Prep = SecDom !Int !ScaleDegree -- "V/X" | SecMin !Int !ScaleDegree -- "v/X" | DiatDom !Int !ScaleDegree -- "Vd" | NoPrep + deriving Generic -- Scalde degree transformations, e.g. tritone substitutions etc. data Trans = Trit !Int !ScaleDegree -- "bII/X" | DimTrit !Int !ScaleDegree -- "bIIb9/X" | DimTrans !Int !ScaleDegree -- "VII0" | NoTrans + deriving Generic -------------------------------------------------------------------------------- -- Binary instances -------------------------------------------------------------------------------- -deriveAllL [ ''HAn, ''Trans, ''Prep, ''HFunc, ''Spec] - -instance Binary HAn where - put = putDefault - get = getDefault -instance Binary Trans where - put = putDefault - get = getDefault -instance Binary Prep where - put = putDefault - get = getDefault -instance Binary HFunc where - put = putDefault - get = getDefault -instance Binary Spec where - put = putDefault - get = getDefault +instance Binary HAn +instance Binary Trans +instance Binary Prep +instance Binary HFunc +instance Binary Spec -------------------------------------------------------------------------------- -- NFData instances
src/HarmTrace/HAnTree/PostProcess.hs view
@@ -4,7 +4,7 @@ , mergeDelChords ) where import HarmTrace.Base.MusicRep -import HarmTrace.Tokenizer.Tokens as CT +import HarmTrace.Models.ChordTokens as CT import HarmTrace.HAnTree.HAn import HarmTrace.HAnTree.Tree @@ -76,7 +76,7 @@ toDelHAn :: Key -> [ChordLabel] -> [Tree HAn] toDelHAn key m = map f m where f c@(Chord r sh _add _loc d) = (Node (HAnChord - (ChordToken (toScaleDegree key r) (toClassType sh) [c] CT.Deleted 1 d)) + (ChordToken (toScaleDegree key r) (toClassType c) [c] CT.Deleted 1 d)) [] Nothing) -- returns the deleted chords, given a list of errors and the input tokes
src/HarmTrace/HarmTrace.hs view
@@ -14,17 +14,17 @@ import HarmTrace.HAnTree.HAn (HFunc (P)) import HarmTrace.HAnTree.PostProcess import HarmTrace.Base.MusicRep -import HarmTrace.Tokenizer.Tokens as CT -import HarmTrace.Tokenizer.Tokenizer +import HarmTrace.Models.ChordTokens as CT +import HarmTrace.Base.ChordTokenizer import Data.Ord (comparing) import Data.List (minimumBy) -- Audio/Annotation Stuff import HarmTrace.Audio.AnnotationParser -import HarmTrace.Audio.ChordTypes import HarmTrace.Base.Parsing (parseDataWithErrors) +import HarmTrace.Base.MusicTime (dropTimed, getData) -- Parser stuff import Text.ParserCombinators.UU @@ -75,7 +75,7 @@ -- taken (on tokenizing and on musical recognition). string2Piece :: Grammar g -> String -> ParseResult g string2Piece g s = let - (PieceLabel key tok, err) = parse ((,) <$> parseSongAbs <*> pEnd) + (PieceLabel key tok, err) = parse ((,) <$> pSongAbs <*> pEnd) (createStr (LineColPos 0 0 0) s) (trees, err2) = case g of Jazz -> parse_h ((,) <$> pJazz key <*> pEnd) @@ -94,7 +94,7 @@ (ks , errK) = parseDataWithErrors parseKeyAnnotationData kstr key = getData . head $ filter (not . isNone . keyRoot . getData) ks (tok, errT) = parseDataWithErrors parseAnnotationData cstr - ppTok = preProcess tok + ppTok = dropTimed tok (ts, errP) = case g of Jazz -> parse_h ((,) <$> pJazz key <*> pEnd) (createStr 0 (toKeyRelTok key ppTok))
src/HarmTrace/IO/BasePaths.hs view
@@ -1,19 +1,19 @@-module HarmTrace.IO.BasePaths ( BasePaths, setPaths - , consBasePaths, basePathsExist - , getVampDir, getFeatDir , getOutDir - , getBeatVampPath, getChromaVampPath - , getKeyVampPath) where +module HarmTrace.IO.BasePaths ( BasePaths (..), setPaths, getLogFile + , consBasePaths, basePathsExist) where -import System.FilePath ((</>)) +import System.FilePath ((</>), (<.>), takeFileName) import HarmTrace.IO.Common (fileExists, dirExists) -import Constants ( defaultVampDir, defaultOutDir, defaultFeatDir - , beatVampPath, chromaVampPath, keyVampPath) +import Constants ( defaultVampDir, defaultOutDir, defaultFeatDir, defaultLogDir + , beatVampPath, chromaVampPath, keyVampPath + , sonicAnnotator, logFileSuffix) -- | A datatype for storing the base directories of the vamp plugins, feature -- csv files, and the chord and log Files. data BasePaths = BasePaths { getVampDir :: FilePath , getFeatDir :: FilePath , getOutDir :: FilePath + , getLogDir :: FilePath + , getSonic :: FilePath , getBeatVampPath :: FilePath , getChromaVampPath :: FilePath , getKeyVampPath :: FilePath @@ -21,17 +21,21 @@ -- | Similar to 'consBasePaths', but instead excepts 'Maybe FilePath's instead -- of 'FilePath's -setPaths :: Maybe FilePath -> Maybe FilePath -> Maybe FilePath -> BasePaths -setPaths mv mf mo = +setPaths :: Maybe FilePath -> Maybe FilePath -> Maybe FilePath + -> Maybe FilePath -> Maybe FilePath -> BasePaths +setPaths mv mf mo ml msa = let vamp = maybe defaultVampDir id mv feat = maybe defaultFeatDir id mf out = maybe defaultOutDir id mo - in consBasePaths vamp feat out + logd = maybe defaultLogDir id ml + sa = maybe sonicAnnotator id msa -- there is a specific function without id, but hackage is down + in consBasePaths vamp feat out logd sa -- | contructs a new 'BasePaths' based on a VAMP transform directory, -- a feature directory (to store the CSV files), and an output directory. -consBasePaths :: FilePath -> FilePath -> FilePath -> BasePaths -consBasePaths vamp feat out = BasePaths vamp feat out +consBasePaths :: FilePath -> FilePath -> FilePath -> FilePath -> FilePath + -> BasePaths +consBasePaths vamp feat out logd sa = BasePaths vamp feat out logd sa (vamp </> beatVampPath) (vamp </> chromaVampPath) (vamp </> keyVampPath) -- | checks (verbosely) if the paths stored in a 'BaseDir' exist. @@ -39,6 +43,10 @@ basePathsExist dirs = do v <- fileExists (getBeatVampPath dirs) c <- fileExists (getChromaVampPath dirs) k <- fileExists (getKeyVampPath dirs) + -- l <- fileExists (getLogDir dirs) f <- dirExists (getFeatDir dirs) o <- dirExists (getOutDir dirs) - return (v && c && k && f && o)+ return (v && c && k && f && o) + +getLogFile :: BasePaths -> FilePath -> FilePath +getLogFile bp fp = getLogDir bp </> takeFileName fp <.> logFileSuffix
src/HarmTrace/IO/Common.hs view
@@ -19,7 +19,6 @@ import Constants (vERSION) -- Library modules -import System.Console.ParseArgs hiding (args) -- cabal install parseargs import Data.List (sort, groupBy) import Control.Arrow ((***)) import System.FilePath
src/HarmTrace/IO/FeatExtract.hs view
@@ -21,15 +21,17 @@ import System.FilePath import System.Directory (removeFile) import System.Process +import System.Exit (ExitCode (..)) import Data.List (isPrefixOf) -import Constants ( keyStr, chromaStr, beatStr, deleteDownsampledWav) +import Constants ( keyStr, chromaStr, beatStr, deleteDownsampledWav, downSample) import HarmTrace.Base.Parsing import HarmTrace.IO.Downsample import HarmTrace.IO.BasePaths( BasePaths, getBeatVampPath, basePathsExist , getKeyVampPath, getChromaVampPath - , getFeatDir, getOutDir) + , getFeatDir, getSonic, getLogFile) + -- | Extracts the features of an arbitrary audio file, given three VAMP plugin -- descriptions. Before the feature extraction the audio is preprocessed -- (downsampled) with the the SoX library (see 'HarmTrace.IO.Downsample') @@ -46,26 +48,31 @@ case exists of False -> return Nothing True -> -- create a log file - do let logf = getOutDir dirs </> takeFileName f <.> "extract.log" - cfp = getFeatDir dirs </> dropExtension f ++ chromaStr - bfp = getFeatDir dirs </> dropExtension f ++ beatStr - kfp = getFeatDir dirs </> dropExtension f ++ keyStr + do let -- logf = getOutDir dirs </> takeFileName f <.> "extract.log" + cfp = getFeatDir dirs </> + dropExtension (takeFileName f) ++ chromaStr + bfp = getFeatDir dirs </> + dropExtension (takeFileName f) ++ beatStr + kfp = getFeatDir dirs </> + dropExtension (takeFileName f) ++ keyStr -- open a logfile - hdl <- openFile logf AppendMode + hdl <- openFile (getLogFile dirs f) AppendMode hSetBuffering hdl LineBuffering -- downsample the audio - hPutStrLn hdl (f ++ ";process audio;0;0") - df <- downsample f - hPutStrLn hdl (f ++ ";process audio;99;19") + hPutStrLn hdl (f ++ ";preprocessing;0;0") + df <- case downSample of + True -> downsample f + False -> return f + hPutStrLn hdl (f ++ ";preprocessing;99;9") -- extract the features - sonicannotatorProcPipe (LogInfo "" "" 0 20) hdl + sonicannotatorProcPipe dirs (LogInfo f "" 0 10) hdl (getBeatVampPath dirs) df bfp - sonicannotatorProcPipe (LogInfo "" "" 0 40) hdl + sonicannotatorProcPipe dirs (LogInfo f "" 0 30) hdl (getChromaVampPath dirs) df cfp - sonicannotatorProcPipe (LogInfo "" "" 0 60) hdl + sonicannotatorProcPipe dirs (LogInfo f "" 0 50) hdl (getKeyVampPath dirs) df kfp hClose hdl @@ -76,18 +83,26 @@ -- executes the sonicannotator and logs all execution info to a log file -- TODO: perhaps output a Bool for loggin if the annotating is successful -sonicannotatorProcPipe :: LogInfo -> Handle -> FilePath -> FilePath -> FilePath - -> IO () -sonicannotatorProcPipe info logh transformfile inp outp= do +sonicannotatorProcPipe :: BasePaths -> LogInfo -> Handle -> FilePath + -> FilePath -> FilePath -> IO () +sonicannotatorProcPipe bp info logh transformfile inp outp= do let par = "--transform" : transformfile : "--writer" : "csv" : "--csv-one-file" : [outp, inp] - (_,_,Just err,p) <- createProcess (proc "sonic-annotator" par) + (_,_,Just err,p) <- createProcess (proc (getSonic bp) par) {std_err = CreatePipe} - x <- hGetContents err - let tlog = transformLog info x + -- {std_out = CreatePipe} + cerr <- hGetContents err + -- cout <- hGetContents out + -- print cerr + -- print cout + let tlog = transformLog info cerr hPutStr logh $ concatMap show tlog void (waitForProcess p) - -- return () + + (Just ex) <- getProcessExitCode p + -- print the error when the sonic-annotator fails + when (ex /= ExitSuccess) + (putStr ("sonic annotator failed to exit normally:\n" ++ cerr)) -------------------------------------------------------------------------------- -- parsing sonic annotator logs @@ -115,13 +130,15 @@ combineLine i (l:ls) = let i' = comb i l in i' : combineLine i' ls -- comb does the actual combining comb :: LogInfo -> LogLine -> LogInfo - comb info (LogFile n) = info {file = n} + -- we do not updated the filename because this file will contain a + -- "downsampled.wav" suffix, and we want the orginal file to appear in the log + comb info (LogFile _) = info -- {file = n} comb info (LogFeat d) = info {feature = d} comb info (LogPerc p) = let t = if p `mod` 5 == 0 then 1 else 0 in info {perc = p, total = total info + t} -- a datatype storing for collecting the information in the log file -data LogInfo = LogInfo {file :: String, +data LogInfo = LogInfo {_file :: String, feature :: String, perc :: Int, total :: Int}
src/HarmTrace/IO/Parse.hs view
@@ -3,7 +3,8 @@ module HarmTrace.IO.Parse ( - parseTree, parseTreeVerb, parseDir + parseTree, parseTreeVerb, parseDir, + parseAnnotation, parseAnnotationVerb, parseAnnotationDir, ) where @@ -34,13 +35,13 @@ -- Symbolic Parsing IO -------------------------------------------------------------------------------- --- parses a string of chords and returns a parse tree with the harmony structure +-- | Parses a string of chords and returns a parse tree with the harmony structure parseTree, parseTreeVerb :: (GTree g) => Grammar g -> [PPOption] -> String -> IO (ParseResult g) parseTree g opts s = do let pr@(ParseResult _ tks _ _ n te pe _) = postProc opts $ string2Piece g s putStrLn ("parsed " ++ show (length tks) ++ " chords into " - ++ show n ++ " ambiguous trees") + ++ show n ++ " analysis tree(s)") if not $ null te then showErrors "tokenizer: " te else putStr "" if not $ null pe then showErrors "parser: " pe @@ -50,14 +51,15 @@ parseTreeVerb g opts s = do let pr@(ParseResult _ tks _ _ n te pe _) = postProc opts $ string2Piece g s putStrLn ("parsed " ++ show (length tks) ++ " chords into " - ++ show n ++ " ambiguous trees") + ++ show n ++ " analysis tree(s)") if not $ null te then mapM_ print te else putStr "" if not $ null pe then mapM_ print pe else putStr "" return pr --- Batch analyzing a directory with chord sequence files with reduced output. +-- | Batch analyses a directory with chord sequence files and presents the +-- user some reduced output. parseDir :: (GTree g) => Grammar g -> [PPOption] -> FilePath -> Maybe FilePath -> IO () parseDir g opts filepath bOut = getDirectoryContents filepath @@ -106,3 +108,80 @@ case bOut of Nothing -> return () Just bf -> encodeFile bf (cs, ts) + +-------------------------------------------------------------------------------- +-- Audio Ground-truth annotations IO +-------------------------------------------------------------------------------- + +parseAnnotation, parseAnnotationVerb :: (GTree g) => Grammar g -> [PPOption] + -> String -> String -> IO (ParseResult g) +-- | Parses a ground-truth chord annotation in standard MIREX format and +-- presents the user some condensed parsing statistics +parseAnnotation g opts k ann = + do let pr@(ParseResult _ tks _ _ n te pe _) = postProc opts $ gt2Piece g k ann + putStrLn ("key: " ++ k) + putStrLn ("parsed " ++ show (length tks)++ " audio chord annotations into " + ++ show n ++ " ambiguous trees") + when (not $ null te) (showErrors "tokenizer: " te) + when (not $ null pe) (showErrors "parser: " pe) + return pr + +-- | Parses a ground-truth chord annotation in standard MIREX format and +-- presents the user somewhat more elaborate parsing statistics +parseAnnotationVerb g opts k ann = + do let pr@(ParseResult _ tks _ _ n te pe _) = postProc opts $ gt2Piece g k ann + putStrLn ("key: " ++ k) + when (not $ null te) (do putStrLn "tokenizer errors:" ; mapM_ print te) + when (not $ null pe) (do putStrLn "parser errors:" ; mapM_ print pe) + putStrLn ("parsed " ++ show (length tks)++ " audio chord annotations into " + ++ show n ++ " ambiguous trees") + return pr + +-- | Parses a directory of annotation files and key description files +-- and prints condensed parsing information to std out +parseAnnotationDir :: GTree a => Grammar a -> [PPOption] -> FilePath -> FilePath + -> IO () +parseAnnotationDir g opts kdir andir = + do ks <- getDirectoryContents kdir + ans <- getDirectoryContents andir + -- prints parse results in one line + let prntParse :: (FilePath,FilePath) -> IO () + prntParse (kfp,anfp) = + do k <- readFile kfp + a <- readFile anfp + printLn . concat $ intersperse "\t" (takeFileName kfp + : (showParseResult . postProc opts $ gt2Piece g k a)) + -- filters .lab files and adds the path + fileFilter :: FilePath -> [FilePath] -> [FilePath] + fileFilter pf = map (combine pf) . filter ((== ".lab") . takeExtension) + + case matchKeyAnn (fileFilter kdir $ reverse ks) + (fileFilter andir $ reverse ans) of + Just x -> do printVersion + printLn ("Filename\tkey\tnrOfTrees\tInsertions\tDeletions" + ++ "\tDelsAtEnd\tTotalErr\tnrOfChords\tTokenizerErr") + mapM_ prntParse x + Nothing -> putStrLn ("the filenames in " ++ kdir + ++ " do not exactly match the ones in " ++ andir) + +-- shows some elements of a ParseResult +showParseResult :: ParseResult a -> [String] +showParseResult (ParseResult k tk _p _han n te pe _pp) = + let pErr = countErrors pe + insert = ins pErr + delete = del pErr + endDel = delEnd pErr + total = insert + delete + endDel + -- key nrOfTrees Insertions Deletions DelsAtEnd TotalErr tokenizerErr + in show k : (map show (n : insert : delete : endDel : total + : length tk : length te :[])) + + +-- Checks if the key and the annotation files all match, if this is the +-- case it will return a paired list of these files +matchKeyAnn :: [FilePath] -> [FilePath] -> Maybe [(FilePath,FilePath)] +matchKeyAnn ks ans = + let match = and $ zipWith eqFileName ks ans + eqFileName :: FilePath -> FilePath -> Bool + eqFileName a b = takeFileName a == takeFileName b + in if match then Just $ zip ks ans else Nothing
src/HarmTrace/IO/Recognise.hs view
@@ -17,127 +17,53 @@ module HarmTrace.IO.Recognise ( - parseAnnotation, parseAnnotationVerb, parseAnnotationDir, - evaluateLabeling, printLabeling, batchLabeling + evaluateLabeling, printLabelling, batchPrintLabelling, batchLabeling + , batchEvaluateKeyFinding, evaluateKeyFinding ) where -- Common IO functions import HarmTrace.IO.Common -import HarmTrace.IO.BasePaths (BasePaths, getOutDir, getFeatDir) -import Constants ( keyStr, chromaStr, beatStr ) +import HarmTrace.IO.BasePaths (BasePaths, getOutDir, getFeatDir, getLogFile) +import Constants ( keyStr, chromaStr, beatStr + , ChordPrintOpts (..) ) -- Parser stuff import Text.ParserCombinators.UU -- Music stuff -import HarmTrace.HarmTrace import HarmTrace.Base.MusicRep import HarmTrace.Models.Jazz.Instances () -import HarmTrace.HAnTree.ToHAnTree -import HarmTrace.IO.Errors import HarmTrace.Base.Parsing +import HarmTrace.Models.Models -- Audio stuff import HarmTrace.Audio.DataParser ( parseChordinoData, parseChromaData - , parseBeatBarData) + , parseBarTimeData) import HarmTrace.Audio.AnnotationParser -import HarmTrace.Audio.Annotate (putSegStats) -import HarmTrace.Audio.Evaluation -import HarmTrace.Audio.ChordTypes +import HarmTrace.Audio.Annotate (putSegStats, mptreeAnnotator) +import HarmTrace.Audio.Evaluation ( relCorrectOverlap, achievScore, avgDistToOne + , chordTriadEq, chordClassEq, majMinEq + , printChordRCO, printRCO, chordChangeRatio ) +import HarmTrace.Base.MusicTime +import HarmTrace.Audio.ChromaChord ( createChordRanks, beatSync ) +import HarmTrace.Audio.Key (getBeatSyncKeyFromChroma) import HarmTrace.IO.FeatExtract import Data.List (genericLength, isSuffixOf, stripPrefix) -- Library modules -import Data.List (sort, intersperse) +import Data.List (sort, unzip6, intersperse) import System.FilePath import System.Directory import System.IO import System.CPUTime import Data.Maybe (isJust, fromJust, isNothing) +import Text.Printf (printf) -- import Control.Parallel.Strategies (parList, rdeepseq, using) -------------------------------------------------------------------------------- --- Audio Ground-truth annotations IO --------------------------------------------------------------------------------- - --- @pedro: I think the three "parse" functions should go to HarmTrace.IO.Parse -parseAnnotation, parseAnnotationVerb :: (GTree g) => Grammar g -> [PPOption] - -> String -> String -> IO (ParseResult g) -parseAnnotation g opts k ann = - do let pr@(ParseResult _ tks _ _ n te pe _) = postProc opts $ gt2Piece g k ann - putStrLn ("key: " ++ k) - putStrLn ("parsed " ++ show (length tks)++ " audio chord annotations into " - ++ show n ++ " ambiguous trees") - if not $ null te then showErrors "tokenizer: " te - else putStr "" - if not $ null pe then showErrors "parser: " pe - else putStr "" - return pr - -parseAnnotationVerb g opts k ann = - do let pr@(ParseResult _ tks _ _ n te pe _) = postProc opts $ gt2Piece g k ann - putStrLn ("key: " ++ k) - if not $ null te then do putStrLn "tokenizer errors:" ; mapM_ print te - else putStr "" - if not $ null pe then do putStrLn "parser errors:" ; mapM_ print pe - else putStr "" - putStrLn ("parsed " ++ show (length tks)++ " audio chord annotations into " - ++ show n ++ " ambiguous trees") - return pr - --- parses a directory of annotation files and key description files --- and prints condenced parsing information to std out -parseAnnotationDir :: GTree a => Grammar a -> [PPOption] -> FilePath -> FilePath - -> IO () -parseAnnotationDir g opts kdir andir = - do ks <- getDirectoryContents kdir - ans <- getDirectoryContents andir - -- prints parse results in one line - let prntParse :: (FilePath,FilePath) -> IO () - prntParse (kfp,anfp) = - do k <- readFile kfp - a <- readFile anfp - printLn . concat $ intersperse "\t" (takeFileName kfp - : (showParseResult . postProc opts $ gt2Piece g k a)) - -- filters .lab files and adds the path - fileFilter :: FilePath -> [FilePath] -> [FilePath] - fileFilter pf = map (combine pf) . filter ((== ".lab") . takeExtension) - - case matchKeyAnn (fileFilter kdir $ reverse ks) - (fileFilter andir $ reverse ans) of - Just x -> do printVersion - printLn ("Filename\tkey\tnrOfTrees\tInsertions\tDeletions" - ++ "\tDelsAtEnd\tTotalErr\tnrOfChords\tTokenizerErr") - mapM_ prntParse x - Nothing -> putStrLn ("the filenames in " ++ kdir - ++ " do not exactly match the ones in " ++ andir) - --- Checks if the key and the annotation files all match, if this is the --- case it will return a paired list of these files -matchKeyAnn :: [FilePath] -> [FilePath] -> Maybe [(FilePath,FilePath)] -matchKeyAnn ks ans = - let match = and $ zipWith eqFileName ks ans - eqFileName :: FilePath -> FilePath -> Bool - eqFileName a b = takeFileName a == takeFileName b - in if match then Just $ zip ks ans else Nothing - --- shows some elements of a ParseResult -showParseResult :: ParseResult a -> [String] -showParseResult (ParseResult k tk _p _han n te pe _pp) = - let pErr = countErrors pe - insert = ins pErr - delete = del pErr - endDel = delEnd pErr - total = insert + delete + endDel - -- key nrOfTrees Insertions Deletions DelsAtEnd TotalErr tokenizerErr - in show k : (map show (n : insert : delete : endDel : total - : length tk : length te :[])) - - --------------------------------------------------------------------------------- --- Audio Data IO +-- Some audiofile specific utitlities -------------------------------------------------------------------------------- -- | Returns True when the argument is an audio feature file based on @@ -161,13 +87,13 @@ chm = stripSuffix chromaStr fp bt = stripSuffix beatStr fp - -- drops a given suffix from a string. It returns nothing if the suffix - -- is not a suffix of the string - stripSuffix :: String -> String -> Maybe String - stripSuffix suf txt - | isJust stp = Just . reverse $ fromJust stp - | otherwise = Nothing - where stp = stripPrefix (reverse suf) (reverse txt) +-- drops a given suffix from a string. It returns nothing if the suffix +-- is not a suffix of the string +stripSuffix :: String -> String -> Maybe String +stripSuffix suf txt + | isJust stp = Just . reverse $ fromJust stp + | otherwise = Nothing + where stp = stripPrefix (reverse suf) (reverse txt) -- maps readAudioFeat over a directory readAudioFeatureDir :: FilePath -> IO [AudioFeat] @@ -185,14 +111,23 @@ -- a chroma, beat and key feature file, 'readAudioFeat' parses all data and returns an 'AudioFeat'. readAudioFeatures :: (FilePath,FilePath,FilePath) -> IO (AudioFeat) readAudioFeatures (cfp,bfp,kfp) = - do dChroma <- readFile cfp + do let bfpid = getAudioFeatureId bfp + cfpid = getAudioFeatureId cfp + kfpid = getAudioFeatureId kfp + when (bfpid /= cfpid && bfpid /= kfpid && isJust bfpid) + (error ("HarmTrace.IO.readAudioFeatures: non matching audio " ++ + "features:\n" ++ show bfp ++ "\n" ++ show cfp ++ "\n" ++ + show kfp ++ "\n")) + + dChroma <- readFile cfp dBeat <- readFile bfp dKey <- readFile kfp - + + -- TODO: use parseDataSafe here let chrm = parseData parseChordinoData dChroma - beats = parseData parseBeatBarData dBeat + beats = parseData parseBarTimeData dBeat keys = parseData parseChromaData dKey - return (AudioFeat chrm beats keys) + return (AudioFeat chrm beats keys (fromJust bfpid)) -- Given one VAMP feature CSV file, HarmTrace will look for the other two -- feature files needed for chord transcription. If the first argument it True @@ -215,11 +150,45 @@ case (cfpExists && bfpExists && kfpExists) of True -> return (Just (cfp,bfp,kfp)) - False -> return Nothing - --- | Evaluluates a single labeling of a piece with a ground truth annotation + False -> + do when verbose (putStrLn ("I did not find 3 " + ++ "matching feature files, I looked for:\n" + ++ cfp ++ "\n" ++ bfp ++ "\n" ++ kfp)) + return Nothing + +-- | Returns True if fp is an audio file +isAudioFile :: FilePath -> Bool +isAudioFile fp = elem (tail $ takeExtension fp) -- all sox file formats :-) + [ "mp3", "wav", "mp4","MP3", "MP4","WAV" -- start with common extensions + , "8svx", "aif", "aifc", "aiff", "aiffc", "al", "amb", "amr-nb", "amr-wb" + , "anb", "au", "avi", "avr", "awb", "cdda", "cdr", "cvs", "cvsd", "cvu", "dat" + , "dvms", "f32", "f4", "f64", "f8", "ffmpeg", "flac", "fssd", "gsm", "gsrt" + , "hcom", "htk", "ima", "ircam", "la", "lpc", "lpc10", "lu", "m4a", "m4b" + , "maud", "mp2", "mpg", "nist", "ogg", "prc", "raw", "s1" + , "s16", "s2", "s24", "s3", "s32", "s4", "s8", "sb", "sds", "sf", "sl", "smp" + , "snd", "sndfile", "sndr", "sndt", "sou", "sox", "sph", "sw", "txw", "u1" + , "u16", "u2", "u24", "u3", "u32", "u4", "u8", "ub", "ul", "uw", "vms", "voc" + , "vorbis", "vox", "wavpcm", "wmv", "wv", "wve", "xa" + -- and the same extensions in uppercase + -- TODO: use equalFilePath + , "8SVX", "AIF", "AIFC", "AIFF", "AIFFC", "AL", "AMB", "AMR-NB", "AMR-WB" + , "ANB", "AU", "AVI", "AVR", "AWB", "CDDA", "CDR", "CVS", "CVSD", "CVU", "DAT" + , "DVMS", "F32", "F4", "F64", "F8", "FFMPEG", "FLAC", "FSSD", "GSM", "GSRT" + , "HCOM", "HTK", "IMA", "IRCAM", "LA", "LPC", "LPC10", "LU", "M4A", "M4B" + , "MAUD", "MP2", "MPG", "NIST", "OGG", "PRC", "RAW", "S1" + , "S16", "S2", "S24", "S3", "S32", "S4", "S8", "SB", "SDS", "SF", "SL", "SMP" + , "SND", "SNDFILE", "SNDR", "SNDT", "SOU", "SOX", "SPH", "SW", "TXW", "U1" + , "U16", "U2", "U24", "U3", "U32", "U4", "U8", "UB", "UL", "UW", "VMS", "VOC" + , "VORBIS", "VOX", "WAVPCM", "WMV", "WV", "WVE", "XA" + ] + +-------------------------------------------------------------------------------- +-- Evaluating Audio Chord Transcription +-------------------------------------------------------------------------------- + +-- | Evaluates a single labeling of a piece with a ground truth annotation -- visually. -evaluateLabeling :: (Maybe [TimedData Key] -> AudioFeat -> ChordBeatAnnotation) +evaluateLabeling :: (Maybe [TimedData Key] -> AudioFeat -> ChordAnnotation) -> Bool -> FilePath -> FilePath -> Maybe FilePath -> IO Double evaluateLabeling annotator prnt gtfp featfp keyfp = do @@ -231,167 +200,302 @@ (True, Nothing,True) -> do printLn ("using key finding") putSegStats Nothing af - printRelCorrectOverlap (annotator Nothing) af gt + printChordRCO (annotator Nothing) (getBeatSyncKeyFromChroma af) af gt (True, Nothing,False) -> - do return (relCorrectOverlap gt (dumpBeats $ annotator Nothing af)) + do return (relCorrectOverlap chordTriadEq gt (dropProb $ annotator Nothing af)) (True, Just k ,True) -> do key <- readAndParseKeyAnn k printLn ("using groundTruth key annotation: " ++ show key) putSegStats (Just key) af - printRelCorrectOverlap (annotator (Just key)) af gt + printChordRCO (annotator (Just key)) key af gt (True, Just k ,False) -> do key <- readAndParseKeyAnn k - return (relCorrectOverlap gt (dumpBeats $ annotator (Just key) af)) + return (relCorrectOverlap chordTriadEq gt (dropProb $ annotator (Just key) af)) (False, _, _) -> return (-1) + +-- | Evaluates the keyfinding for a single audio feature set. +evaluateKeyFinding :: Bool -> FilePath -> FilePath -> IO () +evaluateKeyFinding prnt featfp kdir = do + + maf <- findAudioFeatures True featfp + af <- readAudioFeatures (fromJust maf) + gt <- readAndParseKeyAnn (kdir </> takeFileName (getAudioFeatId af) <.> "lab") + let keys = getBeatSyncKeyFromChroma af + + case (prnt) of + True -> void (printRCO printKeyEq gt keys) + False -> putStrLn (show $ relCorrectOverlap (==) gt keys) + + where printKeyEq :: Key -> Key -> IO (Bool) + printKeyEq a b = do putStrLn (show a ++ " == " ++ show b) + return (a == b) + + -- | Given a ground truth directory and an data directory (containing exactly -- 3 times as much files as the gt directory) all files will be labeled and -- the relative correct overlap wil be corrected an presented to the user -batchLabeling :: (Maybe [TimedData Key] -> AudioFeat -> ChordBeatAnnotation) +batchLabeling :: ChordPrintOpts -> Maybe FilePath -> + (Maybe [TimedData Key] -> AudioFeat -> ChordAnnotation) -> FilePath -> FilePath -> Maybe FilePath -> IO () -batchLabeling annotator gtfp audiofp keyfp = do - gt <- getDirectoryContents gtfp - af <- readAudioFeatureDir audiofp +batchLabeling opts mout annotator gtdir audiodir mkeydir = do - -- parse the key annotations [Maybe [TimedData Key]] - mKeys <- case keyfp of - Nothing -> - do printLn "using key finding" - return $ repeat Nothing - (Just fp) -> - do printLn "using key ground-truth annotations" - kfs <- getDirectoryContents fp - -- better to move also the reading etc. to evalR - ks <- mapM (\x ->readAndParseKeyAnn (fp </> x)) - (sort $ filter ((== ".lab") . takeExtension ) kfs) - return $ map Just ks + -- inform the user wheter we are using ground-truth key annotations or not + maybe (putStrLn "using key finding") + (const $ putStrLn "using key ground-truth annotations") mkeydir + + printVersion -- print the current HarmTrace version + printLn ("file\trun time (seconds)\tRCO triad (maj/min)\tmaj/min errors" ++ + "\tRCO chord class (maj/min/dom/dim)\tMaximum achievable" ++ + "\tchord change ratio") + + result <- readAudioFeatureDir audiodir >>= mapM evalR + + let -- result' = result `using` parList rdeepseq + totNrSongs = genericLength result + printAVG r = show (sum r / totNrSongs) + (triad, majMinErrs, chordClass, maxAchieve, cChangeRat, runTimes) = unzip6 result + -- print some averaged statistics + putStrLn ("average RCO triad level: " ++ printAVG triad) + putStrLn ("average MajMin error ratio: " ++ printAVG majMinErrs) + putStrLn ("average RCO chord class level: " ++ printAVG chordClass) + putStrLn ("average maximum achievable score: " ++ printAVG maxAchieve) + putStrLn ("average chord change ratio (distance to 1.0): " + ++(show . avgDistToOne $ cChangeRat)) + putStrLn ("total running time: " ++ show (sum runTimes )) + + where + -- evaluates one set of audio features, prints the results to the std. out + -- and returns a tuple with the relative correct overlap and the run time + evalR :: AudioFeat -> IO(Double, Double, Double, Double, Double, Double) + evalR af@(AudioFeat crm bts _ afid) = do + + let gtfp = takeFileName afid <.> "lab" + gt <- readAnnotation (gtdir </> gtfp) + + -- if a directory with ground-truth key annotations has been provided + -- we use this directory to find a file with the same name in this + -- directory and parse it + mkey <- case mkeydir of + Just kfp -> readAndParseKeyAnn (kfp </> gtfp) >>= return . Just + Nothing -> return Nothing + + let timedAnno = annotator mkey af + annotation = dropProb timedAnno + resultTriad = relCorrectOverlap chordTriadEq gt annotation + resultClass = relCorrectOverlap chordClassEq gt annotation + majMinErr = relCorrectOverlap majMinEq gt annotation + chordChange = chordChangeRatio chordTriadEq gt annotation + ccList = fmap (fmap (fmap chordLab)) . createChordRanks + $ beatSync bts crm + resultMax = achievScore gt ccList + exec = seq annotation (return ()) + + -- TODO: refactor into seperate function + -- log the execution time + t1 <- getCPUTime + exec -- evaluate the automatic annotation + t2 <- getCPUTime + + -- optionally write the chords to an output file (if an output directory + -- has been specified) + when (isJust mout) + (writeAnnotationNoLog opts (fromJust mout) afid timedAnno) + + let runtime = fromIntegral (t2 - t1) / (1000000000000 :: Double) + + -- print the information to the user + printLn (gtfp ++ '\t' : pPrintDoubles [runtime, resultTriad + , majMinErr, resultClass, resultMax, chordChange]) + return (resultTriad, majMinErr, resultClass, resultMax, chordChange, runtime) - -- this is really hacky, but there is no zipWith3M - let files = zip (sort $ filter ((== ".lab") . takeExtension ) gt) mKeys +-- | Pretty prints a list of doubles separated by tabs. +pPrintDoubles :: [Double] -> String +pPrintDoubles = concat . intersperse "\t" . map (printf "%.3f") + +-- | reads a single chord annotation +readAnnotation :: FilePath -> IO [TimedData ChordLabel] +readAnnotation fp = do f <- readFile fp + return (parseDataSafe parseAnnotationData f) + +-- | Given a 'FilePath' reads and returns a ground-truth 'Key' Annotation. +readAndParseKeyAnn :: FilePath -> IO [TimedData Key] +readAndParseKeyAnn keyfp = do key <- readFile keyfp + return $ parseDataSafe parseKeyAnnotationData key + +-- | Evaluates key finding based on a directory of audio features +batchEvaluateKeyFinding :: FilePath -> FilePath -> IO () +batchEvaluateKeyFinding audiodir keydir = do + putStrLn "Evaluating key finding" + printVersion -- print the current HarmTrace version - printVersion - printLn "file\trun time (seconds)\trelative correct overlap" - res <- zipWithM evalR files af - -- let res' = res `using` parList rdeepseq - printLn ("average: " ++ show (sum (fmap fst res) / genericLength res)) + result <- readAudioFeatureDir audiodir >>= mapM evalKey + + let totNrSongs = genericLength result + (roc,runTimes) = unzip result + -- print some averaged statistics + putStrLn ("average RCO: " ++ show (sum roc / totNrSongs )) + putStrLn ("total running time: " ++ show (sum runTimes )) where - evalR :: (FilePath, Maybe [TimedData Key]) -> AudioFeat -> IO(Double, Float) - -- evalR _ Nothing = error "evalR: Nothing" - evalR (fp, maybeKey) af = do - gt <- readAnnotation (gtfp </> fp) - let result = relCorrectOverlap gt (dumpBeats $ annotator maybeKey af) + + -- Evaluates a the key annotation for a single AudioFeature set + evalKey :: AudioFeat -> IO (Double, Float) + evalKey af = do + + let gtfp = takeFileName (getAudioFeatId af) <.> "lab" + gt <- readAndParseKeyAnn (keydir </> gtfp) + + let result = relCorrectOverlap (==) gt (getBeatSyncKeyFromChroma af) exec = seq result (return ()) + + -- log the execution time t1 <- getCPUTime - exec + exec -- evaluate the automatic annotation t2 <- getCPUTime - let runtime = fromIntegral (t2 - t1) / (1000000000000 :: Float) - printRes fp (result, runtime) + let runtime = fromIntegral (t2 - t1) / (1000000000000 :: Float) + + -- print the information to the user + printLn (gtfp ++ ":\t" ++ showFloat runtime ++ '\t' : show result ) return (result, runtime) + - printRes :: FilePath -> (Double, Float) -> IO () - printRes fp (r,t) = printLn (fp ++ ":\t" ++ showFloat t ++ '\t' : show r) - >> hFlush stdout +-------------------------------------------------------------------------------- +-- Analysing and printing chord labels +-------------------------------------------------------------------------------- +batchPrintLabelling :: ChordPrintOpts -> BasePaths + -> (Maybe [TimedData Key] -> AudioFeat -> ChordAnnotation) + -> FilePath -> IO () +batchPrintLabelling opts dirs annotator fp = + do fl <- readFile fp + mapM_ (printLabelling opts dirs annotator) (lines fl) + -- takes a set of features or an audio file and writes the chords to a file -- the process is also logged in a logfile to keep track of the process -- (see HarmTrace.IO.FeatExtract) -printLabeling :: BasePaths - -> (Maybe [TimedData Key] -> AudioFeat -> ChordBeatAnnotation) - -> FilePath -> IO () -printLabeling dirs annotator fp = do +printLabelling :: ChordPrintOpts -> BasePaths + -> (Maybe [TimedData Key] -> AudioFeat -> ChordAnnotation) + -> FilePath -> IO () +printLabelling opts dirs annotator fp = do let -- we use the same logfile for adding harmtrace status - logf = getOutDir dirs </> takeFileName fp <.> "extract.log" - out = getOutDir dirs </> takeFileName fp <.> "chords.txt" + logf = getLogFile dirs fp + out = getOutDir dirs </> takeFileName fp <.> "chords.txt" ffp = getFeatDir dirs </> dropExtension (takeFileName fp) ++ chromaStr - + -- check (silently) if features have been extracted earlier - hasFt <- findAudioFeatures True ffp + hasFt <- findAudioFeatures False ffp case (isAudioFile fp, hasFt) of -- We found an audio file and no previously extracted features: extract them (True, Nothing) -> do -- extract features maf <- extractFeatures dirs fp + hdl <- openFile logf AppendMode if isNothing maf - then appendFile logf (fp ++";ERROR;0;99\n") - else do appendFile logf (fp ++ ";harmtrace;1;80\n") - -- print chords - readAndPrint out maf annotator - appendFile logf (fp++";harmtrace;100;99\n") - appendFile logf (fp++";DONE;100;100\n") + then harmTraceLogFinal hdl fp "error" + else do readAndPrint opts hdl fp out maf annotator + harmTraceLogFinal hdl fp "done" + hClose hdl -- We found an audio file, but also found matching features - (True,Just maf) -> do -- appendFile logf (fp ++ ";Initialize;1;1\n") - writeFile logf (fp ++ ";harmtrace;1;80\n") - readAndPrint out (Just maf) annotator - appendFile logf (fp ++ ";harmtrace;100;99\n") - appendFile logf (fp ++ ";DONE;100;100\n") + (True,Just _ ) -> do putStrLn ("I found matching feature files for " ++ fp) + hdl <- openFile logf WriteMode + readAndPrint opts hdl fp out hasFt annotator + harmTraceLogFinal hdl fp "done" + hClose hdl -- No audio, but one feature file: look for all feature files - (False,_ ) -> do maf <- findAudioFeatures True fp - readAndPrint out maf annotator + (False,_ ) -> do maf <- findAudioFeatures True fp -- N.B. will print + hdl <- openFile logf WriteMode + if isNothing maf + then harmTraceLogFinal hdl fp "error" + else do readAndPrint opts hdl fp out maf annotator + harmTraceLogFinal hdl fp "done" + hClose hdl + -- reads the features and prints the chords -readAndPrint :: FilePath -> Maybe (FilePath, FilePath, FilePath) - -> (Maybe [TimedData Key] -> AudioFeat -> ChordBeatAnnotation) +readAndPrint :: ChordPrintOpts -> Handle -> FilePath -> FilePath + -> Maybe (FilePath, FilePath, FilePath) + -> (Maybe [TimedData Key] -> AudioFeat -> ChordAnnotation) -> IO () -readAndPrint _ Nothing _ = return () -readAndPrint out (Just af) annotator = +readAndPrint _ _ _ _ Nothing _ = return () +readAndPrint opts logh src out (Just af) annotator = do feat <- readAudioFeatures af - writeAnnotation out . segPerBeat (getBeats feat) $ annotator Nothing feat - --- returns True if fp is an audio file -isAudioFile :: FilePath -> Bool -isAudioFile fp = elem (tail $ takeExtension fp) -- all sox file formats :-) - [ "8svx", "aif", "aifc", "aiff", "aiffc", "al", "amb", "amr-nb", "amr-wb" - , "anb", "au", "avi", "avr", "awb", "cdda", "cdr", "cvs", "cvsd", "cvu", "dat" - , "dvms", "f32", "f4", "f64", "f8", "ffmpeg", "flac", "fssd", "gsm", "gsrt" - , "hcom", "htk", "ima", "ircam", "la", "lpc", "lpc10", "lu", "m4a", "m4b" - , "maud", "mp2", "mp3", "mp4", "mpg", "nist", "ogg", "prc", "raw", "s1" - , "s16", "s2", "s24", "s3", "s32", "s4", "s8", "sb", "sds", "sf", "sl", "smp" - , "snd", "sndfile", "sndr", "sndt", "sou", "sox", "sph", "sw", "txw", "u1" - , "u16", "u2", "u24", "u3", "u32", "u4", "u8", "ub", "ul", "uw", "vms", "voc" - , "vorbis", "vox", "wav", "wavpcm", "wmv", "wv", "wve", "xa" - ] + -- Compute the length of the input list of beats, to be used to provide + -- progress information `writeAnnotation` + let len = genericLength . getBeats $ feat + writeAnnotation logh len src out . expandPerBeat + . applyPrintOpts opts $ annotator Nothing feat --- reads an annotation -readAnnotation :: FilePath -> IO ChordAnnotation -readAnnotation fp = do f <- readFile fp - return (parseData parseAnnotationData f) - -readAndParseKeyAnn :: FilePath -> IO [TimedData Key] -readAndParseKeyAnn keyfp = do key <- readFile keyfp - return $ parseData parseKeyAnnotationData key +-- | writes an annotation to a specific file +writeAnnotation :: Handle -> Float -> FilePath -> FilePath + -> ChordAnnotation -> IO () +writeAnnotation hlog len src out ca = + do hout <- openFile out ReadWriteMode -- perhaps use WriteMode? + let step = 100 / len + -- showLn a = show a ++ "\n" + mapWithStatus (hPutStr hout . showTDPC) (harmTraceLogger hlog src) step ca + hClose hout --- writes an annotation to a specific file -writeAnnotation :: FilePath -> ChordBeatAnnotation -> IO () -writeAnnotation f ca = do hdl <- openFile f ReadWriteMode - mapM_ (hPutStr hdl . show) ca - hClose hdl +-- | Does the same thing as writeAnnotation (storing a chord transcription), +-- but without the logging. +writeAnnotationNoLog :: ChordPrintOpts -> FilePath -> FilePath + -> [TimedData ProbChord] -> IO () +writeAnnotationNoLog opts dir srcfp ca = + do let outfp = dir </> (takeFileName srcfp) <.> "chords.txt" + exists <- doesDirectoryExist dir + if exists + then writeFile outfp . concatMap showTDPC + . expandPerBeat . applyPrintOpts opts $ ca + else hPutStr stderr (dir ++ " does not exits") + +-- Shows chords in such a way that Chordify can understand them +showTDPC :: Show a => TimedData a -> String +showTDPC td = concat . intersperse ";" $ [ show . getBeat $ td + , show . getData $ td + , show . onset $ td + ,(show . offset $ td) ++ "\n" ] --- segment a chord annotation per beat --- ChordAnnotation = [TimedData ChordLabel] and BeatTrackerData = [NumData] -segPerBeat :: BeatBarTrackData -> ChordBeatAnnotation -> ChordBeatAnnotation -segPerBeat bts cds = segment (start : bts) cds where - start = BeatBar (0,prevBeat . snd . beatBar $ head bts) +-- like mapM_, but also print progress information +mapWithStatus :: (a -> IO b) -> (Float -> IO()) -> Float -> [a] -> IO () +mapWithStatus f logger step l = + let statusEval e b cur = logger cur >> f e >> (b (step + cur)) + in foldr statusEval (const (return ())) l 0 + +-- Writes a harmtrace status indicator to a log file in the same format +-- as 'HarmTrace.FeatExtract.FeatExtract'. +harmTraceLogger :: RealFrac a => Handle -> FilePath -> a -> IO () +harmTraceLogger hdl f step = + do let fstep = floor step :: Integer + total = 70 + (fstep * 30 `div` 100) + hPutStr hdl (f ++ ";harmtrace;" ++ show fstep ++ ';' : show total ++ "\n") + +-- Writes a final (100 percent) status message, should be "done" or "error" +harmTraceLogFinal :: Handle -> FilePath -> String -> IO () +harmTraceLogFinal hdl f str = + hPutStr hdl (f ++ ";" ++ str ++";100;100\n") + +applyPrintOpts :: ChordPrintOpts -> ChordAnnotation -> ChordAnnotation +applyPrintOpts PrintMajMin cs = map (updateTPChord toMajMinChord) cs +applyPrintOpts PrintChordClass cs = cs + +-- Expands the previously merged 'TimedData ProbChord's +-- TODO perhaps move to HarmTrace.Base.MusicTime because of it general structure +expandPerBeat :: [TimedData a] -> [TimedData a] +expandPerBeat = concatMap expand where - -- takes a list of beats and a list of chords (where chords can span multiple - -- beats) and returns a list of beat synchronised list chords (every chord - -- annoation has a one beat duration) - segment :: Show a => [BeatBar] -> [BeatTimedData a] -> [BeatTimedData a] - segment _ [] = [] - segment [b] [c] - | on < offset c = [setOnset c on] - | otherwise = [] -- N.B. includes on == offset b! - where on = fst $ beatBar b - segment (a : b : bs) (c:cs) - | onset c <= on && offset c >= off - = BeatTimedData (getData c) bt on off : segment (b:bs) (c:cs) - | otherwise = segment (a : b : bs) cs - where (on, bt) = beatBar a - off = fst $ beatBar b - -- should not happen - segment [] c = error ("we ran out of beats! Chords left: " ++ show c ) - segment [b] c = error ("Asynchroneous beat (" ++ show b ++ - ") and chords: " ++ show c ) + expand :: TimedData a -> [TimedData a] + expand td = let ts = getTimeStamps td + in zipWith3 timedDataBT (repeat . getData $ td) ts (tail ts) +-------------------------------------------------------------------------------- +-- for testing in ghci +-------------------------------------------------------------------------------- + +mptree :: FilePath -> IO [TimedData ProbChord] +mptree f = do maf <- findAudioFeatures True f + case maf of + Nothing -> error ("invalid audiofeature file " ++ show f) + Just af -> do readAudioFeatures af >>= + return . mptreeAnnotator (GrammarEx Pop) Nothing
+ src/HarmTrace/IO/Wave.hs view
@@ -0,0 +1,48 @@+ +-------------------------------------------------------------------------------- +-- | +-- Module : HarmTrace.IO.Wave +-- Copyright : (c) 2010-2012 Universiteit Utrecht, 2012 University of Oxford +-- License : GPL3 +-- +-- Maintainer : bash@cs.uu.nl, jpm@cs.ox.ac.uk +-- Stability : experimental +-- Portability : non-portable +-- +-- Summary: Read .wav files into hmatrix. +-------------------------------------------------------------------------------- + +module HarmTrace.IO.Wave ( + + readWave, waveToVector + + ) where + + +-- HCodecs +import Codec.Wav ( importFile ) +import Data.Audio ( Audio, sampleData, convert ) + +-- hmatrix +import Data.Packed.Vector ( Vector, fromList ) + +-- Other +import Data.Array.Unboxed ( elems ) +import Data.Int ( Int32 ) + +-------------------------------------------------------------------------------- +-- Reading wave files +-------------------------------------------------------------------------------- + +-- | Reads a wave file from disk into a vector +readWave :: FilePath -> IO (Vector Double) +readWave fp = do r <- importFile fp + case r of + Left err -> putStrLn ("Error reading .wav file: " ++ err) + >> return (fromList []) -- return empty Vector + Right a -> return (waveToVector a) + +-- | Converts HCodecs' internal Audio representation to an hmatrix Vector. +-- Innefficient at the moment, because of list conversions. +waveToVector :: Audio Int32 -> Vector Double +waveToVector = fromList . elems . convert . sampleData
src/HarmTrace/Matching/HChord.hs view
@@ -1,7 +1,7 @@ module HarmTrace.Matching.HChord (HChord, Sim, toHChords) where import HarmTrace.Base.MusicRep -import HarmTrace.Tokenizer.Tokens +import HarmTrace.Models.ChordTokens import HarmTrace.Matching.Sim import HarmTrace.HAnTree.HAn import HarmTrace.HAnTree.Tree @@ -21,7 +21,8 @@ instance Show HChord where show (HChord r ct fc pr tr) = show fc ++ ':' : show pr ++ ':' : show tr - ++ ':' : show (scaleDegrees !! r) ++ show ct + ++ ':' : show (toScaleDegree (Key (Note Nothing C) MajMode) (toRoot r)) + ++ show ct toHChords :: Tree HAn -> [HChord] toHChords t = getHAn undefinedHChord t
src/HarmTrace/Matching/Sim.hs view
@@ -4,7 +4,7 @@ import HarmTrace.HAnTree.HAn import HarmTrace.HAnTree.Tree import HarmTrace.Base.MusicRep -import HarmTrace.Tokenizer.Tokens (ChordToken) +import HarmTrace.Models.ChordTokens (ChordToken) -------------------------------------------------------------------------------- -- A class for representing numerical similarity between datatypes
src/HarmTrace/Matching/SimpleChord.hs view
@@ -14,10 +14,13 @@ | otherwise = -1 instance Show SimChord where - show (SimChord r sh) = show (scaleDegrees !! r) + show (SimChord r sh) = show (toScaleDegree (Key (Note Nothing C) MajMode) + (toRoot r)) ++ if sh == MajMode then "" else "m" toSimChords :: ChordDegree -> [SimChord] -toSimChords (Chord r sh _add _loc d) = - -- replicate (d `div1` 2) (SimChord (diaDegToSemi r) (toMode sh)) - replicate d (SimChord (toSemitone r) (toMode sh)) +toSimChords c = replicate (duration c) (SimChord (toSemitone . chordRoot $ c) m) + + where m = case toTriad c of + NoTriad -> MinMode -- this is odd, but this is how it was.... + t -> toMode t
+ src/HarmTrace/Models/ChordTokens.hs view
@@ -0,0 +1,77 @@+{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE EmptyDataDecls #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE GADTs #-} +{-# OPTIONS_GHC -Wall -fno-warn-orphans #-} +module HarmTrace.Models.ChordTokens ( ChordToken (..) + , PieceToken (..) + , ParseStatus (..) + , toKeyRelTok + ) where + +import HarmTrace.Base.MusicRep +import HarmTrace.Base.Instances () +import Data.Binary +import GHC.Generics (Generic) + +-------------------------------------------------------------------------------- +-- Tokens for parsing chords +-------------------------------------------------------------------------------- + +-- merged Chords that will be presented to the parser +data ChordToken = ChordToken { root :: ScaleDegree + , classType :: ClassType + , chords :: [ChordLabel] + , status :: ParseStatus + , chordNumReps :: Int + , dur :: Int -- duration + } deriving Generic + +data ParseStatus = NotParsed | Parsed | Deleted | Inserted + deriving (Eq, Show, Generic) + +-- a datatype to store a tokenized chords +data PieceToken = PieceToken Key [ChordToken] + +-- (previously called mergeDups) +-- TODO rewrite function +-- | Merges duplicate chords and transforms absolute chord labels into key +-- relative tokens that can be parsed by the HarmTrace model +toKeyRelTok :: Key -> [ChordLabel] -> [ChordToken] +toKeyRelTok k (c@(Chord r _sh _add _loc d):cs) = toKeyRelTok' k + (ChordToken (toScaleDegree k r) (toClassType c) [c] NotParsed 1 d) cs +toKeyRelTok _key [] = [] +toKeyRelTok' :: Key -> ChordToken -> [ChordLabel] -> [ChordToken] +toKeyRelTok' _k p [] = [p] +toKeyRelTok' k p@(ChordToken deg clss cs' _stat n d1) (c@(Chord r _sh _a _l d2):cs) + | deg == deg2 && clss == clss2 = + toKeyRelTok' k (ChordToken deg clss (cs' ++ [c]) NotParsed (n+1) (d1+d2)) cs + | otherwise = p : toKeyRelTok' k (ChordToken deg2 clss2 [c] NotParsed 1 d2) cs + where clss2 = toClassType c + deg2 = toScaleDegree k r + + +-------------------------------------------------------------------------------- +-- Instances for Chord Tokens +-------------------------------------------------------------------------------- +instance Eq ChordToken where + (ChordToken sd clss _cs stat _n _d) == (ChordToken sd2 clss2 _cs2 stat2 _n2 _d2) + = sd == sd2 && clss == clss2 && stat == stat2 + +instance Show ChordToken where + show (ChordToken sd clss _cs Inserted _n _d) = show sd ++ show clss++"[Inserted]" + show (ChordToken sd clss cs Deleted _n _d) = + show sd ++ show clss ++ "[Deleted" ++ showChords cs ++ "]" + show (ChordToken sd clss cs _ _n d) = show sd ++ show clss ++ '_' : show d + ++ showChords cs +showChords :: [ChordLabel] -> String +showChords = concatMap (\x -> '[' : show x ++ "]") + + +-------------------------------------------------------------------------------- +-- Binary instances +-------------------------------------------------------------------------------- + +instance Binary ChordToken +instance Binary ParseStatus
src/HarmTrace/Models/Jazz/Instances.hs view
@@ -40,8 +40,8 @@ import HarmTrace.HAnTree.Tree import HarmTrace.HAnTree.ToHAnTree import HarmTrace.HAnTree.HAn -import HarmTrace.Tokenizer.Tokens as CT -import HarmTrace.Base.TypeLevel +import HarmTrace.Models.ChordTokens as CT +import HarmTrace.Models.TypeLevel import HarmTrace.Base.MusicRep -- Library modules @@ -128,17 +128,22 @@ parseG = pChord deg clss where deg = toDegree (undefined :: deg) clss = toClass (undefined :: clss) - - + +-- generic ad-hoc parser that forms the bridge between the type-level and +-- value-level representation pChord :: ScaleDegree -> ClassType -> PMusic (Surface_Chord deg clss (Su n)) -- Do not parse Imp degrees pChord (Note _ Imp) _clss = empty -- General case pChord deg clss = setStatus <$> pSatisfy recognize insertion where - recognize (ChordToken deg' clss' _cs _st _n _d) = deg == deg' && clss == clss' - setStatus (ChordToken r t l NotParsed n d) - = Surface_Chord (ChordToken r t l Parsed n d) - setStatus c = Surface_Chord c + {-# INLINE recognize #-} + recognize ct = deg == root ct && clss == classType ct + + {-# INLINE setStatus #-} + setStatus c = case status c of + NotParsed -> Surface_Chord c {status = Parsed} + _ -> Surface_Chord c + insertion = Insertion "ChordToken" (ChordToken deg clss [] CT.Inserted 1 0) 5 --------------------------------------------------------------------------------
src/HarmTrace/Models/Jazz/Model.hs view
@@ -23,10 +23,10 @@ module HarmTrace.Models.Jazz.Model where -import HarmTrace.Base.TypeLevel +import HarmTrace.Models.TypeLevel import HarmTrace.Base.MusicRep -import HarmTrace.Tokenizer.Tokens +import HarmTrace.Models.ChordTokens import Language.Haskell.TH.Syntax (Name) --------------------------------------------------------------------------------
src/HarmTrace/Models/Parser.hs view
@@ -19,7 +19,11 @@ -- Summary: Semi-generic parser for chords -------------------------------------------------------------------------------- -module HarmTrace.Models.Parser where +module HarmTrace.Models.Parser ( + ParseG (..) + , parseGdefault + , PMusic + ) where -- Parser stuff @@ -30,13 +34,14 @@ import Generics.Instant.Base as G -- Music stuff -import HarmTrace.Tokenizer.Tokens +import HarmTrace.Models.ChordTokens -------------------------------------------------------------------------------- -- The generic part of the parser -------------------------------------------------------------------------------- +-- | a type synoniome for a harmonic analysis of a piece of music type PMusic a = P (Str ChordToken [ChordToken] Int) a class Parse' f where @@ -77,6 +82,7 @@ instance (ParseG a) => ParseG (Maybe a) where parseG = pMaybe parseG +-- | default generic parser parseGdefault :: (Representable a, Parse' (Rep a)) => PMusic a -- parseGdefault = fmap (to . head) (amb parse') -- Previously we used:
src/HarmTrace/Models/Pop/Instances.hs view
@@ -27,8 +27,8 @@ import HarmTrace.HAnTree.Tree import HarmTrace.HAnTree.ToHAnTree import HarmTrace.HAnTree.HAn -import HarmTrace.Tokenizer.Tokens as CT -import HarmTrace.Base.TypeLevel +import HarmTrace.Models.ChordTokens as CT +import HarmTrace.Models.TypeLevel import HarmTrace.Base.MusicRep -- Library modules @@ -101,13 +101,6 @@ -- Ad-hoc cases for Surface_Chord instance ParseG (Surface_Chord deg clss Ze) where parseG = empty - --- instance ( ToDegree deg - -- , ParseG (Surface_Chord (MinThird deg) DimClass n) - -- ) => ParseG (Surface_Chord deg DimClass (Su n)) where - -- parseG = Dim_Chord_Trns <$> parseG - -- <|> pChord deg DimClass - -- where deg = toDegree (undefined :: deg) -- all chords instance ( ToDegree deg, ToClass clss @@ -116,18 +109,23 @@ where deg = toDegree (undefined :: deg) clss = toClass (undefined :: clss) - +-- generic ad-hoc parser that forms the bridge between the type-level and +-- value-level representation pChord :: ScaleDegree -> ClassType -> PMusic (Surface_Chord deg clss (Su n)) -- Do not parse Imp degrees pChord (Note _ Imp) _clss = empty -- General case pChord deg clss = setStatus <$> pSatisfy recognize insertion where - recognize (ChordToken deg' clss' _cs _st _n _d) = deg == deg' && clss == clss' - setStatus (ChordToken r t l NotParsed n d) - = Surface_Chord (ChordToken r t l Parsed n d) - setStatus c = Surface_Chord c + {-# INLINE recognize #-} + recognize ct = deg == root ct && clss == classType ct + + {-# INLINE setStatus #-} + setStatus c = case status c of + NotParsed -> Surface_Chord c {status = Parsed} + _ -> Surface_Chord c + insertion = Insertion "ChordToken" (ChordToken deg clss [] CT.Inserted 1 0) 5 - + -------------------------------------------------------------------------------- -- The non-generic part of the GTree wrapper --------------------------------------------------------------------------------
src/HarmTrace/Models/Pop/Model.hs view
@@ -10,10 +10,10 @@ module HarmTrace.Models.Pop.Model where -import HarmTrace.Base.TypeLevel +import HarmTrace.Models.TypeLevel import HarmTrace.Base.MusicRep -import HarmTrace.Tokenizer.Tokens +import HarmTrace.Models.ChordTokens import Language.Haskell.TH.Syntax (Name) --------------------------------------------------------------------------------
+ src/HarmTrace/Models/TypeLevel.hs view
@@ -0,0 +1,81 @@+{-# LANGUAGE EmptyDataDecls #-} +{-# LANGUAGE KindSignatures #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE UndecidableInstances #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE DeriveDataTypeable #-} + +module HarmTrace.Models.TypeLevel ( + Su, Ze + , T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10 + , T11, T12, T13, T14, T15, T16, T17, T18, T19, T20 + , ToNat(..) + ) where + +import Data.Typeable + + +-- Type level peano naturals +data Su :: * -> * deriving Typeable +data Ze :: * deriving Typeable + +-- Some shorthands +type T0 = Ze +type T1 = Su T0 +type T2 = Su T1 +type T3 = Su T2 +type T4 = Su T3 +type T5 = Su T4 +type T6 = Su T5 +type T7 = Su T6 +type T8 = Su T7 +type T9 = Su T8 +type T10 = Su T9 +type T11 = Su T10 +type T12 = Su T11 +type T13 = Su T12 +type T14 = Su T13 +type T15 = Su T14 +type T16 = Su T15 +type T17 = Su T16 +type T18 = Su T17 +type T19 = Su T18 +type T20 = Su T19 + +class ToNat n where + toNat :: n -> Int + +instance ToNat Ze where toNat _ = 0 +instance (ToNat n) => ToNat (Su n) where toNat _ = 1 + toNat (undefined :: n) +{- +-- Below is some experimentation... + +-- A degree has a distance to root in semi-tones (n in T0..T11) and a +-- class (major or minor) +data Degree n cls + +-- Transposing is a bit like addition... +type family Transpose m n +-- ... but we normalize at the end to stay within T0..T11 +type instance Transpose m T0 = Norm m +type instance Transpose m (Su n) = Transpose (Su m) n + +-- Normalizing is the same as subtracting T12, but only if we can. Else we keep +-- the type unchanged. +type Norm m = Sub m T12 m + +-- Subtraction with an extra type for failure +type family Sub m n fail +-- Inductive case +type instance Sub (Su m) (Su n) fail = Sub m n fail +-- Base case, subtraction succeeded +type instance Sub m T0 fail = m +-- Base case, subtraction failed +type instance Sub T0 (Su n) fail = fail + +-- A secondary dominant is a transposition by 7 semi-tones +type SD deg = Transpose deg T7 + +-- A tritone substitution is a transposition by 6 semi-tones +type TS deg = Transpose deg T6 +-}
− src/HarmTrace/Tokenizer/Tokenizer.hs
@@ -1,155 +0,0 @@-{-# OPTIONS_GHC -Wall -fno-warn-orphans #-} -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE FlexibleContexts #-} - -module HarmTrace.Tokenizer.Tokenizer ( parseRoot, parseShorthand - , parseSongAbs, toKeyRelTok - , parseDegrees, parseDegree ) where - -import HarmTrace.Base.Parsing -import HarmTrace.Base.MusicRep -import HarmTrace.Tokenizer.Tokens - -import Data.Maybe - --------------------------------------------------------------------------------- --- Tokenizing: parsing strings into tokens --------------------------------------------------------------------------------- - --- Merges duplicate chords and transforms absolute chord labels into key --- relative tokens that can be parsed by the HarmTrace model --- (previously called mergeDups) -toKeyRelTok :: Key -> [ChordLabel] -> [ChordToken] -toKeyRelTok k (c@(Chord r sh _add _loc d):cs) = toKeyRelTok' k - (ChordToken (toScaleDegree k r) (toClassType sh) [c] NotParsed 1 d) cs -toKeyRelTok _key [] = [] -toKeyRelTok' :: Key -> ChordToken -> [ChordLabel] -> [ChordToken] -toKeyRelTok' _k p [] = [p] -toKeyRelTok' k p@(ChordToken deg clss cs' _stat n d1) (c@(Chord r sh _a _l d2):cs) - | deg == deg2 && clss == clss2 = - toKeyRelTok' k (ChordToken deg clss (cs' ++ [c]) NotParsed (n+1) (d1+d2)) cs - | otherwise = p : toKeyRelTok' k (ChordToken deg2 clss2 [c] NotParsed 1 d2) cs - where clss2 = toClassType sh - deg2 = toScaleDegree k r - --- Input is a string of whitespace-separated chords, e.g. --- Bb:9(s11) E:min7 Eb:min7 Ab:7 D:min7 G:7(13) C:maj6(9) --- First token is the key of the piece -parseSongAbs :: Parser PieceLabel -- PieceRelToken -- -parseSongAbs = PieceLabel <$> parseKey <* pLineEnd - <*> (setLoc 0 <$> pListSep_ng pLineEnd parseChord ) - <* pList pLineEnd where - setLoc :: Int -> [Chord a] -> [Chord a] - setLoc _ [] = [] - setLoc ix (Chord r c d _ l :cs) = (Chord r c d ix l) : setLoc (ix+1) cs - --- For now, I assume there is always a shorthand, and sometimes extra --- degrees. I guess it might be the case that sometimes there is no shorthand, --- but then there certainly are degrees. -parseChord :: Parser ChordLabel -parseChord = f <$> parseRoot <* pSym ':' <*> pMaybe parseShorthand - <*> (parseDegrees `opt` []) <* pSym ';' <*> pNaturalRaw - where f r (Just s) [] l = Chord r s [] 0 l - -- if there are no degrees and no shorthand (should not occur) - -- we make it a minor chord - f r Nothing [] l = Chord r Maj [] 0 l - -- in case of there is no short hand we analyse the degree list - f r Nothing d l = Chord r (analyseDegs d) d 0 l - -- in case of a sus4/maj we also analyse the degree list - f r (Just Sus4) d l = Chord r (analyseDegs d) d 0 l - f r (Just Maj) d l = Chord r (analyseDegs d) d 0 l - -- if we have another short hand we ignore the degrees list - f r (Just s) d l = Chord r s d 0 l - - -parseKey :: Parser Key -parseKey = f <$> parseRoot <* pSym ':' <*> parseShorthand - where f r m | m == Maj = Key r MajMode - | m == Min = Key r MinMode - | otherwise = error ("Tokenizer: key must be Major or Minor, " - ++ "found: " ++ show m) - - --- analyses a list of Degrees and assigns a shortHand i.e. Chord Class -analyseDegs :: [Addition] -> Shorthand -analyseDegs d - | (Note (Just Fl) I3) `elem` d = Min - | (Note (Just Sh) I5) `elem` d = Sev - | (Note (Just Fl) I7) `elem` d = Sev - | (Note Nothing I7) `elem` d = Maj7 - | (Note (Just Fl) I9) `elem` d = Sev - | (Note (Just Sh) I9) `elem` d = Sev - | (Note Nothing I11) `elem` d = Sev - | (Note (Just Sh) I11) `elem` d = Sev - | (Note (Just Fl) I13) `elem` d = Sev - | (Note Nothing I13) `elem` d = Sev - | (Note Nothing I3) `elem` d = Maj - | otherwise = Maj - - - -parseShorthand :: Parser Shorthand -parseShorthand = Maj <$ pString "maj" - <|> Min <$ pString "min" - <|> Dim <$ pString "dim" - <|> Aug <$ pString "aug" - <|> Maj7 <$ pString "maj7" - <|> Min7 <$ pString "min7" - <|> Sev <$ pString "7" - <|> Dim7 <$ pString "dim7" - <|> HDim7 <$ pString "hdim" <* opt (pSym '7') '7' - <|> MinMaj7 <$ pString "minmaj7" - <|> Maj6 <$ pString "maj6" - <|> Maj6 <$ pString "6" - <|> Min6 <$ pString "min6" - <|> Nin <$ pString "9" - <|> Maj9 <$ pString "maj9" - <|> Min9 <$ pString "min9" - <|> Sus4 <$ pString "sus4" - <|> Sus2 <$ pString "sus2" - <?> "Shorthand" - --- We don't produce intervals for a shorthand. This could easily be added, --- though. -parseDegrees :: Parser [Addition] -parseDegrees = pPacked (pSym '(') (pSym ')') - (catMaybes <$> (pList1Sep (pSym ',') parseDegree)) - -parseDegree :: Parser (Maybe Addition) -parseDegree = (Just <$> (Note <$> pMaybe parseModifier <*> parseInterval)) - <|> Nothing <$ pSym '*' <* pMaybe parseModifier <* parseInterval - -parseModifier :: Parser Modifier -parseModifier = Sh <$ pSym 's' - <|> Sh <$ pSym '#' - <|> Fl <$ pSym 'b' - <|> SS <$ pString "ss" - <|> FF <$ pString "bb" <?> "Modifier" - -parseInterval :: Parser Interval -parseInterval = ((!!) [minBound..] ) . pred <$> pNaturalRaw - -parseRoot :: Parser Root -parseRoot = Note Nothing A <$ pSym 'A' - <|> Note Nothing B <$ pSym 'B' - <|> Note Nothing C <$ pSym 'C' - <|> Note Nothing D <$ pSym 'D' - <|> Note Nothing E <$ pSym 'E' - <|> Note Nothing F <$ pSym 'F' - <|> Note Nothing G <$ pSym 'G' - <|> Note Nothing N <$ pSym 'N' -- for no chord - <|> Note (Just Fl) A <$ pString "Ab" - <|> Note (Just Fl) B <$ pString "Bb" - <|> Note (Just Fl) C <$ pString "Cb" - <|> Note (Just Fl) D <$ pString "Db" - <|> Note (Just Fl) E <$ pString "Eb" - <|> Note (Just Fl) F <$ pString "Fb" - <|> Note (Just Fl) G <$ pString "Gb" - <|> Note (Just Sh) A <$ pString "A#" - <|> Note (Just Sh) B <$ pString "B#" - <|> Note (Just Sh) C <$ pString "C#" - <|> Note (Just Sh) D <$ pString "D#" - <|> Note (Just Sh) E <$ pString "E#" - <|> Note (Just Sh) F <$ pString "F#" - <|> Note (Just Sh) G <$ pString "G#" <?> "Chord root"
− src/HarmTrace/Tokenizer/Tokens.hs
@@ -1,65 +0,0 @@-{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE EmptyDataDecls #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE GADTs #-} -{-# OPTIONS_GHC -Wall -fno-warn-orphans #-} -module HarmTrace.Tokenizer.Tokens ( ChordToken (..), PieceLabel (..) - , PieceToken (..), ParseStatus (..) - ) where - -import HarmTrace.Base.MusicRep -import HarmTrace.HAnTree.Binary -import Generics.Instant.TH -import Data.Binary - --------------------------------------------------------------------------------- --- Tokens for parsing chords --------------------------------------------------------------------------------- - --- merged Chords that will be presented to the parser -data ChordToken = ChordToken { root :: ScaleDegree - , classType :: ClassType - , chords :: [ChordLabel] - , status :: ParseStatus - , chordNumReps :: Int - , dur :: Int -- duration - } - -data ParseStatus = NotParsed | Parsed | Deleted | Inserted - deriving (Eq, Show) - --- a datatype to store a tokenized chords --- type PieceRelToken = PieceToken ChordDegree --- type PieceAbsToken = PieceToken ChordLabel -data PieceToken = PieceToken Key [ChordToken] -data PieceLabel = PieceLabel Key [ChordLabel] - --------------------------------------------------------------------------------- --- Instances for Chord Tokens --------------------------------------------------------------------------------- -instance Eq ChordToken where - (ChordToken sd clss _cs stat _n _d) == (ChordToken sd2 clss2 _cs2 stat2 _n2 _d2) - = sd == sd2 && clss == clss2 && stat == stat2 - -instance Show ChordToken where - show (ChordToken sd clss _cs Inserted _n _d) = show sd ++ show clss++"[Inserted]" - show (ChordToken sd clss cs Deleted _n _d) = - show sd ++ show clss ++ "[Deleted" ++ showChords cs ++ "]" - show (ChordToken sd clss cs _ _n d) = show sd ++ show clss ++ '_' : show d - ++ showChords cs -showChords :: Show a => [Chord a] -> String -showChords = concatMap (\x -> '[' : show x ++ "]") - - --------------------------------------------------------------------------------- --- Binary instances --------------------------------------------------------------------------------- - -deriveAllL [''ChordToken, ''ParseStatus] - -instance Binary ChordToken where - put = putDefault - get = getDefault -instance Binary ParseStatus where - put = putDefault - get = getDefault
src/Main.hs view
@@ -17,7 +17,7 @@ import System.Console.CmdArgs.Explicit hiding (Arg) -import Constants (vERSION) +import Constants (vERSION, defaultChordPrinting) import HarmTrace.HarmTrace import HarmTrace.IO.Main @@ -30,54 +30,68 @@ import HarmTrace.Matching.Alignment (getAlignDist, alignChordLab , pPrintV, alignHAnChord) import HarmTrace.Audio.Annotate ( simpleAnnotator, groupAnnotator - , mptreeAnnotator) -import HarmTrace.Audio.ChordTypes -- (AudioFeat, ChordBeatAnnotation, TimedData) + , mptreeAnnotator, mptreeAnnotatorSTG) +import HarmTrace.Base.MusicTime-- (AudioFeat, ChordBeatAnnotation, TimedData) import HarmTrace.Base.MusicRep (Key) import Data.List (delete) import System.FilePath (takeFileName) import System.Exit (exitSuccess, exitFailure) +import System.Environment ( getArgs ) import Control.Monad (when) +import Data.List ( elemIndex ) +import Data.Monoid ( mempty ) -------------------------------------------------------------------------------- -- Top-level main -------------------------------------------------------------------------------- harmTrace :: Mode [MyArgs] -harmTrace = (modes "harmtrace" emptyArgs "Harmonic Analysis and Retrieval of Music" +harmTrace = (modes "harmtrace" mempty "Harmonic Analysis and Retrieval of Music" [parseMode, matchMode, recogniseMode]) { modeGroupFlags = toGroup (inputFlags : helpVerFlags) } inputFlags :: Flag [MyArgs] -inputFlags = flagReq ["r", "read-flags"] (upd "ReadFlags") +inputFlags = flagReq ["r"] (upd "ReadFlags") "file" "File to read flags from" main :: IO () -main = do -- Parse the arguments - args <- processArgs harmTrace +main = do -- Get the original args + cliArgs <- getArgs + -- Check if we've got -r + let rArgs = case elemIndex "-r" cliArgs of + -- We did, remove all the rest + Just n | length cliArgs > (n+1) + -> [cliArgs !! n, cliArgs !! (n+1)] + -- We did not, so all the flags come from the CLI + _ -> cliArgs + strippedArgs = processValue harmTrace rArgs -- Check if we should get flags from a file -- NOTE: reading flags such as --chords="A B" will not work, because -- there are spaces inside the string "A B", and `words` isn't clever -- enough to ignore those. - newArgs <- case (getArg args "ReadFlags") of - -- Nope, use the flags received - Nothing -> return args - -- Yes, read them from the file and overwrite the + newArgs <- case (getArg strippedArgs "ReadFlags") of + -- Nope, use the original flags + Nothing -> return (processValue harmTrace cliArgs) + -- Yes, read them from the file and merge with the -- received flags - Just f -> readFile f >>= - return . processValue harmTrace . words - + Just f -> do s <- readFile f + return $ processValue harmTrace + (words s ++ cliArgs) + -- Check mode and redirect control to appropriate function case (getArg newArgs "Mode") of Just "Parse" -> mainParse newArgs Just "Match" -> mainMatch newArgs Just "Recognise" -> mainRecognise newArgs Just m -> putStrLn ("Unknown mode: " ++ m) - Nothing -> do -- Handle help and version, if present - handleHelpVer newArgs harmTrace - putStrLn "Please supply a mode" + Nothing -> -- Handle help and version, if present + handleHelpVer newArgs harmTrace + -- If invoked without arguments, print help + when (null cliArgs) $ print (helpText [] HelpFormatDefault harmTrace) + -------------------------------------------------------------------------------- -- Parse mode -------------------------------------------------------------------------------- @@ -95,7 +109,7 @@ ,flagReq ["x", "key-dir"] (upd "AnnotationKeyInputDir") "filepath" "Ground-truth key annotation directory" ,flagNone ["p", "print"] ((Switch "Print"):) "Output a .png of the parse tree" ,flagNone ["s", "print-insertions"] ((Switch "PrintIns"):) "Show inserted nodes" - ] ++ helpVerFlags) + ] ++ inputFlags : helpVerFlags) where -- Flags without input upd0 "--print" args = Right $ Switch "Print" : args @@ -180,7 +194,7 @@ ,flagReq ["i", "in"] (upd "BinaryIn") "filepath" "Input binary file for matching" ,flagNone ["s", "print-insertions"] ((Switch "PrintIns"):) "Show inserted nodes" ,flagReq ["g", "grammar"] updG "string" "Grammar to use (jazz|pop)" - ] ++ helpVerFlags) + ] ++ inputFlags : helpVerFlags) where upd0 "--print-insertions" args = Right $ Switch "PrintIns" : args upd0 "-s" args = Right $ Switch "PrintIns" : args @@ -261,19 +275,28 @@ recogniseMode :: Mode [MyArgs] recogniseMode = mode "recognise" [Arg "Mode" "Recognise"] "Recognise chords from audio files" (flagArg upd0 "") - ([flagReq ["m", "mode"] upd1 "string" "Recognition mode (mptree|group|simple)" + ([flagReq ["m", "mode"] upd1 "string" "Recognition mode (mptree|group|simple|mpt-stg)" ,flagReq ["i", "file"] (upd "SourceInputFile") "filepath" "Input file" + ,flagReq ["l", "list"] (upd "SourceInputFileList") "filepath" "List with input files" ,flagReq ["c", "gt"] (upd "GroundTruthInputFile") "filepath" "Ground truth chord annotation file (for evaluation)" ,flagReq ["k", "key"] (upd "SourceKeyInputFile") "filepath" "Ground truth key annotation file" ,flagReq ["d", "dir"] (upd "InputDir") "filepath" "Input directory" ,flagReq ["v", "vamp-dir"] (upd "VampBaseDir") "filepath" "Vamp-plugin transform specification base directory" - ,flagReq ["o", "out-dir"] (upd "OutputDir") "filepath" "output directory (for logs and chords)" - ,flagReq ["f", "csv-dir"] (upd "CSVBaseDir") "filepath" "directory for storing the feature csv files" - ,flagReq ["t", "gt-dir"] (upd "GroundTruthInputDir") "filepath" "Ground truth chord annotation input directory (for evaluation)" - ,flagReq ["x", "key-dir"] (upd "AnnotationKeyInputDir") "filepath" "Ground truth chord annotation input directory (for evaluation)" + ,flagReq ["o", "out-dir"] (upd "OutputDir") "filepath" "output directory (for logs and chords)" + ,flagReq ["f", "csv-dir"] (upd "CSVBaseDir") "filepath" "directory for storing the feature csv files" + ,flagReq ["w", "log-dir"] (upd "LogBaseDir") "filepath" "directory for storing the feature extraction logs (for every processed file)" + ,flagReq ["s", "sa-path"] (upd "SonicAnnotator") "filepath" "Path to the sonic annotator executable (it is assumed to be in the path by default)" + ,flagReq ["t", "gt-dir"] (upd "GroundTruthInputDir") "filepath" "Ground-truth chord annotation input directory (for evaluation)" + ,flagReq ["x", "key-dir"] (upd "AnnotationKeyInputDir") "filepath" + ("Ground-truth key annotation input directory (for evaluation). " ++ + "This flag also functions as a switch for selecting harmtrace chord " ++ + "recognision using ground-truth key annotations. Using this flag without " ++ + "the --gt-dir flag will trigger the evaluation of the key finding only. " ++ + "Similarly, using this flag together with -i will trigger the key finding" ++ + "of that specific file") ,flagReq ["g", "grammar"] updG "string" "Grammar to use (jazz|pop)" ,flagNone ["p", "print"] ((Switch "Print"):) "Output a .png of the parse tree" - ] ++ helpVerFlags) + ] ++ inputFlags : helpVerFlags) where upd0 "--print" args = Right $ Switch "Print" : args upd0 "-p" args = Right $ Switch "Print" : args @@ -282,21 +305,25 @@ upd1 "group" args = Right $ (RecognitionMode (RecognitionFun groupAnnotator)) : args upd1 "simple" args = Right $ (RecognitionMode (RecognitionFun simpleAnnotator)) : args upd1 "mptree" args = Right $ (RecognitionMode (RecognitionFun mptreeAnnotator)) : args + upd1 "mpt-stg" args = Right $ (RecognitionMode (RecognitionFun mptreeAnnotatorSTG)) : args upd1 s _ = Left $ "Unknown recognition mode: " ++ s mainRecognise :: [MyArgs] -> IO () mainRecognise args = do let af = getArg args "SourceInputFile" gt = getArg args "GroundTruthInputFile" + lst = getArg args "SourceInputFileList" key = getArg args "SourceKeyInputFile" afdir = getArg args "InputDir" gtdir = getArg args "GroundTruthInputDir" keydir = getArg args "AnnotationKeyInputDir" prnt = gotArg args "Print" + mout = getArg args "OutputDir" dirs = setPaths (getArg args "VampBaseDir") (getArg args "CSVBaseDir") - (getArg args "OutputDir") - + mout + (getArg args "LogBaseDir") + (getArg args "SonicAnnotator") -- Handle help and version, if present handleHelpVer args recogniseMode @@ -304,27 +331,40 @@ case (getGram args) of Nothing -> putStrLn "Please supply a grammar to use" >> exitFailure Just g -> - let ann :: Maybe [TimedData Key] -> AudioFeat -> ChordBeatAnnotation + let ann :: Maybe [TimedData Key] -> AudioFeat -> ChordAnnotation ann = (getAnn args) g - in case (gt,af,key,gtdir,afdir,keydir) of + in case (gt,af,key,gtdir,afdir,keydir,lst) of -- evaluates a single audio feature set - (Just g, Just a, Nothing, Nothing, Nothing, Nothing) -> + (Just g, Just a, Nothing, Nothing, Nothing, Nothing, Nothing) -> evaluateLabeling ann prnt g a Nothing >>= print -- evaluates a single audio feature set and key annotation - (Just g, Just a, maybek, Nothing, Nothing, Nothing) -> + (Just g, Just a, maybek, Nothing, Nothing, Nothing, Nothing) -> evaluateLabeling ann prnt g a maybek >>= print + (Nothing, Just a, Nothing, Nothing, Nothing, Just keydir, Nothing) -> + evaluateKeyFinding prnt a keydir -- evaluates a single audio feature set and prints the auto-labelling -- This is the only option that automatically extracts features -- from an audio file (if no features, and an audio file are found) -- TODO enable feature extraction for all options - (Nothing, Just a, Nothing, Nothing, Nothing, Nothing) -> - printLabeling dirs ann a - -- evaluates a directory with audio features - (Nothing, Nothing, Nothing, Just gd, Just ad, Nothing) -> - batchLabeling ann gd ad Nothing + (Nothing, Just a, Nothing, Nothing, Nothing, Nothing, Nothing) -> + printLabelling defaultChordPrinting dirs ann a + -- takes a list of files and prints the chords to files + (Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Just a) -> + batchPrintLabelling defaultChordPrinting dirs ann a + -- evaluates a directory with audio features, when an output flag + -- is present we also store the chords in a file (the actual + -- output dir is stored in a 'BasePaths') + (Nothing, Nothing, Nothing, Just gd, Just ad, Nothing, Nothing) -> + batchLabeling defaultChordPrinting mout ann gd ad Nothing -- evaluates a directory with audio features and key annotations - (Nothing, Nothing, Nothing, Just gd, Just ad, maybek) -> - batchLabeling ann gd ad maybek + (Nothing, Nothing, Nothing, Just gd, Just ad, maybek, Nothing) -> + batchLabeling defaultChordPrinting mout ann gd ad maybek + -- if we only encounter audio features and key annotations (and no + -- ground-truth chords), we evaluate only the key finding. I agree + -- this is rather hacky... (the annotator options are completely + -- ignored) + (Nothing, Nothing, Nothing, Nothing, Just ad, Just key, Nothing) -> + batchEvaluateKeyFinding ad key _ -> usageError args err4 -------------------------------------------------------------------------------- @@ -338,7 +378,7 @@ deriving (Eq, Show) newtype RecognitionFun = RecognitionFun (GrammarEx -> Maybe [TimedData Key] - -> AudioFeat -> ChordBeatAnnotation) + -> AudioFeat -> ChordAnnotation) instance Show RecognitionFun where show _ = "RecognitionFun" @@ -346,9 +386,6 @@ instance Eq RecognitionFun where _ == _ = error "please do not use this" -emptyArgs :: [MyArgs] -emptyArgs = [] - -- Grammar parser updG :: String -> [MyArgs] -> Either String [MyArgs] updG "jazz" args = Right $ (Grammar (GrammarEx Jazz)) : args @@ -370,7 +407,7 @@ getMode (_:t) = getMode t getAnn :: [MyArgs] -> (GrammarEx -> Maybe [TimedData Key] -> AudioFeat - -> ChordBeatAnnotation) + -> ChordAnnotation) getAnn [] = error "impossible?" getAnn ((RecognitionMode (RecognitionFun f)):_) = f getAnn (_:t) = getAnn t @@ -400,7 +437,8 @@ when ver $ putStrLn vERSION >> exitSuccess -- Output help and exit - when help $ print (helpText [] HelpFormatDefault mode) >> exitSuccess + when help $ + print (helpText [] HelpFormatDefault mode) >> exitSuccess -------------------------------------------------------------------------------- -- Misc