Search code examples
unity-game-engineaudiogetfilesaudioclip

Unity3D: Resource.Load defined AudioClip ends up Null going into the AudioSource


What I'm trying to do is contain an audio file in a folder (under Resources) where I can drop any qualifying audio file in the specified folder and have the numerous triggers in my program read from that single point (which is why my AudioClip below is public static so I can reference it). Currently, the same audio file works throughout the program, but to change the file requires manual redefining in the Inspector which my eventual client won't have access to, and besides is tedious due to the numerous reference points that exist.

Here's what I have so far:

public static AudioClip BGM;
public AudioSource BGMSource;
private string formatted1;

void Start()
{
   foreach(string file in System.IO.Directory.GetFiles(Application.dataPath+"/Resources/Audio/BGM"))
   {
      if(file.EndsWith(System.IO.Patch.GetExtension(".mp3")))
      {
        formatted1 = file.Replace(".mp3",string.Empty);
        BGM = Resources.Load<AudioClip>(formatted1); 
         //BGM = (AudioClip)Resources.Load(formatted1,typeof(AudioClip));  <--same result with this
        Debug.Log("found: "+formatted1);
      }

   }
   if(BGM == null)
   {
     Debug.Log("Yeah, its null");
   }

   BGMSource.PlayOneShot(BGM, .9f);

   if(BGMSource.isPlaying != true)
   {
     Debug.Log("I'm not playing");
   }
}

So as is, this just doesn't play, no error messages. Turns out BGM is null. The Debug says as so, but if I were to add a Debug call for BGMSource.clip.name, it will fully error out with a NullReferenceException on that Debug.

The Debug for the formatted1 string (File path and name), it does present the correct file called Test.mp3 ("C:/...Resources/Audio/BGM\Test") formatted without the ".mp3" as was recommended from another site. I did try with the .mp3 extension on, didn't seem to matter, still didn't play. I also tried with a .wav file and .ogg file, same result (note: all files were fine if I attached as a public AudioClip manually as also the AudioSource as written above would play in that case, but as I lead with, we don't want that for this case). Yes, all test audio files were in the directory /Resources/Audio/BGM.

Another site said something about adding to the top of the file [RequireComponent(typeof(AudioClip))] or [RequireComponent(typeof(AudioSource))]but that did nothing.

Lastly, this program will eventually be given to a group that won't have source access so they MUST be able to swap the audio file by dropping any .mp3 in Resources/Audio/BGM for auto play.

Any help is welcome, thanks!


Solution

  • First a general note: Never use + "/" for system file paths! Rather sue Path.Combine which automatically inserts the correct path separators according to the platform it is running on

    string file in System.IO.Directory.GetFiles(Path.Combine(Application.dataPath, "Resources", "Audio", "BGM"))
    

    Then please read the documentation of Resources.Load!

    1. It requires your file(s) being placed inside a folder called Resources which is compiled into the application build and therefore can not be changed afterwards. This seems to be the case for you.

    2. It does not take a full system path like you pass in since Directory.GetFiles returns

      An array of the full names (including paths) for the files in the specified directory

      but rather expects a path within all Resources folders - yes you can have multiple ones.

      Let's say e.g. you put your files in a structure like

      Assets
      |--Resources
      |  |--someFile.mp3
      |
      |--SomeOtherFolder
      |  |--Resources
      |  |  |--someOtherFile.mp3
      |  |  |--ASubFolder
      |  |  |  |--yetAnotherFile.mp3
      

      Then you would address these by using

      Resources.Load<AudioClip>("someFile");
      Resources.Load<AudioClip>("someOtherFile");
      Resources.Load<AudioClip>("ASubfolder/yetAnotherFile");
      

      since when build all Resources are packed together.

      So in your case it should be

      Resources.Load<AudioClip>("Audio/BGM/" + formatted1);
      

      where you have to make sure that formatted1 is not a full path but only the filename! You can simply use Path.GetFileNameWithoutExtension so you don't even need your replace

      var formatted1 = Path.GetFileNameWithoutExtension(file);
      
    3. It is bad ^^

      In their Best Practices for the Resources folder Unity themselves recommend

      Don't use it!


    However

    Since it is not recommneded to use the Resources at all I would rather recommend:

    If you don't want to change them later

    You can't change the Resources afterwards for e.g. replacing a file. So if you can't change the files later anyway, then why not rather directly reference them in the places where they are needed later?

    Simply put your audio files in a folder that is not Resources and reference them in your scripts directly where you need them:

    // Simply drag&drop the clip into this field via the Inspector in Unity
    [SerializeField] private AudioClip someClip;
    

    If you want to change them later

    In case you actually would like to be able to replace them later also after a build you could instead use UnityWebRequestMultimedia.GetAudioClip which can also be used to load files from a system file on runtime. For this you wouldn't put your files into the Resources or any other folder but rather either the StreamingAssets or the Application.persistentDataPath.

    I usually go:

    • In the editor use StreamingAssets folder so all stuff lies inside the project and access it via Application.streamingAssetsPath
    • In a build first check if file exists in Application.persistentDataPath
    • If not copy it from Application.streamingAssetsPath and store it into Application.persistentDataPath
    • otherwise simply load it from Application.persistentDataPath

    Modified API Example

    [RequireComponent(typeof(AudioSource))]
    public class AudioExample : MonoBehaviour
    {
        [SerializeField] private AudioSource _audioSource;
    
        public List<AudioClip> LoadedAudioClips = new List<AudioClip>;
    
        private List<UnityWebRequest> _runningWebRequests = new List<UnityWebRequest>();
    
        private void Awake()
        {
            if(!_audioSource) _audioSource = GetComponent<AudioSource>();
        }
    
        private void Start()
        {
            StartCoroutine(GetAudioClip());
        }
    
        private IEnumerator GetAudioClip()
        {
            foreach(string file in System.IO.Directory.GetFiles(Path.Combine(Application.persistentDataPath, "Audio", "BGM"))
            {
                if(!file.EndsWith(System.IO.Patch.GetExtension(".mp3"))) continue;
    
                UnityWebRequest www = UnityWebRequestMultimedia.GetAudioClip("file:///" + file, AudioType.MPEG);
                {
                   _runningWebRequests.Add(www);
                   www.Send();
                }
            }
    
            while(_runningWebRequests.Count > 0)
            {
                foreach(var www in _runningWebRequests.Where(www => www.isDone))
                {
                    _runningWebRequests.Remove(www);
    
                    if (www.isError)
                    {
                        Debug.LogWarning(www.error);
                    }
                    else
                    {
                        var clip = DownloadHandlerAudioClip.GetContent(www);
                        if(clip == null)
                        {
                            Debug.LogError("Huh?!");
                        }
                        else
                        {
                            LoadedAudioClips.Add(clip);
                        }
                    }
                }
    
                yield return null;
            }
        }
    }
    

    Also saw you comments so:

    StreamingAssets is also a special folder you can store files in you want to read in on runtime. It is local. Also this folder is not "visible" from the outside and can not be altered later.

    If you need to be able to alter stuff later (like e.g. also saving files) you will always need to use the Application.persistentDataPath instead.