diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -2,6 +2,13 @@
 
 ## Unreleased changes
 
+
+## 2.3.0
+
+- Export `playYouTubeWith` and `playYoUTubeWith'` from `Discord.Voice`.
+- Update `discord-haskell` dependency bounds to `>= 1.12.0 && <= 1.13.0`.
+- Migrate from `lens` to `microlens`, following the `opus` package doing the same.
+
 ## 2.2.2
 
 - Update `discord-haskell` dependency to 1.12.0
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 # discord-haskell-voice
 
-![hackage version](https://img.shields.io/hackage/v/discord-haskell-voice?color=%235e5184)
-![discord-haskell version dependency](https://img.shields.io/badge/discord--haskell%20ver.-1.12.0-lightblue)
+[![hackage version](https://img.shields.io/hackage/v/discord-haskell-voice?color=%235e5184)](https://hackage.haskell.org/package/discord-haskell-voice)
+[![discord-haskell version dependency](https://img.shields.io/badge/discord--haskell-%3E=1.12.0%20%26%26%20%3C=1.13.0-lightblue)](https://hackage.haskell.org/package/discord-haskell)
 
 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
@@ -26,7 +26,7 @@
 rickrollHalfVolume :: Channel -> DiscordHandler ()
 rickrollHalfVolume c@(ChannelVoice {}) = do
     void $ runVoice $ do
-        join (channelGuild c) (channelId c)        
+        join (channelGuild c) (channelId c)
         let halfAmplitude = awaitForever $ \current ->
                 yield $ round $ fromIntegral current * 0.5
         playYouTube' "rickroll" $ packInt16C .| halfAmplitude .| unpackInt16C
@@ -36,17 +36,18 @@
 ## Requirements
 
 - The library uses [`saltine`](https://github.com/tel/saltine) for encryption
-and decryption of audio packets. This requires the appropriate libraries to be
-installed on your system. See their README for information.
+and decryption of audio packets. This requires libsodium to be installed on
+your system. See their README for information.
 - The library requires Opus libraries to be installed on your system. The
 `libopus-dev` package available on package repositories should be sufficient
-on most \*nix systems. Windows is unexplored yet (WSL works).
+on most \*nix systems. The `opus` brew package suffices on Mac. Windows
+is unexplored yet (WSL works).
 - If you are to use any variants of `playFile`, `playYouTube`, you will need
 FFmpeg installed. To specify a custom executable name, see the `-With` function
 variants.
 - If you are to use any variants of `playYouTube`, you will additionally need
 youtube-dl installed. This is used to get the stream URL to pass to FFmpeg. To
-specify a custom executable name, use `playYouTubeWith`.
+specify a custom executable name (such as yt-dlp), use `playYouTubeWith`.
 
 ## Features
 
@@ -54,8 +55,8 @@
 
 - Can join/leave Discord voice channels. It is possible to join multiple of them
 simultaneously (one per sever) and stream different contents to each.
-- It is also possible for many voice channels (across many servers) and play the
-same content, radio/subscriber-style.
+- It is also possible to join many voice channels (across many servers) and play
+the same content, radio/subscriber-style.
 - You can play arbitrary PCM audio, arbitrary audio (with FFmpeg), and arbitrary
 internet audio (with youtube-dl).
 - You can transform audio arbitrarily using Conduit.
@@ -84,8 +85,8 @@
 # --- myproject.cabal <truncated>
  build-depends:
       base >=4.7 && <5
-    , discord-haskell ==1.12.0
-    , discord-haskell-voice ==2.2.2
+    , discord-haskell ==1.13.0
+    , discord-haskell-voice ==2.3.0
 ```
 
 ### Stack
@@ -95,16 +96,16 @@
 ```
 # --- stack.yaml <truncated>
 extra-deps:
-- discord-haskell-1.12.0
-- discord-haskell-voice-2.2.2
+- discord-haskell-1.13.0
+- discord-haskell-voice-2.3.0
 ```
 
 ```
 # --- package.yaml <truncated>
 dependencies:
 - base >= 4.7 && < 5
-- discord-haskell == 1.12.0
-- discord-haskell-voice == 2.2.2
+- discord-haskell == 1.13.0
+- discord-haskell-voice == 2.3.0
 ```
 
 ## Documentation
diff --git a/discord-haskell-voice.cabal b/discord-haskell-voice.cabal
--- a/discord-haskell-voice.cabal
+++ b/discord-haskell-voice.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           discord-haskell-voice
-version:        2.2.2
+version:        2.3.0
 synopsis:       Voice support for discord-haskell.
 description:    Supplementary library to discord-haskell. See the project README on GitHub for more information. <https://github.com/yutotakano/discord-haskell-voice>
 category:       Network
@@ -49,9 +49,10 @@
     , base >=4.7 && <5
     , binary ==0.8.*
     , bytestring >=0.10.12.0 && <0.11
-    , conduit ==1.3.4.1
-    , discord-haskell ==1.12.0
-    , lens >=4.19.2 && <5
+    , conduit ==1.3.4.2
+    , discord-haskell >=1.12.0 && <=1.13.0
+    , microlens >=0.4.13.0
+    , microlens-th >=0.4.3.10
     , mtl ==2.2.2
     , network >=3.1.1.1 && <3.2
     , opus ==0.1.0.0
@@ -79,10 +80,11 @@
     , base >=4.7 && <5
     , binary ==0.8.*
     , bytestring >=0.10.12.0 && <0.11
-    , conduit ==1.3.4.1
-    , discord-haskell ==1.12.0
+    , conduit ==1.3.4.2
+    , discord-haskell >=1.12.0 && <=1.13.0
     , discord-haskell-voice
-    , lens >=4.19.2 && <5
+    , microlens >=0.4.13.0
+    , microlens-th >=0.4.3.10
     , mtl ==2.2.2
     , network >=3.1.1.1 && <3.2
     , optparse-applicative >=0.15.1.0 && <0.17
@@ -113,10 +115,11 @@
     , base >=4.7 && <5
     , binary ==0.8.*
     , bytestring >=0.10.12.0 && <0.11
-    , conduit ==1.3.4.1
-    , discord-haskell ==1.12.0
+    , conduit ==1.3.4.2
+    , discord-haskell >=1.12.0 && <=1.13.0
     , discord-haskell-voice
-    , lens >=4.19.2 && <5
+    , microlens >=0.4.13.0
+    , microlens-th >=0.4.3.10
     , mtl ==2.2.2
     , network >=3.1.1.1 && <3.2
     , opus ==0.1.0.0
diff --git a/examples/BasicMusicBot.hs b/examples/BasicMusicBot.hs
--- a/examples/BasicMusicBot.hs
+++ b/examples/BasicMusicBot.hs
@@ -106,7 +106,8 @@
                     let adjustVolume = awaitForever $ \current -> do
                             v' <- liftIO $ readTVarIO volume
                             yield $ round $ fromIntegral current * (fromIntegral v' / 100)
-                    playYouTube' x $ packInt16C .| adjustVolume .| unpackInt16C
+                    playYouTubeWith' "ffmpeg" defaultFFmpegArgs "yt-dlp" x $
+                        packInt16C .| adjustVolume .| unpackInt16C
 
     case result of
         Left e -> liftIO $ print e >> pure ()
diff --git a/src/Discord/Internal/Types/VoiceCommon.hs b/src/Discord/Internal/Types/VoiceCommon.hs
--- a/src/Discord/Internal/Types/VoiceCommon.hs
+++ b/src/Discord/Internal/Types/VoiceCommon.hs
@@ -32,7 +32,7 @@
 import Control.Concurrent ( Chan, MVar, ThreadId )
 import Control.Concurrent.BoundedChan qualified as Bounded
 import Control.Exception.Safe ( Exception, MonadMask, MonadCatch, MonadThrow )
-import Control.Lens ( makeFields )
+import Lens.Micro.TH ( makeFields )
 import Control.Monad.Except
 import Control.Monad.Reader
 import Data.ByteString qualified as B
diff --git a/src/Discord/Internal/Types/VoiceUDP.hs b/src/Discord/Internal/Types/VoiceUDP.hs
--- a/src/Discord/Internal/Types/VoiceUDP.hs
+++ b/src/Discord/Internal/Types/VoiceUDP.hs
@@ -26,7 +26,7 @@
 -}
 module Discord.Internal.Types.VoiceUDP where
 
-import Control.Lens ( makePrisms )
+import Lens.Micro
 import Data.Binary.Get
 import Data.Binary.Put
 import Data.Binary
@@ -47,6 +47,10 @@
     | MalformedPacket BL.ByteString
     deriving (Show, Eq)
 
+_IPDiscovery :: Traversal' VoiceUDPPacket (Integer, T.Text, Integer)
+_IPDiscovery f (IPDiscovery ssrc ip port) = (\(a, b, c) -> IPDiscovery a b c) <$> f (ssrc, ip, port)
+_IPDiscovery f packet = pure packet
+
 data VoiceUDPPacketHeader
     = Header Word8 Word8 Word16 Word32 Word32 
 
@@ -107,4 +111,4 @@
         putLazyByteString a
     put (MalformedPacket a) = putLazyByteString a
 
-$(makePrisms ''VoiceUDPPacket)
+-- $(makePrisms ''VoiceUDPPacket)
diff --git a/src/Discord/Internal/Types/VoiceWebsocket.hs b/src/Discord/Internal/Types/VoiceWebsocket.hs
--- a/src/Discord/Internal/Types/VoiceWebsocket.hs
+++ b/src/Discord/Internal/Types/VoiceWebsocket.hs
@@ -27,7 +27,7 @@
 module Discord.Internal.Types.VoiceWebsocket where
 
 import Control.Applicative ( (<|>) )
-import Control.Lens ( makePrisms )
+import Lens.Micro
 import Data.Aeson
 import Data.Aeson.Types
 import Data.Text qualified as T
@@ -50,6 +50,19 @@
     | Reconnect                                     -- Internal use
     deriving (Show, Eq)
 
+_Ready :: Traversal' VoiceWebsocketReceivable ReadyPayload
+_Ready f (Ready rp) = Ready <$> f rp
+_Ready f rp = pure rp
+
+_SessionDescription :: Traversal' VoiceWebsocketReceivable (T.Text, [Word8])
+_SessionDescription f (SessionDescription t bytes) = uncurry SessionDescription <$> f (t, bytes)
+_SessionDescription f sd = pure sd
+
+_Hello :: Traversal' VoiceWebsocketReceivable Int
+_Hello f (Hello a) = Hello <$> f a
+_Hello f a = pure a
+
+
 data VoiceWebsocketSendable
     = Identify IdentifyPayload                      -- Opcode 0
     | SelectProtocol SelectProtocolPayload          -- Opcode 1
@@ -196,4 +209,4 @@
             ]
         ]
 
-$(makePrisms ''VoiceWebsocketReceivable)
+-- $(makePrisms ''VoiceWebsocketReceivable)
diff --git a/src/Discord/Internal/Voice.hs b/src/Discord/Internal/Voice.hs
--- a/src/Discord/Internal/Voice.hs
+++ b/src/Discord/Internal/Voice.hs
@@ -50,7 +50,8 @@
     )
 import Control.Concurrent.BoundedChan qualified as Bounded
 import Control.Exception.Safe ( finally, bracket, throwTo, catch, throwIO )
-import Control.Lens
+import Lens.Micro
+import Lens.Micro.Extras (view)
 import Control.Monad.Reader ( ask, liftIO, runReaderT )
 import Control.Monad.Except ( runExceptT, throwError )
 import Control.Monad.Trans ( lift )
@@ -149,8 +150,8 @@
         -- There is no way to prevent it, so as a consequence, the bot may
         -- linger in the voice call for a few minutes after the bot program is
         -- killed.
-        mapMOf_ (traverse . guildId) (\x -> updateStatusVoice x Nothing False False) finalState
-        mapMOf_ (traverse . websocket . _1) (liftIO . killWkThread) finalState
+        traverseOf_ (traverse . guildId) (\x -> updateStatusVoice x Nothing False False) finalState
+        traverseOf_ (traverse . websocket . _1) (liftIO . killWkThread) finalState
 
     pure result
 
@@ -303,7 +304,7 @@
 updateSpeakingStatus micStatus = do
     h <- (^. voiceHandles) <$> ask
     handles <- liftIO $ readMVar h
-    flip (mapMOf_ traverse) handles $ \handle ->
+    flip (traverseOf_ traverse) handles $ \handle ->
         liftIO $ writeChan (handle ^. websocket . _2 . _2) $ Speaking $ SpeakingPayload
             { speakingPayloadMicrophone = micStatus
             , speakingPayloadSoundshare = False
@@ -383,9 +384,9 @@
     encoder <- liftIO $ opusEncoderCreate enCfg
     loop encoder
   where
-    enCfg = _EncoderConfig # (opusSR48k, True, app_audio)
+    enCfg = mkEncoderConfig opusSR48k True app_audio
     -- 1275 is the max bytes an opus 20ms frame can have
-    streamCfg = _StreamConfig # (enCfg, 48*20, 1276)
+    streamCfg = mkStreamConfig enCfg (48*20) 1276
     loop encoder = await >>= \case
         Nothing -> do
             -- Send at least 5 blank frames (20ms * 5 = 100 ms)
diff --git a/src/Discord/Internal/Voice/CommonUtils.hs b/src/Discord/Internal/Voice/CommonUtils.hs
--- a/src/Discord/Internal/Voice/CommonUtils.hs
+++ b/src/Discord/Internal/Voice/CommonUtils.hs
@@ -24,7 +24,7 @@
 
 import Control.Concurrent
 import Control.Concurrent.Async ( race )
-import Control.Lens
+import Lens.Micro
 import Data.Text qualified as T
 import Data.Time.Clock.POSIX
 import Data.Time
diff --git a/src/Discord/Internal/Voice/UDPLoop.hs b/src/Discord/Internal/Voice/UDPLoop.hs
--- a/src/Discord/Internal/Voice/UDPLoop.hs
+++ b/src/Discord/Internal/Voice/UDPLoop.hs
@@ -47,7 +47,7 @@
     )
 import Control.Concurrent.BoundedChan qualified as Bounded
 import Control.Exception.Safe ( handle, SomeException, finally, try, bracket )
-import Control.Lens
+import Lens.Micro
 import Control.Monad.IO.Class ( MonadIO )
 import Data.Binary ( encode, decode )
 import Data.ByteString.Lazy qualified as BL
@@ -288,8 +288,8 @@
 
 decodeOpusData :: B.ByteString -> IO B.ByteString
 decodeOpusData bytes = do
-    let deCfg = _DecoderConfig # (opusSR48k, True)
-    let deStreamCfg = _DecoderStreamConfig # (deCfg, 48*20, 0)
+    let deCfg = mkDecoderConfig opusSR48k True
+    let deStreamCfg = mkDecoderStreamConfig deCfg (48*20) 0
     decoder <- opusDecoderCreate deCfg
     decoded <- opusDecode decoder deStreamCfg bytes
     pure decoded
diff --git a/src/Discord/Internal/Voice/WebsocketLoop.hs b/src/Discord/Internal/Voice/WebsocketLoop.hs
--- a/src/Discord/Internal/Voice/WebsocketLoop.hs
+++ b/src/Discord/Internal/Voice/WebsocketLoop.hs
@@ -48,7 +48,7 @@
     , readMVar
     )
 import Control.Exception.Safe ( try, tryAsync, SomeException, finally, handle )
-import Control.Lens
+import Lens.Micro
 import Control.Monad ( forever, guard )
 import Control.Monad.Except ( runExceptT, ExceptT (ExceptT), lift )
 import Control.Monad.IO.Class ( liftIO )
@@ -125,9 +125,7 @@
     -- @opts@ to report back to runVoice, so it can be killed in the
     -- future.
     websocketFsm WSStart retries udpInfo = do
-        -- Use of tryAsync (unsafe, as it catches asynchronous exceptions) is
-        -- justified here, since it will only log, then go to WSClosed.
-        next <- tryAsync $ connect (opts ^. endpoint) $ \conn -> do
+        next <- try $ connect (opts ^. endpoint) $ \conn -> do
             (libSends, sendTid) <- flip (setupSendLoop conn) log $ opts ^. wsHandle . _2
 
             result <- flip finally (killThread sendTid) $ runExceptT $ do
@@ -216,18 +214,16 @@
         -- Connection is now closed.
         case next :: Either SomeException WSState of
             Left e -> do
-                (✍!) log $ "could not connect due to an exception: " <>
+                (✍!) log $ "connection terminated due to a synchronous exception: " <>
                     (tshow e)
                 writeChan (opts ^. wsHandle . _1) $ Left $
                     VoiceWebsocketCouldNotConnect
-                        "could not connect due to an exception"
+                        "connection terminated due to a synchronous exception"
                 websocketFsm WSClosed 0 udpInfo
             Right n -> websocketFsm n 0 udpInfo
 
     websocketFsm WSResume retries udpInfo = do
-        -- Use of tryAsync (unsafe, as it catches asynchronous exceptions) is
-        -- justified here, since it will only log, then go to WSClosed.
-        next <- tryAsync $ connect (opts ^. endpoint) $ \conn -> do
+        next <- try $ connect (opts ^. endpoint) $ \conn -> do
             (libSends, sendTid) <- flip (setupSendLoop conn) log $ opts ^. wsHandle . _2
             helloPacket <- getPayload conn
             case helloPacket of
@@ -247,26 +243,26 @@
                         Right (Discord.Internal.Types.VoiceWebsocket.Resumed) -> do
                             -- use the previous UDP launch options since it's not resent
                             udpLaunchOpts <- readMVar udpInfo
-                            
+
                             -- Pass not the MVar but the raw options, since
                             -- there's no writing to be done.
                             finally (eventStream conn opts interval udpLaunchOpts libSends log) $
                                 (killThread heartGenTid >> killThread sendTid)
                         Right p -> do
-                            (✍!) log $ "First packet after Resume not " <> 
+                            (✍!) log $ "First packet after Resume not " <>
                                 "Opcode 9 Resumed: " <> (tshow p)
                             pure WSClosed
                 Right p -> do
                     (✍!) log $ "First packet not Opcode 8 Hello: " <> (tshow p)
                     pure WSClosed
 
-        -- Connection is now closed.
         case next :: Either SomeException WSState of
-            Left _ -> do
-                (✍!) log $ "could not resume, retrying after 5 seconds"
+            Left e -> do
+                (✍!) log $ "could not resume due to a synchronous exception: " <>
+                    (tshow e) <> ", retrying after 5 seconds"
                 threadDelay $ 5 * (10^(6 :: Int))
                 websocketFsm WSResume (retries + 1) udpInfo
-            Right n -> websocketFsm n 1 udpInfo
+            Right n -> websocketFsm n retries udpInfo
 
 -- | Create the library-specific sending packets Chan, and then create the
 -- thread for eternally sending contents in the said Chan, as well as the
diff --git a/src/Discord/Voice.hs b/src/Discord/Voice.hs
--- a/src/Discord/Voice.hs
+++ b/src/Discord/Voice.hs
@@ -56,6 +56,8 @@
     , playFileWith'
     , playYouTube
     , playYouTube'
+    , playYouTubeWith
+    , playYouTubeWith'
     , defaultFFmpegArgs
     ) where
 
