Skip to content

Commit

Permalink
Release Starmidi - microtonal scales using lanes as piano roll v1.0-b…
Browse files Browse the repository at this point in the history
…eta-1 (#1421)
  • Loading branch information
Starshine09 authored Aug 14, 2024
1 parent deebe5c commit 3908802
Show file tree
Hide file tree
Showing 8 changed files with 978 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
-- @description Starmidi - microtonal scales using lanes as piano roll
-- @author Starshine
-- @version 1.0-beta-1
-- @metapackage
-- @provides
-- [main] starshine_Starmidi - microtonal scales using lanes as piano roll/starshine_Starmidi - Flatten.eel
-- [main] starshine_Starmidi - microtonal scales using lanes as piano roll/starshine_Starmidi - Sharpen.eel
-- [main] starshine_Starmidi - microtonal scales using lanes as piano roll/starshine_Starmidi - Small step down.eel
-- [main] starshine_Starmidi - microtonal scales using lanes as piano roll/starshine_Starmidi - Small step up.eel
-- [main] starshine_Starmidi - microtonal scales using lanes as piano roll/starshine_Starmidi - Parser.eel
-- [main] starshine_Starmidi - microtonal scales using lanes as piano roll/starshine_Starmidi - UI.eel
-- [effect] starshine_Starmidi - microtonal scales using lanes as piano roll/starmidi
-- @about
-- # Starmidi - microtonal scales using lanes as piano roll
--
-- This program seeks to solve several problems for microtonal music producers while streamlining the workflow:
--
-- 1. Microtonal scales may contain far more than 12 notes per octave, and this quickly makes working in the conventional chromatic piano roll cumbersome or even limiting to the instrument's range.
--
-- *This script package and plugin solve this by using an adjustable number of "fixed item lanes" that act as the "piano white key notes" for the given microtonal scale, with other pitches accessible by means of accidentals*
--
-- 2. Different synthesizers and samplers have different ways of loading microtonal scales, sometimes requiring different file formats, inconsistent behavior, timbre warping, and more.
--
-- *These scripts solve this issue by finding the closest MIDI note and applying a pitchbend amount of no more than 50 cents. It's tested working with Vital, Surge XT, and Pianoteq and should work with most synths and samplers.
--
-- To achieve microtonal polyphony, the jsfx plugin (which handles generating the MIDI events from data in gmem) dynamically assigns new notes to free channels. Up to 16 simultaneous and independently tuned notes can be played this way.*
--
-- ### Setup/Usage
--
-- 1. ReaImGui is required
--
-- 2. Preferences > Track Send Defaults > Fixed Lane Defaults > **Uncheck "Automatically delete empty lanes at bottom of track"**
--
-- I strongly recommend selecting "Small lanes" here as well.
--
-- 3. Launch the script "Starshine_starmidi_UI.eel".
--
-- 4. Drag the # of Lanes slider to add 15+ lanes to a track.
--
-- 5. It is beyond the scope of this package documentation to explain microtonal scale theory. If you are interested in learning more, please consider joining the Xenharmonic Alliance discord at https://discord.gg/uxvw5Vzj
--
-- Setting the following combinations for [# of Scale Steps], [Equal Divisions], and [n\EDX as Generator] should yield interesting results. 7\16edo means to select 16 [Equal Divisions] and 7 for [n\EDX as Generator]
--
-- * 7 steps, 7\16edo
-- * 8 steps, 10\27edo
-- * 9 steps, 4\19edo
-- * 7 steps, 13\31edo, mode 5 (this is like a regular major scale but more in tune. roughly "quarter-comma meantone")
--
-- 6. Insert/draw empty MIDI items in lanes. These function as notes. Optionally, you may set a mouse modifier to draw MIDI items so that it functions like the standard piano roll for inserting notes.
--
-- 7. Add a synth to the FX chain; do not enable MPE. Leave pitch bend range at 2 semitones.
--
-- 8. You must click Play from this interface; this runs the parsing script prior to playback. (The script will also auto-add and configure the jsfx.)
--
-- *Optionally, you may create a custom action that runs "starshine_starmidi_parser.eel" and then the Transport: Play/Stop action*

Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
desc:starmidi
options:gmem=fakemidi
//@gmem=fakemidi
// reads "midi events" from gmem by partner reascript
// partner reascript reads items in lanes into gmem
// TODO: handle incoming events on the same bus? for purpose of reassigning channel

slider1:1<1,32,1>track num
// TODO: handle dynamic scale change -- bigger jump, let's get it working the easy way first.

@init

// MIDI command constants
NOTE_OFF_MSG = 0x80;
NOTE_ON_MSG = 0x90;
CHANNEL_MSG = 0xB0;
CHANNEL_STFU = 0x7B;
PITCHBEND = 0xE0;

// gmem data offsets
MEM_PER_TRACK = 196608;
_TR_GMEM_START = 65536;
EVT_WIDTH = 8;
TRACK_COUNT = 32; // about as many tracks as we can support at once
EVT_LENGTH = 16384;
_track_evt_counter = 8192;
PLAYBACK_OKAY = 8000000;

// local memory offsets
_MIDI_EVENTS = 65536; // must match _source_array in reascript
_CHANNELS = 0;

// column reference constants for 2D array _MIDI_EVENTS
EventTime = 0;
EventType = 1;
MidiNote = 2;
Velocity = 3;
PitchbendAmount = 4;
ChannelPointer = 5;

last_play_pos=0;
last_play_state=0;

function _track_evts_gmem_ptr(track_num)(track_num*MEM_PER_TRACK+_TR_GMEM_START);

// the 2D array structure used, array[row][col], depends on doing this
function copy_gmem_to_local()(
_gmem_ptr = _track_evts_gmem_ptr(slider1-1)-1;
_local_ptr = -1;
Loop(EVT_WIDTH*EVT_LENGTH,
_MIDI_EVENTS[_local_ptr+=1] = gmem[_gmem_ptr+=1];
);
);


// unreserve playing channels and turn off every note
function clear_channels()(
ch=-1;
Loop(16,
_CHANNELS[ch+=1]=0;
midisend(0,CHANNEL_MSG|ch, CHANNEL_STFU);
);
);

// scan through sorted events to catch up to current play position
function scan_to_start()(
_play_ptr=-1; t=0;
while( (t<play_position)&&(t>-1) )(
t = _MIDI_EVENTS[_play_ptr+=1][EventTime];
);
);

copy_gmem_to_local();
clear_channels();
scan_to_start();

@slider
slider_show(slider1,0);

@block

@sample

function ANO_if_stopped_or_looping()(
!play_state && last_play_state ? (clear_channels(); scan_to_start(); _play_ptr-=1);
(play_position < last_play_pos) && play_state ? (clear_channels(); scan_to_start(); _play_ptr-=1);
last_play_state = play_state;
last_play_pos = play_position;
);

function scan_to_start_on_play()(
play_state && !last_play_state2 ? (clear_channels(); scan_to_start());
last_play_state2 = play_state;
);

// dynamically obtain an available channel and mark as in-use
function get_free_channel()(
ch=-1; return_ch=-1;
while((return_ch<0) && (ch < 16))
( _CHANNELS[ch+=1] == 0
? ( return_ch = ch;
_CHANNELS[ch] = 1;
);
);
return_ch);


// sends midi pitchbend and note-on. also writes channel for note-off event to use later
function note_on(_ptr)(
chan = get_free_channel();
_chan_ptr = _MIDI_EVENTS[_ptr][ChannelPointer];
_MIDI_EVENTS[_chan_ptr] = chan;
midisend(0, PITCHBEND|chan, _MIDI_EVENTS[_ptr][PitchbendAmount]);
midisend(0, NOTE_ON_MSG|chan, _MIDI_EVENTS[_ptr][MidiNote], _MIDI_EVENTS[_ptr][Velocity]);
);


// sends note-off and frees channel
function note_off(_ptr)(
_chan_ptr = _MIDI_EVENTS[_ptr][ChannelPointer];
chan = _MIDI_EVENTS[_chan_ptr];
_CHANNELS[chan] = 0;
midisend(0, NOTE_OFF_MSG|chan, _MIDI_EVENTS[_ptr][MidiNote], 0);
);


function play_scan()(
while( (t<play_position)&&(t>-1) )(
event = _MIDI_EVENTS[_play_ptr][EventType];
event == NOTE_ON_MSG ? note_on(_play_ptr);
event == NOTE_OFF_MSG ? note_off(_play_ptr);
t = _MIDI_EVENTS[_play_ptr+=1][EventTime];
);
);

function playback_okay()(gmem[PLAYBACK_OKAY+slider1-1]);

ANO_if_stopped_or_looping();

scan_to_start_on_play();

play_state && playback_okay() ? play_scan();














Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// @noindex

// helper script for starmidi tools
// modifies item title by adding or removing # or b symbols

function apply_accidental(take)(
GetSetMediaItemTakeInfo_String(take, "P_NAME", #text, 0);

//todo: check if item is midi

(len=strlen(#text))==0 ? #text ="b" : //case 1: empty. just add a b
match("*b*",#text) ? #text+="b" : //case 2: has one or more b. add another
match("*#*",#text) ? str_setlen(#text,len-1); //case 3: has one or more #. remove one.

GetSetMediaItemTakeInfo_String(take, "P_NAME", #text, 1);

// count the number of b or # to apply colors
pos=-1; r=1; g=1; b=1;

char = str_getchar(#text,0);
(char == 'b') ? (r-=0.125; g-=0.2;);
(char == '#') ? (b-=0.2; g-=0.125;);

Loop(strlen(#text),
char = str_getchar(#text,pos+=1);
char == 'b' ? (r-=0.125; g-=0.2;);
char == '#' ? (b-=0.2; g-=0.125;);
);

r*=255; b*=255; g*=255;
SetMediaItemTakeInfo_Value(take, "I_CUSTOMCOLOR", ColorToNative(r,g,b)|0x1000000);
);

function loop_selected()(
item_idx = -1;
Loop(CountSelectedMediaItems(0),
item = GetSelectedMediaItem(0,item_idx+=1);
take = GetMediaItemTake(item,0);
apply_accidental(take);
);
);

GetMousePosition(x,y);
GetItemFromPoint(x,y,0,take);
!take ? loop_selected() : apply_accidental(take);

Loading

0 comments on commit 3908802

Please sign in to comment.