From d1a6f373cb3565d82f675ce763cae1c1e2cf6662 Mon Sep 17 00:00:00 2001 From: krcroft Date: Mon, 11 Nov 2019 13:36:31 -0800 Subject: [PATCH] Refactor CD-DA flow by removing intermediate buffers and loops Thanks to @ripsaw8080 for insight into CD-DA channel mapping, @hail-to-the-ryzen for testing and flagging a position-tracking bug, and @dreamer_ for guidance and code review. The CD-DA volume and channel mapping loops were moved to generic mixer calls and no longer require a pre-processing loop: - Application-controlled CD-DA volume adjustment is now applied using an existing mixer volume scalar that was previously unused by the CD-DA code. - Mapping of CD-DA left and right channels is now applied at the tail end of the mixer's sample ingest sequence. The following have been removed: - The CD-DA callback chunk-wise circular buffer - The decode buffers in the Opus and MP3 decoders - The decode buffer and conversion buffers in SDL_Sound These removals and API changes allow the image player's buffer to be passed-through ultimately to the audio codec, skipping multiple intermediate buffers. --- include/mixer.h | 42 ++-- src/dos/cdrom.h | 51 ++--- src/dos/cdrom_image.cpp | 353 +++++++++++++--------------------- src/dos/cdrom_ioctl_win32.cpp | 21 +- src/hardware/mixer.cpp | 179 ++++++++++++----- 5 files changed, 310 insertions(+), 336 deletions(-) diff --git a/include/mixer.h b/include/mixer.h index 3c817537..d352a804 100644 --- a/include/mixer.h +++ b/include/mixer.h @@ -46,8 +46,11 @@ extern Bit8u MixTemp[MIXER_BUFSIZE]; class MixerChannel { public: + MixerChannel(MIXER_Handler _handler, Bitu _freq, const char * _name); void SetVolume(float _left,float _right); - void SetScale( float f ); + void SetScale(float f); + void SetScale(float _left, float _right); + void MapChannels(Bit8u _left, Bit8u _right); void UpdateVolume(void); void SetFreq(Bitu _freq); void Mix(Bitu _needed); @@ -77,24 +80,25 @@ public: void FillUp(void); void Enable(bool _yesno); - MIXER_Handler handler; - float volmain[2]; - float scale; - Bit32s volmul[2]; - - //This gets added the frequency counter each mixer step - Bitu freq_add; - //When this flows over a new sample needs to be read from the device - Bitu freq_counter; - //Timing on how many samples have been done and were needed by th emixer - Bitu done, needed; - //Previous and next samples - Bits prevSample[2]; - Bits nextSample[2]; - const char * name; - bool interpolate; - bool enabled; - MixerChannel * next; + + float volmain[2]; + MixerChannel* next; + const char* name; + Bitu done; //Timing on how many samples have been done by the mixer + bool enabled; + +private: + MixerChannel(); + MIXER_Handler handler; + Bitu freq_add; //This gets added the frequency counter each mixer step + Bitu freq_counter; //When this flows over a new sample needs to be read from the device + Bitu needed; //Timing on how many samples were needed by the mixer + Bits prev_sample[2]; //Previous and next samples + Bits next_sample[2]; + Bit32s volmul[2]; + float scale[2]; + Bit8u channel_map[2]; //Output channel mapping + bool interpolate; }; MixerChannel * MIXER_AddChannel(MIXER_Handler handler,Bitu freq,const char * name); diff --git a/src/dos/cdrom.h b/src/dos/cdrom.h index a5931003..9f16c5e4 100644 --- a/src/dos/cdrom.h +++ b/src/dos/cdrom.h @@ -36,13 +36,11 @@ #include "mixer.h" #include "../libs/decoders/SDL_sound.h" -#define RAW_SECTOR_SIZE 2352 -#define COOKED_SECTOR_SIZE 2048 -#define AUDIO_DECODE_BUFFER_SIZE 16512 -// 16512 is 16384 + 128, which enough for four 4KB decode audio chunks plus 128 bytes extra, -// which accomodate the leftovers from typically callbacks, which also helps minimize -// most of the time. This size is also an even multiple of 8-bytes, allowing the compiler to -// use "large-D" SIMD instructions on it. +// CDROM data and audio format constants +#define RAW_SECTOR_SIZE 2352 +#define COOKED_SECTOR_SIZE 2048 +#define BYTES_PER_TRACK_FRAME 4 +#define REDBOOK_FRAMES_PER_SECOND 75 enum { CDROM_USE_SDL, CDROM_USE_ASPI, CDROM_USE_IOCTL_DIO, CDROM_USE_IOCTL_DX, CDROM_USE_IOCTL_MCI }; @@ -53,17 +51,16 @@ typedef struct SMSF { } TMSF; typedef struct SCtrl { - Bit8u out[4]; // output channel - Bit8u vol[4]; // channel volume + Bit8u out[4]; // output channel mapping + Bit8u vol[4]; // channel volume (0 to 255) } TCtrl; // Conversion function from frames to Minutes/Second/Frames // template inline void frames_to_msf(int frames, T *m, T *s, T *f) { - const int cd_fps = 75; - *f = frames % cd_fps; - frames /= cd_fps; + *f = frames % REDBOOK_FRAMES_PER_SECOND; + frames /= REDBOOK_FRAMES_PER_SECOND; *s = frames % 60; frames /= 60; *m = frames; @@ -72,8 +69,7 @@ inline void frames_to_msf(int frames, T *m, T *s, T *f) { // Conversion function from Minutes/Second/Frames to frames // inline int msf_to_frames(int m, int s, int f) { - const int cd_fps = 75; - return m * 60 * cd_fps + s * cd_fps + f; + return m * 60 * REDBOOK_FRAMES_PER_SECOND + s * REDBOOK_FRAMES_PER_SECOND + f; } extern int CDROM_GetMountType(char* path, int force); @@ -161,7 +157,7 @@ private: public: virtual bool read(Bit8u *buffer, int seek, int count) = 0; virtual bool seek(Bit32u offset) = 0; - virtual Bit16u decode(Bit8u *buffer) = 0; + virtual Bit32u decode(Bit16s *buffer, Bit32u desired_track_frames) = 0; virtual Bit16u getEndian() = 0; virtual Bit32u getRate() = 0; virtual Bit8u getChannels() = 0; @@ -175,7 +171,7 @@ private: BinaryFile (const char *filename, bool &error); bool read(Bit8u *buffer, int seek, int count); bool seek(Bit32u offset); - Bit16u decode(Bit8u *buffer); + Bit32u decode(Bit16s *buffer, Bit32u desired_track_frames); Bit16u getEndian(); Bit32u getRate() { return 44100; } Bit8u getChannels() { return 2; } @@ -191,7 +187,7 @@ private: AudioFile (const char *filename, bool &error); bool read(Bit8u *buffer, int seek, int count) { return false; } bool seek(Bit32u offset); - Bit16u decode(Bit8u *buffer); + Bit32u decode(Bit16s *buffer, Bit32u desired_track_frames); Bit16u getEndian(); Bit32u getRate(); Bit8u getChannels(); @@ -237,26 +233,21 @@ static CDROM_Interface_Image* images[26]; private: // player -static void CDAudioCallBack(Bitu len); +static void CDAudioCallBack(Bitu desired_frames); int GetTrack(int sector); static struct imagePlayer { - Bit8u buffer[AUDIO_DECODE_BUFFER_SIZE]; - TCtrl ctrlData; + Bit16s buffer[MIXER_BUFSIZE * 2]; // 2 channels (max) TrackFile *trackFile; MixerChannel *channel; CDROM_Interface_Image *cd; - void (MixerChannel::*addSamples) (Bitu, const Bit16s*); - Bit32u startFrame; - Bit32u currFrame; - Bit32u numFrames; - Bit32u playbackTotal; - Bit32s playbackRemaining; - Bit16u bufferPos; - Bit16u bufferConsumed; + void (MixerChannel::*addFrames) (Bitu, const Bit16s*); + Bit32u startRedbookFrame; + Bit32u totalRedbookFrames; + Bit32u playedTrackFrames; + Bit32u totalTrackFrames; bool isPlaying; bool isPaused; - bool ctrlUsed; } player; void ClearTracks(); @@ -407,7 +398,7 @@ private: SDL_mutex *mutex; Bit8u buffer[8192]; int bufLen; - int currFrame; + int currFrame; int targetFrame; bool isPlaying; bool isPaused; diff --git a/src/dos/cdrom_image.cpp b/src/dos/cdrom_image.cpp index 25725d13..48c90c6a 100644 --- a/src/dos/cdrom_image.cpp +++ b/src/dos/cdrom_image.cpp @@ -27,14 +27,9 @@ #include #include #include -#include //GCC 2.95 #include #include #include -#include "cdrom.h" -#include "drives.h" -#include "support.h" -#include "setup.h" #if !defined(WIN32) #include @@ -42,6 +37,11 @@ #include #endif +#include "cdrom.h" +#include "drives.h" +#include "support.h" +#include "setup.h" + using namespace std; #define MAX_LINE_LENGTH 512 @@ -96,12 +96,8 @@ int CDROM_Interface_Image::BinaryFile::getLength() Bit16u CDROM_Interface_Image::BinaryFile::getEndian() { - // Image files are read into native-endian byte-order -#if defined(WORDS_BIGENDIAN) - return AUDIO_S16MSB; -#else + // Image files are always little endian return AUDIO_S16LSB; -#endif } @@ -111,10 +107,10 @@ bool CDROM_Interface_Image::BinaryFile::seek(Bit32u offset) return !file->fail(); } -Bit16u CDROM_Interface_Image::BinaryFile::decode(Bit8u *buffer) +Bit32u CDROM_Interface_Image::BinaryFile::decode(Bit16s *buffer, Bit32u desired_track_frames) { - file->read((char*)buffer, chunkSize); - return (Bit16u) file->gcount(); + file->read((char*)buffer, desired_track_frames * BYTES_PER_TRACK_FRAME); + return (Bit32u) file->gcount() / BYTES_PER_TRACK_FRAME; } CDROM_Interface_Image::AudioFile::AudioFile(const char *filename, bool &error) @@ -122,7 +118,7 @@ CDROM_Interface_Image::AudioFile::AudioFile(const char *filename, bool &error) { // Use the audio file's actual sample rate and number of channels as opposed to overriding Sound_AudioInfo desired = {AUDIO_S16, 0, 0}; - sample = Sound_NewSampleFromFile(filename, &desired, chunkSize); + sample = Sound_NewSampleFromFile(filename, &desired); if (sample) { error = false; std::string filename_only(filename); @@ -166,11 +162,9 @@ bool CDROM_Interface_Image::AudioFile::seek(Bit32u offset) return result; } -Bit16u CDROM_Interface_Image::AudioFile::decode(Bit8u *buffer) +Bit32u CDROM_Interface_Image::AudioFile::decode(Bit16s *buffer, Bit32u desired_track_frames) { - const Bit16u bytes = Sound_Decode(sample); - memcpy(buffer, sample->buffer, bytes); - return bytes; + return Sound_Decode_Direct(sample, (void*)buffer, desired_track_frames); } Bit16u CDROM_Interface_Image::AudioFile::getEndian() @@ -213,27 +207,21 @@ int CDROM_Interface_Image::AudioFile::getLength() int CDROM_Interface_Image::refCount = 0; CDROM_Interface_Image* CDROM_Interface_Image::images[26] = {}; CDROM_Interface_Image::imagePlayer CDROM_Interface_Image::player = { - {0}, // buffer[] - {0}, // ctrlData struct - nullptr, // TrackFile* - nullptr, // MixerChannel* - nullptr, // CDROM_Interface_Image* - nullptr, // addSamples - 0, // startFrame - 0, // currFrame - 0, // numFrames - 0, // playbackTotal - 0, // playbackRemaining - 0, // bufferPos - 0, // bufferConsumed - false, // isPlaying - false, // isPaused - false // ctrlUsed + {0}, // buffer[] + nullptr, // TrackFile* + nullptr, // MixerChannel* + nullptr, // CDROM_Interface_Image* + nullptr, // addFrames + 0, // startRedbookFrame + 0, // totalRedbookFrames + 0, // playedTrackFrames + 0, // totalTrackFrames + false, // isPlaying + false // isPaused }; - - + CDROM_Interface_Image::CDROM_Interface_Image(Bit8u subUnit) - :subUnit(subUnit) + :subUnit(subUnit) { images[subUnit] = this; if (refCount == 0) { @@ -242,8 +230,7 @@ CDROM_Interface_Image::CDROM_Interface_Image(Bit8u subUnit) player.channel = MIXER_AddChannel(&CDAudioCallBack, 0, "CDAUDIO"); player.channel->Enable(false); #ifdef DEBUG - LOG_MSG("CDROM: Initialized with %d-byte circular buffer", - AUDIO_DECODE_BUFFER_SIZE); + LOG_MSG("%s CDROM: Initialized the CDAUDIO mixer channel", get_time()); #endif } } @@ -262,7 +249,7 @@ CDROM_Interface_Image::~CDROM_Interface_Image() MIXER_DelChannel(player.channel); player.channel = nullptr; #ifdef DEBUG - LOG_MSG("CDROM: Audio channel freed"); + LOG_MSG("%s CDROM: Audio channel freed", get_time()); #endif } } @@ -343,38 +330,50 @@ bool CDROM_Interface_Image::GetAudioTrackInfo(int track, TMSF& start, unsigned c bool CDROM_Interface_Image::GetAudioSub(unsigned char& attr, unsigned char& track, unsigned char& index, TMSF& relPos, TMSF& absPos) { - int cur_track = GetTrack(player.currFrame); - if (cur_track < 1) { - return false; - } - track = (unsigned char)cur_track; - attr = tracks[track - 1].attr; - index = 1; - frames_to_msf(player.currFrame + 150, &absPos.min, &absPos.sec, &absPos.fr); - frames_to_msf(player.currFrame - tracks[track - 1].start, &relPos.min, &relPos.sec, &relPos.fr); + bool rcode = false; + + // Convert our running tally of track-frames played to Redbook-frames played. + // We round up because if our track-frame tally lands in the middle of a (fractional) + // Redbook frame, then that Redbook frame would have to be considered played to produce + // even the smallest amount of track-frames played. This also accurately represents + // the very end of a sequence where the last Redbook frame might only contain a couple + // PCM samples - but the entire last 2352-byte Redbook frame is needed to cover those samples. + const Bit32u playedRedbookFrames = static_cast(ceil( + static_cast(REDBOOK_FRAMES_PER_SECOND * player.playedTrackFrames) / player.trackFile->getRate() )); + + // Add that to the track's starting Redbook frame to determine our absolute current Redbook frame + const Bit32u currentRedbookFrame = player.startRedbookFrame + playedRedbookFrames; + + const char cur_track = GetTrack(currentRedbookFrame); + if (cur_track >= 1) { + track = static_cast(cur_track); + attr = tracks[track - 1].attr; + index = 1; + frames_to_msf(currentRedbookFrame + 150, &absPos.min, &absPos.sec, &absPos.fr); + frames_to_msf(currentRedbookFrame - tracks[track - 1].start, &relPos.min, &relPos.sec, &relPos.fr); + rcode = true; #ifdef DEBUG - LOG_MSG("%s CDROM: GetAudioSub attr=%u, track=%u, index=%u", - get_time(), - attr, - track, - index); - - LOG_MSG("%s CDROM: GetAudioSub absoute offset (%d), MSF=%d:%d:%d", - get_time(), - player.currFrame + 150, - absPos.min, - absPos.sec, - absPos.fr); - - LOG_MSG("%s CDROM: GetAudioSub relative offset (%d), MSF=%d:%d:%d", - get_time(), - player.currFrame - tracks[track - 1].start + 150, - relPos.min, - relPos.sec, - relPos.fr); + LOG_MSG("%s CDROM: GetAudioSub attr=%u, track=%u, index=%u", + get_time(), + attr, + track, + index); + LOG_MSG("%s CDROM: GetAudioSub absoute offset (%d), MSF=%d:%d:%d", + get_time(), + currentRedbookFrame + 150, + absPos.min, + absPos.sec, + absPos.fr); + LOG_MSG("%s CDROM: GetAudioSub relative offset (%d), MSF=%d:%d:%d", + get_time(), + currentRedbookFrame - tracks[track - 1].start + 150, + relPos.min, + relPos.sec, + relPos.fr); #endif - return true; + } + return rcode; } bool CDROM_Interface_Image::GetAudioStatus(bool& playing, bool& pause) @@ -431,7 +430,7 @@ bool CDROM_Interface_Image::PlayAudioSector(unsigned long start, unsigned long l // We can't play audio from a data track (as it would result in garbage/static) else if (track >= 0 && tracks[track].attr == 0x40) - LOG(LOG_MISC,LOG_WARN)("Game tries to play the data track. Not doing this"); + LOG(LOG_MISC,LOG_WARN)("Game tried to play the data track. Not doing this"); // Checks passed, setup the audio stream else { @@ -443,55 +442,46 @@ bool CDROM_Interface_Image::PlayAudioSector(unsigned long start, unsigned long l // only initialize the player elements if our track is playable if (is_playable) { - const Bit8u channels = trackFile->getChannels(); - const Bit32u rate = trackFile->getRate(); + const Bit8u track_channels = trackFile->getChannels(); + const Bit32u track_rate = trackFile->getRate(); player.cd = this; player.trackFile = trackFile; - player.startFrame = start; - player.currFrame = start; - player.numFrames = len; - player.bufferPos = 0; - player.bufferConsumed = 0; + player.startRedbookFrame = start; + player.totalRedbookFrames = len; player.isPlaying = true; player.isPaused = false; - -#if defined(WORDS_BIGENDIAN) - if (trackFile->getEndian() != AUDIO_S16SYS) -#else - if (trackFile->getEndian() == AUDIO_S16SYS) -#endif - player.addSamples = channels == 2 ? &MixerChannel::AddSamples_s16 \ - : &MixerChannel::AddSamples_m16; - else - player.addSamples = channels == 2 ? &MixerChannel::AddSamples_s16_nonnative \ - : &MixerChannel::AddSamples_m16_nonnative; - - const float bytesPerMs = (float) (rate * channels * 2 / 1000.0); - player.playbackTotal = lround(len * tracks[track].sectorSize * bytesPerMs / 176.4); - player.playbackRemaining = player.playbackTotal; + if (trackFile->getEndian() == AUDIO_S16SYS) { + player.addFrames = track_channels == 2 ? &MixerChannel::AddSamples_s16 \ + : &MixerChannel::AddSamples_m16; + } else { + player.addFrames = track_channels == 2 ? &MixerChannel::AddSamples_s16_nonnative \ + : &MixerChannel::AddSamples_m16_nonnative; + } + // Convert Redbook frames to Track frames, rounding up to whole integer frames. + // Round up to whole track frames because the content originated from whole redbook frames, + // which will require the last fractional frames to be represented by a whole PCM frame. + player.totalTrackFrames = static_cast(ceil( + static_cast(track_rate * player.totalRedbookFrames) / REDBOOK_FRAMES_PER_SECOND)); + player.playedTrackFrames = 0; #ifdef DEBUG - LOG_MSG("%s CDROM: Playing track %d at %.1f KHz " - "%d-channel at start sector %lu (%.1f minute-mark), seek %u " - "(skip=%d,dstart=%d,secsize=%d), for %lu sectors (%.1f seconds)", + LOG_MSG("%s CDROM: Playing track %d (%d Hz " + "%d-channel) at starting sector %lu (%.1f minute-mark) " + "for %u Redbook frames (%.1f seconds)", get_time(), track, - rate/1000.0, - channels, + track_rate, + track_channels, start, - offset * (1/10584000.0), - offset, - tracks[track].skip, - tracks[track].start, - tracks[track].sectorSize, - len, - player.playbackRemaining / (1000 * bytesPerMs)); + static_cast(start) / (REDBOOK_FRAMES_PER_SECOND * 60), + player.totalRedbookFrames, + static_cast(player.totalRedbookFrames) / REDBOOK_FRAMES_PER_SECOND); #endif // start the channel! - player.channel->SetFreq(rate); + player.channel->SetFreq(track_rate); player.channel->Enable(true); } } @@ -533,8 +523,15 @@ bool CDROM_Interface_Image::StopAudio(void) void CDROM_Interface_Image::ChannelControl(TCtrl ctrl) { - player.ctrlUsed = (ctrl.out[0]!=0 || ctrl.out[1]!=1 || ctrl.vol[0]<0xfe || ctrl.vol[1]<0xfe); - player.ctrlData = ctrl; + if (player.channel == NULL) return; + + // Adjust the volume of our mixer channel as defined by the application + player.channel->SetScale(static_cast(ctrl.vol[0]/255.0), // left vol + static_cast(ctrl.vol[1]/255.0)); // right vol + + // Map the audio channels in our mixer channel as defined by the application + player.channel->MapChannels(ctrl.out[0], // left map + ctrl.out[1]); // right map } bool CDROM_Interface_Image::ReadSectors(PhysPt buffer, bool raw, unsigned long sector, unsigned long num) @@ -590,137 +587,47 @@ bool CDROM_Interface_Image::ReadSector(Bit8u *buffer, bool raw, unsigned long se if (tracks[track].sectorSize == RAW_SECTOR_SIZE && !tracks[track].mode2 && !raw) seek += 16; if (tracks[track].mode2 && !raw) seek += 24; +#if 0 // Excessively verbose.. only enable if needed #ifdef DEBUG - LOG_MSG("CDROM: ReadSector track=%d, desired raw=%s, sector=%ld, length=%d", + LOG_MSG("%s CDROM: ReadSector track=%d, desired raw=%s, sector=%ld, length=%d", + get_time(), track, raw ? "true":"false", sector, length); #endif +#endif + return tracks[track].file->read(buffer, seek, length); } -void CDROM_Interface_Image::CDAudioCallBack(Bitu len) + +void CDROM_Interface_Image::CDAudioCallBack(Bitu desired_track_frames) { - // Our member object "playbackRemaining" holds the - // exact number of stream-bytes we need to play before meeting the - // DOS program's desired playback duration in sectors. We simply - // decrement this counter each callback until we're done. - if (len == 0 || !player.isPlaying || player.isPaused) { - return; - } + if (desired_track_frames > 0) { - // determine bytes per request (16-bit samples) - const Bit8u channels = player.trackFile->getChannels(); - const Bit8u bytes_per_request = channels * 2; - int total_requested = len * bytes_per_request; + const Bit32u decoded_track_frames = player.trackFile->decode(player.buffer, desired_track_frames); - while (total_requested > 0) { - int requested = total_requested; + // uses either the stereo or mono and native or nonnative AddSamples call assigned during construction + (player.channel->*player.addFrames)(decoded_track_frames, player.buffer); - // Every now and then the callback wants a big number of bytes, - // which can exceed our circular buffer. In these cases we need - // read through as many iteration of our circular buffer as needed. - if (total_requested > AUDIO_DECODE_BUFFER_SIZE) { - requested = AUDIO_DECODE_BUFFER_SIZE; - total_requested -= AUDIO_DECODE_BUFFER_SIZE; - } - else { - total_requested = 0; + // Stop the audio if our decode stream has run dry or when we've played at least the + // total number of frames. We consider "played" to mean the running tally so far plus + // the current requested frames, which takes into account that the number of currently + // decoded frames might be less than desired if we're at the end of the track. + // (The mixer will request frames forever until we stop it, so at some point we /will/ + // decode fewer than requested; in this "tail scenario", we push those remaining decoded + // frames into the mixer and then stop the audio.) + if (decoded_track_frames == 0 + || player.playedTrackFrames + desired_track_frames >= player.totalTrackFrames) { + player.cd->StopAudio(); } - // Three scenarios in order of probabilty: - // - // 1. Consume: If our decoded circular buffer is sufficiently filled to - // satify the Mixer's requested size, then feed the Mixer with - // the requested number of bytes. - // - // 2. Wrap: If we've decoded and consumed to edge of our buffer, then - // we need to wrap any remaining decoded-but-not-consumed - // samples back around to the front of the buffer. - // - // 3. Fill: When our circular buffer is too depleted to satisfy the - // Mixer's requested size, then ask the decoder for more data; - // to either enough to completely fill our buffer or whatever - // is left to satisfy the remaining samples in this requested - // playback sequence (the Mixer is unaware of how much audio - // xwe need to play; instead we decide when to cutoff the mixer). - // - while (true) { - - // 1. Consume - if (player.bufferPos - player.bufferConsumed >= requested) { - if (player.ctrlUsed) { - for (Bit8u i=0; i < channels; i++) { - Bit16s sample; - Bit16s* samples = (Bit16s*)&player.buffer[player.bufferConsumed]; - for (int pos = 0; pos < requested / bytes_per_request; pos++) { -#if defined(WORDS_BIGENDIAN) - sample = (Bit16s)host_readw((HostPt) & samples[pos * 2 + player.ctrlData.out[i]]); -#else - sample = samples[pos * 2 + player.ctrlData.out[i]]; -#endif - samples[pos * 2 + i] = (Bit16s)(sample * player.ctrlData.vol[i] / 255.0); - } - } - } - // uses either the stereo or mono and native or nonnative AddSamples - // call assigned during construction - (player.channel->*player.addSamples)(requested / bytes_per_request, - (Bit16s*)(player.buffer + player.bufferConsumed) ); - player.bufferConsumed += requested; - player.playbackRemaining -= requested; - - // Games can query the current Red Book MSF frame-position, so we keep that up-to-date here. - // We scale the final number of frames by the percent complete, which - // avoids having to keep track of the equivlent number of Red Book frames - // read (which would involve coverting the compressed streams data-rate into - // CDROM Red Book rate, which is more work than simply scaling). - // - const double playbackPercentSoFar = (player.playbackTotal - player.playbackRemaining) / player.playbackTotal; - player.currFrame = player.startFrame + (Bit32u) ceil(player.numFrames * playbackPercentSoFar); - break; - } - - // 2. Wrap - else { - memcpy(player.buffer, - player.buffer + player.bufferConsumed, - player.bufferPos - player.bufferConsumed); - player.bufferPos -= player.bufferConsumed; - player.bufferConsumed = 0; - } - - // 3. Fill - const Bit16u chunkSize = player.trackFile->chunkSize; - while(AUDIO_DECODE_BUFFER_SIZE - player.bufferPos >= chunkSize && - (player.bufferPos - player.bufferConsumed < player.playbackRemaining || - player.bufferPos - player.bufferConsumed < requested) ) { - - const Bit16u decoded = player.trackFile->decode(player.buffer + player.bufferPos); - player.bufferPos += decoded; - - // if we decoded less than expected, which could be due to EOF or if the CUE file specified - // an exact "INDEX 01 MIN:SEC:FRAMES" value but the compressed track is ever-so-slightly less than - // that specified, then simply pad with zeros. - const Bit16s underDecode = chunkSize - decoded; - if (underDecode > 0) { - -#ifdef DEBUG - LOG_MSG("%s CDROM: Underdecoded by %d. Feeding mixer with zeros.", - get_time(), - underDecode); -#endif - - memset(player.buffer + player.bufferPos, 0, underDecode); - player.bufferPos += underDecode; - } - } // end of fill-while - } // end of decode and fill loop - } // end while total_requested - - if (player.playbackRemaining <= 0) { - player.cd->StopAudio(); + // Increment our tally of played frames by those we just decoded (and fed to the mixer). + // Even if we've hit the end of the track and we've stopped the audio, we still want to + // increment our tally so subsequent calls to GetAudioSub() return the full number of + // frames played. + player.playedTrackFrames += decoded_track_frames; } } diff --git a/src/dos/cdrom_ioctl_win32.cpp b/src/dos/cdrom_ioctl_win32.cpp index 83147798..ca1113e1 100644 --- a/src/dos/cdrom_ioctl_win32.cpp +++ b/src/dos/cdrom_ioctl_win32.cpp @@ -470,8 +470,15 @@ bool CDROM_Interface_Ioctl::StopAudio(void) { void CDROM_Interface_Ioctl::ChannelControl(TCtrl ctrl) { - player.ctrlUsed = (ctrl.out[0]!=0 || ctrl.out[1]!=1 || ctrl.vol[0]<0xfe || ctrl.vol[1]<0xfe); - player.ctrlData = ctrl; + if (player.channel == NULL) return; + + // Adjust the volme of our mixer channel as defined by the application + player.channel->SetScale(static_cast(ctrl.vol[0]/255.0), // left vol + static_cast(ctrl.vol[1]/255.0)); // right vol + + // Map the audio channels in our mixer channel as defined by the application + player.channel->MapChannels(ctrl.out[0], // left map + ctrl.out[1]); // right map } bool CDROM_Interface_Ioctl::LoadUnloadMedia(bool unload) { @@ -565,16 +572,6 @@ void CDROM_Interface_Ioctl::dx_CDAudioCallBack(Bitu len) { } } SDL_mutexV(player.mutex); - if (player.ctrlUsed) { - Bit16s sample0,sample1; - Bit16s * samples=(Bit16s *)&player.buffer; - for (Bitu pos=0;posAddSamples_s16(len/4,(Bit16s *)player.buffer); memmove(player.buffer, &player.buffer[len], player.bufLen - len); player.bufLen -= len; diff --git a/src/hardware/mixer.cpp b/src/hardware/mixer.cpp index 41f291f1..19c64fa7 100644 --- a/src/hardware/mixer.cpp +++ b/src/hardware/mixer.cpp @@ -22,15 +22,20 @@ That should call the mixer start from there or something. */ +// #define DEBUG 1 + #include #include #include +#include #if defined (WIN32) //Midi listing #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif +// prevent the Windows header from clobbering std::min and max +#define NOMINMAX #include #include #endif @@ -90,16 +95,35 @@ static struct { Bit8u MixTemp[MIXER_BUFSIZE]; -MixerChannel * MIXER_AddChannel(MIXER_Handler handler,Bitu freq,const char * name) { - MixerChannel * chan=new MixerChannel(); - chan->scale = 1.0; - chan->handler=handler; - chan->name=name; - chan->SetFreq(freq); +MixerChannel::MixerChannel(MIXER_Handler _handler, Bitu _freq, const char * _name) : + // Public member initialization + volmain {0, 0}, + next (nullptr), + name (_name), + done (0), + enabled (false), + + // Private member initialization + handler (_handler), + freq_add (0), + freq_counter (0), + needed (0), + prev_sample {0, 0}, + next_sample {0, 0}, + volmul {0, 0}, + scale {0, 0}, + channel_map {0, 0}, + interpolate (false) { +} + +MixerChannel * MIXER_AddChannel(MIXER_Handler handler, Bitu freq, const char * name) { + MixerChannel * chan=new MixerChannel(handler, freq, name); chan->next=mixer.channels; - chan->SetVolume(1,1); - chan->enabled=false; - chan->interpolate = false; + chan->SetFreq(freq); // also enables 'interpolate' if needed + chan->SetScale(1.0); + chan->SetVolume(1, 1); + chan->MapChannels(0, 1); + chan->Enable(false); mixer.channels=chan; return chan; } @@ -128,19 +152,59 @@ void MIXER_DelChannel(MixerChannel* delchan) { } void MixerChannel::UpdateVolume(void) { - volmul[0]=(Bits)((1 << MIXER_VOLSHIFT)*scale*volmain[0]*mixer.mastervol[0]); - volmul[1]=(Bits)((1 << MIXER_VOLSHIFT)*scale*volmain[1]*mixer.mastervol[1]); + volmul[0]=(Bits)((1 << MIXER_VOLSHIFT)*scale[0]*volmain[0]*mixer.mastervol[0]); + volmul[1]=(Bits)((1 << MIXER_VOLSHIFT)*scale[1]*volmain[1]*mixer.mastervol[1]); +} + +template +T clamp(const T& n, const T& lower, const T& upper) { + return std::max(lower, std::min(n, upper)); } void MixerChannel::SetVolume(float _left,float _right) { - volmain[0]=_left; - volmain[1]=_right; + // Allow unconstrained user-defined values + volmain[0] = _left; + volmain[1] = _right; UpdateVolume(); } -void MixerChannel::SetScale( float f ) { - scale = f; - UpdateVolume(); +void MixerChannel::SetScale(float f) { + SetScale(f, f); +} + +void MixerChannel::SetScale(float _left, float _right) { + // Constrain application-defined volume between 0% and 100% + const float min_volume(0.0); + const float max_volume(1.0); + _left = clamp(_left, min_volume, max_volume); + _right = clamp(_right, min_volume, max_volume); + if (scale[0] != _left || scale[1] != _right) { + scale[0] = _left; + scale[1] = _right; + UpdateVolume(); +#ifdef DEBUG + LOG_MSG("MIXER %-7s channel: application changed left and right volumes to %3.0f%% and %3.0f%%, respectively", + name, scale[0] * 100, scale[1] * 100); +#endif + } +} + +void MixerChannel::MapChannels(Bit8u _left, Bit8u _right) { + // Constrain mapping to the 0 (left) and 1 (right) channel indexes + const Bit8u min_channel(0); + const Bit8u max_channel(1); + _left = clamp(_left, min_channel, max_channel); + _right = clamp(_right, min_channel, max_channel); + if (channel_map[0] != _left || channel_map[1] != _right) { + channel_map[0] = _left; + channel_map[1] = _right; +#ifdef DEBUG + LOG_MSG("MIXER %-7s channel: application changed audio-channel mapping to left=>%s and right=>%s", + name, + channel_map[0] == 0 ? "left" : "right", + channel_map[1] == 0 ? "left" : "right"); +#endif + } } void MixerChannel::Enable(bool _yesno) { @@ -179,8 +243,8 @@ void MixerChannel::AddSilence(void) { if (done= len) return; freq_counter -= FREQ_NEXT; - prevSample[0] = nextSample[0]; + + prev_sample[0] = next_sample[0]; if (stereo) { - prevSample[1] = nextSample[1]; + prev_sample[1] = next_sample[1]; } + if ( sizeof( Type) == 1) { if (!signeddata) { if (stereo) { - nextSample[0]=(((Bit8s)(data[pos*2+0] ^ 0x80)) << 8); - nextSample[1]=(((Bit8s)(data[pos*2+1] ^ 0x80)) << 8); + next_sample[0]=(((Bit8s)(data[pos*2+0] ^ 0x80)) << 8); + next_sample[1]=(((Bit8s)(data[pos*2+1] ^ 0x80)) << 8); } else { - nextSample[0]=(((Bit8s)(data[pos] ^ 0x80)) << 8); + next_sample[0]=(((Bit8s)(data[pos] ^ 0x80)) << 8); } } else { if (stereo) { - nextSample[0]=(data[pos*2+0] << 8); - nextSample[1]=(data[pos*2+1] << 8); + next_sample[0]=(data[pos*2+0] << 8); + next_sample[1]=(data[pos*2+1] << 8); } else { - nextSample[0]=(data[pos] << 8); + next_sample[0]=(data[pos] << 8); } } //16bit and 32bit both contain 16bit data internally @@ -225,50 +291,50 @@ inline void MixerChannel::AddSamples(Bitu len, const Type* data) { if (signeddata) { if (stereo) { if (nativeorder) { - nextSample[0]=data[pos*2+0]; - nextSample[1]=data[pos*2+1]; + next_sample[0]=data[pos*2+0]; + next_sample[1]=data[pos*2+1]; } else { if ( sizeof( Type) == 2) { - nextSample[0]=(Bit16s)host_readw((HostPt)&data[pos*2+0]); - nextSample[1]=(Bit16s)host_readw((HostPt)&data[pos*2+1]); + next_sample[0]=(Bit16s)host_readw((HostPt)&data[pos*2+0]); + next_sample[1]=(Bit16s)host_readw((HostPt)&data[pos*2+1]); } else { - nextSample[0]=(Bit32s)host_readd((HostPt)&data[pos*2+0]); - nextSample[1]=(Bit32s)host_readd((HostPt)&data[pos*2+1]); + next_sample[0]=(Bit32s)host_readd((HostPt)&data[pos*2+0]); + next_sample[1]=(Bit32s)host_readd((HostPt)&data[pos*2+1]); } } } else { if (nativeorder) { - nextSample[0] = data[pos]; + next_sample[0] = data[pos]; } else { if ( sizeof( Type) == 2) { - nextSample[0]=(Bit16s)host_readw((HostPt)&data[pos]); + next_sample[0]=(Bit16s)host_readw((HostPt)&data[pos]); } else { - nextSample[0]=(Bit32s)host_readd((HostPt)&data[pos]); + next_sample[0]=(Bit32s)host_readd((HostPt)&data[pos]); } } } } else { if (stereo) { if (nativeorder) { - nextSample[0]=(Bits)data[pos*2+0]-32768; - nextSample[1]=(Bits)data[pos*2+1]-32768; + next_sample[0]=(Bits)data[pos*2+0]-32768; + next_sample[1]=(Bits)data[pos*2+1]-32768; } else { if ( sizeof( Type) == 2) { - nextSample[0]=(Bits)host_readw((HostPt)&data[pos*2+0])-32768; - nextSample[1]=(Bits)host_readw((HostPt)&data[pos*2+1])-32768; + next_sample[0]=(Bits)host_readw((HostPt)&data[pos*2+0])-32768; + next_sample[1]=(Bits)host_readw((HostPt)&data[pos*2+1])-32768; } else { - nextSample[0]=(Bits)host_readd((HostPt)&data[pos*2+0])-32768; - nextSample[1]=(Bits)host_readd((HostPt)&data[pos*2+1])-32768; + next_sample[0]=(Bits)host_readd((HostPt)&data[pos*2+0])-32768; + next_sample[1]=(Bits)host_readd((HostPt)&data[pos*2+1])-32768; } } } else { if (nativeorder) { - nextSample[0]=(Bits)data[pos]-32768; + next_sample[0]=(Bits)data[pos]-32768; } else { if ( sizeof( Type) == 2) { - nextSample[0]=(Bits)host_readw((HostPt)&data[pos])-32768; + next_sample[0]=(Bits)host_readw((HostPt)&data[pos])-32768; } else { - nextSample[0]=(Bits)host_readd((HostPt)&data[pos])-32768; + next_sample[0]=(Bits)host_readd((HostPt)&data[pos])-32768; } } } @@ -277,19 +343,28 @@ inline void MixerChannel::AddSamples(Bitu len, const Type* data) { //This sample has been handled now, increase position pos++; } + + //Apply the left and right channel mappers only on write[..] + //assignments. This ensures the channels are mapped only once + //(avoiding double-swapping) and also minimizes the places where + //we use our mapping variables as array indexes. + //Note that volumes are independent of the channels mapping. + const Bit8u left_map(channel_map[0]); + const Bit8u right_map(channel_map[1]); + //Where to write mixpos &= MIXER_BUFMASK; Bit32s* write = mixer.work[mixpos]; if (!interpolate) { - write[0] += prevSample[0] * volmul[0]; - write[1] += (stereo ? prevSample[1] : prevSample[0]) * volmul[1]; + write[0] += prev_sample[left_map] * volmul[0]; + write[1] += (stereo ? prev_sample[right_map] : prev_sample[left_map]) * volmul[1]; } else { Bits diff_mul = freq_counter & FREQ_MASK; - Bits sample = prevSample[0] + (((nextSample[0] - prevSample[0]) * diff_mul) >> FREQ_SHIFT); + Bits sample = prev_sample[left_map] + (((next_sample[left_map] - prev_sample[left_map]) * diff_mul) >> FREQ_SHIFT); write[0] += sample*volmul[0]; if (stereo) { - sample = prevSample[1] + (((nextSample[1] - prevSample[1]) * diff_mul) >> FREQ_SHIFT); + sample = prev_sample[right_map] + (((next_sample[right_map] - prev_sample[right_map]) * diff_mul) >> FREQ_SHIFT); } write[1] += sample*volmul[1]; } @@ -318,14 +393,14 @@ void MixerChannel::AddStretched(Bitu len,Bit16s * data) { if (pos != new_pos) { pos = new_pos; //Forward the previous sample - prevSample[0] = data[0]; + prev_sample[0] = data[0]; data++; } - Bits diff = data[0] - prevSample[0]; + Bits diff = data[0] - prev_sample[0]; Bits diff_mul = index & FREQ_MASK; index += index_add; mixpos &= MIXER_BUFMASK; - Bits sample = prevSample[0] + ((diff * diff_mul) >> FREQ_SHIFT); + Bits sample = prev_sample[0] + ((diff * diff_mul) >> FREQ_SHIFT); mixer.work[mixpos][0] += sample * volmul[0]; mixer.work[mixpos][1] += sample * volmul[1]; mixpos++; @@ -702,7 +777,7 @@ void MIXER_Init(Section* sec) { mixer.tick_add=calc_tickadd(mixer.freq); TIMER_AddTickHandler(MIXER_Mix_NoSound); } else { - if((mixer.freq != obtained.freq) || (mixer.blocksize != obtained.samples)) + if((static_cast(mixer.freq) != obtained.freq) || (mixer.blocksize != obtained.samples)) LOG_MSG("MIXER: Got different values from SDL: freq %d, blocksize %d",obtained.freq,obtained.samples); mixer.freq=obtained.freq; mixer.blocksize=obtained.samples;