CMF Buds 2a: Add equalizer and ultra bass

This commit is contained in:
José Rebelo
2026-05-03 16:44:35 +01:00
parent 72dcc1db2c
commit c40d3e63a8
11 changed files with 380 additions and 2 deletions
@@ -374,6 +374,9 @@ public class DeviceSettingsPreferenceConst {
public static final String PREF_NOTHING_EAR1_INEAR = "pref_nothing_inear_detection";
public static final String PREF_NOTHING_EAR1_AUDIOMODE = "pref_nothing_audiomode";
public static final String PREF_NOTHING_EAR1_ULTRA_BASS_ENABLED = "pref_nothing_ultra_bass_enabled";
public static final String PREF_NOTHING_EAR1_ULTRA_BASS_LEVEL = "pref_nothing_ultra_bass_level";
public static final String PREF_HEADPHONES_EQUALIZER = "pref_headphones_equalizer";
public static final String PREF_HEADPHONES_LOW_LATENCY = "pref_headphones_low_latency";
@@ -18,6 +18,9 @@ package nodomain.freeyourgadget.gadgetbridge.devices.nothing;
import androidx.annotation.NonNull;
import java.util.Collections;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
@@ -66,6 +69,12 @@ public abstract class AbstractEarCoordinator extends AbstractBLClassicDeviceCoor
public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings();
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_nothing_ear1);
if (!getEqualizerPresets().isEmpty()) {
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_nothing_equalizer);
}
if (supportsUltraBass()) {
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_nothing_ultra_bass);
}
if (supportsLowLatency()) {
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_headphones_low_latency);
}
@@ -100,4 +109,12 @@ public abstract class AbstractEarCoordinator extends AbstractBLClassicDeviceCoor
public boolean supportsLowLatency() {
return false;
}
public List<NothingEqualizer> getEqualizerPresets() {
return Collections.emptyList();
}
public boolean supportsUltraBass() {
return false;
}
}
@@ -16,6 +16,9 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.nothing;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.R;
@@ -70,4 +73,21 @@ public class CmfBuds2aCoordinator extends AbstractEarCoordinator {
public boolean supportsLowLatency() {
return true;
}
@Override
public List<NothingEqualizer> getEqualizerPresets() {
return Arrays.asList(
NothingEqualizer.POP,
NothingEqualizer.ROCK,
NothingEqualizer.ELECTRONIC,
NothingEqualizer.ENHANCE_VOCALS,
NothingEqualizer.CLASSICAL,
NothingEqualizer.DIRAC
);
}
@Override
public boolean supportsUltraBass() {
return true;
}
}
@@ -18,6 +18,7 @@ package nodomain.freeyourgadget.gadgetbridge.devices.nothing;
import android.os.Parcel;
import androidx.annotation.NonNull;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
@@ -32,7 +33,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpec
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class EarSettingsCustomizer implements DeviceSpecificSettingsCustomizer {
public static final Creator<EarSettingsCustomizer> CREATOR = new Creator<EarSettingsCustomizer>() {
public static final Creator<EarSettingsCustomizer> CREATOR = new Creator<>() {
@Override
public EarSettingsCustomizer createFromParcel(final Parcel in) {
return new EarSettingsCustomizer();
@@ -88,6 +89,33 @@ public class EarSettingsCustomizer implements DeviceSpecificSettingsCustomizer {
((ListPreference) audioModePref).setEntries(entries.toArray(new CharSequence[0]));
((ListPreference) audioModePref).setEntryValues(entryValues.toArray(new CharSequence[0]));
}
final Preference equalizerPref = handler.findPreference(DeviceSettingsPreferenceConst.PREF_HEADPHONES_EQUALIZER);
if (equalizerPref instanceof ListPreference listPreference) {
final List<NothingEqualizer> supportedPresets = earCoordinator.getEqualizerPresets();
if (supportedPresets.isEmpty()) {
listPreference.setVisible(false);
} else {
final CharSequence[] originalEntries = listPreference.getEntries();
final CharSequence[] originalEntryValues = listPreference.getEntryValues();
final List<CharSequence> entries = new ArrayList<>();
final List<CharSequence> entryValues = new ArrayList<>();
for (int i = 0; i < originalEntryValues.length; i++) {
final NothingEqualizer preset = NothingEqualizer.fromPreferenceValue(originalEntryValues[i].toString());
if (preset == null || !supportedPresets.contains(preset)) {
continue;
}
entries.add(originalEntries[i]);
entryValues.add(originalEntryValues[i]);
}
listPreference.setEntries(entries.toArray(new CharSequence[0]));
listPreference.setEntryValues(entryValues.toArray(new CharSequence[0]));
}
}
}
@Override
@@ -101,6 +129,6 @@ public class EarSettingsCustomizer implements DeviceSpecificSettingsCustomizer {
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
public void writeToParcel(@NonNull final Parcel dest, final int flags) {
}
}
@@ -0,0 +1,43 @@
package nodomain.freeyourgadget.gadgetbridge.devices.nothing;
public enum NothingEqualizer {
ROCK((byte) 0x01),
ELECTRONIC((byte) 0x02),
POP((byte) 0x03),
ENHANCE_VOCALS((byte) 0x04),
CLASSICAL((byte) 0x05),
DIRAC((byte) 0x07),
;
private final byte code;
NothingEqualizer(final byte code) {
this.code = code;
}
public byte getCode() {
return this.code;
}
public static NothingEqualizer fromCode(final byte code) {
for (NothingEqualizer value : NothingEqualizer.values()) {
if (value.getCode() == code) {
return value;
}
}
return null;
}
public static NothingEqualizer fromPreferenceValue(final String preferenceValue) {
if (preferenceValue == null) {
return null;
}
try {
return NothingEqualizer.valueOf(preferenceValue);
} catch (final IllegalArgumentException e) {
return null;
}
}
}
@@ -37,6 +37,7 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdateDevi
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.devices.nothing.AbstractEarCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.nothing.NothingEqualizer;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
import nodomain.freeyourgadget.gadgetbridge.service.AbstractHeadphoneBTBRDeviceSupport;
@@ -74,6 +75,12 @@ public class Ear1Support extends AbstractHeadphoneBTBRDeviceSupport {
sendCommand(builder, nothingProtocol.encodeFirmwareVersionReq());
sendCommand(builder, nothingProtocol.encodeBatteryStatusReq());
sendCommand(builder, nothingProtocol.encodeAudioModeStatusReq());
if (!getCoordinator().getEqualizerPresets().isEmpty()) {
sendCommand(builder, nothingProtocol.encodeEqualizerStatusReq());
}
if (getCoordinator().supportsUltraBass()) {
sendCommand(builder, nothingProtocol.encodeUltraBassStatusReq());
}
return builder;
}
@@ -135,6 +142,19 @@ public class Ear1Support extends AbstractHeadphoneBTBRDeviceSupport {
// OFF: 55 6001 40F0 0100 5C 02 28 9353
// OFF Response: 55 6001 4070 0000 5C FF FF95
break;
case DeviceSettingsPreferenceConst.PREF_HEADPHONES_EQUALIZER:
sendCommand("set equalizer", nothingProtocol.encodeEqualizer(prefs.getString(DeviceSettingsPreferenceConst.PREF_HEADPHONES_EQUALIZER, NothingEqualizer.DIRAC.name())));
break;
case DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_ULTRA_BASS_ENABLED:
case DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_ULTRA_BASS_LEVEL:
sendCommand(
"set ultra bass",
nothingProtocol.encodeUltraBass(
prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_ULTRA_BASS_ENABLED, false),
prefs.getInt(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_ULTRA_BASS_LEVEL, 2)
)
);
break;
default:
LOG.debug("CONFIG: " + config);
break;
@@ -166,6 +186,8 @@ public class Ear1Support extends AbstractHeadphoneBTBRDeviceSupport {
private static final short audio_mode_status = (short) 0xc01e;
private static final short audio_mode_status2 = (short) 0xe003;
private static final short low_latency = (short) 0xf040;
private static final short equalizer_status = (short) 0xc050;
private static final short ultra_bass_status = (short) 0xc04e;
private static final short unk_maybe_ack = (short) 0xf002;
private static final short unk_close_case = (short) 0xe002; //sent twice when the case is closed with earphones in
@@ -175,6 +197,8 @@ public class Ear1Support extends AbstractHeadphoneBTBRDeviceSupport {
private static final short in_ear_detection = (short) 0xf004;
private static final short in_ear_detection2 = (short) 0xc00e;
private static final short audio_mode = (short) 0xf00f;
private static final short equalizer = (short) 0xf01d;
private static final short ultra_bass = (short) 0xf051;
private final boolean incrementCounter;
private int messageCounter = 0x00;
@@ -259,6 +283,20 @@ public class Ear1Support extends AbstractHeadphoneBTBRDeviceSupport {
devEvts.add(handleInEarStatus(payload));
break;
case equalizer_status:
case equalizer:
if (payload.length > 0) {
devEvts.add(handleEqualizerStatus(payload));
}
break;
case ultra_bass_status:
case ultra_bass:
if (payload.length > 0) {
devEvts.add(handleUltraBassStatus(payload));
}
break;
case unk_maybe_ack:
LOG.debug("received ack");
break;
@@ -320,6 +358,14 @@ public class Ear1Support extends AbstractHeadphoneBTBRDeviceSupport {
return encodeMessage((short) 0x120, audio_mode_status, new byte[]{});
}
byte[] encodeEqualizerStatusReq() {
return encodeMessage((short) 0x120, equalizer_status, new byte[]{});
}
byte[] encodeUltraBassStatusReq() {
return encodeMessage((short) 0x120, ultra_bass_status, new byte[]{});
}
private GBDeviceEventVersionInfo handleFirmwareVersion(byte[] payload) {
GBDeviceEventVersionInfo evt = new GBDeviceEventVersionInfo();
evt.fwVersion = new String(payload);
@@ -351,6 +397,32 @@ public class Ear1Support extends AbstractHeadphoneBTBRDeviceSupport {
return preferencesEvent;
}
private GBDeviceEventUpdatePreferences handleEqualizerStatus(final byte[] payload) {
final GBDeviceEventUpdatePreferences preferencesEvent = new GBDeviceEventUpdatePreferences();
final NothingEqualizer equalizerPreset = NothingEqualizer.fromCode(payload[0]);
if (equalizerPreset == null) {
LOG.warn("Unknown equalizer code: {}", payload[0] & 0xff);
return preferencesEvent;
}
preferencesEvent.withPreference(DeviceSettingsPreferenceConst.PREF_HEADPHONES_EQUALIZER, equalizerPreset.name());
return preferencesEvent;
}
private GBDeviceEventUpdatePreferences handleUltraBassStatus(final byte[] payload) {
final GBDeviceEventUpdatePreferences preferencesEvent = new GBDeviceEventUpdatePreferences();
preferencesEvent.withPreference(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_ULTRA_BASS_ENABLED, payload[0] == 0x01);
if (payload.length >= 2) {
final int level = Math.max(1, Math.min(5, ((payload[1] & 0xff) + 1) / 2));
preferencesEvent.withPreference(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_ULTRA_BASS_LEVEL, level);
}
return preferencesEvent;
}
byte[] encodeInEarDetection(byte enabled) {
return encodeMessage((short) 0x120, in_ear_detection, new byte[]{0x01, 0x01, enabled});
}
@@ -372,6 +444,26 @@ public class Ear1Support extends AbstractHeadphoneBTBRDeviceSupport {
return encodeMessage((short) 0x120, low_latency, new byte[]{payload});
}
byte[] encodeEqualizer(final String desired) {
NothingEqualizer preset = NothingEqualizer.DIRAC;
final NothingEqualizer requested = NothingEqualizer.fromPreferenceValue(desired);
if (requested != null) {
preset = requested;
} else {
LOG.warn("Unknown equalizer preset: {}, using default", desired);
}
return encodeMessage((short) 0x120, equalizer, new byte[]{preset.getCode(), 0x00});
}
byte[] encodeUltraBass(final boolean enabled, final int level) {
final int clampedLevel = Math.max(1, Math.min(5, level));
final byte rawLevel = (byte) (clampedLevel * 2);
return encodeMessage((short) 0x120, ultra_bass, new byte[]{(byte) (enabled ? 0x01 : 0x00), rawLevel});
}
public byte[] encodeFindDevice(boolean start) {
byte payload = (byte) (start ? 0x01 : 0x00);
return encodeMessage((short) 0x120, find_device, new byte[]{payload});
+18
View File
@@ -3564,6 +3564,24 @@
<item>off</item>
</string-array>
<string-array name="nothing_ear1_equalizer_names">
<item>@string/nothing_equalizer_rock</item>
<item>@string/nothing_equalizer_electronic</item>
<item>@string/nothing_equalizer_pop</item>
<item>@string/nothing_equalizer_enhance_vocals</item>
<item>@string/nothing_equalizer_classical</item>
<item>@string/nothing_equalizer_dirac</item>
</string-array>
<string-array name="nothing_ear1_equalizer_values">
<item>ROCK</item>
<item>ELECTRONIC</item>
<item>POP</item>
<item>ENHANCE_VOCALS</item>
<item>CLASSICAL</item>
<item>DIRAC</item>
</string-array>
<string-array name="notification_list_is_blacklist_names">
<item>@string/pref_title_notification_use_as_deny</item>
<item>@string/pref_title_notification_use_as_allow</item>
+9
View File
@@ -3177,6 +3177,15 @@
<string name="nothing_prefs_inear_title">In-Ear detection</string>
<string name="prefs_in_ear_detection_summary">Play calls through your earbuds when they are in your ears</string>
<string name="nothing_prefs_audiomode_title">Audio mode</string>
<string name="nothing_prefs_ultra_bass_title">Ultra Bass</string>
<string name="nothing_prefs_ultra_bass_summary">Boost low frequencies for a stronger bass response</string>
<string name="nothing_prefs_ultra_bass_level_title">Ultra Bass Level</string>
<string name="nothing_equalizer_rock">Rock</string>
<string name="nothing_equalizer_electronic">Electronic</string>
<string name="nothing_equalizer_pop">Pop</string>
<string name="nothing_equalizer_enhance_vocals">Enhance vocals</string>
<string name="nothing_equalizer_classical">Classical</string>
<string name="nothing_equalizer_dirac">Dirac</string>
<string name="prefs_equalizer_preset">Equalizer Preset</string>
<string name="pref_title_equalizer_normal">Normal</string>
<string name="pref_title_equalizer_bass_boost">Bass boost</string>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<ListPreference
android:icon="@drawable/ic_equalizer"
android:entryValues="@array/nothing_ear1_equalizer_values"
android:entries="@array/nothing_ear1_equalizer_names"
android:key="pref_headphones_equalizer"
app:useSimpleSummaryProvider="true"
android:title="@string/prefs_equalizer_preset" />
</androidx.preference.PreferenceScreen>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<SwitchPreferenceCompat
android:defaultValue="false"
android:icon="@drawable/ic_speaker"
android:key="pref_nothing_ultra_bass"
android:layout="@layout/preference_checkbox"
android:summary="@string/nothing_prefs_ultra_bass_summary"
android:title="@string/nothing_prefs_ultra_bass_title" />
<SeekBarPreference
android:defaultValue="2"
android:dependency="pref_nothing_ultra_bass"
android:icon="@drawable/ic_speaker"
android:key="pref_nothing_ultra_bass_level"
android:max="5"
android:title="@string/nothing_prefs_ultra_bass_level_title"
app:min="1"
app:showSeekBarValue="true" />
</androidx.preference.PreferenceScreen>
@@ -0,0 +1,117 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.nothing;
import org.junit.Assert;
import org.junit.Test;
import java.util.Arrays;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.devices.nothing.NothingEqualizer;
import nodomain.freeyourgadget.gadgetbridge.test.TestBase;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import static nodomain.freeyourgadget.gadgetbridge.util.CheckSums.getCRC16ansi;
public class NothingProtocolTest extends TestBase {
@Test
public void testDecodeEqualizerAndUltraBassStatus() {
final Ear1Support.NothingProtocol protocol = new Ear1Support.NothingProtocol(true);
final GBDeviceEvent[] equalizerEvents = protocol.decodeResponse(GB.hexStringToByteArray("5560015040010060057f43"));
final GBDeviceEventUpdatePreferences equalizerUpdate = findEvent(equalizerEvents, GBDeviceEventUpdatePreferences.class);
Assert.assertNotNull(equalizerUpdate);
Assert.assertEquals(NothingEqualizer.CLASSICAL.name(), equalizerUpdate.preferences.get(DeviceSettingsPreferenceConst.PREF_HEADPHONES_EQUALIZER));
final GBDeviceEvent[] ultraBassEvents = protocol.decodeResponse(GB.hexStringToByteArray("5560014e4002001b00028071"));
final GBDeviceEventUpdatePreferences ultraBassUpdate = findEvent(ultraBassEvents, GBDeviceEventUpdatePreferences.class);
Assert.assertNotNull(ultraBassUpdate);
Assert.assertEquals(false, ultraBassUpdate.preferences.get(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_ULTRA_BASS_ENABLED));
Assert.assertEquals(1, ultraBassUpdate.preferences.get(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_ULTRA_BASS_LEVEL));
}
@Test
public void testEncodeEqualizerCommand() {
final Ear1Support.NothingProtocol protocol = new Ear1Support.NothingProtocol(true);
final byte[] packet = protocol.encodeEqualizer(NothingEqualizer.ELECTRONIC.name());
final byte[] expected = GB.hexStringToByteArray("5560011df00200140200ebcd");
assertMessageEquals(expected, packet);
}
@Test
public void testEncodeUltraBassCommand() {
final Ear1Support.NothingProtocol protocol = new Ear1Support.NothingProtocol(true);
final byte[] packet = protocol.encodeUltraBass(true, 2);
final byte[] expected = GB.hexStringToByteArray("55600151f0020070010426e5");
assertMessageEquals(expected, packet);
}
private void assertMessageEquals(final byte[] expected, final byte[] actual) {
Assert.assertNotNull(expected);
Assert.assertNotNull(actual);
Assert.assertTrue("message too short", expected.length >= 8 && actual.length >= 8);
Assert.assertEquals("sof differs", expected[0], actual[0]);
final int expectedControl = readLeShort(expected, 1);
final int actualControl = readLeShort(actual, 1);
Assert.assertEquals("control differs", expectedControl, actualControl);
final int expectedCommand = readLeShort(expected, 3);
final int actualCommand = readLeShort(actual, 3);
Assert.assertEquals("command differs", expectedCommand, actualCommand);
final int expectedPayloadLength = readLeShort(expected, 5);
final int actualPayloadLength = readLeShort(actual, 5);
Assert.assertEquals("payload length field differs", expectedPayloadLength, actualPayloadLength);
final boolean expectedCrc = (expectedControl & 0x20) != 0;
final boolean actualCrc = (actualControl & 0x20) != 0;
Assert.assertEquals("crc flag differs", expectedCrc, actualCrc);
final int expectedTotalLength = 8 + expectedPayloadLength + (expectedCrc ? 2 : 0);
final int actualTotalLength = 8 + actualPayloadLength + (actualCrc ? 2 : 0);
Assert.assertEquals("expected message has invalid size", expectedTotalLength, expected.length);
Assert.assertEquals("actual message has invalid size", actualTotalLength, actual.length);
// Counter byte at offset 7 is intentionally ignored.
Assert.assertArrayEquals(
"payload differs",
Arrays.copyOfRange(expected, 8, 8 + expectedPayloadLength),
Arrays.copyOfRange(actual, 8, 8 + actualPayloadLength)
);
assertCrcIsValid(expected, expectedCrc);
assertCrcIsValid(actual, actualCrc);
}
private static int readLeShort(final byte[] msg, final int offset) {
return (msg[offset] & 0xff) | ((msg[offset + 1] & 0xff) << 8);
}
private static void assertCrcIsValid(final byte[] msg, final boolean hasCrc) {
if (!hasCrc) {
return;
}
final int payloadLength = readLeShort(msg, 5);
final int crcOffset = 8 + payloadLength;
final int expectedCrc = readLeShort(msg, crcOffset);
final int actualCrc = getCRC16ansi(Arrays.copyOf(msg, crcOffset));
Assert.assertEquals("invalid crc", expectedCrc, actualCrc);
}
@SuppressWarnings("unchecked")
private static <T extends GBDeviceEvent> T findEvent(final GBDeviceEvent[] events, final Class<T> cls) {
for (GBDeviceEvent event : events) {
if (cls.isInstance(event)) {
return (T) event;
}
}
return null;
}
}