From 490702cc45f53476eb2ef25bfb7501f852faf31d Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Tue, 10 Jul 2018 01:33:11 -0400 Subject: [PATCH] Implemented DosBeep in doscalls and a simple audio mixer in lib2ine. --- CMakeLists.txt | 2 + lib2ine.c | 163 +++++++++++++++++++++++++++++++++++++++++-- lib2ine.h | 9 +++ lx_loader.c | 2 + native/doscalls-lx.h | 10 +++ native/doscalls.c | 143 +++++++++++++++++++++++++++++++++++++ native/doscalls.h | 2 + 7 files changed, 324 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3ffe651..4b9eff9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -96,8 +96,10 @@ target_link_libraries(viocalls ncursesw) # !!! FIXME: clean this up/ if(LX_LEGACY) + target_link_libraries(2ine "${CMAKE_CURRENT_SOURCE_DIR}/libSDL2-2.0.so.0") target_link_libraries(pmwin "${CMAKE_CURRENT_SOURCE_DIR}/libSDL2-2.0.so.0") else() + target_link_libraries(2ine "SDL2") target_link_libraries(pmwin "SDL2") endif() diff --git a/lib2ine.c b/lib2ine.c index fbbd99d..3aac15d 100644 --- a/lib2ine.c +++ b/lib2ine.c @@ -23,8 +23,10 @@ #include #include #include +#include #include "lib2ine.h" +#include "SDL.h" extern char **environ; @@ -640,6 +642,157 @@ static void prepOs2Drives(void) #endif } +typedef struct LxAudioGeneratorInfo +{ + LxAudioGeneratorFn fn; + void *data; + struct LxAudioGeneratorInfo *next; +} LxAudioGeneratorInfo; + +static LxAudioGeneratorInfo *audio_generators = NULL; +static SDL_AudioDeviceID sdl_audio_device = 0; +static SDL_AudioSpec sdl_audio_spec; +static Uint32 no_audio_generators_timeout = 0; + +static int SDLCALL closeAudioDeviceThread(void *arg) +{ + const SDL_AudioDeviceID dev = (SDL_AudioDeviceID) (size_t) arg; + //printf("2ine: Closing idle audio device %u.\n", (uint) dev); + SDL_CloseAudioDevice(dev); + return 0; +} + +static void closeAudioDeviceFromAnotherThread(void) +{ + FIXME("Make this atomic"); + const SDL_AudioDeviceID dev = sdl_audio_device; + if (!sdl_audio_device) { + return; + } + sdl_audio_device = 0; + + SDL_Thread *thread = SDL_CreateThread(closeAudioDeviceThread, "2ine_close_audio_device", (void *) (size_t) dev); + if (thread) { + SDL_DetachThread(thread); + } else { + sdl_audio_device = dev; // oh well. + } +} + + +static void SDLCALL audioCallback(void *userdata, Uint8 *stream, int len) +{ + memset(stream, '\0', len); + LxAudioGeneratorInfo *generator = audio_generators; + if (!generator) { + if (!no_audio_generators_timeout) { + no_audio_generators_timeout = SDL_GetTicks() + 5000; + } else if (SDL_TICKS_PASSED(SDL_GetTicks(), no_audio_generators_timeout)) { + closeAudioDeviceFromAnotherThread(); + } + return; + } + + no_audio_generators_timeout = 0; + + const int samples = len / sizeof (float); + LxAudioGeneratorInfo *prev = NULL; + while (generator) { + LxAudioGeneratorInfo *next = generator->next; + if (generator->fn(generator->data, (float *) stream, samples, sdl_audio_spec.freq)) { + prev = generator; + } else { + // remove this generator, it's done. + if (prev) { + prev->next = next; + } else { + assert(generator == audio_generators); + audio_generators = next; + } + free(generator); + } + generator = next; + } +} + +static int registerAudioGenerator_lib2ine(LxAudioGeneratorFn fn, void *data, const int singleton) +{ + if (!sdl_audio_device) { + SDL_Init(SDL_INIT_AUDIO); + SDL_AudioSpec spec; + SDL_zero(spec); + spec.freq = 48000; + spec.format = AUDIO_F32SYS; + spec.channels = 1; + spec.samples = 1024; + spec.callback = audioCallback; + sdl_audio_device = SDL_OpenAudioDevice(NULL, 0, &spec, &sdl_audio_spec, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE); + if (!sdl_audio_device) { + SDL_QuitSubSystem(SDL_INIT_AUDIO); + return 0; + } + no_audio_generators_timeout = 0; + SDL_PauseAudioDevice(sdl_audio_device, 0); + } + + LxAudioGeneratorInfo *info = (LxAudioGeneratorInfo *) malloc(sizeof (LxAudioGeneratorInfo *)); + if (!info) { + return 0; + } + + info->fn = fn; + info->data = data; + + SDL_LockAudioDevice(sdl_audio_device); + + if (singleton) { + for (LxAudioGeneratorInfo *i = audio_generators; i != NULL; i = i->next) { + if (i->fn == fn) { + SDL_UnlockAudioDevice(sdl_audio_device); + free(info); + return 2; // already added. + } + } + } + + no_audio_generators_timeout = 0; + info->next = audio_generators; + audio_generators = info; + SDL_UnlockAudioDevice(sdl_audio_device); + return 1; +} // registerAudioGenerator_lib2ine + + +static void lib2ine_shutdown(void) +{ + if (sdl_audio_device) { + // let the audio callback run about two more times, to let any last queued things fully render. + SDL_Delay(((sdl_audio_spec.samples * 1000) / sdl_audio_spec.freq) * 2); + SDL_CloseAudioDevice(sdl_audio_device); + SDL_QuitSubSystem(SDL_INIT_AUDIO); + sdl_audio_device = 0; + } + + LxAudioGeneratorInfo *generator = audio_generators; + audio_generators = NULL; + while (generator) { + LxAudioGeneratorInfo *next = generator->next; + free(generator); + generator = next; + } + + for (int i = 0; i < (sizeof (GLoaderState.disks) / sizeof (GLoaderState.disks[0])); i++) { + free(GLoaderState.disks[i]); + GLoaderState.disks[i] = NULL; + } + + free(GLoaderState.pib.pib_pchenv); + GLoaderState.pib.pib_pchenv = NULL; + + memset(&GLoaderState, '\0', sizeof (GLoaderState)); + pthread_key_delete(tlskey); +} + LX_NATIVE_CONSTRUCTOR(lib2ine) { if (pthread_key_create(&tlskey, NULL) != 0) { @@ -662,6 +815,8 @@ LX_NATIVE_CONSTRUCTOR(lib2ine) GLoaderState.loadModule = loadModule_lib2ine; GLoaderState.makeUnixPath = makeUnixPath_lib2ine; GLoaderState.terminate = terminate_lib2ine; + GLoaderState.registerAudioGenerator = registerAudioGenerator_lib2ine; + GLoaderState.lib2ine_shutdown = lib2ine_shutdown; cfgLoadFiles(); prepOs2Drives(); @@ -689,13 +844,7 @@ LX_NATIVE_CONSTRUCTOR(lib2ine) LX_NATIVE_DESTRUCTOR(lib2ine) { - for (int i = 0; i < (sizeof (GLoaderState.disks) / sizeof (GLoaderState.disks[0])); i++) { - free(GLoaderState.disks[i]); - GLoaderState.disks[i] = NULL; - } - free(GLoaderState.pib.pib_pchenv); - memset(&GLoaderState, '\0', sizeof (GLoaderState)); - pthread_key_delete(tlskey); + lib2ine_shutdown(); } // end of lib2ine.c ... diff --git a/lib2ine.h b/lib2ine.h index 106f70b..c702550 100644 --- a/lib2ine.h +++ b/lib2ine.h @@ -273,6 +273,13 @@ typedef struct LxPostTIB void *anchor_block; } LxPostTIB; +// There are a few things that need access to the sound hardware: +// DosBeep(), MMOS/2, DART, basic system sounds, etc. We unify all this +// into a single SDL audio device open where possible, so that we don't +// have to deal with multiple devices, but that means it has to move into +// lib2ine so multiple libraries can access it. +typedef int (*LxAudioGeneratorFn)(void *data, float *stream, int len, int freq); + #define LXTIBSIZE (sizeof (LxTIB) + sizeof (LxTIB2) + sizeof (LxPostTIB)) #define LX_MAX_LDT_SLOTS 8192 @@ -318,6 +325,8 @@ typedef struct LxLoaderState LxModule *(*loadModule)(const char *modname); char *(*makeUnixPath)(const char *os2path, uint32 *err); void __attribute__((noreturn)) (*terminate)(const uint32 exitcode); + int (*registerAudioGenerator)(LxAudioGeneratorFn fn, void *data, const int singleton); + void (*lib2ine_shutdown)(void); } LxLoaderState; typedef const LxExport *(*LxNativeModuleInitEntryPoint)(uint32 *lx_num_exports); diff --git a/lx_loader.c b/lx_loader.c index 4db5e72..b16bb47 100644 --- a/lx_loader.c +++ b/lx_loader.c @@ -667,6 +667,8 @@ static void freeLxModule(LxModule *lxmod); static __attribute__((noreturn)) void lxTerminate(const uint32 exitcode) { + GLoaderState.lib2ine_shutdown(); + // free the actual .exe freeLxModule(GLoaderState.main_module); diff --git a/native/doscalls-lx.h b/native/doscalls-lx.h index bc04369..201ac56 100644 --- a/native/doscalls-lx.h +++ b/native/doscalls-lx.h @@ -72,6 +72,12 @@ static APIRET16 bridge16to32_Dos16GetMachineMode(uint8 *args) { return Dos16GetMachineMode(pmode); } +static APIRET16 bridge16to32_Dos16Beep(uint8 *args) { + LX_NATIVE_MODULE_16BIT_BRIDGE_ARG(USHORT, dur); + LX_NATIVE_MODULE_16BIT_BRIDGE_ARG(USHORT, freq); + return Dos16Beep(freq, dur); +} + static APIRET16 bridge16to32_Dos16ChDir(uint8 *args) { LX_NATIVE_MODULE_16BIT_BRIDGE_ARG(ULONG, res); LX_NATIVE_MODULE_16BIT_BRIDGE_PTRARG(PSZ, pszDir); @@ -264,6 +270,7 @@ LX_NATIVE_MODULE_16BIT_SUPPORT() LX_NATIVE_MODULE_16BIT_API(Dos16FreeSeg) LX_NATIVE_MODULE_16BIT_API(Dos16GetHugeShift) LX_NATIVE_MODULE_16BIT_API(Dos16GetMachineMode) + LX_NATIVE_MODULE_16BIT_API(Dos16Beep) LX_NATIVE_MODULE_16BIT_API(Dos16ChDir) LX_NATIVE_MODULE_16BIT_API(Dos16ChgFilePtr) LX_NATIVE_MODULE_16BIT_API(Dos16Close) @@ -308,6 +315,7 @@ static int init16_doscalls(void) { LX_NATIVE_INIT_16BIT_BRIDGE(Dos16FreeSeg, 2) LX_NATIVE_INIT_16BIT_BRIDGE(Dos16GetHugeShift, 4) LX_NATIVE_INIT_16BIT_BRIDGE(Dos16GetMachineMode, 4) + LX_NATIVE_INIT_16BIT_BRIDGE(Dos16Beep, 4) LX_NATIVE_INIT_16BIT_BRIDGE(Dos16ChDir, 8) LX_NATIVE_INIT_16BIT_BRIDGE(Dos16ChgFilePtr, 12) LX_NATIVE_INIT_16BIT_BRIDGE(Dos16Close, 2) @@ -349,6 +357,7 @@ LX_NATIVE_MODULE_INIT({ if (!init16_doscalls()) return 0; }) LX_NATIVE_EXPORT16(Dos16FreeSeg, 39), LX_NATIVE_EXPORT16(Dos16GetHugeShift, 41), LX_NATIVE_EXPORT16(Dos16GetMachineMode, 49), + LX_NATIVE_EXPORT16(Dos16Beep, 50), LX_NATIVE_EXPORT16(Dos16ChDir, 57), LX_NATIVE_EXPORT16(Dos16ChgFilePtr, 58), LX_NATIVE_EXPORT16(Dos16Close, 59), @@ -404,6 +413,7 @@ LX_NATIVE_MODULE_INIT({ if (!init16_doscalls()) return 0; }) LX_NATIVE_EXPORT(DosRead, 281), LX_NATIVE_EXPORT(DosWrite, 282), LX_NATIVE_EXPORT(DosExecPgm, 283), + LX_NATIVE_EXPORT(DosBeep, 286), LX_NATIVE_EXPORT(DosSetProcessCp, 289), LX_NATIVE_EXPORT(DosQueryCp, 291), LX_NATIVE_EXPORT(DosExitList, 296), diff --git a/native/doscalls.c b/native/doscalls.c index 897b91c..6d1ec17 100644 --- a/native/doscalls.c +++ b/native/doscalls.c @@ -25,6 +25,7 @@ #include "doscalls-lx.h" static pthread_mutex_t GMutexDosCalls; +static pthread_mutex_t GMutexDosBeep; typedef struct ExitListItem { @@ -3133,6 +3134,132 @@ APIRET DosDupHandle(HFILE hFile, PHFILE pHfile) return DosDupHandle_implementation(hFile, pHfile); } // DosDupHandle + +typedef struct DosBeepGeneratorInfo +{ + ULONG samples_remaining; + ULONG positive; + ULONG square_remaining; + ULONG square_length; + ULONG square_remainder; + ULONG accumulator; + ULONG next_freq; + ULONG next_duration; +} DosBeepGeneratorInfo; + +static DosBeepGeneratorInfo dos_beep_info; + +static int DosBeepGenerator(void *userdata, float *stream, int samples, int freq) +{ + DosBeepGeneratorInfo *info = &dos_beep_info; + assert(userdata == &dos_beep_info); + + while (samples > 0) { + if (!info->samples_remaining) { // current beep command is finished? + if (!info->next_freq) { + return 0; // out of things to play, remove ourselves from the mixer. + } + + // set up next beep command. + info->square_length = ((ULONG) freq) / info->next_freq; + info->square_remainder = ((ULONG) freq) % info->next_freq; + info->samples_remaining = ((ULONG) ((info->next_duration / 1000.0) * ((ULONG) freq))) * 1; + info->accumulator = 0; + info->next_freq = 0; + info->next_duration = 0; + } else if (!info->square_remaining) { // this part of the square wave is finished, flip it and start the next part. + info->positive = !info->positive; + info->square_remaining = info->square_length; + info->accumulator += info->square_remainder; + if (info->accumulator >= info->square_length) { + info->accumulator -= info->square_length; + info->square_remaining++; + } + } else { // still generating the current piece of the square wave, mix it into the stream. + // VMware emulates the PC Speaker on a sound card _really_ quietly. + const float val = info->positive ? 0.05f : -0.05f; + int total = info->square_remaining; + if (info->samples_remaining < total) total = info->samples_remaining; + if (samples < total) total = samples; + + FIXME("good candidate for SSE"); + for (int i = 0; i < total; i++) { + stream[i] += val; + } + + samples -= total; + info->samples_remaining -= total; + info->square_remaining -= total; + stream += total; + } + } + + return 1; // remain in the mixer to keep generating audio. +} // DosBeepGenerator + +static APIRET DosBeep_implementation(ULONG freq, ULONG duration) +{ + if ((freq < 0x25) || (freq > 0x7FFF)) { + return ERROR_INVALID_FREQUENCY; + } else if (duration == 0) { + return NO_ERROR; + } + + // Either OS/2 or Vmware seems to be doubling the frequency requested, I + // think, as this sounds closer to what DosBeep() generates there. + //FIXME("sanity check this"); + //freq *= 2; + + // assuming OS/2 ran the PC Speaker off the BIOS timer, firing 18.2 times + // a second (about 55 milliseconds), we'll round up the requested + // duration to match. + FIXME("sanity check this"); + const ULONG duration_diff = duration % 55; + const ULONG mix_duration = duration + (duration_diff ? (55 - duration_diff) : 0); + + // we chain beep commands together and return a little before they'll + // end, so the caller can queue another without a gap in playback if + // they're making multiple calls to DosBeep to make a cute little tune. + + // OS/2's implementation appears to hold a mutex while driving the PC + // speaker; if you DosBeep() from two threads at once, one will block + // until the other finishes playing. + grabLock(&GMutexDosBeep); + + FIXME("should be atomic"); + while (*((volatile ULONG *) &dos_beep_info.next_freq) != 0) { + usleep(1000); + } + + dos_beep_info.next_duration = mix_duration; + dos_beep_info.next_freq = freq; + + // wait about the time this was requested to play, but since there is + // variance on when the audio thread will run again, and we padded out + // the play time to 55 milliseconds, chances are good that the next + // DosBeep call will queue more sound to play seamlessly. + // If less than 50 milliseconds, don't sleep at all; if they call DosBeep + // again, it'll block until it can queue the command anyhow. + if (duration > 55) { + usleep((duration-55) * 1000); + } + + const int rc = GLoaderState.registerAudioGenerator(DosBeepGenerator, &dos_beep_info, 1); + if (!rc) { + memset(&dos_beep_info, '\0', sizeof (dos_beep_info)); // oh well. + } + + ungrabLock(&GMutexDosBeep); + + return NO_ERROR; +} // DosBeep_implementation + +APIRET DosBeep(ULONG freq, ULONG duration) +{ + TRACE_NATIVE("DosBeep(%u, %u)", freq, duration); + return DosBeep_implementation(freq, duration); +} // DosBeep + APIRET16 Dos16GetVersion(PUSHORT pver) { TRACE_NATIVE("Dos16GetVersion(%p)", pver); @@ -3486,6 +3613,12 @@ APIRET16 Dos16ChgFilePtr(USHORT handle, LONG distance, USHORT whence, PULONG new return (APIRET16) DosSetFilePtr_implementation(handle, distance, whence, newoffset); } // Dos16ChgFilePtr +APIRET16 Dos16Beep(USHORT freq, USHORT duration) +{ + TRACE_NATIVE("Dos16Beep(%u, %u)", freq, duration); + return DosBeep_implementation(freq, duration); +} // Dos16Beep + LX_NATIVE_CONSTRUCTOR(doscalls) { @@ -3496,10 +3629,17 @@ LX_NATIVE_CONSTRUCTOR(doscalls) abort(); } // if + if (pthread_mutex_init(&GMutexDosBeep, NULL) == -1) { + pthread_mutex_destroy(&GMutexDosCalls); + fprintf(stderr, "pthread_mutex_init failed!\n"); + abort(); + } // if + MaxHFiles = 20; // seems to be OS/2's default. HFiles = (HFileInfo *) malloc(sizeof (HFileInfo) * MaxHFiles); if (!HFiles) { pthread_mutex_destroy(&GMutexDosCalls); + pthread_mutex_destroy(&GMutexDosBeep); fprintf(stderr, "Out of memory!\n"); abort(); } // if @@ -3532,6 +3672,8 @@ LX_NATIVE_CONSTRUCTOR(doscalls) ginfo = (GINFOSEG *) GLoaderState.allocSegment(&ginfosel, 0); linfo = (LINFOSEG *) GLoaderState.allocSegment(&linfosel, 0); if ((!ginfo) || (!linfo)) { + pthread_mutex_destroy(&GMutexDosCalls); + pthread_mutex_destroy(&GMutexDosBeep); fprintf(stderr, "Failed to allocate info segments!\n"); abort(); } @@ -3589,6 +3731,7 @@ LX_NATIVE_DESTRUCTOR(doscalls) GLoaderState.freeSelector(ginfosel); GLoaderState.freeSelector(linfosel); pthread_mutex_destroy(&GMutexDosCalls); + pthread_mutex_destroy(&GMutexDosBeep); } // end of doscalls.c ... diff --git a/native/doscalls.h b/native/doscalls.h index f4c3c02..730a8d8 100644 --- a/native/doscalls.h +++ b/native/doscalls.h @@ -544,6 +544,7 @@ OS2EXPORT APIRET16 OS2API16 Dos16Read(USHORT hFile, PVOID pBuffer, USHORT cbRead OS2EXPORT APIRET16 OS2API16 Dos16ExecPgm(PCHAR pObjname, SHORT cbObjname, USHORT execFlag, PSZ pArg, PSZ pEnv, PRESULTCODES16 pRes, PSZ pName) OS2APIINFO(144); OS2EXPORT APIRET16 OS2API16 Dos16CWait(USHORT action, USHORT option, PRESULTCODES16 pres, PUSHORT ppid, USHORT pid) OS2APIINFO(2); OS2EXPORT APIRET16 OS2API16 Dos16ChgFilePtr(USHORT handle, LONG distance, USHORT whence, PULONG newoffset) OS2APIINFO(58); +OS2EXPORT APIRET16 OS2API16 Dos16Beep(USHORT freq, USHORT dur) OS2APIINFO(50); // !!! FIXME: these should probably get sorted alphabetically and/or grouped // !!! FIXME: into areas of functionality, but for now, I'm just listing them @@ -619,6 +620,7 @@ OS2EXPORT APIRET OS2API DosSetProcessCp(ULONG cp) OS2APIINFO(289); OS2EXPORT APIRET OS2API DosQueryFSAttach(PSZ pszDeviceName, ULONG ulOrdinal, ULONG ulFSAInfoLevel, PFSQBUFFER2 pfsqb, PULONG pcbBuffLength) OS2APIINFO(277); OS2EXPORT APIRET OS2API DosSetFileSize(HFILE hFile, ULONG cbSize) OS2APIINFO(272); OS2EXPORT APIRET OS2API DosSetCurrentDir(PSZ pszName) OS2APIINFO(255); +OS2EXPORT APIRET OS2API DosBeep(ULONG freq, ULONG dur) OS2APIINFO(286); #ifdef __cplusplus }