I Need Help converting Byte[] to Clip

Hello,

I have a code in which i get AudioData byte[ ]
I want to convert it to a List of Clips

I Know the Data are ok, because i can create files in .mp3
But i would like to avoid write/Delete files…

So I try to pass the datas to a wrapperlike this:

using System;
using System.IO;
using UnityEngine;

namespace Flippit
{
    public static class WavUtility
    {
        public static AudioClip ToAudioClip(byte[] audioData, int offset, int length, int lengthSamples, int frequency, bool isStereo)
        {
            if (length <= 0 || audioData == null)
            {
                return null;
            }

            float[] audioFloat = new float[lengthSamples * (isStereo ? 2 : 1)];

            int subChunk1Size = 16;
            int audioFormat = 1; // PCM
            int numChannels = isStereo ? 2 : 1;
            int bitsPerSample = 16;

            int byteRate = frequency * numChannels * bitsPerSample / 8;
            int blockAlign = numChannels * bitsPerSample / 8;
            int subChunk2Size = lengthSamples * numChannels * bitsPerSample / 8;
            int chunkSize = 36 + subChunk2Size;

            using MemoryStream stream = new();
            using BinaryWriter writer = new(stream);
            writer.Write(new char[] { 'R', 'I', 'F', 'F' });
            writer.Write(chunkSize);
            writer.Write(new char[] { 'W', 'A', 'V', 'E' });
            writer.Write(new char[] { 'f', 'm', 't', ' ' });
            writer.Write(subChunk1Size);
            writer.Write(audioFormat);
            writer.Write(numChannels);
            writer.Write(frequency);
            writer.Write(byteRate);
            writer.Write(blockAlign);
            writer.Write(bitsPerSample);
            writer.Write(new char[] { 'd', 'a', 't', 'a' });
            writer.Write(subChunk2Size);

            for (int i = 0; i < lengthSamples; i++)
            {
                float sample = BitConverter.ToSingle(audioData, i * 4 + offset);
                audioFloat[i] = sample;
            }

            AudioClip audioClip = AudioClip.Create("TempClip", lengthSamples, isStereo ? 2 : 1, frequency, false);
            audioClip.SetData(audioFloat, 0);

            return audioClip;
        }

    }
}

each time I try, I get the following error message:

ArgumentException: Length of created clip must be larger than 0

Of course it should, I checked AudioData as A length, and each converted float also
So I’m wondering what i should do to get the correct conversion.

So we had a similar conversation around here: Why does AudioClip.Create create silent clips?

If you want to “create a new audio asset” you will have to write a file to the disc and then import that file.

I understand it’s not ideal, but as this workaround exists, I doubt this is the kind of feature that will be added any time soon to our audio arsenal. You can certainly request it by sending all your hope and love to @KalUnity3D :wink:

Thanks for tagging me @SeventhString - @blackant if you can submit it through here where it says “submit a new idea” I’ll pick up the feature request and look at overall user pain for prioritisation: Audio and video workflow roadmap | Unity

Ok, nice, thanks for your quick answer.
I have already seen those constraints while working with audio.
I hope this feature will be added, because it’s not handy comes time to develop for cross platforms with different aspect of coding.

I made some changes to my code,

now i have the clips created as i want, but the conversion is not good because I can only ear Scrriiitch, scritch…

private void CreateAudioClip(byte[] audioData)
{
    string outputPath = Path.Combine(Application.persistentDataPath, "audio_" + DateTime.Now.ToString("yyyyMMddHHmmss") + ".wav");
    clip = AudioConverter.ConvertToWav(audioData, outputPath, 44100, 16, 1);
    audioClips.Add(clip);
}
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

namespace Flippit
{
   
    public class AudioConverter
    {
       
