This project has moved. For the latest updates, please go here.

Convert streaming mu-law wav to mp3 in real-time

Jun 13, 2014 at 8:57 PM
Edited Jun 13, 2014 at 9:03 PM
I've posted this question on stackoverflow here: question

Background: I am consuming a service which returns data with a MIME type of audio/wav. I need to provide a playback mechanism for this audio (currently built as an MVC application). As an example, my endpoint looks something like https://audio.fooservice.com/GetAudio?audioId=123

The audio is 8kHz, 1-channel u-law.

Due to varying format support across browsers when using the HTML5 <audio> tag, I am unable to use the original u-law wav because Internet Explorer will not play it.

My proposed solution is to do a real-time conversion from the source format to mp3.

I've cobbled together a partially working solution from various other questions here and in the NAudio forums, but it throws an exception as noted in the comments below.

Please note: I have all of about an hour of experience with NAudio (and audio processing in general). This particular piece is a very small piece of a much larger application, so forgive my ignorance/stupidity.

This seems like it should be fairly simple. From my layman's perspective, I want to:
  • Grab an audio stream, which is 8kHz single-channel mu-law
  • Get all of the bytes for that audio stream
  • Convert to PCM (this step may not be necessary?)
  • Convert to mp3
  • Provide that mp3 stream as an endpoint for my application to consume
private void NAudioTest(string url)
{
    Stream outStream = new MemoryStream();
    var format = WaveFormat.CreateMuLawFormat(8000, 1);

    using (Stream ms = new MemoryStream())
    {
        var request = (HttpWebRequest)WebRequest.Create(url);
        request.KeepAlive = false;
        request.ProtocolVersion = HttpVersion.Version10;

        using (Stream stream = request.GetResponse().GetResponseStream())
        {
            using (var reader = new RawSourceWaveStream(stream, format))
            {
                // reader is not seekable; we need to convert to a byte array to seek
                var bytes = reader.ToByteArray();

                // create a new stream from the byte aray
                var seekableStream = new MemoryStream(bytes);

                // instantiating a WaveFileReader as follows will throw an exception:
                // "System.FormatException: Not a WAVE file - no RIFF header"
                using (var waveReader = new WaveFileReader(seekableStream))
                {
                    using (var pcmStream = WaveFormatConversionStream.CreatePcmStream(waveReader))
                    {
                        var pcmBytes = pcmStream.ToByteArray();
                        var mp3 = pcmBytes.ToMp3();
                    }
                }
            }
        }
    }
}

public static class StreamExtensions
{
    public static byte[] ToByteArray(this Stream stream)
    {
        var ms = new MemoryStream();
        var buffer = new byte[1024];
        int bytes = 0;

        while ((bytes = stream.Read(buffer, 0, buffer.Length)) > 0)
            ms.Write(buffer, 0, bytes);

        return ms.ToArray();
    }
}

public static class ByteExtensions
{
    public static byte[] ToMp3(this byte[] bytes)
    {
        using (var outStream = new MemoryStream())
        {
            using (var ms = new MemoryStream(bytes))
            {
                using (var reader = new WaveFileReader(ms))
                {
                    using (var writer = new LameMP3FileWriter(outStream, reader.WaveFormat, 64))
                    {
                        reader.CopyTo(writer);
                        return outStream.ToArray();
                    }
                }
            }
        }
    }
}
Coordinator
Jun 17, 2014 at 8:38 AM
Yes, you need to get to PCM before you go to MP3. Also I'm not even sure all MP3 encoders will encode at such a low sample rate so you may end up having to upsample. I wrote a fairly detailed article on converting between formats with NAudio here which should help you
Jun 17, 2014 at 3:30 PM
Thanks Mark.

I did resolve my RIFF exception (aka, I actually thought about what I was trying to do for a few minutes), but the mp3 stream I'm producing is nothing but white noise.

Do you think that's due to the low sample rate of the source stream?
    [HttpGet]
    public ActionResult GetMp3(string url)
    {
        if (String.IsNullOrWhiteSpace(url))
            return null;

        var muLawFormat = WaveFormat.CreateMuLawFormat(8000, 1);
        var compressedStream = new MemoryStream();

        using (var ms = new MemoryStream())
        {
            var request = (HttpWebRequest)WebRequest.Create(url);
            request.KeepAlive = false;
            request.ProtocolVersion = HttpVersion.Version10;

            using (Stream webStream = request.GetResponse().GetResponseStream())
            {
                var buffer = new byte[4096];
                int read;
                while (webStream != null && (read = webStream.Read(buffer, 0, buffer.Length)) > 0)
                    ms.Write(buffer, 0, read);
            }

            ms.Position = 0;

            using (WaveStream wav = WaveFormatConversionStream.CreatePcmStream(new RawSourceWaveStream(ms, muLawFormat)))
            using (var mp3 = new LameMP3FileWriter(compressedStream, new WaveFormat(), LAMEPreset.MEDIUM_FAST))
                wav.CopyTo(mp3);
        }

        compressedStream.Seek(0, 0);
        return new FileStreamResult(compressedStream, "audio/mpeg");
    }
Coordinator
Jun 17, 2014 at 3:34 PM
I don't know about LameMP3FileWriter's inner workings (it's not part of NAudio), but I'd strongly suspect you are passing in the wrong format of PCM. What is the second parameter of the LameMP3FileWriter actually supposed to be?
Jun 17, 2014 at 7:52 PM
Thanks Mark.

The second parameter, from the source, is as follows:
        /// <summary>Create MP3FileWriter to write to supplied stream</summary>
        /// <param name="outStream">Stream to write encoded data to</param>
        /// <param name="format">Input WaveFormat</param>
        /// <param name="quality">LAME quality preset</param>
        public LameMP3FileWriter(Stream outStream, WaveFormat format, LAMEPreset quality)
            : base()
I think my issue is that I incorrectly assumed that the WaveFormatConversionStream.CreatePcmStream(...) method would return a stream with the default WaveFormat.

I'll certainly post an update when I've got it working.

Thanks for your questions--you lead me in the right direction!