Skip to content

feat(audio): design the equalizer contract (tr-sbp) - #183

Open
jm2 wants to merge 4 commits into
mainfrom
polecat/tr-sbp
Open

feat(audio): design the equalizer contract (tr-sbp)#183
jm2 wants to merge 4 commits into
mainfrom
polecat/tr-sbp

Conversation

@jm2

@jm2 jm2 commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Summary

docs/task.md:1003-1005. Issue #49.
DESIGN-FIRST record (task.md:21-22 requires a design doc or refined issue before implementing cross-output work).
Specify: filter graph, band/preset/preamp/clipping contract, live-reconfiguration boundary, persistence, and a capability matrix across local, AirPlay, Chromecast and MPD outputs.
Blocks the equalizer implementation. Fleet-completable as a design artifact.

Implementation notes

Implemented: corrected rglimiter clip-protection description in docs/equalizer.md to match measured GStreamer 1.28.5 behaviour (soft-knee compressor with −6 dBFS threshold and asymptotic 0 dBFS ceiling; a 0 dBFS input is compressed by ~1.1 dB). Dropped the stale locale enumeration. Added an acceptance-matrix row asserting the real ceiling and threshold. Resolves refinery rejection of d021603 (Gate 1: clip protection).

Refinery handoff

  • Issue: tr-sbp (task, P2)
  • Source branch: polecat/tr-sbp
  • Target: main
  • Rebased on main via Gastown Refinery.

Summary by CodeRabbit

  • Documentation
    • Added a detailed contract/design record for a ten-band equalizer, covering user controls (bypass/preset, preamp, band gains, clip protection), preset behavior, DSP/state behavior, and live reconfiguration rules.
    • Documented equalizer persistence, defaults, and validation/clamping for saved settings.
    • Clarified that the equalizer applies to local playback and is disabled for unsupported output types while preserving last-saved values.
    • Updated the equalizer backlog item to link to the new contract document.

@claude

claude Bot commented Jul 27, 2026

Copy link
Copy Markdown

Claude finished @jm2's task in 2m 56s —— View job


Review complete

Todo list

  • Gather context (PR description, diff, changed files)
  • Read docs/equalizer.md (new design doc, +459 lines)
  • Read docs/task.md diff
  • Check repo conventions (no CLAUDE.md present)
  • Review for correctness, security, performance, testing, documentation
  • Post inline comments for specific issues
  • Post final summary

Summary

This PR is a documentation-only design record (docs/equalizer.md, +459 lines) plus a one-line backlog link update in docs/task.md. There's no executable code in this diff, so the usual security/performance/concurrency/test-coverage categories don't directly apply — I focused the review on internal correctness and consistency of the contract itself, since that's what will bind the eventual implementation and what the linked refinery process (mentioned in the PR body) already rejected once for factual inaccuracy (rglimiter behavior).

The rglimiter correction itself (commits 9930ca4, 3686b33) is well done: the soft-knee-compressor description, −6 dBFS threshold, asymptotic 0 dBFS ceiling, and ~1.1 dB attenuation-at-0-dBFS figure are now stated consistently across the Scope, Filter graph, Band-mechanics, and Acceptance-matrix sections, and the stale locale-count enumeration was correctly dropped in favor of pointing at locales/.

I found three remaining consistency/accuracy issues worth fixing before this is treated as frozen (posted as inline comments):

  1. docs/equalizer.md:197-201 — The doc says Soft clip protection is "the recommended default for fresh installs starting with enabled EQ," but the Bounded-user-surface table and the Fresh-install default-state block both fix the default to Off. This is a direct contradiction in the contract. The paragraph also uses an undefined term, "Pre-LP clip behavior."
  2. docs/equalizer.md:276-297 — The prose says equalizer.cfg values are quoted with "…", but the worked "Fresh-install default state" example shows unquoted values (enabled=false, clip_protect=off, …). One of the two needs to change since an implementer would copy the example literally.
  3. docs/equalizer.md:328 — The MPD capability-matrix row cites eq and setvol as "server-side EQ commands." setvol is a real MPD command but controls volume, not EQ; I couldn't confirm an eq command exists in the MPD protocol or libmpdclient (no web access in this environment to verify further), so this may be citing a command that doesn't exist. Given the PR's own stated purpose is fixing exactly this class of unverified technical claim, I'd flag it for a second look rather than assert it's wrong outright.

Everything else in the contract — the GStreamer element properties (volume gain range/factor conversion, equalizer-10bands band properties and GstChildProxy usage, rglimiter plugin/element identity), the preset gain vectors matching the appendix table, the persistence schema/validation rules, and the live-reconfiguration pause/relink boundary — is internally consistent and reads as carefully verified against the actual GStreamer 1.28.5 behavior.

Not reviewed: I did not attempt to build/lint/test anything, since this PR touches only Markdown and there's no code to execute.

@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.

Run reviewer

TIP This summary will be updated as you push new changes.

Comment thread docs/equalizer.md
Comment on lines +197 to +201
Pre-LP clip behavior is what clip protection actually guards against: the EQ can elevate peaks
above 0 dBFS even with a sane-looking preamp, especially on already-mastered pop/rock material.
When `Clip protection = Off`, the contract explicitly permits clipping and the application must
not pretend it was prevented. The `Soft` option is therefore the recommended default for fresh
installs starting with enabled EQ.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This paragraph contradicts the contract's own stated defaults. It says:

The Soft option is therefore the recommended default for fresh installs starting with enabled EQ.

But the "Bounded user surface" table (line 42) fixes Clip protection default to Off, and the "Fresh-install default state" block (lines 286–297) explicitly persists clip_protect=off. As written, an implementer following the acceptance matrix/persistence sections literally would ship Off as the default, directly conflicting with this sentence.

Given this document states "reviewers must reject any PR that introduces a new band value without the matching entry in the appendix" (line 66-68) for a similar kind of self-consistency requirement, this default-value contradiction should be resolved one way or the other before the doc is treated as the frozen contract.

Separately, "Pre-LP clip behavior" (start of this paragraph) isn't a term defined anywhere else in the document — worth spelling out what "LP" stands for or rewording, since every other technical term in this doc (rglimiter, equalizer-10bands, etc.) is precisely defined.

