Skip to content

Active voice state

Example Usage:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from interactions import slash_command, slash_option, OptionType, SlashContext
from interactions.api.voice.audio import AudioVolume


@slash_command("play")
@slash_option("song", "The song to play", OptionType.STRING, required=True)
async def test_cmd(ctx: SlashContext, song: str):
    await ctx.defer()

    if not ctx.voice_state:
        await ctx.author.voice.channel.connect() # (1)!

    await ctx.send(f"Playing {song}")
    await ctx.voice_state.play(AudioVolume(song)) # (2)!

  1. This connects the bot to the author's voice channel if we are not already connected
  2. Check out the Voice Support Guide for more info on audio playback

ActiveVoiceState

Bases: VoiceState

Source code in interactions/models/internal/active_voice_state.py
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
@attrs.define(eq=False, order=False, hash=False, kw_only=True)
class ActiveVoiceState(VoiceState):
    ws: Optional[VoiceGateway] = attrs.field(repr=False, default=None)
    """The websocket for this voice state"""
    player: Optional[Player] = attrs.field(repr=False, default=None)
    """The playback task that broadcasts audio data to discord"""
    recorder: Optional[Recorder] = attrs.field(default=None)
    """A recorder task to capture audio from discord"""
    _volume: float = attrs.field(repr=False, default=0.5)

    # standard voice states expect this data, this voice state lacks it initially; so we make them optional
    user_id: "Snowflake_Type" = attrs.field(repr=False, default=MISSING, converter=optional(to_snowflake))
    _guild_id: Optional["Snowflake_Type"] = attrs.field(repr=False, default=None, converter=optional(to_snowflake))
    _member_id: Optional["Snowflake_Type"] = attrs.field(repr=False, default=None, converter=optional(to_snowflake))

    def __attrs_post_init__(self) -> None:
        # jank line to handle the two inherently incompatible data structures
        self._member_id = self.user_id = self._client.user.id

    def __del__(self) -> None:
        if self.connected:
            self.ws.close()
        if self.player:
            self.player.stop()

    def __repr__(self) -> str:
        return f"<ActiveVoiceState: channel={self.channel} guild={self.guild} volume={self.volume} playing={self.playing} audio={self.current_audio}>"

    @property
    def current_audio(self) -> Optional["BaseAudio"]:
        """The current audio being played"""
        if self.player:
            return self.player.current_audio

    @property
    def volume(self) -> float:
        """Get the volume of the player"""
        return self._volume

    @volume.setter
    def volume(self, value) -> None:
        """Set the volume of the player"""
        if value < 0.0:
            raise ValueError("Volume may not be negative.")
        self._volume = value
        if self.player and hasattr(self.player.current_audio, "volume"):
            self.player.current_audio.volume = value

    @property
    def paused(self) -> bool:
        """Is the player currently paused"""
        return self.player.paused if self.player else False

    @property
    def playing(self) -> bool:
        """Are we currently playing something?"""
        # noinspection PyProtectedMember
        return bool(self.player and self.current_audio and not self.player.stopped and self.player._resume.is_set())

    @property
    def stopped(self) -> bool:
        """Is the player stopped?"""
        return self.player.stopped if self.player else True

    @property
    def connected(self) -> bool:
        """Is this voice state currently connected?"""
        # noinspection PyProtectedMember
        return False if self.ws is None else self.ws._closed.is_set()

    @property
    def gateway(self) -> "GatewayClient":
        return self._client.get_guild_websocket(self._guild_id)

    async def wait_for_stopped(self) -> None:
        """Wait for the player to stop playing."""
        if self.player:
            # noinspection PyProtectedMember
            await self.player._stopped.wait()

    async def _ws_connect(self) -> None:
        """Runs the voice gateway connection"""
        async with self.ws:
            try:
                await self.ws.run()
            finally:
                if self.playing:
                    await self.stop()

    async def ws_connect(self) -> None:
        """Connect to the voice gateway for this voice state"""
        self.ws = VoiceGateway(self._client._connection_state, self._voice_state.data, self._voice_server.data)

        _ = asyncio.create_task(self._ws_connect())  # noqa: RUF006
        await self.ws.wait_until_ready()

    def _guild_predicate(self, event) -> bool:
        return int(event.data["guild_id"]) == self._guild_id

    async def connect(self, timeout: int = 5) -> None:
        """
        Establish the voice connection.

        Args:
            timeout: How long to wait for state and server information from discord

        Raises:
            VoiceAlreadyConnected: if the voice state is already connected to the voice channel
            VoiceConnectionTimeout: if the voice state fails to connect

        """
        if self.connected:
            raise VoiceAlreadyConnected

        if Intents.GUILD_VOICE_STATES not in self._client.intents:
            raise RuntimeError("Cannot connect to voice without the GUILD_VOICE_STATES intent.")

        tasks = [
            asyncio.create_task(
                self._client.wait_for("raw_voice_state_update", self._guild_predicate, timeout=timeout)
            ),
            asyncio.create_task(
                self._client.wait_for("raw_voice_server_update", self._guild_predicate, timeout=timeout)
            ),
        ]

        await self.gateway.voice_state_update(self._guild_id, self._channel_id, self.self_mute, self.self_deaf)

        self.logger.debug("Waiting for voice connection data...")

        try:
            self._voice_state, self._voice_server = await asyncio.gather(*tasks)
        except asyncio.TimeoutError:
            raise VoiceConnectionTimeout from None

        self.logger.debug("Attempting to initialise voice gateway...")
        await self.ws_connect()

    async def disconnect(self) -> None:
        """Disconnect from the voice channel."""
        await self.gateway.voice_state_update(self._guild_id, None)

    async def move(self, channel: "Snowflake_Type", timeout: int = 5) -> None:
        """
        Move to another voice channel.

        Args:
            channel: The channel to move to
            timeout: How long to wait for state and server information from discord

        """
        target_channel = to_snowflake(channel)
        if target_channel != self._channel_id:
            already_paused = self.paused
            if self.player:
                self.player.pause()

            self._channel_id = target_channel
            await self.gateway.voice_state_update(self._guild_id, self._channel_id, self.self_mute, self.self_deaf)

            self.logger.debug("Waiting for voice connection data...")
            try:
                await self._client.wait_for("raw_voice_state_update", self._guild_predicate, timeout=timeout)
            except asyncio.TimeoutError:
                await self._close_connection()
                raise VoiceConnectionTimeout from None

            if self.player and not already_paused:
                self.player.resume()

    async def stop(self) -> None:
        """Stop playback."""
        self.player.stop()
        await self.player._stopped.wait()

    def pause(self) -> None:
        """Pause playback"""
        self.player.pause()

    def resume(self) -> None:
        """Resume playback."""
        self.player.resume()

    async def play(self, audio: "BaseAudio") -> None:
        """
        Start playing an audio object.

        Waits for the player to stop before returning.

        Args:
            audio: The audio object to play

        """
        if self.player:
            await self.stop()

        with Player(audio, self, asyncio.get_running_loop()) as self.player:
            self.player.play()
            await self.wait_for_stopped()

    def play_no_wait(self, audio: "BaseAudio") -> asyncio.Task:
        """
        Start playing an audio object, but don't wait for playback to finish.

        Args:
            audio: The audio object to play

        """
        return asyncio.create_task(self.play(audio))

    def create_recorder(self) -> Recorder:
        """Create a recorder instance."""
        if not self.recorder:
            self.recorder = Recorder(self, asyncio.get_running_loop())
        return self.recorder

    async def start_recording(self, encoding: Optional[str] = None, *, output_dir: str | Missing = Missing) -> Recorder:
        """
        Start recording the voice channel.

        If no recorder exists, one will be created.

        Args:
            encoding: What format the audio should be encoded to.
            output_dir: The directory to save the audio to

        """
        if not self.recorder:
            self.recorder = Recorder(self, asyncio.get_running_loop())

        if self.recorder.used:
            if self.recorder.recording:
                raise RuntimeError("Another recording is still in progress, please stop it first.")
            self.recorder = Recorder(self, asyncio.get_running_loop())

        if encoding is not None:
            self.recorder.encoding = encoding

        await self.recorder.start_recording(output_dir=output_dir)
        return self.recorder

    async def stop_recording(self) -> dict[int, BytesIO]:
        """
        Stop the recording.

        Returns:
            dict[snowflake, BytesIO]: The recorded audio

        """
        if not self.recorder or not self.recorder.recording or not self.recorder.audio:
            raise RuntimeError("No recorder is running!")
        await self.recorder.stop_recording()

        self.recorder.audio.finished.wait()
        return self.recordings

    @property
    def recordings(self) -> dict[int, BytesIO]:
        return self.recorder.output if self.recorder else {}

    async def _voice_server_update(self, data) -> None:
        """
        An internal receiver for voice server events.

        Args:
            data: voice server data

        """
        self.ws.set_new_voice_server(data)

    async def _voice_state_update(
        self,
        before: Optional[VoiceState],
        after: Optional[VoiceState],
        data: Optional[VoiceStateData],
    ) -> None:
        """
        An internal receiver for voice server state events.

        Args:
            before: The previous voice state
            after: The current voice state
            data: Raw data from gateway

        """
        if after is None:
            # bot disconnected
            self.logger.info(f"Disconnecting from voice channel {self._channel_id}")
            await self._close_connection()
            self._client.cache.delete_bot_voice_state(self._guild_id)
            return

        self.update_from_dict(data)

    async def _close_connection(self) -> None:
        """Close the voice connection."""
        if self.playing:
            await self.stop()
        if self.connected:
            self.ws.close()

