Playback format for scenes

Started by Devilmarkus, 11:52, 16 September 10

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.


I open this thread now to ask:
We know the SNP format which is used in several emulators.
The negative aspect is, that the timings are not always accurate.

It stores the keystrokes / releases each VSync. But the playback is problematic for different emulators.

WinApe also provides the .SNR format which seem to be much more accurate.

Now I want to know:
- How does this format work exactly?
- Would it be easy to implement it into JavaCPC, too?
- How are the data's stored?
- Example codes?

It would be great if there would be a way to record / playback scenes in different emulators and also playback them accurate.
When you put your ear on a hot stove, you can smell how stupid you are ...

Amstrad CPC games in your webbrowser

JavaCPC Desktop Full Release


I think I posted some detailed info on this in another thread...


Yes indeed. You wrote:
Quote from: Executioner on 09:16, 02 June 10
Err... Not really complete. There is a document on the initial SNA Version 3 format here

And   then the format is very similar to the SNP format but with some extras.   I'll have to hunt for any documentation, or failing that analyse the   source code to create it.

Here's the headers from WinAPE which define the SNA V3 format:

  { TODO: Save Disc Image names, active ROMs, CPC Plus Raster Int flag }
  { TODO: PS2 Mouse X/Y and Button states in SNR }

  HeaderSNA: String = 'MV - SNA';
  HeaderSNP: String = 'MV - SNP';
  HeaderSNR: String = 'RW - SNR';



  SFormatInvalid: String = 'Snapshot format invalid';
  TSnapshotType = (SNA, SNR, SNP);

  TBankFlag = (c0, c4c7, cccf, d4d7, dcdf, e4e7, ecef, f4f7, fcff);
  TBankFlags = set of TBankFlag;

  TChunkFlag = ( cfROMs, cfDSCA, cfDSCB, cfBreaks, cfDataAreas );
  TChunkFlags = set of TChunkFlag;

  TSnapshotHeader = packed record
    SnapshotID: packed array[0..7] of char;
    pad0            : packed array[0..7] of char;

    Version: Byte;

    REG_R,REG_I,REG_IFF0,REG_IFF1: byte;
    REG_IM: byte;
    REG_AF1,REG_BC1,REG_DE1,REG_HL1: word;

    GA_Pen: byte;
    GA_Inks: packed array[0..16] of byte;
    GA_ROM, GA_RAM: byte;

    CRTC_Reg: byte;
    CRTC_Regs: packed array [0..17] of byte;

    UpperRom: byte;

    PIO_A,PIO_B,PIO_C,PIO_Control: byte;

    PSG_Reg: byte;
    PSG_Regs: packed array[0..15] of byte;

    MemSize: word;

    CPCType: byte;

    InterruptNo: byte;                     // CPCEMU only
    MultiMode: packed array[0..5] of byte; // CPCEMU only

    // Version 3 only
    Empty1: packed array[$75..$9a] of byte;

    FDD_Fast: Boolean;
    FDD_Motor: Boolean;
    FDD_Cyl: packed array[0..3] of byte;

    Printer_Data: byte;

    Monitor_Line: SmallInt;

    CRTC_Type: byte;

    Empty3: packed array [$a5..$a8] of byte;

    CRTC_HCC: byte;

    Empty4: byte;

    CRTC_VCC: byte;
    CRTC_VLC: byte;
    CRTC_VTAC: byte;
    CRTC_HSYNC: byte;
    CRTC_VSYNC: byte;
    CRTC_Flags: word;

    GA_DelayCount: byte;
    GA_ScanCount: byte;
    Z80_IntStat: byte;
    Z80_ICSR: byte;

    DisablePlus: Boolean;
    PlusPPI: Boolean;
    Empty5: packed array [$b8..$df] of byte;

    EmulatorID: packed array [$e0..$ff] of char;

  TDMARegs = packed record
    Loop: Word;
    Addr: Word;
    Pause: Word;
    PreScale: byte;

  TPlusChunk = packed record
    SpriteData: packed array [0..$7ff] of byte;
    SpriteAtts: packed array [0..$7f] of byte;
    Palette: packed array [0..$3f] of byte;
    Regs: packed array [0..5] of byte;
    SecROM: Word;
    Analogue: packed array [$08..$0f] of byte;
    DMA: packed array [0..$f] of byte;
    DMARegs: packed array [0..2] of TDMARegs;
    GA_Last: byte;
    GA_UnLocked: byte;
    GA_Sequence: byte;

