6
\$\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\$
1
  • \$\begingroup\$ Might want to consider loading the MP3 using .NET rather than a convoluted Unity system. You'll have less overhead AND won't actually need to "stream" the audio so much as play the audio from memory. \$\endgroup\$ Nov 27, 2019 at 17:27

3 Answers 3

6
\$\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 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\$
3
  • 2
    \$\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, 2016 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\$ Aug 11, 2017 at 4:27
  • \$\begingroup\$ This solution may works for Windows, but I couldn't install NAudio for macOS. \$\endgroup\$
    – Cœur
    Sep 25, 2019 at 2:07
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\$
0
\$\begingroup\$

There might be something helpful in this thread:
https://forum.unity.com/threads/how-play-audio-mp3-from-server-url.472243/

They load mp3s from a server and I realize you're trying to get them from the user's hard disk. You may be able to spawn a server to host the directory of mp3s with something like https://www.npmjs.com/package/serve. That should work at least on your system and then you could start exploring ways of packaging it since I assume you'd like this app to run on other people's system too and it may be harder to spawn a server without certain dependencies outside of the Unity build.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .