Showing posts with label Macintosh Plus. Show all posts
Showing posts with label Macintosh Plus. Show all posts

Thursday, 28 December 2023

Dialog Disillusion - The Mac Programming Primer Let Me Down

 Introduction

We did a bit of Macintosh programming at UEA, my undergraduate University between 1986 and 1989. Here we mostly used the interpreted MacPascal and a couple of sheets of the list of ToolBox APIs. We had infrequent access to MPW Pascal on the Mac IIs in the 3rd year, but the vast majority of development was done on Mac 512Ks and Mac Plusses.

This meant that we didn't really learn Macintosh programming properly. That's partly because MacPascal didn't support it properly (it used wacko inline functions to access the ToolBox), partly because we didn't get enough time on the Macs and partly because we just didn't have enough usable documentation.

So, when I found a copy of The Macintosh Pascal Programming Primer in about 1993 when I finally had a Mac (a Performa 400), I was overjoyed! I followed the entire set of examples from beginning to end and found them really educational: a whole bunch of well-written example applications that covered most of the needs of Toolbox API-based programs. The only difference was that I was using THINK C 5.0.4 instead of THINK Pascal, but it was easy to translate.

I used this knowledge to write a 16-bit RISC CPU simulator that gained me access to an MPhil degree in Computer Architecture at the University of Manchester between 1996 and 1998.

The Problem

Recently I've wanted to write a simple simulation framework for the classic Mac OS that consists of a dialog box to enter parameters and a main window to run the simulation. I probably want to integrate the dialog box with the main window and allow it to be updated live, but to start with I thought it would be easier to use a modal dialog, so that the user interaction would be:
  1. Edit the parameters
  2. Start/Restart the simulation
  3. Maybe stop the simulation before it ends
  4. Go back to steps 1 or 2 or Quit.
I started by taking the demo THINK C program: oopsBullseye and then adding the Dialog handling code from the Macintosh Pascal Programming Primer. But it didn't work - it just crashed the Mac every time (actually just a miniVMac emulator, but it's still a crash).

I wondered what kind of mistake I'd made, so I went back to the original Dialog chapter (chapter 6) and followed it through. Lo-and-behold, it worked. I still couldn't see where I'd gone wrong, but I thought it was because on my version, I could see that the dialog box appeared behind the main window, and that seemed to hang it. So I modified the Dialog demo to make it more like my application: the window would be open all the time (not just when the countdown was happening) and countdowns could be interactively started or restarted. I had to support update events.

And then I found out that this modified dialog application didn't work any more either! It had the same problem, the dialog box appeared behind the main window and crashed when ModalDialog was called. I scoured my copy of Inside Macintosh and Macintosh Toolbox essentials (I have a paper copy of both) and found some example Dialog box code for modal dialogs, but it still wasn't obvious what the difference was.

It turns out that the Macintosh Pascal Programming Primer is doing Dialog boxes really badly! I was gutted! Their example uses a couple of poor idioms which would mislead other programmers and it makes all the difference.

Analysis

TMPPP does two basic things that are wrong.

Firstly, it creates a global dialog box in DialogInit() (by using GetNewDialog(..) to read it in from the application's resource) which then sits there in memory all the time as a window you can't see. This means that when any other windows are created, the dialog box will pop up behind them when ShowWindow(dialogPtr) is called and then ModalDialog(..) will crash (the Mac!).

What it should do is create the dialog box when needed using GetNewDialog(..), i.e. in the Dialog Box handler, and when the user has finished with it, dispose of the dialog box (DisposeDialog()). Then the operation of the modal dialog is handled all in one place, and the Mac can deallocate the dialog box memory when it isn't needed, which is what we want.

Secondly, it violates the standard model / view / controller paradigm. Here, essentially, the model is the parameters used the dialog box. But in their example, they store the actual parameters in the dialog box items themselves; then when the dialog handler is called, they're saved to an internal data structure; and only if the user presses cancel, the internal data structure is used to restore back to the dialog box itself.

It should be done the other way around, the model is the internal data structure. When calling the Dialog box handler, the parameters should get copied to the dialog box items (which is equivalent to RestoreSettings(..)); and when the user quits by clicking [Save], the new items' values are copied back to the internal data structure, which is the SaveSettings(..) option ([Cancel] doesn't do anything, it just quits dialog box operations without updating the internal data structure).

The New Dialog Box Demo

So, my new Dialog Box demo is included here. It's significantly shorter, <500 lines; doesn't use the Notification manager, but importantly does use the modal dialog box the way it's supposed to. I avoid most of the repeated copying of lines of code by factoring the code that sets and gets controls: I think that this is going to be just as easy for new programmers to understand, because they won't have to scan a whole set of very similar lines of code to understand what each set of lines is doing: they can just go back to the lower-level getter/setter code and when they do their own dialog boxes, they'll be more likely to factor it too.

The resources are all almost exactly the same. I removed a menu option, because it no longer applied. You don't need the SICN icon.


Re-source¹ Fields.. [Close Window] Info².. [Close, Close]
DITL [OK]  
(see image below. Start with the Save, then Cancel buttons, then the other fields)

ID=400, Name="Alarm", Purgeable (only)
DITL [OK]
(see image below. Start with the OK button, then the text field)

ID=401, Name="About", Purgeable (only)
ALRT [OK] TOP=40, Bottom=142, Left=40, Right=332, DITL=401, Default Color ID=401, Name="About", Purgeable (only)
DLOG [OK] TOP=40, Bottom=200, Left=60, Right=320, DITL=400, Default Color, Standard double-border Dialog style, Not Initially visible, No close box ID=400, Name="Alarm", Purgeable (only)
MENU [OK] [X] Enabled, Title=• Apple Menu[ENTER], [X] Enabled, Title="About..."[ENTER] [ ]Enabled, • Separator line ID=400, No attributes.
MENU [OK] [X] Enabled, Title="File"[ENTER], [X] Enabled, Title="Settings..", Cmd-Key:S[ENTER], Title="Run", Cmd-Key:R[ENTER], Title="Quit", Cmd-Key:Q[ENTER] ID=401, No attributes.
MENU [OK] [X] Enabled, Title="Edit"[ENTER], no options [ ] Enabled: Title="Undo", Cmd-Key:Z[ENTER], Separator Line[Enter], Title="Cut", Cmd-Key:X[ENTER], Title="Copy", Cmd-Key:C[ENTER], Title="Paste", Cmd-Key:V[ENTER], Title="Clear", Cmd-Key:none[ENTER] ID=402, No attributes.
MBAR [OK] Each time, click in '****', choose Resource:Insert New Field(s) for Menu Res IDs 400, 401, 402. Top should say "# of menus 3 at the end." ID=400, No attributes.
WIND [OK] Close, then choose Resource:Open Using Template [WIND] [OK]. Bounds Rect= 70, 36, 106, 156 [Set], Proc ID=0, Visible=false, GoAway=false, RefCon=0, Title="Countdown", Auto Position=$0000. ID=400, Name="Countdown", Purgeable (only)

Parameters Ditl


About Ditl




When you've finished, close the .rsrc file. ResEdit will ask you to save it - save it. Then open up the Dlog.π project.  Choose File:New and create a stub of a C program:

int main(void)
{
    return 0;
}

Choose File:Save to save it as Dlog.c. Choose Project:Add "Dlog.c" to add the file to the project. You don't need to do anything clever to add the rsrc file to the project, THINK C will automatically associate the .rsrc with the same prefix as your application. 

Now you want to replace the dummy program with the rest of file. When you've finished...

Dlog.h

/**
* @file: Reminder.h
*/

#ifndef Reminder_h
#define Reminder_h

