diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..acaf251 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.settings/ +.vscode/ +target/ +.classpath +.project +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +# https://github.com/takari/maven-wrapper#usage-without-binary-jar +.mvn/wrapper/maven-wrapper.jar diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0a04128 --- /dev/null +++ b/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/README.md b/README.md new file mode 100644 index 0000000..627495b --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# Twister Sister Bitwig Controller Extension + +[![Download](https://img.shields.io/github/downloads/dozius/TwisterSister/total.svg)](https://github.com/dozius/TwisterSister/releases/latest) +[![Donate](https://img.shields.io/badge/donate-paypal-blue.svg)](https://www.paypal.me/cisc) + +This is the source code repository for the Twister Sister Bitwig controller extension. + +Precompiled releases can be found [here](https://github.com/dozius/TwisterSister/releases). + +User documentation, including installation instructions, can be found [here](docs/README.md). + +## Compiling + +### Requirements + +- [OpenJDK 12.x](https://adoptopenjdk.net/releases.html?variant=openjdk12) +- [Maven >= 3.1.0](https://maven.apache.org/) + +### Build and install + +1. Follow the installation instructions for each of the above requirements. +2. Run `mvn install`. + +### Debugging + +1. Set an environment variable `BITWIG_DEBUG_PORT` to an unused port number. +2. Restart Bitwig. +3. Setup your debugger to connect to the port from step 1. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..86046c2 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,202 @@ +# Twister Sister + +A Bitwig Studio controller extension for the +[DJ TechTools MIDI Fighter Twister](https://www.midifighter.com/#Twister). + +## Table of Contents + +- [Installation](#installation) +- [Hardware](#hardware) + - [Track & Device Bank (Bank 1)](#track--device-bank-bank-1) + - [Notes](#notes) + - [User Mappable Bank (Bank 2-4)](#user-mappable-bank-bank-2-4) + - [Side Buttons](#side-buttons) +- [Options](#options) + - [Show Bank Popup](#show-bank-popup) + - [Device Row Color](#device-row-color) + - [Global Fine Sensitivity](#global-fine-sensitivity) +- [Project Settings](#project-settings) + - [User Mappable Knob Colors](#user-mappable-knob-colors) +- [Specific Device Settings](#specific-device-settings) + - [Devices](#devices) + - [Controls](#controls) + - [Finding IDs](#finding-ids) + +## Installation + +1. Extract the zip file. + +2. Load the Twister Sister settings into your MIDI Fighter Twister. + + 1. Open the MIDI Fighter Utility and ensure your device is connected. + 2. Click `File -> Import Settings...` and select the file `TwisterSister.mfs`. + 3. Click `SEND TO MIDI FIGHTER`. + +3. Copy `TwisterSister.bwextension` and `SpecificDeviceSettings.toml` to the following location: + + - Windows: `%USERPROFILE%/Documents/Bitwig Studio/Extensions` + - Mac: `~/Documents/Bitwig Studio/Extensions` + - Linux: `~/Bitwig Studio/Extensions` + +4. Add your controller in Bitwig Studio. + + 1. Open Bitwig Studio and navigate to `Settings -> Controller` in the dashboard. + + 2. If auto-detection works then the device will automatically be added and you can skip the + remaining steps. + + 3. If auto-detection has failed then click on `+ Add Controller` then select `DJ TechTools` from + the hardware vendor drop down, `Twister Sister` in the product list and click `Add`. + ![add-device](images/add-device.png) + + 4. If your controller is not automatically added to the MIDI in and out ports then select them + manually. Once both ports have a device assigned the extension will activate. + ![midi-io](images/midi-io.png) + +## Hardware + +![hardware](images/hardware.png) + +### Track & Device Bank (Bank 1) + +| Knob | Twist | Click | Double Click | Long Press | RGB Light | +| ---- | --------------------------- | ------------------------- | --------------- | ------------------------- | -------------------- | +| 1 | Select track | Toggle mute | - | Toggle arm | Follow track color | +| 2 | Track volume | Toggle fine sensitivity | Reset volume | Toggle Solo | Follow track color | +| 3 | Track pan | Toggle sensitivity | Reset pan | - | Follow track color | +| 4 | Current send volume | Cycle through track sends | - | - | Follow send color | +| 5 | Select device | Toggle enable | - | Toggle expand | Device row color | +| 6 | Select remote controls page | Show/hide device UI | - | Show/Hide remote controls | Device row color | +| 7 | Specific device parameter 1 | Toggle fine sensitivity | Reset parameter | Insert device before | Device row color | +| 8 | Specific device parameter 2 | Toggle fine sensitivity | Reset parameter | Insert device after | Device row color | +| 9-16 | Remote control parameter | Toggle fine sensitivity | Reset parameter | - | Remote control color | + +#### Notes + +- The color palette on the Twister is very limited. Colors are matched as closely as possible. + +- If a device or parameter does not exist in the current context then the corresponding lights will + be off. + +- See the section on [specific device settings](#Specific-Device-Settings) for information on + configuring knobs 7 and 8. + +### User Mappable Bank (Bank 2-4) + +| Knob | Twist | Hold & Twist | Double Click | RGB Light | +| ---- | ------------------- | --------------- | ----------------- | --------------- | +| 1-16 | Mapped parameter(s) | Set light color | Reset light color | User selectable | + +### Side Buttons + +These are the settings provided by `TwisterSister.mfs`. The extension does not use the side buttons +in any way, so these can be changed to whatever you prefer in the MIDI Fighter Utility. + +| Button | Function | +| ------------ | ------------- | +| Left Side 1 | CC Hold | +| Left Side 2 | CC Hold | +| Left Side 3 | CC Hold | +| Right Side 1 | Previous Bank | +| Right Side 2 | Next Bank | +| Right Side 3 | Bank 1 | + +## Options + +These are global options that apply to the specific device across all projects. They are accessed in +the controller settings in the Bitwig Studio dashboard. + +![options](images/options.png) + +### Show Bank Popup + +Enable/disable showing a popup notification when banks are changed. + +### Device Row Color + +Sets the color used for knobs 5-8 in the [track & device bank](#Track-&-Device-Bank-(Bank-1)). This +uses the same color values as the [user mappable knobs](#User-Mappable-Knob-Colors). + +### Global Fine Sensitivity + +Sets the fine sensitivity factor for all encoder controls. The lower the number, the finer the +movement. + +## Project Settings + +These are per project settings and will be saved with each project. They are accessed in the I/O +panel of Bitwig Studio. + +![project-settings](images/project-settings.png) + +### User Mappable Knob Colors + +Show or hide the settings for each bank using the bank selector. Each knob can have it's color set +to one of the 125¹ colors supported by the Twister. A value of 0 will turn the light off. + +These settings can also be changed by holding and twisting any of the user mappable knobs. + +_¹ This should be 126 colors but there is a +[bug](https://github.com/DJ-TechTools/Midi_Fighter_Twister_Open_Source/issues/9) in the latest +software._ + +## Specific Device Settings + +The specific device feature allows you to assign two device specific parameters to knobs 7 and 8 in +the [track & device bank](#Track-&-Device-Bank-(Bank-1)). These follow the selected device. + +The settings file `SpecificDeviceSettings.toml` allows you to configure which parameters for which +devices will be assigned. The file is in [TOML](https://toml.io) format. + +### Devices + +Devices are added to the settings file into one of three table arrays. + +- `bitwig` for Bitwig Studio devices +- `vst3` for VST3 devices +- `vst2` for VST2 devices + +Each table in the array consists of an `id` key and a `params` table that contains any number of +parameter ID keys. The type of the ID and parameters is different for each of the three types. + +| Device Type | ID Type | Parameter Type | +| ----------- | ----------- | -------------- | +| Bitwig | UUID String | String | +| VST3 | String | Integer | +| VST2 | Integer | Integer | + +See the included `SpecificDeviceSettings.toml` file for examples. + +### Controls + +The `controls` table contains two string arrays, `knob1` and `knob2`. The strings in these arrays +are the device parameter key names that will be assigned to the respective knob in the +[track & device bank](#Track-&-Device-Bank-(Bank-1)). + +For example, inserting the string `"mix"` into the array will assign any parameters defined with a +key `mix` to that knob. + +See the included `SpecificDeviceSettings.toml` file for examples. + +### Finding IDs + +In order to retreive IDs for devices and their parameters you must add a configuration option to +Bitwig Studio. + +1. Create a file with the name `config.json` in your user settings directory. The location of this + directory is platform dependent: + + - Windows: `%LOCALAPPDATA%/Bitwig Studio` + - Mac: `Library/Application Support/Bitwig/Bitwig Studio` + - Linux: `~/.BitwigStudio` + +2. Add the following line to the `config.json` file: `can-copy-device-and-param-ids: true` + +3. Restart Bitwig Studio + +Once you have this setting in place, you can retreive IDs from the context menu of the device and +parameters. + +![device-id](images/device-id.png) + +![param-id](images/param-id.png) diff --git a/docs/images/add-device.png b/docs/images/add-device.png new file mode 100644 index 0000000..fe760c6 Binary files /dev/null and b/docs/images/add-device.png differ diff --git a/docs/images/device-id.png b/docs/images/device-id.png new file mode 100644 index 0000000..3f88fab Binary files /dev/null and b/docs/images/device-id.png differ diff --git a/docs/images/hardware.png b/docs/images/hardware.png new file mode 100644 index 0000000..b17003e Binary files /dev/null and b/docs/images/hardware.png differ diff --git a/docs/images/midi-io.png b/docs/images/midi-io.png new file mode 100644 index 0000000..a157429 Binary files /dev/null and b/docs/images/midi-io.png differ diff --git a/docs/images/options.png b/docs/images/options.png new file mode 100644 index 0000000..f1d559c Binary files /dev/null and b/docs/images/options.png differ diff --git a/docs/images/param-id.png b/docs/images/param-id.png new file mode 100644 index 0000000..c4f6a7d Binary files /dev/null and b/docs/images/param-id.png differ diff --git a/docs/images/project-settings.png b/docs/images/project-settings.png new file mode 100644 index 0000000..5cfa015 Binary files /dev/null and b/docs/images/project-settings.png differ diff --git a/format.xml b/format.xml new file mode 100644 index 0000000..2775a0b --- /dev/null +++ b/format.xml @@ -0,0 +1,391 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..be8127c --- /dev/null +++ b/pom.xml @@ -0,0 +1,198 @@ + + + + 4.0.0 + io.github.dozius + twister-sister + jar + Twister Sister + 1.0.0 + + + UTF-8 + + + + + windows-profile + + + windows + + + + ${env.USERPROFILE}/Documents/Bitwig Studio/Extensions + + + + mac-profile + + + mac + + + + ~/Documents/Bitwig Studio/Extensions + + + + linux-profile + + + unix + Linux + + + + ~/Bitwig Studio/Extensions + + + + + + + bitwig + Bitwig Maven Repository + https://maven.bitwig.com + + + + + + com.bitwig + extension-api + 12 + + + + org.tomlj + tomlj + 1.0.0 + + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + enforce-maven + + enforce + + + + + 3.1.0 + + + + + + + + + + org.codehaus.mojo + versions-maven-plugin + 2.8.1 + + false + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + package + + shade + + + + + com.bitwig:extension-api + + + + + + *:* + + META-INF/*.MF + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + true + true + 12 + UTF-8 + 1024m + + + + + com.coderplus.maven.plugins + copy-rename-maven-plugin + 1.0.1 + + + install-extension + install + + copy + + + ${project.build.directory}/${project.build.finalName}.jar + ${bitwig.extension.directory}/TwisterSister.bwextension + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.3.0 + + + package-release + package + + single + + + false + TwisterSister-${project.version} + + release.xml + + + + + + + + + diff --git a/release.xml b/release.xml new file mode 100644 index 0000000..f4fb7fd --- /dev/null +++ b/release.xml @@ -0,0 +1,28 @@ + + + package-release + false + + zip + + + + ${project.build.directory}/${project.build.finalName}.jar + TwisterSister.bwextension + + + resources/TwisterSister.mfs + + + resources/SpecificDeviceSettings.toml + + + resources/README.txt + + + LICENSE + + + diff --git a/resources/README.txt b/resources/README.txt new file mode 100644 index 0000000..cf63d54 --- /dev/null +++ b/resources/README.txt @@ -0,0 +1,3 @@ +Twister Sister Bitwig Controller Extension + +For installation instructions please go to https://github.com/dozius/TwisterSister/tree/main/docs. diff --git a/resources/SpecificDeviceSettings.toml b/resources/SpecificDeviceSettings.toml new file mode 100644 index 0000000..9697397 --- /dev/null +++ b/resources/SpecificDeviceSettings.toml @@ -0,0 +1,60 @@ +[controls] +knob1 = [] # param keys that will be bound to knob 1 +knob2 = [] # param keys that will be bound to knob 2 + +# Example +# [controls] +# knob1 = ["mix"] +# knob2 = ["output_gain", "decay_time"] +# + +################################################################################ +# ______ _____ _____ _ _ _____ _____ +# | ___ \_ _|_ _| | | |_ _| __ \ +# | |_/ / | | | | | | | | | | | | \/ +# | ___ \ | | | | | |/\| | | | | | __ +# | |_/ /_| |_ | | \ /\ /_| |_| |_\ \ +# \____/ \___/ \_/ \/ \/ \___/ \____/ +# +################################################################################ +# Examples +# +# [[bitwig]] +# id = "72a3018d-788b-472c-b1d7-16419d00f4c6" +# params.mix = "MIX" +# +# [[bitwig]] +# id = "b5b2b08e-730e-4192-be71-f572ceb5069b" +# params.mix = "MIX" +# params.output_gain = "LEVEL" + +################################################################################ +# _ _ _____ _____ _____ +# | | | / ___|_ _|____ | +# | | | \ `--. | | / / +# | | | |`--. \ | | \ \ +# \ \_/ /\__/ / | | .___/ / +# \___/\____/ \_/ \____/ +# +################################################################################ +# Example +# +# [[vst3]] +# id = "5653547665653376616C68616C6C6176" +# params.mix = 48 +# params.decay_time = 50 + +################################################################################ +# _ _ _____ _____ _____ +# | | | / ___|_ _/ __ \ +# | | | \ `--. | | `' / /' +# | | | |`--. \ | | / / +# \ \_/ /\__/ / | | ./ /___ +# \___/\____/ \_/ \_____/ +# +################################################################################ +# Example +# +# [[vst2]] +# id = 1315513406 +# params.mix = 11 diff --git a/resources/TwisterSister.mfs b/resources/TwisterSister.mfs new file mode 100644 index 0000000..5f51f4f Binary files /dev/null and b/resources/TwisterSister.mfs differ diff --git a/src/main/java/io/github/dozius/TwisterSisterExtension.java b/src/main/java/io/github/dozius/TwisterSisterExtension.java new file mode 100644 index 0000000..c1da088 --- /dev/null +++ b/src/main/java/io/github/dozius/TwisterSisterExtension.java @@ -0,0 +1,445 @@ +/* + * Copyright 2021 Dan Smith + * + * This file is part of Twister Sister. + * + * Twister Sister is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Twister Sister is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with Twister + * Sister. If not, see . + * + */ +package io.github.dozius; + +import java.util.List; +import java.util.Set; + +import com.bitwig.extension.api.Color; +import com.bitwig.extension.controller.api.Bank; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.CursorDevice; +import com.bitwig.extension.controller.api.CursorRemoteControlsPage; +import com.bitwig.extension.controller.api.CursorTrack; +import com.bitwig.extension.controller.api.Device; +import com.bitwig.extension.controller.api.DeviceBank; +import com.bitwig.extension.controller.api.DocumentState; +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extension.controller.api.MidiIn; +import com.bitwig.extension.controller.api.MidiOut; +import com.bitwig.extension.controller.api.Parameter; +import com.bitwig.extension.controller.api.PinnableCursorDevice; +import com.bitwig.extension.controller.api.Preferences; +import com.bitwig.extension.controller.api.RemoteControl; +import com.bitwig.extension.controller.api.Send; +import com.bitwig.extension.controller.api.SendBank; +import com.bitwig.extension.controller.api.SettableBooleanValue; +import com.bitwig.extension.controller.api.SettableRangedValue; +import com.bitwig.extension.controller.api.TrackBank; +import com.bitwig.extension.controller.ControllerExtension; + +import io.github.dozius.settings.AbstractDeviceSetting; +import io.github.dozius.settings.UserColorSettings; +import io.github.dozius.settings.SpecificDeviceSettings; +import io.github.dozius.twister.Twister; +import io.github.dozius.twister.TwisterColors; +import io.github.dozius.twister.TwisterKnob; +import io.github.dozius.util.CursorNormalizedValue; +import io.github.dozius.util.OnOffColorSupplier; + +public class TwisterSisterExtension extends ControllerExtension +{ + public MidiIn midiIn; + public MidiOut midiOut; + public HardwareSurface hardwareSurface; + public Twister twister; + public CursorTrack cursorTrack; + + private DocumentState documentState; + private SpecificDeviceSettings specificDeviceSettings; + private OnOffColorSupplier deviceColorSupplier; + private OnOffColorSupplier devicePageColorSupplier; + private OnOffColorSupplier deviceSpecific1ColorSupplier; + private OnOffColorSupplier deviceSpecific2ColorSupplier; + + protected TwisterSisterExtension(final TwisterSisterExtensionDefinition definition, + final ControllerHost host) + { + super(definition, host); + } + + @Override + public void init() + { + final ControllerHost host = getHost(); + + midiIn = host.getMidiInPort(0); + midiOut = host.getMidiOutPort(0); + hardwareSurface = host.createHardwareSurface(); + twister = new Twister(this); + documentState = host.getDocumentState(); + specificDeviceSettings = new SpecificDeviceSettings(getSpecificDeviceSettingsPath()); + cursorTrack = host.createCursorTrack(1, 0); + deviceColorSupplier = new OnOffColorSupplier(); + devicePageColorSupplier = new OnOffColorSupplier(); + deviceSpecific1ColorSupplier = new OnOffColorSupplier(); + deviceSpecific2ColorSupplier = new OnOffColorSupplier(); + + loadPreferences(); + setupTrackBank(); + setupUserBanks(); + + twister.setActiveBank(0); + } + + @Override + public void exit() + { + if (twister != null) { + twister.lightsOff(); + } + } + + @Override + public void flush() + { + if (hardwareSurface != null) { + hardwareSurface.updateHardware(); + } + } + + /** + * Gets the path to the specific device settings config file in a cross platform manner. + * + * @return Path to "SpecificDeviceSettings.toml". + */ + private String getSpecificDeviceSettingsPath() + { + final String file = "SpecificDeviceSettings.toml"; + + switch (getHost().getPlatformType()) + { + case WINDOWS: + final String userProfile = System.getenv("USERPROFILE").replace("\\", "/"); + return userProfile + "/Documents/Bitwig Studio/Extensions/" + file; + + case MAC: + return "~/Documents/Bitwig Studio/Extensions/" + file; + + case LINUX: + return "~/Bitwig Studio/Extensions/" + file; + + default: + throw new IllegalArgumentException("Unknown Platform"); + } + } + + /** Loads the extension preferences and sets up observers to allow for interactive updates. */ + private void loadPreferences() + { + final Preferences preferences = getHost().getPreferences(); + + // Enable/disable notification popups on bank change + final SettableBooleanValue popupEnabled = preferences.getBooleanSetting("Show Bank Popup", + "Options", false); + twister.setPopupEnabled(popupEnabled.get()); + popupEnabled.addValueObserver(twister::setPopupEnabled); + + // Set the color used for the row of controls related to devices, i.e. second row + final SettableRangedValue deviceRowColor = preferences.getNumberSetting("Device Row Color", + "Options", 0, 125, 1, + null, 115); + setDeviceRowColor(TwisterColors.ALL.get((int) deviceRowColor.getRaw())); + deviceRowColor.addValueObserver(125, + (value) -> setDeviceRowColor(TwisterColors.ALL.get(value))); + + // Sets the fine sensitivity factor for all knobs + final SettableRangedValue globalFineSensitivity = preferences.getNumberSetting("Global Fine Sensitivity", + "Options", 0.01, + 1.00, 0.01, null, + 0.25); + setGlobalFineSensitivity(globalFineSensitivity.get()); + globalFineSensitivity.addValueObserver(this::setGlobalFineSensitivity); + } + + /** + * Sets the RGB light color for the row of device related controls on the track bank. + * + * @param color Color to set the RGB lights. + */ + private void setDeviceRowColor(Color color) + { + deviceColorSupplier.setOnColor(color); + devicePageColorSupplier.setOnColor(color); + deviceSpecific1ColorSupplier.setOnColor(color); + deviceSpecific2ColorSupplier.setOnColor(color); + } + + /** + * Sets the fine sensitivity factor for all controls + * + * @param factor Sensitivity factor to use. + */ + private void setGlobalFineSensitivity(double factor) + { + for (Twister.Bank bank : twister.banks) { + for (TwisterKnob knob : bank.knobs) { + knob.setFineSensitivity(factor); + } + } + } + + /** Sets up all the hardware for the track bank. */ + private void setupTrackBank() + { + addTrackKnobs(); + addDeviceKnobs(); + } + + /** Sets up all the hardware for all 3 user banks. */ + private void setupUserBanks() + { + final UserColorSettings colorSettings = new UserColorSettings(documentState); + + for (int bank = 1; bank < twister.banks.length; ++bank) { + final TwisterKnob[] knobs = twister.banks[bank].knobs; + final int settingsBank = bank - 1; + + for (int idx = 0; idx < knobs.length; ++idx) { + final TwisterKnob knob = knobs[idx]; + final SettableRangedValue colorSetting = colorSettings.getSetting(settingsBank, idx); + + // Ring light follows whatever target is currently bound to the knob + knob.ringLight().observeValue(knob.targetValue()); + + // Change color setting with shift encoder + knob.setShiftBinding(UserColorSettings.createTarget(getHost(), colorSetting)); + + // Send changes from color setting to RGB light + colorSetting.addValueObserver(126, knob.rgbLight()::setRawValue); + + // Reset color on double press + knob.button().addDoubleClickedObserver(() -> colorSetting.set(0.0)); + } + } + } + + /** Sets up all the track related knobs. Track select, volume, pan, etc. */ + private void addTrackKnobs() + { + final TrackBank trackBank = cursorTrack.createSiblingsTrackBank(1, 0, 0, true, true); + final TwisterKnob[] knobs = twister.banks[0].knobs; + final SendBank sendBank = cursorTrack.sendBank(); + final Send send = sendBank.getItemAt(0); + + cursorTrack.color().markInterested(); + sendBank.canScrollForwards().markInterested(); + sendBank.itemCount().markInterested(); + send.sendChannelColor().markInterested(); + + final TwisterKnob selectionKnob = knobs[0]; + selectionKnob.setBinding(cursorTrack); + selectionKnob.ringLight().observeValue(new CursorNormalizedValue(cursorTrack, trackBank)); + selectionKnob.rgbLight().setColorSupplier(cursorTrack.color()); + selectionKnob.button().addClickedObserver(cursorTrack.mute()::toggle); + selectionKnob.button().addLongPressedObserver(cursorTrack.arm()::toggle); + + final TwisterKnob volumeKnob = knobs[1]; + volumeKnob.setBinding(cursorTrack.volume()); + volumeKnob.ringLight().observeValue(cursorTrack.volume().value()); + volumeKnob.rgbLight().setColorSupplier(cursorTrack.color()); + volumeKnob.button().addLongPressedObserver(cursorTrack.solo()::toggle); + volumeKnob.button().addClickedObserver(volumeKnob::toggleSensitivity); + volumeKnob.button().addDoubleClickedObserver(cursorTrack.volume()::reset); + + final TwisterKnob panKnob = knobs[2]; + panKnob.setBinding(cursorTrack.pan()); + panKnob.ringLight().observeValue(cursorTrack.pan().value()); + panKnob.rgbLight().setColorSupplier(cursorTrack.color()); + panKnob.button().addClickedObserver(panKnob::toggleSensitivity); + panKnob.button().addDoubleClickedObserver(cursorTrack.pan()::reset); + + final TwisterKnob sendKnob = knobs[3]; + sendKnob.setBinding(send); + sendKnob.ringLight().observeValue(send.value()); + sendKnob.rgbLight().setColorSupplier(send.sendChannelColor()); + sendKnob.button().addClickedObserver(() -> circularScrollForward(sendBank)); + } + + /** Sets up all the device related knobs. Selection, remote control page, etc. */ + private void addDeviceKnobs() + { + final PinnableCursorDevice cursorDevice = cursorTrack.createCursorDevice(); + final CursorRemoteControlsPage remoteControlsPage = cursorDevice.createCursorRemoteControlsPage(8); + final DeviceBank deviceBank = cursorDevice.createSiblingsDeviceBank(1); + final TwisterKnob[] knobs = twister.banks[0].knobs; + + cursorDevice.exists().addValueObserver(deviceColorSupplier::setOn); + cursorDevice.exists().addValueObserver(devicePageColorSupplier::setOn); + remoteControlsPage.pageCount() + .addValueObserver((count) -> devicePageColorSupplier.setOn(count > 0)); + + final TwisterKnob deviceKnob = knobs[4]; + deviceKnob.setBinding(cursorDevice); + deviceKnob.ringLight().observeValue(new CursorNormalizedValue(cursorDevice, deviceBank)); + deviceKnob.rgbLight().setColorSupplier(deviceColorSupplier); + deviceKnob.button().addClickedObserver(cursorDevice.isEnabled()::toggle); + deviceKnob.button().addLongPressedObserver(cursorDevice.isExpanded()::toggle); + + final TwisterKnob pageKnob = knobs[5]; + pageKnob.setBinding(remoteControlsPage); + pageKnob.ringLight().observeValue(new CursorNormalizedValue(remoteControlsPage)); + pageKnob.rgbLight().setColorSupplier(devicePageColorSupplier); + pageKnob.button().addClickedObserver(cursorDevice.isWindowOpen()::toggle); + pageKnob.button().addLongPressedObserver(cursorDevice.isRemoteControlsSectionVisible()::toggle); + + final TwisterKnob specificDeviceKnob1 = knobs[6]; + bindLongPressedToBrowseBeforeDevice(specificDeviceKnob1, cursorDevice); + setupSpecificDeviceKnob("knob1", specificDeviceKnob1, cursorDevice, + deviceSpecific1ColorSupplier); + + final TwisterKnob specificDeviceKnob2 = knobs[7]; + bindLongPressedToBrowseAfterDevice(specificDeviceKnob2, cursorDevice); + setupSpecificDeviceKnob("knob2", specificDeviceKnob2, cursorDevice, + deviceSpecific2ColorSupplier); + + final int firstKnobIndex = 8; + + for (int i = 0; i < remoteControlsPage.getParameterCount(); ++i) { + final Color color = TwisterColors.BITWIG_PARAMETERS.get(i); + final OnOffColorSupplier colorSupplier = new OnOffColorSupplier(color); + final TwisterKnob knob = knobs[firstKnobIndex + i]; + final RemoteControl control = remoteControlsPage.getParameter(i); + + control.setIndication(true); + control.exists().addValueObserver(colorSupplier::setOn); + knob.setBinding(control); + knob.ringLight().observeValue(control.value()); + knob.rgbLight().setColorSupplier(colorSupplier); + knob.button().addClickedObserver(knob::toggleSensitivity); + knob.button().addDoubleClickedObserver(control::reset); + } + } + + /** + * Helper function to scroll a bank forward in a circular fashion, i.e. wraps around to the + * beginning if scrolling past the end. + * + * @param bank The bank to scroll. + */ + private void circularScrollForward(Bank bank) + { + if (bank.canScrollForwards().get()) { + bank.scrollForwards(); + } + else { + bank.scrollBy(-(bank.itemCount().get() - 1)); + } + } + + /** + * Binds a knob buttons long pressed action to insert a device after the selected device or at the + * start of the chain if no devices exist yet. + * + * @param knob Knob to bind. + * @param cursorDevice Device cursor to follow. + */ + private void bindLongPressedToBrowseAfterDevice(TwisterKnob knob, CursorDevice cursorDevice) + { + cursorDevice.exists().addValueObserver((exists) -> { + final Runnable browseAfter = cursorDevice.afterDeviceInsertionPoint()::browse; + final Runnable browseStart = cursorDevice.deviceChain() + .startOfDeviceChainInsertionPoint()::browse; + + knob.button().setLongPressedObserver(exists ? browseAfter : browseStart); + }); + } + + /** + * Binds a knob buttons long pressed action to insert a device before the selected device or at + * the start of the chain if no devices exist yet. + * + * @param knob Knob to bind. + * @param cursorDevice Device cursor to follow. + */ + private void bindLongPressedToBrowseBeforeDevice(TwisterKnob knob, CursorDevice cursorDevice) + { + cursorDevice.exists().addValueObserver((exists) -> { + final Runnable browseBefore = cursorDevice.beforeDeviceInsertionPoint()::browse; + final Runnable browseStart = cursorDevice.deviceChain() + .startOfDeviceChainInsertionPoint()::browse; + + knob.button().setLongPressedObserver(exists ? browseBefore : browseStart); + }); + } + + /** + * Sets up a knob to control a specific device control parameter based on key name. + * + * @param controlKey The controls key to apply to the knob. + * @param knob The knob to setup. + * @param device The device used to create the parameters. + * @param colorSupplier The color supplier for the knob. + */ + private void setupSpecificDeviceKnob(String controlKey, TwisterKnob knob, Device device, + OnOffColorSupplier colorSupplier) + { + knob.rgbLight().setColorSupplier(colorSupplier); + knob.button().addClickedObserver(knob::toggleSensitivity); + + final Set keys = specificDeviceSettings.controlMap().get(controlKey); + + if (keys == null) { + return; + } + + for (String key : keys) { + bindSpecificDevices(specificDeviceSettings.bitwigDevices(), key, device, knob, colorSupplier); + bindSpecificDevices(specificDeviceSettings.vst3Devices(), key, device, knob, colorSupplier); + bindSpecificDevices(specificDeviceSettings.vst2Devices(), key, device, knob, colorSupplier); + } + } + + /** + * Binds all available parameters from all devices that match the key. + * + * @param The type of the device ID. This will be deduced from settings. + * @param The type of device settings object. This will be deduced from settings. + * @param settings The list of device settings to search. + * @param key The parameter key to match against. + * @param device The device used to create parameters. + * @param knob The knob to bind parameters to. + * @param colorSupplier The knobs color supplier. + */ + private > void bindSpecificDevices(List settings, + String key, + Device device, + TwisterKnob knob, + OnOffColorSupplier colorSupplier) + { + for (final SettingType setting : settings) { + if (setting.parameters().get(key) == null) { + continue; + } + + final Parameter param = setting.createParameter(device, key); + + knob.ringLight().observeValue(param.value()); + + param.exists().markInterested(); + param.exists().addValueObserver((exists) -> { + if (exists) { + knob.setBinding(param); + knob.button().setDoubleClickedObserver(param::reset); + } + + colorSupplier.setOn(exists); + }); + } + } +} diff --git a/src/main/java/io/github/dozius/TwisterSisterExtensionDefinition.java b/src/main/java/io/github/dozius/TwisterSisterExtensionDefinition.java new file mode 100644 index 0000000..76b8ef7 --- /dev/null +++ b/src/main/java/io/github/dozius/TwisterSisterExtensionDefinition.java @@ -0,0 +1,118 @@ +/* + * Copyright 2021 Dan Smith + * + * This file is part of Twister Sister. + * + * Twister Sister is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Twister Sister is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with Twister + * Sister. If not, see . + * + */ +package io.github.dozius; + +import java.util.UUID; + +import com.bitwig.extension.api.PlatformType; +import com.bitwig.extension.controller.AutoDetectionMidiPortNamesList; +import com.bitwig.extension.controller.ControllerExtensionDefinition; +import com.bitwig.extension.controller.api.ControllerHost; + +public class TwisterSisterExtensionDefinition extends ControllerExtensionDefinition +{ + private static final UUID DRIVER_ID = UUID.fromString("b4c9dbb6-c8b8-4f79-8c3a-cd43650fab44"); + + public TwisterSisterExtensionDefinition() + { + } + + @Override + public String getName() + { + return "Twister Sister"; + } + + @Override + public String getAuthor() + { + return "Dan Smith"; + } + + @Override + public String getVersion() + { + return "1.0.0"; + } + + @Override + public UUID getId() + { + return DRIVER_ID; + } + + @Override + public String getHardwareVendor() + { + return "DJ TechTools"; + } + + @Override + public String getHardwareModel() + { + return "MIDI Fighter Twister"; + } + + @Override + public int getRequiredAPIVersion() + { + return 12; + } + + @Override + public int getNumMidiInPorts() + { + return 1; + } + + @Override + public int getNumMidiOutPorts() + { + return 1; + } + + @Override + public void listAutoDetectionMidiPortNames(final AutoDetectionMidiPortNamesList list, + final PlatformType platformType) + { + switch (platformType) + { + case WINDOWS: + case MAC: + list.add(new String[] {"Midi Fighter Twister"}, new String[] {"Midi Fighter Twister"}); + break; + + case LINUX: + list.add(new String[] {"Midi Fighter Twister MIDI 1"}, + new String[] {"Midi Fighter Twister MIDI 1"}); + break; + } + } + + @Override + public TwisterSisterExtension createInstance(final ControllerHost host) + { + return new TwisterSisterExtension(this, host); + } + + @Override + public String getHelpFilePath() + { + return "https://github.com/dozius/TwisterSister/tree/main/docs"; + } +} diff --git a/src/main/java/io/github/dozius/settings/AbstractDeviceSetting.java b/src/main/java/io/github/dozius/settings/AbstractDeviceSetting.java new file mode 100644 index 0000000..be6ad6a --- /dev/null +++ b/src/main/java/io/github/dozius/settings/AbstractDeviceSetting.java @@ -0,0 +1,68 @@ +/* + * Copyright 2021 Dan Smith + * + * This file is part of Twister Sister. + * + * Twister Sister is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Twister Sister is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with Twister + * Sister. If not, see . + * + */ +package io.github.dozius.settings; + +import java.util.Map; + +import com.bitwig.extension.controller.api.Device; +import com.bitwig.extension.controller.api.Parameter; + +/** Base class for specific device settings. */ +public abstract class AbstractDeviceSetting +{ + protected final IdType id; + protected final Map params; + + /** + * Creates a new AbstractDeviceSetting. + * + * @param id The ID of the device. + * @param params The device parameters mapped to string keys. + */ + protected AbstractDeviceSetting(IdType id, Map params) + { + this.id = id; + this.params = params; + } + + /** + * @return The device ID. + */ + public IdType id() + { + return id; + } + + /** + * @return Parameter IDs mapped to string keys. + */ + public Map parameters() + { + return params; + } + + /** + * Creates a specific device parameter for the device based on the key. + * + * @param device The device from which to create the parameter. + * @param key The key of the parameter to create. + * + * @return The parameter if it exists, otherwise null. + */ + public abstract Parameter createParameter(Device device, String key); +} diff --git a/src/main/java/io/github/dozius/settings/BitwigDeviceSetting.java b/src/main/java/io/github/dozius/settings/BitwigDeviceSetting.java new file mode 100644 index 0000000..d4f270d --- /dev/null +++ b/src/main/java/io/github/dozius/settings/BitwigDeviceSetting.java @@ -0,0 +1,73 @@ +/* + * Copyright 2021 Dan Smith + * + * This file is part of Twister Sister. + * + * Twister Sister is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Twister Sister is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with Twister + * Sister. If not, see . + * + */ +package io.github.dozius.settings; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import com.bitwig.extension.controller.api.Device; +import com.bitwig.extension.controller.api.Parameter; + +import org.tomlj.TomlTable; + +/** + * Bitwig specific device settings. + * + * Contains an ID as well as any parameter IDs that were set during construction. + */ +public class BitwigDeviceSetting extends AbstractDeviceSetting +{ + private BitwigDeviceSetting(UUID id, Map params) + { + super(id, params); + } + + /** + * Constructs a BitwigDeviceSetting object from a TOML table. + * + * @param table The TOML table from which to create the object. + * + * @return A new BitwigDeviceSetting. Throws on errors. + */ + public static BitwigDeviceSetting fromToml(TomlTable table) + { + final UUID id = UUID.fromString(table.getString("id")); + + final Map params = new HashMap<>(); + final TomlTable tomlParams = table.getTable("params"); + + for (String key : tomlParams.keySet()) { + final String paramId = tomlParams.getString(key); + + if (paramId == null || paramId.isEmpty()) { + throw new IllegalArgumentException("Parameter ID must not be null or empty"); + } + + params.put(key, paramId); + } + + return new BitwigDeviceSetting(id, params); + } + + @Override + public Parameter createParameter(Device device, String key) + { + return device.createSpecificBitwigDevice(id).createParameter(params.get(key)); + } +} diff --git a/src/main/java/io/github/dozius/settings/SpecificDeviceSettings.java b/src/main/java/io/github/dozius/settings/SpecificDeviceSettings.java new file mode 100644 index 0000000..e6f0929 --- /dev/null +++ b/src/main/java/io/github/dozius/settings/SpecificDeviceSettings.java @@ -0,0 +1,239 @@ +/* + * Copyright 2021 Dan Smith + * + * This file is part of Twister Sister. + * + * Twister Sister is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Twister Sister is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with Twister + * Sister. If not, see . + * + */ +package io.github.dozius.settings; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +import org.tomlj.Toml; +import org.tomlj.TomlArray; +import org.tomlj.TomlParseError; +import org.tomlj.TomlParseResult; +import org.tomlj.TomlTable; + +/** + * Represents data loaded from a specific devices settings file. + * + * This data can be used to programmatically setup specific devices using the specific device + * portion of the extension API. Since the API provides no way to query device and param IDs, this + * allows for more flexibility than a hardcoded approach to specific device setup. + * + * The TOML file is required to a have a specific structure. + * + * A table called "controls". This table can have any number of string array keys. These string + * arrays are loaded into the controlMap. This table is optional. + * + * Three table arrays, "bitwig", "vst3" and "vst2". These represent all the device settings. These + * arrays are optional. + * + * Each table requires an id key that is equal to the device ID taken from Bitwig. For Bitwig and + * VST3 devices it is a string, for VST2 it is an integer. + * + * Each table has a sub table called "params". This table contains any number of keys that are equal + * to parameter IDs taken from Bitwig. Bitwig device parameters are strings, VST3 and VST2 device + * parameters are integers. + * + * Example: + * + *
+ * [controls]
+ * knob1 = ["mix"]
+ * knob2 = ["output_gain", "decay_time"]
+ *
+ * [[bitwig]]
+ * id = "b5b2b08e-730e-4192-be71-f572ceb5069b"
+ * params.mix = "MIX"
+ * params.output_gain = "LEVEL"
+ *
+ * [[vst3]]
+ * id = "5653547665653376616C68616C6C6176"
+ * params.mix = 48
+ * params.decay_time = 50
+ *
+ * [[vst2]]
+ * id = 1315513406
+ * params.mix = 11
+ * 
+ * + * These table arrays end up as the bitwigDevices, vst3Devices and vst2Devices lists. + */ +public class SpecificDeviceSettings +{ + private final Map> controlMap; + private final List bitwigDevices; + private final List vst3Devices; + private final List vst2Devices; + + /** + * Constructs a SpecificDeviceSettings object from the specified TOML file. + * + * @param settingsPath The path to the TOML file to parse. + */ + public SpecificDeviceSettings(Path settingsPath) + { + try { + final TomlParseResult toml = Toml.parse(settingsPath); + + if (toml.hasErrors()) { + final ArrayList errorMessages = new ArrayList<>(); + + for (final TomlParseError error : toml.errors()) { + errorMessages.add(error.toString()); + } + + throw new ParseException(String.join("\n", errorMessages), 0); + } + + controlMap = loadControls(toml); + bitwigDevices = loadDevices(toml, "bitwig", BitwigDeviceSetting::fromToml); + vst3Devices = loadDevices(toml, "vst3", Vst3DeviceSetting::fromToml); + vst2Devices = loadDevices(toml, "vst2", Vst2DeviceSetting::fromToml); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Constructs a SpecificDeviceSettings object from the specified TOML file. + * + * @param settingsPath The path to the TOML file to parse as a string. + */ + public SpecificDeviceSettings(String settingsPath) + { + this(Paths.get(settingsPath)); + } + + /** + * @return Map of controls with parameter keys. + */ + public Map> controlMap() + { + return controlMap; + } + + /** + * @return List of Bitwig devices. + */ + public List bitwigDevices() + { + return bitwigDevices; + } + + /** + * @return List of VST3 devices. + */ + public List vst3Devices() + { + return vst3Devices; + } + + /** + * @return List of VST2 devices. + */ + public List vst2Devices() + { + return vst2Devices; + } + + /** + * Loads all the controls and their device parameter key arrays from the controls table. + * + * @param result The parse result of the the TOML config file. + * + * @return A set of control keys mapped to lists of device parameter keys. + */ + private Map> loadControls(TomlParseResult result) + { + final TomlTable controlsTable = result.getTable("controls"); + + if (controlsTable == null) { + return null; + } + + final Map> output = new HashMap<>(); + + for (String key : controlsTable.keySet()) { + final TomlArray controlParams = controlsTable.getArray(key); + + if (controlParams == null) { + continue; + } + + Set params = new HashSet<>(); + + for (int i = 0; i < controlParams.size(); ++i) { + final String param = controlParams.getString(i); + + if (param == null) { + continue; + } + + params.add(param); + } + + output.put(key, params); + } + + return output; + } + + /** + * Loads all the devices of a specific type. + * + * @param The type of the device ID. + * @param The settings type for the device. + * @param result The parse result of the TOML config file. + * @param key The key of the array of device tables. + * @param deviceBuilder A function that will construct the device setting from a TOML table. + * + * @return A list of valid device settings. + */ + private > List loadDevices(TomlParseResult result, + String key, + Function deviceBuilder) + { + final TomlArray devices = result.getArray(key); + + if (devices == null) { + return null; + } + + final Map settings = new HashMap<>(); + + for (int idx = 0; idx < devices.size(); ++idx) { + final SettingType device = deviceBuilder.apply(devices.getTable(idx)); + + if (settings.containsKey(device.id())) { + throw new RuntimeException("Duplicate " + key + " device ID found: " + device.id()); + } + + settings.put(device.id(), device); + } + + return new ArrayList<>(settings.values()); + } +} diff --git a/src/main/java/io/github/dozius/settings/UserColorSettings.java b/src/main/java/io/github/dozius/settings/UserColorSettings.java new file mode 100644 index 0000000..1a54ea2 --- /dev/null +++ b/src/main/java/io/github/dozius/settings/UserColorSettings.java @@ -0,0 +1,129 @@ +/* + * Copyright 2021 Dan Smith + * + * This file is part of Twister Sister. + * + * Twister Sister is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Twister Sister is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with Twister + * Sister. If not, see . + * + */ +package io.github.dozius.settings; + +import java.util.List; + +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.DocumentState; +import com.bitwig.extension.controller.api.RelativeHardwarControlBindable; +import com.bitwig.extension.controller.api.SettableEnumValue; +import com.bitwig.extension.controller.api.SettableRangedValue; +import com.bitwig.extension.controller.api.Setting; + +import io.github.dozius.twister.Twister; +import io.github.dozius.util.MathUtil; + +/** + * Color settings for the individual RGB lights in each user mappable bank. + * + * These settings can be accessed in the I/O panel. There is also a helper function to create + * bindable targets for a setting so that it can be controlled via hardware. + */ +public class UserColorSettings +{ + private static final int NUM_USER_BANKS = 3; + private static final int NUM_KNOBS_PER_BANK = Twister.Bank.NUM_KNOBS; + + private final List options = List.of("Hide", "2", "3", "4"); + private final SettableRangedValue[][] settings = new SettableRangedValue[NUM_USER_BANKS][NUM_KNOBS_PER_BANK]; + private final SettableEnumValue selector; + + /** + * Creates all the settings from the document state. + * + * @param documentState The document state of the host. + */ + public UserColorSettings(DocumentState documentState) + { + // Setup the bank selector + final String[] strings = options.toArray(String[]::new); + selector = documentState.getEnumSetting("Bank", "Colors", strings, options.get(0)); + selector.addValueObserver((value) -> showBank(options.indexOf(value) - 1)); + + // Create all the individual settings + for (int bank = 0; bank < settings.length; ++bank) { + final SettableRangedValue[] settingsBank = settings[bank]; + + for (int idx = 0; idx < settingsBank.length; ++idx) { + final String label = String.format("Color %02d%" + (bank + 1) + "s", idx + 1, " "); + final SettableRangedValue colorSetting = documentState.getNumberSetting(label, "Colors", 0, + 125, 1, null, 0); + settingsBank[idx] = colorSetting; + ((Setting) colorSetting).hide(); + } + } + } + + /** + * Gets a specific setting. + * + * @param colorBankIndex Bank index of the desired setting. + * @param knobIndex Knob index of the desired setting. + * + * @return The setting for the given bank and index. + */ + public SettableRangedValue getSetting(int colorBankIndex, int knobIndex) + { + return settings[colorBankIndex][knobIndex]; + } + + /** + * Creates a target to a color setting that is able to be bound to hardware. + * + * Despite being a SettableRangedValue, the settings are not compatible targets and this proxy + * target must be created instead. + * + * @param host The controller host. + * @param setting The setting to create a target for. + * + * @return A bindable target to the setting. + */ + public static RelativeHardwarControlBindable createTarget(ControllerHost host, + SettableRangedValue setting) + { + return host.createRelativeHardwareControlAdjustmentTarget((value) -> { + final double adjustedValue = MathUtil.clamp(setting.get() + value, 0.0, 1.0); + setting.set(adjustedValue); + }); + } + + /** Hides all the settings from the UI panel. */ + private void hideAll() + { + for (final SettableRangedValue[] settingsBank : settings) { + for (final SettableRangedValue colorSetting : settingsBank) { + ((Setting) colorSetting).hide(); + } + } + } + + /** Handles bank visibility */ + private void showBank(int index) + { + hideAll(); + + if (index < 0) { + return; + } + + for (final SettableRangedValue colorSetting : settings[index]) { + ((Setting) colorSetting).show(); + } + } +} diff --git a/src/main/java/io/github/dozius/settings/Vst2DeviceSetting.java b/src/main/java/io/github/dozius/settings/Vst2DeviceSetting.java new file mode 100644 index 0000000..e8f69de --- /dev/null +++ b/src/main/java/io/github/dozius/settings/Vst2DeviceSetting.java @@ -0,0 +1,77 @@ +/* + * Copyright 2021 Dan Smith + * + * This file is part of Twister Sister. + * + * Twister Sister is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Twister Sister is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with Twister + * Sister. If not, see . + * + */ +package io.github.dozius.settings; + +import java.util.HashMap; +import java.util.Map; + +import com.bitwig.extension.controller.api.Device; +import com.bitwig.extension.controller.api.Parameter; + +import org.tomlj.TomlTable; + +/** + * VST2 specific device settings. + * + * Contains an ID as well as any parameter IDs that were set during construction. + */ +public class Vst2DeviceSetting extends AbstractDeviceSetting +{ + private Vst2DeviceSetting(Integer id, Map params) + { + super(id, params); + } + + /** + * Constructs a Vst2DeviceSetting object from a TOML table. + * + * @param table The TOML table from which to create the object. + * + * @return A new Vst2DeviceSetting. Throws on errors. + */ + public static Vst2DeviceSetting fromToml(TomlTable table) + { + final Integer id = Math.toIntExact(table.getLong("id")); + + // All VST2 IDs seem to be positive integers + if (id < 0) { + throw new IllegalArgumentException("VST2 device ID must not be negative"); + } + + final Map params = new HashMap<>(); + final TomlTable tomlParams = table.getTable("params"); + + for (String key : tomlParams.keySet()) { + final Integer paramId = Math.toIntExact(tomlParams.getLong(key)); + + if (paramId < 0) { + throw new IllegalArgumentException("Parameter ID must not be negative"); + } + + params.put(key, paramId); + } + + return new Vst2DeviceSetting(id, params); + } + + @Override + public Parameter createParameter(Device device, String key) + { + return device.createSpecificVst2Device(id).createParameter(params.get(key)); + } +} diff --git a/src/main/java/io/github/dozius/settings/Vst3DeviceSetting.java b/src/main/java/io/github/dozius/settings/Vst3DeviceSetting.java new file mode 100644 index 0000000..8f3f9b8 --- /dev/null +++ b/src/main/java/io/github/dozius/settings/Vst3DeviceSetting.java @@ -0,0 +1,77 @@ +/* + * Copyright 2021 Dan Smith + * + * This file is part of Twister Sister. + * + * Twister Sister is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Twister Sister is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with Twister + * Sister. If not, see . + * + */ +package io.github.dozius.settings; + +import java.util.HashMap; +import java.util.Map; + +import com.bitwig.extension.controller.api.Device; +import com.bitwig.extension.controller.api.Parameter; + +import org.tomlj.TomlTable; + +/** + * VST3 specific device settings. + * + * Contains an ID as well as any parameter IDs that were set during construction. + */ +public class Vst3DeviceSetting extends AbstractDeviceSetting +{ + private Vst3DeviceSetting(String id, Map params) + { + super(id, params); + } + + /** + * Constructs a Vst3DeviceSetting object from a TOML table. + * + * @param table The TOML table from which to create the object. + * + * @return A new Vst3DeviceSetting. Throws on errors. + */ + public static Vst3DeviceSetting fromToml(TomlTable table) + { + final String id = table.getString("id"); + + // All VST3 IDs seem to be a 32 character hex strings + if (id.length() != 32) { + throw new IllegalArgumentException("VST3 device ID must be 32 characters long"); + } + + final Map params = new HashMap<>(); + final TomlTable tomlParams = table.getTable("params"); + + for (String key : tomlParams.keySet()) { + final Integer paramId = Math.toIntExact(tomlParams.getLong(key)); + + if (paramId < 0) { + throw new IllegalArgumentException("Parameter ID must not be negative"); + } + + params.put(key, paramId); + } + + return new Vst3DeviceSetting(id, params); + } + + @Override + public Parameter createParameter(Device device, String key) + { + return device.createSpecificVst3Device(id).createParameter(params.get(key)); + } +} diff --git a/src/main/java/io/github/dozius/twister/KnobMidiInfo.java b/src/main/java/io/github/dozius/twister/KnobMidiInfo.java new file mode 100644 index 0000000..ccd6fef --- /dev/null +++ b/src/main/java/io/github/dozius/twister/KnobMidiInfo.java @@ -0,0 +1,38 @@ +/* + * Copyright 2021 Dan Smith + * + * This file is part of Twister Sister. + * + * Twister Sister is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Twister Sister is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with Twister + * Sister. If not, see . + * + */ +package io.github.dozius.twister; + +/** + * Contains MIDI information for a Twister knob. This include the encoder, button and both lights. + */ +public class KnobMidiInfo +{ + public final MidiInfo encoder; + public final MidiInfo button; + public final LightMidiInfo rgbLight; + public final LightMidiInfo ringLight; + + public KnobMidiInfo(MidiInfo encoder, MidiInfo button, LightMidiInfo rgbLight, + LightMidiInfo ringLight) + { + this.encoder = encoder; + this.button = button; + this.rgbLight = rgbLight; + this.ringLight = ringLight; + } +} diff --git a/src/main/java/io/github/dozius/twister/LightMidiInfo.java b/src/main/java/io/github/dozius/twister/LightMidiInfo.java new file mode 100644 index 0000000..495fdc7 --- /dev/null +++ b/src/main/java/io/github/dozius/twister/LightMidiInfo.java @@ -0,0 +1,31 @@ +/* + * Copyright 2021 Dan Smith + * + * This file is part of Twister Sister. + * + * Twister Sister is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Twister Sister is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with Twister + * Sister. If not, see . + * + */ +package io.github.dozius.twister; + +/** Contains MIDI information for a Twister light. */ +public class LightMidiInfo +{ + public final MidiInfo light; + public final MidiInfo animation; + + public LightMidiInfo(int channel, int animationChannel, int cc) + { + this.light = new MidiInfo(channel, cc); + this.animation = new MidiInfo(animationChannel, cc); + } +} diff --git a/src/main/java/io/github/dozius/twister/MidiInfo.java b/src/main/java/io/github/dozius/twister/MidiInfo.java new file mode 100644 index 0000000..3316b4e --- /dev/null +++ b/src/main/java/io/github/dozius/twister/MidiInfo.java @@ -0,0 +1,35 @@ +/* + * Copyright 2021 Dan Smith + * + * This file is part of Twister Sister. + * + * Twister Sister is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Twister Sister is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with Twister + * Sister. If not, see . + * + */ +package io.github.dozius.twister; + +import com.bitwig.extension.api.util.midi.ShortMidiMessage; + +/** Contains basic Twister hardware MIDI information. */ +public class MidiInfo +{ + public final int channel; + public final int cc; + public final int statusByte; + + public MidiInfo(int channel, int cc) + { + this.channel = channel; + this.cc = cc; + this.statusByte = ShortMidiMessage.CONTROL_CHANGE + channel; + } +} diff --git a/src/main/java/io/github/dozius/twister/Twister.java b/src/main/java/io/github/dozius/twister/Twister.java new file mode 100644 index 0000000..ac8c6b1 --- /dev/null +++ b/src/main/java/io/github/dozius/twister/Twister.java @@ -0,0 +1,197 @@ +/* + * Copyright 2021 Dan Smith + * + * This file is part of Twister Sister. + * + * Twister Sister is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Twister Sister is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with Twister + * Sister. If not, see . + * + */ +package io.github.dozius.twister; + +import com.bitwig.extension.api.util.midi.ShortMidiMessage; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extension.controller.api.MidiIn; +import com.bitwig.extension.controller.api.MidiOut; + +import io.github.dozius.TwisterSisterExtension; + +/** + * Twister hardware. + * + * All the hardware available on the MIDI Fighter Twister is setup on construction and accessible + * through this class. + */ +public class Twister +{ + /** Twister MIDI channels. Zero indexed. */ + public static class MidiChannel + { + public static final int ENCODER = 0; + public static final int BUTTON = 1; + public static final int RGB_ANIMATION = 2; + public static final int SIDE_BUTTON = 3; + public static final int SYSTEM = 3; + public static final int SHIFT = 4; + public static final int RING_ANIMATION = 5; + public static final int SEQUENCER = 7; + } + + /** A single bank of Twister hardware. */ + public class Bank + { + public static final int NUM_KNOBS = 16; + public static final int NUM_LEFT_SIDE_BUTTONS = 3; + public static final int NUM_RIGHT_SIDE_BUTTONS = 3; + public static final int NUM_SIDE_BUTTONS = NUM_LEFT_SIDE_BUTTONS + NUM_RIGHT_SIDE_BUTTONS; + + public final TwisterKnob[] knobs = new TwisterKnob[NUM_KNOBS]; + public final TwisterButton[] leftSideButtons = new TwisterButton[NUM_LEFT_SIDE_BUTTONS]; + public final TwisterButton[] rightSideButtons = new TwisterButton[NUM_RIGHT_SIDE_BUTTONS]; + } + + public static final int NUM_BANKS = 4; + + public final Bank[] banks = new Bank[NUM_BANKS]; + + private final MidiOut midiOut; + private final HardwareSurface hardwareSurface; + private final ControllerHost host; + + private boolean popupEnabled = false; + + /** + * Creates a new Twister. + * + * @param extension The parent Bitwig extension. + */ + public Twister(TwisterSisterExtension extension) + { + this.midiOut = extension.midiOut; + this.hardwareSurface = extension.hardwareSurface; + this.host = extension.getHost(); + + createHardware(extension); + createSequencerInput(extension.midiIn); + createBankSwitchListener(extension.midiIn); + } + + /** + * Sets the active bank to the desired index. + * + * @param index Desired bank index. + */ + public void setActiveBank(int index) + { + assert index > 0 && index < NUM_BANKS : "index is invalid"; + midiOut.sendMidi(ShortMidiMessage.CONTROL_CHANGE + MidiChannel.SYSTEM, index, 127); + showBankChangePopup(index); + } + + /** + * Enables/disables any popup notifications related to Twister activity. + * + * @param enabled True to enable, false to disable. + */ + public void setPopupEnabled(boolean enabled) + { + popupEnabled = enabled; + } + + /** Turns off all lights on the Twister. */ + public void lightsOff() + { + // Helps with "stuck" lights when quitting Bitwig + hardwareSurface.updateHardware(); + + for (Twister.Bank bank : banks) { + for (TwisterKnob knob : bank.knobs) { + knob.lightsOff(); + } + } + } + + /** Creates all the hardware for the Twister. */ + private void createHardware(TwisterSisterExtension extension) + { + final int sideButtonsFirstLeftCC = 8; + final int sideButtonsFirstRightCC = sideButtonsFirstLeftCC + Twister.Bank.NUM_LEFT_SIDE_BUTTONS; + + for (int bank = 0; bank < banks.length; ++bank) { + banks[bank] = new Bank(); + + TwisterKnob[] hardwareKnobs = banks[bank].knobs; + TwisterButton[] hardwareLSBs = banks[bank].leftSideButtons; + TwisterButton[] hardwareRSBs = banks[bank].rightSideButtons; + + final int knobsBankOffset = Twister.Bank.NUM_KNOBS * bank; + final int sideButtonsBankOffset = Twister.Bank.NUM_SIDE_BUTTONS * bank; + + for (int knob = 0; knob < hardwareKnobs.length; ++knob) { + final int cc = knob + knobsBankOffset; + final MidiInfo encoder = new MidiInfo(MidiChannel.ENCODER, cc); + final MidiInfo button = new MidiInfo(MidiChannel.BUTTON, cc); + final LightMidiInfo rgbLight = new LightMidiInfo(MidiChannel.BUTTON, + MidiChannel.RGB_ANIMATION, cc); + final LightMidiInfo ringLight = new LightMidiInfo(MidiChannel.ENCODER, + MidiChannel.RING_ANIMATION, cc); + final KnobMidiInfo knobInfo = new KnobMidiInfo(encoder, button, rgbLight, ringLight); + + hardwareKnobs[knob] = new TwisterKnob(extension, knobInfo); + } + + for (int button = 0; button < hardwareLSBs.length; ++button) { + final int cc = sideButtonsFirstLeftCC + button + sideButtonsBankOffset; + final MidiInfo midiInfo = new MidiInfo(MidiChannel.SIDE_BUTTON, cc); + + hardwareLSBs[button] = new TwisterButton(extension, midiInfo, "Side"); + } + + for (int button = 0; button < hardwareRSBs.length; ++button) { + final int cc = sideButtonsFirstRightCC + button + sideButtonsBankOffset; + final MidiInfo midiInfo = new MidiInfo(MidiChannel.SIDE_BUTTON, cc); + + hardwareRSBs[button] = new TwisterButton(extension, midiInfo, "Side"); + } + } + } + + /** + * Create a listener for bank switch messages. Shows popup notification on bank switch if popups + * are enabled. + */ + private void createBankSwitchListener(MidiIn midiIn) + { + midiIn.setMidiCallback((status, data1, data2) -> { + // Filter everything except bank change messages + if (status != 0xB3 || data1 > 3 || data2 != 0x7F) { + return; + } + + showBankChangePopup(data1); + }); + } + + /** Creates a note input for the sequencer channel on the Twister. */ + private void createSequencerInput(MidiIn midiIn) + { + midiIn.createNoteInput("Sequencer", "?7????"); + } + + /** Shows a bank change popup notification if notifications are enabled. */ + private void showBankChangePopup(int index) + { + if (popupEnabled) { + host.showPopupNotification("Twister Bank " + (index + 1)); + } + } +} diff --git a/src/main/java/io/github/dozius/twister/TwisterButton.java b/src/main/java/io/github/dozius/twister/TwisterButton.java new file mode 100644 index 0000000..c1e39e7 --- /dev/null +++ b/src/main/java/io/github/dozius/twister/TwisterButton.java @@ -0,0 +1,230 @@ +/* + * Copyright 2021 Dan Smith + * + * This file is part of Twister Sister. + * + * Twister Sister is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Twister Sister is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with Twister + * Sister. If not, see . + * + */ +package io.github.dozius.twister; + +import java.util.HashSet; +import java.util.Set; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.Timer; + +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.HardwareButton; +import com.bitwig.extension.controller.api.MidiIn; + +import io.github.dozius.TwisterSisterExtension; + +/** + * A button on the Twister. Can be either a knob button or side button. + * + * Provides a clicked, double clicked and long press action that can be observed. + */ +public class TwisterButton +{ + private static final long DOUBLE_CLICK_DURATION = 300 * 1000000; // ms + private static final int LONG_PRESS_DURATION = 250; // ms + private static final int PRESSED_VALUE = 127; + private static final int RELEASED_VALUE = 0; + + private final Set clickedObservers = new HashSet<>(); + private final Set doubleClickedObservers = new HashSet<>(); + private final Set longPressedObservers = new HashSet<>(); + private final Timer longPressTimer; + private final HardwareButton button; + + private long lastReleasedTime; + + /** + * Creates a new TwisterButton. + * + * @param extension The parent Bitwig extension. + * @param midiInfo MIDI info for the button. + * @param idPrefix A prefix string to use when creating the hardware object ID. + */ + public TwisterButton(TwisterSisterExtension extension, MidiInfo midiInfo, String idPrefix) + { + assert !idPrefix.isEmpty() : "ID prefix is empty"; + + final MidiIn midiIn = extension.midiIn; + final ControllerHost host = extension.getHost(); + final int channel = midiInfo.channel; + final int cc = midiInfo.cc; + + button = extension.hardwareSurface.createHardwareButton(idPrefix + " Button " + midiInfo.cc); + + button.pressedAction() + .setActionMatcher(midiIn.createCCActionMatcher(channel, cc, PRESSED_VALUE)); + + button.releasedAction() + .setActionMatcher(midiIn.createCCActionMatcher(channel, cc, RELEASED_VALUE)); + + button.pressedAction() + .setBinding(host.createAction(this::handlePressed, () -> "Handle button pressed")); + + button.releasedAction() + .setBinding(host.createAction(this::handleReleased, () -> "Handle button released")); + + longPressTimer = new Timer(LONG_PRESS_DURATION, new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) + { + notifyLongPressedObservers(); + } + }); + + longPressTimer.setRepeats(false); + } + + /** + * Sets an observer of the double clicked action. This will then be the only observer. + * + * A convenience function that clears and then adds the observer. + * + * @param observer Observer to add. + */ + public void setDoubleClickedObserver(Runnable observer) + { + doubleClickedObservers.clear(); + doubleClickedObservers.add(observer); + } + + /** + * Adds an observer of the double clicked action. + * + * @param observer Observer to add. + * + * @return True if the observer was already added. + */ + public boolean addDoubleClickedObserver(Runnable observer) + { + return doubleClickedObservers.add(observer); + } + + /** Clears all observers of the double clicked action. */ + public void clearDoubleClickedObservers() + { + doubleClickedObservers.clear(); + } + + /** + * Sets an observer of the clicked action. This will then be the only observer. + * + * A convenience function that clears and then adds the observer. + * + * @param observer Observer to add. + */ + public boolean setClickedObserver(Runnable observer) + { + clickedObservers.clear(); + return clickedObservers.add(observer); + } + + /** + * Adds an observer of the clicked action. + * + * @param observer Observer to add. + * + * @return True if the observer was already added. + */ + public boolean addClickedObserver(Runnable observer) + { + return clickedObservers.add(observer); + } + + /** Clears all observers of the clicked action. */ + public void clearClickedObservers() + { + clickedObservers.clear(); + } + + /** + * Sets an observer of the long pressed action. This will then be the only observer. + * + * A convenience function that clears and then adds the observer. + * + * @param observer Observer to add. + */ + public boolean setLongPressedObserver(Runnable observer) + { + longPressedObservers.clear(); + return longPressedObservers.add(observer); + } + + /** + * Adds an observer of the long pressed action. + * + * @param observer Observer to add. + * + * @return True if the observer was already added. + */ + public boolean addLongPressedObserver(Runnable observer) + { + return longPressedObservers.add(observer); + } + + /** Clears all observers of the long pressed action. */ + public void clearLongPressedObserver() + { + longPressedObservers.clear(); + } + + /** Internal handler of the hardware pressed action. */ + private void handlePressed() + { + longPressTimer.start(); + } + + /** Internal handler of the hardware released action. */ + private void handleReleased() + { + if (longPressTimer.isRunning()) { + longPressTimer.stop(); + notifyClickedObservers(); + + long now = System.nanoTime(); + + if ((now - lastReleasedTime) < DOUBLE_CLICK_DURATION) { + notifyDoubleClickedObservers(); + } + + lastReleasedTime = now; + } + } + + private void notifyClickedObservers() + { + for (Runnable observer : clickedObservers) { + observer.run(); + } + } + + private void notifyDoubleClickedObservers() + { + for (Runnable observer : doubleClickedObservers) { + observer.run(); + } + } + + private void notifyLongPressedObservers() + { + for (Runnable observer : longPressedObservers) { + observer.run(); + } + } +} diff --git a/src/main/java/io/github/dozius/twister/TwisterColors.java b/src/main/java/io/github/dozius/twister/TwisterColors.java new file mode 100644 index 0000000..6b76f8e --- /dev/null +++ b/src/main/java/io/github/dozius/twister/TwisterColors.java @@ -0,0 +1,194 @@ +/* + * Copyright 2021 Dan Smith + * + * This file is part of Twister Sister. + * + * Twister Sister is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Twister Sister is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with Twister + * Sister. If not, see . + * + */ +package io.github.dozius.twister; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import com.bitwig.extension.api.Color; + +/** + * Colors from the Twister source code. + * + * Index corresponds to MIDI values 0 to 127. + * + * Note that MIDI values 0 and 127 are special values and won't set the device to the color in the + * list. Value 0 resets the color override and changes back to the "off color" that was set in the + * MIDI Fighter Utility. Value 127 sets the color to the "on color" that was set in the MIDI Fighter + * Utility. + * + * The manual states that 126 should be an override color, but this not the case due to a bug in the + * Twister software. https://github.com/DJ-TechTools/Midi_Fighter_Twister_Open_Source/issues/9 + */ +public class TwisterColors +{ + // All 128 colors from the Twister source code + public static final List ALL = Collections.unmodifiableList(colorList()); + + // Colors settable on the Twister that match the Bitwig device remote parameter colors + public static final List BITWIG_PARAMETERS = Collections.unmodifiableList(parameterColorList()); + + /** Generates the list of all Twister colors. */ + private static List colorList() + { + return Arrays.asList(Color.fromRGB255(0, 0, 0), // 0 + Color.fromRGB255(0, 0, 255), // 1 - Blue + Color.fromRGB255(0, 21, 255), // 2 - Blue (Green Rising) + Color.fromRGB255(0, 34, 255), // + Color.fromRGB255(0, 46, 255), // + Color.fromRGB255(0, 59, 255), // + Color.fromRGB255(0, 68, 255), // + Color.fromRGB255(0, 80, 255), // + Color.fromRGB255(0, 93, 255), // + Color.fromRGB255(0, 106, 255), // + Color.fromRGB255(0, 119, 255), // + Color.fromRGB255(0, 127, 255), // + Color.fromRGB255(0, 140, 255), // + Color.fromRGB255(0, 153, 255), // + Color.fromRGB255(0, 165, 255), // + Color.fromRGB255(0, 178, 255), // + Color.fromRGB255(0, 191, 255), // + Color.fromRGB255(0, 199, 255), // + Color.fromRGB255(0, 212, 255), // + Color.fromRGB255(0, 225, 255), // + Color.fromRGB255(0, 238, 255), // + Color.fromRGB255(0, 250, 255), // 21 - End of Blue's Reign + Color.fromRGB255(0, 255, 250), // 22 - Green (Blue Fading) + Color.fromRGB255(0, 255, 237), // + Color.fromRGB255(0, 255, 225), // + Color.fromRGB255(0, 255, 212), // + Color.fromRGB255(0, 255, 199), // + Color.fromRGB255(0, 255, 191), // + Color.fromRGB255(0, 255, 178), // + Color.fromRGB255(0, 255, 165), // + Color.fromRGB255(0, 255, 153), // + Color.fromRGB255(0, 255, 140), // + Color.fromRGB255(0, 255, 127), // + Color.fromRGB255(0, 255, 119), // + Color.fromRGB255(0, 255, 106), // + Color.fromRGB255(0, 255, 93), // + Color.fromRGB255(0, 255, 80), // + Color.fromRGB255(0, 255, 67), // + Color.fromRGB255(0, 255, 59), // + Color.fromRGB255(0, 255, 46), // + Color.fromRGB255(0, 255, 33), // + Color.fromRGB255(0, 255, 21), // + Color.fromRGB255(0, 255, 8), // + Color.fromRGB255(0, 255, 0), // 43 - Green + Color.fromRGB255(12, 255, 0), // 44 - Green/Red Rising + Color.fromRGB255(25, 255, 0), // + Color.fromRGB255(38, 255, 0), // + Color.fromRGB255(51, 255, 0), // + Color.fromRGB255(63, 255, 0), // + Color.fromRGB255(72, 255, 0), // + Color.fromRGB255(84, 255, 0), // + Color.fromRGB255(97, 255, 0), // + Color.fromRGB255(110, 255, 0), // + Color.fromRGB255(123, 255, 0), // + Color.fromRGB255(131, 255, 0), // + Color.fromRGB255(144, 255, 0), // + Color.fromRGB255(157, 255, 0), // + Color.fromRGB255(170, 255, 0), // + Color.fromRGB255(182, 255, 0), // + Color.fromRGB255(191, 255, 0), // + Color.fromRGB255(203, 255, 0), // + Color.fromRGB255(216, 255, 0), // + Color.fromRGB255(229, 255, 0), // + Color.fromRGB255(242, 255, 0), // + Color.fromRGB255(255, 255, 0), // 64 - Green + Red (Yellow) + Color.fromRGB255(255, 246, 0), // 65 - Red, Green Fading + Color.fromRGB255(255, 233, 0), // + Color.fromRGB255(255, 220, 0), // + Color.fromRGB255(255, 208, 0), // + Color.fromRGB255(255, 195, 0), // + Color.fromRGB255(255, 187, 0), // + Color.fromRGB255(255, 174, 0), // + Color.fromRGB255(255, 161, 0), // + Color.fromRGB255(255, 148, 0), // + Color.fromRGB255(255, 135, 0), // + Color.fromRGB255(255, 127, 0), // + Color.fromRGB255(255, 114, 0), // + Color.fromRGB255(255, 102, 0), // + Color.fromRGB255(255, 89, 0), // + Color.fromRGB255(255, 76, 0), // + Color.fromRGB255(255, 63, 0), // + Color.fromRGB255(255, 55, 0), // + Color.fromRGB255(255, 42, 0), // + Color.fromRGB255(255, 29, 0), // + Color.fromRGB255(255, 16, 0), // + Color.fromRGB255(255, 4, 0), // 85 - End Red/Green Fading + Color.fromRGB255(255, 0, 4), // 86 - Red/ Blue Rising + Color.fromRGB255(255, 0, 16), // + Color.fromRGB255(255, 0, 29), // + Color.fromRGB255(255, 0, 42), // + Color.fromRGB255(255, 0, 55), // + Color.fromRGB255(255, 0, 63), // + Color.fromRGB255(255, 0, 76), // + Color.fromRGB255(255, 0, 89), // + Color.fromRGB255(255, 0, 102), // + Color.fromRGB255(255, 0, 114), // + Color.fromRGB255(255, 0, 127), // + Color.fromRGB255(255, 0, 135), // + Color.fromRGB255(255, 0, 148), // + Color.fromRGB255(255, 0, 161), // + Color.fromRGB255(255, 0, 174), // + Color.fromRGB255(255, 0, 186), // + Color.fromRGB255(255, 0, 195), // + Color.fromRGB255(255, 0, 208), // + Color.fromRGB255(255, 0, 221), // + Color.fromRGB255(255, 0, 233), // + Color.fromRGB255(255, 0, 246), // + Color.fromRGB255(255, 0, 255), // 107 - Blue + Red + Color.fromRGB255(242, 0, 255), // 108 - Blue/ Red Fading + Color.fromRGB255(229, 0, 255), // + Color.fromRGB255(216, 0, 255), // + Color.fromRGB255(204, 0, 255), // + Color.fromRGB255(191, 0, 255), // + Color.fromRGB255(182, 0, 255), // + Color.fromRGB255(169, 0, 255), // + Color.fromRGB255(157, 0, 255), // + Color.fromRGB255(144, 0, 255), // + Color.fromRGB255(131, 0, 255), // + Color.fromRGB255(123, 0, 255), // + Color.fromRGB255(110, 0, 255), // + Color.fromRGB255(97, 0, 255), // + Color.fromRGB255(85, 0, 255), // + Color.fromRGB255(72, 0, 255), // + Color.fromRGB255(63, 0, 255), // + Color.fromRGB255(50, 0, 255), // + Color.fromRGB255(38, 0, 255), // + Color.fromRGB255(25, 0, 255), // 126 - Blue-ish + Color.fromRGB255(240, 240, 225) // 127 - White ? + ); + } + + /** Generates the list of Twister colors that match Bitwig parameters. */ + private static List parameterColorList() + { + // Tweaked by eye for closest match to Bitwig parameter colors + return Arrays.asList(ALL.get(86), // RGB(244, 27, 62) + ALL.get(70), // RGB(255, 127, 23) + ALL.get(64), // RGB(252, 235, 35) + ALL.get(51), // RGB(91, 197, 21) + ALL.get(37), // RGB(101, 206, 146) + ALL.get(14), // RGB(92, 168, 238) + ALL.get(111), // RGB(195, 110, 255) + ALL.get(97)); // RGB(255, 84, 176) + } +} diff --git a/src/main/java/io/github/dozius/twister/TwisterKnob.java b/src/main/java/io/github/dozius/twister/TwisterKnob.java new file mode 100644 index 0000000..9f2d601 --- /dev/null +++ b/src/main/java/io/github/dozius/twister/TwisterKnob.java @@ -0,0 +1,191 @@ +/* + * Copyright 2021 Dan Smith + * + * This file is part of Twister Sister. + * + * Twister Sister is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Twister Sister is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with Twister + * Sister. If not, see . + * + */ +package io.github.dozius.twister; + +import com.bitwig.extension.api.Color; +import com.bitwig.extension.controller.api.RelativeHardwareKnob; +import com.bitwig.extension.controller.api.DoubleValue; +import com.bitwig.extension.controller.api.HardwareBindable; +import com.bitwig.extension.controller.api.RelativeHardwareControlBinding; + +import io.github.dozius.TwisterSisterExtension; + +/** A twister knob, including encoder, shift encoder, button and lights. */ +public class TwisterKnob +{ + private static final int FULL_ROTATION = 127; + + private final RelativeHardwareKnob knob; + private final TwisterButton button; + private final TwisterRGBLight rgbLight; + private final TwisterRingLight ringLight; + private final RelativeHardwareKnob shiftKnob; + private final TwisterRingLight shiftRingLight; + + private boolean isFineSensitivity = false; + private double fineSensitivity = 0.25; + private double sensitivity = 1.0; + + /** + * Creates a new TwisterKnob. + * + * @param extension The parent Bitwig extension. + * @param midiInfo MIDI info for the knob. + */ + public TwisterKnob(TwisterSisterExtension extension, KnobMidiInfo midiInfo) + { + this(extension, midiInfo, Color.nullColor()); + } + + /** + * Creates a new TwisterKnob and sets the RGB light color. + * + * @param extension The parent Bitwig extension. + * @param midiInfo MIDI info for the knob. + * @param color Desired light color. + */ + public TwisterKnob(TwisterSisterExtension extension, KnobMidiInfo midiInfo, Color color) + { + final int cc = midiInfo.encoder.cc; + final int channel = midiInfo.encoder.channel; + + knob = extension.hardwareSurface.createRelativeHardwareKnob("Knob " + cc); + knob.setAdjustValueMatcher(extension.midiIn.createRelativeBinOffsetCCValueMatcher(channel, cc, + FULL_ROTATION)); + + button = new TwisterButton(extension, midiInfo.button, "Knob"); + rgbLight = new TwisterRGBLight(extension, midiInfo.rgbLight, color); + ringLight = new TwisterRingLight(extension, midiInfo.ringLight); + + final int shiftChannel = Twister.MidiChannel.SHIFT; + + shiftKnob = extension.hardwareSurface.createRelativeHardwareKnob("Shift Knob " + cc); + shiftKnob.setAdjustValueMatcher(extension.midiIn.createRelativeBinOffsetCCValueMatcher(shiftChannel, + cc, + FULL_ROTATION)); + shiftRingLight = new TwisterRingLight(extension, + new LightMidiInfo(shiftChannel, + midiInfo.ringLight.animation.channel, + cc)); + } + + /** The associated button for this knob. */ + public TwisterButton button() + { + return button; + } + + /** The associated RGB light for this knob. */ + public TwisterRGBLight rgbLight() + { + return rgbLight; + } + + /** The associated ring light for this knob. */ + public TwisterRingLight ringLight() + { + return ringLight; + } + + /** The associated shift ring light for this knob. */ + public TwisterRingLight shiftRingLight() + { + return shiftRingLight; + } + + /** The value of the target that this knob has been bound to (0-1). */ + public DoubleValue targetValue() + { + return knob.targetValue(); + } + + /** The value of the target that this shift knob has been bound to (0-1). */ + public DoubleValue shiftTargetValue() + { + return shiftKnob.targetValue(); + } + + /** + * Sets the regular sensitivity factor for the knob. If regular sensitivity is active it is + * applied immediately. + * + * @param factor The sensitivity factor to apply. + */ + public void setSensitivity(double factor) + { + sensitivity = factor; + + if (!isFineSensitivity) { + knob.setSensitivity(sensitivity); + } + } + + /** + * Sets the fine sensitivity factor for the knob. If fine sensitivity is active it is applied + * immediately. + * + * @param factor The sensitivity factor to apply. + */ + public void setFineSensitivity(double factor) + { + fineSensitivity = factor; + + if (isFineSensitivity) { + knob.setSensitivity(fineSensitivity); + } + } + + /** Toggles between regular and fine sensitivity. */ + public void toggleSensitivity() + { + isFineSensitivity = !isFineSensitivity; + knob.setSensitivity(isFineSensitivity ? fineSensitivity : sensitivity); + } + + /** + * Binds the knob to the supplied target. + * + * @param target Target to bind. + * + * @return The created binding. + */ + public RelativeHardwareControlBinding setBinding(HardwareBindable target) + { + return knob.setBinding(target); + } + + /** + * Binds the shift knob to the supplied target. + * + * @param target Target to bind. + * + * @return The created binding. + */ + public RelativeHardwareControlBinding setShiftBinding(HardwareBindable target) + { + return shiftKnob.setBinding(target); + } + + /** Resets all animations and turns off all lights. */ + public void lightsOff() + { + ringLight.lightOff(); + shiftRingLight.lightOff(); + rgbLight.lightOff(); + } +} diff --git a/src/main/java/io/github/dozius/twister/TwisterLight.java b/src/main/java/io/github/dozius/twister/TwisterLight.java new file mode 100644 index 0000000..43057de --- /dev/null +++ b/src/main/java/io/github/dozius/twister/TwisterLight.java @@ -0,0 +1,147 @@ +/* + * Copyright 2021 Dan Smith + * + * This file is part of Twister Sister. + * + * Twister Sister is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Twister Sister is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with Twister + * Sister. If not, see . + * + */ +package io.github.dozius.twister; + +import java.util.EnumMap; + +import com.bitwig.extension.controller.api.MidiOut; + +import io.github.dozius.TwisterSisterExtension; +import io.github.dozius.util.MathUtil; + +/** Twister light base class. */ +public abstract class TwisterLight +{ + /** The available animation states of the light. */ + public static enum AnimationState + { + OFF, // animation off + STROBE_8_1, // 8/1 + STROBE_4_1, // 4/1 + STROBE_2_1, // 2/1 + STROBE_1_1, // 1/1 + STROBE_1_2, // 1/2 + STROBE_1_4, // 1/4 + STROBE_1_8, // 1/8 + STROBE_1_16, // 1/16 + PULSE_8_1, // 8/1 + PULSE_4_1, // 4/1 + PULSE_2_1, // 2/1 + PULSE_1_1, // 1/1 + PULSE_1_2, // 1/2 + PULSE_1_4, // 1/4 + PULSE_1_8, // 1/8 + PULSE_1_16, // 1/16 + RAINBOW; + } + + protected final MidiOut midiOut; + + private final MidiInfo midiInfo; + private final EnumMap animationMap; + private final int brightnessStartValue; + + /** + * Creates a new TwisterLight. + * + * @param extension The parent Bitwig extension. + * @param animationMidiInfo MIDI information for animations. + * @param animationStartValue Animation range starting value. + * @param brightnessStartValue Brightness range starting value. + */ + public TwisterLight(TwisterSisterExtension extension, MidiInfo animationMidiInfo, + int animationStartValue, int brightnessStartValue) + { + this.midiOut = extension.midiOut; + this.midiInfo = animationMidiInfo; + this.animationMap = createAnimationMap(animationStartValue); + this.brightnessStartValue = brightnessStartValue; + } + + /** + * Sets the desired animation state. + * + * @param state Animation state. + */ + public void setAnimationState(AnimationState state) + { + assert animationMap.containsKey(state) : "Invalid state"; + sendAnimation(animationMap.get(state)); + } + + /** + * Overrides the brightness setting with a new brightness value. + * + * @param brightness The desired brightness. + */ + public void overrideBrightness(double brightness) + { + final double value = MathUtil.clamp(brightness, 0.0, 1.0); + sendAnimation((int) ((value * 30.0) + brightnessStartValue)); + } + + /** Resets the brightness override. */ + public void resetBrightness() + { + sendAnimation(0); + } + + /** Turns the light off and resets all animations. */ + public abstract void lightOff(); + + /** + * Generates the animation value map based on the start value. + * + * The ring light and RGB light have different starting MIDI values for the animations. By + * providing the starting offset value, the sub class will create the correct value map for + * itself. + * + * @param startValue Starting value of the animation range. + * + * @return A populated animation value map. + */ + private static EnumMap createAnimationMap(int startValue) + { + EnumMap map = new EnumMap<>(AnimationState.class); + + map.put(AnimationState.OFF, 0); + map.put(AnimationState.RAINBOW, 127); + + int i = startValue; + for (AnimationState state : AnimationState.values()) { + if (state == AnimationState.OFF || state == AnimationState.RAINBOW) { + continue; + } + + map.put(state, i); + ++i; + } + + return map; + } + + /** + * Sends the raw animation MIDI value to the device. + * + * @param value The raw MIDI value. + */ + private void sendAnimation(int value) + { + midiOut.sendMidi(midiInfo.statusByte, midiInfo.cc, value); + } +} diff --git a/src/main/java/io/github/dozius/twister/TwisterRGBLight.java b/src/main/java/io/github/dozius/twister/TwisterRGBLight.java new file mode 100644 index 0000000..b41e68d --- /dev/null +++ b/src/main/java/io/github/dozius/twister/TwisterRGBLight.java @@ -0,0 +1,232 @@ +/* + * Copyright 2021 Dan Smith + * + * This file is part of Twister Sister. + * + * Twister Sister is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Twister Sister is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with Twister + * Sister. If not, see . + * + */ +package io.github.dozius.twister; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +import com.bitwig.extension.api.Color; +import com.bitwig.extension.controller.api.HardwareLightVisualState; +import com.bitwig.extension.controller.api.InternalHardwareLightState; +import com.bitwig.extension.controller.api.MidiOut; +import com.bitwig.extension.controller.api.MultiStateHardwareLight; + +import io.github.dozius.TwisterSisterExtension; + +/** The RGB light on a twister knob. */ +public class TwisterRGBLight extends TwisterLight +{ + private static final int ANIMATION_START_VALUE = 1; + private static final int BRIGHTNESS_START_VALUE = 17; + + private final MultiStateHardwareLight light; + private final MidiInfo midiInfo; + + /** + * Creates a new TwisterRGBLight. + * + * @param extension The parent Bitwig extension. + * @param lightMidiInfo MIDI information for the light. + */ + public TwisterRGBLight(TwisterSisterExtension extension, LightMidiInfo lightMidiInfo) + { + this(extension, lightMidiInfo, Color.nullColor()); + } + + /** + * Creates a new TwisterRGBLight and sets the color. + * + * @param extension The parent Bitwig extension. + * @param lightMidiInfo MIDI information for the light. + * @param color Color to set the light. + */ + public TwisterRGBLight(TwisterSisterExtension extension, LightMidiInfo lightMidiInfo, Color color) + { + super(extension, lightMidiInfo.animation, ANIMATION_START_VALUE, BRIGHTNESS_START_VALUE); + + midiInfo = lightMidiInfo.light; + + light = extension.hardwareSurface.createMultiStateHardwareLight("RGB Light " + midiInfo.cc); + + light.setColorToStateFunction(col -> new LightState(col)); + light.state().onUpdateHardware(new LightStateSender(midiOut, midiInfo)); + light.setColor(color); + } + + /** + * Sets the color of the light. + * + * @param color Desired color. + */ + public void setColor(Color color) + { + light.setColor(color); + } + + /** + * Sets the color of the light using a raw MIDI value. + * + * @param value Desired color as a MIDI value. + */ + public void setRawValue(int value) + { + light.setColor(TwisterColors.ALL.get(value)); + } + + /** + * Sets the color supplier for the light. + * + * @param colorSupplier Color supplier for the light. + */ + public void setColorSupplier(Supplier colorSupplier) + { + light.setColorSupplier(colorSupplier); + } + + @Override + public void lightOff() + { + setAnimationState(AnimationState.OFF); + + light.setColor(Color.blackColor()); + + // Force MIDI to be sent immediately + midiOut.sendMidi(midiInfo.statusByte, midiInfo.cc, 0); + } + + /** Handler of internal light state. */ + private class LightState extends InternalHardwareLightState + { + private int colorIndex = 0; + private Color color = Color.nullColor(); + + /** + * Creates a LightState with the desired color. + * + * @param color Desired color. + */ + public LightState(Color color) + { + colorToState(color); + } + + @Override + public HardwareLightVisualState getVisualState() + { + return HardwareLightVisualState.createForColor(color); + } + + @Override + public boolean equals(final Object obj) + { + if (this == obj) { + return true; + } + + if (obj == null) { + return false; + } + + if (getClass() != obj.getClass()) { + return false; + } + + return colorIndex == ((LightState) obj).getColorIndex(); + } + + /** + * Converts a color to the the nearest representable color of the twister. + * + * @param color Desired color. + */ + public void colorToState(Color color) + { + // Find if an exact match exists and use this MIDI value if it does + int existingIndex = TwisterColors.ALL.indexOf(color); + + if (existingIndex >= 0) { + colorIndex = existingIndex; + this.color = color; + return; + } + + // No exact match found, proceed with approximation + final int r = color.getRed255(); + final int g = color.getGreen255(); + final int b = color.getBlue255(); + + // hue = 0, saturation = 1, brightness = 2 + final float[] hsb = java.awt.Color.RGBtoHSB(r, g, b, null); + final float hue = hsb[0]; + final float saturation = hsb[1]; + + /* + * 0 turns off the color override and returns to the inactive color set via sysex. Both 126 & + * 127 enable override but set the color to the active color set via sysex. Seems that the + * inclusion of 126 for this behaviour is a bug. + * + * ref: process_sw_rgb_update() in encoders.c + */ + if (saturation > 0.0) { + final double baseSaturation = 2.0 / 3.0; // RGB 0000FF + colorIndex = Math.min(Math.floorMod((int) (125 * (baseSaturation - hue) + 1), 126), 125); + this.color = color; + } + else { + colorIndex = 0; // Desaturated colors turn off LED + this.color = Color.blackColor(); + } + } + + /** @return Twister color index of the current color state. */ + public int getColorIndex() + { + return colorIndex; + } + } + + /** Consumer that sends the light state to the twister. */ + private class LightStateSender implements Consumer + { + private final MidiOut midiOut; + private final MidiInfo midiInfo; + + /** + * Creates a LightStateSender. + * + * @param midiOut MIDI output port to use. + * @param midiInfo MIDI info for the light. + */ + protected LightStateSender(final MidiOut midiOut, final MidiInfo midiInfo) + { + super(); + this.midiOut = midiOut; + this.midiInfo = midiInfo; + } + + @Override + public void accept(final LightState state) + { + if (state == null) { + return; + } + + midiOut.sendMidi(midiInfo.statusByte, midiInfo.cc, state.getColorIndex()); + } + } +} diff --git a/src/main/java/io/github/dozius/twister/TwisterRingLight.java b/src/main/java/io/github/dozius/twister/TwisterRingLight.java new file mode 100644 index 0000000..59821f1 --- /dev/null +++ b/src/main/java/io/github/dozius/twister/TwisterRingLight.java @@ -0,0 +1,124 @@ +/* + * Copyright 2021 Dan Smith + * + * This file is part of Twister Sister. + * + * Twister Sister is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Twister Sister is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with Twister + * Sister. If not, see . + * + */ +package io.github.dozius.twister; + +import com.bitwig.extension.controller.api.DoubleValue; +import com.bitwig.extension.controller.api.SettableRangedValue; + +import io.github.dozius.TwisterSisterExtension; +import io.github.dozius.util.CursorNormalizedValue; +import io.github.dozius.util.MathUtil; + +/** The ring light on a Twister knob. */ +public class TwisterRingLight extends TwisterLight +{ + private static final int ANIMATION_START_VALUE = 49; + private static final int BRIGHTNESS_START_VALUE = 65; + + private final MidiInfo midiInfo; + + /** + * Creates a new TwisterRingLight. + * + * @param extension The parent bitwig extension. + * @param lightMidiInfo Midi information for the light. + */ + public TwisterRingLight(TwisterSisterExtension extension, LightMidiInfo lightMidiInfo) + { + super(extension, lightMidiInfo.animation, ANIMATION_START_VALUE, BRIGHTNESS_START_VALUE); + this.midiInfo = lightMidiInfo.light; + } + + /** + * Sets the ring value using raw MIDI data. + * + * @param value The desired ring value in MIDI. + */ + public void setRawValue(int value) + { + midiOut.sendMidi(midiInfo.statusByte, midiInfo.cc, MathUtil.clamp(value, 0, 127)); + } + + /** + * Sets the ring value using a normalized 0-1 range. + * + * @param value The desired ring value in normalized range. + */ + public void setValue(double value) + { + setRawValue((int) (value * 127.0)); + } + + /** + * A special normalized 0-1 range value for ring lights being used as cursors in "dot" mode. + * + * Intended to be used with the value from a CursorNormalizedValue wrapper. + * + * Values in the 0-1 range will show the dot, even when at 0. Negative values hide the dot. + * + * @param value The cursor value to set. + */ + public void setCursorValue(double value) + { + if (value < 0) { + setRawValue(0); + return; + } + + setRawValue((int) ((value * 126.0) + 1.0)); + } + + /** + * Makes this light an observer of the passed in value. + * + * @param value The value to observe. + */ + public void observeValue(SettableRangedValue value) + { + value.markInterested(); + value.addValueObserver(128, this::setRawValue); + } + + /** + * Makes this light an observer of the passed in value. + * + * @param value The value to observe. + */ + public void observeValue(DoubleValue value) + { + value.markInterested(); + value.addValueObserver(this::setValue); + } + + /** + * Makes this light an observer of the passed in wrapper. + * + * @param wrapper The wrapper to observe. + */ + public void observeValue(CursorNormalizedValue wrapper) + { + wrapper.addValueObserver(this::setCursorValue); + } + + @Override + public void lightOff() + { + setAnimationState(AnimationState.OFF); + setRawValue(0); + } +} diff --git a/src/main/java/io/github/dozius/util/CursorNormalizedValue.java b/src/main/java/io/github/dozius/util/CursorNormalizedValue.java new file mode 100644 index 0000000..f77feb6 --- /dev/null +++ b/src/main/java/io/github/dozius/util/CursorNormalizedValue.java @@ -0,0 +1,134 @@ +/* + * Copyright 2021 Dan Smith + * + * This file is part of Twister Sister. + * + * Twister Sister is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Twister Sister is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with Twister + * Sister. If not, see . + * + */ +package io.github.dozius.util; + +import java.util.HashSet; +import java.util.Set; + +import com.bitwig.extension.callback.DoubleValueChangedCallback; +import com.bitwig.extension.controller.api.CursorDevice; +import com.bitwig.extension.controller.api.CursorRemoteControlsPage; +import com.bitwig.extension.controller.api.CursorTrack; +import com.bitwig.extension.controller.api.DeviceBank; +import com.bitwig.extension.controller.api.TrackBank; + +/** + * Wraps a cursor and bank in order to provide a normalized 0-1 value for the cursor position. + * + * Sends -1.0 if there are no items in the bank. + */ +public class CursorNormalizedValue +{ + private final Set observers = new HashSet<>(); + private int cursorIndex = -1; + private int cursorCount = -1; + + /** + * Creates a wrapper for a track cursor. + * + * @param cursorTrack The track cursor to wrap. + * @param trackBank The bank for the cursor. + */ + public CursorNormalizedValue(CursorTrack cursorTrack, TrackBank trackBank) + { + trackBank.channelCount().markInterested(); + trackBank.channelCount().addValueObserver(this::setCursorCount); + + cursorTrack.position().markInterested(); + cursorTrack.position().addValueObserver(this::setCursorIndex); + } + + /** + * Creates a wrapper for a device cursor. + * + * @param cursorDevice The device cursor to wrap. + * @param deviceBank The bank for the cursor. + */ + public CursorNormalizedValue(CursorDevice cursorDevice, DeviceBank deviceBank) + { + deviceBank.itemCount().markInterested(); + deviceBank.itemCount().addValueObserver(this::setCursorCount); + + cursorDevice.position().markInterested(); + cursorDevice.position().addValueObserver(this::setCursorIndex); + } + + /** + * Creates a wrapper for remote controls pages. + * + * @param cursorRemoteControlsPage The remote control pages to wrap. + */ + public CursorNormalizedValue(CursorRemoteControlsPage cursorRemoteControlsPage) + { + cursorRemoteControlsPage.pageCount().markInterested(); + cursorRemoteControlsPage.pageCount().addValueObserver(this::setCursorCount); + + cursorRemoteControlsPage.selectedPageIndex().markInterested(); + cursorRemoteControlsPage.selectedPageIndex().addValueObserver(this::setCursorIndex); + } + + /** + * Adds and observer for the wrapped value. + * + * @param callback The observer callback. + * + * @return True if this set did not already contain the specified element. + */ + public Boolean addValueObserver(DoubleValueChangedCallback callback) + { + return observers.add(callback); + } + + /** Handles when the cursor index changes. */ + private void setCursorIndex(int index) + { + cursorIndex = index; + updateCursorFeedback(); + } + + /** Handles when the bank item count changes. */ + private void setCursorCount(int count) + { + cursorCount = count; + updateCursorFeedback(); + } + + /** Generates the normalized value and notifies observers. */ + private void updateCursorFeedback() + { + if (cursorCount < 1 || cursorIndex < 0) { + notifyObservers(-1); + return; + } + + if (cursorCount < 2) { + notifyObservers(0); + return; + } + + final double normalized = cursorIndex / (cursorCount - 1.0); + notifyObservers(MathUtil.clamp(normalized, 0.0, 1.0)); + } + + private void notifyObservers(double value) + { + for (DoubleValueChangedCallback observer : observers) { + observer.valueChanged(value); + } + } +} diff --git a/src/main/java/io/github/dozius/util/DevUtil.java b/src/main/java/io/github/dozius/util/DevUtil.java new file mode 100644 index 0000000..75d0dc0 --- /dev/null +++ b/src/main/java/io/github/dozius/util/DevUtil.java @@ -0,0 +1,57 @@ +/* + * Copyright 2021 Dan Smith + * + * This file is part of Twister Sister. + * + * Twister Sister is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Twister Sister is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with Twister + * Sister. If not, see . + * + */ +package io.github.dozius.util; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import com.bitwig.extension.controller.api.Action; +import com.bitwig.extension.controller.api.ActionCategory; +import com.bitwig.extension.controller.api.Application; +import com.bitwig.extension.controller.api.ControllerHost; + +/** Extension development utilities. */ +public class DevUtil +{ + /** + * Dumps all the available Bitwig actions to a text file. + * + * @param host The extension controller host. + * @param outputFile The path of the output file. + * + * @throws IOException + */ + public static void dumpBitwigActions(ControllerHost host, Path outputFile) throws IOException + { + final Application app = host.createApplication(); + final List output = new ArrayList<>(); + + for (ActionCategory actionCat : app.getActionCategories()) { + output.add(actionCat.getName()); + for (Action action : actionCat.getActions()) { + output.add("--- ID: " + action.getId() + " Name: " + action.getName()); + } + } + + Files.write(outputFile, output, StandardCharsets.UTF_8); + } +} diff --git a/src/main/java/io/github/dozius/util/Logger.java b/src/main/java/io/github/dozius/util/Logger.java new file mode 100644 index 0000000..42ff60a --- /dev/null +++ b/src/main/java/io/github/dozius/util/Logger.java @@ -0,0 +1,62 @@ +/* + * Copyright 2021 Dan Smith + * + * This file is part of Twister Sister. + * + * Twister Sister is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Twister Sister is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with Twister + * Sister. If not, see . + * + */ +package io.github.dozius.util; + +import com.bitwig.extension.controller.api.ControllerHost; + +/** Global logger intended for debugging. */ +public final class Logger +{ + private static ControllerHost host; + + private Logger() + { + } + + /** + * Initialize the Logger. Must be called before any logging can happen. + * + * @param controllerHost The extension controller host. + */ + public static void init(ControllerHost controllerHost) + { + host = controllerHost; + } + + /** + * Logs a message to the Bitwig console with a timestamp. + * + * @param message The message to log. + */ + public static void logTimeStamped(String message) + { + log("[" + java.time.LocalTime.now().toString() + "] " + message); + } + + /** + * Logs a message to the Bitwig console. + * + * @param message The message to log. + */ + public static void log(String message) + { + if (host != null) { + host.println(message); + } + } +} diff --git a/src/main/java/io/github/dozius/util/MathUtil.java b/src/main/java/io/github/dozius/util/MathUtil.java new file mode 100644 index 0000000..747ee5a --- /dev/null +++ b/src/main/java/io/github/dozius/util/MathUtil.java @@ -0,0 +1,68 @@ +/* + * Copyright 2021 Dan Smith + * + * This file is part of Twister Sister. + * + * Twister Sister is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Twister Sister is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with Twister + * Sister. If not, see . + * + */ +package io.github.dozius.util; + +/** + * Generic math utilities. + */ +public class MathUtil +{ + // clamp - note we don't use generics here to avoid boxing/unboxing + + /** + * Clamps the value to be between min and max. + * + * @param val Value to clamp. + * @param min Clamp min. + * @param max Clamp max. + * + * @return The clamped value. + */ + public static float clamp(float val, float min, float max) + { + return Math.max(min, Math.min(max, val)); + } + + /** + * Clamps the value to be between min and max. + * + * @param val Value to clamp. + * @param min Clamp min. + * @param max Clamp max. + * + * @return The clamped value. + */ + public static int clamp(int val, int min, int max) + { + return Math.max(min, Math.min(max, val)); + } + + /** + * Clamps the value to be between min and max. + * + * @param val Value to clamp. + * @param min Clamp min. + * @param max Clamp max. + * + * @return The clamped value. + */ + public static double clamp(double val, double min, double max) + { + return Math.max(min, Math.min(max, val)); + } +} diff --git a/src/main/java/io/github/dozius/util/OnOffColorSupplier.java b/src/main/java/io/github/dozius/util/OnOffColorSupplier.java new file mode 100644 index 0000000..94143b7 --- /dev/null +++ b/src/main/java/io/github/dozius/util/OnOffColorSupplier.java @@ -0,0 +1,95 @@ +/* + * Copyright 2021 Dan Smith + * + * This file is part of Twister Sister. + * + * Twister Sister is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * Twister Sister is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with Twister + * Sister. If not, see . + * + */ +package io.github.dozius.util; + +import java.util.function.Supplier; + +import com.bitwig.extension.api.Color; + +/** A color supplier that can toggle between two color states, an "on" color and "off" color. */ +public class OnOffColorSupplier implements Supplier +{ + private Color onColor; + private Color offColor; + private boolean isOn; + + /** + * Constructs a new OnOffColorSupplier. + * + * @param onColor The color to use for the "on" state. + * @param offColor The color to use for the "off" state. + */ + public OnOffColorSupplier(Color onColor, Color offColor) + { + this.onColor = onColor; + this.offColor = offColor; + } + + /** + * Constructs a new OnOffColorSupplier with the "off" state set to black. + * + * @param onColor The color to use for the "on" state. + */ + public OnOffColorSupplier(Color onColor) + { + this(onColor, Color.blackColor()); + } + + /** Constructs a new OnOffColorSupplier with the "on" and "off" state set to black. */ + public OnOffColorSupplier() + { + this(Color.blackColor(), Color.blackColor()); + } + + /** + * Sets the color for the "on" state. + * + * @param onColor The desired color. + */ + public void setOnColor(Color onColor) + { + this.onColor = onColor; + } + + /** + * Sets the color for the "off" state. + * + * @param offColor The desired color. + */ + public void setOffColor(Color offColor) + { + this.offColor = offColor; + } + + /** + * Sets the state. + * + * @param on "on" if true, "off" if false. + */ + public void setOn(boolean on) + { + this.isOn = on; + } + + /** @return The current state color. */ + @Override + public Color get() + { + return isOn ? onColor : offColor; + } +} diff --git a/src/main/resources/META-INF/services/com.bitwig.extension.ExtensionDefinition b/src/main/resources/META-INF/services/com.bitwig.extension.ExtensionDefinition new file mode 100644 index 0000000..de7f125 --- /dev/null +++ b/src/main/resources/META-INF/services/com.bitwig.extension.ExtensionDefinition @@ -0,0 +1 @@ +io.github.dozius.TwisterSisterExtensionDefinition