I believe I finally found a solution for this issue, for everyone who comes across it in the future.
I had the same problem in 2025 — this horrible screeching/distorted crackling sound on certain Android devices. All the symptoms matched this thread and several others I found across the forum.
I tried everything: reducing sound quality, limiting FPS, exporting audio in different formats… nothing helped.
I also noticed, just like other people mentioned, that the issue only happens on some Android devices and not others, and also that sometimes it happens only with specific music tracks, not all of them.
During development I never had this issue when testing on PC — of course. Then I tested the app on a Galaxy S23 FE — also no issue.
But later I tested on a Galaxy A35, which has a relatively weak processor, so I immediately suspected it might be related to hardware performance. After trying all the suggestions I found in the forums (unsuccessfully), I decided to try something else.
The first thing I did was hide all 3D objects and scripts in the scene, leaving only the audio playing. Immediately, the crackling was completely gone.
Then I started adding objects back into the scene, and the crackling returned — mildly at first, then more and more as I added more objects. This confirmed it was definitely a performance-related issue. After reading more, I understood that this is caused by too much load on the main thread. Some of my scenes were perfectly fine — the ones that didn’t have many models to render — and the audio had zero issues. (I specifically copied the same background music into those scenes for testing.)
Final conclusion: weaker phones with weaker hardware were causing the crackling, and nothing I did inside Unity’s audio system solved it.
So the workaround turned out to be very simple:
Instead of using Unity’s AudioSource on Android, I bypassed it completely and used the Android native media player for background music. This works perfectly.
There are plugins that do this, but I wrote my own script (with help from my friend GPT) that invokes Java through C#, plays background music, handles pause/resume when the app is minimized, and can be reused across different scenes by loading a track by filename. You just need to place your audio files in the Assets/StreamingAssets folder.
This solution works great for background music, but the native media player can only handle one track at a time. So I wrote another script that uses the Android native SoundPool for sound effects and narration. This also works perfectly. And of course, you can add an if/else in your code to use the normal Unity AudioSource when running in the Editor or on iOS.
Hope this helps. Posting the code now 
NativeAudio: just add to an empty game object and pass musicfilename.mp3
using UnityEngine;
using System.Collections;
public class AndroidBGMPlayer : MonoBehaviour
{
[Header("Music Filename in StreamingAssets (e.g., bgm_forest.mp3)")]
public string musicFileName = "bgm_default.mp3";
private AndroidJavaObject mediaPlayer;
private bool isPrepared = false;
void Start()
{
if (Application.platform == RuntimePlatform.Android)
StartCoroutine(InitAndPlay(musicFileName));
}
IEnumerator InitAndPlay(string fileName)
{
yield return new WaitForSeconds(1f); // Optional delay before playing
using (AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
{
AndroidJavaObject activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
AndroidJavaObject context = activity.Call<AndroidJavaObject>("getApplicationContext");
AndroidJavaObject assetManager = context.Call<AndroidJavaObject>("getAssets");
AndroidJavaObject afd = assetManager.Call<AndroidJavaObject>("openFd", fileName);
mediaPlayer = new AndroidJavaObject("android.media.MediaPlayer");
mediaPlayer.Call("setDataSource",
afd.Call<AndroidJavaObject>("getFileDescriptor"),
afd.Call<long>("getStartOffset"),
afd.Call<long>("getLength"));
mediaPlayer.Call("setLooping", true);
mediaPlayer.Call("prepare");
mediaPlayer.Call("start");
isPrepared = true;
}
}
public void Stop()
{
if (mediaPlayer != null && isPrepared)
{
mediaPlayer.Call("stop");
mediaPlayer.Call("release");
mediaPlayer.Dispose();
mediaPlayer = null;
}
}
void OnApplicationPause(bool pause)
{
if (mediaPlayer == null) return;
if (pause) mediaPlayer.Call("pause");
else mediaPlayer.Call("start");
}
void OnDestroy()
{
Stop();
}
}
SoundPool: Just add to an empty game object, add a selector and filename:
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
public class AndroidSoundPoolManager : MonoBehaviour
{
public static AndroidSoundPoolManager Instance;
private AndroidJavaObject soundPool;
private Dictionary<string, int> soundIds = new Dictionary<string, int>();
private List<int> playingStreamIds = new List<int>();
private AndroidJavaObject context;
private bool isReady = false;
[System.Serializable]
public class SoundEntry
{
public string key;
public string fileName;
}
[Header("Sound Files")]
public SoundEntry[] soundEntries;
public bool IsReady() => isReady;
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
#if UNITY_ANDROID && !UNITY_EDITOR
StartCoroutine(InitSoundPoolAsync());
#else
Debug.Log("🧪 SoundPool only works on Android builds.");
#endif
}
else
{
Destroy(gameObject);
}
}
#if UNITY_ANDROID && !UNITY_EDITOR
private IEnumerator InitSoundPoolAsync()
{
using (var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
{
context = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity")
.Call<AndroidJavaObject>("getApplicationContext");
}
soundPool = new AndroidJavaObject("android.media.SoundPool", 10, 3, 0);
foreach (var entry in soundEntries)
{
string sourcePath = Path.Combine(Application.streamingAssetsPath, entry.fileName);
string destPath = Path.Combine(Application.persistentDataPath, entry.fileName);
if (!File.Exists(destPath))
{
UnityWebRequest www = UnityWebRequest.Get(sourcePath);
yield return www.SendWebRequest();
if (www.result == UnityWebRequest.Result.Success)
{
File.WriteAllBytes(destPath, www.downloadHandler.data);
Debug.Log($"✅ Copied: {entry.key} to {destPath}");
}
else
{
Debug.LogError($"❌ Failed to copy {entry.fileName}: {www.error}");
continue;
}
}
int soundId = soundPool.Call<int>("load", destPath, 1);
soundIds[entry.key] = soundId;
Debug.Log($"🎵 Loaded sound: {entry.key} → ID: {soundId}");
}
isReady = true;
}
#endif
public void PlaySound(string key, float volume = 1f)
{
#if UNITY_ANDROID && !UNITY_EDITOR
if (!isReady)
{
Debug.LogWarning("🚫 SoundPool not ready yet.");
return;
}
if (soundIds.TryGetValue(key, out int id))
{
int streamId = soundPool.Call<int>("play", id, volume, volume, 1, 0, 1f);
if (streamId > 0)
{
playingStreamIds.Add(streamId);
}
}
else
{
Debug.LogWarning($"🚫 Sound key not found: '{key}'");
}
#else
Debug.Log($"🔊 [Editor] Would play sound: {key}");
#endif
}
private void OnApplicationPause(bool pause)
{
#if UNITY_ANDROID && !UNITY_EDITOR
if (!isReady || soundPool == null) return;
if (pause)
{
// Pause all playing streams
foreach (int streamId in playingStreamIds)
{
soundPool.Call("pause", streamId);
}
Debug.Log("⏸️ Paused all SoundPool streams");
}
else
{
// Resume all previously playing streams
foreach (int streamId in playingStreamIds)
{
soundPool.Call("resume", streamId);
}
Debug.Log("▶️ Resumed all SoundPool streams");
}
#endif
}
}
Invoke from anywhere in your code:
private IEnumerator Start()
{
#if UNITY_ANDROID && !UNITY_EDITOR
// Wait until SoundPool is initialized before playing
yield return new WaitUntil(() => AndroidSoundPoolManager.Instance != null && AndroidSoundPoolManager.Instance.IsReady());
AndroidSoundPoolManager.Instance.PlaySound("paintIntro");
#else
yield return null;
#endif
}