feat(audio): add native PEQ DSP and linear-phase processing - #60
Conversation
📝 WalkthroughWalkthroughThe PR adds a configurable audio DSP system. It includes serializable models, persistence, PEQ and FIR processing, channel routing, limiting, playback integration, Android TV controls, and WebAPI/WebUI controls. ChangesAudio DSP feature
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 15
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (9)
ui-tv/src/main/kotlin/com/miruplay/tv/ui/settings/AddSourceScreen.kt-3427-3467 (1)
3427-3467: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winState that DSP changes apply to the next playback session.
The control text describes the selected configuration as current audio output. The PR objective specifies that DSP changes apply only to the next playback session. During active playback, this can report an incorrect DSP state.
Update the enable chip, description, and status text to identify the configuration as pending for the next playback session.
Proposed text changes
- text = "电视端仅提供总开关和预设切换,完整频段与线性相位设置请使用 WebUI。" + text = "电视端仅提供总开关和预设切换,完整频段与线性相位设置请使用 WebUI。设置将在下次播放时生效。" ... - text = if (config.enabled) "音频 DSP 已启用" else "音频 DSP 已关闭", + text = if (config.enabled) "下次播放启用音频 DSP" else "下次播放关闭音频 DSP", ... - "当前预设:${config.presets.firstOrNull { it.id == config.selectedPresetId }?.name ?: config.selectedPresetId}" + "下次播放预设:${config.presets.firstOrNull { it.id == config.selectedPresetId }?.name ?: config.selectedPresetId}" ... - "音频保持原始输出,不应用 PEQ 或 FIR" + "下次播放保持原始输出,不应用 PEQ 或 FIR"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ui-tv/src/main/kotlin/com/miruplay/tv/ui/settings/AddSourceScreen.kt` around lines 3427 - 3467, Update the DSP UI text in the AddSourceScreen configuration block so the enable chip, descriptive text, and StatusMessage clearly state that changes are pending and take effect on the next playback session. Preserve the existing controls and state logic while replacing wording that presents the selected configuration as currently applied audio output.web-control/frontend/src/App.vue-1277-1288 (1)
1277-1288: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winThe chart axis label does not match the plotted scale.
audioPreviewPointscomputesmaxLogasMath.log10(Math.max(24_000, ...))at line 1967, so the right edge of the plot area is 24 kHz. The label at line 1282 states "20 kHz". The preview requests frequencies up to 20 kHz, so the curve stops short of the right edge while the label claims the edge is 20 kHz.Use the same bound in both places. Either label the right edge "24 kHz", or clamp
maxLogtoMath.log10(20_000).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web-control/frontend/src/App.vue` around lines 1277 - 1288, Align the right-edge frequency label in the chart rendered by the audio preview template with the bound used by audioPreviewPoints: either change the label to 24 kHz to match maxLog’s 24,000 Hz scale, or change maxLog to use 20,000 Hz so the existing 20 kHz label remains accurate. Keep the plotted curve and axis label on the same frequency scale.web-control/frontend/src/App.vue-3319-3327 (1)
3319-3327: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winFollow the existing download pattern for the JSON export.
exportAudioDspclicks an anchor that is not attached to the document, and revokes the object URL in the same tick.downloadLocalLogsat lines 3078-3084 appends the link, clicks it, removes it, and then revokes the URL. Some browsers ignore a click on a detached anchor, and an immediate revoke can cancel an in-flight download.🐛 Proposed fix
function exportAudioDsp() { const blob = new Blob([JSON.stringify(audioDsp.config, null, 2)], { type: 'application/json' }) const url = URL.createObjectURL(blob) const anchor = document.createElement('a') anchor.href = url anchor.download = 'miruplay-audio-dsp.json' + document.body.appendChild(anchor) anchor.click() + anchor.remove() URL.revokeObjectURL(url) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web-control/frontend/src/App.vue` around lines 3319 - 3327, Update exportAudioDsp to follow the existing downloadLocalLogs pattern: append the created anchor to the document before clicking it, remove the anchor afterward, and revoke the object URL only after the click/download has been initiated.web-control/frontend/src/App.vue-1239-1241 (1)
1239-1241: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAdd labels to the band numeric inputs.
The three
el-input-numbercontrols for frequency, gain, and Q have no label and noaria-label. The only hint is the muted text at line 1248. A screen reader user cannot tell the three spin buttons apart. Line 1231 already usesaria-labelfor the output gain.♿ Proposed fix
- <el-input-number v-model="band.frequencyHz" :min="10" :max="24000" :step="10" controls-position="right" /> - <el-input-number v-model="band.gainDb" :min="-24" :max="24" :step="0.1" :precision="1" controls-position="right" /> - <el-input-number v-model="band.q" :min="0.1" :max="20" :step="0.1" :precision="1" controls-position="right" /> + <el-input-number v-model="band.frequencyHz" :min="10" :max="24000" :step="10" controls-position="right" aria-label="频率 Hz" /> + <el-input-number v-model="band.gainDb" :min="-24" :max="24" :step="0.1" :precision="1" controls-position="right" aria-label="增益 dB" /> + <el-input-number v-model="band.q" :min="0.1" :max="20" :step="0.1" :precision="1" controls-position="right" aria-label="Q 值" />🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web-control/frontend/src/App.vue` around lines 1239 - 1241, Add accessible labels to the three band controls in the template: give the frequencyHz, gainDb, and q el-input-number elements distinct aria-label values describing frequency, gain, and Q. Follow the existing aria-label pattern used by the output gain control while preserving the current numeric constraints and bindings.web-control-core/src/test/kotlin/com/miruplay/tv/webcontrol/WebControlSettingsRouteTest.kt-121-130 (1)
121-130: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winThe invalid-config assertion cannot detect a regression.
The invalid payload at line 125 also sets
"enabled":true. If the server accepted it and the stub stored it,service.audioDspConfig.enabledwould still betrue. The assertion at line 129 therefore passes in both cases.Assert a field that differs between the valid and the invalid payload, for example
preampDb, or count save calls in the stub.💚 Proposed fix
assertEquals(NanoHTTPD.Response.Status.BAD_REQUEST, invalid.status) - assertTrue(service.audioDspConfig.enabled) + assertEquals(1, service.saveAudioDspCalls) + assertEquals(0f, service.audioDspConfig.presets.single().preampDb)Add the counter to the stub:
var saveAudioDspCalls = 0and increment it inside
saveAudioDsp.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web-control-core/src/test/kotlin/com/miruplay/tv/webcontrol/WebControlSettingsRouteTest.kt` around lines 121 - 130, Strengthen the invalid audio-DSP request test around the server call by asserting a value that differs from the invalid payload, such as the existing preampDb configuration, instead of audioDspConfig.enabled. Alternatively, add a saveAudioDspCalls counter to the stub and increment it in saveAudioDsp, then assert the invalid request does not save.Source: Coding guidelines
core/model/src/main/kotlin/com/miruplay/tv/model/AudioDspModels.kt-156-168 (1)
156-168: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winTrim
selectedPresetIdbefore comparing it to normalized preset ids.
AudioDspPreset.normalized()trims each presetid(Line 141), butAudioDspConfig.normalized()comparesnormalizedPresetsids to the rawselectedPresetIdwithout trimming it first. AselectedPresetIdwith incidental leading or trailing whitespace (for example from WebUI JSON import) will fail to match its intended preset and silently fall back to the first preset, with no validation error surfaced.🐛 Proposed fix
fun normalized(): AudioDspConfig { val normalizedPresets = presets .map(AudioDspPreset::normalized) .distinctBy(AudioDspPreset::id) .ifEmpty { listOf(neutralPreset()) } - val selected = normalizedPresets.firstOrNull { it.id == selectedPresetId }?.id + val trimmedSelectedId = selectedPresetId.trim() + val selected = normalizedPresets.firstOrNull { it.id == trimmedSelectedId }?.id ?: normalizedPresets.first().id🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@core/model/src/main/kotlin/com/miruplay/tv/model/AudioDspModels.kt` around lines 156 - 168, Update AudioDspConfig.normalized() to trim selectedPresetId before comparing it with the normalized preset IDs, while preserving the existing fallback to the first preset when no match exists.docs/superpowers/specs/2026-08-03-audio-dsp-design.md-61-61 (1)
61-61: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winSpecify the fenced code-block language.
Add
textafter the opening fence. This resolves markdownlint rule MD040.Proposed fix
-``` +```text🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/superpowers/specs/2026-08-03-audio-dsp-design.md` at line 61, Update the fenced code block at the specified location in the documentation to declare the text language by changing its opening fence to use ```text, preserving the block contents and closing fence.Source: Linters/SAST tools
audio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/FrequencyResponse.kt-37-41 (1)
37-41: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winCompute biquad phase when building minimum-phase frequency response.
The biquad branch only accumulates magnitude, so
phaseRadiansstays0.0for non-linear phase presets such asMINIMUM. A preview response curve that drawsphaseRadianswill show an unrealistic flat phase curve for active biquad presets like peaking and shelf filters. Add a complex value path for biquads and accumulate magnitude and accumulatedatan2phase.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@audio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/FrequencyResponse.kt` around lines 37 - 41, Update the biquad branch in FrequencyResponse’s frequency-response calculation to use a complex-valued path for each biquad, accumulating both its magnitude and atan2-derived phase into the response. Ensure phaseRadians reflects the accumulated biquad phase for active presets such as peaking and shelf filters while preserving the existing magnitude behavior.player-core/src/main/kotlin/com/miruplay/tv/player/DspAudioProcessor.kt-24-35 (1)
24-35: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winRead
runtimeConfig.configonce before configuring the DSP.
configis updated through a@Volatilesetter, butonConfigurereads it separately forenabled, the preset lookup, and the fallback. Capture a local copy first soselectedPresetIdandpresetsbelong to the same snapshot, and remove thecompiledPlan!!assertions.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@player-core/src/main/kotlin/com/miruplay/tv/player/DspAudioProcessor.kt` around lines 24 - 35, Update DspAudioProcessor.onConfigure to capture runtimeConfig.config in a local snapshot before checking enabled or selecting a preset, then use that snapshot consistently for preset lookup and fallback so all values come from one configuration state. Replace the nullable compiledPlan assignment and processor construction to avoid compiledPlan!! assertions while preserving the existing DSP setup behavior.
🧹 Nitpick comments (9)
web-control/frontend/src/App.vue (1)
3329-3343: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueHandle a
FileReadererror during JSON import.
importAudioDspsetsonloadonly. If the read fails, the user receives no feedback and the import silently does nothing.♻️ Proposed change
const reader = new FileReader() + reader.onerror = () => ElMessage.error('读取 JSON 文件失败') reader.onload = () => {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web-control/frontend/src/App.vue` around lines 3329 - 3343, Update importAudioDsp to register a FileReader error handler in addition to its onload handler, and display an ElMessage error when reading the file fails so the import never silently does nothing.web-control/src/main/kotlin/com/miruplay/tv/webcontrol/WebControlService.kt (1)
761-781: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueImport
AudioDspOutputModeinstead of the fully qualified name.Line 776 uses
com.miruplay.tv.model.AudioDspOutputMode.HRTF_BINAURAL. The file already imports the other model types at lines 19-27.♻️ Proposed refactor
- if (config.enabled && preset?.outputMode == com.miruplay.tv.model.AudioDspOutputMode.HRTF_BINAURAL) { + if (config.enabled && preset?.outputMode == AudioDspOutputMode.HRTF_BINAURAL) {Add the import:
import com.miruplay.tv.model.AudioDspOutputMode🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web-control/src/main/kotlin/com/miruplay/tv/webcontrol/WebControlService.kt` around lines 761 - 781, Import AudioDspOutputMode alongside the other model types in WebControlService, then replace the fully qualified AudioDspOutputMode.HRTF_BINAURAL reference in getAudioDsp with the imported symbol.web-control-core/src/main/kotlin/com/miruplay/tv/webcontrol/WebControlEndpointService.kt (1)
83-86: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAlign the DSP defaults with the surrounding "not supported" convention.
getScanSettings,savePlaybackSettings, and the other settings methods throwUnsupportedOperationExceptionwhen a host does not implement them. The DSP defaults instead return success values. A host that does not overridesaveAudioDspreports a successful save to the WebUI, but persists nothing.previewAudioDspreturns a flat zero curve that looks like a valid response.If the neutral defaults are intentional for embedded hosts, keep them. Otherwise throw so unimplemented hosts fail loudly.
♻️ Proposed change
- suspend fun getAudioDsp(): AudioDspDto = AudioDspDto() - suspend fun saveAudioDsp(config: AudioDspConfig): AudioDspDto = AudioDspDto(config = config.normalized()) - suspend fun previewAudioDsp(request: AudioDspPreviewRequest): AudioDspPreviewDto = - AudioDspPreviewDto(request.frequenciesHz, request.frequenciesHz.map { 0f }, request.frequenciesHz.map { 0f }) + suspend fun getAudioDsp(): AudioDspDto = + throw UnsupportedOperationException("音频 DSP not supported") + suspend fun saveAudioDsp(config: AudioDspConfig): AudioDspDto = + throw UnsupportedOperationException("音频 DSP not supported") + suspend fun previewAudioDsp(request: AudioDspPreviewRequest): AudioDspPreviewDto = + throw UnsupportedOperationException("音频 DSP not supported")🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web-control-core/src/main/kotlin/com/miruplay/tv/webcontrol/WebControlEndpointService.kt` around lines 83 - 86, Update the default implementations of getAudioDsp, saveAudioDsp, and previewAudioDsp in WebControlEndpointService to throw UnsupportedOperationException instead of returning successful placeholder values. Preserve host-specific overrides while ensuring unimplemented DSP operations fail loudly like the surrounding settings methods.web-control-core/src/main/kotlin/com/miruplay/tv/webcontrol/NanoHttpWebControlServer.kt (1)
252-263: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
AudioDspConfigvalidation runs twice per request, and each site evaluatesvalidationErrors()twice. The route validates the configuration, thensaveAudioDspvalidates the same object again. Both sites also callvalidationErrors()a second time to build the error message. Keep validation in one layer, and compute the error list once.
web-control-core/src/main/kotlin/com/miruplay/tv/webcontrol/NanoHttpWebControlServer.kt#L252-L263: storeconfig.validationErrors()in a local value, or remove the route-level check and let the service throwIllegalArgumentException, whichservealready maps toBAD_REQUEST.web-control/src/main/kotlin/com/miruplay/tv/webcontrol/WebControlService.kt#L783-L792: storeconfig.validationErrors()in a local value and keep this as the single validation point, so hosts that call the service directly are also protected.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web-control-core/src/main/kotlin/com/miruplay/tv/webcontrol/NanoHttpWebControlServer.kt` around lines 252 - 263, The AudioDspConfig validation is duplicated and validationErrors() is evaluated twice at both sites. In web-control-core/src/main/kotlin/com/miruplay/tv/webcontrol/NanoHttpWebControlServer.kt#L252-L263, remove the route-level validation so saveAudioDsp remains the single validation point; in web-control/src/main/kotlin/com/miruplay/tv/webcontrol/WebControlService.kt#L783-L792, store config.validationErrors() once and reuse it when throwing IllegalArgumentException.web-control-core/src/main/kotlin/com/miruplay/tv/webcontrol/WebControlModels.kt (1)
366-373: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low valueThe capability defaults are static and can misreport the device.
supportedLayouts,sampleRatesHz,maxChannels, andhrtfAvailableare fixed literals.WebControlService.getAudioDspoverrides onlysupportedBackends, so every client receives these hardcoded values regardless of the device output configuration.hrtfAvailable = truealso conflicts with the documented behavior that HRTF is a fixed compatibility downmix matrix, not a real HRIR/SOFA renderer.Consider deriving these fields from the audio output policy, or rename
hrtfAvailableto describe the compatibility downmix.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web-control-core/src/main/kotlin/com/miruplay/tv/webcontrol/WebControlModels.kt` around lines 366 - 373, Update AudioDspCapabilitiesDto and WebControlService.getAudioDsp so supportedLayouts, sampleRatesHz, maxChannels, and the HRTF-related field reflect the device audio output policy instead of static defaults. Remove the misleading hrtfAvailable=true behavior; either derive genuine HRTF availability from the device or rename the field to describe the fixed compatibility downmix, preserving accurate capability reporting for clients.core/model/src/main/kotlin/com/miruplay/tv/model/AudioDspModels.kt (1)
118-147: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract shared constants for preamp and limiter ranges.
AudioDspPreset.normalized()clampspreampDbto-24f..12fas inline literals (Line 143).AudioDspConfig.validationErrors()re-checks the same range as a separate literal (Line 181). The same duplication exists forAudioDspLimiter.ceilingDb(-24f..0f, Lines 124 and 183) andreleaseMs(1f..2_000f, Lines 125 and 186-187).
AudioDspBandandAudioDspChannelRulealready define named constants (MIN_FREQUENCY_HZ,MAX_GAIN_DB, etc.) that bothnormalized()andvalidationErrors()reuse. Apply the same pattern toAudioDspPresetandAudioDspLimiterto prevent the ranges from silently diverging if only one location is updated later.♻️ Proposed constants extraction
data class AudioDspLimiter( val enabled: Boolean = false, val ceilingDb: Float = -1f, val releaseMs: Float = 100f, ) { fun normalized(): AudioDspLimiter = copy( - ceilingDb = ceilingDb.coerceIn(-24f, 0f), - releaseMs = releaseMs.coerceIn(1f, 2_000f), + ceilingDb = ceilingDb.coerceIn(MIN_CEILING_DB, MAX_CEILING_DB), + releaseMs = releaseMs.coerceIn(MIN_RELEASE_MS, MAX_RELEASE_MS), ) + + companion object { + const val MIN_CEILING_DB = -24f + const val MAX_CEILING_DB = 0f + const val MIN_RELEASE_MS = 1f + const val MAX_RELEASE_MS = 2_000f + } }Then reference
AudioDspLimiter.MIN_CEILING_DBetc. invalidationErrors(), and add similarMIN_PREAMP_DB/MAX_PREAMP_DBconstants toAudioDspPreset.Also applies to: 170-209
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@core/model/src/main/kotlin/com/miruplay/tv/model/AudioDspModels.kt` around lines 118 - 147, Extract named range constants in AudioDspLimiter for ceilingDb (-24f..0f) and releaseMs (1f..2_000f), and in AudioDspPreset for preampDb (-24f..12f). Update both normalized() methods and AudioDspConfig.validationErrors() to reference these shared constants, preserving the existing validation ranges and behavior.audio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/FrequencyResponse.kt (1)
9-13: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
ResponseCurveis a data class that holds arrays.The generated
equalsandhashCodecompareFloatArrayreferences, not contents. Two curves with identical values are not equal. Tests or caches that compare curves will fail in a way that is hard to diagnose.Use
List<Float>for the three properties, or overrideequalsandhashCodewithcontentEqualsandcontentHashCode.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@audio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/FrequencyResponse.kt` around lines 9 - 13, Update the ResponseCurve data class so equality and hashing compare frequenciesHz, magnitudeDb, and phaseRadians by contents rather than array references. Prefer converting these properties to List<Float>, or implement content-based equals and hashCode using contentEquals and contentHashCode while preserving the existing curve data.audio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/SurroundDownmix.kt (1)
34-52: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
standardandhrtfduplicate the same loop.The two functions differ only in the per-channel coefficient pairs. The traversal, the accumulation, and the return are identical. The coefficients are also unnamed literals, so their origin is not documented.
Extract one private
mix(source, layout, coefficients)helper. Define the two coefficient tables as named constants with a short comment that states the source of each value.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@audio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/SurroundDownmix.kt` around lines 34 - 52, Refactor standard and hrtf to share a private mix(source, layout, coefficients) helper containing the common traversal, accumulation, and stereo return logic. Define separate named coefficient tables for each mode and replace the inline literals with those tables, adding brief comments documenting the source of each coefficient set. Preserve each function’s existing channel-count handling and output behavior.player-core/src/main/kotlin/com/miruplay/tv/player/DspAudioProcessor.kt (1)
70-74: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winHoist the encoding branch out of the per-sample loop.
outputSamples.forEachtestsencoding == C.ENCODING_PCM_FLOATonce per sample. This runs on the audio thread for every frame of every buffer. The branch value is fixed for the whole configuration, so it belongs outside the loop.Lines 83-87 in
onQueueEndOfStreamrepeat the same pattern and the same conversion arithmetic. Extract one privatewriteSamples(target: ByteBuffer, samples: FloatArray)helper and call it from both places.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@player-core/src/main/kotlin/com/miruplay/tv/player/DspAudioProcessor.kt` around lines 70 - 74, Extract the duplicated sample-writing logic from DspAudioProcessor into a private writeSamples(target: ByteBuffer, samples: FloatArray) helper. Hoist the encoding == C.ENCODING_PCM_FLOAT branch outside the per-sample loop, preserving float writes and clamped PCM-16 conversion, then call the helper from both the current output path and onQueueEndOfStream.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@audio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/AudioDspPlanCompiler.kt`:
- Around line 79-91: The AudioDspChannelTarget.matches extension currently
treats SURROUND_5_1 and SURROUND_7_1 identically and ignores layout. Update this
branch to use the resolved layout: SURROUND_5_1 should match only LS/RS, while
SURROUND_7_1 should additionally match LB/RB; preserve the existing SURROUND
behavior and use the layout parameter to determine applicable channels.
In `@audio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/BiquadDesigner.kt`:
- Around line 30-36: Update BiquadDesigner.design to validate sampleRateHz
before calculating omega: reject nonpositive sample rates and bands whose
normalized frequency is at or above the Nyquist limit (sampleRateHz / 2),
including persisted high-frequency bands. Preserve valid coefficient calculation
and add coverage for low sample rates and high-frequency bands.
In `@audio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/ChannelLayout.kt`:
- Around line 17-29: Update the channel remapping logic in ChannelLayout so
InputOrder.AAC_5_1 uses the canonical mapping 1,2,0,5,3,4. Before indexing with
the selected order in the remapping flow, return samples.copyOf() or otherwise
reject the input when the mapping length does not equal channelCount, preventing
invalid access for layouts with more than six channels.
In
`@audio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/LinearPhaseFirDesigner.kt`:
- Around line 8-34: Replace the direct inverse-DFT summation in
LinearPhaseFirDesigner.design with a radix-2 FFT-based IFFT, reusing the
existing spectrumReal and spectrumImag arrays and power-of-two taps validation.
Preserve the current magnitude interpolation, phase construction, Hermitian
mirroring, normalization by taps, and FloatArray output ordering while reducing
reconstruction complexity to O(taps·log2(taps)).
In `@audio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/LinkedLimiter.kt`:
- Around line 13-23: Move the releaseCoefficient calculation out of
LinkedLimiter.process and cache it as a class-level immutable property
initialized from releaseMs and sampleRateHz. Remove the per-call exp()
computation while preserving the existing clamping and coefficient behavior.
In `@audio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/SurroundDownmix.kt`:
- Line 31: Replace the clamping in the standard downmix return with
normalization or ITU-style attenuation so summed multichannel peaks are reduced
without clipping, while preserving stereo output and allowing LinkedLimiter to
handle remaining peaks. Apply the same treatment to the hrtf downmix return as
well.
In `@docs/superpowers/plans/2026-08-03-audio-dsp.md`:
- Around line 7-9: Align all HRTF documentation with the delivered
compatibility-routing scope: in docs/superpowers/plans/2026-08-03-audio-dsp.md
lines 7-9, replace the full Resonance Audio renderer architecture claim with the
documented downmix compatibility matrix; in lines 256-292, remove native HRTF
renderer tasks and asset requirements; in
docs/superpowers/specs/2026-08-03-audio-dsp-design.md lines 85-91, document the
supported compatibility matrix and explicit limitations; and in lines 99-103,
remove claims about SOFA processing and headphone rendering.
In `@player-core/src/main/kotlin/com/miruplay/tv/player/AudioDspMpvOptions.kt`:
- Around line 21-32: Fix invalid mpv/libavfilter expressions in
AudioDspMpvOptions.kt: in the band mapping at lines 21-32, use equalizer for
PEAKING and omit gain from LOW_PASS, HIGH_PASS, NOTCH, and BAND_PASS; at lines
16-19, emit gain_entry as gain_entry(frequency,gain) without quotes; at lines
41-45, replace the unsupported single-input headphone filter and wrap pan as
lavfi=[pan=stereo|c0=...] to preserve separators. Add tests asserting the exact
generated filter string for every AudioDspFilterType and AudioDspOutputMode.
- Around line 16-19: Update the LINEAR branch in AudioDspMpvOptions so the
firequalizer gain_entry uses FFmpeg-compatible entry(frequency,gain) expressions
instead of colon-separated pairs. Preserve the semicolon-separated entries,
enabled-band filtering, and the existing zero fallback, formatting the fallback
as entry(0,0).
- Around line 41-45: Update the filter construction in AudioDspMpvOptions so
each lavfi graph is wrapped in brackets before joining the af chain, preserving
the pan and headphone pipe expressions as filter arguments. For HRTF_BINAURAL,
define and connect the required HRIR input streams alongside the mapped
headphone filter, rather than emitting only the audio input graph.
In `@player-core/src/main/kotlin/com/miruplay/tv/player/DspAudioProcessor.kt`:
- Around line 49-53: Update queueInput in DspAudioProcessor so the
processor-null path copies the input buffer to the output buffer instead of
advancing the input to its limit and discarding frames. Preserve normal
processing through the active processor when processor is non-null.
In `@player-core/src/main/kotlin/com/miruplay/tv/player/DspRenderersFactory.kt`:
- Line 21: Update the audio processor configuration in DspRenderersFactory to
construct a DefaultAudioProcessorChain containing the existing default
SilenceSkippingAudioProcessor and SonicAudioProcessor plus
DspAudioProcessor(runtimeConfig). Pass that complete chain to the renderer so
playback speed and pitch fallback behavior remains unchanged.
- Around line 17-19: Update DspRenderersFactory.buildAudioSink to apply the full
AudioDspOutputPolicy, not just forcePcm: propagate allowOffload through the
audio capabilities/offload configuration, constrain track selection so
allowPassthrough is respected, and gate renderer/audio-output tunneling with
allowTunneling. Ensure DSP-enabled bitstream tracks cannot bypass
DspAudioProcessor while preserving normal policy-enabled offload, passthrough,
and tunneling behavior.
In
`@player-core/src/main/kotlin/com/miruplay/tv/player/ExperimentalRenderersFactory.kt`:
- Around line 30-35: Update the DefaultAudioSink configuration in
ExperimentalRenderersFactory so DSP-enabled playback is restricted to PCM by
applying AudioDspOutputPolicy to encoded output, offload, and passthrough
capabilities rather than only forcing float output. Add a negotiation test
covering DSP-enabled encoded formats and verify they are rejected while PCM
remains supported.
In `@web-control/src/main/kotlin/com/miruplay/tv/webcontrol/WebControlService.kt`:
- Around line 794-802: Update previewAudioDsp to compile a bounded low-quality
preview preset instead of allowing request.preset to trigger expensive
high-quality linear-phase FIR design, and serialize concurrent preview
compilations using the existing profileInProgress guard or an equivalent
in-flight guard. Preserve the current frequency validation and response
generation while ensuring the guard is released after compilation, including
failure paths.
---
Minor comments:
In `@audio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/FrequencyResponse.kt`:
- Around line 37-41: Update the biquad branch in FrequencyResponse’s
frequency-response calculation to use a complex-valued path for each biquad,
accumulating both its magnitude and atan2-derived phase into the response.
Ensure phaseRadians reflects the accumulated biquad phase for active presets
such as peaking and shelf filters while preserving the existing magnitude
behavior.
In `@core/model/src/main/kotlin/com/miruplay/tv/model/AudioDspModels.kt`:
- Around line 156-168: Update AudioDspConfig.normalized() to trim
selectedPresetId before comparing it with the normalized preset IDs, while
preserving the existing fallback to the first preset when no match exists.
In `@docs/superpowers/specs/2026-08-03-audio-dsp-design.md`:
- Line 61: Update the fenced code block at the specified location in the
documentation to declare the text language by changing its opening fence to use
```text, preserving the block contents and closing fence.
In `@player-core/src/main/kotlin/com/miruplay/tv/player/DspAudioProcessor.kt`:
- Around line 24-35: Update DspAudioProcessor.onConfigure to capture
runtimeConfig.config in a local snapshot before checking enabled or selecting a
preset, then use that snapshot consistently for preset lookup and fallback so
all values come from one configuration state. Replace the nullable compiledPlan
assignment and processor construction to avoid compiledPlan!! assertions while
preserving the existing DSP setup behavior.
In `@ui-tv/src/main/kotlin/com/miruplay/tv/ui/settings/AddSourceScreen.kt`:
- Around line 3427-3467: Update the DSP UI text in the AddSourceScreen
configuration block so the enable chip, descriptive text, and StatusMessage
clearly state that changes are pending and take effect on the next playback
session. Preserve the existing controls and state logic while replacing wording
that presents the selected configuration as currently applied audio output.
In
`@web-control-core/src/test/kotlin/com/miruplay/tv/webcontrol/WebControlSettingsRouteTest.kt`:
- Around line 121-130: Strengthen the invalid audio-DSP request test around the
server call by asserting a value that differs from the invalid payload, such as
the existing preampDb configuration, instead of audioDspConfig.enabled.
Alternatively, add a saveAudioDspCalls counter to the stub and increment it in
saveAudioDsp, then assert the invalid request does not save.
In `@web-control/frontend/src/App.vue`:
- Around line 1277-1288: Align the right-edge frequency label in the chart
rendered by the audio preview template with the bound used by
audioPreviewPoints: either change the label to 24 kHz to match maxLog’s 24,000
Hz scale, or change maxLog to use 20,000 Hz so the existing 20 kHz label remains
accurate. Keep the plotted curve and axis label on the same frequency scale.
- Around line 3319-3327: Update exportAudioDsp to follow the existing
downloadLocalLogs pattern: append the created anchor to the document before
clicking it, remove the anchor afterward, and revoke the object URL only after
the click/download has been initiated.
- Around line 1239-1241: Add accessible labels to the three band controls in the
template: give the frequencyHz, gainDb, and q el-input-number elements distinct
aria-label values describing frequency, gain, and Q. Follow the existing
aria-label pattern used by the output gain control while preserving the current
numeric constraints and bindings.
---
Nitpick comments:
In `@audio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/FrequencyResponse.kt`:
- Around line 9-13: Update the ResponseCurve data class so equality and hashing
compare frequenciesHz, magnitudeDb, and phaseRadians by contents rather than
array references. Prefer converting these properties to List<Float>, or
implement content-based equals and hashCode using contentEquals and
contentHashCode while preserving the existing curve data.
In `@audio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/SurroundDownmix.kt`:
- Around line 34-52: Refactor standard and hrtf to share a private mix(source,
layout, coefficients) helper containing the common traversal, accumulation, and
stereo return logic. Define separate named coefficient tables for each mode and
replace the inline literals with those tables, adding brief comments documenting
the source of each coefficient set. Preserve each function’s existing
channel-count handling and output behavior.
In `@core/model/src/main/kotlin/com/miruplay/tv/model/AudioDspModels.kt`:
- Around line 118-147: Extract named range constants in AudioDspLimiter for
ceilingDb (-24f..0f) and releaseMs (1f..2_000f), and in AudioDspPreset for
preampDb (-24f..12f). Update both normalized() methods and
AudioDspConfig.validationErrors() to reference these shared constants,
preserving the existing validation ranges and behavior.
In `@player-core/src/main/kotlin/com/miruplay/tv/player/DspAudioProcessor.kt`:
- Around line 70-74: Extract the duplicated sample-writing logic from
DspAudioProcessor into a private writeSamples(target: ByteBuffer, samples:
FloatArray) helper. Hoist the encoding == C.ENCODING_PCM_FLOAT branch outside
the per-sample loop, preserving float writes and clamped PCM-16 conversion, then
call the helper from both the current output path and onQueueEndOfStream.
In
`@web-control-core/src/main/kotlin/com/miruplay/tv/webcontrol/NanoHttpWebControlServer.kt`:
- Around line 252-263: The AudioDspConfig validation is duplicated and
validationErrors() is evaluated twice at both sites. In
web-control-core/src/main/kotlin/com/miruplay/tv/webcontrol/NanoHttpWebControlServer.kt#L252-L263,
remove the route-level validation so saveAudioDsp remains the single validation
point; in
web-control/src/main/kotlin/com/miruplay/tv/webcontrol/WebControlService.kt#L783-L792,
store config.validationErrors() once and reuse it when throwing
IllegalArgumentException.
In
`@web-control-core/src/main/kotlin/com/miruplay/tv/webcontrol/WebControlEndpointService.kt`:
- Around line 83-86: Update the default implementations of getAudioDsp,
saveAudioDsp, and previewAudioDsp in WebControlEndpointService to throw
UnsupportedOperationException instead of returning successful placeholder
values. Preserve host-specific overrides while ensuring unimplemented DSP
operations fail loudly like the surrounding settings methods.
In
`@web-control-core/src/main/kotlin/com/miruplay/tv/webcontrol/WebControlModels.kt`:
- Around line 366-373: Update AudioDspCapabilitiesDto and
WebControlService.getAudioDsp so supportedLayouts, sampleRatesHz, maxChannels,
and the HRTF-related field reflect the device audio output policy instead of
static defaults. Remove the misleading hrtfAvailable=true behavior; either
derive genuine HRTF availability from the device or rename the field to describe
the fixed compatibility downmix, preserving accurate capability reporting for
clients.
In `@web-control/frontend/src/App.vue`:
- Around line 3329-3343: Update importAudioDsp to register a FileReader error
handler in addition to its onload handler, and display an ElMessage error when
reading the file fails so the import never silently does nothing.
In `@web-control/src/main/kotlin/com/miruplay/tv/webcontrol/WebControlService.kt`:
- Around line 761-781: Import AudioDspOutputMode alongside the other model types
in WebControlService, then replace the fully qualified
AudioDspOutputMode.HRTF_BINAURAL reference in getAudioDsp with the imported
symbol.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 72fb23f2-e7c9-448a-91e7-70109d38a0a1
📒 Files selected for processing (48)
audio-dsp-core/build.gradle.ktsaudio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/AudioDspPlanCompiler.ktaudio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/BiquadDesigner.ktaudio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/ChannelLayout.ktaudio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/FrequencyResponse.ktaudio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/LinearPhaseFirDesigner.ktaudio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/LinkedLimiter.ktaudio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/StreamingDspProcessor.ktaudio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/SurroundDownmix.ktaudio-dsp-core/src/test/kotlin/com/miruplay/tv/audio/BiquadDesignerTest.ktaudio-dsp-core/src/test/kotlin/com/miruplay/tv/audio/ChannelLayoutTest.ktaudio-dsp-core/src/test/kotlin/com/miruplay/tv/audio/DownmixTest.ktaudio-dsp-core/src/test/kotlin/com/miruplay/tv/audio/LinearPhaseFirDesignerTest.ktaudio-dsp-core/src/test/kotlin/com/miruplay/tv/audio/LinkedLimiterTest.ktaudio-dsp-core/src/test/kotlin/com/miruplay/tv/audio/StreamingDspProcessorTest.ktcore/model/src/main/kotlin/com/miruplay/tv/model/AudioDspModels.ktcore/model/src/test/kotlin/com/miruplay/tv/model/AudioDspModelsTest.ktdata/src/main/kotlin/com/miruplay/tv/data/preferences/PlaybackPreferencesManager.ktdata/src/test/kotlin/com/miruplay/tv/data/preferences/PlaybackPreferencesManagerTest.ktdocs/superpowers/plans/2026-08-03-audio-dsp.mddocs/superpowers/specs/2026-08-03-audio-dsp-design.mddocs/verification/audio-dsp-hk1.mdplayer-core/build.gradle.ktsplayer-core/src/main/kotlin/com/miruplay/tv/player/AudioDspMpvOptions.ktplayer-core/src/main/kotlin/com/miruplay/tv/player/AudioDspOutputPolicy.ktplayer-core/src/main/kotlin/com/miruplay/tv/player/AudioDspRuntimeConfig.ktplayer-core/src/main/kotlin/com/miruplay/tv/player/DiModule.ktplayer-core/src/main/kotlin/com/miruplay/tv/player/DspAudioProcessor.ktplayer-core/src/main/kotlin/com/miruplay/tv/player/DspRenderersFactory.ktplayer-core/src/main/kotlin/com/miruplay/tv/player/EmbeddedMpvSessionOptions.ktplayer-core/src/main/kotlin/com/miruplay/tv/player/ExoPlaybackController.ktplayer-core/src/main/kotlin/com/miruplay/tv/player/ExperimentalRenderersFactory.ktplayer-core/src/test/kotlin/com/miruplay/tv/player/AudioDspMpvOptionsTest.ktplayer-core/src/test/kotlin/com/miruplay/tv/player/AudioDspOutputPolicyTest.ktplayer-core/src/test/kotlin/com/miruplay/tv/player/DspAudioProcessorTest.ktplayer-ijkplayer-android/src/main/kotlin/com/miruplay/tv/player/ijk/android/MiruIjkSurfaceView.ktrepository-api/src/main/kotlin/com/miruplay/tv/repository/PlaybackPreferencesRepository.ktsettings.gradle.ktsui-tv/src/main/kotlin/com/miruplay/tv/ui/settings/AddSourceScreen.ktui-tv/src/main/kotlin/com/miruplay/tv/ui/settings/SettingsViewModel.ktweb-control-core/src/main/kotlin/com/miruplay/tv/webcontrol/NanoHttpWebControlServer.ktweb-control-core/src/main/kotlin/com/miruplay/tv/webcontrol/WebControlEndpointService.ktweb-control-core/src/main/kotlin/com/miruplay/tv/webcontrol/WebControlModels.ktweb-control-core/src/test/kotlin/com/miruplay/tv/webcontrol/WebControlSettingsRouteTest.ktweb-control/build.gradle.ktsweb-control/frontend/src/App.vueweb-control/frontend/src/styles.cssweb-control/src/main/kotlin/com/miruplay/tv/webcontrol/WebControlService.kt
| private fun AudioDspChannelTarget.matches(channel: Channel, layout: ChannelLayout): Boolean = when (this) { | ||
| AudioDspChannelTarget.ALL -> true | ||
| AudioDspChannelTarget.FRONT -> channel == Channel.L || channel == Channel.R || channel == Channel.C | ||
| AudioDspChannelTarget.CENTER_LFE -> channel == Channel.C || channel == Channel.LFE | ||
| AudioDspChannelTarget.SURROUND, AudioDspChannelTarget.SURROUND_5_1, AudioDspChannelTarget.SURROUND_7_1 -> | ||
| channel == Channel.LS || channel == Channel.RS || channel == Channel.LB || channel == Channel.RB | ||
| AudioDspChannelTarget.LEFT -> channel == Channel.L | ||
| AudioDspChannelTarget.RIGHT -> channel == Channel.R | ||
| AudioDspChannelTarget.CENTER -> channel == Channel.C | ||
| AudioDspChannelTarget.LFE -> channel == Channel.LFE | ||
| AudioDspChannelTarget.LEFT_SURROUND -> channel == Channel.LS || channel == Channel.LB | ||
| AudioDspChannelTarget.RIGHT_SURROUND -> channel == Channel.RS || channel == Channel.RB | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect ChannelLayout.kt to determine how 5.1 vs 7.1 layouts are represented,
# to confirm whether SURROUND_5_1/SURROUND_7_1 should be distinguished by layout.
fd ChannelLayout.kt
cat -n audio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/ChannelLayout.ktRepository: ModerRAS/MiruPlay
Length of output: 3258
🏁 Script executed:
#!/bin/bash
# Inspect the DSP plan compiler context for enum usage and how matches() is called.
ast-grep outline audio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/AudioDspPlanCompiler.kt --view expanded
sed -n '1,140p' audio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/AudioDspPlanCompiler.kt
rg -n "AudioDspChannelTarget|matches\\(|SURROUND_5_1|SURROUND_7_1" audio-dsp-core/src/main/kotlin/com/miruplay/tv/audio -SRepository: ModerRAS/MiruPlay
Length of output: 8306
🏁 Script executed:
#!/bin/bash
# Inspect the model declarations for AudioDspChannelTarget/audio DSP preset API.
rg -n "enum class AudioDspChannelTarget|data class AudioDspPreset|data class AudioDspChannelRule|class AudioDspChannelRule" . \
-g '*.kt' \
-g '*.java' \
-g '*.xsd' \
-g '*.xml' \
-g '*.kts'
fd AudioDsp*.kt audio-dsp-core audio-dsp-model . | sortRepository: ModerRAS/MiruPlay
Length of output: 555
🏁 Script executed:
#!/bin/bash
# Inspect the model file around AudioDspChannelTarget to capture explicit semantics/naming.
cat -n core/model/src/main/kotlin/com/miruplay/tv/model/AudioDspModels.kt | sed -n '45,75p'Repository: ModerRAS/MiruPlay
Length of output: 1249
🏁 Script executed:
#!/bin/bash
# Locate and inspect any persisted audio preset files or test fixtures that show whether SURROUND_5_1/SURROUND_7_1 are distinguished.
rg -n '"surround_5_1"|"surround_7_1"|surround_5_1|surround_7_1' \
-g '*.json' -g '*.kts' -g '*.kt' -g '*.xml' -g '*.yaml' -g '*.yml' .
fd -e json -e kt -e kts -e xml -e yaml -e yml . | rg -i 'preset|audio|dsp' | head -100Repository: ModerRAS/MiruPlay
Length of output: 2257
Distinguish SURROUND_5_1 from SURROUND_7_1 in matches().
CH defines SURROUND_5_1 as LS/RS channels and SURROUND_7_1 as LS/RS plus LB/RB. The compiler branch currently maps SURROUND, SURROUND_5_1, and SURROUND_7_1 to the same channel condition and ignores the passed layout, so 5.1 and 7.1 target rules produce the same routing. Match only the channels that belong to the resolved layout for each target.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@audio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/AudioDspPlanCompiler.kt`
around lines 79 - 91, The AudioDspChannelTarget.matches extension currently
treats SURROUND_5_1 and SURROUND_7_1 identically and ignores layout. Update this
branch to use the resolved layout: SURROUND_5_1 should match only LS/RS, while
SURROUND_7_1 should additionally match LB/RB; preserve the existing SURROUND
behavior and use the layout parameter to determine applicable channels.
| fun design(band: AudioDspBand, sampleRateHz: Int): BiquadCoefficients { | ||
| val normalized = band.normalized() | ||
| val omega = 2.0 * Math.PI * normalized.frequencyHz / sampleRateHz | ||
| val alpha = sin(omega) / (2.0 * normalized.q) | ||
| val cosW = cos(omega) | ||
| val gain = 10.0.pow(normalized.gainDb / 40.0) | ||
| val beta = 2.0 * sqrt(gain) * alpha |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Validate the sample rate against the band frequency.
Line 32 calculates coefficients when sampleRateHz is invalid or when frequencyHz is at or above Nyquist. For example, a persisted 24 kHz band on 32 kHz PCM targets an aliased frequency. Reject the plan or clamp the band below Nyquist before coefficient calculation. Add coverage for a low sample rate and a high-frequency band.
One safe rejection option
fun design(band: AudioDspBand, sampleRateHz: Int): BiquadCoefficients {
+ require(sampleRateHz > 0) { "sample rate must be positive" }
val normalized = band.normalized()
+ require(normalized.frequencyHz < sampleRateHz / 2.0) {
+ "band frequency must be below Nyquist"
+ }
val omega = 2.0 * Math.PI * normalized.frequencyHz / sampleRateHz📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fun design(band: AudioDspBand, sampleRateHz: Int): BiquadCoefficients { | |
| val normalized = band.normalized() | |
| val omega = 2.0 * Math.PI * normalized.frequencyHz / sampleRateHz | |
| val alpha = sin(omega) / (2.0 * normalized.q) | |
| val cosW = cos(omega) | |
| val gain = 10.0.pow(normalized.gainDb / 40.0) | |
| val beta = 2.0 * sqrt(gain) * alpha | |
| fun design(band: AudioDspBand, sampleRateHz: Int): BiquadCoefficients { | |
| require(sampleRateHz > 0) { "sample rate must be positive" } | |
| val normalized = band.normalized() | |
| require(normalized.frequencyHz < sampleRateHz / 2.0) { | |
| "band frequency must be below Nyquist" | |
| } | |
| val omega = 2.0 * Math.PI * normalized.frequencyHz / sampleRateHz | |
| val alpha = sin(omega) / (2.0 * normalized.q) | |
| val cosW = cos(omega) | |
| val gain = 10.0.pow(normalized.gainDb / 40.0) | |
| val beta = 2.0 * sqrt(gain) * alpha |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@audio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/BiquadDesigner.kt`
around lines 30 - 36, Update BiquadDesigner.design to validate sampleRateHz
before calculating omega: reject nonpositive sample rates and bands whose
normalized frequency is at or above the Nyquist limit (sampleRateHz / 2),
including persisted high-frequency bands. Preserve valid coefficient calculation
and add coverage for low sample rates and high-frequency bands.
| if (inputOrder == InputOrder.CANONICAL || channelCount < 6) return samples.copyOf() | ||
| val frames = samples.size / channelCount | ||
| if (frames * channelCount != samples.size) return samples.copyOf() | ||
| val order = when (inputOrder) { | ||
| InputOrder.AAC_5_1 -> intArrayOf(0, 1, 2, 5, 3, 4) | ||
| InputOrder.WAV_5_1 -> intArrayOf(0, 1, 2, 3, 4, 5) | ||
| else -> return samples.copyOf() | ||
| } | ||
| return FloatArray(samples.size) { index -> | ||
| val frame = index / channelCount | ||
| val outputChannel = index % channelCount | ||
| samples[frame * channelCount + order[outputChannel]] | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Correct the AAC mapping and handle incompatible channel counts.
Line 21 maps AAC 5.1 samples as if the first three samples were already L,R,C. AAC 5.1 order is C,L,R,LS,RS,LFE, so the canonical mapping must be 1,2,0,5,3,4. Also, Line 28 throws ArrayIndexOutOfBoundsException when AAC_5_1 or WAV_5_1 is passed with more than six channels. Return the unchanged samples, or reject the input order, when the mapping size does not equal channelCount.
Proposed fix
fun normalizeInterleaved(samples: FloatArray, inputOrder: InputOrder): FloatArray {
if (inputOrder == InputOrder.CANONICAL || channelCount < 6) return samples.copyOf()
val frames = samples.size / channelCount
if (frames * channelCount != samples.size) return samples.copyOf()
val order = when (inputOrder) {
- InputOrder.AAC_5_1 -> intArrayOf(0, 1, 2, 5, 3, 4)
+ InputOrder.AAC_5_1 -> intArrayOf(1, 2, 0, 5, 3, 4)
InputOrder.WAV_5_1 -> intArrayOf(0, 1, 2, 3, 4, 5)
else -> return samples.copyOf()
}
+ if (order.size != channelCount) return samples.copyOf()
return FloatArray(samples.size) { index ->📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (inputOrder == InputOrder.CANONICAL || channelCount < 6) return samples.copyOf() | |
| val frames = samples.size / channelCount | |
| if (frames * channelCount != samples.size) return samples.copyOf() | |
| val order = when (inputOrder) { | |
| InputOrder.AAC_5_1 -> intArrayOf(0, 1, 2, 5, 3, 4) | |
| InputOrder.WAV_5_1 -> intArrayOf(0, 1, 2, 3, 4, 5) | |
| else -> return samples.copyOf() | |
| } | |
| return FloatArray(samples.size) { index -> | |
| val frame = index / channelCount | |
| val outputChannel = index % channelCount | |
| samples[frame * channelCount + order[outputChannel]] | |
| } | |
| if (inputOrder == InputOrder.CANONICAL || channelCount < 6) return samples.copyOf() | |
| val frames = samples.size / channelCount | |
| if (frames * channelCount != samples.size) return samples.copyOf() | |
| val order = when (inputOrder) { | |
| InputOrder.AAC_5_1 -> intArrayOf(1, 2, 0, 5, 3, 4) | |
| InputOrder.WAV_5_1 -> intArrayOf(0, 1, 2, 3, 4, 5) | |
| else -> return samples.copyOf() | |
| } | |
| if (order.size != channelCount) return samples.copyOf() | |
| return FloatArray(samples.size) { index -> | |
| val frame = index / channelCount | |
| val outputChannel = index % channelCount | |
| samples[frame * channelCount + order[outputChannel]] | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@audio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/ChannelLayout.kt` around
lines 17 - 29, Update the channel remapping logic in ChannelLayout so
InputOrder.AAC_5_1 uses the canonical mapping 1,2,0,5,3,4. Before indexing with
the selected order in the remapping flow, return samples.copyOf() or otherwise
reject the input when the mapping length does not equal channelCount, preventing
invalid access for layouts with more than six channels.
| fun design(targetMagnitudeDb: FloatArray, sampleRateHz: Int, taps: Int): FloatArray { | ||
| require(taps > 1 && taps and (taps - 1) == 0) { "FIR taps must be a power of two" } | ||
| require(targetMagnitudeDb.isNotEmpty()) { "FIR target cannot be empty" } | ||
| val center = (taps - 1) / 2.0 | ||
| val half = taps / 2 | ||
| val spectrumReal = DoubleArray(taps) | ||
| val spectrumImag = DoubleArray(taps) | ||
| for (k in 0..half) { | ||
| val magnitude = interpolatedMagnitude(targetMagnitudeDb, k.toDouble() / half) | ||
| val phase = -2.0 * Math.PI * k * center / taps | ||
| spectrumReal[k] = magnitude * cos(phase) | ||
| spectrumImag[k] = magnitude * sin(phase) | ||
| if (k in 1 until half) { | ||
| val mirror = taps - k | ||
| spectrumReal[mirror] = spectrumReal[k] | ||
| spectrumImag[mirror] = -spectrumImag[k] | ||
| } | ||
| } | ||
| return FloatArray(taps) { n -> | ||
| var value = 0.0 | ||
| for (k in 0 until taps) { | ||
| val phase = 2.0 * Math.PI * k * n / taps | ||
| value += spectrumReal[k] * cos(phase) - spectrumImag[k] * sin(phase) | ||
| } | ||
| (value / taps).toFloat() | ||
| } | ||
| } |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift
Replace the O(n²) direct inverse DFT with an FFT-based implementation.
design() computes each of the taps output samples with an inner loop over all taps frequency bins (Lines 26-33). This is an O(taps²) direct inverse DFT. AudioDspPlanCompiler.compile() invokes this once per channel when phaseMode == LINEAR, so the cost multiplies by channel count.
For AudioDspFirQuality.HIGH (4096 taps, defined in AudioDspModels.kt) on a 7.1 stream (8 channels), this is roughly 4096 × 4096 × 8 ≈ 134 million inner-loop iterations, each with two trigonometric calls. This compile step runs synchronously as part of onConfigure() on the audio-processor configuration path (per the DspAudioProcessor context and DspAudioProcessorTest.kt), so it can stall playback startup or a preset switch on Android TV hardware.
Since taps is already validated as a power of two (Line 9), use a radix-2 FFT/IFFT instead of the direct summation to reduce this to O(taps·log2(taps)).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@audio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/LinearPhaseFirDesigner.kt`
around lines 8 - 34, Replace the direct inverse-DFT summation in
LinearPhaseFirDesigner.design with a radix-2 FFT-based IFFT, reusing the
existing spectrumReal and spectrumImag arrays and power-of-two taps validation.
Preserve the current magnitude interpolation, phase construction, Hermitian
mirroring, normalization by taps, and FloatArray output ordering while reducing
reconstruction complexity to O(taps·log2(taps)).
| private val ceiling = 10.0.pow(ceilingDb.toDouble() / 20.0).toFloat().coerceIn(0.01f, 1f) | ||
| private var gain = 1f | ||
|
|
||
| fun process(interleaved: FloatArray, channels: Int): FloatArray { | ||
| require(channels > 0) { "channels must be positive" } | ||
| if (interleaved.isEmpty()) return interleaved.copyOf() | ||
| val output = FloatArray(interleaved.size) | ||
| val frames = interleaved.size / channels | ||
| val releaseCoefficient = 1f - exp( | ||
| -1f / (releaseMs.coerceAtLeast(1f) * 0.001f * sampleRateHz.coerceAtLeast(1)), | ||
| ) |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win
Cache releaseCoefficient instead of recomputing it on every process() call.
releaseCoefficient depends only on releaseMs and sampleRateHz, which never change after construction, but it is recomputed with an exp() call every time process() runs. StreamingDspProcessor calls limiter.process() once per single audio frame (see StreamingDspProcessor.kt lines 37-51), so this exp() call and its surrounding array allocation repeat on every audio sample-frame during playback. Move the computation to the class body so it runs once per LinkedLimiter instance.
⚡ Proposed fix to cache the release coefficient
class LinkedLimiter(
ceilingDb: Float = -1f,
private val releaseMs: Float = 100f,
private val sampleRateHz: Int = 48_000,
) {
private val ceiling = 10.0.pow(ceilingDb.toDouble() / 20.0).toFloat().coerceIn(0.01f, 1f)
private var gain = 1f
+ private val releaseCoefficient = 1f - exp(
+ -1f / (releaseMs.coerceAtLeast(1f) * 0.001f * sampleRateHz.coerceAtLeast(1)),
+ )
fun process(interleaved: FloatArray, channels: Int): FloatArray {
require(channels > 0) { "channels must be positive" }
if (interleaved.isEmpty()) return interleaved.copyOf()
val output = FloatArray(interleaved.size)
val frames = interleaved.size / channels
- val releaseCoefficient = 1f - exp(
- -1f / (releaseMs.coerceAtLeast(1f) * 0.001f * sampleRateHz.coerceAtLeast(1)),
- )
for (frame in 0 until frames) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private val ceiling = 10.0.pow(ceilingDb.toDouble() / 20.0).toFloat().coerceIn(0.01f, 1f) | |
| private var gain = 1f | |
| fun process(interleaved: FloatArray, channels: Int): FloatArray { | |
| require(channels > 0) { "channels must be positive" } | |
| if (interleaved.isEmpty()) return interleaved.copyOf() | |
| val output = FloatArray(interleaved.size) | |
| val frames = interleaved.size / channels | |
| val releaseCoefficient = 1f - exp( | |
| -1f / (releaseMs.coerceAtLeast(1f) * 0.001f * sampleRateHz.coerceAtLeast(1)), | |
| ) | |
| private val ceiling = 10.0.pow(ceilingDb.toDouble() / 20.0).toFloat().coerceIn(0.01f, 1f) | |
| private var gain = 1f | |
| private val releaseCoefficient = 1f - exp( | |
| -1f / (releaseMs.coerceAtLeast(1f) * 0.001f * sampleRateHz.coerceAtLeast(1)), | |
| ) | |
| fun process(interleaved: FloatArray, channels: Int): FloatArray { | |
| require(channels > 0) { "channels must be positive" } | |
| if (interleaved.isEmpty()) return interleaved.copyOf() | |
| val output = FloatArray(interleaved.size) | |
| val frames = interleaved.size / channels |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@audio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/LinkedLimiter.kt` around
lines 13 - 23, Move the releaseCoefficient calculation out of
LinkedLimiter.process and cache it as a class-level immutable property
initialized from releaseMs and sampleRateHz. Remove the per-call exp()
computation while preserving the existing clamping and coefficient behavior.
| override fun queueInput(inputBuffer: ByteBuffer) { | ||
| val active = processor ?: run { | ||
| inputBuffer.position(inputBuffer.limit()) | ||
| return | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
A null processor discards the input instead of passing it through.
Line 51 advances the input buffer to its limit and returns without writing any output. The frames are dropped, and the listener hears silence.
This path is reachable. isActive on line 47 reads the live runtimeConfig.config.enabled flag, but processor is only created in onConfigure and onFlush. If the config is enabled after onConfigure returned early on line 24, isActive becomes true while processor stays null. The sink then queues input into a processor that throws it away.
Copy the input to the output buffer when no processor exists.
🐛 Proposed fix
val active = processor ?: run {
- inputBuffer.position(inputBuffer.limit())
+ val remaining = inputBuffer.remaining()
+ if (remaining > 0) {
+ replaceOutputBuffer(remaining).put(inputBuffer).flip()
+ }
return
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| override fun queueInput(inputBuffer: ByteBuffer) { | |
| val active = processor ?: run { | |
| inputBuffer.position(inputBuffer.limit()) | |
| return | |
| } | |
| override fun queueInput(inputBuffer: ByteBuffer) { | |
| val active = processor ?: run { | |
| val remaining = inputBuffer.remaining() | |
| if (remaining > 0) { | |
| replaceOutputBuffer(remaining).put(inputBuffer).flip() | |
| } | |
| return | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@player-core/src/main/kotlin/com/miruplay/tv/player/DspAudioProcessor.kt`
around lines 49 - 53, Update queueInput in DspAudioProcessor so the
processor-null path copies the input buffer to the output buffer instead of
advancing the input to its limit and discarding frames. Preserve normal
processing through the active processor when processor is non-null.
| val policy = AudioDspOutputPolicy.forConfig(runtimeConfig.config) | ||
| return DefaultAudioSink.Builder(context) | ||
| .setEnableFloatOutput(enableFloatOutput || policy.forcePcm) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Find offload, passthrough, and tunneling configuration in the player layer.
rg -nP -C 6 '(setEnableAudioOffload|setAudioOffloadPreferences|setEnableTunneling|AudioOffloadPreferences|TrackSelectionParameters)' player-core/src --type=kotlin
# Description: Check how the ExoPlayer instance is created and released per session.
fd ExoPlaybackController.kt --exec ast-grep outline {} --items allRepository: ModerRAS/MiruPlay
Length of output: 6749
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "## DspRenderersFactory.kt"
fd DspRenderersFactory.kt --exec sh -c 'echo "--- $1"; cat -n "$1"' _ {}
echo "## AudioDspOutputPolicy references"
rg -n -C 4 'class AudioDspOutputPolicy|data class AudioDspOutputPolicy|AudioDspOutputPolicy|forConfig|forcePcm|allowOffload|allowPassthrough|allowTunneling' player-core/src --type=kotlin || true
echo "## Playback config / RuntimeConfig"
rg -n -C 3 'data class .*Runtime|class .*Runtime|runtimeConfig|Dsp|AudioDsp|audio.*dsp|preamp|downmix|eq|equalizer|DspAudioProcessor|AudioSink' model player-core/src --type=kotlin || true
echo "## ExoPlayer construction and release relevant snippets"
sed -n '1,240p' player-core/src/main/kotlin/com/miruplay/tv/player/ExoPlaybackController.kt
printf '\n--- release/player lifecycle refs ---\n'
rg -n -C 5 'preparePlayerForPlayback|standardExoPlayer|experimentalExoPlayer|release\(\)|`@Singleton`|exoplayer|ExoPlayer\.' player-core/src/main/kotlin/com/miruplay/tv/player/ExoPlaybackController.kt --type=kotlinRepository: ModerRAS/MiruPlay
Length of output: 50374
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "## AudioDsp model files"
fd -a 'AudioDsp.*kotlin|AudioDsp.*kt$' . | sed 's#^\./##'
echo "## Relevant model declarations"
for f in $(fd -a 'AudioDsp.*kotlin|AudioDsp.*kt$' . | sed 's#^\./##'); do
echo "--- $f"
grep -n -C 5 'data class \|^enum class \|^val \|^private data class \|^data class AudioDsp|preamp|downmix|eq|preset' "$f" || true
done
echo "## DspAudioProcessor implementation"
fd 'DspAudioProcessor.kt' --exec sh -c 'echo "--- $1"; wc -l "$1"; sed -n "1,220p" "$1"' _ {}
echo "## DefaultRenderersFactory buildAudioSink override context"
rg -n -C 8 'class DspRenderersFactory|fun buildAudioSink|override fun buildAudioSink|setEnableFloatOutput|setAudioOffload|offload|passthrough|tunnel' player-core/src/main/kotlin/com/miruplay/tv/player/DspRenderersFactory.kt player-core/src/main/kotlin/com/miruplay/tv/player/ExperimentalRenderersFactory.kt player-core/src/main/kotlin/com/miruplay/tv/player --type=kotlin || true
echo "## DiModule / ExoPlayer provider snippets"
rg -n -C 4 '`@Singleton`|provide.*ExoPlayer|ExoPlayer|DiModule|AudioDspRuntimeConfig|runtimeConfig' player-core/src/main/kotlin --type=kotlin || true
echo "## Player lifecycle/release snippets"
rg -n -C 4 'override fun release|`@Inject` lateinit |private val |ExoPlayer|standardExoPlayer|experimentalExoPlayer|rebuild|destroy|stopInactivePlayers|preparePlayerForPlayback|refreshRuntimeConfig' player-core/src/main/kotlin/com/miruplay/tv/player/ExoPlaybackController.kt --type=kotlin || trueRepository: ModerRAS/MiruPlay
Length of output: 50374
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "## RuntimeConfig model"
fd -a '.*RuntimeConfig.*kt$' . --exclude .git | sed 's#^\./##'
echo "## DiModule full"
cat -n player-core/src/main/kotlin/com/miruplay/tv/player/DiModule.kt
echo "## PlayerFactoryImpl focused"
sed -n '1,140p' player-core/src/main/kotlin/com/miruplay/tv/player/PlayerFactoryImpl.kt | cat -n
echo "## PlayerModule focused ExoPlayer build"
sed -n '20,70p' player-core/src/main/kotlin/com/miruplay/tv/player/DiModule.kt | cat -n
echo "## Search buildAudioSink call contexts"
rg -n -C 12 'buildAudioSink\(' player-core/src --type=kotlin | sed -n '1,220p'
echo "## Search offload/offloadPreferences/passthrough/tunnel in player-core source"
rg -n -C 5 'AudioOffloadPreferences|AudioSink\.AudioCapabilities|AudioCapabilities|enableAudioOffload|enablePassthrough|enableTunneling|setEnableAudioOffload|setAudioOffloadPreferences|setEnablePassthrough|setEnableTunneling|setAllow*|passthrough|tunnel|offload' player-core/src/main/kotlin --type=kotlin | sed -n '1,260p' || trueRepository: ModerRAS/MiruPlay
Length of output: 18158
🌐 Web query:
ExoPlayer AudioSink AudioCapabilities AudioOffloadPreferences enableAudioOffload setEnableFloatOutput passthrough tunneling
💡 Result:
In Media3/ExoPlayer, these components manage advanced audio playback features like offload, tunneling, and high-resolution output [1][2][3]. AudioSink and AudioCapabilities AudioSink is the primary interface for consuming audio data [4][5]. DefaultAudioSink is the standard implementation [6]. AudioCapabilities represents the audio formats and configurations a device supports, including passthrough playback (e.g., bitstreaming compressed audio like Dolby or DTS directly to a sink) [2]. Audio Offload Audio offload allows the device's DSP to handle audio decoding, saving power. - Configuration: You can configure offload behavior using DefaultAudioSink.Builder or TrackSelectionParameters.AudioOffloadPreferences [7][3]. - Preferences: TrackSelectionParameters.AudioOffloadPreferences lets you specify whether offload is disabled, enabled (when compatible), or required [7]. - Sink Methods: The AudioSink interface provides setOffloadMode to control offload behavior, such as preferring gapless offload [1]. - Caveats: Most Android devices support only one offload AudioTrack at a time, and it may be invalidated. Audio processing (e.g., speed adjustment) is typically unavailable when offload is active [3]. Tunneling Tunneling (often referred to as Audio Tunneling or Compressed Audio Passthrough via Tunneling) allows encoded audio to be passed through to the hardware decoder while maintaining synchronization with video. - Methods: The AudioSink interface supports methods like enableTunnelingV21 and disableTunneling to manage this state [4][5]. Newer implementations may use AudioOutputProvider to configure tunneling (e.g., setEnableTunneling) [8][9]. Float Output ExoPlayer supports 32-bit float audio output for high-fidelity playback. - Configuration: When using DefaultAudioSink.Builder, you can call setEnableFloatOutput(boolean) to enable it [3]. If enabled, the sink will use 32-bit float output if the input is float or high-resolution integer PCM. Note that audio processing features (like speed adjustment) are generally disabled when float output is active [3]. Summary of Key Configuration Methods - Offload: TrackSelectionParameters.AudioOffloadPreferences [7], AudioSink.setOffloadMode [1]. - Tunneling: AudioSink.enableTunnelingV21 [4][5], AudioOutputProvider.FormatConfig.Builder.setEnableTunneling [8]. - Float Output: DefaultAudioSink.Builder.setEnableFloatOutput [3].
Citations:
- 1: https://developer.android.com/reference/androidx/media3/exoplayer/audio/AudioSink
- 2: https://developer.android.com/reference/androidx/media3/exoplayer/audio/AudioCapabilities
- 3: https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/audio/DefaultAudioSink.Builder.html
- 4: https://developer.android.com/reference/kotlin/androidx/media3/exoplayer/audio/AudioSink
- 5: https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/audio/AudioSink.html
- 6: https://developer.android.com/reference/androidx/media3/exoplayer/audio/DefaultAudioSink
- 7: https://developer.android.com/reference/androidx/media3/common/TrackSelectionParameters.AudioOffloadPreferences
- 8: https://developer.android.com/reference/kotlin/androidx/media3/exoplayer/audio/AudioOutputProvider.FormatConfig.Builder
- 9: https://developer.android.com/reference/androidx/media3/exoplayer/audio/AudioOutputProvider.OutputConfig.Builder
Apply DSP output policy flags to the ExoPlayer audio path.
AudioDspOutputPolicy sets allowOffload, allowPassthrough, and allowTunneling, but DspRenderersFactory.buildAudioSink ignores them and only reads forcePcm. setEnableFloatOutput only changes PCM sample-format support and does not force PCM decoding or disable bitstream offload/passthrough. Apply allowOffload on the audio capabilities/offload settings, constrain the track selector for allowPassthrough, and gate allowTunneling at renderer/audio output configuration so DSP-enabled bitstream tracks do not bypass DspAudioProcessor.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@player-core/src/main/kotlin/com/miruplay/tv/player/DspRenderersFactory.kt`
around lines 17 - 19, Update DspRenderersFactory.buildAudioSink to apply the
full AudioDspOutputPolicy, not just forcePcm: propagate allowOffload through the
audio capabilities/offload configuration, constrain track selection so
allowPassthrough is respected, and gate renderer/audio-output tunneling with
allowTunneling. Ensure DSP-enabled bitstream tracks cannot bypass
DspAudioProcessor while preserving normal policy-enabled offload, passthrough,
and tunneling behavior.
| return DefaultAudioSink.Builder(context) | ||
| .setEnableFloatOutput(enableFloatOutput || policy.forcePcm) | ||
| .setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams) | ||
| .setAudioProcessors(arrayOf(DspAudioProcessor(runtimeConfig))) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Find the declared Media3 version.
rg -nP -C 2 'media3' gradle/libs.versions.tomlRepository: ModerRAS/MiruPlay
Length of output: 1223
🌐 Web query:
Media3 DefaultAudioSink Builder setAudioProcessors DefaultAudioProcessorChain SonicAudioProcessor default
💡 Result:
In Android Media3, DefaultAudioSink.Builder provides methods to configure how audio is processed before playback [1][2]. When configuring audio processors, you can use setAudioProcessors(AudioProcessor[]) to pass an array of processors [1][2]. This method is a convenience wrapper equivalent to calling setAudioProcessorChain(new DefaultAudioProcessorChain(audioProcessors)) [1][2]. The DefaultAudioProcessorChain is the default implementation of the AudioProcessorChain interface [3][4]. It automatically manages a sequence that includes any user-defined audio processors you provide, followed by a SilenceSkippingAudioProcessor and a SonicAudioProcessor [3][5]. Key points regarding the default behavior: - If you use setAudioProcessors, your custom processors are applied first, followed by the silence skipping and Sonic processors [3][5]. - The SonicAudioProcessor is responsible for modifying audio speed, pitch, and sample rate when playback parameters require it [6]. - If you do not explicitly set audio processors, the default configuration effectively uses an empty array of user-defined processors, but the chain still includes the silence skipping and Sonic processors to handle standard playback speed adjustments [2][3]. For custom control, you can implement your own AudioProcessorChain or instantiate DefaultAudioProcessorChain with custom instances of SilenceSkippingAudioProcessor and SonicAudioProcessor if needed [3][5].
Citations:
- 1: https://developer.android.com/reference/androidx/media3/exoplayer/audio/DefaultAudioSink.Builder
- 2: https://developer.android.com/reference/kotlin/androidx/media3/exoplayer/audio/DefaultAudioSink.Builder
- 3: https://developer.android.com/reference/androidx/media3/exoplayer/audio/DefaultAudioSink.DefaultAudioProcessorChain
- 4: https://developer.android.com/reference/kotlin/androidx/media3/exoplayer/audio/DefaultAudioSink
- 5: https://developer.android.com/reference/kotlin/androidx/media3/exoplayer/audio/DefaultAudioSink.DefaultAudioProcessorChain
- 6: https://developer.android.com/reference/androidx/media3/common/audio/SonicAudioProcessor
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate and inspect the relevant Media3 audio sink construction site.
wc -l player-core/src/main/kotlin/com/miruplay/tv/player/DspRenderersFactory.kt
cat -n player-core/src/main/kotlin/com/miruplay/tv/player/DspRenderersFactory.kt
# Search for related audio processor usage.
rg -n "DefaultAudioSink|DefaultAudioProcessorChain|DefaultAudioProcessorChain|SonicAudioProcessor|setAudioProcessors|AudioProcessor" player-core -g '*.kt'Repository: ModerRAS/MiruPlay
Length of output: 4318
🌐 Web query:
Android Media3 1.8.0 DefaultAudioSink.Builder setAudioProcessors setAudioProcessorChain default processors Sonic SilenceSkipping
💡 Result:
In Android Media3 1.8.0, you can configure audio processing in DefaultAudioSink using the DefaultAudioSink.Builder class [1][2]. Audio processing is managed through the AudioProcessorChain interface [3][4]. By default, if no custom configuration is provided, the sink uses the DefaultAudioProcessorChain [5][6]. Key configuration methods in DefaultAudioSink.Builder: - setAudioProcessors(AudioProcessor[]): This is a convenience method that allows you to provide an array of user-defined AudioProcessors [1][2]. It is equivalent to creating a DefaultAudioProcessorChain with your custom processors and setting it via setAudioProcessorChain [1][5]. - setAudioProcessorChain(AudioProcessorChain): This method allows for full control over the processor chain [1][2]. The DefaultAudioProcessorChain, which is used by default, applies a chain of user-defined audio processors (if provided) followed by SilenceSkippingAudioProcessor and SonicAudioProcessor [5][6]. Customization details: - Default Processors: When using the default chain, the user-defined processors are applied before the SilenceSkippingAudioProcessor (which handles silence trimming) and the SonicAudioProcessor (which handles speed and pitch adjustments) [5][3]. - Providing Custom Chains: If you need to customize the configuration of the default processors (e.g., changing silence detection thresholds), you can instantiate a new DefaultAudioProcessorChain manually, passing your desired user-defined processors along with specifically configured instances of SilenceSkippingAudioProcessor and SonicAudioProcessor, and then pass this chain to setAudioProcessorChain [5][7]. - Processing chains are supported only for PCM playback and are not used during passthrough or offload modes [1][2]. Note: The SilenceSkippingAudioProcessor allows for configuring parameters like silence threshold levels and retention ratios, while the SonicAudioProcessor handles audio speed and pitch modifications [8][9].
Citations:
- 1: https://developer.android.com/reference/kotlin/androidx/media3/exoplayer/audio/DefaultAudioSink.Builder
- 2: https://developer.android.com/reference/androidx/media3/exoplayer/audio/DefaultAudioSink.Builder
- 3: https://developer.android.com/reference/kotlin/androidx/media3/common/audio/AudioProcessorChain
- 4: https://developer.android.com/reference/androidx/media3/exoplayer/audio/DefaultAudioSink.AudioProcessorChain
- 5: https://developer.android.com/reference/kotlin/androidx/media3/exoplayer/audio/DefaultAudioSink.DefaultAudioProcessorChain
- 6: https://developer.android.com/reference/androidx/media3/exoplayer/audio/DefaultAudioSink
- 7: How to customize the configuration of
SilenceSkippingAudioProcessor? androidx/media#681 - 8: https://developer.android.com/reference/androidx/media3/exoplayer/audio/SilenceSkippingAudioProcessor
- 9: https://developer.android.com/reference/androidx/media3/common/audio/SonicAudioProcessor
Preserve the default audio processors when installing the DSP processor.
setAudioProcessors(arrayOf(DspAudioProcessor(...))) creates a DefaultAudioProcessorChain with only DspAudioProcessor, which removes the default SilenceSkippingAudioProcessor and SonicAudioProcessor. Build a DefaultAudioProcessorChain that keeps the default processors and adds DspAudioProcessor so playback speed/pitch control still falls back to the audio path when track-specific playback params are unavailable.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@player-core/src/main/kotlin/com/miruplay/tv/player/DspRenderersFactory.kt` at
line 21, Update the audio processor configuration in DspRenderersFactory to
construct a DefaultAudioProcessorChain containing the existing default
SilenceSkippingAudioProcessor and SonicAudioProcessor plus
DspAudioProcessor(runtimeConfig). Pass that complete chain to the renderer so
playback speed and pitch fallback behavior remains unchanged.
| val policy = AudioDspOutputPolicy.forConfig(audioDspRuntimeConfig.config) | ||
| return DefaultAudioSink.Builder(context) | ||
| .setEnableFloatOutput(enableFloatOutput || policy.forcePcm) | ||
| .setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams) | ||
| .setAudioProcessors(arrayOf(DspAudioProcessor(audioDspRuntimeConfig))) | ||
| .build() |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ast-grep outline player-core/src/main/kotlin/com/miruplay/tv/player/ExperimentalRenderersFactory.kt --items all
ast-grep outline player-core/src/main/kotlin/com/miruplay/tv/player/DspRenderersFactory.kt --items all
rg -n -C 5 \
'AudioDspOutputPolicy|forcePcm|setOffloadMode|setEnableAudioOffload|AudioCapabilities|DefaultAudioSink' \
player-coreRepository: ModerRAS/MiruPlay
Length of output: 12819
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect dependency declarations and relevant source/test files without running repository code.
git ls-files | rg '(^|/)(build\.gradle|build\.gradle\.kts|libs\.versions\.toml|AndroidManifest\.xml|DspRenderersFactory\.kt|ExperimentalRenderersFactory\.kt|AudioDspOutputPolicy\.kt|AudioDspOutputPolicyTest\.kt)$' || true
for f in player-core/build.gradle.kts build.gradle.kts settings.gradle.kts; do
if [ -f "$f" ]; then
echo "===== $f ====="
sed -n '1,220p' "$f"
fi
done
python3 - <<'PY'
from pathlib import Path
for p in [
Path('player-core/src/main/kotlin/com/miruplay/tv/player/ExperimentalRenderersFactory.kt'),
Path('player-core/src/main/kotlin/com/miruplay/tv/player/DspRenderersFactory.kt'),
Path('player-core/src/main/kotlin/com/miruplay/tv/player/DspAudioProcessor.kt'),
Path('player-core/src/test/kotlin/com/miruplay/tv/player/AudioDspOutputPolicyTest.kt'),
]:
print(f'===== {p} =====')
text = p.read_text()
for needle in ['setOffloadMode', 'setEnableAudioOffload', 'setAudioCapabilities', 'forcePcm', 'allowOffload', 'allowPassthrough', 'allowTunneling', 'AudioCapabilities', 'DefaultAudioSink.Builder']:
idx = text.find(needle)
print(f'{needle} present: {idx >= 0}')
PYRepository: ModerRAS/MiruPlay
Length of output: 7131
🌐 Web query:
AndroidX Media3 DefaultAudioSink.Builder setOffloadMode setEnableAudioOffload encode output passthrough offload
💡 Result:
In AndroidX Media3, audio offload refers to a playback technique where audio processing is moved from the application processor to a dedicated low-power digital signal processor (DSP). This reduces power consumption during playback. Configuration of audio offload is handled through several components in Media3: 1. Setting Offload Mode: The DefaultAudioSink provides a setOffloadMode method that allows you to configure whether offload should be attempted [1][2]. This method requires Android API level 29 or higher [1][2]. The available modes defined in AudioSink.OffloadMode are [3]: - OFFLOAD_MODE_DISABLED: Never use offload mode [1]. - OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED: Prefer offload, but disable it if the track is gapless and the device does not support gapless offload [1]. - OFFLOAD_MODE_ENABLED_GAPLESS_NOT_REQUIRED: Prefer offload even if it might cause silence gaps between tracks [1]. 2. Offload Preferences: While DefaultAudioSink handles the technical sink configuration, high-level preferences for offload are managed via TrackSelectionParameters.AudioOffloadPreferences [4]. This allows the track selector to decide whether to prioritize offload-compatible tracks [4]. You can set preferences such as: - AUDIO_OFFLOAD_MODE_DISABLED: Disables offload [4]. - AUDIO_OFFLOAD_MODE_ENABLED: Enables offload if renderer and track capabilities are compatible [4]. - AUDIO_OFFLOAD_MODE_REQUIRED: Only selects tracks if an offload-compatible playback scenario is available [4]. 3. Passthrough and Offload: In the context of Media3, "passthrough" typically refers to tunneling or passing encoded formats (like AC3/EAC3) directly to the hardware. While offload and passthrough both aim to offload tasks to the DSP, they are distinct mechanisms. Audio offload in Media3 specifically relates to the power-saving offload path, and its support depends on the platform and specific audio format capabilities [5][6]. You can provide custom logic for determining offload support by using the AudioOffloadSupportProvider interface in the DefaultAudioSink.Builder [5][6]. Note that setAudioOffloadSupportProvider is marked as needing careful usage and should not be combined with certain other custom output providers [5][6]. For detailed tracking of offload events, you can use the setExperimentalAudioOffloadListener method in the DefaultAudioSink.Builder [5].
Citations:
- 1: https://developer.android.com/reference/androidx/media3/exoplayer/audio/AudioSink
- 2: https://developer.android.com/reference/androidx/media3/exoplayer/audio/DefaultAudioSink
- 3: https://developer.android.google.cn/reference/androidx/media3/exoplayer/audio/AudioSink.OffloadMode
- 4: https://developer.android.com/reference/androidx/media3/common/TrackSelectionParameters.AudioOffloadPreferences
- 5: https://developer.android.com/reference/kotlin/androidx/media3/exoplayer/audio/DefaultAudioSink.Builder
- 6: https://developer.android.com/reference/androidx/media3/exoplayer/audio/DefaultAudioSink.Builder
🌐 Web query:
AndroidX Media3 1.8.0 DefaultAudioSink setAudioCapabilities AudioCapabilities.Builder setPassthroughSupport
💡 Result:
In AndroidX Media3 (including version 1.8.0), there is no method named setPassthroughSupport within AudioCapabilities.Builder. Audio capabilities in Media3 are primarily determined dynamically by the framework based on the Android Context. The relevant API for interacting with these capabilities is the AudioCapabilities class [1][2], which provides methods to query support for passthrough playback, such as: * isPassthroughPlaybackSupported(Format, AudioAttributes) [1][3][2] * getEncodingAndChannelConfigForPassthrough(Format, AudioAttributes) [1][2] Regarding DefaultAudioSink.Builder.setAudioCapabilities(AudioCapabilities), this method is deprecated [4][5]. It is only intended for use in the absence of an Android Context [4][6]. When a Context is provided to the builder, the system automatically handles audio capability detection. If you do not provide a Context and do not set custom AudioCapabilities, the system assumes default capabilities with no encoded audio passthrough support [4]. If you need to customize or force specific passthrough behaviors—for example, if the system incorrectly reports a lack of support—the recommended approach is to provide a custom implementation of DefaultAudioSink and override its getFormatSupport(Format) method to explicitly return support for desired formats [6].
Citations:
- 1: https://developer.android.com/reference/kotlin/androidx/media3/exoplayer/audio/AudioCapabilities
- 2: https://developer.android.com/reference/androidx/media3/exoplayer/audio/AudioCapabilities
- 3: https://github.com/androidx/media/blob/release/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilities.java
- 4: https://developer.android.com/reference/androidx/media3/exoplayer/audio/DefaultAudioSink.Builder
- 5: https://developer.android.com/reference/kotlin/androidx/media3/exoplayer/audio/DefaultAudioSink.Builder
- 6: DefaultAudioSink resets audioCapabilities on error even when context is null. androidx/media#2168
🌐 Web query:
AndroidX Media3 version declaration libs.androidx.media3.exoplayer gradle libs.versions.toml
💡 Result:
To declare AndroidX Media3 dependencies using a Gradle version catalog (libs.versions.toml), you should define the version in the [versions] block and the library in the [libraries] block [1][2]. All AndroidX Media3 modules used in a project must share the same version [3][4]. As of August 3, 2026, the stable version of AndroidX Media3 is 1.10.1 [5]. Below is the standard configuration for your gradle/libs.versions.toml file: [versions] androidxMedia3 = "1.10.1" [libraries] androidx-media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "androidxMedia3" } androidx-media3-exoplayer-dash = { group = "androidx.media3", name = "media3-exoplayer-dash", version.ref = "androidxMedia3" } androidx-media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "androidxMedia3" } Once defined, you can add these to your module-level build.gradle.kts file: dependencies { implementation(libs.androidx.media3.exoplayer) implementation(libs.androidx.media3.exoplayer.dash) implementation(libs.androidx.media3.ui) } You can also create a bundle in libs.versions.toml to group these dependencies for easier implementation [1][6]: [bundles] media3 = ["androidx-media3-exoplayer", "androidx-media3-exoplayer-dash", "androidx-media3-ui"] Then in your build.gradle.kts: dependencies { implementation(libs.bundles.media3) } Always check the official Android Developers release notes to ensure you are using the latest stable version [5][3].
Citations:
- 1: https://stackoverflow.com/questions/78250344/media3-dependencies-for-build-gradle-kts-for-android-studio-2023-2-1-patch-2
- 2: https://developer.android.com/build/migrate-to-catalogs
- 3: https://developer.android.com/media/media3/exoplayer/hello-world
- 4: https://github.com/androidx/media?tab=readme-ov-file
- 5: https://developer.android.com/jetpack/androidx/releases/media3
- 6: https://github.com/mollyim/mollyim-android/blob/refs/heads/main/gradle/libs.versions.toml
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '===== gradle/libs.versions.toml ====='
cat -n gradle/libs.versions.toml
echo '===== all AudioOffload/OffloadMode/Processors references in repository ====='
rg -n -C 4 'Offload|offload|AudioDspOutputPolicy|DspAudioProcessor|AudioProcessor|passthrough|Passthrough' .
echo '===== all DspRenderersFactory/ExperimentalRenderersFactory usage references ====='
rg -n -C 3 'DspRenderersFactory|ExperimentalRenderersFactory|audioDspRuntimeConfig|EnableDSP|DAP|dsp' .Repository: ModerRAS/MiruPlay
Length of output: 50373
🌐 Web query:
Media3 1.5.0 1.6.0 1.7.0 1.8.0 DefaultAudioSink.Builder setAudioOffloadSupportProvider offload passthrough
💡 Result:
In Media3, the DefaultAudioSink.Builder method setAudioOffloadSupportProvider is used to define how the sink determines offload support capabilities for a given audio format and attributes [1][2]. By default, if this provider is not explicitly set, the DefaultAudioSink uses an instance of DefaultAudioOffloadSupportProvider [1][3]. Key technical details regarding this API and recent changes include: 1. Deprecation and Evolution: The use of setAudioOffloadSupportProvider directly on the DefaultAudioSink.Builder is being superseded. It is now recommended to use setAudioOutputProvider instead [2]. You can then customize the audio output by configuring the AudioTrackAudioOutputProvider.Builder with a specific AudioOffloadSupportProvider [2]. 2. Audio Offload vs. Passthrough: The DefaultAudioSink handles different output modes, specifically: - OUTPUT_MODE_OFFLOAD: The audio sink plays encoded audio in offload mode [4]. - OUTPUT_MODE_PASSTHROUGH: The audio sink plays encoded audio in passthrough mode [4]. - OUTPUT_MODE_PCM: The audio sink plays PCM audio [4]. The AudioOffloadSupportProvider specifically manages the offload capabilities (e.g., format support, gapless, and variable rate support) [5][6], whereas passthrough and offload are distinct modes of the sink [4]. 3. Versioning Context: These architectural changes, such as the introduction of the AudioOutputProvider interface, were integrated into the Media3 library releases between 1.5.0 and 1.8.0 [7]. Specifically, recent versions have focused on refining audio output paths and improving the flexibility of how audio sinks handle various hardware-accelerated modes [8][7]. If you are currently implementing or migrating code, prioritize the use of AudioOutputProvider and the associated builder classes to ensure compatibility with the evolving Media3 API structure [2][7].
Citations:
- 1: https://developer.android.com/reference/kotlin/androidx/media3/exoplayer/audio/DefaultAudioSink.Builder
- 2: https://developer.android.com/reference/androidx/media3/exoplayer/audio/DefaultAudioSink.Builder
- 3: https://developer.android.com/reference/androidx/media3/exoplayer/audio/DefaultAudioSink
- 4: https://developer.android.com/reference/kotlin/androidx/media3/exoplayer/audio/DefaultAudioSink
- 5: https://developer.android.com/reference/kotlin/androidx/media3/exoplayer/audio/DefaultAudioSink.AudioOffloadSupportProvider
- 6: https://github.com/androidx/media/blob/release/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioOffloadSupportProvider.java
- 7: https://github.com/androidx/media/blob/release/RELEASENOTES.md
- 8: https://github.com/androidx/media/releases/tag/1.8.0
Disable encoded output paths when DSP is enabled.
Lines 30-35 only enable float output; they do not restrict DefaultAudioSink to PCM or disable offload and passthrough. Use AudioDspOutputPolicy for the audio output/offload capability policy and add a DSP-enabled encoded-format negotiation test.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@player-core/src/main/kotlin/com/miruplay/tv/player/ExperimentalRenderersFactory.kt`
around lines 30 - 35, Update the DefaultAudioSink configuration in
ExperimentalRenderersFactory so DSP-enabled playback is restricted to PCM by
applying AudioDspOutputPolicy to encoded output, offload, and passthrough
capabilities rather than only forcing float output. Add a negotiation test
covering DSP-enabled encoded formats and verify they are rejected while PCM
remains supported.
| override suspend fun previewAudioDsp(request: AudioDspPreviewRequest): AudioDspPreviewDto = runOnIo { | ||
| val frequencies = request.frequenciesHz | ||
| .filter { it.isFinite() && it in 10f..24_000f } | ||
| .take(512) | ||
| require(frequencies.isNotEmpty()) { "preview frequencies must be between 10 and 24000 Hz" } | ||
| val plan = AudioDspPlanCompiler.compile(request.preset, ChannelLayout.from(2, null), 48_000) | ||
| val curve = FrequencyResponse.sample(plan, frequencies.toFloatArray()) | ||
| AudioDspPreviewDto(curve.frequenciesHz.toList(), curve.magnitudeDb.toList(), curve.phaseRadians.toList()) | ||
| } |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check preset-level validation API and FIR tap counts used by the compiler.
fd -t f 'AudioDspModels.kt' -x rg -n -C 5 'class AudioDspPreset|validationErrors|enum class AudioDspFirQuality' {}
fd -t f 'LinearPhaseFirDesigner.kt' -x rg -n -C 5 'fun design|taps' {}Repository: ModerRAS/MiruPlay
Length of output: 3002
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== candidate files =="
fd -t f 'WebControlService.kt|AudioDspModels.kt|AudioDspPlanCompiler.kt|FrequencyResponse.kt|LinearPhaseFirDesigner.kt'
echo
echo "== WebControlService outline relevant methods =="
ast-grep outline web-control/src/main/kotlin/com/miruplay/tv/webcontrol/WebControlService.kt --match previewAudioDsp --view expanded || true
ast-grep outline web-control/src/main/kotlin/com/miruplay/tv/webcontrol/WebControlService.kt --match saveAudioDsp --view expanded || true
echo
echo "== WebControlService excerpt =="
sed -n '740,830p' web-control/src/main/kotlin/com/miruplay/tv/webcontrol/WebControlService.kt | cat -n
echo
echo "== AudioDspModels excerpt =="
sed -n '1,220p' $(fd -t f 'AudioDspModels.kt' | head -n 1) | cat -n
echo
echo "== AudioDspPlanCompiler outline/excerpt =="
python3 - <<'PY'
from pathlib import Path
for p in Path('.').rglob('AudioDspPlanCompiler.kt'):
print('FILE', p)
try:
ast=Path(p).read_text(encoding='utf-8')
for i,line in enumerate(ast.splitlines(),1):
if any(s in line for s in ['object AudioDspPlanCompiler','fun compile','LinearPhaseFirDesigner','firQuality']):
print(f"{i}: {line}")
except Exception as e:
print('ERR', e)
PYRepository: ModerRAS/MiruPlay
Length of output: 15719
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== WebControlService top fields/mutex/imports =="
sed -n '1,140p' web-control/src/main/kotlin/com/miruplay/tv/webcontrol/WebControlService.kt | cat -n
echo
echo "== compile implementation =="
sed -n '1,130p' audio-dsp-core/src/main/kotlin/com/miruplay/tv/audio/AudioDspPlanCompiler.kt | cat -n
echo
echo "== profiling lock references =="
rg -n "profileInProgress|AudioDspPreviewRequest|previewAudioDsp|Mutex|audioDspPreview" web-control/src/main/kotlin/com/miruplay/tv/webcontrol -S
echo
echo "== preview route registration =="
rg -n "previewAudioDsp|AudioDspPreviewRequest|preview" -S --glob '*.kt' --glob '*.kts' web-control/src/main/kotlin web-control/src/test/kotlinRepository: ModerRAS/MiruPlay
Length of output: 14185
Bound preview compilation time and concurrency.
previewAudioDsp compiles request.preset directly on the request coroutine. If the preset is linear-phase with AudioDspFirQuality.HIGH, this designs one 4096-tap FIR per output channel and can run for many consecutive requests without an in-flight guard or FIR-quality limit.
Add a low-quality preview path and serialize in-flight previews, or reuse the existing profileInProgress guard for the preview compile. The current validation only checks frequencies before calling AudioDspPlanCompiler.compile.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web-control/src/main/kotlin/com/miruplay/tv/webcontrol/WebControlService.kt`
around lines 794 - 802, Update previewAudioDsp to compile a bounded low-quality
preview preset instead of allowing request.preset to trigger expensive
high-quality linear-phase FIR design, and serialize concurrent preview
compilations using the existing profileInProgress guard or an equivalent
in-flight guard. Preserve the current frequency validation and response
generation while ensuring the guard is released after compilation, including
failure paths.
Summary
Validation
./gradlew.bat :ui-tv:testDebugUnitTest --no-daemon --no-build-cache./gradlew.bat :ui-tv:compileDebugKotlin :app:assembleDebug --no-daemon --no-build-cache音频 PEQ / DSP, the master switch, and theNeutralpreset in the visible content area.FATAL EXCEPTIONor AndroidRuntime crash.The current HRTF option is a documented compatibility matrix for downmix routing, not a full SOFA/HRIR renderer. Configuration changes apply to the next playback session.
Summary by CodeRabbit