cd_image_cue.cpp (12039B)
1 // SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com> 2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) 3 4 #include "cd_image.h" 5 #include "cd_subchannel_replacement.h" 6 #include "cue_parser.h" 7 8 #include "common/assert.h" 9 #include "common/error.h" 10 #include "common/file_system.h" 11 #include "common/log.h" 12 #include "common/path.h" 13 14 #include "fmt/format.h" 15 16 #include <algorithm> 17 #include <cinttypes> 18 #include <map> 19 20 Log_SetChannel(CDImageCueSheet); 21 22 namespace { 23 24 class CDImageCueSheet : public CDImage 25 { 26 public: 27 CDImageCueSheet(); 28 ~CDImageCueSheet() override; 29 30 bool OpenAndParse(const char* filename, Error* error); 31 32 bool ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index) override; 33 bool HasNonStandardSubchannel() const override; 34 s64 GetSizeOnDisk() const override; 35 36 protected: 37 bool ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) override; 38 39 private: 40 struct TrackFile 41 { 42 std::string filename; 43 std::FILE* file; 44 u64 file_position; 45 }; 46 47 std::vector<TrackFile> m_files; 48 CDSubChannelReplacement m_sbi; 49 }; 50 51 } // namespace 52 53 CDImageCueSheet::CDImageCueSheet() = default; 54 55 CDImageCueSheet::~CDImageCueSheet() 56 { 57 std::for_each(m_files.begin(), m_files.end(), [](TrackFile& t) { std::fclose(t.file); }); 58 } 59 60 bool CDImageCueSheet::OpenAndParse(const char* filename, Error* error) 61 { 62 std::FILE* fp = FileSystem::OpenSharedCFile(filename, "rb", FileSystem::FileShareMode::DenyWrite, error); 63 if (!fp) 64 { 65 Error::AddPrefixFmt(error, "Failed to open cuesheet '{}': ", Path::GetFileName(filename)); 66 return false; 67 } 68 69 CueParser::File parser; 70 if (!parser.Parse(fp, error)) 71 { 72 std::fclose(fp); 73 return false; 74 } 75 76 std::fclose(fp); 77 78 m_filename = filename; 79 80 u32 disc_lba = 0; 81 82 // for each track.. 83 for (u32 track_num = 1; track_num <= CueParser::MAX_TRACK_NUMBER; track_num++) 84 { 85 const CueParser::Track* track = parser.GetTrack(track_num); 86 if (!track) 87 break; 88 89 const std::string& track_filename = track->file; 90 LBA track_start = track->start.ToLBA(); 91 92 u32 track_file_index = 0; 93 for (; track_file_index < m_files.size(); track_file_index++) 94 { 95 const TrackFile& t = m_files[track_file_index]; 96 if (t.filename == track_filename) 97 break; 98 } 99 if (track_file_index == m_files.size()) 100 { 101 const std::string track_full_filename( 102 !Path::IsAbsolute(track_filename) ? Path::BuildRelativePath(m_filename, track_filename) : track_filename); 103 Error track_error; 104 std::FILE* track_fp = FileSystem::OpenCFile(track_full_filename.c_str(), "rb", &track_error); 105 if (!track_fp && track_file_index == 0) 106 { 107 // many users have bad cuesheets, or they're renamed the files without updating the cuesheet. 108 // so, try searching for a bin with the same name as the cue, but only for the first referenced file. 109 const std::string alternative_filename(Path::ReplaceExtension(filename, "bin")); 110 track_fp = FileSystem::OpenCFile(alternative_filename.c_str(), "rb"); 111 if (track_fp) 112 { 113 WARNING_LOG("Your cue sheet references an invalid file '{}', but this was found at '{}' instead.", 114 track_filename, alternative_filename); 115 } 116 } 117 118 if (!track_fp) 119 { 120 ERROR_LOG("Failed to open track filename '{}' (from '{}' and '{}'): {}", track_full_filename, track_filename, 121 filename, track_error.GetDescription()); 122 Error::SetStringFmt(error, "Failed to open track filename '{}' (from '{}' and '{}'): {}", track_full_filename, 123 track_filename, Path::GetFileName(filename), track_error.GetDescription()); 124 return false; 125 } 126 127 m_files.push_back(TrackFile{track_filename, track_fp, 0}); 128 } 129 130 // data type determines the sector size 131 const TrackMode mode = track->mode; 132 const u32 track_sector_size = GetBytesPerSector(mode); 133 134 // precompute subchannel q flags for the whole track 135 SubChannelQ::Control control{}; 136 control.data = mode != TrackMode::Audio; 137 control.audio_preemphasis = track->HasFlag(CueParser::TrackFlag::PreEmphasis); 138 control.digital_copy_permitted = track->HasFlag(CueParser::TrackFlag::CopyPermitted); 139 control.four_channel_audio = track->HasFlag(CueParser::TrackFlag::FourChannelAudio); 140 141 // determine the length from the file 142 LBA track_length; 143 if (!track->length.has_value()) 144 { 145 FileSystem::FSeek64(m_files[track_file_index].file, 0, SEEK_END); 146 u64 file_size = static_cast<u64>(FileSystem::FTell64(m_files[track_file_index].file)); 147 FileSystem::FSeek64(m_files[track_file_index].file, 0, SEEK_SET); 148 149 file_size /= track_sector_size; 150 if (track_start >= file_size) 151 { 152 ERROR_LOG("Failed to open track {} in '{}': track start is out of range ({} vs {})", track_num, filename, 153 track_start, file_size); 154 Error::SetStringFmt(error, "Failed to open track {} in '{}': track start is out of range ({} vs {}))", 155 track_num, Path::GetFileName(filename), track_start, file_size); 156 return false; 157 } 158 159 track_length = static_cast<LBA>(file_size - track_start); 160 } 161 else 162 { 163 track_length = track->length.value().ToLBA(); 164 } 165 166 const Position* index0 = track->GetIndex(0); 167 LBA pregap_frames; 168 if (index0) 169 { 170 // index 1 is always present, so this is safe 171 pregap_frames = track->GetIndex(1)->ToLBA() - index0->ToLBA(); 172 173 // Pregap/index 0 is in the file, easy. 174 Index pregap_index = {}; 175 pregap_index.start_lba_on_disc = disc_lba; 176 pregap_index.start_lba_in_track = static_cast<LBA>(-static_cast<s32>(pregap_frames)); 177 pregap_index.length = pregap_frames; 178 pregap_index.track_number = track_num; 179 pregap_index.index_number = 0; 180 pregap_index.mode = mode; 181 pregap_index.submode = CDImage::SubchannelMode::None; 182 pregap_index.control.bits = control.bits; 183 pregap_index.is_pregap = true; 184 pregap_index.file_index = track_file_index; 185 pregap_index.file_offset = static_cast<u64>(static_cast<s64>(track_start - pregap_frames)) * track_sector_size; 186 pregap_index.file_sector_size = track_sector_size; 187 188 m_indices.push_back(pregap_index); 189 190 disc_lba += pregap_index.length; 191 } 192 else 193 { 194 // Two seconds pregap for track 1 is assumed if not specified. 195 // Some people have broken (older) dumps where a two second pregap was implicit but not specified in the cuesheet. 196 // The problem is we can't tell between a missing implicit two second pregap and a zero second pregap. Most of 197 // these seem to be a single bin file for all tracks. So if this is the case, we add the two seconds in if it's 198 // not specified. If this is an audio CD (likely when track 1 is not data), we don't add these pregaps, and rely 199 // on the cuesheet. If we did add them, it causes issues in some games (e.g. Dancing Stage featuring DREAMS COME 200 // TRUE). 201 const bool is_multi_track_bin = (track_num > 1 && track_file_index == m_indices[0].file_index); 202 const bool likely_audio_cd = (parser.GetTrack(1)->mode == TrackMode::Audio); 203 204 pregap_frames = track->zero_pregap.has_value() ? track->zero_pregap->ToLBA() : 0; 205 if ((track_num == 1 || is_multi_track_bin) && !track->zero_pregap.has_value() && 206 (track_num == 1 || !likely_audio_cd)) 207 { 208 pregap_frames = 2 * FRAMES_PER_SECOND; 209 } 210 211 // create the index for the pregap 212 if (pregap_frames > 0) 213 { 214 Index pregap_index = {}; 215 pregap_index.start_lba_on_disc = disc_lba; 216 pregap_index.start_lba_in_track = static_cast<LBA>(-static_cast<s32>(pregap_frames)); 217 pregap_index.length = pregap_frames; 218 pregap_index.track_number = track_num; 219 pregap_index.index_number = 0; 220 pregap_index.mode = mode; 221 pregap_index.submode = CDImage::SubchannelMode::None; 222 pregap_index.control.bits = control.bits; 223 pregap_index.is_pregap = true; 224 m_indices.push_back(pregap_index); 225 226 disc_lba += pregap_index.length; 227 } 228 } 229 230 // add the track itself 231 m_tracks.push_back(Track{track_num, disc_lba, static_cast<u32>(m_indices.size()), track_length + pregap_frames, 232 mode, SubchannelMode::None, control}); 233 234 // how many indices in this track? 235 Index last_index; 236 last_index.start_lba_on_disc = disc_lba; 237 last_index.start_lba_in_track = 0; 238 last_index.track_number = track_num; 239 last_index.index_number = 1; 240 last_index.file_index = track_file_index; 241 last_index.file_sector_size = track_sector_size; 242 last_index.file_offset = static_cast<u64>(track_start) * track_sector_size; 243 last_index.mode = mode; 244 last_index.submode = CDImage::SubchannelMode::None; 245 last_index.control.bits = control.bits; 246 last_index.is_pregap = false; 247 248 u32 last_index_offset = track_start; 249 for (u32 index_num = 1;; index_num++) 250 { 251 const Position* pos = track->GetIndex(index_num); 252 if (!pos) 253 break; 254 255 const u32 index_offset = pos->ToLBA(); 256 257 // add an index between the track indices 258 if (index_offset > last_index_offset) 259 { 260 last_index.length = index_offset - last_index_offset; 261 m_indices.push_back(last_index); 262 263 disc_lba += last_index.length; 264 last_index.start_lba_in_track += last_index.length; 265 last_index.start_lba_on_disc = disc_lba; 266 last_index.length = 0; 267 } 268 269 last_index.file_offset = index_offset * last_index.file_sector_size; 270 last_index.index_number = static_cast<u32>(index_num); 271 last_index_offset = index_offset; 272 } 273 274 // and the last index is added here 275 const u32 track_end_index = track_start + track_length; 276 DebugAssert(track_end_index >= last_index_offset); 277 if (track_end_index > last_index_offset) 278 { 279 last_index.length = track_end_index - last_index_offset; 280 m_indices.push_back(last_index); 281 282 disc_lba += last_index.length; 283 } 284 } 285 286 if (m_tracks.empty()) 287 { 288 ERROR_LOG("File '{}' contains no tracks", filename); 289 Error::SetStringFmt(error, "File '{}' contains no tracks", Path::GetFileName(filename)); 290 return false; 291 } 292 293 m_lba_count = disc_lba; 294 AddLeadOutIndex(); 295 296 m_sbi.LoadFromImagePath(filename); 297 298 return Seek(1, Position{0, 0, 0}); 299 } 300 301 bool CDImageCueSheet::ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index) 302 { 303 if (m_sbi.GetReplacementSubChannelQ(index.start_lba_on_disc + lba_in_index, subq)) 304 return true; 305 306 return CDImage::ReadSubChannelQ(subq, index, lba_in_index); 307 } 308 309 bool CDImageCueSheet::HasNonStandardSubchannel() const 310 { 311 return (m_sbi.GetReplacementSectorCount() > 0); 312 } 313 314 bool CDImageCueSheet::ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) 315 { 316 DebugAssert(index.file_index < m_files.size()); 317 318 TrackFile& tf = m_files[index.file_index]; 319 const u64 file_position = index.file_offset + (static_cast<u64>(lba_in_index) * index.file_sector_size); 320 if (tf.file_position != file_position) 321 { 322 if (std::fseek(tf.file, static_cast<long>(file_position), SEEK_SET) != 0) 323 return false; 324 325 tf.file_position = file_position; 326 } 327 328 if (std::fread(buffer, index.file_sector_size, 1, tf.file) != 1) 329 { 330 std::fseek(tf.file, static_cast<long>(tf.file_position), SEEK_SET); 331 return false; 332 } 333 334 tf.file_position += index.file_sector_size; 335 return true; 336 } 337 338 s64 CDImageCueSheet::GetSizeOnDisk() const 339 { 340 // Doesn't include the cue.. but they're tiny anyway, whatever. 341 u64 size = 0; 342 for (const TrackFile& tf : m_files) 343 size += FileSystem::FSize64(tf.file); 344 return size; 345 } 346 347 std::unique_ptr<CDImage> CDImage::OpenCueSheetImage(const char* filename, Error* error) 348 { 349 std::unique_ptr<CDImageCueSheet> image = std::make_unique<CDImageCueSheet>(); 350 if (!image->OpenAndParse(filename, error)) 351 return {}; 352 353 return image; 354 }