diff --git a/src/Base/Chord/Chord.hs b/src/Base/Chord/Chord.hs index 041b8a8..51bfaac 100644 --- a/src/Base/Chord/Chord.hs +++ b/src/Base/Chord/Chord.hs @@ -39,7 +39,7 @@ import Base.Core.Interval import Common.Utils (uncurry4, uncurry5) import Data.Function (on) -import Data.List (sortBy, zip4, zip5) +import Data.List (sortBy, zip4, zip5, intercalate) import qualified Data.List as L import Data.Map as M import Data.Maybe (catMaybes, fromJust) @@ -48,7 +48,11 @@ import Data.Maybe (catMaybes, fromJust) -- but with the specific notes as well. data Chord = Chord { getSymbol :: ChordSymbol , getNotes :: [Note] - } deriving (Eq, Show) + } deriving (Eq) + +instance Show Chord where + show c = show (getSymbol c) ++ ", [" ++ intercalate ", " (show <$> getNotes c) ++ "]" + -- | A Chord has all the properties of the Chordal typeclass instance Chordal Chord where @@ -97,13 +101,13 @@ notesToChord notes = findQuality :: Note -> CQ.Quality findQuality root - | hasInterval IQ.Major 3 && hasInterval IQ.Minor 7 = CQ.Dominant - | hasInterval IQ.Major 3 && hasInterval (IQ.Augmented 1) 5 = CQ.Augmented - | hasInterval IQ.Major 3 = CQ.Major - | hasInterval IQ.Minor 3 && hasInterval IQ.Minor 7 = CQ.Minor - | hasInterval IQ.Minor 3 && hasInterval (IQ.Diminished 1) 5 = CQ.Diminished - | hasInterval IQ.Minor 3 = CQ.Minor - | hasInterval IQ.Minor 7 = CQ.Dominant + | hasInterval IQ.major 3 && hasInterval IQ.minor 7 = CQ.Dominant + | hasInterval IQ.major 3 && hasInterval (fromJust $ IQ.augmented 1) 5 = CQ.Augmented + | hasInterval IQ.major 3 = CQ.Major + | hasInterval IQ.minor 3 && hasInterval IQ.minor 7 = CQ.Minor + | hasInterval IQ.minor 3 && hasInterval (fromJust $ IQ.diminished 1) 5 = CQ.Diminished + | hasInterval IQ.minor 3 = CQ.Minor + | hasInterval IQ.minor 7 = CQ.Dominant | otherwise = CQ.Major where notesContainIntervalFromNote :: [Note] -> Note -> Interval -> Bool @@ -132,7 +136,7 @@ notesToChord notes = cInts = intervalBetweenNotes root <$> roots majorOrNot :: Int -> HighestNatural - majorOrNot = if (quality /= CQ.Major) && fromJust (intervalFrom IQ.Major 7) `elem` cInts + majorOrNot = if (quality /= CQ.Major) && fromJust (intervalFrom IQ.major 7) `elem` cInts then majorNatural else nonMajorNatural @@ -159,10 +163,10 @@ notesToChord notes = containsThird = 3 `elem` (getSize <$> cInts) has2 :: Bool - has2 = fromJust (intervalFrom IQ.Major 2) `elem` cInts + has2 = fromJust (intervalFrom IQ.major 2) `elem` cInts has4 :: Bool - has4 = fromJust (intervalFrom IQ.Perfect 4) `elem` cInts + has4 = fromJust (intervalFrom IQ.perfect 4) `elem` cInts chordSuses :: [Sus] chordSuses = uncurry findSus <$> zip roots highNats @@ -187,10 +191,10 @@ notesToChord notes = noSusInts :: [Interval] noSusInts - | chordSus == sus 2 = L.delete (fromJust (intervalFrom IQ.Major 2)) noNatInts - | chordSus == sus 4 = L.delete (fromJust (intervalFrom IQ.Perfect 4)) noNatInts - | (chordSus == susNoNum && numHighNat < 9) = L.delete (fromJust (intervalFrom IQ.Major 2)) - $ L.delete (fromJust (intervalFrom IQ.Perfect 4)) noNatInts + | chordSus == sus 2 = L.delete (fromJust (intervalFrom IQ.major 2)) noNatInts + | chordSus == sus 4 = L.delete (fromJust (intervalFrom IQ.perfect 4)) noNatInts + | (chordSus == susNoNum && numHighNat < 9) = L.delete (fromJust (intervalFrom IQ.major 2)) + $ L.delete (fromJust (intervalFrom IQ.perfect 4)) noNatInts | otherwise = noNatInts intToExt :: Interval -> Extension diff --git a/src/Base/Chord/Diatonic.hs b/src/Base/Chord/Diatonic.hs index c0c65e5..dee986c 100644 --- a/src/Base/Chord/Diatonic.hs +++ b/src/Base/Chord/Diatonic.hs @@ -82,7 +82,7 @@ diatonicChord scale@(Scale note mode) numNotes degree jumpSize = -- | Given a degree, returns a function that creates a diatonic triadic major chord -- from a key (i.e. a Note) and the number of notes. getMajorFuncChord :: Int -> Note -> Int -> Maybe Chord -getMajorFuncChord deg key numNotes = diatonicChord (Scale key (modeFrom Ionian [])) numNotes deg 2 +getMajorFuncChord deg key numNotes = diatonicChord (Scale key (fromJust (modeFrom Ionian []))) numNotes deg 2 -- | Given a key and a number of notes, this returns a triadic I chord. -- If number of notes is not between 2 and 7, this returns Nothing. diff --git a/src/Base/Chord/Shape.hs b/src/Base/Chord/Shape.hs index 91b8313..d274e8a 100644 --- a/src/Base/Chord/Shape.hs +++ b/src/Base/Chord/Shape.hs @@ -26,13 +26,19 @@ import Base.Class.Chordal import Base.Scale.Heliotonic import Data.Set hiding (foldr) +import Data.List(intercalate) -- | A ChordShape, e.g. a M7 chord, is a rootless abstract shape. data ChordShape = ChordShape { getQuality :: Quality , getHighestNatural :: HighestNatural , getExtensions :: [Extension] , getSus :: Sus - } deriving (Eq, Show) + } deriving (Eq) + +instance Show ChordShape where + show c = show (getQuality c) ++ show (getHighestNatural c) + ++ concat (show <$> getExtensions c) ++ show (getSus c) + -- | ChordShape has all the properties of the Chordal typeclass instance Chordal ChordShape where diff --git a/src/Base/Chord/Subs.hs b/src/Base/Chord/Subs.hs index 3ab7b8c..af0ddee 100644 --- a/src/Base/Chord/Subs.hs +++ b/src/Base/Chord/Subs.hs @@ -47,20 +47,19 @@ import qualified Data.Set as S (delete) import Data.Maybe(fromJust, catMaybes, isJust) import Base.Core.PitchClass(pitchClass) import qualified Data.Map.Strict as M (lookup, elems) -import Base.Scale.Scale +import Base.Scale.Scale as Sc import Language.Parser -- | Removes the P5 interval from a given chord's note if it exists -- and returns the same chord symbol. -- --- prop> remove5 (chord, _) == chord +-- prop> getSymbol $ remove5 chord == getSymbol chord -- -- >>> c = canonicalizeChord $ fromJust $ parseChord "CM7" --- >>> notes = chordToNotes c --- >>> remove5 (c, notes) --- (CM7,[C,E,B]) +-- >>> remove5 (chordFromSymbol c) +-- CM7, [C, E, B] remove5 :: Chord -> Chord -remove5 chord = updateNotes chord $ toNotes (root chord) $ S.delete (fromJust(intervalFrom Perfect 5)) (toIntervals chord) +remove5 chord = updateNotes chord $ toNotes (root chord) $ S.delete (fromJust(intervalFrom perfect 5)) (toIntervals chord) where toNotes :: Note -> Set Interval -> [Note] toNotes root intSet = flip jumpIntervalFromNote root <$> toList intSet @@ -118,11 +117,10 @@ negative key chord = head $ notesToChord newNotes -- this transposes the chord by that interval. Because this function -- is only used internally by the module, it assumes the intervals are -- validly constructed. --- TODO: Make this a safe, general-purpose, exportable function -transposeChord :: Chord -> IQ.Quality -> Int -> Chord -transposeChord chord iQual i = +transposeChord :: Chord -> Interval -> Chord +transposeChord chord interval = let - newChord = transposeToRoot (getSymbol chord )$ respell $ jumpIntervalFromNote (fromJust (intervalFrom iQual i)) $ root chord + newChord = transposeToRoot (getSymbol chord )$ respell $ jumpIntervalFromNote interval $ root chord in chordFromSymbol newChord @@ -130,21 +128,24 @@ transposeChord chord iQual i = -- a list of chord substitutions from its diminished family. -- -- >>> c = canonicalizeChord $ fromJust $ parseChord "C7" --- >>> notes = chordToNotes c --- >>> dimFamilySub (c, notes) --- [(Eb7,[Eb,G,Bb,Db]),(Gb7,[Gb,Bb,Db,Fb]),(A7,[A,C#,E,G])] +-- >>> dimFamilySub (chordFromSymbol c) +-- [Chord {getSymbol = ChordSymbol {getChordRoot = Eb, getShape = ChordShape {getQuality = , getHighestNatural = 7, getExtensions = [], getSus = }}, getNotes = [Eb,G,Bb,Db]},Chord {getSymbol = ChordSymbol {getChordRoot = Gb, getShape = ChordShape {getQuality = , getHighestNatural = 7, getExtensions = [], getSus = }}, getNotes = [Gb,Bb,Db,Fb]},Chord {getSymbol = ChordSymbol {getChordRoot = A, getShape = ChordShape {getQuality = , getHighestNatural = 7, getExtensions = [], getSus = }}, getNotes = [A,C#,E,G]}] dimFamilySub :: Chord -> [Chord] dimFamilySub eChord | quality eChord == CQ.Dominant || - (quality eChord == CQ.Minor && (fromJust (intervalFrom IQ.Major 6) `member` toIntervals eChord)) - = [transposeChord eChord IQ.Minor 3, transposeChord eChord (IQ.Diminished 1) 5, transposeChord eChord IQ.Major 6] + (quality eChord == CQ.Minor && (fromJust (intervalFrom IQ.major 6) `member` toIntervals eChord)) + = [transposeBy IQ.minor 3, transposeBy (fromJust $ IQ.diminished 1) 5, transposeBy IQ.major 6] | otherwise = [] + where + transposeBy :: IQ.Quality -> Int -> Chord + transposeBy iQual i = transposeChord eChord $ fromJust $ intervalFrom iQual i + -- | Returns the same chord, but a tritone away. -- If applied twice in a row, returns the same chord: -- prop> tritoneSub $ tritoneSub (chord, notes) == (_, notes) tritoneSub :: Chord -> Chord -tritoneSub eChord = transposeChord eChord (IQ.Augmented 1) 4 +tritoneSub eChord = transposeChord eChord $ fromJust $ intervalFrom (fromJust $ IQ.augmented 1) 4 -- | Given a key, and a chord in the key, this returns -- a list of chords that are diatonic functional substitutes. @@ -161,10 +162,10 @@ diatonicFuncSub key chord | validSub && degree == 7 = [fromJust (dominant key numNotes)] where validSub :: Bool - validSub = chord `isDiatonicTo` major key + validSub = chord `isDiatonicTo` Sc.major key degree :: Int - degree = 1 + fromJust (elemIndex (root chord) (scaleToNotes (major key))) + degree = 1 + fromJust (elemIndex (root chord) (scaleToNotes (Sc.major key))) numNotes :: Int numNotes = length $ toNotes chord diff --git a/src/Base/Chord/Symbol.hs b/src/Base/Chord/Symbol.hs index adec933..174d788 100644 --- a/src/Base/Chord/Symbol.hs +++ b/src/Base/Chord/Symbol.hs @@ -36,7 +36,11 @@ import Data.Set -- a C and a M7 gives us a CM7. data ChordSymbol = ChordSymbol { getChordRoot :: Note , getShape :: ChordShape - } deriving (Eq, Show) + } deriving (Eq) + +instance Show ChordSymbol where + show c = show (getChordRoot c) ++ show (getShape c) + -- | ChordSymbol has all the properties of the Chordal typeclass, -- most importantly, that it can be converted to a set of intervals. diff --git a/src/Base/Core/Interval.hs b/src/Base/Core/Interval.hs index 067e508..28d16e8 100644 --- a/src/Base/Core/Interval.hs +++ b/src/Base/Core/Interval.hs @@ -44,7 +44,7 @@ import Common.Utils (modByFrom) import Control.Monad (zipWithM) -import Data.Maybe (fromJust) +import Data.Maybe (fromJust, isJust, fromMaybe) import Data.Set as S hiding (filter) -- | Intervals are defined by a quality and a size. @@ -74,8 +74,6 @@ normalizeIntervalSize = modByFrom 7 1 -- TODO: This should probably return an Either String Interval (or an error -- type in place of String) instead of Maybe Interval, in order to facilitate -- more sophisticated error handling / reporting. --- --- TODO: Make sure diminished and augmented don't get arguments > 11 intervalFrom :: Quality -> Int -> Maybe Interval intervalFrom q s = if normalizedSize `elem` goodSizes then Just Interval { getQuality = q, getSize = normalizedSize } @@ -113,7 +111,7 @@ intervalToDistance interval = -- Minor shifts Interval Minor i -> case baseQuality i of - Major -> subtract 1 <$> intervalToDistance (Interval Major i) + Major -> subtract 1 <$> intervalToDistance (Interval major i) Perfect -> Nothing -- Augmented shifts @@ -149,11 +147,11 @@ instance Invertible Interval where newI = normalizeIntervalSize $ 9 - normalizeIntervalSize i newQual = case iQual of - Major -> Minor - Minor -> Major - Perfect -> Perfect - (Augmented x) -> Diminished x - (Diminished x) -> Augmented x + Major -> minor + Minor -> major + Perfect -> perfect + (Augmented x) -> fromJust $ diminished x + (Diminished x) -> fromJust $ augmented x in Interval newQual newI @@ -161,23 +159,23 @@ instance Invertible Interval where -- the interval's distance is increased by that integer without -- changing its size. -- --- TODO: Figure out what to do if dim and aug < 11 and this function --- receives an integer argument > 11. --- -- prop> intervalToDistance i + x == intervalToDistance (i <+> x) -- prop> getSize (i <+> _) == getSize i infixl 6 <+> (<+>) :: Interval -> Int -> Interval -Interval iQual i <+> x = - Interval (iterate modFunc iQual !! abs x) i +Interval iQual i <+> x = Interval newQual i where + unwrapMaybeQ :: (Quality -> Maybe Quality) -> Quality -> Quality + unwrapMaybeQ f qual = fromMaybe (baseQuality i) (f qual) modFunc = case (baseQuality i, signum x) of (Perfect, 1) -> raisePerfect (Major, 1) -> raiseMajor (Perfect, -1) -> lowerPerfect (Major, -1) -> lowerMajor - (_, 0) -> id + (_, 0) -> return + smartMod = unwrapMaybeQ modFunc + newQual = iterate smartMod iQual !! abs x -- | When subtracting an integer from an interval with this infix operator, -- the interval's distance is decreased by that integer without @@ -252,7 +250,5 @@ nthDegreeIntervals ints n = S.map (|-| noteInterval) ints where noteInterval = toAscList ints !! (n - 1) -zipToIntervalSet :: [Quality] -> [Int] -> Maybe (Set Interval) -zipToIntervalSet quals sizes = - do ints <- zipWithM intervalFrom quals sizes - return $ fromList ints +zipToIntervalSet :: [Quality] -> [Int] -> Set Interval +zipToIntervalSet quals sizes = fromList (fromJust . uncurry intervalFrom <$> zip quals sizes) diff --git a/src/Base/Core/Quality/IQuality.hs b/src/Base/Core/Quality/IQuality.hs index c82ea3d..f0c49b6 100644 --- a/src/Base/Core/Quality/IQuality.hs +++ b/src/Base/Core/Quality/IQuality.hs @@ -10,8 +10,21 @@ Portability : POSIX This module exports the (interval) Quality datatype and a function to get the base quality of an interval size. -} + +{-# LANGUAGE PatternSynonyms #-} + module Base.Core.Quality.IQuality - ( Quality(..) + ( Quality + , pattern Major + , pattern Minor + , pattern Perfect + , pattern Diminished + , pattern Augmented + , major + , perfect + , minor + , diminished + , augmented , baseQuality , raisePerfect , raiseMajor @@ -27,16 +40,19 @@ import Common.Utils (modByFrom) 3. Minor 4. Diminished (single, doubly, triply, etc.) 5. Augmented (single, doubly, triply, etc.) - -TODO: Make sure that interval qualities are using smart constructors -so that diminished and augmented only receive integers >= 1. -} data Quality - = Major - | Perfect - | Minor - | Diminished Int - | Augmented Int + = RealMajor + | RealPerfect + | RealMinor + | RealDiminished Int + | RealAugmented Int + +pattern Major <- RealMajor +pattern Perfect <- RealPerfect +pattern Minor <- RealMinor +pattern Diminished x <- RealDiminished x +pattern Augmented x <- RealAugmented x -- | Reasonable defaults for showing interval qualities. Note that -- there is no standard for multiple diminished or augmented intervals @@ -48,44 +64,68 @@ instance Show Quality where show (Diminished i) = if i == 1 then "dim" else show i ++ "dim" show (Augmented i) = if i == 1 then "aug" else show i ++ "aug" +-- | A smart constructor for the @Major@ interval quality. +major :: Quality +major = RealMajor + +-- | A smart constructor for the @Perfect@ interval quality. +perfect :: Quality +perfect = RealPerfect + +-- | A smart constructor for the @Minor@ interval quality. +minor :: Quality +minor = RealMinor + +-- | A smart constructor for the @Diminished@ interval quality. Returns +-- @Nothing@ just in case the given @Int@ is less than or equal to 0 or +-- greater than or equal to 12. +diminished :: Int -> Maybe Quality +diminished x = if x > 0 && x < 12 then Just $ RealDiminished x else Nothing + +-- | A smart constructor for the @Augmented@ interval quality. Returns +-- @Nothing@ just in case the given @Int@ is less than or equal to 0 or +-- greater than or equal to 12. +augmented :: Int -> Maybe Quality +augmented x = if x > 0 && x < 12 then Just $ RealAugmented x else Nothing + -- | Given an interval size, we return the base quality, either Perfect or Major baseQuality :: Int -> Quality baseQuality n - | canonicalized `elem` [1, 4, 5] = Perfect - | canonicalized `elem` [2, 3, 6, 7] = Major + | canonicalized `elem` [1, 4, 5] = RealPerfect + | canonicalized `elem` [2, 3, 6, 7] = RealMajor where canonicalized = modByFrom 7 1 n -- | Given an interval quality, this raises that quality by a semitone -- assuming that its base quality is Perfect. -raisePerfect :: Quality -> Quality -raisePerfect Perfect = Augmented 1 -raisePerfect (Augmented x) = Augmented $ x + 1 -raisePerfect (Diminished 1) = Perfect -raisePerfect (Diminished x) = Diminished $ x - 1 +raisePerfect :: Quality -> Maybe Quality +raisePerfect Perfect = Just $ RealAugmented 1 +raisePerfect (Augmented x) = augmented $ x + 1 +raisePerfect (Diminished 1) = Just RealPerfect +raisePerfect (Diminished x) = diminished $ x - 1 -- | Given an interval quality, this raises that quality by a semitone -- assuming that its base quality is Major. -raiseMajor :: Quality -> Quality -raiseMajor Major = Augmented 1 -raiseMajor (Augmented x) = Augmented $ x + 1 -raiseMajor Minor = Major -raiseMajor (Diminished 1) = Minor -raiseMajor (Diminished x) = Diminished $ x - 1 +raiseMajor :: Quality -> Maybe Quality +raiseMajor Major = Just $ RealAugmented 1 +raiseMajor (Augmented x) = augmented $ x + 1 +raiseMajor Minor = Just RealMajor +raiseMajor (Diminished 1) = Just RealMinor +raiseMajor (Diminished x) = diminished $ x - 1 -- | Given an interval quality, this lowers that quality by a semitone -- assuming that its base quality is Perfect. -lowerPerfect :: Quality -> Quality -lowerPerfect Perfect = Diminished 1 -lowerPerfect (Diminished x) = Diminished $ x + 1 -lowerPerfect (Augmented 1) = Perfect -lowerPerfect (Augmented x) = Augmented $ x-1 +lowerPerfect :: Quality -> Maybe Quality +lowerPerfect Perfect = Just $ RealDiminished 1 +lowerPerfect (Diminished x) = diminished $ x + 1 +lowerPerfect (Augmented 1) = Just RealPerfect +lowerPerfect (Augmented x) = augmented $ x-1 -- | Given an interval quality, this lowers that quality by a semitone -- assuming that its base quality is Major. -lowerMajor :: Quality -> Quality -lowerMajor Major = Minor -lowerMajor Minor = Diminished 1 -lowerMajor (Diminished x) = Diminished $ x + 1 -lowerMajor (Augmented 1) = Major -lowerMajor (Augmented x) = Augmented $ x - 1 +lowerMajor :: Quality -> Maybe Quality +lowerMajor Major = Just RealMinor +lowerMajor Minor = Just $ RealDiminished 1 +lowerMajor (Diminished x) = diminished $ x + 1 +lowerMajor (Augmented 1) = Just RealMajor +lowerMajor (Augmented x) = augmented $ x - 1 \ No newline at end of file diff --git a/src/Base/Scale/BaseMode.hs b/src/Base/Scale/BaseMode.hs index 35d22ef..7f170f7 100644 --- a/src/Base/Scale/BaseMode.hs +++ b/src/Base/Scale/BaseMode.hs @@ -19,11 +19,10 @@ import Base.Core.Interval import Base.Core.Quality.IQuality import Data.Maybe (fromJust) -import Data.Set +import Data.Set hiding (filter) +import qualified Data.Set as S(filter) -- | A BaseMode must be one of these names. --- TODO: Implement: Blues scale, pentatonics --- TODO: Figure out if we're missing any more. data BaseMode = Lydian | Dorian @@ -43,19 +42,26 @@ data BaseMode | DoubleHarmonicMinor | HarmonicMajor | DoubleHarmonicMajor + | Blues + | MajorPentatonic + | MinorPentatonic + | Bebop + | WholeTone + | Hirajoshi + | HalfWhole + | WholeHalf deriving (Show, Enum, Eq) -- | Given a base mode, this returns a set of intervals. Whereas -- some base modes are created from scratch (like Ionian), others -- (e.g. Dorian, Phrygian, etc.) can be derived by shifting those modes. baseModeIntervals :: BaseMode -> Set Interval -baseModeIntervals bm = if fromScratch then - fromJust $ zipToIntervalSet bmQualities [1 .. 7] - else - let - (mode, shift) = modeAndShift - in - nthDegreeIntervals (baseModeIntervals mode) shift +baseModeIntervals bm + | fromScratch = zipToIntervalSet bmQualities [1 .. 7] + | nonHeptatonic = nonHepInts + | otherwise + = let (mode, shift) = modeAndShift + in nthDegreeIntervals (baseModeIntervals mode) shift where -- Discriminate between BaseModes for which we build the intervals from -- scratch and those that are computed from some other interval set @@ -70,26 +76,70 @@ baseModeIntervals bm = if fromScratch then , DoubleHarmonicMajor ] + -- Discriminate between BaseModes that have scale degrees 1-7 and those + -- that do not. + nonHeptatonic :: Bool + nonHeptatonic = bm `elem` [ Blues + , MajorPentatonic + , MinorPentatonic + , Bebop + , WholeTone + , Hirajoshi + , HalfWhole + , WholeHalf + ] + -- The interval qualities for the modal interval sets built from scratch bmQualities :: [Quality] bmQualities = case bm of Ionian -> - [Perfect, Major, Major, Perfect, Perfect, Major, Major] + [perfect, major, major, perfect, perfect, major, major] AugmentedQuality -> - [Perfect, Major, Major, Augmented 1, Augmented 1, Major, Minor] + [perfect, major, major, fromJust $ augmented 1, fromJust $ augmented 1, major, minor] DiminishedQuality -> - [Perfect, Major, Minor, Perfect, Diminished 1, Minor, Diminished 1] + [perfect, major, minor, perfect, fromJust $ diminished 1, minor, fromJust $ diminished 1] MelodicMinor -> - [Perfect, Major, Minor, Perfect, Perfect, Major, Major] + [perfect, major, minor, perfect, perfect, major, major] HarmonicMinor -> - [Perfect, Major, Minor, Perfect, Perfect, Minor, Major] + [perfect, major, minor, perfect, perfect, minor, major] DoubleHarmonicMinor -> - [Perfect, Major, Minor, Augmented 1, Perfect, Minor, Major] + [perfect, major, minor, fromJust $ augmented 1, perfect, minor, major] HarmonicMajor -> - [Perfect, Major, Major, Perfect, Perfect, Minor, Major] + [perfect, major, major, perfect, perfect, minor, major] DoubleHarmonicMajor -> - [Perfect, Minor, Major, Perfect, Perfect, Minor, Major] + [perfect, minor, major, perfect, perfect, minor, major] + + -- The interval sets for the non-heptatonic modes + nonHepInts :: Set Interval + nonHepInts = zipToIntervalSet quals iSizes + where + (quals, iSizes) = + case bm of + Blues -> + ([perfect, minor, perfect, fromJust $ diminished 1, perfect, minor], + [1, 3, 4, 5, 5, 7]) + MajorPentatonic -> + ([perfect, major, major, perfect, major], + [1, 2, 3, 5, 6]) + MinorPentatonic -> + ([perfect, minor, perfect, perfect, minor], + [1, 3, 4, 5, 7]) + Bebop -> + ([perfect, major, major, perfect, perfect, major, minor, major], + [1, 2, 3, 4, 5, 6, 7, 7]) + WholeTone -> + ([perfect, major, major, fromJust $ diminished 1, minor, minor], + [1, 2, 3, 5, 6, 7]) + Hirajoshi -> + ([perfect, major, minor, perfect, minor], + [1, 2, 3, 5, 6]) + HalfWhole -> + ([perfect, minor, minor, major, fromJust $ augmented 1, perfect, major, minor], + [1, 2, 3, 3, 4, 5, 6, 7]) + WholeHalf -> + ([perfect, major, minor, perfect, fromJust $ diminished 1, minor, major, major], + [1, 2, 3, 4, 5, 6, 6, 7]) -- The starting mode and shift for modal interval sets built from other -- interval sets @@ -106,3 +156,4 @@ baseModeIntervals bm = if fromScratch then LydianDom -> (MelodicMinor, 4) Altered -> (MelodicMinor, 7) PhrygianDom -> (HarmonicMinor, 5) + diff --git a/src/Base/Scale/Extension.hs b/src/Base/Scale/Extension.hs index 5db37c3..3eb73cb 100644 --- a/src/Base/Scale/Extension.hs +++ b/src/Base/Scale/Extension.hs @@ -29,6 +29,5 @@ instance Show Extension where show ext = show (getAccidental ext) ++ show (getDegree ext) -- | Smart constructor for a scale extension. --- TODO: Figure out whether this should exclude the AccNatural possibility scaleExtensionFrom :: Accidental -> Int -> Extension scaleExtensionFrom = Extension diff --git a/src/Base/Scale/Heliotonic.hs b/src/Base/Scale/Heliotonic.hs index b975308..af7e32b 100644 --- a/src/Base/Scale/Heliotonic.hs +++ b/src/Base/Scale/Heliotonic.hs @@ -108,4 +108,4 @@ highestNaturalToIntervals scale hn = (int, fromJust interval) insertMajorSeven :: HeliotonicScale -> HeliotonicScale - insertMajorSeven hts = insert 7 (fromJust $ intervalFrom IQ.Major 7) hts + insertMajorSeven hts = insert 7 (fromJust $ intervalFrom IQ.major 7) hts diff --git a/src/Base/Scale/Mode.hs b/src/Base/Scale/Mode.hs index c641754..0f4fe4b 100644 --- a/src/Base/Scale/Mode.hs +++ b/src/Base/Scale/Mode.hs @@ -28,8 +28,9 @@ import Base.Scale.Extension import Data.Function (on) import Data.List (intercalate, sortBy) import Data.Maybe (fromJust) -import Data.Set (Set(..), insert, delete, elemAt, toAscList) -import qualified Data.Set as S(filter, map) +import Data.Set (Set(..), insert, delete, elemAt, toAscList, toList) +import qualified Data.Set as S (filter, map) +import Common.Utils -- | A mode is constructed from a base mode and scale extensions. data Mode = Mode BaseMode [Extension] @@ -41,9 +42,26 @@ instance Show Mode where --Add extensions separated by a comma... ++ intercalate ", " (show <$> exts) --- | Smart constructor for a mode -modeFrom :: BaseMode -> [Extension] -> Mode -modeFrom = Mode +{-| +-- Smart constructor for a mode. If a mode is invalid, +-- this returns Nothing. The possible reasons for an invalid mode are: +-- +-- 1. The list of extensions has a duplicate degree +-- 2. An extension exists where the degree is not in the base mode +-- 3. An extension exists for a degree that occurs multiple times in the mode +-} +modeFrom :: BaseMode -> [Extension] -> Maybe Mode +modeFrom base exts = + let + baseSizes = getSize <$> toList (baseModeIntervals base) + extSizes = getDegree <$> exts + in + if allUnique extSizes + || all (`elem` baseSizes) extSizes + || all (isUnique baseSizes) extSizes + then Just $ Mode base exts + else Nothing + -- | Converts a mode into a set of intervals. modeToIntervals :: Mode -> Set Interval @@ -53,8 +71,6 @@ modeToIntervals (Mode baseMode exts) = extIntervals :: Extension -> Set Interval -> Set Interval extIntervals ext intSet = insert (oldInt <+> impliedShift (getAccidental ext)) (delete oldInt intSet) where - -- TODO: If there isn't only one interval of a certain degree, the mode is - -- ambiguously constructed and we should give a warning. oldInt = elemAt 0 (S.filter (\a -> getSize a == getDegree ext) intSet) -- | Given a mode, returns the number of scale extensions. @@ -121,4 +137,4 @@ modesToExts mode1 mode2 = -- | Given a set of intervals and a set of degrees, this pulls out -- the intervals at those degrees and returns a subset of the intervals. getSubsetModeByDegree :: Set Interval -> Set Int -> Set Interval -getSubsetModeByDegree mode degs = S.filter (\i -> getSize i `elem` degs) mode +getSubsetModeByDegree mode degs = S.filter (\i -> getSize i `elem` degs) mode \ No newline at end of file diff --git a/src/Base/Scale/Scale.hs b/src/Base/Scale/Scale.hs index 8b9d4ad..aa0c3d8 100644 --- a/src/Base/Scale/Scale.hs +++ b/src/Base/Scale/Scale.hs @@ -25,6 +25,7 @@ import Base.Scale.BaseMode import Base.Scale.Mode import Data.Set(Set(..), fromList, toAscList, elemAt, insert, delete, mapMonotonic, isSubsetOf, toList) import qualified Data.Set as S(filter, map) +import Data.Maybe(fromJust) -- | A scale is specified by a note and a mode. data Scale = Scale Note Mode @@ -34,11 +35,11 @@ instance Show Scale where -- | Given a note, this returns a major scale starting from that note. major :: Note -> Scale -major key = Scale key (modeFrom Ionian []) +major key = Scale key $ fromJust $ modeFrom Ionian [] -- | Given a note, this returns a minor scale starting from that note. minor :: Note -> Scale -minor key = Scale key (modeFrom Aeolian []) +minor key = Scale key $ fromJust $ modeFrom Aeolian [] -- | Converts a scale to a list of notes. scaleToNotes :: Scale -> [Note] diff --git a/src/Common/Utils.hs b/src/Common/Utils.hs index eb4bcc2..33c636f 100644 --- a/src/Common/Utils.hs +++ b/src/Common/Utils.hs @@ -17,8 +17,12 @@ module Common.Utils , uncurry4 , uncurry5 , getIndices + , allUnique + , isUnique ) where +import Data.Set (fromList) + -- | Given a modulus and the new lower bound, returns a function -- to mod a number accordingly. modByFrom :: Int -- ^ The modulus @@ -47,4 +51,10 @@ uncurry5 f (a, b, c, d, e) = f a b c d e -- | Given a set of indices and a list, extracts the elements at those indices. -- Note that if the indices are out of bounds, this function will panic. getIndices :: [Int] -> [a] -> [a] -getIndices indices xs = map (xs !!) indices \ No newline at end of file +getIndices indices xs = map (xs !!) indices + +allUnique :: (Ord a) => [a] -> Bool +allUnique xs = length xs == length (fromList xs) + +isUnique :: (Ord a) => [a] -> a -> Bool +isUnique xs x = length (filter (== x) xs) == 1