Implemented DosBeep in doscalls and a simple audio mixer in lib2ine.
authorRyan C. Gordon <icculus@icculus.org>
Tue, 10 Jul 2018 01:33:11 -0400
changeset 151 de653a557d8c
parent 150 71400eb2cadf
child 152 975c7b2f35a8
Implemented DosBeep in doscalls and a simple audio mixer in lib2ine.
CMakeLists.txt
lib2ine.c
lib2ine.h
lx_loader.c
native/doscalls-lx.h
native/doscalls.c
native/doscalls.h
--- a/CMakeLists.txt	Tue Jul 10 01:27:30 2018 -0400
+++ b/CMakeLists.txt	Tue Jul 10 01:33:11 2018 -0400
@@ -96,8 +96,10 @@
 
 # !!! 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()
 
--- a/lib2ine.c	Tue Jul 10 01:27:30 2018 -0400
+++ b/lib2ine.c	Tue Jul 10 01:33:11 2018 -0400
@@ -23,8 +23,10 @@
 #include <pthread.h>
 #include <sys/time.h>
 #include <sys/resource.h>
+#include <assert.h>
 
 #include "lib2ine.h"
+#include "SDL.h"
 
 extern char **environ;
 
@@ -640,6 +642,157 @@
     #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 @@
     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_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 ...
--- a/lib2ine.h	Tue Jul 10 01:27:30 2018 -0400
+++ b/lib2ine.h	Tue Jul 10 01:33:11 2018 -0400
@@ -273,6 +273,13 @@
     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 @@
     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);
--- a/lx_loader.c	Tue Jul 10 01:27:30 2018 -0400
+++ b/lx_loader.c	Tue Jul 10 01:33:11 2018 -0400
@@ -667,6 +667,8 @@
 
 static __attribute__((noreturn)) void lxTerminate(const uint32 exitcode)
 {
+    GLoaderState.lib2ine_shutdown();
+
     // free the actual .exe
     freeLxModule(GLoaderState.main_module);
 
--- a/native/doscalls-lx.h	Tue Jul 10 01:27:30 2018 -0400
+++ b/native/doscalls-lx.h	Tue Jul 10 01:33:11 2018 -0400
@@ -72,6 +72,12 @@
     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_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 @@
         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_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_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),
--- a/native/doscalls.c	Tue Jul 10 01:27:30 2018 -0400
+++ b/native/doscalls.c	Tue Jul 10 01:33:11 2018 -0400
@@ -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 @@
     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 @@
     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 @@
         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 @@
     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 @@
     GLoaderState.freeSelector(ginfosel);
     GLoaderState.freeSelector(linfosel);
     pthread_mutex_destroy(&GMutexDosCalls);
+    pthread_mutex_destroy(&GMutexDosBeep);
 }
 
 // end of doscalls.c ...
--- a/native/doscalls.h	Tue Jul 10 01:27:30 2018 -0400
+++ b/native/doscalls.h	Tue Jul 10 01:33:11 2018 -0400
@@ -544,6 +544,7 @@
 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 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
 }