{-# 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)