diff options
-rw-r--r-- | Adlib.cpp | 145 | ||||
-rw-r--r-- | Makefile | 7 | ||||
-rw-r--r-- | Mpu401.cpp | 7 | ||||
-rw-r--r-- | README.md | 18 | ||||
-rw-r--r-- | midialsa.cpp | 6 | ||||
-rw-r--r-- | pcmalsa.cpp | 42 | ||||
-rw-r--r-- | pcmalsa.h | 8 |
7 files changed, 112 insertions, 121 deletions
@@ -51,9 +51,6 @@ #define LOG_ENABLE_FLOW 1 #define LOG_GROUP LOG_GROUP_DEV_SB16 #include <VBox/vmm/pdmdev.h> -#ifndef IN_RING3 -# include <VBox/vmm/pdmapi.h> -#endif #include <VBox/AssertGuest.h> #include <VBox/version.h> #include <iprt/assert.h> @@ -61,6 +58,10 @@ #include "opl3.h" +#ifndef IN_RING3 +#error "R3-only driver" +#endif + #if RT_OPSYS == RT_OPSYS_LINUX #include "pcmalsa.h" typedef PCMOutAlsa PCMOutBackend; @@ -77,7 +78,7 @@ typedef PCMOutWin PCMOutBackend; #define ADLIB_DEFAULT_OUT_DEVICE "default" #define ADLIB_DEFAULT_SAMPLE_RATE 22055 /* Hz */ -#define ADLIB_NUM_CHANNELS 2 /* as we are actually using OPL3 */ +#define ADLIB_NUM_CHANNELS 2 /* as we are actually supporting OPL3 */ enum { ADLIB_PORT_ADDR = 0, @@ -90,8 +91,11 @@ enum { /** The saved state version. */ #define ADLIB_SAVED_STATE_VERSION 1 +/** Maximum number of sound samples render in one batch by render thread. */ +#define ADLIB_RENDER_BLOCK_TIME 5 /* in millisec */ + /** The render thread will shutdown if this time passes since the last OPL register write. */ -#define ADLIB_RENDER_THREAD_TIMEOUT 5000 /* in millisec */ +#define ADLIB_RENDER_SUSPEND_TIMEOUT 5000 /* in millisec */ #define OPL2_NUM_IO_PORTS 2 #define OPL3_NUM_IO_PORTS 4 @@ -126,9 +130,8 @@ typedef struct { PCMOutBackend pcmOut; /** Thread that connects to PCM out, renders and pushes audio data. */ RTTHREAD hRenderThread; - /** Buffer for the rendering thread to use. */ + /** Buffer for the rendering thread to use, size defined by ADLIB_RENDER_BLOCK_TIME. */ R3PTRTYPE(uint8_t *) pbRenderBuf; - size_t uRenderBufSize; /** Flag to signal render thread to shut down. */ bool volatile fShutdown; /** Flag from render thread indicated it has shutdown (e.g. due to error or timeout). */ @@ -156,6 +159,18 @@ typedef ADLIBSTATE *PADLIBSTATE; #ifndef VBOX_DEVICE_STRUCT_TESTCASE +static inline uint64_t adlibCalculateFramesFromMilli(PADLIBSTATE pThis, uint64_t milli) +{ + uint64_t rate = pThis->uSampleRate; + return (rate * milli) / 1000; +} + +static inline size_t adlibCalculateBytesFromFrames(PADLIBSTATE pThis, uint64_t frames) +{ + NOREF(pThis); + return frames * sizeof(uint16_t) * ADLIB_NUM_CHANNELS; +} + static uint64_t adlibCalculateTimerExpire(PPDMDEVINS pDevIns, uint8_t value, uint64_t period) { uint64_t delay_usec = (0x100 - value) * period; @@ -168,6 +183,12 @@ static uint64_t adlibCalculateTimerExpire(PPDMDEVINS pDevIns, uint8_t value, uin } /** + * The render thread calls into the emulator to render audio frames, and then pushes them + * on the PCM output device. + * We rely on the PCM output device's blocking writes behavior to avoid running continously. + * A small block size (ADLIB_RENDER_BLOCK_TIME) is also used to give the main thread some + * opportunities to run. + * * @callback_method_impl{FNRTTHREAD} */ static DECLCALLBACK(int) adlibRenderThread(RTTHREAD ThreadSelf, void *pvUser) @@ -178,52 +199,37 @@ static DECLCALLBACK(int) adlibRenderThread(RTTHREAD ThreadSelf, void *pvUser) // Compute the max number of frames we can store on our temporary buffer. int16_t *buf = (int16_t*) pThis->pbRenderBuf; - ssize_t buf_size = pThis->uRenderBufSize; - ssize_t buf_samples = buf_size / sizeof(int16_t); - ssize_t buf_frames = buf_samples / ADLIB_NUM_CHANNELS; + uint64_t buf_frames = adlibCalculateFramesFromMilli(pThis, ADLIB_RENDER_BLOCK_TIME); - Log(("adlib: Starting render thread\n")); + Log(("adlib: Starting render thread with buf_frames=%lld\n", buf_frames)); int rc = pPcmOut->open(pThis->pszOutDevice, pThis->uSampleRate, ADLIB_NUM_CHANNELS); AssertLogRelRCReturn(rc, rc); while (!ASMAtomicReadBool(&pThis->fShutdown) - && ASMAtomicReadU64(&pThis->tmLastWrite) + ADLIB_RENDER_THREAD_TIMEOUT >= RTTimeSystemMilliTS()) { - ssize_t avail = pPcmOut->avail(); - - if (avail < 0) { - LogWarn(("adlib: render thread avail err=%d\n", avail)); - break; - } - if (avail == 0) { - rc = pPcmOut->wait(); - AssertLogRelRCBreak(rc); - avail = pPcmOut->avail(); - if (avail < 0) { - LogWarn(("adlib: render thread wait avail err=%d\n", avail)); - break; - } - } - - avail = RT_MIN(avail, buf_frames); - - Log3(("rendering %ld frames\n", avail)); + && ASMAtomicReadU64(&pThis->tmLastWrite) + ADLIB_RENDER_SUSPEND_TIMEOUT >= RTTimeSystemMilliTS()) { + Log3(("rendering %lld frames\n", buf_frames)); RTCritSectEnter(&pThis->critSect); - OPL3_GenerateStream(&pThis->opl, buf, avail); + OPL3_GenerateStream(&pThis->opl, buf, buf_frames); RTCritSectLeave(&pThis->critSect); - ssize_t written_frames = pPcmOut->write(buf, avail); + Log3(("writing %lld frames\n", buf_frames)); + + ssize_t written_frames = pPcmOut->write(buf, buf_frames); if (written_frames < 0) { - LogWarn(("adlib: render thread write err=%d\n", written_frames)); - break; + rc = written_frames; + AssertLogRelMsgFailedBreak(("adlib: render thread write err=%Rrc\n", written_frames)); } + + RTThreadYield(); } - rc = pPcmOut->close(); - AssertLogRelRC(rc); + int rcClose = pPcmOut->close(); + AssertLogRelRC(rcClose); + if (RT_SUCCESS(rc)) rc = rcClose; - Log(("adlib: Stopping render thread\n")); + Log(("adlib: Stopping render thread with rc=%Rrc\n", rc)); ASMAtomicWriteBool(&pThis->fStopped, true); @@ -231,7 +237,7 @@ static DECLCALLBACK(int) adlibRenderThread(RTTHREAD ThreadSelf, void *pvUser) } /** Waits for the render thread to finish and reaps it. */ -static int adlibReapRenderThread(PPDMDEVINS pDevIns, RTMSINTERVAL millies = 1000) +static int adlibReapRenderThread(PPDMDEVINS pDevIns, RTMSINTERVAL millies = 100) { PADLIBSTATE pThis = PDMDEVINS_2_DATA(pDevIns, PADLIBSTATE); @@ -253,6 +259,12 @@ static int adlibStopRenderThread(PPDMDEVINS pDevIns, bool wait = false) { PADLIBSTATE pThis = PDMDEVINS_2_DATA(pDevIns, PADLIBSTATE); + if (pThis->hRenderThread == NIL_RTTHREAD) { + // Already stopped & reaped + return VINF_SUCCESS; + } + + // Raise the flag for the thread ASMAtomicWriteBool(&pThis->fShutdown, true); if (wait) { @@ -263,36 +275,19 @@ static int adlibStopRenderThread(PPDMDEVINS pDevIns, bool wait = false) return VINF_SUCCESS; } -static int adlibResetRenderThread(PPDMDEVINS pDevIns) -{ - PADLIBSTATE pThis = PDMDEVINS_2_DATA(pDevIns, PADLIBSTATE); - - // Thread has to be shutdown - AssertReturn(ASMAtomicReadBool(&pThis->fShutdown), VERR_INVALID_STATE); - - int rc = adlibReapRenderThread(pDevIns); - if (RT_SUCCESS(rc)) { - pThis->fShutdown = false; - pThis->fStopped = false; - } else { - LogWarn(("adlib%d: can't reset render thread, it did not terminate (%Rrc)\n", rc)); - } - - return rc; -} - static void adlibWakeRenderThread(PPDMDEVINS pDevIns) { PADLIBSTATE pThis = PDMDEVINS_2_DATA(pDevIns, PADLIBSTATE); ASMAtomicWriteU64(&pThis->tmLastWrite, RTTimeSystemMilliTS()); - AssertReturnVoid(!ASMAtomicReadBool(&pThis->fShutdown)); - // Reap any existing render thread if it had stopped if (ASMAtomicReadBool(&pThis->fStopped)) { int rc = adlibReapRenderThread(pDevIns); - AssertRCReturnVoid(rc); + AssertLogRelRCReturnVoid(rc); + } else if (ASMAtomicReadBool(&pThis->fShutdown) + && pThis->hRenderThread != NIL_RTTHREAD) { + AssertLogRelMsgFailedReturnVoid(("can't wake render thread -- it's shutting down!\n")); } // If there is no existing render thread, start a new one @@ -305,10 +300,7 @@ static void adlibWakeRenderThread(PPDMDEVINS pDevIns) int rc = RTThreadCreateF(&pThis->hRenderThread, adlibRenderThread, pThis, 0, RTTHREADTYPE_IO, RTTHREADFLAGS_WAITABLE, "adlib%u_render", pDevIns->iInstance); - if (RT_FAILURE(rc)) { - LogWarn(("adlib%d: could not start render thread (%Rrc)\n", pDevIns->iInstance, rc)); - AssertRCReturnVoid(rc); - } + AssertLogRelRCReturnVoid(rc); } } @@ -334,6 +326,10 @@ static uint8_t adlibReadStatus(PPDMDEVINS pDevIns) if (pThis->timer2Enable && tmNow > pThis->timer2Expire) { status |= RT_BIT(7) | RT_BIT(5); } + if (!pThis->fOPL3) { + // OPL2 seems to have this as special signature. + status |= 0x6; + } Log2Func(("status=0x%x\n", status)); @@ -553,14 +549,6 @@ static DECLCALLBACK(void) adlibR3Suspend(PPDMDEVINS pDevIns) } /** - * @interface_method_impl{PDMDEVREG,pfnResume} - */ -static DECLCALLBACK(void) adlibR3Resume(PPDMDEVINS pDevIns) -{ - adlibResetRenderThread(pDevIns); -} - -/** * @interface_method_impl{PDMDEVREG,pfnPowerOff} */ static DECLCALLBACK(void) adlibR3PowerOff(PPDMDEVINS pDevIns) @@ -611,18 +599,17 @@ static DECLCALLBACK(int) adlibR3Construct(PPDMDEVINS pDevIns, int iInstance, PCF adlibR3Reset(pDevIns); /* Initialize now the buffer that will be used by the render thread. */ - // Give it as much space as it could possibly need, like half a second - pThis->uRenderBufSize = (pThis->uSampleRate * ADLIB_NUM_CHANNELS * sizeof(uint16_t)) / 2; - pThis->pbRenderBuf = (uint8_t *) RTMemAllocZ(pThis->uRenderBufSize); + size_t renderBlockSize = adlibCalculateBytesFromFrames(pThis, adlibCalculateFramesFromMilli(pThis, ADLIB_RENDER_BLOCK_TIME)); + pThis->pbRenderBuf = (uint8_t *) RTMemAlloc(renderBlockSize); AssertReturn(pThis->pbRenderBuf, VERR_NO_MEMORY); /* Prepare the render thread, but not create it yet. */ - pThis->fShutdown = false; + pThis->fShutdown = false; pThis->fStopped = false; pThis->hRenderThread = NIL_RTTHREAD; pThis->tmLastWrite = 0; - rc = RTCritSectInit(&pThis->critSect); - AssertRCReturn(rc, rc); + rc = RTCritSectInit(&pThis->critSect); + AssertRCReturn(rc, rc); /* * Register I/O ports. @@ -716,7 +703,7 @@ static const PDMDEVREG g_DeviceAdlib = /* .pfnPowerOn = */ NULL, /* .pfnReset = */ adlibR3Reset, /* .pfnSuspend = */ adlibR3Suspend, - /* .pfnResume = */ adlibR3Resume, + /* .pfnResume = */ NULL, /* .pfnAttach = */ NULL, /* .pfnDetach = */ NULL, /* .pfnQueryInterface = */ NULL, @@ -89,14 +89,17 @@ $(OUTDIR)/ExtPack.xml: ExtPack.xml $(OUTDIR)/ExtPack.signature: echo "todo" > $@ -$(OUTDIR)/ExtPack.manifest: $(OUTDIR) +$(OUTDIR)/ExtPack.manifest: $(OUTDIR) $(OUTOSDIR) cd $(OUTDIR) ;\ find -type f -printf '%P\n' | xargs ../build_manifest.sh > $(@F) pack: $(OUTDIR)/ExtPack.xml $(OUTDIR)/ExtPack.signature $(OUTDIR)/ExtPack.manifest tar --format=ustar --numeric-owner --owner=0 --group=0 --mode='a=rX,u+w' --sort=name -C $(OUTDIR) -f VMusic.vbox-extpack -v -z -c . +strip: + strip $(OUTOSDIR)/*.$(SO) + clean: rm -rf $(OUTDIR) $(OBJDIR) VMusic.vbox-extpack -.PHONY: all build clean pack +.PHONY: all build clean strip pack @@ -51,14 +51,15 @@ #define LOG_ENABLE_FLOW 1 #define LOG_GROUP LOG_GROUP_DEV_SB16 #include <VBox/vmm/pdmdev.h> -#ifndef IN_RING3 -# include <VBox/vmm/pdmapi.h> -#endif #include <VBox/AssertGuest.h> #include <VBox/version.h> #include <iprt/assert.h> #include <iprt/mem.h> +#ifndef IN_RING3 +#error "R3-only driver" +#endif + #if RT_OPSYS == RT_OPSYS_LINUX #include "midialsa.h" typedef MIDIOutAlsa MIDIOutBackend; @@ -49,7 +49,7 @@ VBoxManage setextradata "$vm" VBoxInternal/Devices/mpu401/0/Trusted 1 If the devices have been correctly enabled, you should see the following messages in the VBox.log file of a virtual machine after it has been powered on: -``` +```{ use_pygments=false } 00:00:00.799849 Installed Extension Packs: 00:00:00.799866 VMusic (Version: 0.2 r0; VRDE Module: ) ... @@ -77,7 +77,7 @@ or [Munt](https://sourceforge.net/projects/munt/). First, start the virtual machine. Second, start and configure your software synthesizer. If you run `aconnect -l` at this point, you will see a list of all your real/virtual MIDI devices in the system. Sample output from `aconnect -l`: -``` +```{ use_pygments=false } client 0: 'System' [type=kernel] 0 'Timer ' Connecting To: 142:0 @@ -94,7 +94,7 @@ This indicates that there is a `Munt MT-32` synthesizer at port 129:0 , and a `V The latter is the virtual MIDI device used by the MPU-401 emulation. So, to send the virtual machine's MIDI output to Munt, connect the two ports by running: -``` +```{ use_pygments=false } aconnect 128:0 129:0 ``` @@ -105,16 +105,16 @@ all virtual MIDI ports to it, in which case you may not need to connect anything # Building -You need the standard C++ building tools, _make_, _libasound_ and headers (e.g. `libasound2-dev` in Ubuntu). +You need the standard C++ building tools, make, libasound and headers (e.g. `libasound2-dev` in Ubuntu). First, ensure that, in the directory where the VMusic source resides, you add two extra directories: -* _VirtualBox.src_ containing the unpacked pristine VirtualBox source from [virtualbox.org](https://www.virtualbox.org/wiki/Downloads), e.g. the contents of [VirtualBox-6.1.32.tar.bz2](https://download.virtualbox.org/virtualbox/6.1.32/VirtualBox-6.1.32.tar.bz2). +* `VirtualBox.src` containing the unpacked pristine VirtualBox source from [virtualbox.org](https://www.virtualbox.org/wiki/Downloads), e.g. the contents of [VirtualBox-6.1.32.tar.bz2](https://download.virtualbox.org/virtualbox/6.1.32/VirtualBox-6.1.32.tar.bz2). -* _VirtualBox.linux.amd64_ containing at least the following VirtualBox Linux.amd64 libraries, -either from an official installation or your distribution's package: _VBoxRT.so_ and _VBoxVMM.so_. -E.g. copy _/usr/lib/virtualbox/VBoxRT.so_ into _VirtualBox.linux.amd64/VBoxRT.so_. +* `VirtualBox.linux.amd64` containing at least the following VirtualBox Linux.amd64 libraries, +either from an official installation or your distribution's package: `VBoxRT.so` and `VBoxVMM.so`. +E.g. copy `/usr/lib/virtualbox/VBoxRT.so` into `VirtualBox.linux.amd64/VBoxRT.so`. -After this, just type `make` followed by `make pack` and _VMusic.vbox-extpack_ should be generated. +After this, just type `make` followed by `make pack` and `VMusic.vbox-extpack` should be generated. diff --git a/midialsa.cpp b/midialsa.cpp index e8e653a..ef07d8a 100644 --- a/midialsa.cpp +++ b/midialsa.cpp @@ -36,7 +36,7 @@ MIDIOutAlsa::~MIDIOutAlsa() int MIDIOutAlsa::open(const char *dev) { - int err; + int err; if ((err = snd_rawmidi_open(NULL, &_out, "virtual", SND_RAWMIDI_NONBLOCK))) { LogWarn(("ALSA rawmidi open error: %s\n", snd_strerror(err))); @@ -46,7 +46,7 @@ int MIDIOutAlsa::open(const char *dev) // TODO: Connect somewhere NOREF(dev); - return VINF_SUCCESS; + return VINF_SUCCESS; } int MIDIOutAlsa::close() @@ -55,7 +55,7 @@ int MIDIOutAlsa::close() snd_rawmidi_close(_out); _out = NULL; } - return VINF_SUCCESS; + return VINF_SUCCESS; } ssize_t MIDIOutAlsa::write(uint8_t *data, size_t len) diff --git a/pcmalsa.cpp b/pcmalsa.cpp index eec9b94..5708509 100644 --- a/pcmalsa.cpp +++ b/pcmalsa.cpp @@ -36,13 +36,13 @@ PCMOutAlsa::~PCMOutAlsa() int PCMOutAlsa::open(const char *dev, unsigned int sampleRate, unsigned int channels) { - int err; + int err; err = snd_pcm_open(&_pcm, dev, SND_PCM_STREAM_PLAYBACK, 0); if (err < 0) { - LogWarn(("ALSA playback open error: %s\n", snd_strerror(err))); - return VERR_AUDIO_STREAM_COULD_NOT_CREATE; - } + LogWarn(("ALSA playback open error: %s\n", snd_strerror(err))); + return VERR_AUDIO_STREAM_COULD_NOT_CREATE; + } // TODO: Right now, setting a too large period size means we will not let the main // thread run long enough to actually change notes/voices. Need some actual synchronization. @@ -57,16 +57,16 @@ int PCMOutAlsa::open(const char *dev, unsigned int sampleRate, unsigned int chan return VERR_AUDIO_STREAM_COULD_NOT_CREATE; } - return VINF_SUCCESS; + return VINF_SUCCESS; } int PCMOutAlsa::close() { - if (_pcm) { + if (_pcm) { snd_pcm_drain(_pcm); - snd_pcm_close(_pcm); - } - return VINF_SUCCESS; + snd_pcm_close(_pcm); + } + return VINF_SUCCESS; } ssize_t PCMOutAlsa::avail() @@ -104,16 +104,16 @@ int PCMOutAlsa::wait() ssize_t PCMOutAlsa::write(int16_t *buf, size_t n) { - snd_pcm_sframes_t frames = snd_pcm_writei(_pcm, buf, n); - if (frames < 0) { - LogFlow(("ALSA trying to recover from error: %s\n", snd_strerror(frames))); - frames = snd_pcm_recover(_pcm, frames, 0); - } - if (frames < 0) { + snd_pcm_sframes_t frames = snd_pcm_writei(_pcm, buf, n); + if (frames < 0) { + LogFlow(("ALSA trying to recover from error: %s\n", snd_strerror(frames))); + frames = snd_pcm_recover(_pcm, frames, 0); + } + if (frames < 0) { LogWarn(("ALSA write error: %s\n", snd_strerror(frames))); - return VERR_AUDIO_STREAM_NOT_READY; - } - return frames; + return VERR_AUDIO_STREAM_NOT_READY; + } + return frames; } int PCMOutAlsa::setParams(unsigned int sampleRate, unsigned int channels, unsigned int bufferTime, unsigned int periodTime) @@ -175,19 +175,19 @@ int PCMOutAlsa::setParams(unsigned int sampleRate, unsigned int channels, unsign } err = snd_pcm_hw_params_get_buffer_size(hwparams, &_bufferSize); if (err < 0) { - printf("Unable to get buffer size for playback: %s\n", snd_strerror(err)); + LogWarnFunc(("Unable to get buffer size for playback: %s\n", snd_strerror(err))); return err; } /* set the period time */ err = snd_pcm_hw_params_set_period_time_near(_pcm, hwparams, &periodTime, NULL); if (err < 0) { - printf("Unable to set period time %u for playback: %s\n", periodTime, snd_strerror(err)); + LogWarnFunc(("Unable to set period time %u for playback: %s\n", periodTime, snd_strerror(err))); return err; } err = snd_pcm_hw_params_get_period_size(hwparams, &_periodSize, NULL); if (err < 0) { - printf("Unable to get period size for playback: %s\n", snd_strerror(err)); + LogWarnFunc(("Unable to get period size for playback: %s\n", snd_strerror(err))); return err; } @@ -27,11 +27,11 @@ typedef struct _snd_pcm snd_pcm_t; class PCMOutAlsa { public: - PCMOutAlsa(); - ~PCMOutAlsa(); + PCMOutAlsa(); + ~PCMOutAlsa(); int open(const char *dev, unsigned int sampleRate, unsigned int channels); - int close(); + int close(); ssize_t avail(); int wait(); @@ -42,7 +42,7 @@ private: int setParams(unsigned int sampleRate, unsigned int channels, unsigned int bufferTime, unsigned int periodTime); private: - snd_pcm_t * _pcm; + snd_pcm_t * _pcm; size_t _bufferSize; size_t _periodSize; }; |