1 // Emacs style mode select -*- C++ -*-
2 //-----------------------------------------------------------------------------
3 //
4 // $Id: i_sound.c 1471 2019-10-04 08:59:55Z wesleyjohnson $
5 //
6 // Copyright (C) 1993-1996 by id Software, Inc.
7 //
8 // This source is available for distribution and/or modification
9 // only under the terms of the DOOM Source Code License as
10 // published by id Software. All rights reserved.
11 //
12 // The source is distributed in the hope that it will be useful,
13 // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 // FITNESS FOR A PARTICULAR PURPOSE. See the DOOM Source Code License
15 // for more details.
16 //
17 // Revision 1.2 2003/07/13 13:18:59 hurdler
18 // Revision 1.1 2001/04/17 22:23:38 calumr
19 // Revision 1.1 2000/08/21 21:17:32 metzgermeister
20 // Initial import to CVS
21 //
22 // DESCRIPTION:
23 // System interface for sound.
24 //
25 //-----------------------------------------------------------------------------
26
27 #include <math.h>
28 #include <unistd.h>
29 #include <fcntl.h>
30 #include <Carbon/Carbon.h>
31
32 #include "doomincl.h"
33 #include "doomstat.h"
34
35 #include "i_system.h"
36 #include "i_sound.h"
37 #include "m_argv.h"
38 #include "m_misc.h"
39 #include "m_random.h"
40 #include "w_wad.h"
41 // ?? no longer gets own lumps
42 #include "s_sound.h"
43 #include "command.h"
44 // consvar_t
45 #include "m_swap.h"
46 #include "d_main.h"
47 #include "z_zone.h"
48
49
50 static void COM_PlaySong (void);
51
52 // The number of internal mixing channels,
53 // the samples calculated for each mixing step,
54 // the size of the 16bit, 2 hardware channel (stereo)
55 // mixing buffer, and the samplerate of the raw data.
56
57 // Needed for calling the actual sound output.
58 static int SAMPLECOUNT= 512;
59 #define NUM_CHANNELS 16
60
61 #define SAMPLERATE 11025 // Hz
62
63 // The channel data pointers, start and end.
64 unsigned char* channels[NUM_CHANNELS];
65
66 // Time/gametic that the channel started playing,
67 // used to determine oldest, which automatically
68 // has lowest priority.
69 // In case number of active sounds exceeds
70 // available channels.
71 int channelstart[NUM_CHANNELS];
72
73 // SFX id of the playing sound effect.
74 // Used to catch duplicates (like chainsaw).
75 int channelids[NUM_CHANNELS];
76
77 // Flags for the -nosound and -nomusic options
78 extern boolean nosoundfx;
79 extern boolean nomusic;
80
81 //start of mac stuff
82 static SndChannelPtr soundChannels[NUM_CHANNELS];
83 static int channelbusy[NUM_CHANNELS];
84
soundCallback(SndChannelPtr soundChannel,SndCommand * pCmd)85 static pascal void soundCallback (SndChannelPtr soundChannel, SndCommand *pCmd)
86 {
87 if (pCmd->param1 == 0x1234)
88 {
89 int *channelInUse = (int *)pCmd->param2;
90 *channelInUse = 0;
91 }
92 }
93
94
95 //
96 // This function adds a sound to the list of currently active sounds,
97 // which is maintained as a given number
98 // (eight, usually) of internal channels.
99 // Returns a handle.
100 //
101 // vol : volume, 0..255
102 // sep : separation, +/- 127, SURROUND_SEP special operation
addsfx(int sfxid,int vol,int step,int sep)103 static int addsfx ( int sfxid,
104 int vol,
105 int step,
106 int sep )
107 {
108 int i;
109 int slot;
110 int rightvol, leftvol;
111
112 // Chainsaw troubles.
113 // Play these sound effects only one at a time.
114 if (S_sfx[sfxid].flags & SFX_single)
115 {
116 // Loop all channels, check.
117 for (i=0 ; i<NUM_CHANNELS ; i++)
118 {
119 // Active, and using the same SFX?
120 if ( (channels[i])
121 && (channelids[i] == sfxid) )
122 {
123 if( S_sfx[sfxid].flags & SFX_id_fin )
124 return i; // already have one, return slot
125 // Reset.
126 channels[i] = 0;
127 break;
128 }
129 }
130 }
131
132 // Loop all channels to find oldest SFX.
133 slot = 0; // default
134 int oldest = INT_MAX;
135 for (i=0; i<NUM_CHANNELS; i++)
136 {
137 if ( channels[i] == 0 ) // unused
138 {
139 slot = i;
140 break;
141 }
142 if (channelstart[i] < oldest) // older
143 {
144 slot = i;
145 oldest = channelstart[i];
146 }
147 }
148
149 // Okay, in the less recent channel,
150 // we will handle the new SFX.
151 // Set pointer to raw data.
152 channels[slot] = (unsigned char *) S_sfx[sfxid].data;
153
154 // Should be gametic, I presume.
155 channelstart[slot] = gametic;
156
157 // volume : range 0..255
158 // mix_sfxvolume : range 0..31
159 vol = (vol * mix_sfxvolume) >> 6;
160 // Notice : sdldoom replaced all the calls to avoid this conversion
161
162 #ifdef SURROUND_SOUND
163 if( sep > 128 ) sep = 0; // No SURROUND
164 #endif
165 // Separation, that is, orientation/stereo.
166 // sep : +/- 127, <0 is left, >0 is right
167 sep += 129; // 129 +/- 127 ; ( 1 - 256 )
168 leftvol = vol - ((vol * sep * sep) >> 16);
169 sep = 258 - sep; // 129 +/- 127
170 rightvol = vol - ((vol * sep * sep) >> 16);
171
172 // Sanity check, clamp volume.
173 if (rightvol < 0 || rightvol > 127)
174 {
175 I_SoftError("rightvol out of bounds\n");
176 rightvol = ( rightvol < 0 ) ? 0 : 127;
177 }
178
179 if (leftvol < 0 || leftvol > 127)
180 {
181 I_SoftError("leftvol out of bounds\n");
182 leftvol = ( leftvol < 0 ) ? 0 : 127;
183 }
184
185 // Preserve sound SFX id,
186 // e.g. for avoiding duplicates of chainsaw.
187 channelids[slot] = sfxid;
188
189 {
190 ExtSoundHeader theSndBuffer;
191 SndCommand theCmd;
192
193 theCmd.param1 = 0;
194 theCmd.param2 = (rightvol << 16) + leftvol;
195 theCmd.cmd = volumeCmd;
196 SndDoImmediate (soundChannels[slot], &theCmd);
197
198 theSndBuffer.samplePtr = (Ptr) S_sfx[sfxid].data;
199 theSndBuffer.numFrames = S_sfx[sfxid].length;
200 theSndBuffer.numChannels = 1; // 2 for stereo
201 theSndBuffer.sampleRate = rate11025hz;
202 theSndBuffer.encode = extSH;
203 theSndBuffer.sampleSize = 8; // 8-bit data
204
205 // Send the buffer to the channel
206 theCmd.param1 = 0;
207 theCmd.param2 = (long) &theSndBuffer;
208 theCmd.cmd = bufferCmd;
209
210 //SndDoCommand (soundChannels[slot], &theCmd, false);
211 SndDoImmediate (soundChannels[slot], &theCmd);
212 channelbusy[slot] = 1;
213
214 theCmd.param1 = 0x1234;
215 theCmd.param2 = (long) &channelbusy[slot];
216 theCmd.cmd = callBackCmd;
217 SndDoCommand (soundChannels[slot], &theCmd, false);
218 }
219
220 return slot; // handle is slot
221 }
222
I_SetChannels(void)223 void I_SetChannels(void)
224 {}
225
I_SetSfxVolume(int volume)226 void I_SetSfxVolume(int volume)
227 {
228 // Can use mix_sfxvolume (0..31), or set local volume vars.
229 // mix_sfxvolume = volume;
230 }
231
232
I_GetSfx(sfxinfo_t * sfx)233 void I_GetSfx (sfxinfo_t* sfx)
234 {
235 unsigned char* sfxdata;
236 unsigned char* paddedsfx;
237 int i;
238 int size;
239 int paddedsize;
240
241 S_GetSfxLump( sfx ); // lump to sfx
242 sfxdata = (unsigned char*) sfx->data;
243 if( ! sfxdata ) return;
244 size = sfx->length;
245
246 // Pads the sound effect out to the mixing buffer size.
247 // The original realloc would interfere with zone memory.
248 paddedsize = ((size-8 + (SAMPLECOUNT-1)) / SAMPLECOUNT) * SAMPLECOUNT;
249
250 // Allocate from zone memory.
251 paddedsfx = (unsigned char*)Z_Malloc( paddedsize+8, PU_STATIC, 0 );
252 // ddt: (unsigned char *) realloc(sfx, paddedsize+8);
253 // This should interfere with zone memory handling,
254 // which does not kick in in the soundserver.
255
256 // Now copy and pad.
257 memcpy( paddedsfx, sfxdata, size );
258 for (i=size ; i<paddedsize+8 ; i++)
259 paddedsfx[i] = 128;
260
261 // Remove the cached lump.
262 Z_Free( sfxdata );
263
264 // Preserve padded length.
265 sfx->length = paddedsize;
266 // Return allocated padded data.
267 sfx->data = (void *) (paddedsfx + 8); // skip header
268 }
269
I_FreeSfx(sfxinfo_t * sfx)270 void I_FreeSfx (sfxinfo_t* sfx)
271 {
272 // use default Free
273 if( sfx->data )
274 sfx->data -= 8; // undo skip header, for Z_Free
275 }
276
277 // vol : volume, 0..255
278 // Return a channel handle.
I_StartSound(sfxid_t sfxid,int vol,int sep,int pitch,int priority)279 int I_StartSound(sfxid_t sfxid, int vol, int sep, int pitch, int priority)
280 {
281 // UNUSED
282 priority = 0;
283
284 if(nosoundfx)
285 return 0;
286
287 id = addsfx( id, vol, pitch, sep );
288
289 return id;
290 }
291
292 // handle : the handle returned by StartSound.
I_StopSound(int handle)293 void I_StopSound (int handle)
294 {
295 SndCommand theCmd;
296
297 if (handle < 0 || handle >= NUM_CHANNELS) return;
298
299 // Immediately stop this sound
300 theCmd.param1 = 0;
301 theCmd.param2 = 0;
302 theCmd.cmd = quietCmd;
303 SndDoImmediate (soundChannels[handle], &theCmd);
304 theCmd.cmd = flushCmd;
305 SndDoImmediate (soundChannels[handle], &theCmd);
306
307 channelbusy[handle] = 0;
308 }
309
310 // handle : the handle returned by StartSound.
I_SoundIsPlaying(int handle)311 int I_SoundIsPlaying(int handle)
312 {
313 return channelbusy[handle];
314 }
315
I_UpdateSound(void)316 void I_UpdateSound (void)
317 {
318 MusicEvents(); //for QuickTime music playing
319 }
320
I_SubmitSound(void)321 void I_SubmitSound(void)
322 {}
323
324 // You need the handle returned by StartSound.
I_UpdateSoundParams(int handle,int vol,int sep,int pitch)325 void I_UpdateSoundParams(int handle, int vol, int sep, int pitch)
326 {
327 SndCommand theCmd;
328 int leftvol, rightvol;
329
330 if(nosoundfx)
331 return;
332
333 // vol : range 0..255
334 // mix_sfxvolume : range 0..31
335 vol = (vol * mix_sfxvolume) >> 6;
336
337 #ifdef SURROUND_SOUND
338 if( sep > 128 ) sep = 0; // No SURROUND
339 #endif
340 // Separation, that is, orientation/stereo.
341 // sep : +/- 127, <0 is left, >0 is right
342 sep += 129; // 129 +/- 127 ; ( 1 - 256 )
343 leftvol = vol - ((vol * sep * sep) >> 16);
344 sep = 258 - sep; // 129 +/- 127
345 rightvol = vol - ((vol * sep * sep) >> 16);
346
347 // Send the volume to the channel
348 theCmd.param1 = 0;
349 theCmd.param2 = (rightvol << 16) + leftvol;
350 theCmd.cmd = volumeCmd;
351 SndDoImmediate (soundChannels[handle], &theCmd);
352 }
353
354
I_ShutdownSound(void)355 void I_ShutdownSound(void)
356 {
357 int i;
358
359 if(nosoundfx)
360 return;
361
362 CONS_Printf("I_ShutdownSound:\n");
363
364 for (i = 0; i < NUM_CHANNELS; i++)
365 {
366 SndDisposeChannel (soundChannels[i], true);
367 }
368
369 CONS_Printf("\tshut down\n");
370 }
371
372
I_StartupSound()373 void I_StartupSound()
374 {
375 int i;
376 int err;
377
378 if(nosoundfx)
379 return;
380
381 // Configure sound device
382 CONS_Printf("I_InitSound: \n");
383
384 for (i = 0; i < NUM_CHANNELS; i ++)
385 {
386 soundChannels[i] = NULL;
387 channelbusy[i] = 0;
388 err = SndNewChannel (&soundChannels[i], sampledSynth, initMono, NewSndCallBackUPP(soundCallback));
389 }
390
391 #if 0
392 for (i=1 ; i<NUMSFX ; i++)
393 {
394 // Alias? Example is the chaingun sound linked to pistol.
395 if (S_sfx[i].name) {
396 if (!S_sfx[i].link)
397 {
398 // Load data from WAD file.
399 S_sfx[i].data = getsfx( &S_sfx[i] );
400 }
401 else
402 {
403 // Previously loaded already?
404 S_sfx[i].data = S_sfx[i].link->data;
405 S_sfx[i].length = S_sfx[i].link->length;
406 }
407 }
408 }
409 #endif
410
411 CONS_Printf("\tpre-cached all sound data\n");
412 }
413
414 //
415 // MUSIC API.
416 //
417
418 #include <QuickTime/Movies.h>
419
420 static int mus_song = 0;
421 static int musicVolume = 15;
422
423 static Movie midiMovie;
424 static Boolean midiLoop;
425
426 consvar_t user_songs[PLAYLIST_LENGTH];
427
428 boolean PlayThis(char *name);
429
COM_SkipNext(void)430 static void COM_SkipNext (void)
431 {
432 mus_song++;
433
434 DisposeMovie (midiMovie);
435 midiMovie = NULL;
436
437 if (mus_song==PLAYLIST_LENGTH)
438 mus_song = 0;
439
440 if (PlayThis(user_songs[mus_song].string))
441 {
442 CONS_Printf("Playing next song\n");
443 }
444 else
445 {
446 CV_Set(&user_songs[mus_song], " ");
447 }
448 }
449
COM_SkipPrev(void)450 static void COM_SkipPrev (void)
451 {
452 mus_song--;
453
454 DisposeMovie (midiMovie);
455 midiMovie = NULL;
456
457 if (mus_song==-1)
458 mus_song = PLAYLIST_LENGTH;
459
460 if (PlayThis(user_songs[mus_song].string))
461 {
462 CONS_Printf("Playing next song\n");
463 }
464 else
465 {
466 CV_Set(&user_songs[mus_song], " ");
467 }
468 }
469
COM_PlayListRandom(void)470 static void COM_PlayListRandom (void)
471 {
472 CV_SetValue(&play_mode, playlist_random);
473 mus_song = M_Random() % PLAYLIST_LENGTH;
474
475 CONS_Printf("Playing random user_songs from play list\n");
476 }
477
COM_PlayList(void)478 static void COM_PlayList (void)
479 {
480 CV_SetValue(&play_mode, playlist_normal);
481 mus_song = 0;
482
483 CONS_Printf("Playing play list\n");
484 }
485
COM_PlayListStop(void)486 static void COM_PlayListStop (void)
487 {
488 CV_SetValue(&play_mode, music_normal);
489 mus_song = 0;
490 DisposeMovie (midiMovie);
491 midiMovie = NULL;
492
493 CONS_Printf("Stopped play list\n");
494 }
495
MusicEvents(void)496 void MusicEvents (void)
497 {
498 if (nomusic)
499 return;
500
501 if (midiMovie)
502 {
503 // Let QuickTime get some time
504 MoviesTask (midiMovie, 0);
505
506 // If this song is looping, restart it
507 if (IsMovieDone (midiMovie))
508 {
509 if (midiLoop)
510 {
511 GoToBeginningOfMovie (midiMovie);
512 StartMovie (midiMovie);
513 }
514 else
515 {
516 DisposeMovie (midiMovie);
517 midiMovie = NULL;
518 }
519 }
520 }
521 else if (play_mode.value == playlist_normal)
522 {
523 mus_song++;
524 if (mus_song==PLAYLIST_LENGTH)
525 mus_song = 0;
526 if (PlayThis(user_songs[mus_song].string))
527 {
528 CONS_Printf("Playing next song\n");
529 }
530 else
531 {
532 CV_Set(&user_songs[mus_song], "");
533 }
534 }
535 else if (play_mode.value == playlist_random)
536 {
537 mus_song = M_Random() % PLAYLIST_LENGTH;
538 if (PlayThis(user_songs[mus_song].string))
539 {
540 CONS_Printf("Playing next song\n");
541 }
542 else
543 {
544 CV_Set(&user_songs[mus_song], "");
545 }
546 }
547 }
548
I_ShutdownMusic(void)549 void I_ShutdownMusic(void)
550 {
551 if(nomusic)
552 return;
553
554 CONS_Printf("I_ShutdownMusic:\n");
555
556 if (midiMovie)
557 {
558 StopMovie (midiMovie);
559 DisposeMovie (midiMovie);
560 ExitMovies ();
561 midiMovie = NULL;
562 }
563
564 CONS_Printf("\tshut down\n");
565 }
566
I_InitMusic(void)567 void I_InitMusic(void)
568 {
569 if(nomusic)
570 return;
571
572 CONS_Printf("I_InitMusic:\n");
573
574 if (EnterMovies () != noErr)
575 {
576 CONS_Printf("\tI_InitMusic: Couldn't initialise Quicktime\n");
577 nomusic = true;
578 }
579
580 mus_song = 0;
581 midiMovie = NULL;
582
583 COM_AddCommand ("playsong",COM_PlaySong);
584 COM_AddCommand ("playrandom",COM_PlayListRandom);
585 COM_AddCommand ("playlist",COM_PlayList);
586 COM_AddCommand ("stopplaylist",COM_PlayListStop);
587
588 COM_AddCommand ("nextsong",COM_SkipNext);
589 COM_AddCommand ("prevsong",COM_SkipPrev);
590
591 CONS_Printf("\tdone\n");
592 }
593
I_PlaySong(int handle,int looping)594 void I_PlaySong(int handle, int looping)
595 {
596 if(nomusic)
597 return;
598
599 if (play_mode.value != music_normal)
600 return;
601
602 midiLoop = looping;
603 if (midiMovie)
604 {
605 StartMovie (midiMovie);
606 }
607 }
608
I_PauseSong(int handle)609 void I_PauseSong (int handle)
610 {
611 if(nomusic)
612 return;
613
614 if (play_mode.value != music_normal)
615 return;
616
617 if (midiMovie)
618 {
619 StopMovie (midiMovie);
620 }
621 }
622
I_ResumeSong(int handle)623 void I_ResumeSong (int handle)
624 {
625 if(nomusic)
626 return;
627
628 if (play_mode.value != music_normal)
629 return;
630
631 if (midiMovie)
632 {
633 StartMovie (midiMovie);
634 }
635 }
636
I_StopSong(int handle)637 void I_StopSong(int handle)
638 {
639 if(nomusic)
640 return;
641
642 if (play_mode.value != music_normal)
643 return;
644
645 if (midiMovie)
646 {
647 StopMovie (midiMovie);
648 }
649 }
650
I_UnRegisterSong(int handle)651 void I_UnRegisterSong(int handle)
652 {
653 if(nomusic)
654 return;
655
656 if (play_mode.value != music_normal)
657 return;
658
659 if (midiMovie)
660 {
661 StopMovie (midiMovie);
662 DisposeMovie (midiMovie);
663 midiMovie = NULL;
664 }
665 }
666
PlayThis(char * name)667 boolean PlayThis(char *name)
668 {
669 FSSpec midiSpec;
670 OSErr err;
671 short midiRef;
672 char mid_file[256];
673 FSRef ref;
674
675 if(nomusic)
676 return false;
677
678 if (midiMovie)
679 DisposeMovie (midiMovie);
680 midiMovie = NULL;
681
682 if (!name || *name == 0)
683 return false;
684
685 {
686 char *path;
687
688 if (getenv("DOOMMUSICDIR"))
689 {
690 path = getenv("DOOMMUSICDIR");
691 sprintf(mid_file, "%s/%s", path, name);
692 }
693 else
694 {
695 #ifdef __MACH__
696 //[segabor]: If Music folder is in the Resources folder get mid from there
697 extern char mac_music_home[256];
698
699 if (mac_music_home[0] == '.')
700 sprintf(mid_file, "./Music/%s", name);
701 else
702 sprintf(mid_file, "%s/%s", mac_music_home, name);
703 #else
704 path = malloc(256);
705 if ( getcwd(path, 256) == NULL )
706 path = ".";
707 sprintf(mid_file, "%s/Music/%s", path, name);
708 free(path);
709 #endif
710 }
711 }
712
713 I_OutputMsg("i_sound: Attempting to play %s\n", mid_file);
714
715 err = FSPathMakeRef(mid_file, &ref, NULL);
716 if (err)
717 {
718 I_OutputMsg("PlayThis: FSPathMakeRef = %i\n", err);
719 return false;
720 }
721
722 err = FSGetCatalogInfo(&ref, kFSCatInfoNone, NULL, NULL, &midiSpec, NULL);
723 if (err)
724 {
725 I_OutputMsg("PlayThis: FSGetCatalogInfo = %i\n", err);
726 return false;
727 }
728
729 err = OpenMovieFile (&midiSpec, &midiRef, fsRdPerm);
730 if (err)
731 {
732 I_OutputMsg("PlayThis: OpenMovieFile = %i\n", err);
733 return false;
734 }
735
736 err = NewMovieFromFile (&midiMovie, midiRef, NULL, NULL, newMovieActive, NULL);
737 if (err)
738 {
739 I_OutputMsg("PlayThis: NewMovieFromFile = %i\n", err);
740 return false;
741 }
742
743 GoToBeginningOfMovie (midiMovie);
744 PrerollMovie (midiMovie, 0, 0x10000);
745 SetMovieVolume (midiMovie, musicVolume << 3);
746 StartMovie (midiMovie);
747
748 CloseMovieFile (midiRef);
749
750 return true;
751 }
752
753 // [WDJ] len is unused, keep compatible API
I_RegisterSong(int handle,int len)754 int I_RegisterSong(int handle, int len)
755 {
756 Str63 name = "";
757
758 if (play_mode.value != music_normal)
759 return handle;
760
761 // Make sure song number is valid
762 if (handle < mus_e1m1 || handle > NUMMUSIC)
763 return -1;
764
765 mus_song = handle;
766
767 strcat ((char *)name, S_music[handle].name);
768 strcat((char *)name, ".mid!");
769 strupr(name);
770 PlayThis(name);
771
772 return handle;
773 }
774
COM_PlaySong(void)775 static void COM_PlaySong (void)
776 {
777 char *name;
778
779 if (COM_Argc()<2)
780 {
781 CONS_Printf("Usage: playsong \"name\"\n\tplaysong <number>\n");
782 CONS_Printf("\tRemember to use quotes around the name!\n");
783 return;
784 }
785
786 name = Z_StrDup (COM_Argv(1));
787
788 if (strlen(name)==1)
789 {
790 CONS_Printf("Playing song 0%i from playlist\n", (name[0]-'0'));
791 PlayThis(user_songs[(name[0]-'0')].string);
792 }
793 else if (strlen(name)==2)
794 {
795 CONS_Printf("Playing song %i from playlist\n", (name[1]-'0') + (name[0]-'0')*10);
796 PlayThis(user_songs[(name[1]-'0') + (name[0]-'0')*10].string);
797 }
798 else
799 PlayThis(name);
800
801 Z_Free(name);
802 }
803
I_SetMusicVolume(int volume)804 void I_SetMusicVolume(int volume)
805 {
806 if(nomusic)
807 return;
808
809 musicVolume = volume;
810 if (midiMovie)
811 SetMovieVolume (midiMovie, musicVolume << 3);
812 }
813
814 //Hurdler: TODO
I_StartFMODSong()815 void I_StartFMODSong()
816 {
817 CONS_Printf("I_StartFMODSong: Not yet supported under MacOS.\n");
818 }
819
I_StopFMODSong()820 void I_StopFMODSong()
821 {
822 CONS_Printf("I_StopFMODSong: Not yet supported under MacOS.\n");
823 }
I_SetFMODVolume(int volume)824 void I_SetFMODVolume(int volume)
825 {
826 CONS_Printf("I_SetFMODVolume: Not yet supported under MacOS.\n");
827 }
828