4
\$\begingroup\$

I am making a game in Unity in which the player can select music files in their hard drive to use them in-game. I use the WWW class to load the sound files, but this only works for OGG, WAV and Tracker files. I use the following code:

WWW www = new WWW ("file:///" + soundPath);
while(!www.isDone){
    yield return 0;
}
GetComponent<AudioSource> ().clip = www.GetAudioClip(false, true);

I want to play audio files in mp3 format, which is the most widely used format for music in general, but when i try to play them, this gets printed to the debug log:

Streaming of 'mp3' is not supported on this platform.

How can i easily achieve mp3 playback on desktop?

\$\endgroup\$
4
\$\begingroup\$

To be able to play .mp3 files on desktop, you'll need help from an external library to convert it first to another format.

You can use NAudio (docs) to convert the audio from MP3 to WAV before playing it.

  • You must set the API Compatiblity Level to .NET 2.0 (Edit->Project Settings->Player)
  • Put NAudio.dll in /Assets/Plugins
  • Create a new C# Script, name it "NAudioPlayer.cs" and put the following on it:

    using UnityEngine;
    using System.IO;
    using System;
    using NAudio;
    using NAudio.Wave;
    
    public static class NAudioPlayer {
        public static AudioClip FromMp3Data(byte[] data)
        {
            // Load the data into a stream
            MemoryStream mp3stream = new MemoryStream(data);
            // Convert the data in the stream to WAV format
            Mp3FileReader mp3audio = new Mp3FileReader(mp3stream);
            WaveStream waveStream = WaveFormatConversionStream.CreatePcmStream(mp3audio);
            // Convert to WAV data
            WAV wav = new WAV(AudioMemStream(waveStream).ToArray());
            Debug.Log(wav);
            AudioClip audioClip = AudioClip.Create("testSound", wav.SampleCount, 1,wav.Frequency, false);
            audioClip.SetData(wav.LeftChannel, 0);
            // Return the clip
            return audioClip;
        }
    
        private static MemoryStream AudioMemStream(WaveStream waveStream)
        {
            MemoryStream outputStream = new MemoryStream();
            using (WaveFileWriter waveFileWriter = new WaveFileWriter(outputStream, waveStream.WaveFormat)) 
            { 
                byte[] bytes = new byte[waveStream.Length]; 
                waveStream.Position = 0;
                waveStream.Read(bytes, 0, Convert.ToInt32(waveStream.Length)); 
                waveFileWriter.Write(bytes, 0, bytes.Length); 
                waveFileWriter.Flush(); 
            }
            return outputStream;
        }
    }
    
    /* From http://answers.unity3d.com/questions/737002/wav-byte-to-audioclip.html */
    public class WAV  {
    
        // convert two bytes to one float in the range -1 to 1
        static float bytesToFloat(byte firstByte, byte secondByte) {
            // convert two bytes to one short (little endian)
            short s = (short)((secondByte << 8) | firstByte);
            // convert to range from -1 to (just below) 1
            return s / 32768.0F;
        }
    
        static int bytesToInt(byte[] bytes,int offset=0){
            int value=0;
            for(int i=0;i<4;i++){
                value |= ((int)bytes[offset+i])<<(i*8);
            }
            return value;
        }
        // properties
        public float[] LeftChannel{get; internal set;}
        public float[] RightChannel{get; internal set;}
        public int ChannelCount {get;internal set;}
        public int SampleCount {get;internal set;}
        public int Frequency {get;internal set;}
    
        public WAV(byte[] wav){
    
            // Determine if mono or stereo
            ChannelCount = wav[22];     // Forget byte 23 as 99.999% of WAVs are 1 or 2 channels
    
            // Get the frequency
            Frequency = bytesToInt(wav,24);
    
            // Get past all the other sub chunks to get to the data subchunk:
            int pos = 12;   // First Subchunk ID from 12 to 16
    
            // Keep iterating until we find the data chunk (i.e. 64 61 74 61 ...... (i.e. 100 97 116 97 in decimal))
            while(!(wav[pos]==100 && wav[pos+1]==97 && wav[pos+2]==116 && wav[pos+3]==97)) {
                pos += 4;
                int chunkSize = wav[pos] + wav[pos + 1] * 256 + wav[pos + 2] * 65536 + wav[pos + 3] * 16777216;
                pos += 4 + chunkSize;
            }
            pos += 8;
    
            // Pos is now positioned to start of actual sound data.
            SampleCount = (wav.Length - pos)/2;     // 2 bytes per sample (16 bit sound mono)
            if (ChannelCount == 2) SampleCount /= 2;        // 4 bytes per sample (16 bit stereo)
    
            // Allocate memory (right will be null if only mono sound)
            LeftChannel = new float[SampleCount];
            if (ChannelCount == 2) RightChannel = new float[SampleCount];
            else RightChannel = null;
    
            // Write to double array/s:
            int i=0;
            while (pos < wav.Length) {
                LeftChannel[i] = bytesToFloat(wav[pos], wav[pos + 1]);
                pos += 2;
                if (ChannelCount == 2) {
                    RightChannel[i] = bytesToFloat(wav[pos], wav[pos + 1]);
                    pos += 2;
                }
                i++;
            }
        }
    
        public override string ToString ()
        {
            return string.Format ("[WAV: LeftChannel={0}, RightChannel={1}, ChannelCount={2}, SampleCount={3}, Frequency={4}]", LeftChannel, RightChannel, ChannelCount, SampleCount, Frequency);
        }
    }
    

You can then do this:

WWW www = new WWW (musicUrl);
while(!www.isDone){
    yield return 0;
}
GetComponent<AudioSource> ().clip = NAudioPlayer.FromMp3Data(www.bytes);

Please note that this will likely freeze the game for about a second or two, so use it wisely. If you need to load multiple sounds this way, try to do it during a load screen so the game doesn't get frozen while playing.

\$\endgroup\$
  • 1
    \$\begingroup\$ You can use "yield return www" instead of that while loop to continue only once the load is finished. As for the FromMp3Data, is there any chance that can run on a separate thread (with assigning it as the audiosource's clip happening back on the main thread at the end) to keep the game from stalling? \$\endgroup\$ – DMGregory Jan 15 '16 at 1:44
  • \$\begingroup\$ I've implemented your code and it works like a charm; However, the audio quality has dropped by a very noticeable amount. How could this have happened, and is there any way i can fix it it? I'm creating a music-based game, so maintaining sound quality is very important for me. \$\endgroup\$ – Tanner Fix-It Smith Aug 11 '17 at 4:27
0
\$\begingroup\$

This is only for monochannel audio files:

audioClip.SetData(wav.LeftChannel, 0);

For Stereo Channels, create a new float array which is the result of both left and right channel. (L[0], R[0], L[1], R[1], etc...) Then set data with that array.

Example:

        float[] combinedChannels = new float[wav.LeftChannel.Length + wav.RightChannel.Length];
        int pointer = 0;
        int lpointer = 0;
        int rpointer = 0;
        while(pointer < combinedChannels.Length)
        {
            combinedChannels[pointer] = wav.LeftChannel[lpointer];
            lpointer++;
            pointer++;
            combinedChannels[pointer] = wav.RightChannel[rpointer];
            rpointer++;
            pointer++;
        }
        audioClip.SetData(combinedChannels, 0);

Remember to check if the wav is mono first before doing this.

Also make sure to change the channels to 2 when creating the audioclip so it works properly

AudioClip audioClip = AudioClip.Create("testSound", wav.SampleCount, 2,wav.Frequency, false);

Just dropping a comment here for referencing afterwards if I need it. ;)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.