ThinkC Programmatically (in C) send MIDI out signal from modem port, SE/30

Relating to ThinkC Development

Mu0n

Active Tinkerer
Oct 29, 2021
609
560
93
Quebec
www.youtube.com
Let's get what I'm using as gear out of the way:

Dev Machine:
modern PC with Basilisk II set with System 7.5.5, Symantec C++ 6.0

Target Machine:
SE/30 with System 7.5.3, no Apple MIDI Manager (prefer not to use it since I want to target 6.0.8 and a Mac Plus as well)
PocketMac MIDI interface plugged in the modem port, has 2 MIDI OUT (only using 1) 1 MIDI IN (not using it...for now)
Roland SC-88ST to accept midi signals from the mac and play stuff

Goals:
1) at least play a scale on channel #0, in sync with NoteOn and NoteOff effects
2) load a .mid file and play it using a fine-grain enough delay function as part of a game title screen at first - I'd adapt a python project I have that already works on a modern PC.
3) same as 2, but use interrupt+callback functions to allow game logic, graphics, inputs, not sure how far I can push the old 68k macs (it was painful in Space Quest III on the Mac Plus but there was a lot going with their interpreter as well)

Progress made:
I've already made tons of tests with some python code running on my PC, I can parse a .mid file and deal with the 2 main cases of a type 0 .mid file (every data stuck in 1 track) and type 1 .mid file (info separated into a bunch of tracks, typically 4-8, but can go up to 22 sometimes) and using a delay function to sit idle and respect the events' time delta-to-go information. If you're curious, my code sits at: https://github.com/Mu0n/PythonMidiPlayer

I've settled on using documentation from here:
Inside Macintosh: Serial Driver

Needs:
I need the extra step to make it MIDI compliant and well synced.
1719603138321.png

(source of this capture: https://ccrma.stanford.edu/~gary/controllers/midi.html#:~:text=MIDI is transmitted as asynchronous,is a "data" byte.)

But, SerRest, PBWrite, OpenDriver and such from the inside mac reference above allow me these settings:
1719603147709.png



The first configuration I tried was the default one from the documentation:
9600 bps + 1 stop bit + no parity + 8 data bits and I was met with complete silence.

2nd attempt was to use 19200 bps, and I try to send the note event 5 times.
Only the first and last event do something, there's a huge sync problem.
(put your sound way up):

3rd attempt was to use 57600 and that's total silence again.

How does stuff like cubase and other sequencers do it? They let you pick the modem or serial port and let you choose the base transmission speed like 0.5 MHz, 1 MHz, etc.
I notice that 1 000 000 MHz / 32 is exactly what MIDI wants, 31250.

Help!
 
Last edited:

Mu0n

Active Tinkerer
Oct 29, 2021
609
560
93
Quebec
www.youtube.com
C:
// Insert your include list here

#include <stdio.h>
#include "Serial.h"

// Insert your #define list here

// --- MIDI spec hard coded values ---
// good reference is here: https://www.music.mcgill.ca/~ich/classes/mumt306/StandardMIDIfileformat.html#BM1_1
// from the midi.org itself: https://midi.org/summary-of-midi-1-0-messages

#define kInputBufferSize 1024

// Insert your global variables here
char *buffer;

    // for serial stuff
short gOutputRefNum;
short gInputRefNum;
Handle gInputBufHandle;
OSErr gOSErr;


// Insert your function declarations here
void myInitStuff(void);
void MyOpenSerialDriver(void);
void MyChangeInputBuffer(void);
void MyConfigureThePort(void);
void MySendMessage(void);
void MyRestoreInputBuffer(void);
void MyCloseSerialDriver(void);


// Insert your function definitions here

void MyOpenSerialDriver(void)
    {
    gOSErr = OpenDriver("\p.AOut",&gOutputRefNum);
    if(gOSErr == noErr)
        {
        printf("output port successfully opened, refnum=%d \n",gOutputRefNum);
        gOSErr = OpenDriver("\p.AIn",&gInputRefNum);
        if(gOSErr == noErr) printf("output port successfully opened, refnum=%d \n",gInputRefNum);
        else printf("can't open in port, code=%d \n",gOSErr);
        }
    else printf("can't open out port, code=%d \n",gOSErr);
    
    printf("input refNum %d - output refNum %d\n",gInputRefNum,gOutputRefNum);
    }

void MyChangeInputBuffer(void)
    {
    gInputBufHandle = NewHandle(kInputBufferSize);
    HLock(gInputBufHandle);
    SerSetBuf(gInputRefNum, *gInputBufHandle,kInputBufferSize);
    }
    
void MySetHandshakeOptions(void)
    {
    SerShk mySerShkRec;
    mySerShkRec.fXOn = 0;
    mySerShkRec.fCTS = 0;
    mySerShkRec.errs = 0;
    mySerShkRec.evts = 0;
    mySerShkRec.fInX = 0;
    mySerShkRec.fDTR = 0;
    
    gOSErr = Control(gOutputRefNum, 14, &mySerShkRec);
    
    if(gOSErr == noErr)
        {
        printf("handshake successful\n");
        }
    else printf("handshake unsuccesful, code=%d \n",gOSErr);
    
    }

void MyConfigureThePort(void)
    {
    const int kConfigParam = baud57600+data8+noParity+stop10;
    gOSErr = SerReset(gOutputRefNum, kConfigParam);
    
    if(gOSErr == noErr)
        {
        printf("port configuration successful\n");
        }
    else printf("port configuration unsuccesful, code=%d \n",gOSErr);
    }

void MySendMessage(void)
    {
    char myMessage[3]={0x90,0x3C,0x64};
    long int myMsgLen;
    ParamBlockRec myParamBlock;
    ParmBlkPtr myPBPtr;
    
    myMsgLen=Length(myMessage);
    
    myParamBlock.ioParam.ioRefNum = gOutputRefNum;
    myParamBlock.ioParam.ioBuffer = (Ptr)&(myMessage[1]);
    myParamBlock.ioParam.ioReqCount = myMsgLen;
    myParamBlock.ioParam.ioCompletion = nil;
    myParamBlock.ioParam.ioVRefNum = 0;
    myParamBlock.ioParam.ioPosMode = 0;
    myPBPtr = (ParamBlockRec *)&myParamBlock;
    
    gOSErr = PBWrite(myPBPtr, FALSE);
    if(gOSErr == noErr)
        {
        printf("sending message successful\n");
        }
    else printf("sending message unsuccesful, code=%d \n",gOSErr);
    }   

void MyRestoreInputBuffer(void)
    {
    SerSetBuf(gInputRefNum, *gInputBufHandle,0);
    HUnlock(gInputBufHandle);
    }
    
void MyCloseSerialDriver(void)
    {
    gOSErr = KillIO(gOutputRefNum);
    if(gOSErr == noErr) gOSErr = CloseDriver(gInputRefNum);
    if(gOSErr == noErr) gOSErr = CloseDriver(gOutputRefNum);
    }
    
void myInitStuff()
    {
    InitGraf(&thePort); //qd is the quickdraw global, lets you access thePort (viewable area)
    InitFonts(); //Do this to mess with text and fonts
    InitWindows(); //A must if you plan on drawing on controlled window surfaces as opposed to the raw screen
    InitMenus(); //Do this to mess with menus
    InitDialogs(nil); //Dialog manager for alerts, dialogs, etc. nil = no return resumeProc
    InitCursor(); //Cursor manager, if you don't do this, you'll be stuck with the waiting cursor (watch icon or more modern ones)
    GetDateTime((unsigned long*)(&qd.randSeed)); //if you ever need random numbers, this will prep it with a unique time related seed that changes every launch
    //MoreMasters(); //useful at the very start, allows to have an additional block of master pointers. avoids fragmentation of memory if called at the start. uncomment if needed.
    FlushEvents(everyEvent,0); //Do this to prevent queued events from messing with your app right at launch.
    }
    
    
void main()
    {
    Rect r;
    EventRecord wutup;
    short savedMasks;
    
    myInitStuff(); //do this at startup
    
    SetRect(&r,60,30,350,450);
    
    printf("\nHello World");
    printf("\n");
     SetPort(FrontWindow());
    
    savedMasks = SysEvtMask;
    
    MyOpenSerialDriver();
    MyChangeInputBuffer();
    MySetHandshakeOptions();
    MyConfigureThePort();
    MySendMessage();
    
    SetEventMask(keyDownMask | keyUpMask | mDownMask);
    
    Delay(60,nil);
    MySendMessage();
    Delay(60,nil);
    MySendMessage();
    Delay(60,nil);
    MySendMessage();
    Delay(60,nil);
    MySendMessage();
    Delay(60,nil);
    MySendMessage();
         /*
    while(!Button())
        {
        Delay(60,nil);
        MySendMessage();
        GetNextEvent(everyEvent, &wutup);
        switch(wutup.what)
            {
            case keyDown:
                MySendMessage();
                break;
            case mouseDown:
                MySendMessage();
                break;
            }
        //Insert main loop code here; use functions and avoid clogging the main function
        }
    */
    MyRestoreInputBuffer();
    MyCloseSerialDriver();
    
    SetEventMask(savedMasks);
    ExitToShell(); //Go back to Finder
    }
 

joevt

Tinkerer
Mar 5, 2023
73
32
18
Other thread: https://68kmla.org/bb/index.php?threads/transformers-for-homemade-localtalk-phonenet-dongles.47052

Go to https://developer.apple.com/library/archive/navigation/#section=Platforms&topic=macOS
Search for Serial.
Read all the info.
TN1018 mentions MIDI but I don't think SE/30 has the SerialDMA Driver?

Does MIDI use GPi? DV16 has info. TN1018 says MIDI uses CTS which is actually HSKi.

HW545 says SE/30 doesn't have GPi so it must be HSKi.

Here's some notes (which leads to more notes and source code):
https://68kmla.org/bb/index.php?thr...-localtalk-phonenet-dongles.47052/post-527878
 

Mu0n

Active Tinkerer
Oct 29, 2021
609
560
93
Quebec
www.youtube.com
I assembled a small breaboard midi-in circuit, powered by a bench supply providing 5V, according to this well known circuit:

1720019840836.png


which looks like this (I had optoisolator H11 chips from previous projects)

1720019854220.png


and just to prove it works, I plugged my pocketmac midi out cable to this breadboard while cubase was playing a tune, and I checked an oscilloscope set to decode RS232*, 31250 bps, 1 stop bit and I was able to single shot capture a bunch of midi commands, here's a Note On set of three bytes on channel 9, but since its velocity is 00, it's probably interpreted as a Note Off:
(*I know the mac serial or midi isn't RS232, but it's the closest setting on my scope I can use, and you can modify it to oblivion with the other parameters anyway)

1720019867587.png


For the time being, I'm going through the myriads of suggestions and I've tested a bunch, nothing was conclusive yet. I'm trying to be careful not going into many rabbit holes of technical knowledge that I barely understand, so my first real step is keeping the C code I posted above, but doing small incremental changes. Here's what happened since I last posted it:

1) I added a scanf routine so that I can control when to send a note to the serial (modem) port, it's an ANSI console application, so I'm getting easy text feedback on error messages and whatnot, and can let myself control when to do what so I can be ready with my oscilloscope and prep it for single shot captures.

2) I tried to modify the MyConfigurePort to take in value '1' in its parameters, since it'd get me to 38,400 bps. I don't get anything on the scope that's valid.

C:
void MyConfigureThePort(void)
    {
    const int kConfigParam = 1+data8+noParity+stop10;
    char myParam = 0x40;
    
    gOSErr = SerReset(gOutputRefNum, kConfigParam);
    
    if(gOSErr == noErr)
        {
        printf("port configuration successful\n");
        }
    else printf("port configuration unsuccesful, code=%d \n",gOSErr);
    
    }

3) Right after the MyConfigurePort function, I added another one which uses Control and csCode 16 and I've mainly played with 0x40 as the parameter to send, which sets bit 6 to 1, supposedly to enable external clock. Is there anything else that has to be done? It doesn't seem to be enough, because this is what I get on the scope - not large enough for 3 bytes that I send (0x90, 0x3C, 0x64; each byte has to be 320 us) and the decoding fails to pick anything:

4) that same step 3 can now be activated with my typing - I type 2 hex characters and that's the hex value that'll be sent with a Control, as such:
C:
void TweakClock(char param)
    {
    //Tie the clock to external control
  
    gOSErr = Control(gOutputRefNum, 16, &param);
    if(gOSErr == noErr)
        {
        printf("external clock control successful\n");
        }
    else printf("external clock control  unsuccesful, code=%d \n",gOSErr);
    }

1720019924033.png
 

Mu0n

Active Tinkerer
Oct 29, 2021
609
560
93
Quebec
www.youtube.com
The solution was suggested to me from various people from 68kmla and mastodon, I thank them all.

Here's a quick test of a major scale with every note played with a delay of 1s between each:

this was the magic ingredient that I was missing to finally force the serial port to behave at 31250 bps by keeping a /32 frequency divider in the port configuration, but enable the bits to ensure to use an external clock through the HSKi pin of the serial port. The external clock can come in from a MIDI interface box such as my Pocket Mac from anatek - it's allegedly 1 MHz, making the /32 divider bring it to the correct 31,250 bps.



C:
void TweakClock(void)
    {
    Ptr addr = *(Ptr*)0x1DC;
    addr[2] = 4; //WR0 = register pointer set to WR4
    asm{MOVE.B (SP),(SP)} //2.2 us delay
  
    addr[2]=0x84; //WR4 = x32, 1 stop bit, no parity
    asm{MOVE.B (SP),(SP)} //2.2 us delay
  
    addr[2] = 11; //WR0 = register pointer set to WR11
    asm{MOVE.B (SP),(SP)} //2.2 us delay
  
    addr[2]=0x28; //WR11 = Rx and Tx clock from TRxC pin (HSKi on Macs)
    asm{MOVE.B (SP),(SP)} //2.2 us delay
  
    printf("external clock control successful\n");
    }


this was gleaned and zeroed in from this old "pascal"/asm source from this mactech article:
 
Last edited:
  • Like
Reactions: YMK

joevt

Tinkerer
Mar 5, 2023
73
32
18
This comment:
//WR4 = register pointer set to WR4
should be:
//WR0 = register pointer set to WR4