Skip to content

Commit

Permalink
MP3 : Better detection of misplaced MPEG headers [#284]
Browse files Browse the repository at this point in the history
  • Loading branch information
Zeugma440 committed Oct 1, 2024
1 parent a6450e9 commit e40a223
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 54 deletions.
8 changes: 6 additions & 2 deletions ATL.unit-test/IO/AudioData/AudioData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ public void Audio_MP3()
testGenericAudio("MP3/headerPatternIsNotHeader.mp3", 139, 192, -1, 44100, false, CF_LOSSY, JOINT_STEREO, "MPEG Audio (Layer III)", 1252, 3340);
// Malpositioned header 2
testGenericAudio("MP3/truncated_frame.mp3", 498, 320, -1, 48000, false, CF_LOSSY, STEREO, "MPEG Audio (Layer III)", 954, 19908);
// Fake header + garbage before actual header
testGenericAudio("MP3/garbage_before_header.mp3", 6142, 64, -1, 24000, false, CF_LOSSY, JOINT_STEREO, "MPEG Audio (Layer III)", 141, 49139);
// Contradictory frames
testGenericAudio("MP3/different_bitrates_modes.mp3", 6439, 128, -1, 44100, false, CF_LOSSY, JOINT_STEREO, "MPEG Audio (Layer III)", 45, 103025);

Expand All @@ -118,9 +120,11 @@ public void Audio_MP3()
// VBR
testGenericAudio("MP3/01 - Title Screen.mp3", 3866, 129, -1, 44100, true, CF_LOSSY, JOINT_STEREO, "MPEG Audio (Layer III)", 2048, 62346);
// Malpositioned header
testGenericAudio("MP3/headerPatternIsNotHeader.mp3", 156, 192, -1, 44100, false, CF_LOSSY, JOINT_STEREO, "MPEG Audio (Layer III)", 1252, 3756);
testGenericAudio("MP3/headerPatternIsNotHeader.mp3", 139, 192, -1, 44100, false, CF_LOSSY, JOINT_STEREO, "MPEG Audio (Layer III)", 1252, 3340);
// Malpositioned header 2
testGenericAudio("MP3/truncated_frame.mp3", 504, 320, -1, 48000, false, CF_LOSSY, STEREO, "MPEG Audio (Layer III)", 954, 20160);
testGenericAudio("MP3/truncated_frame.mp3", 498, 320, -1, 48000, false, CF_LOSSY, STEREO, "MPEG Audio (Layer III)", 954, 19908);
// Fake header + garbage before actual header
testGenericAudio("MP3/garbage_before_header.mp3", 6142, 64, -1, 24000, false, CF_LOSSY, JOINT_STEREO, "MPEG Audio (Layer III)", 141, 49139);
// Contradictory frames
testGenericAudio("MP3/different_bitrates_modes.mp3", 6439, 128, -1, 44100, false, CF_LOSSY, JOINT_STEREO, "MPEG Audio (Layer III)", 45, 103025);
}
Expand Down
Binary file not shown.
98 changes: 47 additions & 51 deletions ATL/AudioData/IO/MPEGaudio.cs
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ public int Size
{
if (MPEG_LAYER_I == LayerID)
{
m_size = (int)Math.Floor((Coefficient * BitRate * 1000.0 / SampleRate) + Padding) * 4;
m_size = (int)((Math.Floor(Coefficient * BitRate * 1000.0 / SampleRate) + Padding) * 4);
}
else // Layers II and III
{
Expand Down Expand Up @@ -521,74 +521,70 @@ private static FrameHeader findFirstFrame(Stream source, ref VBRData oVBR, SizeI
* The most solid way to deal with all of them is to "scan" the file until proper MP3 header is found.
* This method may not the be fastest, but ensures audio data is actually detected, whatever garbage lies before
*/

if (!result.Found)
if (!result.Found && (buffer[0] == buffer[1]) && (buffer[1] == buffer[2]) && (buffer[2] == buffer[3]))
{
// "Quick win" for files starting with padding bytes
// 4 identical bytes => file starts with padding bytes => Skip padding
if ((buffer[0] == buffer[1]) && (buffer[1] == buffer[2]) && (buffer[2] == buffer[3]))
{
long paddingEndOffset = StreamUtils.TraversePadding(source);
source.Seek(paddingEndOffset, SeekOrigin.Begin);
long paddingEndOffset = StreamUtils.TraversePadding(source);
source.Seek(paddingEndOffset, SeekOrigin.Begin);

// If padding uses 0xFF bytes, take one step back in case header lies there
if (0xFF == buffer[0]) source.Seek(-1, SeekOrigin.Current);
// If padding uses 0xFF bytes, take one step back in case header lies there
if (0xFF == buffer[0]) source.Seek(-1, SeekOrigin.Current);

if (source.Read(buffer, 0, 4) < 4) return result;
result.Found = IsValidFrameHeader(buffer);
}
if (source.Read(buffer, 0, 4) < 4) return result;
result.Found = IsValidFrameHeader(buffer);
}

// Blindly look for the first frame header
if (!result.Found)
long id3v2Size = (null == sizeInfo) ? 0 : sizeInfo.ID3v2Size;
long limit = id3v2Size + (long)Math.Round((source.Length - id3v2Size) * 0.3);
int iterations = 0;
while (source.Position < limit)
{
// If above method fails, blindly look for the first frame header
// Look for the beginning of the header (2nd byte is variable, so it cannot be searched using a static value)
while (!result.Found && source.Position < limit)
{
source.Seek(-4, SeekOrigin.Current);
long id3v2Size = (null == sizeInfo) ? 0 : sizeInfo.ID3v2Size;
long limit = id3v2Size + (long)Math.Round((source.Length - id3v2Size) * 0.3);
while (0xFF != source.ReadByte() && source.Position < limit) { /* just advance the stream */ }

// Look for the beginning of the header (2nd byte is variable, so it cannot be searched using a static value)
while (!result.Found && source.Position < limit)
{
while (0xFF != source.ReadByte() && source.Position < limit) { /* just advance the stream */ }

source.Seek(-1, SeekOrigin.Current);
if (source.Read(buffer, 0, 4) < 4) break;
result.Found = IsValidFrameHeader(buffer);
source.Seek(-1, SeekOrigin.Current);
if (source.Read(buffer, 0, 4) < 4) break;
result.Found = IsValidFrameHeader(buffer);
}

// Valid header candidate found
// => let's see if it is a legit MP3 header by using its Size descriptor to find the next header
// which in turn should at least share the same version ID and layer
if (result.Found)
{
result.LoadFromByteArray(buffer);
result.Offset = source.Position - 4;
// Valid header candidate found
// => let's see if it is a legit MP3 header by using its Size descriptor to find the next header
// which in turn should at least share the same version ID and layer
if (result.Found)
{
result.LoadFromByteArray(buffer);
result.Offset = source.Position - 4;

FrameHeader nextFrame = findNextFrame(source, result, buffer);
result.Found = nextFrame.Found;
// One single frame to analyze
if (0 == iterations && source.Length <= result.Offset + result.Size) break;

if (result.Found && result.LayerID == nextFrame.LayerID && result.VersionID == nextFrame.VersionID)
{
source.Seek(result.Offset + 4, SeekOrigin.Begin); // Success ! Go back to header candidate position
break;
}
FrameHeader nextFrame = findNextFrame(source, result, buffer);
result.Found = nextFrame.Found;

// Restart looking for a candidate
source.Seek(result.Offset + 1, SeekOrigin.Begin);
result.Found = false;
}
else
{
if (source.Position < limit) source.Seek(-3, SeekOrigin.Current);
result.Found = false;
}
if (result.Found && result.LayerID == nextFrame.LayerID && result.VersionID == nextFrame.VersionID)
{
source.Seek(result.Offset + 4, SeekOrigin.Begin); // Success ! Go back to header candidate position
break;
}

// Restart looking for a candidate
source.Seek(result.Offset + 1, SeekOrigin.Begin);
result.Found = false;
}
else
{
if (source.Position < limit) source.Seek(-3, SeekOrigin.Current);
result.Found = false;
}
iterations++;
}

if (result.Found && oVBR != null)
{
result.LoadFromByteArray(buffer);
result.Offset = source.Position - 4;

// Look for VBR signature
oVBR = findVBR(source, result.Offset + result.VBRDeviation);

Expand Down
2 changes: 1 addition & 1 deletion ATL/Utils/BufferedBinaryReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ public override long Seek(long offset, SeekOrigin origin)
}
else // Jump outside buffer: move the whole buffer at the beginning of the zone to read
{
streamPosition = bufferOffset + cursorPosition + delta;
streamPosition = Math.Min(bufferOffset + cursorPosition + delta, streamSize);
stream.Position = streamPosition;
fillBuffer();
}
Expand Down

0 comments on commit e40a223

Please sign in to comment.