Created
January 16, 2010 18:08
-
-
Save Visuelle-Musik/278929 to your computer and use it in GitHub Desktop.
Set operations based music generation from two incoming MIDI-note-streams into one result-set
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# === Interference: take MIDI-in from two channels, merge to one output - dependent on "set-operation"-method selected from menu... === | |
# Options for calling: :frame_rate => nn sets framerate, :verbose => true|false gives additional printout-information... (see last line of this file!) | |
# --- Options, see Panel-GUI (when application is started) : --------------------------------------------------------------------------------------------------------------- | |
# ° dropdown "velocity_option" used for include or exclude filter range of velocity for operations including this filter-option... | |
# ° slider "in A Channel": Channel for notes of "set A" | |
# ° slider "in B Channel": Channel for notes of "set B" | |
# ° slider "in switch Channel": Channel for notes to switch an operation, only if operation equals "midi_note_selected" | |
# -> Caution: be sure to select 3 different MIDI-Channels for each functionality, or you may get unpreditible results (default is 1,2,3 anyway) | |
# ° slider "velocity lower limit": range-start for operations with velocity-filter | |
# ° slider "velocity upper limit": range-end for operations with velocity-filter | |
# ° dropdown "operation": set-operations on set A and/or B or select e.g. "empty set" and so on, may include velocity-dependencies or not... | |
# -> For an overview of all operations available see contents of array @sets, below. For description of the operation see comment at equvalent method, below. | |
# -> Special cases of "operation" drop-down-list-box: | |
# 1) "midi_note_selected" => operation will be selected by note on switch channel. Secection is done by mapping pitch (modulo) to all operations available (exept "midi_note_selected" itself, of course) | |
# 2) "emty_set_include_veloRange" => empty set (no operation) selected, _plus_ used for filter range of velocity for operations including this filter-option... | |
# 3) "emty_set_exclude_veloRange" => empty set (no operation) selected, _plus_ for filter _not_ in range of velocity for operations including this filter-option... | |
# NOTE: hidden function: velocity-Range can be changed by MIDI-controllers as well, use Bank-Select (0/32) or continous X/Y-controllers 16/17 | |
# Program change can be used to change velocity-filter options (off, include, exclude) here, Program 1==off, 2==include, 3==exclude. Higher values do the same, by modulo -> 4==off and so on. | |
# This hidden function is only valid on the "in switch Channel" like remotly switching operations and will not automatically update the sliders in the GUI - sorry. | |
# If a holdpedal is pressed (MIDI-Controller 64) on the MIDI-remote-channel we supress all note-off events (a bit different than using foot-pedal only, which can be done on the result-set, naturally... | |
# For a feedback we at least change background-color while in "MIDI-remote-mode"... | |
#--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
# === adopt the next 2 lines, as required ! ==================================================================== | |
# Notes have to come as MIDI-note-events from MIDI-in, the output is redirected to the inport of your DAW (Digital Audio Workstation, e.g. ableton live), adopt your MIDI-ports below! | |
MIDI_PORT_IN = 0 # your MIDI-input where notes will be read in, normally the first in is OK ("midi Yoke 1 in" for me) | |
MIDI_PORT_OUT_DAW_IN = 17 # the ports are numberes from 0 to n in the java-sound-API, | |
# this is "midi Yoke 8 out", for me a virtual in port of my "Digital Audio Wortstation", where this output is sent to | |
class Array # extend for note-manipulatin helper function[s]... a note here is just an array of pitch and velocity | |
def note_found(note) # to have distintive note-objects would be cleaner than to open up and extend the standard-class Array, but we want to keep it fast and simple in this small application... | |
return false if ((note == [] || !note) ) | |
self.each {|e| return true if (note[0] == e[0] ) } # check for same pitch | |
false | |
end | |
end # class Array | |
# === Provide several note-based realtime (set-)operations onto 2 streams of MIDI-notes A,B === | |
class Interfer < Processing::App | |
load_library :opengl | |
include_package "processing.opengl" | |
require "control_panel_20100102" # slightly extended Ruby-processing panel-library | |
require "jr_midi_20100102" # class JRmidi for basic MIDI I/O in JRuby... | |
VELOCITY_OPTIONS = ["off", "include_range", "exclude_range"] # options for velecity-filtering | |
MIDI_REMOTE_COLOUR = 0x005555 # we will change background-color to indicate if "MIDI-remote" is active... | |
def initialize(args) | |
# --- options from "main-procedure-call" --- | |
@verbose = args[:verbose] # additional "tracing-information" in console-window | |
@frame_rate = args[:frame_rate] ? args[:frame_rate] : 32 # any value from 32 to 124 should be fine, lower for priority on MIDI-in, higher for better supression of non-relevant note-ons... | |
@send_allNotesOff_onChange = args[:notesOffOnChange] # cut off notes when operation in menu is changed? | |
# --- instance-variables for control_panel --- | |
@rem_velocity_option = @velocity_option = :off # used for filter range of velocity for operations including this filter-option... rem_ is used to remember values wich may get "out of sync" when using "MIDI-remote..." | |
@in_A_channel = 0 # input for "set A" | |
@in_B_channel = 1 # input for "set B" | |
@in_switch_channel = 2 # input for "switch", method for set-operation may be changed be MIDI if set to "change_by_MIDI_note" | |
@background_color = 0x000000 # we will change background-color to indicate if "MIDI-remote" is active... | |
@rem_velocity_lower_limit = @velocity_lower_limit = 60 # velocity-limits for operations, if any (depandent on operation selected...) | |
@rem_velocity_upper_limit = @velocity_upper_limit = 110 # velocity-limits for operations, if any (depandent on operation selected...) | |
@holdpedal_pressed = false # no notes to hold (supress note-offs), here | |
@velo_range = (@velocity_lower_limit..@velocity_upper_limit) # to be updated everytime we change boundaries... | |
# --- note-activity related instance-variables --- | |
@n_on_A = [] # in channel a notes... | |
@n_off_A = [] | |
@n_on_B = [] # in channel b notes... | |
@n_off_B = [] | |
@n_double = [] # remember actual notenumbers currently playing, so to decide on short or long notes (accept first or second note-off event) | |
@n_on_out = [] # list of all note-ons currently pending (to be given out now) | |
@n_off_out = [] # list of all note-offs currently pending (to be given out now) | |
@n_active_A = [] # remember active notes of set A... | |
@n_active_B = [] # remember active notes of set B... | |
@n_active_out = [] # remember active notes of result set, visualisation only... | |
# --- methodnames of operations for menu _and_ callback-functions (implemented below) ... --- | |
@sets = [ | |
"midi_note_selected", | |
"union", "union_allow_double_trigger", "union_long_notes", | |
"relative_complement_A_B", "relative_complement_B_A", | |
"intersection", "symmetric_difference", "set_A", "set_B", "emty_set" | |
] | |
# --- instance-vars to implement special "MIDI-remote-control" for set-operation-selection, below --- | |
@midi_note_sets = @sets.clone.map {|s| s.to_sym} # clone, so we leave the original array unchanged... | |
@midi_note_sets.shift # @midi_note_sets now have the same content, execpt the first entry... | |
@midi_note_sets_length = @midi_note_sets.length # remember lenght for later (fast) access... | |
@operation = @sets[1].to_sym # default function is first in list here, here the function(symbol) to be called for set-operations is stored | |
@midi_note_selection = (@operation == :midi_note_selected) # normally this evaluates to false... | |
super(args) # call Jeremy Jashkenas' ruby-processing "constructor"... | |
end | |
# === ruby-processing setup-routine, will be called directly before rendering-loop draw()... === | |
def setup | |
# --- Use 3D-accelaration for display of "note-sets" (kind of a traffic light), if possible --- | |
library_loaded?(:opengl) ? render_mode(OPENGL) : render_mode(P3D) | |
hint(DISABLE_OPENGL_2X_SMOOTH); # save performance... | |
hint(DISABLE_DEPTH_TEST); # save performance... | |
no_stroke # save performance... | |
# --- Open MIDI input/output and remember the "output-handle", note-events automatically will call handler-functins in here, below from now on --- | |
@midi_out = JRmidi.new( self, "interference", :in_port => MIDI_PORT_IN, :out_port => MIDI_PORT_OUT_DAW_IN, :verbose => @verbose ) | |
# --- Select framerate, make it quite slow for output, to give priority to MIDI-input... (default framerate for redraw is 60) | |
frameRate(@frame_rate) # -> e.g. a framerate of 32 allows usage of up to 1/64 notes at 120 BPM! | |
# --- Use separate Control-Panel to adjust parameters --- | |
control_panel do |c| # slider_coarse, menu_small, checkbox_option: little extension (MB 20100102) of standard-panel-library | |
c.slider_coarse( :in_A_channel, 1..16, @in_A_channel+1) { @in_A_channel -=1; adjust() } # MIDI-channels are internally one lower as displayed | |
c.slider_coarse( :in_B_channel, 1..16, @in_B_channel+1) { @in_B_channel -=1; adjust() } # MIDI-channels are internally one lower as displayed | |
c.slider_coarse( :in_switch_channel, 1..16, @in_switch_channel+1) { @in_switch_channel -=1; adjust() } # MIDI-channels are internally one lower as displayed | |
c.menu_small( :velocity_option, VELOCITY_OPTIONS ) {|vel_opt| @rem_velocity_option = @velocity_option = vel_opt.to_sym; adjust() } | |
c.slider_coarse( :velocity_lower_limit, 0..127, @velocity_lower_limit) { @rem_velocity_lower_limit = @velocity_lower_limit; adjust() } # velocity-limits for operations, if any (depandent on operation selected...) | |
c.slider_coarse( :velocity_upper_limit, 0..127, @velocity_upper_limit) { @rem_velocity_upper_limit = @velocity_upper_limit; adjust() } # velocity-limits for operations, if any (depandent on operation selected...) | |
c.menu_small( :operation, @sets, @operation.to_s ) {|m| adjust(m) } | |
end | |
end | |
# --- Function to be called each time for every control_panel item selected or changed... --- | |
def adjust( item=nil ) # control_panel callback[s] | |
if( item ) | |
@operation = item.to_sym # new method of "interference" selected... | |
if( @operation == :midi_note_selected ) | |
@midi_note_selection = true # from now on selection of set-operation can be "remote-controlled" by MIDI-notes on separate channel | |
@operation = :emty_set # default to "no op" until actually selected by MIDI-note..., don not change display in drop-down-list-box... | |
else | |
@midi_note_selection = false | |
end | |
end | |
@midi_out.send_control_change( 0, 123, 0 ) if @send_allNotesOff_onChange # all notes off (channel 0, controller 123, value 0) | |
@n_active_A = []; @n_active_B = []; @n_active_out = []; @n_double = [] # reinit active notes of A, B and result set... | |
# Restore previous values, in case if changed by MIDI-remote before, so that again WYSIWYG is true ;-) | |
@background_color = 0x000000 # change background-color to indicate that "MIDI-remote" is not active, just in case... | |
@holdpedal_pressed = false # no notes to hold (supress note-offs), here | |
@velocity_option = @rem_velocity_option | |
@velocity_lower_limit = @rem_velocity_lower_limit | |
@velocity_upper_limit = @rem_velocity_upper_limit | |
@velo_range = (@velocity_lower_limit..@velocity_upper_limit) | |
puts "operation %s | velocity_option: %s in-A: %d in-B: %d in_switch_channel: %d \n- midi_note_selection: %s velo_range: %s" % | |
[@operation.inspect, @velocity_option.inspect, @in_A_channel+1, @in_B_channel+1, @in_switch_channel+1, | |
@midi_note_selection.inspect, @velo_range.inspect] if @verbose | |
end | |
# === main ruby-processing loop, will be called each frame, depending on framerate, framerate should be not below 30 to get smooth note-handling... === | |
def draw | |
background (@background_color) | |
( fill(0xFFED0B05); ellipse(65, 80, 123, 123) ) unless (@n_active_A == []) # red | |
( fill(0xFFFFF034); ellipse(65, 225, 123, 123) ) unless (@n_active_B == []) # yellow | |
@n_off_out.each {|off| # delete all sustaining note-outs for wich the same pitch is already marked as note-off! | |
@n_active_out.reject! {|on| off[0] == on[0] } # only relevant for (next) visualisation... | |
} | |
( fill(0xFF3DC300); ellipse(65, 370, 123, 123) ) unless (@n_active_out == []) # green | |
@n_off_out.each {|off| # delete all note-ons for wich the same pitch is already marked as note-off! | |
@n_on_out.reject! {|on| off[0] == on[0] } | |
} | |
@n_on_out.each {|n| @midi_out.send_note_on( 0, *n ) } # note here is an array of pitch plus velocity... | |
@n_on_out = [] # reset all "pending" on out-notes | |
@n_off_out.each {|n| @midi_out.send_note_off( 0, *n ) } unless @holdpedal_pressed # supress note-offs selected by "MIDI-remote" | |
@n_off_out = [] # reset all "pending" off out-notes | |
end | |
# === MIDI-interference functions... (set operation functions) start here === | |
def union() # The union of A and B, denoted A u B, including velocity-range (Vereinigungsmenge) | |
velo_reject( @n_on_A ) # discard note-ons not within given velocity-range... | |
velo_reject( @n_on_B ) # discard note-ons not within given velocity-range... | |
sustain_reject( @n_active_A, @n_on_B ) # check if note has been playing before, to decide if note has to be rejected or not... | |
sustain_reject( @n_active_B, @n_on_A ) # check if note has been playing before, to decide if note has to be rejected or not... | |
@n_active_out += @n_on_out += @n_on_A + @n_on_B | |
sustain_keep( @n_active_A, @n_off_A ) # keep note-off-event, if note was active for this set before (and not filtered by velocity) | |
sustain_keep( @n_active_B, @n_off_B ) # keep note-off-event, if note was active for this set before (and not filtered by velocity) | |
@n_off_out += @n_off_A + @n_off_B | |
remember_in_activity() | |
end | |
def union_allow_double_trigger() # The union of A and B, denoted A u B, including velocity-range (Vereinigungsmenge) | |
velo_reject( @n_on_A ) # discard note-ons not within given velocity-range... | |
velo_reject( @n_on_B ) # discard note-ons not within given velocity-range... | |
@n_active_out += @n_on_out += @n_on_A + @n_on_B | |
sustain_keep( @n_active_A, @n_off_A ) # keep note-off-event, if note was active for this set before (and not filtered by velocity) | |
sustain_keep( @n_active_B, @n_off_B ) # keep note-off-event, if note was active for this set before (and not filtered by velocity) | |
@n_off_out += @n_off_A + @n_off_B | |
remember_in_activity() | |
end | |
def union_long_notes() # The union of A and B, denoted A u B, including velocity-range (Vereinigungsmenge), longer notes have priority for identical ones... | |
velo_reject( @n_on_A ) # discard note-ons not within given velocity-range... | |
velo_reject( @n_on_B ) # discard note-ons not within given velocity-range... | |
sustain_reject( @n_active_A, @n_on_B ) # check if note has been playing before, to decide if note has to be rejected or not... | |
sustain_reject( @n_active_B, @n_on_A ) # check if note has been playing before, to decide if note has to be rejected or not... | |
@n_active_out += @n_on_out += @n_on_A + @n_on_B | |
if( (@n_off_A != []) && @n_double[@n_off_A[0][0]] ) # this note was playing before... | |
@n_double[@n_off_A[0][0]] = false | |
@n_off_A = [] # ignore first note-off, so to have longer notes if identical notes occur | |
else | |
sustain_keep( @n_active_A, @n_off_A ) # keep note-off-event, if note was active for this set before (and not filtered by velocity) # ??? | |
end | |
if( (@n_off_B != []) && @n_double[@n_off_B[0][0]] ) # this note was playing before... | |
@n_double[@n_off_B[0][0]] = false | |
@n_off_B = [] # ignore first note-off, so to have longer notes if identical notes occur | |
else | |
sustain_keep( @n_active_B, @n_off_B ) # keep note-off-event, if note was active for this set before (and not filtered by velocity) # ??? | |
end | |
@n_off_out += @n_off_A + @n_off_B | |
remember_in_activity() | |
end | |
def relative_complement_A_B() # The relative complement of A in B (A\B or A-B), "Channel B" truncates notes on Channel A (Differenzmenge) | |
velo_reject( @n_on_B ) # discard note-ons not within given velocity-range... | |
sustain_keep( @n_active_B, @n_off_B ) # keep note-off-event, if note was active for this set before (and not filtered by velocity) | |
remember_in_activity() | |
sustain_reject( @n_active_B, @n_on_A ) # any new A-note-ons will be ignored if also within set B | |
@n_active_out += @n_on_out += @n_on_A # these are the remaining notes from set A | |
@n_off_out += @n_off_A + @n_on_B + @n_off_B # take A-note-off _plus_ B-note-ons as note_offs (may be no note on before - should work with most DAWs anyway...) | |
end | |
def relative_complement_B_A() # The relative complement of B in A (B\A or B-A), "Channel A" truncates notes on Channel B (Differenzmenge) | |
velo_reject( @n_on_A ) # discard note-ons not within given velocity-range... | |
sustain_keep( @n_active_A, @n_off_A ) # keep note-off-event, if note was active for this set before (and not filtered by velocity) | |
remember_in_activity() | |
sustain_reject( @n_active_A, @n_on_B ) # any new B-note-ons will be ignored if also within set A | |
strip_sustain( @n_active_B, @n_off_B ) # delete sustaining notes if note off for active note given now... | |
@n_active_out += @n_on_out += @n_on_B # these are the remaining notes from set B | |
@n_off_out += @n_off_B + @n_on_A + @n_off_A # take B-note-off _plus_ A-note-ons as note_offs (may be no note on before - should work with most DAWs anyway...) | |
end | |
def intersection() # The intersection of A and B, denoted A n B (Schnittmenge) | |
velo_reject( @n_on_A ) # discard note-ons not within given velocity-range... | |
sustain_keep( @n_active_A, @n_off_A ) # keep note-off-event, if note was active for this set before (and not filtered by velocity) | |
velo_reject( @n_on_B ) # discard note-ons not within given velocity-range... | |
sustain_keep( @n_active_B, @n_off_B ) # keep note-off-event, if note was active for this set before (and not filtered by velocity) | |
remember_in_activity() | |
@n_on_B = [] unless @n_active_A.note_found(@n_on_B[0]) # any new B-note-ons will be ignored if not also within set A | |
@n_on_A = [] unless @n_active_B.note_found(@n_on_A[0]) # any new A-note-ons will be ignored if not also within set B | |
@n_active_out += @n_on_out += @n_on_A + @n_on_B | |
@n_off_out += @n_off_A + @n_off_B | |
end | |
def symmetric_difference # symmetric difference of A and B (Symetrische Differenz, Komplementär zur Schnittmenge) | |
remember_in_activity() | |
difference_off = [] # directly send notes-off if we had a note-on on A plus directly on B | |
if( @velocity_option == :include_range ) | |
if ( @n_active_A.note_found(@n_on_B[0]) && (@velo_range === @n_on_B[0][1]) ) # we get notes one by one, so first is enough to check in active-list... | |
difference_off += @n_on_B | |
end | |
if ( @n_active_B.note_found(@n_on_A[0]) && (@velo_range === @n_on_A[0][1]) ) # if on-note is not within current velo-range it will _not_ be discarded! | |
difference_off += @n_on_A | |
end | |
end | |
if( @velocity_option == :exclude_range ) | |
if ( @n_active_A.note_found(@n_on_B[0]) && (!(@velo_range === @n_on_B[0][1])) ) # we get notes one by one, so first is enough to check in active-list... | |
difference_off += @n_on_B | |
end | |
if ( @n_active_B.note_found(@n_on_A[0]) && (!(@velo_range === @n_on_A[0][1])) ) # if on-note is not within current velo-range it will _not_ be discarded! | |
difference_off += @n_on_A | |
end | |
end | |
sustain_reject( @n_active_A, @n_on_B ) # any new B-note-ons will be ignored if not also within set A | |
sustain_reject( @n_active_B, @n_on_A ) # any new A-note-ons will be ignored if not also within set B | |
@n_active_out += @n_on_out += @n_on_A + @n_on_B | |
@n_off_out += @n_off_A + @n_off_B + difference_off | |
end | |
def set_A # set A (Menge A) | |
velo_reject( @n_on_A ) # discard note-ons not within given velocity-range... | |
sustain_keep( @n_active_A, @n_off_A ) # keep note-off-event, if note was active for this set before (and not filtered by velocity) | |
remember_in_activity() | |
@n_active_out += @n_on_out += @n_on_A | |
@n_off_out += @n_off_A | |
end | |
def set_B # set B (Menge B) | |
velo_reject( @n_on_B ) # discard note-ons not within given velocity-range... | |
sustain_keep( @n_active_B, @n_off_B ) # keep note-off-event, if note was active for this set before (and not filtered by velocity) | |
remember_in_activity() | |
@n_active_out += @n_on_out += @n_on_B | |
@n_off_out += @n_off_B | |
end | |
def emty_set # emty_set (leere Menge ) | |
remember_in_activity() | |
end | |
# === helpers and 'action'-function: give out, then reset all "pending" notes now === | |
def remember_in_activity | |
@n_active_A += @n_on_A # notes from set A are active notes for potential difference-operation | |
strip_sustain( @n_active_A, @n_off_A ) # if "key-up" this note is not "active" anymore... | |
@n_active_B += @n_on_B # notes from set B are active notes for potential difference-operation | |
strip_sustain( @n_active_B, @n_off_B ) # if "key-up" this note is not "active" anymore... | |
end | |
def velo_reject( note_array ) # check if within velocity-range, else return empty array | |
if( (@velocity_option != :off) && (note_array != []) ) | |
if( @velocity_option == :include_range ) | |
note_array.pop unless (@velo_range === note_array[0][1]) | |
end | |
if( @velocity_option == :exclude_range ) | |
note_array.pop if (@velo_range === note_array[0][1]) | |
end | |
end | |
end | |
def sustain_keep( sustain_array, note_array ) # check if note has been playing before, to decide if note-off is relevant in given context (e.g. note-on may be filtered by velocity before), else return empty array | |
if( note_array != [] ) | |
my_note = note_array[0][0] | |
found = false | |
sustain_array.each {|s| if (my_note == s[0]) then found=true; break; end } | |
note_array.pop unless found # keep note-off-event, if note was active for this set before (and not filtered by velocity) | |
end | |
end | |
def sustain_reject( sustain_array, note_array ) # check if note has been playing before, to decide if note has to be rejected or not... | |
if( note_array != [] ) | |
my_note = note_array[0][0] | |
sustain_array.each {|s| | |
if(my_note == s[0]) | |
note_array.pop | |
@n_double[my_note] = true | |
return | |
end | |
} # keep note-off-event, if note was active for this set before (and not filtered by velocity) | |
end | |
end | |
def strip_sustain( active_notes, note_off_array ) # strip notes "ringing", dependant on note-off given (empty array or just one note-off) | |
active_notes.reject! {|e| e[0] == note_off_array[0][0] } if( note_off_array != [] ) | |
end | |
# === MIDI-callbacks called thru JRmidi-Class ... === | |
def note_on( channel, pitch, velocity ) | |
case channel | |
when @in_A_channel | |
@n_on_A = [[pitch, velocity]] # using this notation empty Arrays: [] will be abandoned lateron, notes are an array of pitch+velocity in one array, like: [[60,100]] | |
send( @operation ) | |
@n_on_A = [] # reset "pending" notes | |
when @in_B_channel | |
@n_on_B = [[pitch, velocity]] | |
send( @operation ) | |
@n_on_B = [] # reset "pending" notes | |
when @in_switch_channel | |
if( @midi_note_selection ) # decide on method for set-operation dependant on incoming MIDI-Notes ? (default: an MIDI-channel 3) | |
@operation = @midi_note_sets[pitch % @midi_note_sets_length] # map note to operation, dependant on currently available methods... | |
@midi_out.send_control_change( 0, 123, 0 ) if @send_allNotesOff_onChange # all notes off (channel 0, controller 123, value 0) | |
@n_active_A = []; @n_active_B = []; @n_active_out = []; @n_double = [] # reinit active notes of A, B and result set... | |
puts "new operation %s selected by MIDI-note on channel %d, velocity_option: %s" % [@operation.inspect, @in_switch_channel+1, @velocity_option.inspect] | |
end | |
else | |
puts( "--- note-on, but (channel) not relevant ---") if @verbose | |
end | |
end | |
def note_off( channel, pitch, velocity ) | |
case channel | |
when @in_A_channel | |
@n_off_A = [[pitch, velocity]] | |
send( @operation ) | |
@n_off_A = [] | |
when @in_B_channel | |
@n_off_B = [[pitch, velocity]] | |
send( @operation ) | |
@n_off_B = [] | |
when( @in_switch_channel ) # off event from channel optionally selecting set-operation by MIDI - nothing do do here.. | |
else | |
puts( "--- off, but channel not relevant ---") if @verbose | |
end | |
end | |
def control_change( channel, ctrl, val ) # look if we have to change velocity filter boundaries by MIDI "remote-control" | |
return unless ( channel == @in_switch_channel ) # only allow "MIDI-remote" on correct channel | |
case ctrl | |
when 0 # bank select lower value | |
@background_color = MIDI_REMOTE_COLOUR # change background-color to indicate that "MIDI-remote" is active now | |
@velocity_lower_limit = [(val+1),127].min # caution: banks are displayed one higher in your DAW normally, we correct here | |
puts "velocity_lower_limit set to %d" % @velocity_lower_limit | |
when 32 # bank select upper value | |
@background_color = MIDI_REMOTE_COLOUR # change background-color to indicate that "MIDI-remote" is active now | |
@velocity_upper_limit = [(val+1),127].min # caution: banks are displayed one higher in your DAW normally, we have to take care that because of offset value does not get larger than 127 | |
puts "velocity_upper_limit set to %d" % @velocity_upper_limit | |
when 16 # alternative: continous controller X-val (General Purpose 1) | |
@background_color = MIDI_REMOTE_COLOUR # change background-color to indicate that "MIDI-remote" is active now | |
@velocity_lower_limit = val | |
when 17 # alternative: continous controller Y-val (General Purpose 2) | |
@background_color = MIDI_REMOTE_COLOUR # change background-color to indicate that "MIDI-remote" is active now | |
@velocity_upper_limit = val | |
when 64 | |
@background_color = MIDI_REMOTE_COLOUR # change background-color to indicate that "MIDI-remote" is active now | |
if (val <= 63) | |
@holdpedal_pressed = false | |
else | |
@holdpedal_pressed = true | |
end | |
else | |
puts( "Channel: %d Ctrl %d value: %d, ignored ---" % [channel+1, ctrl, val] ) if @verbose | |
end | |
@velo_range = (@velocity_lower_limit..@velocity_upper_limit) | |
puts( "--- Ctrl %d detected, velocity-filter range now is: %s" % [ctrl, @velo_range.inspect] ) unless (ctrl==123) if @verbose # "reset all controllers" | |
end | |
def program_change( channel, program_number ) # switch velocity-filter options by MIDI "remot-control" | |
return unless ( channel == @in_switch_channel ) # only allow "MIDI-remote" on correct channel | |
@velocity_option = VELOCITY_OPTIONS[program_number % VELOCITY_OPTIONS.length].to_sym | |
puts( "--- Velocity-option set to %s by program-change %d" % [@velocity_option.inspect, program_number+1] ) | |
end | |
end # class Interfer | |
# === Start main-application -> adopt screen resolution, name, :frame_rate => nn sets framerate, :notesOffOnChange => cuts off notes when menu is changed, :verbose => true gives additional printout-information... | |
Interfer.new(:width => 130, :height => 453, :title => "interfere", :frame_rate => 32, :notesOffOnChange => true, :verbose => true) # framerate should not be below 30 to get smooth note-handling, 32 gives 1/64 notes at 120 BPM... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Here's a little library for quickly hooking up controls to sketches. | |
# For messing with the parameters and such. | |
# These controls will set instance variables on the sketches. | |
# You can make sliders, checkboxes, buttons, and drop-down menus. | |
# (optionally) pass the range and default value. | |
# MB 20100102: slightly extended the original juby-processing-library version of Jeremy Ashkenas (added modified methods: slider_coarse() and menu_small() | |
module ControlPanel | |
class Slider < javax.swing.JSlider | |
def initialize(control_panel, name, range, initial_value, proc=nil) | |
min, max = range.begin * 100, range.end * 100 | |
super(min, max) | |
set_minor_tick_spacing((max - min).abs / 10) | |
set_paint_ticks true | |
paint_labels = true | |
set_preferred_size(java.awt.Dimension.new(190, 30)) | |
label = control_panel.add_element(self, name) | |
add_change_listener do | |
update_label(label, name, value) | |
$app.instance_variable_set("@#{name}", value) unless value.nil? | |
proc.call(value) if proc | |
end | |
set_value(initial_value ? initial_value*100 : min) | |
fire_state_changed | |
end | |
def value | |
get_value / 100.0 | |
end | |
def update_label(label, name, value) | |
value = value.to_s | |
value << "0" if value.length < 4 | |
label.set_text "<html><br>#{name.to_s}: #{value}</html>" | |
end | |
end | |
class SliderCoarse < javax.swing.JSlider # MB 20100102 | |
def initialize(control_panel, name, range, initial_value, proc=nil) | |
min, max = range.begin * 100, range.end * 100 | |
super(min, max) | |
slider_ticks = range.to_a.length | |
slider_ticks /= 10.0 if slider_ticks > 100 | |
set_minor_tick_spacing((max - min).abs / slider_ticks) # MB 20100102 | |
set_paint_ticks true | |
paint_labels = true | |
set_preferred_size(java.awt.Dimension.new(190, 30)) | |
label = control_panel.add_element(self, name) | |
add_change_listener do | |
update_label(label, name, value) | |
$app.instance_variable_set("@#{name}", value) unless value.nil? | |
proc.call(value) if proc | |
end | |
set_value(initial_value ? initial_value*100 : min) | |
fire_state_changed | |
end | |
def value | |
(get_value / 100.0).floor # MB 20100102 | |
end | |
def update_label(label, name, value) | |
value = value.to_s | |
# value << "0" if value.length < 4 # MB 20100102 | |
label.set_text "<html><br>#{name.to_s}: #{value}</html>" | |
end | |
end | |
class Menu < javax.swing.JComboBox | |
def initialize(control_panel, name, elements, initial_value, proc=nil) | |
super(elements.to_java(:String)) | |
set_preferred_size(java.awt.Dimension.new(190, 30)) | |
control_panel.add_element(self, name) | |
add_action_listener do | |
$app.instance_variable_set("@#{name}", value) unless value.nil? | |
proc.call(value) if proc | |
end | |
set_selected_index(initial_value ? elements.index(initial_value) : 0) | |
end | |
def value | |
get_selected_item | |
end | |
end | |
class MenuSmall < javax.swing.JComboBox # MB 20100102 | |
def initialize(control_panel, name, elements, initial_value, proc=nil) | |
super(elements.to_java(:String)) | |
set_preferred_size(java.awt.Dimension.new(190, 20)) # MB 20100102 | |
control_panel.add_element(self, name) | |
add_action_listener do | |
$app.instance_variable_set("@#{name}", value) unless value.nil? | |
proc.call(value) if proc | |
end | |
set_selected_index(initial_value ? elements.index(initial_value) : 0) | |
end | |
def value | |
get_selected_item | |
end | |
end | |
class Checkbox < javax.swing.JCheckBox | |
def initialize(control_panel, name, proc=nil) | |
@control_panel = control_panel | |
super(name.to_s) | |
set_preferred_size(java.awt.Dimension.new(190, 64)) | |
set_horizontal_alignment javax.swing.SwingConstants::CENTER | |
control_panel.add_element(self, name, false) | |
add_action_listener do | |
$app.instance_variable_set("@#{name}", value) unless value.nil? | |
proc.call(value) if proc | |
end | |
end | |
def value | |
is_selected | |
end | |
end | |
class CheckboxOption < javax.swing.JCheckBox | |
def initialize(control_panel, name, proc=nil) | |
@control_panel = control_panel | |
super(name.to_s) | |
set_preferred_size(java.awt.Dimension.new(190, 64)) | |
set_horizontal_alignment javax.swing.SwingConstants::CENTER | |
control_panel.add_element(self, name, false) | |
add_action_listener do | |
$app.instance_variable_set("@#{name}", value) unless value.nil? | |
proc.call(value) if proc | |
end | |
end | |
def value | |
is_selected | |
end | |
end | |
class Button < javax.swing.JButton | |
def initialize(control_panel, name, proc=nil) | |
super(name.to_s) | |
set_preferred_size(java.awt.Dimension.new(170, 64)) | |
control_panel.add_element(self, name, false, true) | |
add_action_listener do | |
$app.send(name.to_s) | |
proc.call(value) if proc | |
end | |
end | |
end | |
class Panel < javax.swing.JFrame | |
attr_accessor :elements | |
def initialize | |
super() | |
@elements = [] | |
@panel = javax.swing.JPanel.new(java.awt.FlowLayout.new(1, 0, 0)) | |
end | |
def display | |
add @panel | |
set_size 200, 30 + (64 * @elements.size) | |
set_default_close_operation javax.swing.JFrame::DISPOSE_ON_CLOSE | |
set_resizable false | |
# Need to wait for the sketch to finish sizing... | |
Thread.new do | |
sleep 0.2 while $app.default_size? | |
set_location($app.width + 10, 0) | |
show | |
end | |
end | |
def add_element(element, name, has_label=true, button=false) | |
if has_label | |
label = javax.swing.JLabel.new("<html><br>#{name}</html>") | |
@panel.add label | |
end | |
@elements << element | |
@panel.add element | |
return has_label ? label : nil | |
end | |
def remove | |
remove_all | |
dispose | |
end | |
def slider(name, range=0..100, initial_value = nil, &block) | |
slider = Slider.new(self, name, range, initial_value, block || nil) | |
end | |
def slider_coarse(name, range=0..100, initial_value = nil, &block) # MB 20100102 | |
slider = SliderCoarse.new(self, name, range, initial_value, block || nil) | |
end | |
def menu(name, elements, initial_value = nil, &block) | |
menu = Menu.new(self, name, elements, initial_value, block || nil) | |
end | |
def menu_small(name, elements, initial_value = nil, &block) # MB 20100102 | |
menu = MenuSmall.new(self, name, elements, initial_value, block || nil) | |
end | |
def checkbox(name, initial_value = nil, &block) | |
checkbox = Checkbox.new(self, name, block || nil) | |
checkbox.do_click if initial_value == true | |
end | |
def checkbox_option(name, initial_value = nil, &block) | |
checkbox = Checkbox.new(self, name, block || nil) | |
checkbox.do_click if initial_value == true | |
checkbox # MB 20100108: give back "handle to checkbox", in case if status shall be changed externally... | |
end | |
def button(name, &block) | |
button = Button.new(self, name, block || nil) | |
end | |
end | |
module InstanceMethods | |
def control_panel | |
return if Processing.online? | |
@control_panel = ControlPanel::Panel.new unless @control_panel | |
return @control_panel unless block_given? | |
yield(@control_panel) | |
@control_panel.display | |
end | |
end | |
end | |
Processing::App.send :include, ControlPanel::InstanceMethods |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# --- Very basic Midi-Implementation for JRuby (note on/off, pitchbend, controller, aftertouch), especially for use with ruby-processing, http://wiki.github.com/jashkenas/ruby-processing --- | |
module JavaMidi | |
midi = javax.sound.midi | |
import midi.MidiSystem | |
import midi.MidiDevice | |
import midi.MidiEvent | |
import midi.ShortMessage | |
import midi.Receiver | |
end | |
class JRmidi # Java midi Interface for JRuby (ruby-processing and others) | |
# --- If you want to extent this class, use send(), send_note_on() and other send_.*-methods as a "blueprint" --- | |
# --- For details on the use of Java-MIDI see: http://java.sun.com/j2se/1.5.0/docs/api/javax/sound/midi/MidiSystem.html --- | |
@@RPmidiIn = Hash.new # class variable to monitor number of initialisations... | |
@@RPmidiOut = Hash.new # class variable to monitor number of initialisations... | |
def initialize( processing_instance, instance_name, params ) # establish new MIDI-in or out-connections to be used in the application | |
@processing_instance = processing_instance | |
@name = instance_name | |
@verbose = params[:verbose] | |
@midi_in_port = params[:in_port] | |
@midi_out_port = params[:out_port] | |
@catch_runtime_errors = params[:catch_runtime_errors] | |
@midiMsg = JavaMidi::ShortMessage.new | |
if( @@RPmidiIn[instance_name] || @@RPmidiOut[instance_name] ) # true if initialized before: avoid multiple instances with identical functionality, to be called by Processing for instance... | |
@outputDevice = @@RPmidiOut[instance_name] # "reload" midi-out handle as remembered be previous instance... | |
@inputDevice = @@RPmidiIn[instance_name] # "reload" midi-in handle as remembered be previous instance... | |
else | |
if( @midi_in_port ) # use MIDI-in for this instance? | |
in_port = JavaMidi::MidiSystem.getMidiDeviceInfo[@midi_in_port] | |
puts "INport: " + in_port.to_s | |
@inputDevice = JavaMidi::MidiSystem.getMidiDevice(in_port) | |
@inputDevice.open() | |
transmit = @inputDevice.getTransmitter() | |
puts transmit.to_s if @verbose | |
transmit.setReceiver(self) | |
@@RPmidiIn[instance_name] = @inputDevice # remember "handle" associated to the given MIDI-out, in case if instanciated more than once for midi-out-methods later | |
end | |
if( @midi_out_port ) # use MIDI-out for this instance? | |
out_port = JavaMidi::MidiSystem.getMidiDeviceInfo[@midi_out_port] | |
puts "OUTport: " + out_port.to_s | |
outputDevice = JavaMidi::MidiSystem.getMidiDevice(out_port) | |
outputDevice.open | |
@outputDevice = outputDevice.getReceiver() | |
puts @outputDevice.to_s if @verbose | |
@@RPmidiOut[instance_name] = @outputDevice # remember "handle" associated to the given MIDI-out, in case if instanciated more than once for midi-out-methods later | |
end | |
end | |
end | |
def name; @name.to_s; end # just in case, somebody wants to retreive the "process_name" of a connected "MIDI-out-handle"... | |
def send(msg, time_stamp) # "Interface"-method for javax.sound.midi.Receiver, "callback-method" for MIDI-input | |
case msg.getCommand | |
when 0x90 | |
@processing_instance.note_on( msg.getChannel, msg.getData1, msg.getData2 ) | |
when 0x80 | |
@processing_instance.note_off( msg.getChannel, msg.getData1, msg.getData2 ) | |
when 0xe0 | |
@processing_instance.pitch_bend( msg.getChannel, ((msg.getData2 << 7 ) | msg.getData1)-8192 ) | |
when 0xb0 | |
@processing_instance.control_change( msg.getChannel, msg.getData1, msg.getData2 ) | |
when 0xc0 | |
@processing_instance.program_change( msg.getChannel, msg.getData1 ) # single data-byte message! | |
when 0xd0 | |
@processing_instance.channel_pressure( msg.getChannel, msg.getData1 ) # single data-byte message! | |
else | |
# puts "new MIDI-message 0x%x, channel: %d, data1: %d, data2: %d" % [msg.getCommand, msg.getChannel, msg.getData1, msg.getData2] | |
return # we got a MIDI-message here, that we do not want to process... | |
end | |
rescue => ex | |
puts "%s:%d: exception of type %s occured %s\n%s" % | |
[__FILE__, __LINE__, ex.class, ex.message, ex.backtrace.join("\n")] unless midi_event_not_processed(ex.message) | |
if( @verbose ) | |
puts "MIDI-message 0x%x, channel: %d, data1: %d, data2: %d, not processed..." % | |
[msg.getCommand, msg.getChannel+1, msg.getData1, msg.getData2] | |
end | |
end | |
def close # "Interface"-method for javax.sound.midi.Receiver, can be called remotely to close the device | |
@inputDevice.close | |
end | |
# === User-functions for MIDI-output === | |
def send_note_on( channel, note, velocity ) | |
@midiMsg.setMessage( 0x90, channel, note, velocity ) | |
@outputDevice.send(@midiMsg, -1) # timestamp is second parameter, set to "immediate" | |
end | |
def send_note_off( channel, note, velocity ) | |
@midiMsg.setMessage( 0x80, channel, note, velocity ) | |
@outputDevice.send(@midiMsg, -1) | |
end | |
def send_pitch_bend( channel, pitch ) | |
pitch += 8192 | |
@midiMsg.setMessage( 0xe0, channel, pitch & 0x7f, pitch >> 7 ) | |
@outputDevice.send(@midiMsg, -1) | |
end | |
def send_control_change( channel, controller, value ) | |
@midiMsg.setMessage( 0xb0, channel, controller, value ) | |
@outputDevice.send(@midiMsg, -1) | |
end | |
def send_program_change( channel, controller, value ) | |
@midiMsg.setMessage( 0xc0, channel, program_value, 0 ) # single data-byte message, 3rd parameter set to zero! | |
@outputDevice.send(@midiMsg, -1) | |
end | |
def send_channel_pressure( channel, touch_value ) | |
@midiMsg.setMessage( 0xd0, channel, touch_value, 0 ) # single data-byte message, 3d parameter set to zero! | |
@outputDevice.send(@midiMsg, -1) | |
end | |
# --- helper function --- | |
def midi_event_not_processed(msg) # if this event is mentioned in the error-message, then normally ok because it's optional to implement "call-back"... | |
msg.index(/method.*note_off/) || msg.index(/method.*note_on/) || msg.index(/method.*pitch_bend/) || | |
msg.index(/method.*control_change/) || msg.index(/method.*progam_change/) || msg.index(/method.*channel_pressure/) | |
end | |
end # class JRmidi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment