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));