packages feed

taffybar-7.2.3: src/System/Taffybar/Information/Audio.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

-- |
-- Module      : System.Taffybar.Information.Audio
-- Copyright   : (c) Ivan A. Malison
-- License     : BSD3-style (see LICENSE)
--
-- Maintainer  : Ivan A. Malison
-- Stability   : unstable
-- Portability : unportable
--
-- Unified audio information for PulseAudio and PipeWire/WirePlumber systems.
--
-- PulseAudio is preferred when its DBus protocol is available. On modern
-- PipeWire systems where @pipewire-pulse@ does not expose that DBus protocol,
-- this module falls back to the WirePlumber backend.
module System.Taffybar.Information.Audio
  ( AudioBackend (..),
    AudioInfo (..),
    defaultAudioPulseSink,
    defaultAudioWirePlumberNode,
    audioBackendAvailable,
    getAudioInfo,
    getAudioInfoChan,
    getAudioInfoState,
    getAudioInfoChanAndVar,
    getAudioInfoChanFor,
    getAudioInfoStateFor,
    toggleAudioMute,
    adjustAudioVolume,
    pulseSinkToWirePlumberNode,
  )
where

import Control.Concurrent (forkIO)
import Control.Concurrent.MVar
import Control.Concurrent.STM.TChan
import Control.Monad (forever, when)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.STM (atomically)
import Data.Proxy (Proxy (..))
#ifdef WITH_WIREPLUMBER
import qualified Data.Text as T
#endif
import GHC.TypeLits (KnownSymbol, SomeSymbol (..), Symbol, someSymbolVal, symbolVal)
import System.Taffybar.Context (TaffyIO)
import qualified System.Taffybar.Information.PulseAudio as PulseAudio
#ifdef WITH_WIREPLUMBER
import qualified System.Taffybar.Information.WirePlumber as WirePlumber
#endif

data AudioBackend
  = PulseAudioBackend
  | WirePlumberBackend
  deriving (Eq, Show)

data AudioInfo = AudioInfo
  { audioVolumePercent :: Maybe Int,
    audioMuted :: Maybe Bool,
    audioNodeName :: String,
    audioBackend :: AudioBackend
  }
  deriving (Eq, Show)

defaultAudioPulseSink :: String
defaultAudioPulseSink = "@DEFAULT_SINK@"

defaultAudioWirePlumberNode :: String
defaultAudioWirePlumberNode = "@DEFAULT_AUDIO_SINK@"

newtype AudioInfoChanVar (a :: Symbol)
  = AudioInfoChanVar (TChan (Maybe AudioInfo), MVar (Maybe AudioInfo))

audioBackendAvailable :: IO AudioBackend
audioBackendAvailable = do
#ifdef WITH_WIREPLUMBER
  pulseAvailable <- PulseAudio.pulseAudioAvailable
  pure $
    if pulseAvailable
      then PulseAudioBackend
      else WirePlumberBackend
#else
  pure PulseAudioBackend
#endif

getAudioInfo :: String -> String -> IO (Maybe AudioInfo)
getAudioInfo pulseSink wirePlumberNode = do
  backend <- audioBackendAvailable
  case backend of
    PulseAudioBackend -> fmap fromPulseAudioInfo <$> PulseAudio.getPulseAudioInfo pulseSink
#ifdef WITH_WIREPLUMBER
    WirePlumberBackend -> fmap fromWirePlumberInfo <$> WirePlumber.getWirePlumberInfo wirePlumberNode
#else
    WirePlumberBackend -> wirePlumberNode `seq` pure Nothing
#endif

getAudioInfoChan :: String -> String -> TaffyIO (TChan (Maybe AudioInfo))
getAudioInfoChan pulseSink wirePlumberNode =
  case someSymbolVal $ encodeAudioSpecs pulseSink wirePlumberNode of
    SomeSymbol (Proxy :: Proxy sym) -> getAudioInfoChanFor @sym

getAudioInfoState :: String -> String -> TaffyIO (Maybe AudioInfo)
getAudioInfoState pulseSink wirePlumberNode =
  case someSymbolVal $ encodeAudioSpecs pulseSink wirePlumberNode of
    SomeSymbol (Proxy :: Proxy sym) -> getAudioInfoStateFor @sym

getAudioInfoChanAndVar :: String -> String -> TaffyIO (TChan (Maybe AudioInfo), MVar (Maybe AudioInfo))
getAudioInfoChanAndVar pulseSink wirePlumberNode =
  case someSymbolVal $ encodeAudioSpecs pulseSink wirePlumberNode of
    SomeSymbol (Proxy :: Proxy sym) -> do
      AudioInfoChanVar pair <- getAudioInfoChanVarFor @sym
      pure pair

getAudioInfoChanFor :: forall a. (KnownSymbol a) => TaffyIO (TChan (Maybe AudioInfo))
getAudioInfoChanFor = do
  AudioInfoChanVar (chan, _) <- getAudioInfoChanVarFor @a
  pure chan

