Dr.Godfried-Willem RAES

Kursus Experimentele Muziek: Boekdeel 1: Algoritmische Kompositie

Hogeschool Gent - Departement Muziek en Drama


Terug naar de inhoudstafel

 

1121:

MIDI-real time for the IBM-PC

 

In het vorig paragraafje behandelden we in eerste plaats de Atari machine onder GFA-basic. Een goede procedure -een funktie eigenlijk- die binnen QB4.5 of QBX V7.1 bruikbaar is op IBM achtige PC's is terug te vinden in een van de vorige hoofdstukken. (Procedures Uit(), Funktie Min%()). Verder volgt een uitgewerkt programmavoorbeeld voor real-time I/O op IBM-PC's. Het programma maakt geen gebruik van de hardware interrupt die de midi kaart genereert wanneer een byte binnenloopt op de input, maar implementeert een interrupt via software polling. Dit maakt de software erg eenvoudig en verlost je van de anders opduikende noodzaak om (in assembler) een interrupt driver te schrijven. 

Wanneer je gewoon iets op een midi-keyboard wil spelen en dit in de komputer opslaan, raad ik -praktisch gezien- de studenten aan daarvoor bestaande sequencer-software te gebruiken: bvb. CakePro Professional onder Dos 6.0 op IBM-PC's of Cubase op Atari's. De door deze programmas aangemaakte bestanden kunnen in muzieknotatieprogrammas zoals NoteProcessor of Finale worden ingelezen.

Wil je zelf een programma schrijven dat inkomende midi-informatie in een softwarekompatibele vorm naar disk schrijft, dan moet je eerst en vooral goed de midi-standaard voor sequencer bestanden (Midi-File format) grondig bestuderen. De dokumentatie is beschikbaar in de klas.

Dat we hier toch het inlezen van midi-informatie en de procedures om dit te doen behandelen, is omdat dit ons de boeiende mogelijkheid geeft software te ontwikkelen waarbij komposities op een interaktieve manier in real-time kunnen worden gedefinieerd. Dankzij deze (en andere) mogelijkheden hoeven we met onze algoritmische muziek dus helemaal niet aangewezen te zijn op weergave via een komputer of erger nog, een bandje, bij koncerten. In kombinatie met een input-device kunnen we ook op het podium beslist interpretatief zinnige dingen doen. Dit input-device hoeft daarbij geenszins noodzakelijk een keyboard te zijn: Een midi-pedaal, een wind-controller, drumpads, een klassiek instrument aangesloten via een pitch-to-midi konverter, eigen ontwerpen... zijn allemaal bruikbaar. Meer nog, het inzetten van komputers in interaktieve musiceersituaties KAN onze interpretatieve en technische mogelijkheden zelfs op ongehoorde wijze doen toenemen in vergelijking met het klassieke bespelen van instrumenten.

Een voorbeeld-programma, waarbij -op een IBM-familie PC met een MPU of Musicquest interface- simultaan binnenkomende midi-informatie wordt verwerkt en door de komputer gegenereerde instrukties worden uitgestuurd, ziet eruit alsvolgt:

 

' ***************************************************************************

' * IO.BAS *

' * filename: IO.BAS *

' * demonstratieprogramma voor het gelijktijdig gebruiken van inkomende midi*

' * data en uitgaande data via eenzelfde interface. *

' ***************************************************************************

Midi-berichten worden alleen naar midi-out doorgegeven wanneer ze een volledig geldig midibericht vormen. Dit is een absolute vereiste! Wanneer je de inkomende midi-informatie direkt weer uitstuurt, dan kunnen midi-berichten in hun vastgelegde volgorde verstoord worden en aanleiding geven tot de meest onvoorspelbare rezultaten.

In dit programma wordt gebruik gemaakt van een ringteller (een Fifo- buffer geheugen). De afmetingen van deze buffer moeten minstens evengroot zijn als die van de hardware-buffer in het midi-interface (Voor de Roland MPU401 is deze buffer in UART-mode ca. 1747 bytes groot). In dit programma bepaalden we de grootte op de eerstvolgende macht van 2 verminderd met 1, (&H800-1= &H7FF, of 2047 decimaal) omdat dit ons toelaat een ringteller te bouwen zonder gebruik te moeten maken van een deling (\ en/of MOD).

 

COMMON SHARED i%

COMMON SHARED Fifo%()

 

DECLARE SUB IO (b%)

 

DIM SHARED Fifo%(0 TO &H7FF)

SHELL "C:\bc7\bom\mpuuart"

 

We gebruiken hier het afzonderlijk gekompileerde programma waarmee de MPU in UART mode wordt gebracht. Aangezien dit in al onze programmas gebruikt wordt, is het efficienter dit op deze wijze op te lossen. Ook kan nu binnen DOS het interface in UART gezet worden via het eenvoudige kommando MPUUART. We moet je natuurlijk de directory en path struktuur aanpassen aan je eigen komputersysteem.

 

' bepaal enkele konstanten.