connected: bool property

Is this voice state currently connected?

current_audio: Optional[BaseAudio] property

The current audio being played

paused: bool property

Is the player currently paused

player: Optional[Player] = attrs.field(repr=False, default=None) class-attribute

The playback task that broadcasts audio data to discord

playing: bool property

Are we currently playing something?

recorder: Optional[Recorder] = attrs.field(default=None) class-attribute

A recorder task to capture audio from discord

stopped: bool property

Is the player stopped?

volume: float writable property

Get the volume of the player

ws: Optional[VoiceGateway] = attrs.field(repr=False, default=None) class-attribute

The websocket for this voice state

connect(timeout=5) async

Establish the voice connection.

Parameters:

Name Type Description Default
timeout int

How long to wait for state and server information from discord

5

Raises:

Type Description
VoiceAlreadyConnected

if the voice state is already connected to the voice channel

VoiceConnectionTimeout

if the voice state fails to connect

Source code in interactions/models/internal/active_voice_state.py
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
async def connect(self, timeout: int = 5) -> None:
    """
    Establish the voice connection.

    Args:
        timeout: How long to wait for state and server information from discord

    Raises:
        VoiceAlreadyConnected: if the voice state is already connected to the voice channel
        VoiceConnectionTimeout: if the voice state fails to connect

    """
    if self.connected:
        raise VoiceAlreadyConnected

    if Intents.GUILD_VOICE_STATES not in self._client.intents:
        raise RuntimeError("Cannot connect to voice without the GUILD_VOICE_STATES intent.")

    tasks = [
        asyncio.create_task(
            self._client.wait_for("raw_voice_state_update", self._guild_predicate, timeout=timeout)
        ),
        asyncio.create_task(
            self._client.wait_for("raw_voice_server_update", self._guild_predicate, timeout=timeout)
        ),
    ]

    await self.gateway.voice_state_update(self._guild_id, self._channel_id, self.self_mute, self.self_deaf)

    self.logger.debug("Waiting for voice connection data...")

    try:
        self._voice_state, self._voice_server = await asyncio.gather(*tasks)
    except asyncio.TimeoutError:
        raise VoiceConnectionTimeout from None

    self.logger.debug("Attempting to initialise voice gateway...")
    await self.ws_connect()

