├── .cargo └── config ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── ci.yml │ └── releases.yml ├── .gitignore ├── ARCHITECTURE.md ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── TODO.md ├── about.hbs ├── about.toml ├── contrib └── open-sans │ ├── LICENSE.txt │ ├── OpenSans-Bold.ttf │ ├── OpenSans-BoldItalic.ttf │ ├── OpenSans-ExtraBold.ttf │ ├── OpenSans-ExtraBoldItalic.ttf │ ├── OpenSans-Italic.ttf │ ├── OpenSans-Light.ttf │ ├── OpenSans-LightItalic.ttf │ ├── OpenSans-Regular.ttf │ ├── OpenSans-SemiBold.ttf │ └── OpenSans-SemiBoldItalic.ttf ├── deny.toml ├── images ├── architecture-1.png ├── screenshot-dark.png └── screenshot-light.png ├── octasine-cli ├── Cargo.toml └── src │ ├── bench_process.rs │ ├── main.rs │ └── plot.rs ├── octasine ├── Cargo.toml ├── LICENSE ├── benches │ └── patch_bank_serde.rs ├── build.rs └── src │ ├── audio │ ├── gen │ │ ├── lfo.rs │ │ └── mod.rs │ ├── interpolation.rs │ ├── mod.rs │ ├── parameters │ │ ├── common.rs │ │ ├── lfo_active.rs │ │ ├── lfo_amount.rs │ │ ├── lfo_frequency_free.rs │ │ ├── lfo_target.rs │ │ ├── master_frequency.rs │ │ ├── master_volume.rs │ │ ├── mod.rs │ │ ├── operator_active.rs │ │ ├── operator_frequency_fine.rs │ │ ├── operator_frequency_free.rs │ │ ├── operator_mix.rs │ │ ├── operator_mod_target.rs │ │ ├── operator_panning.rs │ │ ├── operator_sustain_volume.rs │ │ └── operator_volume.rs │ └── voices │ │ ├── envelopes.rs │ │ ├── lfos.rs │ │ ├── log10_table.rs │ │ └── mod.rs │ ├── common.rs │ ├── gui │ ├── boolean_button.rs │ ├── common.rs │ ├── corner.rs │ ├── envelope │ │ ├── canvas │ │ │ ├── common.rs │ │ │ ├── draw.rs │ │ │ ├── events.rs │ │ │ └── mod.rs │ │ └── mod.rs │ ├── knob.rs │ ├── lfo.rs │ ├── lfo_target_picker.rs │ ├── mod.rs │ ├── mod_matrix │ │ ├── common.rs │ │ ├── mix_line.rs │ │ ├── mod.rs │ │ ├── mod_box.rs │ │ ├── mod_lines.rs │ │ ├── operator_box.rs │ │ └── output_box.rs │ ├── mod_target_picker.rs │ ├── operator.rs │ ├── patch_picker.rs │ ├── style │ │ ├── application.rs │ │ ├── boolean_button.rs │ │ ├── button.rs │ │ ├── card.rs │ │ ├── checkbox.rs │ │ ├── colors │ │ │ ├── dark.rs │ │ │ ├── light.rs │ │ │ └── mod.rs │ │ ├── container.rs │ │ ├── envelope.rs │ │ ├── knob.rs │ │ ├── macros.rs │ │ ├── menu.rs │ │ ├── mod.rs │ │ ├── mod_matrix.rs │ │ ├── modal.rs │ │ ├── pick_list.rs │ │ ├── radio.rs │ │ ├── scrollable.rs │ │ ├── text.rs │ │ ├── text_input.rs │ │ ├── wave_display.rs │ │ └── wave_picker.rs │ ├── value_text.rs │ ├── wave_display │ │ ├── gen.rs │ │ └── mod.rs │ └── wave_picker.rs │ ├── lib.rs │ ├── math │ ├── bhaskara.rs │ ├── mod.rs │ └── wave.rs │ ├── parameters │ ├── glide_active.rs │ ├── glide_bpm_sync.rs │ ├── glide_mode.rs │ ├── glide_retrigger.rs │ ├── glide_time.rs │ ├── lfo_active.rs │ ├── lfo_amount.rs │ ├── lfo_bpm_sync.rs │ ├── lfo_frequency_free.rs │ ├── lfo_frequency_ratio.rs │ ├── lfo_key_sync.rs │ ├── lfo_mode.rs │ ├── lfo_shape.rs │ ├── lfo_target.rs │ ├── list.rs │ ├── master_frequency.rs │ ├── master_pitch_bend_range.rs │ ├── master_volume.rs │ ├── mod.rs │ ├── operator_active.rs │ ├── operator_envelope.rs │ ├── operator_feedback.rs │ ├── operator_frequency_fine.rs │ ├── operator_frequency_free.rs │ ├── operator_frequency_ratio.rs │ ├── operator_mix_out.rs │ ├── operator_mod_out.rs │ ├── operator_mod_target.rs │ ├── operator_panning.rs │ ├── operator_volume.rs │ ├── operator_wave_type.rs │ ├── utils.rs │ ├── velocity_sensitivity.rs │ └── voice_mode.rs │ ├── plugin │ ├── clap │ │ ├── descriptor.rs │ │ ├── ext │ │ │ ├── audio_ports.rs │ │ │ ├── gui.rs │ │ │ ├── mod.rs │ │ │ ├── note_ports.rs │ │ │ ├── params.rs │ │ │ ├── state.rs │ │ │ └── voice_info.rs │ │ ├── factory.rs │ │ ├── mod.rs │ │ ├── plugin.rs │ │ └── sync.rs │ ├── common.rs │ ├── mod.rs │ └── vst2 │ │ ├── editor.rs │ │ ├── mod.rs │ │ └── sync.rs │ ├── settings.rs │ ├── simd │ ├── avx.rs │ ├── fallback.rs │ ├── mod.rs │ └── sse2.rs │ ├── sync │ ├── atomic_float.rs │ ├── change_info.rs │ ├── mod.rs │ ├── parameters.rs │ ├── patch_bank.rs │ └── serde │ │ ├── common.rs │ │ ├── mod.rs │ │ ├── v1.rs │ │ └── v2 │ │ ├── compat.rs │ │ └── mod.rs │ └── utils.rs ├── scripts ├── about.sh ├── bench.sh ├── clippy.sh ├── criterion.sh ├── macos │ ├── build-clap-and-install.sh │ └── build-vst2-and-install.sh ├── miri-audio-gen.sh ├── plot.sh ├── run-gui.sh └── test.sh └── xtask ├── Cargo.toml └── src └── main.rs /.cargo/config: -------------------------------------------------------------------------------- 1 | [alias] 2 | xtask = "run --package xtask --release --" 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **What did you try to do** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, please add screenshots to help explain your problem. 25 | 26 | **Log file contents** 27 | Please include the contents of the OctaSine log file (`OctaSine.log`). Its location depends on your operating system (replace __Alice__ with your username): 28 | - _On macOS_: /Users/__Alice__/Library/Application Support/com.OctaSine.OctaSine/OctaSine.log (the file can be opened with TextEdit) 29 | - _On Windows_: C:\\Users\\__Alice__\\Documents\\OctaSine\\OctaSine.log (the file can be opened with Notepad) 30 | - _On Linux_: /home/__Alice__/.config/octasine/OctaSine.log (the file can be opened with a plain text editor) 31 | 32 | ``` 33 | Please replace this text with the log file contents. 34 | 35 | If there is a lot of text, please attach the file instead. 36 | ``` 37 | 38 | **Software (please complete the following information):** 39 | - DAW [e.g. Ableton Live Suite 11] 40 | - OS: [e.g. Windows 10, macOS 10.15] (can be skipped if present in the log file above) 41 | - OctaSine version [e.g. 0.8.2] (can be skipped if present in the log file above). It can be displayed by hovering over the Info button in the bottom right corner of the user interface. 42 | 43 | **Additional context** 44 | Please add any other context about the problem here. -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | jobs: 15 | build-macos: 16 | runs-on: macos-12 17 | timeout-minutes: 20 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | 22 | - name: Install latest Rust (macOS) 23 | uses: dtolnay/rust-toolchain@stable 24 | with: 25 | targets: aarch64-apple-darwin 26 | 27 | - name: Setup Rust dependency caching 28 | uses: Swatinem/rust-cache@v2 29 | 30 | - name: Build plugin 31 | run: cargo build -p octasine --verbose --features "vst2 clap" 32 | 33 | - name: Build plugin for aarch64 34 | run: | 35 | export SDKROOT=$(xcrun -sdk macosx12.3 --show-sdk-path) 36 | echo "SDKROOT=$SDKROOT" 37 | export MACOSX_DEPLOYMENT_TARGET=$(xcrun -sdk macosx12.3 --show-sdk-platform-version) 38 | echo "MACOSX_DEPLOYMENT_TARGET=$MACOSX_DEPLOYMENT_TARGET" 39 | cargo build -p octasine --target=aarch64-apple-darwin 40 | env: 41 | DEVELOPER_DIR: /Applications/Xcode_13.4.1.app/Contents/Developer 42 | shell: bash 43 | if: contains(matrix.os, 'macos') 44 | 45 | build-windows: 46 | runs-on: windows-latest 47 | timeout-minutes: 20 48 | 49 | steps: 50 | - uses: actions/checkout@v3 51 | 52 | - name: Install latest rust (windows/linux) 53 | uses: dtolnay/rust-toolchain@stable 54 | 55 | - name: Setup Rust dependency caching 56 | uses: Swatinem/rust-cache@v2 57 | 58 | - name: Build plugin 59 | run: cargo build -p octasine --verbose --features "vst2 clap" 60 | 61 | build-linux: 62 | runs-on: ubuntu-20.04 63 | timeout-minutes: 20 64 | 65 | steps: 66 | - uses: actions/checkout@v3 67 | 68 | - name: Install baseview dependencies (Linux) 69 | run: sudo apt update && sudo apt install libgl-dev libx11-xcb-dev libxcb1-dev libxcb-dri2-0-dev libxcb-icccm4-dev libxcursor-dev libxkbcommon-dev libxcb-shape0-dev libxcb-xfixes0-dev 70 | 71 | - name: Install latest Rust 72 | uses: dtolnay/rust-toolchain@stable 73 | with: 74 | targets: aarch64-unknown-linux-gnu 75 | 76 | - name: Setup Rust dependency caching 77 | uses: Swatinem/rust-cache@v2 78 | 79 | - name: Build plugin 80 | run: cargo build -p octasine --verbose --features "vst2 clap" 81 | # Disabled: needs more complex setup 82 | # run: | 83 | # cargo build -p octasine --verbose 84 | # cargo build -p octasine --verbose --target=aarch64-unknown-linux-gnu 85 | 86 | test-linux: 87 | runs-on: ubuntu-latest 88 | timeout-minutes: 20 89 | 90 | steps: 91 | - uses: actions/checkout@v3 92 | 93 | - name: Install baseview dependencies 94 | run: sudo apt update && sudo apt install libgl-dev libx11-xcb-dev libxcb1-dev libxcb-dri2-0-dev libxcb-icccm4-dev libxcursor-dev libxkbcommon-dev libxcb-shape0-dev libxcb-xfixes0-dev 95 | 96 | - name: Install latest Rust 97 | uses: dtolnay/rust-toolchain@stable 98 | 99 | - name: Install cargo-nextest 100 | uses: taiki-e/install-action@nextest 101 | 102 | - name: Setup Rust dependency caching 103 | uses: Swatinem/rust-cache@v2 104 | 105 | - name: Run tests 106 | run: cargo nextest run --workspace --verbose --features "vst2 clap" 107 | env: 108 | # Set target-cpu=skylake to enable avx-2 but not avx-512, since the 109 | # latter for some reason leads to SIGILL failures 110 | RUSTFLAGS: "-C target-cpu=skylake" 111 | 112 | cargo-deny: 113 | runs-on: ubuntu-latest 114 | timeout-minutes: 20 115 | steps: 116 | - uses: actions/checkout@v3 117 | - uses: EmbarkStudios/cargo-deny-action@v1 118 | with: 119 | log-level: warn 120 | command: check licenses 121 | arguments: --all-features --workspace 122 | 123 | clap-validator: 124 | runs-on: ubuntu-latest 125 | timeout-minutes: 20 126 | 127 | steps: 128 | - uses: actions/checkout@v3 129 | 130 | - name: Install baseview dependencies 131 | run: sudo apt update && sudo apt install libgl-dev libx11-xcb-dev libxcb1-dev libxcb-dri2-0-dev libxcb-icccm4-dev libxcursor-dev libxkbcommon-dev libxcb-shape0-dev libxcb-xfixes0-dev 132 | 133 | - name: Install latest Rust 134 | uses: dtolnay/rust-toolchain@stable 135 | 136 | - name: Setup Rust dependency caching 137 | uses: Swatinem/rust-cache@v2 138 | 139 | - name: Install clap-validator 140 | run: cargo install --git https://github.com/free-audio/clap-validator.git 141 | 142 | - name: Build and bundle clap plugin 143 | run: cargo xtask bundle octasine --release --verbose --features "clap" 144 | 145 | - name: Validate clap plugin 146 | # Note: skip param-conversions test for now 147 | run: clap-validator validate --invert-filter --test-filter 'param-conversions' ./target/bundled/octasine.clap 148 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | .DS_Store 4 | 5 | /target 6 | **/*.rs.bk 7 | tmp -------------------------------------------------------------------------------- /ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # OctaSine architecture 2 | 3 | ![OctaSine architecture](./images/architecture-1.png) 4 | 5 | - Black lines indicate data flow 6 | - Yellow lines indicate important control flow 7 | - Dotted lines indicate alternative data flow paths that can happen 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | 4 | members = [ 5 | "octasine", 6 | "octasine-cli", 7 | "xtask" 8 | ] 9 | 10 | # [patch.'https://github.com/RustAudio/baseview'] 11 | # baseview = { path = "../baseview" } 12 | 13 | # [patch.'https://github.com/BillyDM/iced_baseview'] 14 | # iced_baseview = { path = "../iced_baseview" } 15 | 16 | # [patch.'https://github.com/iced-rs/iced_audio'] 17 | # [patch.crates-io] 18 | # iced_audio = { path = "../iced_audio" } 19 | 20 | # [patch.'https://github.com/greatest-ape/sleef-trig'] 21 | # sleef-trig = { path = "../sleef-trig" } 22 | 23 | # Same as original repo, but forked for longevity 24 | [patch.'https://github.com/nicokoch/reflink.git'] 25 | reflink = { git = "https://github.com/greatest-ape/reflink.git", rev = "e8d93b4" } 26 | 27 | [profile.release] 28 | debug = false 29 | lto = "thin" 30 | opt-level = 3 31 | 32 | [profile.test] 33 | opt-level = 3 34 | 35 | [profile.release-debug] 36 | inherits = "release" 37 | debug = true 38 | 39 | [profile.bench] 40 | inherits = "release-debug" 41 | 42 | [profile.release.package.sleef-trig] 43 | codegen-units = 1 44 | 45 | [profile.release-debug.package.sleef-trig] 46 | codegen-units = 1 -------------------------------------------------------------------------------- /about.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 36 | 37 | 38 | 39 |
40 |
41 |

Licenses

42 |

This page lists the licenses of the projects used in OctaSine.

43 |
44 | 45 |

Overview of licenses:

46 | 51 | 52 |

All license text:

