Skip to content

Commit

Permalink
Read back config from the device.
Browse files Browse the repository at this point in the history
  • Loading branch information
george-norton committed Jun 14, 2023
1 parent ad455bc commit 3e47c0a
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 35 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@ This is an Alpha test version of Ploopy Headphones Toolbox, it is pretty functio

Grab a firmware image from [here](https://github.com/george-norton/headphones/releases/tag/headphones-toolbox-alpha-v1) and flash it onto your DAC.
Grab a build of Ploopy Headphones Toolbox from [here](https://github.com/george-norton/headphones-toolbox/releases/tag/headphones-toolbox-alpha-v2) and install it on your PC.

If you are a Linux user you will need to set a udev rule to allow users to access the USB device. As root, run:
```
echo 'SUBSYSTEM=="usb", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="fedd", MODE="666"' > /etc/udev/rules.d/100-ploopy-headphones.rules`
udevadm control --reload-rules && udevadm trigger
```

If you are a MacOS user, you may need to `brew install libusb`. The application is not signed so you will see warning messages.

Run the application, it should detect your device. Click the + button to add a configuration, when you modify the filters the frequency response graph and the filters running on the device will change in real time. If you start playing some music, then change the filters you can hear the result instantly. **_WARNING:_ Keep the volume low, and be cautious making changes as filers can cause the sound to become very loud.**
If you come up with a config you like, click the save button in the top right to persist the configuration to flash memory.

Expand All @@ -31,9 +35,9 @@ You can export your faviourite configurations to JSON files and share them with
The application is currently in Alpha status. It is not feature complete, but it does implement quite a bit of useful stuff.

Missing functionality:
- Cannot read the configuration back from the device.
- Cannot confugure the PCM3060 filters.
- Not many errors are reported to the user.
- Does not report device information to the user (version numbers etc..)
- Does not validate the firmware version (should reject devices with newer than expected firmware)

Implemented functionality:
- Device discovery.
Expand All @@ -43,6 +47,8 @@ Implemented functionality:
- Save the configuration to flash.
- Load configs from flash.
- Import/Export configs for sharing.
- Read the configuration back from the device.
- Configure the PCM3060 filters.

Known issues:
- There is a burst of audio noise when when writing a config to flash, this seems to be due to the time it takes to write to flash. I turn the DAC off then back on to mask it, the slight pop sounds much better.
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/resources/configuration.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
],
"preprocessing": {
"preamp": -20,
"reverseStereo": false
"reverse_stereo": false
},
"codec": {
"oversampling": false,
Expand Down
67 changes: 53 additions & 14 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,24 @@ enum StructureTypes {
VersionStatus = 0x400,
}

enum FilterType {
Lowpass = 0,
Highpass,
BandpassSkirt,
BandpassPeak,
Notch,
Allpass,
Peaking,
LowShelf,
HighShelf
}

#[derive(Serialize, Deserialize, Default)]
struct Filter {
filter_type: String,
q: f64,
f0: f64,
db_gain: f64,

enabled: bool
}
#[derive(Serialize, Deserialize, Default)]
Expand Down Expand Up @@ -226,7 +237,7 @@ fn write_config(config: &str, connection_state: State<'_, Mutex<ConnectionState>
filter_payload.extend_from_slice(&filter.q.to_le_bytes());
}
}
preprocessing_payload.extend_from_slice(&cfg.preprocessing.preamp.to_le_bytes());
preprocessing_payload.extend_from_slice(&(cfg.preprocessing.preamp/100.0).to_le_bytes());
preprocessing_payload.push(cfg.preprocessing.reverse_stereo as u8);
preprocessing_payload.extend_from_slice(&[0u8; 3]);

Expand Down Expand Up @@ -273,33 +284,62 @@ fn save_config(connection_state: State<'_, Mutex<ConnectionState>>) -> Result<bo
}

#[tauri::command]
fn load_config(connection_state: State<'_, Mutex<ConnectionState>>) -> Result<bool, ()> {
fn load_config(connection_state: State<'_, Mutex<ConnectionState>>) -> Result<String, ()> {
let mut buf : Vec<u8> = Vec::new();
buf.extend_from_slice(&(StructureTypes::GetStoredConfiguration as u16).to_le_bytes());
buf.extend_from_slice(&(4u16).to_le_bytes());

match &send_cmd(connection_state, &buf) {
Ok(cfg) => {
let mut cur = Cursor::new(cfg);
//println!("Read the following cfg: {:02X?}", cfg);
let _result_type_val = cur.read_u16::<LittleEndian>().unwrap();
let result_length_val = cur.read_u16::<LittleEndian>().unwrap();
//println!("T: {} L: {}", _result_type_val, result_length_val);
let mut position = 4;
let mut cfg : Config = Default::default();
while position < result_length_val {
let type_val = cur.read_u16::<LittleEndian>().unwrap();
let length_val = cur.read_u16::<LittleEndian>().unwrap();
//println!("\tT: {} L: {}", type_val, length_val);
match type_val{
x if x == StructureTypes::PreProcessingConfiguration as u16 => {
cfg.preprocessing.preamp = cur.read_f64::<LittleEndian>().unwrap();
cfg.preprocessing.preamp = cur.read_f64::<LittleEndian>().unwrap() * 100.0;
cfg.preprocessing.reverse_stereo = cur.read_u8().unwrap() != 0;
cur.seek(SeekFrom::Current(3)); // reserved bytes
},
x if x == StructureTypes::FilterConfiguration as u16 => {
// TODO: Read the filters..
cur.seek(SeekFrom::Current((length_val-4).into()));
let end = cur.position() + (length_val-4) as u64;
while (cur.position() < end) {
let filter_type = cur.read_u8().unwrap();
let filter_type_str;
cur.seek(SeekFrom::Current(3)); // reserved bytes
let filter_args;

match filter_type {
x if x == FilterType::Lowpass as u8 => { filter_type_str = "lowpass"; filter_args = 2; },
x if x == FilterType::Highpass as u8 => { filter_type_str = "highpass"; filter_args = 2; },
x if x == FilterType::BandpassSkirt as u8 => { filter_type_str = "bandpass_skirt"; filter_args = 2; },
x if x == FilterType::BandpassPeak as u8 => { filter_type_str = "bandpass_peak"; filter_args = 2; },
x if x == FilterType::Notch as u8 => { filter_type_str = "notch"; filter_args = 2; },
x if x == FilterType::Allpass as u8 => { filter_type_str = "allpass"; filter_args = 2; },
x if x == FilterType::Peaking as u8 => { filter_type_str = "peaking"; filter_args = 3; },
x if x == FilterType::LowShelf as u8 => { filter_type_str = "lowshelf"; filter_args = 3; },
x if x == FilterType::HighShelf as u8 => { filter_type_str = "highshelf"; filter_args = 3; },
_ => return { println!("Unknown filter type {}", filter_type); Err(()) }
}
let f0 = cur.read_f64::<LittleEndian>().unwrap();
let db_gain;
if filter_args == 3 {
db_gain = cur.read_f64::<LittleEndian>().unwrap();
} else {
db_gain = 0.0;
}
let q = cur.read_f64::<LittleEndian>().unwrap();
cfg.filters.push(Filter { filter_type: filter_type_str.to_string(), f0, db_gain, q, enabled: true })
}

if cur.position() != end {
println!("Read off the end of the filters TLV");
return Err(())
}
},
x if x == StructureTypes::Pcm3060Configuration as u16 => {
cfg.codec.oversampling = cur.read_u8().unwrap() != 0;
Expand All @@ -314,23 +354,22 @@ fn load_config(connection_state: State<'_, Mutex<ConnectionState>>) -> Result<bo
//println!("\tT: {} L: {}", type_val, length_val);
position += length_val;
}
//println!("CFG {}", serde_json::to_string(&cfg).unwrap());

return Ok(true)
return Ok(serde_json::to_string(&cfg).unwrap())
}, // TODO: Check for NOK
Err(_) => return Err(())
}
}

#[tauri::command]
fn factory_reset(connection_state: State<'_, Mutex<ConnectionState>>) -> Result<bool, ()> {
println!("factory_reset");
let mut buf : Vec<u8> = Vec::new();
buf.extend_from_slice(&(StructureTypes::FactoryReset as u16).to_le_bytes());
buf.extend_from_slice(&(4u16).to_le_bytes());

match &send_cmd(connection_state, &buf) {
Ok(_) => return Ok(true), // TODO: Check for NOK
Err(_) => return Err(())
Ok(r) => { println!("Factory Reset {:02X?}", r); return Ok(true)}, // TODO: Check for NOK
Err(_) => { println!("Factory Reset Err"); return Err(()) }
}
}

Expand Down
66 changes: 52 additions & 14 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ export default {
}
},
tab() {
invoke("load_config")
this.sendState()
this.saveState()
},
Expand Down Expand Up @@ -114,11 +113,35 @@ export default {
config.codec.rolloff = config.codec.rolloff != 0
config.codec.de_emphasis = config.codec.de_emphasis != 0
}
if ("reverseStereo" in config.preprocessing) {
config.preprocessing.reverse_stereo = config.preprocessing.reverseStereo
delete config.preprocessing.reverseStereo
}
console.log(config)
},
pageHeight(offset) {
const height = offset ? `calc(100vh - ${offset}px)` : '100vh'
return { height: height }
},
readDeviceConfiguration() {
invoke("load_config").then((deviceConfig) => {
var config = JSON.parse(deviceConfig)
config.id = this.tab
config.name = this.tabs[this.tab].name
config.state = this.tabs[this.tab].state
this.tabs[this.tab] = config
})
},
readDefaultConfiguration() {
resolveResource('resources/configuration.json').then((configJson) =>
readTextFile(configJson).then((defaultConfiguration) => {
var config = JSON.parse(defaultConfiguration)
config.id = this.tab
config.name = this.tabs[this.tab].name
config.state = this.tabs[this.tab].state
this.tabs[this.tab] = config
}))
},
addConfiguration() {
var nextId = this.tabs.length
// Try not to make any changes to the sound on the connected headphones
Expand All @@ -132,15 +155,23 @@ export default {
this.tab = nextId
return;
}
// TODO: Read config from device..
resolveResource('resources/configuration.json').then((configJson) =>
readTextFile(configJson).then((defaultConfiguration) => {
var config = JSON.parse(defaultConfiguration)
config.id = nextId
config.state = structuredClone(defaultState)
this.tabs.push(config)
this.tab = nextId
}))
invoke("load_config").then((deviceConfig) => {
var config = JSON.parse(deviceConfig)
config.name = "Unnamed configuration"
config.id = nextId
config.state = structuredClone(defaultState)
this.tabs.push(config)
this.tab = nextId
}).catch(err => {
resolveResource('resources/configuration.json').then((configJson) =>
readTextFile(configJson).then((defaultConfiguration) => {
var config = JSON.parse(defaultConfiguration)
config.id = nextId
config.state = structuredClone(defaultState)
this.tabs.push(config)
this.tab = nextId
}))
})
},
deleteConfiguration() {
for (var i = 0; i < this.tabs.length; i++) {
Expand All @@ -159,7 +190,7 @@ export default {
sendState() {
if (this.connected && this.tab !== undefined && this.tabs[this.tab] !== undefined) {
var sendConfig = {
"preprocessing": { "preamp": this.tabs[this.tab].preprocessing.preamp / 100, "reverse_stereo": this.tabs[this.tab].preprocessing.reverseStereo },
"preprocessing": { "preamp": this.tabs[this.tab].preprocessing.preamp, "reverse_stereo": this.tabs[this.tab].preprocessing.reverse_stereo },
"filters": this.tabs[this.tab].filters,
"codec": this.tabs[this.tab].codec
}
Expand Down Expand Up @@ -327,7 +358,7 @@ export default {
<q-item clickable v-close-popup :disable="!connected" @click="invoke('reboot_bootloader')">
<q-item-section>Reboot into bootloader</q-item-section>
</q-item>
<q-item clickable v-close-popup :disable="!connected">
<q-item clickable v-close-popup :disable="!connected" @click="invoke('factory_reset')">
<q-item-section>Erase saved configuration</q-item-section>
</q-item>
</q-list>
Expand Down Expand Up @@ -386,13 +417,20 @@ export default {
</q-btn>
<q-btn flat dense icon="more_vert">
<q-menu>
<q-list style="min-width: 100px">
<q-list style="min-width: 14em">
<q-item clickable v-close-popup @click="exportConfiguration()">
<q-item-section>Export to JSON</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="importConfiguration()">
<q-item-section>Import from JSON</q-item-section>
</q-item>
<q-separator />
<q-item clickable v-close-popup @click="readDeviceConfiguration()">
<q-item-section>Read config from device</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="readDefaultConfiguration()">
<q-item-section>Reset config to default</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
Expand All @@ -402,7 +440,7 @@ export default {
<q-tab-panel v-for="t in tabs" :name="t.id" class="panel">
<div class="column q-gutter-md q-ma-none">
<PreProcessingCardVue v-model:preamp="t.preprocessing.preamp"
v-model:reverseStereo="t.preprocessing.reverseStereo" v-model:expansion="t.state.expanded[0]" />
v-model:reverse_stereo="t.preprocessing.reverse_stereo" v-model:expansion="t.state.expanded[0]" />
<FilterCardVue v-model:filters="t.filters" v-model:expansion="t.state.expanded[1]" />
<CodecCardVue v-model:oversampling="t.codec.oversampling" v-model:phase="t.codec.phase"
v-model:rolloff="t.codec.rolloff" v-model:de_emphasis="t.codec.de_emphasis"
Expand Down
8 changes: 4 additions & 4 deletions src/components/PreProcessingCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ export default {
},
props: {
preamp: ref(0),
reverseStereo: ref(false),
reverse_stereo: ref(false),
expansion: ref(Boolean)
},
emits: ['update:preamp', 'update:reverseStereo', 'update:expansion']
emits: ['update:preamp', 'update:reverse_stereo', 'update:expansion']
}
</script>
<template>
Expand All @@ -40,8 +40,8 @@ export default {
:label-value="preamp + '%'" label />
</q-item-section>
</q-item>
<q-checkbox label="Reverse Stereo" :model-value="reverseStereo"
@update:model-value="(value) => $emit('update:reverseStereo', value)" />
<q-checkbox label="Reverse Stereo" :model-value="reverse_stereo"
@update:model-value="(value) => $emit('update:reverse_stereo', value)" />
</q-card-section>
</q-expansion-item>
</q-card>
Expand Down

0 comments on commit 3e47c0a

Please sign in to comment.