├── .cargo └── config.toml ├── .github ├── FUNDING.yml └── workflows │ ├── build_linux.yml │ ├── build_macos_arm.yml │ └── build_windows.yml ├── .gitignore ├── BUILDING.md ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE.txt ├── README.md ├── build-linux-appimage.sh ├── build-mac-bundle.sh ├── build.rs ├── icons ├── icon_128x128.png ├── icon_256x256.png └── icon_32x32.png ├── screenshot.png ├── src ├── main.rs ├── messages.rs ├── midi │ ├── mod.rs │ └── sysex.rs ├── params.rs └── ui │ ├── elements │ ├── arp_grid_list.rs │ ├── arp_mode_list.rs │ ├── checkbox.rs │ ├── env_trigger_list.rs │ ├── fx_mode_list.rs │ ├── lfo_phase_list.rs │ ├── lfo_shape_list.rs │ ├── midi_channel_list.rs │ ├── mod.rs │ ├── mod_target_list.rs │ ├── part_list.rs │ ├── shaper_mode_list.rs │ ├── slider.rs │ ├── slider_widget.rs │ └── wavetable_list.rs │ ├── manager │ └── mod.rs │ ├── mod.rs │ ├── multi │ ├── fx.rs │ ├── midi.rs │ ├── mixer.rs │ └── mod.rs │ ├── sound │ ├── amp.rs │ ├── arp.rs │ ├── enva.rs │ ├── envf.rs │ ├── extra.rs │ ├── filter.rs │ ├── lfo1.rs │ ├── lfo2.rs │ ├── misc.rs │ ├── mod.rs │ ├── modulation.rs │ ├── osc1.rs │ ├── osc2.rs │ └── shaper.rs │ └── style.rs ├── tools ├── mac_bundle_post_build.py └── tooro-editor.desktop └── wix ├── DialogBmp.bmp ├── License.rtf ├── SetupIcon.ico └── main.wxs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-apple-darwin] 2 | rustflags=["-C", "link-arg=-mmacosx-version-min=10.11"] 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://www.buymeacoffee.com/sourcebox 2 | -------------------------------------------------------------------------------- /.github/workflows/build_linux.yml: -------------------------------------------------------------------------------- 1 | name: Build Linux 2 | 3 | on: 4 | workflow_dispatch 5 | 6 | env: 7 | CARGO_TERM_COLOR: always 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-18.04 13 | 14 | env: 15 | PROJECT_NAME: ${{ github.event.repository.name }} 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Install Requirements 21 | run: | 22 | export DEBIAN_FRONTED=noninteractive 23 | sudo apt-get -qq update 24 | sudo apt-get install -y libxkbcommon-dev 25 | sudo apt-get install -y libasound2-dev 26 | 27 | - name: Install LinuxDeploy 28 | uses: miurahr/install-linuxdeploy-action@v1 29 | with: 30 | plugins: appimage 31 | 32 | - name: Install cargo-deb 33 | run: cargo install cargo-deb 34 | 35 | - name: Build AppImage 36 | run: ./build-linux-appimage.sh 37 | 38 | - name: Archive AppImage 39 | uses: actions/upload-artifact@v2 40 | with: 41 | name: ${{ env.PROJECT_NAME }}-x86_64.AppImage 42 | path: target/release/appimage/*.AppImage 43 | 44 | - name: Pack .deb package 45 | run: cargo deb --no-build 46 | 47 | - name: Archive .deb package 48 | uses: actions/upload-artifact@v2 49 | with: 50 | name: ${{ env.PROJECT_NAME }}-x86_64.deb 51 | path: target/debian/*.deb 52 | -------------------------------------------------------------------------------- /.github/workflows/build_macos_arm.yml: -------------------------------------------------------------------------------- 1 | name: Build macOS ARM 2 | 3 | on: 4 | workflow_dispatch 5 | 6 | env: 7 | CARGO_TERM_COLOR: always 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-latest 13 | 14 | env: 15 | PROJECT_NAME: ${{ github.event.repository.name }} 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Install cargo-bundle 21 | run: cargo install cargo-bundle 22 | 23 | - name: Build DMG 24 | run: ./build-mac-bundle.sh 25 | 26 | - name: Archive DMG 27 | uses: actions/upload-artifact@v2 28 | with: 29 | name: ${{ env.PROJECT_NAME }}-aarch64.dmg 30 | path: target/release/bundle/osx/*.dmg 31 | -------------------------------------------------------------------------------- /.github/workflows/build_windows.yml: -------------------------------------------------------------------------------- 1 | name: Build Windows 2 | 3 | on: 4 | workflow_dispatch 5 | 6 | env: 7 | CARGO_TERM_COLOR: always 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: windows-latest 13 | 14 | env: 15 | PROJECT_NAME: ${{ github.event.repository.name }} 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Install cargo-wix 21 | run: cargo install cargo-wix 22 | 23 | - name: Build 24 | run: cargo build --release 25 | 26 | - name: Archive .exe 27 | uses: actions/upload-artifact@v2 28 | with: 29 | name: ${{ env.PROJECT_NAME }}-x86_64-pc-windows-msvc.exe 30 | path: target/release/*.exe 31 | 32 | - name: Build installer 33 | run: cargo wix 34 | 35 | - name: Archive .msi 36 | uses: actions/upload-artifact@v2 37 | with: 38 | name: ${{ env.PROJECT_NAME }}-x86_64-pc-windows-msvc.msi 39 | path: target/wix/*.msi 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.vscode -------------------------------------------------------------------------------- /BUILDING.md: -------------------------------------------------------------------------------- 1 | # Building 2 | 3 | ## Build Requirements 4 | 5 | To build the application from source, a [Rust toolchain](https://www.rust-lang.org/) is required. 6 | 7 | - Use `cargo build` to compile or `cargo run` to compile and run the application in debug mode. 8 | - Use `cargo build --release` to compile or `cargo run --release` to compile and run the application in release mode. 9 | 10 | ### Windows Installer (optional) 11 | 12 | [cargo-wix](https://github.com/volks73/cargo-wix) must be installed to create a Windows installer. 13 | 14 | - Run `cargo wix` to build the installer (msi). 15 | - The installer will be created in the `target/wix` folder. 16 | 17 | ### Mac Application Bundle (optional) 18 | 19 | To build a macOS application bundle, additional dependencies must be installed: 20 | 21 | - [cargo-bundle](https://github.com/burtonageo/cargo-bundle) 22 | - [Python3](https://python.org) (any recent version should work) 23 | 24 | Run `./build-mac-bundle.sh` from the project directory. Make sure the script has executable permissions. 25 | 26 | The bundle will be created in the `target/release/bundle/osx` folder. 27 | If the custom app icon does not show up, copy/paste it manually from the icons folder using the finder info dialog. 28 | 29 | ### Linux AppImage (optional) 30 | 31 | To build an AppImage for Linux, additional dependencies must be installed: 32 | 33 | - [linuxdeploy](https://github.com/linuxdeploy/linuxdeploy) 34 | - [linuxdeploy-plugin-appimage](https://github.com/linuxdeploy/linuxdeploy-plugin-appimage) 35 | 36 | Run `./build-linux-appimage.sh` from the project directory. Make sure the script has executable permissions. 37 | The AppImage will be created in the `./target/release/appimage` directory. 38 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ### Added 11 | 12 | - Merge input is remembered between launches. 13 | 14 | ### Changed 15 | 16 | - Migrated `iced` dependency to `0.9`. 17 | - Updated dependencies. 18 | 19 | ## [1.1.0] - 2022-09-28 20 | 21 | ### Added 22 | 23 | - Mouse wheel support for sliders. 24 | - Control-click or right-click to set a slider to its default value. 25 | - Slider fine control by holding shift when dragging. 26 | - Strip symbols from release builds for smaller binaries. 27 | - Disable buttons when device is not connected. 28 | 29 | ### Changed 30 | 31 | - Swapped positions of *Shaper* and *Extra* sections in layout. 32 | - Parameter labels now match the panel printing more precise. 33 | - Lower latency with MIDI merge input. 34 | - Show shorter port names for merge input on Linux. 35 | - Don't show internal port in merge input menu. 36 | - Give merge input menu more space to make longer names fit. 37 | - Show pointer shape mouse cursor when hovering a slider on all platforms. 38 | - Relaxed port name detection for BomeBox compatibility. 39 | - Use 2021 edition of Rust. 40 | - Updated dependencies. 41 | 42 | ## [1.0.0] - 2021-08-02 43 | 44 | First public release. 45 | 46 | ## [0.1.0] - No date specified 47 | 48 | Initial development release for internal use only. 49 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tooro-editor" 3 | version = "1.1.0" 4 | authors = ["Oliver Rockstedt "] 5 | edition = "2021" 6 | rust-version = "1.64" 7 | description = "Editor for the Fred's Lab Töörö hardware synthesizer" 8 | repository = "https://github.com/sourcebox/tooro-editor" 9 | license = "MIT" 10 | keywords = ["midi", "synthesizer"] 11 | 12 | [dependencies] 13 | iced = { version = "0.9", default-features = false, features = ["tokio", "glow"]} 14 | iced_native = "0.10" 15 | iced_style = "0.8" 16 | midir = "0.9" 17 | log = { version="0.4", features = ["max_level_debug", "release_max_level_warn"] } 18 | simple_logger = "4.1" 19 | num-traits = "0.2" 20 | tinyfiledialogs = "3.9" 21 | serde = { version = "1.0", features = ["derive"] } 22 | ron = "0.8" 23 | directories-next = "2.0" 24 | 25 | [profile.release] 26 | lto = true 27 | strip = true 28 | 29 | [package.metadata.bundle] 30 | name = "Töörö Editor" 31 | identifier = "de.sourcebox.tooro-editor" 32 | osx_minimum_system_version = "10.11" 33 | icon = ["icons/icon_32x32.png", "icons/icon_128x128.png", "icons/icon_256x256.png"] 34 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Oliver Rockstedt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Töörö Editor 2 | 3 | Cross-platform sound editor for the [Fred's Lab Töörö](https://fredslab.net/en/tooro-module.php) hardware synthesizer. This application was developed in close cooperation with the manufacturer. 4 | 5 | ![Screenshot](screenshot.png) 6 | 7 | The application is written in Rust and features the [Iced GUI library](https://github.com/hecrj/iced) and the [midir](https://github.com/Boddlnagg/midir) crate for MIDI processing. 8 | 9 | ## Features 10 | 11 | The editor currently supports: 12 | 13 | - Editing of most preset and multi parameters 14 | - Loading and saving of presets as sysex files 15 | 16 | Being an open source project, user contributions to the code are always welcome. 17 | 18 | ## Usage 19 | 20 | Using the editor is mostly self-explanatory, but there are a few things to be noted: 21 | 22 | - The Töörö must be connected to the computer via USB. DIN MIDI will not work. 23 | - When using a DAW at the same time as the editor, make sure it does not loopback sysex messages to the Töörö. 24 | - You can select a MIDI input for playing the Töörö while editing via the **Merge Input** dropdown list on the bottom of the application window. 25 | - Silders can be fine-controlled by holding the *SHIFT* key while dragging. 26 | - To reset a slider value to it's default, use *CTRL*-click or right-click. 27 | - The mouse wheel can also be used to change a slider value. 28 | - The Töörö firmware must be V1.5 or higher. Otherwise, not all parameters can be edited. 29 | - The application tries to detect when you change a parameter on the device itself. Unfortunately, this will not work in all cases. Use the **Update from device** button to force a reload of all parameters. 30 | - A manual update must also be requested when you change a preset or change a parameter via MIDI CCs from another application or source. 31 | - All sysex files must use **.syx** as file extension. 32 | - On larger screens, the window width can be increased to improve the resolution of the sliders. 33 | 34 | ## Known Issues 35 | 36 | - The connection state is not always detected correctly when the Töörö is connected or disconnected while the application is running. 37 | - Resizing the window height is possible but has no use. 38 | - Using more than one Töörö at a time is not supported. 39 | 40 | ## Runtime Requirements 41 | 42 | The following platforms were tested during development: 43 | 44 | - Windows 10 45 | - OS X 10.11 (El Capitan) 46 | - macOS 10.13 (High Sierra) 47 | - macOS 11 (Big Sur) 48 | - Linux Mint 20.3 49 | 50 | ## Building from Source 51 | 52 | See [separate document](BUILDING.md) for detailed instructions. 53 | 54 | ## License 55 | 56 | Published under the MIT license. All contributions to this project must be provided under the same license conditions. 57 | 58 | Author: Oliver Rockstedt 59 | 60 | ## Donations 61 | 62 | If you like to support my work, you can [buy me a 63 | coffee.](https://www.buymeacoffee.com/sourcebox) 64 | 65 | Buy Me A Coffee 66 | -------------------------------------------------------------------------------- /build-linux-appimage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cargo build --release 4 | 5 | rm -rf target/release/appimage/* 6 | 7 | linuxdeploy-x86_64.AppImage \ 8 | --executable ./target/release/tooro-editor \ 9 | --desktop-file ./tools/tooro-editor.desktop \ 10 | --icon-file ./icons/icon_128x128.png \ 11 | --appdir ./target/release/appimage/AppDir \ 12 | --output appimage 13 | 14 | echo "Moving appimage to target directory" 15 | mv *.AppImage ./target/release/appimage/ 16 | -------------------------------------------------------------------------------- /build-mac-bundle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BUNDLE_DIR="./target/release/bundle/osx/Töörö Editor.app" 4 | POST_BUILD_SCRIPT="./tools/mac_bundle_post_build.py" 5 | 6 | cargo bundle --release 7 | 8 | echo "Running post build script $POST_BUILD_SCRIPT" 9 | chmod 755 "$POST_BUILD_SCRIPT" 10 | $POST_BUILD_SCRIPT "$BUNDLE_DIR" 11 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | #[cfg(target_os = "macos")] 3 | build_macos(); 4 | } 5 | 6 | #[allow(dead_code)] 7 | fn build_macos() { 8 | println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.11"); 9 | } 10 | -------------------------------------------------------------------------------- /icons/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcebox/tooro-editor/662f6a34347e1418028bc02cda41190bc9e4b282/icons/icon_128x128.png -------------------------------------------------------------------------------- /icons/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcebox/tooro-editor/662f6a34347e1418028bc02cda41190bc9e4b282/icons/icon_256x256.png -------------------------------------------------------------------------------- /icons/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcebox/tooro-editor/662f6a34347e1418028bc02cda41190bc9e4b282/icons/icon_32x32.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcebox/tooro-editor/662f6a34347e1418028bc02cda41190bc9e4b282/screenshot.png -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! Cross-platform sound editor for the [Fred's Lab Töörö](https://fredslab.net/en/tooro-module.php) hardware synthesizer 2 | 3 | #![windows_subsystem = "windows"] 4 | #![warn(missing_docs)] 5 | 6 | mod messages; 7 | mod midi; 8 | mod params; 9 | mod ui; 10 | 11 | use std::sync::mpsc; 12 | use std::time::{Duration, Instant}; 13 | 14 | use iced::widget::{Column, Container, PickList, Row, Text}; 15 | use iced::{ 16 | executor, time, Alignment, Application, Command, Element, Length, Settings, Subscription, 17 | }; 18 | use serde::{Deserialize, Serialize}; 19 | use simple_logger::SimpleLogger; 20 | use tinyfiledialogs::{open_file_dialog, save_file_dialog_with_filter}; 21 | 22 | use messages::Message; 23 | use midi::MidiConnector; 24 | use params::{GetValue, MultiParameterValues, SoundParameterValues}; 25 | use ui::manager::ManagerPanel; 26 | use ui::multi::MultiPanel; 27 | use ui::sound::SoundPanel; 28 | use ui::style; 29 | 30 | /// Application name used for file path of persistent storage. 31 | const APP_NAME: &str = env!("CARGO_PKG_NAME"); 32 | 33 | /// The main entry point 34 | fn main() -> iced::Result { 35 | SimpleLogger::new() 36 | .with_level(log::LevelFilter::Debug) 37 | .init() 38 | .unwrap(); 39 | 40 | let settings = Settings { 41 | window: iced::window::Settings { 42 | size: (style::WINDOW_WIDTH, style::WINDOW_HEIGHT), 43 | min_size: Some((style::WINDOW_WIDTH, style::WINDOW_HEIGHT)), 44 | max_size: Some((style::WINDOW_WIDTH * 2, style::WINDOW_HEIGHT)), 45 | resizable: true, 46 | ..iced::window::Settings::default() 47 | }, 48 | exit_on_close_request: false, 49 | ..Settings::default() 50 | }; 51 | 52 | EditorApp::run(settings) 53 | } 54 | 55 | /// Persistent state saved between launches. 56 | #[derive(Debug, Serialize, Deserialize, Default)] 57 | struct AppState { 58 | /// Name of the merge input port. 59 | merge_input_name: String, 60 | } 61 | 62 | /// Holds the application data and state 63 | struct EditorApp { 64 | /// Persistent state data. 65 | app_state: AppState, 66 | 67 | /// UI section containing the sound (preset) parameters 68 | sound_panel: SoundPanel, 69 | 70 | /// UI section containing the multi parameters 71 | multi_panel: MultiPanel, 72 | 73 | /// UI section containing global controls 74 | manager_panel: ManagerPanel, 75 | 76 | /// Status bar info if connection is enabled or disabled 77 | status_connection: String, 78 | 79 | /// Status bar info on communication 80 | status_communication: String, 81 | 82 | /// MPSC sender for incoming messages from merge input 83 | merge_input_sender: Option>>, 84 | 85 | /// Current part id 0-3 for part 1-4 86 | part_id: u8, 87 | 88 | /// Current sound (preset) parameter values 89 | sound_params: SoundParameterValues, 90 | 91 | /// Current multi parameter values 92 | multi_params: MultiParameterValues, 93 | 94 | /// MIDI connection handler for all ports 95 | midi: MidiConnector, 96 | 97 | /// Device connection state 98 | device_connected: bool, 99 | 100 | /// Flag for requested sound (preset) parameter update from device 101 | request_sound_update: bool, 102 | 103 | /// Flag for requested multi parameter update from device 104 | request_multi_update: bool, 105 | 106 | /// Time of last dump request 107 | request_time: Option, 108 | 109 | /// File to capture next received preset dump 110 | preset_capture_file: Option, 111 | 112 | /// Flag for app initialisation complete 113 | init_complete: bool, 114 | } 115 | 116 | impl Application for EditorApp { 117 | type Executor = executor::Default; 118 | type Message = Message; 119 | type Flags = (); 120 | type Theme = iced::Theme; 121 | 122 | /// Constructs a new application 123 | fn new(_flags: ()) -> (Self, Command) { 124 | let mut app = Self { 125 | app_state: AppState::default(), 126 | 127 | sound_panel: SoundPanel::new(), 128 | multi_panel: MultiPanel::new(), 129 | manager_panel: ManagerPanel::new(), 130 | 131 | status_connection: String::from("Device disconnected"), 132 | status_communication: String::from("Initializing..."), 133 | 134 | merge_input_sender: None, 135 | 136 | part_id: 0, 137 | 138 | sound_params: SoundParameterValues::with_capacity(128), 139 | multi_params: MultiParameterValues::with_capacity(32), 140 | 141 | midi: MidiConnector::new(), 142 | device_connected: false, 143 | 144 | request_sound_update: false, 145 | request_multi_update: false, 146 | request_time: None, 147 | 148 | preset_capture_file: None, 149 | 150 | init_complete: false, 151 | }; 152 | 153 | app.load_app_state(); 154 | 155 | // If the merge input is not present at startup, clear the stored setting. 156 | app.midi.scan_ports(); 157 | if !app 158 | .midi 159 | .get_merge_inputs() 160 | .contains(&app.app_state.merge_input_name) 161 | { 162 | app.app_state.merge_input_name = String::new(); 163 | } 164 | 165 | (app, Command::none()) 166 | } 167 | 168 | /// Returns the name of the application shown in the title bar 169 | fn title(&self) -> String { 170 | String::from("Töörö Editor") 171 | } 172 | 173 | /// Process a message and update the state accordingly 174 | fn update(&mut self, message: Self::Message) -> Command { 175 | match message { 176 | Message::EventOccurred(event) => { 177 | if event == iced_native::Event::Window(iced_native::window::Event::CloseRequested) { 178 | self.save_app_state(); 179 | return iced::window::close(); 180 | } 181 | } 182 | 183 | Message::SoundParameterChange(param, value) => { 184 | let last_value = self.sound_params.get_value(param); 185 | 186 | if value != last_value { 187 | self.sound_params.insert(param, value); 188 | if self.device_connected { 189 | let message = 190 | midi::sysex::preset_param_dump(0x70 + self.part_id, ¶m, value); 191 | // log::debug!("Sending preset parameter dump {:?}", message); 192 | self.midi.send(&message); 193 | } 194 | } 195 | } 196 | 197 | Message::MultiParameterChange(param, value) => { 198 | let last_value = self.multi_params.get_value(param); 199 | 200 | if value != last_value { 201 | self.multi_params.insert(param, value); 202 | if self.device_connected { 203 | let message = midi::sysex::multi_param_dump(¶m, value); 204 | // log::debug!("Sending multi parameter dump {:?}", message); 205 | self.midi.send(&message); 206 | } 207 | } 208 | } 209 | 210 | Message::PartChange(part_id) => { 211 | self.part_id = part_id; 212 | self.request_sound_update = true; 213 | } 214 | 215 | Message::MergeInputChange(input_name) => { 216 | log::debug!("Merge input changed to {:?}", input_name); 217 | self.app_state.merge_input_name = input_name.clone(); 218 | if let Some(sender) = &self.merge_input_sender { 219 | self.midi.select_merge_input(input_name, sender.clone()); 220 | } 221 | } 222 | 223 | Message::UpdateFromDevice if self.device_connected => { 224 | self.request_sound_update = true; 225 | self.request_multi_update = true; 226 | } 227 | 228 | Message::LoadSysexFile if self.device_connected => { 229 | if let Some(file) = 230 | open_file_dialog("Open syx file", "", Some((&["*.syx"], "Sysex files"))) 231 | { 232 | log::debug!("Loading file {}", file); 233 | let data = std::fs::read(file); 234 | 235 | if let Ok(mut message) = data { 236 | match message[1] { 237 | midi::sysex::SERVICE_PRESET_DUMP 238 | if message.len() == midi::sysex::PRESET_DUMP_LENGTH => 239 | { 240 | let preset_id = 0x70 + self.part_id; 241 | log::debug!("Sending preset dump with id {:#X}", preset_id); 242 | message[2] = preset_id; 243 | self.midi.send(&message); 244 | self.request_sound_update = true; 245 | } 246 | 247 | midi::sysex::SERVICE_MULTI_DUMP 248 | if message.len() == midi::sysex::MULTI_DUMP_LENGTH => 249 | { 250 | let multi_id = 0x7F; 251 | log::debug!("Sending multi dump with id {:#X}", multi_id); 252 | message[2] = multi_id; 253 | self.midi.send(&message); 254 | self.request_multi_update = true; 255 | } 256 | 257 | _ => { 258 | log::error!("Invalid sysex data"); 259 | } 260 | } 261 | } 262 | } 263 | } 264 | 265 | Message::SavePresetSysexFile if self.device_connected => { 266 | if let Some(file) = 267 | save_file_dialog_with_filter("Save syx file", "", &["*.syx"], "Sysex files") 268 | { 269 | let mut file = std::path::PathBuf::from(file); 270 | file.set_extension("syx"); 271 | log::debug!("Capturing next preset dump in file {:?}", file); 272 | self.preset_capture_file = Some(file.into_os_string().into_string().unwrap()); 273 | self.request_sound_update = true; 274 | } 275 | } 276 | 277 | Message::Tick => { 278 | self.midi.scan_ports(); 279 | let connection_state = self.midi.is_connected(); 280 | 281 | if connection_state != self.device_connected { 282 | if connection_state { 283 | self.on_device_connected(); 284 | } else { 285 | self.on_device_disconnected(); 286 | } 287 | self.device_connected = connection_state; 288 | } 289 | 290 | if !self.init_complete { 291 | log::debug!("Init complete"); 292 | self.status_communication = if self.device_connected { 293 | String::new() 294 | } else { 295 | String::from("MIDI communication disabled") 296 | }; 297 | self.init_complete = true; 298 | } 299 | } 300 | 301 | Message::FastTick => { 302 | if let Some(message) = self.midi.receive() { 303 | self.process_midi(&message); 304 | } 305 | 306 | if self.device_connected { 307 | if self.request_sound_update && self.request_time.is_none() { 308 | let preset_id = 0x70 + self.part_id; 309 | log::debug!("Requesting preset with id {:#X}", preset_id); 310 | self.status_communication = String::from("Requesting preset dump..."); 311 | let message = midi::sysex::preset_request(preset_id); 312 | self.midi.send(&message); 313 | self.request_time = Some(Instant::now()); 314 | self.request_sound_update = false; 315 | } 316 | 317 | if self.request_multi_update && self.request_time.is_none() { 318 | let multi_id = 0x7F; 319 | log::debug!("Requesting multi with id {:#X}", multi_id); 320 | self.status_communication = String::from("Requesting multi dump..."); 321 | let message = midi::sysex::multi_request(multi_id); 322 | self.midi.send(&message); 323 | self.request_time = Some(Instant::now()); 324 | self.request_multi_update = false; 325 | } 326 | 327 | if let Some(request_time) = self.request_time { 328 | if request_time.elapsed() >= Duration::new(1, 0) { 329 | log::error!("Response timeout"); 330 | self.status_communication = String::from("Error: response timeout"); 331 | self.request_time = None; 332 | } 333 | } 334 | } 335 | } 336 | 337 | Message::MidiMergeSubscriptionReady(mut sender) => { 338 | let channel: (mpsc::Sender>, mpsc::Receiver>) = mpsc::channel(); 339 | self.merge_input_sender = Some(channel.0); 340 | self.midi.select_merge_input( 341 | self.app_state.merge_input_name.clone(), 342 | self.merge_input_sender.as_ref().unwrap().clone(), 343 | ); 344 | std::thread::spawn(move || loop { 345 | while let Ok(message) = channel.1.recv() { 346 | sender.try_send(message).ok(); 347 | } 348 | }); 349 | } 350 | 351 | Message::MidiMergeInputMessage(message) => { 352 | self.midi.send(&message); 353 | } 354 | 355 | _ => {} 356 | } 357 | 358 | Command::none() 359 | } 360 | 361 | /// Return a subscripton event 362 | fn subscription(&self) -> Subscription { 363 | let event_subscription = iced_native::subscription::events().map(Message::EventOccurred); 364 | 365 | let tick_subscription = time::every(Duration::from_millis(1000)).map(|_| Message::Tick); 366 | let fast_tick_subscription = 367 | time::every(Duration::from_millis(100)).map(|_| Message::FastTick); 368 | 369 | let subscriptions = vec![ 370 | tick_subscription, 371 | fast_tick_subscription, 372 | midi_merge_input_subscription(), 373 | event_subscription, 374 | ]; 375 | 376 | Subscription::batch(subscriptions) 377 | } 378 | 379 | /// Returns the widgets to display 380 | fn view(&self) -> Element> { 381 | Container::new( 382 | Column::new() 383 | .push( 384 | Row::new() 385 | .push( 386 | Column::new() 387 | .push(self.sound_panel.view(&self.sound_params)) 388 | .width(Length::FillPortion(4)), 389 | ) 390 | .push( 391 | Column::new() 392 | .push(self.manager_panel.view(self.part_id, self.device_connected)) 393 | .push(self.multi_panel.view(&self.multi_params)) 394 | .width(Length::FillPortion(1)), 395 | ) 396 | .height(625), 397 | ) 398 | .push( 399 | Row::new() 400 | .push(Column::new().width(10)) 401 | .push( 402 | Column::new() 403 | .push( 404 | Text::new(&self.status_connection) 405 | .size(style::STATUS_TEXT_SIZE), 406 | ) 407 | .width(Length::FillPortion(1)), 408 | ) 409 | .push( 410 | Column::new().push( 411 | Row::new() 412 | .push(Text::new("Merge Input:").size(style::STATUS_TEXT_SIZE)) 413 | .push( 414 | PickList::new( 415 | { 416 | let mut inputs = 417 | self.midi.get_merge_inputs().clone(); 418 | inputs.insert(0, String::from("")); 419 | inputs 420 | }, 421 | Some(self.app_state.merge_input_name.clone()), 422 | Message::MergeInputChange, 423 | ) 424 | .width(250) 425 | .style(style::PickList) 426 | .text_size(style::LIST_ITEM_TEXT_SIZE), 427 | ) 428 | .spacing(10), 429 | ), 430 | ) 431 | .push( 432 | Column::new() 433 | .push( 434 | Text::new(&self.status_communication) 435 | .size(style::STATUS_TEXT_SIZE), 436 | ) 437 | .width(Length::FillPortion(3)) 438 | .align_items(Alignment::Center), 439 | ) 440 | .push( 441 | Column::new() 442 | .push( 443 | #[cfg(debug_assertions)] 444 | Text::new(format!( 445 | "v{} (debug build)", 446 | env!("CARGO_PKG_VERSION") 447 | )) 448 | .size(style::STATUS_TEXT_SIZE), 449 | #[cfg(not(debug_assertions))] 450 | Text::new(format!("v{}", env!("CARGO_PKG_VERSION"))) 451 | .size(style::STATUS_TEXT_SIZE), 452 | ) 453 | .width(200) 454 | .align_items(Alignment::End), 455 | ) 456 | .push(Column::new().width(10)), 457 | ), 458 | ) 459 | .padding(5) 460 | .height(Length::Fill) 461 | .style(style::MainWindow) 462 | .into() 463 | } 464 | 465 | fn theme(&self) -> iced::Theme { 466 | iced::Theme::Dark 467 | } 468 | } 469 | 470 | impl EditorApp { 471 | /// Load persistent state data from file. 472 | fn load_app_state(&mut self) { 473 | if let Some(proj_dirs) = directories_next::ProjectDirs::from("", "", APP_NAME) { 474 | let config_dir = proj_dirs.config_dir().to_path_buf(); 475 | let config_file_path = config_dir.join("config.ron"); 476 | log::info!( 477 | "Loading persistent data from {}", 478 | config_file_path.display() 479 | ); 480 | if let Ok(s) = std::fs::read_to_string(config_file_path) { 481 | if let Ok(app_state) = ron::from_str(s.as_str()) { 482 | self.app_state = app_state; 483 | } 484 | } 485 | } 486 | } 487 | 488 | /// Save persistent state data to file. 489 | fn save_app_state(&self) { 490 | if let Some(proj_dirs) = directories_next::ProjectDirs::from("", "", APP_NAME) { 491 | let config_dir = proj_dirs.config_dir().to_path_buf(); 492 | if let Ok(()) = std::fs::create_dir_all(&config_dir) { 493 | let config_file_path = config_dir.join("config.ron"); 494 | log::info!("Saving persistent data to {}", config_file_path.display()); 495 | if let Ok(config_file) = std::fs::File::create(config_file_path) { 496 | ron::ser::to_writer(config_file, &self.app_state).ok(); 497 | } 498 | } 499 | } 500 | } 501 | 502 | /// Called when device is connected 503 | fn on_device_connected(&mut self) { 504 | log::debug!("Device connected"); 505 | self.status_connection = String::from("Device connected"); 506 | self.request_sound_update = true; 507 | self.request_multi_update = true; 508 | } 509 | 510 | /// Called when device is disconnected 511 | fn on_device_disconnected(&mut self) { 512 | log::debug!("Device disconnected"); 513 | self.status_connection = String::from("Device disconnected"); 514 | self.request_sound_update = false; 515 | self.request_multi_update = false; 516 | self.request_time = None; 517 | } 518 | 519 | /// Process an incoming MIDI message from the device 520 | fn process_midi(&mut self, message: &[u8]) { 521 | match message[0] { 522 | 0xB0..=0xCF => { 523 | // Whenever the device sends a CC or program change message, 524 | // a full parameter update will be requested to keep editor in sync 525 | self.request_sound_update = true; 526 | self.request_multi_update = true; 527 | } 528 | 529 | 0xF0 => { 530 | // Sysex 531 | match message[1] { 532 | midi::sysex::SERVICE_PRESET_DUMP 533 | if message.len() == midi::sysex::PRESET_DUMP_LENGTH => 534 | { 535 | self.process_preset_dump(message); 536 | } 537 | midi::sysex::SERVICE_MULTI_DUMP 538 | if message.len() == midi::sysex::MULTI_DUMP_LENGTH => 539 | { 540 | self.process_multi_dump(message); 541 | } 542 | _ => {} 543 | } 544 | } 545 | 546 | _ => {} 547 | } 548 | } 549 | 550 | /// Process an incoming preset dump from the device 551 | fn process_preset_dump(&mut self, message: &[u8]) { 552 | let preset_id = message[2]; 553 | 554 | log::debug!("Preset dump received with id {:#X}", preset_id); 555 | self.status_communication = String::from(""); 556 | 557 | // Wait a little bit because the dump is possibly echoed by the DAW 558 | std::thread::sleep(Duration::from_millis(100)); 559 | 560 | match preset_id { 561 | 0..=99 => {} 562 | 0x70..=0x73 => { 563 | if self.part_id == preset_id - 0x70 { 564 | let param_values = midi::sysex::unpack_data(&message[3..message.len()]); 565 | midi::sysex::update_sound_params(&mut self.sound_params, ¶m_values); 566 | if let Some(file) = &self.preset_capture_file { 567 | log::debug!("Preset dump captured in file {}", file); 568 | let mut message: Vec = message.to_vec(); 569 | message[2] = 0x70; 570 | std::fs::write(file, message).ok(); 571 | self.preset_capture_file = None; 572 | } 573 | } 574 | } 575 | _ => {} 576 | } 577 | 578 | self.request_time = None; 579 | } 580 | 581 | /// Process an incoming multi dump from the device 582 | fn process_multi_dump(&mut self, message: &[u8]) { 583 | let multi_id = message[2]; 584 | 585 | log::debug!("Multi dump received with id {:#X}", multi_id); 586 | self.status_communication = String::from(""); 587 | 588 | // Wait a little bit because the dump is possibly echoed by the DAW 589 | std::thread::sleep(Duration::from_millis(100)); 590 | 591 | if multi_id == 0x7F { 592 | let param_values = midi::sysex::unpack_data(&message[3..message.len()]); 593 | midi::sysex::update_multi_params(&mut self.multi_params, ¶m_values); 594 | } 595 | 596 | self.request_time = None; 597 | } 598 | } 599 | 600 | /// Return subscription for receiving messages on MIDI merge input 601 | pub fn midi_merge_input_subscription() -> Subscription { 602 | use iced_native::futures::channel::mpsc; 603 | use iced_native::futures::StreamExt; 604 | 605 | enum State { 606 | Starting, 607 | Ready(mpsc::Receiver>), 608 | } 609 | 610 | iced_native::subscription::unfold("MIDI merge input", State::Starting, |state| async move { 611 | match state { 612 | State::Starting => { 613 | let (sender, receiver) = mpsc::channel(64); 614 | ( 615 | Message::MidiMergeSubscriptionReady(sender), 616 | State::Ready(receiver), 617 | ) 618 | } 619 | State::Ready(mut receiver) => { 620 | let message = receiver.select_next_some().await; 621 | ( 622 | Message::MidiMergeInputMessage(message), 623 | State::Ready(receiver), 624 | ) 625 | } 626 | } 627 | }) 628 | } 629 | -------------------------------------------------------------------------------- /src/messages.rs: -------------------------------------------------------------------------------- 1 | //! Application messages definitions 2 | 3 | use crate::params::{MultiParameter, SoundParameter}; 4 | 5 | #[derive(Debug, Clone)] 6 | pub enum Message { 7 | /// Native event from the framework 8 | EventOccurred(iced_native::Event), 9 | 10 | /// Modification of a sound (preset) parameter 11 | SoundParameterChange(SoundParameter, i32), 12 | 13 | /// Modification of a a multi parameter 14 | MultiParameterChange(MultiParameter, i32), 15 | 16 | /// Change of the selected part via the dropdown menu 17 | PartChange(u8), 18 | 19 | /// A new MIDI merge input was selected from the dropdown menu 20 | MergeInputChange(String), 21 | 22 | /// Request the update of parameters from the device 23 | UpdateFromDevice, 24 | 25 | /// Load sysex after the button was pressed 26 | LoadSysexFile, 27 | 28 | /// Save sysex after the button was pressed 29 | SavePresetSysexFile, 30 | 31 | /// Regular tick in 1s intervals 32 | Tick, 33 | 34 | /// Fast regular ticks for processing more time critical tasks 35 | FastTick, 36 | 37 | /// MIDI merge subscription ready, sender is passed as argument 38 | MidiMergeSubscriptionReady(iced_native::futures::channel::mpsc::Sender>), 39 | 40 | /// MIDI message from merge input 41 | #[allow(clippy::enum_variant_names)] 42 | MidiMergeInputMessage(Vec), 43 | } 44 | -------------------------------------------------------------------------------- /src/midi/mod.rs: -------------------------------------------------------------------------------- 1 | //! Module containing all MIDI-related code 2 | 3 | pub mod sysex; 4 | 5 | use std::sync::mpsc; 6 | 7 | use midir::{MidiInput, MidiInputConnection, MidiOutput, MidiOutputConnection}; 8 | 9 | type MpscChannel = (mpsc::Sender>, mpsc::Receiver>); 10 | 11 | /// Container for connections and state 12 | pub struct MidiConnector { 13 | /// Output connection to the device 14 | device_output: Option, 15 | 16 | /// Input connection from the device 17 | device_input: Option>, 18 | 19 | /// MPSC channel to transfer incoming messages from callback to main thread 20 | device_input_mpsc_channel: MpscChannel, 21 | 22 | /// Objects used for port scanning 23 | scan_input: Option, 24 | scan_output: Option, 25 | 26 | /// Vector of port names that are usable as merge inputs 27 | merge_inputs_list: Vec, 28 | 29 | /// Merge input connection 30 | merge_input: Option>, 31 | } 32 | 33 | impl MidiConnector { 34 | /// Constructs a new instance 35 | pub fn new() -> Self { 36 | Self { 37 | device_output: None, 38 | device_input: None, 39 | device_input_mpsc_channel: mpsc::channel(), 40 | scan_input: None, 41 | scan_output: None, 42 | merge_inputs_list: Vec::new(), 43 | merge_input: None, 44 | } 45 | } 46 | 47 | /// Scans the ports and establishes a connection to the device if found 48 | pub fn scan_ports(&mut self) { 49 | if self.scan_input.is_none() { 50 | match MidiInput::new(&(env!("CARGO_PKG_NAME").to_owned() + " scan input")) { 51 | Ok(input) => { 52 | self.scan_input = Some(input); 53 | } 54 | Err(error) => { 55 | log::error!("MIDI scan input error: {}", error); 56 | } 57 | } 58 | } 59 | 60 | if self.scan_input.is_some() { 61 | let mut merge_inputs = Vec::new(); 62 | let input = self.scan_input.as_ref().unwrap(); 63 | 64 | for port in input.ports().iter() { 65 | let port_name = cleanup_port_name(input.port_name(port).unwrap()); 66 | if !port_name.contains("Tooro") && !port_name.contains("tooro") { 67 | merge_inputs.push(port_name); 68 | } 69 | } 70 | 71 | self.merge_inputs_list = merge_inputs; 72 | 73 | let mut connected = false; 74 | let input = self.scan_input.as_ref().unwrap(); 75 | 76 | for port in input.ports().iter() { 77 | let port_name = cleanup_port_name(input.port_name(port).unwrap()); 78 | if port_name.contains("Tooro") { 79 | if self.device_input.is_none() { 80 | log::info!("MIDI input connected to port {}", port_name); 81 | let on_receive_args = OnReceiveArgs { 82 | sender: Some(self.device_input_mpsc_channel.0.clone()), 83 | }; 84 | self.device_input = Some( 85 | self.scan_input 86 | .take() 87 | .unwrap() 88 | .connect(port, "tooro input", on_receive, on_receive_args) 89 | .unwrap(), 90 | ); 91 | } 92 | connected = true; 93 | break; 94 | } 95 | } 96 | 97 | if !connected && self.device_input.is_some() { 98 | log::info!("MIDI input disconnected"); 99 | self.device_input = None; 100 | } 101 | } 102 | 103 | if self.scan_output.is_none() { 104 | match MidiOutput::new(&(env!("CARGO_PKG_NAME").to_owned() + " scan output")) { 105 | Ok(output) => { 106 | self.scan_output = Some(output); 107 | } 108 | Err(error) => { 109 | log::error!("MIDI scan output error: {}", error); 110 | } 111 | } 112 | } 113 | 114 | if self.scan_output.is_some() { 115 | let mut connected = false; 116 | let output = self.scan_output.as_ref().unwrap(); 117 | 118 | for port in output.ports().iter() { 119 | let port_name = cleanup_port_name(output.port_name(port).unwrap()); 120 | if port_name.contains("Tooro") { 121 | if self.device_output.is_none() { 122 | log::info!("MIDI output connected to port {}", port_name); 123 | self.device_output = Some( 124 | self.scan_output 125 | .take() 126 | .unwrap() 127 | .connect(port, "tooro output") 128 | .unwrap(), 129 | ); 130 | } 131 | connected = true; 132 | break; 133 | } 134 | } 135 | 136 | if !connected && self.device_output.is_some() { 137 | log::info!("MIDI output disconnected"); 138 | self.device_output = None; 139 | } 140 | } 141 | } 142 | 143 | /// Sends a message 144 | pub fn send(&mut self, message: &[u8]) { 145 | if let Some(conn) = self.device_output.as_mut() { 146 | conn.send(message).ok(); 147 | } 148 | } 149 | 150 | /// Receives a message 151 | pub fn receive(&mut self) -> Option> { 152 | let receiver = &self.device_input_mpsc_channel.1; 153 | let result = receiver.try_recv(); 154 | 155 | if result.is_err() { 156 | return None; 157 | } 158 | 159 | Some(result.unwrap()) 160 | } 161 | 162 | /// Returns the device connection state 163 | pub fn is_connected(&self) -> bool { 164 | self.device_input.is_some() && self.device_output.is_some() 165 | } 166 | 167 | /// Return a vector of inputs that are suitable for merging 168 | pub fn get_merge_inputs(&self) -> &Vec { 169 | &self.merge_inputs_list 170 | } 171 | 172 | /// Select the input for merging messages 173 | pub fn select_merge_input(&mut self, input_name: String, sender: mpsc::Sender>) { 174 | if self.merge_input.is_some() { 175 | self.merge_input = None; 176 | } 177 | 178 | if self.scan_input.is_none() { 179 | match MidiInput::new(&(env!("CARGO_PKG_NAME").to_owned() + " scan input")) { 180 | Ok(input) => { 181 | self.scan_input = Some(input); 182 | } 183 | Err(error) => { 184 | log::error!("MIDI scan input error: {}", error); 185 | return; 186 | } 187 | } 188 | } 189 | 190 | let input = self.scan_input.as_ref().unwrap(); 191 | 192 | for port in input.ports().iter() { 193 | let port_name = cleanup_port_name(input.port_name(port).unwrap()); 194 | if port_name == input_name { 195 | log::info!("Merge MIDI input connected to port {}", port_name); 196 | let on_receive_args = OnReceiveArgs { 197 | sender: Some(sender), 198 | }; 199 | self.merge_input = Some( 200 | self.scan_input 201 | .take() 202 | .unwrap() 203 | .connect(port, "tooro merge input", on_receive, on_receive_args) 204 | .unwrap(), 205 | ); 206 | break; 207 | } 208 | } 209 | } 210 | } 211 | 212 | /// Arguments for on_receive() callback function 213 | struct OnReceiveArgs { 214 | sender: Option>>, 215 | } 216 | 217 | /// Callback for received messages from device or merge input 218 | fn on_receive(_timestamp: u64, message: &[u8], args: &mut OnReceiveArgs) { 219 | if args.sender.is_some() { 220 | let message = Vec::::from(message); 221 | args.sender.as_ref().unwrap().send(message).ok(); 222 | } 223 | } 224 | 225 | /// Remove client name part from port name on Linux 226 | fn cleanup_port_name(port_name: String) -> String { 227 | #[cfg(target_os = "linux")] 228 | { 229 | if let Some((client_name, remainder)) = port_name.split_once(':') { 230 | if remainder.starts_with(client_name) { 231 | return remainder.to_owned(); 232 | } 233 | } 234 | port_name 235 | } 236 | 237 | #[cfg(not(target_os = "linux"))] 238 | port_name 239 | } 240 | -------------------------------------------------------------------------------- /src/midi/sysex.rs: -------------------------------------------------------------------------------- 1 | //! MIDI sysex definitions and processing 2 | 3 | #![allow(dead_code)] 4 | 5 | use crate::params::{ 6 | GetValue, MultiParameter, MultiParameterValues, SoundParameter, SoundParameterValues, 7 | }; 8 | 9 | // Service ids 10 | pub const SERVICE_MULTI_REQUEST: u8 = 0x01; 11 | pub const SERVICE_PRESET_REQUEST: u8 = 0x02; 12 | pub const SERVICE_PRESET_PARAM_REQUEST: u8 = 0x03; 13 | pub const SERVICE_MULTI_PARAM_REQUEST: u8 = 0x04; 14 | pub const SERVICE_MULTI_DUMP: u8 = 0x11; 15 | pub const SERVICE_PRESET_DUMP: u8 = 0x12; 16 | pub const SERVICE_PRESET_PARAMETER_DUMP: u8 = 0x13; 17 | pub const SERVICE_MULTI_PARAMETER_DUMP: u8 = 0x14; 18 | 19 | // Total dump lengths in bytes (incl. 0xF0 & 0xF7) 20 | pub const MULTI_DUMP_LENGTH: usize = 104; 21 | pub const PRESET_DUMP_LENGTH: usize = 264; 22 | pub const PRESET_PARAM_DUMP_LENGTH: usize = 8; 23 | pub const MULTI_PARAM_DUMP_LENGTH: usize = 8; 24 | 25 | /// Return message for preset request 26 | /// 27 | /// - `preset_id` Preset id, either 0..99 or 0x70..0x73 28 | pub fn preset_request(preset_id: u8) -> Vec { 29 | vec![0xF0, SERVICE_PRESET_REQUEST, preset_id, 0xF7] 30 | } 31 | 32 | /// Return message for multi request 33 | /// 34 | /// - `multi_id` Multi id, either 0..9 or 0x7F 35 | pub fn multi_request(multi_id: u8) -> Vec { 36 | vec![0xF0, SERVICE_MULTI_REQUEST, multi_id, 0xF7] 37 | } 38 | 39 | /// Return message for preset parameter dump 40 | /// 41 | /// - `preset_id` Preset id 0x70..0x73 42 | /// - `param` Sound parameter enum value 43 | /// - `value` Sound parameter value 44 | pub fn preset_param_dump(preset_id: u8, param: &SoundParameter, value: i32) -> Vec { 45 | let (id, value) = match param { 46 | // Osc 1 47 | SoundParameter::Osc1Wave => (0, value * 4), 48 | SoundParameter::Osc1Coarse => (1, value), 49 | SoundParameter::Osc1FMAmount => (2, value * 4), 50 | SoundParameter::Osc1Level => (3, value * 4), 51 | SoundParameter::Osc1Table => (4, value), 52 | SoundParameter::Osc1Fine => (5, value), 53 | SoundParameter::Osc1FMRate => (6, value * 4), 54 | SoundParameter::Osc1Sync => (7, value * 4), 55 | 56 | // Osc 2 57 | SoundParameter::Osc2Wave => (8, value * 4), 58 | SoundParameter::Osc2Coarse => (9, value), 59 | SoundParameter::Osc2FMAmount => (10, value * 4), 60 | SoundParameter::Osc2Level => (11, value * 4), 61 | SoundParameter::Osc2Table => (12, value), 62 | SoundParameter::Osc2Fine => (13, value), 63 | SoundParameter::Osc2FMRate => (14, value * 4), 64 | SoundParameter::Osc2Sync => (15, value * 4), 65 | 66 | // Extra 67 | SoundParameter::ExtraNoise => (16, value * 4), 68 | SoundParameter::ExtraRingMod => (17, value * 4), 69 | 70 | // Shaper 71 | SoundParameter::ShaperCutoff => (18, value * 4), 72 | SoundParameter::ShaperResonance => (19, value * 4), 73 | SoundParameter::ShaperEnvAAmount => (20, value * 4), 74 | SoundParameter::ShaperTrack => (21, value), 75 | SoundParameter::ShaperMode => (22, value), 76 | SoundParameter::ShaperLFO2Amount => (23, value * 4), 77 | 78 | // Filter 79 | SoundParameter::FilterCutoff => (24, value * 4), 80 | SoundParameter::FilterResonance => (25, value * 4), 81 | SoundParameter::FilterEnvFAmount => (26, value * 4), 82 | SoundParameter::FilterTrack => (27, value), 83 | SoundParameter::FilterAfter => (28, value * 4), 84 | SoundParameter::FilterLFO1Amount => (29, value * 4), 85 | 86 | // Env F 87 | SoundParameter::EnvFAttack => (30, value * 4), 88 | SoundParameter::EnvFDecay => (31, value * 4), 89 | SoundParameter::EnvFSustain => (32, value * 4), 90 | SoundParameter::EnvFRelease => (33, value * 4), 91 | SoundParameter::EnvFVelo => (34, value * 4), 92 | SoundParameter::EnvFHold => (35, value * 4), 93 | SoundParameter::EnvFAfter => (36, value * 4), 94 | SoundParameter::EnvFTrigger => (37, value), 95 | 96 | // Env A 97 | SoundParameter::EnvAAttack => (38, value * 4), 98 | SoundParameter::EnvADecay => (39, value * 4), 99 | SoundParameter::EnvASustain => (40, value * 4), 100 | SoundParameter::EnvARelease => (41, value * 4), 101 | SoundParameter::EnvAVelo => (42, value * 4), 102 | SoundParameter::EnvAHold => (43, value * 4), 103 | SoundParameter::EnvAAfter => (44, value * 4), 104 | SoundParameter::EnvATrigger => (45, value), 105 | 106 | // LFO 1 107 | SoundParameter::LFO1Shape => (46, value), 108 | SoundParameter::LFO1Speed => (47, value * 4), 109 | SoundParameter::LFO1Rise => (48, value * 4), 110 | SoundParameter::LFO1Phase => (49, value), 111 | 112 | // LFO 2 113 | SoundParameter::LFO2Shape => (50, value), 114 | SoundParameter::LFO2Speed => (51, value * 4), 115 | SoundParameter::LFO2Rise => (52, value * 4), 116 | SoundParameter::LFO2Phase => (53, value), 117 | 118 | // Arpeggiator 119 | SoundParameter::ArpMode => (54, value), 120 | SoundParameter::ArpGrid => (55, value), 121 | SoundParameter::ArpTempo => (56, (value - 1) * 4), 122 | SoundParameter::ArpHold => (57, value), 123 | 124 | // Amplifier 125 | SoundParameter::AmpLevel => (58, value * 4), 126 | SoundParameter::AmpPan => (59, value * 4), 127 | 128 | // Modulations 129 | SoundParameter::ModEnvFAmount => (60, value * 4), 130 | SoundParameter::ModEnvFTarget => (61, value), 131 | SoundParameter::ModEnvAAmount => (62, value * 4), 132 | SoundParameter::ModEnvATarget => (63, value), 133 | SoundParameter::ModLFO1Amount => (64, value * 4), 134 | SoundParameter::ModLFO1Target => (65, value), 135 | SoundParameter::ModLFO2Amount => (66, value * 4), 136 | SoundParameter::ModLFO2Target => (67, value), 137 | SoundParameter::ModModwheelAmount => (68, value * 4), 138 | SoundParameter::ModModwheelTarget => (69, value), 139 | SoundParameter::ModPitchAmount => (70, value * 4), 140 | SoundParameter::ModPitchTarget => (71, value), 141 | SoundParameter::ModVelocityAmount => (72, value * 4), 142 | SoundParameter::ModVelocityTarget => (73, value), 143 | SoundParameter::ModAftertouchAmount => (74, value * 4), 144 | SoundParameter::ModAftertouchTarget => (75, value), 145 | 146 | // Misc 147 | SoundParameter::Tune => (84, value), 148 | SoundParameter::BendRange => (86, value), 149 | SoundParameter::PolyMode => (87, value), 150 | }; 151 | 152 | let id_low = id & 0x7F; 153 | let id_high = (id >> 7) & 0x7F; 154 | let value_low = (value & 0x7F) as u8; 155 | let value_high = ((value >> 7) & 0x7F) as u8; 156 | 157 | vec![ 158 | 0xF0, 159 | SERVICE_PRESET_PARAMETER_DUMP, 160 | preset_id, 161 | id_low, 162 | id_high, 163 | value_low, 164 | value_high, 165 | 0xF7, 166 | ] 167 | } 168 | 169 | /// Return message for multi parameter dump 170 | /// 171 | /// - `param` Multi parameter enum value 172 | /// - `value` Multi parameter value 173 | pub fn multi_param_dump(param: &MultiParameter, value: i32) -> Vec { 174 | let (id, value) = match param { 175 | // Preset IDs 176 | MultiParameter::PresetPart1 => (0, value), 177 | MultiParameter::PresetPart2 => (1, value), 178 | MultiParameter::PresetPart3 => (2, value), 179 | MultiParameter::PresetPart4 => (3, value), 180 | 181 | // MIDI channels 182 | MultiParameter::ChannelPart1 => (4, value), 183 | MultiParameter::ChannelPart2 => (5, value), 184 | MultiParameter::ChannelPart3 => (6, value), 185 | MultiParameter::ChannelPart4 => (7, value), 186 | 187 | // Volumes 188 | MultiParameter::VolumePart1 => (8, value * 4), 189 | MultiParameter::VolumePart2 => (9, value * 4), 190 | MultiParameter::VolumePart3 => (10, value * 4), 191 | MultiParameter::VolumePart4 => (11, value * 4), 192 | 193 | // Balances 194 | MultiParameter::BalancePart1 => (12, value * 4), 195 | MultiParameter::BalancePart2 => (13, value * 4), 196 | MultiParameter::BalancePart3 => (14, value * 4), 197 | MultiParameter::BalancePart4 => (15, value * 4), 198 | 199 | // FX 200 | MultiParameter::FXLength => (16, value * 4), 201 | MultiParameter::FXFeedback => (17, value * 4), 202 | MultiParameter::FXMix => (18, value * 4), 203 | MultiParameter::FXMode => (19, value), 204 | MultiParameter::FXSpeed => (20, value * 4), 205 | MultiParameter::FXDepth => (21, value * 4), 206 | }; 207 | 208 | let id_low = id & 0x7F; 209 | let id_high = (id >> 7) & 0x7F; 210 | let value_low = (value & 0x7F) as u8; 211 | let value_high = ((value >> 7) & 0x7F) as u8; 212 | 213 | vec![ 214 | 0xF0, 215 | SERVICE_MULTI_PARAMETER_DUMP, 216 | 0, 217 | id_low, 218 | id_high, 219 | value_low, 220 | value_high, 221 | 0xF7, 222 | ] 223 | } 224 | 225 | /// Return message for multi dump 226 | /// 227 | /// - `multi_id` Multi id, either 0..9 or 0x7F 228 | pub fn multi_dump(multi_id: u8, params: &MultiParameterValues) -> Vec { 229 | let mut data = Vec::::new(); 230 | 231 | // Preset IDs 232 | push_param(&mut data, params.get_value(MultiParameter::PresetPart1)); 233 | push_param(&mut data, params.get_value(MultiParameter::PresetPart2)); 234 | push_param(&mut data, params.get_value(MultiParameter::PresetPart3)); 235 | push_param(&mut data, params.get_value(MultiParameter::PresetPart4)); 236 | 237 | // MIDI channels 238 | push_param(&mut data, params.get_value(MultiParameter::ChannelPart1)); 239 | push_param(&mut data, params.get_value(MultiParameter::ChannelPart2)); 240 | push_param(&mut data, params.get_value(MultiParameter::ChannelPart3)); 241 | push_param(&mut data, params.get_value(MultiParameter::ChannelPart4)); 242 | 243 | // Volumes 244 | push_param(&mut data, params.get_value(MultiParameter::VolumePart1) * 4); 245 | push_param(&mut data, params.get_value(MultiParameter::VolumePart2) * 4); 246 | push_param(&mut data, params.get_value(MultiParameter::VolumePart3) * 4); 247 | push_param(&mut data, params.get_value(MultiParameter::VolumePart4) * 4); 248 | 249 | // Balances 250 | push_param( 251 | &mut data, 252 | params.get_value(MultiParameter::BalancePart1) * 4, 253 | ); 254 | push_param( 255 | &mut data, 256 | params.get_value(MultiParameter::BalancePart2) * 4, 257 | ); 258 | push_param( 259 | &mut data, 260 | params.get_value(MultiParameter::BalancePart3) * 4, 261 | ); 262 | push_param( 263 | &mut data, 264 | params.get_value(MultiParameter::BalancePart4) * 4, 265 | ); 266 | 267 | // FX 268 | push_param(&mut data, params.get_value(MultiParameter::FXLength) * 4); 269 | push_param(&mut data, params.get_value(MultiParameter::FXFeedback) * 4); 270 | push_param(&mut data, params.get_value(MultiParameter::FXMix) * 4); 271 | push_param(&mut data, params.get_value(MultiParameter::FXMode)); 272 | push_param(&mut data, params.get_value(MultiParameter::FXSpeed) * 4); 273 | push_param(&mut data, params.get_value(MultiParameter::FXDepth) * 4); 274 | 275 | // Flags etc. and name 276 | data.append(&mut vec![0_u8; 4]); 277 | data.append(&mut vec![32_u8; 32]); 278 | 279 | // Build message 280 | let mut message = Vec::with_capacity(MULTI_DUMP_LENGTH); 281 | message.append(&mut vec![0xF0, SERVICE_MULTI_DUMP, multi_id]); 282 | message.append(&mut pack_data(&data)); 283 | message.push(0xF7); 284 | 285 | message 286 | } 287 | 288 | /// Pack the data and return a vector of it 289 | /// 290 | /// - `data` Data to be packed 291 | pub fn pack_data(data: &[u8]) -> Vec { 292 | let mut result = Vec::new(); 293 | let mut cursor = 0; 294 | let blocks = data.len() / 4; 295 | 296 | for _ in 0..blocks { 297 | let mut tops = 0; 298 | 299 | result.push(data[cursor] & 0x7F); 300 | tops |= (data[cursor] & 0x80) >> 7; 301 | cursor += 1; 302 | result.push(data[cursor] & 0x7F); 303 | tops |= (data[cursor] & 0x80) >> 6; 304 | cursor += 1; 305 | result.push(data[cursor] & 0x7F); 306 | tops |= (data[cursor] & 0x80) >> 5; 307 | cursor += 1; 308 | result.push(data[cursor] & 0x7F); 309 | tops |= (data[cursor] & 0x80) >> 4; 310 | cursor += 1; 311 | 312 | result.push(tops); 313 | } 314 | 315 | result 316 | } 317 | 318 | /// Unpack the data and return a vector of it 319 | /// 320 | /// - `data` Slice of 7-bit sysex payload 321 | pub fn unpack_data(data: &[u8]) -> Vec { 322 | let mut result = Vec::new(); 323 | let mut cursor = 0; 324 | let blocks = data.len() / 5; 325 | 326 | for _ in 0..blocks { 327 | let tops = data[cursor + 4]; 328 | 329 | result.push(data[cursor] | ((tops << 7) & 0x80)); 330 | cursor += 1; 331 | result.push(data[cursor] | ((tops << 6) & 0x80)); 332 | cursor += 1; 333 | result.push(data[cursor] | ((tops << 5) & 0x80)); 334 | cursor += 1; 335 | result.push(data[cursor] | ((tops << 4) & 0x80)); 336 | cursor += 2; 337 | } 338 | 339 | result 340 | } 341 | 342 | /// Update all sound parameters according to sysex data 343 | /// 344 | /// - `params` Parameter map to be updated 345 | /// - `values` Raw values from unpacked sysex data 346 | pub fn update_sound_params(params: &mut SoundParameterValues, values: &[u8]) { 347 | // Osc 1 348 | params.insert(SoundParameter::Osc1Wave, value_from_index(values, 0) / 4); 349 | params.insert(SoundParameter::Osc1Coarse, value_from_index(values, 2)); 350 | params.insert( 351 | SoundParameter::Osc1FMAmount, 352 | value_from_index(values, 4) / 4, 353 | ); 354 | params.insert(SoundParameter::Osc1Level, value_from_index(values, 6) / 4); 355 | params.insert(SoundParameter::Osc1Table, value_from_index(values, 8)); 356 | params.insert(SoundParameter::Osc1Fine, value_from_index(values, 10)); 357 | params.insert(SoundParameter::Osc1FMRate, value_from_index(values, 12) / 4); 358 | params.insert(SoundParameter::Osc1Sync, value_from_index(values, 14) / 4); 359 | 360 | // Osc 2 361 | params.insert(SoundParameter::Osc2Wave, value_from_index(values, 16) / 4); 362 | params.insert(SoundParameter::Osc2Coarse, value_from_index(values, 18)); 363 | params.insert( 364 | SoundParameter::Osc2FMAmount, 365 | value_from_index(values, 20) / 4, 366 | ); 367 | params.insert(SoundParameter::Osc2Level, value_from_index(values, 22) / 4); 368 | params.insert(SoundParameter::Osc2Table, value_from_index(values, 24)); 369 | params.insert(SoundParameter::Osc2Fine, value_from_index(values, 26)); 370 | params.insert(SoundParameter::Osc2FMRate, value_from_index(values, 28) / 4); 371 | params.insert(SoundParameter::Osc2Sync, value_from_index(values, 30) / 4); 372 | 373 | // Extra 374 | params.insert(SoundParameter::ExtraNoise, value_from_index(values, 32) / 4); 375 | params.insert( 376 | SoundParameter::ExtraRingMod, 377 | value_from_index(values, 34) / 4, 378 | ); 379 | 380 | // Shaper 381 | params.insert( 382 | SoundParameter::ShaperCutoff, 383 | value_from_index(values, 36) / 4, 384 | ); 385 | params.insert( 386 | SoundParameter::ShaperResonance, 387 | value_from_index(values, 38) / 4, 388 | ); 389 | params.insert( 390 | SoundParameter::ShaperEnvAAmount, 391 | value_from_index(values, 40) / 4, 392 | ); 393 | params.insert(SoundParameter::ShaperTrack, value_from_index(values, 42)); 394 | params.insert(SoundParameter::ShaperMode, value_from_index(values, 44)); 395 | params.insert( 396 | SoundParameter::ShaperLFO2Amount, 397 | value_from_index(values, 46) / 4, 398 | ); 399 | 400 | // Filter 401 | params.insert( 402 | SoundParameter::FilterCutoff, 403 | value_from_index(values, 48) / 4, 404 | ); 405 | params.insert( 406 | SoundParameter::FilterResonance, 407 | value_from_index(values, 50) / 4, 408 | ); 409 | params.insert( 410 | SoundParameter::FilterEnvFAmount, 411 | value_from_index(values, 52) / 4, 412 | ); 413 | params.insert(SoundParameter::FilterTrack, value_from_index(values, 54)); 414 | params.insert( 415 | SoundParameter::FilterAfter, 416 | value_from_index(values, 56) / 4, 417 | ); 418 | params.insert( 419 | SoundParameter::FilterLFO1Amount, 420 | value_from_index(values, 58) / 4, 421 | ); 422 | 423 | // Env F 424 | params.insert(SoundParameter::EnvFAttack, value_from_index(values, 60) / 4); 425 | params.insert(SoundParameter::EnvFDecay, value_from_index(values, 62) / 4); 426 | params.insert( 427 | SoundParameter::EnvFSustain, 428 | value_from_index(values, 64) / 4, 429 | ); 430 | params.insert( 431 | SoundParameter::EnvFRelease, 432 | value_from_index(values, 66) / 4, 433 | ); 434 | params.insert(SoundParameter::EnvFVelo, value_from_index(values, 68) / 4); 435 | params.insert(SoundParameter::EnvFHold, value_from_index(values, 70) / 4); 436 | params.insert(SoundParameter::EnvFAfter, value_from_index(values, 72) / 4); 437 | params.insert(SoundParameter::EnvFTrigger, value_from_index(values, 74)); 438 | 439 | // Env A 440 | params.insert(SoundParameter::EnvAAttack, value_from_index(values, 76) / 4); 441 | params.insert(SoundParameter::EnvADecay, value_from_index(values, 78) / 4); 442 | params.insert( 443 | SoundParameter::EnvASustain, 444 | value_from_index(values, 80) / 4, 445 | ); 446 | params.insert( 447 | SoundParameter::EnvARelease, 448 | value_from_index(values, 82) / 4, 449 | ); 450 | params.insert(SoundParameter::EnvAVelo, value_from_index(values, 84) / 4); 451 | params.insert(SoundParameter::EnvAHold, value_from_index(values, 86) / 4); 452 | params.insert(SoundParameter::EnvAAfter, value_from_index(values, 88) / 4); 453 | params.insert(SoundParameter::EnvATrigger, value_from_index(values, 90)); 454 | 455 | // LFO 1 456 | params.insert(SoundParameter::LFO1Shape, value_from_index(values, 92)); 457 | params.insert(SoundParameter::LFO1Speed, value_from_index(values, 94) / 4); 458 | params.insert(SoundParameter::LFO1Rise, value_from_index(values, 96) / 4); 459 | params.insert(SoundParameter::LFO1Phase, value_from_index(values, 98)); 460 | 461 | // LFO 2 462 | params.insert(SoundParameter::LFO2Shape, value_from_index(values, 100)); 463 | params.insert(SoundParameter::LFO2Speed, value_from_index(values, 102) / 4); 464 | params.insert(SoundParameter::LFO2Rise, value_from_index(values, 104) / 4); 465 | params.insert(SoundParameter::LFO2Phase, value_from_index(values, 106)); 466 | 467 | // Arpeggiator 468 | params.insert(SoundParameter::ArpMode, value_from_index(values, 108)); 469 | params.insert(SoundParameter::ArpGrid, value_from_index(values, 110)); 470 | params.insert( 471 | SoundParameter::ArpTempo, 472 | value_from_index(values, 112) / 4 + 1, 473 | ); 474 | params.insert(SoundParameter::ArpHold, value_from_index(values, 114)); 475 | 476 | // Amplifier 477 | params.insert(SoundParameter::AmpLevel, value_from_index(values, 116) / 4); 478 | params.insert(SoundParameter::AmpPan, value_from_index(values, 118) / 4); 479 | 480 | // Modulations 481 | params.insert( 482 | SoundParameter::ModEnvFAmount, 483 | value_from_index(values, 120) / 4, 484 | ); 485 | params.insert(SoundParameter::ModEnvFTarget, value_from_index(values, 122)); 486 | params.insert( 487 | SoundParameter::ModEnvAAmount, 488 | value_from_index(values, 124) / 4, 489 | ); 490 | params.insert(SoundParameter::ModEnvATarget, value_from_index(values, 126)); 491 | params.insert( 492 | SoundParameter::ModLFO1Amount, 493 | value_from_index(values, 128) / 4, 494 | ); 495 | params.insert(SoundParameter::ModLFO1Target, value_from_index(values, 130)); 496 | params.insert( 497 | SoundParameter::ModLFO2Amount, 498 | value_from_index(values, 132) / 4, 499 | ); 500 | params.insert(SoundParameter::ModLFO2Target, value_from_index(values, 134)); 501 | params.insert( 502 | SoundParameter::ModModwheelAmount, 503 | value_from_index(values, 136) / 4, 504 | ); 505 | params.insert( 506 | SoundParameter::ModModwheelTarget, 507 | value_from_index(values, 138), 508 | ); 509 | params.insert( 510 | SoundParameter::ModPitchAmount, 511 | value_from_index(values, 140) / 4, 512 | ); 513 | params.insert( 514 | SoundParameter::ModPitchTarget, 515 | value_from_index(values, 142), 516 | ); 517 | params.insert( 518 | SoundParameter::ModVelocityAmount, 519 | value_from_index(values, 144) / 4, 520 | ); 521 | params.insert( 522 | SoundParameter::ModVelocityTarget, 523 | value_from_index(values, 146), 524 | ); 525 | params.insert( 526 | SoundParameter::ModAftertouchAmount, 527 | value_from_index(values, 148) / 4, 528 | ); 529 | params.insert( 530 | SoundParameter::ModAftertouchTarget, 531 | value_from_index(values, 150), 532 | ); 533 | 534 | // Misc 535 | params.insert(SoundParameter::Tune, value_from_index(values, 168)); 536 | params.insert(SoundParameter::BendRange, value_from_index(values, 172)); 537 | params.insert(SoundParameter::PolyMode, value_from_index(values, 174)); 538 | } 539 | 540 | /// Update all multi parameters according to sysex data 541 | /// 542 | /// - `params` Parameter map to be updated 543 | /// - `values` Raw values from unpacked sysex data 544 | pub fn update_multi_params(params: &mut MultiParameterValues, values: &[u8]) { 545 | // Preset IDs 546 | params.insert(MultiParameter::PresetPart1, value_from_index(values, 0)); 547 | params.insert(MultiParameter::PresetPart2, value_from_index(values, 2)); 548 | params.insert(MultiParameter::PresetPart3, value_from_index(values, 4)); 549 | params.insert(MultiParameter::PresetPart4, value_from_index(values, 6)); 550 | 551 | // MIDI channels 552 | params.insert(MultiParameter::ChannelPart1, value_from_index(values, 8)); 553 | params.insert(MultiParameter::ChannelPart2, value_from_index(values, 10)); 554 | params.insert(MultiParameter::ChannelPart3, value_from_index(values, 12)); 555 | params.insert(MultiParameter::ChannelPart4, value_from_index(values, 14)); 556 | 557 | // Volumes 558 | params.insert( 559 | MultiParameter::VolumePart1, 560 | value_from_index(values, 16) / 4, 561 | ); 562 | params.insert( 563 | MultiParameter::VolumePart2, 564 | value_from_index(values, 18) / 4, 565 | ); 566 | params.insert( 567 | MultiParameter::VolumePart3, 568 | value_from_index(values, 20) / 4, 569 | ); 570 | params.insert( 571 | MultiParameter::VolumePart4, 572 | value_from_index(values, 22) / 4, 573 | ); 574 | 575 | // Balances 576 | params.insert( 577 | MultiParameter::BalancePart1, 578 | value_from_index(values, 24) / 4, 579 | ); 580 | params.insert( 581 | MultiParameter::BalancePart2, 582 | value_from_index(values, 26) / 4, 583 | ); 584 | params.insert( 585 | MultiParameter::BalancePart3, 586 | value_from_index(values, 28) / 4, 587 | ); 588 | params.insert( 589 | MultiParameter::BalancePart4, 590 | value_from_index(values, 30) / 4, 591 | ); 592 | 593 | // FX 594 | params.insert(MultiParameter::FXLength, value_from_index(values, 32) / 4); 595 | params.insert(MultiParameter::FXFeedback, value_from_index(values, 34) / 4); 596 | params.insert(MultiParameter::FXMix, value_from_index(values, 36) / 4); 597 | params.insert(MultiParameter::FXMode, value_from_index(values, 38)); 598 | params.insert(MultiParameter::FXSpeed, value_from_index(values, 40) / 4); 599 | params.insert(MultiParameter::FXDepth, value_from_index(values, 40) / 4); 600 | } 601 | 602 | /// Return parameter value as i32 from values vector addressed by index 603 | /// 604 | /// - `values` Vector containing values 605 | /// - `index` Start index in values vector 606 | fn value_from_index(values: &[u8], index: usize) -> i32 { 607 | i16::from_le_bytes([values[index], values[index + 1]]) as i32 608 | } 609 | 610 | /// Rescale a value from one range to another 611 | /// 612 | /// - `value` Input value 613 | /// - `in_min` Minimum input value 614 | /// - `in_max` Maximum input value 615 | /// - `out_min` Minimum output value 616 | /// - `out_max` Maximum output value 617 | fn rescale(value: i32, in_min: i32, in_max: i32, out_min: i32, out_max: i32) -> i32 { 618 | (value - in_min) * (out_max - out_min) / (in_max - in_min) + out_min 619 | } 620 | 621 | /// Push a parameter value to a vector of u8 622 | /// 623 | /// - `vector` Vector to be updated 624 | /// - `value` Parameter value 625 | fn push_param(vector: &mut Vec, value: i32) { 626 | vector.push((value & 0xFF) as u8); 627 | vector.push((value >> 8) as u8); 628 | } 629 | -------------------------------------------------------------------------------- /src/params.rs: -------------------------------------------------------------------------------- 1 | //! Definitions and methods for the preset and multi parameters 2 | 3 | use std::collections::HashMap; 4 | use std::ops::RangeInclusive; 5 | 6 | /// Enum containing all preset parameters 7 | #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] 8 | pub enum SoundParameter { 9 | // Osc 1 10 | Osc1Wave, 11 | Osc1Coarse, 12 | Osc1FMAmount, 13 | Osc1Level, 14 | Osc1Table, 15 | Osc1Fine, 16 | Osc1FMRate, 17 | Osc1Sync, 18 | 19 | // Osc 2 20 | Osc2Wave, 21 | Osc2FMAmount, 22 | Osc2Level, 23 | Osc2Coarse, 24 | Osc2Table, 25 | Osc2Fine, 26 | Osc2FMRate, 27 | Osc2Sync, 28 | 29 | // Extra 30 | ExtraNoise, 31 | ExtraRingMod, 32 | 33 | // Filter 34 | FilterCutoff, 35 | FilterResonance, 36 | FilterEnvFAmount, 37 | FilterTrack, 38 | FilterAfter, 39 | FilterLFO1Amount, 40 | 41 | // Shaper 42 | ShaperCutoff, 43 | ShaperResonance, 44 | ShaperEnvAAmount, 45 | ShaperTrack, 46 | ShaperMode, 47 | ShaperLFO2Amount, 48 | 49 | // Env F 50 | EnvFAttack, 51 | EnvFDecay, 52 | EnvFSustain, 53 | EnvFRelease, 54 | EnvFVelo, 55 | EnvFHold, 56 | EnvFAfter, 57 | EnvFTrigger, 58 | 59 | // Env A 60 | EnvAAttack, 61 | EnvADecay, 62 | EnvASustain, 63 | EnvARelease, 64 | EnvAVelo, 65 | EnvAHold, 66 | EnvAAfter, 67 | EnvATrigger, 68 | 69 | // LFO 1 70 | LFO1Shape, 71 | LFO1Speed, 72 | LFO1Rise, 73 | LFO1Phase, 74 | 75 | // LFO 2 76 | LFO2Shape, 77 | LFO2Speed, 78 | LFO2Rise, 79 | LFO2Phase, 80 | 81 | // Arpeggiator 82 | ArpMode, 83 | ArpGrid, 84 | ArpTempo, 85 | ArpHold, 86 | 87 | // Amplifier 88 | AmpLevel, 89 | AmpPan, 90 | 91 | // Modulations 92 | ModEnvFAmount, 93 | ModEnvFTarget, 94 | ModEnvAAmount, 95 | ModEnvATarget, 96 | ModLFO1Amount, 97 | ModLFO1Target, 98 | ModLFO2Amount, 99 | ModLFO2Target, 100 | ModModwheelAmount, 101 | ModModwheelTarget, 102 | ModPitchAmount, 103 | ModPitchTarget, 104 | ModVelocityAmount, 105 | ModVelocityTarget, 106 | ModAftertouchAmount, 107 | ModAftertouchTarget, 108 | 109 | // Misc 110 | BendRange, 111 | Tune, 112 | PolyMode, 113 | } 114 | 115 | impl SoundParameter { 116 | /// Return the value range of the parameter 117 | pub fn get_range(&self) -> RangeInclusive { 118 | match self { 119 | // Default for bipolar 120 | SoundParameter::Osc1FMRate 121 | | SoundParameter::Osc2FMRate 122 | | SoundParameter::ShaperEnvAAmount 123 | | SoundParameter::ShaperLFO2Amount 124 | | SoundParameter::FilterEnvFAmount 125 | | SoundParameter::FilterLFO1Amount 126 | | SoundParameter::FilterAfter 127 | | SoundParameter::AmpPan 128 | | SoundParameter::ModEnvFAmount 129 | | SoundParameter::ModEnvAAmount 130 | | SoundParameter::ModLFO1Amount 131 | | SoundParameter::ModLFO2Amount 132 | | SoundParameter::ModModwheelAmount 133 | | SoundParameter::ModPitchAmount 134 | | SoundParameter::ModVelocityAmount 135 | | SoundParameter::ModAftertouchAmount => RangeInclusive::new(-128, 128), 136 | 137 | // Special ranges 138 | SoundParameter::Osc1Coarse | SoundParameter::Osc2Coarse => RangeInclusive::new(-36, 36), 139 | SoundParameter::Osc1Fine | SoundParameter::Osc2Fine => RangeInclusive::new(-99, 99), 140 | SoundParameter::ShaperTrack | SoundParameter::FilterTrack => { 141 | RangeInclusive::new(-20, 20) 142 | } 143 | SoundParameter::ArpTempo => RangeInclusive::new(1, 199), 144 | SoundParameter::BendRange => RangeInclusive::new(0, 127), 145 | SoundParameter::Tune => RangeInclusive::new(-99, 99), 146 | 147 | // Lists 148 | SoundParameter::Osc1Table | SoundParameter::Osc2Table => RangeInclusive::new(0, 10), 149 | SoundParameter::ShaperMode => RangeInclusive::new(0, 2), 150 | SoundParameter::EnvATrigger | SoundParameter::EnvFTrigger => RangeInclusive::new(0, 2), 151 | SoundParameter::LFO1Shape | SoundParameter::LFO2Shape => RangeInclusive::new(0, 7), 152 | SoundParameter::LFO1Phase | SoundParameter::LFO2Phase => RangeInclusive::new(0, 5), 153 | SoundParameter::ArpMode => RangeInclusive::new(0, 7), 154 | SoundParameter::ArpGrid => RangeInclusive::new(0, 6), 155 | SoundParameter::ModEnvFTarget 156 | | SoundParameter::ModEnvATarget 157 | | SoundParameter::ModLFO1Target 158 | | SoundParameter::ModLFO2Target 159 | | SoundParameter::ModModwheelTarget 160 | | SoundParameter::ModPitchTarget 161 | | SoundParameter::ModVelocityTarget 162 | | SoundParameter::ModAftertouchTarget => RangeInclusive::new(0, 21), 163 | 164 | // Boolean 165 | SoundParameter::ArpHold | SoundParameter::PolyMode => RangeInclusive::new(0, 1), 166 | 167 | // Default 168 | _ => RangeInclusive::new(0, 255), 169 | } 170 | } 171 | 172 | /// Return the default value for the parameter 173 | pub fn get_default(&self) -> i32 { 174 | 0 175 | } 176 | } 177 | 178 | /// Hashmap type for preset parameters 179 | pub type SoundParameterValues = HashMap; 180 | 181 | /// Enum containing all multi parameters 182 | #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] 183 | pub enum MultiParameter { 184 | // Preset IDs 185 | PresetPart1, 186 | PresetPart2, 187 | PresetPart3, 188 | PresetPart4, 189 | 190 | // MIDI channels 191 | ChannelPart1, 192 | ChannelPart2, 193 | ChannelPart3, 194 | ChannelPart4, 195 | 196 | // Volumes 197 | VolumePart1, 198 | VolumePart2, 199 | VolumePart3, 200 | VolumePart4, 201 | 202 | // Balances 203 | BalancePart1, 204 | BalancePart2, 205 | BalancePart3, 206 | BalancePart4, 207 | 208 | // FX 209 | FXLength, 210 | FXFeedback, 211 | FXMix, 212 | FXMode, 213 | FXSpeed, 214 | FXDepth, 215 | } 216 | 217 | impl MultiParameter { 218 | /// Return the value range of the parameter 219 | pub fn get_range(&self) -> RangeInclusive { 220 | match self { 221 | // Preset IDs 222 | MultiParameter::PresetPart1 223 | | MultiParameter::PresetPart2 224 | | MultiParameter::PresetPart3 225 | | MultiParameter::PresetPart4 => RangeInclusive::new(0, 99), 226 | 227 | // MIDI channels 228 | MultiParameter::ChannelPart1 229 | | MultiParameter::ChannelPart2 230 | | MultiParameter::ChannelPart3 231 | | MultiParameter::ChannelPart4 => RangeInclusive::new(0, 15), 232 | 233 | // Volumes 234 | MultiParameter::VolumePart1 235 | | MultiParameter::VolumePart2 236 | | MultiParameter::VolumePart3 237 | | MultiParameter::VolumePart4 => RangeInclusive::new(0, 255), 238 | 239 | // Balances 240 | MultiParameter::BalancePart1 241 | | MultiParameter::BalancePart2 242 | | MultiParameter::BalancePart3 243 | | MultiParameter::BalancePart4 => RangeInclusive::new(-128, 128), 244 | 245 | // FX 246 | MultiParameter::FXLength 247 | | MultiParameter::FXFeedback 248 | | MultiParameter::FXMix 249 | | MultiParameter::FXSpeed 250 | | MultiParameter::FXDepth => RangeInclusive::new(0, 255), 251 | MultiParameter::FXMode => RangeInclusive::new(0, 4), 252 | } 253 | } 254 | 255 | /// Return the default value for the parameter 256 | pub fn get_default(&self) -> i32 { 257 | 0 258 | } 259 | } 260 | 261 | /// Hashmap type for preset parameters 262 | pub type MultiParameterValues = HashMap; 263 | 264 | /// Trait for returning the current value of a parameter 265 | pub trait GetValue { 266 | /// Return the value of the requested parameter 267 | fn get_value(&self, param: T) -> i32; 268 | } 269 | 270 | /// GetValue trait implementation for preset parameters 271 | impl GetValue for SoundParameterValues { 272 | /// Return the value of the requested preset parameter 273 | fn get_value(&self, param: SoundParameter) -> i32 { 274 | *self.get(¶m).unwrap_or(¶m.get_default()) 275 | } 276 | } 277 | 278 | /// GetValue trait implementation for multi parameters 279 | impl GetValue for MultiParameterValues { 280 | /// Return the value of the requested multi parameter 281 | fn get_value(&self, param: MultiParameter) -> i32 { 282 | *self.get(¶m).unwrap_or(¶m.get_default()) 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/ui/elements/arp_grid_list.rs: -------------------------------------------------------------------------------- 1 | //! Dropdown menu for the arpeggiator grid values 2 | 3 | use iced::widget::{Column, Container, PickList, Row, Text}; 4 | 5 | use crate::messages::Message; 6 | use crate::params::SoundParameter; 7 | use crate::style; 8 | 9 | pub fn arp_grid_list(label: &str, sound_param: SoundParameter, value: i32) -> Container { 10 | let value = match value { 11 | 0 => Some(ArpGrid::Div48), 12 | 1 => Some(ArpGrid::Div32), 13 | 2 => Some(ArpGrid::Div24), 14 | 3 => Some(ArpGrid::Div16), 15 | 4 => Some(ArpGrid::Div12), 16 | 5 => Some(ArpGrid::Div8), 17 | 6 => Some(ArpGrid::Div6), 18 | _ => None, 19 | }; 20 | let pick_list = PickList::new(&ArpGrid::ALL[..], value, move |v| { 21 | Message::SoundParameterChange(sound_param, v as i32) 22 | }) 23 | .style(style::PickList) 24 | .text_size(style::LIST_ITEM_TEXT_SIZE); 25 | 26 | Container::new( 27 | Row::new() 28 | .push( 29 | Column::new() 30 | .push( 31 | Text::new(label) 32 | .size(style::PARAM_LABEL_TEXT_SIZE) 33 | .width(style::PARAM_LABEL_WIDTH), 34 | ) 35 | .padding([4, 0, 0, 0]), 36 | ) 37 | .push(pick_list), 38 | ) 39 | } 40 | 41 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 42 | pub enum ArpGrid { 43 | Div48, 44 | Div32, 45 | Div24, 46 | Div16, 47 | Div12, 48 | Div8, 49 | Div6, 50 | } 51 | 52 | impl ArpGrid { 53 | const ALL: [ArpGrid; 7] = [ 54 | ArpGrid::Div48, 55 | ArpGrid::Div32, 56 | ArpGrid::Div24, 57 | ArpGrid::Div16, 58 | ArpGrid::Div12, 59 | ArpGrid::Div8, 60 | ArpGrid::Div6, 61 | ]; 62 | } 63 | 64 | impl std::fmt::Display for ArpGrid { 65 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 66 | write!( 67 | f, 68 | "{}", 69 | match self { 70 | ArpGrid::Div48 => "1/2", 71 | ArpGrid::Div32 => "1/2 triplet", 72 | ArpGrid::Div24 => "1/4", 73 | ArpGrid::Div16 => "1/4 triplet", 74 | ArpGrid::Div12 => "1/8", 75 | ArpGrid::Div8 => "1/8 triplet", 76 | ArpGrid::Div6 => "1/16", 77 | } 78 | ) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/ui/elements/arp_mode_list.rs: -------------------------------------------------------------------------------- 1 | //! Dropdown menu for the arpeggiator modes 2 | 3 | use iced::widget::{Column, Container, PickList, Row, Text}; 4 | 5 | use crate::messages::Message; 6 | use crate::params::SoundParameter; 7 | use crate::style; 8 | 9 | pub fn arp_mode_list(label: &str, sound_param: SoundParameter, value: i32) -> Container { 10 | let value = match value { 11 | 0 => Some(ArpMode::Off), 12 | 1 => Some(ArpMode::Up), 13 | 2 => Some(ArpMode::Down), 14 | 3 => Some(ArpMode::Random), 15 | 4 => Some(ArpMode::Up2), 16 | 5 => Some(ArpMode::Down2), 17 | 6 => Some(ArpMode::Up4), 18 | 7 => Some(ArpMode::Down4), 19 | _ => None, 20 | }; 21 | let pick_list = PickList::new(&ArpMode::ALL[..], value, move |v| { 22 | Message::SoundParameterChange(sound_param, v as i32) 23 | }) 24 | .style(style::PickList) 25 | .text_size(style::LIST_ITEM_TEXT_SIZE); 26 | 27 | Container::new( 28 | Row::new() 29 | .push( 30 | Column::new() 31 | .push( 32 | Text::new(label) 33 | .size(style::PARAM_LABEL_TEXT_SIZE) 34 | .width(style::PARAM_LABEL_WIDTH), 35 | ) 36 | .padding([4, 0, 0, 0]), 37 | ) 38 | .push(pick_list), 39 | ) 40 | } 41 | 42 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 43 | pub enum ArpMode { 44 | Off, 45 | Up, 46 | Down, 47 | Random, 48 | Up2, 49 | Down2, 50 | Up4, 51 | Down4, 52 | } 53 | 54 | impl ArpMode { 55 | const ALL: [ArpMode; 8] = [ 56 | ArpMode::Off, 57 | ArpMode::Up, 58 | ArpMode::Down, 59 | ArpMode::Random, 60 | ArpMode::Up2, 61 | ArpMode::Down2, 62 | ArpMode::Up4, 63 | ArpMode::Down4, 64 | ]; 65 | } 66 | 67 | impl std::fmt::Display for ArpMode { 68 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 69 | write!( 70 | f, 71 | "{}", 72 | match self { 73 | ArpMode::Off => "Off", 74 | ArpMode::Up => "Up", 75 | ArpMode::Down => "Down", 76 | ArpMode::Random => "Random", 77 | ArpMode::Up2 => "Up 2", 78 | ArpMode::Down2 => "Down 2", 79 | ArpMode::Up4 => "Up 4", 80 | ArpMode::Down4 => "Down 4", 81 | } 82 | ) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/ui/elements/checkbox.rs: -------------------------------------------------------------------------------- 1 | //! Checkbox control wrapped in a container with label 2 | 3 | use iced::widget::{Checkbox, Container, Row, Text}; 4 | 5 | use crate::messages::Message; 6 | use crate::params::SoundParameter; 7 | use crate::style; 8 | 9 | pub fn checkbox_with_labels<'a>( 10 | label: &'a str, 11 | text: &'a str, 12 | sound_param: SoundParameter, 13 | value: i32, 14 | ) -> Container<'a, Message> { 15 | let checkbox = Checkbox::new(text, value != 0, move |v| { 16 | Message::SoundParameterChange(sound_param, v as i32) 17 | }) 18 | .style(style::Checkbox) 19 | .text_size(style::LIST_ITEM_TEXT_SIZE) 20 | .spacing(7); 21 | 22 | Container::new( 23 | Row::new() 24 | .push( 25 | Text::new(label) 26 | .size(style::PARAM_LABEL_TEXT_SIZE) 27 | .width(style::PARAM_LABEL_WIDTH), 28 | ) 29 | .push(checkbox), 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/ui/elements/env_trigger_list.rs: -------------------------------------------------------------------------------- 1 | //! Dropdown menu for the envelope trigger modes 2 | 3 | use iced::widget::{Column, Container, PickList, Row, Text}; 4 | 5 | use crate::messages::Message; 6 | use crate::params::SoundParameter; 7 | use crate::style; 8 | 9 | pub fn env_trigger_list( 10 | label: &str, 11 | sound_param: SoundParameter, 12 | value: i32, 13 | ) -> Container { 14 | let value = match value { 15 | 0 => Some(EnvTrigger::Always), 16 | 1 => Some(EnvTrigger::Never), 17 | 2 => Some(EnvTrigger::Continue), 18 | _ => None, 19 | }; 20 | let pick_list = PickList::new(&EnvTrigger::ALL[..], value, move |v| { 21 | Message::SoundParameterChange(sound_param, v as i32) 22 | }) 23 | .style(style::PickList) 24 | .text_size(style::LIST_ITEM_TEXT_SIZE); 25 | 26 | Container::new( 27 | Row::new() 28 | .push( 29 | Column::new() 30 | .push( 31 | Text::new(label) 32 | .size(style::PARAM_LABEL_TEXT_SIZE) 33 | .width(style::PARAM_LABEL_WIDTH), 34 | ) 35 | .padding([4, 0, 0, 0]), 36 | ) 37 | .push(pick_list), 38 | ) 39 | } 40 | 41 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 42 | pub enum EnvTrigger { 43 | Always, 44 | Never, 45 | Continue, 46 | } 47 | 48 | impl EnvTrigger { 49 | const ALL: [EnvTrigger; 3] = [EnvTrigger::Always, EnvTrigger::Never, EnvTrigger::Continue]; 50 | } 51 | 52 | impl std::fmt::Display for EnvTrigger { 53 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 54 | write!( 55 | f, 56 | "{}", 57 | match self { 58 | EnvTrigger::Always => "Always", 59 | EnvTrigger::Never => "Never", 60 | EnvTrigger::Continue => "Continue", 61 | } 62 | ) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/ui/elements/fx_mode_list.rs: -------------------------------------------------------------------------------- 1 | //! Dropdown menu for the multi fx modes 2 | 3 | use iced::widget::{Column, Container, PickList, Row, Text}; 4 | 5 | use crate::messages::Message; 6 | use crate::params::MultiParameter; 7 | use crate::style; 8 | 9 | pub fn fx_mode_list(label: &str, multi_param: MultiParameter, value: i32) -> Container { 10 | let value = match value { 11 | 0 => Some(FXMode::Off), 12 | 1 => Some(FXMode::MonoDelay), 13 | 2 => Some(FXMode::ChorusFlanger), 14 | 3 => Some(FXMode::StereoDelay), 15 | _ => None, 16 | }; 17 | let pick_list = PickList::new(&FXMode::ALL[..], value, move |v| { 18 | Message::MultiParameterChange(multi_param, v as i32) 19 | }) 20 | .style(style::PickList) 21 | .text_size(style::LIST_ITEM_TEXT_SIZE); 22 | 23 | Container::new( 24 | Row::new() 25 | .push( 26 | Column::new() 27 | .push( 28 | Text::new(label) 29 | .size(style::PARAM_LABEL_TEXT_SIZE) 30 | .width(style::PARAM_LABEL_WIDTH), 31 | ) 32 | .padding([4, 0, 0, 0]), 33 | ) 34 | .push(pick_list), 35 | ) 36 | } 37 | 38 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 39 | pub enum FXMode { 40 | Off, 41 | MonoDelay, 42 | ChorusFlanger, 43 | StereoDelay, 44 | } 45 | 46 | impl FXMode { 47 | const ALL: [FXMode; 4] = [ 48 | FXMode::Off, 49 | FXMode::MonoDelay, 50 | FXMode::ChorusFlanger, 51 | FXMode::StereoDelay, 52 | ]; 53 | } 54 | 55 | impl std::fmt::Display for FXMode { 56 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 57 | write!( 58 | f, 59 | "{}", 60 | match self { 61 | FXMode::Off => "Off", 62 | FXMode::MonoDelay => "Mono Delay", 63 | FXMode::ChorusFlanger => "Chorus/Flanger", 64 | FXMode::StereoDelay => "Stereo Delay", 65 | } 66 | ) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/ui/elements/lfo_phase_list.rs: -------------------------------------------------------------------------------- 1 | //! Dropdown menu for the LFO phase values 2 | 3 | use iced::widget::{Column, Container, PickList, Row, Text}; 4 | 5 | use crate::messages::Message; 6 | use crate::params::SoundParameter; 7 | use crate::style; 8 | 9 | pub fn lfo_phase_list(label: &str, sound_param: SoundParameter, value: i32) -> Container { 10 | let value = match value { 11 | 0 => Some(LFOPhase::Free), 12 | 1 => Some(LFOPhase::Random), 13 | 2 => Some(LFOPhase::Phase0), 14 | 3 => Some(LFOPhase::Phase90), 15 | 4 => Some(LFOPhase::Phase180), 16 | 5 => Some(LFOPhase::Phase270), 17 | 18 | _ => None, 19 | }; 20 | let pick_list = PickList::new(&LFOPhase::ALL[..], value, move |v| { 21 | Message::SoundParameterChange(sound_param, v as i32) 22 | }) 23 | .style(style::PickList) 24 | .text_size(style::LIST_ITEM_TEXT_SIZE); 25 | 26 | Container::new( 27 | Row::new() 28 | .push( 29 | Column::new() 30 | .push( 31 | Text::new(label) 32 | .size(style::PARAM_LABEL_TEXT_SIZE) 33 | .width(style::PARAM_LABEL_WIDTH), 34 | ) 35 | .padding([4, 0, 0, 0]), 36 | ) 37 | .push(pick_list), 38 | ) 39 | } 40 | 41 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 42 | pub enum LFOPhase { 43 | Free, 44 | Random, 45 | Phase0, 46 | Phase90, 47 | Phase180, 48 | Phase270, 49 | } 50 | 51 | impl LFOPhase { 52 | const ALL: [LFOPhase; 6] = [ 53 | LFOPhase::Free, 54 | LFOPhase::Random, 55 | LFOPhase::Phase0, 56 | LFOPhase::Phase90, 57 | LFOPhase::Phase180, 58 | LFOPhase::Phase270, 59 | ]; 60 | } 61 | 62 | impl std::fmt::Display for LFOPhase { 63 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 64 | write!( 65 | f, 66 | "{}", 67 | match self { 68 | LFOPhase::Free => "Free", 69 | LFOPhase::Random => "Random", 70 | LFOPhase::Phase0 => "0°", 71 | LFOPhase::Phase90 => "90°", 72 | LFOPhase::Phase180 => "180°", 73 | LFOPhase::Phase270 => "270°", 74 | } 75 | ) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/ui/elements/lfo_shape_list.rs: -------------------------------------------------------------------------------- 1 | //! Dropdown menu for the LFO shapes 2 | 3 | use iced::widget::{Column, Container, PickList, Row, Text}; 4 | 5 | use crate::messages::Message; 6 | use crate::params::SoundParameter; 7 | use crate::style; 8 | 9 | pub fn lfo_shape_list(label: &str, sound_param: SoundParameter, value: i32) -> Container { 10 | let value = match value { 11 | 0 => Some(LFOShape::Triangle), 12 | 1 => Some(LFOShape::RampUp), 13 | 2 => Some(LFOShape::RampDown), 14 | 3 => Some(LFOShape::Square), 15 | 4 => Some(LFOShape::MWave), 16 | 5 => Some(LFOShape::Random), 17 | 6 => Some(LFOShape::Slew), 18 | 7 => Some(LFOShape::AM), 19 | _ => None, 20 | }; 21 | let pick_list = PickList::new(&LFOShape::ALL[..], value, move |v| { 22 | Message::SoundParameterChange(sound_param, v as i32) 23 | }) 24 | .style(style::PickList) 25 | .text_size(style::LIST_ITEM_TEXT_SIZE); 26 | 27 | Container::new( 28 | Row::new() 29 | .push( 30 | Column::new() 31 | .push( 32 | Text::new(label) 33 | .size(style::PARAM_LABEL_TEXT_SIZE) 34 | .width(style::PARAM_LABEL_WIDTH), 35 | ) 36 | .padding([4, 0, 0, 0]), 37 | ) 38 | .push(pick_list), 39 | ) 40 | } 41 | 42 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 43 | pub enum LFOShape { 44 | Triangle, 45 | RampUp, 46 | RampDown, 47 | Square, 48 | MWave, 49 | Random, 50 | Slew, 51 | AM, 52 | } 53 | 54 | impl LFOShape { 55 | const ALL: [LFOShape; 8] = [ 56 | LFOShape::Triangle, 57 | LFOShape::RampUp, 58 | LFOShape::RampDown, 59 | LFOShape::Square, 60 | LFOShape::MWave, 61 | LFOShape::Random, 62 | LFOShape::Slew, 63 | LFOShape::AM, 64 | ]; 65 | } 66 | 67 | impl std::fmt::Display for LFOShape { 68 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 69 | write!( 70 | f, 71 | "{}", 72 | match self { 73 | LFOShape::Triangle => "Triangle", 74 | LFOShape::RampUp => "Ramp Up", 75 | LFOShape::RampDown => "Ramp Down", 76 | LFOShape::Square => "Square", 77 | LFOShape::MWave => "M-Wave", 78 | LFOShape::Random => "Random", 79 | LFOShape::Slew => "Slew", 80 | LFOShape::AM => "AM", 81 | } 82 | ) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/ui/elements/midi_channel_list.rs: -------------------------------------------------------------------------------- 1 | //! Dropdown menu for the MIDI channels 2 | 3 | use iced::widget::{Column, Container, PickList, Row, Text}; 4 | 5 | use crate::messages::Message; 6 | use crate::params::MultiParameter; 7 | use crate::style; 8 | 9 | pub fn midi_channel_list( 10 | label: &str, 11 | multi_param: MultiParameter, 12 | value: i32, 13 | ) -> Container { 14 | let value = match value { 15 | 0 => Some(MidiChannel::Omni), 16 | 1 => Some(MidiChannel::Channel1), 17 | 2 => Some(MidiChannel::Channel2), 18 | 3 => Some(MidiChannel::Channel3), 19 | 4 => Some(MidiChannel::Channel4), 20 | 5 => Some(MidiChannel::Channel5), 21 | 6 => Some(MidiChannel::Channel6), 22 | 7 => Some(MidiChannel::Channel7), 23 | 8 => Some(MidiChannel::Channel8), 24 | 9 => Some(MidiChannel::Channel9), 25 | 10 => Some(MidiChannel::Channel10), 26 | 11 => Some(MidiChannel::Channel11), 27 | 12 => Some(MidiChannel::Channel12), 28 | 13 => Some(MidiChannel::Channel13), 29 | 14 => Some(MidiChannel::Channel14), 30 | 15 => Some(MidiChannel::Channel15), 31 | 16 => Some(MidiChannel::Channel16), 32 | _ => None, 33 | }; 34 | let pick_list = PickList::new(&MidiChannel::ALL[..], value, move |v| { 35 | Message::MultiParameterChange(multi_param, v as i32) 36 | }) 37 | .style(style::PickList) 38 | .text_size(style::LIST_ITEM_TEXT_SIZE); 39 | 40 | Container::new( 41 | Row::new() 42 | .push( 43 | Column::new() 44 | .push( 45 | Text::new(label) 46 | .size(style::PARAM_LABEL_TEXT_SIZE) 47 | .width(style::PARAM_LABEL_WIDTH), 48 | ) 49 | .padding([4, 0, 0, 0]), 50 | ) 51 | .push(pick_list), 52 | ) 53 | } 54 | 55 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 56 | pub enum MidiChannel { 57 | Omni, 58 | Channel1, 59 | Channel2, 60 | Channel3, 61 | Channel4, 62 | Channel5, 63 | Channel6, 64 | Channel7, 65 | Channel8, 66 | Channel9, 67 | Channel10, 68 | Channel11, 69 | Channel12, 70 | Channel13, 71 | Channel14, 72 | Channel15, 73 | Channel16, 74 | } 75 | 76 | impl MidiChannel { 77 | const ALL: [MidiChannel; 17] = [ 78 | MidiChannel::Omni, 79 | MidiChannel::Channel1, 80 | MidiChannel::Channel2, 81 | MidiChannel::Channel3, 82 | MidiChannel::Channel4, 83 | MidiChannel::Channel5, 84 | MidiChannel::Channel6, 85 | MidiChannel::Channel7, 86 | MidiChannel::Channel8, 87 | MidiChannel::Channel9, 88 | MidiChannel::Channel10, 89 | MidiChannel::Channel11, 90 | MidiChannel::Channel12, 91 | MidiChannel::Channel13, 92 | MidiChannel::Channel14, 93 | MidiChannel::Channel15, 94 | MidiChannel::Channel16, 95 | ]; 96 | } 97 | 98 | impl std::fmt::Display for MidiChannel { 99 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 100 | write!( 101 | f, 102 | "{}", 103 | match self { 104 | MidiChannel::Omni => "Omni", 105 | MidiChannel::Channel1 => "Channel 1", 106 | MidiChannel::Channel2 => "Channel 2", 107 | MidiChannel::Channel3 => "Channel 3", 108 | MidiChannel::Channel4 => "Channel 4", 109 | MidiChannel::Channel5 => "Channel 5", 110 | MidiChannel::Channel6 => "Channel 6", 111 | MidiChannel::Channel7 => "Channel 7", 112 | MidiChannel::Channel8 => "Channel 8", 113 | MidiChannel::Channel9 => "Channel 9", 114 | MidiChannel::Channel10 => "Channel 10", 115 | MidiChannel::Channel11 => "Channel 11", 116 | MidiChannel::Channel12 => "Channel 12", 117 | MidiChannel::Channel13 => "Channel 13", 118 | MidiChannel::Channel14 => "Channel 14", 119 | MidiChannel::Channel15 => "Channel 15", 120 | MidiChannel::Channel16 => "Channel 16", 121 | } 122 | ) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/ui/elements/mod.rs: -------------------------------------------------------------------------------- 1 | //! Individual control elements used in various places 2 | 3 | pub mod arp_grid_list; 4 | pub mod arp_mode_list; 5 | pub mod checkbox; 6 | pub mod env_trigger_list; 7 | pub mod fx_mode_list; 8 | pub mod lfo_phase_list; 9 | pub mod lfo_shape_list; 10 | pub mod midi_channel_list; 11 | pub mod mod_target_list; 12 | pub mod part_list; 13 | pub mod shaper_mode_list; 14 | pub mod slider; 15 | pub mod slider_widget; 16 | pub mod wavetable_list; 17 | -------------------------------------------------------------------------------- /src/ui/elements/mod_target_list.rs: -------------------------------------------------------------------------------- 1 | //! Dropdown menu for the modulation targets 2 | 3 | use iced::widget::{Column, Container, PickList, Row, Text}; 4 | 5 | use crate::messages::Message; 6 | use crate::params::SoundParameter; 7 | use crate::style; 8 | 9 | pub fn mod_target_list(label: &str, sound_param: SoundParameter, value: i32) -> Container { 10 | let value = match value { 11 | 0 => Some(ModTarget::Osc1Wave), 12 | 1 => Some(ModTarget::Osc2Wave), 13 | 2 => Some(ModTarget::Osc1Pitch), 14 | 3 => Some(ModTarget::Osc2Pitch), 15 | 4 => Some(ModTarget::Osc1FMAmount), 16 | 5 => Some(ModTarget::Osc2FMAmount), 17 | 6 => Some(ModTarget::Osc1FMRate), 18 | 7 => Some(ModTarget::Osc2FMRate), 19 | 8 => Some(ModTarget::Osc1Sync), 20 | 9 => Some(ModTarget::Osc2Sync), 21 | 10 => Some(ModTarget::Osc1Level), 22 | 11 => Some(ModTarget::Osc2Level), 23 | 12 => Some(ModTarget::ExtraNoise), 24 | 13 => Some(ModTarget::ExtraRingMod), 25 | 14 => Some(ModTarget::FilterCutoff), 26 | 15 => Some(ModTarget::ShaperCutoff), 27 | 16 => Some(ModTarget::FilterResonance), 28 | 17 => Some(ModTarget::ShaperResonance), 29 | 18 => Some(ModTarget::LFO1Speed), 30 | 19 => Some(ModTarget::LFO2Speed), 31 | 20 => Some(ModTarget::AmpLevel), 32 | 21 => Some(ModTarget::AmpPan), 33 | _ => None, 34 | }; 35 | let pick_list = PickList::new(&ModTarget::ALL[..], value, move |v| { 36 | Message::SoundParameterChange(sound_param, v as i32) 37 | }) 38 | .style(style::PickList) 39 | .text_size(style::LIST_ITEM_TEXT_SIZE); 40 | 41 | Container::new( 42 | Row::new() 43 | .push( 44 | Column::new() 45 | .push( 46 | Text::new(label) 47 | .size(style::PARAM_LABEL_TEXT_SIZE) 48 | .width(style::PARAM_LABEL_WIDTH), 49 | ) 50 | .padding([4, 0, 0, 0]), 51 | ) 52 | .push(pick_list), 53 | ) 54 | } 55 | 56 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 57 | pub enum ModTarget { 58 | Osc1Wave, 59 | Osc2Wave, 60 | Osc1Pitch, 61 | Osc2Pitch, 62 | Osc1FMAmount, 63 | Osc2FMAmount, 64 | Osc1FMRate, 65 | Osc2FMRate, 66 | Osc1Sync, 67 | Osc2Sync, 68 | Osc1Level, 69 | Osc2Level, 70 | ExtraNoise, 71 | ExtraRingMod, 72 | FilterCutoff, 73 | ShaperCutoff, 74 | FilterResonance, 75 | ShaperResonance, 76 | LFO1Speed, 77 | LFO2Speed, 78 | AmpLevel, 79 | AmpPan, 80 | } 81 | 82 | impl ModTarget { 83 | const ALL: [ModTarget; 22] = [ 84 | ModTarget::Osc1Wave, 85 | ModTarget::Osc2Wave, 86 | ModTarget::Osc1Pitch, 87 | ModTarget::Osc2Pitch, 88 | ModTarget::Osc1FMAmount, 89 | ModTarget::Osc2FMAmount, 90 | ModTarget::Osc1FMRate, 91 | ModTarget::Osc2FMRate, 92 | ModTarget::Osc1Sync, 93 | ModTarget::Osc2Sync, 94 | ModTarget::Osc1Level, 95 | ModTarget::Osc2Level, 96 | ModTarget::ExtraNoise, 97 | ModTarget::ExtraRingMod, 98 | ModTarget::FilterCutoff, 99 | ModTarget::ShaperCutoff, 100 | ModTarget::FilterResonance, 101 | ModTarget::ShaperResonance, 102 | ModTarget::LFO1Speed, 103 | ModTarget::LFO2Speed, 104 | ModTarget::AmpLevel, 105 | ModTarget::AmpPan, 106 | ]; 107 | } 108 | 109 | impl std::fmt::Display for ModTarget { 110 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 111 | write!( 112 | f, 113 | "{}", 114 | match self { 115 | ModTarget::Osc1Wave => "Osc 1 Wave", 116 | ModTarget::Osc2Wave => "Osc 2 Wave", 117 | ModTarget::Osc1Pitch => "Osc 1 Pitch", 118 | ModTarget::Osc2Pitch => "Osc 2 Pitch", 119 | ModTarget::Osc1FMAmount => "Osc 1 FM Amount", 120 | ModTarget::Osc2FMAmount => "Osc 2 FM Amount", 121 | ModTarget::Osc1FMRate => "Osc 1 FM Rate", 122 | ModTarget::Osc2FMRate => "Osc 2 FM Rate", 123 | ModTarget::Osc1Sync => "Osc 1 Sync", 124 | ModTarget::Osc2Sync => "Osc 2 Sync", 125 | ModTarget::Osc1Level => "Osc 1 Level", 126 | ModTarget::Osc2Level => "Osc 2 Level", 127 | ModTarget::ExtraNoise => "Extra Noise", 128 | ModTarget::ExtraRingMod => "Extra O1xO2", 129 | ModTarget::FilterCutoff => "Filter Cutoff", 130 | ModTarget::ShaperCutoff => "Shaper Cutoff", 131 | ModTarget::FilterResonance => "Filter Resonance", 132 | ModTarget::ShaperResonance => "Shaper Resonance", 133 | ModTarget::LFO1Speed => "LFO 1 Speed", 134 | ModTarget::LFO2Speed => "LFO 2 Speed", 135 | ModTarget::AmpLevel => "Amp Level", 136 | ModTarget::AmpPan => "Amp Pan", 137 | } 138 | ) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/ui/elements/part_list.rs: -------------------------------------------------------------------------------- 1 | //! Dropdown menu for part selection 2 | 3 | use iced::widget::{Container, PickList}; 4 | 5 | use crate::messages::Message; 6 | use crate::style; 7 | 8 | pub fn part_list<'a>(value: u8) -> Container<'a, Message> { 9 | let value = match value { 10 | 0 => Some(PartList::Part1), 11 | 1 => Some(PartList::Part2), 12 | 2 => Some(PartList::Part3), 13 | 3 => Some(PartList::Part4), 14 | _ => None, 15 | }; 16 | let pick_list = PickList::new(&PartList::ALL[..], value, move |v| { 17 | Message::PartChange(v as u8) 18 | }) 19 | .style(style::PickList) 20 | .text_size(style::LIST_ITEM_TEXT_SIZE); 21 | 22 | Container::new(pick_list) 23 | } 24 | 25 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 26 | pub enum PartList { 27 | Part1, 28 | Part2, 29 | Part3, 30 | Part4, 31 | } 32 | 33 | impl PartList { 34 | const ALL: [PartList; 4] = [ 35 | PartList::Part1, 36 | PartList::Part2, 37 | PartList::Part3, 38 | PartList::Part4, 39 | ]; 40 | } 41 | 42 | impl std::fmt::Display for PartList { 43 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 44 | write!( 45 | f, 46 | "{}", 47 | match self { 48 | PartList::Part1 => "Part 1", 49 | PartList::Part2 => "Part 2", 50 | PartList::Part3 => "Part 3", 51 | PartList::Part4 => "Part 4", 52 | } 53 | ) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/ui/elements/shaper_mode_list.rs: -------------------------------------------------------------------------------- 1 | //! Dropdown menu for the shaper modes 2 | 3 | use iced::widget::{Column, Container, PickList, Row, Text}; 4 | 5 | use crate::messages::Message; 6 | use crate::params::SoundParameter; 7 | use crate::style; 8 | 9 | pub fn shaper_mode_list( 10 | label: &str, 11 | sound_param: SoundParameter, 12 | value: i32, 13 | ) -> Container { 14 | let value = match value { 15 | 0 => Some(ShaperMode::Lowpass), 16 | 1 => Some(ShaperMode::Bandpass), 17 | 2 => Some(ShaperMode::Highpass), 18 | _ => None, 19 | }; 20 | let pick_list = PickList::new(&ShaperMode::ALL[..], value, move |v| { 21 | Message::SoundParameterChange(sound_param, v as i32) 22 | }) 23 | .style(style::PickList) 24 | .text_size(style::LIST_ITEM_TEXT_SIZE); 25 | 26 | Container::new( 27 | Row::new() 28 | .push( 29 | Column::new() 30 | .push( 31 | Text::new(label) 32 | .size(style::PARAM_LABEL_TEXT_SIZE) 33 | .width(style::PARAM_LABEL_WIDTH), 34 | ) 35 | .padding([4, 0, 0, 0]), 36 | ) 37 | .push(pick_list), 38 | ) 39 | } 40 | 41 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 42 | pub enum ShaperMode { 43 | Lowpass, 44 | Bandpass, 45 | Highpass, 46 | } 47 | 48 | impl ShaperMode { 49 | const ALL: [ShaperMode; 3] = [ 50 | ShaperMode::Lowpass, 51 | ShaperMode::Bandpass, 52 | ShaperMode::Highpass, 53 | ]; 54 | } 55 | 56 | impl std::fmt::Display for ShaperMode { 57 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 58 | write!( 59 | f, 60 | "{}", 61 | match self { 62 | ShaperMode::Lowpass => "Low-pass", 63 | ShaperMode::Bandpass => "Band-pass", 64 | ShaperMode::Highpass => "High-pass", 65 | } 66 | ) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/ui/elements/slider.rs: -------------------------------------------------------------------------------- 1 | //! Slider control wrapped in a container with label and value display 2 | 3 | use iced::alignment; 4 | use iced::widget::{Column, Container, Row, Text}; 5 | 6 | use super::slider_widget::Slider; 7 | 8 | use crate::messages::Message; 9 | use crate::params::{MultiParameter, SoundParameter}; 10 | use crate::style; 11 | 12 | /// Returns a slider for a sound (preset) parameter 13 | pub fn slider_with_labels( 14 | label: &str, 15 | sound_param: SoundParameter, 16 | value: i32, 17 | ) -> Container { 18 | let range = sound_param.get_range(); 19 | let slider = Slider::new(range, value, sound_param.get_default(), move |v| { 20 | Message::SoundParameterChange(sound_param, v) 21 | }) 22 | .style(style::Slider); 23 | 24 | Container::new( 25 | Row::new() 26 | .push( 27 | Column::new() 28 | .push( 29 | Text::new(label) 30 | .size(style::PARAM_LABEL_TEXT_SIZE) 31 | .width(style::PARAM_LABEL_WIDTH), 32 | ) 33 | .padding([3, 0, 0, 0]), 34 | ) 35 | .push(slider) 36 | .push( 37 | Column::new() 38 | .push( 39 | Text::new(format!("{}", value)) 40 | .size(style::PARAM_LABEL_TEXT_SIZE) 41 | .horizontal_alignment(alignment::Horizontal::Right) 42 | .width(style::PARAM_VALUE_WIDTH), 43 | ) 44 | .padding([3, 0, 0, 5]), 45 | ), 46 | ) 47 | } 48 | 49 | /// Returns a slider for a multi parameter 50 | pub fn multi_slider_with_labels( 51 | label: &str, 52 | multi_param: MultiParameter, 53 | value: i32, 54 | ) -> Container { 55 | let range = multi_param.get_range(); 56 | let slider = Slider::new(range, value, multi_param.get_default(), move |v| { 57 | Message::MultiParameterChange(multi_param, v) 58 | }) 59 | .style(style::Slider); 60 | 61 | Container::new( 62 | Row::new() 63 | .push( 64 | Column::new() 65 | .push( 66 | Text::new(label) 67 | .size(style::PARAM_LABEL_TEXT_SIZE) 68 | .width(style::PARAM_LABEL_WIDTH), 69 | ) 70 | .padding([3, 0, 0, 0]), 71 | ) 72 | .push(slider) 73 | .push( 74 | Column::new() 75 | .push( 76 | Text::new(format!("{}", value)) 77 | .size(style::PARAM_LABEL_TEXT_SIZE) 78 | .horizontal_alignment(alignment::Horizontal::Right) 79 | .width(style::PARAM_VALUE_WIDTH), 80 | ) 81 | .padding([3, 0, 0, 5]), 82 | ), 83 | ) 84 | } 85 | -------------------------------------------------------------------------------- /src/ui/elements/slider_widget.rs: -------------------------------------------------------------------------------- 1 | //! Display an interactive selector of a single value from a range of values. 2 | //! 3 | //! This is a modified version of the original slider widget from `iced_native` 4 | //! with the following changes: 5 | //! - Use of pointer shape mouse cursor when hovering the slider. 6 | //! - Mouse wheel support. 7 | //! - Control-click/right-click resets slider to a default value. 8 | //! - Shift-drag enables fine control. 9 | //! - Clippy related fixes. 10 | //! 11 | //! A [`Slider`] has some local [`State`]. 12 | 13 | use iced::mouse::ScrollDelta; 14 | use iced_native::event::{self, Event}; 15 | use iced_native::keyboard; 16 | use iced_native::layout; 17 | use iced_native::mouse; 18 | use iced_native::renderer; 19 | use iced_native::touch; 20 | use iced_native::widget::tree::{self, Tree}; 21 | use iced_native::{ 22 | Background, Clipboard, Color, Element, Layout, Length, Point, Rectangle, Shell, Size, Widget, 23 | }; 24 | pub use iced_style::slider::{HandleShape, StyleSheet}; 25 | 26 | use std::ops::{Add, RangeInclusive}; 27 | 28 | /// An horizontal bar and a handle that selects a single value from a range of 29 | /// values. 30 | /// 31 | /// A [`Slider`] will try to fill the horizontal space of its container. 32 | /// 33 | /// The [`Slider`] range of numeric values is generic and its step size defaults 34 | /// to 1 unit. 35 | /// 36 | /// # Example 37 | /// ``` 38 | /// # use iced_native::widget::slider; 39 | /// # use iced_native::renderer::Null; 40 | /// # 41 | /// # type Slider<'a, T, Message> = slider::Slider<'a, T, Message, Null>; 42 | /// # 43 | /// #[derive(Clone)] 44 | /// pub enum Message { 45 | /// SliderChanged(f32), 46 | /// } 47 | /// 48 | /// let value = 50.0; 49 | /// 50 | /// Slider::new(0.0..=100.0, value, Message::SliderChanged); 51 | /// ``` 52 | /// 53 | /// ![Slider drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/slider.png?raw=true) 54 | #[allow(missing_debug_implementations)] 55 | pub struct Slider<'a, T, Message, Renderer> 56 | where 57 | Renderer: iced_native::Renderer, 58 | Renderer::Theme: StyleSheet, 59 | { 60 | range: RangeInclusive, 61 | step: T, 62 | value: T, 63 | default: T, 64 | on_change: Box Message + 'a>, 65 | on_release: Option, 66 | width: Length, 67 | height: u16, 68 | style: ::Style, 69 | } 70 | 71 | impl<'a, T, Message, Renderer> Slider<'a, T, Message, Renderer> 72 | where 73 | T: Copy + From + std::cmp::PartialOrd, 74 | Message: Clone, 75 | Renderer: iced_native::Renderer, 76 | Renderer::Theme: StyleSheet, 77 | { 78 | /// The default height of a [`Slider`]. 79 | pub const DEFAULT_HEIGHT: u16 = 22; 80 | 81 | /// Creates a new [`Slider`]. 82 | /// 83 | /// It expects: 84 | /// * an inclusive range of possible values 85 | /// * the current value of the [`Slider`] 86 | /// * a function that will be called when the [`Slider`] is dragged. 87 | /// 88 | /// It receives the new value of the [`Slider`] and must produce a `Message`. 89 | pub fn new(range: RangeInclusive, value: T, default: T, on_change: F) -> Self 90 | where 91 | F: 'static + Fn(T) -> Message, 92 | { 93 | let value = if value >= *range.start() { 94 | value 95 | } else { 96 | *range.start() 97 | }; 98 | 99 | let value = if value <= *range.end() { 100 | value 101 | } else { 102 | *range.end() 103 | }; 104 | 105 | Slider { 106 | value, 107 | range, 108 | step: T::from(1), 109 | default, 110 | on_change: Box::new(on_change), 111 | on_release: None, 112 | width: Length::Fill, 113 | height: Self::DEFAULT_HEIGHT, 114 | style: Default::default(), 115 | } 116 | } 117 | 118 | /// Sets the release message of the [`Slider`]. 119 | /// This is called when the mouse is released from the slider. 120 | /// 121 | /// Typically, the user's interaction with the slider is finished when this message is produced. 122 | /// This is useful if you need to spawn a long-running task from the slider's result, where 123 | /// the default on_change message could create too many events. 124 | pub fn on_release(mut self, on_release: Message) -> Self { 125 | self.on_release = Some(on_release); 126 | self 127 | } 128 | 129 | /// Sets the width of the [`Slider`]. 130 | pub fn width(mut self, width: Length) -> Self { 131 | self.width = width; 132 | self 133 | } 134 | 135 | /// Sets the height of the [`Slider`]. 136 | pub fn height(mut self, height: u16) -> Self { 137 | self.height = height; 138 | self 139 | } 140 | 141 | /// Sets the style of the [`Slider`]. 142 | pub fn style(mut self, style: impl Into<::Style>) -> Self { 143 | self.style = style.into(); 144 | self 145 | } 146 | 147 | /// Sets the step size of the [`Slider`]. 148 | pub fn step(mut self, step: T) -> Self { 149 | self.step = step; 150 | self 151 | } 152 | } 153 | 154 | /// Processes an [`Event`] and updates the [`State`] of a [`Slider`] 155 | /// accordingly. 156 | #[allow(clippy::too_many_arguments)] 157 | pub fn update( 158 | event: Event, 159 | layout: Layout<'_>, 160 | cursor_position: Point, 161 | shell: &mut Shell<'_, Message>, 162 | state: &mut State, 163 | value: &mut T, 164 | range: &RangeInclusive, 165 | step: T, 166 | default: T, 167 | on_change: &dyn Fn(T) -> Message, 168 | on_release: &Option, 169 | ) -> event::Status 170 | where 171 | T: Default + Copy + Into + Add + Ord + num_traits::FromPrimitive, 172 | Message: Clone, 173 | { 174 | let is_dragging = state.is_dragging; 175 | 176 | match event { 177 | Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) 178 | | Event::Touch(touch::Event::FingerPressed { .. }) => { 179 | let bounds = layout.bounds(); 180 | if bounds.contains(cursor_position) { 181 | if state.control_pressed { 182 | let new_value = default; 183 | shell.publish((on_change)(new_value)); 184 | *value = new_value; 185 | } else { 186 | let step = step.into(); 187 | let start = (*range.start()).into(); 188 | let end = (*range.end()).into(); 189 | let percent = f64::from(cursor_position.x - bounds.x) / f64::from(bounds.width); 190 | let steps = (percent * (end - start) / step).round(); 191 | let v = steps * step + start; 192 | let new_value = T::from_f64(v) 193 | .unwrap_or_default() 194 | .clamp(*range.start(), *range.end()); 195 | shell.publish((on_change)(new_value)); 196 | *value = new_value; 197 | state.is_dragging = true; 198 | state.click_pos_x = cursor_position.x; 199 | state.click_value = v; 200 | } 201 | 202 | return event::Status::Captured; 203 | } 204 | } 205 | Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) 206 | | Event::Touch(touch::Event::FingerLifted { .. }) 207 | | Event::Touch(touch::Event::FingerLost { .. }) => { 208 | if is_dragging { 209 | if let Some(on_release) = on_release.clone() { 210 | shell.publish(on_release); 211 | } 212 | state.is_dragging = false; 213 | 214 | return event::Status::Captured; 215 | } 216 | } 217 | Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => { 218 | if layout.bounds().contains(cursor_position) { 219 | let new_value = default; 220 | shell.publish((on_change)(new_value)); 221 | *value = new_value; 222 | } 223 | } 224 | Event::Mouse(mouse::Event::CursorMoved { .. }) 225 | | Event::Touch(touch::Event::FingerMoved { .. }) => { 226 | if is_dragging { 227 | let bounds = layout.bounds(); 228 | let step = step.into(); 229 | let start = (*range.start()).into(); 230 | let end = (*range.end()).into(); 231 | let mut percent = 232 | f64::from(cursor_position.x - state.click_pos_x) / f64::from(bounds.width); 233 | if state.shift_pressed { 234 | percent /= 4.0; 235 | } 236 | let steps = (percent * (end - start) / step).round(); 237 | let v = state.click_value + steps; 238 | let new_value = T::from_f64(v) 239 | .unwrap_or_default() 240 | .clamp(*range.start(), *range.end()); 241 | shell.publish((on_change)(new_value)); 242 | *value = new_value; 243 | 244 | return event::Status::Captured; 245 | } 246 | } 247 | Event::Mouse(mouse::Event::WheelScrolled { delta }) => { 248 | if layout.bounds().contains(cursor_position) { 249 | let delta = match delta { 250 | ScrollDelta::Lines { x: _, y } => { 251 | if y.is_sign_positive() { 252 | y.ceil() 253 | } else { 254 | y.floor() 255 | } 256 | } 257 | _ => 0.0, 258 | }; 259 | 260 | let new_value = { *value + T::from_f32(delta).unwrap_or_default() } 261 | .clamp(*range.start(), *range.end()); 262 | shell.publish((on_change)(new_value)); 263 | 264 | *value = new_value; 265 | 266 | return event::Status::Captured; 267 | } 268 | } 269 | Event::Keyboard(keyboard::Event::KeyPressed { 270 | key_code: keyboard::KeyCode::LShift | keyboard::KeyCode::RShift, 271 | .. 272 | }) => { 273 | state.shift_pressed = true; 274 | if state.is_dragging { 275 | state.click_pos_x = cursor_position.x; 276 | state.click_value = (*value).into(); 277 | } 278 | 279 | return event::Status::Captured; 280 | } 281 | Event::Keyboard(keyboard::Event::KeyReleased { 282 | key_code: keyboard::KeyCode::LShift | keyboard::KeyCode::RShift, 283 | .. 284 | }) => { 285 | state.shift_pressed = false; 286 | if state.is_dragging { 287 | let bounds = layout.bounds(); 288 | let step = step.into(); 289 | let start = (*range.start()).into(); 290 | let end = (*range.end()).into(); 291 | let percent = f64::from(cursor_position.x - bounds.x) / f64::from(bounds.width); 292 | let steps = (percent * (end - start) / step).round(); 293 | let v = steps * step + start; 294 | let new_value = T::from_f64(v) 295 | .unwrap_or_default() 296 | .clamp(*range.start(), *range.end()); 297 | shell.publish((on_change)(new_value)); 298 | *value = new_value; 299 | state.click_pos_x = cursor_position.x; 300 | state.click_value = (*value).into(); 301 | } 302 | 303 | return event::Status::Captured; 304 | } 305 | Event::Keyboard(keyboard::Event::KeyPressed { 306 | key_code: keyboard::KeyCode::LControl | keyboard::KeyCode::RControl, 307 | .. 308 | }) => { 309 | state.control_pressed = true; 310 | 311 | return event::Status::Captured; 312 | } 313 | Event::Keyboard(keyboard::Event::KeyReleased { 314 | key_code: keyboard::KeyCode::LControl | keyboard::KeyCode::RControl, 315 | .. 316 | }) => { 317 | state.control_pressed = false; 318 | 319 | return event::Status::Captured; 320 | } 321 | _ => {} 322 | } 323 | 324 | event::Status::Ignored 325 | } 326 | 327 | /// Draws a [`Slider`]. 328 | #[allow(clippy::too_many_arguments)] 329 | pub fn draw( 330 | renderer: &mut R, 331 | layout: Layout<'_>, 332 | cursor_position: Point, 333 | state: &State, 334 | value: T, 335 | range: &RangeInclusive, 336 | style_sheet: &dyn StyleSheet