#define kBaseResId 400
#define kAboutAlert 401
#define kBadSysAlert 402

#define kSleep 60

#define kSaveButton 1
#define kCancelButton 2
#define kTimeField 4
#define kSOrMField 5
#define kSoundOnBox 6
#define kIconOnBox 7
#define kAlertOnBox 8
#define kSecsRadio 10
#define kMinsRadio 11

#define kDefaultSecsId 401
#define kDefaultMinsId 402

#define kOff 0
#define kOn 1

#define kSecondsPerMinute 60

#define kTop 25
#define kLeft 12

#define kMarkApplication 1
#define kAppleMenuId (kBaseResId)
#define kFileMenuId (kBaseResId+1)
#define kAboutItem 1

#define kChangeItem 1
#define kStartStopItem 2
#define kQuitItem 3

#define kSysVersion 2

typedef enum{
  kBoolFalse=0,
  kBoolTrue=1
}tBool;

typedef enum {
  kTimeUnitSeconds=0,
  kTimeUnitMinutes=1
}tTimeUnit;

typedef struct {
  long iTime;
  int iSound, iIcon, iAlert;
  tTimeUnit iUnit;
}tSettings;



extern Handle DlogItemGet(DialogPtr aDialog, int aItem);
extern void CtlSet(DialogPtr aDialog, int aItem, int aValue);
extern int CtlGet(DialogPtr aDialog, int aItem);
extern void CtlFlip(DialogPtr aDialog, int aItem);
extern void ITextSet(DialogPtr aDialog, int aItem, Str255 *aStr);

extern void StartCountDown(long aNumSecs);
extern void HandleCountDown(void);
extern void UpdateCountDown(void);

extern void RestoreSettings(DialogPtr aSettingsDialog);
extern void SaveSettings(DialogPtr aSettingsDialog);
extern void HandleDialog(void);
extern void HandleFileChoice(int aTheItem);
extern void HandleAppleChoice(int aTheItem);
extern void HandleMenuChoice(long aMenuChoice);
extern void HandleMouseDown(void);
extern void HandleEvent(void);
extern void MainLoop(void);
extern void MenuBarInit(void);
extern void DialogInit(void);
extern void WinInit(void);
extern tBool Sys6OrLater(void);
extern void ToolboxInit(void);
extern int main(void);

#endif // Reminder_h

Dlog.c

/**
 * Dlog.c
 */

#include "Dlog.h"

tBool gDone;

EventRecord gTheEvent;
tSettings gSavedSettings;


WindowPtr gCountDownWindow;
long gTimeout, gOldTime;
tBool gIsCounting;

Handle DlogItemGet(DialogPtr aDialog, int aItem)
{
  int itemType;
  Rect itemRect;
  Handle itemHandle;
  GetDItem(aDialog, aItem, &itemType, &itemHandle, &itemRect);
  return itemHandle;
}

void CtlSet(DialogPtr aDialog, int aItem, int aValue)
{
  Handle itemHandle=DlogItemGet(aDialog, aItem);
  SetCtlValue((ControlHandle)itemHandle, aValue);
}

int CtlGet(DialogPtr aDialog, int aItem)
{
  Handle itemHandle=DlogItemGet(aDialog, aItem);
  return GetCtlValue((ControlHandle)itemHandle);
}

/*
void ITextSet(DialogPtr aDialog, int aItem, Str255 *aStr)
{
  Handle itemHandle=DlogItemGet(aDialog, aItem);
  SetIText(itemHandle, aStr);
}
*/
void CtlFlip(DialogPtr aDialog, int aItem)
{
  Handle itemHandle=DlogItemGet(aDialog, aItem);
  SetCtlValue((ControlHandle)itemHandle,
    (GetCtlValue((ControlHandle)itemHandle)==kOn)? kOff:kOn);
}

void StartCountDown(long aNumSecs)
{
  GetDateTime(&gOldTime);
  if(gSavedSettings.iUnit==kTimeUnitMinutes) {
    aNumSecs*=kSecondsPerMinute;
  }
  gTimeout=gOldTime+aNumSecs; // this is the timeout.
  gIsCounting=kBoolTrue;
}

// Called on Null event.
void HandleCountDown(void)
{
  if(gIsCounting==kBoolTrue) {
    long myTime;
    GetDateTime(&myTime);
    if(myTime!=gOldTime) {
      GrafPtr oldPort;
      gOldTime=myTime; // gTimeout-gOldTime==remaining seconds.
      // gen update, but how?
      GetPort(&oldPort);
      SetPort((GrafPtr)gCountDownWindow);
      InvalRect(&gCountDownWindow->portRect);
      SetPort(oldPort);
    }
  }
}

void UpdateCountDown(void)
{
  //
  WindowPtr win=(WindowPtr)gTheEvent.message;
  if(win==gCountDownWindow) {
    long remaining=gTimeout-gOldTime;
    Str255 myTimeString;
    BeginUpdate(win);
    MoveTo(kLeft, kTop);
    if(remaining<=0 || gIsCounting==kBoolFalse) {
      remaining=0;
      gIsCounting=kBoolFalse;
    }
    NumToString(remaining, myTimeString);
    EraseRect(&(gCountDownWindow->portRect));
    DrawString(myTimeString);
    EndUpdate(win);
  }
}

void RestoreSettings(DialogPtr aSettingsDialog)
{
  Handle itemHandle;
  Str255 timeString;
  tBool isInSeconds=(gSavedSettings.iUnit==kTimeUnitSeconds)?
      kBoolTrue:kBoolFalse;
 
  itemHandle=DlogItemGet(aSettingsDialog, kTimeField);
  NumToString(gSavedSettings.iTime, &timeString);
  SetIText(itemHandle, timeString);
 
  CtlSet(aSettingsDialog, kSoundOnBox, gSavedSettings.iSound);
  CtlSet(aSettingsDialog, kIconOnBox, gSavedSettings.iIcon);
  CtlSet(aSettingsDialog, kAlertOnBox, gSavedSettings.iAlert);
  CtlSet(aSettingsDialog, kSecsRadio, (isInSeconds==kBoolTrue)?kOn:kOff);
  CtlSet(aSettingsDialog, kMinsRadio, (isInSeconds==kBoolFalse)?kOn:kOff);

  itemHandle=DlogItemGet(aSettingsDialog, kSOrMField);
  SetIText(itemHandle,(gSavedSettings.iUnit==kTimeUnitSeconds)?
      "\pseconds":"\pminutes");
}

void SaveSettings(DialogPtr aSettingsDialog)
{
  Handle itemHandle;
  Str255 timeString;

  itemHandle=DlogItemGet(aSettingsDialog, kTimeField);
  GetIText(itemHandle, &timeString);
  StringToNum(timeString, &gSavedSettings.iTime);
 
  gSavedSettings.iSound=CtlGet(aSettingsDialog, kSoundOnBox);
  gSavedSettings.iIcon=CtlGet(aSettingsDialog, kIconOnBox);
  gSavedSettings.iAlert=CtlGet(aSettingsDialog, kAlertOnBox);
  gSavedSettings.iUnit=(CtlGet(aSettingsDialog, kSecsRadio)==kOn)?
        kTimeUnitSeconds:kTimeUnitMinutes;
}

void HandleDialog(void)
{
  tBool dialogDone;
  int itemHit;
  long alarmDelay;
  Handle itemHandle;
  DialogPtr settingsDialog;
 
  settingsDialog=GetNewDialog(kBaseResId, NULL, (WindowPtr)-1);

  ShowWindow(settingsDialog);
  RestoreSettings(settingsDialog);
 
  dialogDone=kBoolFalse;
  while(dialogDone==kBoolFalse) {
    ModalDialog(NULL, &itemHit);
    switch(itemHit) {
    case kSaveButton:
      SaveSettings(settingsDialog); // update them.
      dialogDone=kBoolTrue;
      break;
    case kCancelButton:
      dialogDone=kBoolTrue;
      break;
    case kSoundOnBox:
    case kIconOnBox:
    case kAlertOnBox:
      CtlFlip(settingsDialog, itemHit);
      break;
    case kSecsRadio:
      CtlSet(settingsDialog, kSecsRadio, kOn);
      CtlSet(settingsDialog, kMinsRadio, kOff);

      itemHandle=DlogItemGet(settingsDialog, kSOrMField);
      SetIText(itemHandle, "\pseconds");
      break;
    case kMinsRadio:
      CtlSet(settingsDialog, kSecsRadio, kOff);
      CtlSet(settingsDialog, kMinsRadio, kOn);

      itemHandle=DlogItemGet(settingsDialog, kSOrMField);
      SetIText(itemHandle, "\pminutes");
      break;
    }
  }
  DisposeDialog(settingsDialog);
}

void HandleFileChoice(int aTheItem)
{
  switch(aTheItem) {
  case kChangeItem:
    HandleDialog();
    break;
  case kStartStopItem:
    HiliteMenu(0);
    StartCountDown(gSavedSettings.iTime);
    break;
  case kQuitItem:
    gDone=true;
    break;
  }
}

void HandleAppleChoice(int aTheItem)
{
  Str255 accName;
  int accNumber, itemNumber, dummy;
  MenuHandle appleMenu;
  switch(aTheItem) {
  case kAboutItem:
    NoteAlert(kAboutAlert, NULL);
    break;
  default:
    appleMenu=GetMHandle(kAppleMenuId);
    GetItem(appleMenu, aTheItem, &accName);
    OpenDeskAcc(accName);
    break;
  }
}

void HandleMenuChoice(long aMenuChoice)
{
  int theMenu, theItem;
  if(aMenuChoice!=0) {
    theMenu=HiWord(aMenuChoice);
    theItem=LoWord(aMenuChoice);
    switch(theMenu) {
    case kAppleMenuId:
      HandleAppleChoice(theItem);
      break;
    case kFileMenuId:
      HandleFileChoice(theItem);
      break;
    }
    HiliteMenu(0);
  }
}

void HandleMouseDown(void)
{
  WindowPtr whichWindow;
  int thePart;
  long menuChoice, windSize;
  thePart=FindWindow(gTheEvent.where, &whichWindow);
  switch(thePart) {
  case inMenuBar:
    menuChoice=MenuSelect(gTheEvent.where);
    HandleMenuChoice(menuChoice);
    break;
  case inSysWindow:
    SystemClick(&gTheEvent, whichWindow);
    break;
  case inDrag:
    DragWindow(whichWindow, gTheEvent.where, &screenBits.bounds);
    break;
  case inGoAway:
    gDone=kBoolTrue;
    break;
  }
}

void HandleEvent(void)
{
  char theChar;
  tBool dummy;
  WaitNextEvent(everyEvent, &gTheEvent, kSleep, NULL);
  switch(gTheEvent.what){
  case mouseDown:
    HandleMouseDown();
    break;
  case keyDown: case autoKey:
    theChar=(char)(gTheEvent.message & charCodeMask);
    if((gTheEvent.modifiers & cmdKey)!=0) {
      HandleMenuChoice(MenuKey(theChar));
    }
    break;
  case nullEvent:
    HandleCountDown();
    break;
  case updateEvt:
    UpdateCountDown();
    break;
  }
}

void MainLoop(void)
{
  gDone=kBoolFalse;
  while(gDone==kBoolFalse) {
    HandleEvent();
  }
}

void MenuBarInit(void)
{
  Handle myMenuBar;
  MenuHandle aMenu;
  myMenuBar=GetNewMBar(kBaseResId);
  SetMenuBar(myMenuBar);
  DisposHandle(myMenuBar);
  aMenu=GetMHandle(kAppleMenuId);
  AddResMenu(aMenu, 'DRVR');
  DrawMenuBar();
}

void WinInit(void)
{
  gCountDownWindow=GetNewWindow(kBaseResId, NULL, (WindowPtr)-1);
  gIsCounting=kBoolFalse;
  SetPort(gCountDownWindow);
  TextFace(bold); // it's the same in THINK C.
  TextSize(24);
  ShowWindow(gCountDownWindow);
}

void DialogInit(void)
{
  gSavedSettings.iTime=12;
 
  gSavedSettings.iSound=kOn;
  gSavedSettings.iIcon=kOn;
  gSavedSettings.iAlert=kOn;
 
  gSavedSettings.iUnit=kTimeUnitSeconds;
}

tBool Sys6OrLater(void)
{
  OSErr status;
  SysEnvRec SysEnvData;
  int dummy;
  tBool result=kBoolTrue;
  status=SysEnvirons(kSysVersion, &SysEnvData);
  if(status!=noErr || SysEnvData.systemVersion<0x600) {
    StopAlert(kBadSysAlert, NULL);
    result=kBoolFalse;
  }
  return result;
}

void ToolboxInit(void)
{
  InitGraf(&thePort);
  InitFonts();
  InitWindows();
  InitMenus();
  TEInit();
  InitDialogs(NULL);
  MaxApplZone();
}

int main(void)
{
  ToolboxInit();
  if(Sys6OrLater()) {
    DialogInit();
    MenuBarInit();
    WinInit();
    InitCursor();
    MainLoop();
  }
  return 0;
}

Conclusion

As a whole, The Macintosh Pascal (and C) Programming Primer is a brilliantly simple introduction to traditional (non-OO) Macintosh programming. However, the Dialog box chapter ("Working With Dialogs") is a major exception. After a bit of sleuthing (which I should never have needed to do), I worked out the problem and rewrote a better demo (and made sure the cursor is an arrow instead of the watch icon). This means I'm closer to my goal of writing a simple simulation framework.

For the chronically lazy amongst you, feel free to download the full project from here.

Saturday, 30 April 2022

Cubase Re-Lited Part 1

In the mid-1980s my parents kindly bought me a Casio CZ-101. It was a great introduction to synthesisers and my way back into keyboard instruments. I had been introduced to synthesiser music thanks to some of  school friends lending me albums by Jean Michel Jarre (Oxygene and Equinoxe), Vangelis (L'Opera Sauvage, Apocalypse Des Animaux, Heaven and Hell) and Tomita (Pictures at An Exhibition, The Planet Suite).

At the time I'd been learning the trumpet, but my mum was well-known in the locality, for playing the piano and organ in primary schools and working-men's clubs, alongside teaching piano the music theory, but I was never able to pick it up - my sister did much better than me, getting up to grade 3 or 4.

The CZ series was largely dismissed since the 1990s, primarily because CASIO wasn't associated with professional instruments; the Yamaha DX series were more capable, but also because new synthesis techniques superseded this early digital techniques. However, in recent years, as artists have increasingly mined and revisited older technology, Phase Distortion and the CZ series has been re-evaluated, alongside the emergence of DAWless production.

Over time I acquired a few more instruments: a Yamaha SY-22; an early Sample and (FM) Synthesis, but capable of creating interesting dynamic soundscapes thanks to its vector synthesis feature. For a while I had a Roland MT-32, which could handle up to 8 parts + a drum kit channel.

Around the same time I bought a copy of Cubase lite (bundled, I believe with a MIDI interface) for my Macintosh Performa 400 and with it sequenced quite a number of pieces and songs. Cubase Lite was well within my needs and even when running on a Mac Plus, it could handle everything I threw at it.

Stupidly I sold it because I didn't think that its sounds were particularly interesting and I never took the opportunity to learn how to program it properly. Finally, I bought a super-ugly Yamaha TQ-5 (for £35 on Ebay); which is a superb 4-operator FM synth / sound module with a built-in sequencer (horrible UI) and an effects unit. It's badly underrated as it's better than a DX-11.

Over the past two decades though I've used Garage Band for my music creation - again, because although it's an entry level program, it's still within my needs What I did miss was being able to connect Garage Band to my earlier synths.

However, now that DAWless music creation has become more fashionable, I thought I'd have a go at trying to re-unite my current keyboards and sound modules with a cheap or free, but simple to use DAWless MIDI application.

And it turns out that's quite a rabbit-hole. It's not that nothing is available. For example, there's a Linux program called RoseGarden. Now, typically, the first question I ask about any application, whether it's for Linux, macOS or whatever, is what are the requirements in terms of RAM and CPU? RoseGarden says it's "demanding". What does that mean? A 1GHz Athlon? A 2.5GHz Core i7? A Raspberry PI 3? Muse looks good, but there's no description at all about the hardware requirements. And at this point I'm already turned off. Why make the effort if my hardware might not run it? Also, as software developers have shifted from producing sequencers to full Digital Audio Workstations, they've blurred the descriptions of what their software does. For example, obviously Garageband does handle MIDI input, but it doesn't handle MIDI output, so I've found it progressively harder to figure out whether a given application would support what I want, or even how to ask the right kind of search question that would provide an answer.

Also, it's such a pain even just finding out about trivial things. Consider part of the Support FAQ (which of course, doesn't even tell you what CPU performance you need):

“MusE requires a high performance timer to be able to keep a stable beat. It is recommended that your operating system be setup to allow at least a 500hz timer, MusE will warn you if this is not available.”

An 8MHz Mac Plus could do this! An Atari ST could do this! An Amiga could do this! A 1980s PC running MS-DOS Could. Do. This! 

So, in the end, I figured that if a mid-80s 16-bit era computer (yes, the 68000 is 32-bit) and emulators for these computers are available that run much faster than the original hardware, then surely it should be possible to get an emulation of a Mac to run my original Cubase Lite and communicate with a MIDI interface.

Picking An Emulator?

Mini VMac

My normal go to Macintosh emulator is miniVMac. It is great and and easy to install on a number of platforms. It's also not too heavy on hardware requirements as it makes efforts to maximise emulator performance.

Also, someone has tried to interface miniVMac to real Midi hardware.

“Hi, i've implemented a midi bridge for Mini vMac which exposes emulator modem and printer midi ports to the host OS. so far no stuck notes, and sysex seems to work both ways. there's systematic jitter though which needs to be resolved.”
The cause of the systematic jitter is fairly easy to understand. Mini Mac has a fairly simple emulation core, which aims to emulate execution in 1/60th of a second chunks. That is, it works out how many instructions can be executed in 1/60th of a real second, according to the emulator's requested speed and executes them all in one go. It's not quite that simple, within the basic emulation loop it actually executes instructions until the next interrupt, but the same principle applies. It means that mini vMac only synchronises every ≈16.67ms. The upshot is that mini vMac receives MIDI events prior to its time slice all at once; sends multiple events too quickly during its time slice and worse still, is unable to send events prior to its time slice, as that would mean generating events backwards in time.





This is why messages end up with jitter. Now, the jitter is often not very noticeable, because there often aren't that many events every 17ms, but for an application which requires sub-millisecond latency, it will be enough to mess up recording and playback.

The developer of MiniVMac is slowly making progress on this (see newsletters 9 and 13), but for me, it's a non-starter.

Basilisk II

Basilisk II isn't capable of emulating a Mac Plus, instead it aims to emulate 68020 to 68040 Macintosh computers. Well, at least the most significant ones. Unfortunately, one of the ones it doesn't emulate is my Performa 400 (a.k.a LC II). In addition, Basilisk II substitutes the emulation of real Mac hardware with drivers to the equivalent peripherals. This will work in most circumstances, but if I understand things correctly, applications like Cubase Lite actually addressed the real hardware, in this case the SCC serial communications controller; rather than go through the driver.

PCE/MacPlus Emulator

I'd come across the PCE MacPlus emulator because of the Javascript version:

https://jamesfriend.com.au/pce-js/

I had originally thought it would be rather sluggish, because it was in Javascript, but in reality it performs pretty well. It was only very recently that I actually followed the links and found out it was a conventional emulator written in C++ and ported to Javascript. Importantly, it's possible to browse through the source code, at which point I found this crucial line:

#define MAC_CPU_SYNC 250
With a comment to say that the emulator is sync'd to the emulating computer that many time per second. So, I reasoned that, if I increased it to 1000, then the emulator's serial I/O's latency would be  

Then I came across a ToughDev article about using the PCE emulator to develop classic Macintosh applications, which gave a decent walkthrough:


Building it shouldn't be that hard, you just need X11-dev and SDL (the Simple Direct Layer) libraries, which I tried to first brew the build on my Mac mini; and later used apt-get on a Linux PC, but unfortunately in both cases, the configuration stage said SDL wasn't there.

So, then I tried it on a Raspberry PI 3 I had to hand, but the Raspberry PI 3 had its own problems with the latest version of apt-get. It kept complaining with messages a bit like:

W: An error occurred during the signature verification. The repository is not updated and the previous index files will be used. GPG error: http://archive.raspberrypi.org/debian buster InRelease: Splitting up /var/lib/apt/lists/archive.raspberrypi.org_debian_dists_buster_InRelease into data and signature failed
W: An error occurred during the signature verification. The repository is not updated and the previous index files will be used. GPG error: http://raspbian.raspberrypi.org/raspbian buster InRelease: Splitting up /var/lib/apt/lists/raspbian.raspberrypi.org_raspbian_dists_buster_InRelease into data and signature failed
W: Failed to fetch http://raspbian.raspberrypi.org/raspbian/dists/buster/InRelease  Splitting up /var/lib/apt/lists/raspbian.raspberrypi.org_raspbian_dists_buster_InRelease into data and signature failed
W: Failed to fetch http://archive.raspberrypi.org/debian/dists/buster/InRelease  Splitting up /var/lib/apt/lists/archive.raspberrypi.org_debian_dists_buster_InRelease into data and signature failed
W: Some index files failed to download. They have been ignored, or old ones used instead.
I found that this was a common problem with libraries being updated to use the new Bullseye OS version, replacing Buster. Eventually I found couple [1], [2] of Raspberry PI forums that told me how to fix it. This meant typing in the following line:
sudo apt-get update --allow-releaseinfo-change
I was then able to follow the rest of the development installation as per the ToughDev web page on compiling PCE.
sudo apt update
sudo apt-get update
sudo apt-get install libx11-dev libx11-doc libxau-dev libxcb1-dev libxdmcp-dev x11proto-core-dev x11proto-input-dev x11proto-kb-dev xorg-sgml-doctools xtrans-dev
sudo apt-get install libsdl1.2-dev libsdl1.2debian
One problem I had was at the configure stage, I couldn't get it to acknowledge that SDL's configuration output supported SDL. It would always say things like:
Terminals built:         null x11
Terminals not built:     sdl

 To make it compile with SDL I had to type:
./configure -with-x --with-sdl=1 --enable-char-ppp --enable-char-tcp --enable-sound-oss --enable-char-slip --enable-char-pty --enable-char-posix --enable-char-termios
cp /usr/local/etc/pce/pce-mac-plus.cfg .

At this point I had a version of PCE that could actually run. In part 2 I'll describe the steps needed to get PCE to work as I'd intended.