mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 12:33:40 +00:00
249 lines
6.6 KiB
Haskell
249 lines
6.6 KiB
Haskell
{-# LANGUAGE ScopedTypeVariables #-}
|
|
{-# LANGUAGE TupleSections #-}
|
|
module Lib.Sound where
|
|
|
|
import Startlude hiding ( rotate )
|
|
|
|
import Control.Monad.Trans.Cont
|
|
import Control.Carrier.Writer.Strict
|
|
import System.FileLock
|
|
|
|
import Util.Function
|
|
|
|
-- General
|
|
|
|
rotate :: forall a . (Enum a, Bounded a) => a -> Int -> a
|
|
rotate base step = toEnum $ (fromEnum base + step) `mod` size + (fromEnum $ minBound @a)
|
|
where size = fromEnum (maxBound @a) - fromEnum (minBound @a) + 1
|
|
{-# INLINE rotate #-}
|
|
|
|
|
|
-- Interface
|
|
|
|
export :: IO ()
|
|
export = writeFile "/sys/class/pwm/pwmchip0/export" "0"
|
|
|
|
unexport :: IO ()
|
|
unexport = writeFile "/sys/class/pwm/pwmchip0/unexport" "0"
|
|
|
|
|
|
-- Constants
|
|
|
|
semitoneK :: Double
|
|
semitoneK = 2 ** (1 / 12)
|
|
{-# INLINE semitoneK #-}
|
|
|
|
|
|
-- Data Types
|
|
|
|
data Note = Note Semitone Word8
|
|
deriving (Eq, Show)
|
|
|
|
data Semitone =
|
|
C
|
|
| Db
|
|
| D
|
|
| Eb
|
|
| E
|
|
| F
|
|
| Gb
|
|
| G
|
|
| Ab
|
|
| A
|
|
| Bb
|
|
| B
|
|
deriving (Eq, Ord, Show, Enum, Bounded)
|
|
|
|
newtype Interval = Interval Int deriving newtype (Num)
|
|
|
|
data TimeSlice =
|
|
Sixteenth
|
|
| Eighth
|
|
| Quarter
|
|
| Half
|
|
| Whole
|
|
| Triplet TimeSlice
|
|
| Dot TimeSlice
|
|
| Tie TimeSlice TimeSlice
|
|
deriving (Eq, Show)
|
|
|
|
|
|
-- Theory Manipulation
|
|
|
|
interval :: Interval -> Note -> Note
|
|
interval (Interval n) (Note step octave) =
|
|
let (o', s') = n `quotRem` 12
|
|
newStep = step `rotate` s'
|
|
offset = if
|
|
| newStep > step && s' < 0 -> subtract 1
|
|
| newStep < step && s' > 0 -> (+ 1)
|
|
| otherwise -> id
|
|
in Note newStep (offset $ octave + fromIntegral o')
|
|
{-# INLINE interval #-}
|
|
|
|
minorThird :: Interval
|
|
minorThird = Interval 3
|
|
|
|
majorThird :: Interval
|
|
majorThird = Interval 3
|
|
|
|
fourth :: Interval
|
|
fourth = Interval 5
|
|
|
|
fifth :: Interval
|
|
fifth = Interval 7
|
|
|
|
circleOfFourths :: Note -> [Note]
|
|
circleOfFourths = iterate (interval fourth)
|
|
|
|
circleOfFifths :: Note -> [Note]
|
|
circleOfFifths = iterate (interval fifth)
|
|
|
|
-- Theory To Interface Target
|
|
|
|
noteFreq :: Note -> Double
|
|
noteFreq (Note semi oct) = semitoneK ** (fromIntegral $ fromEnum semi) * c0 * (2 ** fromIntegral oct)
|
|
where
|
|
a4 = 440
|
|
c0 = a4 / (semitoneK ** 9) / (2 ** 4)
|
|
|
|
-- tempo is in quarters per minute
|
|
timeSliceToMicro :: Word16 -> TimeSlice -> Int
|
|
timeSliceToMicro tempo timeSlice = case timeSlice of
|
|
Sixteenth -> uspq `div` 4
|
|
Eighth -> uspq `div` 2
|
|
Quarter -> uspq
|
|
Half -> uspq * 2
|
|
Whole -> uspq * 4
|
|
Triplet timeSlice' -> timeSliceToMicro tempo timeSlice' * 2 `div` 3
|
|
Dot timeSlice' -> timeSliceToMicro tempo timeSlice' * 3 `div` 2
|
|
Tie ts1 ts2 -> timeSliceToMicro tempo ts1 + timeSliceToMicro tempo ts2
|
|
where uspq = floor @Double $ 60 / fromIntegral tempo * 1_000_000
|
|
|
|
|
|
-- Player
|
|
|
|
periodFile :: FilePath
|
|
periodFile = "/sys/class/pwm/pwmchip0/pwm0/period"
|
|
|
|
dutyFile :: FilePath
|
|
dutyFile = "/sys/class/pwm/pwmchip0/pwm0/duty_cycle"
|
|
|
|
switchFile :: FilePath
|
|
switchFile = "/sys/class/pwm/pwmchip0/pwm0/enable"
|
|
|
|
play :: Note -> IO ()
|
|
play note' = do
|
|
prd' <- readFile periodFile
|
|
case prd' of
|
|
"0\n" -> writeFile periodFile "1000"
|
|
_ -> pure ()
|
|
let prd = round @_ @Int $ 1 / noteFreq note' * 1_000_000_000 -- pwm needs it in nanos
|
|
writeFile dutyFile "0"
|
|
writeFile periodFile (show prd)
|
|
writeFile dutyFile (show $ prd `div` 2)
|
|
writeFile switchFile "1"
|
|
|
|
stop :: IO ()
|
|
stop = writeFile switchFile "0"
|
|
|
|
playForDuration :: Note -> Int -> IO ()
|
|
playForDuration note' duration = handle @SomeException (\e -> stop *> throwIO e) $ do
|
|
play note'
|
|
threadDelay (floor @Double $ fromIntegral duration * 0.95)
|
|
stop
|
|
threadDelay (ceiling @Double $ fromIntegral duration * 0.05)
|
|
|
|
time :: IO () -> IO (UTCTime, UTCTime)
|
|
time action = do
|
|
t0 <- getCurrentTime
|
|
action
|
|
t1 <- getCurrentTime
|
|
pure (t0, t1)
|
|
|
|
playSong :: Word16 -> Song -> IO ()
|
|
playSong = flip runCont id .* playSong'
|
|
{-# INLINE playSong #-}
|
|
|
|
playSongTimed :: Word16 -> Song -> IO (UTCTime, UTCTime)
|
|
playSongTimed tempo song = runCont (playSong' tempo song) time
|
|
{-# INLINE playSongTimed #-}
|
|
|
|
playSong' :: Word16 -> Song -> Cont (IO b) (IO ())
|
|
playSong' tempo song = cont $ \f -> bracket acquire release $ \_ -> f $ do
|
|
for_ song $ \(n, ts) -> do
|
|
let duration = timeSliceToMicro tempo ts
|
|
case n of
|
|
Nothing -> threadDelay duration
|
|
Just x -> playForDuration x duration
|
|
where
|
|
soundLock = "/root/agent/sound.lock"
|
|
acquire = do
|
|
l <- lockFile soundLock Exclusive
|
|
export
|
|
pure l
|
|
release l = do
|
|
void $ try @SomeException stop
|
|
void $ try @SomeException unexport
|
|
unlockFile l
|
|
|
|
|
|
-- Songs
|
|
|
|
type Song = [(Maybe Note, TimeSlice)]
|
|
|
|
marioDeath :: Song
|
|
marioDeath =
|
|
[ (Just $ Note B 4, Quarter)
|
|
, (Just $ Note F 5, Quarter)
|
|
, (Nothing , Quarter)
|
|
, (Just $ Note F 5, Quarter)
|
|
, (Just $ Note F 5, Triplet Half)
|
|
, (Just $ Note E 5, Triplet Half)
|
|
, (Just $ Note D 5, Triplet Half)
|
|
, (Just $ Note C 5, Quarter)
|
|
, (Just $ Note E 4, Quarter)
|
|
, (Nothing , Quarter)
|
|
, (Just $ Note E 4, Quarter)
|
|
, (Just $ Note C 4, Half)
|
|
]
|
|
|
|
marioPowerUp :: Song
|
|
marioPowerUp =
|
|
[ (Just $ Note G 4 , Triplet Eighth)
|
|
, (Just $ Note B 4 , Triplet Eighth)
|
|
, (Just $ Note D 5 , Triplet Eighth)
|
|
, (Just $ Note G 5 , Triplet Eighth)
|
|
, (Just $ Note B 5 , Triplet Eighth)
|
|
, (Just $ Note Ab 4, Triplet Eighth)
|
|
, (Just $ Note C 5 , Triplet Eighth)
|
|
, (Just $ Note Eb 5, Triplet Eighth)
|
|
, (Just $ Note Ab 5, Triplet Eighth)
|
|
, (Just $ Note C 5 , Triplet Eighth)
|
|
, (Just $ Note Bb 4, Triplet Eighth)
|
|
, (Just $ Note D 5 , Triplet Eighth)
|
|
, (Just $ Note F 5 , Triplet Eighth)
|
|
, (Just $ Note Bb 5, Triplet Eighth)
|
|
, (Just $ Note D 6 , Triplet Eighth)
|
|
]
|
|
|
|
marioCoin :: Song
|
|
marioCoin = [(Just $ Note B 5, Eighth), (Just $ Note E 6, Tie (Dot Quarter) Half)]
|
|
|
|
updateInProgress :: Song
|
|
updateInProgress = take 6 $ (, Triplet Eighth) . Just <$> circleOfFifths (Note A 3)
|
|
|
|
beethoven :: Song
|
|
beethoven = run . execWriter $ do
|
|
tell $ replicate 3 (Just $ Note E 5, Eighth)
|
|
tell $ [(Just $ Note C 5, Half)]
|
|
tell $ [(Nothing @Note, Eighth)]
|
|
tell $ replicate 3 (Just $ Note D 5, Eighth)
|
|
tell $ [(Just $ Note B 5, Half)]
|
|
|
|
restoreActionInProgress :: Song
|
|
restoreActionInProgress = take 5 $ (, Triplet Eighth) . Just <$> circleOfFourths (Note C 4)
|
|
|
|
backupActionInProgress :: [(Maybe Note, TimeSlice)]
|
|
backupActionInProgress = reverse restoreActionInProgress
|