\ gm_instrument
\
\ a fancy class of instrument designed to drive General MIDI devices.
\ The class adds additional methods to, and deviates from the behavior
\ of hmsl's cannonical instrument class.
\
\ the instrument serves as the specification class to vl.instrument which
\ drives the yamaha vl70m synthesizer.
\
\
\ description:
\
\ BEND: ( n -- , pitch bend by n cents )
\ RAW.BEND: ( n -- , send midi pitch bend message )
\
\ TONE.ON: ( cent vol -- )
\ TONE.OFF: ( cent vol -- , just turn off last note )
\ TONE.ON.FOR: ( cent vol time -- )
\
\ PUT.NOTE.RANGE: ( lo hi -- , set effective note range of voice )
\ GET.NOTE.RANGE: ( -- lo hi )
\
\ PUT.BEND.RANGE: ( n -- , set bend range from -n to +n semitones )
\ GET.BEND.RANGE: ( -- n )
\
\ in addition to being used as a standard midi.instrument, the gm.instrument
\ class supports the use of pitch specified in cents. For example, BEND:
\ converts the pitch value in cents to midi bend values and calls RAW.BEND:
\
\ TONE.ON: and related methods take the pitch argument as the deviation (in
\ cents) from the instrument's offset value. Note that these methods assume
\ that the instrument is being used monophonically.
\
\ the gm.instrument class also wraps around the note# if it's out of range.
\
\ CONTROL: ( n ctrl -- , send control message )
\
\ PAN: ( n -- , set stereo placement of voice )
\ EXPRESSION: ( n -- , set intensity/loudness of voice )
\ BRIGHTNESS: ( n -- , set the filter cutoff frequency )
\ PUT.ATTACK: ( n -- , set attack time of amplitude envelope )
\ PUT.RELEASE: ( n -- , set release time of amplitude envelope )
\ PUT.PORTAMENTO: ( n -- , set portamento time of the voice )
\
\ the above methods send standard or semi-standard midi control messages.
\
\ the CONTROL: is a single method that can select from these contollers via
\ the "ctrl" argument. Note that the selector is not the actual midi control
\ number, but an index between 0 and gm_#ctrl.
\
\ PUT.VOLUME: ( n -- , set overall volume level of channel )
\ GET.VOLUME: ( -- n )
\
\ set/retrieve the overall volume level of the channel. Passing gm_unknown to
\ PUT.VOLUME: will bypass this functionality, and the volume level set by the
\ midi synthesizer will be unaffected.
\
\ MONO: ( -- , set the device to monophonic mode )
\ POLY: ( -- , set the device to polyphonic mode )
\
\ PORTAMENTO.ON: ( -- , turn portamento on )
\ PORTAMENTO.OFF: ( -- , turn portamento off )
\
\ note that the MONO: and POLY: methods will not output MIDI messages
\ unless/until the instrument is open.
\
\ PUT.FX: ( addr -- , assign an audio effects object )
\ GET.FX: ( -- addr )
\
\ PUT.SCALE: ( addr -- , assign scale object )
\ GET.SCALE: ( -- addr )
\
\ PUT.PATCH: ( addr -- , assign a patch object )
\ GET.PATCH: ( -- addr )
\
\ these methods assign and retrieve "helper" objects to the instrument. See
\ the files gm_fx, gm_scale and gm_patch for more information.
\
\ UPDATE: ( -- , set device to instrument's current state )
\
\ RESET: ( -- , reset controllers and pitch bend )
\ PANIC: ( -- , midi panic on instrument's midi channel )
\
\ RAW.OPEN: ( -- , called first time by open: )
\ RAW.CLOSE: ( -- , called last time by close: )
\
\ subclasses of this instrument may override the above methods.
\
\ PUT.DEVICE.ID: ( n -- , set midi device number )
\ GET.DEVICE.ID: ( -- n )
\
\ PUT.PART.NUMBER: ( ? -- )
\ GET.PART.NUMBER: ( -- ? )
\
\ the above methods are declared for any subclasses that may need them.
\
\
\ code: Han-earl Park
\ copyright 2004 buster & friends' C-ALTO Labs
\ (Valencia, July 1999 -
\ (Southampton, October 2000 -
\
\ MOD: HeP 07/25/99 Started project.
\ MOD: HeP 08/01/99 The instance variables iv-gm-fx and iv-gm-scale are
\ added to the object regardless of whether task-gm_fx
\ or task-gm_scale has been loaded.
\ MOD: HeP 08/26/99 Change name of PICTH.ON: and PITCH.OFF: methods to
\ TONE.ON: and: TONE.OFF: and add the TONE.ON.FOR: method.
\ MOD: HeP 08/27/99 Get rid of the fancy conditional compilation (these were
\ relics of an earlier, simpler design).
\ Move env_resolution from vl_instrument.
\ Add methods for assigning MIDI device and part number.
\ Passes a PUT.PATCH: to gm.scale for consistency.
\ MOD: HeP 10/04/99 Trash RESET.CTRL:
\ MOD: HeP 10/22/99 PANIC: no longer calls the RESET: method.
\ MOD: HeP 11/18/99 Implement GET.PATCH: method.
\ MOD: HeP 11/19/99 Add RAW.BEND: method, and BEND: now takes pitch bend
\ vales in cents.
\ Pitch Bend range (iv-gm-bend-range) is now stored as
\ cent values and not as semitones.
\ Implement TONE.ON: TONE.OFF: and TONE.ON.FOR: methods.
\ MOD: HeP 02/20/00 Move the semi-standard midi controller messages
\ (PUT.BRIGHTNESS: etc) from the vl.instrument to the
\ gm.instrument class.
\ Add the PUT.EXPRESSION: method.
\ MOD: HeP 02/24/00 Check whether value is 0 in PUT.PATCH: method.
\ MOD: HeP 02/26/00 The methods for midi expression and brightness are no
\ longer precede w/ PUT... since the point of these
\ methods are the (immediate) side effects.
\ Add the PAN: method for controlling stereo pan.
\ MOD: HeP 04/15/00 Move the CONTROL: method from the vl.instrument class.
\ CONTROL.ENV: remains defined in the vl.instrument since
\ it's harder to make that work with the gm.instrument.
\ MOD: HeP 06/06/00 Change order (selector#) of controllers in CONTROL: with
\ stereo pan being the last. Add expression as one of these
\ controllers.
\ Unless device state is "unknown" midi mono/poly mode, and
\ portamento on/off will be set with each UPDATE: message.
\ MOD: HeP 06/07/00 Check if open first before setting mono/poly mode in the
\ MONO: and POLY: methods.
\ MOD: HeP 07/12/00 Fix stack error in the CASE statement of CONTROL:
\ MOD: HeP 10/03/00 Redefine OPEN: to prevent it from changing the preset (as
\ defined in the midi.instrument class) before UPDATE: can
\ be called. The solution involves respecifying PUT.PRESET:
\ to call UPDATE: as the PRESET: had previously done.
\ MOD: HeP 10/10/00 Add RAW.OPEN: and RAW.CLOSE: to simplify subclassing.
\ Update comments & docs.
\ MOD: HeP 11/03/00 Prints message on OPEN: and CLOSE: if if-debug is on.
\ MOD: HeP 01/23/01 Implement midi device id# stuff.
\ MOD: HeP 01/25/01 Redefine PRESET: to avoid the midi.instrument's annoying
\ use of -1 as the "null" preset rather than 0. Still
\ backwards compatible though.
\ MOD: HeP 01/27/01 PRESET: checks if open before sending any MIDI messages.
\ MOD: HeP 02/03/01 Add MODULATION: method. Sorry for the use of a noun for
\ a method name.
\ MOD: HeP 02/05/01 Add a useful but only partly functional DETRANSLATE:
\ method.
\ MOD: HeP 04-08-04 Handles channel volume level via the GET/PUT.VOLUME:
\ methods.
\ MOD: HeP 04-09-04 Remove redundant setting of midi channel in UPDATE:
\ PUT.VOLUME: works only when instrument is OPEN:ed.
\ MOD: HeP 04-11-04 Execute the open function at the end of OPEN:
\ Device id# and part# are set to 1 during INIT:
\
\ ToDo: Add CLIP: WRAP: and BOUNCE: methods?
\ ToDo: Should REFRESH: or RESET: also call PRESET:?
\ ToDo: Call the open and close function from RAW.OPEN: and RAW.CLOSE:?
include? task-midi_plus myt:midi_plus
anew task-gm_instrument
.NEED device.debug.print
: DEVICE.DEBUG.PRINT ( $method -- )
>newline
space $.
space name: self tab ascii ( emit .class: self ascii ) emit
;
.THEN
5 value env_resolution \ control envelope resolution in ticks
6 constant gm_#ctrl \ number of available controllers
method BEND: method RAW.BEND:
method TONE.ON: method TONE.OFF:
method TONE.ON.FOR:
method PUT.NOTE.RANGE: method GET.NOTE.RANGE:
method PUT.BEND.RANGE: method GET.BEND.RANGE:
method CONTROL:
method MODULATION: method PAN:
method EXPRESSION: method BRIGHTNESS:
method PUT.ATTACK: method PUT.RELEASE:
method PUT.VOLUME: method GET.VOLUME:
method PUT.PORTAMENTO:
method PORTAMENTO.ON: method PORTAMENTO.OFF:
method MONO: method POLY:
method PUT.FX: method GET.FX:
method PUT.SCALE: method GET.SCALE:
method PUT.PATCH: method GET.PATCH:
method PUT.DEVICE.ID: method GET.DEVICE.ID:
method PUT.PART.NUMBER: method GET.PART.NUMBER:
method PANIC:
method RAW.OPEN: method RAW.CLOSE:
:class OB.GM.INSTRUMENT
;
\ midi channel#
: MIDI.INSTR.SET.CHANNEL ( -- , set midi channel# )
get.channel: self midi.channel!
;
:m PUT.CHANNEL: ( chan -- )
dup put.channel: super
\
iv-gm-fx
IF dup iv-gm-fx put.channel: []
THEN
iv-gm-scale
IF dup iv-gm-scale put.channel: []
THEN
\
drop
;m
\ midi device id#
: MIDI.INSTR.SET.ID ( -- , set midi device id# )
iv-gm-id
?dup
IF midi.device.id!
THEN
;
:m PUT.DEVICE.ID: ( n -- )
dup 00 16 within?
IF
iv=> iv-gm-id
ELSE
" put.device.id:"
" device id number must be between 1 and 16, or 0 for null values"
er_warning ob.report.error
drop
THEN
;m
:m GET.DEVICE.ID: ( -- n )
iv-gm-id
;m
\ init, default and update
:m INIT: ( -- )
init: super
\
1 iv=> iv-gm-id
1 iv=> iv-gm-part
;m
:m DEFAULT: ( -- )
default: super
\
24 put.#voices: self \ minimum GM polyphony
\
0 iv=> iv-gm-bend
200 iv=> iv-gm-bend-range \ default GM specification
\
gm_unknown iv=> iv-gm-mono-mode?
gm_unknown iv=> iv-gm-portamento-on?
\
0 iv=> iv-gm-modulation
64 iv=> iv-gm-portamento-time
64 iv=> iv-gm-attack-time
64 iv=> iv-gm-decay-time
64 iv=> iv-gm-expression
64 iv=> iv-gm-brightness
64 iv=> iv-gm-pan
\
gm_unknown iv=> iv-gm-volume
\
60 put.offset: self \ middle C
36 iv=> iv-gm-note-lo \ low C on standard 5 octave keyboard
61 iv=> iv-gm-note-range \ 5 octaves + 1 semitone
;m
:m UPDATE: ( -- )
iv-ins-#open
IF midi.instr.set.channel
\
iv-gm-bend-range 100 / midi.bend.range
\
iv-gm-mono-mode? gm.not.unknown=
IF iv-gm-mono-mode?
IF midi.mono.mode ELSE midi.poly.mode
THEN
THEN
\
iv-gm-portamento-on? gm.not.unknown=
IF iv-gm-portamento-on?
IF midi.portamento.on ELSE midi.portamento.off
THEN
THEN
\
iv-gm-volume gm.not.unknown=
IF iv-gm-volume midi.volume
THEN
\
0 iv=> iv-gm-modulation
64 iv=> iv-gm-portamento-time
64 iv=> iv-gm-attack-time
64 iv=> iv-gm-decay-time
64 iv=> iv-gm-expression
64 iv=> iv-gm-brightness
64 iv=> iv-gm-pan
THEN
;m
\ open and close instrument
:m RAW.OPEN: ( -- )
self update: []
;m
:m RAW.CLOSE: ( -- )
;m
:m OPEN: ( -- )
\
\ except for the execution of the open function, below is the instrument class' OPEN:
\ method. We execute the open function at the end.
\
1 iv+> iv-ins-#open
limit: self iv-ins-#voices 1+ <
IF iv-ins-#voices 1+ new: self ( allocate space for note tracking )
THEN
iv-ins-mute
IF if-debug @
IF name: self ." muted!" cr
THEN
THEN
\
\ the midi.instrument class calls PRESET: in its open: method. this would
\ interfere with the call to the update: method, so we rewrite the method
\ here without the calling PRESET: here.
\
iv-ins-channel 0<
IF get.channel.range: self allocate.range: midi-allocator
IF
iv=> iv-ins-channel
ELSE
iv-ins-chan-lo dup iv=> iv-ins-channel
mark: midi-allocator
THEN
false iv=> iv-ins-chanset
ELSE
iv-ins-#open 1 = \ if open for first time
IF
iv-ins-channel mark: midi-allocator
true iv=> iv-ins-chanset
THEN
THEN
\
\ gm.instrument specific...
\
iv-ins-#open 1 = \ if open for first time
IF
if-debug @
IF " open:" device.debug.print
THEN
\
iv-ins-preset self preset: []
self RAW.OPEN: []
THEN
\
iv-gm-scale
IF iv-gm-scale open: []
THEN
iv-gm-fx
IF iv-gm-fx open: []
THEN
\
self iv-ins-open-cfa if.exec|drop \ execute the open function
;m
:m CLOSE: ( -- )
close: super
\
iv-ins-#open 0= \ if closed for last time
IF
if-debug @
IF " close:" device.debug.print
THEN
\
iv-ins-preset self preset: []
self RAW.CLOSE: []
THEN
\
iv-gm-scale
IF iv-gm-scale close: []
THEN
iv-gm-fx
IF iv-gm-fx close: []
THEN
;m
\ scale tuning
:m PUT.SCALE: ( addr -- , assign scale object )
get.channel: self
IF get.channel: self over put.channel: []
THEN
iv-ins-#open
IF iv-gm-scale
IF iv-gm-scale close: []
THEN
dup open: []
THEN
iv=> iv-gm-scale
;m
:m GET.SCALE: ( -- addr )
iv-gm-scale
;m
\ audio effects
:m PUT.FX: ( addr -- , assign an audio effects object )
get.channel: self
IF get.channel: self over put.channel: []
THEN
iv-ins-#open
IF iv-gm-fx
IF iv-gm-fx close: []
THEN
dup open: []
THEN
iv=> iv-gm-fx
;m
:m GET.FX: ( -- addr )
iv-gm-fx
;m
\ "note" playing
:m PUT.NOTE.RANGE: ( lo hi -- , set effective note range of voice )
-2sort
dup iv=> iv-gm-note-lo
- iv=> iv-gm-note-range
;m
:m GET.NOTE.RANGE: ( -- lo hi )
iv-gm-note-lo iv-gm-note-range over +
;m
:m TRANSLATE: ( note_index -- note , additionally wrap around range )
translate: super
\
iv-gm-note-lo - iv-gm-note-range mod
iv-gm-note-lo +
;m
:m DETRANSLATE: ( note -- note_index true | false )
iv-gm-note-lo - iv-gm-note-range mod
iv-gm-note-lo +
\
detranslate: super
;m
:m NOTE.ON: ( note_index vol -- )
iv-ins-mute
IF
2drop
ELSE
>r self translate: [] dup r> \ late bound!
dup
IF
many: self iv-ins-#voices = \ turn one off if full
IF first.note.off: self
THEN
self raw.note.on: []
add: self
ELSE
self raw.note.off: []
delete: self
THEN
THEN
;m
:m NOTE.OFF: ( note_index vol -- )
>r self translate: [] dup r> \ late bound!
self raw.note.off: []
delete: self
;m
\ pitch bend
: GM.CENTS->BEND ( cent -- pBend , convert cent value to pitch bend )
$ 2000 * iv-gm-bend-range /
$ -2000 $ 1fff clipto
;
:m RAW.BEND: ( n -- , send midi pitch bend message )
midi.instr.set.channel
dup midi.pitch.bend
iv=> iv-gm-bend
;m
:m BEND: ( n -- , pitch bend by n cents )
gm.cents->bend self raw.bend: []
;m
:m PUT.BEND.RANGE: ( n -- , set bend range from -n to +n semitones )
midi.instr.set.channel
dup midi.bend.range
100 * iv=> iv-gm-bend-range
;m
:m GET.BEND.RANGE: ( -- n )
iv-gm-bend-range 100 /
;m
\ cent based playing
\
\ TONE.ON: and related methods take the pitch argument as the deviation (in
\ cents) from the instrument's offset value. Note that these methods assume
\ that the instrument is being used monophonically.
: GM.CENTS->NOTE ( cent -- note# cent , nearest midi note number )
100 /mod swap \ -- note# mod
dup abs 50 >
IF dup 0<
IF
100 +
swap 1-
ELSE
100 - \ -- note# mod
swap 1+ \ -- mod note#+1
THEN
swap
THEN
;
: GM.TONE.ON ( cent vol -- , play note and send pitch bend )
swap gm.cents->note \ -- vol note# cent
bend: self
swap note.on: self
;
: GM.TONE.OFF ( cent vol -- )
swap gm.cents->note \ -- vol note# cent
drop swap note.off: self
0 bend: self
;
:m TONE.ON: ( cent vol -- )
iv-ins-mute
IF
2drop
ELSE
GM.TONE.ON
THEN
;m
:m TONE.OFF: ( cent vol -- , just turn off last note )
GM.TONE.OFF
;m
:m TONE.ON.FOR: ( cent vol time -- )
>r
2dup self tone.on: []
r> vtime@ >r vtime+! \ advance virtual time
self tone.off: []
r> vtime! \ restore virtual time
;m
\ controllers
: GM.CTRL.REPORT.ERROR ( $ -- , print error message )
" unrecognised controller type" er_warning ob.report.error
;
:m CONTROL: ( n ctrl -- , send control message )
midi.instr.set.channel
\
CASE
0 OF dup midi.expression iv=> iv-gm-expression ENDOF
1 OF dup midi.brightness iv=> iv-gm-brightness ENDOF
2 OF dup midi.attack.time iv=> iv-gm-attack-time ENDOF
3 OF dup midi.release.time iv=> iv-gm-decay-time ENDOF
4 OF dup midi.portamento.time iv=> iv-gm-portamento-time ENDOF
5 OF dup midi.pan iv=> iv-gm-pan ENDOF
\
" control:" GM.CTRL.REPORT.ERROR
ENDCASE
;m
\ envelope control
:m PUT.ATTACK: ( n -- , set attack time of amplitude envelope )
midi.instr.set.channel
dup midi.attack.time iv=> iv-gm-attack-time
;m
:m PUT.RELEASE: ( n -- , set release time of amplitude envelope )
midi.instr.set.channel
dup midi.release.time iv=> iv-gm-decay-time
;m
\ overall channel level
:m PUT.VOLUME: ( n -- , set channel volume level )
dup gm.not.unknown=
iv-ins-#open AND
IF midi.instr.set.channel
dup midi.volume
THEN
iv=> iv-gm-volume
;m
:m GET.VOLUME: ( -- n )
iv-gm-volume
;m
\ general purpose "modulation"
:m MODULATION: ( n -- , set vibrato depth or who knows what )
midi.instr.set.channel
dup midi.modulation iv=> iv-gm-modulation
;m
\ midi "expression" control
:m EXPRESSION: ( n -- , set intensity/loudness of voice )
midi.instr.set.channel
dup midi.expression iv=> iv-gm-expression
;m
\ timbre control
:m BRIGHTNESS: ( n -- , set the filter cutoff frequency )
midi.instr.set.channel
dup midi.brightness iv=> iv-gm-brightness
;m
\ pan control
:m PAN: ( n -- , set stereo placement of voice )
midi.instr.set.channel
dup midi.pan iv=> iv-gm-pan
;m
\ portamento switch and time
:m PORTAMENTO.ON: ( -- , turn portamento on )
midi.instr.set.channel
midi.portamento.on true iv=> iv-gm-portamento-on?
;m
:m PORTAMENTO.OFF: ( -- , turn portamento off )
midi.instr.set.channel
midi.portamento.off false iv=> iv-gm-portamento-on?
;m
:m PUT.PORTAMENTO: ( n -- , set portamento time of the voice )
midi.instr.set.channel
dup midi.portamento.time iv=> iv-gm-portamento-time
;m
\ monophonic/polyphonic modes
:m MONO: ( -- , set the device to monophonic mode )
02 put.#voices: self
true iv=> iv-gm-mono-mode?
iv-ins-#open
IF midi.instr.set.channel midi.mono.mode
THEN
;m
:m POLY: ( -- , set the device to polyphonic mode )
false iv=> iv-gm-mono-mode?
iv-ins-#open
IF midi.instr.set.channel midi.poly.mode
THEN
;m
\ preset and patch
:m PRESET: ( p# -- , change midi preset )
iv-ins-#open
IF
dup
1 128 within?
IF
midi.instr.set.channel
midi.preset
ELSE
drop
THEN
ELSE
drop
THEN
;m
:m PUT.PRESET: ( p# -- , select preset and update )
put.preset: super
\
self update: [] \ late bound!
\
iv-gm-fx
IF iv-gm-fx update: []
THEN
iv-gm-scale
IF iv-gm-scale update: []
THEN
;m
:m PUT.PATCH: ( addr -- , assign a patch object )
dup iv=> iv-gm-patch
\
iv-gm-fx
IF dup iv-gm-fx put.patch: []
THEN
iv-gm-scale
IF dup iv-gm-scale put.patch: []
THEN
\
?dup
IF
dup get.offset: [] put.offset: self
dup get.note.range: [] put.note.range: self
\
dup get.bend.range: [] 100 * iv=> iv-gm-bend-range \ don't update!
\
get.preset: [] put.preset: self
THEN
;m
:m GET.PATCH: ( -- addr )
iv-gm-patch
;m
\ reset and panic
:m RESET: ( -- , reset controllers and pitch bend )
iv-gm-patch
IF iv-gm-patch self put.patch: [] \ late bound!
THEN
\
midi.instr.set.channel midi.reset.ctrl
\
00 iv=> iv-gm-bend
;m
:m PANIC: ( -- , midi panic on instrument's midi channel )
midi.instr.set.channel midi.alloff
all.off: self
;m
\ print
: GM.CHECK.PRINT ( n -- flag , check if value has been set )
gm.not.unknown=
IF true ELSE 2 spaces ." ?" false
THEN
;
:m PRINT: ( -- )
print: super
." Device id# = " iv-gm-id 4 .r cr
\
?pause
\
." Pitch bend = " iv-gm-bend 4 .r cr
." range = " iv-gm-bend-range 100 / 4 .r cr
." Note Range = " iv-gm-note-lo 4 .r
iv-gm-note-range iv-gm-note-lo + 4 .r cr
\
space ." chan volume = "
iv-gm-volume gm.check.print
IF iv-gm-volume 3 .r THEN cr
\
space ." mono/poly = "
iv-gm-mono-mode? gm.check.print
IF iv-gm-mono-mode? IF ." mono" ELSE ." poly" THEN
THEN cr
\
space ." portamento = "
iv-gm-portamento-on? gm.check.print
IF iv-gm-portamento-on? IF space ." on" ELSE ." off" THEN
THEN
\
tab ." porta time = "
iv-gm-portamento-time gm.check.print
IF iv-gm-portamento-time 3 .r THEN cr
\
space ." attack time = "
iv-gm-attack-time gm.check.print
IF iv-gm-attack-time 3 .r THEN
\
tab ." decay time = "
iv-gm-decay-time gm.check.print
IF iv-gm-decay-time 3 .r THEN cr
\
space ." modulation = "
iv-gm-expression gm.check.print
IF iv-gm-modulation 3 .r THEN cr
\
space ." expression = "
iv-gm-expression gm.check.print
IF iv-gm-expression 3 .r THEN
\
tab ." brightness = "
iv-gm-brightness gm.check.print
IF iv-gm-brightness 3 .r THEN
\
tab ." stereo pan = "
iv-gm-pan gm.check.print
IF iv-gm-pan 3 .r THEN cr
\
." Scale = " iv-gm-scale ob.name cr
." Audio effects = " iv-gm-fx ob.name cr
." Patch = " iv-gm-patch ob.name cr
;m
;class
: OB.GM.INSTR ( -- )
ob.gm.instrument
;