private void TagCheckTextIsSame(IList <FlacFormat> flacs, string key) { if (FlacFormat.IsFlacMultiTagAllSame(flacs, key) == false) { IssueModel.Add(key + " tags are inconsistent.", Severity.Warning, IssueTags.BadTag | IssueTags.StrictErr); } }
public void CheckFlacRipTags(IList <FlacFormat> flacs) { int prevTrackNum = -1; foreach (FlacFormat flac in flacs) { var trackTag = flac.GetTagValue("TRACKNUMBER"); var integerRegex = new Regex("^([0-9]+)", RegexOptions.Compiled); MatchCollection reMatches = integerRegex.Matches(trackTag); string trackTagCapture = reMatches.Count == 1 ? reMatches[0].Groups[1].ToString() : trackTag; if (int.TryParse(trackTagCapture, out int trackNum)) { if (prevTrackNum >= 0 && trackNum != prevTrackNum + 1) { IssueModel.Add($"Gap in TRACKNUMBER tags near '{trackTag}'.", Severity.Error, IssueTags.BadTag); } prevTrackNum = trackNum; } } TagCheckTextIsSame(flacs, "TRACKTOTAL"); TagCheckTextIsSame(flacs, "DISCNUMBER"); TagCheckTextIsSame(flacs, "DISCTOTAL"); TagCheckTextIsSame(flacs, "DATE"); TagCheckTextIsSame(flacs, "RELEASE DATE"); bool?isSameAA = FlacFormat.IsFlacTagsAllSame(flacs, "ALBUMARTIST"); if (isSameAA == false) { IssueModel.Add("ALBUMARTIST tags are inconsistent.", Severity.Warning, IssueTags.BadTag | IssueTags.StrictErr); } else if (isSameAA == null) { bool?isSameArtist = FlacFormat.IsFlacTagsAllSame(flacs, "ARTIST"); if (isSameArtist == false) { IssueModel.Add("Inconsistent ARTIST tag or missing ALBUMARTIST tag.", Severity.Warning, IssueTags.BadTag); } else if (isSameArtist == null) { IssueModel.Add("ARTIST is missing.", Severity.Warning, IssueTags.BadTag); } } TagCheckTextIsSame(flacs, "ALBUM"); TagCheckTextIsSame(flacs, "ORGANIZATION"); TagCheckTextIsSame(flacs, "BARCODE"); TagCheckTextIsSame(flacs, "CATALOGNUMBER"); TagCheckTextIsSame(flacs, "ALBUMARTISTSORTORDER"); }
public Model(Stream stream, byte[] hdr, string path) { base._data = Data = new FlacFormat(this, stream, path); Data.MetadataBlockStreamInfoSize = ConvertTo.FromBig24ToInt32(hdr, 5); if (Data.MetadataBlockStreamInfoSize < 34) { IssueModel.Add($"Bad metablock size of {Data.MetadataBlockStreamInfoSize}", Severity.Fatal); return; } var bb = new byte[Data.MetadataBlockStreamInfoSize]; Data.ValidSize = 8; Data.fbs.Position = Data.ValidSize; var got = Data.fbs.Read(bb, 0, Data.MetadataBlockStreamInfoSize); if (got != Data.MetadataBlockStreamInfoSize) { IssueModel.Add("File truncated", Severity.Fatal); return; } Data.MinBlockSize = ConvertTo.FromBig16ToInt32(bb, 0); Data.MinBlockSize = ConvertTo.FromBig16ToInt32(bb, 2); Data.MinFrameSize = ConvertTo.FromBig24ToInt32(bb, 4); Data.MaxFrameSize = ConvertTo.FromBig24ToInt32(bb, 7); Data.MetaSampleRate = bb[10] << 12 | bb[11] << 4 | bb[12] >> 4; Data.ChannelCount = ((bb[12] & 0x0E) >> 1) + 1; Data.BitsPerSample = (((bb[12] & 1) << 4) | (bb[13] >> 4)) + 1; Data.TotalSamples = ((bb[13] & 0x0F) << 32) | bb[14] << 24 | bb[15] << 16 | bb[16] << 8 | bb[17]; Data.storedAudioDataMD5 = new byte[16]; Array.Copy(bb, 18, Data.storedAudioDataMD5, 0, 16); Data.ValidSize += Data.MetadataBlockStreamInfoSize; for (;;) { bb = new byte[12]; try { Data.fbs.Position = Data.ValidSize; } catch (EndOfStreamException) { IssueModel.Add("File truncated near meta data", Severity.Fatal); return; } Data.fbs.Position = Data.ValidSize; got = Data.fbs.Read(bb, 0, 4); if (got != 4) { IssueModel.Add("File truncated near meta data", Severity.Fatal); return; } if (bb[0] == 0xFF) { break; } int blockSize = ConvertTo.FromBig24ToInt32(bb, 1); Data.ValidSize += 4; switch ((FlacBlockType)(bb[0] & 0x7F)) { case FlacBlockType.Padding: Data.Blocks.AddPad((int)Data.ValidSize, blockSize); break; case FlacBlockType.Application: got = Data.fbs.Read(bb, 0, 4); if (got != 4) { IssueModel.Add("File truncated near tags", Severity.Fatal); return; } int appId = ConvertTo.FromBig32ToInt32(bb, 0); Data.Blocks.AddApp((int)Data.ValidSize, blockSize, appId); break; case FlacBlockType.SeekTable: var st = new byte[blockSize]; got = Data.fbs.Read(st, 0, blockSize); if (got != blockSize) { IssueModel.Add("File truncated near seek table", Severity.Fatal); return; } Data.Blocks.AddSeekTable((int)Data.ValidSize, blockSize, st); break; case FlacBlockType.Tags: bb = new byte[blockSize]; Data.fbs.Position = Data.ValidSize; got = Data.fbs.Read(bb, 0, blockSize); if (got != blockSize) { IssueModel.Add("File truncated near tags", Severity.Fatal); return; } if (Data.Blocks.Tags != null) { IssueModel.Add("Contains multiple tag blocks", Severity.Error); } else { Data.Blocks.AddTags((int)Data.ValidSize, blockSize, bb); } break; case FlacBlockType.CueSheet: var sb = new byte[284]; got = Data.fbs.Read(sb, 0, 284); if (got != 284) { IssueModel.Add("File truncated near cuesheet", Severity.Fatal); return; } var isCD = (sb[24] & 0x80) != 0; int trackCount = sb[283]; Data.Blocks.AddCuesheet((int)Data.ValidSize, blockSize, isCD, trackCount); break; case FlacBlockType.Picture: var pb = new byte[blockSize]; got = Data.fbs.Read(pb, 0, blockSize); if (got != blockSize) { IssueModel.Add("File truncated near picture", Severity.Fatal); return; } var picType = (PicType)ConvertTo.FromBig32ToInt32(pb, 0); var mimeLen = ConvertTo.FromBig32ToInt32(pb, 4); var mime = Encoding.UTF8.GetString(pb, 8, mimeLen); var descLen = ConvertTo.FromBig32ToInt32(pb, mimeLen + 8); var desc = Encoding.UTF8.GetString(pb, mimeLen + 12, descLen); var width = ConvertTo.FromBig32ToInt32(pb, mimeLen + descLen + 12); var height = ConvertTo.FromBig32ToInt32(pb, mimeLen + descLen + 16); Data.Blocks.AddPic((int)Data.ValidSize, blockSize, picType, width, height); break; case FlacBlockType.Invalid: IssueModel.Add("Encountered invalid block type", Severity.Fatal); return; default: IssueModel.Add($"Encountered reserved block type '{bb[0]}'", Severity.Warning); break; } Data.ValidSize += blockSize; } try { Data.fbs.Position = Data.ValidSize; } catch (EndOfStreamException) { IssueModel.Add("File truncated near frame header", Severity.Fatal); return; } got = Data.fbs.Read(bb, 0, 4); if (got != 4) { IssueModel.Add("File truncated", Severity.Fatal); return; } // Detect frame header sync code if (bb[0] != 0xFF || (bb[1] & 0xFC) != 0xF8) { IssueModel.Add("Audio data not found", Severity.Fatal); return; } Data.mediaPosition = Data.ValidSize; Data.SampleOrFrameNumber = Data.fbs.ReadWobbly(out byte[] wtfBuf); if (Data.SampleOrFrameNumber < 0) { IssueModel.Add("File truncated or badly formed sample/frame number.", Severity.Fatal); return; } Array.Copy(wtfBuf, 0, bb, 4, wtfBuf.Length); int bPos = 4 + wtfBuf.Length; Data.RawBlockingStrategy = bb[1] & 1; Data.RawBlockSize = bb[2] >> 4; if (Data.RawBlockSize == 0) { Data.BlockSize = 0; } else if (Data.RawBlockSize == 1) { Data.BlockSize = 192; } else if (Data.RawBlockSize >= 2 && Data.RawBlockSize <= 5) { Data.BlockSize = 576 * (1 << (Data.RawBlockSize - 2)); } else if (Data.RawBlockSize == 6) { got = Data.fbs.Read(bb, bPos, 1); Data.BlockSize = bb[bPos] + 1; bPos += 1; } else if (Data.RawBlockSize == 7) { got = Data.fbs.Read(bb, bPos, 2); Data.BlockSize = (bb[bPos] << 8) + bb[bPos + 1] + 1; bPos += 2; } else { Data.BlockSize = 256 * (1 << (Data.RawBlockSize - 8)); } Data.RawSampleRate = bb[2] & 0xF; if (Data.RawSampleRate == 0xC) { got = Data.fbs.Read(bb, bPos, 1); Data.SampleRateText = bb[bPos] + "kHz"; bPos += 1; } else if (Data.RawSampleRate == 0xD || Data.RawSampleRate == 0xE) { got = Data.fbs.Read(bb, bPos, 2); Data.SampleRateText = (bb[bPos] << 8).ToString() + bb[bPos + 1] + (Data.RawSampleRate == 0xD? " Hz" : " kHz"); bPos += 2; } else if (Data.RawSampleRate == 0) { Data.SampleRateText = Data.MetaSampleRate.ToString() + " Hz"; } else { Data.SampleRateText = SampleRateMap[Data.RawSampleRate]; } Data.RawChannelAssignment = bb[3] >> 4; Data.RawSampleSize = (bb[3] & 0xE) >> 1; if (Data.RawSampleSize == 0) { Data.SampleSizeText = Data.BitsPerSample.ToString() + " bits"; } else { Data.SampleSizeText = SampleSizeMap[Data.RawSampleSize]; } Data.aHdr = new byte[bPos]; Array.Copy(bb, Data.aHdr, bPos); Data.ValidSize += bPos; Data.fbs.Position = Data.ValidSize; int octet = Data.fbs.ReadByte(); if (octet < 0) { IssueModel.Add("File truncated near CRC-8", Severity.Fatal); return; } Data.StoredAudioHeaderCRC8 = (Byte)octet; try { Data.fbs.Position = Data.mediaPosition; } catch (EndOfStreamException) { IssueModel.Add("File truncated near audio data", Severity.Fatal); return; } try { Data.fbs.Position = Data.FileSize - 2; } catch (EndOfStreamException) { IssueModel.Add("File truncated looking for end", Severity.Fatal); return; } bb = new byte[2]; if (Data.fbs.Read(bb, 0, 2) != 2) { IssueModel.Add("Read failed on audio block CRC-16", Severity.Fatal); return; } Data.StoredAudioBlockCRC16 = (UInt16)(bb[0] << 8 | bb[1]); Data.MediaCount = Data.FileSize - Data.mediaPosition; GetDiagnostics(); }
public void ValidateRip(IList <FlacFormat> flacs, bool checkTags) { Data.IsLosslessRip = true; Severity baddest = Severity.NoIssue; PerformValidations(); if (baddest < Data.Issues.MaxSeverity) { baddest = Data.Issues.MaxSeverity; } if (baddest >= Severity.Error) { Data.RpIssue = IssueModel.Add($"{Data.Subname} to FLAC rip check failed.", baddest, IssueTags.Failure); } else if (baddest >= Severity.Warning) { Data.RpIssue = IssueModel.Add($"{Data.Subname} to FLAC rip check successful with warnings.", baddest, IssueTags.Success); } else { Data.RpIssue = IssueModel.Add($"{Data.Subname} to FLAC rip check successful!", Severity.Advisory, IssueTags.Success); } return; void PerformValidations() { if (flacs.Count != TracksModel.GetCount() || flacs.Count == 0) { Data.TkIssue = IssueModel.Add($"Folder contains {flacs.Count} .flac files, {Data.Subname} log contains {TracksModel.GetCount()} tracks."); baddest = Severity.Error; return; } int errCount = 0, warnCount = 0; string errs = string.Empty, warns = string.Empty; for (int fx = 0; fx < flacs.Count; ++fx) { FlacFormat flac = flacs[fx]; if (baddest < flac.Issues.MaxSeverity) { baddest = flac.Issues.MaxSeverity; } var level = flac.Issues.MaxLevelWhereAny(IssueTags.BadTag); if (level >= Severity.Error) { if (warnCount < 2) { errs = warnCount == 1 ? "s" + errs + ", " : errs + " "; ++errCount; } else { errs += ", "; } errs = errs + flac.GetTagValue("TRACKNUMBER"); } else if (level == Severity.Warning) { if (warnCount < 2) { warns = warnCount == 1 ? "s" + warns + ", " : warns + " "; ++warnCount; } else { warns += ", "; } warns = warns + flac.GetTagValue("TRACKNUMBER"); } } if (warns.Length > 0) { IssueModel.Add($"Tag issues on track{warns}.", Severity.Warning); } if (errs.Length > 0) { IssueModel.Add($"Tag issues on track{errs}.", Severity.Error); } baddest = flacs.Max(tk => tk.Issues.MaxSeverity); if (flacs.Count != flacs.Where(tk => tk.ActualAudioBlockCRC16 != null).Count()) { IssueModel.Add("FLAC intrinsic CRC checks not performed.", Severity.Warning, IssueTags.StrictErr); } TracksModel.MatchFlacs(flacs); if (TracksModel.Data.RipMismatchCount != 0) { Data.MhIssue = IssueModel.Add("Log CRC-32 match to FLAC PCM CRC-32 failed.", Severity.Error, IssueTags.Failure); } else { Data.MhIssue = IssueModel.Add("Log/FLAC CRC-32s match for all tracks.", Severity.Trivia); if (checkTags) { CheckFlacRipTags(flacs); } } } }