\ 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 ;