From edad78b5e09c4484438a8ee043daec01d8e25378 Mon Sep 17 00:00:00 2001 From: krcroft Date: Tue, 5 Nov 2019 17:24:45 -0800 Subject: [PATCH] Adapt and improve audio-handling in the CDROM sources Runtime improvements: - Replaces the existing audio callback routine with an efficient chunked circular-buffer audio reader - Replaces assumptions that all audio tracks are 44.1 kHz & stereo. The mixer is now fed data at the actual compressed track's rate and channel count - Eliminates all SDL locks in the audio code in favour of mixer state control - Queries the codec for track-length instead of using hundreds of iterative decimating seeks to determine track length (loading a 99-track CUE now takes 0.1 user-seconds versus 3+ seconds previously) - Seeks are performed within the already-decoded buffer (for all codecs) instead of discarding and re-running the decode sequence - SDL_Sound's buffer-size is now set once and never resized, which eliminates repeated re-malloc'ing in the library - Only one seek is performed per-playback sequence followed by sequential decodes, similar to a physical CDROM (The baseline dosbox performs a seek for every 2352-bytes of uncompressed audio) - The DOSBox mixer is now only active during playback sequences and fully dormant otherwise (baseline dosbox instead performs hundreds of calls/second with empty data) - When using Opus audio tracks, and if your dosbox.conf [mixer] rate=48000, then you will (very likely) achieve sample-exact unadulterated pass-through along your entire audio chain from the decoder, to DOSBox, to your operating system's software mixer, to your sound card driver, to your sound card, to your speakers, or to your digital receiver / USB speakers/headphones / or HDMI TV/screen. This is because almost all modern hardware DACs use a native sample rate of 48000 Source-level maintenance improvements: - It strips all pre-processor #ifdef branching for SDL_Sound from the code - Fixes all codec compiler warnings (in the modified files); builds have been tested with GCC 4 to 10, Clang 6 to 10, and MSVC 14 - Tested on Linux, macOS (Xcode), and Windows (MinGW MSYS 1.0) operating systems - Tested on i386, x86_64, ARM, and PowerPC (big-endian) architectures --- src/dos/cdrom.h | 171 ++++--- src/dos/cdrom_image.cpp | 868 +++++++++++++++++++++++++--------- src/dos/cdrom_ioctl_win32.cpp | 2 +- 3 files changed, 744 insertions(+), 297 deletions(-) diff --git a/src/dos/cdrom.h b/src/dos/cdrom.h index 4ac398af..a5931003 100644 --- a/src/dos/cdrom.h +++ b/src/dos/cdrom.h @@ -28,18 +28,21 @@ #include #include #include +#include +#include + #include "dosbox.h" #include "mem.h" #include "mixer.h" -#include "SDL.h" -#include "SDL_thread.h" - -#if defined(C_SDL_SOUND) -#include "SDL_sound.h" -#endif +#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. enum { CDROM_USE_SDL, CDROM_USE_ASPI, CDROM_USE_IOCTL_DIO, CDROM_USE_IOCTL_DX, CDROM_USE_IOCTL_MCI }; @@ -78,7 +81,6 @@ extern int CDROM_GetMountType(char* path, int force); class CDROM_Interface { public: -// CDROM_Interface (void); virtual ~CDROM_Interface (void) {}; virtual bool SetDevice (char* path, int forceCD) = 0; @@ -148,74 +150,88 @@ public: void ChannelControl (TCtrl ctrl) { return; }; bool ReadSectors (PhysPt /*buffer*/, bool /*raw*/, unsigned long /*sector*/, unsigned long /*num*/) { return true; }; bool LoadUnloadMedia (bool /*unload*/) { return true; }; -}; +}; class CDROM_Interface_Image : public CDROM_Interface { private: class TrackFile { + protected: + TrackFile(Bit16u chunkSize) : chunkSize(chunkSize) {} public: - virtual bool read(Bit8u *buffer, int seek, int count) = 0; - virtual int getLength() = 0; - virtual ~TrackFile() { }; + virtual bool read(Bit8u *buffer, int seek, int count) = 0; + virtual bool seek(Bit32u offset) = 0; + virtual Bit16u decode(Bit8u *buffer) = 0; + virtual Bit16u getEndian() = 0; + virtual Bit32u getRate() = 0; + virtual Bit8u getChannels() = 0; + virtual int getLength() = 0; + const Bit16u chunkSize; + virtual ~TrackFile() {} }; class BinaryFile : public TrackFile { public: - BinaryFile(const char *filename, bool &error); + BinaryFile (const char *filename, bool &error); + bool read(Bit8u *buffer, int seek, int count); + bool seek(Bit32u offset); + Bit16u decode(Bit8u *buffer); + Bit16u getEndian(); + Bit32u getRate() { return 44100; } + Bit8u getChannels() { return 2; } + int getLength(); ~BinaryFile(); - bool read(Bit8u *buffer, int seek, int count); - int getLength(); private: + std::ifstream *file; BinaryFile(); - std::ifstream *file; }; - #if defined(C_SDL_SOUND) class AudioFile : public TrackFile { public: - AudioFile(const char *filename, bool &error); + AudioFile (const char *filename, bool &error); + bool read(Bit8u *buffer, int seek, int count) { return false; } + bool seek(Bit32u offset); + Bit16u decode(Bit8u *buffer); + Bit16u getEndian(); + Bit32u getRate(); + Bit8u getChannels(); + int getLength(); ~AudioFile(); - bool read(Bit8u *buffer, int seek, int count); - int getLength(); private: + Sound_Sample *sample; AudioFile(); - Sound_Sample *sample; - int lastCount; - int lastSeek; }; - #endif struct Track { - int number; - int attr; - int start; - int length; - int skip; - int sectorSize; - bool mode2; TrackFile *file; + int number; + int attr; + int start; + int length; + int skip; + int sectorSize; + bool mode2; }; public: - CDROM_Interface_Image (Bit8u subUnit); - virtual ~CDROM_Interface_Image (void); - void InitNewMedia (void); - bool SetDevice (char* path, int forceCD); - bool GetUPC (unsigned char& attr, char* upc); - bool GetAudioTracks (int& stTrack, int& end, TMSF& leadOut); - bool GetAudioTrackInfo (int track, TMSF& start, unsigned char& attr); - bool GetAudioSub (unsigned char& attr, unsigned char& track, unsigned char& index, TMSF& relPos, TMSF& absPos); - bool GetAudioStatus (bool& playing, bool& pause); - bool GetMediaTrayStatus (bool& mediaPresent, bool& mediaChanged, bool& trayOpen); - bool PlayAudioSector (unsigned long start,unsigned long len); - bool PauseAudio (bool resume); - bool StopAudio (void); - void ChannelControl (TCtrl ctrl); - bool ReadSectors (PhysPt buffer, bool raw, unsigned long sector, unsigned long num); - bool LoadUnloadMedia (bool unload); - bool ReadSector (Bit8u *buffer, bool raw, unsigned long sector); - bool HasDataTrack (void); + CDROM_Interface_Image (Bit8u subUnit); + virtual ~CDROM_Interface_Image (void); + void InitNewMedia (void); + bool SetDevice (char* path, int forceCD); + bool GetUPC (unsigned char& attr, char* upc); + bool GetAudioTracks (int& stTrack, int& end, TMSF& leadOut); + bool GetAudioTrackInfo (int track, TMSF& start, unsigned char& attr); + bool GetAudioSub (unsigned char& attr, unsigned char& track, unsigned char& index, TMSF& relPos, TMSF& absPos); + bool GetAudioStatus (bool& playing, bool& pause); + bool GetMediaTrayStatus (bool& mediaPresent, bool& mediaChanged, bool& trayOpen); + bool PlayAudioSector (unsigned long start,unsigned long len); + bool PauseAudio (bool resume); + bool StopAudio (void); + void ChannelControl (TCtrl ctrl); + bool ReadSectors (PhysPt buffer, bool raw, unsigned long sector, unsigned long num); + bool LoadUnloadMedia (bool unload); + bool ReadSector (Bit8u *buffer, bool raw, unsigned long sector); + bool HasDataTrack (void); static CDROM_Interface_Image* images[26]; @@ -225,35 +241,42 @@ static void CDAudioCallBack(Bitu len); int GetTrack(int sector); static struct imagePlayer { + Bit8u buffer[AUDIO_DECODE_BUFFER_SIZE]; + TCtrl ctrlData; + TrackFile *trackFile; + MixerChannel *channel; CDROM_Interface_Image *cd; - MixerChannel *channel; - SDL_mutex *mutex; - Bit8u buffer[8192]; - int bufLen; - int currFrame; - int targetFrame; - bool isPlaying; - bool isPaused; - bool ctrlUsed; - TCtrl ctrlData; + void (MixerChannel::*addSamples) (Bitu, const Bit16s*); + Bit32u startFrame; + Bit32u currFrame; + Bit32u numFrames; + Bit32u playbackTotal; + Bit32s playbackRemaining; + Bit16u bufferPos; + Bit16u bufferConsumed; + bool isPlaying; + bool isPaused; + bool ctrlUsed; } player; - void ClearTracks(); - bool LoadIsoFile(char *filename); - bool CanReadPVD(TrackFile *file, int sectorSize, bool mode2); - // cue sheet processing - bool LoadCueSheet(char *cuefile); - bool GetRealFileName(std::string& filename, std::string& pathname); - bool GetCueKeyword(std::string &keyword, std::istream &in); - bool GetCueFrame(int &frames, std::istream &in); - bool GetCueString(std::string &str, std::istream &in); - bool AddTrack(Track &curr, int &shift, int prestart, int &totalPregap, int currPregap); + void ClearTracks(); + bool LoadIsoFile(char *filename); + bool CanReadPVD(TrackFile *file, int sectorSize, bool mode2); -static int refCount; - std::vector tracks; -typedef std::vector::iterator track_it; - std::string mcn; - Bit8u subUnit; + // cue sheet processing + bool LoadCueSheet(char *cuefile); + bool GetRealFileName(std::string& filename, std::string& pathname); + bool GetCueKeyword(std::string &keyword, std::istream &in); + bool GetCueFrame(int &frames, std::istream &in); + bool GetCueString(std::string &str, std::istream &in); + bool AddTrack(Track &curr, int &shift, int prestart, int &totalPregap, int currPregap); + + // member variables + std::vector tracks; + typedef std::vector::iterator track_it; + std::string mcn; + static int refCount; + Bit8u subUnit; }; #if defined (WIN32) /* Win 32 */ @@ -317,7 +340,7 @@ public: enum cdioctl_cdatype { CDIOCTL_CDA_DIO, CDIOCTL_CDA_MCI, CDIOCTL_CDA_DX }; cdioctl_cdatype cdioctl_cda_selected; - CDROM_Interface_Ioctl (cdioctl_cdatype ioctl_cda); + CDROM_Interface_Ioctl (CDROM_Interface_Ioctl::cdioctl_cdatype ioctl_cda); virtual ~CDROM_Interface_Ioctl(void); bool SetDevice (char* path, int forceCD); diff --git a/src/dos/cdrom_image.cpp b/src/dos/cdrom_image.cpp index 5a846a93..25725d13 100644 --- a/src/dos/cdrom_image.cpp +++ b/src/dos/cdrom_image.cpp @@ -16,6 +16,10 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +// #define DEBUG 1 +#ifdef DEBUG +#include // time_t, tm, time(), and localtime() +#endif #include #include @@ -43,16 +47,30 @@ using namespace std; #define MAX_LINE_LENGTH 512 #define MAX_FILENAME_LENGTH 256 +#ifdef DEBUG +char* get_time() { + static char time_str[] = "00:00:00"; + static time_t rawtime; + time(&rawtime); + const struct tm* ptime = localtime(&rawtime); + snprintf(time_str, sizeof(time_str), "%02d:%02d:%02d", ptime->tm_hour, ptime->tm_min, ptime->tm_sec); + return time_str; +} +#endif + CDROM_Interface_Image::BinaryFile::BinaryFile(const char *filename, bool &error) + :TrackFile(RAW_SECTOR_SIZE) { file = new ifstream(filename, ios::in | ios::binary); - error = (file == NULL) || (file->fail()); + error = (file == nullptr) || (file->fail()); } CDROM_Interface_Image::BinaryFile::~BinaryFile() { - delete file; - file = NULL; + if (file != nullptr) { + delete file; + file = nullptr; + } } bool CDROM_Interface_Image::BinaryFile::read(Bit8u *buffer, int seek, int count) @@ -64,20 +82,58 @@ bool CDROM_Interface_Image::BinaryFile::read(Bit8u *buffer, int seek, int count) int CDROM_Interface_Image::BinaryFile::getLength() { - file->seekg(0, ios::end); - int length = (int)file->tellg(); - if (file->fail()) return -1; + int length = -1; // The maximum value held by a signed int is 2,147,483,647, + // which is larger than the maximum size of a CDROM, which + // is known to be 99 minutes or roughly 870 MB in size. + if (file) { + std::streampos original_pos = file->tellg(); + file->seekg(0, ios::end); + length = static_cast(file->tellg()); + file->seekg(original_pos, ios::end); + } return length; } -#if defined(C_SDL_SOUND) -CDROM_Interface_Image::AudioFile::AudioFile(const char *filename, bool &error) +Bit16u CDROM_Interface_Image::BinaryFile::getEndian() { - Sound_AudioInfo desired = {AUDIO_S16, 2, 44100}; - sample = Sound_NewSampleFromFile(filename, &desired, RAW_SECTOR_SIZE); - lastCount = RAW_SECTOR_SIZE; - lastSeek = 0; - error = (sample == NULL); + // Image files are read into native-endian byte-order +#if defined(WORDS_BIGENDIAN) + return AUDIO_S16MSB; +#else + return AUDIO_S16LSB; +#endif +} + + +bool CDROM_Interface_Image::BinaryFile::seek(Bit32u offset) +{ + file->seekg(offset, ios::beg); + return !file->fail(); +} + +Bit16u CDROM_Interface_Image::BinaryFile::decode(Bit8u *buffer) +{ + file->read((char*)buffer, chunkSize); + return (Bit16u) file->gcount(); +} + +CDROM_Interface_Image::AudioFile::AudioFile(const char *filename, bool &error) + :TrackFile(4096) +{ + // 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); + if (sample) { + error = false; + std::string filename_only(filename); + filename_only = filename_only.substr(filename_only.find_last_of("\\/") + 1); + LOG_MSG("CDROM: Loaded %s [%d Hz %d-channel]", + filename_only.c_str(), + this->getRate(), + this->getChannels()); + } else { + error = true; + } } CDROM_Interface_Image::AudioFile::~AudioFile() @@ -85,65 +141,111 @@ CDROM_Interface_Image::AudioFile::~AudioFile() Sound_FreeSample(sample); } -bool CDROM_Interface_Image::AudioFile::read(Bit8u *buffer, int seek, int count) +bool CDROM_Interface_Image::AudioFile::seek(Bit32u offset) { - if (lastCount != count) { - int success = Sound_SetBufferSize(sample, count); - if (!success) return false; - } - if (lastSeek != (seek - count)) { - int success = Sound_Seek(sample, (int)((double)(seek) / 176.4f)); - if (!success) return false; - } - lastSeek = seek; - int bytes = Sound_Decode(sample); - if (bytes < count) { - memcpy(buffer, sample->buffer, bytes); - memset(buffer + bytes, 0, count - bytes); - } else { - memcpy(buffer, sample->buffer, count); - } - - return !(sample->flags & SOUND_SAMPLEFLAG_ERROR); +#ifdef BENCHMARK +#include + // This benchmarking block requires C++11 to create a trivial ANSI sub-second timer + // that's portable across Windows, Linux, and macOS. Otherwise, to do so using lesser + // standards requires a combination of very lengthly solutions for each operating system: + // https://stackoverflow.com/questions/361363/how-to-measure-time-in-milliseconds-using-ansi-c + // Leave this in place (but #ifdef'ed away) for development and regression testing purposes. + const auto begin = std::chrono::steady_clock::now(); +#endif + + // Convert the byte-offset to a time offset (milliseconds) + const bool result = Sound_Seek(sample, lround(offset/176.4f)); + +#ifdef BENCHMARK + const auto end = std::chrono::steady_clock::now(); + LOG_MSG("%s CDROM: seek(%u) took %f ms", + get_time(), + offset, + chrono::duration (end - begin).count()); +#endif + return result; +} + +Bit16u CDROM_Interface_Image::AudioFile::decode(Bit8u *buffer) +{ + const Bit16u bytes = Sound_Decode(sample); + memcpy(buffer, sample->buffer, bytes); + return bytes; +} + +Bit16u CDROM_Interface_Image::AudioFile::getEndian() +{ + return sample->actual.format; +} + +Bit32u CDROM_Interface_Image::AudioFile::getRate() +{ + return sample ? sample->actual.rate : 0; +} + +Bit8u CDROM_Interface_Image::AudioFile::getChannels() +{ + return sample ? sample->actual.channels : 0; } int CDROM_Interface_Image::AudioFile::getLength() { - int time = 1; - int shift = 0; - if (!(sample->flags & SOUND_SAMPLEFLAG_CANSEEK)) return -1; - - while (true) { - int success = Sound_Seek(sample, (unsigned int)(shift + time)); - if (!success) { - if (time == 1) return lround((double)shift * 176.4f); - shift += time >> 1; - time = 1; - } else { - if (time > ((numeric_limits::max() - shift) / 2)) return -1; - time = time << 1; - } + int length(-1); + + // GetDuration returns milliseconds ... but getLength needs Red Book bytes. + const int duration_ms = Sound_GetDuration(sample); + if (duration_ms > 0) { + // ... so convert ms to "Red Book bytes" by multiplying with 176.4f, + // which is 44,100 samples/second * 2-channels * 2 bytes/sample + // / 1000 milliseconds/second + length = (int) round(duration_ms * 176.4f); } -} +#ifdef DEBUG + LOG_MSG("%s CDROM: AudioFile::getLength is %d bytes", + get_time(), + length); #endif + return length; +} + // initialize static members int CDROM_Interface_Image::refCount = 0; CDROM_Interface_Image* CDROM_Interface_Image::images[26] = {}; CDROM_Interface_Image::imagePlayer CDROM_Interface_Image::player = { - NULL, NULL, NULL, {0}, 0, 0, 0, false, false, false, {0} }; + {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 +}; + - CDROM_Interface_Image::CDROM_Interface_Image(Bit8u subUnit) :subUnit(subUnit) { images[subUnit] = this; if (refCount == 0) { - player.mutex = SDL_CreateMutex(); - if (!player.channel) { - player.channel = MIXER_AddChannel(&CDAudioCallBack, 44100, "CDAUDIO"); + if (player.channel == nullptr) { + // channel is kept dormant except during cdrom playback periods + 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); +#endif } - player.channel->Enable(true); } refCount++; } @@ -151,11 +253,17 @@ CDROM_Interface_Image::CDROM_Interface_Image(Bit8u subUnit) CDROM_Interface_Image::~CDROM_Interface_Image() { refCount--; - if (player.cd == this) player.cd = NULL; + if (player.cd == this) { + player.cd = nullptr; + } ClearTracks(); if (refCount == 0) { - SDL_DestroyMutex(player.mutex); - player.channel->Enable(false); + StopAudio(); + MIXER_DelChannel(player.channel); + player.channel = nullptr; +#ifdef DEBUG + LOG_MSG("CDROM: Audio channel freed"); +#endif } } @@ -165,9 +273,11 @@ void CDROM_Interface_Image::InitNewMedia() bool CDROM_Interface_Image::SetDevice(char* path, int forceCD) { - if (LoadCueSheet(path)) return true; - if (LoadIsoFile(path)) return true; - + if (LoadCueSheet(path) || + LoadIsoFile(path)) { + return true; + } + // print error message on dosbox console char buf[MAX_LINE_LENGTH]; snprintf(buf, MAX_LINE_LENGTH, "Could not load image file: %s\n", path); @@ -180,6 +290,12 @@ bool CDROM_Interface_Image::GetUPC(unsigned char& attr, char* upc) { attr = 0; strcpy(upc, this->mcn.c_str()); +#ifdef DEBUG + LOG_MSG("%s CDROM: GetUPC=%s", + get_time(), + upc); +#endif + return true; } @@ -187,27 +303,77 @@ bool CDROM_Interface_Image::GetAudioTracks(int& stTrack, int& end, TMSF& leadOut { stTrack = 1; end = (int)(tracks.size() - 1); - frames_to_msf(tracks[tracks.size() - 1].start + 150, &leadOut.min, &leadOut.sec, &leadOut.fr); + frames_to_msf(tracks[tracks.size() - 1].start + 150, + &leadOut.min, &leadOut.sec, &leadOut.fr); +#ifdef DEBUG + LOG_MSG("%s CDROM: GetAudioTracks, stTrack=%d, end=%d, " + "leadOut.min=%d, leadOut.sec=%d, leadOut.fr=%d", + get_time(), + stTrack, + end, + leadOut.min, + leadOut.sec, + leadOut.fr); +#endif + return true; } bool CDROM_Interface_Image::GetAudioTrackInfo(int track, TMSF& start, unsigned char& attr) { - if (track < 1 || track > (int)tracks.size()) return false; - frames_to_msf(tracks[track - 1].start + 150, &start.min, &start.sec, &start.fr); + if (track < 1 || track > (int)tracks.size()) { + return false; + } + frames_to_msf(tracks[track - 1].start + 150, + &start.min, &start.sec, &start.fr); attr = tracks[track - 1].attr; + +#ifdef DEBUG + LOG_MSG("%s CDROM: GetAudioTrackInfo track=%d MSF %02d:%02d:%02d, attr=%u", + get_time(), + track, + start.min, + start.sec, + start.fr, + attr); +#endif return true; } -bool CDROM_Interface_Image::GetAudioSub(unsigned char& attr, unsigned char& track, unsigned char& index, TMSF& relPos, TMSF& absPos) +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; + 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); + +#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); +#endif return true; } @@ -215,6 +381,14 @@ bool CDROM_Interface_Image::GetAudioStatus(bool& playing, bool& pause) { playing = player.isPlaying; pause = player.isPaused; + +#ifdef DEBUG + LOG_MSG("%s CDROM: GetAudioStatus playing=%d, paused=%d", + get_time(), + playing, + pause); +#endif + return true; } @@ -223,39 +397,137 @@ bool CDROM_Interface_Image::GetMediaTrayStatus(bool& mediaPresent, bool& mediaCh mediaPresent = true; mediaChanged = false; trayOpen = false; + +#ifdef DEBUG + LOG_MSG("%s CDROM: GetMediaTrayStatus present=%d, changed=%d, open=%d", + get_time(), + mediaPresent, + mediaChanged, + trayOpen); +#endif + return true; } -bool CDROM_Interface_Image::PlayAudioSector(unsigned long start,unsigned long len) +bool CDROM_Interface_Image::PlayAudioSector(unsigned long start, unsigned long len) { - // We might want to do some more checks. E.g valid start and length - SDL_mutexP(player.mutex); - player.cd = this; - player.currFrame = start; - player.targetFrame = start + len; - int track = GetTrack(start) - 1; - if(track >= 0 && tracks[track].attr == 0x40) { + bool is_playable(false); + const int track = GetTrack(start) - 1; + + // The CDROM Red Book standard allows up to 99 tracks, which includes the data track + if ( track < 0 || track > 99 ) + LOG(LOG_MISC, LOG_WARN)("Game tried to load track #%d, which is invalid", track); + + // Attempting to play zero sectors is a no-op + else if (len == 0) + LOG(LOG_MISC, LOG_WARN)("Game tried to play zero sectors, skipping"); + + // The maximum storage achieved on a CDROM was ~900MB or just under 100 minutes + // with overburning, so use this threshold to sanity-check the start sector. + else if (start > 450000) + LOG(LOG_MISC, LOG_WARN)("Game tried to read sector %lu, " + "which is beyond the 100-minute maximum of a CDROM", + start); + + // 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"); - player.isPlaying = false; - //Unclear wether return false should be here. - //specs say that this function returns at once and games should check the status wether the audio is actually playing - //Real drives either fail or succeed as well - } else player.isPlaying = true; - player.isPaused = false; - SDL_mutexV(player.mutex); - return true; + + // Checks passed, setup the audio stream + else { + TrackFile* trackFile = tracks[track].file; + + // Convert the playback start sector to a time offset (milliseconds) relative to the track + const Bit32u offset = tracks[track].skip + (start - tracks[track].start) * tracks[track].sectorSize; + is_playable = trackFile->seek(offset); + + // only initialize the player elements if our track is playable + if (is_playable) { + const Bit8u channels = trackFile->getChannels(); + const Bit32u 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.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; + +#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)", + get_time(), + track, + rate/1000.0, + channels, + start, + offset * (1/10584000.0), + offset, + tracks[track].skip, + tracks[track].start, + tracks[track].sectorSize, + len, + player.playbackRemaining / (1000 * bytesPerMs)); +#endif + + // start the channel! + player.channel->SetFreq(rate); + player.channel->Enable(true); + } + } + if (!is_playable) StopAudio(); + return is_playable; } bool CDROM_Interface_Image::PauseAudio(bool resume) { - player.isPaused = !resume; + // Only switch states if needed + if (player.isPaused == resume) { + player.channel->Enable(resume); + player.isPaused = !resume; + } + +#ifdef DEBUG + LOG_MSG("%s CDROM: PauseAudio, state=%s", + get_time(), resume ? "resumed" : "paused"); +#endif + return true; } bool CDROM_Interface_Image::StopAudio(void) { - player.isPlaying = false; - player.isPaused = false; + // Only switch states if needed + if (player.isPlaying) { + player.channel->Enable(false); + player.isPlaying = false; + player.isPaused = false; + } + +#ifdef DEBUG + LOG_MSG("%s CDROM: StopAudio", get_time()); +#endif + return true; } @@ -270,7 +542,7 @@ bool CDROM_Interface_Image::ReadSectors(PhysPt buffer, bool raw, unsigned long s int sectorSize = raw ? RAW_SECTOR_SIZE : COOKED_SECTOR_SIZE; Bitu buflen = num * sectorSize; Bit8u* buf = new Bit8u[buflen]; - + bool success = true; //Gobliiins reads 0 sectors for(unsigned long i = 0; i < num; i++) { success = ReadSector(&buf[i * sectorSize], raw, sector + i); @@ -279,7 +551,6 @@ bool CDROM_Interface_Image::ReadSectors(PhysPt buffer, bool raw, unsigned long s MEM_BlockWrite(buffer, buf, buflen); delete[] buf; - return success; } @@ -292,11 +563,13 @@ int CDROM_Interface_Image::GetTrack(int sector) { vector::iterator i = tracks.begin(); vector::iterator end = tracks.end() - 1; - + while(i != end) { Track &curr = *i; Track &next = *(i + 1); - if (curr.start <= sector && sector < next.start) return curr.number; + if (curr.start <= sector && sector < next.start) { + return curr.number; + } i++; } return -1; @@ -305,110 +578,215 @@ int CDROM_Interface_Image::GetTrack(int sector) bool CDROM_Interface_Image::ReadSector(Bit8u *buffer, bool raw, unsigned long sector) { int track = GetTrack(sector) - 1; - if (track < 0) return false; - + if (track < 0) { + return false; + } + int seek = tracks[track].skip + (sector - tracks[track].start) * tracks[track].sectorSize; int length = (raw ? RAW_SECTOR_SIZE : COOKED_SECTOR_SIZE); - if (tracks[track].sectorSize != RAW_SECTOR_SIZE && raw) return false; + if (tracks[track].sectorSize != RAW_SECTOR_SIZE && raw) { + return false; + } if (tracks[track].sectorSize == RAW_SECTOR_SIZE && !tracks[track].mode2 && !raw) seek += 16; if (tracks[track].mode2 && !raw) seek += 24; +#ifdef DEBUG + LOG_MSG("CDROM: ReadSector track=%d, desired raw=%s, sector=%ld, length=%d", + track, + raw ? "true":"false", + sector, + length); +#endif return tracks[track].file->read(buffer, seek, length); } void CDROM_Interface_Image::CDAudioCallBack(Bitu len) { - len *= 4; // 16 bit, stereo - if (!len) return; - if (!player.isPlaying || player.isPaused) { - player.channel->AddSilence(); + // 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; } - - SDL_mutexP(player.mutex); - while (player.bufLen < (Bits)len) { - bool success; - if (player.targetFrame > player.currFrame) - success = player.cd->ReadSector(&player.buffer[player.bufLen], true, player.currFrame); - else success = false; - - if (success) { - player.currFrame++; - player.bufLen += RAW_SECTOR_SIZE; - } else { - memset(&player.buffer[player.bufLen], 0, len - player.bufLen); - player.bufLen = len; - player.isPlaying = false; + + // 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; + + while (total_requested > 0) { + int requested = total_requested; + + // 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; } - } - SDL_mutexV(player.mutex); - if (player.ctrlUsed) { - Bit16s sample0,sample1; - Bit16s * samples=(Bit16s *)&player.buffer; - for (Bitu pos=0;pos= 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) - player.channel->AddSamples_s16(len/4,(Bit16s *)player.buffer); - } else player.channel->AddSamples_s16_nonnative(len/4,(Bit16s *)player.buffer); + sample = (Bit16s)host_readw((HostPt) & samples[pos * 2 + player.ctrlData.out[i]]); #else - } - player.channel->AddSamples_s16(len/4,(Bit16s *)player.buffer); + sample = samples[pos * 2 + player.ctrlData.out[i]]; #endif - memmove(player.buffer, &player.buffer[len], player.bufLen - len); - player.bufLen -= len; + 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(); + } } bool CDROM_Interface_Image::LoadIsoFile(char* filename) { tracks.clear(); - + // data track - Track track = {0, 0, 0, 0, 0, 0, false, NULL}; + Track track = {nullptr, // TrackFile* + 0, // number + 0, // attr + 0, // start + 0, // length + 0, // skip + 0, // sectorSize + false}; // mode2 + bool error; track.file = new BinaryFile(filename, error); if (error) { - delete track.file; - track.file = NULL; + if (track.file != nullptr) { + delete track.file; + track.file = nullptr; + } return false; } track.number = 1; track.attr = 0x40;//data - + // try to detect iso type if (CanReadPVD(track.file, COOKED_SECTOR_SIZE, false)) { track.sectorSize = COOKED_SECTOR_SIZE; track.mode2 = false; } else if (CanReadPVD(track.file, RAW_SECTOR_SIZE, false)) { track.sectorSize = RAW_SECTOR_SIZE; - track.mode2 = false; + track.mode2 = false; } else if (CanReadPVD(track.file, 2336, true)) { track.sectorSize = 2336; - track.mode2 = true; + track.mode2 = true; } else if (CanReadPVD(track.file, RAW_SECTOR_SIZE, true)) { track.sectorSize = RAW_SECTOR_SIZE; - track.mode2 = true; - } else return false; - + track.mode2 = true; + } else { + if (track.file != nullptr) { + delete track.file; + track.file = nullptr; + } + return false; + } track.length = track.file->getLength() / track.sectorSize; +#ifdef DEBUG + LOG_MSG("LoadIsoFile: %s, track 1, 0x40, sectorSize=%d, mode2=%s", + filename, + track.sectorSize, + track.mode2 ? "true":"false"); +#endif + tracks.push_back(track); - + // leadout track track.number = 2; track.attr = 0; track.start = track.length; track.length = 0; - track.file = NULL; + track.file = nullptr; tracks.push_back(track); - return true; } @@ -427,9 +805,9 @@ bool CDROM_Interface_Image::CanReadPVD(TrackFile *file, int sectorSize, bool mod #if defined(WIN32) static string dirname(char * file) { char * sep = strrchr(file, '\\'); - if (sep == NULL) + if (sep == nullptr) sep = strrchr(file, '/'); - if (sep == NULL) + if (sep == nullptr) return ""; else { int len = (int)(sep - file); @@ -442,44 +820,55 @@ static string dirname(char * file) { bool CDROM_Interface_Image::LoadCueSheet(char *cuefile) { - Track track = {0, 0, 0, 0, 0, 0, false, NULL}; + Track track = {nullptr, // TrackFile* + 0, // number + 0, // attr + 0, // start + 0, // length + 0, // skip + 0, // sectorSize + false}; // mode2 tracks.clear(); int shift = 0; int currPregap = 0; int totalPregap = 0; - int prestart = 0; + int prestart = -1; bool success; bool canAddTrack = false; - char tmp[MAX_FILENAME_LENGTH]; // dirname can change its argument + char tmp[MAX_FILENAME_LENGTH]; // dirname can change its argument safe_strncpy(tmp, cuefile, MAX_FILENAME_LENGTH); string pathname(dirname(tmp)); ifstream in; in.open(cuefile, ios::in); - if (in.fail()) return false; - + if (in.fail()) { + return false; + } + while(!in.eof()) { // get next line char buf[MAX_LINE_LENGTH]; in.getline(buf, MAX_LINE_LENGTH); - if (in.fail() && !in.eof()) return false; // probably a binary file + if (in.fail() && !in.eof()) { + return false; // probably a binary file + } istringstream line(buf); - + string command; GetCueKeyword(command, line); - + if (command == "TRACK") { if (canAddTrack) success = AddTrack(track, shift, prestart, totalPregap, currPregap); else success = true; - + track.start = 0; track.skip = 0; currPregap = 0; - prestart = 0; - + prestart = -1; + line >> track.number; string type; GetCueKeyword(type, line); - + if (type == "AUDIO") { track.sectorSize = RAW_SECTOR_SIZE; track.attr = 0; @@ -501,7 +890,7 @@ bool CDROM_Interface_Image::LoadCueSheet(char *cuefile) track.attr = 0x40; track.mode2 = true; } else success = false; - + canAddTrack = true; } else if (command == "INDEX") { @@ -509,7 +898,7 @@ bool CDROM_Interface_Image::LoadCueSheet(char *cuefile) line >> index; int frame; success = GetCueFrame(frame, line); - + if (index == 1) track.start = frame; else if (index == 0) prestart = frame; // ignore other indices @@ -518,110 +907,143 @@ bool CDROM_Interface_Image::LoadCueSheet(char *cuefile) if (canAddTrack) success = AddTrack(track, shift, prestart, totalPregap, currPregap); else success = true; canAddTrack = false; - + string filename; GetCueString(filename, line); GetRealFileName(filename, pathname); string type; GetCueKeyword(type, line); - track.file = NULL; + track.file = nullptr; bool error = true; if (type == "BINARY") { track.file = new BinaryFile(filename.c_str(), error); } -#if defined(C_SDL_SOUND) - //The next if has been surpassed by the else, but leaving it in as not - //to break existing cue sheets that depend on this.(mine with OGG tracks specifying MP3 as type) - else if (type == "WAVE" || type == "AIFF" || type == "MP3") { + else { track.file = new AudioFile(filename.c_str(), error); - } else { - const Sound_DecoderInfo **i; - for (i = Sound_AvailableDecoders(); *i != NULL; i++) { - if (*(*i)->extensions == type) { - track.file = new AudioFile(filename.c_str(), error); - break; - } - } + // SDL_Sound first tries using a decoder having a matching registered extension + // as the filename, and then falls back to trying each decoder before finally + // giving up. } -#endif if (error) { - delete track.file; - track.file = NULL; + if (track.file != nullptr) { + delete track.file; + track.file = nullptr; + } success = false; } } else if (command == "PREGAP") success = GetCueFrame(currPregap, line); else if (command == "CATALOG") success = GetCueString(mcn, line); // ignored commands - else if (command == "CDTEXTFILE" || command == "FLAGS" || command == "ISRC" - || command == "PERFORMER" || command == "POSTGAP" || command == "REM" - || command == "SONGWRITER" || command == "TITLE" || command == "") success = true; + else if (command == "CDTEXTFILE" || command == "FLAGS" || command == "ISRC" || + command == "PERFORMER" || command == "POSTGAP" || command == "REM" || + command == "SONGWRITER" || command == "TITLE" || command == "") { + success = true; + } // failure - else success = false; - - if (!success) return false; + else { + if (track.file != nullptr) { + delete track.file; + track.file = nullptr; + } + success = false; + } + if (!success) { + if (track.file != nullptr) { + delete track.file; + track.file = nullptr; + } + return false; + } } // add last track - if (!AddTrack(track, shift, prestart, totalPregap, currPregap)) return false; - + if (!AddTrack(track, shift, prestart, totalPregap, currPregap)) { + return false; + } + // add leadout track track.number++; track.attr = 0;//sync with load iso track.start = 0; track.length = 0; - track.file = NULL; - if(!AddTrack(track, shift, 0, totalPregap, 0)) return false; - + track.file = nullptr; + if (!AddTrack(track, shift, -1, totalPregap, 0)) { + return false; + } return true; } + + bool CDROM_Interface_Image::AddTrack(Track &curr, int &shift, int prestart, int &totalPregap, int currPregap) { + int skip = 0; + // frames between index 0(prestart) and 1(curr.start) must be skipped - int skip; - if (prestart > 0) { - if (prestart > curr.start) return false; + if (prestart >= 0) { + if (prestart > curr.start) { + return false; + } skip = curr.start - prestart; - } else skip = 0; - + } + // first track (track number must be 1) if (tracks.empty()) { - if (curr.number != 1) return false; + if (curr.number != 1) { + return false; + } curr.skip = skip * curr.sectorSize; curr.start += currPregap; totalPregap = currPregap; tracks.push_back(curr); return true; } - + Track &prev = *(tracks.end() - 1); - + // current track consumes data from the same file as the previous if (prev.file == curr.file) { curr.start += shift; - prev.length = curr.start + totalPregap - prev.start - skip; - curr.skip += prev.skip + prev.length * prev.sectorSize + skip * curr.sectorSize; + if (!prev.length) { + prev.length = curr.start + totalPregap - prev.start - skip; + } + curr.skip += prev.skip + prev.length * prev.sectorSize + skip * curr.sectorSize; totalPregap += currPregap; curr.start += totalPregap; // current track uses a different file as the previous track } else { - int tmp = prev.file->getLength() - prev.skip; - prev.length = tmp / prev.sectorSize; - if (tmp % prev.sectorSize != 0) prev.length++; // padding - + if (!prev.length) { + int tmp = prev.file->getLength() - prev.skip; + prev.length = tmp / prev.sectorSize; + if (tmp % prev.sectorSize != 0) prev.length++; // padding + } curr.start += prev.start + prev.length + currPregap; curr.skip = skip * curr.sectorSize; shift += prev.start + prev.length; totalPregap = currPregap; } - + +#ifdef DEBUG + LOG_MSG("%s CDROM: AddTrack cur.start=%d cur.len=%d cur.start+len=%d " + "| prev.start=%d prev.len=%d prev.start+len=%d", + get_time(), + curr.start, + curr.length, + curr.start + curr.length, + prev.start, + prev.length, + prev.start + prev.length); +#endif + // error checks - if (curr.number <= 1) return false; - if (prev.number + 1 != curr.number) return false; - if (curr.start < prev.start + prev.length) return false; - if (curr.length < 0) return false; - + if (curr.number <= 1 || + prev.number + 1 != curr.number || + curr.start < prev.start + prev.length || + curr.length < 0) { + return false; + } + tracks.push_back(curr); return true; } @@ -630,7 +1052,9 @@ bool CDROM_Interface_Image::HasDataTrack(void) { //Data track has attribute 0x40 for(track_it it = tracks.begin(); it != tracks.end(); it++) { - if ((*it).attr == 0x40) return true; + if ((*it).attr == 0x40) { + return true; + } } return false; } @@ -640,8 +1064,10 @@ bool CDROM_Interface_Image::GetRealFileName(string &filename, string &pathname) { // check if file exists struct stat test; - if (stat(filename.c_str(), &test) == 0) return true; - + if (stat(filename.c_str(), &test) == 0) { + return true; + } + // check if file with path relative to cue file exists string tmpstr(pathname + "/" + filename); if (stat(tmpstr.c_str(), &test) == 0) { @@ -653,8 +1079,10 @@ bool CDROM_Interface_Image::GetRealFileName(string &filename, string &pathname) char tmp[CROSS_LEN]; safe_strncpy(tmp, filename.c_str(), CROSS_LEN); Bit8u drive; - if (!DOS_MakeName(tmp, fullname, &drive)) return false; - + if (!DOS_MakeName(tmp, fullname, &drive)) { + return false; + } + localDrive *ldp = dynamic_cast(Drives[drive]); if (ldp) { ldp->GetSystemFilename(tmp, fullname); @@ -666,13 +1094,13 @@ bool CDROM_Interface_Image::GetRealFileName(string &filename, string &pathname) #if defined (WIN32) || defined(OS2) //Nothing #else - //Consider the possibility that the filename has a windows directory seperator (inside the CUE file) + //Consider the possibility that the filename has a windows directory seperator (inside the CUE file) //which is common for some commercial rereleases of DOS games using DOSBox string copy = filename; size_t l = copy.size(); for (size_t i = 0; i < l;i++) { - if(copy[i] == '\\') copy[i] = '/'; + if (copy[i] == '\\') copy[i] = '/'; } if (stat(copy.c_str(), &test) == 0) { @@ -694,7 +1122,7 @@ bool CDROM_Interface_Image::GetCueKeyword(string &keyword, istream &in) { in >> keyword; for(Bitu i = 0; i < keyword.size(); i++) keyword[i] = toupper(keyword[i]); - + return true; } @@ -731,7 +1159,7 @@ void CDROM_Interface_Image::ClearTracks() vector::iterator i = tracks.begin(); vector::iterator end = tracks.end(); - TrackFile* last = NULL; + TrackFile* last = nullptr; while(i != end) { Track &curr = *i; if (curr.file != last) { @@ -744,14 +1172,10 @@ void CDROM_Interface_Image::ClearTracks() } void CDROM_Image_Destroy(Section*) { -#if defined(C_SDL_SOUND) Sound_Quit(); -#endif } -void CDROM_Image_Init(Section* section) { -#if defined(C_SDL_SOUND) +void CDROM_Image_Init(Section* sec) { + sec->AddDestroyFunction(CDROM_Image_Destroy, false); Sound_Init(); - section->AddDestroyFunction(CDROM_Image_Destroy, false); -#endif } diff --git a/src/dos/cdrom_ioctl_win32.cpp b/src/dos/cdrom_ioctl_win32.cpp index 80fbb288..83147798 100644 --- a/src/dos/cdrom_ioctl_win32.cpp +++ b/src/dos/cdrom_ioctl_win32.cpp @@ -176,7 +176,7 @@ bool CDROM_Interface_Ioctl::mci_CDPosition(int *position) { CDROM_Interface_Ioctl::dxPlayer CDROM_Interface_Ioctl::player = { NULL, NULL, NULL, {0}, 0, 0, 0, false, false, false, {0} }; -CDROM_Interface_Ioctl::CDROM_Interface_Ioctl(cdioctl_cdatype ioctl_cda) { +CDROM_Interface_Ioctl::CDROM_Interface_Ioctl(CDROM_Interface_Ioctl::cdioctl_cdatype ioctl_cda) { pathname[0] = 0; hIOCTL = INVALID_HANDLE_VALUE; memset(&oldLeadOut,0,sizeof(oldLeadOut));