CONST True=-1, False=NOT(True), Dummybyte=&H100

 

Merk op -op dit punt zijn de Basic manuals niet erg expliciet- dat konstanten automatisch SHARED zijn tussen hoofdmodule en alle procedures. Ook hun type dient niet expliciet te worden gedeklareerd. [In PowerBasic bestaat de deklaratie CONST niet. Daar kunnen -uitsluitend integers- als konstante gedeklareerd en bepaald worden door de naam van de konstante te laten voorafgaan door een type. (vb: %True = -1, %False= 0, %Dummybyte = &H100)] .De kompiler zorgt daarvoor in funktie van de voor de konstante bij de deklaratie ingevulde waarde. De konstante dummybyte wordt gebruikt om de IO procedure te kunnen pollen ook wanneer geen eigenlijke midi-informatie moet worden uitgestuurd.

 

' maak de hardware buffer van het interface leeg:

PRINT "BUFFER="

DO: PRINT INP(&H330); : LOOP UNTIL INP(&H331) AND 128

PRINT

 

De hier in de lus geplaatste print instruktie is uiteraard alleen bedoeld om te demonstreren hoe de buffer effektief wordt uitgelezen. Uiteraard is alle informatie die in de buffer staat, verloren. Het programma begint pas na het leegmaken van deze buffer.

 ' reset en initializeer de input-variabelen:

Micnt& = 0

i% = 0

j% = 0

 ' leg ritmische gegevens vast voor de door de komputer zelfstandig te genereren nootinformatie (uiteraard is dit alleen bij wijze van voorbeeld hier opgenomen. De uit te sturen informatie kan op eender welke algoritmische of input-informatie afhankelijke wijze worden bepaald):

periodetijd! = .3

duur! = .2

t! = TIMER + periodetijd!

d! = TIMER + duur!

' duur! moet uiteraard kleiner zijn dan periodetijd!

Begin van de hoofdlus van het programma. Merk op dat de eerste instruktie na het openen van de lus, de IO procedure oproept met een dummybyte. Door deze konstruktie wordt een software interrupt door polling geimplementeerd. De i% teller verwijst telkens naar de plaats waar het recentst binnengekomen byte in de fifo-buffer staat, terwijl de j-teller verwijst naar het laatste in de uitleeslus gelezen byte.

 

DO

IO (dummybyte)

WHILE j% <> i%

SELECT CASE Fifo%(j%)

CASE 254

' midi-timecode...increment counter

Micnt& = (Micnt& + 1) AND &H7FFF

' midi-klok informatie wordt in de voorbeeldprogramma

' niet gebruikt.

CASE IS >= 240

' disregard...

status% = Fifo%(j%)

CASE IS >= 224

' pitch-wheel - paired bytes do follow as msb-lsb

msbstat% = 0: lsbstat% = 0

status% = Fifo%(j%) AND &HF0: pwkanaal% = status% AND &HF

CASE IS >= 208

' disregard

status% = Fifo%(j%) AND &HF0: kanaal% = status% AND &HF

CASE IS >= 192

disregard '

status% = Fifo%(j%) AND &HF0: kanaal% = status% AND &HF

CASE IS >= 172

' programm-change

' disregard

status% = Fifo%(j%) AND &HF0: kanaal% = status% AND &HF

CASE IS >= 160

status% = Fifo%(j%) AND &HF0: kanaal% = status% AND &HF

CASE IS >= 144

'note-on command

nootstat% = 0: velostat% = 0

status% = Fifo%(j%) AND &HF0: onkanaal% = status% AND &HF

CASE IS >= 128

'note-off command

nootstat% = 0: velostat% = 0

status% = Fifo%(j%) AND &HF0: offkanaal% = status% AND &HF

CASE IS >= 0

 

SELECT CASE status%

CASE 224

' pitchwheelinformatie:

IF msbstat% = 0 THEN

msb% = Fifo%(j%)

msbstat% = -1

ELSE

lsb% = Fifo%(j%)

' nu pas is het bericht volledig en kan het ononderbroken uitgestuurd worden:

IO (224 + pwkanaal%)

IO (msb%)

IO (lsb%)

msbstat% = 0: lsbstat% = 0

END IF

CASE 144

IF nootstat% = 0 THEN

noot% = Fifo%(j%)

nootstat% = -1

ELSE

velo% = Fifo%(j%)

' nu is het bericht kompleet en kan het direkt terug uitgestuurd worden:

IO (144 + onkanaal%)

IO (noot%)

IO (velo%)

nootstat% = 0: velostat% = 0

END IF

CASE 128

IF nootstat% = 0 THEN

noot% = Fifo%(j%)

nootstat% = -1

ELSE

velo% = Fifo%(j%)

' nu is het bericht kompleet en kan het direkt terug uitgestuurd worden:

IO (144 + offkanaal%)

IO (noot%)

IO (velo%)

nootstat% = 0: velostat% = 0

END IF

END SELECT

CASE ELSE

' disregard - not a valid midi-byte

END SELECT

j% = ABS(NOT j%) AND &H7FF

' deze konstruktie is ekwivalent met j%=(j%+1) MOD 2048

WEND

IF TIMER > t! THEN

t! = TIMER + periodetijd!

d! = t! + duur!

IO (145): unoot% = 24 + (i% MOD 48)

IO (unoot%)

IO (78)

END IF

IF TIMER > d! THEN

IO (129): IO (unoot%): IO (0)

d! = TIMER + 10

END IF

LOOP UNTIL INKEY$ <> ""

 

Simultane I/O Procedure:

 

Dit is de universeel bruikbare procedure voor MPU-achtige interfaces gebruikt in real-time I/O mode. Om het hoe en waarom hiervan goed te begrijpen moet je teruggaan naar de hardware dokumentatie bij dit interface.

Merk op dat dit alleen werkt wanneer het interface in UART-mode wordt gebruikt.

 

SUB IO (b%)

IF (NOT (INP(&H331) OR 127) AND 128) THEN

DO

Fifo%(i%) = INP(&H330): i% = ABS(NOT i%) AND &H7FF

LOOP UNTIL INP(&H331) AND 128

END IF

IF NOT b% AND dummybyte THEN WAIT &H331, 64, 64: OUT &H330, b%

END SUB

 

Opmerkingen:

de konstruktie

i%=ABS(NOT i%) AND &H7FF

is funktioneel volkomen ekwivalent met het meer leesbare:

i%=(i%+1) MOD &H800.

De instruktie NOT, keert alle bits in de operand om en telt 1 op bij het rezultaat (2's komplement). Het rezultaat is nu een negatief getal aangezien het MSB 1 is. Door hiervan de absolute waarde te nemen bekomen we een binaire inkrementeerroutine ekwivalent aan i%=i%+1.

Door nu op het rezultaat de AND funktie met &H7FF toe te passen, (dit is het binaire getal 0111 1111 1111) wordt bereikt dat deze teller wanneer hij aan

&H800 gekomen is, terugspringt naar 0. Immers:

0111 1111 1111

AND 1000 0000 0000

= 0000 0000 0000

Dit is funktioneel ekwivalent met i%= i% MOD &H100. (&H100= 256)

Doordat hierbij alleen Boole funkties gebruikt worden, loopt dit aanzienlijk sneller dan onder gebruikmaking van rekenkundige instrukties zoals optellen, aftrekken, vermenigvuldigen, delen.

Een ervaren programmeur zal wanneer ook maar enigszins mogelijk, gebruik maken van Boole-operaties en funkties, omdat deze het dichtst staan bij de opkodes van de processor zelf en in het geringste aantal klokcycli worden uitgevoerd.

 

In bovenstaand programma is nog verder gaande snelheidsverhoging haalbaar, door het Fifo% buffergeheugen niet als een array te programmeren, doch door de inkomende bytes rechtstreeks naar vaste geheugenadressen weg te schrijven via POKE instrukties. Hierbij een algemeen voorbeeld:

 DEFINT A-Z

CLS

' dimensioneer een (statisch) array van integers.Elk integer neemt 2 bytes in in het geheugen

DIM Fifo%(255)

 

' zoek het beginadres en het geheugensegment op van dit statisch array:

Adress% = VARPTR(Fifo%(0))

Segment% = VARSEG(Fifo%(0))

' kontroleer dit:

PRINT Adress%; Segment%, HEX$(Segment%); ":"; HEX$(Adress%)

PRINT

 

' stel nu het geheugen in op dit segment:

DEF SEG = Segment%

Poke nu een reeks bytes in dit geheugenbereik. Merk op dat er nu tweemaal meer bytes in het array zouden kunnen, aangezien we het geheugen byte per byte gebruiken. Om het array later echter toch normaal te kunnen uitlezen, schrijven we hier om de 2 plekken een cijfer weg.

msb% = 0

 

FOR i% = Adress% TO Adress% + (UBOUND(Fifo%, 1) * 2) STEP 2

POKE i%, lsb%: POKE i% + 1, msb%: lsb% = lsb% + 1

NEXT i%

' reset het oorspronkelijke geheugensegment:

DEF SEG

' lees nu het array op de konventionele wijze:

FOR i% = 0 TO 255

PRINT Fifo%(i%);

NEXT i%

PRINT

' lees het array via een peek instruktie:

DEF SEG = Segment%

FOR i% = Adress% TO Adress% + (UBOUND(Fifo%, 1) * 2) STEP 2

PRINT PEEK(i%);

NEXT i%

PRINT

DEF SEG

wil je 'words' (2-bytes) inlezen , dan gaat dat alsvolgt:

'FOR i% = Adress% TO Adress% + (UBOUND(Fifo%, 1) * 2) STEP 2

' lsb% = PEEK(i%) : msb% = PEEK(i% + 1)

' getal% = (msb% * 256) + lsb%

' PRINT getal%;

'NEXT i%

'DEF SEG


Terug naar de inhoudstafel: <Index-kursus>

Naar homepage dr.Godfried-Willem RAES