Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explicit API for kord-voice #425

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions voice/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ dependencies {
api(libs.ktor.network)
}

// Move this into the build script plugins when implemented in all modules
kotlin {
explicitApi()
}

// by convention, java classes (TweetNaclFast) should be in their own java source.
// however, this breaks atomicfu.
// to work around it we just make the kotlin src directory also a java src directory.
Expand Down
8 changes: 4 additions & 4 deletions voice/src/main/kotlin/AudioFrame.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import dev.kord.common.annotation.KordVoice
*/
@KordVoice
@JvmInline
value class AudioFrame(val data: ByteArray) {
companion object {
val SILENCE = AudioFrame(byteArrayOf(0xFC.toByte(), 0xFF.toByte(), 0xFE.toByte()))
public value class AudioFrame(public val data: ByteArray) {
public companion object {
public val SILENCE: AudioFrame = AudioFrame(byteArrayOf(0xFC.toByte(), 0xFF.toByte(), 0xFE.toByte()))

fun fromData(data: ByteArray?) = data?.let(::AudioFrame)
public fun fromData(data: ByteArray?): AudioFrame? = data?.let(::AudioFrame)
}
}
6 changes: 3 additions & 3 deletions voice/src/main/kotlin/AudioProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,20 @@ import kotlin.time.TimeSource
* which should be transmitted to Discord.
*/
@KordVoice
fun interface AudioProvider {
public fun interface AudioProvider {
/**
* Provides a single frame of audio, [AudioFrame].
*
* @return the frame of audio.
*/
suspend fun provide(): AudioFrame?
public suspend fun provide(): AudioFrame?

/**
* Polls [AudioFrame]s into the [frames] channel at an appropriate interval. Suspends until the coroutine scope is cancelled.
*
* @param frames the channel where [AudioFrame]s will be sent to.
*/
suspend fun CoroutineScope.provideFrames(frames: SendChannel<AudioFrame?>) {
public suspend fun CoroutineScope.provideFrames(frames: SendChannel<AudioFrame?>) {
val mark = TimeSource.Monotonic.markNow()
var nextFrameTimestamp = mark.elapsedNow().inWholeNanoseconds

Expand Down
4 changes: 2 additions & 2 deletions voice/src/main/kotlin/EncryptionMode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable

@KordVoice
@Serializable
enum class EncryptionMode {
public enum class EncryptionMode {
@SerialName("xsalsa20_poly1305")
XSalsa20Poly1305,

Expand All @@ -16,10 +16,10 @@ enum class EncryptionMode {
@SerialName("xsalsa20_poly1305_lite")
XSalsa20Poly1305Lite,

// video/unreleased-audio stuff... unused. though required to allow for serialization of ready
@SerialName("xsalsa20_poly1305_lite_rtpsize")
XSalsa20Poly1305LiteRtpsize,

// video/unreleased-audio stuff... unused. though required to allow for serialization of ready
@SerialName("aead_aes256_gcm_rtpsize")
AeadAes256GcmRtpsize,

Expand Down
14 changes: 7 additions & 7 deletions voice/src/main/kotlin/FrameInterceptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ import kotlin.properties.Delegates
* @param ssrc the current SSRC retrieved from Discord.
*/
@KordVoice
data class FrameInterceptorContext(
public data class FrameInterceptorContext(
val gateway: Gateway,
val voiceGateway: VoiceGateway,
val ssrc: UInt,
)

@KordVoice
class FrameInterceptorContextBuilder(var gateway: Gateway, var voiceGateway: VoiceGateway) {
var ssrc: UInt by Delegates.notNull()
public class FrameInterceptorContextBuilder(public var gateway: Gateway, public var voiceGateway: VoiceGateway) {
public var ssrc: UInt by Delegates.notNull()

fun build() = FrameInterceptorContext(gateway, voiceGateway, ssrc)
public fun build(): FrameInterceptorContext = FrameInterceptorContext(gateway, voiceGateway, ssrc)
}

@KordVoice
Expand All @@ -37,8 +37,8 @@ internal inline fun FrameInterceptorContext(gateway: Gateway, voiceGateway: Voic
* @see DefaultFrameInterceptor
*/
@KordVoice
fun interface FrameInterceptor {
suspend fun intercept(frame: AudioFrame?): AudioFrame?
public fun interface FrameInterceptor {
public suspend fun intercept(frame: AudioFrame?): AudioFrame?
}

private const val FRAMES_OF_SILENCE_TO_PLAY = 5
Expand All @@ -52,7 +52,7 @@ private const val FRAMES_OF_SILENCE_TO_PLAY = 5
* @param speakingState the speaking state that will be used when there is audio data to be sent. By default, it is microphone-only.
*/
@KordVoice
open class DefaultFrameInterceptor(
public open class DefaultFrameInterceptor(
protected val context: FrameInterceptorContext,
private val speakingState: SpeakingFlags = SpeakingFlags { +SpeakingFlag.Microphone }
) : FrameInterceptor {
Expand Down
43 changes: 21 additions & 22 deletions voice/src/main/kotlin/SpeakingFlag.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,30 @@ import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

@KordVoice
enum class SpeakingFlag(val code: Int) {
public enum class SpeakingFlag(public val code: Int) {
Microphone(1 shl 0),
Soundshare(1 shl 1),
Priority(1 shl 2)
}

@KordVoice
@Serializable(with = SpeakingFlags.Serializer::class)
class SpeakingFlags internal constructor(val code: Int) {
val flags = SpeakingFlag.values().filter { code and it.code != 0 }
public class SpeakingFlags internal constructor(public val code: Int) {
public val flags: List<SpeakingFlag> = SpeakingFlag.values().filter { code and it.code != 0 }

operator fun contains(flag: SpeakingFlags) = flag.code and this.code == flag.code
public operator fun contains(flag: SpeakingFlags): Boolean = flag.code and this.code == flag.code

operator fun contains(flags: SpeakingFlag) = flags.code and this.code == flags.code
public operator fun contains(flags: SpeakingFlag): Boolean = flags.code and this.code == flags.code

operator fun plus(flags: SpeakingFlags): SpeakingFlags = SpeakingFlags(this.code or flags.code)
public operator fun plus(flags: SpeakingFlags): SpeakingFlags = SpeakingFlags(this.code or flags.code)

operator fun plus(flags: SpeakingFlag): SpeakingFlags = SpeakingFlags(this.code or flags.code)
public operator fun plus(flags: SpeakingFlag): SpeakingFlags = SpeakingFlags(this.code or flags.code)

operator fun minus(flags: SpeakingFlags): SpeakingFlags = SpeakingFlags(this.code xor flags.code)
public operator fun minus(flags: SpeakingFlags): SpeakingFlags = SpeakingFlags(this.code xor flags.code)

operator fun minus(flags: SpeakingFlag): SpeakingFlags = SpeakingFlags(this.code xor flags.code)
public operator fun minus(flags: SpeakingFlag): SpeakingFlags = SpeakingFlags(this.code xor flags.code)


inline fun copy(block: Builder.() -> Unit): SpeakingFlags {
public inline fun copy(block: Builder.() -> Unit): SpeakingFlags {
val builder = Builder(code)
builder.apply(block)
return builder.flags()
Expand All @@ -58,56 +57,56 @@ class SpeakingFlags internal constructor(val code: Int) {
}
}

class Builder(internal var code: Int = 0) {
operator fun SpeakingFlag.unaryPlus() {
public class Builder(internal var code: Int = 0) {
public operator fun SpeakingFlag.unaryPlus() {
[email protected] = [email protected] or code
}

operator fun SpeakingFlag.unaryMinus() {
public operator fun SpeakingFlag.unaryMinus() {
if ([email protected] and code == code) {
[email protected] = [email protected] xor code
}
}

operator fun SpeakingFlags.unaryPlus() {
public operator fun SpeakingFlags.unaryPlus() {
[email protected] = [email protected] or code
}

operator fun SpeakingFlags.unaryMinus() {
public operator fun SpeakingFlags.unaryMinus() {
if ([email protected] and code == code) {
[email protected] = [email protected] xor code
}
}

fun flags() = SpeakingFlags(code)
public fun flags(): SpeakingFlags = SpeakingFlags(code)
}

}

@KordVoice
@OptIn(ExperimentalContracts::class)
inline fun SpeakingFlags(builder: SpeakingFlags.Builder.() -> Unit): SpeakingFlags {
public inline fun SpeakingFlags(builder: SpeakingFlags.Builder.() -> Unit): SpeakingFlags {
contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) }
return SpeakingFlags.Builder().apply(builder).flags()
}

@KordVoice
fun SpeakingFlags(vararg flags: SpeakingFlag) = SpeakingFlags {
public fun SpeakingFlags(vararg flags: SpeakingFlag): SpeakingFlags = SpeakingFlags {
flags.forEach { +it }
}

@KordVoice
fun SpeakingFlags(vararg flags: SpeakingFlags) = SpeakingFlags {
public fun SpeakingFlags(vararg flags: SpeakingFlags): SpeakingFlags = SpeakingFlags {
flags.forEach { +it }
}

@KordVoice
fun SpeakingFlags(flags: Iterable<SpeakingFlag>) = SpeakingFlags {
public fun SpeakingFlags(flags: Iterable<SpeakingFlag>): SpeakingFlags = SpeakingFlags {
flags.forEach { +it }
}

@KordVoice
@JvmName("SpeakingFlagsWithIterable")
fun SpeakingFlags(flags: Iterable<SpeakingFlags>) = SpeakingFlags {
public fun SpeakingFlags(flags: Iterable<SpeakingFlags>): SpeakingFlags = SpeakingFlags {
flags.forEach { +it }
}
38 changes: 18 additions & 20 deletions voice/src/main/kotlin/VoiceConnection.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ import kotlinx.coroutines.*
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext

/**
* Data that represents a [VoiceConnection], these will never change during the lifetime of a [VoiceConnection].
Expand All @@ -27,7 +25,7 @@ import kotlin.coroutines.EmptyCoroutineContext
* @param guildId the id of the guild that the bot is connecting to.
* @param sessionId the id of the current voice session, given by Discord.
*/
data class VoiceConnectionData(
public data class VoiceConnectionData(
val selfId: Snowflake,
val guildId: Snowflake,
val sessionId: String,
Expand All @@ -46,19 +44,19 @@ data class VoiceConnectionData(
* @param frameInterceptorFactory a factory for [FrameInterceptor]s that is used whenever audio is ready to be sent. See [FrameInterceptor] and [DefaultFrameInterceptor].
*/
@KordVoice
class VoiceConnection(
val data: VoiceConnectionData,
val gateway: Gateway,
val voiceGateway: VoiceGateway,
val socket: VoiceUdpSocket,
var voiceGatewayConfiguration: VoiceGatewayConfiguration,
val streams: Streams,
val audioProvider: AudioProvider,
val frameSender: AudioFrameSender,
val nonceStrategy: NonceStrategy,
val frameInterceptorFactory: (FrameInterceptorContext) -> FrameInterceptor,
public class VoiceConnection(
public val data: VoiceConnectionData,
public val gateway: Gateway,
public val voiceGateway: VoiceGateway,
public val socket: VoiceUdpSocket,
public var voiceGatewayConfiguration: VoiceGatewayConfiguration,
public val streams: Streams,
public val audioProvider: AudioProvider,
public val frameSender: AudioFrameSender,
public val nonceStrategy: NonceStrategy,
public val frameInterceptorFactory: (FrameInterceptorContext) -> FrameInterceptor,
) {
val scope: CoroutineScope =
public val scope: CoroutineScope =
CoroutineScope(SupervisorJob() + CoroutineName("kord-voice-connection[${data.guildId.value}]"))

init {
Expand All @@ -73,7 +71,7 @@ class VoiceConnection(
* Starts the [VoiceGateway] for this [VoiceConnection].
* This will begin the process for audio transmission.
*/
suspend fun connect(scope: CoroutineScope = this.scope) {
public suspend fun connect(scope: CoroutineScope = this.scope) {
scope.launch {
voiceGateway.start(voiceGatewayConfiguration)
}
Expand All @@ -82,15 +80,15 @@ class VoiceConnection(
/**
* Disconnects from the voice servers, does not change the voice state.
*/
suspend fun disconnect() {
public suspend fun disconnect() {
voiceGateway.stop()
socket.stop()
}

/**
* Disconnects from Discord voice servers, and leaves the voice channel.
*/
suspend fun leave() {
public suspend fun leave() {
gateway.send(
UpdateVoiceStatus(
guildId = data.guildId,
Expand All @@ -106,7 +104,7 @@ class VoiceConnection(
/**
* Releases all resources related to this VoiceConnection (except [gateway]) and then stops its CoroutineScope.
*/
suspend fun shutdown() {
public suspend fun shutdown() {
leave()
voiceGateway.detach()

Expand All @@ -129,7 +127,7 @@ class VoiceConnection(
*/
@KordVoice
@OptIn(ExperimentalContracts::class)
suspend inline fun VoiceConnection(
public suspend inline fun VoiceConnection(
gateway: Gateway,
selfId: Snowflake,
channelId: Snowflake,
Expand Down
Loading