Java: leer, manipular y escribir archivos WAV
audio (8)
El paquete javax.sound.sample no es adecuado para procesar archivos WAV si necesita tener acceso a los valores de muestra reales. El paquete le permite cambiar el volumen, la frecuencia de muestreo, etc., pero si desea otros efectos (por ejemplo, agregar un eco), usted está solo. (El tutorial de Java insinúa que debería ser posible procesar los valores de muestra directamente, pero el escritor técnico prometió demasiado).
Este sitio tiene una clase simple para procesar archivos WAV: http://www.labbookpages.co.uk/audio/javaWavFiles.html
En un programa Java, ¿cuál es la mejor manera de leer un archivo de audio (archivo WAV ) en una matriz de números ( float[]
, short[]
, ...) y escribir un archivo WAV a partir de una matriz de números?
Leo archivos WAV a través de AudioInputStream. El siguiente fragmento de los Tutoriales de Java Sound funciona 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...
}
Para escribir un WAV, encontré eso bastante complicado. En la superficie, parece un problema circular, el comando que escribe depende de un AudioInputStream como parámetro. Pero, ¿cómo se escriben los bytes en un AudioInputStream? ¿No debería haber un AudioOutputStream?
Lo que encontré fue que uno puede definir un objeto que tenga acceso a los datos de bytes de audio en bruto para implementar TargetDataLine. Esto requiere la implementación de muchos métodos, pero la mayoría puede permanecer en forma ficticia ya que no son necesarios para escribir datos en un archivo. El método clave para implementar es "read (byte [] buffer, int bufferoffset, int numberofbytestoread)". Como este método probablemente se invoque varias veces, también debe haber una variable de instancia que indique qué tan lejos avanzaron los datos, y actualizar eso como parte del método de "lectura" anterior.
Si puede implementar este método, su objeto se puede usar en la creación de un nuevo AudioInputStream que a su vez se puede usar con:
AudioSystem.write(yourAudioInputStream, AudioFileFormat.WAV, yourFileDestination)
Como recordatorio, se puede crear un AudioInputStream con una TargetDataLine como fuente.
En cuanto a la manipulación directa de los datos, he tenido éxito actuando sobre los datos en el búfer en el bucle más interno del ejemplo de fragmento anterior, "audioBytes". Mientras se encuentra en ese bucle interno, puede convertir los bytes a ints o flotantes y multiplicar un valor de "volumen" (que va de 0.0 a 1.0) y luego convertirlos de nuevo a bytes de endian pequeños. Creo que dado que uno tiene acceso a una serie de muestras en ese búfer, también se puede utilizar varias formas de algoritmos de filtrado DSP en esa etapa. Diré, me parece que es mejor hacer cambios de volumen directamente en los datos en este búfer porque puede hacer el incremento más pequeño posible: un delta por muestra, minimizando la posibilidad de clics debido a discontinuidades inducidas por el volumen. Encuentro que las "líneas de control" para el volumen proporcionado por Java tienden a situaciones donde los saltos en el volumen causarán clics, y creo que esto se debe a que los deltas solo se implementan en la granularidad de una sola lectura del búfer (a menudo en el rango de uno cambio por 1024 muestras) en lugar de dividir el cambio en piezas más pequeñas y agregarlas una por muestra. Pero no soy del todo privvy de cómo se implementaron los Controles de Volumen, así que por favor, tome esa conjetura con un grano de sal.
En general, Java.Sound ha sido un verdadero dolor de cabeza para descubrir. Me falla el tutorial por no incluir un ejemplo explícito de escribir un archivo directamente desde bytes. Me falla el tutorial para enterrar el mejor ejemplo de la codificación Reproducir un archivo en la sección "Cómo convertir ...". Sin embargo, hay MUCHAS informaciones valiosas GRATIS en ese tutorial por las cuales estoy muy agradecido. Ah, la vida de un programador. Los foros como estos y las personas amables y serviciales han sido tremendos, y eso es parte de lo que es tan grandioso sobre Java, esta comunidad.
PD, Kay, gracias por un libro increíble! Consulto Core Java tan regularmente como consulto los Tutoriales.
En primer lugar, es posible que necesite conocer los encabezados y las posiciones de datos de una estructura WAVE, aquí puede encontrar las especificaciones. Tenga en cuenta que los datos son poco endian.
Hay una API que puede ayudarte a lograr tu objetivo.
Especificación de archivo WAV https://ccrma.stanford.edu/courses/422/projects/WaveFormat/
Hay una API para su propósito http://code.google.com/p/musicg/
Los archivos Wave son compatibles con el paquete javax.sound.sample
Como no es una API trivial, debes leer un artículo / tutorial que introduce la API como
Yo uso FileInputStream
con algo de magia:
byte[] byteInput = new byte[(int)file.length() - 44];
short[] input = new short[(int)(byteInput.length / 2f)];
try{
FileInputStream fis = new FileInputStream(file);
fis.read(byteInput, 44, byteInput.length - 45);
ByteBuffer.wrap(byteInput).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(input);
}catch(Exception e ){
e.printStackTrace();
}
¡Sus valores de muestra están en short[] input
!
Este es el código fuente para escribir directamente en un archivo wav. Solo necesita saber las matemáticas y la ingeniería de sonido para producir el sonido que desea. En este ejemplo, la ecuación calcula un ritmo 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 pudieras modificar esto para crear un subgraves de hip hop, sería fantástico porque eso es lo que intento modificar este programa en la actualidad.
Un poco más de detalle sobre lo que le gustaría lograr sería útil. Si los datos RAW en bruto son correctos para usted, simplemente use un FileInputStream y probablemente un Escáner para convertirlo en números. Pero déjame intentar darte un código de muestra significativo para que comiences:
Hay una clase llamada com.sun.media.sound.WaveFileWriter para este propósito.
InputStream in = ...;
OutputStream out = ...;
AudioInputStream in = AudioSystem.getAudioInputStream(in);
WaveFileWriter writer = new WaveFileWriter();
writer.write(in, AudioFileFormat.Type.WAVE, outStream);
Podrías implementar tu propio AudioInputStream que haga lo que sea para convertir tus matrices de números en datos de audio.
writer.write(new VoodooAudioInputStream(numbers), AudioFileFormat.Type.WAVE, outStream);
Como mencionó @stacker , debes familiarizarte con la API, por supuesto.