Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Client/Constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,18 @@ namespace V2Command
inline constexpr unsigned char NCASM_TYPE_XM6 = 0x19;
// Ten-band equalizer inquired type (WH-1000XM6): 56/57/58 04, 10 raw bands 0..12, no clear bass.
inline constexpr unsigned char EQ_TEN_BAND = 0x04;
// EQ + ULT mode inquired type (ULT WEAR / WH-ULT900N): the ULT button's bass mode lives next to the EQ.
// GET 56 03 -> RET 57 03 <preset> <ult 00=off 01=ULT1 02=ULT2> <count=06> <clear bass +10> <400..16k +10>
// SET 58 03 <preset> <ult> <count> <band bytes...>
// Framing from Sound Connect's EqEbbInquiredType::PRESET_EQ_AND_ULT_MODE / EqParamUltMode, as generated in
// mos9527/SonyHeadphonesClient (libmdr ProtocolV2T1.hpp). Only sent after the device answered the GET.
// Verified on a ULT WEAR (fw 2.1.0): 56/58 03 use the usual clear-bass-first order, but this device's 00
// form is one slot off. Its 57 00 reported <x> <clear bass> <400> <1k> <2.5k> <6.3k> (57 03 read
// 57 03 a0 01 06 11 0c 08 0b 0d 0f while 57 00 decoded "400..16k" as +7 +2 -2 +1 +3), the 58 00 clear-bass
// slot changed nothing, and writing the sixth 58 03 byte moved the treble. So on ULT devices the EQ is
// read and written through 56/58 03 only (presets still use 58 00 <preset> 00).
inline constexpr unsigned char EQ_ULT_MODE = 0x03;
inline constexpr unsigned char EQ_ULT_BAND_COUNT = 0x06; // five bands + clear bass
// INIT_REPLY layout: 01 <type> <protocol version, 4 bytes BE> <table1: 00=yes> <table2: 00=yes>
inline constexpr size_t INIT_REPLY_TABLE2_INDEX = 7;
// General settings: up to four device-defined boolean/list settings in slots D1..D4. Which slot holds
Expand Down Expand Up @@ -185,6 +197,14 @@ enum class EQ_PRESET : unsigned char
MANUAL = 0xa0
};

// ULT bass mode (EqUltModeStatus), the same three states the headset's ULT button cycles through.
enum class ULT_MODE : unsigned char
{
OFF = 0x00,
ULT1 = 0x01,
ULT2 = 0x02
};

#define APP_NAME "SonyBridge " __HEADPHONES_APP_VERSION__
#define APP_NAME_W (L"" APP_NAME)

Expand Down
80 changes: 79 additions & 1 deletion Client/CrossPlatformGUI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ bool CrossPlatformGUI::performGUIPass()
this->_drawASMControls();
if (this->_isV2())
{
this->_drawUltMode();
this->_drawEqualizer();
this->_drawDsee();
}
Expand Down Expand Up @@ -228,6 +229,15 @@ void CrossPlatformGUI::_pumpConnectionState()
this->_uiAutoPowerOff = this->_headphones.getAutoPowerOff();
this->_uiSpeakToChat = this->_headphones.getSpeakToChat();
this->_uiAdaptiveVolume = this->_headphones.getAdaptiveVolume();
this->_uiUltMode = (int)this->_headphones.getUltMode();
// On ULT devices the probe's 57 03 read is the first to carry the real clear bass (57 00 doesn't).
if (this->_headphones.hasUltMode() && !this->_featureCommandFuture.valid())
{
this->_uiEqPreset = (int)(unsigned char)this->_headphones.getEqualizerPreset();
for (int i = 0; i < 5; ++i)
this->_uiEqBands[i] = this->_headphones.getEqualizerBand(i);
this->_uiClearBass = this->_headphones.getClearBass();
}
}

