From d1ebd7564128a19972fd4404ad18af2c8421e07c Mon Sep 17 00:00:00 2001 From: DarkWyrm Date: Wed, 20 Jul 2005 01:04:55 +0000 Subject: [PATCH] Refactored CDDB lookup code CDDB data is written in R5 format with the exception of CD:key (still figuring out what to do about that) Fixed some update notification problems with changing CDs git-svn-id: file:///srv/svn/repos/haiku/haiku/trunk@13777 a95241bf-73f2-0310-859d-f6bbb57e9c96 --- src/apps/cdplayer/CDDBSupport.cpp | 532 ++++++++++++++++++------------ src/apps/cdplayer/CDDBSupport.h | 101 +++--- src/apps/cdplayer/CDEngine.cpp | 40 ++- src/apps/cdplayer/CDEngine.h | 3 +- 4 files changed, 395 insertions(+), 281 deletions(-) diff --git a/src/apps/cdplayer/CDDBSupport.cpp b/src/apps/cdplayer/CDDBSupport.cpp index 02f0db35d9..37bb0b56b0 100644 --- a/src/apps/cdplayer/CDDBSupport.cpp +++ b/src/apps/cdplayer/CDDBSupport.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -23,9 +24,24 @@ #include #include #include +#include const int kTerminatingSignal = SIGINT; // SIGCONT; +// The SCSI table of contents consists of a 4-byte header followed by 100 track +// descriptors, which are each 8 bytes. We don't really need the first 5 bytes +// of the track descriptor, so we'll just ignore them. All we really want is the +// length of each track, which happen to be the last 3 bytes of the descriptor. +typedef struct TrackRecord +{ + int8 unused[5]; + + int8 min; + int8 sec; + int8 frame; +}; + + template void InitCheck(InitCheckable *item) @@ -57,8 +73,8 @@ CDDBQuery::CDDBQuery(const char *server, int32 port, bool log) fServerName(server), fPort(port), fConnected(false), - fDiscID(-1), - fState(kInitial) + fState(kInitial), + fDiscID(-1) { } @@ -90,40 +106,40 @@ CDDBQuery::SetToCD(const char *path) if(result != B_OK) return; - + // Calculate the disc's CDDB ID if (fState == kInitial) { - GetDiscID(&toc, fDiscID, fTrackCount, fDiscLength, fFrameOffsetString, fDiscIDStr); + fDiscID = GetDiscID(&toc); + fTrackCount = GetTrackCount(&toc); + + cdaudio_time time = GetDiscTime(&toc); + fDiscLength = (time.minutes * 60) + time.seconds; + fFrameOffsetString = OffsetsToString(&toc); + GetTrackTimes(&toc, fTrackTimes); } else { - int32 tmpDiscID; - int32 tmpDiscLength; - int32 tmpNumTracks; - BString tmpFrameOffsetString; - BString tmpDiscIDStr; + int32 discID = GetDiscID(&toc); - GetDiscID(&toc, tmpDiscID, tmpNumTracks, tmpDiscLength, tmpFrameOffsetString, - tmpDiscIDStr); - - if (fDiscID == tmpDiscID && fDiscLength == tmpDiscLength && fTrackCount == tmpNumTracks - && tmpFrameOffsetString == fFrameOffsetString) + if (fDiscID == discID); return; - - fDiscID = tmpDiscID; - fDiscLength = tmpDiscLength; - fTrackCount = tmpNumTracks; - fFrameOffsetString = tmpFrameOffsetString; - fDiscIDStr = tmpDiscIDStr; + + fTrackCount = GetTrackCount(&toc); + + cdaudio_time time = GetDiscTime(&toc); + fDiscLength = (time.minutes * 60) + time.seconds; + fFrameOffsetString = OffsetsToString(&toc); + GetTrackTimes(&toc, fTrackTimes); } result = B_OK; fState = kReading; - fThread = spawn_thread(&CDDBQuery::LookupBinder, "CDDBLookup", B_NORMAL_PRIORITY, this); + fThread = spawn_thread(&CDDBQuery::QueryThread, "CDDBLookup", B_NORMAL_PRIORITY, this); if (fThread >= 0) resume_thread(fThread); - else { + else + { fState = kError; result = fThread; } @@ -134,65 +150,6 @@ DoNothing(int) { } -int32 -CDDBQuery::LookupBinder(void *castToThis) -{ - CDDBQuery *query = (CDDBQuery *)castToThis; - - BFile file; - entry_ref ref; - bool newFile = false; - - try - { - signal(kTerminatingSignal, DoNothing); - - if (!query->FindOrCreateContentFileForDisk(&file, &ref, query->fDiscID)) - { - // new content file, read it in from the server - Connector connection(query); - query->ReadFromServer(&file); - file.Seek(0, SEEK_SET); - newFile = true; - } - query->ParseResult(&file); - - query->fState = kDone; - query->fThread = -1; - query->fResult = B_OK; - - if (newFile) - { - BString newTitle(query->fTitle); - int32 length = newTitle.Length(); - for (int32 index = 0; index < length; index++) - { - if (newTitle[index] == '/' || newTitle[index] == ':') - newTitle[index] = '-'; - } - - file.Sync(); - BEntry entry(&ref); - entry.Rename(newTitle.String(), false); - } - - } - catch (status_t error) - { - query->fState = kError; - query->fResult = error; - - // the cached file is messed up, remove it - BEntry entry(&ref); - BPath path; - entry.GetPath(&path); - printf("removing bad CD content file %s\n", path.Path()); - entry.Remove(); - } - return 0; -} - - const char * CDDBQuery::GetToken(const char *stream, BString &result) { @@ -283,42 +240,13 @@ cddb_sum(int n) return ret; } -struct ConvertedToc -{ - int32 min; - int32 sec; - int32 frame; -}; - int32 CDDBQuery::GetDiscID(const scsi_toc *toc) { - int32 tmpDiscID; - int32 tmpDiscLength; - int32 tmpNumTracks; - BString tmpFrameOffsetString; - BString tmpDiscIDStr; - - GetDiscID(toc, tmpDiscID, tmpNumTracks, tmpDiscLength, tmpFrameOffsetString, - tmpDiscIDStr); - - return tmpDiscID; -} + // The SCSI TOC data has a 4-byte header and then each track record starts + TrackRecord *tocData = (TrackRecord*)&(toc->toc_data[4]); -void -CDDBQuery::GetDiscID(const scsi_toc *toc, int32 &id, int32 &numTracks, - int32 &length, BString &frameOffsetsString, BString &discIDString) -{ - ConvertedToc tocData[100]; - - // figure out the disc ID - for (int index = 0; index < 100; index++) - { - tocData[index].min = toc->toc_data[9 + 8*index]; - tocData[index].sec = toc->toc_data[10 + 8*index]; - tocData[index].frame = toc->toc_data[11 + 8*index]; - } - numTracks = toc->toc_data[3] - toc->toc_data[2] + 1; + int32 numTracks = toc->toc_data[3] - toc->toc_data[2] + 1; int32 sum1 = 0; int32 sum2 = 0; @@ -330,39 +258,87 @@ CDDBQuery::GetDiscID(const scsi_toc *toc, int32 &id, int32 &numTracks, sum2 += (tocData[index + 1].min * 60 + tocData[index + 1].sec) - (tocData[index].min * 60 + tocData[index].sec); } - id = ((sum1 % 0xff) << 24) + (sum2 << 8) + numTracks; - discIDString = ""; + int32 discID = ((sum1 % 0xff) << 24) + (sum2 << 8) + numTracks; - sprintf(discIDString.LockBuffer(10), "%08lx", id); - discIDString.UnlockBuffer(); + return discID; +} - // compute the total length of the CD. - length = tocData[numTracks].min * 60 + tocData[numTracks].sec; +BString +CDDBQuery::OffsetsToString(const scsi_toc *toc) +{ + TrackRecord *tocData = (TrackRecord*)&(toc->toc_data[4]); - for (int index = 0; index < numTracks; index++) - frameOffsetsString << tocData[index].min * 4500 + tocData[index].sec * 75 + int32 numTracks = toc->toc_data[3] - toc->toc_data[2] + 1; + + BString string; + for (int index = 0; index < numTracks; index++) + { + string << tocData[index].min * 4500 + tocData[index].sec * 75 + tocData[index].frame << ' '; + } + + return string; +} + + +int32 +CDDBQuery::GetTrackCount(const scsi_toc *toc) +{ + return toc->toc_data[3] - toc->toc_data[2] + 1; +} + +cdaudio_time +CDDBQuery::GetDiscTime(const scsi_toc *toc) +{ + int16 trackcount = toc->toc_data[3] - toc->toc_data[2] + 1; + TrackRecord *desc = (TrackRecord*)&(toc->toc_data[4]); + + cdaudio_time disc; + disc.minutes = desc[trackcount].min; + disc.seconds = desc[trackcount].sec; + + return disc; } void -CDDBQuery::ReadFromServer(BDataIO *stream) +CDDBQuery::GetTrackTimes(const scsi_toc *toc, vector ×) { - // Format the query + TrackRecord *tocData = (TrackRecord*)&(toc->toc_data[4]); + int16 trackCount = toc->toc_data[3] - toc->toc_data[2] + 1; + + for (int index = 0; index < trackCount; index++) + { + cdaudio_time cdtime; + cdtime.minutes = tocData[index].min; + cdtime.seconds = tocData[index].sec; + times.push_back(cdtime); + } +} + +void +CDDBQuery::ReadFromServer(BString &data) +{ + // This function queries the given CDDB server for the existence of the disc's data and + // saves the data to file once obtained. + + // Query for the existence of the disc in the database + char idString[10]; + sprintf(idString, "%08lx", fDiscID); + BString query; - query << "cddb query " << fDiscIDStr << ' ' << fTrackCount << ' ' + query << "cddb query " << idString << ' ' << fTrackCount << ' ' << fFrameOffsetString << ' ' << fDiscLength << '\n'; if (fLog) printf(">%s", query.String()); - // Send it off. ThrowIfNotSize( fSocket.Send(query.String(), query.Length()) ); BString tmp; ReadLine(tmp); BString category; - BString queryDiscID(fDiscIDStr); + BString queryDiscID(idString); if(tmp.FindFirst("200") != 0) { @@ -404,54 +380,22 @@ CDDBQuery::ReadFromServer(BDataIO *stream) if (!category.Length()) category = "misc"; + // Query for the disc's data - artist, album, tracks, etc. query = ""; query << "cddb read " << category << ' ' << queryDiscID << '\n' ; ThrowIfNotSize( fSocket.Send(query.String(), query.Length()) ); - - for (;;) + + while(true) { - BString tmp; ReadLine(tmp); - tmp += '\n'; - ThrowIfNotSize( stream->Write(tmp.String(), tmp.Length()) ); + if (tmp == "." || tmp == ".\n") break; - } - fState = kDone; -} - -void -CDDBQuery::ParseResult(BDataIO *source) -{ - fTitle = ""; - fTrackNames.clear(); - for (;;) - { - BString tmp; - ReadLine(source, tmp); - if (tmp == ".") - break; - - if (tmp == "") - throw (status_t)B_ERROR; - if (tmp.FindFirst("DTITLE=") == 0) - fTitle = tmp.String() + sizeof("DTITLE"); - else - if (tmp.FindFirst("TTITLE") == 0) - { - int32 afterIndex = tmp.FindFirst('='); - if (afterIndex > 0) - { - BString trackName(tmp.String() + afterIndex + 1); - fTrackNames.push_back(trackName); - } - } + data << tmp << '\n'; } - fState = kDone; } - void CDDBQuery::Connect() { @@ -497,35 +441,6 @@ CDDBQuery::ReadLine(BString &buffer) printf("<%s\n", buffer.String()); } -void -CDDBQuery::ReadLine(BDataIO *stream, BString &buffer) -{ - // this is super-lame, should use better buffering - // ToDo: - // redo using LockBuffer, growing the buffer by 2k and reading into it - - buffer = ""; - char ch; - for (;;) - { - ssize_t result = stream->Read(&ch, 1); - - // check for read error - if (result < 0) - throw (status_t)result; - - if (result == 0) - break; - - if (ch >= ' ') - buffer += ch; - - if (ch == '\n') - break; - } -} - - void CDDBQuery::IdentifySelf() { @@ -548,10 +463,14 @@ CDDBQuery::IdentifySelf() } bool -CDDBQuery::FindOrCreateContentFileForDisk(BFile *file, entry_ref *fileRef, int32 discID) +CDDBQuery::OpenContentFile(const int32 &discID) { + // Makes sure that the lookup has a valid file to work with for the CD content. + // Returns true if there is an existing file, false if a lookup is required. + + BFile file; BString predicate; - predicate << "cddb:discID == " << discID; + predicate << "CD:key == " << discID; entry_ref ref; BVolumeRoster roster; @@ -564,7 +483,7 @@ CDDBQuery::FindOrCreateContentFileForDisk(BFile *file, entry_ref *fileRef, int32 continue; // make sure the volume we are looking at is indexed right - fs_create_index(volume.Device(), "cddb:discID", B_INT32_TYPE, 0); + fs_create_index(volume.Device(), "CD:key", B_INT32_TYPE, 0); BQuery query; query.SetVolume(&volume); @@ -572,33 +491,210 @@ CDDBQuery::FindOrCreateContentFileForDisk(BFile *file, entry_ref *fileRef, int32 if (query.Fetch() != B_OK) continue; - while (query.GetNextRef(&ref) == B_OK) + if(query.GetNextRef(&ref) == B_OK) { - // we have one already, return - file->SetTo(&ref, O_RDONLY); - *fileRef = ref; + file.SetTo(&ref, B_READ_ONLY); + break; + } + } + + if(file.InitCheck() == B_NO_INIT) + return false; + + // Getting this far means that we have a file. Now we parse the data in it and assign it + // to the appropriate members of the object + attr_info info; + + if(file.GetAttrInfo("CD:tracks",&info)==B_OK && info.size > 0) + { + char trackdata[info.size+2]; + + if(file.ReadAttr("CD:tracks",B_STRING_TYPE,0,trackdata,info.size)>0) + { + trackdata[info.size] = 0; + BString tmp = GetLineFromString(trackdata); + char *index; + + fArtist = tmp; + fArtist.Truncate(fArtist.FindFirst(" - ")); + + fTitle = strchr(tmp.String(),'-') + 2; + + index = strchr(trackdata,'\n') + 1; + while(*index) + { + tmp = GetLineFromString(index); + + fTrackNames.push_back(tmp); + index = strchr(index,'\n') + 1; + } + return true; } } - - BPath path; - ThrowOnError( find_directory(B_USER_DIRECTORY, &path, true) ); - path.Append("cd"); - ThrowOnError( create_directory(path.Path(), 0755) ); - BDirectory dir(path.Path()); - BString name("CDContent"); - for (int32 index = 0; ;index++) - { - if (dir.CreateFile(name.String(), file, true) != B_FILE_EXISTS) - { - BEntry entry(&dir, name.String()); - entry.GetRef(fileRef); - file->WriteAttr("cddb:discID", B_INT32_TYPE, 0, &discID, sizeof(int32)); - break; - } - name = "CDContent"; - name << index; - } + // If we got this far, it means the file is corrupted, so nuke it and get a new one + BEntry entry(&ref); + entry.Remove(); return false; } + +int32 +CDDBQuery::QueryThread(void *owner) +{ + CDDBQuery *query = (CDDBQuery *)owner; + + try + { + signal(kTerminatingSignal, DoNothing); + + if(!query->OpenContentFile(query->fDiscID)) + { + // new content file, read it in from the server + Connector connection(query); + BString data; + query->ReadFromServer(data); + query->ParseData(data); + query->WriteFile(); + } + + query->fState = kDone; + query->fThread = -1; + query->fResult = B_OK; + } + catch (status_t error) + { + query->fState = kError; + query->fResult = error; + } + + return 0; +} + +void +CDDBQuery::WriteFile(void) +{ + BPath path; + if(find_directory(B_USER_DIRECTORY, &path, true)!=B_OK) + return; + + path.Append("cd"); + create_directory(path.Path(), 0755); + + BString filename(path.Path()); + filename << "/" << fArtist << " - " << fTitle; + + BFile file(filename.String(), B_READ_WRITE | B_CREATE_FILE | B_FAIL_IF_EXISTS); + if(file.InitCheck() != B_OK) + return; + + BString entry; + char timestring[10]; + + sprintf(timestring,"%.2ld:%.2ld",fDiscLength / 60, fDiscLength % 60); + + entry << fArtist << " - " << fTitle << "\t" << timestring << "\n"; + file.Write(entry.String(),entry.Length()); + + BString tracksattr(fArtist); + tracksattr << " - " << fTitle << "\n"; + + for(int32 i=0; i 0) + { + fTitle = fArtist = GetLineFromString(data.String() + sizeof("DTITLE") + pos); + + pos = fArtist.FindFirst(" / "); + if(pos > 0) + fArtist.Truncate(pos); + + fTitle = fTitle.String() + pos + sizeof(" / ") - 1; + } + + + fCategory = GetLineFromString(data.String() + 4); + pos = fCategory.FindFirst(" "); + if(pos > 0) + fCategory.Truncate(pos); + + + pos = data.FindFirst("Disc length: "); + if(pos > 0) + { + BString discLengthString = GetLineFromString(data.String() + pos); + pos = discLengthString.FindFirst(" seconds"); + if(pos > 0) + { + discLengthString.Truncate(pos); + + discLengthString = discLengthString.String() + sizeof("Disc length:"); + fDiscLength = atoi(discLengthString.String()); + } + else + fDiscLength = -1; + } + + pos = data.FindFirst("TTITLE0="); + if(pos > 0) + { + int32 trackCount=0; + BString searchString="TTITLE0="; + + while(pos > 0) + { + BString trackName = data.String() + pos + searchString.Length(); + trackName.Truncate(trackName.FindFirst("\n")); + fTrackNames.push_back(trackName); + + trackCount++; + searchString = "TTITLE"; + searchString << trackCount << "="; + pos = data.FindFirst(searchString.String(),pos); + } + fTrackCount = trackCount; + } +} + +BString +CDDBQuery::GetLineFromString(const char *string) +{ + if(!string) + return NULL; + + BString out(string); + + int32 linefeed = out.FindFirst("\n"); + + if(linefeed > 0) + out.Truncate(linefeed); + + return out; +} diff --git a/src/apps/cdplayer/CDDBSupport.h b/src/apps/cdplayer/CDDBSupport.h index 260bf49f53..1f0f7d6642 100644 --- a/src/apps/cdplayer/CDDBSupport.h +++ b/src/apps/cdplayer/CDDBSupport.h @@ -5,37 +5,40 @@ #include #include #include - +#include "CDAudioDevice.h" // based on Jukebox by Chip Paul class CDDBQuery { public: - CDDBQuery(const char *server, int32 port = 888, bool log = false); - void SetToSite(const char *server, int32 port); - void GetSites(bool (*)(const char *site, int port, const char *latitude, - const char *longitude, const char *description, - void *state), void *); + CDDBQuery(const char *server, int32 port = 888, + bool log = false); + void SetToSite(const char *server, int32 port); + void GetSites(bool (*)(const char *site, int port, + const char *latitude, const char *longitude, + const char *description, void *state), void *); - void SetToCD(const char *devicepath); - bool GetTitles(BString *title, vector *tracks, - bigtime_t timeout); + void SetToCD(const char *devicepath); + bool GetTitles(BString *title, vector *tracks, + bigtime_t timeout); - bool Ready() const { return fState == kDone; } - int32 CurrentDiscID() const { return fDiscID; } + bool Ready() const { return fState == kDone; } + int32 CurrentDiscID() const { return fDiscID; } - static int32 GetDiscID(const scsi_toc *); - -private: - static void GetDiscID(const scsi_toc *, int32 &id, int32 &numTracks, - int32 &length, BString &tmpFrameOffsetString, - BString &discIDString); - void Connect(); - void Disconnect(); - bool IsConnected() const; + static int32 GetDiscID(const scsi_toc *); + static int32 GetTrackCount(const scsi_toc *); + static cdaudio_time GetDiscTime(const scsi_toc *); + static void GetTrackTimes(const scsi_toc *, vector ×); + static BString OffsetsToString(const scsi_toc *); - static int32 LookupBinder(void *); +// TODO: make private again once finished with testing +//private: + void Connect(); + void Disconnect(); + bool IsConnected() const; + + static int32 QueryThread(void *); class Connector { @@ -70,39 +73,43 @@ private: kError }; - bool FindOrCreateContentFileForDisk(BFile *file, entry_ref *ref, - int32 discID); + bool OpenContentFile(const int32 &discID); - void ReadFromServer(BDataIO *); - void ParseResult(BDataIO *); + void ReadFromServer(BString &data); + void ParseData(const BString &data); + void WriteFile(void); + + BString GetLineFromString(const char *string); + void ReadLine(BString &); + void IdentifySelf(); - void ReadLine(BString &); - static void ReadLine(BDataIO *, BString &); - void IdentifySelf(); - - const char * GetToken(const char *, BString &); + const char * GetToken(const char *, BString &); - bool fLog; + bool fLog; - // connection description - BString fServerName; - BNetEndpoint fSocket; - int32 fPort; - bool fConnected; + // state data + BString fServerName; + BNetEndpoint fSocket; + int32 fPort; + bool fConnected; - // disc identification - int32 fDiscID; - int32 fDiscLength; - int32 fTrackCount; - BString fFrameOffsetString; - BString fDiscIDStr; + thread_id fThread; + State fState; + status_t fResult; - thread_id fThread; - State fState; - BString fTitle; - vector fTrackNames; - status_t fResult; + // disc information + BString fArtist; + BString fTitle; + BString fCategory; + int32 fDiscID; + int32 fDiscLength; + + vector fTrackNames; + int32 fTrackCount; + + BString fFrameOffsetString; + vector fTrackTimes; }; #endif diff --git a/src/apps/cdplayer/CDEngine.cpp b/src/apps/cdplayer/CDEngine.cpp index 0e3dafc88e..ec1b1c39ce 100644 --- a/src/apps/cdplayer/CDEngine.cpp +++ b/src/apps/cdplayer/CDEngine.cpp @@ -58,20 +58,28 @@ bool PlayState::UpdateState(void) { CDState state = sCDDevice.GetState(); - if(state == kStopped && fEngine->GetState() == kPlaying) + if(state == kStopped) { - // this means we have come to the end of a song, but probably not - // the last song in the playlist - int16 next = sPlayList.GetNextTrack(); - if(next > 0) + if(fEngine->GetState() == kPlaying) { - sCDDevice.Play(next); + // this means we have come to the end of a song, but probably not + // the last song in the playlist + int16 next = sPlayList.GetNextTrack(); + if(next > 0) + { + sCDDevice.Play(next); + return CurrentState(kPlaying); + } + } + else + { + sPlayList.SetTrackCount(sCDDevice.CountTracks()); + sPlayList.SetStartingTrack(sCDDevice.GetTrack()); return CurrentState(kPlaying); } } - -/* if(state == kPlaying && fEngine->GetState() == kStopped) + if(state == kPlaying && fEngine->GetState() == kStopped) { // This happens when the CD drive is started by R5's player // or this app is started while the drive is playing. We should @@ -81,7 +89,7 @@ PlayState::UpdateState(void) sPlayList.SetStartingTrack(sCDDevice.GetTrack()); return CurrentState(kPlaying); } -*/ + return CurrentState(state); } @@ -120,33 +128,35 @@ TrackState::UpdateState() // As a result, we want to make sure this hasn't happened. If it has, then we // need to update the playlist's position. - int16 cdTrack; + int16 cdTrack, count; if(sCDDevice.GetState() == kPlaying) { cdTrack = sCDDevice.GetTrack(); if(cdTrack != sPlayList.GetCurrentTrack()) sPlayList.SetCurrentTrack(cdTrack); - return CurrentState(cdTrack); + return CurrentState(cdTrack, trackCount); } // If we're not playing, just monitor the current track in the playlist cdTrack = sPlayList.GetCurrentTrack(); - return CurrentState(cdTrack); + count = sCDDevice.CountTracks(); + return CurrentState(cdTrack,count); } int32 TrackState::GetNumTracks() const { - return sCDDevice.CountTracks()+1; + return sCDDevice.CountTracks(); } bool -TrackState::CurrentState(int32 track) +TrackState::CurrentState(int32 track, int32 count) { - if (track != currentTrack) + if( (track != currentTrack) || (count != trackCount) ) { currentTrack = track; + trackCount = count; return true; } return false; diff --git a/src/apps/cdplayer/CDEngine.h b/src/apps/cdplayer/CDEngine.h index b8d8641049..7f10f35d43 100644 --- a/src/apps/cdplayer/CDEngine.h +++ b/src/apps/cdplayer/CDEngine.h @@ -72,8 +72,9 @@ public: private: bool UpdateState(); - bool CurrentState(int32); + bool CurrentState(int32,int32); int32 currentTrack; + int32 trackCount; }; // this watcher sends notices to observers that are interested