[Fix this →](https://claude.ai/code?q=In%20docs%2Fequalizer.md%2C%20resolve%20the%20contradiction%20around%20lines%20197-201%3A%20the%20text%20says%20%22Soft%22%20clip%20protection%20is%20%22the%20recommended%20default%20for%20fresh%20installs%20starting%20with%20enabled%20EQ%2C%22%20but%20the%20Bounded%20user%20surface%20table%20(around%20line%2042)%20and%20the%20Fresh-install%20default%20state%20block%20(around%20lines%20286-297)%20both%20fix%20the%20default%20to%20%22Off%22.%20Either%20change%20the%20recommendation%20text%20to%20match%20the%20Off%20default%2C%20or%20change%20the%20documented%20default%20to%20Soft%20consistently%20across%20all%20three%20locations.%20Also%20clarify%20or%20remove%20the%20undefined%20term%20%22Pre-LP%22%20at%20the%20start%20of%20that%20paragraph.&repo=jm2/tributary

@codacy-production codacy-production Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull Request Overview

The PR establishes a comprehensive equalizer design, but several critical issues must be addressed before the contract is finalized. Most importantly, there is a fundamental discrepancy between the PR description and the documentation text regarding the limiter behavior (soft-knee vs. brick-wall). Additionally, technical errors in the GStreamer element properties and pipeline management strategy pose risks to runtime stability and audio quality.

The persistence logic also contains a flaw that prevents users from reverting to default settings. Finally, while the PR is up to standards according to Codacy, an acceptance criterion mentioned in the PR notes (regarding the threshold/ceiling matrix) is missing from the actual documentation.

About this PR

  • There is a significant discrepancy regarding the clipping protection behavior. The PR description notes a shift to a soft-knee compressor model, but the design text still refers to a brick-wall limiter. This needs to be unified to ensure the implementation matches the intended hardware behavior.
  • The proposed manual insertion and removal of elements during playback is high-risk and prone to deadlocks. Utilizing the 'audio-filter' property of 'playbin3' is the idiomatic approach to minimize audio gaps and state transition errors.

Test suggestions

  • Verify fresh install defaults: EQ disabled, Flat preset, 0.0 dB gains/preamp, clip protection off.
  • Enable EQ on local output and verify elements are inserted via pause/relink within 100ms.
  • Update single band gain mid-playback and verify use of property writes without audio gaps.
  • Select Pop preset and verify atomic update of all ten bands and the preamp value.
  • Toggle clip protection and verify rglimiter insertion/removal via the specified pause/relink seam.
  • Verify EQ UI controls are disabled with an appropriate tooltip when the active output is AirPlay.
  • Recover from a malformed 'equalizer.cfg' file by resetting to defaults and logging a warning.
  • Validate that out-of-bounds preamp or band values in the config file are clamped to the [-24, +12] dB range.
  • Confirm EQ settings are correctly re-applied across gapless track transitions.
Prompt proposal for missing tests
Consider implementing these tests if applicable:
1. Verify fresh install defaults: EQ disabled, Flat preset, 0.0 dB gains/preamp, clip protection off.
2. Enable EQ on local output and verify elements are inserted via pause/relink within 100ms.
3. Update single band gain mid-playback and verify use of property writes without audio gaps.
4. Select Pop preset and verify atomic update of all ten bands and the preamp value.
5. Toggle clip protection and verify rglimiter insertion/removal via the specified pause/relink seam.
6. Verify EQ UI controls are disabled with an appropriate tooltip when the active output is AirPlay.
7. Recover from a malformed 'equalizer.cfg' file by resetting to defaults and logging a warning.
8. Validate that out-of-bounds preamp or band values in the config file are clamped to the [-24, +12] dB range.
9. Confirm EQ settings are correctly re-applied across gapless track transitions.

TIP Improve review quality by adding custom instructions
TIP How was this review? Give us feedback

Comment thread docs/equalizer.md Outdated

Persistence uses the same debounced single-writer pattern already used by `Player::save_volume`.
A 750 ms idle interval coalesces slider-drag changes into one write per change-spell. The save
runs on the GTK main loop and is suppressed entirely when the `Enabled = false` state matches

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 HIGH RISK

The suppression logic for saving defaults will cause user settings to revert if they manually return all values to their default states. If a user has custom EQ settings saved and then moves the sliders back to zero (the 'Flat' baseline), the write will be suppressed, leaving the old custom settings on disk to be reloaded on the next application start.

Comment thread docs/equalizer.md Outdated
volume name=eq-preamp volume=<factor> !
equalizer-10bands name=eq
band0=<gain> band1=<gain> ... band9=<gain> !
rglimiter name=clipper enabled=true !

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 HIGH RISK

The 'rglimiter' element does not have an 'enabled' property. It uses the 'peak' property (double) to set the ceiling. To achieve the specified -1 dBFS brick-wall limit, the 'peak' property should be set to '0.891' (the linear equivalent of -1 dBFS).

Suggested change
rglimiter name=clipper enabled=true !
rglimiter name=clipper peak=0.891 !

Comment thread docs/equalizer.md
Implementation acceptance requires the exact conditions listed below. The matrix is exhaustive
for this contract; new conditions require a new revision.

| Scenario | Expected outcome |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 MEDIUM RISK

The acceptance matrix is missing the row 'asserting the real ceiling and threshold' mentioned in the PR implementation notes.

Comment thread docs/equalizer.md Outdated
−1 dBFS". The element takes no other tuning; attack, release, and ceiling are fixed by the
element. Clip protection is the second of the five deliverables named by the bead; the
contract pins it to `rglimiter` rather than to `audioamplify`, which is not a limiter (it is a
static amplifier with hard-clip / wrap / none options, no envelope follower, and no

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 MEDIUM RISK

The text still describes a 'fixed brick-wall ceiling at approximately -1 dBFS', which conflicts with the PR description's claim of a 'soft-knee compressor with -6 dBFS threshold and asymptotic 0 dBFS ceiling'.

Comment thread docs/equalizer.md Outdated
| Preset | enum | `Flat` / `Pop` / `Rock` / `Jazz` / `Classical` | — | `Flat` |
| Preamp | linear dB | `−24.0` … `0.0` … `+12.0` dB; integer or half-step | 0.5 dB | `0.0` dB |
| Bands 1..10 | linear dB | `−24.0` … `0.0` … `+12.0` dB | 0.5 dB | `0.0` dB |
| Clip protection | enum | `Off` / `Soft` (transparent limiter at −1 dBFS) | — | `Off` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 MEDIUM RISK

This value contradicts the PR's stated intent to reflect measured behavior (-6 dBFS threshold and 0 dBFS ceiling).

Comment thread docs/equalizer.md Outdated

## Filter graph

All equalizer DSP runs in the local-output `playbin3` pipeline by inserting a chain of three

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 MEDIUM RISK

Suggestion: Manually relinking the internal graph of a 'playbin3' pipeline between the decoder and sink is complex and can lead to deadlocks. The recommended GStreamer pattern for this is to wrap the preamp, equalizer, and limiter into a single 'GstBin' and assign it to the 'audio-filter' property of 'playbin3'.

Comment thread docs/equalizer.md Outdated
Comment on lines +276 to +297
`volume`. The on-disk format is one line per key, comments are not permitted, key order is
stable (the keys above, in that order), and values are quoted with `"…"` so that whitespace,
quotes, or unicode values cannot break parsing.

Persistence uses the same debounced single-writer pattern already used by `Player::save_volume`.
A 750 ms idle interval coalesces slider-drag changes into one write per change-spell. The save
runs on the GTK main loop and is suppressed entirely when the `Enabled = false` state matches
the default, the preset is `Flat`, all bands are zero, preamp is zero, and clip protection is
`Off`.

Fresh-install default state is exactly:

```ini
schema_version=1
enabled=false
preset=flat
preamp_db=0.0
band0_db=0.0
band9_db=0.0
clip_protect=off
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The stated on-disk format and the worked example disagree. Line 278-279 says:

values are quoted with "…" so that whitespace, quotes, or unicode values cannot break parsing.

But the "Fresh-install default state" example right below (lines 288-297) shows unquoted values (enabled=false, preamp_db=0.0, clip_protect=off, …), not enabled="false" etc. Since this file format is itself part of the frozen contract (an implementer would presumably copy the example verbatim), this ambiguity should be fixed — either add quotes to the example or drop the quoting requirement from the prose.

[Fix this →](https://claude.ai/code?q=In%20docs%2Fequalizer.md%2C%20fix%20the%20mismatch%20between%20the%20stated%20equalizer.cfg%20format%20and%20its%20example.%20Around%20line%20278-279%20the%20prose%20says%20values%20are%20quoted%20with%20%22...%22%2C%20but%20the%20Fresh-install%20default%20state%20example%20around%20lines%20288-297%20shows%20unquoted%20values%20like%20enabled%3Dfalse%20and%20clip_protect%3Doff.%20Make%20the%20example%20match%20the%20stated%20quoting%20rule%2C%20or%20remove%20the%20quoting%20requirement%20from%20the%20prose%20if%20unquoted%20is%20actually%20intended.&repo=jm2/tributary

Comment thread docs/equalizer.md
| Local | supported | Pipeline owns the decoder-to-sink chain; the equalizer chain runs in process. |
| AirPlay (RAOP) | unsupported | The receiving speaker renders audio; in-band equalizer protocol is proprietary and not exposed by the deployed receiver APIs. |
| Chromecast | unsupported | The receiving speaker renders audio; the Cast V2 protocol does not expose a public equalizer channel. |
| MPD | unsupported | MPD exposes server-side EQ commands (`eq`, `setvol`) that require server cooperation and vary by `libmpdclient` build; the canonical contracted behavior is host-side rendering, so host EQ does not reach the receiver. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I'm not confident this claim is accurate: "MPD exposes server-side EQ commands (eq, setvol) that require server cooperation and vary by libmpdclient build". setvol is a real MPD protocol command, but it sets playback volume, not an equalizer — it isn't an "EQ command". I could not find an eq command in the MPD protocol reference or in libmpdclient's API; I wasn't able to verify this via web search in this environment, so flagging it rather than asserting it's wrong.

Given this PR's whole purpose is correcting a previously-rejected factual claim about a GStreamer element (rglimiter, in the prior two commits), it seems worth holding this MPD claim to the same bar — please double check against the actual MPD protocol docs (or drop eq/setvol from the reasoning and just state that MPD has no native EQ capability, if that's the real justification for "unsupported").

[Fix this →](https://claude.ai/code?q=In%20docs%2Fequalizer.md%20around%20line%20328%2C%20verify%20the%20MPD%20capability%20matrix%20row%27s%20claim%20that%20MPD%20exposes%20server-side%20%22eq%22%20and%20%22setvol%22%20commands.%20setvol%20is%20a%20real%20MPD%20volume%20command%20but%20not%20an%20EQ%20command%3B%20confirm%20whether%20an%20%22eq%22%20command%20actually%20exists%20in%20the%20MPD%20protocol%2Flibmpdclient.%20If%20it%20does%20not%2C%20correct%20the%20Reasoning%20column%20to%20accurately%20describe%20why%20MPD%20is%20unsupported%20(e.g.%20MPD%20has%20no%20native%20equalizer%20capability)%20rather%20than%20citing%20a%20nonexistent%20command.&repo=jm2/tributary

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The pull request adds a design contract for Tributary’s ten-band equalizer, covering controls, DSP topology, persistence, output support, UI behavior, diagnostics, accessibility, localization, presets, and acceptance criteria. It also links the contract from the P2.4 backlog item.

Changes

Equalizer contract

Layer / File(s) Summary
Equalizer surface and DSP contract
docs/equalizer.md
Defines controls, ranges, preset vectors, protection modes, filter graphs, and live reconfiguration rules.
Persistence and output behavior
docs/equalizer.md
Specifies equalizer.cfg, validation and replacement behavior, output capability semantics, UI affordances, diagnostics, accessibility, and localization.
Acceptance matrix and backlog tracking
docs/equalizer.md, docs/task.md
Adds acceptance scenarios, implementation boundaries, preset appendices, and the P2.4 contract link.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the PR’s main change: designing the equalizer contract in docs/equalizer.md.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch polecat/tr-sbp

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🤖 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 `@docs/equalizer.md`:
- Around line 330-334: Update the user-visible settings UI behavior for each
unsupported output so the equalizer limitation is communicated through visible
explanatory text or screen-reader-associated text in addition to the disabled
controls' tooltip. Keep the controls disabled and preserve the existing local
equalizer behavior and persisted bands.
- Around line 92-105: Update the equalizer pipeline contract to use playbin3’s
supported audio-filter/audio-sink integration seam instead of requiring direct
decoder-to-playsink relinking. Define the filter as a GstBin containing the
preamp, equalizer-10bands, and optional rglimiter chain, and specify how
enable/disable or clip-protection toggles replace or reconfigure that bin;
alternatively document the supported internal API if direct relinking is
intentional.
- Around line 275-297: Update the equalizer.cfg serialization example to use one
canonical format consistent with the stated contract: quote every value and
order keys according to the preceding table, including placing schema_version
last if that is the table’s order. Ensure the documented fresh-install defaults
and parser/writer expectations use the same ordering and quoting conventions.
- Around line 280-284: Update the persistence suppression logic described around
Player::save_volume so resetting the equalizer to its default state is still
written to disk when older custom settings may exist. Preserve debouncing and
single-writer behavior, but only suppress writes when it is safe to do so
without leaving stale persisted settings that can reappear after restart.
- Around line 99-105: Add a language identifier, such as text or gstreamer, to
the opening fence of each pipeline example in docs/equalizer.md, including the
blocks around the referenced sections, so all fenced code blocks satisfy
markdownlint MD040.
- Around line 303-314: Clarify the validation rules in the “Validation rules on
read” section by defining whether missing keys use per-key defaults while
preserving valid fields or trigger wholesale replacement like malformed values.
Align the malformed-file diagnostic and acceptance behavior with that
precedence, including the affected key and replacement scope.
- Around line 163-170: Clarify the gapless-transition requirement consistently:
the new-generation path must reapply the equalizer-10bands and preamp volume
settings when the gapless event occurs. Update the documentation around the
gapless event hook to state this behavior and add or adjust coverage for that
exact transition, while preserving the existing volume and event-generation
handling.
- Around line 135-138: Update the equalizer pipeline description around the
preamp and post-limiter audioconvert instances to state that audioconvert
handles format and channel conversion only; explicitly account for audioresample
for sample-rate conversion and caps or a mix matrix for stereo output, or adjust
the graph accordingly.
- Around line 183-186: Replace “atomic” wording in the equalizer documentation,
including the descriptions at the referenced preset-update sections, with
“logical” or equivalent language. Clarify that band gains and preamp are applied
through separate property writes and may briefly be inconsistent unless an
explicit snapshot or buffering boundary is implemented; do not claim atomic
behavior without such a mechanism.
- Around line 323-328: Update the MPD row in the equalizer support table to
remove the inaccurate `eq` protocol claim or replace it with a specific
supported server-side equalizer/filter, including its exact plugin or
configuration and supported version. Preserve the documented conclusion that
host-side equalizer processing does not reach the receiver.
🪄 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: 373a9f7c-9a74-4a16-8817-86e6b9ee6e26

📥 Commits

Reviewing files that changed from the base of the PR and between 6afb427 and 3686b33.

📒 Files selected for processing (2)
  • docs/equalizer.md
  • docs/task.md

Comment thread docs/equalizer.md Outdated
Comment thread docs/equalizer.md Outdated
Comment thread docs/equalizer.md Outdated
Comment thread docs/equalizer.md Outdated
Comment thread docs/equalizer.md Outdated
Comment thread docs/equalizer.md Outdated
Comment thread docs/equalizer.md Outdated
Comment on lines +280 to +284
Persistence uses the same debounced single-writer pattern already used by `Player::save_volume`.
A 750 ms idle interval coalesces slider-drag changes into one write per change-spell. The save
runs on the GTK main loop and is suppressed entirely when the `Enabled = false` state matches
the default, the preset is `Flat`, all bands are zero, preamp is zero, and clip protection is
`Off`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Fix default-state suppression so reset settings are persisted.

This is the same unresolved issue from the previous review: suppressing the write when the state equals defaults leaves older custom settings on disk, so resetting to Flat can resurrect those settings after restart.

🤖 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/equalizer.md` around lines 280 - 284, Update the persistence suppression
logic described around Player::save_volume so resetting the equalizer to its
default state is still written to disk when older custom settings may exist.
Preserve debouncing and single-writer behavior, but only suppress writes when it
is safe to do so without leaving stale persisted settings that can reappear
after restart.

Comment thread docs/equalizer.md Outdated
Comment thread docs/equalizer.md
Comment on lines +323 to +328
| Output | Equalizer DSP | Reasoning |
|------------------|---------------|----------------------------------------------------------------------------------------------------------------------------|
| Local | supported | Pipeline owns the decoder-to-sink chain; the equalizer chain runs in process. |
| AirPlay (RAOP) | unsupported | The receiving speaker renders audio; in-band equalizer protocol is proprietary and not exposed by the deployed receiver APIs. |
| Chromecast | unsupported | The receiving speaker renders audio; the Cast V2 protocol does not expose a public equalizer channel. |
| MPD | unsupported | MPD exposes server-side EQ commands (`eq`, `setvol`) that require server cooperation and vary by `libmpdclient` build; the canonical contracted behavior is host-side rendering, so host EQ does not reach the receiver. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

MPD protocol eq command equalizer docs setvol ReplayGain official protocol equalizer plugin version

💡 Result:

The Music Player Daemon (MPD) protocol does not contain a native "eq" or "equalizer" command [1][2][3]. Equalization in MPD is typically handled through external system-level plugins or configuration-based filters, rather than through direct protocol commands sent by clients [4][5][6]. 1. Equalization Methods: - Native MPD Filters: Since MPD version 0.22, you can use the ffmpeg filter plugin to apply equalization natively within the mpd.conf file [4]. This involves defining a filter with plugin "ffmpeg" and a graph string (e.g., "anequalizer") [4]. Once configured, these filters are applied to specific audio_output sections [4]. - ALSA Equalizer Plugin: Historically and commonly, users apply equalization by using the ALSA "equal" plugin [5][6][7]. This is configured in the system's ALSA settings (e.g.,.asoundrc) and accessed as an audio output in MPD [5][7]. Control of this equalizer is generally performed via external shell scripts using amixer or alsamixer, rather than through the MPD protocol itself [5][6][7]. 2. Protocol Commands: - setvol: The setvol {VOL} command is the standard MPD protocol command to set the playback volume (range 0-100) [2][3][8]. It is independent of equalization settings [9]. - ReplayGain: The protocol includes specific commands for ReplayGain management, such as replay_gain_mode {MODE} (options: off, track, album, auto) and replay_gain_status [2][3][8]. These commands control how MPD applies gain metadata to normalize volume, but they do not function as a graphical or parametric equalizer [9]. There is no version-specific "equalizer plugin version" command in the official MPD protocol [1][2][3]. If you require real-time equalization control, it must be implemented at the audio output or filter chain level, which generally falls outside the scope of standard MPD client-to-server protocol commands [4][5][7].

Citations:


Remove the eq claim or replace it with the concrete server-side equalizer/filter. MPD’s protocol has no native eq command; if this row refers to equalization, name the exact plugin/configuration and supported version (for example, an MPD 0.22+ ffmpeg filter or an ALSA equalizer setup).

🤖 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/equalizer.md` around lines 323 - 328, Update the MPD row in the
equalizer support table to remove the inaccurate `eq` protocol claim or replace
it with a specific supported server-side equalizer/filter, including its exact
plugin or configuration and supported version. Preserve the documented
conclusion that host-side equalizer processing does not reach the receiver.

Comment thread docs/equalizer.md Outdated
Comment on lines +330 to +334
For each `unsupported` output, the user-visible settings UI renders the equalizer controls as
disabled with a tool-tip explaining the limitation (e.g. "AirPlay receivers render audio
end-to-end, so Tributary's equalizer cannot reach the speaker.") Disabled controls preserve the
last-saved values locally; the equalizer does run for the local output even while the user's
active output is unsupported, so a later switch back to local reflects the same persisted bands.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Provide an accessible explanation beyond a tooltip on disabled controls.

Disabled controls may not receive focus, so the limitation must also be exposed as visible or screen-reader-associated text. A tooltip alone can make the unsupported-output state undiscoverable by keyboard and assistive-technology users.

🤖 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/equalizer.md` around lines 330 - 334, Update the user-visible settings
UI behavior for each unsupported output so the equalizer limitation is
communicated through visible explanatory text or screen-reader-associated text
in addition to the disabled controls' tooltip. Keep the controls disabled and
preserve the existing local equalizer behavior and persisted bands.

@jm2
jm2 force-pushed the polecat/tr-sbp branch from 3686b33 to d5d0b4f Compare July 27, 2026 16:12
@claude

claude Bot commented Jul 27, 2026

Copy link
Copy Markdown

Claude finished @jm2's task in 1m 45s —— View job


Review complete

Todo list

  • Gather context (PR description, diff, prior review comments)
  • Check what changed in the latest commit (d5d0b4f) vs. prior review
  • Verify whether prior review findings (rglimiter, defaults contradiction, quoting mismatch, MPD eq claim) were addressed
  • Review new/remaining issues
  • Post final summary

Summary

This is a synchronize event — new commits (1103e55, d5d0b4f) were pushed since the earlier review. This is still a documentation-only design record (docs/equalizer.md), so I focused on internal correctness/consistency of the contract, same as before.

What the latest commits fixed well:
The rglimiter re-description (commit d5d0b4f, "describe rglimiter honestly") is accurate and now consistent everywhere it appears — Scope (line 18), Bounded user surface table (line 42), Band-mechanics (lines 123–134), Live-reconfiguration (lines 192–195), and the new acceptance-matrix row (line 406): soft-knee compressor, −6 dBFS threshold, asymptotic 0 dBFS ceiling, ~1.1 dB attenuation at a 0 dBFS input. This matches the "Apply signal compression to raw audio data" element description and correctly drops the earlier "brick-wall −1 dBFS" claim that Codacy/CodeRabbit flagged. The stale per-locale-file enumeration was also correctly replaced with a pointer to locales/.

Issues from the previous review round that are still open (unaddressed by these two commits):

  1. docs/equalizer.md:200-201 — Still contradicts the documented default: text says Soft is "the recommended default for fresh installs starting with enabled EQ," but the Bounded-user-surface table (line 42) and Fresh-install default state (lines 286-297) both fix the default to Off. "Pre-LP" (line 197) is still an undefined term.
  2. docs/equalizer.md:277 vs 288-297 — Still says values are quoted with "…" but the fresh-install example is unquoted. One needs to change since an implementer would copy the example literally.
  3. docs/equalizer.md:328 — Still claims MPD "exposes server-side EQ commands (eq, setvol)". setvol is real but controls volume, not EQ; CodeRabbit's research found no eq command in the MPD protocol or libmpdclient. Worth the same scrutiny this PR already gave the rglimiter claim, since it's the same class of unverified technical assertion the PR states it's fixing.
  4. docs/equalizer.md:169-170 vs 415 — Still contradictory: prose says playbin's gapless hook "only re-applies volume and event generation, not EQ settings," but the acceptance matrix (line 415) requires "EQ settings re-applied to the new generation" on gapless transitions. Needs to pick one behavior.
  5. docs/equalizer.md:280-284 — Persistence write is still suppressed whenever live state equals the default vector. If a user has custom settings saved and manually returns every band/preamp to 0 (without an explicit "reset" affordance), the write is silently suppressed and the old custom values reload on next launch.
  6. docs/equalizer.md:185, 211, 404 — Still describes preset updates as "atomic," but the mechanism (line 211) is sequential g_object_set property writes on two different elements with no freeze/thaw or snapshot boundary, so a transiently mixed preset state is possible. "Logical"/"sequential" would be accurate; "atomic" implies a guarantee this mechanism doesn't provide.
  7. docs/equalizer.md:330-334 — Unsupported-output explanation is still tooltip-only; disabled controls aren't reliably reachable by keyboard/screen-reader users, so the limitation needs an accompanying visible or screen-reader-associated text, not just a tooltip.
  8. docs/equalizer.md:99, 142, 155 — Fenced pipeline blocks still lack a language identifier (markdownlint MD040) — minor.

Design questions raised by CodeRabbit that are legitimate but not factual errors (worth a decision before implementation, not blocking as design-doc issues): using playbin3's audio-filter/audio-sink seam instead of manual decoder↔playsink relinking (lines 92-105), and the audioconvert claim at line 135 overstating its role (it does format/channel conversion, not sample-rate conversion — that's audioresample).

Not reviewed: no build/lint/test run, since this PR touches only Markdown.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 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 `@docs/equalizer.md`:
- Around line 94-95: Update the rglimiter description in the equalizer
documentation to call it a “soft-knee compressor/limiter” instead of a “peak
limiter,” and use that terminology consistently without changing the surrounding
contract language.
- Around line 313-314: Resolve the contradiction between the malformed-file read
rules and the UI reload behavior in docs/equalizer.md: choose whether malformed
files are repaired automatically or only when `Reload defaults from disk` is
invoked, then update both sections consistently. Preserve the documented
diagnostic details and ensure the chosen behavior is explicit.
- Around line 333-338: Clarify the equalizer documentation around the
unsupported active output to state that the local DSP chain is configured but
dormant, not running in a background local pipeline; explain that processing
resumes when local output becomes active while persisted bands remain intact.
- Around line 238-239: Update the EOS-resending property-write wording in the
equalizer documentation to remove the implication that g_object_set() causes a
bus flush or Buffering event. State that spinner behavior depends on actual
pipeline messages, and describe any flushing only as a separate operation.
🪄 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: abdca1ca-2f24-4422-ba4f-5bcb3df6a578

📥 Commits

Reviewing files that changed from the base of the PR and between 3686b33 and d5d0b4f.

📒 Files selected for processing (2)
  • docs/equalizer.md
  • docs/task.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/task.md

Comment thread docs/equalizer.md Outdated
Comment thread docs/equalizer.md Outdated
Comment thread docs/equalizer.md Outdated
Comment on lines +313 to +314
A malformed file is replaced with the default state, the user's prior preferences are recorded
in a typed diagnostic with file path, byte count, and the bad key, and the change is not silent.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Resolve the malformed-file reload contradiction.

The read rules say malformed files are automatically replaced with defaults, but the UI section says Reload defaults from disk is the only way to remove one. Define whether repair happens automatically or only after explicit reload, then align both sections.

Also applies to: 358-360

🤖 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/equalizer.md` around lines 313 - 314, Resolve the contradiction between
the malformed-file read rules and the UI reload behavior in docs/equalizer.md:
choose whether malformed files are repaired automatically or only when `Reload
defaults from disk` is invoked, then update both sections consistently. Preserve
the documented diagnostic details and ensure the chosen behavior is explicit.

Comment thread docs/equalizer.md Outdated
gastown.furiosa and others added 4 commits July 29, 2026 11:04
Adds docs/equalizer.md fixing the equalizer filter graph, ten fixed ISO
bands 31 Hz..16 kHz with five named presets, linear preamp range
-24..+12 dB, optional clip-protection limiter, mid-playback live
reconfiguration boundary, persisted equalizer.cfg with schema_version 1,
and the per-output capability matrix (local supported; AirPlay,
Chromecast, MPD unsupported with disabled controls and honest
explanations). Mirrors task.md entry for P2.4 to cite the in-progress
contract. Implementation record remains pending.
…nt properties

Fixes the four review-blocking defects identified by the refinery against
polecat/tr-sbp (commit 32c5394):

1. equalizer-10bands has no 'preamp' property and no bypass flag.
   Verified: gst-inspect-1.0 shows band0..band9, name, parent, qos only.
   Contract now pins the preamp to a dedicated 'volume' element ahead of
   the equalizer (factor = 10^(dB/20), range 0.0631..3.9811 inside the
   element's 0..10 window), and treats Enabled=false as chain
   insert/remove (no bypass property to flip).

2. audioamplify is not a limiter and has no clip-mode / max-amplitude /
   envelope follower. Verified: gst-inspect-1.0 shows amplification,
   clipping-method (enum 0=clip, 1=wrap-negative, 2=wrap-positive,
   3=none), name, parent, qos only. The doc's claim that 1 is
   'soft-knee' and 0 is 'off' is inverted: 1 is gross distortion and 0
   is hard clip. Replaced with rglimiter (gst-plugins-good), whose
   'enabled' boolean is its only behavioural knob and whose ceiling is
   fixed at approximately -1 dBFS -- exactly the contract's
   'transparent limiter at -1 dBFS' semantics.

3. Band centres stated as 'fixed by the spec' were wrong. The element's
   centres are 29, 59, 119, 237, 474, 947, 1889, 3770, 7523, 15011 Hz.
   Contract now uses these canonical centres and documents the
   GstChildProxy mechanism (band0::freq..band9::freq) for any future
   custom-centre revision; appendix preset vectors relabelled to match.

4. Live-reconfiguration boundary contradicted itself on Enabled=false
   (bypass property vs chain removal vs insert/remove-without-pause)
   and on Clip protection (pause/relink seam). Unified to a single
   pause/insert/resume seam for both Enabled and Clip protection, with
   a per-toggle 100 ms swap budget; band/preamp/preset remain
   property-write (no state transition).

Also widened the band dB range from [-12, +12] to [-24, +12] in the
bounded surface, persistence validation, and accessibility copy to
match the equalizer-10bands element's actual range.

Refinery note from the rejection: persistence, capability matrix,
diagnostics, accessibility, acceptance matrix, and the rest of the
document were considered complete and implementable; this commit
corrects only the four named defects and leaves everything else
untouched.

Refs: tr-sbp, gate 1 review 32c5394.
The clip-protection contract described rglimiter as a transparent
brick-wall limiter at −1 dBFS. Measurement shows the element is a
soft-knee signal compressor ('Apply signal compression to raw audio
data' per gst-inspect), with a fixed −6 dBFS threshold and an
asymptotic 0 dBFS output ceiling; a 0 dBFS input is compressed by
approximately 1.1 dB.

Update the control table label, the Clip-protection paragraph, the
rglimiter element description in Filter graph, the Band-and-preamp-
mechanics paragraph, and the Scope statement to describe what the
element actually does. Drop the stale locale enumeration (main ships
13 locales; the doc named 10) and reference locales/ instead. Add an
acceptance-matrix row asserting the measured threshold and ceiling.

Refs: refinery rejection against d021603 (Gate 1 fail on clip
protection).
Resolves the refinery's semantic review of d5d0b4f (sha 722b8d73) which cited four
internally-contradictory sections in docs/equalizer.md:

1. Filter graph (formerly lines 135-170). The previous chain
   `uridecodebin ! audioconvert ! volume ! equalizer-10bands ! rglimiter ! audioconvert ! playsink`
   promised sample-rate/channel normalisation and F32LE negotiation, but the
   graph defined neither audioresample nor capsfilter and assigned format
   negotiation to an audioconvert placed after rglimiter (audioconvert alone
   does not change sample rate and cannot pin F32LE). Rework the chain as a
   playbin3 audio-filter bin with two audioresample ! audioconvert !
   capsfilter caps=<...> wrappers around the EQ/limiter stage. The pre-EQ
   capsfilter pins audio/x-raw, format=F32LE, channels=2, layout=interleaved
   so both the biquad and rglimiter see the format they require. The
   post-EQ capsfilter pins <sink-caps> filled in at chain-construction time.
   Failure during audio-filter-caps negotiation is now the spec's only
   rollback path: the bin does not insert, no chain is half-installed.

2. Live-reconfiguration (formerly lines 183-239). The previous text claimed
   g_object_set produces a bus-flush Buffering event and described multi-
   property writes as 'atomic' without defining the mechanism. Rework the
   mechanism as a three-step buffer-boundary transaction: capture into
   EqSettings, wrap writes in g_object_freeze_notify / g_object_thaw_notify
   per affected element, then wait for the next GST_EVENT_CAPS / SEGMENT on
   the bus as the buffer-boundary marker. State explicitly that g_object_set
   produces no Buffering event (GObject state mutation, not a pipeline
   event) so the UI does not confuse decoder-side Buffering with EQ writes.
   Add the write-only Custom preset name: manual edits move the persisted
   preset field from any named preset to 'custom', and the UI displays
   'Custom' to signal that the active vector no longer matches a named
   response. Loading a named preset from Custom re-sets the persisted name.

3. Persistence (formerly lines 275-314). The previous text said values are
   quoted with double quotes, but the normative example showed unquoted
   values, and the 750 ms debounce promised no lost writes without any
   atomic-replace or shutdown-flush guarantee. Make the grammar canonical:
   every value is double-quoted UTF-8, in fixed order, with quote and
   backslash escaping. Update the example to match. Add an atomic-replace
   protocol: write to equalizer.cfg.tmp with O_EXCL + fsync, then rename to
   the destination, then fsync the directory. Add a shutdown-flush hook on
   gtk::main_quit and SIGTERM/SIGINT so quitting while the debounce is
   pending still produces a complete file. Update the validation rules
   and preset coercion ('flat' / 'custom' included in the named set).

4. Capability matrix (formerly lines 330-338). The previous text said both
   'the equalizer does run for the local output even while the user's
   active output is unsupported' and 'the equalizer is not applied to
   anything' - a direct contradiction. Collapse to one rule: when the
   active output is unsupported, the audio-filter bin is not installed in
   any pipeline (playbin3.audio-filter is NULL); no DSP runs; the
   persisted state remains on disk and re-attaches when output switches
   back to a supported output. Update the acceptance matrix and
   implementation-boundary section to match the new graph and the new
   persistence grammar.

Validation: docs/equalizer.md is now self-consistent against the four
reviewer concerns; cross-references between the Filter graph,
Live-reconfiguration, Persistence, Capability matrix, and Acceptance
matrix sections now agree on the bin layout, the buffer-boundary
transaction, the quoted grammar, the Custom state, and the one
output-activation rule. No implementation files were touched (this is a
design-only contract document per the header status line); the next bead
on the implementation backlog references this contract verbatim.
@jm2
jm2 force-pushed the polecat/tr-sbp branch from d5d0b4f to b8b87a9 Compare July 29, 2026 15:15
@claude

claude Bot commented Jul 29, 2026

Copy link
Copy Markdown

Claude finished @jm2's task in 3m 53s —— View job


Review complete

Todo list

  • Gather context (PR description, prior review comments, diff since last review)
  • Check what changed in the latest commit (b8b87a9, "resolve equalizer-contract semantic-review defects") vs. prior review rounds
  • Verify whether prior review findings were addressed
  • Review remaining/new issues
  • Post inline comments
  • Post final summary

Summary

b8b87a9 is a substantial rewrite (+246/-155 lines) of the Filter graph, Live-reconfiguration, Persistence, and Capability-matrix sections, responding to a refinery semantic-review rejection. This is still a documentation-only design record, so the review focuses on internal correctness/consistency of the contract.

What this rewrite fixed well (resolving issues flagged in earlier review rounds):

  • The playbin3.audio-filter bin approach replaces manual decoder↔playsink relinking — resolves the earlier "relinking is deadlock-prone" finding.
  • audioconvert vs audioresample responsibilities are now correctly separated and explicitly called out (lines 132-141) — resolves the earlier "audioconvert doesn't do sample-rate conversion" finding.
  • "Atomic" preset-update wording is gone, replaced with an explicit g_object_freeze_notify/thaw_notify buffer-boundary transaction (lines 260-276) — resolves the earlier "atomic implies a guarantee g_object_set doesn't provide" finding.
  • The equalizer.cfg quoting mismatch is fixed — the fresh-install example (lines 366-375) now matches the stated quoted-value grammar.
  • The gapless-transition contradiction is resolved — prose (lines 179-181) and the acceptance matrix (line 502) now agree EQ state is not re-applied automatically across URI transitions.
  • The malformed-file-read vs. UI-reload contradiction is resolved (lines 380-394, 442-444 now agree: malformed files are auto-replaced with defaults on read, "Reload defaults from disk" is a separate explicit affordance).
  • The "equalizer does/doesn't run for local while output is unsupported" contradiction is resolved into one explicit rule (lines 415-422).

Issues from earlier rounds still open (unaddressed by this rewrite — posted as inline comments):

  1. Lines 220-224 — Still says Soft clip protection is "the recommended default for fresh installs starting with enabled EQ," directly contradicting the Off/Enabled=false defaults fixed everywhere else in the doc (line 43, lines 364-375). Flagged in the first two review rounds; this section wasn't touched by the rewrite. "Pre-LP" (line 220) is also still undefined.
  2. Line 408 — Still claims MPD "exposes server-side EQ commands (eq, setvol)." A prior round in this thread already ran the research and confirmed no native eq command exists in the MPD protocol or libmpdclient; setvol is real but controls volume, not EQ. This is now essentially a confirmed factual error left open across three review rounds.
  3. Lines 410-413 — Still tooltip-only for explaining unsupported-output behavior; disabled controls aren't reliably reachable by keyboard/screen-reader users, and the Accessibility section doesn't add any accessible-text requirement for this case. Flagged twice previously, still open.

New/leftover issue found in this round:
4. Lines 351-355 — The debounced-write suppression ("suppressed entirely when the state matches the fresh-install default") still has a reproducible gap under the new grammar: transitioning into the exact default state (e.g., toggling Clip protection from Soft to Off when everything else is already default) skips the write, leaving the stale prior value on disk. The shutdown-flush hook (lines 357-362) likely papers over this on a clean quit, but the doc doesn't say it bypasses the same suppression, so a crash/SIGKILL/power-loss before quit leaves incorrect state on disk. This is the same underlying flaw the earlier "resets don't persist" finding pointed at, just reproducible via a different path now.
5. Line 501 — The acceptance-matrix row "Pre-EQ audioconvert caps remain [1, 2]" wasn't updated for the rewrite and now contradicts the Filter graph's own explicit clarification that capsfilter, not audioconvert, owns caps negotiation in this design (lines 146-147).
6. Line 114 (and 311) — Still-unlabeled fenced code blocks (markdownlint MD040); the persistence example at line 366 was fixed with ini, these two weren't (minor).

Not reviewed: no build/lint/test run, since this PR touches only Markdown.

Comment thread docs/equalizer.md
Comment on lines +220 to +224
Pre-LP clip behavior is what clip protection actually guards against: the EQ can elevate peaks
above 0 dBFS even with a sane-looking preamp, especially on already-mastered pop/rock material.
When `Clip protection = Off`, the contract explicitly permits clipping and the application must
not pretend it was prevented. The `Soft` option is therefore the recommended default for fresh
installs starting with enabled EQ.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This contradiction from the first two review rounds is still unresolved by the latest rewrite (b8b87a9): "The Soft option is therefore the recommended default for fresh installs starting with enabled EQ" directly conflicts with the Bounded-user-surface table (line 43, Clip protection default Off) and the Fresh-install default state block (lines 364-375, clip_protect="off"). The rewrite fixed the Filter graph, Live-reconfiguration, Persistence, and Capability-matrix sections, but this Band-and-preamp-mechanics paragraph wasn't touched. "Pre-LP" (line 220) is also still an undefined term.

[Fix this →](https://claude.ai/code?q=In%20docs%2Fequalizer.md%20around%20lines%20220-224%2C%20resolve%20the%20still-open%20contradiction%3A%20the%20text%20says%20%22Soft%22%20clip%20protection%20is%20%22the%20recommended%20default%20for%20fresh%20installs%20starting%20with%20enabled%20EQ%2C%22%20but%20the%20Bounded%20user%20surface%20table%20(line%2043)%20and%20the%20Fresh-install%20default%20state%20block%20(lines%20364-375)%20both%20fix%20the%20default%20to%20%22Off%22%2FEnabled%3Dfalse.%20Either%20change%20this%20paragraph%20to%20match%20the%20Off%2Fdisabled%20default%2C%20or%20change%20the%20documented%20defaults%20everywhere%20to%20match%20this%20recommendation.%20Also%20define%20or%20remove%20the%20undefined%20term%20%22Pre-LP%22%20at%20the%20start%20of%20line%20220.&repo=jm2/tributary

Comment thread docs/equalizer.md
| Local | supported | Pipeline owns the decoder-to-sink chain; the equalizer chain runs in process. |
| AirPlay (RAOP) | unsupported | The receiving speaker renders audio; in-band equalizer protocol is proprietary and not exposed by the deployed receiver APIs. |
| Chromecast | unsupported | The receiving speaker renders audio; the Cast V2 protocol does not expose a public equalizer channel. |
| MPD | unsupported | MPD exposes server-side EQ commands (`eq`, `setvol`) that require server cooperation and vary by `libmpdclient` build; the canonical contracted behavior is host-side rendering, so host EQ does not reach the receiver. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Still unresolved across three review rounds: "MPD exposes server-side EQ commands (eq, setvol)". A prior review round in this same thread already ran the verification and found no native eq command in the MPD protocol or libmpdclientsetvol is real but controls volume, not equalization. The doc's own stated purpose (correcting the rglimiter misdescription) makes this the same class of unverified/incorrect technical claim it was written to fix. Suggest replacing the reasoning with something verifiable, e.g. "MPD has no native equalizer command; equalization would require an output-side ALSA/ffmpeg filter configured outside the MPD protocol, which this contract does not attempt to control."

[Fix this →](https://claude.ai/code?q=In%20docs%2Fequalizer.md%20around%20line%20408%2C%20the%20MPD%20capability-matrix%20row%20claims%20MPD%20exposes%20server-side%20%22eq%22%20and%20%22setvol%22%20commands.%20setvol%20is%20real%20but%20controls%20volume%20not%20EQ%2C%20and%20no%20%22eq%22%20command%20exists%20in%20the%20MPD%20protocol%20or%20libmpdclient.%20Correct%20the%20Reasoning%20column%20to%20accurately%20state%20why%20MPD%20is%20unsupported%20(MPD%20has%20no%20native%20equalizer%20capability%3B%20any%20EQ%20would%20be%20an%20output-side%20ALSA%2Fffmpeg%20filter%20outside%20the%20protocol)%20instead%20of%20citing%20a%20nonexistent%20%22eq%22%20command.&repo=jm2/tributary

Comment thread docs/equalizer.md
Comment on lines +410 to +413
For each `unsupported` output, the user-visible settings UI renders the equalizer controls as
disabled with a tool-tip explaining the limitation (e.g. "AirPlay receivers render audio
end-to-end, so Tributary's equalizer cannot reach the speaker.") Disabled controls preserve the
last-saved values locally.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Still tooltip-only, unaddressed since the first review round: "the user-visible settings UI renders the equalizer controls as disabled with a tool-tip explaining the limitation." Disabled controls are frequently unreachable by keyboard focus and screen readers, so a tooltip alone can make this limitation undiscoverable to assistive-technology users. The Accessibility and localization section (lines 465-478) doesn't add any accessible-text requirement for this case either. Needs visible or screen-reader-associated explanatory text alongside the disabled controls, not just a tooltip.

[Fix this →](https://claude.ai/code?q=In%20docs%2Fequalizer.md%20around%20lines%20410-413%2C%20update%20the%20unsupported-output%20UI%20behavior%20so%20the%20equalizer%20limitation%20is%20communicated%20through%20visible%20explanatory%20text%20or%20screen-reader-associated%20text%2C%20not%20just%20a%20tooltip%20on%20disabled%20controls.%20Also%20add%20a%20line%20to%20the%20Accessibility%20and%20localization%20section%20(around%20lines%20465-478)%20requiring%20this%20text%20to%20be%20exposed%20accessibly.&repo=jm2/tributary

Comment thread docs/equalizer.md
Comment on lines +351 to +355
Persistence uses a debounced single-writer pattern: a 750 ms idle interval coalesces slider-drag
changes into one write per change-spell. The save runs on the GTK main loop and is suppressed
entirely when the state matches the fresh-install default (Enabled `false`, preset `Flat`, all
bands zero, preamp zero, clip protection `Off`). The debounce timer is reset on every change so
drag-induced writes are flushed on the trailing edge of the gesture.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The persistence-suppression bug flagged in earlier rounds is narrower now but still reproducible under the new grammar. The save is "suppressed entirely when the state matches the fresh-install default." Concrete repro: disk holds (enabled="false", preset="flat", bands=0, preamp="0.0", clip_protect="soft") — not a default match, so it was written normally. User then toggles Clip protection from Soft to Off. New state is now the exact default vector, so the debounced write is suppressed — the on-disk file keeps clip_protect="soft" even though the live/last-chosen state is Off. The shutdown-flush hook (lines 357-362) will correct this on a clean quit, but nothing here says the shutdown flush bypasses this same "matches default" suppression, and an unclean exit (crash, SIGKILL, power loss) leaves the stale clip_protect="soft" on disk to be reloaded next launch. Worth either dropping the default-state suppression (writes are cheap and atomic already) or explicitly stating the shutdown flush always writes unconditionally regardless of this match.

[Fix this →](https://claude.ai/code?q=In%20docs%2Fequalizer.md%20around%20lines%20351-362%2C%20the%20debounced%20writer%20suppresses%20writes%20whenever%20the%20new%20state%20exactly%20matches%20the%20fresh-install%20default.%20This%20means%20a%20transition%20INTO%20the%20exact%20default%20state%20(e.g.%20toggling%20Clip%20protection%20from%20Soft%20to%20Off%20when%20everything%20else%20is%20already%20default)%20never%20gets%20written%2C%20leaving%20stale%20non-default%20values%20on%20disk%20that%20only%20get%20corrected%20by%20the%20shutdown-flush%20hook%20on%20a%20clean%20quit%2C%20not%20on%20crash%2FSIGKILL%2Fpower-loss.%20Either%20remove%20the%20default-state%20write%20suppression%20entirely%2C%20or%20explicitly%20document%20that%20the%20shutdown-flush%20hook%20always%20writes%20unconditionally%20regardless%20of%20whether%20the%20state%20matches%20defaults.&repo=jm2/tributary

Comment thread docs/equalizer.md
| Preamp outside bounds in saved file | Value clamped to range; preset and bands remain valid |
| Band value outside bounds in saved file | Value clamped to range; other bands remain valid |
| Preset name not in the named set on disk | Coerced to `flat`; band vector remains as written on disk |
| Hardware sink with 8-channel layout (macOS) | Pre-EQ `audioconvert` caps remain `[1, 2]`; EQ runs in stereo; same cap fix as existing module |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This row wasn't updated for the Filter-graph rewrite and now contradicts it: "Pre-EQ audioconvert caps remain [1, 2]". Per the rewritten Filter graph section (lines 137-147), audioconvert explicitly does not own or negotiate caps — that's the job of the pre/post-EQ capsfilter elements ("This is the only element in the chain that negotiates a format; audioconvert does conversion work, capsfilter enforces the boundary"). This acceptance-matrix row should say capsfilter caps stay channels=2, not audioconvert caps.

[Fix this →](https://claude.ai/code?q=In%20docs%2Fequalizer.md%20around%20line%20501%2C%20the%20acceptance-matrix%20row%20for%20%22Hardware%20sink%20with%208-channel%20layout%20(macOS)%22%20says%20%22Pre-EQ%20audioconvert%20caps%20remain%20%5B1%2C%202%5D%22.%20This%20contradicts%20the%20rewritten%20Filter%20graph%20section%20(around%20lines%20137-147)%2C%20which%20states%20capsfilter%2C%20not%20audioconvert%2C%20is%20the%20element%20that%20pins%2Fnegotiates%20caps%20in%20this%20bin.%20Update%20the%20row%20to%20reference%20the%20pre-EQ%20capsfilter%27s%20pinned%20channels%3D2%20instead%20of%20audioconvert%20caps.&repo=jm2/tributary

Comment thread docs/equalizer.md

The chain layout for the *enabled, clip-protection-on* state is:

```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor/nit, carried over from earlier rounds: this fence (and the one at line 311) still has no language identifier, so markdownlint MD040 still fires on these two blocks. The persistence example fence at line 366 was already fixed with ini; these two could use text or similar.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@docs/equalizer.md`:
- Around line 260-276: Replace the buffer-boundary acknowledgement described in
the documentation around the “buffer-boundary transaction” with a real
observable mechanism: use an explicit GstMessage, pad probe, or
sequence-number/generation acknowledgement instead of relying on
g_object_freeze_notify or GST_EVENT_CAPS/GST_EVENT_SEGMENT bus messages. Update
the three-step sequence and multi-property transaction guidance to describe the
selected acknowledgement accurately.
- Around line 101-110: Revise the equalizer documentation to describe
negotiation through the internal capsfilter and ghost-pad contract, not a caps
property on the GstBin or sink-pad activate behavior. State that setup failures
are link/negotiation failures, the bin is not inserted, and the pipeline falls
back to the existing passthrough layout without element-by-element fallback or
partial insertion.
🪄 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: 21948466-b175-43d7-a6b0-ab1e9a0393a1

📥 Commits

Reviewing files that changed from the base of the PR and between d5d0b4f and b8b87a9.

📒 Files selected for processing (2)
  • docs/equalizer.md
  • docs/task.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/task.md

Comment thread docs/equalizer.md
Comment on lines +101 to +110
The bin carries a `caps` property pinned to
`audio/x-raw, format=F32LE, channels=2, layout=interleaved, rate=<samplerate>` (where
`<samplerate>` is the rate `playbin3` negotiated with the decoder on the bin's sink pad at
chain-construction time). `playbin3` uses this caps property to negotiate the upstream format;
if the upstream decoder cannot deliver that caps — typically only on a malformed or non-PCM
source — the bin's sink pad `activate` mode returns `FALSE`, `playbin3` propagates the error to
the bus, the implementation does **not** insert the bin, and the pipeline falls back to the
existing passthrough layout (a single info-level diagnostic names the source URI). This is the
spec's only rollback path: there is no element-by-element fallback inside the bin, and a failed
`audio-filter-caps` negotiation does not leave the chain half-inserted.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/sh
set -eu
gst-inspect-1.0 playbin3
gst-inspect-1.0 rglimiter

Repository: jm2/tributary

Length of output: 204


🏁 Script executed:

#!/bin/sh
set -eu

printf '\n== docs/equalizer.md excerpt ==\n'
sed -n '90,125p' docs/equalizer.md

printf '\n== search for caps property / activate mode / playbin3 references ==\n'
rg -n 'caps property|activate mode|playbin3|audio-filter-caps|GstBin|ghost pad|capsfilter' docs/equalizer.md

Repository: jm2/tributary

Length of output: 5327


🏁 Script executed:

#!/bin/sh
set -eu

printf '\n== docs/equalizer.md lines 128-150 ==\n'
sed -n '128,150p' docs/equalizer.md

printf '\n== docs/equalizer.md lines 236-250 ==\n'
sed -n '236,250p' docs/equalizer.md

printf '\n== docs/equalizer.md lines 510-522 ==\n'
sed -n '510,522p' docs/equalizer.md

Repository: jm2/tributary

Length of output: 3551


🌐 Web query:

GStreamer playbin3 audio-filter property GstBin caps property ghost pads capsfilter

💡 Result:

In GStreamer's playbin3, the "audio-filter" property allows you to inject one or more custom audio processing elements into the audio sink chain [1][2]. If you need to apply multiple elements (such as a capsfilter combined with other processing elements) via this property, you should wrap those elements within a GstBin [3][4]. To achieve this: 1. Create a GstBin: Assemble your desired processing chain, such as "capsfilter caps=audio/x-raw,...! your-element", using gst_parse_bin_from_description or manual element construction [3]. 2. Manage Pads with Ghost Pads: Because a bin does not have pads of its own, you use ghost pads to expose the sink and source pads of the internal elements to the bin level [5][6]. When you build a bin manually, you add ghost pads to the bin that target the internal elements' pads so that playbin3 can transparently link to them [5][6]. 3. Apply the Bin: Set the completed GstBin as the value for the "audio-filter" property of the playbin3 instance using g_object_set [3][4]. Using this approach, you treat the entire collection of elements as a single GstElement, which satisfies the "audio-filter" property requirement while allowing complex configurations like forced caps or specific filter sequences [3][4].

Citations:


🌐 Web query:

GStreamer playbin3 negotiation pad activate FALSE capsfilter ghost pad error bus

💡 Result:

In GStreamer, encountering "pad activate FALSE" errors related to playbin3, capsfilter, or ghost pads typically indicates a failure during the pipeline's negotiation or state-change process [1][2]. When a GStreamer pipeline changes state (e.g., READY to PAUSED), it activates pads to prepare for data flow [1]. If playbin3 attempts to activate a path that fails—often due to incompatible caps between a source (like decodebin) and a sink (via a capsfilter or ghost pad)—the negotiation fails [3][2]. Common causes and debugging steps include: 1. Negotiation Mismatches: The most frequent cause is that downstream elements (sinks) cannot accept the caps proposed by upstream elements [3][2]. If you are using a capsfilter, ensure its caps are compatible with the actual stream [2]. If the filter is too restrictive or specifies formats the sink cannot handle, negotiation will fail [2]. 2. Ghost Pad Issues: playbin3 heavily relies on ghost pads to route data to sinks [4][5]. If a ghost pad is created but not properly linked to a target, or if the target sink is not ready to receive data, the pipeline may stall or report activation errors [6]. Ensure that any custom bin or sink structure correctly links internal pads to the ghost pad before the pipeline enters the PAUSED state [4][6]. 3. Debugging: - Use the GST_DEBUG environment variable to isolate the failure. Setting GST_DEBUG=*:3,negotiation:5,pad:5 often reveals exactly which pad fails to negotiate and why [2]. - Verify if the capsfilter is truly necessary. Sometimes replacing it with a capability-aware element or adjusting the video-sink / audio-sink property of playbin3 is safer than forcing caps manually [5]. - Check if your sink supports the format by testing the pipeline segment with gst-launch-1.0 to ensure the sink is not rejecting the caps even without the capsfilter [3]. If you see "pad activate FALSE," it signifies that the core is deactivating a pad after a failed initialization or state transition attempt [1][7]. Focus your investigation on the caps query logs (using GST_DEBUG) immediately preceding this error to identify which element is rejecting the proposed format [2][8].

Citations:


🌐 Web query:

site:gstreamer.freedesktop.org playbin3 audio-filter property GstElement GstBin ghost pads

💡 Result:

In GStreamer, playbin3 uses the audio-filter property to allow applications to insert custom audio processing elements or bins into the audio pipeline [1][2]. The audio-filter property accepts a GstElement [1][3]. If you wish to insert a complex chain of elements rather than a single element, you can create a GstBin that encapsulates those elements [4][5]. To make this GstBin act like a single element that playbin3 can utilize, you use GstGhostPads [5][6]. A GhostPad acts as a proxy for a pad on one of the internal elements within the bin, effectively exposing it on the surface of the bin itself [7][6]. Key implementation steps for this approach: 1. Create a GstBin container [5]. 2. Add your desired audio processing elements into the bin and link them [4][5]. 3. Identify the sink pad of the first element and the source pad of the last element in your chain [4]. 4. Create sink and source GstGhostPads for the bin, pointing them to the corresponding internal sink and source pads, and add these ghost pads to the bin [4][6]. 5. Assign this custom GstBin to the audio-filter property of playbin3 [1][2]. By doing this, the bin appears to playbin3 as a standard element with valid input and output pads, allowing the playback pipeline to integrate your custom processing chain seamlessly [4][6].

Citations:


Use the internal capsfilter/ghost-pad contract here. Don’t describe the GstBin itself as carrying a caps property or negotiating through sink-pad activate; if setup fails, frame it as a link/negotiation failure that falls back to passthrough.

🤖 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/equalizer.md` around lines 101 - 110, Revise the equalizer documentation
to describe negotiation through the internal capsfilter and ghost-pad contract,
not a caps property on the GstBin or sink-pad activate behavior. State that
setup failures are link/negotiation failures, the bin is not inserted, and the
pipeline falls back to the existing passthrough layout without
element-by-element fallback or partial insertion.

Comment thread docs/equalizer.md
Comment on lines +260 to +276
The *buffer-boundary transaction* the spec requires is a three-step sequence on the application
side:

1. Capture the new band vector and preamp into a single typed struct (`EqSettings`).
2. Wrap the property writes in `g_object_freeze_notify` / `g_object_thaw_notify` on each
affected element (`equalizer-10bands` for the ten bands, `volume` for the preamp). Inside
the freeze, each `g_object_set` only mutates the element's internal state; the
`properties-changed` notification is suppressed until `thaw_notify` returns. The bus sees
**one** `properties-changed` notification per element per transaction, not eleven.
3. Wait for the next `GST_MESSAGE_ELEMENT` carrying a `GST_EVENT_CAPS` or `GST_EVENT_SEGMENT`
on the bus from `equalizer-10bands` or `volume`. That message marks the buffer boundary at
which the new coefficients are picked up by the audio thread; `thaw_notify` returning
*before* the buffer-boundary message is published does not mean the new coefficients have
yet been read by the audio thread — it only means the property state is now visible to
readers. Single-band writes skip the freeze/thaw wrapper and skip the boundary wait; only
multi-property transactions (preamp changes, preset loads, multi-band batched edits)
require it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/sh
set -eu
rg -n 'GST_MESSAGE_ELEMENT|GST_EVENT_CAPS|GST_EVENT_SEGMENT|properties-changed' .

Repository: jm2/tributary

Length of output: 834


🏁 Script executed:

#!/bin/sh
set -eu

printf '\n== docs/equalizer.md lines 240-290 ==\n'
sed -n '240,290p' docs/equalizer.md

printf '\n== docs/equalizer.md lines 470-510 ==\n'
sed -n '470,510p' docs/equalizer.md

printf '\n== search for boundary / acknowledgement terminology ==\n'
rg -n 'boundary|acknowledg|properties-changed|freeze_notify|thaw_notify|GST_MESSAGE_ELEMENT|GST_EVENT_CAPS|GST_EVENT_SEGMENT' docs .

Repository: jm2/tributary

Length of output: 50370


🏁 Script executed:

#!/bin/sh
set -eu

printf '\n== search repo for custom bus messages / app messages around equalizer ==\n'
rg -n 'new_custom|gst_message_new|gst_bus_post|GstMessage|message.*equalizer|equalizer.*message|properties-changed' .

printf '\n== search for seqno / generation / ack terminology ==\n'
rg -n 'seqno|generation|acknowledg|probe|pad probe' docs .

Repository: jm2/tributary

Length of output: 50370


🏁 Script executed:

#!/bin/sh
set -eu

printf '\n== files mentioning equalizer-10bands / volume ==\n'
rg -n 'equalizer-10bands|volume' .

printf '\n== list docs/equalizer.md around the table entry at line 490 ==\n'
sed -n '486,494p' docs/equalizer.md

Repository: jm2/tributary

Length of output: 50369


🏁 Script executed:

#!/bin/sh
set -eu

printf '\n== search for equalizer runtime / implementation files ==\n'
git ls-files | rg 'equalizer|gstreamer|gst|audio|volume'

printf '\n== search for property-change notifications or bus posting code ==\n'
rg -n 'notify::|g_object_freeze_notify|g_object_thaw_notify|gst_bus_post|gst_message_new|properties-changed|GST_MESSAGE_ELEMENT|GST_EVENT_CAPS|GST_EVENT_SEGMENT|g_signal_emit' .

Repository: jm2/tributary

Length of output: 8876


🌐 Web query:

GStreamer GST_EVENT_CAPS GST_EVENT_SEGMENT are pad events not GstMessage on the bus properties-changed notify freeze_notify

💡 Result:

You are correct: GST_EVENT_CAPS and GST_EVENT_SEGMENT are GstEvent types, not GstMessage types [1][2]. In GStreamer, these two mechanisms serve distinct purposes: 1. GstEvent (GST_EVENT_CAPS, GST_EVENT_SEGMENT): These are pad events that travel through the pipeline along with the data flow [3][2]. - They are processed by elements via a sink-event function [3][4]. - They are serialized with the buffer flow, meaning they arrive at the element in the exact order relative to the buffers they describe [2][4]. - GST_EVENT_CAPS informs elements about the format of subsequent buffers [1][2]. - GST_EVENT_SEGMENT provides timing information for subsequent buffers [2][4]. 2. GstMessage: These are messages posted by elements to the GstBus to communicate with the application [5]. - They are not part of the data flow and are used for asynchronous notifications (e.g., state changes, errors, EOS, or specific application-requested information) [5]. Regarding properties-changed, notify, and freeze_notify: - Notify and Deep-Notify: These are GObject signals (inherited by GstObject) used to indicate that an object's property has changed [6][7]. They are not GStreamer events or messages in the same sense as the pad events above, though you can configure an element to post a message to the bus when a property changes using specialized API calls like addPropertyNotifyWatch [8][9]. - Freeze_notify: This is a mechanism to temporarily suppress the emission of notify signals for an object, which is useful when updating multiple properties at once to avoid unnecessary overhead or intermediate state notifications [6][10]. In summary, GST_EVENT_CAPS and GST_EVENT_SEGMENT are serialized pad events for in-band control of the data stream, while the property-related signals are GObject-level notifications for tracking object state changes [3][2][6].

Citations:


Define a real buffer-boundary ack here. g_object_freeze_notify() only coalesces notify signals, and GST_EVENT_CAPS / GST_EVENT_SEGMENT are pad events, not bus messages. If this boundary must be observable on the bus, use an explicit GstMessage, pad probe, or seqno/generation ack instead.

🤖 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/equalizer.md` around lines 260 - 276, Replace the buffer-boundary
acknowledgement described in the documentation around the “buffer-boundary
transaction” with a real observable mechanism: use an explicit GstMessage, pad
probe, or sequence-number/generation acknowledgement instead of relying on
g_object_freeze_notify or GST_EVENT_CAPS/GST_EVENT_SEGMENT bus messages. Update
the three-step sequence and multi-property transaction guidance to describe the
selected acknowledgement accurately.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant