Primeros pasos para pogramar un emulador

Foro dedicado a la emulación de sistemas clásicos en el PC o en otros sistemas.
ZX81
MSX Turbo R
MSX Turbo R
Mensajes: 487
Registrado: 20 Abr 2005, 19:18
Gracias dadas: 3 veces

Re: Primeros pasos para pogramar un emulador

Mensajepor ZX81 » 30 Jul 2022, 01:16

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
10 REM ESTA LINEA NO HACE NADA

Avatar de Usuario
robcfg
Amiga 2500
Amiga 2500
Mensajes: 2137
Registrado: 07 May 2009, 15:34
Sistema Favorito: Amstrad CPC
primer_sistema: Atari 800XL/600XL
Ubicación: Estocolmo
Gracias dadas: 847 veces
Gracias recibidas: 168 veces
Contactar:

Re: Primeros pasos para pogramar un emulador

Mensajepor robcfg » 30 Jul 2022, 09:43

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.

ZX81
MSX Turbo R
MSX Turbo R
Mensajes: 487
Registrado: 20 Abr 2005, 19:18
Gracias dadas: 3 veces

Re: Primeros pasos para pogramar un emulador

Mensajepor ZX81 » 30 Jul 2022, 14:08

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.
10 REM ESTA LINEA NO HACE NADA

Avatar de Usuario
robcfg
Amiga 2500
Amiga 2500
Mensajes: 2137
Registrado: 07 May 2009, 15:34
Sistema Favorito: Amstrad CPC
primer_sistema: Atari 800XL/600XL
Ubicación: Estocolmo
Gracias dadas: 847 veces
Gracias recibidas: 168 veces
Contactar:

Re: Primeros pasos para pogramar un emulador

Mensajepor robcfg » 30 Jul 2022, 15:30

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.

ZX81
MSX Turbo R
MSX Turbo R
Mensajes: 487
Registrado: 20 Abr 2005, 19:18
Gracias dadas: 3 veces

Re: Primeros pasos para pogramar un emulador

Mensajepor ZX81 » 30 Jul 2022, 16:59

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.
10 REM ESTA LINEA NO HACE NADA

ZX81
MSX Turbo R
MSX Turbo R
Mensajes: 487
Registrado: 20 Abr 2005, 19:18
Gracias dadas: 3 veces

Re: Primeros pasos para pogramar un emulador

Mensajepor ZX81 » 07 Ago 2022, 13:22

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
10 REM ESTA LINEA NO HACE NADA

ZX-81
Amstrad PCW 8256
Amstrad PCW 8256
Mensajes: 128
Registrado: 04 Ene 2013, 16:43
Sistema Favorito: Spectrum +2
primer_sistema: ZX81
consola_favorita: Nintendo DS/3DS
Primera consola: Sega Genesis/Megadrive
Ubicación: La orilla del mar Mediterráneo
Gracias dadas: 16 veces
Gracias recibidas: 27 veces
Contactar:

Re: Primeros pasos para pogramar un emulador

Mensajepor ZX-81 » 07 Ago 2022, 13:49

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.
Todo espacio de dimensión finita distinta de cero con producto interno tiene una base ortonormal. Tiene sentido, cuando no piensas sobre ello.
Profesor de Matemáticas U.C. Berkeley

Empieza a jugar sin tener que compilar: JSpeccy
Emulador bare-metal para la Raspberry PI 2/3: ZXBaremulator

ZX81
MSX Turbo R
MSX Turbo R
Mensajes: 487
Registrado: 20 Abr 2005, 19:18
Gracias dadas: 3 veces

Re: Primeros pasos para pogramar un emulador

Mensajepor ZX81 » 07 Ago 2022, 14:17

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.
10 REM ESTA LINEA NO HACE NADA

ZX-81
Amstrad PCW 8256
Amstrad PCW 8256
Mensajes: 128
Registrado: 04 Ene 2013, 16:43
Sistema Favorito: Spectrum +2
primer_sistema: ZX81
consola_favorita: Nintendo DS/3DS
Primera consola: Sega Genesis/Megadrive
Ubicación: La orilla del mar Mediterráneo
Gracias dadas: 16 veces
Gracias recibidas: 27 veces
Contactar:

Re: Primeros pasos para pogramar un emulador

Mensajepor ZX-81 » 07 Ago 2022, 14:29

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:
Todo espacio de dimensión finita distinta de cero con producto interno tiene una base ortonormal. Tiene sentido, cuando no piensas sobre ello.
Profesor de Matemáticas U.C. Berkeley

Empieza a jugar sin tener que compilar: JSpeccy
Emulador bare-metal para la Raspberry PI 2/3: ZXBaremulator

ZX81
MSX Turbo R
MSX Turbo R
Mensajes: 487
Registrado: 20 Abr 2005, 19:18
Gracias dadas: 3 veces

Re: Primeros pasos para pogramar un emulador

Mensajepor ZX81 » 08 Ago 2022, 17:28

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:
10 REM ESTA LINEA NO HACE NADA


Volver a “Emuladores”

¿Quién está conectado?

Usuarios navegando por este Foro: No hay usuarios registrados visitando el Foro y 9 invitados