// Poll the button-changeable ASM state so the app reflects changes made on the headphone itself.
Expand All @@ -242,6 +252,13 @@ void CrossPlatformGUI::_pumpConnectionState()
if (lvl > 0) this->_uiAsmLevel = lvl;
this->_uiFocusOnVoice = this->_headphones.getFocusOnVoice();
}
// The ULT button changes ULT mode on the headset; don't let a read that predates a pending write win.
if (!this->_featureCommandFuture.valid() && !this->_ultModeNeedsSync)
this->_uiUltMode = (int)this->_headphones.getUltMode();
// The same read carries clear bass (e.g. changed in the phone app); leave it alone mid-drag or mid-write.
if (this->_headphones.hasUltClearBass() && !this->_featureCommandFuture.valid() && !this->_clearBassDirty
&& !ImGui::IsAnyItemActive())
this->_uiClearBass = this->_headphones.getClearBass();
}
if (!this->_pollFuture.valid() && ++this->_pollCounter >= DYNAMIC_POLL_FRAMES)
{
Expand All @@ -260,6 +277,11 @@ void CrossPlatformGUI::_pumpConnectionState()

this->_pollFuture.setFromAsync([this, withBattery]() {
this->_headphones.requestAmbientState();
// ULT devices only: the probe (phase 2) decides, so other headsets never see this inquiry.
if (this->_headphones.hasUltMode())
{
try { this->_headphones.requestUltMode(); } catch (const std::exception&) {}
}
// Best effort: a device that doesn't answer the battery inquiry must not kill the poll.
if (withBattery)
{
Expand All @@ -282,6 +304,7 @@ void CrossPlatformGUI::_syncUIFromHeadphones()
this->_uiEqBands[i] = this->_headphones.getEqualizerBand(i);
this->_uiClearBass = this->_headphones.getClearBass();
this->_uiDsee = this->_headphones.getDsee();
this->_uiUltMode = (int)this->_headphones.getUltMode();

this->_uiAutoPowerOff = this->_headphones.getAutoPowerOff();
this->_uiSpeakToChat = this->_headphones.getSpeakToChat();
Expand Down Expand Up @@ -314,6 +337,9 @@ const CrossPlatformGUI::DeviceTexture& CrossPlatformGUI::_deviceTexture(const st
std::string slug = name;
std::transform(slug.begin(), slug.end(), slug.begin(), [](unsigned char ch) { return (char)std::tolower(ch); });
std::replace(slug.begin(), slug.end(), ' ', '-');
// The ULT WEAR (WH-ULT900N) advertises itself as "Sony ULT WEAR", not by model number.
if (slug.find("ult-wear") != std::string::npos || slug.find("ult900") != std::string::npos)
slug = "wh-ult900n";

auto it = this->_deviceTextures.find(slug);
if (it != this->_deviceTextures.end())
Expand Down Expand Up @@ -464,6 +490,44 @@ void CrossPlatformGUI::_drawASMControls()
this->_endCard();
}

void CrossPlatformGUI::_drawUltMode()
{
// Only shown once probeCapabilities() saw the headset answer the ULT read (ULT WEAR).
if (!this->_headphones.hasUltMode()) return;
if (!this->_beginCard("##ult")) { this->_endCard(); return; }
// Sound Connect's "Bass Control" on the ULT WEAR: the ULT mode (same as the ULT button) plus Clear Bass.
this->_cardTitle("Bass Control");
ImGui::TextDisabled("ULT mode, same as the ULT button");

static const struct { const char* name; int val; } modes[] = { { "Off", 0 }, { "ULT 1", 1 }, { "ULT 2", 2 } };
const float spacing = ImGui::GetStyle().ItemSpacing.x;
const float cellW = (ImGui::GetContentRegionAvail().x - spacing * 2.f) / 3.f;
for (int i = 0; i < 3; ++i)
{
if (i) ImGui::SameLine();
bool sel = this->_uiUltMode == modes[i].val;
if (sel) ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyle().Colors[ImGuiCol_ButtonActive]);
if (ImGui::Button(modes[i].name, ImVec2(cellW, 0)) && !sel)
{
this->_uiUltMode = modes[i].val;
this->_ultModeNeedsSync = true;
ULT_MODE mode = (ULT_MODE)modes[i].val;
this->_sendFeatureCommand([this, mode]() { this->_headphones.setUltMode(mode); });
}
if (sel) ImGui::PopStyleColor();
}

if (this->_headphones.hasUltClearBass())
{
ImGui::Spacing();
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
if (ImGui::SliderInt("##clearbass", &this->_uiClearBass, -10, 10, "Clear Bass %+d"))
this->_clearBassDirty = true;
}

this->_endCard();
}

void CrossPlatformGUI::_drawEqualizer()
{
if (!this->_beginCard("##eq")) { this->_endCard(); return; }
Expand Down Expand Up @@ -500,7 +564,8 @@ void CrossPlatformGUI::_drawEqualizer()
int* values[] = { &this->_uiClearBass, &this->_uiEqBands[0], &this->_uiEqBands[1],
&this->_uiEqBands[2], &this->_uiEqBands[3], &this->_uiEqBands[4] };
bool changed = false;
for (int i = 0; i < 6; ++i)
// On the ULT WEAR clear bass sits in the Bass Control card instead, as in Sony's app.
for (int i = this->_headphones.hasUltClearBass() ? 1 : 0; i < 6; ++i)
{
std::string id = "##eqband" + std::to_string(i);
std::string fmt = std::string(bandNames[i]) + " %+d";
Expand Down Expand Up @@ -618,6 +683,19 @@ void CrossPlatformGUI::_sendPendingASMChanges()
this->_uiClearBass = this->_headphones.getClearBass();
}
}
// A ULT click is synced back once no feature write is in flight: on success the device's mode is the one
// clicked, on failure (or a click dropped while the worker was busy) it's what the headset really has.
if (this->_clearBassDirty && !this->_featureCommandFuture.valid())
{
this->_clearBassDirty = false;
int cb = this->_uiClearBass;
this->_sendFeatureCommand([this, cb]() { this->_headphones.setClearBass(cb); });
}
if (this->_ultModeNeedsSync && !this->_featureCommandFuture.valid())
{
this->_ultModeNeedsSync = false;
this->_uiUltMode = (int)this->_headphones.getUltMode();
}

if (this->_sendCommandFuture.valid() && this->_sendCommandFuture.ready())
{
Expand Down
8 changes: 8 additions & 0 deletions Client/CrossPlatformGUI.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class CrossPlatformGUI
void _drawDeviceDiscovery();
void _drawStatusHeader();
void _drawASMControls();
void _drawUltMode();
void _drawEqualizer();
void _drawDsee();
void _drawOptionalFeatures();
Expand Down Expand Up @@ -98,6 +99,13 @@ class CrossPlatformGUI
// that preset once it completes instead of keeping the previous preset's.
bool _eqBandsNeedSync = false;
bool _uiDsee = false;
int _uiUltMode = 0; // 0 = Off, 1 = ULT 1, 2 = ULT 2
// Set when a ULT write was requested, so the control takes the device's value once the feature worker
// is done (or immediately falls back to it if the click was dropped because the worker was busy).
bool _ultModeNeedsSync = false;
// Set when the Clear Bass slider moved and the value hasn't been handed to the worker yet; the latest value
// is sent as soon as the worker is free, so the last position of a drag always reaches the headset.
bool _clearBassDirty = false;
int _uiAutoPowerOff = 0;
bool _uiSpeakToChat = false;
bool _uiAdaptiveVolume = false;
Expand Down
Loading