Skip to content
Permalink
4d0b460b4b
Go to file
 
 
Cannot retrieve contributors at this time
1619 lines (1336 sloc) 71 KB
#--
# This file is part of Sonic Pi: http://sonic-pi.net
# Full project source: https://github.com/samaaron/sonic-pi
# License: https://github.com/samaaron/sonic-pi/blob/master/LICENSE.md
#
# Copyright 2016, 2017 by Sam Aaron (http://sam.aaron.name).
# All rights reserved.
#
# Permission is granted for use, copying, modification, and
# distribution of modified versions of this work as long as this
# notice is included.
#++
module SonicPi
module Lang
module Midi
include SonicPi::Lang::Support::DocSystem
def midi_available_ports
['*'].map{|el| el.freeze}.freeze
end
def use_midi_logging(v, &block)
raise DeprecationError, "use_midi_logging does not work with a do/end block. Perhaps you meant with_midi_logging" if block
__thread_locals.set(:sonic_pi_suppress_midi_logging, !v)
end
doc name: :use_midi_logging,
introduced: Version.new(3,0,0),
summary: "Enable and disable MIDI logging",
doc: "Enable or disable log messages created on MIDI functions. This does not disable the MIDI functions themselves, it just stops them from being printed to the log",
args: [[:true_or_false, :boolean]],
opts: nil,
accepts_block: false,
examples: ["use_midi_logging true # Turn on MIDI logging", "use_midi_logging false # Disable MIDI logging"]
def with_midi_logging(v, &block)
raise ArgumentError, "with_midi_logging requires a do/end block. Perhaps you meant use_midi_logging" unless block
current = __thread_locals.get(:sonic_pi_suppress_midi_logging)
__thread_locals.set(:sonic_pi_suppress_midi_logging, !v)
block.call
__thread_locals.set(:sonic_pi_suppress_midi_logging, current)
end
doc name: :with_midi_logging,
introduced: Version.new(3,0,0),
summary: "Block-level enable and disable MIDI logging",
doc: "Similar to use_midi_logging except only applies to code within supplied `do`/`end` block. Previous MIDI log value is restored after block.",
args: [[:true_or_false, :boolean]],
opts: nil,
accepts_block: true,
requires_block: true,
examples: ["
# Turn on MIDI logging:
use_midi_logging true
midi :e1 # message is printed to log
with_midi_logging false do
#MIDI logging is now disabled
midi :f2 # MIDI message *is* sent but not displayed in log
end
sleep 1
# Debug is re-enabled
midi :G3 # message is displayed in log
"]
def use_midi_defaults(*args, &block)
raise "use_midi_defaults does not work with a block. Perhaps you meant with_midi_defaults" if block
args_h = resolve_synth_opts_hash_or_array(args)
args_h.each { |k, v| v.freeze }
__thread_locals.set :sonic_pi_mod_midi_defaults, SonicPi::Core::SPMap.new(args_h)
end
doc name: :use_midi_defaults,
introduced: Version.new(3,0,0),
summary: "Use new MIDI defaults",
doc: "Specify new default values to be used by all subsequent calls to `midi_*` fns. Will remove and override any previous defaults.",
args: [],
opts: {},
accepts_block: false,
examples: ["
midi_note_on :e1 # Sends MIDI :e1 note_on with default opts
use_midi_defaults channel: 3, port: \"foo\"
midi_note_on :e3 # Sends MIDI :e3 note_on to channel 3 on port \"foo\"
use_midi_defaults channel: 1
midi_note_on :e2 # Sends MIDI :e2 note_on to channel 1. Note that the port is back to the default and no longer \"foo\".
"]
def with_midi_defaults(*args, &block)
raise "with_midi_defaults must be called with a do/end block" unless block
current_defs = __thread_locals.get(:sonic_pi_mod_midi_defaults)
args_h = resolve_synth_opts_hash_or_array(args)
args_h.each { |k, v| v.freeze }
__thread_locals.set :sonic_pi_mod_midi_defaults, SonicPi::Core::SPMap.new(args_h)
res = block.call
__thread_locals.set :sonic_pi_mod_midi_defaults, current_defs
res
end
doc name: :with_midi_defaults,
introduced: Version.new(3,0,0),
summary: "Block-level use new MIDI defaults",
doc: "Specify new default values to be used by all calls to `midi_*` fns within the `do`/`end` block. After the `do`/`end` block has completed the previous MIDI defaults (if any) are restored.",
args: [],
opts: {},
accepts_block: true,
requires_block: true,
examples: ["
midi_note_on :e1 # Sends MIDI :e1 note on with default opts
with_midi_defaults channel: 3, port: \"foo\" do
midi_note_on :e3 # Sends MIDI :e3 note on to channel 3 on port \"foo\"
end
use_midi_defaults channel: 1 # this will be overridden by the following
with_midi_defaults channel: 5 do
midi_note_on :e2 # Sends MIDI :e2 note on to channel 5.
# Note that the port is back to the default
end
midi_note_on :e4 # Sends MIDI :e4 note on to channel 1
# Note that the call to use_midi_defaults is now honoured.
"]
def use_merged_midi_defaults(*args, &block)
raise "use_merged_midi_defaults does not work with a block. Perhaps you meant with_midi_defaults" if block
current_defs = __thread_locals.get(:sonic_pi_mod_midi_defaults)
args_h = resolve_synth_opts_hash_or_array(args)
merged_defs = (current_defs || {}).merge(args_h)
__thread_locals.set :sonic_pi_mod_midi_defaults, SonicPi::Core::SPMap.new(merged_defs)
end
doc name: :use_merged_midi_defaults,
introduced: Version.new(3,0,0),
summary: "Merge MIDI defaults",
doc: "Specify new default values to be used by all subsequent calls to `midi_*` fns. Merges the specified values with any previous defaults, rather than replacing them",
args: [],
opts: {},
accepts_block: false,
examples: ["
midi_note_on :e1 # Sends MIDI :e1 note_on with default opts
use_midi_defaults channel: 3, port: \"foo\"
midi_note_on :e3 # Sends MIDI :e3 note_on to channel 3 on port \"foo\"
use_merged_midi_defaults channel: 1
midi_note_on :e2 # Sends MIDI :e2 note_on to channel 1 on port \"foo\".
# This is because the call to use_merged_midi_defaults overrode the
# channel but not the port which got merged in.
"]
def with_merged_midi_defaults(*args, &block)
raise "with_merged_midi_defaults must be called with a do/end block" unless block
current_defs = __thread_locals.get(:sonic_pi_mod_midi_defaults)
args_h = resolve_synth_opts_hash_or_array(args)
merged_defs = (current_defs || {}).merge(args_h)
__thread_locals.set :sonic_pi_mod_midi_defaults, SonicPi::Core::SPMap.new(merged_defs)
res = block.call
__thread_locals.set :sonic_pi_mod_midi_defaults, current_defs
res
end
doc name: :with_merged_midi_defaults,
introduced: Version.new(3,0,0),
summary: "Block-level merge midi defaults",
doc: "Specify opt values to be used by any following call to the `midi_*` fns within the specified `do`/`end` block. Merges the specified values with any previous midi defaults, rather than replacing them. After the `do`/`end` block has completed, previous defaults (if any) are restored.",
args: [],
opts: {},
accepts_block: true,
requires_block: true,
examples: ["
midi_note_on :e1 # Sends MIDI :e1 note_on with default opts
use_midi_defaults channel: 3, port: \"foo\"
midi_note_on :e3 # Sends MIDI :e3 note_on to channel 3 on port \"foo\"
with_merged_midi_defaults channel: 1 do
midi_note_on :e2 # Sends MIDI :e2 note_on to channel 1 on port \"foo\".
# This is because the call to use_merged_midi_defaults overrode the
# channel but not the port which got merged in.
end
midi_note_on :e2 # Sends MIDI :e2 note_on to channel 3 on port \"foo\".
# This is because the previous defaults were restored after
# the call to with_merged_midi_defaults.
"]
def current_midi_defaults
__thread_locals.get(:sonic_pi_mod_midi_defaults) || {}
end
doc name: :current_midi_defaults,
introduced: Version.new(3,0,0),
summary: "Get current MIDI defaults",
doc: "Returns the current MIDI defaults. This is a map of opt names to values
This can be set via the fns `use_midi_defaults`, `with_midi_defaults`, `use_merged_midi_defaults` and `with_merged_midi_defaults`.",
args: [],
opts: nil,
accepts_block: false,
examples: ["
use_midi_defaults channel: 1, port: \"foo\"
midi_note_on :e1 # Sends MIDI :e1 note on to channel 1 on port \"foo\"
current_midi_defaults #=> Prints {channel: 1, port: \"foo\"}"]
def use_midi_ports(*filters_and_procs)
if is_list_like?(filters_and_procs) && filters_and_procs.size == 1 && filters_and_procs[0] == '*'
__thread_locals.set(:sonic_pi_midi_ports, '*'.freeze)
return '*'
end
filters_and_procs = [filters_and_procs] unless is_list_like?(filters_and_procs)
string_filters = []
non_string_f_and_ps = []
filters_and_procs.each do |fp|
if fp.is_a? String
string_filters << fp
else
non_string_f_and_ps << fp
end
end
unless string_filters.empty?
string_filters.map!{|sf| Regexp.escape(sf)}
string_filter_regexp = Regexp.new('.*' + string_filters.join('.*') + '.*')
non_string_f_and_ps.unshift string_filter_regexp
end
candidates = midi_available_ports.clone.to_a
non_string_f_and_ps.each do |f|
case f
when Symbol
candidates.keep_if do |c|
c == f.to_s
end
when Regexp
candidates.keep_if do |c|
f.match c
end
when Fixnum
unless candidates.empty?
candidates = [candidates[f % candidates.size]]
end
when NilClass
# Do nothing
when Proc
raise "MIDI Port Filter Proc accepts 1 argument only. Found #{block.arity}" unless f.arity == 1
found_proc = true
candidates = f.call(candidates)
candidates = [candidates] unless is_list_like?(candidates)
else
raise "Unknown MIDI port filter type: #{f.class} - got: #{f.inspect}"
end
end
__thread_locals.set(:sonic_pi_midi_ports, candidates.freeze)
end
def current_midi_channels
__thread_locals.get(:sonic_pi_midi_channel, ['*'])
end
def current_midi_ports
__thread_locals.get(:sonic_pi_midi_ports, ['*'])
end
@@number_cache = 0.upto(127).map {|i| i.to_s}
def midi_note_on(*args)
params, opts = split_params_and_merge_opts_array(args)
opts = current_midi_defaults.merge(opts)
n, vel = *params
if rest? n
__midi_rest_message "midi_note_on :rest"
return nil
end
on_val = opts.fetch(:on, 1)
if truthy?(on_val)
n = normalise_transpose_and_tune_note_from_args(n, opts)
channels = __resolve_midi_channels(opts)
ports = __resolve_midi_ports(opts)
vel = __resolve_midi_velocity(vel, opts)
n = n.round.min(0).max(127)
chan = pp_el_or_list(channels)
port = pp_el_or_list(ports)
ports.each do |p|
channels.each do |c|
__midi_send_timed_pc("/note_on", p, c, n, vel)
end
end
__midi_message "midi_note_on #{@@number_cache[n]}, #{@@number_cache[vel]}, channel: #{chan}, port: #{port}"
else
__midi_rest_message "midi_note_on :rest, on: 0"
end
nil
end
doc name: :midi_note_on,
introduced: Version.new(3,0,0),
summary: "Send MIDI note on message",
args: [[:note, :midi], [:velocity, :midi]],
alt_args: [[[:note, :midi]]],
returns: :nil,
opts: {
channel: "MIDI channel(s) to send event on",
port: "MIDI port(s) to send to",
velocity: "Note velocity as a MIDI number.",
vel_f: "Velocity as a value between 0 and 1 (will be converted to a MIDI velocity between 0 and 127)",
on: "If specified and false/nil/0 will stop the midi note on message from being sent out. (Ensures all opts are evaluated in this call to `midi_note_on` regardless of value)."},
accepts_block: false,
doc: "Sends a MIDI Note On Event to *all* connected devices on *all* channels. Use the `port:` and `channel:` opts to indepently restrict which MIDI ports and channels are used.
Note and velocity values can be passed as a note symbol such as `:e3` or a MIDI number such as 52. Decimal values will be rounded down or up to the nearest whole number - so values between 3.5 and 4 will be rounded up to 4 and values between 3.49999... and 3 will be rounded down to 3. These values will also be clipped within the range 0->127 so all values lower than 0 will be increased to 0 and all values greater than 127 will be reduced to 127.
The `velocity` param may be omitted - in which case it will default to 127 unless you supply it as an opt via the keys `velocity:` or `vel_f:`.
You may also optionally pass the velocity value as a floating point value between 0 and 1 such as 0.2 or 0.785 (which will be linearly mapped to MIDI values between 0 and 127) using the vel_f: opt.
[MIDI 1.0 Specification - Channel Voice Messages - Note on event](https://www.midi.org/specifications/item/table-1-summary-of-midi-message)
",
examples: [
"midi_note_on :e3 #=> Sends MIDI note on :e3 with the default velocity of 12 to all ports and channels",
"midi_note_on :e3, 12 #=> Sends MIDI note on :e3 with velocity 12 to all channels",
"midi_note_on :e3, 12, channel: 3 #=> Sends MIDI note on :e3 with velocity 12 on channel 3",
"midi_note_on :e3, velocity: 100 #=> Sends MIDI note on for :e3 with velocity 100",
"midi_note_on :e3, vel_f: 0.8 #=> Scales velocity 0.8 to MIDI value 102 and sends MIDI note on for :e3 with velocity 102",
"midi_note_on 60.3, 50.5 #=> Rounds params up or down to the nearest whole number and sends MIDI note on for note 60 with velocity 51",
"midi_note_on :e3, channel: [1, 3, 5] #=> Send MIDI note :e3 on to channels 1, 3, 5 on all connected ports",
"midi_note_on :e3, port: [\"foo\", \"bar\"] #=> Send MIDI note :e3 on to on all channels on ports named \"foo\" and \"bar\"",
"midi_note_on :e3, channel: 1, port: \"foo\" #=> Send MIDI note :e3 on only on channel 1 on port \"foo\""
]
def midi_note_off(*args)
params, opts = split_params_and_merge_opts_array(args)
opts = current_midi_defaults.merge(opts)
n, vel = *params
if rest? n
__midi_rest_message "midi_note_off :rest"
return nil
end
n = normalise_transpose_and_tune_note_from_args(n, opts)
on_val = opts.fetch(:on, 1)
if truthy?(on_val)
channels = __resolve_midi_channels(opts)
ports = __resolve_midi_ports(opts)
vel = __resolve_midi_velocity(vel, opts)
n = note(n).round.min(0).max(127)
chan = pp_el_or_list(channels)
port = pp_el_or_list(ports)
ports.each do |p|
channels.each do |c|
__midi_send_timed_pc("/note_off", p, c, n, vel)
end
end
__midi_message "midi_note_off #{@@number_cache[n]}, #{@@number_cache[vel]}, channel: #{chan}, port: #{port}"
else
__midi_rest_message "midi_note_off :rest, on: 0"
end
nil
end
doc name: :midi_note_off,
introduced: Version.new(3,0,0),
summary: "Send MIDI note off message",
args: [[:note, :midi], [:release_velocity, :midi]],
alt_args: [[[:note, :midi]]],
returns: :nil,
opts: {
channel: "MIDI channel(s) to send event on as a number or list of numbers.",
port: "MIDI port(s) to send to as a string or list of strings.",
velocity: "Release velocity as a MIDI number.",
vel_f: "Release velocity as a value between 0 and 1 (will be converted to a MIDI velocity)",
on: "If specified and false/nil/0 will stop the midi note off message from being sent out. (Ensures all opts are evaluated in this call to `midi_note_off` regardless of value)."},
accepts_block: false,
doc: "Sends the MIDI note off message to *all* connected devices on *all* channels. Use the `port:` and `channel:` opts to restrict which MIDI ports and channels are used.
Note and release velocity values can be passed as a note symbol such as `:e3` or a number. Decimal values will be rounded down or up to the nearest whole number - so values between 3.5 and 4 will be rounded up to 4 and values between 3.49999... and 3 will be rounded down to 3. These values will also be clipped within the range 0->127 so all values lower then 0 will be increased to 0 and all values greater than 127 will be reduced to 127.
The `release_velocity` param may be omitted - in which case it will default to 127 unless you supply it as a named opt via the keys `velocity:` or `vel_f:`.
You may also optionally pass the release velocity value as a floating point value between 0 and 1 such as 0.2 or 0.785 (which will be mapped to MIDI values between 0 and 127) using the `vel_f:` opt.
[MIDI 1.0 Specification - Channel Voice Messages - Note off event](https://www.midi.org/specifications/item/table-1-summary-of-midi-message)
",
examples: [
"midi_note_off :e3 #=> Sends MIDI note off for :e3 with the default release velocity of 127 to all ports and channels",
"midi_note_off :e3, 12 #=> Sends MIDI note off on :e3 with velocity 12 on all channels",
"midi_note_off :e3, 12, channel: 3 #=> Sends MIDI note off on :e3 with velocity 12 to channel 3",
"midi_note_off :e3, velocity: 100 #=> Sends MIDI note on for :e3 with release velocity 100",
"midi_note_off :e3, vel_f: 0.8 #=> Scales release velocity 0.8 to MIDI value 102 and sends MIDI note off for :e3 with release velocity 102",
"midi_note_off 60.3, 50.5 #=> Rounds params up or down to the nearest whole number and sends MIDI note off for note 60 with velocity 51",
"midi_note_off :e3, channel: [1, 3, 5] #=> Send MIDI note off on :e3 to channels 1, 3, 5 on all connected ports",
"midi_note_off :e3, port: [\"foo\", \"bar\"] #=> Send MIDI note off on :e3 to on all channels on ports named \"foo\" and \"bar\"",
"midi_note_off :e3, channel: 1, port: \"foo\" #=> Send MIDI note off on :e3 only on channel 1 on port \"foo\""
]
def midi_poly_pressure(*args)
params, opts = split_params_and_merge_opts_array(args)
opts = current_midi_defaults.merge(opts)
control_num, val = *params
if rest? control_num
__midi_message "midi_poly_pressure :rest"
return nil
end
on_val = opts.fetch(:on, 1)
if truthy?(on_val)
channels = __resolve_midi_channels(opts)
ports = __resolve_midi_ports(opts)
val = __resolve_midi_val(val, opts)
control_num = note(control_num).round.min(0).max(127)
chan = pp_el_or_list(channels)
port = pp_el_or_list(ports)
ports.each do |p|
channels.each do |c|
__midi_send_timed_pc("/poly_pressure", p, c, control_num, val)
end
end
__midi_message "midi_poly_pressure #{control_num}, #{val}, port: #{port}, channel: #{chan}"
else
__midi_rest_message "midi_poly_pressure :rest, on: 0"
end
nil
end
doc name: :midi_poly_pressure,
introduced: Version.new(3,0,0),
summary: "Send a MIDI polyphonic key pressure message",
args: [[:note, :midi], [:value, :midi]],
returns: :nil,
opts: {
channel: "Channel(s) to send to",
port: "MIDI port(s) to send to",
value: "Pressure value as a MIDI number.",
val_f: "Pressure value as a value between 0 and 1 (will be converted to a MIDI value)",
on: "If specified and false/nil/0 will stop the midi poly pressure message from being sent out. (Ensures all opts are evaluated in this call to `midi_poly_pressure` regardless of value)."},
accepts_block: false,
doc: "Sends a MIDI polyphonic key pressure message to *all* connected devices on *all* channels. Use the `port:` and `channel:` opts to restrict which MIDI ports and channels are used.
Note number and pressure value can be passed as a note such as `:e3` and decimal values will be rounded down or up to the nearest whole number - so values between 3.5 and 4 will be rounded up to 4 and values between 3.49999... and 3 will be rounded down to 3.
You may also optionally pass the pressure value as a floating point value between 0 and 1 such as 0.2 or 0.785 (which will be mapped to MIDI values between 0 and 127) using the `val_f:` opt.
[MIDI 1.0 Specification - Channel Voice Messages - Polyphonic Key Pressure (Aftertouch)](https://www.midi.org/specifications/item/table-1-summary-of-midi-message)
",
examples: [
"midi_poly_pressure 100, 32 #=> Sends a MIDI poly key pressure message to control note 100 with value 32 to all ports and channels",
"midi_poly_pressure :e7, 32 #=> Sends a MIDI poly key pressure message to control note 100 with value 32 to all ports and channels",
"midi_poly_pressure 100, 32, channel: 5 #=> Sends MIDI poly key pressure message to control note 100 with value 32 on channel 5 to all ports",
"midi_poly_pressure 100, val_f: 0.8, channel: 5 #=> Sends a MIDI poly key pressure message to control note 100 with value 102 on channel 5 to all ports",
"midi_poly_pressure 100, value: 102, channel: [1, 5] #=> Sends MIDI poly key pressure message to control note 100 with value 102 on channel 1 and 5 to all ports"
]
def midi_cc(*args)
params, opts = split_params_and_merge_opts_array(args)
opts = current_midi_defaults.merge(opts)
control_num, val = *params
if rest? control_num
__midi_message "midi_cc :rest"
return nil
end
on_val = opts.fetch(:on, 1)
if truthy?(on_val)
channels = __resolve_midi_channels(opts)
ports = __resolve_midi_ports(opts)
val = __resolve_midi_val(val, opts)
control_num = note(control_num).round.min(0).max(127)
chan = pp_el_or_list(channels)
port = pp_el_or_list(ports)
ports.each do |p|
channels.each do |c|
__midi_send_timed_pc("/control_change", p, c, control_num, val)
end
end
__midi_message "midi_cc #{control_num}, #{val}, port: #{port}, channel: #{chan}"
else
__midi_rest_message "midi_cc :rest, on: 0"
end
nil
end
doc name: :midi_cc,
introduced: Version.new(3,0,0),
summary: "Send MIDI control change message",
args: [[:control_num, :midi], [:value, :midi]],
returns: :nil,
opts: {
channel: "Channel(s) to send to",
port: "MIDI port(s) to send to",
value: "Control value as a MIDI number.",
val_f: "Control value as a value between 0 and 1 (will be converted to a MIDI value)",
on: "If specified and false/nil/0 will stop the midi cc message from being sent out. (Ensures all opts are evaluated in this call to `midi_cc` regardless of value)."},
accepts_block: false,
doc: "Sends a MIDI control change message to *all* connected devices on *all* channels. Use the `port:` and `channel:` opts to restrict which MIDI ports and channels are used.
Control number and control value can be passed as a note such as `:e3` and decimal values will be rounded down or up to the nearest whole number - so values between 3.5 and 4 will be rounded up to 4 and values between 3.49999... and 3 will be rounded down to 3.
You may also optionally pass the control value as a floating point value between 0 and 1 such as 0.2 or 0.785 (which will be mapped to MIDI values between 0 and 127) using the `val_f:` opt.
[MIDI 1.0 Specification - Channel Voice Messages - Control change](https://www.midi.org/specifications/item/table-1-summary-of-midi-message)
",
examples: [
"midi_cc 100, 32 #=> Sends MIDI cc message to control 100 with value 32 to all ports and channels",
"midi_cc :e7, 32 #=> Sends MIDI cc message to control 100 with value 32 to all ports and channels",
"midi_cc 100, 32, channel: 5 #=> Sends MIDI cc message to control 100 with value 32 on channel 5 to all ports",
"midi_cc 100, val_f: 0.8, channel: 5 #=> Sends MIDI cc message to control 100 with value 102 on channel 5 to all ports",
"midi_cc 100, value: 102, channel: [1, 5] #=> Sends MIDI cc message to control 100 with value 102 on channel 1 and 5 to all ports"
]
def midi_channel_pressure(*args)
params, opts = split_params_and_merge_opts_array(args)
opts = current_midi_defaults.merge(opts)
pressure = params[0]
if params.size > 0 && rest?(pressure)
__midi_message "midi_channel_pressure :rest"
return nil
end
on_val = opts.fetch(:on, 1)
if truthy?(on_val)
channels = __resolve_midi_channels(opts)
ports = __resolve_midi_ports(opts)
pressure = __resolve_midi_val(pressure, opts)
chan = pp_el_or_list(channels)
port = pp_el_or_list(ports)
ports.each do |p|
channels.each do |c|
__midi_send_timed_pc("/channel_pressure", p, c, pressure)
end
end
__midi_message "midi_channel_pressure #{pressure}, port: #{port}, channel: #{chan}"
else
__midi_rest_message "midi_channel_pressure :rest, on: 0"
end
nil
end
doc name: :midi_channel_pressure,
introduced: Version.new(3,0,0),
summary: "Send MIDI channel pressure (aftertouch) message",
args: [[:val, :midi]],
returns: :nil,
opts: {
channel: "Channel(s) to send to",
port: "MIDI port(s) to send to",
value: "Pressure value as a MIDI number.",
val_f: "Pressure value as a value between 0 and 1 (will be converted to a MIDI value)",
on: "If specified and false/nil/0 will stop the midi channel pressure message from being sent out. (Ensures all opts are evaluated in this call to `midi_channel_pressure` regardless of value)."},
accepts_block: false,
doc: "Sends a MIDI channel pressure (aftertouch) message to *all* connected devices on *all* channels. Use the `port:` and `channel:` opts to restrict which MIDI ports and channels are used.
The pressure value can be passed as a note such as `:e3` and decimal values will be rounded down or up to the nearest whole number - so values between 3.5 and 4 will be rounded up to 4 and values between 3.49999... and 3 will be rounded down to 3.
You may also optionally pass the pressure value as a floating point value between 0 and 1 such as 0.2 or 0.785 (which will be mapped to MIDI values between 0 and 127) using the `val_f:` opt.
[MIDI 1.0 Specification - Channel Voice Messages - Channel Pressure (Aftertouch)](https://www.midi.org/specifications/item/table-1-summary-of-midi-message)
",
examples: [
"midi_channel_pressure 50 #=> Sends MIDI channel pressure message with value 50 to all ports and channels",
"midi_channel_pressure :C4 #=> Sends MIDI channel pressure message with value 60 to all ports and channels",
"midi_channel_pressure 0.5 #=> Sends MIDI channel pressure message with value 63.5 to all ports and channels",
"midi_channel_pressure 30, channel: [1, 5] #=> Sends MIDI channel pressure message with value 30 on channel 1 and 5 to all ports"
]
def midi_pitch_bend(*args)
params, opts = split_params_and_merge_opts_array(args)
opts = current_midi_defaults.merge(opts)
delta = params[0]
if params.size > 0 && rest?(delta)
__midi_message "midi_pitch_bend :rest"
return nil
end
on_val = opts.fetch(:on, 1)
if truthy?(on_val)
channels = __resolve_midi_channels(opts)
ports = __resolve_midi_ports(opts)
delta, delta_midi = __resolve_midi_deltas(delta, opts)
chan = pp_el_or_list(channels)
port = pp_el_or_list(ports)
ports.each do |p|
channels.each do |c|
__midi_send_timed_pc("/pitch_bend", p, c, delta_midi)
end
end
__midi_message "midi_pitch_bend #{delta}, delta_midi: #{delta_midi}, port: #{port}, channel: #{chan}"
else
__midi_rest_message "midi_pitch_bend :rest, on: 0"
end
nil
end
doc name: :midi_pitch_bend,
introduced: Version.new(3,0,0),
summary: "Send MIDI pitch bend message",
args: [[:delta, :float01]],
returns: :nil,
opts: {
channel: "Channel(s) to send to",
port: "MIDI port(s) to send to",
delta: "Pitch bend value as a number between 0 and 1 (will be converted to a value between 0 and 16383). No bend is the central value 0.5",
delta_midi: "Pitch bend value as a number between 0 and 16383 inclusively. No bend is central value 8192.",
on: "If specified and false/nil/0 will stop the midi pitch bend message from being sent out. (Ensures all opts are evaluated in this call to `midi_pitch_bend` regardless of value)."},
accepts_block: false,
doc: "Sends a MIDI pitch bend message to *all* connected devices on *all* channels. Use the `port:` and `channel:` opts to restrict which MIDI ports and channels are used.
Delta value is between 0 and 1 with 0.5 representing no pitch bend, 1 max pitch bend and 0 minimum pitch bend.
Typical MIDI values such as note or cc are represented with 7 bit numbers which translates to the range 0-127. This makes sense for keyboards which have at most 88 keys. However, it translates to a poor resolution when working with pitch bend. Therefore, pitch bend is unlike most MIDI values in that it has a much greater range: 0 - 16383 (by virtue of being represented by 14 bits).
* It is also possible to specify the delta value as a (14 bit) MIDI pitch bend value between 0 and 16383 using the `delta_midi:` opt.
* When using the `delta_midi:` opt no pitch bend is the value 8192
[MIDI 1.0 Specification - Channel Voice Messages - Pitch Bend Change](https://www.midi.org/specifications/item/table-1-summary-of-midi-message)
",
examples: [
"midi_pitch_bend 0 #=> Sends MIDI pitch bend message with value 0 to all ports and channels",
"midi_pitch_bend 1 #=> Sends MIDI pitch bend message with value 16383 to all ports and channels",
"midi_pitch_bend 0.5 #=> Sends MIDI pitch bend message with value 8192 to all ports and channels",
"midi_pitch_bend delta_midi: 8192 #=> Sends MIDI pitch bend message with value 8192 to all ports and channels",
"midi_pitch_bend 0, channel: [1, 5] #=> Sends MIDI pitch bend message with value 0 on channel 1 and 5 to all ports"
]
def midi_pc(*args)
params, opts = split_params_and_merge_opts_array(args)
opts = current_midi_defaults.merge(opts)
program_num = params[0]
ports = __resolve_midi_ports(opts)
on_val = opts.fetch(:on, 1)
if program_num == nil #deal with missing midi_pc paramter
return nil
end
if truthy?(on_val)
channels = __resolve_midi_channels(opts)
ports = __resolve_midi_ports(opts)
program_num = note(program_num).round.min(0).max(127)
chan = pp_el_or_list(channels)
port = pp_el_or_list(ports)
ports.each do |p|
channels.each do |c|
__midi_send_timed_pc("/program_change", p, c, program_num)
end
end
__midi_message "midi_pc #{program_num}, port: #{port}, channel: #{chan}"
else
__midi_message "midi_pc #{program_num}, on: 0"
end
nil
end
doc name: :midi_pc,
introduced: Version.new(3,0,2),
summary: "Send MIDI program change message",
args: [[:program_num, :midi]],
returns: :nil,
opts: {
channel: "Channel(s) to send to",
port: "MIDI port(s) to send to",
on: "If specified and false/nil/0 will stop the midi pc message from being sent out. (Ensures all opts are evaluated in this call to `midi_pc` regardless of value)."},
accepts_block: false,
doc: "Sends a MIDI program change message to *all* connected devices on *all* channels. Use the `port:` and `channel:` opts to restrict which MIDI ports and channels are used.
Program number can be passed as a note such as `:e3` and decimal values will be rounded down or up to the nearest whole number - so values between 3.5 and 4 will be rounded up to 4 and values between 3.49999... and 3 will be rounded down to 3.
[MIDI 1.0 Specification - Channel Voice Messages - Program change](https://www.midi.org/specifications/item/table-1-summary-of-midi-message)
",
examples: [
"midi_pc 100 #=> Sends MIDI pc message to all ports and channels",
"midi_pc :e7 #=> Sends MIDI pc message to all ports and channels",
"midi_pc 100, channel: 5 #=> Sends MIDI pc message on channel 5 to all ports",
"midi_pc 100, channel: 5 #=> Sends MIDI pc message on channel 5 to all ports",
"midi_pc 100, channel: [1, 5] #=> Sends MIDI pc message on channel 1 and 5 to all ports"
]
def midi_raw(*args)
params, opts = split_params_and_merge_opts_array(args)
opts = current_midi_defaults.merge(opts)
a, b, c = params
a = a.to_f.round
b = b.to_f.round
c = c.to_f.round
ports = __resolve_midi_ports(opts)
on_val = opts.fetch(:on, 1)
if truthy?(on_val)
ports.each do |p|
__midi_send_timed_param_3("/#{p}/raw", a, b, c)
end
port = pp_el_or_list(ports)
__midi_message "midi_raw #{a}, #{b}, #{c}, port: #{port}"
else
__midi_message "midi_raw #{a}, #{b}, #{c}, on: 0"
end
nil
end
doc name: :midi_raw,
introduced: Version.new(3,0,0),
summary: "Send raw MIDI message",
args: [[:a, :byte], [:b, :byte], [:c, :byte]],
returns: :nil,
opts: {port: "Port(s) to send the raw MIDI message events to",
on: "If specified and false/nil/0 will stop the raw midi message from being sent out. (Ensures all opts are evaluated in this call to `midi_raw` regardless of value)."},
accepts_block: false,
doc: "Sends the raw MIDI message to *all* connected MIDI devices. Gives you direct access to the individual bytes of a MIDI message. Typically this should be rarely used - prefer the other `midi_` fns where possible.
A raw MIDI message consists of 3 separate bytes - the Status Byte and two Data Bytes. These may be passed as base 10 decimal integers between 0 and 255, in hex form by prefixing `0x` such as `0xb0` which in decimal is 176 or binary form by prefixing `0b` such as `0b01111001` which represents 121 in decimal.
Floats will be rounded up or down to the nearest whole number e.g. 176.1 -> 176, 120.5 -> 121, 0.49 -> 0.
Non-number values will be automatically turned into numbers prior to sending the event if possible (if this conversion does not work an Error will be thrown).
See https://www.midi.org/specifications/item/table-1-summary-of-midi-message for a summary of MIDI messages and their corresponding byte structures.
",
examples: [
"midi_raw 176, 121, 0 #=> Sends the MIDI reset command",
"midi_raw 176.1, 120.5, 0.49 #=> Sends the MIDI reset command (values are rounded down, up and down respectively)",
"midi_raw 0xb0, 0x79, 0x0 #=> Sends the MIDI reset command",
"midi_raw 0b10110000, 0b01111001, 0b00000000 #=> Sends the MIDI reset command"
]
def midi_sysex(*args)
params, opts = split_params_and_merge_opts_array(args)
params = params.map { |p| p.to_f.round }
opts = current_midi_defaults.merge(opts)
ports = __resolve_midi_ports(opts)
on_val = opts.fetch(:on, 1)
raise "sysex messages must be at least 3 bytes long" if params.length < 3
raise "sysex messages must start with 0xf0" unless params[0] == 0xf0
raise "sysex messages must end with 0xf7" unless params[-1] == 0xf7
if truthy?(on_val)
ports.each do |p|
__midi_send_timed_param_n("/#{p}/raw", *params)
end
port = pp_el_or_list(ports)
__midi_message "midi_sysex #{params * ', '}, port: #{port}"
else
__midi_message "midi_sysex #{params * ', '}, on: 0"
end
nil
end
doc name: :midi_sysex,
introduced: Version.new(3,2,0),
summary: "Send MIDI System Exclusive (SysEx) message",
args: [],
returns: :nil,
opts: {port: "Port(s) to send the MIDI SysEx message events to",
on: "If specified and false/nil/0 will stop the midi SysEx message from being sent out. (Ensures all opts are evaluated in this call to `midi_sysex` regardless of value)."},
accepts_block: false,
doc: "Sends the MIDI SysEx message to *all* connected MIDI devices.
MIDI SysEx messages, unlike all other MIDI messages, are variable in length. They allow MIDI device manufacturers to define device-specific messages, for example loading/saving patches, or programming device features such as illuminated buttons.
Floats will be rounded up or down to the nearest whole number e.g. 176.1 -> 176, 120.5 -> 121, 0.49 -> 0.
Non-number values will be automatically turned into numbers prior to sending the event if possible (if this conversion does not work an Error will be thrown).
",
examples: [
"midi_sysex 0xf0, 0x00, 0x20, 0x6b, 0x7f, 0x42, 0x02, 0x00, 0x10, 0x77, 0x11, 0xf7 #=> Program an Arturia Beatstep controller to turn the eighth pad pink"
]
def midi_sound_off(*args)
params, opts = split_params_and_merge_opts_array(args)
opts = current_midi_defaults.merge(opts)
on_val = opts.fetch(:on, 1)
if truthy?(on_val)
ports = __resolve_midi_ports(opts)
channels = __resolve_midi_channels(opts)
port = pp_el_or_list(ports)
chan = pp_el_or_list(channels)
ports.each do |p|
channels.each do |c|
__midi_send_timed_pc("/control_change", p, c, 120, 0)
end
end
__midi_message "midi_sound_off port: #{port}, channel: #{chan}"
else
__midi_rest_message "midi_sound_off port: #{port}, channel: #{chan}, on: 0"
end
nil
end
doc name: :midi_sound_off,
introduced: Version.new(3,0,0),
summary: "Silence all MIDI devices",
args: [],
returns: :nil,
opts: {
channel: "Channel to send the sound off message to",
port: "MIDI port to send to",
on: "If specified and false/nil/0 will stop the midi sound off on message from being sent out. (Ensures all opts are evaluated in this call to `midi_sound_off` regardless of value)."},
accepts_block: false,
doc: "Sends a MIDI sound off message to *all* connected devices on *all* channels. Use the `port:` and `channel:` opts to restrict which MIDI ports and channels are used.
All oscillators will turn off, and their volume envelopes are set to zero as soon as possible.
[MIDI 1.0 Specification - Channel Mode Messages - All Sound Off](https://www.midi.org/specifications/item/table-1-summary-of-midi-message)
",
examples: [
"midi_sound_off #=> Silence MIDI devices on all ports and channels",
"midi_sound_off channel: 2 #=> Silence MIDI devices on channel 2"
]
def midi_reset(*args)
__info "Resetting MIDI Subsystems"
params, opts = split_params_and_merge_opts_array(args)
opts = current_midi_defaults.merge(opts)
reset_val = opts[:value] || opts[:val] || params[0] || 0
on_val = opts.fetch(:on, 1)
if truthy?(on_val)
ports = __resolve_midi_ports(opts)
channels = __resolve_midi_channels(opts)
port = pp_el_or_list(ports)
chan = pp_el_or_list(channels)
ports.each do |p|
channels.each do |c|
__midi_send_timed_pc("/control_change", p, c, 121, reset_val)
end
end
__midi_message "midi_reset port: #{port}, channel: #{chan}"
else
__midi_rest_message "midi_reset port: #{port}, channel: #{chan}, on: 0"
end
nil
end
doc name: :midi_reset,
introduced: Version.new(3,0,0),
summary: "Reset MIDI devices",
args: [[:value, :number]],
returns: :nil,
opts: {
channel: "Channel to send the midi reset message to",
port: "MIDI port to send to",
value: "Value must only be zero (the default) unless otherwise allowed in a specific Recommended Practice",
on: "If specified and false/nil/0 will stop the midi reset message from being sent out. (Ensures all opts are evaluated in this call to `midi_reset` regardless of value)."},
accepts_block: false,
doc: "Sends a MIDI reset all controllers message to *all* connected devices on *all* channels. Use the `port:` and `channel:` opts to restrict which MIDI ports and channels are used.
All controller values are reset to their defaults.
[MIDI 1.0 Specification - Channel Mode Messages - Reset All Controllers](https://www.midi.org/specifications/item/table-1-summary-of-midi-message)
",
examples: [
"midi_reset #=> Reset MIDI devices on all channels (and ports)",
"midi_reset channel: 2 #=> Reset MIDI devices on channel 2"
]
def midi_local_control_off(*args)
params, opts = split_params_and_merge_opts_array(args)
opts = current_midi_defaults.merge(opts)
on_val = opts.fetch(:on, 1)
if truthy?(on_val)
ports = __resolve_midi_ports(opts)
channels = __resolve_midi_channels(opts)
port = pp_el_or_list(ports)
chan = pp_el_or_list(channels)
ports.each do |p|
channels.each do |c|
__midi_send_timed_pc("/control_change", p, c, 122, 0)
end
end
__midi_message "midi_mode_local_control_off port: #{port}, channel: #{chan}"
else
__midi_rest_message "midi_mode_local_control_off port: #{port}, channel: #{chan}, on: 0"
end
nil
end
doc name: :midi_local_control_off,
introduced: Version.new(3,0,0),
summary: "Disable local control on MIDI devices",
args: [],
returns: :nil,
opts: {
channel: "Channel to send the local control off message to",
port: "MIDI port to send to",
on: "If specified and false/nil/0 will stop the midi local control off message from being sent out. (Ensures all opts are evaluated in this call to `midi_local_control_off` regardless of value)."},
accepts_block: false,
doc: "Sends a MIDI local control off message to *all* connected devices on *all* channels. Use the `port:` and `channel:` opts to restrict which MIDI ports and channels are used.
All devices on a given channel will respond only to data received over MIDI. Played data, etc. will be ignored. See `midi_local_control_on` to enable local control.
[MIDI 1.0 Specification - Channel Mode Messages - Local Control Off](https://www.midi.org/specifications/item/table-1-summary-of-midi-message)
",
examples: [
"midi_local_control_off #=> Disable local control on MIDI devices on all channels (and ports)",
"midi_local_control_off channel: 2 #=> Disable local control on MIDI devices on channel 2"
]
def midi_local_control_on(*args)
params, opts = split_params_and_merge_opts_array(args)
opts = current_midi_defaults.merge(opts)
on_val = opts.fetch(:on, 1)
if truthy?(on_val)
ports = __resolve_midi_ports(opts)
channels = __resolve_midi_channels(opts)
port = pp_el_or_list(ports)
chan = pp_el_or_list(channels)
ports.each do |p|
channels.each do |c|
__midi_send_timed_pc("/control_change", p, c, 122, 127)
end
end
__midi_message "midi_mode_local_control_on port: #{port}, channel: #{chan}"
else
__midi_rest_message "midi_mode_local_control_on port: #{port}, channel: #{chan}, on: 0"
end
nil
end
doc name: :midi_local_control_on,
introduced: Version.new(3,0,0),
summary: "Enable local control on MIDI devices",
args: [],
returns: :nil,
opts: {
channel: "Channel to send the local control on message to",
port: "MIDI port to send to",
on: "If specified and false/nil/0 will stop the midi local control on message from being sent out. (Ensures all opts are evaluated in this call to `midi_local_control_on` regardless of value)."},
accepts_block: false,
doc: "Sends a MIDI local control on message to *all* connected devices on *all* channels. Use the `port:` and `channel:` opts to restrict which MIDI ports and channels are used.
All devices on a given channel will respond both to data received over MIDI and played data, etc. See `midi_local_control_off` to disable local control.
[MIDI 1.0 Specification - Channel Mode Messages - Local Control On](https://www.midi.org/specifications/item/table-1-summary-of-midi-message)
",
examples: [
"midi_local_control_on #=> Enable local control on MIDI devices on all channels (and ports)",
"midi_local_control_on channel: 2 #=> Enable local control on MIDI devices on channel 2"
]
def midi_mode(*args)
params, opts = split_params_and_merge_opts_array(args)
opts = current_midi_defaults.merge(opts)
on_val = opts.fetch(:on, 1)
mode = opts[:mode] || params[0] || :omni_off
channels = __resolve_midi_channels(opts)
ports = __resolve_midi_ports(opts)
port = pp_el_or_list(ports)
chan = pp_el_or_list(channels)
case mode
when :omni_off
if truthy?(on_val)
ports.each do |p|
channels.each do |c|
__midi_send_timed_pc("/control_change", p, c, 124, 0)
end
end
__midi_message "midi_mode :omni_off, port: #{port}, channel: #{chan}"
else
__midi_rest_message "midi_mode :omni_off, port: #{port}, channel: #{chan}, on: 0"
end
when :omni_on
if truthy?(on_val)
ports.each do |p|
channels.each do |c|
__midi_send_timed_pc("/control_change", p, c, 125, 0)
end
end
__midi_message "midi_mode :omni_on, port: #{port}, channel: #{chan}"
else
__midi_rest_message "midi_mode :omni_on, port: #{port}, channel: #{chan}, on: 0"
end
when :mono
num_chans = opts[:num_chans] || 16
if truthy?(on_val)
ports.each do |p|
channels.each do |c|
__midi_send_timed_pc("/control_change", p, c, 126, num_chans)
end
end
__midi_message "midi_mode :mono, num_chans: #{num_chans}, port: #{port}, channel: #{chan}"
else
__midi_rest_message "midi_mode :mono, num_chans: #{num_chans}, port: #{port}, channel: #{chan}, on: 0"
end
when :poly
if truthy?(on_val)
ports.each do |p|
channels.each do |c|
__midi_send_timed_pc("/control_change", p, c, 127, 0)
end
end
__midi_message "midi_mode :poly, port: #{port}, channel: #{chan}"
else
__midi_rest_message "midi_mode :poly, port: #{port}, channel: #{chan}, on: 0"
end
else
raise "Unknown special mode for midi_mode: #{mode.inspect}. Expected one of: :omni_off, :omni_on, :mono or :poly."
end
nil
end
doc name: :midi_mode,
introduced: Version.new(3,0,0),
summary: "Set Omni/Mono/Poly mode",
args: [[:mode, :mode_keyword]],
returns: :nil,
opts: {
channel: "Channel to send the MIDI mode message to",
port: "MIDI port to send to",
mode: "Mode keyword - one of :omni_off, :omni_on, :mono or :poly",
num_chans: "Used in mono mode only - Number of channels (defaults to 16)",
on: "If specified and false/nil/0 will stop the midi local control off message from being sent out. (Ensures all opts are evaluated in this call to `midi_local_control_off` regardless of value)."},
accepts_block: false,
doc: "Sends the Omni/Mono/Poly MIDI mode message to *all* connected MIDI devices on *all* channels. Use the `port:` and `channel:` opts to restrict which MIDI ports and channels are used.
Valid modes are:
:omni_off - Omni Mode Off
:omni_on - Omni Mode On
:mono - Mono Mode On (Poly Off). Set num_chans: to be the number of channels to use (Omni Off) or 0 (Omni On). Default for num_chans: is 16.
:poly - Poly Mode On (Mono Off)
Note that this fn also includes the behaviour of `midi_all_notes_off`.
[MIDI 1.0 Specification - Channel Mode Messages - Omni Mode Off | Omni Mode On | Mono Mode On (Poly Off) | Poly Mode On](https://www.midi.org/specifications/item/table-1-summary-of-midi-message)
",
examples: [
"midi_mode :omni_on #=> Turn Omni Mode On on all ports and channels",
"midi_mode :mono, num_chans: 5 #=> Mono Mode On, Omni off using 5 channels.",
"midi_mode :mono, num_chans: 0 #=> Mono Mode On, Omni on.",
"midi_mode :mono #=> Mono Mode On, Omni off using 16 channels (the default) ."
]
def midi_all_notes_off(*args)
params, opts = split_params_and_merge_opts_array(args)
opts = current_midi_defaults.merge(opts)
on_val = opts.fetch(:on, 1)
if truthy?(on_val)
channels = __resolve_midi_channels(opts)
ports = __resolve_midi_ports(opts)
port = pp_el_or_list(ports)
chan = pp_el_or_list(channels)
ports.each do |p|
channels.each do |c|
__midi_send_timed_pc("/control_change", p, c, 123, 0)
end
end
__midi_message "midi_all_notes_off port: #{port}, channel: #{chan}"
else
__midi_rest_message "midi_all_notes_off port: #{port}, channel: #{chan}, on: 0"
end
nil
end
doc name: :midi_all_notes_off,
introduced: Version.new(3,0,0),
summary: "Turn off all notes on MIDI devices",
args: [],
returns: :nil,
opts: {
channel: "Channel to send the all notes off message to",
port: "MIDI port to send to",
on: "If specified and false/nil/0 will stop the midi all notes off message from being sent out. (Ensures all opts are evaluated in this call to `midi_all_notes_off` regardless of value)."},
accepts_block: false,
doc: "Sends a MIDI all notes off message to *all* connected MIDI devices. on *all* channels. Use the `port:` and `channel:` opts to restrict which MIDI ports and channels are used.
When an All Notes Off event is received, all oscillators will turn off.
[MIDI 1.0 Specification - Channel Mode Messages - All Notes Off](https://www.midi.org/specifications/item/table-1-summary-of-midi-message)
",
examples: [
"midi_all_notes_off #=> Turn off all notes on MIDI devices on all channels (and ports)",
"midi_all_notes_off channel: 2 #=> Turn off all notes on MIDI devices on channel 2"
]
def midi_clock_tick(*args)
params, opts = split_params_and_merge_opts_array(args)
opts = current_midi_defaults.merge(opts)
on_val = opts.fetch(:on, 1)
if truthy?(on_val)
ports = __resolve_midi_ports(opts)
port = pp_el_or_list(ports)
ports.each do |p|
__midi_send_timed("/#{p}/clock")
end
__midi_message "midi_clock_tick port: #{port}"
else
__midi_rest_message "midi_clock_tick port: #{port}"
end
nil
end
doc name: :midi_clock_tick,
introduced: Version.new(3,0,0),
summary: "Send an individual MIDI clock tick",
args: [[]],
returns: :nil,
opts: {
port: "MIDI port to send to",
on: "If specified and false/nil/0 will stop the midi clock tick message from being sent out. (Ensures all opts are evaluated in this call to `midi_clock_tick` regardless of value)."},
accepts_block: false,
doc: "Sends a MIDI clock tick message to *all* connected devices on *all* channels. Use the `port:` and `channel:` opts to restrict which MIDI ports and channels are used.
Typical MIDI devices expect the clock to send 24 ticks per quarter note (typically a beat). See `midi_clock_beat` for a simple way of sending all the ticks for a given beat.
[MIDI 1.0 Specification - System Real-Time Messages - Timing Clock](https://www.midi.org/specifications/item/table-1-summary-of-midi-message)
",
examples: [
"midi_clock_tick #=> Send an individual clock tick to all connected MIDI devices on all ports."
]
def midi_start(*args)
params, opts = split_params_and_merge_opts_array(args)
opts = current_midi_defaults.merge(opts)
on_val = opts.fetch(:on, 1)
if truthy?(on_val)
ports = __resolve_midi_ports(opts)
port = pp_el_or_list(ports)
ports.each do |p|
__midi_send_timed("/#{p}/start")
end
__midi_message "midi_start port: #{port}"
else
__midi_rest_message "midi_start port: #{port}, on: 0"
end
nil
end
doc name: :midi_start,
introduced: Version.new(3,0,0),
summary: "Send MIDI system message - start",
args: [[]],
returns: :nil,
opts: nil,
accepts_block: false,
doc: "Sends the MIDI start system message to *all* connected MIDI devices on *all* ports. Use the `port:` opt to restrict which MIDI ports are used.
Start the current sequence playing. (This message should be followed with calls to `midi_clock_tick` or `midi_clock_beat`).
[MIDI 1.0 Specification - System Real-Time Messages - Start](https://www.midi.org/specifications/item/table-1-summary-of-midi-message)
",
examples: [
"midi_start #=> Send start message to all connected MIDI devices"
]
def midi_stop(*args)
params, opts = split_params_and_merge_opts_array(args)
opts = current_midi_defaults.merge(opts)
on_val = opts.fetch(:on, 1)
if truthy?(on_val)
ports = __resolve_midi_ports(opts)
port = pp_el_or_list(ports)
ports.each do |p|
__midi_send_timed("/#{p}/stop")
end
__midi_message "midi_stop port: #{port}"
else
__midi_rest_message "midi_stop port: #{port}, on: 0"
end
nil
end
doc name: :midi_stop,
introduced: Version.new(3,0,0),
summary: "Send MIDI system message - stop",
args: [[]],
returns: :nil,
opts: {port: "MIDI Port(s) to send the stop message to"},
accepts_block: false,
doc: "Sends the MIDI stop system message to *all* connected MIDI devices on *all* ports. Use the `port:` opt to restrict which MIDI ports are used.
Stops the current sequence.
[MIDI 1.0 Specification - System Real-Time Messages - Start](https://www.midi.org/specifications/item/table-1-summary-of-midi-message)
",
examples: [
"midi_stop #=> Send stop message to all connected MIDI devices"
]
def midi_continue(*args)
params, opts = split_params_and_merge_opts_array(args)
opts = current_midi_defaults.merge(opts)
on_val = opts.fetch(:on, 1)
if truthy?(on_val)
ports = __resolve_midi_ports(opts)
port = pp_el_or_list(ports)
ports.each do |p|
__midi_send_timed("/#{p}/continue")
end
__midi_message "midi_continue port: #{port}"
else
__midi_rest_message "midi_continue port: #{port}, on: 0"
end
nil
end
doc name: :midi_continue,
introduced: Version.new(3,0,0),
summary: "Send MIDI system message - continue",
args: [[]],
returns: :nil,
opts: {port: "MIDI Port(s) to send the continue message to"},
accepts_block: false,
doc: "Sends the MIDI continue system message to *all* connected MIDI devices on *all* ports. Use the `port:` opt to restrict which MIDI ports are used.
Upon receiving the MIDI continue event, the MIDI device(s) will continue at the point the sequence was stopped.
[MIDI 1.0 Specification - System Real-Time Messages - Continue](https://www.midi.org/specifications/item/table-1-summary-of-midi-message)
",
examples: [
"midi_continue #=> Send continue message to all connected MIDI devices"
]
def midi_clock_beat(*args)
params, opts = split_params_and_merge_opts_array(args)
opts = current_midi_defaults.merge(opts)
on_val = opts.fetch(:on, 1)
if truthy?(on_val)
dur = opts[:duration] || params[0] || 1
ports = __resolve_midi_ports(opts)
port = pp_el_or_list(ports)
if dur == 1
times = [0,
0.041666666666666664,
0.08333333333333333,
0.125,
0.16666666666666666,
0.20833333333333331,
0.24999999999999997,
0.29166666666666663,
0.3333333333333333,
0.375,
0.4166666666666667,
0.45833333333333337,
0.5,
0.5416666666666666,
0.5833333333333333,
0.6249999999999999,
0.6666666666666665,
0.7083333333333331,
0.7499999999999998,
0.7916666666666664,
0.833333333333333,
0.8749999999999997,
0.9166666666666663,
0.9583333333333329]
elsif dur == 0.5
times = [0,
0.020833333333333332,
0.041666666666666664,
0.0625,
0.08333333333333333,
0.10416666666666666,
0.12499999999999999,
0.14583333333333331,
0.16666666666666666,
0.1875,
0.20833333333333334,
0.22916666666666669,
0.25,
0.2708333333333333,
0.29166666666666663,
0.31249999999999994,
0.33333333333333326,
0.3541666666666666,
0.3749999999999999,
0.3958333333333332,
0.4166666666666665,
0.43749999999999983,
0.45833333333333315,
0.47916666666666646]
else
times = (line 0, dur, steps: 24, inclusive: false)
end
ports.each do |p|
time_warp times do |i, el|
__midi_send_timed("/#{p}/clock")
end
end
__midi_message "midi_clock_beat port: #{port}"
else
__midi_rest_message "midi_clock_beat port: #{port}"
end
end
doc name: :midi_clock_beat,
introduced: Version.new(3,0,0),
summary: "Send a quarter-note's worth of MIDI clock ticks",
args: [[:duration, :beats]],
returns: :nil,
opts: {
port: "MIDI port to send to",
on: "If specified and false/nil/0 will stop the midi clock tick messages from being sent out. (Ensures all opts are evaluated in this call to `midi_clock_beat` regardless of value)."},
accepts_block: false,
doc: "Sends enough MIDI clock ticks for one beat to *all* connected MIDI devices. Use the `port:` opt to restrict which MIDI ports are used.
The MIDI specification requires 24 clock tick events to be sent per beat. These can either be sent manually using `midi_clock_tick` or all 24 can be scheduled in one go using this fn. `midi_clock_beat` will therefore schedule for 24 clock ticks to be sent linearly spread over duration beats. This fn will automatically take into account the current BPM and any `time_warp`s.
",
examples: [
"midi_clock_beat #=> Send 24 clock ticks over a period of 1 beat",
"midi_clock_beat 0.5 #=> Send 24 clock ticks over a period of 0.5 beats",
"
live_loop :clock do # Create a live loop which continually sends out MIDI clock
midi_clock_beat # events at the current BPM
sleep 1
end",
"# Ensuring Clock Phase is Correct
live_loop :clock do
midi_start if tick == 0 # Send a midi_start event the first time round the live loop only
midi_clock_beat # this will not just send a steady clock beat, but also ensure
sleep 1 # the clock phase of the MIDI device matches Sonic Pi.
end"
]
def midi(*args)
params, opts = split_params_and_merge_opts_array(args)
opts = current_midi_defaults.merge(opts)
n, vel = *params
if rest? n
__midi_rest_message "midi :rest"
return nil
end
n = normalise_transpose_and_tune_note_from_args(n, opts)
on_val = opts.fetch(:on, 1)
if truthy?(on_val)
return midi_all_notes_off(opts) if n == :off
channels = __resolve_midi_channels(opts)
ports = __resolve_midi_ports(opts)
vel = __resolve_midi_velocity(vel, opts)
sus = opts.fetch(:sustain, 1).to_f
rel_vel = opts.fetch(:release_velocity, 127)
n = n.round.min(0).max(127)
chan = pp_el_or_list(channels)
port = pp_el_or_list(ports)
ports.each do |p|
channels.each do |c|
__midi_send_timed_pc("/note_on", p, c, n, vel)
time_warp sus - 0.01 do
__midi_send_timed_pc("/note_off", p, c, n, rel_vel)
end
end
end
__midi_message "midi #{@@number_cache[n]}, #{@@number_cache[vel]}, sustain: #{sus}, port: #{port}, channel: #{chan}"
else
__midi_rest_message "midi #{@@number_cache[n]}, #{@@number_cache[vel]}, sustain: #{sus}, port: #{port}, channel: #{chan}, on: 0"
end
nil
end
doc name: :midi,
introduced: Version.new(3,0,0),
summary: "Trigger and release an external synth via MIDI",
args: [[:note, :number], ],
returns: :nil,
opts: {sustain: "Duration of note event in beats",
vel: "Velocity of note as a MIDI number",
on: "If specified and false/nil/0 will stop the midi on/off messages from being sent out. (Ensures all opts are evaluated in this call to `midi` regardless of value)."},
accepts_block: false,
doc: "Sends a MIDI note on event to *all* connected MIDI devices and *all* channels and then after sustain beats sends a MIDI note off event. Ensures MIDI trigger is synchronised with standard calls to play and sample. Co-operates completely with Sonic Pi's timing system including `time_warp`.
If `note` is specified as `:off` then all notes will be turned off (same as `midi_all_notes_off`).
",
examples: [
"midi :e1, sustain: 0.3, vel_f: 0.5, channel: 3 # Play E, octave 1 for 0.3 beats at half velocity on channel 3 on all connected MIDI ports.",
"midi :off, channel: 3 #=> Turn off all notes on channel 3 on all connected MIDI ports",
"midi :e1, channel: 3, port: \"foo\" #=> Play note :E1 for 1 beats on channel 3 on MIDI port named \"foo\" only",
"
live_loop :arp do
midi (octs :e1, 3).tick, sustain: 0.1 # repeatedly play a ring of octaves
sleep 0.125
end"
]
def __resolve_midi_channels(opts)
channels = (opts[:channel] || opts[:chan] || current_midi_channels)
if channels == '*'
return ['*']
elsif is_list_like?(channels)
return channels.map do |c|
return ['*'] if c == '*'
c.to_i.min(1).max(16)
end.uniq #requires uniq not uniq! here uniq! will return nil for [1,2,3]
else
return [channels.to_i.min(1).max(16)]
end
end
def __resolve_midi_ports(opts)
ports = (opts[:port]) || current_midi_ports
if is_list_like?(ports)
return ['*'] if ports.include?('*')
return ports
else
return [ports]
end
end
def __resolve_midi_note(n, opts={})
if n = n || opts[:note]
return note(n).round.min(0).max(127)
else
return 60
end
end
def __resolve_midi_velocity(vel, opts={})
if v = opts[:velocity] || opts[:vel] || vel
return note(v).round.min(0).max(127)
elsif v = opts[:velocity_f] || opts[:vel_f]
return (v.to_f * 127).round.min(0).max(127)
else
return 127
end
end
def __resolve_midi_val(val, opts={})
if v = opts[:value] || opts[:val] || val
return note(v).round.min(0).max(127)
elsif v = opts[:value_f] || opts[:val_f]
return (v.to_f * 127).round.min(0).max(127)
else
return 127
end
end
def __resolve_midi_deltas(delta, opts={})
if d = opts[:delta_midi]
delta_midi = d.round.min(0).max(16383)
delta = delta_midi / 16383.0
elsif d = opts[:delta] || opts[:val_f] || delta
delta = d.to_f.min(0).max(1)
delta_midi = (d * 16383).round
else
delta = 0.5
delta_midi = 8192
end
return delta, delta_midi
end
@@midi_path_cache = Hash.new {|h, k| h[k] = Hash.new() }
def __midi_send_timed_pc(path, p, c, args0, args1=nil)
c = -1 if c == '*'
path = @@midi_path_cache[p][path] || @@midi_path_cache[p][path] = "/#{p}#{path}"
if args1.nil?
__midi_send_timed_param_2 path, c, args0
else
__midi_send_timed_param_3 path, c, args0, args1
end
end
def __midi_send_timed(path)
__osc_send "localhost", @ports[:osc_midi_out_port], path
end
def __midi_send_timed_param_2(path, a, b)
__osc_send "localhost", @ports[:osc_midi_out_port], path, a, b
end
def __midi_send_timed_param_3(path, a, b, c)
__osc_send "localhost", @ports[:osc_midi_out_port], path, a, b, c
end
def __midi_send_timed_param_n(path, *args)
__osc_send "localhost", @ports[:osc_midi_out_port], path, *args
end
def __midi_message(m) __delayed_message m unless __thread_locals.get(:sonic_pi_suppress_midi_logging)
end
def __midi_rest_message(m)
__delayed_message m unless __thread_locals.get(:sonic_pi_suppress_midi_logging)
end
def __midi_system_reset(silent=false)
__info "Resetting MIDI subsystems..." unless silent
__schedule_delayed_blocks_and_messages!
@mod_sound_studio.init_or_reset_midi(silent)
end
def __midi_system_start(silent=false)
__info "Starting MIDI subsystems..." unless silent
__schedule_delayed_blocks_and_messages!
@mod_sound_studio.start_midi(silent)
end
def __midi_system_stop(silent=false)
__info "Stopping MIDI subsystems..." unless silent
__schedule_delayed_blocks_and_messages!
@mod_sound_studio.stop_midi(silent)
end
end
end
end