Skip to content

Change default delay for chromecast#2858

Closed
HarvsG wants to merge 1 commit intomusic-assistant:devfrom
HarvsG:patch-1
Closed

Change default delay for chromecast#2858
HarvsG wants to merge 1 commit intomusic-assistant:devfrom
HarvsG:patch-1

Conversation

@HarvsG
Copy link
Copy Markdown

@HarvsG HarvsG commented Dec 19, 2025

Before #2791, 300 and 350 were used.

On

  • Google Inc. / Google Home Mini (first gen) approx -330ms
  • KevinA has tested the Google Inc. / Google Nest Mini using his microphone cross-correlation and found -427ms is perfect
  • Google Inc. / Chromecast Audio approx -330 sounds best
  • On my Google Inc. / Google Nest Hub about -145 sounds best (confirmed by 1 other user)

I think other users should confirm before merge

@HarvsG
Copy link
Copy Markdown
Author

HarvsG commented Dec 19, 2025

I wonder if we should create a dictionary of model_name and default delays...

Haven't found a list of model names yet.. https://github.com/home-assistant-libs/pychromecast/blob/master/pychromecast/models.py

@marcelveldt
Copy link
Copy Markdown
Member

I think adding a mapping table to model and a default value for the sync delay would make a lot of sense.
It would save a lot of people work finding the initial value. Only thing is what we're comparing it against ?
What is our reference here ? The VPE ? Or the servers system clock in an ideal world.

If one person compares it against a web browser with a wrong offset then the default will already not work.
So we should define some neutral territory to which we base these default offsets.

@marcelveldt
Copy link
Copy Markdown
Member

@kahrendt @maximmaxim345 FYI, any thoughts ?

@HarvsG
Copy link
Copy Markdown
Author

HarvsG commented Dec 20, 2025

Only thing is what we're comparing it against ? What is our reference here ? The VPE ? Or the servers system clock in an ideal world.

Agreed, to seems odd that so many have a negative value. It makes me wonder wether the reference (?VPE) has an atypically large delay

@kahrendt
Copy link
Copy Markdown

kahrendt commented Dec 20, 2025

A database seems good, but I'm not fully convinced that even the same two devices types have the same delay. I got one of my Nest Minis pretty close to being in sync now with -484 ms. Using that exact same value on a different Nest Mini had it way off (though I was running a slightly older MA beta, and I think Maxim updated some of that code since then. I'll test it more today on the latest version once everyone in the house wakes up).

Sign conventions are always annoying and I tend to get confused! I believe a negative value means we tell the Chromecast to play the audio earlier. In my case, it takes 484 ms from when we tell the Chromecast audio to play to when it is actually played. So, it's not that the VPE has a delay, it's the Chromecast. Maxim should correct me if I have this backwards!

@kahrendt
Copy link
Copy Markdown

I tested it more and it seems like the same offset works well for all my Nest Minis, so I must have been encountering a bug that has since been fixed when I tested yesterday with an older MA beta. So, I think a database would be a great idea in the long run. I'm hopeful that I can get my offset calculator that uses a microphone a bit more consistent so it's really easy for people to determine this.

@HarvsG
Copy link
Copy Markdown
Author

HarvsG commented Dec 23, 2025

Is using manufacturer and model from Device Info (the two only non-optional variables) enough to identify devices for this purpose?
https://github.com/music-assistant/models/blob/8fd3ea8cb6cf756dc134d6e592cc441d3dfcb11b/music_assistant_models/player.py#L19

Here's some gemini code, I went type safe as nested maps/dicts in python spook me

Dataclass version
import logging
from dataclasses import dataclass, field
from typing import Dict, Optional

@dataclass(frozen=True)
class ModelProfile:
    """Specific configuration for a hardware model."""
    name: str
    delay_ms: int

@dataclass(frozen=True)
class ManufacturerProfile:
    """Manufacturer settings with a baseline default."""
    name: str
    default_delay_ms: int
    models: Dict[str, ModelProfile] = field(default_factory=dict)

