diff --git a/.github/workflows/custom.yml b/.github/workflows/custom.yml new file mode 100644 index 000000000..4fdab1d5c --- /dev/null +++ b/.github/workflows/custom.yml @@ -0,0 +1,69 @@ +name: Custom Build + +on: + discussion_comment: + types: [created] + +jobs: + build: + name: Build Custom Firmware + if: contains(github.event.comment.body, '/buildthis') + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + ref: phazerville + + - name: Setup env + env: + GH_COMMENT: ${{ github.event.comment.body }} + run: | + echo "OC_ARTIFACT_TAG=custom_${GITHUB_ACTOR}-$(software/o_c_REV/resources/oc_build_tag.sh)" | tr '/' '_' >> $GITHUB_ENV + echo "CUSTOM_BUILD_FLAGS=$(python software/o_c_REV/resources/parse_build_request.py)" >> $GITHUB_ENV + + - name: Cache PlatformIO + uses: actions/cache@v3 + with: + path: ~/.platformio + key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} + + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.x' + cache: 'pip' + cache-dependency-path: '**/requirements.txt' + + - run: | + pip3 install --upgrade pip + pip3 install -r .github/workflows/requirements.txt + + - name: Build firmware + working-directory: software/o_c_REV/ + run: | + pio run -e custom + + - name: Copy artifact + uses: actions/upload-artifact@v3 + with: + name: o_C-${{env.OC_ARTIFACT_TAG}} + path: software/o_c_REV/.pio/build/*/*.hex + +# TODO: this will only work with Issues or Pull Requests... +# - name: Add follow-up comment to discussion +# uses: actions/github-script@v6 +# if: always() +# with: +# script: | +# const name = '${{ github.workflow }}'; +# const url = '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'; +# const success = '${{ job.status }}' === 'success'; +# const body = `${name}: ${success ? 'succeeded ✅' : 'failed ❌'}\n${url}`; +# +# await github.rest.issues.createComment({ +# issue_number: context.issue.number, +# owner: context.repo.owner, +# repo: context.repo.repo, +# body: body +# }) diff --git a/.github/workflows/firmware.yml b/.github/workflows/firmware.yml new file mode 100644 index 000000000..eea9e3c42 --- /dev/null +++ b/.github/workflows/firmware.yml @@ -0,0 +1,45 @@ +name: PlatformIO CI + +on: + push: + paths: + - 'software/o_c_REV/**' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup env + run: | + echo "OC_ARTIFACT_TAG=${GITHUB_REF_NAME}-$(software/o_c_REV/resources/oc_build_tag.sh)" | tr '/' '_' >> $GITHUB_ENV + + - name: Cache PlatformIO + uses: actions/cache@v3 + with: + path: ~/.platformio + key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} + + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.x' + cache: 'pip' + cache-dependency-path: '**/requirements.txt' + + - run: | + pip3 install --upgrade pip + pip3 install -r .github/workflows/requirements.txt + + - name: Build firmware + working-directory: software/o_c_REV/ + run: | + pio run + + - name: Copy artifact + uses: actions/upload-artifact@v3 + with: + name: o_C-${{env.OC_ARTIFACT_TAG}} + path: software/o_c_REV/.pio/build/*/*.hex diff --git a/.github/workflows/requirements.txt b/.github/workflows/requirements.txt new file mode 100644 index 000000000..7c715125f --- /dev/null +++ b/.github/workflows/requirements.txt @@ -0,0 +1 @@ +platformio diff --git a/.gitignore b/.gitignore index 8ff86f5db..8831f4658 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ shelved/ Hemisphere\ Suite.cpp builds .pio +.vscode diff --git a/API-notes.md b/API-notes.md new file mode 100644 index 000000000..6a7f6ac20 --- /dev/null +++ b/API-notes.md @@ -0,0 +1,79 @@ +API Notes +=== + +### Basics + +The o_C firmware operates with several "concurrent" threads. +(really they interrupt each other I guess) + +There is a main _loop_ as well as an _ISR_ that fires from a timer. +The _loop_ acts as a watchdog while the _ISR_ does all the work. +UI events are on a seperate _ISR_ timer. +The display driver and input polling are also on independent interrupts. +So that's at least 4? separate processes happening in a cascade +for every cycle, which is 60us (nanoseconds) or 16.666khz + +That's the general idea. + +Anyway, we have top-level *Apps* and then there are *Applets* as implemented +in the _Hemisphere_ *App*. An *Applet* inherits the base class _HemisphereApplet_ +and gains the superpowers necessary to live on half of your module's screen. + +### Base Classes + +The two primary interfaces in the Hemisphere API are _HSApplication_ and _HemisphereApplet_. +They both have many similarly named methods for I/O and graphics, with +_HemisphereApplet_ taking extra considerations for offsetting both in the right side. + +All of the hardware details are neatly abstracted under these two interfaces. If you +simply want to make an applet that does some stuff, these APIs are your starting point. + +### Applets + +There are a few different things an Applet must do: +* Controller - the main logic computed every tick (every time the ISR fires) +* View - draw the pixels on the screen (there are many helpful _gfx*_ functions) +* UI Event Handling: + * OnButtonPress - what to do when the encoder button is pressed + * OnEncoderMove - what to do when the encoder rotated +* OnDataRequest / OnDataReceive - how to save / load state + +There is also a `Start()` function for initializing things at runtime, +plus some Help text. That's about it. + +You can easily try it out by copying the `HEM_Boilerplate.ino.txt` file into place, +and then adding your computations to its skeleton. + +### Applet Functions + +Function? or Method? Either way, this is how you do the things. + +#### I/O Functions +The main argument of each is the channel to operate on - each half of the +screen gets 2 channels. So _n_ is typically either 0 or 1. + +**Input**: +* Clock(n) - has the digital input received a new clock pulse? +* Gate(n) - is the digital input held high? +* In(n) - Raw value of the CV input +* DetentedIn(n) - this one reads 0 until it's past a threshold ~ a quartertone + +**Output**: +* ClockOut(n) - hold the output high for a pulse length +* GateOut(n, on_off) - set the output high or low +* Out(n, raw) - set the output to an explicit value + +I've added a standard case function for modulating a parameter with a certain input. +* Modulate(param, n, min, max) - automatically scales the input and modifies param + +#### gfx Functions +There are many strategies for drawing things on the screen, and therefore, many +graphics related functions. You can see them for yourself in `HemisphereApplet.h` +All of them typically take _x_ and _y_ coordinates for the first two arguments, +followed by _width_ and _height_, or another _x,y_ pair. +_x_ is how many pixels from the left edge of the screen. +_y_ is how many pixels from the top edge of the screen. + +Some essentials: *gfxPrint*, *gfxPixel*, *gfxLine*, *gfxCursor*, + *gfxFrame*, *gfxBitmap*, *gfxInvert* + diff --git a/Hemisphere_Suite.ino b/Hemisphere_Suite.ino deleted file mode 100644 index ca7367d5e..000000000 --- a/Hemisphere_Suite.ino +++ /dev/null @@ -1,12 +0,0 @@ -#include "Arduino.h" -//The setup function is called once at startup of the sketch -void setup() -{ -// Add your initialization code here -} - -// The loop function is called in an endless loop -void loop() -{ -//Add your repeated code here -} diff --git a/README.md b/README.md index 2f5c9b913..a91c0e24c 100755 --- a/README.md +++ b/README.md @@ -1,42 +1,37 @@ -Welcome to Benisphere Suite -=== - -## An active fork expanding upon Hemisphere Suite. - -To download and install the latest release, head to [releases](https://github.com/benirose/O_C-BenisphereSuite/releases). +[![PlatformIO CI](https://github.com/djphazer/O_C-BenisphereSuite/actions/workflows/firmware.yml/badge.svg)](https://github.com/djphazer/O_C-BenisphereSuite/actions/workflows/firmware.yml) -To learn more about what makes this fork different than the original Hemisphere Suite, head to [the wiki](https://github.com/benirose/O_C-BenisphereSuite/wiki). - -Benisphere takes the Hemisphere Suite in a new direction, adding new applets and some enhancements to existing ones, while also removing o_C style apps to make space for these changes. +Phazerville Suite with Relabi - an active o_C firmware fork +=== +[![SynthDad's video overview](http://img.youtube.com/vi/XRGlAmz3AKM/0.jpg)](http://www.youtube.com/watch?v=XRGlAmz3AKM "Phazerville; newest firmware for Ornament and Crime. Tutorial and patch ideas") -### An alternate firmware to an alternate firmware?? +Watch SynthDad's **video overview** (above) or check the [**Wiki**](https://github.com/djphazer/O_C-BenisphereSuite/wiki) for more info. [Download it here](https://github.com/djphazer/O_C-BenisphereSuite/releases). -Yes! This fork was an opportunity for me add stuff I wanted to see on my o_C without having to worry about running up against any legacy issues for other Hemipshere users. When I shared some of the progress I was making in my fork, people seemed excited and interested in trying it out, so I have made it an official fork rather than just my own personal project. I try to give back any improvements I make to the [main Hemispheres repo](https://github.com/Chysn/O_C-HemisphereSuite) when possible, but 1) it is not clear if that repo is still active and 2) there is barely any room remaining on the Teensy for improvements. By deciding that this version of the suite will only contain Hemisphere applets I was able to make room for new applets and other improvements. I do accept feature request issues and pull request, but the things that will make it in to this fork are still largely up to my discression. +## An active fork expanding upon Hemisphere Suite. -### Ok, so what's changed? +This is a fork of djphazer's Phazerville Suite which is a fork of Benisphere which is a fork of Hemisphere! The code is firmware for the Ornament & Crime, a Eurorack synth module that can act as many different modules as selected by the user. This firmware ads the Relabi app that generates chaotic but deterministic control voltage. In addition, it adds a function to the HSVectorOscillator.h file and provides a controller file that allows two Relabi apps to be linked, providing four related chaotic LFOs. -As of v1.0, I have added two new applets inspired by very popular modules ([Mutable Instruments Grids](https://mutable-instruments.net/modules/grids/) and [Noise Engineering Mimetic Digitalis](https://noiseengineering.us/products/mimetic-digitalis)) as well as some minor improvements to existing apps. I've also removed all of the full-width o_C style apps to make space for these changes. To see all of the changes in detail, visit the [wiki](https://github.com/benirose/O_C-BenisphereSuite/wiki). +Read more about the suite [here](https://github.com/djphazer/O_C-Phazerville). Please, go there and read about all the people who have contributed code. -### How do I try it? +## What is Relabi. -Head over to the [releases](https://github.com/benirose/O_C-BenisphereSuite/releases) section and download the latest release. Follow the ["Method A" instructions](https://ornament-and-cri.me/firmware/#method_a) from the Ornament and Crime website, except use the hex file you downloaded from the releases section. +Relabi is an alternative method of defining musical time. While rhythm is concerned with recurring pulses, relabi always slips the pulse. You can read all about the concept in [John Berndt's essay]((https://johnberndt.org/relabi/). -Alternatively, you can follow instructions in [this video](https://www.youtube.com/watch?v=dg_acylaMZU). -Note: you can reload any other previous firmware you had installed (stock o_C or Hemisphere Suite) just as easily. -### How do I build it? -You can download this repo and build the code following the ["Method B" instruction](https://ornament-and-cri.me/firmware/#method_b) from the Ornament and Crime website. Very specific legacy versions of the Arduino IDE and Teensyduino add-on are required to build, and are not installable on 64-bit only systems, like Mac OS. You must use an older version (Mojave or before) or a VM to install these versions. +### How To Get It -### What's with the name? +Check the [Releases](https://github.com/djphazer/O_C-BenisphereSuite/releases) section for a .hex file (to be used with the Teensy loader app), or clone the repository and build it yourself! I think the beauty of this module is the fact that it's relatively easy to modify and build the source code to reprogram it. You are free to customize the firmware, similar to how you've no doubt already selected a custom set of physical modules. -Beta tester [@jroo](https://github.com/jroo) jokingly called it that, and it kind of stuck! It's supposed to be a bit tongue in cheek, so I hope it's not taken too seriously! Also it has a nicer ringer than "Hemisphere Suite BR Fork". +### How To Change It -### Credits +This firmware fork is built using Platform IO, a Python-based build toolchain, available as either a [standalone CLI](https://docs.platformio.org/en/latest/core/installation/methods/installer-script.html) or a [full-featured IDE](https://platformio.org/install/ide), as well as a plugin for VSCode and other existing IDEs. -This is a fork of [Hemisphere Suite](https://github.com/Chysn/O_C-HemisphereSuite) by Jason Justian (aka chysn). I could not have built Hemisphere Suite, so a million thanks to Jason for doing the true hard work and keeping it open source. +The project lives within the `software/o_c_REV` directory. From there, you can Build the desired configuration and Upload via USB to your module: +``` +pio run -e oc_stock2_flipped -t upload +``` +Have a look inside `platformio.ini` for alternative build environment configurations - VOR, Buchla, flipped screen, etc. To build all the defaults consecutively, simply use `pio run` -ornament**s** & crime**s** is a collaborative project by Patrick Dowling (aka pld), mxmxmx and Tim Churches (aka bennelong.bicyclist) (though mostly by pld and bennelong.bicyclist). it **(considerably) extends** the original firmware for the o_C / ASR eurorack module, designed by mxmxmx. +_**Pro-tip**_: If you decide to fork the project, and enable GitHub Actions on your own repo, GitHub will build the files for you... ;) -http://ornament-and-cri.me/ diff --git a/TODO.md b/TODO.md new file mode 100644 index 000000000..3a4dd57d8 --- /dev/null +++ b/TODO.md @@ -0,0 +1,34 @@ +TODO +=== + +* Auto-tuner floor/ceiling detection (fail gracefully) +* Move calibration routines to a proper App +* Runtime filtering/hiding of Applets +* Flexible input remapping for Hemisphere + - generalize applet params for assignment +* global quantizer settings in Hemisphere Config +* applet with modal interchange (CV modulation of scale) +* Subharmonicon app/applets +* Update Boilerplates +* add swing/shuffle to internal clock +* Automatic stop for internal Clock? +* Polyphonic MIDI input tracking +* MIDI output for all apps? + +[APP IDEAS] +* QUADRANTS +* Two Spheres +* Snake Game + +[DONE] +* Add auto-tuner to Calibr8or +* ProbMeloD - alternate melody on 2nd output +* Fix FLIP_180 calibration +* Add Clock Setup to Calibr8or +* Calibr8or screensaver +* Pull in Automatonnetz +* Sync-Start for internal Clock +* General Config screen (long-press right button) +* better MIDI input message delegation (event listeners?) +* import alternative grids_resources patterns for DrumMap2 +* Add Root Note to DualTM diff --git a/hardware/OCP/TH_O_C_MAIN0_1_0.pdf b/hardware/OCP/TH_O_C_MAIN0_1_0.pdf new file mode 100644 index 000000000..8c6eff180 Binary files /dev/null and b/hardware/OCP/TH_O_C_MAIN0_1_0.pdf differ diff --git a/hardware/OCP/TH_O_C_UI-1_0.pdf b/hardware/OCP/TH_O_C_UI-1_0.pdf new file mode 100644 index 000000000..f8d5df06d Binary files /dev/null and b/hardware/OCP/TH_O_C_UI-1_0.pdf differ diff --git a/software/o_c_REV/APP_ASR.ino b/software/o_c_REV/APP_ASR.ino new file mode 100644 index 000000000..33ce712aa --- /dev/null +++ b/software/o_c_REV/APP_ASR.ino @@ -0,0 +1,1104 @@ +// Copyright (c) 2014-2017 Max Stadler, Patrick Dowling +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifdef ENABLE_APP_ASR + +#include "util/util_settings.h" +#include "util/util_trigger_delay.h" +#include "util/util_turing.h" +#include "util/util_ringbuffer.h" +#include "util/util_integer_sequences.h" +#include "OC_DAC.h" +#include "OC_menus.h" +#include "OC_scales.h" +#include "OC_scale_edit.h" +#include "OC_strings.h" +#include "OC_visualfx.h" +#include "peaks_bytebeat.h" +#include "extern/dspinst.h" + +namespace menu = OC::menu; // Ugh. This works for all .ino files + +#define NUM_ASR_CHANNELS 0x4 +#define ASR_MAX_ITEMS 256 // = ASR ring buffer size. +#define ASR_HOLD_BUF_SIZE ASR_MAX_ITEMS / NUM_ASR_CHANNELS // max. delay size +#define NUM_INPUT_SCALING 40 // # steps for input sample scaling (sb) + +// CV input gain multipliers +const int32_t multipliers[NUM_INPUT_SCALING] = { + // 0.0 / 2.0 in 0.05 steps + 3277, 6554, 9830, 13107, 16384, 19661, 22938, 26214, 29491, 32768, + 36045, 39322, 42598, 45875, 49152, 52429, 55706, 58982, 62259, 65536, + 68813, 72090, 75366, 78643, 81920, 85197, 88474, 91750, 95027, 98304, + 101581, 104858, 108134, 111411, 114688, 117964, 121242, 124518, 127795, 131072 +}; + +const uint8_t MULT_ONE = 19; // == 65536, see above + +enum ASRSettings { + ASR_SETTING_SCALE, + ASR_SETTING_OCTAVE, + ASR_SETTING_ROOT, + ASR_SETTING_MASK, + ASR_SETTING_INDEX, + ASR_SETTING_MULT, + ASR_SETTING_DELAY, + ASR_SETTING_BUFFER_LENGTH, + ASR_SETTING_CV_SOURCE, + ASR_SETTING_CV4_DESTINATION, + ASR_SETTING_TURING_LENGTH, + ASR_SETTING_TURING_PROB, + ASR_SETTING_TURING_CV_SOURCE, + ASR_SETTING_BYTEBEAT_EQUATION, + ASR_SETTING_BYTEBEAT_P0, + ASR_SETTING_BYTEBEAT_P1, + ASR_SETTING_BYTEBEAT_P2, + ASR_SETTING_BYTEBEAT_CV_SOURCE, + ASR_SETTING_INT_SEQ_INDEX, + ASR_SETTING_INT_SEQ_MODULUS, + ASR_SETTING_INT_SEQ_START, + ASR_SETTING_INT_SEQ_LENGTH, + ASR_SETTING_INT_SEQ_DIR, + ASR_SETTING_FRACTAL_SEQ_STRIDE, + ASR_SETTING_INT_SEQ_CV_SOURCE, + ASR_SETTING_LAST +}; + +enum ASRChannelSource { + ASR_CHANNEL_SOURCE_CV1, + ASR_CHANNEL_SOURCE_TURING, + ASR_CHANNEL_SOURCE_BYTEBEAT, + ASR_CHANNEL_SOURCE_INTEGER_SEQUENCES, + ASR_CHANNEL_SOURCE_LAST +}; + +enum ASR_CV4_DEST { + ASR_DEST_OCTAVE, + ASR_DEST_ROOT, + ASR_DEST_TRANSPOSE, + ASR_DEST_BUFLEN, + ASR_DEST_INPUT_SCALING, + ASR_DEST_LAST +}; + +typedef int16_t ASR_pitch; + +class ASRApp : public settings::SettingsBase { +public: + static constexpr size_t kHistoryDepth = 5; + + int get_scale(uint8_t dummy) const { + return values_[ASR_SETTING_SCALE]; + } + + uint8_t get_buffer_length() const { + return values_[ASR_SETTING_BUFFER_LENGTH]; + } + + void set_scale(int scale) { + if (scale != get_scale(DUMMY)) { + const OC::Scale &scale_def = OC::Scales::GetScale(scale); + uint16_t mask = get_mask(); + if (0 == (mask & ~(0xffff << scale_def.num_notes))) + mask |= 0x1; + apply_value(ASR_SETTING_MASK, mask); + apply_value(ASR_SETTING_SCALE, scale); + } + } + + // dummy + int get_scale_select() const { + return 0; + } + + // dummy + void set_scale_at_slot(int scale, uint16_t mask, int root, int transpose, uint8_t scale_slot) { + + } + + // dummy + int get_transpose(uint8_t DUMMY) const { + return 0; + } + + uint16_t get_mask() const { + return values_[ASR_SETTING_MASK]; + } + + int get_root() const { + return values_[ASR_SETTING_ROOT]; + } + + int get_root(uint8_t DUMMY) const { + return 0x0; // dummy + } + + int get_index() const { + return values_[ASR_SETTING_INDEX]; + } + + int get_octave() const { + return values_[ASR_SETTING_OCTAVE]; + } + + bool octave_toggle() { + octave_toggle_ = (~octave_toggle_) & 1u; + return octave_toggle_; + } + + bool poke_octave_toggle() const { + return octave_toggle_; + } + + int get_mult() const { + return values_[ASR_SETTING_MULT]; + } + + int get_cv_source() const { + return values_[ASR_SETTING_CV_SOURCE]; + } + + uint8_t get_cv4_destination() const { + return values_[ASR_SETTING_CV4_DESTINATION]; + } + + uint8_t get_turing_length() const { + return values_[ASR_SETTING_TURING_LENGTH]; + } + + uint8_t get_turing_display_length() { + return turing_display_length_; + } + + uint8_t get_turing_probability() const { + return values_[ASR_SETTING_TURING_PROB]; + } + + uint8_t get_turing_CV() const { + return values_[ASR_SETTING_TURING_CV_SOURCE]; + } + + uint32_t get_shift_register() const { + return turing_machine_.get_shift_register(); + } + + uint16_t get_trigger_delay() const { + return values_[ASR_SETTING_DELAY]; + } + + uint8_t get_bytebeat_equation() const { + return values_[ASR_SETTING_BYTEBEAT_EQUATION]; + } + + uint8_t get_bytebeat_p0() const { + return values_[ASR_SETTING_BYTEBEAT_P0]; + } + + uint8_t get_bytebeat_p1() const { + return values_[ASR_SETTING_BYTEBEAT_P1]; + } + + uint8_t get_bytebeat_p2() const { + return values_[ASR_SETTING_BYTEBEAT_P2]; + } + + uint8_t get_bytebeat_CV() const { + return values_[ASR_SETTING_BYTEBEAT_CV_SOURCE]; + } + + uint8_t get_int_seq_index() const { + return values_[ ASR_SETTING_INT_SEQ_INDEX]; + } + + uint8_t get_int_seq_modulus() const { + return values_[ ASR_SETTING_INT_SEQ_MODULUS]; + } + + int16_t get_int_seq_start() const { + return static_cast(values_[ASR_SETTING_INT_SEQ_START]); + } + + int16_t get_int_seq_length() const { + return static_cast(values_[ASR_SETTING_INT_SEQ_LENGTH] - 1); + } + + bool get_int_seq_dir() const { + return static_cast(values_[ASR_SETTING_INT_SEQ_DIR]); + } + + int16_t get_fractal_seq_stride() const { + return static_cast(values_[ASR_SETTING_FRACTAL_SEQ_STRIDE]); + } + + uint8_t get_int_seq_CV() const { + return values_[ASR_SETTING_INT_SEQ_CV_SOURCE]; + } + + int16_t get_int_seq_k() const { + return int_seq_.get_k(); + } + + int16_t get_int_seq_l() const { + return int_seq_.get_l(); + } + + int16_t get_int_seq_i() const { + return int_seq_.get_i(); + } + + int16_t get_int_seq_j() const { + return int_seq_.get_j(); + } + + int16_t get_int_seq_n() const { + return int_seq_.get_n(); + } + + void toggle_delay_mechanics() { + delay_type_ = (~delay_type_) & 1u; + } + + bool get_delay_type() const { + return delay_type_; + } + + void manual_freeze() { + freeze_switch_ = (~freeze_switch_) & 1u; + } + + void clear_freeze() { + freeze_switch_ = false; + } + + bool freeze_state() { + return freeze_switch_; + } + + ASRSettings enabled_setting_at(int index) const { + return enabled_settings_[index]; + } + + int num_enabled_settings() const { + return num_enabled_settings_; + } + + void init() { + + force_update_ = false; + last_scale_ = -0x1; + delay_type_ = false; + octave_toggle_ = false; + freeze_switch_ = false; + TR2_state_ = 0x1; + set_scale(OC::Scales::SCALE_SEMI); + last_mask_ = 0x0; + quantizer_.Init(); + update_scale(true, 0x0); + + _ASR.Init(); + clock_display_.Init(); + for (auto &sh : scrolling_history_) + sh.Init(); + update_enabled_settings(); + + trigger_delay_.Init(); + turing_machine_.Init(); + turing_display_length_ = get_turing_length(); + bytebeat_.Init(); + int_seq_.Init(get_int_seq_start(), get_int_seq_length()); + } + + bool update_scale(bool force, int32_t mask_rotate) { + + const int scale = get_scale(DUMMY); + uint16_t mask = get_mask(); + if (mask_rotate) + mask = OC::ScaleEditor::RotateMask(mask, OC::Scales::GetScale(scale).num_notes, mask_rotate); + + if (force || (last_scale_ != scale || last_mask_ != mask)) { + + last_scale_ = scale; + last_mask_ = mask; + quantizer_.Configure(OC::Scales::GetScale(scale), mask); + return true; + } else { + return false; + } + } + + void force_update() { + force_update_ = true; + } + + // Wrappers for ScaleEdit + void scale_changed() { + force_update_ = true; + } + + uint16_t get_scale_mask(uint8_t scale_select) const { + return get_mask(); + } + + void update_scale_mask(uint16_t mask, uint16_t dummy) { + apply_value(ASR_SETTING_MASK, mask); // Should automatically be updated + } + // + + uint16_t get_rotated_mask() const { + return last_mask_; + } + + void set_display_mask(uint16_t mask) { + last_mask_ = mask; + } + + void update_enabled_settings() { + + ASRSettings *settings = enabled_settings_; + + *settings++ = ASR_SETTING_ROOT; + *settings++ = ASR_SETTING_MASK; + *settings++ = ASR_SETTING_OCTAVE; + *settings++ = ASR_SETTING_INDEX; + *settings++ = ASR_SETTING_BUFFER_LENGTH; + *settings++ = ASR_SETTING_DELAY; + *settings++ = ASR_SETTING_MULT; + + *settings++ = ASR_SETTING_CV4_DESTINATION; + *settings++ = ASR_SETTING_CV_SOURCE; + + switch (get_cv_source()) { + case ASR_CHANNEL_SOURCE_TURING: + *settings++ = ASR_SETTING_TURING_LENGTH; + *settings++ = ASR_SETTING_TURING_PROB; + *settings++ = ASR_SETTING_TURING_CV_SOURCE; + break; + case ASR_CHANNEL_SOURCE_BYTEBEAT: + *settings++ = ASR_SETTING_BYTEBEAT_EQUATION; + *settings++ = ASR_SETTING_BYTEBEAT_P0; + *settings++ = ASR_SETTING_BYTEBEAT_P1; + *settings++ = ASR_SETTING_BYTEBEAT_P2; + *settings++ = ASR_SETTING_BYTEBEAT_CV_SOURCE; + break; + case ASR_CHANNEL_SOURCE_INTEGER_SEQUENCES: + *settings++ = ASR_SETTING_INT_SEQ_INDEX; + *settings++ = ASR_SETTING_INT_SEQ_MODULUS; + *settings++ = ASR_SETTING_INT_SEQ_START; + *settings++ = ASR_SETTING_INT_SEQ_LENGTH; + *settings++ = ASR_SETTING_INT_SEQ_DIR; + *settings++ = ASR_SETTING_FRACTAL_SEQ_STRIDE; + *settings++ = ASR_SETTING_INT_SEQ_CV_SOURCE; + break; + default: + break; + } + + num_enabled_settings_ = settings - enabled_settings_; + } + + void updateASR_indexed(int32_t *_asr_buf, int32_t _sample, int16_t _index, bool _freeze) { + + int16_t _delay = _index, _offset; + + if (_freeze) { + + int8_t _buflen = get_buffer_length(); + if (get_cv4_destination() == ASR_DEST_BUFLEN) { + _buflen += ((OC::ADC::value() + 31) >> 6); + CONSTRAIN(_buflen, NUM_ASR_CHANNELS, ASR_HOLD_BUF_SIZE - 0x1); + } + _ASR.Freeze(_buflen); + } + else + _ASR.Write(_sample); + + // update outputs: + _offset = _delay; + *(_asr_buf + DAC_CHANNEL_A) = _ASR.Poke(_offset++); + // delay mechanics ... + _delay = delay_type_ ? 0x0 : _delay; + // continue updating + _offset +=_delay; + *(_asr_buf + DAC_CHANNEL_B) = _ASR.Poke(_offset++); + _offset +=_delay; + *(_asr_buf + DAC_CHANNEL_C) = _ASR.Poke(_offset++); + _offset +=_delay; + *(_asr_buf + DAC_CHANNEL_D) = _ASR.Poke(_offset++); + } + + inline void update() { + + bool update = OC::DigitalInputs::clocked(); + clock_display_.Update(1, update); + + trigger_delay_.Update(); + + if (update) + trigger_delay_.Push(OC::trigger_delay_ticks[get_trigger_delay()]); + + update = trigger_delay_.triggered(); + + if (update) { + + bool _freeze_switch, _freeze = digitalReadFast(TR2); + int8_t _root = get_root(); + int8_t _index = get_index() + ((OC::ADC::value() + 31) >> 6); + int8_t _octave = get_octave(); + int8_t _transpose = 0; + int8_t _mult = get_mult(); + int32_t _pitch = OC::ADC::raw_pitch_value(ADC_CHANNEL_1); + int32_t _asr_buffer[NUM_ASR_CHANNELS]; + + bool forced_update = force_update_; + force_update_ = false; + update_scale(forced_update, (OC::ADC::value() + 127) >> 8); + + // cv4 destination, defaults to octave: + switch(get_cv4_destination()) { + + case ASR_DEST_OCTAVE: + _octave += (OC::ADC::value() + 255) >> 9; + break; + case ASR_DEST_ROOT: + _root += (OC::ADC::value() + 127) >> 8; + CONSTRAIN(_root, 0, 11); + break; + case ASR_DEST_TRANSPOSE: + _transpose += (OC::ADC::value() + 63) >> 7; + CONSTRAIN(_transpose, -12, 12); + break; + case ASR_DEST_INPUT_SCALING: + _mult += (OC::ADC::value() + 63) >> 7; + CONSTRAIN(_mult, 0, NUM_INPUT_SCALING - 1); + break; + // CV for buffer length happens in updateASR_indexed + default: + break; + } + + // freeze ? + if (_freeze < TR2_state_) + freeze_switch_ = true; + else if (_freeze > TR2_state_) { + freeze_switch_ = false; + } + TR2_state_ = _freeze; + // + _freeze_switch = freeze_switch_; + + // use built in CV sources? + if (!_freeze_switch) { + + switch (get_cv_source()) { + + case ASR_CHANNEL_SOURCE_TURING: + { + int16_t _length = get_turing_length(); + int16_t _probability = get_turing_probability(); + + // _pitch can do other things now -- + switch (get_turing_CV()) { + + case 1: // mult + _mult += ((_pitch + 63) >> 7); + break; + case 2: // LEN, 1-32 + _length += ((_pitch + 255) >> 9); + CONSTRAIN(_length, 1, 32); + break; + case 3: // P + _probability += ((_pitch + 7) >> 4); + CONSTRAIN(_probability, 0, 255); + break; + default: + break; + } + + turing_machine_.set_length(_length); + turing_machine_.set_probability(_probability); + turing_display_length_ = _length; + + _pitch = turing_machine_.Clock(); + + // scale LFSR output (0 - 4095) / compensate for length + if (_length < 12) + _pitch = _pitch << (12 -_length); + else + _pitch = _pitch >> (_length - 12); + _pitch &= 0xFFF; + } + break; + case ASR_CHANNEL_SOURCE_BYTEBEAT: + { + int32_t _bytebeat_eqn = get_bytebeat_equation() << 12; + int32_t _bytebeat_p0 = get_bytebeat_p0() << 8; + int32_t _bytebeat_p1 = get_bytebeat_p1() << 8; + int32_t _bytebeat_p2 = get_bytebeat_p2() << 8; + + // _pitch can do other things now -- + switch (get_bytebeat_CV()) { + + case 1: // 0-15 + _bytebeat_eqn += (_pitch << 4); + _bytebeat_eqn = USAT16(_bytebeat_eqn); + break; + case 2: // P0 + _bytebeat_p0 += (_pitch << 4); + _bytebeat_p0 = USAT16(_bytebeat_p0); + break; + case 3: // P1 + _bytebeat_p1 += (_pitch << 4); + _bytebeat_p1 = USAT16(_bytebeat_p1); + break; + case 4: // P4 + _bytebeat_p2 += (_pitch << 4); + _bytebeat_p2 = USAT16(_bytebeat_p2); + break; + default: // mult + _mult += ((_pitch + 63) >> 7); + break; + } + + bytebeat_.set_equation(_bytebeat_eqn); + bytebeat_.set_p0(_bytebeat_p0); + bytebeat_.set_p0(_bytebeat_p1); + bytebeat_.set_p0(_bytebeat_p2); + + int32_t _bb = (static_cast(bytebeat_.Clock()) & 0xFFF); + _pitch = _bb; + } + break; + case ASR_CHANNEL_SOURCE_INTEGER_SEQUENCES: + { + int16_t _int_seq_index = get_int_seq_index() ; + int16_t _int_seq_modulus = get_int_seq_modulus() ; + int16_t _int_seq_start = get_int_seq_start() ; + int16_t _int_seq_length = get_int_seq_length(); + int16_t _fractal_seq_stride = get_fractal_seq_stride(); + bool _int_seq_dir = get_int_seq_dir(); + + // _pitch can do other things now -- + switch (get_int_seq_CV()) { + + case 1: // integer sequence, 0-8 + _int_seq_index += ((_pitch + 255) >> 9); + CONSTRAIN(_int_seq_index, 0, 8); + break; + case 2: // sequence start point, 0 to kIntSeqLen - 2 + _int_seq_start += ((_pitch + 15) >> 5); + CONSTRAIN(_int_seq_start, 0, kIntSeqLen - 2); + break; + case 3: // sequence loop length, 1 to kIntSeqLen - 1 + _int_seq_length += ((_pitch + 15) >> 5); + CONSTRAIN(_int_seq_length, 1, kIntSeqLen - 1); + break; + case 4: // fractal sequence stride length, 1 to kIntSeqLen - 1 + _fractal_seq_stride += ((_pitch + 15) >> 5); + CONSTRAIN(_fractal_seq_stride, 1, kIntSeqLen - 1); + break; + case 5: // fractal sequence modulus + _int_seq_modulus += ((_pitch + 15) >> 5); + CONSTRAIN(_int_seq_modulus, 2, 121); + break; + default: // mult + _mult += ((_pitch + 63) >> 7); + break; + } + + int_seq_.set_loop_start(_int_seq_start); + int_seq_.set_loop_length(_int_seq_length); + int_seq_.set_int_seq(_int_seq_index); + int_seq_.set_int_seq_modulus(_int_seq_modulus); + int_seq_.set_loop_direction(_int_seq_dir); + int_seq_.set_fractal_stride(_fractal_seq_stride); + + int32_t _is = (static_cast(int_seq_.Clock()) & 0xFFF); + _pitch = _is; + } + break; + default: + break; + } + } + else { + // we hold, so we only need the multiplication CV: + switch (get_cv_source()) { + + case ASR_CHANNEL_SOURCE_TURING: + if (get_turing_CV() == 0x0) + _mult += ((_pitch + 63) >> 7); + break; + case ASR_CHANNEL_SOURCE_INTEGER_SEQUENCES: + if (get_int_seq_CV() == 0x0) + _mult += ((_pitch + 63) >> 7); + break; + case ASR_CHANNEL_SOURCE_BYTEBEAT: + if (get_bytebeat_CV() == 0x0) + _mult += ((_pitch + 63) >> 7); + break; + default: + break; + } + } + // limit gain factor. + CONSTRAIN(_mult, 0, NUM_INPUT_SCALING - 0x1); + // .. and index + CONSTRAIN(_index, 0, ASR_HOLD_BUF_SIZE - 0x1); + // push sample into ring-buffer and/or freeze buffer: + updateASR_indexed(_asr_buffer, _pitch, _index, _freeze_switch); + + // get octave offset : + if (!digitalReadFast(TR3)) + _octave++; + else if (!digitalReadFast(TR4)) + _octave--; + + // quantize buffer outputs: + for (int i = 0; i < NUM_ASR_CHANNELS; ++i) { + + int32_t _sample = _asr_buffer[i]; + + // scale sample + if (_mult != MULT_ONE) { + _sample = signed_multiply_32x16b(multipliers[_mult], _sample); + _sample = signed_saturate_rshift(_sample, 16, 0); + } + + _sample = quantizer_.Process(_sample, _root << 7, _transpose); + _sample = OC::DAC::pitch_to_scaled_voltage_dac(static_cast(i), _sample, _octave, OC::DAC::get_voltage_scaling(i)); + scrolling_history_[i].Push(_sample); + _asr_buffer[i] = _sample; + } + + // ... and write to DAC + for (int i = 0; i < NUM_ASR_CHANNELS; ++i) + OC::DAC::set(static_cast(i), _asr_buffer[i]); + + MENU_REDRAW = 0x1; + } + for (auto &sh : scrolling_history_) + sh.Update(); + } + + uint8_t clockState() const { + return clock_display_.getState(); + } + + const OC::vfx::ScrollingHistory &history(int i) const { + return scrolling_history_[i]; + } + +private: + bool force_update_; + bool delay_type_; + bool octave_toggle_; + bool freeze_switch_; + int8_t TR2_state_; + int last_scale_; + uint16_t last_mask_; + braids::Quantizer quantizer_; + OC::DigitalInputDisplay clock_display_; + util::TriggerDelay trigger_delay_; + util::TuringShiftRegister turing_machine_; + util::RingBuffer _ASR; + int8_t turing_display_length_; + peaks::ByteBeat bytebeat_ ; + util::IntegerSequence int_seq_ ; + OC::vfx::ScrollingHistory scrolling_history_[NUM_ASR_CHANNELS]; + int num_enabled_settings_; + ASRSettings enabled_settings_[ASR_SETTING_LAST]; +}; + +const char* const asr_input_sources[] = { + "CV1", "TM", "ByteB", "IntSq" +}; + +const char* const asr_cv4_destinations[] = { + "oct", "root", "trns", "buf.l", "igain" +}; + +const char* const bb_CV_destinations[] = { + "igain", "eqn", "P0", "P1", "P2" +}; + +const char* const int_seq_CV_destinations[] = { + "igain", "seq", "strt", "len", "strd", "mod" +}; + +// TOTAL EEPROM SIZE: 23 bytes +SETTINGS_DECLARE(ASRApp, ASR_SETTING_LAST) { + { OC::Scales::SCALE_SEMI, 0, OC::Scales::NUM_SCALES - 1, "Scale", OC::scale_names_short, settings::STORAGE_TYPE_U8 }, + { 0, -5, 5, "octave", NULL, settings::STORAGE_TYPE_I8 }, // octave + { 0, 0, 11, "root", OC::Strings::note_names_unpadded, settings::STORAGE_TYPE_U8 }, + { 65535, 1, 65535, "mask", NULL, settings::STORAGE_TYPE_U16 }, // mask + { 0, 0, ASR_HOLD_BUF_SIZE - 1, "buf.index", NULL, settings::STORAGE_TYPE_U8 }, + { MULT_ONE, 0, NUM_INPUT_SCALING - 1, "input gain", OC::Strings::mult, settings::STORAGE_TYPE_U8 }, + { 0, 0, OC::kNumDelayTimes - 1, "trigger delay", OC::Strings::trigger_delay_times, settings::STORAGE_TYPE_U8 }, + { 4, 4, ASR_HOLD_BUF_SIZE - 1, "hold (buflen)", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, ASR_CHANNEL_SOURCE_LAST -1, "CV source", asr_input_sources, settings::STORAGE_TYPE_U4 }, + { 0, 0, ASR_DEST_LAST - 1, "CV4 dest. ->", asr_cv4_destinations, settings::STORAGE_TYPE_U4 }, + { 16, 1, 32, "> LFSR length", NULL, settings::STORAGE_TYPE_U8 }, + { 128, 0, 255, "> LFSR p", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 3, "> LFSR CV1", OC::Strings::TM_aux_cv_destinations, settings::STORAGE_TYPE_U8 }, // ?? + { 0, 0, 15, "> BB eqn", OC::Strings::bytebeat_equation_names, settings::STORAGE_TYPE_U8 }, + { 8, 1, 255, "> BB P0", NULL, settings::STORAGE_TYPE_U8 }, + { 12, 1, 255, "> BB P1", NULL, settings::STORAGE_TYPE_U8 }, + { 14, 1, 255, "> BB P2", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 4, "> BB CV1", bb_CV_destinations, settings::STORAGE_TYPE_U4 }, + { 0, 0, 9, "> IntSeq", OC::Strings::integer_sequence_names, settings::STORAGE_TYPE_U4 }, + { 24, 2, 121, "> IntSeq modul", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, kIntSeqLen - 2, "> IntSeq start", NULL, settings::STORAGE_TYPE_U8 }, + { 8, 2, kIntSeqLen, "> IntSeq len", NULL, settings::STORAGE_TYPE_U8 }, + { 1, 0, 1, "> IntSeq dir", OC::Strings::integer_sequence_dirs, settings::STORAGE_TYPE_U4 }, + { 1, 1, kIntSeqLen - 1, "> Fract stride", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 5, "> IntSeq CV1", int_seq_CV_destinations, settings::STORAGE_TYPE_U4 } +}; + +/* -------------------------------------------------------------------*/ + +class ASRState { +public: + + void Init() { + cursor.Init(ASR_SETTING_SCALE, ASR_SETTING_LAST - 1); + scale_editor.Init(false); + left_encoder_value = OC::Scales::SCALE_SEMI; + } + + inline bool editing() const { + return cursor.editing(); + } + + inline int cursor_pos() const { + return cursor.cursor_pos(); + } + + int left_encoder_value; + menu::ScreenCursor cursor; + menu::ScreenCursor cursor_state; + OC::ScaleEditor scale_editor; +}; + +ASRState asr_state; +ASRApp asr; + +void ASR_init() { + + asr.InitDefaults(); + asr.init(); + asr_state.Init(); + asr.update_enabled_settings(); + asr_state.cursor.AdjustEnd(asr.num_enabled_settings() - 1); +} + +size_t ASR_storageSize() { + return ASRApp::storageSize(); +} + +size_t ASR_restore(const void *storage) { + // init nicely + size_t storage_size = asr.Restore(storage); + asr_state.left_encoder_value = asr.get_scale(DUMMY); + asr.set_scale(asr_state.left_encoder_value); + asr.clear_freeze(); + asr.set_display_mask(asr.get_mask()); + asr.update_enabled_settings(); + asr_state.cursor.AdjustEnd(asr.num_enabled_settings() - 1); + return storage_size; +} + +void ASR_handleAppEvent(OC::AppEvent event) { + switch (event) { + case OC::APP_EVENT_RESUME: + asr_state.cursor.set_editing(false); + asr_state.scale_editor.Close(); + break; + case OC::APP_EVENT_SUSPEND: + case OC::APP_EVENT_SCREENSAVER_ON: + case OC::APP_EVENT_SCREENSAVER_OFF: + asr.update_enabled_settings(); + asr_state.cursor.AdjustEnd(asr.num_enabled_settings() - 1); + break; + } +} + +void ASR_loop() { +} + +void ASR_isr() { + asr.update(); +} + +void ASR_handleButtonEvent(const UI::Event &event) { + if (asr_state.scale_editor.active()) { + asr_state.scale_editor.HandleButtonEvent(event); + return; + } + + if (UI::EVENT_BUTTON_PRESS == event.type) { + switch (event.control) { + case OC::CONTROL_BUTTON_UP: + ASR_topButton(); + break; + case OC::CONTROL_BUTTON_DOWN: + ASR_lowerButton(); + break; + case OC::CONTROL_BUTTON_L: + ASR_leftButton(); + break; + case OC::CONTROL_BUTTON_R: + ASR_rightButton(); + break; + } + } else if (UI::EVENT_BUTTON_LONG_PRESS == event.type) { + if (OC::CONTROL_BUTTON_L == event.control) + ASR_leftButtonLong(); + else if (OC::CONTROL_BUTTON_DOWN == event.control) + ASR_downButtonLong(); + } +} + +void ASR_handleEncoderEvent(const UI::Event &event) { + + if (asr_state.scale_editor.active()) { + asr_state.scale_editor.HandleEncoderEvent(event); + return; + } + + if (OC::CONTROL_ENCODER_L == event.control) { + + int value = asr_state.left_encoder_value + event.value; + CONSTRAIN(value, 0, OC::Scales::NUM_SCALES - 1); + asr_state.left_encoder_value = value; + + } else if (OC::CONTROL_ENCODER_R == event.control) { + + if (asr_state.editing()) { + + ASRSettings setting = asr.enabled_setting_at(asr_state.cursor_pos()); + + if (ASR_SETTING_MASK != setting) { + + if (asr.change_value(setting, event.value)) + asr.force_update(); + + switch(setting) { + + case ASR_SETTING_CV_SOURCE: + asr.update_enabled_settings(); + asr_state.cursor.AdjustEnd(asr.num_enabled_settings() - 1); + // hack/hide extra options when default CV source is selected + if (!asr.get_cv_source()) + asr_state.cursor.Scroll(asr_state.cursor_pos()); + break; + default: + break; + } + } + } else { + asr_state.cursor.Scroll(event.value); + } + } +} + + +void ASR_topButton() { + if (asr.octave_toggle()) + asr.change_value(ASR_SETTING_OCTAVE, 1); + else + asr.change_value(ASR_SETTING_OCTAVE, -1); +} + +void ASR_lowerButton() { + asr.manual_freeze(); +} + +void ASR_rightButton() { + + switch (asr.enabled_setting_at(asr_state.cursor_pos())) { + + case ASR_SETTING_MASK: { + int scale = asr.get_scale(DUMMY); + if (OC::Scales::SCALE_NONE != scale) + asr_state.scale_editor.Edit(&asr, scale); + } + break; + default: + asr_state.cursor.toggle_editing(); + break; + } +} + +void ASR_leftButton() { + + if (asr_state.left_encoder_value != asr.get_scale(DUMMY)) + asr.set_scale(asr_state.left_encoder_value); +} + +void ASR_leftButtonLong() { + + int scale = asr_state.left_encoder_value; + asr.set_scale(asr_state.left_encoder_value); + if (scale != OC::Scales::SCALE_NONE) + asr_state.scale_editor.Edit(&asr, scale); +} + +void ASR_downButtonLong() { + asr.toggle_delay_mechanics(); +} + +size_t ASR_save(void *storage) { + return asr.Save(storage); +} + +void ASR_menu() { + + menu::TitleBar<0, 4, 0>::Draw(); + + int scale = asr_state.left_encoder_value; + graphics.movePrintPos(weegfx::Graphics::kFixedFontW, 0); + graphics.print(OC::scale_names[scale]); + + if (asr.freeze_state()) + graphics.drawBitmap8(1, menu::QuadTitleBar::kTextY, 4, OC::bitmap_hold_indicator_4x8); + else if (asr.get_scale(DUMMY) == scale) + graphics.drawBitmap8(1, menu::QuadTitleBar::kTextY, 4, OC::bitmap_indicator_4x8); + + if (asr.poke_octave_toggle()) { + graphics.setPrintPos(110, 2); + graphics.print("+"); + } + + if (asr.get_delay_type()) + graphics.drawBitmap8(118, menu::QuadTitleBar::kTextY, 4, OC::bitmap_hold_indicator_4x8); + else + graphics.drawBitmap8(118, menu::QuadTitleBar::kTextY, 4, OC::bitmap_indicator_4x8); + + uint8_t clock_state = (asr.clockState() + 3) >> 2; + if (clock_state) + graphics.drawBitmap8(124, 2, 4, OC::bitmap_gate_indicators_8 + (clock_state << 2)); + + menu::SettingsList settings_list(asr_state.cursor); + menu::SettingsListItem list_item; + + while (settings_list.available()) { + + const int setting = asr.enabled_setting_at(settings_list.Next(list_item)); + const int value = asr.get_value(setting); + const settings::value_attr &attr = ASRApp::value_attr(setting); + + switch (setting) { + + case ASR_SETTING_MASK: + menu::DrawMask(menu::kDisplayWidth, list_item.y, asr.get_rotated_mask(), OC::Scales::GetScale(asr.get_scale(DUMMY)).num_notes); + list_item.DrawNoValue(value, attr); + break; + case ASR_SETTING_CV_SOURCE: + if (asr.get_cv_source() == ASR_CHANNEL_SOURCE_TURING) { + + int turing_length = asr.get_turing_display_length(); + int w = turing_length >= 16 ? 16 * 3 : turing_length * 3; + + menu::DrawMask(menu::kDisplayWidth, list_item.y, asr.get_shift_register(), turing_length); + list_item.valuex = menu::kDisplayWidth - w - 1; + list_item.DrawNoValue(value, attr); + } else + list_item.DrawDefault(value, attr); + break; + default: + list_item.DrawDefault(value, attr); + break; + } + } + if (asr_state.scale_editor.active()) + asr_state.scale_editor.Draw(); +} + +uint16_t channel_history[ASRApp::kHistoryDepth]; + +void ASR_screensaver() { + +// Possible variants (w x h) +// 4 x 32x64 px +// 4 x 64x32 px +// "Somehow" overlapping? +// Normalize history to within one octave? That would make steps more visisble for small ranges +// "Zoomed view" to fit range of history... + + for (int i = 0; i < NUM_ASR_CHANNELS; ++i) { + asr.history(i).Read(channel_history); + uint32_t scroll_pos = asr.history(i).get_scroll_pos() >> 5; + + int pos = 0; + weegfx::coord_t x = i * 32, y; + + y = 63 - ((channel_history[pos++] >> 10) & 0x3f); + graphics.drawHLine(x, y, scroll_pos); + x += scroll_pos; + graphics.drawVLine(x, y, 3); + + weegfx::coord_t last_y = y; + for (int c = 0; c < 3; ++c) { + y = 63 - ((channel_history[pos++] >> 10) & 0x3f); + graphics.drawHLine(x, y, 8); + if (y == last_y) + graphics.drawVLine(x, y, 2); + else if (y < last_y) + graphics.drawVLine(x, y, last_y - y + 1); + else + graphics.drawVLine(x, last_y, y - last_y + 1); + x += 8; + last_y = y; +// graphics.drawVLine(x, y, 3); + } + + y = 63 - ((channel_history[pos++] >> 10) & 0x3f); +// graphics.drawHLine(x, y, 8 - scroll_pos); + graphics.drawRect(x, y, 8 - scroll_pos, 2); + if (y == last_y) + graphics.drawVLine(x, y, 3); + else if (y < last_y) + graphics.drawVLine(x, y, last_y - y + 1); + else + graphics.drawVLine(x, last_y, y - last_y + 1); +// x += 8 - scroll_pos; +// graphics.drawVLine(x, y, 3); + } +} + +#ifdef ASR_DEBUG +void ASR_debug() { + for (int i = 0; i < 1; ++i) { + uint8_t ypos = 10*(i + 1) + 2 ; + graphics.setPrintPos(2, ypos); + graphics.print(asr.get_int_seq_i()); + graphics.setPrintPos(32, ypos); + graphics.print(asr.get_int_seq_l()); + graphics.setPrintPos(62, ypos); + graphics.print(asr.get_int_seq_j()); + graphics.setPrintPos(92, ypos); + graphics.print(asr.get_int_seq_k()); + graphics.setPrintPos(122, ypos); + graphics.print(asr.get_int_seq_n()); + } +} +#endif // ASR_DEBUG + +#endif // ENABLE_APP_ASR diff --git a/software/o_c_REV/APP_AUTOMATONNETZ.ino b/software/o_c_REV/APP_AUTOMATONNETZ.ino new file mode 100644 index 000000000..5a85abc59 --- /dev/null +++ b/software/o_c_REV/APP_AUTOMATONNETZ.ino @@ -0,0 +1,734 @@ +// Copyright (c) 2015, 2016 Patrick Dowling, Tim Churches +// +// Initial app implementation: Patrick Dowling (pld@gurkenkiste.com) +// Modifications by: Tim Churches (tim.churches@gmail.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// +// Drive the tonnetz transformations from a grid of cells that contain the type +// of transformation and other goodies. Instead of stepping through the grid +// linearly on each clock, at vector (dx, dy) is used to determine the next +// cell. The cell coordinates use fixed-point internally with a configurable +// sub-step accuracy. +// +// Since the grid is 5x5 and the sub-step accuracy a power-of-two, they might +// not align correctly so there is a cumulative error. As a work-around, the +// grid is given an epsilon and the selectable steps are restricted. Using odd +// dimensions should mean that each cell is reached eventually. +// +// "Vector sequencer" inspired by fcd72, see +// https://dmachinery.wordpress.com/2013/01/05/the-vector-sequencer/ +// +// NOTES +// The initial implementation was using a polling loop (since that's what the +// firmware paradigm was at the time) but has now been updated to be clocked by +// the core ISR. This means some additional hoops are necessary to be able to +// write settings/state from UI and ISR. The main use cases are: +// - Clearing the grid (left long press) +// - Cell event types that modify the cell itself +// +// So these have been wrapped in a minimal critical section/mutex style locking, +// which seems preferable than brute-forcey __disable_irq/__enable_irq, and the +// clear function shouldn't take long anyway. This doesn't cover all cases, but +// but editing while clocking could be classified as "undefined behaviour" ;) +// +// It's also possible for the displayed state to be slightly inconsistent; but +// these should be temporary glitches if they are even noticeable, so the risk +// seems acceptable there. +// +// TODO With fast clocking, trigger out mode might not provide a rising edge + +#ifdef ENABLE_APP_AUTOMATONNETZ + +#include "util/util_grid.h" +#include "util/util_ringbuffer.h" +#include "util/util_settings.h" +#include "util/util_sync.h" +#include "tonnetz/tonnetz_state.h" +#include "OC_bitmaps.h" +#include "OC_menus.h" +#include "OC_trigger_delays.h" + +#define FRACTIONAL_BITS 24 +#define CLOCK_STEP_RES (0x1 << FRACTIONAL_BITS) +#define GRID_EPSILON 6 +#define GRID_DIMENSION 5 +#define GRID_CELLS (GRID_DIMENSION * GRID_DIMENSION) + +// TODO Better selection, e.g. .125, .15 .2 or 3 digits with encoder acceleration + +const size_t clock_fraction[] = { + 0, CLOCK_STEP_RES/8, 1+CLOCK_STEP_RES/7, CLOCK_STEP_RES/6, 1+CLOCK_STEP_RES/5, 1+CLOCK_STEP_RES/4, 1+CLOCK_STEP_RES/3, CLOCK_STEP_RES/2 +}; + +const char *clock_fraction_names[] = { + " \0\0\0\0", " 1/8", " 1/7", " 1/6", " 1/5", " 1/4", " 1/3", " 1/2" +}; + +static constexpr uint32_t TRIGGER_MASK_GRID = OC::DIGITAL_INPUT_1_MASK; +static constexpr uint32_t TRIGGER_MASK_ARP = OC::DIGITAL_INPUT_2_MASK; +static constexpr uint32_t kTriggerOutTicks = 1000U / OC_CORE_TIMER_RATE; + +enum CellSettings { + CELL_SETTING_TRANSFORM, + CELL_SETTING_TRANSPOSE, + CELL_SETTING_INVERSION, + CELL_SETTING_EVENT, + CELL_SETTING_LAST +}; + +enum CellEventMasks { + CELL_EVENT_NONE, + CELL_EVENT_RAND_TRANFORM = 0x1, + CELL_EVENT_RAND_TRANSPOSE = 0x2, + CELL_EVENT_RAND_INVERSION = 0x4, + CELL_EVENT_ALL = 0x7 +}; + +#define CELL_MAX_INVERSION 3 +#define CELL_MIN_INVERSION -3 + +namespace menu = OC::menu; + +class TransformCell : public settings::SettingsBase { +public: + TransformCell() { } + + tonnetz::ETransformType transform() const { + return static_cast(values_[CELL_SETTING_TRANSFORM]); + } + + int transpose() const { + return values_[CELL_SETTING_TRANSPOSE]; + } + + int inversion() const { + return values_[CELL_SETTING_INVERSION]; + } + + CellEventMasks event_masks() const { + return static_cast(values_[CELL_SETTING_EVENT]); + } + + void apply_event_masks() { + int masks = values_[CELL_SETTING_EVENT]; + if (masks & CELL_EVENT_RAND_TRANFORM) + apply_value(CELL_SETTING_TRANSFORM, random(tonnetz::TRANSFORM_LAST + 1)); + if (masks & CELL_EVENT_RAND_TRANSPOSE) + apply_value(CELL_SETTING_TRANSPOSE, -12 + random(24 + 1)); + if (masks & CELL_EVENT_RAND_INVERSION) + apply_value(CELL_SETTING_INVERSION, CELL_MIN_INVERSION + random(2*CELL_MAX_INVERSION + 1)); + } +}; + +const char *cell_event_masks[] = { + "none", + "rT__", // 0x1 + "r_O_", // 0x2 + "rTO_", // 0x1 + 0x2 + "r__I", // 0x4 + "rT_I", // 0x4 + 0x1 + "r_OI", // 0x4 + 0x2 + "rTOI", // 0x4 + 0x2 + 0x1 +}; + +// TOTAL EEPROM SIZE: 25 * 4 bytes +SETTINGS_DECLARE(TransformCell, CELL_SETTING_LAST) { + {0, tonnetz::TRANSFORM_NONE, tonnetz::TRANSFORM_LAST, "Trfm", tonnetz::transform_names_str, settings::STORAGE_TYPE_U8}, + {0, -12, 12, "Offs", NULL, settings::STORAGE_TYPE_I8}, + {0, CELL_MIN_INVERSION, CELL_MAX_INVERSION, "Inv", NULL, settings::STORAGE_TYPE_I8}, + {0, CELL_EVENT_NONE, CELL_EVENT_ALL, "Muta", cell_event_masks, settings::STORAGE_TYPE_U8} +}; + +enum GridSettings { + GRID_SETTING_DX, + GRID_SETTING_DY, + GRID_SETTING_MODE, + GRID_SETTING_OCTAVE, + GRID_SETTING_TRIGGER_DELAY, + GRID_SETTING_OUTPUTMODE, + GRID_SETTING_CLEARMODE, + GRID_SETTING_LAST +}; + +enum EOutputAMode { + OUTPUTA_MODE_ROOT, + OUTPUTA_MODE_TRIG, + OUTPUTA_MODE_ARP, + OUTPUTA_MODE_STRUM, + OUTPUTA_MODE_LAST +}; + +// What happens on long left press +enum ClearMode { + CLEAR_MODE_ZERO, // empty cells + CLEAR_MODE_RAND_TRANSFORM, // random transform + CLEAR_MODE_RAND_TRANSFORM_EV, // random transform event + CLEAR_MODE_LAST +}; + +enum UserAction { + USER_ACTION_RESET, + USER_ACTION_CLOCK, +}; + +class AutomatonnetzState : public settings::SettingsBase { +public: + + void Init() { + InitDefaults(); + memset(cells_, 0, sizeof(cells_)); + grid.Init(cells_); + + quantizer.Init(); + tonnetz_state.init(); + + trigger_delays_.Init(); + strum_inhibit_ = false; + + memset(&ui, 0, sizeof(ui)); + ui.cell_cursor.Init(CELL_SETTING_TRANSFORM, CELL_SETTING_LAST - 1); + ui.grid_cursor.Init(GRID_SETTING_DX, GRID_SETTING_LAST - 1); + + history_ = 0; + cell_transpose_ = cell_inversion_ = 0; + user_actions_.Init(); + critical_section_.Init(); + } + + void ClearGrid() { + util::Lock lock(critical_section_); + ClearGrid(clear_mode()); + } + + void ClearGrid(ClearMode mode) { + memset(cells_, 0, sizeof(cells_)); + switch (mode) { + case CLEAR_MODE_RAND_TRANSFORM: + for (auto &cell : cells_) + cell.apply_value(CELL_SETTING_TRANSFORM, random(tonnetz::TRANSFORM_LAST + 1)); + break; + case CLEAR_MODE_RAND_TRANSFORM_EV: + for (auto &cell : cells_) + cell.apply_value(CELL_SETTING_EVENT, CELL_EVENT_RAND_TRANFORM); + break; + case CLEAR_MODE_ZERO: + default: + break; + } + } + + // Settings wrappers + + size_t dx() const { + const int value = values_[GRID_SETTING_DX]; + return ((value / 8) * CLOCK_STEP_RES) + clock_fraction[value % 8]; + } + + size_t dy() const { + const int value = values_[GRID_SETTING_DY]; + return ((value / 8) * CLOCK_STEP_RES) + clock_fraction[value % 8]; + } + + EMode mode() const { + return static_cast(values_[GRID_SETTING_MODE]); + } + + int octave() const { + return values_[GRID_SETTING_OCTAVE]; + } + + uint16_t get_trigger_delay() const { + return values_[GRID_SETTING_TRIGGER_DELAY]; + } + + EOutputAMode output_mode() const { + return static_cast(values_[GRID_SETTING_OUTPUTMODE]); + } + + ClearMode clear_mode() const { + return static_cast(values_[GRID_SETTING_CLEARMODE]); + } + + // End of settings + + void ISR(); + void Reset(); + + inline void AddUserAction(UserAction action) { + user_actions_.Write(action); + } + + // history length is fixed since it's kept as 4xuint8_t + static const size_t HISTORY_LENGTH = 4; + + uint32_t history() const { + return history_; + } + + TransformCell cells_[GRID_CELLS]; + CellGrid grid; + + OC::SemitoneQuantizer quantizer; + TonnetzState tonnetz_state; + + struct { + int selected_cell; + bool edit_cell; + + menu::ScreenCursor grid_cursor; + menu::ScreenCursor cell_cursor; + } ui; + +private: + + enum CriticalSectionIDs { + CRITICAL_SECTION_ID_MAIN, + CRITICAL_SECTION_ID_ISR + }; + + uint32_t trigger_out_ticks_; + uint_fast8_t arp_index_; + int cell_transpose_, cell_inversion_; + uint32_t history_; + + OC::TriggerDelays trigger_delays_; + bool strum_inhibit_ ; + + util::RingBuffer user_actions_; + util::CriticalSection critical_section_; + + void update_trigger_out(); + void update_outputs(bool chord_changed, int transpose, int inversion); + void push_history(uint32_t pos) { + history_ = (history_ << 8) | (pos & 0xff); + } +}; + +// needed for Automatonnetz to work with or without Harrington 1200 compiled in the build +#ifdef ENABLE_APP_H1200 +extern const char * const mode_names[]; +#else +const char * const mode_names[] = { + "Maj", "Min" +}; +#endif + +const char * const outputa_mode_names[] = { + "root", + "trig", + "arp", + "strm" +}; + +const char * const clear_mode_names[] = { + "zero", "rT", "rTev" +}; + +// TOTAL EEPROM SIZE: 6 bytes +SETTINGS_DECLARE(AutomatonnetzState, GRID_SETTING_LAST) { + {8, 0, 8*GRID_DIMENSION - 1, "dx", NULL, settings::STORAGE_TYPE_I8}, + {4, 0, 8*GRID_DIMENSION - 1, "dy", NULL, settings::STORAGE_TYPE_I8}, + {MODE_MAJOR, 0, MODE_LAST-1, "Mode", mode_names, settings::STORAGE_TYPE_U8}, + #ifdef BUCHLA_4U + {0, 0, 7, "Oct", NULL, settings::STORAGE_TYPE_I8}, + #else + {0, -3, 3, "Oct", NULL, settings::STORAGE_TYPE_I8}, + #endif + { 0, 0, OC::kNumDelayTimes - 1, "TrDly", OC::Strings::trigger_delay_times, settings::STORAGE_TYPE_U8 }, + {OUTPUTA_MODE_ROOT, OUTPUTA_MODE_ROOT, OUTPUTA_MODE_LAST - 1, "OutA", outputa_mode_names, settings::STORAGE_TYPE_U4}, + {CLEAR_MODE_ZERO, CLEAR_MODE_ZERO, CLEAR_MODE_LAST - 1, "Clr", clear_mode_names, settings::STORAGE_TYPE_U4}, +}; + +AutomatonnetzState automatonnetz_state; + +void Automatonnetz_init() { + automatonnetz_state.Init(); + automatonnetz_state.ClearGrid(CLEAR_MODE_RAND_TRANSFORM); + automatonnetz_state.Reset(); +} + +size_t Automatonnetz_storageSize() { + return AutomatonnetzState::storageSize() + + GRID_CELLS * TransformCell::storageSize(); +} + +void FASTRUN AutomatonnetzState::ISR() { + update_trigger_out(); + + uint32_t triggers = OC::DigitalInputs::clocked(); + triggers = trigger_delays_.Process(triggers, OC::trigger_delay_ticks[get_trigger_delay()]); + + bool reset = false; + while (user_actions_.readable()) { + switch (user_actions_.Read()) { + case USER_ACTION_RESET: + reset = true; + break; + case USER_ACTION_CLOCK: + triggers |= TRIGGER_MASK_GRID; + break; + } + } + + if ((triggers & TRIGGER_MASK_GRID) && OC::DigitalInputs::read_immediate()) + reset = true; + + bool update = false; + if (reset) { + grid.MoveToOrigin(); + arp_index_ = 0; + update = true; + } else if (triggers & TRIGGER_MASK_GRID) { + update = grid.move(dx(), dy()); + } + + bool chord_changed = false; + { + util::TryLock lock(critical_section_); + // Minimal safeguard against update while ClearGrid is active. + // We can't use a blocking Lock here since the ISR interrupts the main + // loop, so the lock would never be relinquished at things implode. + // A user-triggered clear will force a reset anyway and update next ISR, + // so we can just skip it. + + if (update && lock.locked()) { + TransformCell ¤t_cell = grid.mutable_current_cell(); + push_history(grid.current_pos_index()); + tonnetz::ETransformType transform = current_cell.transform(); + if (reset || transform >= tonnetz::TRANSFORM_LAST) { + tonnetz_state.reset(mode()); + chord_changed = true; + } else if (transform != tonnetz::TRANSFORM_NONE) { + tonnetz_state.apply_transformation(transform); + chord_changed = true; + } + + cell_transpose_ = current_cell.transpose(); + cell_inversion_ = current_cell.inversion(); + current_cell.apply_event_masks(); + } + } + + // Arp/strum + if (chord_changed && OUTPUTA_MODE_STRUM == output_mode()) { + arp_index_ = 0; + strum_inhibit_ = false; + } else if ((triggers & TRIGGER_MASK_ARP) && + !reset && + !OC::DigitalInputs::read_immediate()) { + ++arp_index_; + if (arp_index_ >= 3) { + arp_index_ = 0; + strum_inhibit_ = true; + } + } + + if ((triggers & TRIGGER_MASK_GRID) || (triggers & TRIGGER_MASK_ARP)) + update_outputs(chord_changed, cell_transpose_, cell_inversion_); +} + +void AutomatonnetzState::Reset() { + // Assumed to be called w/o ISR active! + grid.MoveToOrigin(); + push_history(grid.current_pos_index()); + tonnetz_state.reset(mode()); + arp_index_ = 0; + const TransformCell ¤t_cell = grid.current_cell(); + cell_transpose_ = current_cell.transpose(); + cell_inversion_ = current_cell.inversion(); + update_outputs(true, cell_transpose_, cell_inversion_); +} + +void AutomatonnetzState::update_outputs(bool chord_changed, int transpose, int inversion) { + + int32_t root = + quantizer.Process(OC::ADC::raw_pitch_value(ADC_CHANNEL_1)) + transpose; + + inversion += ((OC::ADC::value() + 255) >> 9); + CONSTRAIN(inversion, CELL_MIN_INVERSION * 2, CELL_MAX_INVERSION * 2); + + tonnetz_state.render(root, inversion); + + switch (output_mode()) { + case OUTPUTA_MODE_ROOT: + OC::DAC::set_voltage_scaled_semitone(tonnetz_state.outputs(0), octave(), OC::DAC::get_voltage_scaling(DAC_CHANNEL_A)); + break; + case OUTPUTA_MODE_TRIG: + if (chord_changed) { + trigger_out_ticks_ = kTriggerOutTicks; + OC::DAC::set_octave(DAC_CHANNEL_A, 5); + } + break; + case OUTPUTA_MODE_ARP: + OC::DAC::set_voltage_scaled_semitone(tonnetz_state.outputs(arp_index_ + 1), octave(), OC::DAC::get_voltage_scaling(DAC_CHANNEL_A)); + case OUTPUTA_MODE_STRUM: + if (!strum_inhibit_) + OC::DAC::set_voltage_scaled_semitone(tonnetz_state.outputs(arp_index_ + 1), octave(), OC::DAC::get_voltage_scaling(DAC_CHANNEL_A)); + break; + case OUTPUTA_MODE_LAST: + default: + break; + } + + OC::DAC::set_voltage_scaled_semitone(tonnetz_state.outputs(1), octave(), OC::DAC::get_voltage_scaling(DAC_CHANNEL_B)); + OC::DAC::set_voltage_scaled_semitone(tonnetz_state.outputs(2), octave(), OC::DAC::get_voltage_scaling(DAC_CHANNEL_C)); + OC::DAC::set_voltage_scaled_semitone(tonnetz_state.outputs(3), octave(), OC::DAC::get_voltage_scaling(DAC_CHANNEL_D)); +} + +void AutomatonnetzState::update_trigger_out() { + if (trigger_out_ticks_) { + uint32_t ticks = trigger_out_ticks_; + --ticks; + if (!ticks) + OC::DAC::set_octave(DAC_CHANNEL_A, 0); + trigger_out_ticks_ = ticks; + } +} + +void Automatonnetz_loop() { +} + +void FASTRUN Automatonnetz_isr() { + // All user actions, etc. handled in ::Update + automatonnetz_state.ISR(); +} + +static const weegfx::coord_t kGridXStart = 0; +static const weegfx::coord_t kGridYStart = 2; +static const weegfx::coord_t kGridH = 12; +static const weegfx::coord_t kGridW = 12; +static const weegfx::coord_t kMenuStartX = 62; +static const weegfx::coord_t kLineHeight = 11; + +namespace automatonnetz { + +void draw_cell_menu() { + + menu::TitleBar::Draw(); + graphics.print("CELL "); + graphics.print(automatonnetz_state.ui.selected_cell / 5 + 1); + graphics.print(','); + graphics.print(automatonnetz_state.ui.selected_cell % 5 + 1); + + const TransformCell &cell = automatonnetz_state.grid.at(automatonnetz_state.ui.selected_cell); + + menu::SettingsList settings_list(automatonnetz_state.ui.cell_cursor); + menu::SettingsListItem list_item; + while (settings_list.available()) { + const int current = settings_list.Next(list_item); + list_item.DrawDefault(cell.get_value(current), TransformCell::value_attr(current)); + } +} + +void draw_grid_menu() { + EMode mode = automatonnetz_state.tonnetz_state.current_chord().mode(); + int outputs[4]; + automatonnetz_state.tonnetz_state.get_outputs(outputs); + + menu::TitleBar::Draw(); + for (size_t i=1; i < 4; ++i) { + graphics.print(note_name(outputs[i])); + graphics.movePrintPos(weegfx::Graphics::kFixedFontW/2, 0); + } + if (MODE_MAJOR == mode) + graphics.print('+'); + else + graphics.print('-'); + + menu::SettingsList settings_list(automatonnetz_state.ui.grid_cursor); + menu::SettingsListItem list_item; + while (settings_list.available()) { + const int current = settings_list.Next(list_item); + const int value = automatonnetz_state.get_value(current); + const settings::value_attr &attr = AutomatonnetzState::value_attr(current); + + if (current <= GRID_SETTING_DY) { + const int integral = value / 8; + const int fraction = value % 8; + char value_str[6]; + memcpy(value_str, clock_fraction_names[fraction], 6); + if (integral || !fraction) + value_str[0] = (char)('0' + integral); + + list_item.valuex = list_item.endx - 30; + list_item.DrawDefault(value_str, value, attr); + } else { + list_item.DrawDefault(value, attr); + } + } +} + +}; // namespace automatonnetz + +void Automatonnetz_menu() { + uint16_t row = 0, col = 0; + for (int i = 0; i < GRID_CELLS; ++i) { + + const TransformCell &cell = automatonnetz_state.grid.at(i); + weegfx::coord_t x = kGridXStart + col * kGridW; + weegfx::coord_t y = kGridYStart + row * kGridH; + + graphics.setPrintPos(x + 3, y + 3); + graphics.print(tonnetz::transform_names[cell.transform()]); + + if (i == automatonnetz_state.ui.selected_cell) + graphics.drawFrame(x, y, kGridW, kGridH); + + if (col < GRID_DIMENSION - 1) { + ++col; + } else { + ++row; + col = 0; + } + } + + const vec2 current_pos = automatonnetz_state.grid.current_pos(); + graphics.invertRect(kGridXStart + current_pos.x * kGridW + 1, + kGridYStart + current_pos.y * kGridH + 1, + kGridW - 2, kGridH - 2); + + if (automatonnetz_state.ui.edit_cell) + automatonnetz::draw_cell_menu(); + else + automatonnetz::draw_grid_menu(); +} + +static const weegfx::coord_t kScreenSaverGridX = kGridXStart + kGridW / 2; +static const weegfx::coord_t kScreenSaverGridY = kGridYStart + kGridH / 2; +static const weegfx::coord_t kScreenSaverGridW = kGridW; +static const weegfx::coord_t kScreenSaverGridH = kGridH; + +static constexpr weegfx::coord_t kNoteCircleX = 96; +static constexpr weegfx::coord_t kNoteCircleY = 32; + +inline vec2 extract_pos(uint32_t history) { + return vec2((history & 0xff) / GRID_DIMENSION, (history & 0xff) % GRID_DIMENSION); +} + +void Automatonnetz_screensaver() { + int outputs[4]; + automatonnetz_state.tonnetz_state.get_outputs(outputs); + uint32_t cell_history = automatonnetz_state.history(); + + uint8_t normalized[3]; + for (size_t i=0; i < 3; ++i) + normalized[i] = (outputs[i + 1] + 120) % 12; + OC::visualize_pitch_classes(normalized, kNoteCircleX, kNoteCircleY); + + vec2 last_pos = extract_pos(cell_history); + cell_history >>= 8; + graphics.drawBitmap8(kScreenSaverGridX + last_pos.x * kScreenSaverGridW - 3, + kScreenSaverGridY + last_pos.y * kScreenSaverGridH - 3, + 8, OC::circle_disk_bitmap_8x8); + for (size_t i = 1; i < AutomatonnetzState::HISTORY_LENGTH; ++i, cell_history >>= 8) { + const vec2 current = extract_pos(cell_history); + graphics.drawLine(kScreenSaverGridX + last_pos.x * kScreenSaverGridW, kScreenSaverGridY + last_pos.y * kScreenSaverGridH, + kScreenSaverGridX + current.x * kScreenSaverGridW, kScreenSaverGridY + current.y * kScreenSaverGridH); + + graphics.drawBitmap8(kScreenSaverGridX + current.x * kScreenSaverGridW - 3, + kScreenSaverGridY + current.y * kScreenSaverGridH - 3, + 8, OC::circle_bitmap_8x8); + last_pos = current; + } +} + +size_t Automatonnetz_save(void *dest) { + char *storage = static_cast(dest); + size_t used = automatonnetz_state.Save(storage); + for (size_t cell = 0; cell < GRID_CELLS; ++cell) + used += automatonnetz_state.cells_[cell].Save(storage + used); + + return used; +} + +size_t Automatonnetz_restore(const void *dest) { + const char *storage = static_cast(dest); + size_t used = automatonnetz_state.Restore(storage); + for (size_t cell = 0; cell < GRID_CELLS; ++cell) + used += automatonnetz_state.cells_[cell].Restore(storage + used); + + return used; +} + +void Automatonnetz_handleAppEvent(OC::AppEvent event) { + switch (event) { + case OC::APP_EVENT_RESUME: + OC::ui.encoder_enable_acceleration(OC::CONTROL_ENCODER_L, false); + automatonnetz_state.AddUserAction(USER_ACTION_RESET); + break; + case OC::APP_EVENT_SUSPEND: + case OC::APP_EVENT_SCREENSAVER_ON: + case OC::APP_EVENT_SCREENSAVER_OFF: + break; + } +} + +void Automatonnetz_handleButtonEvent(const UI::Event &event) { + if (UI::EVENT_BUTTON_PRESS == event.type) { + switch (event.control) { + case OC::CONTROL_BUTTON_UP: + automatonnetz_state.AddUserAction(USER_ACTION_RESET); + break; + case OC::CONTROL_BUTTON_DOWN: + automatonnetz_state.AddUserAction(USER_ACTION_CLOCK); + break; + case OC::CONTROL_BUTTON_L: + automatonnetz_state.ui.edit_cell = !automatonnetz_state.ui.edit_cell; + break; + case OC::CONTROL_BUTTON_R: + if (automatonnetz_state.ui.edit_cell) + automatonnetz_state.ui.cell_cursor.toggle_editing(); + else + automatonnetz_state.ui.grid_cursor.toggle_editing(); + break; + } + } else if (UI::EVENT_BUTTON_LONG_PRESS == event.type && OC::CONTROL_BUTTON_L == event.control) { + automatonnetz_state.ClearGrid(); + // Forcing reset might make critical section even less necesary... + automatonnetz_state.AddUserAction(USER_ACTION_RESET); + } +} + +void Automatonnetz_handleEncoderEvent(const UI::Event &event) { + + if (OC::CONTROL_ENCODER_L == event.control) { + int selected = automatonnetz_state.ui.selected_cell + event.value; + CONSTRAIN(selected, 0, GRID_CELLS - 1); + automatonnetz_state.ui.selected_cell = selected; + } else if (OC::CONTROL_ENCODER_R == event.control) { + if (automatonnetz_state.ui.edit_cell) { + if (automatonnetz_state.ui.cell_cursor.editing()) { + TransformCell &cell = automatonnetz_state.grid.mutable_cell(automatonnetz_state.ui.selected_cell); + cell.change_value(automatonnetz_state.ui.cell_cursor.cursor_pos(), event.value); + } else { + automatonnetz_state.ui.cell_cursor.Scroll(event.value); + } + } else { + if (automatonnetz_state.ui.grid_cursor.editing()) { + automatonnetz_state.change_value(automatonnetz_state.ui.grid_cursor.cursor_pos(), event.value); + } else { + automatonnetz_state.ui.grid_cursor.Scroll(event.value); + } + } + } +} + +#endif // ENABLE_APP_AUTOMATONNETZ diff --git a/software/o_c_REV/APP_BBGEN.ino b/software/o_c_REV/APP_BBGEN.ino new file mode 100644 index 000000000..8d293cf46 --- /dev/null +++ b/software/o_c_REV/APP_BBGEN.ino @@ -0,0 +1,411 @@ +// Copyright (c) 2015, 2016 Patrick Dowling, Max Stadler, Tim Churches +// +// Author of original O+C firmware: Max Stadler (mxmlnstdlr@gmail.com) +// Author of app scaffolding: Patrick Dowling (pld@gurkenkiste.com) +// Modified for bouncing balls: Tim Churches (tim.churches@gmail.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// Bouncing balls app + +#ifdef ENABLE_APP_BBGEN + +#include "OC_apps.h" +#include "OC_bitmaps.h" +#include "OC_digital_inputs.h" +#include "OC_strings.h" +#include "util/util_math.h" +#include "util/util_settings.h" +#include "OC_menus.h" +#include "peaks_bouncing_balls.h" + +enum BouncingBallSettings { + BB_SETTING_GRAVITY, + BB_SETTING_BOUNCE_LOSS, + BB_SETTING_INITIAL_AMPLITUDE, + BB_SETTING_INITIAL_VELOCITY, + BB_SETTING_TRIGGER_INPUT, + BB_SETTING_RETRIGGER_BOUNCES, + BB_SETTING_CV1, + BB_SETTING_CV2, + BB_SETTING_CV3, + BB_SETTING_CV4, + BB_SETTING_HARD_RESET, + BB_SETTING_LAST +}; + +enum BallCVMapping { + BB_CV_MAPPING_NONE, + BB_CV_MAPPING_GRAVITY, + BB_CV_MAPPING_BOUNCE_LOSS, + BB_CV_MAPPING_INITIAL_AMPLITUDE, + BB_CV_MAPPING_INITIAL_VELOCITY, + BB_CV_MAPPING_RETRIGGER_BOUNCES, + BB_CV_MAPPING_LAST +}; + +namespace menu = OC::menu; + +class BouncingBall : public settings::SettingsBase { +public: + + static constexpr int kMaxBouncingBallParameters = 5; + + void Init(OC::DigitalInput default_trigger); + + OC::DigitalInput get_trigger_input() const { + return static_cast(values_[BB_SETTING_TRIGGER_INPUT]); + } + + BallCVMapping get_cv1_mapping() const { + return static_cast(values_[BB_SETTING_CV1]); + } + + BallCVMapping get_cv2_mapping() const { + return static_cast(values_[BB_SETTING_CV2]); + } + + BallCVMapping get_cv3_mapping() const { + return static_cast(values_[BB_SETTING_CV3]); + } + + BallCVMapping get_cv4_mapping() const { + return static_cast(values_[BB_SETTING_CV4]); + } + + bool get_hard_reset() const { + return values_[BB_SETTING_HARD_RESET]; + } + + uint8_t get_initial_amplitude() const { + return values_[BB_SETTING_INITIAL_AMPLITUDE]; + } + + uint8_t get_initial_velocity() const { + return values_[BB_SETTING_INITIAL_VELOCITY]; + } + + uint8_t get_gravity() const { + return values_[BB_SETTING_GRAVITY]; + } + + uint8_t get_bounce_loss() const { + return values_[BB_SETTING_BOUNCE_LOSS]; + } + + int32_t get_retrigger_bounces() const { + return static_cast(values_[BB_SETTING_RETRIGGER_BOUNCES]); + } + +#ifdef BBGEN_DEBUG + uint16_t get_channel_parameter_value(uint8_t param) { + return s[param]; + } + + int16_t get_channel_retrigger_bounces() { + return(bb_.get_retrigger_bounces()) ; + } +#endif // BBGEN_DEBUG + + inline void apply_cv_mapping(BouncingBallSettings cv_setting, const int32_t cvs[ADC_CHANNEL_LAST], int32_t segments[kMaxBouncingBallParameters]) { + int mapping = values_[cv_setting]; + uint8_t bb_cv_rshift = 13 ; + switch (mapping) { + case BB_CV_MAPPING_GRAVITY: + case BB_CV_MAPPING_BOUNCE_LOSS: + case BB_CV_MAPPING_INITIAL_VELOCITY: + bb_cv_rshift = 13 ; + break ; + case BB_CV_MAPPING_INITIAL_AMPLITUDE: + bb_cv_rshift = 12 ; + break; + case BB_CV_MAPPING_RETRIGGER_BOUNCES: + bb_cv_rshift = 14 ; + break; + default: + bb_cv_rshift = 13 ; + break; + } + if (mapping) + segments[mapping - BB_CV_MAPPING_GRAVITY] += (cvs[cv_setting - BB_SETTING_CV1]) << (16 - bb_cv_rshift) ; + } + + template + void Update(uint32_t triggers, const int32_t cvs[ADC_CHANNEL_LAST]) { + + s[0] = SCALE8_16(static_cast(get_gravity())); + s[1] = SCALE8_16(static_cast(get_bounce_loss())); + s[2] = SCALE8_16(static_cast(get_initial_amplitude())); + s[3] = SCALE8_16(static_cast(get_initial_velocity())); + s[4] = SCALE8_16(static_cast(get_retrigger_bounces())); + + apply_cv_mapping(BB_SETTING_CV1, cvs, s); + apply_cv_mapping(BB_SETTING_CV2, cvs, s); + apply_cv_mapping(BB_SETTING_CV3, cvs, s); + apply_cv_mapping(BB_SETTING_CV4, cvs, s); + + s[0] = USAT16(s[0]); + s[1] = USAT16(s[1]); + s[2] = USAT16(s[2]); + s[3] = USAT16(s[3]); + s[4] = USAT16(s[4]); + + bb_.Configure(s) ; + + // hard reset forces the bouncing ball to start at level_[0] on rising gate. + bb_.set_hard_reset(get_hard_reset()); + + OC::DigitalInput trigger_input = get_trigger_input(); + uint8_t gate_state = 0; + if (triggers & DIGITAL_INPUT_MASK(trigger_input)) + gate_state |= peaks::CONTROL_GATE_RISING; + + bool gate_raised = OC::DigitalInputs::read_immediate(trigger_input); + if (gate_raised) + gate_state |= peaks::CONTROL_GATE; + else if (gate_raised_) + gate_state |= peaks::CONTROL_GATE_FALLING; + gate_raised_ = gate_raised; + + // TODO Scale range or offset? + uint32_t value = OC::DAC::get_zero_offset(dac_channel) + bb_.ProcessSingleSample(gate_state, OC::DAC::MAX_VALUE - OC::DAC::get_zero_offset(dac_channel)); + OC::DAC::set(value); + } + + +private: + peaks::BouncingBall bb_; + bool gate_raised_; + int32_t s[kMaxBouncingBallParameters]; + +}; + +void BouncingBall::Init(OC::DigitalInput default_trigger) { + InitDefaults(); + apply_value(BB_SETTING_TRIGGER_INPUT, default_trigger); + bb_.Init(); + gate_raised_ = false; +} + +const char* const bb_cv_mapping_names[BB_CV_MAPPING_LAST] = { + "off", "grav", "bnce", "ampl", "vel", "retr" +}; + +// TOTAL EEPROM SIZE: 4 * 9 bytes +SETTINGS_DECLARE(BouncingBall, BB_SETTING_LAST) { + { 128, 0, 255, "Gravity", NULL, settings::STORAGE_TYPE_U8 }, + { 96, 0, 255, "Bounce loss", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 255, "Amplitude", NULL, settings::STORAGE_TYPE_U8 }, + { 228, 0, 255, "Velocity", NULL, settings::STORAGE_TYPE_U8 }, + { OC::DIGITAL_INPUT_1, OC::DIGITAL_INPUT_1, OC::DIGITAL_INPUT_4, "Trigger input", OC::Strings::trigger_input_names, settings::STORAGE_TYPE_U8 }, + { 0, 0, 255, "Retrigger", NULL, settings::STORAGE_TYPE_U8 }, + { BB_CV_MAPPING_NONE, BB_CV_MAPPING_NONE, BB_CV_MAPPING_LAST - 1, "CV1 -> ", bb_cv_mapping_names, settings::STORAGE_TYPE_U4 }, + { BB_CV_MAPPING_NONE, BB_CV_MAPPING_NONE, BB_CV_MAPPING_LAST - 1, "CV2 -> ", bb_cv_mapping_names, settings::STORAGE_TYPE_U4 }, + { BB_CV_MAPPING_NONE, BB_CV_MAPPING_NONE, BB_CV_MAPPING_LAST - 1, "CV3 -> ", bb_cv_mapping_names, settings::STORAGE_TYPE_U4 }, + { BB_CV_MAPPING_NONE, BB_CV_MAPPING_NONE, BB_CV_MAPPING_LAST - 1, "CV4 -> ", bb_cv_mapping_names, settings::STORAGE_TYPE_U4 }, + { 0, 0, 1, "Hard reset", OC::Strings::no_yes, settings::STORAGE_TYPE_U8 }, +}; + +class QuadBouncingBalls { +public: + static constexpr int32_t kCvSmoothing = 16; + + // bb = env, balls_ = envelopes_, BouncingBall = EnvelopeGenerator + // QuadBouncingBalls = QuadEnvelopeGenerator, bbgen = envgen, BBGEN = ENVGEN + + void Init() { + int input = OC::DIGITAL_INPUT_1; + for (auto &bb : balls_) { + bb.Init(static_cast(input)); + ++input; + } + + ui.left_encoder_value = 0; + ui.left_edit_mode = MODE_EDIT_SETTINGS; + ui.selected_channel = 0; + ui.selected_segment = 0; + ui.cursor.Init(BB_SETTING_GRAVITY, BB_SETTING_LAST - 1); + } + + void ISR() { + cv1.push(OC::ADC::value()); + cv2.push(OC::ADC::value()); + cv3.push(OC::ADC::value()); + cv4.push(OC::ADC::value()); + + const int32_t cvs[ADC_CHANNEL_LAST] = { cv1.value(), cv2.value(), cv3.value(), cv4.value() }; + uint32_t triggers = OC::DigitalInputs::clocked(); + + balls_[0].Update(triggers, cvs); + balls_[1].Update(triggers, cvs); + balls_[2].Update(triggers, cvs); + balls_[3].Update(triggers, cvs); + } + + enum LeftEditMode { + MODE_SELECT_CHANNEL, + MODE_EDIT_SETTINGS + }; + + struct { + LeftEditMode left_edit_mode; + int left_encoder_value; + + int selected_channel; + int selected_segment; + menu::ScreenCursor cursor; + } ui; + + BouncingBall &selected() { + return balls_[ui.selected_channel]; + } + + BouncingBall balls_[4]; + + SmoothedValue cv1; + SmoothedValue cv2; + SmoothedValue cv3; + SmoothedValue cv4; +}; + +QuadBouncingBalls bbgen; + +void BBGEN_init() { + bbgen.Init(); +} + +size_t BBGEN_storageSize() { + return 4 * BouncingBall::storageSize(); +} + +size_t BBGEN_save(void *storage) { + size_t s = 0; + for (auto &bb : bbgen.balls_) + s += bb.Save(static_cast(storage) + s); + return s; +} + +size_t BBGEN_restore(const void *storage) { + size_t s = 0; + for (auto &bb : bbgen.balls_) + s += bb.Restore(static_cast(storage) + s); + return s; +} + +void BBGEN_handleAppEvent(OC::AppEvent event) { + switch (event) { + case OC::APP_EVENT_RESUME: + bbgen.ui.cursor.set_editing(false); + break; + case OC::APP_EVENT_SUSPEND: + case OC::APP_EVENT_SCREENSAVER_ON: + case OC::APP_EVENT_SCREENSAVER_OFF: + break; + } +} + +void BBGEN_loop() { +} + +void BBGEN_menu() { + + menu::QuadTitleBar::Draw(); + for (uint_fast8_t i = 0; i < 4; ++i) { + menu::QuadTitleBar::SetColumn(i); + graphics.print((char)('A' + i)); + } + menu::QuadTitleBar::Selected(bbgen.ui.selected_channel); + + auto const &bb = bbgen.selected(); + menu::SettingsList settings_list(bbgen.ui.cursor); + menu::SettingsListItem list_item; + while (settings_list.available()) { + const int current = settings_list.Next(list_item); + list_item.DrawDefault(bb.get_value(current), BouncingBall::value_attr(current)); + } +} + +void BBGEN_topButton() { + auto &selected_bb = bbgen.selected(); + selected_bb.change_value(BB_SETTING_GRAVITY + bbgen.ui.selected_segment, 32); +} + +void BBGEN_lowerButton() { + auto &selected_bb = bbgen.selected(); + selected_bb.change_value(BB_SETTING_GRAVITY + bbgen.ui.selected_segment, -32); +} + +void BBGEN_handleButtonEvent(const UI::Event &event) { + if (UI::EVENT_BUTTON_PRESS == event.type) { + switch (event.control) { + case OC::CONTROL_BUTTON_UP: + BBGEN_topButton(); + break; + case OC::CONTROL_BUTTON_DOWN: + BBGEN_lowerButton(); + break; + case OC::CONTROL_BUTTON_L: + break; + case OC::CONTROL_BUTTON_R: + bbgen.ui.cursor.toggle_editing(); + break; + } + } +} + +void BBGEN_handleEncoderEvent(const UI::Event &event) { + + if (OC::CONTROL_ENCODER_L == event.control) { + int left_value = bbgen.ui.selected_channel + event.value; + CONSTRAIN(left_value, 0, 3); + bbgen.ui.selected_channel = left_value; + } else if (OC::CONTROL_ENCODER_R == event.control) { + if (bbgen.ui.cursor.editing()) { + auto &selected = bbgen.selected(); + selected.change_value(bbgen.ui.cursor.cursor_pos(), event.value); + } else { + bbgen.ui.cursor.Scroll(event.value); + } + } +} + +#ifdef BBGEN_DEBUG +void BBGEN_debug() { + graphics.setPrintPos(2, 12); + graphics.print(bbgen.cv1.value()); + graphics.setPrintPos(32, 12); + graphics.print(bbgen.balls_[0].get_channel_retrigger_bounces()); + graphics.setPrintPos(2, 22); + graphics.print(bbgen.cv2.value()); + graphics.setPrintPos(2, 32); + graphics.print(bbgen.cv3.value()); + graphics.setPrintPos(2, 42); + graphics.print(bbgen.cv4.value()); +} +#endif // BBGEN_DEBUG + +void BBGEN_screensaver() { + OC::scope_render(); +} + +void FASTRUN BBGEN_isr() { + bbgen.ISR(); +} + +#endif // ENABLE_APP_BBGEN diff --git a/software/o_c_REV/APP_BYTEBEATGEN.ino b/software/o_c_REV/APP_BYTEBEATGEN.ino new file mode 100644 index 000000000..b64302ff2 --- /dev/null +++ b/software/o_c_REV/APP_BYTEBEATGEN.ino @@ -0,0 +1,609 @@ +// Copyright (c) 2015, 2016 Patrick Dowling, Max Stadler, Tim Churches +// +// Author of original O+C firmware: Max Stadler (mxmlnstdlr@gmail.com) +// Author of app scaffolding: Patrick Dowling (pld@gurkenkiste.com) +// Modified for byte beats: Tim Churches (tim.churches@gmail.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// Byte beats app + +#ifdef ENABLE_APP_BYTEBEATGEN + +#include "OC_apps.h" +#include "OC_bitmaps.h" +#include "OC_digital_inputs.h" +#include "OC_strings.h" +#include "util/util_history.h" +#include "util/util_math.h" +#include "util/util_settings.h" +#include "OC_menus.h" +#include "peaks_bytebeat.h" + +enum ByteBeatSettings { + BYTEBEAT_SETTING_EQUATION, + BYTEBEAT_SETTING_SPEED, + BYTEBEAT_SETTING_PITCH, + BYTEBEAT_SETTING_P0, + BYTEBEAT_SETTING_P1, + BYTEBEAT_SETTING_P2, + BYTEBEAT_SETTING_LOOP_MODE, + BYTEBEAT_SETTING_LOOP_START, + BYTEBEAT_SETTING_LOOP_START_MED, + BYTEBEAT_SETTING_LOOP_START_FINE, + BYTEBEAT_SETTING_LOOP_END, + BYTEBEAT_SETTING_LOOP_END_MED, + BYTEBEAT_SETTING_LOOP_END_FINE, + BYTEBEAT_SETTING_TRIGGER_INPUT, + BYTEBEAT_SETTING_STEP_MODE, + BYTEBEAT_SETTING_CV1, + BYTEBEAT_SETTING_CV2, + BYTEBEAT_SETTING_CV3, + BYTEBEAT_SETTING_CV4, + BYTEBEAT_SETTING_LAST, + BYTEBEAT_SETTING_FIRST=BYTEBEAT_SETTING_EQUATION, +}; + +enum ByteBeatCVMapping { + BYTEBEAT_CV_MAPPING_NONE, + BYTEBEAT_CV_MAPPING_EQUATION, + BYTEBEAT_CV_MAPPING_SPEED, + BYTEBEAT_CV_MAPPING_P0, + BYTEBEAT_CV_MAPPING_P1, + BYTEBEAT_CV_MAPPING_P2, + BYTEBEAT_CV_MAPPING_LOOP_START, + BYTEBEAT_CV_MAPPING_LOOP_START_MED, + BYTEBEAT_CV_MAPPING_LOOP_START_FINE, + BYTEBEAT_CV_MAPPING_LOOP_END, + BYTEBEAT_CV_MAPPING_LOOP_END_MED, + BYTEBEAT_CV_MAPPING_LOOP_END_FINE, + BYTEBEAT_CV_MAPPING_PITCH, + BYTEBEAT_CV_MAPPING_LAST, + BYTEBEAT_CV_MAPPING_FIRST=BYTEBEAT_CV_MAPPING_EQUATION + +}; + +namespace menu = OC::menu; + +class ByteBeat : public settings::SettingsBase { +public: + + static constexpr size_t kHistoryDepth = 64; + static constexpr int kMaxByteBeatParameters = 12; + + void Init(OC::DigitalInput default_trigger); + + OC::DigitalInput get_trigger_input() const { + return static_cast(values_[BYTEBEAT_SETTING_TRIGGER_INPUT]); + } + + ByteBeatCVMapping get_cv1_mapping() const { + return static_cast(values_[BYTEBEAT_SETTING_CV1]); + } + + ByteBeatCVMapping get_cv2_mapping() const { + return static_cast(values_[BYTEBEAT_SETTING_CV2]); + } + + ByteBeatCVMapping get_cv3_mapping() const { + return static_cast(values_[BYTEBEAT_SETTING_CV3]); + } + + ByteBeatCVMapping get_cv4_mapping() const { + return static_cast(values_[BYTEBEAT_SETTING_CV4]); + } + + uint8_t get_equation() const { + return values_[BYTEBEAT_SETTING_EQUATION]; + } + + bool get_step_mode() const { + return values_[BYTEBEAT_SETTING_STEP_MODE]; + } + + uint8_t get_speed() const { + return values_[BYTEBEAT_SETTING_SPEED]; + } + + uint8_t get_pitch() const { + return values_[BYTEBEAT_SETTING_PITCH]; + } + + uint8_t get_p0() const { + return values_[BYTEBEAT_SETTING_P0]; + } + + uint8_t get_p1() const { + return values_[BYTEBEAT_SETTING_P1]; + } + + uint8_t get_p2() const { + return values_[BYTEBEAT_SETTING_P2]; + } + + bool get_loop_mode() const { + return values_[BYTEBEAT_SETTING_LOOP_MODE]; + } + + uint8_t get_loop_start() const { + return values_[BYTEBEAT_SETTING_LOOP_START]; + } + + uint8_t get_loop_start_med() const { + return values_[BYTEBEAT_SETTING_LOOP_START_MED]; + } + + uint8_t get_loop_start_fine() const { + return values_[BYTEBEAT_SETTING_LOOP_START_FINE]; + } + + uint8_t get_loop_end() const { + return values_[BYTEBEAT_SETTING_LOOP_END]; + } + + uint8_t get_loop_end_med() const { + return values_[BYTEBEAT_SETTING_LOOP_END_MED]; + } + + uint8_t get_loop_end_fine() const { + return values_[BYTEBEAT_SETTING_LOOP_END_FINE]; + } + + int32_t get_s(uint8_t param) { + return s_[param]; + } + + uint32_t get_t() { + return static_cast(bytebeat_.get_t()); + } + + uint32_t get_eqn_num() { + return static_cast(bytebeat_.get_eqn_num()); + } + + uint32_t get_phase() { + return static_cast(bytebeat_.get_phase()); + } + + uint32_t get_instance_loop_start() { + return static_cast(bytebeat_.get_loop_start()); + } + + uint32_t get_instance_loop_end() { + return static_cast(bytebeat_.get_loop_end()); + } + + uint16_t get_bytepitch() { + return static_cast(bytebeat_.get_bytepitch()); + } + + // Begin conditional menu items infrastructure + // Maintain an internal list of currently available settings, since some are + // dependent on others. It's kind of brute force, but eh, works :) If other + // apps have a similar need, it can be moved to a common wrapper + + int num_enabled_settings() const { + return num_enabled_settings_; + } + + ByteBeatSettings enabled_setting_at(int index) const { + return enabled_settings_[index]; + } + + void update_enabled_settings() { + ByteBeatSettings *settings = enabled_settings_; + *settings++ = BYTEBEAT_SETTING_EQUATION; + *settings++ = BYTEBEAT_SETTING_SPEED; + *settings++ = BYTEBEAT_SETTING_PITCH; + *settings++ = BYTEBEAT_SETTING_P0; + *settings++ = BYTEBEAT_SETTING_P1; + *settings++ = BYTEBEAT_SETTING_P2; + *settings++ = BYTEBEAT_SETTING_LOOP_MODE; + if (get_loop_mode()) { + *settings++ = BYTEBEAT_SETTING_LOOP_START; + *settings++ = BYTEBEAT_SETTING_LOOP_START_MED; + *settings++ = BYTEBEAT_SETTING_LOOP_START_FINE; + *settings++ = BYTEBEAT_SETTING_LOOP_END; + *settings++ = BYTEBEAT_SETTING_LOOP_END_MED; + *settings++ = BYTEBEAT_SETTING_LOOP_END_FINE; + } + *settings++ = BYTEBEAT_SETTING_TRIGGER_INPUT; + *settings++ = BYTEBEAT_SETTING_STEP_MODE; + *settings++ = BYTEBEAT_SETTING_CV1; + *settings++ = BYTEBEAT_SETTING_CV2; + *settings++ = BYTEBEAT_SETTING_CV3; + *settings++ = BYTEBEAT_SETTING_CV4; + + num_enabled_settings_ = settings - enabled_settings_; + } + + static bool indentSetting(ByteBeatSettings s) { + switch (s) { + case BYTEBEAT_SETTING_LOOP_START: + case BYTEBEAT_SETTING_LOOP_START_MED: + case BYTEBEAT_SETTING_LOOP_START_FINE: + case BYTEBEAT_SETTING_LOOP_END: + case BYTEBEAT_SETTING_LOOP_END_MED: + case BYTEBEAT_SETTING_LOOP_END_FINE: + return true; + default: break; + } + return false; + } + // end conditional menu items infrastructure + + inline void apply_cv_mapping(ByteBeatSettings cv_setting, const int32_t cvs[ADC_CHANNEL_LAST], int32_t segments[kMaxByteBeatParameters]) { + int mapping = values_[cv_setting]; + uint8_t bytebeat_cv_rshift = 12; + switch (mapping) { + case BYTEBEAT_CV_MAPPING_EQUATION: + case BYTEBEAT_CV_MAPPING_SPEED: + case BYTEBEAT_CV_MAPPING_PITCH: + case BYTEBEAT_CV_MAPPING_P0: + case BYTEBEAT_CV_MAPPING_P1: + case BYTEBEAT_CV_MAPPING_P2: + bytebeat_cv_rshift = 12; + break; + case BYTEBEAT_CV_MAPPING_LOOP_START: + case BYTEBEAT_CV_MAPPING_LOOP_START_MED: + case BYTEBEAT_CV_MAPPING_LOOP_START_FINE: + case BYTEBEAT_CV_MAPPING_LOOP_END: + case BYTEBEAT_CV_MAPPING_LOOP_END_MED: + case BYTEBEAT_CV_MAPPING_LOOP_END_FINE: + bytebeat_cv_rshift = 12; + default: + break; + } + if (mapping) + segments[mapping - BYTEBEAT_CV_MAPPING_FIRST] += (cvs[cv_setting - BYTEBEAT_SETTING_CV1] * 65536) >> bytebeat_cv_rshift; + } + + template + void Update(uint32_t triggers, const int32_t cvs[ADC_CHANNEL_LAST]) { + + int32_t s[kMaxByteBeatParameters]; + s[0] = SCALE8_16(static_cast(get_equation() << 4)); + s[1] = SCALE8_16(static_cast(get_speed())); + s[2] = SCALE8_16(static_cast(get_p0())); + s[3] = SCALE8_16(static_cast(get_p1())); + s[4] = SCALE8_16(static_cast(get_p2())); + s[5] = SCALE8_16(static_cast(get_loop_start())); + s[6] = SCALE8_16(static_cast(get_loop_start_med())); + s[7] = SCALE8_16(static_cast(get_loop_start_fine())); + s[8] = SCALE8_16(static_cast(get_loop_end())); + s[9] = SCALE8_16(static_cast(get_loop_end_med())); + s[10] = SCALE8_16(static_cast(get_loop_end_fine())); + s[11] = SCALE8_16(static_cast(get_pitch())); + + apply_cv_mapping(BYTEBEAT_SETTING_CV1, cvs, s); + apply_cv_mapping(BYTEBEAT_SETTING_CV2, cvs, s); + apply_cv_mapping(BYTEBEAT_SETTING_CV3, cvs, s); + apply_cv_mapping(BYTEBEAT_SETTING_CV4, cvs, s); + + for (uint_fast8_t i = 0; i < 12; ++i) { + s[i] = USAT16(s[i]) ; + s_[i] = s[i] ; + } + + bytebeat_.Configure(s, get_step_mode(), get_loop_mode()) ; + + OC::DigitalInput trigger_input = get_trigger_input(); + uint8_t gate_state = 0; + if (triggers & DIGITAL_INPUT_MASK(trigger_input)) + gate_state |= peaks::CONTROL_GATE_RISING; + + bool gate_raised = OC::DigitalInputs::read_immediate(trigger_input); + if (gate_raised) + gate_state |= peaks::CONTROL_GATE; + else if (gate_raised_) + gate_state |= peaks::CONTROL_GATE_FALLING; + gate_raised_ = gate_raised; + + // TODO Scale range or offset? + uint16_t b = bytebeat_.ProcessSingleSample(gate_state); + #ifdef BUCHLA_4U + uint32_t value = OC::DAC::get_zero_offset(dac_channel) + b; + #else + uint32_t value = OC::DAC::get_zero_offset(dac_channel) + (int16_t)b; + #endif + OC::DAC::set(value); + + + b >>= 8; + if (b != history_.last()) // This make the effect a bit different + history_.Push(b); + } + + inline void ReadHistory(uint8_t *history) const { + history_.Read(history); + } + +private: + peaks::ByteBeat bytebeat_; + bool gate_raised_; + int32_t s_[kMaxByteBeatParameters]; + + int num_enabled_settings_; + ByteBeatSettings enabled_settings_[BYTEBEAT_SETTING_LAST]; + + util::History history_; +}; + +void ByteBeat::Init(OC::DigitalInput default_trigger) { + InitDefaults(); + apply_value(BYTEBEAT_SETTING_TRIGGER_INPUT, default_trigger); + bytebeat_.Init(); + gate_raised_ = false; + update_enabled_settings(); + history_.Init(0); +} + +const char* const bytebeat_cv_mapping_names[BYTEBEAT_CV_MAPPING_LAST] = { + "off", "equ", "spd", "p0", "p1", "p2", "beg++", "beg+", "beg", "end++", "end+", "end","pitch" +}; + +// TOTAL EEPROM SIZE: 4 * 16 bytes +SETTINGS_DECLARE(ByteBeat, BYTEBEAT_SETTING_LAST) { + { 0, 0, 15, "Equation", OC::Strings::bytebeat_equation_names, settings::STORAGE_TYPE_U8 }, + { 255, 0, 255, "Speed", NULL, settings::STORAGE_TYPE_U8 }, + { 1, 1, 255, "Pitch", NULL, settings::STORAGE_TYPE_U8 }, + { 126, 0, 255, "Parameter 0", NULL, settings::STORAGE_TYPE_U8 }, + { 126, 0, 255, "Parameter 1", NULL, settings::STORAGE_TYPE_U8 }, + { 127, 0, 255, "Parameter 2", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 1, "Loop mode", OC::Strings::no_yes, settings::STORAGE_TYPE_U8 }, + { 0, 0, 255, "Loop begin ++", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 255, "Loop begin +", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 255, "Loop begin", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 255, "Loop end ++", NULL, settings::STORAGE_TYPE_U8 }, + { 1, 0, 255, "Loop end +", NULL, settings::STORAGE_TYPE_U8 }, + { 255, 0, 255, "Loop end", NULL, settings::STORAGE_TYPE_U8 }, + { OC::DIGITAL_INPUT_1, OC::DIGITAL_INPUT_1, OC::DIGITAL_INPUT_4, "Trigger input", OC::Strings::trigger_input_names, settings::STORAGE_TYPE_U4 }, + { 0, 0, 1, "Step mode", OC::Strings::no_yes, settings::STORAGE_TYPE_U4 }, + { BYTEBEAT_CV_MAPPING_NONE, BYTEBEAT_CV_MAPPING_NONE, BYTEBEAT_CV_MAPPING_LAST - 1, "CV1 -> ", bytebeat_cv_mapping_names, settings::STORAGE_TYPE_U4 }, + { BYTEBEAT_CV_MAPPING_NONE, BYTEBEAT_CV_MAPPING_NONE, BYTEBEAT_CV_MAPPING_LAST - 1, "CV2 -> ", bytebeat_cv_mapping_names, settings::STORAGE_TYPE_U4 }, + { BYTEBEAT_CV_MAPPING_NONE, BYTEBEAT_CV_MAPPING_NONE, BYTEBEAT_CV_MAPPING_LAST - 1, "CV3 -> ", bytebeat_cv_mapping_names, settings::STORAGE_TYPE_U4 }, + { BYTEBEAT_CV_MAPPING_NONE, BYTEBEAT_CV_MAPPING_NONE, BYTEBEAT_CV_MAPPING_LAST - 1, "CV4 -> ", bytebeat_cv_mapping_names, settings::STORAGE_TYPE_U4 }, +}; + +class QuadByteBeats { +public: + static constexpr int32_t kCvSmoothing = 16; + + // bb = bytebeat, balls_ = bytebeats_, BouncingBall = Bytebeat + // QuadBouncingBalls = QuadByteBeats, bbgen = bytebeatgen, BBGEN = BYTEBEATGEN + + void Init() { + int input = OC::DIGITAL_INPUT_1; + for (auto &bytebeat : bytebeats_) { + bytebeat.Init(static_cast(input)); + ++input; + } + + ui.left_encoder_value = 0; + ui.left_edit_mode = MODE_EDIT_SETTINGS; + + ui.selected_channel = 0; + ui.selected_segment = 0; + ui.cursor.Init(BYTEBEAT_SETTING_EQUATION, BYTEBEAT_SETTING_LAST - 1); + ui.cursor.AdjustEnd(bytebeats_[0].num_enabled_settings() - 1); + } + + void ISR() { + cv1.push(OC::ADC::value()); + cv2.push(OC::ADC::value()); + cv3.push(OC::ADC::value()); + cv4.push(OC::ADC::value()); + + const int32_t cvs[ADC_CHANNEL_LAST] = { cv1.value(), cv2.value(), cv3.value(), cv4.value() }; + uint32_t triggers = OC::DigitalInputs::clocked(); + + bytebeats_[0].Update(triggers, cvs); + bytebeats_[1].Update(triggers, cvs); + bytebeats_[2].Update(triggers, cvs); + bytebeats_[3].Update(triggers, cvs); + } + + enum LeftEditMode { + MODE_SELECT_CHANNEL, + MODE_EDIT_SETTINGS + }; + + struct { + LeftEditMode left_edit_mode; + int left_encoder_value; + // bool editing; + + int selected_channel; + int selected_segment; + menu::ScreenCursor cursor; + } ui; + + ByteBeat &selected() { + return bytebeats_[ui.selected_channel]; + } + + ByteBeat bytebeats_[4]; + + SmoothedValue cv1; + SmoothedValue cv2; + SmoothedValue cv3; + SmoothedValue cv4; +}; + +QuadByteBeats bytebeatgen; + +void BYTEBEATGEN_init() { + bytebeatgen.Init(); +} + +size_t BYTEBEATGEN_storageSize() { + return 4 * ByteBeat::storageSize(); +} + +size_t BYTEBEATGEN_save(void *storage) { + size_t s = 0; + for (auto &bytebeat : bytebeatgen.bytebeats_) + s += bytebeat.Save(static_cast(storage) + s); + return s; +} + +size_t BYTEBEATGEN_restore(const void *storage) { + size_t s = 0; + for (auto &bytebeat : bytebeatgen.bytebeats_) { + s += bytebeat.Restore(static_cast(storage) + s); + bytebeat.update_enabled_settings(); + } + bytebeatgen.ui.cursor.AdjustEnd(bytebeatgen.bytebeats_[0].num_enabled_settings() - 1); + return s; +} + + +void BYTEBEATGEN_handleAppEvent(OC::AppEvent event) { + switch (event) { + case OC::APP_EVENT_RESUME: + bytebeatgen.ui.cursor.set_editing(false); + break; + case OC::APP_EVENT_SUSPEND: + case OC::APP_EVENT_SCREENSAVER_ON: + case OC::APP_EVENT_SCREENSAVER_OFF: + break; + } +} + +void BYTEBEATGEN_loop() { +} + +void BYTEBEATGEN_menu() { + + menu::QuadTitleBar::Draw(); + for (uint_fast8_t i = 0; i < 4; ++i) { + menu::QuadTitleBar::SetColumn(i); + graphics.print((char)('A' + i)); + } + menu::QuadTitleBar::Selected(bytebeatgen.ui.selected_channel); + + auto const &bytebeat = bytebeatgen.selected(); + menu::SettingsList settings_list(bytebeatgen.ui.cursor); + menu::SettingsListItem list_item; + while (settings_list.available()) { + const int setting = bytebeat.enabled_setting_at(settings_list.Next(list_item)); + const int value = bytebeat.get_value(setting); + const settings::value_attr &attr = ByteBeat::value_attr(setting); + if (ByteBeat::indentSetting(static_cast(setting))) + list_item.x += menu::kIndentDx; + list_item.DrawDefault(value, attr); + } +} + +void BYTEBEATGEN_topButton() { + auto &selected_bytebeat = bytebeatgen.selected(); + selected_bytebeat.change_value(BYTEBEAT_SETTING_EQUATION + bytebeatgen.ui.selected_segment, 1); +} + +void BYTEBEATGEN_lowerButton() { + auto &selected_bytebeat = bytebeatgen.selected(); + selected_bytebeat.change_value(BYTEBEAT_SETTING_EQUATION + bytebeatgen.ui.selected_segment, -1); +} + +void BYTEBEATGEN_handleButtonEvent(const UI::Event &event) { + if (UI::EVENT_BUTTON_PRESS == event.type) { + switch (event.control) { + case OC::CONTROL_BUTTON_UP: + BYTEBEATGEN_topButton(); + break; + case OC::CONTROL_BUTTON_DOWN: + BYTEBEATGEN_lowerButton(); + break; + case OC::CONTROL_BUTTON_L: + break; + case OC::CONTROL_BUTTON_R: + bytebeatgen.ui.cursor.toggle_editing(); + break; + } + } +} + +void BYTEBEATGEN_handleEncoderEvent(const UI::Event &event) { + + if (OC::CONTROL_ENCODER_L == event.control) { + int left_value = bytebeatgen.ui.selected_channel + event.value; + CONSTRAIN(left_value, 0, 3); + bytebeatgen.ui.selected_channel = left_value; + auto &selected = bytebeatgen.selected(); + bytebeatgen.ui.cursor.AdjustEnd(selected.num_enabled_settings() - 1); + } else if (OC::CONTROL_ENCODER_R == event.control) { + if (bytebeatgen.ui.cursor.editing()) { + auto &selected = bytebeatgen.selected(); + ByteBeatSettings setting = selected.enabled_setting_at(bytebeatgen.ui.cursor.cursor_pos()); + selected.change_value(setting, event.value); + switch (setting) { + case BYTEBEAT_SETTING_LOOP_MODE: + selected.update_enabled_settings(); + bytebeatgen.ui.cursor.AdjustEnd(selected.num_enabled_settings() - 1); + break; + default: break; + } + } else { + bytebeatgen.ui.cursor.Scroll(event.value); + } + } +} + +#ifdef BYTEBEAT_DEBUG +void BYTEBEATGEN_debug() { + for (int i = 0; i < 4; ++i) { + uint8_t ypos = 10*(i + 1) + 2 ; + graphics.setPrintPos(2, ypos); + graphics.print(i) ; + graphics.setPrintPos(12, ypos); + graphics.print("t=") ; + graphics.setPrintPos(40, ypos); + graphics.print(bytebeatgen.bytebeats_[i].get_eqn_num(), 12); + } +} +#endif // BYTEBEATGEN_DEBUG + +uint8_t bb_history[ByteBeat::kHistoryDepth]; + +void BYTEBEATGEN_screensaver() { + + // Display raw history values "radiating" from center point by mirroring + // on x and y. Oldest value is at start of buffer after reading history. + + weegfx::coord_t y = 0; + for (const auto & bb : bytebeatgen.bytebeats_) { + bb.ReadHistory(bb_history); + const uint8_t *history = bb_history + ByteBeat::kHistoryDepth - 1; + for (int i = 0; i < 64; ++i) { + uint8_t b = *history-- ; + graphics.drawAlignedByte(64 + i, y + 8, b); + graphics.drawAlignedByte(64 - i -1, y + 8, b); + b = util::reverse_byte(b); + graphics.drawAlignedByte(64 + i, y, b); + graphics.drawAlignedByte(64 - i -1, y, b); + } + y += 16; + } +} + +void FASTRUN BYTEBEATGEN_isr() { + bytebeatgen.ISR(); +} + +#endif // ENABLE_APP_BYTEBEATGEN diff --git a/software/o_c_REV/APP_CALIBR8OR.ino b/software/o_c_REV/APP_CALIBR8OR.ino new file mode 100644 index 000000000..bb1dc3abc --- /dev/null +++ b/software/o_c_REV/APP_CALIBR8OR.ino @@ -0,0 +1,718 @@ +// Copyright (c) 2023, Nicholas J. Michalek +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +/* + * Based on a design spec from Chris Meyer / Alias Zone / Learning Modular + */ + +#ifdef ENABLE_APP_CALIBR8OR + +#include "HSApplication.h" +#include "HSMIDI.h" +#include "HSClockManager.h" +#include "util/util_settings.h" +#include "braids_quantizer.h" +#include "braids_quantizer_scales.h" +#include "OC_scales.h" +#include "OC_autotuner.h" +#include "SegmentDisplay.h" +#include "src/drivers/FreqMeasure/OC_FreqMeasure.h" +#include "HemisphereApplet.h" + +#define CAL8_MAX_TRANSPOSE 60 +const int CAL8OR_PRECISION = 10000; + +// channel configs +struct Cal8ChannelConfig { + int scale; + int root_note; + int last_note; // for S&H mode + + uint8_t clocked_mode; + int8_t offset; // fine-tuning offset + int16_t scale_factor; // precision of 0.01% as an offset from 100% + int8_t transpose; // in semitones + int8_t transpose_active; // held value while waiting for trigger + + DAC_CHANNEL chan_; + DAC_CHANNEL get_channel() { return chan_; } + void ExitAutotune() { + FreqMeasure.end(); + OC::DigitalInputs::reInit(); + } +}; + +// Preset storage spec +enum Cal8Settings { + CAL8_DATA_VALID, // 1 bit + + CAL8_SCALE_A, // 12 bits + CAL8_SCALEFACTOR_A, // 10 bits + CAL8_OFFSET_A, // 8 bits + CAL8_TRANSPOSE_A, // 8 bits + CAL8_ROOTKEY_AND_CLOCKMODE_A, // 4 + 2 bits + + CAL8_SCALE_B, + CAL8_SCALEFACTOR_B, + CAL8_OFFSET_B, + CAL8_TRANSPOSE_B, + CAL8_ROOTKEY_AND_CLOCKMODE_B, + + CAL8_SCALE_C, + CAL8_SCALEFACTOR_C, + CAL8_OFFSET_C, + CAL8_TRANSPOSE_C, + CAL8_ROOTKEY_AND_CLOCKMODE_C, + + CAL8_SCALE_D, + CAL8_SCALEFACTOR_D, + CAL8_OFFSET_D, + CAL8_TRANSPOSE_D, + CAL8_ROOTKEY_AND_CLOCKMODE_D, + + CAL8_SETTING_LAST +}; +enum Cal8Presets { + CAL8_PRESET_A, + CAL8_PRESET_B, + CAL8_PRESET_C, + CAL8_PRESET_D, + + NR_OF_PRESETS +}; + +enum Cal8Channel { + CAL8_CHANNEL_A, + CAL8_CHANNEL_B, + CAL8_CHANNEL_C, + CAL8_CHANNEL_D, + + NR_OF_CHANNELS +}; + +enum Cal8ClockMode { + CONTINUOUS, + TRIG_TRANS, + SAMPLE_AND_HOLD, + + NR_OF_CLOCKMODES +}; + +const char * cal8_preset_id[4] = {"A", "B", "C", "D"}; + +class Calibr8orPreset : public settings::SettingsBase { +public: + bool is_valid() { + return values_[CAL8_DATA_VALID]; + } + bool load_preset(Cal8ChannelConfig *channel) { + if (!is_valid()) return false; // don't try to load a blank + + int ix = 1; // skip validity flag + + for (int ch = 0; ch < NR_OF_CHANNELS; ++ch) { + channel[ch].scale = values_[ix++]; + channel[ch].scale = constrain(channel[ch].scale, 0, OC::Scales::NUM_SCALES - 1); + + channel[ch].scale_factor = values_[ix++] - 500; + channel[ch].offset = values_[ix++] - 63; + channel[ch].transpose = values_[ix++] - CAL8_MAX_TRANSPOSE; + + uint32_t root_and_mode = uint32_t(values_[ix++]); + channel[ch].clocked_mode = ((root_and_mode >> 4) & 0x03) % NR_OF_CLOCKMODES; + channel[ch].root_note = constrain(int(root_and_mode & 0x0f), 0, 11); + } + + return true; + } + void save_preset(Cal8ChannelConfig *channel) { + int ix = 0; + + values_[ix++] = 1; // validity flag + + for (int ch = 0; ch < NR_OF_CHANNELS; ++ch) { + values_[ix++] = channel[ch].scale; + values_[ix++] = channel[ch].scale_factor + 500; + values_[ix++] = channel[ch].offset + 63; + values_[ix++] = channel[ch].transpose + CAL8_MAX_TRANSPOSE; + values_[ix++] = ((channel[ch].clocked_mode & 0x03) << 4) | (channel[ch].root_note & 0x0f); + } + } + +}; + +Calibr8orPreset cal8_presets[NR_OF_PRESETS]; + +class Calibr8or : public HSApplication { +public: + Calibr8or() { + for (int i = DAC_CHANNEL_A; i < DAC_CHANNEL_LAST; ++i) { + channel[i].chan_ = DAC_CHANNEL(i); + } + } + + OC::Autotuner autotuner; + + + void Start() { + segment.Init(SegmentSize::BIG_SEGMENTS); + + // make sure to turn this off, just in case + FreqMeasure.end(); + OC::DigitalInputs::reInit(); + + ClearPreset(); + + autotuner.Init(); + } + + void ClearPreset() { + for (int ch = 0; ch < NR_OF_CHANNELS; ++ch) { + HS::quantizer[ch].Init(); + channel[ch].scale = OC::Scales::SCALE_SEMI; + HS::quantizer[ch].Configure(OC::Scales::GetScale(channel[ch].scale), 0xffff); + + channel[ch].scale_factor = 0; + channel[ch].offset = 0; + channel[ch].root_note = 0; + channel[ch].transpose = 0; + channel[ch].clocked_mode = 0; + channel[ch].last_note = 0; + } + } + void LoadPreset() { + bool success = cal8_presets[index].load_preset(channel); + if (success) { + Resume(); + preset_modified = 0; + } + else + ClearPreset(); + } + void SavePreset() { + cal8_presets[index].save_preset(channel); + preset_modified = 0; + + // initiate actual EEPROM save + OC::CORE::app_isr_enabled = false; + OC::draw_save_message(60); + delay(1); + OC::save_app_data(); + delay(1); + // TODO: display message during save? + OC::CORE::app_isr_enabled = true; + } + + void Resume() { + // restore quantizer settings + for (int ch = 0; ch < NR_OF_CHANNELS; ++ch) { + HS::quantizer[ch].Configure(OC::Scales::GetScale(channel[ch].scale), 0xffff); + HS::quantizer[ch].Requantize(); + } + } + + void ProcessMIDI() { + HS::IOFrame &f = HS::frame; + while (usbMIDI.read()) { + const int message = usbMIDI.getType(); + const int data1 = usbMIDI.getData1(); + const int data2 = usbMIDI.getData2(); + + if (message == usbMIDI.SystemExclusive) { + // TODO: consider implementing SysEx import/export for Calibr8or + continue; + } + + f.MIDIState.ProcessMIDIMsg(usbMIDI.getChannel(), message, data1, data2); + } + } + + void Controller() { + ProcessMIDI(); + + // ClockSetup applet handles internal clock duties + HS::clock_setup_applet.Controller(0, 0); + + // -- core processing -- + for (int ch = 0; ch < NR_OF_CHANNELS; ++ch) { + bool clocked = Clock(ch); + Cal8ChannelConfig &c = channel[ch]; + + // clocked transpose + if (CONTINUOUS == c.clocked_mode || clocked) { + c.transpose_active = c.transpose; + } + + // respect S&H mode + if (c.clocked_mode != SAMPLE_AND_HOLD || clocked) { + // CV value + int pitch = In(ch); + int quantized = HS::quantizer[ch].Process(pitch, c.root_note * 128, c.transpose_active); + c.last_note = quantized; + } + + int output_cv = c.last_note; + if ( OC::DAC::calibration_data_used( DAC_CHANNEL(sel_chan) ) != 0x01 ) // not autotuned + output_cv *= (CAL8OR_PRECISION + c.scale_factor) / CAL8OR_PRECISION; + output_cv += c.offset; + + Out(ch, output_cv); + + // for UI flashers + if (clocked) trigger_flash[ch] = HEMISPHERE_PULSE_ANIMATION_TIME; + else if (trigger_flash[ch]) --trigger_flash[ch]; + } + } + + void View() { + if (clock_setup) { + HS::clock_setup_applet.View(0); + return; + } + + if (autotuner.active()) { + autotuner.Draw(); + return; + } + + gfxHeader("Calibr8or"); + + // Metronome icon + if (clock_m->IsRunning()) { + gfxIcon(56, 1, clock_m->Cycle() ? METRO_L_ICON : METRO_R_ICON); + } else if (clock_m->IsPaused()) { + gfxIcon(56, 1, PAUSE_ICON); + } + + if (preset_select) { + gfxPrint(70, 1, "- Presets"); + DrawPresetSelector(); + } else { + gfxPos(110, 1); + if (preset_modified) gfxPrint("*"); + if (cal8_presets[index].is_valid()) gfxPrint(cal8_preset_id[index]); + + DrawInterface(); + } + } + + ///////////////////////////////////////////////////////////////// + // Control handlers + ///////////////////////////////////////////////////////////////// + void OnLeftButtonPress() { + // handled on button down + if (clock_setup) return; + + // Toggle between Transpose mode and Tracking Compensation + // also doubles as Load or Save for preset select + edit_mode = !edit_mode; + + // prevent saving to the (clear) slot + if (edit_mode && preset_select == 5) preset_select = 4; + } + + void OnLeftButtonLongPress() { + if (preset_select) return; + + if (edit_mode) { + FreqMeasure.begin(); + autotuner.Open(&channel[sel_chan]); + return; + } + + // Toggle triggered transpose mode + ++channel[sel_chan].clocked_mode %= NR_OF_CLOCKMODES; + preset_modified = 1; + } + + void OnRightButtonPress() { + // handled on button down + if (clock_setup) return; + + if (preset_select) { + // special case to clear values + if (!edit_mode && preset_select == NR_OF_PRESETS + 1) { + ClearPreset(); + preset_modified = 1; + } + else { + index = preset_select - 1; + if (edit_mode) SavePreset(); + else LoadPreset(); + } + + preset_select = 0; + return; + } + + // Scale selection + scale_edit = !scale_edit; + } + + void OnButtonDown(const UI::Event &event) { + // check for clock setup secret combo (dual press) + if ( event.control == OC::CONTROL_BUTTON_DOWN || event.control == OC::CONTROL_BUTTON_UP) + UpOrDownButtonPress(event.control == OC::CONTROL_BUTTON_UP); + else if (clock_setup) // pass button down to Clock Setup + HS::clock_setup_applet.OnButtonPress(0); + } + + void UpOrDownButtonPress(bool up) { + if (OC::CORE::ticks - click_tick < HEMISPHERE_DOUBLE_CLICK_TIME && up != first_click) { + // show clock setup if both buttons pressed quickly + clock_setup = 1; + click_tick = 0; + } else { + click_tick = OC::CORE::ticks; + first_click = up; + } + } + + // fires on button release + void SwitchChannel(bool up) { + if (!clock_setup && !preset_select) { + sel_chan += (up? -1 : 1) + NR_OF_CHANNELS; + sel_chan %= NR_OF_CHANNELS; + } + + if (click_tick) { + // always cancel clock setup and preset select on single click + clock_setup = 0; + preset_select = 0; + } + } + + void OnDownButtonLongPress() { + // show preset screen, select last loaded + preset_select = 1 + index; + } + + // Left encoder: Octave or VScaling + Scale Select + void OnLeftEncoderMove(int direction) { + if (clock_setup) { + HS::clock_setup_applet.OnEncoderMove(0, direction); + return; + } + if (preset_select) { + edit_mode = (direction>0); + // prevent saving to the (clear) slot + if (edit_mode && preset_select == 5) preset_select = 4; + return; + } + + preset_modified = 1; + if (scale_edit) { + // Scale Select + int s_ = channel[sel_chan].scale + direction; + if (s_ >= OC::Scales::NUM_SCALES) s_ = 0; + if (s_ < 0) s_ = OC::Scales::NUM_SCALES - 1; + + channel[sel_chan].scale = s_; + HS::quantizer[sel_chan].Configure(OC::Scales::GetScale(s_), 0xffff); + HS::quantizer[sel_chan].Requantize(); + return; + } + + if (!edit_mode) { // Octave jump + int s = OC::Scales::GetScale(channel[sel_chan].scale).num_notes; + channel[sel_chan].transpose += (direction * s); + while (channel[sel_chan].transpose > CAL8_MAX_TRANSPOSE) channel[sel_chan].transpose -= s; + while (channel[sel_chan].transpose < -CAL8_MAX_TRANSPOSE) channel[sel_chan].transpose += s; + } + else if ( OC::DAC::calibration_data_used( DAC_CHANNEL(sel_chan) ) != 0x01 ) // not autotuned + { + // Tracking compensation + channel[sel_chan].scale_factor = constrain(channel[sel_chan].scale_factor + direction, -500, 500); + } + } + + // Right encoder: Semitones or Bias Offset + Root Note + void OnRightEncoderMove(int direction) { + if (clock_setup) { + HS::clock_setup_applet.OnEncoderMove(0, direction); + return; + } + if (preset_select) { + preset_select = constrain(preset_select + direction, 1, NR_OF_PRESETS + (1-edit_mode)); + return; + } + + preset_modified = 1; + if (scale_edit) { + // Root Note + channel[sel_chan].root_note = constrain(channel[sel_chan].root_note + direction, 0, 11); + HS::quantizer[sel_chan].Requantize(); + return; + } + + if (!edit_mode) { + channel[sel_chan].transpose = constrain(channel[sel_chan].transpose + direction, -CAL8_MAX_TRANSPOSE, CAL8_MAX_TRANSPOSE); + } + else { + channel[sel_chan].offset = constrain(channel[sel_chan].offset + direction, -63, 64); + } + } + + int index = 0; + + int sel_chan = 0; + bool edit_mode = 0; + bool scale_edit = 0; + int preset_select = 0; // both a flag and an index + bool preset_modified = 0; + + uint32_t click_tick = 0; + bool first_click = 0; + bool clock_setup = 0; + + int trigger_flash[NR_OF_CHANNELS]; + + SegmentDisplay segment; + Cal8ChannelConfig channel[NR_OF_CHANNELS]; + + ClockManager *clock_m = clock_m->get(); + + void DrawPresetSelector() { + // index is the currently loaded preset (0-3) + // preset_select is current selection (1-4, 5=clear) + int y = 5 + 10*preset_select; + gfxPrint(25, y, edit_mode ? "Save" : "Load"); + gfxIcon(50, y, RIGHT_ICON); + + for (int i = 0; i < NR_OF_PRESETS; ++i) { + gfxPrint(60, 15 + i*10, cal8_preset_id[i]); + if (!cal8_presets[i].is_valid()) + gfxPrint(" (empty)"); + else if (i == index) + gfxPrint(" *"); + } + if (!edit_mode) + gfxPrint(60, 55, "[CLEAR]"); + } + + void DrawInterface() { + // Draw channel tabs + for (int i = 0; i < NR_OF_CHANNELS; ++i) { + gfxLine(i*32, 13, i*32, 22); // vertical line on left + if (channel[i].clocked_mode) gfxIcon(2 + i*32, 14, CLOCK_ICON); + if (channel[i].clocked_mode == SAMPLE_AND_HOLD) gfxIcon(22 + i*32, 14, STAIRS_ICON); + gfxPrint(i*32 + 13, 14, i+1); + + if (i == sel_chan) + gfxInvert(1 + i*32, 13, 31, 11); + } + gfxLine(127, 13, 127, 22); // vertical line + gfxLine(0, 23, 127, 23); + + // Draw parameters for selected channel + int y = 32; + + // Transpose + gfxIcon(9, y, BEND_ICON); + + // -- LCD Display Section -- + gfxFrame(20, y-3, 64, 18); + gfxIcon(23, y+2, (channel[sel_chan].transpose >= 0)? PLUS_ICON : MINUS_ICON); + + int s = OC::Scales::GetScale(channel[sel_chan].scale).num_notes; + int octave = channel[sel_chan].transpose / s; + int semitone = channel[sel_chan].transpose % s; + segment.PrintWhole(33, y, abs(octave), 10); + gfxPrint(53, y+5, "."); + segment.PrintWhole(61, y, abs(semitone), 10); + + // Scale + gfxIcon(89, y, SCALE_ICON); + gfxPrint(99, y, OC::scale_names_short[channel[sel_chan].scale]); + if (scale_edit) { + gfxInvert(98, y-1, 29, 9); + gfxIcon(100, y+10, RIGHT_ICON); + } + // Root Note + gfxPrint(110, y+10, OC::Strings::note_names_unpadded[channel[sel_chan].root_note]); + + // Tracking Compensation + y += 22; + gfxIcon(9, y, ZAP_ICON); + + if ( OC::DAC::calibration_data_used( DAC_CHANNEL(sel_chan) ) == 0x01 ) { + gfxPrint(20, y, "(auto) "); + } else { + int whole = (channel[sel_chan].scale_factor + CAL8OR_PRECISION) / 100; + int decimal = (channel[sel_chan].scale_factor + CAL8OR_PRECISION) % 100; + gfxPrint(20 + pad(100, whole), y, whole); + gfxPrint("."); + if (decimal < 10) gfxPrint("0"); + gfxPrint(decimal); + gfxPrint("% "); + } + if (channel[sel_chan].offset >= 0) gfxPrint("+"); + gfxPrint(channel[sel_chan].offset); + + // mode indicator + if (!scale_edit) + gfxIcon(0, 32 + edit_mode*22, RIGHT_ICON); + } +}; + +// TOTAL EEPROM SIZE: 4 * 29 bytes +SETTINGS_DECLARE(Calibr8orPreset, CAL8_SETTING_LAST) { + {0, 0, 1, "validity flag", NULL, settings::STORAGE_TYPE_U4}, + + {0, 0, 65535, "Scale A", NULL, settings::STORAGE_TYPE_U16}, + {0, 0, 65535, "CV Scaling Factor A", NULL, settings::STORAGE_TYPE_U16}, + {0, 0, 255, "Offset Bias A", NULL, settings::STORAGE_TYPE_U8}, + {0, 0, 255, "Transpose A", NULL, settings::STORAGE_TYPE_U8}, + {0, 0, 255, "Root Key + Mode A", NULL, settings::STORAGE_TYPE_U8}, + + {0, 0, 65535, "Scale B", NULL, settings::STORAGE_TYPE_U16}, + {0, 0, 65535, "CV Scaling Factor B", NULL, settings::STORAGE_TYPE_U16}, + {0, 0, 255, "Offset Bias B", NULL, settings::STORAGE_TYPE_U8}, + {0, 0, 255, "Transpose B", NULL, settings::STORAGE_TYPE_U8}, + {0, 0, 255, "Root Key + Mode B", NULL, settings::STORAGE_TYPE_U8}, + + {0, 0, 65535, "Scale C", NULL, settings::STORAGE_TYPE_U16}, + {0, 0, 65535, "CV Scaling Factor C", NULL, settings::STORAGE_TYPE_U16}, + {0, 0, 255, "Offset Bias C", NULL, settings::STORAGE_TYPE_U8}, + {0, 0, 255, "Transpose C", NULL, settings::STORAGE_TYPE_U8}, + {0, 0, 255, "Root Key + Mode C", NULL, settings::STORAGE_TYPE_U8}, + + {0, 0, 65535, "Scale D", NULL, settings::STORAGE_TYPE_U16}, + {0, 0, 65535, "CV Scaling Factor D", NULL, settings::STORAGE_TYPE_U16}, + {0, 0, 255, "Offset Bias D", NULL, settings::STORAGE_TYPE_U8}, + {0, 0, 255, "Transpose D", NULL, settings::STORAGE_TYPE_U8}, + {0, 0, 255, "Root Key + Mode D", NULL, settings::STORAGE_TYPE_U8} +}; + + +Calibr8or Calibr8or_instance; + +// App stubs +void Calibr8or_init() { Calibr8or_instance.BaseStart(); } + +size_t Calibr8or_storageSize() { + return Calibr8orPreset::storageSize() * NR_OF_PRESETS; +} + +size_t Calibr8or_save(void *storage) { + size_t used = 0; + for (int i = 0; i < 4; ++i) { + used += cal8_presets[i].Save(static_cast(storage) + used); + } + return used; +} + +size_t Calibr8or_restore(const void *storage) { + size_t used = 0; + for (int i = 0; i < 4; ++i) { + used += cal8_presets[i].Restore(static_cast(storage) + used); + } + Calibr8or_instance.LoadPreset(); + return used; +} + +void Calibr8or_isr() { + if (Calibr8or_instance.autotuner.active()) { + Calibr8or_instance.autotuner.ISR(); + return; + } + Calibr8or_instance.BaseController(); +} + +void Calibr8or_handleAppEvent(OC::AppEvent event) { + switch (event) { + case OC::APP_EVENT_RESUME: + Calibr8or_instance.Resume(); + break; + + // The idea is to auto-save when the screen times out... + case OC::APP_EVENT_SUSPEND: + case OC::APP_EVENT_SCREENSAVER_ON: + break; + + default: break; + } +} + +void Calibr8or_loop() {} // Deprecated + +void Calibr8or_menu() { Calibr8or_instance.BaseView(); } + +void Calibr8or_screensaver() { + Calibr8or_instance.BaseScreensaver(true); +} + +void Calibr8or_handleButtonEvent(const UI::Event &event) { + if (Calibr8or_instance.autotuner.active()) { + Calibr8or_instance.autotuner.HandleButtonEvent(event); + return; + } + + // For left encoder, handle press and long press + // For right encoder, only handle press (long press is reserved) + // For up button, handle only press (long press is reserved) + // For down button, handle press and long press + switch (event.type) { + case UI::EVENT_BUTTON_DOWN: + Calibr8or_instance.OnButtonDown(event); + + break; + case UI::EVENT_BUTTON_PRESS: { + switch (event.control) { + case OC::CONTROL_BUTTON_L: + Calibr8or_instance.OnLeftButtonPress(); + break; + case OC::CONTROL_BUTTON_R: + Calibr8or_instance.OnRightButtonPress(); + break; + case OC::CONTROL_BUTTON_DOWN: + case OC::CONTROL_BUTTON_UP: + Calibr8or_instance.SwitchChannel(event.control == OC::CONTROL_BUTTON_UP); + break; + default: break; + } + } break; + case UI::EVENT_BUTTON_LONG_PRESS: + if (event.control == OC::CONTROL_BUTTON_L) { + Calibr8or_instance.OnLeftButtonLongPress(); + } + if (event.control == OC::CONTROL_BUTTON_DOWN) { + Calibr8or_instance.OnDownButtonLongPress(); + } + break; + + default: break; + } +} + +void Calibr8or_handleEncoderEvent(const UI::Event &event) { + if (Calibr8or_instance.autotuner.active()) { + Calibr8or_instance.autotuner.HandleEncoderEvent(event); + return; + } + + // Left encoder turned + if (event.control == OC::CONTROL_ENCODER_L) Calibr8or_instance.OnLeftEncoderMove(event.value); + + // Right encoder turned + if (event.control == OC::CONTROL_ENCODER_R) Calibr8or_instance.OnRightEncoderMove(event.value); +} + +#endif // ENABLE_APP_CALIBR8OR diff --git a/software/o_c_REV/APP_CHORDS.ino b/software/o_c_REV/APP_CHORDS.ino new file mode 100644 index 000000000..7238a261d --- /dev/null +++ b/software/o_c_REV/APP_CHORDS.ino @@ -0,0 +1,1401 @@ +// Copyright (c) 2015, 2016, 2017 Patrick Dowling, Tim Churches, Max Stadler +// +// Initial app implementation: Patrick Dowling (pld@gurkenkiste.com) +// Modifications by: Tim Churches (tim.churches@gmail.com) +// Yet more Modifications by: mxmxmx +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// Quad quantizer app, based around the the quantizer/scales implementation from +// from Braids by Olivier Gillet (see braids_quantizer.h/cc et al.). It has since +// grown a little bit... + +#ifdef ENABLE_APP_CHORDS + +#include "OC_apps.h" +#include "util/util_settings.h" +#include "util/util_trigger_delay.h" +#include "braids_quantizer.h" +#include "braids_quantizer_scales.h" +#include "OC_menus.h" +#include "OC_scales.h" +#include "OC_scale_edit.h" +#include "OC_strings.h" +#include "OC_chords.h" +#include "OC_chords_edit.h" +#include "OC_input_map.h" +#include "OC_input_maps.h" + +enum CHORDS_SETTINGS { + CHORDS_SETTING_SCALE, + CHORDS_SETTING_ROOT, + CHORDS_SETTING_PROGRESSION, + CHORDS_SETTING_MASK, + CHORDS_SETTING_CV_SOURCE, + CHORDS_SETTING_CHORDS_ADVANCE_TRIGGER_SOURCE, + CHORDS_SETTING_PLAYMODES, + CHORDS_SETTING_DIRECTION, + CHORDS_SETTING_BROWNIAN_PROBABILITY, + CHORDS_SETTING_TRIGGER_DELAY, + CHORDS_SETTING_TRANSPOSE, + CHORDS_SETTING_OCTAVE, + CHORDS_SETTING_CHORD_SLOT, + CHORDS_SETTING_NUM_CHORDS_0, + CHORDS_SETTING_NUM_CHORDS_1, + CHORDS_SETTING_NUM_CHORDS_2, + CHORDS_SETTING_NUM_CHORDS_3, + CHORDS_SETTING_CHORD_EDIT, + // CV sources + CHORDS_SETTING_ROOT_CV, + CHORDS_SETTING_MASK_CV, + CHORDS_SETTING_TRANSPOSE_CV, + CHORDS_SETTING_OCTAVE_CV, + CHORDS_SETTING_QUALITY_CV, + CHORDS_SETTING_VOICING_CV, + CHORDS_SETTING_INVERSION_CV, + CHORDS_SETTING_PROGRESSION_CV, + CHORDS_SETTING_DIRECTION_CV, + CHORDS_SETTING_BROWNIAN_CV, + CHORDS_SETTING_NUM_CHORDS_CV, + CHORDS_SETTING_DUMMY, + CHORDS_SETTING_MORE_DUMMY, + CHORDS_SETTING_LAST +}; + +enum CHORDS_CV_SOURCES { + CHORDS_CV_SOURCE_CV1, + CHORDS_CV_SOURCE_CV2, + CHORDS_CV_SOURCE_CV3, + CHORDS_CV_SOURCE_CV4, + CHORDS_CV_SOURCE_LAST +}; + +enum CHORDS_ADVANCE_TRIGGER_SOURCE { + CHORDS_ADVANCE_TRIGGER_SOURCE_TR1, + CHORDS_ADVANCE_TRIGGER_SOURCE_TR2, + CHORDS_ADVANCE_TRIGGER_SOURCE_LAST +}; + +enum CHORDS_CV_DESTINATIONS { + CHORDS_CV_DEST_NONE, + CHORDS_CV_DEST_ROOT, + CHORDS_CV_DEST_OCTAVE, + CHORDS_CV_DEST_TRANSPOSE, + CHORDS_CV_DEST_MASK, + CHORDS_CV_DEST_LAST +}; + +// enum MENU_PAGES { +// PARAMETERS, +// CV_MAPPING +// }; + +enum CHORDS_MENU_PAGES { + MENU_PARAMETERS, + MENU_CV_MAPPING, + MENU_PAGES_LAST +}; + +enum CHORDS_PLAYMODES { + _NONE, + _SEQ1, + _SEQ2, + _SEQ3, + _TR1, + _TR2, + _TR3, + _SH1, + _SH2, + _SH3, + _SH4, + _CV1, + _CV2, + _CV3, + _CV4, + CHORDS_PLAYMODES_LAST +}; + +enum CHORDS_DIRECTIONS { + CHORDS_FORWARD, + CHORDS_REVERSE, + CHORDS_PENDULUM1, + CHORDS_PENDULUM2, + CHORDS_RANDOM, + CHORDS_BROWNIAN, + CHORDS_DIRECTIONS_LAST +}; + +namespace menu = OC::menu; + +extern uint_fast8_t MENU_REDRAW; + +class Chords : public settings::SettingsBase { +public: + + int get_scale(uint8_t selected_scale_slot_) const { + return values_[CHORDS_SETTING_SCALE]; + } + + void set_scale(int scale) { + + if (scale != get_scale(DUMMY)) { + const OC::Scale &scale_def = OC::Scales::GetScale(scale); + uint16_t mask = get_mask(); + if (0 == (mask & ~(0xffff << scale_def.num_notes))) + mask |= 0x1; + apply_value(CHORDS_SETTING_MASK, mask); + apply_value(CHORDS_SETTING_SCALE, scale); + } + } + + // dummy + int get_scale_select() const { + return 0; + } + + // dummy + void set_scale_at_slot(int scale, uint16_t mask, int root, int transpose, uint8_t scale_slot) { + + } + + // dummy + int get_transpose(uint8_t DUMMY) const { + return 0; + } + + bool octave_toggle() { + _octave_toggle = (~_octave_toggle) & 1u; + return _octave_toggle; + } + + bool poke_octave_toggle() const { + return _octave_toggle; + } + + int get_progression() const { + return values_[CHORDS_SETTING_PROGRESSION]; + } + + int get_progression_cv() const { + return values_[CHORDS_SETTING_PROGRESSION_CV]; + } + + int get_active_progression() const { + return active_progression_; + } + + int get_chord_slot() const { + return values_[CHORDS_SETTING_CHORD_SLOT]; + } + + void set_chord_slot(int8_t slot) { + apply_value(CHORDS_SETTING_CHORD_SLOT, slot); + } + + int get_num_chords(uint8_t progression) const { + + int len = 0x0; + switch (progression) { + + case 0: + len = values_[CHORDS_SETTING_NUM_CHORDS_0]; + break; + case 1: + len = values_[CHORDS_SETTING_NUM_CHORDS_1]; + break; + case 2: + len = values_[CHORDS_SETTING_NUM_CHORDS_2]; + break; + case 3: + len = values_[CHORDS_SETTING_NUM_CHORDS_3]; + break; + default: + break; + } + return len; + } + + void set_num_chords(int8_t num_chords, uint8_t progression) { + + // set progression length: + switch (progression) { + case 0: + apply_value(CHORDS_SETTING_NUM_CHORDS_0, num_chords); + break; + case 1: + apply_value(CHORDS_SETTING_NUM_CHORDS_1, num_chords); + break; + case 2: + apply_value(CHORDS_SETTING_NUM_CHORDS_2, num_chords); + break; + case 3: + apply_value(CHORDS_SETTING_NUM_CHORDS_3, num_chords); + break; + default: + break; + } + } + + int get_num_chords_cv() const { + return values_[CHORDS_SETTING_NUM_CHORDS_CV]; + } + + int active_chord() const { + return active_chord_; + } + + int get_playmode() const { + return values_[CHORDS_SETTING_PLAYMODES]; + } + + int get_direction() const { + return values_[CHORDS_SETTING_DIRECTION]; + } + + uint8_t get_direction_cv() const { + return values_[CHORDS_SETTING_DIRECTION_CV]; + } + + uint8_t get_brownian_probability() const { + return values_[CHORDS_SETTING_BROWNIAN_PROBABILITY]; + } + + int8_t get_brownian_probability_cv() const { + return values_[CHORDS_SETTING_BROWNIAN_CV]; + } + + int get_root() const { + return values_[CHORDS_SETTING_ROOT]; + } + + int get_root(uint8_t DUMMY) const { + return 0x0; + } + + uint8_t get_root_cv() const { + return values_[CHORDS_SETTING_ROOT_CV]; + } + + int get_display_num_chords() const { + return display_num_chords_; + } + + uint16_t get_mask() const { + return values_[CHORDS_SETTING_MASK]; + } + + uint16_t get_rotated_mask() const { + return last_mask_; + } + + uint8_t get_mask_cv() const { + return values_[CHORDS_SETTING_MASK_CV]; + } + + int16_t get_cv_source() const { + return values_[CHORDS_SETTING_CV_SOURCE]; + } + + int8_t get_chords_trigger_source() const { + return values_[CHORDS_SETTING_CHORDS_ADVANCE_TRIGGER_SOURCE]; + } + + uint16_t get_trigger_delay() const { + return values_[CHORDS_SETTING_TRIGGER_DELAY]; + } + + int get_transpose() const { + return values_[CHORDS_SETTING_TRANSPOSE]; + } + + uint8_t get_transpose_cv() const { + return values_[CHORDS_SETTING_TRANSPOSE_CV]; + } + + int get_octave() const { + return values_[CHORDS_SETTING_OCTAVE]; + } + + uint8_t get_octave_cv() const { + return values_[CHORDS_SETTING_OCTAVE_CV]; + } + + uint8_t get_quality_cv() const { + return values_[CHORDS_SETTING_QUALITY_CV]; + } + + uint8_t get_inversion_cv() const { + return values_[CHORDS_SETTING_INVERSION_CV]; + } + + uint8_t get_voicing_cv() const { + return values_[CHORDS_SETTING_VOICING_CV]; + } + + uint8_t clockState() const { + return clock_display_.getState(); + } + + uint8_t get_menu_page() const { + return menu_page_; + } + + void set_menu_page(uint8_t _menu_page) { + menu_page_ = _menu_page; + } + + void update_inputmap(int num_slots, uint8_t range) { + input_map_.Configure(OC::InputMaps::GetInputMap(num_slots), range); + } + + void clear_CV_mapping() { + // clear all ... + apply_value(CHORDS_SETTING_ROOT_CV, 0); + apply_value(CHORDS_SETTING_MASK_CV, 0); + apply_value(CHORDS_SETTING_TRANSPOSE_CV, 0); + apply_value(CHORDS_SETTING_OCTAVE_CV, 0); + apply_value(CHORDS_SETTING_QUALITY_CV, 0); + apply_value(CHORDS_SETTING_VOICING_CV, 0); + apply_value(CHORDS_SETTING_INVERSION_CV, 0); + apply_value(CHORDS_SETTING_BROWNIAN_CV, 0); + apply_value(CHORDS_SETTING_DIRECTION_CV, 0); + apply_value(CHORDS_SETTING_PROGRESSION_CV, 0); + apply_value(CHORDS_SETTING_NUM_CHORDS_CV, 0); + } + + void Init() { + + InitDefaults(); + menu_page_ = MENU_PARAMETERS; + apply_value(CHORDS_SETTING_CV_SOURCE, 0x0); + set_scale(OC::Scales::SCALE_SEMI); + force_update_ = true; + _octave_toggle = false; + last_scale_= -1; + last_mask_ = 0; + last_sample_ = 0; + chord_advance_last_ = true; + progression_advance_last_ = true; + active_chord_ = 0; + chord_repeat_ = false; + progression_cnt_ = 0; + active_progression_ = 0; + playmode_last_ = 0; + progression_last_ = 0; + progression_EoP_ = 0; + num_chords_last_ = 0; + chords_direction_ = true; + display_num_chords_ = 0x1; + + trigger_delay_.Init(); + input_map_.Init(); + quantizer_.Init(); + chords_.Init(); + update_scale(true, false); + clock_display_.Init(); + update_enabled_settings(); + } + + void force_update() { + //force_update_ = true; + } + + int8_t _clock(uint8_t sequence_length, uint8_t sequence_count, uint8_t sequence_max, bool _reset) { + + int8_t EoP = 0x0, _clk_cnt, _direction; + bool reset = !digitalReadFast(TR4) | _reset; + + _clk_cnt = active_chord_; + _direction = get_direction(); + + if (get_direction_cv()) { + _direction += (OC::ADC::value(static_cast(get_direction_cv() - 1)) + 255) >> 9; + CONSTRAIN(_direction, 0, CHORDS_DIRECTIONS_LAST - 0x1); + } + + switch (_direction) { + + case CHORDS_FORWARD: + { + _clk_cnt++; + if (reset) + _clk_cnt = 0x0; + // end of sequence? + else if (_clk_cnt > sequence_length) + _clk_cnt = 0x0; + else if (_clk_cnt == sequence_length) + EoP = 0x1; + } + break; + case CHORDS_REVERSE: + { + _clk_cnt--; + if (reset) + _clk_cnt = sequence_length; + // end of sequence? + else if (_clk_cnt < 0) + _clk_cnt = sequence_length; + else if (!_clk_cnt) + EoP = 0x1; + } + break; + case CHORDS_PENDULUM1: + case CHORDS_BROWNIAN: + if (CHORDS_BROWNIAN == get_direction()) { + // Compare Brownian probability and reverse direction if needed + int16_t brown_prb = get_brownian_probability(); + + if (get_brownian_probability_cv()) { + brown_prb += (OC::ADC::value(static_cast(get_brownian_probability_cv() - 1)) + 8) >> 3; + CONSTRAIN(brown_prb, 0, 256); + } + if (random(0,256) < brown_prb) + chords_direction_ = !chords_direction_; + } + { + if (chords_direction_) { + _clk_cnt++; + if (reset) + _clk_cnt = 0x0; + else if (_clk_cnt >= sequence_length) { + + if (sequence_count >= sequence_max) { + chords_direction_ = false; + _clk_cnt = sequence_length; + } + else EoP = 0x1; + } + } + // reverse direction: + else { + _clk_cnt--; + if (reset) + _clk_cnt = sequence_length; + else if (_clk_cnt <= 0) { + // end of sequence ? + if (sequence_count == 0x0) { + chords_direction_ = true; + _clk_cnt = 0x0; + } + else EoP = -0x1; + } + } + } + break; + case CHORDS_PENDULUM2: + { + if (chords_direction_) { + + if (!chord_repeat_) + _clk_cnt++; + chord_repeat_ = false; + + if (reset) + _clk_cnt = 0x0; + else if (_clk_cnt >= sequence_length) { + // end of sequence ? + if (sequence_count >= sequence_max) { + chords_direction_ = false; + _clk_cnt = sequence_length; + chord_repeat_ = true; // repeat last step + } + else EoP = 0x1; + } + } + // reverse direction: + else { + + if (!chord_repeat_) + _clk_cnt--; + chord_repeat_ = false; + + if (reset) + _clk_cnt = sequence_length; + else if (_clk_cnt <= 0x0) { + // end of sequence ? + if (sequence_count == 0x0) { + chords_direction_ = true; + _clk_cnt = 0x0; + chord_repeat_ = true; // repeat first step + } + else EoP = -0x1; + } + } + } + break; + case CHORDS_RANDOM: + _clk_cnt = random(sequence_length + 0x1); + if (reset) + _clk_cnt = 0x0; + // jump to next sequence if we happen to hit the last note: + else if (_clk_cnt >= sequence_length) + EoP = random(0x2); + break; + default: + break; + } + active_chord_ = _clk_cnt; + return EoP; + } + + + inline void Update(uint32_t triggers) { + + bool triggered = triggers & DIGITAL_INPUT_MASK(0x0); + + trigger_delay_.Update(); + if (triggered) + trigger_delay_.Push(OC::trigger_delay_ticks[get_trigger_delay()]); + triggered = trigger_delay_.triggered(); + + int32_t sample_a = last_sample_; + int32_t temp_sample = 0; + + if (triggered) { + + int32_t pitch, cv_source, transpose, octave, root, mask_rotate; + int8_t num_progression, num_progression_cv, num_chords, num_chords_cv, progression_max, progression_cnt, playmode, reset; + + cv_source = get_cv_source(); + transpose = get_transpose(); + octave = get_octave(); + root = get_root(); + num_progression = get_progression(); + progression_max = 0; + progression_cnt = 0; + num_progression_cv = 0; + num_chords = 0; + num_chords_cv = 0; + reset = 0; + mask_rotate = 0; + playmode = get_playmode(); + + // update mask? + if (get_mask_cv()) { + mask_rotate = (OC::ADC::value(static_cast(get_mask_cv() - 0x1)) + 127) >> 8; + } + + update_scale(force_update_, mask_rotate); + + if (num_progression != progression_last_ || playmode != playmode_last_) { + // reset progression: + progression_cnt_ = 0x0; + active_progression_ = num_progression; + } + playmode_last_ = playmode; + progression_last_ = num_progression; + + if (get_progression_cv()) { + num_progression_cv = num_progression += (OC::ADC::value(static_cast(get_progression_cv() - 1)) + 255) >> 9; + CONSTRAIN(num_progression, 0, OC::Chords::NUM_CHORD_PROGRESSIONS - 0x1); + } + + if (get_num_chords_cv()) + num_chords_cv = (OC::ADC::value(static_cast(get_num_chords_cv() - 1)) + 255) >> 9; + + switch (playmode) { + + case _NONE: + active_progression_ = num_progression; + break; + case _SEQ1: + case _SEQ2: + case _SEQ3: + { + progression_max = playmode; + + if (progression_EoP_) { + + // increment progression # + progression_cnt_ += progression_EoP_; + // reset progression # + progression_cnt_ = progression_cnt_ > progression_max ? 0x0 : progression_cnt_; + // update progression + active_progression_ = num_progression + progression_cnt_; + // wrap around: + if (active_progression_ >= OC::Chords::NUM_CHORD_PROGRESSIONS) + active_progression_ -= OC::Chords::NUM_CHORD_PROGRESSIONS; + // reset + _clock(get_num_chords(active_progression_), 0x0, progression_max, true); + reset = true; + } + else if (num_progression_cv) { + active_progression_ += num_progression_cv; + CONSTRAIN(active_progression_, 0, OC::Chords::NUM_CHORD_PROGRESSIONS - 0x1); + } + progression_cnt = progression_cnt_; + } + break; + case _TR1: + case _TR2: + case _TR3: + { + // get trigger + uint8_t _progression_advance_trig = digitalReadFast(TR3); + progression_max = playmode - _SEQ3; + + if (_progression_advance_trig < progression_advance_last_) { + // increment progression # + progression_cnt_++; + // reset progression # + progression_cnt_ = progression_cnt_ > progression_max ? 0x0 : progression_cnt_; + // update progression + active_progression_ = num_progression + progression_cnt_; + // + reset + reset = true; + // wrap around: + if (active_progression_ >= OC::Chords::NUM_CHORD_PROGRESSIONS) + active_progression_ -= OC::Chords::NUM_CHORD_PROGRESSIONS; + } + else if (num_progression_cv) { + active_progression_ += num_progression_cv; + CONSTRAIN(active_progression_, 0, OC::Chords::NUM_CHORD_PROGRESSIONS - 0x1); + } + progression_advance_last_ = _progression_advance_trig; + progression_max = 0x0; + } + break; + case _SH1: + case _SH2: + case _SH3: + case _SH4: + { + // SH? + uint8_t _progression_advance_trig = digitalReadFast(TR3); + if (_progression_advance_trig < progression_advance_last_) { + + num_chords = get_num_chords(num_progression) + num_chords_cv; + if (num_chords_cv) + CONSTRAIN(num_chords, 0, OC::Chords::NUM_CHORDS - 0x1); + // length changed? + if (num_chords_last_ != num_chords) + update_inputmap(num_chords + 0x1, 0x0); + // store values: + num_chords_last_ = num_chords; + active_progression_ = num_progression; + // process input: + active_chord_ = input_map_.Process(OC::ADC::value(static_cast(playmode - _SH1))); + } + progression_advance_last_ = _progression_advance_trig; + } + break; + case _CV1: + case _CV2: + case _CV3: + case _CV4: + { + num_chords = get_num_chords(num_progression) + num_chords_cv; + if (num_chords_cv) + CONSTRAIN(num_chords, 0, OC::Chords::NUM_CHORDS - 0x1); + // length changed ? + if (num_chords_last_ != num_chords) + update_inputmap(num_chords + 0x1, 0x0); + // store values: + num_chords_last_ = num_chords; + active_progression_ = num_progression; + // process input: + active_chord_ = input_map_.Process(OC::ADC::value(static_cast(playmode - _CV1))); + } + break; + default: + break; + } + + num_progression = active_progression_; + + if (playmode < _SH1) { + // next chord via trigger? + uint8_t _advance_trig = get_chords_trigger_source(); + + if (_advance_trig == CHORDS_ADVANCE_TRIGGER_SOURCE_TR2) + _advance_trig = digitalReadFast(TR2); + else if (triggered) { + _advance_trig = 0x0; + chord_advance_last_ = 0x1; + } + + num_chords = get_num_chords(num_progression) + num_chords_cv; + if (num_chords_cv) + CONSTRAIN(num_chords, 0, OC::Chords::NUM_CHORDS - 0x1); + + CONSTRAIN(active_chord_, 0x0, num_chords); + + if (num_chords && (_advance_trig < chord_advance_last_)) + progression_EoP_ = _clock(num_chords, progression_cnt, progression_max, reset); + chord_advance_last_ = _advance_trig; + } + + display_num_chords_ = num_chords; + // active chord: + OC::Chord *active_chord = &OC::user_chords[active_chord_ + num_progression * OC::Chords::NUM_CHORDS]; + + int8_t _base_note = active_chord->base_note; + int8_t _octave = active_chord->octave; + int8_t _quality = active_chord->quality; + int8_t _voicing = active_chord->voicing; + int8_t _inversion = active_chord->inversion; + + octave += _octave; + CONSTRAIN(octave, -6, 6); + + if (_base_note) { + /* + * we don't use the incoming CV pitch value — limit to valid base notes. + */ + int8_t _limit = OC::Scales::GetScale(get_scale(DUMMY)).num_notes; + _base_note = _base_note > _limit ? _limit : _base_note; + pitch = 0x0; + transpose += (_base_note - 0x1); + } + else { + pitch = quantizer_.enabled() + ? OC::ADC::raw_pitch_value(static_cast(cv_source)) + : OC::ADC::pitch_value(static_cast(cv_source)); + } + + switch (cv_source) { + + case CHORDS_CV_SOURCE_CV1: + break; + // todo + default: + break; + } + + // S/H mode + if (get_root_cv()) { + root += (OC::ADC::value(static_cast(get_root_cv() - 1)) + 127) >> 8; + CONSTRAIN(root, 0, 15); + } + + if (get_octave_cv()) { + octave += (OC::ADC::value(static_cast(get_octave_cv() - 1)) + 255) >> 9; + CONSTRAIN(octave, -4, 4); + } + + if (get_transpose_cv()) { + transpose += (OC::ADC::value(static_cast(get_transpose_cv() - 1)) + 63) >> 7; + CONSTRAIN(transpose, -15, 15); + } + + if (get_quality_cv()) { + _quality += (OC::ADC::value(static_cast(get_quality_cv() - 1)) + 255) >> 9; + CONSTRAIN(_quality, 0, OC::Chords::CHORDS_QUALITY_LAST - 1); + } + + if (get_inversion_cv()) { + _inversion += (OC::ADC::value(static_cast(get_inversion_cv() - 1)) + 511) >> 10; + CONSTRAIN(_inversion, 0, OC::Chords::CHORDS_INVERSION_LAST - 1); + } + + if (get_voicing_cv()) { + _voicing += (OC::ADC::value(static_cast(get_voicing_cv() - 1)) + 255) >> 9; + CONSTRAIN(_voicing, 0, OC::Chords::CHORDS_VOICING_LAST - 1); + } + + int32_t quantized = quantizer_.Process(pitch, root << 7, transpose); + // main sample, S/H: + sample_a = temp_sample = OC::DAC::pitch_to_scaled_voltage_dac(DAC_CHANNEL_A, quantized, octave + OC::inversion[_inversion][0], OC::DAC::get_voltage_scaling(DAC_CHANNEL_A)); + + // now derive chords ... + transpose += OC::qualities[_quality][1]; + int32_t sample_b = quantizer_.Process(pitch, root << 7, transpose); + transpose += OC::qualities[_quality][2]; + int32_t sample_c = quantizer_.Process(pitch, root << 7, transpose); + transpose += OC::qualities[_quality][3]; + int32_t sample_d = quantizer_.Process(pitch, root << 7, transpose); + + //todo voicing for root note + sample_b = OC::DAC::pitch_to_scaled_voltage_dac(DAC_CHANNEL_B, sample_b, octave + OC::voicing[_voicing][1] + OC::inversion[_inversion][1], OC::DAC::get_voltage_scaling(DAC_CHANNEL_B)); + sample_c = OC::DAC::pitch_to_scaled_voltage_dac(DAC_CHANNEL_C, sample_c, octave + OC::voicing[_voicing][2] + OC::inversion[_inversion][2], OC::DAC::get_voltage_scaling(DAC_CHANNEL_C)); + sample_d = OC::DAC::pitch_to_scaled_voltage_dac(DAC_CHANNEL_D, sample_d, octave + OC::voicing[_voicing][3] + OC::inversion[_inversion][3], OC::DAC::get_voltage_scaling(DAC_CHANNEL_D)); + + OC::DAC::set(sample_a); + OC::DAC::set(sample_b); + OC::DAC::set(sample_c); + OC::DAC::set(sample_d); + } + + bool changed = (last_sample_ != sample_a); + + if (changed) { + MENU_REDRAW = 1; + last_sample_ = sample_a; + } + + if (triggered) { + clock_display_.Update(1, true); + } else { + clock_display_.Update(1, false); + } + } + + // Wrappers for ScaleEdit + void scale_changed() { + force_update_ = true; + } + + uint16_t get_scale_mask(uint8_t scale_select) const { + return get_mask(); + } + + void update_scale_mask(uint16_t mask, uint8_t scale_select) { + apply_value(CHORDS_SETTING_MASK, mask); + last_mask_ = mask; + force_update_ = true; + } + + // Maintain an internal list of currently available settings, since some are + // dependent on others. It's kind of brute force, but eh, works :) If other + // apps have a similar need, it can be moved to a common wrapper + + int num_enabled_settings() const { + return num_enabled_settings_; + } + + CHORDS_SETTINGS enabled_setting_at(int index) const { + return enabled_settings_[index]; + } + + void update_enabled_settings() { + + CHORDS_SETTINGS *settings = enabled_settings_; + + switch(get_menu_page()) { + + case MENU_PARAMETERS: { + + *settings++ = CHORDS_SETTING_MASK; + // hide root ? + if (get_scale(DUMMY) != OC::Scales::SCALE_NONE) + *settings++ = CHORDS_SETTING_ROOT; + else + *settings++ = CHORDS_SETTING_MORE_DUMMY; + + *settings++ = CHORDS_SETTING_PROGRESSION; + *settings++ = CHORDS_SETTING_CHORD_EDIT; + *settings++ = CHORDS_SETTING_PLAYMODES; + if (get_playmode() < _SH1) + *settings++ = CHORDS_SETTING_DIRECTION; + if (get_direction() == CHORDS_BROWNIAN) + *settings++ = CHORDS_SETTING_BROWNIAN_PROBABILITY; + *settings++ = CHORDS_SETTING_TRANSPOSE; + *settings++ = CHORDS_SETTING_OCTAVE; + *settings++ = CHORDS_SETTING_CV_SOURCE; + *settings++ = CHORDS_SETTING_CHORDS_ADVANCE_TRIGGER_SOURCE; + *settings++ = CHORDS_SETTING_TRIGGER_DELAY; + } + break; + case MENU_CV_MAPPING: { + + *settings++ = CHORDS_SETTING_MASK_CV; + // destinations: + // hide root CV? + if (get_scale(DUMMY) != OC::Scales::SCALE_NONE) + *settings++ = CHORDS_SETTING_ROOT_CV; + else + *settings++ = CHORDS_SETTING_MORE_DUMMY; + + *settings++ = CHORDS_SETTING_PROGRESSION_CV; + *settings++ = CHORDS_SETTING_CHORD_EDIT; + *settings++ = CHORDS_SETTING_NUM_CHORDS_CV; + if (get_playmode() < _SH1) + *settings++ = CHORDS_SETTING_DIRECTION_CV; + if (get_direction() == CHORDS_BROWNIAN) + *settings++ = CHORDS_SETTING_BROWNIAN_CV; + *settings++ = CHORDS_SETTING_TRANSPOSE_CV; + *settings++ = CHORDS_SETTING_OCTAVE_CV; + *settings++ = CHORDS_SETTING_QUALITY_CV; + *settings++ = CHORDS_SETTING_INVERSION_CV; + *settings++ = CHORDS_SETTING_VOICING_CV; + } + break; + default: + break; + } + // end switch + num_enabled_settings_ = settings - enabled_settings_; + } + + void RenderScreensaver(weegfx::coord_t x) const; + +private: + bool force_update_; + bool _octave_toggle; + int last_scale_; + uint16_t last_mask_; + int32_t last_sample_; + uint8_t display_num_chords_; + bool chord_advance_last_; + bool progression_advance_last_; + int8_t active_chord_; + int8_t progression_cnt_; + int8_t progression_EoP_; + bool chord_repeat_; + int8_t active_progression_; + int8_t menu_page_; + bool chords_direction_; + int8_t playmode_last_; + int8_t progression_last_; + int8_t num_chords_last_; + + util::TriggerDelay trigger_delay_; + braids::Quantizer quantizer_; + OC::Input_Map input_map_; + OC::DigitalInputDisplay clock_display_; + OC::Chords chords_; + + int num_enabled_settings_; + CHORDS_SETTINGS enabled_settings_[CHORDS_SETTING_LAST]; + + bool update_scale(bool force, int32_t mask_rotate) { + + force_update_ = false; + const int scale = get_scale(DUMMY); + uint16_t mask = get_mask(); + + if (mask_rotate) + mask = OC::ScaleEditor::RotateMask(mask, OC::Scales::GetScale(scale).num_notes, mask_rotate); + + if (force || (last_scale_ != scale || last_mask_ != mask)) { + last_scale_ = scale; + last_mask_ = mask; + quantizer_.Configure(OC::Scales::GetScale(scale), mask); + return true; + } else { + return false; + } + } +}; + +const char* const chords_advance_trigger_sources[] = { + "TR1", "TR2" +}; + +const char* const chords_slots[] = { + "#1", "#2", "#3", "#4", "#5", "#6", "#7", "#8" +}; + +const char* const chord_playmodes[] = { + "-", "SEQ+1", "SEQ+2", "SEQ+3", "TR3+1", "TR3+2", "TR3+3", "S+H#1", "S+H#2", "S+H#3", "S+H#4", "CV#1", "CV#2", "CV#3", "CV#4" +}; + +// TOTAL EEPROM SIZE: 25 bytes +SETTINGS_DECLARE(Chords, CHORDS_SETTING_LAST) { + { OC::Scales::SCALE_SEMI, OC::Scales::SCALE_SEMI, OC::Scales::NUM_SCALES - 1, "scale", OC::scale_names, settings::STORAGE_TYPE_U8 }, + { 0, 0, 11, "root", OC::Strings::note_names_unpadded, settings::STORAGE_TYPE_U8 }, + { 0, 0, OC::Chords::NUM_CHORD_PROGRESSIONS - 1, "progression", chords_slots, settings::STORAGE_TYPE_U8 }, + { 65535, 1, 65535, "scale -->", NULL, settings::STORAGE_TYPE_U16 }, // mask + { 0, 0, CHORDS_CV_SOURCE_LAST - 1, "CV source", OC::Strings::cv_input_names, settings::STORAGE_TYPE_U8 }, /// to do .. + { CHORDS_ADVANCE_TRIGGER_SOURCE_TR2, 0, CHORDS_ADVANCE_TRIGGER_SOURCE_LAST - 1, "chords trg src", chords_advance_trigger_sources, settings::STORAGE_TYPE_U8 }, + { 0, 0, CHORDS_PLAYMODES_LAST - 1, "playmode", chord_playmodes, settings::STORAGE_TYPE_U8 }, + { 0, 0, CHORDS_DIRECTIONS_LAST - 1, "direction", OC::Strings::seq_directions, settings::STORAGE_TYPE_U8 }, + { 64, 0, 255, "-->brown prob", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, OC::kNumDelayTimes - 1, "TR1 delay", OC::Strings::trigger_delay_times, settings::STORAGE_TYPE_U8 }, + { 0, -5, 7, "transpose", NULL, settings::STORAGE_TYPE_I8 }, + { 0, -4, 4, "octave", NULL, settings::STORAGE_TYPE_I8 }, + { 0, 0, OC::Chords::CHORDS_USER_LAST - 1, "chord:", chords_slots, settings::STORAGE_TYPE_U8 }, + { 0, 0, OC::Chords::CHORDS_USER_LAST - 1, "num.chords", NULL, settings::STORAGE_TYPE_U8 }, // progression 1 + { 0, 0, OC::Chords::CHORDS_USER_LAST - 1, "num.chords", NULL, settings::STORAGE_TYPE_U8 }, // progression 2 + { 0, 0, OC::Chords::CHORDS_USER_LAST - 1, "num.chords", NULL, settings::STORAGE_TYPE_U8 }, // progression 3 + { 0, 0, OC::Chords::CHORDS_USER_LAST - 1, "num.chords", NULL, settings::STORAGE_TYPE_U8 }, // progression 4 + { 0, 0, 0, "chords -->", NULL, settings::STORAGE_TYPE_U4 }, // = chord editor + // CV + { 0, 0, 4, "root CV >", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "mask CV >", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "transpose CV >", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "octave CV >", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "quality CV >", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "voicing CV >", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "inversion CV >", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "prg.slot# CV >", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "direction CV >", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "-->br.prb CV >", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "num.chrds CV >", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 0, "-", NULL, settings::STORAGE_TYPE_U4 }, // DUMMY + { 0, 0, 0, " ", NULL, settings::STORAGE_TYPE_U4 } // MORE DUMMY +}; + +class ChordQuantizer { +public: + void Init() { + cursor.Init(CHORDS_SETTING_SCALE, CHORDS_SETTING_LAST - 1); + scale_editor.Init(false); + chord_editor.Init(); + left_encoder_value = OC::Scales::SCALE_SEMI; + } + + inline bool editing() const { + return cursor.editing(); + } + + inline int cursor_pos() const { + return cursor.cursor_pos(); + } + + menu::ScreenCursor cursor; + // menu::ScreenCursor cursor; + OC::ScaleEditor scale_editor; + OC::ChordEditor chord_editor; + int left_encoder_value; +}; + +ChordQuantizer chords_state; +Chords chords; + +void CHORDS_init() { + + chords.InitDefaults(); + chords.Init(); + chords_state.Init(); + chords.update_enabled_settings(); + chords_state.cursor.AdjustEnd(chords.num_enabled_settings() - 1); +} + +size_t CHORDS_storageSize() { + return Chords::storageSize(); +} + +size_t CHORDS_save(void *storage) { + return chords.Save(storage); +} + +size_t CHORDS_restore(const void *storage) { + + size_t storage_size = chords.Restore(storage); + chords.update_enabled_settings(); + chords_state.left_encoder_value = chords.get_scale(DUMMY); + chords.set_scale(chords_state.left_encoder_value); + chords_state.cursor.AdjustEnd(chords.num_enabled_settings() - 1); + return storage_size; +} + +void CHORDS_handleAppEvent(OC::AppEvent event) { + switch (event) { + case OC::APP_EVENT_RESUME: + chords_state.cursor.set_editing(false); + chords_state.scale_editor.Close(); + chords_state.chord_editor.Close(); + break; + case OC::APP_EVENT_SUSPEND: + case OC::APP_EVENT_SCREENSAVER_ON: + case OC::APP_EVENT_SCREENSAVER_OFF: + break; + } +} + +void CHORDS_isr() { + + uint32_t triggers = OC::DigitalInputs::clocked(); + chords.Update(triggers); +} + +void CHORDS_loop() { +} + +void CHORDS_menu() { + + menu::TitleBar<0, 4, 0>::Draw(); + + // print scale + int scale = chords_state.left_encoder_value; + graphics.movePrintPos(5, 0); + graphics.print(OC::scale_names[scale]); + if (chords.get_scale(DUMMY) == scale) + graphics.drawBitmap8(1, menu::QuadTitleBar::kTextY, 4, OC::bitmap_indicator_4x8); + + // active progression # + graphics.setPrintPos(106, 2); + if (chords.poke_octave_toggle()) + graphics.print("+"); + else + graphics.print("#"); + graphics.print(chords.get_active_progression() + 0x1); + + uint8_t clock_state = (chords.clockState() + 3) >> 2; + if (clock_state && !chords_state.chord_editor.active()) + graphics.drawBitmap8(121, 2, 4, OC::bitmap_gate_indicators_8 + (clock_state << 2)); + + menu::SettingsList settings_list(chords_state.cursor); + menu::SettingsListItem list_item; + + while (settings_list.available()) { + + const int setting = chords.enabled_setting_at(settings_list.Next(list_item)); + const int value = chords.get_value(setting); + const settings::value_attr &attr = Chords::value_attr(setting); + + switch(setting) { + + case CHORDS_SETTING_MASK: + menu::DrawMask(menu::kDisplayWidth, list_item.y, chords.get_rotated_mask(), OC::Scales::GetScale(chords.get_scale(DUMMY)).num_notes); + list_item.DrawNoValue(value, attr); + break; + case CHORDS_SETTING_DUMMY: + case CHORDS_SETTING_CHORD_EDIT: + // to do: draw something that makes sense, presumably some pre-made icons would work best. + menu::DrawMiniChord(menu::kDisplayWidth, list_item.y, chords.get_display_num_chords(), chords.active_chord()); + list_item.DrawNoValue(value, attr); + break; + case CHORDS_SETTING_MORE_DUMMY: + list_item.DrawNoValue(value, attr); + break; + case CHORDS_SETTING_CHORD_SLOT: + //special case: + list_item.DrawValueMax(value, attr, chords.get_num_chords(chords.get_progression())); + break; + default: + list_item.DrawDefault(value, attr); + break; + } + + if (chords_state.scale_editor.active()) + chords_state.scale_editor.Draw(); + else if (chords_state.chord_editor.active()) + chords_state.chord_editor.Draw(); + } +} + +void CHORDS_handleEncoderEvent(const UI::Event &event) { + + if (chords_state.scale_editor.active()) { + chords_state.scale_editor.HandleEncoderEvent(event); + return; + } + else if (chords_state.chord_editor.active()) { + chords_state.chord_editor.HandleEncoderEvent(event); + return; + } + + if (OC::CONTROL_ENCODER_L == event.control) { + + int value = chords_state.left_encoder_value + event.value; + CONSTRAIN(value, OC::Scales::SCALE_SEMI, OC::Scales::NUM_SCALES - 1); + chords_state.left_encoder_value = value; + + } else if (OC::CONTROL_ENCODER_R == event.control) { + + if (chords_state.editing()) { + + CHORDS_SETTINGS setting = chords.enabled_setting_at(chords_state.cursor_pos()); + + if (CHORDS_SETTING_MASK != setting) { + + if (chords.change_value(setting, event.value)) + chords.force_update(); + + switch (setting) { + case CHORDS_SETTING_CHORD_SLOT: + // special case, slot shouldn't be > num.chords + if (chords.get_chord_slot() > chords.get_num_chords(chords.get_progression())) + chords.set_chord_slot(chords.get_num_chords(chords.get_progression())); + break; + case CHORDS_SETTING_DIRECTION: + case CHORDS_SETTING_PLAYMODES: + // show options, or don't: + chords.update_enabled_settings(); + chords_state.cursor.AdjustEnd(chords.num_enabled_settings() - 1); + break; + default: + break; + } + } + } else { + chords_state.cursor.Scroll(event.value); + } + } +} + +void CHORDS_topButton() { + + if (chords.get_menu_page() == MENU_PARAMETERS) { + + if (chords.octave_toggle()) + chords.change_value(CHORDS_SETTING_OCTAVE, 1); + else + chords.change_value(CHORDS_SETTING_OCTAVE, -1); + } + else { + chords.set_menu_page(MENU_PARAMETERS); + chords.update_enabled_settings(); + chords_state.cursor.set_editing(false); + } +} + +void CHORDS_lowerButton() { + // go the CV mapping + + if (!chords_state.chord_editor.active() && !chords_state.scale_editor.active()) { + + uint8_t _menu_page = chords.get_menu_page(); + + switch (_menu_page) { + + case MENU_PARAMETERS: + _menu_page = MENU_CV_MAPPING; + break; + default: + _menu_page = MENU_PARAMETERS; + break; + } + + chords.set_menu_page(_menu_page); + chords.update_enabled_settings(); + chords_state.cursor.set_editing(false); + } +} + +void CHORDS_rightButton() { + + switch (chords.enabled_setting_at(chords_state.cursor_pos())) { + + case CHORDS_SETTING_MASK: { + int scale = chords.get_scale(DUMMY); + if (OC::Scales::SCALE_NONE != scale) + chords_state.scale_editor.Edit(&chords, scale); + } + break; + case CHORDS_SETTING_CHORD_EDIT: + chords_state.chord_editor.Edit(&chords, chords.get_chord_slot(), chords.get_num_chords(chords.get_progression()), chords.get_progression()); + break; + case CHORDS_SETTING_DUMMY: + case CHORDS_SETTING_MORE_DUMMY: + chords.set_menu_page(MENU_PARAMETERS); + chords.update_enabled_settings(); + break; + default: + chords_state.cursor.toggle_editing(); + break; + } +} + +void CHORDS_leftButton() { + + if (chords_state.left_encoder_value != chords.get_scale(DUMMY) || chords_state.left_encoder_value == OC::Scales::SCALE_SEMI) { + chords.set_scale(chords_state.left_encoder_value); + // hide/show root + chords.update_enabled_settings(); + } +} + +void CHORDS_leftButtonLong() { + // todo +} + +void CHORDS_downButtonLong() { + chords.clear_CV_mapping(); + chords_state.cursor.set_editing(false); +} + +void CHORDS_upButtonLong() { + // screensaver short cut +} + +void CHORDS_handleButtonEvent(const UI::Event &event) { + + if (UI::EVENT_BUTTON_LONG_PRESS == event.type) { + switch (event.control) { + case OC::CONTROL_BUTTON_UP: + CHORDS_upButtonLong(); + break; + case OC::CONTROL_BUTTON_DOWN: + CHORDS_downButtonLong(); + break; + case OC::CONTROL_BUTTON_L: + if (!(chords_state.chord_editor.active())) + CHORDS_leftButtonLong(); + break; + default: + break; + } + } + + if (chords_state.scale_editor.active()) { + chords_state.scale_editor.HandleButtonEvent(event); + return; + } + else if (chords_state.chord_editor.active()) { + chords_state.chord_editor.HandleButtonEvent(event); + return; + } + + if (UI::EVENT_BUTTON_PRESS == event.type) { + switch (event.control) { + case OC::CONTROL_BUTTON_UP: + CHORDS_topButton(); + break; + case OC::CONTROL_BUTTON_DOWN: + CHORDS_lowerButton(); + break; + case OC::CONTROL_BUTTON_L: + CHORDS_leftButton(); + break; + case OC::CONTROL_BUTTON_R: + CHORDS_rightButton(); + break; + } + } +} + +static const weegfx::coord_t chords_kBottom = 60; + +inline int32_t chords_render_pitch(int32_t pitch, weegfx::coord_t x, weegfx::coord_t width) { + + CONSTRAIN(pitch, 0, 120 << 7); + int32_t octave = pitch / (12 << 7); + pitch -= (octave * 12 << 7); + graphics.drawHLine(x, chords_kBottom - ((pitch * 4) >> 7), width << 1); + return octave; +} + +void Chords::RenderScreensaver(weegfx::coord_t start_x) const { + + int _active_chord = active_chord(); + int _num_progression = get_active_progression(); + int _num_chords = get_display_num_chords(); + int x = start_x + 4; + int y = 42; + + // todo: CV + for (int j = 0; j <= _num_chords; j++) { + + if (j == _active_chord) + menu::DrawChord(x + (j << 4) + 1, y, 6, j, _num_progression); + else + menu::DrawChord(x + (j << 4) + 2, y, 4, j, _num_progression); + } +} + + +void CHORDS_screensaver() { +#ifdef CHORDS_DEBUG_SCREENSAVER + debug::CycleMeasurement render_cycles; +#endif + + chords.RenderScreensaver(0); + +#ifdef CHORDS_DEBUG_SCREENSAVER + graphics.drawHLine(0, menu::kMenuLineH, menu::kDisplayWidth); + uint32_t us = debug::cycles_to_us(render_cycles.read()); + graphics.setPrintPos(0, 32); + graphics.printf("%u", us); +#endif +} + +#endif // ENABLE_APP_CHORDS diff --git a/software/o_c_REV/APP_DQ.ino b/software/o_c_REV/APP_DQ.ino new file mode 100644 index 000000000..f2e21002e --- /dev/null +++ b/software/o_c_REV/APP_DQ.ino @@ -0,0 +1,1577 @@ +// Copyright (c) 2015, 2016 Patrick Dowling, Tim Churches, Max Stadler +// +// Initial app implementation: Patrick Dowling (pld@gurkenkiste.com) +// Modifications by: Tim Churches (tim.churches@gmail.com) +// Yet more Modifications by: mxmxmx +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// Quad quantizer app, based around the the quantizer/scales implementation from +// from Braids by Olivier Gillet (see braids_quantizer.h/cc et al.). It has since +// grown a little bit... + +#ifdef ENABLE_APP_METAQ + +#include "OC_apps.h" +#include "util/util_settings.h" +#include "util/util_trigger_delay.h" +#include "braids_quantizer.h" +#include "braids_quantizer_scales.h" +#include "OC_menus.h" +#include "OC_visualfx.h" +#include "OC_scales.h" +#include "OC_scale_edit.h" +#include "OC_strings.h" +#include "OC_digital_inputs.h" +#include "OC_ADC.h" +#include "extern/dspinst.h" + +namespace menu = OC::menu; + +#ifdef BUCHLA_4U + #define DQ_OFFSET_X 22 +#else + #define DQ_OFFSET_X 47 +#endif + +const uint8_t NUMCHANNELS = 2; +const uint8_t NUM_SCALE_SLOTS = 4; +// const uint8_t PULSEW_MAX = 255; +// const uint32_t TICKS_TO_MS = 43691; + +enum DQ_ChannelSetting { + DQ_CHANNEL_SETTING_SCALE1, + DQ_CHANNEL_SETTING_SCALE2, + DQ_CHANNEL_SETTING_SCALE3, + DQ_CHANNEL_SETTING_SCALE4, + DQ_CHANNEL_SETTING_ROOT1, + DQ_CHANNEL_SETTING_ROOT2, + DQ_CHANNEL_SETTING_ROOT3, + DQ_CHANNEL_SETTING_ROOT4, + DQ_CHANNEL_SETTING_SCALE_SEQ, + DQ_CHANNEL_SETTING_MASK1, + DQ_CHANNEL_SETTING_MASK2, + DQ_CHANNEL_SETTING_MASK3, + DQ_CHANNEL_SETTING_MASK4, + DQ_CHANNEL_SETTING_SEQ_MODE, + DQ_CHANNEL_SETTING_SOURCE, + DQ_CHANNEL_SETTING_TRIGGER, + DQ_CHANNEL_SETTING_DELAY, + DQ_CHANNEL_SETTING_TRANSPOSE1, + DQ_CHANNEL_SETTING_TRANSPOSE2, + DQ_CHANNEL_SETTING_TRANSPOSE3, + DQ_CHANNEL_SETTING_TRANSPOSE4, + DQ_CHANNEL_SETTING_OCTAVE, + DQ_CHANNEL_SETTING_AUX_OUTPUT, + DQ_CHANNEL_SETTING_PULSEWIDTH, + DQ_CHANNEL_SETTING_AUX_OCTAVE, + DQ_CHANNEL_SETTING_AUX_CV_DEST, + DQ_CHANNEL_SETTING_TURING_LENGTH, + DQ_CHANNEL_SETTING_TURING_PROB, + DQ_CHANNEL_SETTING_TURING_CV_SOURCE, + DQ_CHANNEL_SETTING_TURING_RANGE, + DQ_CHANNEL_SETTING_TURING_TRIG_OUT, + DQ_CHANNEL_SETTING_LAST +}; + +enum DQ_ChannelTriggerSource { + DQ_CHANNEL_TRIGGER_TR1, + DQ_CHANNEL_TRIGGER_TR2, + DQ_CHANNEL_TRIGGER_TR3, + DQ_CHANNEL_TRIGGER_TR4, + DQ_CHANNEL_TRIGGER_CONTINUOUS_UP, + DQ_CHANNEL_TRIGGER_CONTINUOUS_DOWN, + DQ_CHANNEL_TRIGGER_LAST +}; + +enum DQ_ChannelSource { + DQ_CHANNEL_SOURCE_CV1, + DQ_CHANNEL_SOURCE_CV2, + DQ_CHANNEL_SOURCE_CV3, + DQ_CHANNEL_SOURCE_CV4, + DQ_CHANNEL_SOURCE_TURING, + DQ_CHANNEL_SOURCE_LOGISTIC_MAP, + DQ_CHANNEL_SOURCE_BYTEBEAT, + DQ_CHANNEL_SOURCE_INT_SEQ, + DQ_CHANNEL_SOURCE_LAST +}; + +enum DQ_AUX_MODE { + DQ_GATE, + DQ_COPY, + DQ_ASR, + DQ_AUX_MODE_LAST +}; + +enum TRIG_AUX_MODE { + DQ_ECHO, + DQ_LSB, + DQ_CHANGE, + DQ_TRIG_AUX_LAST +}; + +enum DQ_CV_DEST { + DQ_DEST_NONE, + DQ_DEST_SCALE_SLOT, + DQ_DEST_ROOT, + DQ_DEST_OCTAVE, + DQ_DEST_TRANSPOSE, + DQ_DEST_MASK, + DQ_DEST_LAST +}; + +enum DQ_SLOTS { + SLOT1, + SLOT2, + SLOT3, + SLOT4, + LAST_SLOT +}; + +void DQ_topButton(); +void DQ_lowerButton(); +void DQ_leftButton(); +void DQ_rightButton(); +void DQ_leftButtonLong(); +void DQ_rightButton(); +void DQ_leftButton(); +void DQ_lowerButton(); +void DQ_topButton(); +void DQ_downButtonLong(); + +class DQ_QuantizerChannel : public settings::SettingsBase { +public: + enum DQ_STATES { + OFF, + ON = 0xFFFF + }; + int get_scale(uint8_t selected_scale_slot_) const { + + switch(selected_scale_slot_) { + + case SLOT2: + return values_[DQ_CHANNEL_SETTING_SCALE2]; + break; + case SLOT3: + return values_[DQ_CHANNEL_SETTING_SCALE3]; + break; + case SLOT4: + return values_[DQ_CHANNEL_SETTING_SCALE4]; + break; + case SLOT1: + default: + return values_[DQ_CHANNEL_SETTING_SCALE1]; + break; + } + } + + int get_scale_select() const { + return values_[DQ_CHANNEL_SETTING_SCALE_SEQ]; + } + + int get_scale_seq_mode() const { + return values_[DQ_CHANNEL_SETTING_SEQ_MODE]; + } + + int get_display_scale() const { + return display_scale_slot_; + } + + int get_display_root() const { + return display_root_; + } + + void set_scale_at_slot(int scale, uint16_t mask, int root, int transpose, uint8_t scale_slot) { + + if (scale != get_scale(scale_slot) || mask != get_mask(scale_slot) || root != root_last_ || transpose != transpose_last_) { + + const OC::Scale &scale_def = OC::Scales::GetScale(scale); + root_last_ = root; + transpose_last_ = transpose; + + if (0 == (mask & ~(0xffff << scale_def.num_notes))) + mask |= 0x1; + switch (scale_slot) { + case SLOT2: + apply_value(DQ_CHANNEL_SETTING_MASK2, mask); + apply_value(DQ_CHANNEL_SETTING_SCALE2, scale); + apply_value(DQ_CHANNEL_SETTING_ROOT2, root); + apply_value(DQ_CHANNEL_SETTING_TRANSPOSE2, transpose); + break; + case SLOT3: + apply_value(DQ_CHANNEL_SETTING_MASK3, mask); + apply_value(DQ_CHANNEL_SETTING_SCALE3, scale); + apply_value(DQ_CHANNEL_SETTING_ROOT3, root); + apply_value(DQ_CHANNEL_SETTING_TRANSPOSE3, transpose); + break; + case SLOT4: + apply_value(DQ_CHANNEL_SETTING_MASK4, mask); + apply_value(DQ_CHANNEL_SETTING_SCALE4, scale); + apply_value(DQ_CHANNEL_SETTING_ROOT4, root); + apply_value(DQ_CHANNEL_SETTING_TRANSPOSE4, transpose); + break; + case SLOT1: + default: + apply_value(DQ_CHANNEL_SETTING_MASK1, mask); + apply_value(DQ_CHANNEL_SETTING_SCALE1, scale); + apply_value(DQ_CHANNEL_SETTING_ROOT1, root); + apply_value(DQ_CHANNEL_SETTING_TRANSPOSE1, transpose); + break; + } + } + } + + int get_root(uint8_t selected_scale_slot_) const { + + switch(selected_scale_slot_) { + + case SLOT2: + return values_[DQ_CHANNEL_SETTING_ROOT2]; + break; + case SLOT3: + return values_[DQ_CHANNEL_SETTING_ROOT3]; + break; + case SLOT4: + return values_[DQ_CHANNEL_SETTING_ROOT4]; + break; + case SLOT1: + default: + return values_[DQ_CHANNEL_SETTING_ROOT1]; + break; + } + } + + uint16_t get_mask(uint8_t selected_scale_slot_) const { + + switch(selected_scale_slot_) { + + case SLOT2: + return values_[DQ_CHANNEL_SETTING_MASK2]; + break; + case SLOT3: + return values_[DQ_CHANNEL_SETTING_MASK3]; + break; + case SLOT4: + return values_[DQ_CHANNEL_SETTING_MASK4]; + break; + case SLOT1: + default: + return values_[DQ_CHANNEL_SETTING_MASK1]; + break; + } + } + + uint16_t get_rotated_mask(uint8_t selected_scale_slot_) const { + return last_mask_[selected_scale_slot_]; + } + + DQ_ChannelSource get_source() const { + return static_cast(values_[DQ_CHANNEL_SETTING_SOURCE]); + } + + DQ_ChannelTriggerSource get_trigger_source() const { + return static_cast(values_[DQ_CHANNEL_SETTING_TRIGGER]); + } + + OC::DigitalInput get_digital_input() const { + return static_cast(values_[DQ_CHANNEL_SETTING_TRIGGER]); + } + + uint8_t get_aux_cv_dest() const { + return values_[DQ_CHANNEL_SETTING_AUX_CV_DEST]; + } + + uint16_t get_trigger_delay() const { + return values_[DQ_CHANNEL_SETTING_DELAY]; + } + + int get_transpose(uint8_t selected_scale_slot_) const { + + switch(selected_scale_slot_) { + + case SLOT2: + return values_[DQ_CHANNEL_SETTING_TRANSPOSE2]; + break; + case SLOT3: + return values_[DQ_CHANNEL_SETTING_TRANSPOSE3]; + break; + case SLOT4: + return values_[DQ_CHANNEL_SETTING_TRANSPOSE4]; + break; + case SLOT1: + default: + return values_[DQ_CHANNEL_SETTING_TRANSPOSE1]; + break; + } + } + + int get_octave() const { + return values_[DQ_CHANNEL_SETTING_OCTAVE]; + } + + int get_aux_mode() const { + return values_[DQ_CHANNEL_SETTING_AUX_OUTPUT]; + } + + int get_aux_octave() const { + return values_[DQ_CHANNEL_SETTING_AUX_OCTAVE]; + } + + int get_pulsewidth() const { + return values_[DQ_CHANNEL_SETTING_PULSEWIDTH]; + } + + uint8_t get_turing_length() const { + return values_[DQ_CHANNEL_SETTING_TURING_LENGTH]; + } + + uint8_t get_turing_display_length() const { + return turing_display_length_; + } + + uint8_t get_turing_range() const { + return values_[DQ_CHANNEL_SETTING_TURING_RANGE]; + } + + uint8_t get_turing_probability() const { + return values_[DQ_CHANNEL_SETTING_TURING_PROB]; + } + + uint8_t get_turing_CV() const { + return values_[DQ_CHANNEL_SETTING_TURING_CV_SOURCE]; + } + + uint8_t get_turing_trig_out() const { + return values_[DQ_CHANNEL_SETTING_TURING_TRIG_OUT]; + } + + uint32_t get_shift_register() const { + return turing_machine_.get_shift_register(); + } + + void clear_dest() { + // ... + schedule_mask_rotate_ = 0x0; + continuous_offset_ = 0x0; + prev_transpose_cv_ = 0x0; + prev_octave_cv_ = 0x0; + prev_root_cv_ = 0x0; + prev_scale_cv_ = 0x0; + } + + void reset_scale() { + scale_reset_ = true; + } + + void Init(DQ_ChannelSource source, DQ_ChannelTriggerSource trigger_source) { + + InitDefaults(); + apply_value(DQ_CHANNEL_SETTING_SOURCE, source); + apply_value(DQ_CHANNEL_SETTING_TRIGGER, trigger_source); + + force_update_ = true; + + for (int i = 0; i < NUM_SCALE_SLOTS; i++) { + last_scale_[i] = -1; + last_mask_[i] = 0xFFFF; + } + + aux_sample_ = 0; + last_sample_ = 0; + last_aux_sample_ = 0; + continuous_offset_ = 0; + scale_sequence_cnt_ = 0; + scale_reset_ = 0; + active_scale_slot_ = 0; + display_scale_slot_ = 0; + display_root_ = 0; + root_last_ = 0; + transpose_last_ = 0; + prev_scale_slot_ = 0; + scale_advance_ = 0; + scale_advance_state_ = 0; + schedule_scale_update_ = 0; + schedule_mask_rotate_ = 0; + prev_octave_cv_ = 0; + prev_transpose_cv_ = 0; + prev_root_cv_ = 0; + prev_scale_cv_ = 0; + prev_destination_ = 0; + prev_pulsewidth_ = 100; + ticks_ = 0; + channel_frequency_in_ticks_ = 1000; + pulse_width_in_ticks_ = 1000; + + trigger_delay_.Init(); + quantizer_.Init(); + update_scale(true, 0, false); + trigger_display_.Init(); + update_enabled_settings(); + + turing_machine_.Init(); + turing_display_length_ = get_turing_length(); + + scrolling_history_.Init(OC::DAC::kOctaveZero * 12 << 7); + } + + void force_update() { + force_update_ = true; + } + + void schedule_scale_update() { + schedule_scale_update_ = true; + } + + inline void Update(uint32_t triggers, DAC_CHANNEL dac_channel, DAC_CHANNEL aux_channel) { + + ticks_++; + + DQ_ChannelTriggerSource trigger_source = get_trigger_source(); + bool continuous = DQ_CHANNEL_TRIGGER_CONTINUOUS_UP == trigger_source || DQ_CHANNEL_TRIGGER_CONTINUOUS_DOWN == trigger_source; + bool triggered = !continuous && + (triggers & DIGITAL_INPUT_MASK(trigger_source - DQ_CHANNEL_TRIGGER_TR1)); + + trigger_delay_.Update(); + if (triggered) + trigger_delay_.Push(OC::trigger_delay_ticks[get_trigger_delay()]); + triggered = trigger_delay_.triggered(); + + if (triggered) { + channel_frequency_in_ticks_ = ticks_; + ticks_ = 0x0; + update_asr_ = true; + aux_sample_ = ON; + } + + if (scale_reset_) { + // manual change? + scale_reset_ = false; + scale_sequence_cnt_ = 0x0; + scale_advance_state_ = 0x1; + active_scale_slot_ = get_scale_select(); + prev_scale_slot_ = display_scale_slot_ = active_scale_slot_; + } + + if (get_scale_seq_mode()) { + // to do, don't hardcode .. + uint8_t _advance_trig = (dac_channel == DAC_CHANNEL_A) ? digitalReadFast(TR2) : digitalReadFast(TR4); + if (_advance_trig < scale_advance_state_) + scale_advance_ = true; + scale_advance_state_ = _advance_trig; + } + else if (prev_scale_slot_ != get_scale_select()) { + active_scale_slot_ = get_scale_select(); + prev_scale_slot_ = display_scale_slot_ = active_scale_slot_; + } + + if (scale_advance_) { + scale_sequence_cnt_++; + active_scale_slot_ = get_scale_select() + (scale_sequence_cnt_ % (get_scale_seq_mode() + 1)); + + if (active_scale_slot_ >= NUM_SCALE_SLOTS) + active_scale_slot_ -= NUM_SCALE_SLOTS; + scale_advance_ = false; + schedule_scale_update_ = true; + } + + bool update = continuous || triggered; + + int32_t sample = last_sample_; + int32_t temp_sample = 0; + int32_t history_sample = 0; + uint8_t aux_mode = get_aux_mode(); + + if (update) { + + int32_t transpose, pitch, quantized = 0x0; + int source, cv_source, channel_id, octave, root, _aux_cv_destination; + + source = cv_source = get_source(); + _aux_cv_destination = get_aux_cv_dest(); + channel_id = (dac_channel == DAC_CHANNEL_A) ? 1 : 3; // hardcoded to use CV2, CV4, for now + + if (_aux_cv_destination != prev_destination_) + clear_dest(); + prev_destination_ = _aux_cv_destination; + // active scale slot: + display_scale_slot_ = prev_scale_slot_ = active_scale_slot_ + prev_scale_cv_; + // get root value + root = get_root(display_scale_slot_) + prev_root_cv_; + // get transpose value + transpose = get_transpose(display_scale_slot_) + prev_transpose_cv_; + // get octave value + octave = get_octave() + prev_octave_cv_; + + // S/H: ADC values + if (!continuous) { + + switch(_aux_cv_destination) { + + case DQ_DEST_NONE: + break; + case DQ_DEST_SCALE_SLOT: + display_scale_slot_ += (OC::ADC::value(static_cast(channel_id)) + 255) >> 9; + // if scale changes, we have to update the root and transpose values, too; mask gets updated in update_scale + root = get_root(display_scale_slot_); + transpose = get_transpose(display_scale_slot_); + schedule_scale_update_ = true; + break; + case DQ_DEST_ROOT: + root += (OC::ADC::value(static_cast(channel_id)) + 127) >> 8; + break; + case DQ_DEST_MASK: + schedule_mask_rotate_ = (OC::ADC::value(static_cast(channel_id)) + 127) >> 8; + break; + case DQ_DEST_OCTAVE: + octave += (OC::ADC::value(static_cast(channel_id)) + 255) >> 9; + break; + case DQ_DEST_TRANSPOSE: + transpose += (OC::ADC::value(static_cast(channel_id)) + 64) >> 7; + break; + default: + break; + } // end switch + + if (schedule_scale_update_) { + force_update_ = true; + schedule_scale_update_ = false; + } + } // -> triggered update + + // constrain values: + CONSTRAIN(display_scale_slot_, 0, NUM_SCALE_SLOTS-1); + CONSTRAIN(octave, -4, 4); + CONSTRAIN(root, 0, 11); + CONSTRAIN(transpose, -12, 12); + + // update scale? + update_scale(force_update_, display_scale_slot_, schedule_mask_rotate_); + + // internal CV source? + if (source > DQ_CHANNEL_SOURCE_CV4) + cv_source = channel_id - 1; + + // now, acquire + process sample: + pitch = quantizer_.enabled() + ? OC::ADC::raw_pitch_value(static_cast(cv_source)) + : OC::ADC::pitch_value(static_cast(cv_source)); + + switch (source) { + + case DQ_CHANNEL_SOURCE_CV1: + case DQ_CHANNEL_SOURCE_CV2: + case DQ_CHANNEL_SOURCE_CV3: + case DQ_CHANNEL_SOURCE_CV4: + quantized = quantizer_.Process(pitch, root << 7, transpose); + break; + case DQ_CHANNEL_SOURCE_TURING: + { + if (continuous) + break; + + int16_t _length = get_turing_length(); + int16_t _probability = get_turing_probability(); + int16_t _range = get_turing_range(); + + // _pitch can do other things now -- + switch (get_turing_CV()) { + + case 1: // range + _range += ((pitch + 63) >> 6); + CONSTRAIN(_range, 1, 120); + case 2: // LEN, 1-32 + _length += ((pitch + 255) >> 8); + CONSTRAIN(_length, 1, 32); + break; + case 3: // P + _probability += ((pitch + 15) >> 4); + CONSTRAIN(_probability, 0, 255); + break; + default: + break; + } + + turing_machine_.set_length(_length); + turing_machine_.set_probability(_probability); + turing_display_length_ = _length; + + uint32_t _shift_register = turing_machine_.Clock(); + // Since our range is limited anyway, just grab the last byte for lengths > 8, otherwise scale to use bits. + uint32_t shift = turing_machine_.length(); + uint32_t _scaled = (_shift_register & 0xFF) * _range; + _scaled = _scaled >> (shift > 7 ? 8 : shift); + quantized = quantizer_.Lookup(64 + _range / 2 - _scaled + transpose) + (root<< 7); + } + break; + default: + break; + } + + // the output, thus far: + sample = temp_sample = OC::DAC::pitch_to_scaled_voltage_dac(dac_channel, quantized, octave + continuous_offset_, OC::DAC::get_voltage_scaling(dac_channel)); + + // special treatment, continuous update -- only update the modulation values if/when the quantized input changes: + bool _continuous_update = continuous && last_sample_ != sample; + + if (_continuous_update) { + + bool _re_quantize = false; + int _aux_cv = 0; + + switch(_aux_cv_destination) { + + case DQ_DEST_NONE: + break; + case DQ_DEST_SCALE_SLOT: + _aux_cv = (OC::ADC::value(static_cast(channel_id)) + 255) >> 9; + if (_aux_cv != prev_scale_cv_) { + display_scale_slot_ += _aux_cv; + CONSTRAIN(display_scale_slot_, 0, NUM_SCALE_SLOTS - 0x1); + prev_scale_cv_ = _aux_cv; + // update the root and transpose values + root = get_root(display_scale_slot_); + transpose = get_transpose(display_scale_slot_); + // and update quantizer below: + schedule_scale_update_ = true; + _re_quantize = true; + } + break; + case DQ_DEST_TRANSPOSE: + _aux_cv = (OC::ADC::value(static_cast(channel_id)) + 63) >> 7; + if (_aux_cv != prev_transpose_cv_) { + transpose = get_transpose(display_scale_slot_) + _aux_cv; + CONSTRAIN(transpose, -12, 12); + prev_transpose_cv_ = _aux_cv; + _re_quantize = true; + } + break; + case DQ_DEST_ROOT: + _aux_cv = (OC::ADC::value(static_cast(channel_id)) + 127) >> 8; + if (_aux_cv != prev_root_cv_) { + display_root_ = root = get_root(display_scale_slot_) + _aux_cv; + CONSTRAIN(root, 0, 11); + prev_root_cv_ = _aux_cv; + _re_quantize = true; + } + break; + case DQ_DEST_OCTAVE: + _aux_cv = (OC::ADC::value(static_cast(channel_id)) + 255) >> 9; + if (_aux_cv != prev_octave_cv_) { + octave = get_octave() + _aux_cv; + CONSTRAIN(octave, -4, 4); + prev_octave_cv_ = _aux_cv; + _re_quantize = true; + } + break; + case DQ_DEST_MASK: + schedule_mask_rotate_ = (OC::ADC::value(static_cast(channel_id)) + 127) >> 8; + schedule_scale_update_ = true; + break; + default: + break; + } + // end switch + + // update scale? + if (schedule_scale_update_ && _continuous_update) { + update_scale(true, display_scale_slot_, schedule_mask_rotate_); + schedule_scale_update_ = false; + } + + // offset when TR source = continuous ? + int8_t _trigger_offset = 0; + bool _trigger_update = false; + if (OC::DigitalInputs::read_immediate(static_cast(channel_id - 1))) { + _trigger_offset = (trigger_source == DQ_CHANNEL_TRIGGER_CONTINUOUS_UP) ? 1 : -1; + } + if (_trigger_offset != continuous_offset_) + _trigger_update = true; + continuous_offset_ = _trigger_offset; + + // run quantizer again -- presumably could be made more efficient... + if (_re_quantize) + quantized = quantizer_.Process(pitch, root << 7, transpose); + if (_re_quantize || _trigger_update) + sample = OC::DAC::pitch_to_scaled_voltage_dac(dac_channel, quantized, octave + continuous_offset_, OC::DAC::get_voltage_scaling(dac_channel)); + // update ASR? + update_asr_ = (aux_mode == DQ_ASR && last_sample_ != sample); + + } + // end special treatment + + display_root_ = root; + history_sample = quantized + ((OC::DAC::kOctaveZero + octave) * 12 << 7); + + // deal with aux output: + switch (aux_mode) { + + case DQ_COPY: + // offset the quantized value: + aux_sample_ = OC::DAC::pitch_to_scaled_voltage_dac(aux_channel, quantized, octave + continuous_offset_ + get_aux_octave(), OC::DAC::get_voltage_scaling(aux_channel)); + break; + case DQ_ASR: + { + if (update_asr_) { + update_asr_ = false; + aux_sample_ = OC::DAC::pitch_to_scaled_voltage_dac(aux_channel, last_aux_sample_, octave + continuous_offset_ + get_aux_octave(), OC::DAC::get_voltage_scaling(aux_channel)); + last_aux_sample_ = quantized; + } + } + break; + case DQ_GATE: { + + if (source == DQ_CHANNEL_SOURCE_TURING) { + + switch(get_turing_trig_out()) { + + case DQ_ECHO: + break; + case DQ_LSB: + if (!turing_machine_.get_LSB()) + aux_sample_ = OFF; + break; + case DQ_CHANGE: + if (last_sample_ == sample) + aux_sample_ = OFF; + break; + default: + break; + } + } + } + break; + default: + break; + } + } + + // in continuous mode, don't track transposed sample: + bool changed = continuous ? (last_sample_ != temp_sample) : (last_sample_ != sample); + + if (changed) { + + MENU_REDRAW = 1; + last_sample_ = continuous ? temp_sample : sample; + + // in continuous mode, make aux output go high: + if (continuous && aux_mode == DQ_GATE) { + aux_sample_ = ON; + ticks_ = 0x0; + } + } + + // aux outputs: + int32_t aux_sample = aux_sample_; + + switch (aux_mode) { + + case DQ_COPY: + case DQ_ASR: + break; + case DQ_GATE: + { + if (aux_sample) { + + // pulsewidth setting -- + int16_t _pulsewidth = get_pulsewidth(); + + if (_pulsewidth || continuous) { // don't echo + + bool _gates = false; + + if (_pulsewidth == 255) + _gates = true; + // we-can't-echo-hack + if (continuous && !_pulsewidth) + _pulsewidth = 0x1; + + // recalculate (in ticks), if new pulsewidth setting: + if (prev_pulsewidth_ != _pulsewidth || ! ticks_) { + + if (!_gates) { + int32_t _fraction = signed_multiply_32x16b(43691, static_cast(_pulsewidth)); // = * 0.6667f + _fraction = signed_saturate_rshift(_fraction, 16, 0); + pulse_width_in_ticks_ = (_pulsewidth << 4) + _fraction; + } + else { // put out gates/half duty cycle: + + pulse_width_in_ticks_ = channel_frequency_in_ticks_ >> 1; + + if (_pulsewidth != 255) { // CV? + pulse_width_in_ticks_ = signed_multiply_32x16b(static_cast(_pulsewidth) << 8, pulse_width_in_ticks_); // + pulse_width_in_ticks_ = signed_saturate_rshift(pulse_width_in_ticks_, 16, 0); + } + } + } + prev_pulsewidth_ = _pulsewidth; + + // limit pulsewidth, if approaching half duty cycle: + if (!_gates && pulse_width_in_ticks_ >= channel_frequency_in_ticks_>>1) + pulse_width_in_ticks_ = (channel_frequency_in_ticks_ >> 1) | 1u; + + // turn off output? + if (ticks_ >= pulse_width_in_ticks_) + aux_sample_ = OFF; + else // keep on + aux_sample_ = ON; + } + else { + // we simply echo the pulsewidth: + aux_sample_ = OC::DigitalInputs::read_immediate(get_digital_input()) ? ON : OFF; + } + } + } + // scale gate + #ifdef BUCHLA_4U + aux_sample = (aux_sample_ == ON) ? OC::DAC::get_octave_offset(aux_channel, OCTAVES - OC::DAC::kOctaveZero - 0x2) : OC::DAC::get_zero_offset(aux_channel); + #else + aux_sample = (aux_sample_ == ON) ? OC::DAC::get_octave_offset(aux_channel, OCTAVES - OC::DAC::kOctaveZero - 0x1) : OC::DAC::get_zero_offset(aux_channel); + #endif + break; + default: + break; + } + + OC::DAC::set(dac_channel, sample); + OC::DAC::set(aux_channel, aux_sample); + + if (triggered || (continuous && changed)) { + scrolling_history_.Push(history_sample); + trigger_display_.Update(1, true); + } else { + trigger_display_.Update(1, false); + } + scrolling_history_.Update(); + } + + // Wrappers for ScaleEdit + void scale_changed() { + force_update_ = true; + } + + uint16_t get_scale_mask(uint8_t scale_select) const { + return get_mask(scale_select); + } + + void update_scale_mask(uint16_t mask, uint8_t scale_select) { + + switch (scale_select) { + + case SLOT1: + apply_value(DQ_CHANNEL_SETTING_MASK1, mask); + last_mask_[0] = mask; + break; + case SLOT2: + apply_value(DQ_CHANNEL_SETTING_MASK2, mask); + last_mask_[1] = mask; + break; + case SLOT3: + apply_value(DQ_CHANNEL_SETTING_MASK3, mask); + last_mask_[2] = mask; + break; + case SLOT4: + apply_value(DQ_CHANNEL_SETTING_MASK4, mask); + last_mask_[3] = mask; + break; + default: + break; + } + force_update_ = true; + } + // + + uint8_t getTriggerState() const { + return trigger_display_.getState(); + } + + // Maintain an internal list of currently available settings, since some are + // dependent on others. It's kind of brute force, but eh, works :) If other + // apps have a similar need, it can be moved to a common wrapper + + int num_enabled_settings() const { + return num_enabled_settings_; + } + + DQ_ChannelSetting enabled_setting_at(int index) const { + return enabled_settings_[index]; + } + + void update_enabled_settings() { + DQ_ChannelSetting *settings = enabled_settings_; + + switch(get_scale_select()) { + + case SLOT1: + *settings++ = DQ_CHANNEL_SETTING_SCALE1; + break; + case SLOT2: + *settings++ = DQ_CHANNEL_SETTING_SCALE2; + break; + case SLOT3: + *settings++ = DQ_CHANNEL_SETTING_SCALE3; + break; + case SLOT4: + *settings++ = DQ_CHANNEL_SETTING_SCALE4; + break; + default: + break; + } + + // to do -- might as well disable no scale + if (OC::Scales::SCALE_NONE != get_scale(get_scale_select())) { + + switch(get_scale_select()) { + + case SLOT1: + *settings++ = DQ_CHANNEL_SETTING_MASK1; + break; + case SLOT2: + *settings++ = DQ_CHANNEL_SETTING_MASK2; + break; + case SLOT3: + *settings++ = DQ_CHANNEL_SETTING_MASK3; + break; + case SLOT4: + *settings++ = DQ_CHANNEL_SETTING_MASK4; + break; + default: + break; + } + + *settings++ = DQ_CHANNEL_SETTING_SEQ_MODE; + *settings++ = DQ_CHANNEL_SETTING_SCALE_SEQ; + + switch(get_scale_select()) { + + case SLOT1: + *settings++ = DQ_CHANNEL_SETTING_ROOT1; + *settings++ = DQ_CHANNEL_SETTING_TRANSPOSE1; + break; + case SLOT2: + *settings++ = DQ_CHANNEL_SETTING_ROOT2; + *settings++ = DQ_CHANNEL_SETTING_TRANSPOSE2; + break; + case SLOT3: + *settings++ = DQ_CHANNEL_SETTING_ROOT3; + *settings++ = DQ_CHANNEL_SETTING_TRANSPOSE3; + break; + case SLOT4: + *settings++ = DQ_CHANNEL_SETTING_ROOT4; + *settings++ = DQ_CHANNEL_SETTING_TRANSPOSE4; + break; + default: + break; + } + } + + // todo -- item order? + *settings++ = DQ_CHANNEL_SETTING_OCTAVE; + *settings++ = DQ_CHANNEL_SETTING_SOURCE; + + // CV sources: + switch (get_source()) { + + case DQ_CHANNEL_SOURCE_TURING: + *settings++ = DQ_CHANNEL_SETTING_TURING_RANGE; + *settings++ = DQ_CHANNEL_SETTING_TURING_LENGTH; + *settings++ = DQ_CHANNEL_SETTING_TURING_PROB; + *settings++ = DQ_CHANNEL_SETTING_TURING_CV_SOURCE; + *settings++ = DQ_CHANNEL_SETTING_TURING_TRIG_OUT; + break; + default: + break; + } + + *settings++ = DQ_CHANNEL_SETTING_AUX_CV_DEST; + *settings++ = DQ_CHANNEL_SETTING_TRIGGER; + + if (get_trigger_source() < DQ_CHANNEL_TRIGGER_CONTINUOUS_UP) + *settings++ = DQ_CHANNEL_SETTING_DELAY; + + *settings++ = DQ_CHANNEL_SETTING_AUX_OUTPUT; + + switch(get_aux_mode()) { + + case DQ_GATE: + *settings++ = DQ_CHANNEL_SETTING_PULSEWIDTH; + break; + case DQ_COPY: + *settings++ = DQ_CHANNEL_SETTING_AUX_OCTAVE; + break; + case DQ_ASR: + *settings++ = DQ_CHANNEL_SETTING_AUX_OCTAVE; + break; + default: + break; + } + + num_enabled_settings_ = settings - enabled_settings_; + } + + // + + void RenderScreensaver(weegfx::coord_t x) const; + +private: + bool force_update_; + bool update_asr_; + int last_scale_[NUM_SCALE_SLOTS]; + uint16_t last_mask_[NUM_SCALE_SLOTS]; + int scale_sequence_cnt_; + int active_scale_slot_; + int display_scale_slot_; + int display_root_; + int root_last_; + int transpose_last_; + int prev_scale_slot_; + int8_t scale_advance_; + int8_t scale_advance_state_; + bool scale_reset_; + bool schedule_scale_update_; + int32_t schedule_mask_rotate_; + int32_t last_sample_; + int32_t aux_sample_; + int32_t last_aux_sample_; + int8_t continuous_offset_; + uint8_t prev_pulsewidth_; + int8_t prev_destination_; + int8_t prev_octave_cv_; + int8_t prev_transpose_cv_; + int8_t prev_root_cv_; + int8_t prev_scale_cv_; + + uint32_t ticks_; + uint32_t channel_frequency_in_ticks_; + uint32_t pulse_width_in_ticks_; + + util::TriggerDelay trigger_delay_; + braids::Quantizer quantizer_; + OC::DigitalInputDisplay trigger_display_; + + // internal CV sources; + util::TuringShiftRegister turing_machine_; + int8_t turing_display_length_; + + int num_enabled_settings_; + DQ_ChannelSetting enabled_settings_[DQ_CHANNEL_SETTING_LAST]; + + OC::vfx::ScrollingHistory scrolling_history_; + + bool update_scale(bool force, uint8_t scale_select, int32_t mask_rotate) { + + force_update_ = false; + const int scale = get_scale(scale_select); + uint16_t mask = get_mask(scale_select); + + if (mask_rotate) + mask = OC::ScaleEditor::RotateMask(mask, OC::Scales::GetScale(scale).num_notes, mask_rotate); + + if (force || (last_scale_[scale_select] != scale || last_mask_[scale_select] != mask)) { + last_scale_[scale_select] = scale; + last_mask_[scale_select] = mask; + quantizer_.Configure(OC::Scales::GetScale(scale), mask); + return true; + } else { + return false; + } + } +}; + +const char* const dq_seq_scales[] = { + "s#1", "s#2", "s#3", "s#4" +}; + +const char* const dq_seq_modes[] = { + "-", "TR+1", "TR+2", "TR+3" +}; + +const char* const dq_aux_outputs[] = { + "gate", "copy", "asr" +}; + +const char* const dq_aux_cv_dest[] = { + "-", "scl#", "root", "oct", "trns", "mask" +}; + +const char* const dq_tm_trig_out[] = { + "echo", "lsb", "chng" +}; + +// TOTAL EEPROM SIZE: 2 * 34 bytes +SETTINGS_DECLARE(DQ_QuantizerChannel, DQ_CHANNEL_SETTING_LAST) { + { OC::Scales::SCALE_SEMI, 0, OC::Scales::NUM_SCALES - 1, "scale", OC::scale_names, settings::STORAGE_TYPE_U8 }, + { OC::Scales::SCALE_SEMI, 0, OC::Scales::NUM_SCALES - 1, "scale", OC::scale_names, settings::STORAGE_TYPE_U8 }, + { OC::Scales::SCALE_SEMI, 0, OC::Scales::NUM_SCALES - 1, "scale", OC::scale_names, settings::STORAGE_TYPE_U8 }, + { OC::Scales::SCALE_SEMI, 0, OC::Scales::NUM_SCALES - 1, "scale", OC::scale_names, settings::STORAGE_TYPE_U8 }, + { 0, 0, 11, "root #1", OC::Strings::note_names_unpadded, settings::STORAGE_TYPE_U8 }, + { 0, 0, 11, "root #2", OC::Strings::note_names_unpadded, settings::STORAGE_TYPE_U8 }, + { 0, 0, 11, "root #3", OC::Strings::note_names_unpadded, settings::STORAGE_TYPE_U8 }, + { 0, 0, 11, "root #4", OC::Strings::note_names_unpadded, settings::STORAGE_TYPE_U8 }, + { 0, 0, NUM_SCALE_SLOTS - 1, "scale #", dq_seq_scales, settings::STORAGE_TYPE_U8 }, + { 65535, 1, 65535, "--> edit", NULL, settings::STORAGE_TYPE_U16 }, + { 65535, 1, 65535, "--> edit", NULL, settings::STORAGE_TYPE_U16 }, + { 65535, 1, 65535, "--> edit", NULL, settings::STORAGE_TYPE_U16 }, + { 65535, 1, 65535, "--> edit", NULL, settings::STORAGE_TYPE_U16 }, + { 0, 0, 3, "seq_mode", dq_seq_modes, settings::STORAGE_TYPE_U4 }, + { DQ_CHANNEL_SOURCE_CV1, DQ_CHANNEL_SOURCE_CV1, 4, "CV source", OC::Strings::cv_input_names, settings::STORAGE_TYPE_U4 }, /// to do .. + { DQ_CHANNEL_TRIGGER_CONTINUOUS_DOWN, 0, DQ_CHANNEL_TRIGGER_LAST - 1, "trigger source", OC::Strings::channel_trigger_sources, settings::STORAGE_TYPE_U8 }, + { 0, 0, OC::kNumDelayTimes - 1, "--> latency", OC::Strings::trigger_delay_times, settings::STORAGE_TYPE_U8 }, + { 0, -5, 7, "transpose #1", NULL, settings::STORAGE_TYPE_I8 }, + { 0, -5, 7, "transpose #2", NULL, settings::STORAGE_TYPE_I8 }, + { 0, -5, 7, "transpose #3", NULL, settings::STORAGE_TYPE_I8 }, + { 0, -5, 7, "transpose #4", NULL, settings::STORAGE_TYPE_I8 }, + { 0, -4, 4, "octave", NULL, settings::STORAGE_TYPE_I8 }, + { 0, 0, DQ_AUX_MODE_LAST-1, "aux.output", dq_aux_outputs, settings::STORAGE_TYPE_U8 }, + { 25, 0, 255, "--> pw", NULL, settings::STORAGE_TYPE_U8 }, + { 0, -5, 5, "--> aux +/-", NULL, settings::STORAGE_TYPE_I8 }, // aux octave + { 0, 0, DQ_DEST_LAST-1, "CV aux.", dq_aux_cv_dest, settings::STORAGE_TYPE_U8 }, + { 16, 1, 32, " > LFSR length", NULL, settings::STORAGE_TYPE_U8 }, + { 128, 0, 255, " > LFSR p", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 3, " > LFSR CV", OC::Strings::TM_aux_cv_destinations, settings::STORAGE_TYPE_U8 }, // ?? + { 15, 1, 120, " > LFSR range", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, DQ_TRIG_AUX_LAST-1, " > LFSR TRIG", dq_tm_trig_out, settings::STORAGE_TYPE_U8 } +}; + +// WIP refactoring to better encapsulate and for possible app interface change +class DualQuantizer { +public: + void Init() { + selected_channel = 0; + cursor.Init(DQ_CHANNEL_SETTING_SCALE1, DQ_CHANNEL_SETTING_LAST - 1); + scale_editor.Init(true); + } + + inline bool editing() const { + return cursor.editing(); + } + + inline int cursor_pos() const { + return cursor.cursor_pos(); + } + + int selected_channel; + menu::ScreenCursor cursor; + OC::ScaleEditor scale_editor; +}; + +DualQuantizer dq_state; +DQ_QuantizerChannel dq_quantizer_channels[NUMCHANNELS]; + +void DQ_init() { + + dq_state.Init(); + for (size_t i = 0; i < NUMCHANNELS; ++i) { + dq_quantizer_channels[i].Init(static_cast(DQ_CHANNEL_SOURCE_CV1 + 2*i), static_cast(DQ_CHANNEL_TRIGGER_TR1 + 2*i)); + } + + dq_state.cursor.AdjustEnd(dq_quantizer_channels[0].num_enabled_settings() - 1); +} + +size_t DQ_storageSize() { + return NUMCHANNELS * DQ_QuantizerChannel::storageSize(); +} + +size_t DQ_save(void *storage) { + size_t used = 0; + for (size_t i = 0; i < NUMCHANNELS; ++i) { + used += dq_quantizer_channels[i].Save(static_cast(storage) + used); + } + return used; +} + +size_t DQ_restore(const void *storage) { + size_t used = 0; + for (size_t i = 0; i < NUMCHANNELS; ++i) { + used += dq_quantizer_channels[i].Restore(static_cast(storage) + used); + //int scale = dq_quantizer_channels[i].get_scale_select(); + for (size_t j = SLOT1; j < LAST_SLOT; j++) { + dq_quantizer_channels[i].update_scale_mask(dq_quantizer_channels[i].get_mask(j), j); + } + dq_quantizer_channels[i].update_enabled_settings(); + } + dq_state.cursor.AdjustEnd(dq_quantizer_channels[0].num_enabled_settings() - 1); + return used; +} + +void DQ_handleAppEvent(OC::AppEvent event) { + switch (event) { + case OC::APP_EVENT_RESUME: + dq_state.cursor.set_editing(false); + dq_state.scale_editor.Close(); + break; + case OC::APP_EVENT_SUSPEND: + case OC::APP_EVENT_SCREENSAVER_ON: + case OC::APP_EVENT_SCREENSAVER_OFF: + break; + } +} + +void DQ_isr() { + + uint32_t triggers = OC::DigitalInputs::clocked(); + + dq_quantizer_channels[0].Update(triggers, DAC_CHANNEL_A, DAC_CHANNEL_C); + dq_quantizer_channels[1].Update(triggers, DAC_CHANNEL_B, DAC_CHANNEL_D); +} + +void DQ_loop() { +} + +void DQ_menu() { + + menu::DualTitleBar::Draw(); + + for (int i = 0, x = 0; i < NUMCHANNELS; ++i, x += 21) { + + const DQ_QuantizerChannel &channel = dq_quantizer_channels[i]; + menu::DualTitleBar::SetColumn(i); + menu::DualTitleBar::DrawGateIndicator(i, channel.getTriggerState()); + + graphics.movePrintPos(5, 0); + graphics.print((char)('A' + i)); + graphics.movePrintPos(2, 0); + graphics.print('#'); + graphics.print(channel.get_display_scale() + 1); + graphics.movePrintPos(12, 0); + if (channel.get_aux_cv_dest() == DQ_DEST_ROOT) + graphics.print(OC::Strings::note_names[channel.get_display_root()]); + else + graphics.print(OC::Strings::note_names[channel.get_root(channel.get_display_scale())]); + int octave = channel.get_octave(); + if (octave) + graphics.pretty_print(octave); + } + menu::DualTitleBar::Selected(dq_state.selected_channel); + + + const DQ_QuantizerChannel &channel = dq_quantizer_channels[dq_state.selected_channel]; + + menu::SettingsList settings_list(dq_state.cursor); + menu::SettingsListItem list_item; + while (settings_list.available()) { + const int setting = + channel.enabled_setting_at(settings_list.Next(list_item)); + const int value = channel.get_value(setting); + const settings::value_attr &attr = DQ_QuantizerChannel::value_attr(setting); + + switch (setting) { + case DQ_CHANNEL_SETTING_SCALE1: + case DQ_CHANNEL_SETTING_SCALE2: + case DQ_CHANNEL_SETTING_SCALE3: + case DQ_CHANNEL_SETTING_SCALE4: + list_item.SetPrintPos(); + if (list_item.editing) { + menu::DrawEditIcon(6, list_item.y, value, attr); + graphics.movePrintPos(6, 0); + } + graphics.print(OC::scale_names[value]); + list_item.DrawCustom(); + break; + case DQ_CHANNEL_SETTING_MASK1: + case DQ_CHANNEL_SETTING_MASK2: + case DQ_CHANNEL_SETTING_MASK3: + case DQ_CHANNEL_SETTING_MASK4: + menu::DrawMask(menu::kDisplayWidth, list_item.y, channel.get_rotated_mask(channel.get_display_scale()), OC::Scales::GetScale(channel.get_scale(channel.get_display_scale())).num_notes); + list_item.DrawNoValue(value, attr); + break; + case DQ_CHANNEL_SETTING_TRIGGER: + { + if (channel.get_source() > DQ_CHANNEL_SOURCE_CV4) + list_item.DrawValueMax(value, attr, DQ_CHANNEL_TRIGGER_TR4); + else + list_item.DrawDefault(value, attr); + } + break; + case DQ_CHANNEL_SETTING_SOURCE: + { + if (channel.get_source() == DQ_CHANNEL_SOURCE_TURING) { + + int turing_length = channel.get_turing_display_length(); + int w = turing_length >= 16 ? 16 * 3 : turing_length * 3; + + menu::DrawMask(menu::kDisplayWidth, list_item.y, channel.get_shift_register(), turing_length); + list_item.valuex = menu::kDisplayWidth - w - 1; + list_item.DrawNoValue(value, attr); + } + else if (channel.get_trigger_source() > DQ_CHANNEL_TRIGGER_TR4) + list_item.DrawValueMax(value, attr, DQ_CHANNEL_SOURCE_CV4); + else + list_item.DrawDefault(value, attr); + } + break; + case DQ_CHANNEL_SETTING_PULSEWIDTH: + list_item.Draw_PW_Value(value, attr); + break; + default: + list_item.DrawDefault(value, attr); + break; + } + } + + if (dq_state.scale_editor.active()) + dq_state.scale_editor.Draw(); +} + +void DQ_handleButtonEvent(const UI::Event &event) { + + if (UI::EVENT_BUTTON_LONG_PRESS == event.type && OC::CONTROL_BUTTON_DOWN == event.control) + DQ_downButtonLong(); + + if (dq_state.scale_editor.active()) { + dq_state.scale_editor.HandleButtonEvent(event); + return; + } + + if (UI::EVENT_BUTTON_PRESS == event.type) { + switch (event.control) { + case OC::CONTROL_BUTTON_UP: + DQ_topButton(); + break; + case OC::CONTROL_BUTTON_DOWN: + DQ_lowerButton(); + break; + case OC::CONTROL_BUTTON_L: + DQ_leftButton(); + break; + case OC::CONTROL_BUTTON_R: + DQ_rightButton(); + break; + } + } else if (UI::EVENT_BUTTON_LONG_PRESS == event.type) { + if (OC::CONTROL_BUTTON_L == event.control) + DQ_leftButtonLong(); + } +} + +void DQ_handleEncoderEvent(const UI::Event &event) { + if (dq_state.scale_editor.active()) { + dq_state.scale_editor.HandleEncoderEvent(event); + return; + } + + if (OC::CONTROL_ENCODER_L == event.control) { + + int selected_channel = dq_state.selected_channel + event.value; + CONSTRAIN(selected_channel, 0, NUMCHANNELS - 0x1); + dq_state.selected_channel = selected_channel; + + DQ_QuantizerChannel &selected = dq_quantizer_channels[dq_state.selected_channel]; + selected.update_enabled_settings(); + dq_state.cursor.AdjustEnd(selected.num_enabled_settings() - 1); + //?? + dq_state.cursor.Scroll(0x0); + + } else if (OC::CONTROL_ENCODER_R == event.control) { + + DQ_QuantizerChannel &selected = dq_quantizer_channels[dq_state.selected_channel]; + + if (dq_state.editing()) { + + DQ_ChannelSetting setting = selected.enabled_setting_at(dq_state.cursor_pos()); + if (DQ_CHANNEL_SETTING_MASK1 != setting || DQ_CHANNEL_SETTING_MASK2 != setting || DQ_CHANNEL_SETTING_MASK3 != setting || DQ_CHANNEL_SETTING_MASK4 != setting) { + + int event_value = event.value; + + // hack disable internal sources when mode = continuous: + switch (setting) { + + case DQ_CHANNEL_SETTING_TRIGGER: + { + if (selected.get_trigger_source() == DQ_CHANNEL_TRIGGER_TR4 && selected.get_source() > DQ_CHANNEL_SOURCE_CV4 && event_value > 0) + event_value = 0x0; + } + break; + case DQ_CHANNEL_SETTING_SOURCE: + { + if (selected.get_source() == DQ_CHANNEL_SOURCE_CV4 && selected.get_trigger_source() > DQ_CHANNEL_TRIGGER_TR4 && event_value > 0) + event_value = 0x0; + } + break; + default: + break; + } + + if (selected.change_value(setting, event_value)) + selected.force_update(); + + switch (setting) { + case DQ_CHANNEL_SETTING_SCALE1: + case DQ_CHANNEL_SETTING_SCALE2: + case DQ_CHANNEL_SETTING_SCALE3: + case DQ_CHANNEL_SETTING_SCALE4: + case DQ_CHANNEL_SETTING_TRIGGER: + case DQ_CHANNEL_SETTING_SOURCE: + case DQ_CHANNEL_SETTING_AUX_OUTPUT: + selected.update_enabled_settings(); + dq_state.cursor.AdjustEnd(selected.num_enabled_settings() - 1); + break; + case DQ_CHANNEL_SETTING_SCALE_SEQ: + case DQ_CHANNEL_SETTING_SEQ_MODE: + selected.update_enabled_settings(); + dq_state.cursor.AdjustEnd(selected.num_enabled_settings() - 1); + selected.reset_scale(); + break; + default: + break; + } + } + } else { + dq_state.cursor.Scroll(event.value); + } + } +} + +void DQ_topButton() { + DQ_QuantizerChannel &selected = dq_quantizer_channels[dq_state.selected_channel]; + if (selected.change_value(DQ_CHANNEL_SETTING_OCTAVE, 1)) { + selected.force_update(); + } +} + +void DQ_lowerButton() { + DQ_QuantizerChannel &selected = dq_quantizer_channels[dq_state.selected_channel]; + if (selected.change_value(DQ_CHANNEL_SETTING_OCTAVE, -1)) { + selected.force_update(); + } +} + +void DQ_rightButton() { + DQ_QuantizerChannel &selected = dq_quantizer_channels[dq_state.selected_channel]; + switch (selected.enabled_setting_at(dq_state.cursor_pos())) { + case DQ_CHANNEL_SETTING_MASK1: + case DQ_CHANNEL_SETTING_MASK2: + case DQ_CHANNEL_SETTING_MASK3: + case DQ_CHANNEL_SETTING_MASK4: { + int scale = selected.get_scale(selected.get_scale_select()); + if (OC::Scales::SCALE_NONE != scale) { + dq_state.scale_editor.Edit(&selected, scale); + } + } + break; + default: + dq_state.cursor.toggle_editing(); + break; + } +} + +void DQ_leftButton() { + dq_state.selected_channel = (dq_state.selected_channel + 1) & 1u; + DQ_QuantizerChannel &selected = dq_quantizer_channels[dq_state.selected_channel]; + dq_state.cursor.AdjustEnd(selected.num_enabled_settings() - 1); +} + +void DQ_leftButtonLong() { + + // copy scale settings to all slots: + DQ_QuantizerChannel &selected_channel = dq_quantizer_channels[dq_state.selected_channel]; + int _slot = selected_channel.get_scale_select(); + int scale = selected_channel.get_scale(_slot); + int mask = selected_channel.get_mask(_slot); + int root = selected_channel.get_root(_slot); + int transpose = selected_channel.get_transpose(_slot); + + for (int i = 0; i < NUM_SCALE_SLOTS; ++i) { + for (int j = 0; j < NUMCHANNELS; ++j) + dq_quantizer_channels[j].set_scale_at_slot(scale, mask, root, transpose, i); + } +} + +void DQ_downButtonLong() { + // reset mask + DQ_QuantizerChannel &selected_channel = dq_quantizer_channels[dq_state.selected_channel]; + int scale_slot = selected_channel.get_scale_select(); + selected_channel.set_scale_at_slot(selected_channel.get_scale(scale_slot), 0xFFFF, selected_channel.get_root(scale_slot), selected_channel.get_transpose(scale_slot), scale_slot); +} + +int32_t dq_history[5]; +static const weegfx::coord_t dq_kBottom = 60; + +inline int32_t dq_render_pitch(int32_t pitch, weegfx::coord_t x, weegfx::coord_t width) { + CONSTRAIN(pitch, 0, 120 << 7); + int32_t octave = pitch / (12 << 7); + pitch -= (octave * 12 << 7); + graphics.drawHLine(x, dq_kBottom - ((pitch * 4) >> 7), width << 1); + return octave; +} + +void DQ_QuantizerChannel::RenderScreensaver(weegfx::coord_t start_x) const { + + // History + scrolling_history_.Read(dq_history); + weegfx::coord_t scroll_pos = (scrolling_history_.get_scroll_pos() * 6) >> 8; + + // Top: Show gate & CV (or register bits) + menu::DrawGateIndicator(start_x + 1, 2, getTriggerState()); + const DQ_ChannelSource source = get_source(); + + switch (source) { + case DQ_CHANNEL_SOURCE_TURING: + menu::DrawMask(start_x + 58, 1, get_shift_register(), get_turing_display_length()); + break; + default: { + graphics.setPixel(start_x + DQ_OFFSET_X - 16, 4); + int32_t cv = OC::ADC::value(static_cast(source)); + cv = (cv * 20 + 2047) >> 11; + if (cv < 0) + graphics.drawRect(start_x + DQ_OFFSET_X - 16 + cv, 6, -cv, 2); + else if (cv > 0) + graphics.drawRect(start_x + DQ_OFFSET_X - 16, 6, cv, 2); + else + graphics.drawRect(start_x + DQ_OFFSET_X - 16, 6, 1, 2); + } + break; + } + +#ifdef DQ_DEBUG_SCREENSAVER + graphics.drawVLinePattern(start_x + 56, 0, 64, 0x55); +#endif + + // Draw semitone intervals, 4px apart + weegfx::coord_t x = start_x + 56; + weegfx::coord_t y = dq_kBottom; + for (int i = 0; i < 12; ++i, y -= 4) + graphics.setPixel(x, y); + + x = start_x + 1; + dq_render_pitch(dq_history[0], x, scroll_pos); x += scroll_pos; + dq_render_pitch(dq_history[1], x, 6); x += 6; + dq_render_pitch(dq_history[2], x, 6); x += 6; + dq_render_pitch(dq_history[3], x, 6); x += 6; + + int32_t octave = dq_render_pitch(dq_history[4], x, 6 - scroll_pos); + graphics.drawBitmap8(start_x + 58, dq_kBottom - octave * 4 - 1, OC::kBitmapLoopMarkerW, OC::bitmap_loop_markers_8 + OC::kBitmapLoopMarkerW); +} + +void DQ_screensaver() { +#ifdef DQ_DEBUG_SCREENSAVER + debug::CycleMeasurement render_cycles; +#endif + + dq_quantizer_channels[0].RenderScreensaver(0); + dq_quantizer_channels[1].RenderScreensaver(64); + +#ifdef DQ_DEBUG_SCREENSAVER + graphics.drawHLine(0, menu::kMenuLineH, menu::kDisplayWidth); + uint32_t us = debug::cycles_to_us(render_cycles.read()); + graphics.setPrintPos(0, 32); + graphics.printf("%u", us); +#endif +} + +#endif // ENABLE_APP_METAQ diff --git a/software/o_c_REV/APP_ENIGMA.ino b/software/o_c_REV/APP_ENIGMA.ino index 069cac1ef..7572bbdf9 100644 --- a/software/o_c_REV/APP_ENIGMA.ino +++ b/software/o_c_REV/APP_ENIGMA.ino @@ -1158,6 +1158,7 @@ private: // The first 32 song steps @ 4 bytes each = 128 bytes // Four track settings @ 1 byte each = 4 bytes // Song length = 1 byte +// TOTAL EEPROM SIZE: 150 bytes #define ENIGMA_EEPROM_DATA {0,0,255,"St",NULL,settings::STORAGE_TYPE_U8}, #define ENIGMA_DO_THIRTY_TIMES(A) A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A SETTINGS_DECLARE(EnigmaTMWS, ENIGMA_SETTING_LAST) { @@ -1211,14 +1212,14 @@ void EnigmaTMWS_handleButtonEvent(const UI::Event &event) { // For left encoder, handle press and long press if (event.control == OC::CONTROL_BUTTON_L) { if (event.type == UI::EVENT_BUTTON_LONG_PRESS) EnigmaTMWS_instance.OnLeftButtonLongPress(); - else EnigmaTMWS_instance.OnLeftButtonPress(); + if (event.type == UI::EVENT_BUTTON_PRESS) EnigmaTMWS_instance.OnLeftButtonPress(); } // For right encoder, only handle press (long press is reserved) if (event.control == OC::CONTROL_BUTTON_R && event.type == UI::EVENT_BUTTON_PRESS) EnigmaTMWS_instance.OnRightButtonPress(); // For up button, handle only press (long press is reserved) - if (event.control == OC::CONTROL_BUTTON_UP) EnigmaTMWS_instance.OnUpButtonPress(); + if (event.control == OC::CONTROL_BUTTON_UP && event.type == UI::EVENT_BUTTON_PRESS) EnigmaTMWS_instance.OnUpButtonPress(); // For down button, handle press and long press if (event.control == OC::CONTROL_BUTTON_DOWN) { @@ -1236,4 +1237,4 @@ void EnigmaTMWS_handleEncoderEvent(const UI::Event &event) { } -#endif \ No newline at end of file +#endif diff --git a/software/o_c_REV/APP_ENVGEN.ino b/software/o_c_REV/APP_ENVGEN.ino new file mode 100644 index 000000000..af8fd750b --- /dev/null +++ b/software/o_c_REV/APP_ENVGEN.ino @@ -0,0 +1,1265 @@ +// +// Initial app implementation: Patrick Dowling (pld@gurkenkiste.com) +// Modifications by: Tim Churches (tim.churches@gmail.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// Quad enevelope generator app, based on the multistage envelope implementation +// from Peaks by Emilie Gillet (see peaks_multistage_envelope.h/cpp) + +#ifdef ENABLE_APP_PIQUED + +#include "OC_apps.h" +#include "OC_bitmaps.h" +#include "OC_digital_inputs.h" +#include "OC_menus.h" +#include "OC_strings.h" +#include "util/util_math.h" +#include "util/util_settings.h" +#include "peaks_multistage_envelope.h" +#include "bjorklund.h" +#include "OC_euclidean_mask_draw.h" + +// peaks::MultistageEnvelope allow setting of more parameters per stage, but +// that will involve more editing code, so keeping things simple for now +// with one value per stage. +// +// MultistageEnvelope maps times to lut_env_increments directly, so only 256 discrete values (no interpolation) +// Levels are 0-32767 to be positive on Peaks' bipolar output + +enum EnvelopeSettings { + ENV_SETTING_TYPE, + ENV_SETTING_SEG1_VALUE, + ENV_SETTING_SEG2_VALUE, + ENV_SETTING_SEG3_VALUE, + ENV_SETTING_SEG4_VALUE, + ENV_SETTING_TRIGGER_INPUT, + ENV_SETTING_TRIGGER_DELAY_MODE, + ENV_SETTING_TRIGGER_DELAY_COUNT, + ENV_SETTING_TRIGGER_DELAY_MILLISECONDS, + ENV_SETTING_TRIGGER_DELAY_SECONDS, + ENV_SETTING_EUCLIDEAN_LENGTH, + ENV_SETTING_EUCLIDEAN_FILL, + ENV_SETTING_EUCLIDEAN_OFFSET, + ENV_SETTING_EUCLIDEAN_RESET_INPUT, + ENV_SETTING_EUCLIDEAN_RESET_CLOCK_DIV, + ENV_SETTING_CV1, + ENV_SETTING_CV2, + ENV_SETTING_CV3, + ENV_SETTING_CV4, + ENV_SETTING_ATTACK_RESET_BEHAVIOUR, + ENV_SETTING_ATTACK_FALLING_GATE_BEHAVIOUR, + ENV_SETTING_DECAY_RELEASE_RESET_BEHAVIOUR, + ENV_SETTING_GATE_HIGH, + ENV_SETTING_ATTACK_SHAPE, + ENV_SETTING_DECAY_SHAPE, + ENV_SETTING_RELEASE_SHAPE, + ENV_SETTING_ATTACK_TIME_MULTIPLIER, + ENV_SETTING_DECAY_TIME_MULTIPLIER, + ENV_SETTING_RELEASE_TIME_MULTIPLIER, + ENV_SETTING_AMPLITUDE, + ENV_SETTING_SAMPLED_AMPLITUDE, + ENV_SETTING_MAX_LOOPS, + ENV_SETTING_INVERTED, + ENV_SETTING_LAST +}; + +enum CVMapping { + CV_MAPPING_NONE, + CV_MAPPING_SEG1, + CV_MAPPING_SEG2, + CV_MAPPING_SEG3, + CV_MAPPING_SEG4, + CV_MAPPING_ADR, + CV_MAPPING_EUCLIDEAN_LENGTH, + CV_MAPPING_EUCLIDEAN_FILL, + CV_MAPPING_EUCLIDEAN_OFFSET, + CV_MAPPING_DELAY_MSEC, + CV_MAPPING_AMPLITUDE, + CV_MAPPING_MAX_LOOPS, + CV_MAPPING_LAST +}; + +enum EnvelopeType { + ENV_TYPE_AD, + ENV_TYPE_ADSR, + ENV_TYPE_ADR, + ENV_TYPE_AR, + ENV_TYPE_ADSAR, + ENV_TYPE_ADAR, + ENV_TYPE_ADL2, + ENV_TYPE_ADRL3, + ENV_TYPE_ADL2R, + ENV_TYPE_ADAL2R, + ENV_TYPE_ADARL4, + ENV_TYPE_LAST, ENV_TYPE_FIRST = ENV_TYPE_AD +}; + +enum TriggerDelayMode { + TRIGGER_DELAY_OFF, + TRIGGER_DELAY_QUEUE, // Queue up to kMaxDelayedTriggers delays, additional triggers ignored + TRIGGER_DELAY_RING, // Queue up to kMaxDelayedTriggers delays, additional triggers overwrite oldest + TRIGGER_DELAY_LAST +}; + +// With only one type, the U4 setting size still works. Each of the these maps to 3 values in the +// setting, 1 for each channel other than the current. +// Ordering here ideally maps to peaks::EnvStateBitMask shifts +enum IntTriggerType { + INT_TRIGGER_EOC, + INT_TRIGGER_LAST +}; + +inline int TriggerSettingToChannel(int setting_value) __attribute__((always_inline)); +inline int TriggerSettingToChannel(int setting_value) { + return (setting_value - OC::DIGITAL_INPUT_LAST) / INT_TRIGGER_LAST; +} + +static inline IntTriggerType TriggerSettingToType(int setting_value, int channel) __attribute__((always_inline)); +static inline IntTriggerType TriggerSettingToType(int setting_value, int channel) { + return static_cast((setting_value - OC::DIGITAL_INPUT_LAST) - channel * INT_TRIGGER_LAST); +} + +namespace menu = OC::menu; + +class EnvelopeGenerator : public settings::SettingsBase { +public: + + static constexpr int kMaxSegments = 4; + static constexpr int kEuclideanParams = 3; + static constexpr int kDelayParams = 1; + static constexpr int kAmplitudeParams = 2; // incremented to 2 to cover the MAX_LOOPS parameter + static constexpr size_t kMaxDelayedTriggers = 24; + + struct DelayedTrigger { + uint32_t delay; + uint32_t time_left; + + inline void Activate(uint32_t t) { + delay = time_left = t; + } + + inline void Reset() { + delay = time_left = 0; + } + }; + + void Init(OC::DigitalInput default_trigger); + + EnvelopeType get_type() const { + return static_cast(values_[ENV_SETTING_TYPE]); + } + + int get_trigger_input() const { + return values_[ENV_SETTING_TRIGGER_INPUT]; + } + + int32_t get_trigger_delay_ms() const { + return 1000U * values_[ENV_SETTING_TRIGGER_DELAY_SECONDS] + values_[ENV_SETTING_TRIGGER_DELAY_MILLISECONDS] ; + } + + TriggerDelayMode get_trigger_delay_mode() const { + return static_cast(values_[ENV_SETTING_TRIGGER_DELAY_MODE]); + } + + peaks::EnvResetBehaviour get_attack_reset_behaviour() const { + return static_cast(values_[ENV_SETTING_ATTACK_RESET_BEHAVIOUR]); + } + + peaks::EnvFallingGateBehaviour get_attack_falling_gate_behaviour() const { + return static_cast(values_[ENV_SETTING_ATTACK_FALLING_GATE_BEHAVIOUR]); + } + + peaks::EnvResetBehaviour get_decay_release_reset_behaviour() const { + return static_cast(values_[ENV_SETTING_DECAY_RELEASE_RESET_BEHAVIOUR]); + } + + uint8_t get_euclidean_length() const { + return values_[ENV_SETTING_EUCLIDEAN_LENGTH]; + } + + uint8_t get_euclidean_fill() const { + return values_[ENV_SETTING_EUCLIDEAN_FILL]; + } + + uint8_t get_euclidean_offset() const { + return values_[ENV_SETTING_EUCLIDEAN_OFFSET]; + } + + uint8_t get_euclidean_reset_trigger_input() const { + return values_[ENV_SETTING_EUCLIDEAN_RESET_INPUT]; + } + + uint8_t get_euclidean_reset_clock_div() const { + return values_[ENV_SETTING_EUCLIDEAN_RESET_CLOCK_DIV]; + } + + uint32_t get_euclidean_counter() const { + return euclidean_counter_; + } + + uint16_t get_amplitude() const { + return values_[ENV_SETTING_AMPLITUDE] << 9 ; + } + + bool is_amplitude_sampled() const { + return static_cast(values_[ENV_SETTING_SAMPLED_AMPLITUDE]); + } + + uint16_t get_max_loops() const { + return values_[ENV_SETTING_MAX_LOOPS] << 9 ; + } + + bool is_inverted() const { + return static_cast(values_[ENV_SETTING_INVERTED]); + } + + uint8_t get_s_euclidean_length() const { + return s_euclidean_length_; + } + + uint8_t get_s_euclidean_fill() const { + return s_euclidean_fill_; + } + + uint8_t get_s_euclidean_offset() const { + return s_euclidean_offset_; + } + + uint32_t get_trigger_delay_count() const { + return values_[ENV_SETTING_TRIGGER_DELAY_COUNT]; + } + + CVMapping get_cv1_mapping() const { + return static_cast(values_[ENV_SETTING_CV1]); + } + + CVMapping get_cv2_mapping() const { + return static_cast(values_[ENV_SETTING_CV2]); + } + + CVMapping get_cv3_mapping() const { + return static_cast(values_[ENV_SETTING_CV3]); + } + + CVMapping get_cv4_mapping() const { + return static_cast(values_[ENV_SETTING_CV4]); + } + +// bool get_hard_reset() const { +// return values_[ENV_SETTING_HARD_RESET]; +// } + + bool get_gate_high() const { + return values_[ENV_SETTING_GATE_HIGH]; + } + + peaks::EnvelopeShape get_attack_shape() const { + return static_cast(values_[ENV_SETTING_ATTACK_SHAPE]); + } + + peaks::EnvelopeShape get_decay_shape() const { + return static_cast(values_[ENV_SETTING_DECAY_SHAPE]); + } + + peaks::EnvelopeShape get_release_shape() const { + return static_cast(values_[ENV_SETTING_RELEASE_SHAPE]); + } + + uint16_t get_attack_time_multiplier() const { + return static_cast(values_[ENV_SETTING_ATTACK_TIME_MULTIPLIER]); + } + + uint16_t get_decay_time_multiplier() const { + return static_cast(values_[ENV_SETTING_DECAY_TIME_MULTIPLIER]); + } + + uint16_t get_release_time_multiplier() const { + return static_cast(values_[ENV_SETTING_RELEASE_TIME_MULTIPLIER]); + } + + // Utils + uint16_t get_segment_value(int segment) const { + return values_[ENV_SETTING_SEG1_VALUE + segment]; + } + + int num_editable_segments() const { + switch (get_type()) { + case ENV_TYPE_AD: + case ENV_TYPE_AR: + case ENV_TYPE_ADL2: + return 2; + case ENV_TYPE_ADR: + case ENV_TYPE_ADSR: + case ENV_TYPE_ADSAR: + case ENV_TYPE_ADAR: + case ENV_TYPE_ADRL3: + case ENV_TYPE_ADL2R: + case ENV_TYPE_ADARL4: + case ENV_TYPE_ADAL2R: + return 4; + default: break; + } + return 0; + } + + inline void apply_cv_mapping(EnvelopeSettings cv_setting, const int32_t cvs[ADC_CHANNEL_LAST], int32_t segments[CV_MAPPING_LAST]) { + // segments is indexed directly with CVMapping enum values + int mapping = values_[cv_setting]; + switch (mapping) { + case CV_MAPPING_SEG1: + case CV_MAPPING_SEG2: + case CV_MAPPING_SEG3: + case CV_MAPPING_SEG4: + segments[mapping] += (cvs[cv_setting - ENV_SETTING_CV1] * 65536) >> 12; + break; + case CV_MAPPING_ADR: + segments[CV_MAPPING_SEG1] += (cvs[cv_setting - ENV_SETTING_CV1] * 65536) >> 12; + segments[CV_MAPPING_SEG2] += (cvs[cv_setting - ENV_SETTING_CV1] * 65536) >> 12; + segments[CV_MAPPING_SEG4] += (cvs[cv_setting - ENV_SETTING_CV1] * 65536) >> 12; + break; + case CV_MAPPING_EUCLIDEAN_LENGTH: + case CV_MAPPING_EUCLIDEAN_FILL: + case CV_MAPPING_EUCLIDEAN_OFFSET: + segments[mapping] += cvs[cv_setting - ENV_SETTING_CV1] >> 6; + break; + case CV_MAPPING_DELAY_MSEC: + segments[mapping] += cvs[cv_setting - ENV_SETTING_CV1] >> 2; + break; + case CV_MAPPING_AMPLITUDE: + segments[mapping] += cvs[cv_setting - ENV_SETTING_CV1] << 5 ; + break; + case CV_MAPPING_MAX_LOOPS: + segments[mapping] += cvs[cv_setting - ENV_SETTING_CV1] << 2 ; + break; + default: + break; + } + } + + int num_enabled_settings() const { + return num_enabled_settings_; + } + + EnvelopeSettings enabled_setting_at(int index) const { + return enabled_settings_[index]; + } + + void update_enabled_settings() { + EnvelopeSettings *settings = enabled_settings_; + + *settings++ = ENV_SETTING_TYPE; + *settings++ = ENV_SETTING_TRIGGER_INPUT; + *settings++ = ENV_SETTING_TRIGGER_DELAY_MODE; + if (get_trigger_delay_mode()) { + *settings++ = ENV_SETTING_TRIGGER_DELAY_COUNT; + *settings++ = ENV_SETTING_TRIGGER_DELAY_MILLISECONDS; + *settings++ = ENV_SETTING_TRIGGER_DELAY_SECONDS; + } + + *settings++ = ENV_SETTING_EUCLIDEAN_LENGTH; + if (get_euclidean_length()) { + //*settings++ = ENV_SETTING_EUCLIDEAN_FILL; + *settings++ = ENV_SETTING_EUCLIDEAN_OFFSET; + *settings++ = ENV_SETTING_EUCLIDEAN_RESET_INPUT; + *settings++ = ENV_SETTING_EUCLIDEAN_RESET_CLOCK_DIV; + } + + *settings++ = ENV_SETTING_ATTACK_SHAPE; + *settings++ = ENV_SETTING_DECAY_SHAPE; + *settings++ = ENV_SETTING_RELEASE_SHAPE; + + *settings++ = ENV_SETTING_ATTACK_TIME_MULTIPLIER; + *settings++ = ENV_SETTING_DECAY_TIME_MULTIPLIER; + *settings++ = ENV_SETTING_RELEASE_TIME_MULTIPLIER; + + *settings++ = ENV_SETTING_CV1; + *settings++ = ENV_SETTING_CV2; + *settings++ = ENV_SETTING_CV3; + *settings++ = ENV_SETTING_CV4; + *settings++ = ENV_SETTING_ATTACK_RESET_BEHAVIOUR; + *settings++ = ENV_SETTING_ATTACK_FALLING_GATE_BEHAVIOUR; + *settings++ = ENV_SETTING_DECAY_RELEASE_RESET_BEHAVIOUR; + *settings++ = ENV_SETTING_GATE_HIGH; + *settings++ = ENV_SETTING_AMPLITUDE; + *settings++ = ENV_SETTING_SAMPLED_AMPLITUDE; + *settings++ = ENV_SETTING_MAX_LOOPS; + *settings++ = ENV_SETTING_INVERTED; + + num_enabled_settings_ = settings - enabled_settings_; + } + + static bool indentSetting(EnvelopeSettings setting) { + switch (setting) { + case ENV_SETTING_TRIGGER_DELAY_COUNT: + case ENV_SETTING_TRIGGER_DELAY_SECONDS: + case ENV_SETTING_TRIGGER_DELAY_MILLISECONDS: + case ENV_SETTING_EUCLIDEAN_FILL: + case ENV_SETTING_EUCLIDEAN_OFFSET: + case ENV_SETTING_EUCLIDEAN_RESET_INPUT: + case ENV_SETTING_EUCLIDEAN_RESET_CLOCK_DIV: + return true; + default: + break; + } + return false; + } + + template + void Update(uint32_t triggers, uint32_t internal_trigger_mask, const int32_t cvs[ADC_CHANNEL_LAST]) { + int32_t s[CV_MAPPING_LAST]; + s[CV_MAPPING_NONE] = 0; // unused, but needs a placeholder to align with enum CVMapping + s[CV_MAPPING_SEG1] = SCALE8_16(static_cast(get_segment_value(0))); + s[CV_MAPPING_SEG2] = SCALE8_16(static_cast(get_segment_value(1))); + s[CV_MAPPING_SEG3] = SCALE8_16(static_cast(get_segment_value(2))); + s[CV_MAPPING_SEG4] = SCALE8_16(static_cast(get_segment_value(3))); + s[CV_MAPPING_ADR] = 0; // unused, but needs a placeholder to align with enum CVMapping + s[CV_MAPPING_EUCLIDEAN_LENGTH] = static_cast(get_euclidean_length()); + s[CV_MAPPING_EUCLIDEAN_FILL] = static_cast(get_euclidean_fill()); + s[CV_MAPPING_EUCLIDEAN_OFFSET] = static_cast(get_euclidean_offset()); + s[CV_MAPPING_DELAY_MSEC] = get_trigger_delay_ms(); + s[CV_MAPPING_AMPLITUDE] = get_amplitude(); + s[CV_MAPPING_MAX_LOOPS] = get_max_loops(); + + apply_cv_mapping(ENV_SETTING_CV1, cvs, s); + apply_cv_mapping(ENV_SETTING_CV2, cvs, s); + apply_cv_mapping(ENV_SETTING_CV3, cvs, s); + apply_cv_mapping(ENV_SETTING_CV4, cvs, s); + + s[CV_MAPPING_SEG1] = USAT16(s[CV_MAPPING_SEG1]); + s[CV_MAPPING_SEG2] = USAT16(s[CV_MAPPING_SEG2]); + s[CV_MAPPING_SEG3] = USAT16(s[CV_MAPPING_SEG3]); + s[CV_MAPPING_SEG4] = USAT16(s[CV_MAPPING_SEG4]); + CONSTRAIN(s[CV_MAPPING_EUCLIDEAN_LENGTH], 0, 31); + CONSTRAIN(s[CV_MAPPING_EUCLIDEAN_FILL], 0, 32); + CONSTRAIN(s[CV_MAPPING_EUCLIDEAN_OFFSET], 0, 32); + CONSTRAIN(s[CV_MAPPING_DELAY_MSEC], 0, 65535); + CONSTRAIN(s[CV_MAPPING_AMPLITUDE], 0, 65535); + CONSTRAIN(s[CV_MAPPING_MAX_LOOPS], 0, 65535); + + EnvelopeType type = get_type(); + switch (type) { + case ENV_TYPE_AD: env_.set_ad(s[CV_MAPPING_SEG1], s[CV_MAPPING_SEG2], 0, 0); break; + case ENV_TYPE_ADSR: env_.set_adsr(s[CV_MAPPING_SEG1], s[CV_MAPPING_SEG2], s[CV_MAPPING_SEG3]>>1, s[CV_MAPPING_SEG4]); break; + case ENV_TYPE_ADR: env_.set_adr(s[CV_MAPPING_SEG1], s[CV_MAPPING_SEG2], s[CV_MAPPING_SEG3]>>1, s[CV_MAPPING_SEG4], 0, 0 ); break; + case ENV_TYPE_AR: env_.set_ar(s[CV_MAPPING_SEG1], s[CV_MAPPING_SEG2]); break; + case ENV_TYPE_ADSAR: env_.set_adsar(s[CV_MAPPING_SEG1], s[CV_MAPPING_SEG2], s[CV_MAPPING_SEG3]>>1, s[CV_MAPPING_SEG4]); break; + case ENV_TYPE_ADAR: env_.set_adar(s[CV_MAPPING_SEG1], s[CV_MAPPING_SEG2], s[CV_MAPPING_SEG3]>>1, s[CV_MAPPING_SEG4], 0, 0); break; + case ENV_TYPE_ADL2: env_.set_ad(s[CV_MAPPING_SEG1], s[CV_MAPPING_SEG2], 0, 2); break; + case ENV_TYPE_ADRL3: env_.set_adr(s[CV_MAPPING_SEG1], s[CV_MAPPING_SEG2], s[CV_MAPPING_SEG3]>>1, s[CV_MAPPING_SEG4], 0, 3); break; + case ENV_TYPE_ADL2R: env_.set_adr(s[CV_MAPPING_SEG1], s[CV_MAPPING_SEG2], s[CV_MAPPING_SEG3]>>1, s[CV_MAPPING_SEG4], 0, 2); break; + case ENV_TYPE_ADARL4: env_.set_adar(s[CV_MAPPING_SEG1], s[CV_MAPPING_SEG2], s[CV_MAPPING_SEG3]>>1, s[CV_MAPPING_SEG4], 0, 4); break; + case ENV_TYPE_ADAL2R: env_.set_adar(s[CV_MAPPING_SEG1], s[CV_MAPPING_SEG2], s[CV_MAPPING_SEG3]>>1, s[CV_MAPPING_SEG4], 1, 3); break; // was 2, 4 + default: + break; + } + + // set the amplitude + env_.set_amplitude(s[CV_MAPPING_AMPLITUDE], is_amplitude_sampled()) ; + + if (type != last_type_) { + last_type_ = type; + env_.reset(); + } + + // set the specified reset behaviours + env_.set_attack_reset_behaviour(get_attack_reset_behaviour()); + env_.set_attack_falling_gate_behaviour(get_attack_falling_gate_behaviour()); + env_.set_decay_release_reset_behaviour(get_decay_release_reset_behaviour()); + + // set the envelope segment shapes + env_.set_attack_shape(get_attack_shape()); + env_.set_decay_shape(get_decay_shape()); + env_.set_release_shape(get_release_shape()); + + // set the envelope segment time multipliers + env_.set_attack_time_multiplier(get_attack_time_multiplier()); + env_.set_decay_time_multiplier(get_decay_time_multiplier()); + env_.set_release_time_multiplier(get_release_time_multiplier()); + + // set the looping envelope maximum number of loops + env_.set_max_loops(s[CV_MAPPING_MAX_LOOPS]); + + int trigger_input = get_trigger_input(); + bool triggered = false; + bool gate_raised = false; + if (trigger_input < OC::DIGITAL_INPUT_LAST) { + triggered = triggers & DIGITAL_INPUT_MASK(trigger_input); + gate_raised = OC::DigitalInputs::read_immediate(static_cast(trigger_input)); + } else { + const int trigger_channel = TriggerSettingToChannel(trigger_input); + const IntTriggerType trigger_type = TriggerSettingToType(trigger_input, trigger_channel); + + triggered = (internal_trigger_mask >> (trigger_setting_to_channel_index(trigger_channel) * 8)) & (0x1 << trigger_type); + gate_raised = triggered; + } + + trigger_display_.Update(1, triggered || gate_raised_); + + if (triggered) ++euclidean_counter_; + uint8_t euclidean_length = static_cast(s[CV_MAPPING_EUCLIDEAN_LENGTH]); + uint8_t euclidean_fill = static_cast(s[CV_MAPPING_EUCLIDEAN_FILL]); + uint8_t euclidean_offset = static_cast(s[CV_MAPPING_EUCLIDEAN_OFFSET]); + + // Process Euclidean pattern reset + uint8_t euclidean_reset_trigger_input = get_euclidean_reset_trigger_input(); + if (euclidean_reset_trigger_input) { + if (triggers & DIGITAL_INPUT_MASK(static_cast(euclidean_reset_trigger_input - 1))) { + ++euclidean_reset_counter_; + if (euclidean_reset_counter_ >= get_euclidean_reset_clock_div()) { + euclidean_counter_ = 0; + euclidean_reset_counter_= 0; + } + } + } + + if (triggered && get_euclidean_length() && !EuclideanFilter(euclidean_length, euclidean_fill, euclidean_offset, euclidean_counter_)) { + triggered = false; + } + + s_euclidean_length_ = euclidean_length; + s_euclidean_fill_ = euclidean_fill; + s_euclidean_offset_ = euclidean_offset; + + if (triggered) { + TriggerDelayMode delay_mode = get_trigger_delay_mode(); + // uint32_t delay = get_trigger_delay_ms() * 1000U; + uint32_t delay = static_cast(s[CV_MAPPING_DELAY_MSEC] * 1000U); + if (delay_mode && delay) { + triggered = false; + if (TRIGGER_DELAY_QUEUE == delay_mode) { + if (delayed_triggers_free_ < get_trigger_delay_count()) + delayed_triggers_[delayed_triggers_free_].Activate(delay); + } else { // TRIGGER_DELAY_RING + // Assume these are mostly in order, so the "next" is also the oldest + if (delayed_triggers_free_ < get_trigger_delay_count()) + delayed_triggers_[delayed_triggers_free_].Activate(delay); + else + delayed_triggers_[delayed_triggers_next_].Activate(delay); + } + } + } + + if (DelayedTriggers()) + triggered = true; + + uint8_t gate_state = 0; + if (triggered) + gate_state |= peaks::CONTROL_GATE_RISING; + + if (gate_raised || get_gate_high()) + gate_state |= peaks::CONTROL_GATE; + else if (gate_raised_) + gate_state |= peaks::CONTROL_GATE_FALLING; + gate_raised_ = gate_raised; + + uint32_t value = env_.ProcessSingleSample(gate_state); // 0 to 32767 + if (is_inverted()) value = 32767 - value; + const int max_val = OC::DAC::MAX_VALUE; + + // Scale range and offset +#ifdef VOR + // Full range for Plum Audio + const uint32_t offset = OC::DAC::get_octave_offset(dac_channel, -OC::DAC::kOctaveZero); +#else + // Regular O_C settles to 0V + const uint32_t offset = OC::DAC::get_zero_offset(dac_channel); +#endif + + // scale value + value = offset + (value * (max_val - offset) / 32767); + + OC::DAC::set(value); + } + + uint16_t RenderPreview(int16_t *values, uint16_t *segment_start_points, uint16_t *loop_points, uint16_t ¤t_phase) const { + return env_.RenderPreview(values, segment_start_points, loop_points, current_phase); + } + + uint16_t RenderFastPreview(int16_t *values) const { + return env_.RenderFastPreview(values); + } + + uint8_t getTriggerState() const { + return trigger_display_.getState(); + } + + inline void get_next_trigger(DelayedTrigger &trigger) const { + trigger = delayed_triggers_[delayed_triggers_next_]; + } + +#ifdef ENVGEN_DEBUG + inline uint16_t get_amplitude_value() { + return(env_.get_amplitude_value()) ; + } + + inline uint16_t get_sampled_amplitude_value() { + return(env_.get_sampled_amplitude_value()) ; + } + + inline bool get_is_amplitude_sampled() { + return(env_.get_is_amplitude_sampled()) ; + } +#endif + + inline int trigger_setting_to_channel_index(int s) const { + return s < channel_index_ ? s : s + 1; + } + + uint32_t internal_trigger_mask() const { + return env_.get_state_mask(); + } + +private: + + int channel_index_; + + peaks::MultistageEnvelope env_; + EnvelopeType last_type_; + bool gate_raised_; + uint32_t euclidean_counter_; + uint32_t euclidean_reset_counter_; + + // debug/live-view only + uint8_t s_euclidean_length_; + uint8_t s_euclidean_fill_; + uint8_t s_euclidean_offset_; + + DelayedTrigger delayed_triggers_[kMaxDelayedTriggers]; + size_t delayed_triggers_free_; + size_t delayed_triggers_next_; + + int num_enabled_settings_; + EnvelopeSettings enabled_settings_[ENV_SETTING_LAST]; + + OC::DigitalInputDisplay trigger_display_; + + bool DelayedTriggers() { + bool triggered = false; + + delayed_triggers_free_ = kMaxDelayedTriggers; + delayed_triggers_next_ = 0; + uint32_t min_time_left = -1; + + size_t i = kMaxDelayedTriggers; + while (i--) { + DelayedTrigger &trigger = delayed_triggers_[i]; + uint32_t time_left = trigger.time_left; + if (time_left) { + if (time_left > OC_CORE_TIMER_RATE) { + time_left -= OC_CORE_TIMER_RATE; + if (time_left < min_time_left) { + min_time_left = time_left; + delayed_triggers_next_ = i; + } + trigger.time_left = time_left; + } else { + trigger.Reset(); + delayed_triggers_free_ = i; + triggered = true; + } + } else { + delayed_triggers_free_ = i; + } + } + + return triggered; + } +}; + +void EnvelopeGenerator::Init(OC::DigitalInput default_trigger) { + InitDefaults(); + apply_value(ENV_SETTING_TRIGGER_INPUT, default_trigger); + env_.Init(); + channel_index_ = default_trigger; + last_type_ = ENV_TYPE_LAST; + gate_raised_ = false; + euclidean_counter_ = 0; + euclidean_reset_counter_ = 0; + + memset(delayed_triggers_, 0, sizeof(delayed_triggers_)); + delayed_triggers_free_ = delayed_triggers_next_ = 0; + + trigger_display_.Init(); + + update_enabled_settings(); +} + +const char* const envelope_types[ENV_TYPE_LAST] = { + "AD", "ADSR", "ADR", "ASR", "ADSAR", "ADAR", "ADL2", "ADRL3", "ADL2R", "ADAL2R", "ADARL4" +}; + +const char* const segment_names[] = { + "Attack", "Decay", "Sustain/Level", "Release" +}; + +const char* const cv_mapping_names[CV_MAPPING_LAST] = { + "None", "Att", "Dec", "Sus", "Rel", "ADR", "Eleng", "Efill", "Eoffs", "Delay", "Ampl", "Loops" +}; + +const char* const trigger_delay_modes[TRIGGER_DELAY_LAST] = { + "Off", "Queue", "Ring" +}; + +const char* const euclidean_lengths[] = { + "Off", " 2", " 3", " 4", " 5", " 6", " 7", " 8", " 9", " 10", + " 11", " 12", " 13", " 14", " 15", " 16", " 17", " 18", " 19", " 20", + " 21", " 22", " 23", " 24", " 25", " 26", " 27", " 28", " 29", " 30", + " 31", " 32", +}; + +const char* const time_multipliers[] = { + "1", " 2", " 4", " 8", " 16", " 32", " 64", " 128", " 256", " 512", "1024", "2048", "4096", "8192" +}; + +const char* const internal_trigger_types[INT_TRIGGER_LAST] = { + "EOC", // Keep length == 3 +}; + +// TOTAL EEPROM SIZE: 4 * 30 bytes +SETTINGS_DECLARE(EnvelopeGenerator, ENV_SETTING_LAST) { + { ENV_TYPE_AD, ENV_TYPE_FIRST, ENV_TYPE_LAST-1, "TYPE", envelope_types, settings::STORAGE_TYPE_U8 }, + { 128, 0, 255, "S1", NULL, settings::STORAGE_TYPE_U16 }, // u16 in case resolution proves insufficent + { 128, 0, 255, "S2", NULL, settings::STORAGE_TYPE_U16 }, + { 128, 0, 255, "S3", NULL, settings::STORAGE_TYPE_U16 }, + { 128, 0, 255, "S4", NULL, settings::STORAGE_TYPE_U16 }, + { OC::DIGITAL_INPUT_1, OC::DIGITAL_INPUT_1, OC::DIGITAL_INPUT_4 + 3 * INT_TRIGGER_LAST, "Trigger input", OC::Strings::trigger_input_names, settings::STORAGE_TYPE_U4 }, + { TRIGGER_DELAY_OFF, TRIGGER_DELAY_OFF, TRIGGER_DELAY_LAST - 1, "Tr delay mode", trigger_delay_modes, settings::STORAGE_TYPE_U4 }, + { 1, 1, EnvelopeGenerator::kMaxDelayedTriggers, "Tr delay count", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 999, "Tr delay msecs", NULL, settings::STORAGE_TYPE_U16 }, + { 0, 0, 64, "Tr delay secs", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 31, "Eucl length", euclidean_lengths, settings::STORAGE_TYPE_U8 }, + { 1, 0, 32, "Fill", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 32, "Offset", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 4, "Eucl reset", OC::Strings::trigger_input_names_none, settings::STORAGE_TYPE_U8 }, + { 1, 1, 255, "Eucl reset div", NULL, settings::STORAGE_TYPE_U8 }, + { CV_MAPPING_NONE, CV_MAPPING_NONE, CV_MAPPING_LAST - 1, "CV1 -> ", cv_mapping_names, settings::STORAGE_TYPE_U4 }, + { CV_MAPPING_NONE, CV_MAPPING_NONE, CV_MAPPING_LAST - 1, "CV2 -> ", cv_mapping_names, settings::STORAGE_TYPE_U4 }, + { CV_MAPPING_NONE, CV_MAPPING_NONE, CV_MAPPING_LAST - 1, "CV3 -> ", cv_mapping_names, settings::STORAGE_TYPE_U4 }, + { CV_MAPPING_NONE, CV_MAPPING_NONE, CV_MAPPING_LAST - 1, "CV4 -> ", cv_mapping_names, settings::STORAGE_TYPE_U4 }, + { peaks::RESET_BEHAVIOUR_NULL, peaks::RESET_BEHAVIOUR_NULL, peaks::RESET_BEHAVIOUR_LAST - 1, "Attack reset", OC::Strings::reset_behaviours, settings::STORAGE_TYPE_U4 }, + { peaks::FALLING_GATE_BEHAVIOUR_IGNORE, peaks::FALLING_GATE_BEHAVIOUR_IGNORE, peaks::FALLING_GATE_BEHAVIOUR_LAST - 1, "Att fall gt", OC::Strings::falling_gate_behaviours, settings::STORAGE_TYPE_U8 }, + { peaks::RESET_BEHAVIOUR_SEGMENT_PHASE, peaks::RESET_BEHAVIOUR_NULL, peaks::RESET_BEHAVIOUR_LAST - 1, "DecRel reset", OC::Strings::reset_behaviours, settings::STORAGE_TYPE_U4 }, + { 0, 0, 1, "Gate high", OC::Strings::no_yes, settings::STORAGE_TYPE_U4 }, + { peaks::ENV_SHAPE_QUARTIC, peaks::ENV_SHAPE_LINEAR, peaks::ENV_SHAPE_LAST - 1, "Attack shape", OC::Strings::envelope_shapes, settings::STORAGE_TYPE_U4 }, + { peaks::ENV_SHAPE_EXPONENTIAL, peaks::ENV_SHAPE_LINEAR, peaks::ENV_SHAPE_LAST - 1, "Decay shape", OC::Strings::envelope_shapes, settings::STORAGE_TYPE_U4 }, + { peaks::ENV_SHAPE_EXPONENTIAL, peaks::ENV_SHAPE_LINEAR, peaks::ENV_SHAPE_LAST - 1, "Release shape", OC::Strings::envelope_shapes, settings::STORAGE_TYPE_U4 }, + { 0, 0, 13, "Attack mult", time_multipliers, settings::STORAGE_TYPE_U4 }, + { 0, 0, 13, "Decay mult", time_multipliers, settings::STORAGE_TYPE_U4 }, + {0, 0, 13, "Release mult", time_multipliers, settings::STORAGE_TYPE_U4 }, + {127, 0, 127, "Amplitude", NULL, settings::STORAGE_TYPE_U8 }, + {0, 0, 1, "Sampled Ampl", OC::Strings::no_yes, settings::STORAGE_TYPE_U4 }, + {0, 0, 127, "Max loops", NULL, settings::STORAGE_TYPE_U8 }, + {0, 0, 1, "Inverted", OC::Strings::no_yes, settings::STORAGE_TYPE_U8 }, +}; + +class QuadEnvelopeGenerator { +public: + static constexpr int32_t kCvSmoothing = 16; + + void Init() { + int input = OC::DIGITAL_INPUT_1; + for (auto &env : envelopes_) { + env.Init(static_cast(input)); + ++input; + } + + ui.edit_mode = MODE_EDIT_SEGMENTS; + ui.selected_channel = 0; + ui.selected_segment = 0; + ui.segment_editing = false; + ui.cursor.Init(0, envelopes_[0].num_enabled_settings() - 1); + ui.euclidean_mask_draw.Init(); + ui.euclidean_edit_length = false; + } + + void ISR() { + cv1.push(OC::ADC::value()); + cv2.push(OC::ADC::value()); + cv3.push(OC::ADC::value()); + cv4.push(OC::ADC::value()); + + const int32_t cvs[ADC_CHANNEL_LAST] = { cv1.value(), cv2.value(), cv3.value(), cv4.value() }; + uint32_t triggers = OC::DigitalInputs::clocked(); + + uint32_t internal_trigger_mask = + envelopes_[0].internal_trigger_mask() | + envelopes_[1].internal_trigger_mask() << 8 | + envelopes_[2].internal_trigger_mask() << 16 | + envelopes_[3].internal_trigger_mask() << 24; + + envelopes_[0].Update(triggers, internal_trigger_mask, cvs); + envelopes_[1].Update(triggers, internal_trigger_mask, cvs); + envelopes_[2].Update(triggers, internal_trigger_mask, cvs); + envelopes_[3].Update(triggers, internal_trigger_mask, cvs); + } + + bool euclidean_edit_active() const { + return + ui.cursor.editing() && + ENV_SETTING_EUCLIDEAN_OFFSET == selected().enabled_setting_at(ui.cursor.cursor_pos()); + } + + enum EnvEditMode { + MODE_EDIT_SEGMENTS, + MODE_EDIT_SETTINGS + }; + + struct { + EnvEditMode edit_mode; + + int selected_channel; + int selected_segment; + bool segment_editing; + + menu::ScreenCursor cursor; + OC::EuclideanMaskDraw euclidean_mask_draw; + bool euclidean_edit_length; + } ui; + + EnvelopeGenerator &selected() { + return envelopes_[ui.selected_channel]; + } + + const EnvelopeGenerator &selected() const { + return envelopes_[ui.selected_channel]; + } + + EnvelopeGenerator envelopes_[4]; + + SmoothedValue cv1; + SmoothedValue cv2; + SmoothedValue cv3; + SmoothedValue cv4; +}; + +QuadEnvelopeGenerator envgen; + +void ENVGEN_init() { + envgen.Init(); +} + +size_t ENVGEN_storageSize() { + return 4 * EnvelopeGenerator::storageSize(); +} + +size_t ENVGEN_save(void *storage) { + size_t s = 0; + for (auto &env : envgen.envelopes_) + s += env.Save(static_cast(storage) + s); + return s; +} + +size_t ENVGEN_restore(const void *storage) { + size_t s = 0; + for (auto &env : envgen.envelopes_) { + s += env.Restore(static_cast(storage) + s); + env.update_enabled_settings(); + } + + envgen.ui.cursor.AdjustEnd(envgen.envelopes_[0].num_enabled_settings() - 1); + return s; +} + +void ENVGEN_handleAppEvent(OC::AppEvent event) { + switch (event) { + case OC::APP_EVENT_RESUME: + break; + case OC::APP_EVENT_SUSPEND: + case OC::APP_EVENT_SCREENSAVER_ON: + case OC::APP_EVENT_SCREENSAVER_OFF: + break; + } +} + +void ENVGEN_loop() { +} + +static constexpr weegfx::coord_t kPreviewH = 32; +static constexpr weegfx::coord_t kPreviewTopY = 32; +static constexpr weegfx::coord_t kPreviewBottomY = 32 + kPreviewH - 1; + +static constexpr weegfx::coord_t kLoopMarkerY = 28; +static constexpr weegfx::coord_t kCurrentSegmentCursorY = 26; + +int16_t preview_values[128 + 64]; +uint16_t preview_segment_starts[peaks::kMaxNumSegments]; +uint16_t preview_loop_points[peaks::kMaxNumSegments]; +static constexpr uint16_t kPreviewTerminator = 0xffff; + +settings::value_attr segment_editing_attr = { 128, 0, 255, "DOH!", NULL, settings::STORAGE_TYPE_U16 }; + +void ENVGEN_menu_preview() { + auto const &env = envgen.selected(); + + menu::SettingsListItem list_item; + menu::SettingsList::AbsoluteLine(0, list_item); + list_item.selected = false; + list_item.editing = envgen.ui.segment_editing; + const int selected_segment = envgen.ui.selected_segment; + + segment_editing_attr.name = segment_names[selected_segment]; + list_item.DrawDefault(env.get_segment_value(selected_segment), segment_editing_attr); + + // Current envelope shape + uint16_t current_phase = 0; + weegfx::coord_t x = 0; + weegfx::coord_t w = env.RenderPreview(preview_values, preview_segment_starts, preview_loop_points, current_phase); + const int16_t *data = preview_values; + while (x <= static_cast(current_phase)) { + const int16_t value = *data++ >> 10; + graphics.drawVLine(x++, kPreviewBottomY - value, value + 1); + } + + while (x < w) { + const int16_t value = *data++ >> 10; + graphics.setPixel(x++, kPreviewBottomY - value); + } + + if (x < menu::kDisplayWidth) + graphics.drawHLine(x, kPreviewBottomY, menu::kDisplayWidth - x); + + // Minimal cursor thang (x is end of preview) + weegfx::coord_t start = preview_segment_starts[selected_segment]; + weegfx::coord_t end = preview_segment_starts[selected_segment + 1]; + w = kPreviewTerminator == end ? x - start + 1 : end - start + 1; + if (w < 4) w = 4; + graphics.drawRect(start, kCurrentSegmentCursorY, w, 2); + + // Current types only loop over full envelope, so just pixel dust + uint16_t *loop_points = preview_loop_points; + uint_fast8_t i = 0; + while (*loop_points != kPreviewTerminator) { + // odd: end marker, even: start marker + if (i++ & 1) + graphics.drawBitmap8(*loop_points++ - 1, kLoopMarkerY, OC::kBitmapLoopMarkerW, OC::bitmap_loop_markers_8 + OC::kBitmapLoopMarkerW); + else + graphics.drawBitmap8(*loop_points++, kLoopMarkerY, OC::kBitmapLoopMarkerW, OC::bitmap_loop_markers_8); + } + + // Brute-force way of handling "pathological" cases where A/D has no visible + // pixels instead of line-drawing between points + uint16_t *segment_start = preview_segment_starts; + while (*segment_start != kPreviewTerminator) { + weegfx::coord_t x = *segment_start++; + weegfx::coord_t value = preview_values[x] >> 10; + graphics.drawVLine(x, kPreviewBottomY - value, value); + } +} + +void ENVGEN_menu_settings() { + auto const &env = envgen.selected(); + + bool draw_euclidean_editor = false; + + menu::SettingsList settings_list(envgen.ui.cursor); + menu::SettingsListItem list_item; + + while (settings_list.available()) { + const int setting = + env.enabled_setting_at(settings_list.Next(list_item)); + const int value = env.get_value(setting); + const settings::value_attr &attr = EnvelopeGenerator::value_attr(setting); + + switch (setting) { + case ENV_SETTING_TYPE: + list_item.SetPrintPos(); + if (list_item.editing) { + menu::DrawEditIcon(6, list_item.y, value, attr); + graphics.movePrintPos(6, 0); + } + graphics.print(attr.value_names[value]); + list_item.DrawCustom(); + break; + case ENV_SETTING_TRIGGER_INPUT: + if (EnvelopeGenerator::indentSetting(static_cast(setting))) + list_item.x += menu::kIndentDx; + if (value < OC::DIGITAL_INPUT_LAST) { + list_item.DrawDefault(value, attr); + } else { + const int trigger_channel = TriggerSettingToChannel(value); + const IntTriggerType trigger_type = TriggerSettingToType(value, trigger_channel); + + char s[6] = "_ xxx"; + s[0] = 'A' + env.trigger_setting_to_channel_index(trigger_channel); + memcpy(s + 2, internal_trigger_types[trigger_type], 3); + list_item.DrawDefault(s, value, attr); + } + break; + case ENV_SETTING_EUCLIDEAN_OFFSET: + if (!list_item.editing) { + // Use the live values + envgen.ui.euclidean_mask_draw.Render(menu::kDisplayWidth, list_item.y, + env.get_s_euclidean_length(), env.get_s_euclidean_fill(), env.get_s_euclidean_offset(), + env.get_euclidean_counter()); + list_item.DrawCustom(); + } else { + draw_euclidean_editor = true; + } + break; + + default: + if (EnvelopeGenerator::indentSetting(static_cast(setting))) + list_item.x += menu::kIndentDx; + list_item.DrawDefault(value, attr); + break; + } + } + + // Ugly. With a capital blargh. + if (draw_euclidean_editor) { + weegfx::coord_t y = 32 - menu::kMenuLineH / 2 - 1; + graphics.clearRect(0, y, menu::kDisplayWidth, menu::kMenuLineH * 2 + 2); + graphics.drawFrame(0, y, menu::kDisplayWidth, menu::kMenuLineH * 2 + 2); + + y += 2; + envgen.ui.euclidean_mask_draw.Render(menu::kDisplayWidth - 2, y, + env.get_euclidean_length(), env.get_euclidean_fill(), env.get_euclidean_offset(), + env.get_euclidean_counter()); + + y += menu::kMenuLineH; + menu::SettingsListItem list_item; + list_item.selected = false; + list_item.editing = true; + list_item.y = y; + + list_item.x = 1; + list_item.valuex = 38; + list_item.endx = 60 - 2; + if (envgen.ui.euclidean_edit_length) { + auto attr = EnvelopeGenerator::value_attr(ENV_SETTING_EUCLIDEAN_LENGTH); + attr.min_ = 1; + attr.name = "Len"; + list_item.DrawDefault(env.get_euclidean_length(), attr); + } else { + auto attr = EnvelopeGenerator::value_attr(ENV_SETTING_EUCLIDEAN_FILL); + list_item.DrawValueMax(env.get_euclidean_fill(), attr, env.get_euclidean_length() + 0x1); + } + + list_item.editing = true; + list_item.x = 60; + list_item.valuex = 106; + list_item.endx = menu::kDisplayWidth - 2; + list_item.DrawValueMax(env.get_euclidean_offset(), EnvelopeGenerator::value_attr(ENV_SETTING_EUCLIDEAN_OFFSET), env.get_euclidean_length()); + } +} + +void ENVGEN_menu() { + + menu::QuadTitleBar::Draw(); + for (uint_fast8_t i = 0; i < 4; ++i) { + menu::QuadTitleBar::SetColumn(i); + graphics.print((char)('A' + i)); + menu::QuadTitleBar::DrawGateIndicator(i, envgen.envelopes_[i].getTriggerState()); + + + EnvelopeGenerator::DelayedTrigger trigger; + envgen.envelopes_[i].get_next_trigger(trigger); + if (trigger.delay) { + weegfx::coord_t x = menu::QuadTitleBar::ColumnStartX(i) + 28; + weegfx::coord_t h = (trigger.time_left * 8) / trigger.delay; + graphics.drawRect(x, menu::QuadTitleBar::kTextY + 7 - h, 2, 1 + h); + } + } + // If settings mode, draw level in title bar? + menu::QuadTitleBar::Selected(envgen.ui.selected_channel); + + if (QuadEnvelopeGenerator::MODE_EDIT_SEGMENTS == envgen.ui.edit_mode) + ENVGEN_menu_preview(); + else + ENVGEN_menu_settings(); +} + +void ENVGEN_topButton() { + auto &selected_env = envgen.selected(); + selected_env.change_value(ENV_SETTING_SEG1_VALUE + envgen.ui.selected_segment, 32); +} + +void ENVGEN_lowerButton() { + auto &selected_env = envgen.selected(); + selected_env.change_value(ENV_SETTING_SEG1_VALUE + envgen.ui.selected_segment, -32); +} + +void ENVGEN_rightButton() { + + if (QuadEnvelopeGenerator::MODE_EDIT_SEGMENTS == envgen.ui.edit_mode) { + envgen.ui.segment_editing = !envgen.ui.segment_editing; + } else { + envgen.ui.cursor.toggle_editing(); + envgen.ui.euclidean_edit_length = false; + } +} + +void ENVGEN_leftButton() { + if (QuadEnvelopeGenerator::MODE_EDIT_SETTINGS == envgen.ui.edit_mode) { + if (!envgen.euclidean_edit_active()) { + envgen.ui.edit_mode = QuadEnvelopeGenerator::MODE_EDIT_SEGMENTS; + envgen.ui.cursor.set_editing(false); + } else { + envgen.ui.euclidean_edit_length = !envgen.ui.euclidean_edit_length; + } + } else { + envgen.ui.edit_mode = QuadEnvelopeGenerator::MODE_EDIT_SETTINGS; + envgen.ui.segment_editing = false; + } +} + +void ENVGEN_handleButtonEvent(const UI::Event &event) { + if (UI::EVENT_BUTTON_PRESS == event.type) { + switch (event.control) { + case OC::CONTROL_BUTTON_UP: + ENVGEN_topButton(); + break; + case OC::CONTROL_BUTTON_DOWN: + ENVGEN_lowerButton(); + break; + case OC::CONTROL_BUTTON_L: + ENVGEN_leftButton(); + break; + case OC::CONTROL_BUTTON_R: + ENVGEN_rightButton(); + break; + } + } +} + +void ENVGEN_handleEncoderEvent(const UI::Event &event) { + + if (OC::CONTROL_ENCODER_L == event.control) { + if (envgen.euclidean_edit_active()) { + if (envgen.ui.euclidean_edit_length) { + // Artificially constrain length here + int length = envgen.selected().get_euclidean_length() + event.value; + if (length > 0) { + envgen.selected().apply_value(ENV_SETTING_EUCLIDEAN_LENGTH, length); + // constrain k, offset: + if (length < envgen.selected().get_euclidean_fill()) + envgen.selected().apply_value(ENV_SETTING_EUCLIDEAN_FILL, length + 0x1); + if (length < envgen.selected().get_euclidean_offset()) + envgen.selected().apply_value(ENV_SETTING_EUCLIDEAN_OFFSET, length); + } + } else { + // constrain k: + if (envgen.selected().get_euclidean_fill() <= envgen.selected().get_euclidean_length()) + envgen.selected().change_value(ENV_SETTING_EUCLIDEAN_FILL, event.value); + else if (event.value < 0) + envgen.selected().change_value(ENV_SETTING_EUCLIDEAN_FILL, event.value); + } + } else { + int left_value = envgen.ui.selected_channel + event.value; + CONSTRAIN(left_value, 0, 3); + envgen.ui.selected_channel = left_value; + auto &selected_env = envgen.selected(); + CONSTRAIN(envgen.ui.selected_segment, 0, selected_env.num_editable_segments() - 1); + envgen.ui.cursor.AdjustEnd(selected_env.num_enabled_settings() - 1); + } + } else if (OC::CONTROL_ENCODER_R == event.control) { + if (QuadEnvelopeGenerator::MODE_EDIT_SEGMENTS == envgen.ui.edit_mode) { + auto &selected_env = envgen.selected(); + if (envgen.ui.segment_editing) { + selected_env.change_value(ENV_SETTING_SEG1_VALUE + envgen.ui.selected_segment, event.value); + } else { + int selected_segment = envgen.ui.selected_segment + event.value; + CONSTRAIN(selected_segment, 0, selected_env.num_editable_segments() - 1); + envgen.ui.selected_segment = selected_segment; + } + } else { + if (envgen.ui.cursor.editing()) { + auto &selected_env = envgen.selected(); + EnvelopeSettings setting = selected_env.enabled_setting_at(envgen.ui.cursor.cursor_pos()); + + if (ENV_SETTING_EUCLIDEAN_OFFSET == setting) { + // constrain offset + if (selected_env.get_euclidean_offset() < selected_env.get_euclidean_length()) + selected_env.change_value(ENV_SETTING_EUCLIDEAN_OFFSET, event.value); + else if (event.value < 0) + selected_env.change_value(ENV_SETTING_EUCLIDEAN_OFFSET, event.value); + } + else { + selected_env.change_value(setting, event.value); + } + + if (ENV_SETTING_TRIGGER_DELAY_MODE == setting || ENV_SETTING_EUCLIDEAN_LENGTH == setting) + selected_env.update_enabled_settings(); + envgen.ui.cursor.AdjustEnd(selected_env.num_enabled_settings() - 1); + } else { + envgen.ui.cursor.Scroll(event.value); + } + } + } +} + +int16_t fast_preview_values[peaks::kFastPreviewWidth + 32]; + +template +void RenderFastPreview() { + uint16_t w = envgen.envelopes_[index].RenderFastPreview(fast_preview_values); + CONSTRAIN(w, 0, peaks::kFastPreviewWidth); // Just-in-case + weegfx::coord_t x = startx; + const int16_t *values = fast_preview_values; + while (w--) { + const int16_t value = 1 + ((*values++ >> 10) & 0x1f); + graphics.drawVLine(x++, y + 32 - value, value); + } +} + +void ENVGEN_screensaver() { +#ifdef ENVGEN_DEBUG_SCREENSAVER + debug::CycleMeasurement render_cycles; +#endif + + #ifdef BUCHLA_4U + RenderFastPreview<0, 0, 32>(); + RenderFastPreview<1, 64, 32>(); + RenderFastPreview<2, 0, 0>(); + RenderFastPreview<3, 64, 0>(); + #else + RenderFastPreview<0, 0, 0>(); + RenderFastPreview<1, 64, 0>(); + RenderFastPreview<2, 0, 32>(); + RenderFastPreview<3, 64, 32>(); + #endif + OC::scope_render(); + +#ifdef ENVGEN_DEBUG_SCREENSAVER + uint32_t us = debug::cycles_to_us(render_cycles.read()); + graphics.setPrintPos(2, 56); + graphics.printf("%u", us); +#endif +} + +#ifdef ENVGEN_DEBUG +void ENVGEN_debug() { + for (int i = 0; i < 4; ++i) { + uint8_t ypos = 10*(i + 1) + 2 ; + graphics.setPrintPos(2, ypos); + graphics.print(envgen.envelopes_[i].get_amplitude_value()) ; + graphics.setPrintPos(50, ypos); + graphics.print(envgen.envelopes_[i].get_sampled_amplitude_value()) ; + graphics.setPrintPos(100, ypos); + graphics.print(envgen.envelopes_[i].get_is_amplitude_sampled()) ; + } +} +#endif // ENVGEN_DEBUG + +void FASTRUN ENVGEN_isr() { + envgen.ISR(); +} + +#endif // ENABLE_APP_PIQUED diff --git a/software/o_c_REV/APP_FPART.ino b/software/o_c_REV/APP_FPART.ino new file mode 100644 index 000000000..8a3098cc2 --- /dev/null +++ b/software/o_c_REV/APP_FPART.ino @@ -0,0 +1,926 @@ +// Copyright (c) 2023 Jesse Dinneen +// +// Author of this app: Jesse Dinneen (jdinneen@gmail.com) +// Authors of O+C firmware and apps: Patrick Dowling (pld@gurkenkiste.com) +// Tim Churches (tim.churches@gmail.com), mxmxmx (https://github.com/mxmxmx) +// With great help from tutorials and examples by Chysn (https://github.com/Chysn) +// and Naomi Seyfer (https://github.com/sixolet) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// FPART: a 4-channel step sequencer with staff-like visualisation. +// To use, add to OC_app.ino app array: +// DECLARE_APP('F','P', "4 Parts", FPART, FPART_isr), +// +// Calling the files/classes "4-Part" (w/ number) causes errors, +// so the admittedly unattractive "fpart" it is (for now)... + +#ifdef ENABLE_APP_FPART + +// refs to other files, data structures, etc +#include "OC_apps.h" +#include "util/util_settings.h" +#include "OC_menus.h" +#include "OC_strings.h" +#include "OC_DAC.h" +#include "OC_digital_inputs.h" +#include "OC_ADC.h" + +/* list each setting that should appear in the settings menu + _SETTING_LAST is required and must come last + to read these in the class: int root = values_[FPART_SETTING_ROOT]; + to write one: apply_value(int index, int value) */ +enum FPART_SETTINGS { + FPART_SETTING_ROOT, + FPART_SETTING_SCALE, + FPART_SETTING_LOOPSTART, + FPART_SETTING_LOOPEND, + FPART_SETTING_A_OCTAVE, + FPART_SETTING_B_OCTAVE, + FPART_SETTING_C_OCTAVE, + FPART_SETTING_D_OCTAVE, + FPART_SETTING_LBUTTON_TOGGLE, + FPART_SETTING_ACTIVECHORD, + FPART_SETTING_CHORD0, + FPART_SETTING_CHORD1, + FPART_SETTING_CHORD2, + FPART_SETTING_CHORD3, + FPART_SETTING_CHORD4, + FPART_SETTING_CHORD5, + FPART_SETTING_CHORD6, + FPART_SETTING_CHORD7, + FPART_SETTING_CHORD8, + FPART_SETTING_CHORD9, + FPART_SETTING_CHORD10, + FPART_SETTING_CHORD11, + FPART_SETTING_CHORD12, + FPART_SETTING_CHORD13, + FPART_SETTING_CHORD14, + FPART_SETTING_CHORD15, + FPART_SETTING_CHORD16, + FPART_SETTING_CHORD17, + FPART_SETTING_CHORD18, + FPART_SETTING_CHORD19, + FPART_SETTING_CHORD20, + FPART_SETTING_CHORD21, + FPART_SETTING_CHORD22, + FPART_SETTING_CHORD23, + FPART_SETTING_CHORD24, + FPART_SETTING_CHORD25, + FPART_SETTING_CHORD26, + FPART_SETTING_CHORD27, + FPART_SETTING_CHORD28, + FPART_SETTING_CHORD29, + FPART_SETTING_CHORD30, + FPART_SETTING_CHORD31, + FPART_SETTING_CHORD32, + FPART_SETTING_CHORD33, + FPART_SETTING_CHORD34, + FPART_SETTING_CHORD35, + FPART_SETTING_CHORD36, + FPART_SETTING_CHORD37, + FPART_SETTING_CHORD38, + FPART_SETTING_CHORD39, + FPART_SETTING_CHORD40, + FPART_SETTING_CHORD41, + FPART_SETTING_CHORD42, + FPART_SETTING_CHORD43, + FPART_SETTING_CHORD44, + FPART_SETTING_CHORD45, + FPART_SETTING_CHORD46, + FPART_SETTING_CHORD47, + FPART_SETTING_CHORD48, + FPART_SETTING_CHORD49, + FPART_SETTING_CHORD50, + FPART_SETTING_CHORD51, + FPART_SETTING_CHORD52, + FPART_SETTING_CHORD53, + FPART_SETTING_CHORD54, + FPART_SETTING_CHORD55, + FPART_SETTING_CHORD56, + FPART_SETTING_CHORD57, + FPART_SETTING_CHORD58, + FPART_SETTING_CHORD59, + FPART_SETTING_CHORD60, + FPART_SETTING_CHORD61, + FPART_SETTING_CHORD62, + FPART_SETTING_CHORD63, + FPART_SETTING_CHORD64, + FPART_SETTING_CHORD65, + FPART_SETTING_CHORD66, + FPART_SETTING_CHORD67, + FPART_SETTING_CHORD68, + FPART_SETTING_CHORD69, + FPART_SETTING_CHORD70, + FPART_SETTING_CHORD71, + FPART_SETTING_CHORD72, + FPART_SETTING_CHORD73, + FPART_SETTING_CHORD74, + FPART_SETTING_CHORD75, + FPART_SETTING_CHORD76, + FPART_SETTING_CHORD77, + FPART_SETTING_CHORD78, + FPART_SETTING_CHORD79, + FPART_SETTING_CHORD80, + FPART_SETTING_CHORD81, + FPART_SETTING_CHORD82, + FPART_SETTING_CHORD83, + FPART_SETTING_CHORD84, + FPART_SETTING_CHORD85, + FPART_SETTING_CHORD86, + FPART_SETTING_CHORD87, + FPART_SETTING_CHORD88, + FPART_SETTING_CHORD89, + FPART_SETTING_CHORD90, + FPART_SETTING_CHORD91, + FPART_SETTING_CHORD92, + FPART_SETTING_CHORD93, + FPART_SETTING_CHORD94, + FPART_SETTING_CHORD95, + FPART_SETTING_CHORD96, + FPART_SETTING_CHORD97, + FPART_SETTING_CHORD98, + FPART_SETTING_LAST +}; + +enum FPART_MENU_PAGES { + FPART_MENU_PARAMETERS, + FPART_MENU_STAFFS, + FPART_MENU_PAGES_LAST +}; + +namespace menu = OC::menu; + +// define roots and scales and their names; root names come from OC_strings +const char * const scale_names[7] = { // name the possible scales, indices match scale_pitches + "IONI", "DORI", "PHRY", "LYDI", "MIXO", "AEOL", "LOCR" +}; +int scale_pitches[7][7] = { // define the pitch values for those scales + { 0, 256, 512, 640, 896, 1152, 1408 }, // Ionian + { 0, 256, 384, 640, 896, 1152, 1280 }, // Dorian + { 0, 128, 384, 640, 896, 1024, 1280 }, // Phrygian + { 0, 256, 512, 768, 896, 1152, 1408 }, // Lydian + { 0, 256, 512, 640, 896, 1152, 1280 }, // Mixolydian + { 0, 256, 384, 640, 896, 1024, 1280 }, // Aeolian + { 0, 128, 384, 640, 768, 1024, 1280 } // Locrian +}; +int root_degree_lookup[12] = { // for each semitone from C, convert it to a degree + 0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6 +}; +// define values for FPART_SETTING_LBUTTON_TOGGLE +const char * const l_options[2] = { // user will set 0 for C&P or 1 for Reset + "COPY/PASTE", "GO TO 0" +}; + +// define the main app class +class Fpart : public settings::SettingsBase { +public: + + menu::ScreenCursor cursor; // instantiate a cursor + + bool a_or_b = true; // track which channel's note icon is being moved (true = a/c) + bool c_or_d = true; + int last_cv1_index; // for tracking changes to CV1 and CV2 + int last_cv2_index; + bool copy = true; + int copyBuffer = 0; + + // define functions to get values + int get_root() const { + return values_[FPART_SETTING_ROOT]; + } + int get_scale() const { + return values_[FPART_SETTING_SCALE]; + } + int get_a_oct() const { + return values_[FPART_SETTING_A_OCTAVE]; + } + int get_b_oct() const { + return values_[FPART_SETTING_B_OCTAVE]; + } + int get_c_oct() const { + return values_[FPART_SETTING_C_OCTAVE]; + } + int get_d_oct() const { + return values_[FPART_SETTING_D_OCTAVE]; + } + int get_loopend() const { + return values_[FPART_SETTING_LOOPEND]; + } + int get_loopstart() const { + return values_[FPART_SETTING_LOOPSTART]; + } + int get_activechord() const { + return values_[FPART_SETTING_ACTIVECHORD]; + } + int get_chord_int(int chord_number) const { + return values_[chord_number + 10]; // assumes chord_nums 0-98 and that SETTING_CHORD_0 is in slot 10 + } + int get_l_option() const { + return values_[FPART_SETTING_LBUTTON_TOGGLE]; + } + + //define functions to set values + void set_activechord(int chordnum) { + int reasonable_num = constrain(chordnum, 0, 98); + apply_value(FPART_SETTING_ACTIVECHORD, reasonable_num); + } + void set_chord_int(int chord_num, int chord_int) { + int reasonable_chord_num = constrain(chord_num, 0, 98); + int reasonable_chord_int = constrain(chord_int, 0, 32323232); + apply_value(reasonable_chord_num + 10, reasonable_chord_int); // assumes chord_nums 0-98 and that SETTING_CHORD_0 is in slot 10 + } + + // given four 0-22 note values, set to 10-32, put in right place in 8-digit int, return it + int build_chord_int(int a_note, int b_note, int c_note, int d_note) const { + int built_integer = (a_note + 10) * 1000000; // e.g. turns 22 into 32000000 + built_integer += (b_note + 10) * 10000; // e.g. adds 320000, producing 32320000 + built_integer += (c_note + 10) * 100; // adds four digits + built_integer += (d_note + 10); // adds last two + return built_integer; + } + + // given some chord (an 8-digit long integer), + // and an index 0-3 referring to which set of digits are desired, + // get and return those two digits as an int + // TODO: replace with bitshift? + int get_note_from_chord_int(int chord_int, int index) const { + int note = 0; + if (index == 0) { // digits 8 and 7 (counting from right) i.e. channel A + note = (chord_int / 1000000) % 100; + return note; + } else if (index == 1) { //digits 6 and 5, ch. B + note = (chord_int / 10000) % 100; + return note; + } else if (index == 2) { //digits 4 and 3, ch. C + note = (chord_int / 100) % 100; + return note; + } else if (index == 3) { //digits 2 and 1, ch. D + note = chord_int % 100; + return note; + } + return EXIT_SUCCESS; + } + + // define function to calculate a note's pitch in some key + int32_t get_pitch_from_note(int note) const { + // considering the current root (0-11, from note_names) and scale/mode (0-6, scale_pitches), + // calculate the possible pitches of the resulting key. + int root_val = get_root(); + int root_pitch = root_val * 128; + int mode = get_scale(); + int key_pitches[7] = { + root_pitch + scale_pitches[mode][0], + root_pitch + scale_pitches[mode][1], + root_pitch + scale_pitches[mode][2], + root_pitch + scale_pitches[mode][3], + root_pitch + scale_pitches[mode][4], + root_pitch + scale_pitches[mode][5], + root_pitch + scale_pitches[mode][6] + }; + // then given a note's position on the staffs (0-22, see table below), + // and where that note is relative to the root's distance from C (distance of 0-6), + // get that note's output pitch from the generated key_pitches. + // table: + // 22-19 F just below bass clef to bass B + // 18-12 bass C to to B (just below middle c) + // 11-5 middle C to treble B + // 4-0 treble C to G just above clef + int note_val; //for conditionally converting screen positions 0-22 to notes 0-6 (i.e. C-B) plus octaves + int root_shift_amt = root_degree_lookup[root_val]; // convert root from semitone (0-11) to wholetone (0-6) + int32_t note_pitch; // 0C 1D 2E 3F 4G 5A 6B 0C 1D 2E 3F 4G 5A 6B 0C + int note_scale_degree = 0;// for finding which pitch/degree the note is in our new scale + if (note > 18) { // F-B::22-19, add no octaves + note_val = 25 - note; // subtract val (<=22) from (4 + 3*7), because I foolishly assign higher note values the further from the top of the staff they go. + note_scale_degree = note_val - root_shift_amt; //get the distance between the root and note + if (note_scale_degree < 0) { //put it into a usable range if it's too low + note_scale_degree += 7; + note_pitch = key_pitches[note_scale_degree]; // get the pitch from our scale at the index we calculated + note_pitch -= 12 << 7; //tone it back down + } else { + note_pitch = key_pitches[note_scale_degree]; // get the pitch from our scale at the index we calculated + } + } else if (note > 11) { // C-B::18-12, add an octave + note_val = 18 - note; // 4 + 2*7 + note_scale_degree = note_val - root_shift_amt; + if (note_scale_degree < 0) { + note_scale_degree += 7; + note_pitch = key_pitches[note_scale_degree]; + } else { + note_pitch = key_pitches[note_scale_degree]; + note_pitch += 12 << 7; + } + } else if (note > 4) { // C-B::11-5, add two octaves + note_val = 11 - note; // 4 + 1*7 + note_scale_degree = note_val - root_shift_amt; + if (note_scale_degree < 0) { + note_scale_degree += 7; + note_pitch = key_pitches[note_scale_degree]; + note_pitch += 12 << 7; + } else { + note_pitch = key_pitches[note_scale_degree]; + note_pitch += 12 << 7; + note_pitch += 12 << 7; + } + } else { // C-G::4-0, add three octaves + note_val = 4 - note; // 4 + 0*7 + note_scale_degree = note_val - root_shift_amt; + if (note_scale_degree < 0) { + note_scale_degree += 7; + note_pitch = key_pitches[note_scale_degree]; + note_pitch += 12 << 7; + note_pitch += 12 << 7; + } else { + note_pitch = key_pitches[note_scale_degree]; + note_pitch += 12 << 7; + note_pitch += 12 << 7; + note_pitch += 12 << 7; + } + } + return note_pitch; + } + + //define copy and paste actions to be called by long_press_left + void copyChord() { + copyBuffer = get_chord_int(get_activechord()); + } + void pasteChord() { + set_chord_int(get_activechord(), copyBuffer); + } + + //define how to draw each channel symbol + void drawA(int x, int y) { //empty circle + graphics.drawCircle(x+2, y+2, 2.5); //the circle is smaller than the square and its coords refer to its centre so an offset is necessary + } + void drawB(int x, int y) { //empty square + graphics.drawFrame(x, y, 5, 5); + } + void drawC(int x, int y) { //filled circle + graphics.drawCircle(x+2, y+2, 2); //offset circle + graphics.drawRect(x+1, y+1, 3, 3); + } + void drawD(int x, int y) { //filled square + graphics.drawRect(x, y, 5, 5); + } + + //menu-related functions + uint8_t get_menu_page() const { + return menu_page_; + } + void set_menu_page(uint8_t _menu_page) { + menu_page_ = _menu_page; + } + + // initialise variables + void Init() { + InitDefaults(); + menu_page_ = FPART_MENU_STAFFS; + last_cv1_index = 0; + last_cv2_index = 0; + } + + void Loop(); + + void ISR() { + //for each channel, get the note value, get that note's pitch + int chord_int = get_chord_int(get_activechord()); + int a_note = get_note_from_chord_int(chord_int, 0) - 10; //subtract 10 to scale notes back down to 0-22 values expected + int b_note = get_note_from_chord_int(chord_int, 1) - 10; + int c_note = get_note_from_chord_int(chord_int, 2) - 10; + int d_note = get_note_from_chord_int(chord_int, 3) - 10; + int a_oct = get_a_oct(); + int b_oct = get_b_oct(); + int c_oct = get_c_oct(); + int d_oct = get_d_oct(); + int32_t a_pitch = get_pitch_from_note(a_note); + int32_t b_pitch = get_pitch_from_note(b_note); + int32_t c_pitch = get_pitch_from_note(c_note); + int32_t d_pitch = get_pitch_from_note(d_note); + //send pitches to the DAC + //send channel (a-d), pitch (int32), and octave (int32) + OC::DAC::set_pitch(DAC_CHANNEL_A, a_pitch, a_oct); + OC::DAC::set_pitch(DAC_CHANNEL_B, b_pitch, b_oct); + OC::DAC::set_pitch(DAC_CHANNEL_C, c_pitch, c_oct); + OC::DAC::set_pitch(DAC_CHANNEL_D, d_pitch, d_oct); + } + +private: + int8_t menu_page_; +}; + +// declare sources to generate the settings menu (need 4 or more) +SETTINGS_DECLARE(Fpart, FPART_SETTING_LAST) { + { 0, 0, 11, "tonic", OC::Strings::note_names_unpadded, settings::STORAGE_TYPE_U8 }, + { 0, 0, 6, "mode", scale_names, settings::STORAGE_TYPE_U8 }, + { 0, 0, 97, "loop start", NULL, settings::STORAGE_TYPE_U8 }, + { 3, 1, 98, "loop end", NULL, settings::STORAGE_TYPE_U8 }, + { 0, -3, 6, "chan a oct", NULL, settings::STORAGE_TYPE_I8 }, + { 0, -3, 6, "chan b oct", NULL, settings::STORAGE_TYPE_I8 }, + { 0, -3, 6, "chan c oct", NULL, settings::STORAGE_TYPE_I8 }, + { 0, -3, 6, "chan d oct", NULL, settings::STORAGE_TYPE_I8 }, + { 0, 0, 1, "long L", l_options, settings::STORAGE_TYPE_I8 }, + { 0, 0, 98, "active chord", NULL, settings::STORAGE_TYPE_I8 }, // FPART_SETTING_ACTIVECHORD + { 10101010, 10101010, 32323232, "chord0 int", NULL, settings::STORAGE_TYPE_U32 },// FPART_SETTING_CHORD0 + { 10101010, 10101010, 32323232, "chord1 int", NULL, settings::STORAGE_TYPE_U32 },// FPART_SETTING_CHORD1 + { 10101010, 10101010, 32323232, "chord2 int", NULL, settings::STORAGE_TYPE_U32 },// ...2 + { 10101010, 10101010, 32323232, "chord3 int", NULL, settings::STORAGE_TYPE_U32 },// 3 + { 10101010, 10101010, 32323232, "chord4 int", NULL, settings::STORAGE_TYPE_U32 },// 4 + { 10101010, 10101010, 32323232, "chord5 int", NULL, settings::STORAGE_TYPE_U32 },// 5 + { 10101010, 10101010, 32323232, "chord6 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord7 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord8 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord9 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord10 int", NULL, settings::STORAGE_TYPE_U32 },// 10 + { 10101010, 10101010, 32323232, "chord11 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord12 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord13 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord14 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord15 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord16 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord17 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord18 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord19 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord20 int", NULL, settings::STORAGE_TYPE_U32 },// 20 + { 10101010, 10101010, 32323232, "chord21 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord22 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord23 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord24 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord25 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord26 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord27 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord28 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord29 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord30 int", NULL, settings::STORAGE_TYPE_U32 },// 30 + { 10101010, 10101010, 32323232, "chord31 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord32 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord33 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord34 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord35 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord36 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord37 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord38 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord39 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord40 int", NULL, settings::STORAGE_TYPE_U32 },// 40 + { 10101010, 10101010, 32323232, "chord41 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord42 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord43 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord44 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord45 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord46 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord47 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord48 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord49 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord50 int", NULL, settings::STORAGE_TYPE_U32 },// 50 + { 10101010, 10101010, 32323232, "chord51 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord52 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord53 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord54 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord55 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord56 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord57 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord58 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord59 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord60 int", NULL, settings::STORAGE_TYPE_U32 },// 60 + { 10101010, 10101010, 32323232, "chord61 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord62 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord63 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord64 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord65 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord66 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord67 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord68 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord69 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord70 int", NULL, settings::STORAGE_TYPE_U32 },// 70 + { 10101010, 10101010, 32323232, "chord71 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord72 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord73 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord74 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord75 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord76 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord77 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord78 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord79 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord80 int", NULL, settings::STORAGE_TYPE_U32 },// 80 + { 10101010, 10101010, 32323232, "chord81 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord82 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord83 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord84 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord85 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord86 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord87 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord88 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord89 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord90 int", NULL, settings::STORAGE_TYPE_U32 },// 90 + { 10101010, 10101010, 32323232, "chord91 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord92 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord93 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord94 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord95 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord96 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord97 int", NULL, settings::STORAGE_TYPE_U32 }, + { 10101010, 10101010, 32323232, "chord98 int", NULL, settings::STORAGE_TYPE_U32 } // 98 / last chord slot +}; + +// create an instance of the main app class +Fpart fpart_instance; + +// required global functions +void FPART_init() { + fpart_instance.cursor.Init(FPART_SETTING_ROOT, FPART_SETTING_LAST - 1); + fpart_instance.Init(); +} + +size_t FPART_storageSize() { + return Fpart::storageSize(); +} + +size_t FPART_save(void *storage) { + return fpart_instance.Save(storage); +} + +size_t FPART_restore(const void *storage) { + return fpart_instance.Restore(storage); +} + +void FPART_isr() { + // call class's isr function + fpart_instance.ISR(); + //first the digital inputs (clocks/triggers) + uint32_t triggers = OC::DigitalInputs::clocked(); + int current_chord = fpart_instance.get_activechord(); + if (triggers & (1 << OC::DIGITAL_INPUT_1)) { //input_1 means decrement chord + if (current_chord > fpart_instance.get_loopstart()){ //assuming it's not at loopstart + fpart_instance.set_activechord(current_chord - 1); + } + else { //go to loopend + fpart_instance.set_activechord(fpart_instance.get_loopend()); + } + } + if (triggers & (1 << OC::DIGITAL_INPUT_2)) { //input_2 means increment chord + if (current_chord < fpart_instance.get_loopend()) { //assuming it's not at loopend + fpart_instance.set_activechord(current_chord + 1); + } + else { //go to loopstart + fpart_instance.set_activechord(fpart_instance.get_loopstart()); + } + } + if (triggers & (1 << OC::DIGITAL_INPUT_3)) { //input_3 means go straight to loopstart + fpart_instance.set_activechord(fpart_instance.get_loopstart()); + } + if (triggers & (1 << OC::DIGITAL_INPUT_4)) { //input_4 means go straight to loopend + fpart_instance.set_activechord(fpart_instance.get_loopend()); + } + + // TODO: these work but don't seem to get you to the final possible chords + //now the CV inputs + int32_t adc1_value = OC::ADC::raw_pitch_value(ADC_CHANNEL_1); + if (adc1_value < 90) adc1_value = 0; + int cv1_index = adc1_value / (7800 / 99); // Chysn suggests 7800 is the highest value the ADC method used will return + cv1_index = constrain(cv1_index, 0, 98); + if (cv1_index != fpart_instance.last_cv1_index) { + fpart_instance.last_cv1_index = cv1_index; + fpart_instance.set_activechord(cv1_index); + } + + int32_t adc2_value = OC::ADC::raw_pitch_value(ADC_CHANNEL_2); + if (adc2_value < 90) adc2_value = 0; + int loop_length = fpart_instance.get_loopend() - fpart_instance.get_loopstart(); + loop_length = constrain(loop_length, 0, fpart_instance.get_loopend()); + int cv2_index = adc2_value / (7800 / loop_length); // Chysn suggests 7800 is the highest value the ADC method used will return + cv2_index = constrain(cv2_index, 0, loop_length); + cv2_index += fpart_instance.get_loopstart(); //offset it by loop start amount + if (cv2_index != fpart_instance.last_cv2_index) { + fpart_instance.last_cv2_index = cv2_index; + fpart_instance.set_activechord(cv2_index); + } +} + +// has to be here, but currently unused +void FPART_handleAppEvent(OC::AppEvent event) { + //if (event == OC::APP_EVENT_RESUME) { + //break; +} + +void FPART_loop() { + //FPART_updateChannels(); +} + +void FPART_menu() { + if (fpart_instance.get_menu_page() == FPART_MENU_STAFFS) { + // draw two staffs + for (int s = 0; s < 11; s++) // the screen fits two staffs and middle C line, i.e. 11 lines + { + int y = 7 + (5 * s); // initial offset plus distance between lines + if (s == 5) { // middle C row + //graphics.drawHLineDots(5, y, 64); //could use this to draw the middle C line + continue; + } + graphics.drawLine(8, y, 114, y); + } + //vertical lines to indicate active chord on staffs + graphics.drawVLine(40,0,64); + graphics.drawVLine(52,0,64); + //prepare to draw note symbols + //x coordinates for chord columns. Note: changing active_x entails changing the box/VLines. + int prior_x = 12; + int active_x = 44; + int next_x = 75; + int last_x = 106; + //y coordinates for each of the possible note locations; inconsistent but carefully chosen for clear presentation + int y[23] = {2, 5, 7, 10, 12, 15, 17, 20, 22, 25, 27, 30, 32, 35, 37, 40, 42, 45, 47, 50, 52, 55, 57}; + int chordID = fpart_instance.get_activechord(); + if (chordID > 0) { //if there could be a prior chord, get and display its values + int chord_int = fpart_instance.get_chord_int(fpart_instance.get_activechord() -1); + int a_note = fpart_instance.get_note_from_chord_int(chord_int, 0) - 10; //subtract 10 to scale notes back down to 0-22 values expected + int b_note = fpart_instance.get_note_from_chord_int(chord_int, 1) - 10; + int c_note = fpart_instance.get_note_from_chord_int(chord_int, 2) - 10; + int d_note = fpart_instance.get_note_from_chord_int(chord_int, 3) - 10; + fpart_instance.drawA(prior_x,y[a_note]); + fpart_instance.drawB(prior_x,y[b_note]); + fpart_instance.drawC(prior_x,y[c_note]); + fpart_instance.drawD(prior_x,y[d_note]); + } + // get and display the active chord's values + int chord_int = fpart_instance.get_chord_int(fpart_instance.get_activechord()); + int a_note = fpart_instance.get_note_from_chord_int(chord_int, 0) - 10; + int b_note = fpart_instance.get_note_from_chord_int(chord_int, 1) - 10; + int c_note = fpart_instance.get_note_from_chord_int(chord_int, 2) - 10; + int d_note = fpart_instance.get_note_from_chord_int(chord_int, 3) - 10; + fpart_instance.drawA(active_x,y[a_note]); + fpart_instance.drawB(active_x,y[b_note]); + fpart_instance.drawC(active_x,y[c_note]); + fpart_instance.drawD(active_x,y[d_note]); + if (chordID <= 99 - 2 ) { //if there could be a next chord, get and display its values + int chord_int = fpart_instance.get_chord_int(fpart_instance.get_activechord() + 1); + int a_note = fpart_instance.get_note_from_chord_int(chord_int, 0) - 10; + int b_note = fpart_instance.get_note_from_chord_int(chord_int, 1) - 10; + int c_note = fpart_instance.get_note_from_chord_int(chord_int, 2) - 10; + int d_note = fpart_instance.get_note_from_chord_int(chord_int, 3) - 10; + fpart_instance.drawA(next_x,y[a_note]); + fpart_instance.drawB(next_x,y[b_note]); + fpart_instance.drawC(next_x,y[c_note]); + fpart_instance.drawD(next_x,y[d_note]); + } + if (chordID <= 99 - 3 ) { //if there could be a next-next chord, get and display its values + int chord_int = fpart_instance.get_chord_int(fpart_instance.get_activechord() + 2); + int a_note = fpart_instance.get_note_from_chord_int(chord_int, 0) - 10; + int b_note = fpart_instance.get_note_from_chord_int(chord_int, 1) - 10; + int c_note = fpart_instance.get_note_from_chord_int(chord_int, 2) - 10; + int d_note = fpart_instance.get_note_from_chord_int(chord_int, 3) - 10; + fpart_instance.drawA(last_x,y[a_note]); + fpart_instance.drawB(last_x,y[b_note]); + fpart_instance.drawC(last_x,y[c_note]); + fpart_instance.drawD(last_x,y[d_note]); + } + //draw a number to indicate current step + graphics.setPrintPos(116, 9); // any further right and some 2-digit values go to next line + graphics.print(chordID); + //draw L-button setting + graphics.setPrintPos(116, 19); + graphics.print("L"); + graphics.setPrintPos(122, 19); + if (fpart_instance.get_l_option() == 1) { + graphics.print(0); + } else if (fpart_instance.copy) { + graphics.print("C"); + } else { + graphics.print("P"); + } + //draw output-selection indicators + graphics.setPrintPos(116, 39); + if (fpart_instance.a_or_b) { + graphics.print("1"); + fpart_instance.drawA(123, 40); + } else { + graphics.print("2"); + fpart_instance.drawB(123, 40); + } + graphics.setPrintPos(116, 49);; + if (fpart_instance.c_or_d) { + graphics.print("3"); + fpart_instance.drawC(123, 50); + } else { + graphics.print("4"); + fpart_instance.drawD(123, 50); + } + } else if (fpart_instance.get_menu_page() == FPART_MENU_PARAMETERS) { //draw the settings + menu::DefaultTitleBar::Draw(); + graphics.print("4 Parts"); + menu::SettingsList settings_list(fpart_instance.cursor); + menu::SettingsListItem list_item; + while (settings_list.available()) + { + const int current = settings_list.Next(list_item); + const int value = fpart_instance.get_value(current); + if (current < 10) { // draw settings 0-9, i.e. don't show chord ints + list_item.DrawDefault(value, Fpart::value_attr(current)); + } else { + break; + } + } + } +} + +// define what should happen when the screensaver is activated +void FPART_screensaver() { + if (fpart_instance.get_menu_page() == FPART_MENU_PARAMETERS) { //go to staffs + fpart_instance.set_menu_page(FPART_MENU_STAFFS); + } + FPART_menu(); //this effectively blocks the screensaver; good while composing, but bad for screen +} + +// direct button presses to correct functions +void FPART_handleButtonEvent(const UI::Event &event) { + if (UI::EVENT_BUTTON_LONG_PRESS == event.type) { //long presses + switch (event.control) { + case OC::CONTROL_BUTTON_DOWN: + FPART_downButtonLong(); + break; + case OC::CONTROL_BUTTON_L: + FPART_leftButtonLong(); + break; + } + } + if (UI::EVENT_BUTTON_PRESS == event.type) { //short presses + switch (event.control) { + case OC::CONTROL_BUTTON_UP: + FPART_upButton(); + break; + case OC::CONTROL_BUTTON_DOWN: + FPART_downButton(); + break; + case OC::CONTROL_BUTTON_L: + FPART_leftButton(); + break; + case OC::CONTROL_BUTTON_R: + FPART_rightButton(); + break; + } + } +} + +// handle encoder turns +void FPART_handleEncoderEvent(const UI::Event &event) { + if (fpart_instance.get_menu_page() == FPART_MENU_STAFFS) { //staffs page + int chordID = fpart_instance.get_activechord(); + int chord_int = fpart_instance.get_chord_int(chordID); + int a_note = fpart_instance.get_note_from_chord_int(chord_int, 0) - 10; //subtract 10 to scale notes back down to 0-22 values expected + int b_note = fpart_instance.get_note_from_chord_int(chord_int, 1) - 10; + int c_note = fpart_instance.get_note_from_chord_int(chord_int, 2) - 10; + int d_note = fpart_instance.get_note_from_chord_int(chord_int, 3) - 10; + if (OC::CONTROL_ENCODER_L == event.control) { + if (event.value > 0) { // increment A or B value depending on what is selected + if (fpart_instance.a_or_b) { //if channel A + if (a_note < 22) { + ++a_note; + } + } + else { // channel B + if (b_note < 22) { + ++b_note; + } + } + } else if (event.value < 0) { // decrement A or B value + if (fpart_instance.a_or_b) { // channel A + if (a_note > 0) { + --a_note; + } + } + else { // channel B + if (b_note > 0) { + --b_note; + } + } + } + } else if (OC::CONTROL_ENCODER_R == event.control) { // now the same for channels C and D + if (event.value > 0) { //increment C or D + if (fpart_instance.c_or_d) { + if (c_note < 22) { + ++c_note; + } + } + else { + if (d_note < 22) { + ++d_note; + } + } + } else if (event.value < 0) { // decrement C or D + if (fpart_instance.c_or_d) { + if (c_note > 0) { + --c_note; + } + } + else { + if (d_note > 0) { + --d_note; + } + } + } + } + //save the adjustment: build the chord integer, store it in the right place + int new_chord_int = fpart_instance.build_chord_int(a_note, b_note, c_note, d_note); + fpart_instance.set_chord_int(chordID, new_chord_int); + } else if (fpart_instance.get_menu_page() == FPART_MENU_PARAMETERS) { //on menu page, encoder scrolls through setting names/values + if (OC::CONTROL_ENCODER_R == event.control) { + if (fpart_instance.cursor.editing()) { //scroll setting values + fpart_instance.change_value(fpart_instance.cursor.cursor_pos(), event.value); + } else { // TODO: stop user scrolling past last drawn setting + fpart_instance.cursor.Scroll(event.value); //scroll setting names (i.e. rows) + } + } else if (OC::CONTROL_ENCODER_L == event.control){ + //left encoder behaviour in settings menu? + } + } +} + +void FPART_upButton() { + if (fpart_instance.get_menu_page() == FPART_MENU_STAFFS) { //staff page, down a chord + int chordID = fpart_instance.get_activechord(); + if (chordID > 0){ + --chordID; + fpart_instance.set_activechord(chordID); + } + else { + fpart_instance.set_activechord(98); //go to last chord + } + } + else { + //settings menu, scroll cursor up? + } +} + +void FPART_downButton() { + if (fpart_instance.get_menu_page() == FPART_MENU_STAFFS) { //staff page, up a chord + int chordID = fpart_instance.get_activechord(); + if (chordID < 98) { + ++chordID; + fpart_instance.set_activechord(chordID); + } + else { + fpart_instance.set_activechord(0); //go to first chord + } + } else { + //settings menu, scroll cursor down? + } +} + +void FPART_leftButton() { + if (fpart_instance.get_menu_page() == FPART_MENU_STAFFS) { //staff page, toggle channels A/B + fpart_instance.a_or_b = !fpart_instance.a_or_b; + } + else { + //menu stuff? + } +} + +void FPART_rightButton() { + if (fpart_instance.get_menu_page() == FPART_MENU_STAFFS) { + fpart_instance.c_or_d = !fpart_instance.c_or_d; //staff page, toggle channels C/D + } + else if (fpart_instance.get_menu_page() == FPART_MENU_PARAMETERS) { //menu page, toggle editing + fpart_instance.cursor.toggle_editing(); + } +} + +void FPART_leftButtonLong() { + if (fpart_instance.get_menu_page() == FPART_MENU_STAFFS) { //staff page, jump to first chord + if (fpart_instance.get_l_option() == 1) { + fpart_instance.set_activechord(0); + } else if (fpart_instance.copy) { + fpart_instance.copyChord(); + fpart_instance.copy = !fpart_instance.copy; + } else { + fpart_instance.pasteChord(); + fpart_instance.copy = !fpart_instance.copy; + } + } + else { + //do menu stuff? + } +} + +void FPART_downButtonLong() { // toggle between settings/staff pages + uint8_t _menu_page = fpart_instance.get_menu_page(); + if (_menu_page == FPART_MENU_PARAMETERS) { + fpart_instance.set_menu_page(FPART_MENU_STAFFS); + } + else { fpart_instance.set_menu_page(FPART_MENU_PARAMETERS); } +} + +#endif // ENABLE_APP_FPART diff --git a/software/o_c_REV/APP_H1200.ino b/software/o_c_REV/APP_H1200.ino new file mode 100644 index 000000000..c17d6d5d1 --- /dev/null +++ b/software/o_c_REV/APP_H1200.ino @@ -0,0 +1,1218 @@ +// Copyright (c) 2015, 2016 Patrick Dowling, Tim Churches +// +// Initial app implementation: Patrick Dowling (pld@gurkenkiste.com) +// Modifications by: Tim Churches (tim.churches@gmail.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// Trigger-driven Neo-Riemannian Tonnetz transformations to generate chords + +#ifdef ENABLE_APP_H1200 + +#include "OC_bitmaps.h" +#include "OC_strings.h" +#include "OC_trigger_delays.h" +#include "tonnetz/tonnetz_state.h" +#include "util/util_settings.h" +#include "util/util_ringbuffer.h" +#include "bjorklund.h" + +namespace menu = OC::menu; + +// NOTE: H1200 state is updated in the ISR, and we're accessing shared state +// (e.g. outputs) without any sync mechanism. So there is a chance of the +// display being slightly inconsistent but the risk seems acceptable. +// Similarly for changing settings, but this should also be safe(ish) since +// - Each setting should be atomic (int) +// - Changing more than one settings happens seldomly +// - Settings aren't modified in the ISR + +enum OutputMode { + OUTPUT_CHORD_VOICING, + OUTPUT_TUNE, + OUTPUT_MODE_LAST +}; + +enum TransformPriority { + TRANSFORM_PRIO_XPLR, + TRANSFORM_PRIO_XLRP, + TRANSFORM_PRIO_XRPL, + TRANSFORM_PRIO_XPRL, + TRANSFORM_PRIO_XRLP, + TRANSFORM_PRIO_XLPR, + TRANSFORM_PRIO_PLR_LAST +}; + +enum NshTransformPriority { + TRANSFORM_PRIO_XNSH, + TRANSFORM_PRIO_XSHN, + TRANSFORM_PRIO_XHNS, + TRANSFORM_PRIO_XNHS, + TRANSFORM_PRIO_XHSN, + TRANSFORM_PRIO_XSNH, + TRANSFORM_PRIO_NSH_LAST +}; + +enum H1200Setting { + H1200_SETTING_ROOT_OFFSET, + H1200_SETTING_ROOT_OFFSET_CV, + H1200_SETTING_OCTAVE, + H1200_SETTING_OCTAVE_CV, + H1200_SETTING_MODE, + H1200_SETTING_INVERSION, + H1200_SETTING_INVERSION_CV, + H1200_SETTING_PLR_TRANSFORM_PRIO, + H1200_SETTING_PLR_TRANSFORM_PRIO_CV, + H1200_SETTING_NSH_TRANSFORM_PRIO, + H1200_SETTING_NSH_TRANSFORM_PRIO_CV, + H1200_SETTING_CV_SAMPLING, + H1200_SETTING_OUTPUT_MODE, + H1200_SETTING_TRIGGER_DELAY, + H1200_SETTING_TRIGGER_TYPE, + H1200_SETTING_EUCLIDEAN_CV1_MAPPING, + H1200_SETTING_EUCLIDEAN_CV2_MAPPING, + H1200_SETTING_EUCLIDEAN_CV3_MAPPING, + H1200_SETTING_EUCLIDEAN_CV4_MAPPING, + H1200_SETTING_P_EUCLIDEAN_LENGTH, + H1200_SETTING_P_EUCLIDEAN_FILL, + H1200_SETTING_P_EUCLIDEAN_OFFSET, + H1200_SETTING_L_EUCLIDEAN_LENGTH, + H1200_SETTING_L_EUCLIDEAN_FILL, + H1200_SETTING_L_EUCLIDEAN_OFFSET, + H1200_SETTING_R_EUCLIDEAN_LENGTH, + H1200_SETTING_R_EUCLIDEAN_FILL, + H1200_SETTING_R_EUCLIDEAN_OFFSET, + H1200_SETTING_N_EUCLIDEAN_LENGTH, + H1200_SETTING_N_EUCLIDEAN_FILL, + H1200_SETTING_N_EUCLIDEAN_OFFSET, + H1200_SETTING_S_EUCLIDEAN_LENGTH, + H1200_SETTING_S_EUCLIDEAN_FILL, + H1200_SETTING_S_EUCLIDEAN_OFFSET, + H1200_SETTING_H_EUCLIDEAN_LENGTH, + H1200_SETTING_H_EUCLIDEAN_FILL, + H1200_SETTING_H_EUCLIDEAN_OFFSET, + H1200_SETTING_LAST +}; + +enum H1200CvSampling { + H1200_CV_SAMPLING_CONT, + H1200_CV_SAMPLING_TRIG, + H1200_CV_SAMPLING_LAST +}; + +enum H1200CvSource { + H1200_CV_SOURCE_NONE, + H1200_CV_SOURCE_CV1, + H1200_CV_SOURCE_CV2, + H1200_CV_SOURCE_CV3, + H1200_CV_SOURCE_CV4, + H1200_CV_SOURCE_LAST +}; + +enum H1200TriggerTypes { + H1200_TRIGGER_TYPE_PLR, + H1200_TRIGGER_TYPE_NSH, + H1200_TRIGGER_TYPE_EUCLIDEAN, + H1200_TRIGGER_TYPE_LAST +}; + +enum H1200EuclCvMappings { + H1200_EUCL_CV_MAPPING_NONE, + H1200_EUCL_CV_MAPPING_P_EUCLIDEAN_LENGTH, + H1200_EUCL_CV_MAPPING_P_EUCLIDEAN_FILL, + H1200_EUCL_CV_MAPPING_P_EUCLIDEAN_OFFSET, + H1200_EUCL_CV_MAPPING_L_EUCLIDEAN_LENGTH, + H1200_EUCL_CV_MAPPING_L_EUCLIDEAN_FILL, + H1200_EUCL_CV_MAPPING_L_EUCLIDEAN_OFFSET, + H1200_EUCL_CV_MAPPING_R_EUCLIDEAN_LENGTH, + H1200_EUCL_CV_MAPPING_R_EUCLIDEAN_FILL, + H1200_EUCL_CV_MAPPING_R_EUCLIDEAN_OFFSET, + H1200_EUCL_CV_MAPPING_N_EUCLIDEAN_LENGTH, + H1200_EUCL_CV_MAPPING_N_EUCLIDEAN_FILL, + H1200_EUCL_CV_MAPPING_N_EUCLIDEAN_OFFSET, + H1200_EUCL_CV_MAPPING_S_EUCLIDEAN_LENGTH, + H1200_EUCL_CV_MAPPING_S_EUCLIDEAN_FILL, + H1200_EUCL_CV_MAPPING_S_EUCLIDEAN_OFFSET, + H1200_EUCL_CV_MAPPING_H_EUCLIDEAN_LENGTH, + H1200_EUCL_CV_MAPPING_H_EUCLIDEAN_FILL, + H1200_EUCL_CV_MAPPING_H_EUCLIDEAN_OFFSET, + H1200_EUCL_CV_MAPPING_LAST +} ; + +class H1200Settings : public settings::SettingsBase { +public: + + H1200CvSampling get_cv_sampling() const { + return static_cast(values_[H1200_SETTING_CV_SAMPLING]); + } + + int root_offset() const { + return values_[H1200_SETTING_ROOT_OFFSET]; + } + + H1200CvSource get_root_offset_cv_src() const { + return static_cast(values_[H1200_SETTING_ROOT_OFFSET_CV]); + } + + int octave() const { + return values_[H1200_SETTING_OCTAVE]; + } + + H1200CvSource get_octave_cv_src() const { + return static_cast(values_[H1200_SETTING_OCTAVE_CV]); + } + + EMode mode() const { + return static_cast(values_[H1200_SETTING_MODE]); + } + + int inversion() const { + return values_[H1200_SETTING_INVERSION]; + } + + H1200CvSource get_inversion_cv_src() const { + return static_cast(values_[H1200_SETTING_INVERSION_CV]); + } + + uint8_t get_transform_priority() const { + return values_[H1200_SETTING_PLR_TRANSFORM_PRIO]; + } + + H1200CvSource get_transform_priority_cv_src() const { + return static_cast(values_[H1200_SETTING_PLR_TRANSFORM_PRIO_CV]); + } + + uint8_t get_nsh_transform_priority() const { + return values_[H1200_SETTING_NSH_TRANSFORM_PRIO]; + } + + H1200CvSource get_nsh_transform_priority_cv_src() const { + return static_cast(values_[H1200_SETTING_NSH_TRANSFORM_PRIO_CV]); + } + + OutputMode output_mode() const { + return static_cast(values_[H1200_SETTING_OUTPUT_MODE]); + } + + H1200TriggerTypes get_trigger_type() const { + return static_cast(values_[H1200_SETTING_TRIGGER_TYPE]); + } + + uint16_t get_trigger_delay() const { + return values_[H1200_SETTING_TRIGGER_DELAY]; + } + + uint8_t get_euclidean_cv1_mapping() const { + return values_[H1200_SETTING_EUCLIDEAN_CV1_MAPPING]; + } + + uint8_t get_euclidean_cv2_mapping() const { + return values_[H1200_SETTING_EUCLIDEAN_CV2_MAPPING]; + } + + uint8_t get_euclidean_cv3_mapping() const { + return values_[H1200_SETTING_EUCLIDEAN_CV3_MAPPING]; + } + + uint8_t get_euclidean_cv4_mapping() const { + return values_[H1200_SETTING_EUCLIDEAN_CV4_MAPPING]; + } + + uint8_t get_p_euclidean_length() const { + return values_[H1200_SETTING_P_EUCLIDEAN_LENGTH]; + } + + uint8_t get_p_euclidean_fill() const { + return values_[H1200_SETTING_P_EUCLIDEAN_FILL]; + } + + uint8_t get_p_euclidean_offset() const { + return values_[H1200_SETTING_P_EUCLIDEAN_OFFSET]; + } + + uint8_t get_l_euclidean_length() const { + return values_[H1200_SETTING_L_EUCLIDEAN_LENGTH]; + } + + uint8_t get_l_euclidean_fill() const { + return values_[H1200_SETTING_L_EUCLIDEAN_FILL]; + } + + uint8_t get_l_euclidean_offset() const { + return values_[H1200_SETTING_L_EUCLIDEAN_OFFSET]; + } + + uint8_t get_r_euclidean_length() const { + return values_[H1200_SETTING_R_EUCLIDEAN_LENGTH]; + } + + uint8_t get_r_euclidean_fill() const { + return values_[H1200_SETTING_R_EUCLIDEAN_FILL]; + } + + uint8_t get_r_euclidean_offset() const { + return values_[H1200_SETTING_R_EUCLIDEAN_OFFSET]; + } + + uint8_t get_n_euclidean_length() const { + return values_[H1200_SETTING_N_EUCLIDEAN_LENGTH]; + } + + uint8_t get_n_euclidean_fill() const { + return values_[H1200_SETTING_N_EUCLIDEAN_FILL]; + } + + uint8_t get_n_euclidean_offset() const { + return values_[H1200_SETTING_N_EUCLIDEAN_OFFSET]; + } + + uint8_t get_s_euclidean_length() const { + return values_[H1200_SETTING_S_EUCLIDEAN_LENGTH]; + } + + uint8_t get_s_euclidean_fill() const { + return values_[H1200_SETTING_S_EUCLIDEAN_FILL]; + } + + uint8_t get_s_euclidean_offset() const { + return values_[H1200_SETTING_S_EUCLIDEAN_OFFSET]; + } + + uint8_t get_h_euclidean_length() const { + return values_[H1200_SETTING_H_EUCLIDEAN_LENGTH]; + } + + uint8_t get_h_euclidean_fill() const { + return values_[H1200_SETTING_H_EUCLIDEAN_FILL]; + } + + uint8_t get_h_euclidean_offset() const { + return values_[H1200_SETTING_H_EUCLIDEAN_OFFSET]; + } + + + void Init() { + InitDefaults(); + update_enabled_settings(); + manual_mode_change_ = false; + } + + H1200Setting enabled_setting_at(int index) const { + return enabled_settings_[index]; + } + + int num_enabled_settings() const { + return num_enabled_settings_; + } + + void mode_change(bool yn) { + manual_mode_change_ = yn; + } + + bool mode_manual_change() const { + return manual_mode_change_; + } + + void update_enabled_settings() { + + H1200Setting *settings = enabled_settings_; + + *settings++ = H1200_SETTING_ROOT_OFFSET; + *settings++ = H1200_SETTING_ROOT_OFFSET_CV; + *settings++ = H1200_SETTING_OCTAVE; + *settings++ = H1200_SETTING_OCTAVE_CV; + *settings++ = H1200_SETTING_MODE; + *settings++ = H1200_SETTING_INVERSION; + *settings++ = H1200_SETTING_INVERSION_CV; + *settings++ = H1200_SETTING_PLR_TRANSFORM_PRIO; + *settings++ = H1200_SETTING_PLR_TRANSFORM_PRIO_CV; + *settings++ = H1200_SETTING_NSH_TRANSFORM_PRIO; + *settings++ = H1200_SETTING_NSH_TRANSFORM_PRIO_CV; + *settings++ = H1200_SETTING_CV_SAMPLING; + *settings++ = H1200_SETTING_OUTPUT_MODE; + *settings++ = H1200_SETTING_TRIGGER_DELAY; + *settings++ = H1200_SETTING_TRIGGER_TYPE; + + switch (get_trigger_type()) { + case H1200_TRIGGER_TYPE_EUCLIDEAN: + *settings++ = H1200_SETTING_EUCLIDEAN_CV1_MAPPING; + *settings++ = H1200_SETTING_EUCLIDEAN_CV2_MAPPING; + *settings++ = H1200_SETTING_EUCLIDEAN_CV3_MAPPING; + *settings++ = H1200_SETTING_EUCLIDEAN_CV4_MAPPING; + *settings++ = H1200_SETTING_P_EUCLIDEAN_LENGTH; + *settings++ = H1200_SETTING_P_EUCLIDEAN_FILL; + *settings++ = H1200_SETTING_P_EUCLIDEAN_OFFSET; + *settings++ = H1200_SETTING_L_EUCLIDEAN_LENGTH; + *settings++ = H1200_SETTING_L_EUCLIDEAN_FILL; + *settings++ = H1200_SETTING_L_EUCLIDEAN_OFFSET; + *settings++ = H1200_SETTING_R_EUCLIDEAN_LENGTH; + *settings++ = H1200_SETTING_R_EUCLIDEAN_FILL; + *settings++ = H1200_SETTING_R_EUCLIDEAN_OFFSET; + *settings++ = H1200_SETTING_N_EUCLIDEAN_LENGTH; + *settings++ = H1200_SETTING_N_EUCLIDEAN_FILL; + *settings++ = H1200_SETTING_N_EUCLIDEAN_OFFSET; + *settings++ = H1200_SETTING_S_EUCLIDEAN_LENGTH; + *settings++ = H1200_SETTING_S_EUCLIDEAN_FILL; + *settings++ = H1200_SETTING_S_EUCLIDEAN_OFFSET; + *settings++ = H1200_SETTING_H_EUCLIDEAN_LENGTH; + *settings++ = H1200_SETTING_H_EUCLIDEAN_FILL; + *settings++ = H1200_SETTING_H_EUCLIDEAN_OFFSET; + break; + default: + break; + } + + num_enabled_settings_ = settings - enabled_settings_; + } + +private: + int num_enabled_settings_; + bool manual_mode_change_; + H1200Setting enabled_settings_[H1200_SETTING_LAST]; +}; + +const char * const output_mode_names[] = { + "Chord", + "Tune" +}; + +const char * const plr_trigger_mode_names[] = { + "P>L>R", + "L>R>P", + "R>P>L", + "P>R>L", + "R>L>P", + "L>P>R", +}; + +const char * const nsh_trigger_mode_names[] = { + "N>S>H", + "S>H>N", + "H>N>S", + "N>H>S", + "H>S>N", + "S>N>H", +}; + +const char * const trigger_type_names[] = { + "PLR", + "NSH", + "Eucl", +}; + +const char * const mode_names[] = { + "Maj", "Min" +}; + +const char* const h1200_cv_sampling[2] = { + "Cont", "Trig" +}; + +const char* const h1200_eucl_cv_mappings[] = { + "None", + "Plen", "Pfil", "Poff", + "Llen", "Lfil", "Loff", + "Rlen", "Rfil", "Roff", + "Nlen", "Nfil", "Noff", + "Slen", "Sfil", "Soff", + "Hlen", "Hfil", "Hoff", +}; + +// TOTAL EEPROM SIZE: 37 bytes +SETTINGS_DECLARE(H1200Settings, H1200_SETTING_LAST) { + {0, -11, 11, "Transpose", NULL, settings::STORAGE_TYPE_I8}, + {H1200_CV_SOURCE_NONE, H1200_CV_SOURCE_NONE, H1200_CV_SOURCE_LAST-1, "Transpose CV", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U8}, + #ifdef BUCHLA_4U + {0, 0, 7, "Octave", NULL, settings::STORAGE_TYPE_I8}, + #else + {0, -3, 3, "Octave", NULL, settings::STORAGE_TYPE_I8}, + #endif + {H1200_CV_SOURCE_NONE, H1200_CV_SOURCE_NONE, H1200_CV_SOURCE_LAST-1, "Octave CV", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U8}, + {MODE_MAJOR, 0, MODE_LAST-1, "Root mode", mode_names, settings::STORAGE_TYPE_U8}, + {0, -3, 3, "Inversion", NULL, settings::STORAGE_TYPE_I8}, + {H1200_CV_SOURCE_NONE, H1200_CV_SOURCE_NONE, H1200_CV_SOURCE_LAST-1, "Inversion CV", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U8}, + {TRANSFORM_PRIO_XPLR, 0, TRANSFORM_PRIO_PLR_LAST-1, "PLR Priority", plr_trigger_mode_names, settings::STORAGE_TYPE_U8}, + {H1200_CV_SOURCE_NONE, H1200_CV_SOURCE_NONE, H1200_CV_SOURCE_LAST-1, "PLR Prior CV", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U8}, + {TRANSFORM_PRIO_XNSH, 0, TRANSFORM_PRIO_NSH_LAST-1, "NSH Priority", nsh_trigger_mode_names, settings::STORAGE_TYPE_U8}, + {H1200_CV_SOURCE_NONE, H1200_CV_SOURCE_NONE, H1200_CV_SOURCE_LAST-1, "NSH Prior CV", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U8}, + {H1200_CV_SAMPLING_CONT, H1200_CV_SAMPLING_CONT, H1200_CV_SAMPLING_LAST-1, "CV sampling", h1200_cv_sampling, settings::STORAGE_TYPE_U8}, + {OUTPUT_CHORD_VOICING, 0, OUTPUT_MODE_LAST-1, "Output mode", output_mode_names, settings::STORAGE_TYPE_U8}, + { 0, 0, OC::kNumDelayTimes - 1, "Trigger delay", OC::Strings::trigger_delay_times, settings::STORAGE_TYPE_U8 }, + {H1200_TRIGGER_TYPE_PLR, 0, H1200_TRIGGER_TYPE_LAST-1, "Trigger type", trigger_type_names, settings::STORAGE_TYPE_U8}, + {H1200_EUCL_CV_MAPPING_NONE, H1200_EUCL_CV_MAPPING_NONE, H1200_EUCL_CV_MAPPING_LAST-1, "Eucl CV1 map", h1200_eucl_cv_mappings, settings::STORAGE_TYPE_U8}, + {H1200_EUCL_CV_MAPPING_NONE, H1200_EUCL_CV_MAPPING_NONE, H1200_EUCL_CV_MAPPING_LAST-1, "Eucl CV2 map", h1200_eucl_cv_mappings, settings::STORAGE_TYPE_U8}, + {H1200_EUCL_CV_MAPPING_NONE, H1200_EUCL_CV_MAPPING_NONE, H1200_EUCL_CV_MAPPING_LAST-1, "Eucl CV3 map", h1200_eucl_cv_mappings, settings::STORAGE_TYPE_U8}, + {H1200_EUCL_CV_MAPPING_NONE, H1200_EUCL_CV_MAPPING_NONE, H1200_EUCL_CV_MAPPING_LAST-1, "Eucl CV4 map", h1200_eucl_cv_mappings, settings::STORAGE_TYPE_U8}, + { 8, 2, 32, " P EuLeng", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 32, " P EuFill", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 31, " P EuOffs", NULL, settings::STORAGE_TYPE_U8 }, + { 8, 2, 32, " L EuLeng", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 32, " L EuFill", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 31, " L EuOffs", NULL, settings::STORAGE_TYPE_U8 }, + { 8, 2, 32, " R EuLeng", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 32, " R EuFill", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 31, " R EuOffs", NULL, settings::STORAGE_TYPE_U8 }, + { 8, 2, 32, " N EuLeng", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 32, " N EuFill", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 31, " N EuOffs", NULL, settings::STORAGE_TYPE_U8 }, + { 8, 2, 32, " S EuLeng", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 32, " S EuFill", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 31, " S EuOffs", NULL, settings::STORAGE_TYPE_U8 }, + { 8, 2, 32, " H EuLeng", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 32, " H EuFill", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 31, " H EuOffs", NULL, settings::STORAGE_TYPE_U8 }, +}; + +static constexpr uint32_t TRIGGER_MASK_TR1 = OC::DIGITAL_INPUT_1_MASK; +static constexpr uint32_t TRIGGER_MASK_P = OC::DIGITAL_INPUT_2_MASK; +static constexpr uint32_t TRIGGER_MASK_L = OC::DIGITAL_INPUT_3_MASK; +static constexpr uint32_t TRIGGER_MASK_R = OC::DIGITAL_INPUT_4_MASK; +static constexpr uint32_t TRIGGER_MASK_N = OC::DIGITAL_INPUT_2_MASK; +static constexpr uint32_t TRIGGER_MASK_S = OC::DIGITAL_INPUT_3_MASK; +static constexpr uint32_t TRIGGER_MASK_H = OC::DIGITAL_INPUT_4_MASK; +static constexpr uint32_t TRIGGER_MASK_DIRTY = 0x10; +static constexpr uint32_t TRIGGER_MASK_RESET = TRIGGER_MASK_TR1 | TRIGGER_MASK_DIRTY; + +namespace H1200 { + enum UserActions { + ACTION_FORCE_UPDATE, + ACTION_MANUAL_RESET + }; + + typedef uint32_t UiAction; +}; + +H1200Settings h1200_settings; + +class H1200State { +public: + static constexpr int kMaxInversion = 3; + + void Init() { + cursor.Init(H1200_SETTING_ROOT_OFFSET, H1200_SETTING_LAST - 1); + display_notes = true; + + quantizer.Init(); + tonnetz_state.init(); + trigger_delays_.Init(); + + euclidean_counter_ = 0; + root_sample_ = false; + root_ = 0; + p_euclidean_length_ = 8; + p_euclidean_fill_ = 0; + p_euclidean_offset_ = 0; + l_euclidean_length_ = 8; + l_euclidean_fill_ = 0; + l_euclidean_offset_ = 0; + r_euclidean_length_ = 8; + r_euclidean_fill_ = 0; + r_euclidean_offset_ = 0; + n_euclidean_length_ = 8; + n_euclidean_fill_ = 0; + n_euclidean_offset_ = 0; + s_euclidean_length_ = 8; + s_euclidean_fill_ = 0; + s_euclidean_offset_ = 0; + h_euclidean_length_ = 8; + h_euclidean_fill_ = 0; + h_euclidean_offset_ = 0; + + } + + void map_euclidean_cv(uint8_t cv_mapping, int channel_cv) { + switch(cv_mapping) { + case H1200_EUCL_CV_MAPPING_P_EUCLIDEAN_LENGTH: + p_euclidean_length_ = p_euclidean_length_ + channel_cv; + CONSTRAIN(p_euclidean_length_, 2, 32); + break; + case H1200_EUCL_CV_MAPPING_P_EUCLIDEAN_FILL: + p_euclidean_fill_ = p_euclidean_fill_ + channel_cv; + CONSTRAIN(p_euclidean_fill_, 0, 32); + break; + case H1200_EUCL_CV_MAPPING_P_EUCLIDEAN_OFFSET: + p_euclidean_offset_ = p_euclidean_offset_ + channel_cv; + CONSTRAIN(p_euclidean_offset_, 0, 31); + break; + case H1200_EUCL_CV_MAPPING_L_EUCLIDEAN_LENGTH: + l_euclidean_length_ = l_euclidean_length_ + channel_cv; + CONSTRAIN(l_euclidean_length_, 2, 32); + break; + case H1200_EUCL_CV_MAPPING_L_EUCLIDEAN_FILL: + l_euclidean_fill_ = l_euclidean_fill_ + channel_cv; + CONSTRAIN(l_euclidean_fill_, 0, 32); + break; + case H1200_EUCL_CV_MAPPING_L_EUCLIDEAN_OFFSET: + l_euclidean_offset_ = l_euclidean_offset_ + channel_cv; + CONSTRAIN(l_euclidean_offset_, 0, 31); + break; + case H1200_EUCL_CV_MAPPING_R_EUCLIDEAN_LENGTH: + r_euclidean_length_ = r_euclidean_length_ + channel_cv; + CONSTRAIN(r_euclidean_length_, 2, 32); + break; + case H1200_EUCL_CV_MAPPING_R_EUCLIDEAN_FILL: + r_euclidean_fill_ = r_euclidean_fill_ + channel_cv; + CONSTRAIN(r_euclidean_fill_, 0, 32); + break; + case H1200_EUCL_CV_MAPPING_R_EUCLIDEAN_OFFSET: + r_euclidean_offset_ = r_euclidean_offset_ + channel_cv; + CONSTRAIN(r_euclidean_offset_, 0, 31); + break; + case H1200_EUCL_CV_MAPPING_N_EUCLIDEAN_LENGTH: + n_euclidean_length_ = n_euclidean_length_ + channel_cv; + CONSTRAIN(n_euclidean_length_, 2, 32); + break; + case H1200_EUCL_CV_MAPPING_N_EUCLIDEAN_FILL: + n_euclidean_fill_ = n_euclidean_fill_ + channel_cv; + CONSTRAIN(n_euclidean_fill_, 0, 32); + break; + case H1200_EUCL_CV_MAPPING_N_EUCLIDEAN_OFFSET: + n_euclidean_offset_ = n_euclidean_offset_ + channel_cv; + CONSTRAIN(n_euclidean_offset_, 0, 31); + break; + case H1200_EUCL_CV_MAPPING_S_EUCLIDEAN_LENGTH: + s_euclidean_length_ = s_euclidean_length_ + channel_cv; + CONSTRAIN(s_euclidean_length_, 2, 32); + break; + case H1200_EUCL_CV_MAPPING_S_EUCLIDEAN_FILL: + s_euclidean_fill_ = s_euclidean_fill_ + channel_cv; + CONSTRAIN(s_euclidean_fill_, 0, 32); + break; + case H1200_EUCL_CV_MAPPING_S_EUCLIDEAN_OFFSET: + s_euclidean_offset_ = s_euclidean_offset_ + channel_cv; + CONSTRAIN(s_euclidean_offset_, 0, 31); + break; + case H1200_EUCL_CV_MAPPING_H_EUCLIDEAN_LENGTH: + h_euclidean_length_ = h_euclidean_length_ + channel_cv; + CONSTRAIN(h_euclidean_length_, 2, 32); + break; + case H1200_EUCL_CV_MAPPING_H_EUCLIDEAN_FILL: + h_euclidean_fill_ = h_euclidean_fill_ + channel_cv; + CONSTRAIN(h_euclidean_fill_, 0, 32); + break; + case H1200_EUCL_CV_MAPPING_H_EUCLIDEAN_OFFSET: + h_euclidean_offset_ = h_euclidean_offset_ + channel_cv; + CONSTRAIN(h_euclidean_offset_, 0, 31); + break; + default: + break; + } + } + + void force_update() { + ui_actions.Write(H1200::ACTION_FORCE_UPDATE); + } + + void manual_reset() { + ui_actions.Write(H1200::ACTION_MANUAL_RESET); + } + + void Render(int32_t root, int inversion, int octave, OutputMode output_mode) { + tonnetz_state.render(root + octave * 12, inversion); + + switch (output_mode) { + case OUTPUT_CHORD_VOICING: { + OC::DAC::set_voltage_scaled_semitone(tonnetz_state.outputs(0), 0, OC::DAC::get_voltage_scaling(DAC_CHANNEL_A)); + OC::DAC::set_voltage_scaled_semitone(tonnetz_state.outputs(1), 0, OC::DAC::get_voltage_scaling(DAC_CHANNEL_B)); + OC::DAC::set_voltage_scaled_semitone(tonnetz_state.outputs(2), 0, OC::DAC::get_voltage_scaling(DAC_CHANNEL_C)); + OC::DAC::set_voltage_scaled_semitone(tonnetz_state.outputs(3), 0, OC::DAC::get_voltage_scaling(DAC_CHANNEL_D)); + } + break; + case OUTPUT_TUNE: { + OC::DAC::set_voltage_scaled_semitone(tonnetz_state.outputs(0), 0, OC::DAC::get_voltage_scaling(DAC_CHANNEL_A)); + OC::DAC::set_voltage_scaled_semitone(tonnetz_state.outputs(0), 0, OC::DAC::get_voltage_scaling(DAC_CHANNEL_B)); + OC::DAC::set_voltage_scaled_semitone(tonnetz_state.outputs(0), 0, OC::DAC::get_voltage_scaling(DAC_CHANNEL_C)); + OC::DAC::set_voltage_scaled_semitone(tonnetz_state.outputs(0), 0, OC::DAC::get_voltage_scaling(DAC_CHANNEL_D)); + } + break; + default: break; + } + } + + menu::ScreenCursor cursor; + menu::ScreenCursor cursor_state; + bool display_notes; + + inline int cursor_pos() const { + return cursor.cursor_pos(); + } + + OC::SemitoneQuantizer quantizer; + TonnetzState tonnetz_state; + util::RingBuffer ui_actions; + OC::TriggerDelays trigger_delays_; + uint32_t euclidean_counter_; + bool root_sample_ ; + int32_t root_ ; + uint8_t p_euclidean_length_ ; + uint8_t p_euclidean_fill_ ; + uint8_t p_euclidean_offset_ ; + uint8_t l_euclidean_length_ ; + uint8_t l_euclidean_fill_ ; + uint8_t l_euclidean_offset_ ; + uint8_t r_euclidean_length_ ; + uint8_t r_euclidean_fill_ ; + uint8_t r_euclidean_offset_ ; + uint8_t n_euclidean_length_ ; + uint8_t n_euclidean_fill_ ; + uint8_t n_euclidean_offset_ ; + uint8_t s_euclidean_length_ ; + uint8_t s_euclidean_fill_ ; + uint8_t s_euclidean_offset_ ; + uint8_t h_euclidean_length_ ; + uint8_t h_euclidean_fill_ ; + uint8_t h_euclidean_offset_ ; + +}; + +H1200State h1200_state; + +void FASTRUN H1200_clock(uint32_t triggers) { + + triggers = h1200_state.trigger_delays_.Process(triggers, OC::trigger_delay_ticks[h1200_settings.get_trigger_delay()]); + + // Reset has priority + if (triggers & TRIGGER_MASK_TR1) { + h1200_state.tonnetz_state.reset(h1200_settings.mode()); + h1200_settings.mode_change(false); + } + + // Reset on next trigger = manual change min/maj + if (h1200_settings.mode_manual_change()) { + + if ((triggers & OC::DIGITAL_INPUT_2_MASK) || (triggers & OC::DIGITAL_INPUT_3_MASK) || (triggers & OC::DIGITAL_INPUT_4_MASK)) { + h1200_settings.mode_change(false); + h1200_state.tonnetz_state.reset(h1200_settings.mode()); + } + } + + int32_t root_ = h1200_settings.root_offset(); + int8_t octave_ = h1200_settings.octave(); + int inversion_ = h1200_settings.inversion(); + uint8_t plr_transform_priority_ = h1200_settings.get_transform_priority(); + uint8_t nsh_transform_priority_ = h1200_settings.get_nsh_transform_priority(); + + if (triggers || (h1200_settings.get_cv_sampling() == H1200_CV_SAMPLING_CONT)) { + switch (h1200_settings.get_root_offset_cv_src()) { + case H1200_CV_SOURCE_CV1: + root_ += h1200_state.quantizer.Process(OC::ADC::raw_pitch_value(ADC_CHANNEL_1)); + break ; + case H1200_CV_SOURCE_CV2: + root_ += h1200_state.quantizer.Process(OC::ADC::raw_pitch_value(ADC_CHANNEL_2)); + break ; + case H1200_CV_SOURCE_CV3: + root_ += h1200_state.quantizer.Process(OC::ADC::raw_pitch_value(ADC_CHANNEL_3)); + break ; + case H1200_CV_SOURCE_CV4: + root_ += h1200_state.quantizer.Process(OC::ADC::raw_pitch_value(ADC_CHANNEL_4)); + break ; + default: + break; + } + + switch (h1200_settings.get_octave_cv_src()) { + case H1200_CV_SOURCE_CV1: + octave_ += ((OC::ADC::value() + 255) >> 9); + break ; + case H1200_CV_SOURCE_CV2: + octave_ += ((OC::ADC::value() + 255) >> 9); + break ; + case H1200_CV_SOURCE_CV3: + octave_ += ((OC::ADC::value() + 255) >> 9); + break ; + case H1200_CV_SOURCE_CV4: + octave_ += ((OC::ADC::value() + 255) >> 9); + break ; + default: + break; + } + #ifdef BUCHLA_4U + CONSTRAIN(octave_, 0, 7); + #else + CONSTRAIN(octave_, -3, 3); + #endif + + switch (h1200_settings.get_inversion_cv_src()) { + case H1200_CV_SOURCE_CV1: + inversion_ += ((OC::ADC::value() + 255) >> 9); + break ; + case H1200_CV_SOURCE_CV2: + inversion_ += ((OC::ADC::value() + 255) >> 9); + break ; + case H1200_CV_SOURCE_CV3: + inversion_ += ((OC::ADC::value() + 255) >> 9); + break ; + case H1200_CV_SOURCE_CV4: + inversion_ += ((OC::ADC::value() + 255) >> 9); + break ; + default: + break; + } + CONSTRAIN(inversion_,-H1200State::kMaxInversion, H1200State::kMaxInversion); + + switch (h1200_settings.get_transform_priority_cv_src()) { + case H1200_CV_SOURCE_CV1: + plr_transform_priority_ += ((OC::ADC::value() + 255) >> 9); + break ; + case H1200_CV_SOURCE_CV2: + plr_transform_priority_ += ((OC::ADC::value() + 255) >> 9); + break ; + case H1200_CV_SOURCE_CV3: + plr_transform_priority_ += ((OC::ADC::value() + 255) >> 9); + break ; + case H1200_CV_SOURCE_CV4: + plr_transform_priority_ += ((OC::ADC::value() + 255) >> 9); + break ; + default: + break; + } + + CONSTRAIN(plr_transform_priority_, TRANSFORM_PRIO_XPLR, TRANSFORM_PRIO_PLR_LAST-1); + + switch (h1200_settings.get_nsh_transform_priority_cv_src()) { + case H1200_CV_SOURCE_CV1: + nsh_transform_priority_ += ((OC::ADC::value() + 255) >> 9); + break ; + case H1200_CV_SOURCE_CV2: + nsh_transform_priority_ += ((OC::ADC::value() + 255) >> 9); + break ; + case H1200_CV_SOURCE_CV3: + nsh_transform_priority_ += ((OC::ADC::value() + 255) >> 9); + break ; + case H1200_CV_SOURCE_CV4: + nsh_transform_priority_ += ((OC::ADC::value() + 255) >> 9); + break ; + default: + break; + } + + CONSTRAIN(nsh_transform_priority_, TRANSFORM_PRIO_XNSH, TRANSFORM_PRIO_NSH_LAST-1); + } + + if (h1200_settings.get_trigger_type() == H1200_TRIGGER_TYPE_PLR) { + + // Since there can be simultaneous triggers, there is a definable priority. + // Reset always has top priority + + switch (plr_transform_priority_) { + case TRANSFORM_PRIO_XPLR: + if (triggers & TRIGGER_MASK_P) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_P); + if (triggers & TRIGGER_MASK_L) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_L); + if (triggers & TRIGGER_MASK_R) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_R); + break; + + case TRANSFORM_PRIO_XLRP: + if (triggers & TRIGGER_MASK_L) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_L); + if (triggers & TRIGGER_MASK_R) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_R); + if (triggers & TRIGGER_MASK_P) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_P); + break; + + case TRANSFORM_PRIO_XRPL: + if (triggers & TRIGGER_MASK_R) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_R); + if (triggers & TRIGGER_MASK_P) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_P); + if (triggers & TRIGGER_MASK_L) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_L); + break; + + case TRANSFORM_PRIO_XPRL: + if (triggers & TRIGGER_MASK_P) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_P); + if (triggers & TRIGGER_MASK_R) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_R); + if (triggers & TRIGGER_MASK_L) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_L); + break; + + case TRANSFORM_PRIO_XRLP: + if (triggers & TRIGGER_MASK_R) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_R); + if (triggers & TRIGGER_MASK_L) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_L); + if (triggers & TRIGGER_MASK_P) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_P); + break; + + case TRANSFORM_PRIO_XLPR: + if (triggers & TRIGGER_MASK_L) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_L); + if (triggers & TRIGGER_MASK_P) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_P); + if (triggers & TRIGGER_MASK_R) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_R); + break; + + default: break; + } + } else if (h1200_settings.get_trigger_type() == H1200_TRIGGER_TYPE_NSH) { + + // Since there can be simultaneous triggers, there is a definable priority. + // Reset always has top priority + + switch (nsh_transform_priority_) { + case TRANSFORM_PRIO_XNSH: + if (triggers & TRIGGER_MASK_N) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_N); + if (triggers & TRIGGER_MASK_S) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_S); + if (triggers & TRIGGER_MASK_H) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_H); + break; + + case TRANSFORM_PRIO_XSHN: + if (triggers & TRIGGER_MASK_S) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_S); + if (triggers & TRIGGER_MASK_H) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_H); + if (triggers & TRIGGER_MASK_N) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_N); + break; + + case TRANSFORM_PRIO_XHNS: + if (triggers & TRIGGER_MASK_H) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_H); + if (triggers & TRIGGER_MASK_N) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_N); + if (triggers & TRIGGER_MASK_S) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_S); + break; + + case TRANSFORM_PRIO_XNHS: + if (triggers & TRIGGER_MASK_N) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_N); + if (triggers & TRIGGER_MASK_H) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_H); + if (triggers & TRIGGER_MASK_S) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_S); + break; + + case TRANSFORM_PRIO_XHSN: + if (triggers & TRIGGER_MASK_H) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_H); + if (triggers & TRIGGER_MASK_S) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_S); + if (triggers & TRIGGER_MASK_N) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_N); + break; + + case TRANSFORM_PRIO_XSNH: + if (triggers & TRIGGER_MASK_S) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_S); + if (triggers & TRIGGER_MASK_N) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_N); + if (triggers & TRIGGER_MASK_H) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_H); + break; + + default: break; + } + } else { + + if (triggers) { + + h1200_state.p_euclidean_length_ = h1200_settings.get_p_euclidean_length() ; + h1200_state.p_euclidean_fill_ = h1200_settings.get_p_euclidean_fill() ; + h1200_state.p_euclidean_offset_ = h1200_settings.get_p_euclidean_offset() ; + h1200_state.l_euclidean_length_ = h1200_settings.get_l_euclidean_length() ; + h1200_state.l_euclidean_fill_ = h1200_settings.get_l_euclidean_fill() ; + h1200_state.l_euclidean_offset_ = h1200_settings.get_l_euclidean_offset() ; + h1200_state.r_euclidean_length_ = h1200_settings.get_r_euclidean_length() ; + h1200_state.r_euclidean_fill_ = h1200_settings.get_r_euclidean_fill() ; + h1200_state.r_euclidean_offset_ = h1200_settings.get_r_euclidean_offset() ; + h1200_state.n_euclidean_length_ = h1200_settings.get_n_euclidean_length() ; + h1200_state.n_euclidean_fill_ = h1200_settings.get_n_euclidean_fill() ; + h1200_state.n_euclidean_offset_ = h1200_settings.get_n_euclidean_offset() ; + h1200_state.s_euclidean_length_ = h1200_settings.get_s_euclidean_length() ; + h1200_state.s_euclidean_fill_ = h1200_settings.get_s_euclidean_fill() ; + h1200_state.s_euclidean_offset_ = h1200_settings.get_s_euclidean_offset() ; + h1200_state.h_euclidean_length_ = h1200_settings.get_h_euclidean_length() ; + h1200_state.h_euclidean_fill_ = h1200_settings.get_h_euclidean_fill() ; + h1200_state.h_euclidean_offset_ = h1200_settings.get_h_euclidean_offset() ; + + int channel_1_cv_ = ((OC::ADC::value() + 127) >> 8); + int channel_2_cv_ = ((OC::ADC::value() + 127) >> 8); + int channel_3_cv_ = ((OC::ADC::value() + 127) >> 8); + int channel_4_cv_ = ((OC::ADC::value() + 127) >> 8); + + + h1200_state.map_euclidean_cv(h1200_settings.get_euclidean_cv1_mapping(), channel_1_cv_) ; + h1200_state.map_euclidean_cv(h1200_settings.get_euclidean_cv2_mapping(), channel_2_cv_) ; + h1200_state.map_euclidean_cv(h1200_settings.get_euclidean_cv3_mapping(), channel_3_cv_) ; + h1200_state.map_euclidean_cv(h1200_settings.get_euclidean_cv4_mapping(), channel_4_cv_) ; + + ++h1200_state.euclidean_counter_; + + switch (plr_transform_priority_) { + case TRANSFORM_PRIO_XPLR: + if (EuclideanFilter(h1200_state.p_euclidean_length_, h1200_state.p_euclidean_fill_, h1200_state.p_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_P); + if (EuclideanFilter(h1200_state.l_euclidean_length_, h1200_state.l_euclidean_fill_, h1200_state.l_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_L); + if (EuclideanFilter(h1200_state.r_euclidean_length_, h1200_state.r_euclidean_fill_, h1200_state.r_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_R); + break; + case TRANSFORM_PRIO_XLRP: + if (EuclideanFilter(h1200_state.l_euclidean_length_, h1200_state.l_euclidean_fill_, h1200_state.l_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_L); + if (EuclideanFilter(h1200_state.r_euclidean_length_, h1200_state.r_euclidean_fill_, h1200_state.r_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_R); + if (EuclideanFilter(h1200_state.p_euclidean_length_, h1200_state.p_euclidean_fill_, h1200_state.p_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_P); + break; + case TRANSFORM_PRIO_XRPL: + if (EuclideanFilter(h1200_state.r_euclidean_length_, h1200_state.r_euclidean_fill_, h1200_state.r_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_R); + if (EuclideanFilter(h1200_state.p_euclidean_length_, h1200_state.p_euclidean_fill_, h1200_state.p_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_P); + if (EuclideanFilter(h1200_state.l_euclidean_length_, h1200_state.l_euclidean_fill_, h1200_state.l_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_L); + break; + case TRANSFORM_PRIO_XPRL: + if (EuclideanFilter(h1200_state.p_euclidean_length_, h1200_state.p_euclidean_fill_, h1200_state.p_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_P); + if (EuclideanFilter(h1200_state.r_euclidean_length_, h1200_state.r_euclidean_fill_, h1200_state.r_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_R); + if (EuclideanFilter(h1200_state.l_euclidean_length_, h1200_state.l_euclidean_fill_, h1200_state.l_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_L); + break; + case TRANSFORM_PRIO_XRLP: + if (EuclideanFilter(h1200_state.r_euclidean_length_, h1200_state.r_euclidean_fill_, h1200_state.r_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_R); + if (EuclideanFilter(h1200_state.l_euclidean_length_, h1200_state.l_euclidean_fill_, h1200_state.l_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_L); + if (EuclideanFilter(h1200_state.p_euclidean_length_, h1200_state.p_euclidean_fill_, h1200_state.p_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_P); + break; + case TRANSFORM_PRIO_XLPR: + if (EuclideanFilter(h1200_state.l_euclidean_length_, h1200_state.l_euclidean_fill_, h1200_state.l_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_L); + if (EuclideanFilter(h1200_state.p_euclidean_length_, h1200_state.p_euclidean_fill_, h1200_state.p_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_P); + if (EuclideanFilter(h1200_state.r_euclidean_length_, h1200_state.r_euclidean_fill_, h1200_state.r_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_R); + break; + + default: break; + } + + switch (nsh_transform_priority_) { + case TRANSFORM_PRIO_XNSH: + if (EuclideanFilter(h1200_state.n_euclidean_length_, h1200_state.n_euclidean_fill_, h1200_state.n_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_N); + if (EuclideanFilter(h1200_state.s_euclidean_length_, h1200_state.s_euclidean_fill_, h1200_state.s_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_S); + if (EuclideanFilter(h1200_state.h_euclidean_length_, h1200_state.h_euclidean_fill_, h1200_state.h_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_H); + break; + case TRANSFORM_PRIO_XSHN: + if (EuclideanFilter(h1200_state.s_euclidean_length_, h1200_state.s_euclidean_fill_, h1200_state.s_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_S); + if (EuclideanFilter(h1200_state.h_euclidean_length_, h1200_state.h_euclidean_fill_, h1200_state.h_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_H); + if (EuclideanFilter(h1200_state.n_euclidean_length_, h1200_state.n_euclidean_fill_, h1200_state.n_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_N); + break; + case TRANSFORM_PRIO_XHNS: + if (EuclideanFilter(h1200_state.h_euclidean_length_, h1200_state.h_euclidean_fill_, h1200_state.h_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_H); + if (EuclideanFilter(h1200_state.n_euclidean_length_, h1200_state.n_euclidean_fill_, h1200_state.n_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_N); + if (EuclideanFilter(h1200_state.s_euclidean_length_, h1200_state.s_euclidean_fill_, h1200_state.s_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_S); + break; + case TRANSFORM_PRIO_XNHS: + if (EuclideanFilter(h1200_state.n_euclidean_length_, h1200_state.n_euclidean_fill_, h1200_state.n_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_N); + if (EuclideanFilter(h1200_state.h_euclidean_length_, h1200_state.h_euclidean_fill_, h1200_state.h_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_H); + if (EuclideanFilter(h1200_state.s_euclidean_length_, h1200_state.s_euclidean_fill_, h1200_state.s_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_S); + break; + case TRANSFORM_PRIO_XHSN: + if (EuclideanFilter(h1200_state.h_euclidean_length_, h1200_state.h_euclidean_fill_, h1200_state.h_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_H); + if (EuclideanFilter(h1200_state.s_euclidean_length_, h1200_state.s_euclidean_fill_, h1200_state.s_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_S); + if (EuclideanFilter(h1200_state.n_euclidean_length_, h1200_state.n_euclidean_fill_, h1200_state.n_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_N); + break; + case TRANSFORM_PRIO_XSNH: + if (EuclideanFilter(h1200_state.s_euclidean_length_, h1200_state.s_euclidean_fill_, h1200_state.s_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_S); + if (EuclideanFilter(h1200_state.n_euclidean_length_, h1200_state.n_euclidean_fill_, h1200_state.n_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_N); + if (EuclideanFilter(h1200_state.h_euclidean_length_, h1200_state.h_euclidean_fill_, h1200_state.h_euclidean_offset_, h1200_state.euclidean_counter_)) h1200_state.tonnetz_state.apply_transformation(tonnetz::TRANSFORM_H); + break; + + default: break; + } + + } + } + + // Finally, we're ready to actually render the triad transformation! + if (triggers || (h1200_settings.get_cv_sampling() == H1200_CV_SAMPLING_CONT)) h1200_state.Render(root_, inversion_, octave_, h1200_settings.output_mode()); + + if (triggers) + MENU_REDRAW = 1; +} + +void H1200_init() { + h1200_settings.Init(); + h1200_state.Init(); + h1200_settings.update_enabled_settings(); + h1200_state.cursor.AdjustEnd(h1200_settings.num_enabled_settings() - 1); +} + +size_t H1200_storageSize() { + return H1200Settings::storageSize(); +} + +size_t H1200_save(void *storage) { + return h1200_settings.Save(storage); +} + +size_t H1200_restore(const void *storage) { + h1200_settings.update_enabled_settings(); + h1200_state.cursor.AdjustEnd(h1200_settings.num_enabled_settings() - 1); + return h1200_settings.Restore(storage); +} + +void H1200_handleAppEvent(OC::AppEvent event) { + switch (event) { + case OC::APP_EVENT_RESUME: + h1200_state.cursor.set_editing(false); + h1200_state.tonnetz_state.reset(h1200_settings.mode()); + break; + case OC::APP_EVENT_SUSPEND: + case OC::APP_EVENT_SCREENSAVER_ON: + case OC::APP_EVENT_SCREENSAVER_OFF: + h1200_settings.update_enabled_settings(); + h1200_state.cursor.AdjustEnd(h1200_settings.num_enabled_settings() - 1); + break; + } +} + +void H1200_isr() { + uint32_t triggers = OC::DigitalInputs::clocked(); + + while (h1200_state.ui_actions.readable()) { + switch (h1200_state.ui_actions.Read()) { + case H1200::ACTION_FORCE_UPDATE: + triggers |= TRIGGER_MASK_DIRTY; + break; + case H1200::ACTION_MANUAL_RESET: + triggers |= TRIGGER_MASK_RESET; + break; + default: + break; + } + } + + H1200_clock(triggers); +} + +void H1200_loop() { +} + +void H1200_handleButtonEvent(const UI::Event &event) { + if (UI::EVENT_BUTTON_PRESS == event.type) { + switch (event.control) { + case OC::CONTROL_BUTTON_UP: + if (h1200_settings.change_value(H1200_SETTING_OCTAVE, 1)) + h1200_state.force_update(); + break; + case OC::CONTROL_BUTTON_DOWN: + if (h1200_settings.change_value(H1200_SETTING_OCTAVE, -1)) + h1200_state.force_update(); + break; + case OC::CONTROL_BUTTON_L: + h1200_state.display_notes = !h1200_state.display_notes; + break; + case OC::CONTROL_BUTTON_R: + h1200_state.cursor.toggle_editing(); + break; + } + } else if (UI::EVENT_BUTTON_LONG_PRESS == event.type && OC::CONTROL_BUTTON_L == event.control) { + h1200_settings.InitDefaults(); + h1200_state.manual_reset(); + } +} + +void H1200_handleEncoderEvent(const UI::Event &event) { + + if (OC::CONTROL_ENCODER_L == event.control) { + if (h1200_settings.change_value(H1200_SETTING_INVERSION, event.value)) + h1200_state.force_update(); + } else if (OC::CONTROL_ENCODER_R == event.control) { + if (h1200_state.cursor.editing()) { + H1200Setting setting = h1200_settings.enabled_setting_at(h1200_state.cursor_pos()); + + if (h1200_settings.change_value(h1200_state.cursor.cursor_pos(), event.value)) { + if (setting == H1200_SETTING_TRIGGER_TYPE) { + h1200_settings.update_enabled_settings(); + h1200_state.cursor.AdjustEnd(h1200_settings.num_enabled_settings() - 1); + } + h1200_state.force_update(); + + switch(setting) { + + case H1200_SETTING_TRIGGER_TYPE: + h1200_settings.update_enabled_settings(); + h1200_state.cursor.AdjustEnd(h1200_settings.num_enabled_settings() - 1); + // hack/hide extra options when default trigger type is selected + if (h1200_settings.get_trigger_type() != H1200_TRIGGER_TYPE_EUCLIDEAN) + h1200_state.cursor.Scroll(h1200_state.cursor_pos()); + break; + case H1200_SETTING_MODE: + h1200_settings.mode_change(true); + break; + default: + break; + } + + } + } else { + h1200_state.cursor.Scroll(event.value); + } + } +} + +void H1200_menu() { + + /* show mode change instantly, because it's somewhat confusing (inconsistent?) otherwise */ + const EMode current_mode = h1200_settings.mode(); // const EMode current_mode = h1200_state.tonnetz_state.current_chord().mode(); + int outputs[4]; + h1200_state.tonnetz_state.get_outputs(outputs); + + menu::DefaultTitleBar::Draw(); + graphics.print(note_name(outputs[0])); + graphics.movePrintPos(weegfx::Graphics::kFixedFontW, 0); + graphics.print(mode_names[current_mode]); + graphics.movePrintPos(weegfx::Graphics::kFixedFontW, 0); + + if (h1200_state.display_notes) { + for (size_t i=1; i < 4; ++i) { + graphics.movePrintPos(weegfx::Graphics::kFixedFontW, 0); + graphics.print(note_name(outputs[i])); + } + } else { + for (size_t i=1; i < 4; ++i) { + graphics.movePrintPos(weegfx::Graphics::kFixedFontW, 0); + graphics.pretty_print(outputs[i]); + } + } + + menu::SettingsList settings_list(h1200_state.cursor); + menu::SettingsListItem list_item; + while (settings_list.available()) { + + const int setting = h1200_settings.enabled_setting_at(settings_list.Next(list_item)); + const int value = h1200_settings.get_value(setting); + const settings::value_attr &attr = H1200Settings::value_attr(setting); + + list_item.DrawDefault(value, attr); + } +} + +void H1200_screensaver() { + uint8_t y = 0; + static const uint8_t x_col_0 = 66; + static const uint8_t x_col_1 = 66 + 24; + static const uint8_t line_h = 16; + static const weegfx::coord_t note_circle_x = 32; + static const weegfx::coord_t note_circle_y = 32; + + uint32_t history = h1200_state.tonnetz_state.history(); + int outputs[4]; + h1200_state.tonnetz_state.get_outputs(outputs); + + uint8_t normalized[3]; + y = 8; + for (size_t i=0; i < 3; ++i, y += line_h) { + int note = outputs[i + 1]; + int octave = note / 12; + note = (note + 120) % 12; + normalized[i] = note; + + graphics.setPrintPos(x_col_1, y); + graphics.print(OC::Strings::note_names_unpadded[note]); + graphics.print(octave + 1); + } + y = 0; + + size_t len = 4; + while (len--) { + graphics.setPrintPos(x_col_0, y); + graphics.print(history & 0x80 ? '+' : '-'); + graphics.print(tonnetz::transform_names[static_cast(history & 0x7f)]); + y += line_h; + history >>= 8; + } + + OC::visualize_pitch_classes(normalized, note_circle_x, note_circle_y); +} + +#ifdef H1200_DEBUG +void H1200_debug() { + int cv = OC::ADC::value(); + int scaled = ((OC::ADC::value() + 127) >> 8); + + graphics.setPrintPos(2, 12); + graphics.printf("I: %4d %4d", cv, scaled); +} +#endif // H1200_DEBUG + +#endif // ENABLE_APP_H1200 diff --git a/software/o_c_REV/APP_HEMISPHERE.ino b/software/o_c_REV/APP_HEMISPHERE.ino index 39537ffc1..abcf7c58c 100644 --- a/software/o_c_REV/APP_HEMISPHERE.ino +++ b/software/o_c_REV/APP_HEMISPHERE.ino @@ -25,36 +25,15 @@ #include "src/drivers/FreqMeasure/OC_FreqMeasure.h" namespace menu = OC::menu; -#include "hemisphere_config.h" #include "HemisphereApplet.h" +#include "HSApplication.h" #include "HSicons.h" #include "HSMIDI.h" #include "HSClockManager.h" -#define DECLARE_APPLET(id, categories, class_name) \ -{ id, categories, class_name ## _Start, class_name ## _Controller, class_name ## _View, \ - class_name ## _OnButtonPress, class_name ## _OnEncoderMove, class_name ## _ToggleHelpScreen, \ - class_name ## _OnDataRequest, class_name ## _OnDataReceive \ -} - -#define HEMISPHERE_DOUBLE_CLICK_TIME 8000 -#define HEMISPHERE_PULSE_ANIMATION_TIME 500 -#define HEMISPHERE_PULSE_ANIMATION_TIME_LONG 1200 - -typedef struct Applet { - int id; - uint8_t categories; - void (*Start)(bool); // Initialize when selected - void (*Controller)(bool, bool); // Interrupt Service Routine - void (*View)(bool); // Draw main view - void (*OnButtonPress)(bool); // Encoder button has been pressed - void (*OnEncoderMove)(bool, int); // Encoder has been rotated - void (*ToggleHelpScreen)(bool); // Help Screen has been requested - uint64_t (*OnDataRequest)(bool); // Get a data int from the applet - void (*OnDataReceive)(bool, uint64_t); // Send a data int to the applet -} Applet; - -// The settings specify the selected applets, and 32 bits of data for each applet +// The settings specify the selected applets, and 64 bits of data for each applet, +// plus 64 bits of data for the ClockSetup applet (which includes some misc config). +// This is the structure of a HemispherePreset in eeprom. enum HEMISPHERE_SETTINGS { HEMISPHERE_SELECTED_LEFT_ID, HEMISPHERE_SELECTED_RIGHT_ID, @@ -66,105 +45,312 @@ enum HEMISPHERE_SETTINGS { HEMISPHERE_RIGHT_DATA_B3, HEMISPHERE_LEFT_DATA_B4, HEMISPHERE_RIGHT_DATA_B4, - HEMISPHERE_CLOCK_DATA, + HEMISPHERE_CLOCK_DATA1, + HEMISPHERE_CLOCK_DATA2, + HEMISPHERE_CLOCK_DATA3, + HEMISPHERE_CLOCK_DATA4, HEMISPHERE_SETTING_LAST }; +static constexpr int HEMISPHERE_AVAILABLE_APPLETS = ARRAY_SIZE(HS::available_applets); +static const int HEM_NR_OF_PRESETS = 4; + +static const char * hem_preset_name[HEM_NR_OF_PRESETS] = { "A", "B", "C", "D" }; + +/* Hemisphere Preset + * - conveniently store/recall multiple configurations + */ +class HemispherePreset : public SystemExclusiveHandler, + public settings::SettingsBase { +public: + int GetAppletId(int h) { + return (h == LEFT_HEMISPHERE) ? values_[HEMISPHERE_SELECTED_LEFT_ID] + : values_[HEMISPHERE_SELECTED_RIGHT_ID]; + } + void SetAppletId(int h, int id) { + apply_value(h, id); + } + bool is_valid() { + return values_[HEMISPHERE_SELECTED_LEFT_ID] != 0; + } + + uint64_t GetClockData() { + return ( (uint64_t(values_[HEMISPHERE_CLOCK_DATA4]) << 48) | + (uint64_t(values_[HEMISPHERE_CLOCK_DATA3]) << 32) | + (uint64_t(values_[HEMISPHERE_CLOCK_DATA2]) << 16) | + uint64_t(values_[HEMISPHERE_CLOCK_DATA1]) ); + } + void SetClockData(const uint64_t data) { + apply_value(HEMISPHERE_CLOCK_DATA1, data & 0xffff); + apply_value(HEMISPHERE_CLOCK_DATA2, (data >> 16) & 0xffff); + apply_value(HEMISPHERE_CLOCK_DATA3, (data >> 32) & 0xffff); + apply_value(HEMISPHERE_CLOCK_DATA4, (data >> 48) & 0xffff); + } + + + // Manually get data for one side + uint64_t GetData(int h) { + return (uint64_t(values_[8 + h]) << 48) | + (uint64_t(values_[6 + h]) << 32) | + (uint64_t(values_[4 + h]) << 16) | + (uint64_t(values_[2 + h])); + } + + /* Manually store state data for one side */ + void SetData(int h, uint64_t data) { + apply_value(2 + h, data & 0xffff); + apply_value(4 + h, (data >> 16) & 0xffff); + apply_value(6 + h, (data >> 32) & 0xffff); + apply_value(8 + h, (data >> 48) & 0xffff); + } + + // TODO: I haven't updated the SysEx data structure here because I don't use it. + // Clock data would probably be useful if it's not too big. -NJM + void OnSendSysEx() { + // Describe the data structure for the audience + uint8_t V[18]; + V[0] = (uint8_t)values_[HEMISPHERE_SELECTED_LEFT_ID]; + V[1] = (uint8_t)values_[HEMISPHERE_SELECTED_RIGHT_ID]; + V[2] = (uint8_t)(values_[HEMISPHERE_LEFT_DATA_B1] & 0xff); + V[3] = (uint8_t)((values_[HEMISPHERE_LEFT_DATA_B1] >> 8) & 0xff); + V[4] = (uint8_t)(values_[HEMISPHERE_RIGHT_DATA_B1] & 0xff); + V[5] = (uint8_t)((values_[HEMISPHERE_RIGHT_DATA_B1] >> 8) & 0xff); + V[6] = (uint8_t)(values_[HEMISPHERE_LEFT_DATA_B2] & 0xff); + V[7] = (uint8_t)((values_[HEMISPHERE_LEFT_DATA_B2] >> 8) & 0xff); + V[8] = (uint8_t)(values_[HEMISPHERE_RIGHT_DATA_B2] & 0xff); + V[9] = (uint8_t)((values_[HEMISPHERE_RIGHT_DATA_B2] >> 8) & 0xff); + V[10] = (uint8_t)(values_[HEMISPHERE_LEFT_DATA_B3] & 0xff); + V[11] = (uint8_t)((values_[HEMISPHERE_LEFT_DATA_B3] >> 8) & 0xff); + V[12] = (uint8_t)(values_[HEMISPHERE_RIGHT_DATA_B3] & 0xff); + V[13] = (uint8_t)((values_[HEMISPHERE_RIGHT_DATA_B3] >> 8) & 0xff); + V[14] = (uint8_t)(values_[HEMISPHERE_LEFT_DATA_B4] & 0xff); + V[15] = (uint8_t)((values_[HEMISPHERE_LEFT_DATA_B4] >> 8) & 0xff); + V[16] = (uint8_t)(values_[HEMISPHERE_RIGHT_DATA_B4] & 0xff); + V[17] = (uint8_t)((values_[HEMISPHERE_RIGHT_DATA_B4] >> 8) & 0xff); + + // Pack it up, ship it out + UnpackedData unpacked; + unpacked.set_data(18, V); + PackedData packed = unpacked.pack(); + SendSysEx(packed, 'H'); + } + + void OnReceiveSysEx() { + uint8_t V[18]; + if (ExtractSysExData(V, 'H')) { + values_[HEMISPHERE_SELECTED_LEFT_ID] = V[0]; + values_[HEMISPHERE_SELECTED_RIGHT_ID] = V[1]; + values_[HEMISPHERE_LEFT_DATA_B1] = ((uint16_t)V[3] << 8) + V[2]; + values_[HEMISPHERE_RIGHT_DATA_B1] = ((uint16_t)V[5] << 8) + V[4]; + values_[HEMISPHERE_LEFT_DATA_B2] = ((uint16_t)V[7] << 8) + V[6]; + values_[HEMISPHERE_RIGHT_DATA_B2] = ((uint16_t)V[9] << 8) + V[8]; + values_[HEMISPHERE_LEFT_DATA_B3] = ((uint16_t)V[11] << 8) + V[10]; + values_[HEMISPHERE_RIGHT_DATA_B3] = ((uint16_t)V[13] << 8) + V[12]; + values_[HEMISPHERE_LEFT_DATA_B4] = ((uint16_t)V[15] << 8) + V[14]; + values_[HEMISPHERE_RIGHT_DATA_B4] = ((uint16_t)V[17] << 8) + V[16]; + } + } + +}; + +// HemispherePreset hem_config; // special place for Clock data and Config data, 64 bits each + +HemispherePreset hem_presets[HEM_NR_OF_PRESETS]; +HemispherePreset *hem_active_preset; + //////////////////////////////////////////////////////////////////////////////// //// Hemisphere Manager //////////////////////////////////////////////////////////////////////////////// -class HemisphereManager : public SystemExclusiveHandler, - public settings::SettingsBase { +using namespace HS; + +class HemisphereManager : public HSApplication { public: - void Init() { - select_mode = -1; // Not selecting - midi_in_hemisphere = -1; // No MIDI In - Applet applets[] = HEMISPHERE_APPLETS; - memcpy(&available_applets, &applets, sizeof(applets)); - ClockSetup = DECLARE_APPLET(9999, 0x01, ClockSetup); + void Start() { + //select_mode = -1; // Not selecting help_hemisphere = -1; clock_setup = 0; - SetApplet(0, get_applet_index_by_id(57)); // ADSR - SetApplet(1, get_applet_index_by_id(58)); // Scale Duet + for (int i = 0; i < 4; ++i) { + quant_scale[i] = OC::Scales::SCALE_SEMI; + quantizer[i].Init(); + quantizer[i].Configure(OC::Scales::GetScale(quant_scale[i]), 0xffff); + } + + SetApplet(0, get_applet_index_by_id(18)); // DualTM + SetApplet(1, get_applet_index_by_id(15)); // EuclidX } void Resume() { + if (!hem_active_preset) + LoadFromPreset(0); + // TODO: restore quantizer settings... + } + void Suspend() { + if (hem_active_preset) { + if (HS::auto_save_enabled) StoreToPreset(preset_id); + hem_active_preset->OnSendSysEx(); + } + } + + void StoreToPreset(HemispherePreset* preset) { + bool doSave = (preset != hem_active_preset); + + hem_active_preset = preset; for (int h = 0; h < 2; h++) { - int index = get_applet_index_by_id(values_[h]); - SetApplet(h, index); - uint64_t data = - (uint64_t(values_[8 + h]) << 48) | - (uint64_t(values_[6 + h]) << 32) | - (uint64_t(values_[4 + h]) << 16) | - (uint64_t(values_[2 + h])); - available_applets[index].OnDataReceive(h, data); + int index = my_applet[h]; + if (hem_active_preset->GetAppletId(h) != HS::available_applets[index].id) + doSave = 1; + hem_active_preset->SetAppletId(h, HS::available_applets[index].id); + + uint64_t data = HS::available_applets[index].OnDataRequest(h); + if (data != applet_data[h]) doSave = 1; + applet_data[h] = data; + hem_active_preset->SetData(h, data); + } + uint64_t data = HS::clock_setup_applet.OnDataRequest(0); + if (data != clock_data) doSave = 1; + clock_data = data; + hem_active_preset->SetClockData(data); + + // initiate actual EEPROM save - ONLY if necessary! + if (doSave) { + OC::CORE::app_isr_enabled = false; + OC::draw_save_message(60); + delay(1); + OC::save_app_data(); + delay(1); + OC::CORE::app_isr_enabled = true; + } + + } + void StoreToPreset(int id) { + StoreToPreset( (HemispherePreset*)(hem_presets + id) ); + preset_id = id; + } + void LoadFromPreset(int id) { + hem_active_preset = (HemispherePreset*)(hem_presets + id); + if (hem_active_preset->is_valid()) { + clock_data = hem_active_preset->GetClockData(); + HS::clock_setup_applet.OnDataReceive(0, clock_data); + + for (int h = 0; h < 2; h++) + { + int index = get_applet_index_by_id( hem_active_preset->GetAppletId(h) ); + applet_data[h] = hem_active_preset->GetData(h); + SetApplet(h, index); + HS::available_applets[index].OnDataReceive(h, applet_data[h]); + } } - ClockSetup.OnDataReceive(0, uint64_t(values_[HEMISPHERE_CLOCK_DATA])); + preset_id = id; } + // does not modify the preset, only the manager void SetApplet(int hemisphere, int index) { my_applet[hemisphere] = index; - if (midi_in_hemisphere == hemisphere) midi_in_hemisphere = -1; - if (available_applets[index].id & 0x80) midi_in_hemisphere = hemisphere; - available_applets[index].Start(hemisphere); - apply_value(hemisphere, available_applets[index].id); + HS::available_applets[index].Start(hemisphere); } - void ChangeApplet(int dir) { - if (SelectModeEnabled() and help_hemisphere == -1) { - int index = get_next_applet_index(my_applet[select_mode], dir); - SetApplet(select_mode, index); - } + void ChangeApplet(int h, int dir) { + int index = get_next_applet_index(my_applet[h], dir); + SetApplet(select_mode, index); } bool SelectModeEnabled() { return select_mode > -1; } - void ExecuteControllers() { - if (midi_in_hemisphere == -1) { - // Only one ISR can look for MIDI messages at a time, so we need to check - // for another MIDI In applet before looking for sysex. Note that applets - // that use MIDI In should check for sysex themselves; see Midi In for an - // example. - if (usbMIDI.read() && usbMIDI.getType() == 7) { - OnReceiveSysEx(); + void ProcessMIDI() { + HS::IOFrame &f = HS::frame; + + while (usbMIDI.read()) { + const int message = usbMIDI.getType(); + const int data1 = usbMIDI.getData1(); + const int data2 = usbMIDI.getData2(); + + if (message == usbMIDI.SystemExclusive) { + ReceiveManagerSysEx(); + continue; + } + + if (message == usbMIDI.ProgramChange) { + int slot = usbMIDI.getData1(); + if (slot < 4) LoadFromPreset(slot); + continue; } + + f.MIDIState.ProcessMIDIMsg(usbMIDI.getChannel(), message, data1, data2); } + } + + void Controller() { + // top-level MIDI-to-CV handling - alters frame outputs + ProcessMIDI(); - if (clock_setup) ClockSetup.Controller(LEFT_HEMISPHERE, clock_m->IsForwarded()); + // Clock Setup applet handles internal clock duties + HS::clock_setup_applet.Controller(LEFT_HEMISPHERE, 0); + // execute Applets for (int h = 0; h < 2; h++) { int index = my_applet[h]; - available_applets[index].Controller(h, clock_m->IsForwarded()); + if (HS::available_applets[index].id != 150) // not MIDI In + { + ForEachChannel(ch) { + int chan = h*2 + ch; + // mix CV inputs with applicable MIDI signals + switch (HS::frame.MIDIState.function[chan]) { + case HEM_MIDI_CC_OUT: + case HEM_MIDI_NOTE_OUT: + case HEM_MIDI_VEL_OUT: + case HEM_MIDI_AT_OUT: + case HEM_MIDI_PB_OUT: + HS::frame.inputs[chan] += HS::frame.MIDIState.outputs[chan]; + break; + case HEM_MIDI_GATE_OUT: + HS::frame.gate_high[chan] |= (HS::frame.MIDIState.outputs[chan] > (12 << 7)); + break; + case HEM_MIDI_TRIG_OUT: + case HEM_MIDI_CLOCK_OUT: + case HEM_MIDI_START_OUT: + HS::frame.clocked[chan] |= HS::frame.MIDIState.trigout_q[chan]; + HS::frame.MIDIState.trigout_q[chan] = 0; + break; + } + } + } + HS::available_applets[index].Controller(h, 0); } } - void DrawViews() { + void View() { + if (config_menu) { + DrawConfigMenu(); + return; + } + if (clock_setup) { - ClockSetup.View(LEFT_HEMISPHERE); - } else if (help_hemisphere > -1) { + HS::clock_setup_applet.View(LEFT_HEMISPHERE); + return; + } + + if (help_hemisphere > -1) { int index = my_applet[help_hemisphere]; - available_applets[index].View(help_hemisphere); + HS::available_applets[index].View(help_hemisphere); } else { for (int h = 0; h < 2; h++) { int index = my_applet[h]; - available_applets[index].View(h); - if (h == 0) { - if (clock_m->IsRunning() || clock_m->IsPaused()) { - // Metronome icon - graphics.drawBitmap8(56, 1, 8, clock_m->Cycle() ? METRO_L_ICON : METRO_R_ICON); - } else if (clock_m->IsForwarded()) { - // CV Forwarding Icon - graphics.drawBitmap8(56, 1, 8, CLOCK_ICON); - } - } + HS::available_applets[index].View(h); + } + + if (clock_m->IsRunning()) { + // Metronome icon + gfxIcon(56, 1, clock_m->Cycle() ? METRO_L_ICON : METRO_R_ICON); + } else if (clock_m->IsPaused()) { + gfxIcon(56, 1, PAUSE_ICON); } if (select_mode == LEFT_HEMISPHERE) graphics.drawFrame(0, 0, 64, 64); @@ -173,163 +359,307 @@ public: } void DelegateEncoderPush(const UI::Event &event) { + bool down = (event.type == UI::EVENT_BUTTON_DOWN); int h = (event.control == OC::CONTROL_BUTTON_L) ? LEFT_HEMISPHERE : RIGHT_HEMISPHERE; - if (clock_setup) { - ClockSetup.OnButtonPress(LEFT_HEMISPHERE); - } else if (select_mode == h) { + + if (config_menu) { + // button release for config screen + if (!down) ConfigButtonPush(h); + return; + } + + // button down + if (down) { + // Clock Setup is more immediate for manual triggers + if (clock_setup) HS::clock_setup_applet.OnButtonPress(LEFT_HEMISPHERE); + // TODO: consider a new OnButtonDown handler for applets + return; + } + + // button release + if (select_mode == h) { select_mode = -1; // Pushing a button for the selected side turns off select mode - } else { + } else if (!clock_setup) { + // regular applets get button release int index = my_applet[h]; - if (event.type == UI::EVENT_BUTTON_PRESS) { - available_applets[index].OnButtonPress(h); - } + HS::available_applets[index].OnButtonPress(h); } } - void DelegateSelectButtonPush(int hemisphere) { - if (OC::CORE::ticks - click_tick < HEMISPHERE_DOUBLE_CLICK_TIME && hemisphere == first_click) { - // This is a double-click, so activate corresponding help screen, leave - // Select Mode, and reset the double-click timer - SetHelpScreen(hemisphere); - select_mode = -1; - click_tick = 0; - } else { - // This is a single click. If a help screen is already selected, and the - // button is for the opposite one, go to the other help screen + void DelegateSelectButtonPush(const UI::Event &event) { + bool down = (event.type == UI::EVENT_BUTTON_DOWN); + int hemisphere = (event.control == OC::CONTROL_BUTTON_UP) ? LEFT_HEMISPHERE : RIGHT_HEMISPHERE; + + if (config_menu) { + // cancel preset select, or config screen on select button release + if (!down) { + if (preset_cursor) { + preset_cursor = 0; + } + else config_menu = 0; + } + return; + } + + if (clock_setup && !down) { + clock_setup = 0; // Turn off clock setup with any single-click button release + return; + } + + // -- button down + if (down) { + // dual press for Clock Setup... check first_click, so we only process the 2nd button event + if (event.mask == (OC::CONTROL_BUTTON_UP | OC::CONTROL_BUTTON_DOWN) && hemisphere != first_click) { + clock_setup = 1; + SetHelpScreen(-1); + select_mode = -1; + OC::ui.SetButtonIgnoreMask(); // ignore button release + return; + } + + if (OC::CORE::ticks - click_tick < HEMISPHERE_DOUBLE_CLICK_TIME) { + // This is a double-click on one button. Activate corresponding help screen and deactivate select mode. + if (hemisphere == first_click) + SetHelpScreen(hemisphere); + + // reset double-click timer either way + click_tick = 0; + return; + } + + // -- Single click + // If a help screen is already selected, and the button is for + // the opposite one, go to the other help screen if (help_hemisphere > -1) { if (help_hemisphere != hemisphere) SetHelpScreen(hemisphere); - else SetHelpScreen(-1); // Leave help screen if corresponding button is clicked - } else if (!clock_setup) { - // If we're in the clock setup screen, we want to exit the setup without turning on Select Mode - if (hemisphere == select_mode) select_mode = -1; // Leave Select Mode is same button is pressed - else select_mode = hemisphere; // Otherwise, set Select Mode - click_tick = OC::CORE::ticks; + else SetHelpScreen(-1); // Exit help screen if same button is clicked + OC::ui.SetButtonIgnoreMask(); // ignore release } + + // mark this single click + click_tick = OC::CORE::ticks; first_click = hemisphere; + return; } - clock_setup = 0; // Turn off clock setup with any button press + // -- button release + if (!clock_setup) { + // Select Mode + if (hemisphere == select_mode) select_mode = -1; // Exit Select Mode if same button is pressed + else if (help_hemisphere < 0) // Otherwise, set Select Mode - UNLESS there's a help screen + select_mode = hemisphere; + } } void DelegateEncoderMovement(const UI::Event &event) { int h = (event.control == OC::CONTROL_ENCODER_L) ? LEFT_HEMISPHERE : RIGHT_HEMISPHERE; + if (config_menu) { + ConfigEncoderAction(h, event.value); + return; + } + if (clock_setup) { - ClockSetup.OnEncoderMove(LEFT_HEMISPHERE, event.value); + HS::clock_setup_applet.OnEncoderMove(LEFT_HEMISPHERE, event.value); } else if (select_mode == h) { - ChangeApplet(event.value); + ChangeApplet(h, event.value); } else { int index = my_applet[h]; - available_applets[index].OnEncoderMove(h, event.value); + HS::available_applets[index].OnEncoderMove(h, event.value); } } void ToggleClockRun() { - if (clock_m->IsRunning()) clock_m->Pause(); - else if (clock_m->IsPaused()) clock_m->Start(); - else clock_m->ToggleForwarding(); + if (clock_m->IsRunning()) { + clock_m->Stop(); + } else { + bool p = clock_m->IsPaused(); + clock_m->Start( !p ); + } } void ToggleClockSetup() { clock_setup = 1 - clock_setup; } + void ToggleConfigMenu() { + config_menu = !config_menu; + if (config_menu) SetHelpScreen(-1); + } + void SetHelpScreen(int hemisphere) { if (help_hemisphere > -1) { // Turn off the previous help screen int index = my_applet[help_hemisphere]; - available_applets[index].ToggleHelpScreen(help_hemisphere); + HS::available_applets[index].ToggleHelpScreen(help_hemisphere); } if (hemisphere > -1) { // Turn on the next hemisphere's screen int index = my_applet[hemisphere]; - available_applets[index].ToggleHelpScreen(hemisphere); + HS::available_applets[index].ToggleHelpScreen(hemisphere); } help_hemisphere = hemisphere; } - void RequestAppletData() { - for (int h = 0; h < 2; h++) - { - int index = my_applet[h]; - uint64_t data = available_applets[index].OnDataRequest(h); - apply_value(2 + h, data & 0xffff); - apply_value(4 + h, (data >> 16) & 0xffff); - apply_value(6 + h, (data >> 32) & 0xffff); - apply_value(8 + h, (data >> 48) & 0xffff); +private: + int preset_id = 0; + int preset_cursor = 0; + int my_applet[2]; // Indexes to available_applets + uint64_t clock_data, applet_data[2]; // cache of applet data + bool clock_setup; + bool config_menu; + bool isEditing = false; + int config_cursor = 0; + + int help_hemisphere; // Which of the hemispheres (if any) is in help mode, or -1 if none + uint32_t click_tick; // Measure time between clicks for double-click + int first_click; // The first button pushed of a double-click set, to see if the same one is pressed + ClockManager *clock_m = clock_m->get(); + + enum HEMConfigCursor { + LOAD_PRESET, SAVE_PRESET, + AUTO_SAVE, + TRIG_LENGTH, + SCREENSAVER_MODE, + CURSOR_MODE, + + MAX_CURSOR = CURSOR_MODE + }; + + void ConfigEncoderAction(int h, int dir) { + if (!isEditing && !preset_cursor) { + config_cursor += dir; + config_cursor = constrain(config_cursor, 0, MAX_CURSOR); + ResetCursor(); + return; + } + + switch (config_cursor) { + case TRIG_LENGTH: + HS::trig_length = (uint32_t) constrain( int(HS::trig_length + dir), 1, 127); + break; + //case SCREENSAVER_MODE: + // TODO? + //break; + case SAVE_PRESET: + case LOAD_PRESET: + preset_cursor = constrain(preset_cursor + dir, 1, HEM_NR_OF_PRESETS); + break; } - apply_value(HEMISPHERE_CLOCK_DATA, ClockSetup.OnDataRequest(0)); } + void ConfigButtonPush(int h) { + if (preset_cursor) { + // Save or Load on button push + if (config_cursor == SAVE_PRESET) + StoreToPreset(preset_cursor-1); + else + LoadFromPreset(preset_cursor-1); + + preset_cursor = 0; // deactivate preset selection + config_menu = 0; + isEditing = false; + return; + } - void OnSendSysEx() { - // Set the values_ array prior to packing it - RequestAppletData(); + switch (config_cursor) { + case SAVE_PRESET: + case LOAD_PRESET: + preset_cursor = preset_id + 1; + break; - // Describe the data structure for the audience - uint8_t V[18]; - V[0] = (uint8_t)values_[HEMISPHERE_SELECTED_LEFT_ID]; - V[1] = (uint8_t)values_[HEMISPHERE_SELECTED_RIGHT_ID]; - V[2] = (uint8_t)(values_[HEMISPHERE_LEFT_DATA_B1] & 0xff); - V[3] = (uint8_t)((values_[HEMISPHERE_LEFT_DATA_B1] >> 8) & 0xff); - V[4] = (uint8_t)(values_[HEMISPHERE_RIGHT_DATA_B1] & 0xff); - V[5] = (uint8_t)((values_[HEMISPHERE_RIGHT_DATA_B1] >> 8) & 0xff); - V[6] = (uint8_t)(values_[HEMISPHERE_LEFT_DATA_B2] & 0xff); - V[7] = (uint8_t)((values_[HEMISPHERE_LEFT_DATA_B2] >> 8) & 0xff); - V[8] = (uint8_t)(values_[HEMISPHERE_RIGHT_DATA_B2] & 0xff); - V[9] = (uint8_t)((values_[HEMISPHERE_RIGHT_DATA_B2] >> 8) & 0xff); - V[10] = (uint8_t)(values_[HEMISPHERE_LEFT_DATA_B3] & 0xff); - V[11] = (uint8_t)((values_[HEMISPHERE_LEFT_DATA_B3] >> 8) & 0xff); - V[12] = (uint8_t)(values_[HEMISPHERE_RIGHT_DATA_B3] & 0xff); - V[13] = (uint8_t)((values_[HEMISPHERE_RIGHT_DATA_B3] >> 8) & 0xff); - V[14] = (uint8_t)(values_[HEMISPHERE_LEFT_DATA_B4] & 0xff); - V[15] = (uint8_t)((values_[HEMISPHERE_LEFT_DATA_B4] >> 8) & 0xff); - V[16] = (uint8_t)(values_[HEMISPHERE_RIGHT_DATA_B4] & 0xff); - V[17] = (uint8_t)((values_[HEMISPHERE_RIGHT_DATA_B4] >> 8) & 0xff); + case AUTO_SAVE: + HS::auto_save_enabled = !HS::auto_save_enabled; + break; - // Pack it up, ship it out - UnpackedData unpacked; - unpacked.set_data(18, V); - PackedData packed = unpacked.pack(); - SendSysEx(packed, 'H'); - } + case TRIG_LENGTH: + isEditing = !isEditing; + break; - void OnReceiveSysEx() { - uint8_t V[18]; - if (ExtractSysExData(V, 'H')) { - values_[HEMISPHERE_SELECTED_LEFT_ID] = V[0]; - values_[HEMISPHERE_SELECTED_RIGHT_ID] = V[1]; - values_[HEMISPHERE_LEFT_DATA_B1] = ((uint16_t)V[3] << 8) + V[2]; - values_[HEMISPHERE_RIGHT_DATA_B1] = ((uint16_t)V[5] << 8) + V[4]; - values_[HEMISPHERE_LEFT_DATA_B2] = ((uint16_t)V[7] << 8) + V[6]; - values_[HEMISPHERE_RIGHT_DATA_B2] = ((uint16_t)V[9] << 8) + V[8]; - values_[HEMISPHERE_LEFT_DATA_B3] = ((uint16_t)V[11] << 8) + V[10]; - values_[HEMISPHERE_RIGHT_DATA_B3] = ((uint16_t)V[13] << 8) + V[12]; - values_[HEMISPHERE_LEFT_DATA_B4] = ((uint16_t)V[15] << 8) + V[14]; - values_[HEMISPHERE_RIGHT_DATA_B4] = ((uint16_t)V[17] << 8) + V[16]; - Resume(); + case SCREENSAVER_MODE: + ++HS::screensaver_mode %= 4; + break; + + case CURSOR_MODE: + HS::CycleEditMode(); + break; } } -private: - Applet available_applets[HEMISPHERE_AVAILABLE_APPLETS]; - Applet ClockSetup; - int my_applet[2]; // Indexes to available_applets - int select_mode; - bool clock_setup; - int help_hemisphere; // Which of the hemispheres (if any) is in help mode, or -1 if none - int midi_in_hemisphere; // Which of the hemispheres (if any) is using MIDI In - uint32_t click_tick; // Measure time between clicks for double-click - int first_click; // The first button pushed of a double-click set, to see if the same one is pressed - ClockManager *clock_m = clock_m->get(); + void DrawConfigMenu() { + // --- Preset Selector + if (preset_cursor) { + DrawPresetSelector(); + return; + } - void DrawClockSetup() { + // --- Config Selection + gfxHeader("Hemisphere Config"); + gfxPrint(1, 15, "Preset: "); + gfxPrint(48, 15, "Load"); + gfxIcon(100, 15, HS::auto_save_enabled ? CHECK_ON_ICON : CHECK_OFF_ICON ); + gfxPrint(48, 25, "Save (auto)"); + + gfxPrint(1, 35, "Trig Length: "); + gfxPrint(HS::trig_length); + gfxPrint("ms"); + + const char * ssmodes[4] = { "[blank]", "Meters", "Zaps", + #if defined(__IMXRT1062__) + "Stars" + #else + "Zips" + #endif + }; + gfxPrint(1, 45, "Screensaver: "); + gfxPrint( ssmodes[HS::screensaver_mode] ); + + const char * cursor_mode_name[3] = { "legacy", "modal", "modal+wrap" }; + gfxPrint(1, 55, "Cursor: "); + gfxPrint(cursor_mode_name[HS::modal_edit_mode]); + + switch (config_cursor) { + case LOAD_PRESET: + case SAVE_PRESET: + gfxIcon(73, 15 + (config_cursor - LOAD_PRESET)*10, LEFT_ICON); + break; + + case AUTO_SAVE: + gfxIcon(90, 15, RIGHT_ICON); + break; + + case TRIG_LENGTH: + if (isEditing) gfxInvert(79, 34, 25, 9); + else gfxCursor(80, 43, 24); + break; + case SCREENSAVER_MODE: + gfxIcon(73, 45, RIGHT_ICON); + break; + case CURSOR_MODE: + gfxIcon(43, 55, RIGHT_ICON); + break; + } + } + void DrawPresetSelector() { + gfxHeader("Hemisphere Presets"); + int y = 5 + preset_cursor*10; + gfxPrint(1, y, (config_cursor == SAVE_PRESET) ? "Save" : "Load"); + gfxIcon(26, y, RIGHT_ICON); + for (int i = 0; i < HEM_NR_OF_PRESETS; ++i) { + y = 15 + i*10; + gfxPrint(35, y, hem_preset_name[i]); + + if (!hem_presets[i].is_valid()) + gfxPrint(" (empty)"); + else if (i == preset_id) + gfxIcon(45, y, ZAP_ICON); + } } int get_applet_index_by_id(int id) { int index = 0; for (int i = 0; i < HEMISPHERE_AVAILABLE_APPLETS; i++) { - if (available_applets[i].id == id) index = i; + if (HS::available_applets[i].id == id) index = i; } return index; } @@ -339,19 +669,12 @@ private: if (index >= HEMISPHERE_AVAILABLE_APPLETS) index = 0; if (index < 0) index = HEMISPHERE_AVAILABLE_APPLETS - 1; - // If an applet uses MIDI In, it can only be selected in one - // hemisphere, and is designated by bit 7 set in its id. - if (available_applets[index].id & 0x80) { - if (midi_in_hemisphere == (1 - select_mode)) { - return get_next_applet_index(index, dir); - } - } - return index; } }; -SETTINGS_DECLARE(HemisphereManager, HEMISPHERE_SETTING_LAST) { +// TOTAL EEPROM SIZE: 4 * 26 bytes +SETTINGS_DECLARE(HemispherePreset, HEMISPHERE_SETTING_LAST) { {0, 0, 255, "Applet ID L", NULL, settings::STORAGE_TYPE_U8}, {0, 0, 255, "Applet ID R", NULL, settings::STORAGE_TYPE_U8}, {0, 0, 65535, "Data L block 1", NULL, settings::STORAGE_TYPE_U16}, @@ -362,13 +685,17 @@ SETTINGS_DECLARE(HemisphereManager, HEMISPHERE_SETTING_LAST) { {0, 0, 65535, "Data R block 3", NULL, settings::STORAGE_TYPE_U16}, {0, 0, 65535, "Data L block 4", NULL, settings::STORAGE_TYPE_U16}, {0, 0, 65535, "Data R block 4", NULL, settings::STORAGE_TYPE_U16}, - {0, 0, 65535, "Clock data", NULL, settings::STORAGE_TYPE_U16} + {0, 0, 65535, "Clock data 1", NULL, settings::STORAGE_TYPE_U16}, + {0, 0, 65535, "Clock data 2", NULL, settings::STORAGE_TYPE_U16}, + {0, 0, 65535, "Clock data 3", NULL, settings::STORAGE_TYPE_U16}, + {0, 0, 65535, "Clock data 4", NULL, settings::STORAGE_TYPE_U16} }; HemisphereManager manager; void ReceiveManagerSysEx() { - manager.OnReceiveSysEx(); + if (hem_active_preset) + hem_active_preset->OnReceiveSysEx(); } //////////////////////////////////////////////////////////////////////////////// @@ -377,55 +704,153 @@ void ReceiveManagerSysEx() { // App stubs void HEMISPHERE_init() { - manager.Init(); + manager.BaseStart(); } size_t HEMISPHERE_storageSize() { - return HemisphereManager::storageSize(); + return HemispherePreset::storageSize() * HEM_NR_OF_PRESETS; } size_t HEMISPHERE_save(void *storage) { - manager.RequestAppletData(); - return manager.Save(storage); + size_t used = 0; + for (int i = 0; i < HEM_NR_OF_PRESETS; ++i) { + used += hem_presets[i].Save(static_cast(storage) + used); + } + return used; } size_t HEMISPHERE_restore(const void *storage) { - size_t s = manager.Restore(storage); + size_t used = 0; + for (int i = 0; i < HEM_NR_OF_PRESETS; ++i) { + used += hem_presets[i].Restore(static_cast(storage) + used); + } manager.Resume(); - return s; + return used; } void FASTRUN HEMISPHERE_isr() { - manager.ExecuteControllers(); + manager.BaseController(); } void HEMISPHERE_handleAppEvent(OC::AppEvent event) { - if (event == OC::APP_EVENT_SUSPEND) { - manager.OnSendSysEx(); + switch (event) { + case OC::APP_EVENT_RESUME: + break; + + case OC::APP_EVENT_SCREENSAVER_ON: + case OC::APP_EVENT_SUSPEND: + manager.Suspend(); + break; + + default: break; } } void HEMISPHERE_loop() {} // Essentially deprecated in favor of ISR void HEMISPHERE_menu() { - manager.DrawViews(); + manager.View(); +} + +typedef struct { + int x = 0; + int y = 0; + int x_v = 6; + int y_v = 3; + + void Move(bool stars) { + if (stars) Move(6100, 2900); + else Move(); + } + void Move(int target_x = -1, int target_y = -1) { + x += x_v; + y += y_v; + if (x > 12700 || x < 0 || y > 6300 || y < 0) { + if (target_x < 0 || target_y < 0) { + x = random(12700); + y = random(6300); + } else { + x = target_x + random(400); + y = target_y + random(400); + CONSTRAIN(x, 0, 12700); + CONSTRAIN(y, 0, 6300); + } + + x_v = random(30) - 15; + y_v = random(30) - 15; + if (x_v == 0) ++x_v; + if (y_v == 0) ++y_v; + } + } +} Zap; +static constexpr int HOW_MANY_ZAPS = 30; +static Zap zaps[HOW_MANY_ZAPS]; +static void ZapScreensaver(const bool stars = false) { + static int frame_delay = 0; + for (int i = 0; i < (stars ? HOW_MANY_ZAPS : 5); i++) { + if (frame_delay & 0x1) { + #if defined(__IMXRT1062__) + zaps[i].Move(stars); // centered starfield + #else + // Zips respawn from their previous sibling + if (0 == i) zaps[0].Move(); + else zaps[i].Move(zaps[i-1].x, zaps[i-1].y); + #endif + } + + if (stars && frame_delay == 0) { + // accel + zaps[i].x_v *= 2; + zaps[i].y_v *= 2; + } + + if (stars) + gfxPixel(zaps[i].x/100, zaps[i].y/100); + else + gfxIcon(zaps[i].x/100, zaps[i].y/100, ZAP_ICON); + } + if (--frame_delay < 0) frame_delay = 100; } -void HEMISPHERE_screensaver() {} // Deprecated in favor of screen blanking +void HEMISPHERE_screensaver() { + switch (HS::screensaver_mode) { + case 0x3: // Zips or Stars + ZapScreensaver(true); + break; + case 0x2: // Zaps + ZapScreensaver(); + break; + case 0x1: // Meters + manager.BaseScreensaver(true); // show note names + break; + default: break; // blank screen + } +} void HEMISPHERE_handleButtonEvent(const UI::Event &event) { - if (event.type == UI::EVENT_BUTTON_PRESS) { + switch (event.type) { + case UI::EVENT_BUTTON_DOWN: + #ifdef VOR + if (event.control == OC::CONTROL_BUTTON_M) { + manager.ToggleClockRun(); + OC::ui.SetButtonIgnoreMask(); // ignore release and long-press + break; + } + #endif + case UI::EVENT_BUTTON_PRESS: if (event.control == OC::CONTROL_BUTTON_UP || event.control == OC::CONTROL_BUTTON_DOWN) { - int hemisphere = (event.control == OC::CONTROL_BUTTON_UP) ? LEFT_HEMISPHERE : RIGHT_HEMISPHERE; - manager.DelegateSelectButtonPush(hemisphere); - } else { + manager.DelegateSelectButtonPush(event); + } else if (event.control == OC::CONTROL_BUTTON_L || event.control == OC::CONTROL_BUTTON_R) { manager.DelegateEncoderPush(event); } - } + break; - if (event.type == UI::EVENT_BUTTON_LONG_PRESS) { - if (event.control == OC::CONTROL_BUTTON_DOWN) manager.ToggleClockSetup(); + case UI::EVENT_BUTTON_LONG_PRESS: + if (event.control == OC::CONTROL_BUTTON_DOWN) manager.ToggleConfigMenu(); if (event.control == OC::CONTROL_BUTTON_L) manager.ToggleClockRun(); + break; + + default: break; } } diff --git a/software/o_c_REV/APP_LORENZ.ino b/software/o_c_REV/APP_LORENZ.ino new file mode 100644 index 000000000..bfee5a194 --- /dev/null +++ b/software/o_c_REV/APP_LORENZ.ino @@ -0,0 +1,371 @@ +// Copyright (c) 2015, 2016 Patrick Dowling, Max Stadler, Tim Churches +// +// Author of original O+C firmware: Max Stadler (mxmlnstdlr@gmail.com) +// Author of app scaffolding: Patrick Dowling (pld@gurkenkiste.com) +// Modified for Lorenz and Rössler generators: Tim Churches (tim.churches@gmail.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// Lorenz and Rössler generator app + +#ifdef ENABLE_APP_LORENZ + +#include "streams_lorenz_generator.h" +#include "util/util_math.h" +#include "OC_digital_inputs.h" + +enum LORENZ_SETTINGS { + LORENZ_SETTING_FREQ1, + LORENZ_SETTING_FREQ2, + LORENZ_SETTING_RHO1, + LORENZ_SETTING_RHO2, + LORENZ_SETTING_FREQ_RANGE1, + LORENZ_SETTING_FREQ_RANGE2, + LORENZ_SETTING_OUT_A, + LORENZ_SETTING_OUT_B, + LORENZ_SETTING_OUT_C, + LORENZ_SETTING_OUT_D, + LORENZ_SETTING_LAST +}; + +const char * const lorenz_output_names[] = { + "Lx1", + "Ly1", + "Lz1", + "Lx2", + "Ly2", + "Lz2", + "Rx1", + "Ry1", + "Rz1", + "Rx2", + "Ry2", + "Rz2", + "Lx1+Rx1", + "Lx1+Rz1", + "Lx1+Ly2", + "Lx1+Lz2", + "Lx1+Rx2", + "Lx1+Rz2", + "Lx1xLy1", + "Lx1xLx2", + "Lx1xRx1", + "Lx1xRx2", +}; + +class LorenzGenerator : public settings::SettingsBase { +public: + + uint16_t get_freq1() const { + return values_[LORENZ_SETTING_FREQ1]; + } + + uint16_t get_freq2() const { + return values_[LORENZ_SETTING_FREQ2]; + } + + uint8_t get_freq_range1() const { + return values_[LORENZ_SETTING_FREQ_RANGE1]; + } + + uint8_t get_freq_range2() const { + return values_[LORENZ_SETTING_FREQ_RANGE2]; + } + + uint16_t get_rho1() const { + return values_[LORENZ_SETTING_RHO1]; + } + + uint16_t get_rho2() const { + return values_[LORENZ_SETTING_RHO2]; + } + + uint8_t get_out_a() const { + return values_[LORENZ_SETTING_OUT_A]; + } + + uint8_t get_out_b() const { + return values_[LORENZ_SETTING_OUT_B]; + } + + uint8_t get_out_c() const { + return values_[LORENZ_SETTING_OUT_C]; + } + + uint8_t get_out_d() const { + return values_[LORENZ_SETTING_OUT_D]; + } + + void Init(); + + void freeze() { + frozen_ = true; + } + + void thaw() { + frozen_ = false; + } + + bool frozen() const { + return frozen_; + } + + streams::LorenzGenerator lorenz; + bool frozen_; + + // ISR update is at 16.666kHz, we don't need it that fast so smooth the values to ~1Khz + static constexpr int32_t kSmoothing = 16; + + SmoothedValue cv_freq1; + SmoothedValue cv_freq2; + SmoothedValue cv_rho1; + SmoothedValue cv_rho2; +}; + +void LorenzGenerator::Init() { + InitDefaults(); + lorenz.Init(0); + lorenz.Init(1); + frozen_= false; +} + +const char* const lorenz_freq_range_names[5] = { + "sloth", "lazy", "slow", "med", "fast", +}; + +// TOTAL EEPROM SIZE: 9 bytes +SETTINGS_DECLARE(LorenzGenerator, LORENZ_SETTING_LAST) { + #ifdef BUCHLA_4U + { 0, 0, 255, "Freq 1", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 255, "Freq 2", NULL, settings::STORAGE_TYPE_U8 }, + #else + { 128, 0, 255, "Freq 1", NULL, settings::STORAGE_TYPE_U8 }, + { 128, 0, 255, "Freq 2", NULL, settings::STORAGE_TYPE_U8 }, + #endif + { 63, 4, 127, "Rho/c 1", NULL, settings::STORAGE_TYPE_U8 }, + { 63, 4, 127, "Rho/c 2", NULL, settings::STORAGE_TYPE_U8 }, + { 2, 0, 4, "LFreq 1 Rng", lorenz_freq_range_names, settings::STORAGE_TYPE_U4 }, + { 2, 0, 4, "LFreq 2 Rng", lorenz_freq_range_names, settings::STORAGE_TYPE_U4 }, + {streams::LORENZ_OUTPUT_X1, streams::LORENZ_OUTPUT_X1, streams::LORENZ_OUTPUT_LAST - 1, "Out A ", lorenz_output_names, settings::STORAGE_TYPE_U8}, + {streams::LORENZ_OUTPUT_Y1, streams::LORENZ_OUTPUT_X1, streams::LORENZ_OUTPUT_LAST - 1, "Out B ", lorenz_output_names, settings::STORAGE_TYPE_U8}, + {streams::LORENZ_OUTPUT_X2, streams::LORENZ_OUTPUT_X1, streams::LORENZ_OUTPUT_LAST - 1, "Out C ", lorenz_output_names, settings::STORAGE_TYPE_U8}, + {streams::LORENZ_OUTPUT_Y2, streams::LORENZ_OUTPUT_X1, streams::LORENZ_OUTPUT_LAST - 1, "Out D ", lorenz_output_names, settings::STORAGE_TYPE_U8}, +}; + +LorenzGenerator lorenz_generator; +struct { + bool selected_generator; + + menu::ScreenCursor cursor; +} lorenz_generator_state; + +void FASTRUN LORENZ_isr() { + + bool reset1_phase = OC::DigitalInputs::clocked(); + bool reset2_phase = OC::DigitalInputs::clocked(); + bool reset_both_phase = OC::DigitalInputs::clocked(); + bool freeze = OC::DigitalInputs::read_immediate(); + + lorenz_generator.cv_freq1.push(OC::ADC::value()); + lorenz_generator.cv_rho1.push(OC::ADC::value()); + lorenz_generator.cv_freq2.push(OC::ADC::value()); + lorenz_generator.cv_rho2.push(OC::ADC::value()); + + // Range in settings is (0-256] so this gets scaled to (0,65535] + // CV value is 12 bit so also needs scaling + + int32_t freq1 = SCALE8_16(lorenz_generator.get_freq1()) + (lorenz_generator.cv_freq1.value() * 16); + freq1 = USAT16(freq1); + + int32_t freq2 = SCALE8_16(lorenz_generator.get_freq2()) + (lorenz_generator.cv_freq2.value() * 16); + freq2 = USAT16(freq2); + + const int32_t rho_lower_limit = 4 << 8 ; + const int32_t rho_upper_limit = 127 << 8 ; + + int32_t rho1 = SCALE8_16(lorenz_generator.get_rho1()) + (lorenz_generator.cv_rho1.value() * 16) ; + if (rho1 < rho_lower_limit) rho1 = rho_lower_limit; + else if (rho1 > rho_upper_limit) rho1 = rho_upper_limit ; + lorenz_generator.lorenz.set_rho1(USAT16(rho1)); + + int32_t rho2 = SCALE8_16(lorenz_generator.get_rho2()) + (lorenz_generator.cv_rho2.value() * 16) ; + if (rho2 < rho_lower_limit) rho2 = rho_lower_limit; + else if (rho2 > rho_upper_limit) rho2 = rho_upper_limit ; + lorenz_generator.lorenz.set_rho2(USAT16(rho2)); + + uint8_t out_a = lorenz_generator.get_out_a() ; + lorenz_generator.lorenz.set_out_a(out_a); + + uint8_t out_b = lorenz_generator.get_out_b() ; + lorenz_generator.lorenz.set_out_b(out_b); + + uint8_t out_c = lorenz_generator.get_out_c() ; + lorenz_generator.lorenz.set_out_c(out_c); + + uint8_t out_d = lorenz_generator.get_out_d() ; + lorenz_generator.lorenz.set_out_d(out_d); + + if (reset_both_phase) { + reset1_phase = true ; + reset2_phase = true ; + } + if (!freeze && !lorenz_generator.frozen()) + lorenz_generator.lorenz.Process(freq1, freq2, reset1_phase, reset2_phase, lorenz_generator.get_freq_range1(), lorenz_generator.get_freq_range2()); + + OC::DAC::set(lorenz_generator.lorenz.dac_code(0)); + OC::DAC::set(lorenz_generator.lorenz.dac_code(1)); + OC::DAC::set(lorenz_generator.lorenz.dac_code(2)); + OC::DAC::set(lorenz_generator.lorenz.dac_code(3)); +} + +void LORENZ_init() { + lorenz_generator_state.selected_generator = 0; + lorenz_generator_state.cursor.Init(LORENZ_SETTING_RHO1, LORENZ_SETTING_LAST - 1); + lorenz_generator.Init(); +} + +size_t LORENZ_storageSize() { + return LorenzGenerator::storageSize(); +} + +size_t LORENZ_save(void *storage) { + return lorenz_generator.Save(storage); +} + +size_t LORENZ_restore(const void *storage) { + return lorenz_generator.Restore(storage); +} + +void LORENZ_loop() { +} + +void LORENZ_menu() { + + menu::DualTitleBar::Draw(); + graphics.print("Freq1 "); + int32_t freq1 = SCALE8_16(lorenz_generator.get_freq1()) + (lorenz_generator.cv_freq1.value() * 16); + freq1 = USAT16(freq1); + graphics.print(freq1 >> 8); + + menu::DualTitleBar::SetColumn(1); + graphics.print("Freq2 "); + int32_t freq2 = SCALE8_16(lorenz_generator.get_freq2()) + (lorenz_generator.cv_freq2.value() * 16); + freq2 = USAT16(freq2); + graphics.print(freq2 >> 8); + + menu::DualTitleBar::Selected(lorenz_generator_state.selected_generator); + + menu::SettingsList settings_list(lorenz_generator_state.cursor); + menu::SettingsListItem list_item; + while (settings_list.available()) { + const int current = settings_list.Next(list_item); + list_item.DrawDefault(lorenz_generator.get_value(current), LorenzGenerator::value_attr(current)); + } +} + +void LORENZ_screensaver() { + OC::vectorscope_render(); +} + +void LORENZ_handleAppEvent(OC::AppEvent event) { + switch (event) { + case OC::APP_EVENT_RESUME: + break; + case OC::APP_EVENT_SUSPEND: + case OC::APP_EVENT_SCREENSAVER_ON: + case OC::APP_EVENT_SCREENSAVER_OFF: + break; + } +} + +void LORENZ_topButton() { + if (lorenz_generator_state.selected_generator) { + lorenz_generator.change_value(LORENZ_SETTING_FREQ2, 32); + } else { + lorenz_generator.change_value(LORENZ_SETTING_FREQ1, 32); + } +} + +void LORENZ_lowerButton() { + if (lorenz_generator_state.selected_generator) { + lorenz_generator.change_value(LORENZ_SETTING_FREQ2, -32); + } else { + lorenz_generator.change_value(LORENZ_SETTING_FREQ1, -32); + } +} + +void LORENZ_rightButton() { + lorenz_generator_state.cursor.toggle_editing(); +} + +void LORENZ_leftButton() { + lorenz_generator_state.selected_generator = 1 - lorenz_generator_state.selected_generator; +} + +void LORENZ_handleButtonEvent(const UI::Event &event) { + if (UI::EVENT_BUTTON_PRESS == event.type) { + switch (event.control) { + case OC::CONTROL_BUTTON_UP: + LORENZ_topButton(); + break; + case OC::CONTROL_BUTTON_DOWN: + LORENZ_lowerButton(); + break; + case OC::CONTROL_BUTTON_L: + LORENZ_leftButton(); + break; + case OC::CONTROL_BUTTON_R: + LORENZ_rightButton(); + break; + } + } +} + +void LORENZ_handleEncoderEvent(const UI::Event &event) { + + if (OC::CONTROL_ENCODER_L == event.control) { + if (lorenz_generator_state.selected_generator) { + lorenz_generator.change_value(LORENZ_SETTING_FREQ2, event.value); + } else { + lorenz_generator.change_value(LORENZ_SETTING_FREQ1, event.value); + } + } else if (OC::CONTROL_ENCODER_R == event.control) { + if (lorenz_generator_state.cursor.editing()) { + lorenz_generator.change_value(lorenz_generator_state.cursor.cursor_pos(), event.value); + } else { + lorenz_generator_state.cursor.Scroll(event.value); + } + } +} + +void LORENZ_debug() { + graphics.setPrintPos(2, 12); + graphics.print(lorenz_generator.cv_freq1.value()); + graphics.print(" "); + int32_t value = SCALE8_16(lorenz_generator.get_freq1()); + graphics.print(value); + graphics.setPrintPos(2, 22); + graphics.print(lorenz_generator.cv_freq2.value()); + graphics.print(" "); + value = SCALE8_16(lorenz_generator.get_freq2()); + graphics.print(value); + +} + +#endif // ENABLE_APP_LORENZ diff --git a/software/o_c_REV/APP_MIDI.ino b/software/o_c_REV/APP_MIDI.ino index 04191eec7..8ff44d482 100644 --- a/software/o_c_REV/APP_MIDI.ino +++ b/software/o_c_REV/APP_MIDI.ino @@ -638,10 +638,10 @@ private: int data2 = usbMIDI.getData2(); // Handle system exclusive dump for Setup data - if (message == MIDI_MSG_SYSEX) OnReceiveSysEx(); + if (message == HEM_MIDI_SYSEX) OnReceiveSysEx(); // Listen for incoming clock - if (message == MIDI_MSG_REALTIME && data1 == 0) { + if (message == HEM_MIDI_CLOCK) { if (++clock_count >= 24) clock_count = 0; } @@ -655,7 +655,7 @@ private: int in_fn = get_in_assign(ch); int in_ch = get_in_channel(ch); bool indicator = 0; - if (message == MIDI_MSG_NOTE_ON && in_ch == channel) { + if (message == HEM_MIDI_NOTE_ON && in_ch == channel) { if (note_in[ch] == -1) { // If this channel isn't already occupied with another note, handle Note On if (in_fn == MIDI_IN_NOTE && !note_captured) { // Send quantized pitch CV. Isolate transposition to quantizer so that it notes off aren't @@ -694,7 +694,7 @@ private: } } - if (message == MIDI_MSG_NOTE_OFF && in_ch == channel) { + if (message == HEM_MIDI_NOTE_OFF && in_ch == channel) { if (note_in[ch] == data1) { // If the note off matches the note on assingned to this output note_in[ch] = -1; if (in_fn == MIDI_IN_GATE) { @@ -711,7 +711,7 @@ private: } bool cc = (in_fn == MIDI_IN_MOD || in_fn >= MIDI_IN_EXPRESSION); - if (cc && message == MIDI_MSG_MIDI_CC && in_ch == channel) { + if (cc && message == HEM_MIDI_CC && in_ch == channel) { uint8_t cc = 1; // Modulation wheel if (in_fn == MIDI_IN_EXPRESSION) cc = 11; if (in_fn == MIDI_IN_PAN) cc = 10; @@ -728,14 +728,14 @@ private: } } - if (message == MIDI_MSG_AFTERTOUCH && in_fn == MIDI_IN_AFTERTOUCH && in_ch == channel) { + if (message == HEM_MIDI_AFTERTOUCH && in_fn == MIDI_IN_AFTERTOUCH && in_ch == channel) { // Send aftertouch to CV - Out(ch, Proportion(data2, 127, HSAPPLICATION_5V)); + Out(ch, Proportion(data1, 127, HSAPPLICATION_5V)); UpdateLog(1, ch, 3, in_ch, data1, data2); indicator = 1; } - if (message == MIDI_MSG_PITCHBEND && in_fn == MIDI_IN_PITCHBEND && in_ch == channel) { + if (message == HEM_MIDI_PITCHBEND && in_fn == MIDI_IN_PITCHBEND && in_ch == channel) { // Send pitch bend to CV int data = (data2 << 7) + data1 - 8192; Out(ch, Proportion(data, 0x7fff, HSAPPLICATION_3V)); @@ -830,6 +830,7 @@ private: } }; +// TOTAL EEPROM SIZE: 40*4 + 1 == 161 bytes SETTINGS_DECLARE(CaptainMIDI, MIDI_SETTING_LAST) { MIDI_SETUP_PARAMETER_LIST MIDI_SETUP_PARAMETER_LIST @@ -884,10 +885,11 @@ void MIDI_handleButtonEvent(const UI::Event &event) { captain_midi_instance.ToggleCursor(); if (event.control == OC::CONTROL_BUTTON_L) { if (event.type == UI::EVENT_BUTTON_LONG_PRESS) captain_midi_instance.Panic(); - else captain_midi_instance.ToggleDisplay(); + if (event.type == UI::EVENT_BUTTON_PRESS) captain_midi_instance.ToggleDisplay(); } - if (event.control == OC::CONTROL_BUTTON_UP) captain_midi_instance.SwitchSetup(1); + if (event.control == OC::CONTROL_BUTTON_UP && event.type == UI::EVENT_BUTTON_PRESS) + captain_midi_instance.SwitchSetup(1); if (event.control == OC::CONTROL_BUTTON_DOWN) { if (event.type == UI::EVENT_BUTTON_PRESS) captain_midi_instance.SwitchSetup(-1); if (event.type == UI::EVENT_BUTTON_LONG_PRESS) captain_midi_instance.ToggleCopyMode(); @@ -909,4 +911,4 @@ void MIDI_handleEncoderEvent(const UI::Event &event) { } -#endif \ No newline at end of file +#endif diff --git a/software/o_c_REV/APP_NeuralNetwork.ino b/software/o_c_REV/APP_NeuralNetwork.ino index 17c0513c7..7fb73d8e8 100644 --- a/software/o_c_REV/APP_NeuralNetwork.ino +++ b/software/o_c_REV/APP_NeuralNetwork.ino @@ -576,7 +576,7 @@ private: } }; -// Declare 216 bytes for storage +// TOTAL EEPROM SIZE: 216 bytes #define NN_EEPROM_DATA {0,0,255,"St",NULL,settings::STORAGE_TYPE_U8}, #define NN_DO_TWENTYFOUR_TIMES(A) A A A A A A A A A A A A A A A A A A A A A A A A SETTINGS_DECLARE(NeuralNetwork, NN_SETTING_LAST) { @@ -628,14 +628,14 @@ void NeuralNetwork_handleButtonEvent(const UI::Event &event) { // For left encoder, handle press and long press if (event.control == OC::CONTROL_BUTTON_L) { if (event.type == UI::EVENT_BUTTON_LONG_PRESS) NeuralNetwork_instance.OnLeftButtonLongPress(); - else NeuralNetwork_instance.OnLeftButtonPress(); + if (event.type == UI::EVENT_BUTTON_PRESS) NeuralNetwork_instance.OnLeftButtonPress(); } // For right encoder, only handle press (long press is reserved) if (event.control == OC::CONTROL_BUTTON_R && event.type == UI::EVENT_BUTTON_PRESS) NeuralNetwork_instance.OnRightButtonPress(); // For up button, handle only press (long press is reserved) - if (event.control == OC::CONTROL_BUTTON_UP) NeuralNetwork_instance.OnUpButtonPress(); + if (event.control == OC::CONTROL_BUTTON_UP && event.type == UI::EVENT_BUTTON_PRESS) NeuralNetwork_instance.OnUpButtonPress(); // For down button, handle press and long press if (event.control == OC::CONTROL_BUTTON_DOWN) { @@ -652,4 +652,4 @@ void NeuralNetwork_handleEncoderEvent(const UI::Event &event) { if (event.control == OC::CONTROL_ENCODER_R) NeuralNetwork_instance.OnRightEncoderMove(event.value); } -#endif \ No newline at end of file +#endif diff --git a/software/o_c_REV/APP_PASSENCORE.ino b/software/o_c_REV/APP_PASSENCORE.ino new file mode 100644 index 000000000..9c87da1d4 --- /dev/null +++ b/software/o_c_REV/APP_PASSENCORE.ino @@ -0,0 +1,1131 @@ +// Copyright (c) 2021 Naomi Seyfer +// +// Author of original O+C firmware: Max Stadler (mxmlnstdlr@gmail.com) +// Author of app scaffolding: Patrick Dowling (pld@gurkenkiste.com) +// Quantizer code: Emilie Gillet +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifdef ENABLE_APP_PASSENCORE + +#include + +#include "OC_apps.h" +#include "util/util_settings.h" +#include "util/util_trigger_delay.h" +#include "braids_quantizer.h" +#include "braids_quantizer_scales.h" +#include "OC_menus.h" +#include "OC_scales.h" +#include "OC_scale_edit.h" +#include "OC_strings.h" +#include "OC_chords.h" +#include "OC_chords_edit.h" +#include "OC_input_map.h" +#include "OC_input_maps.h" + +#define POSSIBLE_LEN 32 + +enum PASSENCORE_SETTINGS { + PASSENCORE_SETTING_SCALE, + PASSENCORE_SETTING_MASK, + PASSENCORE_SETTING_SAMPLE_TRIGGER, + PASSENCORE_SETTING_TARGET_TRIGGER, + PASSENCORE_SETTING_PASSING_TRIGGER, + PASSENCORE_SETTING_RESET_TRIGGER, + PASSENCORE_SETTING_A_OCTAVE, + PASSENCORE_SETTING_A_MIDRANGE, + PASSENCORE_SETTING_B_OCTAVE, + PASSENCORE_SETTING_B_MIDRANGE, + PASSENCORE_SETTING_C_OCTAVE, + PASSENCORE_SETTING_C_MIDRANGE, + PASSENCORE_SETTING_D_OCTAVE, + PASSENCORE_SETTING_D_MIDRANGE, + PASSENCORE_SETTING_BORROW_CHORDS, + PASSENCORE_SETTING_BASE_COLOR, + PASSENCORE_SETTING_CV3_ROLE, + PASSENCORE_SETTING_CV4_ROLE, + PASSENCORE_SETTING_LAST +}; + +enum PASSENCORE_FUNCTIONS { + PASSENCORE_FUNCTIONS_TONIC, + PASSENCORE_FUNCTIONS_TONIC_PREDOMINANT, + PASSENCORE_FUNCTIONS_PREDOMINANT, + PASSENCORE_FUNCTIONS_PREDOMINANT_DOMINANT, + PASSENCORE_FUNCTIONS_DOMINANT, + PASSENCORE_FUNCTIONS_MEDIANT, + PASSENCORE_FUNCTIONS_LAST, +}; + +enum PASSENCORE_COLORS { + PASSENCORE_COLORS_POWER, + PASSENCORE_COLORS_CLASSIC, + PASSENCORE_COLORS_EXTENDED, + PASSENCORE_COLORS_SUBSTITUTED, + PASSENCORE_COLORS_JAZZ, + PASSENCORE_COLORS_LAST, +}; + +const char* const passencore_color_names[] = {"power", "classic", "interesting", "ext/subst", "iono jazz?"}; + +enum PASSENCORE_CV_ROLES { + PASSENCORE_CV_ROLE_NONE, + PASSENCORE_CV_ROLE_INCLUDE, + PASSENCORE_CV_ROLE_ROOT, + PASSENCORE_CV_ROLE_BASS, + PASSENCORE_CV_ROLE_LAST, + // Future. + PASSENCORE_CV_ROLE_MELODY, + PASSENCORE_CV_ROLE_CHROMATIC_TRANSPOSE, +}; + +const char* const passencore_cv_role_names[] = {"-", "include", "root", "bass"}; + +enum PASSENCORE_MELODY_QUANT { + PASSENCORE_MELODY_QUANT_ACTIVE_SCALE, + PASSENCORE_MELODY_QUANT_TONIC_PENTATONIC, + PASSENCORE_MELODY_QUANT_CHORD_PENTATONIC, + PASSENCORE_MELODY_QUANT_MODAL_TRANSPOSE, + PASSENCORE_MELODY_QUANT_LAST, +}; + +PASSENCORE_FUNCTIONS PASSENCORE_FUNCTION_TABLE[] = { + PASSENCORE_FUNCTIONS_TONIC, + PASSENCORE_FUNCTIONS_PREDOMINANT_DOMINANT, + PASSENCORE_FUNCTIONS_MEDIANT, + PASSENCORE_FUNCTIONS_PREDOMINANT, + PASSENCORE_FUNCTIONS_DOMINANT, + PASSENCORE_FUNCTIONS_TONIC_PREDOMINANT, + PASSENCORE_FUNCTIONS_DOMINANT, +}; + +enum PASSENCORE_CHORD_TYPES { + CHORD_TYPES_NONE, + CHORD_TYPES_MAJOR, + CHORD_TYPES_MINOR, + CHORD_TYPES_DIMINISHED, + CHORD_TYPES_AUGMENTED, + CHORD_TYPES_LAST, +}; + + +struct PassenChord { + // 1-indexed scale degree + int8_t root; + // In order of importance to the chord. 1-indexed from the root, ex [0, 2, 4, 7] is a triad w/8ve + int8_t intervals[4]; + + int8_t accidental; + + // Attributes to score by; mutable. + PASSENCORE_FUNCTIONS function; + PASSENCORE_COLORS color; + + // Fitness; mutable; we sort by this. + int32_t score; + + int32_t samples[4]; + + PASSENCORE_CHORD_TYPES triad_type; + PASSENCORE_CHORD_TYPES seventh_type; + PASSENCORE_CHORD_TYPES ninth_type; + + void print() { + int acc = accidental; + CONSTRAIN(acc, -2, 2); + const char* accidental_part = OC::Strings::accidentals[2 + acc]; + int r = root; + CONSTRAIN(r, 0, 7); + const char* degree_part = (triad_type == CHORD_TYPES_MINOR || triad_type == CHORD_TYPES_DIMINISHED) ? OC::Strings::scale_degrees_min[root - 1] : OC::Strings::scale_degrees_maj[root - 1]; + char aug_dim = ' '; + if (triad_type == CHORD_TYPES_DIMINISHED) { + aug_dim = 0xb0; + } else if (triad_type == CHORD_TYPES_AUGMENTED) { + aug_dim = '+'; + } + graphics.printf("%s%s%c", accidental_part, degree_part, aug_dim); + if (triad_type == CHORD_TYPES_NONE) { + graphics.print("sus"); + } + switch (seventh_type) { + case CHORD_TYPES_NONE: + break; + case CHORD_TYPES_MINOR: + graphics.print("7"); + break; + case CHORD_TYPES_MAJOR: + graphics.print("maj7"); + break; + case CHORD_TYPES_DIMINISHED: + graphics.print("dim7"); + break; + case CHORD_TYPES_AUGMENTED: + graphics.print("aug7"); + break; + case CHORD_TYPES_LAST: + break; + } + switch (ninth_type) { + case CHORD_TYPES_NONE: + break; + case CHORD_TYPES_MINOR: + graphics.print("b9"); + break; + case CHORD_TYPES_MAJOR: + graphics.printf("add9"); + break; + case CHORD_TYPES_DIMINISHED: + graphics.printf("dim9"); + break; + case CHORD_TYPES_AUGMENTED: + graphics.printf("#9"); + break; + case CHORD_TYPES_LAST: + break; + } + } + + void print_notes() { + for (int i = 0; i < 4; i++) { + int tone = (samples[i]>>7)%12; + if (tone < 0) tone += 12; + int octave = (samples[i]>>7)/12; + graphics.print(OC::Strings::note_names_unpadded[tone]); + graphics.pretty_print(octave); + } + } +}; + +namespace menu = OC::menu; + +class PASSENCORE : public settings::SettingsBase { + public: + int get_scale(uint8_t selected_scale_slot_) const { + return values_[PASSENCORE_SETTING_SCALE]; + } + uint16_t get_mask() const { + return values_[PASSENCORE_SETTING_MASK]; + } + + // Wrappers for ScaleEdit + void scale_changed() { + force_update_ = true; + } + + int get_root(uint8_t DUMMY) const { + return 0x0; + } + // dummy + int get_scale_select() const { + return 0; + } + + // dummy + void set_scale_at_slot(int scale, uint16_t mask, int root, int transpose, uint8_t scale_slot) { + + } + + // dummy + int get_transpose(uint8_t DUMMY) const { + return 0; + } + uint16_t get_scale_mask(uint8_t scale_select) const { + return get_mask(); + } + + void update_scale_mask(uint16_t mask, uint8_t scale_select) { + apply_value(PASSENCORE_SETTING_MASK, mask); + last_mask_ = mask; + force_update_ = true; + } + + void set_scale(int scale) { + + if (scale != get_scale(DUMMY)) { + const OC::Scale &scale_def = OC::Scales::GetScale(scale); + uint16_t mask = get_mask(); + if (0 == (mask & ~(0xffff << scale_def.num_notes))) + mask = 0xffff; + apply_value(PASSENCORE_SETTING_MASK, mask); + apply_value(PASSENCORE_SETTING_SCALE, scale); + } + } + + void Init() { + last_mask_ = 0; + last_scale_ = -1; + set_scale((int)OC::Scales::SCALE_SEMI + 1); + quantizer_.Init(); + update_scale(true, false); + // start the playing chord on somethig we can start leading pleasantly from + add_chord(5, 1, 3, 5, 8, PASSENCORE_FUNCTIONS_DOMINANT, PASSENCORE_COLORS_CLASSIC); + voice_basic(possibilities[0]); + soundingChord = targetChord = possibilities[0]; + } + + void ISR(); + + void Loop(); + + void Draw(); + + private: + braids::Quantizer quantizer_; + PassenChord possibilities[POSSIBLE_LEN]; + int8_t p_len; + PassenChord soundingChord; + PassenChord targetChord; + int8_t function; + int8_t color; + int last_scale_; + uint16_t last_mask_; + bool force_update_; + + bool calc_new_chord_; + bool play_target_; + bool play_passing_chord_; + bool reset_; + + int32_t sample_a, sample_b, sample_c, sample_d; + + void reset_possibilities() { + p_len = 0; + } + + void find_new_chord(); + void add_variants(); + void add_basic_triads(); + void take_top_four(); + void score_by_function(); + void score_by_root(); + void score_by_include(); + void score_by_bass(); + void score_by_color(); + void add_voicings(); + void score_voicings(); + void add_passing_chords(); + void score_passing_voicing(); + void find_passing_chord(); + int score_voicing(const PassenChord& from, const PassenChord& to); + void voice(PassenChord& target, const PassenChord& previous, uint8_t leading_mask, bool force_bass_movement, bool constant); + void voice_basic(PassenChord& target); + + void add_chord(int8_t root, int8_t i1, int8_t i2, int8_t i3, int8_t i4, PASSENCORE_FUNCTIONS function, PASSENCORE_COLORS color); + void play_chord(PassenChord& chord) { + for (int i = 0; i < 4; i++) { + OC::DAC::set_pitch((DAC_CHANNEL)i, chord.samples[i], 0); + } + soundingChord = chord; + } + + bool update_scale(bool force, int32_t mask_rotate) { + + force_update_ = false; + const int scale = get_scale(DUMMY); + uint16_t mask = get_mask(); + + if (mask_rotate) + mask = OC::ScaleEditor::RotateMask(mask, OC::Scales::GetScale(scale).num_notes, mask_rotate); + + if (force || (last_scale_ != scale || last_mask_ != mask)) { + last_scale_ = scale; + last_mask_ = mask; + quantizer_.Configure(OC::Scales::GetScale(scale), mask); + return true; + } else { + return false; + } + } +}; + +// Returns the version of `sample` less than an octave above `comparison` +int32_t nearest_above(int32_t sample, int32_t comparison) { + int octaves_above = (sample - comparison) / (12 << 7); + return sample - octaves_above * (12 << 7); +} + +int32_t nearest_below(int32_t sample, int32_t comparison) { + int32_t above = nearest_above(sample, comparison); + if (above == sample) return above; + return above - (12 << 7); +} + +int32_t nearest_note(int32_t sample, int32_t comparison) { + int32_t above = nearest_above(sample, comparison); + int32_t below = nearest_below(sample, comparison); + return (abs(comparison - above) < abs(comparison - below)) ? above : below; +} + +void PASSENCORE::voice_basic(PassenChord& target) { + for (int i = 0; i < 4; i++) { + target.samples[i] = nearest_note( + target.samples[i], + (values_[PASSENCORE_SETTING_A_OCTAVE + 2 * i] + values_[PASSENCORE_SETTING_A_MIDRANGE + 2 * i]) << 7 + ); + } +} + +// Voice the target with reference to previous, using the leading mask to determine whether +// the voices should go up or down from previous - 0 is down, 1 is up, lsb is bass. +void PASSENCORE::voice(PassenChord& target, const PassenChord& previous, uint8_t leading_mask, bool force_bass_movement, bool constant) { + for (int v = 0; v < 4; v++) { + bool up = (leading_mask >> v) & 0x1; + // Look at voices we have not assigned yet, find the one that's nearest in the given direction to the same voice in the prev. chord. + int32_t best_score = 12 << 7; + int best_p = 0; + int32_t note, best_note = 0; + for (int p = v; constant ? (p == v) : (p < 4); p++) { + if (constant) { + note = nearest_note(target.samples[p], previous.samples[v]); + } else if (up) { + note = nearest_above(target.samples[p], previous.samples[v]); + } else { + note = nearest_below(target.samples[p], previous.samples[v]); + } + int score = abs(note - previous.samples[v]); + if (v == 0 && force_bass_movement && score == 0) score = (12 << 7); + if (score < best_score) { + best_score = score; + best_p = p; + best_note = note; + } + } + // Now swap the best note into place. + target.samples[best_p] = target.samples[v]; + target.samples[v] = best_note; + std::swap(target.intervals[best_p], target.intervals[v]); + } +} + +uint8_t PASSENCORE_VOICINGS[] = {0b0011, 0b1100, 0b0101, 0b1010, 0b1001, 0b0110}; + +void PASSENCORE::add_voicings() { + int len = p_len; + for (int i = 0; i < len; i++) { + for (int j = 0; j < 5; j++) { + if (p_len >= POSSIBLE_LEN) break; + possibilities[p_len] = possibilities[i]; + voice(possibilities[p_len], soundingChord, PASSENCORE_VOICINGS[j], j > 2, false); + p_len++; + } + // voice the orig. + voice(possibilities[i], soundingChord, PASSENCORE_VOICINGS[5], true, true); + } +} + +int PASSENCORE::score_voicing(const PassenChord& from, const PassenChord& to) { + int score = 8; + int held = 0; + bool prefer_root = false; + int32_t last_sample = -4 * (12 << 7); + for (int v = 0; v < 4; v++) { + if (to.intervals[v] > 8) { + prefer_root = true; + } + int movement = (abs(to.samples[v] - from.samples[v])) >> 7; + if (movement < 4) { + score += 2; + } + // We like staying constant and half-steps + if (movement == 0) { + score += 1; + held += 1; + } + if (movement == 1) { + score += 2; + } + if (v == 1 || v == 2) { + if (movement > 4) score -= 1; + if (movement > 8) score -= (movement - 8); + } else { + // Soprano and Bass can move more + if (movement > 7) score -= 1; + if (movement > 12) score -= (movement - 12); + } + // Gently keep our voices in range. + int32_t octave = values_[PASSENCORE_SETTING_A_OCTAVE + 2 * v]; + int32_t midrange = values_[PASSENCORE_SETTING_A_MIDRANGE + 2 * v]; + int32_t center = (12 * octave + midrange); + int32_t distance = abs((to.samples[v] >> 7) - center); + + if (distance > 8) score -= (distance - 8); + if (distance > 12) score -= (distance - 12); + + // penalize neighboring half-steps in the same chord. This should eliminate "false" sus chords. + if ((abs(last_sample - to.samples[v]) >> 7) == 1) { + score -= 10; + } + last_sample = to.samples[v]; + } + // penalize holding the same chord by the voicing score it got for holding those notes. + if (held == 4) { + score -= 12; + } + if (to.intervals[0] > 5) { + // avoid third inversions. + score -= 3; + } + if (to.intervals[0] == 1) { + // prefer root position, but not strongly + // score += 1; + } else if (prefer_root) { + score -= 3; + } + return score; +} + +void PASSENCORE::score_voicings() { + for (int c = 0; c < p_len; c++) { + int score = score_voicing(soundingChord, possibilities[c]); + possibilities[c].score *= score; + } +} + +void PASSENCORE::Draw() { + graphics.setPrintPos(0, 0); + for (int i = 0; i <= function; i++) { + graphics.print("**"); + } + graphics.setPrintPos(20, 10); + soundingChord.print(); + graphics.setPrintPos(20, 20); + soundingChord.print_notes(); + graphics.setPrintPos(50, 30); + graphics.print("to"); + graphics.setPrintPos(20, 40); + targetChord.print(); + graphics.setPrintPos(50, 50); + graphics.print(passencore_color_names[color]); + /* + graphics.setPrintPos(0, 0); + graphics.print("r"); + graphics.pretty_print(possibilities[0].root); + graphics.print("f"); + graphics.pretty_print(function); + graphics.print("c"); + graphics.pretty_print(color); + graphics.pretty_print(p_len); + graphics.setPrintPos(0, 10); + graphics.print("r"); + graphics.pretty_print(soundingChord.root); + //graphics.print("f"); + //graphics.pretty_print(sounding_chord.function); + graphics.print("c"); + graphics.pretty_print(soundingChord.color); + graphics.pretty_print(soundingChord.intervals[0]); + graphics.pretty_print(soundingChord.intervals[1]); + graphics.pretty_print(soundingChord.intervals[2]); + graphics.pretty_print(soundingChord.intervals[3]); + graphics.print("s"); + graphics.pretty_print(soundingChord.score); + for (int i = 1; i < 5; i++) { + graphics.setPrintPos(0, (i + 1) * 10); + graphics.print("r"); + graphics.pretty_print(possibilities[i].root); + //graphics.print("f"); + //graphics.pretty_print(possibilities[i].function); + graphics.print("c"); + graphics.pretty_print(possibilities[i].color); + graphics.pretty_print(possibilities[i].intervals[0]); + graphics.pretty_print(possibilities[i].intervals[1]); + graphics.pretty_print(possibilities[i].intervals[2]); + graphics.pretty_print(possibilities[i].intervals[3]); + graphics.print("s"); + graphics.pretty_print(possibilities[i].score); + + } + */ +} + +void PASSENCORE::Loop() { +} + +void PASSENCORE::find_passing_chord() { + add_passing_chords(); + for (int i = 0; i < p_len; i++) { + possibilities[i].score = 1; + } + score_by_color(); + take_top_four(); + add_voicings(); + score_passing_voicing(); + take_top_four(); +} + +void PASSENCORE::score_by_root() { + bool root = false; + int root_sample = 0; + + if (values_[PASSENCORE_SETTING_CV3_ROLE] == PASSENCORE_CV_ROLE_ROOT) { + root = true; + root_sample = chromatic_tone(quantizer_.Process(OC::ADC::raw_pitch_value(ADC_CHANNEL_3), 0, 0)); + } else if (values_[PASSENCORE_SETTING_CV4_ROLE] == PASSENCORE_CV_ROLE_ROOT) { + root = true; + root_sample = chromatic_tone(quantizer_.Process(OC::ADC::raw_pitch_value(ADC_CHANNEL_4), 0, 0)); + } + + for (int i = 0; i < p_len; i++) { + if (root) { + if (chromatic_tone(possibilities[i].samples[0]) == root_sample) { + possibilities[i].score += 1; + } else { + possibilities[i].score = 0; + } + } + } +} + + +int chromatic_tone(int32_t sample) { + int ret = (sample>>7)%12; + if (ret < 0) { + return ret + 12; + } + return ret; +} + +void PASSENCORE::score_by_bass() { + bool bass = false; + int bass_sample = 0; + + if (values_[PASSENCORE_SETTING_CV3_ROLE] == PASSENCORE_CV_ROLE_BASS) { + bass = true; + bass_sample = chromatic_tone(quantizer_.Process(OC::ADC::raw_pitch_value(ADC_CHANNEL_3), 0, 0)); + } else if (values_[PASSENCORE_SETTING_CV4_ROLE] == PASSENCORE_CV_ROLE_BASS) { + bass = true; + bass_sample = chromatic_tone(quantizer_.Process(OC::ADC::raw_pitch_value(ADC_CHANNEL_4), 0, 0)); + } + + for (int i = 0; i < p_len; i++) { + if (bass) { + if (chromatic_tone(possibilities[i].samples[0]) == bass_sample) { + possibilities[i].score += 1; + } else { + possibilities[i].score = 0; + } + } + } +} + +void PASSENCORE::score_by_include() { + bool include = false; + int include_sample = 0; + if (values_[PASSENCORE_SETTING_CV3_ROLE] == PASSENCORE_CV_ROLE_INCLUDE) { + include = true; + include_sample = chromatic_tone(quantizer_.Process(OC::ADC::raw_pitch_value(ADC_CHANNEL_3), 0, 0)); + } else if (values_[PASSENCORE_SETTING_CV4_ROLE] == PASSENCORE_CV_ROLE_INCLUDE) { + include = true; + include_sample = chromatic_tone(quantizer_.Process(OC::ADC::raw_pitch_value(ADC_CHANNEL_4), 0, 0)); + } else if (values_[PASSENCORE_SETTING_CV3_ROLE] == PASSENCORE_CV_ROLE_BASS) { + include = true; + include_sample = chromatic_tone(quantizer_.Process(OC::ADC::raw_pitch_value(ADC_CHANNEL_3), 0, 0)); + } else if (values_[PASSENCORE_SETTING_CV4_ROLE] == PASSENCORE_CV_ROLE_BASS) { + include = true; + include_sample = chromatic_tone(quantizer_.Process(OC::ADC::raw_pitch_value(ADC_CHANNEL_4), 0, 0)); + } + + for (int i = 0; i < p_len; i++) { + bool found = false; + if (include) { + for (int j = 0; j < 4; j++) { + if (chromatic_tone(possibilities[i].samples[j]) == include_sample) { + possibilities[i].score += 1; + found = true; + } + } + if (!found) { + possibilities[i].score = 0; + } + } + } +} + + +void PASSENCORE::find_new_chord() { + function = (int)((OC::ADC::value((ADC_CHANNEL)0) + 128) >> 9); + CONSTRAIN(function, 0, PASSENCORE_FUNCTIONS_MEDIANT - 1); + color = values_[PASSENCORE_SETTING_BASE_COLOR] + (int)((OC::ADC::value((ADC_CHANNEL)1) + 255) >> 9); + CONSTRAIN(color, 0, PASSENCORE_COLORS_LAST - 1); + + reset_possibilities(); + add_basic_triads(); + score_by_function(); + score_by_root(); + take_top_four(); + add_variants(); + score_by_function(); + score_by_root(); + score_by_color(); + score_by_include(); + if (reset_) { + score_by_bass(); + take_top_four(); + voice_basic(possibilities[0]); + reset_ = false; + } else { + take_top_four(); + add_voicings(); + score_voicings(); + score_by_bass(); + take_top_four(); + } + targetChord = possibilities[0]; +} + +void PASSENCORE::add_basic_triads() { + for (int degree = 1; degree < 8; degree++) { + add_chord(degree, 1, 3, 5, 8, PASSENCORE_FUNCTION_TABLE[degree - 1], PASSENCORE_COLORS_CLASSIC); + } +} + +void PASSENCORE::score_passing_voicing() { + + for (int i = 0; i < p_len; i++) { + int to_passing = score_voicing(soundingChord, possibilities[i]); + if (to_passing < 1) { + to_passing = 1; + } + int from_passing = score_voicing(possibilities[i], targetChord); + // Slightly nudge away from the same root as the target chord, since otherwise we'll only play sus chords as passing chords, they lead so well. + if (possibilities[i].root == targetChord.root) { + from_passing -= 2; + } + if (from_passing < 1) { + from_passing = 1; + } + possibilities[i].score *= (to_passing * from_passing); + } +} + +void PASSENCORE::add_passing_chords() { + p_len = 1; + // sus variants of target, but at lower color + add_chord(targetChord.root, 1, 2, 5, 8, targetChord.function, PASSENCORE_COLORS_CLASSIC); + add_chord(targetChord.root, 1, 4, 5, 8, targetChord.function, PASSENCORE_COLORS_CLASSIC); + add_chord(targetChord.root, 1, 3, 7, 9, targetChord.function, PASSENCORE_COLORS_EXTENDED); + add_chord(targetChord.root, 1, 4, 7, 5, targetChord.function, PASSENCORE_COLORS_EXTENDED); + // variants of the secondary dominant + int secondary_dominant = (targetChord.root + 3) % 7 + 1; + add_chord(secondary_dominant, 1, 3, 5, 8, PASSENCORE_FUNCTIONS_DOMINANT, PASSENCORE_COLORS_CLASSIC); + add_chord(secondary_dominant, 1, 3, 5, 7, PASSENCORE_FUNCTIONS_DOMINANT, PASSENCORE_COLORS_EXTENDED); + add_chord(secondary_dominant, 1, 4, 9, 7, PASSENCORE_FUNCTIONS_DOMINANT, PASSENCORE_COLORS_SUBSTITUTED); + // Stepwise passing chord + if (abs(soundingChord.root - targetChord.root) == 2) { + add_chord((soundingChord.root + targetChord.root) / 2, 1, 3, 7, 5, PASSENCORE_FUNCTIONS_PREDOMINANT, PASSENCORE_COLORS_EXTENDED); + add_chord((soundingChord.root + targetChord.root) / 2, 1, 3, 5, 8, PASSENCORE_FUNCTIONS_PREDOMINANT, PASSENCORE_COLORS_CLASSIC); + } else if (values_[PASSENCORE_SETTING_BORROW_CHORDS] && abs(soundingChord.samples[0] - targetChord.samples[0]) == (2 << 7)) { + int increment = (targetChord.samples[0] - soundingChord.samples[0]) / 2; + possibilities[p_len] = soundingChord; + possibilities[p_len].color = PASSENCORE_COLORS_JAZZ; + possibilities[p_len].accidental = (increment >> 7); + for (int i = 0; i < 4; i++) { + possibilities[p_len].samples[i] += increment; + } + p_len++; + } + // Neighbor of the secondary dominant + int neighbor_dominant = (secondary_dominant + 4) % 7 + 1; + add_chord(neighbor_dominant, 1, 3, 5, 8, PASSENCORE_FUNCTIONS_PREDOMINANT_DOMINANT, PASSENCORE_COLORS_CLASSIC); + add_chord(neighbor_dominant, 1, 3, 7, 5, PASSENCORE_FUNCTIONS_PREDOMINANT_DOMINANT, PASSENCORE_COLORS_SUBSTITUTED); +} + +void PASSENCORE::add_variants() { + int len = p_len; + for (int i = 0; i < len; i++) { + // Power chord + add_chord(possibilities[i].root, 1, 5, 8, 12, possibilities[i].function, PASSENCORE_COLORS_POWER); + // Don't resolve to a sus chord here; tonic sus chords can go as passing chords though + if (possibilities[i].root != 1) { + //Sus 2 + add_chord(possibilities[i].root, 1, 2, 5, 8, possibilities[i].function, PASSENCORE_COLORS_SUBSTITUTED); + // Sus 4 + add_chord(possibilities[i].root, 1, 4, 5, 8, possibilities[i].function, PASSENCORE_COLORS_SUBSTITUTED); + } + // 7th + add_chord(possibilities[i].root, 1, 3, 7, 5, possibilities[i].function, PASSENCORE_COLORS_EXTENDED); + // 7th+9 + add_chord(possibilities[i].root, 1, 3, 7, 9, possibilities[i].function, PASSENCORE_COLORS_JAZZ); + // 7th Sus 4 + add_chord(possibilities[i].root, 1, 4, 7, 5, possibilities[i].function, PASSENCORE_COLORS_JAZZ); + + if (possibilities[i].function == PASSENCORE_FUNCTIONS_DOMINANT) { + // Dominant 9 sus 4 + add_chord(possibilities[i].root, 1, 4, 9, 7, possibilities[i].function, PASSENCORE_COLORS_JAZZ); + } + } +} + + +void PASSENCORE::take_top_four() { + std::sort(possibilities, possibilities + p_len, [](PassenChord lhs, PassenChord rhs) { + return lhs.score > rhs.score; + }); + p_len = 4; +} + +void PASSENCORE::score_by_function() { + for (int c = 0; c < p_len; c++) { + int function_score = 2 - abs(function - possibilities[c].function); + // Major chords lead to the tonic better in both major (V) and minor (VII). + if (function == PASSENCORE_FUNCTIONS_DOMINANT && possibilities[c].triad_type == CHORD_TYPES_MAJOR) { + function_score++; + // Extra bonus function score for dominant 7ths + if (possibilities[c].seventh_type == CHORD_TYPES_MINOR) { + function_score++; + } + } + if (function_score < 0) { + function_score = 0; + } else if (function_score > 0) { + // Increase it a little so it won't have such a huge effect when we multiply by it later. + function_score += 2; + } + possibilities[c].score = function_score; + } +} + +void PASSENCORE::score_by_color() { + for (int c = 0; c < p_len; c++) { + int function_score = possibilities[c].score; + // Chords with some resemblance to the right function gain a color bonus as valid substitutions. + int color_modifier = (function_score > 0 && function_score < 3 && possibilities[c].color > 0) ? 2 : 0; + // Diminished and augmented chords are weird and only deserve to be played at high color. + if (possibilities[c].triad_type == CHORD_TYPES_DIMINISHED || possibilities[c].triad_type == CHORD_TYPES_AUGMENTED) { + color_modifier += 2; + } + int color_score = 3 - abs(color - (possibilities[c].color + color_modifier)); + if (color_score > 0) { + color_score += 1; + } else if (color_score < 0) { + // Increase it a little so it won't have such a huge effect when we multiply by it later. + color_score = 1; + } + possibilities[c].score *= color_score; + } +} + + +void PASSENCORE::ISR() { + // value() + uint32_t triggers = OC::DigitalInputs::clocked(); + calc_new_chord_ = triggers & DIGITAL_INPUT_MASK(values_[PASSENCORE_SETTING_SAMPLE_TRIGGER]); + play_target_ = triggers & DIGITAL_INPUT_MASK(values_[PASSENCORE_SETTING_TARGET_TRIGGER]); + play_passing_chord_ = triggers & DIGITAL_INPUT_MASK(values_[PASSENCORE_SETTING_PASSING_TRIGGER]); + reset_ = triggers & DIGITAL_INPUT_MASK(values_[PASSENCORE_SETTING_RESET_TRIGGER]); + update_scale(force_update_, false); + if (calc_new_chord_) { + find_new_chord(); + calc_new_chord_ = false; + } + if (play_passing_chord_) { + find_passing_chord(); + play_chord(possibilities[0]); + play_passing_chord_ = false; + } + if (play_target_) { + play_chord(targetChord); + play_target_ = false; + } +} + +void PASSENCORE::add_chord(int8_t root, int8_t i1, int8_t i2, int8_t i3, int8_t i4, PASSENCORE_FUNCTIONS f, PASSENCORE_COLORS c) { + if (p_len >= POSSIBLE_LEN) return; + possibilities[p_len].root = root; + possibilities[p_len].intervals[0] = i1; + possibilities[p_len].intervals[1] = i2; + possibilities[p_len].intervals[2] = i3; + possibilities[p_len].intervals[3] = i4; + possibilities[p_len].function = f; + possibilities[p_len].color = c; + possibilities[p_len].triad_type = CHORD_TYPES_NONE; + possibilities[p_len].seventh_type = CHORD_TYPES_NONE; + possibilities[p_len].ninth_type = CHORD_TYPES_NONE; + + int32_t root_note = quantizer_.Process(0, 0, root - 1); + int third = -1; + int seventh = -1; + bool borrow_dominant = values_[PASSENCORE_SETTING_BORROW_CHORDS] && f == PASSENCORE_FUNCTIONS_DOMINANT && (root == 5 || color >= PASSENCORE_COLORS_SUBSTITUTED); + + for (int i = 0; i < 4; i++) { + int32_t note = quantizer_.Process(0, 0, root + possibilities[p_len].intervals[i] - 2); + possibilities[p_len].samples[i] = note; + int halfsteps = ((note - root_note) >> 7); + switch (halfsteps) { + case 0: break; + case 1: + break; + case 2: break; + case 3: + third = i; + possibilities[p_len].triad_type = CHORD_TYPES_MINOR; + break; + case 4: + third = i; + possibilities[p_len].triad_type = CHORD_TYPES_MAJOR; + break; + case 5: break; + case 6: + possibilities[p_len].triad_type = CHORD_TYPES_DIMINISHED; + break; + case 7: break; + case 8: + possibilities[p_len].triad_type = CHORD_TYPES_AUGMENTED; + break; + case 9: + seventh = i; + possibilities[p_len].seventh_type = CHORD_TYPES_DIMINISHED; + break; + case 10: + seventh = i; + possibilities[p_len].seventh_type = CHORD_TYPES_MINOR; + break; + case 11: + seventh = i; + possibilities[p_len].seventh_type = CHORD_TYPES_MAJOR; + break; + case 12: break; + + case 13: + possibilities[p_len].ninth_type = CHORD_TYPES_MINOR; + break; + case 14: + possibilities[p_len].ninth_type = CHORD_TYPES_MAJOR; + break; + } + } + if (borrow_dominant) { + if (third >= 0 && possibilities[p_len].triad_type == CHORD_TYPES_MINOR) { + // Raise the minor third to major + possibilities[p_len].samples[third] += (1 << 7); + possibilities[p_len].triad_type = CHORD_TYPES_MAJOR; + } + if (seventh >= 0 && possibilities[p_len].seventh_type == CHORD_TYPES_MAJOR) { + possibilities[p_len].samples[seventh] -= (1 << 7); + } + } + p_len++; +} + +class PassencoreState { + public: + void Init() { + cursor.Init(PASSENCORE_SETTING_SCALE, PASSENCORE_SETTING_LAST - 1); + scale_editor.Init(false); + //chord_editor.Init(); + left_encoder_value = OC::Scales::SCALE_SEMI + 1; + } + + inline bool editing() const { + return cursor.editing(); + } + + inline int cursor_pos() const { + return cursor.cursor_pos(); + } + + menu::ScreenCursor cursor; + OC::ScaleEditor scale_editor; + //OC::ChordEditor chord_editor; + int left_encoder_value; +}; + + +// TOTAL EEPROM SIZE: 17 bytes +SETTINGS_DECLARE(PASSENCORE, PASSENCORE_SETTING_LAST) { + //PASSENCORE_SETTING_SCALE, + { OC::Scales::SCALE_SEMI + 1, OC::Scales::SCALE_SEMI, OC::Scales::NUM_SCALES - 1, "scale", OC::scale_names, settings::STORAGE_TYPE_U8 }, + //PASSENCORE_SETTING_MASK, + { 65535, 1, 65535, "mask -->", NULL, settings::STORAGE_TYPE_U16 }, // mask + //PASSENCORE_SETTING_SAMPLE_TRIGGER, + { 0, 0, 3, "sample trigger", OC::Strings::trigger_input_names, settings::STORAGE_TYPE_U4}, + //PASSENCORE_SETTING_TARGET_TRIGGER, + { 0, 0, 3, "target trigger", OC::Strings::trigger_input_names, settings::STORAGE_TYPE_U4}, + //PASSENCORE_SETTING_PASSING_TRIGGER, + { 2, 0, 3, "passing trigger", OC::Strings::trigger_input_names, settings::STORAGE_TYPE_U4}, + //PASSENCORE_SETTING_RESET_TRIGGER, + { 3, 0, 3, "reset trigger", OC::Strings::trigger_input_names, settings::STORAGE_TYPE_U4}, + //PASSENCORE_SETTING_A_OCTAVE, + { -1, -2, 3, "A octave", NULL, settings::STORAGE_TYPE_I8 }, + //PASSENCORE_SETTING_A_MIDRANGE, + { 0, -6, 6, "A mid range", NULL, settings::STORAGE_TYPE_I8}, + //PASSENCORE_SETTING_B_OCTAVE, + { -1, -2, 3, "B octave", NULL, settings::STORAGE_TYPE_I8 }, + //PASSENCORE_SETTING_B_MIDRANGE, + { 0, -6, 6, "B mid range", NULL, settings::STORAGE_TYPE_I8}, + //PASSENCORE_SETTING_C_OCTAVE, + { -1, -2, 3, "C octave", NULL, settings::STORAGE_TYPE_I8 }, + //PASSENCORE_SETTING_C_MIDRANGE, + { 0, -6, 6, "C mid range", NULL, settings::STORAGE_TYPE_I8}, + //PASSENCORE_SETTING_D_OCTAVE, + { -1, -2, 3, "D octave", NULL, settings::STORAGE_TYPE_I8 }, + //PASSENCORE_SETTING_D_MIDRANGE, + { 0, -6, 6, "D mid range", NULL, settings::STORAGE_TYPE_I8}, + //PASSENCORE_SETTING_BORROW_CHORDS, + { 1, 0, 1, "Borrow chords?", OC::Strings::no_yes, settings::STORAGE_TYPE_I8}, + // PASSENCORE_SETTING_BASE_COLOR, + { PASSENCORE_COLORS_CLASSIC, PASSENCORE_COLORS_POWER, PASSENCORE_COLORS_JAZZ, "color", passencore_color_names, settings::STORAGE_TYPE_I8}, + // PASSENCORE_SETTING_CV3_ROLE + { PASSENCORE_CV_ROLE_NONE, PASSENCORE_CV_ROLE_NONE, PASSENCORE_CV_ROLE_LAST - 1, "CV3", passencore_cv_role_names, settings::STORAGE_TYPE_I8}, + // PASSENCORE_SETTING_CV4_ROLE + + { PASSENCORE_CV_ROLE_NONE, PASSENCORE_CV_ROLE_NONE, PASSENCORE_CV_ROLE_LAST - 1, "CV4", passencore_cv_role_names, settings::STORAGE_TYPE_I8}, + +}; + +PassencoreState passencore_state; +PASSENCORE passencore_instance; + +void PASSENCORE_init() { + passencore_instance.Init(); + passencore_state.Init(); +} + +size_t PASSENCORE_storageSize() { + return PASSENCORE::storageSize(); +} + +size_t PASSENCORE_save(void *storage) { + return passencore_instance.Save(storage); +} + +void PASSENCORE_isr() { + return passencore_instance.ISR(); +} + +void PASSENCORE_leftButton() { + + if (passencore_state.left_encoder_value != passencore_instance.get_scale(DUMMY)) { + passencore_instance.set_scale(passencore_state.left_encoder_value); + } +} + + +void PASSENCORE_handleAppEvent(OC::AppEvent event) { + switch (event) { + case OC::APP_EVENT_RESUME: + passencore_state.cursor.set_editing(false); + passencore_state.scale_editor.Close(); + //passencore_state.chord_editor.Close(); + break; + case OC::APP_EVENT_SUSPEND: + case OC::APP_EVENT_SCREENSAVER_ON: + case OC::APP_EVENT_SCREENSAVER_OFF: + break; + } +} + +void PASSENCORE_loop() { + passencore_instance.Loop(); +} + +void PASSENCORE_menu() { + menu::TitleBar<0, 4, 0>::Draw(); + + // print scale + int scale = passencore_state.left_encoder_value; + graphics.movePrintPos(5, 0); + graphics.print(OC::scale_names[scale]); + if (passencore_instance.get_scale(DUMMY) == scale) + graphics.drawBitmap8(1, menu::QuadTitleBar::kTextY, 4, OC::bitmap_indicator_4x8); + menu::SettingsList < menu::kScreenLines, 0, menu::kDefaultValueX - 1 > settings_list(passencore_state.cursor); + menu::SettingsListItem list_item; + while (settings_list.available()) + { + const int current = settings_list.Next(list_item); + const int value = passencore_instance.get_value(current); + if (current == PASSENCORE_SETTING_MASK) { + menu::DrawMask(menu::kDisplayWidth, list_item.y, passencore_instance.get_mask(), OC::Scales::GetScale(passencore_instance.get_scale(DUMMY)).num_notes); + list_item.DrawNoValue(value, PASSENCORE::value_attr(current)); + } else { + list_item.DrawDefault(value, PASSENCORE::value_attr(current)); + } + + if (passencore_state.scale_editor.active()) + passencore_state.scale_editor.Draw(); + } +} + +void PASSENCORE_screensaver() { + passencore_instance.Draw(); +} + +void PASSENCORE_handleButtonEvent(const UI::Event & event) { + if (passencore_state.scale_editor.active()) { + passencore_state.scale_editor.HandleButtonEvent(event); + return; + } + //else if (passencore_state.chord_editor.active()) { + // passencore_state.chord_editor.HandleButtonEvent(event); + // return; + //} + if (UI::EVENT_BUTTON_PRESS == event.type) { + switch (event.control) { + case OC::CONTROL_BUTTON_UP: + //PASSENCORE_topButton(); + break; + case OC::CONTROL_BUTTON_DOWN: + //PASSENCORE_lowerButton(); + break; + case OC::CONTROL_BUTTON_L: + PASSENCORE_leftButton(); + break; + case OC::CONTROL_BUTTON_R: + if (passencore_state.cursor_pos() == PASSENCORE_SETTING_MASK) { + int scale = passencore_instance.get_scale(DUMMY); + if (OC::Scales::SCALE_NONE != scale) + passencore_state.scale_editor.Edit(&passencore_instance, scale); + } else { + passencore_state.cursor.toggle_editing(); + } + break; + } + } +} + +void PASSENCORE_handleEncoderEvent(const UI::Event & event) { + if (passencore_state.scale_editor.active()) { + passencore_state.scale_editor.HandleEncoderEvent(event); + return; + } + //else if (passencore_state.chord_editor.active()) { + // passencore_state.chord_editor.HandleEncoderEvent(event); + // return; + //} + if (OC::CONTROL_ENCODER_L == event.control) { + int value = passencore_state.left_encoder_value + event.value; + CONSTRAIN(value, OC::Scales::SCALE_SEMI, OC::Scales::NUM_SCALES - 1); + passencore_state.left_encoder_value = value; + } else if (OC::CONTROL_ENCODER_R == event.control) { + if (passencore_state.cursor.editing()) { + passencore_instance.change_value(passencore_state.cursor.cursor_pos(), event.value); + } else { + passencore_state.cursor.Scroll(event.value); + } + } +} + +size_t PASSENCORE_restore(const void *storage) { + size_t storage_size = passencore_instance.Restore(storage); + passencore_state.left_encoder_value = passencore_instance.get_scale(DUMMY); + passencore_instance.set_scale(passencore_state.left_encoder_value); + return storage_size; +} + +#endif // ENABLE_APP_PASSENCORE diff --git a/software/o_c_REV/APP_POLYLFO.ino b/software/o_c_REV/APP_POLYLFO.ino new file mode 100644 index 000000000..6f2216bf5 --- /dev/null +++ b/software/o_c_REV/APP_POLYLFO.ino @@ -0,0 +1,529 @@ +// Copyright (c) 2016 Patrick Dowling, Tim Churches +// +// Initial app implementation: Patrick Dowling (pld@gurkenkiste.com) +// Modifications by: Tim Churches (tim.churches@gmail.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// Quadrrature LFO app, based on the Mutable Instruments Frames Easter egg +// quadrature wavetable LFO by Olivier Gillet (see frames_poly_lfo.h/cpp) + +#ifdef ENABLE_APP_POLYLFO + +#include "OC_apps.h" +#include "OC_digital_inputs.h" +#include "OC_menus.h" + +#include "util/util_math.h" +#include "util/util_settings.h" +#include "frames_poly_lfo.h" + +#include "VBiasManager.h" + +enum POLYLFO_SETTINGS { + POLYLFO_SETTING_COARSE, + POLYLFO_SETTING_FINE, + POLYLFO_SETTING_TAP_TEMPO, + POLYLFO_SETTING_SHAPE, + POLYLFO_SETTING_SHAPE_SPREAD, + POLYLFO_SETTING_SPREAD, + POLYLFO_SETTING_COUPLING, + POLYLFO_SETTING_ATTENUATION, + POLYLFO_SETTING_OFFSET, + POLYLFO_SETTING_FREQ_RANGE, + POLYLFO_SETTING_FREQ_DIV_B, + POLYLFO_SETTING_FREQ_DIV_C, + POLYLFO_SETTING_FREQ_DIV_D, + POLYLFO_SETTING_B_XOR_A, + POLYLFO_SETTING_C_XOR_A, + POLYLFO_SETTING_D_XOR_A, + POLYLFO_SETTING_B_AM_BY_A, + POLYLFO_SETTING_C_AM_BY_B, + POLYLFO_SETTING_D_AM_BY_C, + POLYLFO_SETTING_CV4, + POLYLFO_SETTING_TR4_MULT, +#ifdef VOR + POLYLFO_SETTING_VBIAS, +#endif + POLYLFO_SETTING_LAST +}; + +class PolyLfo : public settings::SettingsBase { +public: + + uint16_t get_coarse() const { + return values_[POLYLFO_SETTING_COARSE]; + } + + int16_t get_fine() const { + return values_[POLYLFO_SETTING_FINE]; + } + + bool get_tap_tempo() const { + return static_cast(values_[POLYLFO_SETTING_TAP_TEMPO]); + } + + uint16_t get_freq_range() const { + return values_[POLYLFO_SETTING_FREQ_RANGE]; + } + + uint16_t get_shape() const { + return values_[POLYLFO_SETTING_SHAPE]; + } + + int16_t get_shape_spread() const { + return values_[POLYLFO_SETTING_SHAPE_SPREAD]; + } + + int16_t get_spread() const { + return values_[POLYLFO_SETTING_SPREAD]; + } + + int16_t get_coupling() const { + return values_[POLYLFO_SETTING_COUPLING]; + } + + uint16_t get_attenuation() const { + return values_[POLYLFO_SETTING_ATTENUATION]; + } + + int16_t get_offset() const { + return values_[POLYLFO_SETTING_OFFSET]; + } + + frames::PolyLfoFreqMultipliers get_freq_div_b() const { + return static_cast(values_[POLYLFO_SETTING_FREQ_DIV_B]); + } + + frames::PolyLfoFreqMultipliers get_freq_div_c() const { + return static_cast(values_[POLYLFO_SETTING_FREQ_DIV_C]); + } + + frames::PolyLfoFreqMultipliers get_freq_div_d() const { + return static_cast(values_[POLYLFO_SETTING_FREQ_DIV_D]); + } + + uint8_t get_b_xor_a() const { + return values_[POLYLFO_SETTING_B_XOR_A]; + } + + uint8_t get_c_xor_a() const { + return values_[POLYLFO_SETTING_C_XOR_A]; + } + + uint8_t get_d_xor_a() const { + return values_[POLYLFO_SETTING_D_XOR_A]; + } + + uint8_t get_b_am_by_a() const { + return values_[POLYLFO_SETTING_B_AM_BY_A]; + } + + uint8_t get_c_am_by_b() const { + return values_[POLYLFO_SETTING_C_AM_BY_B]; + } + + uint8_t get_d_am_by_c() const { + return values_[POLYLFO_SETTING_D_AM_BY_C]; + } + + uint8_t cv4_destination() const { + return values_[POLYLFO_SETTING_CV4]; + } + + uint8_t tr4_multiplier() const { + return values_[POLYLFO_SETTING_TR4_MULT]; + } + +#ifdef VOR + void saveVbias() { + VBiasManager *v = v->get(); + values_[POLYLFO_SETTING_VBIAS] = v->GetState(); + } + void restoreVbias() { + if (values_[POLYLFO_SETTING_VBIAS] <= 2) + { + VBiasManager *v = v->get(); + VBiasManager::VState bias_state = (VBiasManager::VState)values_[POLYLFO_SETTING_VBIAS]; + v->SetState( bias_state ); + //v->DrawPopupPerhaps(); + } + } +#endif + + void Init(); + + void freeze() { + frozen_ = true; + } + + void thaw() { + frozen_ = false; + } + + bool frozen() const { + return frozen_; + } + + uint8_t freq_mult() const { + return freq_mult_; + } + + void set_freq_mult(uint8_t freq_mult) { + freq_mult_ = freq_mult; + } + + frames::PolyLfo lfo; + bool frozen_; + uint8_t freq_mult_; + + // ISR update is at 16.666kHz, we don't need it that fast so smooth the values to ~1Khz + static constexpr int32_t kSmoothing = 16; + + SmoothedValue cv_freq; + SmoothedValue cv_shape; + SmoothedValue cv_spread; + SmoothedValue cv_mappable; +}; + +void PolyLfo::Init() { + InitDefaults(); + lfo.Init(); + frozen_= false; + freq_mult_ = 0x3; // == x2 / default +} + +const char* const freq_range_names[12] = { + "cosm", "geol", "glacl", "snail", "sloth", "vlazy", "lazy", "vslow", "slow", "med", "fast", "vfast", +}; + +const char* const freq_div_names[frames::POLYLFO_FREQ_MULT_LAST] = { + "16/1", "15/1", "14/1", "13/1", "12/1", "11/1", "10/1", "9/1", "8/1", "7/1", "6/1", "5/1", "4/1", "3/1", "5/2", "2/1", "5/3", "3/2", "5/4", + "unity", + "4/5", "2/3", "3/5", "1/2", "2/5", "1/3", "1/4", "1/5", "1/6", "1/7", "1/8", "1/9", "1/10", "1/11", "1/12", "1/13", "1/14", "1/15", "1/16" +}; + +const char* const xor_levels[9] = { + "off", " 1", " 2", " 3", " 4", " 5", " 6", " 7", " 8" +}; + +const char* const cv4_destinations[7] = { + "cplg", "sprd", " rng", "offs", "a->b", "b->c", "c->d" +}; + +const char* const tr4_multiplier[6] = { + "/8", "/4", "/2", "x2", "x4", "x8" +}; + +// TOTAL EEPROM SIZE: 22 bytes +SETTINGS_DECLARE(PolyLfo, POLYLFO_SETTING_LAST) { + { 64, 0, 255, "C", NULL, settings::STORAGE_TYPE_U8 }, + { 0, -128, 127, "F", NULL, settings::STORAGE_TYPE_I16 }, + { 0, 0, 1, "Tap tempo", OC::Strings::off_on, settings::STORAGE_TYPE_U8 }, + { 0, 0, 255, "Shape", NULL, settings::STORAGE_TYPE_U8 }, + { 0, -128, 127, "Shape spread", NULL, settings::STORAGE_TYPE_I8 }, + { 0, -128, 127, "Phase/frq sprd", NULL, settings::STORAGE_TYPE_I8 }, + { 0, -128, 127, "Coupling", NULL, settings::STORAGE_TYPE_I8 }, + { 230, 0, 230, "Output range", NULL, settings::STORAGE_TYPE_U8 }, + { 0, -128, 127, "Offset", NULL, settings::STORAGE_TYPE_I8 }, + { 9, 0, 11, "Freq range", freq_range_names, settings::STORAGE_TYPE_U8 }, + { frames::POLYLFO_FREQ_MULT_NONE, frames::POLYLFO_FREQ_MULT_BY16, frames::POLYLFO_FREQ_MULT_LAST - 1, "B freq ratio", freq_div_names, settings::STORAGE_TYPE_U8 }, + { frames::POLYLFO_FREQ_MULT_NONE, frames::POLYLFO_FREQ_MULT_BY16, frames::POLYLFO_FREQ_MULT_LAST - 1, "C freq ratio", freq_div_names, settings::STORAGE_TYPE_U8 }, + { frames::POLYLFO_FREQ_MULT_NONE, frames::POLYLFO_FREQ_MULT_BY16, frames::POLYLFO_FREQ_MULT_LAST - 1, "D freq ratio", freq_div_names, settings::STORAGE_TYPE_U8 }, + { 0, 0, 8, "B XOR A", xor_levels, settings::STORAGE_TYPE_U8 }, + { 0, 0, 8, "C XOR A", xor_levels, settings::STORAGE_TYPE_U8 }, + { 0, 0, 8, "D XOR A", xor_levels, settings::STORAGE_TYPE_U8 }, + { 0, 0, 127, "B AM by A", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 127, "C AM by B", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 127, "D AM by C", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 6, "CV4: DEST", cv4_destinations, settings::STORAGE_TYPE_U8 }, + { 3, 0, 5, "TR4: MULT", tr4_multiplier, settings::STORAGE_TYPE_U4 }, +#ifdef VOR + { 0, 0, 2, "VBias", OC::Strings::VOR_offsets, settings::STORAGE_TYPE_U4 }, +#endif + }; + +PolyLfo poly_lfo; +struct { + + POLYLFO_SETTINGS left_edit_mode; + menu::ScreenCursor cursor; + +} poly_lfo_state; + +void FASTRUN POLYLFO_isr() { + + bool reset_phase = OC::DigitalInputs::clocked(); + bool freeze = OC::DigitalInputs::read_immediate(); + bool tempo_sync = OC::DigitalInputs::clocked(); + + poly_lfo.cv_freq.push(OC::ADC::value()); + poly_lfo.cv_shape.push(OC::ADC::value()); + poly_lfo.cv_spread.push(OC::ADC::value()); + poly_lfo.cv_mappable.push(OC::ADC::value()); + + // Range in settings is (0-256] so this gets scaled to (0,65535] + // CV value is 12 bit so also needs scaling + + int32_t freq = SCALE8_16(poly_lfo.get_coarse()) + (poly_lfo.cv_freq.value() * 16) + poly_lfo.get_fine() * 2; + freq = USAT16(freq); + + poly_lfo.lfo.set_freq_range(poly_lfo.get_freq_range()); + + poly_lfo.lfo.set_sync(poly_lfo.get_tap_tempo()); + + int32_t shape = SCALE8_16(poly_lfo.get_shape()) + (poly_lfo.cv_shape.value() * 16); + poly_lfo.lfo.set_shape(USAT16(shape)); + + int32_t spread = SCALE8_16(poly_lfo.get_spread() + 128) + (poly_lfo.cv_spread.value() * 16); + poly_lfo.lfo.set_spread(USAT16(spread)); + + int32_t coupling = 0; + int32_t shape_spread = 0; + int32_t attenuation = 0; + int32_t offset = 0; + int32_t b_am_by_a = 0; + int32_t c_am_by_b = 0; + int32_t d_am_by_c = 0; + + switch (poly_lfo.cv4_destination()) { + case 1: // shape spread: -128, 127 + shape_spread = poly_lfo.cv_mappable.value() << 4; + break; + case 2: // attenuation: 0, 230 + attenuation = poly_lfo.cv_mappable.value() << 4; + break; + case 3: // offset: -128, 127 + offset = poly_lfo.cv_mappable.value() << 4; + break; + case 4: // "a->b", 0-127 + b_am_by_a = (poly_lfo.cv_mappable.value() + 15) >> 5; + break; + case 5: // "b->c", 0-127 + c_am_by_b = (poly_lfo.cv_mappable.value() + 15) >> 5; + break; + case 6: // "c->d", 0-127 + d_am_by_c = (poly_lfo.cv_mappable.value() + 15) >> 5; + break; + case 0: // coupling, -128, 127 + default: + coupling = poly_lfo.cv_mappable.value() << 4; + break; + } + + coupling += SCALE8_16(poly_lfo.get_coupling() + 127); + poly_lfo.lfo.set_coupling(USAT16(coupling)); + + shape_spread += SCALE8_16(poly_lfo.get_shape_spread() + 127); + poly_lfo.lfo.set_shape_spread(USAT16(shape_spread)); + + attenuation += SCALE8_16(poly_lfo.get_attenuation()); + poly_lfo.lfo.set_attenuation(USAT16(attenuation)); + + offset += SCALE8_16(poly_lfo.get_offset()); + poly_lfo.lfo.set_offset(USAT16(offset)); + + poly_lfo.lfo.set_freq_div_b(poly_lfo.get_freq_div_b()); + poly_lfo.lfo.set_freq_div_c(poly_lfo.get_freq_div_c()); + poly_lfo.lfo.set_freq_div_d(poly_lfo.get_freq_div_d()); + + poly_lfo.lfo.set_b_xor_a(poly_lfo.get_b_xor_a()); + poly_lfo.lfo.set_c_xor_a(poly_lfo.get_c_xor_a()); + poly_lfo.lfo.set_d_xor_a(poly_lfo.get_d_xor_a()); + + b_am_by_a += poly_lfo.get_b_am_by_a(); + CONSTRAIN(b_am_by_a, 0, 127); + poly_lfo.lfo.set_b_am_by_a(b_am_by_a); + + c_am_by_b += poly_lfo.get_c_am_by_b(); + CONSTRAIN(c_am_by_b, 0, 127); + poly_lfo.lfo.set_c_am_by_b(c_am_by_b); + + d_am_by_c += poly_lfo.get_d_am_by_c(); + CONSTRAIN(d_am_by_c, 0, 127); + poly_lfo.lfo.set_d_am_by_c(d_am_by_c); + + // div/multiply frequency if TR4 / gate high + int8_t freq_mult = digitalReadFast(TR4) ? 0xFF : poly_lfo.tr4_multiplier(); + poly_lfo.set_freq_mult(freq_mult); + + if (!freeze && !poly_lfo.frozen()) + poly_lfo.lfo.Render(freq, reset_phase, tempo_sync, freq_mult); + + OC::DAC::set(poly_lfo.lfo.dac_code(0)); + OC::DAC::set(poly_lfo.lfo.dac_code(1)); + OC::DAC::set(poly_lfo.lfo.dac_code(2)); + OC::DAC::set(poly_lfo.lfo.dac_code(3)); +} + +void POLYLFO_init() { + + poly_lfo_state.left_edit_mode = POLYLFO_SETTING_COARSE; + poly_lfo_state.cursor.Init(POLYLFO_SETTING_TAP_TEMPO, POLYLFO_SETTING_TR4_MULT); + poly_lfo.Init(); +} + +size_t POLYLFO_storageSize() { + return PolyLfo::storageSize(); +} + +size_t POLYLFO_save(void *storage) { + return poly_lfo.Save(storage); +} + +size_t POLYLFO_restore(const void *storage) { + return poly_lfo.Restore(storage); +} + +void POLYLFO_loop() { +} + +static const size_t kSmallPreviewBufferSize = 32; +uint16_t preview_buffer[kSmallPreviewBufferSize]; + +void POLYLFO_menu() { + + menu::DefaultTitleBar::Draw(); + if (poly_lfo.get_tap_tempo()) { + graphics.print("(T) Ch A: tap tempo") ; + } else { + float menu_freq_ = poly_lfo.lfo.get_freq_ch1(); + + if (poly_lfo.freq_mult() < 0xFF) + graphics.drawBitmap8(122, menu::DefaultTitleBar::kTextY, 4, OC::bitmap_indicator_4x8); + if (menu_freq_ >= 0.1f) { + const int f = int(floor(menu_freq_ * 100)); + const int value = f / 100; + const int cents = f % 100; + graphics.printf("(%s) Ch A: %3u.%02u Hz", PolyLfo::value_attr(poly_lfo_state.left_edit_mode).name, value, cents); + } else { + const int f = int(floor(1.0f / menu_freq_ * 1000)); + const int value = f / 1000; + const int cents = f % 1000; + graphics.printf("(%s) Ch A: %6u.%03us", PolyLfo::value_attr(poly_lfo_state.left_edit_mode).name, value, cents); + } + } + menu::SettingsList settings_list(poly_lfo_state.cursor); + menu::SettingsListItem list_item; + while (settings_list.available()) { + const int current = settings_list.Next(list_item); + const int value = poly_lfo.get_value(current); + if (POLYLFO_SETTING_SHAPE != current) { + list_item.DrawDefault(value, PolyLfo::value_attr(current)); + } else { + poly_lfo.lfo.RenderPreview(value << 8, preview_buffer, kSmallPreviewBufferSize); + const uint16_t *preview = preview_buffer; + uint16_t count = kSmallPreviewBufferSize; + weegfx::coord_t x = list_item.valuex; + while (count--) + graphics.setPixel(x++, list_item.y + 8 - (*preview++ >> 13)); + + list_item.endx = menu::kDefaultMenuEndX - 39; + list_item.DrawDefault(value, PolyLfo::value_attr(current)); + } + } +} + +void POLYLFO_screensaver() { + OC::scope_render(); +} + +void POLYLFO_handleAppEvent(OC::AppEvent event) { + switch (event) { + case OC::APP_EVENT_RESUME: + poly_lfo_state.cursor.set_editing(false); +#ifdef VOR + poly_lfo.restoreVbias(); +#endif + break; + case OC::APP_EVENT_SUSPEND: +#ifdef VOR + poly_lfo.saveVbias(); +#endif + case OC::APP_EVENT_SCREENSAVER_ON: + case OC::APP_EVENT_SCREENSAVER_OFF: + break; + } +} + +void POLYLFO_handleButtonEvent(const UI::Event &event) { + if (UI::EVENT_BUTTON_PRESS == event.type) { + switch (event.control) { + case OC::CONTROL_BUTTON_UP: + if (!poly_lfo.get_tap_tempo()) poly_lfo.change_value(POLYLFO_SETTING_COARSE, 32); + break; + case OC::CONTROL_BUTTON_DOWN: + if (!poly_lfo.get_tap_tempo()) poly_lfo.change_value(POLYLFO_SETTING_COARSE, -32); + break; + case OC::CONTROL_BUTTON_L: + if (!poly_lfo.get_tap_tempo()) { + if (POLYLFO_SETTING_COARSE == poly_lfo_state.left_edit_mode) + poly_lfo_state.left_edit_mode = POLYLFO_SETTING_FINE; + else + poly_lfo_state.left_edit_mode = POLYLFO_SETTING_COARSE; + } + break; + case OC::CONTROL_BUTTON_R: + poly_lfo_state.cursor.toggle_editing(); + break; + } + } + + if (UI::EVENT_BUTTON_LONG_PRESS == event.type) { + switch (event.control) { + case OC::CONTROL_BUTTON_DOWN: + poly_lfo.lfo.set_phase_reset_flag(true); + break; + default: + break; + } + } + +} + + +void POLYLFO_handleEncoderEvent(const UI::Event &event) { + if (OC::CONTROL_ENCODER_L == event.control) { + if (!poly_lfo.get_tap_tempo()) poly_lfo.change_value(poly_lfo_state.left_edit_mode, event.value); + } else if (OC::CONTROL_ENCODER_R == event.control) { + if (poly_lfo_state.cursor.editing()) { + poly_lfo.change_value(poly_lfo_state.cursor.cursor_pos(), event.value); + } else { + poly_lfo_state.cursor.Scroll(event.value); + } + } +} + +#ifdef POLYLFO_DEBUG +void POLYLFO_debug() { + graphics.setPrintPos(2, 12); + graphics.print(poly_lfo.cv_shape.value()); + graphics.print(" "); + int32_t value = SCALE8_16(poly_lfo.get_shape()); + graphics.print(value); + graphics.print(" "); + graphics.print(poly_lfo.cv_shape.value() * 16); + value += poly_lfo.cv_shape.value() * 16; + graphics.setPrintPos(2, 22); + graphics.print(value); graphics.print(" "); + value = USAT16(value); +} +#endif // POLYLFO_DEBUG + +#endif // ENABLE_APP_POLYLFO diff --git a/software/o_c_REV/APP_QQ.ino b/software/o_c_REV/APP_QQ.ino new file mode 100644 index 000000000..c99a6d5b2 --- /dev/null +++ b/software/o_c_REV/APP_QQ.ino @@ -0,0 +1,1585 @@ +// Copyright (c) 2015, 2016 Patrick Dowling, Tim Churches +// +// Initial app implementation: Patrick Dowling (pld@gurkenkiste.com) +// Modifications by: Tim Churches (tim.churches@gmail.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// Quad quantizer app, based around the the quantizer/scales implementation from +// from Braids by Olivier Gillet (see braids_quantizer.h/cc et al.). It has since +// grown a little bit... + +#ifdef ENABLE_APP_QUANTERMAIN + +#include "OC_apps.h" +#include "util/util_logistic_map.h" +#include "util/util_settings.h" +#include "util/util_trigger_delay.h" +#include "util/util_turing.h" +#include "util/util_integer_sequences.h" +#include "peaks_bytebeat.h" +#include "braids_quantizer.h" +#include "braids_quantizer_scales.h" +#include "OC_menus.h" +#include "OC_scales.h" +#include "OC_scale_edit.h" +#include "OC_strings.h" + +namespace menu = OC::menu; + +// unsigned long LAST_REDRAW_TIME = 0; +extern uint_fast8_t MENU_REDRAW; +// OC::UiMode ui_mode = OC::UI_MODE_MENU; + +#ifdef BUCHLA_4U + #define QQ_OFFSET_X 20 +#else + #define QQ_OFFSET_X 31 +#endif + +void QQ_downButtonLong(); +void QQ_topButton(); +void QQ_lowerButton(); +void QQ_leftButton(); +void QQ_rightButton(); +void QQ_leftButtonLong(); + +enum ChannelSetting { + CHANNEL_SETTING_SCALE, + CHANNEL_SETTING_ROOT, + CHANNEL_SETTING_MASK, + CHANNEL_SETTING_SOURCE, + CHANNEL_SETTING_AUX_SOURCE_DEST, + CHANNEL_SETTING_TRIGGER, + CHANNEL_SETTING_CLKDIV, + CHANNEL_SETTING_DELAY, + CHANNEL_SETTING_TRANSPOSE, + CHANNEL_SETTING_OCTAVE, + CHANNEL_SETTING_FINE, + CHANNEL_SETTING_TURING_LENGTH, + CHANNEL_SETTING_TURING_PROB, + CHANNEL_SETTING_TURING_MODULUS, + CHANNEL_SETTING_TURING_RANGE, + CHANNEL_SETTING_TURING_PROB_CV_SOURCE, + CHANNEL_SETTING_TURING_MODULUS_CV_SOURCE, + CHANNEL_SETTING_TURING_RANGE_CV_SOURCE, + CHANNEL_SETTING_LOGISTIC_MAP_R, + CHANNEL_SETTING_LOGISTIC_MAP_RANGE, + CHANNEL_SETTING_LOGISTIC_MAP_R_CV_SOURCE, + CHANNEL_SETTING_LOGISTIC_MAP_RANGE_CV_SOURCE, + CHANNEL_SETTING_BYTEBEAT_EQUATION, + CHANNEL_SETTING_BYTEBEAT_RANGE, + CHANNEL_SETTING_BYTEBEAT_P0, + CHANNEL_SETTING_BYTEBEAT_P1, + CHANNEL_SETTING_BYTEBEAT_P2, + CHANNEL_SETTING_BYTEBEAT_EQUATION_CV_SOURCE, + CHANNEL_SETTING_BYTEBEAT_RANGE_CV_SOURCE, + CHANNEL_SETTING_BYTEBEAT_P0_CV_SOURCE, + CHANNEL_SETTING_BYTEBEAT_P1_CV_SOURCE, + CHANNEL_SETTING_BYTEBEAT_P2_CV_SOURCE, + CHANNEL_SETTING_INT_SEQ_INDEX, + CHANNEL_SETTING_INT_SEQ_MODULUS, + CHANNEL_SETTING_INT_SEQ_RANGE, + CHANNEL_SETTING_INT_SEQ_DIRECTION, + CHANNEL_SETTING_INT_SEQ_BROWNIAN_PROB, + CHANNEL_SETTING_INT_SEQ_LOOP_START, + CHANNEL_SETTING_INT_SEQ_LOOP_LENGTH, + CHANNEL_SETTING_INT_SEQ_FRAME_SHIFT_PROB, + CHANNEL_SETTING_INT_SEQ_FRAME_SHIFT_RANGE, + CHANNEL_SETTING_INT_SEQ_STRIDE, + CHANNEL_SETTING_INT_SEQ_INDEX_CV_SOURCE, + CHANNEL_SETTING_INT_SEQ_MODULUS_CV_SOURCE, + CHANNEL_SETTING_INT_SEQ_RANGE_CV_SOURCE, + CHANNEL_SETTING_INT_SEQ_STRIDE_CV_SOURCE, + CHANNEL_SETTING_INT_SEQ_RESET_TRIGGER, + CHANNEL_SETTING_LAST +}; + +enum ChannelTriggerSource { + CHANNEL_TRIGGER_TR1, + CHANNEL_TRIGGER_TR2, + CHANNEL_TRIGGER_TR3, + CHANNEL_TRIGGER_TR4, + CHANNEL_TRIGGER_CONTINUOUS_UP, + CHANNEL_TRIGGER_CONTINUOUS_DOWN, + CHANNEL_TRIGGER_LAST +}; + +enum ChannelSource { + CHANNEL_SOURCE_CV1, + CHANNEL_SOURCE_CV2, + CHANNEL_SOURCE_CV3, + CHANNEL_SOURCE_CV4, + CHANNEL_SOURCE_TURING, + CHANNEL_SOURCE_LOGISTIC_MAP, + CHANNEL_SOURCE_BYTEBEAT, + CHANNEL_SOURCE_INT_SEQ, + CHANNEL_SOURCE_LAST +}; + +enum QQ_CV_DEST { + QQ_DEST_NONE, + QQ_DEST_ROOT, + QQ_DEST_OCTAVE, + QQ_DEST_TRANSPOSE, + QQ_DEST_MASK, + QQ_DEST_LAST +}; + +class QuantizerChannel : public settings::SettingsBase { +public: + + int get_scale(uint8_t dummy) const { + return values_[CHANNEL_SETTING_SCALE]; + } + + void set_scale(int scale) { + if (scale != get_scale(DUMMY)) { + const OC::Scale &scale_def = OC::Scales::GetScale(scale); + uint16_t mask = get_mask(); + if (0 == (mask & ~(0xffff << scale_def.num_notes))) + mask |= 0x1; + apply_value(CHANNEL_SETTING_MASK, mask); + apply_value(CHANNEL_SETTING_SCALE, scale); + } + } + + // dummy + int get_scale_select() const { + return 0; + } + + // dummy + void set_scale_at_slot(int scale, uint16_t mask, int root, int transpose, uint8_t scale_slot) { + + } + + // dummy + int get_transpose(uint8_t DUMMY) const { + return 0; + } + + int get_root() const { + return values_[CHANNEL_SETTING_ROOT]; + } + + int get_root(uint8_t DUMMY) const { + return 0x0; + } + + uint16_t get_mask() const { + return values_[CHANNEL_SETTING_MASK]; + } + + uint16_t get_rotated_scale_mask() const { + return last_mask_; + } + + ChannelSource get_source() const { + return static_cast(values_[CHANNEL_SETTING_SOURCE]); + } + + ChannelTriggerSource get_trigger_source() const { + return static_cast(values_[CHANNEL_SETTING_TRIGGER]); + } + + uint8_t get_channel_index() const { + return channel_index_; + } + + uint8_t get_clkdiv() const { + return values_[CHANNEL_SETTING_CLKDIV]; + } + + uint16_t get_trigger_delay() const { + return values_[CHANNEL_SETTING_DELAY]; + } + + int get_transpose() const { + return values_[CHANNEL_SETTING_TRANSPOSE]; + } + + int get_octave() const { + return values_[CHANNEL_SETTING_OCTAVE]; + } + + int get_fine() const { + return values_[CHANNEL_SETTING_FINE]; + } + + uint8_t get_aux_cv_dest() const { + return values_[CHANNEL_SETTING_AUX_SOURCE_DEST]; + } + + uint8_t get_turing_length() const { + return values_[CHANNEL_SETTING_TURING_LENGTH]; + } + + uint8_t get_turing_prob() const { + return values_[CHANNEL_SETTING_TURING_PROB]; + } + + uint8_t get_turing_modulus() const { + return values_[CHANNEL_SETTING_TURING_MODULUS]; + } + + uint8_t get_turing_range() const { + return values_[CHANNEL_SETTING_TURING_RANGE]; + } + + uint8_t get_turing_prob_cv_source() const { + return values_[CHANNEL_SETTING_TURING_PROB_CV_SOURCE]; + } + + uint8_t get_turing_modulus_cv_source() const { + return values_[CHANNEL_SETTING_TURING_MODULUS_CV_SOURCE]; + } + + uint8_t get_turing_range_cv_source() const { + return values_[CHANNEL_SETTING_TURING_RANGE_CV_SOURCE]; + } + + uint8_t get_logistic_map_r() const { + return values_[CHANNEL_SETTING_LOGISTIC_MAP_R]; + } + + uint8_t get_logistic_map_range() const { + return values_[CHANNEL_SETTING_LOGISTIC_MAP_RANGE]; + } + + uint8_t get_logistic_map_r_cv_source() const { + return values_[CHANNEL_SETTING_LOGISTIC_MAP_R_CV_SOURCE]; + } + + uint8_t get_logistic_map_range_cv_source() const { + return values_[CHANNEL_SETTING_LOGISTIC_MAP_RANGE_CV_SOURCE]; + } + + uint8_t get_bytebeat_equation() const { + return values_[CHANNEL_SETTING_BYTEBEAT_EQUATION]; + } + + uint8_t get_bytebeat_range() const { + return values_[CHANNEL_SETTING_BYTEBEAT_RANGE]; + } + + uint8_t get_bytebeat_p0() const { + return values_[CHANNEL_SETTING_BYTEBEAT_P0]; + } + + uint8_t get_bytebeat_p1() const { + return values_[CHANNEL_SETTING_BYTEBEAT_P1]; + } + + uint8_t get_bytebeat_p2() const { + return values_[CHANNEL_SETTING_BYTEBEAT_P2]; + } + + uint8_t get_bytebeat_equation_cv_source() const { + return values_[CHANNEL_SETTING_BYTEBEAT_EQUATION_CV_SOURCE]; + } + + uint8_t get_bytebeat_range_cv_source() const { + return values_[CHANNEL_SETTING_BYTEBEAT_RANGE_CV_SOURCE]; + } + + uint8_t get_bytebeat_p0_cv_source() const { + return values_[CHANNEL_SETTING_BYTEBEAT_P0_CV_SOURCE]; + } + + uint8_t get_bytebeat_p1_cv_source() const { + return values_[CHANNEL_SETTING_BYTEBEAT_P1_CV_SOURCE]; + } + + uint8_t get_bytebeat_p2_cv_source() const { + return values_[CHANNEL_SETTING_BYTEBEAT_P2_CV_SOURCE]; + } + + uint8_t get_int_seq_index() const { + return values_[CHANNEL_SETTING_INT_SEQ_INDEX]; + } + + uint8_t get_int_seq_modulus() const { + return values_[CHANNEL_SETTING_INT_SEQ_MODULUS]; + } + + uint8_t get_int_seq_range() const { + return values_[CHANNEL_SETTING_INT_SEQ_RANGE]; + } + + int16_t get_int_seq_start() const { + return static_cast(values_[CHANNEL_SETTING_INT_SEQ_LOOP_START]); + } + + void set_int_seq_start(uint8_t start_pos) { + values_[CHANNEL_SETTING_INT_SEQ_LOOP_START] = start_pos; + } + + int16_t get_int_seq_length() const { + return static_cast(values_[CHANNEL_SETTING_INT_SEQ_LOOP_LENGTH] - 1); + } + + bool get_int_seq_dir() const { + return static_cast(values_[CHANNEL_SETTING_INT_SEQ_DIRECTION]); + } + + int16_t get_int_seq_brownian_prob() const { + return static_cast(values_[CHANNEL_SETTING_INT_SEQ_BROWNIAN_PROB]); + } + + uint8_t get_int_seq_index_cv_source() const { + return values_[CHANNEL_SETTING_INT_SEQ_INDEX_CV_SOURCE]; + } + + uint8_t get_int_seq_modulus_cv_source() const { + return values_[CHANNEL_SETTING_INT_SEQ_MODULUS_CV_SOURCE]; + } + + uint8_t get_int_seq_range_cv_source() const { + return values_[CHANNEL_SETTING_INT_SEQ_RANGE_CV_SOURCE]; + } + + uint8_t get_int_seq_frame_shift_prob() const { + return values_[CHANNEL_SETTING_INT_SEQ_FRAME_SHIFT_PROB]; + } + + uint8_t get_int_seq_frame_shift_range() const { + return values_[CHANNEL_SETTING_INT_SEQ_FRAME_SHIFT_RANGE]; + } + + uint8_t get_int_seq_stride() const { + return values_[CHANNEL_SETTING_INT_SEQ_STRIDE]; + } + + uint8_t get_int_seq_stride_cv_source() const { + return values_[CHANNEL_SETTING_INT_SEQ_STRIDE_CV_SOURCE]; + } + + ChannelTriggerSource get_int_seq_reset_trigger_source() const { + return static_cast(values_[CHANNEL_SETTING_INT_SEQ_RESET_TRIGGER]); + } + + void clear_dest() { + // ... + schedule_mask_rotate_ = 0x0; + continuous_offset_ = 0x0; + prev_transpose_cv_ = 0x0; + prev_transpose_cv_ = 0x0; + prev_root_cv_ = 0x0; + } + + void Init(ChannelSource source, ChannelTriggerSource trigger_source) { + InitDefaults(); + apply_value(CHANNEL_SETTING_SOURCE, source); + apply_value(CHANNEL_SETTING_TRIGGER, trigger_source); + + channel_index_ = source; + force_update_ = true; + instant_update_ = false; + last_scale_ = -1; + last_mask_ = 0; + last_sample_ = 0; + clock_ = 0; + int_seq_reset_ = false; + continuous_offset_ = false; + schedule_mask_rotate_ = false; + prev_octave_cv_ = 0; + prev_transpose_cv_ = 0; + prev_root_cv_ = 0; + prev_destination_ = 0; + + trigger_delay_.Init(); + turing_machine_.Init(); + logistic_map_.Init(); + bytebeat_.Init(); + int_seq_.Init(get_int_seq_start(), get_int_seq_length()); + quantizer_.Init(); + update_scale(true, false); + trigger_display_.Init(); + update_enabled_settings(); + + scrolling_history_.Init(OC::DAC::kOctaveZero * 12 << 7); + } + + void force_update() { + force_update_ = true; + } + + void instant_update() { + instant_update_ = (~instant_update_) & 1u; + } + + inline void Update(uint32_t triggers, DAC_CHANNEL dac_channel) { + + uint8_t index = channel_index_; + + ChannelSource source = get_source(); + ChannelTriggerSource trigger_source = get_trigger_source(); + bool continuous = CHANNEL_TRIGGER_CONTINUOUS_UP == trigger_source || CHANNEL_TRIGGER_CONTINUOUS_DOWN == trigger_source; + bool triggered = !continuous && + (triggers & DIGITAL_INPUT_MASK(trigger_source - CHANNEL_TRIGGER_TR1)); + + if (source == CHANNEL_SOURCE_INT_SEQ) { + ChannelTriggerSource int_seq_reset_trigger_source = get_int_seq_reset_trigger_source() ; + int_seq_reset_ = (triggers & DIGITAL_INPUT_MASK(int_seq_reset_trigger_source - 1)); + } + + trigger_delay_.Update(); + if (triggered) + trigger_delay_.Push(OC::trigger_delay_ticks[get_trigger_delay()]); + triggered = trigger_delay_.triggered(); + + if (triggered) { + ++clock_; + if (clock_ >= get_clkdiv()) { + clock_ = 0; + } else { + triggered = false; + } + } + + bool update = continuous || triggered; + + if (update) + update_scale(force_update_, schedule_mask_rotate_); + + int32_t sample = last_sample_; + int32_t temp_sample = 0; + int32_t history_sample = 0; + + + switch (source) { + case CHANNEL_SOURCE_TURING: { + // this doesn't make sense when continuously quantizing; should be hidden via the menu ... + if (continuous) + break; + + turing_machine_.set_length(get_turing_length()); + int32_t probability = get_turing_prob(); + if (get_turing_prob_cv_source()) { + probability += (OC::ADC::value(static_cast(get_turing_prob_cv_source() - 1)) + 7) >> 4; + CONSTRAIN(probability, 0, 255); + } + turing_machine_.set_probability(probability); + if (triggered) { + uint32_t shift_register = turing_machine_.Clock(); + uint8_t range = get_turing_range(); + if (get_turing_range_cv_source()) { + range += (OC::ADC::value(static_cast(get_turing_range_cv_source() - 1)) + 15) >> 5; + CONSTRAIN(range, 1, 120); + } + + if (quantizer_.enabled()) { + + uint8_t modulus = get_turing_modulus(); + if (get_turing_modulus_cv_source()) { + modulus += (OC::ADC::value(static_cast(get_turing_modulus_cv_source() - 1)) + 15) >> 5; + CONSTRAIN(modulus, 2, 121); + } + + // Since our range is limited anyway, just grab the last byte for lengths > 8, + // otherwise scale to use bits. And apply the modulus + uint32_t shift = turing_machine_.length(); + uint32_t scaled = (shift_register & 0xff) * range; + scaled = (scaled >> (shift > 7 ? 8 : shift)) % modulus; + + // The quantizer uses a lookup codebook with 128 entries centered + // about 0, so we use the range/scaled output to lookup a note + // directly instead of changing to pitch first. + int32_t pitch = + quantizer_.Lookup(64 + range / 2 - scaled + get_transpose()) + (get_root() << 7); + sample = OC::DAC::pitch_to_scaled_voltage_dac(dac_channel, pitch, get_octave(), OC::DAC::get_voltage_scaling(dac_channel)); + history_sample = pitch + ((OC::DAC::kOctaveZero + get_octave()) * 12 << 7); + } else { + // Scale range by 128, so 12 steps = 1V + // We dont' need a calibrated value here, really. + uint32_t scaled = multiply_u32xu32_rshift(range << 7, shift_register, get_turing_length()); + scaled += get_transpose() << 7; + sample = OC::DAC::pitch_to_scaled_voltage_dac(dac_channel, scaled, get_octave(), OC::DAC::get_voltage_scaling(dac_channel)); + history_sample = scaled + ((OC::DAC::kOctaveZero + get_octave()) * 12 << 7); + } + } + } + break; + case CHANNEL_SOURCE_BYTEBEAT: { + // this doesn't make sense when continuously quantizing; should be hidden via the menu ... + if (continuous) + break; + + int32_t bytebeat_eqn = get_bytebeat_equation() << 12; + if (get_bytebeat_equation_cv_source()) { + bytebeat_eqn += (OC::ADC::value(static_cast(get_bytebeat_equation_cv_source() - 1)) << 4); + bytebeat_eqn = USAT16(bytebeat_eqn); + } + bytebeat_.set_equation(bytebeat_eqn); + + int32_t bytebeat_p0 = get_bytebeat_p0() << 8; + if (get_bytebeat_p0_cv_source()) { + bytebeat_p0 += (OC::ADC::value(static_cast(get_bytebeat_p0_cv_source() - 1)) << 4); + bytebeat_p0 = USAT16(bytebeat_p0); + } + bytebeat_.set_p0(bytebeat_p0); + + int32_t bytebeat_p1 = get_bytebeat_p1() << 8; + if (get_bytebeat_p1_cv_source()) { + bytebeat_p1 += (OC::ADC::value(static_cast(get_bytebeat_p1_cv_source() - 1)) << 4); + bytebeat_p1 = USAT16(bytebeat_p1); + } + bytebeat_.set_p1(bytebeat_p1); + + int32_t bytebeat_p2 = get_bytebeat_p2() << 8; + if (get_bytebeat_p2_cv_source()) { + bytebeat_p2 += (OC::ADC::value(static_cast(get_bytebeat_p2_cv_source() - 1)) << 4); + bytebeat_p2 = USAT16(bytebeat_p2); + } + bytebeat_.set_p2(bytebeat_p2); + + if (triggered) { + uint32_t bb = bytebeat_.Clock(); + uint8_t range = get_bytebeat_range(); + if (get_bytebeat_range_cv_source()) { + range += (OC::ADC::value(static_cast(get_bytebeat_range_cv_source() - 1)) + 15) >> 5; + CONSTRAIN(range, 1, 120); + } + + if (quantizer_.enabled()) { + + // Since our range is limited anyway, just grab the last byte + uint32_t scaled = ((bb >> 8) * range) >> 8; + + // The quantizer uses a lookup codebook with 128 entries centered + // about 0, so we use the range/scaled output to lookup a note + // directly instead of changing to pitch first. + int32_t pitch = + quantizer_.Lookup(64 + range / 2 - scaled + get_transpose()) + (get_root() << 7); + sample = OC::DAC::pitch_to_scaled_voltage_dac(dac_channel, pitch, get_octave(), OC::DAC::get_voltage_scaling(dac_channel)); + history_sample = pitch + ((OC::DAC::kOctaveZero + get_octave()) * 12 << 7); + } else { + // We dont' need a calibrated value here, really + int octave = get_octave(); + CONSTRAIN(octave, 0, 6); + sample = OC::DAC::get_octave_offset(dac_channel, octave) + (get_transpose() << 7); + // range is actually 120 (10 oct) but 65535 / 128 is close enough + sample += multiply_u32xu32_rshift32((static_cast(range) * 65535U) >> 7, bb << 16); + sample = USAT16(sample); + history_sample = sample; + } + } + } + break; + case CHANNEL_SOURCE_LOGISTIC_MAP: { + // this doesn't make sense when continuously quantizing; should be hidden via the menu ... + if (continuous) + break; + + logistic_map_.set_seed(123); + int32_t logistic_map_r = get_logistic_map_r(); + if (get_logistic_map_r_cv_source()) { + logistic_map_r += (OC::ADC::value(static_cast(get_logistic_map_r_cv_source() - 1)) + 7) >> 4; + CONSTRAIN(logistic_map_r, 0, 255); + } + logistic_map_.set_r(logistic_map_r); + if (triggered) { + int64_t logistic_map_x = logistic_map_.Clock(); + uint8_t range = get_logistic_map_range(); + if (get_logistic_map_range_cv_source()) { + range += (OC::ADC::value(static_cast(get_logistic_map_range_cv_source() - 1)) + 15) >> 5; + CONSTRAIN(range, 1, 120); + } + + if (quantizer_.enabled()) { + uint32_t logistic_scaled = (logistic_map_x * range) >> 24; + + // See above, may need tweaking + int32_t pitch = + quantizer_.Lookup(64 + range / 2 - logistic_scaled + get_transpose()) + (get_root() << 7); + sample = OC::DAC::pitch_to_scaled_voltage_dac(dac_channel, pitch, get_octave(), OC::DAC::get_voltage_scaling(dac_channel)); + history_sample = pitch + ((OC::DAC::kOctaveZero + get_octave()) * 12 << 7); + } else { + int octave = get_octave(); + CONSTRAIN(octave, 0, 6); + sample = OC::DAC::get_octave_offset(dac_channel, octave) + (get_transpose() << 7); + sample += multiply_u32xu32_rshift24((static_cast(range) * 65535U) >> 7, logistic_map_x); + sample = USAT16(sample); + history_sample = sample; + } + } + } + break; + case CHANNEL_SOURCE_INT_SEQ: { + // this doesn't make sense when continuously quantizing; should be hidden via the menu ... + if (continuous) + break; + + int_seq_.set_loop_direction(get_int_seq_dir()); + int_seq_.set_brownian_prob(get_int_seq_brownian_prob()); + int16_t int_seq_index = get_int_seq_index(); + int16_t int_seq_stride = get_int_seq_stride(); + + if (get_int_seq_index_cv_source()) { + int_seq_index += (OC::ADC::value(static_cast(get_int_seq_index_cv_source() - 1)) + 127) >> 8; + } + if (int_seq_index < 0) int_seq_index = 0; + if (int_seq_index > 11) int_seq_index = 11; + int_seq_.set_int_seq(int_seq_index); + int16_t int_seq_modulus_ = get_int_seq_modulus(); + if (get_int_seq_modulus_cv_source()) { + int_seq_modulus_ += (OC::ADC::value(static_cast(get_int_seq_modulus_cv_source() - 1)) + 31) >> 6; + CONSTRAIN(int_seq_modulus_, 2, 121); + } + int_seq_.set_int_seq_modulus(int_seq_modulus_); + + if (get_int_seq_stride_cv_source()) { + int_seq_stride += (OC::ADC::value(static_cast(get_int_seq_stride_cv_source() - 1)) + 31) >> 6; + } + if (int_seq_stride < 1) int_seq_stride = 1; + if (int_seq_stride > kIntSeqLen - 1) int_seq_stride = kIntSeqLen - 1; + int_seq_.set_fractal_stride(int_seq_stride); + + int_seq_.set_loop_start(get_int_seq_start()); + + int_seq_.set_loop_length(get_int_seq_length()); + + if (int_seq_reset_) { + int_seq_.reset_loop(); + int_seq_reset_ = false; + } + + if (triggered) { + // uint32_t is = int_seq_.Clock(); + // check whether frame should be shifted and if so, by how much. + if (get_int_seq_pass_go()) { + // OK, we're at the start of a loop or at one end of a pendulum swing + uint8_t fs_prob = get_int_seq_frame_shift_prob(); + uint8_t fs_range = get_int_seq_frame_shift_range(); + // Serial.print("fs_prob="); + // Serial.println(fs_prob); + // Serial.print("fs_range="); + // Serial.println(fs_range); + uint8_t fs_rand = static_cast(random(0,256)) ; + // Serial.print("fs_rand="); + // Serial.println(fs_rand); + // Serial.println("---"); + if (fs_rand < fs_prob) { + // OK, move the frame! + int16_t frame_shift = random(-fs_range, fs_range + 1) ; + // Serial.print("frame_shift="); + // Serial.println(frame_shift); + // Serial.print("current start pos="); + // Serial.println(get_int_seq_start()); + int16_t new_start_pos = get_int_seq_start() + frame_shift ; + // Serial.print("new_start_pos="); + // Serial.println(new_start_pos); + // Serial.println("==="); + if (new_start_pos < 0) new_start_pos = 0; + if (new_start_pos > kIntSeqLen - 2) new_start_pos = kIntSeqLen - 2; + set_int_seq_start(static_cast(new_start_pos)) ; + int_seq_.set_loop_start(get_int_seq_start()); + } + } + uint32_t is = int_seq_.Clock(); + int16_t range_ = get_int_seq_range(); + if (get_int_seq_range_cv_source()) { + range_ += (OC::ADC::value(static_cast(get_int_seq_range_cv_source() - 1)) + 31) >> 6; + CONSTRAIN(range_, 1, 120); + } + if (quantizer_.enabled()) { + + // Since our range is limited anyway, just grab the last byte + uint32_t scaled = ((is >> 4) * range_) >> 8; + + // The quantizer uses a lookup codebook with 128 entries centered + // about 0, so we use the range/scaled output to lookup a note + // directly instead of changing to pitch first. + int32_t pitch = + quantizer_.Lookup(64 + range_ / 2 - scaled + get_transpose()) + (get_root() << 7); + sample = OC::DAC::pitch_to_scaled_voltage_dac(dac_channel, pitch, get_octave(), OC::DAC::get_voltage_scaling(dac_channel)); + history_sample = pitch + ((OC::DAC::kOctaveZero + get_octave()) * 12 << 7); + } else { + // We dont' need a calibrated value here, really + int octave = get_octave(); + CONSTRAIN(octave, 0, 6); + sample = OC::DAC::get_octave_offset(dac_channel, octave) + (get_transpose() << 7); + // range is actually 120 (10 oct) but 65535 / 128 is close enough + sample += multiply_u32xu32_rshift32((static_cast(range_) * 65535U) >> 7, is << 20); + sample = USAT16(sample); + history_sample = sample; + } + } + } + break; + + default: { + if (update) { + + int32_t transpose = get_transpose() + prev_transpose_cv_; + int octave = get_octave() + prev_octave_cv_; + int root = get_root() + prev_root_cv_; + + int32_t pitch = quantizer_.enabled() + ? OC::ADC::raw_pitch_value(static_cast(source)) + : OC::ADC::pitch_value(static_cast(source)); + + // repurpose channel CV input? -- + uint8_t _aux_cv_destination = get_aux_cv_dest(); + + if (_aux_cv_destination != prev_destination_) + clear_dest(); + prev_destination_ = _aux_cv_destination; + + if (!continuous && index != source) { + // this doesn't really work all that well for continuous quantizing... + // see below + + switch(_aux_cv_destination) { + + case QQ_DEST_NONE: + break; + case QQ_DEST_TRANSPOSE: + transpose += (OC::ADC::value(static_cast(index)) + 63) >> 7; + break; + case QQ_DEST_ROOT: + root += (OC::ADC::value(static_cast(index)) + 127) >> 8; + break; + case QQ_DEST_OCTAVE: + octave += (OC::ADC::value(static_cast(index)) + 255) >> 9; + break; + case QQ_DEST_MASK: + update_scale(false, (OC::ADC::value(static_cast(index)) + 127) >> 8); + break; + default: + break; + } + } + + // limit: + CONSTRAIN(octave, -4, 4); + CONSTRAIN(root, 0, 11); + CONSTRAIN(transpose, -12, 12); + + int32_t quantized = quantizer_.Process(pitch, root << 7, transpose); + sample = temp_sample = OC::DAC::pitch_to_scaled_voltage_dac(dac_channel, quantized, octave + continuous_offset_, OC::DAC::get_voltage_scaling(dac_channel)); + + // continuous mode needs special treatment to give useful results. + // basically, update on note change only + + if (continuous && last_sample_ != sample) { + + bool _re_quantize = false; + int _aux_cv = 0; + + if (index != source) { + + switch(_aux_cv_destination) { + + case QQ_DEST_NONE: + break; + case QQ_DEST_TRANSPOSE: + _aux_cv = (OC::ADC::value(static_cast(index)) + 63) >> 7; + if (_aux_cv != prev_transpose_cv_) { + transpose = get_transpose() + _aux_cv; + CONSTRAIN(transpose, -12, 12); + prev_transpose_cv_ = _aux_cv; + _re_quantize = true; + } + break; + case QQ_DEST_ROOT: + _aux_cv = (OC::ADC::value(static_cast(index)) + 127) >> 8; + if (_aux_cv != prev_root_cv_) { + root = get_root() + _aux_cv; + CONSTRAIN(root, 0, 11); + prev_root_cv_ = _aux_cv; + _re_quantize = true; + } + break; + case QQ_DEST_OCTAVE: + _aux_cv = (OC::ADC::value(static_cast(index)) + 255) >> 9; + if (_aux_cv != prev_octave_cv_) { + octave = get_octave() + _aux_cv; + CONSTRAIN(octave, -4, 4); + prev_octave_cv_ = _aux_cv; + _re_quantize = true; + } + break; + case QQ_DEST_MASK: + schedule_mask_rotate_ = (OC::ADC::value(static_cast(index)) + 127) >> 8; + update_scale(force_update_, schedule_mask_rotate_); + break; + default: + break; + } + // end switch + } + + // offset when TR source = continuous ? + int8_t _trigger_offset = 0; + bool _trigger_update = false; + if (OC::DigitalInputs::read_immediate(static_cast(index))) { + _trigger_offset = (trigger_source == CHANNEL_TRIGGER_CONTINUOUS_UP) ? 1 : -1; + } + if (_trigger_offset != continuous_offset_) + _trigger_update = true; + continuous_offset_ = _trigger_offset; + + // run quantizer again -- presumably could be made more efficient... + if (_re_quantize) + quantized = quantizer_.Process(pitch, root << 7, transpose); + if (_re_quantize || _trigger_update) + sample = OC::DAC::pitch_to_scaled_voltage_dac(dac_channel, quantized, octave + continuous_offset_, OC::DAC::get_voltage_scaling(dac_channel)); + } + // end special treatment + + history_sample = quantized + ((OC::DAC::kOctaveZero + octave + continuous_offset_) * 12 << 7); + } + } + } // end switch + + bool changed = continuous ? (last_sample_ != temp_sample) : (last_sample_ != sample); + + if (changed) { + MENU_REDRAW = 1; + last_sample_ = continuous ? temp_sample : sample; + } + + OC::DAC::set(dac_channel, sample + get_fine()); + + if (triggered || (continuous && changed)) { + scrolling_history_.Push(history_sample); + trigger_display_.Update(1, true); + } else { + trigger_display_.Update(1, false); + } + scrolling_history_.Update(); + } + + // Wrappers for ScaleEdit + void scale_changed() { + force_update_ = true; + } + + uint16_t get_scale_mask(uint8_t scale_select) const { + return get_mask(); + } + + void update_scale_mask(uint16_t mask, uint16_t dummy) { + apply_value(CHANNEL_SETTING_MASK, mask); // Should automatically be updated + last_mask_ = mask; + force_update_ = true; + } + // + + uint8_t getTriggerState() const { + return trigger_display_.getState(); + } + + uint32_t get_shift_register() const { + return turing_machine_.get_shift_register(); + } + + uint32_t get_logistic_map_register() const { + return logistic_map_.get_register(); + } + + uint32_t get_bytebeat_register() const { + return bytebeat_.get_last_sample(); + } + + uint32_t get_int_seq_register() const { + return int_seq_.get_register(); + } + + int16_t get_int_seq_k() const { + return int_seq_.get_k(); + } + + int16_t get_int_seq_l() const { + return int_seq_.get_l(); + } + + int16_t get_int_seq_i() const { + return int_seq_.get_i(); + } + + int16_t get_int_seq_j() const { + return int_seq_.get_j(); + } + + int16_t get_int_seq_n() const { + return int_seq_.get_n(); + } + + int16_t get_int_seq_x() const { + return int_seq_.get_x(); + } + + bool get_int_seq_pass_go() const { + return int_seq_.get_pass_go(); + } + + // Maintain an internal list of currently available settings, since some are + // dependent on others. It's kind of brute force, but eh, works :) If other + // apps have a similar need, it can be moved to a common wrapper + + int num_enabled_settings() const { + return num_enabled_settings_; + } + + ChannelSetting enabled_setting_at(int index) const { + return enabled_settings_[index]; + } + + void update_enabled_settings() { + ChannelSetting *settings = enabled_settings_; + *settings++ = CHANNEL_SETTING_SCALE; + if (OC::Scales::SCALE_NONE != get_scale(DUMMY)) { + *settings++ = CHANNEL_SETTING_ROOT; + *settings++ = CHANNEL_SETTING_MASK; + } + *settings++ = CHANNEL_SETTING_SOURCE; + switch (get_source()) { + case CHANNEL_SOURCE_CV1: + case CHANNEL_SOURCE_CV2: + case CHANNEL_SOURCE_CV3: + case CHANNEL_SOURCE_CV4: + if (get_source() != get_channel_index()) + *settings++ = CHANNEL_SETTING_AUX_SOURCE_DEST; + break; + case CHANNEL_SOURCE_TURING: + *settings++ = CHANNEL_SETTING_TURING_LENGTH; + if (OC::Scales::SCALE_NONE != get_scale(DUMMY)) + *settings++ = CHANNEL_SETTING_TURING_MODULUS; + *settings++ = CHANNEL_SETTING_TURING_RANGE; + *settings++ = CHANNEL_SETTING_TURING_PROB; + if (OC::Scales::SCALE_NONE != get_scale(DUMMY)) + *settings++ = CHANNEL_SETTING_TURING_MODULUS_CV_SOURCE; + *settings++ = CHANNEL_SETTING_TURING_RANGE_CV_SOURCE; + *settings++ = CHANNEL_SETTING_TURING_PROB_CV_SOURCE; + break; + case CHANNEL_SOURCE_LOGISTIC_MAP: + *settings++ = CHANNEL_SETTING_LOGISTIC_MAP_R; + *settings++ = CHANNEL_SETTING_LOGISTIC_MAP_RANGE; + *settings++ = CHANNEL_SETTING_LOGISTIC_MAP_R_CV_SOURCE; + *settings++ = CHANNEL_SETTING_LOGISTIC_MAP_RANGE_CV_SOURCE; + break; + case CHANNEL_SOURCE_BYTEBEAT: + *settings++ = CHANNEL_SETTING_BYTEBEAT_EQUATION; + *settings++ = CHANNEL_SETTING_BYTEBEAT_RANGE; + *settings++ = CHANNEL_SETTING_BYTEBEAT_P0; + *settings++ = CHANNEL_SETTING_BYTEBEAT_P1; + *settings++ = CHANNEL_SETTING_BYTEBEAT_P2; + *settings++ = CHANNEL_SETTING_BYTEBEAT_EQUATION_CV_SOURCE; + *settings++ = CHANNEL_SETTING_BYTEBEAT_RANGE_CV_SOURCE; + *settings++ = CHANNEL_SETTING_BYTEBEAT_P0_CV_SOURCE; + *settings++ = CHANNEL_SETTING_BYTEBEAT_P1_CV_SOURCE; + *settings++ = CHANNEL_SETTING_BYTEBEAT_P2_CV_SOURCE; + break; + case CHANNEL_SOURCE_INT_SEQ: + *settings++ = CHANNEL_SETTING_INT_SEQ_INDEX; + *settings++ = CHANNEL_SETTING_INT_SEQ_MODULUS; + *settings++ = CHANNEL_SETTING_INT_SEQ_RANGE; + *settings++ = CHANNEL_SETTING_INT_SEQ_DIRECTION; + *settings++ = CHANNEL_SETTING_INT_SEQ_BROWNIAN_PROB; + *settings++ = CHANNEL_SETTING_INT_SEQ_LOOP_START; + *settings++ = CHANNEL_SETTING_INT_SEQ_LOOP_LENGTH; + *settings++ = CHANNEL_SETTING_INT_SEQ_STRIDE; + *settings++ = CHANNEL_SETTING_INT_SEQ_STRIDE_CV_SOURCE; + *settings++ = CHANNEL_SETTING_INT_SEQ_FRAME_SHIFT_PROB; + *settings++ = CHANNEL_SETTING_INT_SEQ_FRAME_SHIFT_RANGE; + *settings++ = CHANNEL_SETTING_INT_SEQ_INDEX_CV_SOURCE; + *settings++ = CHANNEL_SETTING_INT_SEQ_MODULUS_CV_SOURCE; + *settings++ = CHANNEL_SETTING_INT_SEQ_RANGE_CV_SOURCE; + *settings++ = CHANNEL_SETTING_INT_SEQ_RESET_TRIGGER; + break; + default: + break; + } + *settings++ = CHANNEL_SETTING_TRIGGER; + if (get_trigger_source() < CHANNEL_TRIGGER_CONTINUOUS_UP) { + *settings++ = CHANNEL_SETTING_CLKDIV; + *settings++ = CHANNEL_SETTING_DELAY; + } + *settings++ = CHANNEL_SETTING_OCTAVE; + *settings++ = CHANNEL_SETTING_TRANSPOSE; + *settings++ = CHANNEL_SETTING_FINE; + + num_enabled_settings_ = settings - enabled_settings_; + } + + static bool indentSetting(ChannelSetting s) { + switch (s) { + case CHANNEL_SETTING_TURING_LENGTH: + case CHANNEL_SETTING_TURING_MODULUS: + case CHANNEL_SETTING_TURING_RANGE: + case CHANNEL_SETTING_TURING_PROB: + case CHANNEL_SETTING_TURING_MODULUS_CV_SOURCE: + case CHANNEL_SETTING_TURING_RANGE_CV_SOURCE: + case CHANNEL_SETTING_TURING_PROB_CV_SOURCE: + case CHANNEL_SETTING_LOGISTIC_MAP_R: + case CHANNEL_SETTING_LOGISTIC_MAP_RANGE: + case CHANNEL_SETTING_LOGISTIC_MAP_R_CV_SOURCE: + case CHANNEL_SETTING_LOGISTIC_MAP_RANGE_CV_SOURCE: + case CHANNEL_SETTING_BYTEBEAT_EQUATION: + case CHANNEL_SETTING_BYTEBEAT_RANGE: + case CHANNEL_SETTING_BYTEBEAT_P0: + case CHANNEL_SETTING_BYTEBEAT_P1: + case CHANNEL_SETTING_BYTEBEAT_P2: + case CHANNEL_SETTING_BYTEBEAT_EQUATION_CV_SOURCE: + case CHANNEL_SETTING_BYTEBEAT_RANGE_CV_SOURCE: + case CHANNEL_SETTING_BYTEBEAT_P0_CV_SOURCE: + case CHANNEL_SETTING_BYTEBEAT_P1_CV_SOURCE: + case CHANNEL_SETTING_BYTEBEAT_P2_CV_SOURCE: + case CHANNEL_SETTING_INT_SEQ_INDEX: + case CHANNEL_SETTING_INT_SEQ_MODULUS: + case CHANNEL_SETTING_INT_SEQ_RANGE: + case CHANNEL_SETTING_INT_SEQ_DIRECTION: + case CHANNEL_SETTING_INT_SEQ_BROWNIAN_PROB: + case CHANNEL_SETTING_INT_SEQ_LOOP_START: + case CHANNEL_SETTING_INT_SEQ_LOOP_LENGTH: + case CHANNEL_SETTING_INT_SEQ_FRAME_SHIFT_PROB: + case CHANNEL_SETTING_INT_SEQ_FRAME_SHIFT_RANGE: + case CHANNEL_SETTING_INT_SEQ_STRIDE: + case CHANNEL_SETTING_INT_SEQ_INDEX_CV_SOURCE: + case CHANNEL_SETTING_INT_SEQ_MODULUS_CV_SOURCE: + case CHANNEL_SETTING_INT_SEQ_RANGE_CV_SOURCE: + case CHANNEL_SETTING_INT_SEQ_STRIDE_CV_SOURCE: + case CHANNEL_SETTING_INT_SEQ_RESET_TRIGGER: + case CHANNEL_SETTING_CLKDIV: + case CHANNEL_SETTING_DELAY: + return true; + default: break; + } + return false; + } + + void RenderScreensaver(weegfx::coord_t x) const; + +private: + bool force_update_; + bool instant_update_; + int last_scale_; + uint16_t last_mask_; + int32_t last_sample_; + uint8_t clock_; + bool int_seq_reset_; + int8_t continuous_offset_; + int8_t channel_index_; + int32_t schedule_mask_rotate_; + int8_t prev_destination_; + int8_t prev_octave_cv_; + int8_t prev_transpose_cv_; + int8_t prev_root_cv_; + + util::TriggerDelay trigger_delay_; + util::TuringShiftRegister turing_machine_; + util::LogisticMap logistic_map_; + peaks::ByteBeat bytebeat_ ; + util::IntegerSequence int_seq_ ; + braids::Quantizer quantizer_; + OC::DigitalInputDisplay trigger_display_; + + int num_enabled_settings_; + ChannelSetting enabled_settings_[CHANNEL_SETTING_LAST]; + + OC::vfx::ScrollingHistory scrolling_history_; + + bool update_scale(bool force, int32_t mask_rotate) { + + force_update_ = false; + const int scale = get_scale(DUMMY); + uint16_t mask = get_mask(); + + if (mask_rotate) + mask = OC::ScaleEditor::RotateMask(mask, OC::Scales::GetScale(scale).num_notes, mask_rotate); + + if (force || (last_scale_ != scale || last_mask_ != mask)) { + last_scale_ = scale; + last_mask_ = mask; + quantizer_.Configure(OC::Scales::GetScale(scale), mask); + return true; + } else { + return false; + } + } +}; + +const char* const channel_input_sources[CHANNEL_SOURCE_LAST] = { + "CV1", "CV2", "CV3", "CV4", "Turing", "Lgstc", "ByteB", "IntSq" +}; + +const char* const aux_cv_dest[5] = { + "-", "root", "oct", "trns", "mask" +}; + +// TOTAL EEPROM SIZE: 4 * 40 bytes +SETTINGS_DECLARE(QuantizerChannel, CHANNEL_SETTING_LAST) { + { OC::Scales::SCALE_SEMI, 0, OC::Scales::NUM_SCALES - 1, "Scale", OC::scale_names, settings::STORAGE_TYPE_U8 }, + { 0, 0, 11, "Root", OC::Strings::note_names_unpadded, settings::STORAGE_TYPE_U8 }, + { 65535, 1, 65535, "Active notes", NULL, settings::STORAGE_TYPE_U16 }, + { CHANNEL_SOURCE_CV1, CHANNEL_SOURCE_CV1, CHANNEL_SOURCE_LAST - 1, "CV Source", channel_input_sources, settings::STORAGE_TYPE_U8 }, + { QQ_DEST_NONE, QQ_DEST_NONE, QQ_DEST_LAST - 1, "CV aux >", aux_cv_dest, settings::STORAGE_TYPE_U8 }, + { CHANNEL_TRIGGER_CONTINUOUS_DOWN, 0, CHANNEL_TRIGGER_LAST - 1, "Trigger source", OC::Strings::channel_trigger_sources, settings::STORAGE_TYPE_U8 }, + { 1, 1, 16, "Clock div", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, OC::kNumDelayTimes - 1, "Trigger delay", OC::Strings::trigger_delay_times, settings::STORAGE_TYPE_U8 }, + { 0, -5, 7, "Transpose", NULL, settings::STORAGE_TYPE_I8 }, + { 0, -4, 4, "Octave", NULL, settings::STORAGE_TYPE_I8 }, + { 0, -999, 999, "Fine", NULL, settings::STORAGE_TYPE_I16 }, + { 16, 1, 32, "LFSR length", NULL, settings::STORAGE_TYPE_U8 }, + { 128, 0, 255, "LFSR prb", NULL, settings::STORAGE_TYPE_U8 }, + { 24, 2, 121, "LFSR modulus", NULL, settings::STORAGE_TYPE_U8 }, + { 12, 1, 120, "LFSR range", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 4, "LFSR prb CV >", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "LFSR mod CV >", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "LFSR rng CV >", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 128, 1, 255, "Logistic r", NULL, settings::STORAGE_TYPE_U8 }, + { 12, 1, 120, "Logistic range", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 4, "Log r CV >", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "Log rng CV >", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 15, "Bytebeat eqn", OC::Strings::bytebeat_equation_names, settings::STORAGE_TYPE_U8 }, + { 12, 1, 120, "Bytebeat rng", NULL, settings::STORAGE_TYPE_U8 }, + { 8, 1, 255, "Bytebeat P0", NULL, settings::STORAGE_TYPE_U8 }, + { 12, 1, 255, "Bytebeat P1", NULL, settings::STORAGE_TYPE_U8 }, + { 14, 1, 255, "Bytebeat P2", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 4, "Bb eqn CV src", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "Bb rng CV src", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "Bb P0 CV src", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "Bb P1 CV src", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "Bb P2 CV src", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 11, "IntSeq", OC::Strings::integer_sequence_names, settings::STORAGE_TYPE_U4 }, + { 24, 2, 121, "IntSeq modul.", NULL, settings::STORAGE_TYPE_U8 }, + { 12, 1, 120, "IntSeq range", NULL, settings::STORAGE_TYPE_U8 }, + { 1, 0, 1, "IntSeq dir", OC::Strings::integer_sequence_dirs, settings::STORAGE_TYPE_U4 }, + { 0, 0, 255, "> Brownian prob", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, kIntSeqLen - 2, "IntSeq start", NULL, settings::STORAGE_TYPE_U8 }, + { 8, 2, kIntSeqLen, "IntSeq len", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 255, "IntSeq FS prob", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 5, "IntSeq FS rng", NULL, settings::STORAGE_TYPE_U4 }, + { 1, 1, kIntSeqLen - 1, "Fractal stride", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 4, "IntSeq CV >", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "IntSeq mod CV", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "IntSeq rng CV", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "F. stride CV >", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "IntSeq reset", OC::Strings::trigger_input_names_none, settings::STORAGE_TYPE_U4 } +}; + +// WIP refactoring to better encapsulate and for possible app interface change +class QuadQuantizer { +public: + void Init() { + selected_channel = 0; + cursor.Init(CHANNEL_SETTING_SCALE, CHANNEL_SETTING_LAST - 1); + scale_editor.Init(false); + } + + inline bool editing() const { + return cursor.editing(); + } + + inline int cursor_pos() const { + return cursor.cursor_pos(); + } + + int selected_channel; + menu::ScreenCursor cursor; + OC::ScaleEditor scale_editor; +}; + +QuadQuantizer qq_state; +QuantizerChannel quantizer_channels[4]; + +void QQ_init() { + + qq_state.Init(); + for (size_t i = 0; i < 4; ++i) { + quantizer_channels[i].Init(static_cast(CHANNEL_SOURCE_CV1 + i), + static_cast(CHANNEL_TRIGGER_TR1 + i)); + } + + qq_state.cursor.AdjustEnd(quantizer_channels[0].num_enabled_settings() - 1); +} + +size_t QQ_storageSize() { + return 4 * QuantizerChannel::storageSize(); +} + +size_t QQ_save(void *storage) { + size_t used = 0; + for (size_t i = 0; i < 4; ++i) { + used += quantizer_channels[i].Save(static_cast(storage) + used); + } + return used; +} + +size_t QQ_restore(const void *storage) { + size_t used = 0; + for (size_t i = 0; i < 4; ++i) { + used += quantizer_channels[i].Restore(static_cast(storage) + used); + quantizer_channels[i].update_scale_mask(quantizer_channels[i].get_mask(), 0x0); + quantizer_channels[i].update_enabled_settings(); + } + qq_state.cursor.AdjustEnd(quantizer_channels[0].num_enabled_settings() - 1); + return used; +} + +void QQ_handleAppEvent(OC::AppEvent event) { + switch (event) { + case OC::APP_EVENT_RESUME: + qq_state.cursor.set_editing(false); + qq_state.scale_editor.Close(); + break; + case OC::APP_EVENT_SUSPEND: + case OC::APP_EVENT_SCREENSAVER_ON: + case OC::APP_EVENT_SCREENSAVER_OFF: + break; + } +} + +void QQ_isr() { + uint32_t triggers = OC::DigitalInputs::clocked(); + quantizer_channels[0].Update(triggers, DAC_CHANNEL_A); + quantizer_channels[1].Update(triggers, DAC_CHANNEL_B); + quantizer_channels[2].Update(triggers, DAC_CHANNEL_C); + quantizer_channels[3].Update(triggers, DAC_CHANNEL_D); +} + +void QQ_loop() { +} + +void QQ_menu() { + + menu::QuadTitleBar::Draw(); + for (int i = 0, x = 0; i < 4; ++i, x += 32) { + const QuantizerChannel &channel = quantizer_channels[i]; + menu::QuadTitleBar::SetColumn(i); + graphics.print((char)('A' + i)); + graphics.movePrintPos(2, 0); + int octave = channel.get_octave(); + if (octave) + graphics.pretty_print(octave); + + menu::QuadTitleBar::DrawGateIndicator(i, channel.getTriggerState()); + } + menu::QuadTitleBar::Selected(qq_state.selected_channel); + + + const QuantizerChannel &channel = quantizer_channels[qq_state.selected_channel]; + + menu::SettingsList settings_list(qq_state.cursor); + menu::SettingsListItem list_item; + while (settings_list.available()) { + const int setting = + channel.enabled_setting_at(settings_list.Next(list_item)); + const int value = channel.get_value(setting); + const settings::value_attr &attr = QuantizerChannel::value_attr(setting); + + switch (setting) { + case CHANNEL_SETTING_SCALE: + list_item.SetPrintPos(); + if (list_item.editing) { + menu::DrawEditIcon(6, list_item.y, value, attr); + graphics.movePrintPos(6, 0); + } + graphics.print(OC::scale_names[value]); + list_item.DrawCustom(); + break; + case CHANNEL_SETTING_MASK: + menu::DrawMask(menu::kDisplayWidth, list_item.y, channel.get_rotated_scale_mask(), OC::Scales::GetScale(channel.get_scale(DUMMY)).num_notes); + list_item.DrawNoValue(value, attr); + break; + case CHANNEL_SETTING_TRIGGER: + { + if (channel.get_source() > CHANNEL_SOURCE_CV4) + list_item.DrawValueMax(value, attr, CHANNEL_TRIGGER_TR4); + else + list_item.DrawDefault(value, attr); + } + break; + case CHANNEL_SETTING_SOURCE: + if (CHANNEL_SOURCE_TURING == channel.get_source()) { + int turing_length = channel.get_turing_length(); + int w = turing_length >= 16 ? 16 * 3 : turing_length * 3; + + menu::DrawMask(menu::kDisplayWidth, list_item.y, channel.get_shift_register(), turing_length); + list_item.valuex = menu::kDisplayWidth - w - 1; + list_item.DrawNoValue(value, attr); + break; + // Fall through if not Turing + } + default: + if (QuantizerChannel::indentSetting(static_cast(setting))) + list_item.x += menu::kIndentDx; + if (setting == CHANNEL_SETTING_SOURCE && channel.get_trigger_source() > CHANNEL_TRIGGER_TR4) + list_item.DrawValueMax(value, attr, CHANNEL_TRIGGER_TR4); + else list_item.DrawDefault(value, attr); + break; + } + } + + if (qq_state.scale_editor.active()) + qq_state.scale_editor.Draw(); +} + +void QQ_handleButtonEvent(const UI::Event &event) { + + if (UI::EVENT_BUTTON_LONG_PRESS == event.type && OC::CONTROL_BUTTON_DOWN == event.control) + QQ_downButtonLong(); + + if (qq_state.scale_editor.active()) { + qq_state.scale_editor.HandleButtonEvent(event); + return; + } + + if (UI::EVENT_BUTTON_PRESS == event.type) { + switch (event.control) { + case OC::CONTROL_BUTTON_UP: + QQ_topButton(); + break; + case OC::CONTROL_BUTTON_DOWN: + QQ_lowerButton(); + break; + case OC::CONTROL_BUTTON_L: + QQ_leftButton(); + break; + case OC::CONTROL_BUTTON_R: + QQ_rightButton(); + break; + } + } else if (UI::EVENT_BUTTON_LONG_PRESS == event.type) { + if (OC::CONTROL_BUTTON_L == event.control) + QQ_leftButtonLong(); + } +} + +void QQ_handleEncoderEvent(const UI::Event &event) { + if (qq_state.scale_editor.active()) { + qq_state.scale_editor.HandleEncoderEvent(event); + return; + } + + if (OC::CONTROL_ENCODER_L == event.control) { + int selected_channel = qq_state.selected_channel + event.value; + CONSTRAIN(selected_channel, 0, 3); + qq_state.selected_channel = selected_channel; + + QuantizerChannel &selected = quantizer_channels[qq_state.selected_channel]; + qq_state.cursor.AdjustEnd(selected.num_enabled_settings() - 1); + } else if (OC::CONTROL_ENCODER_R == event.control) { + QuantizerChannel &selected = quantizer_channels[qq_state.selected_channel]; + if (qq_state.editing()) { + ChannelSetting setting = selected.enabled_setting_at(qq_state.cursor_pos()); + if (CHANNEL_SETTING_MASK != setting) { + + int event_value = event.value; + + switch (setting) { + case CHANNEL_SETTING_TRIGGER: + { + if (selected.get_trigger_source() == CHANNEL_TRIGGER_TR4 && selected.get_source() > CHANNEL_SOURCE_CV4 && event.value > 0) + event_value = 0x0; + } + break; + case CHANNEL_SETTING_SOURCE: { + if (selected.get_source() == CHANNEL_SOURCE_CV4 && selected.get_trigger_source() > CHANNEL_TRIGGER_TR4 && event.value > 0) + event_value = 0x0; + } + break; + default: + break; + } + + if (selected.change_value(setting, event_value)) + selected.force_update(); + + switch (setting) { + case CHANNEL_SETTING_SCALE: + case CHANNEL_SETTING_TRIGGER: + case CHANNEL_SETTING_SOURCE: + selected.update_enabled_settings(); + qq_state.cursor.AdjustEnd(selected.num_enabled_settings() - 1); + break; + default: + break; + } + } + } else { + qq_state.cursor.Scroll(event.value); + } + } +} + +void QQ_topButton() { + QuantizerChannel &selected = quantizer_channels[qq_state.selected_channel]; + if (selected.change_value(CHANNEL_SETTING_OCTAVE, 1)) { + selected.force_update(); + } +} + +void QQ_lowerButton() { + QuantizerChannel &selected = quantizer_channels[qq_state.selected_channel]; + if (selected.change_value(CHANNEL_SETTING_OCTAVE, -1)) { + selected.force_update(); + } +} + +void QQ_rightButton() { + QuantizerChannel &selected = quantizer_channels[qq_state.selected_channel]; + switch (selected.enabled_setting_at(qq_state.cursor_pos())) { + case CHANNEL_SETTING_MASK: { + int scale = selected.get_scale(DUMMY); + if (OC::Scales::SCALE_NONE != scale) { + qq_state.scale_editor.Edit(&selected, scale); + } + } + break; + default: + qq_state.cursor.toggle_editing(); + break; + } +} + +void QQ_leftButton() { + qq_state.selected_channel = (qq_state.selected_channel + 1) & 3; + QuantizerChannel &selected = quantizer_channels[qq_state.selected_channel]; + qq_state.cursor.AdjustEnd(selected.num_enabled_settings() - 1); +} + +void QQ_leftButtonLong() { + QuantizerChannel &selected_channel = quantizer_channels[qq_state.selected_channel]; + int scale = selected_channel.get_scale(DUMMY); + int root = selected_channel.get_root(); + for (int i = 0; i < 4; ++i) { + if (i != qq_state.selected_channel) { + quantizer_channels[i].apply_value(CHANNEL_SETTING_ROOT, root); + quantizer_channels[i].set_scale(scale); + } + } +} + +void QQ_downButtonLong() { + + QuantizerChannel &selected_channel = quantizer_channels[qq_state.selected_channel]; + selected_channel.update_scale_mask(0xFFFF, 0x0); +} + +int32_t history[5]; +static const weegfx::coord_t kBottom = 60; + +inline int32_t render_pitch(int32_t pitch, weegfx::coord_t x, weegfx::coord_t width) { + CONSTRAIN(pitch, 0, 120 << 7); + int32_t octave = pitch / (12 << 7); + pitch -= (octave * 12 << 7); + graphics.drawHLine(x, kBottom - ((pitch * 4) >> 7), width); + return octave; +} + +void QuantizerChannel::RenderScreensaver(weegfx::coord_t start_x) const { + + // History + scrolling_history_.Read(history); + weegfx::coord_t scroll_pos = (scrolling_history_.get_scroll_pos() * 6) >> 8; + + // Top: Show gate & CV (or register bits) + menu::DrawGateIndicator(start_x + 1, 2, getTriggerState()); + const ChannelSource source = get_source(); + switch (source) { + case CHANNEL_SOURCE_TURING: + menu::DrawMask(start_x + 31, 1, get_shift_register(), get_turing_length()); + break; + case CHANNEL_SOURCE_LOGISTIC_MAP: + menu::DrawMask(start_x + 31, 1, get_logistic_map_register(), 32); + break; + case CHANNEL_SOURCE_BYTEBEAT: + menu::DrawMask(start_x + 31, 1, get_bytebeat_register(), 8); + break; + case CHANNEL_SOURCE_INT_SEQ: + // graphics.setPrintPos(start_x + 31 - 16, 4); + graphics.setPrintPos(start_x + 8, 4); + graphics.print(get_int_seq_k()); + // menu::DrawMask(start_x + 31, 1, get_int_seq_register(), 8); + break; + default: { + graphics.setPixel(start_x + QQ_OFFSET_X - 16, 4); + int32_t cv = OC::ADC::value(static_cast(source)); + cv = (cv * 24 + 2047) >> 12; + if (cv < 0) + graphics.drawRect(start_x + QQ_OFFSET_X - 16 + cv, 6, -cv, 2); + else if (cv > 0) + graphics.drawRect(start_x + QQ_OFFSET_X - 16, 6, cv, 2); + else + graphics.drawRect(start_x + QQ_OFFSET_X - 16, 6, 1, 2); + } + break; + } + +#ifdef QQ_DEBUG_SCREENSAVER + graphics.drawVLinePattern(start_x + 31, 0, 64, 0x55); +#endif + + // Draw semitone intervals, 4px apart + weegfx::coord_t x = start_x + 26; + weegfx::coord_t y = kBottom; + for (int i = 0; i < 12; ++i, y -= 4) + graphics.setPixel(x, y); + + x = start_x + 1; + render_pitch(history[0], x, scroll_pos); x += scroll_pos; + render_pitch(history[1], x, 6); x += 6; + render_pitch(history[2], x, 6); x += 6; + render_pitch(history[3], x, 6); x += 6; + + int32_t octave = render_pitch(history[4], x, 6 - scroll_pos); + graphics.drawBitmap8(start_x + 28, kBottom - octave * 4 - 1, OC::kBitmapLoopMarkerW, OC::bitmap_loop_markers_8 + OC::kBitmapLoopMarkerW); +} + +void QQ_screensaver() { +#ifdef QQ_DEBUG_SCREENSAVER + debug::CycleMeasurement render_cycles; +#endif + + quantizer_channels[0].RenderScreensaver(0); + quantizer_channels[1].RenderScreensaver(32); + quantizer_channels[2].RenderScreensaver(64); + quantizer_channels[3].RenderScreensaver(96); + +#ifdef QQ_DEBUG_SCREENSAVER + graphics.drawHLine(0, menu::kMenuLineH, menu::kDisplayWidth); + uint32_t us = debug::cycles_to_us(render_cycles.read()); + graphics.setPrintPos(0, 32); + graphics.printf("%u", us); +#endif +} + +#ifdef QQ_DEBUG +void QQ_debug() { + for (int i = 0; i < 4; ++i) { + uint8_t ypos = 10*(i + 1) + 2 ; + graphics.setPrintPos(2, ypos); + graphics.print(quantizer_channels[i].get_int_seq_i()); + graphics.setPrintPos(30, ypos); + graphics.print(quantizer_channels[i].get_int_seq_l()); + graphics.setPrintPos(58, ypos); + graphics.print(quantizer_channels[i].get_int_seq_j()); + graphics.setPrintPos(80, ypos); + graphics.print(quantizer_channels[i].get_int_seq_k()); + graphics.setPrintPos(104, ypos); + graphics.print(quantizer_channels[i].get_int_seq_x()); + } +} +#endif // QQ_DEBUG + +#endif // ENABLE_APP_QUANTERMAIN diff --git a/software/o_c_REV/APP_REFS.ino b/software/o_c_REV/APP_REFS.ino new file mode 100644 index 000000000..81f63bc09 --- /dev/null +++ b/software/o_c_REV/APP_REFS.ino @@ -0,0 +1,631 @@ +// Copyright (c) 2016 Patrick Dowling, 2017 Max Stadler & Tim Churches +// +// Author: Patrick Dowling (pld@gurkenkiste.com) +// Enhancements: Max Stadler and Tim Churches +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// Very simple "reference" voltage app (not so simple any more...) + +#ifdef ENABLE_APP_REFERENCES + +#include "OC_apps.h" +#include "OC_menus.h" +#include "OC_strings.h" +#include "util/util_settings.h" +#include "OC_autotuner.h" +#include "src/drivers/FreqMeasure/OC_FreqMeasure.h" + +// +static constexpr double kAaboveMidCtoC0 = 0.03716272234383494188492; + +// +#ifdef FLIP_180 +const uint8_t DAC_CHANNEL_FTM = DAC_CHANNEL_A; +#else +const uint8_t DAC_CHANNEL_FTM = DAC_CHANNEL_D; +#endif + +const uint8_t NUM_REF_CHANNELS = DAC_CHANNEL_LAST; + +enum ReferenceSetting { + REF_SETTING_OCTAVE, + REF_SETTING_SEMI, + REF_SETTING_RANGE, + REF_SETTING_RATE, + REF_SETTING_NOTES_OR_BPM, + REF_SETTING_A_ABOVE_MID_C_INTEGER, + REF_SETTING_A_ABOVE_MID_C_MANTISSA, + REF_SETTING_PPQN, + REF_SETTING_AUTOTUNE, + REF_SETTING_DUMMY, + REF_SETTING_LAST +}; + +enum ChannelPpqn { + CHANNEL_PPQN_1, + CHANNEL_PPQN_2, + CHANNEL_PPQN_4, + CHANNEL_PPQN_8, + CHANNEL_PPQN_16, + CHANNEL_PPQN_24, + CHANNEL_PPQN_32, + CHANNEL_PPQN_48, + CHANNEL_PPQN_64, + CHANNEL_PPQN_96, + CHANNEL_PPQN_LAST +}; + +class ReferenceChannel : public settings::SettingsBase { +public: + + void Init(DAC_CHANNEL dac_channel) { + InitDefaults(); + + rate_phase_ = 0; + mod_offset_ = 0; + last_pitch_ = 0; + dac_channel_ = dac_channel; + update_enabled_settings(); + } + + int get_octave() const { + return values_[REF_SETTING_OCTAVE]; + } + + DAC_CHANNEL get_channel() const { + return dac_channel_; + } + + void ExitAutotune() { } + + int32_t get_semitone() const { + return values_[REF_SETTING_SEMI]; + } + + int get_range() const { + return values_[REF_SETTING_RANGE]; + } + + uint32_t get_rate() const { + return values_[REF_SETTING_RATE]; + } + + uint8_t get_notes_or_bpm() const { + return values_[REF_SETTING_NOTES_OR_BPM]; + } + + double get_a_above_mid_c() const { + double mantissa_divisor = 100.0; + return static_cast(values_[REF_SETTING_A_ABOVE_MID_C_INTEGER]) + (static_cast(values_[REF_SETTING_A_ABOVE_MID_C_MANTISSA])/mantissa_divisor) ; + } + + uint8_t get_a_above_mid_c_mantissa() const { + return values_[REF_SETTING_A_ABOVE_MID_C_MANTISSA]; + } + + ChannelPpqn get_channel_ppqn() const { + return static_cast(values_[REF_SETTING_PPQN]); + } + + void Update() { + + int octave = get_octave(); + int range = get_range(); + if (range) { + rate_phase_ += OC_CORE_TIMER_RATE; + if (rate_phase_ >= get_rate() * 1000000UL) { + rate_phase_ = 0; + mod_offset_ = 1 - mod_offset_; + } + octave += mod_offset_ * range; + } else { + rate_phase_ = 0; + mod_offset_ = 0; + } + + int32_t semitone = get_semitone(); + OC::DAC::set(dac_channel_, OC::DAC::semitone_to_scaled_voltage_dac(dac_channel_, semitone, octave, OC::DAC::get_voltage_scaling(dac_channel_))); + last_pitch_ = (semitone + octave * 12) << 7; + } + + int num_enabled_settings() const { + return num_enabled_settings_; + } + + ReferenceSetting enabled_setting_at(int index) const { + return enabled_settings_[index]; + } + + void update_enabled_settings() { + ReferenceSetting *settings = enabled_settings_; + *settings++ = REF_SETTING_OCTAVE; + *settings++ = REF_SETTING_SEMI; + *settings++ = REF_SETTING_RANGE; + *settings++ = REF_SETTING_RATE; + *settings++ = REF_SETTING_AUTOTUNE; + //*settings++ = REF_SETTING_AUTOTUNE_ERROR; + + if (DAC_CHANNEL_FTM == dac_channel_) { + *settings++ = REF_SETTING_NOTES_OR_BPM; + *settings++ = REF_SETTING_A_ABOVE_MID_C_INTEGER; + *settings++ = REF_SETTING_A_ABOVE_MID_C_MANTISSA; + *settings++ = REF_SETTING_PPQN; + } + else { + *settings++ = REF_SETTING_DUMMY; + *settings++ = REF_SETTING_DUMMY; + } + + num_enabled_settings_ = settings - enabled_settings_; + } + + void RenderScreensaver(weegfx::coord_t start_x, uint8_t chan) const; + +private: + uint32_t rate_phase_; + int mod_offset_; + int32_t last_pitch_; + DAC_CHANNEL dac_channel_; + + int num_enabled_settings_; + ReferenceSetting enabled_settings_[REF_SETTING_LAST]; +}; + +const char* const notes_or_bpm[2] = { + "notes", "bpm", +}; + +const char* const ppqn_labels[10] = { + " 1", " 2", " 4", " 8", "16", "24", "32", "48", "64", "96", +}; + +const char* const error[] = { + "0.050", "0.125", "0.250", "0.500", "1.000", "2.000", "4.000" +}; + +// EEPROM size: 11 bytes * 4 channels == 44 bytes +SETTINGS_DECLARE(ReferenceChannel, REF_SETTING_LAST) { + #ifdef BUCHLA_4U + { 0, 0, 9, "Octave", nullptr, settings::STORAGE_TYPE_I8 }, + #elif defined(VOR) + {0, -5, 10, "Octave", nullptr, settings::STORAGE_TYPE_I8 }, + #else + { 0, -3, 6, "Octave", nullptr, settings::STORAGE_TYPE_I8 }, + #endif + { 0, 0, 11, "Semitone", OC::Strings::note_names_unpadded, settings::STORAGE_TYPE_U8 }, + { 0, -3, 3, "Mod range oct", nullptr, settings::STORAGE_TYPE_U8 }, + { 0, 0, 30, "Mod rate (s)", nullptr, settings::STORAGE_TYPE_U8 }, + { 0, 0, 1, "Notes/BPM :", notes_or_bpm, settings::STORAGE_TYPE_U8 }, + { 440, 400, 480, "A above mid C", nullptr, settings::STORAGE_TYPE_U16 }, + { 0, 0, 99, " > mantissa", nullptr, settings::STORAGE_TYPE_U8 }, + { CHANNEL_PPQN_4, CHANNEL_PPQN_1, CHANNEL_PPQN_LAST - 1, "> ppqn", ppqn_labels, settings::STORAGE_TYPE_U8 }, + { 0, 0, 0, "--> autotune", NULL, settings::STORAGE_TYPE_U8 }, + { 0, 0, 0, "-", NULL, settings::STORAGE_TYPE_U8 } // dummy +}; + +class ReferencesApp { +public: + ReferencesApp() { } + + OC::Autotuner autotuner; + + void Init() { + int dac_channel = DAC_CHANNEL_A; + for (auto &channel : channels_) + channel.Init(static_cast(dac_channel++)); + + ui.selected_channel = DAC_CHANNEL_FTM; + ui.cursor.Init(0, channels_[DAC_CHANNEL_FTM].num_enabled_settings() - 1); + + freq_sum_ = 0; + freq_count_ = 0; + frequency_ = 0; + autotuner.Init(); + } + + void ISR() { + + if (autotuner.active()) { + autotuner.ISR(); + return; + } + + for (auto &channel : channels_) + channel.Update(); + + if (FreqMeasure.available()) { + // average several readings together + freq_sum_ = freq_sum_ + FreqMeasure.read(); + freq_count_ = freq_count_ + 1; + + if (milliseconds_since_last_freq_ > 750) { + frequency_ = FreqMeasure.countToFrequency(freq_sum_ / freq_count_); + freq_sum_ = 0; + freq_count_ = 0; + milliseconds_since_last_freq_ = 0; + } + } else if (milliseconds_since_last_freq_ > 100000) { + frequency_ = 0.0f; + } + } + + ReferenceChannel &selected_channel() { + return channels_[ui.selected_channel]; + } + + struct { + int selected_channel; + menu::ScreenCursor cursor; + } ui; + + ReferenceChannel channels_[DAC_CHANNEL_LAST]; + + float get_frequency( ) { + return(frequency_) ; + } + + float get_ppqn() { + float ppqn_ = 4.0 ; + switch(channels_[DAC_CHANNEL_FTM].get_channel_ppqn()){ + case CHANNEL_PPQN_1: + ppqn_ = 1.0; + break; + case CHANNEL_PPQN_2: + ppqn_ = 2.0; + break; + case CHANNEL_PPQN_4: + ppqn_ = 4.0; + break; + case CHANNEL_PPQN_8: + ppqn_ = 8.0; + break; + case CHANNEL_PPQN_16: + ppqn_ = 16.0; + break; + case CHANNEL_PPQN_24: + ppqn_ = 24.0; + break; + case CHANNEL_PPQN_32: + ppqn_ = 32.0; + break; + case CHANNEL_PPQN_48: + ppqn_ = 48.0; + break; + case CHANNEL_PPQN_64: + ppqn_ = 64.0; + break; + case CHANNEL_PPQN_96: + ppqn_ = 96.0; + break; + default: + ppqn_ = 8.0 ; + break; + } + return(ppqn_); + } + + float get_bpm( ) { + return((60.0 * frequency_)/get_ppqn()) ; + } + + bool get_notes_or_bpm( ) { + return(static_cast(channels_[DAC_CHANNEL_FTM].get_notes_or_bpm())) ; + } + + float get_C0_freq() { + return(static_cast(channels_[DAC_CHANNEL_FTM].get_a_above_mid_c() * kAaboveMidCtoC0)); + } + +private: + double freq_sum_; + uint32_t freq_count_; + float frequency_ ; + elapsedMillis milliseconds_since_last_freq_; +}; + +ReferencesApp references_app; + +// App stubs +void REFS_init() { + references_app.Init(); +} + +size_t REFS_storageSize() { + return NUM_REF_CHANNELS * ReferenceChannel::storageSize(); +} + +size_t REFS_save(void *storage) { + size_t used = 0; + for (size_t i = 0; i < NUM_REF_CHANNELS; ++i) { + used += references_app.channels_[i].Save(static_cast(storage) + used); + } + return used; +} + +size_t REFS_restore(const void *storage) { + size_t used = 0; + for (size_t i = 0; i < NUM_REF_CHANNELS; ++i) { + used += references_app.channels_[i].Restore(static_cast(storage) + used); + references_app.channels_[i].update_enabled_settings(); + } + references_app.ui.cursor.AdjustEnd(references_app.channels_[0].num_enabled_settings() - 1); + return used; +} + +void REFS_isr() { + return references_app.ISR(); +} + +void REFS_handleAppEvent(OC::AppEvent event) { + switch (event) { + case OC::APP_EVENT_RESUME: + references_app.ui.cursor.set_editing(false); + FreqMeasure.begin(); + references_app.autotuner.Close(); + break; + case OC::APP_EVENT_SUSPEND: + case OC::APP_EVENT_SCREENSAVER_ON: + case OC::APP_EVENT_SCREENSAVER_OFF: + references_app.autotuner.Reset(); + break; + } +} + +void REFS_loop() { +} + +void REFS_menu() { + // autotuner ... + if (references_app.autotuner.active()) { + references_app.autotuner.Draw(); + return; + } + + menu::QuadTitleBar::Draw(); + for (uint_fast8_t i = 0; i < NUM_REF_CHANNELS; ++i) { + menu::QuadTitleBar::SetColumn(i); + graphics.print((char)('A' + i)); + } + menu::QuadTitleBar::Selected(references_app.ui.selected_channel); + + const auto &channel = references_app.selected_channel(); + menu::SettingsList settings_list(references_app.ui.cursor); + menu::SettingsListItem list_item; + + while (settings_list.available()) { + const int setting = + channel.enabled_setting_at(settings_list.Next(list_item)); + const int value = channel.get_value(setting); + const settings::value_attr &attr = ReferenceChannel::value_attr(setting); + + switch (setting) { + case REF_SETTING_AUTOTUNE: + case REF_SETTING_DUMMY: + list_item.DrawNoValue(value, attr); + break; + default: + list_item.DrawDefault(value, attr); + break; + } + } +} + +void print_voltage(int octave, int fraction) { + graphics.printf("%01d", octave); + graphics.movePrintPos(-1, 0); graphics.print('.'); + graphics.movePrintPos(-2, 0); graphics.printf("%03d", fraction); +} + +void ReferenceChannel::RenderScreensaver(weegfx::coord_t start_x, uint8_t chan) const { + + // Mostly borrowed from QQ + + weegfx::coord_t x = start_x + 26; + weegfx::coord_t y = 34 ; // was 60 + // for (int i = 0; i < 5 ; ++i, y -= 4) // was i < 12 + graphics.setPixel(x, y); + + int32_t pitch = last_pitch_ ; + int32_t unscaled_pitch = last_pitch_ ; + + #ifdef BUCHLA_SUPPORT + switch (OC::DAC::get_voltage_scaling(chan)) { + + case VOLTAGE_SCALING_1_2V_PER_OCT: // 1.2V/oct + pitch = (pitch * 19661) >> 14 ; + break; + case VOLTAGE_SCALING_2V_PER_OCT: // 2V/oct + pitch = pitch << 1 ; + break; + default: // 1V/oct + break; + } + #endif + + pitch += (OC::DAC::kOctaveZero * 12) << 7; + unscaled_pitch += (OC::DAC::kOctaveZero * 12) << 7; + + + CONSTRAIN(pitch, 0, 120 << 7); + + int32_t octave = pitch / (12 << 7); + int32_t unscaled_octave = unscaled_pitch / (12 << 7); + pitch -= (octave * 12 << 7); + unscaled_pitch -= (unscaled_octave * 12 << 7); + int semitone = pitch >> 7; + int unscaled_semitone = unscaled_pitch >> 7; + + y = 34 - unscaled_semitone * 2; // was 60, multiplier was 4 + if (unscaled_semitone < 6) + graphics.setPrintPos(start_x + menu::kIndentDx, y - 7); + else + graphics.setPrintPos(start_x + menu::kIndentDx, y); + graphics.print(OC::Strings::note_names_unpadded[unscaled_semitone]); + + graphics.drawHLine(start_x + 16, y, 8); + graphics.drawBitmap8(start_x + 28, 34 - unscaled_octave * 2 - 1, OC::kBitmapLoopMarkerW, OC::bitmap_loop_markers_8 + OC::kBitmapLoopMarkerW); // was 60 + + #ifdef BUCHLA_SUPPORT + // Try and round to 3 digits + switch (OC::DAC::get_voltage_scaling(chan)) { + + case VOLTAGE_SCALING_1_2V_PER_OCT: // 1.2V/oct + semitone = (semitone * 10000 + 40) / 100; + break; + case VOLTAGE_SCALING_2V_PER_OCT: // 2V/oct + default: // 1V/oct + semitone = (semitone * 10000 + 50) / 120; + break; + } + #else + semitone = (semitone * 10000 + 50) / 120; + #endif + + semitone %= 1000; + octave -= OC::DAC::kOctaveZero; + + + // We want [sign]d.ddd = 6 chars in 32px space; with the current font width + // of 6px that's too tight, so squeeze in the mini minus... + y = menu::kTextDy; + graphics.setPrintPos(start_x + menu::kIndentDx, y); + if (octave >= 0) { + print_voltage(octave, semitone); + } else { + graphics.drawHLine(start_x, y + 3, 2); + if (semitone) + print_voltage(-octave - 1, 1000 - semitone); + else + print_voltage(-octave, 0); + } +} + +/* +void printFloat(float f) { + const int f_ = int(floor(f * 1000)); + const int value = f_ / 1000; + const int cents = f_ % 1000; + graphics.printf("%6u.%03u", value, cents); +} +*/ + +void REFS_screensaver() { + references_app.channels_[0].RenderScreensaver( 0, 0); + references_app.channels_[1].RenderScreensaver(32, 1); + references_app.channels_[2].RenderScreensaver(64, 2); + references_app.channels_[3].RenderScreensaver(96, 3); + graphics.setPrintPos(2, 44); + + const float frequency_ = references_app.get_frequency() ; + const float c0_freq_ = references_app.get_C0_freq() ; + const float bpm_ = (60.0 * frequency_)/references_app.get_ppqn() ; + + int32_t freq_decicents_deviation_ = round(12000.0 * log2f(frequency_ / c0_freq_)) + 500; + int8_t freq_octave_ = -2 + ((freq_decicents_deviation_)/ 12000) ; + int8_t freq_note_ = (freq_decicents_deviation_ - ((freq_octave_ + 2) * 12000)) / 1000; + int32_t freq_decicents_residual_ = ((freq_decicents_deviation_ - ((freq_octave_ - 1) * 12000)) % 1000) - 500; + + if (frequency_ > 0.0) { + { + const int f = int(floor(frequency_ * 1000)); + const int value = f / 1000; + const int cents = f % 1000; + #ifdef FLIP_180 + graphics.printf("TR1 %7d.%03d Hz", value, cents); + #else + graphics.printf("TR4 %7d.%03d Hz", value, cents); + #endif + } + graphics.setPrintPos(2, 56); + if (references_app.get_notes_or_bpm()) { + const int f = int(floor(bpm_ * 100)); + const int value = f / 100; + const int cents = f % 100; + graphics.printf("%5d.%02d bpm %2.0fppqn", value, cents, references_app.get_ppqn()); + } else if(frequency_ >= c0_freq_) { + const int f = int(floor(freq_decicents_residual_)); + const int value = f / 10; + const int cents = abs(f) % 10; + graphics.printf("%+i %s %+5d.%01dc", freq_octave_, OC::Strings::note_names[freq_note_], value, cents) ; + } + } else { + graphics.print("TR4 no input") ; + } +} + +void REFS_handleButtonEvent(const UI::Event &event) { + + if (references_app.autotuner.active()) { + references_app.autotuner.HandleButtonEvent(event); + return; + } + + if (OC::CONTROL_BUTTON_R == event.control && event.type == UI::EVENT_BUTTON_PRESS) { + + auto &selected_channel = references_app.selected_channel(); + switch (selected_channel.enabled_setting_at(references_app.ui.cursor.cursor_pos())) { + case REF_SETTING_AUTOTUNE: + references_app.autotuner.Open(&selected_channel); + break; + case REF_SETTING_DUMMY: + break; + default: + references_app.ui.cursor.toggle_editing(); + break; + } + } +} + +void REFS_handleEncoderEvent(const UI::Event &event) { + + if (references_app.autotuner.active()) { + references_app.autotuner.HandleEncoderEvent(event); + return; + } + + if (OC::CONTROL_ENCODER_L == event.control) { + + int previous = references_app.selected_channel().num_enabled_settings(); + int selected = references_app.ui.selected_channel + event.value; + CONSTRAIN(selected, 0, NUM_REF_CHANNELS - 0x1); + references_app.ui.selected_channel = selected; + + // hack -- deal w/ menu items / channels + if ((references_app.ui.cursor.cursor_pos() > 4) && (previous > references_app.selected_channel().num_enabled_settings())) { + references_app.ui.cursor.Init(0, 0); + references_app.ui.cursor.AdjustEnd(references_app.selected_channel().num_enabled_settings() - 1); + } + else + references_app.ui.cursor.AdjustEnd(references_app.selected_channel().num_enabled_settings() - 1); + } else if (OC::CONTROL_ENCODER_R == event.control) { + if (references_app.ui.cursor.editing()) { + auto &selected_channel = references_app.selected_channel(); + ReferenceSetting setting = selected_channel.enabled_setting_at(references_app.ui.cursor.cursor_pos()); + if (setting == REF_SETTING_DUMMY) + references_app.ui.cursor.set_editing(false); + selected_channel.change_value(setting, event.value); + selected_channel.update_enabled_settings(); + } else { + references_app.ui.cursor.Scroll(event.value); + } + } +} + +#endif // ENABLE_APP_REFERENCES diff --git a/software/o_c_REV/APP_SCALEEDITOR.ino b/software/o_c_REV/APP_SCALEEDITOR.ino index 81a34a92e..097191654 100644 --- a/software/o_c_REV/APP_SCALEEDITOR.ino +++ b/software/o_c_REV/APP_SCALEEDITOR.ino @@ -30,17 +30,16 @@ public: void Start() { current_scale = 0; current_note = 0; - quantizer.Configure(OC::Scales::GetScale(current_scale), 0xffff); current_import_scale = 5; undo_value = OC::user_scales[current_scale].notes[0]; octave = 1; - QuantizeCurrent(); segment.Init(SegmentSize::BIG_SEGMENTS); tinynumbers.Init(SegmentSize::TINY_SEGMENTS); } void Resume() { - + HS::quantizer[0].Configure(OC::Scales::GetScale(current_scale), 0xffff); + QuantizeCurrent(); } void Controller() { @@ -48,7 +47,7 @@ public: // Scale monitor int32_t pitch = In(0); - int32_t quantized = quantizer.Process(pitch, 0, 0); + int32_t quantized = HS::quantizer[0].Process(pitch, 0, 0); Out(0, quantized); // Current note monitor @@ -116,7 +115,7 @@ public: current_note = 0; undo_value = OC::user_scales[current_scale].notes[current_note]; // Configure and force requantize for real-time monitoring purposes - quantizer.Configure(OC::Scales::GetScale(current_scale), 0xffff); + HS::quantizer[0].Configure(OC::Scales::GetScale(current_scale), 0xffff); QuantizeCurrent(); } } @@ -170,7 +169,6 @@ private: int undo_value; bool length_set_mode; bool import_mode; - braids::Quantizer quantizer; int current_quantized; int octave; SegmentDisplay segment; @@ -235,7 +233,7 @@ private: current_scale = constrain(current_scale + direction, 0, OC::Scales::SCALE_USER_LAST - 1); // Configure and force requantize for real-time monitoring purposes - quantizer.Configure(OC::Scales::GetScale(current_scale), 0xffff); + HS::quantizer[0].Configure(OC::Scales::GetScale(current_scale), 0xffff); QuantizeCurrent(); uint8_t length = static_cast(OC::user_scales[current_scale].num_notes); @@ -293,7 +291,7 @@ private: } // Configure and force requantize for real-time monitoring purposes - quantizer.Configure(OC::Scales::GetScale(current_scale), 0xffff); + HS::quantizer[0].Configure(OC::Scales::GetScale(current_scale), 0xffff); QuantizeCurrent(); ResetCursor(); @@ -315,14 +313,14 @@ private: OC::user_scales[current_scale].notes[current_note] = new_value; // Configure and force requantize for real-time monitoring purposes - quantizer.Configure(OC::Scales::GetScale(current_scale), 0xffff); + HS::quantizer[0].Configure(OC::Scales::GetScale(current_scale), 0xffff); QuantizeCurrent(); } void ImportScale() { OC::Scale source = OC::Scales::GetScale(current_import_scale); memcpy(&OC::user_scales[current_scale], &source, sizeof(source)); - quantizer.Configure(OC::Scales::GetScale(current_scale), 0xffff); + HS::quantizer[0].Configure(OC::Scales::GetScale(current_scale), 0xffff); QuantizeCurrent(); import_mode = 0; undo_value = OC::user_scales[current_scale].notes[current_note]; @@ -335,9 +333,9 @@ private: void QuantizeCurrent() { int transpose = OC::user_scales[current_scale].span * octave; - quantizer.Requantize(); - current_quantized = quantizer.Process(OC::user_scales[current_scale].notes[current_note] + transpose, 0, 0); - quantizer.Requantize(); // This is for the next one in the Controller + HS::quantizer[0].Requantize(); + current_quantized = HS::quantizer[0].Process(OC::user_scales[current_scale].notes[current_note] + transpose, 0, 0); + HS::quantizer[0].Requantize(); // This is for the next one in the Controller } }; @@ -361,6 +359,9 @@ void SCALEEDITOR_handleAppEvent(OC::AppEvent event) { if (event == OC::APP_EVENT_SUSPEND) { scale_editor_instance.OnSendSysEx(); } + if (event == OC::APP_EVENT_RESUME) { + scale_editor_instance.Resume(); + } } void SCALEEDITOR_loop() {} @@ -374,15 +375,15 @@ void SCALEEDITOR_screensaver() {} void SCALEEDITOR_handleButtonEvent(const UI::Event &event) { // For left encoder, handle press and long press if (event.control == OC::CONTROL_BUTTON_L) { + if (event.type == UI::EVENT_BUTTON_PRESS) scale_editor_instance.OnLeftButtonPress(); if (event.type == UI::EVENT_BUTTON_LONG_PRESS) scale_editor_instance.OnLeftButtonLongPress(); - else scale_editor_instance.OnLeftButtonPress(); } // For right encoder, only handle press (long press is reserved) if (event.control == OC::CONTROL_BUTTON_R && event.type == UI::EVENT_BUTTON_PRESS) scale_editor_instance.OnRightButtonPress(); // For up button, handle only press (long press is reserved) - if (event.control == OC::CONTROL_BUTTON_UP) scale_editor_instance.OnUpButtonPress(); + if (event.control == OC::CONTROL_BUTTON_UP && event.type == UI::EVENT_BUTTON_PRESS) scale_editor_instance.OnUpButtonPress(); // For down button, handle press and long press if (event.control == OC::CONTROL_BUTTON_DOWN) { diff --git a/software/o_c_REV/APP_SCENES.ino b/software/o_c_REV/APP_SCENES.ino new file mode 100644 index 000000000..20b85821f --- /dev/null +++ b/software/o_c_REV/APP_SCENES.ino @@ -0,0 +1,600 @@ +// Copyright (c) 2023, Nicholas J. Michalek +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +/* + * Based loosely on the Traffic module from Jasmine & Olive Trees + * Also similar to Mutable Instruments Frames + */ + +#ifdef ENABLE_APP_SCENES + +#include "HSApplication.h" +#include "HSMIDI.h" +#include "util/util_settings.h" +#include "src/drivers/FreqMeasure/OC_FreqMeasure.h" +#include "HemisphereApplet.h" + +static const int NR_OF_SCENE_PRESETS = 4; +static const int NR_OF_SCENE_CHANNELS = 4; + +#define SCENE_MAX_VAL HEMISPHERE_MAX_CV +#define SCENE_MIN_VAL HEMISPHERE_MIN_CV +#define SCENE_ACCEL_MIN 16 +#define SCENE_ACCEL_MAX 256 + +struct Scene { + int16_t values[4]; + + void Init() { + for (int i = 0; i < NR_OF_SCENE_CHANNELS; ++i) values[i] = 0; + } +}; + +// Preset storage spec +enum ScenesSettings { + SCENE_FLAGS, // 8 bits + + // 16 bits each + SCENE1_VALUE_A, + SCENE1_VALUE_B, + SCENE1_VALUE_C, + SCENE1_VALUE_D, + + SCENE2_VALUE_A, + SCENE2_VALUE_B, + SCENE2_VALUE_C, + SCENE2_VALUE_D, + + SCENE3_VALUE_A, + SCENE3_VALUE_B, + SCENE3_VALUE_C, + SCENE3_VALUE_D, + + SCENE4_VALUE_A, + SCENE4_VALUE_B, + SCENE4_VALUE_C, + SCENE4_VALUE_D, + + SCENES_SETTING_LAST +}; + +class ScenesAppPreset : public settings::SettingsBase { +public: + + bool is_valid() { + return (values_[SCENE_FLAGS] & 0x01); + } + bool load_preset(Scene *s) { + if (!is_valid()) return false; // don't try to load a blank + + int ix = 1; // skip validity flag + for (int ch = 0; ch < NR_OF_SCENE_CHANNELS; ++ch) { + // somestuff = values_[ix++]; + s[ch].values[0] = values_[ix++]; + s[ch].values[1] = values_[ix++]; + s[ch].values[2] = values_[ix++]; + s[ch].values[3] = values_[ix++]; + } + + return true; + } + void save_preset(Scene *s) { + int ix = 0; + + values_[ix++] = 1; // validity flag + + for (int ch = 0; ch < NR_OF_SCENE_CHANNELS; ++ch) { + values_[ix++] = s[ch].values[0]; + values_[ix++] = s[ch].values[1]; + values_[ix++] = s[ch].values[2]; + values_[ix++] = s[ch].values[3]; + } + } + +}; + +ScenesAppPreset scene_presets[NR_OF_SCENE_PRESETS]; +static const char * scene_preset_id[] = { "A", "B", "C", "D" }; + +class ScenesApp : public HSApplication { +public: + + void Start() { + // make sure to turn this off, just in case + FreqMeasure.end(); + OC::DigitalInputs::reInit(); + } + + void ClearPreset() { + for (int ch = 0; ch < NR_OF_SCENE_CHANNELS; ++ch) { + scene[ch].Init(); + } + //SavePreset(); + } + void LoadPreset() { + bool success = scene_presets[index].load_preset(scene); + if (success) { + for (int ch = 0; ch < NR_OF_SCENE_CHANNELS; ++ch) { + } + preset_modified = 0; + } + else + ClearPreset(); + } + void SavePreset() { + scene_presets[index].save_preset(scene); + preset_modified = 0; + } + + void Resume() { + } + + void Controller() { + // Keep the MIDI clock ticking + HS::IOFrame &f = HS::frame; + while (usbMIDI.read()) { + const int message = usbMIDI.getType(); + const int data1 = usbMIDI.getData1(); + const int data2 = usbMIDI.getData2(); + f.MIDIState.ProcessMIDIMsg(usbMIDI.getChannel(), message, data1, data2); + } + HS::clock_setup_applet.Controller(0, 0); + + const int OCTAVE = (12 << 7); + // -- core processing -- + + // explicit gate/trigger priority right here: + if (Gate(0)) // TR1 takes precedence + trig_chan = 0; + else if (Gate(1)) // TR2 + trig_chan = 1; + else if (Gate(2)) // TR3 + trig_chan = 2; + else if (Gate(3)) // TR4 - TODO: aux trigger modes, random, etc. + trig_chan = 3; + // else, it's unchanged + + scene4seq = (In(3) > 2 * OCTAVE); // gate at CV4 + if (scene4seq) { + if (!Sequence.active) Sequence.Generate(); + if (Clock(3)) Sequence.Advance(); + } else { + Sequence.active = 0; + } + + // CV2: bipolar offset added to all values + cv_offset = DetentedIn(1); + + // CV3: Slew/Smoothing + smoothing_mod = smoothing; + if (DetentedIn(2) > 0) { + Modulate(smoothing_mod, 2, 0, 127); + } + + // -- update active scene values, with smoothing + // CV1: smooth interpolation offset, starting from last triggered scene + if (DetentedIn(0)) { + int cv = In(0); + int direction = (cv < 0) ? -1 : 1; + int volt = cv / OCTAVE; + int partial = abs(cv % OCTAVE); + + // for display cursor - scaled to pixels, 32px per volt + smooth_offset = cv * 32 / OCTAVE; + + int first = (trig_chan + volt + NR_OF_SCENE_CHANNELS) % NR_OF_SCENE_CHANNELS; + int second = (first + direction + NR_OF_SCENE_CHANNELS) % NR_OF_SCENE_CHANNELS; + + for (int i = 0; i < NR_OF_SCENE_CHANNELS; ++i) { + int16_t v1 = scene[first].values[i]; + int16_t v2 = scene[second].values[i]; + + // the sequence will determine which other value is blended in for Scene 4 + if (scene4seq) { + if (first == 3) + v1 = scene[Sequence.Get(i) / 4].values[Sequence.Get(i) % 4]; + if (second == 3) + v2 = scene[Sequence.Get(i) / 4].values[Sequence.Get(i) % 4]; + } + + // a weighted average of the two chosen scene values + int target = ( v1 * (OCTAVE - partial) + v2 * partial ) / OCTAVE; + target = constrain(target + cv_offset, SCENE_MIN_VAL, SCENE_MAX_VAL); + + slew(active_scene.values[i], target); + } + } else if (scene4seq && trig_chan == 3) { // looped sequence for TR4 + for (int i = 0; i < NR_OF_SCENE_CHANNELS; ++i) { + int target = scene[ Sequence.Get(i) / 4 ].values[ Sequence.Get(i) % 4 ]; + target = constrain(target + cv_offset, SCENE_MIN_VAL, SCENE_MAX_VAL); + slew(active_scene.values[i], target); + } + smooth_offset = 0; + } else { // a simple scene copy will suffice + for (int i = 0; i < NR_OF_SCENE_CHANNELS; ++i) { + int target = constrain(scene[trig_chan].values[i] + cv_offset, SCENE_MIN_VAL, SCENE_MAX_VAL); + slew(active_scene.values[i], target); + } + smooth_offset = 0; + } + + // set outputs + for (int ch = 0; ch < NR_OF_SCENE_CHANNELS; ++ch) { + if (trigsum_mode && ch == 3) { // TrigSum output overrides D + if (Clock(0) || Clock(1) || Clock(2) || Clock(3)) + ClockOut(3); + continue; + } + Out(ch, active_scene.values[ch]); + } + + // encoder deceleration + if (left_accel > SCENE_ACCEL_MIN) --left_accel; + else left_accel = SCENE_ACCEL_MIN; // just in case lol + + if (right_accel > SCENE_ACCEL_MIN) --right_accel; + else right_accel = SCENE_ACCEL_MIN; + } + + void View() { + gfxHeader("Scenes"); + + if (preset_select) { + gfxPrint(70, 1, "- Presets"); + DrawPresetSelector(); + } else { + gfxPos(110, 1); + if (preset_modified) gfxPrint("*"); + if (scene_presets[index].is_valid()) gfxPrint(scene_preset_id[index]); + + DrawInterface(); + } + } + + ///////////////////////////////////////////////////////////////// + // Control handlers + ///////////////////////////////////////////////////////////////// + void OnLeftButtonPress() { + // Toggle between A or B editing + // also doubles as Load or Save for preset select + edit_mode_left = !edit_mode_left; + + // prevent saving to the (clear) slot + if (edit_mode_left && preset_select == 5) preset_select = 4; + } + + void OnLeftButtonLongPress() { + //if (preset_select) return; + trigsum_mode = !trigsum_mode; + } + + void OnRightButtonPress() { + if (preset_select) { + // special case to clear values + if (!edit_mode_left && preset_select == NR_OF_SCENE_PRESETS + 1) { + ClearPreset(); + preset_modified = 1; + } + else { + index = preset_select - 1; + if (edit_mode_left) SavePreset(); + else LoadPreset(); + } + + preset_select = 0; + return; + } + + // Toggle between C or D editing + edit_mode_right = !edit_mode_right; + } + + void SwitchChannel(bool up) { + if (!preset_select) { + sel_chan += (up? 1 : -1) + NR_OF_SCENE_CHANNELS; + sel_chan %= NR_OF_SCENE_CHANNELS; + } else { + // always cancel preset select on single click + preset_select = 0; + } + } + + void OnDownButtonLongPress() { + // show preset screen, select last loaded + preset_select = 1 + index; + } + + // Left encoder: Edit A or B on current scene + void OnLeftEncoderMove(int direction) { + if (preset_select) { + edit_mode_left = (direction>0); + // prevent saving to the (clear) slot + if (edit_mode_left && preset_select == 5) preset_select = 4; + return; + } + + preset_modified = 1; + + int idx = 0 + edit_mode_left; + scene[sel_chan].values[idx] += direction * left_accel; + scene[sel_chan].values[idx] = constrain( scene[sel_chan].values[idx], SCENE_MIN_VAL, SCENE_MAX_VAL ); + + left_accel <<= 3; + if (left_accel > SCENE_ACCEL_MAX) left_accel = SCENE_ACCEL_MAX; + } + + // Right encoder: Edit C or D on current scene + void OnRightEncoderMove(int direction) { + if (preset_select) { + preset_select = constrain(preset_select + direction, 1, NR_OF_SCENE_PRESETS + (1-edit_mode_left)); + return; + } + + preset_modified = 1; + + int idx = 2 + edit_mode_right; + scene[sel_chan].values[idx] += direction * right_accel; + scene[sel_chan].values[idx] = constrain( scene[sel_chan].values[idx], SCENE_MIN_VAL, SCENE_MAX_VAL ); + + right_accel <<= 3; + if (right_accel > SCENE_ACCEL_MAX) right_accel = SCENE_ACCEL_MAX; + } + +private: + static const int SEQ_LENGTH = 16; + + int index = 0; + int cv_offset = 0; + + int sel_chan = 0; + int trig_chan = 0; + int preset_select = 0; // both a flag and an index + bool preset_modified = 0; + bool edit_mode_left = 0; + bool edit_mode_right = 0; + bool trigsum_mode = 0; + bool scene4seq = 0; + // oh jeez, why do we have so many bools?! + + struct { + bool active = 0; + uint64_t sequence[4]; // four 16-step sequences of 4-bit values + uint8_t step; + + void Generate() { + for (int i = 0; i < NR_OF_SCENE_PRESETS; ++i) { + sequence[i] = random() | (uint64_t(random()) << 32); + } + step = 0; + active = 1; + } + void Advance() { + ++step %= SEQ_LENGTH; + } + const uint8_t Get(int i) { + return (uint8_t)( (sequence[i] >> (step * 4)) & 0x0F ); // 4-bit value, 0 to 15 + } + } Sequence; + + uint16_t left_accel = SCENE_ACCEL_MIN; + uint16_t right_accel = SCENE_ACCEL_MIN; + + int smooth_offset = 0; // -128 to 128, for display + + Scene scene[NR_OF_SCENE_CHANNELS]; + Scene active_scene; + + int smoothing, smoothing_mod; + + template + void slew(T &old_val, const int new_val = 0) { + const int s = 1 + smoothing_mod; + // more smoothing causes more ticks to be skipped + if (OC::CORE::ticks % s) return; + + old_val = (old_val * (s - 1) + new_val) / s; + } + + void DrawPresetSelector() { + // index is the currently loaded preset (0-3) + // preset_select is current selection (1-4, 5=clear) + int y = 5 + 10*preset_select; + gfxPrint(25, y, edit_mode_left ? "Save" : "Load"); + gfxIcon(50, y, RIGHT_ICON); + + for (int i = 0; i < NR_OF_SCENE_PRESETS; ++i) { + gfxPrint(60, 15 + i*10, scene_preset_id[i]); + if (!scene_presets[i].is_valid()) + gfxPrint(" (empty)"); + else if (i == index) + gfxPrint(" *"); + } + if (!edit_mode_left) + gfxPrint(60, 55, "[CLEAR]"); + } + + void DrawInterface() { + for (int i = 0; i < NR_OF_SCENE_CHANNELS; ++i) { + gfxPrint(i*32 + 13, 14, i+1); + } + // active scene indicator + uint8_t x = (12 + trig_chan*32 + smooth_offset + 128) % 128; + gfxInvert(x, 13, 9, 10); + + // edit pointer + gfxIcon(sel_chan*32 + 13, 25, UP_ICON); + + gfxPrint(8, 35, "A:"); + gfxPrintVoltage(scene[sel_chan].values[0]); + gfxPrint(8, 45, "B:"); + gfxPrintVoltage(scene[sel_chan].values[1]); + + gfxIcon(0, 35 + 10*edit_mode_left, RIGHT_ICON); + + gfxPrint(72, 35, "C:"); + gfxPrintVoltage(scene[sel_chan].values[2]); + gfxPrint(72, 45, "D:"); + if (trigsum_mode) + gfxPrint("(trig)"); + else + gfxPrintVoltage(scene[sel_chan].values[3]); + + gfxIcon(64, 35 + 10*edit_mode_right, RIGHT_ICON); + + // ------------------- // + gfxLine(0, 54, 127, 54); + + // -- Input indicators + // bias (CV2) + if (cv_offset) { + gfxIcon(0, 56, UP_DOWN_ICON); + gfxPos(10, 56); + gfxPrintVoltage(cv_offset); + } + // slew amount (CV3) + gfxIcon(64, 56, SLEW_ICON); + gfxPrint(74, 56, smoothing_mod); + + // sequencer (CV4) + if (scene4seq) gfxIcon( 108, 56, LOOP_ICON ); + } +}; + +// TOTAL EEPROM SIZE: 264 bytes +SETTINGS_DECLARE(ScenesAppPreset, SCENES_SETTING_LAST) { + {0, 0, 255, "Flags", NULL, settings::STORAGE_TYPE_U8}, + + {0, 0, 65535, "Scene1ValA", NULL, settings::STORAGE_TYPE_U16}, + {0, 0, 65535, "Scene1ValB", NULL, settings::STORAGE_TYPE_U16}, + {0, 0, 65535, "Scene1ValC", NULL, settings::STORAGE_TYPE_U16}, + {0, 0, 65535, "Scene1ValD", NULL, settings::STORAGE_TYPE_U16}, + + {0, 0, 65535, "Scene2ValA", NULL, settings::STORAGE_TYPE_U16}, + {0, 0, 65535, "Scene2ValB", NULL, settings::STORAGE_TYPE_U16}, + {0, 0, 65535, "Scene2ValC", NULL, settings::STORAGE_TYPE_U16}, + {0, 0, 65535, "Scene2ValD", NULL, settings::STORAGE_TYPE_U16}, + + {0, 0, 65535, "Scene3ValA", NULL, settings::STORAGE_TYPE_U16}, + {0, 0, 65535, "Scene3ValB", NULL, settings::STORAGE_TYPE_U16}, + {0, 0, 65535, "Scene3ValC", NULL, settings::STORAGE_TYPE_U16}, + {0, 0, 65535, "Scene3ValD", NULL, settings::STORAGE_TYPE_U16}, + + {0, 0, 65535, "Scene4ValA", NULL, settings::STORAGE_TYPE_U16}, + {0, 0, 65535, "Scene4ValB", NULL, settings::STORAGE_TYPE_U16}, + {0, 0, 65535, "Scene4ValC", NULL, settings::STORAGE_TYPE_U16}, + {0, 0, 65535, "Scene4ValD", NULL, settings::STORAGE_TYPE_U16}, +}; + + +ScenesApp ScenesApp_instance; + +// App stubs +void ScenesApp_init() { ScenesApp_instance.BaseStart(); } + +size_t ScenesApp_storageSize() { + return ScenesAppPreset::storageSize() * NR_OF_SCENE_PRESETS; +} + +size_t ScenesApp_save(void *storage) { + size_t used = 0; + for (int i = 0; i < 4; ++i) { + used += scene_presets[i].Save(static_cast(storage) + used); + } + return used; +} + +size_t ScenesApp_restore(const void *storage) { + size_t used = 0; + for (int i = 0; i < 4; ++i) { + used += scene_presets[i].Restore(static_cast(storage) + used); + } + ScenesApp_instance.LoadPreset(); + return used; +} + +void ScenesApp_isr() { return ScenesApp_instance.BaseController(); } + +void ScenesApp_handleAppEvent(OC::AppEvent event) { + switch (event) { + case OC::APP_EVENT_RESUME: + //ScenesApp_instance.Resume(); + break; + + case OC::APP_EVENT_SUSPEND: + case OC::APP_EVENT_SCREENSAVER_ON: + break; + + default: break; + } +} + +void ScenesApp_loop() {} // Deprecated + +void ScenesApp_menu() { ScenesApp_instance.BaseView(); } + +void ScenesApp_screensaver() { + ScenesApp_instance.BaseScreensaver(); +} + +void ScenesApp_handleButtonEvent(const UI::Event &event) { + // For left encoder, handle press and long press + // For right encoder, only handle press (long press is reserved) + // For up button, handle only press (long press is reserved) + // For down button, handle press and long press + switch (event.type) { + case UI::EVENT_BUTTON_DOWN: + break; + case UI::EVENT_BUTTON_PRESS: { + switch (event.control) { + case OC::CONTROL_BUTTON_L: + ScenesApp_instance.OnLeftButtonPress(); + break; + case OC::CONTROL_BUTTON_R: + ScenesApp_instance.OnRightButtonPress(); + break; + case OC::CONTROL_BUTTON_DOWN: + case OC::CONTROL_BUTTON_UP: + ScenesApp_instance.SwitchChannel(event.control == OC::CONTROL_BUTTON_DOWN); + break; + default: break; + } + } break; + case UI::EVENT_BUTTON_LONG_PRESS: + if (event.control == OC::CONTROL_BUTTON_L) { + ScenesApp_instance.OnLeftButtonLongPress(); + } + if (event.control == OC::CONTROL_BUTTON_DOWN) { + ScenesApp_instance.OnDownButtonLongPress(); + } + break; + + default: break; + } +} + +void ScenesApp_handleEncoderEvent(const UI::Event &event) { + // Left encoder turned + if (event.control == OC::CONTROL_ENCODER_L) ScenesApp_instance.OnLeftEncoderMove(event.value); + + // Right encoder turned + if (event.control == OC::CONTROL_ENCODER_R) ScenesApp_instance.OnRightEncoderMove(event.value); +} + +#endif // ENABLE_APP_SCENES diff --git a/software/o_c_REV/APP_SEQ.ino b/software/o_c_REV/APP_SEQ.ino new file mode 100644 index 000000000..39e75b686 --- /dev/null +++ b/software/o_c_REV/APP_SEQ.ino @@ -0,0 +1,2636 @@ +// Copyright (c) 2015, 2016 Max Stadler, Patrick Dowling +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifdef ENABLE_APP_SEQUINS + +#include "util/util_settings.h" +#include "util/util_trigger_delay.h" +#include "OC_apps.h" +#include "OC_DAC.h" +#include "OC_menus.h" +#include "OC_ui.h" +#include "OC_strings.h" +#include "OC_visualfx.h" +#include "OC_sequence_edit.h" +#include "OC_patterns.h" +#include "OC_scales.h" +#include "OC_scale_edit.h" +#include "OC_input_map.h" +#include "OC_input_maps.h" +#include "braids_quantizer.h" +#include "braids_quantizer_scales.h" +#include "extern/dspinst.h" +#include "util/util_arp.h" +#include "peaks_multistage_envelope.h" + + +namespace menu = OC::menu; + +const uint8_t NUM_CHANNELS = 2; +const uint8_t MULT_MAX = 26; // max multiplier +const uint8_t MULT_BY_ONE = 19; // default multiplication +const uint8_t PULSEW_MAX = 255; // max pulse width [ms] + +const uint32_t SCALE_PULSEWIDTH = 58982; // 0.9 for signed_multiply_32x16b +const uint32_t TICKS_TO_MS = 43691; // 0.6667f : fraction, if TU_CORE_TIMER_RATE = 60 us : 65536U * ((1000 / TU_CORE_TIMER_RATE) - 16) +const uint32_t TICK_JITTER = 0xFFFFFFF; // 1/16 : threshold/double triggers reject -> ext_frequency_in_ticks_ +const uint32_t TICK_SCALE = 0xC0000000; // 0.75 for signed_multiply_32x32 +const uint32_t COPYTIMEOUT = 200000; // in ticks + +void SEQ_leftButton(); +void SEQ_leftButtonLong(); +void SEQ_upButtonLong(); +void SEQ_downButtonLong(); +void SEQ_upButton(); +void SEQ_downButton(); +void SEQ_rightButton(); + +uint32_t ticks_src1 = 0; // main clock frequency (top) +uint32_t ticks_src2 = 0; // sec. clock frequency (bottom) + +// copy sequence, global +uint8_t copy_sequence = 0; +uint8_t copy_length = OC::Patterns::kMax; +uint16_t copy_mask = 0xFFFF; +uint64_t copy_timeout = COPYTIMEOUT; + +const uint64_t multipliers_[] = { + + 0xFFFFFFFF, // x1 + 0x80000000, // x2 + 0x55555555, // x3 + 0x40000000, // x4 + 0x33333333, // x5 + 0x2AAAAAAB, // x6 + 0x24924925, // x7 + 0x20000000 // x8 + +}; // = 2^32 / multiplier + +const uint64_t pw_scale_[] = { + + 0xFFFFFFFF, // /64 + 0xFBFFFFFF, // /63 + 0xF7FFFFFF, // /62 + 0xC3FFFFFF, // /49 + 0xC0000000, // /48 + 0xBBFFFFFF, // /47 + 0x83FFFFFF, // /33 + 0x80000000, // /32 + 0x7BFFFFFF, // /31 + 0x43FFFFFF, // /17 + 0x40000000, // /16 + 0x3BFFFFFF, // /15 + 0x20000000, // /8 + 0x1C000000, // /7 + 0x18000000, // /6 + 0x14000000, // /5 + 0x10000000, // /4 + 0xC000000, // /3 + 0x8000000, // /2 + 0x4000000, // x1 + +}; // = 0xFFFFFFFF * divisor / 64 + +const uint8_t divisors_[] = { + 64, + 63, + 62, + 49, + 48, + 47, + 33, + 32, + 31, + 17, + 16, + 15, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1 +}; + +enum SEQ_ChannelSetting { + + SEQ_CHANNEL_SETTING_MODE, // + SEQ_CHANNEL_SETTING_CLOCK, + SEQ_CHANNEL_SETTING_TRIGGER_DELAY, + SEQ_CHANNEL_SETTING_RESET, + SEQ_CHANNEL_SETTING_MULT, + SEQ_CHANNEL_SETTING_PULSEWIDTH, + // + SEQ_CHANNEL_SETTING_SCALE, + SEQ_CHANNEL_SETTING_OCTAVE, + SEQ_CHANNEL_SETTING_ROOT, + SEQ_CHANNEL_SETTING_OCTAVE_AUX, + SEQ_CHANNEL_SETTING_SCALE_MASK, + // + SEQ_CHANNEL_SETTING_MASK1, + SEQ_CHANNEL_SETTING_MASK2, + SEQ_CHANNEL_SETTING_MASK3, + SEQ_CHANNEL_SETTING_MASK4, + SEQ_CHANNEL_SETTING_SEQUENCE, + SEQ_CHANNEL_SETTING_SEQUENCE_LEN1, + SEQ_CHANNEL_SETTING_SEQUENCE_LEN2, + SEQ_CHANNEL_SETTING_SEQUENCE_LEN3, + SEQ_CHANNEL_SETTING_SEQUENCE_LEN4, + SEQ_CHANNEL_SETTING_SEQUENCE_PLAYMODE, + SEQ_CHANNEL_SETTING_SEQUENCE_DIRECTION, + SEQ_CHANNEL_SETTING_SEQUENCE_PLAYMODE_CV_RANGES, + SEQ_CHANNEL_SETTING_SEQUENCE_ARP_DIRECTION, + SEQ_CHANNEL_SETTING_SEQUENCE_ARP_RANGE, + SEQ_CHANNEL_SETTING_BROWNIAN_PROBABILITY, + // cv sources + SEQ_CHANNEL_SETTING_MULT_CV_SOURCE, + SEQ_CHANNEL_SETTING_TRANSPOSE_CV_SOURCE, + SEQ_CHANNEL_SETTING_PULSEWIDTH_CV_SOURCE, + SEQ_CHANNEL_SETTING_OCTAVE_CV_SOURCE, + SEQ_CHANNEL_SETTING_ROOT_CV_SOURCE, + SEQ_CHANNEL_SETTING_OCTAVE_AUX_CV_SOURCE, + SEQ_CHANNEL_SETTING_SEQ_CV_SOURCE, + SEQ_CHANNEL_SETTING_SCALE_MASK_CV_SOURCE, + SEQ_CHANNEL_SETTING_SEQUENCE_ARP_DIRECTION_CV_SOURCE, + SEQ_CHANNEL_SETTING_SEQUENCE_ARP_RANGE_CV_SOURCE, + SEQ_CHANNEL_SETTING_DIRECTION_CV_SOURCE, + SEQ_CHANNEL_SETTING_BROWNIAN_CV_SOURCE, + SEQ_CHANNEL_SETTING_LENGTH_CV_SOURCE, + SEQ_CHANNEL_SETTING_ENV_ATTACK_CV_SOURCE, + SEQ_CHANNEL_SETTING_ENV_DECAY_CV_SOURCE, + SEQ_CHANNEL_SETTING_ENV_SUSTAIN_CV_SOURCE, + SEQ_CHANNEL_SETTING_ENV_RELEASE_CV_SOURCE, + SEQ_CHANNEL_SETTING_ENV_LOOPS_CV_SOURCE, + SEQ_CHANNEL_SETTING_DUMMY, + // aux envelope settings + SEQ_CHANNEL_SETTING_ENV_ATTACK_DURATION, + SEQ_CHANNEL_SETTING_ENV_ATTACK_SHAPE, + SEQ_CHANNEL_SETTING_ENV_DECAY_DURATION, + SEQ_CHANNEL_SETTING_ENV_DECAY_SHAPE, + SEQ_CHANNEL_SETTING_ENV_SUSTAIN_LEVEL, + SEQ_CHANNEL_SETTING_ENV_RELEASE_DURATION, + SEQ_CHANNEL_SETTING_ENV_RELEASE_SHAPE, + SEQ_CHANNEL_SETTING_ENV_MAX_LOOPS, + SEQ_CHANNEL_SETTING_ENV_ATTACK_RESET_BEHAVIOUR, + SEQ_CHANNEL_SETTING_ENV_ATTACK_FALLING_GATE_BEHAVIOUR, + SEQ_CHANNEL_SETTING_ENV_DECAY_RELEASE_RESET_BEHAVIOUR, + // marker + SEQ_CHANNEL_SETTING_LAST +}; + +enum SEQ_ChannelTriggerSource { + SEQ_CHANNEL_TRIGGER_TR1, + SEQ_CHANNEL_TRIGGER_TR2, + SEQ_CHANNEL_TRIGGER_NONE, + SEQ_CHANNEL_TRIGGER_FREEZE_HI2, + SEQ_CHANNEL_TRIGGER_FREEZE_LO2, + SEQ_CHANNEL_TRIGGER_FREEZE_HI4, + SEQ_CHANNEL_TRIGGER_FREEZE_LO4, + SEQ_CHANNEL_TRIGGER_LAST +}; + +enum SEQ_ChannelCV_Mapping { + CHANNEL_CV_MAPPING_CV1, + CHANNEL_CV_MAPPING_CV2, + CHANNEL_CV_MAPPING_CV3, + CHANNEL_CV_MAPPING_CV4, + CHANNEL_CV_MAPPING_LAST +}; + +enum SEQ_CLOCKSTATES { + OFF, + ON = 0xFFFF +}; + +enum MENU_PAGES { + PARAMETERS, + CV_MAPPING +}; + +enum SEQ_UPDATE { + ALL_OK, + WAIT, + CLEAR +}; + +enum PLAY_MODES { + PM_NONE, + PM_SEQ1, + PM_SEQ2, + PM_SEQ3, + PM_TR1, + PM_TR2, + PM_TR3, + PM_ARP, + PM_SH1, + PM_SH2, + PM_SH3, + PM_SH4, + PM_CV1, + PM_CV2, + PM_CV3, + PM_CV4, + PM_LAST +}; + +enum SEQ_DIRECTIONS { + FORWARD, + REVERSE, + PENDULUM1, + PENDULUM2, + RANDOM, + BROWNIAN, + SEQ_DIRECTIONS_LAST +}; + +enum SQ_AUX_MODES { + GATE_OUT, + COPY, + ENV_AD, + ENV_ADR, + ENV_ADSR, + SQ_AUX_MODES_LAST +}; + +uint64_t ext_frequency[SEQ_CHANNEL_TRIGGER_NONE + 1]; + +class SEQ_Channel : public settings::SettingsBase { +public: + + uint8_t get_menu_page() const { + return menu_page_; + } + + bool octave_toggle() { + octave_toggle_ = (~octave_toggle_) & 1u; + return octave_toggle_; + } + + bool poke_octave_toggle() const { + return octave_toggle_; + } + + bool wait_for_EoS() { + return wait_for_EoS_; + } + + void toggle_EoS() { + wait_for_EoS_ = (~wait_for_EoS_) & 1u; + apply_value(SEQ_CHANNEL_SETTING_DUMMY, wait_for_EoS_); + } + + void set_EoS_update() { + wait_for_EoS_ = values_[SEQ_CHANNEL_SETTING_DUMMY]; + } + + void set_menu_page(uint8_t _menu_page) { + menu_page_ = _menu_page; + } + + uint8_t get_aux_mode() const { + return values_[SEQ_CHANNEL_SETTING_MODE]; + } + + uint8_t get_root(uint8_t DUMMY) const { + return values_[SEQ_CHANNEL_SETTING_ROOT]; + } + + uint8_t get_clock_source() const { + return values_[SEQ_CHANNEL_SETTING_CLOCK]; + } + + uint16_t get_trigger_delay() const { + return values_[SEQ_CHANNEL_SETTING_TRIGGER_DELAY]; + } + + void set_clock_source(uint8_t _src) { + apply_value(SEQ_CHANNEL_SETTING_CLOCK, _src); + } + + int get_octave() const { + return values_[SEQ_CHANNEL_SETTING_OCTAVE]; + } + + int get_octave_aux() const { + return values_[SEQ_CHANNEL_SETTING_OCTAVE_AUX]; + } + + int8_t get_multiplier() const { + return values_[SEQ_CHANNEL_SETTING_MULT]; + } + + uint16_t get_pulsewidth() const { + return values_[SEQ_CHANNEL_SETTING_PULSEWIDTH]; + } + + uint8_t get_reset_source() const { + return values_[SEQ_CHANNEL_SETTING_RESET]; + } + + void set_reset_source(uint8_t src) { + apply_value(SEQ_CHANNEL_SETTING_RESET, src); + } + + int get_sequence() const { + return values_[SEQ_CHANNEL_SETTING_SEQUENCE]; + } + + int get_current_sequence() const { + return display_num_sequence_; + } + + int get_direction() const { + return values_[SEQ_CHANNEL_SETTING_SEQUENCE_DIRECTION]; + } + + int get_direction_cv() const { + return values_[SEQ_CHANNEL_SETTING_DIRECTION_CV_SOURCE]; + } + + int get_playmode() const { + return values_[SEQ_CHANNEL_SETTING_SEQUENCE_PLAYMODE]; + } + + int draw_clock() const { + return get_playmode() != PM_ARP; + } + + int get_arp_direction() const { + return values_[SEQ_CHANNEL_SETTING_SEQUENCE_ARP_DIRECTION]; + } + + int get_arp_range() const { + return values_[SEQ_CHANNEL_SETTING_SEQUENCE_ARP_RANGE]; + } + + int get_display_num_sequence() const { + return display_num_sequence_; + } + + int get_display_length() const { + return active_sequence_length_; + } + + void set_display_num_sequence(uint8_t seq) { + display_num_sequence_ = seq; + } + + int get_display_mask() const { + return display_mask_; + } + + void set_sequence(uint8_t seq) { + apply_value(SEQ_CHANNEL_SETTING_SEQUENCE, seq); + } + + void set_gate(uint16_t _state) { + gate_state_ = _state; + } + + uint8_t get_sequence_length(uint8_t _num_seq) const { + + switch (_num_seq) { + + case 0: + return values_[SEQ_CHANNEL_SETTING_SEQUENCE_LEN1]; + break; + case 1: + return values_[SEQ_CHANNEL_SETTING_SEQUENCE_LEN2]; + break; + case 2: + return values_[SEQ_CHANNEL_SETTING_SEQUENCE_LEN3]; + break; + case 3: + return values_[SEQ_CHANNEL_SETTING_SEQUENCE_LEN4]; + break; + default: + return values_[SEQ_CHANNEL_SETTING_SEQUENCE_LEN1]; + break; + } + } + + uint8_t get_sequence_length_cv_source() const { + return values_[SEQ_CHANNEL_SETTING_LENGTH_CV_SOURCE]; + } + + uint8_t get_mult_cv_source() const { + return values_[SEQ_CHANNEL_SETTING_MULT_CV_SOURCE]; + } + + uint8_t get_transpose_cv_source() const { + return values_[SEQ_CHANNEL_SETTING_TRANSPOSE_CV_SOURCE]; + } + + uint8_t get_pulsewidth_cv_source() const { + return values_[SEQ_CHANNEL_SETTING_PULSEWIDTH_CV_SOURCE]; + } + + uint8_t get_scale_mask_cv_source() const { + return values_[SEQ_CHANNEL_SETTING_SCALE_MASK_CV_SOURCE]; + } + + uint8_t get_sequence_cv_source() const { + return values_[SEQ_CHANNEL_SETTING_SEQ_CV_SOURCE]; + } + + uint8_t get_octave_cv_source() const { + return values_[SEQ_CHANNEL_SETTING_OCTAVE_CV_SOURCE]; + } + + uint8_t get_root_cv_source() const { + return values_[SEQ_CHANNEL_SETTING_ROOT_CV_SOURCE]; + } + + uint8_t get_octave_aux_cv_source() const { + return values_[SEQ_CHANNEL_SETTING_OCTAVE_AUX_CV_SOURCE]; + } + + uint8_t get_arp_direction_cv_source() const { + return values_[SEQ_CHANNEL_SETTING_SEQUENCE_ARP_DIRECTION_CV_SOURCE]; + } + + uint8_t get_arp_range_cv_source() const { + return values_[SEQ_CHANNEL_SETTING_SEQUENCE_ARP_RANGE_CV_SOURCE]; + } + + uint8_t get_brownian_probability() const { + return values_[SEQ_CHANNEL_SETTING_BROWNIAN_PROBABILITY]; + } + + int8_t get_brownian_probability_cv() const { + return values_[SEQ_CHANNEL_SETTING_BROWNIAN_CV_SOURCE]; + } + + uint16_t get_attack_duration() const { + return SCALE8_16(values_[SEQ_CHANNEL_SETTING_ENV_ATTACK_DURATION]); + } + + int8_t get_attack_duration_cv() const { + return values_[SEQ_CHANNEL_SETTING_ENV_ATTACK_CV_SOURCE]; + } + + peaks::EnvelopeShape get_attack_shape() const { + return static_cast(values_[SEQ_CHANNEL_SETTING_ENV_ATTACK_SHAPE]); + } + + uint16_t get_decay_duration() const { + return SCALE8_16(values_[SEQ_CHANNEL_SETTING_ENV_DECAY_DURATION]); + } + + int8_t get_decay_duration_cv() const { + return values_[SEQ_CHANNEL_SETTING_ENV_DECAY_CV_SOURCE]; + } + + peaks::EnvelopeShape get_decay_shape() const { + return static_cast(values_[SEQ_CHANNEL_SETTING_ENV_DECAY_SHAPE]); + } + + uint16_t get_sustain_level() const { + return SCALE8_16(values_[SEQ_CHANNEL_SETTING_ENV_SUSTAIN_LEVEL]); + } + + int8_t get_sustain_level_cv() const { + return values_[SEQ_CHANNEL_SETTING_ENV_SUSTAIN_CV_SOURCE]; + } + + uint16_t get_release_duration() const { + return SCALE8_16(values_[SEQ_CHANNEL_SETTING_ENV_RELEASE_DURATION]); + } + + int8_t get_release_duration_cv() const { + return values_[SEQ_CHANNEL_SETTING_ENV_RELEASE_CV_SOURCE]; + } + + peaks::EnvelopeShape get_release_shape() const { + return static_cast(values_[SEQ_CHANNEL_SETTING_ENV_RELEASE_SHAPE]); + } + + peaks::EnvResetBehaviour get_attack_reset_behaviour() const { + return static_cast(values_[SEQ_CHANNEL_SETTING_ENV_ATTACK_RESET_BEHAVIOUR]); + } + + peaks::EnvFallingGateBehaviour get_attack_falling_gate_behaviour() const { + return static_cast(values_[SEQ_CHANNEL_SETTING_ENV_ATTACK_FALLING_GATE_BEHAVIOUR]); + } + + peaks::EnvResetBehaviour get_decay_release_reset_behaviour() const { + return static_cast(values_[SEQ_CHANNEL_SETTING_ENV_DECAY_RELEASE_RESET_BEHAVIOUR]); + } + + uint16_t get_max_loops() const { + return values_[SEQ_CHANNEL_SETTING_ENV_MAX_LOOPS] << 9 ; + } + + uint8_t get_env_loops_cv_source() const { + return values_[SEQ_CHANNEL_SETTING_ENV_LOOPS_CV_SOURCE]; + } + + void update_pattern_mask(uint16_t mask, uint8_t sequence) { + + switch(sequence) { + + case 1: + apply_value(SEQ_CHANNEL_SETTING_MASK2, mask); + break; + case 2: + apply_value(SEQ_CHANNEL_SETTING_MASK3, mask); + break; + case 3: + apply_value(SEQ_CHANNEL_SETTING_MASK4, mask); + break; + default: + apply_value(SEQ_CHANNEL_SETTING_MASK1, mask); + break; + } + } + + int get_mask(uint8_t _this_num_sequence) const { + + switch(_this_num_sequence) { + + case 1: + return values_[SEQ_CHANNEL_SETTING_MASK2]; + break; + case 2: + return values_[SEQ_CHANNEL_SETTING_MASK3]; + break; + case 3: + return values_[SEQ_CHANNEL_SETTING_MASK4]; + break; + default: + return values_[SEQ_CHANNEL_SETTING_MASK1]; + break; + } + } + + void set_sequence_length(uint8_t len, uint8_t seq) { + + switch(seq) { + case 0: + apply_value(SEQ_CHANNEL_SETTING_SEQUENCE_LEN1, len); + break; + case 1: + apply_value(SEQ_CHANNEL_SETTING_SEQUENCE_LEN2, len); + break; + case 2: + apply_value(SEQ_CHANNEL_SETTING_SEQUENCE_LEN3, len); + break; + case 3: + apply_value(SEQ_CHANNEL_SETTING_SEQUENCE_LEN4, len); + break; + default: + break; + } + } + + uint16_t get_clock_cnt() const { + return clk_cnt_; + } + + uint32_t get_step_pitch() const { + return step_pitch_; + } + + uint32_t get_step_pitch_aux() const { + return step_pitch_aux_; + } + + uint32_t get_step_gate() const { + return gate_state_; + } + + uint32_t get_step_state() const { + return step_state_; + } + + int32_t get_pitch_at_step(uint8_t seq, uint8_t step) const { + + uint8_t _channel_offset = !channel_id_ ? 0x0 : OC::Patterns::NUM_PATTERNS; + + OC::Pattern *read_pattern_ = &OC::user_patterns[seq + _channel_offset]; + return read_pattern_->notes[step]; + } + + void set_pitch_at_step(uint8_t seq, uint8_t step, int32_t pitch) { + + uint8_t _channel_offset = !channel_id_ ? 0x0 : OC::Patterns::NUM_PATTERNS; + + OC::Pattern *write_pattern_ = &OC::user_patterns[seq + _channel_offset]; + write_pattern_->notes[step] = pitch; + } + + uint16_t get_rotated_scale_mask() const { + return last_scale_mask_; + } + + void clear_user_pattern(uint8_t seq) { + + uint8_t _channel_offset = !channel_id_ ? 0x0 : OC::Patterns::NUM_PATTERNS; + memcpy(&OC::user_patterns[seq + _channel_offset], &OC::patterns[0], sizeof(OC::Pattern)); + } + + void copy_seq(uint8_t seq, uint8_t len, uint16_t mask) { + + // which sequence ? + copy_sequence = seq + (!channel_id_ ? 0x0 : OC::Patterns::NUM_PATTERNS); + copy_length = len; + copy_mask = mask; + copy_timeout = 0; + } + + uint8_t paste_seq(uint8_t seq) { + + if (copy_timeout < COPYTIMEOUT) { + + // which sequence to copy to ? + uint8_t sequence = seq + (!channel_id_ ? 0x0 : OC::Patterns::NUM_PATTERNS); + // copy length: + set_sequence_length(copy_length, seq); + // copy mask: + update_pattern_mask(copy_mask, seq); + // copy note values: + memcpy(&OC::user_patterns[sequence], &OC::user_patterns[copy_sequence], sizeof(OC::Pattern)); + // give more time for more pasting... + copy_timeout = 0; + + return copy_length; + } + else + return 0; + } + + uint8_t getTriggerState() const { + return clock_display_.getState(); + } + + int num_enabled_settings() const { + return num_enabled_settings_; + } + + void pattern_changed(uint16_t mask, bool force_update) { + + force_update_ = force_update; + if (force_update) + display_mask_ = mask; + + if (get_playmode() == PM_ARP) { + // update note stack + uint8_t seq = active_sequence_; + arpeggiator_.UpdateArpeggiator(channel_id_, seq, get_mask(seq), get_sequence_length(seq)); + } + } + + void sync() { + pending_sync_ = true; + } + + void clear_CV_mapping() { + apply_value(SEQ_CHANNEL_SETTING_PULSEWIDTH_CV_SOURCE, 0); + apply_value(SEQ_CHANNEL_SETTING_MULT_CV_SOURCE, 0); + apply_value(SEQ_CHANNEL_SETTING_OCTAVE_CV_SOURCE, 0); + apply_value(SEQ_CHANNEL_SETTING_ROOT_CV_SOURCE, 0); + apply_value(SEQ_CHANNEL_SETTING_OCTAVE_AUX_CV_SOURCE, 0); + apply_value(SEQ_CHANNEL_SETTING_SEQ_CV_SOURCE, 0); + apply_value(SEQ_CHANNEL_SETTING_SCALE_MASK_CV_SOURCE, 0); + apply_value(SEQ_CHANNEL_SETTING_TRANSPOSE_CV_SOURCE, 0); + apply_value(SEQ_CHANNEL_SETTING_DIRECTION_CV_SOURCE, 0); + apply_value(SEQ_CHANNEL_SETTING_SEQUENCE_ARP_DIRECTION_CV_SOURCE, 0); + apply_value(SEQ_CHANNEL_SETTING_SEQUENCE_ARP_RANGE_CV_SOURCE, 0); + apply_value(SEQ_CHANNEL_SETTING_BROWNIAN_CV_SOURCE, 0); + apply_value(SEQ_CHANNEL_SETTING_LENGTH_CV_SOURCE, 0); + apply_value(SEQ_CHANNEL_SETTING_ENV_ATTACK_CV_SOURCE, 0); + apply_value(SEQ_CHANNEL_SETTING_ENV_DECAY_CV_SOURCE, 0); + apply_value(SEQ_CHANNEL_SETTING_ENV_SUSTAIN_CV_SOURCE, 0); + apply_value(SEQ_CHANNEL_SETTING_ENV_RELEASE_CV_SOURCE, 0); + apply_value(SEQ_CHANNEL_SETTING_ENV_LOOPS_CV_SOURCE, 0); + } + + int get_scale(uint8_t dummy) const { + return values_[SEQ_CHANNEL_SETTING_SCALE]; + } + + void set_scale(int scale) { + apply_value(SEQ_CHANNEL_SETTING_SCALE, scale); + } + + // dummy + int get_scale_select() const { + return 0; + } + + // dummy + void set_scale_at_slot(int scale, uint16_t mask, int root, int transpose, uint8_t scale_slot) { + + } + + // dummy + int get_transpose(uint8_t DUMMY) const { + return 0; + } + + int get_scale_mask(uint8_t scale_select) const { + return values_[SEQ_CHANNEL_SETTING_SCALE_MASK]; + } + + void scale_changed() { + force_scale_update_ = true; + } + + void update_scale_mask(uint16_t mask, uint16_t dummy) { + force_scale_update_ = true; + apply_value(SEQ_CHANNEL_SETTING_SCALE_MASK, mask); // Should automatically be updated + } + + void update_inputmap(int num_slots, uint8_t range) { + input_map_.Configure(OC::InputMaps::GetInputMap(num_slots), range); + } + + int get_cv_input_range() const { + return values_[SEQ_CHANNEL_SETTING_SEQUENCE_PLAYMODE_CV_RANGES]; + } + + void Init(SEQ_ChannelTriggerSource trigger_source, uint8_t id) { + + InitDefaults(); + trigger_delay_.Init(); + channel_id_ = id; + octave_toggle_ = false; + wait_for_EoS_ = false; + note_repeat_ = false; + menu_page_ = PARAMETERS; + apply_value(SEQ_CHANNEL_SETTING_CLOCK, trigger_source); + quantizer_.Init(); + quantizer_.Requantize(); + input_map_.Init(); + env_.Init(); + force_update_ = true; + force_scale_update_ = true; + gate_state_ = step_state_ = OFF; + step_pitch_ = 0; + step_pitch_aux_ = 0; + subticks_ = 0; + tickjitter_ = 10000; + clk_cnt_ = 0; + clk_src_ = get_clock_source(); + prev_reset_state_ = false; + reset_pending_ = false; + prev_multiplier_ = get_multiplier(); + prev_pulsewidth_ = get_pulsewidth(); + prev_input_range_ = 0; + prev_playmode_ = get_playmode(); + pending_sync_ = false; + sequence_change_pending_ = 0x0; + prev_gate_raised_ = 0 ; + env_gate_raised_ = 0 ; + env_gate_state_ = 0 ; + + ext_frequency_in_ticks_ = 0xFFFFFFFF; + channel_frequency_in_ticks_ = 0xFFFFFFFF; + pulse_width_in_ticks_ = get_pulsewidth() << 10; + + display_num_sequence_ = get_sequence(); + display_mask_ = get_mask(display_num_sequence_); + active_sequence_ = display_num_sequence_; + sequence_manual_ = display_num_sequence_; + sequence_advance_state_ = false; + pendulum_fwd_ = true; + uint32_t _seed = OC::ADC::value() + OC::ADC::value() + OC::ADC::value() + OC::ADC::value(); + randomSeed(_seed); + clock_display_.Init(); + arpeggiator_.Init(); + update_enabled_settings(0); + } + + bool rotate_scale(int32_t mask_rotate) { + + uint16_t scale_mask = get_scale_mask(DUMMY); + const int scale = get_scale(DUMMY); + + if (mask_rotate) + scale_mask = OC::ScaleEditor::RotateMask(scale_mask, OC::Scales::GetScale(scale).num_notes, mask_rotate); + + if (last_scale_mask_ != scale_mask) { + + force_scale_update_ = false; + last_scale_ = scale; + last_scale_mask_ = scale_mask; + quantizer_.Configure(OC::Scales::GetScale(scale), scale_mask); + return true; + } else { + return false; + } + } + + bool update_scale(bool force) { + + const int scale = get_scale(DUMMY); + + if (force || last_scale_ != scale) { + + uint16_t scale_mask = get_scale_mask(DUMMY); + force_scale_update_ = false; + last_scale_ = scale; + last_scale_mask_ = scale_mask; + quantizer_.Configure(OC::Scales::GetScale(scale), scale_mask); + return true; + } else { + return false; + } + } + + void force_update() { + force_update_ = true; + } + + bool update_timeout() { + // wait for ~ 1.5 sec + return (subticks_ > 30000) ? true : false; + } + + /* main channel update below: */ + + inline void Update(uint32_t triggers, DAC_CHANNEL dac_channel) { + + // increment channel ticks .. + subticks_++; + + int8_t _clock_source, _reset_source = 0x0, _aux_mode, _playmode; + int8_t _multiplier = 0x0; + bool _none, _triggered, _tock, _sync, _continuous; + uint32_t _subticks = 0x0, prev_channel_frequency_in_ticks_ = 0x0; + + // core channel parameters -- + // 1. clock source: + _clock_source = get_clock_source(); + + // 2. sequencer aux channel / play modes - + _aux_mode = get_aux_mode(); + _playmode = get_playmode(); + _continuous = _playmode >= PM_SH1 ? true : false; + + // 3. update scale? + update_scale(force_scale_update_); + + // clocked ? + _none = SEQ_CHANNEL_TRIGGER_NONE == _clock_source; + // TR1 or TR3? + _triggered = _clock_source ? (!_none && (triggers & (1 << OC::DIGITAL_INPUT_3))) : (!_none && (triggers & (1 << OC::DIGITAL_INPUT_1))); + _tock = false; + _sync = false; + + // trigger delay: + if (_playmode < PM_SH1) { + + trigger_delay_.Update(); + if (_triggered) + trigger_delay_.Push(OC::trigger_delay_ticks[get_trigger_delay()]); + _triggered = trigger_delay_.triggered(); + } + + // new tick frequency: + if (_clock_source <= SEQ_CHANNEL_TRIGGER_TR2) { + + if (_triggered || clk_src_ != _clock_source) { + ext_frequency_in_ticks_ = ext_frequency[_clock_source]; + _tock = true; + div_cnt_--; + } + } + // store clock source: + clk_src_ = _clock_source; + + if (!_continuous) { + + _multiplier = get_multiplier(); + + if (get_mult_cv_source()) { + _multiplier += (OC::ADC::value(static_cast(get_mult_cv_source() - 1)) + 127) >> 8; + CONSTRAIN(_multiplier, 0, MULT_MAX); + } + + // new multiplier ? + if (prev_multiplier_ != _multiplier) + _tock |= true; + prev_multiplier_ = _multiplier; + + // if so, recalculate channel frequency and corresponding jitter-thresholds: + if (_tock) { + + // when multiplying, skip too closely spaced triggers: + if (_multiplier > MULT_BY_ONE) { + prev_channel_frequency_in_ticks_ = multiply_u32xu32_rshift32(channel_frequency_in_ticks_, TICK_SCALE); + // new frequency: + channel_frequency_in_ticks_ = multiply_u32xu32_rshift32(ext_frequency_in_ticks_, multipliers_[_multiplier-MULT_BY_ONE]); + } + else { + prev_channel_frequency_in_ticks_ = 0x0; + // new frequency (used for pulsewidth): + channel_frequency_in_ticks_ = multiply_u32xu32_rshift32(ext_frequency_in_ticks_, pw_scale_[_multiplier]) << 6; + } + + tickjitter_ = multiply_u32xu32_rshift32(channel_frequency_in_ticks_, TICK_JITTER); + } + // limit frequency to > 0 + if (!channel_frequency_in_ticks_) + channel_frequency_in_ticks_ = 1u; + + // reset? + _reset_source = get_reset_source(); + + if (_reset_source < SEQ_CHANNEL_TRIGGER_NONE && !reset_pending_) { + + uint8_t reset_state_ = !_reset_source ? digitalReadFast(TR2) : digitalReadFast(TR4); // TR1, TR3 are main clock sources + + // ? + if (reset_state_ < prev_reset_state_) { + div_cnt_ = 0x0; + reset_pending_ = true; // reset clock counter below + } + prev_reset_state_ = reset_state_; + } + + /* + * brute force ugly sync hack: + * this, presumably, is needlessly complicated. + * but seems to work ok-ish, w/o too much jitter and missing clocks... + */ + + _subticks = subticks_; + // sync? (manual) + div_cnt_ = pending_sync_ ? 0x0 : div_cnt_; + + if (_multiplier <= MULT_BY_ONE && _triggered && div_cnt_ <= 0) { + // division, so we track + _sync = true; + div_cnt_ = divisors_[_multiplier]; + subticks_ = channel_frequency_in_ticks_; // force sync + } + else if (_multiplier <= MULT_BY_ONE && _triggered) { + // division, mute output: + step_state_ = OFF; + } + else if (_multiplier > MULT_BY_ONE && _triggered) { + // multiplication, force sync, if clocked: + _sync = true; + subticks_ = channel_frequency_in_ticks_; + } + else if (_multiplier > MULT_BY_ONE) + _sync = true; + // end of ugly hack + } + else { + // S+H mode + if (_playmode <= PM_SH4) { + + if (_triggered) { + // new frequency (used for pulsewidth): + channel_frequency_in_ticks_ = ext_frequency_in_ticks_; + subticks_ = 0x0; + } + else + // don't trigger, if no trigger - see below + _continuous = _sync = false; + } + } + + // time to output ? + if ((subticks_ >= channel_frequency_in_ticks_ && _sync) || _continuous) { + + if (!_continuous) { + // reset ticks + subticks_ = 0x0; + //reject, if clock is too jittery or skip quasi-double triggers when ext. frequency increases: + if (_subticks < tickjitter_ || (_subticks < prev_channel_frequency_in_ticks_ && !reset_pending_)) + return; + } + + // resync/clear pending sync + if (_triggered && pending_sync_) { + pending_sync_ = false; + clk_cnt_ = 0x0; + } + + // mute output ? + bool mute = 0; + + switch (_reset_source) { + + case SEQ_CHANNEL_TRIGGER_TR1: + case SEQ_CHANNEL_TRIGGER_TR2: + case SEQ_CHANNEL_TRIGGER_NONE: + break; + case SEQ_CHANNEL_TRIGGER_FREEZE_HI2: + mute = !digitalReadFast(TR2); + break; + case SEQ_CHANNEL_TRIGGER_FREEZE_LO2: + mute = digitalReadFast(TR2); + break; + case SEQ_CHANNEL_TRIGGER_FREEZE_HI4: + mute = !digitalReadFast(TR4); + break; + case SEQ_CHANNEL_TRIGGER_FREEZE_LO4: + mute = digitalReadFast(TR4); + break; + default: + break; + } + + if (mute) + return; + + // mask CV ? + if (get_scale_mask_cv_source()) { + int16_t _rotate = (OC::ADC::value(static_cast(get_scale_mask_cv_source() - 1)) + 127) >> 8; + rotate_scale(_rotate); + } + + // finally, process trigger + output: + if (process_num_seq_channel(_playmode, reset_pending_)) { + + // turn on gate + gate_state_ = ON; + + int8_t _octave = get_octave(); + if (get_octave_cv_source()) + _octave += (OC::ADC::value(static_cast(get_octave_cv_source() - 1)) + 255) >> 9; + + int8_t _transpose = 0x0; + if (get_transpose_cv_source()) { + _transpose += (OC::ADC::value(static_cast(get_transpose_cv_source() - 1)) + 64) >> 7; + CONSTRAIN(_transpose, -12, 12); + } + + int8_t _root = get_root(0x0); + if (get_root_cv_source()) { + _root += (OC::ADC::value(static_cast(get_root_cv_source() - 1)) + 127) >> 8; + CONSTRAIN(_root, 0, 11); + } + + if (_playmode != PM_ARP) { + // use the current sequence, updated in process_num_seq_channel(): + step_pitch_ = get_pitch_at_step(display_num_sequence_, clk_cnt_) + (_octave * 12 << 7); + } + else { + + int8_t arp_range = get_arp_range(); + if (get_arp_range_cv_source()) { + arp_range += (OC::ADC::value(static_cast(get_arp_range_cv_source() - 1)) + 255) >> 9; + CONSTRAIN(arp_range, 0, 4); + } + arpeggiator_.set_range(arp_range); + + int8_t arp_direction = get_arp_direction(); + if (get_arp_direction_cv_source()) { + arp_direction += (OC::ADC::value(static_cast(get_arp_direction_cv_source() - 1)) + 255) >> 9; + CONSTRAIN(arp_direction, 0, 4); + } + arpeggiator_.set_direction(arp_direction); + + step_pitch_ = arpeggiator_.ClockArpeggiator() + (_octave * 12 << 7); + // mute ? + if (step_pitch_ == 0xFFFFFF) + gate_state_ = step_state_ = OFF; + } + // update output: + step_pitch_ = quantizer_.Process(step_pitch_, _root << 7, _transpose); + + int32_t _attack = get_attack_duration(); + int32_t _decay = get_decay_duration(); + int32_t _sustain = get_sustain_level(); + int32_t _release = get_release_duration(); + int32_t _loops = get_max_loops(); + + switch (_aux_mode) { + case ENV_AD: + case ENV_ADR: + case ENV_ADSR: + if (get_attack_duration_cv()) { + _attack += OC::ADC::value(static_cast(get_attack_duration_cv() - 1)) << 3; + USAT16(_attack) ; + } + if (get_decay_duration_cv()) { + _decay += OC::ADC::value(static_cast(get_decay_duration_cv() - 1)) << 3; + USAT16(_decay); + } + if (get_sustain_level_cv()) { + _sustain += OC::ADC::value(static_cast(get_sustain_level_cv() - 1)) << 4; + CONSTRAIN(_sustain, 0, 65534); + } + if (get_release_duration_cv()) { + _release += OC::ADC::value(static_cast(get_release_duration_cv() - 1)) << 3; + USAT16(_release) ; + } + if (get_env_loops_cv_source()) { + _loops += OC::ADC::value(static_cast(get_env_loops_cv_source() - 1)) ; + CONSTRAIN(_loops,1<<8, 65534) ; + } + // set the specified reset behaviours + env_.set_attack_reset_behaviour(get_attack_reset_behaviour()); + env_.set_attack_falling_gate_behaviour(get_attack_falling_gate_behaviour()); + env_.set_decay_release_reset_behaviour(get_decay_release_reset_behaviour()); + // set number of loops + env_.set_max_loops(_loops); + break; + default: + break; + } + + switch (_aux_mode) { + + case COPY: + { + int8_t _octave_aux = _octave + get_octave_aux(); + if (get_octave_aux_cv_source()) + _octave_aux += (OC::ADC::value(static_cast(get_octave_aux_cv_source() - 1)) + 255) >> 9; + + if (_playmode != PM_ARP) + step_pitch_aux_ = get_pitch_at_step(display_num_sequence_, clk_cnt_) + (_octave_aux * 12 << 7); + else + // this *might* not be quite a copy... + step_pitch_aux_ = step_pitch_ + (_octave_aux * 12 << 7); + step_pitch_aux_ = quantizer_.Process(step_pitch_aux_, _root << 7, _transpose); + } + break; + case ENV_AD: + { + env_.set_ad(_attack, _decay, 0, 2); + env_.set_attack_shape(get_attack_shape()); + env_.set_decay_shape(get_decay_shape()); + } + break; + case ENV_ADR: + { + env_.set_adr(_attack, _decay, _sustain >> 1, _release, 0, 2); + env_.set_attack_shape(get_attack_shape()); + env_.set_decay_shape(get_decay_shape()); + env_.set_release_shape(get_release_shape()); + } + break; + case ENV_ADSR: + { + env_.set_adsr(_attack, _decay, _sustain >> 1, _release); + env_.set_attack_shape(get_attack_shape()); + env_.set_decay_shape(get_decay_shape()); + env_.set_release_shape(get_release_shape()); + } + break; + default: + break; + } + } + // clear for reset: + reset_pending_ = false; + } + + /* + * below: pulsewidth stuff + */ + + if (_aux_mode != COPY && gate_state_) { + + // pulsewidth setting -- + int16_t _pulsewidth = get_pulsewidth(); + bool _we_cannot_echo = _playmode >= PM_CV1 ? true : false; + + if (_pulsewidth || _multiplier > MULT_BY_ONE || _we_cannot_echo) { + + bool _gates = false; + + // do we echo && multiply? if so, do half-duty cycle: + if (!_pulsewidth) + _pulsewidth = PULSEW_MAX; + + if (_pulsewidth == PULSEW_MAX) + _gates = true; + // CV? + if (get_pulsewidth_cv_source()) { + + _pulsewidth += (OC::ADC::value(static_cast(get_pulsewidth_cv_source() - 1)) + 4) >> 3; + if (!_gates) + CONSTRAIN(_pulsewidth, 1, PULSEW_MAX); + else // CV for 50% duty cycle: + CONSTRAIN(_pulsewidth, 1, (PULSEW_MAX<<1) - 55); // incl margin, max < 2x mult. see below + } + // recalculate (in ticks), if new pulsewidth setting: + if (prev_pulsewidth_ != _pulsewidth || ! subticks_) { + if (!_gates || _we_cannot_echo) { + int32_t _fraction = signed_multiply_32x16b(TICKS_TO_MS, static_cast(_pulsewidth)); // = * 0.6667f + _fraction = signed_saturate_rshift(_fraction, 16, 0); + pulse_width_in_ticks_ = (_pulsewidth << 4) + _fraction; + } + else { // put out gates/half duty cycle: + pulse_width_in_ticks_ = channel_frequency_in_ticks_ >> 1; + + if (_pulsewidth != PULSEW_MAX) { // CV? + pulse_width_in_ticks_ = signed_multiply_32x16b(static_cast(_pulsewidth) << 8, pulse_width_in_ticks_); // + pulse_width_in_ticks_ = signed_saturate_rshift(pulse_width_in_ticks_, 16, 0); + } + } + } + prev_pulsewidth_ = _pulsewidth; + + // limit pulsewidth, if approaching half duty cycle: + if (!_gates && pulse_width_in_ticks_ >= channel_frequency_in_ticks_>>1) + pulse_width_in_ticks_ = (channel_frequency_in_ticks_ >> 1) | 1u; + + // turn off output? + if (subticks_ >= pulse_width_in_ticks_) + gate_state_ = OFF; + else // keep on + gate_state_ = ON; + } + else { + // we simply echo the pulsewidth: + bool _state = (_clock_source == SEQ_CHANNEL_TRIGGER_TR1) ? !digitalReadFast(TR1) : !digitalReadFast(TR3); + + if (_state) + gate_state_ = ON; + else + gate_state_ = OFF; + } + } + } // end update + + /* details re: sequence processing happens (mostly) here: */ + inline bool process_num_seq_channel(uint8_t playmode, uint8_t reset) { + + bool _out = true; + bool _change = true; + bool _reset = reset; + int8_t _playmode, sequence_max, sequence_cnt, _num_seq, num_sequence_cv, sequence_length, sequence_length_cv; + + _num_seq = get_sequence(); + _playmode = playmode; + sequence_max = 0x0; + sequence_cnt = 0x0; + num_sequence_cv = 0x0; + sequence_length = 0x0; + sequence_length_cv = 0x0; + + if (_num_seq != sequence_manual_) { + // setting changed ... + if (!wait_for_EoS_) { + _reset = true; + if (_playmode >= PM_TR1 && _playmode <= PM_TR3) + active_sequence_ = _num_seq; + } + else if (_playmode < PM_SH1) + sequence_change_pending_ = WAIT; + } + sequence_manual_ = _num_seq; + + if (sequence_change_pending_ == WAIT) + _num_seq = active_sequence_; + else if (sequence_change_pending_ == CLEAR) { + _reset = true; + sequence_change_pending_ = ALL_OK; + if (_playmode >= PM_TR1 && _playmode <= PM_TR3) + active_sequence_ = _num_seq; + } + + if (get_sequence_cv_source()) { + num_sequence_cv = _num_seq += (OC::ADC::value(static_cast(get_sequence_cv_source() - 1)) + 255) >> 9; + CONSTRAIN(_num_seq, 0, OC::Patterns::PATTERN_USER_LAST - 0x1); + } + + if (get_sequence_length_cv_source()) + sequence_length_cv = (OC::ADC::value(static_cast(get_sequence_length_cv_source() - 1)) + 64) >> 7; + + switch (_playmode) { + + case PM_NONE: + active_sequence_ = _num_seq; + break; + case PM_ARP: + sequence_change_pending_ = ALL_OK; + sequence_length = get_sequence_length(_num_seq) + sequence_length_cv; + CONSTRAIN(sequence_length, OC::Patterns::kMin, OC::Patterns::kMax); + + if (active_sequence_ != _num_seq || sequence_length != active_sequence_length_ || prev_playmode_ != _playmode) + arpeggiator_.UpdateArpeggiator(channel_id_, _num_seq, get_mask(_num_seq), sequence_length); + active_sequence_ = _num_seq; + active_sequence_length_ = sequence_length; + if (_reset) + arpeggiator_.reset(); + prev_playmode_ = _playmode; + // and skip the stuff below: + _playmode = 0xFF; + break; + case PM_SEQ1: + case PM_SEQ2: + case PM_SEQ3: + { + // concatenate sequences: + sequence_max = _playmode; + + if (sequence_EoS_) { + + // increment sequence # + sequence_cnt_ += sequence_EoS_; + // reset sequence # + sequence_cnt_ = sequence_cnt_ > sequence_max ? 0x0 : sequence_cnt_; + // update + active_sequence_ = _num_seq + sequence_cnt_; + // wrap around: + if (active_sequence_ >= OC::Patterns::PATTERN_USER_LAST) + active_sequence_ -= OC::Patterns::PATTERN_USER_LAST; + // reset + _clock(get_sequence_length(active_sequence_), 0x0, sequence_max, true); + _reset = true; + } + else if (num_sequence_cv) { + active_sequence_ += num_sequence_cv; + CONSTRAIN(active_sequence_, 0, OC::Patterns::PATTERN_USER_LAST - 1); + } + sequence_cnt = sequence_cnt_; + } + break; + case PM_TR1: + case PM_TR2: + case PM_TR3: + { + sequence_max = _playmode - PM_SEQ3; + prev_playmode_ = _playmode; + // trigger? + uint8_t _advance_trig = (channel_id_ == DAC_CHANNEL_A) ? digitalReadFast(TR2) : digitalReadFast(TR4); + + if (_advance_trig < sequence_advance_state_) { + + // increment sequence # + sequence_cnt_++; + // reset sequence # + sequence_cnt_ = sequence_cnt_ > sequence_max ? 0x0 : sequence_cnt_; + // update + active_sequence_ = _num_seq + sequence_cnt_; + // + reset + _reset = true; + // wrap around: + if (active_sequence_ >= OC::Patterns::PATTERN_USER_LAST) + active_sequence_ -= OC::Patterns::PATTERN_USER_LAST; + } + else if (num_sequence_cv) { + active_sequence_ += num_sequence_cv; + CONSTRAIN(active_sequence_, 0, OC::Patterns::PATTERN_USER_LAST - 1); + } + sequence_advance_state_ = _advance_trig; + sequence_max = 0x0; + } + break; + case PM_SH1: + case PM_SH2: + case PM_SH3: + case PM_SH4: + { + int input_range; + sequence_length = get_sequence_length(_num_seq) + sequence_length_cv; + CONSTRAIN(sequence_length, OC::Patterns::kMin, OC::Patterns::kMax); + input_range = get_cv_input_range(); + + // length or range changed ? + if (active_sequence_length_ != sequence_length || input_range != prev_input_range_ || prev_playmode_ != _playmode) + update_inputmap(sequence_length, input_range); + // store values: + active_sequence_ = _num_seq; + active_sequence_length_ = sequence_length; + prev_input_range_ = input_range; + prev_playmode_ = _playmode; + + // process input: + if (!input_range) + clk_cnt_ = input_map_.Process(OC::ADC::value(static_cast(_playmode - PM_SH1))); + else + clk_cnt_ = input_map_.Process(0xFFF - OC::ADC::smoothed_raw_value(static_cast(_playmode - PM_SH1))); + } + break; + case PM_CV1: + case PM_CV2: + case PM_CV3: + case PM_CV4: + { + int input_range; + sequence_length = get_sequence_length(_num_seq) + sequence_length_cv; + CONSTRAIN(sequence_length, OC::Patterns::kMin, OC::Patterns::kMax); + input_range = get_cv_input_range(); + // length changed ? + if (active_sequence_length_ != sequence_length || input_range != prev_input_range_ || prev_playmode_ != _playmode) + update_inputmap(sequence_length, input_range); + // store values: + active_sequence_length_ = sequence_length; + prev_input_range_ = input_range; + active_sequence_ = _num_seq; + prev_playmode_ = _playmode; + + // process input: + if (!input_range) + clk_cnt_ = input_map_.Process(OC::ADC::value(static_cast(_playmode - PM_CV1))); // = 5V + else + clk_cnt_ = input_map_.Process(0xFFF - OC::ADC::smoothed_raw_value(static_cast(_playmode - PM_CV1))); // = 10V + + // update output, if slot # changed: + if (prev_slot_ == clk_cnt_) + _change = false; + else { + subticks_ = 0x0; + prev_slot_ = clk_cnt_; + } + } + break; + default: + break; + } + // end switch + + _num_seq = active_sequence_; + + if (_playmode < PM_SH1) { + + sequence_length = get_sequence_length(_num_seq) + sequence_length_cv; + if (sequence_length_cv) + CONSTRAIN(sequence_length, OC::Patterns::kMin, OC::Patterns::kMax); + + CONSTRAIN(clk_cnt_, 0x0, sequence_length); + sequence_EoS_ = _clock(sequence_length - 0x1, sequence_cnt, sequence_max, _reset); + } + + // this is the current sequence # (USER1-USER4): + display_num_sequence_ = _num_seq; + // and corresponding pattern mask: + display_mask_ = get_mask(_num_seq); + // ... and the length + active_sequence_length_ = sequence_length; + + // slot at current position: + if (_playmode != PM_ARP) { + _out = (display_mask_ >> clk_cnt_) & 1u; + step_state_ = _out ? ON : OFF; + _out = (_out && _change) ? true : false; + } + else { + step_state_ = ON; + clk_cnt_ = 0x0; + } + // return step: + return _out; + } + + // update sequencer clock, return -1, 0, 1 when EoS is reached: + + int8_t _clock(uint8_t sequence_length, uint8_t sequence_count, uint8_t sequence_max, bool _reset) { + + int8_t EoS = 0x0, _clk_cnt, _direction; + bool reset = _reset; + + _clk_cnt = clk_cnt_; + _direction = get_direction(); + + if (get_direction_cv()) { + _direction += (OC::ADC::value(static_cast(get_direction_cv() - 1)) + 255) >> 9; + CONSTRAIN(_direction, 0, SEQ_DIRECTIONS_LAST - 0x1); + } + + switch (_direction) { + + case FORWARD: + { + _clk_cnt++; + if (reset) + _clk_cnt = 0x0; + // end of sequence? + else if (_clk_cnt > sequence_length) + _clk_cnt = 0x0; + else if (_clk_cnt == sequence_length) { + EoS = 0x1; + } + } + break; + case REVERSE: + { + _clk_cnt--; + if (reset) + _clk_cnt = sequence_length; + // end of sequence? + else if (_clk_cnt < 0) + _clk_cnt = sequence_length; + else if (!_clk_cnt) + EoS = 0x1; + } + break; + case PENDULUM1: + case BROWNIAN: + if (BROWNIAN == _direction) { + // Compare Brownian probability and reverse direction if needed + int16_t brown_prb = get_brownian_probability(); + + if (get_brownian_probability_cv()) { + brown_prb += (OC::ADC::value(static_cast(get_brownian_probability_cv() - 1)) + 8) >> 3; + CONSTRAIN(brown_prb, 0, 256); + } + if (random(0,256) < brown_prb) + pendulum_fwd_ = !pendulum_fwd_; + } + { + if (pendulum_fwd_) { + _clk_cnt++; + if (reset) + _clk_cnt = 0x0; + else if (_clk_cnt >= sequence_length) { + + if (sequence_count >= sequence_max) { + pendulum_fwd_ = false; + _clk_cnt = sequence_length; + } + else EoS = 0x1; + // pendulum needs special care (when PM_NONE) + if (!pendulum_fwd_ && sequence_change_pending_ == WAIT) sequence_change_pending_ = CLEAR; + } + } + // reverse direction: + else { + _clk_cnt--; + if (reset) + _clk_cnt = sequence_length; + else if (_clk_cnt <= 0) { + // end of sequence ? + if (sequence_count == 0x0) { + pendulum_fwd_ = true; + _clk_cnt = 0x0; + } + else EoS = -0x1; + if (pendulum_fwd_ && sequence_change_pending_ == WAIT) sequence_change_pending_ = CLEAR; + } + } + } + break; + case PENDULUM2: + { + if (pendulum_fwd_) { + + if (!note_repeat_) + _clk_cnt++; + note_repeat_ = false; + + if (reset) + _clk_cnt = 0x0; + else if (_clk_cnt >= sequence_length) { + // end of sequence ? + if (sequence_count >= sequence_max) { + pendulum_fwd_ = false; + _clk_cnt = sequence_length; + note_repeat_ = true; // repeat last step + } + else EoS = 0x1; + if (!pendulum_fwd_ && sequence_change_pending_ == WAIT) sequence_change_pending_ = CLEAR; + } + } + // reverse direction: + else { + + if (!note_repeat_) + _clk_cnt--; + note_repeat_ = false; + + if (reset) + _clk_cnt = sequence_length; + else if (_clk_cnt <= 0x0) { + // end of sequence ? + if (sequence_count == 0x0) { + pendulum_fwd_ = true; + _clk_cnt = 0x0; + note_repeat_ = true; // repeat first step + } + else EoS = -0x1; + if (pendulum_fwd_ && sequence_change_pending_ == WAIT) sequence_change_pending_ = CLEAR; + } + } + } + break; + case RANDOM: + _clk_cnt = random(sequence_length + 0x1); + if (reset) + _clk_cnt = 0x0; + // jump to next sequence if we happen to hit the last note: + else if (_clk_cnt >= sequence_length) + EoS = random(0x2); + break; + default: + break; + } + clk_cnt_ = _clk_cnt; + + if (EoS && sequence_change_pending_ == WAIT) sequence_change_pending_ = CLEAR; + return EoS; + } + + SEQ_ChannelSetting enabled_setting_at(int index) const { + return enabled_settings_[index]; + } + + void update_enabled_settings(uint8_t channel_id) { + + SEQ_ChannelSetting *settings = enabled_settings_; + + switch(get_menu_page()) { + + case PARAMETERS: { + + *settings++ = SEQ_CHANNEL_SETTING_SCALE; + *settings++ = SEQ_CHANNEL_SETTING_SCALE_MASK; + *settings++ = SEQ_CHANNEL_SETTING_SEQUENCE; + + switch (get_sequence()) { + + case 0: + *settings++ = SEQ_CHANNEL_SETTING_MASK1; + break; + case 1: + *settings++ = SEQ_CHANNEL_SETTING_MASK2; + break; + case 2: + *settings++ = SEQ_CHANNEL_SETTING_MASK3; + break; + case 3: + *settings++ = SEQ_CHANNEL_SETTING_MASK4; + break; + default: + break; + } + + *settings++ = SEQ_CHANNEL_SETTING_SEQUENCE_PLAYMODE; + + if (get_playmode() < PM_SH1) { + + *settings++ = (get_playmode() == PM_ARP) ? SEQ_CHANNEL_SETTING_SEQUENCE_ARP_DIRECTION : SEQ_CHANNEL_SETTING_SEQUENCE_DIRECTION; + if (get_playmode() == PM_ARP) + *settings++ = SEQ_CHANNEL_SETTING_SEQUENCE_ARP_RANGE; + else if (get_direction() == BROWNIAN) + *settings++ = SEQ_CHANNEL_SETTING_BROWNIAN_PROBABILITY; + *settings++ = SEQ_CHANNEL_SETTING_MULT; + } + else + *settings++ = SEQ_CHANNEL_SETTING_SEQUENCE_PLAYMODE_CV_RANGES; + + *settings++ = SEQ_CHANNEL_SETTING_OCTAVE; + *settings++ = SEQ_CHANNEL_SETTING_ROOT; + // aux output: + *settings++ = SEQ_CHANNEL_SETTING_MODE; + + switch (get_aux_mode()) { + case GATE_OUT: + *settings++ = SEQ_CHANNEL_SETTING_PULSEWIDTH; + break; + case COPY: + *settings++ = SEQ_CHANNEL_SETTING_OCTAVE_AUX; + break; + case ENV_AD: + *settings++ = SEQ_CHANNEL_SETTING_ENV_ATTACK_DURATION; + *settings++ = SEQ_CHANNEL_SETTING_ENV_ATTACK_SHAPE; + *settings++ = SEQ_CHANNEL_SETTING_ENV_DECAY_DURATION; + *settings++ = SEQ_CHANNEL_SETTING_ENV_DECAY_SHAPE; + *settings++ = SEQ_CHANNEL_SETTING_PULSEWIDTH; + *settings++ = SEQ_CHANNEL_SETTING_ENV_MAX_LOOPS; + break; + case ENV_ADR: + *settings++ = SEQ_CHANNEL_SETTING_ENV_ATTACK_DURATION; + *settings++ = SEQ_CHANNEL_SETTING_ENV_ATTACK_SHAPE; + *settings++ = SEQ_CHANNEL_SETTING_ENV_DECAY_DURATION; + *settings++ = SEQ_CHANNEL_SETTING_ENV_DECAY_SHAPE; + *settings++ = SEQ_CHANNEL_SETTING_PULSEWIDTH; + *settings++ = SEQ_CHANNEL_SETTING_ENV_SUSTAIN_LEVEL; + *settings++ = SEQ_CHANNEL_SETTING_ENV_RELEASE_DURATION; + *settings++ = SEQ_CHANNEL_SETTING_ENV_RELEASE_SHAPE; + *settings++ = SEQ_CHANNEL_SETTING_ENV_MAX_LOOPS; + break; + case ENV_ADSR: + *settings++ = SEQ_CHANNEL_SETTING_ENV_ATTACK_DURATION; + *settings++ = SEQ_CHANNEL_SETTING_ENV_ATTACK_SHAPE; + *settings++ = SEQ_CHANNEL_SETTING_ENV_DECAY_DURATION; + *settings++ = SEQ_CHANNEL_SETTING_ENV_DECAY_SHAPE; + *settings++ = SEQ_CHANNEL_SETTING_PULSEWIDTH; + *settings++ = SEQ_CHANNEL_SETTING_ENV_SUSTAIN_LEVEL; + *settings++ = SEQ_CHANNEL_SETTING_ENV_RELEASE_DURATION; + *settings++ = SEQ_CHANNEL_SETTING_ENV_RELEASE_SHAPE; + break; + default: + break; + } + + if (get_playmode() < PM_SH1) { + *settings++ = SEQ_CHANNEL_SETTING_RESET; + *settings++ = SEQ_CHANNEL_SETTING_CLOCK; + } + } + break; + + case CV_MAPPING: { + + *settings++ = SEQ_CHANNEL_SETTING_TRANSPOSE_CV_SOURCE; // = transpose SCALE + *settings++ = SEQ_CHANNEL_SETTING_SCALE_MASK_CV_SOURCE; // = rotate mask + *settings++ = SEQ_CHANNEL_SETTING_SEQ_CV_SOURCE; // sequence # + + switch (get_sequence()) { + + case 0: + *settings++ = SEQ_CHANNEL_SETTING_MASK1; + break; + case 1: + *settings++ = SEQ_CHANNEL_SETTING_MASK2; + break; + case 2: + *settings++ = SEQ_CHANNEL_SETTING_MASK3; + break; + case 3: + *settings++ = SEQ_CHANNEL_SETTING_MASK4; + break; + default: + break; + } + + *settings++ = SEQ_CHANNEL_SETTING_LENGTH_CV_SOURCE; // = playmode + + if (get_playmode() < PM_SH1) { + + if (get_playmode() == PM_ARP) { + *settings++ = SEQ_CHANNEL_SETTING_SEQUENCE_ARP_DIRECTION_CV_SOURCE; + *settings++ = SEQ_CHANNEL_SETTING_SEQUENCE_ARP_RANGE_CV_SOURCE; + } + else *settings++ = SEQ_CHANNEL_SETTING_DIRECTION_CV_SOURCE; // = directions + + if (get_playmode() != PM_ARP && get_direction() == BROWNIAN) + *settings++ = SEQ_CHANNEL_SETTING_BROWNIAN_CV_SOURCE; + + *settings++ = SEQ_CHANNEL_SETTING_MULT_CV_SOURCE; + + } + else + *settings++ = SEQ_CHANNEL_SETTING_DUMMY; // = range + + *settings++ = SEQ_CHANNEL_SETTING_OCTAVE_CV_SOURCE; + *settings++ = SEQ_CHANNEL_SETTING_ROOT_CV_SOURCE; + *settings++ = SEQ_CHANNEL_SETTING_DUMMY; // = mode + + switch (get_aux_mode()) { + + case GATE_OUT: + *settings++ = SEQ_CHANNEL_SETTING_PULSEWIDTH_CV_SOURCE; + break; + case COPY: + *settings++ = SEQ_CHANNEL_SETTING_OCTAVE_AUX_CV_SOURCE; + break; + case ENV_AD: + *settings++ = SEQ_CHANNEL_SETTING_ENV_ATTACK_CV_SOURCE; + *settings++ = SEQ_CHANNEL_SETTING_ENV_DECAY_CV_SOURCE; + *settings++ = SEQ_CHANNEL_SETTING_ENV_LOOPS_CV_SOURCE; + *settings++ = SEQ_CHANNEL_SETTING_ENV_ATTACK_RESET_BEHAVIOUR; + *settings++ = SEQ_CHANNEL_SETTING_ENV_ATTACK_FALLING_GATE_BEHAVIOUR; + *settings++ = SEQ_CHANNEL_SETTING_ENV_DECAY_RELEASE_RESET_BEHAVIOUR; + break; + case ENV_ADR: + case ENV_ADSR: + *settings++ = SEQ_CHANNEL_SETTING_ENV_ATTACK_CV_SOURCE; + *settings++ = SEQ_CHANNEL_SETTING_ENV_DECAY_CV_SOURCE; + *settings++ = SEQ_CHANNEL_SETTING_ENV_SUSTAIN_CV_SOURCE; + *settings++ = SEQ_CHANNEL_SETTING_ENV_RELEASE_CV_SOURCE; + *settings++ = SEQ_CHANNEL_SETTING_ENV_LOOPS_CV_SOURCE; + *settings++ = SEQ_CHANNEL_SETTING_ENV_ATTACK_RESET_BEHAVIOUR; + *settings++ = SEQ_CHANNEL_SETTING_ENV_ATTACK_FALLING_GATE_BEHAVIOUR; + *settings++ = SEQ_CHANNEL_SETTING_ENV_DECAY_RELEASE_RESET_BEHAVIOUR; + break; + default: + break; + } + + if (get_playmode() < PM_SH1) { + *settings++ = SEQ_CHANNEL_SETTING_TRIGGER_DELAY; // + *settings++ = SEQ_CHANNEL_SETTING_CLOCK; // = reset source + } + + *settings++ = SEQ_CHANNEL_SETTING_DUMMY; // = mode + *settings++ = SEQ_CHANNEL_SETTING_DUMMY; // = mode + *settings++ = SEQ_CHANNEL_SETTING_DUMMY; // = mode + *settings++ = SEQ_CHANNEL_SETTING_DUMMY; // = mode + + } + break; + default: + break; + } + num_enabled_settings_ = settings - enabled_settings_; + } + + template + void update_main_channel() { + int32_t _output = OC::DAC::pitch_to_scaled_voltage_dac(dacChannel, get_step_pitch(), 0, OC::DAC::get_voltage_scaling(dacChannel)); + OC::DAC::set(_output); + + } + + template + void update_aux_channel() + { + + int8_t _mode = get_aux_mode(); + uint32_t _output = 0; + + switch (_mode) { + + case GATE_OUT: // gate + #ifdef BUCHLA_4U + _output = (get_step_gate() == ON) ? OC::DAC::get_octave_offset(dacChannel, OCTAVES - OC::DAC::kOctaveZero - 0x2) : OC::DAC::get_zero_offset(dacChannel); + #else + _output = (get_step_gate() == ON) ? OC::DAC::get_octave_offset(dacChannel, OCTAVES - OC::DAC::kOctaveZero - 0x1) : OC::DAC::get_zero_offset(dacChannel); + #endif + break; + case COPY: // copy + _output = OC::DAC::pitch_to_scaled_voltage_dac(dacChannel, get_step_pitch_aux(), 0, OC::DAC::get_voltage_scaling(dacChannel)); + break; + // code to process envelopes here + case ENV_AD: + case ENV_ADR: + case ENV_ADSR: + env_gate_state_ = 0; + env_gate_raised_ = (get_step_gate() == ON); + if (env_gate_raised_ && !prev_gate_raised_) + env_gate_state_ |= peaks::CONTROL_GATE_RISING; + if (env_gate_raised_) + env_gate_state_ |= peaks::CONTROL_GATE; + else if (prev_gate_raised_) + env_gate_state_ |= peaks::CONTROL_GATE_FALLING; + prev_gate_raised_ = env_gate_raised_; + _output = OC::DAC::get_zero_offset(dacChannel) + env_.ProcessSingleSample(env_gate_state_); + break; + default: + break; + } + OC::DAC::set(_output); + } + + void RenderScreensaver() const; + +private: + + bool channel_id_; + bool octave_toggle_; + bool wait_for_EoS_; + bool note_repeat_; + bool sequence_EoS_; + uint8_t menu_page_; + uint16_t _sync_cnt; + bool force_update_; + bool force_scale_update_; + uint8_t clk_src_; + bool prev_reset_state_; + bool reset_pending_; + uint32_t subticks_; + uint32_t tickjitter_; + int32_t clk_cnt_; + int32_t prev_slot_; + int16_t div_cnt_; + uint32_t ext_frequency_in_ticks_; + uint32_t channel_frequency_in_ticks_; + uint32_t pulse_width_in_ticks_; + uint16_t gate_state_; + uint8_t prev_gate_raised_; + uint8_t env_gate_state_; + uint8_t env_gate_raised_; + uint16_t step_state_; + int32_t step_pitch_; + int32_t step_pitch_aux_; + uint8_t prev_multiplier_; + uint8_t prev_pulsewidth_; + uint8_t display_num_sequence_; + uint16_t display_mask_; + int8_t active_sequence_; + int8_t sequence_manual_; + int8_t active_sequence_length_; + int32_t sequence_cnt_; + int8_t sequence_advance_state_; + int8_t sequence_change_pending_; + int8_t pendulum_fwd_; + int last_scale_; + uint16_t last_scale_mask_; + uint8_t prev_input_range_; + uint8_t prev_playmode_; + bool pending_sync_; + + util::TriggerDelay trigger_delay_; + util::Arpeggiator arpeggiator_; + + int num_enabled_settings_; + SEQ_ChannelSetting enabled_settings_[SEQ_CHANNEL_SETTING_LAST]; + braids::Quantizer quantizer_; + OC::Input_Map input_map_; + OC::DigitalInputDisplay clock_display_; + peaks::MultistageEnvelope env_; + +}; + +const char* const SEQ_CHANNEL_TRIGGER_sources[] = { + "TR1", "TR3", " - " +}; + +const char* const reset_trigger_sources[] = { + "RST2", "RST4", " - ", "=HI2", "=LO2", "=HI4", "=LO4" +}; + +const char* const display_multipliers[] = { + "/64", "/63", "/62", "/49", "/48", "/47", "/33", "/32", "/31", "/17", "/16", "/15", "/8", "/7", "/6", "/5", "/4", "/3", "/2", "-", "x2", "x3", "x4", "x5", "x6", "x7", "x8" +}; + +const char* const modes[] = { + "gate", "copy", "AD", "ADR", "ADSR", +}; + +const char* const cv_ranges[] = { + " 5V", "10V" +}; + +const char* const arp_directions[] = { + "up", "down", "u/d", "rnd" +}; + +const char* const arp_range[] = { + "1", "2", "3", "4" +}; + +// TOTAL EEPROM SIZE: 2 * 54 bytes +SETTINGS_DECLARE(SEQ_Channel, SEQ_CHANNEL_SETTING_LAST) { + + { 0, 0, 4, "aux. mode", modes, settings::STORAGE_TYPE_U4 }, + { SEQ_CHANNEL_TRIGGER_TR1, 0, SEQ_CHANNEL_TRIGGER_NONE, "clock src", SEQ_CHANNEL_TRIGGER_sources, settings::STORAGE_TYPE_U4 }, + { 0, 0, OC::kNumDelayTimes - 1, "trigger delay", OC::Strings::trigger_delay_times, settings::STORAGE_TYPE_U8 }, + { 2, 0, SEQ_CHANNEL_TRIGGER_LAST - 1, "reset/mute", reset_trigger_sources, settings::STORAGE_TYPE_U8 }, + { MULT_BY_ONE, 0, MULT_MAX, "mult/div", display_multipliers, settings::STORAGE_TYPE_U8 }, + { 25, 0, PULSEW_MAX, "--> pw", NULL, settings::STORAGE_TYPE_U8 }, + // + { OC::Scales::SCALE_SEMI, 0, OC::Scales::NUM_SCALES - 1, "scale", OC::scale_names_short, settings::STORAGE_TYPE_U8 }, + { 0, -5, 5, "octave", NULL, settings::STORAGE_TYPE_I8 }, // octave + { 0, 0, 11, "root", OC::Strings::note_names_unpadded, settings::STORAGE_TYPE_U8 }, + { 0, -5, 5, "--> aux +/-", NULL, settings::STORAGE_TYPE_I8 }, // aux octave + { 65535, 1, 65535, "--> edit", NULL, settings::STORAGE_TYPE_U16 }, // mask + // seq + { 65535, 0, 65535, "--> edit", NULL, settings::STORAGE_TYPE_U16 }, // seq 1 + { 65535, 0, 65535, "--> edit", NULL, settings::STORAGE_TYPE_U16 }, // seq 2 + { 65535, 0, 65535, "--> edit", NULL, settings::STORAGE_TYPE_U16 }, // seq 3 + { 65535, 0, 65535, "--> edit", NULL, settings::STORAGE_TYPE_U16 }, // seq 4 + { OC::Patterns::PATTERN_USER_0_1, 0, OC::Patterns::PATTERN_USER_LAST-1, "sequence #", OC::pattern_names_short, settings::STORAGE_TYPE_U8 }, + { OC::Patterns::kMax, OC::Patterns::kMin, OC::Patterns::kMax, "sequence length", NULL, settings::STORAGE_TYPE_U8 }, // seq 1 + { OC::Patterns::kMax, OC::Patterns::kMin, OC::Patterns::kMax, "sequence length", NULL, settings::STORAGE_TYPE_U8 }, // seq 2 + { OC::Patterns::kMax, OC::Patterns::kMin, OC::Patterns::kMax, "sequence length", NULL, settings::STORAGE_TYPE_U8 }, // seq 3 + { OC::Patterns::kMax, OC::Patterns::kMin, OC::Patterns::kMax, "sequence length", NULL, settings::STORAGE_TYPE_U8 }, // seq 4 + { 0, 0, PM_LAST - 1, "playmode", OC::Strings::seq_playmodes, settings::STORAGE_TYPE_U8 }, + { 0, 0, SEQ_DIRECTIONS_LAST - 1, "direction", OC::Strings::seq_directions, settings::STORAGE_TYPE_U8 }, + { 0, 0, 1, "CV adr. range", cv_ranges, settings::STORAGE_TYPE_U4 }, + { 0, 0, 3, "direction", arp_directions, settings::STORAGE_TYPE_U4 }, + { 0, 0, 3, "arp.range", arp_range, settings::STORAGE_TYPE_U8 }, + { 64, 0, 255, "-->brown prob", NULL, settings::STORAGE_TYPE_U8 }, + // cv sources + { 0, 0, 4, "mult/div CV ->", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "transpose ->", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "--> pw ->", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "octave +/- ->", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "root +/- ->", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "--> aux +/- ->", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "sequence # ->", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "mask rotate ->", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "direction ->", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "arp.range ->", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "direction ->", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "-->brwn.prb ->", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "seq.length ->", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "att dur ->", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "dec dur ->", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "sus lvl ->", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "rel dur ->", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U4 }, + { 0, 0, 4, "env loops ->", OC::Strings::cv_input_names_none, settings::STORAGE_TYPE_U8 }, + { 0, 0, 1, "-", NULL, settings::STORAGE_TYPE_U4 }, // DUMMY, use to store update behaviour + // envelope parameters + { 128, 0, 255, "--> att dur", NULL, settings::STORAGE_TYPE_U8 }, + { peaks::ENV_SHAPE_QUARTIC, peaks::ENV_SHAPE_LINEAR, peaks::ENV_SHAPE_LAST - 1, "--> att shape", OC::Strings::envelope_shapes, settings::STORAGE_TYPE_U16 }, + { 128, 0, 255, "--> dec dur", NULL, settings::STORAGE_TYPE_U8 }, + { peaks::ENV_SHAPE_EXPONENTIAL, peaks::ENV_SHAPE_LINEAR, peaks::ENV_SHAPE_LAST - 1, "--> dec shape", OC::Strings::envelope_shapes, settings::STORAGE_TYPE_U16 }, + { 128, 0, 255, "--> sus lvl", NULL, settings::STORAGE_TYPE_U16 }, + { 128, 0, 255, "--> rel dur", NULL, settings::STORAGE_TYPE_U8 }, + { peaks::ENV_SHAPE_EXPONENTIAL, peaks::ENV_SHAPE_LINEAR, peaks::ENV_SHAPE_LAST - 1, "--> rel shape", OC::Strings::envelope_shapes, settings::STORAGE_TYPE_U16 }, + {1, 1, 127, "--> loops", NULL, settings::STORAGE_TYPE_U8 }, + { peaks::RESET_BEHAVIOUR_NULL, peaks::RESET_BEHAVIOUR_NULL, peaks::RESET_BEHAVIOUR_LAST - 1, "att reset", OC::Strings::reset_behaviours, settings::STORAGE_TYPE_U8 }, + { peaks::FALLING_GATE_BEHAVIOUR_IGNORE, peaks::FALLING_GATE_BEHAVIOUR_IGNORE, peaks::FALLING_GATE_BEHAVIOUR_LAST - 1, "att fall gt", OC::Strings::falling_gate_behaviours, settings::STORAGE_TYPE_U8 }, + { peaks::RESET_BEHAVIOUR_SEGMENT_PHASE, peaks::RESET_BEHAVIOUR_NULL, peaks::RESET_BEHAVIOUR_LAST - 1, "dec/rel reset", OC::Strings::reset_behaviours, settings::STORAGE_TYPE_U8 }, +}; + +class SEQ_State { +public: + void Init() { + selected_channel = 0; + cursor.Init(SEQ_CHANNEL_SETTING_MODE, SEQ_CHANNEL_SETTING_LAST - 1); + pattern_editor.Init(); + scale_editor.Init(false); + } + + inline bool editing() const { + return cursor.editing(); + } + + inline int cursor_pos() const { + return cursor.cursor_pos(); + } + + int selected_channel; + menu::ScreenCursor cursor; + menu::ScreenCursor cursor_state; + OC::PatternEditor pattern_editor; + OC::ScaleEditor scale_editor; +}; + +SEQ_State seq_state; +SEQ_Channel seq_channel[NUM_CHANNELS]; + +void SEQ_init() { + + ext_frequency[SEQ_CHANNEL_TRIGGER_TR1] = 0xFFFFFFFF; + ext_frequency[SEQ_CHANNEL_TRIGGER_TR2] = 0xFFFFFFFF; + ext_frequency[SEQ_CHANNEL_TRIGGER_NONE] = 0xFFFFFFFF; + + seq_state.Init(); + for (size_t i = 0; i < NUM_CHANNELS; ++i) + seq_channel[i].Init(static_cast(SEQ_CHANNEL_TRIGGER_TR1), i); + seq_state.cursor.AdjustEnd(seq_channel[0].num_enabled_settings() - 1); +} + +size_t SEQ_storageSize() { + return NUM_CHANNELS * SEQ_Channel::storageSize(); +} + +size_t SEQ_save(void *storage) { + + size_t used = 0; + for (size_t i = 0; i < NUM_CHANNELS; ++i) { + used += seq_channel[i].Save(static_cast(storage) + used); + } + return used; +} + +size_t SEQ_restore(const void *storage) { + + size_t used = 0; + + for (size_t i = 0; i < NUM_CHANNELS; ++i) { + used += seq_channel[i].Restore(static_cast(storage) + used); + // update display + seq_channel[i].pattern_changed(seq_channel[i].get_mask(seq_channel[i].get_sequence()), true); + seq_channel[i].set_display_num_sequence(seq_channel[i].get_sequence()); + seq_channel[i].update_enabled_settings(i); + seq_channel[i].set_EoS_update(); + } + seq_state.cursor.AdjustEnd(seq_channel[0].num_enabled_settings() - 1); + return used; +} + +void SEQ_handleAppEvent(OC::AppEvent event) { + switch (event) { + case OC::APP_EVENT_RESUME: + seq_state.cursor.set_editing(false); + seq_state.pattern_editor.Close(); + seq_state.scale_editor.Close(); + break; + case OC::APP_EVENT_SUSPEND: + case OC::APP_EVENT_SCREENSAVER_ON: + case OC::APP_EVENT_SCREENSAVER_OFF: + { + SEQ_Channel &selected = seq_channel[seq_state.selected_channel]; + selected.set_menu_page(PARAMETERS); + selected.update_enabled_settings(seq_state.selected_channel); + } + break; + } +} + +void SEQ_loop() { +} + +void SEQ_isr() { + + ticks_src1++; // src #1 ticks + ticks_src2++; // src #2 ticks + copy_timeout++; + + uint32_t triggers = OC::DigitalInputs::clocked(); + + if (triggers & (1 << OC::DIGITAL_INPUT_1)) { + ext_frequency[SEQ_CHANNEL_TRIGGER_TR1] = ticks_src1; + ticks_src1 = 0x0; + } + if (triggers & (1 << OC::DIGITAL_INPUT_3)) { + ext_frequency[SEQ_CHANNEL_TRIGGER_TR2] = ticks_src2; + ticks_src2 = 0x0; + } + + // update sequencer channels 1, 2: + seq_channel[0].Update(triggers, DAC_CHANNEL_A); + seq_channel[1].Update(triggers, DAC_CHANNEL_B); + // update DAC channels A, B: + seq_channel[0].update_main_channel(); + seq_channel[1].update_main_channel(); + // update DAC channels C, D: + seq_channel[0].update_aux_channel(); + seq_channel[1].update_aux_channel(); +} + +void SEQ_handleButtonEvent(const UI::Event &event) { + + if (UI::EVENT_BUTTON_LONG_PRESS == event.type) { + switch (event.control) { + case OC::CONTROL_BUTTON_UP: + SEQ_upButtonLong(); + break; + case OC::CONTROL_BUTTON_DOWN: + SEQ_downButtonLong(); + break; + case OC::CONTROL_BUTTON_L: + if (!(seq_state.pattern_editor.active())) + SEQ_leftButtonLong(); + break; + default: + break; + } + } + + if (seq_state.pattern_editor.active()) { + seq_state.pattern_editor.HandleButtonEvent(event); + return; + } + else if (seq_state.scale_editor.active()) { + seq_state.scale_editor.HandleButtonEvent(event); + return; + } + + if (UI::EVENT_BUTTON_PRESS == event.type) { + switch (event.control) { + case OC::CONTROL_BUTTON_UP: + SEQ_upButton(); + break; + case OC::CONTROL_BUTTON_DOWN: + SEQ_downButton(); + break; + case OC::CONTROL_BUTTON_L: + SEQ_leftButton(); + break; + case OC::CONTROL_BUTTON_R: + SEQ_rightButton(); + break; + } + } +} + +void SEQ_handleEncoderEvent(const UI::Event &event) { + + if (seq_state.pattern_editor.active()) { + seq_state.pattern_editor.HandleEncoderEvent(event); + return; + } + else if (seq_state.scale_editor.active()) { + seq_state.scale_editor.HandleEncoderEvent(event); + return; + } + + if (OC::CONTROL_ENCODER_L == event.control) { + + int selected_channel = seq_state.selected_channel + event.value; + CONSTRAIN(selected_channel, 0, NUM_CHANNELS-1); + seq_state.selected_channel = selected_channel; + + SEQ_Channel &selected = seq_channel[seq_state.selected_channel]; + + selected.update_enabled_settings(seq_state.selected_channel); + seq_state.cursor.Init(SEQ_CHANNEL_SETTING_MODE, 0); + seq_state.cursor.AdjustEnd(selected.num_enabled_settings() - 1); + + } else if (OC::CONTROL_ENCODER_R == event.control) { + + SEQ_Channel &selected = seq_channel[seq_state.selected_channel]; + + if (seq_state.editing()) { + + SEQ_ChannelSetting setting = selected.enabled_setting_at(seq_state.cursor_pos()); + + if (SEQ_CHANNEL_SETTING_SCALE_MASK != setting || SEQ_CHANNEL_SETTING_MASK1 != setting || SEQ_CHANNEL_SETTING_MASK2 != setting || SEQ_CHANNEL_SETTING_MASK3 != setting || SEQ_CHANNEL_SETTING_MASK4 != setting) { + + if (selected.change_value(setting, event.value)) + selected.force_update(); + + switch (setting) { + + case SEQ_CHANNEL_SETTING_SEQUENCE: + { + uint8_t seq = selected.get_sequence(); + uint8_t playmode = selected.get_playmode(); + // details: update mask/sequence, depending on mode. + if (!playmode || playmode >= PM_CV1 || selected.get_current_sequence() == seq || selected.update_timeout()) { + selected.set_display_num_sequence(seq); + selected.pattern_changed(selected.get_mask(seq), true); + } + } + break; + case SEQ_CHANNEL_SETTING_MODE: + case SEQ_CHANNEL_SETTING_SEQUENCE_PLAYMODE: + case SEQ_CHANNEL_SETTING_SEQUENCE_DIRECTION: + selected.update_enabled_settings(seq_state.selected_channel); + seq_state.cursor.AdjustEnd(selected.num_enabled_settings() - 1); + break; + default: + break; + } + } + } else { + seq_state.cursor.Scroll(event.value); + } + } +} + +void SEQ_upButton() { + + SEQ_Channel &selected = seq_channel[seq_state.selected_channel]; + + if (selected.get_menu_page() == PARAMETERS) { + + if (selected.octave_toggle()) + selected.change_value(SEQ_CHANNEL_SETTING_OCTAVE, 1); + else + selected.change_value(SEQ_CHANNEL_SETTING_OCTAVE, -1); + } + else { + selected.set_menu_page(PARAMETERS); + selected.update_enabled_settings(seq_state.selected_channel); + seq_state.cursor.set_editing(false); + } +} + +void SEQ_downButton() { + + SEQ_Channel &selected = seq_channel[seq_state.selected_channel]; + + if (!seq_state.pattern_editor.active() && !seq_state.scale_editor.active()) { + + uint8_t _menu_page = selected.get_menu_page(); + + switch (_menu_page) { + + case PARAMETERS: + _menu_page = CV_MAPPING; + break; + default: + _menu_page = PARAMETERS; + break; + + } + + selected.set_menu_page(_menu_page); + selected.update_enabled_settings(seq_state.selected_channel); + seq_state.cursor.set_editing(false); + } + /* + SEQ_Channel &selected = seq_channel[seq_state.selected_channel]; + if (selected.get_menu_page() == PARAMETERS) + selected.change_value(SEQ_CHANNEL_SETTING_OCTAVE, -1); + else { + selected.set_menu_page(PARAMETERS); + selected.update_enabled_settings(seq_state.selected_channel); + seq_state.cursor.set_editing(false); + } + */ +} + +void SEQ_rightButton() { + + SEQ_Channel &selected = seq_channel[seq_state.selected_channel]; + + switch (selected.enabled_setting_at(seq_state.cursor_pos())) { + + case SEQ_CHANNEL_SETTING_SCALE: + seq_state.cursor.toggle_editing(); + selected.update_scale(true); + break; + case SEQ_CHANNEL_SETTING_SCALE_MASK: + { + int scale = selected.get_scale(DUMMY); + if (OC::Scales::SCALE_NONE != scale) { + seq_state.scale_editor.Edit(&selected, scale); + } + } + break; + case SEQ_CHANNEL_SETTING_MASK1: + case SEQ_CHANNEL_SETTING_MASK2: + case SEQ_CHANNEL_SETTING_MASK3: + case SEQ_CHANNEL_SETTING_MASK4: + { + int pattern = selected.get_sequence(); + if (OC::Patterns::PATTERN_NONE != pattern) { + seq_state.pattern_editor.Edit(&selected, pattern); + } + } + break; + case SEQ_CHANNEL_SETTING_DUMMY: + selected.set_menu_page(PARAMETERS); + selected.update_enabled_settings(seq_state.selected_channel); + break; + default: + seq_state.cursor.toggle_editing(); + break; + } +} + +void SEQ_leftButton() { + // sync: + for (int i = 0; i < NUM_CHANNELS; ++i) + seq_channel[i].sync(); +} + +void SEQ_leftButtonLong() { + // copy scale + if (!seq_state.pattern_editor.active() && !seq_state.scale_editor.active()) { + + uint8_t this_channel, the_other_channel, scale; + uint16_t mask; + + this_channel = seq_state.selected_channel; + scale = seq_channel[this_channel].get_scale(DUMMY); + mask = seq_channel[this_channel].get_rotated_scale_mask(); + + the_other_channel = (~this_channel) & 1u; + seq_channel[the_other_channel].set_scale(scale); + seq_channel[the_other_channel].update_scale_mask(mask, DUMMY); + seq_channel[the_other_channel].update_scale(true); + } +} + +void SEQ_upButtonLong() { + // screensaver short cut (happens elsewhere) +} + +void SEQ_downButtonLong() { + // clear CV mappings: + SEQ_Channel &selected = seq_channel[seq_state.selected_channel]; + + if (selected.get_menu_page() == CV_MAPPING) { + selected.clear_CV_mapping(); + seq_state.cursor.set_editing(false); + } + else { // toggle update behaviour: + seq_channel[0x0].toggle_EoS(); + seq_channel[0x1].toggle_EoS(); + } +} + +void SEQ_menu() { + + menu::DualTitleBar::Draw(); + + for (int i = 0, x = 0; i < NUM_CHANNELS; ++i, x += 21) { + + const SEQ_Channel &channel = seq_channel[i]; + menu::DualTitleBar::SetColumn(i); + + // draw gate/step indicator + uint8_t gate = 1; + if (channel.get_step_gate() == ON) + gate += 14; + else if (channel.get_step_state() == ON) + gate += 10; + menu::DualTitleBar::DrawGateIndicator(i, gate); + + graphics.movePrintPos(5, 0); + // channel id: + graphics.print("#"); + graphics.print((char)('A' + i)); + // sequence id: + graphics.print("/"); + graphics.print(1 + channel.get_display_num_sequence()); + // octave: + graphics.movePrintPos(22, 0); + if (channel.poke_octave_toggle()) + graphics.print("+"); + } + + const SEQ_Channel &channel = seq_channel[seq_state.selected_channel]; + + menu::DualTitleBar::Selected(seq_state.selected_channel); + + menu::SettingsList settings_list(seq_state.cursor); + + menu::SettingsListItem list_item; + + while (settings_list.available()) { + const int setting = + channel.enabled_setting_at(settings_list.Next(list_item)); + const int value = channel.get_value(setting); + const settings::value_attr &attr = SEQ_Channel::value_attr(setting); + + switch (setting) { + + case SEQ_CHANNEL_SETTING_SCALE: + list_item.SetPrintPos(); + if (list_item.editing) { + menu::DrawEditIcon(6, list_item.y, value, attr); + graphics.movePrintPos(6, 0); + } + graphics.print(OC::scale_names[value]); + list_item.DrawCustom(); + break; + case SEQ_CHANNEL_SETTING_SCALE_MASK: + menu::DrawMask(menu::kDisplayWidth, list_item.y, channel.get_rotated_scale_mask(), OC::Scales::GetScale(channel.get_scale(DUMMY)).num_notes); + list_item.DrawNoValue(value, attr); + break; + case SEQ_CHANNEL_SETTING_MASK1: + case SEQ_CHANNEL_SETTING_MASK2: + case SEQ_CHANNEL_SETTING_MASK3: + case SEQ_CHANNEL_SETTING_MASK4: + { + int clock_indicator = channel.get_clock_cnt(); + if (channel.get_playmode() == PM_ARP) + clock_indicator = 0xFF; + menu::DrawMask(menu::kDisplayWidth, list_item.y, channel.get_display_mask(), channel.get_display_length(), clock_indicator); + list_item.DrawNoValue(value, attr); + } + break; + case SEQ_CHANNEL_SETTING_PULSEWIDTH: + if (channel.get_aux_mode() < ENV_AD) { + list_item.Draw_PW_Value(value, attr); + } else { + list_item.Draw_PW_Value_Char(value, attr, "--> sus dur"); + } + break; + case SEQ_CHANNEL_SETTING_DUMMY: + list_item.DrawNoValue(value, attr); + break; + default: + list_item.DrawDefault(value, attr); + break; + } + } + + if (seq_state.pattern_editor.active()) + seq_state.pattern_editor.Draw(); + else if (seq_state.scale_editor.active()) + seq_state.scale_editor.Draw(); +} + + +void SEQ_Channel::RenderScreensaver() const { + + uint8_t seq_id = channel_id_; + uint8_t clock_x_pos = seq_channel[seq_id].get_clock_cnt(); + int32_t _dac_value = seq_channel[seq_id].get_step_pitch(); + int32_t _dac_overflow = 0, _dac_overflow2 = 0; + + // reposition ARP: + if (seq_channel[seq_id].get_playmode() == PM_ARP) + clock_x_pos = 0x6 + (seq_id << 2); + + clock_x_pos = (seq_id << 6) + (clock_x_pos << 2); + + // clock/step indicator: + if(seq_channel[seq_id].step_state_ == OFF) { + graphics.drawRect(clock_x_pos, 63, 5, 2); + _dac_value = 0; + } + else + graphics.drawRect(clock_x_pos, 60, 5, 5); + + // separate windows ... + graphics.drawVLine(64, 0, 68); + + // display pitch values as squares/frames: + if (_dac_value < 0) { + // display negative values as frame (though they're not negative...) + _dac_value = (_dac_value - (_dac_value << 1 )) >> 6; + _dac_overflow = _dac_value - 40; + _dac_overflow2 = _dac_overflow - 40; + + CONSTRAIN(_dac_value, 1, 40); + + int8_t x = 2 + clock_x_pos - (_dac_value >> 1); + int8_t x_size = 0; + + // limit size of frame to window size + if (seq_id && x < 64) { + x_size = 64 - x; + x = 64; + } + else if (!seq_id && (x + _dac_value > 63)) + x_size = (x + _dac_value) - 64; + // draw + graphics.drawFrame(x, 30 - (_dac_value >> 1), _dac_value - x_size, _dac_value); + + if (_dac_overflow > 0) { + + CONSTRAIN(_dac_overflow, 1, 40); + + x = 2 + clock_x_pos - (_dac_overflow >> 1); + + if (seq_id && x < 64) { + x_size = 64 - x; + x = 64; + } + else if (!seq_id && (x + _dac_overflow > 63)) + x_size = (x + _dac_overflow) - 64; + + graphics.drawRect(x, 30 - (_dac_overflow >> 1), _dac_overflow - x_size, _dac_overflow); + } + + if (_dac_overflow2 > 0) { + + CONSTRAIN(_dac_overflow2, 1, 40); + + x = 2 + clock_x_pos - (_dac_overflow2 >> 1); + + if (seq_id && x < 64) { + x_size = 64 - x; + x = 64; + } + else if (!seq_id && (x + _dac_overflow2 > 63)) + x_size = (x + _dac_overflow2) - 64; + + graphics.clearRect(x, 30 - (_dac_overflow2 >> 1), _dac_overflow2 - x_size, _dac_overflow2); + } + } + else { + // positive output as rectangle + _dac_value = (_dac_value >> 6); + _dac_overflow = _dac_value - 40; + _dac_overflow2 = _dac_overflow - 40; + + CONSTRAIN(_dac_value, 1, 40); + + int8_t x = 2 + clock_x_pos - (_dac_value >> 1); + int8_t x_size = 0; + // limit size of rectangle to window size + if (seq_id && x < 64) { + x_size = 64 - x; + x = 64; + } + else if (!seq_id && (x + _dac_value > 63)) + x_size = (x + _dac_value) - 64; + // draw + graphics.drawRect(x, 30 - (_dac_value >> 1), _dac_value - x_size, _dac_value); + + if (_dac_overflow > 0) { + + CONSTRAIN(_dac_overflow, 1, 40); + + x = 2 + clock_x_pos - (_dac_overflow >> 1); + + if (seq_id && x < 64) { + x_size = 64 - x; + x = 64; + } + else if (!seq_id && (x + _dac_overflow > 63)) + x_size = (x + _dac_overflow) - 64; + + graphics.clearRect(x, 30 - (_dac_overflow >> 1), _dac_overflow - x_size, _dac_overflow); + } + + if (_dac_overflow2 > 0) { + + CONSTRAIN(_dac_overflow2, 1, 40); + + x = 2 + clock_x_pos - (_dac_overflow2 >> 1); + + if (seq_id && x < 64) { + x_size = 64 - x; + x = 64; + } + else if (!seq_id && (x + _dac_overflow2 > 63)) + x_size = (x + _dac_overflow2) - 64; + + graphics.drawRect(x, 30 - (_dac_overflow2 >> 1), _dac_overflow2 - x_size, _dac_overflow2); + } + } +} + +void SEQ_screensaver() { + + seq_channel[0].RenderScreensaver(); + seq_channel[1].RenderScreensaver(); +} + + +#endif // ENABLE_APP_SEQUINS diff --git a/software/o_c_REV/APP_SETTINGS.ino b/software/o_c_REV/APP_SETTINGS.ino index 72e750fe2..993dd79b1 100644 --- a/software/o_c_REV/APP_SETTINGS.ino +++ b/software/o_c_REV/APP_SETTINGS.ino @@ -19,44 +19,7 @@ // SOFTWARE. #include "HSApplication.h" - -// Bitmap representation of QR code for access to http://www.beigemaze.com/hs, which -// redirects to Hemisphere Suite documentation. -// -// And no, the QR code doesn't seem to work. I'm sure that the pixels are too small, -// and that the dark spots are too illuminated by adjacent pixels, or whatever. -// But I'm leaving this in because with the right phone and the right display, who -// knows? -// Ah, the heck with it. Commenting it out. -/* -const uint32_t QR[25] = { - 0x1fdeb7f, - 0x1042d41, - 0x174455d, - 0x174f75d, - 0x174ad5d, - 0x105c441, - 0x1fd557f, - 0x8500, - 0x1f6536a, - 0x9cb1b8, - 0x9356cb, - 0x13b29a0, - 0x131cb6d, - 0x1757138, - 0x1d94d5c, - 0x92d5a6, - 0x9f6ef7, - 0x314f00, - 0xb5147f, - 0x1f1ff41, - 0x1bf545d, - 0x19ee55d, - 0x177105d, - 0x12c7741, - 0x1e4dc7f -}; -*/ +#include "OC_strings.h" class Settings : public HSApplication { public: @@ -67,65 +30,52 @@ public: } void Controller() { + #ifdef PEWPEWPEW + HS::frame.Load();PewPewTime.PEWPEW(Clock(3)<<1|Clock(0));} + struct{bool go=0;int idx=0;struct{uint8_t x,y;int x_v,y_v;}pewpews[8]; + void PEWPEW(uint8_t mask){uint32_t t=OC::CORE::ticks;for(int i=0;i<8;++i){auto &p=pewpews[i]; + if(mask>>i&0x01){auto &pp=pewpews[idx++];pp.x=0+120*i;pp.y=55;pp.x_v=(6+random(3))*(i?-1:1);pp.y_v=-9;idx%=8;} + if(t%500==0){p.x+=p.x_v;p.y+=p.y_v;if(p.y>=55&&p.y_v>0)p.y_v=-p.y_v;else ++p.y_v;} + if(t%10000==0){p.x_v=p.x_v*100/101;p.y_v=p.y_v*10/11;}}}}PewPewTime; + void PEWPEW(){for(int i=0;i<8;++i){auto &p=PewPewTime.pewpews[i];gfxIcon(p.x%128,p.y%64,ZAP_ICON);} + #endif } void View() { gfxHeader("Setup / About"); - gfxPrint(0, 15, "Benisphere Suite"); - gfxPrint(0, 25, OC_VERSION); - gfxPrint(0, 35, "github.com/benirose"); - gfxPrint(0, 55, "[CALIBRATE] [RESET]"); -#ifdef BUCHLA_4U - gfxPrint(60, 25, "Buchla"); -#endif - - //DrawQRAt(103, 15); + #if defined(ARDUINO_TEENSY40) + gfxPrint(100, 0, "T4.0"); + //gfxPrint(0, 45, "E2END="); gfxPrint(E2END); + #elif defined(ARDUINO_TEENSY41) + gfxPrint(100, 0, "T4.1"); + #else + gfxPrint(100, 0, "T3.2"); + #endif + + gfxIcon(0, 15, ZAP_ICON); + gfxIcon(120, 15, ZAP_ICON); + #ifdef PEWPEWPEW + gfxPrint(21, 15, "PEW! PEW! PEW!"); + #else + gfxPrint(12, 15, "Phazerville Suite"); + #endif + gfxPrint(0, 25, OC::Strings::VERSION); + gfxPrint(0, 35, "github.com/djphazer"); + gfxPrint(0, 55, "[CALIBRATE] [RESET]"); } ///////////////////////////////////////////////////////////////// // Control handlers ///////////////////////////////////////////////////////////////// - void OnLeftButtonPress() { + void Calibration() { OC::ui.Calibrate(); } - void OnLeftButtonLongPress() { - } - - void OnRightButtonPress() { + void FactoryReset() { OC::apps::Init(1); } - void OnUpButtonPress() { - } - - void OnDownButtonPress() { - } - - void OnDownButtonLongPress() { - } - - void OnLeftEncoderMove(int direction) { - } - - void OnRightEncoderMove(int direction) { - } - -private: -/* - void DrawQRAt(byte x, byte y) { - for (byte c = 0; c < 25; c++) // Column - { - uint32_t col = QR[c]; - for (byte b = 0; b < 25; b++) // Bit - { - if (col & (1 << b)) gfxPixel(x + c, y + b); - } - } - } -*/ - }; Settings Settings_instance; @@ -141,7 +91,11 @@ size_t Settings_save(void *storage) {return 0;} size_t Settings_restore(const void *storage) {return 0;} void Settings_isr() { - return Settings_instance.BaseController(); +#ifdef PEWPEWPEW + Settings_instance.Controller(); +#endif +// skip the Controller to avoid I/O conflict with Calibration + return; } void Settings_handleAppEvent(OC::AppEvent event) { @@ -156,32 +110,27 @@ void Settings_menu() { Settings_instance.BaseView(); } -void Settings_screensaver() {} // Deprecated +void Settings_screensaver() { +#ifdef PEWPEWPEW + Settings_instance.PEWPEW(); +#endif +} void Settings_handleButtonEvent(const UI::Event &event) { - // For left encoder, handle press and long press if (event.control == OC::CONTROL_BUTTON_L) { - if (event.type == UI::EVENT_BUTTON_LONG_PRESS) Settings_instance.OnLeftButtonLongPress(); - else Settings_instance.OnLeftButtonPress(); + if (event.type == UI::EVENT_BUTTON_PRESS) Settings_instance.Calibration(); } - // For right encoder, only handle press (long press is reserved) - if (event.control == OC::CONTROL_BUTTON_R && event.type == UI::EVENT_BUTTON_PRESS) Settings_instance.OnRightButtonPress(); - - // For up button, handle only press (long press is reserved) - if (event.control == OC::CONTROL_BUTTON_UP) Settings_instance.OnUpButtonPress(); - - // For down button, handle press and long press - if (event.control == OC::CONTROL_BUTTON_DOWN) { - if (event.type == UI::EVENT_BUTTON_PRESS) Settings_instance.OnDownButtonPress(); - if (event.type == UI::EVENT_BUTTON_LONG_PRESS) Settings_instance.OnDownButtonLongPress(); - } + if (event.control == OC::CONTROL_BUTTON_R && event.type == UI::EVENT_BUTTON_PRESS) Settings_instance.FactoryReset(); } void Settings_handleEncoderEvent(const UI::Event &event) { + (void)event; + /* // Left encoder turned if (event.control == OC::CONTROL_ENCODER_L) Settings_instance.OnLeftEncoderMove(event.value); // Right encoder turned if (event.control == OC::CONTROL_ENCODER_R) Settings_instance.OnRightEncoderMove(event.value); + */ } diff --git a/software/o_c_REV/APP_THEDARKESTTIMELINE.ino b/software/o_c_REV/APP_THEDARKESTTIMELINE.ino index 81379c838..6af368ada 100644 --- a/software/o_c_REV/APP_THEDARKESTTIMELINE.ino +++ b/software/o_c_REV/APP_THEDARKESTTIMELINE.ino @@ -52,8 +52,6 @@ class TheDarkestTimeline : public HSApplication, public SystemExclusiveHandler, public settings::SettingsBase { public: void Start() { - quantizer.Init(); - quantizer.Configure(OC::Scales::GetScale(5), 0xffff); Resume(); } @@ -67,6 +65,8 @@ public: values_[DT_MIDI_CHANNEL] = 11; values_[DT_MIDI_CHANNEL_ALT] = 12; } + HS::quantizer[0].Init(); + HS::quantizer[0].Configure(OC::Scales::GetScale(scale()), 0xffff); } void Controller() { @@ -81,9 +81,9 @@ public: int data2 = usbMIDI.getData2(); // Handle system exclusive dump for Setup data - if (message == MIDI_MSG_SYSEX) OnReceiveSysEx(); + if (message == HEM_MIDI_SYSEX) OnReceiveSysEx(); - if (message == MIDI_MSG_NOTE_ON && channel == midi_channel_in()) { + if (message == HEM_MIDI_NOTE_ON && channel == midi_channel_in()) { note_on = 1; in_note_number = data1; in_velocity = data2; @@ -156,13 +156,13 @@ public: if (tl == 0) { // This is a CV Timeline, so output the normal universe note - int32_t pitch = quantizer.Process(cv, root() << 7, transpose); + int32_t pitch = HS::quantizer[0].Process(cv, root() << 7, transpose); Out(0, pitch); // and then output the alternate universe note uint8_t alt_idx = (idx + length()) % 32; int alt_cv = get_data_at(alt_idx, tl); - pitch = quantizer.Process(alt_cv, root() << 7, transpose); + pitch = HS::quantizer[0].Process(alt_cv, root() << 7, transpose); Out(1, pitch); } else if (clocked) { // This is the Probability Timeline, and it's only calculated when @@ -352,7 +352,7 @@ public: if (setup_screen == 0) change_value(DT_LENGTH, -direction); else change_value(setup_screen + 1, direction); - quantizer.Configure(OC::Scales::GetScale(scale()), 0xffff); + HS::quantizer[0].Configure(OC::Scales::GetScale(scale()), 0xffff); if (setup_screen > 0) setup_screen_timeout_countdown = DT_SETUP_SCREEN_TIMEOUT; } @@ -370,7 +370,6 @@ private: int8_t cursor; // The play/record point within the sequence bool record[2]; // 0 = CV Timeline, 1 = Proability Timeline bool index_edit_enabled; // The index is being edited via the panel - braids::Quantizer quantizer; uint8_t setup_screen; // Setup screen state int setup_screen_timeout_countdown; bool clocked; // Sequencer has been clocked, and a probability trigger needs to be determined @@ -513,6 +512,7 @@ private: }; // MIDI channels are U8 instead of U4 because the channel number is not zero-indexed; 0 means "off" +// TOTAL EEPROM SIZE: 8 bytes SETTINGS_DECLARE(TheDarkestTimeline, DT_SETTING_LAST) { {16, 1, 32, "Length", NULL, settings::STORAGE_TYPE_U8}, {0, 0, 31, "Index", NULL, settings::STORAGE_TYPE_U8}, @@ -568,14 +568,14 @@ void TheDarkestTimeline_handleButtonEvent(const UI::Event &event) { // For left encoder, handle press and long press if (event.control == OC::CONTROL_BUTTON_L) { if (event.type == UI::EVENT_BUTTON_LONG_PRESS) TheDarkestTimeline_instance.OnLeftButtonLongPress(); - else TheDarkestTimeline_instance.OnLeftButtonPress(); + if (event.type == UI::EVENT_BUTTON_PRESS) TheDarkestTimeline_instance.OnLeftButtonPress(); } // For right encoder, only handle press (long press is reserved) if (event.control == OC::CONTROL_BUTTON_R && event.type == UI::EVENT_BUTTON_PRESS) TheDarkestTimeline_instance.OnRightButtonPress(); // For up button, handle only press (long press is reserved) - if (event.control == OC::CONTROL_BUTTON_UP) TheDarkestTimeline_instance.OnUpButtonPress(); + if (event.control == OC::CONTROL_BUTTON_UP && event.type == UI::EVENT_BUTTON_PRESS) TheDarkestTimeline_instance.OnUpButtonPress(); // For down button, handle press and long press if (event.control == OC::CONTROL_BUTTON_DOWN) { @@ -592,4 +592,4 @@ void TheDarkestTimeline_handleEncoderEvent(const UI::Event &event) { if (event.control == OC::CONTROL_ENCODER_R) TheDarkestTimeline_instance.OnRightEncoderMove(event.value); } -#endif \ No newline at end of file +#endif diff --git a/software/o_c_REV/APP_WAVEFORMEDITOR.ino b/software/o_c_REV/APP_WAVEFORMEDITOR.ino index dc7d12a55..2d7e927e0 100644 --- a/software/o_c_REV/APP_WAVEFORMEDITOR.ino +++ b/software/o_c_REV/APP_WAVEFORMEDITOR.ino @@ -400,14 +400,14 @@ void WaveformEditor_handleButtonEvent(const UI::Event &event) { // For left encoder, handle press and long press if (event.control == OC::CONTROL_BUTTON_L) { if (event.type == UI::EVENT_BUTTON_LONG_PRESS) WaveformEditor_instance.OnLeftButtonLongPress(); - else WaveformEditor_instance.OnLeftButtonPress(); + if (event.type == UI::EVENT_BUTTON_PRESS) WaveformEditor_instance.OnLeftButtonPress(); } // For right encoder, only handle press (long press is reserved) if (event.control == OC::CONTROL_BUTTON_R && event.type == UI::EVENT_BUTTON_PRESS) WaveformEditor_instance.OnRightButtonPress(); // For up button, handle only press (long press is reserved) - if (event.control == OC::CONTROL_BUTTON_UP) WaveformEditor_instance.OnUpButtonPress(); + if (event.control == OC::CONTROL_BUTTON_UP && event.type == UI::EVENT_BUTTON_PRESS) WaveformEditor_instance.OnUpButtonPress(); // For down button, handle press and long press if (event.control == OC::CONTROL_BUTTON_DOWN) { diff --git a/software/o_c_REV/HEM_ADEG.ino b/software/o_c_REV/HEM_ADEG.ino index fe9564ee4..f3ff67742 100644 --- a/software/o_c_REV/HEM_ADEG.ino +++ b/software/o_c_REV/HEM_ADEG.ino @@ -55,8 +55,8 @@ public: //if (signal != target) { // Logarhythm fix 8/2020 int segment = phase == 1 - ? effective_attack + Proportion(DetentedIn(0), HEMISPHERE_MAX_CV, HEM_ADEG_MAX_VALUE) - : effective_decay + Proportion(DetentedIn(1), HEMISPHERE_MAX_CV, HEM_ADEG_MAX_VALUE); + ? effective_attack + Proportion(DetentedIn(0), HEMISPHERE_MAX_INPUT_CV, HEM_ADEG_MAX_VALUE) + : effective_decay + Proportion(DetentedIn(1), HEMISPHERE_MAX_INPUT_CV, HEM_ADEG_MAX_VALUE); segment = constrain(segment, 0, HEM_ADEG_MAX_VALUE); simfloat remaining = target - signal; @@ -88,7 +88,6 @@ public: } void View() { - gfxHeader(applet_name()); DrawIndicator(); } @@ -98,11 +97,11 @@ public: void OnEncoderMove(int direction) { if (cursor == 0) { - attack = constrain(attack += direction, 0, HEM_ADEG_MAX_VALUE); + attack = constrain(attack + direction, 0, HEM_ADEG_MAX_VALUE); last_ms_value = Proportion(attack, HEM_ADEG_MAX_VALUE, HEM_ADEG_MAX_TICKS) / 17; } else { - decay = constrain(decay += direction, 0, HEM_ADEG_MAX_VALUE); + decay = constrain(decay + direction, 0, HEM_ADEG_MAX_VALUE); last_ms_value = Proportion(decay, HEM_ADEG_MAX_VALUE, HEM_ADEG_MAX_TICKS) / 17; } last_change_ticks = OC::CORE::ticks; diff --git a/software/o_c_REV/HEM_ADSREG.ino b/software/o_c_REV/HEM_ADSREG.ino index d99c39864..d5988346d 100644 --- a/software/o_c_REV/HEM_ADSREG.ino +++ b/software/o_c_REV/HEM_ADSREG.ino @@ -18,6 +18,24 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. + +/* + ghostils: + Envelopes are now independent for control and mod source/destination allowing two individual ADSR's with Release MOD CV input per hemisphere. + * CV mod is now limited to release for each channel + * Output Level indicators have been shrunk to make room for additional on screen indicators for which envelope you are editing. + * Switching between envelopes is currently handled by simply pressing the encoder button until you pass the release stage on each envelope which will toggle the active envelope you are editing + * Envelope is indicated by A or B just above the ADSR segments. + * + * TODO: UI Design: + * Update to allow menu to select CV destinations for CV Input Sources on CH1/CH2 + * This could be assignable to a different destination based on probability potentially as well + * Update to allow internal GATE/Trig count to apply a modulation value to any or each of the envelope segments + +*/ + + + #define HEM_EG_ATTACK 0 #define HEM_EG_DECAY 1 #define HEM_EG_SUSTAIN 2 @@ -28,6 +46,12 @@ #define HEM_SUSTAIN_CONST 35 #define HEM_EG_DISPLAY_HEIGHT 30 +//-ghostils: DEFINE Main menu inactivity timeout ~5secs this will return the user to the main menu: +#define HEM_EG_UI_INACT_TICKS 41666 + +//-ghostils: amount of time to handle Double Encoder Press ~250ms. +#define HEM_EG_UI_DBLPRESS_TICKS 4096 + // About four seconds #define HEM_EG_MAX_TICKS_AD 33333 @@ -43,22 +67,37 @@ public: void Start() { edit_stage = 0; - attack = 20; - decay = 30; - sustain = 120; - release = 25; + //attack = 20; + //decay = 30; + //sustain = 120; + //release = 25; ForEachChannel(ch) { stage_ticks[ch] = 0; gated[ch] = 0; stage[ch] = HEM_EG_NO_STAGE; + + //-ghostils:Initialize ADSR channels independently + attack[ch] = 20; + decay[ch] = 30; + sustain[ch] = 120; + release[ch] = 25; + release_mod[ch] = 0; } + + //-ghostils:Multiple ADSR Envelope Tracking: + curEG = 0; + } void Controller() { - // Look for CV modification of attack - attack_mod = get_modification_with_input(0); - release_mod = get_modification_with_input(1); + // Look for CV modification + //attack_mod = get_modification_with_input(0); + //release_mod[0] = get_modification_with_input(1); + + //-ghostils: Update CV1/CV2 to support release only but on each ADSR independently: + release_mod[0] = get_modification_with_input(0); + release_mod[1] = get_modification_with_input(1); ForEachChannel(ch) { @@ -88,64 +127,89 @@ public: gated[ch] = 0; } + Out(ch, GetAmplitudeOf(ch)); } + } void View() { - gfxHeader(applet_name()); DrawIndicator(); DrawADSR(); } void OnButtonPress() { - if (++edit_stage > HEM_EG_RELEASE) {edit_stage = HEM_EG_ATTACK;} + //if (++edit_stage > HEM_EG_RELEASE) {edit_stage = HEM_EG_ATTACK;} + //-ghostils: flip editing focus between A/B ADSR when we hit the end of the Release stage: + if (++edit_stage > HEM_EG_RELEASE) { + edit_stage = HEM_EG_ATTACK; + curEG ^= 1; + } } void OnEncoderMove(int direction) { - int adsr[4] = {attack, decay, sustain, release}; + //-ghostils:Reference curEG as the indexer to current ADSR when editing stages: + int adsr[4] = {attack[curEG], decay[curEG], sustain[curEG], release[curEG]}; adsr[edit_stage] = constrain(adsr[edit_stage] += direction, 1, HEM_EG_MAX_VALUE); - attack = adsr[HEM_EG_ATTACK]; - decay = adsr[HEM_EG_DECAY]; - sustain = adsr[HEM_EG_SUSTAIN]; - release = adsr[HEM_EG_RELEASE]; + attack[curEG] = adsr[HEM_EG_ATTACK]; + decay[curEG] = adsr[HEM_EG_DECAY]; + sustain[curEG] = adsr[HEM_EG_SUSTAIN]; + release[curEG] = adsr[HEM_EG_RELEASE]; } uint64_t OnDataRequest() { + //-ghostils:Update to use an array and snapshot the values using curEG as the index uint64_t data = 0; - Pack(data, PackLocation {0,8}, attack); - Pack(data, PackLocation {8,8}, decay); - Pack(data, PackLocation {16,8}, sustain); - Pack(data, PackLocation {24,8}, release); + Pack(data, PackLocation {0,8}, attack[curEG]); + Pack(data, PackLocation {8,8}, decay[curEG]); + Pack(data, PackLocation {16,8}, sustain[curEG]); + Pack(data, PackLocation {24,8}, release[curEG]); return data; } void OnDataReceive(uint64_t data) { - attack = Unpack(data, PackLocation {0,8}); - decay = Unpack(data, PackLocation {8,8}); - sustain = Unpack(data, PackLocation {16,8}); - release = Unpack(data, PackLocation {24,8}); + //-ghostils:Update to use an array and snapshot the values using curEG as the index + attack[curEG] = Unpack(data, PackLocation {0,8}); + decay[curEG] = Unpack(data, PackLocation {8,8}); + sustain[curEG] = Unpack(data, PackLocation {16,8}); + release[curEG] = Unpack(data, PackLocation {24,8}); - if (attack == 0) Start(); // If empty data, initialize + if (attack[curEG] == 0) Start(); // If empty data, initialize } protected: /* Set help text. Each help section can have up to 18 characters. Be concise! */ void SetHelp() { + /* help[HEMISPHERE_HELP_DIGITALS] = "Gate 1=Ch1 2=Ch2"; help[HEMISPHERE_HELP_CVS] = "Mod 1=Att 2=Rel"; help[HEMISPHERE_HELP_OUTS] = "Amp A=Ch1 B=Ch2"; help[HEMISPHERE_HELP_ENCODER] = "A/D/S/R"; + */ + + //-ghostils:Update onboard help: + help[HEMISPHERE_HELP_DIGITALS] = "Gate 1=Ch1 2=Ch2"; + help[HEMISPHERE_HELP_CVS] = "Mod 1=Rel 2=Rel"; + help[HEMISPHERE_HELP_OUTS] = "Amp A=Ch1 B=Ch2"; + help[HEMISPHERE_HELP_ENCODER] = "A/D/S/R"; } - + private: int edit_stage; - int attack; // Attack rate from 1-255 where 1 is fast - int decay; // Decay rate from 1-255 where 1 is fast - int sustain; // Sustain level from 1-255 where 1 is low - int release; // Release rate from 1-255 where 1 is fast + int attack[2]; // Attack rate from 1-255 where 1 is fast + int decay[2]; // Decay rate from 1-255 where 1 is fast + int sustain[2]; // Sustain level from 1-255 where 1 is low + int release[2]; // Release rate from 1-255 where 1 is fast + + //-ghostils:TODO Modify to adjust independently for each envelope, we won't be able to do both Attack and Release simultaneously so we either have to build a menu or just do Release. + //-Parameterize + int attack_mod; // Modification to attack from CV1 - int release_mod; // Modification to release from CV2 + + int release_mod[2]; // Modification to release from CV2 + + //-ghostils:Additions for tracking multiple ADSR's in each Hemisphere: + int curEG; // Stage management int stage[2]; // The current ASDR stage of the current envelope @@ -161,12 +225,23 @@ private: ForEachChannel(ch) { int w = Proportion(GetAmplitudeOf(ch), HEMISPHERE_MAX_CV, 62); - gfxRect(0, 15 + (ch * 10), w, 6); + //-ghostils:Update to make smaller to allow for additional information on the screen: + //gfxRect(0, 15 + (ch * 10), w, 6); + gfxRect(0, 15 + (ch * 3), w, 2); + } + + //-ghostils:Indicate which ADSR envelope we are selected on: + if(curEG == 0) { + gfxPrint(0,22,"A"); + gfxInvert(0,21,7,9); + }else{ + gfxPrint(0,22,"B"); + gfxInvert(0,21,7,9); } } void DrawADSR() { - int length = attack + decay + release + HEM_SUSTAIN_CONST; // Sustain is constant because it's a level + int length = attack[curEG] + decay[curEG] + release[curEG] + HEM_SUSTAIN_CONST; // Sustain is constant because it's a level int x = 0; x = DrawAttack(x, length); x = DrawDecay(x, length); @@ -175,22 +250,25 @@ private: } int DrawAttack(int x, int length) { - int xA = x + Proportion(attack, length, 62); + //-ghostils:Update to reference curEG: + int xA = x + Proportion(attack[curEG], length, 62); gfxLine(x, BottomAlign(0), xA, BottomAlign(HEM_EG_DISPLAY_HEIGHT), edit_stage != HEM_EG_ATTACK); return xA; } int DrawDecay(int x, int length) { - int xD = x + Proportion(decay, length, 62); + //-ghostils:Update to reference curEG: + int xD = x + Proportion(decay[curEG], length, 62); if (xD < 0) xD = 0; - int yS = Proportion(sustain, HEM_EG_MAX_VALUE, HEM_EG_DISPLAY_HEIGHT); + int yS = Proportion(sustain[curEG], HEM_EG_MAX_VALUE, HEM_EG_DISPLAY_HEIGHT); gfxLine(x, BottomAlign(HEM_EG_DISPLAY_HEIGHT), xD, BottomAlign(yS), edit_stage != HEM_EG_DECAY); return xD; } int DrawSustain(int x, int length) { int xS = x + Proportion(HEM_SUSTAIN_CONST, length, 62); - int yS = Proportion(sustain, HEM_EG_MAX_VALUE, HEM_EG_DISPLAY_HEIGHT); + //-ghostils:Update to reference curEG: + int yS = Proportion(sustain[curEG], HEM_EG_MAX_VALUE, HEM_EG_DISPLAY_HEIGHT); if (yS < 0) yS = 0; if (xS < 0) xS = 0; gfxLine(x, BottomAlign(yS), xS, BottomAlign(yS), edit_stage != HEM_EG_SUSTAIN); @@ -198,14 +276,18 @@ private: } int DrawRelease(int x, int length) { - int xR = x + Proportion(release, length, 62); - int yS = Proportion(sustain, HEM_EG_MAX_VALUE, HEM_EG_DISPLAY_HEIGHT); + //-ghostils:Update to reference curEG: + int xR = x + Proportion(release[curEG], length, 62); + int yS = Proportion(sustain[curEG], HEM_EG_MAX_VALUE, HEM_EG_DISPLAY_HEIGHT); gfxLine(x, BottomAlign(yS), xR, BottomAlign(0), edit_stage != HEM_EG_RELEASE); return xR; } void AttackAmplitude(int ch) { - int effective_attack = constrain(attack + attack_mod, 1, HEM_EG_MAX_VALUE); + //-ghostils:Update to reference current channel: + //-Remove attack_mod CV: + //int effective_attack = constrain(attack[ch] + attack_mod, 1, HEM_EG_MAX_VALUE); + int effective_attack = constrain(attack[ch], 1, HEM_EG_MAX_VALUE); int total_stage_ticks = Proportion(effective_attack, HEM_EG_MAX_VALUE, HEM_EG_MAX_TICKS_AD); int ticks_remaining = total_stage_ticks - stage_ticks[ch]; if (effective_attack == 1) ticks_remaining = 0; @@ -221,14 +303,15 @@ private: } void DecayAmplitude(int ch) { - int total_stage_ticks = Proportion(decay, HEM_EG_MAX_VALUE, HEM_EG_MAX_TICKS_AD); + //-ghostils:Update to reference current channel: + int total_stage_ticks = Proportion(decay[ch], HEM_EG_MAX_VALUE, HEM_EG_MAX_TICKS_AD); int ticks_remaining = total_stage_ticks - stage_ticks[ch]; - simfloat amplitude_remaining = amplitude[ch] - int2simfloat(Proportion(sustain, HEM_EG_MAX_VALUE, HEMISPHERE_MAX_CV)); - if (sustain == 1) ticks_remaining = 0; + simfloat amplitude_remaining = amplitude[ch] - int2simfloat(Proportion(sustain[ch], HEM_EG_MAX_VALUE, HEMISPHERE_MAX_CV)); + if (sustain[ch] == 1) ticks_remaining = 0; if (ticks_remaining <= 0) { // End of decay; move to sustain stage[ch] = HEM_EG_SUSTAIN; stage_ticks[ch] = 0; - amplitude[ch] = int2simfloat(Proportion(sustain, HEM_EG_MAX_VALUE, HEMISPHERE_MAX_CV)); + amplitude[ch] = int2simfloat(Proportion(sustain[ch], HEM_EG_MAX_VALUE, HEMISPHERE_MAX_CV)); } else { simfloat decrease = amplitude_remaining / ticks_remaining; amplitude[ch] -= decrease; @@ -236,11 +319,14 @@ private: } void SustainAmplitude(int ch) { - amplitude[ch] = int2simfloat(Proportion(sustain - 1, HEM_EG_MAX_VALUE, HEMISPHERE_MAX_CV)); + //-ghostils:Update to reference current channel: + amplitude[ch] = int2simfloat(Proportion(sustain[ch] - 1, HEM_EG_MAX_VALUE, HEMISPHERE_MAX_CV)); } void ReleaseAmplitude(int ch) { - int effective_release = constrain(release + release_mod, 1, HEM_EG_MAX_VALUE) - 1; + //-ghostils:Update to reference current channel: + //-CV1 = ADSR A release MOD, CV2 = ADSR A release MOD + int effective_release = constrain(release[ch] + release_mod[ch], 1, HEM_EG_MAX_VALUE) - 1; int total_stage_ticks = Proportion(effective_release, HEM_EG_MAX_VALUE, HEM_EG_MAX_TICKS_R); int ticks_remaining = total_stage_ticks - stage_ticks[ch]; if (effective_release == 0) ticks_remaining = 0; @@ -256,7 +342,7 @@ private: int get_modification_with_input(int in) { int mod = 0; - mod = Proportion(DetentedIn(in), HEMISPHERE_MAX_CV, HEM_EG_MAX_VALUE / 2); + mod = Proportion(DetentedIn(in), HEMISPHERE_MAX_INPUT_CV, HEM_EG_MAX_VALUE / 2); return mod; } }; diff --git a/software/o_c_REV/HEM_ASR.ino b/software/o_c_REV/HEM_ASR.ino index dc41bd059..587cc0e08 100644 --- a/software/o_c_REV/HEM_ASR.ino +++ b/software/o_c_REV/HEM_ASR.ino @@ -31,64 +31,77 @@ public: void Start() { scale = OC::Scales::SCALE_SEMI; - buffer_m->SetIndex(1); - quantizer.Configure(OC::Scales::GetScale(scale), 0xffff); // Semi-tone + buffer_m.SetIndex(1); + ForEachChannel(ch) { + QuantizerConfigure(ch, scale); + } } void Controller() { - buffer_m->Register(hemisphere); + buffer_m.Register(hemisphere); + bool secondary = buffer_m.IsLinked() && hemisphere == RIGHT_HEMISPHERE; + + if (Clock(0) && !secondary) { + StartADCLag(); + } - if (Clock(0)) StartADCLag(); + if (EndOfADCLag()) { + // advance the buffer first, then write the new value + buffer_m.Advance(); - if (EndOfADCLag() || buffer_m->Ready(hemisphere)) { - if (!Gate(1) && !buffer_m->IsLinked(hemisphere)) { + if (!Gate(1)) { int cv = In(0); - buffer_m->WriteValueToBuffer(cv, hemisphere); + buffer_m.WriteValue(cv); } - index_mod = Proportion(DetentedIn(1), HEMISPHERE_MAX_CV, 32); - ForEachChannel(ch) - { - int cv = buffer_m->ReadNextValue(ch, hemisphere, index_mod); - int quantized = quantizer.Process(cv, 0, 0); - Out(ch, quantized); - } - buffer_m->Advance(); + } + + index_mod = Proportion(DetentedIn(1), HEMISPHERE_MAX_INPUT_CV, 32); + ForEachChannel(ch) + { + int cv = buffer_m.ReadValue(ch + secondary*2, index_mod); + int quantized = Quantize(ch, cv, 0, 0); + Out(ch, quantized); } } void View() { - gfxHeader(applet_name()); DrawInterface(); DrawData(); } void OnButtonPress() { - if (++cursor > 1) cursor = 0; + CursorAction(cursor, 1); } void OnEncoderMove(int direction) { + if (!EditMode()) { + MoveCursor(cursor, direction, 1); + return; + } + if (cursor == 0) { // Index - byte ix = buffer_m->GetIndex(); - buffer_m->SetIndex(ix + direction); + uint8_t ix = buffer_m.GetIndex(); + buffer_m.SetIndex(ix + direction); } if (cursor == 1) { // Scale selection scale += direction; if (scale >= OC::Scales::NUM_SCALES) scale = 0; if (scale < 0) scale = OC::Scales::NUM_SCALES - 1; - quantizer.Configure(OC::Scales::GetScale(scale), 0xffff); + ForEachChannel(ch) + QuantizerConfigure(ch, scale); } } uint64_t OnDataRequest() { uint64_t data = 0; - byte ix = buffer_m->GetIndex(); + uint8_t ix = buffer_m.GetIndex(); Pack(data, PackLocation {0,8}, ix); Pack(data, PackLocation {8,8}, scale); return data; } void OnDataReceive(uint64_t data) { - buffer_m->SetIndex(Unpack(data, PackLocation {0,8})); + buffer_m.SetIndex(Unpack(data, PackLocation {0,8})); scale = Unpack(data, PackLocation {8,8}); } @@ -104,17 +117,15 @@ protected: private: int cursor; - RingBufferManager *buffer_m = buffer_m->get(); - braids::Quantizer quantizer; int scale; int index_mod; // Effect of modulation void DrawInterface() { // Show Link icon if linked with another ASR - if (buffer_m->IsLinked(hemisphere)) gfxIcon(56, 1, LINK_ICON); + if (buffer_m.IsLinked() && hemisphere == RIGHT_HEMISPHERE) gfxIcon(56, 1, LINK_ICON); // Index (shared between all instances of ASR) - byte ix = buffer_m->GetIndex() + index_mod; + uint8_t ix = buffer_m.GetIndex() + index_mod; gfxPrint(1, 15, "Index: "); gfxPrint(pad(100, ix), ix); if (index_mod != 0) gfxIcon(54, 26, CV_ICON); @@ -129,9 +140,9 @@ private: } void DrawData() { - for (byte x = 0; x < 64; x++) + for (uint8_t x = 0; x < 64; ++x) { - int y = buffer_m->GetYAt(x, hemisphere) + 40; + int y = buffer_m.GetYAt(x, hemisphere) + 40; gfxPixel(x, y); } } diff --git a/software/o_c_REV/HEM_AnnularFusion.ino b/software/o_c_REV/HEM_AnnularFusion.ino deleted file mode 100644 index dee77a50e..000000000 --- a/software/o_c_REV/HEM_AnnularFusion.ino +++ /dev/null @@ -1,287 +0,0 @@ -// Copyright (c) 2018, Jason Justian -// -// Bjorklund pattern filter, Copyright (c) 2016 Tim Churches -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -#include "bjorklund.h" -#define AF_DISPLAY_TIMEOUT 330000 - -struct AFStepCoord { - uint8_t x; - uint8_t y; -}; - -class AnnularFusion : public HemisphereApplet { -public: - - const char* applet_name() { - return "AnnularFu"; - } - - void Start() { - display_timeout = AF_DISPLAY_TIMEOUT; - ForEachChannel(ch) - { - length[ch] = 16; - beats[ch] = 4 + (ch * 4); - pattern[ch] = EuclideanPattern(length[ch] - 1, beats[ch], 0);; - } - step = 0; - SetDisplayPositions(0, 24); - SetDisplayPositions(1, 16); - last_clock = OC::CORE::ticks; - } - - void Controller() { - if (Clock(1)) step = 0; // Reset - - // Advance both rings - if (Clock(0)) { - last_clock = OC::CORE::ticks; - ForEachChannel(ch) - { - int rotation = Proportion(DetentedIn(ch), HEMISPHERE_MAX_CV, length[ch]); - - // Store the pattern for display - pattern[ch] = EuclideanPattern(length[ch] - 1, beats[ch], rotation); - int sb = step % length[ch]; - if ((pattern[ch] >> sb) & 0x01) { - ClockOut(ch); - } - } - - // Plan for the thing to run forever and ever - if (++step >= length[0] * length[1]) step = 0; - } - if (display_timeout > 0) --display_timeout; - } - - void View() { - gfxHeader(applet_name()); - DrawSteps(); - if (display_timeout > 0) DrawEditor(); - } - - void OnButtonPress() { - display_timeout = AF_DISPLAY_TIMEOUT; - if (++cursor > 3) cursor = 0; - ResetCursor(); - } - - void OnEncoderMove(int direction) { - display_timeout = AF_DISPLAY_TIMEOUT; - int ch = cursor < 2 ? 0 : 1; - int f = cursor - (ch * 2); // Cursor function - if (f == 0) { - length[ch] = constrain(length[ch] + direction, 3, 32); - if (beats[ch] > length[ch]) beats[ch] = length[ch]; - SetDisplayPositions(ch, 24 - (8 * ch)); - } - if (f == 1) { - beats[ch] = constrain(beats[ch] + direction, 1, length[ch]); - } - } - - uint64_t OnDataRequest() { - uint64_t data = 0; - Pack(data, PackLocation {0,4}, length[0] - 1); - Pack(data, PackLocation {4,4}, beats[0] - 1); - Pack(data, PackLocation {8,4}, length[1] - 1); - Pack(data, PackLocation {12,4}, beats[1] - 1); - return data; - } - - void OnDataReceive(uint64_t data) { - length[0] = Unpack(data, PackLocation {0,4}) + 1; - beats[0] = Unpack(data, PackLocation {4,4}) + 1; - length[1] = Unpack(data, PackLocation {8,4}) + 1; - beats[1] = Unpack(data, PackLocation {12,4}) + 1; - SetDisplayPositions(0, 24); - SetDisplayPositions(1, 16); - } - -protected: - void SetHelp() { - // "------------------" <-- Size Guide - help[HEMISPHERE_HELP_DIGITALS] = "1=Clock 2=Reset"; - help[HEMISPHERE_HELP_CVS] = "Rotate 1=Ch1 2=Ch2"; - help[HEMISPHERE_HELP_OUTS] = "Clock A=Ch1 B=Ch2"; - help[HEMISPHERE_HELP_ENCODER] = "Length/Hits Ch1,2"; - // "------------------" <-- Size Guide - } - -private: - int step; - int cursor = 0; // Ch1: 0=Length, 1=Hits; Ch2: 2=Length 3=Hits - AFStepCoord disp_coord[2][32]; - uint32_t pattern[2]; - int last_clock; - uint32_t display_timeout; - - // Settings - int length[2]; - int beats[2]; - - void DrawSteps() { - ForEachChannel(ch) - { - DrawActiveSegment(ch); - DrawPatternPoints(ch); - } - } - - void DrawActiveSegment(int ch) { - if (last_clock && OC::CORE::ticks - last_clock < 166666) { - int s1 = step % length[ch]; - int s2 = s1 + 1 == length[ch] ? 0 : s1 + 1; - - AFStepCoord s1_c = disp_coord[ch][s1]; - AFStepCoord s2_c = disp_coord[ch][s2]; - gfxLine(s1_c.x, s1_c.y, s2_c.x, s2_c.y); - } - } - - void DrawPatternPoints(int ch) { - for (int p = 0; p < length[ch]; p++) - { - if ((pattern[ch] >> p) & 0x01) { - gfxPixel(disp_coord[ch][p].x, disp_coord[ch][p].y); - gfxPixel(disp_coord[ch][p].x + 1, disp_coord[ch][p].y); - } - } - } - - void DrawEditor() { - int ch = cursor < 2 ? 0 : 1; // Cursor channel - int f = cursor - (ch * 2); // Cursor function - - // Length cursor - gfxBitmap(1, 15, 8, LOOP_ICON); - gfxPrint(12 + pad(10, length[ch]), 15, length[ch]); - if (f == 0) gfxCursor(13, 23, 12); - - // Beats cursor - gfxBitmap(1, 25, 8, X_NOTE_ICON); - gfxPrint(12 + pad(10, beats[ch]), 25, beats[ch]); - if (f == 1) gfxCursor(13, 33, 12); - - // Ring indicator - gfxCircle(8, 52, 8); - gfxCircle(8, 52, 4); - - if (ch == 0) gfxCircle(8, 52, 7); - else gfxCircle(8, 52, 5); - } - - /* Get coordinates of circle in two halves, from the top and from the bottom */ - void SetDisplayPositions(int ch, int r) { - int cx = 31; // Center coordinates - int cy = 39; - int di = 0; // Display index (positions actually used in the display) - int c_count = 0; // Count of pixels along the circumference - int x_per_step = (r * 4) / 32; - uint32_t pattern = EuclideanPattern(31, length[ch], 0); - - // Sweep across the top of the circle looking for positions within the - // radius of the circle. Left to right: - for (uint8_t x = 0; x < 63; x++) - { - // Top down - for (uint8_t y = 0; y < 63; y++) - { - int rx = cx - x; // Positions relative to center - int ry = cy - y; - - // Is this point within the radius? - if (rx * rx + ry * ry < r * r + 1) { - if (c_count++ % x_per_step == 0) { - if (pattern & 0x01) disp_coord[ch][di++] = AFStepCoord {x, y}; - pattern = pattern >> 0x01; - } - break; // Only use the first point - } - } - } - - // Sweep across the top of the circle looking for positions within the - // radius of the circle. Right to left: - for (uint8_t x = 63; x > 0; x--) - { - // Bottom up - for (uint8_t y = 63; y > 0; y--) - { - int rx = cx - x; // Positions relative to center - int ry = cy - y; - - // Is this point within the radius? - if (rx * rx + ry * ry < r * r + 1) { - if (c_count++ % x_per_step == 0) { - if (pattern & 0x01) disp_coord[ch][di++] = AFStepCoord {x, y}; - pattern = pattern >> 0x01; - } - break; // Only use the first point - } - } - } - } -}; - - -//////////////////////////////////////////////////////////////////////////////// -//// Hemisphere Applet Functions -/// -/// Once you run the find-and-replace to make these refer to AnnularFusion, -/// it's usually not necessary to do anything with these functions. You -/// should prefer to handle things in the HemisphereApplet child class -/// above. -//////////////////////////////////////////////////////////////////////////////// -AnnularFusion AnnularFusion_instance[2]; - -void AnnularFusion_Start(bool hemisphere) { - AnnularFusion_instance[hemisphere].BaseStart(hemisphere); -} - -void AnnularFusion_Controller(bool hemisphere, bool forwarding) { - AnnularFusion_instance[hemisphere].BaseController(forwarding); -} - -void AnnularFusion_View(bool hemisphere) { - AnnularFusion_instance[hemisphere].BaseView(); -} - -void AnnularFusion_OnButtonPress(bool hemisphere) { - AnnularFusion_instance[hemisphere].OnButtonPress(); -} - -void AnnularFusion_OnEncoderMove(bool hemisphere, int direction) { - AnnularFusion_instance[hemisphere].OnEncoderMove(direction); -} - -void AnnularFusion_ToggleHelpScreen(bool hemisphere) { - AnnularFusion_instance[hemisphere].HelpScreen(); -} - -uint64_t AnnularFusion_OnDataRequest(bool hemisphere) { - return AnnularFusion_instance[hemisphere].OnDataRequest(); -} - -void AnnularFusion_OnDataReceive(bool hemisphere, uint64_t data) { - AnnularFusion_instance[hemisphere].OnDataReceive(data); -} diff --git a/software/o_c_REV/HEM_AttenuateOffset.ino b/software/o_c_REV/HEM_AttenuateOffset.ino index 0922d3e4d..115c18324 100644 --- a/software/o_c_REV/HEM_AttenuateOffset.ino +++ b/software/o_c_REV/HEM_AttenuateOffset.ino @@ -19,6 +19,7 @@ // SOFTWARE. #define ATTENOFF_INCREMENTS 128 +#define ATTENOFF_MAX_LEVEL 63 class AttenuateOffset : public HemisphereApplet { public: @@ -28,16 +29,16 @@ public: } void Start() { - ForEachChannel(ch) level[ch] = 63; + ForEachChannel(ch) level[ch] = ATTENOFF_MAX_LEVEL; } void Controller() { - mix_final = mix || Gate(0); + mix_final = mix || Gate(1); int prevSignal = 0; ForEachChannel(ch) { - int signal = Proportion(level[ch], 63, In(ch)) + (offset[ch] * ATTENOFF_INCREMENTS); + int signal = Proportion(level[ch], ATTENOFF_MAX_LEVEL, In(ch)) + (offset[ch] * ATTENOFF_INCREMENTS); if (ch == 1 && mix_final) { signal = signal + prevSignal; } @@ -45,36 +46,41 @@ public: // use the unconstrained signal for mixing prevSignal = signal; - signal = constrain(signal, -HEMISPHERE_3V_CV, HEMISPHERE_MAX_CV); + signal = constrain(signal, -HEMISPHERE_MAX_CV, HEMISPHERE_MAX_CV); Out(ch, signal); } } void View() { - gfxHeader(applet_name()); DrawInterface(); } void OnButtonPress() { - if (++cursor > 4) cursor = 0; - ResetCursor(); + if (cursor == 4 && !EditMode()) // special case when modal editing + mix = !mix; + else + CursorAction(cursor, 4); } void OnEncoderMove(int direction) { - if (cursor == 4) { - mix = (direction > 0); - } else - { - uint8_t ch = cursor / 2; - if (cursor == 0 || cursor == 2) { - // Change offset voltage - int min = -HEMISPHERE_MAX_CV / ATTENOFF_INCREMENTS; - int max = HEMISPHERE_MAX_CV / ATTENOFF_INCREMENTS; - offset[ch] = constrain(offset[ch] + direction, min, max); - } else { - // Change level percentage - level[ch] = constrain(level[ch] + direction, -63, 63); - } + if (!EditMode()) { + MoveCursor(cursor, direction, 4); + return; + } + if (cursor == 4) { // non-modal editing special case toggle + mix = !mix; + return; + } + + uint8_t ch = cursor / 2; + if (cursor == 0 || cursor == 2) { + // Change offset voltage + int min = -HEMISPHERE_MAX_CV / ATTENOFF_INCREMENTS; + int max = HEMISPHERE_MAX_CV / ATTENOFF_INCREMENTS; + offset[ch] = constrain(offset[ch] + direction, min, max); + } else { + // Change level percentage (+/-200%) + level[ch] = constrain(level[ch] + direction, -ATTENOFF_MAX_LEVEL*2, ATTENOFF_MAX_LEVEL*2); } } @@ -82,24 +88,24 @@ public: uint64_t data = 0; Pack(data, PackLocation {0,9}, offset[0] + 256); Pack(data, PackLocation {10,9}, offset[1] + 256); - Pack(data, PackLocation {19,7}, level[0] + 64); - Pack(data, PackLocation {26,7}, level[1] + 64); - Pack(data, PackLocation {34,1}, mix); + Pack(data, PackLocation {19,8}, level[0] + ATTENOFF_MAX_LEVEL*2); + Pack(data, PackLocation {27,8}, level[1] + ATTENOFF_MAX_LEVEL*2); + Pack(data, PackLocation {35,1}, mix); return data; } void OnDataReceive(uint64_t data) { offset[0] = Unpack(data, PackLocation {0,9}) - 256; offset[1] = Unpack(data, PackLocation {10,9}) - 256; - level[0] = Unpack(data, PackLocation {19,7}) - 64; - level[1] = Unpack(data, PackLocation {26,7}) - 64; - mix = Unpack(data, PackLocation {34,1}); + level[0] = Unpack(data, PackLocation {19,8}) - ATTENOFF_MAX_LEVEL*2; + level[1] = Unpack(data, PackLocation {27,8}) - ATTENOFF_MAX_LEVEL*2; + mix = Unpack(data, PackLocation {35,1}); } protected: void SetHelp() { // "------------------" <-- Size Guide - help[HEMISPHERE_HELP_DIGITALS] = "1=Mix A&B"; + help[HEMISPHERE_HELP_DIGITALS] = "2=Mix A&B"; help[HEMISPHERE_HELP_CVS] = "CV Inputs 1,2"; help[HEMISPHERE_HELP_OUTS] = "Outputs A,B"; help[HEMISPHERE_HELP_ENCODER] = "Offset V / Level %"; @@ -124,18 +130,15 @@ private: } if (mix_final) { - gfxLine(3, 24, 3, 31); - gfxIcon(0, 25, DOWN_BTN_ICON); + gfxIcon(1, 25, DOWN_ICON); } if (cursor == 4) { if (CursorBlink()) { gfxFrame(0, 24, 9, 10); } - } else{ - int ch = cursor / 2; - if (cursor == 0 or cursor == 2) gfxCursor(13, 23 + (ch * 20), 36); - else gfxCursor(13, 33 + (ch * 20), 36); + } else { + gfxCursor(12, 23 + cursor * 10, 37); } } diff --git a/software/o_c_REV/HEM_Binary.ino b/software/o_c_REV/HEM_Binary.ino index 9f85b1f1f..3de38f919 100644 --- a/software/o_c_REV/HEM_Binary.ino +++ b/software/o_c_REV/HEM_Binary.ino @@ -51,7 +51,6 @@ public: } void View() { - gfxHeader(applet_name()); DrawDisplay(); } diff --git a/software/o_c_REV/HEM_BootsNCat.ino.disabled b/software/o_c_REV/HEM_BootsNCat.ino similarity index 94% rename from software/o_c_REV/HEM_BootsNCat.ino.disabled rename to software/o_c_REV/HEM_BootsNCat.ino index 44462c815..d75284473 100644 --- a/software/o_c_REV/HEM_BootsNCat.ino.disabled +++ b/software/o_c_REV/HEM_BootsNCat.ino @@ -95,15 +95,19 @@ public: } void View() { - gfxHeader(applet_name()); DrawInterface(); } void OnButtonPress() { - if (++cursor > 4) cursor = 0; + CursorAction(cursor, 4); } void OnEncoderMove(int direction) { + if (!EditMode()) { + MoveCursor(cursor, direction, 4); + return; + } + if (cursor == 4) { // Blend blend = constrain(blend + direction, 0, BNC_MAX_PARAM); } else { @@ -121,11 +125,10 @@ public: SetEGFreq(ch); } } - ResetCursor(); } - uint32_t OnDataRequest() { - uint32_t data = 0; + uint64_t OnDataRequest() { + uint64_t data = 0; Pack(data, PackLocation {0,6}, tone[0]); Pack(data, PackLocation {6,6}, decay[0]); Pack(data, PackLocation {12,6}, tone[1]); @@ -134,7 +137,7 @@ public: return data; } - void OnDataReceive(uint32_t data) { + void OnDataReceive(uint64_t data) { tone[0] = Unpack(data, PackLocation {0,6}); decay[0] = Unpack(data, PackLocation {6,6}); tone[1] = Unpack(data, PackLocation {12,6}); @@ -191,6 +194,7 @@ private: byte p = is_cursor ? 1 : 3; gfxDottedLine(x, y + 4, 62, y + 4, p); gfxRect(x + w, y, 2, 7); + if (EditMode() && is_cursor) gfxInvert(x, y, 18, 7); } void SetBDFreq() { @@ -219,5 +223,5 @@ void BootsNCat_View(bool hemisphere) {BootsNCat_instance[hemisphere].BaseView(); void BootsNCat_OnButtonPress(bool hemisphere) {BootsNCat_instance[hemisphere].OnButtonPress();} void BootsNCat_OnEncoderMove(bool hemisphere, int direction) {BootsNCat_instance[hemisphere].OnEncoderMove(direction);} void BootsNCat_ToggleHelpScreen(bool hemisphere) {BootsNCat_instance[hemisphere].HelpScreen();} -uint32_t BootsNCat_OnDataRequest(bool hemisphere) {return BootsNCat_instance[hemisphere].OnDataRequest();} -void BootsNCat_OnDataReceive(bool hemisphere, uint32_t data) {BootsNCat_instance[hemisphere].OnDataReceive(data);} +uint64_t BootsNCat_OnDataRequest(bool hemisphere) {return BootsNCat_instance[hemisphere].OnDataRequest();} +void BootsNCat_OnDataReceive(bool hemisphere, uint64_t data) {BootsNCat_instance[hemisphere].OnDataReceive(data);} diff --git a/software/o_c_REV/HEM_Brancher.ino b/software/o_c_REV/HEM_Brancher.ino index a6dedfbad..a1303774a 100644 --- a/software/o_c_REV/HEM_Brancher.ino +++ b/software/o_c_REV/HEM_Brancher.ino @@ -26,39 +26,42 @@ public: } void Start() { - p = 50; - choice = 0; + p = 50; + choice = 0; } void Controller() { - bool master_clock = MasterClockForwarded(); + p_mod = p; + Modulate(p_mod, 0, 0, 100); + // handles physical and logical clock if (Clock(0)) { - int prob = p + Proportion(DetentedIn(0), HEMISPHERE_MAX_CV, 100); - choice = (random(1, 100) <= prob) ? 0 : 1; + choice = (random(1, 100) <= p_mod) ? 0 : 1; - // If Master Clock Forwarding is enabled, respond to this clock by - // sending a clock - if (master_clock) ClockOut(choice); + // will be true only for logical clocks + clocked = !Gate(0); + + if (clocked) ClockOut(choice); } - // Pass along the gate state if Master Clock Forwarding is off. If it's on, - // the clock is handled above - if (!master_clock) GateOut(choice, Gate(0)); + // only pass thru physical gates + if (!clocked) { + GateOut(choice, Gate(0)); + GateOut(1 - choice, 0); // reset the other output + } } void View() { - gfxHeader("Brancher"); DrawInterface(); } void OnButtonPress() { - choice = 1 - choice; + choice = 1 - choice; } /* Change the pability */ void OnEncoderMove(int direction) { - p = constrain(p += direction, 0, 100); + p = constrain(p + direction, 0, 100); } uint64_t OnDataRequest() { @@ -80,15 +83,17 @@ protected: } private: - int p; + int p, p_mod; int choice; + bool clocked; // indicates a logical clock without a physical gate void DrawInterface() { // Show the probability in the middle gfxPrint(1, 15, "p="); - gfxPrint(15 + pad(100, p), 15, p); - gfxPrint(33, 15, hemisphere ? "% C" : "% A"); + gfxPrint(15 + pad(100, p_mod), 15, p_mod); + gfxPrint(33, 15, hemisphere ? "% C" : "% A"); gfxCursor(15, 23, 18); + if (p != p_mod) gfxIcon(39, 12, CV_ICON); gfxPrint(12, 45, hemisphere ? "C" : "A"); gfxPrint(44, 45, hemisphere ? "D" : "B"); diff --git a/software/o_c_REV/HEM_BugCrack.ino b/software/o_c_REV/HEM_BugCrack.ino index 7e92c6949..9f739bb73 100644 --- a/software/o_c_REV/HEM_BugCrack.ino +++ b/software/o_c_REV/HEM_BugCrack.ino @@ -97,8 +97,8 @@ public: int32_t bd_signal = 0; int32_t sd_signal = 0; int32_t ns_signal = 0; - cv_kick = Proportion(DetentedIn(CH_KICK), HEMISPHERE_MAX_CV, BNC_MAX_PARAM); - cv_snare = Proportion(DetentedIn(CH_SNARE), HEMISPHERE_MAX_CV, BNC_MAX_PARAM); + cv_kick = Proportion(DetentedIn(CH_KICK), HEMISPHERE_MAX_INPUT_CV, BNC_MAX_PARAM); + cv_snare = Proportion(DetentedIn(CH_SNARE), HEMISPHERE_MAX_INPUT_CV, BNC_MAX_PARAM); // Kick drum if (cv_mode_kick == CV_MODE_TONE) { @@ -121,7 +121,7 @@ public: } else { _decay_punch = decay_punch; } - if (Clock(CH_KICK, 1)) { + if (Clock(CH_KICK, 0)) { SetEnvDecayKick(_decay_kick); SetEnvDecayPunch(_decay_punch); env_kick.Start(); @@ -170,7 +170,7 @@ public: } else { _blend_snare = blend_snare; } - if (Clock(CH_SNARE, 1)) { + if (Clock(CH_SNARE, 0)) { SetEnvDecaySnare(_decay_snare); SetEnvDecaySnap(_decay_snare); env_snare.Start(); @@ -219,50 +219,55 @@ public: } void View() { - gfxHeader(applet_name()); DrawInterface(); } void OnButtonPress() { - if (++cursor > 9) cursor = 0; + CursorAction(cursor, 9); } void OnEncoderMove(int direction) { + if (!EditMode()) { + MoveCursor(cursor, direction, 9); + return; + } + + switch (cursor) { // Kick drum - if (cursor == 0) { + case 0: tone_kick = constrain(tone_kick + direction, 0, BNC_MAX_PARAM); - } - if (cursor == 1) { + break; + case 1: decay_kick = constrain(decay_kick + direction, 0, BNC_MAX_PARAM); - } - if (cursor == 2) { + break; + case 2: punch = constrain(punch + direction, 0, BNC_MAX_PARAM); - } - if (cursor == 3) { + break; + case 3: decay_punch = constrain(decay_punch + direction, 0, BNC_MAX_PARAM); - } - if (cursor == 4) { + break; + case 4: cv_mode_kick = constrain(cv_mode_kick + direction, 0, 4); - } + break; // Snare drum - if (cursor == 5) { + case 5: tone_snare = constrain(tone_snare + direction, 0, BNC_MAX_PARAM); - } - if (cursor == 6) { + break; + case 6: decay_snare = constrain(decay_snare + direction, 0, BNC_MAX_PARAM); - } - if (cursor == 7) { + break; + case 7: snap = constrain(snap + direction, 0, BNC_MAX_PARAM); - } - if (cursor == 8) { + break; + case 8: blend_snare = constrain(blend_snare + direction, 0, BNC_MAX_PARAM); - } - if (cursor == 9) { + break; + case 9: cv_mode_snare = constrain(cv_mode_snare + direction, 0, 4); + break; } - ResetCursor(); } uint64_t OnDataRequest() { @@ -358,41 +363,40 @@ private: DrawDrumBody(1, _tone_kick, _decay_kick, _punch, _decay_punch, 0); DrawDrumBody(32, _tone_snare, _decay_snare, _snap, _blend_snare, 1); - // CV modes - gfxIcon(1, 57, CV_ICON); - gfxPrint(10, 55, CV_MODE_NAMES_BD[cv_mode_kick]); - gfxIcon(32, 57, CV_ICON); - gfxPrint(41, 55, CV_MODE_NAMES_SN[cv_mode_snare]); - switch (cursor) { // Kick drum case 0: - gfxPrint(1, 45, Proportion(_tone_kick, BNC_MAX_PARAM, 30) + 30); - gfxIcon(22, 44, HERTZ_ICON); + gfxPrint(1, 55, Proportion(_tone_kick, BNC_MAX_PARAM, 30) + 30); + gfxIcon(22, 54, HERTZ_ICON); break; case 1: - gfxPrint(1, 45, "decay"); break; + gfxPrint(1, 55, "decay"); break; case 2: - gfxPrint(1, 45, "punch"); break; + gfxPrint(1, 55, "punch"); break; case 3: - gfxPrint(1, 45, "drop"); break; - case 4: - gfxInvert(1, 54, 30, 9); break; + gfxPrint(1, 55, "drop"); break; + case 4: // CV modes + gfxIcon(1, 57, CV_ICON); + gfxPrint(10, 55, CV_MODE_NAMES_BD[cv_mode_kick]); + break; // Snare drum case 5: - gfxPrint(35, 45, Proportion(_tone_snare, BNC_MAX_PARAM, 600) + 100); - gfxIcon(54, 44, HERTZ_ICON); + gfxPrint(35, 55, Proportion(_tone_snare, BNC_MAX_PARAM, 600) + 100); + gfxIcon(54, 54, HERTZ_ICON); break; case 6: - gfxPrint(32, 45, "decay"); break; + gfxPrint(32, 55, "decay"); break; case 7: - gfxPrint(32, 45, "snap"); break; + gfxPrint(32, 55, "snap"); break; case 8: - gfxPrint(32, 45, "blend"); break; - case 9: - gfxInvert(32, 54, 30, 9); break; + gfxPrint(32, 55, "blend"); break; + case 9: // CV modes + gfxIcon(32, 57, CV_ICON); + gfxPrint(41, 55, CV_MODE_NAMES_SN[cv_mode_snare]); + break; } + if (EditMode()) gfxInvert(1 + (cursor<5?0:31), 54, 31, 9); // Level indicators ForEachChannel(ch) diff --git a/software/o_c_REV/HEM_Burst.ino b/software/o_c_REV/HEM_Burst.ino index 3746d0148..df425f54f 100644 --- a/software/o_c_REV/HEM_Burst.ino +++ b/software/o_c_REV/HEM_Burst.ino @@ -27,6 +27,9 @@ class Burst : public HemisphereApplet { public: + enum BurstCursor { + + }; const char* applet_name() { return "Burst"; @@ -51,7 +54,7 @@ public: number = constrain(number, 1, HEM_BURST_NUMBER_MAX); last_number_cv_tick = OC::CORE::ticks; } - int spacing_mod = clocked ? 0 : Proportion(DetentedIn(1), HEMISPHERE_MAX_CV, 500); + int spacing_mod = clocked ? 0 : Proportion(DetentedIn(1), HEMISPHERE_MAX_INPUT_CV, 500); // Get timing information if (Clock(0)) { @@ -116,37 +119,42 @@ public: } void View() { - gfxHeader(applet_name()); DrawSelector(); DrawIndicator(); } void OnButtonPress() { - cursor += 1; - if (cursor > 4) cursor = 0; - if (cursor > 3 && !clocked) cursor = 0; + CursorAction(cursor, clocked ? 4 : 3); } void OnEncoderMove(int direction) { - if (cursor == 0) number = constrain(number += direction, 1, HEM_BURST_NUMBER_MAX); - if (cursor == 1) { - spacing = constrain(spacing += direction, HEM_BURST_SPACING_MIN, HEM_BURST_SPACING_MAX); - clocked = 0; - } - if (cursor == 2) { - accel = constrain(accel += direction, -HEM_BURST_ACCEL_MAX, HEM_BURST_ACCEL_MAX); + if (!EditMode()) { + MoveCursor(cursor, direction, clocked ? 4 : 3); + return; } - if (cursor == 3) { - jitter = constrain(jitter += direction, 0, HEM_BURST_JITTER_MAX); - } - - if (cursor == 4) { + switch (cursor) { + case 0: + number = constrain(number + direction, 1, HEM_BURST_NUMBER_MAX); + break; + case 1: + spacing = constrain(spacing + direction, HEM_BURST_SPACING_MIN, HEM_BURST_SPACING_MAX); + clocked = 0; + break; + case 2: + accel = constrain(accel + direction, -HEM_BURST_ACCEL_MAX, HEM_BURST_ACCEL_MAX); + break; + case 3: + jitter = constrain(jitter + direction, 0, HEM_BURST_JITTER_MAX); + break; + + case 4: div += direction; if (div > HEM_BURST_CLOCKDIV_MAX) div = HEM_BURST_CLOCKDIV_MAX; if (div < -HEM_BURST_CLOCKDIV_MAX) div = -HEM_BURST_CLOCKDIV_MAX; if (div == 0) div = direction > 0 ? 1 : -2; // No such thing as 1/1 Multiple if (div == -1) div = 1; // Must be moving up to hit -1 (see previous line) + break; } } @@ -199,7 +207,7 @@ private: void DrawSelector() { // Number gfxPrint(1, 15, number); - gfxPrint(28, 15, "bursts"); + gfxPrint(27, 15, "bursts"); // Spacing gfxPrint(1, 25, clocked ? get_effective_spacing() : spacing); @@ -210,8 +218,8 @@ private: gfxPrint(10, 35, accel); // Jitter - gfxIcon(30, 34, RANDOM_ICON); - gfxPrint(38, 35, jitter); + gfxIcon(32, 34, RANDOM_ICON); + gfxPrint(40, 35, jitter); // Div if (clocked) { @@ -222,9 +230,9 @@ private: } // Cursor - if (cursor < 2) gfxCursor(1, 23 + (cursor * 10), 62); - if (cursor == 2) gfxCursor(10, 43, 12); - if (cursor == 3) gfxCursor(38, 43, 12); + if (cursor < 2) gfxCursor(1, 23 + (cursor * 10), 13 + 12*cursor); + if (cursor == 2) gfxCursor(10, 43, 19); + if (cursor == 3) gfxCursor(40, 43, 13); if (cursor == 4) gfxCursor(1, 53, 62); } diff --git a/software/o_c_REV/HEM_Button.ino b/software/o_c_REV/HEM_Button.ino index e36a0ca15..9e4d4784f 100644 --- a/software/o_c_REV/HEM_Button.ino +++ b/software/o_c_REV/HEM_Button.ino @@ -22,38 +22,42 @@ class Button : public HemisphereApplet { public: const char* applet_name() { // Maximum 10 characters - return "Button"; + return "Button2"; } /* Run when the Applet is selected */ void Start() { - toggle_st = 0; // Set toggle state to off - trigger_out = 0; // Set trigger out queue to off trigger_countdown = 0; } /* Run during the interrupt service routine, 16667 times per second */ void Controller() { - if (Clock(0, 1)) OnButtonPress(); // Clock at Dig 0 emulates press (ignore forwarding) - if (trigger_out) { - ClockOut(0); - trigger_out = 0; // Clear trigger queue - trigger_countdown = 1667; // Trigger display countdown + ForEachChannel(ch) { + // Check physical trigger input to emulate button press (ignore forwarding) + if (Clock(ch, 1)) PressButton(ch); + + // Handle output if triggered + if (trigger_out[ch]) { + if (gate_mode[ch]) + GateOut(ch, toggle_st[ch]); + else + ClockOut(ch); + + trigger_out[ch] = 0; // Clear trigger queue + } } - GateOut(1, toggle_st); // Send toggle state if (trigger_countdown) trigger_countdown--; } /* Draw the screen */ void View() { - gfxHeader(applet_name()); DrawIndicator(); } /* Called when the encoder button for this hemisphere is pressed */ void OnButtonPress() { - trigger_out = 1; // Set trigger queue - toggle_st = 1 - toggle_st; // Alternate toggle state when pressed + PressButton(channel); + trigger_countdown = 1667; // Trigger display countdown } /* Called when the encoder for this hemisphere is rotated @@ -61,7 +65,15 @@ public: * direction -1 is counterclockwise */ void OnEncoderMove(int direction) { - /* Nothing for this applet */ + if (direction > 0) // CW toggles which channel + channel = 1 - channel; + else { // CCW toggles between Trig/Gate output + // retrigger if switching from a Gate On to Trig mode + trigger_out[channel] = gate_mode[channel] && toggle_st[channel]; + + gate_mode[channel] = 1 - gate_mode[channel]; + toggle_st[channel] = 0; // reset Gate to off + } } /* No state data for this applet @@ -81,23 +93,44 @@ protected: /* Set help text. Each help section can have up to 18 characters. Be concise! */ void SetHelp() { // "------------------" <-- Size Guide - help[HEMISPHERE_HELP_DIGITALS] = "1=Press Button"; + help[HEMISPHERE_HELP_DIGITALS] = "1,2=Press Button"; help[HEMISPHERE_HELP_CVS] = ""; - help[HEMISPHERE_HELP_OUTS] = "A=Trg B=Toggle"; - help[HEMISPHERE_HELP_ENCODER] = "P=Trg/Toggle"; + help[HEMISPHERE_HELP_OUTS] = "A,B=Trig/Gate Out"; + help[HEMISPHERE_HELP_ENCODER] = "T=Config P=Button"; // "------------------" <-- Size Guide } private: - bool trigger_out; // Trigger output queue (output A/C) - bool toggle_st; // Toggle state (output B/D) + bool trigger_out[2] = {0,0}; // Trigger output queue (output A/C) + bool toggle_st[2] = {0,0}; // Toggle state (output B/D) int trigger_countdown; // For momentary display of trigger output + bool channel = 0; // selected channel, 0=(output A/C) 1=(output B/D) + bool gate_mode[2] = {0,1}; // mode flag for each channel, 0=trig, 1=gate toggle void DrawIndicator() { - if (trigger_countdown) gfxFrame(9, 42, 13, 13); - gfxBitmap(12, 45, 8, CLOCK_ICON); - gfxBitmap(44, 45, 8, toggle_st ? CLOSED_ICON : OPEN_ICON); + // Guide text + gfxIcon(1, 15, ROTATE_R_ICON); + gfxPrint(10, 15, "Channel"); + gfxIcon(1, 24, ROTATE_L_ICON); + gfxPrint(10, 24, "Mode"); + + if (trigger_countdown) gfxFrame(9 + 32*channel, 42, 13, 13); // momentary manual trigger indicator + + gfxIcon(12 + channel*32, 35, DOWN_BTN_ICON); // channel selector + + ForEachChannel(ch) { + if (!gate_mode[ch]) + gfxIcon(12 + ch*32, 45, CLOCK_ICON); + else + gfxIcon(12 + ch*32, 45, toggle_st[ch] ? CLOSED_ICON : OPEN_ICON); + } + } + + void PressButton(bool ch = 0) + { + toggle_st[ch] = 1 - toggle_st[ch]; // Alternate toggle state when pressed + trigger_out[ch] = 1; // Set trigger queue } }; diff --git a/software/o_c_REV/HEM_CVRecV2.ino b/software/o_c_REV/HEM_CVRecV2.ino index 20f16a9b5..21e70f40f 100644 --- a/software/o_c_REV/HEM_CVRecV2.ino +++ b/software/o_c_REV/HEM_CVRecV2.ino @@ -37,16 +37,28 @@ public: } void Controller() { - bool reset = Clock(1); + if (Clock(1)) reset = true; + if (reset) { + step = start; + if (punch_out) punch_out = end - start + 1; // inclusive + } - if (Clock(0) || reset) { - step++; - if (step > end || step < start) step = start; - if (reset) { - step = start; - if (punch_out) punch_out = end - start; + // check for deferred recording + if (EndOfADCLag() && punch_out && mode) { + ForEachChannel(ch) + { + if (mode & (0x01 << ch)) { // Record this channel + cv[ch][step] = In(ch); + } + } + if (!reset) { + if (--punch_out == 0) mode = 0; } - bool rec = 0; + } + + if (Clock(0) ) { // sequence advance + if (!reset) step++; + if (step > end || step < start) step = start; ForEachChannel(ch) { signal[ch] = int2simfloat(cv[ch][step]); @@ -54,17 +66,11 @@ public: if (next_step > end) next_step = start; if (smooth) rise[ch] = (int2simfloat(cv[ch][next_step]) - int2simfloat(cv[ch][step])) / ClockCycleTicks(0); else rise[ch] = 0; - - if (mode & (0x01 << ch)) { // Record this channel - if (punch_out > 0) { - rec = 1; - cv[ch][step] = In(ch); - } - } - } - if (rec) { - if (--punch_out == 0) mode = 0; } + + // defer recording + StartADCLag(); + reset = false; } ForEachChannel(ch) @@ -79,34 +85,50 @@ public: } void View() { - gfxHeader(applet_name()); DrawInterface(); } void OnButtonPress() { - if (cursor == 3) { - // Check recording status - if (mode > 0) punch_out = end - start; - else punch_out = 0; + if (cursor == 2 && !EditMode()) { // special case to toggle smoothing + smooth = 1 - smooth; + ResetCursor(); + return; } - if (++cursor > 3) cursor = 0; - ResetCursor(); + + if (cursor == 3 && EditMode()) { // activate recording if selected + punch_out = (mode > 0) ? end - start + 1 : 0; + } + + CursorAction(cursor, 3); } void OnEncoderMove(int direction) { - if (cursor == 0) { + if (!EditMode()) { //not editing, move cursor + MoveCursor(cursor, direction, 3); + return; + } + + switch (cursor) { + case 0: { int16_t fs = start; // Former start value start = constrain(start + direction, 0, end - 1); if (fs != start && punch_out) punch_out -= direction; + break; } - if (cursor == 1) { + case 1: { int16_t fe = end; // Former end value end = constrain(end + direction, start + 1, CVREC_MAX_STEP - 1); if (fe != end && punch_out) punch_out += direction; + break; + } + case 2: + smooth = 1 - smooth; + ResetCursor(); + break; + case 3: + mode = constrain(mode + direction, 0, 3); + break; } - if (cursor == 2) smooth = direction > 0 ? 1 : 0; - if (cursor == 3) mode = constrain(mode + direction, 0, 3); - ResetCursor(); } uint64_t OnDataRequest() { @@ -141,6 +163,7 @@ private: simfloat rise[2]; simfloat signal[2]; bool smooth; + bool reset = true; // Transport int mode = 0; // 0=Playback, 1=Rec Track 1, 2=Rec Track 2, 3= Rec Tracks 1 & 2 @@ -163,11 +186,6 @@ private: // Record Mode gfxPrint(1, 35, CVRecV2_MODES[mode]); - // Cursor - if (cursor == 0) gfxCursor(19, 23, 18); - if (cursor == 1) gfxCursor(43, 23, 18); - if (cursor == 3) gfxCursor(1, 43, 63); - // Status icon if (mode > 0 && punch_out > 0) { if (!CursorBlink()) gfxIcon(54, 35, RECORD_ICON); @@ -177,6 +195,13 @@ private: // Record time indicator if (punch_out > 0) gfxInvert(0, 34, punch_out / 6, 9); + // Cursor + switch(cursor){ + case 0: gfxCursor(19, 23, 18); break; + case 1: gfxCursor(43, 23, 18); break; + case 3: gfxCursor(1, 43, 63); break; + } + // Step indicator segment.PrintWhole(hemisphere * 64, 50, step + 1, 100); diff --git a/software/o_c_REV/HEM_Calculate.ino b/software/o_c_REV/HEM_Calculate.ino index 46bb1824a..2d491b409 100644 --- a/software/o_c_REV/HEM_Calculate.ino +++ b/software/o_c_REV/HEM_Calculate.ino @@ -22,7 +22,7 @@ #define HEMISPHERE_NUMBER_OF_CALC 7 int hem_MIN(int v1, int v2) {return (v1 < v2) ? v1 : v2;} int hem_MAX(int v1, int v2) {return (v1 > v2) ? v1 : v2;} -int hem_SUM(int v1, int v2) {return constrain(v1 + v2, -HEMISPHERE_3V_CV, HEMISPHERE_MAX_CV);} +int hem_SUM(int v1, int v2) {return constrain(v1 + v2, -HEMISPHERE_MAX_CV, HEMISPHERE_MAX_CV);} int hem_DIFF(int v1, int v2) {return hem_MAX(v1, v2) - hem_MIN(v1, v2);} int hem_MEAN(int v1, int v2) {return (v1 + v2) / 2;} typedef int(*CalcFunction)(int, int); @@ -41,16 +41,6 @@ public: operation[ch] = ch; rand_clocked[ch] = 0; } - const char * op_name_list[] = {"Min", "Max", "Sum", "Diff", "Mean", "S&H", "Rnd"}; - // hem_MIN goes in the Rand and S&H slots, because those are handled in Controller() and - // don't need functions. But providing 0 isn't safe because the encoder change can - // happen any time and cause the system to try to run one of those null functions. - CalcFunction calc_fn_list[] = {hem_MIN, hem_MAX, hem_SUM, hem_DIFF, hem_MEAN, hem_MIN, hem_MIN}; - for(int i = 0; i < HEMISPHERE_NUMBER_OF_CALC; i++) - { - op_name[i] = op_name_list[i]; - calc_fn[i] = calc_fn_list[i]; - } } void Controller() { @@ -61,14 +51,18 @@ public: if (idx == 5) { // S&H if (Clock(ch)) Out(ch, In(ch)); } else if (idx == 6) { // Rand - // The first time a clock comes in, Rand becomes clocked, freezing a random - // value with each clock pulse. Otherwise, Rand is unclocked, and outputs - // a random value with each tick. - if (Clock(ch)) { - Out(ch, random(0, HEMISPHERE_MAX_CV)); + // The first time a clock comes in, Rand becomes clocked. + // Otherwise, Rand is unclocked, and outputs a random value with each tick. + + bool clk = Clock(ch); + bool recalc = clk || !rand_clocked[ch]; + if (clk) rand_clocked[ch] = 1; - } - else if (!rand_clocked[ch]) Out(ch, random(0, HEMISPHERE_MAX_CV)); + else if (ch == 1 && rand_clocked[0] && !rand_clocked[1]) // normalled clock input from TR1 for channel 2 + recalc = Clock(0); + + if (recalc) + Out(ch, random(0, HEMISPHERE_MAX_CV)); } else if (idx < 5) { int result = calc_fn[idx](In(0), In(1)); Out(ch, result); @@ -77,18 +71,20 @@ public: } void View() { - gfxHeader(applet_name()); DrawSelector(); gfxSkyline(); } void OnButtonPress() { - selected = 1 - selected; - ResetCursor(); + CursorAction(selected, 1); } void OnEncoderMove(int direction) { - operation[selected] = constrain(operation[selected] += direction, 0, HEMISPHERE_NUMBER_OF_CALC - 1); + if (!EditMode()) { + MoveCursor(selected, direction, 1); + return; + } + operation[selected] = constrain(operation[selected] + direction, 0, HEMISPHERE_NUMBER_OF_CALC - 1); rand_clocked[selected] = 0; } @@ -114,8 +110,11 @@ protected: } private: - const char* op_name[HEMISPHERE_NUMBER_OF_CALC]; - CalcFunction calc_fn[HEMISPHERE_NUMBER_OF_CALC]; + const char* op_name[HEMISPHERE_NUMBER_OF_CALC] = {"Min", "Max", "Sum", "Diff", "Mean", "S&H", "Rnd"}; + // hem_MIN goes in the Rand and S&H slots, because those are handled in Controller() and + // don't need functions. But providing 0 isn't safe because the encoder change can + // happen any time and cause the system to try to run one of those null functions. + CalcFunction calc_fn[HEMISPHERE_NUMBER_OF_CALC] = {hem_MIN, hem_MAX, hem_SUM, hem_DIFF, hem_MEAN, hem_MIN, hem_MIN}; int hold[2]; int operation[2]; int selected; @@ -126,10 +125,10 @@ private: ForEachChannel(ch) { gfxPrint(31 * ch, 15, op_name[operation[ch]]); - if (ch == selected) gfxCursor(0 + (31 * ch), 23, 30); - // Show the icon if this random calculator is clocked if (operation[ch] == 6 && rand_clocked[ch]) gfxIcon(20 + 31 * ch, 15, CLOCK_ICON); + + if (ch == selected) gfxCursor(0 + (31 * ch), 23, 30); } } }; diff --git a/software/o_c_REV/HEM_Carpeggio.ino b/software/o_c_REV/HEM_Carpeggio.ino index f5cba191a..affe41960 100644 --- a/software/o_c_REV/HEM_Carpeggio.ino +++ b/software/o_c_REV/HEM_Carpeggio.ino @@ -27,6 +27,14 @@ class Carpeggio : public HemisphereApplet { public: + enum CarpeggioCursor { + CHORD, + TRANSPOSE, + SHUFFLE, + NOTE_EDIT, + LAST_SETTING = NOTE_EDIT + }; + const char* applet_name() { return "Carpeggio"; } @@ -73,7 +81,7 @@ public: } // Modulation output - int xy = (In(0) * In(1)) / HEMISPHERE_MAX_CV; + int xy = (In(0) * In(1)) / HEMISPHERE_MAX_INPUT_CV; Out(1, xy); // Handle imprint confirmation animation @@ -84,34 +92,51 @@ public: } void View() { - gfxHeader(applet_name()); DrawSelector(); DrawGrid(); } void OnButtonPress() { + if (cursor == SHUFFLE && !EditMode()) // special case toggle + shuffle ? ImprintChord(sel_chord) : ShuffleChord(); + else // Advance or toggle cursor + CursorAction(cursor, LAST_SETTING); + // Set a chord imprint if a new chord is picked - if (cursor == 1 && chord != sel_chord) { - cursor = 0; // Don't advance cursor when chord is changed + if (chord != sel_chord) { ImprintChord(chord); + cursor = CHORD; // keep cursor on chord selection } - if (++cursor > 3) cursor = 0; - ResetCursor(); } void OnEncoderMove(int direction) { - if (cursor == 0) sequence[step] = constrain(sequence[step] += direction, -24, 60); - if (cursor == 1) chord = constrain(chord += direction, 0, Nr_of_arp_chords - 1); - if (cursor == 2) transpose = constrain(transpose += direction, -24, 24); - if (cursor == 3) { + if (!EditMode()) { + MoveCursor(cursor, direction, LAST_SETTING); + return; + } + + switch ((CarpeggioCursor)cursor) { + case NOTE_EDIT: + sequence[step] = constrain(sequence[step] + direction, -24, 60); + break; + case CHORD: + chord = constrain(chord + direction, 0, Nr_of_arp_chords - 1); + break; + case TRANSPOSE: + transpose = constrain(transpose + direction, -24, 24); + break; + + case SHUFFLE: if (shuffle && direction < 0) { ImprintChord(sel_chord); } if (direction > 0) { ShuffleChord(); } + break; } - if (cursor != 1) replay = 1; + + if (cursor != CHORD) replay = 1; } uint64_t OnDataRequest() { @@ -137,7 +162,7 @@ protected: } private: - int cursor; // 0=notes, 1=chord + int cursor; // CarpeggioCursor // Sequencer state uint8_t step; // Current step number @@ -158,7 +183,7 @@ private: void DrawSelector() { // Chord selector gfxPrint(0, 15, Arp_Chords[chord].chord_name); - if (cursor == 1) { + if (cursor == CHORD) { gfxCursor(1, 23, 53); gfxBitmap(55, 14, 8, chord == sel_chord ? CHECK_ON_ICON : CHECK_OFF_ICON); } @@ -167,18 +192,18 @@ private: gfxPrint(32, 25, "Tr"); gfxPrint(transpose < 0 ? "" : "+"); gfxPrint(transpose); - if (cursor == 2) gfxCursor(32, 33, 30); + if (cursor == TRANSPOSE) gfxCursor(32, 33, 30); // Shuffle selector gfxBitmap(37, 36, 8, PLAY_ICON); gfxBitmap(49, 36, 8, LOOP_ICON); gfxInvert(36 + (shuffle ? 12 : 0), 35, 10, 10); - if (cursor == 3) gfxCursor(37, 46, 20); + if (cursor == SHUFFLE) gfxCursor(36, 46, 22, 11); // Note name editor uint8_t midi_note = constrain(sequence[step] + 36 + transpose, 0, 127); gfxPrint(38, 50, midi_note_numbers[midi_note]); - if (cursor == 0) gfxCursor(32, 58, 30); + if (cursor == NOTE_EDIT) gfxCursor(32, 58, 30); } void DrawGrid() { diff --git a/software/o_c_REV/HEM_Chordinator.ino b/software/o_c_REV/HEM_Chordinator.ino new file mode 100644 index 000000000..2cd556570 --- /dev/null +++ b/software/o_c_REV/HEM_Chordinator.ino @@ -0,0 +1,231 @@ +// Copyright (c) 2021, Bryan Head +// +// Based on Braids Quantizer, Copyright 2015 Émilie Gillet. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#include "braids_quantizer.h" +#include "bjorklund.h" + +class Chordinator : public HemisphereApplet { +public: + const char *applet_name() { return "Chordnate"; } + + void Start() { + scale = 5; + continuous[0] = 1; + continuous[1] = 1; + set_scale(scale); + update_chord_quantizer(); + } + + void Controller() { + ForEachChannel(ch) { + if (Clock(ch)) { + continuous[ch] = 0; + StartADCLag(ch); + } + } + + if (continuous[0] || EndOfADCLag(0)) { + chord_root_raw = In(0); + int32_t new_root_pitch = + Quantize(0, chord_root_raw, root << 7, 0); + if (new_root_pitch != chord_root_pitch) { + update_chord_quantizer(); + chord_root_pitch = new_root_pitch; + } + Out(0, chord_root_pitch); + } + + if (continuous[1] || EndOfADCLag(1)) { + harm_pitch = + Quantize(1, In(1) + chord_root_pitch, root << 7, 0); + Out(1, harm_pitch); + } + } + + void View() { + gfxPrint(0, 15, OC::scale_names_short[scale]); + if (cursor == 0) gfxCursor(0, 23, 30); + + gfxPrint(36, 15, OC::Strings::note_names_unpadded[root]); + if (cursor == 1) gfxCursor(36, 23, 12); + + uint16_t mask = chord_mask; + for (int i = 0; i < int(active_scale.num_notes); i++) { + int y = 7*(i / 12); // longer scales spill over + int x = 5*(i % 12); + if (mask & 1) { + gfxRect(x, 25 + y, 4, 4); + } else { + gfxFrame(x, 25 + y, 4, 4); + } + if (cursor - 2 == i) { + gfxCursor(x, 30 + y, 4); + } + + mask >>= 1; + } + + size_t root_ix = note_ix(chord_root_pitch); + gfxBitmap(5 * root_ix, 40, 8, NOTE4_ICON); + gfxBitmap(5 * note_ix(harm_pitch), 50, 8, NOTE4_ICON); + } + + void OnButtonPress() { + if (cursor < 2) { + isEditing = !isEditing; + } else { + chord_mask ^= 1 << (cursor - 2); + update_chord_quantizer(); + } + } + + void OnEncoderMove(int direction) { + if (!isEditing) { + MoveCursor(cursor, direction, 1 + int(active_scale.num_notes)); + return; + } + + switch (cursor) { + case 0: + set_scale(scale + direction); + break; + case 1: + root = constrain(root + direction, 0, 11); + break; + default: + // shouldn't happen... + break; + } + update_chord_quantizer(); + } + + uint64_t OnDataRequest() { + uint64_t data = 0; + Pack(data, PackLocation{0, 8}, scale); + Pack(data, PackLocation{8, 4}, root); + Pack(data, PackLocation{12, 16}, chord_mask); + return data; + } + + void OnDataReceive(uint64_t data) { + set_scale(Unpack(data, PackLocation{0, 8})); + root = Unpack(data, PackLocation{8, 4}); + chord_mask = Unpack(data, PackLocation{12, 16}); + update_chord_quantizer(); + } + +protected: + void SetHelp() { + help[HEMISPHERE_HELP_DIGITALS] = "Sample, Lrn/Sync"; + help[HEMISPHERE_HELP_CVS] = "Weight, Signal"; + help[HEMISPHERE_HELP_OUTS] = "Sampled out, Thru"; + help[HEMISPHERE_HELP_ENCODER] = "Scl,root,wght,lrn"; + } + +private: + int scale; // SEMI + int16_t root; + bool continuous[2]; + braids::Scale active_scale; + + int cursor = 0; + + // Leftmost is root, second to left is 2, etc. Defaulting here to basic triad. + uint16_t chord_mask = 0b10101; + + int16_t chord_root_raw = 0; + int16_t chord_root_pitch = 0; + + int16_t harm_pitch = 0; + + void update_chord_quantizer() { + size_t num_notes = active_scale.num_notes; + chord_root_pitch = Quantize(0, chord_root_raw, root, 0); + size_t chord_root = note_ix(chord_root_pitch); + uint16_t mask = rotl32(chord_mask, num_notes, chord_root); + QuantizerConfigure(1, scale, mask); + } + + size_t note_ix(int pitch) { + int rel_pitch = pitch % active_scale.span; + int d = active_scale.span; + size_t p = 0; + for (int i = 0; i < (int) active_scale.num_notes; i++) { + int e = abs(rel_pitch - active_scale.notes[i]); + if (e < d) { + p = i; + d = e; + } + } + return p; + } + + void set_scale(int value) { + if (value < 0) scale = OC::Scales::NUM_SCALES - 1; + else if (value >= OC::Scales::NUM_SCALES) scale = 0; + else scale = value; + active_scale = OC::Scales::GetScale(scale); + QuantizerConfigure(0, scale); + } +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Hemisphere Applet Functions +/// +/// Once you run the find-and-replace to make these refer to Chordinator, +/// it's usually not necessary to do anything with these functions. You +/// should prefer to handle things in the HemisphereApplet child class +/// above. +//////////////////////////////////////////////////////////////////////////////// +Chordinator Chordinator_instance[2]; + +void Chordinator_Start(bool hemisphere) { + Chordinator_instance[hemisphere].BaseStart(hemisphere); +} + +void Chordinator_Controller(bool hemisphere, bool forwarding) { + Chordinator_instance[hemisphere].BaseController(forwarding); +} + +void Chordinator_View(bool hemisphere) { + Chordinator_instance[hemisphere].BaseView(); +} + +void Chordinator_OnButtonPress(bool hemisphere) { + Chordinator_instance[hemisphere].OnButtonPress(); +} + +void Chordinator_OnEncoderMove(bool hemisphere, int direction) { + Chordinator_instance[hemisphere].OnEncoderMove(direction); +} + +void Chordinator_ToggleHelpScreen(bool hemisphere) { + Chordinator_instance[hemisphere].HelpScreen(); +} + +uint64_t Chordinator_OnDataRequest(bool hemisphere) { + return Chordinator_instance[hemisphere].OnDataRequest(); +} + +void Chordinator_OnDataReceive(bool hemisphere, uint64_t data) { + Chordinator_instance[hemisphere].OnDataReceive(data); +} diff --git a/software/o_c_REV/HEM_ClockDivider.ino b/software/o_c_REV/HEM_ClockDivider.ino index 060fef88e..0de4669fc 100644 --- a/software/o_c_REV/HEM_ClockDivider.ino +++ b/software/o_c_REV/HEM_ClockDivider.ino @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -#define HEM_CLOCKDIV_MAX 8 +#define HEM_CLOCKDIV_MAX 24 class ClockDivider : public HemisphereApplet { public: @@ -27,16 +27,7 @@ public: return "Clock Div"; } - void Start() { - ForEachChannel(ch) - { - div[ch] = ch + 1; - count[ch] = 0; - next_clock[ch] = 0; - } - cycle_time = 0; - cursor = 0; - } + void Start() { } void Controller() { int this_tick = OC::CORE::ticks; @@ -44,9 +35,9 @@ public: // Set division via CV ForEachChannel(ch) { - int input = DetentedIn(ch) - HEMISPHERE_CENTER_CV; + int input = DetentedIn(ch); if (input) { - div[ch] = Proportion(input, HEMISPHERE_MAX_CV / 2, HEM_CLOCKDIV_MAX); + div[ch] = Proportion(input, HEMISPHERE_MAX_INPUT_CV / 2, HEM_CLOCKDIV_MAX); div[ch] = constrain(div[ch], -HEM_CLOCKDIV_MAX, HEM_CLOCKDIV_MAX); if (div[ch] == 0 || div[ch] == -1) div[ch] = 1; } @@ -64,10 +55,8 @@ public: { count[ch]++; if (div[ch] > 0) { // Positive value indicates clock division - if (count[ch] >= div[ch]) { - count[ch] = 0; // Reset - ClockOut(ch); - } + if (count[ch] == 1) ClockOut(ch); // fire on first step + if (count[ch] >= div[ch]) count[ch] = 0; // Reset on last step } else { // Calculate next clock for multiplication on each clock int clock_every = (cycle_time / -div[ch]); @@ -91,16 +80,19 @@ public: } void View() { - gfxHeader(applet_name()); DrawSelector(); } void OnButtonPress() { - cursor = 1 - cursor; - ResetCursor(); + CursorAction(cursor, 1); } void OnEncoderMove(int direction) { + if (!EditMode()) { + MoveCursor(cursor, direction, 1); + return; + } + div[cursor] += direction; if (div[cursor] > HEM_CLOCKDIV_MAX) div[cursor] = HEM_CLOCKDIV_MAX; if (div[cursor] < -HEM_CLOCKDIV_MAX) div[cursor] = -HEM_CLOCKDIV_MAX; @@ -130,17 +122,16 @@ protected: } private: - int div[2]; // Division data for outputs. Positive numbers are divisions, negative numbers are multipliers - int count[2]; // Number of clocks since last output (for clock divide) - int next_clock[2]; // Tick number for the next output (for clock multiply) - int cursor; // Which output is currently being edited - int cycle_time; // Cycle time between the last two clock inputs + int div[2] = {1, 2}; // Division data for outputs. Positive numbers are divisions, negative numbers are multipliers + int count[2] = {0,0}; // Number of clocks since last output (for clock divide) + int next_clock[2] = {0,0}; // Tick number for the next output (for clock multiply) + int cursor = 0; // Which output is currently being edited + int cycle_time = 0; // Cycle time between the last two clock inputs void DrawSelector() { ForEachChannel(ch) { int y = 15 + (ch * 25); - if (ch == cursor) gfxCursor(0, y + 8, 63); if (div[ch] > 0) { gfxPrint(1, y, "/"); @@ -153,6 +144,7 @@ private: gfxPrint(" Mult"); } } + gfxCursor(0, 23 + (cursor * 25), 63); } }; diff --git a/software/o_c_REV/HEM_ClockSetup.ino b/software/o_c_REV/HEM_ClockSetup.ino index 44fc2ef5a..868879323 100644 --- a/software/o_c_REV/HEM_ClockSetup.ino +++ b/software/o_c_REV/HEM_ClockSetup.ino @@ -21,63 +21,201 @@ class ClockSetup : public HemisphereApplet { public: + enum ClockSetupCursor { + PLAY_STOP, + TEMPO, + SHUFFLE, + EXT_PPQN, + MULT1, + MULT2, + MULT3, + MULT4, + TRIG1, + TRIG2, + TRIG3, + TRIG4, + BOOP1, + BOOP2, + BOOP3, + BOOP4, + LAST_SETTING = BOOP4 + }; + const char* applet_name() { return "ClockSet"; } void Start() { } - // When the ClockSetup is active, the selected applets should continue to function, so - // there's no need to have a controller for ClockSetup. - void Controller() { } + // The ClockSetup controller handles MIDI Clock and Transport Start/Stop + void Controller() { + bool clock_sync = OC::DigitalInputs::clocked(); + + // MIDI Clock is filtered to 2 PPQN + if (frame.MIDIState.clock_q) { + frame.MIDIState.clock_q = 0; + clock_sync = 1; + } + if (frame.MIDIState.start_q) { + frame.MIDIState.start_q = 0; + clock_m->DisableMIDIOut(); + clock_m->Start(); + } + if (frame.MIDIState.stop_q) { + frame.MIDIState.stop_q = 0; + clock_m->Stop(); + clock_m->EnableMIDIOut(); + } + + // Paused means wait for clock-sync to start + if (clock_m->IsPaused() && clock_sync) + clock_m->Start(); + // TODO: automatically stop... + + // Advance internal clock, sync to external clock / reset + if (clock_m->IsRunning()) + clock_m->SyncTrig( clock_sync ); + + // ------------ // + if (clock_m->IsRunning() && clock_m->MIDITock()) usbMIDI.sendRealTime(usbMIDI.Clock); + + // 4 internal clock flashers + for (int i = 0; i < 4; ++i) { + if (clock_m->Tock(i)) + flash_ticker[i] = HEMISPHERE_PULSE_ANIMATION_TIME; + else if (flash_ticker[i]) + --flash_ticker[i]; + } + + if (button_ticker) --button_ticker; + } void View() { DrawInterface(); } void OnButtonPress() { - if (++cursor > 2) cursor = 0; - } + if (!EditMode()) { // special cases for toggle buttons + if (cursor == PLAY_STOP) PlayStop(); + else if (cursor >= BOOP1) { + clock_m->Boop(cursor-BOOP1); + button_ticker = HEMISPHERE_PULSE_ANIMATION_TIME_LONG; + } + else CursorAction(cursor, LAST_SETTING); + } + else CursorAction(cursor, LAST_SETTING); - void OnEncoderMove(int direction) { - if (cursor == 0) { // Source - if (clock_m->IsRunning() || clock_m->IsPaused()) clock_m->Stop(); - else { - clock_m->Start(); - clock_m->Pause(); + if (cursor == TEMPO) { + // Tap Tempo detection + if (last_tap_tick) { + tap_time[taps] = OC::CORE::ticks - last_tap_tick; + + if (tap_time[taps] > CLOCK_TICKS_MAX) { + taps = 0; + last_tap_tick = 0; + } + else if (++taps == NR_OF_TAPS) + clock_m->SetTempoFromTaps(tap_time, taps); + + taps %= NR_OF_TAPS; } + last_tap_tick = OC::CORE::ticks; } + } - if (cursor == 1) { // Set tempo - uint16_t bpm = clock_m->GetTempo(); - bpm += direction; - clock_m->SetTempoBPM(bpm); + void OnEncoderMove(int direction) { + taps = 0; + last_tap_tick = 0; + if (!EditMode()) { + MoveCursor(cursor, direction, LAST_SETTING); + return; } - if (cursor == 2) { // Set multiplier - int8_t mult = clock_m->GetMultiply(); - mult += direction; - clock_m->SetMultiply(mult); + switch ((ClockSetupCursor)cursor) { + case PLAY_STOP: + PlayStop(); + break; + + case TRIG1: + case TRIG2: + case TRIG3: + case TRIG4: + HS::trigger_mapping[cursor-TRIG1] = constrain( HS::trigger_mapping[cursor-TRIG1] + direction, 0, 4); + break; + + case BOOP1: + case BOOP2: + case BOOP3: + case BOOP4: + clock_m->Boop(cursor-BOOP1); + button_ticker = HEMISPHERE_PULSE_ANIMATION_TIME_LONG; + break; + + case EXT_PPQN: + clock_m->SetClockPPQN(clock_m->GetClockPPQN() + direction); + break; + case TEMPO: + clock_m->SetTempoBPM(clock_m->GetTempo() + direction); + break; + case SHUFFLE: + clock_m->SetShuffle(clock_m->GetShuffle() + direction); + break; + + case MULT1: + case MULT2: + case MULT3: + case MULT4: + clock_m->SetMultiply(clock_m->GetMultiply(cursor - MULT1) + direction, cursor - MULT1); + break; + + default: break; } } uint64_t OnDataRequest() { uint64_t data = 0; - Pack(data, PackLocation { 0, 1 }, clock_m->IsRunning() || clock_m->IsPaused()); - Pack(data, PackLocation { 1, 9 }, clock_m->GetTempo()); - Pack(data, PackLocation { 10, 5 }, clock_m->GetMultiply()); + // first 2 bits are reserved + Pack(data, PackLocation { 2, 9 }, clock_m->GetTempo()); + Pack(data, PackLocation { 11, 5 }, clock_m->GetClockPPQN()); + for (size_t i = 0; i < 4; ++i) { + Pack(data, PackLocation { 16+i*6, 6 }, clock_m->GetMultiply(i)+32); + Pack(data, PackLocation { 40+i*3, 3 }, HS::trigger_mapping[i] + 1); + } + + Pack(data, PackLocation { 52, 7 }, HS::trig_length); + Pack(data, PackLocation { 59, 1 }, HS::auto_save_enabled); + Pack(data, PackLocation { 60, 2 }, HS::modal_edit_mode); + Pack(data, PackLocation { 62, 2 }, HS::screensaver_mode); + return data; } void OnDataReceive(uint64_t data) { - if (Unpack(data, PackLocation { 0, 1 })) { - clock_m->Start(); - clock_m->Pause(); - } else { - clock_m->Stop(); + // bit 0 - reserved + // bit 1 - backward compatibility with Clock Forwarding + if (Unpack(data, PackLocation { 1, 1 })) HS::trigger_mapping[2] = 1; + + if (!clock_m->IsRunning()) + clock_m->SetTempoBPM(Unpack(data, PackLocation { 2, 9 })); + clock_m->SetClockPPQN(Unpack(data, PackLocation { 11, 5 })); + for (size_t i = 0; i < 4; ++i) { + clock_m->SetMultiply(Unpack(data, PackLocation { 16+i*6, 6 })-32, i); + } + + HS::modal_edit_mode = Unpack(data, PackLocation { 60, 2 }); + for (size_t i = 0; i < 4; ++i) { + uint8_t t = Unpack(data, PackLocation { 40+i*3, 3 }); + if (t) HS::trigger_mapping[i] = t - 1; + else { + // backward compatibility + HS::modal_edit_mode = Unpack(data, PackLocation { 50, 2 }); + break; + } } - clock_m->SetTempoBPM(Unpack(data, PackLocation { 1, 9 })); - clock_m->SetMultiply(Unpack(data, PackLocation { 10, 5 })); + + HS::trig_length = constrain( Unpack(data, PackLocation { 52, 7 }), 1, 127); + HS::auto_save_enabled = Unpack(data, PackLocation { 59, 1 }); + HS::screensaver_mode = Unpack(data, PackLocation { 62, 2 }); } protected: @@ -91,44 +229,122 @@ protected: } private: - int cursor; // 0=Source, 1=Tempo, 2=Multiply + int cursor; // ClockSetupCursor + int flash_ticker[4]; + int button_ticker; ClockManager *clock_m = clock_m->get(); + static const int NR_OF_TAPS = 3; + + int taps = 0; // tap tempo + uint32_t tap_time[NR_OF_TAPS]; // buffer of past tap tempo measurements + uint32_t last_tap_tick = 0; + + void PlayStop() { + if (clock_m->IsRunning()) { + clock_m->Stop(); + } else { + bool p = clock_m->IsPaused(); + clock_m->Start( !p ); // stop->pause->start + } + } + void DrawInterface() { // Header: This is sort of a faux applet, so its header // needs to extend across the screen graphics.setPrintPos(1, 2); - graphics.print("Clock Setup"); - gfxLine(0, 10, 62, 10); - gfxLine(0, 12, 62, 12); - graphics.drawLine(0, 10, 127, 10); - graphics.drawLine(0, 12, 127, 12); + graphics.print("Clocks/Triggers"); + gfxLine(0, 10, 127, 10); - // Clock Source + int y = 14; + // Clock State + gfxIcon(1, y, CLOCK_ICON); if (clock_m->IsRunning()) { - gfxIcon(1, 15, PLAY_ICON); - gfxPrint(16, 15, "Internal"); + gfxIcon(12, y, PLAY_ICON); } else if (clock_m->IsPaused()) { - gfxIcon(1, 15, PAUSE_ICON); - gfxPrint(16, 15, "Internal"); + gfxIcon(12, y, PAUSE_ICON); } else { - gfxIcon(1, 15, CLOCK_ICON); - gfxPrint(16, 15, "Forward"); + gfxIcon(12, y, STOP_ICON); } // Tempo - gfxIcon(1, 25, NOTE4_ICON); - gfxPrint(9, 25, "= "); - gfxPrint(pad(100, clock_m->GetTempo()), clock_m->GetTempo()); - gfxPrint(" BPM"); - - // Multiply - gfxPrint(1, 35, "x"); - gfxPrint(clock_m->GetMultiply()); - - if (cursor == 0) gfxCursor(16, 23, 46); - if (cursor == 1) gfxCursor(23, 33, 18); - if (cursor == 2) gfxCursor(8, 43, 12); + gfxPrint(22 + pad(100, clock_m->GetTempo()), y, clock_m->GetTempo()); + if (cursor != SHUFFLE) + gfxPrint(" BPM"); + else { + // Shuffle + gfxIcon(44, y, METRO_R_ICON); + gfxPrint(52 + pad(10, clock_m->GetShuffle()), y, clock_m->GetShuffle()); + gfxPrint("%"); + } + + // Input PPQN + gfxPrint(79, y, "Sync="); + gfxPrint(clock_m->GetClockPPQN()); + + y += 10; + for (int ch=0; ch<4; ++ch) { + const int x = ch * 32; + + // Multipliers + int mult = clock_m->GetMultiply(ch); + if (0 != mult || cursor == MULT1 + ch) { // hide if 0 + gfxPrint(1 + x, y, (mult >= 0) ? "x" : "/"); + gfxPrint( (mult >= 0) ? mult : 1 - mult ); + } + + // Physical trigger input mappings + gfxPrint(1 + x, y + 13, OC::Strings::trigger_input_names_none[ HS::trigger_mapping[ch] ] ); + + // Manual trigger buttons + gfxIcon(4 + x, 47, (button_ticker && ch == cursor-BOOP1)?BTN_ON_ICON:BTN_OFF_ICON); + + // Trigger indicators + gfxIcon(4 + x, 54, DOWN_BTN_ICON); + if (flash_ticker[ch]) gfxInvert(3 + x, 56, 9, 8); + } + + y += 10; + gfxDottedLine(0, y, 127, y, 3); + + switch ((ClockSetupCursor)cursor) { + case PLAY_STOP: + gfxFrame(11, 13, 10, 10); + break; + case TEMPO: + gfxCursor(22, 22, 19); + break; + case SHUFFLE: + gfxCursor(52, 22, 13); + break; + case EXT_PPQN: + gfxCursor(109,22, 13); + break; + + case MULT1: + case MULT2: + case MULT3: + case MULT4: + gfxCursor(8 + 32*(cursor-MULT1), 32, 12); + break; + + case TRIG1: + case TRIG2: + case TRIG3: + case TRIG4: + gfxCursor(1 + 32*(cursor-TRIG1), 45, 19); + break; + + case BOOP1: + case BOOP2: + case BOOP3: + case BOOP4: + if (0 == button_ticker) + gfxIcon(12 + 32*(cursor-BOOP1), 49, LEFT_ICON); + break; + + default: break; + } } }; @@ -144,8 +360,10 @@ private: ClockSetup ClockSetup_instance[1]; void ClockSetup_Start(bool hemisphere) {ClockSetup_instance[hemisphere].BaseStart(hemisphere);} -void ClockSetup_Controller(bool hemisphere, bool forwarding) {ClockSetup_instance[hemisphere].BaseController(forwarding);} -void ClockSetup_View(bool hemisphere) {ClockSetup_instance[hemisphere].BaseView();} +// NJM: don't call BaseController for ClockSetup +void ClockSetup_Controller(bool hemisphere, bool forwarding) {ClockSetup_instance[hemisphere].Controller();} +// don't call BaseView either... this isn't a real applet, is it? +void ClockSetup_View(bool hemisphere) {ClockSetup_instance[hemisphere].View();} void ClockSetup_OnButtonPress(bool hemisphere) {ClockSetup_instance[hemisphere].OnButtonPress();} void ClockSetup_OnEncoderMove(bool hemisphere, int direction) {ClockSetup_instance[hemisphere].OnEncoderMove(direction);} void ClockSetup_ToggleHelpScreen(bool hemisphere) {ClockSetup_instance[hemisphere].HelpScreen();} diff --git a/software/o_c_REV/HEM_ClockSkip.ino b/software/o_c_REV/HEM_ClockSkip.ino index fe84e92cb..cc6454e5d 100644 --- a/software/o_c_REV/HEM_ClockSkip.ino +++ b/software/o_c_REV/HEM_ClockSkip.ino @@ -37,7 +37,7 @@ public: ForEachChannel(ch) { if (Clock(ch)) { - int prob = p[ch] + Proportion(DetentedIn(ch), HEMISPHERE_MAX_CV, 100); + int prob = p[ch] + Proportion(DetentedIn(ch), HEMISPHERE_MAX_INPUT_CV, 100); if (random(1, 100) <= prob) { ClockOut(ch); trigger_countdown[ch] = 1667; @@ -49,17 +49,20 @@ public: } void View() { - gfxHeader(applet_name()); DrawSelector(); DrawIndicator(); } void OnButtonPress() { - cursor = 1 - cursor; + CursorAction(cursor, 1); } void OnEncoderMove(int direction) { - p[cursor] = constrain(p[cursor] += direction, 0, 100); + if (!EditMode()) { + MoveCursor(cursor, direction, 1); + return; + } + p[cursor] = constrain(p[cursor] + direction, 0, 100); } uint64_t OnDataRequest() { @@ -85,7 +88,7 @@ protected: private: int16_t p[2]; int trigger_countdown[2]; - uint8_t cursor; + int cursor; void DrawSelector() { diff --git a/software/o_c_REV/HEM_Compare.ino b/software/o_c_REV/HEM_Compare.ino index 6b0cabbdb..4a33ce86e 100644 --- a/software/o_c_REV/HEM_Compare.ino +++ b/software/o_c_REV/HEM_Compare.ino @@ -49,7 +49,6 @@ public: } void View() { - gfxHeader(applet_name()); DrawInterface(); } @@ -57,7 +56,7 @@ public: } void OnEncoderMove(int direction) { - level = constrain(level += direction, 0, HEM_COMPARE_MAX_VALUE); + level = constrain(level + direction, 0, HEM_COMPARE_MAX_VALUE); } uint64_t OnDataRequest() { diff --git a/software/o_c_REV/HEM_DrCrusher.ino b/software/o_c_REV/HEM_DrCrusher.ino.disabled similarity index 99% rename from software/o_c_REV/HEM_DrCrusher.ino rename to software/o_c_REV/HEM_DrCrusher.ino.disabled index 6fafb8882..4f76c8666 100644 --- a/software/o_c_REV/HEM_DrCrusher.ino +++ b/software/o_c_REV/HEM_DrCrusher.ino.disabled @@ -50,7 +50,6 @@ public: } void View() { - gfxHeader(applet_name()); DrawInterface(); } diff --git a/software/o_c_REV/HEM_DrumMap.ino b/software/o_c_REV/HEM_DrumMap.ino index e95619fca..474af2e2f 100644 --- a/software/o_c_REV/HEM_DrumMap.ino +++ b/software/o_c_REV/HEM_DrumMap.ino @@ -20,9 +20,13 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +#ifdef DRUMMAP_GRIDS2 +#include "grids2_resources.h" +#else #include "grids_resources.h" +#endif -#define HEM_DRUMMAP_PULSE_ANIMATION_TICKS 500 +#define HEM_DRUMMAP_PULSE_ANIMATION_TICKS 1000 #define HEM_DRUMMAP_VALUE_ANIMATION_TICKS 16000 #define HEM_DRUMMAP_AUTO_RESET_TICKS 30000 @@ -39,30 +43,30 @@ public: } void Controller() { - cv1 = Proportion(DetentedIn(0), HEMISPHERE_MAX_CV, 255); - cv2 = Proportion(DetentedIn(1), HEMISPHERE_MAX_CV, 255); + _fill[0] = fill[0]; + _fill[1] = fill[1]; + _x = x; + _y = y; + _chaos = chaos; - int _fill[2] = {fill[0], fill[1]}; - if (cv_mode == 0) { - _fill[0] = constrain(_fill[0]+cv1, 0, 255); - _fill[1] = constrain(_fill[1]+cv2, 0, 255); - } + switch (cv_mode) { + case 0: + Modulate(_fill[0], 0, 0, 255); + Modulate(_fill[1], 1, 0, 255); + break; - int _x = x; - int _y = y; - if (cv_mode == 1) { - _x = constrain(_x+cv1, 0, 255); - _y = constrain(_y+cv2, 0, 255); - } + case 1: + Modulate(_x, 0, 0, 255); + Modulate(_y, 1, 0, 255); + break; - int _chaos = chaos; - if (cv_mode == 2) { - _fill[0] = constrain(_fill[0]+cv1, 0, 255); - _chaos = constrain(_chaos+cv2, 0, 255); + case 2: + Modulate(_fill[0], 0, 0, 255); + Modulate(_chaos, 1, 0, 255); + break; } - - if (Clock(1)) Reset(); // Reset + if (Clock(1)) Reset(); if (Clock(0)) { // generate randomness for each drum type on first step of the pattern @@ -123,41 +127,61 @@ public: } void View() { - gfxHeader(applet_name()); DrawInterface(); } void OnButtonPress() { - if (++cursor > 7) cursor = 0; - if (mode[1] > 2 && cursor == 3) cursor = 4; + CursorAction(cursor, 7); + if (mode[1] > 2 && cursor == 3) ++cursor; } void OnEncoderMove(int direction) { + if (!EditMode()) { + do { + MoveCursor(cursor, direction, 7); + } while (mode[1] > 2 && cursor == 3); + + ResetCursor(); + return; + } + int accel = knob_accel >> 8; // modes - if (cursor == 0) { + switch (cursor) { + case 0: mode[0] += direction; if (mode[0] > 2) mode[0] = 0; if (mode[0] < 0) mode[0] = 2; - } - if (cursor == 1) { + break; + case 1: mode[1] += direction; if (mode[1] > 3) mode[1] = 0; if (mode[1] < 0) mode[1] = 3; - } + break; // fill - if (cursor == 2) fill[0] = constrain(fill[0] += (direction * accel), 0, 255); - if (cursor == 3) fill[1] = constrain(fill[1] += (direction * accel), 0, 255); + case 2: + fill[0] = constrain(fill[0] + (direction * accel), 0, 255); + break; + case 3: + fill[1] = constrain(fill[1] + (direction * accel), 0, 255); + break; // x/y - if (cursor == 4) x = constrain(x += (direction * accel), 0, 255); - if (cursor == 5) y = constrain(y += (direction * accel), 0, 255); + case 4: + x = constrain(x + (direction * accel), 0, 255); + break; + case 5: + y = constrain(y + (direction * accel), 0, 255); + break; // chaos - if (cursor == 6) chaos = constrain(chaos += (direction * accel), 0, 255); + case 6: + chaos = constrain(chaos + (direction * accel), 0, 255); + break; // cv assign - if (cursor == 7) { - cv_mode += direction; - if (cv_mode > 2) cv_mode = 0; - if (cv_mode < 0) cv_mode = 2; + case 7: + cv_mode += direction; + if (cv_mode > 2) cv_mode = 0; + if (cv_mode < 0) cv_mode = 2; + break; } // knob acceleration and value display for slider params @@ -192,6 +216,7 @@ public: mode[0] = Unpack(data, PackLocation {40,8}); mode[1] = Unpack(data, PackLocation {48,8}); cv_mode = Unpack(data, PackLocation {56,8}); + Reset(); } protected: @@ -206,9 +231,10 @@ protected: private: const uint8_t *MODE_ICONS[3] = {BD_ICON,SN_ICON,HH_ICON}; + const uint8_t *MODE_PULSE_ICON[3] = {BD_HIT_ICON,SN_HIT_ICON,HH_HIT_ICON}; const char *CV_MODE_NAMES[3] = {"FILL A/B", "X/Y", "FA/CHAOS"}; const int *VALUE_MAP[5] = {&fill[0], &fill[1], &x, &y, &chaos}; - uint8_t cursor = 0; + int cursor = 0; uint8_t step; uint8_t randomness[3] = {0, 0, 0}; int pulse_animation[2] = {0, 0}; @@ -219,12 +245,14 @@ private: // settings int8_t mode[2] = {0, 1}; int fill[2] = {128, 128}; + int _fill[2] = {128, 128}; int x = 0; + int _x = 0; int y = 0; + int _y = 0; int chaos = 0; + int _chaos = 0; int8_t cv_mode = 0; // 0 = Fill A/B, 1 = X/Y, 2 = Fill A/Chaos - int cv1 = 0; // internal tracking of cv inputs - int cv2 = 0; uint8_t ReadDrumMap(uint8_t step, uint8_t part, uint8_t x, uint8_t y) { uint8_t i = x >> 6; @@ -250,70 +278,54 @@ private: void DrawInterface() { // output selection gfxPrint(1,15,"A:"); - gfxIcon(14,14,MODE_ICONS[mode[0]]); + gfxIcon(15,15, (pulse_animation[0] > 0)? MODE_PULSE_ICON[mode[0]] : MODE_ICONS[mode[0]] ); + gfxPrint(32,15,"B:"); if (mode[1] == 3) { // accent - gfxIcon(45,14,MODE_ICONS[mode[0]]); + gfxIcon(46,15,MODE_ICONS[mode[0]]); gfxPrint(53,15,">"); } else { // standard - gfxIcon(45,14,MODE_ICONS[mode[1]]); + gfxIcon(46,15,(pulse_animation[1] > 0)? MODE_PULSE_ICON[mode[1]] : MODE_ICONS[mode[1]]); } + /* // pulse animation per channel ForEachChannel(ch){ if (pulse_animation[ch] > 0) { gfxInvert(1+ch*32,15,8,8); } } + */ // fill gfxPrint(1,25,"F"); - // add cv1 to fill_a value if cv1 mode is set to Fill A - int fa = fill[0]; - if (cv_mode == 0 || cv_mode == 2) fa = constrain(fa+cv1, 0, 255); - DrawKnobAt(9,25,20,fa,cursor == 2); + DrawKnobAt(9,25,20,_fill[0],cursor == 2); // don't show fill for channel b if it is an accent mode if (mode[1] < 3) { gfxPrint(32,25,"F"); - // add cv1 to fill_a value if cv1 mode is set to Fill A - int fb = fill[1]; - if (cv_mode == 0) fb = constrain(fb+cv2, 0, 255); - DrawKnobAt(40,25,20,fb,cursor == 3); + DrawKnobAt(40,25,20,_fill[1],cursor == 3); } // x & y - int _x = x; - if (cv_mode == 1) _x = constrain(_x+cv1, 0, 255); gfxPrint(1,35,"X"); DrawKnobAt(9,35,20,_x,cursor == 4); - int _y = y; - if (cv_mode == 1) _y = constrain(_y+cv2, 0, 255); gfxPrint(32,35,"Y"); DrawKnobAt(40,35,20,_y,cursor == 5); // chaos - int _chaos = chaos; - if (cv_mode == 2) _chaos = constrain(_chaos+cv2, 0, 255); gfxPrint(1,45,"CHAOS"); DrawKnobAt(32,45,28,_chaos,cursor == 6); - // cv input assignment - gfxIcon(1,57,CV_ICON); - gfxPrint(10,55,CV_MODE_NAMES[cv_mode]); - // step count in header gfxPrint((step < 9 ? 49 : 43),2,step+1); // cursor for non-knobs - if (cursor == 0) gfxCursor(14,23,16); // Part A - if (cursor == 1) gfxCursor(45,23,16); // Part B - if (cursor == 7) gfxCursor(10,63,50); // CV Assign + if (cursor <= 1) + gfxCursor(14+cursor*31,23,16); // Part A / B // display value for knobs if (value_animation > 0 && cursor >= 2 && cursor <= 6) { - gfxRect(1, 54, 60, 10); - gfxInvert(1, 54, 60, 10); int val = *VALUE_MAP[cursor-2]; int xPos = 27; if (val > 99) { @@ -322,15 +334,22 @@ private: xPos = 24; } gfxPrint(xPos, 55, val); - gfxInvert(1, 54, 60, 10); + gfxInvert(1, 54, 63, 10); + } else { + // cv input assignment + gfxIcon(1,57,CV_ICON); + gfxPrint(10,55,CV_MODE_NAMES[cv_mode]); + if (cursor == 7) gfxCursor(10,63,50); // CV Assign } + } void DrawKnobAt(byte x, byte y, byte len, byte value, bool is_cursor) { - byte w = Proportion(value, 255, len); + byte w = Proportion(value, 255, len-1); // minus 1 because width is 2 byte p = is_cursor ? 1 : 3; gfxDottedLine(x, y + 4, x + len, y + 4, p); - gfxRect(x + w, y, 2, 7); + gfxRect(x + w, y, 2, 8); + if (is_cursor && EditMode()) gfxInvert(x, y, len+1, 8); } void Reset() { diff --git a/software/o_c_REV/HEM_DualQuant.ino b/software/o_c_REV/HEM_DualQuant.ino index 88c4be341..46a776964 100644 --- a/software/o_c_REV/HEM_DualQuant.ino +++ b/software/o_c_REV/HEM_DualQuant.ino @@ -35,9 +35,8 @@ public: cursor = 0; ForEachChannel(ch) { - quantizer[ch].Init(); scale[ch] = ch + 5; - quantizer[ch].Configure(OC::Scales::GetScale(scale[ch]), 0xffff); + QuantizerConfigure(ch, scale[ch]); last_note[ch] = 0; continuous[ch] = 1; } @@ -53,7 +52,7 @@ public: if (continuous[ch] || EndOfADCLag(ch)) { int32_t pitch = In(ch); - int32_t quantized = quantizer[ch].Process(pitch, root[ch] << 7, 0); + int32_t quantized = Quantize(ch, pitch, root[ch] << 7, 0); Out(ch, quantized); last_note[ch] = quantized; } @@ -61,23 +60,26 @@ public: } void View() { - gfxHeader(applet_name()); DrawSelector(); } void OnButtonPress() { - if (++cursor > 3) cursor = 0; - ResetCursor(); + CursorAction(cursor, 3); } void OnEncoderMove(int direction) { + if (!EditMode()) { + MoveCursor(cursor, direction, 3); + return; + } + uint8_t ch = cursor / 2; if (cursor == 0 || cursor == 2) { // Scale selection scale[ch] += direction; if (scale[ch] >= OC::Scales::NUM_SCALES) scale[ch] = 0; if (scale[ch] < 0) scale[ch] = OC::Scales::NUM_SCALES - 1; - quantizer[ch].Configure(OC::Scales::GetScale(scale[ch]), 0xffff); + QuantizerConfigure(ch, scale[ch]); continuous[ch] = 1; // Re-enable continuous mode when scale is changed } else { // Root selection @@ -103,7 +105,7 @@ public: ForEachChannel(ch) { root[0] = constrain(root[0], 0, 11); - quantizer[ch].Configure(OC::Scales::GetScale(scale[ch]), 0xffff); + QuantizerConfigure(ch, scale[ch]); } } @@ -117,7 +119,6 @@ protected: } private: - braids::Quantizer quantizer[2]; int last_note[2]; // Last quantized note bool continuous[2]; // Each channel starts as continuous and becomes clocked when a clock is received int cursor; @@ -128,8 +129,7 @@ private: void DrawSelector() { - const uint8_t notes[2][8] = {{0xc0, 0xe0, 0xe0, 0xe0, 0x7f, 0x02, 0x14, 0x08}, - {0xc0, 0xa0, 0xa0, 0xa0, 0x7f, 0x00, 0x00, 0x00}}; + const uint8_t * notes[2] = {NOTE_ICON, NOTE2_ICON}; ForEachChannel(ch) { @@ -139,10 +139,9 @@ private: gfxPrint(10 + (31 * ch), 25, OC::Strings::note_names_unpadded[root[ch]]); // Draw cursor - int cursor_ch = cursor / 2; - if (ch == cursor_ch) { - if (cursor == 0 || cursor == 2) gfxCursor(0 + (31 * ch), 23, 30); - else gfxCursor(10 + (31 * ch), 33, 12); + int y = cursor % 2; // 0=top line, 1=bottom + if (ch == (cursor / 2)) { + gfxCursor(y*10 + ch*31, 23 + y*10, 12+(1-y)*18); } // Little note display diff --git a/software/o_c_REV/HEM_EbbAndLfo.ino b/software/o_c_REV/HEM_EbbAndLfo.ino new file mode 100644 index 000000000..2c5a32e8e --- /dev/null +++ b/software/o_c_REV/HEM_EbbAndLfo.ino @@ -0,0 +1,350 @@ +#include "resources/tideslite.h" + +class EbbAndLfo : public HemisphereApplet { +public: + const char *applet_name() { return "Ebb & LFO"; } + + void Start() { phase = 0; } + + void Controller() { + if (Clock(1)) phase = 0; + if (Clock(0)) { + clocks_received++; + //uint32_t next_tick = predictor.Predict(ClockCycleTicks(0)); + if (clocks_received > 1) { + int new_freq = 0xffffffff / ClockCycleTicks(0); + pitch = ComputePitch(new_freq); + phase = 0; + } + } + + // handle CV inputs + pitch_mod = pitch; + slope_mod = slope; + shape_mod = shape; + fold_mod = fold; + + ForEachChannel(ch) { + switch (cv_type(ch)) { + case FREQ: + pitch_mod += In(ch); + break; + case SLOPE: + Modulate(slope_mod, ch, 0, 127); + break; + case SHAPE: + shape_mod += Proportion(DetentedIn(ch), HEMISPHERE_MAX_INPUT_CV, 127); + while (shape_mod < 0) shape_mod += 128; + while (shape_mod > 127) shape_mod -= 128; + break; + case FOLD: + Modulate(fold_mod, ch, 0, 127); + break; + } + } + + uint32_t phase_increment = ComputePhaseIncrement(pitch_mod); + phase += phase_increment; + + // COMPUTE + int s = constrain(slope_mod * 65535 / 127, 0, 65535); + ProcessSample(s, shape_mod * 65535 / 127, fold_mod * 32767 / 127, phase, sample); + + ForEachChannel(ch) { + switch (output(ch)) { + case UNIPOLAR: + Out(ch, Proportion(sample.unipolar, 65535, HEMISPHERE_MAX_CV)); + break; + case BIPOLAR: + #ifdef VOR + Out(ch, Proportion(sample.bipolar, 32767, 7680)); // hardcoded at 5V for Plum Audio + #else + Out(ch, Proportion(sample.bipolar, 32767, HEMISPHERE_MAX_CV / 2)); + #endif + break; + case EOA: + GateOut(ch, sample.flags & FLAG_EOA); + break; + case EOR: + GateOut(ch, sample.flags & FLAG_EOR); + break; + } + } + + if (knob_accel > (1 << 8)) + knob_accel--; + } + + void View() { + ForEachChannel(ch) { + int h = 17; + int bottom = 32 + (h + 1) * ch; + int last = bottom; + for (int i = 0; i < 64; i++) { + ProcessSample(slope_mod * 65535 / 127, shape_mod * 65535 / 127, + fold_mod * 32767 / 127, 0xffffffff / 64 * i, disp_sample); + int next = 0; + switch (output(ch)) { + case UNIPOLAR: + next = bottom - disp_sample.unipolar * h / 65535; + break; + case BIPOLAR: + next = bottom - (disp_sample.bipolar + 32767) * h / 65535; + break; + case EOA: + next = bottom - ((disp_sample.flags & FLAG_EOA) ? h : 0); + break; + case EOR: + next = bottom - ((disp_sample.flags & FLAG_EOR) ? h : 0); + break; + } + if (i > 0) gfxLine(i - 1, last, i, next); + last = next; + // gfxPixel(i, 50 - disp_sample.unipolar * 35 / 65536); + } + } + uint32_t p = phase / (0xffffffff / 64); + gfxLine(p, 15, p, 50); + + switch (cursor) { + case 0: + // gfxPrint(0, 55, "Frq:"); + gfxPos(0, 56); + gfxPrintFreq(pitch); + break; + case 1: + gfxPrint(0, 56, "Slope: "); + gfxPrint(slope); + break; + case 2: + gfxPrint(0, 56, "Shape: "); + gfxPrint(shape); + break; + case 3: + gfxPrint(0, 56, "Fold: "); + gfxPrint(fold); + break; + case 4: + gfxPrint(0, 56, hemisphere == 0 ? "A:" : "C:"); + gfxPrint(out_labels[output(0)]); + gfxPrint(hemisphere == 0 ? " B:" : " D:"); + gfxPrint(out_labels[output(1)]); + break; + case 5: + ForEachChannel(ch) { + gfxIcon(0 + ch*32, 56, CV_ICON); + gfxBitmap(8 + ch*32, 56, 3, ch ? SUB_TWO : SUP_ONE); + gfxPrint(13 + ch*32, 56, cv_labels[cv_type(ch)]); + } + break; + } + if (EditMode()) gfxInvert(0, 55, 64, 9); + } + + void OnButtonPress() { + CursorAction(cursor, 5); + } + + void OnEncoderMove(int direction) { + if (!EditMode()) { + MoveCursor(cursor, direction, 5); + return; + } + + switch (cursor) { + case 0: { + uint32_t old_pi = ComputePhaseIncrement(pitch); + pitch += (knob_accel >> 8) * direction; + while (ComputePhaseIncrement(pitch) == old_pi) { + pitch += direction; + } + break; + } + case 1: { + // slope += (knob_accel >> 4) * direction; + slope = constrain(slope + direction, 0, 127); + break; + } + case 2: { + shape += direction; + while (shape < 0) shape += 128; + while (shape > 127) shape -= 128; + break; + } + case 3: { + fold = constrain(fold + direction, 0, 127); + break; + } + case 4: { + out += direction; + out %= 0b10000; + break; + } + case 5: + cv += direction; + cv %= 0b10000; + break; + } + + if (knob_accel < (1 << 13)) + knob_accel <<= 1; + } + + uint64_t OnDataRequest() { + uint64_t data = 0; + Pack(data, PackLocation { 0, 16 }, pitch + (1 << 15)); + Pack(data, PackLocation { 16, 7 }, slope); + Pack(data, PackLocation { 23, 7 }, shape); + Pack(data, PackLocation { 30, 7 }, fold); + Pack(data, PackLocation { 37, 4 }, out); + Pack(data, PackLocation { 41, 4 }, cv); + return data; + } + + void OnDataReceive(uint64_t data) { + clocks_received = 0; + pitch = Unpack(data, PackLocation { 0, 16 }) - (1 << 15); + slope = Unpack(data, PackLocation { 16, 7 }); + shape = Unpack(data, PackLocation { 23, 7 }); + fold = Unpack(data, PackLocation { 30, 7 }); + out = Unpack(data, PackLocation { 37, 4 }); + cv = Unpack(data, PackLocation { 41, 4 }); + } + +protected: + void SetHelp() { + // "------------------" <-- Size Guide + help[HEMISPHERE_HELP_DIGITALS] = "1=Clock 2=Reset"; + help[HEMISPHERE_HELP_CVS] = "1,2=Assignable"; + help[HEMISPHERE_HELP_OUTS] = "A=OutA B=OutB"; + help[HEMISPHERE_HELP_ENCODER] = "Select/Edit params"; + // "------------------" <-- Size Guide + } + +private: + + enum Output { + UNIPOLAR, + BIPOLAR, + EOA, + EOR, + }; + const char* out_labels[4] = {"Un", "Bi", "Hi", "Lo"}; + + enum CV { + FREQ, + SLOPE, + SHAPE, + FOLD, + }; + const char* cv_labels[4] = {"Hz", "Sl", "Sh", "Fo"}; + + int cursor = 0; + int16_t pitch = -3 * 12 * 128; + int slope = 64; + int shape = 48; // triangle + int fold = 0; + + // actual values after CV mod + int16_t pitch_mod; + int slope_mod; + int shape_mod; + int fold_mod; + + int clocks_received = 0; + + uint8_t out = 0b0001; // Unipolar on A, bipolar on B + uint8_t cv = 0b0001; // Freq on 1, shape on 2 + TidesLiteSample disp_sample; + TidesLiteSample sample; + + int knob_accel = 1 << 8; + + uint32_t phase; + + Output output(int ch) { + return (Output) ((out >> ((1 - ch) * 2)) & 0b11); + } + + CV cv_type(int ch) { + return (CV) ((cv >> ((1 - ch) * 2)) & 0b11); + } + + void gfxPrintFreq(int16_t pitch) { + uint32_t num = ComputePhaseIncrement(pitch); + uint32_t denom = 0xffffffff / 16666; + bool swap = num < denom; + if (swap) { + uint32_t t = num; + num = denom; + denom = t; + } + int int_part = num / denom; + int digits = 0; + if (int_part < 10) + digits = 1; + else if (int_part < 100) + digits = 2; + else if (int_part < 1000) + digits = 3; + else + digits = 4; + + gfxPrint(int_part); + gfxPrint("."); + + num %= denom; + while (digits < 4) { + num *= 10; + gfxPrint(num / denom); + num %= denom; + digits++; + } + if (swap) { + gfxPrint("s"); + } else { + gfxPrint("Hz"); + } + } +}; +//////////////////////////////////////////////////////////////////////////////// +//// Hemisphere Applet Functions +/// +/// Once you run the find-and-replace to make these refer to EbbAndLfo, +/// it's usually not necessary to do anything with these functions. You +/// should prefer to handle things in the HemisphereApplet child class +/// above. +//////////////////////////////////////////////////////////////////////////////// +EbbAndLfo EbbAndLfo_instance[2]; + +void EbbAndLfo_Start(bool hemisphere) { + EbbAndLfo_instance[hemisphere].BaseStart(hemisphere); +} + +void EbbAndLfo_Controller(bool hemisphere, bool forwarding) { + EbbAndLfo_instance[hemisphere].BaseController(forwarding); +} + +void EbbAndLfo_View(bool hemisphere) { + EbbAndLfo_instance[hemisphere].BaseView(); +} + +void EbbAndLfo_OnButtonPress(bool hemisphere) { + EbbAndLfo_instance[hemisphere].OnButtonPress(); +} + +void EbbAndLfo_OnEncoderMove(bool hemisphere, int direction) { + EbbAndLfo_instance[hemisphere].OnEncoderMove(direction); +} + +void EbbAndLfo_ToggleHelpScreen(bool hemisphere) { + EbbAndLfo_instance[hemisphere].HelpScreen(); +} + +uint64_t EbbAndLfo_OnDataRequest(bool hemisphere) { + return EbbAndLfo_instance[hemisphere].OnDataRequest(); +} + +void EbbAndLfo_OnDataReceive(bool hemisphere, uint64_t data) { + EbbAndLfo_instance[hemisphere].OnDataReceive(data); +} diff --git a/software/o_c_REV/HEM_EnigmaJr.ino b/software/o_c_REV/HEM_EnigmaJr.ino index b188cdda9..849b67c09 100644 --- a/software/o_c_REV/HEM_EnigmaJr.ino +++ b/software/o_c_REV/HEM_EnigmaJr.ino @@ -69,15 +69,19 @@ public: } void View() { - gfxHeader(applet_name()); DrawInterface(); } void OnButtonPress() { - if (++cursor > 3) cursor = 0; + CursorAction(cursor, 3); } void OnEncoderMove(int direction) { + if (!EditMode()) { + MoveCursor(cursor, direction, 3); + return; + } + if (cursor == 0) { // Switch TM byte prev_tm = tm_state.GetTMIndex(); byte new_tm = constrain(prev_tm + direction, 0, HS::TURING_MACHINE_COUNT - 1); diff --git a/software/o_c_REV/HEM_EnvFollow.ino b/software/o_c_REV/HEM_EnvFollow.ino index 4d6d04772..893ea9b8e 100644 --- a/software/o_c_REV/HEM_EnvFollow.ino +++ b/software/o_c_REV/HEM_EnvFollow.ino @@ -19,10 +19,18 @@ // SOFTWARE. #define HEM_ENV_FOLLOWER_SAMPLES 166 +#define HEM_ENV_FOLLOWER_MAXSPEED 16 class EnvFollow : public HemisphereApplet { public: + enum EnvFollowCursor { + MODE_1, MODE_2, + GAIN_1, GAIN_2, + SPEED, + MAX_CURSOR = SPEED + }; + const char* applet_name() { return "EnvFollow"; } @@ -51,29 +59,49 @@ public: ForEachChannel(ch) { - if (In(ch) > max[ch]) max[ch] = In(ch); - if (target[ch] > signal[ch]) signal[ch]++; - else if (target[ch] < signal[ch]) signal[ch]--; + int v = abs(In(ch)); + if (v > max[ch]) max[ch] = v; + if (target[ch] > signal[ch]) { + signal[ch] += speed; + if (signal[ch] > target[ch]) signal[ch] = target[ch]; + } + else if (target[ch] < signal[ch]) { + signal[ch] -= speed; + if (signal[ch] < target[ch]) signal[ch] = target[ch]; + } Out(ch, signal[ch]); } } void View() { - gfxHeader(applet_name()); DrawInterface(); gfxSkyline(); } void OnButtonPress() { - if (++cursor > 3) cursor = 0; - ResetCursor(); + CursorAction(cursor, MAX_CURSOR); } void OnEncoderMove(int direction) { - if (cursor < 2) { // Gain per channel - gain[cursor] = constrain(gain[cursor] + direction, 1, 31); - } else { - duck[cursor - 2] = 1 - duck[cursor - 2]; + if (!EditMode()) { + MoveCursor(cursor, direction, MAX_CURSOR); + return; + } + + switch (cursor) { + case GAIN_1: + case GAIN_2: + gain[cursor - GAIN_1] = constrain(gain[cursor - GAIN_1] + direction, 1, 31); + break; + + case MODE_1: + case MODE_2: + duck[cursor] = 1 - duck[cursor]; + break; + + case SPEED: + speed = constrain(speed + direction, 1, HEM_ENV_FOLLOWER_MAXSPEED); + break; } ResetCursor(); } @@ -84,6 +112,7 @@ public: Pack(data, PackLocation {5,5}, gain[1]); Pack(data, PackLocation {10,1}, duck[0]); Pack(data, PackLocation {11,1}, duck[1]); + Pack(data, PackLocation {12,4}, speed - 1); return data; } @@ -92,6 +121,8 @@ public: gain[1] = Unpack(data, PackLocation {5,5}); duck[0] = Unpack(data, PackLocation {10,1}); duck[1] = Unpack(data, PackLocation {11,1}); + speed = Unpack(data, PackLocation {12,4}) + 1; + speed = constrain(speed, 1, HEM_ENV_FOLLOWER_MAXSPEED); } protected: @@ -100,12 +131,12 @@ protected: help[HEMISPHERE_HELP_DIGITALS] = ""; help[HEMISPHERE_HELP_CVS] = "Inputs 1,2"; help[HEMISPHERE_HELP_OUTS] = "Follow/Duck"; - help[HEMISPHERE_HELP_ENCODER] = "Gain/Assign"; + help[HEMISPHERE_HELP_ENCODER] = "Gain/Assign/Speed"; // "------------------" <-- Size Guide } private: - uint8_t cursor; + int cursor; int max[2]; uint8_t countdown; int signal[2]; @@ -114,6 +145,7 @@ private: // Setting uint8_t gain[2]; bool duck[2]; // Choose between follow and duck per channel + int speed = 1; // attack/release rate void DrawInterface() { ForEachChannel(ch) @@ -124,10 +156,27 @@ private: // Gain gfxFrame(32 * ch, 25, gain[ch], 3); - if (cursor == ch && CursorBlink()) gfxRect(32 * ch, 25, gain[ch], 3); + } + + switch (cursor) { + case MODE_1: + case MODE_2: + gfxCursor(1 + (38 * cursor), 23, 24); + break; + + case GAIN_1: + case GAIN_2: + gfxCursor(32 * (cursor - GAIN_1), 29, gain[cursor - GAIN_1], 5); + break; + + case SPEED: + gfxIcon(20, 31, GAUGE_ICON); + gfxPrint(28, 31, speed); + gfxCursor(28, 39, 14); + break; + default: break; } - if (cursor > 1) gfxCursor(1 + (38 * (cursor - 2)), 23, 24); } }; diff --git a/software/o_c_REV/HEM_EuclidX.ino b/software/o_c_REV/HEM_EuclidX.ino new file mode 100644 index 000000000..a83de375c --- /dev/null +++ b/software/o_c_REV/HEM_EuclidX.ino @@ -0,0 +1,339 @@ +// Copyright (c) 2022, Bryan Head +// Copyright (c) 2022, Nicholas J. Michalek +// Copyright (c) 2022, Alessio Degani +// Copyright (c) 2018, Jason Justian +// +// Bjorklund pattern filter, Copyright (c) 2016 Tim Churches +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +/* This applet is a replacement for the original Annular Fusion Euclidean Drummer, + * redesigned by qiemem and modified by djphazer to add CV modulation. + * The CV input logic as well as the name were copied from a separate rewrite by adegani. + */ + +#include "bjorklund.h" + +const int NUM_PARAMS = 5; +const int PARAM_SIZE = 6; + +class EuclidX : public HemisphereApplet { +public: + + enum EuclidXParam { + LENGTH1, BEATS1, OFFSET1, PADDING1, + LENGTH2, BEATS2, OFFSET2, PADDING2, + CV_DEST1, + CV_DEST2, + LAST_SETTING = CV_DEST2 + }; + + const char* applet_name() { + return "EuclidX"; + } + + void Start() { + ForEachChannel(ch) + { + length[ch] = 16; + beats[ch] = 4 + ch*4; + offset[ch] = 0; + padding[ch] = ch*16; + pattern[ch] = EuclideanPattern(length[ch], beats[ch], offset[ch], padding[ch]); + } + step = 0; + } + + void Controller() { + if (Clock(1)) step = 0; // Reset + + // continuously recalculate pattern with CV offsets + ForEachChannel(ch) { + actual_length[ch] = length[ch]; + actual_beats[ch] = beats[ch]; + actual_offset[ch] = offset[ch]; + actual_padding[ch] = padding[ch]; + + // process CV inputs + ForEachChannel(cv_ch) { + switch (cv_dest[cv_ch] - ch * LENGTH2) { // this is dumb, but efficient + case LENGTH1: + Modulate(actual_length[ch], ch, 2, 32); + + if (actual_beats[ch] > actual_length[ch]) + actual_beats[ch] = actual_length[ch]; + if (actual_padding[ch] > 32 - actual_length[ch]) + actual_padding[ch] = 32 - actual_length[ch]; + if (actual_offset[ch] >= actual_length[ch] + actual_padding[ch]) + actual_offset[ch] = actual_length[ch] + actual_padding[ch] - 1; + + break; + case BEATS1: + Modulate(actual_beats[ch], ch, 0, actual_length[ch]); + break; + case OFFSET1: + Modulate(actual_offset[ch], ch, 0, actual_length[ch] + padding[ch]); + break; + case PADDING1: + Modulate(actual_padding[ch], ch, 0, 32 - actual_length[ch]); + if (actual_offset[ch] >= actual_length[ch] + actual_padding[ch]) + actual_offset[ch] = actual_length[ch] + actual_padding[ch] - 1; + break; + default: break; + } + } + + // Store the pattern for display + pattern[ch] = EuclideanPattern(actual_length[ch], actual_beats[ch], actual_offset[ch], actual_padding[ch]); + } + + // Process triggers and step forward on clock + if (Clock(0)) { + + ForEachChannel(ch) { + // actually output the triggers + int sb = step % (actual_length[ch] + actual_padding[ch]); + if ((pattern[ch] >> sb) & 0x01) { + ClockOut(ch); + } + } + + // Plan for the thing to run forever and ever + if (++step >= (actual_length[0]+actual_padding[0]) * (actual_length[1]+actual_padding[1])) step = 0; + } + } + + void View() { + DrawSteps(); + DrawEditor(); + } + + void OnButtonPress() { + CursorAction(cursor, LAST_SETTING); + } + + void OnEncoderMove(int direction) { + if (!EditMode()) { + MoveCursor(cursor, direction, LAST_SETTING); + return; + } + + int ch = cursor < LENGTH2 ? 0 : 1; + switch (cursor) { + case LENGTH1: + case LENGTH2: + actual_length[ch] = length[ch] = constrain(length[ch] + direction, 2, 32); + if (beats[ch] > length[ch]) + beats[ch] = length[ch]; + if (padding[ch] > 32 - length[ch]) + padding[ch] = 32 - length[ch]; + if (offset[ch] >= length[ch] + padding[ch]) + offset[ch] = length[ch] + padding[ch] - 1; + break; + case BEATS1: + case BEATS2: + actual_beats[ch] = beats[ch] = constrain(beats[ch] + direction, 0, length[ch]); + break; + case OFFSET1: + case OFFSET2: + actual_offset[ch] = offset[ch] = constrain(offset[ch] + direction, 0, length[ch] + padding[ch] - 1); + break; + case PADDING1: + case PADDING2: + padding[ch] = constrain(padding[ch] + direction, 0, 32 - length[ch]); + if (offset[ch] >= length[ch] + padding[ch]) + offset[ch] = length[ch] + padding[ch] - 1; + break; + case CV_DEST1: + case CV_DEST2: + cv_dest[cursor - CV_DEST1] = (EuclidXParam) constrain(cv_dest[cursor - CV_DEST1] + direction, LENGTH1, PADDING2); + break; + } + } + + uint64_t OnDataRequest() { + uint64_t data = 0; + size_t idx = 0; + ForEachChannel(ch) { + Pack(data, PackLocation {idx++ * PARAM_SIZE, PARAM_SIZE}, length[ch] - 1); + Pack(data, PackLocation {idx++ * PARAM_SIZE, PARAM_SIZE}, beats[ch]); + Pack(data, PackLocation {idx++ * PARAM_SIZE, PARAM_SIZE}, offset[ch]); + Pack(data, PackLocation {idx++ * PARAM_SIZE, PARAM_SIZE}, padding[ch]); + Pack(data, PackLocation {idx++ * PARAM_SIZE, PARAM_SIZE}, cv_dest[ch]); + } + return data; + } + + void OnDataReceive(uint64_t data) { + size_t idx = 0; + ForEachChannel(ch) { + actual_length[ch] = length[ch] = Unpack(data, PackLocation {idx++ * PARAM_SIZE, PARAM_SIZE}) + 1; + actual_beats[ch] = beats[ch] = Unpack(data, PackLocation {idx++ * PARAM_SIZE, PARAM_SIZE}); + actual_offset[ch] = offset[ch] = Unpack(data, PackLocation {idx++ * PARAM_SIZE, PARAM_SIZE}); + actual_padding[ch] = padding[ch] = Unpack(data, PackLocation {idx++ * PARAM_SIZE, PARAM_SIZE}); + cv_dest[ch] = (EuclidXParam) Unpack(data, PackLocation {idx++ * PARAM_SIZE, PARAM_SIZE}); + } + step = 0; // reset + } + +protected: + void SetHelp() { + // "------------------" <-- Size Guide + help[HEMISPHERE_HELP_DIGITALS] = "1=Clock 2=Reset"; + help[HEMISPHERE_HELP_CVS] = "Assignable"; + help[HEMISPHERE_HELP_OUTS] = "Clock A=Ch1 B=Ch2"; + help[HEMISPHERE_HELP_ENCODER] = "Len/Hits/Rot/CV"; + // "------------------" <-- Size Guide + } + +private: + int step; + int cursor = LENGTH1; // EuclidXParam + uint32_t pattern[2]; + + // Settings + uint8_t length[2]; + uint8_t beats[2]; + uint8_t offset[2]; + uint8_t padding[2]; + uint8_t actual_length[2]; + uint8_t actual_beats[2]; + uint8_t actual_offset[2]; + uint8_t actual_padding[2]; + + EuclidXParam cv_dest[2] = {BEATS1, BEATS2}; // input modulation + + void DrawSteps() { + gfxLine(0, 45, 63, 45); + gfxLine(0, 62, 63, 62); + gfxLine(0, 53, 63, 53); + gfxLine(0, 54, 63, 54); + ForEachChannel(ch) { + for (int i = 0; i < 16; i++) { + if ((pattern[ch] >> ((i + step) % (actual_length[ch]+actual_padding[ch]) )) & 0x1) { + gfxRect(4 * i + 1, 48 + 9 * ch, 3, 3); + //gfxLine(4 * i + 2, 47 + 9 * ch, 4 * i + 2, 47 + 9 * ch + 4); + } else { + gfxPixel(4 * i + 2, 47 + 9 * ch + 2); + } + + if ((i + step) % (actual_length[ch]+actual_padding[ch]) == 0) { + //gfxLine(4 * i, 46 + 9 * ch, 4 * i, 52 + 9 * ch); + gfxLine(4 * i, 46 + 9 * ch, 4 * i, 46 + 9 * ch + 1); + gfxLine(4 * i, 52 + 9 * ch - 1, 4 * i, 52 + 9 * ch); + } + } + } + } + + void DrawEditor() { + const int spacing = 16; + const int pad_left = 5; + + if (cursor < CV_DEST1) { + gfxBitmap(pad_left + 0 * spacing, 15, 8, LENGTH_ICON); + } + gfxBitmap(pad_left + 1 * spacing, 15, 8, PULSES_ICON); + gfxBitmap(pad_left + 2 * spacing, 15, 8, ROTATE_ICON); + gfxIcon(pad_left + 3 * spacing, 15, OFFSET_ICON); + + int y = 15; + ForEachChannel (ch) { + y += 10; + gfxPrint(0 * spacing + pad(10, actual_length[ch]), y, actual_length[ch]); + gfxPrint(1 * spacing + pad(10, actual_beats[ch]), y, actual_beats[ch]); + gfxPrint(2 * spacing + pad(10, actual_offset[ch]), y, actual_offset[ch]); + gfxPrint(3 * spacing + pad(10, actual_padding[ch]), y, actual_padding[ch]); + + // CV assignment indicators + ForEachChannel(ch_dest) { + int ff = cv_dest[ch_dest] - LENGTH2*ch; + if (ff >= 0 && ff < LENGTH2) + gfxBitmap(13 + ff * spacing, y, 3, ch_dest?SUB_TWO:SUP_ONE); + } + } + + int ch = cursor < LENGTH2 ? 0 : 1; + int f = cursor - ch * LENGTH2; + y = 33; + switch (cursor) { + case LENGTH2: + case BEATS2: + case OFFSET2: + case PADDING2: + y += 10; + case LENGTH1: + case BEATS1: + case OFFSET1: + case PADDING1: + gfxCursor(f * spacing, y, 13); + break; + + case CV_DEST1: + case CV_DEST2: + gfxBitmap(0, 13 + (cursor - CV_DEST1)*5, 8, CV_ICON); + gfxBitmap(8, 15, 3, (cursor - CV_DEST1)? SUB_TWO : SUP_ONE); + gfxCursor(0, 19 + (cursor - CV_DEST1)*5, 11, 7); + break; + } + } +}; + + +//////////////////////////////////////////////////////////////////////////////// +//// Hemisphere Applet Functions +/// +/// Once you run the find-and-replace to make these refer to EuclidX, +/// it's usually not necessary to do anything with these functions. You +/// should prefer to handle things in the HemisphereApplet child class +/// above. +//////////////////////////////////////////////////////////////////////////////// +EuclidX EuclidX_instance[2]; + +void EuclidX_Start(bool hemisphere) { + EuclidX_instance[hemisphere].BaseStart(hemisphere); +} + +void EuclidX_Controller(bool hemisphere, bool forwarding) { + EuclidX_instance[hemisphere].BaseController(forwarding); +} + +void EuclidX_View(bool hemisphere) { + EuclidX_instance[hemisphere].BaseView(); +} + +void EuclidX_OnButtonPress(bool hemisphere) { + EuclidX_instance[hemisphere].OnButtonPress(); +} + +void EuclidX_OnEncoderMove(bool hemisphere, int direction) { + EuclidX_instance[hemisphere].OnEncoderMove(direction); +} + +void EuclidX_ToggleHelpScreen(bool hemisphere) { + EuclidX_instance[hemisphere].HelpScreen(); +} + +uint64_t EuclidX_OnDataRequest(bool hemisphere) { + return EuclidX_instance[hemisphere].OnDataRequest(); +} + +void EuclidX_OnDataReceive(bool hemisphere, uint64_t data) { + EuclidX_instance[hemisphere].OnDataReceive(data); +} diff --git a/software/o_c_REV/HEM_GameOfLife.ino b/software/o_c_REV/HEM_GameOfLife.ino new file mode 100644 index 000000000..d2da8a8bd --- /dev/null +++ b/software/o_c_REV/HEM_GameOfLife.ino @@ -0,0 +1,233 @@ +#define GOL_ABS(X) (X < 0 ? -X : X) + +class GameOfLife : public HemisphereApplet { +public: + + const char* applet_name() { + return "Game/Life"; + } + + void Start() { + for (int i = 0; i < 80; i++) board[i] = 0; + weight = 30; + tx = 0; + ty = 0; + + // Start off with a sweet-looking board + for (int x = 0; x < 6; x++) + { + AddToBoard(x + 26, x + 23); + AddToBoard(x + 33, (5 - x) + 23); + } + AddToBoard(32, 28); + } + + void Controller() { + tx = ProportionCV(In(0), 63); + ty = ProportionCV(In(1), 39); + + if (Clock(0)) { + ProcessGameBoard(tx, ty); + } + if (Gate(1)) AddToBoard(tx, ty); + + int global_density_cv = Proportion(global_density, 1200 - (weight * 10), HEMISPHERE_MAX_CV); + int local_density_cv = Proportion(local_density, 225, HEMISPHERE_MAX_CV); + Out(0, constrain(global_density_cv, 0, HEMISPHERE_MAX_CV)); + Out(1, constrain(local_density_cv, 0, HEMISPHERE_MAX_CV)); + } + + void View() { + DrawBoard(); + DrawIndicator(); + DrawCrosshairs(); + } + + void ScreensaverView() { + DrawBoard(); + DrawIndicator(); + } + + void OnButtonPress() { + for (int i = 0; i < 80; i++) board[i] = 0; + } + + void OnEncoderMove(int direction) { + weight = constrain(weight += direction, 0, 100); + } + + uint64_t OnDataRequest() { + uint64_t data = 0; + Pack(data, PackLocation {0,6}, weight); + return data; + } + + void OnDataReceive(uint64_t data) { + weight = Unpack(data, PackLocation {0,6}); + } + +protected: + void SetHelp() { + // "------------------" <-- Size Guide + help[HEMISPHERE_HELP_DIGITALS] = "1=Clock 2=Draw"; + help[HEMISPHERE_HELP_CVS] = "1=X pos 2=Y pos"; + help[HEMISPHERE_HELP_OUTS] = "A=Global B=Local"; + help[HEMISPHERE_HELP_ENCODER] = "T=Weight P=Clear"; + // "------------------" <-- Size Guide + } + +private: + uint64_t board[80]; // 64x40 board + int weight; // Weight of each cell + int global_density; // Count of all live cells + int local_density; // Count of cells in the vicinity of the Traveler + int tx; + int ty; + + void DrawBoard() { + for (int y = 0; y < 40; y++) + { + for (int b = 0; b < 2; b++) + { + for (int c = 0; c < 32; c++) + { + if ((board[(y * 2) + b] >> c) & 0x01) gfxPixel(c + (32 * b), y + 22); + } + } + } + } + + void DrawIndicator() { + // Output indicators + ForEachChannel(ch) + { + gfxRect(1, 15 + (ch * 4), ProportionCV(ViewOut(ch), 62), 2); + } + } + + void DrawCrosshairs() { + gfxLine(tx, 23, tx, 63); + gfxLine(0, ty + 22, 62, ty + 22); + } + + void ProcessGameBoard(int tx, int ty) { + uint64_t next_gen[80]; + global_density = 0; + local_density = 0; + for (int y = 0; y < 40; y++) + { + next_gen[y * 2] = 0; // Clear next generation board + next_gen[y * 2 + 1] = 0; + for (int x = 0; x < 64; x++) + { + bool live = ValueAtCell(x, y); + int ln = CountLiveNeighborsAt(x, y); + + // 1: Any live cell with fewer than two live neighbours dies (referred to as underpopulation or exposure). + // 2: Any live cell with more than three live neighbours dies (referred to as overpopulation or overcrowding). + // Nothing to do to observe these rules, since we start with an empty board + + // 3: Any live cell with two or three live neighbours lives, unchanged, to the next generation. + // 4: Any dead cell with exactly three live neighbours will come to life. + if (((ln == 2 || ln == 3) && live) || (ln == 3 && !live)) { + int i = y * 2; + int xb = x; + if (x > 31) { + i += 1; + xb -= 32; + } + next_gen[i] = next_gen[i] | (0x01 << xb); + global_density++; + + if (GOL_ABS(tx - x) < 8 && GOL_ABS(ty - y) < 8) local_density++; + } + } + } + + memcpy(&board, &next_gen, sizeof(next_gen)); + } + + int CountLiveNeighborsAt(int x, int y) { + int count = 0; + for (int nx = -1; nx < 2; nx++) + { + for (int ny = -1; ny < 2; ny++) + { + if (!(nx == 0 && ny == 0)) count += ValueAtCell(x + nx, y + ny); + } + } + return count; + } + + bool ValueAtCell(int x, int y) { + // Toroid operation + if (x > 63) x -= 64; + if (x < 0) x += 64; + if (y > 39) y -= 40; + if (y < 0) y += 40; + + int i = y * 2; + if (x > 31) { + i += 1; + x -= 32; + } + return ((board[i] >> x) & 0x01); + } + + void AddToBoard(int x, int y) { + int i = y * 2; + int xb = x; + if (x > 31) { + i += 1; + xb -= 32; + } + board[i] = board[i] | (0x01 << xb); + } +}; + + +//////////////////////////////////////////////////////////////////////////////// +//// Hemisphere Applet Functions +/// +/// Once you run the find-and-replace to make these refer to GameOfLife, +/// it's usually not necessary to do anything with these functions. You +/// should prefer to handle things in the HemisphereApplet child class +/// above. +//////////////////////////////////////////////////////////////////////////////// +GameOfLife GameOfLife_instance[2]; + +void GameOfLife_Start(bool hemisphere) { + GameOfLife_instance[hemisphere].BaseStart(hemisphere); +} + +void GameOfLife_Controller(bool hemisphere, bool forwarding) { + GameOfLife_instance[hemisphere].BaseController(forwarding); +} + +void GameOfLife_View(bool hemisphere) { + GameOfLife_instance[hemisphere].BaseView(); +} + +void GameOfLife_Screensaver(bool hemisphere) { + GameOfLife_instance[hemisphere].BaseScreensaverView(); +} + +void GameOfLife_OnButtonPress(bool hemisphere) { + GameOfLife_instance[hemisphere].OnButtonPress(); +} + +void GameOfLife_OnEncoderMove(bool hemisphere, int direction) { + GameOfLife_instance[hemisphere].OnEncoderMove(direction); +} + +void GameOfLife_ToggleHelpScreen(bool hemisphere) { + GameOfLife_instance[hemisphere].HelpScreen(); +} + +uint64_t GameOfLife_OnDataRequest(bool hemisphere) { + return GameOfLife_instance[hemisphere].OnDataRequest(); +} + +void GameOfLife_OnDataReceive(bool hemisphere, uint64_t data) { + GameOfLife_instance[hemisphere].OnDataReceive(data); +} diff --git a/software/o_c_REV/HEM_GateDelay.ino b/software/o_c_REV/HEM_GateDelay.ino index c7c3401f1..1bde3d03f 100644 --- a/software/o_c_REV/HEM_GateDelay.ino +++ b/software/o_c_REV/HEM_GateDelay.ino @@ -42,7 +42,7 @@ public: ForEachChannel(ch) { record(ch, Gate(ch)); - int mod_time = Proportion(DetentedIn(ch), HEMISPHERE_MAX_CV, 1000) + time[ch]; + int mod_time = Proportion(DetentedIn(ch), HEMISPHERE_MAX_INPUT_CV, 1000) + time[ch]; mod_time = constrain(mod_time, 0, 2000); bool p = play(ch, mod_time); @@ -56,19 +56,23 @@ public: } void View() { - gfxHeader(applet_name()); DrawInterface(); } void OnButtonPress() { - cursor = 1 - cursor; + CursorAction(cursor, 1); } void OnEncoderMove(int direction) { + if (!EditMode()) { + MoveCursor(cursor, direction, 1); + return; + } + if (time[cursor] > 100) direction *= 2; if (time[cursor] > 500) direction *= 2; if (time[cursor] > 1000) direction *= 2; - time[cursor] = constrain(time[cursor] += direction, 0, 2000); + time[cursor] = constrain(time[cursor] + direction, 0, 2000); } uint64_t OnDataRequest() { @@ -98,20 +102,20 @@ private: int time[2]; // Length of each channel (in ms) uint16_t location[2]; // Location of record head (playback head = location + time) uint32_t last_gate[2]; // Time of last gate, for display of icon - uint8_t cursor; + int cursor; int16_t ms_countdown; // Countdown for 1 ms void DrawInterface() { ForEachChannel(ch) { int y = 15 + (ch * 25); - if (ch == cursor) gfxCursor(0, y + 8, 63); gfxPrint(1, y, time[ch]); gfxPrint("ms"); if (OC::CORE::ticks - last_gate[ch] < 1667) gfxBitmap(54, y, 8, CLOCK_ICON); } + gfxCursor(0, 23 + (cursor * 25), 63); } /* Write the gate state into the tape at the tape head */ diff --git a/software/o_c_REV/HEM_GatedVCA.ino b/software/o_c_REV/HEM_GatedVCA.ino index d3476d51f..60822f9be 100644 --- a/software/o_c_REV/HEM_GatedVCA.ino +++ b/software/o_c_REV/HEM_GatedVCA.ino @@ -45,7 +45,6 @@ public: } void View() { - gfxHeader(applet_name()); DrawInterface(); } @@ -53,7 +52,7 @@ public: } void OnEncoderMove(int direction) { - amp_offset_pct = constrain(amp_offset_pct += direction, 0, 100); + amp_offset_pct = constrain(amp_offset_pct + direction, 0, 100); amp_offset_cv = Proportion(amp_offset_pct, 100, HEMISPHERE_MAX_CV); } diff --git a/software/o_c_REV/HEM_ICONS.ino.disabled b/software/o_c_REV/HEM_ICONS.ino.disabled index 98c692297..89f7febb1 100644 --- a/software/o_c_REV/HEM_ICONS.ino.disabled +++ b/software/o_c_REV/HEM_ICONS.ino.disabled @@ -43,7 +43,6 @@ public: } void View() { - gfxHeader(applet_name()); DrawInterface(); } @@ -116,7 +115,7 @@ private: LEFT_RIGHT_ICON, SEGMENT_ICON, WAVEFORM_ICON - }; + };gfxIcon void DrawInterface() { gfxSkyline(); diff --git a/software/o_c_REV/HEM_LoFiPCM.ino b/software/o_c_REV/HEM_LoFiPCM.ino index c6a4673d1..f2b33c677 100644 --- a/software/o_c_REV/HEM_LoFiPCM.ino +++ b/software/o_c_REV/HEM_LoFiPCM.ino @@ -19,150 +19,170 @@ // SOFTWARE. #define HEM_LOFI_PCM_BUFFER_SIZE 2048 -#define HEM_LOFI_PCM_SPEED 8 -#define LOFI_PCM2CV(S) ((uint32_t)S << 8) - 32767; +#define HEM_LOFI_PCM_SPEED 4 + +// #define CLIPLIMIT 32512 +#define CLIPLIMIT HEMISPHERE_3V_CV + +#define PCM_TO_CV(S) Proportion((int)S - 127, 127, CLIPLIMIT) +#define CV_TO_PCM(S) Proportion(constrain(S, -CLIPLIMIT, CLIPLIMIT), CLIPLIMIT, 127) + 127 + +uint8_t lofi_pcm_buffer[HEM_LOFI_PCM_BUFFER_SIZE]; class LoFiPCM : public HemisphereApplet { public: + const int length = HEM_LOFI_PCM_BUFFER_SIZE; const char* applet_name() { // Maximum 10 characters - return "LoFi Tape"; + return "LoFi Echo"; } void Start() { countdown = HEM_LOFI_PCM_SPEED; - for (int i = 0; i < HEM_LOFI_PCM_BUFFER_SIZE; i++) pcm[i] = 127; + // this might take too long, which causes crashes. It's not crucial. + //for (int i = 0; i < HEM_LOFI_PCM_BUFFER_SIZE; i++) lofi_pcm_buffer[i] = 127; + cursor = 1; //for gui } void Controller() { play = !Gate(0); // Continuously play unless gated - gated_record = Gate(1); - - countdown--; - if (countdown == 0) { - if (play || record || gated_record) head++; - if (head >= length) { - head = 0; - record = 0; - ClockOut(1); - } + fdbk_g = Gate(1) ? 100 : feedback; // Feedback = 100 when gated + + if (play) { + if (--countdown == 0) { + if (++head >= length) { + head = 0; + //ClockOut(1); + } + + int cv = SmoothedIn(0); - if (record || gated_record) { - uint32_t s = (In(0) + 32767) >> 8; - pcm[head] = (char)s; + // bitcrush the input + cv = cv >> depth; + cv = cv << depth; + + // int dt = dt_pct * length / 100; //convert delaytime to length in samples + head_w = (head + length + dt_pct*length/100) % length; //have to add the extra length to keep modulo positive in case delaytime is neg + + // mix input into the buffer ahead, respecting feedback + int fbmix = PCM_TO_CV(lofi_pcm_buffer[head]) * fdbk_g / 100 + cv; + lofi_pcm_buffer[head_w] = CV_TO_PCM(fbmix); + + rate_mod = rate; + Modulate(rate_mod, 1, 1, 64); + + countdown = rate_mod; } - uint32_t s = LOFI_PCM2CV(pcm[head]); - int SOS = In(1); // Sound-on-sound - int live = Proportion(SOS, HEMISPHERE_MAX_CV, In(0)); - int loop = play ? Proportion(HEMISPHERE_MAX_CV - SOS, HEMISPHERE_MAX_CV, s) : 0; - Out(0, live + loop); - countdown = HEM_LOFI_PCM_SPEED; + SmoothedOut(0, PCM_TO_CV(lofi_pcm_buffer[head]), (rate_mod+1)/2); + SmoothedOut(1, PCM_TO_CV(lofi_pcm_buffer[length-1 - head]), (rate_mod+1)/2); // reverse buffer! } } void View() { - gfxHeader(applet_name()); - DrawTransportBar(); - DrawWaveform(); + DrawSelector(); + if (play) DrawWaveform(); } void OnButtonPress() { - record = 1 - record; - play = 0; - head = 0; + CursorAction(cursor, 3); } void OnEncoderMove(int direction) { - length = constrain(length += (direction * 32), 32, HEM_LOFI_PCM_BUFFER_SIZE); + if (!EditMode()) { + MoveCursor(cursor, direction, 3); + return; + } + + switch (cursor) { + case 0: + dt_pct = constrain(dt_pct + direction, 0, 99); + break; + case 1: + feedback = constrain(feedback + direction, 0, 125); + break; + case 2: + rate = constrain(rate + direction, 1, 32); + break; + case 3: + depth = constrain(depth + direction, 0, 13); + break; + } } uint64_t OnDataRequest() { uint64_t data = 0; + Pack(data, PackLocation {0,7}, dt_pct); + Pack(data, PackLocation {7,7}, feedback); + Pack(data, PackLocation {14,5}, rate); + Pack(data, PackLocation {19,4}, depth); return data; } void OnDataReceive(uint64_t data) { + dt_pct = Unpack(data, PackLocation {0,7}); + feedback = Unpack(data, PackLocation {7,7}); + rate = Unpack(data, PackLocation {14,5}); + depth = Unpack(data, PackLocation {19,4}); } protected: void SetHelp() { // "------------------" <-- Size Guide - help[HEMISPHERE_HELP_DIGITALS] = "Gate 1=Pause 2=Rec"; - help[HEMISPHERE_HELP_CVS] = "1=Audio 2=SOS"; - help[HEMISPHERE_HELP_OUTS] = "A=Audio B=EOC Trg"; - help[HEMISPHERE_HELP_ENCODER] = "T=End Pt P=Rec"; + help[HEMISPHERE_HELP_DIGITALS] = "1=Pause 2= Fb=100"; + help[HEMISPHERE_HELP_CVS] = "1=Audio 2=RateMod"; + help[HEMISPHERE_HELP_OUTS] = "A=Audio B=Reverse"; + help[HEMISPHERE_HELP_ENCODER] = "Time/Fbk/Rate/Bits"; // "------------------" <-- Size Guide } private: - char pcm[HEM_LOFI_PCM_BUFFER_SIZE]; - bool record = 0; // Record activated via button - bool gated_record = 0; // Record gated via digital in - bool play = 0; - int head = 0; // Locatioon of play/record head - int countdown = HEM_LOFI_PCM_SPEED; - int length = HEM_LOFI_PCM_BUFFER_SIZE; - - void DrawTransportBar() { - DrawStop(3, 15); - DrawPlay(26, 15); - DrawRecord(50, 15); - } + bool play = 0; //play always on unless gated on Digital 1 + uint16_t head = 0; // Location of read/play head + uint16_t head_w = 0; // Location of write/record head + int8_t dt_pct = 50; //delaytime as percentage of delayline buffer + int8_t feedback = 50; + int8_t fdbk_g = feedback; + int8_t countdown = HEM_LOFI_PCM_SPEED; + uint8_t rate = HEM_LOFI_PCM_SPEED; + uint8_t rate_mod = rate; + int depth = 0; // bit reduction depth aka bitcrush + int cursor; //for gui void DrawWaveform() { - int inc = HEM_LOFI_PCM_BUFFER_SIZE / 256; - int disp[32]; - int high = 1; - int pos = head - (inc * 15) - random(1,3); // Try to center the head - if (head < 0) head += length; - for (int i = 0; i < 32; i++) + int inc = rate_mod/2 + 1; + int pos = head - (inc * 31) - random(1,3); // Try to center the head + if (pos < 0) pos += length; + for (int i = 0; i < 64; i++) { - int v = (int)pcm[pos] - 127; - if (v < 0) v = 0; - if (v > high) high = v; + int height = Proportion(127 - (int)lofi_pcm_buffer[pos], 128, 16); + gfxLine(i, 46, i, 46+height); + pos += inc; - if (pos >= HEM_LOFI_PCM_BUFFER_SIZE) pos -= length; - disp[i] = v; - } - - for (int x = 0; x < 32; x++) - { - int height = Proportion(disp[x], high, 30); - int margin = (32 - height) / 2; - gfxLine(x * 2, 30 + margin, x * 2, height + 30 + margin); + if (pos >= length) pos -= length; } } - void DrawStop(int x, int y) { - if (record || play || gated_record) gfxFrame(x, y, 11, 11); - else gfxRect(x, y, 11, 11); - } - - void DrawPlay(int x, int y) { - if (play) { - for (int i = 0; i < 11; i += 2) - { - gfxLine(x + i, y + i/2, x + i, y + 10 - i/2); - gfxLine(x + i + 1, y + i/2, x + i + 1, y + 10 - i/2); + void DrawSelector() + { + if (cursor < 2) { + for (int param = 0; param < 2; param++) { + gfxIcon(31 * param, 15, param ? GAUGE_ICON : CLOCK_ICON ); } + gfxPrint(4 + pad(100, dt_pct), 15, dt_pct); + gfxPrint(36 + pad(1000, fdbk_g), 15, fdbk_g); + gfxCursor(10 + 31 * cursor, 23, 20); } else { - gfxLine(x, y, x, y + 10); - gfxLine(x, y, x + 10, y + 5); - gfxLine(x, y + 10, x + 10, y + 5); + gfxIcon(0, 15, WAVEFORM_ICON); + gfxIcon(8, 15, BURST_ICON); + gfxIcon(22, 15, LEFT_RIGHT_ICON); + gfxPrint(30, 15, rate_mod); + gfxIcon(42, 15, UP_DOWN_ICON); + gfxPrint(50, 15, depth); + gfxCursor(30 + (cursor-2)*20, 23, 14); } } - - void DrawRecord(int x, int y) { - gfxCircle(x + 5, y + 5, 5); - if (record || gated_record) { - for (int r = 1; r < 5; r++) - { - gfxCircle(x + 5, y + 5, r); - } - } - } - + }; diff --git a/software/o_c_REV/HEM_Logic.ino b/software/o_c_REV/HEM_Logic.ino index d97542f9d..dea6cc3e4 100644 --- a/software/o_c_REV/HEM_Logic.ino +++ b/software/o_c_REV/HEM_Logic.ino @@ -49,13 +49,6 @@ public: selected = 0; operation[0] = 0; operation[1] = 2; - const char * op_name_list[] = {"AND", "OR", "XOR", "NAND", "NOR", "XNOR", "-CV-"}; - LogicGateFunction logic_gate_list[] = {hem_AND, hem_OR, hem_XOR, hem_NAND, hem_NOR, hem_XNOR, hem_null}; - for(int i = 0; i < HEMISPHERE_NUMBER_OF_LOGIC; i++) - { - op_name[i] = op_name_list[i]; - logic_gate[i] = logic_gate_list[i]; - } } void Controller() { @@ -78,17 +71,20 @@ public: } void View() { - gfxHeader(applet_name()); DrawSelector(); DrawIndicator(); } void OnButtonPress() { - selected = 1 - selected; - ResetCursor(); + CursorAction(selected, 1); } void OnEncoderMove(int direction) { + if (!EditMode()) { + MoveCursor(selected, direction, 1); + return; + } + operation[selected] += direction; if (operation[selected] == HEMISPHERE_NUMBER_OF_LOGIC) operation[selected] = 0; if (operation[selected] < 0) operation[selected] = HEMISPHERE_NUMBER_OF_LOGIC - 1; @@ -116,8 +112,8 @@ protected: } private: - const char* op_name[HEMISPHERE_NUMBER_OF_LOGIC]; - LogicGateFunction logic_gate[HEMISPHERE_NUMBER_OF_LOGIC]; + const char* op_name[HEMISPHERE_NUMBER_OF_LOGIC] = {"AND", "OR", "XOR", "NAND", "NOR", "XNOR", "-CV-"}; + LogicGateFunction logic_gate[HEMISPHERE_NUMBER_OF_LOGIC] = {hem_AND, hem_OR, hem_XOR, hem_NAND, hem_NOR, hem_XNOR, hem_null}; int operation[2]; bool result[2]; int source[2]; diff --git a/software/o_c_REV/HEM_LowerRenz.ino b/software/o_c_REV/HEM_LowerRenz.ino index 18556fc4c..3e9af5ac9 100644 --- a/software/o_c_REV/HEM_LowerRenz.ino +++ b/software/o_c_REV/HEM_LowerRenz.ino @@ -39,8 +39,8 @@ public: void Controller() { if (!Gate(1)) { // Freeze if gated - int freq_cv = Proportion(In(0), HEMISPHERE_MAX_CV, 63); - int rho_cv = Proportion(In(1), HEMISPHERE_MAX_CV, 31); + int freq_cv = Proportion(In(0), HEMISPHERE_MAX_INPUT_CV, 63); + int rho_cv = Proportion(In(1), HEMISPHERE_MAX_INPUT_CV, 31); int32_t freq_h = SCALE8_16(constrain(freq + freq_cv, 0, 255)); freq_h = USAT16(freq_h); @@ -62,18 +62,22 @@ public: } void View() { - gfxHeader(applet_name()); DrawEditor(); DrawOutput(); } void OnButtonPress() { - cursor = 1 - cursor; + CursorAction(cursor, 1); } void OnEncoderMove(int direction) { - if (cursor == 0) freq = constrain(freq += direction, 0, 255); - if (cursor == 1) rho = constrain(rho += direction, 4, 127); + if (!EditMode()) { + MoveCursor(cursor, direction, 1); + return; + } + + if (cursor == 0) freq = constrain(freq + direction, 0, 255); + if (cursor == 1) rho = constrain(rho + direction, 4, 127); } uint64_t OnDataRequest() { diff --git a/software/o_c_REV/HEM_Metronome.ino b/software/o_c_REV/HEM_Metronome.ino index a551698a0..1c5c2fc06 100644 --- a/software/o_c_REV/HEM_Metronome.ino +++ b/software/o_c_REV/HEM_Metronome.ino @@ -34,36 +34,28 @@ public: // Outputs if (clock_m->IsRunning()) { - if (clock_m->Tock()) { + if (clock_m->Tock(hemisphere*2)) { ClockOut(0); - if (clock_m->EndOfBeat()) ClockOut(1); + if (clock_m->EndOfBeat(hemisphere)) ClockOut(1); } } } void View() { - gfxHeader(applet_name()); DrawInterface(); } void OnButtonPress() { } void OnEncoderMove(int direction) { - uint16_t bpm = clock_m->GetTempo(); - bpm += direction; - clock_m->SetTempoBPM(bpm); + clock_m->SetTempoBPM(clock_m->GetTempo() + direction); } uint64_t OnDataRequest() { - uint64_t data = 0; - Pack(data, PackLocation {0,16}, clock_m->GetTempo()); - Pack(data, PackLocation {16,5}, clock_m->GetMultiply() - 1); - return data; + return 0; } void OnDataReceive(uint64_t data) { - clock_m->SetTempoBPM(Unpack(data, PackLocation {0,16})); - clock_m->SetMultiply(Unpack(data, PackLocation {16,5}) + 1); } protected: @@ -77,7 +69,6 @@ protected: } private: - int cursor; // 0=Tempo, 1=Multiply, 2=Start/Stop ClockManager *clock_m = clock_m->get(); void DrawInterface() { @@ -106,7 +97,7 @@ private: gfxCircle(40,51,1); // Winder // Pendulum arm - if (clock_m->Cycle()) gfxLine(29,50,21,31); + if (clock_m->Cycle(hemisphere)) gfxLine(29,50,21,31); else gfxLine(29,50,37,32); } diff --git a/software/o_c_REV/HEM_MixerBal.ino b/software/o_c_REV/HEM_MixerBal.ino index 71f6698cd..c6335ea0e 100644 --- a/software/o_c_REV/HEM_MixerBal.ino +++ b/software/o_c_REV/HEM_MixerBal.ino @@ -46,7 +46,6 @@ public: } void View() { - gfxHeader(applet_name()); DrawBalanceIndicator(); gfxSkyline(); } diff --git a/software/o_c_REV/HEM_MultiScale.ino b/software/o_c_REV/HEM_MultiScale.ino new file mode 100644 index 000000000..bd5a44361 --- /dev/null +++ b/software/o_c_REV/HEM_MultiScale.ino @@ -0,0 +1,199 @@ +// Copyright (c) 2023, Jakob Zerbian +// +// Based on Braids Quantizer, Copyright 2015 Émilie Gillet. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#include "braids_quantizer.h" +#include "braids_quantizer_scales.h" +#include "OC_scales.h" + +#define MS_QUANT_SCALES_COUNT 4 + +class MultiScale : public HemisphereApplet { +public: + + const char* applet_name() { + return "MultiScale"; + } + + void Start() { + // init all scales no all notes + for (uint8_t i = 0; i < MS_QUANT_SCALES_COUNT; i++) { + scale_mask[i] = 0x0001; + } + QuantizerConfigure(0, 5, scale_mask[0]); + } + + void Controller() { + + // user selected scale from input 2 + int scale = DetentedIn(1); + if (scale < 0) scale = 0; + if (scale > 0) { + scale = constrain(ProportionCV(scale, MS_QUANT_SCALES_COUNT + 1), 0, MS_QUANT_SCALES_COUNT - 1); + } + if (scale != current_scale) { + current_scale = scale; + QuantizerConfigure(0, 5, scale_mask[current_scale]); + ClockOut(1); // send clock at second output + } + + // quantize notes from input 1, can be clocked + if (Clock(0)) { + continuous = false; + StartADCLag(0); + } + + // Unclock + if (Gate(1)) { + continuous = true; + } + + if (continuous || EndOfADCLag(0)) { + int32_t quantized = Quantize(0, In(0), 0, 0); + Out(0, quantized); + } + } + + void View() { + DrawKeyboard(); + DrawIndicators(); + } + + void ToggleBit(const uint8_t bit) { + scale_mask[scale_page] ^= (0x01 << bit); // togle bit at position + if (scale_page == current_scale) { + QuantizerConfigure(0, 5, scale_mask[current_scale]); + } + } + void OnButtonPress() { + if (cursor == 0) { // scale page selection mode + CursorAction(cursor, 12); + } else { // scale note edit mode + const uint8_t bit = cursor - 1; + ToggleBit(bit); + } + + } + + void OnEncoderMove(int direction) { + if (!EditMode()) { + // move cursor along the "keyboard" + MoveCursor(cursor, direction, 12); + return; + } + + // select page + scale_page = constrain(scale_page + direction, 0, MS_QUANT_SCALES_COUNT - 1); + } + + uint64_t OnDataRequest() { + uint64_t data = 0; + Pack(data, PackLocation { 0, 12}, scale_mask[0]); + Pack(data, PackLocation {12, 12}, scale_mask[1]); + Pack(data, PackLocation {24, 12}, scale_mask[2]); + Pack(data, PackLocation {36, 12}, scale_mask[3]); + return data; + } + + void OnDataReceive(uint64_t data) { + scale_mask[0] = Unpack(data, PackLocation { 0, 12}); + scale_mask[1] = Unpack(data, PackLocation {12, 12}); + scale_mask[2] = Unpack(data, PackLocation {24, 12}); + scale_mask[3] = Unpack(data, PackLocation {36, 12}); + QuantizerConfigure(0, 5, scale_mask[0]); + } + +protected: + void SetHelp() { + // "------------------" <-- Size Guide + help[HEMISPHERE_HELP_DIGITALS] = "1=Clock 2=Unclock"; + help[HEMISPHERE_HELP_CVS] = "1=CV 2=Scale"; + help[HEMISPHERE_HELP_OUTS] = "A=Pitch B=Gate"; + help[HEMISPHERE_HELP_ENCODER] = "Edit Scales"; + // "------------------" <-- Size Guide + } + +private: + int cursor = 0; + bool continuous = true; + uint16_t scale_mask[MS_QUANT_SCALES_COUNT]; + + int8_t current_scale = 0; + int8_t scale_page = 0; + + void DrawKeyboard() { + gfxFrame(4, 27, 56, 32); + + // white keys + for (uint8_t x = 0; x < 7; x++) { + gfxLine(x * 8 + 4, 27, x*8 + 4, 58); + } + + // black keys + for (uint8_t i = 0; i < 6; i++) { + if (i != 2) { + uint8_t x = (i * 8) + 10; + gfxRect(x, 27, 5, 16); + } + } + + gfxBitmap(1, 14, 8, PLAY_ICON); + gfxPrint(12, 15, current_scale + 1); + + gfxBitmap(32, 14, 8, EDIT_ICON); + gfxPrint(43, 15, scale_page + 1); + + } + + void DrawIndicators() { + if (cursor == 0) { + // draw cursor at scale page text + gfxCursor(31, 23, 20); + } + uint8_t x[12] = {2, 7, 10, 15, 18, 26, 31, 34, 39, 42, 47, 50}; + uint8_t p[12] = {0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0}; + for (uint8_t i = 0; i < 12; i++) { + if ((scale_mask[scale_page] >> i) & 0x01) gfxInvert(x[i] + 4, (p[i] ? 37 : 51), 4 - p[i], 4 - p[i]); + + if (i == (cursor - 1)) gfxCursor(x[i] + 3, p[i] ? 25 : 60, 6); + } + } +}; + + +//////////////////////////////////////////////////////////////////////////////// +//// Hemisphere Applet Functions +/// +/// Once you run the find-and-replace to make these refer to MultiScale, +/// it's usually not necessary to do anything with these functions. You +/// should prefer to handle things in the HemisphereApplet child class +/// above. +//////////////////////////////////////////////////////////////////////////////// +MultiScale MultiScale_instance[2]; + +void MultiScale_Start(bool hemisphere) {MultiScale_instance[hemisphere].BaseStart(hemisphere);} +void MultiScale_Controller(bool hemisphere, bool forwarding) {MultiScale_instance[hemisphere].BaseController(forwarding);} +void MultiScale_View(bool hemisphere) {MultiScale_instance[hemisphere].BaseView();} +void MultiScale_OnButtonPress(bool hemisphere) {MultiScale_instance[hemisphere].OnButtonPress();} +void MultiScale_OnEncoderMove(bool hemisphere, int direction) {MultiScale_instance[hemisphere].OnEncoderMove(direction);} +void MultiScale_ToggleHelpScreen(bool hemisphere) {MultiScale_instance[hemisphere].HelpScreen();} +uint64_t MultiScale_OnDataRequest(bool hemisphere) {return MultiScale_instance[hemisphere].OnDataRequest();} +void MultiScale_OnDataReceive(bool hemisphere, uint64_t data) {MultiScale_instance[hemisphere].OnDataReceive(data);} diff --git a/software/o_c_REV/HEM_Palimpsest.ino b/software/o_c_REV/HEM_Palimpsest.ino index 2ef3616b8..dbcb65cd9 100644 --- a/software/o_c_REV/HEM_Palimpsest.ino +++ b/software/o_c_REV/HEM_Palimpsest.ino @@ -80,21 +80,31 @@ public: } void View() { - gfxHeader(applet_name()); DrawControls(); DrawSequence(); } void OnButtonPress() { - cursor++; - if (cursor > 2) cursor = 0; - ResetCursor(); + CursorAction(cursor, 2); } void OnEncoderMove(int direction) { - if (cursor == 0) compose = constrain(compose += direction, 0, HEM_PALIMPSEST_MAX_VALUE); - if (cursor == 1) decompose = constrain(decompose -= direction, 0, HEM_PALIMPSEST_MAX_VALUE); - if (cursor == 2) length = constrain(length += direction, 2, 16); + if (!EditMode()) { + MoveCursor(cursor, direction, 2); + return; + } + + switch (cursor) { + case 0: + compose = constrain(compose + direction, 0, HEM_PALIMPSEST_MAX_VALUE); + break; + case 1: + decompose = constrain(decompose - direction, 0, HEM_PALIMPSEST_MAX_VALUE); + break; + case 2: + length = constrain(length + direction, 2, 16); + break; + } ResetCursor(); } diff --git a/software/o_c_REV/HEM_Pigeons.ino b/software/o_c_REV/HEM_Pigeons.ino new file mode 100644 index 000000000..1bf559ff9 --- /dev/null +++ b/software/o_c_REV/HEM_Pigeons.ino @@ -0,0 +1,219 @@ +// Copyright (c) 2023, Nicholas J. Michalek +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// Hijacking ProbDiv triggers, if available, by pretending to be ProbMeloD ;) +#include "HSProbLoopLinker.h" + +class Pigeons : public HemisphereApplet { +public: + ProbLoopLinker *loop_linker = loop_linker->get(); + + enum PigeonCursor { + CHAN1_V1, CHAN1_V2, + CHAN1_MOD, + CHAN2_V1, CHAN2_V2, + CHAN2_MOD, + SCALE, ROOT_NOTE, + + CURSOR_LAST = ROOT_NOTE + }; + + const char* applet_name() { + return "Pigeons"; + } + + void Start() { + ForEachChannel(ch) { + pigeons[ch].mod = 7 + ch*3; + QuantizerConfigure(ch, scale); + } + } + + void Controller() { + loop_linker->RegisterMelo(hemisphere); + + ForEachChannel(ch) + { + // CV modulation of modulo value + pigeons[ch].mod_v = pigeons[ch].mod; + Modulate(pigeons[ch].mod_v, ch, 1, 64); + + if (loop_linker->TrigPop(ch) || Clock(ch)) { + int signal = QuantizerLookup(ch, pigeons[ch].Bump() + 64) + (root_note << 7); + Out(ch, signal); + + pigeons[ch].pulse = HEMISPHERE_PULSE_ANIMATION_TIME_LONG; + } + if (pigeons[ch].pulse) --pigeons[ch].pulse; + } + } + + void View() { + DrawInterface(); + } + + void OnButtonPress() { + CursorAction(cursor, CURSOR_LAST); + } + + void OnEncoderMove(int direction) { + if (!EditMode()) { + MoveCursor(cursor, direction, CURSOR_LAST); + return; + } + + // param LUT + const struct { uint8_t &p; int min, max; } params[] = { + { pigeons[0].val[0], 0, 63 }, // CHAN1_V1 + { pigeons[0].val[1], 0, 63 }, // CHAN1_V2 + { pigeons[0].mod, 1, 64 }, // CHAN1_MOD + { pigeons[1].val[0], 0, 63 }, // CHAN2_V1 + { pigeons[1].val[1], 0, 63 }, // CHAN2_V2 + { pigeons[1].mod, 1, 64 }, // CHAN2_MOD + { root_note, 0, 11 }, // DUMMY? + { root_note, 0, 11 }, // ROOT NOTE + }; + + if (cursor == SCALE) { + scale += direction; + if (scale >= OC::Scales::NUM_SCALES) scale = 0; + if (scale < 0) scale = OC::Scales::NUM_SCALES - 1; + QuantizerConfigure(0, scale); + QuantizerConfigure(1, scale); + + return; + } + + params[cursor].p = constrain(params[cursor].p + direction, params[cursor].min, params[cursor].max); + } + + uint64_t OnDataRequest() { + uint64_t data = 0; + Pack(data, PackLocation {0,6}, pigeons[0].val[0]); + Pack(data, PackLocation {6,6}, pigeons[0].val[1]); + Pack(data, PackLocation {12,6}, pigeons[0].mod - 1); + Pack(data, PackLocation {18,6}, pigeons[1].val[0]); + Pack(data, PackLocation {24,6}, pigeons[1].val[1]); + Pack(data, PackLocation {30,6}, pigeons[1].mod - 1); + Pack(data, PackLocation {36,8}, scale); + Pack(data, PackLocation {44,4}, root_note); + return data; + } + + void OnDataReceive(uint64_t data) { + pigeons[0].val[0] = Unpack(data, PackLocation {0,6}); + pigeons[0].val[1] = Unpack(data, PackLocation {6,6}); + pigeons[0].mod = Unpack(data, PackLocation {12,6}) + 1; + pigeons[1].val[0] = Unpack(data, PackLocation {18,6}); + pigeons[1].val[1] = Unpack(data, PackLocation {24,6}); + pigeons[1].mod = Unpack(data, PackLocation {30,6}) + 1; + scale = Unpack(data, PackLocation {36,8}); + root_note = Unpack(data, PackLocation {44,4}); + } + +protected: + void SetHelp() { + // "------------------" <-- Size Guide + help[HEMISPHERE_HELP_DIGITALS] = "Clock 1,2"; + help[HEMISPHERE_HELP_CVS] = "Modulo 1,2"; + help[HEMISPHERE_HELP_OUTS] = "Pitch 1,2"; + help[HEMISPHERE_HELP_ENCODER] = "Params"; + // "------------------" <-- Size Guide + } + +private: + int cursor; + struct Pigeon { + bool index; + uint8_t val[2] = {1, 2}; + uint8_t mod = 10; + uint8_t mod_v = 10; + uint32_t pulse; + + uint8_t Get() { return val[index]; } + uint8_t Bump() { + val[index] = (val[0] + val[1]) % mod_v; + index = !index; + return val[index]; + } + } pigeons[2]; + + int scale = 16; // Pentatonic minor + uint8_t root_note = 4; // key of E + + void DrawInterface() { + // cursor LUT + const struct { uint8_t x, y, w; } cur[CURSOR_LAST+1] = { + { 1, 22, 13 }, // val1 + { 25, 22, 13 }, // val2 + { 49, 22, 13 }, // mod + + { 1, 43, 13 }, // val1 + { 25, 43, 13 }, // val2 + { 49, 43, 13 }, // mod + + { 10, 63, 25 }, // scale + { 40, 63, 13 }, // root note + }; + + ForEachChannel(ch) { + int y = 14+ch*21; + gfxPrint(1 + pad(10, pigeons[ch].val[0]), y, pigeons[ch].val[0]); + gfxPrint(16, y, "+"); + gfxPrint(25 + pad(10, pigeons[ch].val[1]), y, pigeons[ch].val[1]); + gfxPrint(40, y, "%"); + gfxPrint(49, y, pigeons[ch].mod_v); + + y += 10; + gfxIcon( 5+(pigeons[ch].index ? 24 : 0), y, NOTE_ICON); + gfxIcon( 16, y, pigeons[ch].pulse ? SINGING_PIGEON_ICON : SILENT_PIGEON_ICON ); + + y += 8; + gfxLine(4, y, 54, y); // ---------------------------------- // + } + + gfxPrint(10, 55, OC::scale_names_short[scale]); + gfxPrint(40, 55, OC::Strings::note_names_unpadded[root_note]); + + // i'm proud of this one: + gfxCursor( cur[cursor].x, cur[cursor].y, cur[cursor].w ); + } + +}; + + +//////////////////////////////////////////////////////////////////////////////// +//// Hemisphere Applet Functions +/// +/// Once you run the find-and-replace to make these refer to Pigeons, +/// it's usually not necessary to do anything with these functions. You +/// should prefer to handle things in the HemisphereApplet child class +/// above. +//////////////////////////////////////////////////////////////////////////////// +Pigeons Pigeons_instance[2]; + +void Pigeons_Start(bool hemisphere) {Pigeons_instance[hemisphere].BaseStart(hemisphere);} +void Pigeons_Controller(bool hemisphere, bool forwarding) {Pigeons_instance[hemisphere].BaseController(forwarding);} +void Pigeons_View(bool hemisphere) {Pigeons_instance[hemisphere].BaseView();} +void Pigeons_OnButtonPress(bool hemisphere) {Pigeons_instance[hemisphere].OnButtonPress();} +void Pigeons_OnEncoderMove(bool hemisphere, int direction) {Pigeons_instance[hemisphere].OnEncoderMove(direction);} +void Pigeons_ToggleHelpScreen(bool hemisphere) {Pigeons_instance[hemisphere].HelpScreen();} +uint64_t Pigeons_OnDataRequest(bool hemisphere) {return Pigeons_instance[hemisphere].OnDataRequest();} +void Pigeons_OnDataReceive(bool hemisphere, uint64_t data) {Pigeons_instance[hemisphere].OnDataReceive(data);} diff --git a/software/o_c_REV/HEM_ProbabilityDivider.ino b/software/o_c_REV/HEM_ProbabilityDivider.ino index 9e4f66f00..b1bbba27c 100644 --- a/software/o_c_REV/HEM_ProbabilityDivider.ino +++ b/software/o_c_REV/HEM_ProbabilityDivider.ino @@ -26,6 +26,12 @@ class ProbabilityDivider : public HemisphereApplet { public: + enum ProbDivCursor { + WEIGHT1, WEIGHT2, WEIGHT4, WEIGHT8, + LOOP_LENGTH, + LAST_SETTING = LOOP_LENGTH + }; + const char* applet_name() { return "ProbDiv"; } @@ -48,13 +54,13 @@ public: loop_linker->RegisterDiv(hemisphere); // CV 1 control over loop length - int lengthCv = DetentedIn(0); - if (lengthCv < 0) loop_length = 0; - if (lengthCv > 0) { - loop_length = constrain(ProportionCV(lengthCv, HEM_PROB_DIV_MAX_LOOP_LENGTH + 1), 0, HEM_PROB_DIV_MAX_LOOP_LENGTH); + loop_length_mod = loop_length; + if (DetentedIn(0)) { + Modulate(loop_length_mod, 0, 0, HEM_PROB_DIV_MAX_LOOP_LENGTH); + // TODO: regenerate if changing from 0? } - loop_linker->SetLooping(loop_length > 0); + loop_linker->SetLooping(loop_length_mod > 0); // reset if (Clock(1)) { @@ -79,7 +85,7 @@ public: } // reset loop - if (loop_length > 0 && loop_step >= loop_length) { + if (loop_length_mod > 0 && loop_step >= loop_length_mod) { loop_step = 0; loop_index = 0; skip_steps = 0; @@ -90,15 +96,16 @@ public: // continue with active division if (--skip_steps > 0) { - if (loop_length > 0) { + if (loop_length_mod > 0) { loop_step++; } ClockOut(1); + loop_linker->Trigger(1); return; } // get next weighted div or next div from loop - if (loop_length > 0) { + if (loop_length_mod > 0) { skip_steps = GetNextLoopDiv(); } else { skip_steps = GetNextWeightedDiv(); @@ -110,7 +117,7 @@ public: } ClockOut(0); - loop_linker->Trigger(); + loop_linker->Trigger(0); pulse_animation = HEMISPHERE_PULSE_ANIMATION_TIME; } @@ -126,28 +133,37 @@ public: } void View() { - gfxHeader(applet_name()); DrawInterface(); } void OnButtonPress() { - if (++cursor > 4) cursor = 0; + CursorAction(cursor, LAST_SETTING); } void OnEncoderMove(int direction) { - if (cursor == 0) weight_1 = constrain(weight_1 += direction, 0, HEM_PROB_DIV_MAX_WEIGHT); - if (cursor == 1) weight_2 = constrain(weight_2 += direction, 0, HEM_PROB_DIV_MAX_WEIGHT); - if (cursor == 2) weight_4 = constrain(weight_4 += direction, 0, HEM_PROB_DIV_MAX_WEIGHT); - if (cursor == 3) weight_8 = constrain(weight_8 += direction, 0, HEM_PROB_DIV_MAX_WEIGHT); - if (cursor == 4) { + if (!EditMode()) { + MoveCursor(cursor, direction, LAST_SETTING); + return; + } + + switch ((ProbDivCursor)cursor) { + case WEIGHT1: weight_1 = constrain(weight_1 + direction, 0, HEM_PROB_DIV_MAX_WEIGHT); break; + case WEIGHT2: weight_2 = constrain(weight_2 + direction, 0, HEM_PROB_DIV_MAX_WEIGHT); break; + case WEIGHT4: weight_4 = constrain(weight_4 + direction, 0, HEM_PROB_DIV_MAX_WEIGHT); break; + case WEIGHT8: weight_8 = constrain(weight_8 + direction, 0, HEM_PROB_DIV_MAX_WEIGHT); break; + case LOOP_LENGTH: { int old = loop_length; - loop_length = constrain(loop_length += direction, 0, HEM_PROB_DIV_MAX_LOOP_LENGTH); + loop_length = constrain(loop_length + direction, 0, HEM_PROB_DIV_MAX_LOOP_LENGTH); if (old == 0 && loop_length > 0) { // seed loop GenerateLoop(true); } + break; } - if (cursor < 4 && loop_length > 0) { + default: break; + } + + if (cursor < LOOP_LENGTH && loop_length > 0) { GenerateLoop(false); } } @@ -187,12 +203,12 @@ protected: } private: - int cursor; + int cursor; // ProbDivCursor int weight_1; int weight_2; int weight_4; int weight_8; - int loop_length; + int loop_length, loop_length_mod; int loop[HEM_PROB_DIV_MAX_LOOP_LENGTH]; int loop_index; int loop_step; @@ -229,12 +245,12 @@ private: if (reseed_animation > 0) { gfxInvert(4, 55, 12, 8); } - if (loop_length == 0) { + if (loop_length_mod == 0) { gfxPrint(19, 55, "off"); } else { - gfxPrint(19, 55, loop_length); + gfxPrint(19, 55, loop_length_mod); } - if (cursor == 4) gfxCursor(19, 63, 18); + if (cursor == LOOP_LENGTH) gfxCursor(19, 63, 18); if (reset_animation > 0) { gfxPrint(52, 55, "R"); @@ -243,9 +259,10 @@ private: void DrawKnobAt(byte x, byte y, byte len, byte value, bool is_cursor) { byte p = is_cursor ? 1 : 3; - byte w = Proportion(value, HEM_PROB_DIV_MAX_WEIGHT, len); - gfxDottedLine(x, y + 4, x + len, y + 4, p); + byte w = Proportion(value, HEM_PROB_DIV_MAX_WEIGHT, len-1); + gfxDottedLine(x, y + 3, x + len, y + 3, p); gfxRect(x + w, y, 2, 7); + if (EditMode() && is_cursor) gfxInvert(x-1, y, len+3, 7); } int GetNextWeightedDiv() { diff --git a/software/o_c_REV/HEM_ProbabilityMelody.ino b/software/o_c_REV/HEM_ProbabilityMelody.ino index 49a7e7d00..6e93de16b 100644 --- a/software/o_c_REV/HEM_ProbabilityMelody.ino +++ b/software/o_c_REV/HEM_ProbabilityMelody.ino @@ -27,6 +27,12 @@ class ProbabilityMelody : public HemisphereApplet { public: + enum ProbMeloCursor { + LOWER, UPPER, + FIRST_NOTE = 2, + LAST_NOTE = 13 + }; + const char* applet_name() { return "ProbMeloD"; } @@ -40,50 +46,45 @@ public: void Controller() { loop_linker->RegisterMelo(hemisphere); - // looping enabled from ProbDiv - if (loop_linker->IsLooping() && !isLooping) { - isLooping = true; - GenerateLoop(); - } - - // reseed from ProbDiv - if (loop_linker->ShouldReseed()) { - GenerateLoop(); - } - - isLooping = loop_linker->IsLooping(); - + // stash these to check for regen + int oldDown = down_mod; + int oldUp = up_mod; - int downCv = DetentedIn(0); - int oldDown = down; - if (downCv < 0) down = 1; - if (downCv > 0) { - down = constrain(ProportionCV(downCv, HEM_PROB_MEL_MAX_RANGE + 1), 1, up); - } + // CV modulation + down_mod = down; + up_mod = up; + // down scales to the up setting + Modulate(down_mod, 0, 1, up); + // up scales full range, with down value as a floor + Modulate(up_mod, 1, down_mod, HEM_PROB_MEL_MAX_RANGE); - int upCv = DetentedIn(1); - int oldUp = up; - if (upCv < 0) up = 1; - if (upCv > 0) { - up = constrain(ProportionCV(upCv, HEM_PROB_MEL_MAX_RANGE + 1), down, HEM_PROB_MEL_MAX_RANGE); - } + // regen when looping was enabled from ProbDiv + bool regen = loop_linker->IsLooping() && !isLooping; + isLooping = loop_linker->IsLooping(); + // reseed from ProbDiv + regen = regen || loop_linker->ShouldReseed(); + // reseed loop if range has changed due to CV - if (isLooping && (oldDown != down || oldUp != up)) { + regen = regen || (isLooping && (down_mod != oldDown || up_mod != oldUp)); + + if (regen) { GenerateLoop(); } - if (Clock(0) || loop_linker->Ready()) { - if (isLooping) { - pitch = loop[loop_linker->GetLoopStep()] + 60; - } else { - pitch = GetNextWeightedPitch() + 60; - } - if (pitch != -1) { - Out(0, MIDIQuantizer::CV(pitch)); - pulse_animation = HEMISPHERE_PULSE_ANIMATION_TIME_LONG; - } else { - Out(0, 0); + ForEachChannel(ch) { + if (loop_linker->TrigPop(ch) || Clock(ch)) { + if (isLooping) { + pitch = seqloop[ch][loop_linker->GetLoopStep()] + 60; + } else { + pitch = GetNextWeightedPitch() + 60; + } + if (pitch != -1) { + Out(ch, MIDIQuantizer::CV(pitch)); + pulse_animation = HEMISPHERE_PULSE_ANIMATION_TIME_LONG; + } else { + Out(ch, 0); + } } } @@ -98,35 +99,32 @@ public: } void View() { - gfxHeader(applet_name()); DrawParams(); DrawKeyboard(); } void OnButtonPress() { - isEditing = !isEditing; - ResetCursor(); + CursorAction(cursor, LAST_NOTE); } void OnEncoderMove(int direction) { - if (!isEditing) { - cursor += direction; - if (cursor < 0) cursor = 13; - if (cursor > 13) cursor = 0; - ResetCursor(); + if (!EditMode()) { + MoveCursor(cursor, direction, LAST_NOTE); + return; + } + + if (cursor >= FIRST_NOTE) { + // editing note probability + int i = cursor - FIRST_NOTE; + weights[i] = constrain(weights[i] + direction, 0, HEM_PROB_MEL_MAX_WEIGHT); + value_animation = HEMISPHERE_CURSOR_TICKS; } else { - if (cursor < 12) { - // editing note probability - weights[cursor] = constrain(weights[cursor] += direction, 0, HEM_PROB_MEL_MAX_WEIGHT); - value_animation = HEMISPHERE_CURSOR_TICKS; - } else { - // editing scaling - if (cursor == 12) down = constrain(down += direction, 1, up); - if (cursor == 13) up = constrain(up += direction, down, 60); - } - if (isLooping) { - GenerateLoop(); // regenerate loop on any param changes - } + // editing scaling + if (cursor == LOWER) down = constrain(down + direction, 1, up); + if (cursor == UPPER) up = constrain(up + direction, down, 60); + } + if (isLooping) { + GenerateLoop(); // regenerate loop on any param changes } } @@ -169,22 +167,21 @@ public: protected: void SetHelp() { // "------------------" <-- Size Guide - help[HEMISPHERE_HELP_DIGITALS] = "1=Clock"; + help[HEMISPHERE_HELP_DIGITALS] = "1=ClockA 2=ClockB"; help[HEMISPHERE_HELP_CVS] = "1=LowRng 2=HighRng"; - help[HEMISPHERE_HELP_OUTS] = "A=Out"; + help[HEMISPHERE_HELP_OUTS] = "A,B=Pitch out"; help[HEMISPHERE_HELP_ENCODER] = "Push to edit value"; // "------------------" <-- Size Guide } private: - int cursor; - bool isEditing = false; + int cursor; // ProbMeloCursor int weights[12] = {10,0,0,2,0,0,0,2,0,0,4,0}; - int up; - int down; + int up, up_mod; + int down, down_mod; int pitch; bool isLooping = false; - int loop[HEM_PROB_MEL_MAX_LOOP_LENGTH]; + int seqloop[2][HEM_PROB_MEL_MAX_LOOP_LENGTH]; ProbLoopLinker *loop_linker = loop_linker->get(); @@ -197,12 +194,12 @@ private: int GetNextWeightedPitch() { int total_weights = 0; - for(int i = down-1; i < up; i++) { + for(int i = down_mod-1; i < up_mod; i++) { total_weights += weights[i % 12]; } int rnd = random(0, total_weights + 1); - for(int i = down-1; i < up; i++) { + for(int i = down_mod-1; i < up_mod; i++) { int weight = weights[i % 12]; if (rnd <= weight && weight > 0) { return i; @@ -215,7 +212,8 @@ private: void GenerateLoop() { // always fill the whole loop to make things easier for (int i = 0; i < HEM_PROB_MEL_MAX_LOOP_LENGTH; i++) { - loop[i] = GetNextWeightedPitch(); + seqloop[0][i] = GetNextWeightedPitch(); + seqloop[1][i] = GetNextWeightedPitch(); } } @@ -255,7 +253,7 @@ private: if (pulse_animation > 0 && note == i) { gfxRect(xOffset - 1, yOffset, 3, 10); } else { - if (isEditing && i == cursor) { + if (EditMode() && i == (cursor - FIRST_NOTE)) { // blink line when editing if (CursorBlink()) { gfxLine(xOffset, yOffset, xOffset, yOffset + 10); @@ -270,31 +268,28 @@ private: } // cursor for keys - if (!isEditing) { - if (cursor < 12) { - gfxCursor(x[cursor] - 1, p[cursor] ? 24 : 60, p[cursor] ? 5 : 6); - gfxCursor(x[cursor] - 1, p[cursor] ? 25 : 61, p[cursor] ? 5 : 6); + if (!EditMode()) { + if (cursor >= FIRST_NOTE) { + int i = cursor - FIRST_NOTE; + gfxCursor(x[i] - 1, p[i] ? 24 : 60, p[i] ? 5 : 6); + gfxCursor(x[i] - 1, p[i] ? 25 : 61, p[i] ? 5 : 6); } - if (cursor == 12) gfxCursor(7, 23, 22); - if (cursor == 13) gfxCursor(37, 23, 22); } // scaling params gfxIcon(0, 13, DOWN_BTN_ICON); - gfxPrint(8, 15, ((down - 1) / 12) + 1); + gfxPrint(8, 15, ((down_mod - 1) / 12) + 1); gfxPrint(13, 15, "."); - gfxPrint(17, 15, ((down - 1) % 12) + 1); + gfxPrint(17, 15, ((down_mod - 1) % 12) + 1); gfxIcon(30, 16, UP_BTN_ICON); - gfxPrint(38, 15, ((up - 1) / 12) + 1); + gfxPrint(38, 15, ((up_mod - 1) / 12) + 1); gfxPrint(43, 15, "."); - gfxPrint(47, 15, ((up - 1) % 12) + 1); + gfxPrint(47, 15, ((up_mod - 1) % 12) + 1); - if (isEditing) { - if (cursor == 12) gfxInvert(9, 14, 21, 9); - if (cursor == 13) gfxInvert(38, 14, 21, 9); - } + if (cursor == LOWER) gfxCursor(9, 23, 21); + if (cursor == UPPER) gfxCursor(39, 23, 21); if (pulse_animation > 0) { // int note = pitch % 12; @@ -304,15 +299,17 @@ private: gfxRect(58, 54 - (octave * 6), 3, 3); } - if (value_animation > 0 && cursor < 12) { + if (value_animation > 0 && cursor >= FIRST_NOTE) { + int i = cursor - FIRST_NOTE; + gfxRect(1, 15, 60, 10); gfxInvert(1, 15, 60, 10); - gfxPrint(18, 16, n[cursor]); - if (p[cursor]) { + gfxPrint(18, 16, n[i]); + if (p[i]) { gfxPrint(24, 16, "#"); } - gfxPrint(34, 16, weights[cursor]); + gfxPrint(34, 16, weights[i]); gfxInvert(1, 15, 60, 10); } } diff --git a/software/o_c_REV/HEM_Relabi.ino b/software/o_c_REV/HEM_Relabi.ino new file mode 100644 index 000000000..ed1b01805 --- /dev/null +++ b/software/o_c_REV/HEM_Relabi.ino @@ -0,0 +1,652 @@ +// Copyright (c) 2024, Samuel Burt +// Relabi code by Samuel Burt based on a concept by John Berndt. +// +// Copyright (c) 2018, Jason Justian +// Jason Justian created the original Ornament & Crime Hemisphere applets. +// This code is built on his initial work and from an app template. +// +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#include "vector_osc/HSVectorOscillator.h" +#include "vector_osc/WaveformManager.h" +#include "HSRelabiManager.h" +#include "HSicons.h" + +class Relabi : public HemisphereApplet { + +public: + + const char* applet_name() { + return "Relabi"; + } + + void Start() { + freq[0] = 120; + freq[1] = 360; + freq[2] = 600; + freq[3] = 840; + xmod[0] = 0; + xmod[1] = 0; + xmod[2] = 0; + xmod[3] = 0; + xmodplus = 0; + freqLinkMul = 1; + freqLinkDiv = 1; + freqKnobMul = 1; + freqKnobDiv = 1; + bipolar = true; + for (uint8_t i = 0; i < 4; i++) { + lfoThreshold[i] = 0; // Default threshold is 0% + } + outputAssign[0] = 0; // Default to LFO1 + outputAssign[1] = 1; // Default to LFO2 + + + + for (uint8_t count = 0; count < 4; count++) { + if (freq[count] > 2000) {freqKnob[count] = (freq[count] - 2000) / 100 + 380;} + if (freq[count] < 2000) {freqKnob[count] = (freq[count] - 200) / 10 + 200;} + if (freq[count] < 200) {freqKnob[count] = freq[count];} + xmodKnob[count] = xmod[count]; + osc[count] = WaveformManager::VectorOscillatorFromWaveform(35); + osc[count].SetFrequency(freq[count]); + + } + } + + void Controller() { + + RelabiManager * manager = RelabiManager::get(); + manager->RegisterRelabi(hemisphere); + linked = manager->IsLinked(); + int wave1; + int wave2; + uint8_t clkCalc; + + + if (LEFT_HEMISPHERE) {clkCalc = 1;} + if (RIGHT_HEMISPHERE) {clkCalc = 3;} + + if (Clock(0) && oldClock == 0) { // Rising edge detected on TRIG1 input + for (uint8_t pcount = 0; pcount < 4; pcount++) { + // Get the total number of segments in the waveform + // byte totalSegments = osc[pcount].GetSegment(0).Segments(); // Use first segment's TOC + + // Calculate phase position based on phase percentage (0–100) + //int setPhase = round((phase[pcount] / 100.0) * totalSegments); + + // Reset the phase of the oscillator + //osc[pcount].SetPhase(setPhase); + osc[pcount].Reset(); + } + } + + //} + oldClock = Clock(0); + + if (clkDiv == clkCalc) { + + + + + + + + + if (linked && hemisphere == RIGHT_HEMISPHERE) { + + + + + // Linked: Receive lfo values from RelabiManager + manager->ReadValues(sample[0], sample[1], sample[2], sample[3]); + wave1 = (static_cast(sample[2])); + wave2 = (static_cast(sample[3])); + bool receivedGateStates[4]; + manager->ReadGates(receivedGateStates); + for (int i = 0; i < 4; i++) { + gateState[i] = receivedGateStates[i]; + + } + + + + + } else { + + cvIn = (In(0))/51.15; + cvIn2 = (In(1))/51.15; + + float normalizedCV = ((cvIn / 100.0) + 1.0) / 2.0; + cvIn = 10000.0 * normalizedCV; //set range for cvIn to be 0 to 10000 + + float normalizedCV2 = ((cvIn2 / 100.0) + 1.0) / 2.0; + cvIn2 = 10000.0 * normalizedCV2; //set range for cvIn to be 0 to 10000 + + + + // frequency modulation: + for (uint8_t lfo = 0; lfo < 4; lfo++) { + osc[lfo].SetScale(HEMISPHERE_3V_CV); + + // Use freqKnobMul and freqKnobDiv to adjust frequency + float globalFreqFactor = static_cast(freqKnobMul) / static_cast(freqKnobDiv); + + + // Calculate gate outputs based on thresholds + if (sample[lfo] >= (lfoThreshold[lfo] * HEMISPHERE_MAX_CV) / 200) { + // Gate is high + gateState[lfo] = true; + } else if (sample[lfo] < (lfoThreshold[lfo] * HEMISPHERE_MAX_CV) / 200) { + // Signal is below or equal to the threshold, clear the gate + gateState[lfo] = false; + } + + // Normalize the CV input to a range between 0 and 1 + float normalizedCV = static_cast(cvIn) / 2500.0; // Adjust divisor if needed based on CV range + + // Incorporate CV2 with cross-modulation + float xmodCombo = xmodplus + xmod[lfo] + (cvIn2 / 100) - 50; + + // Calculate cross-frequency modulation factor + float crossFreqMod = (static_cast(xmodCombo) / 100.0) * (static_cast(sample[(lfo + 3) % 4]) / HEMISPHERE_3V_CV); + + // Combine base frequency, cross-modulation, and CV input + float modulatedFreq = normalizedCV * globalFreqFactor * (freq[lfo] + (freq[lfo] * crossFreqMod)); + + // float modulatedFreq = freq[lfo] + (freq[lfo] * crossFreqMod * normalizedCV); + + // Ensure the frequency stays within valid bounds + modulatedFreq = constrain(modulatedFreq, 0.01, 15000.0); // Example: limit to 1 Hz to 20 kHz + + // Set the modulated frequency to the oscillator + osc[lfo].SetFrequency(modulatedFreq * 15); + + // Update sample + sample[lfo] = osc[lfo].Next(); + + // Store the display frequency for visualization + displayFreq[lfo] = modulatedFreq; + } + + + if (manager->IsLinked() && hemisphere == LEFT_HEMISPHERE) { + + // Linked: Send lfo values to RelabiManager + manager->WriteValues(sample[0], sample[1], sample[2], sample[3]); + // Linked: Send gate values to RelabiManager + manager->WriteGates(gateState); + } + + if (manager->IsLinked() && hemisphere == RIGHT_HEMISPHERE) { + manager->ReadValues(sample[0], sample[1], sample[2], sample[3]); + manager->ReadGates(gateState); + } + + // CV1 outputs LFO1 // CV2 outputs LFO2 + + + wave1 = (static_cast(sample[0]) /*+ HEMISPHERE_CENTER_CV + HEMISPHERE_3V_CV*/); + wave2 = (static_cast(sample[1]) /*+ HEMISPHERE_CENTER_CV + HEMISPHERE_3V_CV*/); + + } + } + + + + if (!bipolar) { + wave1 = wave1 + HEMISPHERE_CENTER_CV + HEMISPHERE_3V_CV; + wave2 = wave2 + HEMISPHERE_CENTER_CV + HEMISPHERE_3V_CV; + } else { + wave1 = wave1; + wave2 = wave2; + } + + // Set outputs based on assignments + int outputA = GetOutputValue(outputAssign[0]); + int outputB = GetOutputValue(outputAssign[1]); + Out(0, outputA); + Out(1, outputB); + + // Out(0, wave1); + // Out(1, wave2); + + clkDiv++; + clkDiv = clkDiv %32; + } + + + + +void View() { + if (currentPage == 0) { + if (linked && hemisphere == RIGHT_HEMISPHERE) { + + gfxPrint(1, 55, "C:"); + DrawOutputOption(13, 55, outputAssign[0]); // OUT3 + + gfxPrint(31, 55, "D:"); + DrawOutputOption(43, 55, outputAssign[1]); // OUT4 + + + DrawVUMetersRight(); + + + // Highlight selected parameter + switch (selectedParam) { + // case 0: HighlightParameter(2, 35, 14); break; // MULT + // case 1: HighlightParameter(22, 35, 14); break; // DIV + case 0: HighlightParameter(2, 63, 30); break; // OUT3 + case 1: HighlightParameter(32, 63, 30); break; // OUT4 + } + + } else { + // Page 1: Main parameters for non-linked or left hemisphere + gfxPrint(1, 15, "LFO"); + gfxPrint(21, 15, selectedChannel + 1); + + gfxPrint(1, 26, "FREQ"); + float fDisplay = freq[selectedChannel]; + gfxPrint(2, 35, ones(fDisplay)); + + if (fDisplay < 2000) { + if (fDisplay < 199) { + gfxPrint("."); // Add decimal point for low frequencies + int h = hundredths(fDisplay); + if (h < 10) { gfxPrint("0"); } // Add leading zero for single digits + gfxPrint(h); + } else { + gfxPrint("."); // Add decimal point for midrange frequencies + int t = hundredths(fDisplay); + t = (t / 10) % 10; // Display only the first decimal digit + gfxPrint(t); + } + } + + + gfxPrint(31, 26, "XFM"); + gfxPrint(32, 35, xmod[selectedChannel]); + + gfxPrint(1, 46, "PHAS"); + gfxPrint(2, 55, phase[selectedChannel]); + + gfxPrint(31, 46, "THRS"); + gfxPrint(32, 55, lfoThreshold[selectedChannel]); + + // Highlight selected parameter + switch (selectedParam) { + case 0: HighlightParameter(1, 23, 30); break; // OSC + case 1: HighlightParameter(2, 43, 30); break; // FREQ + case 2: HighlightParameter(32, 43, 30); break; // XMOD + case 3: HighlightParameter(2, 63, 30); break; // PHAS + case 4: HighlightParameter(32, 63, 30); break; // THRS + } + DrawVUMetersLeft(); + } + } else if (currentPage == 1) { + + if (linked && hemisphere == RIGHT_HEMISPHERE) { + } else { + + // Page 2: Clock mult, Polarity, and Output page + + gfxPrint(1, 15, "ALL LFOs"); + gfxPrint(2, 25, "FREQx "); + gfxPrint(1, 35, freqKnobMul); + + gfxPrint(16, 35, "/"); + gfxPrint(22, 35, freqKnobDiv); + + gfxPrint(1, 55, "C:"); + DrawOutputOption(13, 55, outputAssign[0]); // OUT3 + + gfxPrint(31, 55, "D:"); + DrawOutputOption(43, 55, outputAssign[1]); // OUT4 + + gfxPrint(36, 26, "XFM"); + gfxPrint(37, 35, xmodoffset); // ALL CROSS_MODULATION + + gfxPrint(1, 46, "POL"); + gfxPrint(20, 46, bipolar ? "+-" : "+ "); + + gfxPrint(1, 55, "A:"); + DrawOutputOption(13, 55, outputAssign[0]); // OUT1 + + gfxPrint(31, 55, "B:"); + DrawOutputOption(43, 55, outputAssign[1]); // OUT2 + + + // Highlight selected parameter (use your original locations) + switch (selectedParam) { + case 5: HighlightParameter(1, 44, 14); break; // MULT + case 6: HighlightParameter(22, 44, 14); break; // DIV + case 7: HighlightParameter(36, 44, 20); break; // ALLMOD + case 8: HighlightParameter(1, 54, 35); break; // POL + case 9: HighlightParameter(2, 63, 30); break; // OUT1 + case 10: HighlightParameter(32, 63, 30); break; // OUT2 + } + } + } + +} + +void DrawOutputOption(int x, int y, uint8_t assign) { + if (assign < 4) { + // LFO output + gfxBitmap(x, y, 8, WAVEFORM_ICON); + const uint8_t* subscript = (assign == 0) ? SUP_ONE : (assign == 1) ? SUB_TWO : (assign == 2) ? SUP_THREE : SUB_FOUR; + gfxBitmap(x + 9, y, 3, subscript); + } else { + // Gate output + gfxBitmap(x, y, 8, GATE_ICON); + const uint8_t* subscript = (assign == 4) ? SUP_ONE : (assign == 5) ? SUB_TWO : (assign == 6) ? SUP_THREE : SUB_FOUR; + gfxBitmap(x + 9, y, 3, subscript); + } +} + + + + // A helper method to highlight parameters depending on EditMode + // If editing, invert rectangle; if not editing, draw a blinking cursor line + void HighlightParameter(int x, int y, int w) { + // y is the baseline where we normally draw the parameter text + // We'll highlight the line above it (like in original code) + // and depending on EditMode, invert or draw a line + + if (EditMode()) { + // Editing: invert the rectangular area to show a highlight + gfxInvert(x, y - 9, w, 9); // 9 is line height + } else { + // Not editing: show a blinking cursor line + // The Hemisphere system already uses CursorBlink() internally, + // so this will only draw the line half the time for blinking effect. + if (CursorBlink()) { + gfxLine(x, y, x + w - 1, y); + gfxPixel(x, y-1); + gfxPixel(x + w - 1, y-1); + } + } + } + +void DrawVUMetersRight() { + int bar[4]; + for (int i = 0; i < 4; ++i) { + // Calculate bar height based on sample value (assuming bipolar -3V to +3V range) + bar[i] = 14.0 * (sample[i] + HEMISPHERE_3V_CV) / HEMISPHERE_3V_CV; + + // Draw vertical bars (adjust x-position and width as needed) + gfxRect(2 + (15 * i), 42 - bar[i], 13, bar[i]); + } +} + +void DrawVUMetersLeft() { + int bar[4]; + for (int i = 0; i < 4; ++i) { + // Calculate bar height based on sample value (assuming bipolar -3V to +3V range) + // Smaller bars for left hemisphere + bar[i] = 4.0 * (sample[i] + HEMISPHERE_3V_CV) / HEMISPHERE_3V_CV; + gfxRect(31 + (6 * i), 22 - bar[i], 5, bar[i]); // Adjusted for smaller size + + } +} + + + + + void OnButtonPress() { + if (!EditMode()) { + // Enter EditMode on button press + isEditing = true; + } else { + // Exit EditMode on button press + isEditing = false; + } + + } + + + + + +void OnEncoderMove(int direction) { + // Determine how many parameters we have based on linkage and hemisphere + // linked && RIGHT_HEMISPHERE: 2 parameters (0 and 1) + // otherwise: adjust for both pages (parameters 0–7) + int max_param = (linked && hemisphere == RIGHT_HEMISPHERE) ? 1 : 10; + + if (!EditMode()) { + // Not editing: move the cursor through the available parameters + selectedParam = (selectedParam + direction + max_param + 1) % (max_param + 1); + + // Determine the current page based on the selected parameter + if (selectedParam <= 4) { + currentPage = 0; // Page 0: Main parameters + } else { + currentPage = 1; // Page 1: THRES, OUT1, OUT2 + } + } else { + // Editing: adjust parameters based on the current page and selected parameter + if (currentPage == 0) { + // Page 0: Main parameters + if (linked && hemisphere == RIGHT_HEMISPHERE) { + // Linked and RIGHT Hemisphere: Only global frequency multiplier or divider + switch (selectedParam) { + case 0: // OUT3 assignment + outputAssign[0] = (outputAssign[0] + direction + 8) % 8; + break; + case 1: // OUT4 assignment + outputAssign[1] = (outputAssign[1] + direction + 8) % 8; + break; + } + } else { + // Not linked or LEFT Hemisphere: Full parameter set (0–4) + switch (selectedParam) { + case 0: // Cycle through OSC selection + selectedChannel = (selectedChannel + direction + 4) % 4; + break; + + case 1: // FREQ (0–20.0) scaling + freqKnob[selectedChannel] += direction; + if (freqKnob[selectedChannel] < 0) { freqKnob[selectedChannel] = 510; } + if (freqKnob[selectedChannel] > 510) { freqKnob[selectedChannel] = 0; } + if (freqKnob[selectedChannel] < 200) { + freq[selectedChannel] = freqKnob[selectedChannel]; + } else if (freqKnob[selectedChannel] < 380) { + freq[selectedChannel] = 200 + ((freqKnob[selectedChannel] - 200) * 10); + } else { + freq[selectedChannel] = 2000 + ((freqKnob[selectedChannel] - 380) * 100); + } + break; + + case 2: // XMOD (0–100) scaling + xmodKnob[selectedChannel] += direction; + xmodKnob[selectedChannel] = xmodKnob[selectedChannel] + 401; + xmodKnob[selectedChannel] = xmodKnob[selectedChannel] % 401; + xmod[selectedChannel] = xmodKnob[selectedChannel]; + break; + + case 3: // PHAS (0–100) + phase[selectedChannel] += direction; + phase[selectedChannel] = (phase[selectedChannel] + 101) % 101; + break; + + case 4: // THRS adjustment + lfoThreshold[selectedChannel] = constrain(lfoThreshold[selectedChannel] + direction, -100, 100); + break; + } + } + } else if (currentPage == 1) { + // Page 1: THRES, OUT1, OUT2 + switch (selectedParam) { + + case 5: // Global frequency multiplier + freqKnobMul += direction; + freqKnobMul = freqKnobMul + 65; + freqKnobMul = freqKnobMul % 65; + freqLinkMul = freqKnobMul; + break; + case 6: // Global frequency divider + freqKnobDiv = (freqKnobDiv + 64 + direction - 1) % 64 + 1; // Cycle from 1 to 64 + freqLinkDiv = freqKnobDiv; + break; + case 7: // Global cross modulation offset + xmodoffset += direction; + xmodoffset = xmodoffset + 101; + xmodoffset = xmodoffset % 101; + xmodplus = xmodoffset; + break; + case 8: // POLARITY (+ or +-) + bipolar = (bipolar + direction + 2) % 2; + break; + case 9: // OUT1 assignment + outputAssign[0] = (outputAssign[0] + direction + 8) % 8; + break; + case 10: // OUT2 assignment + outputAssign[1] = (outputAssign[1] + direction + 8) % 8; + break; + } + } + } +} + + + uint64_t OnDataRequest() { + uint64_t data = 0; + return data; + } + + void OnDataReceive(uint64_t data) { + + } + +protected: + void SetHelp() { + if (linked && hemisphere == RIGHT_HEMISPHERE) { + help[HEMISPHERE_HELP_DIGITALS] = "1=NA 2=NA"; + help[HEMISPHERE_HELP_CVS] = "1=NA 2=NA"; + help[HEMISPHERE_HELP_OUTS] = "A=LFO3 B=LFO4"; + help[HEMISPHERE_HELP_ENCODER] = "GlobalFreqMul/Div"; + } else if (currentPage == 0) { + help[HEMISPHERE_HELP_DIGITALS] = "1=ResetPhase 2=NA"; + help[HEMISPHERE_HELP_CVS] = "1=AllFreq 2=AllXFM"; + help[HEMISPHERE_HELP_OUTS] = "A=LFO1 B=LFO2"; + help[HEMISPHERE_HELP_ENCODER] = "Frq/XFM/Phs/Thrs"; + } else if (currentPage == 1) { + help[HEMISPHERE_HELP_DIGITALS] = "1=NA 2=NA"; + help[HEMISPHERE_HELP_CVS] = "1=Thresh"; + help[HEMISPHERE_HELP_OUTS] = "A=OUT1 B=OUT2"; + help[HEMISPHERE_HELP_ENCODER] = "Thresh/Pol/Out"; + } + } + + +private: + static constexpr int pow10_lut[] = { 1, 10, 100, 1000 }; + int cursor; // 0=Freq A; 1=Cross Mod A; 2=Phase A; 3=Freq B; 4=Cross Mod B; etc. + bool isEditing = false; // Indicates if the user is editing a parameter + uint8_t modal_edit_mode = 2; + VectorOscillator osc[4]; + constexpr static uint8_t ch = 4; + constexpr static uint8_t numParams = 5; + uint8_t selectedOsc; + float freq[ch]; // in centihertz + uint16_t xmod[ch]; + uint16_t xmodoffset; + uint16_t xmodplus; + //uint8_t selectedXmod; + uint8_t phase[ch]; + int selectedChannel = 0; + int selectedParam = 0; + int sample[ch]; + float outFreq[ch]; + float freqKnob[4]; + float cvIn; + float cvIn2; + uint16_t xmodKnob[4]; + uint8_t countLimit = 0; + int waveform_number[4]; + int ones(int n) {return (n / 100);} + int hundredths(int n) {return (n % 100);} + int valueToDisplay; + int GetOutputValue(uint8_t assign) { + if (assign < 4) { + // LFO outputs + return sample[assign]; + } else { + // Gate outputs + return gateState[assign - 4] ? HEMISPHERE_MAX_CV : 0; + } + } + int8_t lfoThreshold[ch]; // Thresholds for each LFO (0-100%) + uint8_t outputAssign[2]; // Output assignments for A and B (0-7 for LFO1-LFO4, GATE1-GATE4) + uint8_t clkDiv = 0; // clkDiv allows us to calculate every other tick to save cycles + uint8_t clkDivDisplay = 0; // clkDivDisplay allows us to update the display fewer times per second + uint8_t oldClock = 0; + float displayFreq[ch]; + uint8_t freqLinkMul; + uint8_t freqKnobMul; + uint8_t freqLinkDiv; + uint8_t freqKnobDiv; + uint8_t mulLink; + uint8_t divLink; + uint8_t currentPage = 0; // 0 = main page, 1 = threshold/output page + bool linked; + bool bipolar; + bool gateState[4] = {false, false, false, false}; + bool EditMode() { return isEditing; }; + const char* GetOutputLabel(uint8_t assign) { + switch (assign) { + case 0: return "LFO1"; + case 1: return "LFO2"; + case 2: return "LFO3"; + case 3: return "LFO4"; + case 4: return "GAT1"; + case 5: return "GAT2"; + case 6: return "GAT3"; + case 7: return "GAT4"; + default: return "???"; + } + } + + const uint8_t SUP_THREE[3] = {0x09, 0x0D, 0x06}; // Superscript "3" + const uint8_t SUB_FOUR[3] = {0x30, 0x20, 0xF0}; // Subscript "4" + +}; + + + +//////////////////////////////////////////////////////////////////////////////// +//// Hemisphere Applet Functions +/// +/// Once you run the find-and-replace to make these refer to Relabi, +/// it's usually not necessary to do anything with these functions. You +/// should prefer to handle things in the HemisphereApplet child class +/// above. +//////////////////////////////////////////////////////////////////////////////// +Relabi Relabi_instance[2]; + +void Relabi_Start(bool hemisphere) {Relabi_instance[hemisphere].BaseStart(hemisphere);} +void Relabi_Controller(bool hemisphere, bool forwarding) {Relabi_instance[hemisphere].BaseController(forwarding);} +void Relabi_View(bool hemisphere) {Relabi_instance[hemisphere].BaseView();} +void Relabi_OnButtonPress(bool hemisphere) {Relabi_instance[hemisphere].OnButtonPress();} +void Relabi_OnEncoderMove(bool hemisphere, int direction) {Relabi_instance[hemisphere].OnEncoderMove(direction);} +void Relabi_ToggleHelpScreen(bool hemisphere) {Relabi_instance[hemisphere].HelpScreen();} +uint64_t Relabi_OnDataRequest(bool hemisphere) {return Relabi_instance[hemisphere].OnDataRequest();} +void Relabi_OnDataReceive(bool hemisphere, uint64_t data) {Relabi_instance[hemisphere].OnDataReceive(data);} diff --git a/software/o_c_REV/HEM_ResetClock.ino b/software/o_c_REV/HEM_ResetClock.ino new file mode 100644 index 000000000..0443744f9 --- /dev/null +++ b/software/o_c_REV/HEM_ResetClock.ino @@ -0,0 +1,204 @@ +// Copyright (c) 2020, Peter Kyme +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#define RC_MIN_SPACING 6 +#define RC_TICKS_PER_MS 17 + +class ResetClock : public HemisphereApplet { +public: + + const char* applet_name() { + return "ResetClk"; + } + + void Start() { + cursor = 0; + spacing = 6; + ticks_since_clock = 0; + pending_clocks = 0; + length = 8; + position = 0; + offset = 0; + } + + void Controller() { + // CV1 modulates offset + int cv1_mod = offset; + Modulate(cv1_mod, 0, 0, length-1); + + if (cv1_mod != offset_mod) // changed + UpdateOffset(offset_mod, cv1_mod); + + if (Clock(1)) { + pending_clocks += (length - position + offset_mod - 1) % length; + } + + if (Clock(0)) pending_clocks++; + + if (pending_clocks && (ticks_since_clock > spacing * RC_TICKS_PER_MS)) { + ClockOut(0, (spacing * RC_TICKS_PER_MS)/2); + if (pending_clocks==1) { + ClockOut(1, (spacing * RC_TICKS_PER_MS)/2); + } + ticks_since_clock = 0; + position = (position + 1) % length; + pending_clocks--; + } else { + ticks_since_clock++; + } + } + + void View() { + DrawInterface(); + } + + void OnButtonPress() { + CursorAction(cursor, 3); + } + + void OnEncoderMove(int direction) { + if (!EditMode()) { + MoveCursor(cursor, direction, 3); + return; + } + + switch (cursor) { + case 0: // length + length = constrain(length + direction, 1, 32); + position %= length; + offset %= length; + break; + case 1: // offset + offset = constrain(offset + direction, 0, length - 1); + break; + case 2: // spacing + spacing = constrain(spacing + direction, RC_MIN_SPACING, 100); + break; + case 3: // position + position = ( position + direction + length ) % length; + break; + } + } + void UpdateOffset(int &o, int new_offset) { + int prev_offset = o; + o = constrain(new_offset, 0, length-1); + pending_clocks += o - prev_offset; + if (pending_clocks < 0) pending_clocks += length; + } + + uint64_t OnDataRequest() { + uint64_t data = 0; + Pack(data, PackLocation {0,5}, length-1); + Pack(data, PackLocation {5,5}, offset); + Pack(data, PackLocation {10,7}, spacing); + return data; + } + + void OnDataReceive(uint64_t data) { + length = Unpack(data, PackLocation {0,5}) + 1; + offset = Unpack(data, PackLocation {5,5}); + spacing = Unpack(data, PackLocation {10,7}); + } + +protected: + void SetHelp() { + // "------------------" <-- Size Guide + help[HEMISPHERE_HELP_DIGITALS] = "Clock, Reset"; + help[HEMISPHERE_HELP_CVS] = "1=Offset"; + help[HEMISPHERE_HELP_OUTS] = "Clock, Reset"; + help[HEMISPHERE_HELP_ENCODER] = "Len/Offst/Spac/Pos"; + // "------------------" <-- Size Guide + } + +private: + int cursor; + int ticks_since_clock; + int pending_clocks; + + // Settings + int length; + int position; + int offset, offset_mod; + int spacing; + + void DrawInterface() { + // Length + gfxIcon(1, 14, LOOP_ICON); + gfxPrint(12 + pad(10, length), 15, length); + + // Offset + gfxIcon(32, 15, ROTATE_R_ICON); + gfxPrint(40 + pad(10, offset_mod), 15, offset_mod); + + // Spacing + gfxIcon(1, 25, CLOCK_ICON); + gfxPrint(12 + pad(100, spacing), 25, spacing); + gfxPrint("ms"); + + DrawIndicator(); + + switch (cursor) { + case 0: gfxCursor(13, 23, 12); break; // length + case 1: gfxCursor(41, 23, 12); break; // offset + case 2: gfxCursor(13, 33, 18); break; // spacing + case 3: gfxCursor(0, 62, 64, 17); break; // position + } + } + + void DrawIndicator() { + gfxLine(0, 45, 63, 45); + if (cursor != 3) gfxLine(0, 62, 63, 62); + for(int i = 0; i < length; i++) + { + int x0 = (i*64)/length; + int width = ((i+1)*64)/length - x0; + if (position == i) + { + gfxRect(x0, 46, width, 16); + } + else + { + gfxLine(x0, 46, x0, 61); + } + } + gfxLine(63, 46, 63, 61); + } + +}; + + +//////////////////////////////////////////////////////////////////////////////// +//// Hemisphere Applet Functions +/// +/// Once you run the find-and-replace to make these refer to ResetClock, +/// it's usually not necessary to do anything with these functions. You +/// should prefer to handle things in the HemisphereApplet child class +/// above. +//////////////////////////////////////////////////////////////////////////////// +ResetClock ResetClock_instance[2]; + +void ResetClock_Start(bool hemisphere) {ResetClock_instance[hemisphere].BaseStart(hemisphere);} +void ResetClock_Controller(bool hemisphere, bool forwarding) {ResetClock_instance[hemisphere].BaseController(forwarding);} +void ResetClock_View(bool hemisphere) {ResetClock_instance[hemisphere].BaseView();} +void ResetClock_OnButtonPress(bool hemisphere) {ResetClock_instance[hemisphere].OnButtonPress();} +void ResetClock_OnEncoderMove(bool hemisphere, int direction) {ResetClock_instance[hemisphere].OnEncoderMove(direction);} +void ResetClock_ToggleHelpScreen(bool hemisphere) {ResetClock_instance[hemisphere].HelpScreen();} +uint64_t ResetClock_OnDataRequest(bool hemisphere) {return ResetClock_instance[hemisphere].OnDataRequest();} +void ResetClock_OnDataReceive(bool hemisphere, uint64_t data) {ResetClock_instance[hemisphere].OnDataReceive(data);} diff --git a/software/o_c_REV/HEM_RndWalk.ino b/software/o_c_REV/HEM_RndWalk.ino new file mode 100644 index 000000000..7dd941202 --- /dev/null +++ b/software/o_c_REV/HEM_RndWalk.ino @@ -0,0 +1,331 @@ +// Copyright (c) 2022, Alessio Degani +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#include "OC_core.h" + +#define PROB_UP 500 +#define PROB_DN 500 + +#define MAX_RANGE 255 +#define MAX_STEP 255 +#define MAX_SMOOTH 255 + +#define HEM_1V 1536 //ADC value for 1 Volt +#define HEM_1ST 128 //ADC value for 1 semitone +#define HEM_HST 64 //ADC value for half semitone + +class RndWalk : public HemisphereApplet { +public: + + const char* applet_name() { // Maximum 10 characters + return "RndWalk"; + } + + void Start() { + ForEachChannel(ch) + { + // rndSeed[ch] = random(1, 255); + currentVal[ch] = 0; + currentOut[ch] = 0; + UpdateAlpha(); + } + cursor = 0; + } + + void Controller() { + // Main LOOP + // for triggers read from Clock(0|1) + // for CV in read from In(0|1) + // for CV out write to Out(0|1, value) + // INPUT + int maxVal = HEMISPHERE_MAX_CV; + switch (cvRange) { + case 0: + maxVal = HEM_HST; + break; + case 1: + maxVal = HEM_1ST; + break; + case 2: + maxVal = HEM_1V; + break; + default: + break; + } + int rangeCv = Proportion(In(0), HEMISPHERE_MAX_INPUT_CV, MAX_RANGE); + int stepCv = Proportion(In(1), HEMISPHERE_MAX_INPUT_CV, MAX_STEP); + + ForEachChannel(ch) { + // OUTPUT + if ( ((ch == 0) && Clock(0)) || + ((ch == 1) && Clock(yClkSrc)) ) + { + if ((ch == 1) && ((clkMod++ % yClkDiv) > 0) ){ + continue; + } + int randInt = random(0, 1000); + int randStep = (float)(random(1, constrain(step+stepCv, 0, MAX_STEP)))/MAX_STEP*maxVal/2; + int rangeScaled = (int)( ((float)constrain(range + rangeCv, 0, MAX_RANGE))/MAX_RANGE * maxVal); + currentVal[ch] += randStep * (((randInt > PROB_UP) && (currentVal[ch] < rangeScaled)) - + ((randInt < PROB_DN) && (currentVal[ch] > -rangeScaled))); + } + currentOut[ch] = alpha*currentOut[ch] + (1-alpha)*(float)currentVal[ch]; + + Out(ch, constrain((int)currentOut[ch], -HEMISPHERE_MAX_CV, HEMISPHERE_MAX_CV)); + } + } + + void View() { + DrawDisplay(); + } + + void OnButtonPress() { + CursorAction(cursor, 5); + } + + void OnEncoderMove(int direction) { + if (!EditMode()) { + MoveCursor(cursor, direction, 5); + return; + } + + // Parameter Change handler + // var cursor is the param pointer + // var direction is the the movement of the encoder + // use valConstrained = constrain(val, min, max) to apply value limit + if (cursor == 0) { + range = constrain(range + direction, 0, MAX_RANGE); + } else if (cursor == 1) { + step = constrain(step + direction, 1, MAX_STEP); + } else if (cursor == 2) { + smoothness = constrain(smoothness + direction, 0, MAX_SMOOTH); + UpdateAlpha(); + } else if (cursor == 3) { + yClkSrc = constrain(yClkSrc + direction, 0, 1); + } else if (cursor == 4) { + yClkDiv = constrain(yClkDiv + direction, 1, 32); + } else if (cursor == 5) { + cvRange = constrain(cvRange + direction, 0, 3); + int maxVal = HEMISPHERE_MAX_CV; + switch (cvRange) { + case 0: + maxVal = HEM_HST; + break; + case 1: + maxVal = HEM_1ST; + break; + case 2: + maxVal = HEM_1V; + break; + default: + break; + } + ForEachChannel(ch) { + currentVal[ch] = currentVal[ch]%maxVal; + } + } + + } + + uint64_t OnDataRequest() { + uint64_t data = 0; + Pack(data, PackLocation {0,1}, yClkSrc); + Pack(data, PackLocation {1,4}, yClkDiv); + Pack(data, PackLocation {5,8}, range); + Pack(data, PackLocation {13,8}, step); + Pack(data, PackLocation {21,8}, smoothness); + Pack(data, PackLocation {29,2}, cvRange); + return data; + } + + void OnDataReceive(uint64_t data) { + yClkSrc = Unpack(data, PackLocation {0,1}); + yClkDiv = Unpack(data, PackLocation {1,4}); + range = Unpack(data, PackLocation {5,8}); + step = Unpack(data, PackLocation {13,8}); + smoothness = Unpack(data, PackLocation {21,8}); + cvRange = Unpack(data, PackLocation {29,2}); + UpdateAlpha(); + } + +protected: + void SetHelp() { + // "------------------" <-- Size Guide + help[HEMISPHERE_HELP_DIGITALS] = "1=X Clock 2=Y Clk"; + help[HEMISPHERE_HELP_CVS] = "1=Range 2=step"; + help[HEMISPHERE_HELP_OUTS] = "RndWalk A=X B=Y"; + help[HEMISPHERE_HELP_ENCODER] = "T=Set P=Select"; + // "------------------" <-- Size Guide + } + +private: + // Parameters (saved in EEPROM) + bool yClkSrc = 0; // 0=TR1, 1=TR2 + uint8_t yClkDiv = 1; // 4 bits [1 .. 32] + int range = 20; // 8 bits + int step = 20; // 8 bits + uint8_t smoothness = 20; // 8 bits + uint8_t cvRange = 3; // 2 bit + uint8_t clkMod = 0; //not stored, used for clock division + float alpha; // not stored, used for smoothing + + // Runtime parameters + // unsigned int rndSeed[2]; + int currentVal[2]; + float currentOut[2]; + int cursor; // 0=Y clk src, 1=Y clk div, 2=Range, 3=step, 4=Smoothnes + + void DrawDisplay() { + + if (cursor < 3) { + gfxPrint(1, 15, "Range"); + gfxPrint(43, 15, range); + + gfxPrint(1, 25, "Step"); + gfxPrint(43, 25, step); + gfxPrint(1, 35, "Smooth"); + gfxPrint(43, 35, smoothness); + } else { + + gfxPrint(1, 15, "Y TRIG"); + if (yClkSrc == 0) { + gfxPrint(43, 15, "TR1"); + } else { + gfxPrint(43, 15, "TR2"); + } + + gfxPrint(1, 25, "Y CLK /"); + gfxPrint(43, 25, yClkDiv); + + gfxPrint(1, 35, "CVRng"); + if (cvRange == 0) { + gfxPrint(40, 35, ".5st"); + } else if (cvRange == 1) { + gfxPrint(40, 35, "1 st"); + } else if (cvRange == 2) { + gfxPrint(40, 35, "1oct"); + } else if (cvRange == 3) { + gfxPrint(40, 35, "FULL"); + } + } + switch (cursor) { + case 0: + case 3: gfxCursor(43, 23, 18); break; + + case 1: + case 4: gfxCursor(43, 33, 18); break; + + case 2: gfxCursor(43, 43, 18); break; + case 5: gfxCursor(40, 43, 24); break; + } + + // gfxPrint(1, 38, "x"); + // gfxPrint(1, 50, "y"); + + // gfxPrint(7, 38, currentVal[0]); + // gfxPrint(7, 50, currentVal[1]); + + int maxVal = HEMISPHERE_MAX_CV; + switch (cvRange) { + case 0: + maxVal = HEM_HST; + break; + case 1: + maxVal = HEM_1ST; + break; + case 2: + maxVal = HEM_1V; + break; + default: + break; + } + // gfxPrint(1, 47, currentVal[0]); + // gfxPrint(1, 55, currentVal[1]); + gfxPrint(1, 47, "x"); + gfxPrint(55, 55, "y"); + ForEachChannel(ch) { + int w = 0; + if (range > 0) { + w = (currentOut[ch]/((float)range/MAX_RANGE*maxVal))*31; + if (w > 31) { + w = 31; + } + if (w < -31) { + w = -31; + } + } + if (w >= 0) { + gfxInvert(31, 48 + (8 * ch), w, 7); + } else { + gfxInvert(31+w, 48 + (8 * ch), -w, 7); + } + } + + } + + void UpdateAlpha() { + // Use log mapping for better feeling + alpha = log(1+smoothness)/log(1+MAX_SMOOTH); + // alpha = (float)smoothness/(float)MAX_SMOOTH; + } +}; + + +//////////////////////////////////////////////////////////////////////////////// +//// Hemisphere Applet Functions +/// +/// Once you run the find-and-replace to make these refer to RndWalk, +/// it's usually not necessary to do anything with these functions. You +/// should prefer to handle things in the HemisphereApplet child class +/// above. +//////////////////////////////////////////////////////////////////////////////// +RndWalk RndWalk_instance[2]; + +void RndWalk_Start(bool hemisphere) { + RndWalk_instance[hemisphere].BaseStart(hemisphere); +} + +void RndWalk_Controller(bool hemisphere, bool forwarding) { + RndWalk_instance[hemisphere].BaseController(forwarding); +} + +void RndWalk_View(bool hemisphere) { + RndWalk_instance[hemisphere].BaseView(); +} + +void RndWalk_OnButtonPress(bool hemisphere) { + RndWalk_instance[hemisphere].OnButtonPress(); +} + +void RndWalk_OnEncoderMove(bool hemisphere, int direction) { + RndWalk_instance[hemisphere].OnEncoderMove(direction); +} + +void RndWalk_ToggleHelpScreen(bool hemisphere) { + RndWalk_instance[hemisphere].HelpScreen(); +} + +uint64_t RndWalk_OnDataRequest(bool hemisphere) { + return RndWalk_instance[hemisphere].OnDataRequest(); +} + +void RndWalk_OnDataReceive(bool hemisphere, uint64_t data) { + RndWalk_instance[hemisphere].OnDataReceive(data); +} diff --git a/software/o_c_REV/HEM_RunglBook.ino b/software/o_c_REV/HEM_RunglBook.ino index 2925cc427..fab5fbefc 100644 --- a/software/o_c_REV/HEM_RunglBook.ino +++ b/software/o_c_REV/HEM_RunglBook.ino @@ -27,6 +27,8 @@ public: void Start() { threshold = (12 << 7) * 2; + bitdepth = 3; + parameterSelection = 0; // Start by adjusting threshold } void Controller() { @@ -38,36 +40,67 @@ public: byte b0 = In(0) > threshold ? 0x01 : 0x00; reg = (reg << 1) | b0; } - - int rungle = Proportion(reg & 0x07, 0x07, HEMISPHERE_MAX_CV); - int rungle_tap = Proportion((reg >> 5) & 0x07, 0x07, HEMISPHERE_MAX_CV); + int bitmask = (1 << bitdepth) - 1; + int rungle = Proportion(reg & bitmask, bitmask, HEMISPHERE_MAX_CV); + //int rungle_tap = Proportion((reg >> 5) & bitmask, bitmask, HEMISPHERE_MAX_CV); + int msb = (reg >> 7) & 0x01; + int out2 = Proportion(msb, 1, HEMISPHERE_MAX_CV); + Out(0, rungle); - Out(1, rungle_tap); + Out(1, out2); } } + void View() { - gfxHeader(applet_name()); - gfxPrint(1, 15, "Thr:"); - gfxPrintVoltage(threshold); + if (parameterSelection == 0) { + gfxPrint(1, 15, "Thr:"); + gfxPrintVoltage(threshold); + + } else { + gfxPrint(1, 15, "Bit:"); + gfxPrint(bitdepth); + } + if (adjustingParameter) { + gfxCursor(25, 24, 36); + } gfxSkyline(); } - void OnButtonPress() { } + void OnButtonPress() { + // Toggle parameter adjustment mode when button is pressed + adjustingParameter = !adjustingParameter; + } - void OnEncoderMove(int direction) { - threshold += (direction * 128); - threshold = constrain(threshold, (12 << 7), (12 << 7) * 5); // 1V - 5V +void OnEncoderMove(int direction) { + if (!adjustingParameter) { + // Toggle between parameter selections (Thr and Bit) + parameterSelection = 1 - parameterSelection; // Toggle between 0 and 1 + } else { + // Adjust the currently selected parameter + if (parameterSelection == 0) { + // Adjust threshold + threshold += (direction * 128); + threshold = constrain(threshold, (12 << 7), (12 << 7) * 5); // 1V - 5V + } else { + // Adjust bitdepth + bitdepth += direction; + bitdepth = constrain(bitdepth, 1, 8); // Limit bitdepth between 1 and 8 + } } +} + uint64_t OnDataRequest() { uint64_t data = 0; Pack(data, PackLocation {0,16}, threshold); + Pack(data, PackLocation {16,8}, bitdepth); return data; } void OnDataReceive(uint64_t data) { threshold = Unpack(data, PackLocation {0,16}); + bitdepth = Unpack(data, PackLocation {16,8}); } protected: @@ -75,14 +108,17 @@ protected: // "------------------" <-- Size Guide help[HEMISPHERE_HELP_DIGITALS] = "1=Clock 2=Freeze"; help[HEMISPHERE_HELP_CVS] = "1=Signal"; - help[HEMISPHERE_HELP_OUTS] = "A=Rungle B=Alt"; - help[HEMISPHERE_HELP_ENCODER] = "Threshold"; + help[HEMISPHERE_HELP_OUTS] = "A=Rungle B=bit1"; + help[HEMISPHERE_HELP_ENCODER] = "Threshold/BitDepth"; // "------------------" <-- Size Guide } private: byte reg; uint16_t threshold; + uint8_t bitdepth; + uint8_t parameterSelection; // 0: Threshold, 1: Bitdepth + bool adjustingParameter; // Flag to indicate parameter adjustment mode }; diff --git a/software/o_c_REV/HEM_ScaleDuet.ino b/software/o_c_REV/HEM_ScaleDuet.ino index 347f9e2f8..65e3bda18 100644 --- a/software/o_c_REV/HEM_ScaleDuet.ino +++ b/software/o_c_REV/HEM_ScaleDuet.ino @@ -36,8 +36,7 @@ public: { mask[scale] = 0xffff; } - quantizer.Init(); - quantizer.Configure(OC::Scales::GetScale(5), mask[0]); + QuantizerConfigure(0, OC::Scales::SCALE_SEMI, mask[0]); last_scale = 0; adc_lag_countdown = 0; } @@ -50,17 +49,16 @@ public: if (EndOfADCLag()) { uint8_t scale = Gate(1); if (scale != last_scale) { - quantizer.Configure(OC::Scales::GetScale(5), mask[scale]); + QuantizerConfigure(0, OC::Scales::SCALE_SEMI, mask[scale]); last_scale = scale; } int32_t pitch = In(0); - int32_t quantized = quantizer.Process(pitch, 0, 0); + int32_t quantized = Quantize(0, pitch, 0, 0); Out(0, quantized); } } void View() { - gfxHeader(applet_name()); DrawKeyboard(); DrawMaskIndicators(); } @@ -71,12 +69,13 @@ public: // Toggle the mask bit at the cursor position mask[scale] ^= (0x01 << bit); - if (scale == last_scale) quantizer.Configure(OC::Scales::GetScale(5), mask[scale]); + if (scale == last_scale) + QuantizerConfigure(0, OC::Scales::SCALE_SEMI, mask[scale]); } void OnEncoderMove(int direction) { if (cursor == 0 && direction == -1) cursor = 1; - cursor = constrain(cursor += direction, 0, 23); + cursor = constrain(cursor + direction, 0, 23); ResetCursor(); } @@ -92,7 +91,7 @@ public: mask[1] = Unpack(data, PackLocation {12,12}); last_scale = 0; - quantizer.Configure(OC::Scales::GetScale(5), mask[last_scale]); + QuantizerConfigure(0, OC::Scales::SCALE_SEMI, mask[last_scale]); } protected: @@ -106,7 +105,6 @@ protected: } private: - braids::Quantizer quantizer; uint16_t mask[2]; uint8_t cursor; // 0-11=Scale 1; 12-23=Scale 2 uint8_t last_scale; // The most-recently-used scale (used to set the mask when necessary) diff --git a/software/o_c_REV/HEM_Schmitt.ino b/software/o_c_REV/HEM_Schmitt.ino index 769fa32fa..8e217b8d4 100644 --- a/software/o_c_REV/HEM_Schmitt.ino +++ b/software/o_c_REV/HEM_Schmitt.ino @@ -46,7 +46,6 @@ public: } void View() { - gfxHeader(applet_name()); DrawInterface(); } diff --git a/software/o_c_REV/HEM_Scope.ino b/software/o_c_REV/HEM_Scope.ino index 895568ab4..b32651310 100644 --- a/software/o_c_REV/HEM_Scope.ino +++ b/software/o_c_REV/HEM_Scope.ino @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -const uint8_t HEM_PPQN_VALUES[] = {1, 2, 4, 8, 16, 24}; +#define SCOPE_CURRENT_SETTING_TIMEOUT 50001 class Scope : public HemisphereApplet { public: @@ -33,6 +33,8 @@ public: sample_ticks = 320; freeze = 0; last_scope_tick = 0; + current_setting = 0; + current_display = 0; } void Controller() { @@ -54,14 +56,17 @@ public: } if (!freeze) { - last_cv = In(1); + last_cv = In((current_display & 1) == 1); if (--sample_countdown < 1) { sample_countdown = sample_ticks; - if (++sample_num > 63) sample_num = 0; - int sample = Proportion(In(0), HEMISPHERE_MAX_CV, 128); - sample = constrain(sample, -128, 127) + 127; - snapshot[sample_num] = (uint8_t)sample; + sample_num = LoopInt(++sample_num, 63); + + for (int n = 0; n < 2; n++) { + int sample = Proportion(In(n) + HEMISPHERE_MAX_INPUT_CV, 2*HEMISPHERE_MAX_INPUT_CV, 255); + sample = constrain(sample, 0, 255); + snapshot[n][sample_num] = (uint8_t)sample; + } } ForEachChannel(ch) Out(ch, In(ch)); @@ -69,23 +74,41 @@ public: } void View() { - gfxHeader(applet_name()); - DrawBPM(); - DrawInput1(); - DrawInput2(); + if(current_display == 4) { + DrawInput(-1); + } else { + DrawBPM(); + DrawInput((current_display & 2) == 2); + PrintInput(); + } + + DrawCurrentSetting(); if (freeze) { gfxInvert(0, 24, 64, 40); } } void OnButtonPress() { - freeze = 1 - freeze; + if (current_setting == 2 && !EditMode()) // FREEZE button + freeze = !freeze; + else if (OC::CORE::ticks - last_encoder_move < SCOPE_CURRENT_SETTING_TIMEOUT) // params visible? toggle edit + CursorAction(current_setting, 2); + else // show params + last_encoder_move = OC::CORE::ticks; } void OnEncoderMove(int direction) { - if (sample_ticks < 32) sample_ticks += direction; - else sample_ticks += direction * 10; - sample_ticks = constrain(sample_ticks, 2, 64000); + if (!EditMode()) { // switch setting + MoveCursor(current_setting, direction, 2); + } else { // edit + if(current_setting == 0) { + if (sample_ticks < 32) sample_ticks += direction; + else sample_ticks += direction * 10; + sample_ticks = constrain(sample_ticks, 2, 64000); + } else if(current_setting == 1) { + current_display = constrain(current_display + direction, 0, 4); + } + } last_encoder_move = OC::CORE::ticks; } @@ -107,7 +130,7 @@ protected: help[HEMISPHERE_HELP_DIGITALS] = "Clk 1=BPM 2=Cycle1"; help[HEMISPHERE_HELP_CVS] = "1=CV1 2=CV2"; help[HEMISPHERE_HELP_OUTS] = "A=CV1 B=CV2"; - help[HEMISPHERE_HELP_ENCODER] = "T=SmplRate P=Freez"; + help[HEMISPHERE_HELP_ENCODER] = "T=Value P=Setting"; // "------------------" <-- Size Guide } @@ -121,15 +144,19 @@ private: bool freeze; // Scope - uint8_t snapshot[64]; + int current_display; + int current_setting; + uint8_t snapshot[2][64]; int sample_ticks; // Ticks between samples int sample_countdown; // Last time a sample was taken int sample_num; // Current sample number at the start int last_encoder_move; // The last the the sample_ticks value was changed int last_scope_tick; // Used to auto-calculate sample countdown - // Icons - + int LoopInt(int n, int max) { + return n > max ? 0 : n; + } + void DrawBPM() { gfxPrint(9, 15, "BPM "); gfxPrint(bpm / 4); @@ -138,26 +165,54 @@ private: if (OC::CORE::ticks - last_bpm_tick < 1666) gfxBitmap(1, 15, 8, CLOCK_ICON); } - void DrawInput1() { - for (int s = 0; s < 64; s++) - { - int x = s + sample_num; - if (x > 63) x -= 64; - int l = Proportion(snapshot[x], 255, 28); - gfxPixel(x, (28 - l) + 24); - } + void DrawCurrentSetting() { + if (OC::CORE::ticks - last_encoder_move < SCOPE_CURRENT_SETTING_TIMEOUT) { + if(current_setting == 0) { + gfxPrint(1, 26, "Rate"); + gfxPrint(32, 26, sample_ticks); + } else if(current_setting == 1) { + gfxPrint(1, 26, "Mode "); + if(current_display == 4) { + gfxPrint("1,2"); + } else { + gfxPrint((current_display & 2) == 2 ? 2 : 1); + gfxPrint("+"); + gfxPrint((current_display & 1) == 1 ? 2 : 1); + } + } else if(current_setting == 2) { + gfxPrint(1, 26, "Freeze "); + gfxPrint(freeze ? "ON" : "OFF"); + } - if (OC::CORE::ticks - last_encoder_move < 16667) { - gfxPrint(1, 26, sample_ticks); + if (EditMode()) gfxInvert(1, 25, 31, 9); } } - void DrawInput2() { + void PrintInput() { gfxLine(0, 53, 63, 53); gfxBitmap(1, 55, 8, CV_ICON); gfxPos(12, 55); gfxPrintVoltage(last_cv); } + + void DrawInput(int input) { + int max = input < 0 ? 54 : 28; + for (int s = 0; s < 64; s++) + { + int n = s + sample_num; + int px, py; + if (n > 63) n -= 64; + if (input < 0) { // X-Y mode + px = Proportion(snapshot[0][n], 255, 63); + py = Proportion(snapshot[1][n], 255, max); + gfxPixel(px, constrain((max - py) + 10, 0, 63)); + } else { + px = n; + py = Proportion(snapshot[input][n], 255, max); + gfxPixel(px, (max - py) + 24); + } + } + } }; diff --git a/software/o_c_REV/HEM_Sequence5.ino b/software/o_c_REV/HEM_Sequence5.ino deleted file mode 100644 index 75d335a1a..000000000 --- a/software/o_c_REV/HEM_Sequence5.ino +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright (c) 2018, Jason Justian -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -#include "HSMIDI.h" - -class Sequence5 : public HemisphereApplet { -public: - - const char* applet_name() { // Maximum 10 characters - return "Sequence5"; - } - - void Start() { - for (int s = 0; s < 5; s++) note[s] = random(0, 30); - play = 1; - reset = true; - } - - void Controller() { - // Reset sequencer - if (Clock(1)) { - step = 0; - reset = true; - ClockOut(1); - } - - int transpose = 0; - if (DetentedIn(0)) { - transpose = In(0) / 128; // 128 ADC steps per semitone - } - int play_note = note[step] + 60 + transpose; - play_note = constrain(play_note, 0, 127); - - if (Clock(0)) StartADCLag(); - - if (EndOfADCLag()) { - Advance(step); - if (step == 0) ClockOut(1); - play = 1; - } - - if (play) { - int cv = MIDIQuantizer::CV(play_note); - Out(0, cv); - } - } - - void View() { - gfxHeader(applet_name()); - DrawPanel(); - } - - void OnButtonPress() { - if (++cursor == 5) cursor = 0; - } - - void OnEncoderMove(int direction) { - if (note[cursor] + direction < 0 && cursor > 0) { - // If turning past zero, set the mute bit for this step - muted |= (0x01 << cursor); - } else { - note[cursor] = constrain(note[cursor] += direction, 0, 30); - muted &= ~(0x01 << cursor); - } - play = 1; // Replay the changed step in the controller, so it can be heard - } - - uint64_t OnDataRequest() { - uint64_t data = 0; - for (int s = 0; s < 5; s++) - { - Pack(data, PackLocation {s * 5,5}, note[s]); - } - Pack(data, PackLocation{25,5}, muted); - return data; - } - - void OnDataReceive(uint64_t data) { - for (int s = 0; s < 5; s++) - { - note[s] = Unpack(data, PackLocation {s * 5,5}); - } - muted = Unpack(data, PackLocation {25,5}); - } - -protected: - void SetHelp() { - // "------------------" <-- Size Guide - help[HEMISPHERE_HELP_DIGITALS] = "1=Clock 2=Reset"; - help[HEMISPHERE_HELP_CVS] = "1=Transpose"; - help[HEMISPHERE_HELP_OUTS] = "A=CV B=Clk Step 1"; - help[HEMISPHERE_HELP_ENCODER] = "Note"; - // "------------------" <-- Size Guide - } - -private: - int cursor = 0; - char muted = 0; // Bitfield for muted steps; ((muted >> step) & 1) means muted - int note[5]; // Sequence value (0 - 30) - int step = 0; // Current sequencer step - bool play; // Play the note - bool reset; - - void Advance(int starting_point) { - if (!reset) step++; - reset = false; - if (step == 5) step = 0; - // If all the steps have been muted, stay where we were - if (step_is_muted(step) && step != starting_point) Advance(starting_point); - } - - void DrawPanel() { - // Sliders - for (int s = 0; s < 5; s++) - { - int x = 6 + (12 * s); - if (!step_is_muted(s)) { - gfxLine(x, 25, x, 63); - - // When cursor, there's a heavier bar and a solid slider - if (s == cursor) { - gfxLine(x + 1, 25, x + 1, 63); - gfxRect(x - 4, BottomAlign(note[s]), 9, 3); - } else gfxFrame(x - 4, BottomAlign(note[s]), 9, 3); - - // When on this step, there's an indicator circle - if (s == step) {gfxCircle(x, 20, 3);} - } else if (s == cursor) { - gfxLine(x, 25, x, 63); - gfxLine(x + 1, 25, x + 1, 63); - } - } - } - - bool step_is_muted(int step) { - return (muted & (0x01 << step)); - } -}; - - -//////////////////////////////////////////////////////////////////////////////// -//// Hemisphere Applet Functions -/// -/// Once you run the find-and-replace to make these refer to Sequence5, -/// it's usually not necessary to do anything with these functions. You -/// should prefer to handle things in the HemisphereApplet child class -/// above. -//////////////////////////////////////////////////////////////////////////////// -Sequence5 Sequence5_instance[2]; - -void Sequence5_Start(bool hemisphere) { - Sequence5_instance[hemisphere].BaseStart(hemisphere); -} - -void Sequence5_Controller(bool hemisphere, bool forwarding) { - Sequence5_instance[hemisphere].BaseController(forwarding); -} - -void Sequence5_View(bool hemisphere) { - Sequence5_instance[hemisphere].BaseView(); -} - -void Sequence5_OnButtonPress(bool hemisphere) { - Sequence5_instance[hemisphere].OnButtonPress(); -} - -void Sequence5_OnEncoderMove(bool hemisphere, int direction) { - Sequence5_instance[hemisphere].OnEncoderMove(direction); -} - -void Sequence5_ToggleHelpScreen(bool hemisphere) { - Sequence5_instance[hemisphere].HelpScreen(); -} - -uint64_t Sequence5_OnDataRequest(bool hemisphere) { - return Sequence5_instance[hemisphere].OnDataRequest(); -} - -void Sequence5_OnDataReceive(bool hemisphere, uint64_t data) { - Sequence5_instance[hemisphere].OnDataReceive(data); -} diff --git a/software/o_c_REV/HEM_SequenceX.ino b/software/o_c_REV/HEM_SequenceX.ino new file mode 100644 index 000000000..f453cc005 --- /dev/null +++ b/software/o_c_REV/HEM_SequenceX.ino @@ -0,0 +1,223 @@ +// Copyright (c) 2018, Jason Justian +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +/* original 8-step mod by Logarhythm, adapted by djphazer + */ + +#include "HSMIDI.h" + +// DON'T GO PAST 8! +#define SEQX_STEPS 8 +#define SEQX_MAX_VALUE 31 + +class SequenceX : public HemisphereApplet { +public: + + const char* applet_name() { // Maximum 10 characters + return "SequenceX"; + } + + void Start() { + Randomize(); + } + + void Randomize() { + for (int s = 0; s < SEQX_STEPS; s++) note[s] = random(0, SEQX_MAX_VALUE); + } + + void Controller() { + if (DetentedIn(1) > (24 << 7) ) // 24 semitones == 2V + { + // new random sequence if CV2 goes high + if (!cv2_gate) { + cv2_gate = 1; + Randomize(); + } + } + else cv2_gate = 0; + + if (Clock(1)) { // reset + Reset(); + } + if (Clock(0)) { // clock + + if (!reset) Advance(step); + reset = false; + // send trigger on first step + if (step == 0) ClockOut(1); + } + + // continuously compute the note + int transpose = DetentedIn(0) / 128; // 128 ADC steps per semitone + int play_note = note[step] + 60 + transpose; + play_note = constrain(play_note, 0, 127); + // set CV output + Out(0, MIDIQuantizer::CV(play_note)); + + } + + void View() { + DrawPanel(); + } + + void OnButtonPress() { + CursorAction(cursor, SEQX_STEPS-1); + } + + void OnEncoderMove(int direction) { + if (!EditMode()) { + MoveCursor(cursor, direction, SEQX_STEPS-1); + return; + } + + if (note[cursor] + direction < 0 && cursor > 0) { + // If turning past zero, set the mute bit for this step + muted |= (0x01 << cursor); + } else { + note[cursor] = constrain(note[cursor] + direction, 0, SEQX_MAX_VALUE); + muted &= ~(0x01 << cursor); + } + } + + uint64_t OnDataRequest() { + uint64_t data = 0; + for (int s = 0; s < SEQX_STEPS; s++) + { + Pack(data, PackLocation {uint8_t(s * 5),5}, note[s]); + } + Pack(data, PackLocation{SEQX_STEPS * 5, SEQX_STEPS}, muted); + return data; + } + + void OnDataReceive(uint64_t data) { + for (int s = 0; s < SEQX_STEPS; s++) + { + note[s] = Unpack(data, PackLocation {uint8_t(s * 5),5}); + } + muted = Unpack(data, PackLocation {SEQX_STEPS * 5, SEQX_STEPS}); + Reset(); + } + +protected: + void SetHelp() { + // "------------------" <-- Size Guide + help[HEMISPHERE_HELP_DIGITALS] = "1=Clock 2=Reset"; + help[HEMISPHERE_HELP_CVS] = "1=Trans 2=RndSeq"; + help[HEMISPHERE_HELP_OUTS] = "A=CV B=Clk Step 1"; + help[HEMISPHERE_HELP_ENCODER] = "Edit Step"; + // "------------------" <-- Size Guide + } + +private: + int cursor = 0; + char muted = 0; // Bitfield for muted steps; ((muted >> step) & 1) means muted + int note[SEQX_STEPS]; // Sequence value (0 - 30) + int step = 0; // Current sequencer step + bool reset = true; + bool cv2_gate = false; + + void Advance(int starting_point) { + if (++step == SEQX_STEPS) step = 0; + // If all the steps have been muted, stay where we were + if (step_is_muted(step) && step != starting_point) Advance(starting_point); + } + + void Reset() { + step = 0; + reset = true; + } + + void DrawPanel() { + // Sliders + for (int s = 0; s < SEQX_STEPS; s++) + { + //int x = 6 + (12 * s); + int x = 6 + (7 * s); // APD: narrower to fit more + + if (!step_is_muted(s)) { + gfxLine(x, 27, x, 63, (s != cursor) ); // dotted line for unselected steps + + // When cursor, there's a solid slider + if (s == cursor) { + gfxRect(x - 2, BottomAlign(note[s]), 5, 3); // APD + } else { + gfxFrame(x - 2, BottomAlign(note[s]), 5, 3); // APD + } + + // Arrow indicator for current step + if (s == step) { + gfxIcon(x - 3, 17, DOWN_ICON); + //gfxCircle(x, 20, 3); + } + } else if (s == cursor) { + gfxLine(x, 27, x, 63); + } + + if (s == cursor && EditMode()) gfxInvert(x - 2, 27, 5, 37); + } + } + + bool step_is_muted(int step) { + return (muted & (0x01 << step)); + } +}; + + +//////////////////////////////////////////////////////////////////////////////// +//// Hemisphere Applet Functions +/// +/// Once you run the find-and-replace to make these refer to SequenceX, +/// it's usually not necessary to do anything with these functions. You +/// should prefer to handle things in the HemisphereApplet child class +/// above. +//////////////////////////////////////////////////////////////////////////////// +SequenceX SequenceX_instance[2]; + +void SequenceX_Start(bool hemisphere) { + SequenceX_instance[hemisphere].BaseStart(hemisphere); +} + +void SequenceX_Controller(bool hemisphere, bool forwarding) { + SequenceX_instance[hemisphere].BaseController(forwarding); +} + +void SequenceX_View(bool hemisphere) { + SequenceX_instance[hemisphere].BaseView(); +} + +void SequenceX_OnButtonPress(bool hemisphere) { + SequenceX_instance[hemisphere].OnButtonPress(); +} + +void SequenceX_OnEncoderMove(bool hemisphere, int direction) { + SequenceX_instance[hemisphere].OnEncoderMove(direction); +} + +void SequenceX_ToggleHelpScreen(bool hemisphere) { + SequenceX_instance[hemisphere].HelpScreen(); +} + +uint64_t SequenceX_OnDataRequest(bool hemisphere) { + return SequenceX_instance[hemisphere].OnDataRequest(); +} + +void SequenceX_OnDataReceive(bool hemisphere, uint64_t data) { + SequenceX_instance[hemisphere].OnDataReceive(data); +} diff --git a/software/o_c_REV/HEM_ShiftGate.ino b/software/o_c_REV/HEM_ShiftGate.ino index 574d62a58..243de9867 100644 --- a/software/o_c_REV/HEM_ShiftGate.ino +++ b/software/o_c_REV/HEM_ShiftGate.ino @@ -42,15 +42,19 @@ public: } void View() { - gfxHeader(applet_name()); DrawInterface(); } void OnButtonPress() { - if (++cursor > 3) cursor = 0; + CursorAction(cursor, 3); } void OnEncoderMove(int direction) { + if (!EditMode()) { + MoveCursor(cursor, direction, 3); + return; + } + byte ch = cursor > 1 ? 1 : 0; byte c = cursor > 1 ? cursor - 2 : cursor; if (c == 0) length[ch] = constrain(length[ch] + direction, 1, 16); diff --git a/software/o_c_REV/HEM_Shredder.ino b/software/o_c_REV/HEM_Shredder.ino index b57e50d89..e0fd89027 100644 --- a/software/o_c_REV/HEM_Shredder.ino +++ b/software/o_c_REV/HEM_Shredder.ino @@ -39,9 +39,8 @@ public: replay = 0; reset = true; quant_channels = 0; - quantizer.Init(); scale = OC::Scales::SCALE_NONE; - quantizer.Configure(OC::Scales::GetScale(scale), 0xffff); + QuantizerConfigure(0, scale); ForEachChannel(ch) { Shred(ch); } @@ -91,18 +90,21 @@ public: // decrement delay and if it's 0, move the cursor if (--double_click_delay < 1) { // if we hit zero before being reset (aka no double click), move the cursor - if (++cursor > 3) cursor = 0; // we should never be > 3, so this is just for safety + CursorButton(); } } } void View() { - gfxHeader(applet_name()); DrawParams(); DrawMeters(); DrawGrid(); } + void CursorButton() { + CursorAction(cursor, 3); + } + void OnButtonPress() { if (cursor < 2) { // first two cursor params support double-click to shred voltages @@ -115,11 +117,16 @@ public: Shred(cursor); } } else { - if (++cursor > 3) cursor = 0; + CursorButton(); } } void OnEncoderMove(int direction) { + if (!EditMode()) { + MoveCursor(cursor, direction, 3); + return; + } + if (cursor < 2) { range[cursor] += direction; if (bipolar[cursor]) { @@ -140,12 +147,12 @@ public: } } } - if (cursor == 2) quant_channels = constrain(quant_channels += direction, 0, 2); + if (cursor == 2) quant_channels = constrain(quant_channels + direction, 0, 2); if (cursor == 3) { scale += direction; if (scale >= OC::Scales::NUM_SCALES) scale = 0; if (scale < 0) scale = OC::Scales::NUM_SCALES - 1; - quantizer.Configure(OC::Scales::GetScale(scale), 0xffff); + QuantizerConfigure(0, scale); } } @@ -168,7 +175,7 @@ public: bipolar[1] = Unpack(data, PackLocation {12,1}); quant_channels = Unpack(data, PackLocation {16,8}); scale = Unpack(data, PackLocation {24,8}); - quantizer.Configure(OC::Scales::GetScale(scale), 0xffff); + QuantizerConfigure(0, scale); ForEachChannel(ch) { Shred(ch); } @@ -200,7 +207,6 @@ private: bool bipolar[2] = {false, false}; int8_t quant_channels; int scale; - braids::Quantizer quantizer; // Variables to handle imprint confirmation animation int confirm_animation_countdown; @@ -300,7 +306,7 @@ private: ForEachChannel(ch) { current[ch] = sequence[ch][step]; int8_t qc = quant_channels - 1; - if (qc < 0 || qc == ch) current[ch] = quantizer.Process(current[ch], 0, 0); + if (qc < 0 || qc == ch) current[ch] = Quantize(0, current[ch], 0, 0); Out(ch, current[ch]); } } diff --git a/software/o_c_REV/HEM_Shuffle.ino b/software/o_c_REV/HEM_Shuffle.ino index 9a63dbee6..64c98dd6c 100644 --- a/software/o_c_REV/HEM_Shuffle.ino +++ b/software/o_c_REV/HEM_Shuffle.ino @@ -18,6 +18,8 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +// Logarhythm: Added triplets output (3 triggers per 4 input clocks) to the unused 2nd output + class Shuffle : public HemisphereApplet { public: @@ -31,41 +33,79 @@ public: which = 0; cursor = 1; last_tick = 0; + + triplet_which = 0; // Triplets + next_trip_trigger = 0; + triplet_time = 0; } void Controller() { uint32_t tick = OC::CORE::ticks; - if (Clock(1)) { + if (Clock(1)) + { which = 0; // Reset (next trigger will be even clock) last_tick = tick; + triplet_which = 0; // Triplets reset to down beat + } + + // continuously update CV modulated delay values, for display + ForEachChannel(ch) + { + _delay[ch] = delay[ch]; + Modulate(_delay[ch], ch, 0, 100); } - if (Clock(0) && !Gate(1)) { + if (Clock(0)) + { + // Triplets: Track what triplet timing should be to span 4 normal clocks + triplet_time = (ClockCycleTicks(0) * 4) / 3; + if(triplet_which == 0) + { + next_trip_trigger = tick; // Trigger right now (downbeat) + } + + if(++triplet_which > 3) + { + triplet_which = 0; + } + + // Swing which = 1 - which; if (last_tick) { tempo = tick - last_tick; - int16_t d = delay[which] + Proportion(DetentedIn(which), HEMISPHERE_MAX_CV, 100); - d = constrain(d, 0, 100); - uint32_t delay_ticks = Proportion(d, 100, tempo); + uint32_t delay_ticks = Proportion(_delay[which], 100, tempo); next_trigger = tick + delay_ticks; } last_tick = tick; } + // Shuffle output if (tick == next_trigger) ClockOut(0); + + // Logarhythm: Triplets output + if(tick == next_trip_trigger) + { + next_trip_trigger = tick + triplet_time; // Schedule the next triplet output + triplet_time = 0; // Ensure that triplets will stop being scheduled if clocks aren't received + ClockOut(1); + } + } void View() { - gfxHeader(applet_name()); DrawSelector(); DrawIndicator(); } void OnButtonPress() { - cursor = 1 - cursor; + CursorAction(cursor, 1); } void OnEncoderMove(int direction) { + if (!EditMode()) { + MoveCursor(cursor, direction, 1); + return; + } delay[cursor] += direction; delay[cursor] = constrain(delay[cursor], 0, 99); } @@ -87,7 +127,7 @@ protected: // "------------------" <-- Size Guide help[HEMISPHERE_HELP_DIGITALS] = "1=Clock 2=Reset"; help[HEMISPHERE_HELP_CVS] = "1=Odd Mod 2=Even"; - help[HEMISPHERE_HELP_OUTS] = "A=Clock"; + help[HEMISPHERE_HELP_OUTS] = "A=Clock B=Triplets"; help[HEMISPHERE_HELP_ENCODER] = "Odd/Even Delay"; // "------------------" <-- Size Guide } @@ -99,15 +139,19 @@ private: uint32_t next_trigger; // The tick of the next scheduled trigger uint32_t tempo; // Calculated time between ticks + // Logarhythm: Triplets (output on out B) + uint32_t triplet_which; // The current 4 count of clocks used to determine triplet reset + uint32_t next_trip_trigger; // The tick of the next scheduled triplet trigger + uint32_t triplet_time; // Number of ticks between triplet output pulses + // Settings int16_t delay[2]; // Percentage delay for even (0) and odd (1) clock + int16_t _delay[2]; // after CV modulation void DrawSelector() { - for (int i = 0; i < 2; i++) + ForEachChannel(i) { - int16_t d = delay[i] + Proportion(DetentedIn(i), HEMISPHERE_MAX_CV, 100); - d = constrain(d, 0, 100); - gfxPrint(32 + pad(10, d), 15 + (i * 10), d); + gfxPrint(32 + pad(10, _delay[i]), 15 + (i * 10), _delay[i]); gfxPrint("%"); if (cursor == i) gfxCursor(32, 23 + (i * 10), 18); } @@ -130,11 +174,9 @@ private: gfxCircle(53, 47, 1); gfxCircle(53, 55, 1); - for (int n = 0; n < 2; n++) + ForEachChannel(n) { - int16_t d = delay[n] + Proportion(DetentedIn(n), HEMISPHERE_MAX_CV, 100); - d = constrain(d, 0, 100); - int x = Proportion(d, 100, 20) + (n * 20) + 4; + int x = Proportion(_delay[n], 100, 20) + (n * 20) + 4; gfxBitmap(x, 48 - (which == n ? 3 : 0), 8, which == n ? NOTE_ICON : X_NOTE_ICON); } diff --git a/software/o_c_REV/HEM_SkewedLFO.ino b/software/o_c_REV/HEM_SkewedLFO.ino deleted file mode 100644 index 3f78d5a65..000000000 --- a/software/o_c_REV/HEM_SkewedLFO.ino +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright (c) 2018, Jason Justian -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -#define HEM_LFO_HIGH 40000 -#define HEM_LFO_LOW 800 -#define HEM_LFO_MAX_VALUE 120 - -class SkewedLFO : public HemisphereApplet { -public: - - const char* applet_name() { - return "SkewedLFO"; - } - - void Start() { - rate = 61; - skew = 61; - cursor = 0; - cycle_tick = 0; - } - - void Controller() { - rate_mod = get_modification_with_input(0); - skew_mod = get_modification_with_input(1); - - cycle_tick++; - - // Handle reset trigger - if (Clock(0)) cycle_tick = 0; - - // Handle LFO output - if (cycle_tick >= TicksAtRate()) { - cycle_tick = 0; - ClockOut(1); - } - int cv = AmplitudeAtPosition(cycle_tick, HEMISPHERE_MAX_CV) - (HEMISPHERE_MAX_CV / 2); // subtract for bipolar - Out(0, cv); - } - - void View() { - gfxHeader(applet_name()); - - DrawSkewedWaveform(); - DrawRateIndicator(); - DrawWaveformPosition(); - } - - void OnButtonPress() { - cursor = 1 - cursor; - } - - void OnEncoderMove(int direction) { - if (cursor == 0) { - rate = constrain(rate += direction, 0, HEM_LFO_MAX_VALUE); - } else { - skew = constrain(skew += direction, 0, HEM_LFO_MAX_VALUE); - } - } - - uint64_t OnDataRequest() { - uint64_t data = 0; - Pack(data, PackLocation {0, 8}, skew); - Pack(data, PackLocation {8, 8}, rate); - return data; - } - - void OnDataReceive(uint64_t data) { - skew = Unpack(data, PackLocation {0,8}); - rate = Unpack(data, PackLocation {8,8}); - } - -protected: - void SetHelp() { - help[HEMISPHERE_HELP_DIGITALS] = "1=Reset"; - help[HEMISPHERE_HELP_CVS] = "Mod 1=Rate 2=Skew"; - help[HEMISPHERE_HELP_OUTS] = "A=CV B=Clock"; - help[HEMISPHERE_HELP_ENCODER] = "Rate/Skew"; - } - -private: - int rate; // LFO rate between 0 and HEM_LFO_MAX_VALUE, where 0 is slowest - int skew; // LFO skew, where 0 is saw, HEM_LFO_MAX_VALUE is ramp, and 31 is triangle - int cursor; // Whether knob is editing rate (0) or skew (1) - int cycle_tick; // The current tick number within the cycle - int rate_mod; // Modification of rate from CV 1 - int skew_mod; // Modification of skew from CV 2 - - void DrawRateIndicator() { - gfxFrame(1, 15, 62, 6); - int x = Proportion(rate, HEM_LFO_MAX_VALUE, 62); - gfxLine(x, 15, x, 20); - if (cursor == 0) gfxRect(1, 16, x, 4); - } - - void DrawSkewedWaveform() { - int x = Proportion(skew, HEM_LFO_MAX_VALUE, 62); - gfxLine(0, 62, x, 33, cursor == 0); - gfxLine(x, 33, 62, 62, cursor == 0); - - // Draw zero-crossing line - gfxDottedLine(0, 48, 63, 48, 5); - } - - void DrawWaveformPosition() { - int height = AmplitudeAtPosition(cycle_tick, 30); - int x = Proportion(cycle_tick, TicksAtRate(), 62); - gfxLine(x, 63, x, 63 - height); - } - - int AmplitudeAtPosition(int position, int max_amplitude) { - int amplitude = 0; - int effective_skew = constrain(skew + skew_mod, 0, HEM_LFO_MAX_VALUE); - int ticks_at_rate = TicksAtRate(); - int fall_point = Proportion(effective_skew, HEM_LFO_MAX_VALUE, ticks_at_rate); - if (position < fall_point) { - // Rise portion - amplitude = Proportion(position, fall_point, max_amplitude); - } else { - // Fall portion - amplitude = Proportion(ticks_at_rate - position, ticks_at_rate - fall_point, max_amplitude); - } - - return amplitude; - } - - int TicksAtRate() { - int effective_rate = constrain(rate + rate_mod, 0, HEM_LFO_MAX_VALUE); - int inv_rate = HEM_LFO_MAX_VALUE - effective_rate; - int range = HEM_LFO_HIGH - HEM_LFO_LOW; - int ticks_at_rate = Proportion(inv_rate, HEM_LFO_MAX_VALUE, range) + HEM_LFO_LOW; - return ticks_at_rate; - } - - int get_modification_with_input(int in) { - int mod = 0; - mod = Proportion(DetentedIn(in), HEMISPHERE_MAX_CV, HEM_LFO_MAX_VALUE / 2); - return mod; - } -}; - - -//////////////////////////////////////////////////////////////////////////////// -//// Hemisphere Applet Functions -/// -/// Once you run the find-and-replace to make these refer to SkewedLFO, -/// it's usually not necessary to do anything with these functions. You -/// should prefer to handle things in the HemisphereApplet child class -/// above. -//////////////////////////////////////////////////////////////////////////////// -SkewedLFO SkewedLFO_instance[2]; - -void SkewedLFO_Start(bool hemisphere) { - SkewedLFO_instance[hemisphere].BaseStart(hemisphere); -} - -void SkewedLFO_Controller(bool hemisphere, bool forwarding) { - SkewedLFO_instance[hemisphere].BaseController(forwarding); -} - -void SkewedLFO_View(bool hemisphere) { - SkewedLFO_instance[hemisphere].BaseView(); -} - -void SkewedLFO_OnButtonPress(bool hemisphere) { - SkewedLFO_instance[hemisphere].OnButtonPress(); -} - -void SkewedLFO_OnEncoderMove(bool hemisphere, int direction) { - SkewedLFO_instance[hemisphere].OnEncoderMove(direction); -} - -void SkewedLFO_ToggleHelpScreen(bool hemisphere) { - SkewedLFO_instance[hemisphere].HelpScreen(); -} - -uint64_t SkewedLFO_OnDataRequest(bool hemisphere) { - return SkewedLFO_instance[hemisphere].OnDataRequest(); -} - -void SkewedLFO_OnDataReceive(bool hemisphere, uint64_t data) { - SkewedLFO_instance[hemisphere].OnDataReceive(data); -} diff --git a/software/o_c_REV/HEM_Slew.ino b/software/o_c_REV/HEM_Slew.ino index 48405dfa1..1bdabe8b5 100644 --- a/software/o_c_REV/HEM_Slew.ino +++ b/software/o_c_REV/HEM_Slew.ino @@ -44,11 +44,11 @@ public: int segment = (input > signal[ch]) ? rise : fall; simfloat remaining = input - signal[ch]; - // The number of ticks it would take to get from 0 to HEMISPHERE_MAX_CV + // The number of ticks it would take to get from 0 to HEMISPHERE_MAX_INPUT_CV int max_change = Proportion(segment, HEM_SLEW_MAX_VALUE, HEM_SLEW_MAX_TICKS); // The number of ticks it would take to move the remaining amount at max_change - int ticks_to_remaining = Proportion(simfloat2int(remaining), HEMISPHERE_MAX_CV, max_change); + int ticks_to_remaining = Proportion(simfloat2int(remaining), HEMISPHERE_MAX_INPUT_CV, max_change); if (ticks_to_remaining < 0) ticks_to_remaining = -ticks_to_remaining; simfloat delta; @@ -65,7 +65,6 @@ public: } void View() { - gfxHeader(applet_name()); DrawIndicator(); } @@ -75,11 +74,11 @@ public: void OnEncoderMove(int direction) { if (cursor == 0) { - rise = constrain(rise += direction, 0, HEM_SLEW_MAX_VALUE); + rise = constrain(rise + direction, 0, HEM_SLEW_MAX_VALUE); last_ms_value = Proportion(rise, HEM_SLEW_MAX_VALUE, HEM_SLEW_MAX_TICKS) / 17; } else { - fall = constrain(fall += direction, 0, HEM_SLEW_MAX_VALUE); + fall = constrain(fall + direction, 0, HEM_SLEW_MAX_VALUE); last_ms_value = Proportion(fall, HEM_SLEW_MAX_VALUE, HEM_SLEW_MAX_TICKS) / 17; } last_change_ticks = OC::CORE::ticks; diff --git a/software/o_c_REV/HEM_Squanch.ino b/software/o_c_REV/HEM_Squanch.ino index 4620b20f8..08cb279e8 100644 --- a/software/o_c_REV/HEM_Squanch.ino +++ b/software/o_c_REV/HEM_Squanch.ino @@ -26,15 +26,22 @@ class Squanch : public HemisphereApplet { public: + enum SquanchCursor { + SHIFT1, SHIFT2, + SCALE, ROOT_NOTE, + + LAST_SETTING = ROOT_NOTE + }; const char* applet_name() { return "Squanch"; } void Start() { - quantizer.Init(); scale = 5; - quantizer.Configure(OC::Scales::GetScale(scale), 0xffff); + ForEachChannel(ch) { + QuantizerConfigure(ch, scale); + } } void Controller() { @@ -52,8 +59,8 @@ public: // output, the output is raised by one octave when Digital 2 is gated. int32_t shift_alt = (ch == 1) ? DetentedIn(1) : Gate(1) * (12 << 7); - int32_t quantized = quantizer.Process(pitch, 0, shift[ch]); - Out(ch, quantized + shift_alt); + int32_t quantized = Quantize(ch, pitch + shift_alt, root << 7, shift[ch]); + Out(ch, quantized); last_note[ch] = quantized; } } @@ -61,23 +68,37 @@ public: } void View() { - gfxHeader(applet_name()); DrawInterface(); } void OnButtonPress() { - if (++cursor > 2) cursor = 0; + CursorAction(cursor, LAST_SETTING); } void OnEncoderMove(int direction) { - if (cursor == 2) { // Scale selection + if (!EditMode()) { + MoveCursor(cursor, direction, LAST_SETTING); + return; + } + + switch (cursor) { + case SHIFT1: + case SHIFT2: + shift[cursor] = constrain(shift[cursor] + direction, -48, 48); + break; + + case SCALE: scale += direction; if (scale >= OC::Scales::NUM_SCALES) scale = 0; if (scale < 0) scale = OC::Scales::NUM_SCALES - 1; - quantizer.Configure(OC::Scales::GetScale(scale), 0xffff); + ForEachChannel(ch) + QuantizerConfigure(ch, scale); continuous = 1; // Re-enable continuous mode when scale is changed - } else { - shift[cursor] = constrain(shift[cursor] + direction, -48, 48); + break; + + case ROOT_NOTE: + root = constrain(root + direction, 0, 11); + break; } } @@ -86,6 +107,7 @@ public: Pack(data, PackLocation {0,8}, scale); Pack(data, PackLocation {8,8}, shift[0] + 48); Pack(data, PackLocation {16,8}, shift[1] + 48); + Pack(data, PackLocation {24,4}, root); return data; } @@ -93,7 +115,10 @@ public: scale = Unpack(data, PackLocation {0,8}); shift[0] = Unpack(data, PackLocation {8,8}) - 48; shift[1] = Unpack(data, PackLocation {16,8}) - 48; - quantizer.Configure(OC::Scales::GetScale(scale), 0xffff); + root = Unpack(data, PackLocation {24,4}); + root = constrain(root, 0, 11); + ForEachChannel(ch) + QuantizerConfigure(ch, scale); } protected: @@ -110,37 +135,43 @@ private: int cursor; // 0=A shift, 1=B shift, 2=Scale bool continuous = 1; int last_note[2]; // Last quantized note - braids::Quantizer quantizer; // Settings int scale; + uint8_t root; int16_t shift[2]; void DrawInterface() { - const uint8_t notes[2][8] = {{0xc0, 0xe0, 0xe0, 0xe0, 0x7f, 0x02, 0x14, 0x08}, - {0xc0, 0xa0, 0xa0, 0xa0, 0x7f, 0x00, 0x00, 0x00}}; - - // Display icon if clocked - if (!continuous) gfxIcon(56, 25, CLOCK_ICON); + const uint8_t * notes[2] = {NOTE_ICON, NOTE2_ICON}; // Shift for A/C - gfxIcon(1, 14, notes[0]); - gfxPrint(11, 15, shift[0] > -1 ? "+" : ""); - gfxPrint(shift[0]); + ForEachChannel(ch) { + gfxIcon(1 + ch*32, 14, notes[ch]); + gfxPrint(10 + pad(10, shift[ch]) + ch*32, 15, shift[ch] > -1 ? "+" : ""); + gfxPrint(shift[ch]); + } - // Shift for B/D - gfxIcon(32, 14, notes[1]); - gfxPrint(43 + pad(10, shift[1]), 15, shift[1] > -1 ? "+" : ""); - gfxPrint(shift[1]); + // Scale & Root Note + gfxIcon(1, 24, SCALE_ICON); + gfxPrint(10, 25, OC::scale_names_short[scale]); + gfxPrint(40, 25, OC::Strings::note_names_unpadded[root]); - // Scale - gfxBitmap(1, 24, 8, SCALE_ICON); - gfxPrint(12, 25, OC::scale_names_short[scale]); + // Display icon if clocked + if (!continuous) gfxIcon(56, 25, CLOCK_ICON); // Cursors - if (cursor == 0) gfxCursor(10, 23, 18); - if (cursor == 1) gfxCursor(42, 23, 18); - if (cursor == 2) gfxCursor(13, 33, 30); // Scale Cursor + switch (cursor) { + case SHIFT1: + case SHIFT2: + gfxCursor(10 + (cursor - SHIFT1)*32, 23, 19); + break; + case SCALE: + gfxCursor(10, 33, 25); + break; + case ROOT_NOTE: + gfxCursor(40, 33, 13); + break; + } // Little note display @@ -149,9 +180,10 @@ private: ForEachChannel(ch) { int semitone = (last_note[ch] / 128) % 12; - int note_x = semitone * 4; // 4 pixels per semitone - if (note_x < 0) note_x = 0; - gfxIcon(10 + note_x, 41 + (10 * ch), notes[ch]); + while (semitone < 0) semitone += 12; + int note_x = semitone * 3; // pixels per semitone + gfxPrint(0, 41 + 10*ch, midi_note_numbers[MIDIQuantizer::NoteNumber(last_note[ch])] ); + gfxIcon(19 + note_x, 41 + (10 * ch), notes[ch]); } } diff --git a/software/o_c_REV/HEM_Stairs.ino b/software/o_c_REV/HEM_Stairs.ino index 390b62239..d9f5ebe95 100644 --- a/software/o_c_REV/HEM_Stairs.ino +++ b/software/o_c_REV/HEM_Stairs.ino @@ -32,8 +32,11 @@ class Stairs : public HemisphereApplet { public: - // Icons made with http://beigemaze.com/bitmap8x8.html (Thanks for making this public!) - const uint8_t STAIRS_ICON[8] = {0x00,0x20,0x20,0x38,0x08,0x0e,0x02,0x02}; // Some stairs going up + enum StairsCursor { + STEPS, + DIRECTION, + RANDOM + }; const char* applet_name() { return "Stairs"; @@ -195,34 +198,34 @@ public: } void View() { - gfxHeader(applet_name()); DrawDisplay(); } void OnButtonPress() { - if(++cursor > 2) cursor = 0; - - ResetCursor(); // Reset blink so it's immediately visible when moved + CursorAction(cursor, 2); } void OnEncoderMove(int direction) { - if (cursor == 0) - { - steps = constrain( steps += direction, 0, HEM_STAIRS_MAX_STEPS-1); // constrain includes max + if (!EditMode()) { + MoveCursor(cursor, direction, 2); + return; } - else if (cursor == 1) - { - dir = constrain( dir += direction, 0, 2); + + switch ((StairsCursor)cursor) { + case STEPS: + steps = constrain( steps + direction, 0, HEM_STAIRS_MAX_STEPS-1); // constrain includes max + break; + case DIRECTION: + dir = constrain( dir + direction, 0, 2); // Don't change current direction if up/down mode if(dir != 1) - { reverse = (dir == 2); // Change current trend to up or down if required - } - } - else - { + + break; + case RANDOM: rand = 1-rand; + break; } } @@ -271,8 +274,7 @@ private: //int8_t graph_pos; // Current position on the graph //int8_t graph_points[HEM_STAIRS_GRAPH_SIZE]; - - int cursor; // 0 = steps, 1 = direction, 2 = random + int cursor; // StairsCursor void DrawDisplay() { @@ -337,17 +339,16 @@ private: */ // Cursor - if(cursor == 0) - { + switch ((StairsCursor)cursor) { + case STEPS: gfxCursor(16, 23, 15); // flashing underline on the number - } - else if(cursor == 1) - { + break; + case DIRECTION: gfxCursor(34, 23, 9); // flashing underline on up/down icon - } - else - { + break; + case RANDOM: gfxCursor(16, 43, 20); // flashing underline on the random setting + break; } } }; diff --git a/software/o_c_REV/HEM_Switch.ino b/software/o_c_REV/HEM_Switch.ino index 2d772a4ed..488cf6fb9 100644 --- a/software/o_c_REV/HEM_Switch.ino +++ b/software/o_c_REV/HEM_Switch.ino @@ -49,7 +49,6 @@ public: } void View() { - gfxHeader(applet_name()); DrawCaptions(); DrawIndicator(); gfxSkyline(); diff --git a/software/o_c_REV/HEM_TB3PO.ino b/software/o_c_REV/HEM_TB3PO.ino index b9bd2ad98..68e4cfe07 100644 --- a/software/o_c_REV/HEM_TB3PO.ino +++ b/software/o_c_REV/HEM_TB3PO.ino @@ -18,7 +18,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. - // TB-3PO Hemisphere Applet // A random generator of TB-303 style acid patterns, closely following 303 gate timings // CV output 1 is pitch, CV output 2 is gates @@ -28,7 +27,6 @@ // Contributions: // Thanks to Github/Muffwiggler user Qiemem for adding reseed(), to break the small cycle of available seed values that was occurring in practice - #include "braids_quantizer.h" #include "braids_quantizer_scales.h" #include "OC_scales.h" @@ -36,956 +34,713 @@ #define ACID_HALF_STEPS 16 #define ACID_MAX_STEPS 32 -class TB_3PO : public HemisphereApplet -{ +class TB_3PO: public HemisphereApplet { public: - const char* applet_name() { // Maximum 10 characters - return "TB-3PO"; + const char * applet_name() { // Maximum 10 characters + return "TB-3PO"; } - void Start() - { - manual_reset_flag = 0; - rand_apply_anim = 0; - curr_step_semitone = 0; - - root = 0; - octave_offset = 0; - - // Init the quantizer for selecting pitches / CVs from - scale = 29; // GUNA scale sounds cool //OC::Scales::SCALE_SEMI; // semi sounds pretty bunk - quantizer.Init(); - set_quantizer_scale(scale); + void Start() { + manual_reset_flag = 0; + rand_apply_anim = 0; + curr_step_semitone = 0; - // This quantizer is for displaying a keyboard graphic, mapping the current scale to semitones - display_semi_quantizer.Init(); - display_semi_quantizer.Configure(OC::Scales::GetScale(OC::Scales::SCALE_SEMI), 0xffff); - - density = 12; - density_encoder_display = 0; + root = 0; + octave_offset = 0; - num_steps = 16; - - gate_off_clock = 0; - cycle_time = 0; - - curr_gate_cv = 0; - curr_pitch_cv = 0; - - slide_start_cv = 0; - slide_end_cv = 0; - - //transpose_note_in = 0; - - lock_seed = 0; - reseed(); - regenerate_all(); + // Init the quantizer for selecting pitches / CVs from + scale = 29; // GUNA scale sounds cool //OC::Scales::SCALE_SEMI; // semi sounds pretty bunk + quantizer = GetQuantizer(0); + set_quantizer_scale(scale); - } + density = 12; + density_encoder_display = 0; - void Controller() - { - // Track timing to set gate timing at ~32nd notes per recent clocks - int this_tick = OC::CORE::ticks; - - // Regenerate / Reset - if (Clock(1) || manual_reset_flag) - { - manual_reset_flag = 0; - // If the seed is not locked, then randomize it on every reset pulse - // Otherwise, the user has locked it, so leave it as set - if(lock_seed == 0) - { - reseed(); - } + num_steps = 16; - // Apply the seed to regenerate the pattern` - // This is deterministic so if the seed is held, the pattern will not change - regenerate_all(); + gate_off_clock = 0; + cycle_time = 0; - // Reset step - step = 0; - } + curr_gate_cv = 0; + curr_pitch_cv = 0; - // Control transpose from cv1 (Very fun to wiggle) - //transpose_note_in = 99; // Display only: flag no xpose for 0v (would be a pitch like -24, etc) - transpose_cv = 0; - if (DetentedIn(0)) - { - // Original: 1v == 12 scale steps - // Note: This appears to frequently result in coming up short on some notes when using a cv keyboard (e.g. c# might be c) (prefer interpreting this via a windowed quantizer?) - //transpose_note_in = In(0) / 128; // 128 ADC steps per semitone - - // This will accuarately get notes from an imperfect cv keyboard in semitones - //transpose_cv = display_semi_quantizer.Process(In(0), 0, 0); // Use root == 0 to start at c - //transpose_note_in = display_semi_quantizer.GetLatestNoteNumber() - 64; - - // Quantize the transpose CV to the same scale as the sequence, always based on c. - // This allows a CV keyboard or sequencer to work reliably to transpose (e.g. every c is another octave) regardless of scale. - // However, the transposition is limited to only in-scale notes so arpeggiations via LFOs, etc are still easily done. - // (This CV is summed to the sequence pitch CV directly before output, rather than affecting its note indices.) - transpose_cv = quantizer.Process(In(0), 0, 0); // Use root == 0 to start at c - //transpose_note_in = quantizer.GetLatestNoteNumber() - 64; // For debug readout! - } - - // Offset density from its encoder-set value with cv2 (Wiggling can build up & break down patterns nicely, especially if seed is locked) - { - // -2.5v to +5v (HEMISPHERE_MAX_CV), giving about -8 to +15 added to encoder density value - // Note: DetentedIn is used to cut out noise near 0, even though it's being quantized to int below (primarily to make the cv icon work better) - int signal = constrain(DetentedIn(1), -HEMISPHERE_3V_CV, HEMISPHERE_MAX_CV); // Allow negative to go about as far as it will reach - density_cv = Proportion(abs(signal), HEMISPHERE_MAX_CV, 15); // Apply proportion uniformly to +- voltages as + for symmetry (Avoids rounding differences) - if(signal <0) - { - density_cv *= -1; // Restore negative sign if -v - } - density = static_cast(constrain(density_encoder + density_cv, 0, 14)); - } - - // Wait for the ADC since transpose CV is needed - if (Clock(0)) - { - cycle_time = ClockCycleTicks(0); // Track latest interval of clock 0 for gate timings + slide_start_cv = 0; + slide_end_cv = 0; - // Sneak this in here before clock is 'applied' and the next step is reached, to re-apply density to the pattern if required - regenerate_if_density_or_scale_changed(); // Flag to do the actual update at end of Controller() - - StartADCLag(); - } + lock_seed = 0; + reseed(); + regenerate_all(); - if (EndOfADCLag() && !Gate(1)) // Reset not held - { - int step_pv = step; - - // Advance the step - step = get_next_step(step); - - // Was step before this one set to 'slide'? - // If so, engage a the 'slide circuit' from its pitch to this new step's pitch - if(step_is_slid(step_pv)) - { - // Slide begins from the prior step's pitch (TODO: just use current dac output?) - slide_start_cv = get_pitch_for_step(step_pv); - - // Jump current pitch to prior step's value if not there already - // TODO: Consider just gliding from whereever it is? - curr_pitch_cv = slide_start_cv; - - // Slide target is this step's pitch - slide_end_cv = get_pitch_for_step(step); - } - else - { - // Prior step was not slid, so snap to current pitch - curr_pitch_cv = get_pitch_for_step(step); - slide_start_cv = curr_pitch_cv; - slide_end_cv = curr_pitch_cv; - } - - // Open the gate if this step is gated, or hold it open for at least 1/2 step if the prior step was slid - if(step_is_gated(step) || step_is_slid(step_pv)) - { - // Accented gates get a higher voltage, so it can drive VCA gain in addition to triggering envelope generators - curr_gate_cv = step_is_accent(step) ? HEMISPHERE_MAX_CV : HEMISPHERE_3V_CV; + } - // On each clock, schedule the next clock at a multiplied rate - int gate_time = (cycle_time / 2); // multiplier of 2 - gate_off_clock = this_tick + gate_time; - } + void Controller() { + const int this_tick = OC::CORE::ticks; - // When changing steps, compute the nearest semitone at the base octave to show on the keyboard - curr_step_semitone = get_semitone_for_step(step); - - } - - // Update the clock multiplier for gate off timings - if(curr_gate_cv > 0 && gate_off_clock > 0 && this_tick >= gate_off_clock) - { - // Handle turning the gate off, unless sliding - gate_off_clock = 0; - - // Do nothing if the current step should be slid - if(!step_is_slid(step)) - { - curr_gate_cv = 0;//HEMISPHERE_CENTER_CV; - } + if (Clock(1) || manual_reset_flag) { + manual_reset_flag = 0; + if (lock_seed == 0) { + reseed(); } - // Update slide if needed - if(curr_pitch_cv != slide_end_cv) - { - // This gives constant rate linear glide (but we want expo fixed-time): - // curr_pitch_cv += (slide_end_cv - curr_pitch_cv > 0 ? 1 : -1); - - // (This could optionally use peak's lut_env_expo[] for interpolation instead) - // Expo slide (code assist from CBS) - int k = 0x0003; // expo constant: 0 = infinite time to settle, 0xFFFF ~= 1, fastest rate - // Choose this to give 303-like pitch slide timings given the O&C's update rate - // k = 0x3 sounds good here with >>=18 - - int x = slide_end_cv; - x -= curr_pitch_cv; - x >>= 18; - x *= k; - curr_pitch_cv += x; - - // TODO: Check constrain - if(slide_start_cv < slide_end_cv) - { - curr_pitch_cv = constrain(curr_pitch_cv, slide_start_cv, slide_end_cv); + regenerate_all(); - // set a bit if constrain was needed - } - else - { - curr_pitch_cv = constrain(curr_pitch_cv, slide_end_cv, slide_start_cv); + step = 0; + } - // set a bit if constrain was needed - } - } + transpose_cv = 0; + if (DetentedIn(0)) { + transpose_cv = quantizer->Process(In(0), 0, 0); // Use root == 0 to start at c + } - // Pitch out - Out(0, curr_pitch_cv); - - // Gate out (as CV) - Out(1, curr_gate_cv); + density_cv = Proportion(DetentedIn(1), HEMISPHERE_MAX_INPUT_CV, 15); + density = static_cast(constrain(density_encoder + density_cv, 0, 14)); + if (Clock(0)) { + cycle_time = ClockCycleTicks(0); // Track latest interval of clock 0 for gate timings - // Timesliced generation of new patterns, if triggered - // Do this last to not interfere with the body of the time for this hemisphere's update - // (This is speculation without knowing how to best profile performance on this system) - update_regeneration(); - } + regenerate_if_density_or_scale_changed(); // Flag to do the actual update at end of Controller() - void View() { - gfxHeader(applet_name()); - DrawGraphics(); + StartADCLag(); } - void OnButtonPress() + if (EndOfADCLag() && !Gate(1)) // Reset not held { - if(cursor == 0) - { - cursor = lock_seed ? 1 : 5; + int step_pv = step; + + step = get_next_step(step); + + if (step_is_slid(step_pv)) { + slide_start_cv = get_pitch_for_step(step_pv); + + // TODO: Consider just gliding from whereever it is? + curr_pitch_cv = slide_start_cv; + + slide_end_cv = get_pitch_for_step(step); + } else { + curr_pitch_cv = get_pitch_for_step(step); + slide_start_cv = curr_pitch_cv; + slide_end_cv = curr_pitch_cv; } - else if (++cursor > 8) - { - cursor = 0; + + if (step_is_gated(step) || step_is_slid(step_pv)) { + curr_gate_cv = step_is_accent(step) ? HEMISPHERE_MAX_CV : HEMISPHERE_3V_CV; + + int gate_time = (cycle_time / 2); // multiplier of 2 + gate_off_clock = this_tick + gate_time; } - - ResetCursor(); // Reset blink so it's immediately visible when moved + + curr_step_semitone = get_semitone_for_step(step); + } - void OnEncoderMove(int direction) - { - if(cursor == 0) - { - // Toggle the seed between auto (randomized every reset input pulse) - // or Manual (seed becomes locked, cursor can be moved to edit each digit) - lock_seed += direction; - - // See if the turn would move beyond the random die to the left or the lock to the right - // If so, take this as a manual input just like receiving a reset pulse (handled in Controller()) - // regenerate_all() will honor the random or locked icon shown (seed will be randomized or not) - manual_reset_flag = (lock_seed > 1 || lock_seed < 0) ? 1 : 0; - - // constrain to legal values before regeneration - lock_seed = constrain(lock_seed, 0, 1); - } - else if (cursor <= 4) - { - // Editing one of the 4 hex digits of the seed - - // cursor==1 is at the most significant byte, - // cursor==4 is at least significant byte - int byte_offs = 4-cursor; - int shift_amt = byte_offs*4; - - uint32_t nib = (seed >> shift_amt)& 0xf; // Abduct the nibble - uint8_t c = nib; - c = constrain(c+direction, 0, 0xF); // Edit the nibble - nib = c; - uint32_t mask = 0xf; - seed &= ~(mask << shift_amt); // Clear bits where this nibble lives - seed |= (nib << shift_amt); // Move the nibble to its home + if (curr_gate_cv > 0 && gate_off_clock > 0 && this_tick >= gate_off_clock) { + gate_off_clock = 0; + + if (!step_is_slid(step)) { + curr_gate_cv = 0; } - else if (cursor == 5) - { - density_encoder = constrain(density_encoder + direction, 0, 14); // Treated as a bipolar -7 to 7 in practice - density_encoder_display = 400; // How long to show the encoder version of density in the number display for - - //density = constrain(density + direction, 0, 14); // Treated as a bipolar -7 to 7 in practice - - // Disabled: Let this occur when detected on the next step - //regenerate_density_if_changed(); - } - else if(cursor == 6) - { - // Scale selection - scale += direction; - if (scale >= OC::Scales::NUM_SCALES) scale = 0; - if (scale < 0) scale = OC::Scales::NUM_SCALES - 1; - // Apply to the quantizer - set_quantizer_scale(scale); - -// New: Constrain root to scale size (leave oct offset where it is) - int max_root = scale_size > 12 ? 12 : scale_size; - if(max_root > 0) - { - root = constrain(root, 0, max_root-1); - } + } + + if (curr_pitch_cv != slide_end_cv) { + int k = 0x0003; // expo constant: 0 = infinite time to settle, 0xFFFF ~= 1, fastest rate + // Choose this to give 303-like pitch slide timings given the O&C's update rate + // k = 0x3 sounds good here with >>=18 + + int x = slide_end_cv; + x -= curr_pitch_cv; + x >>= 18; + x *= k; + curr_pitch_cv += x; + + // TODO: Check constrain, set a bit if constrain was needed + if (slide_start_cv < slide_end_cv) { + curr_pitch_cv = constrain(curr_pitch_cv, slide_start_cv, slide_end_cv); + } else { + curr_pitch_cv = constrain(curr_pitch_cv, slide_end_cv, slide_start_cv); } - else if(cursor == 7) - { - // Root note selection + } - // No oct version - //root = constrain(root + direction, 0, 11); + Out(0, curr_pitch_cv); + Out(1, curr_gate_cv); - // Add in handling for octave settings without affecting root's range - int r = root + direction; + // Timesliced generation of new patterns, if triggered + // Do this last to not interfere with the body of the time for this hemisphere's update + // (This is speculation without knowing how to best profile performance on this system) + update_regeneration(); + } - int max_root = scale_size > 12 ? 12 : scale_size; - - //if(direction > 0 && r > 11 && octave_offset < 3) - if(direction > 0 && r >= max_root && octave_offset < 3) - { - ++octave_offset; // Go up to next octave - r = 0; // Roll around root note - } - else if(direction < 0 && r < 0 && octave_offset > -3) - { - --octave_offset; + void View() { + DrawGraphics(); + } - r = max_root-1; - //r = 11; // Roll around root note - } + void OnButtonPress() { + CursorAction(cursor, 8); + } - // Limit root value - //root = constrain(r, 0, 11); - root = constrain(r, 0, max_root-1); + void OnEncoderMove(int direction) { + if (!EditMode()) { // move cursor + MoveCursor(cursor, direction, 8); - } - else - { - num_steps = constrain(num_steps + direction, 1, 32); - } + if (!lock_seed && cursor == 1) cursor = 5; // skip from 1 to 5 if not locked + if (!lock_seed && cursor == 4) cursor = 0; // skip from 4 to 0 if not locked + + return; } - uint64_t OnDataRequest() { - uint64_t data = 0; - - Pack(data, PackLocation {0,8}, scale); - Pack(data, PackLocation {8,4}, root); - Pack(data, PackLocation {12,4}, density_encoder); - Pack(data, PackLocation {16,16}, seed); - Pack(data, PackLocation {32,8}, octave_offset); - return data; + // edit param + switch (cursor) { + case 0: + lock_seed += direction; + + manual_reset_flag = (lock_seed > 1 || lock_seed < 0); + + lock_seed = constrain(lock_seed, 0, 1); + break; + case 1: + case 2: + case 3: + case 4: { // Editing one of the 4 hex digits of the seed + int byte_offs = 4 - cursor; + int shift_amt = byte_offs * 4; + + uint32_t nib = (seed >> shift_amt) & 0xf; // Abduct the nibble + uint8_t c = nib; + c = constrain(c + direction, 0, 0xF); // Edit the nibble + nib = c; + uint32_t mask = 0xf; + seed &= ~(mask << shift_amt); // Clear bits where this nibble lives + seed |= (nib << shift_amt); // Move the nibble to its home + break; + } + case 5: // density + density_encoder = constrain(density_encoder + direction, 0, 14); // Treated as a bipolar -7 to 7 in practice + density_encoder_display = 400; // How long to show the encoder version of density in the number display for + + break; + case 6: { // Scale selection + scale += direction; + if (scale >= OC::Scales::NUM_SCALES) scale = 0; + if (scale < 0) scale = OC::Scales::NUM_SCALES - 1; + set_quantizer_scale(scale); + break; } + case 7: { // Root note selection - void OnDataReceive(uint64_t data) { - - scale = Unpack(data, PackLocation {0,8}); - root = Unpack(data, PackLocation {8,4}); - density_encoder = Unpack(data, PackLocation {12,4}); - seed = Unpack(data, PackLocation {16,16}); - octave_offset = Unpack(data, PackLocation {32,8}); + int r = root + direction; + const int max_root = 12; - //const braids::Scale & quant_scale = OC::Scales::GetScale(scale); - set_quantizer_scale(scale); - - //scale = constrain(0, OC::Scales::NUM_SCALES-1); - root = constrain(root, 0, 11); - density_encoder = constrain(density_encoder, 0, 14); // Internally just positive - density = density_encoder; - octave_offset = constrain(octave_offset,-3,3); - - // Restore all seed-derived settings! - regenerate_all(); + if (direction > 0 && r >= max_root && octave_offset < 3) { + ++octave_offset; // Go up to next octave + r = 0; // Roll around root note + } else if (direction < 0 && r < 0 && octave_offset > -3) { + --octave_offset; - // Reset step position - step = 0; + r = max_root - 1; + } + + root = constrain(r, 0, max_root - 1); + + break; } + case 8: // pattern length + num_steps = constrain(num_steps + direction, 1, 32); + break; + } //switch + } //OnEncoderMove + + uint64_t OnDataRequest() { + uint64_t data = 0; + + Pack(data, PackLocation { 0, 8 }, scale); + Pack(data, PackLocation { 8, 4 }, root); + Pack(data, PackLocation { 12, 4 }, density_encoder); + Pack(data, PackLocation { 16, 16 }, seed); + Pack(data, PackLocation { 32, 8 }, octave_offset); + Pack(data, PackLocation { 40, 5 }, num_steps - 1); + return data; + } + + void OnDataReceive(uint64_t data) { + + scale = Unpack(data, PackLocation { 0, 8 }); + root = Unpack(data, PackLocation { 8, 4 }); + density_encoder = Unpack(data, PackLocation { 12, 4 }); + seed = Unpack(data, PackLocation { 16, 16 }); + octave_offset = Unpack(data, PackLocation { 32, 8 }); + num_steps = Unpack(data, PackLocation { 40, 5 }) + 1; + + set_quantizer_scale(scale); + + root = constrain(root, 0, 11); + density_encoder = constrain(density_encoder, 0, 14); // Internally just positive + density = density_encoder; + octave_offset = constrain(octave_offset, -3, 3); + + // Restore all seed-derived settings! + regenerate_all(); + + // Reset step position + step = 0; + } + +protected: + void SetHelp() { + // "------------------" <-- Size Guide + help[HEMISPHERE_HELP_DIGITALS] = "1=Clock 2=Regen"; + help[HEMISPHERE_HELP_CVS] = "1=Transp 2=Density"; + help[HEMISPHERE_HELP_OUTS] = "A=CV+glide B=Gate"; + help[HEMISPHERE_HELP_ENCODER] = "seed/dens/qnt/len"; + // "------------------" <-- Size Guide + } + +private: + int cursor = 0; + braids::Quantizer * quantizer; + + // User settings + + // Bool + bool manual_reset_flag = 0; // Manual trigger to reset/regen + + // bool + int lock_seed; // If 1, the seed won't randomize (and manual editing is enabled) + + uint16_t seed; // The random seed that deterministically builds the sequence + + int scale; // Active quantization & generation scale + uint8_t root; // Root note + int8_t octave_offset; // Manual octave offset (based on size of current scale, added to root note) + + uint8_t density; // The density parameter controls a couple of things at once. Its 0-14 value is mapped to -7..+7 range + // The larger the magnitude from zero in either direction, the more dense the note patterns are (fewer rests) + // For values mapped < 0 (e.g. left range,) the more negative the value is, the less chance consecutive pitches will + // change from the prior pitch, giving repeating lines (note: octave jumps still apply) + + uint8_t current_pattern_density; // Track what density value was used to generate the current pattern (to detect if regeneration is required) + + // Density controls (Encoder sets center point, CV can apply +-) + int density_encoder; // density value contributed by the encoder (center point) + int density_cv; // density value (+-) contributed by CV + int density_encoder_display; // Countdown of frames to show the encoder's density value (centerpoint) + uint8_t num_steps; // How many steps of the generated pattern to play before looping + + // Playback + uint8_t step = 0; // Current sequencer step + + int32_t transpose_cv; // Quantized transpose in cv + + // Generated sequence data + uint32_t gates = 0; // Bitfield of gates; ((gates >> step) & 1) means gate + uint32_t slides = 0; // Bitfield of slide steps; ((slides >> step) & 1) means slide + uint32_t accents = 0; // Bitfield of accent steps; ((accents >> step) & 1) means accent + uint32_t oct_ups = 0; // Bitfield of octave ups + uint32_t oct_downs = 0; // Bitfield of octave downs + uint8_t notes[ACID_MAX_STEPS]; // Note values + + uint8_t scale_size; // The size of the currently set quantizer scale (for octave detection, etc) + uint8_t current_pattern_scale_size; // Track what size scale was used to render the current pattern (for change detection) + + // For gate timing as ~32nd notes at tempo, detect clock rate like a clock multiplier + int gate_off_clock; // Scheduled cycle at which the gate should be turned off (when applicable) + int cycle_time; // Cycle time between the last two clock inputs + + // CV output values + int32_t curr_gate_cv = 0; + int32_t curr_pitch_cv = 0; - protected: - void SetHelp() { - // "------------------" <-- Size Guide - help[HEMISPHERE_HELP_DIGITALS] = "1=Clock 2=Regen"; - help[HEMISPHERE_HELP_CVS] = "1=Transp 2=Density"; - help[HEMISPHERE_HELP_OUTS] = "A=CV+glide B=Gate"; - help[HEMISPHERE_HELP_ENCODER] = "seed/dens/qnt/len"; - // "------------------" <-- Size Guide + // Pitch slide cv tracking + int32_t slide_start_cv = 0; + int32_t slide_end_cv = 0; + + // Display + int curr_step_semitone = 0; // The pitch converted to nearest semitone, for showing as an index onto the keyboard + uint8_t rand_apply_anim = 0; // Countdown to animate icons for when regenerate occurs + + uint8_t regenerate_phase = 0; // Split up random generation over multiple frames + + // Get the cv value to use for a given step including root + transpose values + int get_pitch_for_step(int step_num) { + int quant_note = 64 + int(notes[step_num]); + + // transpose in scale degrees, proportioned from semitones + quant_note += (MIDIQuantizer::NoteNumber(transpose_cv) - 60) * scale_size / 12; + + // Apply the manual octave offset + quant_note += (int(octave_offset) * int(scale_size)); + + // Transpose by one octave up or down if flagged to (note this is one full span of whatever scale is active to give doubling octave behavior) + if (step_is_oct_up(step_num)) { + quant_note += scale_size; + } else if (step_is_oct_down(step_num)) { + quant_note -= scale_size; } - - private: - int cursor = 0; - - braids::Quantizer quantizer; // Helper for note index --> pitch cv - braids::Quantizer display_semi_quantizer; // Quantizer to interpret the current note for display on a keyboard - - - // User settings - - // Bool - int manual_reset_flag = 0; // Manual trigger to reset/regen - - // bool - int lock_seed; // If 1, the seed won't randomize (and manual editing is enabled) - - uint16_t seed; // The random seed that deterministically builds the sequence - - int scale; // Active quantization & generation scale - uint8_t root; // Root note - int8_t octave_offset; // Manual octave offset (based on size of current scale, added to root note) - - uint8_t density; // The density parameter controls a couple of things at once. Its 0-14 value is mapped to -7..+7 range - // The larger the magnitude from zero in either direction, the more dense the note patterns are (fewer rests) - // For values mapped < 0 (e.g. left range,) the more negative the value is, the less chance consecutive pitches will - // change from the prior pitch, giving repeating lines (note: octave jumps still apply) - - uint8_t current_pattern_density; // Track what density value was used to generate the current pattern (to detect if regeneration is required) - - // Density controls (Encoder sets center point, CV can apply +-) - int density_encoder; // density value contributed by the encoder (center point) - int density_cv; // density value (+-) contributed by CV - int density_encoder_display; // Countdown of frames to show the encoder's density value (centerpoint) - uint8_t num_steps; // How many steps of the generated pattern to play before looping - - // Playback - uint8_t step = 0; // Current sequencer step - - //int transpose_note_in; // Current transposition from cv in (initially a cv value) TEMP: REMOVE - int32_t transpose_cv; // Quantized transpose in cv - - // Generated sequence data - uint32_t gates = 0; // Bitfield of gates; ((gates >> step) & 1) means gate - uint32_t slides = 0; // Bitfield of slide steps; ((slides >> step) & 1) means slide - uint32_t accents = 0; // Bitfield of accent steps; ((accents >> step) & 1) means accent - uint32_t oct_ups = 0; // Bitfield of octave ups - uint32_t oct_downs = 0; // Bitfield of octave downs - uint8_t notes[ACID_MAX_STEPS]; // Note values - - uint8_t scale_size; // The size of the currently set quantizer scale (for octave detection, etc) - uint8_t current_pattern_scale_size; // Track what size scale was used to render the current pattern (for change detection) - - // For gate timing as ~32nd notes at tempo, detect clock rate like a clock multiplier - //int timing_count; - int gate_off_clock; // Scheduled cycle at which the gate should be turned off (when applicable) - int cycle_time; // Cycle time between the last two clock inputs - - // CV output values - int32_t curr_gate_cv = 0; - int32_t curr_pitch_cv = 0; - - // Pitch slide cv tracking - int32_t slide_start_cv = 0; - int32_t slide_end_cv = 0; - - // Display - int curr_step_semitone = 0; // The pitch converted to nearest semitone, for showing as an index onto the keyboard - - uint8_t rand_apply_anim = 0; // Countdown to animate icons for when regenerate occurs - - uint8_t regenerate_phase = 0; // Split up random generation over multiple frames - - // Get the cv value to use for a given step including root + transpose values - int get_pitch_for_step(int step_num) - { - // Original: Transpose pre-quantize - //int quant_note = 64 + int(notes[step_num]) + int(root) + int(transpose_note_in); - int quant_note = 64 + int(notes[step_num]) + int(root); + quant_note = constrain(quant_note, 0, 127); - // Apply the manual octave offset - quant_note += (int(octave_offset) * int(scale_size)); + // root note is the semitone offset after quantization + return quantizer->Lookup(quant_note) + (root << 7); + //return quantizer->Lookup( 64 ); // Test: note 64 is definitely 0v=c4 if output directly, on ALL scales + } - // Transpose by one octave up or down if flagged to (note this is one full span of whatever scale is active to give doubling octave behavior) - if(step_is_oct_up(step_num)) - { - quant_note += scale_size; - } - else if(step_is_oct_down(step_num)) - { - quant_note -= scale_size; - } + int get_semitone_for_step(int step_num) { + // Don't add in octaves-- use the current quantizer limited to the base octave + int quant_note = 64 + notes[step_num]; // + transpose_note_in; + int32_t cv_note = quantizer->Lookup(constrain(quant_note, 0, 127)); + return (MIDIQuantizer::NoteNumber(cv_note) + root) % 12; + } - int out_note = constrain(quant_note, 0, 127); + void reseed() { + randomSeed(micros()); + seed = random(0, 65535); // 16 bits + } - // New: Transpose post-quantize - int pitch_cv = quantizer.Lookup(out_note) + transpose_cv; - return pitch_cv; + void regenerate_all() { + regenerate_phase = 1; // Set to regenerate on loop + rand_apply_anim = 40; // Show that regenerate started (anim for this many display updates) + } - // Original: Output quantized after transposition added - //return quantizer.Lookup( out_note ); - - //return quantizer.Lookup( 64 ); // Test: note 64 is definitely 0v=c4 if output directly, on ALL scales + void regenerate_if_density_or_scale_changed() { + // Skip if density has not changed, or if currently regenerating + if (regenerate_phase == 0) { + if (density != current_pattern_density || scale_size != current_pattern_scale_size) { + regenerate_phase = 1; // regenerate all since pitches take density into account + } } + } - int get_semitone_for_step(int step_num) - { - // Don't add in octaves-- use the current quantizer limited to the base octave - int quant_note = 64 + notes[step_num] + root;// + transpose_note_in; - int32_t cv_note = quantizer.Lookup( constrain(quant_note, 0, 127)); - display_semi_quantizer.Process(cv_note, 0, 0); // Use root == 0 to start at c - return display_semi_quantizer.GetLatestNoteNumber() % 12; - } - - void reseed() - { - randomSeed(micros()); - seed = random(0, 65535); // 16 bits + // Amortize random generation over multiple frames + void update_regeneration() { + if (regenerate_phase == 0) { + return; } - - // Trigger generating the sequence deterministically using the seed (over the next couple of Controller() calls) - void regenerate_all() - { - regenerate_phase = 1; // Set to regenerate on loop - rand_apply_anim = 40; // Show that regenerate started (anim for this many display updates) - } - - void regenerate_if_density_or_scale_changed() - { - // Skip if density has not changed, or if currently regenerating - if(regenerate_phase == 0) - { - if(density != current_pattern_density || scale_size != current_pattern_scale_size) + + randomSeed(seed + regenerate_phase); // Ensure random()'s seed at each phase for determinism (note: offset to decouple phase behavior correllations that would result) + + switch (regenerate_phase) { + // 1st set of 16 steps + case 1: + regenerate_pitches(); + ++regenerate_phase; + break; + case 2: + apply_density(); + ++regenerate_phase; + break; + // 2nd set of 16 steps + case 3: + regenerate_pitches(); + ++regenerate_phase; + break; + case 4: + apply_density(); + regenerate_phase = 0; + break; + default: + break; + } + } + + // Generate the notes sequence based on the seed and modified by density + void regenerate_pitches() { + bool bFirstHalf = regenerate_phase < 3; + + // How much pitch variety to use from the available pitches (one of the factors of the 'density' control when < centerpoint) + int pitch_change_dens = get_pitch_change_density(); + int available_pitches = 0; + if (scale_size > 0) { + if (pitch_change_dens > 7) { + available_pitches = scale_size - 1; + } else if (pitch_change_dens < 2) { + // Give the behavior of just the root note (0) at lowest density, and 0&1 at 2nd lowest (for 303 half-step style) + available_pitches = pitch_change_dens; + } else // Range 3-7 + { + int range_from_scale = scale_size - 3; + if (range_from_scale < 4) // Ok to saturate at full note count { - regenerate_phase = 1; // regenerate all since pitches take density into account + range_from_scale = 4; } + // Range from 2 pitches to just <= full scale available + available_pitches = 3 + Proportion(pitch_change_dens - 3, 4, range_from_scale); + available_pitches = constrain(available_pitches, 1, scale_size - 1); } } - // Amortize random generation over multiple frames - // Without having profiled this properly, I'm less concerned about overrunning isr times alloted to this app if it's amortized - void update_regeneration() - { - if(regenerate_phase == 0) - { - return; - } - - randomSeed(seed+regenerate_phase); // Ensure random()'s seed at each phase for determinism (note: offset to decouple phase behavior correllations that would result) - - switch(regenerate_phase) - { - // 1st set of 16 steps - case 1: regenerate_pitches(); ++regenerate_phase; break; - //case 2: apply_density(); regenerate_phase = 0; break; - case 2: apply_density(); ++regenerate_phase;break; - // 2nd set of 16 steps - case 3: regenerate_pitches(); ++regenerate_phase; break; - // After doing the 2nd set of bitvectors, swap the low and high 16 bits to align the first 16 steps to the steps they would have had - // when this app only rendered 16 steps - //case 4: apply_density(); restore_legacy_byte_orders(); regenerate_phase = 0; break; - case 4: apply_density(); regenerate_phase = 0; break; - default: break; - } + if (bFirstHalf) { + oct_ups = 0; + oct_downs = 0; } - - // Generate the notes sequence based on the seed and modified by density - void regenerate_pitches() - { - - // 32 steps are computed across two passes of this function - // Determine if the first 16 or second 16 steps are being handled here - bool bFirstHalf = regenerate_phase < 3; - - // How much pitch variety to use from the available pitches (one of the factors of the 'density' control when < centerpoint) - int pitch_change_dens = get_pitch_change_density(); - int available_pitches = 0; - if(scale_size > 0) - { - if(pitch_change_dens > 7) - { - available_pitches = scale_size-1; - } - else if(pitch_change_dens < 2) - { - // Give the behavior of just the root note (0) at lowest density, and 0&1 at 2nd lowest (for 303 half-step style) - available_pitches = pitch_change_dens; - } - else // Range 3-7 - { - int range_from_scale = scale_size - 3; - if(range_from_scale < 4) // Ok to saturate at full note count - { - range_from_scale = 4; + int max_step = (bFirstHalf ? ACID_HALF_STEPS : ACID_MAX_STEPS); + for (int s = (bFirstHalf ? 0 : ACID_HALF_STEPS); s < max_step; s++) { + int force_repeat_note_prob = 50 - (pitch_change_dens * 6); + if (s > 0 && rand_bit(force_repeat_note_prob)) { + notes[s] = notes[s - 1]; + } else { + notes[s] = random(0, available_pitches + 1); // Looking at the source, random(min,max) appears to return the range: min to max-1 + + oct_ups <<= 1; + oct_downs <<= 1; + + if (rand_bit(40)) { + if (rand_bit(50)) { + oct_ups |= 0x1; + } else { + oct_downs |= 0x1; } - // Range from 2 pitches to just <= full scale available - available_pitches = 3 + Proportion(pitch_change_dens-3, 4, range_from_scale); - available_pitches = constrain(available_pitches, 1, scale_size -1); } } + } - // Set notes and octave up / octave down bitvectors - if(bFirstHalf) - { - // Clear only on the first pass (otherwise, resume with current value) - oct_ups = 0; - oct_downs = 0; - } + if (scale_size == 0) { + scale_size = 12; + } - // Do either the first or second set of steps on this pass - int max_step = (bFirstHalf ? ACID_HALF_STEPS : ACID_MAX_STEPS); - for (int s = (bFirstHalf ? 0 : ACID_HALF_STEPS); s < max_step; s++) - { - // Increased chance to repeat the prior note, the smaller the pitch change aspect of 'density' is - // 0-8, least to most likely to change pitch - int force_repeat_note_prob = 50 - (pitch_change_dens * 6); - if(s > 0 && rand_bit(force_repeat_note_prob)) - { - notes[s] = notes[s-1]; - } - else - { - // Grab a random note index from the scale's available pitches - // Since this starts at 0, the root note will always be included, and adjacent scale notes are included as the range grows - notes[s] = random(0,available_pitches+1); // Looking at the source, random(min,max) appears to return the range: min to max-1 - - // Random oct up or down (Treating octave based on the scale's number of notes) - oct_ups <<= 1; - oct_downs <<= 1; - - if(rand_bit(40)) - { - if(rand_bit(50)) - { - oct_ups |= 0x1; - } - else - { - oct_downs |= 0x1; - } - } - } - } + current_pattern_scale_size = scale_size; + } - // Handle size as semitone scale for display if 'off' - if(scale_size == 0) - { - scale_size = 12; - } + // Change pattern density without affecting pitches + void apply_density() { + int latest_slide = 0; // Track previous bit for some algos + int latest_accent = 0; // Track previous bit for some algos - current_pattern_scale_size = scale_size; - } - - // Change pattern density without affecting pitches - void apply_density() - { - int latest_slide = 0; // Track previous bit for some algos - int latest_accent = 0; // Track previous bit for some algos - - // Get gate probability from the 'density' value - int on_off_dens = get_on_off_density(); - int densProb = 10 + on_off_dens * 14; // Should start >0 and reach 100+ - - // Clear if this is the first 16 steps to generate (otherwise append to these bit vectors for the 2nd set of 16) - bool bFirstHalf = regenerate_phase < 3; - if(bFirstHalf) - { - gates = 0; - slides = 0; - accents = 0; - } + // Get gate probability from the 'density' value + int on_off_dens = get_on_off_density(); + int densProb = 10 + on_off_dens * 14; // Should start >0 and reach 100+ - // Apply to each step - // Do half of the steps on each pass of this func - for(int i=0; i< ACID_HALF_STEPS; ++i) - { - gates <<= 1; - gates |= rand_bit(densProb); - - // Less probability of consecutive slides - slides <<= 1; - latest_slide = rand_bit((latest_slide ? 10 : 18)); - slides |= latest_slide; - - // Less probability of consecutive accents - accents <<= 1; - latest_accent = rand_bit((latest_accent ? 7 : 16)); - accents |= latest_accent; - } - - // Track the value of density used to render the pattern (to detect changes) - current_pattern_density = density; - - } - - // Get on/off likelihood from the current value of 'density' - int get_on_off_density() - { - // density has a range 0-14 - // Convert density to a bipolar value from -7..+7, with the +-7 extremes in either direction - // as high note density, and the 0 point as lowest possible note density - int note_dens = int(density) - 7; - return abs(note_dens); + bool bFirstHalf = regenerate_phase < 3; + if (bFirstHalf) { + gates = 0; + slides = 0; + accents = 0; } - // Get the degree to which pitches should change based on the value of 'density' - // The density slider's center and right half indicate full pitch change range - // The further the slider is to the left of the centerpoint, the less pitches should change - int get_pitch_change_density() - { - // Smaller values indicate fewer pitches should be drawn from - return constrain(density, 0,8); // Note that the right half of the slider is clamped to full range - } - - bool step_is_gated(int step_num) { - return (gates & (0x01 << step_num)); - } - - bool step_is_slid(int step_num) { - return (slides & (0x01 << step_num)); - } - - bool step_is_accent(int step_num) { - return (accents & (0x01 << step_num)); - } + for (int i = 0; i < ACID_HALF_STEPS; ++i) { + gates <<= 1; + gates |= rand_bit(densProb); - bool step_is_oct_up(int step_num){ - return (oct_ups & (0x01 << step_num)); - } - - bool step_is_oct_down(int step_num){ - return (oct_downs & (0x01 << step_num)); + // Less probability of consecutive slides + slides <<= 1; + latest_slide = rand_bit((latest_slide ? 10 : 18)); + slides |= latest_slide; + + // Less probability of consecutive accents + accents <<= 1; + latest_accent = rand_bit((latest_accent ? 7 : 16)); + accents |= latest_accent; } - int get_next_step(int step_num) - { - // loop at the current loop point - if(++step_num >= num_steps) - { - return 0; - } - return step_num; // Advanced by one - } + current_pattern_density = density; + } - // Pass in a probability 0-100 to get that % chance to return 1 - int rand_bit(int prob) - { - return (random(1, 100) <= prob) ? 1 : 0; - } + int get_on_off_density() { + int note_dens = int(density) - 7; + return abs(note_dens); + } + int get_pitch_change_density() { + return constrain(density, 0, 8); // Note that the right half of the slider is clamped to full range + } - void set_quantizer_scale(int new_scale) - { - const braids::Scale & quant_scale = OC::Scales::GetScale(new_scale); - quantizer.Configure(quant_scale, 0xffff); - scale_size = quant_scale.num_notes; // Track this scale size for octaves and display + bool step_is_gated(int step_num) { + return (gates & (0x01 << step_num)); + } + + bool step_is_slid(int step_num) { + return (slides & (0x01 << step_num)); + } + + bool step_is_accent(int step_num) { + return (accents & (0x01 << step_num)); + } + + bool step_is_oct_up(int step_num) { + return (oct_ups & (0x01 << step_num)); + } + + bool step_is_oct_down(int step_num) { + return (oct_downs & (0x01 << step_num)); + } + + int get_next_step(int step_num) { + if (++step_num >= num_steps) { + return 0; } - - void DrawGraphics() - { - // Wiggle the icon when the sequence regenerates - int heart_y = 15; - int die_y = 15; - if(rand_apply_anim > 0) - { - --rand_apply_anim; - // First the heart jumps, then the die if not locked - if(rand_apply_anim > 20) - { - heart_y = 13; - } - else - { - die_y = 13; - } + return step_num; // Advanced by one + } + + int rand_bit(int prob) { + return (random(1, 100) <= prob) ? 1 : 0; + } + + void set_quantizer_scale(int new_scale) { + const braids::Scale & quant_scale = OC::Scales::GetScale(new_scale); + quantizer->Configure(quant_scale, 0xffff); + scale_size = quant_scale.num_notes; // Track this scale size for octaves and display + } + + void DrawGraphics() { + int heart_y = 15; + int die_y = 15; + if (rand_apply_anim > 0) { + --rand_apply_anim; + + if (rand_apply_anim > 20) { + heart_y = 13; + } else { + die_y = 13; } + } - // Heart represents the seed/favorite - gfxBitmap(4, heart_y, 8, FAVORITE_ICON); - - // Indicate if seed is randomized on reset pulse, or if it's locked for user editing - // (If unlocked, this also wiggles on regenerate because the seed has been randomized) - gfxBitmap(15, (lock_seed ? 15 : die_y), 8, (lock_seed ? LOCK_ICON : RANDOM_ICON)); - - // Show the 16-bit seed as 4 hex digits - int disp_seed = seed; //0xABCD // test display accuracy - char sz[2]; sz[1] = 0; // Null terminated string for easy print - gfxPos(25, 15); - for(int i=3; i>=0; --i) - { - // Grab each nibble in turn, starting with most significant - int nib = (disp_seed >> (i*4))& 0xF; - if(nib<=9) - { - gfxPrint(nib); - } - else - { - sz[0] = 'a' + nib - 10; - gfxPrint(static_cast(sz)); - } + // Heart represents the seed/favorite + gfxIcon(4, heart_y, FAVORITE_ICON); + gfxIcon(15, (lock_seed ? 15 : die_y), (lock_seed ? LOCK_ICON : RANDOM_ICON)); + + // Show the 16-bit seed as 4 hex digits + int disp_seed = seed; //0xABCD // test display accuracy + char sz[2]; + sz[1] = 0; // Null terminated string for easy print + gfxPos(25, 15); + for (int i = 3; i >= 0; --i) { + // Grab each nibble in turn, starting with most significant + int nib = (disp_seed >> (i * 4)) & 0xF; + if (nib <= 9) { + gfxPrint(nib); + } else { + sz[0] = 'a' + nib - 10; + gfxPrint(static_cast(sz)); } - - // Display density - - int gate_dens = get_on_off_density(); - int pitch_dens = get_pitch_change_density(); - - //gfxLine(9,36, 29, 36, true); // dotted line - int xd = 5 + 7-gate_dens; - int yd = (64*pitch_dens)/256; // Multiply for better fidelity - gfxBitmap(12-xd, 27+yd, 8, NOTE4_ICON); - gfxBitmap(12, 27-yd, 8, NOTE4_ICON); - gfxBitmap(12+xd, 27, 8, NOTE4_ICON); - - // Display a number value for density - int dens_display = gate_dens; - bool dens_neg = false; - if(density_encoder_display > 0) - { - // The density encoder value was recently changed, so show it momentarily instead of the cv+encoder value normally shown - --density_encoder_display; - dens_display = abs(density_encoder-7); //Map from 0 to 14 --> -7 to 7 - dens_neg = density_encoder < 7; + } - if(density_cv != 0) // When cv is applied, show that this is the centered value being displayed - { - // Draw a knob to the left to represent the centerpoint being set - gfxCircle(3, 40, 3); - gfxLine(3, 38, 3, 40); - } - - } - else - { - dens_display = gate_dens; - dens_neg = density < 7; - // Indicate if cv is affecting the density - if(density_cv != 0) // Density integer contribution from CV (not raw cv) - { - gfxBitmap(22, 37, 8, CV_ICON); - } - } + // Display density - if(dens_neg) - { - gfxPrint(8, 37, "-"); // Print minus sign this way to right-align the number - } - gfxPrint(14, 37, dens_display); - - /* CV offset test - int test = Proportion(abs(density_cv), HEMISPHERE_3V_CV, 7); - if(density_cv < 0) test *= -1; - gfxPos(0, 27);gfxPrint(test); - gfxPos(0, 37); gfxPrintVoltage(density_cv); - */ - - // Scale and root note select - xd = (scale < 4) ? 32 : 39; // Slide/crowd to the left a bit if showing the "USER1"-"USER4" scales, which are uniquely five instead of four characters - gfxPrint(xd, 27, OC::scale_names_short[scale]); - - gfxPrint((octave_offset == 0 ? 45 : 39), 36, OC::Strings::note_names_unpadded[root]); - if(octave_offset != 0) - { - gfxPrint(51, 36, octave_offset); - } - - //gfxPrint(" (");gfxPrint(density);gfxPrint(")"); // Debug print of actual density value - - // Current / total steps - int display_step = step+1; // Protocol droids know that humans count from 1 - //gfxPrint(1 + pad(100,display_step), 45, display_step); gfxPrint("/");gfxPrint(num_steps); // Pad x enough to hold width steady - gfxPrint(1+pad(10,display_step), 47, display_step); gfxPrint("/");gfxPrint(num_steps); // Pad x enough to hold width steady - - // Show octave icons - if(step_is_oct_down(step)) + int gate_dens = get_on_off_density(); + int pitch_dens = get_pitch_change_density(); + + int xd = 5 + 7 - gate_dens; + int yd = (64 * pitch_dens) / 256; // Multiply for better fidelity + gfxBitmap(12 - xd, 27 + yd, 8, NOTE4_ICON); + gfxBitmap(12, 27 - yd, 8, NOTE4_ICON); + gfxBitmap(12 + xd, 27, 8, NOTE4_ICON); + + // Display a number value for density + int dens_display = gate_dens; + bool dens_neg = false; + if (density_encoder_display > 0) { + // The density encoder value was recently changed, so show it momentarily instead of the cv+encoder value normally shown + --density_encoder_display; + dens_display = abs(density_encoder - 7); //Map from 0 to 14 --> -7 to 7 + dens_neg = density_encoder < 7; + + if (density_cv != 0) // When cv is applied, show that this is the centered value being displayed { - gfxBitmap(41, 54, 8, DOWN_BTN_ICON); + // Draw a knob to the left to represent the centerpoint being set + gfxCircle(3, 40, 3); + gfxLine(3, 38, 3, 40); } - else if(step_is_oct_up(step)) + + } else { + dens_display = gate_dens; + dens_neg = density < 7; + // Indicate if cv is affecting the density + if (density_cv != 0) // Density integer contribution from CV (not raw cv) { - gfxBitmap(41, 54, 8, UP_BTN_ICON); + gfxBitmap(22, 37, 8, CV_ICON); } + } - int keyboard_pitch = curr_step_semitone -4; // Translate from 0v - if(keyboard_pitch < 0) keyboard_pitch+=12; // Deal with c being at the start, not middle of keyboard + if (dens_neg) { + gfxPrint(8, 37, "-"); // Print minus sign this way to right-align the number + } + gfxPrint(14, 37, dens_display); - gfxPrint(49, 55, keyboard_pitch); + // Scale and root note select + gfxPrint(39, 26, OC::scale_names_short[scale]); - // Debug - //gfxPrint(40, 55, scale_size); - //gfxPrint(40, 55, transpose_note_in); // N.B. if pushed further right, this can crash on hemi2 when it'd print offscreen + gfxPrint((octave_offset == 0 ? 45 : 39), 36, OC::Strings::note_names_unpadded[root]); + if (octave_offset != 0) { + gfxPrint(51, 36, octave_offset); + } - // gfxBitmap(1, 55, 8, CV_ICON); gfxPos(12, 55); gfxPrintVoltage(pitches[step]); + // Current / total steps + int display_step = step + 1; // Protocol droids know that humans count from 1 + gfxPrint(1 + pad(10, display_step), 47, display_step); // Pad x enough to hold width steady + gfxPrint("/"); + gfxPrint(num_steps); + + // Show octave icons + if (step_is_oct_down(step)) { + gfxBitmap(41, 54, 8, DOWN_BTN_ICON); + } else if (step_is_oct_up(step)) { + gfxBitmap(41, 54, 8, UP_BTN_ICON); + } - // Draw a TB-303 style octave of a piano keyboard, indicating the playing pitch - int x = 1; - int y = 61; - int keyPatt = 0x054A; // keys encoded as 0=white 1=black, starting at c, backwards: b 0 0101 0100 1010 - for(int i=0; i<12; ++i) - { - // Black key? - y = ( keyPatt & 0x1 ) ? 56 : 61; - keyPatt >>= 1; - - // Two white keys in a row E and F - if( i == 5 ) x+=3; - - if(keyboard_pitch == i && step_is_gated(step)) // Only render a pitch if gated - { - gfxRect(x-1, y-1, 5, 4); // Larger box - - } - else - { - gfxRect(x, y, 3, 2); // Small filled box - } - x += 3; - } + gfxPrint(49, 55, curr_step_semitone); - // Indicate if the current step has an accent - if(step_is_accent(step)) - { - gfxPrint(37, 46, "!"); - } + // Draw a TB-303 style octave of a piano keyboard, indicating the playing pitch + int x = 1; + int y; + const int keyPatt = 0x054A; // keys encoded as 0=white 1=black, starting at c, backwards: b 0 0101 0100 1010 + for (int i = 0; i < 12; ++i) { + // Black key? + y = ((keyPatt >> i) & 0x1) ? 56 : 61; - // Indicate if the current step has a slide - if(step_is_slid(step)) - { - gfxBitmap(42, 46, 8, BEND_ICON); - } - - // Show that the "slide circuit" is actively - // sliding the pitch (one step after the slid step) - if(curr_pitch_cv != slide_end_cv) - { - gfxBitmap(52, 46, 8, WAVEFORM_ICON); - } - - // Draw edit cursor - if (cursor == 0) - { - // Set length to indicate length - gfxCursor(14, 23, lock_seed ? 12 : 35); // Seed = auto-randomize / locked-manual - } - else if (cursor <= 4) // seed, 4 positions (1-4) - { - gfxCursor(24 + 6*(cursor-1), 23, 8); - } - else if(cursor == 5) - { - gfxCursor(9, 34, 14); // density - } - else if(cursor == 6) - { - gfxCursor(38, 34, 26); // scale - } - else if(cursor == 7) - { - gfxCursor(42, 43, 16); // root note - } - else if(cursor == 8) + // Two white keys in a row E and F + if (i == 5) x += 3; + + if (curr_step_semitone == i && step_is_gated(step)) // Only render a pitch if gated { - gfxCursor(20, 54, 12); // step + gfxRect(x - 1, y - 1, 5, 4); // Larger box + + } else { + gfxRect(x, y, 3, 2); // Small filled box } + x += 3; } -}; + if (step_is_accent(step)) { + gfxPrint(37, 46, "!"); + } + if (step_is_slid(step)) { + gfxBitmap(42, 46, 8, BEND_ICON); + } + + // Show that the "slide circuit" is actively + // sliding the pitch (one step after the slid step) + if (curr_pitch_cv != slide_end_cv) { + gfxBitmap(52, 46, 8, WAVEFORM_ICON); + } + + // Draw edit cursor + switch (cursor) { + case 0: + // Set length to indicate length + gfxCursor(14, 23, lock_seed ? 11 : 36); // Seed = auto-randomize / locked-manual + break; + case 1: + case 2: + case 3: + case 4: // seed, 4 positions (1-4) + gfxCursor(25 + 6 * (cursor - 1), 23, 7); + break; + case 5: + gfxCursor(9, 45, 14); // density + break; + case 6: + gfxCursor(39, 34, 25); // scale + break; + case 7: + gfxCursor(39, 44, 24); // root note + break; + case 8: + gfxCursor(20, 54, 12, 8); // step + break; + } + } + +}; //////////////////////////////////////////////////////////////////////////////// //// Hemisphere Applet Functions @@ -998,33 +753,33 @@ class TB_3PO : public HemisphereApplet TB_3PO TB_3PO_instance[2]; void TB_3PO_Start(bool hemisphere) { - TB_3PO_instance[hemisphere].BaseStart(hemisphere); + TB_3PO_instance[hemisphere].BaseStart(hemisphere); } void TB_3PO_Controller(bool hemisphere, bool forwarding) { - TB_3PO_instance[hemisphere].BaseController(forwarding); + TB_3PO_instance[hemisphere].BaseController(forwarding); } void TB_3PO_View(bool hemisphere) { - TB_3PO_instance[hemisphere].BaseView(); + TB_3PO_instance[hemisphere].BaseView(); } void TB_3PO_OnButtonPress(bool hemisphere) { - TB_3PO_instance[hemisphere].OnButtonPress(); + TB_3PO_instance[hemisphere].OnButtonPress(); } void TB_3PO_OnEncoderMove(bool hemisphere, int direction) { - TB_3PO_instance[hemisphere].OnEncoderMove(direction); + TB_3PO_instance[hemisphere].OnEncoderMove(direction); } void TB_3PO_ToggleHelpScreen(bool hemisphere) { - TB_3PO_instance[hemisphere].HelpScreen(); + TB_3PO_instance[hemisphere].HelpScreen(); } uint64_t TB_3PO_OnDataRequest(bool hemisphere) { - return TB_3PO_instance[hemisphere].OnDataRequest(); + return TB_3PO_instance[hemisphere].OnDataRequest(); } void TB_3PO_OnDataReceive(bool hemisphere, uint64_t data) { - TB_3PO_instance[hemisphere].OnDataReceive(data); + TB_3PO_instance[hemisphere].OnDataReceive(data); } diff --git a/software/o_c_REV/HEM_TLNeuron.ino b/software/o_c_REV/HEM_TLNeuron.ino index fe353ef03..826165da8 100644 --- a/software/o_c_REV/HEM_TLNeuron.ino +++ b/software/o_c_REV/HEM_TLNeuron.ino @@ -44,7 +44,7 @@ public: dendrite_activated[ch] = 0; } } - if (In(0) > (HEMISPHERE_MAX_CV / 2)) { + if (In(0) > (HEMISPHERE_MAX_INPUT_CV / 2)) { sum += dendrite_weight[2]; dendrite_activated[2] = 1; } else { @@ -67,7 +67,6 @@ public: } void View() { - gfxHeader(applet_name()); DrawDendrites(); DrawAxon(); DrawStates(); diff --git a/software/o_c_REV/HEM_TM.ino b/software/o_c_REV/HEM_TM.ino.disabled similarity index 65% rename from software/o_c_REV/HEM_TM.ino rename to software/o_c_REV/HEM_TM.ino.disabled index 774c21fdf..175c14c3c 100644 --- a/software/o_c_REV/HEM_TM.ino +++ b/software/o_c_REV/HEM_TM.ino.disabled @@ -31,8 +31,6 @@ #include "braids_quantizer_scales.h" #include "OC_scales.h" -// Logarhythm mod: Allow all scales, even though only the first 64 serialize correctly -//#define TM_MAX_SCALE 63 #define TM_MAX_SCALE OC::Scales::NUM_SCALES #define TM_MIN_LENGTH 2 @@ -67,81 +65,82 @@ public: // CV 2 bi-polar modulation of probability int pCv = Proportion(DetentedIn(1), HEMISPHERE_MAX_CV, 100); + bool clk = Clock(0); - if (Clock(0)) { + if (clk) { // If the cursor is not on the p value, and Digital 2 is not gated, the sequence remains the same int prob = (cursor == 1 || Gate(1)) ? p + pCv : 0; - prob = constrain(prob, 0, 100); - // Grab the bit that's about to be shifted away - int last = (reg >> (length - 1)) & 0x01; - - // Does it change? - if (random(0, 99) < prob) last = 1 - last; - - // Shift left, then potentially add the bit from the other side - reg = (reg << 1) + last; + AdvanceRegister( constrain(prob, 0, 100) ); } // Send 5-bit quantized CV - int32_t note = reg & 0x1f; - // APD: Scale this to the range of notes allowed by quant_range: 32 should be all // This defies the faithful Turing Machine sim aspect of this code but gives a useful addition that the Disting adds to the concept - // scaled = note * quant_range / 0x1f - note *= quant_range; - simfloat x = int2simfloat(note) / (int32_t)0x1f; - note = simfloat2int(x); - //tmp = note; - + int32_t note = Proportion(reg & 0x1f, 0x1f, quant_range); Out(0, quantizer.Lookup(note + 64)); - if (cv2 == 0) { + switch (cv2) { + case 0: // Send 8-bit proportioned CV - int cv = Proportion(reg & 0x00ff, 255, HEMISPHERE_MAX_CV); - Out(1, cv); - } else if (cv2 == 1) { - if (Clock(0)) { - ClockOut(1); - } - } else if (cv2 == 2) { + Out(1, Proportion(reg & 0x00ff, 255, HEMISPHERE_MAX_CV) ); + break; + case 1: + if (clk) + ClockOut(1); + break; + case 2: // only trigger if 1st bit is high - if (Clock(0) && (reg & 0x01) == 1) { + if (clk && (reg & 0x01) == 1) ClockOut(1); - } + break; + case 3: // duplicate of Out A + Out(1, quantizer.Lookup(note + 64)); + break; + case 4: // alternative 6-bit pitch + note = Proportion( (reg >> 8 & 0x3f), 0x3f, quant_range); + Out(1, quantizer.Lookup(note + 64)); + break; } } void View() { - gfxHeader(applet_name()); DrawSelector(); DrawIndicator(); } void OnButtonPress() { - if (++cursor > 4) cursor = 0; + isEditing = !isEditing; } void OnEncoderMove(int direction) { - if (cursor == 0) length = constrain(length += direction, TM_MIN_LENGTH, TM_MAX_LENGTH); - if (cursor == 1) p = constrain(p += direction, 0, 100); - if (cursor == 2) { - scale += direction; - if (scale >= TM_MAX_SCALE) scale = 0; - if (scale < 0) scale = TM_MAX_SCALE - 1; - quantizer.Configure(OC::Scales::GetScale(scale), 0xffff); - } - if(cursor == 3){ - quant_range = constrain(quant_range += direction, 1, 32); - } - if(cursor == 4){ - cv2 += direction; - if (cv2 > 2) { - cv2 = 0; - } - if (cv2 < 0) { - cv2 = 2; - } + if (!isEditing) { + cursor += direction; + if (cursor < 0) cursor = 4; + if (cursor > 4) cursor = 0; + + ResetCursor(); // Reset blink so it's immediately visible when moved + } else { + switch (cursor) { + case 0: + length = constrain(length + direction, TM_MIN_LENGTH, TM_MAX_LENGTH); + break; + case 1: + p = constrain(p + direction, 0, 100); + break; + case 2: + scale += direction; + if (scale >= TM_MAX_SCALE) scale = 0; + if (scale < 0) scale = TM_MAX_SCALE - 1; + quantizer.Configure(OC::Scales::GetScale(scale), 0xffff); + break; + case 3: + quant_range = constrain(quant_range + direction, 1, 32); + break; + case 4: + cv2 = constrain(cv2 + direction, 0, 4); + break; + } } } @@ -150,13 +149,10 @@ public: Pack(data, PackLocation {0,16}, reg); Pack(data, PackLocation {16,7}, p); Pack(data, PackLocation {23,4}, length - 1); - Pack(data, PackLocation {28,6}, quant_range); - Pack(data, PackLocation {34,4}, cv2); + Pack(data, PackLocation {27,5}, quant_range - 1); + Pack(data, PackLocation {32,4}, cv2); + Pack(data, PackLocation {36,8}, constrain(scale, 0, 255)); - // Logarhythm mod: Since scale can exceed 6 bits now, clamp mathematically rather than surprising the user with a roll over of larger numbers - //Pack(data, PackLocation {27,6}, scale); - Pack(data, PackLocation {38,6}, constrain(scale, 0, 63)); - return data; } @@ -164,11 +160,10 @@ public: reg = Unpack(data, PackLocation {0,16}); p = Unpack(data, PackLocation {16,7}); length = Unpack(data, PackLocation {23,4}) + 1; - quant_range = Unpack(data, PackLocation{28,6}); - cv2 = Unpack(data, PackLocation {34,4}); - scale = Unpack(data, PackLocation {38,6}); + quant_range = Unpack(data, PackLocation{27,5}) + 1; + cv2 = Unpack(data, PackLocation {32,4}); + scale = Unpack(data, PackLocation {36,8}); quantizer.Configure(OC::Scales::GetScale(scale), 0xffff); - } protected: @@ -176,7 +171,7 @@ protected: // "------------------" <-- Size Guide help[HEMISPHERE_HELP_DIGITALS] = "1=Clock 2=p Gate"; help[HEMISPHERE_HELP_CVS] = "1=Length 2=p Mod"; - help[HEMISPHERE_HELP_OUTS] = "A=Quant5-bit B=CV8"; + help[HEMISPHERE_HELP_OUTS] = "A=Quant5-bit B=CV2"; help[HEMISPHERE_HELP_ENCODER] = "Len/Prob/Scl/Range"; // "------------------" <-- Size Guide } @@ -184,6 +179,7 @@ protected: private: int length; // Sequence length int cursor; // 0 = length, 1 = p, 2 = scale + bool isEditing = false; braids::Quantizer quantizer; // Settings @@ -192,19 +188,18 @@ private: //int8_t scale; // Scale used for quantized output int scale; // Logarhythm: hold larger values //int tmp = 0; - int quant_range; // APD - int cv2 = 0; + uint8_t quant_range; // APD + uint8_t cv2 = 0; // 2nd output mode: 0=mod; 1=trig; 2=trig-on-msb; 3=duplicate of A; 4=alternate pitch void DrawSelector() { gfxBitmap(1, 14, 8, LOOP_ICON); gfxPrint(12 + pad(10, length), 15, length); gfxPrint(32, 15, "p="); - if (cursor == 1 || Gate(1)) { + if (cursor == 1 || Gate(1)) { // p unlocked int pCv = Proportion(DetentedIn(1), HEMISPHERE_MAX_CV, 100); int prob = constrain(p + pCv, 0, 100); - if (cursor == 1) gfxCursor(45, 23, 18); // Probability Cursor gfxPrint(pad(100, prob), prob); - } else { + } else { // p is disabled gfxBitmap(49, 14, 8, LOCK_ICON); } gfxBitmap(1, 24, 8, SCALE_ICON); @@ -212,20 +207,40 @@ private: gfxBitmap(41, 24, 8, NOTE4_ICON); gfxPrint(49, 25, quant_range); // APD gfxPrint(1, 35, "CV2:"); - if (cv2 == 0) { - gfxBitmap(28, 35, 8, WAVEFORM_ICON); - } else { - gfxBitmap(28, 35, 8, CLOCK_ICON); - if (cv2 == 2) { + switch (cv2) { + case 0: // modulation output + gfxBitmap(28, 35, 8, WAVEFORM_ICON); + break; + case 2: // clock out only on msb gfxPrint(36, 35, "1"); - } + case 1: // clock out icon + gfxBitmap(28, 35, 8, CLOCK_ICON); + break; + case 3: // double output A + gfxBitmap(28, 35, 8, LINK_ICON); + break; + case 4: // alternate 6-bit pitch + gfxBitmap(28, 35, 8, CV_ICON); + break; } //gfxPrint(1, 35, tmp); - if (cursor == 0) gfxCursor(13, 23, 12); // Length Cursor - if (cursor == 2) gfxCursor(13, 33, 30); // Scale Cursor - if (cursor == 3) gfxCursor(49, 33, 14); // Quant Range Cursor // APD - if (cursor == 4) gfxCursor(27, 43, (cv2 == 2) ? 18 : 10); // cv2 mode + switch (cursor) { + case 0: gfxCursor(13, 23, 12); break; // Length Cursor + case 1: gfxCursor(45, 23, 18); break; // Probability Cursor + case 2: gfxCursor(12, 33, 25); break; // Scale Cursor + case 3: gfxCursor(49, 33, 14); break; // Quant Range Cursor // APD + case 4: gfxCursor(27, 43, (cv2 == 2) ? 18 : 10); // cv2 mode + } + if (isEditing) { + switch (cursor) { + case 0: gfxInvert(13, 14, 12, 9); break; + case 1: gfxInvert(45, 14, 18, 9); break; + case 2: gfxInvert(12, 24, 25, 9); break; + case 3: gfxInvert(49, 24, 14, 9); break; + case 4: gfxInvert(27, 34, (cv2 == 2) ? 18 : 10, 9); // cv2 mode + } + } } void DrawIndicator() { @@ -240,7 +255,7 @@ private: void AdvanceRegister(int prob) { // Before shifting, determine the fate of the last bit - int last = (reg >> 15) & 0x01; + int last = (reg >> (length - 1)) & 0x01; if (random(0, 99) < prob) last = 1 - last; // Shift left, then potentially add the bit from the other side @@ -290,4 +305,4 @@ uint64_t TM_OnDataRequest(bool hemisphere) { void TM_OnDataReceive(bool hemisphere, uint64_t data) { TM_instance[hemisphere].OnDataReceive(data); -} +} diff --git a/software/o_c_REV/HEM_TM2.ino b/software/o_c_REV/HEM_TM2.ino new file mode 100644 index 000000000..d6834dc31 --- /dev/null +++ b/software/o_c_REV/HEM_TM2.ino @@ -0,0 +1,532 @@ +// Copyright (c) 2018, Jason Justian +// Copyright (c) 2022, Nicholas J. Michalek +// +// Based on Braids Quantizer, Copyright 2015 Émilie Gillet. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +/* + * Turing Machine based on https://thonk.co.uk/documents/random%20sequencer%20documentation%20v2-1%20THONK%20KIT_LargeImages.pdf + * + * Thanks to Tom Whitwell for creating the concept, and for clarifying some things + * Thanks to Jon Wheeler for the CV length and probability updates + * + * Heavily adapted as DualTM from ShiftReg/TM by djphazer (Nicholas J. Michalek) + */ + +#include "braids_quantizer.h" +#include "braids_quantizer_scales.h" +#include "OC_scales.h" + +#define TM2_MAX_SCALE OC::Scales::NUM_SCALES + +#define TM2_MIN_LENGTH 2 +#define TM2_MAX_LENGTH 32 + +class DualTM : public HemisphereApplet { +public: + + enum TM2Cursor { + LENGTH, + PROB, + SCALE, + ROOT_NOTE, + RANGE, + SLEW, + CVMODE1, + CVMODE2, + OUT_A, + OUT_B, + LAST_SETTING = OUT_B + }; + + enum OutputMode { + PITCH_BLEND, + PITCH1, + PITCH2, + MOD1, + MOD2, + TRIG1, + TRIG2, + GATE1, + GATE2, + GATE_SUM, + OUTMODE_LAST + }; + + enum InputMode { + SLEW_MOD, + LENGTH_MOD, + P_MOD, + RANGE_MOD, + TRANSPOSE1, + TRANSPOSE2, + BLEND_XFADE, // actually crossfade blend of both pitches + INMODE_LAST + }; + + const char* applet_name() { + return "DualTM"; + } + + void Start() { + reg[0] = random(0, 65535); + reg[1] = ~reg[0]; + } + + void Controller() { + bool clk = Clock(0); + if (clk) StartADCLag(0); + bool update_cv = EndOfADCLag(0); + + int cv_data[2]; + cv_data[0] = DetentedIn(0); + cv_data[1] = DetentedIn(1); + + // default to no mod + p_mod = p; + len_mod = length; + range_mod = range; + smooth_mod = smoothing; + int trans_mod[3] = {0, 0, 0}; // default transpose + + // process CV inputs + ForEachChannel(ch) { + switch (cvmode[ch]) { + case SLEW_MOD: + Modulate(smooth_mod, ch, 0, 127); + break; + case LENGTH_MOD: + Modulate(len_mod, ch, TM2_MIN_LENGTH, TM2_MAX_LENGTH); + break; + + case P_MOD: + Modulate(p_mod, ch, 0, 100); + break; + + case RANGE_MOD: + Modulate(range_mod, ch, 1, 32); + break; + + // bi-polar transpose before quantize + case TRANSPOSE1: + case TRANSPOSE2: + case BLEND_XFADE: + if (update_cv) // S&H style transpose + trans_mod[cvmode[ch] - TRANSPOSE1] = MIDIQuantizer::NoteNumber(cv_data[ch], 0) - 60; // constrain to range_mod? + break; + + default: break; + } + } + + if (update_cv) { + // Update transpose values + for (int i = 0; i < 3; ++i) { note_trans[i] = trans_mod[i]; } + } + + // Advance the register on clock, flipping bits as necessary + if (clk) { + // If the cursor is not on the p value, and Digital 2 is not gated, the sequence remains the same + int prob = (cursor == PROB || Gate(1)) ? p_mod : 0; + + for (int i = 0; i < 2; ++i) { + // Grab the bit that's about to be shifted away + int last = (reg[i] >> (len_mod - 1)) & 0x01; + + // Does it change? + if (random(0, 99) < prob) last = 1 - last; + + // Shift left, then potentially add the bit from the other side + reg[i] = (reg[i] << 1) + last; + } + } + + // Send 8-bit scaled and quantized CV + int32_t note = Proportion(reg[0] & 0xff, 0xff, range_mod) + 64; + int32_t note2 = Proportion(reg[1] & 0xff, 0xff, range_mod) + 64; + + ForEachChannel(ch) { + switch (outmode[ch]) { + case PITCH_BLEND: { + // this is the unique case where input CV crossfades between the two melodies + int x = constrain(note_trans[2], -range_mod, range_mod); + int y = range_mod; + int n = (note * (y + x) + note2 * (y - x)) / (2*y); + slew(Output[ch], QuantizerLookup(0, n)); + break; + } + case PITCH1: + slew(Output[ch], QuantizerLookup(0, note + note_trans[0])); + break; + case PITCH2: + slew(Output[ch], QuantizerLookup(0, note2 + note_trans[1])); + break; + case MOD1: // 8-bit bi-polar proportioned CV + slew(Output[ch], Proportion( int(reg[0] & 0xff)-0x7f, 0x80, HEMISPHERE_MAX_CV) ); + break; + case MOD2: + slew(Output[ch], Proportion( int(reg[1] & 0xff)-0x7f, 0x80, HEMISPHERE_MAX_CV) ); + break; + case TRIG1: + case TRIG2: + if (clk && (reg[outmode[ch]-TRIG1] & 0x01) == 1) // trigger if 1st bit is high + { + Output[ch] = HEMISPHERE_MAX_CV; //ClockOut(ch); + trigpulse[ch] = HEMISPHERE_CLOCK_TICKS * trig_length; + } + else // decay + { + // hold until it's time to pull it down + if (--trigpulse[ch] < 0) + slew(Output[ch]); + } + break; + case GATE1: + case GATE2: + slew(Output[ch], (reg[outmode[ch] - GATE1] & 0x01)*HEMISPHERE_MAX_CV ); + break; + + case GATE_SUM: + slew(Output[ch], ((reg[0] & 0x01)+(reg[1] & 0x01))*HEMISPHERE_MAX_CV/2 ); + break; + + default: break; + } + + Out(ch, Output[ch]); + } + } + + void View() { + DrawSelector(); + DrawIndicator(); + } + + void OnButtonPress() { + CursorAction(cursor, LAST_SETTING); + } + + void OnEncoderMove(int direction) { + if (!EditMode()) { + MoveCursor(cursor, direction, LAST_SETTING); + return; + } + + switch ((TM2Cursor)cursor) { + case LENGTH: + length = constrain(length + direction, TM2_MIN_LENGTH, TM2_MAX_LENGTH); + break; + case PROB: + p = constrain(p + direction, 0, 100); + break; + case SCALE: + NudgeScale(0, direction); + NudgeScale(1, direction); + break; + case ROOT_NOTE: + root_note = GetRootNote(0); + root_note = SetRootNote(0, constrain(root_note + direction, 0, 11)); + break; + case RANGE: + range = constrain(range + direction, 1, 32); + break; + case OUT_A: + outmode[0] = (OutputMode) constrain(outmode[0] + direction, 0, OUTMODE_LAST-1); + break; + case OUT_B: + outmode[1] = (OutputMode) constrain(outmode[1] + direction, 0, OUTMODE_LAST-1); + break; + case CVMODE1: + cvmode[0] = (InputMode) constrain(cvmode[0] + direction, 0, INMODE_LAST-1); + break; + case CVMODE2: + cvmode[1] = (InputMode) constrain(cvmode[1] + direction, 0, INMODE_LAST-1); + break; + case SLEW: + smoothing = constrain(smoothing + direction, 0, 127); + break; + + default: break; + } + } + + uint64_t OnDataRequest() { + uint64_t data = 0; + Pack(data, PackLocation {0,7}, p); + Pack(data, PackLocation {7,5}, length - 1); + Pack(data, PackLocation {12,5}, range - 1); + Pack(data, PackLocation {17,4}, outmode[0]); + Pack(data, PackLocation {21,4}, outmode[1]); + Pack(data, PackLocation {25,8}, constrain(GetScale(0), 0, 255)); + Pack(data, PackLocation {33,4}, cvmode[0]); + Pack(data, PackLocation {37,4}, cvmode[1]); + Pack(data, PackLocation {41,6}, smoothing); + Pack(data, PackLocation {47,4}, root_note); + + // TODO: utilize enigma's global turing machine storage for the registers + + return data; + } + + void OnDataReceive(uint64_t data) { + p = Unpack(data, PackLocation {0,7}); + length = Unpack(data, PackLocation {7,5}) + 1; + range = Unpack(data, PackLocation{12,5}) + 1; + outmode[0] = (OutputMode) Unpack(data, PackLocation {17,4}); + outmode[1] = (OutputMode) Unpack(data, PackLocation {21,4}); + int scale = Unpack(data, PackLocation {25,8}); + cvmode[0] = (InputMode) Unpack(data, PackLocation {33,4}); + cvmode[1] = (InputMode) Unpack(data, PackLocation {37,4}); + smoothing = Unpack(data, PackLocation {41,6}); + smoothing = constrain(smoothing, 0, 127); + root_note = Unpack(data, PackLocation {47,4}); + + QuantizerConfigure(0, scale); + SetRootNote(0, root_note); + } + +protected: + void SetHelp() { + // "------------------" <-- Size Guide + help[HEMISPHERE_HELP_DIGITALS] = "1=Clock 2=p Gate"; + help[HEMISPHERE_HELP_CVS] = "Assignable"; + help[HEMISPHERE_HELP_OUTS] = "Assignable"; + help[HEMISPHERE_HELP_ENCODER] = "Select/Push 2 Edit"; + // "------------------" <-- Size Guide + } + +private: + int cursor; // TM2Cursor + + int root_note = 0; + + // TODO: consider using the TuringMachine class or whatev + uint32_t reg[2]; // 32-bit sequence registers + + // most recent output values + int Output[2] = {0, 0}; + int trigpulse[2] = {0, 0}; // tick timer for Trig output modes + + // Settings and modulated copies + int length = 16; // Sequence length + int len_mod; // actual length after CV mod + int p = 0; // Probability of bit flipping on each cycle + int p_mod; + int range = 24; + int range_mod; + int smoothing = 0; + int smooth_mod; + int note_trans[3] = {0, 0, 0}; // transpose from CV input + + OutputMode outmode[2] = {PITCH1, TRIG2}; + InputMode cvmode[2] = {LENGTH_MOD, RANGE_MOD}; + + void slew(int &old_val, const int new_val = 0) { + const int s = 1 + smooth_mod; + // more smoothing causes more ticks to be skipped + if (OC::CORE::ticks % s) return; + + old_val = (old_val * (s - 1) + new_val) / s; + } + + void DrawOutputMode(int ch) { + const int y = 35; + const int x = 34*ch; + + gfxPrint(x+1, y+1, ch ? (hemisphere ? "D" : "B") : (hemisphere ? "C" : "A") ); + gfxPrint(":"); + + switch (outmode[ch]) { + case PITCH_BLEND: gfxBitmap(24+x, y, 3, SUP_ONE); + case PITCH1: + case PITCH2: + gfxBitmap(15 + x, y, 8, NOTE_ICON); + break; + case MOD1: + case MOD2: + gfxBitmap(15 + x, y, 8, WAVEFORM_ICON); + break; + case TRIG1: + case TRIG2: + gfxBitmap(15 + x, y, 8, CLOCK_ICON); + break; + case GATE_SUM: gfxBitmap(24+x, y, 3, SUB_TWO); + case GATE1: + case GATE2: + gfxBitmap(15 + x, y, 8, GATE_ICON); + break; + + default: break; + } + + // indicator for reg1 or reg2 + gfxBitmap(24+x, y, 3, (outmode[ch] % 2) ? SUP_ONE : SUB_TWO ); + } + + void DrawCVMode(int ch) { + const int y = 25; + const int x = 34*ch; + + gfxIcon(1 + x, y, CV_ICON); + gfxBitmap(9 + x, y, 3, ch ? SUB_TWO : SUP_ONE); + + switch (cvmode[ch]) { + case SLEW_MOD: + gfxIcon(15 + x, y, SLEW_ICON); + break; + case LENGTH_MOD: + gfxIcon(15 + x, y, LOOP_ICON); + break; + case P_MOD: + gfxIcon(15 + x, y, TOSS_ICON); + break; + case RANGE_MOD: + gfxIcon(15 + x, y, UP_DOWN_ICON); + break; + case TRANSPOSE1: + gfxIcon(15 + x, y, BEND_ICON); + gfxBitmap(24+x, y, 3, SUP_ONE); + break; + case BLEND_XFADE: + gfxBitmap(24+x, y, 3, SUP_ONE); + case TRANSPOSE2: + gfxIcon(15 + x, y, BEND_ICON); + gfxBitmap(24+x, y, 3, SUB_TWO); + break; + + default: break; + } + + } + + void DrawSelector() { + gfxBitmap(1, 14, 8, LOOP_ICON); + gfxPrint(12 + pad(10, len_mod), 15, len_mod); + gfxPrint(35 + pad(100, p_mod), 15, p_mod); + if (cursor == PROB || Gate(1)) { // p unlocked + gfxBitmap(55, 15, 8, TOSS_ICON); + } else { // p is disabled + gfxBitmap(55, 15, 8, LOCK_ICON); + } + + // two separate pages of params + switch ((TM2Cursor)cursor){ + default: + case SLEW: + gfxBitmap(1, 25, 8, SCALE_ICON); + gfxPrint(9, 25, OC::scale_names_short[GetScale(0)]); + gfxPrint(39, 25, OC::Strings::note_names_unpadded[GetRootNote(0)]); + + gfxBitmap(1, 35, 8, UP_DOWN_ICON); + gfxPrint(10, 35, range_mod); + + gfxIcon(35, 35, SLEW_ICON); + gfxPrint(44, 35, smooth_mod); + + break; + case CVMODE1: + case CVMODE2: + case OUT_A: + case OUT_B: + ForEachChannel(ch) { + DrawCVMode(ch); + DrawOutputMode(ch); + } + + break; + } + + // TODO: generalize this as a cursor LUT for all applets + switch ((TM2Cursor)cursor) { + case LENGTH: gfxCursor(12, 23, 13); break; + case PROB: gfxCursor(35, 23, 19); break; + case SCALE: gfxCursor( 9, 33, 25); break; + case ROOT_NOTE: gfxCursor( 39, 33, 13); break; + case RANGE: gfxCursor(10, 43, 13); break; + case SLEW: gfxCursor(44, 43, 19); break; + + case CVMODE1: + case CVMODE2: + gfxCursor(14 + 34*(cursor-CVMODE1), 33, 10); + break; + + case OUT_A: gfxCursor(14, 43, 10); break; + case OUT_B: gfxCursor(48, 43, 10); break; + + default: break; + } + } + + void DrawIndicator() { + gfxLine(0, 45, 63, 45); + gfxLine(0, 62, 63, 62); + for (int b = 0; b < 16; b++) + { + int v = (reg[0] >> b) & 0x01; + int v2 = (reg[1] >> b) & 0x01; + if (v) gfxRect(60 - (4 * b), 47, 3, 7); + if (v2) gfxRect(60 - (4 * b), 54, 3, 7); + } + } + +}; + + +//////////////////////////////////////////////////////////////////////////////// +//// Hemisphere Applet Functions +/// +/// Once you run the find-and-replace to make these refer to DualTM, +/// it's usually not necessary to do anything with these functions. You +/// should prefer to handle things in the HemisphereApplet child class +/// above. +//////////////////////////////////////////////////////////////////////////////// +DualTM DualTM_instance[2]; + +void DualTM_Start(bool hemisphere) { + DualTM_instance[hemisphere].BaseStart(hemisphere); +} + +void DualTM_Controller(bool hemisphere, bool forwarding) { + DualTM_instance[hemisphere].BaseController(forwarding); +} + +void DualTM_View(bool hemisphere) { + DualTM_instance[hemisphere].BaseView(); +} + +void DualTM_OnButtonPress(bool hemisphere) { + DualTM_instance[hemisphere].OnButtonPress(); +} + +void DualTM_OnEncoderMove(bool hemisphere, int direction) { + DualTM_instance[hemisphere].OnEncoderMove(direction); +} + +void DualTM_ToggleHelpScreen(bool hemisphere) { + DualTM_instance[hemisphere].HelpScreen(); +} + +uint64_t DualTM_OnDataRequest(bool hemisphere) { + return DualTM_instance[hemisphere].OnDataRequest(); +} + +void DualTM_OnDataReceive(bool hemisphere, uint64_t data) { + DualTM_instance[hemisphere].OnDataReceive(data); +} diff --git a/software/o_c_REV/HEM_Trending.ino b/software/o_c_REV/HEM_Trending.ino index 2271aaf73..c0f04b3ea 100644 --- a/software/o_c_REV/HEM_Trending.ino +++ b/software/o_c_REV/HEM_Trending.ino @@ -86,17 +86,20 @@ public: } void View() { - gfxHeader(applet_name()); DrawSelector(); DrawIndicator(); } void OnButtonPress() { - if (++cursor > 2) cursor = 0; - ResetCursor(); + CursorAction(cursor, 2); } void OnEncoderMove(int direction) { + if (!EditMode()) { + MoveCursor(cursor, direction, 2); + return; + } + if (cursor < 2) { assign[cursor] = constrain(assign[cursor] + direction, 0, 5); reset[cursor] = 1; diff --git a/software/o_c_REV/HEM_TrigSeq.ino b/software/o_c_REV/HEM_TrigSeq.ino index c7bc1a3e6..b40265ff9 100644 --- a/software/o_c_REV/HEM_TrigSeq.ino +++ b/software/o_c_REV/HEM_TrigSeq.ino @@ -37,28 +37,23 @@ public: } void Controller() { - if (Clock(1)) { - reset = true; - ForEachChannel(ch) step[ch] = 0; + if (Clock(1)) { // reset + ForEachChannel(ch) step[ch] = -1; } - if (Clock(0)) { + if (Clock(0)) { // clock advance bool swap = In(0) >= HEMISPHERE_3V_CV; - if (!reset) { - ForEachChannel(ch) step[ch]++; - } - reset = false; ForEachChannel(ch) { - if (step[ch] > end_step[ch]) step[ch] = 0; - if ((pattern[ch] >> step[ch]) & 0x01) ClockOut(swap ? (1 - ch) : ch); + if (step[ch] >= end_step[ch]) step[ch] = -1; + step[ch]++; + active_step[ch] = Step(ch); + if ((pattern[ch] >> active_step[ch]) & 0x01) ClockOut(swap ? (1 - ch) : ch); } - } } void View() { - gfxHeader(applet_name()); DrawDisplay(); } @@ -74,7 +69,7 @@ public: // Update end_step if (this_cursor == 2) { - end_step[ch] = constrain(end_step[ch] += direction, 0, 7); + end_step[ch] = constrain(end_step[ch] + direction, 0, 7); } else { // Get the current pattern int curr_patt = pattern[ch]; @@ -115,19 +110,38 @@ protected: help[HEMISPHERE_HELP_ENCODER] = "T=Set P=Select"; // "------------------" <-- Size Guide } - + private: int step[2]; // Current step of each channel + int active_step[2]; uint8_t pattern[2]; int end_step[2]; int cursor; // 0=ch1 low, 1=ch1 hi, 2=c1 end_step, 3=ch2 low, 4=ch3 hi, 5=ch2 end_step bool reset; + + inline int Length(int ch) const { + return end_step[ch] + 1; + } + + int Step(int ch) { + int s = step[ch] + Offset(ch); + s %= Length(ch); + return s; + } + + int Offset(int ch) { + int offset = Proportion(DetentedIn(1), HEMISPHERE_MAX_INPUT_CV, end_step[ch]); + if (offset < 0) offset += Length(ch); + offset %= Length(ch); + return offset; + } void DrawDisplay() { ForEachChannel(ch) { int this_cursor = cursor - (3 * ch); int x = 10 + (31 * ch); + int offset = Offset(ch); // Draw the steps for this channel bool stop = 0; // Stop displaying when end_step is reached @@ -145,10 +159,14 @@ private: } } - if (s == step[ch]) { + if (s == active_step[ch]) { gfxLine(x + 4, y, x + 10, y); } + if (s == offset) { + gfxFrame(x - 4, y - 4, 9, 9); + } + // Draw the end_step cursor if (s == end_step[ch]) { if (this_cursor == 2 && CursorBlink()) { diff --git a/software/o_c_REV/HEM_TrigSeq16.ino b/software/o_c_REV/HEM_TrigSeq16.ino index 567fa9484..1b02e113b 100644 --- a/software/o_c_REV/HEM_TrigSeq16.ino +++ b/software/o_c_REV/HEM_TrigSeq16.ino @@ -33,32 +33,27 @@ public: step = 0; end_step = 15; cursor = 0; - reset = true; } void Controller() { - if (Clock(1)) { - reset = true; - step = 0; - } + if (Clock(1)) step = -1; // reset regardless of clock if (Clock(0)) { bool swap = In(0) >= HEMISPHERE_3V_CV; - if (!reset) step++; - reset = false; - if (step > end_step) step = 0; - if (step < 8) { - if ((pattern[0] >> step) & 0x01) ClockOut(swap ? 1 : 0); + if (step >= end_step) step = -1; + step++; + active_step = Step(); // actual step after Offset modulation + if (active_step < 8) { + if ((pattern[0] >> active_step) & 0x01) ClockOut(swap ? 1 : 0); else ClockOut(swap ? 0 : 1); } else { - if ((pattern[1] >> (step - 8)) & 0x01) ClockOut(swap ? 1 : 0); + if ((pattern[1] >> (active_step - 8)) & 0x01) ClockOut(swap ? 1 : 0); else ClockOut(swap ? 0 : 1); } } } void View() { - gfxHeader(applet_name()); DrawDisplay(); } @@ -71,7 +66,7 @@ public: void OnEncoderMove(int direction) { // Update end_step if (cursor == 4) { - end_step = constrain(end_step += direction, 0, 15); + end_step = constrain(end_step + direction, 0, 15); } else { int ch = cursor > 1 ? 1 : 0; int this_cursor = cursor - (ch * 2); @@ -116,14 +111,33 @@ protected: private: int step; // Current step + int active_step; uint8_t pattern[2]; int end_step; int cursor; // 0=ch1 low, 1=ch1 hi, 2=ch2 low, 3=ch3 hi, 4=end_step - bool reset; - + + int Offset() { + int offset = Proportion(DetentedIn(1), HEMISPHERE_MAX_INPUT_CV, end_step); + if (offset < 0) offset += Length(); + offset %= Length(); + return offset; + } + + inline int Length() const { + return end_step + 1; + } + + int Step() { + int s = step + Offset(); + s %= Length(); + return s; + } + void DrawDisplay() { bool stop = 0; // Stop displaying when end_step is reached + int offset = Offset(); + ForEachChannel(ch) { int x = 10 + (31 * ch); @@ -143,10 +157,14 @@ private: } } - if (s + (ch * 8) == step) { + if (s + (ch * 8) == active_step) { gfxLine(x + 4, y, x + 10, y); } + if (s + (ch * 8) == offset) { + gfxFrame(x - 4, y - 4, 9, 9); + } + // Draw the end_step cursor if (s + (ch * 8) == end_step) { if (cursor == 4) { diff --git a/software/o_c_REV/HEM_Tuner.ino b/software/o_c_REV/HEM_Tuner.ino index 61ce7fc65..4c8d56904 100644 --- a/software/o_c_REV/HEM_Tuner.ino +++ b/software/o_c_REV/HEM_Tuner.ino @@ -70,7 +70,6 @@ public: } void View() { - gfxHeader(applet_name()); #ifdef FLIP_180 if (hemisphere == 0) DrawTuner(); #else @@ -99,10 +98,11 @@ public: protected: void SetHelp() { - if (hemisphere == 1) { #ifdef FLIP_180 + if (hemisphere == 0) { help[HEMISPHERE_HELP_DIGITALS] = "1=Input"; #else + if (hemisphere == 1) { help[HEMISPHERE_HELP_DIGITALS] = "2=Input"; #endif help[HEMISPHERE_HELP_CVS] = ""; diff --git a/software/o_c_REV/HEM_VectorEG.ino b/software/o_c_REV/HEM_VectorEG.ino index accbeacd5..6f87562d8 100644 --- a/software/o_c_REV/HEM_VectorEG.ino +++ b/software/o_c_REV/HEM_VectorEG.ino @@ -57,15 +57,19 @@ public: } void View() { - gfxHeader(applet_name()); DrawInterface(); } void OnButtonPress() { - if (++cursor > 3) cursor = 0; + CursorAction(cursor, 3); } void OnEncoderMove(int direction) { + if (!EditMode()) { + MoveCursor(cursor, direction, 3); + return; + } + byte c = cursor; byte ch = cursor < 2 ? 0 : 1; if (ch) c -= 2; @@ -135,7 +139,7 @@ private: DrawWaveform(ch); if (c == 0) gfxCursor(8, 23, 55); - if (c == 1 && CursorBlink()) gfxFrame(0, 24, 63, 40); + if (c == 1 && (EditMode() || CursorBlink()) ) gfxFrame(0, 24, 63, 40); } void DrawWaveform(byte ch) { diff --git a/software/o_c_REV/HEM_VectorLFO.ino b/software/o_c_REV/HEM_VectorLFO.ino index d5ef56eb0..1653b1d82 100644 --- a/software/o_c_REV/HEM_VectorLFO.ino +++ b/software/o_c_REV/HEM_VectorLFO.ino @@ -28,7 +28,7 @@ public: return "VectorLFO"; } - static constexpr int min_freq = 3; + static constexpr int min_freq = 8; static constexpr int max_freq = 100000; void Start() { @@ -43,17 +43,13 @@ public: void Controller() { // Input 1 is frequency modulation for channel 1 - if (Changed(0)) { - int mod = Proportion(DetentedIn(0), HEMISPHERE_3V_CV, 3000); - mod = constrain(mod, -3000, 3000); - if (mod + freq[0] > 10) osc[0].SetFrequency(freq[0] + mod); - } + int freq_mod = Proportion(DetentedIn(0), HEMISPHERE_3V_CV, 3000); + freq_mod = constrain(freq[0] + freq_mod, min_freq, max_freq); + osc[0].SetFrequency(freq_mod); - // Input 2 determines signal 1's attenuation on the B/D output mix; at 0V, signal 1 - // accounts for 50% of the B/D output. At 5V, signal 1 accounts for none of the - // B/D output. - int atten1 = DetentedIn(1); - atten1 = constrain(atten1, 0, HEMISPHERE_MAX_CV); + // Input 2 determines signal 1's level on the B/D output mix + int mix_level = DetentedIn(1); + mix_level = constrain(mix_level, -HEMISPHERE_MAX_CV, HEMISPHERE_MAX_CV); int signal = 0; // Declared here because the first channel's output is used in the second channel; see below ForEachChannel(ch) @@ -61,9 +57,8 @@ public: if (Clock(ch)) { uint32_t ticks = ClockCycleTicks(ch); int new_freq = 1666666 / ticks; - new_freq = constrain(new_freq, min_freq, max_freq); - osc[ch].SetFrequency(new_freq); - freq[ch] = new_freq; + freq[ch] = constrain(new_freq, min_freq, max_freq); + osc[ch].SetFrequency(freq[ch]); osc[ch].Reset(); } @@ -71,31 +66,33 @@ public: // Out A is always just the first oscillator at full amplitude signal = osc[ch].Next(); } else { - // Out B can have channel 1 blended into it, depending on the value of atten1. At a value - // of 0, Out B is a 50/50 mix of channels 1 and 2. At a value of 5V, channel 1 is absent - // from Out B. - signal = Proportion(HEMISPHERE_MAX_CV - atten1, HEMISPHERE_MAX_CV, signal); // signal from channel 1's iteration + // Out B can have channel 1 blended into it, depending on the value of mix_level. + // At a value of 6V, Out B is a 50/50 mix of channels 1 and 2. + // At a value of 0V, channel 1 is absent from Out B. + signal = Proportion(mix_level, HEMISPHERE_MAX_CV, signal); // signal from channel 1's iteration signal += osc[ch].Next(); - // Proportionally blend the signal, depending on attenuation. If atten1 is 0, then this - // effectively divides the signal by 2. If atten1 is 5V, then the channel 2 signal will be - // output at full amplitude. - signal = Proportion(HEMISPHERE_MAX_CV, HEMISPHERE_MAX_CV + (HEMISPHERE_MAX_CV - atten1), signal); + // Proportionally blend the signal, depending on mix. + // If mix_level is at (+ or -) max, then this effectively divides the signal by 2. + signal = Proportion(HEMISPHERE_MAX_CV, HEMISPHERE_MAX_CV + abs(mix_level), signal); } Out(ch, signal); } } void View() { - gfxHeader(applet_name()); DrawInterface(); } void OnButtonPress() { - if (++cursor > 3) cursor = 0; + CursorAction(cursor, 3); } void OnEncoderMove(int direction) { + if (!EditMode()) { + MoveCursor(cursor, direction, 3); + return; + } byte c = cursor; byte ch = cursor < 2 ? 0 : 1; if (ch) c -= 2; @@ -132,10 +129,10 @@ public: if (freq[i] > 250) exponent++; if (freq[i] > 1000) exponent++; if (freq[i] > 10000) exponent++; - Pack(data, PackLocation {12 + i * 10, 2}, exponent); + Pack(data, PackLocation {uint8_t(12 + i * 10), 2}, exponent); int mantissa = freq[i] / pow10_lut[exponent]; - Pack(data, PackLocation {12 + i * 10 + 2, 8}, mantissa); + Pack(data, PackLocation {uint8_t(12 + i * 10 + 2), 8}, mantissa); } return data; @@ -143,8 +140,8 @@ public: void OnDataReceive(uint64_t data) { for (int i = 0; i < 2; ++i) { - int exponent = Unpack(data, PackLocation {12 + i * 10, 2}); - int mantissa = Unpack(data, PackLocation {12 + i * 10 + 2, 8}); + int exponent = Unpack(data, PackLocation {uint8_t(12 + i * 10), 2}); + int mantissa = Unpack(data, PackLocation {uint8_t(12 + i * 10 + 2), 8}); freq[i] = mantissa * pow10_lut[exponent]; } @@ -156,7 +153,7 @@ protected: void SetHelp() { // "------------------" <-- Size Guide help[HEMISPHERE_HELP_DIGITALS] = "1,2=Sync"; - help[HEMISPHERE_HELP_CVS] = "1=Freq1 2=Atten1@B"; + help[HEMISPHERE_HELP_CVS] = "1=Freq1 2=Mix 1@B"; help[HEMISPHERE_HELP_OUTS] = "Out A=1, B=2+1"; help[HEMISPHERE_HELP_ENCODER] = "Freq./Waveform"; // "------------------" <-- Size Guide @@ -194,7 +191,7 @@ private: DrawWaveform(ch); if (c == 0) gfxCursor(8, 23, 55); - if (c == 1 && CursorBlink()) gfxFrame(0, 24, 63, 40); + if (c == 1 && (EditMode() || CursorBlink()) ) gfxFrame(0, 24, 63, 40); } void DrawWaveform(byte ch) { diff --git a/software/o_c_REV/HEM_VectorMod.ino b/software/o_c_REV/HEM_VectorMod.ino index 78933d6d6..2fed21854 100644 --- a/software/o_c_REV/HEM_VectorMod.ino +++ b/software/o_c_REV/HEM_VectorMod.ino @@ -50,15 +50,18 @@ public: } void View() { - gfxHeader(applet_name()); DrawInterface(); } void OnButtonPress() { - if (++cursor > 3) cursor = 0; + CursorAction(cursor, 3); } void OnEncoderMove(int direction) { + if (!EditMode()) { + MoveCursor(cursor, direction, 3); + return; + } byte c = cursor; byte ch = cursor < 2 ? 0 : 1; if (ch) c -= 2; @@ -127,7 +130,7 @@ private: DrawWaveform(ch); if (c == 0) gfxCursor(8, 23, 55); - if (c == 1 && CursorBlink()) gfxFrame(0, 24, 63, 40); + if (c == 1 && (EditMode() || CursorBlink()) ) gfxFrame(0, 24, 63, 40); } void DrawWaveform(byte ch) { diff --git a/software/o_c_REV/HEM_VectorMorph.ino b/software/o_c_REV/HEM_VectorMorph.ino index a7685de8c..638ae310f 100644 --- a/software/o_c_REV/HEM_VectorMorph.ino +++ b/software/o_c_REV/HEM_VectorMorph.ino @@ -45,7 +45,7 @@ public: ForEachChannel(ch) { if (!linked || ch == 0) { - cv_phase = Proportion(In(ch), HEMISPHERE_MAX_CV, 3599); + cv_phase = Proportion(In(ch), HEMISPHERE_MAX_INPUT_CV, 3599); cv_phase = constrain(cv_phase, -3599, 3599); } last_phase[ch] = (phase[ch] * 10) + cv_phase; @@ -54,15 +54,18 @@ public: } void View() { - gfxHeader(applet_name()); DrawInterface(); } void OnButtonPress() { - if (++cursor > 3) cursor = 0; + CursorAction(cursor, 3); } void OnEncoderMove(int direction) { + if (!EditMode()) { + MoveCursor(cursor, direction, 3); + return; + } byte c = cursor; byte ch = cursor < 2 ? 0 : 1; if (ch) c -= 2; @@ -131,7 +134,7 @@ private: // Cursors if (c == 0) gfxCursor(8, 23, 55); - if (c == 1 && CursorBlink()) gfxFrame(0, 24, 63, 40); + if (c == 1 && (EditMode() || CursorBlink()) ) gfxFrame(0, 24, 63, 40); // Link icon if (linked) gfxIcon(54, 15, LINK_ICON); diff --git a/software/o_c_REV/HEM_Voltage.ino b/software/o_c_REV/HEM_Voltage.ino index 30f0b4a31..1d56116b4 100644 --- a/software/o_c_REV/HEM_Voltage.ino +++ b/software/o_c_REV/HEM_Voltage.ino @@ -51,20 +51,23 @@ public: } void View() { - gfxHeader(applet_name()); DrawInterface(); } void OnButtonPress() { - if (++cursor > 3) cursor = 0; - ResetCursor(); + CursorAction(cursor, 3); } void OnEncoderMove(int direction) { + if (!EditMode()) { + MoveCursor(cursor, direction, 3); + return; + } + uint8_t ch = cursor / 2; if (cursor == 0 || cursor == 2) { // Change voltage - int min = -HEMISPHERE_3V_CV / VOLTAGE_INCREMENTS; + int min = -HEMISPHERE_MAX_CV / VOLTAGE_INCREMENTS; int max = HEMISPHERE_MAX_CV / VOLTAGE_INCREMENTS; voltage[ch] = constrain(voltage[ch] + direction, min, max); } else { @@ -117,9 +120,7 @@ private: if (view[ch]) gfxInvert(0, 14 + (ch * 20), 7, 9); } - int ch = cursor / 2; - if (cursor == 0 or cursor == 2) gfxCursor(13, 23 + (ch * 20), 36); - else gfxCursor(13, 33 + (ch * 20), 36); + gfxCursor(12, 23 + cursor * 10, 37); } }; diff --git a/software/o_c_REV/HEM_hMIDIIn.ino b/software/o_c_REV/HEM_hMIDIIn.ino index 75e295a9d..5ce472b80 100644 --- a/software/o_c_REV/HEM_hMIDIIn.ino +++ b/software/o_c_REV/HEM_hMIDIIn.ino @@ -20,192 +20,110 @@ // See https://www.pjrc.com/teensy/td_midi.html -#define HEM_MIDI_CLOCK_DIVISOR 12 - -#define HEM_MIDI_NOTE_ON 1 -#define HEM_MIDI_NOTE_OFF 0 -#define HEM_MIDI_CC 3 -#define HEM_MIDI_AFTERTOUCH 5 -#define HEM_MIDI_PITCHBEND 6 -#define HEM_MIDI_SYSEX 7 -#define HEM_MIDI_REALTIME 8 - -// The functions available for each output -#define HEM_MIDI_NOTE_OUT 0 -#define HEM_MIDI_TRIG_OUT 1 -#define HEM_MIDI_GATE_OUT 2 -#define HEM_MIDI_VEL_OUT 3 -#define HEM_MIDI_CC_OUT 4 -#define HEM_MIDI_AT_OUT 5 -#define HEM_MIDI_PB_OUT 6 -#define HEM_MIDI_REALTIME_OUT 7 - -struct MIDILogEntry { - int message; - int data1; - int data2; -}; - class hMIDIIn : public HemisphereApplet { public: + enum hMIDIInCursor { + MIDI_CHANNEL_A, + MIDI_CHANNEL_B, + OUTPUT_MODE_A, + OUTPUT_MODE_B, + LOG_VIEW, + + MIDI_CURSOR_LAST = LOG_VIEW + }; + const char* applet_name() { return "MIDIIn"; } void Start() { - first_note = -1; - channel = 0; // Default channel 1 - - const char * fn_name_list[] = {"Note#", "Trig", "Gate", "Veloc", "Mod", "Aft", "Bend", "Clock"}; - for (int i = 0; i < 8; i++) fn_name[i] = fn_name_list[i]; - ForEachChannel(ch) { - function[ch] = ch * 2; + int ch_ = ch + io_offset; + frame.MIDIState.channel[ch_] = 0; // Default channel 1 + frame.MIDIState.function[ch_] = HEM_MIDI_NOOP; + frame.MIDIState.outputs[ch_] = 0; Out(ch, 0); } - log_index = 0; - clock_count = 0; + frame.MIDIState.log_index = 0; + frame.MIDIState.clock_count = 0; } void Controller() { - if (usbMIDI.read()) { - int message = usbMIDI.getType(); - int data1 = usbMIDI.getData1(); - int data2 = usbMIDI.getData2(); - - if (message == HEM_MIDI_SYSEX) ReceiveManagerSysEx(); - - // Listen for incoming clock - if (message == HEM_MIDI_REALTIME && data1 == 0) { - if (++clock_count == 1) { - ForEachChannel(ch) - { - if (function[ch] == HEM_MIDI_REALTIME_OUT) { - ClockOut(ch); - } - } + // MIDI input is processed at a higher level + // here, we just pass the MIDI signals on to physical outputs + ForEachChannel(ch) { + int ch_ = ch + io_offset; + switch (frame.MIDIState.function[ch_]) { + case HEM_MIDI_NOOP: + break; + case HEM_MIDI_CLOCK_OUT: + case HEM_MIDI_START_OUT: + case HEM_MIDI_TRIG_OUT: + if (frame.MIDIState.trigout_q[ch_]) { + frame.MIDIState.trigout_q[ch_] = 0; + ClockOut(ch); } - if (clock_count == HEM_MIDI_CLOCK_DIVISOR) clock_count = 0; - } - - if (usbMIDI.getChannel() == (channel + 1)) { - last_tick = OC::CORE::ticks; - bool log_this = false; - - if (message == HEM_MIDI_NOTE_ON) { // Note on - if (first_note == -1) first_note = data1; - - // Should this message go out on any channel? - ForEachChannel(ch) - { - if (function[ch] == HEM_MIDI_NOTE_OUT) - Out(ch, MIDIQuantizer::CV(data1)); - - if (function[ch] == HEM_MIDI_TRIG_OUT) - ClockOut(ch); - - if (function[ch] == HEM_MIDI_GATE_OUT) - GateOut(ch, 1); - - if (function[ch] == HEM_MIDI_VEL_OUT) - Out(ch, Proportion(data2, 127, HEMISPHERE_MAX_CV)); - } - - log_this = 1; // Log all MIDI notes. Other stuff is conditional. - } - - if (message == HEM_MIDI_NOTE_OFF) { // Note off - if (data1 == first_note) first_note = -1; - - // Should this message go out on any channel? - ForEachChannel(ch) - { - if (function[ch] == HEM_MIDI_GATE_OUT) { - GateOut(ch, 0); - log_this = 1; - } - } - } - - if (message == HEM_MIDI_CC) { // Modulation wheel - ForEachChannel(ch) - { - if (function[ch] == HEM_MIDI_CC_OUT && data1 == 1) { - int data = data2 << 8; - Out(ch, Proportion(data, 0x7fff, HEMISPHERE_MAX_CV)); - log_this = 1; - } - } - - } - - if (message == HEM_MIDI_AFTERTOUCH) { // Aftertouch - ForEachChannel(ch) - { - if (function[ch] == HEM_MIDI_AT_OUT) { - int data = data2 << 8; - Out(ch, Proportion(data, 0x7fff, HEMISPHERE_MAX_CV)); - log_this = 1; - } - } - - UpdateLog(message, data1, data2); - } - - if (message == HEM_MIDI_PITCHBEND) { // Pitch Bend - ForEachChannel(ch) - { - if (function[ch] == HEM_MIDI_PB_OUT) { - int data = (data2 << 7) + data1 - 8192; - Out(ch, Proportion(data, 0x7fff, HEMISPHERE_3V_CV)); - log_this = 1; - } - } - } - - if (log_this) UpdateLog(message, data1, data2); + break; + default: + Out(ch, frame.MIDIState.outputs[ch_]); + break; } } } void View() { - gfxHeader(applet_name()); DrawMonitor(); - if (cursor == 3) DrawLog(); + if (cursor == LOG_VIEW) DrawLog(); else DrawSelector(); } void OnButtonPress() { - if (++cursor > 3) cursor = 0; - ResetCursor(); + CursorAction(cursor, MIDI_CURSOR_LAST); } void OnEncoderMove(int direction) { - if (cursor == 3) return; - if (cursor == 0) channel = constrain(channel += direction, 0, 15); + if (!EditMode()) { + MoveCursor(cursor, direction, MIDI_CURSOR_LAST); + return; + } + + // Log view + if (cursor == LOG_VIEW) return; + + if (cursor == MIDI_CHANNEL_A || cursor == MIDI_CHANNEL_B) { + int ch = io_offset + cursor - MIDI_CHANNEL_A; + frame.MIDIState.channel[ch] = constrain(frame.MIDIState.channel[ch] + direction, 0, 15); + } else { - int ch = cursor - 1; - function[ch] = constrain(function[ch] += direction, 0, 7); - clock_count = 0; + int ch = io_offset + cursor - OUTPUT_MODE_A; + frame.MIDIState.function[ch] = constrain(frame.MIDIState.function[ch] + direction, 0, HEM_MIDI_MAX_FUNCTION); + frame.MIDIState.function_cc[ch] = -1; // auto-learn MIDI CC + frame.MIDIState.clock_count = 0; } ResetCursor(); } uint64_t OnDataRequest() { uint64_t data = 0; - Pack(data, PackLocation {0,8}, channel); - Pack(data, PackLocation {8,3}, function[0]); - Pack(data, PackLocation {11,3}, function[1]); + Pack(data, PackLocation {0,4}, frame.MIDIState.channel[io_offset + 0]); + Pack(data, PackLocation {4,4}, frame.MIDIState.channel[io_offset + 1]); + Pack(data, PackLocation {8,3}, frame.MIDIState.function[io_offset + 0]); + Pack(data, PackLocation {11,3}, frame.MIDIState.function[io_offset + 1]); + Pack(data, PackLocation {14,7}, frame.MIDIState.function_cc[io_offset + 0] + 1); + Pack(data, PackLocation {21,7}, frame.MIDIState.function_cc[io_offset + 1] + 1); return data; } void OnDataReceive(uint64_t data) { - channel = Unpack(data, PackLocation {0,8}); - function[0] = Unpack(data, PackLocation {8,3}); - function[1] = Unpack(data, PackLocation {11,3}); + frame.MIDIState.channel[io_offset + 0] = Unpack(data, PackLocation {0,4}); + frame.MIDIState.channel[io_offset + 1] = Unpack(data, PackLocation {4,4}); + frame.MIDIState.function[io_offset + 0] = Unpack(data, PackLocation {8,3}); + frame.MIDIState.function[io_offset + 1] = Unpack(data, PackLocation {11,3}); + frame.MIDIState.function_cc[io_offset + 0] = Unpack(data, PackLocation {14,7}) - 1; + frame.MIDIState.function_cc[io_offset + 1] = Unpack(data, PackLocation {21,7}) - 1; } protected: @@ -219,98 +137,91 @@ protected: } private: - // Settings - int channel; // MIDI channel number - int function[2]; // Function for each channel - // Housekeeping int cursor; // 0=MIDI channel, 1=A/C function, 2=B/D function - int last_tick; // Tick of last received message - int first_note; // First note received, for awaiting Note Off - const char* fn_name[8]; - uint8_t clock_count; // MIDI clock counter (24ppqn) - // Logging - MIDILogEntry log[7]; - int log_index; - - void UpdateLog(int message, int data1, int data2) { - log[log_index++] = {message, data1, data2}; - if (log_index == 7) { - for (int i = 0; i < 6; i++) - { - memcpy(&log[i], &log[i+1], sizeof(log[i+1])); - } - log_index--; - } - } - void DrawMonitor() { - if (OC::CORE::ticks - last_tick < 4000) { + if (OC::CORE::ticks - frame.MIDIState.last_msg_tick < 4000) { gfxBitmap(46, 1, 8, MIDI_ICON); } } void DrawSelector() { - // MIDI Channel - gfxPrint(1, 15, "Ch:"); - gfxPrint(24, 15, channel + 1); + // MIDI Channels + gfxPrint(1, 15, hemisphere == 0 ? "ChA:" : "ChC:"); + gfxPrint(24, 15, frame.MIDIState.channel[io_offset + 0] + 1); + gfxPrint(1, 25, hemisphere == 0 ? "ChB:" : "ChD:"); + gfxPrint(24, 25, frame.MIDIState.channel[io_offset + 1] + 1); // Output 1 function - if (hemisphere == 0) gfxPrint(1, 25, "A :"); - else gfxPrint(1, 25, "C :"); - gfxPrint(24, 25, fn_name[function[0]]); + gfxPrint(1, 35, hemisphere == 0 ? "A :" : "C :"); + gfxPrint(24, 35, midi_fn_name[frame.MIDIState.function[io_offset + 0]]); + if (frame.MIDIState.function[io_offset + 0] == HEM_MIDI_CC_OUT) + gfxPrint(frame.MIDIState.function_cc[io_offset + 0]); // Output 2 function - if (hemisphere == 0) gfxPrint(1, 35, "B :"); - else gfxPrint(1, 35, "D :"); - gfxPrint(24, 35, fn_name[function[1]]); + gfxPrint(1, 45, hemisphere == 0 ? "B :" : "D :"); + gfxPrint(24, 45, midi_fn_name[frame.MIDIState.function[io_offset + 1]]); + if (frame.MIDIState.function[io_offset + 1] == HEM_MIDI_CC_OUT) + gfxPrint(frame.MIDIState.function_cc[io_offset + 1]); // Cursor gfxCursor(24, 23 + (cursor * 10), 39); // Last log entry - if (log_index > 0) { - log_entry(56, log_index - 1); + if (frame.MIDIState.log_index > 0) { + PrintLogEntry(56, frame.MIDIState.log_index - 1); } gfxInvert(0, 55, 63, 9); } void DrawLog() { - if (log_index) { - for (int i = 0; i < log_index; i++) + if (frame.MIDIState.log_index) { + for (int i = 0; i < frame.MIDIState.log_index; i++) { - log_entry(15 + (i * 8), i); + PrintLogEntry(15 + (i * 8), i); } } } - void log_entry(int y, int index) { - if (log[index].message == HEM_MIDI_NOTE_ON) { + void PrintLogEntry(int y, int index) { + MIDILogEntry &log_entry_ = frame.MIDIState.log[index]; + + switch ( log_entry_.message ) { + case HEM_MIDI_NOTE_ON: gfxBitmap(1, y, 8, NOTE_ICON); - gfxPrint(10, y, midi_note_numbers[log[index].data1]); - gfxPrint(40, y, log[index].data2); - } + gfxPrint(10, y, midi_note_numbers[log_entry_.data1]); + gfxPrint(40, y, log_entry_.data2); + break; - if (log[index].message == HEM_MIDI_NOTE_OFF) { + case HEM_MIDI_NOTE_OFF: gfxPrint(1, y, "-"); - gfxPrint(10, y, midi_note_numbers[log[index].data1]); - } + gfxPrint(10, y, midi_note_numbers[log_entry_.data1]); + break; - if (log[index].message == HEM_MIDI_CC) { + case HEM_MIDI_CC: gfxBitmap(1, y, 8, MOD_ICON); - gfxPrint(10, y, log[index].data2); - } + gfxPrint(10, y, log_entry_.data2); + break; - if (log[index].message == HEM_MIDI_AFTERTOUCH) { + case HEM_MIDI_AFTERTOUCH: gfxBitmap(1, y, 8, AFTERTOUCH_ICON); - gfxPrint(10, y, log[index].data2); - } + gfxPrint(10, y, log_entry_.data1); + break; - if (log[index].message == HEM_MIDI_PITCHBEND) { - int data = (log[index].data2 << 7) + log[index].data1 - 8192; + case HEM_MIDI_PITCHBEND: { + int data = (log_entry_.data2 << 7) + log_entry_.data1 - 8192; gfxBitmap(1, y, 8, BEND_ICON); gfxPrint(10, y, data); + break; + } + + default: + gfxPrint(1, y, "?"); + gfxPrint(10, y, log_entry_.data1); + gfxPrint(" "); + gfxPrint(log_entry_.data2); + break; } } }; diff --git a/software/o_c_REV/HEM_hMIDIOut.ino b/software/o_c_REV/HEM_hMIDIOut.ino index 01be880b3..a10e76e47 100644 --- a/software/o_c_REV/HEM_hMIDIOut.ino +++ b/software/o_c_REV/HEM_hMIDIOut.ino @@ -42,8 +42,6 @@ public: legato = 1; log_index = 0; - const char * fn_name_list[] = {"Mod", "Aft", "Bend", "Veloc"}; - for (int i = 0; i < 4; i++) fn_name[i] = fn_name_list[i]; } void Controller() { @@ -130,21 +128,29 @@ public: } void View() { - gfxHeader(applet_name()); DrawMonitor(); if (cursor == 4) DrawLog(); else DrawSelector(); } void OnButtonPress() { - if (++cursor > 4) cursor = 0; - ResetCursor(); + if (cursor == 3 && !EditMode()) { // special case to toggle legato + legato = 1 - legato; + ResetCursor(); + return; + } + + CursorAction(cursor, 4); } void OnEncoderMove(int direction) { - if (cursor == 0) channel = constrain(channel += direction, 0, 15); - if (cursor == 1) transpose = constrain(transpose += direction, -24, 24); - if (cursor == 2) function = constrain(function += direction, 0, 3); + if (!EditMode()) { + MoveCursor(cursor, direction, 4); + return; + } + if (cursor == 0) channel = constrain(channel + direction, 0, 15); + if (cursor == 1) transpose = constrain(transpose + direction, -24, 24); + if (cursor == 2) function = constrain(function + direction, 0, 3); if (cursor == 3) legato = direction > 0 ? 1 : 0; ResetCursor(); } @@ -189,7 +195,7 @@ private: bool legato_on; // The note handler may currently respond to legato note changes int last_tick; // Most recent MIDI message sent int adc_lag_countdown; - const char* fn_name[4]; + const char* fn_name[4] = {"Mod", "Aft", "Bend", "Veloc"}; // Logging MIDILogEntry log[7]; diff --git a/software/o_c_REV/HSApplication.h b/software/o_c_REV/HSApplication.h index 2150d8aa6..73631804c 100644 --- a/software/o_c_REV/HSApplication.h +++ b/software/o_c_REV/HSApplication.h @@ -25,6 +25,8 @@ * for consistency in development, or ease of porting apps or applets in either direction. */ +#pragma once + #ifndef int2simfloat #define int2simfloat(x) (x << 14) #define simfloat2int(x) (x >> 14) @@ -32,21 +34,23 @@ typedef int32_t simfloat; #endif #include "HSicons.h" - -#ifndef HSAPPLICATION_H_ -#define HSAPPLICATION_H_ +#include "HSClockManager.h" +#include "HemisphereApplet.h" +#include "HSUtils.h" #define HSAPPLICATION_CURSOR_TICKS 12000 #define HSAPPLICATION_5V 7680 #define HSAPPLICATION_3V 4608 #define HSAPPLICATION_CHANGE_THRESHOLD 32 -#ifdef BUCHLA_4U -#define PULSE_VOLTAGE 8 +#if defined(BUCHLA_4U) || defined(VOR) +#define HSAPP_PULSE_VOLTAGE 8 #else -#define PULSE_VOLTAGE 5 +#define HSAPP_PULSE_VOLTAGE 5 #endif +using namespace HS; + class HSApplication { public: virtual void Start(); @@ -55,32 +59,27 @@ class HSApplication { virtual void Resume(); void BaseController() { - for (uint8_t ch = 0; ch < 4; ch++) - { - // Set ADC input values - inputs[ch] = OC::ADC::raw_pitch_value((ADC_CHANNEL)ch); - if (abs(inputs[ch] - last_cv[ch]) > HSAPPLICATION_CHANGE_THRESHOLD) { - changed_cv[ch] = 1; - last_cv[ch] = inputs[ch]; - } else changed_cv[ch] = 0; - - if (clock_countdown[ch] > 0) { - if (--clock_countdown[ch] == 0) Out(ch, 0); - } - } + // Load the IO frame from CV inputs + HS::frame.Load(); // Cursor countdowns. See CursorBlink(), ResetCursor(), gfxCursor() if (--cursor_countdown < -HSAPPLICATION_CURSOR_TICKS) cursor_countdown = HSAPPLICATION_CURSOR_TICKS; Controller(); + + // set outputs from IO frame + HS::frame.Send(); } void BaseStart() { // Initialize some things for startup for (uint8_t ch = 0; ch < 4; ch++) { - clock_countdown[ch] = 0; - adc_lag_countdown[ch] = 0; + frame.clock_countdown[ch] = 0; + frame.inputs[ch] = 0; + frame.outputs[ch] = 0; + frame.outputs_smooth[ch] = 0; + frame.adc_lag_countdown[ch] = 0; } cursor_countdown = HSAPPLICATION_CURSOR_TICKS; @@ -92,21 +91,42 @@ class HSApplication { last_view_tick = OC::CORE::ticks; } - int Proportion(int numerator, int denominator, int max_value) { - simfloat proportion = int2simfloat((int32_t)numerator) / (int32_t)denominator; - int scaled = simfloat2int(proportion * max_value); - return scaled; + // general screensaver view, visualizing inputs and outputs + void BaseScreensaver(bool notenames = 0) { + gfxDottedLine(0, 32, 127, 32, 3); // horizontal baseline + for (int ch = 0; ch < 4; ++ch) + { + if (notenames) { + // approximate notes being output + gfxPrint(8 + 32*ch, 55, midi_note_numbers[MIDIQuantizer::NoteNumber(HS::frame.outputs[ch])] ); + } + + // trigger/gate indicators + if (HS::frame.gate_high[ch]) gfxIcon(11 + 32*ch, 0, CLOCK_ICON); + + // input + int height = ProportionCV(HS::frame.inputs[ch], 32); + int y = constrain(32 - height, 0, 32); + gfxFrame(3 + (32 * ch), y, 6, abs(height)); + + // output + height = ProportionCV(HS::frame.outputs[ch], 32); + y = constrain(32 - height, 0, 32); + gfxInvert(11 + (32 * ch), y, 12, abs(height)); + + gfxDottedLine(32 * ch, 0, 32*ch, 63, 3); // vertical divider, left side + } + gfxDottedLine(127, 0, 127, 63, 3); // vertical line, right side } //////////////// Hemisphere-like IO methods //////////////////////////////////////////////////////////////////////////////// void Out(int ch, int value, int octave = 0) { - OC::DAC::set_pitch((DAC_CHANNEL)ch, value, octave); - outputs[ch] = value + (octave * (12 << 7)); + frame.Out( (DAC_CHANNEL)(ch), value + (octave * (12 << 7))); } int In(int ch) { - return inputs[ch]; + return frame.inputs[ch]; } // Apply small center detent to input, so it reads zero before a threshold @@ -114,45 +134,53 @@ class HSApplication { return (In(ch) > 64 || In(ch) < -64) ? In(ch) : 0; } + // Standard bi-polar CV modulation scenario + template + void Modulate(T ¶m, const int ch, const int min = 0, const int max = 255) { + int cv = DetentedIn(ch); + param = constrain(param + Proportion(cv, HEMISPHERE_MAX_INPUT_CV, max), min, max); + } + bool Changed(int ch) { - return changed_cv[ch]; + return frame.changed_cv[ch]; } bool Gate(int ch) { - bool high = 0; - if (ch == 0) high = OC::DigitalInputs::read_immediate(); - if (ch == 1) high = OC::DigitalInputs::read_immediate(); - if (ch == 2) high = OC::DigitalInputs::read_immediate(); - if (ch == 3) high = OC::DigitalInputs::read_immediate(); - return high; + return frame.gate_high[ch]; } void GateOut(int ch, bool high) { - Out(ch, 0, (high ? PULSE_VOLTAGE : 0)); + Out(ch, 0, (high ? HSAPP_PULSE_VOLTAGE : 0)); } bool Clock(int ch) { bool clocked = 0; - if (ch == 0) clocked = OC::DigitalInputs::clocked(); - if (ch == 1) clocked = OC::DigitalInputs::clocked(); - if (ch == 2) clocked = OC::DigitalInputs::clocked(); - if (ch == 3) clocked = OC::DigitalInputs::clocked(); + ClockManager *clock_m = clock_m->get(); + + if (clock_m->IsRunning() && clock_m->GetMultiply(ch) != 0) + clocked = clock_m->Tock(ch); + else { + clocked = frame.clocked[ch]; + } + + // manual triggers + clocked = clocked || clock_m->Beep(ch); + if (clocked) { - cycle_ticks[ch] = OC::CORE::ticks - last_clock[ch]; - last_clock[ch] = OC::CORE::ticks; + frame.cycle_ticks[ch] = OC::CORE::ticks - frame.last_clock[ch]; + frame.last_clock[ch] = OC::CORE::ticks; } return clocked; } void ClockOut(int ch, int ticks = 100) { - clock_countdown[ch] = ticks; - Out(ch, 0, PULSE_VOLTAGE); + frame.ClockOut( (DAC_CHANNEL)ch, ticks ); } // Buffered I/O functions for use in Views - int ViewIn(int ch) {return inputs[ch];} - int ViewOut(int ch) {return outputs[ch];} - int ClockCycleTicks(int ch) {return cycle_ticks[ch];} + int ViewIn(int ch) {return frame.inputs[ch];} + int ViewOut(int ch) {return frame.outputs[ch];} + int ClockCycleTicks(int ch) {return frame.cycle_ticks[ch];} /* ADC Lag: There is a small delay between when a digital input can be read and when an ADC can be * read. The ADC value lags behind a bit in time. So StartADCLag() and EndADCLag() are used to @@ -165,95 +193,12 @@ class HSApplication { * // etc... * } */ - void StartADCLag(int ch) {adc_lag_countdown[ch] = 96;} - bool EndOfADCLag(int ch) {return (--adc_lag_countdown[ch] == 0);} + void StartADCLag(int ch) {frame.adc_lag_countdown[ch] = 96;} + bool EndOfADCLag(int ch) {return (--frame.adc_lag_countdown[ch] == 0);} - //////////////// Hemisphere-like graphics methods for easy porting - //////////////////////////////////////////////////////////////////////////////// void gfxCursor(int x, int y, int w) { if (CursorBlink()) gfxLine(x, y, x + w - 1, y); } - - void gfxPos(int x, int y) { - graphics.setPrintPos(x, y); - } - - void gfxPrint(int x, int y, const char *str) { - graphics.setPrintPos(x, y); - graphics.print(str); - } - - void gfxPrint(int x, int y, int num) { - graphics.setPrintPos(x, y); - graphics.print(num); - } - - void gfxPrint(int x_adv, int num) { // Print number with character padding - for (int c = 0; c < (x_adv / 6); c++) gfxPrint(" "); - gfxPrint(num); - } - - void gfxPrint(const char *str) { - graphics.print(str); - } - - void gfxPrint(int num) { - graphics.print(num); - } - - void gfxPixel(int x, int y) { - graphics.setPixel(x, y); - } - - void gfxFrame(int x, int y, int w, int h) { - graphics.drawFrame(x, y, w, h); - } - - void gfxRect(int x, int y, int w, int h) { - graphics.drawRect(x, y, w, h); - } - - void gfxInvert(int x, int y, int w, int h) { - graphics.invertRect(x, y, w, h); - } - - void gfxLine(int x, int y, int x2, int y2) { - graphics.drawLine(x, y, x2, y2); - } - - void gfxDottedLine(int x, int y, int x2, int y2, uint8_t p = 2) { -#ifdef HS_GFX_MOD - graphics.drawLine(x, y, x2, y2, p); -#else - graphics.drawLine(x, y, x2, y2); -#endif - } - - void gfxCircle(int x, int y, int r) { - graphics.drawCircle(x, y, r); - } - - void gfxBitmap(int x, int y, int w, const uint8_t *data) { - graphics.drawBitmap8(x, y, w, data); - } - - // Like gfxBitmap, but always 8x8 - void gfxIcon(int x, int y, const uint8_t *data) { - gfxBitmap(x, y, 8, data); - } - - - uint8_t pad(int range, int number) { - uint8_t padding = 0; - while (range > 1) - { - if (abs(number) < range) padding += 6; - range = range / 10; - } - if (number < 0 && padding > 0) padding -= 6; // Compensate for minus sign - return padding; - } - void gfxHeader(const char *str) { gfxPrint(1, 2, str); gfxLine(0, 10, 127, 10); @@ -265,22 +210,11 @@ class HSApplication { bool CursorBlink() { return (cursor_countdown > 0); } - void ResetCursor() { cursor_countdown = HSAPPLICATION_CURSOR_TICKS; } private: - int clock_countdown[4]; // For clock output timing - int adc_lag_countdown[4]; // Lag countdown for each input channel int cursor_countdown; // Timer for cursor blinkin' uint32_t last_view_tick; // Time since the last view, for activating screen blanking - int inputs[4]; // Last ADC values - int outputs[4]; // Last DAC values; inputs[] and outputs[] are used to allow access to values in Views - bool changed_cv[4]; // Has the input changed by more than 1/8 semitone since the last read? - int last_cv[4]; // For change detection - uint32_t last_clock[4]; // Tick number of the last clock observed by the child class - uint32_t cycle_ticks[4]; // Number of ticks between last two clocks }; - -#endif /* HSAPPLICATION_H_ */ diff --git a/software/o_c_REV/HSClockManager.h b/software/o_c_REV/HSClockManager.h index 8b7734740..69d1e6b56 100644 --- a/software/o_c_REV/HSClockManager.h +++ b/software/o_c_REV/HSClockManager.h @@ -24,34 +24,48 @@ #ifndef CLOCK_MANAGER_H #define CLOCK_MANAGER_H -const uint16_t CLOCK_TEMPO_MIN = 10; -const uint16_t CLOCK_TEMPO_MAX = 300; +static constexpr uint16_t CLOCK_TEMPO_MIN = 1; +static constexpr uint16_t CLOCK_TEMPO_MAX = 300; +static constexpr uint32_t CLOCK_TICKS_MIN = 1000000 / CLOCK_TEMPO_MAX; +static constexpr uint32_t CLOCK_TICKS_MAX = 1000000 / CLOCK_TEMPO_MIN; + +constexpr int MIDI_OUT_PPQN = 24; +constexpr int CLOCK_MAX_MULTIPLE = 24; +constexpr int CLOCK_MIN_MULTIPLE = -31; // becomes /32 class ClockManager { static ClockManager *instance; - uint32_t ticks_per_tock; // Based on the selected tempo in BPM - uint32_t last_tock_tick; // The tick of the most recent tock - uint32_t last_tock_check; // To avoid checking the tock more than once per tick - bool tock; // The most recent tock value + + enum ClockOutput { + LEFT_CLOCK1, + LEFT_CLOCK2, + RIGHT_CLOCK1, + RIGHT_CLOCK2, + MIDI_CLOCK, + NR_OF_CLOCKS + }; + uint16_t tempo; // The set tempo, for display somewhere else - bool running; // Specifies whether the clock is running for interprocess communication - bool paused; // Specifies whethr the clock is paused - int8_t tocks_per_beat; // Multiplier - bool cycle; // Alternates for each tock, for display purposes - byte count; // Multiple counter - bool forwarded; // Master clock forwarding is enabled when true + uint32_t ticks_per_beat; // Based on the selected tempo in BPM + bool running = 0; // Specifies whether the clock is running for interprocess communication + bool paused = 0; // Specifies whethr the clock is paused + bool midi_out_enabled = 1; + + bool tickno = 0; + uint32_t clock_tick[2] = {0,0}; // previous ticks when a physical clock was received on DIGITAL 1 + uint32_t beat_tick = 0; // The tick to count from + bool tock[NR_OF_CLOCKS] = {0,0,0,0,0}; // The current tock value + int16_t tocks_per_beat[NR_OF_CLOCKS] = {4,0, 8,0, MIDI_OUT_PPQN}; // Multiplier + int count[NR_OF_CLOCKS] = {0,0,0,0,0}; // Multiple counter, 0 is a special case when first starting the clock + int8_t shuffle = 0; // 0 to 100 + + int clock_ppqn = 4; // external clock multiple + bool cycle = 0; // Alternates for each tock, for display purposes + + bool boop[4] = {0,0,0,0}; // Manual triggers ClockManager() { SetTempoBPM(120); - SetMultiply(1); - running = 0; - paused = 0; - cycle = 0; - last_tock_tick = 0; - last_tock_check = 0; - count = 0; - tock = 0; - forwarded = 0; } public: @@ -60,9 +74,17 @@ class ClockManager { return instance; } - void SetMultiply(int8_t multiply) { - multiply = constrain(multiply, 1, 24); - tocks_per_beat = multiply; + void EnableMIDIOut() { midi_out_enabled = 1; } + void DisableMIDIOut() { midi_out_enabled = 0; } + + void SetMultiply(int multiply, int ch = 0) { + multiply = constrain(multiply, CLOCK_MIN_MULTIPLE, CLOCK_MAX_MULTIPLE); + tocks_per_beat[ch] = multiply; + } + + // adjusts the expected clock multiple for external clock pulses + void SetClockPPQN(int clkppqn) { + clock_ppqn = constrain(clkppqn, 0, 24); } /* Set ticks per tock, based on one million ticks per minute divided by beats per minute. @@ -71,63 +93,182 @@ class ClockManager { */ void SetTempoBPM(uint16_t bpm) { bpm = constrain(bpm, CLOCK_TEMPO_MIN, CLOCK_TEMPO_MAX); - ticks_per_tock = 1000000 / bpm; + ticks_per_beat = 1000000 / bpm; tempo = bpm; } + + void SetTempoFromTaps(uint32_t *taps, int count) { + uint32_t total = 0; + for (int i = 0; i < count; ++i) { + total += taps[i]; + } + + // update the tempo + uint32_t clock_diff = total / count; + ticks_per_beat = constrain(clock_diff, CLOCK_TICKS_MIN, CLOCK_TICKS_MAX); // time since last clock is new tempo + tempo = 1000000 / ticks_per_beat; // imprecise, for display purposes + } - int8_t GetMultiply() {return tocks_per_beat;} + int GetMultiply(int ch = 0) {return tocks_per_beat[ch];} + int GetClockPPQN() { return clock_ppqn; } + + void SetShuffle(int8_t sh_) { shuffle = constrain(sh_, 0, 99); } + int8_t GetShuffle() { return shuffle; } /* Gets the current tempo. This can be used between client processes, like two different * hemispheres. */ uint16_t GetTempo() {return tempo;} - void Reset() { - last_tock_tick = OC::CORE::ticks; - count = 0; + // Reset - Resync multipliers, optionally skipping the first tock + void Reset(bool count_skip = 0) { + beat_tick = OC::CORE::ticks; + if (0 == count_skip) { + clock_tick[0] = 0; + clock_tick[1] = 0; + } + for (int ch = 0; ch < NR_OF_CLOCKS; ch++) { + if (tocks_per_beat[ch] > 0 || 0 == count_skip) count[ch] = count_skip; + } cycle = 1 - cycle; } - void Start() { - forwarded = 0; + // Nudge - Used to align the internal clock with incoming clock pulses + // The rationale is that it's better to be short by 1 than to overshoot by 1 + void Nudge(int diff) { + if (diff > 0) diff--; + if (diff < 0) diff++; + beat_tick += diff; + } + + // call this on every tick when clock is running, before all Controllers + void SyncTrig(bool clocked, bool hard_reset = false) { + //if (!IsRunning()) return; + if (hard_reset) Reset(); + + const uint32_t now = OC::CORE::ticks; + + // Reset only when all multipliers have been met + bool reset = 1; + + // count and calculate Tocks + for (int ch = 0; ch < NR_OF_CLOCKS; ch++) { + if (tocks_per_beat[ch] == 0) { // disabled + tock[ch] = 0; continue; + } + + if (tocks_per_beat[ch] > 0) { // multiply + uint32_t next_tock_tick = beat_tick + count[ch]*ticks_per_beat / static_cast(tocks_per_beat[ch]); + if (shuffle && MIDI_CLOCK != ch && count[ch] % 2 == 1 && count[ch] < tocks_per_beat[ch]) + next_tock_tick += shuffle * ticks_per_beat / 100 / static_cast(tocks_per_beat[ch]); + + tock[ch] = now >= next_tock_tick; + if (tock[ch]) ++count[ch]; // increment multiplier counter + + reset = reset && (count[ch] > tocks_per_beat[ch]); // multiplier has been exceeded + } else { // division: -1 becomes /2, -2 becomes /3, etc. + int div = 1 - tocks_per_beat[ch]; + uint32_t next_beat = beat_tick + (count[ch] ? ticks_per_beat : 0); + bool beat_exceeded = (now >= next_beat); + if (beat_exceeded) { + ++count[ch]; + tock[ch] = (count[ch] % div) == 1; + } + else + tock[ch] = 0; + + // resync on every beat + reset = reset && beat_exceeded; + if (tock[ch]) count[ch] = 1; + } + + } + if (reset) Reset(1); // skip the one we're already on + + // handle syncing to physical clocks + if (clocked && clock_tick[tickno] && clock_ppqn) { + + uint32_t clock_diff = now - clock_tick[tickno]; + + // too slow, reset clock tracking + if (clock_ppqn * clock_diff > CLOCK_TICKS_MAX) { + clock_tick[0] = 0; + clock_tick[1] = 0; + } + + // if there are two previous clock ticks, update tempo and sync + if (clock_tick[1-tickno] && clock_diff) { + uint32_t avg_diff = (clock_diff + (clock_tick[tickno] - clock_tick[1-tickno])) / 2; + + // update the tempo + ticks_per_beat = constrain(clock_ppqn * avg_diff, CLOCK_TICKS_MIN, CLOCK_TICKS_MAX); + tempo = 1000000 / ticks_per_beat; // imprecise, for display purposes + + int ticks_per_clock = ticks_per_beat / clock_ppqn; // rounded down + + // time since last beat + int tick_offset = now - beat_tick; + + // too long ago? time til next beat + if (tick_offset > ticks_per_clock / 2) tick_offset -= ticks_per_beat; + + // within half a clock pulse of the nearest beat AND significantly large + if (abs(tick_offset) < ticks_per_clock / 2 && abs(tick_offset) > 4) + Nudge(tick_offset); // nudge the beat towards us + + } + } + // clock has been physically ticked + if (clocked) { + tickno = 1 - tickno; + clock_tick[tickno] = now; + } + } + + void Start(bool p = 0) { + Reset(); running = 1; - Unpause(); + paused = p; + if (!p && midi_out_enabled) usbMIDI.sendRealTime(usbMIDI.Start); } void Stop() { running = 0; - Unpause(); + paused = 0; + if (midi_out_enabled) usbMIDI.sendRealTime(usbMIDI.Stop); } void Pause() {paused = 1;} - void Unpause() {paused = 0;} - - void ToggleForwarding() {forwarded = 1 - forwarded;} - bool IsRunning() {return (running && !paused);} bool IsPaused() {return paused;} - bool IsForwarded() {return forwarded;} - - /* Returns true if the clock should fire on this tick, based on the current tempo */ - bool Tock() { - uint32_t now = OC::CORE::ticks; - if (now != last_tock_check) { - last_tock_check = now; - if (now >= (last_tock_tick + (ticks_per_tock / static_cast(tocks_per_beat)))) { - tock = 1; - last_tock_tick = now; - if (++count >= tocks_per_beat) Reset(); - } else tock = 0; + // beep boop + void Boop(int ch = 0) { + boop[ch] = true; + } + bool Beep(int ch = 0) { + if (boop[ch]) { + boop[ch] = false; + return true; } - return tock; + return false; + } + + /* Returns true if the clock should fire on this tick, based on the current tempo and multiplier */ + bool Tock(int ch = 0) { + return tock[ch]; + } + + // Returns true if MIDI Clock should be sent on this tick + bool MIDITock() { + return midi_out_enabled && Tock(MIDI_CLOCK); } - bool EndOfBeat() {return count == 0;} + bool EndOfBeat(int ch = 0) {return count[ch] == 1;} - bool Cycle() {return cycle;} + bool Cycle(int ch = 0) {return cycle;} }; ClockManager *ClockManager::instance = 0; diff --git a/software/o_c_REV/HSIOFrame.h b/software/o_c_REV/HSIOFrame.h new file mode 100644 index 000000000..93eaa6639 --- /dev/null +++ b/software/o_c_REV/HSIOFrame.h @@ -0,0 +1,240 @@ +/* Copyright (c) 2023 Nicholas J. Michalek + * + * IOFrame & friends + * attempts at making applet I/O more flexible and portable + * + * Some processing logic adapted from the MIDI In applet + * + */ + +namespace HS { + +braids::Quantizer quantizer[4]; // global shared quantizers +int quant_scale[4]; +int root_note[4]; + +bool auto_save_enabled = false; +int trigger_mapping[] = { 1, 2, 3, 4 }; +uint8_t trig_length = 10; // in ms, multiplier for HEMISPHERE_CLOCK_TICKS +uint8_t screensaver_mode = 2; // 0 = blank, 1 = Meters, 2 = Zaps + +typedef struct MIDILogEntry { + int message; + int data1; + int data2; +} MIDILogEntry; + +// shared IO Frame, updated every tick +// this will allow chaining applets together, multiple stages of processing +typedef struct IOFrame { + bool clocked[4]; + bool gate_high[4]; + int inputs[4]; + int outputs[4]; + int outputs_smooth[4]; + int clock_countdown[4]; + int adc_lag_countdown[4]; // Time between a clock event and an ADC read event + uint32_t last_clock[4]; // Tick number of the last clock observed by the child class + uint32_t cycle_ticks[4]; // Number of ticks between last two clocks + bool changed_cv[4]; // Has the input changed by more than 1/8 semitone since the last read? + int last_cv[4]; // For change detection + + /* MIDI message queue/cache */ + struct { + int channel[4]; // MIDI channel number + int function[4]; // Function for each channel + int function_cc[4]; // CC# for each channel + + // Output values and ClockOut triggers, handled by MIDIIn applet + int outputs[4]; + bool trigout_q[4]; + + int8_t notes_on[16]; // attempts to track how many notes are on, per MIDI channel + int last_msg_tick; // Tick of last received message + uint8_t clock_count; // MIDI clock counter (24ppqn) + + // Clock/Start/Stop are handled by ClockSetup applet + bool clock_q; + bool start_q; + bool stop_q; + + // Logging + MIDILogEntry log[7]; + int log_index; + + void UpdateLog(int message, int data1, int data2) { + log[log_index++] = {message, data1, data2}; + if (log_index == 7) { + for (int i = 0; i < 6; i++) + { + memcpy(&log[i], &log[i+1], sizeof(log[i+1])); + } + log_index--; + } + last_msg_tick = OC::CORE::ticks; + } + void ProcessMIDIMsg(const int midi_chan, const int message, const int data1, const int data2) { + switch (message) { + case usbMIDI.Clock: + if (++clock_count == 1) { + clock_q = 1; + ForAllChannels(ch) + { + if (function[ch] == HEM_MIDI_CLOCK_OUT) { + trigout_q[ch] = 1; + } + } + } + if (clock_count == HEM_MIDI_CLOCK_DIVISOR) clock_count = 0; + return; + break; + + case usbMIDI.Continue: // treat Continue like Start + case usbMIDI.Start: + start_q = 1; + ForAllChannels(ch) + { + if (function[ch] == HEM_MIDI_START_OUT) { + trigout_q[ch] = 1; + } + } + + //UpdateLog(message, data1, data2); + return; + break; + + case usbMIDI.SystemReset: + case usbMIDI.Stop: + stop_q = 1; + // a way to reset stuck notes + for (int i = 0; i < 16; ++i) notes_on[i] = 0; + return; + break; + + case usbMIDI.NoteOn: + ++notes_on[midi_chan - 1]; + break; + case usbMIDI.NoteOff: + --notes_on[midi_chan - 1]; + break; + + } + + ForAllChannels(ch) { + if (function[ch] == HEM_MIDI_NOOP) continue; + + // skip unwanted MIDI Channels + if (midi_chan - 1 != channel[ch]) continue; + + bool log_this = false; + + switch (message) { + case usbMIDI.NoteOn: + + // Should this message go out on this channel? + if (function[ch] == HEM_MIDI_NOTE_OUT) + outputs[ch] = MIDIQuantizer::CV(data1); + + if (function[ch] == HEM_MIDI_TRIG_OUT) + trigout_q[ch] = 1; + + if (function[ch] == HEM_MIDI_GATE_OUT) + outputs[ch] = PULSE_VOLTAGE * (12 << 7); + + if (function[ch] == HEM_MIDI_VEL_OUT) + outputs[ch] = Proportion(data2, 127, HEMISPHERE_MAX_CV); + + log_this = 1; // Log all MIDI notes. Other stuff is conditional. + break; + + case usbMIDI.NoteOff: + // turn gate off only when all notes are off + if (notes_on[midi_chan - 1] <= 0) + { + notes_on[midi_chan - 1] = 0; // just in case it becomes negative... + if (function[ch] == HEM_MIDI_GATE_OUT) { + outputs[ch] = 0; + log_this = 1; + } + } + break; + + case usbMIDI.ControlChange: // Modulation wheel or other CC + if (function[ch] == HEM_MIDI_CC_OUT) { + if (function_cc[ch] < 0) function_cc[ch] = data1; + + if (function_cc[ch] == data1) { + outputs[ch] = Proportion(data2, 127, HEMISPHERE_MAX_CV); + log_this = 1; + } + } + break; + + // TODO: consider adding support for AfterTouchPoly + case usbMIDI.AfterTouchChannel: + if (function[ch] == HEM_MIDI_AT_OUT) { + outputs[ch] = Proportion(data1, 127, HEMISPHERE_MAX_CV); + log_this = 1; + } + break; + + case usbMIDI.PitchBend: + if (function[ch] == HEM_MIDI_PB_OUT) { + int data = (data2 << 7) + data1 - 8192; + outputs[ch] = Proportion(data, 8192, HEMISPHERE_3V_CV); + log_this = 1; + } + break; + + } + + if (log_this) UpdateLog(message, data1, data2); + } + } + + } MIDIState; + + // --- Soft IO --- + void Out(DAC_CHANNEL channel, int value) { + outputs[channel] = value; + } + void ClockOut(DAC_CHANNEL ch, const int pulselength = HEMISPHERE_CLOCK_TICKS * HS::trig_length) { + clock_countdown[ch] = pulselength; + outputs[ch] = PULSE_VOLTAGE * (12 << 7); + } + + // TODO: Hardware IO should be extracted + // --- Hard IO --- + void Load() { + clocked[0] = OC::DigitalInputs::clocked(); + clocked[1] = OC::DigitalInputs::clocked(); + clocked[2] = OC::DigitalInputs::clocked(); + clocked[3] = OC::DigitalInputs::clocked(); + gate_high[0] = OC::DigitalInputs::read_immediate(); + gate_high[1] = OC::DigitalInputs::read_immediate(); + gate_high[2] = OC::DigitalInputs::read_immediate(); + gate_high[3] = OC::DigitalInputs::read_immediate(); + + ForAllChannels(i) { + // Set CV inputs + inputs[i] = OC::ADC::raw_pitch_value(ADC_CHANNEL(i)); + if (abs(inputs[i] - last_cv[i]) > HEMISPHERE_CHANGE_THRESHOLD) { + changed_cv[i] = 1; + last_cv[i] = inputs[i]; + } else changed_cv[i] = 0; + + // Handle clock pulse timing + if (clock_countdown[i] > 0) { + if (--clock_countdown[i] == 0) outputs[i] = 0; + } + } + } + void Send() { + ForAllChannels(i) { + OC::DAC::set_pitch(DAC_CHANNEL(i), outputs[i], 0); + } + } + +} IOFrame; + +} // namespace HS diff --git a/software/o_c_REV/HSMIDI.h b/software/o_c_REV/HSMIDI.h index c96643329..2077bf8f1 100644 --- a/software/o_c_REV/HSMIDI.h +++ b/software/o_c_REV/HSMIDI.h @@ -32,13 +32,19 @@ // Teensyduino USB MIDI Library message numbers // See https://www.pjrc.com/teensy/td_midi.html -const uint8_t MIDI_MSG_NOTE_ON = 1; -const uint8_t MIDI_MSG_NOTE_OFF = 0; -const uint8_t MIDI_MSG_MIDI_CC = 3; -const uint8_t MIDI_MSG_AFTERTOUCH = 5; -const uint8_t MIDI_MSG_PITCHBEND = 6; -const uint8_t MIDI_MSG_SYSEX = 7; -const uint8_t MIDI_MSG_REALTIME = 8; + +#define HEM_MIDI_NOTE_ON usbMIDI.NoteOn +#define HEM_MIDI_NOTE_OFF usbMIDI.NoteOff +#define HEM_MIDI_CC usbMIDI.ControlChange +#define HEM_MIDI_AFTERTOUCH usbMIDI.AfterTouchChannel +#define HEM_MIDI_PITCHBEND usbMIDI.PitchBend +#define HEM_MIDI_SYSEX usbMIDI.SystemExclusive + +#define HEM_MIDI_CLOCK usbMIDI.Clock +#define HEM_MIDI_START usbMIDI.Start +#define HEM_MIDI_STOP usbMIDI.Stop + +#define HEM_MIDI_CLOCK_DIVISOR 12 const char* const midi_note_numbers[128] = { "C-1","C#-1","D-1","D#-1","E-1","F-1","F#-1","G-1","G#-1","A-1","A#-1","B-1", @@ -59,6 +65,23 @@ const char* const midi_channels[17] = { " 9", "10", "11", "12", "13", "14", "15", "16" }; +// The functions available for each output +enum MIDIFunctions { + HEM_MIDI_NOOP = 0, + HEM_MIDI_NOTE_OUT, + HEM_MIDI_TRIG_OUT, + HEM_MIDI_GATE_OUT, + HEM_MIDI_VEL_OUT, + HEM_MIDI_CC_OUT, + HEM_MIDI_AT_OUT, + HEM_MIDI_PB_OUT, + HEM_MIDI_CLOCK_OUT, + HEM_MIDI_START_OUT, + + HEM_MIDI_MAX_FUNCTION = HEM_MIDI_START_OUT +}; +const char* const midi_fn_name[HEM_MIDI_MAX_FUNCTION + 1] = {"None", "Note#", "Trig", "Gate", "Veloc", "CC#", "Aft", "Bend", "Clock", "Start"}; + /* Hemisphere Suite Data Packing * @@ -228,7 +251,7 @@ class SystemExclusiveHandler { bool ListenForSysEx() { bool heard_sysex = 0; if (usbMIDI.read()) { - if (usbMIDI.getType() == 7) { + if (usbMIDI.getType() == usbMIDI.SystemExclusive) { OnReceiveSysEx(); heard_sysex = 1; } diff --git a/software/o_c_REV/HSProbLoopLinker.h b/software/o_c_REV/HSProbLoopLinker.h index 30a6605a4..6c577735c 100644 --- a/software/o_c_REV/HSProbLoopLinker.h +++ b/software/o_c_REV/HSProbLoopLinker.h @@ -23,7 +23,7 @@ class ProbLoopLinker { static ProbLoopLinker *instance; - bool ready = false; + bool trig_q[2] = {0, 0}; uint8_t hemDiv; uint8_t hemMelo; uint32_t registered[2]; @@ -65,13 +65,13 @@ class ProbLoopLinker { && (t - registered[RIGHT_HEMISPHERE] < 160)); } - void Trigger() { - ready = true; + void Trigger(int ch) { + trig_q[ch] = true; } - bool Ready() { - if (IsLinked() && ready) { - ready = false; + bool TrigPop(int ch) { + if (IsLinked() && trig_q[ch]) { + trig_q[ch] = false; return true; } return false; @@ -97,7 +97,7 @@ class ProbLoopLinker { reseed = true; } - int ShouldReseed() { + bool ShouldReseed() { if (reseed) { reseed = false; return true; @@ -109,4 +109,4 @@ class ProbLoopLinker { ProbLoopLinker *ProbLoopLinker::instance = 0; -#endif \ No newline at end of file +#endif diff --git a/software/o_c_REV/HSRelabiManager.h b/software/o_c_REV/HSRelabiManager.h new file mode 100644 index 000000000..4005d27ba --- /dev/null +++ b/software/o_c_REV/HSRelabiManager.h @@ -0,0 +1,102 @@ +// Copyright (c) 2024, Samuel Burt +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + + + +class RelabiManager { + static RelabiManager *instance; + + int lfo1; + int lfo2; + int lfo3; + int lfo4; + bool gateStates[4]; + uint8_t leftOn; + uint8_t rightOn; + bool linked; + uint32_t registered[2]; + uint32_t lastRegistered[2]; + uint8_t hemRelabi; + uint8_t linkedFreqMultiplier; + uint8_t linkedFreqDivider; + + + RelabiManager() { + leftOn = 0; + rightOn = 0; + linked = false; + registered[LEFT_HEMISPHERE] = 0; + registered[RIGHT_HEMISPHERE] = 0; + + // Initialize gate states + for (int i = 0; i < 4; i++) { + gateStates[i] = false; + } + } + +public: + + static RelabiManager *get() { + if (!instance) instance = new RelabiManager; + return instance; + } + + void RegisterRelabi(bool hemisphere) { + hemRelabi = hemisphere; + registered[hemisphere] = OC::CORE::ticks; + } + + bool IsLinked() { + uint32_t t = OC::CORE::ticks; + return ((t - registered[LEFT_HEMISPHERE] < 160) + && (t - registered[RIGHT_HEMISPHERE] < 160)); + } + + void WriteValues(int value1, int value2, int value3, int value4) { + // Update individual variables + lfo1 = value1; + lfo2 = value2; + lfo3 = value3; + lfo4 = value4; + } + + void ReadValues(int &value1, int &value2, int &value3, int &value4) const { + // Read values into the referenced variables + value1 = lfo1; + value2 = lfo2; + value3 = lfo3; + value4 = lfo4; + } + + void WriteGates(bool gates[4]) { + for (int i = 0; i < 4; i++) { + gateStates[i] = gates[i]; + } + } + + void ReadGates(bool gates[4]) const { + for (int i = 0; i < 4; i++) { + gates[i] = gateStates[i]; + } + } +}; + +RelabiManager *RelabiManager::instance = 0; + diff --git a/software/o_c_REV/HSRingBufferManager.h b/software/o_c_REV/HSRingBufferManager.h index 455f69ad7..7a4527f4a 100644 --- a/software/o_c_REV/HSRingBufferManager.h +++ b/software/o_c_REV/HSRingBufferManager.h @@ -18,86 +18,48 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -class RingBufferManager { - static RingBufferManager *instance; +struct RingBufferManager { int buffer[256]; - byte position = 0; - byte index = 0; - bool ready = 0; // Allows a second instance to know that a first instance has been clocked + uint8_t position = 0; + uint8_t index = 0; uint32_t registered[2]; - uint32_t last_advance_tick = 0; // To prevent double-advancing - - RingBufferManager() { - for (byte i = 0; i < 255; i++) buffer[i] = 0; - registered[LEFT_HEMISPHERE] = 0; - registered[RIGHT_HEMISPHERE] = 0; - } - -public: - static RingBufferManager *get() { - if (!instance) instance = new RingBufferManager; - return instance; - } - void Register(bool hemisphere) { registered[hemisphere] = OC::CORE::ticks; } - bool IsLinked(bool hemisphere) { - uint32_t t = OC::CORE::ticks; - return (hemisphere == RIGHT_HEMISPHERE - && (t - registered[LEFT_HEMISPHERE] < 160) - && (t - registered[RIGHT_HEMISPHERE] < 160)); - } - - // Allows a second instance to know that a first instance has been clocked - bool Ready(bool hemisphere) { - bool r = 0; - if (IsLinked(hemisphere)) { - r = ready; - ready = 0; - } - return r; + bool IsLinked() { + const uint32_t t = OC::CORE::ticks; + return ( (t - registered[LEFT_HEMISPHERE] < 160) + && (t - registered[RIGHT_HEMISPHERE] < 160)); } - void SetIndex(byte ix) {index = ix;} - - byte GetIndex() {return index;} + inline void SetIndex(uint8_t ix) {index = ix;} - byte GetPosition() {return position;} + inline uint8_t GetIndex() {return index;} - void WriteValueToBuffer(int cv, bool hemisphere) { - if (!IsLinked(hemisphere)) { - buffer[position] = cv; - } + inline void WriteValue(int cv) { + buffer[position] = cv; } - int ReadNextValue(byte output, bool hemisphere, int index_mod = 0) { - if (IsLinked(hemisphere)) output += 2; - byte ix = position - (index * output) - index_mod; + int ReadValue(uint8_t output, int index_mod = 0) { + uint8_t ix = position - (index * output) - index_mod; int cv = buffer[ix]; return cv; } - void Advance() { - if (OC::CORE::ticks > last_advance_tick) { - ++position; // No need to check range; 256 positions and an 8-bit counter - last_advance_tick = OC::CORE::ticks; - ready = 1; - } + inline void Advance() { + ++position; // No need to check range; 256 positions and an 8-bit counter } - int GetYAt(byte x, bool hemisphere) { - // If there are two instances, and this is the right hemisphere, show the - // second 128 bytes. Otherwise, just show the first 128. - byte offset = (IsLinked(hemisphere)) ? 64 : 0; - byte ix = (x + offset + position + 64); - int cv = buffer[ix] + HEMISPHERE_MAX_CV; // Force this positive - int y = (((cv << 7) / (HEMISPHERE_MAX_CV * 2)) * 23) >> 7; + int GetYAt(const uint8_t &x, bool hemisphere) { + // If there are two instances, and this is the left hemisphere, start further back + uint8_t offset = (IsLinked() && hemisphere == LEFT_HEMISPHERE) ? -128 : -64; + uint8_t ix = (x + offset + position); + int cv = buffer[ix] + HEMISPHERE_MAX_INPUT_CV; // Force this positive + int y = (((cv << 7) / (HEMISPHERE_MAX_INPUT_CV * 2)) * 23) >> 7; y = constrain(y, 0, 23); return 23 - y; } -}; +} buffer_m; -RingBufferManager *RingBufferManager::instance = 0; diff --git a/software/o_c_REV/HSUtils.h b/software/o_c_REV/HSUtils.h new file mode 100644 index 000000000..58ab2b5f7 --- /dev/null +++ b/software/o_c_REV/HSUtils.h @@ -0,0 +1,153 @@ +#pragma once + +// misc. utility functions extracted from Hemisphere +// -NJM + +//////////////// Calculation methods +//////////////////////////////////////////////////////////////////////////////// + +/* Proportion method using simfloat, useful for calculating scaled values given + * a fractional value. + * + * Solves this: numerator ??? + * ----------- = ----------- + * denominator max + * + * For example, to convert a parameter with a range of 1 to 100 into value scaled + * to HEMISPHERE_MAX_CV, to be sent to the DAC: + * + * Out(ch, Proportion(value, 100, HEMISPHERE_MAX_CV)); + * + */ +int Proportion(int numerator, int denominator, int max_value) { + simfloat proportion = int2simfloat((int32_t)numerator) / (int32_t)denominator; + int scaled = simfloat2int(proportion * max_value); + return scaled; +} + +/* Proportion CV values into pixels for display purposes. + * + * Solves this: cv_value ??? + * ----------------- = ---------- + * HEMISPHERE_MAX_CV max_pixels + */ +int ProportionCV(int cv_value, int max_pixels) { + // TODO: variable scaling for VOR? + int prop = constrain(Proportion(cv_value, HEMISPHERE_MAX_INPUT_CV, max_pixels), -max_pixels, max_pixels); + return prop; +} + +// Specifies where data goes in flash storage for each selcted applet, and how big it is +typedef struct PackLocation { + size_t location; + size_t size; +} PackLocation; + +/* Add value to a 64-bit storage unit at the specified location */ +void Pack(uint64_t &data, PackLocation p, uint64_t value) { + data |= (value << p.location); +} + +/* Retrieve value from a 64-bit storage unit at the specified location and of the specified bit size */ +int Unpack(uint64_t data, PackLocation p) { + uint64_t mask = 1; + for (size_t i = 1; i < p.size; i++) mask |= (0x01 << i); + return (data >> p.location) & mask; +} + +//////////////// Hemisphere-like graphics methods for easy porting +//////////////////////////////////////////////////////////////////////////////// +void gfxPos(int x, int y) { + graphics.setPrintPos(x, y); +} + +void gfxPrint(int x, int y, const char *str) { + graphics.setPrintPos(x, y); + graphics.print(str); +} + +void gfxPrint(int x, int y, int num) { + graphics.setPrintPos(x, y); + graphics.print(num); +} + +void gfxPrint(const char *str) { + graphics.print(str); +} + +void gfxPrint(int num) { + graphics.print(num); +} + +void gfxPrint(int x_adv, int num) { // Print number with character padding + for (int c = 0; c < (x_adv / 6); c++) gfxPrint(" "); + gfxPrint(num); +} + +/* Convert CV value to voltage level and print to two decimal places */ +void gfxPrintVoltage(int cv) { + int v = (cv * 100) / (12 << 7); + bool neg = v < 0 ? 1 : 0; + if (v < 0) v = -v; + int wv = v / 100; // whole volts + int dv = v - (wv * 100); // decimal + gfxPrint(neg ? "-" : "+"); + gfxPrint(wv); + gfxPrint("."); + if (dv < 10) gfxPrint("0"); + gfxPrint(dv); + gfxPrint("V"); +} + +void gfxPixel(int x, int y) { + graphics.setPixel(x, y); +} + +void gfxFrame(int x, int y, int w, int h) { + graphics.drawFrame(x, y, w, h); +} + +void gfxRect(int x, int y, int w, int h) { + graphics.drawRect(x, y, w, h); +} + +void gfxInvert(int x, int y, int w, int h) { + graphics.invertRect(x, y, w, h); +} + +void gfxLine(int x, int y, int x2, int y2) { + graphics.drawLine(x, y, x2, y2); +} + +void gfxDottedLine(int x, int y, int x2, int y2, uint8_t p = 2) { +#ifdef HS_GFX_MOD + graphics.drawLine(x, y, x2, y2, p); +#else + graphics.drawLine(x, y, x2, y2); +#endif +} + +void gfxCircle(int x, int y, int r) { + graphics.drawCircle(x, y, r); +} + +void gfxBitmap(int x, int y, int w, const uint8_t *data) { + graphics.drawBitmap8(x, y, w, data); +} + +// Like gfxBitmap, but always 8x8 +void gfxIcon(int x, int y, const uint8_t *data) { + gfxBitmap(x, y, 8, data); +} + +uint8_t pad(int range, int number) { + uint8_t padding = 0; + while (range > 1) + { + if (abs(number) < range) padding += 6; + range = range / 10; + } + if (number < 0 && padding > 0) padding -= 6; // Compensate for minus sign + return padding; +} + diff --git a/software/o_c_REV/HSicons.h b/software/o_c_REV/HSicons.h index daff3fcbe..40cab47d1 100644 --- a/software/o_c_REV/HSicons.h +++ b/software/o_c_REV/HSicons.h @@ -1,34 +1,59 @@ // Icons! Made with http://beigemaze.com/bitmap8x8.html +// Retouched with https://bitca--goatama.repl.co/ (Column major MSB Bottom) + #ifndef HS_ICON_SET #define HS_ICON_SET +const uint8_t VOCT_ICON[8] = { 0x07, 0x08, 0x27, 0x10, 0x08, 0xe4, 0xa0, 0xe0 }; +const uint8_t GATE_ICON[8] = {0x40, 0x7e, 0x02, 0x02, 0x02, 0x7e, 0x40, 0x40}; +const uint8_t SLEW_ICON[8] = { 0x40, 0x7e, 0x02, 0x02, 0x0c, 0x30, 0x40, 0x40}; +const uint8_t LENGTH_ICON[8] = { 0x0a, 0x01, 0x09, 0x22, 0x48, 0x40, 0x28, 0x00 }; +//const uint8_t LENGTH_ICON[8] = { 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00 }; +const uint8_t RANGE_ICON[8] = { 0x42, 0x7e, 0x42, 0x00, 0x7e, 0x7e, 0x00, 0x78 }; +const uint8_t PULSES_ICON[8] = {0x08, 0x00, 0x1c, 0x1c, 0x1c, 0x00, 0x08, 0x00}; +const uint8_t OFFSET_ICON[8] = { 0x08, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x1c, 0x00 }; +const uint8_t ROTATE_ICON[8] = { 0x08, 0x00, 0x08, 0x00, 0x1c, 0x1c, 0x1c, 0x00}; +const uint8_t TOSS_ICON[8] = { 0x0c, 0x12, 0x52, 0x91, 0x91, 0x09, 0x49, 0x06}; + const uint8_t METER_ICON[8] = {0x00,0xff,0x00,0xfc,0x00,0xff,0x00,0xfc}; -const uint8_t CLOCK_ICON[8] = {0x9c,0xa2,0xc1,0xcf,0xc9,0xa2,0x9c,0x00}; +const uint8_t CLOCK_ICON[8] = { 0x3c, 0x42, 0x81, 0x8d, 0x91, 0x81, 0x42, 0x3c }; +const uint8_t ENV_ICON[8] = { 0x20, 0x18, 0x04, 0x08, 0x08, 0x10, 0x20, 0x20 }; const uint8_t MOD_ICON[8] = {0x30,0x08,0x04,0x08,0x10,0x20,0x10,0x0c}; -const uint8_t BEND_ICON[8] = {0x20,0x70,0x70,0x3f,0x20,0x14,0x0c,0x1c}; +const uint8_t BEND_ICON[8] = { 0x00, 0x30, 0x3e, 0x82, 0x70, 0x0e, 0x01, 0x00 }; const uint8_t AFTERTOUCH_ICON[8] = {0x00,0x00,0x20,0x42,0xf5,0x48,0x20,0x00}; const uint8_t MIDI_ICON[8] = {0x3c,0x42,0x91,0x45,0x45,0x91,0x42,0x3c}; -const uint8_t CV_ICON[8] = {0x1f,0x11,0x11,0x00,0x07,0x18,0x07,0x00}; -const uint8_t SCALE_ICON[8] = {0x81,0x7f,0x9f,0x81,0x7f,0x9f,0x81,0x7f}; -const uint8_t LOCK_ICON[8] = {0x00,0xf8,0xfe,0xf9,0x89,0xf9,0xfe,0xf8}; +const uint8_t CV_ICON[8] = { 0x00, 0x3e, 0x22, 0x00, 0x1e, 0x20, 0x1e, 0x00 }; +const uint8_t TR_ICON[8] = { 0x00, 0x02, 0x3e, 0x02, 0x00, 0x3e, 0x0a, 0x36 }; +const uint8_t SCALE_ICON[8] = { 0x00, 0x00, 0x0e, 0x8d, 0x61, 0x1e, 0x00, 0x00}; +const uint8_t LOCK_ICON[8] = {0xf8, 0x8e, 0x89, 0xa9, 0x89, 0x8e, 0xf8, 0x00}; +// old lock 0x00,0xf8,0xfe,0xf9,0x89,0xf9,0xfe,0xf8 const uint8_t FAVORITE_ICON[8] = {0x0e,0x11,0x21,0x42,0x42,0x21,0x11,0x0e}; -const uint8_t ROTATE_L_ICON[8] = {0x0c,0x1e,0x3f,0x0c,0x0c,0x08,0x70,0x00}; -const uint8_t ROTATE_R_ICON[8] = {0x00,0x70,0x08,0x0c,0x0c,0x3f,0x1e,0x0c}; +const uint8_t ROTATE_L_ICON[8] = { 0x0c, 0x1e, 0x3f, 0x0c, 0x0c, 0x1c, 0x78, 0x00 }; +const uint8_t ROTATE_R_ICON[8] = { 0x00, 0x78, 0x1c, 0x0c, 0x0c, 0x3f, 0x1e, 0x0c }; const uint8_t MONITOR_ICON[8] = {0x1f,0x51,0x51,0x71,0x71,0x51,0x51,0x1f}; const uint8_t AUDITION_ICON[8] = {0x78,0x68,0x68,0x78,0x48,0x4c,0x4a,0x79}; const uint8_t LINK_ICON[8] = {0x70,0xd8,0x88,0xda,0x5b,0x11,0x1b,0x0e}; const uint8_t CHECK_OFF_ICON[8] = {0xff,0x81,0x81,0x81,0x81,0x81,0x81,0xff}; const uint8_t CHECK_ON_ICON[8] = {0xcb,0x99,0xb1,0xb1,0x99,0x8c,0x86,0xf3}; const uint8_t CHECK_ICON[8] = {0x08,0x18,0x30,0x30,0x18,0x0c,0x06,0x03}; -const uint8_t BD_ICON[8] = {0x1C,0xA2,0x41,0x41,0x41,0x41,0xA2,0x1C}; -const uint8_t SN_ICON[8] = {0x0F,0x09,0x89,0x79,0x79,0x89,0x09,0x0F}; -const uint8_t HH_ICON[8] = {0x00,0x0A,0x0A,0xFF,0x4A,0x8A,0x00,0x00}; + +// Drums for DrumMap +const uint8_t BD_ICON[8] = {0x1c,0xa2,0x41,0x41,0x41,0xa2,0x1c,0x00}; +const uint8_t SN_ICON[8] = {0x3e,0xe5,0x25,0x25,0x25,0xe5,0x3e,0x00}; +const uint8_t HH_ICON[8] = {0x04,0x8a,0x4a,0x79,0x4a,0x8a,0x04,0x00}; +const uint8_t BD_HIT_ICON[8] = {0xf3,0x21,0x80,0xf0,0x80,0x21,0xf3,0x00}; +const uint8_t SN_HIT_ICON[8] = {0xc1,0x04,0xc4,0xc4,0xc4,0x04,0xc1,0x00}; +const uint8_t HH_HIT_ICON[8] = {0x0b,0x91,0x51,0x70,0x51,0x91,0x0b,0x00}; + const uint8_t RANDOM_ICON[8] = {0x7c,0x82,0x8a,0x82,0xa2,0x82,0x7c,0x00}; // A die showing '2' const uint8_t BURST_ICON[8] = {0x11,0x92,0x54,0x00,0xd7,0x00,0x54,0x92}; const uint8_t GAUGE_ICON[8] = {0x38,0x44,0x02,0x32,0x32,0x0a,0x44,0x3a}; const uint8_t CLOSED_ICON[8] = {0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18}; const uint8_t OPEN_ICON[8] = {0x18,0x0c,0x04,0x06,0x03,0x01,0x00,0x18}; +const uint8_t BTN_OFF_ICON[8] = {0x7e,0x8b,0x91,0x91,0x91,0x8b,0x7e,0x00}; +const uint8_t BTN_ON_ICON[8] = {0x71,0xaa,0xa8,0xab,0xa8,0xaa,0x71,0x00}; +const uint8_t ZAP_ICON[8] = {0x00,0x08,0x9c,0x5e,0x7a,0x39,0x10,0x00}; // Transport const uint8_t LOOP_ICON[8] = {0x34,0x64,0x4e,0x4e,0xe4,0xe4,0x4c,0x58}; @@ -38,6 +63,7 @@ const uint8_t PAUSE_ICON[8] = {0x00,0x7e,0x7e,0x00,0x00,0x7e,0x7e,0x00}; const uint8_t RESET_ICON[8] = {0x00,0x7e,0x00,0x00,0x18,0x3c,0x7e,0x00}; const uint8_t RECORD_ICON[8] = {0x00,0x3c,0x7e,0x7e,0x7e,0x7e,0x3c,0x00}; const uint8_t STOP_ICON[8] = {0x00,0x7e,0x7e,0x7e,0x7e,0x7e,0x7e,0x00}; +const uint8_t EDIT_ICON[8] = {0xc0,0xb0,0x48,0x44,0x22,0x15,0x0a,0x04}; // Direction Buttons const uint8_t UP_BTN_ICON[8] = {0x00,0x08,0x0c,0x0e,0x0e,0x0c,0x08,0x00}; @@ -45,22 +71,40 @@ const uint8_t DOWN_BTN_ICON[8] = {0x00,0x10,0x30,0x70,0x70,0x30,0x10,0x00}; const uint8_t LEFT_BTN_ICON[8] = {0x00,0x18,0x3c,0x7e,0x00,0x00,0x00,0x00}; const uint8_t RIGHT_BTN_ICON[8] = {0x00,0x00,0x00,0x00,0x7e,0x3c,0x18,0x00}; +const uint8_t UP_ICON[8] = {0x08,0x0c,0x7e,0x7f,0x7e,0x0c,0x08,0x00}; +const uint8_t DOWN_ICON[8] = {0x10,0x30,0x7e,0xfe,0x7e,0x30,0x10,0x00}; +const uint8_t LEFT_ICON[8] = {0x08,0x1c,0x3e,0x7f,0x1c,0x1c,0x1c,0x00}; +const uint8_t RIGHT_ICON[8] = {0x00,0x1c,0x1c,0x7f,0x3e,0x1c,0x08,0x00}; + // Metronome -const uint8_t METRO_L_ICON[8] = {0x00,0xc2,0xb4,0x88,0x94,0x88,0xb0,0xc0}; -const uint8_t METRO_R_ICON[8] = {0x00,0xc0,0xb0,0x88,0x94,0x88,0xb4,0xc2}; +const uint8_t METRO_L_ICON[8] = {0xf3,0x8c,0x9a,0xa2,0x82,0x8c,0xf0,0x00}; +const uint8_t METRO_R_ICON[8] = {0xf0,0x8c,0x82,0xa2,0x9a,0x8c,0xf3,0x00}; // Notes -const uint8_t X_NOTE_ICON[8] = {0x00,0xa0,0x40,0xa0,0x1f,0x02,0x0c,0x00}; -const uint8_t NOTE_ICON[8] = {0xc0,0xe0,0xe0,0xe0,0x7f,0x02,0x14,0x08}; -const uint8_t NOTE4_ICON[8] = {0x00,0x00,0x60,0x70,0x70,0x3f,0x00,0x00}; +const uint8_t X_NOTE_ICON[8] = { 0x00, 0x00, 0xa0, 0x40, 0xa0, 0x1f, 0x00, 0x00 }; +const uint8_t NOTE_ICON[8] = { 0x00, 0xe0, 0xe0, 0x7f, 0x02, 0x1c, 0x00, 0x00 }; +const uint8_t NOTE2_ICON[8] = { 0x00, 0x00, 0xe0, 0xa0, 0xff, 0x00, 0x00, 0x00 }; +const uint8_t NOTE4_ICON[8] = { 0x00, 0x00, 0xe0, 0xe0, 0xff, 0x00, 0x00, 0x00 }; + +// Pigeons +const uint8_t SILENT_PIGEON_ICON[8] = { 0x3c, 0xc2, 0x01, 0x0d, 0x01, 0x22, 0x54, 0x88 }; +const uint8_t SINGING_PIGEON_ICON[8] = { 0x3c, 0xc2, 0x01, 0x0d, 0x01, 0x02, 0x48, 0x94 }; // Waveform const uint8_t UP_DOWN_ICON[8] = {0x00,0x00,0x24,0x66,0xff,0x66,0x24,0x00}; const uint8_t LEFT_RIGHT_ICON[8] = {0x10,0x38,0x7c,0x10,0x10,0x7c,0x38,0x10}; const uint8_t SEGMENT_ICON[8] = {0xc0,0xc0,0x20,0x10,0x08,0x06,0x06,0x00}; -const uint8_t WAVEFORM_ICON[8] = {0x10,0x08,0x04,0x08,0x10,0x20,0x10,0x08}; +const uint8_t WAVEFORM_ICON[8] = {0x10, 0x0c, 0x02, 0x0c, 0x70, 0x80, 0x60, 0x10 }; + +const uint8_t STAIRS_ICON[8] = {0x00,0x20,0x20,0x38,0x08,0x0e,0x02,0x02}; // Some stairs going up + +// Superscript and subscript 1 and 2 +const uint8_t SUP_ONE[3] = {0x0a,0x0f,0x08}; +const uint8_t SUB_TWO[3] = {0x90,0xd0,0xa0}; // Units const uint8_t HERTZ_ICON[8] = {0xfe,0x10,0x10,0xfe,0x00,0xc8,0xa8,0x98}; +const uint8_t PLUS_ICON[8] = {0x10,0x18,0x18,0xfe,0x7f,0x18,0x18,0x08}; +const uint8_t MINUS_ICON[8] = {0x10,0x18,0x18,0x18,0x18,0x18,0x18,0x08}; #endif // HS_ICON_SET diff --git a/software/o_c_REV/HemisphereApplet.h b/software/o_c_REV/HemisphereApplet.h index a8162adda..a8851652f 100644 --- a/software/o_c_REV/HemisphereApplet.h +++ b/software/o_c_REV/HemisphereApplet.h @@ -25,35 +25,47 @@ //// Hemisphere Applet Base Class //////////////////////////////////////////////////////////////////////////////// +#pragma once + #include "HSicons.h" #include "HSClockManager.h" +#include "src/drivers/FreqMeasure/OC_FreqMeasure.h" +#include "util/util_math.h" #define LEFT_HEMISPHERE 0 #define RIGHT_HEMISPHERE 1 #ifdef BUCHLA_4U +#define PULSE_VOLTAGE 8 #define HEMISPHERE_MAX_CV 15360 -#define HEMISPHERE_CENTER_CV 7680 +#define HEMISPHERE_CENTER_CV 7680 // 5V +#define HEMISPHERE_MIN_CV 0 +#elif defined(VOR) +#define PULSE_VOLTAGE 8 +#define HEMISPHERE_MAX_CV (HS::octave_max * 12 << 7) +#define HEMISPHERE_CENTER_CV 0 +#define HEMISPHERE_MIN_CV (HEMISPHERE_MAX_CV - 15360) #else -#define HEMISPHERE_MAX_CV 7680 +#define PULSE_VOLTAGE 5 +#define HEMISPHERE_MAX_CV 9216 // 6V #define HEMISPHERE_CENTER_CV 0 +#define HEMISPHERE_MIN_CV -4608 // -3V #endif #define HEMISPHERE_3V_CV 4608 -#define HEMISPHERE_CLOCK_TICKS 100 +#define HEMISPHERE_MAX_INPUT_CV 9216 // 6V +#define HEMISPHERE_CENTER_DETENT 80 +#define HEMISPHERE_CLOCK_TICKS 17 // one millisecond #define HEMISPHERE_CURSOR_TICKS 12000 #define HEMISPHERE_ADC_LAG 33 #define HEMISPHERE_CHANGE_THRESHOLD 32 -#ifdef BUCHLA_4U -#define PULSE_VOLTAGE 8 -#else -#define PULSE_VOLTAGE 5 -#endif - // Codes for help system sections -#define HEMISPHERE_HELP_DIGITALS 0 -#define HEMISPHERE_HELP_CVS 1 -#define HEMISPHERE_HELP_OUTS 2 -#define HEMISPHERE_HELP_ENCODER 3 +enum HEM_HELP_SECTIONS { +HEMISPHERE_HELP_DIGITALS = 0, +HEMISPHERE_HELP_CVS = 1, +HEMISPHERE_HELP_OUTS = 2, +HEMISPHERE_HELP_ENCODER = 3 +}; +const char * HEM_HELP_SECTION_NAMES[4] = {"Dig", "CV", "Out", "Enc"}; // Simulated fixed floats by multiplying and dividing by powers of 2 #ifndef int2simfloat @@ -64,16 +76,60 @@ typedef int32_t simfloat; // Hemisphere-specific macros #define BottomAlign(h) (62 - h) -#define ForEachChannel(ch) for(int ch = 0; ch < 2; ch++) +#define ForEachChannel(ch) for(int_fast8_t ch = 0; ch < 2; ++ch) +#define ForAllChannels(ch) for(int_fast8_t ch = 0; ch < 4; ++ch) +#define gfx_offset (hemisphere * 64) // Graphics offset, based on the side +#define io_offset (hemisphere * 2) // Input/Output offset, based on the side + +#define HEMISPHERE_SIM_CLICK_TIME 1000 +#define HEMISPHERE_DOUBLE_CLICK_TIME 8000 +#define HEMISPHERE_PULSE_ANIMATION_TIME 500 +#define HEMISPHERE_PULSE_ANIMATION_TIME_LONG 1200 -// Specifies where data goes in flash storage for each selcted applet, and how big it is -typedef struct PackLocation { - int location; - int size; -} PackLocation; +#define DECLARE_APPLET(id, categories, class_name) \ +{ id, categories, class_name ## _Start, class_name ## _Controller, class_name ## _View, \ + class_name ## _OnButtonPress, class_name ## _OnEncoderMove, class_name ## _ToggleHelpScreen, \ + class_name ## _OnDataRequest, class_name ## _OnDataReceive \ +} + +#include "hemisphere_config.h" +#include "braids_quantizer.h" +#include "HSUtils.h" +#include "HSIOFrame.h" + +namespace HS { + +typedef struct Applet { + int id; + uint8_t categories; + void (*Start)(bool); // Initialize when selected + void (*Controller)(bool, bool); // Interrupt Service Routine + void (*View)(bool); // Draw main view + void (*OnButtonPress)(bool); // Encoder button has been pressed + void (*OnEncoderMove)(bool, int); // Encoder has been rotated + void (*ToggleHelpScreen)(bool); // Help Screen has been requested + uint64_t (*OnDataRequest)(bool); // Get a data int from the applet + void (*OnDataReceive)(bool, uint64_t); // Send a data int to the applet +} Applet; + +Applet available_applets[] = HEMISPHERE_APPLETS; +Applet clock_setup_applet = DECLARE_APPLET(9999, 0x01, ClockSetup); + +static IOFrame frame; + +int octave_max = 5; + +int select_mode = -1; +uint8_t modal_edit_mode = 2; // 0=old behavior, 1=modal editing, 2=modal with wraparound +static void CycleEditMode() { ++modal_edit_mode %= 3; } + +} + +using namespace HS; class HemisphereApplet { public: + static int cursor_countdown[2]; virtual const char* applet_name(); // Maximum of 9 characters virtual void Start(); @@ -82,19 +138,10 @@ class HemisphereApplet { void BaseStart(bool hemisphere_) { hemisphere = hemisphere_; - gfx_offset = hemisphere * 64; - io_offset = hemisphere * 2; // Initialize some things for startup - ForEachChannel(ch) - { - clock_countdown[ch] = 0; - inputs[ch] = 0; - outputs[ch] = 0; - adc_lag_countdown[ch] = 0; - } help_active = 0; - cursor_countdown = HEMISPHERE_CURSOR_TICKS; + cursor_countdown[hemisphere] = HEMISPHERE_CURSOR_TICKS; // Shutdown FTM capture on Digital 4, used by Tuner #ifdef FLIP_180 @@ -114,35 +161,23 @@ class HemisphereApplet { } } - void BaseController(bool master_clock_on) { - master_clock_bus = (master_clock_on && hemisphere == RIGHT_HEMISPHERE); - ForEachChannel(ch) - { - // Set CV inputs - ADC_CHANNEL channel = (ADC_CHANNEL)(ch + io_offset); - inputs[ch] = OC::ADC::raw_pitch_value(channel); - if (abs(inputs[ch] - last_cv[ch]) > HEMISPHERE_CHANGE_THRESHOLD) { - changed_cv[ch] = 1; - last_cv[ch] = inputs[ch]; - } else changed_cv[ch] = 0; - - // Handle clock timing - if (clock_countdown[ch] > 0) { - if (--clock_countdown[ch] == 0) Out(ch, 0); - } - } + void BaseController(bool master_clock_on = false) { + // I moved the IO-related stuff to the parent HemisphereManager app. + // The IOFrame gets loaded before calling Controllers, and outputs are handled after. + // -NJM // Cursor countdowns. See CursorBlink(), ResetCursor(), gfxCursor() - if (--cursor_countdown < -HEMISPHERE_CURSOR_TICKS) cursor_countdown = HEMISPHERE_CURSOR_TICKS; + if (--cursor_countdown[hemisphere] < -HEMISPHERE_CURSOR_TICKS) cursor_countdown[hemisphere] = HEMISPHERE_CURSOR_TICKS; Controller(); } void BaseView() { + //if (HS::select_mode == hemisphere) + gfxHeader(applet_name()); // If help is active, draw the help screen instead of the application screen if (help_active) DrawHelpScreen(); else View(); - last_view_tick = OC::CORE::ticks; } // Screensavers are deprecated in favor of screen blanking, but the BaseScreensaverView() remains @@ -156,24 +191,21 @@ class HemisphereApplet { /* Check cursor blink cycle. */ bool CursorBlink() { - return (cursor_countdown > 0); + return (cursor_countdown[hemisphere] > 0); } void ResetCursor() { - cursor_countdown = HEMISPHERE_CURSOR_TICKS; + cursor_countdown[hemisphere] = HEMISPHERE_CURSOR_TICKS; } void DrawHelpScreen() { - gfxHeader(applet_name()); SetHelp(); + for (int section = 0; section < 4; section++) { int y = section * 12 + 16; graphics.setPrintPos(0, y); - if (section == HEMISPHERE_HELP_DIGITALS) graphics.print("Dig"); - if (section == HEMISPHERE_HELP_CVS) graphics.print("CV"); - if (section == HEMISPHERE_HELP_OUTS) graphics.print("Out"); - if (section == HEMISPHERE_HELP_ENCODER) graphics.print("Enc"); + graphics.print( HEM_HELP_SECTION_NAMES[section] ); graphics.invertRect(0, y - 1, 19, 9); graphics.setPrintPos(20, y); @@ -181,10 +213,46 @@ class HemisphereApplet { } } + // handle modal edit mode toggle or cursor advance + void CursorAction(int &cursor, int max) { + if (modal_edit_mode) { + isEditing = !isEditing; + } else { + cursor++; + cursor %= max + 1; + ResetCursor(); + } + } + void MoveCursor(int &cursor, int direction, int max) { + cursor += direction; + if (modal_edit_mode == 2) { // wrap cursor + if (cursor < 0) cursor = max; + else cursor %= max + 1; + } else { + cursor = constrain(cursor, 0, max); + } + ResetCursor(); + } + bool EditMode() { + return (isEditing || !modal_edit_mode); + } + + // Override HSUtils function to only return positive values + // Not ideal, but too many applets rely on this. + int ProportionCV(int cv_value, int max_pixels) { + int prop = constrain(Proportion(cv_value, HEMISPHERE_MAX_INPUT_CV, max_pixels), 0, max_pixels); + return prop; + } + //////////////// Offset graphics methods //////////////////////////////////////////////////////////////////////////////// - void gfxCursor(int x, int y, int w) { - if (CursorBlink()) gfxLine(x, y, x + w - 1, y); + void gfxCursor(int x, int y, int w, int h = 9) { // assumes standard text height for highlighting + if (isEditing) gfxInvert(x, y - h, w, h); + else if (CursorBlink()) { + gfxLine(x, y, x + w - 1, y); + gfxPixel(x, y-1); + gfxPixel(x + w - 1, y-1); + } } void gfxPos(int x, int y) { @@ -195,40 +263,22 @@ class HemisphereApplet { graphics.setPrintPos(x + gfx_offset, y); graphics.print(str); } - void gfxPrint(int x, int y, int num) { graphics.setPrintPos(x + gfx_offset, y); graphics.print(num); } - - void gfxPrint(int x_adv, int num) { // Print number with character padding - for (int c = 0; c < (x_adv / 6); c++) gfxPrint(" "); - gfxPrint(num); - } - void gfxPrint(const char *str) { graphics.print(str); } - void gfxPrint(int num) { graphics.print(num); } - - /* Convert CV value to voltage level and print to two decimal places */ - void gfxPrintVoltage(int cv) { - int v = (cv * 100) / (12 << 7); - bool neg = v < 0 ? 1 : 0; - if (v < 0) v = -v; - int wv = v / 100; // whole volts - int dv = v - (wv * 100); // decimal - gfxPrint(neg ? "-" : "+"); - gfxPrint(wv); - gfxPrint("."); - if (dv < 10) gfxPrint("0"); - gfxPrint(dv); - gfxPrint("V"); + void gfxPrint(int x_adv, int num) { // Print number with character padding + for (int c = 0; c < (x_adv / 6); c++) gfxPrint(" "); + gfxPrint(num); } + /* Convert CV value to voltage level and print to two decimal places */ void gfxPixel(int x, int y) { graphics.setPixel(x + gfx_offset, y); } @@ -264,21 +314,10 @@ class HemisphereApplet { void gfxBitmap(int x, int y, int w, const uint8_t *data) { graphics.drawBitmap8(x + gfx_offset, y, w, data); } - void gfxIcon(int x, int y, const uint8_t *data) { gfxBitmap(x, y, 8, data); } - uint8_t pad(int range, int number) { - uint8_t padding = 0; - while (range > 1) - { - if (abs(number) < range) padding += 6; - range = range / 10; - } - return padding; - } - //////////////// Hemisphere-specific graphics methods //////////////////////////////////////////////////////////////////////////////// @@ -296,25 +335,74 @@ class HemisphereApplet { void gfxHeader(const char *str) { gfxPrint(1, 2, str); - gfxLine(0, 10, 62, 10); - gfxLine(0, 12, 62, 12); + gfxDottedLine(0, 10, 62, 10); + //gfxLine(0, 11, 62, 11); } //////////////// Offset I/O methods //////////////////////////////////////////////////////////////////////////////// int In(int ch) { - return inputs[ch]; + return frame.inputs[io_offset + ch]; } // Apply small center detent to input, so it reads zero before a threshold int DetentedIn(int ch) { - return (In(ch) > (HEMISPHERE_CENTER_CV + 64) || In(ch) < (HEMISPHERE_CENTER_CV - 64)) ? In(ch) : HEMISPHERE_CENTER_CV; + return (In(ch) > (HEMISPHERE_CENTER_CV + HEMISPHERE_CENTER_DETENT) || In(ch) < (HEMISPHERE_CENTER_CV - HEMISPHERE_CENTER_DETENT)) + ? In(ch) : HEMISPHERE_CENTER_CV; + } + + int SmoothedIn(int ch) { + ADC_CHANNEL channel = (ADC_CHANNEL)(ch + io_offset); + return OC::ADC::value(channel); + } + + braids::Quantizer* GetQuantizer(int ch) { + return &HS::quantizer[io_offset + ch]; + } + int Quantize(int ch, int cv, int root, int transpose) { + return HS::quantizer[io_offset + ch].Process(cv, root, transpose); + } + int QuantizerLookup(int ch, int note) { + return HS::quantizer[io_offset + ch].Lookup(note) + (HS::root_note[io_offset+ch] << 7); + } + void QuantizerConfigure(int ch, int scale, uint16_t mask = 0xffff) { + HS::quant_scale[io_offset + ch] = scale; + HS::quantizer[io_offset + ch].Configure(OC::Scales::GetScale(scale), mask); + } + int GetScale(int ch) { + return HS::quant_scale[io_offset + ch]; + } + int GetRootNote(int ch) { + return HS::root_note[io_offset + ch]; + } + int SetRootNote(int ch, int root) { + return (HS::root_note[io_offset + ch] = root); + } + void NudgeScale(int ch, int dir) { + const int max = OC::Scales::NUM_SCALES; + int &s = HS::quant_scale[io_offset + ch]; + + s+= dir; + if (s >= max) s = 0; + if (s < 0) s = max - 1; + QuantizerConfigure(ch, s); + } + + // Standard bi-polar CV modulation scenario + template + void Modulate(T ¶m, const int ch, const int min = 0, const int max = 255) { + int cv = DetentedIn(ch); + param = constrain(param + Proportion(cv, HEMISPHERE_MAX_INPUT_CV, max), min, max); } void Out(int ch, int value, int octave = 0) { + frame.Out( (DAC_CHANNEL)(ch + io_offset), value + (octave * (12 << 7))); + } + + void SmoothedOut(int ch, int value, int kSmoothing) { DAC_CHANNEL channel = (DAC_CHANNEL)(ch + io_offset); - OC::DAC::set_pitch(channel, value, octave); - outputs[ch] = value + (octave * (12 << 7)); + value = (frame.outputs_smooth[channel] * (kSmoothing - 1) + value) / kSmoothing; + frame.outputs[channel] = frame.outputs_smooth[channel] = value; } /* @@ -325,43 +413,32 @@ class HemisphereApplet { */ bool Clock(int ch, bool physical = 0) { bool clocked = 0; - if (hemisphere == 0) { - if (ch == 0) clocked = OC::DigitalInputs::clocked(); - if (ch == 1) clocked = OC::DigitalInputs::clocked(); - } else if (hemisphere == 1) { - if (ch == 0) clocked = OC::DigitalInputs::clocked(); - if (ch == 1) clocked = OC::DigitalInputs::clocked(); - } + ClockManager *clock_m = clock_m->get(); + bool useTock = (!physical && clock_m->IsRunning()); - if (ch == 0 && !physical) { - ClockManager *clock_m = clock_m->get(); - if (clock_m->IsRunning()) clocked = clock_m->Tock(); - else if (master_clock_bus) clocked = OC::DigitalInputs::clocked(); - } + // clock triggers + if (useTock && clock_m->GetMultiply(ch + io_offset) != 0) + clocked = clock_m->Tock(ch + io_offset); + else if (trigger_mapping[ch + io_offset]) + clocked = frame.clocked[ trigger_mapping[ch + io_offset] - 1 ]; + + // Try to eat a boop + clocked = clocked || clock_m->Beep(io_offset + ch); if (clocked) { - cycle_ticks[ch] = OC::CORE::ticks - last_clock[ch]; - last_clock[ch] = OC::CORE::ticks; + frame.cycle_ticks[io_offset + ch] = OC::CORE::ticks - frame.last_clock[io_offset + ch]; + frame.last_clock[io_offset + ch] = OC::CORE::ticks; } return clocked; } - void ClockOut(int ch, int ticks = HEMISPHERE_CLOCK_TICKS) { - clock_countdown[ch] = ticks; - Out(ch, 0, PULSE_VOLTAGE); + void ClockOut(const int ch, const int ticks = HEMISPHERE_CLOCK_TICKS * trig_length) { + frame.ClockOut( (DAC_CHANNEL)(io_offset + ch), ticks); } bool Gate(int ch) { - bool high = 0; - if (hemisphere == 0) { - if (ch == 0) high = OC::DigitalInputs::read_immediate(); - if (ch == 1) high = OC::DigitalInputs::read_immediate(); - } - if (hemisphere == 1) { - if (ch == 0) high = OC::DigitalInputs::read_immediate(); - if (ch == 1) high = OC::DigitalInputs::read_immediate(); - } - return high; + const int t = trigger_mapping[ch + io_offset]; + return t ? frame.gate_high[t - 1] : false; } void GateOut(int ch, bool high) { @@ -369,13 +446,14 @@ class HemisphereApplet { } // Buffered I/O functions - int ViewIn(int ch) {return inputs[ch];} - int ViewOut(int ch) {return outputs[ch];} - int ClockCycleTicks(int ch) {return cycle_ticks[ch];} - bool Changed(int ch) {return changed_cv[ch];} + int ViewIn(int ch) {return frame.inputs[io_offset + ch];} + int ViewOut(int ch) {return frame.outputs[io_offset + ch];} + int ClockCycleTicks(int ch) {return frame.cycle_ticks[io_offset + ch];} + bool Changed(int ch) {return frame.changed_cv[io_offset + ch];} protected: bool hemisphere; // Which hemisphere (0, 1) this applet uses + bool isEditing = false; // modal editing toggle const char* help[4]; virtual void SetHelp(); @@ -386,50 +464,6 @@ class HemisphereApplet { applet_started = 0; } - //////////////// Calculation methods - //////////////////////////////////////////////////////////////////////////////// - - /* Proportion method using simfloat, useful for calculating scaled values given - * a fractional value. - * - * Solves this: numerator ??? - * ----------- = ----------- - * denominator max - * - * For example, to convert a parameter with a range of 1 to 100 into value scaled - * to HEMISPHERE_MAX_CV, to be sent to the DAC: - * - * Out(ch, Proportion(value, 100, HEMISPHERE_MAX_CV)); - * - */ - int Proportion(int numerator, int denominator, int max_value) { - simfloat proportion = int2simfloat((int32_t)numerator) / (int32_t)denominator; - int scaled = simfloat2int(proportion * max_value); - return scaled; - } - - /* Proportion CV values into pixels for display purposes. - * - * Solves this: cv_value ??? - * ----------------- = ---------- - * HEMISPHERE_MAX_CV max_pixels - */ - int ProportionCV(int cv_value, int max_pixels) { - int prop = constrain(Proportion(cv_value, HEMISPHERE_MAX_CV, max_pixels), 0, max_pixels); - return prop; - } - - /* Add value to a 64-bit storage unit at the specified location */ - void Pack(uint64_t &data, PackLocation p, uint64_t value) { - data |= (value << p.location); - } - - /* Retrieve value from a 64-bit storage unit at the specified location and of the specified bit size */ - int Unpack(uint64_t data, PackLocation p) { - uint64_t mask = 1; - for (int i = 1; i < p.size; i++) mask |= (0x01 << i); - return (data >> p.location) & mask; - } /* ADC Lag: There is a small delay between when a digital input can be read and when an ADC can be * read. The ADC value lags behind a bit in time. So StartADCLag() and EndADCLag() are used to @@ -442,31 +476,19 @@ class HemisphereApplet { * // etc... * } */ - void StartADCLag(int ch = 0) { - adc_lag_countdown[ch] = HEMISPHERE_ADC_LAG; + void StartADCLag(bool ch = 0) { + frame.adc_lag_countdown[io_offset + ch] = HEMISPHERE_ADC_LAG; } - bool EndOfADCLag(int ch = 0) { - return (--adc_lag_countdown[ch] == 0); + bool EndOfADCLag(bool ch = 0) { + if (frame.adc_lag_countdown[io_offset + ch] < 0) return false; + return (--frame.adc_lag_countdown[io_offset + ch] == 0); } - /* Master Clock Forwarding is activated. This is updated with each ISR cycle by the Hemisphere Manager */ - bool MasterClockForwarded() {return master_clock_bus;} - private: - int gfx_offset; // Graphics offset, based on the side - int io_offset; // Input/Output offset, based on the side - int inputs[2]; - int outputs[2]; - uint32_t last_clock[2]; // Tick number of the last clock observed by the child class - uint32_t cycle_ticks[2]; // Number of ticks between last two clocks - int clock_countdown[2]; - int cursor_countdown; - int adc_lag_countdown[2]; // Time between a clock event and an ADC read event - bool master_clock_bus; // Clock forwarding was on during the last ISR cycle bool applet_started; // Allow the app to maintain state during switching - int last_view_tick; // Tick number of the most recent view - int help_active; - bool changed_cv[2]; // Has the input changed by more than 1/8 semitone since the last read? - int last_cv[2]; // For change detection + bool help_active; }; + +int HemisphereApplet::cursor_countdown[2]; + diff --git a/software/o_c_REV/OC_ADC.cpp b/software/o_c_REV/OC_ADC.cpp index e6ecf1e6f..f37a35573 100644 --- a/software/o_c_REV/OC_ADC.cpp +++ b/software/o_c_REV/OC_ADC.cpp @@ -1,105 +1,364 @@ #include "OC_ADC.h" #include "OC_gpio.h" - +#include "DMAChannel.h" #include namespace OC { -template struct ChannelDesc { }; -template <> struct ChannelDesc { - static const int PIN = CV1; -}; -template <> struct ChannelDesc { - static const int PIN = CV2; -}; -template <> struct ChannelDesc { - static const int PIN = CV3; -}; -template <> struct ChannelDesc { - static const int PIN = CV4; -}; - +#if defined(__MK20DX256__) /*static*/ ::ADC ADC::adc_; -/*static*/ size_t ADC::scan_channel_; +#endif /*static*/ ADC::CalibrationData *ADC::calibration_data_; /*static*/ uint32_t ADC::raw_[ADC_CHANNEL_LAST]; /*static*/ uint32_t ADC::smoothed_[ADC_CHANNEL_LAST]; -#ifdef ENABLE_ADC_DEBUG -/*static*/ volatile uint32_t ADC::busy_waits_; +#ifdef OC_ADC_ENABLE_DMA_INTERRUPT +/*static*/ volatile bool ADC::ready_; #endif -/*static*/ void ADC::Init(CalibrationData *calibration_data) { +#if defined(__MK20DX256__) +constexpr uint16_t ADC::SCA_CHANNEL_ID[DMA_NUM_CH]; // ADCx_SCA register channel numbers +DMAChannel* dma0 = new DMAChannel(false); // dma0 channel, fills adcbuffer_0 +DMAChannel* dma1 = new DMAChannel(false); // dma1 channel, updates ADC0_SC1A which holds the channel/pin IDs +DMAMEM static volatile uint16_t __attribute__((aligned(DMA_BUF_SIZE+0))) adcbuffer_0[DMA_BUF_SIZE]; - // According to Paul Stoffregen: You do NOT want to have the pin in digital mode when using it as analog input. - // https://forum.pjrc.com/threads/34319-Analog-input-impedance-and-pull-up?p=103543&viewfull=1#post103543 - //pinMode(ChannelDesc::PIN, INPUT); - //pinMode(ChannelDesc::PIN, INPUT); - //pinMode(ChannelDesc::PIN, INPUT); - //pinMode(ChannelDesc::PIN, INPUT); +#elif defined(__IMXRT1062__) +#define ADC_SAMPLE_RATE 66000.0f +extern "C" void xbar_connect(unsigned int input, unsigned int output); +DMAChannel dma0(false); +typedef struct { + uint16_t adc[4]; +} adcframe_t; +// sizeof(adc_buffer) must be multiple of 32 byte cache row size +static const int adc_buffer_len = 32; +static DMAMEM __attribute__((aligned(32))) adcframe_t adc_buffer[adc_buffer_len]; +static PROGMEM const uint8_t adc2_pin_to_channel[] = { + 7, // 0/A0 AD_B1_02 + 8, // 1/A1 AD_B1_03 + 12, // 2/A2 AD_B1_07 + 11, // 3/A3 AD_B1_06 + 6, // 4/A4 AD_B1_01 + 5, // 5/A5 AD_B1_00 + 15, // 6/A6 AD_B1_10 + 0, // 7/A7 AD_B1_11 + 13, // 8/A8 AD_B1_08 + 14, // 9/A9 AD_B1_09 + 255, // 10/A10 AD_B0_12 - can't use this pin! + 255, // 11/A11 AD_B0_13 - can't use this pin! + 3, // 12/A12 AD_B1_14 + 4, // 13/A13 AD_B1_15 + 7, // 14/A0 AD_B1_02 + 8, // 15/A1 AD_B1_03 + 12, // 16/A2 AD_B1_07 + 11, // 17/A3 AD_B1_06 + 6, // 18/A4 AD_B1_01 + 5, // 19/A5 AD_B1_00 + 15, // 20/A6 AD_B1_10 + 0, // 21/A7 AD_B1_11 + 13, // 22/A8 AD_B1_08 + 14, // 23/A9 AD_B1_09 + 255, // 24/A10 AD_B0_12 - can't use this pin! + 255, // 25/A11 AD_B0_13 - can't use this pin! + 3, // 26/A12 AD_B1_14 + 4, // 27/A13 AD_B1_15 +#ifdef ARDUINO_TEENSY41 + 255, // 28 + 255, // 29 + 255, // 30 + 255, // 31 + 255, // 32 + 255, // 33 + 255, // 34 + 255, // 35 + 255, // 36 + 255, // 37 + 1, // 38/A14 AD_B1_12 + 2, // 39/A15 AD_B1_13 + 9, // 40/A16 AD_B1_04 + 10, // 41/A17 AD_B1_05 +#endif +}; +#endif // __IMXRT1062__ + + +#if defined(__MK20DX256__) +/*static*/ void ADC::Init(CalibrationData *calibration_data) { adc_.setReference(ADC_REF_3V3); adc_.setResolution(kAdcScanResolution); adc_.setConversionSpeed(kAdcConversionSpeed); adc_.setSamplingSpeed(kAdcSamplingSpeed); adc_.setAveraging(kAdcScanAverages); - adc_.disableDMA(); - adc_.disableInterrupts(); - adc_.disableCompare(); - scan_channel_ = ADC_CHANNEL_1; - adc_.startSingleRead(ChannelDesc::PIN); + calibration_data_ = calibration_data; + std::fill(raw_, raw_ + ADC_CHANNEL_LAST, 0); + std::fill(smoothed_, smoothed_ + ADC_CHANNEL_LAST, 0); + std::fill(adcbuffer_0, adcbuffer_0 + DMA_BUF_SIZE, 0); + + adc_.enableDMA(); +} +#elif defined(__IMXRT1062__) +/*static*/ void ADC::Init(CalibrationData *calibration_data) { calibration_data_ = calibration_data; std::fill(raw_, raw_ + ADC_CHANNEL_LAST, 0); std::fill(smoothed_, smoothed_ + ADC_CHANNEL_LAST, 0); -#ifdef ENABLE_ADC_DEBUG - busy_waits_ = 0; -#endif } -// As I understand it, only CV4 can be muxed to ADC1, so it's not possible to -// use ADC::startSynchronizedSingleRead, which would allow reading two channels -// simultaneously +#endif // __IMXRT1062__ + +#ifdef OC_ADC_ENABLE_DMA_INTERRUPT +/*static*/ void ADC::DMA_ISR() { + ADC::ready_ = true; + dma0->clearInterrupt(); + /* restart DMA in ADC::Scan_DMA() */ +} +#endif + +/* + * + * DMA/ADC à la https://forum.pjrc.com/threads/30171-Reconfigure-ADC-via-a-DMA-transfer-to-allow-multiple-Channel-Acquisition + * basically, this sets up two DMA channels and cycles through the four adc mux channels (until the buffer is full), resets, and so on; dma1 advances SCA_CHANNEL_ID + * somewhat like https://www.nxp.com/docs/en/application-note/AN4590.pdf but w/o the PDB. + * +*/ + +#if defined(__MK20DX256__) +void ADC::Init_DMA() { + + dma0->begin(true); // allocate the DMA channel + dma0->TCD->SADDR = &ADC0_RA; + dma0->TCD->SOFF = 0; + dma0->TCD->ATTR = 0x101; + dma0->TCD->NBYTES = 2; + dma0->TCD->SLAST = 0; + dma0->TCD->DADDR = &adcbuffer_0[0]; + dma0->TCD->DOFF = 2; + dma0->TCD->DLASTSGA = -(2 * DMA_BUF_SIZE); + dma0->TCD->BITER = DMA_BUF_SIZE; + dma0->TCD->CITER = DMA_BUF_SIZE; + dma0->triggerAtHardwareEvent(DMAMUX_SOURCE_ADC0); + dma0->disableOnCompletion(); +#ifdef OC_ADC_ENABLE_DMA_INTERRUPT + dma0->interruptAtCompletion(); + dma0->attachInterrupt(DMA_ISR); + ready_ = false; +#endif + + dma1->begin(true); // allocate the DMA channel + dma1->TCD->SADDR = &ADC::SCA_CHANNEL_ID[0]; + dma1->TCD->SOFF = 2; // source increment each transfer (n bytes) + dma1->TCD->ATTR = 0x101; + dma1->TCD->SLAST = - DMA_NUM_CH*2; // num ADC0 samples * 2 + dma1->TCD->BITER = DMA_NUM_CH; + dma1->TCD->CITER = DMA_NUM_CH; + dma1->TCD->DADDR = &ADC0_SC1A; + dma1->TCD->DLASTSGA = 0; + dma1->TCD->NBYTES = 2; + dma1->TCD->DOFF = 0; + dma1->triggerAtTransfersOf(*dma0); + dma1->triggerAtCompletionOf(*dma0); + + dma0->enable(); + dma1->enable(); +} + +#elif defined(__IMXRT1062__) +void ADC::Init_DMA() { + // Ornament & Crime CV inputs are 19/A5 18/A4 20/A6 17/A3 +#ifdef FLIP_180 + const int pin4 = A5; + const int pin3 = A4; + const int pin2 = A6; + const int pin1 = A3; +#else + const int pin1 = A5; + const int pin2 = A4; + const int pin3 = A6; + const int pin4 = A3; +#endif + pinMode(pin1, INPUT_DISABLE); + pinMode(pin2, INPUT_DISABLE); + pinMode(pin3, INPUT_DISABLE); + pinMode(pin4, INPUT_DISABLE); + + // configure a timer to trigger ADC + const int comp1 = ((float)F_BUS_ACTUAL) / (ADC_SAMPLE_RATE) / 2.0f + 0.5f; + TMR4_ENBL &= ~(1<<3); + TMR4_SCTRL3 = TMR_SCTRL_OEN | TMR_SCTRL_FORCE; + TMR4_CSCTRL3 = TMR_CSCTRL_CL1(1) | TMR_CSCTRL_TCF1EN; + TMR4_CNTR3 = 0; + TMR4_LOAD3 = 0; + TMR4_COMP13 = comp1; + TMR4_CMPLD13 = comp1; + TMR4_CTRL3 = TMR_CTRL_CM(1) | TMR_CTRL_PCS(8) | TMR_CTRL_LENGTH | TMR_CTRL_OUTMODE(3); + TMR4_DMA3 = TMR_DMA_CMPLD1DE; + TMR4_CNTR3 = 0; + TMR4_ENBL |= (1<<3); -/*static*/ void FASTRUN ADC::Scan() { + // connect the timer output the ADC_ETC input + const int trigger = 4; // 0-3 for ADC1, 4-7 for ADC2 + CCM_CCGR2 |= CCM_CCGR2_XBAR1(CCM_CCGR_ON); + xbar_connect(XBARA1_IN_QTIMER4_TIMER3, XBARA1_OUT_ADC_ETC_TRIG00 + trigger); -#ifdef ENABLE_ADC_DEBUG - if (!adc_.isComplete(ADC_0)) { - ++busy_waits_; - while (!adc_.isComplete(ADC_0)); + // turn on ADC_ETC and configure to receive trigger + if (ADC_ETC_CTRL & (ADC_ETC_CTRL_SOFTRST | ADC_ETC_CTRL_TSC_BYPASS)) { + ADC_ETC_CTRL = 0; // clears SOFTRST only + ADC_ETC_CTRL = 0; // clears TSC_BYPASS } + ADC_ETC_CTRL |= ADC_ETC_CTRL_TRIG_ENABLE(1 << trigger) | ADC_ETC_CTRL_DMA_MODE_SEL; + ADC_ETC_DMA_CTRL |= ADC_ETC_DMA_CTRL_TRIQ_ENABLE(trigger); + + // configure ADC_ETC trigger4 to make four ADC2 measurements + const int len = 4; + IMXRT_ADC_ETC.TRIG[trigger].CTRL = ADC_ETC_TRIG_CTRL_TRIG_CHAIN(len - 1) | + ADC_ETC_TRIG_CTRL_TRIG_PRIORITY(7); + IMXRT_ADC_ETC.TRIG[trigger].CHAIN_1_0 = + ADC_ETC_TRIG_CHAIN_HWTS0(1) | ADC_ETC_TRIG_CHAIN_HWTS1(1) | + ADC_ETC_TRIG_CHAIN_CSEL0(adc2_pin_to_channel[pin1]) | ADC_ETC_TRIG_CHAIN_B2B0 | + ADC_ETC_TRIG_CHAIN_CSEL1(adc2_pin_to_channel[pin2]) | ADC_ETC_TRIG_CHAIN_B2B1; + IMXRT_ADC_ETC.TRIG[trigger].CHAIN_3_2 = + ADC_ETC_TRIG_CHAIN_HWTS0(1) | ADC_ETC_TRIG_CHAIN_HWTS1(1) | + ADC_ETC_TRIG_CHAIN_CSEL0(adc2_pin_to_channel[pin3]) | ADC_ETC_TRIG_CHAIN_B2B0 | + ADC_ETC_TRIG_CHAIN_CSEL1(adc2_pin_to_channel[pin4]) | ADC_ETC_TRIG_CHAIN_B2B1; + + // set up ADC2 for 12 bit mode, hardware trigger + // ADLPC=0, ADHSC=1, 12 bit mode, 40 MHz max ADC clock + // ADLPC=0, ADHSC=0, 12 bit mode, 30 MHz max ADC clock + // ADLPC=1, ADHSC=0, 12 bit mode, 20 MHz max ADC clock + uint32_t cfg = ADC_CFG_ADTRG; + cfg |= ADC_CFG_MODE(2); // 2 = 12 bits + cfg |= ADC_CFG_AVGS(0); // # samples to average + cfg |= ADC_CFG_ADSTS(2); // sampling time, 0-3 + //cfg |= ADC_CFG_ADLSMP; // long sample time + cfg |= ADC_CFG_ADHSC; // high speed conversion + //cfg |= ADC_CFG_ADLPC; // low power + cfg |= ADC_CFG_ADICLK(0);// 0:ipg, 1=ipg/2, 3=adack (10 or 20 MHz) + cfg |= ADC_CFG_ADIV(2); // 0:div1, 1=div2, 2=div4, 3=div8 + ADC2_CFG = cfg; + //ADC2_GC &= ~ADC_GC_AVGE; // single sample, no averaging + ADC2_GC |= ADC_GC_AVGE; // use averaging + ADC2_HC0 = ADC_HC_ADCH(16); // 16 = controlled by ADC_ETC + + // use a DMA channel to capture ADC_ETC output + dma0.begin(); + dma0.TCD->SADDR = &(IMXRT_ADC_ETC.TRIG[4].RESULT_1_0); + dma0.TCD->SOFF = 4; + dma0.TCD->ATTR = DMA_TCD_ATTR_SSIZE(2) | DMA_TCD_ATTR_DSIZE(2); + dma0.TCD->NBYTES_MLNO = DMA_TCD_NBYTES_MLOFFYES_NBYTES(8) | // sizeof(adcframe_t) = 8 + DMA_TCD_NBYTES_SMLOE | DMA_TCD_NBYTES_MLOFFYES_MLOFF(-8); + dma0.TCD->SLAST = -8; + dma0.TCD->DADDR = adc_buffer; + dma0.TCD->DOFF = 4; + dma0.TCD->CITER_ELINKNO = sizeof(adc_buffer) / 8; + dma0.TCD->DLASTSGA = -sizeof(adc_buffer); + dma0.TCD->BITER_ELINKNO = sizeof(adc_buffer) / 8; + dma0.TCD->CSR = 0; + dma0.triggerAtHardwareEvent(DMAMUX_SOURCE_ADC_ETC); + dma0.enable(); +} +#endif // __IMXRT1062__ + + +#if defined(__MK20DX256__) +/*static*/void FASTRUN ADC::Scan_DMA() { + +#ifdef OC_ADC_ENABLE_DMA_INTERRUPT + if (ADC::ready_) { + ADC::ready_ = false; +#else + if (dma0->complete()) { + // On Teensy 3.2, this runs every 180us (every 3rd call from 60us timer) + dma0->clearComplete(); #endif - const uint16_t value = adc_.readSingle(ADC_0); - - size_t channel = scan_channel_; - switch (channel) { - case ADC_CHANNEL_1: - adc_.startSingleRead(ChannelDesc::PIN, ADC_0); - update(value); - ++channel; - break; - - case ADC_CHANNEL_2: - adc_.startSingleRead(ChannelDesc::PIN, ADC_0); - update(value); - ++channel; - break; - - case ADC_CHANNEL_3: - adc_.startSingleRead(ChannelDesc::PIN, ADC_0); - update(value); - ++channel; - break; - - case ADC_CHANNEL_4: - adc_.startSingleRead(ChannelDesc::PIN, ADC_0); - update(value); - channel = ADC_CHANNEL_1; - break; + dma0->TCD->DADDR = &adcbuffer_0[0]; + + /* + * collect results from adcbuffer_0; as things are, there's DMA_BUF_SIZE = 16 samples in the buffer. + */ + uint32_t value; + + value = (adcbuffer_0[0] + adcbuffer_0[4] + adcbuffer_0[8] + adcbuffer_0[12]) >> 2; // / 4 = DMA_BUF_SIZE / DMA_NUM_CH + update(value); + + value = (adcbuffer_0[1] + adcbuffer_0[5] + adcbuffer_0[9] + adcbuffer_0[13]) >> 2; + update(value); + + value = (adcbuffer_0[2] + adcbuffer_0[6] + adcbuffer_0[10] + adcbuffer_0[14]) >> 2; + update(value); + + value = (adcbuffer_0[3] + adcbuffer_0[7] + adcbuffer_0[11] + adcbuffer_0[15]) >> 2; + update(value); + + /* restart */ + dma0->enable(); + } +} + +#elif defined(__IMXRT1062__) +static void sum_adc(uint32_t *sum, const adcframe_t *n) { + sum[0] += n->adc[0]; + sum[1] += n->adc[1]; + sum[2] += n->adc[2]; + sum[3] += n->adc[3]; +} + +/*static*/void FASTRUN ADC::Scan_DMA() { + static int old_idx = 0; + + // find the most recently DMA-stored ADC data frame + const adcframe_t *p = (adcframe_t *)dma0.TCD->DADDR; + arm_dcache_delete(adc_buffer, sizeof(adc_buffer)); + //asm("dsb"); + + uint32_t sum[4] = {0, 0, 0, 0}; + int idx = p - adc_buffer; + int count = idx - old_idx; + if (count < 0) count += adc_buffer_len; + if (count) { + for (int i=0; i < count ; i++) { + sum_adc(sum, &adc_buffer[(idx + i) % adc_buffer_len]); + } + + const int mult = 16; + update(sum[0] * mult / count); + update(sum[1] * mult / count); + update(sum[2] * mult / count); + update(sum[3] * mult / count); + + old_idx = idx; } - scan_channel_ = channel; } +#if defined(ARDUINO_TEENSY41) // Teensy 4.1 - A17 pin identifies PCB hardware +FLASHMEM static +float read_id_voltage() { + const unsigned int count = 50; + unsigned int sum=0; + delayMicroseconds(10); + for (unsigned int i=0; i < count; i++) { + sum += analogRead(A17); + } + return (float)sum * (3.3f / 1023.0f / (float)count); +} + +FLASHMEM +float ADC::Read_ID_Voltage() { + pinMode(A17, INPUT_PULLUP); + float volts_pullup = read_id_voltage(); + pinMode(A17, INPUT_PULLDOWN); + float volts_pulldown = read_id_voltage(); + pinMode(A17, INPUT_DISABLE); + if (volts_pullup - volts_pulldown > 2.5f) return 0; // pin not connected + return read_id_voltage(); +} +#else // Teensy 4.0 +FLASHMEM float ADC::Read_ID_Voltage() { return 0; } +#endif + + +#endif // __IMXRT1062__ + + /*static*/ void ADC::CalibratePitch(int32_t c2, int32_t c4) { // This is the method used by the Mutable Instruments calibration and // extrapolates from two octaves. I guess an alternative would be to get the diff --git a/software/o_c_REV/OC_ADC.h b/software/o_c_REV/OC_ADC.h index 4d1a7de29..9562c48c2 100644 --- a/software/o_c_REV/OC_ADC.h +++ b/software/o_c_REV/OC_ADC.h @@ -1,14 +1,16 @@ #ifndef OC_ADC_H_ #define OC_ADC_H_ -#include #include "src/drivers/ADC/OC_util_ADC.h" #include "OC_config.h" +#include "OC_options.h" #include #include -//#define ENABLE_ADC_DEBUG +// If enabled, use an interrupt to track DMA completion; otherwise use polling +//#define OC_ADC_ENABLE_DMA_INTERRUPT + enum ADC_CHANNEL { ADC_CHANNEL_1, @@ -18,6 +20,9 @@ enum ADC_CHANNEL { ADC_CHANNEL_LAST, }; +#define DMA_BUF_SIZE 16 +#define DMA_NUM_CH ADC_CHANNEL_LAST + namespace OC { class ADC { @@ -31,10 +36,9 @@ class ADC { // These values should be tweaked so startSingleRead/readSingle run in main ISR update time // 16 bit has best-case 13 bits useable, but we only want 12 so we discard 4 anyway static constexpr uint8_t kAdcScanResolution = 16; - static constexpr uint8_t kAdcScanAverages = 16; + static constexpr uint8_t kAdcScanAverages = 4; static constexpr uint8_t kAdcSamplingSpeed = ADC_HIGH_SPEED_16BITS; static constexpr uint8_t kAdcConversionSpeed = ADC_HIGH_SPEED; - static constexpr uint32_t kAdcValueShift = kAdcSmoothBits; @@ -45,13 +49,9 @@ class ADC { }; static void Init(CalibrationData *calibration_data); - - // Read the value of the last conversion and update current channel, then - // start the next conversion. If necessary, some channels could be given - // priority by scanning them more often. Even better might be some kind of - // continuous/DMA sampling to make things even more independent of the main - // ISR timing restrictions. - static void Scan(); + static void Init_DMA(); + static void DMA_ISR(); + static void Scan_DMA(); template static int32_t value() { @@ -79,23 +79,10 @@ class ADC { return (value * calibration_data_->pitch_cv_scale) >> 12; } -#ifdef ENABLE_ADC_DEBUG - // DEBUG - static uint16_t fail_flag0() { - return adc_.adc0->fail_flag; - } - - static uint16_t fail_flag1() { - return adc_.adc1->fail_flag; - } - - static uint32_t busy_waits() { - return busy_waits_; - } -#endif - static void CalibratePitch(int32_t c2, int32_t c4); + static float Read_ID_Voltage(); + private: template @@ -108,15 +95,25 @@ class ADC { } static ::ADC adc_; +#ifdef OC_ADC_ENABLE_DMA_INTERRUPT + static volatile bool ready_; +#endif static size_t scan_channel_; static CalibrationData *calibration_data_; static uint32_t raw_[ADC_CHANNEL_LAST]; static uint32_t smoothed_[ADC_CHANNEL_LAST]; -#ifdef ENABLE_ADC_DEBUG - static volatile uint32_t busy_waits_; -#endif + /* + * below: channel ids for the ADCx_SCA register: we have 4 inputs + * CV1 (19) = A5 = 0x4C; CV2 (18) = A4 = 0x4D; CV3 (20) = A6 = 0x46; CV4 (17) = A3 = 0x49 + * for some reason the IDs must be in order: CV2, CV3, CV4, CV1 resp. (when flipped) CV3, CV2, CV1, CV4 + */ + #ifdef FLIP_180 + static constexpr uint16_t SCA_CHANNEL_ID[DMA_NUM_CH] = { 0x46, 0x4D, 0x4C, 0x49 }; + #else + static constexpr uint16_t SCA_CHANNEL_ID[DMA_NUM_CH] = { 0x4D, 0x46, 0x49, 0x4C }; + #endif }; }; diff --git a/software/o_c_REV/OC_DAC.cpp b/software/o_c_REV/OC_DAC.cpp index 7b899130c..58695f487 100644 --- a/software/o_c_REV/OC_DAC.cpp +++ b/software/o_c_REV/OC_DAC.cpp @@ -40,11 +40,18 @@ #include "OC_calibration.h" #include "OC_autotune_presets.h" #include "OC_autotune.h" +#if defined(__IMXRT1062__) +#include +#endif #define SPICLOCK_30MHz (SPI_CTAR_PBR(0) | SPI_CTAR_BR(0) | SPI_CTAR_DBR) //(60 / 2) * ((1+1)/2) = 30 MHz (= 24MHz, when F_BUS == 48000000) namespace OC { +#ifdef VOR +int DAC::kOctaveZero = 0; +#endif + /*static*/ void DAC::Init(CalibrationData *calibration_data) { @@ -53,21 +60,35 @@ void DAC::Init(CalibrationData *calibration_data) { restore_scaling(0x0); // set up DAC pins - pinMode(DAC_CS, OUTPUT); - pinMode(DAC_RST,OUTPUT); + OC::pinMode(DAC_CS, OUTPUT); + + // set Vbias, using onboard DAC - does nothing on non-VOR hardware + init_Vbias(); + set_Vbias(2760); // default to Asymmetric + delay(10); +#ifndef VOR + // VOR button uses the same pin as DAC_RST + OC::pinMode(DAC_RST,OUTPUT); #ifdef DAC8564 // A0 = 0, A1 = 0 digitalWrite(DAC_RST, LOW); #else // default to DAC8565 - pull RST high digitalWrite(DAC_RST, HIGH); #endif +#endif history_tail_ = 0; memset(history_, 0, sizeof(uint16_t) * kHistoryDepth * DAC_CHANNEL_LAST); +#if defined(__MK20DX256__) if (F_BUS == 60000000 || F_BUS == 48000000) SPIFIFO.begin(DAC_CS, SPICLOCK_30MHz, SPI_MODE0); +#elif defined(__IMXRT1062__) + SPI.begin(); + IOMUXC_SW_MUX_CTL_PAD_GPIO_B0_00 = 3; // DAC CS pin controlled by SPI +#endif + set_all(0xffff); Update(); } @@ -193,6 +214,30 @@ uint32_t DAC::store_scaling() { _scaling |= (DAC_scaling[i] << (i * 8)); return _scaling; } + +#if defined(__MK20DX256__) +/*static*/ +void DAC::init_Vbias() { + /* using MK20 DAC0 for Vbias*/ + VREF_TRM = 0x60; VREF_SC = 0xE1; // enable 1v2 reference + SIM_SCGC2 |= SIM_SCGC2_DAC0; // DAC clock + DAC0_C0 = DAC_C0_DACEN; // enable module + use internal 1v2 reference +} +/*static*/ +void DAC::set_Vbias(uint32_t data) { + *(volatile int16_t *)&(DAC0_DAT0L) = data; +} + +#elif defined(__IMXRT1062__) +void DAC::init_Vbias() { + // TODO Teensy 4.1 +} +void DAC::set_Vbias(uint32_t data) { + // TODO Teensy 4.1 +} + +#endif + /*static*/ DAC::CalibrationData *DAC::calibration_data_ = nullptr; /*static*/ @@ -205,6 +250,7 @@ volatile size_t DAC::history_tail_; uint8_t DAC::DAC_scaling[DAC_CHANNEL_LAST]; }; // namespace OC +#if defined(__MK20DX256__) void set8565_CHA(uint32_t data) { #ifdef BUCHLA_cOC uint32_t _data = data; @@ -269,8 +315,69 @@ void set8565_CHD(uint32_t data) { SPIFIFO.read(); } +#elif defined(__IMXRT1062__) // Teensy 4.1 +void set8565_CHA(uint32_t data) { + LPSPI4_TCR = (LPSPI4_TCR & 0xF8000000) | LPSPI_TCR_FRAMESZ(23) + | LPSPI_TCR_PCS(0) | LPSPI_TCR_RXMSK; + #ifdef BUCHLA_cOC + uint32_t _data = data; + #else + uint32_t _data = OC::DAC::MAX_VALUE - data; + #endif + #ifdef FLIP_180 + _data = (0b00010110 << 16) | (_data & 0xFFFF); + #else + _data = (0b00010000 << 16) | (_data & 0xFFFF); + #endif + LPSPI4_TDR = _data; +} + +void set8565_CHB(uint32_t data) { + #ifdef BUCHLA_cOC + uint32_t _data = data; + #else + uint32_t _data = OC::DAC::MAX_VALUE - data; + #endif + #ifdef FLIP_180 + _data = (0b00010100 << 16) | (_data & 0xFFFF); + #else + _data = (0b00010010 << 16) | (_data & 0xFFFF); + #endif + LPSPI4_TDR = _data; +} +void set8565_CHC(uint32_t data) { + #ifdef BUCHLA_cOC + uint32_t _data = data; + #else + uint32_t _data = OC::DAC::MAX_VALUE - data; + #endif + #ifdef FLIP_180 + _data = (0b00010010 << 16) | (_data & 0xFFFF); + #else + _data = (0b00010100 << 16) | (_data & 0xFFFF); + #endif + LPSPI4_TDR = _data; +} +void set8565_CHD(uint32_t data) { + #ifdef BUCHLA_cOC + uint32_t _data = data; + #else + uint32_t _data = OC::DAC::MAX_VALUE - data; + #endif + #ifdef FLIP_180 + _data = (0b00010000 << 16) | (_data & 0xFFFF); + #else + _data = (0b00010110 << 16) | (_data & 0xFFFF); + #endif + LPSPI4_SR = LPSPI_SR_TCF; // clear transmit complete flag before last write to FIFO + LPSPI4_TDR = _data; +} + +#endif // __IMXRT1062__ + // adapted from https://github.com/xxxajk/spi4teensy3 (MISO disabled) : +#if defined(__MK20DX256__) void SPI_init() { uint32_t ctar0, ctar1; @@ -303,4 +410,12 @@ void SPI_init() { SPI0_MCR = mcr; } } + +#elif defined(__IMXRT1062__) +void SPI_init() { + // TODO Teensy 4.1 +} + +#endif // __IMXRT1062__ + // OC_DAC diff --git a/software/o_c_REV/OC_DAC.h b/software/o_c_REV/OC_DAC.h index 219b1a8e7..a3a300c80 100644 --- a/software/o_c_REV/OC_DAC.h +++ b/software/o_c_REV/OC_DAC.h @@ -41,6 +41,11 @@ class DAC { #ifdef BUCHLA_4U static constexpr int kOctaveZero = 0; + #elif defined(VOR) + static int kOctaveZero; + static constexpr int VBiasUnipolar = 3900; // onboard DAC @ Vref 1.2V (internal), 1.75x gain + static constexpr int VBiasBipolar = 2000; // onboard DAC @ Vref 1.2V (internal), 1.75x gain + static constexpr int VBiasAsymmetric = 2760; // onboard DAC @ Vref 1.2V (internal), 1.75x gain #else static constexpr int kOctaveZero = 3; #endif @@ -62,6 +67,8 @@ class DAC { static void restore_scaling(uint32_t scaling); static uint8_t get_voltage_scaling(uint8_t channel_id); static uint32_t store_scaling(); + static void set_Vbias(uint32_t data); + static void init_Vbias(); static void set_all(uint32_t value) { for (int i = DAC_CHANNEL_A; i < DAC_CHANNEL_LAST; ++i) diff --git a/software/o_c_REV/OC_apps.h b/software/o_c_REV/OC_apps.h index 4744e429e..7d8e146ff 100644 --- a/software/o_c_REV/OC_apps.h +++ b/software/o_c_REV/OC_apps.h @@ -24,6 +24,7 @@ #define OC_APP_H_ #include "UI/ui_events.h" +#include "util/util_turing.h" #include "util/util_misc.h" namespace OC { @@ -83,6 +84,9 @@ namespace apps { }; // namespace apps +void draw_save_message(uint8_t c); +void save_app_data(); + }; // namespace OC #endif // OC_APP_H_ diff --git a/software/o_c_REV/OC_apps.ino b/software/o_c_REV/OC_apps.ino index 9e5462453..dab8e2f86 100644 --- a/software/o_c_REV/OC_apps.ino +++ b/software/o_c_REV/OC_apps.ino @@ -23,6 +23,9 @@ #include "OC_apps.h" #include "OC_digital_inputs.h" #include "OC_autotune.h" +#include "OC_patterns.h" +#include "enigma/TuringMachine.h" +#include "src/drivers/FreqMeasure/OC_FreqMeasure.h" #define DECLARE_APP(a, b, name, prefix) \ { TWOCC::value, name, \ @@ -35,7 +38,59 @@ } OC::App available_apps[] = { + + #ifdef ENABLE_APP_CALIBR8OR + DECLARE_APP('C','8', "Calibr8or", Calibr8or), + #endif + #ifdef ENABLE_APP_SCENES + DECLARE_APP('S','X', "Scenes", ScenesApp), + #endif + DECLARE_APP('H','S', "Hemisphere", HEMISPHERE), + #ifdef ENABLE_APP_ASR + DECLARE_APP('A','S', "CopierMaschine", ASR), + #endif + #ifdef ENABLE_APP_H1200 + DECLARE_APP('H','A', "Harrington 1200", H1200), + #endif + #ifdef ENABLE_APP_AUTOMATONNETZ + DECLARE_APP('A','T', "Automatonnetz", Automatonnetz), + #endif + #ifdef ENABLE_APP_QUANTERMAIN + DECLARE_APP('Q','Q', "Quantermain", QQ), + #endif + #ifdef ENABLE_APP_METAQ + DECLARE_APP('M','!', "Meta-Q", DQ), + #endif + #ifdef ENABLE_APP_POLYLFO + DECLARE_APP('P','L', "Quadraturia", POLYLFO), + #endif + #ifdef ENABLE_APP_LORENZ + DECLARE_APP('L','R', "Low-rents", LORENZ), + #endif + #ifdef ENABLE_APP_PIQUED + DECLARE_APP('E','G', "Piqued", ENVGEN), + #endif + #ifdef ENABLE_APP_SEQUINS + DECLARE_APP('S','Q', "Sequins", SEQ), + #endif + #ifdef ENABLE_APP_BBGEN + DECLARE_APP('B','B', "Dialectic Pong", BBGEN), + #endif + #ifdef ENABLE_APP_BYTEBEATGEN + DECLARE_APP('B','Y', "Viznutcracker", BYTEBEATGEN), + #endif + #ifdef ENABLE_APP_CHORDS + DECLARE_APP('A','C', "Acid Curds", CHORDS), + #endif + #ifdef ENABLE_APP_FPART + DECLARE_APP('F','P', "4 Parts", FPART), + #endif + #ifdef ENABLE_APP_PASSENCORE + // boring name version + // DECLARE_APP('P','Q', "Tension", PASSENCORE), + DECLARE_APP('P','Q', "Passencore", PASSENCORE), + #endif #ifdef ENABLE_APP_MIDI DECLARE_APP('M','I', "Captain MIDI", MIDI), #endif @@ -53,6 +108,9 @@ OC::App available_apps[] = { #ifdef ENABLE_APP_PONG DECLARE_APP('P','O', "Pong", PONGGAME), #endif + #ifdef ENABLE_APP_REFERENCES + DECLARE_APP('R','F', "References", REFS), + #endif DECLARE_APP('B','R', "Backup / Restore", Backup), DECLARE_APP('S','E', "Setup / About", Settings), }; @@ -71,7 +129,7 @@ struct GlobalSettings { bool reserved1; uint32_t DAC_scaling; uint16_t current_app_id; - + OC::Scale user_scales[OC::Scales::SCALE_USER_LAST]; OC::Pattern user_patterns[OC::Patterns::PATTERN_USER_ALL]; HS::TuringMachine user_turing_machines[HS::TURING_MACHINE_COUNT]; @@ -119,7 +177,7 @@ void save_global_settings() { memcpy(global_settings.auto_calibration_data, OC::auto_calibration_data, sizeof(OC::auto_calibration_data)); // scaling settings: global_settings.DAC_scaling = OC::DAC::store_scaling(); - + global_settings_storage.Save(global_settings); SERIAL_PRINTLN("Saved global settings: page_index %d", global_settings_storage.page_index()); } @@ -208,6 +266,10 @@ namespace apps { void set_current_app(int index) { current_app = &available_apps[index]; global_settings.current_app_id = current_app->id; + #ifdef VOR + VBiasManager *vbias_m = vbias_m->get(); + vbias_m->SetStateForApp(current_app); + #endif } App *current_app = &available_apps[DEFAULT_APP_INDEX]; @@ -238,7 +300,7 @@ void Init(bool reset_settings) { global_settings.encoders_enable_acceleration = OC_ENCODERS_ENABLE_ACCELERATION_DEFAULT; global_settings.reserved0 = false; global_settings.reserved1 = false; - global_settings.DAC_scaling = VOLTAGE_SCALING_1V_PER_OCT; + global_settings.DAC_scaling = VOLTAGE_SCALING_1V_PER_OCT; if (reset_settings) { if (ui.ConfirmReset()) { @@ -322,13 +384,19 @@ void draw_app_menu(const menu::ScreenCursor<5> &cursor) { item.SetPrintPos(); graphics.movePrintPos(weegfx::Graphics::kFixedFontW, 0); graphics.print(available_apps[current].name); -// if (global_settings.current_app_id == available_apps[current].id) -// graphics.drawBitmap8(item.x + 2, item.y + 1, 4, bitmap_indicator_4x8); + + // if (global_settings.current_app_id == available_apps[current].id) + // graphics.drawBitmap8(item.x + 2, item.y + 1, 4, bitmap_indicator_4x8); graphics.drawBitmap8(0, item.y + 1, 8, global_settings.current_app_id == available_apps[current].id ? CHECK_ON_ICON : CHECK_OFF_ICON); item.DrawCustom(); } +#ifdef VOR + VBiasManager *vbias_m = vbias_m->get(); + vbias_m->DrawPopupPerhaps(); +#endif + GRAPHICS_END_FRAME(); } @@ -360,22 +428,44 @@ void Ui::AppSettings() { if (IgnoreEvent(event)) continue; - if (UI::EVENT_ENCODER == event.type && CONTROL_ENCODER_R == event.control) { - cursor.Scroll(event.value); - } else if (CONTROL_BUTTON_R == event.control) { + switch (event.control) { + case CONTROL_ENCODER_R: + if (UI::EVENT_ENCODER == event.type) + cursor.Scroll(event.value); + break; + + case CONTROL_BUTTON_R: save = event.type == UI::EVENT_BUTTON_LONG_PRESS; - change_app = true; - } else if (CONTROL_BUTTON_L == event.control) { - ui.DebugStats(); - } else if (CONTROL_BUTTON_UP == event.control) { - bool enabled = !global_settings.encoders_enable_acceleration; - SERIAL_PRINTLN("Encoder acceleration: %s", enabled ? "enabled" : "disabled"); - ui.encoders_enable_acceleration(enabled); - global_settings.encoders_enable_acceleration = enabled; + change_app = event.type != UI::EVENT_BUTTON_DOWN; // true on button release + break; + case CONTROL_BUTTON_L: + if (UI::EVENT_BUTTON_PRESS == event.type) + ui.DebugStats(); + break; + case CONTROL_BUTTON_UP: +#ifdef VOR + // VBias menu for units without Range button + if (UI::EVENT_BUTTON_LONG_PRESS == event.type || UI::EVENT_BUTTON_DOWN == event.type) { + VBiasManager *vbias_m = vbias_m->get(); + vbias_m->AdvanceBias(); + } +#endif + break; + case CONTROL_BUTTON_DOWN: + if (UI::EVENT_BUTTON_PRESS == event.type) { + bool enabled = !global_settings.encoders_enable_acceleration; + SERIAL_PRINTLN("Encoder acceleration: %s", enabled ? "enabled" : "disabled"); + ui.encoders_enable_acceleration(enabled); + global_settings.encoders_enable_acceleration = enabled; + } + break; + + default: break; } } draw_app_menu(cursor); + delay(2); // VOR calibration hack } event_queue_.Flush(); @@ -417,10 +507,10 @@ bool Ui::ConfirmReset() { UI::Event event = event_queue_.PullEvent(); if (IgnoreEvent(event)) continue; - if (CONTROL_BUTTON_R == event.control) { + if (CONTROL_BUTTON_R == event.control && UI::EVENT_BUTTON_PRESS == event.type) { confirm = true; done = true; - } else if (CONTROL_BUTTON_L == event.control) { + } else if (CONTROL_BUTTON_L == event.control && UI::EVENT_BUTTON_PRESS == event.type) { confirm = false; done = true; } diff --git a/software/o_c_REV/OC_autotuner.h b/software/o_c_REV/OC_autotuner.h index 543817a4f..abe2d87d4 100644 --- a/software/o_c_REV/OC_autotuner.h +++ b/software/o_c_REV/OC_autotuner.h @@ -3,17 +3,71 @@ #include "OC_autotune.h" #include "OC_options.h" +#include "OC_visualfx.h" -#ifdef BUCHLA_4U +// autotune constants: +#ifdef VOR +static constexpr int ACTIVE_OCTAVES = OCTAVES; +#else +static constexpr int ACTIVE_OCTAVES = OCTAVES - 1; +#endif + +static constexpr size_t kHistoryDepth = 10; + +#define ZERO_OFFSET (5 - OC::DAC::kOctaveZero) + +#define FREQ_MEASURE_TIMEOUT 512 +#define ERROR_TIMEOUT (FREQ_MEASURE_TIMEOUT << 0x4) +#define MAX_NUM_PASSES 1500 +#define CONVERGE_PASSES 5 + +#if defined(BUCHLA_4U) && !defined(IO_10V) const char* const AT_steps[] = { + " ", " ", " ", " ", " ", // 5 blanks "0.0V", "1.2V", "2.4V", "3.6V", "4.8V", "6.0V", "7.2V", "8.4V", "9.6V", "10.8V", " " }; #else const char* const AT_steps[] = { - "-3V", "-2V", "-1V", " 0V", "+1V", "+2V", "+3V", "+4V", "+5V", "+6V", " " + "-5V", "-4V", "-3V", "-2V", "-1V", " 0V", "+1V", "+2V", "+3V", "+4V", "+5V", "+6V", "+7V", "+8V", "+9V", "+10V", " " }; #endif +// +#ifdef BUCHLA_4U + constexpr float target_multipliers[OCTAVES] = { 1.0f, 2.0f, 4.0f, 8.0f, 16.0f, 32.0f, 64.0f, 128.0f, 256.0f, 512.0f }; +#else + constexpr float target_multipliers[OCTAVES + 6] = { 0.03125f, 0.0625f, 0.125f, 0.25f, 0.5f, 1.0f, 2.0f, 4.0f, 8.0f, 16.0f, 32.0f, 64.0f, 128.0f, 256.0f, 512.0f, 1024.0f }; +// constexpr float target_multipliers[OCTAVES] = { 0.125f, 0.25f, 0.5f, 1.0f, 2.0f, 4.0f, 8.0f, 16.0f, 32.0f, 64.0f }; +#endif + +#ifdef BUCHLA_SUPPORT + constexpr float target_multipliers_1V2[OCTAVES] = { + 0.1767766952966368931843f, + 0.3149802624737182976666f, + 0.5612310241546865086093f, + 1.0f, + 1.7817974362806785482150f, + 3.1748021039363991668836f, + 5.6568542494923805818985f, + 10.0793683991589855253324f, + 17.9593927729499718282113f, + 32.0f + }; + + constexpr float target_multipliers_2V0[OCTAVES] = { + 0.3535533905932737863687f, + 0.5f, + 0.7071067811865475727373f, + 1.0f, + 1.4142135623730951454746f, + 2.0f, + 2.8284271247461902909492f, + 4.0f, + 5.6568542494923805818985f, + 8.0f + }; +#endif + namespace OC { enum AUTO_MENU_ITEMS { @@ -26,6 +80,7 @@ enum AUTO_MENU_ITEMS { enum AT_STATUS { AT_OFF, AT_READY, + AT_WAIT, AT_RUN, AT_ERROR, AT_DONE, @@ -36,6 +91,7 @@ enum AUTO_CALIBRATION_STEP { DAC_VOLT_0_ARM, DAC_VOLT_0_BASELINE, DAC_VOLT_TARGET_FREQUENCIES, + DAC_VOLT_WAIT, DAC_VOLT_3m, DAC_VOLT_2m, DAC_VOLT_1m, @@ -46,6 +102,9 @@ enum AUTO_CALIBRATION_STEP { DAC_VOLT_4, DAC_VOLT_5, DAC_VOLT_6, +#ifdef VOR + DAC_VOLT_7, +#endif AUTO_CALIBRATION_STEP_LAST }; @@ -58,9 +117,28 @@ class Autotuner { owner_ = nullptr; cursor_pos_ = 0; data_select_ = 0; - channel_ = 0; + channel_ = DAC_CHANNEL_A; calibration_data_ = 0; - auto_tune_running_status_ = 0; + run_status_ = AT_OFF; + + // moved from REFS + armed_ = false; + step_ = OC::DAC_VOLT_0_ARM; + auto_DAC_offset_error_ = 0; + auto_frequency_ = 0; + auto_last_frequency_ = 0; + auto_freq_sum_ = 0; + auto_freq_count_ = 0; + ready_ = 0; + ticks_since_last_freq_ = 0; + completed_ = false; + F_correction_factor_ = 0xFF; + correction_direction_ = false; + correction_cnt_positive_ = 0x0; + correction_cnt_negative_ = 0x0; + reset_calibration_data(); + + history_.Init(0x0); } bool active() const { @@ -73,6 +151,11 @@ class Autotuner { Begin(); } + void Reset() { + reset_autotuner(); + } + + void ISR(); void Close(); void Draw(); void HandleButtonEvent(const UI::Event &event); @@ -83,9 +166,33 @@ class Autotuner { Owner *owner_; size_t cursor_pos_; size_t data_select_; - int8_t channel_; + DAC_CHANNEL channel_; uint8_t calibration_data_; - uint8_t auto_tune_running_status_; + AT_STATUS run_status_; + + // moved from References + OC::vfx::ScrollingHistory history_; + + bool armed_; + uint8_t step_; + int32_t auto_DAC_offset_error_; + uint32_t auto_frequency_; + uint32_t auto_target_frequencies_[OCTAVES + 1]; + int16_t auto_calibration_data_[OCTAVES + 1]; + uint32_t auto_last_frequency_; + bool error_; + bool ready_; + bool completed_; + uint32_t auto_freq_sum_; + uint32_t auto_freq_count_; + uint32_t ticks_since_last_freq_; + uint32_t auto_num_passes_; + uint16_t F_correction_factor_; + bool correction_direction_; + int16_t correction_cnt_positive_; + int16_t correction_cnt_negative_; + int16_t octaves_cnt_; + // ----- void Begin(); void move_cursor(int offset); @@ -93,8 +200,331 @@ class Autotuner { void handleButtonLeft(const UI::Event &event); void handleButtonUp(const UI::Event &event); void handleButtonDown(const UI::Event &event); + + + void autotuner_arm(uint8_t _status) { + reset_autotuner(); + armed_ = _status ? true : false; + } + + void autotuner_run() { + SERIAL_PRINTLN("Starting autotuner..."); + step_ = armed_ ? OC::DAC_VOLT_0_BASELINE : OC::DAC_VOLT_0_ARM; + if (step_ == OC::DAC_VOLT_0_BASELINE) + // we start, so reset data to defaults: + OC::DAC::set_default_channel_calibration_data(channel_); + } + + void auto_next_step() { + SERIAL_PRINTLN("Autotuner reset step..."); + ticks_since_last_freq_ = 0x0; + auto_num_passes_ = 0x0; + auto_DAC_offset_error_ = 0x0; + correction_direction_ = false; + correction_cnt_positive_ = correction_cnt_negative_ = 0x0; + F_correction_factor_ = 0xFF; + ready_ = false; + step_++; + } + + void reset_autotuner() { + ticks_since_last_freq_ = 0x0; + auto_frequency_ = 0x0; + auto_last_frequency_ = 0x0; + error_ = 0x0; + ready_ = 0x0; + armed_ = 0x0; + step_ = 0x0; + F_correction_factor_ = 0xFF; + correction_direction_ = false; + correction_cnt_positive_ = 0x0; + correction_cnt_negative_ = 0x0; + octaves_cnt_ = 0x0; + auto_num_passes_ = 0x0; + auto_DAC_offset_error_ = 0x0; + completed_ = 0x0; + reset_calibration_data(); + } + + void reset_calibration_data() { + + for (int i = 0; i < OCTAVES + 1; i++) { + auto_calibration_data_[i] = 0; + auto_target_frequencies_[i] = 0.0f; + } + } + + uint8_t data_available() { + return OC::DAC::calibration_data_used(channel_); + } + + void use_default() { + OC::DAC::set_default_channel_calibration_data(channel_); + } + + void use_auto_calibration() { + OC::DAC::set_auto_channel_calibration_data(channel_); + } + + bool auto_frequency() { + + bool _f_result = false; + + if (ticks_since_last_freq_ > ERROR_TIMEOUT) { + error_ = true; + } + + if (FreqMeasure.available()) { + + auto_freq_sum_ = auto_freq_sum_ + FreqMeasure.read(); + auto_freq_count_ = auto_freq_count_ + 1; + + // take more time as we're converging toward the target frequency + uint32_t _wait = (F_correction_factor_ == 0x1) ? (FREQ_MEASURE_TIMEOUT << 2) : (FREQ_MEASURE_TIMEOUT >> 2); + + if (ticks_since_last_freq_ > _wait) { + + // store frequency, reset, and poke ui to preempt screensaver: + auto_frequency_ = uint32_t(FreqMeasure.countToFrequency(auto_freq_sum_ / auto_freq_count_) * 1000); + history_.Push(auto_frequency_); + auto_freq_sum_ = 0; + ready_ = true; + auto_freq_count_ = 0; + _f_result = true; + ticks_since_last_freq_ = 0x0; + OC::ui._Poke(); + history_.Update(); + } + } + return _f_result; + } + + void measure_frequency_and_calc_error() { + + switch(step_) { + + case OC::DAC_VOLT_0_ARM: + case OC::DAC_VOLT_WAIT: + // do nothing + break; + case OC::DAC_VOLT_0_BASELINE: + // 0V baseline / calibration point: in this case, we don't correct. + { + bool _update = auto_frequency(); + if (_update && auto_num_passes_ > kHistoryDepth) { + + auto_last_frequency_ = auto_frequency_; + uint32_t history[kHistoryDepth]; + uint32_t average = 0; + // average + history_.Read(history); + for (uint8_t i = 0; i < kHistoryDepth; i++) + average += history[i]; + // ... and derive target frequency at 0V + auto_frequency_ = ((auto_frequency_ + average) / (kHistoryDepth + 1)); // 0V + SERIAL_PRINTLN("Baseline auto_frequency_ = %4.d", auto_frequency_); + // reset step, and proceed: + auto_next_step(); + // wait for user to patch output to oscillator V/Oct + run_status_ = AT_WAIT; + } + else if (_update) + auto_num_passes_++; + } + break; + case OC::DAC_VOLT_TARGET_FREQUENCIES: + { + #ifdef BUCHLA_SUPPORT + + switch(OC::DAC::get_voltage_scaling(channel_)) { + + case VOLTAGE_SCALING_1_2V_PER_OCT: // 1.2V/octave + auto_target_frequencies_[octaves_cnt_] = auto_frequency_ * target_multipliers_1V2[octaves_cnt_]; + break; + case VOLTAGE_SCALING_2V_PER_OCT: // 2V/octave + auto_target_frequencies_[octaves_cnt_] = auto_frequency_ * target_multipliers_2V0[octaves_cnt_]; + break; + default: // 1V/octave + auto_target_frequencies_[octaves_cnt_] = auto_frequency_ * target_multipliers[octaves_cnt_]; + break; + } + #else + auto_target_frequencies_[octaves_cnt_] = auto_frequency_ * target_multipliers[octaves_cnt_ + ZERO_OFFSET]; + #endif + octaves_cnt_++; + // go to next step, if done: + if (octaves_cnt_ > ACTIVE_OCTAVES) { + octaves_cnt_ = 0x0; + step_++; + } + } + break; + case OC::DAC_VOLT_3m: + case OC::DAC_VOLT_2m: + case OC::DAC_VOLT_1m: + case OC::DAC_VOLT_0: + case OC::DAC_VOLT_1: + case OC::DAC_VOLT_2: + case OC::DAC_VOLT_3: + case OC::DAC_VOLT_4: + case OC::DAC_VOLT_5: + case OC::DAC_VOLT_6: + #ifdef VOR + case OC::DAC_VOLT_7: + #endif + { + bool _update = auto_frequency(); + + if (_update && (auto_num_passes_ > MAX_NUM_PASSES)) { + /* target frequency reached */ + SERIAL_PRINTLN("* Target Frequency Reached *"); + + int step_idx = step_ - OC::DAC_VOLT_3m; + + // if things don't seem to double ... we've hit the ceiling. + if ((step_ > OC::DAC_VOLT_2m) && (auto_last_frequency_ * 1.25f > auto_frequency_)) + step_ = OC::AUTO_CALIBRATION_STEP_LAST - 1; + + // average: + uint32_t history[kHistoryDepth]; + uint32_t average = 0; + history_.Read(history); + for (uint8_t i = 0; i < kHistoryDepth; i++) + average += history[i]; + + // store last frequency: + auto_last_frequency_ = (auto_frequency_ + average) / (kHistoryDepth + 1); + // and DAC correction value: + auto_calibration_data_[step_idx] = auto_DAC_offset_error_; + + // reset and step forward + auto_next_step(); + } + else if (_update) + { + auto_num_passes_++; // count passes + + SERIAL_PRINTLN("auto_target_frequencies[%3d]_ = %3d", step_ - OC::DAC_VOLT_3m, + auto_target_frequencies_[step_ - OC::DAC_VOLT_3m] ); + SERIAL_PRINTLN("auto_frequency_ = %3d", auto_frequency_); + + // and correct frequency + if (auto_target_frequencies_[step_ - OC::DAC_VOLT_3m] > auto_frequency_) + { + // update correction factor? + if (!correction_direction_) + F_correction_factor_ = (F_correction_factor_ >> 1) | 1u; + + correction_direction_ = true; + + auto_DAC_offset_error_ += F_correction_factor_; + + // we're converging -- count passes, so we can stop after x attempts: + if (F_correction_factor_ == 0x1) correction_cnt_positive_++; + } + else if (auto_target_frequencies_[step_ - OC::DAC_VOLT_3m] < auto_frequency_) + { + // update correction factor? + if (correction_direction_) + F_correction_factor_ = (F_correction_factor_ >> 1) | 1u; + + correction_direction_ = false; + + auto_DAC_offset_error_ -= F_correction_factor_; + + // we're converging -- count passes, so we can stop after x attempts: + if (F_correction_factor_ == 0x1) correction_cnt_negative_++; + } + + SERIAL_PRINTLN("auto_DAC_offset_error_ = %3d", auto_DAC_offset_error_); + + // approaching target? if so, go to next step. + if (correction_cnt_positive_ > CONVERGE_PASSES && correction_cnt_negative_ > CONVERGE_PASSES) + auto_num_passes_ = MAX_NUM_PASSES << 1; + } + } + break; + case OC::AUTO_CALIBRATION_STEP_LAST: + // step through the octaves: + if (ticks_since_last_freq_ > 2000) { + int32_t new_auto_calibration_point = OC::calibration_data.dac.calibrated_octaves[channel_][octaves_cnt_] + auto_calibration_data_[octaves_cnt_]; + // write to DAC and update data + if (new_auto_calibration_point >= 65536 || new_auto_calibration_point < 0) { + error_ = true; + step_++; + } + else { + OC::DAC::set(channel_, new_auto_calibration_point); + OC::DAC::update_auto_channel_calibration_data(channel_, octaves_cnt_, new_auto_calibration_point); + ticks_since_last_freq_ = 0x0; + octaves_cnt_++; + } + } + // then stop ... + if (octaves_cnt_ > OCTAVES) { + completed_ = true; + // and point to auto data ... + OC::DAC::set_auto_channel_calibration_data(channel_); + step_++; + } + break; + default: + step_ = OC::DAC_VOLT_0_ARM; + armed_ = 0x0; + break; + } + } + + void updateDAC() { + + switch(step_) { + + case OC::DAC_VOLT_0_ARM: + { + F_correction_factor_ = 0x1; // don't go so fast + auto_frequency(); + OC::DAC::set(channel_, OC::calibration_data.dac.calibrated_octaves[channel_][OC::DAC::kOctaveZero]); + } + break; + case OC::DAC_VOLT_0_BASELINE: + // set DAC to 0.000V, default calibration: + OC::DAC::set(channel_, OC::calibration_data.dac.calibrated_octaves[channel_][OC::DAC::kOctaveZero]); + break; + case OC::DAC_VOLT_TARGET_FREQUENCIES: + case OC::DAC_VOLT_WAIT: + case OC::AUTO_CALIBRATION_STEP_LAST: + // do nothing + break; + default: + // set DAC to calibration point + error + { + int32_t _default_calibration_point = OC::calibration_data.dac.calibrated_octaves[channel_][step_ - OC::DAC_VOLT_3m]; + OC::DAC::set(channel_, _default_calibration_point + auto_DAC_offset_error_); + } + break; + } + } }; + template + void Autotuner::ISR() { + if (armed_) { + updateDAC(); + measure_frequency_and_calc_error(); + ticks_since_last_freq_++; + } + + if (error_) { + run_status_ = AT_ERROR; + reset_autotuner(); + } + else if (completed_) { + run_status_ = AT_DONE; + completed_ = false; + } + + } + template void Autotuner::Draw() { @@ -103,22 +533,13 @@ class Autotuner { weegfx::coord_t y = 0; weegfx::coord_t h = 64; - graphics.clearRect(x, y, w, h); + //graphics.clearRect(x, y, w, h); graphics.drawFrame(x, y, w, h); graphics.setPrintPos(x + 2, y + 3); graphics.print(OC::Strings::channel_id[channel_]); x = 16; y = 15; - if (owner_->autotuner_error()) { - auto_tune_running_status_ = AT_ERROR; - owner_->reset_autotuner(); - } - else if (owner_->autotuner_completed()) { - auto_tune_running_status_ = AT_DONE; - owner_->autotuner_reset_completed(); - } - for (size_t i = 0; i < (AUTO_MENU_ITEMS_LAST - 0x1); ++i, y += 20) { // graphics.setPrintPos(x + 2, y + 4); @@ -140,7 +561,7 @@ class Autotuner { } else if (i == AUTOTUNE) { - switch (auto_tune_running_status_) { + switch (run_status_) { //to display progress, if running case AT_OFF: graphics.print("run --> "); @@ -148,28 +569,41 @@ class Autotuner { break; case AT_READY: { graphics.print("arm > "); - float _freq = owner_->get_auto_frequency(); - if (_freq == 0.0f) - graphics.printf("wait ..."); + const uint32_t _freq = auto_frequency_; + if (_freq == 0) + graphics.print("wait ..."); else - graphics.printf("%7.3f", _freq); + { + const uint32_t value = _freq / 1000; + const uint32_t cents = _freq % 1000; + graphics.printf("%4u.%03u", value, cents); } + } + break; + case AT_WAIT: + graphics.print("Patch V/Oct"); + graphics.print(" now!"); break; case AT_RUN: { - int _octave = owner_->get_octave_cnt(); + int _octave = octaves_cnt_ + 0x1; if (_octave > 1 && _octave < OCTAVES) { for (int i = 0; i <= _octave; i++, x += 6) graphics.drawBitmap8(x + 18, y + 4, 4, OC::bitmap_indicator_4x8); } - else if (owner_->auto_tune_step() == DAC_VOLT_0_BASELINE || owner_->auto_tune_step() == DAC_VOLT_TARGET_FREQUENCIES) // this goes too quick, so ... + else if (step_ == DAC_VOLT_0_BASELINE || step_ == DAC_VOLT_TARGET_FREQUENCIES) // this goes too quick, so ... graphics.print(" 0.0V baseline"); else { - graphics.print(AT_steps[owner_->auto_tune_step() - DAC_VOLT_3m]); - if (!owner_->_ready()) + graphics.print(AT_steps[step_ - DAC_VOLT_3m + ZERO_OFFSET]); + if (!ready_) graphics.print(" "); else - graphics.printf(" > %7.3f", owner_->get_auto_frequency()); + { + const uint32_t f = auto_frequency_; + const uint32_t value = f / 1000; + const uint32_t cents = f % 1000; + graphics.printf(" > %4u.%03u", value, cents); + } } } break; @@ -180,7 +614,7 @@ class Autotuner { case AT_DONE: graphics.print(OC::Strings::channel_id[channel_]); graphics.print(" --> a-ok!"); - calibration_data_ = owner_->data_available(); + calibration_data_ = data_available(); break; default: break; @@ -213,7 +647,7 @@ class Autotuner { handleButtonLeft(event); break; case OC::CONTROL_BUTTON_R: - owner_->reset_autotuner(); + reset_autotuner(); Close(); break; default: @@ -254,16 +688,16 @@ class Autotuner { template void Autotuner::move_cursor(int offset) { - if (auto_tune_running_status_ < AT_RUN) { + if (run_status_ < AT_RUN) { int cursor_pos = cursor_pos_ + offset; CONSTRAIN(cursor_pos, 0, AUTO_MENU_ITEMS_LAST - 0x2); cursor_pos_ = cursor_pos; // if (cursor_pos_ == DATA_SELECT) - auto_tune_running_status_ = AT_OFF; + run_status_ = AT_OFF; } - else if (auto_tune_running_status_ == AT_ERROR || auto_tune_running_status_ == AT_DONE) - auto_tune_running_status_ = AT_OFF; + else if (run_status_ == AT_ERROR || run_status_ == AT_DONE) + run_status_ = AT_OFF; } template @@ -272,7 +706,7 @@ class Autotuner { switch (cursor_pos_) { case DATA_SELECT: { - uint8_t data = owner_->data_available(); + uint8_t data = data_available(); if (!data) { // no data -- calibration_data_ = 0x0; data_select_ = 0x0; @@ -283,25 +717,25 @@ class Autotuner { data_select_ = _data_sel; if (_data_sel == 0x0) { calibration_data_ = 0xFF; - owner_->use_default(); + use_default(); } else { calibration_data_ = 0x01; - owner_->use_auto_calibration(); + use_auto_calibration(); } } } break; case AUTOTUNE: { - if (auto_tune_running_status_ < AT_RUN) { - int _status = auto_tune_running_status_ + offset; + if (run_status_ < AT_RUN) { + int _status = run_status_ + offset; CONSTRAIN(_status, 0, AT_READY); - auto_tune_running_status_ = _status; - owner_->autotuner_arm(_status); + run_status_ = AT_STATUS(_status); + autotuner_arm(_status); } - else if (auto_tune_running_status_ == AT_ERROR || auto_tune_running_status_ == AT_DONE) - auto_tune_running_status_ = 0x0; + else if (run_status_ == AT_ERROR || run_status_ == AT_DONE) + run_status_ = AT_OFF; } break; default: @@ -312,28 +746,35 @@ class Autotuner { template void Autotuner::handleButtonUp(const UI::Event &event) { - if (cursor_pos_ == AUTOTUNE && auto_tune_running_status_ == AT_OFF) { + if (cursor_pos_ == AUTOTUNE && run_status_ == AT_OFF) { // arm the tuner - auto_tune_running_status_ = AT_READY; - owner_->autotuner_arm(auto_tune_running_status_); + run_status_ = AT_READY; + autotuner_arm(run_status_); } else { - owner_->reset_autotuner(); - auto_tune_running_status_ = AT_OFF; + reset_autotuner(); + run_status_ = AT_OFF; } } template void Autotuner::handleButtonDown(const UI::Event &event) { + if (run_status_ == AT_ERROR) { + reset_autotuner(); + run_status_ = AT_OFF; + return; + } - if (cursor_pos_ == AUTOTUNE && auto_tune_running_status_ == AT_READY) { - owner_->autotuner_run(); - auto_tune_running_status_ = AT_RUN; + if (cursor_pos_ == AUTOTUNE) { + if (run_status_ == AT_READY) { + autotuner_run(); + run_status_ = AT_RUN; + } + else if (run_status_ == AT_WAIT) { + auto_next_step(); + run_status_ = AT_RUN; + } } - else if (auto_tune_running_status_ == AT_ERROR) { - owner_->reset_autotuner(); - auto_tune_running_status_ = AT_OFF; - } } template @@ -351,12 +792,13 @@ class Autotuner { else data_select_ = 0x00; cursor_pos_ = 0x0; - auto_tune_running_status_ = 0x0; + run_status_ = AT_OFF; } template void Autotuner::Close() { ui.SetButtonIgnoreMask(); + owner_->ExitAutotune(); owner_ = nullptr; } }; // namespace OC diff --git a/software/o_c_REV/OC_calibration.h b/software/o_c_REV/OC_calibration.h index fa61a22ba..275de48ac 100644 --- a/software/o_c_REV/OC_calibration.h +++ b/software/o_c_REV/OC_calibration.h @@ -44,7 +44,12 @@ struct CalibrationData { uint32_t flags; uint8_t screensaver_timeout; // 0: default, else seconds uint8_t reserved0[3]; +#ifdef VOR + /* less complicated this way than adding it to DAC::CalibrationData... */ + uint32_t v_bias; +#else uint32_t reserved1; +#endif EncoderConfig encoder_config() const { return static_cast(flags & CALIBRATION_FLAG_ENCODER_MASK); diff --git a/software/o_c_REV/OC_calibration.ino b/software/o_c_REV/OC_calibration.ino index fd45a2aaf..85bbf9cad 100644 --- a/software/o_c_REV/OC_calibration.ino +++ b/software/o_c_REV/OC_calibration.ino @@ -7,10 +7,11 @@ */ #include "OC_calibration.h" +namespace menu = OC::menu; using OC::DAC; -#ifdef BUCHLA_cOC +#if defined(BUCHLA_cOC) || defined(VOR) static constexpr uint16_t DAC_OFFSET = 0; // DAC offset, initial approx., ish (Easel card) #else static constexpr uint16_t DAC_OFFSET = 4890; // DAC offset, initial approx., ish --> -3.5V to 6V @@ -33,11 +34,16 @@ bool calibration_data_loaded = false; const OC::CalibrationData kCalibrationDefaults = { // DAC { { - #ifdef BUCHLA_cOC + #ifdef BUCHLA_cOC {197, 6634, 13083, 19517, 25966, 32417, 38850, 45301, 51733, 58180, 64400}, {197, 6634, 13083, 19517, 25966, 32417, 38850, 45301, 51733, 58180, 64400}, {197, 6634, 13083, 19517, 25966, 32417, 38850, 45301, 51733, 58180, 64400}, {197, 6634, 13083, 19517, 25966, 32417, 38850, 45301, 51733, 58180, 64400} + #elif defined(VOR) + {880, 7190, 13510, 19830, 26300, 32460, 38770, 45090, 51410, 57720, 64040}, + {880, 7190, 13510, 19830, 26300, 32460, 38770, 45090, 51410, 57720, 64040}, + {880, 7190, 13510, 19830, 26300, 32460, 38770, 45090, 51410, 57720, 64040}, + {880, 7190, 13510, 19830, 26300, 32460, 38770, 45090, 51410, 57720, 64040} #else {0, 6553, 13107, 19661, 26214, 32768, 39321, 45875, 52428, 58981, 65535}, {0, 6553, 13107, 19661, 26214, 32768, 39321, 45875, 52428, 58981, 65535}, @@ -54,8 +60,13 @@ const OC::CalibrationData kCalibrationDefaults = { // display_offset SH1106_128x64_Driver::kDefaultOffset, OC_CALIBRATION_DEFAULT_FLAGS, - SCREENSAVER_TIMEOUT_S, { 0, 0, 0 }, - 0 // reserved + SCREENSAVER_TIMEOUT_S, + { 0, 0, 0 }, // reserved0 + #ifdef VOR + DAC::VBiasBipolar | (DAC::VBiasAsymmetric << 16) // default v_bias values + #else + 0 // reserved1 + #endif }; void calibration_reset() { @@ -68,6 +79,22 @@ void calibration_reset() { } } +#ifdef FLIP_180 +void calibration_flip() { + uint16_t flip_dac[OCTAVES + 1]; + uint16_t flip_adc; + for (int i = 0; i < 2; ++i) { + flip_adc = OC::calibration_data.adc.offset[i]; + OC::calibration_data.adc.offset[i] = OC::calibration_data.adc.offset[ADC_CHANNEL_LAST-1 - i]; + OC::calibration_data.adc.offset[ADC_CHANNEL_LAST-1 - i] = flip_adc; + + memcpy(flip_dac, OC::calibration_data.dac.calibrated_octaves[i], sizeof(flip_dac)); + memcpy(OC::calibration_data.dac.calibrated_octaves[i], OC::calibration_data.dac.calibrated_octaves[DAC_CHANNEL_LAST-1 - i], sizeof(flip_dac)); + memcpy(OC::calibration_data.dac.calibrated_octaves[DAC_CHANNEL_LAST-1 - i], flip_dac, sizeof(flip_dac)); + } +} +#endif + void calibration_load() { SERIAL_PRINTLN("Cal.Storage: PAGESIZE=%u, PAGES=%u, LENGTH=%u", OC::CalibrationStorage::PAGESIZE, OC::CalibrationStorage::PAGES, OC::CalibrationStorage::LENGTH); @@ -86,6 +113,9 @@ void calibration_load() { SERIAL_PRINTLN("No calibration data, using defaults"); #endif } else { +#ifdef FLIP_180 + calibration_flip(); +#endif SERIAL_PRINTLN("Calibration data loaded..."); } @@ -101,18 +131,32 @@ void calibration_load() { void calibration_save() { SERIAL_PRINTLN("Saving calibration data"); +#ifdef FLIP_180 + calibration_flip(); +#endif OC::calibration_storage.Save(OC::calibration_data); +#ifdef FLIP_180 + calibration_flip(); +#endif } enum CALIBRATION_STEP { HELLO, CENTER_DISPLAY, - + + #ifdef VOR + DAC_A_VOLT_3m, DAC_A_VOLT_2m, DAC_A_VOLT_1m, DAC_A_VOLT_0, DAC_A_VOLT_1, DAC_A_VOLT_2, DAC_A_VOLT_3, DAC_A_VOLT_4, DAC_A_VOLT_5, DAC_A_VOLT_6, DAC_A_VOLT_7, + DAC_B_VOLT_3m, DAC_B_VOLT_2m, DAC_B_VOLT_1m, DAC_B_VOLT_0, DAC_B_VOLT_1, DAC_B_VOLT_2, DAC_B_VOLT_3, DAC_B_VOLT_4, DAC_B_VOLT_5, DAC_B_VOLT_6, DAC_B_VOLT_7, + DAC_C_VOLT_3m, DAC_C_VOLT_2m, DAC_C_VOLT_1m, DAC_C_VOLT_0, DAC_C_VOLT_1, DAC_C_VOLT_2, DAC_C_VOLT_3, DAC_C_VOLT_4, DAC_C_VOLT_5, DAC_C_VOLT_6, DAC_C_VOLT_7, + DAC_D_VOLT_3m, DAC_D_VOLT_2m, DAC_D_VOLT_1m, DAC_D_VOLT_0, DAC_D_VOLT_1, DAC_D_VOLT_2, DAC_D_VOLT_3, DAC_D_VOLT_4, DAC_D_VOLT_5, DAC_D_VOLT_6, DAC_D_VOLT_7, + V_BIAS_BIPOLAR, V_BIAS_ASYMMETRIC, + #else DAC_A_VOLT_3m, DAC_A_VOLT_2m, DAC_A_VOLT_1m, DAC_A_VOLT_0, DAC_A_VOLT_1, DAC_A_VOLT_2, DAC_A_VOLT_3, DAC_A_VOLT_4, DAC_A_VOLT_5, DAC_A_VOLT_6, DAC_B_VOLT_3m, DAC_B_VOLT_2m, DAC_B_VOLT_1m, DAC_B_VOLT_0, DAC_B_VOLT_1, DAC_B_VOLT_2, DAC_B_VOLT_3, DAC_B_VOLT_4, DAC_B_VOLT_5, DAC_B_VOLT_6, DAC_C_VOLT_3m, DAC_C_VOLT_2m, DAC_C_VOLT_1m, DAC_C_VOLT_0, DAC_C_VOLT_1, DAC_C_VOLT_2, DAC_C_VOLT_3, DAC_C_VOLT_4, DAC_C_VOLT_5, DAC_C_VOLT_6, DAC_D_VOLT_3m, DAC_D_VOLT_2m, DAC_D_VOLT_1m, DAC_D_VOLT_0, DAC_D_VOLT_1, DAC_D_VOLT_2, DAC_D_VOLT_3, DAC_D_VOLT_4, DAC_D_VOLT_5, DAC_D_VOLT_6, - + #endif + CV_OFFSET_0, CV_OFFSET_1, CV_OFFSET_2, CV_OFFSET_3, ADC_PITCH_C2, ADC_PITCH_C4, CALIBRATION_SCREENSAVER_TIMEOUT, @@ -124,6 +168,10 @@ enum CALIBRATION_STEP { enum CALIBRATION_TYPE { CALIBRATE_NONE, CALIBRATE_OCTAVE, + #ifdef VOR + CALIBRATE_VBIAS_BIPOLAR, + CALIBRATE_VBIAS_ASYMMETRIC, + #endif CALIBRATE_ADC_OFFSET, CALIBRATE_ADC_1V, CALIBRATE_ADC_3V, @@ -149,7 +197,8 @@ DAC_CHANNEL step_to_channel(int step) { if (step >= DAC_D_VOLT_3m) return DAC_CHANNEL_D; if (step >= DAC_C_VOLT_3m) return DAC_CHANNEL_C; if (step >= DAC_B_VOLT_3m) return DAC_CHANNEL_B; - /*if (step >= DAC_B_VOLT_3m)*/ return DAC_CHANNEL_A; + /*if (step >= DAC_B_VOLT_3m)*/ + return DAC_CHANNEL_A; } struct CalibrationState { @@ -178,7 +227,7 @@ const CalibrationStep calibration_steps[CALIBRATION_STEP_LAST] = { { HELLO, "Setup: Calibrate", "Use defaults? ", select_help, start_footer, CALIBRATE_NONE, 0, OC::Strings::no_yes, 0, 1 }, { CENTER_DISPLAY, "Center Display", "Pixel offset ", default_help_r, default_footer, CALIBRATE_DISPLAY, 0, nullptr, 0, 2 }, - #ifdef BUCHLA_4U + #if defined(BUCHLA_4U) && !defined(IO_10V) { DAC_A_VOLT_3m, "DAC A 0.0 volts", "-> 0.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 0, nullptr, 0, DAC::MAX_VALUE }, { DAC_A_VOLT_2m, "DAC A 1.2 volts", "-> 1.200V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 1, nullptr, 0, DAC::MAX_VALUE }, { DAC_A_VOLT_1m, "DAC A 2.4 volts", "-> 2.400V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 2, nullptr, 0, DAC::MAX_VALUE }, @@ -222,6 +271,98 @@ const CalibrationStep calibration_steps[CALIBRATION_STEP_LAST] = { { DAC_D_VOLT_4, "DAC D 8.4 volts", "-> 8.400V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 7, nullptr, 0, DAC::MAX_VALUE }, { DAC_D_VOLT_5, "DAC D 9.6 volts", "-> 9.600V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 8, nullptr, 0, DAC::MAX_VALUE }, { DAC_D_VOLT_6, "DAC D 10.8 volts", "-> 10.800V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 9, nullptr, 0, DAC::MAX_VALUE }, + #elif defined(IO_10V) && !defined(VOR) + { DAC_A_VOLT_3m, "DAC A 0.0 volts", "-> 0.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 0, nullptr, 0, DAC::MAX_VALUE }, + { DAC_A_VOLT_2m, "DAC A 1.0 volts", "-> 1.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 1, nullptr, 0, DAC::MAX_VALUE }, + { DAC_A_VOLT_1m, "DAC A 2.0 volts", "-> 2.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 2, nullptr, 0, DAC::MAX_VALUE }, + { DAC_A_VOLT_0, "DAC A 3.0 volts", "-> 3.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 3, nullptr, 0, DAC::MAX_VALUE }, + { DAC_A_VOLT_1, "DAC A 4.0 volts", "-> 4.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 4, nullptr, 0, DAC::MAX_VALUE }, + { DAC_A_VOLT_2, "DAC A 5.0 volts", "-> 5.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 5, nullptr, 0, DAC::MAX_VALUE }, + { DAC_A_VOLT_3, "DAC A 6.0 volts", "-> 6.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 6, nullptr, 0, DAC::MAX_VALUE }, + { DAC_A_VOLT_4, "DAC A 7.0 volts", "-> 7.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 7, nullptr, 0, DAC::MAX_VALUE }, + { DAC_A_VOLT_5, "DAC A 8.0 volts", "-> 8.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 8, nullptr, 0, DAC::MAX_VALUE }, + { DAC_A_VOLT_6, "DAC A 9.0 volts", "-> 9.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 9, nullptr, 0, DAC::MAX_VALUE }, + + { DAC_B_VOLT_3m, "DAC B 0.0 volts", "-> 0.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 0, nullptr, 0, DAC::MAX_VALUE }, + { DAC_B_VOLT_2m, "DAC B 1.0 volts", "-> 1.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 1, nullptr, 0, DAC::MAX_VALUE }, + { DAC_B_VOLT_1m, "DAC B 2.0 volts", "-> 2.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 2, nullptr, 0, DAC::MAX_VALUE }, + { DAC_B_VOLT_0, "DAC B 3.0 volts", "-> 3.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 3, nullptr, 0, DAC::MAX_VALUE }, + { DAC_B_VOLT_1, "DAC B 4.0 volts", "-> 4.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 4, nullptr, 0, DAC::MAX_VALUE }, + { DAC_B_VOLT_2, "DAC B 5.0 volts", "-> 5.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 5, nullptr, 0, DAC::MAX_VALUE }, + { DAC_B_VOLT_3, "DAC B 6.0 volts", "-> 6.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 6, nullptr, 0, DAC::MAX_VALUE }, + { DAC_B_VOLT_4, "DAC B 7.0 volts", "-> 7.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 7, nullptr, 0, DAC::MAX_VALUE }, + { DAC_B_VOLT_5, "DAC B 8.0 volts", "-> 8.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 8, nullptr, 0, DAC::MAX_VALUE }, + { DAC_B_VOLT_6, "DAC B 9.0 volts", "-> 9.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 9, nullptr, 0, DAC::MAX_VALUE }, + + { DAC_C_VOLT_3m, "DAC C 0.0 volts", "-> 0.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 0, nullptr, 0, DAC::MAX_VALUE }, + { DAC_C_VOLT_2m, "DAC C 1.0 volts", "-> 1.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 1, nullptr, 0, DAC::MAX_VALUE }, + { DAC_C_VOLT_1m, "DAC C 2.0 volts", "-> 2.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 2, nullptr, 0, DAC::MAX_VALUE }, + { DAC_C_VOLT_0, "DAC C 3.0 volts", "-> 3.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 3, nullptr, 0, DAC::MAX_VALUE }, + { DAC_C_VOLT_1, "DAC C 4.0 volts", "-> 4.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 4, nullptr, 0, DAC::MAX_VALUE }, + { DAC_C_VOLT_2, "DAC C 5.0 volts", "-> 5.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 5, nullptr, 0, DAC::MAX_VALUE }, + { DAC_C_VOLT_3, "DAC C 6.0 volts", "-> 6.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 6, nullptr, 0, DAC::MAX_VALUE }, + { DAC_C_VOLT_4, "DAC C 7.0 volts", "-> 7.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 7, nullptr, 0, DAC::MAX_VALUE }, + { DAC_C_VOLT_5, "DAC C 8.0 volts", "-> 8.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 8, nullptr, 0, DAC::MAX_VALUE }, + { DAC_C_VOLT_6, "DAC C 9.0 volts", "-> 9.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 9, nullptr, 0, DAC::MAX_VALUE }, + + { DAC_D_VOLT_3m, "DAC D 0.0 volts", "-> 0.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 0, nullptr, 0, DAC::MAX_VALUE }, + { DAC_D_VOLT_2m, "DAC D 1.0 volts", "-> 1.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 1, nullptr, 0, DAC::MAX_VALUE }, + { DAC_D_VOLT_1m, "DAC D 2.0 volts", "-> 2.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 2, nullptr, 0, DAC::MAX_VALUE }, + { DAC_D_VOLT_0, "DAC D 3.0 volts", "-> 3.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 3, nullptr, 0, DAC::MAX_VALUE }, + { DAC_D_VOLT_1, "DAC D 4.0 volts", "-> 4.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 4, nullptr, 0, DAC::MAX_VALUE }, + { DAC_D_VOLT_2, "DAC D 5.0 volts", "-> 5.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 5, nullptr, 0, DAC::MAX_VALUE }, + { DAC_D_VOLT_3, "DAC D 6.0 volts", "-> 6.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 6, nullptr, 0, DAC::MAX_VALUE }, + { DAC_D_VOLT_4, "DAC D 7.0 volts", "-> 7.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 7, nullptr, 0, DAC::MAX_VALUE }, + { DAC_D_VOLT_5, "DAC D 8.0 volts", "-> 8.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 8, nullptr, 0, DAC::MAX_VALUE }, + { DAC_D_VOLT_6, "DAC D 9.0 volts", "-> 9.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 9, nullptr, 0, DAC::MAX_VALUE }, + #elif defined(VOR) + { DAC_A_VOLT_3m, "DAC A 0.0 volts", "-> 0.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 0, nullptr, 0, DAC::MAX_VALUE }, + { DAC_A_VOLT_2m, "DAC A 1.0 volts", "-> 1.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 1, nullptr, 0, DAC::MAX_VALUE }, + { DAC_A_VOLT_1m, "DAC A 2.0 volts", "-> 2.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 2, nullptr, 0, DAC::MAX_VALUE }, + { DAC_A_VOLT_0, "DAC A 3.0 volts", "-> 3.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 3, nullptr, 0, DAC::MAX_VALUE }, + { DAC_A_VOLT_1, "DAC A 4.0 volts", "-> 4.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 4, nullptr, 0, DAC::MAX_VALUE }, + { DAC_A_VOLT_2, "DAC A 5.0 volts", "-> 5.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 5, nullptr, 0, DAC::MAX_VALUE }, + { DAC_A_VOLT_3, "DAC A 6.0 volts", "-> 6.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 6, nullptr, 0, DAC::MAX_VALUE }, + { DAC_A_VOLT_4, "DAC A 7.0 volts", "-> 7.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 7, nullptr, 0, DAC::MAX_VALUE }, + { DAC_A_VOLT_5, "DAC A 8.0 volts", "-> 8.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 8, nullptr, 0, DAC::MAX_VALUE }, + { DAC_A_VOLT_6, "DAC A 9.0 volts", "-> 9.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 9, nullptr, 0, DAC::MAX_VALUE }, + { DAC_A_VOLT_7, "DAC A 10.0 volts", "-> 10.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 10, nullptr, 0, DAC::MAX_VALUE }, + + { DAC_B_VOLT_3m, "DAC B 0.0 volts", "-> 0.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 0, nullptr, 0, DAC::MAX_VALUE }, + { DAC_B_VOLT_2m, "DAC B 1.0 volts", "-> 1.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 1, nullptr, 0, DAC::MAX_VALUE }, + { DAC_B_VOLT_1m, "DAC B 2.0 volts", "-> 2.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 2, nullptr, 0, DAC::MAX_VALUE }, + { DAC_B_VOLT_0, "DAC B 3.0 volts", "-> 3.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 3, nullptr, 0, DAC::MAX_VALUE }, + { DAC_B_VOLT_1, "DAC B 4.0 volts", "-> 4.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 4, nullptr, 0, DAC::MAX_VALUE }, + { DAC_B_VOLT_2, "DAC B 5.0 volts", "-> 5.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 5, nullptr, 0, DAC::MAX_VALUE }, + { DAC_B_VOLT_3, "DAC B 6.0 volts", "-> 6.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 6, nullptr, 0, DAC::MAX_VALUE }, + { DAC_B_VOLT_4, "DAC B 7.0 volts", "-> 7.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 7, nullptr, 0, DAC::MAX_VALUE }, + { DAC_B_VOLT_5, "DAC B 8.0 volts", "-> 8.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 8, nullptr, 0, DAC::MAX_VALUE }, + { DAC_B_VOLT_6, "DAC B 9.0 volts", "-> 9.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 9, nullptr, 0, DAC::MAX_VALUE }, + { DAC_B_VOLT_7, "DAC B 10.0 volts", "-> 10.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 10, nullptr, 0, DAC::MAX_VALUE }, + + { DAC_C_VOLT_3m, "DAC C 0.0 volts", "-> 0.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 0, nullptr, 0, DAC::MAX_VALUE }, + { DAC_C_VOLT_2m, "DAC C 1.0 volts", "-> 1.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 1, nullptr, 0, DAC::MAX_VALUE }, + { DAC_C_VOLT_1m, "DAC C 2.0 volts", "-> 2.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 2, nullptr, 0, DAC::MAX_VALUE }, + { DAC_C_VOLT_0, "DAC C 3.0 volts", "-> 3.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 3, nullptr, 0, DAC::MAX_VALUE }, + { DAC_C_VOLT_1, "DAC C 4.0 volts", "-> 4.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 4, nullptr, 0, DAC::MAX_VALUE }, + { DAC_C_VOLT_2, "DAC C 5.0 volts", "-> 5.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 5, nullptr, 0, DAC::MAX_VALUE }, + { DAC_C_VOLT_3, "DAC C 6.0 volts", "-> 6.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 6, nullptr, 0, DAC::MAX_VALUE }, + { DAC_C_VOLT_4, "DAC C 7.0 volts", "-> 7.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 7, nullptr, 0, DAC::MAX_VALUE }, + { DAC_C_VOLT_5, "DAC C 8.0 volts", "-> 8.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 8, nullptr, 0, DAC::MAX_VALUE }, + { DAC_C_VOLT_6, "DAC C 9.0 volts", "-> 9.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 9, nullptr, 0, DAC::MAX_VALUE }, + { DAC_C_VOLT_7, "DAC C 10.0 volts", "-> 10.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 10, nullptr, 0, DAC::MAX_VALUE }, + + { DAC_D_VOLT_3m, "DAC D 0.0 volts", "-> 0.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 0, nullptr, 0, DAC::MAX_VALUE }, + { DAC_D_VOLT_2m, "DAC D 1.0 volts", "-> 1.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 1, nullptr, 0, DAC::MAX_VALUE }, + { DAC_D_VOLT_1m, "DAC D 2.0 volts", "-> 2.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 2, nullptr, 0, DAC::MAX_VALUE }, + { DAC_D_VOLT_0, "DAC D 3.0 volts", "-> 3.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 3, nullptr, 0, DAC::MAX_VALUE }, + { DAC_D_VOLT_1, "DAC D 4.0 volts", "-> 4.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 4, nullptr, 0, DAC::MAX_VALUE }, + { DAC_D_VOLT_2, "DAC D 5.0 volts", "-> 5.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 5, nullptr, 0, DAC::MAX_VALUE }, + { DAC_D_VOLT_3, "DAC D 6.0 volts", "-> 6.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 6, nullptr, 0, DAC::MAX_VALUE }, + { DAC_D_VOLT_4, "DAC D 7.0 volts", "-> 7.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 7, nullptr, 0, DAC::MAX_VALUE }, + { DAC_D_VOLT_5, "DAC D 8.0 volts", "-> 8.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 8, nullptr, 0, DAC::MAX_VALUE }, + { DAC_D_VOLT_6, "DAC D 9.0 volts", "-> 9.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 9, nullptr, 0, DAC::MAX_VALUE }, + { DAC_D_VOLT_7, "DAC D 10.0 volts", "-> 10.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 10, nullptr, 0, DAC::MAX_VALUE }, #else { DAC_A_VOLT_3m, "DAC A -3 volts", "-> -3.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, -3, nullptr, 0, DAC::MAX_VALUE }, { DAC_A_VOLT_2m, "DAC A -2 volts", "-> -2.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, -2, nullptr, 0, DAC::MAX_VALUE }, @@ -267,13 +408,18 @@ const CalibrationStep calibration_steps[CALIBRATION_STEP_LAST] = { { DAC_D_VOLT_5, "DAC D 5 volts", "-> 5.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 5, nullptr, 0, DAC::MAX_VALUE }, { DAC_D_VOLT_6, "DAC D 6 volts", "-> 6.000V ", default_help_r, default_footer, CALIBRATE_OCTAVE, 6, nullptr, 0, DAC::MAX_VALUE }, #endif + + #ifdef VOR + { V_BIAS_BIPOLAR, "0.000V: bipolar", "--> 0.000V", default_help_r, default_footer, CALIBRATE_VBIAS_BIPOLAR, 0, nullptr, 0, 4095 }, + { V_BIAS_ASYMMETRIC, "0.000V: asym.", "--> 0.000V", default_help_r, default_footer, CALIBRATE_VBIAS_ASYMMETRIC, 0, nullptr, 0, 4095 }, + #endif { CV_OFFSET_0, "ADC CV1", "ADC value at 0V", default_help_r, default_footer, CALIBRATE_ADC_OFFSET, ADC_CHANNEL_1, nullptr, 0, 4095 }, { CV_OFFSET_1, "ADC CV2", "ADC value at 0V", default_help_r, default_footer, CALIBRATE_ADC_OFFSET, ADC_CHANNEL_2, nullptr, 0, 4095 }, { CV_OFFSET_2, "ADC CV3", "ADC value at 0V", default_help_r, default_footer, CALIBRATE_ADC_OFFSET, ADC_CHANNEL_3, nullptr, 0, 4095 }, { CV_OFFSET_3, "ADC CV4", "ADC value at 0V", default_help_r, default_footer, CALIBRATE_ADC_OFFSET, ADC_CHANNEL_4, nullptr, 0, 4095 }, - #ifdef BUCHLA_4U + #if defined(BUCHLA_4U) && !defined(IO_10V) { ADC_PITCH_C2, "ADC cal. octave #1", "CV1: Input 1.2V", "[R] Long press to set", default_footer, CALIBRATE_ADC_1V, 0, nullptr, 0, 0 }, { ADC_PITCH_C4, "ADC cal. octave #3", "CV1: Input 3.6V", "[R] Long press to set", default_footer, CALIBRATE_ADC_3V, 0, nullptr, 0, 0 }, #else @@ -308,6 +454,12 @@ void OC::Ui::Calibrate() { tick_count.Init(); encoder_enable_acceleration(CONTROL_ENCODER_R, true); + #ifdef VOR + { + VBiasManager *vb = vb->get(); + vb->SetState(VBiasManager::UNI); + } + #endif bool calibration_complete = false; while (!calibration_complete) { @@ -320,7 +472,7 @@ void OC::Ui::Calibrate() { while (event_queue_.available()) { const UI::Event event = event_queue_.PullEvent(); - if (IgnoreEvent(event)) + if (IgnoreEvent(event) || event.type == UI::EVENT_BUTTON_DOWN) continue; switch (event.control) { @@ -398,9 +550,26 @@ void OC::Ui::Calibrate() { case CALIBRATE_OCTAVE: calibration_state.encoder_value = OC::calibration_data.dac.calibrated_octaves[step_to_channel(next_step->step)][next_step->index + DAC::kOctaveZero]; + #ifdef VOR + /* set 0V @ unipolar range */ + DAC::set_Vbias(DAC::VBiasUnipolar); + #endif break; + + #ifdef VOR + case CALIBRATE_VBIAS_BIPOLAR: + calibration_state.encoder_value = (0xFFFF & OC::calibration_data.v_bias); // bipolar = lower 2 bytes + break; + case CALIBRATE_VBIAS_ASYMMETRIC: + calibration_state.encoder_value = (OC::calibration_data.v_bias >> 16); // asymmetric = upper 2 bytes + break; + #endif + case CALIBRATE_ADC_OFFSET: calibration_state.encoder_value = OC::calibration_data.adc.offset[next_step->index]; + #ifdef VOR + DAC::set_Vbias(DAC::VBiasUnipolar); + #endif break; case CALIBRATE_DISPLAY: calibration_state.encoder_value = OC::calibration_data.display_offset; @@ -435,6 +604,7 @@ void OC::Ui::Calibrate() { calibration_update(calibration_state); calibration_draw(calibration_state); + delay(2); // VOR calibration hack } if (calibration_state.encoder_value) { @@ -449,9 +619,12 @@ void calibration_draw(const CalibrationState &state) { GRAPHICS_BEGIN_FRAME(true); const CalibrationStep *step = state.current_step; + /* graphics.drawLine(0, 10, 127, 10); graphics.drawLine(0, 12, 127, 12); graphics.setPrintPos(1, 2); + */ + menu::DefaultTitleBar::Draw(); graphics.print(step->title); weegfx::coord_t y = menu::CalcLineY(0); @@ -462,6 +635,10 @@ void calibration_draw(const CalibrationState &state) { switch (step->calibration_type) { case CALIBRATE_OCTAVE: case CALIBRATE_SCREENSAVER: + #ifdef VOR + case CALIBRATE_VBIAS_BIPOLAR: + case CALIBRATE_VBIAS_ASYMMETRIC: + #endif graphics.print(step->message); graphics.setPrintPos(kValueX, y + 2); graphics.print((int)state.encoder_value, 5); @@ -539,7 +716,7 @@ void calibration_draw(const CalibrationState &state) { x += 5; } - graphics.drawStr(0, menu::kDisplayHeight - menu::kFontHeight - 1, step->footer); + graphics.drawStr(1, menu::kDisplayHeight - menu::kFontHeight - 3, step->footer); static constexpr uint16_t step_width = (menu::kDisplayWidth << 8 ) / (CALIBRATION_STEP_LAST - 1); graphics.drawRect(0, menu::kDisplayHeight - 2, (state.step * step_width) >> 8, 2); @@ -563,6 +740,20 @@ void calibration_update(CalibrationState &state) { state.encoder_value; DAC::set_all_octave(step->index); break; + #ifdef VOR + case CALIBRATE_VBIAS_BIPOLAR: + /* set 0V @ bipolar range */ + DAC::set_all_octave(5); + OC::calibration_data.v_bias = (OC::calibration_data.v_bias & 0xFFFF0000) | state.encoder_value; + DAC::set_Vbias(0xFFFF & OC::calibration_data.v_bias); + break; + case CALIBRATE_VBIAS_ASYMMETRIC: + /* set 0V @ asym. range */ + DAC::set_all_octave(3); + OC::calibration_data.v_bias = (OC::calibration_data.v_bias & 0xFFFF) | (state.encoder_value << 16); + DAC::set_Vbias(OC::calibration_data.v_bias >> 16); + break; + #endif case CALIBRATE_ADC_OFFSET: OC::calibration_data.adc.offset[step->index] = state.encoder_value; DAC::set_all_octave(0); @@ -594,42 +785,4 @@ uint32_t adc_average() { OC::ADC::smoothed_raw_value(ADC_CHANNEL_3) + OC::ADC::smoothed_raw_value(ADC_CHANNEL_4); } -#ifdef CALIBRATION_LOAD_LEGACY -/* read settings from original O&C */ -void calibration_read_old() { - - delay(1000); - uint8_t byte0, byte1, adr; - - adr = 0; - SERIAL_PRINTLN("Loading original O&C calibration from eeprom:"); - - for (int i = 0; i < OCTAVES; i++) { - - byte0 = EEPROM.read(adr); - adr++; - byte1 = EEPROM.read(adr); - adr++; - OC::calibration_data.dac.octaves[i] = (uint16_t)(byte0 << 8) + byte1; - SERIAL_PRINTLN(" OCTAVE %2d: %u", i, OC::calibration_data.dac.octaves[i]); - } - - uint16_t _offset[ADC_CHANNEL_LAST]; - - for (int i = 0; i < ADC_CHANNEL_LAST; i++) { - - byte0 = EEPROM.read(adr); - adr++; - byte1 = EEPROM.read(adr); - adr++; - _offset[i] = (uint16_t)(byte0 << 8) + byte1; - SERIAL_PRINTLN("ADC %d: %u", i, _offset[i]); - } - - OC::calibration_data.adc.offset[ADC_CHANNEL_1] = _offset[0]; - OC::calibration_data.adc.offset[ADC_CHANNEL_2] = _offset[1]; - OC::calibration_data.adc.offset[ADC_CHANNEL_3] = _offset[2]; - OC::calibration_data.adc.offset[ADC_CHANNEL_4] = _offset[3]; - SERIAL_PRINTLN("......"); -} -#endif +// end diff --git a/software/o_c_REV/OC_chords.cpp b/software/o_c_REV/OC_chords.cpp new file mode 100644 index 000000000..14016c749 --- /dev/null +++ b/software/o_c_REV/OC_chords.cpp @@ -0,0 +1,29 @@ +#include "OC_chords.h" +#include "OC_chords_presets.h" + +namespace OC { + + Chord user_chords[Chords::CHORDS_USER_LAST]; + + /*static*/ + const int Chords::NUM_CHORD_PROGRESSIONS = 0x4; + const int Chords::NUM_CHORDS_TOTAL = OC::Chords::CHORDS_USER_LAST; // = 8 + const int Chords::NUM_CHORDS_PROPERTIES = sizeof(Chord); + const int Chords::NUM_CHORDS = Chords::NUM_CHORDS_TOTAL / Chords::NUM_CHORD_PROGRESSIONS; + + /*static*/ + // + void Chords::Init() { + for (size_t i = 0; i < OC::Chords::CHORDS_USER_LAST; ++i) + memcpy(&user_chords[i], &OC::chords[0], sizeof(Chord)); + } + + const Chord &Chords::GetChord(int index, int progression) { + + uint8_t _index = index + progression * Chords::NUM_CHORDS; + if (_index < CHORDS_USER_LAST) + return user_chords[_index]; + else + return user_chords[0x0]; + } +} // namespace OC diff --git a/software/o_c_REV/OC_chords.h b/software/o_c_REV/OC_chords.h new file mode 100644 index 000000000..c9dec06d1 --- /dev/null +++ b/software/o_c_REV/OC_chords.h @@ -0,0 +1,105 @@ +#ifndef OC_CHORDS_H_ +#define OC_CHORDS_H_ + +#include "OC_chords_presets.h" + +namespace OC { + +typedef OC::Chord Chord; + +class Chords { +public: + + static const int NUM_CHORDS_TOTAL; + static const int NUM_CHORDS; + static const int NUM_CHORDS_PROPERTIES; + static const int NUM_CHORD_PROGRESSIONS; + + enum CHORD_SLOTS + { + CHORDS_USER_0_0, + CHORDS_USER_1_0, + CHORDS_USER_2_0, + CHORDS_USER_3_0, + CHORDS_USER_4_0, + CHORDS_USER_5_0, + CHORDS_USER_6_0, + CHORDS_USER_7_0, + CHORDS_USER_0_1, + CHORDS_USER_1_1, + CHORDS_USER_2_1, + CHORDS_USER_3_1, + CHORDS_USER_4_1, + CHORDS_USER_5_1, + CHORDS_USER_6_1, + CHORDS_USER_7_1, + CHORDS_USER_0_2, + CHORDS_USER_1_2, + CHORDS_USER_2_2, + CHORDS_USER_3_2, + CHORDS_USER_4_2, + CHORDS_USER_5_2, + CHORDS_USER_6_2, + CHORDS_USER_7_2, + CHORDS_USER_0_3, + CHORDS_USER_1_3, + CHORDS_USER_2_3, + CHORDS_USER_3_3, + CHORDS_USER_4_3, + CHORDS_USER_5_3, + CHORDS_USER_6_3, + CHORDS_USER_7_3, + CHORDS_USER_LAST + }; + + enum QUALITY + { + CHORDS_FIFTH, + CHORDS_TRIAD, + CHORDS_SEVENTH, + CHORDS_SUSPENDED, + CHORDS_SUSPENDED_SEVENTH, + CHORDS_SIXTH, + CHORDS_ADDED_NINTH, + CHORDS_ADDED_ELEVENTH, + CHORDS_UNISONO, + CHORDS_QUALITY_LAST + }; + + enum CHORDS_TYPE + { + CHORDS_TYPE_MONAD, + CHORDS_TYPE_DYAD, + CHORDS_TYPE_TRIAD, + CHORDS_TYPE_TETRAD, + CHORDS_TYPE_LAST + }; + + enum VOICING + { + CHORDS_CLOSE, + CHORDS_DROP_1, + CHORDS_DROP_2, + CHORDS_DROP_3, + CHORDS_SPREAD, + CHORDS_VOICING_LAST + }; + + enum INVERSION + { + CHORDS_INVERSION_NONE, + CHORDS_INVERSION_FIRST, + CHORDS_INVERSION_SECOND, + CHORDS_INVERSION_THIRD, + CHORDS_INVERSION_LAST + }; + + static void Init(); + static const Chord &GetChord(int index, int progression); +}; + + +extern Chord user_chords[OC::Chords::CHORDS_USER_LAST]; +}; + +#endif // OC_CHORDS_H_ diff --git a/software/o_c_REV/OC_chords_edit.h b/software/o_c_REV/OC_chords_edit.h new file mode 100644 index 000000000..9f45edde7 --- /dev/null +++ b/software/o_c_REV/OC_chords_edit.h @@ -0,0 +1,450 @@ +// Copyright (c) 2015, 2016, 2017 Patrick Dowling, Max Stadler, Tim Churches +// +// Author of original O+C firmware: Max Stadler (mxmlnstdlr@gmail.com) +// Author of app scaffolding: Patrick Dowling (pld@gurkenkiste.com) +// Modified for bouncing balls: Tim Churches (tim.churches@gmail.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef OC_CHORDS_EDIT_H_ +#define OC_CHORDS_EDIT_H_ + +#include "OC_bitmaps.h" +#include "OC_chords.h" +#include "OC_chords_presets.h" + +namespace OC { + +template +class ChordEditor { +public: + + enum EDIT_PAGE { + CHORD_SELECT, + CHORD_EDIT, + CHORD_EDIT_PAGE_LAST + }; + + void Init() { + owner_ = nullptr; + cursor_pos_ = 0; + cursor_quality_pos_ = 0; + edit_this_chord_ = 0; + edit_this_progression_ = 0; + edit_page_ = CHORD_SELECT; + // + chord_quality_ = 0; + chord_voicing_ = 0; + chord_inversion_ = 0; + chord_base_note_ = 0; + chord_octave_ = 0; + max_chords_ = OC::Chords::NUM_CHORDS - 1; + } + + bool active() const { + return nullptr != owner_; + } + + void Edit(Owner *owner, int chord, int num_chords, int num_progression) { + + if (chord > OC::Chords::CHORDS_USER_LAST - 1) + return; + + edit_this_progression_ = num_progression; + chord_ = &OC::user_chords[chord + edit_this_progression_ * OC::Chords::NUM_CHORDS]; + max_chords_ = num_chords; + owner_ = owner; + BeginEditing(); + } + + void Close(); + void Draw(); + void HandleButtonEvent(const UI::Event &event); + void HandleEncoderEvent(const UI::Event &event); + +private: + + Owner *owner_; + const OC::Chord *chord_; + int8_t edit_this_chord_; + int8_t edit_this_progression_; + size_t cursor_pos_; + size_t cursor_quality_pos_; + int8_t chord_quality_; + int8_t chord_voicing_; + int8_t chord_inversion_; + int8_t chord_base_note_; + int8_t chord_octave_; + int8_t max_chords_; + bool edit_page_; + + void BeginEditing(); + void move_cursor(int offset, int page); + void update_chord(int8_t chord_num); + void copy_chord(); + void paste_chord(); + + void change_property(size_t chord_property, int delta, bool notify); + void handleButtonLeft(const UI::Event &event); + void handleButtonUp(const UI::Event &event); + void handleButtonDown(const UI::Event &event); +}; + +template +void ChordEditor::Draw() { + + weegfx::coord_t w = 128; + weegfx::coord_t x = 0; + weegfx::coord_t y = 0; + weegfx::coord_t h = 64; + + graphics.clearRect(x, y, w, h); + graphics.drawFrame(x, y, w, h); + + // chord select: + size_t max_chords = max_chords_ + 1; + uint8_t indicator = owner_->active_chord(); + bool active = owner_->get_active_progression() == edit_this_progression_; + + x = 6; + y = 6; + // which progression ? ... + graphics.setPrintPos(x, y + 2); + graphics.print(edit_this_progression_ + 0x1); + // now draw steps: + x += 11; + + for (size_t i = 0; i < max_chords; ++i, x += 13) { + + if (active && i == indicator) { + graphics.drawFrame(x, y, 10, 10); + graphics.drawRect(x + 2, y + 2, 6, 6); + } + else if (edit_page_ == CHORD_SELECT) + graphics.drawRect(x, y, 10, 10); + else + graphics.drawRect(x + 2, y + 2, 6, 6); + + // cursor: + if (i == cursor_pos_) + graphics.drawFrame(x - 2, y - 2, 14, 14); + } + + // end marker: + graphics.drawFrame(x, y, 2, 2); + graphics.drawFrame(x, y + 4, 2, 2); + graphics.drawFrame(x, y + 8, 2, 2); + + if (cursor_pos_ == max_chords) + graphics.drawFrame(x - 2, y - 2, 6, 14); + + // draw extra cv num chords? + if (owner_->get_num_chords_cv()) { + int extra_slots = owner_->get_display_num_chords() - (max_chords - 0x1); + if (active && extra_slots > 0x0) { + x += 5; + indicator -= max_chords; + for (size_t i = 0; i < (uint8_t)extra_slots; ++i, x += 10) { + if (i == indicator) + graphics.drawRect(x, y + 2, 6, 6); + else + graphics.drawFrame(x, y + 2, 6, 6); + } + } + } + + // chord properties: + x = 6; + y = 23; + + for (size_t i = 0; i < sizeof(Chord); ++i, x += 24) { + + // draw values + + switch(i) { + + case 0: // quality + graphics.setPrintPos(x + 1, y + 7); + graphics.print(OC::quality_very_short_names[chord_quality_]); + break; + case 1: // voicing + graphics.setPrintPos(x + 1, y + 7); + graphics.print(voicing_names_short[chord_voicing_]); + break; + case 2: // inversion + graphics.setPrintPos(x + 7, y + 7); + graphics.print(inversion_names[chord_inversion_]); + break; + case 3: // base note + { + if (chord_base_note_ > 9) + graphics.setPrintPos(x + 1, y + 7); + else + graphics.setPrintPos(x + 4, y + 7); + graphics.print(base_note_names[chord_base_note_]); + // indicate if note is out-of-range: + if (chord_base_note_ > (uint8_t)OC::Scales::GetScale(owner_->get_scale(DUMMY)).num_notes) { + graphics.drawBitmap8(x + 3, y + 25, 4, OC::bitmap_indicator_4x8); + graphics.drawBitmap8(x + 14, y + 25, 4, OC::bitmap_indicator_4x8); + } + } + break; + case 4: // octave + { + if (chord_octave_ < 0) + graphics.setPrintPos(x + 4, y + 7); + else + graphics.setPrintPos(x + 7, y + 7); + graphics.print((int)chord_octave_); + } + break; + default: + break; + } + // draw property name + graphics.setPrintPos(x + 7, y + 26); + graphics.print(OC::Strings::chord_property_names[i]); + + // cursor: + if (i == cursor_quality_pos_) { + graphics.invertRect(x, y, 21, 21); + if (edit_page_ == CHORD_EDIT) + graphics.invertRect(x, y + 22, 21, 14); + else + graphics.drawFrame(x, y + 22, 21, 14); + } + else { + graphics.drawFrame(x, y, 21, 21); + graphics.drawFrame(x, y + 22, 21, 14); + } + } +} + +template +void ChordEditor::HandleButtonEvent(const UI::Event &event) { + + if (UI::EVENT_BUTTON_PRESS == event.type) { + switch (event.control) { + case OC::CONTROL_BUTTON_UP: + handleButtonUp(event); + break; + case OC::CONTROL_BUTTON_DOWN: + handleButtonDown(event); + break; + case OC::CONTROL_BUTTON_L: + handleButtonLeft(event); + break; + case OC::CONTROL_BUTTON_R: + Close(); + break; + default: + break; + } + } + else if (UI::EVENT_BUTTON_LONG_PRESS == event.type) { + switch (event.control) { + case OC::CONTROL_BUTTON_UP: + // screensaver // TODO: ideally, needs to be overridden ... invert_mask(); + break; + case OC::CONTROL_BUTTON_DOWN: + break; + case OC::CONTROL_BUTTON_L: + break; + case OC::CONTROL_BUTTON_R: + // app menu + break; + default: + break; + } + } +} + +template +void ChordEditor::HandleEncoderEvent(const UI::Event &event) { + + if (OC::CONTROL_ENCODER_L == event.control) { + move_cursor(event.value, edit_page_); + } + else if (OC::CONTROL_ENCODER_R == event.control) { + + if (cursor_pos_ < (uint8_t)(max_chords_ + 1)) { + + // write to the right slot, at the right index/offset (a nicer struct would be nicer, but well) + OC::Chord *edit_user_chord_ = &OC::user_chords[edit_this_chord_ + edit_this_progression_ * OC::Chords::NUM_CHORDS]; + + switch(cursor_quality_pos_) { + + case 0: // quality: + { + chord_quality_ += event.value; + CONSTRAIN(chord_quality_, 0, OC::Chords::CHORDS_QUALITY_LAST - 1); + edit_user_chord_->quality = chord_quality_; + } + break; + case 1: // voicing: + { + chord_voicing_ += event.value; + CONSTRAIN(chord_voicing_, 0, OC::Chords::CHORDS_VOICING_LAST - 1); + edit_user_chord_->voicing = chord_voicing_; + } + break; + case 2: // inversion: + { + chord_inversion_ += event.value; + CONSTRAIN(chord_inversion_, 0, OC::Chords::CHORDS_INVERSION_LAST - 1); + edit_user_chord_->inversion = chord_inversion_; + } + break; + case 3: // base note + { + chord_base_note_ += event.value; + const OC::Scale &scale_def = OC::Scales::GetScale(owner_->get_scale(DUMMY)); + CONSTRAIN(chord_base_note_, 0, (uint8_t)scale_def.num_notes); + edit_user_chord_->base_note = chord_base_note_; + } + break; + case 4: // octave + { + chord_octave_ += event.value; + CONSTRAIN(chord_octave_, -4, 4); + edit_user_chord_->octave = chord_octave_; + } + break; + default: + break; + } + } + else { + // expand/contract + int max_chords = max_chords_; + max_chords += event.value; + CONSTRAIN(max_chords, 0, OC::Chords::NUM_CHORDS - 0x1); + + max_chords_ = max_chords; + cursor_pos_ = max_chords_ + 1; + owner_->set_num_chords(max_chords, edit_this_progression_); + } + } +} + +template +void ChordEditor::update_chord(int8_t chord_num) { + // update chord properties: + const OC::Chord &chord_def = OC::Chords::GetChord(chord_num, edit_this_progression_); + chord_quality_ = chord_def.quality; + chord_voicing_ = chord_def.voicing; + chord_inversion_ = chord_def.inversion; + chord_base_note_ = chord_def.base_note; + chord_octave_ = chord_def.octave; + max_chords_ = owner_->get_num_chords(edit_this_progression_); +} + +template +void ChordEditor::move_cursor(int offset, int page) { + + if (page == CHORD_SELECT) { + int cursor_pos = cursor_pos_ + offset; + CONSTRAIN(cursor_pos, 0, max_chords_ + 1); + cursor_pos_ = cursor_pos; + edit_this_chord_ = cursor_pos; + // update .. + if (cursor_pos <= max_chords_) + update_chord(cursor_pos); + } + else { + int cursor_quality_pos = cursor_quality_pos_ + offset; + CONSTRAIN(cursor_quality_pos, 0, (int8_t)(sizeof(Chord) - 1)); + cursor_quality_pos_ = cursor_quality_pos; + } +} + +template +void ChordEditor::handleButtonUp(const UI::Event &event) { + // go to next chords progression + edit_this_progression_++; + if (edit_this_progression_ >= OC::Chords::NUM_CHORD_PROGRESSIONS) + edit_this_progression_ = 0x0; + update_chord(cursor_pos_); +} + +template +void ChordEditor::handleButtonDown(const UI::Event &event) { + // go to previous chords progression + edit_this_progression_--; + if (edit_this_progression_ < 0x0) + edit_this_progression_ = OC::Chords::NUM_CHORD_PROGRESSIONS - 1; + update_chord(cursor_pos_); +} + +template +void ChordEditor::handleButtonLeft(const UI::Event &) { + + if (edit_page_ == CHORD_SELECT) { + + if (cursor_pos_ < (uint8_t)(max_chords_ + 1)) { + edit_page_ = CHORD_EDIT; + // edit chord: + edit_this_chord_ = cursor_pos_; + update_chord(cursor_pos_); + } + // select previous chord if clicking on end-marker: + else if (cursor_pos_ == (uint8_t)(max_chords_ + 1)) { + edit_page_ = CHORD_EDIT; + cursor_pos_--; + edit_this_chord_ = cursor_pos_; + } + } + else { + edit_page_ = CHORD_SELECT; + cursor_pos_ = edit_this_chord_; + } +} + +template +void ChordEditor::copy_chord() { + // todo +} + +template +void ChordEditor::paste_chord() { + // todo +} + +template +void ChordEditor::BeginEditing() { + + cursor_pos_ = edit_this_chord_= owner_->get_chord_slot(); + const OC::Chord &chord_def = OC::Chords::GetChord(edit_this_chord_, edit_this_progression_); + chord_quality_ = chord_def.quality; + chord_voicing_ = chord_def.voicing; + chord_inversion_ = chord_def.inversion; + chord_base_note_ = chord_def.base_note; + chord_octave_ = chord_def.octave; + edit_page_ = CHORD_SELECT; +} + +template +void ChordEditor::Close() { + ui.SetButtonIgnoreMask(); + owner_ = nullptr; +} + +}; // namespace OC + +#endif // OC_CHORD_EDIT_H_ diff --git a/software/o_c_REV/OC_chords_presets.h b/software/o_c_REV/OC_chords_presets.h new file mode 100644 index 000000000..2285fa73c --- /dev/null +++ b/software/o_c_REV/OC_chords_presets.h @@ -0,0 +1,84 @@ +#ifndef OC_CHORDS_PRESETS_H_ +#define OC_CHORDS_PRESETS_H_ + +#include "OC_scales.h" +#include "OC_chords.h" +#include + +namespace OC { + + struct Chord { + + int8_t quality; + int8_t inversion; + int8_t voicing; + int8_t base_note; + int8_t octave; + }; + + const Chord chords[] = { + // default + { 0, 0, 0, 0, 0 } + }; + +// these intervals are for notes-in-scale/key, last element is the chord type (monad=1, dyad=2, traid=3, tetrad=4) + + const int8_t qualities[][4] = + { + { 0, 0, 4, 0 }, // fifth + { 0, 2, 2, 0 }, // triad + { 0, 2, 2, 2 }, // seventh + { 0, 3, 1, 0 }, // suspended + { 0, 3, 1, 2 }, // susp. seventh + { 0, 2, 2, 1 }, // sixth + { 0, 2, 2, 4 }, // added ninth + { 0, 2, 2, 6 }, // added eleventh + { 0, 0, 0, 0 }, // unisono + }; + + const int8_t voicing[][4] = + { + // this can't work like this, because it should operate on the inversions, too. + { 0, 0, 0, 0 }, // close + { 0, 0, 0, -1}, // drop 1 + { 0, 0, -1, 0}, // drop 2 + { 0, -1, 0, 0}, // drop 3 + { -1, 1, 1, 1} // spread + }; + + const int8_t inversion[][4] { + { 0, 0, 0, 0 }, + { 1, 0, 0, 0 }, + { 1, 1, 0, 0 }, + { 1, 1, 1, 0 } + }; + + const char* const quality_names[] { + "fifth", "triad", "seventh", "suspended", "susp 7th", "sixth", "added 9th", "added 11th", "unisono" + }; + + const char* const quality_short_names[] { + "5th", "triad", "7th", "susp", "sus7", "6th", "+9th", "+11th", "uni" + }; + + const char* const quality_very_short_names[] { + "5th", "tri", "7th", "sus", "su7", "6th", "9th", "+11", "uni" + }; + + const char* const voicing_names[] { + "close", "drop 1", "drop 2", "drop 3", "spread" + }; + + const char* const voicing_names_short[] { + " - ", "dr1", "dr2", "dr3", "spr" + }; + + const char* const inversion_names[] { + "-", "1", "2", "3" + }; + + const char* const base_note_names[] { + "CV", "#1", "#2", "#3", "#4", "#5", "#6", "#7", "#8", "#9", "#10", "#11", "#12", "#13", "#14", "#15", "#16" + }; +} +#endif // OC_CHORDS_PRESETS_H_ diff --git a/software/o_c_REV/OC_config.h b/software/o_c_REV/OC_config.h index 4b2ce8ee0..de9902966 100644 --- a/software/o_c_REV/OC_config.h +++ b/software/o_c_REV/OC_config.h @@ -1,8 +1,8 @@ #ifndef OC_CONFIG_H_ #define OC_CONFIG_H_ -#if F_CPU != 120000000 -#error "Please compile O&C firmware with CPU speed 120MHz" +#if defined(__MK20DX256__) && F_CPU != 120000000 +#error "Please compile O&C firmware for Teensy 3.2 with CPU speed 120MHz" #endif // 60us = 16.666...kHz : Works, SPI transfer ends 2uS before next ISR @@ -45,7 +45,9 @@ static constexpr unsigned long SETTINGS_SAVE_TIMEOUT_MS = 1000; #define EEPROM_APPDATA_END EEPROMStorage::LENGTH // This is the available space for all apps' settings (\sa OC_apps.ino) -#define EEPROM_APPDATA_BINARY_SIZE (1000 - 4) +#define EEPROM_APPDATA_BINARY_SIZE (EEPROM_APPDATA_END - EEPROM_APPDATA_START - 92) +// (I'm not entirely sure where 92 comes from... it was originally (1000 - 4) which leaves 92 bytes free +// -NJM #define OC_UI_DEBUG #define OC_UI_SEPARATE_ISR diff --git a/software/o_c_REV/OC_debug.cpp b/software/o_c_REV/OC_debug.cpp index d9628edc5..e2471cd32 100644 --- a/software/o_c_REV/OC_debug.cpp +++ b/software/o_c_REV/OC_debug.cpp @@ -5,6 +5,7 @@ #include "OC_debug.h" #include "OC_menus.h" #include "OC_ui.h" +#include "OC_strings.h" #include "util/util_misc.h" #include "extern/dspinst.h" @@ -78,6 +79,26 @@ static void debug_menu_core() { #endif } +static void debug_menu_version() +{ + graphics.setPrintPos(2, 12); + graphics.print(Strings::NAME); + graphics.setPrintPos(2, 22); + graphics.print(Strings::VERSION); + + weegfx::coord_t y = 32; + graphics.setPrintPos(2, y); y += 10; +#ifdef OC_DEV + graphics.print("DEV"); +#else + graphics.print("PROD"); +#endif +#ifdef USB_SERIAL + graphics.setPrintPos(2, y); y += 10; + graphics.print("USB_SERIAL"); +#endif +} + static void debug_menu_gfx() { graphics.drawFrame(0, 0, 128, 64); @@ -117,6 +138,7 @@ struct DebugMenu { static const DebugMenu debug_menus[] = { { " CORE", debug_menu_core }, + { " VERS", debug_menu_version }, { " GFX", debug_menu_gfx }, { " ADC", debug_menu_adc }, #ifdef POLYLFO_DEBUG @@ -159,9 +181,9 @@ void Ui::DebugStats() { while (event_queue_.available()) { UI::Event event = event_queue_.PullEvent(); - if (CONTROL_BUTTON_R == event.control) { + if (CONTROL_BUTTON_R == event.control && UI::EVENT_BUTTON_PRESS == event.type) { exit_loop = true; - } else if (CONTROL_BUTTON_L == event.control) { + } else if (CONTROL_BUTTON_L == event.control && UI::EVENT_BUTTON_PRESS == event.type) { ++current_menu; if (!current_menu->title || !current_menu->display_fn) current_menu = &debug_menus[0]; diff --git a/software/o_c_REV/OC_digital_inputs.cpp b/software/o_c_REV/OC_digital_inputs.cpp index c48cbc2b3..01c7dbd10 100644 --- a/software/o_c_REV/OC_digital_inputs.cpp +++ b/software/o_c_REV/OC_digital_inputs.cpp @@ -4,25 +4,27 @@ #include "OC_gpio.h" #include "OC_options.h" +#if defined(__MK20DX256__) // Teensy 3.2 + /*static*/ uint32_t OC::DigitalInputs::clocked_mask_; /*static*/ volatile uint32_t OC::DigitalInputs::clocked_[DIGITAL_INPUT_LAST]; -void FASTRUN tr1_ISR() { +void FASTRUN OC::tr1_ISR() { OC::DigitalInputs::clock(); } // main clock -void FASTRUN tr2_ISR() { +void FASTRUN OC::tr2_ISR() { OC::DigitalInputs::clock(); } -void FASTRUN tr3_ISR() { +void FASTRUN OC::tr3_ISR() { OC::DigitalInputs::clock(); } -void FASTRUN tr4_ISR() { +void FASTRUN OC::tr4_ISR() { OC::DigitalInputs::clock(); } @@ -84,3 +86,66 @@ void OC::DigitalInputs::Scan() { ScanInput() | ScanInput(); } + +#endif // Teensy 3.2 + + +#if defined(__IMXRT1062__) // Teensy 4.0 or 4.1 + +uint8_t OC::DigitalInputs::clocked_mask_; +IMXRT_GPIO_t * OC::DigitalInputs::port[DIGITAL_INPUT_LAST]; +uint32_t OC::DigitalInputs::bitmask[DIGITAL_INPUT_LAST]; + +FLASHMEM +void OC::DigitalInputs::Init() { + pinMode(TR1, INPUT_PULLUP); + pinMode(TR2, INPUT_PULLUP); + pinMode(TR3, INPUT_PULLUP); + pinMode(TR4, INPUT_PULLUP); + port[0] = (IMXRT_GPIO_t *)digitalPinToPortReg(TR1); + port[1] = (IMXRT_GPIO_t *)digitalPinToPortReg(TR2); + port[2] = (IMXRT_GPIO_t *)digitalPinToPortReg(TR3); + port[3] = (IMXRT_GPIO_t *)digitalPinToPortReg(TR4); + bitmask[0] = digitalPinToBitMask(TR1); + bitmask[1] = digitalPinToBitMask(TR2); + bitmask[2] = digitalPinToBitMask(TR3); + bitmask[3] = digitalPinToBitMask(TR4); + for (unsigned int i=0; i < 4; i++) { + unsigned int bitnum = __builtin_ctz(bitmask[i]); + if (bitnum < 16) { + port[i]->ICR1 |= (0x03 << (bitnum * 2)); // falling edge detect, bits 0-15 + } else { + port[i]->ICR2 |= (0x03 << ((bitnum - 16) * 2)); // falling edge detect, bits 16-31 + } + port[i]->ISR = bitmask[i]; // clear any prior detected edge + } +} + +void OC::DigitalInputs::Scan() { + uint32_t mask[4]; + noInterrupts(); + mask[0] = port[0]->ISR & bitmask[0]; + port[0]->ISR = mask[0]; + mask[1] = port[1]->ISR & bitmask[1]; + port[1]->ISR = mask[1]; + mask[2] = port[2]->ISR & bitmask[2]; + port[2]->ISR = mask[2]; + mask[3] = port[3]->ISR & bitmask[3]; + port[3]->ISR = mask[3]; + interrupts(); + uint8_t new_clocked_mask = 0; + if (mask[0]) new_clocked_mask |= 0x01; + if (mask[1]) new_clocked_mask |= 0x02; + if (mask[2]) new_clocked_mask |= 0x04; + if (mask[3]) new_clocked_mask |= 0x08; + clocked_mask_ = new_clocked_mask; + #if 0 + if (clocked_mask_) { + static elapsedMicros usec; + Serial.printf("%u %u\n", clocked_mask_, (int)usec); + usec = 0; + } + #endif +} + +#endif // Teensy 4.0 or 4.1 diff --git a/software/o_c_REV/OC_digital_inputs.h b/software/o_c_REV/OC_digital_inputs.h index ce8cedbfb..539481a59 100644 --- a/software/o_c_REV/OC_digital_inputs.h +++ b/software/o_c_REV/OC_digital_inputs.h @@ -23,12 +23,19 @@ static constexpr uint32_t DIGITAL_INPUT_2_MASK = DIGITAL_INPUT_MASK(DIGITAL_INPU static constexpr uint32_t DIGITAL_INPUT_3_MASK = DIGITAL_INPUT_MASK(DIGITAL_INPUT_3); static constexpr uint32_t DIGITAL_INPUT_4_MASK = DIGITAL_INPUT_MASK(DIGITAL_INPUT_4); +#if defined(__MK20DX256__) // Teensy 3.2 + template struct InputPinDesc { }; template <> struct InputPinDesc { static constexpr int PIN = TR1; }; template <> struct InputPinDesc { static constexpr int PIN = TR2; }; template <> struct InputPinDesc { static constexpr int PIN = TR3; }; template <> struct InputPinDesc { static constexpr int PIN = TR4; }; +void tr1_ISR(); +void tr2_ISR(); +void tr3_ISR(); +void tr4_ISR(); + class DigitalInputs { public: @@ -38,17 +45,17 @@ class DigitalInputs { static void Scan(); - // @return mask of all pins cloked since last call, reset state + // @return mask of all pins cloked since last call (does not reset state) static inline uint32_t clocked() { return clocked_mask_; } - // @return mask if pin clocked since last call and reset state + // @return mask if pin clocked since last call (does not reset state) template static inline uint32_t clocked() { return clocked_mask_ & (0x1 << input); } - // @return mask if pin clocked since last call, reset state + // @return mask if pin clocked since last call (does not reset state) static inline uint32_t clocked(DigitalInput input) { return clocked_mask_ & (0x1 << input); } @@ -61,6 +68,12 @@ class DigitalInputs { return !digitalReadFast(InputPinMap(input)); } +private: + // clock() only called from interrupt functions + friend void tr1_ISR(); + friend void tr2_ISR(); + friend void tr3_ISR(); + friend void tr4_ISR(); template static inline void clock() { clocked_[input] = 1; } @@ -92,6 +105,48 @@ class DigitalInputs { } }; + +#elif defined(__IMXRT1062__) // Teensy 4.0 or 4.1 + +class DigitalInputs { +public: + static void Init(); + static void reInit() { Init(); } + static void Scan(); + + // @return mask of all pins clocked since last Scan() + static inline uint32_t clocked() { + return clocked_mask_; + } + // @return mask if pin clocked since last Scan() + template static inline uint32_t clocked() { + return clocked(input); + } + static inline uint32_t clocked(DigitalInput input) { + return clocked_mask_ & (0x1 << input); + } + template static inline bool read_immediate() { + return read_immediate(input); + } + static inline bool read_immediate(DigitalInput input) { + switch (input) { + case DIGITAL_INPUT_1: return (digitalRead(TR1) == LOW) ? true : false; + case DIGITAL_INPUT_2: return (digitalRead(TR2) == LOW) ? true : false; + case DIGITAL_INPUT_3: return (digitalRead(TR3) == LOW) ? true : false; + case DIGITAL_INPUT_4: return (digitalRead(TR4) == LOW) ? true : false; + case DIGITAL_INPUT_LAST: break; + } + return false; + } +private: + static uint8_t clocked_mask_; + static IMXRT_GPIO_t *port[DIGITAL_INPUT_LAST]; + static uint32_t bitmask[DIGITAL_INPUT_LAST]; +}; + +#endif + + // Helper class for visualizing digital inputs with decay // Uses 4 bits for decay class DigitalInputDisplay { diff --git a/software/o_c_REV/OC_gpio.cpp b/software/o_c_REV/OC_gpio.cpp new file mode 100644 index 000000000..01e74d6af --- /dev/null +++ b/software/o_c_REV/OC_gpio.cpp @@ -0,0 +1,70 @@ +#include +#include +#include "OC_digital_inputs.h" +#include "OC_gpio.h" +#include "OC_ADC.h" +//#include "OC_options.h" + +// custom pins only for Teensy 4.1 +#if defined(__IMXRT1062__) && defined(ARDUINO_TEENSY41) + +// default settings for traditional O_C hardware wired for Teensy 3.2 +uint8_t CV1=19, CV2=18, CV3=20, CV4=17; +uint8_t TR1=0, TR2=1, TR3=2, TR4=3; +uint8_t OLED_DC=6, OLED_RST=7, OLED_CS=8; +uint8_t DAC_CS=10, DAC_RST=9; +uint8_t encL1=22, encL2=21, butL=23, encR1=16, encR2=15, butR=14; +uint8_t but_top=4, but_bot=5, but_mid=255, but_top2=255, but_bot2=255; +uint8_t OC_GPIO_DEBUG_PIN1=24, OC_GPIO_DEBUG_PIN2=25; +bool ADC33131D_Uses_FlexIO=false; +bool OLED_Uses_SPI1=false; +bool DAC8558_Uses_SPI=false; +bool I2S2_Audio_ADC=false; +bool I2S2_Audio_DAC=false; +bool I2C_Expansion=false; +bool MIDI_Uses_Serial8=false; + +FLASHMEM +void OC::Pinout_Detect() { + float id_voltage = OC::ADC::Read_ID_Voltage(); + Serial.printf("ID voltage (pin A17) = %.3f\n", id_voltage); + + if (id_voltage >= 0.05f && id_voltage < 0.15f) { + CV1 = 255; + CV2 = 255; // CV inputs with ADC33131D + CV3 = 255; + CV4 = 255; + TR1 = 0; + TR2 = 1; + TR3 = 23; + TR4 = 22; + but_top = 14; + but_top2 = 15; + but_bot = 29; + but_bot2 = 28; + but_mid = 20; + OLED_Uses_SPI1 = true; // pins 26=MOSI, 27=SCK + OLED_DC = 39; + OLED_RST = 38; + OLED_CS = 40; + DAC8558_Uses_SPI = true; // pins 10=CS, 11=MOSI, 13=SCK + DAC_CS = 255; // pin 10 controlled by SPI hardware + DAC_RST = 255; + encR1 = 36; + encR2 = 37; + butR = 25; + encL1 = 30; + encL2 = 31; + butL = 24; + OC_GPIO_DEBUG_PIN1 = 16; + OC_GPIO_DEBUG_PIN2 = 17; + ADC33131D_Uses_FlexIO = true; // pins 6=A0, 7=A1, 8=SCK, 9=CS, 12=DATA, 32=A2 + I2S2_Audio_ADC = true; // pins 3=LRCLK, 4=BCLK, 5=DATA, 33=MCLK + I2S2_Audio_DAC = true; // pins 2=DATA, 3=LRCLK, 4=BCLK, 33=MCLK + I2C_Expansion = true; // pins 18=SDA, 19=SCL + MIDI_Uses_Serial8 = true; // pins 34=IN, 35=OUT + } + +} + +#endif // __IMXRT1062__ && ARDUINO_TEENSY41 diff --git a/software/o_c_REV/OC_gpio.h b/software/o_c_REV/OC_gpio.h index 21cd5fc9c..982f3bd1a 100644 --- a/software/o_c_REV/OC_gpio.h +++ b/software/o_c_REV/OC_gpio.h @@ -3,6 +3,10 @@ #include "OC_options.h" +// Teensy 3.2 or Teensy 4.0 have fixed pinout +// +#if defined(__MK20DX256__) || (defined(__IMXRT1062__) && defined(ARDUINO_TEENSY40)) + #ifdef FLIP_180 #define CV4 19 #define CV3 18 @@ -35,13 +39,14 @@ #define OLED_RST 7 #define OLED_CS 8 -// OLED CS is active low -#define OLED_CS_ACTIVE LOW -#define OLED_CS_INACTIVE HIGH - -#define DAC_RST 9 #define DAC_CS 10 +#ifdef VOR + #define but_mid 9 +#else + #define DAC_RST 9 +#endif + // NOTE: encoder pins R1/R2 changed for rev >= 2c #ifdef FLIP_180 #define encL1 16 @@ -65,8 +70,78 @@ #define OC_GPIO_DEBUG_PIN1 24 #define OC_GPIO_DEBUG_PIN2 25 +#endif // Teensy 3.2 or Teensy 4.0 + +// Teensy 4.1 has different pinouts depending on voltage at pin 41/A17 +// +#if defined(__IMXRT1062__) && defined(ARDUINO_TEENSY41) + +extern uint8_t CV1, CV2, CV3, CV4; +extern uint8_t TR1, TR2, TR3, TR4; +extern uint8_t OLED_DC, OLED_RST, OLED_CS; +extern uint8_t DAC_CS, DAC_RST; +extern uint8_t encL1, encL2, butL, encR1, encR2, butR; +extern uint8_t but_top, but_bot, but_mid, but_top2, but_bot2; +extern uint8_t OC_GPIO_DEBUG_PIN1, OC_GPIO_DEBUG_PIN2; + +#endif + +// OLED CS is active low +#define OLED_CS_ACTIVE LOW +#define OLED_CS_INACTIVE HIGH + #define OC_GPIO_BUTTON_PINMODE INPUT_PULLUP #define OC_GPIO_TRx_PINMODE INPUT_PULLUP #define OC_GPIO_ENC_PINMODE INPUT_PULLUP +/* local copy of pinMode (cf. cores/pins_teensy.c), using faster slew rate */ + +namespace OC { + +void inline pinMode(uint8_t pin, uint8_t mode) { + +#if defined(__MK20DX256__) + volatile uint32_t *config; + + if (pin >= CORE_NUM_DIGITAL) return; + config = portConfigRegister(pin); + + if (mode == OUTPUT || mode == OUTPUT_OPENDRAIN) { + #ifdef KINETISK + *portModeRegister(pin) = 1; + #else + *portModeRegister(pin) |= digitalPinToBitMask(pin); // TODO: atomic + #endif + /* use fast slew rate for output */ + *config = PORT_PCR_DSE | PORT_PCR_MUX(1); + if (mode == OUTPUT_OPENDRAIN) { + *config |= PORT_PCR_ODE; + } else { + *config &= ~PORT_PCR_ODE; + } + } else { + #ifdef KINETISK + *portModeRegister(pin) = 0; + #else + *portModeRegister(pin) &= ~digitalPinToBitMask(pin); + #endif + if (mode == INPUT) { + *config = PORT_PCR_MUX(1); + } else if (mode == INPUT_PULLUP) { + *config = PORT_PCR_MUX(1) | PORT_PCR_PE | PORT_PCR_PS; + } else if (mode == INPUT_PULLDOWN) { + *config = PORT_PCR_MUX(1) | PORT_PCR_PE; + } else { // INPUT_DISABLE + *config = 0; + } + } +#else + ::pinMode(pin, mode); // for Teensy 4.x, just use normal pinMode +#endif + } + +void Pinout_Detect(); + +} + #endif // OC_GPIO_H_ diff --git a/software/o_c_REV/OC_menus.cpp b/software/o_c_REV/OC_menus.cpp index eed06a8d0..9cea82638 100644 --- a/software/o_c_REV/OC_menus.cpp +++ b/software/o_c_REV/OC_menus.cpp @@ -1,37 +1,63 @@ #include +#include #include "OC_config.h" #include "OC_core.h" #include "OC_bitmaps.h" #include "OC_menus.h" #include "OC_DAC.h" #include "OC_options.h" +#include "util/util_templates.h" namespace OC { -static constexpr weegfx::coord_t note_circle_r = 28; - -static struct coords { +struct coords { weegfx::coord_t x, y; -} circle_pos_lut[12]; - -static void init_circle_lut() { - static const float pi = 3.14159265358979323846f; - static const float semitone_radians = (2.f * pi / 12.f); - - for (int i = 0; i < 12; ++i) { - float rads = ((i + 12 - 3) % 12) * semitone_radians; - float x = note_circle_r * cos(rads); - float y = note_circle_r * sin(rads); - circle_pos_lut[i].x = x; - circle_pos_lut[i].y = y; - } +}; + +static constexpr float note_circle_r = 28.f; +static constexpr float pi = 3.14159265358979323846f; +static constexpr float semitone_radians = (2.f * pi / 12.f); +constexpr float index_to_rads(size_t index) { return ((index + 12 - 3) % 12) * semitone_radians; } + +template constexpr coords generate_circle_coords() { + return { + static_cast(note_circle_r * cosf(index_to_rads(index))), + static_cast(note_circle_r * sinf(index_to_rads(index))) + }; +} + +template +constexpr std::array generate_circle_pos_lut(util::index_sequence) { + return { generate_circle_coords()... }; } +static constexpr std::array circle_pos_lut = generate_circle_pos_lut(util::make_index_sequence<12>::type()); + namespace menu { -void Init() { - init_circle_lut(); +void Init() +{ }; +void DrawEditIcon(weegfx::coord_t x, weegfx::coord_t y, int value, int min_value, int max_value) { + const uint8_t *src = OC::bitmap_edit_indicators_8; + if (value == max_value) + src += OC::kBitmapEditIndicatorW * 2; + else if (value == min_value) + src += OC::kBitmapEditIndicatorW; + + graphics.drawBitmap8(x - 5, y + 1, OC::kBitmapEditIndicatorW, src); +} + +void DrawEditIcon(weegfx::coord_t x, weegfx::coord_t y, int value, const settings::value_attr &attr) { + const uint8_t *src = OC::bitmap_edit_indicators_8; + if (value == attr.max_) + src += OC::kBitmapEditIndicatorW * 2; + else if (value == attr.min_) + src += OC::kBitmapEditIndicatorW; + + graphics.drawBitmap8(x - 5, y + 1, OC::kBitmapEditIndicatorW, src); +} + }; // namespace menu diff --git a/software/o_c_REV/OC_menus.h b/software/o_c_REV/OC_menus.h index 088528c60..53edd7a3c 100644 --- a/software/o_c_REV/OC_menus.h +++ b/software/o_c_REV/OC_menus.h @@ -31,6 +31,7 @@ #include "util/util_misc.h" #include "util/util_settings.h" #include "OC_DAC.h" +#include "OC_chords.h" namespace OC { @@ -144,27 +145,9 @@ class ScreenCursor { int screen_line_; }; -inline void DrawEditIcon(weegfx::coord_t x, weegfx::coord_t y, int value, int min_value, int max_value) { - const uint8_t *src = OC::bitmap_edit_indicators_8; - if (value == max_value) - src += OC::kBitmapEditIndicatorW * 2; - else if (value == min_value) - src += OC::kBitmapEditIndicatorW; +void DrawEditIcon(weegfx::coord_t x, weegfx::coord_t y, int value, int min_value, int max_value); +void DrawEditIcon(weegfx::coord_t x, weegfx::coord_t y, int value, const settings::value_attr &attr); - graphics.drawBitmap8(x - 5, y + 1, OC::kBitmapEditIndicatorW, src); -} - -inline void DrawEditIcon(weegfx::coord_t x, weegfx::coord_t y, int value, const settings::value_attr &attr) { - const uint8_t *src = OC::bitmap_edit_indicators_8; - if (value == attr.max_) - src += OC::kBitmapEditIndicatorW * 2; - else if (value == attr.min_) - src += OC::kBitmapEditIndicatorW; - - graphics.drawBitmap8(x - 5, y + 1, OC::kBitmapEditIndicatorW, src); -} - -/* Removed jej 8/26/2018 inline void DrawChord(weegfx::coord_t x, weegfx::coord_t y, int width, int value, int mem_offset) { OC::Chord *active_chord = &OC::user_chords[value + mem_offset * OC::Chords::NUM_CHORDS]; @@ -190,7 +173,7 @@ inline void DrawChord(weegfx::coord_t x, weegfx::coord_t y, int width, int value y_pos = y + OC::voicing[_voicing][3] * 16; CONSTRAIN(y_pos, 8, 64); graphics.drawFrame(x, y_pos, width, width); -}*/ +} inline void DrawMiniChord(weegfx::coord_t x, weegfx::coord_t y, uint8_t count, uint8_t indicator) { int8_t _x = x - count * 3 - 4; diff --git a/software/o_c_REV/OC_options.h b/software/o_c_REV/OC_options.h index 61c46b725..3a1d4f139 100644 --- a/software/o_c_REV/OC_options.h +++ b/software/o_c_REV/OC_options.h @@ -18,19 +18,39 @@ /* ------------ print debug messages to USB serial -------------------------------------------------- */ //#define PRINT_DEBUG /* ------------ flip screen / IO mapping ------------------------------------------------------------ */ -//#define FLIP_180 +// #define FLIP_180 /* ------------ invert screen pixels ---------------------------------------------------------------- */ //#define INVERT_DISPLAY /* ------------ use DAC8564 ------------------------------------------------------------------------- */ //#define DAC8564 + +// Phazerville Suite includes full support for the Plum Audio hardware variants with VOR +// The VOR flag is set in platformio.ini. +/* ------------ uncomment for use with Plum Audio VOR anabled versions (OCP, 1uO_c v2, 4Robots) --------------------------------------------------------- */ +//#define VOR + +// idk what this means so I'm keeping it -NJM +#if defined(VOR) + #define IO_10V +#endif + /* Flags for the full-width apps, these enable/disable them in OC_apps.ino but also zero out the app */ -/* files to prevent them from taking up space. Only Enigma is enabled by default. */ -#define ENABLE_APP_ENIGMA -#define ENABLE_APP_MIDI -#define ENABLE_APP_NEURAL_NETWORK -#define ENABLE_APP_PONG -#define ENABLE_APP_DARKEST_TIMELINE +/* files to prevent them from taking up space. */ +// Sets of these are now defined in platformio.ini for convenience. -NJM + +// Hemisphere apps +// #define ENABLE_APP_ENIGMA +// #define ENABLE_APP_MIDI +// #define ENABLE_APP_NEURAL_NETWORK +// #define ENABLE_APP_PONG +// #define ENABLE_APP_DARKEST_TIMELINE + +// Stock O&C apps +// #define ENABLE_APP_QUANTERMAIN +// #define ENABLE_APP_METAQ +// #define ENABLE_APP_CHORDS +// #define ENABLE_APP_SEQUINS #endif diff --git a/software/o_c_REV/OC_scale_edit.h b/software/o_c_REV/OC_scale_edit.h new file mode 100644 index 000000000..0f56446fc --- /dev/null +++ b/software/o_c_REV/OC_scale_edit.h @@ -0,0 +1,656 @@ +#ifndef OC_SCALE_EDIT_H_ +#define OC_SCALE_EDIT_H_ + +#include "OC_bitmaps.h" +#include "OC_strings.h" +#include "OC_DAC.h" + +namespace OC { + +// Scale editor +// Edits both scale length and note note values, as well as a mask of "active" +// notes that the quantizer uses; some scales are read-only, in which case +// only the mask is editable +// +// The owner class needs to provide callbacks to get notification when values +// change: see ASR, A_SEQ, DQ, etc for details +// + +const bool DUMMY = false; + +enum SCALE_EDIT_PAGES { + _SCALE, + _ROOT, + _TRANSPOSE, + _SCALING +}; + +template +class ScaleEditor { +public: + + void Init(bool mode) { + owner_ = nullptr; + scale_name_ = "?!"; + scale_ = mutable_scale_ = &dummy_scale; + mask_ = 0; + cursor_pos_ = 0; + scaling_cursor_pos_ = 0; + num_notes_ = 0; + edit_this_scale_ = 0; + /* mode: true = Meta-Q; false = scale editor */ + SEQ_MODE = mode; + edit_page_ = _SCALE; + ticks_ = 0; + } + + bool active() const { + return nullptr != owner_; + } + + void Edit(Owner *owner, int scale) { + if (OC::Scales::SCALE_NONE == scale) + return; + + if (scale < OC::Scales::SCALE_USER_LAST) { + scale_ = mutable_scale_ = &OC::user_scales[scale]; + scale_name_ = OC::scale_names_short[scale]; + // Serial.print("Editing mutable scale "); + // Serial.println(scale_name_); + } else { + scale_ = &OC::Scales::GetScale(scale); + mutable_scale_ = nullptr; + scale_name_ = OC::scale_names_short[scale]; + // Serial.print("Editing const scale "); + // Serial.println(scale_name_); + } + owner_ = owner; + BeginEditing(SEQ_MODE); + } + + void Close(); + void Draw(); + void HandleButtonEvent(const UI::Event &event); + void HandleEncoderEvent(const UI::Event &event); + static uint16_t RotateMask(uint16_t mask, int num_notes, int amount); + +private: + + Owner *owner_; + const char * scale_name_; + const braids::Scale *scale_; + Scale *mutable_scale_; + + uint16_t mask_; + size_t cursor_pos_; + size_t scaling_cursor_pos_; + size_t num_notes_; + int8_t edit_this_scale_; + bool SEQ_MODE; + uint8_t edit_page_; + uint32_t ticks_; + + void BeginEditing(bool mode); + void move_cursor(int offset); + + void toggle_mask(); + void invert_mask(); + + void apply_mask(uint16_t mask) { + + if (mask_ != mask) { + mask_ = mask; + owner_->update_scale_mask(mask_, edit_this_scale_); + } + } + + void reset_scale(); + void change_note(size_t pos, int delta, bool notify); + void handleButtonLeft(const UI::Event &event); + void handleButtonUp(const UI::Event &event); + void handleButtonDown(const UI::Event &event); +}; + +template +void ScaleEditor::Draw() { + + if (edit_page_ < _SCALING) { + + size_t num_notes = num_notes_; + static constexpr weegfx::coord_t kMinWidth = 64; + weegfx::coord_t w = + mutable_scale_ ? ((num_notes + 1) * 7) : (num_notes * 7); + + if (w < kMinWidth || (edit_page_ == _ROOT) || (edit_page_ == _TRANSPOSE)) w = kMinWidth; + + weegfx::coord_t x = 64 - (w + 1) / 2; + weegfx::coord_t y = 16 - 3; + weegfx::coord_t h = 36; + + graphics.clearRect(x, y, w + 4, h); + graphics.drawFrame(x, y, w + 5, h); + + x += 2; + y += 3; + + graphics.setPrintPos(x, y); + graphics.print(scale_name_); + + if (SEQ_MODE) { + ticks_++; + uint8_t id = edit_this_scale_; + if (edit_this_scale_ == owner_->get_scale_select()) + id += 4; + graphics.print(OC::Strings::scale_id[id]); + } + + if (SEQ_MODE && edit_page_) { + + switch (edit_page_) { + + case _ROOT: + case _TRANSPOSE: { + x += w / 2 - (25 + 3); y += 10; + graphics.drawFrame(x, y, 25, 20); + graphics.drawFrame(x + 32, y, 25, 20); + // print root: + int root = owner_->get_root(edit_this_scale_); + if (root == 1 || root == 3 || root == 6 || root == 8 || root == 10) + graphics.setPrintPos(x + 7, y + 7); + else + graphics.setPrintPos(x + 9, y + 7); + graphics.print(OC::Strings::note_names_unpadded[root]); + // print transpose: + int transpose = owner_->get_transpose(edit_this_scale_); + graphics.setPrintPos(x + 32 + 5, y + 7); + if (transpose >= 0) + graphics.print("+"); + graphics.print(transpose); + // draw cursor + if (edit_page_ == _ROOT) + graphics.invertRect(x - 1, y - 1, 27, 22); + else + graphics.invertRect(x + 32 - 1, y - 1, 27, 22); + } + break; + default: + break; + } + } + else { + // edit scale + graphics.setPrintPos(x, y + 24); + if (cursor_pos_ != num_notes) { + graphics.movePrintPos(weegfx::Graphics::kFixedFontW, 0); + if (mutable_scale_ && OC::ui.read_immediate(OC::CONTROL_BUTTON_L)) + graphics.drawBitmap8(x + 1, y + 23, kBitmapEditIndicatorW, bitmap_edit_indicators_8); + else if (mutable_scale_) + graphics.drawBitmap8(x + 1, y + 23, 4, bitmap_indicator_4x8); + + uint32_t note_value = scale_->notes[cursor_pos_]; + uint32_t cents = (note_value * 100) >> 7; + uint32_t frac_cents = ((note_value * 100000) >> 7) - cents * 1000; + // move print position, so that things look somewhat nicer + if (cents < 10) + graphics.movePrintPos(weegfx::Graphics::kFixedFontW * -3, 0); + else if (cents < 100) + graphics.movePrintPos(weegfx::Graphics::kFixedFontW * -2, 0); + else if (cents < 1000) + graphics.movePrintPos(weegfx::Graphics::kFixedFontW * -1, 0); + // justify left ... + if (! mutable_scale_) + graphics.movePrintPos(weegfx::Graphics::kFixedFontW * -1, 0); + graphics.printf("%4u.%02uc", cents, (frac_cents + 5) / 10); + + } else { + graphics.print((int)num_notes); + } + + x += mutable_scale_ ? (w >> 0x1) - (((num_notes) * 7 + 4) >> 0x1) : (w >> 0x1) - (((num_notes - 1) * 7 + 4) >> 0x1); + y += 11; + uint16_t mask = mask_; + for (size_t i = 0; i < num_notes; ++i, x += 7, mask >>= 1) { + if (mask & 0x1) + graphics.drawRect(x, y, 4, 8); + else + graphics.drawBitmap8(x, y, 4, bitmap_empty_frame4x8); + + if (i == cursor_pos_) + graphics.drawFrame(x - 2, y - 2, 8, 12); + } + if (mutable_scale_) { + graphics.drawBitmap8(x, y, 4, bitmap_end_marker4x8); + if (cursor_pos_ == num_notes) + graphics.drawFrame(x - 2, y - 2, 8, 12); + } + } + } + else { + // scaling ... + weegfx::coord_t x = 0; + weegfx::coord_t y = 0; + weegfx::coord_t h = 64; + weegfx::coord_t w = 128; + + graphics.clearRect(x, y, w, h); + graphics.drawFrame(x, y, w, h); + + x += 5; + y += 5; + graphics.setPrintPos(x, y); + graphics.print(OC::Strings::scaling_string[0]); + graphics.print(OC::Strings::channel_id[DAC_CHANNEL_A]); + graphics.setPrintPos(x + 87, y); + graphics.print(OC::voltage_scalings[OC::DAC::get_voltage_scaling(DAC_CHANNEL_A)]); + y += 16; + graphics.setPrintPos(x, y); + graphics.print(OC::Strings::scaling_string[0]); + graphics.print(OC::Strings::channel_id[DAC_CHANNEL_B]); + graphics.setPrintPos(x + 87, y); + graphics.print(OC::voltage_scalings[OC::DAC::get_voltage_scaling(DAC_CHANNEL_B)]); + y += 16; + graphics.setPrintPos(x, y); + graphics.print(OC::Strings::scaling_string[0]); + graphics.print(OC::Strings::channel_id[DAC_CHANNEL_C]); + graphics.setPrintPos(x + 87, y); + graphics.print(OC::voltage_scalings[OC::DAC::get_voltage_scaling(DAC_CHANNEL_C)]); + y += 16; + graphics.setPrintPos(x, y); + graphics.print(OC::Strings::scaling_string[0]); + graphics.print(OC::Strings::channel_id[DAC_CHANNEL_D]); + graphics.setPrintPos(x + 87, y); + graphics.print(OC::voltage_scalings[OC::DAC::get_voltage_scaling(DAC_CHANNEL_D)]); + // draw cursor: + graphics.invertRect(x - 2, (scaling_cursor_pos_ << 4) + 3, w - 6, 11); + } +} + +template +void ScaleEditor::HandleButtonEvent(const UI::Event &event) { + if (UI::EVENT_BUTTON_PRESS == event.type) { + switch (event.control) { + case OC::CONTROL_BUTTON_UP: + handleButtonUp(event); + break; + case OC::CONTROL_BUTTON_DOWN: + handleButtonDown(event); + break; + case OC::CONTROL_BUTTON_L: + handleButtonLeft(event); + break; + case OC::CONTROL_BUTTON_R: + { + if (edit_page_ < _SCALING) + Close(); + else + edit_page_ = _SCALE; + } + break; + default: + break; + } + } + else if (UI::EVENT_BUTTON_LONG_PRESS == event.type) { + + switch (event.control) { + case OC::CONTROL_BUTTON_L: + { + if (edit_page_ < _SCALING) + edit_page_ = _SCALING; + } + break; + default: + { + if (edit_page_ == _SCALING) + edit_page_ = _SCALE; + } + break; + } + } +} + +template +void ScaleEditor::HandleEncoderEvent(const UI::Event &event) { + bool scale_changed = false; + uint16_t mask = mask_; + + if (OC::CONTROL_ENCODER_L == event.control) { + + if (SEQ_MODE && OC::ui.read_immediate(OC::CONTROL_BUTTON_UP)) { + + // we're in Meta-Q, so we can change the scale: + + int _scale = owner_->get_scale(edit_this_scale_) + event.value; + CONSTRAIN(_scale, OC::Scales::SCALE_USER_0, OC::Scales::NUM_SCALES-1); + + if (_scale == OC::Scales::SCALE_NONE) { + // just skip this here ... + if (event.value > 0) + _scale++; + else + _scale--; + } + // update active scale with mask/root/transpose settings, and set flag: + owner_->set_scale_at_slot(_scale, mask_, owner_->get_root(edit_this_scale_), owner_->get_transpose(edit_this_scale_), edit_this_scale_); + scale_changed = true; + + if (_scale < OC::Scales::SCALE_USER_LAST) { + scale_ = mutable_scale_ = &OC::user_scales[_scale]; + scale_name_ = OC::scale_names_short[_scale]; + } + else { + scale_ = &OC::Scales::GetScale(_scale); + mutable_scale_ = nullptr; + scale_name_ = OC::scale_names_short[_scale]; + } + cursor_pos_ = 0; + num_notes_ = scale_->num_notes; + mask_ = owner_->get_scale_mask(edit_this_scale_); // ? can go, because the mask didn't change + ticks_ = 0; + } + else { + move_cursor(event.value); + } + } else if (OC::CONTROL_ENCODER_R == event.control) { + + switch (edit_page_) { + + case _SCALE: + { + bool handled = false; + if (mutable_scale_) { + if (cursor_pos_ < num_notes_) { + if (event.mask & OC::CONTROL_BUTTON_L) { + OC::ui.IgnoreButton(OC::CONTROL_BUTTON_L); + change_note(cursor_pos_, event.value, false); + scale_changed = true; + handled = true; + } + } else { + if (cursor_pos_ == num_notes_) { + int num_notes = num_notes_; + num_notes += event.value; + CONSTRAIN(num_notes, kMinScaleLength, kMaxScaleLength); + + num_notes_ = num_notes; + if (event.value > 0) { + for (size_t pos = cursor_pos_; pos < num_notes_; ++pos) + change_note(pos, 0, false); + + // Enable new notes by default + mask |= ~(0xffff << (num_notes_ - cursor_pos_)) << cursor_pos_; + } else { + // scale might be shortened to where no notes are active in mask + if (0 == (mask & ~(0xffff < num_notes_))) + mask |= 0x1; + } + + mutable_scale_->num_notes = num_notes_; + cursor_pos_ = num_notes_; + handled = scale_changed = true; + } + } + } + if (!handled) { + mask = RotateMask(mask_, num_notes_, event.value); + } + } + break; + case _ROOT: + { + int _root = owner_->get_root(edit_this_scale_) + event.value; + CONSTRAIN(_root, 0, 11); + owner_->set_scale_at_slot(owner_->get_scale(edit_this_scale_), mask_, _root, owner_->get_transpose(edit_this_scale_), edit_this_scale_); + } + break; + case _TRANSPOSE: + { + int _transpose = owner_->get_transpose(edit_this_scale_) + event.value; + CONSTRAIN(_transpose, -12, 12); + owner_->set_scale_at_slot(owner_->get_scale(edit_this_scale_), mask_, owner_->get_root(edit_this_scale_), _transpose, edit_this_scale_); + } + break; + case _SCALING: + { + int item = scaling_cursor_pos_ + event.value; + CONSTRAIN(item, DAC_CHANNEL_A, DAC_CHANNEL_LAST - 0x1); + scaling_cursor_pos_ = item; + } + break; + default: + break; + } + } + // This isn't entirely atomic + apply_mask(mask); + if (scale_changed) + owner_->scale_changed(); +} + +template +void ScaleEditor::move_cursor(int offset) { + + switch (edit_page_) { + + case _ROOT: + case _TRANSPOSE: + { + int8_t item = edit_page_ + offset; + CONSTRAIN(item, _ROOT, _TRANSPOSE); + edit_page_ = item; + } + break; + case _SCALING: + { + int8_t scaling = OC::DAC::get_voltage_scaling(scaling_cursor_pos_) + offset; + CONSTRAIN(scaling, VOLTAGE_SCALING_1V_PER_OCT, VOLTAGE_SCALING_LAST - 0x1); + OC::DAC::set_scaling(scaling, scaling_cursor_pos_); + } + break; + default: + { + int cursor_pos = cursor_pos_ + offset; + const int max = mutable_scale_ ? num_notes_ : num_notes_ - 1; + CONSTRAIN(cursor_pos, 0, max); + cursor_pos_ = cursor_pos; + } + break; + } +} + +template +void ScaleEditor::handleButtonUp(const UI::Event &event) { + + if (event.mask & OC::CONTROL_BUTTON_L) { + OC::ui.IgnoreButton(OC::CONTROL_BUTTON_L); + if (cursor_pos_ == num_notes_) + reset_scale(); + else + change_note(cursor_pos_, 128, true); + } else { + if (!SEQ_MODE) + invert_mask(); + else { + + if (ticks_ > 250) { + edit_this_scale_++; + if (edit_this_scale_ > 0x3) + edit_this_scale_ = 0; + + uint8_t scale = owner_->get_scale(edit_this_scale_); + // Serial.println(scale); + if (scale < OC::Scales::SCALE_USER_LAST) { + scale_ = mutable_scale_ = &OC::user_scales[scale]; + scale_name_ = OC::scale_names_short[scale]; + // Serial.print("Editing mutable scale "); + // Serial.println(scale_name_); + } + else { + scale_ = &OC::Scales::GetScale(scale); + mutable_scale_ = nullptr; + scale_name_ = OC::scale_names_short[scale]; + // Serial.print("Editing const scale "); + // Serial.println(scale_name_); + } + cursor_pos_ = 0; + num_notes_ = scale_->num_notes; + mask_ = owner_->get_scale_mask(edit_this_scale_); + } + } + } +} + +template +void ScaleEditor::handleButtonDown(const UI::Event &event) { + if (event.mask & OC::CONTROL_BUTTON_L) { + OC::ui.IgnoreButton(OC::CONTROL_BUTTON_L); + change_note(cursor_pos_, -128, true); + } else { + if (!SEQ_MODE) + invert_mask(); + else { + + if (ticks_ > 250) { + edit_this_scale_--; + if (edit_this_scale_ < 0) + edit_this_scale_ = 3; + + uint8_t scale = owner_->get_scale(edit_this_scale_); + // Serial.println(scale); + if (scale < OC::Scales::SCALE_USER_LAST) { + scale_ = mutable_scale_ = &OC::user_scales[scale]; + scale_name_ = OC::scale_names_short[scale]; + // Serial.print("Editing mutable scale "); + // Serial.println(scale_name_); + } + else { + scale_ = &OC::Scales::GetScale(scale); + mutable_scale_ = nullptr; + scale_name_ = OC::scale_names_short[scale]; + // Serial.print("Editing const scale "); + // Serial.println(scale_name_); + } + cursor_pos_ = 0; + num_notes_ = scale_->num_notes; + mask_ = owner_->get_scale_mask(edit_this_scale_); + } + } + } +} + +template +void ScaleEditor::handleButtonLeft(const UI::Event &) { + + if (SEQ_MODE && OC::ui.read_immediate(OC::CONTROL_BUTTON_UP)) { + + if (edit_page_ == _SCALE) + edit_page_ = _ROOT; // switch to root + // and don't accidentally change scale slot: + ticks_ = 0x0; + } + else { + // edit scale mask + if (edit_page_ > _SCALE) + edit_page_ = _SCALE; + else { + uint16_t m = 0x1 << cursor_pos_; + uint16_t mask = mask_; + + if (cursor_pos_ < num_notes_) { + // toggle note active state; avoid 0 mask + if (mask & m) { + if ((mask & ~(0xffff << num_notes_)) != m) + mask &= ~m; + } else { + mask |= m; + } + apply_mask(mask); + } + } + } +} + +template +void ScaleEditor::invert_mask() { + uint16_t m = ~(0xffffU << num_notes_); + uint16_t mask = mask_; + // Don't invert to zero + if ((mask & m) != m) + mask ^= m; + apply_mask(mask); +} + +template +/*static*/ uint16_t ScaleEditor::RotateMask(uint16_t mask, int num_notes, int amount) { + uint16_t used_bits = ~(0xffffU << num_notes); + mask &= used_bits; + + if (amount < 0) { + amount = -amount % num_notes; + mask = (mask >> amount) | (mask << (num_notes - amount)); + } else { + amount = amount % num_notes; + mask = (mask << amount) | (mask >> (num_notes - amount)); + } + return mask | ~used_bits; // fill upper bits +} + +template +void ScaleEditor::reset_scale() { + // Serial.println("Resetting scale to SEMI"); + + *mutable_scale_ = OC::Scales::GetScale(OC::Scales::SCALE_SEMI); + num_notes_ = mutable_scale_->num_notes; + cursor_pos_ = num_notes_; + mask_ = ~(0xfff << num_notes_); + apply_mask(mask_); +} + +template +void ScaleEditor::change_note(size_t pos, int delta, bool notify) { + if (mutable_scale_ && pos < num_notes_) { + int32_t note = mutable_scale_->notes[pos] + delta; + + const int32_t min = pos > 0 ? mutable_scale_->notes[pos - 1] : 0; + const int32_t max = pos < num_notes_ - 1 ? mutable_scale_->notes[pos + 1] : mutable_scale_->span; + + // TODO It's probably possible to construct a pothological scale, + // maybe factor cursor_pos into it somehow? + if (note <= min) note = pos > 0 ? min + 1 : 0; + if (note >= max) note = max - 1; + mutable_scale_->notes[pos] = note; +// braids::SortScale(*mutable_scale_); // TODO side effects? + + if (notify) + owner_->scale_changed(); + } +} + +template +void ScaleEditor::BeginEditing(bool mode) { + + cursor_pos_ = 0; + num_notes_ = scale_->num_notes; + + if (mode) { // == meta-Q + edit_this_scale_ = owner_->get_scale_select(); + mask_ = owner_->get_scale_mask(edit_this_scale_); + OC::ui._preemptScreensaver(true); + } + else { + mask_ = owner_->get_scale_mask(DUMMY); + } +} + +template +void ScaleEditor::Close() { + ui.SetButtonIgnoreMask(); + OC::ui._preemptScreensaver(false); + owner_ = nullptr; + edit_this_scale_ = 0; + edit_page_ = _SCALE; +} + +}; // namespace OC + +#endif // OC_SCALE_EDIT_H_ diff --git a/software/o_c_REV/OC_scales.cpp b/software/o_c_REV/OC_scales.cpp index 344b37128..6f823c8a2 100644 --- a/software/o_c_REV/OC_scales.cpp +++ b/software/o_c_REV/OC_scales.cpp @@ -24,10 +24,10 @@ const Scale &Scales::GetScale(int index) { } const char* const scale_names_short[] = { - "USER1", - "USER2", - "USER3", - "USER4", + "USR1", + "USR2", + "USR3", + "USR4", "OFF ", "SEMI", "IONI", @@ -159,7 +159,26 @@ const char* const scale_names_short[] = { "14S3", "12S3", "10S3", - "8S3" + "8S3", + + "5+7", // Root +5th + 7th (5+7), + "5+6", // Root + 5th + 6th (5+6), + "3b7-",// Minor Triad + 7 (3b+5+7), + "3b7+",// Major Triad + 7 (Triad+7), + "3b6-",// Minor Triad + 6th (3b+5+6), + "3b6+",// Major Triad + 6th (Triad+6), + "5th", // Fifth, + "3b+", // major triad (Triad), + "3b-", // minor triad (3b+5), + "HAR-",// Harmonic Minor (Harm Minor), aka "HMIN" + + "LOn6", + "IAUG", + "MBKH", + "FREY", + "LY#9", + "UTLO", + }; const char* const scale_names[] = { @@ -298,7 +317,26 @@ const char* const scale_names[] = { "21-7-SD3[14]", "18-6-SD3[12]", "15-5-SD3[10]", - "12-4-SD3[8]" + "12-4-SD3[8]", + + "5th+7th", // Root +5th + 7th (5+7), + "5th+6th", // Root + 5th + 6th (5+6), + "Triad min+7",// Minor Triad + 7 (3b+5+7), + "Triad maj+7",// Major Triad + 7 (Triad+7), + "Triad min+6",// Minor Triad + 6th (3b+5+6), + "Triad maj+6",// Major Triad + 6th (Triad+6), + "Fifth",// Fifth, + "TriadMaj", // major triad (Triad), + "TriadMin",// minor triad (3b+5), + "HarmonicMin",// Harmonic Minor (Harm Minor), + + "Locrian n6", + "Ionian aug", + "Misheberakh", + "Freygish", + "Lydian #9", + "Ultralocrian", + }; const char* const voltage_scalings[] = { diff --git a/software/o_c_REV/OC_sequence_edit.h b/software/o_c_REV/OC_sequence_edit.h new file mode 100644 index 000000000..5c42d5785 --- /dev/null +++ b/software/o_c_REV/OC_sequence_edit.h @@ -0,0 +1,422 @@ +#ifndef OC_SEQUENCE_EDIT_H_ +#define OC_SEQUENCE_EDIT_H_ + +#include "OC_bitmaps.h" +#include "OC_patterns.h" +#include "OC_patterns_presets.h" +#include "OC_options.h" + +namespace OC { + +// Pattern editor +// based on scale editor written by Patrick Dowling, adapted for TU, re-adapted for OC +// + +template +class PatternEditor { +public: + + void Init() { + owner_ = nullptr; + pattern_name_ = "?!"; + pattern_ = mutable_pattern_ = &dummy_pattern; + mask_ = 0; + cursor_pos_ = 0; + num_slots_ = 0; + edit_this_sequence_ = 0; + } + + bool active() const { + return nullptr != owner_; + } + + void Edit(Owner *owner, int pattern) { + if (OC::Patterns::PATTERN_NONE == pattern) + return; + + pattern_ = mutable_pattern_ = &OC::user_patterns[pattern]; + pattern_name_ = OC::pattern_names_short[pattern]; + // Serial.print("Editing user pattern "); + // Serial.println(pattern_name_); + owner_ = owner; + + BeginEditing(); + } + + void Close(); + + void Draw(); + void HandleButtonEvent(const UI::Event &event); + void HandleEncoderEvent(const UI::Event &event); + static uint16_t RotateMask(uint16_t mask, int num_slots, int amount); + +private: + + Owner *owner_; + const char * pattern_name_; + const OC::Pattern *pattern_; + Pattern *mutable_pattern_; + + uint16_t mask_; + int8_t edit_this_sequence_; + size_t cursor_pos_; + size_t num_slots_; + + void BeginEditing(); + + void move_cursor(int offset); + void toggle_mask(); + void invert_mask(); + void clear_mask(); + void copy_sequence(); + void paste_sequence(); + + void apply_mask(uint16_t mask) { + + if (mask_ != mask) { + mask_ = mask; + owner_->update_pattern_mask(mask_, edit_this_sequence_); + } + + bool force = (owner_->get_current_sequence() == edit_this_sequence_); + owner_->pattern_changed(mask, force); + } + + void change_slot(size_t pos, int delta, bool notify); + void handleButtonLeft(const UI::Event &event); + void handleButtonUp(const UI::Event &event); + void handleButtonDown(const UI::Event &event); +}; + +template +void PatternEditor::Draw() { + size_t num_slots = num_slots_; + + weegfx::coord_t const w = 128; + weegfx::coord_t const h = 64; + weegfx::coord_t x = 0; + weegfx::coord_t y = 0; + graphics.clearRect(x, y, w, h); + graphics.drawFrame(x, y, w, h); + + x += 2; + y += 3; + + graphics.setPrintPos(x, y); + + uint8_t id = edit_this_sequence_; + + if (edit_this_sequence_ == owner_->get_sequence()) + graphics.printf("#%d", id + 1); + else { + graphics.drawBitmap8(x, y, 4, OC::bitmap_indicator_4x8); + graphics.setPrintPos(x + 4, y); + graphics.print(id + 1); + } + + if (cursor_pos_ == num_slots) { + // print length + graphics.print(":"); + if (num_slots > 9) + graphics.print((int)num_slots, 2); + else + graphics.print((int)num_slots, 1); + } + else { + // print pitch value at current step ... + // 0 -> 0V, 1536 -> 1V, 3072 -> 2V + int pitch = (int)owner_->get_pitch_at_step(edit_this_sequence_, cursor_pos_); + int32_t octave = pitch / (12 << 7); + int32_t frac = pitch - octave * (12 << 7); + int32_t cents = (int)((float)(frac%128)/1.28); + int32_t pClass = (int)floor(frac/128); + octave += owner_->get_octave(); + if (pitch < 0) { + pClass = (pClass+12)%12; + if (frac < -127) { + octave--; + } + graphics.printf(": %s%d %dc (%d)", OC::Strings::note_names_unpadded[pClass], octave, cents, frac%128); //, (float)frac / 128.0f); + } else { + graphics.printf(": %s%d +%dc (%d)", OC::Strings::note_names_unpadded[pClass], octave, cents, frac%128); //, (float)frac / 128.0f); + } + } + + x += 3 + (w >> 0x1) - (num_slots << 0x2); y += 40; + #ifdef BUCHLA_4U + y += 16; + #endif + + uint8_t clock_pos= owner_->get_clock_cnt(); + bool _draw_clock = (owner_->get_current_sequence() == edit_this_sequence_) && owner_->draw_clock(); + uint16_t mask = mask_; + + for (size_t i = 0; i < num_slots; ++i, x += 7, mask >>= 1) { + + int pitch = (int)owner_->get_pitch_at_step(edit_this_sequence_, i); + + bool _clock = (i == clock_pos && _draw_clock); + + if (mask & 0x1 & (pitch >= 0)) { + pitch += 0x100; + if (_clock) + graphics.drawRect(x - 1, y - (pitch >> 8), 6, pitch >> 8); + else + graphics.drawRect(x, y - (pitch >> 8), 4, pitch >> 8); + } + else if (mask & 0x1) { + pitch -= 0x100; + if (_clock) + graphics.drawRect(x - 1, y, 6, abs(pitch) >> 8); + else + graphics.drawRect(x, y, 4, abs(pitch) >> 8); + } + else if (pitch > - 0x200 && pitch < 0x200) { + // disabled steps not visible otherwise.. + graphics.drawRect(x + 1, y - 1, 2, 2); + } + else if (pitch >= 0) { + pitch += 0x100; + graphics.drawFrame(x, y - (pitch >> 8), 4, pitch >> 8); + } + else { + pitch -= 0x100; + graphics.drawFrame(x, y, 4, abs(pitch) >> 8); + } + + if (i == cursor_pos_) { + if (OC::ui.read_immediate(OC::CONTROL_BUTTON_L)) + graphics.drawFrame(x - 3, y - 5, 10, 10); + else + graphics.drawFrame(x - 2, y - 4, 8, 8); + } + + if (_clock) + graphics.drawRect(x, y + 17, 4, 2); + + } + if (mutable_pattern_) { + graphics.drawFrame(x, y - 2, 4, 4); + if (cursor_pos_ == num_slots) + graphics.drawFrame(x - 2, y - 4, 8, 8); + } +} + +template +void PatternEditor::HandleButtonEvent(const UI::Event &event) { + + if (UI::EVENT_BUTTON_PRESS == event.type) { + switch (event.control) { + case OC::CONTROL_BUTTON_UP: + handleButtonUp(event); + break; + case OC::CONTROL_BUTTON_DOWN: + handleButtonDown(event); + break; + case OC::CONTROL_BUTTON_L: + handleButtonLeft(event); + break; + case OC::CONTROL_BUTTON_R: + Close(); + break; + default: + break; + } + } + else if (UI::EVENT_BUTTON_LONG_PRESS == event.type) { + switch (event.control) { + case OC::CONTROL_BUTTON_UP: + // screensaver // TODO: ideally, needs to be overridden ... invert_mask(); + break; + case OC::CONTROL_BUTTON_DOWN: + { + if (OC::ui.read_immediate(OC::CONTROL_BUTTON_L)) + clear_mask(); + else + paste_sequence(); + } + break; + case OC::CONTROL_BUTTON_L: + { + if (OC::ui.read_immediate(OC::CONTROL_BUTTON_DOWN)) + clear_mask(); + else + copy_sequence(); + } + break; + case OC::CONTROL_BUTTON_R: + // app menu + break; + default: + break; + } + } +} + +template +void PatternEditor::HandleEncoderEvent(const UI::Event &event) { + + uint16_t mask = mask_; + + if (OC::CONTROL_ENCODER_L == event.control) { + move_cursor(event.value); + } else if (OC::CONTROL_ENCODER_R == event.control) { + bool handled = false; + if (mutable_pattern_) { + if (cursor_pos_ >= num_slots_) { + + if (cursor_pos_ == num_slots_) { + int num_slots = num_slots_; + num_slots += event.value; + CONSTRAIN(num_slots, kMinPatternLength, kMaxPatternLength); + + num_slots_ = num_slots; + if (event.value > 0) { + // erase slots when expanding? + if (OC::ui.read_immediate(OC::CONTROL_BUTTON_L)) { + mask &= ~(~(0xffff << (num_slots_ - cursor_pos_)) << cursor_pos_); + owner_->set_pitch_at_step(edit_this_sequence_, num_slots_, 0x0); + } + } + // empty patterns are ok -- + owner_->set_sequence_length(num_slots_, edit_this_sequence_); + cursor_pos_ = num_slots_; + handled = true; + } + } + } + + if (!handled) { + + int32_t pitch = owner_->get_pitch_at_step(edit_this_sequence_, cursor_pos_); + // Q? might be better to actually add whatever is in the scale + // or semitone/finetune? + int16_t delta = event.value; + if (OC::ui.read_immediate(OC::CONTROL_BUTTON_L)) + pitch += delta; // fine + else + pitch += (delta << 7); // semitone + #ifdef BUCHLA_4U + CONSTRAIN(pitch, 0x0, 8 * (12 << 7) - 128); + #else + CONSTRAIN(pitch, -3 * (12 << 7), 5 * (12 << 7) - 128); + #endif + owner_->set_pitch_at_step(edit_this_sequence_, cursor_pos_, pitch); + + } + } + // This isn't entirely atomic + apply_mask(mask); +} + +template +void PatternEditor::move_cursor(int offset) { + + int cursor_pos = cursor_pos_ + offset; + const int max = mutable_pattern_ ? num_slots_ : num_slots_ - 1; + CONSTRAIN(cursor_pos, 0, max); + cursor_pos_ = cursor_pos; +} + +template +void PatternEditor::handleButtonUp(const UI::Event &event) { + + // next pattern / edit 'offline': + edit_this_sequence_++; + if (edit_this_sequence_ > OC::Patterns::PATTERN_USER_LAST-1) + edit_this_sequence_ = 0; + + cursor_pos_ = 0; + num_slots_ = owner_->get_sequence_length(edit_this_sequence_); + mask_ = owner_->get_mask(edit_this_sequence_); +} + +template +void PatternEditor::handleButtonDown(const UI::Event &event) { + + // next pattern / edit 'offline': + edit_this_sequence_--; + if (edit_this_sequence_ < 0) + edit_this_sequence_ = OC::Patterns::PATTERN_USER_LAST-1; + + cursor_pos_ = 0; + num_slots_ = owner_->get_sequence_length(edit_this_sequence_); + mask_ = owner_->get_mask(edit_this_sequence_); +} + +template +void PatternEditor::handleButtonLeft(const UI::Event &) { + uint16_t m = 0x1 << cursor_pos_; + uint16_t mask = mask_; + + if (cursor_pos_ < num_slots_) { + // toggle slot active state; allow 0 mask + if (mask & m) + mask &= ~m; + else + mask |= m; + apply_mask(mask); + } +} + +template +void PatternEditor::invert_mask() { + uint16_t m = ~(0xffffU << num_slots_); + uint16_t mask = mask_ ^ m; + apply_mask(mask); +} + +template +void PatternEditor::clear_mask() { + // clear the mask + apply_mask(0x00); + // and the user pattern: + owner_->clear_user_pattern(edit_this_sequence_); +} + +template +void PatternEditor::copy_sequence() { + owner_->copy_seq(edit_this_sequence_, num_slots_, mask_); +} + +template +void PatternEditor::paste_sequence() { + uint8_t newslots = owner_->paste_seq(edit_this_sequence_); + num_slots_ = newslots ? newslots : num_slots_; + mask_ = owner_->get_mask(edit_this_sequence_); +} + +template +/*static*/ uint16_t PatternEditor::RotateMask(uint16_t mask, int num_slots, int amount) { + uint16_t used_bits = ~(0xffffU << num_slots); + mask &= used_bits; + + if (amount < 0) { + amount = -amount % num_slots; + mask = (mask >> amount) | (mask << (num_slots - amount)); + } else { + amount = amount % num_slots; + mask = (mask << amount) | (mask >> (num_slots - amount)); + } + return mask | ~used_bits; // fill upper bits +} + +template +void PatternEditor::BeginEditing() { + + cursor_pos_ = 0; + uint8_t seq = owner_->get_sequence(); + edit_this_sequence_ = seq; + num_slots_ = owner_->get_sequence_length(seq); + mask_ = owner_->get_mask(seq); +} + +template +void PatternEditor::Close() { + ui.SetButtonIgnoreMask(); + owner_ = nullptr; +} + +}; // namespace OC + +#endif // OC_PATTERN_EDIT_H_ diff --git a/software/o_c_REV/OC_strings.cpp b/software/o_c_REV/OC_strings.cpp index e96f2c7b9..1bae5ee99 100644 --- a/software/o_c_REV/OC_strings.cpp +++ b/software/o_c_REV/OC_strings.cpp @@ -4,18 +4,47 @@ namespace OC { namespace Strings { + const char * const VERSION = +#include "OC_version.h" +#ifdef OC_VERSION_EXTRA + OC_VERSION_EXTRA +#endif + "-" +#ifdef OC_BUILD_TAG + OC_BUILD_TAG +#endif + ; +#ifdef BUCHLA_cOC + const char * const NAME = "NLM card O_C"; + const char * const SHORT_NAME = "cOC"; +#elif defined(VOR) + const char * const NAME = "Plum Audio O_C+"; + const char * const SHORT_NAME = "OC+"; +#else + const char * const NAME = "Ornaments & Crimes"; + const char * const SHORT_NAME = "o_C"; +#endif + const char * const seq_playmodes[] = {" -", "SEQ+1", "SEQ+2", "SEQ+3", "TR+1", "TR+2", "TR+3", "ARP", "S+H#1", "S+H#2", "S+H#3", "S+H#4", "CV#1", "CV#2", "CV#3", "CV#4"}; const char * const channel_trigger_sources[] = {"TR1", "TR2", "TR3", "TR4", "cnt+", "cnt-"}; const char * const seq_directions[] = {"fwd", "rev", "pnd1", "pnd2", "rnd", "brwn"}; + const char * const scale_degrees_maj[] = {"I", "II", "III", "IV", "V", "VI", "VII"}; + + const char* const scale_degrees_min[] = {"i", "ii", "iii", "iv", "v", "vi", "vii"}; + + const char* const accidentals[] = {"bb", "b", "", "#", "##"}; + const char * const scale_id[] = { ">#1", ">#2", ">#3", ">#4", " #1", " #2", " #3", " #4"}; const char * const note_names[12] = { "C ", "C#", "D ", "D#", "E ", "F ", "F#", "G ", "G#", "A ", "A#", "B " }; const char * const note_names_unpadded[12] = { "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" }; + const char * const VOR_offsets[3] = { "-5V", "-3V", "0V"}; + const char * const trigger_input_names[4] = { "TR1", "TR2", "TR3", "TR4" }; const char * const trigger_input_names_none[5] = { " - ", "TR1", "TR2", "TR3", "TR4" }; diff --git a/software/o_c_REV/OC_strings.h b/software/o_c_REV/OC_strings.h index d69a7d9dc..6a0c93d55 100644 --- a/software/o_c_REV/OC_strings.h +++ b/software/o_c_REV/OC_strings.h @@ -10,13 +10,22 @@ namespace OC { static const int kNumDelayTimes = 8; namespace Strings { + extern const char * const NAME; + extern const char * const SHORT_NAME; + extern const char * const VERSION; + extern const char * const seq_playmodes[]; extern const char * const channel_trigger_sources[]; extern const char * const seq_directions[]; + + extern const char * const scale_degrees_maj[]; + extern const char * const scale_degrees_min[]; + extern const char * const accidentals[]; extern const char * const scale_id[]; extern const char * const channel_id[]; extern const char * const note_names[]; extern const char * const note_names_unpadded[]; + extern const char * const VOR_offsets[]; extern const char * const trigger_input_names[]; extern const char * const trigger_input_names_none[]; extern const char * const cv_input_names[]; diff --git a/software/o_c_REV/OC_ui.cpp b/software/o_c_REV/OC_ui.cpp index 5f8abe36e..fe742d95a 100644 --- a/software/o_c_REV/OC_ui.cpp +++ b/software/o_c_REV/OC_ui.cpp @@ -1,6 +1,7 @@ #include #include +#include "OC_strings.h" #include "OC_apps.h" #include "OC_bitmaps.h" #include "OC_calibration.h" @@ -9,10 +10,14 @@ #include "OC_gpio.h" #include "OC_menus.h" #include "OC_ui.h" -#include "OC_version.h" #include "OC_options.h" #include "src/drivers/display.h" +#ifdef VOR +#include "VBiasManager.h" +VBiasManager *VBiasManager::instance = 0; +#endif + extern uint_fast8_t MENU_REDRAW; namespace OC { @@ -23,7 +28,12 @@ void Ui::Init() { ticks_ = 0; set_screensaver_timeout(SCREENSAVER_TIMEOUT_S); +#if defined(VOR) + static const int button_pins[] = { but_top, but_bot, butL, butR, but_mid }; +#else static const int button_pins[] = { but_top, but_bot, butL, butR }; +#endif + for (size_t i = 0; i < CONTROL_BUTTON_LAST; ++i) { buttons_[i].Init(button_pins[i], OC_GPIO_BUTTON_PINMODE); } @@ -78,11 +88,14 @@ void FASTRUN Ui::Poll() { auto &button = buttons_[i]; if (button.just_pressed()) { button_press_time_[i] = now; + PushEvent(UI::EVENT_BUTTON_DOWN, control_mask(i), 0, button_state); } else if (button.released()) { if (now - button_press_time_[i] < kLongPressTicks) PushEvent(UI::EVENT_BUTTON_PRESS, control_mask(i), 0, button_state); - else - PushEvent(UI::EVENT_BUTTON_LONG_PRESS, control_mask(i), 0, button_state); + button_press_time_[i] = 0; + } else if (button.pressed() && (now - button_press_time_[i] == kLongPressTicks)) { + button_state &= ~control_mask(i); + PushEvent(UI::EVENT_BUTTON_LONG_PRESS, control_mask(i), 0, button_state); } } @@ -112,10 +125,22 @@ UiMode Ui::DispatchEvents(App *app) { case UI::EVENT_BUTTON_PRESS: app->HandleButtonEvent(event); break; + case UI::EVENT_BUTTON_DOWN: +#ifdef VOR + // dual encoder press + if ( ((OC::CONTROL_BUTTON_L | OC::CONTROL_BUTTON_R) == event.mask) ) + { + VBiasManager *vbias_m = vbias_m->get(); + vbias_m->AdvanceBias(); + SetButtonIgnoreMask(); // ignore release and long-press + } + else +#endif + app->HandleButtonEvent(event); + break; case UI::EVENT_BUTTON_LONG_PRESS: if (OC::CONTROL_BUTTON_UP == event.control) { - if (!preempt_screensaver_) - screensaver_ = true; + if (!preempt_screensaver_) screensaver_ = true; } else if (OC::CONTROL_BUTTON_R == event.control) return UI_MODE_APP_SETTINGS; @@ -156,7 +181,7 @@ UiMode Ui::Splashscreen(bool &reset_settings) { mode = UI_MODE_APP_SETTINGS; reset_settings = - #ifdef BUCHLA_4U + #if defined(BUCHLA_4U) && !defined(IO_10V) read_immediate(CONTROL_BUTTON_UP) && read_immediate(CONTROL_BUTTON_R); #else read_immediate(CONTROL_BUTTON_UP) && read_immediate(CONTROL_BUTTON_DOWN); diff --git a/software/o_c_REV/OC_ui.h b/software/o_c_REV/OC_ui.h index f9b92a80d..8ef46bf94 100644 --- a/software/o_c_REV/OC_ui.h +++ b/software/o_c_REV/OC_ui.h @@ -2,6 +2,7 @@ #define OC_UI_H_ #include "OC_config.h" +#include "OC_options.h" #include "OC_debug.h" #include "UI/ui_button.h" #include "UI/ui_encoder.h" @@ -27,13 +28,22 @@ enum UiControl { CONTROL_BUTTON_L = 0x4, CONTROL_BUTTON_R = 0x8, #endif - CONTROL_BUTTON_MASK = 0xf, +#ifdef VOR + CONTROL_BUTTON_M = 0x10, + CONTROL_ENCODER_L = 0x20, + CONTROL_ENCODER_R = 0x40, + + CONTROL_LAST = 6, + CONTROL_BUTTON_LAST = 5, +#else + CONTROL_BUTTON_MASK = 0xf, CONTROL_ENCODER_L = 0x10, CONTROL_ENCODER_R = 0x20, CONTROL_LAST = 5, CONTROL_BUTTON_LAST = 4, +#endif }; static inline uint16_t control_mask(unsigned i) { @@ -50,7 +60,7 @@ enum UiMode { class Ui { public: static const size_t kEventQueueDepth = 16; - static const uint32_t kLongPressTicks = 1000; + static const uint32_t kLongPressTicks = 500; Ui() { } @@ -117,8 +127,8 @@ class Ui { uint32_t ticks_; uint32_t screensaver_timeout_; - UI::Button buttons_[4]; - uint32_t button_press_time_[4]; + UI::Button buttons_[CONTROL_BUTTON_LAST]; + uint32_t button_press_time_[CONTROL_BUTTON_LAST]; uint16_t button_state_; uint16_t button_ignore_mask_; bool screensaver_; @@ -154,6 +164,7 @@ class Ui { } if (screensaver_) { screensaver_ = false; + SetButtonIgnoreMask(); // ignore whatever button is about to be released ignore = true; } diff --git a/software/o_c_REV/OC_version.h b/software/o_c_REV/OC_version.h index 32c68c4e8..4b3c50335 100644 --- a/software/o_c_REV/OC_version.h +++ b/software/o_c_REV/OC_version.h @@ -1,4 +1,2 @@ -#ifndef OC_VERSION_H_ -#define OC_VERSION_H_ -#define OC_VERSION "v1.3.1" -#endif +// NOTE: DO NOT INCLUDE DIRECTLY, USE OC::Strings::VERSION +"v1.6.999" diff --git a/software/o_c_REV/UI/ui_encoder.h b/software/o_c_REV/UI/ui_encoder.h index 0e930aa8e..675ac665f 100644 --- a/software/o_c_REV/UI/ui_encoder.h +++ b/software/o_c_REV/UI/ui_encoder.h @@ -27,7 +27,11 @@ namespace UI { +#if defined(__IMXRT1062__) && defined(ARDUINO_TEENSY41) // Teensy 4.1 has dynamic pins +template +#else // default are macros (Teensy 3.2 or 4.0) template +#endif class Encoder { public: diff --git a/software/o_c_REV/UI/ui_events.h b/software/o_c_REV/UI/ui_events.h index 8b870c93c..1b6f73a24 100644 --- a/software/o_c_REV/UI/ui_events.h +++ b/software/o_c_REV/UI/ui_events.h @@ -27,6 +27,7 @@ namespace UI { enum EventType { EVENT_NONE, + EVENT_BUTTON_DOWN, EVENT_BUTTON_PRESS, EVENT_BUTTON_LONG_PRESS, EVENT_ENCODER diff --git a/software/o_c_REV/VBiasManager.h b/software/o_c_REV/VBiasManager.h new file mode 100644 index 000000000..860c59ab9 --- /dev/null +++ b/software/o_c_REV/VBiasManager.h @@ -0,0 +1,178 @@ +// Copyright (c) 2019, Jason Justian +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// This singleton class has a few jobs related to keeping track of the bias value +// selected by the user and/or the app: +// +// (1) It allows advancing bias through three settings, one at a time +// (2) It allows setting the bias directly with a state +// (3) It shows a popup indicator for one second when the setting is advanced + +#ifndef VBIAS_MANAGER_H +#define VBIAS_MANAGER_H + +#include "OC_options.h" + +#ifdef VOR + +#define BIAS_EDITOR_TIMEOUT 20000 + +namespace HS { +extern int octave_max; +} + +class VBiasManager { + static VBiasManager *instance; + int bias_state; + uint32_t last_advance_tick; + + VBiasManager() { + bias_state = 0; + last_advance_tick = 0; + } + +public: + enum VState { + BI = 0, + ASYM, + UNI, + }; + /* + static const int BI = 0; + static const int ASYM = 1; + static const int UNI = 2; + */ + const int OCTAVE_BIAS[3] = {5, 3, 0}; + const int OCTAVE_MAX[3] = {5, 7, 10}; + + static VBiasManager *get() { + if (!instance) instance = new VBiasManager; + return instance; + } + + /* + * Advance to the next state, when the button is pushed + */ + void AdvanceBias() { + // Only advance the bias if it's been less than a second since the last button press. + // This is so that the first button press shows the popup without changing anything. + if (OC::CORE::ticks - last_advance_tick < BIAS_EDITOR_TIMEOUT) { + if (++bias_state > 2) bias_state = 0; + instance->SetState(VBiasManager::VState(bias_state)); + } + last_advance_tick = OC::CORE::ticks; + } + + int IsEditing() { + return (OC::CORE::ticks - last_advance_tick < BIAS_EDITOR_TIMEOUT); + } + + /* + * Change to a specific state. This should replace a direct call to OC::DAC::set_Vbias(), because it + * allows VBiasManager to keep track of the current state so that the button advances the state as + * expected. For example: + * + * #ifdef VOR + * VBiasManager *vbias_m = vbias_m->get(); + * vbias_m->ChangeBiasToState(VBiasManager::BI); + * #endif + * + */ + void SetState(VState new_bias_state) { + int new_bias_value = OC::calibration_data.v_bias & 0xFFFF; // Bipolar = lower 2 bytes + if (new_bias_state == VBiasManager::UNI) new_bias_value = OC::DAC::VBiasUnipolar; + if (new_bias_state == VBiasManager::ASYM) new_bias_value = (OC::calibration_data.v_bias >> 16); // asym. = upper 2 bytes + OC::DAC::set_Vbias(new_bias_value); + bias_state = new_bias_state; + + OC::DAC::kOctaveZero = OCTAVE_BIAS[bias_state]; + HS::octave_max = OCTAVE_MAX[bias_state]; + } + int GetState() { + return bias_state; + } + + // Vbias auto-config helper + // Cross-reference OC_apps.ino for app IDs + void SetStateForApp(OC::App *app) { + VState new_state = VBiasManager::ASYM; // default case + + switch (app->id) + { + /* Default cases can be omitted + case TWOCC<'C','8'>::value: // Calibr8or + case TWOCC<'A','S'>::value: // CopierMachine (or) ASR + case TWOCC<'H','A'>::value: // Harrington 1200 (or) Triads + case TWOCC<'A','T'>::value: // Automatonnetz (or) Vectors + case TWOCC<'Q','Q'>::value: // Quantermain (or) 4x Quantizer + case TWOCC<'M','!'>::value: // Meta-Q (or) 2x Quantizer + case TWOCC<'S','Q'>::value: // Sequins (or) 2x Sequencer + case TWOCC<'A','C'>::value: // Acid Curds (or) Chords + new_state = VBiasManager::ASYM; + break; + */ + // Bi-polar +/-5V + case TWOCC<'H','S'>::value: // Hemisphere + case TWOCC<'L','R'>::value: // Low-rents (or) Lorenz + new_state = VBiasManager::BI; + break; + // Uni-polar 0-10V + case TWOCC<'E','G'>::value: // Piqued (or) 4x EG + case TWOCC<'B','B'>::value: // Dialectic Ping Pong (or) Balls + case TWOCC<'B','Y'>::value: // Viznutcracker sweet (or) Bytebeats + case TWOCC<'R','F'>::value: // References + new_state = VBiasManager::UNI; + break; + case TWOCC<'P','L'>::value: // Quadraturia (or) Quadrature LFO + return; // cancel, it has its own VBias setting + } + instance->SetState(new_state); + } + + /* + * If the last state advance (with the button) was less than a second ago, draw the popup indicator + */ + void DrawPopupPerhaps() { + if (OC::CORE::ticks - last_advance_tick < BIAS_EDITOR_TIMEOUT) { + graphics.clearRect(17, 7, 82, 43); + graphics.drawFrame(18, 8, 80, 42); + + graphics.setPrintPos(20, 10); + graphics.print("Range:"); + + // Bipolar state + graphics.setPrintPos(30, 20); + graphics.print("-5V -> 5V"); + // Asym State + graphics.setPrintPos(30, 30); + graphics.print("-3V -> 7V"); + // Unipolar state + graphics.setPrintPos(30, 40); + graphics.print(" 0V -> 10V"); + + graphics.setPrintPos(20, 20 + (bias_state * 10)); + graphics.print("> "); + } + } +}; + +#endif + +#endif // VBIAS_MANAGER_H diff --git a/software/o_c_REV/bjorklund.cpp b/software/o_c_REV/bjorklund.cpp index 4fd3d12f6..c96557699 100644 --- a/software/o_c_REV/bjorklund.cpp +++ b/software/o_c_REV/bjorklund.cpp @@ -57,14 +57,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 00 // 31 beats 00 // 32 beats 00 -0 , 1 , 3 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , +0 , 1 , 3 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , 0 , // 3 step patterns // 0 beats 000 // 1 beats 100 @@ -99,14 +99,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 000 // 31 beats 000 // 32 beats 000 -0 , 1 , 3 , 7 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , +0 , 1 , 3 , 7 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , 0 , // 4 step patterns // 0 beats 0000 // 1 beats 1000 @@ -141,14 +141,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 0000 // 31 beats 0000 // 32 beats 0000 -0 , 1 , 5 , 7 , -15 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , +0 , 1 , 5 , 7 , +15 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , 0 , // 5 step patterns // 0 beats 00000 // 1 beats 10000 @@ -183,14 +183,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 00000 // 31 beats 00000 // 32 beats 00000 -0 , 1 , 5 , 21 , -15 , 31 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , +0 , 1 , 5 , 21 , +15 , 31 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , 0 , // 6 step patterns // 0 beats 000000 // 1 beats 100000 @@ -225,14 +225,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 000000 // 31 beats 000000 // 32 beats 000000 -0 , 1 , 9 , 21 , -27 , 31 , 63 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , +0 , 1 , 9 , 21 , +27 , 31 , 63 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , 0 , // 7 step patterns // 0 beats 0000000 // 1 beats 1000000 @@ -267,14 +267,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 0000000 // 31 beats 0000000 // 32 beats 0000000 -0 , 1 , 9 , 21 , -85 , 91 , 63 , 127 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , +0 , 1 , 9 , 21 , +85 , 91 , 63 , 127 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , 0 , // 8 step patterns // 0 beats 00000000 // 1 beats 10000000 @@ -309,14 +309,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 00000000 // 31 beats 00000000 // 32 beats 00000000 -0 , 1 , 17 , 73 , -85 , 109 , 119 , 127 , -255 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , +0 , 1 , 17 , 73 , +85 , 109 , 119 , 127 , +255 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , 0 , // 9 step patterns // 0 beats 000000000 // 1 beats 100000000 @@ -351,14 +351,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 000000000 // 31 beats 000000000 // 32 beats 000000000 -0 , 1 , 17 , 73 , -85 , 341 , 219 , 375 , -255 , 511 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , +0 , 1 , 17 , 73 , +85 , 341 , 219 , 375 , +255 , 511 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , 0 , // 10 step patterns // 0 beats 0000000000 // 1 beats 1000000000 @@ -393,14 +393,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 0000000000 // 31 beats 0000000000 // 32 beats 0000000000 -0 , 1 , 33 , 73 , -165 , 341 , 693 , 731 , -495 , 511 , 1023 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , +0 , 1 , 33 , 73 , +165 , 341 , 693 , 731 , +495 , 511 , 1023 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , 0 , // 11 step patterns // 0 beats 00000000000 // 1 beats 10000000000 @@ -435,14 +435,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 00000000000 // 31 beats 00000000000 // 32 beats 00000000000 -0 , 1 , 33 , 273 , -585 , 341 , 1365 , 877 , -955 , 1519 , 1023 , 2047 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , +0 , 1 , 33 , 273 , +585 , 341 , 1365 , 877 , +955 , 1519 , 1023 , 2047 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , 0 , // 12 step patterns // 0 beats 000000000000 // 1 beats 100000000000 @@ -477,14 +477,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 000000000000 // 31 beats 000000000000 // 32 beats 000000000000 -0 , 1 , 65 , 273 , -585 , 1189 , 1365 , 1717 , -1755 , 1911 , 2015 , 2047 , -4095 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , +0 , 1 , 65 , 273 , +585 , 1189 , 1365 , 1717 , +1755 , 1911 , 2015 , 2047 , +4095 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , 0 , // 13 step patterns // 0 beats 0000000000000 // 1 beats 1000000000000 @@ -519,14 +519,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 0000000000000 // 31 beats 0000000000000 // 32 beats 0000000000000 -0 , 1 , 65 , 273 , -585 , 1321 , 1365 , 5461 , -5549 , 5851 , 6007 , 6111 , -4095 , 8191 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , +0 , 1 , 65 , 273 , +585 , 1321 , 1365 , 5461 , +5549 , 5851 , 6007 , 6111 , +4095 , 8191 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , 0 , // 14 step patterns // 0 beats 00000000000000 // 1 beats 10000000000000 @@ -561,14 +561,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 00000000000000 // 31 beats 00000000000000 // 32 beats 00000000000000 -0 , 1 , 129 , 1057 , -1161 , 4681 , 2709 , 5461 , -10965 , 7021 , 11739 , 7927 , -8127 , 8191 , 16383 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , +0 , 1 , 129 , 1057 , +1161 , 4681 , 2709 , 5461 , +10965 , 7021 , 11739 , 7927 , +8127 , 8191 , 16383 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , 0 , // 15 step patterns // 0 beats 000000000000000 // 1 beats 100000000000000 @@ -603,14 +603,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 000000000000000 // 31 beats 000000000000000 // 32 beats 000000000000000 -0 , 1 , 129 , 1057 , -4369 , 4681 , 5285 , 5461 , -21845 , 22197 , 14043 , 15291 , -15855 , 24511 , 16383 , 32767 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , +0 , 1 , 129 , 1057 , +4369 , 4681 , 5285 , 5461 , +21845 , 22197 , 14043 , 15291 , +15855 , 24511 , 16383 , 32767 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , 0 , // 16 step patterns // 0 beats 0000000000000000 // 1 beats 1000000000000000 @@ -645,14 +645,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 0000000000000000 // 31 beats 0000000000000000 // 32 beats 0000000000000000 -0 , 1 , 257 , 1057 , -4369 , 4681 , 18761 , 19093 , -21845 , 27349 , 28013 , 46811 , -30583 , 48623 , 32639 , 32767 , -65535 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , +0 , 1 , 257 , 1057 , +4369 , 4681 , 18761 , 19093 , +21845 , 27349 , 28013 , 46811 , +30583 , 48623 , 32639 , 32767 , +65535 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , 0 , // 17 step patterns // 0 beats 00000000000000000 // 1 beats 10000000000000000 @@ -687,14 +687,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 00000000000000000 // 31 beats 00000000000000000 // 32 beats 00000000000000000 -0 , 1 , 257 , 4161 , -4369 , 17545 , 37449 , 38053 , -21845 , 87381 , 54965 , 56173 , -60891 , 96119 , 64495 , 98175 , -65535 , 131071 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , +0 , 1 , 257 , 4161 , +4369 , 17545 , 37449 , 38053 , +21845 , 87381 , 54965 , 56173 , +60891 , 96119 , 64495 , 98175 , +65535 , 131071 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , 0 , // 18 step patterns // 0 beats 000000000000000000 // 1 beats 100000000000000000 @@ -729,14 +729,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 000000000000000000 // 31 beats 000000000000000000 // 32 beats 000000000000000000 -0 , 1 , 513 , 4161 , -8721 , 18577 , 37449 , 42281 , -43605 , 87381 , 174933 , 177581 , -112347 , 187835 , 192375 , 128991 , -130815 , 131071 , 262143 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , +0 , 1 , 513 , 4161 , +8721 , 18577 , 37449 , 42281 , +43605 , 87381 , 174933 , 177581 , +112347 , 187835 , 192375 , 128991 , +130815 , 131071 , 262143 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , 0 , // 19 step patterns // 0 beats 0000000000000000000 // 1 beats 1000000000000000000 @@ -771,14 +771,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 0000000000000000000 // 31 beats 0000000000000000000 // 32 beats 0000000000000000000 -0 , 1 , 513 , 4161 , -33825 , 69905 , 37449 , 84297 , -86693 , 87381 , 349525 , 350901 , -355693 , 374491 , 244667 , 253687 , -391135 , 392959 , 262143 , 524287 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , +0 , 1 , 513 , 4161 , +33825 , 69905 , 37449 , 84297 , +86693 , 87381 , 349525 , 350901 , +355693 , 374491 , 244667 , 253687 , +391135 , 392959 , 262143 , 524287 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , 0 , // 20 step patterns // 0 beats 00000000000000000000 // 1 beats 10000000000000000000 @@ -813,14 +813,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 00000000000000000000 // 31 beats 00000000000000000000 // 32 beats 00000000000000000000 -0 , 1 , 1025 , 16513 , -33825 , 69905 , 74825 , 299593 , -169125 , 305749 , 349525 , 437077 , -710325 , 449389 , 749275 , 489335 , -507375 , 520159 , 523775 , 524287 , -1048575 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , +0 , 1 , 1025 , 16513 , +33825 , 69905 , 74825 , 299593 , +169125 , 305749 , 349525 , 437077 , +710325 , 449389 , 749275 , 489335 , +507375 , 520159 , 523775 , 524287 , +1048575 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , 0 , // 21 step patterns // 0 beats 000000000000000000000 // 1 beats 100000000000000000000 @@ -855,14 +855,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 000000000000000000000 // 31 beats 000000000000000000000 // 32 beats 000000000000000000000 -0 , 1 , 1025 , 16513 , -33825 , 69905 , 148617 , 299593 , -600361 , 346773 , 349525 , 1398101 , -1403605 , 896429 , 898779 , 1502683 , -1537911 , 1555951 , 1040319 , 1572351 , -1048575 , 2097151 , 0 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , +0 , 1 , 1025 , 16513 , +33825 , 69905 , 148617 , 299593 , +600361 , 346773 , 349525 , 1398101 , +1403605 , 896429 , 898779 , 1502683 , +1537911 , 1555951 , 1040319 , 1572351 , +1048575 , 2097151 , 0 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , 0 , // 22 step patterns // 0 beats 0000000000000000000000 // 1 beats 1000000000000000000000 @@ -897,14 +897,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 0000000000000000000000 // 31 beats 0000000000000000000000 // 32 beats 0000000000000000000000 -0 , 1 , 2049 , 16513 , -67617 , 270865 , 559377 , 299593 , -1198665 , 1217701 , 698709 , 1398101 , -2796885 , 1758901 , 1796973 , 2995931 , -1956795 , 2027383 , 3112431 , 3137471 , -2096127 , 2097151 , 4194303 , 0 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , +0 , 1 , 2049 , 16513 , +67617 , 270865 , 559377 , 299593 , +1198665 , 1217701 , 698709 , 1398101 , +2796885 , 1758901 , 1796973 , 2995931 , +1956795 , 2027383 , 3112431 , 3137471 , +2096127 , 2097151 , 4194303 , 0 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , 0 , // 23 step patterns // 0 beats 00000000000000000000000 // 1 beats 10000000000000000000000 @@ -939,14 +939,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 00000000000000000000000 // 31 beats 00000000000000000000000 // 32 beats 00000000000000000000000 -0 , 1 , 2049 , 65793 , -266305 , 279073 , 1118481 , 1123401 , -2396745 , 1353001 , 2443925 , 1398101 , -5592405 , 3500757 , 5682605 , 3595117 , -3895003 , 3914683 , 6156023 , 4127727 , -4177855 , 6290431 , 4194303 , 8388607 , -0 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , +0 , 1 , 2049 , 65793 , +266305 , 279073 , 1118481 , 1123401 , +2396745 , 1353001 , 2443925 , 1398101 , +5592405 , 3500757 , 5682605 , 3595117 , +3895003 , 3914683 , 6156023 , 4127727 , +4177855 , 6290431 , 4194303 , 8388607 , +0 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , 0 , // 24 step patterns // 0 beats 000000000000000000000000 // 1 beats 100000000000000000000000 @@ -981,14 +981,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 000000000000000000000000 // 31 beats 000000000000000000000000 // 32 beats 000000000000000000000000 -0 , 1 , 4097 , 65793 , -266305 , 1082401 , 1118481 , 2245769 , -2396745 , 4802889 , 4871333 , 4893013 , -5592405 , 6991189 , 7034549 , 7171437 , -7190235 , 7794139 , 7829367 , 8118007 , -8255455 , 8355711 , 8386559 , 8388607 , -16777215 , 0 , 0 , 0 , -0 , 0 , 0 , 0 , +0 , 1 , 4097 , 65793 , +266305 , 1082401 , 1118481 , 2245769 , +2396745 , 4802889 , 4871333 , 4893013 , +5592405 , 6991189 , 7034549 , 7171437 , +7190235 , 7794139 , 7829367 , 8118007 , +8255455 , 8355711 , 8386559 , 8388607 , +16777215 , 0 , 0 , 0 , +0 , 0 , 0 , 0 , 0 , // 25 step patterns // 0 beats 0000000000000000000000000 // 1 beats 1000000000000000000000000 @@ -1023,14 +1023,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 0000000000000000000000000 // 31 beats 0000000000000000000000000 // 32 beats 0000000000000000000000000 -0 , 1 , 4097 , 65793 , -266305 , 1082401 , 1118481 , 2377873 , -2396745 , 5392969 , 5412005 , 5581461 , -5592405 , 22369621 , 22391509 , 22730421 , -22768493 , 23967451 , 24042939 , 24606583 , -16236015 , 25032671 , 25132927 , 25163775 , -16777215 , 33554431 , 0 , 0 , -0 , 0 , 0 , 0 , +0 , 1 , 4097 , 65793 , +266305 , 1082401 , 1118481 , 2377873 , +2396745 , 5392969 , 5412005 , 5581461 , +5592405 , 22369621 , 22391509 , 22730421 , +22768493 , 23967451 , 24042939 , 24606583 , +16236015 , 25032671 , 25132927 , 25163775 , +16777215 , 33554431 , 0 , 0 , +0 , 0 , 0 , 0 , 0 , // 26 step patterns // 0 beats 00000000000000000000000000 // 1 beats 10000000000000000000000000 @@ -1065,14 +1065,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 00000000000000000000000000 // 31 beats 00000000000000000000000000 // 32 beats 00000000000000000000000000 -0 , 1 , 8193 , 262657 , -532545 , 1082401 , 2236689 , 4753681 , -4792905 , 19173961 , 10822953 , 11096741 , -11183445 , 22369621 , 44741973 , 44915381 , -45462957 , 28760941 , 47937243 , 48094139 , -49215351 , 49790447 , 50067423 , 33488767 , -33550335 , 33554431 , 67108863 , 0 , -0 , 0 , 0 , 0 , +0 , 1 , 8193 , 262657 , +532545 , 1082401 , 2236689 , 4753681 , +4792905 , 19173961 , 10822953 , 11096741 , +11183445 , 22369621 , 44741973 , 44915381 , +45462957 , 28760941 , 47937243 , 48094139 , +49215351 , 49790447 , 50067423 , 33488767 , +33550335 , 33554431 , 67108863 , 0 , +0 , 0 , 0 , 0 , 0 , // 27 step patterns // 0 beats 000000000000000000000000000 // 1 beats 100000000000000000000000000 @@ -1107,14 +1107,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 000000000000000000000000000 // 31 beats 000000000000000000000000000 // 32 beats 000000000000000000000000000 -0 , 1 , 8193 , 262657 , -2113665 , 4261921 , 4465169 , 17895697 , -9577609 , 19173961 , 21580105 , 38966437 , -22325845 , 22369621 , 89478485 , 89566037 , -56284853 , 91057517 , 57521883 , 95907291 , -62634939 , 98496375 , 66026991 , 66580447 , -66977535 , 100659199 , 67108863 , 134217727 , -0 , 0 , 0 , 0 , +0 , 1 , 8193 , 262657 , +2113665 , 4261921 , 4465169 , 17895697 , +9577609 , 19173961 , 21580105 , 38966437 , +22325845 , 22369621 , 89478485 , 89566037 , +56284853 , 91057517 , 57521883 , 95907291 , +62634939 , 98496375 , 66026991 , 66580447 , +66977535 , 100659199 , 67108863 , 134217727 , +0 , 0 , 0 , 0 , 0 , // 28 step patterns // 0 beats 0000000000000000000000000000 // 1 beats 1000000000000000000000000000 @@ -1149,14 +1149,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 0000000000000000000000000000 // 31 beats 0000000000000000000000000000 // 32 beats 0000000000000000000000000000 -0 , 1 , 16385 , 262657 , -2113665 , 4327489 , 17318945 , 17895697 , -19022985 , 19173961 , 76698185 , 43296041 , -44386965 , 78292309 , 89478485 , 111850837 , -179661525 , 181843373 , 115039085 , 191739611 , -192343515 , 125269879 , 129883895 , 199195631 , -133160895 , 201195263 , 134209535 , 134217727 , -268435455 , 0 , 0 , 0 , +0 , 1 , 16385 , 262657 , +2113665 , 4327489 , 17318945 , 17895697 , +19022985 , 19173961 , 76698185 , 43296041 , +44386965 , 78292309 , 89478485 , 111850837 , +179661525 , 181843373 , 115039085 , 191739611 , +192343515 , 125269879 , 129883895 , 199195631 , +133160895 , 201195263 , 134209535 , 134217727 , +268435455 , 0 , 0 , 0 , 0 , // 29 step patterns // 0 beats 00000000000000000000000000000 // 1 beats 10000000000000000000000000000 @@ -1191,14 +1191,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 00000000000000000000000000000 // 31 beats 00000000000000000000000000000 // 32 beats 00000000000000000000000000000 -0 , 1 , 16385 , 1049601 , -2113665 , 17043521 , 34636833 , 17895697 , -71600273 , 71901769 , 153391689 , 153692457 , -88757413 , 156543573 , 89478485 , 357913941 , -223783765 , 359356085 , 229485997 , 230087533 , -249263835 , 250469819 , 393705335 , 259776247 , -264174575 , 401596351 , 268173055 , 402644991 , -268435455 , 536870911 , 0 , 0 , +0 , 1 , 16385 , 1049601 , +2113665 , 17043521 , 34636833 , 17895697 , +71600273 , 71901769 , 153391689 , 153692457 , +88757413 , 156543573 , 89478485 , 357913941 , +223783765 , 359356085 , 229485997 , 230087533 , +249263835 , 250469819 , 393705335 , 259776247 , +264174575 , 401596351 , 268173055 , 402644991 , +268435455 , 536870911 , 0 , 0 , 0 , // 30 step patterns // 0 beats 000000000000000000000000000000 // 1 beats 100000000000000000000000000000 @@ -1233,14 +1233,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 111111111111111111111111111111 // 31 beats 000000000000000000000000000000 // 32 beats 000000000000000000000000000000 -0 , 1 , 32769 , 1049601 , -4227201 , 17043521 , 34636833 , 69345553 , -143167761 , 76620873 , 153391689 , 306858313 , -173184165 , 312822421 , 178951509 , 357913941 , -715838805 , 448096981 , 727373493 , 460025197 , -460175067 , 767258331 , 501070779 , 518977399 , -519552495 , 528349151 , 803200959 , 536346111 , -536854527 , 536870911 , 1073741823 , 0 , +0 , 1 , 32769 , 1049601 , +4227201 , 17043521 , 34636833 , 69345553 , +143167761 , 76620873 , 153391689 , 306858313 , +173184165 , 312822421 , 178951509 , 357913941 , +715838805 , 448096981 , 727373493 , 460025197 , +460175067 , 767258331 , 501070779 , 518977399 , +519552495 , 528349151 , 803200959 , 536346111 , +536854527 , 536870911 , 1073741823 , 0 , 0 , // 31 step patterns // 0 beats 0000000000000000000000000000000 // 1 beats 1000000000000000000000000000000 @@ -1275,14 +1275,14 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 1111111111111111111111111111110 // 31 beats 1111111111111111111111111111111 // 32 beats 0000000000000000000000000000000 -0 , 1 , 32769 , 1049601 , -16843009 , 17043521 , 34636833 , 138682897 , -286331153 , 287458441 , 153391689 , 345133641 , -614802729 , 623530661 , 357739093 , 357913941 , -1431655765 , 1432005461 , 900422325 , 917878189 , -1457216365 , 1533916891 , 997649883 , 1002159035 , -1038020471 , 1593294319 , 1602090975 , 1069531071 , -1610087935 , 1610596351 , 1073741823 , 2147483647 , +0 , 1 , 32769 , 1049601 , +16843009 , 17043521 , 34636833 , 138682897 , +286331153 , 287458441 , 153391689 , 345133641 , +614802729 , 623530661 , 357739093 , 357913941 , +1431655765 , 1432005461 , 900422325 , 917878189 , +1457216365 , 1533916891 , 997649883 , 1002159035 , +1038020471 , 1593294319 , 1602090975 , 1069531071 , +1610087935 , 1610596351 , 1073741823 , 2147483647 , 0 , // 32 step patterns // 0 beats 00000000000000000000000000000000 // 1 beats 10000000000000000000000000000000 @@ -1317,42 +1317,31 @@ const uint32_t bjorklund_patterns[] = { // 30 beats 11111111111111101111111111111110 // 31 beats 11111111111111111111111111111110 // 32 beats 11111111111111111111111111111111 -0 , 1 , 65537 , 4196353 , -16843009 , 67641409 , 69272609 , 142885409 , -286331153 , 304367761 , 306778697 , 1227133513 , -1229539657 , 1246925989 , 1251297941 , 1252693333 , -1431655765 , 1789580629 , 1792371413 , 1801115317 , -1835887981 , 1840700269 , 3067852507 , 3077496251 , -2004318071 , 3151884023 , 3186605551 , 2130442207 , -2139062143 , 2146434559 , 2147450879 , 2147483647 , +0 , 1 , 65537 , 4196353 , +16843009 , 67641409 , 69272609 , 142885409 , +286331153 , 304367761 , 306778697 , 1227133513 , +1229539657 , 1246925989 , 1251297941 , 1252693333 , +1431655765 , 1789580629 , 1792371413 , 1801115317 , +1835887981 , 1840700269 , 3067852507 , 3077496251 , +2004318071 , 3151884023 , 3186605551 , 2130442207 , +2139062143 , 2146434559 , 2147450879 , 2147483647 , 4294967295 , }; bool EuclideanFilter(uint8_t num_steps, uint8_t num_beats, uint8_t rotation, uint32_t clock) { - if (num_beats > (num_steps + 1)) { - num_beats = num_steps + 1; - } - uint32_t pattern = bjorklund_patterns[((num_steps - 1) * 33) + num_beats]; - if (rotation) { - // Serial.print(pattern); - // Serial.print("\n"); - rotation = rotation % (num_steps + 1); - pattern = rotl32(pattern, num_steps, rotation) ; - // Serial.print(pattern); - // Serial.print("\n------\n"); - } - uint8_t position = clock % (num_steps + 1) ; - return static_cast(pattern & (0x01 << position)) ; + uint32_t pattern = EuclideanPattern(num_steps, num_beats, rotation); + clock %= num_steps; + return static_cast(pattern & (0x01 << clock)) ; } -uint32_t EuclideanPattern(uint8_t num_steps, uint8_t num_beats, uint8_t rotation) { - if (num_beats > (num_steps + 1)) { - num_beats = num_steps + 1; - } - uint32_t pattern = bjorklund_patterns[((num_steps - 1) * 33) + num_beats]; +uint32_t EuclideanPattern(uint8_t num_steps, uint8_t num_beats, uint8_t rotation, uint8_t padding) { + if (num_steps < 2) num_steps = 2; + if (num_beats > num_steps) num_beats = num_steps; + + uint32_t pattern = bjorklund_patterns[((num_steps - 2) * 33) + num_beats]; if (rotation) { - rotation = rotation % (num_steps + 1); - pattern = rotl32(pattern, num_steps, rotation) ; + rotation = rotation % (num_steps + padding); + pattern = rotl32(pattern, num_steps + padding, rotation) ; } return pattern; } diff --git a/software/o_c_REV/bjorklund.h b/software/o_c_REV/bjorklund.h index 966ceb065..91d69a7fb 100644 --- a/software/o_c_REV/bjorklund.h +++ b/software/o_c_REV/bjorklund.h @@ -45,10 +45,10 @@ inline uint32_t rotl32(uint32_t input, unsigned int length, unsigned int count) __attribute__((always_inline)); inline uint32_t rotl32(uint32_t input, unsigned int length, unsigned int count) { input &= ~(0xffffffff << length); - return (input << count) | (input >> (length - count + 1)); // off-by-ones or parenthesis mismatch likely + return (input << count) | (input >> (length - count)); // off-by-ones or parenthesis mismatch likely } bool EuclideanFilter(uint8_t num_steps, uint8_t num_beats, uint8_t rotation, uint32_t clock); -uint32_t EuclideanPattern(uint8_t num_steps, uint8_t num_beats, uint8_t rotation); +uint32_t EuclideanPattern(uint8_t num_steps, uint8_t num_beats, uint8_t rotation, uint8_t padding = 0); #endif // BJORKLUND_H_ diff --git a/software/o_c_REV/braids_quantizer.cpp b/software/o_c_REV/braids_quantizer.cpp index b417ca40b..ee4a04f89 100644 --- a/software/o_c_REV/braids_quantizer.cpp +++ b/software/o_c_REV/braids_quantizer.cpp @@ -89,28 +89,33 @@ int32_t Quantizer::Process(int32_t pitch, int32_t root, int32_t transpose) { q = num_notes_ - 1; } - q += transpose; - octave += q / num_notes_; - q %= num_notes_; - if (q < 0) { - q += num_notes_; - octave--; - } - - note_number_ = octave * num_notes_ + q; + // set boundaries for hysteresis codeword_ = notes_[q] + octave * span_; previous_boundary_ = q == 0 ? notes_[num_notes_ - 1] + (octave - 1) * span_ : notes_[q - 1] + octave * span_; - previous_boundary_ = (NEIGHBOR_WEIGHT * previous_boundary_ + CUR_WEIGHT * codeword_) >> 4; + next_boundary_ = q == num_notes_ - 1 ? notes_[0] + (octave + 1) * span_ : notes_[q + 1] + octave * span_; next_boundary_ = (NEIGHBOR_WEIGHT * next_boundary_ + CUR_WEIGHT * codeword_) >> 4; + // apply transpose after setting boundaries + q += transpose; + octave += q / num_notes_; + q %= num_notes_; + if (q < 0) { + q += num_notes_; + octave--; + } + + // set final values + note_number_ = (octave + 2) * num_notes_ + q + 64; // 64 is C2 + codeword_ = notes_[q] + octave * span_; + transpose_ = transpose; pitch = codeword_; } @@ -125,8 +130,12 @@ int32_t Quantizer::Process(int32_t pitch, int32_t root, int32_t transpose) { int32_t Quantizer::Lookup(int32_t index) const { index -= 64; - int16_t octave = index / num_notes_ - (index < 0 ? 1 : 0); - int16_t rel_ix = index - octave * num_notes_; + int16_t octave = index / num_notes_; + int16_t rel_ix = index % num_notes_; + if (rel_ix < 0) { + octave--; + rel_ix += num_notes_; + } int32_t pitch = notes_[rel_ix] + octave * span_; return pitch; } diff --git a/software/o_c_REV/braids_quantizer.h b/software/o_c_REV/braids_quantizer.h index 1d810841e..1acfe6650 100644 --- a/software/o_c_REV/braids_quantizer.h +++ b/software/o_c_REV/braids_quantizer.h @@ -94,6 +94,6 @@ class Quantizer { DISALLOW_COPY_AND_ASSIGN(Quantizer); }; -} // namespace stages +} // namespace braids -#endif \ No newline at end of file +#endif diff --git a/software/o_c_REV/braids_quantizer_scales.h b/software/o_c_REV/braids_quantizer_scales.h index 9f24aba8e..385d0e390 100644 --- a/software/o_c_REV/braids_quantizer_scales.h +++ b/software/o_c_REV/braids_quantizer_scales.h @@ -301,7 +301,52 @@ const Scale scales[] = { // // 15-5-HD3 (10 step subharmonic series scale on the tritave) - see Xen-Arts VSTi microtuning library at http://www.xen-arts.net/Xen-Arts%20VSTi%20Microtuning%20Library.zip { 12 << 7, 10, { 0, 96, 200, 312, 434, 567, 714, 879, 1066, 1281} }, // // 12-4-HD3 (8 step subharmonic series scale on the tritave) - see Xen-Arts VSTi microtuning library at http://www.xen-arts.net/Xen-Arts%20VSTi%20Microtuning%20Library.zip -{ 12 << 7, 8, { 0, 122, 255, 402, 567, 754, 969, 1224} } +{ 12 << 7, 8, { 0, 122, 255, 402, 567, 754, 969, 1224} }, + + // Logarhythm Mod: Adding some musically useful equal temperment scales from the Disting mk.4 set that are otherwise not present here + // Root +5th + 7th (5+7), + { 12 << 7, 3, { 0, 896, 1408} }, + + // Root + 5th + 6th (5+6), + { 12 << 7, 3, { 0, 896, 1152} }, + + // Minor Triad + 7 (3b+5+7), + { 12 << 7, 4, { 0, 384, 896, 1280} }, + + // Major Triad + 7 (Triad+7), + { 12 << 7, 4, { 0, 512, 896, 1408} }, + + // Minor Triad + 6th (3b+5+6), + { 12 << 7, 4, { 0, 384, 896, 1152} }, + + // Major Triad + 6th (Triad+6), + { 12 << 7, 4, { 0, 512, 896, 1152} }, + + // Fifth, + { 12 << 7, 2, { 0, 896} }, + + // major triad (Triad), + { 12 << 7, 3, { 0, 512, 896} }, + + // minor triad (3b+5), + { 12 << 7, 3, { 0, 384, 896} }, + + // Harmonic Minor (Harm Minor), + { 12 << 7, 7, { 0, 256, 384, 640, 896, 1024, 1408} }, + + // Locrian n6 + { 12 << 7, 7, { 0, 128, 384, 640, 768, 1152, 1280} }, + // Ionian augmented + { 12 << 7, 7, { 0, 256, 512, 640, 1024, 1152, 1408} }, + // Misheberakh + { 12 << 7, 7, { 0, 256, 384, 768, 896, 1152, 1280} }, + // Freygish + { 12 << 7, 7, { 0, 128, 512, 640, 896, 1024, 1280} }, + // Lydian #9 + { 12 << 7, 7, { 0, 384, 512, 768, 896, 1152, 1408} }, + // Ultralocrian + { 12 << 7, 7, { 0, 128, 384, 512, 768, 1024, 1152} }, + } ; }// namespace braids diff --git a/software/o_c_REV/extern/avr/eeprom.h b/software/o_c_REV/extern/avr/eeprom.h new file mode 100644 index 000000000..6e087c27d --- /dev/null +++ b/software/o_c_REV/extern/avr/eeprom.h @@ -0,0 +1,53 @@ +/* Simple compatibility headers for AVR code used with ARM chips + * Copyright (c) 2015 Paul Stoffregen + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +// Guidelines for editing this file: +// https://forum.pjrc.com/threads/34537-Teensy-LC-Increase-EEPROM-Size/page2 + +// modified version for Phazerville Suite +// This is a combo of the T3 and T4 versions of this file, with the 4.0 value bumped up to 3071 + +#ifndef _AVR_EEPROM_H_ +#define _AVR_EEPROM_H_ 1 + +#include +#include + +#include "avr_functions.h" + +#if defined(ARDUINO_TEENSY40) + #define E2END 0xBFF // 0x437 +#elif defined(ARDUINO_TEENSY41) + #define E2END 0x10BB +#elif defined(ARDUINO_TEENSY_MICROMOD) + #define E2END 0x10BB +#elif defined(__MK20DX128__) || defined(__MK20DX256__) + #define E2END 0x7FF +#elif defined(__MK64FX512__) || defined(__MK66FX1M0__) + #define E2END 0xFFF +#elif defined(__MKL26Z64__) + #define E2END 0x7F +#else + #define E2END 0 +#endif + +#endif diff --git a/software/o_c_REV/extern/dspinst.h b/software/o_c_REV/extern/dspinst.h index c5cd6355b..4c302fa53 100644 --- a/software/o_c_REV/extern/dspinst.h +++ b/software/o_c_REV/extern/dspinst.h @@ -33,7 +33,7 @@ static inline int32_t signed_saturate_rshift(int32_t val, int bits, int rshift) __attribute__((always_inline, unused)); static inline int32_t signed_saturate_rshift(int32_t val, int bits, int rshift) { -#if defined(KINETISK) +#if defined(__ARM_ARCH_7EM__) int32_t out; asm volatile("ssat %0, %1, %2, asr %3" : "=r" (out) : "I" (bits), "r" (val), "I" (rshift)); return out; @@ -54,7 +54,7 @@ static inline int32_t signed_saturate_rshift(int32_t val, int bits, int rshift) static inline int16_t saturate16(int32_t val) __attribute__((always_inline, unused)); static inline int16_t saturate16(int32_t val) { -#if defined(KINETISK) +#if defined(__ARM_ARCH_7EM__) int16_t out; int32_t tmp; asm volatile("ssat %0, %1, %2" : "=r" (tmp) : "I" (16), "r" (val) ); @@ -69,7 +69,7 @@ static inline int16_t saturate16(int32_t val) static inline int32_t signed_multiply_32x16b(int32_t a, uint32_t b) __attribute__((always_inline, unused)); static inline int32_t signed_multiply_32x16b(int32_t a, uint32_t b) { -#if defined(KINETISK) +#if defined(__ARM_ARCH_7EM__) int32_t out; asm volatile("smulwb %0, %1, %2" : "=r" (out) : "r" (a), "r" (b)); return out; @@ -82,7 +82,7 @@ static inline int32_t signed_multiply_32x16b(int32_t a, uint32_t b) static inline int32_t signed_multiply_32x16t(int32_t a, uint32_t b) __attribute__((always_inline, unused)); static inline int32_t signed_multiply_32x16t(int32_t a, uint32_t b) { -#if defined(KINETISK) +#if defined(__ARM_ARCH_7EM__) int32_t out; asm volatile("smulwt %0, %1, %2" : "=r" (out) : "r" (a), "r" (b)); return out; @@ -95,7 +95,7 @@ static inline int32_t signed_multiply_32x16t(int32_t a, uint32_t b) static inline int32_t multiply_32x32_rshift32(int32_t a, int32_t b) __attribute__((always_inline, unused)); static inline int32_t multiply_32x32_rshift32(int32_t a, int32_t b) { -#if defined(KINETISK) +#if defined(__ARM_ARCH_7EM__) int32_t out; asm volatile("smmul %0, %1, %2" : "=r" (out) : "r" (a), "r" (b)); return out; @@ -108,7 +108,7 @@ static inline int32_t multiply_32x32_rshift32(int32_t a, int32_t b) static inline uint32_t multiply_u32xu32_rshift32(uint32_t a, uint32_t b) __attribute__((always_inline)); static inline uint32_t multiply_u32xu32_rshift32(uint32_t a, uint32_t b) { -#if defined(KINETISK) +#if defined(__ARM_ARCH_7EM__) uint32_t out, tmp; asm volatile("umull %0, %1, %2, %3" : "=r" (tmp), "=r" (out) : "r" (a), "r" (b)); return out; @@ -121,7 +121,7 @@ static inline uint32_t multiply_u32xu32_rshift32(uint32_t a, uint32_t b) static inline int32_t multiply_32x32_rshift32_rounded(int32_t a, int32_t b) __attribute__((always_inline, unused)); static inline int32_t multiply_32x32_rshift32_rounded(int32_t a, int32_t b) { -#if defined(KINETISK) +#if defined(__ARM_ARCH_7EM__) int32_t out; asm volatile("smmulr %0, %1, %2" : "=r" (out) : "r" (a), "r" (b)); return out; @@ -134,7 +134,7 @@ static inline int32_t multiply_32x32_rshift32_rounded(int32_t a, int32_t b) static inline int32_t multiply_accumulate_32x32_rshift32_rounded(int32_t sum, int32_t a, int32_t b) __attribute__((always_inline, unused)); static inline int32_t multiply_accumulate_32x32_rshift32_rounded(int32_t sum, int32_t a, int32_t b) { -#if defined(KINETISK) +#if defined(__ARM_ARCH_7EM__) int32_t out; asm volatile("smmlar %0, %2, %3, %1" : "=r" (out) : "r" (sum), "r" (a), "r" (b)); return out; @@ -147,7 +147,7 @@ static inline int32_t multiply_accumulate_32x32_rshift32_rounded(int32_t sum, in static inline int32_t multiply_subtract_32x32_rshift32_rounded(int32_t sum, int32_t a, int32_t b) __attribute__((always_inline, unused)); static inline int32_t multiply_subtract_32x32_rshift32_rounded(int32_t sum, int32_t a, int32_t b) { -#if defined(KINETISK) +#if defined(__ARM_ARCH_7EM__) int32_t out; asm volatile("smmlsr %0, %2, %3, %1" : "=r" (out) : "r" (sum), "r" (a), "r" (b)); return out; @@ -161,7 +161,7 @@ static inline int32_t multiply_subtract_32x32_rshift32_rounded(int32_t sum, int3 static inline uint32_t pack_16t_16t(int32_t a, int32_t b) __attribute__((always_inline, unused)); static inline uint32_t pack_16t_16t(int32_t a, int32_t b) { -#if defined(KINETISK) +#if defined(__ARM_ARCH_7EM__) int32_t out; asm volatile("pkhtb %0, %1, %2, asr #16" : "=r" (out) : "r" (a), "r" (b)); return out; @@ -174,7 +174,7 @@ static inline uint32_t pack_16t_16t(int32_t a, int32_t b) static inline uint32_t pack_16t_16b(int32_t a, int32_t b) __attribute__((always_inline, unused)); static inline uint32_t pack_16t_16b(int32_t a, int32_t b) { -#if defined(KINETISK) +#if defined(__ARM_ARCH_7EM__) int32_t out; asm volatile("pkhtb %0, %1, %2" : "=r" (out) : "r" (a), "r" (b)); return out; @@ -187,7 +187,7 @@ static inline uint32_t pack_16t_16b(int32_t a, int32_t b) static inline uint32_t pack_16b_16b(int32_t a, int32_t b) __attribute__((always_inline, unused)); static inline uint32_t pack_16b_16b(int32_t a, int32_t b) { -#if defined(KINETISK) +#if defined(__ARM_ARCH_7EM__) int32_t out; asm volatile("pkhbt %0, %1, %2, lsl #16" : "=r" (out) : "r" (b), "r" (a)); return out; diff --git a/software/o_c_REV/frames_poly_lfo.cpp b/software/o_c_REV/frames_poly_lfo.cpp new file mode 100644 index 000000000..aee41e0a4 --- /dev/null +++ b/software/o_c_REV/frames_poly_lfo.cpp @@ -0,0 +1,257 @@ +// Copyright 2013 Olivier Gillet, 2015, 2016 Patrick Dowling and Tim Churches +// +// Original author: Olivier Gillet (ol.gillet@gmail.com) +// Adapted and modified for use in the Ornament + Crime module by: +// Patrick Dowling (pld@gurkenkiste.com) and Tim Churches (tim.churches@gmail.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +// See http://creativecommons.org/licenses/MIT/ for more information. +// +// ----------------------------------------------------------------------------- +// +// Poly LFO. + +#include "frames_poly_lfo.h" + +#include +#include + +#include "extern/stmlib_utils_dsp.h" +#include "util/util_math.h" +#include "frames_resources.h" + +namespace frames { + +using namespace std; +using namespace stmlib; + +void PolyLfo::Init() { + freq_range_ = 9; + spread_ = 0; + shape_ = 0; + shape_spread_ = 0; + coupling_ = 0; + attenuation_ = 58880; + offset_ = 0 ; + freq_div_b_ = freq_div_c_ = freq_div_d_ = POLYLFO_FREQ_MULT_NONE ; + b_am_by_a_ = 0 ; + c_am_by_b_ = 0 ; + d_am_by_c_ = 0 ; + phase_reset_flag_ = false; + sync_counter_ = 0 ; + sync_ = false; + period_ = 0 ; + std::fill(&value_[0], &value_[kNumChannels], 0); + std::fill(&wt_value_[0], &wt_value_[kNumChannels], 0); + std::fill(&phase_[0], &phase_[kNumChannels], 0); + last_phase_difference_ = 0; + last_phase_difference_ = 0; + pattern_predictor_.Init(); +} + +/* static */ +uint32_t PolyLfo::FrequencyToPhaseIncrement(int32_t frequency, uint16_t frq_rng) { + int32_t shifts = frequency / 5040; + int32_t index = frequency - shifts * 5040; + uint32_t a; + uint32_t b; + switch(frq_rng){ + // "cosm", "geol", "glacl", "snail", "sloth", "vlazy", "lazy", "vslow", "slow", "med", "fast", "vfast", + + case 0: // cosmological + a = lut_increments_med[index >> 5] >> 11; + b = lut_increments_med[(index >> 5) + 1] >> 11; + break; + case 1: // geological + a = lut_increments_med[index >> 5] >> 9; + b = lut_increments_med[(index >> 5) + 1] >> 9; + break; + case 2: // glacial + a = lut_increments_med[index >> 5] >> 7; + b = lut_increments_med[(index >> 5) + 1] >> 7; + break; + case 3: // snail + a = lut_increments_med[index >> 5] >> 6; + b = lut_increments_med[(index >> 5) + 1] >> 6; + break; + case 4: // sloth + a = lut_increments_med[index >> 5] >> 5; + b = lut_increments_med[(index >> 5) + 1] >> 5; + break; + case 5: // vlazy + a = lut_increments_med[index >> 5] >> 4; + b = lut_increments_med[(index >> 5) + 1] >> 4; + break; + case 6: // lazy + a = lut_increments_med[index >> 5] >> 3; + b = lut_increments_med[(index >> 5) + 1] >> 3; + break; + case 7: // vslow + a = lut_increments_med[index >> 5] >> 2; + b = lut_increments_med[(index >> 5) + 1] >> 2; + break; + case 8: //slow + a = lut_increments_med[index >> 5] >> 1 ; + b = lut_increments_med[(index >> 5) + 1] >> 1 ; + break; + case 9: // medium + a = lut_increments_med[index >> 5] ; + b = lut_increments_med[(index >> 5) + 1]; + break; + case 10: // fast + a = lut_increments_med[index >> 5] << 1; + b = lut_increments_med[(index >> 5) + 1] << 1; + break; + case 11: // vfast + a = lut_increments_med[index >> 5] << 2; + b = lut_increments_med[(index >> 5) + 1] << 2; + break; + default: + a = lut_increments_med[index >> 5]; + b = lut_increments_med[(index >> 5) + 1]; + break; + } + return (a + ((b - a) * (index & 0x1f) >> 5)) << shifts; +} + +void PolyLfo::Render(int32_t frequency, bool reset_phase, bool tempo_sync, uint8_t freq_mult) { + ++sync_counter_; + if (tempo_sync && sync_) { + if (sync_counter_ < kSyncCounterMaxTime) { + uint32_t period = 0; + if (sync_counter_ < 167) { + period = (3 * period_ + sync_counter_) >> 2; + tempo_sync = false; + } else { + period = pattern_predictor_.Predict(sync_counter_); + } + if (period != period_) { + period_ = period; + sync_phase_increment_ = 0xffffffff / period_; + //sync_phase_increment_ = multiply_u32xu32_rshift24((0xffffffff / period_), 16183969) ; + } + } + sync_counter_ = 0; + } + + + // reset phase + if (reset_phase || phase_reset_flag_) { + std::fill(&phase_[0], &phase_[kNumChannels], 0); + phase_reset_flag_ = false ; + } else { + // increment freqs for each LFO + if (sync_) { + phase_increment_ch1_ = sync_phase_increment_; + } else { + phase_increment_ch1_ = FrequencyToPhaseIncrement(frequency, freq_range_); + } + + // double F (via TR4) ? ... "/8", "/4", "/2", "x2", "x4", "x8" + if (freq_mult < 0xFF) { + phase_increment_ch1_ = (freq_mult < 0x3) ? (phase_increment_ch1_ >> (0x3 - freq_mult)) : phase_increment_ch1_ << (freq_mult - 0x2); + } + + phase_[0] += phase_increment_ch1_; + PolyLfoFreqMultipliers FreqDivs[] = {POLYLFO_FREQ_MULT_NONE, freq_div_b_, freq_div_c_ , freq_div_d_} ; + for (uint8_t i = 1; i < kNumChannels; ++i) { + if (FreqDivs[i] == POLYLFO_FREQ_MULT_NONE) { + phase_[i] += phase_increment_ch1_; + } else { + phase_[i] += multiply_u32xu32_rshift24(phase_increment_ch1_, PolyLfoFreqMultNumerators[FreqDivs[i]]) ; + } + } + + // Advance phasors. + if (spread_ >= 0) { + phase_difference_ = static_cast(spread_) << 15; + if (freq_div_b_ == POLYLFO_FREQ_MULT_NONE) { + phase_[1] = phase_[0] + phase_difference_; + } else { + phase_[1] = phase_[1] - last_phase_difference_ + phase_difference_; + } + if (freq_div_c_ == POLYLFO_FREQ_MULT_NONE) { + phase_[2] = phase_[0] + (2 * phase_difference_); + } else { + phase_[2] = phase_[2] - last_phase_difference_ + phase_difference_; + } + if (freq_div_d_ == POLYLFO_FREQ_MULT_NONE) { + phase_[3] = phase_[0] + (3 * phase_difference_); + } else { + phase_[3] = phase_[3] - last_phase_difference_ + phase_difference_; + } + } else { + for (uint8_t i = 1; i < kNumChannels; ++i) { + // phase_[i] += FrequencyToPhaseIncrement(frequency, freq_range_); + phase_[i] -= i * (phase_increment_ch1_ >> 16) * spread_ ; + // frequency -= 5040 * spread_ >> 15; + } + } + last_phase_difference_ = phase_difference_; + } + + const uint8_t* sine = &wt_lfo_waveforms[17 * 257]; + + uint16_t wavetable_index = shape_; + uint8_t xor_depths[] = {0, b_xor_a_, c_xor_a_, d_xor_a_ } ; + uint8_t am_depths[] = {0, b_am_by_a_, c_am_by_b_, d_am_by_c_ } ; + // Wavetable lookup + for (uint8_t i = 0; i < kNumChannels; ++i) { + uint32_t phase = phase_[i]; + if (coupling_ > 0) { + phase += value_[(i + 1) % kNumChannels] * coupling_; + } else { + phase += value_[(i + kNumChannels - 1) % kNumChannels] * -coupling_; + } + const uint8_t* a = &wt_lfo_waveforms[(wavetable_index >> 12) * 257]; + const uint8_t* b = a + 257; + wt_value_[i] = Crossfade(a, b, phase, wavetable_index << 4) ; + value_[i] = Interpolate824(sine, phase); + level_[i] = (wt_value_[i] + 32768) >> 8; + // add bit-XOR + uint8_t depth_xor = xor_depths[i]; + if (depth_xor) { + dac_code_[i] = (wt_value_[i] + 32768) ^ (((wt_value_[0] + 32768) >> depth_xor) << depth_xor) ; + } else { + dac_code_[i] = wt_value_[i] + 32768; //Keyframer::ConvertToDacCode(value + 32768, 0); + } + // cross-channel AM + dac_code_[i] = (dac_code_[i] * (65535 - (((65535 - dac_code_[i-1]) * am_depths[i]) >> 8))) >> 16 ; + // attenuationand offset + dac_code_[i] = ((dac_code_[i] * attenuation_) >> 16) + offset_ ; + wavetable_index += shape_spread_; + } +} + +void PolyLfo::RenderPreview(uint16_t shape, uint16_t *buffer, size_t size) { + uint16_t wavetable_index = shape; + uint32_t phase = 0; + uint32_t phase_increment = (0xff << 24) / size; + while (size--) { + const uint8_t* a = &wt_lfo_waveforms[(wavetable_index >> 12) * 257]; + const uint8_t* b = a + 257; + int16_t value = Crossfade(a, b, phase, wavetable_index << 4); + *buffer++ = value + 32768; + phase += phase_increment; + } +} + + +} // namespace frames diff --git a/software/o_c_REV/frames_poly_lfo.h b/software/o_c_REV/frames_poly_lfo.h new file mode 100644 index 000000000..2925a4689 --- /dev/null +++ b/software/o_c_REV/frames_poly_lfo.h @@ -0,0 +1,310 @@ +// Copyright 2013 Olivier Gillet, 2015, 2016 Patrick Dowling and Tim Churches +// +// Original author: Olivier Gillet (ol.gillet@gmail.com) +// Adapted and modified for use in the Ornament + Crime module by: +// Patrick Dowling (pld@gurkenkiste.com) and Tim Churches (tim.churches@gmail.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +// See http://creativecommons.org/licenses/MIT/ for more information. +// +// ----------------------------------------------------------------------------- +// +// Poly LFO. + +#ifndef FRAMES_POLY_LFO_H_ +#define FRAMES_POLY_LFO_H_ + +#include "util/util_macros.h" +//#include "stmlib/stmlib.h" +//#include "frames/keyframer.h" +#include "peaks_pattern_predictor.h" + +const uint32_t kSyncCounterMaxTime = 8 * 16667; + +namespace frames { + +const size_t kNumChannels = 4; + +enum PolyLfoFreqMultipliers { + POLYLFO_FREQ_MULT_BY16, // 0 + POLYLFO_FREQ_MULT_BY15, // 1 + POLYLFO_FREQ_MULT_BY14, // 2 + POLYLFO_FREQ_MULT_BY13, // 3 + POLYLFO_FREQ_MULT_BY12, // 4 + POLYLFO_FREQ_MULT_BY11, // 5 + POLYLFO_FREQ_MULT_BY10, // 6 + POLYLFO_FREQ_MULT_BY9, // 7 + POLYLFO_FREQ_MULT_BY8, // 8 + POLYLFO_FREQ_MULT_BY7, // 9 + POLYLFO_FREQ_MULT_BY6, // 10 + POLYLFO_FREQ_MULT_BY5, // 11 + POLYLFO_FREQ_MULT_BY4, // 12 + POLYLFO_FREQ_MULT_BY3, // 13 + POLYLFO_FREQ_MULT_5_OVER_2, // 14 + POLYLFO_FREQ_MULT_BY2, // 15 + POLYLFO_FREQ_MULT_5_OVER_3, // 16 + POLYLFO_FREQ_MULT_3_OVER_2, // 17 + POLYLFO_FREQ_MULT_5_OVER_4, // 18 + POLYLFO_FREQ_MULT_NONE, // 19 + POLYLFO_FREQ_MULT_4_OVER_5, // 20 + POLYLFO_FREQ_MULT_2_OVER_3, // 21 + POLYLFO_FREQ_MULT_3_OVER_5, // 22 + POLYLFO_FREQ_MULT_1_OVER_2, // 23 + POLYLFO_FREQ_MULT_2_OVER_5, // 24 + POLYLFO_FREQ_MULT_1_OVER_3, // 25 + POLYLFO_FREQ_MULT_1_OVER_4, // 26 + POLYLFO_FREQ_MULT_1_OVER_5, // 27 + POLYLFO_FREQ_MULT_1_OVER_6, // 28 + POLYLFO_FREQ_MULT_1_OVER_7, // 29 + POLYLFO_FREQ_MULT_1_OVER_8, // 30 + POLYLFO_FREQ_MULT_1_OVER_9, // 31 + POLYLFO_FREQ_MULT_1_OVER_10, // 32 + POLYLFO_FREQ_MULT_1_OVER_11, // 33 + POLYLFO_FREQ_MULT_1_OVER_12, // 34 + POLYLFO_FREQ_MULT_1_OVER_13, // 35 + POLYLFO_FREQ_MULT_1_OVER_14, // 36 + POLYLFO_FREQ_MULT_1_OVER_15, // 37 + POLYLFO_FREQ_MULT_1_OVER_16, // 38 + POLYLFO_FREQ_MULT_LAST // 39 +}; + +int32_t const PolyLfoFreqMultNumerators[] = { + 268435456, // POLYLFO_FREQ_MULT_BY16, = 0 + 251658240, // POLYLFO_FREQ_MULT_BY15, = 1 + 234881024, // POLYLFO_FREQ_MULT_BY14, = 2 + 218103808, // POLYLFO_FREQ_MULT_BY13, = 3 + 201326592, // POLYLFO_FREQ_MULT_BY12, = 4 + 184549376, // POLYLFO_FREQ_MULT_BY11, = 5 + 167772160, // POLYLFO_FREQ_MULT_BY10, = 6 + 150994944, // POLYLFO_FREQ_MULT_BY9, = 7 + 134217728, // POLYLFO_FREQ_MULT_BY8, = 8 + 117440512, // POLYLFO_FREQ_MULT_BY7, = 9 + 100663296, // POLYLFO_FREQ_MULT_BY6, = 10 + 83886080, // POLYLFO_FREQ_MULT_BY5, = 11 + 67108864, // POLYLFO_FREQ_MULT_BY4, = 12 + 50331648, // POLYLFO_FREQ_MULT_BY3, = 13 + 41943040, // POLYLFO_FREQ_MULT_5_OVER_2, = 14 + 33554432, // POLYLFO_FREQ_MULT_BY2, = 15 + 27962027, // POLYLFO_FREQ_MULT_5_OVER_3, = 16 + 25165824, // POLYLFO_FREQ_MULT_3_OVER_2, = 17 + 20971520, // POLYLFO_FREQ_MULT_5_OVER_4, = 18 + 16777216, // POLYLFO_FREQ_MULT_NONE, = 19 + 13421772, // POLYLFO_FREQ_MULT_4_OVER_5, = 20 + 11184810, // POLYLFO_FREQ_MULT_2_OVER_3, = 21 + 10066329, // POLYLFO_FREQ_MULT_3_OVER_5, = 22 + 8388608, // POLYLFO_FREQ_MULT_1_OVER_2, = 23 + 6710886, // POLYLFO_FREQ_MULT_2_OVER_5, = 24 + 5592405, // POLYLFO_FREQ_MULT_1_OVER_3, = 25 + 4194304, // POLYLFO_FREQ_MULT_1_OVER_4, = 26 + 3355443, // POLYLFO_FREQ_MULT_1_OVER_5, = 27 + 2796202, // POLYLFO_FREQ_MULT_1_OVER_6, = 28 + 2396745, // POLYLFO_FREQ_MULT_1_OVER_7, = 29 + 2097152, // POLYLFO_FREQ_MULT_1_OVER_8, = 30 + 1864135, // POLYLFO_FREQ_MULT_1_OVER_9, = 31 + 1677721, // POLYLFO_FREQ_MULT_1_OVER_10, = 32 + 1525201, // POLYLFO_FREQ_MULT_1_OVER_11, = 33 + 1398101, // POLYLFO_FREQ_MULT_1_OVER_12, = 34 + 1290555, // POLYLFO_FREQ_MULT_1_OVER_13, = 35 + 1198372, // POLYLFO_FREQ_MULT_1_OVER_14, = 36 + 1118481, // POLYLFO_FREQ_MULT_1_OVER_15, = 37 + 1048576, // POLYLFO_FREQ_MULT_1_OVER_16, = 38 +} ; + +class PolyLfo { + public: + PolyLfo() { } + ~PolyLfo() { } + + void Init(); + void Render(int32_t frequency, bool reset_phase, bool tempo_sync, uint8_t freq_mult); + void RenderPreview(uint16_t shape, uint16_t *buffer, size_t size); + + inline void set_freq_range(uint16_t freq_range) { + freq_range_ = freq_range; + } + + inline void set_shape(uint16_t shape) { + shape_ = shape; + } + + inline void set_shape_spread(uint16_t shape_spread) { + shape_spread_ = static_cast(shape_spread - 32767) >> 1; + } + + inline void set_spread(uint16_t spread) { + if (spread < 32768) { + int32_t x = spread - 32768; + int32_t scaled = -(x * x >> 15); + spread_ = (x + 3 * scaled) >> 2; + } else { + spread_ = spread - 32768; + } + } + + inline void set_coupling(uint16_t coupling) { + int32_t x = coupling - 32768; + int32_t scaled = x * x >> 15; + scaled = x > 0 ? scaled : - scaled; + scaled = (x + 3 * scaled) >> 2; + coupling_ = (scaled >> 4) * 10; + + } + + inline void set_attenuation(uint16_t attenuation) { + attenuation_ = attenuation; + // attenuation_ = 65535; + } + + inline void set_offset(int16_t offs) { + offset_ = offs; + } + + inline void set_freq_div_b(PolyLfoFreqMultipliers div) { + if (div != freq_div_b_) { + freq_div_b_ = div; + phase_reset_flag_ = true; + } + } + + inline void set_freq_div_c(PolyLfoFreqMultipliers div) { + if (div != freq_div_c_) { + freq_div_c_ = div; + phase_reset_flag_ = true; + } + } + + inline void set_freq_div_d(PolyLfoFreqMultipliers div) { + if (div != freq_div_d_) { + freq_div_d_ = div; + phase_reset_flag_ = true; + } + } + + inline void set_b_xor_a(uint8_t xor_value) { + if (xor_value) { + b_xor_a_ = 16 - xor_value ; + } else { + b_xor_a_ = 0; + } + } + + inline void set_c_xor_a(uint8_t xor_value) { + if (xor_value) { + c_xor_a_ = 16 - xor_value ; + } else { + c_xor_a_ = 0; + } + } + + inline void set_d_xor_a(uint8_t xor_value) { + if (xor_value) { + d_xor_a_ = 16 - xor_value ; + } else { + d_xor_a_ = 0; + } + } + + inline void set_b_am_by_a(uint8_t am_value) { + b_am_by_a_ = (am_value << 1); + } + + inline void set_c_am_by_b(uint8_t am_value) { + c_am_by_b_ = (am_value << 1); + } + + inline void set_d_am_by_c(uint8_t am_value) { + d_am_by_c_ = (am_value << 1); + } + + inline void set_phase_reset_flag(bool reset) { + phase_reset_flag_ = reset; + } + + inline void set_sync(bool sync) { + sync_ = sync; + } + + inline int get_sync() { + return static_cast(sync_); + } + + inline long get_sync_phase_increment() { + return sync_phase_increment_; + } + + inline float get_freq_ch1() { + return(static_cast(16666.6666666666666666667f * static_cast(phase_increment_ch1_) / static_cast(0xffffffff))); + } + + inline long get_sync_counter() { + return sync_counter_; + } + + inline uint8_t level(uint8_t index) const { + return level_[index]; + } + + inline const uint16_t dac_code(uint8_t index) const { + return dac_code_[index]; + } + + static uint32_t FrequencyToPhaseIncrement(int32_t frequency, uint16_t frq_rng); + + + private: + uint16_t freq_range_ ; + uint16_t shape_; + int16_t shape_spread_; + int32_t spread_; + int16_t coupling_; + int32_t attenuation_; + int32_t offset_; + PolyLfoFreqMultipliers freq_div_b_; + PolyLfoFreqMultipliers freq_div_c_; + PolyLfoFreqMultipliers freq_div_d_; + uint8_t b_xor_a_ ; + uint8_t c_xor_a_ ; + uint8_t d_xor_a_ ; + uint8_t b_am_by_a_ ; + uint8_t c_am_by_b_ ; + uint8_t d_am_by_c_ ; + bool phase_reset_flag_ ; + + int16_t value_[kNumChannels]; + int16_t wt_value_[kNumChannels]; + uint32_t phase_[kNumChannels]; + uint32_t phase_increment_ch1_; + uint8_t level_[kNumChannels]; + uint16_t dac_code_[kNumChannels]; + + bool sync_ ; + uint32_t sync_counter_; + stmlib::PatternPredictor<32, 8> pattern_predictor_; + uint32_t period_; + uint32_t sync_phase_increment_; + uint32_t phase_difference_ ; + uint32_t last_phase_difference_ ; + + DISALLOW_COPY_AND_ASSIGN(PolyLfo); +}; + +} // namespace frames + +#endif // FRAMES_POLY_LFO_H_ diff --git a/software/o_c_REV/frames_resources.cpp b/software/o_c_REV/frames_resources.cpp new file mode 100644 index 000000000..6179a2957 --- /dev/null +++ b/software/o_c_REV/frames_resources.cpp @@ -0,0 +1,2770 @@ +// Copyright 2013 Olivier Gillet. +// +// Author: Olivier Gillet (ol.gillet@gmail.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +// See http://creativecommons.org/licenses/MIT/ for more information. +// +// ----------------------------------------------------------------------------- +// +// Resources definitions. +// +// Automatically generated with: +// make resources + + +#include "frames_resources.h" + +namespace frames { + +static const char str_dummy[] = "dummy"; + + +const char* string_table[] = { + str_dummy, +}; + +/* +const uint16_t lut_easing_in_quartic[] = { + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 1, 1, 1, + 1, 1, 1, 1, + 1, 1, 1, 1, + 1, 2, 2, 2, + 2, 2, 2, 2, + 2, 3, 3, 3, + 3, 3, 3, 4, + 4, 4, 4, 4, + 5, 5, 5, 5, + 5, 6, 6, 6, + 6, 7, 7, 7, + 8, 8, 8, 9, + 9, 9, 10, 10, + 10, 11, 11, 11, + 12, 12, 13, 13, + 14, 14, 15, 15, + 15, 16, 17, 17, + 18, 18, 19, 19, + 20, 20, 21, 22, + 22, 23, 24, 24, + 25, 26, 27, 27, + 28, 29, 30, 30, + 31, 32, 33, 34, + 35, 36, 37, 38, + 39, 40, 41, 42, + 43, 44, 45, 46, + 47, 48, 49, 50, + 52, 53, 54, 55, + 57, 58, 59, 61, + 62, 63, 65, 66, + 68, 69, 71, 72, + 74, 76, 77, 79, + 80, 82, 84, 86, + 87, 89, 91, 93, + 95, 97, 99, 101, + 103, 105, 107, 109, + 111, 113, 115, 118, + 120, 122, 125, 127, + 129, 132, 134, 137, + 139, 142, 144, 147, + 150, 152, 155, 158, + 161, 163, 166, 169, + 172, 175, 178, 181, + 184, 188, 191, 194, + 197, 201, 204, 207, + 211, 214, 218, 221, + 225, 229, 232, 236, + 240, 244, 248, 252, + 255, 260, 264, 268, + 272, 276, 280, 285, + 289, 293, 298, 302, + 307, 312, 316, 321, + 326, 331, 335, 340, + 345, 350, 356, 361, + 366, 371, 376, 382, + 387, 393, 398, 404, + 410, 415, 421, 427, + 433, 439, 445, 451, + 457, 463, 470, 476, + 482, 489, 495, 502, + 509, 515, 522, 529, + 536, 543, 550, 557, + 564, 572, 579, 586, + 594, 601, 609, 617, + 624, 632, 640, 648, + 656, 664, 673, 681, + 689, 698, 706, 715, + 724, 732, 741, 750, + 759, 768, 777, 787, + 796, 805, 815, 824, + 834, 844, 854, 864, + 874, 884, 894, 904, + 915, 925, 936, 946, + 957, 968, 979, 990, + 1001, 1012, 1023, 1034, + 1046, 1057, 1069, 1081, + 1093, 1105, 1117, 1129, + 1141, 1153, 1166, 1178, + 1191, 1204, 1216, 1229, + 1242, 1255, 1269, 1282, + 1295, 1309, 1323, 1336, + 1350, 1364, 1378, 1393, + 1407, 1421, 1436, 1450, + 1465, 1480, 1495, 1510, + 1525, 1541, 1556, 1572, + 1587, 1603, 1619, 1635, + 1651, 1667, 1684, 1700, + 1717, 1734, 1750, 1767, + 1785, 1802, 1819, 1837, + 1854, 1872, 1890, 1908, + 1926, 1944, 1962, 1981, + 2000, 2018, 2037, 2056, + 2075, 2095, 2114, 2134, + 2153, 2173, 2193, 2213, + 2234, 2254, 2274, 2295, + 2316, 2337, 2358, 2379, + 2400, 2422, 2444, 2465, + 2487, 2509, 2532, 2554, + 2577, 2599, 2622, 2645, + 2668, 2692, 2715, 2739, + 2762, 2786, 2810, 2834, + 2859, 2883, 2908, 2933, + 2958, 2983, 3008, 3034, + 3059, 3085, 3111, 3137, + 3164, 3190, 3217, 3243, + 3270, 3297, 3325, 3352, + 3380, 3408, 3436, 3464, + 3492, 3520, 3549, 3578, + 3607, 3636, 3665, 3695, + 3725, 3755, 3785, 3815, + 3845, 3876, 3907, 3938, + 3969, 4000, 4032, 4064, + 4095, 4128, 4160, 4192, + 4225, 4258, 4291, 4324, + 4357, 4391, 4425, 4459, + 4493, 4528, 4562, 4597, + 4632, 4667, 4703, 4738, + 4774, 4810, 4846, 4883, + 4919, 4956, 4993, 5030, + 5068, 5105, 5143, 5181, + 5219, 5258, 5297, 5336, + 5375, 5414, 5454, 5493, + 5533, 5574, 5614, 5655, + 5696, 5737, 5778, 5819, + 5861, 5903, 5945, 5988, + 6031, 6073, 6117, 6160, + 6203, 6247, 6291, 6336, + 6380, 6425, 6470, 6515, + 6560, 6606, 6652, 6698, + 6745, 6791, 6838, 6885, + 6933, 6980, 7028, 7076, + 7124, 7173, 7222, 7271, + 7320, 7370, 7420, 7470, + 7520, 7571, 7622, 7673, + 7724, 7776, 7828, 7880, + 7932, 7985, 8038, 8091, + 8144, 8198, 8252, 8306, + 8361, 8416, 8471, 8526, + 8582, 8638, 8694, 8750, + 8807, 8864, 8921, 8978, + 9036, 9094, 9153, 9211, + 9270, 9329, 9389, 9449, + 9509, 9569, 9630, 9690, + 9752, 9813, 9875, 9937, + 9999, 10062, 10125, 10188, + 10252, 10316, 10380, 10444, + 10509, 10574, 10639, 10705, + 10771, 10837, 10903, 10970, + 11037, 11105, 11173, 11241, + 11309, 11378, 11447, 11516, + 11586, 11656, 11726, 11797, + 11868, 11939, 12010, 12082, + 12154, 12227, 12300, 12373, + 12446, 12520, 12594, 12669, + 12744, 12819, 12894, 12970, + 13046, 13123, 13199, 13277, + 13354, 13432, 13510, 13588, + 13667, 13746, 13826, 13906, + 13986, 14067, 14148, 14229, + 14310, 14392, 14475, 14557, + 14640, 14724, 14807, 14891, + 14976, 15061, 15146, 15231, + 15317, 15403, 15490, 15577, + 15664, 15752, 15840, 15929, + 16017, 16106, 16196, 16286, + 16376, 16467, 16558, 16649, + 16741, 16833, 16926, 17019, + 17112, 17206, 17300, 17394, + 17489, 17585, 17680, 17776, + 17873, 17969, 18067, 18164, + 18262, 18361, 18459, 18559, + 18658, 18758, 18858, 18959, + 19060, 19162, 19264, 19366, + 19469, 19572, 19676, 19780, + 19885, 19989, 20095, 20200, + 20307, 20413, 20520, 20627, + 20735, 20843, 20952, 21061, + 21171, 21280, 21391, 21502, + 21613, 21724, 21836, 21949, + 22062, 22175, 22289, 22403, + 22518, 22633, 22749, 22865, + 22981, 23098, 23215, 23333, + 23451, 23570, 23689, 23809, + 23929, 24049, 24170, 24291, + 24413, 24535, 24658, 24781, + 24905, 25029, 25154, 25279, + 25404, 25530, 25657, 25784, + 25911, 26039, 26168, 26296, + 26426, 26555, 26686, 26816, + 26948, 27079, 27212, 27344, + 27477, 27611, 27745, 27880, + 28015, 28150, 28286, 28423, + 28560, 28698, 28836, 28974, + 29113, 29253, 29393, 29533, + 29674, 29816, 29958, 30101, + 30244, 30387, 30531, 30676, + 30821, 30967, 31113, 31260, + 31407, 31555, 31703, 31852, + 32001, 32151, 32301, 32452, + 32603, 32755, 32908, 33061, + 33214, 33368, 33523, 33678, + 33833, 33990, 34146, 34304, + 34461, 34620, 34779, 34938, + 35098, 35259, 35420, 35581, + 35744, 35906, 36070, 36234, + 36398, 36563, 36728, 36895, + 37061, 37228, 37396, 37565, + 37734, 37903, 38073, 38244, + 38415, 38587, 38759, 38932, + 39106, 39280, 39454, 39630, + 39805, 39982, 40159, 40336, + 40515, 40693, 40873, 41053, + 41233, 41414, 41596, 41778, + 41961, 42145, 42329, 42514, + 42699, 42885, 43072, 43259, + 43447, 43635, 43824, 44014, + 44204, 44395, 44586, 44778, + 44971, 45164, 45358, 45553, + 45748, 45944, 46140, 46337, + 46535, 46733, 46932, 47132, + 47332, 47533, 47735, 47937, + 48140, 48343, 48547, 48752, + 48957, 49163, 49370, 49577, + 49785, 49994, 50203, 50413, + 50624, 50835, 51047, 51260, + 51473, 51687, 51901, 52116, + 52332, 52549, 52766, 52984, + 53203, 53422, 53642, 53863, + 54084, 54306, 54529, 54752, + 54976, 55201, 55426, 55652, + 55879, 56107, 56335, 56564, + 56793, 57024, 57255, 57486, + 57719, 57952, 58186, 58420, + 58655, 58891, 59128, 59365, + 59603, 59842, 60081, 60322, + 60563, 60804, 61047, 61290, + 61534, 61778, 62023, 62269, + 62516, 62764, 63012, 63261, + 63510, 63761, 64012, 64264, + 64516, 64770, 65024, 65279, + 65535, +}; +const uint16_t lut_easing_out_quartic[] = { + 0, 255, 510, 764, + 1018, 1270, 1522, 1773, + 2024, 2273, 2522, 2770, + 3018, 3265, 3511, 3756, + 4000, 4244, 4487, 4730, + 4971, 5212, 5453, 5692, + 5931, 6169, 6406, 6643, + 6879, 7114, 7348, 7582, + 7815, 8048, 8279, 8510, + 8741, 8970, 9199, 9427, + 9655, 9882, 10108, 10333, + 10558, 10782, 11005, 11228, + 11450, 11671, 11892, 12112, + 12331, 12550, 12768, 12985, + 13202, 13418, 13633, 13847, + 14061, 14274, 14487, 14699, + 14910, 15121, 15331, 15540, + 15749, 15957, 16164, 16371, + 16577, 16782, 16987, 17191, + 17394, 17597, 17799, 18001, + 18202, 18402, 18602, 18801, + 18999, 19197, 19394, 19590, + 19786, 19981, 20176, 20370, + 20563, 20756, 20948, 21139, + 21330, 21520, 21710, 21899, + 22087, 22275, 22462, 22649, + 22835, 23020, 23205, 23389, + 23573, 23756, 23938, 24120, + 24301, 24481, 24661, 24841, + 25019, 25198, 25375, 25552, + 25729, 25904, 26080, 26254, + 26428, 26602, 26775, 26947, + 27119, 27290, 27461, 27631, + 27800, 27969, 28138, 28306, + 28473, 28639, 28806, 28971, + 29136, 29300, 29464, 29628, + 29790, 29953, 30114, 30275, + 30436, 30596, 30755, 30914, + 31073, 31230, 31388, 31544, + 31701, 31856, 32011, 32166, + 32320, 32473, 32626, 32779, + 32931, 33082, 33233, 33383, + 33533, 33682, 33831, 33979, + 34127, 34274, 34421, 34567, + 34713, 34858, 35003, 35147, + 35290, 35433, 35576, 35718, + 35860, 36001, 36141, 36281, + 36421, 36560, 36698, 36836, + 36974, 37111, 37248, 37384, + 37519, 37654, 37789, 37923, + 38057, 38190, 38322, 38455, + 38586, 38718, 38848, 38979, + 39108, 39238, 39366, 39495, + 39623, 39750, 39877, 40004, + 40130, 40255, 40380, 40505, + 40629, 40753, 40876, 40999, + 41121, 41243, 41364, 41485, + 41605, 41725, 41845, 41964, + 42083, 42201, 42319, 42436, + 42553, 42669, 42785, 42901, + 43016, 43131, 43245, 43359, + 43472, 43585, 43698, 43810, + 43921, 44032, 44143, 44254, + 44363, 44473, 44582, 44691, + 44799, 44907, 45014, 45121, + 45227, 45334, 45439, 45545, + 45649, 45754, 45858, 45962, + 46065, 46168, 46270, 46372, + 46474, 46575, 46676, 46776, + 46876, 46975, 47075, 47173, + 47272, 47370, 47467, 47565, + 47661, 47758, 47854, 47949, + 48045, 48140, 48234, 48328, + 48422, 48515, 48608, 48701, + 48793, 48885, 48976, 49067, + 49158, 49248, 49338, 49428, + 49517, 49605, 49694, 49782, + 49870, 49957, 50044, 50131, + 50217, 50303, 50388, 50473, + 50558, 50643, 50727, 50810, + 50894, 50977, 51059, 51142, + 51224, 51305, 51386, 51467, + 51548, 51628, 51708, 51788, + 51867, 51946, 52024, 52102, + 52180, 52257, 52335, 52411, + 52488, 52564, 52640, 52715, + 52790, 52865, 52940, 53014, + 53088, 53161, 53234, 53307, + 53380, 53452, 53524, 53595, + 53666, 53737, 53808, 53878, + 53948, 54018, 54087, 54156, + 54225, 54293, 54361, 54429, + 54497, 54564, 54631, 54697, + 54763, 54829, 54895, 54960, + 55025, 55090, 55154, 55218, + 55282, 55346, 55409, 55472, + 55535, 55597, 55659, 55721, + 55782, 55844, 55904, 55965, + 56025, 56085, 56145, 56205, + 56264, 56323, 56381, 56440, + 56498, 56556, 56613, 56670, + 56727, 56784, 56840, 56896, + 56952, 57008, 57063, 57118, + 57173, 57228, 57282, 57336, + 57390, 57443, 57496, 57549, + 57602, 57654, 57706, 57758, + 57810, 57861, 57912, 57963, + 58014, 58064, 58114, 58164, + 58214, 58263, 58312, 58361, + 58410, 58458, 58506, 58554, + 58601, 58649, 58696, 58743, + 58789, 58836, 58882, 58928, + 58974, 59019, 59064, 59109, + 59154, 59198, 59243, 59287, + 59331, 59374, 59417, 59461, + 59503, 59546, 59589, 59631, + 59673, 59715, 59756, 59797, + 59838, 59879, 59920, 59960, + 60001, 60041, 60080, 60120, + 60159, 60198, 60237, 60276, + 60315, 60353, 60391, 60429, + 60466, 60504, 60541, 60578, + 60615, 60651, 60688, 60724, + 60760, 60796, 60831, 60867, + 60902, 60937, 60972, 61006, + 61041, 61075, 61109, 61143, + 61177, 61210, 61243, 61276, + 61309, 61342, 61374, 61406, + 61439, 61470, 61502, 61534, + 61565, 61596, 61627, 61658, + 61689, 61719, 61749, 61779, + 61809, 61839, 61869, 61898, + 61927, 61956, 61985, 62014, + 62042, 62070, 62098, 62126, + 62154, 62182, 62209, 62237, + 62264, 62291, 62317, 62344, + 62370, 62397, 62423, 62449, + 62475, 62500, 62526, 62551, + 62576, 62601, 62626, 62651, + 62675, 62700, 62724, 62748, + 62772, 62795, 62819, 62842, + 62866, 62889, 62912, 62935, + 62957, 62980, 63002, 63025, + 63047, 63069, 63090, 63112, + 63134, 63155, 63176, 63197, + 63218, 63239, 63260, 63280, + 63300, 63321, 63341, 63361, + 63381, 63400, 63420, 63439, + 63459, 63478, 63497, 63516, + 63534, 63553, 63572, 63590, + 63608, 63626, 63644, 63662, + 63680, 63697, 63715, 63732, + 63749, 63767, 63784, 63800, + 63817, 63834, 63850, 63867, + 63883, 63899, 63915, 63931, + 63947, 63962, 63978, 63993, + 64009, 64024, 64039, 64054, + 64069, 64084, 64098, 64113, + 64127, 64141, 64156, 64170, + 64184, 64198, 64211, 64225, + 64239, 64252, 64265, 64279, + 64292, 64305, 64318, 64330, + 64343, 64356, 64368, 64381, + 64393, 64405, 64417, 64429, + 64441, 64453, 64465, 64477, + 64488, 64500, 64511, 64522, + 64533, 64544, 64555, 64566, + 64577, 64588, 64598, 64609, + 64619, 64630, 64640, 64650, + 64660, 64670, 64680, 64690, + 64700, 64710, 64719, 64729, + 64738, 64747, 64757, 64766, + 64775, 64784, 64793, 64802, + 64810, 64819, 64828, 64836, + 64845, 64853, 64861, 64870, + 64878, 64886, 64894, 64902, + 64910, 64917, 64925, 64933, + 64940, 64948, 64955, 64962, + 64970, 64977, 64984, 64991, + 64998, 65005, 65012, 65019, + 65025, 65032, 65039, 65045, + 65052, 65058, 65064, 65071, + 65077, 65083, 65089, 65095, + 65101, 65107, 65113, 65119, + 65124, 65130, 65136, 65141, + 65147, 65152, 65158, 65163, + 65168, 65173, 65178, 65184, + 65189, 65194, 65199, 65203, + 65208, 65213, 65218, 65222, + 65227, 65232, 65236, 65241, + 65245, 65249, 65254, 65258, + 65262, 65266, 65270, 65274, + 65279, 65282, 65286, 65290, + 65294, 65298, 65302, 65305, + 65309, 65313, 65316, 65320, + 65323, 65327, 65330, 65333, + 65337, 65340, 65343, 65346, + 65350, 65353, 65356, 65359, + 65362, 65365, 65368, 65371, + 65373, 65376, 65379, 65382, + 65384, 65387, 65390, 65392, + 65395, 65397, 65400, 65402, + 65405, 65407, 65409, 65412, + 65414, 65416, 65419, 65421, + 65423, 65425, 65427, 65429, + 65431, 65433, 65435, 65437, + 65439, 65441, 65443, 65445, + 65447, 65448, 65450, 65452, + 65454, 65455, 65457, 65458, + 65460, 65462, 65463, 65465, + 65466, 65468, 65469, 65471, + 65472, 65473, 65475, 65476, + 65477, 65479, 65480, 65481, + 65482, 65484, 65485, 65486, + 65487, 65488, 65489, 65490, + 65491, 65492, 65493, 65494, + 65495, 65496, 65497, 65498, + 65499, 65500, 65501, 65502, + 65503, 65504, 65504, 65505, + 65506, 65507, 65507, 65508, + 65509, 65510, 65510, 65511, + 65512, 65512, 65513, 65514, + 65514, 65515, 65515, 65516, + 65516, 65517, 65517, 65518, + 65519, 65519, 65519, 65520, + 65520, 65521, 65521, 65522, + 65522, 65523, 65523, 65523, + 65524, 65524, 65524, 65525, + 65525, 65525, 65526, 65526, + 65526, 65527, 65527, 65527, + 65528, 65528, 65528, 65528, + 65529, 65529, 65529, 65529, + 65529, 65530, 65530, 65530, + 65530, 65530, 65531, 65531, + 65531, 65531, 65531, 65531, + 65532, 65532, 65532, 65532, + 65532, 65532, 65532, 65532, + 65533, 65533, 65533, 65533, + 65533, 65533, 65533, 65533, + 65533, 65533, 65533, 65533, + 65534, 65534, 65534, 65534, + 65534, 65534, 65534, 65534, + 65534, 65534, 65534, 65534, + 65534, 65534, 65534, 65534, + 65534, 65534, 65534, 65534, + 65534, 65534, 65534, 65534, + 65534, 65534, 65534, 65534, + 65534, 65534, 65534, 65534, + 65534, 65534, 65534, 65534, + 65534, 65534, 65534, 65534, + 65534, 65534, 65534, 65534, + 65534, 65534, 65534, 65534, + 65534, 65534, 65534, 65534, + 65534, 65534, 65534, 65534, + 65534, 65534, 65534, 65534, + 65534, 65534, 65534, 65534, + 65535, +}; +const uint16_t lut_easing_in_out_sine[] = { + 0, 0, 0, 1, + 2, 3, 5, 7, + 9, 12, 15, 18, + 22, 26, 30, 34, + 39, 44, 49, 55, + 61, 67, 74, 81, + 88, 96, 104, 112, + 120, 129, 138, 148, + 157, 167, 178, 188, + 199, 210, 222, 234, + 246, 258, 271, 284, + 298, 311, 325, 340, + 354, 369, 384, 400, + 416, 432, 448, 465, + 482, 499, 517, 535, + 553, 572, 590, 610, + 629, 649, 669, 689, + 710, 731, 752, 774, + 796, 818, 840, 863, + 886, 910, 933, 957, + 982, 1006, 1031, 1056, + 1082, 1107, 1133, 1160, + 1186, 1213, 1241, 1268, + 1296, 1324, 1353, 1381, + 1410, 1440, 1469, 1499, + 1530, 1560, 1591, 1622, + 1653, 1685, 1717, 1749, + 1782, 1815, 1848, 1881, + 1915, 1949, 1983, 2018, + 2053, 2088, 2123, 2159, + 2195, 2231, 2268, 2305, + 2342, 2380, 2417, 2455, + 2494, 2532, 2571, 2610, + 2650, 2690, 2730, 2770, + 2811, 2852, 2893, 2934, + 2976, 3018, 3060, 3103, + 3146, 3189, 3232, 3276, + 3320, 3364, 3408, 3453, + 3498, 3544, 3589, 3635, + 3681, 3728, 3774, 3821, + 3869, 3916, 3964, 4012, + 4060, 4109, 4158, 4207, + 4256, 4306, 4356, 4406, + 4457, 4508, 4559, 4610, + 4661, 4713, 4765, 4818, + 4870, 4923, 4976, 5030, + 5083, 5137, 5191, 5246, + 5300, 5355, 5411, 5466, + 5522, 5578, 5634, 5691, + 5747, 5804, 5862, 5919, + 5977, 6035, 6093, 6152, + 6210, 6269, 6329, 6388, + 6448, 6508, 6568, 6629, + 6689, 6750, 6812, 6873, + 6935, 6997, 7059, 7121, + 7184, 7247, 7310, 7374, + 7437, 7501, 7565, 7630, + 7694, 7759, 7824, 7890, + 7955, 8021, 8087, 8153, + 8220, 8286, 8353, 8420, + 8488, 8556, 8623, 8691, + 8760, 8828, 8897, 8966, + 9035, 9105, 9174, 9244, + 9314, 9385, 9455, 9526, + 9597, 9668, 9739, 9811, + 9883, 9955, 10027, 10100, + 10172, 10245, 10319, 10392, + 10465, 10539, 10613, 10687, + 10762, 10836, 10911, 10986, + 11061, 11137, 11212, 11288, + 11364, 11440, 11517, 11593, + 11670, 11747, 11824, 11902, + 11980, 12057, 12135, 12214, + 12292, 12371, 12449, 12528, + 12607, 12687, 12766, 12846, + 12926, 13006, 13086, 13167, + 13247, 13328, 13409, 13490, + 13572, 13653, 13735, 13817, + 13899, 13981, 14064, 14147, + 14229, 14312, 14396, 14479, + 14562, 14646, 14730, 14814, + 14898, 14982, 15067, 15152, + 15236, 15321, 15407, 15492, + 15578, 15663, 15749, 15835, + 15921, 16007, 16094, 16181, + 16267, 16354, 16441, 16529, + 16616, 16704, 16791, 16879, + 16967, 17055, 17143, 17232, + 17321, 17409, 17498, 17587, + 17676, 17766, 17855, 17945, + 18034, 18124, 18214, 18304, + 18395, 18485, 18576, 18666, + 18757, 18848, 18939, 19030, + 19122, 19213, 19305, 19396, + 19488, 19580, 19672, 19765, + 19857, 19949, 20042, 20135, + 20227, 20320, 20413, 20507, + 20600, 20693, 20787, 20880, + 20974, 21068, 21162, 21256, + 21350, 21444, 21539, 21633, + 21728, 21823, 21917, 22012, + 22107, 22203, 22298, 22393, + 22488, 22584, 22680, 22775, + 22871, 22967, 23063, 23159, + 23255, 23351, 23448, 23544, + 23641, 23737, 23834, 23931, + 24027, 24124, 24221, 24319, + 24416, 24513, 24610, 24708, + 24805, 24903, 25000, 25098, + 25196, 25294, 25392, 25490, + 25588, 25686, 25784, 25882, + 25980, 26079, 26177, 26276, + 26374, 26473, 26572, 26670, + 26769, 26868, 26967, 27066, + 27165, 27264, 27363, 27462, + 27562, 27661, 27760, 27860, + 27959, 28058, 28158, 28258, + 28357, 28457, 28556, 28656, + 28756, 28856, 28956, 29055, + 29155, 29255, 29355, 29455, + 29555, 29655, 29755, 29855, + 29956, 30056, 30156, 30256, + 30356, 30457, 30557, 30657, + 30758, 30858, 30958, 31059, + 31159, 31260, 31360, 31460, + 31561, 31661, 31762, 31862, + 31963, 32063, 32164, 32264, + 32365, 32465, 32566, 32666, + 32767, 32868, 32968, 33069, + 33169, 33270, 33370, 33471, + 33571, 33672, 33772, 33873, + 33973, 34074, 34174, 34274, + 34375, 34475, 34576, 34676, + 34776, 34877, 34977, 35077, + 35178, 35278, 35378, 35478, + 35578, 35679, 35779, 35879, + 35979, 36079, 36179, 36279, + 36379, 36479, 36578, 36678, + 36778, 36878, 36978, 37077, + 37177, 37276, 37376, 37476, + 37575, 37674, 37774, 37873, + 37972, 38072, 38171, 38270, + 38369, 38468, 38567, 38666, + 38765, 38864, 38962, 39061, + 39160, 39258, 39357, 39455, + 39554, 39652, 39750, 39848, + 39946, 40044, 40142, 40240, + 40338, 40436, 40534, 40631, + 40729, 40826, 40924, 41021, + 41118, 41215, 41313, 41410, + 41507, 41603, 41700, 41797, + 41893, 41990, 42086, 42183, + 42279, 42375, 42471, 42567, + 42663, 42759, 42854, 42950, + 43046, 43141, 43236, 43331, + 43427, 43522, 43617, 43711, + 43806, 43901, 43995, 44090, + 44184, 44278, 44372, 44466, + 44560, 44654, 44747, 44841, + 44934, 45027, 45121, 45214, + 45307, 45399, 45492, 45585, + 45677, 45769, 45862, 45954, + 46046, 46138, 46229, 46321, + 46412, 46504, 46595, 46686, + 46777, 46868, 46958, 47049, + 47139, 47230, 47320, 47410, + 47500, 47589, 47679, 47768, + 47858, 47947, 48036, 48125, + 48213, 48302, 48391, 48479, + 48567, 48655, 48743, 48830, + 48918, 49005, 49093, 49180, + 49267, 49353, 49440, 49527, + 49613, 49699, 49785, 49871, + 49956, 50042, 50127, 50213, + 50298, 50382, 50467, 50552, + 50636, 50720, 50804, 50888, + 50972, 51055, 51138, 51222, + 51305, 51387, 51470, 51553, + 51635, 51717, 51799, 51881, + 51962, 52044, 52125, 52206, + 52287, 52367, 52448, 52528, + 52608, 52688, 52768, 52847, + 52927, 53006, 53085, 53163, + 53242, 53320, 53399, 53477, + 53554, 53632, 53710, 53787, + 53864, 53941, 54017, 54094, + 54170, 54246, 54322, 54397, + 54473, 54548, 54623, 54698, + 54772, 54847, 54921, 54995, + 55069, 55142, 55215, 55289, + 55362, 55434, 55507, 55579, + 55651, 55723, 55795, 55866, + 55937, 56008, 56079, 56149, + 56220, 56290, 56360, 56429, + 56499, 56568, 56637, 56706, + 56774, 56843, 56911, 56978, + 57046, 57114, 57181, 57248, + 57314, 57381, 57447, 57513, + 57579, 57644, 57710, 57775, + 57840, 57904, 57969, 58033, + 58097, 58160, 58224, 58287, + 58350, 58413, 58475, 58537, + 58599, 58661, 58722, 58784, + 58845, 58905, 58966, 59026, + 59086, 59146, 59205, 59265, + 59324, 59382, 59441, 59499, + 59557, 59615, 59672, 59730, + 59787, 59843, 59900, 59956, + 60012, 60068, 60123, 60179, + 60234, 60288, 60343, 60397, + 60451, 60504, 60558, 60611, + 60664, 60716, 60769, 60821, + 60873, 60924, 60975, 61026, + 61077, 61128, 61178, 61228, + 61278, 61327, 61376, 61425, + 61474, 61522, 61570, 61618, + 61665, 61713, 61760, 61806, + 61853, 61899, 61945, 61990, + 62036, 62081, 62126, 62170, + 62214, 62258, 62302, 62345, + 62388, 62431, 62474, 62516, + 62558, 62600, 62641, 62682, + 62723, 62764, 62804, 62844, + 62884, 62924, 62963, 63002, + 63040, 63079, 63117, 63154, + 63192, 63229, 63266, 63303, + 63339, 63375, 63411, 63446, + 63481, 63516, 63551, 63585, + 63619, 63653, 63686, 63719, + 63752, 63785, 63817, 63849, + 63881, 63912, 63943, 63974, + 64004, 64035, 64065, 64094, + 64124, 64153, 64181, 64210, + 64238, 64266, 64293, 64321, + 64348, 64374, 64401, 64427, + 64452, 64478, 64503, 64528, + 64552, 64577, 64601, 64624, + 64648, 64671, 64694, 64716, + 64738, 64760, 64782, 64803, + 64824, 64845, 64865, 64885, + 64905, 64924, 64944, 64962, + 64981, 64999, 65017, 65035, + 65052, 65069, 65086, 65102, + 65118, 65134, 65150, 65165, + 65180, 65194, 65209, 65223, + 65236, 65250, 65263, 65276, + 65288, 65300, 65312, 65324, + 65335, 65346, 65356, 65367, + 65377, 65386, 65396, 65405, + 65414, 65422, 65430, 65438, + 65446, 65453, 65460, 65467, + 65473, 65479, 65485, 65490, + 65495, 65500, 65504, 65508, + 65512, 65516, 65519, 65522, + 65525, 65527, 65529, 65531, + 65532, 65533, 65534, 65534, + 65535, +}; +const uint16_t lut_easing_in_out_bounce[] = { + 0, 0, 1, 4, + 7, 11, 17, 23, + 30, 38, 47, 57, + 68, 79, 92, 106, + 120, 136, 153, 170, + 189, 208, 228, 250, + 272, 295, 319, 344, + 370, 397, 425, 454, + 483, 514, 546, 578, + 612, 647, 682, 718, + 756, 794, 833, 873, + 915, 957, 1000, 1044, + 1088, 1134, 1181, 1229, + 1278, 1327, 1378, 1429, + 1482, 1535, 1589, 1645, + 1701, 1758, 1816, 1875, + 1935, 1996, 2058, 2121, + 2185, 2250, 2315, 2382, + 2450, 2518, 2588, 2658, + 2730, 2802, 2875, 2949, + 3024, 3101, 3178, 3256, + 3335, 3414, 3495, 3577, + 3660, 3743, 3828, 3914, + 4000, 4087, 4176, 4265, + 4355, 4447, 4539, 4632, + 4726, 4821, 4917, 5014, + 5112, 5210, 5310, 5411, + 5512, 5615, 5719, 5823, + 5928, 6035, 6142, 6250, + 6359, 6470, 6581, 6693, + 6806, 6920, 7034, 7150, + 7267, 7385, 7503, 7623, + 7743, 7865, 7987, 8111, + 8235, 8360, 8486, 8614, + 8742, 8871, 9001, 9132, + 9263, 9396, 9530, 9665, + 9800, 9937, 10074, 10213, + 10352, 10493, 10634, 10776, + 10920, 11064, 11209, 11355, + 11502, 11650, 11799, 11949, + 12099, 12251, 12404, 12557, + 12712, 12867, 13024, 13181, + 13340, 13499, 13659, 13820, + 13982, 14145, 14309, 14474, + 14640, 14807, 14975, 15144, + 15313, 15484, 15656, 15828, + 16002, 16176, 16351, 16528, + 16705, 16883, 17062, 17242, + 17423, 17605, 17788, 17972, + 18157, 18343, 18529, 18717, + 18905, 19095, 19285, 19477, + 19669, 19863, 20057, 20252, + 20448, 20645, 20843, 21042, + 21242, 21443, 21645, 21848, + 22051, 22256, 22462, 22668, + 22876, 23084, 23294, 23504, + 23715, 23927, 24141, 24355, + 24570, 24786, 25003, 25221, + 25439, 25659, 25880, 26102, + 26324, 26548, 26772, 26998, + 27224, 27451, 27680, 27909, + 28139, 28370, 28602, 28835, + 29069, 29304, 29540, 29777, + 30015, 30253, 30493, 30734, + 30975, 31217, 31461, 31705, + 31951, 32197, 32444, 32692, + 32941, 33191, 33442, 33694, + 33947, 34201, 34456, 34711, + 34968, 35226, 35484, 35744, + 36004, 36265, 36528, 36791, + 37055, 37320, 37586, 37853, + 38121, 38390, 38660, 38931, + 39203, 39476, 39749, 40024, + 40299, 40576, 40853, 41132, + 41411, 41691, 41973, 42255, + 42538, 42822, 43107, 43393, + 43680, 43968, 44256, 44546, + 44837, 45129, 45421, 45715, + 46009, 46304, 46601, 46898, + 47196, 47496, 47796, 48097, + 48399, 48702, 49006, 49311, + 49616, 49923, 50231, 50539, + 50849, 51160, 51471, 51783, + 52097, 52411, 52726, 53043, + 53360, 53678, 53997, 54317, + 54638, 54960, 55282, 55606, + 55931, 56257, 56583, 56911, + 57239, 57569, 57899, 58230, + 58563, 58896, 59230, 59565, + 59901, 60238, 60576, 60915, + 61255, 61596, 61937, 62280, + 62624, 62968, 63314, 63660, + 64008, 64356, 64705, 65055, + 65407, 65423, 65248, 65074, + 64901, 64729, 64558, 64387, + 64218, 64050, 63882, 63716, + 63551, 63386, 63222, 63060, + 62898, 62737, 62578, 62419, + 62261, 62104, 61948, 61793, + 61639, 61485, 61333, 61182, + 61032, 60882, 60734, 60586, + 60440, 60294, 60149, 60006, + 59863, 59721, 59580, 59440, + 59301, 59163, 59026, 58890, + 58755, 58620, 58487, 58355, + 58223, 58093, 57963, 57835, + 57707, 57580, 57454, 57330, + 57206, 57083, 56961, 56840, + 56720, 56600, 56482, 56365, + 56249, 56133, 56019, 55905, + 55793, 55681, 55571, 55461, + 55352, 55244, 55138, 55032, + 54927, 54823, 54720, 54617, + 54516, 54416, 54317, 54218, + 54121, 54024, 53929, 53834, + 53741, 53648, 53556, 53466, + 53376, 53287, 53199, 53112, + 53026, 52941, 52856, 52773, + 52691, 52610, 52529, 52450, + 52371, 52294, 52217, 52141, + 52067, 51993, 51920, 51848, + 51777, 51707, 51638, 51570, + 51503, 51437, 51371, 51307, + 51244, 51181, 51120, 51059, + 51000, 50941, 50883, 50827, + 50771, 50716, 50662, 50609, + 50557, 50506, 50456, 50407, + 50358, 50311, 50265, 50219, + 50175, 50131, 50089, 50047, + 50006, 49967, 49928, 49890, + 49853, 49817, 49782, 49748, + 49715, 49683, 49651, 49621, + 49592, 49563, 49536, 49509, + 49484, 49459, 49436, 49413, + 49391, 49370, 49350, 49331, + 49313, 49296, 49280, 49265, + 49251, 49237, 49225, 49214, + 49203, 49194, 49185, 49178, + 49171, 49165, 49161, 49157, + 49154, 49152, 49151, 49151, + 49152, 49154, 49156, 49160, + 49165, 49170, 49177, 49185, + 49193, 49202, 49213, 49224, + 49236, 49250, 49264, 49279, + 49295, 49312, 49330, 49349, + 49368, 49389, 49411, 49433, + 49457, 49482, 49507, 49533, + 49561, 49589, 49618, 49649, + 49680, 49712, 49745, 49779, + 49814, 49850, 49887, 49924, + 49963, 50003, 50043, 50085, + 50127, 50171, 50215, 50260, + 50307, 50354, 50402, 50451, + 50501, 50552, 50604, 50657, + 50711, 50766, 50821, 50878, + 50936, 50994, 51054, 51114, + 51176, 51238, 51301, 51366, + 51431, 51497, 51564, 51632, + 51701, 51771, 51842, 51914, + 51986, 52060, 52135, 52210, + 52287, 52364, 52443, 52522, + 52602, 52684, 52766, 52849, + 52933, 53018, 53104, 53191, + 53279, 53368, 53457, 53548, + 53640, 53732, 53826, 53920, + 54016, 54112, 54209, 54308, + 54407, 54507, 54608, 54710, + 54813, 54917, 55022, 55128, + 55235, 55342, 55451, 55561, + 55671, 55783, 55895, 56009, + 56123, 56238, 56354, 56472, + 56590, 56709, 56829, 56950, + 57072, 57194, 57318, 57443, + 57569, 57695, 57823, 57951, + 58081, 58211, 58343, 58475, + 58608, 58742, 58877, 59014, + 59151, 59289, 59427, 59567, + 59708, 59850, 59993, 60136, + 60281, 60426, 60573, 60720, + 60869, 61018, 61168, 61319, + 61472, 61625, 61779, 61934, + 62090, 62246, 62404, 62563, + 62723, 62883, 63045, 63208, + 63371, 63536, 63701, 63867, + 64035, 64203, 64372, 64542, + 64713, 64885, 65058, 65232, + 65407, 65511, 65423, 65337, + 65252, 65167, 65084, 65001, + 64920, 64839, 64759, 64680, + 64603, 64526, 64450, 64375, + 64301, 64228, 64156, 64084, + 64014, 63945, 63876, 63809, + 63743, 63677, 63612, 63549, + 63486, 63424, 63364, 63304, + 63245, 63187, 63130, 63074, + 63019, 62964, 62911, 62859, + 62808, 62757, 62708, 62659, + 62612, 62565, 62519, 62475, + 62431, 62388, 62346, 62305, + 62265, 62226, 62188, 62151, + 62115, 62079, 62045, 62012, + 61979, 61948, 61917, 61887, + 61859, 61831, 61804, 61778, + 61754, 61730, 61707, 61685, + 61664, 61643, 61624, 61606, + 61589, 61572, 61557, 61542, + 61529, 61516, 61505, 61494, + 61484, 61475, 61467, 61461, + 61455, 61450, 61445, 61442, + 61440, 61439, 61439, 61439, + 61441, 61443, 61447, 61451, + 61457, 61463, 61470, 61478, + 61488, 61498, 61509, 61521, + 61534, 61548, 61562, 61578, + 61595, 61612, 61631, 61651, + 61671, 61693, 61715, 61738, + 61763, 61788, 61814, 61841, + 61869, 61898, 61928, 61959, + 61991, 62024, 62057, 62092, + 62128, 62164, 62202, 62240, + 62280, 62320, 62361, 62403, + 62447, 62491, 62536, 62582, + 62629, 62677, 62726, 62775, + 62826, 62878, 62930, 62984, + 63039, 63094, 63150, 63208, + 63266, 63325, 63386, 63447, + 63509, 63572, 63636, 63701, + 63767, 63833, 63901, 63970, + 64040, 64110, 64182, 64254, + 64328, 64402, 64477, 64554, + 64631, 64709, 64788, 64868, + 64949, 65031, 65114, 65198, + 65283, 65368, 65455, 65531, + 65487, 65445, 65403, 65362, + 65323, 65284, 65246, 65209, + 65174, 65139, 65105, 65072, + 65040, 65008, 64978, 64949, + 64921, 64893, 64867, 64841, + 64817, 64793, 64771, 64749, + 64728, 64708, 64689, 64671, + 64655, 64638, 64623, 64609, + 64596, 64584, 64573, 64562, + 64553, 64544, 64537, 64530, + 64525, 64520, 64516, 64513, + 64512, 64511, 64511, 64512, + 64514, 64516, 64520, 64525, + 64531, 64537, 64545, 64554, + 64563, 64574, 64585, 64597, + 64611, 64625, 64640, 64656, + 64673, 64691, 64710, 64730, + 64751, 64773, 64795, 64819, + 64844, 64869, 64896, 64923, + 64952, 64981, 65011, 65042, + 65075, 65108, 65142, 65177, + 65213, 65250, 65288, 65326, + 65366, 65407, 65448, 65491, + 65535, +}; +const uint16_t lut_response_balance[] = { + 0, 8, 23, 42, + 64, 90, 118, 149, + 182, 217, 254, 294, + 335, 377, 422, 467, + 515, 564, 615, 666, + 720, 774, 830, 888, + 946, 1006, 1067, 1129, + 1192, 1257, 1322, 1389, + 1457, 1525, 1595, 1666, + 1738, 1811, 1885, 1960, + 2036, 2113, 2190, 2269, + 2349, 2429, 2511, 2593, + 2676, 2760, 2845, 2931, + 3017, 3105, 3193, 3282, + 3372, 3463, 3554, 3647, + 3740, 3834, 3928, 4024, + 4120, 4217, 4315, 4413, + 4512, 4612, 4713, 4814, + 4916, 5019, 5122, 5227, + 5331, 5437, 5543, 5650, + 5758, 5866, 5975, 6085, + 6195, 6306, 6418, 6530, + 6643, 6756, 6871, 6985, + 7101, 7217, 7334, 7451, + 7569, 7687, 7807, 7926, + 8047, 8168, 8289, 8412, + 8534, 8658, 8782, 8906, + 9032, 9157, 9284, 9410, + 9538, 9666, 9795, 9924, + 10053, 10184, 10315, 10446, + 10578, 10710, 10843, 10977, + 11111, 11246, 11381, 11517, + 11653, 11790, 11927, 12065, + 12204, 12343, 12482, 12622, + 12762, 12903, 13045, 13187, + 13330, 13473, 13616, 13760, + 13905, 14050, 14196, 14342, + 14488, 14635, 14783, 14931, + 15080, 15229, 15378, 15528, + 15679, 15830, 15981, 16133, + 16286, 16439, 16592, 16746, + 16900, 17055, 17210, 17366, + 17522, 17679, 17836, 17994, + 18152, 18310, 18469, 18629, + 18789, 18949, 19110, 19271, + 19433, 19595, 19758, 19921, + 20084, 20248, 20412, 20577, + 20743, 20908, 21074, 21241, + 21408, 21576, 21743, 21912, + 22081, 22250, 22419, 22589, + 22760, 22931, 23102, 23274, + 23446, 23619, 23792, 23965, + 24139, 24313, 24488, 24663, + 24839, 25015, 25191, 25368, + 25545, 25723, 25901, 26079, + 26258, 26437, 26617, 26797, + 26977, 27158, 27339, 27521, + 27703, 27886, 28068, 28252, + 28435, 28619, 28804, 28989, + 29174, 29360, 29546, 29732, + 29919, 30106, 30294, 30481, + 30670, 30859, 31048, 31237, + 31427, 31617, 31808, 31999, + 32190, 32382, 32574, 32767, +}; +const uint16_t lut_vca_linear[] = { + 63130, 50916, 46455, 43664, + 41628, 40024, 38701, 37575, + 36594, 35726, 34946, 34240, + 33594, 32998, 32446, 31931, + 31449, 30996, 30568, 30163, + 29779, 29413, 29064, 28730, + 28411, 28104, 27809, 27525, + 27252, 26988, 26733, 26486, + 26247, 26015, 25790, 25572, + 25359, 25153, 24952, 24756, + 24565, 24379, 24197, 24019, + 23846, 23676, 23510, 23348, + 23189, 23033, 22881, 22731, + 22585, 22441, 22299, 22161, + 22025, 21891, 21760, 21630, + 21503, 21378, 21255, 21135, + 21015, 20898, 20783, 20669, + 20557, 20447, 20338, 20231, + 20125, 20020, 19918, 19816, + 19716, 19617, 19519, 19423, + 19328, 19234, 19141, 19049, + 18959, 18869, 18781, 18693, + 18607, 18521, 18436, 18353, + 18270, 18188, 18107, 18027, + 17948, 17869, 17792, 17715, + 17639, 17564, 17489, 17415, + 17342, 17269, 17198, 17127, + 17056, 16986, 16917, 16849, + 16781, 16713, 16647, 16581, + 16515, 16450, 16386, 16322, + 16258, 16195, 16133, 16071, + 16010, 15949, 15889, 15829, + 15769, 15710, 15652, 15594, + 15536, 15479, 15422, 15366, + 15310, 15255, 15200, 15145, + 15091, 15037, 14983, 14930, + 14877, 14825, 14773, 14721, + 14670, 14619, 14568, 14517, + 14467, 14418, 14368, 14319, + 14271, 14222, 14174, 14126, + 14079, 14032, 13985, 13938, + 13892, 13846, 13800, 13754, + 13709, 13664, 13619, 13575, + 13531, 13487, 13443, 13400, + 13357, 13314, 13271, 13228, + 13186, 13144, 13102, 13061, + 13020, 12979, 12938, 12897, + 12857, 12816, 12776, 12737, + 12697, 12658, 12618, 12579, + 12541, 12502, 12464, 12426, + 12388, 12350, 12312, 12275, + 12237, 12200, 12164, 12127, + 12090, 12054, 12018, 11982, + 11946, 11910, 11875, 11839, + 11804, 11769, 11734, 11700, + 11665, 11631, 11596, 11562, + 11528, 11495, 11461, 11428, + 11394, 11361, 11328, 11295, + 11262, 11230, 11197, 11165, + 11133, 11101, 11069, 11037, + 11005, 10974, 10942, 10911, + 10880, 10849, 10818, 10787, + 10757, 10726, 10696, 10666, + 10635, 10605, 10576, 10546, + 10516, 10486, 10457, 10428, + 10398, 10369, 10340, 10311, + 10283, 10254, 10225, 10197, + 10169, 10140, 10112, 10084, + 10056, 10029, 10001, 9973, + 9946, 9918, 9891, 9864, + 9837, 9810, 9783, 9756, + 9729, 9702, 9676, 9649, + 9623, 9597, 9570, 9544, + 9518, 9492, 9467, 9441, + 9415, 9390, 9364, 9339, + 9313, 9288, 9263, 9238, + 9213, 9188, 9163, 9138, + 9114, 9089, 9065, 9040, + 9016, 8992, 8967, 8943, + 8919, 8895, 8871, 8848, + 8824, 8800, 8777, 8753, + 8730, 8706, 8683, 8660, + 8637, 8614, 8591, 8568, + 8545, 8522, 8499, 8477, + 8454, 8431, 8409, 8386, + 8364, 8342, 8320, 8298, + 8275, 8253, 8231, 8210, + 8188, 8166, 8144, 8123, + 8101, 8080, 8058, 8037, + 8015, 7994, 7973, 7952, + 7931, 7910, 7889, 7868, + 7847, 7826, 7805, 7785, + 7764, 7743, 7723, 7702, + 7682, 7662, 7641, 7621, + 7601, 7581, 7561, 7541, + 7521, 7501, 7481, 7461, + 7441, 7421, 7402, 7382, + 7363, 7343, 7324, 7304, + 7285, 7265, 7246, 7227, + 7208, 7189, 7170, 7150, + 7132, 7113, 7094, 7075, + 7056, 7037, 7019, 7000, + 6981, 6963, 6944, 6926, + 6907, 6889, 6871, 6852, + 6834, 6816, 6798, 6780, + 6761, 6743, 6725, 6707, + 6690, 6672, 6654, 6636, + 6618, 6601, 6583, 6565, + 6548, 6530, 6513, 6495, + 6478, 6460, 6443, 6426, + 6409, 6391, 6374, 6357, + 6340, 6323, 6306, 6289, + 6272, 6255, 6238, 6221, + 6204, 6188, 6171, 6154, + 6138, 6121, 6104, 6088, + 6071, 6055, 6038, 6022, + 6006, 5989, 5973, 5957, + 5941, 5924, 5908, 5892, + 5876, 5860, 5844, 5828, + 5812, 5796, 5780, 5764, + 5749, 5733, 5717, 5701, + 5686, 5670, 5654, 5639, + 5623, 5608, 5592, 5577, + 5561, 5546, 5530, 5515, + 5500, 5485, 5469, 5454, + 5439, 5424, 5409, 5393, + 5378, 5363, 5348, 5333, + 5318, 5304, 5289, 5274, + 5259, 5244, 5229, 5215, + 5200, 5185, 5171, 5156, + 5141, 5127, 5112, 5098, + 5083, 5069, 5054, 5040, + 5026, 5011, 4997, 4983, + 4968, 4954, 4940, 4926, + 4911, 4897, 4883, 4869, + 4855, 4841, 4827, 4813, + 4799, 4785, 4771, 4757, + 4743, 4730, 4716, 4702, + 4688, 4675, 4661, 4647, + 4634, 4620, 4606, 4593, + 4579, 4566, 4552, 4539, + 4525, 4512, 4498, 4485, + 4472, 4458, 4445, 4432, + 4418, 4405, 4392, 4379, + 4365, 4352, 4339, 4326, + 4313, 4300, 4287, 4274, + 4261, 4248, 4235, 4222, + 4209, 4196, 4183, 4170, + 4158, 4145, 4132, 4119, + 4106, 4094, 4081, 4068, + 4056, 4043, 4030, 4018, + 4005, 3993, 3980, 3968, + 3955, 3943, 3930, 3918, + 3905, 3893, 3881, 3868, + 3856, 3844, 3831, 3819, + 3807, 3795, 3783, 3770, + 3758, 3746, 3734, 3722, + 3710, 3698, 3686, 3674, + 3662, 3650, 3638, 3626, + 3614, 3602, 3590, 3578, + 3566, 3554, 3542, 3531, + 3519, 3507, 3495, 3484, + 3472, 3460, 3448, 3437, + 3425, 3414, 3402, 3390, + 3379, 3367, 3356, 3344, + 3333, 3321, 3310, 3298, + 3287, 3275, 3264, 3253, + 3241, 3230, 3219, 3207, + 3196, 3185, 3173, 3162, + 3151, 3140, 3128, 3117, + 3106, 3095, 3084, 3073, + 3062, 3051, 3039, 3028, + 3017, 3006, 2995, 2984, + 2973, 2962, 2952, 2941, + 2930, 2919, 2908, 2897, + 2886, 2875, 2865, 2854, + 2843, 2832, 2821, 2811, + 2800, 2789, 2779, 2768, + 2757, 2747, 2736, 2725, + 2715, 2704, 2694, 2683, + 2672, 2662, 2651, 2641, + 2630, 2620, 2610, 2599, + 2589, 2578, 2568, 2557, + 2547, 2537, 2526, 2516, + 2506, 2495, 2485, 2475, + 2465, 2454, 2444, 2434, + 2424, 2414, 2403, 2393, + 2383, 2373, 2363, 2353, + 2343, 2332, 2322, 2312, + 2302, 2292, 2282, 2272, + 2262, 2252, 2242, 2232, + 2222, 2213, 2203, 2193, + 2183, 2173, 2163, 2153, + 2143, 2134, 2124, 2114, + 2104, 2094, 2085, 2075, + 2065, 2055, 2046, 2036, + 2026, 2017, 2007, 1997, + 1988, 1978, 1969, 1959, + 1949, 1940, 1930, 1921, + 1911, 1902, 1892, 1883, + 1873, 1864, 1854, 1845, + 1835, 1826, 1816, 1807, + 1798, 1788, 1779, 1769, + 1760, 1751, 1741, 1732, + 1723, 1714, 1704, 1695, + 1686, 1676, 1667, 1658, + 1649, 1640, 1630, 1621, + 1612, 1603, 1594, 1585, + 1575, 1566, 1557, 1548, + 1539, 1530, 1521, 1512, + 1503, 1494, 1485, 1476, + 1467, 1458, 1449, 1440, + 1431, 1422, 1413, 1404, + 1395, 1386, 1377, 1369, + 1360, 1351, 1342, 1333, + 1324, 1316, 1307, 1298, + 1289, 1280, 1272, 1263, + 1254, 1245, 1237, 1228, + 1219, 1211, 1202, 1193, + 1184, 1176, 1167, 1159, + 1150, 1141, 1133, 1124, + 1116, 1107, 1098, 1090, + 1081, 1073, 1064, 1056, + 1047, 1039, 1030, 1022, + 1013, 1005, 996, 988, + 979, 971, 963, 954, + 946, 937, 929, 921, + 912, 904, 896, 887, + 879, 871, 862, 854, + 846, 837, 829, 821, + 813, 804, 796, 788, + 780, 772, 763, 755, + 747, 739, 731, 722, + 714, 706, 698, 690, + 682, 674, 666, 658, + 649, 641, 633, 625, + 617, 609, 601, 593, + 585, 577, 569, 561, + 553, 545, 537, 529, + 521, 513, 506, 498, + 490, 482, 474, 466, + 458, 450, 442, 435, + 427, 419, 411, 403, + 395, 388, 380, 372, + 364, 357, 349, 341, + 333, 326, 318, 310, + 302, 295, 287, 279, + 272, 264, 256, 249, + 241, 233, 226, 218, + 210, 203, 195, 188, + 180, 172, 165, 157, + 150, 142, 135, 127, + 119, 112, 104, 97, + 89, 82, 74, 67, + 60, 52, 45, 37, + 30, 22, 15, 7, + 0, +}; +const uint16_t lut_exponential[] = { + 21, 22, 23, 24, + 24, 25, 26, 27, + 28, 29, 30, 31, + 32, 33, 34, 35, + 36, 37, 38, 39, + 41, 42, 43, 45, + 46, 48, 49, 51, + 52, 54, 56, 58, + 59, 61, 63, 65, + 68, 70, 72, 74, + 77, 79, 82, 84, + 87, 90, 93, 96, + 99, 102, 105, 108, + 112, 115, 119, 123, + 127, 131, 135, 139, + 144, 149, 153, 158, + 163, 168, 174, 179, + 185, 191, 197, 203, + 210, 217, 224, 231, + 238, 246, 254, 262, + 270, 279, 287, 297, + 306, 316, 326, 336, + 347, 358, 370, 381, + 394, 406, 419, 432, + 446, 461, 475, 490, + 506, 522, 539, 556, + 574, 592, 611, 630, + 651, 671, 693, 715, + 738, 761, 785, 810, + 836, 863, 890, 919, + 948, 978, 1010, 1042, + 1075, 1109, 1145, 1181, + 1219, 1258, 1298, 1339, + 1382, 1426, 1471, 1518, + 1567, 1617, 1668, 1721, + 1776, 1833, 1891, 1952, + 2014, 2078, 2144, 2212, + 2283, 2356, 2431, 2508, + 2588, 2671, 2756, 2844, + 2934, 3028, 3125, 3224, + 3327, 3433, 3542, 3655, + 3772, 3892, 4016, 4144, + 4276, 4412, 4553, 4698, + 4848, 5002, 5162, 5326, + 5496, 5671, 5852, 6039, + 6231, 6430, 6635, 6846, + 7064, 7290, 7522, 7762, + 8009, 8264, 8528, 8799, + 9080, 9369, 9668, 9976, + 10294, 10622, 10961, 11310, + 11670, 12042, 12426, 12822, + 13231, 13652, 14088, 14537, + 15000, 15478, 15971, 16480, + 17005, 17547, 18107, 18684, + 19279, 19894, 20528, 21182, + 21857, 22554, 23272, 24014, + 24779, 25569, 26384, 27225, + 28093, 28988, 29912, 30865, + 31849, 32864, 33911, 34992, + 36107, 37258, 38445, 39671, + 40935, 42240, 43586, 44975, + 46408, 47887, 49413, 50988, + 52613, 54290, 56020, 57806, + 59648, 61549, 63510, 65535, +}; + + +const uint16_t* lookup_table_table[] = { + lut_easing_in_quartic, + lut_easing_out_quartic, + lut_easing_in_out_sine, + lut_easing_in_out_bounce, + lut_response_balance, + lut_vca_linear, + lut_exponential, +}; +*/ + +// import numpy +// def phase_lut(octave): +// frequency = 110 * 2 ** (numpy.arange(0, 159.0) / 158.0 - octave) +// phase_increment = frequency / 16667 * (1 << 32) +// return(numpy.round(phase_increment).astype(int)) + +/* +// phase_lut(15) +const uint32_t lut_increments_vslow[] = { + 865, 869, 873, 877, 880, 884, 888, 892, 896, 900, 904, + 908, 912, 916, 920, 924, 928, 932, 936, 940, 944, 949, + 953, 957, 961, 965, 970, 974, 978, 982, 987, 991, 995, + 1000, 1004, 1009, 1013, 1018, 1022, 1026, 1031, 1036, 1040, 1045, + 1049, 1054, 1058, 1063, 1068, 1073, 1077, 1082, 1087, 1091, 1096, + 1101, 1106, 1111, 1116, 1121, 1126, 1130, 1135, 1140, 1145, 1151, + 1156, 1161, 1166, 1171, 1176, 1181, 1186, 1192, 1197, 1202, 1207, + 1213, 1218, 1223, 1229, 1234, 1240, 1245, 1251, 1256, 1262, 1267, + 1273, 1278, 1284, 1290, 1295, 1301, 1307, 1312, 1318, 1324, 1330, + 1336, 1341, 1347, 1353, 1359, 1365, 1371, 1377, 1383, 1389, 1395, + 1402, 1408, 1414, 1420, 1426, 1433, 1439, 1445, 1452, 1458, 1464, + 1471, 1477, 1484, 1490, 1497, 1504, 1510, 1517, 1523, 1530, 1537, + 1544, 1550, 1557, 1564, 1571, 1578, 1585, 1592, 1599, 1606, 1613, + 1620, 1627, 1634, 1641, 1649, 1656, 1663, 1670, 1678, 1685, 1693, + 1700, 1707, 1715, 1723, 1730 +}; + +// phase_lut(14) +const uint32_t lut_increments_slow[] = { + 1730, 1738, 1745, 1753, 1761, 1768, 1776, 1784, 1792, 1800, 1808, + 1816, 1824, 1832, 1840, 1848, 1856, 1864, 1872, 1881, 1889, 1897, + 1905, 1914, 1922, 1931, 1939, 1948, 1956, 1965, 1973, 1982, 1991, + 2000, 2008, 2017, 2026, 2035, 2044, 2053, 2062, 2071, 2080, 2089, + 2098, 2108, 2117, 2126, 2136, 2145, 2154, 2164, 2173, 2183, 2193, + 2202, 2212, 2222, 2231, 2241, 2251, 2261, 2271, 2281, 2291, 2301, + 2311, 2321, 2331, 2342, 2352, 2362, 2373, 2383, 2394, 2404, 2415, + 2425, 2436, 2447, 2458, 2468, 2479, 2490, 2501, 2512, 2523, 2534, + 2545, 2556, 2568, 2579, 2590, 2602, 2613, 2625, 2636, 2648, 2659, + 2671, 2683, 2695, 2707, 2718, 2730, 2742, 2754, 2767, 2779, 2791, + 2803, 2816, 2828, 2840, 2853, 2865, 2878, 2891, 2903, 2916, 2929, + 2942, 2955, 2968, 2981, 2994, 3007, 3020, 3034, 3047, 3060, 3074, + 3087, 3101, 3114, 3128, 3142, 3156, 3170, 3184, 3198, 3212, 3226, + 3240, 3254, 3268, 3283, 3297, 3312, 3326, 3341, 3356, 3370, 3385, + 3400, 3415, 3430, 3445, 3460 +}; +*/ + +// phase_lut(13) +const uint32_t lut_increments_med[] = { + 3460, 3475, 3491, 3506, 3521, 3537, 3553, 3568, 3584, 3600, 3615, + 3631, 3647, 3663, 3679, 3696, 3712, 3728, 3745, 3761, 3778, 3794, + 3811, 3828, 3844, 3861, 3878, 3895, 3912, 3930, 3947, 3964, 3982, + 3999, 4017, 4034, 4052, 4070, 4088, 4106, 4124, 4142, 4160, 4179, + 4197, 4215, 4234, 4253, 4271, 4290, 4309, 4328, 4347, 4366, 4385, + 4404, 4424, 4443, 4463, 4482, 4502, 4522, 4542, 4562, 4582, 4602, + 4622, 4643, 4663, 4683, 4704, 4725, 4746, 4766, 4787, 4808, 4830, + 4851, 4872, 4894, 4915, 4937, 4958, 4980, 5002, 5024, 5046, 5068, + 5091, 5113, 5135, 5158, 5181, 5203, 5226, 5249, 5272, 5296, 5319, + 5342, 5366, 5389, 5413, 5437, 5461, 5485, 5509, 5533, 5557, 5582, + 5606, 5631, 5656, 5681, 5706, 5731, 5756, 5781, 5807, 5832, 5858, + 5884, 5909, 5935, 5962, 5988, 6014, 6040, 6067, 6094, 6121, 6147, + 6174, 6202, 6229, 6256, 6284, 6311, 6339, 6367, 6395, 6423, 6451, + 6480, 6508, 6537, 6566, 6594, 6623, 6653, 6682, 6711, 6741, 6770, + 6800, 6830, 6860, 6890, 6920 +}; + +/* +// phase_lut(12) +const uint32_t lut_increments_fast[] = { + 6920, 6951, 6981, 7012, 7043, 7074, 7105, 7136, 7168, + 7199, 7231, 7263, 7295, 7327, 7359, 7391, 7424, 7456, + 7489, 7522, 7555, 7588, 7622, 7655, 7689, 7723, 7757, + 7791, 7825, 7859, 7894, 7929, 7963, 7998, 8034, 8069, + 8104, 8140, 8176, 8212, 8248, 8284, 8321, 8357, 8394, + 8431, 8468, 8505, 8543, 8580, 8618, 8656, 8694, 8732, + 8770, 8809, 8848, 8887, 8926, 8965, 9004, 9044, 9084, + 9124, 9164, 9204, 9244, 9285, 9326, 9367, 9408, 9449, + 9491, 9533, 9575, 9617, 9659, 9702, 9744, 9787, 9830, + 9873, 9917, 9960, 10004, 10048, 10092, 10137, 10181, 10226, + 10271, 10316, 10361, 10407, 10453, 10499, 10545, 10591, 10638, + 10685, 10732, 10779, 10826, 10874, 10921, 10969, 11018, 11066, + 11115, 11164, 11213, 11262, 11312, 11361, 11411, 11461, 11512, + 11562, 11613, 11664, 11716, 11767, 11819, 11871, 11923, 11975, + 12028, 12081, 12134, 12187, 12241, 12295, 12349, 12403, 12458, + 12513, 12568, 12623, 12678, 12734, 12790, 12846, 12903, 12959, + 13016, 13074, 13131, 13189, 13247, 13305, 13364, 13422, 13481, + 13541, 13600, 13660, 13720, 13780, 13841 +}; + +// phase_lut(11) +const uint32_t lut_increments_vfast[] = { + 13841, 13902, 13963, 14024, 14086, 14148, 14210, 14273, 14335, + 14398, 14462, 14525, 14589, 14653, 14718, 14782, 14847, 14913, + 14978, 15044, 15110, 15177, 15243, 15310, 15378, 15445, 15513, + 15581, 15650, 15719, 15788, 15857, 15927, 15997, 16067, 16138, + 16209, 16280, 16352, 16424, 16496, 16568, 16641, 16714, 16788, + 16862, 16936, 17010, 17085, 17160, 17236, 17311, 17388, 17464, + 17541, 17618, 17695, 17773, 17851, 17930, 18009, 18088, 18167, + 18247, 18327, 18408, 18489, 18570, 18652, 18734, 18816, 18899, + 18982, 19066, 19149, 19234, 19318, 19403, 19488, 19574, 19660, + 19747, 19833, 19921, 20008, 20096, 20184, 20273, 20362, 20452, + 20542, 20632, 20723, 20814, 20905, 20997, 21090, 21182, 21276, + 21369, 21463, 21557, 21652, 21747, 21843, 21939, 22035, 22132, + 22230, 22327, 22426, 22524, 22623, 22723, 22823, 22923, 23024, + 23125, 23227, 23329, 23431, 23534, 23638, 23742, 23846, 23951, + 24056, 24162, 24268, 24375, 24482, 24590, 24698, 24806, 24915, + 25025, 25135, 25246, 25357, 25468, 25580, 25692, 25805, 25919, + 26033, 26147, 26262, 26378, 26494, 26610, 26727, 26845, 26963, + 27081, 27200, 27320, 27440, 27561, 27682 +}; +*/ + +const uint32_t* lookup_table_hr_table[] = { + // lut_increments_vslow, + // lut_increments_slow, + lut_increments_med, + // lut_increments_fast, + // lut_increments_vfast, +}; + +const uint8_t wt_lfo_waveforms[] = { + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 1, 1, + 1, 1, 1, 1, + 1, 1, 1, 1, + 1, 1, 1, 1, + 1, 1, 1, 2, + 2, 2, 2, 2, + 2, 2, 2, 2, + 2, 3, 3, 3, + 3, 3, 3, 3, + 4, 4, 4, 4, + 4, 5, 5, 5, + 5, 5, 6, 6, + 6, 6, 7, 7, + 7, 7, 8, 8, + 8, 9, 9, 9, + 9, 10, 10, 11, + 11, 11, 12, 12, + 13, 13, 13, 14, + 14, 15, 15, 16, + 16, 17, 17, 18, + 18, 19, 19, 20, + 21, 21, 22, 23, + 23, 24, 25, 25, + 26, 27, 27, 28, + 29, 30, 31, 31, + 32, 33, 34, 35, + 36, 37, 38, 39, + 40, 41, 42, 43, + 44, 45, 46, 47, + 48, 49, 50, 52, + 53, 54, 55, 57, + 58, 59, 61, 62, + 63, 65, 66, 68, + 69, 71, 72, 74, + 75, 77, 79, 80, + 82, 84, 85, 87, + 89, 91, 93, 95, + 96, 98, 100, 102, + 104, 107, 109, 111, + 113, 115, 117, 120, + 122, 124, 126, 129, + 131, 134, 136, 139, + 141, 144, 146, 149, + 152, 155, 157, 160, + 163, 166, 169, 172, + 175, 178, 181, 184, + 187, 190, 194, 197, + 200, 203, 207, 210, + 214, 217, 221, 224, + 228, 232, 236, 239, + 243, 247, 251, 255, + 0, 0, 1, 2, + 3, 4, 5, 6, + 7, 8, 9, 10, + 11, 12, 13, 14, + 15, 16, 17, 18, + 19, 20, 21, 22, + 23, 24, 25, 26, + 27, 28, 29, 30, + 31, 32, 33, 34, + 35, 36, 37, 38, + 39, 40, 41, 42, + 43, 44, 45, 46, + 47, 48, 49, 50, + 51, 52, 53, 54, + 55, 56, 57, 58, + 59, 60, 61, 62, + 63, 64, 65, 66, + 67, 68, 69, 70, + 71, 72, 73, 74, + 75, 76, 77, 78, + 79, 80, 81, 82, + 83, 84, 85, 86, + 87, 88, 89, 90, + 91, 92, 93, 94, + 95, 96, 97, 98, + 99, 100, 101, 102, + 103, 104, 105, 106, + 107, 108, 109, 110, + 111, 112, 113, 114, + 115, 116, 117, 118, + 119, 120, 121, 122, + 123, 124, 125, 126, + 127, 128, 129, 130, + 131, 132, 133, 134, + 135, 136, 137, 138, + 139, 140, 141, 142, + 143, 144, 145, 146, + 147, 148, 149, 150, + 151, 152, 153, 154, + 155, 156, 157, 158, + 159, 160, 161, 162, + 163, 164, 165, 166, + 167, 168, 169, 170, + 171, 172, 173, 174, + 175, 176, 177, 178, + 179, 180, 181, 182, + 183, 184, 185, 186, + 187, 188, 189, 190, + 191, 192, 193, 194, + 195, 196, 197, 198, + 199, 200, 201, 202, + 203, 204, 205, 206, + 207, 208, 209, 210, + 211, 212, 213, 214, + 215, 216, 217, 218, + 219, 220, 221, 222, + 223, 224, 225, 226, + 227, 228, 229, 230, + 231, 232, 233, 234, + 235, 236, 237, 238, + 239, 240, 241, 242, + 243, 244, 245, 246, + 247, 248, 249, 250, + 251, 252, 253, 254, + 255, 0, 0, 2, + 4, 6, 8, 10, + 12, 14, 16, 18, + 20, 22, 24, 26, + 28, 30, 32, 34, + 36, 38, 40, 42, + 44, 46, 48, 50, + 52, 54, 56, 58, + 60, 62, 64, 66, + 68, 70, 72, 74, + 76, 78, 80, 82, + 84, 86, 88, 90, + 92, 94, 96, 98, + 100, 102, 104, 106, + 108, 110, 112, 114, + 116, 118, 120, 122, + 124, 126, 128, 129, + 131, 133, 135, 137, + 139, 141, 143, 145, + 147, 149, 151, 153, + 155, 157, 159, 161, + 163, 165, 167, 169, + 171, 173, 175, 177, + 179, 181, 183, 185, + 187, 189, 191, 193, + 195, 197, 199, 201, + 203, 205, 207, 209, + 211, 213, 215, 217, + 219, 221, 223, 225, + 227, 229, 231, 233, + 235, 237, 239, 241, + 243, 245, 247, 249, + 251, 253, 255, 253, + 251, 249, 247, 245, + 243, 241, 239, 237, + 235, 233, 231, 229, + 227, 225, 223, 221, + 219, 217, 215, 213, + 211, 209, 207, 205, + 203, 201, 199, 197, + 195, 193, 191, 189, + 187, 185, 183, 181, + 179, 177, 175, 173, + 171, 169, 167, 165, + 163, 161, 159, 157, + 155, 153, 151, 149, + 147, 145, 143, 141, + 139, 137, 135, 133, + 131, 129, 128, 126, + 124, 122, 120, 118, + 116, 114, 112, 110, + 108, 106, 104, 102, + 100, 98, 96, 94, + 92, 90, 88, 86, + 84, 82, 80, 78, + 76, 74, 72, 70, + 68, 66, 64, 62, + 60, 58, 56, 54, + 52, 50, 48, 46, + 44, 42, 40, 38, + 36, 34, 32, 30, + 28, 26, 24, 22, + 20, 18, 16, 14, + 12, 10, 8, 6, + 4, 2, 0, 255, + 254, 253, 252, 251, + 250, 249, 248, 247, + 246, 245, 244, 243, + 242, 241, 240, 239, + 238, 237, 236, 235, + 234, 233, 232, 231, + 230, 229, 228, 227, + 226, 225, 224, 223, + 222, 221, 220, 219, + 218, 217, 216, 215, + 214, 213, 212, 211, + 210, 209, 208, 207, + 206, 205, 204, 203, + 202, 201, 200, 199, + 198, 197, 196, 195, + 194, 193, 192, 191, + 190, 189, 188, 187, + 186, 185, 184, 183, + 182, 181, 180, 179, + 178, 177, 176, 175, + 174, 173, 172, 171, + 170, 169, 168, 167, + 166, 165, 164, 163, + 162, 161, 160, 159, + 158, 157, 156, 155, + 154, 153, 152, 151, + 150, 149, 148, 147, + 146, 145, 144, 143, + 142, 141, 140, 139, + 138, 137, 136, 135, + 134, 133, 132, 131, + 130, 129, 128, 127, + 126, 125, 124, 123, + 122, 121, 120, 119, + 118, 117, 116, 115, + 114, 113, 112, 111, + 110, 109, 108, 107, + 106, 105, 104, 103, + 102, 101, 100, 99, + 98, 97, 96, 95, + 94, 93, 92, 91, + 90, 89, 88, 87, + 86, 85, 84, 83, + 82, 81, 80, 79, + 78, 77, 76, 75, + 74, 73, 72, 71, + 70, 69, 68, 67, + 66, 65, 64, 63, + 62, 61, 60, 59, + 58, 57, 56, 55, + 54, 53, 52, 51, + 50, 49, 48, 47, + 46, 45, 44, 43, + 42, 41, 40, 39, + 38, 37, 36, 35, + 34, 33, 32, 31, + 30, 29, 28, 27, + 26, 25, 24, 23, + 22, 21, 20, 19, + 18, 17, 16, 15, + 14, 13, 12, 11, + 10, 9, 8, 7, + 6, 5, 4, 3, + 2, 1, 0, 255, + 255, 251, 247, 243, + 239, 236, 232, 228, + 224, 221, 217, 214, + 210, 207, 203, 200, + 197, 194, 190, 187, + 184, 181, 178, 175, + 172, 169, 166, 163, + 160, 157, 155, 152, + 149, 146, 144, 141, + 139, 136, 134, 131, + 129, 126, 124, 122, + 120, 117, 115, 113, + 111, 109, 107, 104, + 102, 100, 98, 96, + 95, 93, 91, 89, + 87, 85, 84, 82, + 80, 79, 77, 75, + 74, 72, 71, 69, + 68, 66, 65, 63, + 62, 61, 59, 58, + 57, 55, 54, 53, + 52, 50, 49, 48, + 47, 46, 45, 44, + 43, 42, 41, 40, + 39, 38, 37, 36, + 35, 34, 33, 32, + 31, 31, 30, 29, + 28, 27, 27, 26, + 25, 25, 24, 23, + 23, 22, 21, 21, + 20, 19, 19, 18, + 18, 17, 17, 16, + 16, 15, 15, 14, + 14, 13, 13, 13, + 12, 12, 11, 11, + 11, 10, 10, 9, + 9, 9, 9, 8, + 8, 8, 7, 7, + 7, 7, 6, 6, + 6, 6, 5, 5, + 5, 5, 5, 4, + 4, 4, 4, 4, + 3, 3, 3, 3, + 3, 3, 3, 2, + 2, 2, 2, 2, + 2, 2, 2, 2, + 2, 1, 1, 1, + 1, 1, 1, 1, + 1, 1, 1, 1, + 1, 1, 1, 1, + 1, 1, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 255, 0, 0, 0, + 0, 0, 0, 1, + 1, 2, 3, 4, + 5, 6, 8, 10, + 12, 14, 17, 20, + 23, 27, 31, 35, + 39, 44, 49, 54, + 59, 65, 71, 77, + 84, 90, 97, 104, + 111, 118, 125, 132, + 139, 147, 154, 161, + 168, 175, 182, 188, + 195, 201, 207, 213, + 218, 223, 228, 233, + 237, 241, 244, 247, + 249, 251, 253, 254, + 255, 255, 255, 254, + 253, 251, 249, 247, + 244, 241, 237, 233, + 228, 223, 218, 213, + 207, 201, 195, 188, + 182, 175, 168, 161, + 154, 147, 139, 132, + 125, 118, 111, 104, + 97, 90, 84, 77, + 71, 65, 59, 54, + 49, 44, 39, 35, + 31, 27, 23, 20, + 17, 14, 12, 10, + 8, 6, 5, 4, + 3, 2, 1, 1, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 255, 128, + 128, 128, 128, 128, + 128, 128, 128, 128, + 129, 129, 130, 131, + 131, 132, 133, 135, + 136, 137, 139, 141, + 143, 145, 147, 149, + 152, 154, 157, 160, + 163, 166, 169, 173, + 176, 179, 183, 186, + 190, 194, 197, 201, + 204, 208, 211, 215, + 218, 222, 225, 228, + 231, 234, 237, 239, + 242, 244, 246, 248, + 249, 251, 252, 253, + 254, 255, 255, 255, + 255, 255, 254, 253, + 252, 251, 249, 248, + 246, 244, 242, 239, + 237, 234, 231, 228, + 225, 222, 218, 215, + 211, 208, 204, 201, + 197, 194, 190, 186, + 183, 179, 176, 173, + 169, 166, 163, 160, + 157, 154, 152, 149, + 147, 145, 143, 141, + 139, 137, 136, 135, + 133, 132, 131, 131, + 130, 129, 129, 128, + 128, 128, 128, 128, + 128, 128, 128, 128, + 127, 127, 127, 127, + 127, 127, 127, 127, + 126, 126, 125, 124, + 124, 123, 122, 120, + 119, 118, 116, 114, + 112, 110, 108, 106, + 103, 101, 98, 95, + 92, 89, 86, 82, + 79, 76, 72, 69, + 65, 61, 58, 54, + 51, 47, 44, 40, + 37, 33, 30, 27, + 24, 21, 18, 16, + 13, 11, 9, 7, + 6, 4, 3, 2, + 1, 0, 0, 0, + 0, 0, 1, 2, + 3, 4, 6, 7, + 9, 11, 13, 16, + 18, 21, 24, 27, + 30, 33, 37, 40, + 44, 47, 51, 54, + 58, 61, 65, 69, + 72, 76, 79, 82, + 86, 89, 92, 95, + 98, 101, 103, 106, + 108, 110, 112, 114, + 116, 118, 119, 120, + 122, 123, 124, 124, + 125, 126, 126, 127, + 127, 127, 127, 127, + 127, 127, 127, 128, + 128, 131, 134, 137, + 140, 143, 146, 149, + 152, 155, 158, 162, + 165, 167, 170, 173, + 176, 179, 182, 185, + 188, 190, 193, 196, + 198, 201, 203, 206, + 208, 211, 213, 215, + 218, 220, 222, 224, + 226, 228, 230, 232, + 234, 235, 237, 238, + 240, 241, 243, 244, + 245, 246, 248, 249, + 250, 250, 251, 252, + 253, 253, 254, 254, + 254, 255, 255, 255, + 255, 255, 255, 255, + 254, 254, 254, 253, + 253, 252, 251, 250, + 250, 249, 248, 246, + 245, 244, 243, 241, + 240, 238, 237, 235, + 234, 232, 230, 228, + 226, 224, 222, 220, + 218, 215, 213, 211, + 208, 206, 203, 201, + 198, 196, 193, 190, + 188, 185, 182, 179, + 176, 173, 170, 167, + 165, 162, 158, 155, + 152, 149, 146, 143, + 140, 137, 134, 131, + 128, 124, 121, 118, + 115, 112, 109, 106, + 103, 100, 97, 93, + 90, 88, 85, 82, + 79, 76, 73, 70, + 67, 65, 62, 59, + 57, 54, 52, 49, + 47, 44, 42, 40, + 37, 35, 33, 31, + 29, 27, 25, 23, + 21, 20, 18, 17, + 15, 14, 12, 11, + 10, 9, 7, 6, + 5, 5, 4, 3, + 2, 2, 1, 1, + 1, 0, 0, 0, + 0, 0, 0, 0, + 1, 1, 1, 2, + 2, 3, 4, 5, + 5, 6, 7, 9, + 10, 11, 12, 14, + 15, 17, 18, 20, + 21, 23, 25, 27, + 29, 31, 33, 35, + 37, 40, 42, 44, + 47, 49, 52, 54, + 57, 59, 62, 65, + 67, 70, 73, 76, + 79, 82, 85, 88, + 90, 93, 97, 100, + 103, 106, 109, 112, + 115, 118, 121, 124, + 128, 128, 133, 138, + 143, 148, 153, 158, + 163, 167, 172, 177, + 182, 186, 191, 195, + 199, 203, 207, 211, + 215, 218, 222, 225, + 228, 231, 234, 237, + 239, 241, 244, 246, + 247, 249, 250, 251, + 253, 253, 254, 255, + 255, 255, 255, 255, + 254, 254, 253, 252, + 251, 250, 249, 247, + 245, 244, 242, 240, + 237, 235, 233, 230, + 228, 225, 222, 220, + 217, 214, 211, 208, + 205, 202, 198, 195, + 192, 189, 186, 183, + 180, 176, 173, 170, + 167, 164, 162, 159, + 156, 153, 151, 148, + 146, 143, 141, 139, + 137, 135, 133, 131, + 130, 128, 127, 125, + 124, 123, 122, 121, + 120, 120, 119, 119, + 118, 118, 118, 118, + 118, 118, 118, 118, + 119, 119, 119, 120, + 121, 121, 122, 123, + 123, 124, 125, 126, + 127, 128, 128, 129, + 130, 131, 132, 132, + 133, 134, 134, 135, + 136, 136, 136, 137, + 137, 137, 137, 137, + 137, 137, 137, 136, + 136, 135, 135, 134, + 133, 132, 131, 130, + 128, 127, 125, 124, + 122, 120, 118, 116, + 114, 112, 109, 107, + 104, 102, 99, 96, + 93, 91, 88, 85, + 82, 79, 75, 72, + 69, 66, 63, 60, + 57, 53, 50, 47, + 44, 41, 38, 35, + 33, 30, 27, 25, + 22, 20, 18, 15, + 13, 11, 10, 8, + 6, 5, 4, 3, + 2, 1, 1, 0, + 0, 0, 0, 0, + 1, 2, 2, 4, + 5, 6, 8, 9, + 11, 14, 16, 18, + 21, 24, 27, 30, + 33, 37, 40, 44, + 48, 52, 56, 60, + 64, 69, 73, 78, + 83, 88, 92, 97, + 102, 107, 112, 117, + 122, 128, 128, 135, + 143, 151, 158, 166, + 173, 180, 187, 194, + 200, 206, 212, 217, + 223, 228, 232, 236, + 240, 243, 246, 249, + 251, 252, 254, 255, + 255, 255, 255, 254, + 253, 251, 250, 248, + 245, 242, 240, 236, + 233, 230, 226, 222, + 218, 214, 210, 206, + 202, 198, 194, 190, + 187, 183, 180, 176, + 173, 171, 168, 166, + 164, 162, 161, 159, + 159, 158, 158, 158, + 159, 159, 161, 162, + 164, 166, 168, 171, + 173, 176, 180, 183, + 187, 190, 194, 198, + 202, 206, 210, 214, + 218, 222, 226, 230, + 233, 236, 240, 242, + 245, 248, 250, 251, + 253, 254, 255, 255, + 255, 255, 254, 252, + 251, 249, 246, 243, + 240, 236, 232, 228, + 223, 217, 212, 206, + 200, 194, 187, 180, + 173, 166, 158, 151, + 143, 135, 128, 120, + 112, 104, 97, 89, + 82, 75, 68, 61, + 55, 49, 43, 38, + 32, 27, 23, 19, + 15, 12, 9, 6, + 4, 3, 1, 0, + 0, 0, 0, 1, + 2, 4, 5, 7, + 10, 13, 15, 19, + 22, 25, 29, 33, + 37, 41, 45, 49, + 53, 57, 61, 65, + 68, 72, 75, 79, + 82, 84, 87, 89, + 91, 93, 94, 96, + 96, 97, 97, 97, + 96, 96, 94, 93, + 91, 89, 87, 84, + 82, 79, 75, 72, + 68, 65, 61, 57, + 53, 49, 45, 41, + 37, 33, 29, 25, + 22, 19, 15, 13, + 10, 7, 5, 4, + 2, 1, 0, 0, + 0, 0, 1, 3, + 4, 6, 9, 12, + 15, 19, 23, 27, + 32, 38, 43, 49, + 55, 61, 68, 75, + 82, 89, 97, 104, + 112, 120, 128, 128, + 136, 144, 152, 160, + 167, 174, 180, 186, + 191, 195, 199, 202, + 204, 205, 205, 205, + 204, 202, 199, 196, + 193, 189, 184, 179, + 175, 170, 165, 160, + 155, 151, 147, 143, + 140, 138, 136, 135, + 135, 135, 136, 138, + 141, 144, 148, 153, + 158, 164, 170, 177, + 183, 190, 197, 204, + 211, 218, 224, 230, + 236, 241, 245, 248, + 251, 253, 255, 255, + 255, 253, 251, 248, + 245, 241, 236, 230, + 224, 218, 211, 204, + 197, 190, 183, 177, + 170, 164, 158, 153, + 148, 144, 141, 138, + 136, 135, 135, 135, + 136, 138, 140, 143, + 147, 151, 155, 160, + 165, 170, 175, 179, + 184, 189, 193, 196, + 199, 202, 204, 205, + 205, 205, 204, 202, + 199, 195, 191, 186, + 180, 174, 167, 160, + 152, 144, 136, 128, + 119, 111, 103, 95, + 88, 81, 75, 69, + 64, 60, 56, 53, + 51, 50, 50, 50, + 51, 53, 56, 59, + 62, 66, 71, 76, + 80, 85, 90, 95, + 100, 104, 108, 112, + 115, 117, 119, 120, + 120, 120, 119, 117, + 114, 111, 107, 102, + 97, 91, 85, 78, + 72, 65, 58, 51, + 44, 37, 31, 25, + 19, 14, 10, 7, + 4, 2, 0, 0, + 0, 2, 4, 7, + 10, 14, 19, 25, + 31, 37, 44, 51, + 58, 65, 72, 78, + 85, 91, 97, 102, + 107, 111, 114, 117, + 119, 120, 120, 120, + 119, 117, 115, 112, + 108, 104, 100, 95, + 90, 85, 80, 76, + 71, 66, 62, 59, + 56, 53, 51, 50, + 50, 50, 51, 53, + 56, 60, 64, 69, + 75, 81, 88, 95, + 103, 111, 119, 128, + 128, 132, 137, 142, + 146, 151, 156, 161, + 165, 170, 175, 179, + 184, 189, 194, 198, + 203, 208, 212, 217, + 222, 227, 231, 236, + 241, 246, 250, 255, + 250, 246, 241, 236, + 231, 227, 222, 217, + 212, 208, 203, 198, + 194, 189, 184, 179, + 175, 170, 165, 161, + 156, 151, 146, 142, + 137, 132, 128, 123, + 118, 113, 109, 104, + 99, 94, 90, 85, + 80, 85, 90, 94, + 99, 104, 109, 113, + 118, 123, 128, 132, + 137, 142, 146, 151, + 156, 161, 165, 170, + 175, 179, 184, 189, + 194, 198, 203, 208, + 212, 217, 222, 227, + 231, 236, 241, 246, + 250, 255, 250, 246, + 241, 236, 231, 227, + 222, 217, 212, 208, + 203, 198, 194, 189, + 184, 179, 175, 170, + 165, 161, 156, 151, + 146, 142, 137, 132, + 128, 123, 118, 113, + 109, 104, 99, 94, + 90, 85, 80, 76, + 71, 66, 61, 57, + 52, 47, 42, 38, + 33, 28, 24, 19, + 14, 9, 5, 0, + 5, 9, 14, 19, + 24, 28, 33, 38, + 42, 47, 52, 57, + 61, 66, 71, 76, + 80, 85, 90, 94, + 99, 104, 109, 113, + 118, 123, 128, 132, + 137, 142, 146, 151, + 156, 161, 165, 170, + 175, 170, 165, 161, + 156, 151, 146, 142, + 137, 132, 128, 123, + 118, 113, 109, 104, + 99, 94, 90, 85, + 80, 76, 71, 66, + 61, 57, 52, 47, + 42, 38, 33, 28, + 24, 19, 14, 9, + 5, 0, 5, 9, + 14, 19, 24, 28, + 33, 38, 42, 47, + 52, 57, 61, 66, + 71, 76, 80, 85, + 90, 94, 99, 104, + 109, 113, 118, 123, + 128, 128, 124, 120, + 116, 112, 108, 104, + 100, 96, 92, 88, + 84, 80, 76, 72, + 68, 64, 60, 56, + 52, 48, 44, 40, + 36, 32, 28, 24, + 20, 16, 12, 8, + 4, 0, 4, 8, + 12, 16, 20, 24, + 28, 32, 36, 40, + 44, 48, 52, 56, + 60, 64, 68, 72, + 76, 80, 84, 88, + 92, 96, 100, 104, + 108, 112, 116, 120, + 124, 128, 131, 135, + 139, 143, 147, 151, + 155, 159, 163, 167, + 171, 175, 179, 183, + 187, 191, 195, 199, + 203, 207, 211, 215, + 219, 223, 227, 231, + 235, 239, 243, 247, + 251, 255, 251, 247, + 243, 239, 235, 231, + 227, 223, 219, 215, + 211, 207, 203, 199, + 195, 191, 187, 183, + 179, 175, 171, 167, + 163, 159, 155, 151, + 147, 143, 139, 135, + 131, 128, 131, 135, + 139, 143, 147, 151, + 155, 159, 163, 167, + 171, 175, 179, 183, + 187, 191, 195, 199, + 203, 207, 211, 215, + 219, 223, 227, 231, + 235, 239, 243, 247, + 251, 255, 251, 247, + 243, 239, 235, 231, + 227, 223, 219, 215, + 211, 207, 203, 199, + 195, 191, 187, 183, + 179, 175, 171, 167, + 163, 159, 155, 151, + 147, 143, 139, 135, + 131, 128, 124, 120, + 116, 112, 108, 104, + 100, 96, 92, 88, + 84, 80, 76, 72, + 68, 64, 60, 56, + 52, 48, 44, 40, + 36, 32, 28, 24, + 20, 16, 12, 8, + 4, 0, 4, 8, + 12, 16, 20, 24, + 28, 32, 36, 40, + 44, 48, 52, 56, + 60, 64, 68, 72, + 76, 80, 84, 88, + 92, 96, 100, 104, + 108, 112, 116, 120, + 124, 128, 63, 60, + 57, 54, 51, 48, + 45, 42, 39, 36, + 33, 30, 27, 24, + 21, 18, 15, 12, + 9, 6, 3, 0, + 1, 4, 7, 10, + 13, 16, 19, 22, + 25, 28, 31, 34, + 37, 40, 43, 46, + 49, 52, 55, 58, + 61, 64, 67, 70, + 73, 76, 79, 82, + 85, 88, 91, 94, + 97, 100, 103, 106, + 109, 112, 115, 118, + 121, 124, 128, 131, + 134, 137, 140, 143, + 146, 149, 152, 155, + 158, 161, 164, 167, + 170, 173, 176, 179, + 182, 185, 188, 191, + 194, 197, 200, 203, + 206, 209, 212, 215, + 218, 221, 224, 227, + 230, 233, 236, 239, + 242, 245, 248, 251, + 254, 255, 252, 249, + 246, 243, 240, 237, + 234, 231, 228, 225, + 222, 219, 216, 213, + 210, 207, 204, 201, + 198, 195, 192, 195, + 198, 201, 204, 207, + 210, 213, 216, 219, + 222, 225, 228, 231, + 234, 237, 240, 243, + 246, 249, 252, 255, + 254, 251, 248, 245, + 242, 239, 236, 233, + 230, 227, 224, 221, + 218, 215, 212, 209, + 206, 203, 200, 197, + 194, 191, 188, 185, + 182, 179, 176, 173, + 170, 167, 164, 161, + 158, 155, 152, 149, + 146, 143, 140, 137, + 134, 131, 128, 124, + 121, 118, 115, 112, + 109, 106, 103, 100, + 97, 94, 91, 88, + 85, 82, 79, 76, + 73, 70, 67, 64, + 61, 58, 55, 52, + 49, 46, 43, 40, + 37, 34, 31, 28, + 25, 22, 19, 16, + 13, 10, 7, 4, + 1, 0, 3, 6, + 9, 12, 15, 18, + 21, 24, 27, 30, + 33, 36, 39, 42, + 45, 48, 51, 54, + 57, 60, 63, 0, + 0, 1, 1, 2, + 2, 2, 3, 3, + 4, 4, 5, 5, + 6, 6, 7, 7, + 8, 8, 9, 9, + 10, 10, 11, 12, + 12, 13, 14, 14, + 15, 16, 16, 17, + 18, 19, 19, 20, + 21, 22, 23, 23, + 24, 25, 26, 27, + 28, 29, 30, 31, + 32, 33, 34, 35, + 37, 38, 39, 40, + 41, 43, 44, 45, + 47, 48, 50, 51, + 52, 54, 56, 57, + 59, 60, 62, 64, + 66, 67, 69, 71, + 73, 75, 77, 79, + 81, 83, 86, 88, + 90, 93, 95, 97, + 100, 102, 105, 108, + 110, 113, 116, 119, + 122, 125, 128, 131, + 135, 138, 141, 145, + 148, 152, 156, 159, + 163, 167, 171, 175, + 180, 184, 188, 193, + 197, 202, 207, 212, + 217, 222, 227, 232, + 238, 243, 249, 255, + 249, 243, 238, 232, + 227, 222, 217, 212, + 207, 202, 197, 193, + 188, 184, 180, 175, + 171, 167, 163, 159, + 156, 152, 148, 145, + 141, 138, 135, 131, + 128, 125, 122, 119, + 116, 113, 110, 108, + 105, 102, 100, 97, + 95, 93, 90, 88, + 86, 83, 81, 79, + 77, 75, 73, 71, + 69, 67, 66, 64, + 62, 60, 59, 57, + 56, 54, 52, 51, + 50, 48, 47, 45, + 44, 43, 41, 40, + 39, 38, 37, 35, + 34, 33, 32, 31, + 30, 29, 28, 27, + 26, 25, 24, 23, + 23, 22, 21, 20, + 19, 19, 18, 17, + 16, 16, 15, 14, + 14, 13, 12, 12, + 11, 10, 10, 9, + 9, 8, 8, 7, + 7, 6, 6, 5, + 5, 4, 4, 3, + 3, 2, 2, 2, + 1, 1, 0, 0, + 205, 134, 30, 163, + 23, 84, 109, 141, + 160, 178, 202, 33, + 87, 51, 180, 8, + 232, 103, 194, 121, + 73, 192, 24, 105, + 72, 99, 222, 20, + 142, 140, 84, 248, + 73, 130, 37, 50, + 215, 1, 200, 213, + 239, 248, 212, 16, + 103, 95, 129, 250, + 209, 48, 178, 174, + 255, 123, 186, 203, + 66, 41, 178, 230, + 234, 79, 244, 185, + 5, 185, 148, 244, + 189, 50, 242, 219, + 114, 210, 255, 144, + 150, 108, 229, 113, + 147, 168, 77, 5, + 214, 81, 96, 46, + 212, 47, 247, 178, + 154, 126, 181, 66, + 166, 159, 213, 91, + 244, 16, 221, 141, + 227, 191, 53, 124, + 151, 222, 204, 244, + 44, 27, 160, 95, + 166, 175, 121, 170, + 243, 46, 179, 114, + 134, 23, 171, 233, + 84, 247, 44, 14, + 15, 199, 144, 34, + 123, 168, 238, 138, + 72, 55, 189, 127, + 42, 245, 91, 102, + 175, 238, 98, 89, + 95, 43, 45, 164, + 249, 18, 33, 209, + 199, 250, 53, 239, + 31, 178, 233, 133, + 102, 197, 210, 243, + 17, 49, 200, 172, + 69, 0, 151, 209, + 117, 168, 132, 244, + 210, 102, 228, 68, + 138, 64, 135, 124, + 250, 131, 77, 56, + 142, 98, 218, 15, + 22, 128, 93, 190, + 26, 245, 174, 10, + 21, 162, 198, 73, + 76, 69, 71, 19, + 138, 1, 189, 215, + 27, 68, 41, 143, + 244, 48, 239, 131, + 66, 198, 87, 82, + 212, 57, 37, 61, + 46, 68, 89, 209, + 80, 214, 9, 148, + 151, 151, 166, 228, + 51, 10, 86, 144, + 186, 209, 212, 28, + 205, 128, 131, 134, + 137, 140, 143, 146, + 149, 152, 155, 158, + 162, 165, 167, 170, + 173, 176, 179, 182, + 185, 188, 190, 193, + 196, 198, 201, 203, + 206, 208, 211, 213, + 215, 218, 220, 222, + 224, 226, 228, 230, + 232, 234, 235, 237, + 238, 240, 241, 243, + 244, 245, 246, 248, + 249, 250, 250, 251, + 252, 253, 253, 254, + 254, 254, 255, 255, + 255, 255, 255, 255, + 255, 254, 254, 254, + 253, 253, 252, 251, + 250, 250, 249, 248, + 246, 245, 244, 243, + 241, 240, 238, 237, + 235, 234, 232, 230, + 228, 226, 224, 222, + 220, 218, 215, 213, + 211, 208, 206, 203, + 201, 198, 196, 193, + 190, 188, 185, 182, + 179, 176, 173, 170, + 167, 165, 162, 158, + 155, 152, 149, 146, + 143, 140, 137, 134, + 131, 128, 124, 121, + 118, 115, 112, 109, + 106, 103, 100, 97, + 93, 90, 88, 85, + 82, 79, 76, 73, + 70, 67, 65, 62, + 59, 57, 54, 52, + 49, 47, 44, 42, + 40, 37, 35, 33, + 31, 29, 27, 25, + 23, 21, 20, 18, + 17, 15, 14, 12, + 11, 10, 9, 7, + 6, 5, 5, 4, + 3, 2, 2, 1, + 1, 1, 0, 0, + 0, 0, 0, 0, + 0, 1, 1, 1, + 2, 2, 3, 4, + 5, 5, 6, 7, + 9, 10, 11, 12, + 14, 15, 17, 18, + 20, 21, 23, 25, + 27, 29, 31, 33, + 35, 37, 40, 42, + 44, 47, 49, 52, + 54, 57, 59, 62, + 65, 67, 70, 73, + 76, 79, 82, 85, + 88, 90, 93, 97, + 100, 103, 106, 109, + 112, 115, 118, 121, + 124, 128, +}; + + +const uint8_t* wt_table[] = { + wt_lfo_waveforms, +}; + + +} // namespace frames diff --git a/software/o_c_REV/frames_resources.h b/software/o_c_REV/frames_resources.h new file mode 100644 index 000000000..5e7c91666 --- /dev/null +++ b/software/o_c_REV/frames_resources.h @@ -0,0 +1,96 @@ +// Copyright 2013 Olivier Gillet. +// +// Author: Olivier Gillet (ol.gillet@gmail.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +// See http://creativecommons.org/licenses/MIT/ for more information. +// +// ----------------------------------------------------------------------------- +// +// Resources definitions. +// +// Automatically generated with: +// make resources + + +#ifndef FRAMES_RESOURCES_H_ +#define FRAMES_RESOURCES_H_ + + +#include + + + +namespace frames { + +typedef uint8_t ResourceId; + +extern const char* string_table[]; + +// extern const uint16_t* lookup_table_table[]; + +extern const uint32_t* lookup_table_hr_table[]; + +extern const uint8_t* wt_table[]; + +// extern const uint16_t lut_easing_in_quartic[]; +// extern const uint16_t lut_easing_out_quartic[]; +// extern const uint16_t lut_easing_in_out_sine[]; +// extern const uint16_t lut_easing_in_out_bounce[]; +// extern const uint16_t lut_response_balance[]; +// extern const uint16_t lut_vca_linear[]; +// extern const uint16_t lut_exponential[]; +// extern const uint32_t lut_increments_vslow[]; +// extern const uint32_t lut_increments_slow[]; +extern const uint32_t lut_increments_med[]; +// extern const uint32_t lut_increments_fast[]; +// extern const uint32_t lut_increments_vfast[]; +extern const uint8_t wt_lfo_waveforms[]; +#define STR_DUMMY 0 // dummy +//#define LUT_EASING_IN_QUARTIC 0 +//#define LUT_EASING_IN_QUARTIC_SIZE 1025 +//#define LUT_EASING_OUT_QUARTIC 1 +//#define LUT_EASING_OUT_QUARTIC_SIZE 1025 +//#define LUT_EASING_IN_OUT_SINE 2 +//#define LUT_EASING_IN_OUT_SINE_SIZE 1025 +//#define LUT_EASING_IN_OUT_BOUNCE 3 +//#define LUT_EASING_IN_OUT_BOUNCE_SIZE 1025 +//#define LUT_RESPONSE_BALANCE 4 +//#define LUT_RESPONSE_BALANCE_SIZE 256 +//#define LUT_VCA_LINEAR 5 +//#define LUT_VCA_LINEAR_SIZE 1025 +//#define LUT_EXPONENTIAL 6 +//#define LUT_EXPONENTIAL_SIZE 256 +// #define LUT_INCREMENTS_VSLOW 0 +// #define LUT_INCREMENTS_VSLOW_SIZE 159 +// #define LUT_INCREMENTS_SLOW 1 +// #define LUT_INCREMENTS_SLOW_SIZE 159 +#define LUT_INCREMENTS_MED 0 // was 2 +#define LUT_INCREMENTS_MED_SIZE 159 +// #define LUT_INCREMENTS_FAST 3 +// #define LUT_INCREMENTS_FAST_SIZE 159 +// #define LUT_INCREMENTS_VFAST 4 +// #define LUT_INCREMENTS_VFAST_SIZE 159 +#define WT_LFO_WAVEFORMS 0 +#define WT_LFO_WAVEFORMS_SIZE 4626 + +} // namespace frames + +#endif // FRAMES_RESOURCES_H_ diff --git a/software/o_c_REV/grids2_resources.h b/software/o_c_REV/grids2_resources.h new file mode 100644 index 000000000..568367e13 --- /dev/null +++ b/software/o_c_REV/grids2_resources.h @@ -0,0 +1,492 @@ +// Copyright 2012 Emilie Gillet. +// +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// This program 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 General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ----------------------------------------------------------------------------- +// +// Resources definitions. +// +// Automatically generated with: +// make resources + +#include + +#ifndef GRIDS2_RESOURCES_H_ +#define GRIDS2_RESOURCES_H_ + +namespace grids { + +const uint8_t node_0[] = { + // KICKS + 255, 10, 170, 10, 90, 10, 40, 10, + 230, 10, 160, 10, 90, 10, 40, 10, + 240, 10, 170, 10, 90, 10, 40, 10, + 230, 10, 160, 10, 90, 10, 180, 10, + // SNARES + 40, 10, 40, 10, 40, 10, 40, 10, + 255, 10, 140, 10, 40, 10, 40, 10, + 40, 10, 40, 10, 40, 10, 40, 10, + 255, 10, 140, 10, 40, 40, 180, 10, + // HI-HATS + 90, 20, 60, 20, 250, 20, 140, 20, + 90, 20, 60, 20, 250, 20, 140, 20, + 90, 20, 60, 20, 250, 20, 140, 20, + 90, 20, 60, 20, 250, 20, 140, 20, +}; +// GET CONNECTED +const uint8_t node_1[] = { + // KICK + 200, 0, 0, 0, 0, 0, 0, 0, + 160, 0, 0, 0, 0, 0, 0, 0, + 200, 0, 0, 0, 0, 0, 160, 0, + 160, 0, 160, 0, 0, 0, 160, 0, + // SNARE + 60, 0, 40, 0, 60, 0, 40, 0, + 200, 0, 40, 0, 60, 0, 40, 0, + 60, 0, 40, 0, 60, 0, 40, 0, + 200, 0, 40, 0, 60, 0, 40, 0, + // HIHAT + 0, 0, 90, 0, 190, 0, 0, 0, + 0, 0, 90, 0, 190, 0, 0, 0, + 0, 0, 90, 0, 190, 0, 0, 0, + 0, 0, 90, 0, 190, 0, 0, 0, +}; +// EMPTY: +/* + // KICK + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + // SNARE + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + // HIHAT + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, +*/ +// SHE'S JUST TOO MUCH +const uint8_t node_2[] = { + // KICK + 200, 0, 0, 100, 200, 0, 100, 0, + 0 , 0, 0, 0, 0, 0, 0, 0, + 200, 0, 0, 150, 150, 0, 200, 0, + 0, 0, 0, 60, 60, 0, 60, 0, + // SNARE + 0, 0, 0, 0, 0, 0, 0, 0, + 150, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 150, 0, 0, 0, 0, 0, 0, 0, + // HIHAT + 0, 0, 200, 0, 0, 0, 170, 0, + 150, 160, 150, 160, 150, 0, 150, 0, + 0, 0, 150, 150, 150, 0, 0, 0, + 0, 0, 150, 150, 150, 0, 150, 0, +}; +// COMPUTER SCREEN +const uint8_t node_3[] = { + // KICK + 200, 0, 0, 0, 0, 0, 0, 0, + 200, 0, 0, 0, 0, 0, 0, 0, + 200, 0, 0, 0, 0, 0, 0, 0, + 200, 0, 0, 0, 0, 0, 0, 0, + // SNARE + 100, 0, 0, 0, 80, 0, 0, 0, + 200, 0, 0, 0, 80, 0, 0, 0, + 100, 0, 0, 0, 80, 0, 0, 0, + 200, 0, 0, 0, 80, 0, 0, 0, + // HIHAT + 120, 0, 120, 0, 180, 0, 120, 0, + 0, 0, 0, 0, 180, 0, 0, 0, + 0, 0, 0, 0, 180, 0, 0, 0, + 100, 0, 100, 0, 180, 0, 100, 0, +}; +const uint8_t node_4[] = { + // KICK + 240, 0, 100, 0, 0, 0, 0, 40, + 200, 0, 0, 0, 100, 0, 0, 0, + 220, 0, 100, 0, 0, 0, 0, 40, + 200, 0, 0, 0, 80, 0, 0, 0, + // SNARE + 100, 0, 100, 0, 0, 0, 0, 50, + 200, 0, 100, 0, 0, 0, 0, 50, + 100, 0, 100, 0, 0, 0, 0, 150, + 200, 0, 100, 50, 150, 0, 50, 50, + // HIHAT + 0, 0, 0, 100, 200, 0, 0, 0, + 0, 0, 0, 0, 200, 0, 0, 0, + 0, 0, 0, 100, 200, 0, 0, 0, + 0, 0, 0, 0, 200, 0, 0, 0, +}; +// YOU CAN DO WITHOUT - A +const uint8_t node_5[] = { + // KICK + 200, 10, 100, 0, 200, 0, 100, 20, + 50, 20, 200, 0, 100, 0, 0, 100, + 200, 20, 100, 0, 100, 0, 200, 0, + 50, 10, 200, 0, 100, 0, 200, 20, + + // SNARE + 80, 60,0,100,20,20,100,50, + 200,20,0,100,20,20,100,50, + 70,20, 80,100, 200,40, 100,50, + 70,20, 80,100, 200,40, 100,50, + + // HIHAT + 200, 50, 150, 50,180,50,150,50, + 200, 50, 150, 50,180,50,150,50, + 200, 50, 150, 50,180,50,150,50, + 200, 50, 150, 50,180,50,150,50, + +}; +// YOU CAN DO WITHOUT - B +const uint8_t node_6[] = { + // KICK + 200, 10, 50, 40, 160, 10, 50, 100, + 100, 10, 150, 40, 100, 10, 170, 70, + 200, 10, 50, 40, 150, 10, 50, 10, + 200, 10, 150, 40, 100, 10, 170, 80, + // SNARE + 10, 10, 10, 40, 100, 10, 10, 10, + 200, 10, 10, 40, 100, 10, 10, 60, + 10, 10, 10, 40, 100, 10, 10, 10, + 30, 10, 10, 40, 200, 10, 10, 80, + // HIHAT + 120, 80, 120, 80, 180, 80, 180, 80, + 120, 80, 120, 80, 180, 80, 180, 80, + 120, 80, 120, 80, 180, 80, 180, 80, + 120, 80, 120, 80, 150, 80, 180, 80, + +}; +// 22ND FLOOR - A +const uint8_t node_7[] = { + // KICK + 150, 30, 20, 30, 50, 10, 20, 10, + 200, 30, 20, 30, 100, 10, 20, 10, + 150, 30, 20, 30, 50, 10, 20, 10, + 200, 30, 20, 30, 100, 10, 20, 10, + // SNARE + 200, 10, 50, 10, 100, 10, 200, 10, + 100, 10, 50, 10, 200, 10, 100, 10, + 200, 10, 50, 10, 100, 10, 200, 10, + 100, 10, 50, 10, 200, 10, 100, 10, + // HIHAT + 200,40,80,40,180,40,80,40, + 200,40,80,40,180,40,80,40, + 200,40,80,40,180,40,80, 40, + 200,40,80,40,180,40,80, 40, + +}; +// 22ND FLOOR - B +const uint8_t node_8[] = { + // KICK + 250, 0, 0, 0, 100, 0, 50, 0, + 250, 0, 0, 0, 100, 0, 50, 0, + 250, 0, 0, 0, 100, 0, 50, 0, + 250, 0, 0, 0, 100, 0, 195, 0, + // SNARE + 150, 20, 150, 20, 50, 20, 50, 20, + 200, 20, 150, 20, 50, 20, 100, 20, + 150, 20, 150, 20, 50, 20, 50, 20, + 200, 20, 150, 20, 50, 20, 170, 20, + // HIHAT + 10, 10, 180, 10, 200, 10, 180, 10, + 10, 10, 180, 10, 200, 10, 200, 10, + 10, 10, 180, 10, 200, 10, 180, 10, + 10, 10, 180, 10, 200, 10, 200, 10, + +}; +// FIRE ON THE FLOOR - A +// TRIPLETS! +const uint8_t node_9[] = { + // KICK + 200, 0, 60, 0, 60, 0, + 190, 0, 60, 0, 60, 0, + 180, 0, 60, 0, 150, 0, + // 200, 0, 0, 0, 0, 0, + // SNARE + 180, 50, 150, 50, 150, 50, + 180, 50, 150, 50, 150, 50, + 200, 50, 150, 50, 150, 50, + // 150, 0, 150, 0, 150, 0, + // HIHAT + 200, 50, 180, 50, 180, 50, + 50, 50, 180, 50, 180, 50, + 50, 50, 180, 50, 180, 50, + // 0, 0, 180, 0, 180, 0, + +}; +// FIRE ON THE FLOOR - B +const uint8_t node_10[] = { + // KICK + 200, 0, 50, 0, 100, 0, 50, 0, + 200, 0, 50, 0, 100, 0, 50, 0, + 200, 0, 50, 0, 100, 0, 50, 0, + 200, 0, 50, 0, 100, 0, 150, 0, + // SNARE + 0, 0, 0, 0, 0, 0, 0, 0, + 200, 0, 0, 0, 0, 0, 100, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 200, 0, 100, 0, 0, 0, 150, 0, + // HIHAT + 180, 0, 120, 0, 240, 0, 100, 0, + 180, 0, 120, 0, 240, 0, 100, 0, + 180, 0, 120, 0, 240, 0, 100, 0, + 180, 0, 120, 0, 240, 0, 100, 0, + +}; +// IF I HAVE TO - A +const uint8_t node_11[] = { + // KICK + 200, 0, 0, 0, 0, 0, 70, 0, + 130, 0, 0, 0, 0, 0, 100, 0, + 130, 0, 0, 0, 100, 0, 0, 0, + 130, 0, 0, 0, 100, 0, 70, 0, + // SNARE + 250, 120, 180, 120, 180, 120, 180, 120, + 200, 120, 180, 120, 180, 120, 180, 120, + 250, 120, 180, 120, 180, 120, 180, 120, + 200, 120, 180, 120, 180, 120, 180, 120, + // HIHAT + 160, 50, 120, 50, 200, 50, 120, 50, + 160, 50, 120, 50, 200, 50, 120, 50, + 160, 50, 120, 50, 200, 50, 120, 50, + 160, 50, 120, 50, 150, 50, 120, 50, + +}; +// IF I HAVE TO - B +const uint8_t node_12[] = { + // KICK + 250, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 250, 0, + 0, 0, 0, 0, 250, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + // SNARE + 100, 0, 0, 0, 100, 0, 0, 0, + 200, 0, 0, 0, 100, 0, 0, 0, + 100, 0, 0, 0, 100, 0, 0, 0, + 200, 0, 150, 0, 100, 0, 150, 0, + // HIHAT + 50, 20, 150, 150, 150, 20, 150, 20, + 50, 20, 150, 50, 200, 20, 150, 20, + 150, 20, 200, 50, 150, 20, 200, 20, + 50, 20, 100, 100, 100, 20, 100, 20, + +}; +// CHOCOLATE - A +const uint8_t node_13[] = { + // KICK + 200, 0, 0, 0, 0, 0, 180, 0, + 0, 0, 180, 0, 0, 0, 0, 0, + 200, 0, 0, 0, 0, 0, 180, 0, + 0, 0, 180, 0, 0, 0, 0, 0, + // SNARE + 50, 0, 50, 0, 50, 0, 50, 0, + 250, 0, 150, 0, 50, 0, 100, 0, + 50, 0, 100, 0, 100, 0, 50, 0, + 250, 0, 150, 0, 50, 0, 180, 0, + // HIHAT + 200, 50, 180, 50, 250, 50, 180, 50, + 180, 50, 250, 50, 180, 50, 180, 50, + 250, 50, 180, 50, 180, 50, 250, 50, + 180, 50, 180, 50, 250, 50, 180, 50, +}; + +// CHOCOLATE - B +const uint8_t node_14[] = { + // KICK + 250, 0, 50, 10, 250, 0, 50, 0, + 50, 0, 200, 10, 50, 0, 180, 10, + 190, 0, 50, 10, 100, 0, 50, 10, + 50, 0, 100, 10, 50, 0, 100, 10, + // SNARE + 100, 10, 60, 10, 100, 10, 60, 10, + 250, 10, 60, 10, 100, 10, 60, 10, + 100, 10, 60, 10, 100, 10, 60, 10, + 250, 30, 200, 30, 100, 30, 190, 30, + // HIHAT + 50, 20, 100, 20, 250, 20, 200, 20, + 50, 20, 100, 20, 250, 20, 200, 20, + 50, 20, 100, 20, 250, 20, 200, 20, + 50, 20, 100, 20, 250, 20, 200, 20, + +}; +// SHE DOESN'T MIND THING +const uint8_t node_15[] = { + // KICK + 250, 0, 50, 0, 140, 0, 250, 0, + 140, 0, 50, 0, 250, 0, 50, 0, + 250, 0, 50, 0, 200, 0, 50, 0, + 200, 0, 50, 0, 140, 0, 50, 120, + // SNARE + 100, 100, 0, 0, 100, 100, 0, 0, + 250, 100, 0, 130, 130, 100, 130, 0, + 100, 100, 0, 0, 100, 100, 0, 0, + 250, 100, 0, 160, 160, 0, 160, 160, + // HIHAT + 80, 30, 180, 200, 180, 30, 180, 100, + 80, 30, 180, 200, 180, 30, 180, 100, + 80, 30, 180, 200, 180, 30, 180, 100, + 80, 30, 180, 200, 180, 30, 180, 180, + +}; + +// PARTY NEXT DOOR +// SHE DOESN'T MIND +// GARAGE +// DOUBLE SHARP POP +// NOT USED ANY MORE: +const uint8_t node_16[] = { + 255, 0, 0, 0, 0, 0, 95, 0, + 0, 0, 127, 0, 0, 0, 0, 0, + 223, 0, 95, 0, 63, 0, 31, 0, + 191, 0, 0, 0, 159, 0, 0, 0, + 0, 0, 31, 0, 255, 0, 0, 0, + 0, 0, 95, 0, 223, 0, 0, 0, + 0, 0, 63, 0, 191, 0, 0, 0, + 0, 0, 0, 0, 159, 0, 127, 0, + 141, 0, 28, 0, 28, 0, 28, 0, + 113, 0, 8, 0, 8, 0, 8, 0, + 255, 0, 0, 0, 226, 0, 0, 0, + 198, 0, 56, 0, 170, 0, 85, 0, +}; +const uint8_t node_17[] = { + 255, 0, 0, 0, 8, 0, 0, 0, + 182, 0, 0, 0, 72, 0, 0, 0, + 218, 0, 0, 0, 36, 0, 0, 0, + 145, 0, 0, 0, 109, 0, 0, 0, + 0, 0, 51, 25, 76, 25, 25, 0, + 153, 0, 0, 0, 127, 102, 178, 0, + 204, 0, 0, 0, 0, 0, 255, 0, + 0, 0, 102, 0, 229, 0, 76, 0, + 113, 0, 0, 0, 141, 0, 85, 0, + 0, 0, 0, 0, 170, 0, 0, 0, + 56, 28, 255, 0, 0, 0, 0, 0, + 198, 0, 0, 0, 226, 0, 0, 0, +}; +const uint8_t node_18[] = { + 255, 0, 8, 0, 28, 0, 28, 0, + 198, 0, 56, 0, 56, 0, 85, 0, + 255, 0, 85, 0, 113, 0, 113, 0, + 226, 0, 141, 0, 170, 0, 141, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 255, 0, 0, 0, 127, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 63, 0, 0, 0, 191, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 127, 0, + 0, 0, 85, 0, 0, 0, 212, 0, + 0, 0, 212, 0, 42, 0, 170, 0, + 0, 0, 127, 0, 0, 0, 0, 0, +}; +const uint8_t node_19[] = { + 255, 0, 0, 0, 0, 0, 218, 0, + 182, 0, 0, 0, 0, 0, 145, 0, + 145, 0, 36, 0, 0, 0, 109, 0, + 109, 0, 0, 0, 72, 0, 36, 0, + 0, 0, 0, 0, 109, 0, 8, 0, + 72, 0, 0, 0, 255, 0, 182, 0, + 0, 0, 0, 0, 145, 0, 8, 0, + 36, 0, 8, 0, 218, 0, 182, 0, + 255, 0, 0, 0, 0, 0, 226, 0, + 85, 0, 0, 0, 141, 0, 0, 0, + 0, 0, 0, 0, 170, 0, 56, 0, + 198, 0, 0, 0, 113, 0, 28, 0, +}; +const uint8_t node_20[] = { + 255, 0, 0, 0, 113, 0, 0, 0, + 198, 0, 56, 0, 85, 0, 28, 0, + 255, 0, 0, 0, 226, 0, 0, 0, + 170, 0, 0, 0, 141, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 255, 0, 145, 0, 109, 0, 218, 0, + 36, 0, 182, 0, 72, 0, 72, 0, + 255, 0, 0, 0, 0, 0, 109, 0, + 36, 0, 36, 0, 145, 0, 0, 0, + 72, 0, 72, 0, 182, 0, 0, 0, + 72, 0, 72, 0, 218, 0, 0, 0, + 109, 0, 109, 0, 255, 0, 0, 0, +}; +const uint8_t node_21[] = { + 255, 0, 0, 0, 218, 0, 0, 0, + 145, 0, 0, 0, 36, 0, 0, 0, + 218, 0, 0, 0, 36, 0, 0, 0, + 182, 0, 72, 0, 0, 0, 109, 0, + 0, 0, 0, 0, 8, 0, 0, 0, + 255, 0, 85, 0, 212, 0, 42, 0, + 0, 0, 0, 0, 8, 0, 0, 0, + 85, 0, 170, 0, 127, 0, 42, 0, + 109, 0, 109, 0, 255, 0, 0, 0, + 72, 0, 72, 0, 218, 0, 0, 0, + 145, 0, 182, 0, 255, 0, 0, 0, + 36, 0, 36, 0, 218, 0, 8, 0, +}; +const uint8_t node_22[] = { + 255, 0, 0, 0, 42, 0, 0, 0, + 212, 0, 0, 0, 8, 0, 212, 0, + 170, 0, 0, 0, 85, 0, 0, 0, + 212, 0, 8, 0, 127, 0, 8, 0, + 255, 0, 85, 0, 0, 0, 0, 0, + 226, 0, 85, 0, 0, 0, 198, 0, + 0, 0, 141, 0, 56, 0, 0, 0, + 170, 0, 28, 0, 0, 0, 113, 0, + 113, 0, 56, 0, 255, 0, 0, 0, + 85, 0, 56, 0, 226, 0, 0, 0, + 0, 0, 170, 0, 0, 0, 141, 0, + 28, 0, 28, 0, 198, 0, 28, 0, +}; +const uint8_t node_23[] = { + 255, 0, 0, 0, 229, 0, 0, 0, + 204, 0, 204, 0, 0, 0, 76, 0, + 178, 0, 153, 0, 51, 0, 178, 0, + 178, 0, 127, 0, 102, 51, 51, 25, + 0, 0, 0, 0, 0, 0, 0, 31, + 0, 0, 0, 0, 255, 0, 0, 31, + 0, 0, 8, 0, 0, 0, 191, 159, + 127, 95, 95, 0, 223, 0, 63, 0, + 255, 0, 255, 0, 204, 204, 204, 204, + 0, 0, 51, 51, 51, 51, 0, 0, + 204, 0, 204, 0, 153, 153, 153, 153, + 153, 0, 0, 0, 102, 102, 102, 102, +}; +const uint8_t node_24[] = { + 170, 0, 0, 0, 0, 255, 0, 0, + 198, 0, 0, 0, 0, 28, 0, 0, + 141, 0, 0, 0, 0, 226, 0, 0, + 56, 0, 0, 113, 0, 85, 0, 0, + 255, 0, 0, 0, 0, 113, 0, 0, + 85, 0, 0, 0, 0, 226, 0, 0, + 141, 0, 0, 8, 0, 170, 56, 56, + 198, 0, 0, 56, 0, 141, 28, 0, + 255, 0, 0, 0, 0, 191, 0, 0, + 159, 0, 0, 0, 0, 223, 0, 0, + 95, 0, 0, 0, 0, 63, 0, 0, + 127, 0, 0, 0, 0, 31, 0, 0, +}; + + +const uint8_t* drum_map[5][5] = { + { node_0, node_1, node_2, node_3, node_4 }, + { node_5, node_6, node_7, node_8, node_9 }, + { node_10, node_11, node_12, node_13, node_14 }, + { node_15, node_16, node_17, node_18, node_19 }, + { node_20, node_21, node_22, node_23, node_24 }, +}; + +} // namespace grids + +#endif // GRIDS_RESOURCES_H_ diff --git a/software/o_c_REV/hemisphere_config.h b/software/o_c_REV/hemisphere_config.h index 28ccb9312..48a2c3a25 100644 --- a/software/o_c_REV/hemisphere_config.h +++ b/software/o_c_REV/hemisphere_config.h @@ -11,31 +11,75 @@ // * Category filtering is deprecated at 1.8, but I'm leaving the per-applet categorization // alone to avoid breaking forked codebases by other developers. -#define HEMISPHERE_AVAILABLE_APPLETS 58 - ////////////////// id cat class name + + #define HEMISPHERE_APPLETS { \ + DECLARE_APPLET( 34, 0x01, ADEG), \ + DECLARE_APPLET( 47, 0x09, ASR), \ + DECLARE_APPLET( 56, 0x10, AttenuateOffset), \ + DECLARE_APPLET( 41, 0x41, Binary), \ + DECLARE_APPLET( 55, 0x80, BootsNCat), \ + DECLARE_APPLET( 51, 0x80, BugCrack), \ + DECLARE_APPLET( 31, 0x04, Burst), \ + DECLARE_APPLET( 65, 0x10, Button), \ + DECLARE_APPLET( 12, 0x10, Calculate), \ + DECLARE_APPLET( 6, 0x04, ClockDivider), \ + DECLARE_APPLET( 28, 0x04, ClockSkip), \ + DECLARE_APPLET( 30, 0x10, Compare), \ + DECLARE_APPLET( 9, 0x08, DualQuant), \ + DECLARE_APPLET( 18, 0x02, DualTM), \ + DECLARE_APPLET( 7, 0x01, EbbAndLfo), \ + DECLARE_APPLET( 42, 0x11, EnvFollow), \ + DECLARE_APPLET( 15, 0x02, EuclidX), \ + DECLARE_APPLET( 10, 0x44, Logic), \ + DECLARE_APPLET( 50, 0x04, Metronome), \ + DECLARE_APPLET( 73, 0x08, MultiScale), \ + DECLARE_APPLET( 71, 0x02, Pigeons), \ + DECLARE_APPLET( 59, 0x04, ProbabilityDivider), \ + DECLARE_APPLET( 62, 0x04, ProbabilityMelody), \ + DECLARE_APPLET (65, 0x01, Relabi), \ + DECLARE_APPLET( 70, 0x14, ResetClock), \ + DECLARE_APPLET( 69, 0x01, RndWalk), \ + DECLARE_APPLET( 44, 0x01, RunglBook), \ + DECLARE_APPLET( 40, 0x40, Schmitt), \ + DECLARE_APPLET( 23, 0x80, Scope), \ + DECLARE_APPLET( 14, 0x02, SequenceX), \ + DECLARE_APPLET( 48, 0x45, ShiftGate), \ + DECLARE_APPLET( 19, 0x01, Slew), \ + DECLARE_APPLET( 61, 0x01, Stairs), \ + DECLARE_APPLET( 3, 0x10, Switch), \ + DECLARE_APPLET( 13, 0x40, TLNeuron), \ + DECLARE_APPLET( 39, 0x80, Tuner), \ + DECLARE_APPLET( 43, 0x10, Voltage), \ +} +/* + DECLARE_APPLET(127, 0x80, DIAGNOSTIC), \ + DECLARE_APPLET( 8, 0x01, ADSREG), \ DECLARE_APPLET( 34, 0x01, ADEG), \ - DECLARE_APPLET( 15, 0x02, AnnularFusion), \ DECLARE_APPLET( 47, 0x09, ASR), \ DECLARE_APPLET( 56, 0x10, AttenuateOffset), \ DECLARE_APPLET( 41, 0x41, Binary), \ - DECLARE_APPLET( 4, 0x14, Brancher), \ + DECLARE_APPLET( 55, 0x80, BootsNCat), \ DECLARE_APPLET( 51, 0x80, BugCrack), \ DECLARE_APPLET( 31, 0x04, Burst), \ - DECLARE_APPLET( 63, 0x10, Button), \ + DECLARE_APPLET( 65, 0x10, Button), \ DECLARE_APPLET( 12, 0x10, Calculate),\ DECLARE_APPLET( 32, 0x0a, Carpeggio), \ + DECLARE_APPLET( 64, 0x08, Chordinator), \ DECLARE_APPLET( 6, 0x04, ClockDivider), \ DECLARE_APPLET( 28, 0x04, ClockSkip), \ DECLARE_APPLET( 30, 0x10, Compare), \ DECLARE_APPLET( 24, 0x02, CVRecV2), \ - DECLARE_APPLET( 55, 0x80, DrCrusher), \ DECLARE_APPLET( 57, 0x02, DrumMap), \ DECLARE_APPLET( 9, 0x08, DualQuant), \ + DECLARE_APPLET( 18, 0x02, DualTM), \ + DECLARE_APPLET( 7, 0x01, EbbAndLfo), \ DECLARE_APPLET( 45, 0x02, EnigmaJr), \ DECLARE_APPLET( 42, 0x11, EnvFollow), \ + DECLARE_APPLET( 15, 0x02, EuclidX), \ + DECLARE_APPLET( 22, 0x01, GameOfLife), \ DECLARE_APPLET( 29, 0x04, GateDelay), \ DECLARE_APPLET( 17, 0x50, GatedVCA), \ DECLARE_APPLET( 16, 0x80, LoFiPCM), \ @@ -45,19 +89,21 @@ DECLARE_APPLET(150, 0x20, hMIDIIn), \ DECLARE_APPLET( 27, 0x20, hMIDIOut), \ DECLARE_APPLET( 33, 0x10, MixerBal), \ + DECLARE_APPLET( 73, 0x08, MultiScale), \ DECLARE_APPLET( 20, 0x02, Palimpsest), \ + DECLARE_APPLET( 71, 0x02, Pigeons), \ DECLARE_APPLET( 59, 0x04, ProbabilityDivider), \ DECLARE_APPLET( 62, 0x04, ProbabilityMelody), \ + DECLARE_APPLET( 70, 0x14, ResetClock), \ + DECLARE_APPLET( 69, 0x01, RndWalk), \ DECLARE_APPLET( 44, 0x01, RunglBook), \ DECLARE_APPLET( 26, 0x08, ScaleDuet), \ DECLARE_APPLET( 40, 0x40, Schmitt), \ DECLARE_APPLET( 23, 0x80, Scope), \ - DECLARE_APPLET( 14, 0x02, Sequence5), \ + DECLARE_APPLET( 14, 0x02, SequenceX), \ DECLARE_APPLET( 48, 0x45, ShiftGate), \ - DECLARE_APPLET( 18, 0x02, TM), \ DECLARE_APPLET( 58, 0x01, Shredder), \ DECLARE_APPLET( 36, 0x04, Shuffle), \ - DECLARE_APPLET( 7, 0x01, SkewedLFO), \ DECLARE_APPLET( 19, 0x01, Slew), \ DECLARE_APPLET( 46, 0x08, Squanch), \ DECLARE_APPLET( 61, 0x01, Stairs), \ @@ -72,6 +118,5 @@ DECLARE_APPLET( 49, 0x01, VectorLFO), \ DECLARE_APPLET( 53, 0x01, VectorMod), \ DECLARE_APPLET( 54, 0x01, VectorMorph), \ - DECLARE_APPLET( 43, 0x10, Voltage), \ -} -/* DECLARE_APPLET(127, 0x80, DIAGNOSTIC), \ */ + DECLARE_APPLET( 43, 0x10, Voltage); +*/ diff --git a/software/o_c_REV/o_c_REV.ino b/software/o_c_REV/o_c_REV.ino index 2a0db4c45..c1a75cc7c 100644 --- a/software/o_c_REV/o_c_REV.ino +++ b/software/o_c_REV/o_c_REV.ino @@ -36,11 +36,12 @@ #include "OC_menus.h" #include "OC_strings.h" #include "OC_ui.h" -#include "OC_version.h" #include "OC_options.h" +#include "OC_strings.h" #include "src/drivers/display.h" #include "src/drivers/ADC/OC_util_ADC.h" #include "util/util_debugpins.h" +#include "VBiasManager.h" unsigned long LAST_REDRAW_TIME = 0; uint_fast8_t MENU_REDRAW = true; @@ -74,13 +75,8 @@ void FASTRUN CORE_timer_ISR() { OC::DAC::Update(); display::Update(); - // The ADC scan uses async startSingleRead/readSingle and single channel each - // loop, so should be fast enough even at 60us (check ADC::busy_waits() == 0) - // to verify. Effectively, the scan rate is ISR / 4 / ADC::kAdcSmoothing - // 100us: 10kHz / 4 / 4 ~ .6kHz - // 60us: 16.666K / 4 / 4 ~ 1kHz - // kAdcSmoothing == 4 has some (maybe 1-2LSB) jitter but seems "Good Enough". - OC::ADC::Scan(); + // see OC_ADC.h for details; empirically (with current parameters), Scan_DMA() picks up new samples @ 5.55kHz + OC::ADC::Scan_DMA(); // Pin changes are tracked in separate ISRs, so depending on prio it might // need extra precautions. @@ -102,15 +98,29 @@ void FASTRUN CORE_timer_ISR() { void setup() { delay(50); + Serial.begin(9600); +#if defined(__IMXRT1062__) + if (CrashReport) { + while (!Serial && millis() < 3000) ; // wait + Serial.println(CrashReport); + delay(1500); + } + #if defined(ARDUINO_TEENSY41) + OC::Pinout_Detect(); + #endif +#endif +#if defined(__MK20DX256__) NVIC_SET_PRIORITY(IRQ_PORTB, 0); // TR1 = 0 = PTB16 +#endif SPI_init(); SERIAL_PRINTLN("* O&C BOOTING..."); - SERIAL_PRINTLN("* %s", OC_VERSION); + SERIAL_PRINTLN("* %s", OC::Strings::VERSION); OC::DEBUG::Init(); OC::DigitalInputs::Init(); delay(400); OC::ADC::Init(&OC::calibration_data.adc); // Yes, it's using the calibration_data before it's loaded... + OC::ADC::Init_DMA(); OC::DAC::Init(&OC::calibration_data.dac); display::Init(); @@ -146,6 +156,11 @@ void setup() { } OC::ui.set_screensaver_timeout(OC::calibration_data.screensaver_timeout); +#ifdef VOR + VBiasManager *vbias_m = vbias_m->get(); + vbias_m->SetState(VBiasManager::BI); +#endif + // initialize apps OC::apps::Init(reset_settings); } @@ -172,9 +187,15 @@ void FASTRUN loop() { OC_DEBUG_PROFILE_SCOPE(OC::DEBUG::MENU_draw_cycles); OC::apps::current_app->DrawMenu(); ++menu_redraws; + + #ifdef VOR + // JEJ:On app screens, show the bias popup, if necessary + VBiasManager *vbias_m = vbias_m->get(); + vbias_m->DrawPopupPerhaps(); + #endif + } else { - //Blank the screen instead of drawing the screensaver (chysn 9/2/2018) - //OC::apps::current_app->DrawScreensaver(); + OC::apps::current_app->DrawScreensaver(); } MENU_REDRAW = 0; LAST_REDRAW_TIME = millis(); diff --git a/software/o_c_REV/peaks_multistage_envelope.cpp b/software/o_c_REV/peaks_multistage_envelope.cpp index 9bf452c36..ca7d49711 100644 --- a/software/o_c_REV/peaks_multistage_envelope.cpp +++ b/software/o_c_REV/peaks_multistage_envelope.cpp @@ -135,11 +135,7 @@ uint16_t MultistageEnvelope::ProcessSingleSample(uint8_t control) { } else { scaled_value_ = (value_ * amplitude_) >> 16; } - #ifdef BUCHLA_4U - return(static_cast(scaled_value_ << 1)); - #else - return(static_cast(scaled_value_)); - #endif + return(static_cast(scaled_value_)); } uint16_t MultistageEnvelope::RenderPreview( diff --git a/software/o_c_REV/platformio.ini b/software/o_c_REV/platformio.ini index 5cb7180a7..7b0a4631c 100644 --- a/software/o_c_REV/platformio.ini +++ b/software/o_c_REV/platformio.ini @@ -1,14 +1,17 @@ ; PlatformIO Project Configuration File ; -; Build options: build flags, source filter, extra scripting -; Upload options: custom port, speed and extra flags +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags ; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; ; Please visit documentation for the other options and examples -; http://docs.platformio.org/page/projectconf.html +; https://docs.platformio.org/page/projectconf.html [platformio] src_dir = . -default_envs = oc_prod +default_envs = + pewpewpew [env] platform = teensy@4.17.0 @@ -16,15 +19,190 @@ framework = arduino board = teensy31 board_build.f_cpu = 120000000 lib_deps = EEPROM -build_flags = - -DTEENSY_OPT_SMALLEST_CODE - -DUSB_MIDI +build_flags = + -DTEENSY_OPT_SMALLEST_CODE + -DUSB_MIDI + -Iextern +build_src_filter = + +<*> + - +extra_scripts = pre:resources/progname.py +upload_protocol = teensy-cli + +[env:custom] +build_flags = + ${env.build_flags} + ${sysenv.CUSTOM_BUILD_FLAGS} + -DOC_VERSION_EXTRA="\"_custom\"" + +[env:T4] +build_flags = + -DTEENSY_OPT_SMALLEST_CODE + -DUSB_MIDI + -Iextern + -DPEWPEWPEW + -DDRUMMAP_GRIDS2 + -DENABLE_APP_CALIBR8OR + -DENABLE_APP_SCENES + -DENABLE_APP_ENIGMA + -DENABLE_APP_MIDI + -DENABLE_APP_PONG + -DENABLE_APP_PIQUED + -DENABLE_APP_POLYLFO + -DENABLE_APP_H1200 + -DENABLE_APP_BYTEBEATGEN + -DENABLE_APP_NEURAL_NETWORK + -DENABLE_APP_DARKEST_TIMELINE + -DENABLE_APP_LORENZ + -DENABLE_APP_ASR + -DENABLE_APP_QUANTERMAIN + -DENABLE_APP_METAQ + -DENABLE_APP_CHORDS + -DENABLE_APP_PASSENCORE + -DENABLE_APP_SEQUINS + -DENABLE_APP_AUTOMATONNETZ + -DENABLE_APP_BBGEN + +[env:T40] +board = teensy40 +board_build.f_cpu = 600000000 +build_flags = + ${env:T4.build_flags} + -DOC_VERSION_EXTRA="\"_T40\"" + +[env:T41] +board = teensy41 +board_build.f_cpu = 600000000 +build_flags = + ${env:T4.build_flags} + -DOC_VERSION_EXTRA="\"_T41\"" + +[env:pewpewpew] +build_flags = + ${env.build_flags} + -DDRUMMAP_GRIDS2 + -DENABLE_APP_CALIBR8OR + -DENABLE_APP_SCENES + -DENABLE_APP_ENIGMA + -DENABLE_APP_PONG + -DENABLE_APP_PIQUED + -DENABLE_APP_POLYLFO + -DENABLE_APP_PASSENCORE + -DENABLE_APP_H1200 + -DENABLE_APP_BYTEBEATGEN + -DPEWPEWPEW + -DOC_VERSION_EXTRA="\"_phz\"" + +[env:wepwepwep] +build_flags = + ${env:pewpewpew.build_flags} + -DFLIP_180 + +[env:pewpewvor] +build_flags = + ${env:pewpewpew.build_flags} + -DVOR + +[env:wepwepvor] +build_flags = + ${env:pewpewpew.build_flags} + -DVOR + +[env:oc_stock1] +build_flags = + ${env.build_flags} + -DENABLE_APP_PONG + -DENABLE_APP_ASR + -DENABLE_APP_QUANTERMAIN + -DENABLE_APP_PIQUED + -DENABLE_APP_CHORDS + -DENABLE_APP_SEQUINS + -DENABLE_APP_AUTOMATONNETZ + -DENABLE_APP_LORENZ + -DOC_VERSION_EXTRA="\"+stock1\"" + +[env:main] +build_flags = + ${env.build_flags} + -DDRUMMAP_GRIDS2 + -DENABLE_APP_CALIBR8OR + -DENABLE_APP_SCENES + -DENABLE_APP_ENIGMA + -DENABLE_APP_MIDI + -DENABLE_APP_NEURAL_NETWORK + -DENABLE_APP_PONG + -DENABLE_APP_DARKEST_TIMELINE + -DENABLE_APP_PIQUED + -DENABLE_APP_POLYLFO + -DOC_VERSION_EXTRA="\"+main\"" + +[env:oc_stock2] +build_flags = + ${env.build_flags} + -DENABLE_APP_PONG + -DENABLE_APP_ASR + -DENABLE_APP_QUANTERMAIN + -DENABLE_APP_METAQ + -DENABLE_APP_CHORDS + -DENABLE_APP_POLYLFO + -DENABLE_APP_H1200 + -DENABLE_APP_AUTOMATONNETZ + -DENABLE_APP_LORENZ + -DENABLE_APP_BBGEN + -DENABLE_APP_BYTEBEATGEN + -DOC_VERSION_EXTRA="\"+stock2\"" + +[env:main_flipped] +build_flags = + ${env:main.build_flags} + -DFLIP_180 + +[env:oc_stock1_flipped] +build_flags = + ${env:oc_stock1.build_flags} + -DFLIP_180 + +[env:oc_stock2_flipped] +build_flags = + ${env:oc_stock2.build_flags} + -DFLIP_180 + +[env:main_vor] +build_flags = + ${env:main.build_flags} + -DVOR + +[env:oc_stock1_vor] +build_flags = + ${env:oc_stock1.build_flags} + -DVOR + +[env:main_vor_flipped] +build_flags = + ${env:main_flipped.build_flags} + -DVOR + +[env:oc_stock1_vor_flipped] +build_flags = + ${env:oc_stock1_flipped.build_flags} + -DVOR + +[env:oc_stock2_vor] +build_flags = + ${env:oc_stock2.build_flags} + -DVOR -upload_protocol = teensy-gui +[env:oc_stock2_vor_flipped] +build_flags = + ${env:oc_stock2_flipped.build_flags} + -DVOR -[env:oc_prod] -build_flags = ${env.build_flags} +[env:buchla] +build_flags = + ${env:main.build_flags} + -DBUCHLA_SUPPORT + -DBUCHLA_4U + -DOC_VERSION_EXTRA="\" Buchla\"" [env:oc_dev] build_flags = ${env.build_flags} -DOC_DEV -; -DPRINT_DEBUG diff --git a/software/o_c_REV/resources/oc_build_tag.sh b/software/o_c_REV/resources/oc_build_tag.sh new file mode 100755 index 000000000..2cb9deb14 --- /dev/null +++ b/software/o_c_REV/resources/oc_build_tag.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +git_rev="$(git rev-parse --short HEAD)" +git_status="$(git status -s --untracked-files=no)" + +if [ -n "$git_status" ] ; then + suffix="dirty" +fi + +echo "${git_rev}${suffix}" diff --git a/software/o_c_REV/resources/oc_version.sh b/software/o_c_REV/resources/oc_version.sh index b071eaace..3ed7891d2 100755 --- a/software/o_c_REV/resources/oc_version.sh +++ b/software/o_c_REV/resources/oc_version.sh @@ -1,5 +1,5 @@ #!/bin/sh -# Run from source directory, e.g. ./resources/oc_version.sh "1.0.0 $(git rev-parse --short HEAD)" +# Run from source directory, e.g. ./resources/oc_version.sh "1.0.0" if [ -z "$1" ]; then echo "Please specify version string" @@ -7,11 +7,6 @@ if [ -z "$1" ]; then fi cat > OC_version.h < +#include + +int main(int argc, char **argv) { + TidesLiteSample sample; + uint64_t samples = atol(argv[1]); + int16_t pitch = atoi(argv[2]); + uint32_t phase_inc = ComputePhaseIncrement(pitch); + uint16_t slope = atoi(argv[3]); + uint16_t shape = atoi(argv[4]); + int16_t fold = atoi(argv[5]); + + uint32_t phase = 0; + printf("unipolar,bipolar,high,low\n"); + for (uint64_t i = 0; i < samples; i++) { + ProcessSample(slope, shape, fold, phase, sample); + printf("%d,%d,%d,%d\n", sample.unipolar, sample.bipolar, + (sample.flags & FLAG_EOA) ? 65535 : 0, + (sample.flags & FLAG_EOR) ? 65535 : 0); + phase += phase_inc; + } + return 0; +} \ No newline at end of file diff --git a/software/o_c_REV/resources/tideslite.h b/software/o_c_REV/resources/tideslite.h new file mode 100644 index 000000000..99f8bcf30 --- /dev/null +++ b/software/o_c_REV/resources/tideslite.h @@ -0,0 +1,280 @@ +#include + +#define LUT_INCREMENTS_SIZE 97 + +const int16_t kOctave = 12 * 128; +const uint16_t kSlopeBits = 12; + +enum TidesLiteFlagBitMask { FLAG_EOA = 1, FLAG_EOR = 2 }; + +struct TidesLiteSample { + uint16_t unipolar; + int16_t bipolar; + uint8_t flags; +}; + +/* +import numpy as np + +sample_rate = 16666 + +excursion = float(1 << 32) + +a4_midi = 69 +a4_pitch = 440.0 +notes = np.arange(0 * 128.0, 12 * 128.0 + 16, 16) / 128.0 +pitches = a4_pitch * 2 **((notes - a4_midi) / 12) +increments = excursion / sample_rate * pitches + +increments.astype(int) +*/ +const uint32_t lut_increments[] = { + 2106971, 2122239, 2137618, 2153108, 2168710, 2184425, 2200255, 2216199, + 2232258, 2248434, 2264727, 2281138, 2297668, 2314318, 2331089, 2347981, + 2364995, 2382133, 2399395, 2416782, 2434295, 2451935, 2469702, 2487599, + 2505625, 2523782, 2542070, 2560491, 2579046, 2597734, 2616559, 2635519, + 2654617, 2673854, 2693230, 2712746, 2732404, 2752204, 2772147, 2792235, + 2812469, 2832850, 2853377, 2874054, 2894881, 2915858, 2936988, 2958270, + 2979707, 3001300, 3023048, 3044954, 3067019, 3089244, 3111630, 3134178, + 3156890, 3179766, 3202808, 3226017, 3249394, 3272940, 3296657, 3320546, + 3344608, 3368845, 3393257, 3417846, 3442613, 3467560, 3492687, 3517996, + 3543489, 3569167, 3595031, 3621082, 3647321, 3673751, 3700373, 3727187, + 3754196, 3781401, 3808802, 3836402, 3864202, 3892204, 3920409, 3948818, + 3977432, 4006254, 4035285, 4064527, 4093980, 4123647, 4153528, 4183626, + 4213943}; + +/* +import numpy + +WAVESHAPER_SIZE = 256 + +x = numpy.arange(0, WAVESHAPER_SIZE + 1) / (WAVESHAPER_SIZE / 2.0) - 1.0 +x[-1] = x[-2] +sine = numpy.sin(8 * numpy.pi * x) +window = numpy.exp(-x * x * 4) ** 2 +bipolar_fold = sine * window + numpy.arctan(3 * x) * (1 - window) +bipolar_fold /= numpy.abs(bipolar_fold).max() +(numpy.round(32767) * bipolar_fold).astype(int) + +x = numpy.arange(0, WAVESHAPER_SIZE + 1) / float(WAVESHAPER_SIZE) +x[-1] = x[-2] +sine = numpy.sin(8 * numpy.pi * x) +window = numpy.exp(-x * x * 4) ** 2 +unipolar_fold = (0.5 * sine + 2 * x) * window + numpy.arctan(4 * x) * (1 - +window) unipolar_fold /= numpy.abs(unipolar_fold).max() (numpy.round(32767 * +unipolar_fold)).astype(int) +*/ + +const int16_t wav_bipolar_fold[] = { + -32767, -32701, -32634, -32566, -32496, -32425, -32353, -32279, -32204, + -32129, -32053, -31977, -31901, -31825, -31749, -31674, -31600, -31526, + -31453, -31379, -31305, -31229, -31150, -31068, -30980, -30886, -30783, + -30671, -30546, -30409, -30258, -30091, -29910, -29713, -29503, -29281, + -29049, -28812, -28572, -28335, -28106, -27890, -27693, -27520, -27374, + -27261, -27180, -27133, -27117, -27129, -27160, -27203, -27246, -27274, + -27271, -27222, -27108, -26911, -26617, -26212, -25683, -25026, -24239, + -23327, -22300, -21176, -19978, -18738, -17489, -16273, -15131, -14106, + -13240, -12572, -12135, -11953, -12041, -12403, -13028, -13893, -14960, + -16179, -17485, -18805, -20059, -21161, -22027, -22574, -22730, -22433, + -21639, -20321, -18479, -16132, -13326, -10133, -6644, -2973, 752, + 4393, 7806, 10850, 13391, 15310, 16512, 16925, 16512, 15267, + 13223, 10446, 7040, 3136, -1106, -5510, -9885, -14039, -17784, + -20947, -23377, -24951, -25584, -25230, -23886, -21592, -18430, -14523, + -10025, -5117, 0, 5117, 10025, 14523, 18430, 21592, 23886, + 25230, 25584, 24951, 23377, 20947, 17784, 14039, 9885, 5510, + 1106, -3136, -7040, -10446, -13223, -15267, -16512, -16925, -16512, + -15310, -13391, -10850, -7806, -4393, -752, 2973, 6644, 10133, + 13326, 16132, 18479, 20321, 21639, 22433, 22730, 22574, 22027, + 21161, 20059, 18805, 17485, 16179, 14960, 13893, 13028, 12403, + 12041, 11953, 12135, 12572, 13240, 14106, 15131, 16273, 17489, + 18738, 19978, 21176, 22300, 23327, 24239, 25026, 25683, 26212, + 26617, 26911, 27108, 27222, 27271, 27274, 27246, 27203, 27160, + 27129, 27117, 27133, 27180, 27261, 27374, 27520, 27693, 27890, + 28106, 28335, 28572, 28812, 29049, 29281, 29503, 29713, 29910, + 30091, 30258, 30409, 30546, 30671, 30783, 30886, 30980, 31068, + 31150, 31229, 31305, 31379, 31453, 31526, 31600, 31674, 31749, + 31825, 31901, 31977, 32053, 32129, 32204, 32279, 32353, 32425, + 32496, 32566, 32634, 32701, 32701}; + +const int16_t wav_unipolar_fold[] = { + 0, 1405, 2797, 4165, 5496, 6779, 8003, 9157, 10232, 11219, 12110, + 12900, 13581, 14151, 14606, 14944, 15166, 15271, 15261, 15141, 14915, 14588, + 14166, 13659, 13073, 12419, 11706, 10944, 10146, 9321, 8481, 7638, 6803, + 5986, 5198, 4451, 3752, 3111, 2537, 2036, 1616, 1281, 1035, 883, + 827, 868, 1006, 1240, 1568, 1988, 2495, 3085, 3753, 4493, 5297, + 6158, 7069, 8021, 9007, 10017, 11043, 12077, 13110, 14133, 15141, 16124, + 17076, 17990, 18861, 19683, 20453, 21166, 21818, 22408, 22935, 23396, 23793, + 24125, 24394, 24601, 24749, 24841, 24881, 24871, 24816, 24722, 24592, 24431, + 24245, 24039, 23817, 23585, 23347, 23109, 22874, 22648, 22433, 22234, 22054, + 21896, 21762, 21654, 21574, 21523, 21502, 21511, 21551, 21621, 21720, 21847, + 22002, 22182, 22386, 22612, 22858, 23120, 23398, 23688, 23988, 24296, 24608, + 24923, 25238, 25552, 25861, 26164, 26459, 26744, 27019, 27281, 27529, 27764, + 27983, 28187, 28375, 28547, 28703, 28843, 28968, 29079, 29174, 29256, 29326, + 29383, 29430, 29466, 29494, 29514, 29527, 29534, 29536, 29535, 29531, 29525, + 29518, 29511, 29505, 29500, 29497, 29496, 29498, 29503, 29512, 29524, 29541, + 29561, 29586, 29614, 29646, 29682, 29722, 29765, 29811, 29860, 29912, 29966, + 30022, 30080, 30139, 30199, 30260, 30321, 30382, 30444, 30505, 30565, 30624, + 30683, 30740, 30796, 30850, 30903, 30955, 31005, 31053, 31099, 31144, 31187, + 31228, 31268, 31307, 31343, 31379, 31413, 31446, 31478, 31509, 31539, 31568, + 31597, 31624, 31652, 31678, 31705, 31731, 31757, 31782, 31808, 31833, 31858, + 31884, 31909, 31934, 31960, 31985, 32011, 32036, 32062, 32088, 32114, 32140, + 32166, 32192, 32218, 32244, 32270, 32296, 32322, 32348, 32374, 32399, 32425, + 32451, 32476, 32501, 32526, 32551, 32576, 32601, 32625, 32649, 32673, 32697, + 32720, 32744, 32767, 32767}; + +inline int16_t Interpolate1022(const int16_t *table, uint32_t phase) { + int32_t a = table[phase >> 22]; + int32_t b = table[(phase >> 22) + 1]; + return a + ((b - a) * static_cast((phase >> 6) & 0xffff) >> 16); +} + +inline int16_t Interpolate824(const int16_t *table, uint32_t phase) { + int32_t a = table[phase >> 24]; + int32_t b = table[(phase >> 24) + 1]; + return a + ((b - a) * static_cast((phase >> 8) & 0xffff) >> 16); +} + +uint32_t ComputePhaseIncrement(int16_t pitch) { + int16_t num_shifts = 0; + while (pitch < 0) { + pitch += kOctave; + --num_shifts; + } + while (pitch >= kOctave) { + pitch -= kOctave; + ++num_shifts; + } + // Lookup phase increment + uint32_t a = lut_increments[pitch >> 4]; + uint32_t b = lut_increments[(pitch >> 4) + 1]; + uint32_t phase_increment = a + ((b - a) * (pitch & 0xf) >> 4); + // Compensate for downsampling + return num_shifts >= 0 ? phase_increment << num_shifts + : phase_increment >> -num_shifts; +} + +int16_t ComputePitch(uint32_t phase_increment) { + uint32_t first = lut_increments[0]; + uint32_t last = lut_increments[LUT_INCREMENTS_SIZE - 2]; + int16_t pitch = 0; + + if (phase_increment == 0) { + phase_increment = 1; + } + + while (phase_increment > last) { + phase_increment >>= 1; + pitch += kOctave; + } + + while (phase_increment < first) { + phase_increment <<= 1; + pitch -= kOctave; + } + + int i = 0; + int j = LUT_INCREMENTS_SIZE - 1; + while (j - i > 1) { + int k = i + (j - i) / 2; + uint32_t mid = lut_increments[k]; + if (phase_increment < mid) { + j = k; + } else { + i = k; + } + } + pitch += (i << 4); + return pitch; +} + + +const uint32_t max_16 = 0xffff; +const uint32_t max_8 = 0xff; + +uint32_t WarpPhase(uint16_t phase, uint16_t curve) { + int32_t c = (curve - 32767) >> 8; + bool flip = c < 0; + if (flip) + phase = max_16 - phase; + uint32_t a = 128 * c * c; + phase = (max_8 + a / max_8) * phase / + ((max_16 + a / max_8 * phase / max_8) / max_8); + if (flip) + phase = max_16 - phase; + return phase; +} + +uint16_t ShapePhase(uint16_t phase, uint16_t attack_curve, + uint16_t decay_curve) { + return phase < (1UL << 15) + ? WarpPhase(phase << 1, attack_curve) + : WarpPhase((0xffff - phase) << 1, decay_curve); +} + +uint16_t ShapePhase(uint16_t phase, uint16_t shape) { + uint32_t att = 0; + uint32_t dec = 0; + if (shape < 1 * 65536 / 4) { + shape *= 4; + att = 0; + dec = 65535 - shape; + } else if (shape < 2 * 65536 / 4) { + // shape between -24576 and 81 + shape = (shape - 65536 / 4) * 4; + att = shape; + dec = shape; + } else if (shape < 3 * 65536 / 4) { + shape = (shape - 2 * 65536 / 4) * 4; + att = 65535; + dec = 65535 - shape; + } else { + shape = (shape - 3 * 65536 / 4) * 4; + att = 65535 - shape; + dec = shape; + } + return ShapePhase(phase, att, dec); +} + +void ProcessSample(uint16_t slope, uint16_t shape, int16_t fold, uint32_t phase, + TidesLiteSample &sample) { + uint32_t eoa = slope << 16; + // uint32_t skewed_phase = phase; + slope = slope ? slope : 1; + uint32_t decay_factor = (32768 << kSlopeBits) / slope; + uint32_t attack_factor = (32768 << kSlopeBits) / (65536 - slope); + + uint32_t skewed_phase = phase; + if (phase <= eoa) { + skewed_phase = (phase >> kSlopeBits) * decay_factor; + } else { + skewed_phase = ((phase - eoa) >> kSlopeBits) * attack_factor; + skewed_phase += 1L << 31; + } + + sample.unipolar = ShapePhase(skewed_phase >> 16, shape); + sample.bipolar = ShapePhase(skewed_phase >> 15, shape) >> 1; + if (skewed_phase >= (1UL << 31)) { + sample.bipolar = -sample.bipolar; + } + + sample.flags = 0; + if (phase <= eoa) { + sample.flags |= FLAG_EOR; + } else { + sample.flags |= FLAG_EOA; + } + + if (fold > 0) { + int32_t wf_gain = 2048; + wf_gain += fold * (32767 - 1024) >> 14; + int32_t wf_balance = fold; + + int32_t original = sample.unipolar; + int32_t folded = Interpolate824(wav_unipolar_fold, original * wf_gain) << 1; + sample.unipolar = original + ((folded - original) * wf_balance >> 15); + + original = sample.bipolar; + folded = Interpolate824(wav_bipolar_fold, original * wf_gain + (1UL << 31)); + sample.bipolar = original + ((folded - original) * wf_balance >> 15); + } +} diff --git a/software/o_c_REV/src/drivers/ADC/ADC_Module.cpp b/software/o_c_REV/src/drivers/ADC/ADC_Module.cpp index c9246cb51..a09597594 100644 --- a/software/o_c_REV/src/drivers/ADC/ADC_Module.cpp +++ b/software/o_c_REV/src/drivers/ADC/ADC_Module.cpp @@ -27,7 +27,7 @@ * */ - +#if defined(__MK20DX256__) #include "ADC_Module.h" @@ -1304,3 +1304,4 @@ void ADC_Module::stopPDB() { } #endif +#endif // __MK20DX256__ diff --git a/software/o_c_REV/src/drivers/ADC/ADC_Module.h b/software/o_c_REV/src/drivers/ADC/ADC_Module.h index 104473906..2140d6918 100644 --- a/software/o_c_REV/src/drivers/ADC/ADC_Module.h +++ b/software/o_c_REV/src/drivers/ADC/ADC_Module.h @@ -44,6 +44,9 @@ #define ADC_TEENSY_3_4 #elif defined(__MK66FX1M0__) // Teensy 3.5 #define ADC_TEENSY_3_5 +#elif defined(__IMXRT1062__) // Teensy 4.x (not really supported, but don't error) +#define ADC_NUM_ADCS 1 +#define ADC_DIFF_PAIRS 0 #else #error "Board not supported!" #endif @@ -298,6 +301,8 @@ cycles. ADHSC should be used when the ADCLK exceeds the limit for ADHSC = 0. #define ADC_CFG1_HI_SPEED (ADC_CFG1_2MHZ) #define ADC_CFG1_VERY_HIGH_SPEED ADC_CFG1_HI_SPEED +#elif defined(__IMXRT1062__) // Teensy 4.0 or 4.1 +// don't give a compile error #else #error "F_BUS must be 108, 60, 56, 54, 48, 40, 36, 24, 4 or 2 MHz" #endif @@ -592,7 +597,7 @@ class ADC_Module { //! Disable PGA void disablePGA(); - +#if defined(__MK20DX256__) //! Set continuous conversion mode void continuousMode() __attribute__((always_inline)) { setBit(ADC_SC3, ADC_SC3_ADCO_BIT); @@ -651,7 +656,7 @@ class ADC_Module { volatile bool isPGAEnabled() __attribute__((always_inline)) { return getBit(ADC_PGA, ADC_PGA_PGAEN_BIT); } - +#endif //////////////// INFORMATION ABOUT VALID PINS ////////////////// diff --git a/software/o_c_REV/src/drivers/ADC/OC_util_ADC.cpp b/software/o_c_REV/src/drivers/ADC/OC_util_ADC.cpp index 1c5d2a9a2..bec21261f 100644 --- a/software/o_c_REV/src/drivers/ADC/OC_util_ADC.cpp +++ b/software/o_c_REV/src/drivers/ADC/OC_util_ADC.cpp @@ -26,7 +26,7 @@ /* ADC.cpp: Implements the control of one or more ADC modules of Teensy 3.x, LC * */ - +#if defined(__MK20DX256__) #include "OC_util_ADC.h" @@ -1305,4 +1305,5 @@ ADC::Sync_result ADC::readSynchronizedContinuous() {ADC::Sync_result res={0}; re void ADC::stopSynchronizedContinuous() {} #endif +#endif // __MK20DX256__ diff --git a/software/o_c_REV/src/drivers/ADC/RingBuffer.cpp b/software/o_c_REV/src/drivers/ADC/RingBuffer.cpp index 470b1b1f4..6a9fe8e39 100644 --- a/software/o_c_REV/src/drivers/ADC/RingBuffer.cpp +++ b/software/o_c_REV/src/drivers/ADC/RingBuffer.cpp @@ -23,6 +23,7 @@ * SOFTWARE. */ +#if defined(__MK20DX256__) #include "RingBuffer.h" @@ -62,3 +63,4 @@ int RingBuffer::read() { int RingBuffer::increase(int p) { return (p + 1)&(2*b_size-1); } +#endif // __MK20DX256__ diff --git a/software/o_c_REV/src/drivers/ADC/RingBufferDMA.cpp b/software/o_c_REV/src/drivers/ADC/RingBufferDMA.cpp index 367b43165..4b39d0ee9 100644 --- a/software/o_c_REV/src/drivers/ADC/RingBufferDMA.cpp +++ b/software/o_c_REV/src/drivers/ADC/RingBufferDMA.cpp @@ -23,6 +23,7 @@ * SOFTWARE. */ +#if defined(__MK20DX256__) #include "RingBufferDMA.h" // Point static_ringbuffer_dma to an RingBufferDMA object @@ -171,3 +172,4 @@ int16_t RingBufferDMA::read() { uint16_t RingBufferDMA::increase(uint16_t p) { return (p + 1)&(2*b_size-1); } +#endif // __MK20DX256__ diff --git a/software/o_c_REV/src/drivers/FreqMeasure/OC_FreqMeasure.cpp b/software/o_c_REV/src/drivers/FreqMeasure/OC_FreqMeasure.cpp index 7f50a5650..f7f9ee171 100755 --- a/software/o_c_REV/src/drivers/FreqMeasure/OC_FreqMeasure.cpp +++ b/software/o_c_REV/src/drivers/FreqMeasure/OC_FreqMeasure.cpp @@ -26,6 +26,8 @@ #include "OC_FreqMeasure.h" #include "OC_FreqMeasureCapture.h" +#if defined(__MK20DX256__) + #define FREQMEASURE_BUFFER_LEN 12 static volatile uint32_t buffer_value[FREQMEASURE_BUFFER_LEN]; static volatile uint8_t buffer_head; @@ -119,5 +121,36 @@ void FTM_ISR_NAME (void) } } +#elif defined(__IMXRT1062__) +void FreqMeasureClass::begin(void) +{ + // TODO Teensy 4.1 +} + +uint8_t FreqMeasureClass::available(void) +{ + // TODO Teensy 4.1 + return 0; +} + +uint32_t FreqMeasureClass::read(void) +{ + // TODO Teensy 4.1 + return 0; +} + +float FreqMeasureClass::countToFrequency(uint32_t count) +{ + // TODO Teensy 4.1 + return 0.0f; +} + +void FreqMeasureClass::end(void) +{ + // TODO Teensy 4.1 +} + +#endif // __IMXRT1062__ + FreqMeasureClass FreqMeasure; diff --git a/software/o_c_REV/src/drivers/FreqMeasure/OC_FreqMeasureCapture.h b/software/o_c_REV/src/drivers/FreqMeasure/OC_FreqMeasureCapture.h index ef3f233d8..40079031a 100755 --- a/software/o_c_REV/src/drivers/FreqMeasure/OC_FreqMeasureCapture.h +++ b/software/o_c_REV/src/drivers/FreqMeasure/OC_FreqMeasureCapture.h @@ -61,6 +61,9 @@ #elif defined(__AVR_AT90USB162__) #define CAPTURE_USE_TIMER1 // ICP1 is pin 16 +// Teensy 4.0 & 4.1 +#elif defined(__IMXRT1062__) + // Sanguino #elif defined(__AVR_ATmega644P__) || defined(__AVR_ATmega644__) #define CAPTURE_USE_TIMER1 // ICP1 is pin 14 diff --git a/software/o_c_REV/src/drivers/SH1106_128x64_driver.cpp b/software/o_c_REV/src/drivers/SH1106_128x64_driver.cpp index d5e1842cc..f866322a1 100644 --- a/software/o_c_REV/src/drivers/SH1106_128x64_driver.cpp +++ b/software/o_c_REV/src/drivers/SH1106_128x64_driver.cpp @@ -27,16 +27,31 @@ #include "SH1106_128x64_driver.h" #include "../../OC_gpio.h" #include "../../OC_options.h" +#include "../../util/util_debugpins.h" +#if defined(__IMXRT1062__) +#include +#endif +// NOTE: Don't disable DMA unless you absolutely know what you're doing. It will hurt you. +#if defined(__MK20DX256__) #define DMA_PAGE_TRANSFER #ifdef DMA_PAGE_TRANSFER #include static DMAChannel page_dma; +static bool page_dma_active = false; #endif #ifndef SPI_SR_RXCTR #define SPI_SR_RXCTR 0XF0 #endif +// Teensy 4.1 has large SPI FIFO, so FIFO and interrupt is used rather than DMA +#elif defined(__IMXRT1062__) +static void spi_sendpage_isr(); +static int sendpage_state; +static int sendpage_count; +static const uint32_t *sendpage_src; +#endif + static uint8_t SH1106_data_start_seq[] = { // u8g_dev_ssd1306_128x64_data_start 0x10, /* set upper 4 bit of the col adr to 0 */ @@ -84,9 +99,9 @@ static uint8_t SH1106_display_on_seq[] = { /*static*/ void SH1106_128x64_Driver::Init() { - pinMode(OLED_CS, OUTPUT); - pinMode(OLED_RST, OUTPUT); - pinMode(OLED_DC, OUTPUT); + OC::pinMode(OLED_CS, OUTPUT); + OC::pinMode(OLED_RST, OUTPUT); + OC::pinMode(OLED_DC, OUTPUT); //SPI_init(); // u8g_teensy::U8G_COM_MSG_INIT @@ -107,10 +122,13 @@ void SH1106_128x64_Driver::Init() { digitalWriteFast(OLED_CS, OLED_CS_ACTIVE); // U8G_ESC_CS(1), /* enable chip */ + // assumes OC::DAC:Init already initialized SPI SPI_send(SH1106_init_seq, sizeof(SH1106_init_seq)); digitalWriteFast(OLED_CS, OLED_CS_INACTIVE); // U8G_ESC_CS(0), /* disable chip */ + delayMicroseconds(1); +#if defined(__MK20DX256__) #ifdef DMA_PAGE_TRANSFER page_dma.destination((volatile uint8_t&)SPI0_PUSHR); page_dma.transferSize(1); @@ -118,7 +136,17 @@ void SH1106_128x64_Driver::Init() { page_dma.disableOnCompletion(); page_dma.triggerAtHardwareEvent(DMAMUX_SOURCE_SPI0_TX); page_dma.disable(); -#endif +#endif // DMA_PAGE_TRANSFER + +#elif defined(__IMXRT1062__) + LPSPI4_IER = 0; + LPSPI4_SR = 0x3F00; // clear any prior pending interrupt flags + LPSPI4_FCR = LPSPI_FCR_RXWATER(0) | LPSPI_FCR_TXWATER(3); + attachInterruptVector(IRQ_LPSPI4, spi_sendpage_isr); + NVIC_CLEAR_PENDING(IRQ_LPSPI4); + NVIC_SET_PRIORITY(IRQ_LPSPI4, 48); + NVIC_ENABLE_IRQ(IRQ_LPSPI4); +#endif // __IMXRT1062__ Clear(); } @@ -126,14 +154,32 @@ void SH1106_128x64_Driver::Init() { /*static*/ void SH1106_128x64_Driver::Flush() { #ifdef DMA_PAGE_TRANSFER - // Assume DMA transfer has completed, else we're doomed - digitalWriteFast(OLED_CS, OLED_CS_INACTIVE); // U8G_ESC_CS(0) - page_dma.clearComplete(); - page_dma.disable(); - // DmaSpi.h::post_finishCurrentTransfer_impl - SPI0_RSER = 0; - SPI0_SR = 0xFF0F0000; -#endif + // Famous last words: "Assume DMA transfer has completed, else we're doomed" + // Because it turns out there are conditions(*) where the timing is shifted + // such that it hasn't completed here, which causes weird display glitches + // from which there's no recovery. + // + // (*) If app processing in frame N takes too long, the next frame starts + // late; this leaves less time for frame N+1, and in N+2 the display CS line + // would be pulled high too soon. Why this effect is more pronounced with + // gcc >= 5.4.1 is a different mystery. + +#if defined(__MK20DX256__) + if (page_dma_active) { + while (!page_dma.complete()) { } + while (0 != (SPI0_SR & 0x0000f000)); // SPIx_SR TXCTR + while (!(SPI0_SR & SPI_SR_TCF)); + page_dma_active = false; + + digitalWriteFast(OLED_CS, OLED_CS_INACTIVE); // U8G_ESC_CS(0) + page_dma.clearComplete(); + page_dma.disable(); + // DmaSpi.h::post_finishCurrentTransfer_impl + SPI0_RSER = 0; + SPI0_SR = 0xFF0F0000; + } +#endif // __MK20DX256__ +#endif // DMA_PAGE_TRANSFER } static uint8_t empty_page[SH1106_128x64_Driver::kPageSize]; @@ -150,13 +196,16 @@ void SH1106_128x64_Driver::Clear() { for (size_t p = 0; p < kNumPages; ++p) SPI_send(empty_page, kPageSize); digitalWriteFast(OLED_CS, OLED_CS_INACTIVE); // U8G_ESC_CS(0) + delayMicroseconds(1); digitalWriteFast(OLED_DC, LOW); digitalWriteFast(OLED_CS, OLED_CS_ACTIVE); SPI_send(SH1106_display_on_seq, sizeof(SH1106_display_on_seq)); digitalWriteFast(OLED_DC, HIGH); + digitalWriteFast(OLED_CS, OLED_CS_INACTIVE); } +#if defined(__MK20DX256__) /*static*/ void SH1106_128x64_Driver::SendPage(uint_fast8_t index, const uint8_t *data) { SH1106_data_start_seq[2] = 0xb0 | index; @@ -173,12 +222,82 @@ void SH1106_128x64_Driver::SendPage(uint_fast8_t index, const uint8_t *data) { page_dma.sourceBuffer(data, kPageSize); page_dma.enable(); // go -#else + page_dma_active = true; +#else // not DMA_PAGE_TRANSFER SPI_send(data, kPageSize); digitalWriteFast(OLED_CS, OLED_CS_INACTIVE); // U8G_ESC_CS(0) #endif } +#elif defined(__IMXRT1062__) +/*static*/ +void SH1106_128x64_Driver::SendPage(uint_fast8_t index, const uint8_t *data) { + SH1106_data_start_seq[2] = 0xb0 | index; + sendpage_state = 0; + sendpage_src = (const uint32_t *)data; // frame buffer is 32 bit aligned + sendpage_count = kPageSize >> 2; // number of 32 bit words to write into FIFO + // don't clear SPI status flags, already cleared before DAC data was loaded into FIFO + LPSPI4_IER = LPSPI_IER_TCIE; // run spi_sendpage_isr() when DAC data complete +} + +static void spi_sendpage_isr() { + DEBUG_PIN_SCOPE(OC_GPIO_DEBUG_PIN2); + uint32_t status = LPSPI4_SR; + LPSPI4_SR = status; // clear interrupt status flags + if (sendpage_state == 0) { + // begin command phase + digitalWriteFast(OLED_DC, LOW); + digitalWriteFast(OLED_CS, OLED_CS_ACTIVE); + LPSPI4_TCR = (LPSPI4_TCR & 0xF8000000) | LPSPI_TCR_FRAMESZ(23) + | LPSPI_TCR_PCS(3) | LPSPI_TCR_RXMSK; + LPSPI4_TDR = (SH1106_data_start_seq[0] << 16) | (SH1106_data_start_seq[1] << 8) + | SH1106_data_start_seq[2]; + sendpage_state = 1; + LPSPI4_IER = LPSPI_IER_TCIE; // run spi_sendpage_isr() when command complete + return; // FIFO loaded with 3 byte command + } + if (sendpage_state == 1) { + // begin data phase + digitalWriteFast(OLED_DC, HIGH); + LPSPI4_CR |= LPSPI_CR_RRF | LPSPI_CR_RTF; // clear FIFO + LPSPI4_IER = LPSPI_IER_TDIE; // run spi_sendpage_isr() when FIFO wants data + const size_t nbits = SH1106_128x64_Driver::kPageSize * 8; + LPSPI4_TCR = (LPSPI4_TCR & 0xF8000000) | LPSPI_TCR_FRAMESZ(nbits-1) + | LPSPI_TCR_PCS(3) | LPSPI_TCR_RXMSK | LPSPI_TCR_BYSW; + sendpage_state = 2; + } + if (sendpage_state == 2) { + // feed display data into the FIFO + if (!(status & LPSPI_SR_TDF)) return; + const int fifo_space = 16 - (LPSPI4_FSR & 0x1F); + if (fifo_space < sendpage_count) { + // we have more data than the FIFO can hold + LPSPI4_IER = LPSPI_IER_TDIE; // run spi_sendpage_isr() when FIFO wants more data + for (int i=0; i < fifo_space; i++) { + LPSPI4_TDR = *sendpage_src++; + asm volatile ("dsb":::"memory"); + } + sendpage_count -= fifo_space; + } else { + // remaining data fits in FIFO + LPSPI4_IER = LPSPI_IER_TCIE; // run spi_sendpage_isr() when all display data finished + for (int i=0; i < sendpage_count; i++) { + LPSPI4_TDR = *sendpage_src++; + asm volatile ("dsb":::"memory"); + } + sendpage_count = 0; + sendpage_state = 3; + } + return; + } else { + // finished + digitalWriteFast(OLED_CS, OLED_CS_INACTIVE); + LPSPI4_IER = 0; + } +} +#endif // __IMXRT1062__ + +#if defined(__MK20DX256__) void SH1106_128x64_Driver::SPI_send(void *bufr, size_t n) { // adapted from https://github.com/xxxajk/spi4teensy3 @@ -222,6 +341,15 @@ void SH1106_128x64_Driver::SPI_send(void *bufr, size_t n) { } } +#elif defined(__IMXRT1062__) +void SH1106_128x64_Driver::SPI_send(void *bufr, size_t n) { + SPI.beginTransaction(SPISettings(24000000, MSBFIRST, SPI_MODE0)); + LPSPI4_TCR |= LPSPI_TCR_PCS(3); // do not interfere with DAC's CS pin + SPI.transfer(bufr, NULL, n); + SPI.endTransaction(); +} +#endif // __IMXRT1062__ + /*static*/ void SH1106_128x64_Driver::AdjustOffset(uint8_t offset) { SH1106_data_start_seq[1] = offset; // lower 4 bits of col adr diff --git a/software/o_c_REV/src/drivers/display.h b/software/o_c_REV/src/drivers/display.h index 99ab83428..750307104 100644 --- a/software/o_c_REV/src/drivers/display.h +++ b/software/o_c_REV/src/drivers/display.h @@ -59,7 +59,6 @@ extern weegfx::Graphics graphics; #define GRAPHICS_BEGIN_FRAME(wait) \ do { \ - DEBUG_PIN_SCOPE(OC_GPIO_DEBUG_PIN1); \ uint8_t *frame = NULL; \ do { \ if (display::frame_buffer.writeable()) \ diff --git a/software/o_c_REV/src/drivers/weegfx.cpp b/software/o_c_REV/src/drivers/weegfx.cpp index d725aa77b..f62fa0952 100644 --- a/software/o_c_REV/src/drivers/weegfx.cpp +++ b/software/o_c_REV/src/drivers/weegfx.cpp @@ -406,7 +406,7 @@ void Graphics::draw_char(char c, coord_t x, coord_t y) { coord_t w = Graphics::kFixedFontW; coord_t h = Graphics::kFixedFontH; font_glyph data = get_char_glyph(c); - if (c + w > kWidth) w = kWidth - x; + if (x + w > kWidth) w = kWidth - x; if (x < 0) { w += x; data += x; diff --git a/software/o_c_REV/tonnetz/tonnetz.h b/software/o_c_REV/tonnetz/tonnetz.h index a78a3cf0a..37d7e9d1b 100644 --- a/software/o_c_REV/tonnetz/tonnetz.h +++ b/software/o_c_REV/tonnetz/tonnetz.h @@ -41,10 +41,12 @@ namespace tonnetz { '*', 'P', 'L', 'R', 'N', 'S', 'H', '@' }; + static const char *transform_names_str[TRANSFORM_LAST + 1] = { "*", "P", "L", "R", "N", "S", "H", "@" }; + static struct transformation { size_t root_shift; // +1 = root -> third, +2 root -> fifth int offsets[abstract_triad::NOTES]; // root, third, fifth diff --git a/software/o_c_REV/util/EEPROMStorage.h b/software/o_c_REV/util/EEPROMStorage.h index 89ffa2e0e..5dc1d02dc 100644 --- a/software/o_c_REV/util/EEPROMStorage.h +++ b/software/o_c_REV/util/EEPROMStorage.h @@ -25,7 +25,7 @@ /* Define a storage implemenation using teensy EEPROM */ struct EEPROMStorage { - static const size_t LENGTH = 2048; + static const size_t LENGTH = E2END + 1; static void update(size_t addr, const void *data, size_t length) { EEPtr e = addr; diff --git a/software/o_c_REV/util/util_macros.h b/software/o_c_REV/util/util_macros.h index c11d5574b..e4e50ae5f 100644 --- a/software/o_c_REV/util/util_macros.h +++ b/software/o_c_REV/util/util_macros.h @@ -9,8 +9,8 @@ #endif #define DISALLOW_COPY_AND_ASSIGN(TypeName) \ - TypeName(const TypeName&); \ - void operator=(const TypeName&) + TypeName(const TypeName&) = delete; \ + void operator=(const TypeName&) = delete #define CLIP(x) if (x < -32767) x = -32767; if (x > 32767) x = 32767; diff --git a/software/o_c_REV/util/util_math.h b/software/o_c_REV/util/util_math.h index dd86f7191..0fb056712 100644 --- a/software/o_c_REV/util/util_math.h +++ b/software/o_c_REV/util/util_math.h @@ -55,7 +55,7 @@ inline uint32_t USAT16(int32_t value) { static inline uint32_t multiply_u32xu32_rshift24(uint32_t a, uint32_t b) __attribute__((always_inline)); static inline uint32_t multiply_u32xu32_rshift24(uint32_t a, uint32_t b) { - register uint32_t lo, hi; + /*register*/ uint32_t lo, hi; asm volatile("umull %0, %1, %2, %3" : "=r" (lo), "=r" (hi) : "r" (a), "r" (b)); return (lo >> 24) | (hi << 8); } @@ -63,7 +63,7 @@ static inline uint32_t multiply_u32xu32_rshift24(uint32_t a, uint32_t b) static inline uint32_t multiply_u32xu32_rshift(uint32_t a, uint32_t b, uint32_t shift) __attribute__((always_inline)); static inline uint32_t multiply_u32xu32_rshift(uint32_t a, uint32_t b, uint32_t shift) { - register uint32_t lo, hi; + /*register*/ uint32_t lo, hi; asm volatile("umull %0, %1, %2, %3" : "=r" (lo), "=r" (hi) : "r" (a), "r" (b)); return (lo >> shift) | (hi << (32 - shift)); } diff --git a/software/o_c_REV/util/util_templates.h b/software/o_c_REV/util/util_templates.h new file mode 100644 index 000000000..524ba7a44 --- /dev/null +++ b/software/o_c_REV/util/util_templates.h @@ -0,0 +1,57 @@ +// Copyright 2020 Patrick Dowling +// +// Author: Patrick Dowling (pld@gurkenkiste.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +// See http://creativecommons.org/licenses/MIT/ for more information. +// +#ifndef UTIL_TEMPLATES_H_ +#define UTIL_TEMPLATES_H_ + +#include + +namespace util { + +// C++11, >= 14 have their own +template +struct index_sequence { + template using append = index_sequence; +}; + +template +struct make_index_sequence { + using type = typename make_index_sequence::type::template append; +}; + +template <> +struct make_index_sequence<0U> { + using type = index_sequence<>; +}; + +template +struct sum : std::integral_constant { }; + +template +struct sum : std::integral_constant::value > { }; + + +} // util + +#endif // UTIL_TEMPLATES_H_ diff --git a/software/o_c_REV/vector_osc/HSVectorOscillator.h b/software/o_c_REV/vector_osc/HSVectorOscillator.h index 0e2943720..2e4733b7e 100644 --- a/software/o_c_REV/vector_osc/HSVectorOscillator.h +++ b/software/o_c_REV/vector_osc/HSVectorOscillator.h @@ -111,8 +111,10 @@ class VectorOscillator { /* frequency is centihertz (e.g., 440 Hz is 44000) */ void SetFrequency(uint32_t frequency_) { - frequency = frequency_; - rise = calculate_rise(segment_index); + if (frequency_ != frequency) { + frequency = frequency_; + rise = calculate_rise(segment_index); + } } bool GetEOC() {return eoc;} @@ -134,6 +136,14 @@ class VectorOscillator { eoc = !cycle; } + void SetPhase(uint8_t index) { + segment_index = index; + signal = scale_level(segments[segment_count - 1].level); + rise = calculate_rise(segment_index); + sustained = 0; + eoc = !cycle; + } + int32_t Next() { // For non-cycling waveforms, send the level of the last step if eoc if (eoc && cycle == 0) { @@ -145,8 +155,8 @@ class VectorOscillator { if (validate()) { if (rise) { signal += rise; - if (rise >= 0 && signal >= target) advance_segment(); - if (rise < 0 && signal <= target) advance_segment(); + if (rise > 0 && signal >= target) advance_segment(); + else if (rise < 0 && signal <= target) advance_segment(); } else { if (countdown) { --countdown;