discord-haskell-voice-2.2.1: Voice support for discord-haskell.
Copyright(c) Yuto Takano (2021)
LicenseMIT
Maintainermoa17stock@gmail.com
Safe HaskellNone
LanguageHaskell2010

Discord.Voice

Description

Welcome to discord-haskell-voice! This library provides you with a high-level interface for interacting with Discord's Voice API, building on top of the discord-haskell library by Karl.

For a quick intuitive introduction to what this library enables you to do, see the following snippet of code:

rickroll :: Channel -> DiscordHandler ()
rickroll c@(ChannelVoice {}) = do
    result <- runVoice $ do
        join (channelGuild c) (channelId c)
        playYouTube "https://www.youtube.com/watch?v=dQw4w9WgXcQ"

    case result of
        Left err -> liftIO $ print err
        Right _  -> pure ()

We can see that this library introduces a dedicated monad for voice operations, which opaquely guarantees that you won't accidentally keep hold of a closed voice connection, or try to use it after a network error had occurred.

You'll also see further down the docs, that you can use conduit to stream arbitrary ByteString data as audio, as well as manipulate and transform streams using its interface. This is quite a powerful feature!

Let's dive in :)

Synopsis

Monad for Voice Operations

data Voice a Source #

Voice is a newtype Monad containing a composition of ReaderT and ExceptT transformers over the DiscordHandler monad. It holds references to voice connections/threads. The content of the reader handle is strictly internal and is hidden deliberately behind the newtype wrapper.

Developer Note: ExceptT is on the base rather than ReaderT, so that when a critical exception/error occurs in Voice, it can propagate down the transformer stack, kill the threads referenced in the Reader state as necessary, and halt the entire computation and return to DiscordHandler. If ExceptT were on top of ReaderT, then errors would be swallowed before it propagates below ReaderT, and the monad would not halt there, continuing computation with an unstable state.

Instances

Instances details
Monad Voice Source # 
Instance details

Defined in Discord.Internal.Types.VoiceCommon

Methods

(>>=) :: Voice a -> (a -> Voice b) -> Voice b #

(>>) :: Voice a -> Voice b -> Voice b #

return :: a -> Voice a #

Functor Voice Source # 
Instance details

Defined in Discord.Internal.Types.VoiceCommon

Methods

fmap :: (a -> b) -> Voice a -> Voice b #

(<$) :: a -> Voice b -> Voice a #

MonadFail Voice Source #

MonadFail is for internal use, identical in function to the MonadFail instance of ReaderT.

Instance details

Defined in Discord.Internal.Types.VoiceCommon

Methods

fail :: String -> Voice a #

Applicative Voice Source # 
Instance details

Defined in Discord.Internal.Types.VoiceCommon

Methods

pure :: a -> Voice a #

(<*>) :: Voice (a -> b) -> Voice a -> Voice b #

liftA2 :: (a -> b -> c) -> Voice a -> Voice b -> Voice c #

(*>) :: Voice a -> Voice b -> Voice b #

(<*) :: Voice a -> Voice b -> Voice a #

MonadIO Voice Source #

MonadIO gives the ability to perform liftIO.

Instance details

Defined in Discord.Internal.Types.VoiceCommon

Methods

liftIO :: IO a -> Voice a #

MonadThrow Voice Source #

MonadThrow, MonadCatch, and MonadMask are for internal use, to utilise exception handling functions like bracket.

Instance details

Defined in Discord.Internal.Types.VoiceCommon

Methods

throwM :: Exception e => e -> Voice a #

MonadCatch Voice Source # 
Instance details

Defined in Discord.Internal.Types.VoiceCommon

Methods

catch :: Exception e => Voice a -> (e -> Voice a) -> Voice a #

MonadMask Voice Source # 
Instance details

Defined in Discord.Internal.Types.VoiceCommon

Methods

mask :: ((forall a. Voice a -> Voice a) -> Voice b) -> Voice b #

uninterruptibleMask :: ((forall a. Voice a -> Voice a) -> Voice b) -> Voice b #

generalBracket :: Voice a -> (a -> ExitCase b -> Voice c) -> (a -> Voice b) -> Voice (b, c) #

MonadReader DiscordBroadcastHandle Voice Source #

MonadReader is for internal use, to read the held broadcast handle.

Instance details

Defined in Discord.Internal.Types.VoiceCommon

MonadError VoiceError Voice Source #

MonadError is for internal use, to propagate errors.

Instance details

Defined in Discord.Internal.Types.VoiceCommon

Methods

throwError :: VoiceError -> Voice a #

catchError :: Voice a -> (VoiceError -> Voice a) -> Voice a #

runVoice :: Voice () -> DiscordHandler (Either VoiceError ()) Source #

Execute the voice actions stored in the Voice monad.

A single mutex and sending packet channel is used throughout all voice connections within the actions, which enables multi-channel broadcasting. The following demonstrates how a single playback is streamed to multiple connections.

runVoice $ do
    join (read "123456789012345") (read "67890123456789012")
    join (read "098765432123456") (read "12345698765456709")
    playYouTube "https://www.youtube.com/watch?v=dQw4w9WgXcQ"