The SNR file can contain RIFF sections defining the Disk images and ROM   images active at the time of recording.
The   SNR file ends with an SNR RIFF chunk (ID: 'SNR '). This contains all   the keystrokes and timing information as well as Symbiface Mouse and RTC   information. The following constants are used to define the ID's of   these:

  { Mouse Bits for SNR recording }
  SNR_MOUSE = $80;
  SNR_MB    = $01;
  SNR_MX    = $60;
  SNR_MX_B  = $20;
  SNR_MX_W  = $40;
  SNR_MY    = $18;
  SNR_MY_B  = $08;
  SNR_MY_W  = $10;
  SNR_MW    = $06;
  SNR_MW_B  = $02;
  SNR_MW_W  = $04;

  { Real-Time Clock IDs }
  SNR_RTC   = $E0;
  SNR_MIN   = $E1;
  SNR_HR    = $E2;
  SNR_DAY   = $E3;
  SNR_MTH   = $E4;
  SNR_YR    = $E5;
  SNR_MIL   = $E6;

The   actual SNR format is quite complicated due to limitations of the   original SNP format and the work-arounds to get it accurate.  This is by   no means complete, but I think it's something along the lines of:

Count   (Byte) - Number of frames until next change or 00. If 00, then a   further word is used to indicate the number of frames. For example:

05 = 5 frames.
00 01 02 = 513 frames.

After   that number of frames has passed, the next byte is read (the block   length specifier (BLS)). Bit 7 (and #80) determines if there is a   synchronisation block. If this is set, the following data (bytes) are   contained in the synch block:

+0: Least significant byte of current Z80 PC (Program Counter)
+1: Vertical Line Count of CRTC.
+2: Horizontal Character Count of CRTC.
+3: Least significant byte of the TotalCycles counter (always reset to zero at the start of the SNR file snapshot).

The   following 7 bits of the BLS define the number of bytes that follow   unless they are all 1 (ie. BLS and #7f == #7f). This has a special   meaning, which is to repeat the same changes as the last frame.

Each byte following for (BLS and #7f bytes) represents either a key number or a Mouse or RTC update.

If   the byte (call it CHG) is E0..FF it is treated as an RTC update. The   number of following bytes is (CHG and #1F) + 1, so, if CHG is SNR_RTC   (#E0) only the RTC second is updated (1 following byte), if CHG is   SNR_DAY (#E4), the RTC second, minute, hour, day and day of week is   updated. The Day of Week is the fourth byte and #07, the Day (1..31) is   the same byte SHL 3 (div 8) .

If the CHG is #80 .. #df it is treated as a Mouse Update.
- If (CHG and SNR_MB) <> 0 the Mouse Button byte is read.
- If (CHG and SNR_MX) <> 0 the Mouse X position is updated (if (CHG and SNR_MX_W) then it's a word else a byte).
- If (CHG and SNR_MY) <> 0 the Mouse Y position is updated (if (CHG and SNR_MY_W) then it's a word else a byte).
- If (CHG and SNR_MW) <> 0 the Mouse Wheel position is updated. (if (CHG and SNR_MW_W) then it's a word else a byte).


And that's all there is to it. Really simple eh !


P.S.   I would make a Wiki page about the SNR format from this post but I'm   useless at Wiki code and I don't have much time. Anyone else feel free   to do so.

But I totally don't understand this :D :police:

I need to know:
- Which informations / cycle infos or like this are stored from Z80?
- How do you catch these infos in WinApe? (Read and also write them)

how often do you update informations? VSyncwise, cyclewise or...??

etc... etc... :D

When you put your ear on a hot stove, you can smell how stupid you are ...

Amstrad CPC games in your webbrowser

JavaCPC Desktop Full Release


The keyboard scan code updates are only done once per frame while recording an SNR/SNP file so the key is guaranteed to be in a consistent state no matter which point in the frame it's tested. All the updates to the SNR/SNP file are carried out on the VSYNC, but only if it actually triggers a monitor VSYNC. The first part of the file is actually a snapshot so the state is saved followed by each key change (and mouse move) following the snapshot.


Quote from: Executioner on 16:07, 16 September 10
All the updates to the SNR/SNP file are carried out on the VSYNC, but only if it actually triggers a monitor VSYNC.

Well I am triggering this in during checkVSync():
where vpos is set to 0 I set a boolean in CPC to true.

During CPC's cycles this boolean is checked. If true, it updates the SNP state and sets itself to false.

But somewhere in code is a glitch!
I don't know why, but if I even change 1 line of my code to different sense but same result,
the glitch is also different. (Hard to explain)

My code so far:
    private int SNAsize;
    public static boolean playSNP = false;
    private byte[] keys = null;
    private byte[] snpbyte;
    private String snpname;

    private void SNP_Load(String name, byte[] data, boolean showinfo) {
        System.out.println("Recorded data file opened");
        reloadsnp = false;
        snpname = name;
        snpbyte = data;
        int memSize = getWord(data, MEM_SIZE) * 1024;
        SNAsize = 0x100 + memSize;
        byte[] s = new byte[SNAsize];
        System.arraycopy(data, 0, s, 0, SNAsize);
        int keylength = data.length - SNAsize;
        keys = new byte[keylength];
        System.arraycopy(data, SNAsize, keys, 0, keylength);
        byte[] info = new byte[32];
        System.arraycopy(data, 0xe0, info, 0, 32);
        String inf = "";
        int p = 0;
        try {
            while (info[p] != 0) {
                inf += (char) info[p];
        } catch (Exception e) {
        if (showinfo) {
            JOptionPane.showMessageDialog(new Frame(), "Recorded with " + inf);
        SNA_Load("buffer", s);
        playSNP = true;
        keyOffset = 0;
        FrameCount = 0;
        keytime = 0;
    private int keyOffset = 0;
    private int FrameCount = 0;
    private int numKeys;
    private byte time;
    private boolean reloadsnp = false;

    private void SetKeys() {
        numKeys = (int) keys[keyOffset++] & 0x0ff;
        while (numKeys > 0) {

    private void UpdateSNP(boolean loop) {
        if (playSNP) {
            if (FrameCount <= 0) {
                try {
                } catch (Exception e) {
                    for (int i = 0; i <= 127; i++) {
                        getKeyboard().ReleaseKey((byte) i);
                    playSNP = false;
                    if (loop) {
                        reloadsnp = true;
        } else if (StoreSNP) {

    private void GetFrameCount() {
        if ((time = keys[keyOffset++]) != 0) {
            FrameCount = (int) (time & 0x0ff);
        FrameCount = getDWord(keys, keyOffset);
        keyOffset += 4;
    private byte[] keybytes = null;
    private BufferedOutputStream snpout = null;
    private int snpcounter = 0;
    public static boolean StoreSNP = false;

    public void SNP_Save() {
        snpCapture = true;

    private void SNP_Capture() {
        try {
            if (snpout != null) {
            byte[] sna = bufferSNA();
            snpcounter = 0;
            String catcher = SNP_EYECATCHER;
            for (int i = 0; i < 19; i++) {
                catcher += (char) 0;
            System.arraycopy(SNP_HEADER.getBytes("UTF-8"), 0, sna, 0, SNP_HEADER.length());
            System.arraycopy(catcher.getBytes("UTF-8"), 0, sna, 0xe0, catcher.length());
            if (snpout == null) {
                File snpfile = new File("output.snp");
                String add = "_";
                int ad = 0;
                while (snpfile.canRead()) {
                    snpfile = new File("output" + add + ad++ + ".snp");
                snpout = new BufferedOutputStream(new FileOutputStream(snpfile));
                StoreSNP = true;
                keytime = 0;
        } catch (final IOException iox) {

    public void SNP_Stop() {
        if (snpout != null) {
            try {
            } catch (final IOException iox) {
        snpout = null;
        StoreSNP = false;

    private void StoreSNP() {
        try {
            keybytes = getKeyboard().getKeyNum();
            if (keybytes.length > 0) {
                if (snpcounter < 256) {
                } else {
                    byte[] leng = new byte[4];
                    putDWord(leng, 0, snpcounter);
                snpcounter = 0;
        } catch (IOException e) {

    protected void processKey(byte keynum) {
        if ((keyBytes[(keynum / 8)] & (1 << (keynum & 0x07))) == 0) {
            keyBytes[(keynum / 8)] |= (1 << (keynum & 0x07));
        } else {
            keyBytes[(keynum / 8)] &= ~(1 << (keynum & 0x07));

    protected void ReleaseKey(byte keynum) {
        keyBytes[(keynum / 8)] |= (1 << (keynum & 0x07));

    protected void PressKey(byte keynum) {
        keyBytes[(keynum / 8)] &= ~(1 << (keynum & 0x07));

    protected boolean IsPressed(byte keynum) {
        return (keyBytes[(keynum / 8)] & (1 << (keynum & 0x07))) == 0;

    protected int[] previousbytes = new int[10];

    protected void initBytes() {
        for (int i = 0; i < 10; i++) {
            previousbytes[i] = keyBytes[i];

    protected byte[] getKeyNum() {
        Vector keynums = new Vector();
        for (int i = 0; i < 10; i++) {
            if (previousbytes[i] != keyBytes[i]) {
                for (int b = 0; b < 8; b++) {
                    if (((previousbytes[i] ^ keyBytes[i]) & (1 << b)) != 0) {
                        keynums.add((i * 8) + b);
                previousbytes[i] = keyBytes[i];
        Object[] array = keynums.toArray();
        byte[] keys = new byte[array.length];
        for (int i = 0; i < keys.length; i++) {
            keys[i] = (byte) Integer.parseInt(array[i].toString());
        return keys;

Any idea, what is bad here?
When you put your ear on a hot stove, you can smell how stupid you are ...

Amstrad CPC games in your webbrowser

JavaCPC Desktop Full Release


Quote from: Devilmarkus on 11:52, 16 September 10
- Would it be easy to implement it into JavaCPC, too?

You should ask the programmer of that emulator, he is supposed to know!  :P
TFM of FutureSoft
Also visit the CPC and Plus users favorite OS: FutureOS - The Revolution on CPC6128 and 6128Plus


Hello Executioner,
Following your advice in this post, I've implemented the SNR emulation in my emulator CPCEmuPower.
Now it is working fine, but sometimes 1 frame is missing before the start of the SNR and sometimes during the execution of the SNR there is a frame drop / desynchronization...
So I have a few questions not documented here :
a) in the chunk 'SNR '
after the 10 bytes of 'initial keyboard state', there is 0x48 (=72) bytes of data, what is it exactly ? Mouse and RTC Init or synchronisation block ? I really don't understand what is it for !

b) what is exactly synchronisation block (BLS&0x80) for ?
you said that to avoid emulator specific stuff, the keys are taken when GateArray send VSYNC to the monitor... So what to do with this information which seems to be internal specific emulation of WINAPE

c) when BLS==0x80, is it ok that it's only a 'synchronization block' with only 4 bytes then following the bytes (1 or 1+2 if first was 0) of the frameCounter ? no key stroke after in this case ?

d) when BLS = 0xff, is it ok that it's only a 'synchronization block' + ((BLS&0x7f)==0x7f) 'repeat inverse last key values' ?
then following the bytes (1 or 1+2 if first was 0) of the frameCounter ? no key stroke after in this case ?

Thanks in advance to your help.

I will publish my code here to help others emulators writers to implemente SNR playback if it's working in another emulator than WinAPE ;-) finaly !

Powered by SMFPacks Menu Editor Mod