getAudioInfoStateFor :: forall a. (KnownSymbol a) => TaffyIO (Maybe AudioInfo)
getAudioInfoStateFor = do
  AudioInfoChanVar (_, var) <- getAudioInfoChanVarFor @a
  liftIO $ readMVar var

getAudioInfoChanVarFor :: forall a. (KnownSymbol a) => TaffyIO (AudioInfoChanVar a)
getAudioInfoChanVarFor = do
  let (pulseSink, wirePlumberNode) = decodeAudioSpecs $ symbolVal (Proxy @a)
  backend <- liftIO audioBackendAvailable
  case backend of
    PulseAudioBackend -> do
      (pulseChan, pulseVar) <- PulseAudio.getPulseAudioInfoChanAndVar pulseSink
      buildMappedChanVar fromPulseAudioInfo pulseChan pulseVar
#ifdef WITH_WIREPLUMBER
    WirePlumberBackend -> do
      (wireChan, wireVar) <- WirePlumber.getWirePlumberInfoChanAndVar wirePlumberNode
      buildMappedChanVar fromWirePlumberInfo wireChan wireVar
#else
    WirePlumberBackend -> liftIO $ wirePlumberNode `seq` buildUnavailableChanVar
#endif

toggleAudioMute :: String -> String -> IO Bool
toggleAudioMute pulseSink wirePlumberNode = do
  backend <- audioBackendAvailable
  case backend of
    PulseAudioBackend -> PulseAudio.togglePulseAudioMute pulseSink
#ifdef WITH_WIREPLUMBER
    WirePlumberBackend -> WirePlumber.toggleWirePlumberMute wirePlumberNode
#else
    WirePlumberBackend -> wirePlumberNode `seq` pure False
#endif

adjustAudioVolume :: String -> String -> Int -> IO Bool
adjustAudioVolume pulseSink wirePlumberNode deltaPercent = do
  backend <- audioBackendAvailable
  case backend of
    PulseAudioBackend -> PulseAudio.adjustPulseAudioVolume pulseSink deltaPercent
#ifdef WITH_WIREPLUMBER
    WirePlumberBackend -> WirePlumber.adjustWirePlumberVolume wirePlumberNode deltaPercent
#else
    WirePlumberBackend -> wirePlumberNode `seq` pure False
#endif

pulseSinkToWirePlumberNode :: String -> String
pulseSinkToWirePlumberNode "" = defaultAudioWirePlumberNode
pulseSinkToWirePlumberNode sink
  | sink == defaultAudioPulseSink = defaultAudioWirePlumberNode
  | otherwise = sink

buildMappedChanVar ::
  (backendInfo -> AudioInfo) ->
  TChan (Maybe backendInfo) ->
  MVar (Maybe backendInfo) ->
  TaffyIO (AudioInfoChanVar a)
buildMappedChanVar convert backendChan backendVar = do
  initialInfo <- liftIO $ fmap convert <$> readMVar backendVar
  liftIO $ do
    chan <- newBroadcastTChanIO
    var <- newMVar initialInfo
    atomically $ writeTChan chan initialInfo
    _ <- forkIO $ do
      ourChan <- atomically $ dupTChan backendChan
      forever $ do
        info <- fmap convert <$> atomically (readTChan ourChan)
        oldInfo <- swapMVar var info
        when (oldInfo /= info) $
          atomically $
            writeTChan chan info
    pure $ AudioInfoChanVar (chan, var)

#ifndef WITH_WIREPLUMBER
buildUnavailableChanVar :: IO (AudioInfoChanVar a)
buildUnavailableChanVar = do
  chan <- newBroadcastTChanIO
  var <- newMVar Nothing
  atomically $ writeTChan chan Nothing
  pure $ AudioInfoChanVar (chan, var)
#endif

fromPulseAudioInfo :: PulseAudio.PulseAudioInfo -> AudioInfo
fromPulseAudioInfo info =
  AudioInfo
    { audioVolumePercent = PulseAudio.pulseAudioVolumePercent info,
      audioMuted = PulseAudio.pulseAudioMuted info,
      audioNodeName = PulseAudio.pulseAudioSinkName info,
      audioBackend = PulseAudioBackend
    }

#ifdef WITH_WIREPLUMBER
fromWirePlumberInfo :: WirePlumber.WirePlumberInfo -> AudioInfo
fromWirePlumberInfo info =
  AudioInfo
    { audioVolumePercent = Just $ round $ WirePlumber.wirePlumberVolume info * 100,
      audioMuted = Just $ WirePlumber.wirePlumberMuted info,
      audioNodeName = T.unpack $ WirePlumber.wirePlumberNodeName info,
      audioBackend = WirePlumberBackend
    }
#endif

encodeAudioSpecs :: String -> String -> String
encodeAudioSpecs pulseSink wirePlumberNode = pulseSink ++ "\n" ++ wirePlumberNode

decodeAudioSpecs :: String -> (String, String)
decodeAudioSpecs encoded =
  case break (== '\n') encoded of
    (pulseSink, '\n' : wirePlumberNode) -> (pulseSink, wirePlumberNode)
    (pulseSink, _) -> (pulseSink, pulseSinkToWirePlumberNode pulseSink)