The return type of runVoice represents result status of the voice computation. It is isomorphic to Maybe, but the use of Either explicitly denotes that the correct/successful/Right behaviour is (), and that the potentially- existing value is of failure.

liftDiscord :: DiscordHandler a -> Voice a Source #

liftDiscord lifts a computation in DiscordHandler into a computation in Voice. This is useful for performing DiscordHandler actions inside the Voice monad.

Usage:

runVoice $ do
    join (read "123456789012345") (read "67890123456789012")
    liftDiscord $ void $ restCall $ R.CreateMessage (read "2938481828383") "Joined!"
    liftIO $ threadDelay 5e6
    playYouTube "Rate of Reaction of Sodium Hydroxide and Hydrochloric Acid"
    liftDiscord $ void $ restCall $ R.CreateMessage (read "2938481828383") "Finished!"
void $ restCall $ R.CreateMessage (read "2938481828383") "Finished all voice actions!"

Joining a Voice Channel

join :: GuildId -> ChannelId -> Voice (Voice ()) Source #

Join a specific voice channel, given the Guild and Channel ID of the voice channel. Since the Channel ID is globally unique, there is theoretically no need to specify the Guild ID, but it is provided until discord-haskell fully caches the mappings internally.

This function returns a Voice action that, when executed, will leave the joined voice channel. For example:

runVoice $ do
  leave <- join (read "123456789012345") (read "67890123456789012")
  playYouTube "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
  leave

The above use is not meaningful in practice, since runVoice will perform the appropriate cleanup and leaving as necessary at the end of all actions. However, it may be useful to interleave leave with other Voice actions.

Since the leave function will gracefully do nothing if the voice connection is already severed, it is safe to escape this function from the Voice monad and use it in a different context. That is, the following is allowed and is encouraged if you are building a /leave command of any sort:

-- On /play
runVoice $ do
  leave <- join (read "123456789012345") (read "67890123456789012")
  liftIO $ putMVar futureLeaveFunc leave
  forever $
    playYouTube "https://www.youtube.com/watch?v=dQw4w9WgXcQ"

-- On /leave, from a different thread
leave <- liftIO $ takeMVar futureLeaveFunc
runVoice leave

The above will join a voice channel, play a YouTube video, but immediately quit and leave the channel when the /leave command is received, regardless of the playback status.

Play Some Audio

play :: ConduitT () ByteString (ResourceT DiscordHandler) () -> Voice () Source #

play source plays some sound from the conduit source, provided in the form of 16-bit Little Endian PCM. The use of Conduit allows you to perform arbitrary lazy transformations of audio data, using all the advantages that Conduit brings. As the base monad for the Conduit is ResourceT DiscordHandler, you can access any DiscordHandler effects (through lift) or IO effects (through liftIO) in the conduit as well.

For a more specific interface that is easier to use, see the playPCMFile, playFile, and playYouTube functions.

import Conduit ( sourceFile )

runVoice $ do
  join gid cid
  play $ sourceFile "./audio/example.pcm"

More Accessible Variants

While play is the most fundamental way to play audio, it is often inconvenient to write a Conduit, especially if you want to perform common actions like streaming YouTube audio, or playing arbitrary audio files in arbitrary formats. This is why we provide a number of more accessible variants of play, which provide a more convenient interface to playing your favourite media.

Some of the functions in this section are marked with an apostrophe, which indicate that they accept a Conduit processor as an argument to manipulate the audio stream on the fly (such as changing volume).

The following table gives a comparative overview of all the functions provided in this module for playing audio:

Variant \ Audio SourceByteString ConduitPCM Encoded FileArbitrary Audio FileYouTube Search/Video
BasicplayplayPCMFileplayFileplayFileWithplayYouTubeplayYouTubeWith
Post-process audio-playPCMFile'playFile'playFileWith'playYouTube'playYouTubeWith'

The functions that end with -With accept arguments to specify executable names, and in the case of FFmpeg, any arguments to FFmpeg.

playPCMFile Source #

Arguments

:: FilePath

The path to the PCM file to play

-> Voice () 

playPCMFile file plays the sound stored in the file located at file, provided it is in the form of 16-bit Little Endian PCM. playPCMFile is defined as a handy alias for the following:

playPCMFile ≡ play . sourceFile

For a variant of this function that allows arbitrary transformations of the audio data through a conduit component, see playPCMFile'.

To play any other format, it will need to be transcoded using FFmpeg. See playFile for such usage.

playPCMFile' Source #

Arguments

:: FilePath

The path to the PCM file to play

-> ConduitT ByteString ByteString (ResourceT DiscordHandler) ()

Any processing that needs to be done on the audio data

-> Voice () 

playPCMFile' file processor plays the sound stored in the file located at file, provided it is in the form of 16-bit Little Endian PCM. Audio data will be passed through the processor conduit component, allowing arbitrary transformations to audio data before playback. playPCMFile' is defined as the following:

playPCMFile' file processor ≡ play $ sourceFile file .| processor

