Skip to content

feat: dynamic range compressor for dialogue boost / action limiting - #3117

Open
fgmitesh wants to merge 57 commits into
recloudstream:masterfrom
fgmitesh:feat/dynamic-range-compressor
Open

fgmitesh wants to merge 57 commits into
recloudstream:masterfrom
fgmitesh:feat/dynamic-range-compressor

Conversation

@fgmitesh

@fgmitesh fgmitesh commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Adds a real-time dynamic range compressor accessible via a new
"Compressor" button next to Tracks in the player controls.

Ported from VLC's compressor.c (LGPL, Steve Harris / Ronald Wright).
Implemented as a Media3 AudioProcessor injected into DefaultAudioSink
— sits directly in the audio pipeline, zero overhead when disabled.

Controls: threshold, ratio, attack, release, makeup gain.
All adjustable live via sliders with no player reload needed.
Settings are persisted across sessions.
Defaults: threshold -14dB, ratio 4:1, attack 10ms, release 50ms,
makeup +6dB.

Works with both nextlib and default decoder paths.
Full TV remote navigation. Correctly included in isDialogOpen().

This PR was developed with AI assistance (Claude by Anthropic) for implementation and bug hunting. All code has been tested on a real Android TV device and a phone.

This class implements a real-time dynamic range compressor as an AudioProcessor, allowing for audio signal compression based on specified parameters. It includes methods for configuring audio format, processing input, and managing internal state.
Added support for dynamic range compression in audio sink.
Added a DynamicRangeCompressor instance to the audio sink for live parameter updates.
@fgmitesh

fgmitesh commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

TESTING IS REMAINING = DRAFT FOR NOW

@fgmitesh

fgmitesh commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

It's almost done (Yeah it's W.I.P)

@fgmitesh

Copy link
Copy Markdown
Contributor Author

It's working now but needs little work

@fgmitesh
fgmitesh marked this pull request as ready for review August 11, 2026 07:58
@fgmitesh

Copy link
Copy Markdown
Contributor Author

done +tested+optional (player settings) defualt off

Updated compressor settings restoration logic to always restore settings when opening the dialog and when the player UI loads.
Refactor compressor dialog functions for clarity and efficiency, updating variable names and UI interactions.
Updated compressor dialog layout to follow the speed_dialog.xml pattern, including adjustments to width, orientation, and component arrangement.
Updated default compressor settings for better audio control.
@fgmitesh

Copy link
Copy Markdown
Contributor Author

Fixed some minor bugs now it's fully ready I checked it on tv and phone it works perfectly now

Removed LGPL attribution and simplified comment.

@fire-light42 fire-light42 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First review based on quick testing and reading. It is a very good and cool feature, but needs some small refinement.

Comment thread app/src/main/java/com/lagradost/cloudstream3/ui/player/DynamicRangeCompressor.kt Outdated
Comment thread app/src/main/java/com/lagradost/cloudstream3/ui/player/DynamicRangeCompressor.kt Outdated
Comment thread app/src/main/res/layout/compressor_dialog.xml Outdated
Comment thread app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt Outdated
Comment thread app/src/main/java/com/lagradost/cloudstream3/ui/player/DynamicRangeCompressor.kt Outdated
Comment thread app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt Outdated
@fgmitesh

Copy link
Copy Markdown
Contributor Author

I'll tweak stuff according to reviews u done thanks

@fgmitesh
fgmitesh requested a review from fire-light42 August 27, 2026 10:23
@fgmitesh

Copy link
Copy Markdown
Contributor Author

fixing minor error

@fgmitesh

fgmitesh commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Waiting for re review

@fire-light42 fire-light42 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many smaller changes, but they should be easy to fix.

The feature works well, but it is a bit confusing for users and the code contains a few warnings and a deprecation. It should be ready to merge after this review.

Do not worry about the new settings system, I will update the code before merging.


// ── Threshold slider + FABs ────────────────────────────────────────
binding.compressorThresholdBar.addOnChangeListener { _, value, fromUser ->
if (fromUser) { compressor.threshold = value; updateThresholdLabel(); updateCurrentLabel() }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use newlines instead of ";" to follow the rest of the code base.

</LinearLayout>

<!-- Threshold row: FAB minus | slider | FAB plus -->
<TextView

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to briefly explain how users should think about what this threshold, akin to Sync Subs "Use this if the subtitle are shown 100 ms too late".

Something to tell the user what changing the slider actually does, and preferably what + and - does.

It can be dynamic like the sync subs info, just focus on making it simple and understandable without much text.


<!-- Makeup Gain row: FAB minus | slider | FAB plus -->
<TextView
android:id="@+id/compressor_makeup_label"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing here, just a small text to tell the users what changing this does in a simple manner. It would do wonders for usability.

protected var selectSubtitlesDialog: Dialog? = null
protected var selectCompressorDialog: Dialog? = null
protected var playBackCompressorEnabled = false
set(value) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This setter no longer refers to the selectSubtitlesDialog!

playerBinding?.apply {
playerSpeedBtt.isVisible = playBackSpeedEnabled
playerCompressorBtt.isVisible = playBackCompressorEnabled
if (playBackCompressorEnabled) restoreCompressorSettings()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use braces here to reduce risks of bugs when refactoring.

if (compressor != null) arrayOf(compressor) else emptyArray()
return DefaultAudioSink.Builder(context)
.setEnableFloatOutput(enableFloatOutput)
.setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is deprecated, please use setEnableAudioOutputPlaybackParameters instead

private var playBackSpeed: Float = 1.0f

/** Shared compressor — created once, injected into the audio sink, params updated live. */
val compressor = DynamicRangeCompressor()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the compressor is always used even if the setting is turned off, it just affects how to player compressor logic works. I would prefer if the dynamic compressor object is not used at all when turned off in player settings.

This is because I cannot trust all android TV variants to behave as expected. Some TV variants will undoubtedly have unexpected issues with a custom audio processor even if the compressor is perfect (we already have many issues with FireOS). I therefore want to keep the default playback experience unmodified.

syncPresetButtons(activeBtn)
}

syncPresetButtons(null) // none selected by default

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please automatically highlight the selected button based on the preset. Reopening the dialog removes the visual selection.

@fire-light42

Copy link
Copy Markdown
Collaborator

Great job on the pull request!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants