├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE.txt ├── README.md ├── Yazz_ControllerMapping.ysn ├── Yazz_FactoryBank.ysn ├── Yazz_Markers.ysn ├── bitbucket-pipelines.yml ├── doc ├── Screenshot1.png └── manual.md ├── plot.txt ├── src ├── ctrl_map.rs ├── main.rs ├── midi_handler.rs ├── modulation.rs ├── parameter.rs ├── ringbuffer.rs ├── sound.rs ├── storage.rs ├── synth │ ├── delay.rs │ ├── engine.rs │ ├── envelope.rs │ ├── filter │ │ ├── filter.rs │ │ ├── korg35.rs │ │ ├── mod.rs │ │ ├── moog_improved.rs │ │ ├── ober_moog.rs │ │ ├── oberheim.rs │ │ ├── onepole.rs │ │ ├── reson_z.rs │ │ ├── rlpf.rs │ │ ├── sem.rs │ │ └── va_onepole.rs │ ├── lfo.rs │ ├── mod.rs │ ├── oscillator.rs │ ├── sample_generator.rs │ ├── synth.rs │ ├── voice.rs │ └── wt_oscillator.rs ├── tui │ ├── bar.rs │ ├── button.rs │ ├── canvas.rs │ ├── color_scheme.rs │ ├── container.rs │ ├── controller.rs │ ├── dial.rs │ ├── display.rs │ ├── item_selection.rs │ ├── label.rs │ ├── marker_manager.rs │ ├── midi_learn.rs │ ├── mod.rs │ ├── mouse.rs │ ├── observer.rs │ ├── printer.rs │ ├── select.rs │ ├── slider.rs │ ├── statemachine.rs │ ├── stdio_printer.rs │ ├── surface.rs │ ├── termion_wrapper.rs │ ├── tui.rs │ ├── value.rs │ ├── value_display.rs │ ├── waveforms.rs │ └── widget.rs └── value_range.rs └── todo.txt /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /log_files/ 3 | /data/ 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "yazz" 3 | version = "0.1.0" 4 | authors = ["ICS"] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | clap = "2" 9 | cpal = "0.10.0" 10 | crossbeam-channel = "0.3" 11 | failure = "0.1.5" 12 | flexi_logger = "0.14" 13 | lazy_static = "1.3" 14 | log = "0.4" 15 | midir = "0.5.0" 16 | num = "*" 17 | num-traits = "0.2.6" 18 | rand = "0.7" 19 | regex = "1" 20 | serde = { version = "1.0", features = ["derive"] } 21 | serde_json = "1.0" 22 | term_cursor = "0.2.1" 23 | termion = "1.5" 24 | wavetable = "0.1.2" 25 | 26 | [[bin]] 27 | name = "yazz" 28 | path = "src/main.rs" 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2019 ICS 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yazz - Yet Another Software Synth 2 | 3 | Yazz is a subtractive synth written in Rust. It comes with a simple terminal 4 | UI that allows all parameters to be edited by key sequences and/ or MIDI 5 | controllers. 6 | 7 | ![rust-screenshot.png](doc/Screenshot1.png) 8 | 9 | The main focus of this project is on mouse-free editing: Yazz is a synth for 10 | terminal lovers. 11 | 12 | This is still work in progress. The sound engine works, but some features are 13 | missing, and the parameter ranges are not perfectly balanced yet. 14 | 15 | ## Features 16 | 17 | - 3 wavetable oscillators per voice, 32 voice polyphony 18 | - Up to 7 instances per oscillator with frequency spreading 19 | - Oscillator sync 20 | - 2 independent filters with individual oscillator routing 21 | (parallel, serial, bypassed) 22 | - Wavetable scanning 23 | - User wavetable import 24 | - Voice stereo spreading 25 | - Up to 16 modulation assignments to almost all sound parameters 26 | - 2 LFOs per voice plus 2 global LFOs 27 | - 3 ADSR envelopes per voice, with adjustable slope 28 | - Delay (mono or ping pong, BPM-synced) 29 | - 36 sets of MIDI controller assignments 30 | 31 | For a detailed description, have a look at the [manual in the doc folder](doc/manual.md). 32 | 33 | ## Compiling, running and troubleshooting 34 | 35 | Yazz should run on both MacOS and Linux. Assuming you have the Rust toolchain 36 | installed, a simple `cargo build --release` should download all dependencies 37 | and compile the synth. 38 | 39 | Make sure to build and run the release version, otherwise the audio engine 40 | might have performance problems (it's not optimized yet). 41 | 42 | For Linux, the dev-package for ALSA needs to be installed (usually 43 | libasound2-dev or alsa-lib-devel, see https://github.com/RustAudio/cpal for 44 | more infos). 45 | 46 | Yazz connects to MIDI port 0 per default. If you get a MIDI port error on 47 | startup, or if Yazz doesn't react to MIDI messages, try connecting to a 48 | different port with `yazz -m 1`. 49 | 50 | Check the documentation for additional command line parameters. 51 | 52 | ## Near future enhancements 53 | 54 | - Chorus 55 | - Multitap delay 56 | - Additional key tuning tables for alternate tunings 57 | - Additional oscillators (PM, FM) 58 | - Editing via MIDI note commands 59 | 60 | ## Far away future enhancements 61 | 62 | - Optional GUI 63 | - Implement VST plugin interface 64 | -------------------------------------------------------------------------------- /Yazz_Markers.ysn: -------------------------------------------------------------------------------- 1 | { 2 | "marker": { 3 | "2": { 4 | "function": "Oscillator", 5 | "function_id": 2, 6 | "parameter": "Level" 7 | }, 8 | "m": { 9 | "function": "Modulation", 10 | "function_id": 1, 11 | "parameter": "Target" 12 | }, 13 | "f": { 14 | "function": "Filter", 15 | "function_id": 1, 16 | "parameter": "Cutoff" 17 | }, 18 | "e": { 19 | "function": "Envelope", 20 | "function_id": 1, 21 | "parameter": "Attack" 22 | }, 23 | "1": { 24 | "function": "Oscillator", 25 | "function_id": 1, 26 | "parameter": "Level" 27 | }, 28 | "3": { 29 | "function": "Oscillator", 30 | "function_id": 3, 31 | "parameter": "Level" 32 | }, 33 | "r": { 34 | "function": "Filter", 35 | "function_id": 1, 36 | "parameter": "Resonance" 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /bitbucket-pipelines.yml: -------------------------------------------------------------------------------- 1 | image: rust:1.34 2 | 3 | pipelines: 4 | custom: 5 | default: 6 | - step: 7 | script: 8 | - apt-get update 9 | - apt-get install -y libasound2-dev 10 | - cargo build -v --release 11 | - cargo test -v 12 | -------------------------------------------------------------------------------- /doc/Screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icsga/Yazz/593103400a53008e61a66237ca8c05656b2ceffc/doc/Screenshot1.png -------------------------------------------------------------------------------- /doc/manual.md: -------------------------------------------------------------------------------- 1 | # Yazz - Yet Another Subtractive Synth 2 | 3 | ## Introduction 4 | 5 | Thanks for trying out Yazz. This project is still in development, so not 6 | everything works as expected, or at all. I'm happy about any feedback. 7 | 8 | ## Architecture 9 | 10 | Yazz has a fixed signal flow with the following components: 11 | 12 | * 32 voices 13 | * 3 wavetable-based oscillators per voice 14 | * 2 filters (parallel or serial routing) with LP-/ HP- and BP-Modes per voice 15 | * 3 ADSR envelopes per voice 16 | * 2 LFOs per voice 17 | * 2 global LFOs 18 | * Delay 19 | 20 | ## Help and exit 21 | 22 | At any time, press F1 to show a help page. Press any key to exit the help page. 23 | 24 | Press F12 to exit Yazz. 25 | 26 | ## Loading and saving sounds 27 | 28 | This functionality is still under construction. 29 | 30 | Currently the sound filename is hardwired to "Yazz_FactoryBank.ysn". It is 31 | loaded automatically on startup. To load the file manually, press F3. 32 | 33 | You can change the current program by sending MIDI program change commands. 34 | In Play mode, you can additionally use the '+', '-' keys for program changing. 35 | 36 | **WARNING: All changes made to a sound will be lost when changing the current 37 | program without saving.** 38 | 39 | To save a sound, press F2. Currently, only saving the complete sound bank is 40 | supported. There is also no safety dialog yet to prevent accidentally overwriting 41 | sounds. Making manual backups might be a good idea if you made a cool sound and 42 | want to keep it. 43 | 44 | ## Copying and renaming sounds 45 | 46 | Press `` to copy the current sound to the internal clipboard. 47 | 48 | Press `` to paste the contents of the internal clipboard to the 49 | current sound patch. 50 | 51 | Press `` to rename the current sound. 52 | 53 | ## Operating modes 54 | 55 | Yazz has two distinct operating modes: Edit mode and Play mode. The mode is 56 | switched with the TAB key. The main difference is that Edit mode captures one 57 | dedicated MIDI controller (modulation wheel per default) as data input to 58 | change parameter values, while play mode allows easy switching between MIDI 59 | controller sets. 60 | 61 | ## Edit Mode: Editing parameters 62 | 63 | Yazz is controlled entirely with the (computer-)keyboard and/ or MIDI 64 | controllers. The top line of the UI is the **command line** that is used 65 | for changing parameters. 66 | 67 | Every parameter consists of four parts: 68 | 69 | * A **function**: The function group, e.g. Oscillator, Envelope etc, 70 | * A **function ID**, in case there is more than one instance of the current 71 | function, e.g. Oscillator 3, Envelope 1, 72 | * A **parameter**: A parameter of the selected function (e.g. Oscillator Level, 73 | Envelope Attack), 74 | * A **value**: The actual value of the selected parameter. This can itself be 75 | another parameter, e.g. for modulation source and target. 76 | 77 | To make entering values easier, a MIDI controller can be assigned as value 78 | input control. Currently this is hardwired to the modulation wheel. 79 | 80 | ### Function selection 81 | 82 | When starting to edit a parameter, the function must be selected. The UI shows 83 | a drop-down list of available functions. Every function has a shortcut 84 | associated with it, e.g. 'o' for oscillator, 'l' for LFO. The function can 85 | be selected by: 86 | 87 | * Entering the shortcut key, 88 | * Using the CursorUp/ CursorDown keys and pressing Enter or CursorRight. 89 | 90 | ### Function ID selection 91 | 92 | The function ID can be selected by entering the number, using the cursor keys 93 | or using +/ -. If only a single instance of the function is available (e.g. 94 | Delay), the input will immediately progress to the parameter selection. 95 | 96 | ### Parameter selection 97 | 98 | The parameter selection uses the same mechanism as the function selection 99 | 100 | ### Value selection 101 | 102 | The value can be changed by: 103 | 104 | * Typing in the target value and pressing Enter, e.g. "3.14" for a float value, 105 | * Using '+'/ '-' to increment/ decrement the value by a fixed amount, 106 | * Using the input controller (currently the modulation wheel) to set the value. 107 | * Using a MIDI controller assigned to this parameter to set the value. 108 | 109 | After adjusting the value, pressing Enter will return to the Parameter 110 | selection, pressing Escape will return to the function selection. 111 | 112 | ### Keyboard shortcuts 113 | 114 | While setting the value in the command line, there are a number of additional 115 | keyboard shortcuts for faster navigation: 116 | 117 | * **PageUp/ PageDown** will change the function ID of the current parameter. 118 | That way it is easy to adjust the same parameter for all 3 oscillators, or to 119 | search for the modulation slot that is assigned to a particular target. 120 | * **[/ ]** will step through the parameter list of the selected function. 121 | * **** will move backwards/ forwards through the history of selected 122 | parameters. 123 | * **"\** adds a marker at the selected parameter. MarkerId can be any 124 | valid ASCII character. Markers are saved between sessions. 125 | * **'\** goes to the selected marker if it has been defined. 126 | * **/** creates a new modulator with the current parameter as target, 127 | activates it and sets the command line to the modulation source selection. 128 | 129 | ## Assigning MIDI controllers 130 | 131 | Yazz supports assigning MIDI controllers to most sound parameters. To assign a 132 | controller: 133 | 134 | * Select the target parameter in the command line (e.g. "o1l" to select 135 | oscillator 1 level. 136 | * Enter MIDI learn mode by pressing ``. The command line will show the text 137 | **MIDI learn: Send controller data**. 138 | * Send values with the MIDI controller. Yazz needs at least two distinct values 139 | to be able to distinguish between absolute and relative controller types. 140 | 141 | After having detected the controller, the command line switches back to value 142 | input mode. 143 | 144 | To cancel MIDI learn mode without assigning a controller, press Escape. 145 | 146 | To clear a previous controller assignment of a parameter, select MIDI learn 147 | mode for that parameter and press Backspace. 148 | 149 | Controller assignments are global settings, not sound specific. They are saved 150 | automatically after every controller assignment change. 151 | 152 | ## Modulation ## 153 | 154 | Yazz has a flexible modulation matrix, which allows using most signal outputs 155 | as modulation values for sound parameters. There are two different types of 156 | modulation sources and targets: Global and Local. Global parameters are note- 157 | and voice-independent and only represent a single value at any time. Local 158 | parameters are note- or voice-specific, with individual values per note or 159 | voice. 160 | 161 | Global modulation sources can modulate both global and local modulation 162 | targets. Local modulation sources can modulate only local modulation targets. 163 | 164 | * Global modulation sources: 165 | * Channel aftertouch 166 | * Pitch wheel 167 | * Modulation wheel 168 | * Global LFOs 169 | * Local modulation sources: 170 | * Note on velocity 171 | * Oscillator output 172 | * Envelope output 173 | * LFO output 174 | * Global modulation targets: 175 | * Patch volume 176 | * Delay parameters 177 | * Modulation amount and status 178 | * Local modulation targets: 179 | * All voice parameters 180 | 181 | To assign a modulator, select one the 16 available Modulation function slots. 182 | Both the source and the target parameters can be entered the same way as 183 | selecting a synth parameter. Modulation source requires only Function and 184 | Function ID, while Modulation Target also requires the Parameter to modulate. 185 | 186 | Any modulator can be adjusted in intensity and can be turned on/ off. Both 187 | intensity and on/ off status can themselves be modulated by other modulators. 188 | 189 | ## User wavetables 190 | 191 | It's possible to use external wavetables as sound source. On startup, Yazz looks 192 | for a "data" folder in its runtime directory. If the folder exists, it is 193 | scanned for Wave files. Any files found that are in the right format are added 194 | to the list of available wavetables. 195 | 196 | Currently the only supported format for wavetable files is single-channel files 197 | with 32-bit float values, with a length of 2048 samples. Support for other 198 | formats is in the works. 199 | 200 | Sounds only store a reference to the wavetable, not the actual wavetable data 201 | itself, so if an external table was used for a sound, the corresponding file 202 | needs to remain in the data folder. If a wavetable file is not found, the sound 203 | will use the internal default wavetable instead. 204 | 205 | ## Play Mode: Select controller set 206 | 207 | Yazz groups MIDI controller assignments into 36 controller sets. That means 208 | that even with just a single controller available, it is possible to control 36 209 | different parameters by switching the active set. 210 | 211 | In play mode, pressing any valid controller set identifier ('0' - '9', 212 | 'a' - 'z') will activate the controller set with that ID. The default set 213 | active on startup is '0'. 214 | 215 | A typical setup would be to group the controllers according to the controller 216 | set ID, so that the assignment is easy to remember. If for example the used 217 | MIDI controller has 8 knobs or faders, one could use controller set '1' for 218 | controlling level, tune, spread and wave index of oscillator 1, and attack, 219 | decay, sustain, release of envelope 1. Set '2' could control the same 220 | parameters for oscillator and envelope 2 and so on. Set 'd' can be used for 221 | delay values, while 'p' controlls the patch parameters like patch level. 222 | 223 | ## Sound editing notes 224 | 225 | ### Envelopes 226 | 227 | By default, the level of the mix of all oscillators is modulated by envelope 1. 228 | This can be disabled by setting the patch parameter "EnvDepth" to 0. Then you 229 | can assign the envelopes to the oscillators individually by using them as 230 | modulation source and modulating the oscillator level. The oscillator level 231 | parameter itself should be set to 0 in this case. 232 | 233 | ### Filters 234 | 235 | There are two independent filters. You can choose which filter an oscillator 236 | should be routed to with the "Routing" option in the oscillator parameters. 237 | You can also choose to route the output of filter one through filter two by 238 | setting the patch parameter "filter_routing" to Serial instead of Parallel. 239 | 240 | -------------------------------------------------------------------------------- /plot.txt: -------------------------------------------------------------------------------- 1 | #set xrange [0:88200] 2 | set term gif size 1200, 800 3 | set output "out.gif" 4 | 5 | set yrange [-1.1:1.1] 6 | plot "env_output.txt" using :1 with lines 7 | -------------------------------------------------------------------------------- /src/ctrl_map.rs: -------------------------------------------------------------------------------- 1 | //! Maps MIDI controllers to synth parameters. 2 | 3 | use super::{ParamId, MenuItem}; 4 | use super::SoundData; 5 | use super::SynthParam; 6 | use super::ValueRange; 7 | 8 | use log::{info, trace}; 9 | use serde::{Serialize, Deserialize}; 10 | 11 | use std::collections::HashMap; 12 | use std::fs::File; 13 | use std::io::BufReader; 14 | use std::io::prelude::*; 15 | 16 | #[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)] 17 | pub enum MappingType { 18 | None, 19 | Absolute, 20 | Relative 21 | } 22 | 23 | #[derive(Clone, Copy, Debug)] 24 | pub struct CtrlMapEntry { 25 | id: ParamId, 26 | map_type: MappingType, 27 | val_range: &'static ValueRange, 28 | } 29 | 30 | /// ValueRange contains a reference, so it can't be stored easily. Instead we 31 | /// store only ParamId and MappingType and rely on the TUI to look up the 32 | /// value range when loading the data. 33 | /// 34 | #[derive(Serialize, Deserialize, Clone, Copy, Debug)] 35 | pub struct CtrlMapStorageEntry { 36 | id: ParamId, 37 | map_type: MappingType, 38 | } 39 | 40 | type CtrlHashMap = HashMap; 41 | type CtrlHashMapStorage = HashMap; 42 | 43 | pub struct CtrlMap { 44 | map: Vec 45 | } 46 | 47 | impl CtrlMap { 48 | pub fn new() -> CtrlMap { 49 | // 36 sets of controller mappings (0-9, a-z) 50 | CtrlMap{map: vec!(CtrlHashMap::new(); 36)} 51 | } 52 | 53 | /// Reset the map, removing all controller assignments. 54 | pub fn reset(&mut self) { 55 | for m in &mut self.map { 56 | m.clear(); 57 | } 58 | } 59 | 60 | /// Add a new mapping entry for a controller. 61 | /// 62 | /// set: The selected controller map set 63 | /// controller: The controller number to add 64 | /// map_type: Type of value change (absolute or relative) 65 | /// parameter: The parameter changed with this controller 66 | /// val_range: The valid values for the parameter 67 | /// 68 | pub fn add_mapping(&mut self, 69 | set: usize, 70 | controller: u64, 71 | map_type: MappingType, 72 | parameter: ParamId, 73 | val_range: &'static ValueRange) { 74 | trace!("add_mapping: Set {}, ctrl {}, type {:?}, param {:?}, val range {:?}", 75 | set, controller, map_type, parameter, val_range); 76 | self.map[set].insert(controller, 77 | CtrlMapEntry{id: parameter, 78 | map_type, 79 | val_range}); 80 | } 81 | 82 | /// Delete all mappings for a parameter. 83 | /// 84 | /// Returns true if one or more mappings were deleted, false otherwise 85 | pub fn delete_mapping(&mut self, 86 | set: usize, 87 | parameter: ParamId) -> bool { 88 | trace!("delete_mapping: Set {}, param {:?}", set, parameter); 89 | let mut controller: u64; 90 | let mut found = false; 91 | loop { 92 | controller = 0; 93 | for (key, val) in self.map[set].iter() { 94 | if val.id == parameter { 95 | controller = *key; 96 | } 97 | } 98 | if controller > 0 { 99 | self.map[set].remove(&controller); 100 | found = true; 101 | } else { 102 | break; 103 | } 104 | } 105 | found 106 | } 107 | 108 | /// Update a value according to the controller value. 109 | /// 110 | /// Uses the parameter's val_range to translate the controller value into 111 | /// a valid parameter value. 112 | /// 113 | /// set: The selected controller map set 114 | /// controller: The controller number to look up 115 | /// value: New value of the controller 116 | /// sound: Currently active sound 117 | /// result: SynthParam that receives the changed value 118 | /// 119 | /// Return true if result was updated, false otherwise 120 | /// 121 | pub fn get_value(&self, 122 | set: usize, 123 | controller: u64, 124 | value: u64, 125 | sound: &SoundData) -> Result { 126 | // Get mapping 127 | if !self.map[set].contains_key(&controller) { 128 | return Err(()); 129 | } 130 | let mapping = &self.map[set][&controller]; 131 | let mut result = SynthParam::new_from(&mapping.id); 132 | match mapping.map_type { 133 | MappingType::Absolute => { 134 | // For absolute: Translate value 135 | let trans_val = mapping.val_range.translate_value(value); 136 | result.value = trans_val; 137 | } 138 | MappingType::Relative => { 139 | // For relative: Increase/ decrease value 140 | let sound_value = sound.get_value(&mapping.id); 141 | let delta = if value >= 64 { -1 } else { 1 }; 142 | result.value = mapping.val_range.add_value(sound_value, delta); 143 | } 144 | MappingType::None => panic!(), 145 | }; 146 | Ok(result) 147 | } 148 | 149 | // Load controller mappings from file 150 | pub fn load(&mut self, filename: &str) -> std::io::Result<()> { 151 | info!("Reading controller mapping from {}", filename); 152 | let file = File::open(filename)?; 153 | let mut reader = BufReader::new(file); 154 | self.reset(); 155 | let mut serialized = String::new(); 156 | reader.read_to_string(&mut serialized)?; 157 | let storage_map: Vec = serde_json::from_str(&serialized).unwrap(); 158 | 159 | for (i, item) in storage_map.iter().enumerate() { 160 | for (key, value) in item { 161 | let val_range = MenuItem::get_val_range(value.id.function, value.id.parameter); 162 | self.map[i].insert(*key, CtrlMapEntry{id: value.id, map_type: value.map_type, val_range}); 163 | } 164 | } 165 | Ok(()) 166 | } 167 | 168 | // Store controller mappings to file 169 | pub fn save(&self, filename: &str) -> std::io::Result<()> { 170 | info!("Writing controller mapping to {}", filename); 171 | 172 | // Transfer data into serializable format 173 | let mut storage_map = vec!(CtrlHashMapStorage::new(); 36); 174 | for (i, item) in self.map.iter().enumerate() { 175 | for (key, value) in item { 176 | storage_map[i].insert(*key, CtrlMapStorageEntry{id: value.id, map_type: value.map_type}); 177 | } 178 | } 179 | 180 | let mut file = File::create(filename)?; 181 | let serialized = serde_json::to_string_pretty(&storage_map).unwrap(); 182 | file.write_all(serialized.as_bytes())?; 183 | Ok(()) 184 | } 185 | } 186 | 187 | // ---------------------------------------------- 188 | // Unit tests 189 | // ---------------------------------------------- 190 | 191 | #[cfg(test)] 192 | mod tests { 193 | 194 | use super::{CtrlMap, MappingType}; 195 | use super::super::Float; 196 | use super::super::{Parameter, ParamId, ParameterValue, MenuItem}; 197 | use super::super::{SoundData, SoundPatch}; 198 | use super::super::SynthParam; 199 | 200 | use std::cell::RefCell; 201 | use std::rc::Rc; 202 | 203 | struct TestContext { 204 | map: CtrlMap, 205 | sound: SoundPatch, 206 | sound_data: Rc>, 207 | param_id: ParamId, 208 | synth_param: SynthParam, 209 | } 210 | 211 | impl TestContext { 212 | fn new() -> TestContext { 213 | let map = CtrlMap::new(); 214 | let sound = SoundPatch::new(); 215 | let sound_data = Rc::new(RefCell::new(sound.data)); 216 | let param_id = ParamId::new(Parameter::Oscillator, 1, Parameter::Level); 217 | let synth_param = SynthParam::new(Parameter::Oscillator, 218 | 1, 219 | Parameter::Level, 220 | ParameterValue::Float(0.0)); 221 | TestContext{map, sound, sound_data, param_id, synth_param} 222 | } 223 | 224 | pub fn add_controller(&mut self, ctrl_no: u64, ctrl_type: MappingType) { 225 | let val_range = MenuItem::get_val_range(self.param_id.function, self.param_id.parameter); 226 | self.map.add_mapping(1, 227 | ctrl_no, 228 | ctrl_type, 229 | self.param_id, 230 | val_range); 231 | } 232 | 233 | pub fn handle_controller(&mut self, ctrl_no: u64, value: u64) -> bool { 234 | let sound_data = &mut self.sound_data.borrow_mut(); 235 | match self.map.get_value(1, ctrl_no, value, sound_data) { 236 | Ok(result) => { 237 | sound_data.set_parameter(&result); 238 | true 239 | } 240 | Err(()) => false 241 | } 242 | } 243 | 244 | pub fn has_value(&mut self, value: Float) -> bool { 245 | let pval = self.sound_data.borrow().get_value(&self.param_id); 246 | if let ParameterValue::Float(x) = pval { 247 | println!("\nIs: {} Expected: {}", x, value); 248 | x == value 249 | } else { 250 | false 251 | } 252 | } 253 | 254 | pub fn delete_controller(&mut self) -> bool { 255 | self.map.delete_mapping(1, self.param_id) 256 | } 257 | } 258 | 259 | #[test] 260 | fn controller_without_mapping_returns_no_value() { 261 | let mut context = TestContext::new(); 262 | assert_eq!(context.handle_controller(1, 50), false); 263 | } 264 | 265 | #[test] 266 | fn absolute_controller_can_be_added() { 267 | let mut context = TestContext::new(); 268 | context.add_controller(1, MappingType::Absolute); 269 | assert_eq!(context.handle_controller(1, 50), true); 270 | } 271 | 272 | #[test] 273 | fn value_can_be_changed_absolute() { 274 | let mut context = TestContext::new(); 275 | assert_eq!(context.has_value(50.0), true); 276 | context.add_controller(1, MappingType::Absolute); 277 | assert_eq!(context.handle_controller(1, 0), true); 278 | assert_eq!(context.has_value(0.0), true); 279 | } 280 | 281 | #[test] 282 | fn relative_controller_can_be_added() { 283 | let mut context = TestContext::new(); 284 | context.add_controller(1, MappingType::Relative); 285 | assert_eq!(context.handle_controller(1, 50), true); 286 | } 287 | 288 | #[test] 289 | fn value_can_be_changed_relative() { 290 | let mut context = TestContext::new(); 291 | assert_eq!(context.has_value(50.0), true); 292 | context.add_controller(1, MappingType::Relative); 293 | 294 | // Increase value 295 | assert_eq!(context.handle_controller(1, 0), true); 296 | assert_eq!(context.has_value(51.0), true); 297 | 298 | // Decrease value 299 | assert_eq!(context.handle_controller(1, 127), true); 300 | assert_eq!(context.has_value(50.0), true); 301 | } 302 | 303 | #[test] 304 | fn mapping_can_be_deleted() { 305 | let mut context = TestContext::new(); 306 | context.add_controller(1, MappingType::Relative); 307 | assert_eq!(context.handle_controller(1, 127), true); 308 | assert_eq!(context.delete_controller(), true); 309 | assert_eq!(context.handle_controller(1, 127), false); 310 | } 311 | 312 | #[test] 313 | fn nonexisting_mapping_isnt_deleted() { 314 | let mut context = TestContext::new(); 315 | assert_eq!(context.delete_controller(), false); 316 | } 317 | 318 | } // mod tests 319 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! Yazz - Yet another subtractive synthesizer 2 | //! 3 | //! # Running the synth 4 | //! 5 | //! The synth needs to find a MIDI interface and a soundcard in the system. 6 | //! For sound, the default output device is used. The MIDI device to use as 7 | //! input can be selected with the "-m " command line parameter. 8 | //! 9 | //! # Running the tests 10 | //! 11 | //! The test code supports writing output to a logfile. Since only a single 12 | //! logfile writer can exist, the tests currently can't run in parallel. The 13 | //! easiest way around this problem is by starting the test with a single 14 | //! thread: 15 | //! > RUST_TEST_THREADS=1 cargo test 16 | //! 17 | 18 | #![allow(dead_code)] 19 | 20 | mod ctrl_map; 21 | use ctrl_map::{CtrlMap, MappingType}; 22 | 23 | mod midi_handler; 24 | use midi_handler::{MidiHandler, MidiMessage}; 25 | 26 | mod modulation; 27 | use modulation::ModData; 28 | 29 | mod parameter; 30 | use parameter::*; 31 | 32 | mod sound; 33 | use sound::{SoundData, SyncValue}; 34 | 35 | mod storage; 36 | use storage::{SoundBank, SoundPatch}; 37 | 38 | mod synth; 39 | use synth::*; 40 | use voice::Voice; 41 | 42 | mod tui; 43 | use tui::{Tui, Index}; 44 | use tui::termion_wrapper::TermionWrapper; 45 | 46 | mod value_range; 47 | use value_range::ValueRange; 48 | 49 | extern crate termion; 50 | use termion::event::Key; 51 | 52 | extern crate midir; 53 | use midir::MidiInputConnection; 54 | 55 | extern crate crossbeam_channel; 56 | use crossbeam_channel::unbounded; 57 | use crossbeam_channel::{Sender, Receiver}; 58 | 59 | use flexi_logger::{Logger, opt_format}; 60 | use log::error; 61 | 62 | extern crate clap; 63 | use clap::{Arg, App}; 64 | 65 | extern crate wavetable; 66 | use wavetable::{WtInfo, WtManager}; 67 | 68 | use std::io::prelude::*; 69 | use std::fs::File; 70 | use std::sync::{Arc, Mutex}; 71 | use std::thread::JoinHandle; 72 | use std::time::Duration; 73 | use std::vec::Vec; 74 | 75 | pub const SYNTH_ENGINE_VERSION: &str = "0.0.8"; 76 | pub const SOUND_DATA_VERSION: &str = "0.0.8"; 77 | 78 | type Float = f64; 79 | 80 | // Messages sent to the synth engine 81 | pub enum SynthMessage { 82 | Midi(MidiMessage), 83 | Param(SynthParam), 84 | Sound(Box), 85 | Wavetable(WtInfo), 86 | SampleBuffer(Vec, SynthParam), 87 | Bpm(Float), 88 | Exit 89 | } 90 | 91 | // Messages sent to the UI 92 | pub enum UiMessage { 93 | Midi(MidiMessage), 94 | Key(Key), 95 | MousePress{x: Index, y: Index}, 96 | MouseHold{x: Index, y: Index}, 97 | MouseRelease{x: Index, y: Index}, 98 | SampleBuffer(Vec, SynthParam), 99 | EngineSync(Duration, Duration), 100 | Exit, 101 | } 102 | 103 | fn setup_logging() { 104 | Logger::with_env_or_str("myprog=debug, mylib=warn") 105 | .log_to_file() 106 | .directory("log_files") 107 | .format(opt_format) 108 | .start() 109 | .unwrap(); 110 | } 111 | 112 | fn setup_messaging() -> (Sender, Receiver, Sender, Receiver) { 113 | // Prepare communication channels 114 | let (to_ui_sender, ui_receiver) = unbounded::(); // MIDI and Synth to UI 115 | let (to_synth_sender, synth_receiver) = unbounded::(); // MIDI and UI to Synth 116 | (to_ui_sender, ui_receiver, to_synth_sender, synth_receiver) 117 | } 118 | 119 | fn setup_midi(m2s_sender: Sender, m2u_sender: Sender, midi_port: usize, mut midi_channel: u8) -> Result, ()> { 120 | println!("Setting up MIDI... "); 121 | if midi_channel < 1 || midi_channel > 16 { 122 | midi_channel = 16; // Omni 123 | } else { 124 | midi_channel -= 1; // 0 - 15 125 | } 126 | let conn_in = MidiHandler::run(m2s_sender, m2u_sender, midi_port, midi_channel); 127 | println!("... finished."); 128 | conn_in 129 | } 130 | 131 | fn setup_ui(to_synth_sender: Sender, to_ui_sender: Sender, ui_receiver: Receiver, show_tui: bool) -> Result<(JoinHandle<()>, JoinHandle<()>), ()> { 132 | println!("Setting up UI..."); 133 | let termion_result = TermionWrapper::new(); 134 | let termion = match termion_result { 135 | Ok(t) => t, 136 | Err(e) => { 137 | println!("\nError setting terminal into raw mode: {}", e); 138 | return Err(()); 139 | } 140 | }; 141 | let term_handle = TermionWrapper::run(to_ui_sender); 142 | let tui_handle = Tui::run(to_synth_sender, ui_receiver, show_tui, termion); 143 | println!("\r... finished"); 144 | Ok((term_handle, tui_handle)) 145 | } 146 | 147 | fn setup_synth(sample_rate: u32, s2u_sender: Sender, synth_receiver: Receiver) -> (Arc>, std::thread::JoinHandle<()>) { 148 | println!("\rSetting up synth engine..."); 149 | let synth = Synth::new(sample_rate, s2u_sender); 150 | let synth = Arc::new(Mutex::new(synth)); 151 | let synth_handle = Synth::run(synth.clone(), synth_receiver); 152 | println!("\r... finished"); 153 | (synth, synth_handle) 154 | } 155 | 156 | fn setup_audio() -> Result<(Engine, u32), ()> { 157 | println!("\rSetting up audio engine..."); 158 | let result = Engine::new(); 159 | let engine = match result { 160 | Ok(e) => e, 161 | Err(()) => { 162 | error!("Failed to start audio engine"); 163 | println!("Failed to start audio engine"); 164 | return Err(()); 165 | } 166 | }; 167 | let sample_rate = engine.get_sample_rate(); 168 | println!("\r sample_rate: {}", sample_rate); 169 | println!("\r... finished"); 170 | Ok((engine, sample_rate)) 171 | } 172 | 173 | // Save one table of a wavetable set as a CSV file. 174 | fn save_wave(id: usize) -> std::io::Result<()> { 175 | let wt_manager = WtManager::new(44100.0, "."); 176 | let mut filename = "synth_wave_".to_string(); 177 | filename += &id.to_string(); 178 | filename += ".csv"; 179 | let mut file = File::create(filename)?; 180 | let wt = wt_manager.get_table(0).unwrap(); 181 | let t = &wt.table[id]; 182 | // Write only the first octave table (= first 2048 values) 183 | for (i, sample) in t.iter().enumerate().take(2048) { 184 | let s = format!("{}, {:?}\n", i, sample); 185 | file.write_all(s.as_bytes())?; 186 | } 187 | Ok(()) 188 | } 189 | 190 | // Save one samplebuffer of a voice as CSV file. 191 | fn save_voice() -> std::io::Result<()> { 192 | let wt_manager = WtManager::new(44100.0, "."); 193 | let filename = "synth_voice.csv".to_string(); 194 | let mut file = File::create(filename)?; 195 | let wt = wt_manager.get_table(0).unwrap(); 196 | let mut voice = Voice::new(44100, wt); 197 | let mut sound_global = SoundData::new(); 198 | let mut sound_local = SoundData::new(); 199 | let global_state = SynthState{freq_factor: 1.0}; 200 | 201 | sound_global.init(); 202 | sound_global.osc[0].level = 1.0; 203 | sound_global.env[0].attack = 0.0; 204 | sound_global.env[0].decay = 0.0; 205 | sound_global.env[0].sustain = 1.0; 206 | sound_global.env[0].factor = 1.0; 207 | sound_global.filter[0].filter_type = 0; // Bypass 208 | 209 | voice.set_freq(21.533203125); 210 | voice.trigger(0, 0, &sound_global); 211 | 212 | for i in 0..2048 { 213 | let value = voice.get_sample(i, &sound_global, &mut sound_local, &global_state); 214 | let s = format!("{}, {:?}\n", i, value); 215 | file.write_all(s.as_bytes())?; 216 | } 217 | Ok(()) 218 | } 219 | 220 | fn main() { 221 | setup_logging(); 222 | 223 | // Command line arguments 224 | let matches = App::new("Yazz") 225 | .version(SYNTH_ENGINE_VERSION) 226 | .about("Yet Another Subtractive Synth") 227 | .arg(Arg::with_name("version") 228 | .short("v") 229 | .long("version") 230 | .help("Shows the version of the sound engine and the sound file format")) 231 | .arg(Arg::with_name("notui") 232 | .short("n") 233 | .long("no-tui") 234 | .help("Disable drawing of text user interface")) 235 | .arg(Arg::with_name("savewave") 236 | .short("s") 237 | .long("save") 238 | .help("Saves selected wave to file") 239 | .takes_value(true)) 240 | .arg(Arg::with_name("savevoice") 241 | .short("o") 242 | .long("voice") 243 | .help("Saves output of single voice to file")) 244 | .arg(Arg::with_name("midiport") 245 | .short("m") 246 | .long("midiport") 247 | .help("Selects the MIDI port to receive MIDI events on (1 - n, default 1)") 248 | .takes_value(true)) 249 | .arg(Arg::with_name("midichannel") 250 | .short("c") 251 | .long("midichannel") 252 | .help("Selects the MIDI channel to receive MIDI events on (1 - 16, default = omni)") 253 | .takes_value(true)) 254 | .get_matches(); 255 | let midi_port = matches.value_of("midiport").unwrap_or("1"); 256 | let midi_port: usize = midi_port.parse().unwrap_or(1); 257 | let midi_channel = matches.value_of("midichannel").unwrap_or("0"); 258 | let midi_channel: u8 = midi_channel.parse().unwrap_or(0); 259 | let show_tui = !matches.is_present("notui"); 260 | 261 | // Show version 262 | if matches.is_present("version") { 263 | println!("Yazz v{}, sound file v{}", SYNTH_ENGINE_VERSION, SOUND_DATA_VERSION); 264 | return; 265 | } 266 | 267 | // For debugging: Save selected wavetable as file 268 | let wave_index = matches.value_of("savewave").unwrap_or(""); 269 | if !wave_index.is_empty() { 270 | let wave_index: usize = wave_index.parse().unwrap_or(1); 271 | save_wave(wave_index).unwrap(); 272 | return; 273 | } 274 | if matches.is_present("savevoice") { 275 | println!("Saving voice output"); 276 | save_voice().unwrap(); 277 | return; 278 | } 279 | 280 | // Do setup 281 | let (to_ui_sender, ui_receiver, to_synth_sender, synth_receiver) = setup_messaging(); 282 | let result = setup_midi(to_synth_sender.clone(), to_ui_sender.clone(), midi_port, midi_channel); 283 | let midi_connection = match result { 284 | Ok(c) => c, 285 | Err(()) => return, 286 | }; 287 | 288 | let result = setup_audio(); 289 | let (mut engine, sample_rate) = match result { 290 | Ok((e, s)) => (e, s), 291 | Err(()) => return, 292 | }; 293 | 294 | let result = setup_ui(to_synth_sender, to_ui_sender.clone(), ui_receiver, show_tui); 295 | let (term_handle, tui_handle) = match result { 296 | Ok((term, tui)) => (term, tui), 297 | Err(_) => return, // TODO: Reset terminal to non-raw state 298 | }; 299 | 300 | let (synth, synth_handle) = setup_synth(sample_rate, to_ui_sender.clone(), synth_receiver); 301 | 302 | // Run 303 | println!("\r... finished, starting processing"); 304 | engine.run(synth, to_ui_sender).unwrap(); 305 | 306 | // Cleanup 307 | term_handle.join().unwrap(); 308 | println!("\rTerminal handler finished"); 309 | midi_connection.close(); 310 | tui_handle.join().unwrap(); 311 | println!("TUI finished"); 312 | synth_handle.join().unwrap(); 313 | println!("Synth engine finished"); 314 | } 315 | 316 | -------------------------------------------------------------------------------- /src/midi_handler.rs: -------------------------------------------------------------------------------- 1 | use midir::{MidiInput, MidiInputConnection, Ignore}; 2 | 3 | use crossbeam_channel::Sender; 4 | use log::{info, error}; 5 | 6 | use super::{SynthMessage, UiMessage}; 7 | use super::Float; 8 | 9 | #[derive(Clone, Copy, Debug)] 10 | pub enum MidiMessage { 11 | NoteOff {channel: u8, key: u8, velocity: u8}, 12 | NoteOn {channel: u8, key: u8, velocity: u8}, 13 | KeyAT {channel: u8, key: u8, pressure: u8}, 14 | ControlChg {channel: u8, controller: u8, value: u8}, 15 | ProgramChg {channel: u8, program: u8}, 16 | ChannelAT {channel: u8, pressure: u8}, 17 | Pitchbend {channel: u8, pitch: i16}, 18 | SongPos {position: u16}, 19 | TimingClock, 20 | Start, 21 | Continue, 22 | Stop, 23 | ActiveSensing, 24 | Reset, 25 | } 26 | 27 | pub struct MidiHandler { 28 | last_timestamp: u64, 29 | bpm: Float 30 | } 31 | 32 | impl MidiHandler { 33 | fn new() -> Self { 34 | MidiHandler{last_timestamp: 0, bpm: 0.0} 35 | } 36 | 37 | /** Starts the thread for receiving MIDI events. 38 | * 39 | * If the midi_channel argument is 0, all events are forwarded. If it is 40 | * between 1 and 16, only events arriving on that channel are forwarded. 41 | */ 42 | pub fn run(m2s_sender: Sender, 43 | m2u_sender: Sender, 44 | midi_port: usize, 45 | midi_channel: u8) -> Result, ()> { 46 | let result = MidiInput::new("Yazz MIDI input"); 47 | let mut midi_in = match result { 48 | Ok(m) => m, 49 | Err(e) => { 50 | error!("Can't open MIDI input connection: {:?}", e); 51 | println!("Can't open MIDI input connection: {:?}", e); 52 | return Err(()); 53 | } 54 | }; 55 | midi_in.ignore(Ignore::None); 56 | let result = midi_in.port_name(midi_port); 57 | let in_port_name = match result { 58 | Ok(n) => n, 59 | Err(e) => { 60 | error!("Can't get MIDI port name: {:?}", e); 61 | println!("Can't get MIDI port name: {:?}", e); 62 | return Err(()); 63 | } 64 | }; 65 | let mut mh = MidiHandler::new(); 66 | info!(" Connecting to MIDI port {}", in_port_name); 67 | println!(" Connecting to MIDI port {}", in_port_name); 68 | let conn_result = midi_in.connect(midi_port, "midir-read-input", move |timestamp, message, _| { 69 | if midi_channel < 16 && (message[0] & 0x0F) != midi_channel { 70 | return; 71 | } 72 | let m = MidiHandler::get_midi_message(message); 73 | info!("MidiMessage: {:?}", m); 74 | match m { 75 | MidiMessage::ControlChg{channel: _, controller: _, value: _} | 76 | MidiMessage::ProgramChg{channel: _, program: _} => { 77 | // Send control change and program change to UI 78 | m2u_sender.send(UiMessage::Midi(m)).unwrap(); 79 | } 80 | MidiMessage::TimingClock => { 81 | // Calculate BPM 82 | let bpm_changed = mh.calc_bpm(timestamp); 83 | if bpm_changed { 84 | m2s_sender.send(SynthMessage::Bpm(mh.bpm)).unwrap(); 85 | } 86 | } 87 | _ => { 88 | // Send everything else directly to the synth engine 89 | m2s_sender.send(SynthMessage::Midi(m)).unwrap(); 90 | } 91 | } 92 | }, ()); 93 | match conn_result { 94 | Ok(c) => Ok(c), 95 | Err(e) => { 96 | error!("Failed to connect to MIDI port: {:?}", e); 97 | Err(()) 98 | } 99 | } 100 | } 101 | 102 | pub fn get_midi_message(message: &[u8]) -> MidiMessage { 103 | let param = if message.len() > 1 { message[1] } else { 0 }; 104 | let value = if message.len() > 2 { message[2] } else { 0 }; 105 | 106 | match message[0] { 107 | 0xF2 => { 108 | let mut position: u16 = param as u16; 109 | position |= (value as u16) << 7; 110 | MidiMessage::SongPos{position} 111 | } 112 | 0xF8 => MidiMessage::TimingClock, 113 | 0xFA => MidiMessage::Start, 114 | 0xFB => MidiMessage::Continue, 115 | 0xFC => MidiMessage::Stop, 116 | 0xFE => MidiMessage::ActiveSensing, 117 | 0xFF => MidiMessage::Reset, 118 | _ => { 119 | let channel = message[0] & 0x0F; 120 | match message[0] & 0xF0 { 121 | 0x90 => MidiMessage::NoteOn{channel, key: param, velocity: value}, 122 | 0x80 => MidiMessage::NoteOff{channel, key: param, velocity: value}, 123 | 0xA0 => MidiMessage::KeyAT{channel, key: param, pressure: value}, 124 | 0xB0 => MidiMessage::ControlChg{channel, controller: param, value}, 125 | 0xC0 => MidiMessage::ProgramChg{channel, program: param}, 126 | 0xD0 => MidiMessage::ChannelAT{channel, pressure: param}, 127 | 0xE0 => { 128 | let mut pitch: i16 = param as i16; 129 | pitch |= (value as i16) << 7; 130 | pitch -= 0x2000; 131 | MidiMessage::Pitchbend{channel, pitch} 132 | }, 133 | _ => panic!(), 134 | } 135 | } 136 | } 137 | } 138 | 139 | fn calc_bpm(&mut self, timestamp: u64) -> bool { 140 | let mut bpm_changed = false; 141 | if self.last_timestamp != 0 { 142 | // We have a previous TS, so we can calculate the current BPM 143 | let diff = (timestamp - self.last_timestamp) * 24; // Diff is in usec 144 | let bpm = 60000000.0 / diff as f64; 145 | //let bpm = self.avg.add_value(bpm); 146 | // Calculate up to 1 decimal of BPM 147 | let bpm = (bpm * 10.0).round() / 10.0; 148 | if bpm != self.bpm { 149 | self.bpm = bpm; 150 | bpm_changed = true; 151 | } 152 | } 153 | self.last_timestamp = timestamp; 154 | bpm_changed 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /src/modulation.rs: -------------------------------------------------------------------------------- 1 | use super::Float; 2 | use super::{Parameter, MenuItem, ValueRange}; 3 | use super::{ParamId, FunctionId}; 4 | use super::voice::{NUM_OSCILLATORS, NUM_ENVELOPES, NUM_LFOS}; 5 | use super::synth::NUM_GLOBAL_LFOS; 6 | 7 | use log::info; 8 | use serde::{Serialize, Deserialize}; 9 | 10 | /* 11 | #[derive(Debug)] 12 | pub enum ModValRange { 13 | IntRange(i64, i64), 14 | Float(Float, Float), 15 | } 16 | 17 | impl Default for ModValRange { 18 | fn default() -> Self { ModValRange::IntRange(0, 0) } 19 | } 20 | */ 21 | 22 | /** Defines a source of modulation data and its value range. */ 23 | #[derive(Debug, Default)] 24 | pub struct ModSource { 25 | pub function: Parameter, 26 | pub index_range: (usize, usize), // Min, Max 27 | pub val_range: ValueRange, 28 | pub is_global: bool, 29 | } 30 | 31 | /** Static list of available modulation data sources. */ 32 | static MOD_SOURCE: [ModSource; 9] = [ 33 | ModSource{function: Parameter::GlobalLfo, index_range: (1, NUM_GLOBAL_LFOS), val_range: ValueRange::Float(-1.0, 1.0, 0.1), is_global: true}, 34 | ModSource{function: Parameter::Aftertouch, index_range: (1, 1), val_range: ValueRange::Float(0.0, 1.0, 0.1), is_global: true}, 35 | ModSource{function: Parameter::Pitchbend, index_range: (1, 1), val_range: ValueRange::Float(-1.0, 1.0, 0.01), is_global: true}, 36 | ModSource{function: Parameter::ModWheel, index_range: (1, 1), val_range: ValueRange::Float(0.0, 127.0, 0.1), is_global: true}, 37 | ModSource{function: Parameter::SustainPedal,index_range: (1, 1), val_range: ValueRange::Float(0.0, 1.0, 1.0), is_global: true}, 38 | 39 | ModSource{function: Parameter::Envelope, index_range: (1, NUM_ENVELOPES), val_range: ValueRange::Float(0.0, 1.0, 0.01), is_global: false}, 40 | ModSource{function: Parameter::Lfo, index_range: (1, NUM_LFOS), val_range: ValueRange::Float(-1.0, 1.0, 0.01), is_global: false}, 41 | ModSource{function: Parameter::Oscillator, index_range: (1, NUM_OSCILLATORS), val_range: ValueRange::Float(-1.0, 1.0, 0.01), is_global: false}, 42 | ModSource{function: Parameter::Velocity, index_range: (1, 1), val_range: ValueRange::Float(0.0, 1.0, 0.1), is_global: false}, 43 | ]; 44 | 45 | #[derive(Serialize, Deserialize, Copy, Clone, Debug, Default)] 46 | pub struct ModData { 47 | pub source_func: Parameter, 48 | pub source_func_id: usize, 49 | pub target_func: Parameter, 50 | pub target_func_id: usize, 51 | pub target_param: Parameter, 52 | pub amount: Float, 53 | pub active: bool, 54 | pub is_global: bool, 55 | pub scale: Float, 56 | } 57 | 58 | impl ModData { 59 | pub fn new() -> ModData { 60 | let source_func = Parameter::Lfo; 61 | let source_func_id = 1; 62 | let target_func = Parameter::Oscillator; 63 | let target_func_id = 1; 64 | let target_param = Parameter::Level; 65 | let amount = 0.0; 66 | let active = false; 67 | let is_global = false; 68 | let scale = 0.0; 69 | ModData{source_func, source_func_id, target_func, target_func_id, target_param, amount, active, is_global, scale} 70 | } 71 | 72 | pub fn set_source(&mut self, func: &FunctionId) { 73 | self.source_func = func.function; 74 | self.source_func_id = func.function_id; 75 | self.update(); 76 | } 77 | 78 | pub fn set_target(&mut self, param: &ParamId) { 79 | self.target_func = param.function; 80 | self.target_func_id = param.function_id; 81 | self.target_param = param.parameter; 82 | self.update(); 83 | } 84 | 85 | pub fn set_amount(&mut self, amount: Float) { 86 | self.amount = amount; 87 | self.update(); 88 | } 89 | 90 | pub fn update(&mut self) { 91 | // Modulation source 92 | let source = ModData::get_mod_source(self.source_func); 93 | let (source_min, source_max) = source.val_range.get_min_max(); 94 | 95 | // Modulation target 96 | let dest_range = MenuItem::get_val_range(self.target_func, self.target_param); 97 | let (dest_min, dest_max) = dest_range.get_min_max(); 98 | 99 | // Calculate scale factor 100 | // Scale is the factor applied to the mod source value to cover the 101 | // total target value range. Mod amount limits it to a smaller range. 102 | self.scale = ((dest_max - dest_min) / (source_max - source_min)) * self.amount; 103 | self.is_global = source.is_global; 104 | info!("Updated modulator {:?}", self); 105 | } 106 | 107 | pub fn get_source(&self) -> FunctionId { 108 | FunctionId{function: self.source_func, function_id: self.source_func_id} 109 | } 110 | 111 | pub fn get_target(&self) -> ParamId { 112 | ParamId{function: self.target_func, function_id: self.target_func_id, parameter: self.target_param} 113 | } 114 | 115 | fn get_mod_source(function: Parameter) -> &'static ModSource { 116 | for s in &MOD_SOURCE { 117 | if s.function == function { 118 | return &s; 119 | } 120 | } 121 | panic!(); 122 | } 123 | } 124 | 125 | -------------------------------------------------------------------------------- /src/ringbuffer.rs: -------------------------------------------------------------------------------- 1 | pub struct Ringbuffer { 2 | write_pos: usize, // Position to write next value to 3 | } 4 | 5 | impl Ringbuffer { 6 | pub fn new() -> Ringbuffer { 7 | let write_pos = 0; 8 | Ringbuffer{write_pos} 9 | } 10 | 11 | pub fn init(&mut self, buffer: &mut [T]) { 12 | for i in 0..buffer.len() { 13 | buffer[i] = T::default(); 14 | } 15 | } 16 | 17 | pub fn add(&mut self, buffer: &mut [T], value: T) { 18 | let len = buffer.len(); 19 | self.write_pos = Ringbuffer::inc(self.write_pos, len); 20 | buffer[self.write_pos] = value; 21 | } 22 | 23 | /** Get a value from the ringbuffer. 24 | * 25 | * index is the age of the value, the higher the index, the further back from the write 26 | * position it is. 27 | */ 28 | pub fn get(&mut self, buffer: &[T], mut index: usize) -> T { 29 | let len = buffer.len(); 30 | while index >= len { 31 | index -= len; 32 | } 33 | let index = Ringbuffer::sub(self.write_pos, index, len); 34 | buffer[index] 35 | } 36 | 37 | fn inc(mut val: usize, max: usize) -> usize { 38 | val += 1; 39 | val = if val >= max { val - max } else { val }; 40 | val 41 | } 42 | 43 | fn sub(mut value: usize, dec: usize, len: usize) -> usize { 44 | if value < dec { 45 | value += len; 46 | } 47 | value - dec 48 | } 49 | } 50 | 51 | #[cfg(test)] 52 | #[test] 53 | fn ringbuff_can_be_filled_and_read() { 54 | let mut buffer = [0.0; 4]; 55 | let mut rb = Ringbuffer::new(); 56 | rb.init(&mut buffer); 57 | 58 | rb.add(&mut buffer, 1.0); 59 | rb.add(&mut buffer, 2.0); 60 | rb.add(&mut buffer, 3.0); 61 | rb.add(&mut buffer, 4.0); 62 | 63 | assert_eq!(rb.get(&mut buffer, 0), 4.0); 64 | assert_eq!(rb.get(&mut buffer, 1), 3.0); 65 | assert_eq!(rb.get(&mut buffer, 2), 2.0); 66 | assert_eq!(rb.get(&mut buffer, 3), 1.0); 67 | assert_eq!(rb.get(&mut buffer, 4), 4.0); 68 | 69 | rb.add(&mut buffer, 5.0); 70 | assert_eq!(rb.get(&mut buffer, 0), 5.0); 71 | assert_eq!(rb.get(&mut buffer, 1), 4.0); 72 | assert_eq!(rb.get(&mut buffer, 2), 3.0); 73 | assert_eq!(rb.get(&mut buffer, 3), 2.0); 74 | } 75 | -------------------------------------------------------------------------------- /src/storage.rs: -------------------------------------------------------------------------------- 1 | use super::SoundData; 2 | use super::WtInfo; 3 | 4 | use log::info; 5 | use serde::{Serialize, Deserialize}; 6 | 7 | use std::fs::File; 8 | use std::io::BufReader; 9 | use std::io::prelude::*; 10 | 11 | #[derive(Clone, Debug, Serialize, Deserialize)] 12 | struct SoundBankInfo { 13 | sound_data_version: String, 14 | synth_engine_version: String, 15 | } 16 | 17 | #[derive(Clone, Debug, Serialize, Deserialize)] 18 | pub struct SoundPatch { 19 | pub name: String, 20 | pub data: SoundData 21 | } 22 | 23 | impl SoundPatch { 24 | pub fn new() -> SoundPatch { 25 | Default::default() 26 | } 27 | } 28 | 29 | impl Default for SoundPatch { 30 | fn default() -> Self { 31 | let name = "Init".to_string(); 32 | let mut data = SoundData{..Default::default()}; 33 | data.init(); 34 | SoundPatch{name, data} 35 | } 36 | } 37 | 38 | #[derive(Clone, Debug, Serialize, Deserialize)] 39 | pub struct SoundBank { 40 | info: SoundBankInfo, // Binary and sound version 41 | sounds: Vec, // List of sound patches 42 | pub wt_list: Vec // List of available wavetables 43 | } 44 | 45 | impl SoundBank { 46 | pub fn new(sound_data_version: &'static str, synth_engine_version: &'static str) -> SoundBank { 47 | let info = SoundBankInfo{sound_data_version: sound_data_version.to_string(), 48 | synth_engine_version: synth_engine_version.to_string()}; 49 | let sounds = vec!(SoundPatch{..Default::default()}; 128); 50 | let wt_list: Vec = Vec::new(); 51 | SoundBank{info, sounds, wt_list} 52 | } 53 | 54 | pub fn load_bank(&mut self, filename: &str) -> std::io::Result<()> { 55 | let result = File::open(filename); 56 | match result { 57 | Ok(file) => { 58 | let mut reader = BufReader::new(file); 59 | let mut serialized = String::new(); 60 | reader.read_to_string(&mut serialized)?; 61 | let result: Result = serde_json::from_str(&serialized); 62 | if let Ok(data) = result { 63 | *self = data; 64 | } 65 | } 66 | Err(err) => info!("Error reading default sound file: {}, using empty bank.", err), 67 | } 68 | Ok(()) 69 | } 70 | 71 | pub fn save_bank(&self, filename: &str) -> std::io::Result<()> { 72 | let mut file = File::create(filename)?; 73 | let serialized = serde_json::to_string_pretty(&self).unwrap(); 74 | file.write_all(serialized.as_bytes())?; 75 | Ok(()) 76 | } 77 | 78 | pub fn get_sound(&self, sound_index: usize) -> &SoundPatch { 79 | &self.sounds[sound_index] 80 | } 81 | 82 | pub fn set_sound(&mut self, sound_index: usize, to_sound: &SoundPatch) { 83 | self.sounds[sound_index].name = to_sound.name.clone(); 84 | self.sounds[sound_index].data = to_sound.data; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/synth/delay.rs: -------------------------------------------------------------------------------- 1 | use super::Float; 2 | use super::SyncValue; 3 | use super::filter::OnePole; 4 | 5 | use serde::{Serialize, Deserialize}; 6 | 7 | const BUFF_LEN: usize = 44100; 8 | 9 | #[derive(Serialize, Deserialize, Copy, Clone, Default, Debug)] 10 | pub struct DelayData { 11 | pub level: Float, 12 | pub feedback: Float, 13 | pub time: Float, 14 | pub sync: SyncValue, 15 | pub tone: Float, 16 | pub delay_type: usize, 17 | } 18 | 19 | impl DelayData { 20 | pub fn init(&mut self) { 21 | self.level = 0.0; 22 | self.feedback = 0.5; 23 | self.time = 0.5; 24 | self.sync = SyncValue::Off; 25 | self.tone = 1600.0; 26 | } 27 | } 28 | 29 | pub struct Delay { 30 | sample_rate: Float, 31 | bb_l: [Float; BUFF_LEN], // Buffer with samples 32 | bb_r: [Float; BUFF_LEN], // Second buffer with samples 33 | position: Float, // Current read/ write position 34 | quant_pos: usize, // Last position, quantized to usize 35 | filter_l: OnePole, 36 | filter_r: OnePole, 37 | } 38 | 39 | impl Delay { 40 | pub fn new(sample_rate: u32) -> Delay { 41 | let mut filter_l = OnePole::new(sample_rate); 42 | let mut filter_r = OnePole::new(sample_rate); 43 | let sample_rate = sample_rate as Float; 44 | let bb_l = [0.0; BUFF_LEN]; 45 | let bb_r = [0.0; BUFF_LEN]; 46 | let position = 0.1; 47 | let quant_pos = 0; 48 | filter_l.update(2000.0); // Initial frequency at 2kHz 49 | filter_r.update(2000.0); // Initial frequency at 2kHz 50 | Delay{sample_rate, bb_l, bb_r, position, quant_pos, filter_l, filter_r} 51 | } 52 | 53 | pub fn reset(&mut self) { 54 | for sample in self.bb_l.iter_mut() { 55 | *sample = 0.0; 56 | } 57 | for sample in self.bb_r.iter_mut() { 58 | *sample = 0.0; 59 | } 60 | self.position = 0.1; 61 | self.quant_pos = 0; 62 | } 63 | 64 | pub fn process(&mut self, sample_l: Float, sample_r: Float, _sample_clock: i64, data: &DelayData) -> (Float, Float) { 65 | // TODO: Calculate the passed time using sample_clock 66 | let step = (self.bb_l.len() as Float / data.time) / self.sample_rate; // The amount of samples we step forward, as float 67 | let step = Delay::addf(step, 0.0); 68 | self.position = Delay::addf(self.position, step); 69 | let new_quant_pos = Delay::add(self.position.round() as usize, 0); // Add 0 to get the wrapping protection 70 | let num_samples = Delay::diff(new_quant_pos, self.quant_pos); // Actual number of samples we will be stepping over 71 | 72 | // Left side 73 | // --------- 74 | // Get the average of all samples we're stepping over 75 | let mut sample_sum_l = 0.0; 76 | let mut sample_sum_r = 0.0; 77 | let mut pos = self.quant_pos; 78 | for _ in 0..num_samples { 79 | pos = Delay::add(pos, 1); 80 | sample_sum_l += self.bb_l[pos]; 81 | sample_sum_r += self.bb_r[pos]; 82 | } 83 | sample_sum_l /= num_samples as Float; 84 | sample_sum_r /= num_samples as Float; 85 | 86 | let mut filtered_value_l: Float; 87 | let mut filtered_value_r: Float; 88 | pos = self.quant_pos; 89 | if data.delay_type == 1 { 90 | // PingPong 91 | 92 | // Mix delay signal to input and update memory. This step exchanges 93 | // the samples between left and right. 94 | // (steps through all positions that we jumped over when averaging) 95 | pos = self.quant_pos; 96 | for _ in 0..num_samples as usize { 97 | pos = Delay::add(pos, 1); 98 | filtered_value_l = self.filter_l.process(sample_l + self.bb_l[pos] * data.feedback); 99 | filtered_value_r = self.filter_r.process( self.bb_r[pos] * data.feedback); 100 | self.bb_l[pos] = filtered_value_r; 101 | self.bb_r[pos] = filtered_value_l; 102 | } 103 | } else { 104 | // Stereo 105 | // Mix delay signal to input and update memory 106 | // (steps through all positions that we jumped over when averaging) 107 | for _ in 0..num_samples as usize { 108 | pos = Delay::add(pos, 1); 109 | filtered_value_l = self.filter_l.process(sample_l + self.bb_l[pos] * data.feedback); 110 | self.bb_l[pos] = filtered_value_l; 111 | filtered_value_r = self.filter_r.process(sample_r + self.bb_r[pos] * data.feedback); 112 | self.bb_r[pos] = filtered_value_r; 113 | } 114 | } 115 | 116 | let mixed_sample_l = sample_l + sample_sum_l * data.level; 117 | let mixed_sample_r = sample_r + sample_sum_r * data.level; 118 | self.quant_pos = new_quant_pos; 119 | (mixed_sample_l, mixed_sample_r) 120 | } 121 | 122 | pub fn update(&mut self, data: &DelayData) { 123 | self.filter_l.update(data.tone); 124 | self.filter_r.update(data.tone); 125 | } 126 | 127 | pub fn update_bpm(&mut self, data: &mut DelayData, bpm: Float) { 128 | if data.sync == SyncValue::Off { 129 | return; 130 | } 131 | let num_sixteenths = match data.sync { 132 | SyncValue::Whole => 16.0, 133 | SyncValue::DottedHalf => 12.0, 134 | SyncValue::Half => 8.0, 135 | SyncValue::DottedQuarter => 6.0, 136 | SyncValue::Quarter => 4.0, 137 | SyncValue::DottedEigth => 3.0, 138 | SyncValue::Eigth => 2.0, 139 | SyncValue::Sixteenth => 1.0, 140 | SyncValue::Off => panic!(), 141 | }; 142 | let time = num_sixteenths / ((bpm * 4.0) / 60.0); 143 | data.time = if time < 0.01 { 144 | 0.01 145 | } else if time > 1.0 { 146 | 1.0 147 | } else { 148 | time 149 | } 150 | } 151 | 152 | fn add(mut value: usize, add: usize) -> usize { 153 | value += add as usize; 154 | while value >= BUFF_LEN { 155 | value -= BUFF_LEN; 156 | } 157 | value 158 | } 159 | 160 | fn addf(mut value: Float, add: Float) -> Float { 161 | value += add; 162 | while value >= BUFF_LEN as Float { 163 | value -= BUFF_LEN as Float ; 164 | } 165 | value 166 | } 167 | 168 | fn diff(a: usize, b: usize) -> usize { 169 | if a > b { a - b } else { (a + BUFF_LEN) - b} 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/synth/engine.rs: -------------------------------------------------------------------------------- 1 | extern crate cpal; 2 | extern crate failure; 3 | 4 | use super::UiMessage; 5 | use super::synth::Synth; 6 | 7 | use cpal::traits::{DeviceTrait, EventLoopTrait, HostTrait}; 8 | use crossbeam_channel::Sender; 9 | use log::error; 10 | 11 | use std::sync::{Arc, Mutex}; 12 | use std::time::SystemTime; 13 | 14 | pub struct Engine { 15 | sample_rate: u32, 16 | } 17 | 18 | impl Engine { 19 | pub fn new() -> Result { 20 | //Engine::enumerate(); 21 | let host = cpal::default_host(); 22 | println!("\r Chose host {:?}", host.id()); 23 | let device = host.default_output_device().expect("Failed to find a default output device"); 24 | println!("\r Chose device {:?}", device.name()); 25 | let result = device.default_output_format(); 26 | let format = match result { 27 | Ok(f) => f, 28 | Err(e) => { 29 | error!("Failed to query audio output format: {:?}", e); 30 | println!("Failed to query audio output format: {:?}", e); 31 | return Err(()); 32 | } 33 | }; 34 | let sample_rate = format.sample_rate.0; 35 | Ok(Engine{sample_rate}) 36 | } 37 | 38 | pub fn get_sample_rate(&self) -> u32 { 39 | self.sample_rate 40 | } 41 | 42 | pub fn run(&mut self, synth: Arc>, to_ui_sender: Sender) -> Result<(), ()> { 43 | let host = cpal::default_host(); 44 | let device = host.default_output_device().expect("failed to find a default output device"); 45 | let mut sample_clock = 0i64; 46 | let num_channels = 2; 47 | 48 | let event_loop = host.event_loop(); 49 | let result = device.default_output_format(); 50 | let mut format = match result { 51 | Ok(f) => f, 52 | Err(e) => { 53 | error!("Failed to query audio output format: {:?}", e); 54 | println!("Failed to query audio output format: {:?}", e); 55 | return Err(()); 56 | } 57 | }; 58 | if format.channels > num_channels as u16 { 59 | format.channels = num_channels as u16; 60 | } 61 | let stream_id = event_loop.build_output_stream(&device, &format).unwrap(); 62 | let mut time = SystemTime::now(); 63 | event_loop.play_stream(stream_id).unwrap(); 64 | 65 | let _handle = std::thread::spawn(move || { 66 | event_loop.run(move |id, result| { 67 | let data = match result { 68 | Ok(data) => data, 69 | Err(err) => { 70 | eprintln!("an error occurred on stream {:?}: {}", id, err); 71 | return; 72 | } 73 | }; 74 | if let cpal::StreamData::Output { buffer: cpal::UnknownTypeOutputBuffer::F32(mut buffer) } = data { 75 | let locked_synth = &mut synth.lock().unwrap(); 76 | 77 | let idle = time.elapsed().expect("Went back in time"); 78 | time = SystemTime::now(); 79 | 80 | for sample in buffer.chunks_mut(num_channels) { 81 | sample_clock += 1; 82 | let (left, right) = locked_synth.get_sample(sample_clock); 83 | sample[0] = left as f32; 84 | sample[1] = right as f32; 85 | } 86 | 87 | let busy = time.elapsed().expect("Went back in time"); 88 | time = SystemTime::now(); 89 | to_ui_sender.send(UiMessage::EngineSync(idle, busy)).unwrap(); 90 | 91 | locked_synth.update(); // Update the state of the synth voices 92 | } 93 | }); 94 | }); 95 | Ok(()) 96 | } 97 | 98 | /* 99 | fn enumerate() { 100 | println!("\rSupported hosts:\n\r {:?}", cpal::ALL_HOSTS); 101 | let available_hosts = cpal::available_hosts(); 102 | println!("\rAvailable hosts:\n\r {:?}", available_hosts); 103 | 104 | for host_id in available_hosts { 105 | println!("\r{:?}", host_id); 106 | let host = cpal::host_from_id(host_id).unwrap(); 107 | let default_in = host.default_input_device().map(|e| e.name().unwrap()); 108 | let default_out = match host.default_output_device().map(|e| e.name()) { 109 | Some(n) => match n { 110 | Ok(s) => s, 111 | Err(_) => "".to_string(), 112 | } 113 | None => "".to_string(), 114 | }; 115 | println!("\r Default Input Device:\n\r {:?}", default_in); 116 | println!("\r Default Output Device:\n\r {:?}", default_out); 117 | 118 | let devices = host.devices().unwrap(); 119 | println!("\r Devices: "); 120 | for (device_index, device) in devices.enumerate() { 121 | let name = match device.name() { 122 | Ok(n) => n, 123 | Err(_) => "Unknown".to_string(), 124 | }; 125 | println!("\r {}. \"{}\"", device_index + 1, name); 126 | 127 | // Input formats 128 | if let Ok(fmt) = device.default_input_format() { 129 | println!("\r Default input stream format:\n\r {:?}", fmt); 130 | } 131 | let mut input_formats = match device.supported_input_formats() { 132 | Ok(f) => f.peekable(), 133 | Err(e) => { 134 | println!("Error: {:?}", e); 135 | continue; 136 | }, 137 | }; 138 | if input_formats.peek().is_some() { 139 | println!("\r All supported input stream formats:"); 140 | for (format_index, format) in input_formats.enumerate() { 141 | println!("\r {}.{}. {:?}", device_index + 1, format_index + 1, format); 142 | } 143 | } 144 | 145 | // Output formats 146 | if let Ok(fmt) = device.default_output_format() { 147 | println!("\r Default output stream format:\n\r {:?}", fmt); 148 | } 149 | let mut output_formats = match device.supported_output_formats() { 150 | Ok(f) => f.peekable(), 151 | Err(e) => { 152 | println!("Error: {:?}", e); 153 | continue; 154 | }, 155 | }; 156 | if output_formats.peek().is_some() { 157 | println!("\r All supported output stream formats:"); 158 | for (format_index, format) in output_formats.enumerate() { 159 | println!("\r {}.{}. {:?}", device_index + 1, format_index + 1, format); 160 | } 161 | } 162 | } 163 | } 164 | } 165 | */ 166 | } 167 | 168 | -------------------------------------------------------------------------------- /src/synth/filter/filter.rs: -------------------------------------------------------------------------------- 1 | use crate::Float; 2 | use super::korg35::K35; 3 | use super::ober_moog::OberMoog; 4 | use super::sem::SEM; 5 | 6 | use serde::{Serialize, Deserialize}; 7 | 8 | #[derive(Debug)] 9 | pub enum FilterType { 10 | LPF1, // 1-pole low pass filter 11 | LPF2, // 2-pole low pass filter 12 | LPF4, // 4-pole low pass filter 13 | BPF2, // 2-pole band pass filter 14 | BPF4, // 4-pole band pass filter 15 | BSF2, // 2-pole band stop filter 16 | HPF1, // 1-pole high pass filter 17 | HPF2, // 2-pole high pass filter 18 | HPF4, // 4-pole high pass filter 19 | } 20 | 21 | #[derive(Serialize, Deserialize, Copy, Clone, Default, Debug)] 22 | pub struct FilterData { 23 | pub filter_type: usize, 24 | pub cutoff: Float, 25 | pub resonance: Float, 26 | pub gain: Float, 27 | pub aux: Float, // General purpose control, usage is filter dependent 28 | pub env_depth: Float, // Depth of Envevlope 2 cutoff modulation 29 | pub key_follow: i64, 30 | } 31 | 32 | impl FilterData { 33 | pub fn init(&mut self) { 34 | self.filter_type = 1; 35 | self.cutoff = 3000.0; 36 | self.resonance = 0.0; 37 | self.gain = 0.0; 38 | self.aux = 0.0; 39 | self.env_depth = 0.0; 40 | self.key_follow = 0; 41 | } 42 | } 43 | 44 | pub struct Filter { 45 | last_cutoff: Float, 46 | last_resonance: Float, 47 | 48 | sem_lpf: SEM, 49 | sem_bpf: SEM, 50 | sem_hpf: SEM, 51 | sem_bsf: SEM, 52 | k35_lpf: K35, 53 | k35_hpf: K35, 54 | om_lpf: OberMoog, 55 | om_bpf: OberMoog, 56 | om_hpf: OberMoog, 57 | } 58 | 59 | impl Filter { 60 | pub fn new(sample_rate: u32) -> Filter { 61 | let sample_rate: Float = sample_rate as Float; 62 | Filter{last_cutoff: 0.0, 63 | last_resonance: 0.0, 64 | sem_lpf: SEM::new(sample_rate, FilterType::LPF2), 65 | sem_bpf: SEM::new(sample_rate, FilterType::BPF2), 66 | sem_hpf: SEM::new(sample_rate, FilterType::HPF2), 67 | sem_bsf: SEM::new(sample_rate, FilterType::BSF2), 68 | k35_lpf: K35::new(sample_rate, FilterType::LPF2), 69 | k35_hpf: K35::new(sample_rate, FilterType::HPF2), 70 | om_lpf: OberMoog::new(sample_rate, FilterType::LPF4), 71 | om_bpf: OberMoog::new(sample_rate, FilterType::BPF4), 72 | om_hpf: OberMoog::new(sample_rate, FilterType::HPF4), 73 | } 74 | } 75 | 76 | pub fn reset(&mut self) { 77 | self.sem_lpf.reset(); 78 | self.sem_bpf.reset(); 79 | self.sem_hpf.reset(); 80 | self.sem_bsf.reset(); 81 | self.k35_lpf.reset(); 82 | self.k35_hpf.reset(); 83 | self.om_lpf.reset(); 84 | self.om_bpf.reset(); 85 | self.om_hpf.reset(); 86 | } 87 | 88 | pub fn process(&mut self, sample: Float, data: &mut FilterData, freq: Float, fmod: Float) -> Float { 89 | 90 | // Calculate effective cutoff frequency 91 | let mut cutoff = data.cutoff; 92 | if data.key_follow == 1 { 93 | cutoff = freq * (cutoff / 440.0); 94 | if cutoff > 8000.0 { 95 | cutoff = 8000.0; 96 | } else if cutoff < 1.0 { 97 | cutoff = 1.0; 98 | } 99 | } 100 | 101 | // Apply filter envelope 102 | if data.env_depth > 0.0 { 103 | cutoff *= fmod * data.env_depth; 104 | } 105 | 106 | // If a parameter changed, update coefficients 107 | if cutoff != self.last_cutoff || data.resonance != self.last_resonance { 108 | self.update(data, cutoff); 109 | } 110 | 111 | // Run the sample through the filter 112 | match data.filter_type { 113 | 0 => sample, // Bypass 114 | 1 => self.sem_lpf.process(sample, data), 115 | 2 => self.sem_bpf.process(sample, data), 116 | 3 => self.sem_hpf.process(sample, data), 117 | 4 => self.sem_bsf.process(sample, data), 118 | 5 => self.k35_lpf.process(sample, data), 119 | 6 => self.k35_hpf.process(sample, data), 120 | 7 => self.om_lpf.process(sample, data), 121 | 8 => self.om_bpf.process(sample, data), 122 | 9 => self.om_hpf.process(sample, data), 123 | _ => panic!(), 124 | } 125 | } 126 | 127 | // Called if cutoff or resonance have changed 128 | pub fn update(&mut self, data: &FilterData, cutoff: Float) { 129 | match data.filter_type { 130 | 0 => (), 131 | 1 => self.sem_lpf.update(data, cutoff), 132 | 2 => self.sem_bpf.update(data, cutoff), 133 | 3 => self.sem_hpf.update(data, cutoff), 134 | 4 => self.sem_bsf.update(data, cutoff), 135 | 5 => self.k35_lpf.update(data, cutoff), 136 | 6 => self.k35_hpf.update(data, cutoff), 137 | 7 => self.om_lpf.update(data, cutoff), 138 | 8 => self.om_bpf.update(data, cutoff), 139 | 9 => self.om_hpf.update(data, cutoff), 140 | _ => panic!(), 141 | } 142 | self.last_resonance = data.resonance; 143 | self.last_cutoff = cutoff; 144 | } 145 | 146 | // --------- 147 | // Utilities 148 | // --------- 149 | 150 | /* 151 | * These functions were used in the old filter models. 152 | 153 | // TODO: Switch to faster version 154 | pub fn max(a: Float, b: Float) -> Float { 155 | if a >= b { a } else { b } 156 | } 157 | 158 | // Pull values close to zero down to zero 159 | pub fn normalize(value: Float) -> Float { 160 | if value >= 0.0 { 161 | if value > 1e-15 as Float && value < 1e15 as Float { 162 | value 163 | } else { 164 | 0.0 165 | } 166 | } else { 167 | if value < -1e-15 as Float && value > -1e15 as Float { 168 | value 169 | } else { 170 | 0.0 171 | } 172 | } 173 | } 174 | */ 175 | } 176 | -------------------------------------------------------------------------------- /src/synth/filter/korg35.rs: -------------------------------------------------------------------------------- 1 | //! Based on the Sallen-Key filter used in the Korg35, as presented in the book 2 | //! "Designing Software Synthesizer Plug-Ins in C++" by Will Pirkle. 3 | 4 | use crate::Float; 5 | use super::{FilterData, FilterType, VAOnePole}; 6 | 7 | /// Sallen-Key filter as used in the Korg35 8 | pub struct K35 { 9 | sample_rate: Float, 10 | filter_type: FilterType, 11 | 12 | lpf1: VAOnePole, 13 | lpf2: VAOnePole, 14 | hpf1: VAOnePole, 15 | hpf2: VAOnePole, 16 | k: Float, 17 | alpha0: Float, 18 | } 19 | 20 | impl K35 { 21 | pub fn new(sample_rate: Float, filter_type: FilterType) -> Self { 22 | K35{sample_rate, 23 | filter_type, 24 | lpf1: VAOnePole::new(sample_rate, FilterType::LPF1), 25 | lpf2: VAOnePole::new(sample_rate, FilterType::LPF1), 26 | hpf1: VAOnePole::new(sample_rate, FilterType::HPF1), 27 | hpf2: VAOnePole::new(sample_rate, FilterType::HPF1), 28 | k: 0.01, 29 | alpha0: 0.0, 30 | } 31 | } 32 | 33 | pub fn reset(&mut self) { 34 | self.lpf1.reset(); 35 | self.lpf2.reset(); 36 | self.hpf1.reset(); 37 | self.hpf2.reset(); 38 | } 39 | 40 | pub fn update(&mut self, data: &FilterData, freq: Float) { 41 | // Map resonance from [0.0, 1.0] to the range [0.01, 2.0] 42 | self.k = (2.0 - 0.01) * data.resonance + 0.01; 43 | 44 | let wd = (std::f64::consts::PI * 2.0) * freq; 45 | let t = 1.0 / self.sample_rate; 46 | let wa = (2.0 / t) * (wd * t / 2.0).tan(); 47 | let g = wa * t / 2.0; 48 | let gg = g / (1.0 + g); 49 | self.lpf1.set_alpha(gg); 50 | self.lpf2.set_alpha(gg); 51 | self.hpf1.set_alpha(gg); 52 | self.hpf2.set_alpha(gg); 53 | self.alpha0 = 1.0 / (1.0 - self.k * gg + self.k * gg * gg); 54 | match self.filter_type { 55 | FilterType::LPF2 => { 56 | self.lpf2.set_beta((self.k - self.k * gg) / (1.0 + g)); 57 | self.hpf1.set_beta(-1.0 / (1.0 + g)); 58 | } 59 | FilterType::HPF2 => { 60 | self.hpf2.set_beta(-1.0 * gg / (1.0 + g)); 61 | self.lpf1.set_beta(1.0 / (1.0 + g)); 62 | } 63 | _ => panic!(), 64 | } 65 | } 66 | 67 | pub fn process(&mut self, s: Float, data: &FilterData) -> Float { 68 | let mut y: Float; 69 | 70 | match self.filter_type { 71 | FilterType::LPF2 => { 72 | let y1 = self.lpf1.process(s); 73 | let s35 = self.hpf1.get_feedback_output() + 74 | self.lpf2.get_feedback_output(); 75 | let mut u = self.alpha0 * (y1 + s35); 76 | if data.gain > 0.0 { 77 | u = (data.gain * u).tanh(); 78 | } 79 | y = self.k * self.lpf2.process(u); 80 | self.hpf1.process(y); 81 | } 82 | FilterType::HPF2 => { 83 | let y1 = self.hpf1.process(s); 84 | let s35 = self.hpf2.get_feedback_output() + 85 | self.lpf1.get_feedback_output(); 86 | let u = self.alpha0 * y1 + s35; 87 | y = self.k * u; 88 | if data.gain > 0.0 { 89 | y = (data.gain * y).tanh(); 90 | } 91 | self.lpf1.process(self.hpf2.process(y)); 92 | } 93 | _ => panic!(), 94 | } 95 | if self.k > 0.0 { 96 | y *= 1.0 / self.k; 97 | } 98 | y 99 | } 100 | } 101 | 102 | -------------------------------------------------------------------------------- /src/synth/filter/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod filter; 2 | pub mod onepole; 3 | 4 | mod korg35; 5 | mod ober_moog; 6 | mod sem; 7 | mod va_onepole; 8 | //mod moog_improved; 9 | //mod reson_z; 10 | //mod rlpf; 11 | 12 | pub use filter::{Filter, FilterData, FilterType}; 13 | pub use onepole::OnePole; 14 | 15 | use va_onepole::VAOnePole; 16 | -------------------------------------------------------------------------------- /src/synth/filter/moog_improved.rs: -------------------------------------------------------------------------------- 1 | // Ported from C++ code at https://github.com/ddiakopoulos/MoogLadders/ 2 | /* 3 | Copyright 2012 Stefano D'Angelo 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | */ 17 | 18 | /* 19 | This model is based on a reference implementation of an algorithm developed by 20 | Stefano D'Angelo and Vesa Valimaki, presented in a paper published at ICASSP in 2013. 21 | This improved model is based on a circuit analysis and compared against a reference 22 | Ngspice simulation. In the paper, it is noted that this particular model is 23 | more accurate in preserving the self-oscillating nature of the real filter. 24 | References: "An Improved Virtual Analog Model of the Moog Ladder Filter" 25 | Original Implementation: D'Angelo, Valimaki 26 | */ 27 | 28 | use crate::Float; 29 | use super::FilterData; 30 | 31 | pub struct MoogImproved { 32 | sample_rate: Float, 33 | double_sample_rate: Float, 34 | resonance: Float, // Scaled from [0.0, 1.0] to [0.0, 4.0] 35 | v: [Float; 4], 36 | dv: [Float; 4], 37 | tv: [Float; 4], 38 | x: Float, 39 | g: Float, 40 | } 41 | 42 | impl MoogImproved { 43 | pub fn new(sample_rate: Float) -> Self { 44 | MoogImproved{ 45 | sample_rate: sample_rate, 46 | double_sample_rate: sample_rate * 2.0, 47 | resonance: 1.0, 48 | v: [0.0; 4], 49 | dv: [0.0; 4], 50 | tv: [0.0; 4], 51 | x: 0.0, 52 | g: 0.0, 53 | } 54 | } 55 | 56 | pub fn reset(&mut self) { 57 | for v in &mut self.v { 58 | *v = 0.0; 59 | } 60 | for dv in &mut self.dv { 61 | *dv = 0.0; 62 | } 63 | for tv in &mut self.tv { 64 | *tv = 0.0; 65 | } 66 | self.x = 0.0; 67 | self.g = 0.0; 68 | } 69 | 70 | pub fn process(&mut self, sample: Float, data: &FilterData) -> Float { 71 | 72 | // Thermal voltage (26 milliwats at room temperature) 73 | const VT: Float = 0.312; 74 | let dv0: Float; 75 | let dv1: Float; 76 | let dv2: Float; 77 | let dv3: Float; 78 | 79 | dv0 = -self.g * (((data.gain * sample + self.resonance * self.v[3]) / (2.0 * VT)).tanh() + self.tv[0]); 80 | self.v[0] += (dv0 + self.dv[0]) / self.double_sample_rate; 81 | self.dv[0] = dv0; 82 | self.tv[0] = (self.v[0] / (2.0 * VT)).tanh(); 83 | 84 | dv1 = self.g * (self.tv[0] - self.tv[1]); 85 | self.v[1] += (dv1 + self.dv[1]) / self.double_sample_rate; 86 | self.dv[1] = dv1; 87 | self.tv[1] = (self.v[1] / (2.0 * VT)).tanh(); 88 | 89 | dv2 = self.g * (self.tv[1] - self.tv[2]); 90 | self.v[2] += (dv2 + self.dv[2]) / self.double_sample_rate; 91 | self.dv[2] = dv2; 92 | self.tv[2] = (self.v[2] / (2.0 * VT)).tanh(); 93 | 94 | dv3 = self.g * (self.tv[2] - self.tv[3]); 95 | self.v[3] += (dv3 + self.dv[3]) / self.double_sample_rate; 96 | self.dv[3] = dv3; 97 | self.tv[3] = (self.v[3] / (2.0 * VT)).tanh(); 98 | 99 | self.v[3] 100 | } 101 | 102 | pub fn update(&mut self, data: &FilterData, freq: Float) { 103 | const VT: Float = 0.312; 104 | self.x = (std::f64::consts::PI * freq) / self.sample_rate; 105 | self.g = 4.0 * std::f64::consts::PI * VT * freq * (1.0 - self.x) / (1.0 + self.x); 106 | self.resonance = data.resonance * 4.0; 107 | } 108 | } 109 | 110 | -------------------------------------------------------------------------------- /src/synth/filter/ober_moog.rs: -------------------------------------------------------------------------------- 1 | //! Based on the Moog ladder filter with Oberheim variations, as presented in 2 | //! the book "Designing Software Synthesizer Plug-Ins in C++" by Will Pirkle. 3 | 4 | use crate::Float; 5 | use super::{FilterData, FilterType}; 6 | use super::VAOnePole; 7 | 8 | pub struct OberMoog { 9 | sample_rate: Float, 10 | filter_type: FilterType, 11 | 12 | lpf1: VAOnePole, 13 | lpf2: VAOnePole, 14 | lpf3: VAOnePole, 15 | lpf4: VAOnePole, 16 | 17 | k: Float, 18 | alpha0: Float, 19 | gamma: Float, 20 | oberheim_coefs: [Float; 5], 21 | } 22 | 23 | impl OberMoog { 24 | 25 | pub fn new(sample_rate: Float, filter_type: FilterType) -> Self { 26 | OberMoog{ 27 | sample_rate, 28 | filter_type, 29 | lpf1: VAOnePole::new(sample_rate, FilterType::LPF1), 30 | lpf2: VAOnePole::new(sample_rate, FilterType::LPF1), 31 | lpf3: VAOnePole::new(sample_rate, FilterType::LPF1), 32 | lpf4: VAOnePole::new(sample_rate, FilterType::LPF1), 33 | k: 0.0, 34 | alpha0: 0.0, 35 | gamma: 0.0, 36 | oberheim_coefs: [0.0, 0.0, 0.0, 0.0, 0.0] 37 | } 38 | } 39 | 40 | pub fn reset(&mut self) { 41 | self.lpf1.reset(); 42 | self.lpf2.reset(); 43 | self.lpf3.reset(); 44 | self.lpf4.reset(); 45 | } 46 | 47 | pub fn update(&mut self, data: &FilterData, freq: Float) { 48 | // Map resonance from [0.0, 1.0] to the range [0.0, 4.0] 49 | self.k = 4.0 * data.resonance; 50 | 51 | // prewarp for BZT 52 | let wd = 2.0 * std::f64::consts::PI * freq; 53 | let t = 1.0 / self.sample_rate; 54 | let wa = (2.0 / t) * (wd * t / 2.0).tan(); 55 | let g = wa * t / 2.0; 56 | 57 | // Feedforward coeff 58 | let gg = g / (1.0 + g); 59 | 60 | self.lpf1.set_alpha(gg); 61 | self.lpf2.set_alpha(gg); 62 | self.lpf3.set_alpha(gg); 63 | self.lpf4.set_alpha(gg); 64 | 65 | self.lpf1.set_beta(gg * gg * gg / (1.0 + g)); 66 | self.lpf2.set_beta(gg * gg / (1.0 + g)); 67 | self.lpf3.set_beta(gg / (1.0 + g)); 68 | self.lpf4.set_beta(1.0 / (1.0 + g)); 69 | 70 | self.gamma = gg * gg * gg * gg; 71 | self.alpha0 = 1.0 / (1.0 + self.k * self.gamma); 72 | 73 | // Oberheim variations / LPF4 74 | match self.filter_type { 75 | FilterType::LPF4 => { 76 | self.oberheim_coefs[0] = 0.0; 77 | self.oberheim_coefs[1] = 0.0; 78 | self.oberheim_coefs[2] = 0.0; 79 | self.oberheim_coefs[3] = 0.0; 80 | self.oberheim_coefs[4] = 1.0; 81 | } 82 | FilterType::LPF2 => { 83 | self.oberheim_coefs[0] = 0.0; 84 | self.oberheim_coefs[1] = 0.0; 85 | self.oberheim_coefs[2] = 1.0; 86 | self.oberheim_coefs[3] = 0.0; 87 | self.oberheim_coefs[4] = 0.0; 88 | } 89 | FilterType::BPF4 => { 90 | self.oberheim_coefs[0] = 0.0; 91 | self.oberheim_coefs[1] = 0.0; 92 | self.oberheim_coefs[2] = 4.0; 93 | self.oberheim_coefs[3] = -8.0; 94 | self.oberheim_coefs[4] = 4.0; 95 | } 96 | FilterType::BPF2 => { 97 | self.oberheim_coefs[0] = 0.0; 98 | self.oberheim_coefs[1] = 2.0; 99 | self.oberheim_coefs[2] = -2.0; 100 | self.oberheim_coefs[3] = 0.0; 101 | self.oberheim_coefs[4] = 0.0; 102 | } 103 | FilterType::HPF4 => { 104 | self.oberheim_coefs[0] = 1.0; 105 | self.oberheim_coefs[1] = -4.0; 106 | self.oberheim_coefs[2] = 6.0; 107 | self.oberheim_coefs[3] = -4.0; 108 | self.oberheim_coefs[4] = 1.0; 109 | } 110 | FilterType::HPF2 => { 111 | self.oberheim_coefs[0] = 1.0; 112 | self.oberheim_coefs[1] = -2.0; 113 | self.oberheim_coefs[2] = 1.0; 114 | self.oberheim_coefs[3] = 0.0; 115 | self.oberheim_coefs[4] = 0.0; 116 | } 117 | _ => panic!(), 118 | } 119 | } 120 | 121 | pub fn process(&mut self, s: Float, data: &FilterData) -> Float { 122 | let input = s; 123 | 124 | let sigma = self.lpf1.get_feedback_output() + 125 | self.lpf2.get_feedback_output() + 126 | self.lpf3.get_feedback_output() + 127 | self.lpf4.get_feedback_output(); 128 | 129 | // For passband gain compensation 130 | //input *= 1.0 + data.aux * self.k; 131 | 132 | // calculate input to first filter 133 | let mut u = (input - self.k * sigma) * self.alpha0; 134 | 135 | if data.gain > 0.0 { 136 | u = (data.gain * u).tanh(); 137 | } 138 | 139 | let lp1 = self.lpf1.process(u); 140 | let lp2 = self.lpf2.process(lp1); 141 | let lp3 = self.lpf3.process(lp2); 142 | let lp4 = self.lpf4.process(lp3); 143 | 144 | // Calculate result (Oberheim variation) 145 | self.oberheim_coefs[0] * u + 146 | self.oberheim_coefs[1] * lp1 + 147 | self.oberheim_coefs[2] * lp2 + 148 | self.oberheim_coefs[3] * lp3 + 149 | self.oberheim_coefs[4] * lp4 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/synth/filter/oberheim.rs: -------------------------------------------------------------------------------- 1 | use super::VAOnePole; 2 | 3 | struct Oberheim { 4 | lpf1: VAOnePole, 5 | lpf2: VAOnePole, 6 | lpf3: VAOnePole, 7 | lpf4: VAOnePole, 8 | 9 | K: Float, 10 | gamma: Float, 11 | alpha0: Float, 12 | Q: Float, 13 | saturation: Float, 14 | oberheimCoefs: [Float; 5], 15 | } 16 | 17 | impl Oberheim { 18 | // Oberheim 19 | 20 | pub fn new(sample_rate: Float) -> Self { 21 | Oberheim{ 22 | lpf1: VAOnePole::new(sample_rate), 23 | lpf2: VAOnePole::new(sample_rate), 24 | lpf3: VAOnePole::new(sample_rate), 25 | lpf4: VAOnePole::new(sample_rate), 26 | K: 0.0, 27 | gamma: 0.0, 28 | alpha0: 0.0, 29 | Q: 3.0, 30 | saturation: 1.0, 31 | oberheimCoefs: [0.0, 0.0, 0.0, 0.0, 0.0] 32 | } 33 | // SetCutoff(1000.f); 34 | // SetResonance(0.1f); 35 | } 36 | 37 | pub fn process(&mut self, sample: Float, data: &FilterData) -> Float { 38 | let mut input = sample; 39 | 40 | double sigma = 41 | self.lpf1->get_feedback_output() + 42 | self.lpf2->get_feedback_output() + 43 | self.lpf3->get_feedback_output() + 44 | self.lpf4->get_feedback_output(); 45 | 46 | input *= 1.0 + self.K; 47 | 48 | // calculate input to first filter 49 | let mut u: Float = (input - self.K * self.sigma) * self.alpha0; 50 | 51 | u = (self.saturation * u).tanh(); 52 | 53 | stage1 = lpf1.process(u); 54 | stage2 = lpf2.process(stage1); 55 | stage3 = lpf3.process(stage2); 56 | stage4 = lpf4.process(stage3); 57 | 58 | // Oberheim variations 59 | // TODO: Optimize this if the coeffs are mostly 0 anyways 60 | oberheimCoefs[0] * u + 61 | oberheimCoefs[1] * stage1 + 62 | oberheimCoefs[2] * stage2 + 63 | oberheimCoefs[3] * stage3 + 64 | oberheimCoefs[4] * stage4; 65 | } 66 | 67 | pub fn update(&mut self, data: &FilterData, freq: Float) { 68 | self.set_resonance(data.resonance); 69 | self.set_cutoff(freq); 70 | } 71 | 72 | fn set_resonance(&mut self, r: Float) { 73 | // TODO: Adjust for Yazz range 74 | // this maps resonance = 1->10 to K = 0 -> 4 75 | self.K = (4.0) * (r - 1.0)/(10.0 - 1.0); 76 | } 77 | 78 | fn set_cutoff(&mut self, c: Float) { 79 | let cutoff = c; 80 | 81 | // prewarp for BZT 82 | let wd = 2.0 * std::F64::PI * cutoff; 83 | let T = 1.0 / self.sample_rate; 84 | let wa = (2.0 / T) * tan(wd * T / 2.0); 85 | let g = wa * T / 2.0; 86 | 87 | // Feedforward coeff 88 | let G = g / (1.0 + g); 89 | 90 | self.lpf1->set_alpha(G); 91 | self.lpf2->set_alpha(G); 92 | self.lpf3->set_alpha(G); 93 | self.lpf4->set_alpha(G); 94 | 95 | self.lpf1->set_beta(G * G * G / (1.0 + g)); 96 | self.lpf2->set_beta(G * G / (1.0 + g)); 97 | self.lpf3->set_beta(G / (1.0 + g)); 98 | self.lpf4->set_beta(1.0 / (1.0 + g)); 99 | 100 | self.gamma = G * G * G * G; 101 | self.alpha0 = 1.0 / (1.0 + self.K * self.gamma); 102 | 103 | // Oberheim variations / LPF4 104 | self.oberheimCoefs[0] = 0.0; 105 | self.oberheimCoefs[1] = 0.0; 106 | self.oberheimCoefs[2] = 0.0; 107 | self.oberheimCoefs[3] = 0.0; 108 | self.oberheimCoefs[4] = 1.0; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/synth/filter/onepole.rs: -------------------------------------------------------------------------------- 1 | use crate::Float; 2 | 3 | /* Taken from https://www.earlevel.com/main/2012/12/15/a-one-pole-filter/ */ 4 | pub struct OnePole { 5 | sample_rate: Float, 6 | a0: Float, 7 | b1: Float, 8 | z1: Float 9 | } 10 | 11 | impl OnePole { 12 | pub fn new(sample_rate: u32) -> OnePole { 13 | OnePole{sample_rate: sample_rate as Float, a0: 0.0, b1: 0.0, z1: 0.0} 14 | } 15 | 16 | pub fn update(&mut self, cutoff: Float) { 17 | let freq = cutoff / self.sample_rate; 18 | self.b1 = (-2.0 * std::f64::consts::PI * freq).exp(); 19 | self.a0 = 1.0 - self.b1; 20 | } 21 | 22 | pub fn process(&mut self, sample: Float) -> Float { 23 | self.z1 = sample * self.a0 + self.z1 * self.b1; 24 | self.z1 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/synth/filter/reson_z.rs: -------------------------------------------------------------------------------- 1 | // Adapted from SC3's ResonZ 2 | 3 | use crate::Float; 4 | use super::{Filter, FilterData}; 5 | 6 | pub struct ResonZ { 7 | sample_rate: Float, 8 | radians_per_sample: Float, 9 | reso_factor: Float, 10 | reso_offset: Float, 11 | 12 | y1: Float, 13 | y2: Float, 14 | a0: Float, 15 | b1: Float, 16 | b2: Float, 17 | } 18 | 19 | impl ResonZ { 20 | pub fn new(sample_rate: Float) -> ResonZ { 21 | ResonZ{sample_rate: sample_rate, 22 | radians_per_sample: (std::f64::consts::PI * 2.0) / sample_rate, 23 | reso_factor: 1.0 / (5.0 - 1.0), 24 | reso_offset: 1.0, 25 | y1: 0.0, y2: 0.0, a0: 0.0, b1: 0.0, b2: 0.0,} 26 | } 27 | 28 | pub fn reset(&mut self) { 29 | self.y1 = 0.0; 30 | self.y2 = 0.0; 31 | self.a0 = 0.0; 32 | self.b1 = 0.0; 33 | self.b2 = 0.0; 34 | } 35 | 36 | pub fn process(&mut self, sample: Float) -> Float { 37 | let y0 = sample + self.b1 * self.y1 + self.b2 * self.y2; 38 | let result = self.a0 * (y0 - self.y2); 39 | self.y2 = Filter::normalize(self.y1); 40 | self.y1 = Filter::normalize(y0); 41 | result 42 | } 43 | 44 | pub fn update(&mut self, data: &FilterData, freq: Float) { 45 | let resonance = data.resonance * self.reso_factor + self.reso_offset; 46 | let pfreq = freq * self.radians_per_sample; 47 | let b = pfreq / resonance; 48 | let r = 1.0 - b * 0.5; 49 | let r2 = 2.0 * r; 50 | let r22 = r * r; 51 | let cost = (r2 * f64::cos(pfreq)) / (1.0 + r22); 52 | let next_b1 = r2 * cost; 53 | let next_b2 = -r22; 54 | let next_a0 = (1.0 - r22) * 0.5; 55 | 56 | self.a0 = next_a0; 57 | self.b1 = next_b1; 58 | self.b2 = next_b2; 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /src/synth/filter/rlpf.rs: -------------------------------------------------------------------------------- 1 | // Adapated from SC3's RLPF 2 | 3 | use super::{Filter, FilterData}; 4 | use crate::Float; 5 | 6 | pub struct Rlpf { 7 | sample_rate: Float, 8 | radians_per_sample: Float, 9 | reso_factor: Float, 10 | reso_offset: Float, 11 | 12 | y1: Float, 13 | y2: Float, 14 | a0: Float, 15 | b1: Float, 16 | b2: Float, 17 | } 18 | 19 | impl Rlpf { 20 | pub fn new(sample_rate: Float) -> Self { 21 | Rlpf{sample_rate: sample_rate, 22 | radians_per_sample: (std::f64::consts::PI * 2.0) / sample_rate, 23 | reso_factor: 2000.0 - 30.0, // Filter bandwidth (Q) 24 | reso_offset: 30.0, // Minimum Q to avoid distortion 25 | y1: 0.0, y2: 0.0, a0: 0.0, b1: 0.0, b2: 0.0} 26 | } 27 | 28 | pub fn reset(&mut self) { 29 | self.y1 = 0.0; 30 | self.y2 = 0.0; 31 | self.a0 = 0.0; 32 | self.b1 = 0.0; 33 | self.b2 = 0.0; 34 | } 35 | 36 | pub fn process(&mut self, sample: Float) -> Float { 37 | let y0 = self.a0 * sample + self.b1 * self.y1 + self.b2 * self.y2; 38 | let result = y0 + 2.0 * self.y1 + self.y2; 39 | self.y2 = Filter::normalize(self.y1); 40 | self.y1 = Filter::normalize(y0); 41 | result 42 | } 43 | 44 | pub fn update(&mut self, data: &FilterData, freq: Float) { 45 | let q = (1.0 - data.resonance) * self.reso_factor + self.reso_offset; 46 | 47 | // According to SC3 docs, this should be reciprocal of Q, so 48 | // bandwidth / cutoff. That seems to reduce the resonance for 49 | // lower frequencies. 50 | //let resonance = q / freq; 51 | let resonance = q / 2000.0; 52 | 53 | let qres = Filter::max(0.001, resonance); 54 | let pfreq = freq * self.radians_per_sample; 55 | 56 | let d = f64::tan(pfreq * qres * 0.5); 57 | let c = (1.0 - d) / (1.0 + d); 58 | let cosf = f64::cos(pfreq); 59 | let next_b1 = (1.0 + c) * cosf; 60 | let next_b2 = -c; 61 | let next_a0 = (1.0 + c - next_b1) * 0.25; 62 | 63 | self.a0 = next_a0; 64 | self.b1 = next_b1; 65 | self.b2 = next_b2; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/synth/filter/sem.rs: -------------------------------------------------------------------------------- 1 | use crate::Float; 2 | use super::{FilterData, FilterType}; 3 | 4 | // One pole filter used to construct Oberheim Moog ladder filter 5 | pub struct SEM { 6 | sample_rate: Float, 7 | filter_type: FilterType, 8 | resonance: Float, 9 | 10 | alpha: Float, 11 | alpha0: Float, 12 | rho: Float, 13 | z11: Float, 14 | z12: Float, 15 | } 16 | 17 | impl SEM { 18 | pub fn new(sample_rate: Float, filter_type: FilterType) -> Self { 19 | SEM{ 20 | sample_rate, 21 | filter_type, 22 | resonance: 0.5, 23 | alpha: 1.0, 24 | alpha0: 1.0, 25 | rho: 1.0, 26 | z11: 0.0, 27 | z12: 0.0 28 | } 29 | } 30 | 31 | pub fn reset(&mut self) { 32 | self.resonance = 0.5; 33 | self.alpha = 1.0; 34 | self.alpha0 = 1.0; 35 | self.rho = 1.0; 36 | self.z11 = 0.0; 37 | self.z12 = 0.0; 38 | } 39 | 40 | pub fn update(&mut self, data: &FilterData, freq: Float) { 41 | // Map resonance from [0.0, 1.0] to the range [0.5, 25] 42 | self.resonance = (25.0 - 0.5) * data.resonance + 0.5; 43 | 44 | let wd = (std::f64::consts::PI * 2.0) * freq; 45 | let t = 1.0 / self.sample_rate; 46 | let wa = (2.0 / t) * (wd * t / 2.0).tan(); 47 | let g = wa * t / 2.0; 48 | let r = 1.0 / (2.0 * self.resonance); 49 | 50 | self.alpha0 = 1.0 / (1.0 + (2.0 * r * g) + (g * g)); 51 | self.alpha = g; 52 | self.rho = 2.0 * r + g; 53 | } 54 | 55 | pub fn process(&mut self, s: Float, data: &FilterData) -> Float { 56 | let hpf = self.alpha0 * (s - self.rho * self.z11 - self.z12); 57 | let mut bpf = self.alpha * hpf + self.z11; 58 | if data.gain > 0.0 { 59 | bpf = (bpf + data.gain).tanh(); 60 | } 61 | let lpf = self.alpha * bpf + self.z12; 62 | let _sem_bsf = data.aux * hpf + (1.0 - data.aux) * lpf; // TODO: Lost something here when translating 63 | self.z11 = self.alpha * hpf + bpf; 64 | self.z12 = self.alpha * bpf + lpf; 65 | match self.filter_type { 66 | FilterType::LPF2 => lpf, 67 | FilterType::BPF2 => bpf, 68 | FilterType::HPF2 => hpf, 69 | FilterType::BSF2 => { 70 | let r = 1.0 / (2.0 * self.resonance); 71 | s - 2.0 * r * bpf 72 | } 73 | _ => panic!(), 74 | } 75 | } 76 | } 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/synth/filter/va_onepole.rs: -------------------------------------------------------------------------------- 1 | use crate::Float; 2 | //use super::FilterData; 3 | use super::FilterType; 4 | 5 | // One pole filter used to construct Oberheim Moog ladder filter 6 | pub struct VAOnePole { 7 | //sample_rate: Float, // Only needed if filter is used stand-alone 8 | filter_type: FilterType, 9 | alpha: Float, 10 | beta: Float, 11 | gamma: Float, 12 | delta: Float, 13 | epsilon: Float, 14 | a0: Float, 15 | feedback: Float, 16 | z1: Float, 17 | } 18 | 19 | impl VAOnePole { 20 | pub fn new(_sample_rate: Float, filter_type: FilterType) -> Self { 21 | VAOnePole{ 22 | //sample_rate: sample_rate, 23 | filter_type, 24 | alpha: 1.0, 25 | beta: 0.0, 26 | gamma: 1.0, 27 | delta: 0.0, 28 | epsilon: 0.0, 29 | a0: 1.0, 30 | feedback: 0.0, 31 | z1: 0.0} 32 | } 33 | 34 | pub fn reset(&mut self) { 35 | self.alpha = 1.0; 36 | self.beta = 0.0; 37 | self.gamma = 1.0; 38 | self.delta = 0.0; 39 | self.epsilon = 0.0; 40 | self.a0 = 1.0; 41 | self.feedback = 0.0; 42 | self.z1 = 0.0; 43 | } 44 | 45 | /* 46 | // Only needed if filter is used stand-alone 47 | pub fn update(&mut self, _data: &FilterData, freq: Float) { 48 | let wd = (std::f64::consts::PI * 2.0) * freq; 49 | let t = 1.0 / self.sample_rate; 50 | let wa = (2.0 / t) * (wd * t / 2.0).tan(); 51 | let g = wa * t / 2.0; 52 | self.alpha = g / (1.0 + g); 53 | } 54 | */ 55 | 56 | pub fn process(&mut self, s: Float) -> Float { 57 | let s = s * self.gamma + self.feedback + self.epsilon * self.get_feedback_output(); 58 | let vn = (self.a0 * s - self.z1) * self.alpha; 59 | let out = vn + self.z1; 60 | self.z1 = vn + out; 61 | match self.filter_type { 62 | FilterType::LPF1 => out, 63 | FilterType::HPF1 => s - out, 64 | _ => panic!(), 65 | } 66 | } 67 | 68 | //pub fn set_feedback(&mut self, fb: Float) { self.feedback = fb; } 69 | pub fn set_alpha(&mut self, a: Float) { self.alpha = a; } 70 | pub fn set_beta(&mut self, b: Float) { self.beta = b; } 71 | pub fn get_feedback_output(&self) -> Float { self.beta * (self.z1 + self.feedback * self.delta) } 72 | } 73 | 74 | -------------------------------------------------------------------------------- /src/synth/lfo.rs: -------------------------------------------------------------------------------- 1 | extern crate num; 2 | 3 | use super::{Float, SyncValue}; 4 | 5 | use serde::{Serialize, Deserialize}; 6 | 7 | #[derive(Serialize, Deserialize, Clone, Copy, Debug)] 8 | pub enum LfoWaveform { 9 | Sine, 10 | Tri, 11 | Saw, 12 | SawDown, 13 | Square, 14 | SnH, 15 | Noise, 16 | } 17 | 18 | impl Default for LfoWaveform { 19 | fn default() -> Self { LfoWaveform::Sine } 20 | } 21 | 22 | #[derive(Serialize, Deserialize, Clone, Copy, Debug, Default)] 23 | pub struct LfoData { 24 | pub waveform: LfoWaveform, 25 | pub frequency: Float, 26 | pub sync: SyncValue, 27 | pub phase: Float, 28 | pub amount: Float, 29 | } 30 | 31 | impl LfoData { 32 | pub fn init(&mut self) { 33 | self.select_wave(0); 34 | self.frequency = 1.0; 35 | self.phase = 0.0; 36 | self.amount = 1.0; 37 | } 38 | 39 | pub fn select_wave(&mut self, value: usize) { 40 | self.waveform = match value { 41 | 0 => LfoWaveform::Sine, 42 | 1 => LfoWaveform::Tri, 43 | 2 => LfoWaveform::Saw, 44 | 3 => LfoWaveform::SawDown, 45 | 4 => LfoWaveform::Square, 46 | 5 => LfoWaveform::SnH, 47 | 6 => LfoWaveform::Noise, 48 | _ => panic!(), 49 | } 50 | } 51 | 52 | pub fn get_waveform(&self) -> usize { 53 | match self.waveform { 54 | LfoWaveform::Sine => 0, 55 | LfoWaveform::Tri => 1, 56 | LfoWaveform::Saw => 2, 57 | LfoWaveform::SawDown => 3, 58 | LfoWaveform::Square => 4, 59 | LfoWaveform::SnH => 5, 60 | LfoWaveform::Noise => 6, 61 | } 62 | } 63 | } 64 | 65 | pub struct Lfo { 66 | pub sample_rate: u32, 67 | last_update: i64, // Time of last sample 68 | position: Float, // Position in the wave at last update, going from 0.0 to 1.0 69 | last_value: Float, // Value of previous iteration (only used for S&H) 70 | } 71 | 72 | impl Lfo { 73 | pub fn new(sample_rate: u32) -> Lfo { 74 | let last_update = 0; 75 | let position = 0.0; 76 | let last_value = 0.0; 77 | Lfo{sample_rate, last_update, position, last_value} 78 | } 79 | 80 | fn get_sample_sine(&mut self) -> Float { 81 | (self.position * 2.0 * std::f64::consts::PI).sin() 82 | } 83 | 84 | fn get_sample_triangle(&mut self) -> Float { 85 | /* This version allows for asymmetrical triangle, but we're not using 86 | * it at the moment. 87 | let rate_q1 = 2.0 / phase; 88 | let rate_q2 = 2.0 / (1.0 - phase); 89 | let mut pos = self.position + (phase / 2.0); 90 | if pos > 1.0 { pos -= 1.0 } 91 | if pos < phase { 92 | (pos * rate_q1) - 1.0 93 | } else { 94 | 1.0 - ((pos - phase) * rate_q2) 95 | } 96 | */ 97 | // Faster version 98 | 1.0 - 2.0 * (2.0 * self.position - 1.0).abs() 99 | } 100 | 101 | fn get_sample_saw_down(&mut self) -> Float { 102 | 1.0 - (self.position * 2.0) 103 | } 104 | 105 | fn get_sample_saw_up(&mut self) -> Float { 106 | (self.position * 2.0) - 1.0 107 | } 108 | 109 | fn get_sample_square(&mut self, phase: Float) -> Float { 110 | if self.position < phase { 111 | 1.0 112 | } else { 113 | -1.0 114 | } 115 | } 116 | 117 | fn get_sample_noise(&mut self) -> Float { 118 | (rand::random::() * 2.0) - 1.0 119 | } 120 | 121 | fn get_sample_snh(&mut self, get_new_value: bool) -> Float { 122 | if get_new_value { 123 | self.last_value = (rand::random::() * 2.0) - 1.0; 124 | } 125 | self.last_value 126 | } 127 | 128 | pub fn get_sample(&mut self, sample_clock: i64, data: &LfoData, reset: bool) -> (Float, bool) { 129 | let dt = sample_clock - self.last_update; 130 | let dt_f = dt as Float; 131 | let result: Float; 132 | let mut complete = false; 133 | if reset { 134 | self.reset(sample_clock - 1, data.phase); 135 | complete = true; 136 | } 137 | 138 | let freq_speed = data.frequency / self.sample_rate as Float; 139 | let diff = freq_speed * dt_f; 140 | self.position += diff; 141 | if self.position > 1.0 { 142 | // Completed one wave cycle 143 | self.position -= 1.0; 144 | complete = true; 145 | } 146 | 147 | result = match data.waveform { 148 | LfoWaveform::Sine => self.get_sample_sine(), 149 | LfoWaveform::Tri => self.get_sample_triangle(), 150 | LfoWaveform::Saw => self.get_sample_saw_up(), 151 | LfoWaveform::SawDown => self.get_sample_saw_down(), 152 | LfoWaveform::Square => self.get_sample_square(0.5), 153 | LfoWaveform::Noise => self.get_sample_noise(), 154 | LfoWaveform::SnH => self.get_sample_snh(complete), 155 | } * data.amount; 156 | 157 | self.last_update += dt; 158 | if result > 1.0 { 159 | panic!("LFO overrun"); 160 | } 161 | (result, complete) 162 | } 163 | 164 | pub fn reset(&mut self, sample_clock: i64, phase: Float) { 165 | self.last_update = sample_clock; 166 | self.position = phase; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/synth/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod delay; 2 | pub mod engine; 3 | pub mod envelope; 4 | pub mod filter; 5 | pub mod lfo; 6 | pub mod oscillator; 7 | pub mod sample_generator; 8 | pub mod synth; 9 | pub mod voice; 10 | pub mod wt_oscillator; 11 | 12 | pub use delay::{Delay, DelayData}; 13 | pub use engine::Engine; 14 | pub use envelope::{Envelope, EnvelopeData}; 15 | pub use filter::{Filter, FilterData, OnePole}; 16 | pub use lfo::{Lfo, LfoData}; 17 | pub use oscillator::{Oscillator, OscData, OscType, OscRouting}; 18 | pub use sample_generator::SampleGenerator; 19 | pub use synth::{ 20 | Synth, PatchData, SynthState, 21 | PlayMode, FilterRouting, VoiceAllocation, PanOrigin, 22 | NUM_VOICES, NUM_GLOBAL_LFOS, NUM_MODULATORS 23 | }; 24 | pub use wt_oscillator::{WtOsc, WtOscData}; 25 | 26 | use super::Float; 27 | use super::MidiMessage; 28 | use super::{Parameter, SynthParam, ParamId, MenuItem}; 29 | use super::{SoundData, SyncValue}; 30 | use super::SynthMessage; 31 | use super::UiMessage; 32 | -------------------------------------------------------------------------------- /src/synth/oscillator.rs: -------------------------------------------------------------------------------- 1 | use super::Float; 2 | use super::{WtOsc, WtOscData}; 3 | use wavetable::WavetableRef; 4 | 5 | use serde::{Serialize, Deserialize}; 6 | 7 | #[derive(Serialize, Deserialize, Clone, Copy, Debug)] 8 | pub enum OscType { 9 | Wavetable, 10 | Noise 11 | } 12 | 13 | impl OscType { 14 | pub fn from_int(param: usize) -> OscType { 15 | match param { 16 | 0 => OscType::Wavetable, 17 | 1 => OscType::Noise, 18 | _ => panic!(), 19 | } 20 | } 21 | 22 | pub fn to_int(&self) -> usize { 23 | match self { 24 | OscType::Wavetable => 0, 25 | OscType::Noise => 1, 26 | } 27 | } 28 | } 29 | 30 | impl Default for OscType { 31 | fn default() -> Self { 32 | OscType::Wavetable 33 | } 34 | } 35 | 36 | #[derive(Serialize, Deserialize, Clone, Copy, Debug)] 37 | pub enum OscRouting { 38 | Filter1, 39 | Filter2, 40 | Direct 41 | } 42 | 43 | impl OscRouting { 44 | pub fn from_int(param: usize) -> OscRouting { 45 | match param { 46 | 0 => OscRouting::Filter1, 47 | 1 => OscRouting::Filter2, 48 | 2 => OscRouting::Direct, 49 | _ => panic!(), 50 | } 51 | } 52 | 53 | pub fn to_int(&self) -> usize { 54 | match self { 55 | OscRouting::Filter1 => 0, 56 | OscRouting::Filter2 => 1, 57 | OscRouting::Direct => 2, 58 | } 59 | } 60 | } 61 | 62 | impl Default for OscRouting { 63 | fn default() -> Self { 64 | OscRouting::Filter1 65 | } 66 | } 67 | 68 | #[derive(Serialize, Deserialize, Clone, Copy, Debug, Default)] 69 | pub struct OscData { 70 | pub level: Float, 71 | pub tune_halfsteps: i64, 72 | pub tune_cents: Float, 73 | pub freq_offset: Float, // Value derived from tune_halfsteps and tune_cents 74 | pub sync: i64, 75 | pub key_follow: i64, 76 | pub routing: OscRouting, 77 | pub osc_type: OscType, 78 | 79 | // Oscillator-specific data 80 | pub wt_osc_data: WtOscData, 81 | } 82 | 83 | impl OscData { 84 | pub fn init(&mut self) { 85 | self.level = 0.5; 86 | self.set_halfsteps(0); 87 | self.set_cents(0.0); 88 | self.sync = 0; 89 | self.key_follow = 1; 90 | self.wt_osc_data.init(); 91 | } 92 | 93 | /** Coarse tuning of oscillator (+/- 2 octaves). */ 94 | pub fn set_halfsteps(&mut self, halfsteps: i64) { 95 | self.tune_halfsteps = halfsteps; 96 | self.calc_freq_offset(); 97 | } 98 | 99 | /** Fine tuning of oscillator (+/- 1 halfsteps). */ 100 | pub fn set_cents(&mut self, cents: Float) { 101 | self.tune_cents = cents; 102 | self.calc_freq_offset(); 103 | } 104 | 105 | /** Calculate resulting frequence of tuning settings. */ 106 | fn calc_freq_offset(&mut self) { 107 | let inc: Float = 1.059463; 108 | self.freq_offset = inc.powf(self.tune_halfsteps as Float + self.tune_cents); 109 | } 110 | } 111 | 112 | pub struct Oscillator { 113 | last_update: i64, 114 | last_sample: Float, 115 | last_complete: bool, 116 | 117 | // Values to control the signal routing 118 | pub filter1_out: Float, 119 | pub filter2_out: Float, 120 | pub direct_out: Float, 121 | 122 | wt_osc: WtOsc, 123 | } 124 | 125 | impl Oscillator { 126 | pub fn new(sample_rate: u32, default_wt: WavetableRef) -> Self { 127 | Oscillator{ 128 | last_update: 0, 129 | last_sample: 0.0, 130 | last_complete: false, 131 | filter1_out: 1.0, 132 | filter2_out: 0.0, 133 | direct_out: 0.0, 134 | wt_osc: WtOsc::new(sample_rate, default_wt) 135 | } 136 | } 137 | 138 | pub fn get_sample(&mut self, frequency: Float, sample_clock: i64, data: &OscData, reset: bool) -> (Float, bool) { 139 | if reset { 140 | self.reset(sample_clock - 1); 141 | } 142 | 143 | // Check if we already calculated a matching value 144 | // TODO: Check if we also need to test the frequency here. 145 | if sample_clock == self.last_update { 146 | return (self.last_sample, self.last_complete); 147 | } 148 | 149 | let dt = sample_clock - self.last_update; 150 | let (result, complete) = match data.osc_type { 151 | OscType::Wavetable => self.wt_osc.get_sample(frequency, dt, &data.wt_osc_data), 152 | OscType::Noise => (Oscillator::get_sample_noise(), false), 153 | }; 154 | 155 | self.last_update += dt; 156 | self.last_sample = result; 157 | self.last_complete = complete; 158 | (result, complete) 159 | } 160 | 161 | pub fn reset(&mut self, sample_clock: i64) { 162 | self.wt_osc.reset(); 163 | self.last_update = sample_clock; 164 | } 165 | 166 | fn get_sample_noise() -> Float { 167 | (rand::random::() * 2.0) - 1.0 168 | } 169 | 170 | pub fn set_wavetable(&mut self, wavetable: WavetableRef) { 171 | self.wt_osc.set_wavetable(wavetable); 172 | } 173 | 174 | pub fn update_routing(&mut self, data: &OscData) { 175 | match data.routing { 176 | OscRouting::Filter1 => { self.filter1_out = 1.0; self.filter2_out = 0.0; self.direct_out = 0.0; } 177 | OscRouting::Filter2 => { self.filter1_out = 0.0; self.filter2_out = 1.0; self.direct_out = 0.0; } 178 | OscRouting::Direct => { self.filter1_out = 0.0; self.filter2_out = 0.0; self.direct_out = 1.0; } 179 | } 180 | } 181 | } 182 | 183 | -------------------------------------------------------------------------------- /src/synth/sample_generator.rs: -------------------------------------------------------------------------------- 1 | use super::Float; 2 | use super::SoundData; 3 | 4 | pub trait SampleGenerator { 5 | fn get_sample(&mut self, frequency: Float, sample_clock: i64, data: &SoundData, reset: bool) -> (Float, bool); 6 | fn reset(&mut self, sample_clock: i64); 7 | } 8 | 9 | -------------------------------------------------------------------------------- /src/synth/voice.rs: -------------------------------------------------------------------------------- 1 | use super::Envelope; 2 | use super::Filter; 3 | use super::Float; 4 | use super::Lfo; 5 | use super::{Parameter, ParamId, SynthParam, MenuItem}; 6 | use super::{PlayMode, FilterRouting}; 7 | use super::SynthState; 8 | use super::{Oscillator, OscData}; 9 | use super::SoundData; 10 | 11 | use wavetable::{Wavetable, WavetableRef}; 12 | 13 | use std::sync::Arc; 14 | 15 | pub const NUM_OSCILLATORS: usize = 3; 16 | pub const NUM_ENVELOPES: usize = 3; 17 | pub const NUM_FILTERS: usize = 2; 18 | pub const NUM_LFOS: usize = 2; 19 | 20 | pub struct Voice { 21 | // Components 22 | osc: [Oscillator; NUM_OSCILLATORS], 23 | env: [Envelope; NUM_ENVELOPES], 24 | pub filter: [Filter; NUM_FILTERS], 25 | lfo: [Lfo; NUM_LFOS], 26 | 27 | // Static config 28 | pan_l: Float, // Panning of this voice in the stereo field 29 | pan_r: Float, // Panning of this voice in the stereo field 30 | 31 | // Current state 32 | triggered: bool, 33 | pub trigger_seq: u64, // Sequence number for keeping track of trigger order 34 | pub key: u8, // Key that was pressed to trigger this voice 35 | velocity: Float, // Raw velocity of NoteOn event (for use as modulation source) 36 | scaled_vel: Float, // Velocity scaled according to sound settings (for use as amplifier) 37 | input_freq: Float, // Frequency to play as received from Synth 38 | last_update: i64, 39 | } 40 | 41 | impl Voice { 42 | pub fn new(sample_rate: u32, default_wavetable: Arc) -> Self { 43 | let osc = [ 44 | Oscillator::new(sample_rate, Arc::clone(&default_wavetable)), 45 | Oscillator::new(sample_rate, Arc::clone(&default_wavetable)), 46 | Oscillator::new(sample_rate, Arc::clone(&default_wavetable)), 47 | ]; 48 | let env = [ 49 | Envelope::new(sample_rate as Float), 50 | Envelope::new(sample_rate as Float), 51 | Envelope::new(sample_rate as Float), 52 | ]; 53 | let filter = [ 54 | Filter::new(sample_rate), 55 | Filter::new(sample_rate), 56 | ]; 57 | let lfo = [ 58 | Lfo::new(sample_rate), 59 | Lfo::new(sample_rate), 60 | ]; 61 | Voice{ 62 | osc, 63 | env, 64 | filter, 65 | lfo, 66 | pan_l: 0.5, 67 | pan_r: 0.5, 68 | triggered: false, 69 | trigger_seq: 0, 70 | key: 0, 71 | velocity: 0.0, 72 | scaled_vel: 0.0, 73 | input_freq: 440.0, 74 | last_update: 0i64} 75 | } 76 | 77 | pub fn reset(&mut self) { 78 | self.triggered = false; 79 | for e in &mut self.env { 80 | e.reset(); 81 | } 82 | for f in &mut self.filter { 83 | f.reset(); 84 | } 85 | } 86 | 87 | fn get_frequency(data: &OscData, input_freq: Float) -> Float { 88 | let mut freq = if data.key_follow == 0 { 89 | 440.0 90 | } else { 91 | input_freq 92 | }; 93 | freq *= data.freq_offset; 94 | freq 95 | } 96 | 97 | fn get_mod_values(&mut self, sample_clock: i64, sound_global: &SoundData, sound_local: &mut SoundData) { 98 | // Get modulated values from global sound and discard values that were 99 | // modulated for the previous sample. Complete copy is faster than 100 | // looping over the modulators. 101 | *sound_local = *sound_global; 102 | 103 | // Then update the local sound with mod values 104 | let mut param_id = ParamId{..Default::default()}; 105 | let mut synth_param = SynthParam{..Default::default()}; 106 | for m in sound_global.modul.iter() { 107 | 108 | if !m.active { 109 | continue; 110 | } 111 | 112 | // Get current value of target parameter 113 | param_id.set(m.target_func, m.target_func_id, m.target_param); 114 | let mut current_val = sound_local.get_value(¶m_id); 115 | 116 | if !m.is_global { 117 | 118 | // Get modulator source output 119 | let mod_val: Float = match m.source_func { 120 | Parameter::Oscillator => { 121 | let id = m.source_func_id - 1; 122 | let freq = Voice::get_frequency(&sound_local.osc[id], self.input_freq); 123 | let (val, _) = self.osc[id].get_sample(freq, sample_clock, &sound_local.osc[id], false); 124 | val 125 | }, 126 | Parameter::Lfo => { 127 | let (val, _) = self.lfo[m.source_func_id - 1].get_sample(sample_clock, &sound_local.lfo[m.source_func_id - 1], false); 128 | val 129 | }, 130 | Parameter::Envelope => { 131 | self.env[m.source_func_id - 1].get_sample(sample_clock, &sound_local.env[m.source_func_id - 1]) 132 | } 133 | Parameter::Velocity => { 134 | self.velocity 135 | } 136 | _ => 0.0, 137 | } * m.scale; 138 | 139 | let mut val = current_val.as_float(); 140 | 141 | // Update value 142 | let dest_range = MenuItem::get_val_range(param_id.function, param_id.parameter); 143 | val = dest_range.safe_add(val, mod_val); 144 | 145 | // Update parameter in voice sound data 146 | current_val.set_from_float(val); 147 | } 148 | 149 | // TODO: Too much copying 150 | synth_param.set(m.target_func, m.target_func_id, m.target_param, current_val); 151 | sound_local.set_parameter(&synth_param); 152 | } 153 | } 154 | 155 | pub fn get_sample(&mut self, 156 | sample_clock: i64, 157 | sound_global: &SoundData, 158 | sound_local: &mut SoundData, 159 | global_state: &SynthState) -> (Float, Float) { 160 | if !self.is_running() { 161 | return (0.0, 0.0); 162 | } 163 | let mut input_f1 = 0.0; 164 | let mut input_f2 = 0.0; 165 | let mut result_direct = 0.0; 166 | self.last_update = sample_clock; 167 | let mut reset = false; 168 | let input_freq = self.input_freq * global_state.freq_factor; 169 | let mut freq: Float; 170 | 171 | // Prepare modulation values 172 | self.get_mod_values(sample_clock, sound_global, sound_local); 173 | 174 | // Get mixed output from oscillators 175 | for (i, osc) in self.osc.iter_mut().enumerate() { 176 | freq = Voice::get_frequency(&sound_local.osc[i], input_freq); 177 | let (sample, wave_complete) = osc.get_sample(freq, sample_clock, &sound_local.osc[i], reset); 178 | // TODO: Add panning here 179 | let sample_amped = sample * sound_local.osc[i].level * self.scaled_vel; 180 | input_f1 += sample_amped * osc.filter1_out; 181 | input_f2 += sample_amped * osc.filter2_out; 182 | result_direct += sample_amped * osc.direct_out; 183 | // Sync oscillator 1 to 0 184 | reset = i == 0 && wave_complete && sound_local.osc[1].sync == 1; 185 | } 186 | 187 | // Feed it into the filters 188 | let mut result: Float = self.apply_filter(sample_clock, 189 | sound_local, 190 | input_f1, 191 | input_f2, 192 | input_freq); 193 | result += result_direct; 194 | 195 | // Apply the volume envelope 196 | let env_amp = self.env[0].get_sample(sample_clock, &sound_local.env[0]); 197 | if sound_local.patch.env_depth > 0.0 { 198 | result *= env_amp * sound_local.patch.env_depth; 199 | } 200 | if result > 1.0 { 201 | result = 1.0; 202 | } else if result < -1.0 { 203 | result = -1.0; 204 | } 205 | 206 | // Pan result 207 | // TODO: Use actual panning algorithm 208 | let result_l = result * self.pan_l; 209 | let result_r = result * self.pan_r; 210 | 211 | (result_l, result_r) 212 | } 213 | 214 | pub fn apply_filter(&mut self, 215 | sample_clock: i64, 216 | sound_local: &mut SoundData, 217 | input_f1: Float, 218 | mut input_f2: Float, 219 | input_freq: Float) -> Float { 220 | let filter_env = self.env[1].get_sample(sample_clock, &sound_local.env[1]); // Env2 is normaled to filter cutoff 221 | let output_f1 = self.filter[0].process(input_f1, &mut sound_local.filter[0], input_freq, filter_env); 222 | let mut result = match sound_local.patch.filter_routing { 223 | FilterRouting::Parallel => { 224 | output_f1 225 | } 226 | FilterRouting::Serial => { 227 | input_f2 += output_f1; 228 | 0.0 229 | } 230 | }; 231 | result += self.filter[1].process(input_f2, &mut sound_local.filter[1], input_freq, filter_env); 232 | result 233 | } 234 | 235 | pub fn set_key(&mut self, key: u8) { 236 | self.key = key; 237 | } 238 | 239 | pub fn set_freq(&mut self, freq: Float) { 240 | self.input_freq = freq; 241 | } 242 | 243 | pub fn set_velocity(&mut self, velocity: u8, sensitivity: Float) { 244 | self.velocity = velocity as Float / 127.0; 245 | // Sensitivity gives us the range of velocity values from the maximum. 246 | // Sens 0.7 means scaled_velocity ranges from 0.3 - 1.0. 247 | self.scaled_vel = (1.0 - sensitivity) + (self.velocity * sensitivity); 248 | } 249 | 250 | pub fn set_wavetable(&mut self, osc_id: usize, wt: WavetableRef) { 251 | self.osc[osc_id].set_wavetable(wt); 252 | } 253 | 254 | // Set panning. 0.0 = left, 1.0 = right 255 | pub fn set_pan(&mut self, pan: Float) { 256 | self.pan_l = 1.0 - pan; 257 | self.pan_r = pan; 258 | } 259 | 260 | pub fn update_routing(&mut self, osc_id: usize, osc_data: &OscData) { 261 | self.osc[osc_id].update_routing(osc_data); 262 | } 263 | 264 | pub fn trigger(&mut self, trigger_seq: u64, trigger_time: i64, sound: &SoundData) { 265 | let trigger = match sound.patch.play_mode { 266 | PlayMode::Poly => true, // Poly: Always retrigger 267 | PlayMode::Mono => true, // Mono: Always retrigger 268 | PlayMode::Legato => { // Legator: Only retrigger if not already triggered 269 | !self.triggered 270 | } 271 | }; 272 | self.trigger_seq = trigger_seq; 273 | if trigger { 274 | if !self.is_running() { 275 | for osc in self.osc.iter_mut() { 276 | osc.reset(trigger_time); 277 | } 278 | } 279 | for i in 0..NUM_ENVELOPES { 280 | self.env[i].trigger(trigger_time, &sound.env[i]); 281 | } 282 | for (i, lfo) in self.lfo.iter_mut().enumerate() { 283 | lfo.reset(trigger_time, sound.lfo[i].phase); 284 | } 285 | } 286 | self.triggered = true; 287 | } 288 | 289 | // TODO: Release velocity 290 | pub fn key_release(&mut self, _velocity: u8, pedal_held: bool, sound: &SoundData) { 291 | self.triggered = false; 292 | if !pedal_held { 293 | self.release_envelopes(sound); 294 | } 295 | } 296 | 297 | pub fn pedal_release(&mut self, sound: &SoundData) { 298 | if !self.triggered { 299 | self.release_envelopes(sound); 300 | } 301 | } 302 | 303 | pub fn is_triggered(&self) -> bool { 304 | self.triggered 305 | } 306 | 307 | pub fn is_running(&self) -> bool { 308 | self.triggered || self.env[0].is_running() 309 | } 310 | 311 | fn release_envelopes(&mut self, sound: &SoundData) { 312 | for i in 0..NUM_ENVELOPES { 313 | self.env[i].release(self.last_update, &sound.env[i]); 314 | } 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /src/synth/wt_oscillator.rs: -------------------------------------------------------------------------------- 1 | use super::Float; 2 | use wavetable::WavetableRef; 3 | 4 | use serde::{Serialize, Deserialize}; 5 | 6 | const MAX_VOICES: usize = 7; 7 | 8 | /// Sound data for the wavetable oscillator 9 | #[derive(Serialize, Deserialize, Clone, Copy, Debug, Default)] 10 | pub struct WtOscData { 11 | pub num_voices: i64, 12 | pub voice_spread: Float, 13 | pub wave_index: Float, // Index into the wave tables 14 | pub wavetable: usize, 15 | } 16 | 17 | impl WtOscData { 18 | pub fn init(&mut self) { 19 | self.set_voice_num(1); 20 | self.wave_index = 0.0; 21 | } 22 | 23 | /** Number of detuned voices per oscillator. */ 24 | pub fn set_voice_num(&mut self, voices: i64) { 25 | self.num_voices = if voices > MAX_VOICES as i64 { MAX_VOICES as i64 } else { voices }; 26 | } 27 | 28 | /** Detune amount per voice. */ 29 | pub fn set_voice_spread(&mut self, spread: Float) { 30 | self.voice_spread = spread; 31 | } 32 | } 33 | 34 | const NUM_SAMPLES_PER_TABLE: usize = 2048; 35 | const NUM_VALUES_PER_TABLE: usize = NUM_SAMPLES_PER_TABLE + 1; // Add one sample for easier interpolation on last sample 36 | 37 | pub struct WtOsc { 38 | pub sample_rate: Float, 39 | last_pos: [Float; MAX_VOICES], // State for up to MAX_VOICES oscillators running in sync 40 | wave: WavetableRef, 41 | } 42 | 43 | /// Wavetable oscillator implementation. 44 | /// 45 | /// The WT oscillator uses multiple tables per waveform to avoid aliasing. Each 46 | /// table is filled by adding all harmonics that will not exceed the Nyquist 47 | /// frequency for the given usable range of the table (one octave). 48 | /// 49 | impl WtOsc { 50 | 51 | /// Create a new wavetable oscillator. 52 | /// 53 | pub fn new(sample_rate: u32, wave: WavetableRef) -> WtOsc { 54 | let sample_rate = sample_rate as Float; 55 | let last_pos = [0.0; MAX_VOICES]; 56 | WtOsc{sample_rate, 57 | last_pos, 58 | wave} 59 | } 60 | 61 | pub fn set_wavetable(&mut self, wavetable: WavetableRef) { 62 | self.wave = wavetable; 63 | } 64 | 65 | // Interpolate between two sample values with the given ratio. 66 | fn interpolate(val_a: Float, val_b: Float, ratio: Float) -> Float { 67 | val_a + ((val_b - val_a) * ratio) 68 | } 69 | 70 | // Get a sample from the given table at the given position. 71 | // 72 | // Uses linear interpolation for positions that don't map directly to a 73 | // table index. 74 | // 75 | fn get_wave_sample(table: &[Float], table_index: usize, position: Float) -> Float { 76 | let floor_pos = position as usize; 77 | let frac = position - floor_pos as Float; 78 | let position = floor_pos + table_index * NUM_VALUES_PER_TABLE; 79 | if frac > 0.9 { 80 | // Close to upper sample 81 | table[position + 1] 82 | } else if frac < 0.1 { 83 | // Close to lower sample 84 | table[position] 85 | } else { 86 | // Interpolate for differences > 10% 87 | let value_left = table[position]; 88 | let value_right = table[position + 1]; 89 | WtOsc::interpolate(value_left, value_right, frac) 90 | } 91 | } 92 | 93 | pub fn get_sample(&mut self, frequency: Float, dt: i64, data: &WtOscData) -> (Float, bool) { 94 | let dt_f = dt as Float; 95 | let mut result = 0.0; 96 | let mut complete = false; 97 | 98 | for i in 0..data.num_voices { 99 | let mut last_pos = self.last_pos[i as usize]; 100 | let freq_diff = (frequency / 100.0) * (data.voice_spread * i as Float) * (1 - ((i & 0x01) * 2)) as Float; 101 | let frequency = frequency + freq_diff; 102 | let freq_speed = frequency * (NUM_SAMPLES_PER_TABLE as Float / self.sample_rate); 103 | let diff = freq_speed * dt_f; 104 | last_pos += diff; 105 | if last_pos > (NUM_SAMPLES_PER_TABLE as Float) { 106 | // Completed one wave cycle 107 | last_pos -= NUM_SAMPLES_PER_TABLE as Float; 108 | complete = true; // Sync signal for other oscillators 109 | } 110 | 111 | let translated_index = (self.wave.table.len() - 1) as Float * data.wave_index; 112 | let lower_wave = translated_index as usize; 113 | let lower_wave_float = lower_wave as Float; 114 | let lower_fract: Float = 1.0 - (translated_index - lower_wave_float); 115 | let upper_fract: Float = if lower_fract != 1.0 { 1.0 - lower_fract } else { 0.0 }; 116 | 117 | let table_index = WtOsc::get_table_index(self.wave.num_octaves, frequency); 118 | 119 | let mut voice_result = WtOsc::get_wave_sample(&self.wave.table[lower_wave], table_index, last_pos) * lower_fract; 120 | if upper_fract > 0.0 { 121 | voice_result += WtOsc::get_wave_sample(&self.wave.table[lower_wave + 1], table_index, last_pos) * upper_fract; 122 | } 123 | result += voice_result; 124 | self.last_pos[i as usize] = last_pos; 125 | } 126 | (result, complete) 127 | } 128 | 129 | // Look up the octave table matching the current frequency. 130 | fn get_table_index(num_octaves: usize, freq: Float) -> usize { 131 | let two: Float = 2.0; 132 | let mut compare_freq = (440.0 / 32.0) * (two.powf((-9.0) / 12.0)); 133 | for i in 0..num_octaves { 134 | if freq < compare_freq * 2.0 { 135 | return i; 136 | } 137 | compare_freq *= 2.0; 138 | } 139 | num_octaves - 1 140 | } 141 | 142 | pub fn reset(&mut self) { 143 | for i in 0..MAX_VOICES { 144 | self.last_pos[i] = 0.0; 145 | } 146 | } 147 | 148 | } 149 | 150 | #[cfg(test)] 151 | #[test] 152 | fn test_get_table_index() { 153 | assert_eq!(WtOsc::get_table_index(11, 10.0), 0); 154 | assert_eq!(WtOsc::get_table_index(11, 20.0), 1); 155 | assert_eq!(WtOsc::get_table_index(11, 40.0), 2); 156 | assert_eq!(WtOsc::get_table_index(11, 80.0), 3); 157 | assert_eq!(WtOsc::get_table_index(11, 160.0), 4); 158 | assert_eq!(WtOsc::get_table_index(11, 320.0), 5); 159 | assert_eq!(WtOsc::get_table_index(11, 640.0), 6); 160 | assert_eq!(WtOsc::get_table_index(11, 1280.0), 7); 161 | assert_eq!(WtOsc::get_table_index(11, 2560.0), 8); 162 | assert_eq!(WtOsc::get_table_index(11, 5120.0), 9); 163 | assert_eq!(WtOsc::get_table_index(11, 10240.0), 10); 164 | assert_eq!(WtOsc::get_table_index(11, 20480.0), 10); 165 | } 166 | 167 | #[test] 168 | fn test_interpolate() { 169 | assert_eq!(WtOsc::interpolate(2.0, 3.0, 0.0), 2.0); // Exactly left value 170 | assert_eq!(WtOsc::interpolate(2.0, 3.0, 1.0), 3.0); // Exactly right value 171 | assert_eq!(WtOsc::interpolate(2.0, 3.0, 0.5), 2.5); // Middle 172 | } 173 | 174 | #[test] 175 | fn test_get_sample() { 176 | //fn get_sample(table: &[Float], table_index: usize, position: Float) -> Float{ 177 | let mut table = [0.0; NUM_VALUES_PER_TABLE]; 178 | table[0] = 2.0; 179 | table[1] = 3.0; 180 | assert_eq!(WtOsc::get_wave_sample(&table, 0, 0.0), 2.0); // Exactly first value 181 | assert_eq!(WtOsc::get_wave_sample(&table, 0, 1.0), 3.0); // Exactly second value 182 | assert_eq!(WtOsc::get_wave_sample(&table, 0, 0.5), 2.5); // Middle 183 | assert_eq!(WtOsc::get_wave_sample(&table, 0, 0.09), 2.0); // Close to first 184 | assert_eq!(WtOsc::get_wave_sample(&table, 0, 0.99), 3.0); // Close to second 185 | } 186 | -------------------------------------------------------------------------------- /src/tui/bar.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::hash::Hash; 3 | use std::rc::Rc; 4 | 5 | use super::Observer; 6 | use super::{Value, get_int, get_float}; 7 | use super::{Printer, Widget, WidgetProperties}; 8 | 9 | type BarRef = Rc>>; 10 | 11 | /** A horizontal bar representing a value. 12 | * 13 | * Can have logarithmic scaling to improve visibility of smaller values. 14 | */ 15 | pub struct Bar { 16 | props: WidgetProperties, 17 | min: Value, 18 | max: Value, 19 | value: Value, 20 | logarithmic: bool, // Use logarithmic curve for values 21 | } 22 | 23 | impl Bar { 24 | pub fn new(min: Value, max: Value, value: Value) -> BarRef { 25 | let width = 10; 26 | let height = 1; 27 | let props = WidgetProperties::new(width, height); 28 | let logarithmic = false; 29 | Rc::new(RefCell::new(Bar{props, min, max, value, logarithmic})) 30 | } 31 | 32 | pub fn set_logarithmic(&mut self, l: bool) { 33 | self.logarithmic = l; 34 | } 35 | 36 | fn get_length(&self, value: &Value) -> usize { 37 | let min: f64; 38 | let max: f64; 39 | let fvalue: f64; 40 | match value { 41 | Value::Int(v) => { 42 | min = get_int(&self.min) as f64; 43 | max = get_int(&self.max) as f64; 44 | fvalue = *v as f64; 45 | } 46 | Value::Float(v) => { 47 | min = get_float(&self.min); 48 | max = get_float(&self.max); 49 | fvalue = *v; 50 | } 51 | Value::Str(_) => panic!(), 52 | } 53 | let offset = min * -1.0; 54 | let range = max - min; 55 | let scale = self.props.width as f64 / range; 56 | let mut value = fvalue + offset; 57 | if self.logarithmic { 58 | // Using a logarithmic curve makes smaller values easier to see. 59 | let percent = value / range; 60 | let factor = percent.sqrt().sqrt(); // TODO: Slow, find a nicer way 61 | value = factor * range; 62 | } 63 | (value * scale) as usize 64 | } 65 | } 66 | 67 | impl Widget for Bar { 68 | fn get_widget_properties_mut(&mut self) -> &mut WidgetProperties { 69 | &mut self.props 70 | } 71 | 72 | fn get_widget_properties(&self) -> &WidgetProperties { 73 | &self.props 74 | } 75 | 76 | fn draw(&self, p: &mut dyn Printer) { 77 | let index = self.get_length(&self.value); 78 | // TODO: Optimize by using array 79 | p.set_color(self.props.colors.fg_compl, self.props.colors.bg_compl); 80 | for x in self.props.pos_x..self.props.pos_x + index { 81 | p.print(x, self.props.pos_y,"‾"); 82 | } 83 | } 84 | } 85 | 86 | impl Observer for Bar { 87 | fn update(&mut self, value: Value) { 88 | self.value = value; 89 | self.set_dirty(true); 90 | } 91 | } 92 | 93 | #[test] 94 | fn test_bar_translation() { 95 | // ===== 96 | // Float 97 | // ===== 98 | // Case 1: 0.0 - 1.0 99 | let b: BarRef = Bar::new(Value::Float(0.0), Value::Float(1.0), Value::Float(0.0)); 100 | assert_eq!(b.borrow().get_length(&Value::Float(0.0)), 0); 101 | assert_eq!(b.borrow().get_length(&Value::Float(0.5)), 5); 102 | assert_eq!(b.borrow().get_length(&Value::Float(1.0)), 10); 103 | 104 | // Case 2: -1.0 - 1.0 105 | let b: BarRef = Bar::new(Value::Float(-1.0), Value::Float(1.0), Value::Float(0.0)); 106 | assert_eq!(b.borrow().get_length(&Value::Float(-1.0)), 0); 107 | assert_eq!(b.borrow().get_length(&Value::Float(0.0)), 5); 108 | assert_eq!(b.borrow().get_length(&Value::Float(1.0)), 10); 109 | 110 | // Case 3: 2.0 - 10.0 111 | let b: BarRef = Bar::new(Value::Float(2.0), Value::Float(10.0), Value::Float(0.0)); 112 | assert_eq!(b.borrow().get_length(&Value::Float(2.0)), 0); 113 | assert_eq!(b.borrow().get_length(&Value::Float(6.0)), 5); 114 | assert_eq!(b.borrow().get_length(&Value::Float(10.0)), 10); 115 | 116 | // === 117 | // Int 118 | // === 119 | // Case 1: 0 - 8 120 | let b: BarRef = Bar::new(Value::Int(0), Value::Int(8), Value::Int(0)); 121 | assert_eq!(b.borrow().get_length(&Value::Int(0)), 0); 122 | assert_eq!(b.borrow().get_length(&Value::Int(4)), 5); 123 | assert_eq!(b.borrow().get_length(&Value::Int(8)), 10); 124 | 125 | // Case 2: -4 - 4 126 | let b: BarRef = Bar::new(Value::Int(-4), Value::Int(4), Value::Int(0)); 127 | assert_eq!(b.borrow().get_length(&Value::Int(-4)), 0); 128 | assert_eq!(b.borrow().get_length(&Value::Int(0)), 5); 129 | assert_eq!(b.borrow().get_length(&Value::Int(4)), 10); 130 | 131 | // Case 3: 2 - 10 132 | let b: BarRef = Bar::new(Value::Int(2), Value::Int(10), Value::Int(0)); 133 | assert_eq!(b.borrow().get_length(&Value::Int(2)), 0); 134 | assert_eq!(b.borrow().get_length(&Value::Int(6)), 5); 135 | assert_eq!(b.borrow().get_length(&Value::Int(10)), 10); 136 | } 137 | -------------------------------------------------------------------------------- /src/tui/button.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::hash::Hash; 3 | use std::rc::Rc; 4 | 5 | use super::Observer; 6 | use super::{Value, get_int}; 7 | use super::{Printer, Widget, WidgetProperties}; 8 | 9 | type ButtonRef = Rc>>; 10 | 11 | /** A button for boolean values */ 12 | pub struct Button { 13 | props: WidgetProperties, 14 | value: Value, 15 | } 16 | 17 | impl Button { 18 | pub fn new(value: Value) -> ButtonRef { 19 | let width = 1; 20 | let height = 1; 21 | let props = WidgetProperties::new(width, height); 22 | Rc::new(RefCell::new(Button{props, value})) 23 | } 24 | } 25 | 26 | impl Widget for Button { 27 | fn get_widget_properties_mut(&mut self) -> &mut WidgetProperties { 28 | &mut self.props 29 | } 30 | 31 | fn get_widget_properties(&self) -> &WidgetProperties { 32 | &self.props 33 | } 34 | 35 | fn draw(&self, p: &mut dyn Printer) { 36 | //let value = if let Value::Int(x) = self.value { x } else { panic!() }; 37 | let value = get_int(&self.value); 38 | let chars = if value > 0 { "▣" } else { "□" }; 39 | p.set_color(self.props.colors.fg_compl_l, self.props.colors.bg_compl_l); 40 | p.print(self.props.pos_x, self.props.pos_y, chars); 41 | } 42 | } 43 | 44 | impl Observer for Button { 45 | fn update(&mut self, value: Value) { 46 | self.value = value; 47 | self.set_dirty(true); 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /src/tui/canvas.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::hash::Hash; 3 | use std::rc::Rc; 4 | use std::vec::Vec; 5 | 6 | use super::Float; 7 | use super::{Index, Printer, Widget, WidgetProperties}; 8 | 9 | pub type CanvasRef = Rc>>; 10 | 11 | pub struct Canvas { 12 | props: WidgetProperties, 13 | byte: Vec, 14 | line_buff: RefCell, 15 | } 16 | 17 | impl Canvas { 18 | pub fn new(width: Index, height: Index) -> CanvasRef { 19 | let byte = vec!{' '; (width * height) as usize}; 20 | let props = WidgetProperties::new(width, height); 21 | let line_buff = RefCell::new(String::with_capacity(width)); 22 | Rc::new(RefCell::new(Canvas{props, byte, line_buff})) 23 | } 24 | 25 | pub fn clear(&mut self) { 26 | self.byte.iter_mut().for_each(|x| *x = ' '); 27 | } 28 | 29 | // Set the value of a single cell in the canvas 30 | pub fn set(&mut self, mut x: Index, mut y: Index, val: char) { 31 | if x >= self.props.width { 32 | x = self.props.width - 1; 33 | } 34 | if y >= self.props.height { 35 | y = self.props.height - 1; 36 | } 37 | self.byte[(y * self.props.width + x) as usize] = val; 38 | } 39 | 40 | /** Transfer the graph into the output buffer. 41 | * 42 | * The number of values to print can be greater or less than the width of 43 | * the Canvas, so we need to either combine or stretch the values to make 44 | * them fit. 45 | * 46 | * To make things look nice, we also fill spaces between points that are in 47 | * neighboring columns, but more than one row apart. 48 | */ 49 | pub fn plot(&mut self, buff: &[Float], min: Float, max: Float) { 50 | let (scale_x, scale_y, offset) = self.calc_scaling(min, max, buff.len()); 51 | if min < 0.0 && max > 0.0 { 52 | // X axis lies on screen. Calculate its position and print it. 53 | let x_axis = self.calc_x_axis_position(min, max); 54 | self.plot_x_axis(x_axis as Index); 55 | } 56 | 57 | // Plot points 58 | let mut x_pos: Index = 0; 59 | let mut y_pos: Index; 60 | let mut prev_y_pos = self.val_to_y(buff[0], offset, scale_y, min, max); 61 | let mut prev_x_pos: Index = 0; 62 | let mut y_accu: Float = 0.0; 63 | 64 | for (i, value) in buff.iter().enumerate() { 65 | x_pos = (i as Float * scale_x) as Index; 66 | if x_pos == prev_x_pos { 67 | // Take min/ max of all values that fall on the same column on screen 68 | if *value >= 0.0 && *value > y_accu 69 | || *value < 0.0 && *value < y_accu { 70 | y_accu = *value; 71 | } 72 | } else { 73 | // Advanced to next column, print previous max value 74 | y_pos = self.val_to_y(y_accu, offset, scale_y, min, max); 75 | self.draw_point(prev_x_pos, x_pos, prev_y_pos, y_pos); 76 | y_accu = *value; 77 | prev_x_pos = x_pos; 78 | prev_y_pos = y_pos; 79 | } 80 | } 81 | 82 | // Plot last point 83 | y_pos = self.val_to_y(y_accu, offset, scale_y, min, max); 84 | self.draw_point(prev_x_pos, x_pos, prev_y_pos, y_pos); 85 | self.props.set_dirty(true); 86 | } 87 | 88 | fn draw_point(&mut self, prev_x_pos: Index, x_pos: Index, prev_y_pos: Index, y_pos: Index) { 89 | let diff: i64 = Self::diff(y_pos, prev_y_pos); 90 | if diff > 1 { 91 | // Current and previous points are more than one row apart, fill 92 | // the space between. 93 | self.connect_points(prev_x_pos, y_pos, prev_y_pos); 94 | } 95 | 96 | // Draw actualy point 97 | self.set(prev_x_pos, y_pos, '∘'); 98 | 99 | // Stretch point over skipped columns if needed 100 | if x_pos - prev_x_pos > 1 { 101 | self.stretch_point(prev_x_pos + 1, x_pos, y_pos); 102 | } 103 | } 104 | 105 | // Draw a vertical line between values more than one row apart. 106 | fn connect_points(&mut self, x_pos: Index, y_pos: Index, prev_y_pos: Index) { 107 | let end_y_pos: Index; 108 | let start_y_pos: Index; 109 | if y_pos > prev_y_pos { 110 | start_y_pos = prev_y_pos + 1; 111 | end_y_pos = y_pos; 112 | } else { 113 | start_y_pos = prev_y_pos; 114 | end_y_pos = y_pos + 1; 115 | }; 116 | let (x1, from, _x2, to) = Self::sort(x_pos as Index, start_y_pos, x_pos as Index, end_y_pos); 117 | for i in from..to { 118 | self.set(x1, i, '⋅'); 119 | } 120 | } 121 | 122 | fn stretch_point(&mut self, prev_x_pos: Index, x_pos: Index, y_pos: Index) { 123 | for x in prev_x_pos..x_pos { 124 | self.set(x as Index, y_pos, '∘'); 125 | } 126 | } 127 | 128 | pub fn calc_scaling(&self, min: Float, max: Float, num_values: usize) -> (Float, Float, Float) { 129 | let scale_x = self.props.width as Float / num_values as Float; 130 | let scale_y = (self.props.height - 1) as Float / (max - min); 131 | let offset = min * -1.0; 132 | (scale_x, scale_y, offset) 133 | } 134 | 135 | pub fn calc_x_axis_position(&self, min: Float, max: Float) -> Index { 136 | ((self.props.height as Float / (max - min)) * (min * -1.0)) as Index 137 | } 138 | 139 | // Transform a value to a graph coordinate 140 | fn val_to_y(&self, mut value: Float, offset: Float, scale: Float, min: Float, max: Float) -> Index { 141 | if value < min { 142 | value = min; 143 | } else if value > max { 144 | value = max; 145 | } 146 | ((value + offset) * scale).round() as Index 147 | } 148 | 149 | // Draw the x-axis line into the buffer 150 | fn plot_x_axis(&mut self, y_pos: Index) { 151 | for x in 0..self.props.width { 152 | self.set(x, y_pos, '-'); 153 | } 154 | } 155 | 156 | fn diff(a: Index, b: Index) -> i64 { 157 | let result = if a > b { a - b } else { b - a }; 158 | result as i64 159 | } 160 | 161 | fn sort(x_a: Index, y_a: Index, x_b: Index, y_b: Index) -> (Index, Index, Index, Index) { 162 | if y_a < y_b { (x_a, y_a, x_b, y_b) } else { (x_b, y_b, x_a, y_a) } 163 | } 164 | } 165 | 166 | impl Widget for Canvas { 167 | fn get_widget_properties_mut(&mut self) -> &mut WidgetProperties { 168 | &mut self.props 169 | } 170 | 171 | fn get_widget_properties(&self) -> &WidgetProperties { 172 | &self.props 173 | } 174 | 175 | // Print the output buffer to the screen 176 | fn draw(&self, p: &mut dyn Printer) { 177 | let pos_x = self.props.pos_x; 178 | let pos_y = self.props.pos_y; 179 | let mut buff = self.line_buff.borrow_mut(); 180 | p.set_color(self.props.colors.fg_base, self.props.colors.bg_base_l); 181 | for y in 0..self.props.height { 182 | buff.clear(); 183 | for x in 0..self.props.width { 184 | (*buff).push(self.byte[(y * self.props.width + x) as usize]); 185 | } 186 | let y_coord = pos_y + ((self.props.height - 1) - y); 187 | p.print(pos_x, y_coord, &buff); 188 | } 189 | } 190 | } 191 | 192 | // ---------- 193 | // Unit tests 194 | // ---------- 195 | 196 | #[test] 197 | fn scaling_works() { 198 | let canvas: CanvasRef = Canvas::new(100, 21); // 100 wide, 21 high 199 | let c = canvas.borrow(); 200 | 201 | let (scale_x, scale_y, offset) = c.calc_scaling(-1.0, 1.0, 100); // 100 values from -1.0 to 1.0 202 | assert_eq!(scale_x, 1.0); 203 | assert_eq!(scale_y, 10.0); // 10.5 up and 10.5 down 204 | assert_eq!(offset, 1.0); 205 | 206 | let (scale_x, scale_y, offset) = c.calc_scaling(0.0, 1.0, 200); // 200 values from 0.0 to 1.0 207 | assert_eq!(scale_x, 0.5); 208 | assert_eq!(scale_y, 20.0); // 21 up 209 | assert_eq!(offset, 0.0); 210 | } 211 | 212 | #[test] 213 | fn x_axis_is_placed_correctly() { 214 | let canvas: CanvasRef = Canvas::new(100, 21); 215 | let c = canvas.borrow(); 216 | let x_axis = c.calc_x_axis_position(-1.0, 1.0); 217 | assert_eq!(x_axis, 10); 218 | } 219 | 220 | #[test] 221 | fn translation_works() { 222 | let canvas: CanvasRef = Canvas::new(100, 21); 223 | let c = canvas.borrow(); 224 | 225 | let (min, max) = (-1.0, 1.0); 226 | let (_, scale_y, offset) = c.calc_scaling(min, max, 100); 227 | let y = c.val_to_y(0.0, offset, scale_y, min, max); 228 | assert_eq!(y, 10); 229 | let y = c.val_to_y(-1.0, offset, scale_y, min, max); 230 | assert_eq!(y, 0); 231 | let y = c.val_to_y(1.0, offset, scale_y, min, max); 232 | assert_eq!(y, 20); 233 | 234 | let (min, max) = (0.0, 1.0); 235 | let (_, scale_y, offset) = c.calc_scaling(min, max, 100); 236 | let y = c.val_to_y(0.0, offset, scale_y, min, max); 237 | assert_eq!(y, 0); 238 | let y = c.val_to_y(0.5, offset, scale_y, min, max); 239 | assert_eq!(y, 10); 240 | let y = c.val_to_y(1.0, offset, scale_y, min, max); 241 | assert_eq!(y, 20); 242 | } 243 | 244 | #[test] 245 | fn out_of_range_values_are_caught() { 246 | let canvas: CanvasRef = Canvas::new(100, 21); 247 | let c = canvas.borrow(); 248 | 249 | let (min, max) = (-1.0, 1.0); 250 | let (_, scale_y, offset) = c.calc_scaling(min, max, 100); 251 | let y = c.val_to_y(-1.1, offset, scale_y, min, max); 252 | assert_eq!(y, 0); 253 | let y = c.val_to_y(1.1, offset, scale_y, min, max); 254 | assert_eq!(y, 20); 255 | 256 | let (min, max) = (0.0, 1.0); 257 | let (_, scale_y, offset) = c.calc_scaling(min, max, 100); 258 | let y = c.val_to_y(-0.1, offset, scale_y, min, max); 259 | assert_eq!(y, 0); 260 | let y = c.val_to_y(1.1, offset, scale_y, min, max); 261 | assert_eq!(y, 20); 262 | } 263 | -------------------------------------------------------------------------------- /src/tui/color_scheme.rs: -------------------------------------------------------------------------------- 1 | use termion::color::AnsiValue; 2 | 3 | use std::cmp; 4 | 5 | #[derive(Debug)] 6 | pub struct ColorScheme { 7 | pub fg_base: AnsiValue, 8 | pub fg_base_l: AnsiValue, 9 | pub fg_compl: AnsiValue, 10 | pub fg_compl_l: AnsiValue, 11 | pub bg_base: AnsiValue, 12 | pub bg_base_l: AnsiValue, 13 | pub bg_compl: AnsiValue, 14 | pub bg_compl_l: AnsiValue, 15 | } 16 | 17 | impl ColorScheme { 18 | pub fn new() -> ColorScheme { 19 | ColorScheme::light() 20 | } 21 | 22 | pub fn light() -> ColorScheme { 23 | ColorScheme { 24 | fg_base: AnsiValue(16), // 16 = black 25 | fg_base_l: AnsiValue(244), // 240 = dark grey 26 | fg_compl: AnsiValue(0), // 231 = white 27 | fg_compl_l: AnsiValue(8), // 188 = light grey 28 | bg_base: AnsiValue(231), 29 | bg_base_l: AnsiValue(188), 30 | bg_compl: AnsiValue(231), 31 | bg_compl_l: AnsiValue(188), 32 | } 33 | } 34 | 35 | pub fn dark() -> ColorScheme { 36 | ColorScheme { 37 | fg_base: AnsiValue(15), // 15 = White 38 | fg_base_l: AnsiValue(7), // 7 = Light Grey 39 | fg_compl: AnsiValue(0), // 0 = Black 40 | fg_compl_l: AnsiValue(8), // 8 = Dark Gray 41 | bg_base: AnsiValue(0), 42 | bg_base_l: AnsiValue(8), 43 | bg_compl: AnsiValue(15), 44 | bg_compl_l: AnsiValue(7), 45 | } 46 | } 47 | 48 | pub fn amber() -> ColorScheme { 49 | ColorScheme { 50 | fg_base: AnsiValue(208), 51 | fg_base_l: AnsiValue(214), 52 | fg_compl: AnsiValue(208), 53 | fg_compl_l: AnsiValue(214), 54 | bg_base: AnsiValue(0), 55 | bg_base_l: AnsiValue(8), 56 | bg_compl: AnsiValue(0), 57 | bg_compl_l: AnsiValue(8), 58 | } 59 | } 60 | 61 | // Get the highest color value in the schema. 62 | // For checking if this schema can be used with the current terminal. 63 | pub fn max_color(&self) -> u8 { 64 | let mut max = 0u8; 65 | max = cmp::max(self.fg_base.0, max); 66 | max = cmp::max(self.fg_base_l.0, max); 67 | max = cmp::max(self.fg_compl.0, max); 68 | max = cmp::max(self.fg_compl_l.0, max); 69 | max = cmp::max(self.bg_base.0, max); 70 | max = cmp::max(self.bg_base_l.0, max); 71 | max = cmp::max(self.bg_compl.0, max); 72 | max = cmp::max(self.bg_compl_l.0, max); 73 | max 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/tui/container.rs: -------------------------------------------------------------------------------- 1 | use super::Index; 2 | use super::Printer; 3 | use super::ColorScheme; 4 | use super::{Widget, WidgetProperties, WidgetRef}; 5 | 6 | use std::cell::RefCell; 7 | use std::hash::Hash; 8 | use std::rc::Rc; 9 | use std::fmt; 10 | 11 | pub type ContainerRef = Rc>>; 12 | 13 | pub const JOIN_NONE: u32 = 0x00; 14 | pub const JOIN_LEFT: u32 = 0x01; 15 | pub const JOIN_UP: u32 = 0x02; 16 | pub const JOIN_LEFT_UP: u32 = 0x03; 17 | pub const JOIN_RIGHT: u32 = 0x04; 18 | pub const JOIN_RIGHT_UP: u32 = 0x06; 19 | pub const JOIN_DOWN: u32 = 0x08; 20 | pub const JOIN_LEFT_DOWN: u32 = 0x09; 21 | pub const JOIN_RIGHT_DOWN: u32 = 0x0C; 22 | 23 | pub const MASK_LEFT_UP: u32 = 0x03; 24 | pub const MASK_RIGHT_UP: u32 = 0x06; 25 | pub const MASK_LEFT_DOWN: u32 = 0x09; 26 | pub const MASK_RIGHT_DOWN: u32 = 0x0C; 27 | 28 | pub struct Container { 29 | title: String, 30 | props: WidgetProperties, 31 | draw_border: bool, 32 | join_border: [u32; 4], // Bitmask with borders to join to neighbor 33 | children: Vec>, 34 | } 35 | 36 | impl fmt::Debug for Container { 37 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 38 | f.debug_struct("Container") 39 | .field("title", &self.title) 40 | .field("props.x", &self.props.pos_x) 41 | .field("props.y", &self.props.pos_y) 42 | .field("props.width", &self.props.width) 43 | .field("props.height", &self.props.height) 44 | .field("draw_border", &self.draw_border) 45 | .finish() 46 | } 47 | } 48 | 49 | impl Container { 50 | pub fn new() -> Container { 51 | let title = "".to_string(); 52 | let props = WidgetProperties::new(0, 0); 53 | let draw_border = false; 54 | let join_border = [0; 4]; 55 | let children = vec!{}; 56 | Container{title, props, draw_border, join_border, children} 57 | } 58 | 59 | pub fn join_border(&mut self, upper_left: u32, upper_right: u32, lower_left: u32, lower_right:u32) { 60 | self.join_border[0] = upper_left; 61 | self.join_border[1] = upper_right; 62 | self.join_border[2] = lower_left; 63 | self.join_border[3] = lower_right; 64 | } 65 | 66 | pub fn add_child + 'static>(&mut self, child: Rc>, pos_x: Index, pos_y: Index) { 67 | // Check if we need to leave space for drawing the border 68 | let (x_offset, y_offset) = if self.draw_border { (1, 1) } else { (0, 0) }; 69 | // Update child with current absolute position 70 | child.borrow_mut().set_position(self.props.pos_x + pos_x + x_offset, 71 | self.props.pos_y + pos_y + y_offset); 72 | let (child_width, child_height) = child.borrow().get_size(); 73 | let x_size = pos_x + child_width + x_offset * 2; 74 | let y_size = pos_y + child_height + y_offset * 2; 75 | let (width, height) = self.props.get_size(); 76 | if x_size > width { 77 | self.props.set_width(x_size); 78 | } 79 | if y_size > height { 80 | self.props.set_height(y_size); 81 | } 82 | self.children.push(child); 83 | } 84 | 85 | pub fn enable_border(&mut self, enable: bool) { 86 | if enable && !self.draw_border { 87 | self.props.set_width(self.props.get_width() + 2); 88 | self.props.set_height(self.props.get_height() + 2); 89 | } else if !enable && self.draw_border { 90 | self.props.set_width(self.props.get_width() - 2); 91 | self.props.set_height(self.props.get_height() - 2); 92 | } 93 | self.draw_border = enable; 94 | } 95 | 96 | pub fn set_title(&mut self, title: String) { 97 | self.title = format!("┤ {} ├", title); 98 | } 99 | 100 | fn draw_border(&self, p: &mut dyn Printer) { 101 | let mut buff = String::with_capacity(self.props.width * 4); 102 | p.set_color(self.props.colors.fg_base, self.props.colors.bg_base); 103 | 104 | // Overall position of frame 105 | let x_start = self.props.pos_x; 106 | let x_end = x_start + self.props.width; 107 | let y_start = self.props.pos_y; 108 | let y_end = y_start + self.props.height - 1; 109 | 110 | // Calculate position and width of container title, if any 111 | let title_len = if self.title.len() > 4 { self.title.len() - 4 } else { 0 }; // Unicode chars have wrong len. TODO: Handle case where title is longer than width 112 | let x_middle_left = x_start + (self.props.width / 2) - (title_len / 2) as Index; 113 | let mut x_middle_right = x_start + (self.props.width / 2) + (title_len / 2) as Index; 114 | if (x_middle_left - x_start) + title_len as Index + (x_end - x_middle_right) > self.props.width { 115 | x_middle_right += 1; 116 | } 117 | 118 | // Draw upper line and title 119 | // TODO: match can be replaced by array lookup with corner bitmap 120 | match self.join_border[0] { 121 | JOIN_NONE => buff.push('┌'), 122 | JOIN_LEFT => buff.push('┬'), 123 | JOIN_UP => buff.push('├'), 124 | JOIN_LEFT_UP => buff.push('┼'), 125 | _ => panic!("Unexpected border condition {}", self.join_border[0]), 126 | } 127 | for _x in (x_start + 1)..(x_middle_left) { 128 | buff.push('─'); 129 | } 130 | buff.push_str(&self.title); 131 | for _x in (x_middle_right)..(x_end) { 132 | buff.push('─'); 133 | } 134 | match self.join_border[1] { 135 | JOIN_NONE => buff.push('┐'), 136 | JOIN_RIGHT => buff.push('┬'), 137 | JOIN_UP => buff.push('┤'), 138 | JOIN_RIGHT_UP => buff.push('┼'), 139 | _ => panic!("Unexpected border condition {}", self.join_border[1]), 140 | } 141 | p.print(self.props.pos_x, self.props.pos_y, &buff); 142 | buff.clear(); 143 | 144 | // Draw side lines 145 | for y in (y_start + 1)..(y_end) { 146 | p.print(x_start, y, "│"); 147 | p.print(x_end, y, "│"); 148 | } 149 | 150 | // Draw lower line 151 | match self.join_border[2] { 152 | JOIN_NONE => buff.push('└'), 153 | JOIN_LEFT => buff.push('┴'), 154 | JOIN_DOWN => buff.push('├'), 155 | JOIN_LEFT_DOWN => buff.push('┼'), 156 | _ => panic!("Unexpected border condition {}", self.join_border[2]), 157 | } 158 | for _x in (x_start + 1)..(x_end) { 159 | buff.push('─'); 160 | } 161 | match self.join_border[3] { 162 | JOIN_NONE => buff.push('┘'), 163 | JOIN_RIGHT => buff.push('┴'), 164 | JOIN_DOWN => buff.push('┤'), 165 | JOIN_RIGHT_DOWN => buff.push('┼'), 166 | _ => panic!("Unexpected border condition {}", self.join_border[3]), 167 | } 168 | p.print(self.props.pos_x, y_end, &buff); 169 | } 170 | 171 | /** Get widget at given position. */ 172 | pub fn get_at_pos(&self, x: Index, y: Index) -> Option { 173 | if self.is_inside(x, y) { 174 | for c in self.children.iter() { 175 | let result = c.borrow().get_at_pos(x, y); 176 | if result.is_some() { return result; }; 177 | } 178 | } 179 | None 180 | } 181 | } 182 | 183 | impl Widget for Container { 184 | // TODO: Implement dynamic resizing of children 185 | 186 | fn get_widget_properties_mut(&mut self) -> &mut WidgetProperties { 187 | &mut self.props 188 | } 189 | 190 | fn get_widget_properties(&self) -> &WidgetProperties { 191 | &self.props 192 | } 193 | 194 | /** Set the container's and all its children's position. 195 | * 196 | * TODO: Check that new position is valid 197 | */ 198 | fn set_position(&mut self, x: Index, y: Index) -> bool { 199 | let (x_old, y_old) = self.props.get_position(); 200 | let x_diff = (x as i32) - (x_old as i32); 201 | let y_diff = (y as i32) - (y_old as i32); 202 | self.props.set_position(x, y); 203 | for child in self.children.iter_mut() { 204 | let (x_child, y_child) = child.borrow().get_position(); 205 | let x_new = (x_child as i32) + x_diff; 206 | let y_new = (y_child as i32) + y_diff; 207 | child.borrow_mut().set_position(x_new as Index, y_new as Index); 208 | } 209 | true 210 | } 211 | 212 | fn set_color_scheme(&mut self, colors: Rc) { 213 | for c in self.children.iter_mut() { 214 | c.borrow_mut().set_color_scheme(colors.clone()); 215 | } 216 | self.get_widget_properties_mut().set_color_scheme(colors); 217 | } 218 | 219 | fn set_dirty(&mut self, is_dirty: bool) { 220 | self.props.dirty = is_dirty; 221 | for child in self.children.iter() { 222 | child.borrow_mut().set_dirty(is_dirty); 223 | } 224 | } 225 | 226 | fn is_dirty(&self) -> bool { 227 | for child in self.children.iter() { 228 | if child.borrow().is_dirty() { 229 | return true; 230 | } 231 | } 232 | false 233 | } 234 | 235 | fn draw(&self, printer: &mut dyn Printer) { 236 | // Only draw border if container has explicitly been marked as dirty 237 | // (used for full redraw). 238 | if self.props.dirty && self.draw_border { 239 | self.draw_border(printer); 240 | } 241 | for child in self.children.iter() { 242 | if child.borrow().is_dirty() { 243 | child.borrow_mut().draw(printer); 244 | } 245 | } 246 | } 247 | } 248 | 249 | // ---------- 250 | // Unit tests 251 | // ---------- 252 | 253 | #[cfg(test)] 254 | 255 | use super::{Label}; // Needed for tests. TODO: There's got to be a better way 256 | 257 | fn validate_properties(c: &Container, pos_x: Index, pos_y: Index, width: Index, height: Index) -> bool { 258 | if c.props.pos_x == pos_x 259 | && c.props.pos_y == pos_y 260 | && c.props.width == width 261 | && c.props.height == height { 262 | true 263 | } else { 264 | println!("\n{:?}", c); 265 | false 266 | } 267 | } 268 | 269 | #[test] 270 | fn size_changes_when_child_added_no_border() { 271 | let mut c: Container = Container::new(); 272 | assert!(validate_properties(&c, 0, 0, 0, 0)); 273 | c.add_child(Label::new("12345".to_string(), 5), 0, 0); 274 | assert!(validate_properties(&c, 0, 0, 5, 1)); 275 | } 276 | 277 | #[test] 278 | fn size_changes_when_child_added_border() { 279 | let mut c: Container = Container::new(); 280 | c.enable_border(true); 281 | assert!(validate_properties(&c, 0, 0, 2, 2)); 282 | c.add_child(Label::new("12345".to_string(), 5), 0, 0); 283 | assert!(validate_properties(&c, 0, 0, 7, 3)); 284 | c.add_child(Label::new("12345".to_string(), 5), 0, 1); 285 | assert!(validate_properties(&c, 0, 0, 7, 4)); 286 | } 287 | -------------------------------------------------------------------------------- /src/tui/controller.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::rc::Rc; 3 | use std::cell::RefCell; 4 | use std::cmp::Eq; 5 | use std::hash::Hash; 6 | 7 | use super::{MouseMessage, Observer, Value}; 8 | 9 | pub struct Controller { 10 | pub observers: HashMap>>, 11 | } 12 | 13 | impl Controller { 14 | pub fn new() -> Controller { 15 | let observers = HashMap::new(); 16 | Controller{observers} 17 | } 18 | 19 | pub fn add_observer(&mut self, key: &Key, observer: Rc>) { 20 | self.observers.insert(*key, observer); 21 | } 22 | 23 | pub fn update(&mut self, key: &Key, value: Value) { 24 | self.observers.entry(*key).and_modify(|e| e.borrow_mut().update(value)); 25 | } 26 | 27 | pub fn handle_mouse_event(&self, _key: &Key, _msg: &MouseMessage) { 28 | //self.observers.entry(*key).and_modify(|e| e.borrow_mut().handle_mouse_event(msg)); 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/tui/dial.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::hash::Hash; 3 | use std::rc::Rc; 4 | 5 | use super::Observer; 6 | use super::{Value, get_int, get_float}; 7 | use super::{Printer, Widget, WidgetProperties}; 8 | 9 | type DialRef = Rc>>; 10 | 11 | /** A circular dial representing a value. 12 | * 13 | * Can have logarithmic scaling to improve visibility of smaller values. 14 | */ 15 | pub struct Dial { 16 | props: WidgetProperties, 17 | min: Value, 18 | max: Value, 19 | value: Value, 20 | logarithmic: bool, // Use logarithmic curve for values 21 | } 22 | 23 | impl Dial { 24 | pub fn new(min: Value, max: Value, value: Value) -> DialRef { 25 | let width = 2; 26 | let height = 2; 27 | let props = WidgetProperties::new(width, height); 28 | let logarithmic = false; 29 | Rc::new(RefCell::new(Dial{props, min, max, value, logarithmic})) 30 | } 31 | 32 | pub fn set_logarithmic(&mut self, l: bool) { 33 | self.logarithmic = l; 34 | } 35 | 36 | pub fn get_index(&self, value: &Value) -> usize { 37 | let min: f64; 38 | let max: f64; 39 | let fvalue: f64; 40 | match value { 41 | Value::Int(v) => { 42 | min = get_int(&self.min) as f64; 43 | max = get_int(&self.max) as f64; 44 | fvalue = *v as f64; 45 | } 46 | Value::Float(v) => { 47 | min = get_float(&self.min); 48 | max = get_float(&self.max); 49 | fvalue = *v; 50 | } 51 | Value::Str(_) => panic!(), 52 | } 53 | let offset = min * -1.0; 54 | let range = max - min; 55 | let scale = 8.0 / range; 56 | let mut value = fvalue + offset; 57 | if self.logarithmic { 58 | // Using a logarithmic curve makes smaller values easier to see. 59 | let percent = value / range; 60 | let factor = percent.sqrt().sqrt(); // TODO: Slow, find a nicer way 61 | value = factor * range; 62 | } 63 | (value * scale) as usize 64 | } 65 | } 66 | 67 | impl Widget for Dial { 68 | fn get_widget_properties_mut(&mut self) -> &mut WidgetProperties { 69 | &mut self.props 70 | } 71 | 72 | fn get_widget_properties(&self) -> &WidgetProperties { 73 | &self.props 74 | } 75 | 76 | fn draw(&self, p: &mut dyn Printer) { 77 | let index = self.get_index(&self.value); 78 | // TODO: Optimize by using array 79 | let chars = match index { 80 | 0 => " ", 81 | 1 => " ", 82 | 2 => "▁ ", 83 | 3 => "\\ ", 84 | 4 => " ▏", 85 | 5 => " /", 86 | 6 => " ▁", 87 | 7 => " ", 88 | _ => " ", 89 | //_ => " ", 90 | }; 91 | p.set_color(self.props.colors.fg_compl_l, self.props.colors.bg_compl_l); 92 | p.print(self.props.pos_x, self.props.pos_y, chars); 93 | let chars = match index { 94 | 0 => "/ ", 95 | 1 => "▔ ", 96 | 2 => " ", 97 | 3 => " ", 98 | 4 => " ", 99 | 5 => " ", 100 | 6 => " ", 101 | 7 => " ▔", 102 | _ => " \\", 103 | //_ => " ▏", 104 | }; 105 | p.print(self.props.pos_x, self.props.pos_y + 1, chars); 106 | } 107 | } 108 | 109 | impl Observer for Dial { 110 | fn update(&mut self, value: Value) { 111 | self.value = value; 112 | self.set_dirty(true); 113 | } 114 | 115 | /* 116 | fn handle_mouse_event(&mut self, msg: &MouseMessage) { 117 | self.value = self.get_widget_properties().update_mouse(msg); 118 | } 119 | */ 120 | } 121 | 122 | #[test] 123 | fn dial_translation() { 124 | // ===== 125 | // Float 126 | // ===== 127 | // Case 1: 0.0 - 1.0 128 | let d: DialRef = Dial::new(Value::Float(0.0), Value::Float(1.0), Value::Float(0.0)); 129 | assert_eq!(d.borrow().get_index(&Value::Float(0.0)), 0); 130 | assert_eq!(d.borrow().get_index(&Value::Float(0.5)), 4); 131 | assert_eq!(d.borrow().get_index(&Value::Float(1.0)), 8); 132 | 133 | // Case 2: -1.0 - 1.0 134 | let d: DialRef = Dial::new(Value::Float(-1.0), Value::Float(1.0), Value::Float(0.0)); 135 | assert_eq!(d.borrow().get_index(&Value::Float(-1.0)), 0); 136 | assert_eq!(d.borrow().get_index(&Value::Float(0.0)), 4); 137 | assert_eq!(d.borrow().get_index(&Value::Float(1.0)), 8); 138 | 139 | // Case 3: 2.0 - 10.0 140 | let d: DialRef = Dial::new(Value::Float(2.0), Value::Float(10.0), Value::Float(0.0)); 141 | assert_eq!(d.borrow().get_index(&Value::Float(2.0)), 0); 142 | assert_eq!(d.borrow().get_index(&Value::Float(6.0)), 4); 143 | assert_eq!(d.borrow().get_index(&Value::Float(10.0)), 8); 144 | 145 | // === 146 | // Int 147 | // === 148 | // Case 1: 0 - 8 149 | let d: DialRef = Dial::new(Value::Int(0), Value::Int(8), Value::Int(0)); 150 | assert_eq!(d.borrow().get_index(&Value::Int(0)), 0); 151 | assert_eq!(d.borrow().get_index(&Value::Int(4)), 4); 152 | assert_eq!(d.borrow().get_index(&Value::Int(8)), 8); 153 | 154 | // Case 2: -4 - 4 155 | let d: DialRef = Dial::new(Value::Int(-4), Value::Int(4), Value::Int(0)); 156 | assert_eq!(d.borrow().get_index(&Value::Int(-4)), 0); 157 | assert_eq!(d.borrow().get_index(&Value::Int(0)), 4); 158 | assert_eq!(d.borrow().get_index(&Value::Int(4)), 8); 159 | 160 | // Case 3: 2 - 10 161 | let d: DialRef = Dial::new(Value::Int(2), Value::Int(10), Value::Int(0)); 162 | assert_eq!(d.borrow().get_index(&Value::Int(2)), 0); 163 | assert_eq!(d.borrow().get_index(&Value::Int(6)), 4); 164 | assert_eq!(d.borrow().get_index(&Value::Int(10)), 8); 165 | } 166 | -------------------------------------------------------------------------------- /src/tui/label.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::hash::Hash; 3 | use std::rc::Rc; 4 | 5 | use super::Index; 6 | use super::Observer; 7 | use super::{Value, get_str}; 8 | use super::{Printer, Widget, WidgetProperties}; 9 | 10 | type LabelRef = Rc>>; 11 | 12 | pub struct Label { 13 | props: WidgetProperties, 14 | value: Value, 15 | } 16 | 17 | impl Label { 18 | pub fn new(value: String, size: Index) -> LabelRef { 19 | let width = size; 20 | let height = 1; 21 | let props = WidgetProperties::new(width, height); 22 | let value = Value::Str(value); 23 | Rc::new(RefCell::new(Label{props, value})) 24 | } 25 | } 26 | 27 | impl Widget for Label { 28 | fn get_widget_properties_mut(&mut self) -> &mut WidgetProperties { 29 | &mut self.props 30 | } 31 | 32 | fn get_widget_properties(&self) -> &WidgetProperties { 33 | &self.props 34 | } 35 | 36 | fn draw(&self, p: &mut dyn Printer) { 37 | p.set_color(self.props.colors.fg_base, self.props.colors.bg_base); 38 | p.print(self.props.pos_x, self.props.pos_y, get_str(&self.value)); 39 | } 40 | } 41 | 42 | impl Observer for Label { 43 | fn update(&mut self, value: Value) { 44 | self.value = value; 45 | self.set_dirty(true); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/tui/marker_manager.rs: -------------------------------------------------------------------------------- 1 | use super::ParamId; 2 | 3 | use log::info; 4 | use serde::{Serialize, Deserialize}; 5 | 6 | use std::collections::HashMap; 7 | use std::fs::File; 8 | use std::io::BufReader; 9 | use std::io::prelude::*; 10 | 11 | #[derive(Clone, Debug, Serialize, Deserialize)] 12 | pub struct MarkerManager { 13 | marker: HashMap, 14 | } 15 | 16 | impl MarkerManager { 17 | pub fn new() -> Self { 18 | let mut mm = MarkerManager{marker: HashMap::new()}; 19 | mm.load().unwrap(); 20 | mm 21 | } 22 | 23 | pub fn add(&mut self, marker: char, param: ParamId) { 24 | self.marker.insert(marker, param); 25 | self.store().unwrap(); 26 | } 27 | 28 | pub fn get(&self, marker: char) -> Option { 29 | if self.marker.contains_key(&marker) { 30 | Some(self.marker[&marker]) 31 | } else { 32 | None 33 | } 34 | } 35 | 36 | pub fn load(&mut self) -> std::io::Result<()> { 37 | let result = File::open("Yazz_Markers.ysn"); 38 | match result { 39 | Ok(file) => { 40 | let mut reader = BufReader::new(file); 41 | let mut serialized = String::new(); 42 | reader.read_to_string(&mut serialized)?; 43 | let result: Result = serde_json::from_str(&serialized); 44 | if let Ok(data) = result { 45 | *self = data; 46 | } 47 | } 48 | Err(err) => info!("Error loading marker file: {}", err), 49 | } 50 | Ok(()) 51 | } 52 | 53 | pub fn store(&self) -> std::io::Result<()> { 54 | let mut file = File::create("Yazz_Markers.ysn")?; 55 | let serialized = serde_json::to_string_pretty(&self).unwrap(); 56 | file.write_all(serialized.as_bytes())?; 57 | Ok(()) 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /src/tui/midi_learn.rs: -------------------------------------------------------------------------------- 1 | use super::MappingType; 2 | use log::info; 3 | 4 | #[derive(Debug)] 5 | pub struct MidiLearn { 6 | pub complete: bool, 7 | pub ctrl: u64, 8 | pub mapping_type: MappingType, 9 | 10 | val1: u64, 11 | val2: u64, 12 | num_events_received: u64 13 | } 14 | 15 | impl MidiLearn { 16 | pub fn new() -> Self { 17 | MidiLearn{ 18 | complete: false, 19 | ctrl: 0, 20 | mapping_type: MappingType::None, 21 | val1: 0, 22 | val2: 0, 23 | num_events_received: 0 24 | } 25 | } 26 | 27 | pub fn reset(&mut self) { 28 | self.complete = false; 29 | self.ctrl = 0; 30 | self.mapping_type = MappingType::None; 31 | self.val1 = 0; 32 | self.val2 = 0; 33 | self.num_events_received = 0; 34 | } 35 | 36 | pub fn handle_controller(&mut self, controller: u64, value: u64) -> bool { 37 | info!("handle_controller: ctrl {} val {}, {} events received", 38 | controller, value, self.num_events_received); 39 | match self.num_events_received { 40 | 0 => { 41 | self.ctrl = controller; 42 | self.val1 = value; 43 | self.num_events_received = 1; 44 | } 45 | 1 => { 46 | if controller == self.ctrl { 47 | let diff = (self.val1 as i64 - value as i64).abs(); 48 | if diff >= 5 { 49 | self.val2 = value; 50 | self.num_events_received = 2; 51 | self.complete = true; 52 | self.mapping_type = if diff.abs() >= 125 { 53 | MappingType::Relative 54 | } else { 55 | MappingType::Absolute 56 | }; 57 | info!("handle_controller: MIDI learn complete"); 58 | } 59 | } else { 60 | self.ctrl = controller; 61 | self.val1 = value; 62 | self.num_events_received = 1; 63 | } 64 | } 65 | _ => panic!() 66 | } 67 | self.complete 68 | } 69 | 70 | pub fn clear_controller(&mut self) { 71 | self.reset(); 72 | self.complete = true; 73 | } 74 | } 75 | 76 | // ---------------------------------------------- 77 | // Unit tests 78 | // ---------------------------------------------- 79 | 80 | #[test] 81 | fn full_absolute_value_can_be_set() { 82 | let mut ml = MidiLearn::new(); 83 | assert_eq!(ml.complete, false); 84 | 85 | ml.handle_controller(7, 10); 86 | assert_eq!(ml.complete, false); 87 | 88 | ml.handle_controller(7, 1); 89 | assert_eq!(ml.complete, true); 90 | assert_eq!(ml.ctrl, 7); 91 | assert_eq!(ml.mapping_type, MappingType::Absolute); 92 | } 93 | 94 | #[test] 95 | fn full_relative_value_can_be_set() { 96 | let mut ml = MidiLearn::new(); 97 | assert_eq!(ml.complete, false); 98 | 99 | ml.handle_controller(7, 127); 100 | assert_eq!(ml.complete, false); 101 | 102 | ml.handle_controller(7, 0); 103 | assert_eq!(ml.complete, true); 104 | assert_eq!(ml.ctrl, 7); 105 | assert_eq!(ml.mapping_type, MappingType::Relative); 106 | } 107 | 108 | #[test] 109 | fn same_values_are_not_counted() { 110 | let mut ml = MidiLearn::new(); 111 | ml.handle_controller(7, 5); 112 | assert_eq!(ml.complete, false); 113 | ml.handle_controller(7, 5); 114 | assert_eq!(ml.complete, false); 115 | } 116 | 117 | #[test] 118 | fn reset_works_after_full_value() { 119 | let mut ml = MidiLearn::new(); 120 | ml.handle_controller(7, 10); 121 | ml.handle_controller(7, 1); 122 | assert_eq!(ml.complete, true); 123 | ml.reset(); 124 | assert_eq!(ml.complete, false); 125 | } 126 | 127 | #[test] 128 | fn reset_works_after_partial_value() { 129 | let mut ml = MidiLearn::new(); 130 | ml.handle_controller(7, 5); 131 | ml.reset(); 132 | ml.handle_controller(7, 1); 133 | assert_eq!(ml.complete, false); 134 | } 135 | -------------------------------------------------------------------------------- /src/tui/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod termion_wrapper; 2 | pub mod tui; 3 | 4 | mod bar; 5 | mod button; 6 | mod canvas; 7 | mod color_scheme; 8 | mod container; 9 | mod controller; 10 | mod dial; 11 | mod display; 12 | mod item_selection; 13 | mod label; 14 | mod marker_manager; 15 | mod midi_learn; 16 | mod mouse; 17 | mod observer; 18 | mod printer; 19 | mod select; 20 | mod slider; 21 | mod statemachine; 22 | mod stdio_printer; 23 | mod surface; 24 | mod value; 25 | mod value_display; 26 | mod widget; 27 | 28 | use bar::Bar; 29 | use button::Button; 30 | use canvas::{Canvas, CanvasRef}; 31 | use color_scheme::ColorScheme; 32 | use container::{Container, ContainerRef}; 33 | use controller::Controller; 34 | use dial::Dial; 35 | use display::Display; 36 | use item_selection::ItemSelection; 37 | use label::Label; 38 | use marker_manager::MarkerManager; 39 | use midi_learn::MidiLearn; 40 | use mouse::{MouseHandler, MouseMessage}; 41 | use observer::{Observer, ObserverRef}; 42 | use printer::Printer; 43 | pub use printer::Index; 44 | use select::{RetCode, SelectorEvent, SelectorState, ParamSelector, next}; 45 | use slider::Slider; 46 | use statemachine::{StateMachine, SmEvent, SmResult}; 47 | use stdio_printer::StdioPrinter; 48 | pub use tui::Tui; 49 | use termion_wrapper::TermionWrapper; 50 | use value::{Value, get_int, get_float, get_str}; 51 | use value_display::ValueDisplay; 52 | pub use widget::{Widget, WidgetProperties, WidgetRef}; 53 | 54 | use super::{CtrlMap, MappingType}; 55 | use super::Float; 56 | use super::MidiMessage; 57 | use super::SoundData; 58 | use super::{SoundBank, SoundPatch}; 59 | use super::SynthMessage; 60 | use super::{Parameter, ParameterValue, ParamId, FunctionId, SynthParam, MenuItem, FUNCTIONS, MOD_SOURCES}; 61 | use super::UiMessage; 62 | use super::WtInfo; 63 | use super::{SOUND_DATA_VERSION, SYNTH_ENGINE_VERSION}; 64 | use super::value_range::ValueRange; 65 | -------------------------------------------------------------------------------- /src/tui/mouse.rs: -------------------------------------------------------------------------------- 1 | use super::{Container, Controller, Index}; 2 | 3 | use log::info; 4 | 5 | use std::cmp::Eq; 6 | use std::hash::Hash; 7 | 8 | #[derive(Debug)] 9 | enum MhState { 10 | Idle, 11 | Clicked 12 | } 13 | 14 | #[derive(Debug)] 15 | pub enum MouseMessage { 16 | Press{x: Index, y: Index}, 17 | Hold{x: Index, y: Index}, 18 | Release{x: Index, y: Index}, 19 | } 20 | 21 | pub struct MouseHandler { 22 | state: MhState, 23 | current_key: Option, 24 | } 25 | 26 | impl MouseHandler { 27 | pub fn new() -> MouseHandler { 28 | let current_key = None; 29 | MouseHandler{state: MhState::Idle, current_key} 30 | } 31 | 32 | pub fn handle_event(&mut self, msg: &MouseMessage, window: &Container, controller: &Controller) { 33 | match self.state { 34 | MhState::Idle => self.idle_state(msg, window, controller), 35 | MhState::Clicked => self.clicked_state(msg, window, controller), 36 | } 37 | } 38 | 39 | fn idle_state(&mut self, msg: &MouseMessage, window: &Container, _controller: &Controller) { 40 | match msg { 41 | MouseMessage::Press{x, y} => { 42 | // Select widget to be updated 43 | self.change_state(MhState::Clicked); 44 | self.current_key = window.get_at_pos(*x, *y); 45 | //controller.handle_mouse_event(&self.current_key.unwrap(), msg); 46 | }, 47 | MouseMessage::Hold{x: _, y: _} | MouseMessage::Release{x: _, y: _} => {}, 48 | } 49 | } 50 | 51 | fn clicked_state(&mut self, msg: &MouseMessage, _window: &Container, _controller: &Controller) { 52 | match msg { 53 | MouseMessage::Press{x: _, y: _} => {}, 54 | MouseMessage::Hold{x: _, y: _} => { 55 | // Update selected widget 56 | }, 57 | MouseMessage::Release{x: _, y: _} => { 58 | // Finished with widget value change 59 | self.change_state(MhState::Idle); 60 | }, 61 | } 62 | } 63 | 64 | fn change_state(&mut self, new_state: MhState) { 65 | info!("Mouse: Change state {:?} -> {:?}", self.state, new_state); 66 | self.state = new_state; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/tui/observer.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::rc::Rc; 3 | 4 | use super::Value; 5 | 6 | pub type ObserverRef = Rc>; 7 | 8 | pub trait Observer { 9 | fn update(&mut self, value: Value); 10 | 11 | //fn handle_mouse_event(&mut self, msg: &MouseMessage); 12 | } 13 | -------------------------------------------------------------------------------- /src/tui/printer.rs: -------------------------------------------------------------------------------- 1 | use termion::color::AnsiValue; 2 | 3 | pub type Index = usize; 4 | 5 | pub trait Printer { 6 | 7 | // Set foreground and background color. 8 | // Color stays set until changed again. 9 | fn set_color(&mut self, fg_color: AnsiValue, bg_color: AnsiValue); 10 | 11 | // Print some text (might not update the screen) 12 | fn print(&mut self, x: Index, y: Index, text: &str); 13 | 14 | // Update the screen contents 15 | fn update(&mut self); 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/tui/slider.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::hash::Hash; 3 | use std::rc::Rc; 4 | 5 | use super::Observer; 6 | use super::{Value, get_int, get_float}; 7 | use super::{Printer, Widget, WidgetProperties}; 8 | 9 | pub type SliderRef = Rc>>; 10 | 11 | pub struct Slider { 12 | props: WidgetProperties, 13 | min: Value, 14 | max: Value, 15 | value: Value, 16 | logarithmic: bool, // Use logarithmic curve for values 17 | } 18 | 19 | impl Slider { 20 | pub fn new(min: Value, max: Value, value: Value) -> SliderRef { 21 | let width = 1; 22 | let height = 5; 23 | let props = WidgetProperties::new(width, height); 24 | let logarithmic = false; 25 | Rc::new(RefCell::new(Slider{props, min, max, value, logarithmic})) 26 | } 27 | 28 | pub fn get_index(&self, value: &Value) -> usize { 29 | let min: f64; 30 | let max: f64; 31 | let fvalue: f64; 32 | match value { 33 | Value::Int(v) => { 34 | min = get_int(&self.min) as f64; 35 | max = get_int(&self.max) as f64; 36 | fvalue = *v as f64; 37 | } 38 | Value::Float(v) => { 39 | min = get_float(&self.min); 40 | max = get_float(&self.max); 41 | fvalue = *v; 42 | } 43 | Value::Str(_) => panic!(), 44 | } 45 | let offset = min * -1.0; 46 | let range = max - min; 47 | let scale = (self.props.height * 8) as f64 / range; 48 | let mut value = fvalue + offset; 49 | if self.logarithmic { 50 | // Using a logarithmic curve makes smaller values easier to see. 51 | let percent = value / range; 52 | let factor = percent.sqrt().sqrt(); // TODO: Slow, find a nicer way 53 | value = factor * range; 54 | } 55 | (value * scale) as usize 56 | } 57 | 58 | pub fn set_logarithmic(&mut self, l: bool) { 59 | self.logarithmic = l; 60 | } 61 | } 62 | 63 | impl Widget for Slider { 64 | fn get_widget_properties_mut(&mut self) -> &mut WidgetProperties { 65 | &mut self.props 66 | } 67 | 68 | fn get_widget_properties(&self) -> &WidgetProperties { 69 | &self.props 70 | } 71 | 72 | fn draw(&self, p: &mut dyn Printer) { 73 | let mut index = self.get_index(&self.value); 74 | p.set_color(self.props.colors.fg_compl_l, self.props.colors.bg_compl_l); 75 | for i in 0..self.props.height { 76 | let chars = if index >= 8 { "█" } else { 77 | match index % 8 { 78 | 0 => " ", 79 | 1 => "▁", 80 | 2 => "▂", 81 | 3 => "▃", 82 | 4 => "▄", 83 | 5 => "▅", 84 | 6 => "▆", 85 | 7 => "▇", 86 | _ => panic!(), 87 | } 88 | }; 89 | index = if index > 8 { index - 8 } else { 0 }; 90 | p.print(self.props.pos_x, self.props.pos_y + (4 - i), chars); 91 | } 92 | } 93 | } 94 | 95 | impl Observer for Slider { 96 | fn update(&mut self, value: Value) { 97 | self.value = value; 98 | self.set_dirty(true); 99 | } 100 | } 101 | 102 | #[test] 103 | fn test_slider_translation() { 104 | // ===== 105 | // Float 106 | // ===== 107 | // Case 1: 0.0 - 1.0 108 | // Slider has default height of 4, so a value range of 0 - 32 109 | let d: SliderRef = Slider::new(Value::Float(0.0), Value::Float(1.0), Value::Float(0.0)); 110 | assert_eq!(d.borrow().get_index(&Value::Float(0.0)), 0); 111 | assert_eq!(d.borrow().get_index(&Value::Float(0.5)), 16); 112 | assert_eq!(d.borrow().get_index(&Value::Float(1.0)), 32); 113 | 114 | // Case 2: -1.0 - 1.0 115 | let d: SliderRef = Slider::new(Value::Float(-1.0), Value::Float(1.0), Value::Float(0.0)); 116 | assert_eq!(d.borrow().get_index(&Value::Float(-1.0)), 0); 117 | assert_eq!(d.borrow().get_index(&Value::Float(0.0)), 16); 118 | assert_eq!(d.borrow().get_index(&Value::Float(1.0)), 32); 119 | 120 | // Case 3: 2.0 - 10.0 121 | let d: SliderRef = Slider::new(Value::Float(2.0), Value::Float(10.0), Value::Float(0.0)); 122 | assert_eq!(d.borrow().get_index(&Value::Float(2.0)), 0); 123 | assert_eq!(d.borrow().get_index(&Value::Float(6.0)), 16); 124 | assert_eq!(d.borrow().get_index(&Value::Float(10.0)), 32); 125 | 126 | // === 127 | // Int 128 | // === 129 | // Case 1: 0 - 8 130 | let d: SliderRef = Slider::new(Value::Int(0), Value::Int(8), Value::Int(0)); 131 | assert_eq!(d.borrow().get_index(&Value::Int(0)), 0); 132 | assert_eq!(d.borrow().get_index(&Value::Int(4)), 16); 133 | assert_eq!(d.borrow().get_index(&Value::Int(8)), 32); 134 | 135 | // Case 2: -4 - 4 136 | let d: SliderRef = Slider::new(Value::Int(-4), Value::Int(4), Value::Int(0)); 137 | assert_eq!(d.borrow().get_index(&Value::Int(-4)), 0); 138 | assert_eq!(d.borrow().get_index(&Value::Int(0)), 16); 139 | assert_eq!(d.borrow().get_index(&Value::Int(4)), 32); 140 | 141 | // Case 3: 2 - 10 142 | let d: SliderRef = Slider::new(Value::Int(2), Value::Int(10), Value::Int(0)); 143 | assert_eq!(d.borrow().get_index(&Value::Int(2)), 0); 144 | assert_eq!(d.borrow().get_index(&Value::Int(6)), 16); 145 | assert_eq!(d.borrow().get_index(&Value::Int(10)), 32); 146 | } 147 | -------------------------------------------------------------------------------- /src/tui/statemachine.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub enum SmEvent { 3 | EnterState, 4 | ExitState, 5 | Event(E), 6 | } 7 | 8 | pub enum SmResult { 9 | EventHandled, 10 | ChangeState(fn(&mut C, &SmEvent) -> SmResult), 11 | Error, 12 | } 13 | 14 | impl std::fmt::Debug for SmResult { 15 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 16 | write!(f, "SmResult") 17 | } 18 | } 19 | 20 | pub struct StateMachine { 21 | pub current_state: fn(&mut C, &SmEvent) -> SmResult, 22 | } 23 | 24 | impl StateMachine { 25 | pub fn new(initial_state: fn(&mut C, &SmEvent) -> SmResult) -> StateMachine { 26 | StateMachine{current_state: initial_state} 27 | } 28 | 29 | pub fn init(&mut self, context: &mut C) { 30 | self.handle_event(context, &SmEvent::EnterState) 31 | } 32 | 33 | pub fn handle_event(&mut self, context: &mut C, event: &SmEvent) { 34 | let result = (self.current_state)(context, event); 35 | match result { 36 | SmResult::EventHandled => {}, 37 | SmResult::ChangeState(new_state) => { 38 | (self.current_state)(context, &SmEvent::ExitState); 39 | self.current_state = new_state; 40 | (self.current_state)(context, &SmEvent::EnterState); 41 | } 42 | SmResult::Error => panic!("Error handling event") 43 | } 44 | } 45 | } 46 | 47 | impl std::fmt::Debug for StateMachine { 48 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 49 | write!(f, "StateMachine") 50 | } 51 | } 52 | 53 | 54 | // ---------------------------------------------- 55 | // Unit tests 56 | // ---------------------------------------------- 57 | 58 | #[cfg(test)] 59 | mod tests { 60 | use super::SmEvent; 61 | use super::SmResult; 62 | use super::StateMachine; 63 | 64 | #[derive(Debug, PartialEq)] 65 | enum TestState { 66 | Init, 67 | Entered, 68 | Handled, 69 | Exited 70 | } 71 | 72 | /* Test object that records the state machine progress for feedback */ 73 | struct TestApp { 74 | state1: TestState, 75 | state2: TestState, 76 | state3: TestState, 77 | last_value: i32 78 | } 79 | 80 | /* Test object that implements the state machine functions (states) */ 81 | impl TestApp { 82 | fn new() -> TestApp { 83 | TestApp{ 84 | state1: TestState::Init, 85 | state2: TestState::Init, 86 | state3: TestState::Init, 87 | last_value: 0, 88 | } 89 | } 90 | 91 | fn state_1(context: &mut TestApp, event: &SmEvent) -> SmResult { 92 | match event { 93 | SmEvent::EnterState => {context.state1 = TestState::Entered; SmResult::EventHandled}, 94 | SmEvent::ExitState => {context.state1 = TestState::Exited; SmResult::EventHandled}, 95 | SmEvent::Event(e) => { 96 | match e { 97 | 2 => SmResult::ChangeState(TestApp::state_2), 98 | 3 => SmResult::ChangeState(TestApp::state_3), 99 | _ => { 100 | context.last_value = *e; 101 | context.state1 = TestState::Handled; 102 | SmResult::EventHandled 103 | } 104 | } 105 | }, 106 | } 107 | } 108 | 109 | fn state_2(context: &mut TestApp, event: &SmEvent) -> SmResult { 110 | match event { 111 | SmEvent::EnterState => {context.state2 = TestState::Entered; SmResult::EventHandled}, 112 | SmEvent::ExitState => {context.state2 = TestState::Exited; SmResult::EventHandled}, 113 | SmEvent::Event(e) => { 114 | match e { 115 | 1 => SmResult::ChangeState(TestApp::state_1), 116 | 3 => SmResult::ChangeState(TestApp::state_3), 117 | _ => { 118 | context.last_value = *e; 119 | context.state1 = TestState::Handled; 120 | SmResult::EventHandled 121 | } 122 | } 123 | }, 124 | } 125 | } 126 | 127 | fn state_3(context: &mut TestApp, event: &SmEvent) -> SmResult { 128 | match event { 129 | SmEvent::EnterState => {context.state3 = TestState::Entered; SmResult::EventHandled}, 130 | SmEvent::ExitState => {context.state3 = TestState::Exited; SmResult::EventHandled}, 131 | SmEvent::Event(e) => { 132 | match e { 133 | 1 => SmResult::ChangeState(TestApp::state_1), 134 | 2 => SmResult::ChangeState(TestApp::state_2), 135 | _ => { 136 | context.last_value = *e; 137 | context.state1 = TestState::Handled; 138 | SmResult::EventHandled 139 | } 140 | } 141 | }, 142 | } 143 | } 144 | } 145 | 146 | struct TestContext { 147 | sm: StateMachine, 148 | app: TestApp, 149 | } 150 | 151 | impl TestContext { 152 | fn new() -> TestContext { 153 | let mut tc = TestContext{sm: StateMachine::new(TestApp::state_1), 154 | app: TestApp::new()}; 155 | tc.sm.init(&mut tc.app); 156 | tc 157 | } 158 | } 159 | 160 | #[test] 161 | fn initial_state_can_be_entered() { 162 | let context = TestContext::new(); 163 | 164 | assert_eq!(context.app.state1, TestState::Entered); 165 | assert_eq!(context.app.state2, TestState::Init); 166 | assert_eq!(context.app.state3, TestState::Init); 167 | } 168 | 169 | #[test] 170 | fn events_are_handled() { 171 | let mut context = TestContext::new(); 172 | 173 | assert_eq!(context.app.state1, TestState::Entered); 174 | assert_eq!(context.app.last_value, 0); 175 | 176 | context.sm.handle_event(&mut context.app, &SmEvent::Event(42)); 177 | 178 | assert_eq!(context.app.state1, TestState::Handled); 179 | assert_eq!(context.app.last_value, 42); 180 | } 181 | 182 | #[test] 183 | fn state_can_be_changed() { 184 | let mut context = TestContext::new(); 185 | 186 | context.sm.handle_event(&mut context.app, &SmEvent::Event(2)); 187 | 188 | assert_eq!(context.app.state1, TestState::Exited); 189 | assert_eq!(context.app.state2, TestState::Entered); 190 | assert_eq!(context.app.state3, TestState::Init); 191 | 192 | context.sm.handle_event(&mut context.app, &SmEvent::Event(3)); 193 | 194 | assert_eq!(context.app.state1, TestState::Exited); 195 | assert_eq!(context.app.state2, TestState::Exited); 196 | assert_eq!(context.app.state3, TestState::Entered); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/tui/stdio_printer.rs: -------------------------------------------------------------------------------- 1 | use termion::{color, cursor}; 2 | use termion::color::AnsiValue; 3 | 4 | use super::printer::{Index, Printer}; 5 | 6 | use std::io::{stdout, Write}; 7 | 8 | pub struct StdioPrinter { 9 | last_fg: AnsiValue, 10 | last_bg: AnsiValue, 11 | last_x: Index, 12 | last_y: Index 13 | } 14 | 15 | impl StdioPrinter { 16 | pub fn new() -> Self { 17 | StdioPrinter{ 18 | last_fg: AnsiValue(15), 19 | last_bg: AnsiValue(0), 20 | last_x: 0, 21 | last_y: 0 22 | } 23 | } 24 | } 25 | 26 | impl Printer for StdioPrinter { 27 | fn set_color(&mut self, fg_color: AnsiValue, bg_color: AnsiValue) { 28 | //if !matches!(self.last_fg, fg_color) || !matches!(self.last_bg, bg_color) { 29 | print!("{}{}", color::Bg(bg_color), color::Fg(fg_color)); 30 | //self.last_bg = bg_color; 31 | //self.last_fg = fg_color; 32 | //} 33 | } 34 | 35 | // Print some text 36 | fn print(&mut self, x: Index, y: Index, text: &str) { 37 | // TODO: Use string to buffer adjacent x-values 38 | if y == self.last_y && x == self.last_x { 39 | // No need to move cursor, alredy at right position 40 | print!("{}", text); 41 | self.last_x = self.last_x + text.len(); 42 | } else { 43 | print!("{}{}", cursor::Goto(x as u16, y as u16), text); 44 | self.last_x = x + text.len(); 45 | self.last_y = y; 46 | } 47 | } 48 | 49 | // Update the screen contents 50 | fn update(&mut self) { 51 | stdout().flush().ok(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/tui/termion_wrapper.rs: -------------------------------------------------------------------------------- 1 | 2 | use termion::cursor; 3 | use termion::event::*; 4 | use termion::input::{TermRead, MouseTerminal}; 5 | use termion::raw::{IntoRawMode, RawTerminal}; 6 | use termion::color::DetectColors; 7 | 8 | use super::UiMessage; 9 | use super::Index; 10 | 11 | use crossbeam_channel::{Sender}; 12 | use log::info; 13 | 14 | use std::io::{stdout, stdin}; 15 | use std::thread::spawn; 16 | 17 | pub struct TermionWrapper { 18 | stdout: MouseTerminal>, 19 | } 20 | 21 | impl TermionWrapper { 22 | pub fn new() -> Result { 23 | println!("{}", cursor::Hide); 24 | let mut t = TermionWrapper{ 25 | stdout: MouseTerminal::from(stdout().into_raw_mode()?), 26 | }; 27 | let count = t.stdout.available_colors().unwrap(); 28 | println!("Available colors: {}", count); 29 | Ok(t) 30 | } 31 | 32 | pub fn run(to_ui_sender: Sender) -> std::thread::JoinHandle<()> { 33 | spawn(move || { 34 | let mut exit = false; 35 | let stdin = stdin(); 36 | 37 | for e in stdin.events() { 38 | let e = e.unwrap(); 39 | match e { 40 | Event::Key(c) => { 41 | match c { 42 | // Exit. 43 | Key::F(12) => { 44 | info!("Stopping terminal handler"); 45 | exit = true; 46 | to_ui_sender.send(UiMessage::Exit).unwrap(); 47 | break; 48 | } 49 | _ => to_ui_sender.send(UiMessage::Key(c)).unwrap(), 50 | }; 51 | //termion.stdout.flush().unwrap(); 52 | } 53 | Event::Mouse(m) => { 54 | match m { 55 | MouseEvent::Press(_, x, y) => to_ui_sender.send(UiMessage::MousePress{x: x as Index, y: y as Index}).unwrap(), 56 | MouseEvent::Hold(x, y) => to_ui_sender.send(UiMessage::MouseHold{x: x as Index, y: y as Index}).unwrap(), 57 | MouseEvent::Release(x, y) => to_ui_sender.send(UiMessage::MouseRelease{x: x as Index, y: y as Index}).unwrap(), 58 | } 59 | } 60 | _ => {} 61 | } 62 | } 63 | if exit { 64 | println!("{}", termion::cursor::Show); 65 | return; 66 | } 67 | }) 68 | } 69 | } 70 | 71 | -------------------------------------------------------------------------------- /src/tui/value.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub enum Value { 3 | Int(i64), 4 | Float(f64), 5 | Str(String), 6 | } 7 | 8 | pub fn get_int(value: &Value) -> i64 { 9 | if let Value::Int(x) = value { *x } else { panic!("Got value {:?}", value) } 10 | } 11 | 12 | pub fn get_float(value: &Value) -> f64 { 13 | if let Value::Float(x) = value { *x } else { panic!("Got value {:?}", value) } 14 | } 15 | 16 | pub fn get_str(value: &Value) -> &String { 17 | if let Value::Str(x) = value { &x } else { panic!("Got value {:?}", value) } 18 | } 19 | -------------------------------------------------------------------------------- /src/tui/value_display.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::hash::Hash; 3 | use std::rc::Rc; 4 | 5 | use termion::cursor; 6 | 7 | use super::Observer; 8 | use super::Value; 9 | use super::{Printer, Widget, WidgetProperties}; 10 | 11 | pub type ValueDisplayRef = Rc>>; 12 | 13 | /** A label displayin a numerical value. */ 14 | pub struct ValueDisplay { 15 | props: WidgetProperties, 16 | value: Value, 17 | } 18 | 19 | impl ValueDisplay { 20 | pub fn new(value: Value) -> ValueDisplayRef { 21 | let width = 8; 22 | let height = 1; 23 | let props = WidgetProperties::new(width, height); 24 | Rc::new(RefCell::new(ValueDisplay{props, value})) 25 | } 26 | } 27 | 28 | impl Widget for ValueDisplay { 29 | fn get_widget_properties_mut(&mut self) -> &mut WidgetProperties { 30 | &mut self.props 31 | } 32 | 33 | fn get_widget_properties(&self) -> &WidgetProperties { 34 | &self.props 35 | } 36 | 37 | fn draw(&self, p: &mut dyn Printer) { 38 | p.set_color(self.props.colors.fg_compl, self.props.colors.bg_compl_l); 39 | print!("{} ", cursor::Goto(self.props.pos_x as u16, self.props.pos_y as u16)); 40 | match &self.value { 41 | Value::Int(v) => { 42 | print!("{:6} ", v); 43 | }, 44 | Value::Float(v) => { 45 | print!("{:.3} ", v); 46 | }, 47 | Value::Str(v) => { 48 | print!("{:.3} ", v); 49 | } 50 | } 51 | } 52 | } 53 | 54 | impl Observer for ValueDisplay { 55 | fn update(&mut self, value: Value) { 56 | self.value = value; 57 | self.set_dirty(true); 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /src/tui/waveforms.rs: -------------------------------------------------------------------------------- 1 | 2 | let square_1 = "∏ "; 3 | let square_2 = " ∐"; 4 | 5 | let sine_1 = "∩ "; 6 | let sine_2 = " ∪"; 7 | 8 | let tri_1 = "⋀ "; 9 | let tri_2 = " ⋁"; 10 | 11 | let saw_up_1 = " ∕"; 12 | let saw_up_2 = "∕ "; 13 | 14 | let saw_down_1 = "∖ "; 15 | let saw_down_2 = " ∖"; 16 | 17 | let rand_1 = "▏▏"; 18 | let rand_2 = "▕|"; 19 | -------------------------------------------------------------------------------- /src/tui/widget.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::hash::Hash; 3 | use std::rc::Rc; 4 | 5 | //use super::MouseMessage; 6 | use super::ColorScheme; 7 | use super::{Index, Printer}; 8 | 9 | pub type WidgetRef = Rc>>; 10 | 11 | #[derive(Debug)] 12 | pub struct WidgetProperties { 13 | key: Option, 14 | pub pos_x: Index, 15 | pub pos_y: Index, 16 | pub width: Index, 17 | pub height: Index, 18 | pub dirty: bool, 19 | pub colors: Rc, 20 | } 21 | 22 | impl WidgetProperties { 23 | pub fn new(width: Index, height: Index) -> WidgetProperties { 24 | let pos_x: Index = 0; 25 | let pos_y: Index = 0; 26 | let dirty = false; 27 | let colors = Rc::new(ColorScheme::new()); 28 | WidgetProperties{key: None, pos_x, pos_y, width, height, dirty, colors} 29 | } 30 | 31 | pub fn set_position(&mut self, x: Index, y: Index) -> bool { 32 | // TODO: Check that new position is valid 33 | self.pos_x = x; 34 | self.pos_y = y; 35 | true 36 | } 37 | 38 | pub fn set_width(&mut self, width: Index) -> bool { 39 | // TODO: Check that width is valid 40 | self.width = width; 41 | true 42 | } 43 | 44 | pub fn set_height(&mut self, height: Index) -> bool { 45 | // TODO: Check that height is valid 46 | self.height = height; 47 | true 48 | } 49 | 50 | pub fn set_dirty(&mut self, is_dirty: bool) { 51 | self.dirty = is_dirty; 52 | } 53 | 54 | pub fn set_color_scheme(&mut self, colors: Rc) { 55 | self.colors = colors; 56 | } 57 | 58 | pub fn set_key(&mut self, key: Key) { 59 | self.key = Some(key); 60 | } 61 | 62 | pub fn is_dirty(&self) -> bool { 63 | self.dirty 64 | } 65 | 66 | pub fn get_position(&self) -> (Index, Index) { 67 | (self.pos_x, self.pos_y) 68 | } 69 | 70 | pub fn get_width(&self) -> Index { 71 | self.width 72 | } 73 | 74 | pub fn get_height(&self) -> Index { 75 | self.height 76 | } 77 | 78 | pub fn get_size(&self) -> (Index, Index) { 79 | (self.get_width(), self.get_height()) 80 | } 81 | 82 | pub fn is_inside(&self, x: Index, y: Index) -> bool { 83 | x >= self.pos_x && 84 | x <= self.pos_x + self.width && 85 | y >= self.pos_y && 86 | y <= self.pos_y + self.height 87 | } 88 | 89 | /* 90 | pub fn get_mouse_offset(&self, msg: &MouseMessage) -> (Index, Index) { 91 | } 92 | */ 93 | } 94 | 95 | pub trait Widget { 96 | 97 | // ------------------------------------------- 98 | // Must be implemented by the deriving Widgets 99 | 100 | fn get_widget_properties_mut(&mut self) -> &mut WidgetProperties; 101 | fn get_widget_properties(&self) -> &WidgetProperties; 102 | fn draw(&self, printer: &mut dyn Printer); 103 | 104 | // ------------------------------------------- 105 | 106 | // ------------------------------------------- 107 | // Default implementations forward to WidgetProperties 108 | 109 | fn set_position(&mut self, x: Index, y: Index) -> bool { 110 | return self.get_widget_properties_mut().set_position(x, y); 111 | } 112 | 113 | fn set_width(&mut self, width: Index) -> bool { 114 | return self.get_widget_properties_mut().set_width(width); 115 | } 116 | 117 | fn set_height(&mut self, height: Index) -> bool { 118 | return self.get_widget_properties_mut().set_height(height); 119 | } 120 | 121 | fn set_dirty(&mut self, is_dirty: bool) { 122 | self.get_widget_properties_mut().set_dirty(is_dirty); 123 | } 124 | 125 | fn set_color_scheme(&mut self, colors: Rc) { 126 | self.get_widget_properties_mut().set_color_scheme(colors); 127 | } 128 | 129 | fn set_key(&mut self, key: Key) { 130 | self.get_widget_properties_mut().set_key(key); 131 | } 132 | 133 | fn is_dirty(&self) -> bool { 134 | return self.get_widget_properties().is_dirty(); 135 | } 136 | 137 | fn is_inside(&self, x: Index, y: Index) -> bool { 138 | return self.get_widget_properties().is_inside(x, y); 139 | } 140 | 141 | fn get_position(&self) -> (Index, Index) { // x, y 142 | return self.get_widget_properties().get_position(); 143 | } 144 | 145 | fn get_key(&self) -> Option { 146 | self.get_widget_properties().key 147 | } 148 | 149 | fn get_at_pos(&self, x: Index, y: Index) -> Option { 150 | if self.is_inside(x, y) { 151 | self.get_key() 152 | } else { 153 | None 154 | } 155 | } 156 | 157 | fn get_width(&self) -> Index { 158 | return self.get_widget_properties().get_width(); 159 | } 160 | 161 | fn get_height(&self) -> Index { 162 | return self.get_widget_properties().get_height(); 163 | } 164 | 165 | fn get_size(&self) -> (Index, Index) { // width, height 166 | return self.get_widget_properties().get_size(); 167 | } 168 | 169 | // ------------------------------------------- 170 | } 171 | -------------------------------------------------------------------------------- /src/value_range.rs: -------------------------------------------------------------------------------- 1 | use super::Float; 2 | use super::parameter::{Parameter, ParameterValue, MenuItem}; 3 | 4 | /** Enum for ranges of valid values */ 5 | #[derive(Clone, Copy, Debug)] 6 | pub enum ValueRange { 7 | Int(i64, i64), // Range (min, max) of integer values 8 | Float(Float, Float, Float), // Range (min, max, step) of float values 9 | Choice(&'static [MenuItem]), // A list of items to choose from 10 | Func(&'static [MenuItem]), // A list of (function-id) function entries 11 | Param(&'static [MenuItem]), // A list of (function-id-param) parameter entries 12 | Dynamic(Parameter), // List is dynamically generated according to the ID 13 | NoRange 14 | } 15 | 16 | impl ValueRange { 17 | 18 | /** Translates an integer value into a parameter value of the value range. 19 | * 20 | * This is currently only used for controller values in the range 0 - 127. 21 | */ 22 | pub fn translate_value(&self, val: u64) -> ParameterValue { 23 | match self { 24 | ValueRange::Int(min, max) => { 25 | let inc: Float = (max - min) as Float / 127.0; 26 | let value = min + (val as Float * inc) as i64; 27 | ParameterValue::Int(value) 28 | } 29 | ValueRange::Float(min, max, _) => { 30 | let inc: Float = (max - min) / 127.0; 31 | let value = min + val as Float * inc; 32 | ParameterValue::Float(value) 33 | } 34 | ValueRange::Choice(choice_list) => { 35 | let inc: Float = choice_list.len() as Float / 127.0; 36 | let value = (val as Float * inc) as i64; 37 | ParameterValue::Choice(value as usize) 38 | } 39 | ValueRange::Dynamic(param) => { 40 | ParameterValue::Dynamic(*param, val as usize) 41 | } 42 | _ => ParameterValue::NoValue 43 | } 44 | } 45 | 46 | /** Adds or subtracts two integers if the result is within the given range. */ 47 | pub fn add_value(&self, val: ParameterValue, addsub: i64) -> ParameterValue { 48 | match self { 49 | ValueRange::Int(min, max) => { 50 | let mut value = if let ParameterValue::Int(x) = val { 51 | x 52 | } else { 53 | panic!() 54 | }; 55 | let result = value + addsub; 56 | value = if result >= *max { 57 | *max 58 | } else if result <= *min { 59 | *min 60 | } else { 61 | result 62 | }; 63 | ParameterValue::Int(value) 64 | } 65 | ValueRange::Float(min, max, step) => { 66 | let mut value = if let ParameterValue::Float(x) = val { 67 | x 68 | } else { 69 | panic!() 70 | }; 71 | let result = value + (addsub as Float * step); 72 | value = if result >= *max { 73 | *max 74 | } else if result <= *min { 75 | *min 76 | } else { 77 | result 78 | }; 79 | ParameterValue::Float(value) 80 | } 81 | ValueRange::Choice(choice_list) => { 82 | let mut value = if let ParameterValue::Choice(x) = val { 83 | x 84 | } else { 85 | panic!() 86 | }; 87 | let result = value + addsub as usize; 88 | if result < choice_list.len() { 89 | value = result; 90 | } 91 | ParameterValue::Choice(value) 92 | } 93 | ValueRange::Dynamic(param) => { 94 | let value = if let ParameterValue::Dynamic(_p, x) = val { 95 | x 96 | } else { 97 | panic!() 98 | }; 99 | let result = if addsub > 0 || value > 0 { 100 | value + addsub as usize 101 | } else { 102 | value 103 | }; 104 | ParameterValue::Dynamic(*param, result) 105 | } 106 | _ => ParameterValue::NoValue 107 | } 108 | } 109 | 110 | pub fn get_min_max(&self) -> (Float, Float) { 111 | match self { 112 | ValueRange::Int(min, max) => (*min as Float, *max as Float), 113 | ValueRange::Float(min, max, _) => (*min, *max), 114 | ValueRange::Choice(itemlist) => (0.0, itemlist.len() as Float), 115 | _ => panic!("Unexpected value range, cannot get min and max"), 116 | } 117 | } 118 | 119 | /** Adds two floats, keeps result within value range. */ 120 | pub fn safe_add(&self, a: Float, b: Float) -> Float { 121 | let result = a + b; 122 | let (min, max) = self.get_min_max(); 123 | if result < min { 124 | min 125 | } else if result > max { 126 | max 127 | } else { 128 | result 129 | } 130 | } 131 | } 132 | 133 | impl Default for ValueRange { 134 | fn default() -> Self { ValueRange::NoRange } 135 | } 136 | 137 | -------------------------------------------------------------------------------- /todo.txt: -------------------------------------------------------------------------------- 1 | A list of features to implement, roughly in order of importance: 2 | 3 | Synth engine: 4 | - Finish modulation sources/ targets 5 | - Finish filter 6 | - Envelope amount + delay 7 | - Implement additional oscillators: 8 | - Sample 9 | - FM 10 | - PM 11 | - Poly/ Mono modes 12 | 13 | UI: 14 | - Loading of wavetable files 15 | - Improved loading/ saving/ copying of sounds 16 | - Dynamic parameter lists (needed when having multiple oscillators) 17 | - Macro controller to controll multiple parameters 18 | 19 | MIDI: 20 | - Pitchwheel 21 | - Velocity 22 | - Default controller mapping 23 | 24 | General: 25 | - Performance improvements 26 | - Test coverage 27 | --------------------------------------------------------------------------------