1

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!

  • 1
    First of all, resources can not be replaced if you do not have source access. For that i think you are limited to StreamingAssets or just any relative folder using the streamingassets method of access. Which i think happens with a Web request instead. You are also limited to what audio types unity can load on a runtime build. Not all types can be imported in runtime like you would in the editor. Since the entire code would change to work with streaming assets, it is no use trying to debug this now though. – Smileynator Jan 30 '20 at 23:11
  • I took a quick glance at streaming assets, isn't something I've used before. I'll need more time to test with that, but so far its the same result. Not sure if it matters for streaming asset use, but this program will likely always be offline. I'd have thought streaming meant online only, but the [doc site](https://docs.unity3d.com/2018.3/Documentation/Manual/StreamingAssets.html) doesn't strictly say online, so we'll see if I can make something happen. Thanks for the tip! – user2402654 Jan 30 '20 at 23:55
  • I suggest you read the documentation on streaming assets. But in a nutshell: assets normally are in the game and hard referenced by unity so unity knows what to do. Resources are still packed in the game, but in a special zip like folder, and it's own file structure to access. And then there is streaming assets which basically just brings the file "as is' in the build folder. You use UnityWebRequest to get them, even though they are local files (but they do not have to be) – Smileynator Jan 31 '20 at 06:37

1 Answers1

2

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.

derHugo
  • 83,094
  • 9
  • 75
  • 115
  • Well that was thorough, lol! So while as you mentioned, the Resoures.Load isn't going to do what I need it to do for this purpose, the standards adjustments you mentioned are absolutely welcome as I'm using my same ghetto variations of "replace" and +/ in other places in my prog that can be updated. The web bit I comically already had in my code before but got frustrated with the string of errors and trashed it. As is protocol, what I missed was simple and your example shows what I missed. Thank you very much! – user2402654 Jan 31 '20 at 16:07