This is PageSupplier.m in view mode; [Download] [Up]
/* (c) 1992 Dirk Schwarzhans, Matthias Braun Use under the terms of the GNU General Public License */ #import "PageSupplier.h" #import <appkit/Application.h> #import <soundkit/soundkit.h> #import <mach_error.h> #import <sys/message.h> #import <sound/sound.h> #import <sound/sounddriver.h> #import <string.h> #import <appkit/Panel.h> /* Host-Messages and den DSP (ACHTUNG: muû mit dsp.asm öbereinstimmen) */ #define HM_REQ_PAGE 0x200000 // setzt Anforderungsdaten #define HM_DISP_CNTL 0x210000 // setzt die Display-Control-Register #define HM_DISP_MODE 0x220000 // setzt das Display-Modus-Register #define HM_DSP_STOP 0x230000 // Arbeit einstellen #define HM_SYS_CONFIG 0x240000 // Messages und Uhrzeit schalten #define HM_MODE 0x250000 // Modus-Register Bits 2:0 schreiben #define DMA_BUFFER_SIZE 1024 // in Bytes #define LOW_WATER_MARK 4 * 1024 // Konfiguration des šbertragungskanals #define HIGH_WATER_MARK 8 * 1024 // vom DSP #define READ_WIDTH 1 // Bytes pro Sample #define BIN2ASC(a) (((a) < 10) ? ((a) + '0') : ((a) + 'A' - 10)) // die folgende Struktur nimmt die Daten för jedes Fenster auf typedef struct { VTPageNumber page; // angeforderte Seite VTSubpageNumber subpage; // und Unterseite BOOL hold; // Flag, ob Fenster angehalten BOOL careSubpage; // Flag, ob bestimmte Unterseite angef. } windowInfo; extern const char *NXArgv[]; static void dspThread(PageSupplier *self); static void comThread(PageSupplier *self); static void dataHandler(PageSupplier *self, int tag, unsigned char *data, int n); static void msgHandler(PageSupplier *self, int *data, int n); @implementation PageSupplier // lokale Methoden // holt das Storage-Objekt einer Seite; legt es notfalls an, wenn gefordert - (Storage *)dataOfPage:(VTPageNumber)page subpage:(VTSubpageNumber)subpage create:(BOOL)flag { void *key; HashTable *subpages; Storage *pageContainer; key = *((void **)&page); // Umwandlung för HashTable erforderlich subpages = [allPages valueForKey:key]; if (subpages == nil && flag) { // Tabelle mit allen Unterseiten dieser Seite neu anlegen subpages = [[HashTable alloc] initKeyDesc:"i" valueDesc:"@"]; [allPages insertKey:key value:subpages]; } // möglicherweise ist subpages nil, was aber nichts ausmacht key = *((void **)&subpage); pageContainer = [subpages valueForKey:key]; if (pageContainer == nil && flag) { // Unterseitenspeicher neu anlegen pageContainer = [[Storage alloc] initCount:1 elementSize:1024 description:"[1024c]"]; [subpages insertKey:key value:pageContainer]; } return pageContainer; } // holt von einer gegebenen Seite die höchste Unterseite unterhalb einer // öbergebenen Unterseitennummer; gibt bei Nichtvorhandensein nil zuröck - (Storage *)greatestSubpageOf:(VTPageNumber)page beforeSubpage:(VTSubpageNumber)margin newSubpage:(VTSubpageNumber *)new { NXHashState state; VTSubpageNumber maxSubpage, subpage; HashTable *subpages; Storage *maxPageContainer,*pageContainer; subpages = [allPages valueForKey:*((void **)&page)]; maxSubpage = (VTSubpageNumber)0; maxPageContainer = nil; state = [subpages initState]; while ([subpages nextState:&state key:(void *)&subpage value:(void *)&pageContainer]) { if (subpage < margin && subpage >= maxSubpage) { // bessere Seite gefunden maxSubpage = subpage; maxPageContainer = pageContainer; } } if (new != NULL && maxPageContainer != nil) *new = maxSubpage; return maxPageContainer; } // holt von einer gegebenen Seite die kleinste Unterseite oberhalb einer // öbergebenen Unterseitennummer; gibt bei Nichtvorhandensein nil zuröck - (Storage *)smallestSubpageOf:(VTPageNumber)page afterSubpage:(VTSubpageNumber)margin newSubpage:(VTSubpageNumber *)new { NXHashState state; VTSubpageNumber minSubpage, subpage; HashTable *subpages; Storage *minPageContainer,*pageContainer; subpages = [allPages valueForKey:*((void **)&page)]; minSubpage = (VTSubpageNumber)0x00ffffff; minPageContainer = nil; state = [subpages initState]; while ([subpages nextState:&state key:(void *)&subpage value:(void *)&pageContainer]) { if (subpage > margin && subpage <= minSubpage) { // bessere Seite gefunden minSubpage = subpage; minPageContainer = pageContainer; } } if (new != NULL && minPageContainer != nil) *new = minSubpage; return minPageContainer; } // öberpröft, ob eine Seite gesendet werden kann und tut dies gegebenfalls // wenn das nicht möglich sein sollte, wird die laufende Seitennummer eing. - sendPageIfPossible { Storage *pageContainer; windowInfo *info; if ((info = [(Storage *)[windowTable valueForKey:actualWindow] elementAt:0]) != NULL) { if (info->hold) [self requestPageMessages:NO]; else { pageContainer = [self dataOfPage:info->page subpage:info->subpage create:NO]; if (pageContainer == nil && !info->careSubpage) { // möglicherweise andere Unterseite finden pageContainer = [self smallestSubpageOf:info->page afterSubpage:(VTSubpageNumber)0 newSubpage:&info->subpage]; } if (pageContainer != nil) { mutex_lock(self->comMem); self->pagePacket.data = [pageContainer elementAt:0]; self->pagePacket.window = actualWindow; self->pagePacket.tag = tag; self->pageReady = YES; mutex_unlock(self->comMem); condition_signal(self->comMemChanged); } else [self requestPageMessages:YES]; } } return self; } - init { return [self initPort:PORT_NULL]; } - initPort:(port_t)port { int soundError, protocol; Sound *dspCode; BOOL retry; dev_port=0; owner_port=0; do { retry = NO; soundError = SNDAcquire(SND_ACCESS_DSP, 0, 0, 0, NULL_NEGOTIATION_FUN, 0, &dev_port, &owner_port); if (soundError != SND_ERR_NONE) { retry = (NXRunAlertPanel(NULL, "DSP schon belegt.", "Nochmal", "Abbruch", NULL) == NX_ALERTDEFAULT); if (!retry) return nil; } }while(retry); snddriver_get_dsp_cmd_port(dev_port, owner_port, &cmd_port); port_allocate(task_self(), &reply_port); protocol = SNDDRIVER_DSP_PROTO_RAW; snddriver_stream_setup(dev_port, owner_port, SNDDRIVER_DMA_STREAM_FROM_DSP, DMA_BUFFER_SIZE, READ_WIDTH, LOW_WATER_MARK, HIGH_WATER_MARK, &protocol, &read_port); snddriver_dsp_protocol(dev_port, owner_port, protocol); dspCode = [Sound newFromMachO:"DSP.snd"]; soundError = SNDBootDSP(dev_port, owner_port, [dspCode soundStruct]); [dspCode free]; if (soundError != SND_ERR_NONE) { NXRunAlertPanel(NULL, "DSP-Fehler: konnte nicht booten!", NULL, NULL, NULL); return nil; } DSPThreadAborted = threadsAborted = NO; strcpy(clockString,"12345678"); strcpy(pageString,"123"); interruptedSequenz = NO; allPages = [[HashTable alloc] initKeyDesc:"i" valueDesc:"@"]; windowTable = [[HashTable alloc] initKeyDesc:"@" valueDesc:"@"]; actualWindow = nil; tag = 0; clockMessages = pageMessages = NO; depositFull = NO; depositValid = NO; deposit = [[Storage alloc] initCount:1 elementSize:1024 description:"[1024c]"]; VTNumbersFromInt(&(VTPageNumber)depositPage, &(VTSubpageNumber)depositSubpage, 0, 0); memoryInUse = mutex_alloc(); comMem = mutex_alloc(); comMemChanged = condition_alloc(); sending = mutex_alloc(); pageReady = clockReady = pageNumReady = error = NO; errorString = NULL; port_allocate(task_self(), &speakerReplyPort); speaker = [[VTSpeaker alloc] init]; [speaker setSendPort:port]; [speaker setReplyPort:speakerReplyPort]; cthread_detach(cthread_fork((cthread_fn_t)dspThread, (any_t)self)); cthread_detach(cthread_fork((cthread_fn_t)comThread, (any_t)self)); return self; } - stopThreads { int hostMsg; hostMsg = HM_DSP_STOP; mutex_lock(sending); snddriver_dsp_write(cmd_port, &hostMsg, 1, sizeof(int), SNDDRIVER_HIGH_PRIORITY); mutex_unlock(sending); return self; } - free { if (!threadsAborted) return self; SNDRelease(SND_ACCESS_DSP, dev_port, owner_port); // Ports freigeben ??? port_deallocate(task_self(), read_port); port_deallocate(task_self(), reply_port); port_deallocate(task_self(), cmd_port); [windowTable freeObjects]; [windowTable free]; [self forgetAll]; [allPages free]; [deposit free]; [speaker free]; port_deallocate(task_self(), speakerReplyPort); mutex_free(memoryInUse); mutex_free(comMem); condition_free(comMemChanged); mutex_free(sending); return [super free]; } // löscht den gesamten Seitenspeicher und fordert die aktuelle Seite neu an - (unsigned)forgetAll { NXHashState state; VTPageNumber pageNum; HashTable *allSubpages; windowInfo *info; int hostMsg; mutex_lock(memoryInUse); tag++; depositFull = NO; // vorsichtshalber // Seitenspeicher löschen state = [allPages initState]; while ([allPages nextState:&state key:(void *)&pageNum value:(void *)&allSubpages]) { [allSubpages freeObjects]; } [allPages freeObjects]; // falls ein Fenster aktiv ist, dessen Seite neu anfordern if ((info = [(Storage *)[windowTable valueForKey:actualWindow] elementAt:0]) != NULL) { [self requestPageMessages:YES]; info->hold = NO; hostMsg = HM_REQ_PAGE | info->page; mutex_lock(sending); snddriver_dsp_write(cmd_port, &hostMsg, 1, sizeof(int), SNDDRIVER_HIGH_PRIORITY); mutex_unlock(sending); } mutex_unlock(memoryInUse); return tag; } // aktuelles Fenster setzen - (unsigned)setMainWindow:(Window *)window { Storage *infoContainer; windowInfo *info; int hostMsg; mutex_lock(memoryInUse); tag++; // Wenn das Fenster bisher unbekannt ist, dann Tabelleneintrag erzeugen und // nach Seite 100.XXXX suchen if ((infoContainer = [windowTable valueForKey:window]) == nil) { infoContainer = [[Storage alloc] initCount:1 elementSize:sizeof(*info) description:"iicc"]; [windowTable insertKey:window value:infoContainer]; info = [infoContainer elementAt:0]; VTNumbersFromInt(&info->page, &info->subpage, 100, 0); info->hold = NO; info->careSubpage = NO; } actualWindow = window; info = [infoContainer elementAt:0]; // Seitenanforderung an den DSP senden hostMsg = HM_REQ_PAGE | info->page; mutex_lock(sending); snddriver_dsp_write(cmd_port, &hostMsg, 1, sizeof(int), SNDDRIVER_HIGH_PRIORITY); mutex_unlock(sending); // evtl. schon im Cache vorhandene Seite zuröckgeben [self sendPageIfPossible]; mutex_unlock(memoryInUse); return tag; } // ein Fenster wurde geschlossen - (unsigned)windowClosed:(Window *)window { mutex_lock(memoryInUse); tag++; if (window == actualWindow) { [self requestPageMessages:NO]; actualWindow = nil; } [(Storage *)[windowTable valueForKey:window] free]; [windowTable removeKey:window]; mutex_unlock(memoryInUse); return tag; } // Seitenanforderung ohne Beachtung von Unterseitennummern - (unsigned)pageRequest:(VTPageNumber)number { windowInfo *info; int hostMsg; mutex_lock(memoryInUse); tag++; if ((info = [(Storage *)[windowTable valueForKey:actualWindow] elementAt:0]) != NULL) { VTNumbersFromInt(&info->page, &info->subpage, 0, 0); info->page = number; info->hold = info->careSubpage = NO; hostMsg = HM_REQ_PAGE | number; mutex_lock(sending); snddriver_dsp_write(cmd_port, &hostMsg, 1, sizeof(int), SNDDRIVER_HIGH_PRIORITY); mutex_unlock(sending); [self sendPageIfPossible]; } mutex_unlock(memoryInUse); return tag; } // Seitenanforderung mit Beachtung von Unterseitennummern - (unsigned)pageRequest:(VTPageNumber)page subpage:(VTSubpageNumber)subpage { windowInfo *info; int hostMsg; mutex_lock(memoryInUse); tag++; if ((info = [(Storage *)[windowTable valueForKey:actualWindow] elementAt:0]) != NULL) { info = [(Storage *)[windowTable valueForKey:actualWindow] elementAt:0]; info->subpage = subpage; info->page = page; info->hold = NO; info->careSubpage = YES; hostMsg = HM_REQ_PAGE | page; mutex_lock(sending); snddriver_dsp_write(cmd_port, &hostMsg, 1, sizeof(int), SNDDRIVER_HIGH_PRIORITY); mutex_unlock(sending); [self sendPageIfPossible]; } mutex_unlock(memoryInUse); return tag; } - (unsigned)doCareSubpage:(VTSubpageNumber)number { windowInfo *info; mutex_lock(memoryInUse); tag++; if ((info = [(Storage *)[windowTable valueForKey:actualWindow] elementAt:0]) != NULL) { info->hold = NO; info->subpage = number; info->careSubpage = YES; [self sendPageIfPossible]; } mutex_unlock(memoryInUse); return tag; } - (unsigned)dontCareSubpage { windowInfo *info; mutex_lock(memoryInUse); tag++; if ((info = [(Storage *)[windowTable valueForKey:actualWindow] elementAt:0]) != NULL) { info->careSubpage = NO; } mutex_unlock(memoryInUse); return tag; } - (unsigned)holdPage:(BOOL)hold; { windowInfo *info; mutex_lock(memoryInUse); tag++; if ((info = [(Storage *)[windowTable valueForKey:actualWindow] elementAt:0]) != NULL) { info->hold = hold; [self sendPageIfPossible]; } mutex_unlock(memoryInUse); return tag; } - (unsigned)nextSubpage:(VTSubpageNumber *)requested { windowInfo *info; VTSubpageNumber new; mutex_lock(memoryInUse); tag++; if ((info = [(Storage *)[windowTable valueForKey:actualWindow] elementAt:0]) != NULL) { info->hold = NO; info->careSubpage = YES; if ([self smallestSubpageOf:info->page afterSubpage:info->subpage newSubpage:&new] != nil) { info->subpage = new; } if (requested != NULL) *requested = info->subpage; [self sendPageIfPossible]; } mutex_unlock(memoryInUse); return tag; } - (unsigned)previousSubpage:(VTSubpageNumber *)requested { windowInfo *info; VTSubpageNumber new; mutex_lock(memoryInUse); tag++; if ((info = [(Storage *)[windowTable valueForKey:actualWindow] elementAt:0]) != NULL) { info->hold = NO; info->careSubpage = YES; if ([self greatestSubpageOf:info->page beforeSubpage:info->subpage newSubpage:&new] != nil) { info->subpage = new; } if (requested != NULL) *requested = info->subpage; [self sendPageIfPossible]; } mutex_unlock(memoryInUse); return tag; } - requestClockMessages:(BOOL)flag { int hostMsg; clockMessages = flag; hostMsg = HM_SYS_CONFIG; if (pageMessages) hostMsg |= 1; if (clockMessages) hostMsg |= 2; mutex_lock(sending); snddriver_dsp_write(cmd_port, &hostMsg, 1, sizeof(int), SNDDRIVER_HIGH_PRIORITY); mutex_unlock(sending); return self; } - requestPageMessages:(BOOL)flag { int hostMsg; pageMessages = flag; hostMsg = HM_SYS_CONFIG; if (pageMessages) hostMsg |= 1; if (clockMessages) hostMsg |= 2; mutex_lock(sending); snddriver_dsp_write(cmd_port, &hostMsg, 1, sizeof(int), SNDDRIVER_HIGH_PRIORITY); mutex_unlock(sending); return self; } - writeModeRegister:(unsigned char)data { int hostMsg; hostMsg = HM_MODE | data; mutex_lock(sending); snddriver_dsp_write(cmd_port, &hostMsg, 1, sizeof(int), SNDDRIVER_HIGH_PRIORITY); mutex_unlock(sending); return self; } - writeDisplayControlRegisters:(unsigned char)normal:(unsigned char)news { int hostMsg; hostMsg = HM_DISP_CNTL | (normal << 8) | news; mutex_lock(sending); snddriver_dsp_write(cmd_port, &hostMsg, 1, sizeof(int), SNDDRIVER_HIGH_PRIORITY); mutex_unlock(sending); return self; } - writeDisplayModeRegister:(unsigned char)data { int hostMsg; hostMsg = HM_DISP_MODE | data; mutex_lock(sending); snddriver_dsp_write(cmd_port, &hostMsg, 1, sizeof(int), SNDDRIVER_HIGH_PRIORITY); mutex_unlock(sending); return self; } @end static void dspThread(PageSupplier *self) { snddriver_handlers_t handlers = {self, 0, 0, 0, 0, 0, 0, 0, (sndreply_recorded_data_t)dataHandler, 0, (sndreply_dsp_msg_t)msgHandler}; msg_header_t *reply_msg; msg_return_t retValue; reply_msg = (msg_header_t *)malloc(MSG_SIZE_MAX); snddriver_stream_start_reading(self->read_port, NULL, DMA_BUFFER_SIZE, 0, 0, 0, 0, 0, 0, 0, self->reply_port); snddriver_stream_start_reading(self->read_port, NULL, DMA_BUFFER_SIZE, 0, 0, 0, 0, 0, 0, 0, self->reply_port); snddriver_dspcmd_req_msg(self->cmd_port, self->reply_port); while (!self->DSPThreadAborted) { reply_msg->msg_size = MSG_SIZE_MAX; reply_msg->msg_local_port = self->reply_port; retValue = msg_receive(reply_msg, MSG_OPTION_NONE, 0); if (retValue != RCV_SUCCESS) { mutex_lock(self->comMem); self->error = YES; self->errorString = "Fehler beim Nachrichtenempfang"; mutex_unlock(self->comMem); condition_signal(self->comMemChanged); self->DSPThreadAborted = YES; } else { snddriver_reply_handler(reply_msg, &handlers); } } } static void dataHandler(PageSupplier *self, int tag, unsigned char *data, int n) { Storage *pageContainer; VTPageNumber pageNum; VTSubpageNumber subpageNum; windowInfo *info; if (n == 1024) { VTNumbersFromPageData(&pageNum, &subpageNum, data); mutex_lock(self->memoryInUse); // Daten in Cache eintragen, wenn erlaubt if (!self->depositValid || self->depositPage != pageNum || self->depositSubpage != subpageNum) { pageContainer = [self dataOfPage:pageNum subpage:subpageNum create:YES]; [pageContainer replace:data at:0]; } else // sonst zwischenspeichern { [self->deposit replace:data at:0]; self->depositFull = YES; pageContainer = nil; } if (((info = [(Storage *)[self->windowTable valueForKey:self->actualWindow] elementAt:0]) != NULL) && (!info->hold) && (info->page == pageNum) && ((!info->careSubpage) || (info->subpage == subpageNum))) { info->subpage = subpageNum; if (pageContainer != nil) { mutex_lock(self->comMem); self->pagePacket.data = [pageContainer elementAt:0]; self->pagePacket.window = self->actualWindow; self->pagePacket.tag = self->tag; self->pageReady = YES; mutex_unlock(self->comMem); condition_signal(self->comMemChanged); } } mutex_unlock(self->memoryInUse); } if (snddriver_stream_start_reading(self->read_port, NULL, DMA_BUFFER_SIZE, 0, 0, 0, 0, 0, 0, 0, self->reply_port) != 0) { mutex_lock(self->comMem); self->error = YES; self->errorString = "Fehler beim Anfordern weiterer Seitendaten."; mutex_unlock(self->comMem); condition_signal(self->comMemChanged); self->DSPThreadAborted = YES; } vm_deallocate(task_self(), (pointer_t)data, n); } static void msgHandler(PageSupplier *self, int *data, int n) { int i,j; for (i=0; i<n; i++) { switch (data[i] >> 16) { case 0x10: self->pageString[2] = BIN2ASC((data[i] >> 10) & 0xf); self->pageString[1] = BIN2ASC((data[i] >> 5) & 0xf); break; case 0x11: break; case 0x12: self->interruptedSequenz = (data[i] & 0x1000) ? YES : NO; self->pageString[0] = BIN2ASC(data[i] & 0x7); if (self->pageString[0] == '0') self->pageString[0] = '8'; mutex_lock(self->comMem); if (!self->interruptedSequenz && self->pageMessages) { for (j=0; j<4; j++) self->pageString2[j] = self->pageString[j]; self->pageNumReady = YES; } mutex_unlock(self->comMem); condition_signal(self->comMemChanged); break; case 0x13: self->clockString[0] = (data[i] >> 8) & 0xff; self->clockString[1] = data[i] & 0xff; break; case 0x14: self->clockString[2] = (data[i] >> 8) & 0xff; self->clockString[3] = data[i] & 0xff; break; case 0x15: self->clockString[4] = (data[i] >> 8) & 0xff; self->clockString[5] = data[i] & 0xff; break; case 0x16: self->clockString[6] = (data[i] >> 8) & 0xff; self->clockString[7] = data[i] & 0xff; mutex_lock(self->comMem); for (j=0; j<9; j++) self->clockString2[j] = self->clockString[j]; self->clockReady = YES; mutex_unlock(self->comMem); condition_signal(self->comMemChanged); break; case 0x17: mutex_lock(self->comMem); self->error = YES; mutex_unlock(self->comMem); condition_signal(self->comMemChanged); self->DSPThreadAborted = YES; break; default: break; } } if (!self->DSPThreadAborted) if (snddriver_dspcmd_req_msg(self->cmd_port, self->reply_port) != 0) { mutex_lock(self->comMem); self->error = YES; self->errorString = "Fehler beim Anfordern weiterer DSP-Messages."; mutex_unlock(self->comMem); condition_signal(self->comMemChanged); self->DSPThreadAborted = YES; } } static void comThread(PageSupplier *self) { enum {pag, clk, pgn, stop} whatToDo; PagePacket pagePacket; char clockString[9]; char pageString[4]; char *errorString; int i, dummy; errorString = NULL; while (!self->threadsAborted) { mutex_lock(self->comMem); while(!self->pageReady && !self->clockReady && !self->pageNumReady && !self->error) condition_wait(self->comMemChanged, self->comMem); if (self->pageReady) { whatToDo = pag; pagePacket = self->pagePacket; self->pageReady = NO; } else if (self->clockReady) { whatToDo = clk; for (i=0; i<9; i++) clockString[i] = self->clockString2[i]; self->clockReady = NO; } else if (self->pageNumReady) { whatToDo = pgn; for (i=0; i<4; i++) pageString[i] = self->pageString2[i]; self->pageNumReady = NO; } else { whatToDo = stop; errorString = (char *)self->errorString; } mutex_unlock(self->comMem); switch (whatToDo) { case pag: mutex_lock(self->memoryInUse); if (self->tag == pagePacket.tag) { [self requestPageMessages:NO]; // ab jetzt dörfen die Seitendaten nicht mehr verÙndert werden VTNumbersFromPageData(&(VTPageNumber)self->depositPage, &(VTSubpageNumber)self->depositSubpage, pagePacket.data); self->depositValid = YES; mutex_unlock(self->memoryInUse); // richtige Seitennummer senden stringFromVTPageNumber(pageString, self->depositPage); [self->speaker updateRollingHeader:pageString size:4 return:&dummy]; // Seitendaten senden [self->speaker updatePageData:(char *)&pagePacket size:sizeof(pagePacket) return:&dummy]; // evtl. zwischengespeicherte Seite einfögen mutex_lock(self->memoryInUse); if (self->depositFull) { [[self dataOfPage:self->depositPage subpage:self->depositSubpage create:YES] replace:[self->deposit elementAt:0] at:0]; } self->depositFull = NO; self->depositValid = NO; mutex_unlock(self->memoryInUse); // šberschreiben der Seitennummer verhindern self->pageNumReady = NO; } else mutex_unlock(self->memoryInUse); break; case clk: [self->speaker updateClock:clockString size:9 return:&dummy]; break; case pgn: [self->speaker updateRollingHeader:pageString size:4 return:&dummy]; break; case stop: if (errorString != NULL) [self->speaker printError:errorString]; [self->speaker threadsStopped]; self->threadsAborted = YES; break; } } }
These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.