53 | 67 |
68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /about.toml: -------------------------------------------------------------------------------- 1 | accepted = [ 2 | "MIT", 3 | "Apache-2.0", 4 | "BSD-2-Clause", 5 | "BSD-3-Clause", 6 | "ISC", 7 | "MPL-2.0", 8 | "FTL", 9 | "OFL-1.1", 10 | "Zlib", 11 | "AGPL-3.0", 12 | "BSL-1.0", 13 | "Unicode-DFS-2016", 14 | "zlib-acknowledgement", 15 | "MIT-0", 16 | "CC0-1.0" 17 | ] 18 | 19 | targets = [ 20 | "x86_64-unknown-linux-gnu", 21 | "x86_64-pc-windows-msvc", 22 | "x86_64-apple-darwin", 23 | "aarch64-apple-darwin" 24 | ] -------------------------------------------------------------------------------- /contrib/open-sans/OpenSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greatest-ape/OctaSine/e347a9334a3a976029746719afa0bf0875181b20/contrib/open-sans/OpenSans-Bold.ttf -------------------------------------------------------------------------------- /contrib/open-sans/OpenSans-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greatest-ape/OctaSine/e347a9334a3a976029746719afa0bf0875181b20/contrib/open-sans/OpenSans-BoldItalic.ttf -------------------------------------------------------------------------------- /contrib/open-sans/OpenSans-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greatest-ape/OctaSine/e347a9334a3a976029746719afa0bf0875181b20/contrib/open-sans/OpenSans-ExtraBold.ttf -------------------------------------------------------------------------------- /contrib/open-sans/OpenSans-ExtraBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greatest-ape/OctaSine/e347a9334a3a976029746719afa0bf0875181b20/contrib/open-sans/OpenSans-ExtraBoldItalic.ttf -------------------------------------------------------------------------------- /contrib/open-sans/OpenSans-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greatest-ape/OctaSine/e347a9334a3a976029746719afa0bf0875181b20/contrib/open-sans/OpenSans-Italic.ttf -------------------------------------------------------------------------------- /contrib/open-sans/OpenSans-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greatest-ape/OctaSine/e347a9334a3a976029746719afa0bf0875181b20/contrib/open-sans/OpenSans-Light.ttf -------------------------------------------------------------------------------- /contrib/open-sans/OpenSans-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greatest-ape/OctaSine/e347a9334a3a976029746719afa0bf0875181b20/contrib/open-sans/OpenSans-LightItalic.ttf -------------------------------------------------------------------------------- /contrib/open-sans/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greatest-ape/OctaSine/e347a9334a3a976029746719afa0bf0875181b20/contrib/open-sans/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /contrib/open-sans/OpenSans-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greatest-ape/OctaSine/e347a9334a3a976029746719afa0bf0875181b20/contrib/open-sans/OpenSans-SemiBold.ttf -------------------------------------------------------------------------------- /contrib/open-sans/OpenSans-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greatest-ape/OctaSine/e347a9334a3a976029746719afa0bf0875181b20/contrib/open-sans/OpenSans-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /images/architecture-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greatest-ape/OctaSine/e347a9334a3a976029746719afa0bf0875181b20/images/architecture-1.png -------------------------------------------------------------------------------- /images/screenshot-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greatest-ape/OctaSine/e347a9334a3a976029746719afa0bf0875181b20/images/screenshot-dark.png -------------------------------------------------------------------------------- /images/screenshot-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greatest-ape/OctaSine/e347a9334a3a976029746719afa0bf0875181b20/images/screenshot-light.png -------------------------------------------------------------------------------- /octasine-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "octasine-cli" 3 | version = "0.9.1" 4 | authors = ["Joakim Frostegård "] 5 | license = "AGPL-3.0" 6 | edition = "2021" 7 | 8 | 9 | [features] 10 | default = ["glow", "plot"] 11 | glow = ["octasine/glow", "simplelog"] 12 | plot = ["plotlib"] 13 | wgpu = ["octasine/wgpu", "simplelog"] 14 | 15 | [dependencies] 16 | octasine = { path = "../octasine", version = "0.9", default-features = false, features = ["vst2"] } 17 | 18 | anyhow = "1" 19 | clap = { version = "4", features = ["derive"] } 20 | colored = "2" 21 | fastrand = "2" 22 | serde = "1" 23 | serde_json = "1" 24 | sha2 = "0.10" 25 | vst = "0.4" 26 | 27 | # run-gui 28 | simplelog = { version = "0.12", default-features = false, features = ["local-offset"], optional = true } 29 | 30 | # plot 31 | plotlib = { version = "0.5", optional = true } -------------------------------------------------------------------------------- /octasine-cli/src/main.rs: -------------------------------------------------------------------------------- 1 | mod bench_process; 2 | #[cfg(feature = "plot")] 3 | mod plot; 4 | 5 | use clap::{Parser, Subcommand}; 6 | 7 | #[derive(Parser)] 8 | #[command(author, version, about, long_about = None)] 9 | struct Cli { 10 | #[command(subcommand)] 11 | command: Commands, 12 | } 13 | 14 | #[derive(Subcommand)] 15 | enum Commands { 16 | /// Run OctaSine GUI (without audio generation) 17 | #[cfg(any(feature = "glow", feature = "wgpu"))] 18 | RunGui, 19 | /// Benchmark OctaSine process functions and check output sample accuracy 20 | BenchProcess, 21 | /// Plot envelope and LFO curves (useful during development) 22 | #[cfg(feature = "plot")] 23 | Plot, 24 | } 25 | 26 | fn main() -> anyhow::Result<()> { 27 | let cli = Cli::parse(); 28 | 29 | match cli.command { 30 | #[cfg(any(feature = "glow", feature = "wgpu"))] 31 | Commands::RunGui => { 32 | use std::sync::Arc; 33 | 34 | use octasine::{plugin::vst2::editor::Editor, sync::SyncState}; 35 | use simplelog::{ConfigBuilder, LevelFilter, SimpleLogger}; 36 | use vst::plugin::HostCallback; 37 | 38 | SimpleLogger::init( 39 | LevelFilter::Info, 40 | ConfigBuilder::new() 41 | .set_time_offset_to_local() 42 | .unwrap() 43 | .build(), 44 | ) 45 | .unwrap(); 46 | 47 | let sync_state = Arc::new(SyncState::::new(None)); 48 | 49 | Editor::open_blocking(sync_state); 50 | 51 | Ok(()) 52 | } 53 | Commands::BenchProcess => bench_process::run(), 54 | #[cfg(feature = "plot")] 55 | Commands::Plot => plot::run(), 56 | } 57 | } 58 | 59 | #[test] 60 | fn verify_cli() { 61 | use clap::CommandFactory; 62 | Cli::command().debug_assert() 63 | } 64 | -------------------------------------------------------------------------------- /octasine/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "octasine" 3 | version = "0.9.1" 4 | authors = ["Joakim Frostegård "] 5 | license = "AGPL-3.0" 6 | edition = "2021" 7 | 8 | [features] 9 | default = ["glow"] 10 | # Enable clap plugin support 11 | clap = ["atomic_refcell", "bytemuck", "clap-sys", "parking_lot"] 12 | # Enable VST2 plugin support 13 | vst2 = ["vst", "parking_lot"] 14 | # Use glow (OpenGL) for graphics 15 | glow = ["gui", "iced_baseview/glow", "iced_audio/glow"] 16 | # Use wgpu for graphics 17 | wgpu = ["gui", "iced_baseview/wgpu", "iced_audio/wgpu"] 18 | # Internal use only 19 | gui = ["iced_baseview/canvas", "iced_audio", "iced_aw", "palette", "raw-window-handle", "rwh06", "rfd", "tinyfiledialogs"] 20 | 21 | [lib] 22 | name = "octasine" 23 | crate-type = ["cdylib", "lib"] 24 | 25 | [[bench]] 26 | name = "patch_bank_serde" 27 | harness = false 28 | 29 | [dev-dependencies] 30 | assert_approx_eq = "1" 31 | criterion = "0.5" 32 | quickcheck = { version = "1", default-features = false } 33 | 34 | [dependencies] 35 | ahash = "0.8" 36 | anyhow = "1" 37 | arc-swap = "1" 38 | array-init = "2" 39 | arrayvec = "0.7" 40 | byteorder = "1" 41 | cfg-if = "1" 42 | cbor4ii = { version = "0.3", features = ["serde1", "use_std"] } 43 | compact_str = { version = "0.7", features = ["serde"] } 44 | directories = "5" 45 | duplicate = "1" 46 | fast-math = "0.1" 47 | fastrand = "2" 48 | flate2 = "1" 49 | git-testament = "0.2" 50 | indexmap = { version = "2", features = ["serde"] } 51 | log = { version = "0.4", default-features = false } 52 | log-panics = "2" 53 | memchr = "2" 54 | once_cell = "1" 55 | os_info = "3" 56 | ringbuf = "0.3" 57 | seahash = "4" 58 | semver = { version = "1", features = ["serde"] } 59 | serde = { version = "1", features = ["derive"] } 60 | serde_json = "1" 61 | simplelog = { version = "0.12", default-features = false, features = ["local-offset"] } 62 | sleef-trig = "0.1.0" 63 | 64 | # vst2 65 | 66 | vst = { version = "0.4", optional = true } 67 | 68 | # clap 69 | 70 | atomic_refcell = { version = "0.1", optional = true } 71 | bytemuck = { version = "1", optional = true } 72 | clap-sys = { version = "0.3", optional = true } 73 | 74 | # vst2 / clap 75 | parking_lot = { version = "0.12", optional = true } 76 | 77 | # GUI 78 | 79 | iced_audio = { version = "0.12", default-features = false, optional = true } 80 | iced_aw = { version = "0.5", features = ["modal", "card"], optional = true } 81 | palette = { version = "0.6", optional = true } 82 | raw-window-handle = { version = "0.5", optional = true } 83 | rwh06 = { package = "raw-window-handle", version = "0.6", optional = true } 84 | tinyfiledialogs = { version = "3", optional = true } 85 | 86 | [dependencies.iced_baseview] 87 | git = "https://github.com/greatest-ape/iced_baseview.git" 88 | rev = "055d88f" # branch octasine-0.9 89 | default-features = false 90 | features = ["canvas"] 91 | optional = true 92 | 93 | [target.'cfg(target_os="macos")'.dependencies] 94 | objc = "0.2.7" 95 | rfd = { version = "0.14", optional = true, default-features = false, features = ["xdg-portal"] } 96 | 97 | [target.'cfg(target_os="windows")'.dependencies] 98 | rfd = { version = "0.14", optional = true, default-features = false, features = ["xdg-portal"] } -------------------------------------------------------------------------------- /octasine/benches/patch_bank_serde.rs: -------------------------------------------------------------------------------- 1 | use criterion::criterion_group; 2 | use criterion::criterion_main; 3 | use criterion::BenchmarkId; 4 | use criterion::Criterion; 5 | 6 | use octasine::sync::PatchBank; 7 | 8 | fn create_bank() -> PatchBank { 9 | fastrand::seed(123); 10 | 11 | let bank = PatchBank::default(); 12 | 13 | for patch in bank.patches.iter() { 14 | for p in patch.parameters.values() { 15 | p.set_value(fastrand::f32()); 16 | } 17 | } 18 | 19 | bank 20 | } 21 | 22 | fn export_plain(c: &mut Criterion) { 23 | let bank = create_bank(); 24 | 25 | c.bench_with_input( 26 | BenchmarkId::new("export_plain", "randomized"), 27 | &bank, 28 | |b, bank| { 29 | b.iter(|| bank.export_plain_bytes()); 30 | }, 31 | ); 32 | } 33 | 34 | fn export_fxb(c: &mut Criterion) { 35 | let bank = create_bank(); 36 | 37 | c.bench_with_input( 38 | BenchmarkId::new("export_fxb", "randomized"), 39 | &bank, 40 | |b, bank| { 41 | b.iter(|| bank.export_fxb_bytes()); 42 | }, 43 | ); 44 | } 45 | 46 | fn import_plain(c: &mut Criterion) { 47 | let data = create_bank().export_plain_bytes(); 48 | 49 | c.bench_with_input( 50 | BenchmarkId::new("import_plain", "randomized"), 51 | &data, 52 | |b, data| { 53 | b.iter(|| PatchBank::new_from_bytes(data)); 54 | }, 55 | ); 56 | } 57 | 58 | fn import_fxb(c: &mut Criterion) { 59 | let data = create_bank().export_fxb_bytes(); 60 | 61 | c.bench_with_input( 62 | BenchmarkId::new("import_fxb", "randomized"), 63 | &data, 64 | |b, data| { 65 | b.iter(|| PatchBank::new_from_bytes(data)); 66 | }, 67 | ); 68 | } 69 | 70 | criterion_group!(benches, export_plain, export_fxb, import_plain, import_fxb); 71 | criterion_main!(benches); 72 | -------------------------------------------------------------------------------- /octasine/build.rs: -------------------------------------------------------------------------------- 1 | //! Hack to get const Parameter to parameter index mapping 2 | 3 | use std::env; 4 | use std::fs::File; 5 | use std::io::{BufWriter, Write}; 6 | use std::path::Path; 7 | 8 | include!("./src/parameters/list.rs"); 9 | 10 | fn main() { 11 | let path = Path::new(&env::var("OUT_DIR").unwrap()).join("codegen.rs"); 12 | let mut file = BufWriter::new(File::create(&path).unwrap()); 13 | 14 | write!( 15 | &mut file, 16 | "const fn parameter_to_index(parameter: Parameter) -> u8 {{ match parameter {{" 17 | ) 18 | .unwrap(); 19 | 20 | for (parameter_index, parameter) in PARAMETERS.iter().copied().enumerate() { 21 | match parameter { 22 | Parameter::None => unreachable!(), 23 | Parameter::Master(p) => writeln!( 24 | &mut file, 25 | "Parameter::Master(MasterParameter::{:?}) => {},", 26 | p, parameter_index 27 | ) 28 | .unwrap(), 29 | Parameter::Operator(operator_index, p) => writeln!( 30 | &mut file, 31 | "Parameter::Operator({}, OperatorParameter::{:?}) => {},", 32 | operator_index, p, parameter_index 33 | ) 34 | .unwrap(), 35 | Parameter::Lfo(lfo_index, p) => writeln!( 36 | &mut file, 37 | "Parameter::Lfo({}, LfoParameter::{:?}) => {},", 38 | lfo_index, p, parameter_index 39 | ) 40 | .unwrap(), 41 | }; 42 | } 43 | 44 | write!(&mut file, "_ => unreachable!(),").unwrap(); 45 | writeln!(&mut file, "}}}}").unwrap(); 46 | 47 | println!("cargo:rerun-if-changed=build.rs"); 48 | } 49 | -------------------------------------------------------------------------------- /octasine/src/audio/gen/lfo.rs: -------------------------------------------------------------------------------- 1 | use arrayvec::ArrayVec; 2 | 3 | use crate::audio::parameters::{common::AudioParameter, LfoAudioParameters}; 4 | use crate::audio::voices::lfos::VoiceLfo; 5 | use crate::common::*; 6 | use crate::parameters::{LfoParameter, PARAMETERS}; 7 | 8 | pub struct LfoTargetValues { 9 | values: [Option; PARAMETERS.len()], 10 | set_indices: ArrayVec, 11 | } 12 | 13 | impl Default for LfoTargetValues { 14 | fn default() -> Self { 15 | Self { 16 | values: [None; PARAMETERS.len()], 17 | set_indices: Default::default(), 18 | } 19 | } 20 | } 21 | 22 | impl LfoTargetValues { 23 | pub fn get(&self, target: u8) -> Option { 24 | self.values[target as usize] 25 | } 26 | 27 | fn set_or_add(&mut self, target: u8, value: f32) { 28 | match &mut self.values[target as usize] { 29 | Some(v) => *v += value, 30 | v @ None => { 31 | *v = Some(value); 32 | 33 | self.set_indices.push(target); 34 | } 35 | } 36 | } 37 | 38 | fn clear_set(&mut self) { 39 | for i in self.set_indices.iter().copied() { 40 | self.values[i as usize] = None; 41 | } 42 | 43 | self.set_indices.clear(); 44 | } 45 | } 46 | 47 | pub fn update_lfo_target_values( 48 | lfo_values: &mut LfoTargetValues, 49 | lfo_parameters: &mut [LfoAudioParameters; NUM_LFOS], 50 | voice_lfos: &mut [VoiceLfo; NUM_LFOS], 51 | sample_rate: SampleRate, 52 | time_per_sample: TimePerSample, 53 | bpm_lfo_multiplier: BpmLfoMultiplier, 54 | ) { 55 | const AMOUNT_PARAMETER_INDICES: [u8; NUM_LFOS] = LfoParameter::Amount.index_array(); 56 | const SHAPE_PARAMETER_INDICES: [u8; NUM_LFOS] = LfoParameter::Shape.index_array(); 57 | const RATIO_PARAMETER_INDICES: [u8; NUM_LFOS] = LfoParameter::FrequencyRatio.index_array(); 58 | const FREE_PARAMETER_INDICES: [u8; NUM_LFOS] = LfoParameter::FrequencyFree.index_array(); 59 | 60 | lfo_values.clear_set(); 61 | 62 | for (lfo_index, (voice_lfo, lfo_parameter)) in voice_lfos 63 | .iter_mut() 64 | .zip(lfo_parameters.iter_mut()) 65 | .enumerate() 66 | .rev() 67 | { 68 | assert!(lfo_index < NUM_LFOS); 69 | 70 | let target_index = lfo_parameter.target.get_value().index(); 71 | 72 | let target_index = match (target_index, voice_lfo.is_stopped()) { 73 | (None, _) | (_, true) => continue, 74 | (Some(index), false) => index, 75 | }; 76 | 77 | let amount = lfo_parameter.active.get_value() 78 | * lfo_parameter 79 | .amount 80 | .get_value_with_lfo_addition(lfo_values.get(AMOUNT_PARAMETER_INDICES[lfo_index])); 81 | 82 | let mode = lfo_parameter.mode.get_value(); 83 | let bpm_sync = lfo_parameter.bpm_sync.get_value(); 84 | 85 | let shape = lfo_parameter 86 | .shape 87 | .get_value_with_lfo_addition(lfo_values.get(SHAPE_PARAMETER_INDICES[lfo_index])); 88 | let frequency_ratio = lfo_parameter 89 | .frequency_ratio 90 | .get_value_with_lfo_addition(lfo_values.get(RATIO_PARAMETER_INDICES[lfo_index])); 91 | let frequency_free = lfo_parameter 92 | .frequency_free 93 | .get_value_with_lfo_addition(lfo_values.get(FREE_PARAMETER_INDICES[lfo_index])); 94 | 95 | let bpm_lfo_multiplier = if bpm_sync { 96 | bpm_lfo_multiplier 97 | } else { 98 | BpmLfoMultiplier(1.0) 99 | }; 100 | 101 | voice_lfo.advance_one_sample( 102 | sample_rate, 103 | time_per_sample, 104 | bpm_lfo_multiplier, 105 | shape, 106 | mode, 107 | frequency_ratio * frequency_free, 108 | ); 109 | 110 | let addition = voice_lfo.get_value(amount); 111 | 112 | lfo_values.set_or_add(target_index, addition); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /octasine/src/audio/parameters/common.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use crate::audio::interpolation::{InterpolationDuration, Interpolator}; 4 | use crate::common::SampleRate; 5 | use crate::parameters::*; 6 | 7 | /// Parameter storage for audio generation. Not thread-safe. 8 | pub trait AudioParameter { 9 | type ParameterValue: ParameterValue; 10 | 11 | fn advance_one_sample(&mut self, sample_rate: SampleRate); 12 | fn get_value(&self) -> ::Value; 13 | fn set_from_patch(&mut self, value: f32); 14 | fn get_value_with_lfo_addition( 15 | &mut self, 16 | lfo_addition: Option, 17 | ) -> ::Value; 18 | 19 | fn get_parameter_value(&self) -> Self::ParameterValue { 20 | Self::ParameterValue::new_from_audio(self.get_value()) 21 | } 22 | } 23 | 24 | #[derive(Debug, Clone)] 25 | pub struct InterpolatableAudioParameter { 26 | interpolator: Interpolator, 27 | phantom_data: PhantomData, 28 | } 29 | 30 | impl Default for InterpolatableAudioParameter 31 | where 32 | V: ParameterValue + Default, 33 | { 34 | fn default() -> Self { 35 | Self { 36 | interpolator: Interpolator::new( 37 | V::default().get(), 38 | InterpolationDuration::approx_1ms(), 39 | ), 40 | phantom_data: Default::default(), 41 | } 42 | } 43 | } 44 | 45 | impl AudioParameter for InterpolatableAudioParameter 46 | where 47 | V: ParameterValue, 48 | { 49 | type ParameterValue = V; 50 | 51 | fn advance_one_sample(&mut self, sample_rate: SampleRate) { 52 | self.interpolator 53 | .advance_one_sample(sample_rate, &mut |_| ()) 54 | } 55 | fn get_value(&self) -> ::Value { 56 | self.interpolator.get_value() 57 | } 58 | fn set_from_patch(&mut self, value: f32) { 59 | self.interpolator.set_value(V::new_from_patch(value).get()) 60 | } 61 | fn get_value_with_lfo_addition( 62 | &mut self, 63 | lfo_addition: Option, 64 | ) -> ::Value { 65 | if let Some(lfo_addition) = lfo_addition { 66 | let patch_value = V::new_from_audio(self.get_value()).to_patch(); 67 | 68 | V::new_from_patch((patch_value + lfo_addition).min(1.0).max(0.0)).get() 69 | } else { 70 | self.get_value() 71 | } 72 | } 73 | } 74 | 75 | pub struct SimpleAudioParameter { 76 | value: V, 77 | patch_value_cache: f32, 78 | } 79 | 80 | impl Default for SimpleAudioParameter { 81 | fn default() -> Self { 82 | Self { 83 | value: V::default(), 84 | patch_value_cache: V::default().to_patch(), 85 | } 86 | } 87 | } 88 | 89 | impl AudioParameter for SimpleAudioParameter { 90 | type ParameterValue = V; 91 | 92 | fn advance_one_sample(&mut self, _sample_rate: SampleRate) {} 93 | fn get_value(&self) -> ::Value { 94 | self.value.get() 95 | } 96 | fn set_from_patch(&mut self, value: f32) { 97 | self.patch_value_cache = value; 98 | self.value = V::new_from_patch(value); 99 | } 100 | fn get_value_with_lfo_addition( 101 | &mut self, 102 | lfo_addition: Option, 103 | ) -> ::Value { 104 | if let Some(lfo_addition) = lfo_addition { 105 | V::new_from_patch((self.patch_value_cache + lfo_addition).min(1.0).max(0.0)).get() 106 | } else { 107 | self.get_value() 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /octasine/src/audio/parameters/lfo_active.rs: -------------------------------------------------------------------------------- 1 | use crate::audio::interpolation::{InterpolationDuration, Interpolator}; 2 | use crate::common::SampleRate; 3 | use crate::parameters::{LfoActiveValue, ParameterValue}; 4 | 5 | use super::common::AudioParameter; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct LfoActiveAudioParameter(Interpolator); 9 | 10 | impl Default for LfoActiveAudioParameter { 11 | fn default() -> Self { 12 | Self(Interpolator::new( 13 | LfoActiveValue::default().get(), 14 | InterpolationDuration::exactly_50ms(), 15 | )) 16 | } 17 | } 18 | 19 | impl AudioParameter for LfoActiveAudioParameter { 20 | type ParameterValue = LfoActiveValue; 21 | 22 | fn advance_one_sample(&mut self, sample_rate: SampleRate) { 23 | self.0.advance_one_sample(sample_rate, &mut |_| ()) 24 | } 25 | fn get_value(&self) -> ::Value { 26 | self.0.get_value() 27 | } 28 | fn set_from_patch(&mut self, value: f32) { 29 | self.0 30 | .set_value(Self::ParameterValue::new_from_patch(value).get()) 31 | } 32 | fn get_value_with_lfo_addition( 33 | &mut self, 34 | _lfo_addition: Option, 35 | ) -> ::Value { 36 | self.get_value() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /octasine/src/audio/parameters/lfo_amount.rs: -------------------------------------------------------------------------------- 1 | use crate::audio::interpolation::{InterpolationDuration, Interpolator}; 2 | use crate::common::SampleRate; 3 | use crate::math::exp2_fast; 4 | use crate::parameters::{LfoAmountValue, ParameterValue}; 5 | 6 | use super::common::AudioParameter; 7 | 8 | #[derive(Debug, Clone)] 9 | pub struct LfoAmountAudioParameter(Interpolator); 10 | 11 | impl Default for LfoAmountAudioParameter { 12 | fn default() -> Self { 13 | Self(Interpolator::new( 14 | LfoAmountValue::default().get(), 15 | InterpolationDuration::approx_1ms(), 16 | )) 17 | } 18 | } 19 | 20 | impl AudioParameter for LfoAmountAudioParameter { 21 | type ParameterValue = LfoAmountValue; 22 | 23 | fn advance_one_sample(&mut self, sample_rate: SampleRate) { 24 | self.0.advance_one_sample(sample_rate, &mut |_| ()) 25 | } 26 | fn get_value(&self) -> ::Value { 27 | self.0.get_value() 28 | } 29 | fn set_from_patch(&mut self, value: f32) { 30 | self.0 31 | .set_value(Self::ParameterValue::new_from_patch(value).get()) 32 | } 33 | fn get_value_with_lfo_addition( 34 | &mut self, 35 | lfo_addition: Option, 36 | ) -> ::Value { 37 | if let Some(lfo_addition) = lfo_addition { 38 | self.get_value() * exp2_fast(lfo_addition) 39 | } else { 40 | self.get_value() 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /octasine/src/audio/parameters/lfo_frequency_free.rs: -------------------------------------------------------------------------------- 1 | use crate::common::SampleRate; 2 | use crate::math::exp2_fast; 3 | use crate::parameters::{LfoFrequencyFreeValue, ParameterValue}; 4 | 5 | use super::common::AudioParameter; 6 | 7 | #[derive(Default)] 8 | pub struct LfoFrequencyFreeAudioParameter(LfoFrequencyFreeValue); 9 | 10 | impl AudioParameter for LfoFrequencyFreeAudioParameter { 11 | type ParameterValue = LfoFrequencyFreeValue; 12 | 13 | fn advance_one_sample(&mut self, _sample_rate: SampleRate) {} 14 | fn get_value(&self) -> ::Value { 15 | self.0.get() 16 | } 17 | fn set_from_patch(&mut self, value: f32) { 18 | self.0 = Self::ParameterValue::new_from_patch(value); 19 | } 20 | fn get_value_with_lfo_addition( 21 | &mut self, 22 | lfo_addition: Option, 23 | ) -> ::Value { 24 | if let Some(lfo_addition) = lfo_addition { 25 | self.get_value() * exp2_fast(3.0 * lfo_addition) as f64 26 | } else { 27 | self.get_value() 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /octasine/src/audio/parameters/lfo_target.rs: -------------------------------------------------------------------------------- 1 | use crate::common::SampleRate; 2 | use crate::parameters::*; 3 | 4 | use super::{ 5 | common::{AudioParameter, SimpleAudioParameter}, 6 | AudioParameterPatchInteraction, 7 | }; 8 | 9 | pub enum LfoTargetAudioParameter { 10 | One(SimpleAudioParameter), 11 | Two(SimpleAudioParameter), 12 | Three(SimpleAudioParameter), 13 | Four(SimpleAudioParameter), 14 | } 15 | 16 | impl LfoTargetAudioParameter { 17 | pub fn new(lfo_index: usize) -> Self { 18 | match lfo_index { 19 | 0 => Self::One(Default::default()), 20 | 1 => Self::Two(Default::default()), 21 | 2 => Self::Three(Default::default()), 22 | 3 => Self::Four(Default::default()), 23 | _ => unreachable!(), 24 | } 25 | } 26 | 27 | pub fn set_from_patch(&mut self, value: f32) { 28 | match self { 29 | Self::One(p) => p.set_from_patch(value), 30 | Self::Two(p) => p.set_from_patch(value), 31 | Self::Three(p) => p.set_from_patch(value), 32 | Self::Four(p) => p.set_from_patch(value), 33 | } 34 | } 35 | 36 | pub fn get_value(&self) -> LfoTargetParameter { 37 | match self { 38 | Self::One(p) => p.get_value(), 39 | Self::Two(p) => p.get_value(), 40 | Self::Three(p) => p.get_value(), 41 | Self::Four(p) => p.get_value(), 42 | } 43 | } 44 | 45 | pub fn advance_one_sample(&mut self, sample_rate: SampleRate) { 46 | match self { 47 | Self::One(p) => p.advance_one_sample(sample_rate), 48 | Self::Two(p) => p.advance_one_sample(sample_rate), 49 | Self::Three(p) => p.advance_one_sample(sample_rate), 50 | Self::Four(p) => p.advance_one_sample(sample_rate), 51 | } 52 | } 53 | } 54 | 55 | impl AudioParameterPatchInteraction for LfoTargetAudioParameter { 56 | fn set_patch_value(&mut self, value: f32) { 57 | self.set_from_patch(value) 58 | } 59 | 60 | #[cfg(test)] 61 | fn compare_patch_value(&mut self, value: f32) -> bool { 62 | let a = match self { 63 | Self::One(_) => Lfo1TargetParameterValue::new_from_patch(value).to_patch(), 64 | Self::Two(_) => Lfo2TargetParameterValue::new_from_patch(value).to_patch(), 65 | Self::Three(_) => Lfo3TargetParameterValue::new_from_patch(value).to_patch(), 66 | Self::Four(_) => Lfo4TargetParameterValue::new_from_patch(value).to_patch(), 67 | }; 68 | 69 | let b = match self { 70 | Self::One(p) => p.get_parameter_value().to_patch(), 71 | Self::Two(p) => p.get_parameter_value().to_patch(), 72 | Self::Three(p) => p.get_parameter_value().to_patch(), 73 | Self::Four(p) => p.get_parameter_value().to_patch(), 74 | }; 75 | 76 | a == b 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /octasine/src/audio/parameters/master_frequency.rs: -------------------------------------------------------------------------------- 1 | use crate::common::SampleRate; 2 | use crate::math::exp2_fast; 3 | use crate::parameters::{MasterFrequencyValue, ParameterValue}; 4 | 5 | use super::common::AudioParameter; 6 | 7 | #[derive(Default)] 8 | pub struct MasterFrequencyAudioParameter(MasterFrequencyValue); 9 | 10 | impl AudioParameter for MasterFrequencyAudioParameter { 11 | type ParameterValue = MasterFrequencyValue; 12 | 13 | fn advance_one_sample(&mut self, _sample_rate: SampleRate) {} 14 | fn get_value(&self) -> ::Value { 15 | self.0.get() 16 | } 17 | fn set_from_patch(&mut self, value: f32) { 18 | self.0 = Self::ParameterValue::new_from_patch(value); 19 | } 20 | fn get_value_with_lfo_addition( 21 | &mut self, 22 | lfo_addition: Option, 23 | ) -> ::Value { 24 | if let Some(lfo_addition) = lfo_addition { 25 | // log2(1.5) / 2 26 | const FACTOR: f32 = 0.584_962_5 / 2.0; 27 | 28 | self.get_value() * exp2_fast(FACTOR * lfo_addition) as f64 29 | } else { 30 | self.get_value() 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /octasine/src/audio/parameters/master_volume.rs: -------------------------------------------------------------------------------- 1 | use crate::audio::interpolation::{InterpolationDuration, Interpolator}; 2 | use crate::common::SampleRate; 3 | use crate::math::exp2_fast; 4 | use crate::parameters::{MasterVolumeValue, ParameterValue}; 5 | 6 | use super::common::AudioParameter; 7 | 8 | #[derive(Debug, Clone)] 9 | pub struct MasterVolumeAudioParameter(Interpolator); 10 | 11 | impl Default for MasterVolumeAudioParameter { 12 | fn default() -> Self { 13 | Self(Interpolator::new( 14 | MasterVolumeValue::default().get(), 15 | InterpolationDuration::approx_1ms(), 16 | )) 17 | } 18 | } 19 | 20 | impl AudioParameter for MasterVolumeAudioParameter { 21 | type ParameterValue = MasterVolumeValue; 22 | 23 | fn advance_one_sample(&mut self, sample_rate: SampleRate) { 24 | self.0.advance_one_sample(sample_rate, &mut |_| ()) 25 | } 26 | fn get_value(&self) -> ::Value { 27 | self.0.get_value() 28 | } 29 | fn set_from_patch(&mut self, value: f32) { 30 | self.0 31 | .set_value(Self::ParameterValue::new_from_patch(value).get()) 32 | } 33 | fn get_value_with_lfo_addition( 34 | &mut self, 35 | lfo_addition: Option, 36 | ) -> ::Value { 37 | if let Some(lfo_addition) = lfo_addition { 38 | self.get_value() * exp2_fast(lfo_addition / 2.0) 39 | } else { 40 | self.get_value() 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /octasine/src/audio/parameters/operator_active.rs: -------------------------------------------------------------------------------- 1 | use crate::audio::interpolation::{InterpolationDuration, Interpolator}; 2 | use crate::common::SampleRate; 3 | use crate::parameters::{OperatorActiveValue, ParameterValue}; 4 | 5 | use super::common::AudioParameter; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct OperatorActiveAudioParameter(Interpolator); 9 | 10 | impl Default for OperatorActiveAudioParameter { 11 | fn default() -> Self { 12 | Self(Interpolator::new( 13 | OperatorActiveValue::default().get(), 14 | InterpolationDuration::exactly_50ms(), 15 | )) 16 | } 17 | } 18 | 19 | impl AudioParameter for OperatorActiveAudioParameter { 20 | type ParameterValue = OperatorActiveValue; 21 | 22 | fn advance_one_sample(&mut self, sample_rate: SampleRate) { 23 | self.0.advance_one_sample(sample_rate, &mut |_| ()) 24 | } 25 | fn get_value(&self) -> ::Value { 26 | self.0.get_value() 27 | } 28 | fn set_from_patch(&mut self, value: f32) { 29 | self.0 30 | .set_value(Self::ParameterValue::new_from_patch(value).get()) 31 | } 32 | fn get_value_with_lfo_addition( 33 | &mut self, 34 | _lfo_addition: Option, 35 | ) -> ::Value { 36 | self.get_value() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /octasine/src/audio/parameters/operator_frequency_fine.rs: -------------------------------------------------------------------------------- 1 | use crate::common::SampleRate; 2 | use crate::math::exp2_fast; 3 | use crate::parameters::{OperatorFrequencyFineValue, ParameterValue}; 4 | 5 | use super::common::AudioParameter; 6 | 7 | #[derive(Default)] 8 | pub struct OperatorFrequencyFineAudioParameter(OperatorFrequencyFineValue); 9 | 10 | impl AudioParameter for OperatorFrequencyFineAudioParameter { 11 | type ParameterValue = OperatorFrequencyFineValue; 12 | 13 | fn advance_one_sample(&mut self, _sample_rate: SampleRate) {} 14 | fn get_value(&self) -> ::Value { 15 | self.0.get() 16 | } 17 | fn set_from_patch(&mut self, value: f32) { 18 | self.0 = Self::ParameterValue::new_from_patch(value); 19 | } 20 | fn get_value_with_lfo_addition( 21 | &mut self, 22 | lfo_addition: Option, 23 | ) -> ::Value { 24 | if let Some(lfo_addition) = lfo_addition { 25 | // log2(1.5) / 2 26 | const FACTOR: f32 = 0.584_962_5 / 2.0; 27 | 28 | self.get_value() * exp2_fast(FACTOR * lfo_addition) as f64 29 | } else { 30 | self.get_value() 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /octasine/src/audio/parameters/operator_frequency_free.rs: -------------------------------------------------------------------------------- 1 | use crate::common::SampleRate; 2 | use crate::math::exp2_fast; 3 | use crate::parameters::{OperatorFrequencyFreeValue, ParameterValue}; 4 | 5 | use super::common::AudioParameter; 6 | 7 | #[derive(Default)] 8 | pub struct OperatorFrequencyFreeAudioParameter(OperatorFrequencyFreeValue); 9 | 10 | impl AudioParameter for OperatorFrequencyFreeAudioParameter { 11 | type ParameterValue = OperatorFrequencyFreeValue; 12 | 13 | fn advance_one_sample(&mut self, _sample_rate: SampleRate) {} 14 | fn get_value(&self) -> ::Value { 15 | self.0.get() 16 | } 17 | fn set_from_patch(&mut self, value: f32) { 18 | self.0 = Self::ParameterValue::new_from_patch(value); 19 | } 20 | fn get_value_with_lfo_addition( 21 | &mut self, 22 | lfo_addition: Option, 23 | ) -> ::Value { 24 | if let Some(lfo_addition) = lfo_addition { 25 | self.get_value() * exp2_fast(4.0 * lfo_addition) as f64 26 | } else { 27 | self.get_value() 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /octasine/src/audio/parameters/operator_mix.rs: -------------------------------------------------------------------------------- 1 | use crate::audio::interpolation::{InterpolationDuration, Interpolator}; 2 | use crate::common::SampleRate; 3 | use crate::parameters::{OperatorMixOutValue, ParameterValue}; 4 | 5 | use super::common::AudioParameter; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct OperatorMixAudioParameter(Interpolator); 9 | 10 | impl OperatorMixAudioParameter { 11 | pub fn new(operator_index: usize) -> Self { 12 | Self(Interpolator::new( 13 | OperatorMixOutValue::new(operator_index).get(), 14 | InterpolationDuration::approx_1ms(), 15 | )) 16 | } 17 | } 18 | 19 | impl AudioParameter for OperatorMixAudioParameter { 20 | type ParameterValue = OperatorMixOutValue; 21 | 22 | fn advance_one_sample(&mut self, sample_rate: SampleRate) { 23 | self.0.advance_one_sample(sample_rate, &mut |_| ()) 24 | } 25 | fn get_value(&self) -> ::Value { 26 | self.0.get_value() 27 | } 28 | fn set_from_patch(&mut self, value: f32) { 29 | self.0 30 | .set_value(Self::ParameterValue::new_from_patch(value).get()) 31 | } 32 | fn get_value_with_lfo_addition( 33 | &mut self, 34 | lfo_addition: Option, 35 | ) -> ::Value { 36 | if let Some(lfo_addition) = lfo_addition { 37 | let patch_value = Self::ParameterValue::new_from_audio(self.get_value()).to_patch(); 38 | 39 | Self::ParameterValue::new_from_patch((patch_value + lfo_addition).min(1.0).max(0.0)) 40 | .get() 41 | } else { 42 | self.get_value() 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /octasine/src/audio/parameters/operator_mod_target.rs: -------------------------------------------------------------------------------- 1 | use crate::common::SampleRate; 2 | use crate::parameters::{ 3 | ModTargetStorage, Operator2ModulationTargetValue, Operator3ModulationTargetValue, 4 | Operator4ModulationTargetValue, 5 | }; 6 | 7 | use super::common::{AudioParameter, SimpleAudioParameter}; 8 | use super::AudioParameterPatchInteraction; 9 | 10 | pub enum OperatorModulationTargetAudioParameter { 11 | Two(SimpleAudioParameter), 12 | Three(SimpleAudioParameter), 13 | Four(SimpleAudioParameter), 14 | } 15 | 16 | impl OperatorModulationTargetAudioParameter { 17 | pub fn opt_new(operator_index: usize) -> Option { 18 | match operator_index { 19 | 1 => Some(OperatorModulationTargetAudioParameter::Two( 20 | Default::default(), 21 | )), 22 | 2 => Some(OperatorModulationTargetAudioParameter::Three( 23 | Default::default(), 24 | )), 25 | 3 => Some(OperatorModulationTargetAudioParameter::Four( 26 | Default::default(), 27 | )), 28 | _ => None, 29 | } 30 | } 31 | 32 | pub fn get_value(&self) -> ModTargetStorage { 33 | match self { 34 | Self::Two(p) => p.get_value(), 35 | Self::Three(p) => p.get_value(), 36 | Self::Four(p) => p.get_value(), 37 | } 38 | } 39 | 40 | pub fn advance_one_sample(&mut self, sample_rate: SampleRate) { 41 | match self { 42 | Self::Two(p) => p.advance_one_sample(sample_rate), 43 | Self::Three(p) => p.advance_one_sample(sample_rate), 44 | Self::Four(p) => p.advance_one_sample(sample_rate), 45 | } 46 | } 47 | } 48 | 49 | impl AudioParameterPatchInteraction for OperatorModulationTargetAudioParameter { 50 | fn set_patch_value(&mut self, value: f32) { 51 | match self { 52 | Self::Two(p) => p.set_from_patch(value), 53 | Self::Three(p) => p.set_from_patch(value), 54 | Self::Four(p) => p.set_from_patch(value), 55 | } 56 | } 57 | 58 | #[cfg(test)] 59 | fn compare_patch_value(&mut self, value: f32) -> bool { 60 | use crate::parameters::ParameterValue; 61 | 62 | let a = match self { 63 | Self::Two(_) => Operator2ModulationTargetValue::new_from_patch(value).to_patch(), 64 | Self::Three(_) => Operator3ModulationTargetValue::new_from_patch(value).to_patch(), 65 | Self::Four(_) => Operator4ModulationTargetValue::new_from_patch(value).to_patch(), 66 | }; 67 | 68 | let b = match self { 69 | Self::Two(p) => p.get_parameter_value().to_patch(), 70 | Self::Three(p) => p.get_parameter_value().to_patch(), 71 | Self::Four(p) => p.get_parameter_value().to_patch(), 72 | }; 73 | 74 | a == b 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /octasine/src/audio/parameters/operator_panning.rs: -------------------------------------------------------------------------------- 1 | use crate::audio::interpolation::{InterpolationDuration, Interpolator}; 2 | use crate::common::SampleRate; 3 | use crate::parameters::{OperatorPanningValue, ParameterValue}; 4 | 5 | use super::common::AudioParameter; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct OperatorPanningAudioParameter { 9 | value: Interpolator, 10 | pub left_and_right: [f32; 2], 11 | pub lfo_active: bool, 12 | } 13 | 14 | impl OperatorPanningAudioParameter { 15 | pub fn calculate_left_and_right(panning: f32) -> [f32; 2] { 16 | OperatorPanningValue::new_from_audio(panning).calculate_left_and_right() 17 | } 18 | } 19 | 20 | impl AudioParameter for OperatorPanningAudioParameter { 21 | type ParameterValue = OperatorPanningValue; 22 | 23 | fn advance_one_sample(&mut self, sample_rate: SampleRate) { 24 | let mut opt_new_left_and_right = None; 25 | 26 | self.value 27 | .advance_one_sample(sample_rate, &mut |new_panning| { 28 | opt_new_left_and_right = Some(Self::calculate_left_and_right(new_panning)); 29 | }); 30 | 31 | if let Some(new_left_and_right) = opt_new_left_and_right { 32 | self.left_and_right = new_left_and_right; 33 | } else if self.lfo_active { 34 | self.left_and_right = Self::calculate_left_and_right(self.get_value()); 35 | } 36 | 37 | self.lfo_active = false; 38 | } 39 | fn get_value(&self) -> ::Value { 40 | self.value.get_value() 41 | } 42 | fn set_from_patch(&mut self, value: f32) { 43 | self.value 44 | .set_value(Self::ParameterValue::new_from_patch(value).get()) 45 | } 46 | fn get_value_with_lfo_addition( 47 | &mut self, 48 | lfo_addition: Option, 49 | ) -> ::Value { 50 | if let Some(lfo_addition) = lfo_addition { 51 | let patch_value = Self::ParameterValue::new_from_audio(self.get_value()).to_patch(); 52 | 53 | let new_panning = Self::ParameterValue::new_from_patch( 54 | (patch_value + lfo_addition).min(1.0).max(0.0), 55 | ) 56 | .get(); 57 | 58 | self.left_and_right = Self::calculate_left_and_right(new_panning); 59 | self.lfo_active = true; 60 | 61 | new_panning 62 | } else { 63 | self.get_value() 64 | } 65 | } 66 | } 67 | 68 | impl Default for OperatorPanningAudioParameter { 69 | fn default() -> Self { 70 | let default = OperatorPanningValue::default().get(); 71 | 72 | Self { 73 | value: Interpolator::new(default, InterpolationDuration::approx_1ms()), 74 | left_and_right: Self::calculate_left_and_right(default), 75 | lfo_active: false, 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /octasine/src/audio/parameters/operator_sustain_volume.rs: -------------------------------------------------------------------------------- 1 | use crate::audio::interpolation::{InterpolationDuration, Interpolator}; 2 | use crate::common::SampleRate; 3 | use crate::parameters::{OperatorSustainVolumeValue, ParameterValue}; 4 | 5 | use super::common::AudioParameter; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct OperatorSustainVolumeAudioParameter { 9 | interpolator: Interpolator, 10 | } 11 | 12 | impl Default for OperatorSustainVolumeAudioParameter { 13 | fn default() -> Self { 14 | Self { 15 | interpolator: Interpolator::new( 16 | OperatorSustainVolumeValue::default().get(), 17 | InterpolationDuration::approx_3ms(), 18 | ), 19 | } 20 | } 21 | } 22 | 23 | impl AudioParameter for OperatorSustainVolumeAudioParameter { 24 | type ParameterValue = OperatorSustainVolumeValue; 25 | 26 | fn advance_one_sample(&mut self, sample_rate: SampleRate) { 27 | self.interpolator 28 | .advance_one_sample(sample_rate, &mut |_| ()) 29 | } 30 | fn get_value(&self) -> f32 { 31 | self.interpolator.get_value().min(1.0) 32 | } 33 | fn set_from_patch(&mut self, value: f32) { 34 | self.interpolator 35 | .set_value(Self::ParameterValue::new_from_patch(value).get()) 36 | } 37 | fn get_value_with_lfo_addition(&mut self, _lfo_addition: Option) -> f32 { 38 | self.get_value() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /octasine/src/audio/parameters/operator_volume.rs: -------------------------------------------------------------------------------- 1 | use crate::audio::interpolation::{InterpolationDuration, Interpolator}; 2 | use crate::common::SampleRate; 3 | use crate::math::exp2_fast; 4 | use crate::parameters::{OperatorVolumeValue, ParameterValue}; 5 | 6 | use super::common::AudioParameter; 7 | 8 | #[derive(Debug, Clone)] 9 | pub struct OperatorVolumeAudioParameter(Interpolator); 10 | 11 | impl Default for OperatorVolumeAudioParameter { 12 | fn default() -> Self { 13 | Self(Interpolator::new( 14 | OperatorVolumeValue::default().get(), 15 | InterpolationDuration::approx_1ms(), 16 | )) 17 | } 18 | } 19 | 20 | impl AudioParameter for OperatorVolumeAudioParameter { 21 | type ParameterValue = OperatorVolumeValue; 22 | 23 | fn advance_one_sample(&mut self, sample_rate: SampleRate) { 24 | self.0.advance_one_sample(sample_rate, &mut |_| ()) 25 | } 26 | fn get_value(&self) -> ::Value { 27 | self.0.get_value() 28 | } 29 | fn set_from_patch(&mut self, value: f32) { 30 | self.0 31 | .set_value(Self::ParameterValue::new_from_patch(value).get()) 32 | } 33 | fn get_value_with_lfo_addition( 34 | &mut self, 35 | lfo_addition: Option, 36 | ) -> ::Value { 37 | if let Some(lfo_addition) = lfo_addition { 38 | self.get_value() * exp2_fast(lfo_addition / 2.0) 39 | } else { 40 | self.get_value() 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /octasine/src/audio/voices/log10_table.rs: -------------------------------------------------------------------------------- 1 | const TABLE_SIZE: usize = 1 << 5; 2 | const TABLE_SIZE_MINUS_ONE_FLOAT: f32 = (TABLE_SIZE - 1) as f32; 3 | 4 | /// Log10 based lookup table for envelope curve, with linear interpolation 5 | /// 6 | /// Maps inputs 0.0-1.0 to output 0.0-1.0 7 | pub struct Log10Table([f32; TABLE_SIZE]); 8 | 9 | impl Log10Table { 10 | #[inline] 11 | pub fn reference(value: f64) -> f64 { 12 | (1.0 + value * 9.0).log10() 13 | } 14 | 15 | /// Get volume. Only defined where value >= 0.0 && value <= 1.0 16 | #[inline] 17 | pub fn calculate(&self, value: f32) -> f32 { 18 | let index_float = value * TABLE_SIZE_MINUS_ONE_FLOAT; 19 | let index_fract = index_float.fract(); 20 | 21 | let index_floor = index_float as usize; 22 | let index_ceil = index_floor + 1; 23 | 24 | let approximation_low = self.0[index_floor]; 25 | let approximation_high = self.0[index_ceil.min(TABLE_SIZE - 1)]; 26 | 27 | approximation_low + index_fract * (approximation_high - approximation_low) 28 | } 29 | } 30 | 31 | impl Default for Log10Table { 32 | fn default() -> Self { 33 | let mut table = [0.0; TABLE_SIZE]; 34 | 35 | let increment = 1.0 / TABLE_SIZE_MINUS_ONE_FLOAT as f64; 36 | 37 | for (i, v) in table.iter_mut().enumerate() { 38 | *v = Self::reference(i as f64 * increment) as f32; 39 | } 40 | 41 | Self(table) 42 | } 43 | } 44 | 45 | #[cfg(test)] 46 | mod tests { 47 | use quickcheck::{quickcheck, TestResult}; 48 | 49 | use super::*; 50 | 51 | #[test] 52 | fn test_table_calculate() { 53 | fn prop(value: f32) -> TestResult { 54 | if !(0.0..=1.0).contains(&value) || value.is_nan() { 55 | return TestResult::discard(); 56 | } 57 | 58 | let table = Log10Table::default(); 59 | 60 | let table_result = table.calculate(value); 61 | let reference_result = Log10Table::reference(value as f64) as f32; 62 | let diff = (table_result - reference_result).abs(); 63 | 64 | let success = diff < 0.005; 65 | 66 | if !success { 67 | println!(); 68 | println!("input value: {}", value); 69 | println!("table result: {}", table_result); 70 | println!("reference result: {}", reference_result); 71 | println!("difference: {}", diff); 72 | } 73 | 74 | TestResult::from_bool(success) 75 | } 76 | 77 | quickcheck(prop as fn(f32) -> TestResult); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /octasine/src/common.rs: -------------------------------------------------------------------------------- 1 | use crate::parameters::ParameterKey; 2 | 3 | pub const NUM_OPERATORS: usize = 4; 4 | pub const NUM_LFOS: usize = 4; 5 | 6 | pub const OPERATOR_MOD_INDEX_STEPS: [f32; 16] = [ 7 | 0.0, 0.01, 0.1, 0.2, 0.5, 1.0, 2.0, 3.0, 5.0, 10.0, 20.0, 35.0, 50.0, 75.0, 100.0, 1000.0, 8 | ]; 9 | 10 | pub type IndexMap = indexmap::IndexMap; 11 | pub type IndexSet = indexmap::IndexSet; 12 | 13 | pub trait WaveformChoices: PartialEq + Copy { 14 | fn calculate_for_current(self, phase: Phase) -> f32; 15 | fn choices() -> &'static [Self]; 16 | } 17 | 18 | /// Phase. value >= 0.0 && value < 1.0 19 | #[derive(Debug, Copy, Clone)] 20 | pub struct Phase(pub f64); 21 | 22 | #[derive(Debug, Copy, Clone, PartialEq)] 23 | pub struct SampleRate(pub f64); 24 | 25 | impl Default for SampleRate { 26 | fn default() -> Self { 27 | Self(44100.0) 28 | } 29 | } 30 | 31 | impl From for TimePerSample { 32 | fn from(val: SampleRate) -> Self { 33 | TimePerSample(1.0 / val.0) 34 | } 35 | } 36 | 37 | #[derive(Debug, Copy, Clone, PartialEq)] 38 | pub struct TimePerSample(pub f64); 39 | 40 | #[derive(Debug, Copy, Clone, PartialEq)] 41 | pub struct BeatsPerMinute(pub f64); 42 | 43 | impl BeatsPerMinute { 44 | pub fn one_hertz() -> Self { 45 | Self(60.0) 46 | } 47 | } 48 | 49 | impl Default for BeatsPerMinute { 50 | fn default() -> Self { 51 | Self(120.0) 52 | } 53 | } 54 | 55 | #[derive(Debug, Copy, Clone, PartialEq)] 56 | pub struct BpmLfoMultiplier(pub f64); 57 | 58 | impl From for BpmLfoMultiplier { 59 | fn from(bpm: BeatsPerMinute) -> Self { 60 | Self(bpm.0 / 120.0) 61 | } 62 | } 63 | 64 | #[derive(Debug, Copy, Clone, PartialEq)] 65 | pub enum EnvelopeStage { 66 | Attack, 67 | Decay, 68 | Sustain, 69 | Release, 70 | Ended, 71 | Kill, 72 | } 73 | 74 | #[derive(Debug, Clone, Copy)] 75 | pub struct NoteEvent { 76 | pub delta_frames: u32, 77 | pub event: NoteEventInner, 78 | } 79 | 80 | #[derive(Debug, Clone, Copy)] 81 | pub enum NoteEventInner { 82 | Midi { 83 | data: [u8; 3], 84 | }, 85 | ClapNoteOn { 86 | key: u8, 87 | velocity: f64, 88 | clap_note_id: i32, 89 | }, 90 | ClapNoteOff { 91 | key: u8, 92 | }, 93 | ClapNotePressure { 94 | key: u8, 95 | // 0..1 96 | pressure: f64, 97 | }, 98 | ClapBpm { 99 | bpm: BeatsPerMinute, 100 | }, 101 | } 102 | 103 | #[derive(Debug, Clone, Copy)] 104 | pub enum EventToHost { 105 | StartAutomating(ParameterKey), 106 | Automate(ParameterKey, f32), 107 | EndAutomating(ParameterKey), 108 | RescanValues, 109 | StateChanged, 110 | } 111 | -------------------------------------------------------------------------------- /octasine/src/gui/common.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use iced_baseview::{ 4 | widget::Column, 5 | widget::Row, 6 | widget::Space, 7 | widget::{tooltip::Position, Container, Tooltip}, 8 | Element, Length, 9 | }; 10 | 11 | use super::LINE_HEIGHT; 12 | 13 | use super::{ 14 | style::{container::ContainerStyle, Theme}, 15 | Message, 16 | }; 17 | 18 | pub fn container_l1<'a, T>(contents: T) -> Container<'a, Message, Theme> 19 | where 20 | T: Into>, 21 | { 22 | Container::new(contents).style(ContainerStyle::L1) 23 | } 24 | 25 | pub fn container_l2<'a, T>(contents: T) -> Container<'a, Message, Theme> 26 | where 27 | T: Into>, 28 | { 29 | let padding_x = LINE_HEIGHT.into(); 30 | let padding_y = 0.0; 31 | 32 | let contents = Row::new() 33 | .push(Space::with_width(Length::Fixed(padding_x))) 34 | .push( 35 | Column::new() 36 | .push(Space::with_height(Length::Fixed(padding_y))) 37 | .push(contents) 38 | .push(Space::with_height(Length::Fixed(padding_y))), 39 | ) 40 | .push(Space::with_width(Length::Fixed(padding_x))); 41 | 42 | Container::new(contents) 43 | .padding(0) 44 | .style(ContainerStyle::L2) 45 | } 46 | 47 | pub fn container_l3<'a, T>(contents: T) -> Container<'a, Message, Theme> 48 | where 49 | T: Into>, 50 | { 51 | let padding_x = 0.0; 52 | let padding_y = LINE_HEIGHT.into(); 53 | 54 | let contents = Row::new() 55 | .push(Space::with_width(Length::Fixed(padding_x))) 56 | .push( 57 | Column::new() 58 | .push(Space::with_height(Length::Fixed(padding_y))) 59 | .push(contents) 60 | .push(Space::with_height(Length::Fixed(padding_y))), 61 | ) 62 | .push(Space::with_width(Length::Fixed(padding_x))); 63 | 64 | Container::new(contents) 65 | .padding(0) 66 | .style(ContainerStyle::L3) 67 | } 68 | 69 | pub fn triple_container<'a, T>(contents: T) -> Container<'a, Message, Theme> 70 | where 71 | T: Into>, 72 | { 73 | container_l1(container_l2(container_l3(contents))) 74 | } 75 | 76 | pub fn space_l2<'a>() -> Container<'a, Message, Theme> { 77 | Container::new(Space::with_width(Length::Fixed(LINE_HEIGHT.into()))) 78 | } 79 | 80 | pub fn space_l3<'a>() -> Container<'a, Message, Theme> { 81 | Container::new(Space::with_width(Length::Fixed(0.0))) 82 | } 83 | 84 | pub fn tooltip<'a>( 85 | theme: &Theme, 86 | text: impl Into>, 87 | position: Position, 88 | contents: impl Into>, 89 | ) -> Tooltip<'a, Message, Theme> { 90 | Tooltip::new(contents, text, position) 91 | .font(theme.font_regular()) 92 | .style(ContainerStyle::Tooltip) 93 | .padding(theme.tooltip_padding()) 94 | } 95 | -------------------------------------------------------------------------------- /octasine/src/gui/lfo_target_picker.rs: -------------------------------------------------------------------------------- 1 | use iced_baseview::widget::PickList; 2 | use iced_baseview::{Element, Length}; 3 | 4 | use crate::parameters::lfo_target::LfoTargetParameter; 5 | use crate::parameters::{ 6 | get_lfo_target_parameters, Lfo1TargetParameterValue, Lfo2TargetParameterValue, 7 | Lfo3TargetParameterValue, Lfo4TargetParameterValue, LfoParameter, Parameter, ParameterValue, 8 | WrappedParameter, 9 | }; 10 | 11 | use super::{style::Theme, GuiSyncHandle, Message, FONT_SIZE}; 12 | 13 | #[derive(Clone, PartialEq, Eq)] 14 | struct LfoTarget { 15 | value: LfoTargetParameter, 16 | title: String, 17 | } 18 | 19 | impl ToString for LfoTarget { 20 | fn to_string(&self) -> String { 21 | self.title.clone() 22 | } 23 | } 24 | 25 | pub struct LfoTargetPicker { 26 | options: Vec, 27 | selected: usize, 28 | lfo_index: usize, 29 | parameter: WrappedParameter, 30 | } 31 | 32 | impl LfoTargetPicker { 33 | pub fn new(sync_handle: &H, lfo_index: usize) -> Self { 34 | let parameter = Parameter::Lfo(lfo_index as u8, LfoParameter::Target).into(); 35 | let sync_value = sync_handle.get_parameter(parameter); 36 | let selected = Self::get_index_from_sync(lfo_index, sync_value); 37 | let target_parameters = get_lfo_target_parameters(lfo_index); 38 | 39 | let options = target_parameters 40 | .iter() 41 | .map(|target| LfoTarget { 42 | value: *target, 43 | title: target.parameter().name().to_uppercase(), 44 | }) 45 | .collect(); 46 | 47 | Self { 48 | options, 49 | selected, 50 | lfo_index, 51 | parameter, 52 | } 53 | } 54 | 55 | fn get_index_from_sync(lfo_index: usize, sync_value: f32) -> usize { 56 | let target = match lfo_index { 57 | 0 => Lfo1TargetParameterValue::new_from_patch(sync_value).0, 58 | 1 => Lfo2TargetParameterValue::new_from_patch(sync_value).0, 59 | 2 => Lfo3TargetParameterValue::new_from_patch(sync_value).0, 60 | 3 => Lfo4TargetParameterValue::new_from_patch(sync_value).0, 61 | _ => unreachable!(), 62 | }; 63 | 64 | let target_parameters = get_lfo_target_parameters(lfo_index); 65 | 66 | for (i, t) in target_parameters.iter().enumerate() { 67 | if *t == target { 68 | return i; 69 | } 70 | } 71 | 72 | unreachable!() 73 | } 74 | 75 | pub fn set_value(&mut self, sync_value: f32) { 76 | self.selected = Self::get_index_from_sync(self.lfo_index, sync_value); 77 | } 78 | 79 | pub fn view(&self, theme: &Theme) -> Element { 80 | let lfo_index = self.lfo_index; 81 | let parameter = self.parameter; 82 | 83 | PickList::new( 84 | &self.options[..], 85 | Some(self.options[self.selected].clone()), 86 | move |option| { 87 | let sync = match lfo_index { 88 | 0 => Lfo1TargetParameterValue::new_from_audio(option.value).to_patch(), 89 | 1 => Lfo2TargetParameterValue::new_from_audio(option.value).to_patch(), 90 | 2 => Lfo3TargetParameterValue::new_from_audio(option.value).to_patch(), 91 | 3 => Lfo4TargetParameterValue::new_from_audio(option.value).to_patch(), 92 | _ => unreachable!(), 93 | }; 94 | 95 | Message::ChangeSingleParameterImmediate(parameter, sync) 96 | }, 97 | ) 98 | .font(theme.font_regular()) 99 | .text_size(FONT_SIZE) 100 | .padding(theme.picklist_padding()) 101 | .width(Length::Fill) 102 | .into() 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /octasine/src/gui/mod_matrix/common.rs: -------------------------------------------------------------------------------- 1 | use iced_baseview::{Point, Size, Vector}; 2 | 3 | use super::SCALE; 4 | 5 | #[derive(Default)] 6 | pub enum BoxStatus { 7 | #[default] 8 | Normal, 9 | Hover, 10 | Dragging { 11 | from: Point, 12 | original_value: f32, 13 | }, 14 | } 15 | 16 | impl BoxStatus { 17 | pub fn is_dragging(&self) -> bool { 18 | matches!(self, Self::Dragging { .. }) 19 | } 20 | } 21 | 22 | pub fn get_box_base_point_and_size(bounds: Size, x: usize, y: usize) -> (Point, Size) { 23 | let x_bla = bounds.width / 7.0; 24 | let y_bla = bounds.height / 8.0; 25 | 26 | let base_top_left = Point::new(x as f32 * x_bla, y as f32 * y_bla); 27 | 28 | let base_size = Size::new(x_bla, y_bla); 29 | 30 | (base_top_left, base_size) 31 | } 32 | 33 | pub fn scale_point(bounds: Size, point: Point) -> Point { 34 | let translation = Vector { 35 | x: (1.0 - SCALE) * bounds.width / 2.0, 36 | y: (1.0 - SCALE) * bounds.height / 2.0, 37 | }; 38 | 39 | let scaled = Point { 40 | x: point.x * SCALE, 41 | y: point.y * SCALE, 42 | }; 43 | 44 | scaled + translation 45 | } 46 | 47 | pub fn scale_size(size: Size) -> Size { 48 | Size::new(size.width * SCALE, size.height * SCALE) 49 | } 50 | -------------------------------------------------------------------------------- /octasine/src/gui/mod_matrix/mix_line.rs: -------------------------------------------------------------------------------- 1 | use iced_baseview::widget::canvas::{Frame, Path, Stroke}; 2 | use iced_baseview::{Color, Point}; 3 | use palette::gradient::Gradient; 4 | use palette::Srgba; 5 | 6 | use crate::gui::style::Theme; 7 | use crate::gui::SnapPoint; 8 | 9 | use super::StyleSheet; 10 | 11 | pub struct MixOutLine { 12 | path: Path, 13 | additive: f32, 14 | } 15 | 16 | impl MixOutLine { 17 | pub fn new(from: Point, to_y: f32, additive: f32) -> Self { 18 | let mut to = from; 19 | 20 | to.y = to_y; 21 | 22 | let path = Path::line(from.snap(), to.snap()); 23 | 24 | Self { path, additive } 25 | } 26 | 27 | pub fn update(&mut self, additive: f32) { 28 | self.additive = additive; 29 | } 30 | 31 | fn calculate_color(&self, additive: f32, theme: &Theme) -> Color { 32 | let bg = theme.appearance().background_color; 33 | let c = theme.appearance().line_max_color; 34 | let line_color = theme.appearance().mix_out_line_color; 35 | 36 | let gradient = Gradient::new(vec![ 37 | Srgba::new(bg.r, bg.g, bg.b, 1.0).into_linear(), 38 | // Srgba::new(0.23, 0.69, 0.06, 1.0).into_linear(), 39 | Srgba::new(line_color.r, line_color.g, line_color.b, line_color.a).into_linear(), 40 | Srgba::new(c.r, c.g, c.b, 1.0).into_linear(), 41 | ]); 42 | 43 | let color = gradient.get(additive); 44 | let color = Srgba::from_linear(color); 45 | 46 | Color::new(color.red, color.green, color.blue, color.alpha) 47 | } 48 | 49 | pub fn draw(&self, frame: &mut Frame, theme: &Theme) { 50 | let calculated_color = self.calculate_color(self.additive, theme); 51 | 52 | let stroke = Stroke::default() 53 | .with_width(3.0) 54 | .with_color(calculated_color); 55 | 56 | frame.stroke(&self.path, stroke); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /octasine/src/gui/mod_matrix/mod_lines.rs: -------------------------------------------------------------------------------- 1 | use arrayvec::ArrayVec; 2 | use iced_baseview::widget::canvas::{path, Frame, Path, Stroke}; 3 | use iced_baseview::Point; 4 | 5 | use crate::gui::style::Theme; 6 | 7 | use super::StyleSheet; 8 | 9 | pub struct ModOutLines { 10 | from: Point, 11 | paths: ArrayVec, 12 | } 13 | 14 | impl ModOutLines { 15 | pub fn new(from: Point) -> Self { 16 | Self { 17 | from, 18 | paths: Default::default(), 19 | } 20 | } 21 | 22 | pub fn update>(&mut self, lines: I) { 23 | self.paths = lines 24 | .map(|points| { 25 | let mut builder = path::Builder::new(); 26 | 27 | builder.move_to(self.from); 28 | 29 | for point in points.iter() { 30 | builder.line_to(*point); 31 | } 32 | 33 | builder.build() 34 | }) 35 | .collect(); 36 | } 37 | 38 | pub fn draw(&self, frame: &mut Frame, theme: &Theme) { 39 | let color = theme.appearance().mod_out_line_color; 40 | 41 | for path in self.paths.iter() { 42 | let stroke = Stroke::default().with_width(3.0).with_color(color); 43 | 44 | frame.stroke(path, stroke); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /octasine/src/gui/mod_matrix/output_box.rs: -------------------------------------------------------------------------------- 1 | use iced_baseview::widget::canvas::{Frame, Path, Stroke}; 2 | use iced_baseview::{Point, Size}; 3 | 4 | use crate::gui::style::Theme; 5 | use crate::gui::SnapPoint; 6 | 7 | use super::{common::*, StyleSheet, OPERATOR_BOX_SCALE}; 8 | 9 | pub struct OutputBox { 10 | path: Path, 11 | pub y: f32, 12 | } 13 | 14 | impl OutputBox { 15 | pub fn new(bounds: Size) -> Self { 16 | let (base_top_left, base_size) = get_box_base_point_and_size(bounds, 0, 7); 17 | 18 | let height = base_size.height * OPERATOR_BOX_SCALE; 19 | let width = base_size.width * 6.0 + base_size.width * OPERATOR_BOX_SCALE; 20 | 21 | let left = Point { 22 | x: base_top_left.x - (OPERATOR_BOX_SCALE - 1.0) * base_size.width / 2.0, 23 | y: base_top_left.y - (OPERATOR_BOX_SCALE - 1.0) * base_size.height / 2.0 + height, 24 | }; 25 | let right = Point { 26 | x: left.x + width, 27 | y: left.y, 28 | }; 29 | 30 | let mut left = scale_point(bounds, left); 31 | let mut right = scale_point(bounds, right); 32 | 33 | // left.x += 1.0; 34 | // right.x += 1.0; 35 | 36 | left = left.snap(); 37 | right = right.snap(); 38 | 39 | let path = Path::line(left, right); 40 | 41 | Self { path, y: left.y } 42 | } 43 | 44 | pub fn draw(&self, frame: &mut Frame, theme: &Theme) { 45 | let stroke = Stroke::default() 46 | .with_color(theme.appearance().box_border_color) 47 | .with_width(1.0); 48 | 49 | frame.stroke(&self.path, stroke); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /octasine/src/gui/mod_target_picker.rs: -------------------------------------------------------------------------------- 1 | use iced_baseview::widget::tooltip::Position; 2 | use iced_baseview::{ 3 | alignment::Horizontal, widget::Checkbox, widget::Column, widget::Space, widget::Text, 4 | Alignment, Element, Length, 5 | }; 6 | 7 | use crate::parameters::operator_mod_target::ModTargetStorage; 8 | use crate::parameters::{ 9 | Operator2ModulationTargetValue, Operator3ModulationTargetValue, Operator4ModulationTargetValue, 10 | OperatorParameter, Parameter, ParameterValue, WrappedParameter, 11 | }; 12 | use crate::sync::GuiSyncHandle; 13 | 14 | use super::common::tooltip; 15 | use super::style::Theme; 16 | use super::{Message, FONT_SIZE, LINE_HEIGHT}; 17 | 18 | pub fn operator_2_target( 19 | sync_handle: &H, 20 | operator_index: usize, 21 | ) -> ModTargetPicker { 22 | ModTargetPicker::new(sync_handle, operator_index, "TARGET", vec![0]) 23 | } 24 | 25 | pub fn operator_3_target( 26 | sync_handle: &H, 27 | operator_index: usize, 28 | ) -> ModTargetPicker { 29 | ModTargetPicker::new(sync_handle, operator_index, "TARGET", vec![1, 0]) 30 | } 31 | 32 | pub fn operator_4_target( 33 | sync_handle: &H, 34 | operator_index: usize, 35 | ) -> ModTargetPicker { 36 | ModTargetPicker::new(sync_handle, operator_index, "TARGET", vec![2, 1, 0]) 37 | } 38 | 39 | #[derive(Debug, Clone)] 40 | pub struct ModTargetPicker

