Search code examples
rubymp3

How concatenate mp3 files with ruby


I have few mp3 files as binary strings with same number of channels and same sample rate. I need to concatenate them in memory without using command line tools.

Currently I just do string concatenation, like this:

out = ''
mp3s.each { |mp3| out << mp3 }

Audio players can play the result, but with some warnings, because mp3 headers were not handled correctly as far as I understand.

Is there a way to proceed the concatenation in more correct way?


Solution

  • After reading this article about MP3 in russian I came up with solution. You must be able to get complete ID3 specification at http://id3.org/ but it seems to be down at the moment.

    Usually Mp3 file have the next format:

    [ID3 head(10 bytes) | ID3 tags | MP3 frames ]
    

    ID3 is not part of MP3 format, but it's kind of container which is used to put information like artists, albums, etc...

    The audio data itself are stored in MP3 frames.Every frame starts with 4 bytes header which provides meta info (codecs, bitrate, etc).

    Every frame has fixed size. So if there are not enough samples at the end of last frame, coder adds silence to make frame have necessary size. I also found there chunks like LAME3.97 (name and version of coder).

    So, all we need to do is to get rid of ID3 container. The following solution works for me perfect, no warnings anymore and out file became smaller:

    # Length of header that describes ID3 container
    ID3_HEADER_SIZE = 10
    
    # Get size of ID3 container.
    # Length is stored in 4 bytes, and the 7th bit of every byte is ignored.
    #
    # Example:
    #         Hex: 00       00       07       76
    #         Bin: 00000000 00000000 00000111 01110110
    #    Real bin:                        111  1110110
    #    Real dec: 1014
    #
    def get_id3_size(header)
      result = 0
      str = header[6..9]
    
      # Read 4 size bytes from left to right applying bit mask to exclude 7th bit
      # in every byte.
      4.times do |i|
        result += (str[i].ord & 0x7F) * (2 ** (7 * (3-i)))
      end
    
      result
    end
    
    def strip_mp3!(raw_mp3)
      # 10 bytes that describe ID3 container.
      id3_header = raw_mp3[0...ID3_HEADER_SIZE]
      id3_size = get_id3_size(id3_header)
    
      # Offset from which mp3 frames start
      offset = id3_size + ID3_HEADER_SIZE
    
      # Get rid of ID3 container
      raw_mp3.slice!(0...offset)
      raw_mp3
    end
    
    # Read raw mp3s
    hi  = File.binread('hi.mp3')
    bye = File.binread('bye.mp3')
    
    # Get rid of ID3 tags
    strip_mp3!(hi)
    strip_mp3!(bye)
    
    # Concatenate mp3 frames
    hi << bye
    
    # Save result to disk
    File.binwrite('out.mp3', hi)