create_recorder()

Create a recorder instance.

Source code in interactions/models/internal/active_voice_state.py
235
236
237
238
239
def create_recorder(self) -> Recorder:
    """Create a recorder instance."""
    if not self.recorder:
        self.recorder = Recorder(self, asyncio.get_running_loop())
    return self.recorder

disconnect() async

Disconnect from the voice channel.

Source code in interactions/models/internal/active_voice_state.py
163
164
165
async def disconnect(self) -> None:
    """Disconnect from the voice channel."""
    await self.gateway.voice_state_update(self._guild_id, None)

move(channel, timeout=5) async

Move to another voice channel.

Parameters:

Name Type Description Default
channel Snowflake_Type

The channel to move to

required
timeout int

How long to wait for state and server information from discord

5
Source code in interactions/models/internal/active_voice_state.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
async def move(self, channel: "Snowflake_Type", timeout: int = 5) -> None:
    """
    Move to another voice channel.

    Args:
        channel: The channel to move to
        timeout: How long to wait for state and server information from discord

    """
    target_channel = to_snowflake(channel)
    if target_channel != self._channel_id:
        already_paused = self.paused
        if self.player:
            self.player.pause()

        self._channel_id = target_channel
        await self.gateway.voice_state_update(self._guild_id, self._channel_id, self.self_mute, self.self_deaf)

        self.logger.debug("Waiting for voice connection data...")
        try:
            await self._client.wait_for("raw_voice_state_update", self._guild_predicate, timeout=timeout)
        except asyncio.TimeoutError:
            await self._close_connection()
            raise VoiceConnectionTimeout from None

        if self.player and not already_paused:
            self.player.resume()

