Skip to content

Instantly share code, notes, and snippets.

@Visuelle-Musik
Created January 16, 2010 18:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Visuelle-Musik/278929 to your computer and use it in GitHub Desktop.
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
# === 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...
# 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
# --- 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