packages feed

discord-haskell-voice-2.2.0: src/Discord/Voice/Conduit.hs

{-# LANGUAGE ImportQualifiedPost #-}
{-|
Module      : Discord.Voice.Conduit
Description : Convenient Conduits for transforming voice data
Copyright   : (c) Yuto Takano (2021)
License     : MIT
Maintainer  : moa17stock@gmail.com

This module provides convenient Conduits (see the @conduit@ package for an
introduction to conduits, but essentially streaming pipes) for transforming
audio data, to be used with the apostrophe-marked functions in "Discord.Voice".

The apostrophe-marked functions, such as 'playFile'', take as argument a
Conduit of the following type:

@
ConduitT B.ByteString B.ByteString (ResourceT DiscordHandler) ()
@

That is, the Conduit's needs to take @ByteString@ values from the upstream and
give to the downstream, @ByteString@s. This ByteString is formatted according to
a 16-bit signed little-endian representation of PCM data, with a sample rate of
48kHz. Since ByteStrings are stored as 'Word8' (8-bits) internally, this module
provides conduits to pack every two bytes into a single signed 16-bit 'Int16',
and vice versa. See 'packInt16C' and 'unpackInt16C'.

Since the audio data is stereo, there are also conduits provided that pack to
and unpack from tuples of @(left, right) :: (Int16, Int16)@ values.

These enable us to have an easier type signature to work with when performing
arithmetics on audio data:

@
yourConduit :: ConduitT Int16 Int16 (ResourceT DiscordHandler) ()

playYouTube' "never gonna give you up" $ packInt16C .| yourConduit .| unpackInt16C
@

Despite the pack/unpack being a common pattern, unfortunately due to library
design, it is not the default behaviour for apostrophe-marked functions (the
reason being that the non-apostrophe-marked-functions are simply aliases for
the apostrophe ones but with a @awaitForever yield@ conduit; this means adding
pack/unpack to the apostrophe versions would slow down the streaming of
untransformed data).

An example usage of these conduits is:

@
runVoice $ do
    join (read "123456789012345") (read "67890123456789012")
    playFile' "Lost in the Woods.mp3" $ packTo16CT .| toMono .| packFrom16CT
@
-}
module Discord.Voice.Conduit
    (
      -- * Conduits to transform ByteString into workable Int16 data
      packInt16C
    , packInt16CT
    , unpackInt16C
    , unpackInt16CT
      -- * Common audio operations exemplars (see source for inspiration)
    , toMono
    )
where

import Conduit
import Data.Bits ( shiftL, shiftR, (.|.) )
import Data.ByteString qualified as B
import Data.Int ( Int16 )
import Data.Word ( Word16, Word8 )
import Discord

-- | A conduit that transforms 16-bit signed little-endian PCM ByteString to
-- streams of Int16 values. The Int16 values are in the range [-32768, 32767]
-- and alternate between left and right channels (stereo audio). See
-- 'packInt16CT' for a version that produces tuples for both channels.
packInt16C :: ConduitT B.ByteString Int16 (ResourceT DiscordHandler) ()
packInt16C = chunksOfExactlyCE 2 .| loop
  where
    loop = awaitForever $ \bytes -> do
        let [b1, b2] = B.unpack bytes
        -- little-endian arrangement
        yield $ (fromIntegral $ (shiftL (fromIntegral b2 :: Word16) 8) .|. (fromIntegral b1 :: Word16) :: Int16)

-- | A conduit that transforms streams of Int16 values to a ByteString, laid
-- out in 16-bit little-endian PCM format. Alternating inputs to this conduit
-- will be taken as left and right channels. See 'unpackInt16CT' for a version
-- that takes a stream of tuples of both channels.
unpackInt16C :: ConduitT Int16 B.ByteString (ResourceT DiscordHandler) ()
unpackInt16C = awaitForever $ \i ->
    yield $ B.pack
        [ fromIntegral i :: Word8
        , fromIntegral $ shiftR (fromIntegral i :: Word16) 8 :: Word8
        ]

-- | A conduit that transforms 16-bit signed little-endian PCM ByteString to
-- streams of (left, right)-tupled Int16 values.
packInt16CT :: ConduitT B.ByteString (Int16, Int16) (ResourceT DiscordHandler) ()
packInt16CT = chunksOfExactlyCE 4 .| loop
  where
    loop = awaitForever $ \bytes -> do
        let [b1, b2, b3, b4] = B.unpack bytes
        -- little-endian arrangement
        let left = (fromIntegral $ (shiftL (fromIntegral b2 :: Word16) 8) .|. (fromIntegral b1 :: Word16) :: Int16)
        let right = (fromIntegral $ (shiftL (fromIntegral b4 :: Word16) 8) .|. (fromIntegral b3 :: Word16) :: Int16)
        yield (left, right)

-- | A conduit that transforms (left, right)-tupled Int16 values into 16-bit
-- signed little-endian PCM ByteString.
unpackInt16CT :: ConduitT (Int16, Int16) B.ByteString (ResourceT DiscordHandler) ()
unpackInt16CT = awaitForever $ \(l, r) ->
    yield $ B.pack
        [ fromIntegral l :: Word8
        , fromIntegral $ shiftR (fromIntegral l :: Word16) 8 :: Word8
        , fromIntegral r :: Word8
        , fromIntegral $ shiftR (fromIntegral r :: Word16) 8 :: Word8
        ]

-- | A conduit to transform stereo audio to mono audio by taking the average
-- of the left and right channel values.
toMono :: ConduitT (Int16, Int16) (Int16, Int16) (ResourceT DiscordHandler) ()
toMono = awaitForever $ \(l, r) -> do
    -- take the average of the left and right channels
    let avg = l `div` 2 + r `div` 2
    yield (avg, avg)