web-dev-qa-db-fra.com

Java - lecture, manipulation et écriture de fichiers WAV

Dans un programme Java, quelle est la meilleure façon de lire un fichier audio ( WAV fichier) dans un tableau de nombres (float[], short[], ...), et pour écrire un fichier WAV à partir d'un tableau de nombres?

19
yonatan

Je lis les fichiers WAV via un AudioInputStream. L'extrait suivant des Java Sound Tutorials fonctionne bien.

int totalFramesRead = 0;
File fileIn = new File(somePathName);
// somePathName is a pre-existing string whose value was
// based on a user selection.
try {
  AudioInputStream audioInputStream = 
    AudioSystem.getAudioInputStream(fileIn);
  int bytesPerFrame = 
    audioInputStream.getFormat().getFrameSize();
    if (bytesPerFrame == AudioSystem.NOT_SPECIFIED) {
    // some audio formats may have unspecified frame size
    // in that case we may read any amount of bytes
    bytesPerFrame = 1;
  } 
  // Set an arbitrary buffer size of 1024 frames.
  int numBytes = 1024 * bytesPerFrame; 
  byte[] audioBytes = new byte[numBytes];
  try {
    int numBytesRead = 0;
    int numFramesRead = 0;
    // Try to read numBytes bytes from the file.
    while ((numBytesRead = 
      audioInputStream.read(audioBytes)) != -1) {
      // Calculate the number of frames actually read.
      numFramesRead = numBytesRead / bytesPerFrame;
      totalFramesRead += numFramesRead;
      // Here, do something useful with the audio data that's 
      // now in the audioBytes array...
    }
  } catch (Exception ex) { 
    // Handle the error...
  }
} catch (Exception e) {
  // Handle the error...
}

Pour écrire un WAV, j'ai trouvé cela assez délicat. En surface, cela semble être un problème circulaire, la commande qui écrit repose sur un AudioInputStream comme paramètre.

Mais comment écrivez-vous des octets dans un AudioInputStream? Ne devrait-il pas y avoir un AudioOutputStream?

Ce que j'ai trouvé, c'est que l'on peut définir un objet qui a accès aux données d'octets audio brutes pour implémenter TargetDataLine.

Cela nécessite la mise en œuvre de nombreuses méthodes, mais la plupart peuvent rester sous forme fictive car elles ne sont pas nécessaires pour écrire des données dans un fichier. La méthode clé à implémenter est read(byte[] buffer, int bufferoffset, int numberofbytestoread).

Comme cette méthode sera probablement appelée plusieurs fois, il devrait également y avoir une variable d'instance qui indique dans quelle mesure les données ont progressé, et la mettre à jour dans le cadre de la méthode read ci-dessus.

Lorsque vous avez implémenté cette méthode, votre objet peut être utilisé pour créer un nouveau AudioInputStream qui à son tour peut être utilisé avec:

AudioSystem.write(yourAudioInputStream, AudioFileFormat.WAV, yourFileDestination)

Pour rappel, un AudioInputStream peut être créé avec un TargetDataLine comme source.

En ce qui concerne la manipulation directe des données, j'ai eu un bon succès en agissant sur les données dans le tampon dans la boucle la plus interne de l'exemple d'extrait ci-dessus, audioBytes.

Pendant que vous êtes dans cette boucle interne, vous pouvez convertir les octets en nombres entiers ou flottants et multiplier une valeur volume (allant de 0.0 À 1.0), Puis les reconvertir en petits octets endiens.

Je crois que puisque vous avez accès à une série d'échantillons dans ce tampon, vous pouvez également engager diverses formes d'algorithmes de filtrage DSP à ce stade. D'après mon expérience, j'ai trouvé qu'il est préférable de faire des changements de volume directement sur les données dans ce tampon, car vous pouvez alors faire le plus petit incrément possible: un delta par échantillon, minimisant le risque de clics en raison de discontinuités induites par le volume.

Je trouve que les "lignes de contrôle" pour le volume fournies par Java ont tendance à des situations où les sauts de volume provoqueront des clics, et je pense que c'est parce que les deltas ne sont implémentés qu'à la granularité d'un seul lecture du tampon (souvent dans la plage d'un changement par 1024 échantillons) plutôt que de diviser le changement en morceaux plus petits et de les ajouter un par échantillon. Mais je ne suis pas au courant de la façon dont les contrôles de volume ont été mis en œuvre, veuillez donc prendre cette conjecture avec un grain de sel.

Dans l'ensemble, Java.Sound a été un vrai casse-tête à comprendre. Je reproche au Tutoriel de ne pas inclure d'exemple explicite d'écriture d'un fichier directement à partir d'octets. Je reproche au tutoriel d'enterrer le meilleur exemple de codage de lecture d'un fichier dans la section "Comment convertir ...". Cependant, il y a BEAUCOUP d'informations précieuses GRATUITES dans ce tutoriel.


EDIT: 13/12/17

Depuis, j'ai utilisé le code suivant pour écrire de l'audio à partir d'un fichier PCM dans mes propres projets. Au lieu d'implémenter TargetDataLine, on peut étendre InputStream et l'utiliser comme paramètre de la méthode AudioInputStream.write.

public class StereoPcmInputStream extends InputStream
{
    private float[] dataFrames;
    private int framesCounter;
    private int cursor;
    private int[] pcmOut = new int[2];
    private int[] frameBytes = new int[4];
    private int idx;

    private int framesToRead;

    public void setDataFrames(float[] dataFrames)
    {
        this.dataFrames = dataFrames;
        framesToRead = dataFrames.length / 2;
    }

    @Override
    public int read() throws IOException
    {
        while(available() > 0)
        {
            idx &= 3; 
            if (idx == 0) // set up next frame's worth of data
            {
                framesCounter++; // count elapsing frames

                // scale to 16 bits
                pcmOut[0] = (int)(dataFrames[cursor++] * Short.MAX_VALUE);
                pcmOut[1] = (int)(dataFrames[cursor++] * Short.MAX_VALUE);

                // output as unsigned bytes, in range [0..255]
                frameBytes[0] = (char)pcmOut[0];
                frameBytes[1] = (char)(pcmOut[0] >> 8);
                frameBytes[2] = (char)pcmOut[1];
                frameBytes[3] = (char)(pcmOut[1] >> 8);

            }
            return frameBytes[idx++]; 
        }
        return -1;
    }

    @Override 
    public int available()
    {
        // NOTE: not concurrency safe.
        // 1st half of sum: there are 4 reads available per frame to be read
        // 2nd half of sum: the # of bytes of the current frame that remain to be read
        return 4 * ((framesToRead - 1) - framesCounter) 
                + (4 - (idx % 4));
    }    

    @Override
    public void reset()
    {
        cursor = 0;
        framesCounter = 0;
        idx = 0;
    }

    @Override
    public void close()
    {
        System.out.println(
            "StereoPcmInputStream stopped after reading frames:" 
                + framesCounter);
    }
}

Les données source à exporter ici se présentent sous la forme de flottants stéréo allant de -1 à 1. Le format du flux résultant est 16 bits, stéréo, petit-boutiste.

J'ai omis les méthodes skip et markSupported pour mon application particulière. Mais il ne devrait pas être difficile de les ajouter s'ils sont nécessaires.

10
Phil Freihofner

Un peu plus de détails sur ce que vous aimeriez réaliser serait utile. Si les données WAV brutes vous conviennent, utilisez simplement un FileInputStream et probablement un scanner pour les transformer en chiffres. Mais permettez-moi d'essayer de vous donner un exemple de code significatif pour vous aider à démarrer:

Il existe une classe appelée com.Sun.media.sound.WaveFileWriter à cet effet.

InputStream in = ...;
OutputStream out = ...;

AudioInputStream in = AudioSystem.getAudioInputStream(in);

WaveFileWriter writer = new WaveFileWriter();
writer.write(in, AudioFileFormat.Type.WAVE, outStream);

Vous pouvez implémenter votre propre AudioInputStream qui fait tout ce qui va vaudou pour transformer vos tableaux de numéros en données audio.

writer.write(new VoodooAudioInputStream(numbers), AudioFileFormat.Type.WAVE, outStream);

Comme @ stacker mentionné, vous devez bien sûr vous familiariser avec l'API.

8
sfussenegger

Le package javax.sound.sample n'est pas adapté au traitement des fichiers WAV si vous devez avoir accès aux valeurs d'échantillonnage réelles. Le package vous permet de modifier le volume, la fréquence d'échantillonnage, etc., mais si vous voulez d'autres effets (par exemple, ajouter un écho), vous êtes seul. (Le didacticiel Java indique qu'il devrait être possible de traiter directement les valeurs d'échantillon, mais l'auteur de la technologie a fait des concessions.)

Ce site a une classe simple pour le traitement des fichiers WAV: http://www.labbookpages.co.uk/audio/javaWavFiles.html

6
cayhorstmann

Spécification du fichier WAV https://ccrma.stanford.edu/courses/422/projects/WaveFormat/

Il existe une API pour votre but http://code.google.com/p/musicg/

5
c'quet

Il s'agit du code source pour écrire directement dans un fichier wav. Il vous suffit de connaître les mathématiques et l'ingénierie du son pour produire le son que vous souhaitez. Dans cet exemple, l'équation calcule un battement binaural.

import Java.io.ByteArrayInputStream;
import Java.io.File;
import Java.io.IOException;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;

public class Example 
{
    public static void main(String[] args) throws  IOException {

    double sampleRate = 44100.0;
    double frequency = 440;
    double frequency2 = 90;
    double amplitude = 1.0;
    double seconds = 2.0;
    double twoPiF = 2 * Math.PI * frequency;
    double piF = Math.PI * frequency2;
    float[] buffer = new float[(int) (seconds * sampleRate)];
    for (int sample = 0; sample < buffer.length; sample++) 
    {
        double time = sample / sampleRate;
        buffer[sample] = (float) (amplitude * Math.cos((double)piF *time)* Math.sin(twoPiF * time));
    }
    final byte[] byteBuffer = new byte[buffer.length * 2];
    int bufferIndex = 0;
    for (int i = 0; i < byteBuffer.length; i++) {
    final int x = (int) (buffer[bufferIndex++] * 32767.0);
    byteBuffer[i] = (byte) x;
    i++;
    byteBuffer[i] = (byte) (x >>> 8);
    }
    File out = new File("out10.wav");
    boolean bigEndian = false;
    boolean signed = true;
    int bits = 16;
    int channels = 1;
    AudioFormat format;
    format = new AudioFormat((float)sampleRate, bits, channels, signed, bigEndian);
    ByteArrayInputStream bais = new ByteArrayInputStream(byteBuffer);
    AudioInputStream audioInputStream;
    audioInputStream = new AudioInputStream(bais, format,buffer.length);
    AudioSystem.write(audioInputStream, AudioFileFormat.Type.WAVE, out);
    audioInputStream.close();
    }

}

Si vous pouviez modifier cela pour créer une sous-basse hip hop qui serait cool parce que c'est actuellement ce que j'essaie de modifier ce programme.

4
Joshua Beckford

Les fichiers Wave sont pris en charge par le package javax.sound.sample

Comme ce n'est pas une API triviale, vous devriez lire un article/tutoriel qui présente l'API comme

Java Sound, une introduction

2
stacker

Tout d'abord, vous devrez peut-être connaître les en-têtes et les positions des données d'une structure WAVE, vous pouvez trouver la spécification ici . Sachez que les données sont peu endiennes.

Il y a un API qui peut vous aider à atteindre votre objectif.

2
user1039358