Skip to content

Commit

Permalink
feat(client): mute player source if a direct source from the same pla…
Browse files Browse the repository at this point in the history
…yer is active
  • Loading branch information
Apehum committed Nov 16, 2024
1 parent 89df241 commit 9359a30
Show file tree
Hide file tree
Showing 7 changed files with 61 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ interface ClientConfig {

val panning: BooleanConfigEntry

val mutePlayerOnDirect: BooleanConfigEntry

val cameraSoundListener: BooleanConfigEntry

val exponentialVolumeSlider: BooleanConfigEntry
Expand Down
3 changes: 2 additions & 1 deletion client/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ so there’s no need to worry if the server hasn't been updated to 2.1.x.
- Fixed buffer overflow when using AudioSender with delayed first frame.
- Fixed EncoderException on server switch on servers with proxy and proxy-side addons (e.g, groups addon).
- Fixed `pv.activation.*` permission is not being updated on the client without reconnect.
- Fixed textfield input not being handled.
- Fixed textfield input not being handled.
- Player sources are now automatically muted if the direct source from the same player becomes active. Can be disabled in menu `Advanced`/`Mute Player On Direct`.
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,9 @@ public static class Advanced implements ClientConfig.Advanced {
@ConfigField
private BooleanConfigEntry panning = new BooleanConfigEntry(true);

@ConfigField
private BooleanConfigEntry mutePlayerOnDirect = new BooleanConfigEntry(true);

@ConfigField
private BooleanConfigEntry cameraSoundListener = new BooleanConfigEntry(true);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ public void init() {
));
addEntry(createStereoToMonoSources());
addEntry(createPanning());
addEntry(createToggleEntry(
McTextComponent.translatable("gui.plasmovoice.advanced.mute_player_on_direct"),
McTextComponent.translatable("gui.plasmovoice.advanced.mute_player_on_direct.tooltip"),
config.getAdvanced().getMutePlayerOnDirect()
));

addEntry(new CategoryEntry(McTextComponent.translatable("gui.plasmovoice.advanced.exponential_volume")));
addEntry(createToggleEntry(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,42 +344,44 @@ abstract class BaseClientAudioSource<T>(
// so we need to make sure that source is not closed rn
if (closed.get()) return

// packet compensation
if (lastSequenceNumber >= 0) {
val packetsToCompensate = (packet.sequenceNumber - (lastSequenceNumber + 1)).toInt()
if (packetsToCompensate in 1..4) {
BaseVoice.DEBUG_LOGGER.warn("Compensate {} lost packets", packetsToCompensate)
for (i in 0 until packetsToCompensate) {
val compensatedSequenceNumber = lastSequenceNumber + i + 1

if (decoder != null && decoder is AudioDecoderPlc && !sourceInfo.isStereo) {
try {
write((decoder as AudioDecoderPlc).decodePLC(), compensatedSequenceNumber)
} catch (e: CodecException) {
LOGGER.warn("Failed to decode source audio", e)
return
if (shouldWrite()) {
// packet compensation
if (lastSequenceNumber >= 0) {
val packetsToCompensate = (packet.sequenceNumber - (lastSequenceNumber + 1)).toInt()
if (packetsToCompensate in 1..4) {
BaseVoice.DEBUG_LOGGER.warn("Compensate {} lost packets", packetsToCompensate)
for (i in 0 until packetsToCompensate) {
val compensatedSequenceNumber = lastSequenceNumber + i + 1

if (decoder != null && decoder is AudioDecoderPlc && !sourceInfo.isStereo) {
try {
write((decoder as AudioDecoderPlc).decodePLC(), compensatedSequenceNumber)
} catch (e: CodecException) {
LOGGER.warn("Failed to decode source audio", e)
return
}
} else {
write(ShortArray(0), compensatedSequenceNumber)
}
} else {
write(ShortArray(0), compensatedSequenceNumber)
}
}
}
}

// decrypt & decode samples
try {
val decrypted = encryption?.decrypt(packet.data) ?: packet.data
val decoded = decoder?.decode(decrypted) ?: AudioUtil.bytesToShorts(decrypted)
// decrypt & decode samples
try {
val decrypted = encryption?.decrypt(packet.data) ?: packet.data
val decoded = decoder?.decode(decrypted) ?: AudioUtil.bytesToShorts(decrypted)

if (sourceInfo.isStereo && config.advanced.stereoSourcesToMono.value()) {
write(AudioUtil.convertToMonoShorts(decoded), packet.sequenceNumber)
} else {
write(decoded, packet.sequenceNumber)
if (sourceInfo.isStereo && config.advanced.stereoSourcesToMono.value()) {
write(AudioUtil.convertToMonoShorts(decoded), packet.sequenceNumber)
} else {
write(decoded, packet.sequenceNumber)
}
} catch (e: EncryptionException) {
BaseVoice.DEBUG_LOGGER.warn("Failed to decrypt source audio", e)
} catch (e: CodecException) {
BaseVoice.DEBUG_LOGGER.warn("Failed to decode source audio", e)
}
} catch (e: EncryptionException) {
BaseVoice.DEBUG_LOGGER.warn("Failed to decrypt source audio", e)
} catch (e: CodecException) {
BaseVoice.DEBUG_LOGGER.warn("Failed to decode source audio", e)
}

lastSequenceNumber = packet.sequenceNumber
Expand Down Expand Up @@ -548,6 +550,14 @@ abstract class BaseClientAudioSource<T>(
return sourceInfo.isStereo && !config.advanced.stereoSourcesToMono.value()
}

/**
* Determines if audio should be written to the underlying AL source.
*
* Because we don't want to disable source icons in some cases,
* we should just disable writing samples to the actual source.
*/
protected open fun shouldWrite(): Boolean = true

companion object {
private val OUTER_ANGLE: Double = 180.0
private val LOGGER: Logger = LogManager.getLogger(BaseClientAudioSource::class.java)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ class ClientPlayerSource(
override fun isPanningDisabled(): Boolean =
sourcePlayer == getListener() || super.isPanningDisabled()

override fun shouldWrite(): Boolean =
!voiceClient.config.advanced.mutePlayerOnDirect.value() ||
voiceClient.sourceManager.sources
.filterIsInstance<ClientDirectSource>()
.filter { it.isActivated() }
.none { it.sourceInfo.sender?.id == sourceInfo.playerInfo.playerId }

private val sourceMute: BooleanConfigEntry
get() {
return config.voice
Expand Down
4 changes: 3 additions & 1 deletion client/src/main/resources/assets/plasmovoice/lang/en_us.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@
"gui.plasmovoice.advanced.directional_sources_angle.tooltip": "An angle in which you will hear the source at 100% if directional sources option is enabled.\n\n360 is like if the option is disabled.",
"gui.plasmovoice.advanced.stereo_sources_to_mono": "Mono Stereo Sources",
"gui.plasmovoice.advanced.stereo_sources_to_mono.tooltip": "Addons use stereo sources for better audio quality. The quality is much better, but facing doesn't affect the panning. Sound only fades out with distance.\n\nWhen this option is enabled, stereo sources are converted to the usual mono sources. Quality is worse, but they will now have panning.",
"gui.plasmovoice.advanced.panning": "Stereo positioning",
"gui.plasmovoice.advanced.panning": "Stereo Positioning",
"gui.plasmovoice.advanced.mute_player_on_direct": "Mute Player On Direct",
"gui.plasmovoice.advanced.mute_player_on_direct.tooltip": "When enabled, the player's source will be automatically muted if the direct source from the same player becomes active. Use this to avoid overlapping audio or conflicting playback from multiple sources.",

"gui.plasmovoice.advanced.exponential_volume": "Exponential Volume",
"gui.plasmovoice.advanced.exponential_volume.volume_slider": "Volume Slider",
Expand Down

0 comments on commit 9359a30

Please sign in to comment.