@dataclass(frozen=True)
class HardwareDelayMap:
    """A frozen registry that handles its own resolution logic."""
    profiles: Dict[str, ManufacturerProfile]
    global_fallback_delay: int = 50
    issue_url: str = "https://github.com/your-repo/issues"

    def get_delay(self, manufacturer: str, model: str, logger: Optional[logging.Logger] = None) -> int:
        """
        Resolves the sync delay. If a default is used, warns the user about 
        potential sync issues and requests feedback.
        """
        # Fallback to a dummy logger if none provided to avoid AttributeErrors
        log = logger or logging.getLogger(__name__)
        
        manuf_profile = self.profiles.get(manufacturer)
        
        # 1. Unknown Manufacturer
        if not manuf_profile:
            log.warning(
                "Manufacturer '%s' is not in our database. Using global fallback of %dms. "
                "You may experience audio sync issues. If you find the perfect sync value, "
                "please create an issue at: %s", 
                manufacturer, self.global_fallback_delay, self.issue_url
            )
            return self.global_fallback_delay

        # 2. Match specific Model
        model_profile = manuf_profile.models.get(model)
        if model_profile:
            log.debug(
                "Exact sync match found: %s %s = %dms", 
                manuf_profile.name, model_profile.name, model_profile.delay_ms
            )
            return model_profile.delay_ms

        # 3. Fallback to Manufacturer default
        log.warning(
            "Model '%s' not found for %s. Falling back to manufacturer default: %dms. "
            "Audio may be slightly out of sync. Please report the ideal delay at: %s", 
            model, manuf_profile.name, manuf_profile.default_delay_ms, self.issue_url
        )
        return manuf_profile.default_delay_ms

# --- Usage ---

# Define the Registry
DELAY_REGISTRY = HardwareDelayMap(
    profiles={
        "Google Inc.": ManufacturerProfile(
            name="Google Inc.",
            default_delay_ms=-330,
            models={
                "Google Nest Mini": ModelProfile(name="Google Nest Mini", delay_ms=-427),
            }
        )
    }
)
Heres a less strongly typed version
import logging
from typing import Dict, Optional

class HardwareDelayMap:
    def __init__(self, logger: Optional[logging.Logger] = None) -> None:
        self.logger: logging.Logger = logger or logging.getLogger(__name__)
        
        # Structure: { manufacturer_name: { "default": int, model_name: int } }
        self._profiles: Dict[str, Dict[str, int]] = {
            "Sony": {
                "default": 100,
                "WH-1000XM4": 150,
                "HT-G700": 40,
            },
            "Bose": {
                "default": 80,
                "QC45": 160,
            },
            "Samsung": {
                "default": 30,
                "HW-Q950A": 0,
            }
        }
        
        self.global_fallback_delay: int = 50
        self.issue_url: str = "https://github.com/your-repo/issues"

    def get_delay(self, manufacturer: str, model: str) -> int:
        """
        Resolves the sync delay with a clear fallback hierarchy.
        """
        manufacturer_map = self._profiles.get(manufacturer)

        # 1. Unknown Manufacturer
        if manufacturer_map is None:
            self.logger.warning(
                "Manufacturer '%s' not recognized. Using global fallback: %dms. "
                "You may experience sync issues. Report better values at: %s",
                manufacturer, self.global_fallback_delay, self.issue_url
            )
            return self.global_fallback_delay

        # 2. Match specific Model (excluding the reserved 'default' key)
        if model != "default" and model in manufacturer_map:
            delay = manufacturer_map[model]
            self.logger.debug("Sync match found: %s %s = %dms", manufacturer, model, delay)
            return delay

        # 3. Fallback to Manufacturer default
        # We use .get() here to safely handle cases where the 'default' key might be missing
        manufacturer_default = manufacturer_map.get("default", self.global_fallback_delay)
        
        self.logger.warning(
            "Model '%s' not found for manufacturer '%s'. Using default: %dms. "
            "You may have sync issues. Report better values at: %s",
            model, manufacturer, manufacturer_default, self.issue_url
        )
        return manufacturer_default

@HarvsG
Copy link
Copy Markdown
Author

HarvsG commented Dec 28, 2025

Just waiting for kahrendt's microphone delay tester to launch then will update this PR with some measured values. Leaning towards the less strongly typed implementation as at the moment the dict is static and private.

@HarvsG
Copy link
Copy Markdown
Author

HarvsG commented Jan 12, 2026

Ok, I've done some testing using: Sendspin/sendspin-cli#27
Relative to a mic calibrated against the VAPE, I found the following.

  1. Google Inc. / Google Home Mini (first gen) -330ms
  2. Google Inc. / Chromecast Audio approx -335 (This goes via line-level to a set of studio monitors which I don't think adds any delay but I'm not sure I can be absolutely certain)
  3. On my Google Inc. / Google Nest Hub about -188
  4. I tested Google Inc. / Chromecast but got very poor sendspin performance Discord Link to video

N.B KevinA has tested the Google Inc. / Google Nest Mini using his microphone cross-correlation and found -427ms is perfect

@MarvinSchenkel MarvinSchenkel marked this pull request as draft January 14, 2026 07:46
@OzGav
Copy link
Copy Markdown
Contributor

OzGav commented Mar 25, 2026

@HarvsG any progress here?

@marcelveldt
Copy link
Copy Markdown
Member

superseded by #3689

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants