Página 19 de 20

Re: Primeros pasos para pogramar un emulador

Publicado: 30 Jul 2022, 01:16
por ZX81
Por ejemplo con este código que hace sonar cuatro pitidos por el altavoz (300Hz):

Código: Seleccionar todo

package musica;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.SourceDataLine;

/**
 *
 * @author char_
 */
public class Musica {
 
    private static AudioFormat format = null;
    private static SourceDataLine line = null;
    private static final int SAMPLE_RATE = 22050; // num muestras por segundo
    private static final double MAX_AMPLITUD = 32760; // Máx volúmen altavoz
    private static final int MIN_FREQ = 250;

    public static void main(String[] args) {
        createOutput();
        play();
    }

    //Inicializamos audio
    private static void createOutput() {
        format = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,
                SAMPLE_RATE, 16, 2, 4, SAMPLE_RATE, false);
        System.out.println("Audio format: " + format);
        try {
            DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
            if (!AudioSystem.isLineSupported(info)) {
                System.out.println("Line does not support: " + format);
                System.exit(0);
            }
            line = (SourceDataLine) AudioSystem.getLine(info);
            line.open(format);
        } catch (Exception e) {
            System.out.println(e.getMessage());
            System.exit(0);
        }
    }

    private static void play() {
        //Calculamos el tamaño de buffer
        int maxSize = (int) Math.round((SAMPLE_RATE * format.getFrameSize()) / MIN_FREQ);
        //Cada frame tiene 4 bytes
        byte[] samples = new byte[maxSize];
        line.start();
        //Enviamos sonido al altavoz con una frecuencia de 300Hz 4 veces
        for (int n = 0; n < 4; n++) {
            sendNote(300, samples);
        }
        //Cerramos la línea
        line.drain();
        line.stop();
        line.close();
    }

    //Generamos y reproducimos la onda senoidal
    private static void sendNote(int freq, byte[] samples) {
        int numMuestrasDentroDeLaOnda = (int) Math.round(((double) SAMPLE_RATE) / freq);
        int idx = 0;
        //Creamos la onda senoidal
        for (int i = 0; i < numMuestrasDentroDeLaOnda; i++) {
            double sine = Math.sin(((double) i / numMuestrasDentroDeLaOnda) * 2.0 * Math.PI);
            int sample = (int) (sine * MAX_AMPLITUD);
            // Canal izquierdo
            samples[idx + 0] = (byte) (sample & 0xFF); // low byte
            samples[idx + 1] = (byte) ((sample >> 8) & 0xFF); // high byte
            // Canal derecho
            samples[idx + 2] = (byte) (sample & 0xFF);
            samples[idx + 3] = (byte) ((sample >> 8) & 0xFF);
            idx += 4;
        }
        // Reproduce el sonido
        int offset = 0;
        long t1 = System.currentTimeMillis();
        while (offset < idx) {
            offset += line.write(samples, offset, idx - offset);
        }
        long t2 = System.currentTimeMillis();
        System.out.println("Tiempo en pausa por reproducción del audio " + (t2 - t1) + " ms");
    }
}


Obtengo este resultado:
Audio format: PCM_SIGNED 22050.0 Hz, 16 bit, stereo, 4 bytes/frame, little-endian
Tiempo en pausa por reproducción del audio 32 ms
Tiempo en pausa por reproducción del audio 0 ms
Tiempo en pausa por reproducción del audio 0 ms
Tiempo en pausa por reproducción del audio 0 ms
BUILD SUCCESSFUL (total time: 0 seconds)


¿ No se supone que si llamo 4 veces a la función "sendNote", cada vez que se reproduce el audio tendría que pausarse el programa mientras dura la reproducción del sonido unos 3,3ms ? Y tendría que mostrar algo como esto:

Audio format: PCM_SIGNED 22050.0 Hz, 16 bit, stereo, 4 bytes/frame, little-endian
Tiempo en pausa por reproducción del audio 3 ms
Tiempo en pausa por reproducción del audio 3 ms
Tiempo en pausa por reproducción del audio 3 ms
Tiempo en pausa por reproducción del audio 3 ms
BUILD SUCCESSFUL (total time: 0 seconds)

Gracias y a ver si alguien me puede echar un cable. :rezo

Re: Primeros pasos para pogramar un emulador

Publicado: 30 Jul 2022, 09:43
por robcfg
Lo que estás midiendo es el tiempo que tardas en escribir los datos en la linea, no el tiempo de reproducción de audio.

Re: Primeros pasos para pogramar un emulador

Publicado: 30 Jul 2022, 14:08
por ZX81
robcfg escribió:Lo que estás midiendo es el tiempo que tardas en escribir los datos en la linea, no el tiempo de reproducción de audio.

Ops! No lo sabia. =D>
Entonces sabes cómo se puede averiguar cuando ha dejado de reproducirse un frame? Hay algún método?

Gracias.

Re: Primeros pasos para pogramar un emulador

Publicado: 30 Jul 2022, 15:30
por robcfg
Pues deberías mirar cómo funciona la librería que usas para el audio.

Lo más probable es que una vez que cierres o hagas un flush de la linea, lo siguiente ocurra en otro hilo aparte del que ejecuta tu aplicación.

Pero lo principal es conocer cómo funciona la librería que usas y a partir de ahí haces tus cábalas.

Re: Primeros pasos para pogramar un emulador

Publicado: 30 Jul 2022, 16:59
por ZX81
robcfg escribió:Pues deberías mirar cómo funciona la librería que usas para el audio.

Lo más probable es que una vez que cierres o hagas un flush de la linea, lo siguiente ocurra en otro hilo aparte del que ejecuta tu aplicación.

Pero lo principal es conocer cómo funciona la librería que usas y a partir de ahí haces tus cábalas.


Hola,
Probaré con el método "drain" a ver si funciona, según este texto que he encontrado en la documentación oficial.

Dado que write regresa antes de que todos los datos hayan terminado de reproducirse, ¿cómo sabe cuándo se completó realmente la reproducción? Una forma es invocar el drain método de DataLine después de escribir los datos del último búfer. Este método bloquea hasta que se han reproducido todos los datos.

Gracias de nuevo.

Re: Primeros pasos para pogramar un emulador

Publicado: 07 Ago 2022, 13:22
por ZX81
Hola a todos,
Acabo de probar a ver si con el método "drain" me hacia una temporización correcta de unos 20ms al terminar de reproducirse el sonido, pero veo que siempre me hace una pausa entre 30 y 55ms, por lo que el emulador se ralentiza un montón.

He echado un vistazo al emulador JSpeccy para ver como éste hace la pausa de 20ms, y lo que me he encontrado es que en la clase "Audio" sólo hace uso del método "write" para enviar los datos del buffer a la tarjeta de sonido y reproducirlos, pero no veo como hace que mediante la tarjeta de sonido se haga una pausa de 20ms. Vamos, que no hace uso del método "drain" para que se detenga el hilo hasta que se termine de reproducir el audio.

Código: Seleccionar todo

    synchronized public void sendAudioFrame() {
        if (line != null)
            line.write(buf, 0, frameSize);
    }


Así que la verdad no tengo ni idea de como puedo hacer que mediante la tarjeta de sonido pueda hacer una pausa de 20ms para que mi emulador funcione a una velocidad correcta, lo he intentado todo y no sé que se me escapa.

A ver si alguien que haya programado algún emulador, o sepa del tema me pudiese dar una idea de como se puede hacer esto, le estaría enormemente agradecido. Ya he mirado foros y foros, y no veo ninguna información del tema que pudiese aclararme algo sobre este tema.

Gracias. :D

Re: Primeros pasos para pogramar un emulador

Publicado: 07 Ago 2022, 13:49
por ZX-81
La idea es que el sistema de audio tiene un buffer que es configurable, si no recuerdo mal, el doble de grande en Linux/Unix que en Windows. No hay que llamar a drain, simplemente cuando el buffer de audio se llena, la llamada a write se bloquea hasta que hay sitio de nuevo.

En la práctica, los dos primeros frames se ejecutan a toda leche, pero un usuario normal no lo nota, al tercer frame la llamada a write se bloquea y se desbloquea cuando hay suficiente sitio en el buffer para el frame del tercer cuadro. A partir de ahí, es el bloqueo/desbloqueo del write lo que proporciona la temporización, porque es la tarjeta de sonido la que regula la velocidad del emulador. A condición, eso sí, de que generes las muestras exactas que necesitas para cada frame, como eso no lo hagas bien el emulador irá a tirones.

Otra cosa es que nececites, además, un método de temporización para cuando el usuario desactiva el sonido o el dispositivo de sonido no existe o no está disponible por la razón que sea. Se puede usar un scheduleAtFixedRate, pero ese sistema no es tan preciso como el de la tarjeta de sonido. Si tú programas una tarea cada 20ms, siempre habrá una pequeñísima deriva, que proviene de la JVM y del scheduling del S.O., por eso no vale este sistema si hay sonido, ya que esa deriva provoca que al final el buffer de sonido se vacíe y empieces a escuchar ruidos.

En algún podcast lo he dicho y lo repito aquí: el sonido es una p*t*d* porque es lo único que necesita tiempo real, sin fallos.

En ZXBaremulator no uso ese sistema porque sí tengo un timer que me salta de manera precisa cuando lo necesito y así el buffer nunca llega a vaciarse del todo, pero claro, estoy haciendo labores más propias de un S.O. que de una aplicación.

Ten paciencia porque yo tardé meses, muchos, en tener algo que sonara decente. Que sonara bien... mejor no te lo cuento.

Re: Primeros pasos para pogramar un emulador

Publicado: 07 Ago 2022, 14:17
por ZX81
Hola,
Lo primero gracias por contestar con tanta celeridad.

ZX-81 escribió:En la práctica, los dos primeros frames se ejecutan a toda leche, pero un usuario normal no lo nota, al tercer frame la llamada a write se bloquea y se desbloquea cuando hay suficiente sitio en el buffer para el frame del tercer cuadro. A partir de ahí, es el bloqueo/desbloqueo del write lo que proporciona la temporización, porque es la tarjeta de sonido la que regula la velocidad del emulador. A condición, eso sí, de que generes las muestras exactas que necesitas para cada frame, como eso no lo hagas bien el emulador irá a tirones.
.


Esto que comentas no me funciona, a cada llamada que hago a "write" me da una temporización diferente:
Temporización=125 ms
Temporización=1 ms
Temporización=0 ms
Temporización=0 ms
Temporización=0 ms
Temporización=0 ms
Temporización=0 ms
Temporización=0 ms
Temporización=0 ms
Temporización=125 ms
Temporización=0 ms

La primera vez que llamo a "write" me da un tiempo de 125ms, que será cuando pasan los datos a la tarjeta de sonido, y el resto de llamadas da 0ms, luego se vuelve a repetir el ciclo. Pero cuando hay 0ms de pausa es cuando el emulador va a toda velocidad, y en este caso es cuando se muestran las imágenes que van a trompicones, como si faltasen cuadros por mostrar, sin embargo si pongo una pausa con un Thread.Sleep a 20ms, las imágenes se muestran suaves y no se pierde ningún cuadro.

Las muestras que se generan en mi emulador están entre 957 y 958, no llegan a 960 cada 73 T-States, pero no creo que esto influya en que no hace una pausa aproximada a 20ms.

Resumiendo, cuando llamo al método "write" no me hace la pausa de 20ms y no tengo ni idea del porque pasa. No sé si se podría mirar alguna cosa más. :D

Gracias.

Re: Primeros pasos para pogramar un emulador

Publicado: 07 Ago 2022, 14:29
por ZX-81
Me pillas fuera de casa, y un poco complicado para mirar los fuentes de mi propio emulador. Pero una cosa que hay que hacer es configurar ese buffer al tamaño que necesites. Yo creo que se debe hacer en Audio.java, pero no te lo juraría ahora mismo.

Generar los frames exactos es fundamental, al menos por cada segundo de ejecución. Si necesitas 960 samples por frame, no pasa nada si un frame generas 959 y al siguiente 961, lo que importa es que generes los 48.000 samples por segundo. Ya te comenté que ajustarse a 73 t-states/sample supone ajustar todos los tiempos del emulador, incluído el sample rate.

A ver si tengo un rato y miro el repo de JSpeccy desde el portátil de la empresa. No es que me haga mucha gracia, la verdad, pero confío en que no me despidan por ello... :roll:

Re: Primeros pasos para pogramar un emulador

Publicado: 08 Ago 2022, 17:28
por ZX81
ZX-81 escribió:A ver si tengo un rato y miro el repo de JSpeccy desde el portátil de la empresa. No es que me haga mucha gracia, la verdad, pero confío en que no me despidan por ello... :roll:


Espero que no jejeje... :mrgreen: