-- | Tools for creating and manipulation Pitch Factor Diagrams, a tool for representing musical -- intervals and examining their relations. module Boopadoop.Diagram where import Data.Ratio import Data.Bits import Data.Monoid import Data.List import Data.Numbers.Primes import Data.Align (salign) -- | 12 tone equal temperament semitone ratio. Equal to @2 ** (1/12)@. semi :: Floating a => a semi = 2 ** (1/12) -- | 12 tone equal temperament ratios for all semitones in an octave. allSemis :: Floating a => [a] allSemis = map (semi **) . map fromIntegral $ [0..11 :: Int] -- | List multiples of the single octave semitone ratios upto a certain amount. takeFinAlignments :: Floating a => Int -> [[a]] takeFinAlignments fin = map (\k -> map (*k) . map fromIntegral $ [1.. fin]) allSemis -- | A pitch factor diagram is a list of prime exponents that represents a rational number -- via 'diagramToRatio'. These are useful because pitches with few prime factors, that is, -- small 'PitchFactorDiagram's with small factors in them, are generally consonant, and -- many interesting just intonation intervals can be written this way (see 'Boopadoop.Interval.perfectFifth' -- and 'Boopadoop.Interval.majorThird'). newtype PitchFactorDiagram = Factors {getFactors :: [Integer]} deriving Show -- | 'mempty' is the unison PFD, with ratio @1@. instance Monoid PitchFactorDiagram where mempty = Factors [] mappend = addPFD -- | 'PitchFactorDiagram's are combined by multiplying their underlying ratios (adding factors). instance Semigroup PitchFactorDiagram where (<>) = addPFD -- | Convert a factor diagram to the underlying ratio by raising each prime (starting from two) to the power in the factor list. For instance, going up two perfect fifths and down three major thirds yields: -- @ -- diagramToRatio (Factors [4,2,-3]) = (2 ^^ 4) * (3 ^^ 2) * (5 ^^ (-3)) = 144/125 -- @ diagramToRatio :: Fractional a => PitchFactorDiagram -> a diagramToRatio = product . zipWith (^^) (map fromIntegral primes) . getFactors -- | Similar to 'diagramToRatio', but simplifies the resulting ratio to the simplest ratio within @0.05@. diagramToFloatyRatio :: PitchFactorDiagram -> Rational diagramToFloatyRatio = flip approxRational 0.05 . diagramToRatio -- | Convert a PFD to its decimal number of semitones. Useful for approximating weird ratios in a twelvetone scale: -- @ -- diagramToSemi (normalizePFD $ Factors [0,0,0,1]) = diagramToSemi (countPFD (7/4)) = 9.688259064691248 -- @ diagramToSemi :: Floating a => PitchFactorDiagram -> a diagramToSemi = (12 *) . logBase 2 . realToFrac . diagramToRatio . normalizePFD -- | Normalize a PFD by raising or lowering it by octaves until its ratio lies between @1@ (unison) and @2@ (one octave up). -- This operation is idempotent. normalizePFD :: PitchFactorDiagram -> PitchFactorDiagram normalizePFD (Factors []) = Factors [] normalizePFD (Factors (_:xs)) = Factors $ (negate . floor . logBase 2 . realToFrac . diagramToRatio . Factors . (0:) $ xs) : xs -- | Same as 'countPFD' but makes an effort to simplify the ratio from a 'Double' slightly to the simplest rational number within @0.0001@. countPFDFuzzy :: Double -> PitchFactorDiagram countPFDFuzzy = countPFD . flip approxRational 0.0001 -- | Calculates the 'PitchFactorDiagram' corresponding to a given frequency ratio by finding prime factors of the numerator and denominator. countPFD :: Rational -> PitchFactorDiagram countPFD k = Factors $ go (primeFactors $ numerator k,primeFactors $ denominator k) primes where count = (genericLength .) . filter go :: ([Integer],[Integer]) -> [Integer] -> [Integer] go ([],[]) _ = [] go (nfs,dfs) (p:ps) = count (==p) nfs - count (==p) dfs : go (filter (/=p) nfs,filter (/=p) dfs) ps -- | Converts a PFD into an operation on frequencies. @'intervalOf' 'Boopadoop.Interval.perfectFifth' 'Boopadoop.concertA'@ is the just intonation E5. intervalOf :: PitchFactorDiagram -> (Double -> Double) intervalOf = (*) . (realToFrac . diagramToRatio) -- | Scale a PFD by raising the underlying ratio to the given power. @'scalePFD' 2 'Boopadoop.Interval.perfectFifth' = 'addPFD' 'Boopadoop.Interval.octave' 'Boopadoop.Interval.majorSecond'@ scalePFD :: Integer -> PitchFactorDiagram -> PitchFactorDiagram scalePFD lambda = Factors . map (*lambda) . getFactors -- | Inverts a PFD. @'invertPFD' = 'scalePFD' (-1)@ invertPFD :: PitchFactorDiagram -> PitchFactorDiagram invertPFD = scalePFD (-1) -- | Adds two PFDs together by multiplying their ratios. @'addPFD' minorThird 'Boopadoop.Interval.majorThird' = 'Boopadoop.Interval.perfectFifth'@ addPFD :: PitchFactorDiagram -> PitchFactorDiagram -> PitchFactorDiagram addPFD a b = Factors . map getSum $ salign (map Sum $ getFactors a) (map Sum $ getFactors b) -- | Prints the natural numbers from the given value up to @128@, highlighting primes and powers of two. -- Interesting musical intervals are build out of the relative distance of a prime between the two -- nearest powers of two. printTheSequence :: Int -> IO () printTheSequence k | k > 128 = putStrLn "" | k .&. (k-1) == 0 = putStr ("|\n[" ++ show k ++ "]") >> printTheSequence (k+1) | isPrime k = putStr ("(" ++ show k ++ ")") >> printTheSequence (k+1) | otherwise = putStr " . " >> printTheSequence (k+1)