pause()

Pause playback

Source code in interactions/models/internal/active_voice_state.py
200
201
202
def pause(self) -> None:
    """Pause playback"""
    self.player.pause()

play(audio) async

Start playing an audio object.

Waits for the player to stop before returning.

Parameters:

Name Type Description Default
audio BaseAudio

The audio object to play

required
Source code in interactions/models/internal/active_voice_state.py
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
async def play(self, audio: "BaseAudio") -> None:
    """
    Start playing an audio object.

    Waits for the player to stop before returning.

    Args:
        audio: The audio object to play

    """
    if self.player:
        await self.stop()

    with Player(audio, self, asyncio.get_running_loop()) as self.player:
        self.player.play()
        await self.wait_for_stopped()

play_no_wait(audio)

Start playing an audio object, but don't wait for playback to finish.

Parameters:

Name Type Description Default
audio BaseAudio

The audio object to play

required
Source code in interactions/models/internal/active_voice_state.py
225
226
227
228
229
230
231
232
233
def play_no_wait(self, audio: "BaseAudio") -> asyncio.Task:
    """
    Start playing an audio object, but don't wait for playback to finish.

    Args:
        audio: The audio object to play

    """
    return asyncio.create_task(self.play(audio))

resume()

Resume playback.

Source code in interactions/models/internal/active_voice_state.py
204
205
206
def resume(self) -> None:
    """Resume playback."""
    self.player.resume()

start_recording(encoding=None, *, output_dir=Missing) async

Start recording the voice channel.

If no recorder exists, one will be created.

Parameters:

Name Type Description Default
encoding Optional[str]

What format the audio should be encoded to.

None
output_dir str | Missing

The directory to save the audio to

Missing
Source code in interactions/models/internal/active_voice_state.py
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
async def start_recording(self, encoding: Optional[str] = None, *, output_dir: str | Missing = Missing) -> Recorder:
    """
    Start recording the voice channel.

    If no recorder exists, one will be created.

    Args:
        encoding: What format the audio should be encoded to.
        output_dir: The directory to save the audio to

    """
    if not self.recorder:
        self.recorder = Recorder(self, asyncio.get_running_loop())

    if self.recorder.used:
        if self.recorder.recording:
            raise RuntimeError("Another recording is still in progress, please stop it first.")
        self.recorder = Recorder(self, asyncio.get_running_loop())

    if encoding is not None:
        self.recorder.encoding = encoding

    await self.recorder.start_recording(output_dir=output_dir)
    return self.recorder

stop() async

Stop playback.

Source code in interactions/models/internal/active_voice_state.py
195
196
197
198
async def stop(self) -> None:
    """Stop playback."""
    self.player.stop()
    await self.player._stopped.wait()

stop_recording() async

Stop the recording.

Returns:

Type Description
dict[int, BytesIO]

dict[snowflake, BytesIO]: The recorded audio

Source code in interactions/models/internal/active_voice_state.py
266
267
268
269
270
271
272
273
274
275
276
277
278
279
async def stop_recording(self) -> dict[int, BytesIO]:
    """
    Stop the recording.

    Returns:
        dict[snowflake, BytesIO]: The recorded audio

    """
    if not self.recorder or not self.recorder.recording or not self.recorder.audio:
        raise RuntimeError("No recorder is running!")
    await self.recorder.stop_recording()

    self.recorder.audio.finished.wait()
    return self.recordings

wait_for_stopped() async

Wait for the player to stop playing.

Source code in interactions/models/internal/active_voice_state.py
 99
100
101
102
103
async def wait_for_stopped(self) -> None:
    """Wait for the player to stop playing."""
    if self.player:
        # noinspection PyProtectedMember
        await self.player._stopped.wait()

ws_connect() async

Connect to the voice gateway for this voice state

Source code in interactions/models/internal/active_voice_state.py
114
115
116
117
118
119
async def ws_connect(self) -> None:
    """Connect to the voice gateway for this voice state"""
    self.ws = VoiceGateway(self._client._connection_state, self._voice_state.data, self._voice_server.data)

    _ = asyncio.create_task(self._ws_connect())  # noqa: RUF006
    await self.ws.wait_until_ready()