3

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?

Sergey Potapov
  • 3,819
  • 3
  • 27
  • 46
  • 2
    *"I need to concatenate them in memory without using command line tools."* - why? – Stefan Oct 15 '13 at 13:53
  • Perfomance issue. I know there is a tool like `sox` but it requires writing file to disk, then running `sox` command , than reading output file and removing temporary files.. – Sergey Potapov Oct 15 '13 at 14:03
  • I'm surprised that simply concatenating the strings works. The player is being resilient to the unexpected data, which is good for you. However, you might want to try some alternative players/browsers if you go with that solution. In terms of effort versus reward, it's a big win. – Neil Slater Oct 15 '13 at 14:07
  • You may have other options than concatenation - various ways to manage a playlist at a client for instance. What is your use case? – Neil Slater Oct 15 '13 at 14:09
  • We get audio chunks from remote service and cache them in Reddis. We combine them using template engine similar to ERB(I hope it will be open sourced soon) but for audio chunks. Then we put audio to customer with send_data method. – Sergey Potapov Oct 15 '13 at 14:13
  • You *really* should be using audio tools to do this rather than slamming the MP3 files together end to end. That's completely non-standard, and that players can handle it at all is surprising. You can do this with raw audio data, but generally not framed formats like MP3. Use a tool like `sox` to extract the raw audio data, concatenate that, and re-encode to MP3 to produce a valid file. – tadman Oct 15 '13 at 14:58
  • Worth reading this though: http://stackoverflow.com/questions/6222225/using-cat-to-join-mp3-files-what-is-this-black-sorcery – Neil Slater Oct 15 '13 at 16:21
  • Thanks everbody for the answers. I've found the solution (see my answer below). – Sergey Potapov Oct 16 '13 at 09:59

1 Answers1

6

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)
Sergey Potapov
  • 3,819
  • 3
  • 27
  • 46
  • Kudos for working it out yourself, I had a look around (at e.g. ruby-audio etc) but couldn't find any Ruby libraries that could do this in memory for you. – Neil Slater Oct 17 '13 at 13:18
  • Sorry, I am not familar with C# . But I guess you can understand the algorithm written in ruby. – Sergey Potapov Mar 01 '15 at 23:46