From 3e47c0abb2ae124b220b71b9b19b7edc75dead7d Mon Sep 17 00:00:00 2001 From: George Norton Date: Wed, 14 Jun 2023 22:51:55 +0100 Subject: [PATCH] Read back config from the device. --- README.md | 10 +++- src-tauri/resources/configuration.json | 2 +- src-tauri/src/main.rs | 67 ++++++++++++++++++++------ src/App.vue | 66 +++++++++++++++++++------ src/components/PreProcessingCard.vue | 8 +-- 5 files changed, 118 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 0e778d6..bdea342 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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. @@ -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. diff --git a/src-tauri/resources/configuration.json b/src-tauri/resources/configuration.json index d5225d8..8ef7a68 100644 --- a/src-tauri/resources/configuration.json +++ b/src-tauri/resources/configuration.json @@ -40,7 +40,7 @@ ], "preprocessing": { "preamp": -20, - "reverseStereo": false + "reverse_stereo": false }, "codec": { "oversampling": false, diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 6ae2704..5d2b1ba 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -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)] @@ -226,7 +237,7 @@ fn write_config(config: &str, connection_state: State<'_, Mutex 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]); @@ -273,7 +284,7 @@ fn save_config(connection_state: State<'_, Mutex>) -> Result>) -> Result { +fn load_config(connection_state: State<'_, Mutex>) -> Result { let mut buf : Vec = Vec::new(); buf.extend_from_slice(&(StructureTypes::GetStoredConfiguration as u16).to_le_bytes()); buf.extend_from_slice(&(4u16).to_le_bytes()); @@ -281,25 +292,54 @@ fn load_config(connection_state: State<'_, Mutex>) -> Result { let mut cur = Cursor::new(cfg); - //println!("Read the following cfg: {:02X?}", cfg); let _result_type_val = cur.read_u16::().unwrap(); let result_length_val = cur.read_u16::().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::().unwrap(); let length_val = cur.read_u16::().unwrap(); - //println!("\tT: {} L: {}", type_val, length_val); match type_val{ x if x == StructureTypes::PreProcessingConfiguration as u16 => { - cfg.preprocessing.preamp = cur.read_f64::().unwrap(); + cfg.preprocessing.preamp = cur.read_f64::().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::().unwrap(); + let db_gain; + if filter_args == 3 { + db_gain = cur.read_f64::().unwrap(); + } else { + db_gain = 0.0; + } + let q = cur.read_f64::().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; @@ -314,9 +354,7 @@ fn load_config(connection_state: State<'_, Mutex>) -> Result return Err(()) } @@ -324,13 +362,14 @@ fn load_config(connection_state: State<'_, Mutex>) -> Result>) -> Result { + println!("factory_reset"); let mut buf : Vec = 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(()) } } } diff --git a/src/App.vue b/src/App.vue index c240633..1e13b90 100644 --- a/src/App.vue +++ b/src/App.vue @@ -48,7 +48,6 @@ export default { } }, tab() { - invoke("load_config") this.sendState() this.saveState() }, @@ -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 @@ -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++) { @@ -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 } @@ -327,7 +358,7 @@ export default { Reboot into bootloader - + Erase saved configuration @@ -386,13 +417,20 @@ export default { - + Export to JSON Import from JSON + + + Read config from device + + + Reset config to default + @@ -402,7 +440,7 @@ export default {
+ v-model:reverse_stereo="t.preprocessing.reverse_stereo" v-model:expansion="t.state.expanded[0]" />