Copyright | (c) Yuto Takano (2021) |
---|---|
License | MIT |
Maintainer | moa17stock@gmail.com |
Safe Haskell | None |
Language | Haskell2010 |
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
- data Voice a
- runVoice :: Voice () -> DiscordHandler (Either VoiceError ())
- liftDiscord :: DiscordHandler a -> Voice a
- join :: GuildId -> ChannelId -> Voice (Voice ())
- play :: ConduitT () ByteString (ResourceT DiscordHandler) () -> Voice ()
- playPCMFile :: FilePath -> Voice ()
- playPCMFile' :: FilePath -> ConduitT ByteString ByteString (ResourceT DiscordHandler) () -> Voice ()
- playFile :: FilePath -> Voice ()
- playFile' :: FilePath -> ConduitT ByteString ByteString (ResourceT DiscordHandler) () -> Voice ()
- playFileWith :: String -> (String -> [String]) -> FilePath -> Voice ()
- playFileWith' :: String -> (String -> [String]) -> String -> ConduitT ByteString ByteString (ResourceT DiscordHandler) () -> Voice ()
- playYouTube :: String -> Voice ()
- playYouTube' :: String -> ConduitT ByteString ByteString (ResourceT DiscordHandler) () -> Voice ()
- defaultFFmpegArgs :: FilePath -> [String]
Monad for Voice Operations
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
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 Source | ByteString Conduit | PCM Encoded File | Arbitrary Audio File | YouTube Search/Video | ||
---|---|---|---|---|---|---|
Basic | play | playPCMFile | playFile | playFileWith | playYouTube | playYouTubeWith |
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 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.
:: 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 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.
:: 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.
:: 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.
:: 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 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'
.
:: 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"