        public static AudioClip ConvertToWav(byte[] audioData, string outputPath, int sampleRate, int bitsPerSample, int channels)
        {
            int numSamples = audioData.Length / (bitsPerSample / 8);
            float[] audioSamples = new float[numSamples];

            // Convertissez les données audio brutes en float[]
            for (int i = 0; i < numSamples; i++)
            {
                audioSamples[i] = BitConverter.ToInt16(audioData, i * (bitsPerSample / 8)) / 32768f;
            }
            AudioClip clip = AudioClip.Create("MyAudioClip", numSamples, channels, sampleRate, false);

            using FileStream fileStream = new(outputPath, FileMode.Create);
            using BinaryWriter writer = new(fileStream);

            writer.Write(new char[] { 'R', 'I', 'F', 'F' });
            writer.Write(audioData.Length + 36);

            writer.Write(new char[] { 'W', 'A', 'V', 'E' });
            writer.Write(new char[] { 'f', 'm', 't', ' ' });
            writer.Write(16);
            writer.Write((ushort)1);
            writer.Write((ushort)channels);
            writer.Write(sampleRate);
            writer.Write((ushort)bitsPerSample);
            writer.Write((ushort)(bitsPerSample / 8 * channels));
            writer.Write((ushort)bitsPerSample);
            writer.Write(new char[] { 'd', 'a', 't', 'a' });
            writer.Write(audioData.Length);
            writer.Write(audioData);

            clip.SetData(audioSamples, 0);

            return clip;
        }
    }
}

I don’t know how to finally get the .mp3 received as byte[ ] , traited correctly.

Est-ce possible de tester le fichier résultant dans un autre lecteur média? Le problème est probablement un détail d’implémentation. Pour être certain, j’utiliserais plutôt quelque chose comme NAudio qui est en mesure d’écrire simplement et fiablement des fichiers, en plus d’avoir une panoplie intéressante de features.

using NAudio.Wave;
using NAudio.Wave.SampleProviders;

public void SaveToWav(float[] samples, string filename, int sampleRate = 44100, int channels = 1)
{
    var waveFormat = WaveFormat.CreateIeeeFloatWaveFormat(sampleRate, channels);
    using (var writer = new WaveFileWriter(filename, waveFormat))
    {
        var sampleProvider = new BufferedSampleProvider(waveFormat);
        sampleProvider.AddSamples(samples, 0, samples.Length);
        var sampleBuffer = new float[samples.Length];
        sampleProvider.Read(sampleBuffer, 0, samples.Length);
        writer.WriteSamples(sampleBuffer, 0, sampleBuffer.Length);
    }
}

Non, je ne peux tester le fichier à l’extérieur,

Pour expliquer plus en profondeur la démarche,
Je travaille sur le développement d’un Plugin qui permet de parler avec un personnage, envoyé le prompt vers le serveur, recevoir différentes infos par websockets, dont les données Audio,

else if (message.Contains("audio"))
{
    AudioMessage audioMessage = JsonUtility.FromJson<AudioMessage>(message);
    byte[] decodedAudioBytes = Convert.FromBase64String(audioMessage.audio_bytes);
    dialSc.WriteIntoFile(decodedAudioBytes);
    GetComponent<DialogueWindow>().ReceiveVisemes(audioMessage.viseme_bytes);
}

À partir de ces infos, j’ai réussi à créer les fichiers en local dans persistant Data, et faire lire l’audio, et le détruire une fois lu.

        public void WriteIntoFile(byte[] audioData)
        {
            Debug.Log("Taile de AudioData -> " + audioData.Length);
            if (audioData != null && audioData.Length > 0)
            {

#if USE_WEBGL
                CreateAudioClip(audioData);
#else
                fileName = GenerateUniqueFileName();
                string filePath = Path.Combine(Application.persistentDataPath, fileName);

                Debug.Log("Write File at --> " + filePath);
                while (files.Contains(filePath))
                {
                    Debug.Log("File contain (regenerate name) --> " + filePath);
                    fileName = GenerateUniqueFileName();

                    filePath = Path.Combine(Application.persistentDataPath, fileName);
                }

                files.Add(filePath);
                using MemoryStream memoryStream = new(audioData);
                using FileStream filestream = new(filePath, FileMode.Create);
                byte[] buffer = new byte[8 * 1024];
                int bytesRead;
                while ((bytesRead = memoryStream.Read(buffer, offset: 0, count: buffer.Length)) > 0)
                {
                    filestream.Write(buffer, offset: 0, count: bytesRead);
                }
#endif
            }
            else
            {
                Debug.LogError("Error decoding audio data.");
            }
        }

Mais dès que j’essaye de faire la même chose en WebGl… ça se complique…
Et j’ai beau tourner et retourner le code dans tous les sens, le mieux que j’obtiens, c’est un grésillement.

Si ça fonctionne bien sur toutes les platformes SAUF sur WebGL c’est probablement un bug ou un feature qui n’y est pas disponible au niveau de AudioClip. L’arsenal audio sur le web est moins garni à cause de l’implémentation différente de l’engin audio (aka pas FMOD) de cette plateforme…

Une fois le fichier WAV écrit et disponible localement, au lieu d’associer les data directement à un AudioClip, je suggère de le loader avec une UnityWebRequest pour être certain de bien passer dans la machine à saucisse et garantir une initialisation complète du fichier en tant que média audio du point de vue de l’engin.

using UnityEngine;
using UnityEngine.Networking;

public class LoadAudio : MonoBehaviour
{
    private AudioSource audioSource;

    void Start()
    {
        audioSource = GetComponent<AudioSource>();
        StartCoroutine(LoadAudioClip());
    }

    IEnumerator LoadAudioClip()
    {
        string url = "file:///path/to/your/file.wav";

        using (UnityWebRequest www = UnityWebRequestMultimedia.GetAudioClip(url, AudioType.WAV))
        {
            yield return www.SendWebRequest();

            if (www.result == UnityWebRequest.Result.ConnectionError)
            {
                Debug.LogError("Failed to load audio clip: " + www.error);
            }
            else
            {
                AudioClip audioClip = DownloadHandlerAudioClip.GetContent(www);
                audioSource.clip = audioClip;
            }
        }
    }
}

Je viens de faire un test avec un lien de dl retourné par le debug:

Log:
Fichier → /idbfs/9e931b3a7c2d452f5dd25408067fb02f/audio_20231017183615.wav

LogError:
GET http://localhost:52106/idbfs/9e931b3a7c2d452f5dd25408067fb02f/audio_20231017183615.wav

InvalidOperationException: HTTP/1.1 404 Not Found
at UnityEngine.Networking.DownloadHandler.GetCheckedDownloader[T] (UnityEngine.Networking.UnityWebRequest www) [0x00000] in <00000000000000000000000000000000>:0

avec ce code:

        private void CreateAudioClip(byte[] audioData)
        {
            filePath = Path.Combine(Application.persistentDataPath, "audio_" + DateTime.Now.ToString("yyyyMMddHHmmss") + ".wav");
            Debug.Log("Fichier --> "+ filePath);
           
            AudioConverter.ConvertToWav(audioData, filePath, 44100, 16, 1);
            StartCoroutine(LoadAudioClip());
           
        }
        IEnumerator LoadAudioClip()
        {
            using UnityWebRequest www = UnityWebRequestMultimedia.GetAudioClip(filePath, AudioType.WAV);
            yield return www.SendWebRequest();

            if (www.result == UnityWebRequest.Result.ConnectionError)
            {
                Debug.LogError("Failed to load audio clip: " + www.error);
            }
            else
            {
                AudioClip audioClip = DownloadHandlerAudioClip.GetContent(www);
                audioClips.Add(audioClip);
            }
        }

Et celui ci:

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

namespace Flippit
{
   
    public class AudioConverter
    {
       
        public static AudioClip ConvertToWav(byte[] audioData, string outputPath, int sampleRate, int bitsPerSample, int channels)
        {
            int numSamples = audioData.Length / (bitsPerSample / 8);
            float[] audioSamples = new float[numSamples];

            // Convertissez les données audio brutes en float[]
            for (int i = 0; i < numSamples; i++)
            {
                audioSamples[i] = BitConverter.ToInt16(audioData, i * (bitsPerSample / 8)) / 32768f;
            }
            AudioClip clip = AudioClip.Create("MyAudioClip", numSamples, channels, sampleRate, false);

            using FileStream fileStream = new(outputPath, FileMode.Create);
            using BinaryWriter writer = new(fileStream);

            writer.Write(new char[] { 'R', 'I', 'F', 'F' });
            writer.Write(audioData.Length + 36);

            writer.Write(new char[] { 'W', 'A', 'V', 'E' });
            writer.Write(new char[] { 'f', 'm', 't', ' ' });
            writer.Write(16);
            writer.Write((ushort)1);
            writer.Write((ushort)channels);
            writer.Write(sampleRate);
            writer.Write((ushort)bitsPerSample);
            writer.Write((ushort)(bitsPerSample / 8 * channels));
            writer.Write((ushort)bitsPerSample);
            writer.Write(new char[] { 'd', 'a', 't', 'a' });
            writer.Write(audioData.Length);
            writer.Write(audioData);

            clip.SetData(audioSamples, 0);

            return clip;
        }
    }
}

C’est déjà bien d’avoir réussi à compiler, mais je n’ai plus du tout le grésillement que j’avais avec l’autre méthode(foireuse) de conversion.

Excellente nouvelle!

Peut-être qu’il faudrait ajouter un System.IO.File.Exists(filePath) quelque part pour attendre que le fichier soit dispo. Il me semble que le data devrait être accessible, mais avec la nature asynchrone du web et du javascript qui sert de backend ici, le code du script est peut-être “trop rapide”.

trop bizare…
j’ai mis la condition:
if(File.Exists(filePath)) StartCoroutine(LoadAudioClip());

Donc il est pas censé allez nulle part si y’a pas de fichier…

et pourtant il continue, donc le fichier existe:

UnityWebRequest.Result.Success → prochaine etape Get www

mais…

Webgl.loader.js:1

GET http://localhost:55455/idbfs/31eec8c258efcef9036b1b85da00af25/audio_20231017191702.wav 404 (Not Found)

InvalidOperationException: HTTP/1.1 404 Not Found
at UnityEngine.Networking.DownloadHandler.GetCheckedDownloader[T] (UnityEngine.Networking.UnityWebRequest www) [0x00000] in <00000000000000000000000000000000>:0

J’ai tenté file:// mais le navigateur intedit l’acces…
On va repenser cette partie là.

C’est peut-être un probleme de CORS. Qu’est-ce que ça donne avec Chrome si c’est démarré comme ça: chrome.exe --disable-web-security

j’ai pas l’impression que ça aide, au contraire…
maintenant il me dit:

Build completed with a result of ‘Succeeded’ in 39 seconds (39410 ms)
UnityEditor.BuildPlayerWindow:BuildPlayerAndRun ()

Sauf qu’aucun jeu ne s’ouvre dans aucun navigateur…

Normalement je ne pense pas qu’on peut faire Build And Run pour ce scénario. Il faut Build, puis aller dans le dossier en question, lancer un serveur pour distribuer le build (python -m http.server, par exemple), démarrer Chrome avec l’argument en question puis aller à l’adresse du serveur.

J’ai un bug mentionnant Get/SetData sur WebGL sur lequel je dois travailler dans les prochains jours, je penserai à ce problème en regardant ce code.

This code worked however the clip says it really fast. Did you find a solution overall for this problem?

I changed the audioSource.pitch = 0.5f; and that works.