{ 41 | title: String, 42 | parameter: WrappedParameter, 43 | choices: Vec, 44 | parameter_value: P, 45 | } 46 | 47 | impl

ModTargetPicker

48 | where 49 | P: 'static + ParameterValue + Copy, 50 | { 51 | fn new( 52 | sync_handle: &H, 53 | operator_index: usize, 54 | title: &str, 55 | choices: Vec, 56 | ) -> Self { 57 | let parameter = 58 | Parameter::Operator(operator_index as u8, OperatorParameter::ModTargets).into(); 59 | let sync_value = sync_handle.get_parameter(parameter); 60 | 61 | Self { 62 | title: title.into(), 63 | parameter, 64 | choices, 65 | parameter_value: P::new_from_patch(sync_value), 66 | } 67 | } 68 | 69 | pub fn set_value(&mut self, value: f32) { 70 | self.parameter_value = P::new_from_patch(value); 71 | } 72 | 73 | pub fn view(&self, theme: &Theme) -> Element { 74 | let title = Text::new(self.title.clone()) 75 | .horizontal_alignment(Horizontal::Center) 76 | .font(theme.font_bold()) 77 | .height(Length::Fixed(LINE_HEIGHT.into())); 78 | let title = tooltip( 79 | theme, 80 | "Target operators for modulation", 81 | Position::Top, 82 | title, 83 | ); 84 | 85 | let mut checkboxes = Column::new().spacing(4); 86 | 87 | for index in self.choices.iter().copied() { 88 | let active = self.parameter_value.get().index_active(index); 89 | let label = format!("{}", index + 1); 90 | let v = self.parameter_value.get(); 91 | let parameter = self.parameter; 92 | 93 | let checkbox = Checkbox::new(label, active, move |active| { 94 | let mut v = v; 95 | 96 | v.set_index(index, active); 97 | 98 | let sync = P::new_from_audio(v).to_patch(); 99 | 100 | Message::ChangeSingleParameterImmediate(parameter, sync) 101 | }) 102 | .font(theme.font_regular()) 103 | .size(FONT_SIZE) 104 | .text_size(FONT_SIZE) 105 | .spacing(4); 106 | 107 | checkboxes = checkboxes.push(checkbox); 108 | } 109 | 110 | Column::new() 111 | .width(Length::Fixed(f32::from(LINE_HEIGHT * 4))) 112 | .height(Length::Fixed(f32::from(LINE_HEIGHT * 6))) 113 | .align_items(Alignment::Center) 114 | .push(title) 115 | .push(Space::with_height(Length::Fixed(LINE_HEIGHT.into()))) 116 | .push(checkboxes) 117 | .into() 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /octasine/src/gui/style/application.rs: -------------------------------------------------------------------------------- 1 | use iced_baseview::{ 2 | widget::application::{Appearance, StyleSheet}, 3 | Color, 4 | }; 5 | 6 | use super::Theme; 7 | 8 | impl StyleSheet for Theme { 9 | type Style = (); 10 | 11 | fn appearance(&self, _style: &Self::Style) -> Appearance { 12 | match self { 13 | Self::Light => Appearance { 14 | background_color: Color::WHITE, 15 | text_color: Color::BLACK, 16 | }, 17 | Self::Dark => Appearance { 18 | background_color: Color::BLACK, 19 | text_color: Color::WHITE, 20 | }, 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /octasine/src/gui/style/boolean_button.rs: -------------------------------------------------------------------------------- 1 | use iced_baseview::Color; 2 | 3 | use crate::gui::boolean_button::{Appearance, StyleSheet}; 4 | 5 | use super::Theme; 6 | 7 | #[derive(Default, Clone, Copy)] 8 | pub enum BooleanButtonStyle { 9 | #[default] 10 | Regular, 11 | Mute, 12 | } 13 | 14 | impl StyleSheet for Theme { 15 | type Style = BooleanButtonStyle; 16 | 17 | fn active(&self, style: &Self::Style, hover: bool) -> Appearance { 18 | match self { 19 | Self::Dark => { 20 | use super::colors::dark::*; 21 | 22 | let color = match style { 23 | Self::Style::Regular => BLUE, 24 | Self::Style::Mute => RED, 25 | }; 26 | 27 | Appearance { 28 | background_color: Color::TRANSPARENT, 29 | border_color: color, 30 | text_color: color, 31 | } 32 | } 33 | Self::Light => { 34 | use super::colors::light::*; 35 | 36 | let color = match style { 37 | Self::Style::Regular => BLUE, 38 | Self::Style::Mute => RED, 39 | }; 40 | 41 | Appearance { 42 | background_color: if hover { SURFACE_HOVER } else { SURFACE }, 43 | border_color: color, 44 | text_color: color, 45 | } 46 | } 47 | } 48 | } 49 | 50 | fn inactive(&self, _style: &Self::Style, hover: bool) -> Appearance { 51 | match self { 52 | Self::Dark => { 53 | use super::colors::dark::*; 54 | 55 | if hover { 56 | Appearance { 57 | background_color: Color::TRANSPARENT, 58 | border_color: GRAY_800, 59 | text_color: GRAY_900, 60 | } 61 | } else { 62 | Appearance { 63 | background_color: Color::TRANSPARENT, 64 | border_color: BORDER_DARK, 65 | text_color: GRAY_700, 66 | } 67 | } 68 | } 69 | Self::Light => { 70 | use super::colors::light::*; 71 | 72 | Appearance { 73 | background_color: if hover { SURFACE_HOVER } else { SURFACE }, 74 | border_color: BORDER, 75 | text_color: TEXT, 76 | } 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /octasine/src/gui/style/button.rs: -------------------------------------------------------------------------------- 1 | use iced_baseview::{ 2 | widget::button::{Appearance, StyleSheet}, 3 | Color, 4 | }; 5 | 6 | use super::Theme; 7 | 8 | #[derive(Default)] 9 | pub enum ButtonStyle { 10 | #[default] 11 | Regular, 12 | Value, 13 | } 14 | 15 | impl StyleSheet for Theme { 16 | type Style = ButtonStyle; 17 | 18 | fn active(&self, style: &Self::Style) -> Appearance { 19 | match style { 20 | Self::Style::Regular => match self { 21 | Self::Light => { 22 | use super::colors::light::*; 23 | 24 | Appearance { 25 | background: SURFACE.into(), 26 | border_radius: 3.0, 27 | border_width: 1.0, 28 | border_color: BORDER, 29 | text_color: TEXT, 30 | ..Default::default() 31 | } 32 | } 33 | Self::Dark => { 34 | use super::colors::dark::*; 35 | 36 | Appearance { 37 | background: SURFACE.into(), 38 | border_radius: 3.0, 39 | border_width: 0.0, 40 | border_color: TEXT, 41 | text_color: TEXT, 42 | ..Default::default() 43 | } 44 | } 45 | }, 46 | Self::Style::Value => match self { 47 | Self::Light => { 48 | use super::colors::light::*; 49 | 50 | Appearance { 51 | background: Color::TRANSPARENT.into(), 52 | border_radius: 3.0, 53 | border_width: 0.0, 54 | border_color: Color::TRANSPARENT, 55 | text_color: TEXT, 56 | ..Default::default() 57 | } 58 | } 59 | Self::Dark => { 60 | use super::colors::dark::*; 61 | 62 | Appearance { 63 | background: Color::TRANSPARENT.into(), 64 | border_radius: 3.0, 65 | border_width: 0.0, 66 | border_color: Color::TRANSPARENT, 67 | text_color: TEXT, 68 | ..Default::default() 69 | } 70 | } 71 | }, 72 | } 73 | } 74 | 75 | fn hovered(&self, style: &Self::Style) -> Appearance { 76 | match style { 77 | Self::Style::Regular => match self { 78 | Self::Light => { 79 | use super::colors::light::*; 80 | 81 | Appearance { 82 | background: SURFACE_HOVER.into(), 83 | ..self.active(style) 84 | } 85 | } 86 | Self::Dark => { 87 | use super::colors::dark::*; 88 | 89 | Appearance { 90 | background: SURFACE_HOVER.into(), 91 | text_color: HOVERED, 92 | ..self.active(style) 93 | } 94 | } 95 | }, 96 | Self::Style::Value => match self { 97 | Self::Light => { 98 | use super::colors::light::*; 99 | 100 | Appearance { 101 | background: SURFACE_HOVER.into(), 102 | ..self.active(style) 103 | } 104 | } 105 | Self::Dark => { 106 | use super::colors::dark::*; 107 | 108 | Appearance { 109 | background: SURFACE_HOVER.into(), 110 | text_color: HOVERED, 111 | ..self.active(style) 112 | } 113 | } 114 | }, 115 | } 116 | } 117 | 118 | fn pressed(&self, style: &Self::Style) -> Appearance { 119 | self.hovered(style) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /octasine/src/gui/style/card.rs: -------------------------------------------------------------------------------- 1 | use iced_aw::native::card::StyleSheet; 2 | use iced_aw::style::card::Appearance; 3 | use iced_baseview::Color; 4 | 5 | use super::Theme; 6 | 7 | impl StyleSheet for Theme { 8 | type Style = (); 9 | 10 | fn active(&self, _style: Self::Style) -> Appearance { 11 | match self { 12 | Self::Dark => { 13 | use super::colors::dark::{BACKGROUND, GRAY_100, GRAY_200, TEXT}; 14 | 15 | Appearance { 16 | background: BACKGROUND.into(), 17 | border_radius: 3.0, 18 | border_width: 0.0, 19 | border_color: Color::TRANSPARENT, 20 | head_background: GRAY_200.into(), 21 | head_text_color: TEXT, 22 | body_background: GRAY_100.into(), 23 | body_text_color: TEXT, 24 | foot_background: GRAY_100.into(), 25 | foot_text_color: TEXT, 26 | close_color: TEXT, 27 | } 28 | } 29 | Self::Light => { 30 | use super::colors::light::{BACKGROUND, BLUE, GRAY_900, TEXT}; 31 | 32 | Appearance { 33 | background: BACKGROUND.into(), 34 | border_radius: 3.0, 35 | border_width: 0.0, 36 | border_color: Color::TRANSPARENT, 37 | head_background: BLUE.into(), 38 | head_text_color: Color::WHITE, 39 | body_background: Color::WHITE.into(), 40 | body_text_color: TEXT, 41 | foot_background: GRAY_900.into(), 42 | foot_text_color: TEXT, 43 | close_color: TEXT, 44 | } 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /octasine/src/gui/style/checkbox.rs: -------------------------------------------------------------------------------- 1 | use iced_baseview::{ 2 | widget::checkbox::{Appearance, StyleSheet}, 3 | Color, 4 | }; 5 | 6 | use super::Theme; 7 | 8 | impl StyleSheet for Theme { 9 | type Style = (); 10 | 11 | fn active(&self, _style: &Self::Style, _is_checked: bool) -> Appearance { 12 | match self { 13 | Self::Light => { 14 | use super::colors::light::*; 15 | 16 | Appearance { 17 | background: SURFACE.into(), 18 | icon_color: BLUE, 19 | text_color: Some(TEXT), 20 | border_width: 1.0, 21 | border_color: BORDER, 22 | border_radius: 3.0, 23 | } 24 | } 25 | Self::Dark => { 26 | use super::colors::dark::*; 27 | 28 | Appearance { 29 | background: Color::TRANSPARENT.into(), 30 | icon_color: BLUE, 31 | text_color: Some(TEXT), 32 | border_width: 1.0, 33 | border_color: BORDER, 34 | border_radius: 3.0, 35 | } 36 | } 37 | } 38 | } 39 | 40 | fn hovered(&self, style: &Self::Style, is_checked: bool) -> Appearance { 41 | match self { 42 | Self::Light => { 43 | use super::colors::light::*; 44 | 45 | Appearance { 46 | background: SURFACE_HOVER.into(), 47 | ..self.active(style, is_checked) 48 | } 49 | } 50 | Self::Dark => { 51 | use super::colors::dark::*; 52 | 53 | Appearance { 54 | border_color: BORDER_HOVERED, 55 | ..self.active(style, is_checked) 56 | } 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /octasine/src/gui/style/colors/dark.rs: -------------------------------------------------------------------------------- 1 | use iced_baseview::Color; 2 | 3 | use crate::{hex, hex_gray}; 4 | 5 | pub const RED: Color = hex!(0xEF, 0x53, 0x50); 6 | pub const BLUE: Color = hex!(0x50, 0x9D, 0xEF); 7 | pub const GREEN: Color = hex!(0x50, 0xEF, 0xA2); 8 | 9 | pub const GRAY_100: Color = hex_gray!(0x20); 10 | pub const GRAY_200: Color = hex_gray!(0x2A); 11 | pub const GRAY_300: Color = hex_gray!(0x40); 12 | pub const GRAY_400: Color = hex_gray!(0x50); 13 | pub const GRAY_500: Color = hex_gray!(0x60); 14 | pub const GRAY_600: Color = hex_gray!(0x70); 15 | pub const GRAY_700: Color = hex_gray!(0x90); 16 | pub const GRAY_800: Color = hex_gray!(0xB0); 17 | pub const GRAY_900: Color = hex_gray!(0xD0); 18 | 19 | pub const BACKGROUND: Color = hex_gray!(0x00); 20 | pub const SURFACE: Color = GRAY_400; 21 | pub const SURFACE_HOVER: Color = GRAY_500; 22 | pub const TEXT: Color = GRAY_900; 23 | pub const HOVERED: Color = hex_gray!(0xF8); 24 | pub const PRESSED: Color = hex_gray!(0xFF); 25 | pub const BORDER: Color = GRAY_700; 26 | pub const BORDER_DARK: Color = GRAY_500; 27 | pub const BORDER_HOVERED: Color = GRAY_900; 28 | -------------------------------------------------------------------------------- /octasine/src/gui/style/colors/light.rs: -------------------------------------------------------------------------------- 1 | use iced_baseview::Color; 2 | 3 | use crate::{hex, hex_gray}; 4 | 5 | pub const RED: Color = hex!(0xEF, 0x00, 0x00); 6 | pub const BLUE: Color = hex!(0x00, 0x78, 0xEF); 7 | pub const GREEN: Color = hex!(0x00, 0xEF, 0x78); 8 | 9 | pub const GRAY_300: Color = hex_gray!(0x60); 10 | pub const GRAY_400: Color = hex_gray!(0x77); 11 | pub const GRAY_450: Color = hex_gray!(0x87); 12 | pub const GRAY_500: Color = hex_gray!(0xA0); 13 | pub const GRAY_600: Color = hex_gray!(0xB0); 14 | pub const GRAY_700: Color = hex_gray!(0xD0); 15 | pub const GRAY_800: Color = hex_gray!(0xE0); 16 | pub const GRAY_900: Color = hex_gray!(0xEA); 17 | 18 | pub const BACKGROUND: Color = GRAY_700; 19 | pub const SURFACE: Color = Color::WHITE; 20 | pub const SURFACE_HOVER: Color = GRAY_800; 21 | pub const SURFACE_PRESS: Color = GRAY_700; 22 | pub const TEXT: Color = Color::BLACK; 23 | pub const BORDER: Color = GRAY_500; 24 | -------------------------------------------------------------------------------- /octasine/src/gui/style/colors/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod dark; 2 | pub mod light; 3 | -------------------------------------------------------------------------------- /octasine/src/gui/style/container.rs: -------------------------------------------------------------------------------- 1 | use iced_baseview::{ 2 | widget::container::{Appearance, StyleSheet}, 3 | Color, 4 | }; 5 | 6 | use super::{colors, Theme}; 7 | 8 | #[derive(Default)] 9 | pub enum ContainerStyle { 10 | #[default] 11 | Transparent, 12 | L0, 13 | L1, 14 | L2, 15 | L3, 16 | Tooltip, 17 | } 18 | 19 | impl StyleSheet for Theme { 20 | type Style = ContainerStyle; 21 | 22 | fn appearance(&self, style: &Self::Style) -> Appearance { 23 | match self { 24 | Self::Dark => { 25 | use colors::dark::*; 26 | 27 | match style { 28 | Self::Style::Transparent => Appearance { 29 | text_color: None, 30 | background: Color::TRANSPARENT.into(), 31 | border_radius: 0.0, 32 | border_width: 0.0, 33 | border_color: Color::TRANSPARENT, 34 | }, 35 | Self::Style::L0 => Appearance { 36 | background: BACKGROUND.into(), 37 | text_color: TEXT.into(), 38 | ..Default::default() 39 | }, 40 | Self::Style::L1 => Appearance { 41 | background: Some(GRAY_100.into()), 42 | border_radius: 4.0, 43 | ..Default::default() 44 | }, 45 | Self::Style::L2 => Appearance { 46 | background: Some(GRAY_200.into()), 47 | border_radius: 4.0, 48 | ..Default::default() 49 | }, 50 | Self::Style::L3 => Appearance { 51 | background: Some(GRAY_200.into()), 52 | border_radius: 4.0, 53 | ..Default::default() 54 | }, 55 | Self::Style::Tooltip => Appearance { 56 | background: GRAY_200.into(), 57 | text_color: TEXT.into(), 58 | border_width: 3.0, 59 | border_radius: 3.0, 60 | border_color: GRAY_200, 61 | }, 62 | } 63 | } 64 | Self::Light => { 65 | use colors::light::*; 66 | 67 | match style { 68 | Self::Style::Transparent => Appearance { 69 | text_color: None, 70 | background: Color::TRANSPARENT.into(), 71 | border_radius: 0.0, 72 | border_width: 0.0, 73 | border_color: Color::TRANSPARENT, 74 | }, 75 | Self::Style::L0 => Appearance { 76 | background: BACKGROUND.into(), 77 | text_color: TEXT.into(), 78 | ..Default::default() 79 | }, 80 | Self::Style::L1 => Appearance { 81 | background: Some(GRAY_900.into()), 82 | border_radius: 4.0, 83 | ..Default::default() 84 | }, 85 | Self::Style::L2 => Appearance { 86 | background: Some(Color::WHITE.into()), 87 | border_radius: 4.0, 88 | ..Default::default() 89 | }, 90 | Self::Style::L3 => Appearance { 91 | background: Some(Color::WHITE.into()), 92 | border_radius: 4.0, 93 | ..Default::default() 94 | }, 95 | Self::Style::Tooltip => Appearance { 96 | background: BLUE.into(), 97 | text_color: Color::WHITE.into(), 98 | border_width: 3.0, 99 | border_radius: 3.0, 100 | border_color: BLUE, 101 | }, 102 | } 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /octasine/src/gui/style/envelope.rs: -------------------------------------------------------------------------------- 1 | use iced_baseview::Color; 2 | 3 | use crate::gui::envelope::canvas::{Appearance, StyleSheet}; 4 | 5 | use super::Theme; 6 | 7 | impl StyleSheet for Theme { 8 | fn appearance(&self) -> Appearance { 9 | match self { 10 | Self::Light => { 11 | use super::colors::light::*; 12 | 13 | Appearance { 14 | background_color: Color::WHITE, 15 | border_color: BORDER, 16 | drag_border_color: GRAY_700, 17 | text_color: TEXT, 18 | time_marker_minor_color: GRAY_900, 19 | time_marker_color_major: GRAY_700, 20 | path_color: BLUE, 21 | dragger_fill_color_active: SURFACE, 22 | dragger_fill_color_hover: SURFACE_HOVER, 23 | dragger_fill_color_dragging: SURFACE_PRESS, 24 | dragger_border_color: BORDER, 25 | viewport_indicator_border: GRAY_300, 26 | viewport_indicator_border_active: BLUE, 27 | } 28 | } 29 | Self::Dark => { 30 | use super::colors::dark::*; 31 | 32 | Appearance { 33 | background_color: GRAY_200, 34 | border_color: BORDER_DARK, 35 | drag_border_color: GRAY_400, 36 | text_color: TEXT, 37 | time_marker_minor_color: GRAY_300, 38 | time_marker_color_major: GRAY_500, 39 | path_color: BLUE, 40 | dragger_fill_color_active: TEXT, 41 | dragger_fill_color_hover: HOVERED, 42 | dragger_fill_color_dragging: PRESSED, 43 | dragger_border_color: SURFACE, 44 | viewport_indicator_border: GRAY_600, 45 | viewport_indicator_border_active: BLUE, 46 | } 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /octasine/src/gui/style/knob.rs: -------------------------------------------------------------------------------- 1 | use iced_audio::style::knob::{Appearance, StyleSheet, TickMarksAppearance}; 2 | 3 | use super::Theme; 4 | 5 | #[derive(Default, Clone, Copy)] 6 | pub enum KnobStyle { 7 | #[default] 8 | Regular, 9 | Bipolar, 10 | } 11 | 12 | impl StyleSheet for Theme { 13 | type Style = KnobStyle; 14 | 15 | fn active(&self, style: &Self::Style) -> Appearance { 16 | use iced_audio::knob::{ 17 | ArcAppearance, ArcBipolarAppearance, LineCap, LineNotch, NotchShape, StyleLength, 18 | }; 19 | 20 | let (filled_color, empty_color, notch_color) = match self { 21 | Self::Dark => { 22 | use super::colors::dark::*; 23 | 24 | (BLUE, GRAY_500, GRAY_900) 25 | } 26 | Self::Light => { 27 | use super::colors::light::*; 28 | 29 | (BLUE, GRAY_600, TEXT) 30 | } 31 | }; 32 | 33 | let notch = NotchShape::Line(LineNotch { 34 | color: notch_color, 35 | width: StyleLength::Fixed(2.0), 36 | length: StyleLength::Fixed(6.0), 37 | cap: LineCap::Round, 38 | offset: StyleLength::Fixed(3.0), 39 | }); 40 | 41 | let arc_width = StyleLength::Fixed(2.0); 42 | let arc_cap = LineCap::Square; 43 | 44 | match style { 45 | Self::Style::Regular => Appearance::Arc(ArcAppearance { 46 | width: arc_width, 47 | empty_color, 48 | filled_color, 49 | cap: arc_cap, 50 | notch, 51 | }), 52 | Self::Style::Bipolar => Appearance::ArcBipolar(ArcBipolarAppearance { 53 | width: arc_width, 54 | empty_color, 55 | left_filled_color: filled_color, 56 | right_filled_color: filled_color, 57 | cap: arc_cap, 58 | notch_center: notch, 59 | notch_left_right: None, 60 | }), 61 | } 62 | } 63 | 64 | fn hovered(&self, style: &Self::Style) -> Appearance { 65 | self.active(style) 66 | } 67 | 68 | fn dragging(&self, style: &Self::Style) -> Appearance { 69 | self.active(style) 70 | } 71 | 72 | fn tick_marks_appearance(&self, _style: &Self::Style) -> Option { 73 | use iced_audio::style::tick_marks::{Appearance, Shape}; 74 | 75 | let (tier_1, tier_2) = match self { 76 | Self::Dark => { 77 | use super::colors::dark::*; 78 | 79 | (GRAY_600, GRAY_800) 80 | } 81 | Self::Light => { 82 | use super::colors::light::*; 83 | 84 | (GRAY_600, GRAY_300) 85 | } 86 | }; 87 | 88 | Some(TickMarksAppearance { 89 | style: Appearance { 90 | tier_1: Shape::Circle { 91 | diameter: 3.0, 92 | color: tier_1, 93 | }, 94 | tier_2: Shape::Circle { 95 | diameter: 3.0, 96 | color: tier_2, 97 | }, 98 | tier_3: Shape::Line { 99 | length: 3.0, 100 | width: 2.0, 101 | color: tier_2, 102 | }, 103 | }, 104 | offset: 3.0, 105 | }) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /octasine/src/gui/style/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! hex_gray { 3 | ($hex:literal) => { 4 | ::iced_baseview::Color::from_rgb( 5 | $hex as f32 / 255.0, 6 | $hex as f32 / 255.0, 7 | $hex as f32 / 255.0, 8 | ) 9 | }; 10 | } 11 | 12 | #[macro_export] 13 | macro_rules! hex { 14 | ($r:literal, $g:literal, $b:literal) => { 15 | ::iced_baseview::Color::from_rgb($r as f32 / 255.0, $g as f32 / 255.0, $b as f32 / 255.0) 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /octasine/src/gui/style/menu.rs: -------------------------------------------------------------------------------- 1 | use iced_baseview::overlay::menu::{Appearance, StyleSheet}; 2 | 3 | use super::Theme; 4 | 5 | impl StyleSheet for Theme { 6 | type Style = (); 7 | 8 | fn appearance(&self, _style: &Self::Style) -> Appearance { 9 | match self { 10 | Self::Light => { 11 | use super::colors::light::*; 12 | 13 | Appearance { 14 | background: SURFACE.into(), 15 | text_color: TEXT, 16 | selected_background: SURFACE_HOVER.into(), 17 | selected_text_color: TEXT, 18 | border_width: 1.0, 19 | border_color: SURFACE, 20 | border_radius: 3.0, 21 | } 22 | } 23 | Self::Dark => { 24 | use super::colors::dark::*; 25 | 26 | Appearance { 27 | background: GRAY_300.into(), 28 | selected_background: SURFACE_HOVER.into(), 29 | text_color: TEXT, 30 | selected_text_color: HOVERED, 31 | border_width: 1.0, 32 | border_color: GRAY_300, 33 | border_radius: 3.0, 34 | } 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /octasine/src/gui/style/mod.rs: -------------------------------------------------------------------------------- 1 | mod macros; 2 | 3 | pub mod application; 4 | pub mod boolean_button; 5 | pub mod button; 6 | pub mod card; 7 | pub mod checkbox; 8 | pub mod colors; 9 | pub mod container; 10 | pub mod envelope; 11 | pub mod knob; 12 | pub mod menu; 13 | pub mod mod_matrix; 14 | pub mod modal; 15 | pub mod pick_list; 16 | pub mod radio; 17 | pub mod scrollable; 18 | pub mod text; 19 | pub mod text_input; 20 | pub mod wave_display; 21 | pub mod wave_picker; 22 | 23 | use iced_baseview::Font; 24 | use serde::{Deserialize, Serialize}; 25 | 26 | const OPEN_SANS_REGULAR: Font = Font::External { 27 | name: "Open Sans Regular", 28 | bytes: super::OPEN_SANS_BYTES_REGULAR, 29 | }; 30 | const OPEN_SANS_SEMI_BOLD: Font = Font::External { 31 | name: "Open Sans Semi Bold", 32 | bytes: super::OPEN_SANS_BYTES_SEMI_BOLD, 33 | }; 34 | const OPEN_SANS_BOLD: Font = Font::External { 35 | name: "Open Sans Bold", 36 | bytes: super::OPEN_SANS_BYTES_BOLD, 37 | }; 38 | const OPEN_SANS_EXTRA_BOLD: Font = Font::External { 39 | name: "Open Sans Extra Bold", 40 | bytes: super::OPEN_SANS_BYTES_EXTRA_BOLD, 41 | }; 42 | 43 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] 44 | #[serde(rename_all = "lowercase")] 45 | pub enum Theme { 46 | #[default] 47 | Light, 48 | Dark, 49 | } 50 | 51 | impl Theme { 52 | pub fn font_regular(&self) -> Font { 53 | match self { 54 | Theme::Dark => OPEN_SANS_REGULAR, 55 | Theme::Light => OPEN_SANS_SEMI_BOLD, 56 | } 57 | } 58 | pub fn font_bold(&self) -> Font { 59 | match self { 60 | Theme::Dark => OPEN_SANS_SEMI_BOLD, 61 | Theme::Light => OPEN_SANS_BOLD, 62 | } 63 | } 64 | pub fn font_extra_bold(&self) -> Font { 65 | match self { 66 | Theme::Dark => OPEN_SANS_BOLD, 67 | Theme::Light => OPEN_SANS_EXTRA_BOLD, 68 | } 69 | } 70 | pub fn font_heading(&self) -> Font { 71 | match self { 72 | Theme::Dark => OPEN_SANS_BOLD, 73 | Theme::Light => OPEN_SANS_BOLD, 74 | } 75 | } 76 | 77 | pub fn tooltip_padding(&self) -> u16 { 78 | 3 79 | } 80 | 81 | pub fn button_padding(&self) -> u16 { 82 | 3 83 | } 84 | 85 | pub fn picklist_padding(&self) -> u16 { 86 | 3 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /octasine/src/gui/style/mod_matrix.rs: -------------------------------------------------------------------------------- 1 | use iced_baseview::Color; 2 | 3 | use crate::gui::mod_matrix::{Appearance, StyleSheet}; 4 | 5 | use super::Theme; 6 | 7 | impl StyleSheet for Theme { 8 | fn appearance(&self) -> Appearance { 9 | match self { 10 | Self::Light => { 11 | use super::colors::light::*; 12 | 13 | Appearance { 14 | background_color: Color::WHITE, 15 | border_color: Color::TRANSPARENT, 16 | text_color: TEXT, 17 | box_border_color: BORDER, 18 | operator_box_color_active: SURFACE, 19 | operator_box_border_color: Some(BORDER), 20 | operator_box_color_hover: SURFACE_HOVER, 21 | operator_box_color_dragging: SURFACE_PRESS, 22 | modulation_box_color_active: SURFACE, 23 | modulation_box_color_inactive: Color::TRANSPARENT, 24 | modulation_box_color_hover: SURFACE_HOVER, 25 | line_max_color: Color::BLACK, 26 | mod_out_line_color: BLUE, 27 | mix_out_line_color: GREEN, 28 | } 29 | } 30 | Self::Dark => { 31 | use super::colors::dark::*; 32 | 33 | Appearance { 34 | background_color: GRAY_200, 35 | border_color: Color::TRANSPARENT, 36 | text_color: TEXT, 37 | box_border_color: GRAY_500, 38 | operator_box_border_color: None, 39 | operator_box_color_active: SURFACE, 40 | operator_box_color_hover: SURFACE_HOVER, 41 | operator_box_color_dragging: GRAY_600, 42 | modulation_box_color_active: TEXT, 43 | modulation_box_color_inactive: Color::TRANSPARENT, 44 | modulation_box_color_hover: HOVERED, 45 | line_max_color: Color::WHITE, 46 | mod_out_line_color: BLUE, 47 | mix_out_line_color: GREEN, 48 | } 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /octasine/src/gui/style/modal.rs: -------------------------------------------------------------------------------- 1 | use iced_aw::native::modal::StyleSheet; 2 | use iced_aw::style::modal::Appearance; 3 | use iced_baseview::Color; 4 | 5 | use super::Theme; 6 | 7 | impl StyleSheet for Theme { 8 | type Style = (); 9 | 10 | fn active(&self, _style: Self::Style) -> Appearance { 11 | match self { 12 | Self::Dark => { 13 | let mut color = Color::BLACK; 14 | 15 | color.a = 0.75; 16 | 17 | Appearance { 18 | background: color.into(), 19 | } 20 | } 21 | Self::Light => { 22 | let mut color = Color::BLACK; 23 | 24 | color.a = 0.5; 25 | 26 | Appearance { 27 | background: color.into(), 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /octasine/src/gui/style/pick_list.rs: -------------------------------------------------------------------------------- 1 | use iced_baseview::widget::pick_list::{Appearance, StyleSheet}; 2 | 3 | use super::Theme; 4 | 5 | impl StyleSheet for Theme { 6 | type Style = (); 7 | 8 | fn active(&self, _style: &Self::Style) -> Appearance { 9 | match self { 10 | Self::Light => { 11 | use super::colors::light::*; 12 | 13 | Appearance { 14 | background: SURFACE.into(), 15 | text_color: TEXT, 16 | border_color: BORDER, 17 | border_width: 1.0, 18 | border_radius: 3.0, 19 | placeholder_color: TEXT, 20 | handle_color: TEXT, 21 | } 22 | } 23 | Self::Dark => { 24 | use super::colors::dark::*; 25 | 26 | Appearance { 27 | background: SURFACE.into(), 28 | text_color: TEXT, 29 | border_color: TEXT, 30 | border_width: 0.0, 31 | border_radius: 3.0, 32 | placeholder_color: TEXT, 33 | handle_color: TEXT, 34 | } 35 | } 36 | } 37 | } 38 | fn hovered(&self, style: &Self::Style) -> Appearance { 39 | match self { 40 | Self::Light => { 41 | use super::colors::light::*; 42 | 43 | Appearance { 44 | background: SURFACE_HOVER.into(), 45 | ..self.active(style) 46 | } 47 | } 48 | Self::Dark => { 49 | use super::colors::dark::*; 50 | 51 | Appearance { 52 | background: SURFACE_HOVER.into(), 53 | text_color: HOVERED, 54 | ..self.active(style) 55 | } 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /octasine/src/gui/style/radio.rs: -------------------------------------------------------------------------------- 1 | use iced_baseview::widget::radio::{Appearance, StyleSheet}; 2 | 3 | use super::Theme; 4 | 5 | impl StyleSheet for Theme { 6 | type Style = (); 7 | 8 | fn active(&self, _style: &Self::Style, _is_selected: bool) -> Appearance { 9 | match self { 10 | Self::Light => { 11 | use super::colors::light::*; 12 | 13 | Appearance { 14 | background: SURFACE.into(), 15 | dot_color: TEXT, 16 | text_color: Some(TEXT), 17 | border_width: 1.0, 18 | border_color: BORDER, 19 | } 20 | } 21 | Self::Dark => { 22 | use super::colors::dark::*; 23 | 24 | Appearance { 25 | background: SURFACE.into(), 26 | dot_color: TEXT, 27 | text_color: Some(TEXT), 28 | border_width: 1.0, 29 | border_color: TEXT, 30 | } 31 | } 32 | } 33 | } 34 | 35 | fn hovered(&self, style: &Self::Style, is_selected: bool) -> Appearance { 36 | match self { 37 | Self::Light => { 38 | use super::colors::light::*; 39 | 40 | Appearance { 41 | background: SURFACE_HOVER.into(), 42 | ..self.active(style, is_selected) 43 | } 44 | } 45 | Self::Dark => { 46 | use super::colors::dark::*; 47 | 48 | Appearance { 49 | border_color: HOVERED, 50 | ..self.active(style, is_selected) 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /octasine/src/gui/style/scrollable.rs: -------------------------------------------------------------------------------- 1 | use iced_baseview::{ 2 | widget::scrollable::{Scrollbar, Scroller, StyleSheet}, 3 | Color, 4 | }; 5 | 6 | use super::Theme; 7 | 8 | // FIXME 9 | impl StyleSheet for Theme { 10 | type Style = (); 11 | 12 | fn active(&self, _style: &Self::Style) -> Scrollbar { 13 | match self { 14 | Self::Light => { 15 | use super::colors::light::*; 16 | 17 | Scrollbar { 18 | background: GRAY_700.into(), 19 | border_radius: 5.0, 20 | border_width: 1.0, 21 | border_color: Color::TRANSPARENT, 22 | scroller: Scroller { 23 | color: GRAY_450, 24 | border_radius: 5.0, 25 | border_width: 1.0, 26 | border_color: Color::TRANSPARENT, 27 | }, 28 | } 29 | } 30 | Self::Dark => { 31 | use super::colors::dark::*; 32 | 33 | Scrollbar { 34 | background: GRAY_400.into(), 35 | border_radius: 5.0, 36 | border_width: 1.0, 37 | border_color: GRAY_300, 38 | scroller: Scroller { 39 | color: GRAY_600, 40 | border_radius: 5.0, 41 | border_width: 1.0, 42 | border_color: Color::TRANSPARENT, 43 | }, 44 | } 45 | } 46 | } 47 | } 48 | 49 | fn dragging(&self, style: &Self::Style) -> Scrollbar { 50 | self.hovered(style, true) 51 | } 52 | 53 | fn hovered(&self, style: &Self::Style, is_mouse_over_scrollbar: bool) -> Scrollbar { 54 | let mut appearance = self.active(style); 55 | 56 | if is_mouse_over_scrollbar { 57 | match self { 58 | Self::Light => { 59 | use super::colors::light::*; 60 | 61 | appearance.scroller.color = GRAY_400; 62 | } 63 | Self::Dark => { 64 | use super::colors::dark::*; 65 | 66 | appearance.scroller.color = GRAY_800; 67 | } 68 | } 69 | } 70 | 71 | appearance 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /octasine/src/gui/style/text.rs: -------------------------------------------------------------------------------- 1 | use iced_baseview::widget::text::{Appearance, StyleSheet}; 2 | 3 | use super::Theme; 4 | 5 | impl StyleSheet for Theme { 6 | type Style = (); 7 | 8 | fn appearance(&self, _style: Self::Style) -> Appearance { 9 | Appearance { color: None } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /octasine/src/gui/style/text_input.rs: -------------------------------------------------------------------------------- 1 | use iced_baseview::widget::text_input::{Appearance, StyleSheet}; 2 | 3 | use super::Theme; 4 | 5 | impl StyleSheet for Theme { 6 | type Style = (); 7 | 8 | fn active(&self, _style: &Self::Style) -> Appearance { 9 | match self { 10 | Self::Dark => { 11 | use super::colors::dark::GRAY_300; 12 | 13 | Appearance { 14 | background: GRAY_300.into(), 15 | border_radius: 3.0, 16 | border_width: 1.0, 17 | border_color: GRAY_300, 18 | icon_color: GRAY_300, 19 | } 20 | } 21 | Self::Light => { 22 | use super::colors::light::{BORDER, SURFACE}; 23 | 24 | Appearance { 25 | background: SURFACE.into(), 26 | border_radius: 3.0, 27 | border_width: 1.0, 28 | border_color: BORDER, 29 | icon_color: BORDER, 30 | } 31 | } 32 | } 33 | } 34 | 35 | fn focused(&self, style: &Self::Style) -> Appearance { 36 | self.active(style) 37 | } 38 | fn disabled(&self, style: &Self::Style) -> Appearance { 39 | self.active(style) 40 | } 41 | 42 | fn placeholder_color(&self, _style: &Self::Style) -> iced_baseview::Color { 43 | match self { 44 | Self::Dark => super::colors::dark::GRAY_800, 45 | Self::Light => super::colors::light::GRAY_300, 46 | } 47 | } 48 | 49 | fn value_color(&self, _style: &Self::Style) -> iced_baseview::Color { 50 | match self { 51 | Self::Dark => super::colors::dark::TEXT, 52 | Self::Light => super::colors::light::TEXT, 53 | } 54 | } 55 | 56 | fn selection_color(&self, _style: &Self::Style) -> iced_baseview::Color { 57 | match self { 58 | Self::Dark => super::colors::dark::GRAY_500, 59 | Self::Light => super::colors::light::GRAY_700, 60 | } 61 | } 62 | 63 | fn disabled_color(&self, style: &Self::Style) -> iced_baseview::Color { 64 | self.value_color(style) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /octasine/src/gui/style/wave_display.rs: -------------------------------------------------------------------------------- 1 | use iced_baseview::Color; 2 | 3 | use crate::gui::wave_display::{Appearance, StyleSheet}; 4 | 5 | use super::Theme; 6 | 7 | impl StyleSheet for Theme { 8 | fn appearance(&self) -> Appearance { 9 | match self { 10 | Self::Light => { 11 | use super::colors::light::*; 12 | Appearance { 13 | background_color: SURFACE, 14 | border_color: BORDER, 15 | middle_line_color: GRAY_600, 16 | wave_line_color: BLUE, 17 | } 18 | } 19 | Self::Dark => { 20 | use super::colors::dark::*; 21 | Appearance { 22 | background_color: Color::TRANSPARENT, 23 | border_color: BORDER_DARK, 24 | middle_line_color: GRAY_400, 25 | wave_line_color: BLUE, 26 | } 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /octasine/src/gui/style/wave_picker.rs: -------------------------------------------------------------------------------- 1 | use iced_baseview::Color; 2 | 3 | use crate::gui::wave_picker::{Appearance, StyleSheet}; 4 | 5 | use super::Theme; 6 | 7 | impl StyleSheet for Theme { 8 | fn appearance(&self) -> Appearance { 9 | match self { 10 | Self::Light => { 11 | use super::colors::light::*; 12 | Appearance { 13 | background_color: SURFACE, 14 | border_color_active: BORDER, 15 | border_color_hovered: BORDER, 16 | middle_line_color: GRAY_600, 17 | shape_line_color_active: BLUE, 18 | shape_line_color_hovered: BLUE, 19 | } 20 | } 21 | Self::Dark => { 22 | use super::colors::dark::*; 23 | Appearance { 24 | background_color: Color::TRANSPARENT, 25 | border_color_active: BORDER, 26 | border_color_hovered: BORDER_HOVERED, 27 | middle_line_color: GRAY_400, 28 | shape_line_color_active: BLUE, 29 | shape_line_color_hovered: BLUE, 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /octasine/src/gui/value_text.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use compact_str::CompactString; 4 | use iced_baseview::alignment::Horizontal; 5 | use iced_baseview::widget::Text; 6 | use iced_baseview::{widget::Button, Element, Length}; 7 | 8 | use crate::parameters::{ParameterValue, WrappedParameter}; 9 | 10 | use super::style::button::ButtonStyle; 11 | use super::LINE_HEIGHT; 12 | use super::{style::Theme, GuiSyncHandle, Message}; 13 | 14 | #[derive(Debug, Clone)] 15 | pub struct ValueText { 16 | parameter: WrappedParameter, 17 | value_text: CompactString, 18 | phantom_data: PhantomData

