Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle looping music #390

Open
ViMaSter opened this issue Sep 7, 2023 · 3 comments · May be fixed by #411
Open

Handle looping music #390

ViMaSter opened this issue Sep 7, 2023 · 3 comments · May be fixed by #411
Assignees
Labels
feature New feature or request
Milestone

Comments

@ViMaSter
Copy link
Member

ViMaSter commented Sep 7, 2023

Description

Music can optionally have loop sections defined. Currently, these files are simply looped from beginning to end.

Acceptance Criteria

This story will be finished when Unity handles two types of .ogg and .wav files:

  • files without loop markers
    • loop from beginning to end
  • files with loop markers
    • play optional intro sections once
    • continue looping from LOOP_START to LOOP_END
    • once enabled, play their outro after reaching LOOP_END the next time
      • for verification purposes, attempting to queue the outro should fail with a visible error message if the file has an outro section of less than 1 second (or none at all)
@ViMaSter ViMaSter added the feature New feature or request label Sep 7, 2023
@ViMaSter ViMaSter added this to the 1.0: Case 2 milestone Sep 7, 2023
@ViMaSter
Copy link
Member Author

ViMaSter commented Sep 7, 2023

Potential implementation:

  • Load a file
  • Split the samples based on the loop markers (code that trims silence as reference code)
    AudioClip trimSilence(AudioClip inputAudio, float threshold = 0.05f)
    {
      // Copy samples from input audio to an array. AudioClip uses interleaved format so the length in samples is multiplied by channel count
      float[] samplesOriginal = new float[inputAudio.samples * inputAudio.channels];
      inputAudio.GetData(samplesOriginal, 0);
    
      // Find first and last sample (from any channel) that exceed the threshold
      int audioStart = Array.FindIndex(samplesOriginal, sample => sample > threshold),
          audioEnd = Array.FindLastIndex(samplesOriginal, sample => sample > threshold);
    
      // Copy trimmed audio data into another array
      float[] samplesTrimmed = new float[audioEnd - audioStart];
      Array.Copy(samplesOriginal, audioStart, samplesTrimmed, 0, samplesTrimmed.Length);
    
      // Create new AudioClip for trimmed audio data
      AudioClip trimmedAudio = AudioClip.Create(inputAudio.name, samplesTrimmed.Length / inputAudio.channels, inputAudio.channels, inputAudio.frequency, false);
      trimmedAudio.SetData(samplesTrimmed, 0);
    
      return trimmedAudio;
    }
  • Create separate AudioClip instances from it (Intro, Loop, Outro)
  • Immediately Play() intro AudioClip
  • PlayScheduled() the loop
    void Start () 
    {
      musicSource.PlayOneShot(musicStart);
      musicSource.PlayScheduled(AudioSettings.dspTime + musicStart.length);
    }

@ViMaSter
Copy link
Member Author

So...

I tried to extract the data as late as possible:

  • at runtime won't work:
    .ogg and .wav files can only be accessed via AudioClip:
    it's possible to get samples, but not raw byte data of the original file
    it is possible to read the raw files in the editor, but not inside builds1; Unity compresses all that data away
  • at build time won't work:
    the step would only be executed before packaging builds, so loops wouldn't work in Editor builds
    not really acceptable IMO
  • at import time doesn't work either:
    Unity needs to store the exported data in a ScriptableObject; but Calls to "AssetDatabase.CreateAsset" are restricted during asset importing."

The "simplest" but most labor-intensive way is adding a MenuItem (like Assets -> Export and store loop markers) to the editor, which you need to run manually, in the editor, on each music file. I feel there's a lot of room for oversight and I'm not a fan:

  • forgetting to delete it when the original music file is renamed
  • forgetting to delete it when the original music file is deleted
  • not running the export at all

There's one potential implementation which works around 1: StreamingAssets
This would allow raw access to the original files. Meaning, there's no need to do anything special in the editor. However:

You can only have one StreamingAssets folder, and it must be placed in the root of the Project, directly within the Assets folder.

We'd have to relocate all music files to that specific directory (or a subdirectory thereof, Assets/StreamingAssets/Music/CoolPatrolX.ogg) for this to work, however, it would offer all advantages:

  • music files wouldn't have to be processed manually (it'll simply happen during runtime)
  • the loop markers wouldn't have to be extracted and stored as ScriptableObject instances, separate from the actual music file
  • this part of the feature already requires support for streaming files from the file system

Opinions on this implementation or alternatives, that offer the same set of advantages?

@lyndsiWilliams
Copy link
Member

I don't see any downfalls to using StreamingAssets based on what you've presented. It's always good to go for the route with fewer possibilities for "user error", as you've explained. Having all the music in one place sounds like a good idea too. I say go for it, nice solution!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature or request
Projects
None yet
2 participants