# This work is hereby released into the # Public Domain. To view a copy of the # public domain dedication, visit # # http://creativecommons.org/licenses/publicdomain/ # # or send a letter to # # Creative Commons, # 559 Nathan Abbott Way, # Stanford, California 94305, USA. ################################################################################ package mp3; ################################################################################ use constant DEBUG => 0; ## DEBUG prints all sorts of interesting garbage use constant DEBUGSEARCH => 0; ## Same as DEBUG, but for the header-searching part use constant DEBUGPREPARSE => 0; ## Prints junk about the pre-parse pass use constant READSIZE => 65536; ## How much junk to read at the same time (should probably stay at 64Ki) use POSIX qw(floor ceil); use strict; #use reed::Displayer; %mp3::Bitrates = ( '1' => { '1' => [qw( 0 32 64 96 128 160 192 224 256 288 320 352 384 416 448 0 )], '2' => [qw( 0 32 48 56 64 80 96 112 128 160 192 224 256 320 384 0 )], '3' => [qw( 0 32 40 48 56 64 80 96 112 128 160 192 224 256 320 0 )], }, '2' => { '1' => [qw( 0 32 48 56 64 80 96 112 128 144 160 176 192 224 256 0 )], '2' => [qw( 0 8 16 24 32 40 48 56 64 80 96 112 128 144 160 0 )], '3' => [qw( 0 8 16 24 32 40 48 56 64 80 96 112 128 144 160 0 )], }, '2.5' => { '1' => [qw( 0 32 48 56 64 80 96 112 128 144 160 176 192 224 256 0 )], '2' => [qw( 0 8 16 24 32 40 48 56 64 80 96 112 128 144 160 0 )], '3' => [qw( 0 8 16 24 32 40 48 56 64 80 96 112 128 144 160 0 )], } ); %mp3::Samplerates = ( '1' => [44100, 48000, 32000, 0], '2' => [22050, 24000, 16000, 0], '2.5' => [11025, 12000, 8000, 0], ); # 32 bytes for MPEG1 stereo # 17 bytes for MPEG1 mono, MPEG2 stereo # 9 bytes for MPEG2 mono %mp3::SideInfoSize = ( '1' => {'Stereo' => 32, 'JS' => 32, 'Dual' => 32, 'Mono' => 17}, '2' => {'Stereo' => 17, 'JS' => 17, 'Dual' => 17, 'Mono' => 9}, '2.5' => {'Stereo' => 17, 'JS' => 17, 'Dual' => 17, 'Mono' => 9}, ); %mp3::MaxBitReservoir = ( '1' => 511, '2' => 255, '2.5' => 255, ); %mp3::SamplesPerFrame = ( '1' => {'1' => 384, '2' => 1152, '3' => 1152}, '2' => {'1' => 384, '2' => 1152, '3' => 576}, '2.5' => {'1' => 384, '2' => 1152, '3' => 576}, ); @mp3::IDs = (2.5, 0, 2, 1); @mp3::Layers = (0, 3, 2, 1); @mp3::ChannelMode = ('Stereo', 'JS', 'Dual', 'Mono'); @mp3::MP12bandJoin = (4, 8, 12, 16); @mp3::Emphasis = ('None', '50/15 ms', 'INVALID', 'CCITT J.17'); @mp3::SourceFreq = (32000, 44100, 48000, 'More'); %mp3::SourceFreqInv = ( '32000' => 0, '44100' => 1, '48000' => 2, 'More' => 3, ); @mp3::CRClookup = ( # Stolen directly out of the LAME VbrTag.c file (I don't know how to do CRCs! Why am I doing this?!) 0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241, 0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440, 0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40, 0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841, 0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40, 0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41, 0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641, 0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040, 0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240, 0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441, 0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41, 0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840, 0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41, 0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40, 0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640, 0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041, 0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240, 0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441, 0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41, 0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840, 0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41, 0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40, 0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640, 0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041, 0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241, 0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440, 0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40, 0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841, 0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40, 0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41, 0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641, 0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040 ); ################################# ## General file-related things ## ################################# ###################################### ## Creates a new object from a file ## sub new { open(my $file, $_[0]) or (warn "WARNING: Can't open file '$_[0]' for reading!\n$!\n" and return 0); binmode($file); my $self = { 'handle' => $file, 'size' => -s $_[0], 'buffer' => '', 'bufferOffset' => 0, # The beginning of the buffer maps to this byte in the file 'bitstream' => '', # The last 511 bytes of the bitstream. Used for reading from the bit reservoir. 'onFrame' => 0 # Simple frame index }; return bless $self; } ######################################## ## Reads READSIZE bytes to the buffer ## sub readToBuffer { my $self = $_[0]; my $pos1 = pos($self->{'buffer'}); my $a = read($self->{'handle'}, $self->{'buffer'}, READSIZE, length($self->{'buffer'})); # or die "ERREUR (fronch)!\n$!"; pos($self->{'buffer'}) = $pos1; return $a; } ###################################################################### ## Gets rid of the part of the buffer that's already been read once ## sub cleanBuffer { my $self = $_[0]; my $pos1 = pos($self->{'buffer'}); $self->{'buffer'} = substr($self->{'buffer'}, $pos1); } ########################################################## ## Sets the buffer, and the file, back to the beginning ## sub resetBuffer { my $self = $_[0]; seek($self->{'handle'}, 0, 0); $self->{'buffer'} = ''; } ########################### ## MP3-specific commands ## ########################### ###################################################### ## Searches the buffer for a valid XING tag, ## ## then stores it into the {xing} key of the object ## sub searchBufferForXingTag { my ($self, $setPos) = @_; if($self->{'buffer'} =~ /\xFF([\xE0-\xFF])(.)(.)..\x00{7,32}(?:Xing|Info)/) { my $xingPos = length($`); # The tag is at this position in the buffer (I do it this way so that pos(buffer) isn't changed) my $header = mp3::interpretHeader($1, $2, $3); my %header = %$header; # Make sure there is enough junk in the buffer to fit the entire tag if($xingPos + $header{'frameLen'} > length($self->{'buffer'})) { my $pos1 = pos($self->{'buffer'}); read($self->{'handle'}, $self->{'buffer'}, $xingPos + $header{'frameLen'} - length($self->{'buffer'}), length($self->{'buffer'})); pos($self->{'buffer'}) = $pos1; if($xingPos + $header{'frameLen'} > length($self->{'buffer'})) { # STILL not long enough! return; } } my $xingTag = substr($self->{'buffer'}, $xingPos + 4 + $header{'sideInfoSize'}, $header{'frameLen'} - 4 - $header{'sideInfoSize'}); my $xingRawHeader = substr($self->{'buffer'}, $xingPos, 4); my ($headerType, $flags, $numFrames, $numBytes, $quality, $encoder) = unpack("A4NNNx100NA20", $xingTag); # Test to see whether this is a LAME tag or not my $lamePart = substr($xingTag, 140); $lamePart =~ s/\x00*//g; if(length($lamePart)) { # Interpret it as a LAME tag! # Note that we need to re-define the $encoder, as LAME only uses 9 bytes to store it... my ($encoder, $revision, $lowpass, $peakAmplitude, $rgTrack, $rgAlbum, $lameFlags, $abrBitrate, $delay1, $delay2, $delay3, $misc, $mp3gain, $preset, $musicLength, $musicCRC, $tagCRC) = unpack("x120A9CCNnnCCCCCCcnNnn", $xingTag); $self->{'xing'} = { isLame => 1, raw => $xingTag, rawHeader => $xingRawHeader, position => $xingPos, headerType => $headerType, flagValidFrames => ($flags & 1) ? 1 : 0, flagValidBytes => ($flags & 2) ? 1 : 0, flagValidTOC => ($flags & 4) ? 1 : 0, flagValidQuality => ($flags & 8) ? 1 : 0, numFrames => $numFrames, numBytes => $numBytes, quality => $quality, encoder => $encoder, # These are the extra LAME thingies revision => $revision >> 4, vbrMethod => $revision & 15, lowpass => $lowpass * 100, peakAmplitude => $peakAmplitude, rgTrack => $rgTrack, rgAlbum => $rgAlbum, flagNspsytune => ($lameFlags & 16) ? 1 : 0, flagNssafejoint => ($lameFlags & 32) ? 1 : 0, flagNogapNext => ($lameFlags & 64) ? 1 : 0, flagNogapPrev => ($lameFlags & 128) ? 1 : 0, lameATHtype => $lameFlags & 15, abrBitrate => $abrBitrate, delayStart => ($delay1 << 4) + ($delay2 >> 4), delayEnd => (($delay2 & 15) << 8) + $delay3, noiseShaping => $misc & 3, stereoMode => ($misc >> 2) & 7, unwise => ($misc & 32) ? 1 : 0, sourceFrequency => $mp3::SourceFreq[$misc >> 6], mp3gain => $mp3gain, surround => ($preset >> 11) & 7, preset => $preset & 2047, musicLength => $musicLength, }; } else { # Just yer standard XING tag, I suppose... $self->{'xing'} = { isLame => 0, raw => $xingTag, rawHeader => $xingRawHeader, position => $xingPos, headerType => $headerType, flagValidFrames => ($flags & 1) ? 1 : 0, flagValidBytes => ($flags & 2) ? 1 : 0, flagValidTOC => ($flags & 4) ? 1 : 0, flagValidQuality => ($flags & 8) ? 1 : 0, numFrames => $numFrames, numBytes => $numBytes, quality => $quality, encoder => $encoder, }; } $self->checkHeader(\%header); if($setPos) { # Update the current location, if the user specifies pos($self->{'buffer'}) = $xingPos + $header{'frameLen'}; } return 1; } return 0; } ########################################################################## ## This is probably the biggest reading function. ## ## It stores the next frame's data in $self->{'frameData'}{frameNumber} ## ## or simply returns the data if $dontChangeSelf is set ## sub getNextHeader { my ($self, $dontChangeSelf) = @_; my $returnThis; while(1) { print "Starting search for next header at ", pos($self->{'buffer'}), " (tell: ", pos($self->{'buffer'}) + $self->{'bufferOffset'}, ")\n" if DEBUGSEARCH; if($self->{'buffer'} =~ m/\xFF([\xE2-\xFF])(.)(.)/gs) { # Found something which may be a header (has the right sync bits) my $headerPosInBuffer = pos($self->{'buffer'}) - 4; my $headerPosInFile = $headerPosInBuffer + $self->{'bufferOffset'}; print " Found a possible header at $headerPosInBuffer (tell: $headerPosInFile)\n" if DEBUGSEARCH; my $header = interpretHeader($1, $2, $3); if($self->checkHeader($header)) { # Looks like the header checked out OK print " Probably an OK header...\n" if DEBUGSEARCH; # Make sure the buffer is big enough to grab the entire frame if($headerPosInBuffer + $header->{'frameLen'} > length($self->{'buffer'})) { print " The buffer isn't big enough to fit the entire frame!\n" if DEBUGSEARCH; my $pos1 = pos($self->{'buffer'}); read($self->{'handle'}, $self->{'buffer'}, $headerPosInBuffer + $header->{'frameLen'} - length($self->{'buffer'}), length($self->{'buffer'})); pos($self->{'buffer'}) = $pos1; print " Now the buffer's ", length($self->{'buffer'}), " bytes long ($self->{'bufferOffset'} - ", $self->{'bufferOffset'} + length($self->{'buffer'}) - 1, ")\n" if DEBUGSEARCH; if($headerPosInBuffer + $header->{'frameLen'} > length($self->{'buffer'})) { print " But that STILL isn't enough! EOF?\n" if DEBUGSEARCH; return 0; } } # Now read the frame data my $frameGuts = substr($self->{'buffer'}, $headerPosInBuffer, $header->{'frameLen'}); my $a = $self->{'sideInfoSize'}; # Split the header, side info, and bitstream my ($frameHeaderGuts, $frameSideGuts, $frameStreamGuts); if($self->{'crc'}) { if($frameGuts =~ /(.{4})..(.{$self->{'sideInfoSize'}})(.*)/s) { $frameHeaderGuts = $1; $frameSideGuts = $2; $frameStreamGuts = $3; } else { # For some reason the map didn't work! die "ERROR: Can't take the regex /(.{4})..(.{$self->{'sideInfoSize'}})(.*)/ with only ", length($frameGuts), " bytes!\n"; } } else { if($frameGuts =~ /(.{4})(.{$self->{'sideInfoSize'}})(.*)/s) { $frameHeaderGuts = $1; $frameSideGuts = $2; $frameStreamGuts = $3; } else { # For some reason the map didn't work! die "ERROR: Can't take the regex /(.{4})(.{$self->{'sideInfoSize'}})(.*)/ with only ", length($frameGuts), " bytes!\n"; } } ## Append the bitstream to the previous bitty-streamy-thingy my $currentBitstreamLength = length($self->{'bitstream'}); $self->{'bitstream'} .= $frameStreamGuts; ## Figure out where this frame's data is my ($dataStart, $dataLengthInBits) = $self->frameDataLocation($frameSideGuts); ## Grab the proper data from the bitstream buffer my $thisFrameData = substr($self->{'bitstream'}, $currentBitstreamLength - $dataStart, ceil($dataLengthInBits / 8)); ## Save it to $self, I guess... not sure what else to do. ## (Use a hash to easily get rid of frames if needed) unless($dontChangeSelf) { $self->{'frameData'}{$self->{'onFrame'}} = { 'MP3IS' => $header->{'MP3IS'}, 'MP3MS' => $header->{'MP3MS'}, 'header' => $frameHeaderGuts, 'side' => $frameSideGuts, 'data' => $thisFrameData, 'dataLength' => length($thisFrameData), 'position' => $headerPosInFile, }; } $returnThis = { 'frameNumber' => $self->{'onFrame'}, 'MP3IS' => $header->{'MP3IS'}, 'MP3MS' => $header->{'MP3MS'}, 'header' => $frameHeaderGuts, 'side' => $frameSideGuts, 'data' => $thisFrameData, 'dataLength' => length($thisFrameData), 'position' => $headerPosInFile, }; ## Update the frame stuff pos($self->{'buffer'}) = $headerPosInBuffer + $header->{'frameLen'} - 0; $self->{'onFrame'} ++; ## Update the end of the last valid frame (for finding tags after the data has stopped) $self->{'endOfLastFrame'} = $headerPosInBuffer + $self->{'bufferOffset'} + $header->{'frameLen'}; # Cut off the bitstream data to 511 bytes (Don't need any more than that) $self->{'bitstream'} = substr($self->{'bitstream'}, -511); } else { # Header is no good; retry print " Oops. No good\n" if DEBUGSEARCH; # Make sure to back up 3 when retrying; otherwise it might miss a potential header # EX: Assume the hex FF FF FB 90 64. First the program will sync to FF FF FB 90, throw it out, # and start back up searching from 64. This is BAD, since FF FB 90 64 is a valid header pos($self->{'buffer'}) -= 3; next; } } else { # $self->{'buffer'} =~ *headerish* # Didn't find anything which might be a header... update the buffer! print " Found no more headers in the buffer. Make it bigger!\n" if DEBUGSEARCH; $self->{'bufferOffset'} = tell($self->{'handle'}) - 4; $self->{'buffer'} = substr($self->{'buffer'}, -4); my $readBytes = $self->readToBuffer(); if($readBytes) { print " Buffer's now ", length($self->{'buffer'}), " bytes long (", $self->{'bufferOffset'}, " - ", $self->{'bufferOffset'} + length($self->{'buffer'}) - 1, ")\n" if DEBUGSEARCH; next; } else { return 0; } } last; } # while(1) return $returnThis; } ################################################## ## Finds the size of all the data in each frame ## sub findFrameSizes { my ($self, $memoryHog) = @_; my $tellStart = pos($self->{'buffer'}) + $self->{'bufferOffset'}; my $frameNumberStart = $self->{'onFrame'}; my $largestFrameSize = ceil($mp3::Bitrates{$self->{'id'}}{$self->{'layer'}}[14] * $self->{'byteSecondsPerFrameKilobit'}) - 4 - $self->{'sideInfoSize'} - $self->{'crcSize'}; my @frameSizes; while(my $currentFrameData = $self->getNextHeader(1)) { print "Pre-parsing frame ", $currentFrameData->{'frameNumber'} + 1, "\t\t\r" unless DEBUGPREPARSE; if($memoryHog) { $frameSizes[$currentFrameData->{'frameNumber'}] = $currentFrameData; } else { $frameSizes[$currentFrameData->{'frameNumber'}] = {'dataLength' => $currentFrameData->{'dataLength'}}; } } my $requiredReservoir = 0; for my $frameNumber (reverse $frameNumberStart .. $#frameSizes) { if($frameSizes[$frameNumber]{'dataLength'} + $requiredReservoir > $largestFrameSize) { # Work backward to spread the frame data around print "Frame number $frameNumber is $frameSizes[$frameNumber]{'dataLength'} + $requiredReservoir = ", $frameSizes[$frameNumber]{'dataLength'} + $requiredReservoir if DEBUGPREPARSE; $frameSizes[$frameNumber]{'requiredReservoir'} = $requiredReservoir; $requiredReservoir = $frameSizes[$frameNumber]{'dataLength'} + $requiredReservoir - $largestFrameSize; print " = $largestFrameSize + $requiredReservoir\n" if DEBUGPREPARSE; } elsif($requiredReservoir == 0) { # Everythings normal $frameSizes[$frameNumber]{'requiredReservoir'} = 0; } else { # Add carryover to the current frame print "Frame number $frameNumber is $frameSizes[$frameNumber]{'dataLength'} + $requiredReservoir = ", $frameSizes[$frameNumber]{'dataLength'} + $requiredReservoir, "\n" if DEBUGPREPARSE; $frameSizes[$frameNumber]{'requiredReservoir'} = $requiredReservoir; $requiredReservoir = 0; } } # Pretend that this never happened... $self->{'onFrame'} = $frameNumberStart; seek($self->{'handle'}, $tellStart, 0); $self->readToBuffer(); return wantarray ? @frameSizes : \@frameSizes; } ############################################################## ## THIS is the main function of the file ## ## Repacks one file into another, depending on some options ## sub doEverything { my ($self, $outputFile, $outputOptions) = @_; my $xing; # A big ol' collection of new data for the new XING header ####################################################################################################### ## Try to find an XING tag. If none is found then search for the first frame and use its information ## ####################################################################################################### my $firstFramePos = 0; $self->readToBuffer(1); # Not quite sure what this argument does... maybe I should remove it if($self->searchBufferForXingTag(1)) { # The new XING tag header should be the same as the old one print "XING tag found; use it!\n" if DEBUG; $xing->{'header'} = $self->{'xing'}{'rawHeader'}; $firstFramePos = $xing->{'position'}; } else { # Need to find the options for this file, so decode the first frame print "No XING tag found; read first header\n" if DEBUG; my $firstFrame = $self->getNextHeader(1); # This function automatically fills in the missing info $xing->{'header'} = $firstFrame->{'header'}; # Save the header info to the new XING header $self->resetBuffer(); # Pretend that this didn't happen... $self->readToBuffer(); $self->{'onFrame'} = 0; $firstFramePos = $firstFrame->{'position'}; } ##################################################################### ## Make sure the program can understand the file (must be layer 3) ## ##################################################################### if($self->{'layer'} != 3) { die "ERROR: File given is not an MP3! This program can't repack MP1 or MP2 files\n"; } ########################### ## Interpret the options ## ########################### # Minimum bitrate for the output file my @minBitrate; if(exists($outputOptions->{'minBitrate'})) { my $amountOfData = int($outputOptions->{'minBitrate'} * $self->{'byteSecondsPerFrameKilobit'}) - 4 - $self->{'sideInfoSize'} - $self->{'crcSize'}; @minBitrate = $self->findSmallestFrame($amountOfData); } else { @minBitrate = $self->findSmallestFrame(0); } # Maximum bit reservoir size; 511 for MPEG1, 255 for 2 and 2.5 my $maxBitReservoir = int($mp3::MaxBitReservoir{$self->{'id'}}); # Slurp the entire file into memory? my $memoryHog = exists($outputOptions->{'memoryHog'}) ? $outputOptions->{'memoryHog'} : 0; # Save any garbage found at the end of the file? (Defaults to yes) my $saveEndJunk = exists($outputOptions->{'saveEndJunk'}) ? $outputOptions->{'saveEndJunk'} : 1; # Save any garbage found at the beginning of the file? (Defaults to... hmm... yes. Probably.) my $saveBeginningJunk = exists($outputOptions->{'saveBeginningJunk'}) ? $outputOptions->{'saveBeginningJunk'} : 1; # What should the padding look like? (Use 'U' by default. ASCII 'U' = binary 01010101) my $padding = exists($outputOptions->{'padding'}) ? $outputOptions->{'padding'} : ('U' x 1536); # Open the file for writing open(OUT, '>' . $outputFile) or die "ERROR: Can't open file '$outputFile' for writing:\n$!\n"; binmode(OUT); # Write filler for the output XING tag (and for junk at the beginning, if saved) if($saveBeginningJunk) { print OUT 'U' x $firstFramePos; } if(exists($self->{'xing'}) && $self->{'xing'}{'isLame'}) { # The source file used a LAME header; save enough room for an actual LAME header in the output file $xing->{'bitrate'} = $self->findSmallestFrame(156); # 192 bytes in the actual frame } else { # The tag for the input file was normal XING, or the file didn't have a VBR header, so the output file should also be normal XING. $xing->{'bitrate'} = $self->findSmallestFrame(140); } print OUT 'U' x $xing->{'bitrate'}[2]; ########################################################################## ## Find any frames which are larger than the maximum frame size allowed ## ########################################################################## my @framePreparse = $self->findFrameSizes($memoryHog); print "\n" unless DEBUG; # This is so the writing of which frame the main loop is on doesn't interfere with the writing of the preparse my %framesOut; my $bitReservoir = 0; # my $nextHeaderPos = @{$self->findSmallestFrame(348)}[2]; my $nextHeaderPos = $xing->{'bitrate'}[2]; my $overflowWarning; my $frameNumber; ######################### ## It's the main loop! ## ######################### while(1) { my $currentFrameData; if($memoryHog) { last unless($currentFrameData = $framePreparse[$frameNumber]); } else { last unless($currentFrameData = $self->getNextHeader(1)); } # Old way of printing the frame numbers. New one depends on the preparse pass # if($self->{'xing'}{'numFrames'}) { # print "On frame ", $currentFrameData->{'frameNumber'} + 1, " of $self->{'xing'}{'numFrames'} (", int($currentFrameData->{'frameNumber'} / $self->{'xing'}{'numFrames'} * 100), "%)\t\t\r" unless DEBUG; # } else { # print "On frame ", $currentFrameData->{'frameNumber'} + 1, "\t\t\r" unless DEBUG; # } print "On frame ", $currentFrameData->{'frameNumber'} + 1, " of ", $#framePreparse + 1, "\t\t\r" unless DEBUG; print " Frame $currentFrameData->{'frameNumber'}\n" if DEBUG; print " $currentFrameData->{'dataLength'} bytes of data\n" if DEBUG; print " $framePreparse[$currentFrameData->{'frameNumber'}]{'requiredReservoir'} bytes required in reservoir\n" if(DEBUG && $framePreparse[$currentFrameData->{'frameNumber'}]{'requiredReservoir'}); my $dataToStore = $currentFrameData->{'dataLength'} - $bitReservoir; my $dataToPretend = $dataToStore + $framePreparse[$currentFrameData->{'frameNumber'}]{'requiredReservoir'}; print " $dataToStore bytes to store, pretend with $dataToPretend ($bitReservoir bytes in the bit reservoir)\n" if DEBUG; my @bitrateToUse = $self->findSmallestFrame($dataToPretend); if($bitrateToUse[2] < $minBitrate[2]) { # The 'perfect' bitrate is less than the minimum bitrate specified @bitrateToUse = @minBitrate; } print " Output frame:\n" if DEBUG; print " $bitrateToUse[0] kbps\n" if DEBUG; print " $bitrateToUse[2] bytes in this frame, ", $bitrateToUse[2] - 4 - $self->{'sideInfoSize'}, " usable", $bitrateToUse[1] ? " (P!)\n" : "\n" if DEBUG; # Update the minimum bitrate for the file if( ! defined $xing->{'minBitrate'} || $xing->{'minBitrate'} > $bitrateToUse[0]) { $xing->{'minBitrate'} = $bitrateToUse[0]; } my $storeThisData = $currentFrameData->{'data'}; my $offset = $bitReservoir; # This is the offset (from the end of the frame) to start the current data #################################### ## Store the data in some frames! ## #################################### if($offset == 0) { # The bit reservoir isn't used; shtick it in the current frame only print " NO bit reservoir!\n" if DEBUG; $framesOut{$currentFrameData->{'frameNumber'}} = { 'bitrate' => \@bitrateToUse, 'usableBytes' => $bitrateToUse[2] - 4 - $self->{'sideInfoSize'}, 'data' => substr($storeThisData . $padding, 0, $bitrateToUse[2] - 4 - $self->{'sideInfoSize'}), 'header' => mp3::resizeFrame($currentFrameData->{'header'}, $bitrateToUse[0], $bitrateToUse[1]), 'side' => mp3::moveData($currentFrameData->{'side'}, $self->{'id'}, 0), }; $xing->{'framePositions'}[$currentFrameData->{'frameNumber'}] = $nextHeaderPos; $nextHeaderPos += $bitrateToUse[2]; print OUT mp3::purgeToFrame(\%framesOut, $currentFrameData->{'frameNumber'}, $padding); print " Purged to frame $currentFrameData->{'frameNumber'}\n" if DEBUG; $bitReservoir = $framesOut{$currentFrameData->{'frameNumber'}}{'usableBytes'} - $currentFrameData->{'dataLength'}; print " $bitReservoir bytes now in bit reservoir\n" if DEBUG; } else { print " Use bit reservoir!\n" if DEBUG; my $frameNow = 0; foreach my $frameNowInky (sort {$b <=> $a} keys %framesOut) { last if $storeThisData eq ''; # No reason to do any of this if there's no data to store # Go through all the frames backwards to find the starting frame print " Searching in frame $frameNowInky for offset $offset\n" if DEBUG; if($offset > $framesOut{$frameNowInky}{'usableBytes'}) { # The data should start before the start of this frame; keep foreach-ing print " But it only uses $framesOut{$frameNowInky}{'usableBytes'} bytes\n" if DEBUG; $offset -= $framesOut{$frameNowInky}{'usableBytes'}; print " Now using an offset of $offset\n" if DEBUG; } else { print " Write $offset bytes of data starting at ", $framesOut{$frameNowInky}{'usableBytes'} - $offset, " of frame $frameNowInky\n" if DEBUG; # The amount to store in the current frame is equal to $offset substr($framesOut{$frameNowInky}{'data'}, $framesOut{$frameNowInky}{'usableBytes'} - $offset, $offset) = substr($storeThisData . $padding, 0, $offset); substr($storeThisData, 0, $offset) = ''; # The beginning data has been stored $frameNow = $frameNowInky; last; } } print OUT mp3::purgeToFrame(\%framesOut, $frameNow, $padding); print " Purged to frame $frameNow\n" if DEBUG; print " Start writing to frames\n" if DEBUG; $frameNow ++; # Keep writing on the NEXT frame... # I know it's kind of strange to have a variable determined in terms of itself, but it should work... # Write data to the frames after that foreach $frameNow ($frameNow .. $currentFrameData->{'frameNumber'} - 1) { last if $storeThisData eq ''; # No more data to store print " Wrote $framesOut{$frameNow}{'usableBytes'} bytes of data to frame $frameNow " if DEBUG; $framesOut{$frameNow}{'data'} = substr($storeThisData . $padding, 0, $framesOut{$frameNow}{'usableBytes'}); substr($storeThisData, 0, $framesOut{$frameNow}{'usableBytes'}) = ''; print "(", length($storeThisData), ")\n" if DEBUG; } # Now stick the remaining data into the current frame $framesOut{$currentFrameData->{'frameNumber'}} = { 'bitrate' => \@bitrateToUse, 'usableBytes' => $bitrateToUse[2] - 4 - $self->{'sideInfoSize'}, 'data' => substr($storeThisData . $padding, 0, $bitrateToUse[2] - 4 - $self->{'sideInfoSize'}), 'header' => mp3::resizeFrame($currentFrameData->{'header'}, $bitrateToUse[0], $bitrateToUse[1]), 'side' => mp3::moveData($currentFrameData->{'side'}, $self->{'id'}, $bitReservoir), }; $xing->{'framePositions'}[$currentFrameData->{'frameNumber'}] = $nextHeaderPos; $nextHeaderPos += $bitrateToUse[2]; print " Stuck the remaining ", length($storeThisData), " bytes in the current frame\n" if DEBUG; $bitReservoir = $framesOut{$currentFrameData->{'frameNumber'}}{'usableBytes'} - $dataToStore; print " $bitReservoir bytes now in bit reservoir\n" if DEBUG; } # Make sure the bit reservoir is smaller than the max... if($bitReservoir > $maxBitReservoir) { $bitReservoir = $maxBitReservoir; print " Too much! Reset to $maxBitReservoir\n" if DEBUG; } print "\n\n********************************************************************************\n" if DEBUG; $frameNumber ++; } # File's done! Dump the rest print OUT mp3::purgeAllFrames(\%framesOut, $padding); ################# ## END OF FILE ## ################# if($self->{'endOfLastFrame'} < $self->{'size'} && $saveEndJunk) { # There seems to be some junk at the end of the file # Bah. I don't need any object-oriented routine to get this... print "Save the end junk!\n" if DEBUG; my $tellTemp = tell($self->{'handle'}); seek($self->{'handle'}, $self->{'endOfLastFrame'}, 0); read($self->{'handle'}, my $temp, $self->{'size'} - $self->{'endOfLastFrame'}); print OUT $temp; seek($self->{'handle'}, $tellTemp, 0); } ####################### ## BEGINNING OF FILE ## ####################### if($firstFramePos && $saveBeginningJunk) { # There's stuff at the beginning of the file. let's save it! print "Save the beginning junk!\n" if DEBUG; seek($self->{'handle'}, 0, 0); read($self->{'handle'}, my $temp, $firstFramePos); seek(OUT, 0, 0); print OUT $temp; } ################# ## XING HEADER ## ################# close(OUT); # Let's see... $xing->{'fileSize'} = -s $outputFile; open(OUT, '+<' . $outputFile) or die "ERROR: Can't open file '$outputFile' for writing:\n$!\n"; binmode(OUT); if($saveBeginningJunk) { seek(OUT, $firstFramePos, 0); # $firstFramePos refers to the first frame in the input file. However, it is also the postiion of the XING tag in the output file } else { seek(OUT, 0, 0); } $xing->{'numFrames'} = $#{$xing->{'framePositions'}} + 1; #die unpack("H*", $self->createXingFrame($xing)); print "\n"; print OUT $self->createXingFrame($xing); close(OUT); # Don't print this any more, since there's nothing that can be done about it now. # if($overflowWarning) { # print < $b} keys %$framesOut) { return $out if $key >= $frameTo; $out .= $framesOut->{$key}{'header'}; $out .= $framesOut->{$key}{'side'}; $out .= substr($framesOut->{$key}{'data'} . $padding, 0, $framesOut->{$key}{'usableBytes'}); delete($framesOut->{$key}); } return $out; } ############################################################# ## Outputs ALL the frames which haven't been outputted yet ## sub purgeAllFrames { my ($framesOut, $padding) = @_; my $out; foreach my $key (sort {$a <=> $b} keys %$framesOut) { $out .= $framesOut->{$key}{'header'}; $out .= $framesOut->{$key}{'side'}; $out .= substr($framesOut->{$key}{'data'} . $padding, 0, $framesOut->{$key}{'usableBytes'}); delete($framesOut->{$key}); } return $out; } ################# ## Information ## ################# ########################################### ## Updates a CRC value with another byte ## sub updateCRC { my ($value, $crc) = @_; my $temp = $crc ^ $value; $crc = ($crc >> 8) ^ $mp3::CRClookup[$temp & 255]; return $crc; } ################################# ## Create a CRC given a string ## sub createCRC { my ($string, $initialValue) = @_; my $crc = $initialValue; # 0 for the header thingie foreach my $char (split //, $string) { $crc = mp3::updateCRC(ord($char), $crc); } return $crc; } ########################################################################## ## Makes an XING tag frame given the old XING frame and some new values ## sub createXingFrame { my ($self, $xing) = @_; my $outXing = mp3::resizeFrame($xing->{'header'}, $xing->{'bitrate'}[0], $xing->{'bitrate'}[1]) . (chr(0) x $self->{'sideInfoSize'}) . 'Xing'; if(defined $self->{'xing'}{'quality'}) { $outXing .= pack('NNN', 15, $xing->{'numFrames'}, $xing->{'fileSize'}); } else { $outXing .= pack('NNN', 7, $xing->{'numFrames'}, $xing->{'fileSize'}); } ## TOC! ## print "$xing->{'numFrames'} frames in $xing->{'fileSize'} bytes\n"; for my $percent (0 .. 99) { my $frameNumber = int($percent * $xing->{'numFrames'} * 0.01); $outXing .= chr(int($xing->{'framePositions'}[$frameNumber] * 256 / $xing->{'fileSize'})); printf("%2d,%% at %" . length($xing->{'numFrames'}) . "d, byte offset %" . length($xing->{'fileSize'}) . "d, byte value %3d,\n", $percent, $frameNumber, $xing->{'framePositions'}[$frameNumber], int($xing->{'framePositions'}[$frameNumber] * 256 / $xing->{'fileSize'})) if DEBUG; } if(defined $self->{'xing'} && $self->{'xing'}{'isLame'}) { # The initial tag was a LAME tag; write a LAME tag to the output file too $outXing .= pack('N', $self->{'xing'}{'quality'}); $outXing .= substr($self->{'xing'}{'encoder'} . (chr(0) x 9), 0, 9); $outXing .= chr(($self->{'xing'}{'revision'} << 4) + $self->{'xing'}{'vbrMethod'}); $outXing .= chr($self->{'xing'}{'lowpass'} / 100); $outXing .= pack('N', $self->{'xing'}{'peakAmplitude'}); $outXing .= pack('nn', $self->{'xing'}{'rgTrack'}, $self->{'xing'}{'rgAmplitude'}); $outXing .= chr($self->{'xing'}{'flagNspsytune'} * 16 + $self->{'xing'}{'flagNssafejoint'} * 32 + $self->{'xing'}{'flagNogapNext'} * 64 + $self->{'xing'}{'flagNogapPrev'} * 128 + $self->{'xing'}{'lameATHtype'}); $outXing .= ($xing->{'minBitrate'} > 255) ? chr(255) : chr($xing->{'minBitrate'}); { # Encoder delays my $delay1 = $self->{'xing'}{'delayStart'} >> 4; my $delay2 = (($self->{'xing'}{'delayStart'} << 4) & 240) + ($self->{'xing'}{'delayEnd'} >> 8); my $delay3 = $self->{'xing'}{'delayEnd'} & 255; $outXing .= chr($delay1) . chr($delay2) . chr($delay3); } $outXing .= chr(($mp3::SourceFreqInv{$self->{'xing'}{'sourceFrequency'}} << 6) + $self->{'xing'}{'unwise'} * 32 + ($self->{'xing'}{'stereoMode'} << 2) + $self->{'xing'}{'noiseShaping'}); $outXing .= chr($self->{'xing'}{'mp3gain'}); $outXing .= pack('n', ($self->{'xing'}{'surround'} << 11) + $self->{'xing'}{'preset'}); $outXing .= pack('N', $self->{'xing'} {'musicLength'}); $outXing .= chr(0) . chr(0); # Music CRC... I really don't want to calculate it $outXing .= pack('n', mp3::createCRC($outXing)); # Header CRC } elsif(defined $self->{'xing'}) { # The source is NOT LAME, so don't save anything that's not required, or known $outXing .= pack('N', $self->{'xing'}{'quality'}); $outXing .= substr($self->{'xing'}{'encoder'} . (chr(0) x 20), 0, 20); } else { # There was no XING header, so just make a non-LAME version for the output. $outXing .= pack('N', 0); $outXing .= substr('MP3Packer' . $main::version . (chr(0) x 20), 0, 20); } # print length($outXing), "\n", unpack("H*", $outXing); # die; return $outXing; } ############################################################################# ## Returns a hash of the header, given the second, third, and fourth bytes ## sub interpretHeader { my ($header1, $header2, $header3) = @_; $header1 = ord($header1); $header2 = ord($header2); $header3 = ord($header3); # print "- $header1 - $header2 - $header3 -\n"; my %out; $out{id} = $mp3::IDs[($header1 >> 3) & 3]; $out{layer} = $mp3::Layers[($header1 >> 1) & 3]; # CRC is present if this is 0! $out{crc} = ($header1 & 1) ? 0 : 1; $out{bitrate} = $mp3::Bitrates{$out{id}}{$out{layer}}[($header2 >> 4) & 15]; $out{samplerate} = $mp3::Samplerates{$out{id}}[($header2 >> 2) & 3]; $out{samplerateIndex} = ($header2 >> 2) & 3; $out{padding} = ($header2 & 2) ? 1 : 0; $out{private} = ($header2 & 1) ? 1 : 0; $out{channelMode} = $mp3::ChannelMode[($header3 >> 6) & 3]; $out{channelModeIndex} = ($header3 >> 6) & 3; if($out{layer} == 3) { $out{MP3MS} = ($header3 & 32) ? 1 : 0; $out{MP3IS} = ($header3 & 16) ? 1 : 0; } else { # Layers 1 and 2 join channels from this band up $out{MP12band} = $mp3::MP12bandJoin[($header3 >> 4) & 3]; } $out{channelExtIndex} = ($header3 >> 4) & 3; $out{copyright} = ($header3 & 8) ? 1 : 0; $out{original} = ($header3 & 4) ? 1 : 0; $out{emphasis} = $mp3::Emphasis[$header3 & 3]; $out{emphasisIndex} = $header3 & 3; if($out{samplerate}) { $out{frameLen} = mp3::unpaddedFrameLength($out{bitrate}, $out{samplerate}) + $out{padding}; } # Now calculate how many byte seconds per frame kilobit... if($out{samplerate} && $out{id} && $out{layer}) { $out{byteSecondsPerFrameKilobit} = $mp3::SamplesPerFrame{$out{id}}{$out{layer}} / $out{samplerate} * 125; if($out{layer} == 3) { $out{sideInfoSize} = $mp3::SideInfoSize{$out{id}}{$out{channelMode}}; } } return \%out; } ################################################################################# ## Checks the file's header information against the current header ## ## returns 1 if they're the same, 0 if not. Will also fill in any missing info ## sub checkHeader { my ($self, $header) = @_; # If the header to be checked against has invalid data, throw it out for sure return 0 if $header->{id} == 0; return 0 if $header->{layer} == 0; return 0 if $header->{bitrate} == 0; return 0 if $header->{samplerate} == 0; return 0 if $header->{emphasis} eq 'INVALID'; # Return 0 if some of the data doesn't match up return 0 if((exists $self->{id} ) && ($self->{id} != $header->{id} )); return 0 if((exists $self->{layer} ) && ($self->{layer} != $header->{layer} )); return 0 if((exists $self->{samplerate} ) && ($self->{samplerate} != $header->{samplerate} )); return 0 if((exists $self->{channelMode}) && ($self->{channelMode} ne $header->{channelMode})); return 0 if((exists $self->{copyright} ) && ($self->{copyright} != $header->{copyright} )); return 0 if((exists $self->{original} ) && ($self->{original} != $header->{original} )); return 0 if((exists $self->{emphasis} ) && ($self->{emphasis} ne $header->{emphasis} )); return 0 if((exists $self->{crc} ) && ($self->{crc} != $header->{crc} )); # Update the data that doesn't exist yet (for whatever reason) $self->{id} = $header->{id} unless exists $self->{id}; $self->{layer} = $header->{layer} unless exists $self->{layer}; $self->{samplerate} = $header->{samplerate} unless exists $self->{samplerate}; $self->{samplerateIndex} = $header->{samplerateIndex} unless exists $self->{samplerateIndex}; $self->{channelMode} = $header->{channelMode} unless exists $self->{channelMode}; $self->{channelModeIndex} = $header->{channelModeIndex} unless exists $self->{channelModeIndex}; $self->{copyright} = $header->{copyright} unless exists $self->{copyright}; $self->{original} = $header->{original} unless exists $self->{original}; $self->{emphasis} = $header->{emphasis} unless exists $self->{emphasis}; $self->{emphasisIndex} = $header->{emphasisIndex} unless exists $self->{emphasisIndex}; $self->{crc} = $header->{crc} unless exists $self->{crc}; # This is 2 if there exists CRC, 0 otherwise $self->{crcSize} = 2 * $self->{crc}; # Also update the size of the side info $self->{sideInfoSize} = $mp3::SideInfoSize{$header->{id}}{$header->{channelMode}} unless exists $self->{sideInfoSize}; # Then update the file's "byte seconds per frame kilobit" constant $self->{byteSecondsPerFrameKilobit} = $header->{byteSecondsPerFrameKilobit} unless exists $self->{byteSecondsPerFrameKilobit}; # If it's made it this far, it must be OK... return 1; } ######################################################################################## ## Simple little function to return the number of bytes in a frame of a given bitrate ## sub unpaddedFrameLength { my ($bitrate, $samplerate) = @_; if($samplerate < 32000) { # MPEG2 return int(72000 * $bitrate / $samplerate); } else { # MPEG1 return int(144000 * $bitrate / $samplerate); } } #################################################################################### ## Read the side info to find out where the data actually starts in the reservoir ## sub frameDataLocation { my ($self, $side) = @_; die "ERROR: Can't run frame location on layer 1 or 2 data" unless $self->{'layer'} == 3; my $main_data_begin = unpack("n", substr($side, 0, 2)); if($self->{'id'} == 1) { $main_data_begin = $main_data_begin >> 7; } else { $main_data_begin = $main_data_begin >> 8; } my $lengthInBits; # print "Side length: ", length($side), "\n"; if($self->{'channelMode'} eq 'Mono') { if($self->{'id'} == 1) { # Mono MPEG1 my ($a, $b) = unpack("xxnxxxxN", $side); $lengthInBits = (($a >> 2) & 4095) + (($b >> 7) & 4095); } else { # Mono MPEG2 my $a = unpack("xn", $side); $lengthInBits = (($a >> 3) & 4095); } } else { if($self->{'id'} == 1) { # Stereo MPEG1 <= PROBABLY THE MOST COMMON my ($a, $b, $c, $d) = unpack("xxnxxxxNxxxxxnxxxxN", $side); $lengthInBits = (($a >> 0) & 4095) + (($b >> 5) & 4095) + (($c >> 2) & 4095) + (($d >> 7) & 4095); } else { # Stereo MPEG2 my ($a, $b) = unpack("xnxxxxxxn", $side); $lengthInBits = (($a >> 2) & 4095) + (($b >> 3) & 4095); } } return wantarray ? ($main_data_begin, $lengthInBits) : [$main_data_begin, $lengthInBits]; } ######################################################################################################## ## Finds the smallest frame which is large enough to fit all the data ## ## Returns the bitrate and padding amount required, along with the TOTAL number of bytes in the frame ## sub findSmallestFrame { my ($self, $dataLength) = @_; $dataLength += 4 + $self->{'sideInfoSize'}; my @bitratePossibilities = @{$mp3::Bitrates{$self->{'id'}}{$self->{'layer'}}}; for my $inky (0 .. $#bitratePossibilities) { my $bitrate = $bitratePossibilities[$inky]; next unless $bitrate; my $frameSize = int($self->{'byteSecondsPerFrameKilobit'} * $bitrate); if($frameSize >= $dataLength) { # No padding OK return wantarray ? ($bitrate, 0, $frameSize, $inky) : [$bitrate, 0, $frameSize, $inky]; } elsif($frameSize + 1 >= $dataLength) { # OK with padding return wantarray ? ($bitrate, 1, $frameSize + 1, $inky) : [$bitrate, 1, $frameSize + 1, $inky]; } } return wantarray ? (0, 0, 0, 0) : [0, 0, 0, 0]; } ################################################ ## Looks like I don't need this one after all ## sub makeFrameHeader { my ($id, $layer, $protected, $bitrateIndex, $samplerate, $padding, $private, $channelMode, $ms, $is, $copyright, $original, $emphasis); ## Not quite finished... } ############################################################## ## Change a frame header to make the frame a different size ## sub resizeFrame { my ($oldFrameHeader, $newFrameBitrate, $newPadding) = @_; my $old = interpretHeader(substr($oldFrameHeader, 1, 1), substr($oldFrameHeader, 2, 1), substr($oldFrameHeader, 3, 1)); my $headerNumber = unpack('N', $oldFrameHeader); # Make sure there's no CRC. Kind of a nasty hack to add this here, but it SHOULD work # (Getting rid of CRCs saves 612.5 bits/second! Yay!) $headerNumber |= 65536; # Clear the old bitrate and padding $headerNumber &= 4294905343; my @bitrates = @{$mp3::Bitrates{$old->{'id'}}{$old->{'layer'}}}; my $inky; for my $bitrateIndex (0 .. $#bitrates) { if($bitrates[$bitrateIndex] >= $newFrameBitrate) { $inky = $bitrateIndex; last; } } if($inky == 0) { # UH-oh! The specified bitrate is too large for the id, layer selected! die "ERROR: The specified bitrate $newFrameBitrate is too large for MPEG$old->{'id'} layer $old->{'layer'}\n"; } # add in the new bitrate and padding return pack('N', $headerNumber | (($inky << 12) + ($newPadding << 9))); } ################################################################# ## Updates the side info of a frame for moving the data around ## sub moveData { # This had better be an MP3! my ($oldSide, $id, $newOffset) = @_; my $firstTwoBytes = unpack('n', substr($oldSide, 0, 2)); if($id == 1) { # MPEG1 die "ERROR: Offset $newOffset must be from 0 to 511 for MPEG1" if($newOffset < 0 || $newOffset > 511); $firstTwoBytes &= 127; # Get rid of the old bit reservoir $firstTwoBytes |= int($newOffset) << 7; # OR in the new one } else { die "ERROR: Offset $newOffset must be from 0 to 255 for MPEG2 and MPEG2.5" if($newOffset < 0 || $newOffset > 255); $firstTwoBytes &= 255; # Same as above $firstTwoBytes |= int($newOffset) << 8; } # die $oldSide; return pack('n', $firstTwoBytes) . substr($oldSide, 2); } __END__ # 12 bits = 4095 ## Side info: # 9/17/32 BYTES = 136/256 bits # 9: main_data_begin (8 for MPEG2) # ?: private_bits # 4: SCFI Band # 59: Side Information Granule # 12: part2_3 length (main data for this channel, granule in bits) # 9: Big values # 8: Global gain # 4: Scalefactor compress (9 for MPEG2) # 1: Window switch flag # if 1: # 2: Block type # 1: Mix block flag # 5x2: Table Select [region] # 3x3: sub_block_gain [window] # if 0: # 5x3: Table select [region] # 4: Region 0 count # 3: Region 1 count # 1: Pre flag (NOT FOR MPEG2) # 1: Scale factor scale # 1: Count1 table select # MPEG1 mono: # [9 main data] [5 privates] [4 SCFI] [59 Gr0] [59 Gr1] # (18 - 30) (77 - 89) # MPEG1 stereo: # [9 main data] [3 privates] [4 SCFI0] [4 SCFI1] [59 Gr0ch1] [59 Gr0ch2] [59 Gr1ch1] [59 Gr1ch2] # 20 79 138 197 # MPEG2 mono: # [8 main data] [1 privates] [63 Gr*] # (9 - 21) # MPEG2 stereo: # [8 main data] [2 privates] [63 Gr*ch1] [63 Gr*ch2] # (10 - 22) (73 - 85)