For a variant of this function with no processing, see playPCMFile.

To play any other format, it will need to be transcoded using FFmpeg. See playFile for such usage.

playFile Source #

Arguments

:: FilePath

The path to the audio file to play

-> Voice () 

playFile file plays the sound stored in the file located at file. It supports any format supported by FFmpeg by transcoding it, which means it can play a wide range of file types. This function expects "ffmpeg" to be available in the system PATH.

For a variant that allows you to specify the executable and/or any arguments, see playFileWith.

For a variant of this function that allows arbitrary transformations of the audio data through a conduit component, see playFile'.

If the file is already known to be in 16-bit little endian PCM, using playPCMFile is much more efficient as it does not go through FFmpeg.

playFile' Source #

Arguments

:: FilePath

The path to the audio file to play

-> ConduitT ByteString ByteString (ResourceT DiscordHandler) ()

Any processing that needs to be done on the audio data

-> Voice () 

playFile' file processor plays the sound stored in the file located at file. It supports any format supported by FFmpeg by transcoding it, which means it can play a wide range of file types. This function expects "ffmpeg" to be available in the system PATH. Audio data will be passed through the processor conduit component, allowing arbitrary transformations to audio data before playback.

For a variant that allows you to specify the executable and/or any arguments, see playFileWith'.

For a variant of this function with no processing, see playFile.

If the file is already known to be in 16-bit little endian PCM, using playPCMFile' is much more efficient as it does not go through FFmpeg.

playFileWith Source #

Arguments

:: String

The name of the FFmpeg executable

-> (String -> [String])

FFmpeg argument generator function, given the filepath

-> FilePath

The path to the audio file to play

-> Voice () 

playFileWith exe args file plays the sound stored in the file located at file, using the specified FFmpeg executable exe and an argument generator function args (see defaultFFmpegArgs for the default). It supports any format supported by FFmpeg by transcoding it, which means it can play a wide range of file types.

For a variant of this function that uses the "ffmpeg" executable in your PATH automatically, see playFile.

For a variant of this function that allows arbitrary transformations of the audio data through a conduit component, see playFileWith'.

If the file is known to be in 16-bit little endian PCM, using playPCMFile is more efficient as it does not go through FFmpeg.

playFileWith' Source #

Arguments

:: String

The name of the FFmpeg executable

-> (String -> [String])

FFmpeg argument generator function, given the filepath

-> String

The path to the audio file to play

-> ConduitT ByteString ByteString (ResourceT DiscordHandler) ()

Any processing that needs to be done on the audio data

-> Voice () 

playFileWith' exe args file processor plays the sound stored in the file located at file, using the specified FFmpeg executable exe and an argument generator function args (see defaultFFmpegArgs for the default). It supports any format supported by FFmpeg by transcoding it, which means it can play a wide range of file types. Audio data will be passed through the processor conduit component, allowing arbitrary transformations to audio data before playback.

For a variant of this function that uses the "ffmpeg" executable in your PATH automatically, see playFile'.

For a variant of this function with no processing, see playFileWith.

If the file is known to be in 16-bit little endian PCM, using playPCMFile' is more efficient as it does not go through FFmpeg.

playYouTube Source #

Arguments

:: String

Search query (or video URL)

-> Voice () 

playYouTube query plays the first result of searching query on YouTube. If a direct video URL is given, YouTube will always return that as the first result, which means playYouTube also supports playing links. It supports all videos, by automatically transcoding to PCM using FFmpeg. Since it streams the data instead of downloading it first, it can play live videos as well. This function expects "ffmpeg" and "youtube-dl" to be available in the system PATH.

For a variant that allows you to specify the executable and/or any arguments, see playYouTubeWith.

For a variant of this function that allows arbitrary transformations of the audio data through a conduit component, see playYouTube'.

playYouTube' Source #

Arguments

:: String

Search query (or video URL)

-> ConduitT ByteString ByteString (ResourceT DiscordHandler) ()

Any processing that needs to be done on the audio data

-> Voice () 

playYouTube' query processor plays the first result of searching query on YouTube. If a direct video URL is given, YouTube will always return that as the first result, which means playYouTube also supports playing links. It supports all videos, by automatically transcoding to PCM using FFmpeg. Since it streams the data instead of downloading it first, it can play live videos as well. This function expects "ffmpeg" and "youtube-dl" to be available in the system PATH. Audio data will be passed through the processor conduit component, allowing arbitrary transformations to audio data before playback.

For a variant that allows you to specify the executable and/or any arguments, see playYouTubeWith'.

For a variant of this function with no processing, see playYouTube.

defaultFFmpegArgs :: FilePath -> [String] Source #

defaultFFmpegArgs is a generator function for the default FFmpeg arguments used when streaming audio into 16-bit little endian PCM on stdout.

This function takes in the input file path as an argument, because FFmpeg arguments are position sensitive in relation to the placement of -i.

It is defined semantically as:

defaultFFmpegArgs FILE ≡ "-i FILE -f s16le -ar 48000 -ac 2 -loglevel warning pipe:1"