, 19 | } 20 | 21 | impl ValueText

{ 22 | pub fn new(sync_handle: &H, parameter: WrappedParameter) -> Self { 23 | let value_patch = sync_handle.get_parameter(parameter); 24 | let value_text = P::new_from_patch(value_patch).get_formatted(); 25 | 26 | Self { 27 | parameter, 28 | value_text, 29 | phantom_data: Default::default(), 30 | } 31 | } 32 | 33 | pub fn set_value(&mut self, value: f32) { 34 | self.value_text = P::new_from_patch(value).get_formatted(); 35 | } 36 | 37 | pub fn view(&self, theme: &Theme) -> Element { 38 | Button::new( 39 | Text::new(self.value_text.clone()) 40 | .horizontal_alignment(Horizontal::Center) 41 | .width(Length::Fill) 42 | .font(theme.font_regular()) 43 | .height(Length::Fixed(LINE_HEIGHT.into())), 44 | ) 45 | .padding(0) 46 | .width(Length::Fill) 47 | .style(ButtonStyle::Value) 48 | .on_press(Message::ChangeParameterByTextInput { 49 | parameter: self.parameter, 50 | value_text: self.value_text.clone(), 51 | }) 52 | .into() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /octasine/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod audio; 2 | pub mod common; 3 | pub mod math; 4 | pub mod parameters; 5 | pub mod plugin; 6 | pub mod settings; 7 | pub mod simd; 8 | pub mod sync; 9 | pub mod utils; 10 | 11 | #[cfg(feature = "gui")] 12 | pub mod gui; 13 | 14 | #[cfg(feature = "clap")] 15 | #[no_mangle] 16 | pub static clap_entry: ::clap_sys::entry::clap_plugin_entry = plugin::clap::CLAP_ENTRY; 17 | 18 | #[cfg(feature = "vst2")] 19 | ::vst::plugin_main!(plugin::vst2::OctaSine); 20 | 21 | #[cfg(test)] 22 | mod tests { 23 | use crate::{ 24 | audio::AudioState, common::SampleRate, parameters::PARAMETERS, sync::SyncState, 25 | utils::update_audio_parameters, 26 | }; 27 | 28 | #[test] 29 | fn test_parameter_interaction() { 30 | let mut audio = AudioState::default(); 31 | 32 | audio.set_sample_rate(SampleRate(44100.0)); 33 | 34 | let sync = SyncState::<()>::new(None); 35 | 36 | let mut patch_values = Vec::new(); 37 | 38 | for i in 0..PARAMETERS.len() { 39 | let patch_value = fastrand::f32(); 40 | 41 | sync.patches.set_parameter_from_host(i, patch_value); 42 | 43 | patch_values.push(patch_value) 44 | } 45 | 46 | update_audio_parameters(&mut audio, &sync); 47 | 48 | for _ in 0..44100 { 49 | audio.advance_one_sample(); 50 | } 51 | 52 | for (i, parameter) in PARAMETERS.iter().copied().enumerate() { 53 | assert_eq!(i, parameter.to_index() as usize); 54 | 55 | let values_approx_eq = audio.compare_parameter_patch_value(parameter, patch_values[i]); 56 | 57 | if !values_approx_eq { 58 | println!("Parameter: {:?}", parameter); 59 | println!("Set patch value: {}", patch_values[i]); 60 | } 61 | 62 | assert!(values_approx_eq) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /octasine/src/math/bhaskara.rs: -------------------------------------------------------------------------------- 1 | /// Approximate cos(a * PI / 2) for range 0.0 to 1.0 2 | #[allow(dead_code)] 3 | pub fn bhaskara_cos_frac_pi_2(a: f32) -> f32 { 4 | let a2 = a * a; 5 | 6 | 4.0 * ((1.0 - a2) / (4.0 + a2)) 7 | } 8 | 9 | /// Approximate sin(a * PI / 2) for range 0.0 to 1.0 10 | #[allow(dead_code)] 11 | pub fn bhaskara_sin_frac_pi_2(a: f32) -> f32 { 12 | bhaskara_cos_frac_pi_2(1.0 - a) 13 | } 14 | 15 | /// Approximate [cos(a * PI / 2), sin(a * PI / 2)] for range 0.0 to 1.0 16 | #[allow(dead_code)] 17 | pub fn bhaskara_constant_power_panning(pan: f32) -> [f32; 2] { 18 | cfg_if::cfg_if! { 19 | if #[cfg(target_arch = "x86_64")] { 20 | unsafe { 21 | use std::arch::x86_64::*; 22 | 23 | let mut arr = [pan, 1.0 - pan, 0.0, 0.0]; 24 | 25 | let a = _mm_loadu_ps(arr.as_ptr()); 26 | let a2 = _mm_mul_ps(a, a); 27 | 28 | let result = _mm_mul_ps(_mm_set1_ps(4.0), 29 | _mm_div_ps( 30 | _mm_sub_ps(_mm_set1_ps(1.0), a2), 31 | _mm_add_ps(_mm_set1_ps(4.0), a2), 32 | ) 33 | ); 34 | 35 | _mm_storeu_ps(arr.as_mut_ptr(), result); 36 | 37 | let mut output = [0.0f32; 2]; 38 | 39 | output.copy_from_slice(&arr[..2]); 40 | 41 | output 42 | } 43 | } else { 44 | [ 45 | bhaskara_cos_frac_pi_2(pan), 46 | bhaskara_sin_frac_pi_2(pan), 47 | ] 48 | } 49 | } 50 | } 51 | 52 | #[cfg(test)] 53 | mod tests { 54 | use std::f32::consts::FRAC_PI_2; 55 | 56 | use super::*; 57 | 58 | #[test] 59 | fn test_bhaskara() { 60 | let precision = u16::MAX; 61 | 62 | for i in 0..=precision { 63 | let a = f32::from(i) / f32::from(precision); 64 | 65 | assert_approx_eq::assert_approx_eq!( 66 | bhaskara_cos_frac_pi_2(a), 67 | (a * FRAC_PI_2).cos(), 68 | 0.005 69 | ); 70 | assert_approx_eq::assert_approx_eq!( 71 | bhaskara_sin_frac_pi_2(a), 72 | (a * FRAC_PI_2).sin(), 73 | 0.005 74 | ); 75 | 76 | let [l, r] = bhaskara_constant_power_panning(a); 77 | 78 | assert_approx_eq::assert_approx_eq!(l, (a * FRAC_PI_2).cos(), 0.005); 79 | assert_approx_eq::assert_approx_eq!(r, (a * FRAC_PI_2).sin(), 0.005); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /octasine/src/math/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod bhaskara; 2 | pub mod wave; 3 | 4 | #[inline(always)] 5 | pub fn exp2_fast(value: f32) -> f32 { 6 | fast_math::exp2_raw(value) 7 | } 8 | -------------------------------------------------------------------------------- /octasine/src/math/wave.rs: -------------------------------------------------------------------------------- 1 | /// Triangle wave 2 | #[inline] 3 | pub fn triangle(x: f64) -> f64 { 4 | let x = x + 0.25; 5 | 6 | (2.0 * (2.0 * (x - (x + 0.5).floor())).abs()) - 1.0 7 | } 8 | 9 | /// Square wave with smooth transitions 10 | /// 11 | /// Check absence of branches by removing #[inline] statement and running: 12 | /// cargo asm --lib --no-default-features --full-name --rust -p octasine "octasine::math::wave::square" 13 | #[inline] 14 | pub fn square(x: f64) -> f64 { 15 | // If x is negative, final result should be negated 16 | let negate_if_x_negative: f64 = if x.is_sign_negative() { -1.0 } else { 1.0 }; 17 | 18 | // x is now between 0.0 and 1.0 19 | let mut x = x.abs().fract(); 20 | 21 | // If x > 0.5, final result should be negated 22 | let negate_if_x_gt_half: f64 = if x > 0.5 { -1.0 } else { 1.0 }; 23 | 24 | let sign_mask = negate_if_x_negative.to_bits() ^ negate_if_x_gt_half.to_bits(); 25 | 26 | // Adjust for x > 0.5 27 | if x > 0.5 { 28 | x = 1.0 - x; 29 | } 30 | 31 | // More iterations cause "tighter interpolation" 32 | // 33 | // Do repeated multiplications instead of using powf to be consistent with 34 | // SIMD implementations. 35 | let a = x * 4.0 - 1.0; 36 | let a2 = a * a; 37 | let a4 = a2 * a2; 38 | let a8 = a4 * a4; 39 | let a16 = a8 * a8; 40 | let a32 = a16 * a16; 41 | let a64 = a32 * a32; 42 | let a128 = a64 * a64; 43 | 44 | let approximation = 2.0 * ((1.0 / (1.0 + a128)) - 0.5); 45 | 46 | f64::from_bits(approximation.to_bits() ^ sign_mask) 47 | } 48 | 49 | /// Saw wave with smooth transitions 50 | /// 51 | /// Check absence of branches by removing #[inline] statement and running: 52 | /// cargo asm --lib --no-default-features --full-name --rust -p octasine "octasine::math::wave::saw" 53 | #[inline] 54 | pub fn saw(x: f64) -> f64 { 55 | const DOWN_FACTOR: f64 = 50.0; 56 | const X_INTERSECTION: f64 = 1.0 - (1.0 / DOWN_FACTOR); 57 | const UP_FACTOR: f64 = 1.0 / X_INTERSECTION; 58 | 59 | let x_is_negative = x < 0.0; 60 | 61 | let mut x = x.abs().fract(); 62 | 63 | if x_is_negative { 64 | x = 1.0 - x; 65 | } 66 | 67 | let up = x * UP_FACTOR; 68 | let down = DOWN_FACTOR - DOWN_FACTOR * x; 69 | 70 | let y = if x < X_INTERSECTION { up } else { down }; 71 | 72 | (y - 0.5) * 2.0 73 | } 74 | -------------------------------------------------------------------------------- /octasine/src/parameters/glide_active.rs: -------------------------------------------------------------------------------- 1 | use compact_str::{format_compact, CompactString}; 2 | 3 | use super::{ 4 | utils::{map_patch_value_to_step, map_step_to_patch_value}, 5 | ParameterValue, SerializableRepresentation, 6 | }; 7 | 8 | pub const GLIDE_ACTIVE_STEPS: &[GlideActive] = 9 | &[GlideActive::Off, GlideActive::Legato, GlideActive::On]; 10 | 11 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 12 | pub enum GlideActive { 13 | #[default] 14 | Off, 15 | Legato, 16 | On, 17 | } 18 | 19 | impl ::std::fmt::Display for GlideActive { 20 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 21 | f.write_str(match self { 22 | Self::Off => "OFF", 23 | Self::Legato => "LEG", 24 | Self::On => "ON", 25 | }) 26 | } 27 | } 28 | 29 | #[derive(Debug, Clone, Copy, Default)] 30 | pub struct GlideActiveValue(GlideActive); 31 | 32 | impl ParameterValue for GlideActiveValue { 33 | type Value = GlideActive; 34 | 35 | fn new_from_audio(value: Self::Value) -> Self { 36 | Self(value) 37 | } 38 | fn new_from_text(text: &str) -> Option { 39 | match text.trim().to_lowercase().as_str() { 40 | "off" => Some(Self(GlideActive::Off)), 41 | "leg" | "legato" => Some(Self(GlideActive::Legato)), 42 | "on" => Some(Self(GlideActive::On)), 43 | _ => None, 44 | } 45 | } 46 | fn get(self) -> Self::Value { 47 | self.0 48 | } 49 | fn new_from_patch(value: f32) -> Self { 50 | Self(map_patch_value_to_step(&GLIDE_ACTIVE_STEPS[..], value)) 51 | } 52 | fn to_patch(self) -> f32 { 53 | map_step_to_patch_value(&GLIDE_ACTIVE_STEPS[..], self.0) 54 | } 55 | fn get_formatted(self) -> CompactString { 56 | format_compact!("{}", self.0) 57 | } 58 | 59 | fn get_serializable(&self) -> SerializableRepresentation { 60 | SerializableRepresentation::Other(self.get_formatted()) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /octasine/src/parameters/glide_bpm_sync.rs: -------------------------------------------------------------------------------- 1 | use compact_str::{format_compact, CompactString}; 2 | 3 | use super::{ParameterValue, SerializableRepresentation}; 4 | 5 | #[derive(Debug, Clone, Copy)] 6 | pub struct GlideBpmSyncValue(bool); 7 | 8 | impl Default for GlideBpmSyncValue { 9 | fn default() -> Self { 10 | Self(true) 11 | } 12 | } 13 | 14 | impl ParameterValue for GlideBpmSyncValue { 15 | type Value = bool; 16 | 17 | fn new_from_audio(value: Self::Value) -> Self { 18 | Self(value) 19 | } 20 | fn new_from_text(text: &str) -> Option { 21 | match text.trim().to_lowercase().as_str() { 22 | "off" => Some(Self(false)), 23 | "on" => Some(Self(true)), 24 | _ => None, 25 | } 26 | } 27 | fn get(self) -> Self::Value { 28 | self.0 29 | } 30 | fn new_from_patch(value: f32) -> Self { 31 | Self(value > 0.5) 32 | } 33 | fn to_patch(self) -> f32 { 34 | if self.0 { 35 | 1.0 36 | } else { 37 | 0.0 38 | } 39 | } 40 | fn get_formatted(self) -> CompactString { 41 | format_compact!("{}", if self.0 { "ON" } else { "OFF" }) 42 | } 43 | 44 | fn get_serializable(&self) -> SerializableRepresentation { 45 | SerializableRepresentation::Other(self.get_formatted()) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /octasine/src/parameters/glide_mode.rs: -------------------------------------------------------------------------------- 1 | use compact_str::{format_compact, CompactString}; 2 | 3 | use super::{ 4 | utils::{map_patch_value_to_step, map_step_to_patch_value}, 5 | ParameterValue, SerializableRepresentation, 6 | }; 7 | 8 | pub const GLIDE_MODE_STEPS: &[GlideMode] = &[GlideMode::Lct, GlideMode::Lcr]; 9 | 10 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 11 | pub enum GlideMode { 12 | #[default] 13 | Lct, 14 | Lcr, 15 | } 16 | 17 | impl ::std::fmt::Display for GlideMode { 18 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 19 | f.write_str(match self { 20 | Self::Lct => "LCT", 21 | Self::Lcr => "LCR", 22 | }) 23 | } 24 | } 25 | 26 | #[derive(Debug, Clone, Copy, Default)] 27 | pub struct GlideModeValue(GlideMode); 28 | 29 | impl ParameterValue for GlideModeValue { 30 | type Value = GlideMode; 31 | 32 | fn new_from_audio(value: Self::Value) -> Self { 33 | Self(value) 34 | } 35 | fn new_from_text(text: &str) -> Option { 36 | match text.trim().to_lowercase().as_str() { 37 | "lct" => Some(Self(GlideMode::Lct)), 38 | "lcr" => Some(Self(GlideMode::Lcr)), 39 | _ => None, 40 | } 41 | } 42 | fn get(self) -> Self::Value { 43 | self.0 44 | } 45 | fn new_from_patch(value: f32) -> Self { 46 | Self(map_patch_value_to_step(&GLIDE_MODE_STEPS[..], value)) 47 | } 48 | fn to_patch(self) -> f32 { 49 | map_step_to_patch_value(&GLIDE_MODE_STEPS[..], self.0) 50 | } 51 | fn get_formatted(self) -> CompactString { 52 | format_compact!("{}", self.0) 53 | } 54 | 55 | fn get_serializable(&self) -> SerializableRepresentation { 56 | SerializableRepresentation::Other(self.get_formatted()) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /octasine/src/parameters/glide_retrigger.rs: -------------------------------------------------------------------------------- 1 | use compact_str::{format_compact, CompactString}; 2 | 3 | use super::{ParameterValue, SerializableRepresentation}; 4 | 5 | #[derive(Debug, Clone, Copy)] 6 | pub struct GlideRetriggerValue(bool); 7 | 8 | impl Default for GlideRetriggerValue { 9 | fn default() -> Self { 10 | Self(false) 11 | } 12 | } 13 | 14 | impl ParameterValue for GlideRetriggerValue { 15 | type Value = bool; 16 | 17 | fn new_from_audio(value: Self::Value) -> Self { 18 | Self(value) 19 | } 20 | fn new_from_text(text: &str) -> Option { 21 | match text.trim().to_lowercase().as_str() { 22 | "off" => Some(Self(false)), 23 | "on" => Some(Self(true)), 24 | _ => None, 25 | } 26 | } 27 | fn get(self) -> Self::Value { 28 | self.0 29 | } 30 | fn new_from_patch(value: f32) -> Self { 31 | Self(value > 0.5) 32 | } 33 | fn to_patch(self) -> f32 { 34 | if self.0 { 35 | 1.0 36 | } else { 37 | 0.0 38 | } 39 | } 40 | fn get_formatted(self) -> CompactString { 41 | format_compact!("{}", if self.0 { "ON" } else { "OFF" }) 42 | } 43 | 44 | fn get_serializable(&self) -> SerializableRepresentation { 45 | SerializableRepresentation::Other(self.get_formatted()) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /octasine/src/parameters/glide_time.rs: -------------------------------------------------------------------------------- 1 | use compact_str::{format_compact, CompactString}; 2 | 3 | use super::{ 4 | utils::{map_audio_to_patch_value_with_steps, map_patch_to_audio_value_with_steps}, 5 | ParameterValue, SerializableRepresentation, 6 | }; 7 | 8 | const STEPS: &[f32] = &[0.0, 1.0, 8.0]; 9 | 10 | #[derive(Debug, Clone, Copy)] 11 | pub struct GlideTimeValue(f32); 12 | 13 | impl Default for GlideTimeValue { 14 | fn default() -> Self { 15 | Self(0.125) 16 | } 17 | } 18 | 19 | impl ParameterValue for GlideTimeValue { 20 | type Value = f32; 21 | 22 | fn new_from_audio(value: Self::Value) -> Self { 23 | Self(value) 24 | } 25 | fn new_from_text(text: &str) -> Option { 26 | text.parse::() 27 | .ok() 28 | .map(|time| Self(time.clamp(0.0, *STEPS.last().unwrap()))) 29 | } 30 | fn get(self) -> Self::Value { 31 | self.0 32 | } 33 | fn new_from_patch(value: f32) -> Self { 34 | Self(map_patch_to_audio_value_with_steps(&STEPS[..], value)) 35 | } 36 | fn to_patch(self) -> f32 { 37 | map_audio_to_patch_value_with_steps(&STEPS[..], self.0) 38 | } 39 | fn get_formatted(self) -> CompactString { 40 | format_compact!("{:.4}", self.0) 41 | } 42 | 43 | fn get_serializable(&self) -> SerializableRepresentation { 44 | SerializableRepresentation::Float(self.0 as f64) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /octasine/src/parameters/lfo_active.rs: -------------------------------------------------------------------------------- 1 | use compact_str::CompactString; 2 | 3 | use super::{ParameterValue, SerializableRepresentation}; 4 | 5 | #[derive(Debug, Clone, Copy)] 6 | pub struct LfoActiveValue(f32); 7 | 8 | impl Default for LfoActiveValue { 9 | fn default() -> Self { 10 | Self(1.0) 11 | } 12 | } 13 | 14 | impl ParameterValue for LfoActiveValue { 15 | type Value = f32; 16 | 17 | fn new_from_audio(value: Self::Value) -> Self { 18 | Self(value.round()) 19 | } 20 | fn new_from_text(text: &str) -> Option { 21 | match text.trim().to_lowercase().as_str() { 22 | "on" | "active" => Some(Self(1.0)), 23 | "off" | "inactive" => Some(Self(0.0)), 24 | _ => None, 25 | } 26 | } 27 | fn get(self) -> Self::Value { 28 | self.0 29 | } 30 | fn new_from_patch(value: f32) -> Self { 31 | Self(value.round()) 32 | } 33 | fn to_patch(self) -> f32 { 34 | self.0 35 | } 36 | fn get_formatted(self) -> CompactString { 37 | if self.0 < 0.5 { 38 | "Off".into() 39 | } else { 40 | "On".into() 41 | } 42 | } 43 | 44 | fn get_serializable(&self) -> SerializableRepresentation { 45 | SerializableRepresentation::Other(self.get_formatted()) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /octasine/src/parameters/lfo_amount.rs: -------------------------------------------------------------------------------- 1 | use compact_str::{format_compact, CompactString}; 2 | 3 | use super::{utils::parse_valid_f32, ParameterValue, SerializableRepresentation}; 4 | 5 | #[derive(Debug, Clone, Copy)] 6 | pub struct LfoAmountValue(pub f32); 7 | 8 | impl Default for LfoAmountValue { 9 | fn default() -> Self { 10 | Self(0.0) 11 | } 12 | } 13 | 14 | impl ParameterValue for LfoAmountValue { 15 | type Value = f32; 16 | 17 | fn new_from_audio(value: Self::Value) -> Self { 18 | Self(value) 19 | } 20 | fn new_from_text(text: &str) -> Option { 21 | parse_valid_f32(text, 0.0, 2.0).map(Self) 22 | } 23 | fn get(self) -> Self::Value { 24 | self.0 25 | } 26 | fn new_from_patch(value: f32) -> Self { 27 | Self(value * 2.0) 28 | } 29 | fn to_patch(self) -> f32 { 30 | self.0 * 0.5 31 | } 32 | fn get_formatted(self) -> CompactString { 33 | format_compact!("{:.04}", self.0) 34 | } 35 | 36 | fn get_serializable(&self) -> SerializableRepresentation { 37 | SerializableRepresentation::Float(self.0.into()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /octasine/src/parameters/lfo_bpm_sync.rs: -------------------------------------------------------------------------------- 1 | use compact_str::CompactString; 2 | 3 | use super::{ParameterValue, SerializableRepresentation}; 4 | 5 | #[derive(Debug, Clone, Copy)] 6 | pub struct LfoBpmSyncValue(pub bool); 7 | 8 | impl Default for LfoBpmSyncValue { 9 | fn default() -> Self { 10 | Self(true) 11 | } 12 | } 13 | 14 | impl ParameterValue for LfoBpmSyncValue { 15 | type Value = bool; 16 | 17 | fn new_from_audio(value: Self::Value) -> Self { 18 | Self(value) 19 | } 20 | fn new_from_text(text: &str) -> Option { 21 | match text.to_lowercase().as_ref() { 22 | "true" | "on" => Some(Self(true)), 23 | "false" | "off" => Some(Self(false)), 24 | _ => None, 25 | } 26 | } 27 | fn get(self) -> Self::Value { 28 | self.0 29 | } 30 | fn new_from_patch(value: f32) -> Self { 31 | Self(value <= 0.5) 32 | } 33 | fn to_patch(self) -> f32 { 34 | if self.0 { 35 | 0.0 36 | } else { 37 | 1.0 38 | } 39 | } 40 | fn get_formatted(self) -> CompactString { 41 | if self.0 { 42 | "On".into() 43 | } else { 44 | "Off".into() 45 | } 46 | } 47 | 48 | fn get_serializable(&self) -> SerializableRepresentation { 49 | SerializableRepresentation::Other(self.get_formatted()) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /octasine/src/parameters/lfo_frequency_free.rs: -------------------------------------------------------------------------------- 1 | use compact_str::format_compact; 2 | use compact_str::CompactString; 3 | 4 | use super::utils::*; 5 | use super::ParameterValue; 6 | use super::SerializableRepresentation; 7 | 8 | const LFO_FREQUENCY_FREE_STEPS: [f32; 7] = [1.0 / 16.0, 0.5, 0.9, 1.0, 1.1, 2.0, 16.0]; 9 | 10 | #[derive(Debug, Clone, Copy)] 11 | pub struct LfoFrequencyFreeValue(pub f64); 12 | 13 | impl Default for LfoFrequencyFreeValue { 14 | fn default() -> Self { 15 | Self(1.0) 16 | } 17 | } 18 | 19 | impl ParameterValue for LfoFrequencyFreeValue { 20 | type Value = f64; 21 | 22 | fn new_from_audio(value: Self::Value) -> Self { 23 | Self(value) 24 | } 25 | fn new_from_text(text: &str) -> Option { 26 | const MIN: f32 = LFO_FREQUENCY_FREE_STEPS[0]; 27 | const MAX: f32 = LFO_FREQUENCY_FREE_STEPS[LFO_FREQUENCY_FREE_STEPS.len() - 1]; 28 | 29 | parse_valid_f32(text, MIN, MAX).map(|v| Self(v.into())) 30 | } 31 | fn get(self) -> Self::Value { 32 | self.0 33 | } 34 | fn new_from_patch(value: f32) -> Self { 35 | Self(map_patch_to_audio_value_with_steps(&LFO_FREQUENCY_FREE_STEPS, value) as f64) 36 | } 37 | fn to_patch(self) -> f32 { 38 | map_audio_to_patch_value_with_steps(&LFO_FREQUENCY_FREE_STEPS, self.0 as f32) 39 | } 40 | fn get_formatted(self) -> CompactString { 41 | format_compact!("{:.04}", self.0) 42 | } 43 | 44 | fn get_serializable(&self) -> SerializableRepresentation { 45 | SerializableRepresentation::Float(self.0) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /octasine/src/parameters/lfo_frequency_ratio.rs: -------------------------------------------------------------------------------- 1 | use compact_str::format_compact; 2 | use compact_str::CompactString; 3 | 4 | use super::utils::*; 5 | use super::ParameterValue; 6 | use super::SerializableRepresentation; 7 | 8 | const LFO_FREQUENCY_RATIO_STEPS: [f32; 9] = [ 9 | 1.0 / 16.0, 10 | 1.0 / 8.0, 11 | 1.0 / 4.0, 12 | 1.0 / 2.0, 13 | 1.0, 14 | 2.0, 15 | 4.0, 16 | 8.0, 17 | 16.0, 18 | ]; 19 | 20 | #[derive(Debug, Clone, Copy)] 21 | pub struct LfoFrequencyRatioValue(pub f64); 22 | 23 | impl Default for LfoFrequencyRatioValue { 24 | fn default() -> Self { 25 | Self(1.0) 26 | } 27 | } 28 | 29 | impl ParameterValue for LfoFrequencyRatioValue { 30 | type Value = f64; 31 | 32 | fn new_from_audio(value: Self::Value) -> Self { 33 | Self(value) 34 | } 35 | fn new_from_text(text: &str) -> Option { 36 | const MIN: f32 = LFO_FREQUENCY_RATIO_STEPS[0]; 37 | const MAX: f32 = LFO_FREQUENCY_RATIO_STEPS[LFO_FREQUENCY_RATIO_STEPS.len() - 1]; 38 | 39 | let value = parse_valid_f32(text, MIN, MAX)?; 40 | 41 | Some(Self( 42 | round_to_step(&LFO_FREQUENCY_RATIO_STEPS[..], value).into(), 43 | )) 44 | } 45 | fn get(self) -> Self::Value { 46 | self.0 47 | } 48 | fn new_from_patch(value: f32) -> Self { 49 | Self(map_patch_value_to_step(&LFO_FREQUENCY_RATIO_STEPS, value) as f64) 50 | } 51 | fn to_patch(self) -> f32 { 52 | map_step_to_patch_value(&LFO_FREQUENCY_RATIO_STEPS, self.0 as f32) 53 | } 54 | fn get_formatted(self) -> CompactString { 55 | format_compact!("{:.04}", self.0) 56 | } 57 | 58 | fn get_serializable(&self) -> SerializableRepresentation { 59 | SerializableRepresentation::Float(self.0) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /octasine/src/parameters/lfo_key_sync.rs: -------------------------------------------------------------------------------- 1 | use compact_str::CompactString; 2 | 3 | use super::{ParameterValue, SerializableRepresentation}; 4 | 5 | #[derive(Debug, Clone, Copy)] 6 | pub struct LfoKeySyncValue(pub bool); 7 | 8 | impl Default for LfoKeySyncValue { 9 | fn default() -> Self { 10 | Self(true) 11 | } 12 | } 13 | 14 | impl ParameterValue for LfoKeySyncValue { 15 | type Value = bool; 16 | 17 | fn new_from_audio(value: Self::Value) -> Self { 18 | Self(value) 19 | } 20 | fn new_from_text(text: &str) -> Option { 21 | match text.to_lowercase().as_ref() { 22 | "true" | "on" => Some(Self(true)), 23 | "false" | "off" => Some(Self(false)), 24 | _ => None, 25 | } 26 | } 27 | fn get(self) -> Self::Value { 28 | self.0 29 | } 30 | fn new_from_patch(value: f32) -> Self { 31 | Self(value <= 0.5) 32 | } 33 | fn to_patch(self) -> f32 { 34 | if self.0 { 35 | 0.0 36 | } else { 37 | 1.0 38 | } 39 | } 40 | fn get_formatted(self) -> CompactString { 41 | if self.0 { 42 | "ON".into() 43 | } else { 44 | "OFF".into() 45 | } 46 | } 47 | 48 | fn get_serializable(&self) -> SerializableRepresentation { 49 | SerializableRepresentation::Other(self.get_formatted()) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /octasine/src/parameters/lfo_mode.rs: -------------------------------------------------------------------------------- 1 | use compact_str::CompactString; 2 | 3 | use super::utils::*; 4 | use super::ParameterValue; 5 | use super::SerializableRepresentation; 6 | 7 | const LFO_MODE_STEPS: [LfoMode; 2] = [LfoMode::Forever, LfoMode::Once]; 8 | 9 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] 10 | pub enum LfoMode { 11 | Once, 12 | #[default] 13 | Forever, 14 | } 15 | 16 | #[derive(Debug, Clone, Copy, Default)] 17 | pub struct LfoModeValue(pub LfoMode); 18 | 19 | impl ParameterValue for LfoModeValue { 20 | type Value = LfoMode; 21 | 22 | fn new_from_audio(value: Self::Value) -> Self { 23 | Self(value) 24 | } 25 | fn new_from_text(text: &str) -> Option { 26 | match text.to_lowercase().as_ref() { 27 | "once" => Some(Self(LfoMode::Once)), 28 | "forever" => Some(Self(LfoMode::Forever)), 29 | _ => None, 30 | } 31 | } 32 | fn get(self) -> Self::Value { 33 | self.0 34 | } 35 | fn new_from_patch(value: f32) -> Self { 36 | Self(map_patch_value_to_step(&LFO_MODE_STEPS[..], value)) 37 | } 38 | fn to_patch(self) -> f32 { 39 | map_step_to_patch_value(&LFO_MODE_STEPS[..], self.0) 40 | } 41 | fn get_formatted(self) -> CompactString { 42 | match self.0 { 43 | LfoMode::Once => "ONCE".into(), 44 | LfoMode::Forever => "LOOP".into(), 45 | } 46 | } 47 | 48 | fn get_serializable(&self) -> SerializableRepresentation { 49 | SerializableRepresentation::Other(self.get_formatted()) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /octasine/src/parameters/master_frequency.rs: -------------------------------------------------------------------------------- 1 | use compact_str::format_compact; 2 | use compact_str::CompactString; 3 | 4 | use super::utils::*; 5 | use super::ParameterValue; 6 | use super::SerializableRepresentation; 7 | 8 | const MASTER_FREQUENCY_STEPS: &[f32] = &[ 9 | 20.0, 220.0, 400.0, 435.0, 438.0, 440.0, 442.0, 445.0, 480.0, 880.0, 20_000.0, 10 | ]; 11 | 12 | #[derive(Debug, Clone, Copy)] 13 | pub struct MasterFrequencyValue(f64); 14 | 15 | impl Default for MasterFrequencyValue { 16 | fn default() -> Self { 17 | Self(440.0) 18 | } 19 | } 20 | 21 | impl ParameterValue for MasterFrequencyValue { 22 | type Value = f64; 23 | 24 | fn new_from_audio(value: Self::Value) -> Self { 25 | Self(value) 26 | } 27 | fn new_from_text(text: &str) -> Option { 28 | const MIN: f32 = MASTER_FREQUENCY_STEPS[0]; 29 | const MAX: f32 = MASTER_FREQUENCY_STEPS[MASTER_FREQUENCY_STEPS.len() - 1]; 30 | 31 | parse_valid_f32(text, MIN, MAX).map(|v| Self(v.into())) 32 | } 33 | fn get(self) -> Self::Value { 34 | self.0 35 | } 36 | fn new_from_patch(value: f32) -> Self { 37 | Self(map_patch_to_audio_value_with_steps(MASTER_FREQUENCY_STEPS, value) as f64) 38 | } 39 | fn to_patch(self) -> f32 { 40 | map_audio_to_patch_value_with_steps(MASTER_FREQUENCY_STEPS, self.0 as f32) 41 | } 42 | fn get_formatted(self) -> CompactString { 43 | if self.0 < 10000.0 { 44 | format_compact!("{:.02} Hz", self.0) 45 | } else { 46 | format_compact!("{:.02}", self.0) 47 | } 48 | } 49 | 50 | fn get_serializable(&self) -> SerializableRepresentation { 51 | SerializableRepresentation::Float(self.0) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /octasine/src/parameters/master_pitch_bend_range.rs: -------------------------------------------------------------------------------- 1 | use compact_str::format_compact; 2 | use compact_str::CompactString; 3 | 4 | use super::utils::*; 5 | use super::ParameterValue; 6 | use super::SerializableRepresentation; 7 | 8 | const STEPS: &[f32] = &[ 9 | -48.0, -24.0, -12.0, -11.0, -10.0, -9.0, -8.0, -7.0, -6.0, -5.0, -4.0, -3.0, -2.0, -1.0, 0.0, 10 | 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 24.0, 48.0, 11 | ]; 12 | 13 | #[derive(Debug, Clone, Copy)] 14 | pub struct MasterPitchBendRangeUpValue(f32); 15 | 16 | impl Default for MasterPitchBendRangeUpValue { 17 | fn default() -> Self { 18 | Self(2.0) 19 | } 20 | } 21 | 22 | impl ParameterValue for MasterPitchBendRangeUpValue { 23 | type Value = f32; 24 | 25 | fn new_from_audio(value: Self::Value) -> Self { 26 | Self(value) 27 | } 28 | fn new_from_text(text: &str) -> Option { 29 | const MIN: f32 = STEPS[0]; 30 | const MAX: f32 = STEPS[STEPS.len() - 1]; 31 | 32 | Some(Self( 33 | round_to_step(STEPS, parse_valid_f32(text, MIN, MAX)?).into(), 34 | )) 35 | } 36 | fn get(self) -> Self::Value { 37 | self.0 38 | } 39 | fn new_from_patch(value: f32) -> Self { 40 | Self(map_patch_value_to_step(STEPS, value)) 41 | } 42 | fn to_patch(self) -> f32 { 43 | map_step_to_patch_value(STEPS, self.0) 44 | } 45 | fn get_formatted(self) -> CompactString { 46 | format_compact!("{:.0} SEMIS", self.0) 47 | } 48 | 49 | fn get_serializable(&self) -> SerializableRepresentation { 50 | SerializableRepresentation::Float(self.0 as f64) 51 | } 52 | } 53 | 54 | #[derive(Debug, Clone, Copy)] 55 | pub struct MasterPitchBendRangeDownValue(f32); 56 | 57 | impl Default for MasterPitchBendRangeDownValue { 58 | fn default() -> Self { 59 | Self(-2.0) 60 | } 61 | } 62 | 63 | impl ParameterValue for MasterPitchBendRangeDownValue { 64 | type Value = f32; 65 | 66 | fn new_from_audio(value: Self::Value) -> Self { 67 | Self(value) 68 | } 69 | fn new_from_text(text: &str) -> Option { 70 | const MIN: f32 = STEPS[0]; 71 | const MAX: f32 = STEPS[STEPS.len() - 1]; 72 | 73 | Some(Self( 74 | round_to_step(STEPS, parse_valid_f32(text, MIN, MAX)?).into(), 75 | )) 76 | } 77 | fn get(self) -> Self::Value { 78 | self.0 79 | } 80 | fn new_from_patch(value: f32) -> Self { 81 | Self(map_patch_value_to_step(STEPS, value)) 82 | } 83 | fn to_patch(self) -> f32 { 84 | map_step_to_patch_value(STEPS, self.0) 85 | } 86 | fn get_formatted(self) -> CompactString { 87 | format_compact!("{:.0} SEMIS", self.0) 88 | } 89 | 90 | fn get_serializable(&self) -> SerializableRepresentation { 91 | SerializableRepresentation::Float(self.0 as f64) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /octasine/src/parameters/master_volume.rs: -------------------------------------------------------------------------------- 1 | use compact_str::{format_compact, CompactString}; 2 | 3 | use super::{utils::parse_valid_f32, ParameterValue, SerializableRepresentation}; 4 | 5 | #[derive(Debug, Clone, Copy)] 6 | pub struct MasterVolumeValue(f32); 7 | 8 | impl Default for MasterVolumeValue { 9 | fn default() -> Self { 10 | Self(1.0) 11 | } 12 | } 13 | 14 | impl ParameterValue for MasterVolumeValue { 15 | type Value = f32; 16 | 17 | fn new_from_audio(value: Self::Value) -> Self { 18 | Self(value) 19 | } 20 | fn new_from_text(text: &str) -> Option { 21 | parse_valid_f32(text, 0.0, 2.0).map(Self) 22 | } 23 | fn get(self) -> Self::Value { 24 | self.0 25 | } 26 | fn new_from_patch(value: f32) -> Self { 27 | Self(value * 2.0) 28 | } 29 | fn to_patch(self) -> f32 { 30 | self.0 / 2.0 31 | } 32 | fn get_formatted(self) -> CompactString { 33 | format_compact!("{:.2} dB", 20.0 * self.0.log10()) 34 | } 35 | 36 | fn get_serializable(&self) -> SerializableRepresentation { 37 | SerializableRepresentation::Float(self.0.into()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /octasine/src/parameters/operator_active.rs: -------------------------------------------------------------------------------- 1 | use compact_str::CompactString; 2 | 3 | use super::{ParameterValue, SerializableRepresentation}; 4 | 5 | #[derive(Debug, Clone, Copy)] 6 | pub struct OperatorActiveValue(f32); 7 | 8 | impl Default for OperatorActiveValue { 9 | fn default() -> Self { 10 | Self(1.0) 11 | } 12 | } 13 | 14 | impl ParameterValue for OperatorActiveValue { 15 | type Value = f32; 16 | 17 | fn new_from_audio(value: Self::Value) -> Self { 18 | Self(value.round()) 19 | } 20 | 21 | fn new_from_text(text: &str) -> Option { 22 | match text.trim().to_lowercase().as_str() { 23 | "on" | "active" => Some(Self(1.0)), 24 | "off" | "inactive" => Some(Self(0.0)), 25 | _ => None, 26 | } 27 | } 28 | fn get(self) -> Self::Value { 29 | self.0 30 | } 31 | fn new_from_patch(value: f32) -> Self { 32 | Self(value.round()) 33 | } 34 | fn to_patch(self) -> f32 { 35 | self.0 36 | } 37 | fn get_formatted(self) -> CompactString { 38 | if self.0 < 0.5 { 39 | "Off".into() 40 | } else { 41 | "On".into() 42 | } 43 | } 44 | 45 | fn get_serializable(&self) -> SerializableRepresentation { 46 | SerializableRepresentation::Other(self.get_formatted()) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /octasine/src/parameters/operator_feedback.rs: -------------------------------------------------------------------------------- 1 | use compact_str::{format_compact, CompactString}; 2 | 3 | use super::{utils::*, ParameterValue, SerializableRepresentation}; 4 | use crate::common::OPERATOR_MOD_INDEX_STEPS; 5 | 6 | #[derive(Debug, Clone, Copy)] 7 | pub struct OperatorFeedbackValue(f32); 8 | 9 | impl Default for OperatorFeedbackValue { 10 | fn default() -> Self { 11 | Self(0.0) 12 | } 13 | } 14 | 15 | impl ParameterValue for OperatorFeedbackValue { 16 | type Value = f32; 17 | 18 | fn new_from_audio(value: Self::Value) -> Self { 19 | Self(value) 20 | } 21 | fn new_from_text(text: &str) -> Option { 22 | const MIN: f32 = OPERATOR_MOD_INDEX_STEPS[0]; 23 | const MAX: f32 = OPERATOR_MOD_INDEX_STEPS[OPERATOR_MOD_INDEX_STEPS.len() - 1]; 24 | 25 | parse_valid_f32(text, MIN, MAX).map(Self) 26 | } 27 | fn get(self) -> Self::Value { 28 | self.0 29 | } 30 | fn new_from_patch(value: f32) -> Self { 31 | Self(map_patch_to_audio_value_with_steps( 32 | &OPERATOR_MOD_INDEX_STEPS[..], 33 | value, 34 | )) 35 | } 36 | fn to_patch(self) -> f32 { 37 | map_audio_to_patch_value_with_steps(&OPERATOR_MOD_INDEX_STEPS[..], self.0) 38 | } 39 | fn get_formatted(self) -> CompactString { 40 | format_compact!("{:.04}", self.0) 41 | } 42 | 43 | fn get_serializable(&self) -> SerializableRepresentation { 44 | SerializableRepresentation::Float(self.0.into()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /octasine/src/parameters/operator_frequency_fine.rs: -------------------------------------------------------------------------------- 1 | use compact_str::format_compact; 2 | use compact_str::CompactString; 3 | 4 | use super::utils::*; 5 | use super::ParameterValue; 6 | use super::SerializableRepresentation; 7 | 8 | const OPERATOR_FINE_STEPS: [f32; 17] = [ 9 | 0.8, 0.85, 0.9, 0.95, 0.97, 0.98, 0.99, 0.995, 1.0, 1.005, 1.01, 1.02, 1.03, 1.05, 1.1, 1.15, 10 | 1.2, 11 | ]; 12 | 13 | #[derive(Debug, Clone, Copy)] 14 | pub struct OperatorFrequencyFineValue(f64); 15 | 16 | impl Default for OperatorFrequencyFineValue { 17 | fn default() -> Self { 18 | Self(1.0) 19 | } 20 | } 21 | 22 | impl ParameterValue for OperatorFrequencyFineValue { 23 | type Value = f64; 24 | 25 | fn new_from_audio(value: Self::Value) -> Self { 26 | Self(value) 27 | } 28 | fn new_from_text(text: &str) -> Option { 29 | const MIN: f32 = OPERATOR_FINE_STEPS[0]; 30 | const MAX: f32 = OPERATOR_FINE_STEPS[OPERATOR_FINE_STEPS.len() - 1]; 31 | 32 | parse_valid_f32(text, MIN, MAX).map(|v| Self(v.into())) 33 | } 34 | fn get(self) -> Self::Value { 35 | self.0 36 | } 37 | fn new_from_patch(value: f32) -> Self { 38 | Self(map_patch_to_audio_value_with_steps(&OPERATOR_FINE_STEPS, value) as f64) 39 | } 40 | fn to_patch(self) -> f32 { 41 | map_audio_to_patch_value_with_steps(&OPERATOR_FINE_STEPS, self.0 as f32) 42 | } 43 | fn get_formatted(self) -> CompactString { 44 | format_compact!("{:.04}", self.0) 45 | } 46 | 47 | fn get_serializable(&self) -> SerializableRepresentation { 48 | SerializableRepresentation::Float(self.0) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /octasine/src/parameters/operator_frequency_free.rs: -------------------------------------------------------------------------------- 1 | use compact_str::format_compact; 2 | use compact_str::CompactString; 3 | 4 | use super::utils::*; 5 | use super::ParameterValue; 6 | use super::SerializableRepresentation; 7 | 8 | const OPERATOR_FREE_STEPS: &[f32] = &[ 9 | 1.0 / 1024.0, 10 | 1.0 / 64.0, 11 | 1.0 / 16.0, 12 | 0.25, 13 | 0.5, 14 | 0.75, 15 | 1.0, 16 | 1.5, 17 | 2.0, 18 | 4.0, 19 | 16.0, 20 | 64.0, 21 | 1024.0, 22 | ]; 23 | 24 | #[derive(Debug, Clone, Copy)] 25 | pub struct OperatorFrequencyFreeValue(f64); 26 | 27 | impl Default for OperatorFrequencyFreeValue { 28 | fn default() -> Self { 29 | Self(1.0) 30 | } 31 | } 32 | 33 | impl ParameterValue for OperatorFrequencyFreeValue { 34 | type Value = f64; 35 | 36 | fn new_from_audio(value: Self::Value) -> Self { 37 | Self(value) 38 | } 39 | fn new_from_text(text: &str) -> Option { 40 | const MIN: f32 = OPERATOR_FREE_STEPS[0]; 41 | const MAX: f32 = OPERATOR_FREE_STEPS[OPERATOR_FREE_STEPS.len() - 1]; 42 | 43 | parse_valid_f32(text, MIN, MAX).map(|v| Self(v.into())) 44 | } 45 | fn get(self) -> Self::Value { 46 | self.0 47 | } 48 | fn new_from_patch(value: f32) -> Self { 49 | Self(map_patch_to_audio_value_with_steps(OPERATOR_FREE_STEPS, value) as f64) 50 | } 51 | fn to_patch(self) -> f32 { 52 | map_audio_to_patch_value_with_steps(OPERATOR_FREE_STEPS, self.0 as f32) 53 | } 54 | fn get_formatted(self) -> CompactString { 55 | format_compact!("{:.04}", self.0) 56 | } 57 | 58 | fn get_serializable(&self) -> SerializableRepresentation { 59 | SerializableRepresentation::Float(self.0) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /octasine/src/parameters/operator_mix_out.rs: -------------------------------------------------------------------------------- 1 | use compact_str::{format_compact, CompactString}; 2 | 3 | use super::{utils::parse_valid_f32, ParameterValue, SerializableRepresentation}; 4 | 5 | #[derive(Default, Debug, Clone, Copy)] 6 | pub struct OperatorMixOutValue(f32); 7 | 8 | impl OperatorMixOutValue { 9 | pub fn new(index: usize) -> Self { 10 | if index == 0 { 11 | Self(1.0) 12 | } else { 13 | Self(0.0) 14 | } 15 | } 16 | } 17 | 18 | impl ParameterValue for OperatorMixOutValue { 19 | type Value = f32; 20 | 21 | fn new_from_audio(value: Self::Value) -> Self { 22 | Self(value) 23 | } 24 | fn new_from_text(text: &str) -> Option { 25 | parse_valid_f32(text, 0.0, 2.0).map(Self) 26 | } 27 | fn get(self) -> Self::Value { 28 | self.0 29 | } 30 | fn new_from_patch(value: f32) -> Self { 31 | Self(value * 2.0) 32 | } 33 | fn to_patch(self) -> f32 { 34 | self.0 / 2.0 35 | } 36 | fn get_formatted(self) -> CompactString { 37 | format_compact!("{:.04}", self.0) 38 | } 39 | 40 | fn get_serializable(&self) -> SerializableRepresentation { 41 | SerializableRepresentation::Float(self.0.into()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /octasine/src/parameters/operator_mod_out.rs: -------------------------------------------------------------------------------- 1 | use compact_str::{format_compact, CompactString}; 2 | 3 | use super::{utils::*, ParameterValue, SerializableRepresentation}; 4 | use crate::common::OPERATOR_MOD_INDEX_STEPS; 5 | 6 | #[derive(Debug, Clone, Copy)] 7 | pub struct OperatorModOutValue(f32); 8 | 9 | impl Default for OperatorModOutValue { 10 | fn default() -> Self { 11 | Self(0.0) 12 | } 13 | } 14 | 15 | impl ParameterValue for OperatorModOutValue { 16 | type Value = f32; 17 | 18 | fn new_from_audio(value: Self::Value) -> Self { 19 | Self(value) 20 | } 21 | fn new_from_text(text: &str) -> Option { 22 | const MIN: f32 = OPERATOR_MOD_INDEX_STEPS[0]; 23 | const MAX: f32 = OPERATOR_MOD_INDEX_STEPS[OPERATOR_MOD_INDEX_STEPS.len() - 1]; 24 | 25 | parse_valid_f32(text, MIN, MAX).map(Self) 26 | } 27 | fn get(self) -> Self::Value { 28 | self.0 29 | } 30 | fn new_from_patch(value: f32) -> Self { 31 | Self(map_patch_to_audio_value_with_steps( 32 | &OPERATOR_MOD_INDEX_STEPS[..], 33 | value, 34 | )) 35 | } 36 | fn to_patch(self) -> f32 { 37 | map_audio_to_patch_value_with_steps(&OPERATOR_MOD_INDEX_STEPS[..], self.0) 38 | } 39 | fn get_formatted(self) -> CompactString { 40 | format_compact!("{:.04}", self.0) 41 | } 42 | 43 | fn get_serializable(&self) -> SerializableRepresentation { 44 | SerializableRepresentation::Float(self.0.into()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /octasine/src/parameters/operator_panning.rs: -------------------------------------------------------------------------------- 1 | use std::f32::consts::FRAC_PI_2; 2 | 3 | use compact_str::{format_compact, CompactString}; 4 | 5 | use super::{utils::parse_valid_f32, ParameterValue, SerializableRepresentation}; 6 | 7 | #[derive(Debug, Clone, Copy)] 8 | pub struct OperatorPanningValue(f32); 9 | 10 | impl OperatorPanningValue { 11 | pub fn calculate_left_and_right(&self) -> [f32; 2] { 12 | let pan_phase = self.0 * FRAC_PI_2; 13 | 14 | [ 15 | ::sleef_trig::Sleef_cosf1_u35purec_range125(pan_phase), 16 | ::sleef_trig::Sleef_sinf1_u35purec_range125(pan_phase), 17 | ] 18 | } 19 | } 20 | 21 | impl Default for OperatorPanningValue { 22 | fn default() -> Self { 23 | Self(0.5) 24 | } 25 | } 26 | 27 | impl ParameterValue for OperatorPanningValue { 28 | type Value = f32; 29 | 30 | fn new_from_audio(value: Self::Value) -> Self { 31 | Self(value) 32 | } 33 | fn new_from_text(text: &str) -> Option { 34 | let text = text.trim().to_lowercase(); 35 | 36 | if text.as_str() == "c" || text.as_str() == "0" { 37 | Some(Self(0.5)) 38 | } else if let Some(index) = text.rfind('r') { 39 | let mut text = text; 40 | 41 | text.remove(index); 42 | 43 | let value = parse_valid_f32(&text, 0.0, 50.0)?; 44 | 45 | Some(Self((0.5 + value / 100.0).min(1.0).max(0.0))) 46 | } else if let Some(index) = text.rfind('l') { 47 | let mut text = text; 48 | 49 | text.remove(index); 50 | 51 | let value = parse_valid_f32(&text, 0.0, 50.0)?; 52 | 53 | Some(Self((0.5 - value / 100.0).min(1.0).max(0.0))) 54 | } else { 55 | None 56 | } 57 | } 58 | fn get(self) -> Self::Value { 59 | self.0 60 | } 61 | fn new_from_patch(value: f32) -> Self { 62 | Self(value) 63 | } 64 | fn to_patch(self) -> f32 { 65 | self.0 66 | } 67 | fn get_formatted(self) -> CompactString { 68 | let pan = ((self.0 - 0.5) * 100.0).round() as isize; 69 | 70 | match pan.cmp(&0) { 71 | std::cmp::Ordering::Greater => format_compact!("{}R", pan), 72 | std::cmp::Ordering::Less => format_compact!("{}L", pan.abs()), 73 | std::cmp::Ordering::Equal => "C".into(), 74 | } 75 | } 76 | 77 | fn get_serializable(&self) -> SerializableRepresentation { 78 | SerializableRepresentation::Float(self.0.into()) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /octasine/src/parameters/operator_volume.rs: -------------------------------------------------------------------------------- 1 | use compact_str::{format_compact, CompactString}; 2 | 3 | use super::{utils::parse_valid_f32, ParameterValue, SerializableRepresentation}; 4 | 5 | #[derive(Debug, Clone, Copy)] 6 | pub struct OperatorVolumeValue(f32); 7 | 8 | impl Default for OperatorVolumeValue { 9 | fn default() -> Self { 10 | Self(1.0) 11 | } 12 | } 13 | 14 | impl ParameterValue for OperatorVolumeValue { 15 | type Value = f32; 16 | 17 | fn new_from_audio(value: Self::Value) -> Self { 18 | Self(value) 19 | } 20 | fn new_from_text(text: &str) -> Option { 21 | parse_valid_f32(text, 0.0, 2.0).map(Self) 22 | } 23 | fn get(self) -> Self::Value { 24 | self.0 25 | } 26 | fn new_from_patch(value: f32) -> Self { 27 | Self(value * 2.0) 28 | } 29 | fn to_patch(self) -> f32 { 30 | self.0 / 2.0 31 | } 32 | fn get_formatted(self) -> CompactString { 33 | format_compact!("{:.04}", self.0) 34 | } 35 | 36 | fn get_serializable(&self) -> SerializableRepresentation { 37 | SerializableRepresentation::Float(self.0.into()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /octasine/src/parameters/operator_wave_type.rs: -------------------------------------------------------------------------------- 1 | use std::f32::consts::TAU; 2 | 3 | use compact_str::CompactString; 4 | 5 | use crate::common::*; 6 | 7 | use super::{ 8 | utils::{map_patch_value_to_step, map_step_to_patch_value}, 9 | {ParameterValue, SerializableRepresentation}, 10 | }; 11 | 12 | const OPERATOR_WAVEFORMS: &[WaveType] = &[ 13 | WaveType::Sine, 14 | WaveType::Square, 15 | WaveType::Triangle, 16 | WaveType::Saw, 17 | WaveType::WhiteNoise, 18 | ]; 19 | 20 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] 21 | pub enum WaveType { 22 | #[default] 23 | Sine, 24 | Square, 25 | Triangle, 26 | Saw, 27 | WhiteNoise, 28 | } 29 | 30 | impl WaveformChoices for WaveType { 31 | fn calculate_for_current(self, phase: Phase) -> f32 { 32 | match self { 33 | Self::Sine => ::sleef_trig::Sleef_sinf1_u35purec_range125(phase.0 as f32 * TAU), 34 | Self::Saw => crate::math::wave::saw(phase.0) as f32, 35 | Self::Triangle => crate::math::wave::triangle(phase.0) as f32, 36 | Self::Square => crate::math::wave::square(phase.0) as f32, 37 | Self::WhiteNoise => { 38 | // Ensure same numbers are generated each time for GUI 39 | // consistency. This will however break if fastrand changes 40 | // its algorithm. 41 | let seed = phase.0.to_bits() + 2; 42 | 43 | // Generate f64 because that exact value looks nice 44 | ((fastrand::Rng::with_seed(seed).f64() - 0.5) * 2.0) as f32 45 | } 46 | } 47 | } 48 | fn choices() -> &'static [Self] { 49 | OPERATOR_WAVEFORMS 50 | } 51 | } 52 | 53 | #[derive(Debug, Clone, Copy, Default)] 54 | pub struct OperatorWaveTypeValue(pub WaveType); 55 | 56 | impl ParameterValue for OperatorWaveTypeValue { 57 | type Value = WaveType; 58 | 59 | fn new_from_audio(value: Self::Value) -> Self { 60 | Self(value) 61 | } 62 | fn new_from_text(text: &str) -> Option { 63 | match text.to_lowercase().trim() { 64 | "sine" => Some(Self(WaveType::Sine)), 65 | "square" => Some(Self(WaveType::Square)), 66 | "triangle" => Some(Self(WaveType::Triangle)), 67 | "saw" => Some(Self(WaveType::Saw)), 68 | "noise" => Some(Self(WaveType::WhiteNoise)), 69 | _ => None, 70 | } 71 | } 72 | fn get(self) -> Self::Value { 73 | self.0 74 | } 75 | fn new_from_patch(value: f32) -> Self { 76 | Self(map_patch_value_to_step(OPERATOR_WAVEFORMS, value)) 77 | } 78 | fn to_patch(self) -> f32 { 79 | map_step_to_patch_value(OPERATOR_WAVEFORMS, self.0) 80 | } 81 | fn get_formatted(self) -> CompactString { 82 | match self.0 { 83 | WaveType::Sine => "SINE".into(), 84 | WaveType::Square => "SQUARE".into(), 85 | WaveType::Triangle => "TRIANGLE".into(), 86 | WaveType::Saw => "SAW".into(), 87 | WaveType::WhiteNoise => "NOISE".into(), 88 | } 89 | } 90 | 91 | fn get_serializable(&self) -> SerializableRepresentation { 92 | SerializableRepresentation::Other(self.get_formatted()) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /octasine/src/parameters/velocity_sensitivity.rs: -------------------------------------------------------------------------------- 1 | use compact_str::{format_compact, CompactString}; 2 | 3 | use super::{utils::parse_valid_f32, ParameterValue, SerializableRepresentation}; 4 | 5 | #[derive(Debug, Clone, Copy)] 6 | pub struct VelocitySensitivityValue(f32); 7 | 8 | impl Default for VelocitySensitivityValue { 9 | fn default() -> Self { 10 | Self(1.0) 11 | } 12 | } 13 | 14 | impl ParameterValue for VelocitySensitivityValue { 15 | type Value = f32; 16 | 17 | fn new_from_audio(value: Self::Value) -> Self { 18 | Self(value) 19 | } 20 | fn new_from_text(text: &str) -> Option { 21 | parse_valid_f32(text, 0.0, 1.0).map(Self) 22 | } 23 | fn get(self) -> Self::Value { 24 | self.0 25 | } 26 | fn new_from_patch(value: f32) -> Self { 27 | Self(value) 28 | } 29 | fn to_patch(self) -> f32 { 30 | self.0 31 | } 32 | fn get_formatted(self) -> CompactString { 33 | format_compact!("{:.04}", self.0) 34 | } 35 | 36 | fn get_serializable(&self) -> SerializableRepresentation { 37 | SerializableRepresentation::Float(self.0.into()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /octasine/src/parameters/voice_mode.rs: -------------------------------------------------------------------------------- 1 | use compact_str::CompactString; 2 | 3 | use super::{ 4 | utils::{map_patch_value_to_step, map_step_to_patch_value}, 5 | ParameterValue, SerializableRepresentation, 6 | }; 7 | 8 | const STEPS: &[VoiceMode] = &[VoiceMode::Polyphonic, VoiceMode::Monophonic]; 9 | 10 | #[derive(Debug, Clone, Copy, Default, PartialEq)] 11 | pub enum VoiceMode { 12 | #[default] 13 | Polyphonic, 14 | Monophonic, 15 | } 16 | 17 | #[derive(Debug, Clone, Copy, Default)] 18 | pub struct VoiceModeValue(VoiceMode); 19 | 20 | impl ParameterValue for VoiceModeValue { 21 | type Value = VoiceMode; 22 | 23 | fn new_from_audio(value: Self::Value) -> Self { 24 | Self(value) 25 | } 26 | fn new_from_text(text: &str) -> Option { 27 | let text = text.to_lowercase(); 28 | 29 | if text.contains("poly") { 30 | Some(Self(VoiceMode::Polyphonic)) 31 | } else if text.contains("mono") { 32 | Some(Self(VoiceMode::Monophonic)) 33 | } else { 34 | None 35 | } 36 | } 37 | fn get(self) -> Self::Value { 38 | self.0 39 | } 40 | fn new_from_patch(value: f32) -> Self { 41 | Self(map_patch_value_to_step(&STEPS[..], value)) 42 | } 43 | fn to_patch(self) -> f32 { 44 | map_step_to_patch_value(&STEPS[..], self.0) 45 | } 46 | fn get_formatted(self) -> CompactString { 47 | match self.0 { 48 | VoiceMode::Polyphonic => "POLY".into(), 49 | VoiceMode::Monophonic => "MONO".into(), 50 | } 51 | } 52 | 53 | fn get_serializable(&self) -> SerializableRepresentation { 54 | SerializableRepresentation::Other(self.get_formatted()) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /octasine/src/plugin/clap/descriptor.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ffi::{c_char, CStr, CString}, 3 | ptr::null, 4 | }; 5 | 6 | use clap_sys::{ 7 | plugin::clap_plugin_descriptor, 8 | plugin_features::{ 9 | CLAP_PLUGIN_FEATURE_INSTRUMENT, CLAP_PLUGIN_FEATURE_STEREO, CLAP_PLUGIN_FEATURE_SYNTHESIZER, 10 | }, 11 | version::CLAP_VERSION, 12 | }; 13 | use once_cell::sync::Lazy; 14 | 15 | pub const ID: *const c_char = 16 | unsafe { CStr::from_bytes_with_nul_unchecked(b"OctaSine\0").as_ptr() }; 17 | const NAME: *const c_char = unsafe { CStr::from_bytes_with_nul_unchecked(b"OctaSine\0").as_ptr() }; 18 | const VENDOR: *const c_char = 19 | unsafe { CStr::from_bytes_with_nul_unchecked(b"Joakim Frostegard\0").as_ptr() }; 20 | const URL: *const c_char = 21 | unsafe { CStr::from_bytes_with_nul_unchecked(b"https://octasine.com\0").as_ptr() }; 22 | 23 | const FEATURES: &[*const c_char] = &[ 24 | CLAP_PLUGIN_FEATURE_INSTRUMENT.as_ptr(), 25 | CLAP_PLUGIN_FEATURE_SYNTHESIZER.as_ptr(), 26 | CLAP_PLUGIN_FEATURE_STEREO.as_ptr(), 27 | null(), 28 | ]; 29 | 30 | static VERSION: Lazy = Lazy::new(|| CString::new(crate::crate_version!()).unwrap()); 31 | 32 | pub static DESCRIPTOR: Lazy = Lazy::new(|| clap_plugin_descriptor { 33 | clap_version: CLAP_VERSION, 34 | id: ID, 35 | name: NAME, 36 | vendor: VENDOR, 37 | url: URL, 38 | manual_url: null(), 39 | support_url: null(), 40 | version: Lazy::force(&VERSION).as_ptr(), 41 | description: null(), 42 | features: FEATURES.as_ptr(), 43 | }); 44 | -------------------------------------------------------------------------------- /octasine/src/plugin/clap/ext/audio_ports.rs: -------------------------------------------------------------------------------- 1 | use clap_sys::{ 2 | ext::audio_ports::{ 3 | clap_audio_port_info, clap_plugin_audio_ports, CLAP_AUDIO_PORT_IS_MAIN, CLAP_PORT_STEREO, 4 | }, 5 | id::CLAP_INVALID_ID, 6 | plugin::clap_plugin, 7 | }; 8 | 9 | pub extern "C" fn count(_plugin: *const clap_plugin, is_input: bool) -> u32 { 10 | if is_input { 11 | 0 12 | } else { 13 | 1 14 | } 15 | } 16 | pub unsafe extern "C" fn get( 17 | _plugin: *const clap_plugin, 18 | index: u32, 19 | is_input: bool, 20 | info: *mut clap_audio_port_info, 21 | ) -> bool { 22 | if index == 0 && !is_input { 23 | let info = &mut *info; 24 | 25 | info.id = 0; 26 | info.channel_count = 2; 27 | info.flags = CLAP_AUDIO_PORT_IS_MAIN; 28 | info.port_type = CLAP_PORT_STEREO.as_ptr(); 29 | info.in_place_pair = CLAP_INVALID_ID; 30 | 31 | true 32 | } else { 33 | return false; 34 | } 35 | } 36 | 37 | pub const CONFIG: clap_plugin_audio_ports = clap_plugin_audio_ports { 38 | count: Some(count), 39 | get: Some(get), 40 | }; 41 | -------------------------------------------------------------------------------- /octasine/src/plugin/clap/ext/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod audio_ports; 2 | pub mod gui; 3 | pub mod note_ports; 4 | pub mod params; 5 | pub mod state; 6 | pub mod voice_info; 7 | -------------------------------------------------------------------------------- /octasine/src/plugin/clap/ext/note_ports.rs: -------------------------------------------------------------------------------- 1 | use clap_sys::{ 2 | ext::note_ports::{ 3 | clap_note_port_info, clap_plugin_note_ports, CLAP_NOTE_DIALECT_CLAP, CLAP_NOTE_DIALECT_MIDI, 4 | }, 5 | plugin::clap_plugin, 6 | }; 7 | 8 | pub extern "C" fn count(_plugin: *const clap_plugin, _is_input: bool) -> u32 { 9 | // One input port, one output port 10 | 1 11 | } 12 | pub unsafe extern "C" fn get( 13 | _plugin: *const clap_plugin, 14 | index: u32, 15 | is_input: bool, 16 | info: *mut clap_note_port_info, 17 | ) -> bool { 18 | if index < 2 { 19 | let info = &mut *info; 20 | 21 | info.id = 0; 22 | info.supported_dialects = if is_input { 23 | CLAP_NOTE_DIALECT_MIDI | CLAP_NOTE_DIALECT_CLAP 24 | } else { 25 | CLAP_NOTE_DIALECT_CLAP 26 | }; 27 | info.preferred_dialect = CLAP_NOTE_DIALECT_CLAP; 28 | 29 | true 30 | } else { 31 | false 32 | } 33 | } 34 | 35 | pub const CONFIG: clap_plugin_note_ports = clap_plugin_note_ports { 36 | count: Some(count), 37 | get: Some(get), 38 | }; 39 | -------------------------------------------------------------------------------- /octasine/src/plugin/clap/ext/state.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::c_void; 2 | 3 | use clap_sys::{ 4 | ext::state::clap_plugin_state, 5 | plugin::clap_plugin, 6 | stream::{clap_istream, clap_ostream}, 7 | }; 8 | 9 | use crate::plugin::clap::plugin::OctaSine; 10 | 11 | const VERSION: u8 = 1; 12 | 13 | pub const CONFIG: clap_plugin_state = clap_plugin_state { 14 | save: Some(save), 15 | load: Some(load), 16 | }; 17 | 18 | unsafe extern "C" fn save(plugin: *const clap_plugin, stream: *const clap_ostream) -> bool { 19 | let plugin = &*((*plugin).plugin_data as *const OctaSine); 20 | 21 | if stream.is_null() { 22 | return false; 23 | } 24 | 25 | let write = if let Some(write) = (&*stream).write { 26 | write 27 | } else { 28 | return false; 29 | }; 30 | 31 | let mut bytes = plugin.sync.patches.export_plain_bytes(); 32 | 33 | // Add format version as first byte for future proofing 34 | bytes.insert(0, VERSION); 35 | 36 | let mut offset = 0; 37 | 38 | loop { 39 | let buffer = &bytes[offset..]; 40 | let result = write( 41 | stream, 42 | buffer.as_ptr() as *const c_void, 43 | buffer.len() as u64, 44 | ); 45 | 46 | if result > 0 { 47 | offset += result as u64 as usize; 48 | 49 | if offset == bytes.len() { 50 | return true; 51 | } 52 | } else { 53 | return false; 54 | } 55 | } 56 | } 57 | 58 | unsafe extern "C" fn load(plugin: *const clap_plugin, stream: *const clap_istream) -> bool { 59 | let plugin = &*((*plugin).plugin_data as *const OctaSine); 60 | 61 | if stream.is_null() { 62 | return false; 63 | } 64 | 65 | let read = if let Some(read) = (&*stream).read { 66 | read 67 | } else { 68 | return false; 69 | }; 70 | 71 | let mut full_buffer = Vec::new(); 72 | 73 | loop { 74 | let mut buffer = [0u8; 4096]; 75 | 76 | match read( 77 | stream, 78 | buffer.as_mut_ptr() as *mut c_void, 79 | buffer.len() as u64, 80 | ) { 81 | -1 => return false, 82 | 0 => break, 83 | n => { 84 | full_buffer.extend_from_slice(&buffer[..n as u64 as usize]); 85 | } 86 | } 87 | } 88 | 89 | if full_buffer.len() < 2 { 90 | return false; 91 | } 92 | 93 | // Remove first byte, it is the version signifier 94 | let full_buffer = &full_buffer[1..]; 95 | 96 | match plugin.sync.patches.import_bank_from_bytes(full_buffer) { 97 | Ok(()) => true, 98 | Err(err) => { 99 | ::log::error!("load OctaSineClapState: {:#}", err); 100 | 101 | false 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /octasine/src/plugin/clap/ext/voice_info.rs: -------------------------------------------------------------------------------- 1 | use clap_sys::{ 2 | ext::draft::voice_info::{clap_plugin_voice_info, clap_voice_info}, 3 | plugin::clap_plugin, 4 | }; 5 | 6 | unsafe extern "C" fn get(_plugin: *const clap_plugin, voice_info: *mut clap_voice_info) -> bool { 7 | *voice_info = clap_voice_info { 8 | voice_count: 128, 9 | voice_capacity: 128, 10 | flags: 0, 11 | }; 12 | 13 | true 14 | } 15 | 16 | pub const CONFIG: clap_plugin_voice_info = clap_plugin_voice_info { get: Some(get) }; 17 | -------------------------------------------------------------------------------- /octasine/src/plugin/clap/factory.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ffi::{c_char, CStr}, 3 | ptr::null, 4 | sync::Arc, 5 | }; 6 | 7 | use clap_sys::{ 8 | host::clap_host, 9 | plugin::{clap_plugin, clap_plugin_descriptor}, 10 | plugin_factory::clap_plugin_factory, 11 | }; 12 | use once_cell::sync::Lazy; 13 | 14 | use super::{descriptor::DESCRIPTOR, plugin::OctaSine}; 15 | 16 | pub const FACTORY: clap_plugin_factory = clap_plugin_factory { 17 | get_plugin_count: Some(get_plugin_count), 18 | get_plugin_descriptor: Some(get_plugin_descriptor), 19 | create_plugin: Some(create_plugin), 20 | }; 21 | 22 | pub extern "C" fn get_plugin_count(_factory: *const clap_plugin_factory) -> u32 { 23 | 1 24 | } 25 | 26 | pub extern "C" fn get_plugin_descriptor( 27 | _factory: *const clap_plugin_factory, 28 | index: u32, 29 | ) -> *const clap_plugin_descriptor { 30 | if index == 0 { 31 | Lazy::force(&DESCRIPTOR) as *const _ 32 | } else { 33 | null() 34 | } 35 | } 36 | 37 | pub unsafe extern "C" fn create_plugin( 38 | _factory: *const clap_plugin_factory, 39 | host: *const clap_host, 40 | plugin_id: *const c_char, 41 | ) -> *const clap_plugin { 42 | if !plugin_id.is_null() && CStr::from_ptr(plugin_id) == CStr::from_ptr(super::descriptor::ID) { 43 | (*Arc::into_raw(OctaSine::new(host))).clap_plugin.as_ptr() 44 | } else { 45 | null() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /octasine/src/plugin/clap/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod descriptor; 2 | pub mod ext; 3 | pub mod factory; 4 | pub mod plugin; 5 | pub mod sync; 6 | 7 | use std::{ 8 | ffi::{c_char, c_void, CStr}, 9 | ptr::null, 10 | }; 11 | 12 | use clap_sys::{ 13 | entry::clap_plugin_entry, plugin_factory::CLAP_PLUGIN_FACTORY_ID, version::CLAP_VERSION, 14 | }; 15 | 16 | pub const CLAP_ENTRY: clap_plugin_entry = clap_plugin_entry { 17 | clap_version: CLAP_VERSION, 18 | init: Some(init), 19 | deinit: Some(deinit), 20 | get_factory: Some(entry_get_factory), 21 | }; 22 | 23 | pub extern "C" fn init(_path: *const c_char) -> bool { 24 | true 25 | } 26 | 27 | pub extern "C" fn deinit() {} 28 | 29 | pub unsafe extern "C" fn entry_get_factory(factory_id: *const c_char) -> *const c_void { 30 | let factory_id = unsafe { CStr::from_ptr(factory_id) }; 31 | 32 | if factory_id == CLAP_PLUGIN_FACTORY_ID { 33 | &factory::FACTORY as *const _ as *const c_void 34 | } else { 35 | null() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /octasine/src/plugin/common.rs: -------------------------------------------------------------------------------- 1 | pub const PLUGIN_UNIQUE_VST2_ID: i32 = 1_438_048_626; 2 | pub const PLUGIN_SEMVER_NAME: &str = "OctaSine v0.9"; 3 | 4 | pub fn crate_version_to_vst2_format(crate_version: &str) -> i32 { 5 | format!("{:0<4}", crate_version.replace('.', "")) 6 | .parse() 7 | .expect("convert crate version to i32") 8 | } 9 | 10 | #[cfg(test)] 11 | mod tests { 12 | use super::*; 13 | 14 | #[allow(clippy::zero_prefixed_literal)] 15 | #[test] 16 | fn test_crate_version_to_vst_format() { 17 | assert_eq!(crate_version_to_vst2_format("1"), 1000); 18 | assert_eq!(crate_version_to_vst2_format("0.1"), 0100); 19 | assert_eq!(crate_version_to_vst2_format("0.0.2"), 0020); 20 | assert_eq!(crate_version_to_vst2_format("0.5.2"), 0520); 21 | assert_eq!(crate_version_to_vst2_format("1.0.1"), 1010); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /octasine/src/plugin/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "clap")] 2 | pub mod clap; 3 | pub mod common; 4 | #[cfg(feature = "vst2")] 5 | pub mod vst2; 6 | -------------------------------------------------------------------------------- /octasine/src/plugin/vst2/editor.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use iced_baseview::{open_blocking, open_parented, window::WindowHandle}; 4 | use parking_lot::Mutex; 5 | use raw_window_handle::{HasRawWindowHandle, RawWindowHandle}; 6 | 7 | use crate::{ 8 | gui::{get_iced_baseview_settings, Message, GUI_HEIGHT, GUI_WIDTH}, 9 | plugin::vst2::PLUGIN_SEMVER_NAME, 10 | sync::GuiSyncHandle, 11 | }; 12 | 13 | use crate::gui::OctaSineIcedApplication; 14 | 15 | pub struct Editor { 16 | sync_state: H, 17 | window_handle: Option, 18 | } 19 | 20 | impl Editor { 21 | pub fn new(sync_state: H) -> Self { 22 | Self { 23 | sync_state, 24 | window_handle: None, 25 | } 26 | } 27 | 28 | pub fn open_blocking(sync_handle: H) { 29 | open_blocking::>(get_iced_baseview_settings( 30 | sync_handle, 31 | PLUGIN_SEMVER_NAME.to_string(), 32 | )); 33 | } 34 | } 35 | 36 | impl vst::editor::Editor for Editor { 37 | fn size(&self) -> (i32, i32) { 38 | (GUI_WIDTH as i32, GUI_HEIGHT as i32) 39 | } 40 | 41 | fn position(&self) -> (i32, i32) { 42 | (0, 0) 43 | } 44 | 45 | fn open(&mut self, parent: *mut ::core::ffi::c_void) -> bool { 46 | if self.window_handle.is_some() { 47 | return false; 48 | } 49 | 50 | let window_handle = open_parented::, ParentWindow>( 51 | &ParentWindow(parent), 52 | get_iced_baseview_settings(self.sync_state.clone(), PLUGIN_SEMVER_NAME.to_string()), 53 | ); 54 | 55 | self.window_handle = Some(WindowHandleWrapper::new(window_handle)); 56 | 57 | true 58 | } 59 | 60 | fn close(&mut self) { 61 | if let Some(window_handle) = self.window_handle.take() { 62 | window_handle.close(); 63 | } 64 | } 65 | 66 | fn is_open(&mut self) -> bool { 67 | self.window_handle.is_some() 68 | } 69 | } 70 | 71 | struct WindowHandleWrapper(Arc>>); 72 | 73 | impl WindowHandleWrapper { 74 | fn new(window_handle: WindowHandle) -> Self { 75 | Self(Arc::new(Mutex::new(window_handle))) 76 | } 77 | 78 | fn close(&self) { 79 | self.0.lock().close_window(); 80 | } 81 | } 82 | 83 | // Partly dubious workaround for Send requirement on vst::Plugin and the (new) 84 | // baseview api contract requiring explicitly telling window to close. 85 | // 86 | // This is essentially a way of avoiding reimplementing vst2 support on top of 87 | // vst2-sys. It should be noted that WindowHandleWrapper.close() is only called 88 | // from a method that has mutable access to the editor object, e.g., Rust vst 89 | // API authors assume it will only be called by the correct thread. 90 | unsafe impl Send for WindowHandleWrapper {} 91 | 92 | pub struct ParentWindow(pub *mut ::core::ffi::c_void); 93 | 94 | unsafe impl HasRawWindowHandle for ParentWindow { 95 | #[cfg(target_os = "macos")] 96 | fn raw_window_handle(&self) -> RawWindowHandle { 97 | let mut handle = raw_window_handle::AppKitWindowHandle::empty(); 98 | 99 | handle.ns_view = self.0; 100 | 101 | RawWindowHandle::AppKit(handle) 102 | } 103 | 104 | #[cfg(target_os = "windows")] 105 | fn raw_window_handle(&self) -> RawWindowHandle { 106 | let mut handle = raw_window_handle::Win32WindowHandle::empty(); 107 | 108 | handle.hwnd = self.0; 109 | 110 | RawWindowHandle::Win32(handle) 111 | } 112 | 113 | #[cfg(target_os = "linux")] 114 | fn raw_window_handle(&self) -> RawWindowHandle { 115 | let mut handle = raw_window_handle::XcbWindowHandle::empty(); 116 | 117 | handle.window = self.0 as u32; 118 | 119 | RawWindowHandle::Xcb(handle) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /octasine/src/settings.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::utils::get_file_storage_dir; 6 | 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | pub struct Settings { 9 | pub schema_version: usize, 10 | #[cfg(feature = "gui")] 11 | pub gui: super::gui::GuiSettings, 12 | } 13 | 14 | impl Default for Settings { 15 | fn default() -> Self { 16 | Self { 17 | schema_version: 1, 18 | #[cfg(feature = "gui")] 19 | gui: Default::default(), 20 | } 21 | } 22 | } 23 | 24 | impl Settings { 25 | fn get_config_file_path() -> anyhow::Result { 26 | get_file_storage_dir().map(|path| path.join("OctaSine.json")) 27 | } 28 | 29 | pub fn save(&self) -> anyhow::Result<()> { 30 | let _ = ::std::fs::create_dir(get_file_storage_dir()?); // Ignore creation errors 31 | 32 | let file = ::std::fs::File::create(Self::get_config_file_path()?)?; 33 | 34 | ::serde_json::to_writer_pretty(file, self)?; 35 | 36 | Ok(()) 37 | } 38 | 39 | fn load() -> anyhow::Result { 40 | let file = ::std::fs::File::open(Self::get_config_file_path()?)?; 41 | 42 | let settings = ::serde_json::from_reader(file)?; 43 | 44 | Ok(settings) 45 | } 46 | 47 | pub fn load_or_default() -> Self { 48 | match Self::load() { 49 | Ok(settings) => settings, 50 | Err(err) => { 51 | ::log::warn!("Couldn't load settings: {}", err); 52 | 53 | Settings::default() 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /octasine/src/simd/fallback.rs: -------------------------------------------------------------------------------- 1 | use crate::math::wave::{saw, square, triangle}; 2 | 3 | use super::{Simd, SimdPackedDouble}; 4 | 5 | use std::ops::{Add, AddAssign, Mul, Sub}; 6 | 7 | macro_rules! apply_to_arrays { 8 | ($f:expr, $a:expr) => {{ 9 | let [a1, a2] = $a; 10 | 11 | [$f(a1), $f(a2)] 12 | }}; 13 | ($f:expr, $a:expr, $b:expr) => {{ 14 | let [a1, a2] = $a; 15 | let [b1, b2] = $b; 16 | 17 | [$f(a1, b1), $f(a2, b2)] 18 | }}; 19 | } 20 | 21 | pub struct Fallback; 22 | 23 | impl Simd for Fallback { 24 | type Pd = FallbackPackedDouble; 25 | } 26 | 27 | #[derive(Clone, Copy)] 28 | pub struct FallbackPackedDouble([f64; 2]); 29 | 30 | impl SimdPackedDouble for FallbackPackedDouble { 31 | const WIDTH: usize = 2; 32 | 33 | type Arr = [f64; Self::WIDTH]; 34 | 35 | #[inline(always)] 36 | unsafe fn new(value: f64) -> Self { 37 | Self([value, value]) 38 | } 39 | #[inline(always)] 40 | unsafe fn new_zeroed() -> Self { 41 | Self([0.0, 0.0]) 42 | } 43 | #[inline(always)] 44 | unsafe fn new_from_pair(l: f64, r: f64) -> Self { 45 | Self([l, r]) 46 | } 47 | #[inline(always)] 48 | unsafe fn from_arr(arr: Self::Arr) -> Self { 49 | Self(arr) 50 | } 51 | #[inline(always)] 52 | unsafe fn to_arr(self) -> Self::Arr { 53 | self.0 54 | } 55 | #[inline(always)] 56 | unsafe fn min(self, other: Self) -> Self { 57 | Self(apply_to_arrays!(f64::min, self.0, other.0)) 58 | } 59 | #[inline(always)] 60 | unsafe fn max(self, other: Self) -> Self { 61 | Self(apply_to_arrays!(f64::max, self.0, other.0)) 62 | } 63 | #[inline(always)] 64 | unsafe fn pairwise_horizontal_sum(self) -> Self { 65 | let [l, r] = self.0; 66 | 67 | Self([l + r, l + r]) 68 | } 69 | #[inline(always)] 70 | unsafe fn interleave(self, other: Self) -> Self { 71 | Self([self.0[0], other.0[1]]) 72 | } 73 | #[inline(always)] 74 | unsafe fn any_over_zero(self) -> bool { 75 | (self.0[0] > 0.0) | (self.0[1] > 0.0) 76 | } 77 | #[inline(always)] 78 | unsafe fn floor(self) -> Self { 79 | Self(apply_to_arrays!(f64::floor, self.0)) 80 | } 81 | #[inline(always)] 82 | unsafe fn abs(self) -> Self { 83 | Self(apply_to_arrays!(f64::abs, self.0)) 84 | } 85 | #[inline(always)] 86 | unsafe fn fast_sin(self) -> Self { 87 | Self(apply_to_arrays!(sleef_trig::Sleef_sind1_u35purec, self.0)) 88 | } 89 | #[inline(always)] 90 | unsafe fn triangle(self) -> Self { 91 | Self(apply_to_arrays!(triangle, self.0)) 92 | } 93 | #[inline(always)] 94 | unsafe fn square(self) -> Self { 95 | Self(apply_to_arrays!(square, self.0)) 96 | } 97 | #[inline(always)] 98 | unsafe fn saw(self) -> Self { 99 | Self(apply_to_arrays!(saw, self.0)) 100 | } 101 | } 102 | 103 | impl Add for FallbackPackedDouble { 104 | type Output = Self; 105 | 106 | #[inline(always)] 107 | fn add(self, rhs: Self) -> Self::Output { 108 | Self(apply_to_arrays!(Add::add, self.0, rhs.0)) 109 | } 110 | } 111 | 112 | impl AddAssign for FallbackPackedDouble 113 | where 114 | FallbackPackedDouble: Copy, 115 | { 116 | #[inline(always)] 117 | fn add_assign(&mut self, rhs: Self) { 118 | *self = *self + rhs; 119 | } 120 | } 121 | 122 | impl Sub for FallbackPackedDouble { 123 | type Output = Self; 124 | 125 | #[inline(always)] 126 | fn sub(self, rhs: Self) -> Self::Output { 127 | Self(apply_to_arrays!(Sub::sub, self.0, rhs.0)) 128 | } 129 | } 130 | 131 | impl Mul for FallbackPackedDouble { 132 | type Output = Self; 133 | 134 | #[inline(always)] 135 | fn mul(self, rhs: Self) -> Self::Output { 136 | Self(apply_to_arrays!(Mul::mul, self.0, rhs.0)) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /octasine/src/simd/mod.rs: -------------------------------------------------------------------------------- 1 | //! SIMD abstraction 2 | 3 | use std::ops::{Add, AddAssign, Index, Mul, Sub}; 4 | 5 | #[cfg(target_arch = "x86_64")] 6 | pub mod avx; 7 | pub mod fallback; 8 | #[cfg(target_arch = "x86_64")] 9 | pub mod sse2; 10 | 11 | #[cfg(target_arch = "x86_64")] 12 | pub use avx::*; 13 | pub use fallback::*; 14 | #[cfg(target_arch = "x86_64")] 15 | pub use sse2::*; 16 | 17 | pub trait Simd { 18 | type Pd: SimdPackedDouble; 19 | } 20 | 21 | pub trait SimdPackedDouble: Copy + Add + AddAssign + Sub + Mul { 22 | // Number of doubles that this packed double fits 23 | const WIDTH: usize; 24 | /// Number of stereo audio samples that this packed double fits 25 | const SAMPLES: usize = Self::WIDTH / 2; 26 | 27 | /// f64 array with same number of members as this packed double 28 | type Arr: Index; 29 | 30 | unsafe fn new(value: f64) -> Self; 31 | unsafe fn new_zeroed() -> Self; 32 | unsafe fn new_from_pair(l: f64, r: f64) -> Self; 33 | unsafe fn from_arr(arr: Self::Arr) -> Self; 34 | unsafe fn to_arr(self) -> Self::Arr; 35 | unsafe fn min(self, other: Self) -> Self; 36 | unsafe fn max(self, other: Self) -> Self; 37 | unsafe fn pairwise_horizontal_sum(self) -> Self; 38 | unsafe fn interleave(self, other: Self) -> Self; 39 | unsafe fn any_over_zero(self) -> bool; 40 | unsafe fn floor(self) -> Self; 41 | unsafe fn abs(self) -> Self; 42 | unsafe fn fast_sin(self) -> Self; 43 | unsafe fn triangle(self) -> Self; 44 | unsafe fn square(self) -> Self; 45 | unsafe fn saw(self) -> Self; 46 | } 47 | 48 | #[cfg(test)] 49 | mod tests { 50 | macro_rules! wave_test { 51 | ($name:ident, $wave_fn:ident) => { 52 | #[cfg(target_arch = "x86_64")] 53 | #[test] 54 | fn $name() { 55 | use quickcheck::{quickcheck, TestResult}; 56 | 57 | use crate::simd::SimdPackedDouble; 58 | 59 | assert!(is_x86_feature_detected!("avx")); 60 | 61 | fn prop(x: f64) -> TestResult { 62 | if x.is_infinite() || x.is_nan() { 63 | return TestResult::discard(); 64 | } 65 | 66 | let fallback = 67 | unsafe { super::FallbackPackedDouble::new(x).$wave_fn().to_arr() }; 68 | let sse2 = unsafe { super::Sse2PackedDouble::new(x).$wave_fn().to_arr() }; 69 | let avx = unsafe { super::AvxPackedDouble::new(x).$wave_fn().to_arr() }; 70 | 71 | let mut all = fallback.to_vec(); 72 | 73 | all.extend_from_slice(&sse2[..]); 74 | all.extend_from_slice(&avx[..]); 75 | 76 | let first = *all.get(0).unwrap(); 77 | 78 | for y in all.into_iter() { 79 | if y != first { 80 | dbg!(x, fallback, sse2, avx); 81 | 82 | return TestResult::failed(); 83 | } 84 | } 85 | 86 | TestResult::passed() 87 | } 88 | 89 | quickcheck(prop as fn(f64) -> TestResult); 90 | } 91 | }; 92 | } 93 | 94 | wave_test!(test_triangle, triangle); 95 | wave_test!(test_square, square); 96 | wave_test!(test_saw, saw); 97 | } 98 | -------------------------------------------------------------------------------- /octasine/src/sync/atomic_float.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicU32, Ordering}; 2 | 3 | #[derive(Debug)] 4 | pub struct AtomicFloat(AtomicU32); 5 | 6 | impl AtomicFloat { 7 | pub fn new(value: f32) -> Self { 8 | Self(AtomicU32::new(value.to_bits())) 9 | } 10 | 11 | #[inline] 12 | pub fn get(&self) -> f32 { 13 | f32::from_bits(self.0.load(Ordering::Relaxed)) 14 | } 15 | 16 | #[inline] 17 | pub fn set(&self, value: f32) { 18 | self.0.store(value.to_bits(), Ordering::Relaxed); 19 | } 20 | } 21 | 22 | #[cfg(test)] 23 | mod tests { 24 | use super::*; 25 | 26 | #[allow(clippy::float_cmp)] 27 | #[test] 28 | fn test_atomic_double() { 29 | let a = 13.5; 30 | 31 | let atomic_float = AtomicFloat::new(a); 32 | 33 | assert_eq!(atomic_float.get(), a); 34 | 35 | for i in 0..100 { 36 | let b = 23_896.35 - i as f32; 37 | 38 | atomic_float.set(b); 39 | 40 | assert_eq!(atomic_float.get(), b); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /octasine/src/sync/mod.rs: -------------------------------------------------------------------------------- 1 | mod atomic_float; 2 | pub mod change_info; 3 | mod parameters; 4 | mod patch_bank; 5 | mod serde; 6 | 7 | use std::path::PathBuf; 8 | 9 | use compact_str::CompactString; 10 | pub use patch_bank::PatchBank; 11 | 12 | /// Thread-safe state used for parameter and preset calls 13 | pub struct SyncState { 14 | /// Host should always be set when running as real plugin, but having the 15 | /// option of leaving this field empty is useful when benchmarking. 16 | pub host: Option, 17 | pub patches: PatchBank, 18 | } 19 | 20 | impl SyncState { 21 | pub fn new(host: Option) -> Self { 22 | Self { 23 | host, 24 | patches: built_in_patch_bank(), 25 | } 26 | } 27 | } 28 | 29 | cfg_if::cfg_if! { 30 | if #[cfg(feature = "gui")] { 31 | use crate::parameters::WrappedParameter; 32 | use self::change_info::MAX_NUM_PARAMETERS; 33 | 34 | /// Trait passed to GUI code for encapsulation 35 | pub trait GuiSyncHandle: Clone + Send + Sync + 'static { 36 | fn begin_edit(&self, parameter: WrappedParameter); 37 | fn end_edit(&self, parameter: WrappedParameter); 38 | fn set_parameter(&self, parameter: WrappedParameter, value: f32); 39 | /// Set parameter immediately. Wrap in begin and end edit commands if necessary 40 | fn set_parameter_immediate(&self, parameter: WrappedParameter, value: f32); 41 | fn parse_parameter_from_text(&self, parameter: WrappedParameter, text: &str) -> Option; 42 | fn get_parameter_text_choices(&self, parameter: WrappedParameter) -> Option>; 43 | /// Set parameter without telling host 44 | fn set_parameter_audio_only(&self, parameter: WrappedParameter, value: f32); 45 | fn get_parameter(&self, parameter: WrappedParameter) -> f32; 46 | fn format_parameter_value(&self, parameter: WrappedParameter, value: f32) -> CompactString; 47 | fn get_patches(&self) -> (usize, Vec); 48 | fn set_patch_index(&self, index: usize); 49 | fn get_current_patch_name(&self) -> CompactString; 50 | fn set_current_patch_name(&self, name: &str); 51 | fn get_changed_parameters(&self) -> Option<[Option; MAX_NUM_PARAMETERS]>; 52 | fn have_patches_changed(&self) -> bool; 53 | fn get_gui_settings(&self) -> crate::gui::GuiSettings; 54 | fn export_patch(&self) -> (CompactString, Vec); 55 | fn export_bank(&self) -> Vec; 56 | fn import_bank_or_patches_from_paths(&self, paths: &[PathBuf]); 57 | fn clear_patch(&self); 58 | fn clear_bank(&self); 59 | } 60 | } 61 | } 62 | 63 | fn built_in_patch_bank() -> PatchBank { 64 | PatchBank::default() 65 | } 66 | -------------------------------------------------------------------------------- /octasine/src/sync/serde/common.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{BigEndian, WriteBytesExt}; 2 | 3 | use crate::{ 4 | crate_version, 5 | plugin::common::{crate_version_to_vst2_format, PLUGIN_UNIQUE_VST2_ID}, 6 | }; 7 | 8 | pub fn make_fxp( 9 | patch_bytes: &[u8], 10 | patch_name: &str, 11 | num_parameters: usize, 12 | ) -> anyhow::Result> { 13 | let mut bytes = Vec::new(); 14 | 15 | bytes.extend_from_slice(b"CcnK"); // fxp/fxp identifier 16 | bytes.write_i32::((5 * 4 + 28 + 4 + patch_bytes.len()).try_into()?)?; 17 | 18 | bytes.extend_from_slice(b"FPCh"); // fxp opaque chunk 19 | bytes.write_i32::(1)?; // fxp version 20 | bytes.write_i32::(PLUGIN_UNIQUE_VST2_ID)?; 21 | bytes.write_i32::(crate_version_to_vst2_format(crate_version!()))?; 22 | 23 | bytes.write_i32::(num_parameters.try_into()?)?; 24 | 25 | let name_buf = { 26 | let mut buf = [0u8; 28]; 27 | 28 | // Iterate through all buffer items except last, where a null 29 | // terminator must be left in place. If there are less than 27 30 | // chars, the last one will automatically be followed by a null 31 | // byte. 32 | for (b, c) in buf[..27].iter_mut().zip( 33 | patch_name 34 | .chars() 35 | .filter_map(|c| c.is_ascii().then_some(c as u8)), 36 | ) { 37 | *b = c; 38 | } 39 | 40 | buf 41 | }; 42 | 43 | bytes.extend_from_slice(&name_buf); 44 | 45 | bytes.write_i32::(patch_bytes.len().try_into()?)?; 46 | bytes.extend_from_slice(patch_bytes); 47 | 48 | Ok(bytes) 49 | } 50 | 51 | pub fn make_fxb(bank_bytes: &[u8], num_patches: usize) -> anyhow::Result> { 52 | let mut bytes = Vec::new(); 53 | 54 | bytes.extend_from_slice(b"CcnK"); // fxp/fxp identifier 55 | bytes.write_i32::((5 * 4 + 128 + 4 + bank_bytes.len()).try_into()?)?; 56 | 57 | bytes.extend_from_slice(b"FBCh"); // fxb opaque chunk 58 | bytes.write_i32::(1)?; // fxb version (1 or 2) 59 | bytes.write_i32::(PLUGIN_UNIQUE_VST2_ID)?; 60 | bytes.write_i32::(crate_version_to_vst2_format(crate_version!()))?; 61 | 62 | bytes.write_i32::(num_patches.try_into()?)?; 63 | bytes.extend(::std::iter::repeat(0).take(128)); // reserved padding for fxb version 1 64 | 65 | bytes.write_i32::(bank_bytes.len().try_into()?)?; 66 | bytes.extend_from_slice(bank_bytes); 67 | 68 | Ok(bytes) 69 | } 70 | -------------------------------------------------------------------------------- /octasine/src/sync/serde/mod.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | mod v1; 3 | mod v2; 4 | 5 | use std::io::Write; 6 | 7 | use super::patch_bank::{Patch, PatchBank}; 8 | 9 | /// Remember to update relevant metadata if changes were indeed made 10 | pub fn update_bank_from_bytes(bank: &PatchBank, bytes: &[u8]) -> anyhow::Result> { 11 | let serde_bank = if v2::bytes_are_v2(bytes) { 12 | v2::SerdePatchBank::from_bytes(bytes)? 13 | } else { 14 | v2::SerdePatchBank::from_v1(v1::SerdePatchBank::from_bytes(bytes)?)? 15 | }; 16 | 17 | let default_serde_patch = v2::SerdePatch::new(&Patch::default()); 18 | 19 | for (index, patch) in bank.patches.iter().enumerate() { 20 | let serde_patch = if let Some(serde_patch) = serde_bank.patches.get(index) { 21 | patch.set_name(serde_patch.name.as_str()); 22 | 23 | serde_patch 24 | } else { 25 | patch.set_name(""); 26 | 27 | &default_serde_patch 28 | }; 29 | 30 | for (key, parameter) in patch.parameters.iter() { 31 | if let Some(serde_parameter) = serde_patch.parameters.get(key) { 32 | parameter.set_value(serde_parameter.value_patch); 33 | } 34 | } 35 | } 36 | 37 | Ok(serde_bank.selected_patch_index) 38 | } 39 | 40 | /// Remember to update relevant metadata if changes were indeed made 41 | pub fn update_patch_from_bytes(patch: &Patch, bytes: &[u8]) -> anyhow::Result<()> { 42 | let serde_patch = if v2::bytes_are_v2(bytes) { 43 | v2::SerdePatch::from_bytes(bytes)? 44 | } else { 45 | v2::SerdePatch::from_v1(v1::SerdePatch::from_bytes(bytes)?)? 46 | }; 47 | 48 | patch.set_name(serde_patch.name.as_str()); 49 | 50 | for (key, parameter) in patch.parameters.iter() { 51 | if let Some(serde_parameter) = serde_patch.parameters.get(key) { 52 | parameter.set_value(serde_parameter.value_patch); 53 | } 54 | } 55 | 56 | Ok(()) 57 | } 58 | 59 | pub fn serialize_bank_plain_bytes( 60 | writer: &mut W, 61 | bank: &PatchBank, 62 | ) -> anyhow::Result<()> { 63 | v2::SerdePatchBank::new(bank).serialize_plain(writer) 64 | } 65 | 66 | pub fn serialize_bank_fxb_bytes(bank: &PatchBank) -> anyhow::Result> { 67 | v2::SerdePatchBank::new(bank).serialize_fxb_bytes() 68 | } 69 | 70 | pub fn serialize_patch_fxp_bytes(patch: &Patch) -> anyhow::Result> { 71 | v2::SerdePatch::new(patch).serialize_fxp_bytes() 72 | } 73 | -------------------------------------------------------------------------------- /octasine/src/sync/serde/v1.rs: -------------------------------------------------------------------------------- 1 | use flate2::read::GzDecoder; 2 | use semver::Version; 3 | use serde::{de::DeserializeOwned, Deserialize, Serialize}; 4 | 5 | const PREFIX: &[u8] = b"\n\nOCTASINE-GZ-DATA-V1-BEGIN\n\n"; 6 | const SUFFIX: &[u8] = b"\n\nOCTASINE-GZ-DATA-V1-END\n\n"; 7 | 8 | #[derive(Serialize, Debug)] 9 | pub struct SerdePatchParameterValue(String); 10 | 11 | impl SerdePatchParameterValue { 12 | pub fn as_f32(&self) -> f32 { 13 | self.0 14 | .parse() 15 | .expect("deserialize SerdePresetParameterValue") 16 | } 17 | 18 | fn deserialize<'de, D>(deserializer: D) -> Result 19 | where 20 | D: ::serde::de::Deserializer<'de>, 21 | { 22 | struct V; 23 | 24 | impl<'de> ::serde::de::Visitor<'de> for V { 25 | type Value = SerdePatchParameterValue; 26 | 27 | fn expecting(&self, formatter: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { 28 | formatter.write_str("f32 or string") 29 | } 30 | 31 | fn visit_str(self, value: &str) -> Result 32 | where 33 | E: ::serde::de::Error, 34 | { 35 | Ok(SerdePatchParameterValue(value.to_owned())) 36 | } 37 | } 38 | 39 | deserializer.deserialize_any(V) 40 | } 41 | 42 | fn serialize(&self, serializer: S) -> Result 43 | where 44 | S: ::serde::ser::Serializer, 45 | { 46 | serializer.serialize_str(&self.0) 47 | } 48 | } 49 | 50 | #[derive(Serialize, Deserialize, Debug)] 51 | pub struct SerdePatchParameter { 52 | pub name: String, 53 | #[serde( 54 | deserialize_with = "SerdePatchParameterValue::deserialize", 55 | serialize_with = "SerdePatchParameterValue::serialize" 56 | )] 57 | pub value_float: SerdePatchParameterValue, 58 | pub value_text: String, 59 | } 60 | 61 | #[derive(Serialize, Deserialize, Debug)] 62 | pub struct SerdePatch { 63 | pub octasine_version: String, 64 | pub name: String, 65 | pub parameters: Vec, 66 | } 67 | 68 | impl SerdePatch { 69 | pub fn from_bytes(bytes: &[u8]) -> anyhow::Result { 70 | Ok(generic_from_bytes(bytes)?) 71 | } 72 | } 73 | 74 | #[derive(Serialize, Deserialize)] 75 | pub struct SerdePatchBank { 76 | pub octasine_version: String, 77 | pub patches: Vec, 78 | } 79 | 80 | impl SerdePatchBank { 81 | pub fn from_bytes(bytes: &[u8]) -> anyhow::Result { 82 | Ok(generic_from_bytes(bytes)?) 83 | } 84 | } 85 | 86 | pub fn parse_version(v1_version: &str) -> anyhow::Result { 87 | let mut chars = v1_version.chars(); 88 | 89 | // Drop initial "v" 90 | chars.next(); 91 | 92 | Ok(Version::parse(&chars.take(5).collect::())?) 93 | } 94 | 95 | fn generic_from_bytes( 96 | mut bytes: &[u8], 97 | ) -> Result { 98 | bytes = split_off_slice_prefix(bytes, PREFIX); 99 | bytes = split_off_slice_suffix(bytes, SUFFIX); 100 | 101 | let mut decoder = GzDecoder::new(bytes); 102 | 103 | serde_json::from_reader(&mut decoder) 104 | } 105 | 106 | fn split_off_slice_suffix<'a>(mut bytes: &'a [u8], suffix: &[u8]) -> &'a [u8] { 107 | if let Some(index) = find_in_slice(bytes, suffix) { 108 | bytes = &bytes[..index]; 109 | } 110 | 111 | bytes 112 | } 113 | 114 | fn split_off_slice_prefix<'a>(mut bytes: &'a [u8], prefix: &[u8]) -> &'a [u8] { 115 | if let Some(index) = find_in_slice(bytes, prefix) { 116 | bytes = &bytes[index + prefix.len()..]; 117 | } 118 | 119 | bytes 120 | } 121 | 122 | fn find_in_slice(haystack: &[u8], needle: &[u8]) -> Option { 123 | if needle.is_empty() { 124 | return None; 125 | } 126 | 127 | for (i, window) in haystack.windows(needle.len()).enumerate() { 128 | if window == needle { 129 | return Some(i); 130 | } 131 | } 132 | 133 | None 134 | } 135 | 136 | #[cfg(test)] 137 | mod tests { 138 | use super::*; 139 | 140 | #[test] 141 | fn test_split_off_slice_prefix() { 142 | assert_eq!(split_off_slice_prefix(b"abcdef", b"abc"), b"def"); 143 | assert_eq!(split_off_slice_prefix(b"abcdef", b"bcd"), b"ef"); 144 | assert_eq!(split_off_slice_prefix(b"abcdef", b"def"), b""); 145 | assert_eq!(split_off_slice_prefix(b"abcdef", b"abcdef"), b""); 146 | assert_eq!(split_off_slice_prefix(b"abcdef", b"abcdefg"), b"abcdef"); 147 | assert_eq!(split_off_slice_prefix(b"abcdef", b"z"), b"abcdef"); 148 | assert_eq!(split_off_slice_prefix(b"abcdef", b"zzzzzz"), b"abcdef"); 149 | assert_eq!(split_off_slice_prefix(b"abcdef", b"zzzzzzz"), b"abcdef"); 150 | assert_eq!(split_off_slice_prefix(b"abcdef", b""), b"abcdef"); 151 | assert_eq!(split_off_slice_prefix(b"", b""), b""); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /octasine/src/sync/serde/v2/compat.rs: -------------------------------------------------------------------------------- 1 | use semver::Version; 2 | 3 | use crate::parameters::{OperatorParameter, Parameter, SerializableRepresentation}; 4 | 5 | use super::SerdePatch; 6 | 7 | pub const COMPATIBILITY_CHANGES: &[(Version, fn(&mut SerdePatch))] = 8 | &[(Version::new(0, 8, 5), compat_0_8_5)]; 9 | 10 | /// New operator wave forms 11 | /// 12 | /// Prior versions only had sine and white noise variants 13 | #[allow(dead_code)] 14 | pub fn compat_0_8_5(patch: &mut SerdePatch) { 15 | let parameter_keys = [ 16 | Parameter::Operator(0, OperatorParameter::WaveType).key(), 17 | Parameter::Operator(1, OperatorParameter::WaveType).key(), 18 | Parameter::Operator(2, OperatorParameter::WaveType).key(), 19 | Parameter::Operator(3, OperatorParameter::WaveType).key(), 20 | ]; 21 | 22 | for key in parameter_keys { 23 | let p = patch.parameters.get_mut(&key).unwrap(); 24 | 25 | match &p.value_serializable { 26 | SerializableRepresentation::Other(s) => { 27 | // These values will in most (but not all) cases already be set 28 | match s.as_str() { 29 | "SINE" => { 30 | p.value_patch = 0.0; 31 | } 32 | "NOISE" => { 33 | p.value_patch = 1.0; 34 | } 35 | v => { 36 | ::log::error!( 37 | "converting patch for 0.8.5 compatibility: unrecognized operator wave type: {}", 38 | v 39 | ); 40 | } 41 | } 42 | } 43 | SerializableRepresentation::Float(v) => { 44 | ::log::error!( 45 | "converting patch for 0.8.5 compatibility: incorrect serializable representation for operator wave type: {}", 46 | v 47 | ); 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /octasine/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use crate::{audio::AudioState, parameters::Parameter, sync::SyncState}; 4 | 5 | #[macro_export] 6 | macro_rules! crate_version { 7 | () => { 8 | env!("CARGO_PKG_VERSION") 9 | }; 10 | } 11 | 12 | pub fn update_audio_parameters(audio: &mut AudioState, sync: &SyncState) { 13 | if let Some(indeces) = sync.patches.get_changed_parameters_from_audio() { 14 | for (index, opt_new_value) in indeces.iter().enumerate() { 15 | if let Some(new_value) = opt_new_value { 16 | if let Some(parameter) = Parameter::from_index(index) { 17 | audio.set_parameter_from_patch(parameter, *new_value); 18 | } 19 | } 20 | } 21 | } 22 | } 23 | 24 | pub fn init_logging(plugin_type: &str) -> anyhow::Result<()> { 25 | let log_folder: PathBuf = get_file_storage_dir()?; 26 | 27 | // Ignore any creation error 28 | let _ = ::std::fs::create_dir(log_folder.clone()); 29 | 30 | let log_file = ::std::fs::File::create(log_folder.join("OctaSine.log"))?; 31 | 32 | let log_config = match simplelog::ConfigBuilder::new().set_time_offset_to_local() { 33 | Ok(builder) => builder.build(), 34 | Err(builder) => builder.build(), 35 | }; 36 | 37 | simplelog::WriteLogger::init(simplelog::LevelFilter::Info, log_config, log_file)?; 38 | 39 | log_panics::init(); 40 | 41 | ::log::info!("init"); 42 | 43 | ::log::info!("OS: {}", ::os_info::get()); 44 | ::log::info!("OctaSine build: {} ({})", get_version_info(), plugin_type); 45 | 46 | ::log::set_max_level(simplelog::LevelFilter::Error); 47 | 48 | Ok(()) 49 | } 50 | 51 | pub fn get_version_info() -> String { 52 | use git_testament::{git_testament, CommitKind}; 53 | 54 | let mut info = format!("v{}", env!("CARGO_PKG_VERSION")); 55 | 56 | git_testament!(GIT_TESTAMENT); 57 | 58 | match GIT_TESTAMENT.commit { 59 | CommitKind::NoTags(commit, _) | CommitKind::FromTag(_, commit, _, _) => { 60 | let commit = commit.chars().take(7).collect::(); 61 | 62 | info.push_str(&format!(" ({})", commit)); 63 | } 64 | _ => (), 65 | }; 66 | 67 | if !GIT_TESTAMENT.modifications.is_empty() { 68 | info.push_str(" (M)"); 69 | } 70 | 71 | #[cfg(feature = "wgpu")] 72 | info.push_str(" (wgpu)"); 73 | 74 | #[cfg(feature = "glow")] 75 | info.push_str(" (gl)"); 76 | 77 | info 78 | } 79 | 80 | cfg_if::cfg_if! { 81 | if #[cfg(target_os = "windows")] { 82 | pub fn get_file_storage_dir() -> anyhow::Result { 83 | ::directories::UserDirs::new() 84 | .and_then(|d| d.document_dir().map(|d| d.join("OctaSine"))) 85 | .ok_or(anyhow::anyhow!("Couldn't extract file storage dir")) 86 | } 87 | } else { 88 | pub fn get_file_storage_dir() -> anyhow::Result { 89 | ::directories::ProjectDirs::from("com", "OctaSine", "OctaSine") 90 | .map(|d| d.config_dir().to_owned()) 91 | .ok_or(anyhow::anyhow!("Couldn't extract file storage dir")) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /scripts/about.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir -p tmp 4 | 5 | cargo-about generate -o tmp/licenses.html about.hbs -m "octasine/Cargo.toml" 6 | -------------------------------------------------------------------------------- /scripts/bench.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cargo run --profile "release-debug" -p octasine-cli --no-default-features -- bench-process -------------------------------------------------------------------------------- /scripts/clippy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cargo +nightly clippy --workspace --all-targets --features "vst2 clap" 4 | -------------------------------------------------------------------------------- /scripts/criterion.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cargo bench -p octasine --no-default-features -------------------------------------------------------------------------------- /scripts/macos/build-clap-and-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Validate with: https://github.com/free-audio/clap-validator 4 | 5 | set -e 6 | 7 | cargo xtask bundle -p octasine --profile "release-debug" --features "clap" 8 | 9 | TARGET="/Library/Audio/Plug-Ins/CLAP/OctaSine.clap" 10 | 11 | if [ -d "$TARGET" ]; then 12 | rm -r "$TARGET" 13 | fi 14 | 15 | cp -r "./target/bundled/octasine.clap" "$TARGET" 16 | echo "Copied CLAP bundle to $TARGET" 17 | -------------------------------------------------------------------------------- /scripts/macos/build-vst2-and-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Validate with: https://github.com/free-audio/clap-validator 4 | 5 | set -e 6 | 7 | cargo xtask bundle -p octasine --profile "release-debug" --features "vst2" 8 | 9 | TARGET="/Library/Audio/Plug-Ins/VST/OctaSine.vst" 10 | 11 | if [ -d "$TARGET" ]; then 12 | rm -r "$TARGET" 13 | fi 14 | 15 | cp -r "./target/bundled/octasine.vst" "$TARGET" 16 | echo "Copied VST bundle to $TARGET" 17 | -------------------------------------------------------------------------------- /scripts/miri-audio-gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Currently doesn't build 4 | cargo +nightly miri run --profile "release-debug" -p octasine-cli --no-default-features -- bench-process -------------------------------------------------------------------------------- /scripts/plot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cargo run -p octasine-cli --no-default-features --features plot -- plot -------------------------------------------------------------------------------- /scripts/run-gui.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [[ -z $1 ]]; then 4 | echo "Usage: $0 [glow|wgpu]" 5 | else 6 | cargo run -p octasine-cli --no-default-features --features $1 -- run-gui 7 | fi 8 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | RUSTFLAGS="-C target-cpu=native" cargo test --features "vst2 clap" -------------------------------------------------------------------------------- /xtask/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xtask" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "ISC" 6 | 7 | [dependencies] 8 | nih_plug_xtask = { git = "https://github.com/robbert-vdh/nih-plug.git", rev = "5ae23f5" } 9 | -------------------------------------------------------------------------------- /xtask/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() -> nih_plug_xtask::Result<()> { 2 | nih_plug_xtask::main() 3 | } 4 | --------------------------------------------------------------------------------