diff -Nu b-em-40246d4-vanilla/src/6502.c b-em-40246d4-TOHv4.2/src/6502.c --- b-em-40246d4-vanilla/src/6502.c 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/6502.c 2025-07-06 14:17:40.649656039 +0100 @@ -2,6 +2,7 @@ 6502/65c02 host CPU emulation*/ #include "b-em.h" +#include "main.h" /* TOHv4: for set_shutdown_exit_code() */ #include "6502.h" #include "adc.h" @@ -230,6 +231,47 @@ static int otherstuffcount = 0; int romsel; +/* it's 2 MHz even though 1 MHz is fed to the real Serial ULA, because + we need to be able to generate 0.5 bits of TDRE delay on the transmit side */ +/* new in TOHv4 */ +static int tape_2mhz_main (tape_state_t *ts, + tape_vars_t *tv, + ACIA *acia, + int num_cycles, + uint8_t my_sound_tape); +static void tape_poll_2mhz (tape_state_t *ts, + tape_vars_t *tv, + ACIA *acia, + int num_cycles, + uint8_t emit_tapenoise); +static int tape_2mhz_handle_tape (tape_state_t *ts, + tape_vars_t *tv, + ACIA *acia, + int num_cycles, + uint8_t my_sound_tape, + uint8_t *throw_eof_out); +static int tape_2mhz_handle_rs423_motor1 (tape_state_t *ts, + tape_vars_t *tv, + ACIA *acia, + int num_cycles, + uint8_t emit_tapenoise, + uint8_t *throw_eof_out); +static void tape_2mhz_handle_rs423_motor0(tape_state_t *ts, int num_cycles); /* TOHv4.1-rc9 */ +/* TOHv4 */ +/* Simulates the 16/13 MHz clock generated by the serial ULA. */ +static int +ula_2mhz_clock_for_tape (int32_t ula_rx_thresh_ns, + int32_t ula_tx_thresh_ns, + int32_t ula_rx_divider, + int32_t ula_tx_divider, + uint8_t *fire_acia_rxc_out, + uint8_t *fire_acia_2txc_out, + uint8_t *fire_dcd_out, + int32_t *rx_ns_inout, + int32_t *tx_ns_inout, + int32_t *dcd_2mhz_counter_inout); + + static void polltime(int c) { cycles -= c; @@ -240,6 +282,11 @@ music5000_poll(c); stopwatch += c; otherstuffcount -= c; + tape_poll_2mhz (&(tape_state), + &(tape_vars), + &sysacia, + c, + sound_tape); if (motoron) { if (fdc_time) { fdc_time -= c; @@ -1076,16 +1123,338 @@ return temp; } + +#include + +static void tape_poll_2mhz (tape_state_t *ts, + tape_vars_t *tv, + ACIA *acia, + int num_cycles, + uint8_t my_sound_tape) { + int e; + e = tape_2mhz_main (ts, tv, acia, num_cycles, my_sound_tape); + if ( (TAPE_E_OK != e) && ! ts->disabled_due_to_error ) { + tape_handle_exception (ts, /* handle tape exceptions at top level */ + tv, + e, + tv->testing_mode & TAPE_TEST_QUIT_ON_EOF, + tv->testing_mode & TAPE_TEST_QUIT_ON_ERR, + 1); /* alter menus on failure */ + } +} + + +/* TOHv4.1-rc8: fix tapenoise cutting off too early on LOAD, when motor stops. + * Need to separate this out from ula_2mhz_clock_for_tape(), because it needs to fire + * both in RS423 and tape mode, and whether the motor is running or not. + */ +static int +ula_2mhz_clock_for_tapenoise(int32_t *tapenoise_counter_inout, + uint8_t *fire_tapenoise_out) { + *fire_tapenoise_out = 0; + (*tapenoise_counter_inout)++; + if (*tapenoise_counter_inout > 13*128) { + *fire_tapenoise_out = 1; + *tapenoise_counter_inout = 0; + } + return TAPE_E_OK; +} + + + +/* TOHv4 */ +/* Simulates the 16/13 MHz clock generated by the serial ULA. + + This is in 6502.c instead of serial.c, in order to avoid + potentially expensive cross-module function calls at + 2 MHz :( */ +static int +ula_2mhz_clock_for_tape (int32_t ula_rx_thresh_ns, + int32_t ula_tx_thresh_ns, + int32_t ula_rx_divider, + int32_t ula_tx_divider, + uint8_t *fire_acia_rxc_out, + uint8_t *fire_acia_2txc_out, + uint8_t *fire_dcd_out, + /*uint8_t *fire_tapenoise_out,*/ + int32_t *rx_ns_inout, + int32_t *tx_ns_inout, + int32_t *dcd_2mhz_counter_inout + /*int32_t *tapenoise_counter_inout*/ ) { + + *fire_acia_rxc_out = 0; + *fire_acia_2txc_out = 0; + *fire_dcd_out = 0; + /**fire_tapenoise_out = 0;*/ + + /* 2 MHz tick */ + (*rx_ns_inout) += 500; + (*tx_ns_inout) += 500; + (*dcd_2mhz_counter_inout)++; + /*(*tapenoise_counter_inout)++;*/ + + if ((*dcd_2mhz_counter_inout) >= 423) { + *fire_dcd_out = 1; + *dcd_2mhz_counter_inout = 0; + } + + /* accumulate enough 2 MHz ticks to cross the + threshold of the next 16/13 MHz tick, then fire */ + if ((*rx_ns_inout) > ula_rx_thresh_ns) { + *fire_acia_rxc_out = 1; + (*rx_ns_inout) -= ula_rx_thresh_ns; + /* fudge potential situation where we have to fire twice */ + /* WARNING: savestates may set rx_ns_inout to any value + and it won't be validated, so this has to be able to + catch OOB values here */ + if ((*rx_ns_inout) > ula_rx_thresh_ns) { + *rx_ns_inout = 0; + } + } + + if ((*tx_ns_inout) > ula_tx_thresh_ns) { + *fire_acia_2txc_out = 1; + (*tx_ns_inout) -= ula_tx_thresh_ns; + /* fudge potential situation where we would have to fire twice + (this fudge is what causes ULA /1 case + to diverge from the hardware a bit) */ + if ((*tx_ns_inout) > ula_tx_thresh_ns) { + *tx_ns_inout = 0; + } + } + + /* + if (*tapenoise_counter_inout > 13*128) { + *fire_tapenoise_out = 1; + *tapenoise_counter_inout = 0; + } + */ + + return TAPE_E_OK; + +} + + +#include "tapenoise.h" +/* it's 2 MHz even though 1 MHz is fed to the real Serial ULA, because + we need to be able to generate 0.5 bits of TDRE delay on the transmit side */ +/* new in TOHv4 */ +static int tape_2mhz_main (tape_state_t *ts, + tape_vars_t *tv, + ACIA *acia, + int num_cycles, + uint8_t my_sound_tape) { + + int e; + uint8_t throw_tape_eof; + uint8_t fire_tapenoise; + int i; + + if (tv->expired_quit) { return TAPE_E_EXPIRY; } + + throw_tape_eof = 0; + e = TAPE_E_OK; + + if (ts->ula_ctrl_reg & 0x40) { + if (ts->ula_motor) { + /* RS423 with *MOTOR 1 */ + e = tape_2mhz_handle_rs423_motor1 (ts, tv, acia, num_cycles, my_sound_tape, &throw_tape_eof); + } else { + tape_2mhz_handle_rs423_motor0 (ts, num_cycles); + } + } else { + /* tape mode */ + e = tape_2mhz_handle_tape (ts, tv, acia, num_cycles, my_sound_tape, &throw_tape_eof); + } + + /* TOHv4.1-rc9: forgot to loop for num_cycles */ + for (i=0; (itape_noise_counter), &fire_tapenoise); + /* Tape noise needs to continue after the motor switches off, + to flush out any remaining audio in the ring buffer. Otherwise, + the last few bits may not be reproduced. */ + if (fire_tapenoise && my_sound_tape) { // 1201.9 Hz + tapenoise_play_ringbuf(); + } + } + + /* TOHv4.1: suppress EOF in record mode */ + return ((throw_tape_eof && ! tv->record_activated) ? TAPE_E_EOF : TAPE_E_OK); + + return e; + +} + +/* new in TOHv4.1-rc9 */ +static void tape_2mhz_handle_rs423_motor0(tape_state_t *ts, int num_cycles) { + int i; + for (i=0; (i < num_cycles); i++) { + if ( ts->ula_rs423_taperoll_2mhz_counter >= TAPE_1200TH_IN_2MHZ_INT ) { /*(128*13) ) {*/ + /* TOHv4.1-rc9 */ + /* NEW: RS423 WITH MOTOR OFF */ + /* Keep tapenoise buffer filled with silence. Don't allow it + * just to fall empty if the motor is off. + * Doing so caused problems with Ultron, because tapenoise was being + * played back out of the ringbuffer during motor off breaks, but wasn't + * being replenished again, so eventually you would get dropouts. */ + tapenoise_send_1200 ('S', &(ts->tapenoise_no_emsgs)); + ts->ula_rs423_taperoll_2mhz_counter=0; + } + (ts->ula_rs423_taperoll_2mhz_counter)++; /* counter shared with MOTOR 1 case */ + } +} + + +/* TOHv4.1: it turns out it is possible for this function + * to be polled even if the motor is stopped. As + * long as tape mode is selected in the ULA, this + * function is called regardless of motor state. */ +static int tape_2mhz_handle_tape (tape_state_t *ts, + tape_vars_t *tv, + ACIA *acia, + int num_cycles, + uint8_t my_sound_tape, + uint8_t *throw_eof_out) { + + int e,j; + + e = TAPE_E_OK; + + for (j=0; (TAPE_E_OK == e) && (j < num_cycles); j++) { + + uint8_t fire_ula_rxc, fire_ula_2txc, fire_dcd; /*, fire_tapenoise;*/ + + /* 1. which clocks fire? */ + e = ula_2mhz_clock_for_tape (ts->ula_rx_thresh_ns, + ts->ula_tx_thresh_ns, + ts->ula_rx_divider, + ts->ula_tx_divider, + &fire_ula_rxc, /* fire RXC? always 16/13 MHz * 1/64 (19.2KHz) for tape */ + &fire_ula_2txc, /* fire 2TXC? from /1 (2 x 1.23 MHz) to /256 (2 x 4.8 KHz) */ + &fire_dcd, /* fire DCD tick? every ~211 us */ + /*&fire_tapenoise,*/ /* fire 1201.9 Hz tick for tapenoise? */ + &(ts->ula_rx_ns), /* counter updated */ + &(ts->ula_tx_ns), /* counter updated */ + &(ts->ula_dcd_2mhz_counter) /* counter updated */ + /*&(ts->tape_noise_counter)*/); /* counter updated */ + + /* 2. handle RX */ + if (fire_ula_rxc && ts->ula_motor) { /* TOHv4.1: gated by ula_motor now */ + e = serial_rxc_clock_for_tape (ts, tv, acia, my_sound_tape && ! tv->record_activated, throw_eof_out); + } + + /* 3. handle TX */ + if ((TAPE_E_OK == e) && fire_ula_2txc) { + e = serial_2txc_clock_for_tape(ts, tv, acia, my_sound_tape && tv->record_activated); + } + +#ifdef BUILD_TAPE_SANITY + if (TAPE_E_ACIA_TX_BITCLK_NOT_READY == e) { + log_warn("6502: BUG: e == TAPE_E_ACIA_TX_BITCLK_NOT_READY"); + return TAPE_E_BUG; + } +#endif + + /* 4. handle DCD + DCD timing is *independent* of the divided clock */ + if (fire_dcd) { serial_handle_dcd_tick(ts, tv, acia); } + + /* Tape noise needs to continue after the motor switches off, + to flush out any remaining audio in the ring buffer. Otherwise, + the last few bits may not be reproduced. */ + /* TOHv4.1-rc8: MOVED OUT OF THIS FUNCTION */ + /* + if (fire_tapenoise && my_sound_tape) { // 1201.9 Hz + tapenoise_play_ringbuf(); + } + */ + + } /* next 2 MHz cycle */ + + /* TOHv4: trap EOF if no tape is loaded */ + if ( *throw_eof_out && ( ! TAPE_IS_LOADED (ts->filetype_bits) ) ) { + *throw_eof_out = 0; + } + + return e; + +} + + + + + + +/* ONLY called while tape is rolling */ +static int tape_2mhz_handle_rs423_motor1 (tape_state_t *ts, + tape_vars_t *tv, + ACIA *acia, + int num_cycles, + uint8_t emit_tapenoise, + uint8_t *throw_eof_out) { + + int j; + + /* RS423 selected; but if the motor is running, then we have to advance the tape anyway + (Haunted Abbey: https://stardot.org.uk/forums/viewtopic.php?p=426148#p426148 ) + Divide 2 MHz by 13*128 to get 1201.9 baud ... */ + + for (j=0; + (j < num_cycles); + j++, (ts->ula_rs423_taperoll_2mhz_counter)++, (ts->ula_dcd_2mhz_counter)++) { /* 2 MHz tick */ + + int e; + + /* while tape is rolling, discard each 1200th, + send silence to tapenoise, store prevailing bit */ + if ( ts->ula_rs423_taperoll_2mhz_counter >= TAPE_1200TH_IN_2MHZ_INT ) { /*(128*13) ) {*/ + e = tape_rs423_eat_1200th (ts, tv, acia, emit_tapenoise, throw_eof_out); + if (TAPE_E_OK != e) { return e; } + } + + /* Actually have no idea if internal tape DCD logic + and run-in detection etc. should continue + to function while RS423 mode is being used? + I think beebjit does this, which is usually right. + + Tape DCD logic continues to run and the + internal tape DCD line is updated but not pushed out to the + ACIA by serial_push_dcd_cts_lines_to_acia(), + because DCD is always low in RS423 mode. */ + + /* ~423 ticks at 2 MHz is the length of a DCD blip. + We use this as a time quantum for DCD operations, + so the pre-blip counter is also measured in 423-tick + units. Since the real timing is based on a capacitor + charging, it need not be accurate. */ + if (ts->ula_dcd_2mhz_counter >= TAPE_DCD_BLIP_TICKS_2MHZ) { /* 423 */ + ts->ula_dcd_2mhz_counter = 0; + serial_poll_dcd_blipticks (ts->ula_prevailing_rx_bit_value, + tv->strip_silence_and_leader, /* fast DCD? */ + &(ts->ula_dcd_blipticks), /* DCD's blips counter UPDATED */ + &(ts->ula_dcd_tape)); /* logic line updated */ + } + + if (ts->ula_rs423_taperoll_2mhz_counter >= TAPE_1200TH_IN_2MHZ_INT) { //(128*13)) { + ts->ula_rs423_taperoll_2mhz_counter = 0; + } + + } /* next 2 MHz cycle */ + + /* note that the DCD line is set low by this call + regardless of the value of ula_dcd_tape (RS423 mode) */ + serial_push_dcd_cts_lines_to_acia (acia, ts); + + return TAPE_E_OK; + +} + static void otherstuff_poll(void) { otherstuffcount += 128; - acia_poll(&sysacia); - if (sound_music5000) - music2000_poll(); - if (!tapelcount) { - tape_poll(); - tapelcount = tapellatch; - } - tapelcount--; +/* if (sound_music5000) + music2000_poll();*/ if (motorspin) { motorspin--; if (!motorspin) @@ -1106,6 +1475,13 @@ mcount = 6; mouse_poll(); } + /* TOHv4: implement -expire shutdown option */ + if (tape_vars.testing_expire_ticks > 0) { + tape_vars.testing_expire_ticks--; + if ( 1 == tape_vars.testing_expire_ticks ) { + tape_vars.expired_quit = 1; + } + } } #define getw() getsw() diff -Nu b-em-40246d4-vanilla/src/acia.c b-em-40246d4-TOHv4.2/src/acia.c --- b-em-40246d4-vanilla/src/acia.c 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/acia.c 2025-07-06 14:17:40.649656039 +0100 @@ -1,126 +1,362 @@ /*B-em v2.2 by Tom Walker 6850 ACIA emulation*/ +/* - TOHv4 overhaul by 'Diminished' */ + +/* some code (mc6850_... functions) + taken from beebjit, (c) Chris Evans, under GPL */ + #include #include "b-em.h" #include "6502.h" #include "acia.h" /* Status register flags */ +#include "tape2.h" // error codes -#define RXD_REG_FUL 0x01 -#define TXD_REG_EMP 0x02 -#define DCD 0x04 -#define CTS 0x08 -#define FRAME_ERR 0x10 -#define RX_OVERRUN 0x20 -#define PARITY_ERR 0x40 -#define INTERUPT 0x80 - -static inline int rx_int(ACIA *acia) { - return (acia->status_reg & INTERUPT) && (acia->control_reg & INTERUPT); -} - -static inline int tx_int(ACIA *acia) { - return (acia->status_reg & TXD_REG_EMP) && ((acia->control_reg & 0x60) == 0x20); -} - -static void acia_updateint(ACIA *acia) { - if (rx_int(acia) || tx_int(acia)) { - log_debug("acia: %s, interrupt asserted", acia->name); - interrupt |= acia->intnum; - } - else { - log_debug("acia: %s, interrupt de-asserted", acia->name); - interrupt &= ~acia->intnum; +/* control register flags (now named in TOHv3) */ +#define CTRL_DIVSEL_1 0x1 +#define CTRL_DIVSEL_2 0x2 +#define CTRL_WORDSEL_1 0x4 +#define CTRL_WORDSEL_2 0x8 +#define CTRL_WORDSEL_3 0x10 +/* TOHv3: renamed and moved transmit control flags into acia.h */ +/*#define CTRL_TXCTRL_1 0x20 +#define CTRL_TXCTRL_2 0x40*/ +#define CTRL_IRQ 0x80 + + +/* ********** BEGIN BEEBJIT CODE ********** + mc6850_... functions and these enums are taken from beebjit, + (c) Chris Evans, under GPL */ +static uint8_t mc6850_read(ACIA *acia, uint8_t reg); +static void mc6850_write(ACIA *acia, uint8_t reg, uint8_t val); +static void mc6850_set_DCD(ACIA *acia, uint8_t is_DCD); +static int mc6850_receive(ACIA *acia, uint8_t byte); +static int mc6850_receive_bit (ACIA *acia, uint8_t bit); +static void mc6850_reset(ACIA *acia); +void mc6850_set_CTS(ACIA *acia, uint8_t is_CTS); +static void mc6850_update_irq_and_status_read(ACIA *acia); +enum { + k_serial_acia_control_clock_divider_mask = 0x03, + k_serial_acia_control_clock_divider_64 = 0x02, + k_serial_acia_control_8_bits = 0x10, + k_serial_acia_control_TCB_mask = 0x60, + k_serial_acia_control_RIE = 0x80, +}; +enum { + k_serial_acia_TCB_RTS_and_TIE = 0x20, + k_serial_acia_TCB_no_RTS_no_TIE = 0x40, +}; +enum { + k_mc6850_state_null = 0, + k_mc6850_state_need_start = 1, + k_mc6850_state_need_data = 2, + k_mc6850_state_need_parity = 3, + k_mc6850_state_need_stop = 4, +}; +/* ********** END BEEBJIT CODE ********** */ + +static int run_tx_shift_register (ACIA *acia, serial_framing_t *f, char *bit_out); +static int wp3_cp_tx_datareg_to_shiftreg (ACIA *a); +static void reset_tx_shift_register(ACIA *a) ; +static int acia_tx_shift (ACIA *acia, serial_framing_t *f); +static int poll_maybe_cp_datareg_to_shiftreg (ACIA *a); + +/* glue */ +static void state_6502_set_irq_level (int level) { + if (level) { + interrupt |= 4; + } else { + interrupt &= ~4; + } +} + +#include "sysacia.h" + +/* TOHv4: used to implement no-poll mode for serial-to-file */ +void acia_hack_consume_tx_byte_immediately (ACIA *acia) { + acia->tx_data_reg_loaded = 0; + acia->tx_shift_reg_loaded = 0; + acia->tx_shift_reg_shift = 0; + acia->status_reg |= k_serial_acia_status_TDRE; + mc6850_update_irq_and_status_read(acia); +} + +/* glue */ +uint8_t acia_read (ACIA *acia, uint16_t addr) { + return mc6850_read(acia, addr & 1); +} + +/* glue */ +void acia_write (ACIA *acia, uint16_t addr, uint8_t val) { + mc6850_write (acia, addr & 1, val); +} + +/* TOHv4: do RX clock divider */ +int acia_poll_rxc (ACIA *acia, + uint8_t *fire_rxc_out, + int32_t *divider_out) { + + uint8_t cr3; + + *divider_out = 0; /* cat, pigeons */ + cr3 = 3 & acia->control_reg; + + *fire_rxc_out = 0; + + if (0x0 == cr3) { + *divider_out = 1; /* if tape selected then 19200 baud */ + } else if (0x1 == cr3) { + *divider_out = 16; /* if tape selected then 1200 baud */ + } else if (0x2 == cr3) { + *divider_out = 64; /* if tape selected then 300 baud */ + } else { + log_warn("acia: BUG: acia_poll_rxc: bad control_reg &%x\n", acia->control_reg); + return TAPE_E_BUG; } + + if (acia->rxc_count >= *divider_out) { + acia->rxc_count = 0; + *fire_rxc_out = 1; /* fire divided clock */ + } + + (acia->rxc_count)++; + + return TAPE_E_OK; + } -uint8_t acia_read(ACIA *acia, uint16_t addr) { - if (addr & 1) { - uint8_t temp = acia->rx_data_reg; - acia->status_reg &= ~(INTERUPT | RXD_REG_FUL); - acia_updateint(acia); - return temp; - } - else { - uint8_t temp = acia->status_reg & ~INTERUPT; - if (rx_int(acia) || tx_int(acia)) - temp |= INTERUPT; - return temp; +void acia_get_framing (uint8_t ctrl_reg, serial_framing_t *f) { + + if ((ctrl_reg & 0x1c) == 0) { + f->num_data_bits = 7; + f->num_stop_bits = 2; + f->parity = 'E'; + } else if ((ctrl_reg & 0x1c) == 4) { + f->num_data_bits = 7; + f->num_stop_bits = 2; + f->parity = 'O'; + } else if ((ctrl_reg & 0x1c) == 8) { + f->num_data_bits = 7; + f->num_stop_bits = 1; + f->parity = 'E'; + } else if ((ctrl_reg & 0x1c) == 0xc) { + f->num_data_bits = 7; + f->num_stop_bits = 1; + f->parity = 'O'; + } else if ((ctrl_reg & 0x1c) == 0x10) { + f->num_data_bits = 8; + f->num_stop_bits = 2; + f->parity = 'N'; + } else if ((ctrl_reg & 0x1c) == 0x14) { + f->num_data_bits = 8; + f->num_stop_bits = 1; + f->parity = 'N'; + } else if ((ctrl_reg & 0x1c) == 0x18) { + f->num_data_bits = 8; + f->num_stop_bits = 1; + f->parity = 'E'; + } else if ((ctrl_reg & 0x1c) == 0x1c) { + f->num_data_bits = 8; + f->num_stop_bits = 1; + f->parity = 'O'; } -} -void acia_write(ACIA *acia, uint16_t addr, uint8_t val) { - if (addr & 1) { - acia->tx_data_reg = val; - if (acia->tx_hook) - acia->tx_hook(acia, val); - acia->status_reg &= ~TXD_REG_EMP; - acia_updateint(acia); - } - else if (val != acia->control_reg) { - if ((val & 0x60) != 0x20) // interupt being turned off - if (acia->tx_end) - acia->tx_end(acia); - acia->control_reg = val; - if (val == 3) { - log_debug("acia: %s, master reset", acia->name); - acia->status_reg &= CTS|DCD; - if (!(acia->status_reg & CTS)) - acia->status_reg |= TXD_REG_EMP; + } + + +int acia_run_tx_shift_register(ACIA *acia, serial_framing_t *f, char *bit_out) { + return run_tx_shift_register(acia,f,bit_out); + } + +static int run_tx_shift_register (ACIA *acia, serial_framing_t *f, char *bit_out) { + + int e; + + if (acia->tx_shift_reg_loaded) { + e = acia_tx_shift (acia, f); + if (TAPE_E_OK != e) { return e; } + if (NULL != bit_out) { + *bit_out = (acia->tx_shift_reg_value & 1) ? '1' : '0'; } - if (acia->set_params) - acia->set_params(acia, val); - acia_updateint(acia); + } + + e = poll_maybe_cp_datareg_to_shiftreg(acia); + + return e; + +} + + +static int acia_tx_shift (ACIA *acia, serial_framing_t *f) { + + uint8_t total_bits; + +#ifdef BUILD_TAPE_SANITY + if ((8!=f->num_data_bits)&&(7!=f->num_data_bits)) { + log_warn("acia: BUG: framing illegal (%u data bits)", f->num_data_bits); + return TAPE_E_BUG; + } +#endif + + if ( (acia->tx_shift_reg_shift > 0) ) { + acia->tx_shift_reg_value >>= 1; } + + /* TOHv4.1: let's try that again */ + total_bits = f->num_data_bits + f->num_stop_bits + ((f->parity == 'N') ? 0 : 1); + + if (acia->tx_shift_reg_shift < total_bits) { + (acia->tx_shift_reg_shift)++; + } else { + acia->tx_shift_reg_loaded = 0; + acia->tx_shift_reg_shift = 0; + } + + // if (acia->tx_shift_reg_shift < (1 + f->num_data_bits)) { + // (acia->tx_shift_reg_shift)++; + // /* TOHv4.1: parity bit ... */ + // } else if ((f->parity != 'N') && (acia->tx_shift_reg_shift == (1 + f->num_data_bits))) { + // (acia->tx_shift_reg_shift)++; + // } else if ((2 == f->num_stop_bits) && (acia->tx_shift_reg_shift == (2 + f->num_data_bits))) + // } else if (acia->tx_shift_reg_shift >= (1 + f->num_data_bits)) { /* frame done */ + // acia->tx_shift_reg_loaded = 0; + // (acia->tx_shift_reg_shift) = 0; + // } else { + // log_warn("acia: illegal shift %d", acia->tx_shift_reg_shift); + // return TAPE_E_BUG; + // } + + return TAPE_E_OK; + } void acia_dcdhigh(ACIA *acia) { - if (acia->status_reg & DCD) - return; - acia->status_reg |= DCD | INTERUPT; - acia_updateint(acia); + mc6850_set_DCD(acia, 1); } void acia_dcdlow(ACIA *acia) { - acia->status_reg &= ~DCD; - acia_updateint(acia); + mc6850_set_DCD(acia, 0); } -void acia_ctson(ACIA *acia) -{ - if (acia->status_reg & CTS) { - acia->status_reg = (acia->status_reg & ~CTS) | TXD_REG_EMP; - acia_updateint(acia); - } -} -void acia_ctsoff(ACIA *acia) + +#include "tape.h" + +#ifdef BUILD_TAPE_INCOMPATIBLE_NEW_SAVE_STATES +int acia_savestate_BEMSNAP4(ACIA *acia, FILE *f) { - if (!(acia->status_reg & CTS)) { - acia->status_reg = (acia->status_reg & ~TXD_REG_EMP) | CTS; - acia_updateint(acia); - } + + uint8_t bytes[27]; + uint8_t u4[4]; + + bytes[0] = acia->control_reg; + bytes[1] = acia->status_reg; + bytes[2] = acia->line_cts; + bytes[3] = acia->line_dcd; + bytes[4] = acia->rx_data_reg; + bytes[5] = acia->tx_data_reg; + bytes[6] = acia->rx_shift_reg_count; + bytes[7] = acia->rx_shift_reg; + bytes[8] = (uint8_t) acia->rx_sr_overflow; + bytes[9] = (uint8_t) acia->rx_sr_parity_error; + bytes[10] = (uint8_t) acia->rx_sr_framing_error; + bytes[11] = (uint8_t) acia->state; + bytes[12] = (uint8_t) acia->parity_accumulator; + tape_write_u32(u4, (uint32_t) acia->rxc_count); + memcpy(bytes+13, u4, 4); + tape_write_u32(u4, (uint32_t) acia->txc_count); + memcpy(bytes+17, u4, 4); + bytes[21] = acia->tx_shift_reg_value; + bytes[22] = (uint8_t) acia->tx_shift_reg_shift; + bytes[23] = acia->tx_shift_reg_loaded; + bytes[24] = acia->tx_data_reg_loaded; + bytes[25] = acia->tx_waiting_to_set_tdre; + bytes[26] = acia->tx_odd_ones; /* TOHv4.1: parity for TX operations */ + + return (1==fwrite(bytes, sizeof(bytes), 1, f)) ? TAPE_E_OK : TAPE_E_SAVESTATE; + } -void acia_poll(ACIA *acia) + +int acia_loadstate_BEMSNAP4(ACIA *acia, FILE *f) { - if (!(acia->status_reg & (TXD_REG_EMP|CTS))) { - log_debug("acia: %s, setting TXDR empty flag", acia->name); - acia->status_reg |= TXD_REG_EMP; - acia_updateint(acia); + uint8_t bytes[27]; + int8_t s; + + if (1 != fread(bytes, sizeof(bytes), 1, f)) { + log_warn("acia: load state: read failed: %s", strerror(errno)); + return TAPE_E_LOADSTATE; + } + + if (bytes[2]&0xfe) { + log_warn("acia: load state: state is corrupt @ line_cts"); + return TAPE_E_LOADSTATE; + } + if (bytes[3]&0xfe) { + log_warn("acia: load state: state is corrupt @ line_dcd"); + return TAPE_E_LOADSTATE; + } + if (bytes[8]&0xfe) { + log_warn("acia: load state: state is corrupt @ rx_sr_overflow"); + return TAPE_E_LOADSTATE; + } + if (bytes[9]&0xfe) { + log_warn("acia: load state: state is corrupt @ rx_sr_parity_error"); + return TAPE_E_LOADSTATE; + } + if (bytes[10]&0xfe) { + log_warn("acia: load state: state is corrupt @ rx_sr_framing_error"); + return TAPE_E_LOADSTATE; + } + s = (int8_t) bytes[11]; + if ( (s!=k_mc6850_state_null) + && (s!=k_mc6850_state_need_start) + && (s!=k_mc6850_state_need_data) + && (s!=k_mc6850_state_need_parity) + && (s!=k_mc6850_state_need_stop)) { + log_warn("acia: load state: state is corrupt @ state"); + return TAPE_E_LOADSTATE; + } + if (bytes[22]>9) { /* illegal shift reg shift */ + log_warn("acia: load state: state is corrupt @ tx_shift_reg_shift (%u)", bytes[22]); + return TAPE_E_LOADSTATE; + } + if (bytes[26]>1) { + log_warn("acia: load state: state is corrupt @ tx_odd_ones"); + return TAPE_E_LOADSTATE; } -} -void acia_receive(ACIA *acia, uint8_t val) { /*Called when the acia recives some data*/ - acia->rx_data_reg = val; - acia->status_reg |= RXD_REG_FUL | INTERUPT; - if (acia->rx_hook) - acia->rx_hook(acia, val); - acia_updateint(acia); -} + acia->control_reg = bytes[ 0]; + acia->status_reg = bytes[ 1]; + acia->line_cts = bytes[ 2] ? 1 : 0; + acia->line_dcd = bytes[ 3] ? 1 : 0; + acia->rx_data_reg = bytes[ 4]; + acia->tx_data_reg = bytes[ 5]; + acia->rx_shift_reg_count = bytes[ 6]; + acia->rx_shift_reg = bytes[ 7]; + acia->rx_sr_overflow = bytes[ 8] ? 1 : 0; + acia->rx_sr_parity_error = bytes[ 9] ? 1 : 0; + acia->rx_sr_framing_error = bytes[10] ? 1 : 0; + acia->state = s; + acia->parity_accumulator = ( int8_t) bytes[12]; + acia->rxc_count = (int32_t) tape_read_u32(bytes+13); /* u32 -> i32 */ + acia->txc_count = (int32_t) tape_read_u32(bytes+17); /* u32 -> i32 */ + acia->tx_shift_reg_value = bytes[21]; + acia->tx_shift_reg_shift = ( int8_t) bytes[22]; + acia->tx_shift_reg_loaded = bytes[23] ? 1 : 0; + acia->tx_data_reg_loaded = bytes[24] ? 1 : 0; + acia->tx_waiting_to_set_tdre = bytes[25] ? 1 : 0; + acia->tx_odd_ones = bytes[26] ? 1 : 0; /* TOHv4.1 */ + + mc6850_update_irq_and_status_read(acia); + + return TAPE_E_OK; +} +#else /* not def. BUILD_TAPE_INCOMPATIBLE_NEW_SAVE_STATES */ +/* old BEMSNAP3 save states */ void acia_savestate(ACIA *acia, FILE *f) { unsigned char bytes[2]; @@ -128,6 +364,7 @@ bytes[1] = acia->status_reg; fwrite(bytes, sizeof(bytes), 1, f); } +#endif void acia_loadstate(ACIA *acia, FILE *f) { @@ -135,4 +372,618 @@ fread(bytes, sizeof(bytes), 1, f); acia->control_reg = bytes[0]; acia->status_reg = bytes[1]; + mc6850_update_irq_and_status_read(acia); +} + + +void acia_hack_tx_reset_shift_register (ACIA *a) { + /*log_warn("WARNING! acia_hack_tx_reset_shift_register()");*/ + reset_tx_shift_register(a); +} + +static void reset_tx_shift_register(ACIA *a) { + a->tx_shift_reg_shift = 0; + a->tx_shift_reg_loaded = 0; + a->tx_shift_reg_value = 0; /* TOHv3.2 */ +} + +int acia_receive_bit (ACIA *acia, uint8_t bit) { + return mc6850_receive_bit (acia, bit); +} + +int acia_receive_bit_code (ACIA *acia, char code) { + uint8_t bit; + /*if ((code != '1') && (code != '0') && (code != 'S') && (code != 'L')) {*/ + if ( ! TAPE_TONECODE_IS_LEGAL(code) ) { + log_warn("acia: BUG: acia_receive_bit_code: bad code: %x", code); + return TAPE_E_BUG; + } +/* putchar(code);fflush(stdout); */ + bit = (code == '0') ? 0 : 1; + return mc6850_receive_bit (acia, bit); +} + + +uint8_t acia_rx_awaiting_start (ACIA *a) { + return (k_mc6850_state_need_start == a->state); +} + +uint8_t acia_rx_frame_ready (ACIA *a) { + return (a->state == k_mc6850_state_need_stop); +} + + +void acia_init (ACIA *acia) { + + acia->line_cts = 0; + acia->line_dcd = 0; + acia->control_reg = 0; + acia->status_reg = 0; + acia->status_reg_for_read = 0; + acia->rx_data_reg = 0; + acia->tx_data_reg = 0; + acia->rx_shift_reg_count = 0; + acia->rx_sr_overflow = 0; + acia->rx_sr_parity_error = 0; + acia->rx_sr_framing_error = 0; + acia->state = k_mc6850_state_need_start; + acia->parity_accumulator = 0; + acia->rxc_count = 0; + acia->txc_count = 0; + acia->tx_shift_reg_value = 0; + acia->tx_shift_reg_shift = 0; + acia->tx_shift_reg_loaded = 0; + acia->tx_waiting_to_set_tdre = 0; + acia->msg_printed_recv_buf_full = 0; +#ifdef BUILD_TAPE_DEV_MENU + acia->corrupt_next_read = 0; + acia->misframe_next_read = 0; + acia->gen_parity_error_next_read = 0; +#endif + + mc6850_reset(acia); + +} + + +/* TOHv3.3: moved here from tape.c */ +/* synced to tx bitclk */ + +static int +wp3_cp_tx_datareg_to_shiftreg (ACIA *acia) { + + /* yes; copy data reg into shift reg */ + acia->tx_shift_reg_value = acia->tx_data_reg; + acia->tx_shift_reg_loaded = 1; + acia->tx_shift_reg_shift = 0; + + /* empty data reg */ + acia->tx_data_reg_loaded = 0; + + /* TOHv4 */ + acia->tx_waiting_to_set_tdre = 1; + + mc6850_update_irq_and_status_read(acia); + + return TAPE_E_OK; + } + + +static int poll_maybe_cp_datareg_to_shiftreg (ACIA *acia) { + + int e; + + /* tx data pipelining, part two */ + if ( ( ! acia->tx_shift_reg_loaded ) /* if shift reg is emptied ... */ + && acia->tx_data_reg_loaded ) { /* and data reg is able to replenish it ... */ + + e = wp3_cp_tx_datareg_to_shiftreg (acia); + if (TAPE_E_OK != e) { return e; } + + } + + return TAPE_E_OK; + +} + + +void acia_ctson(ACIA *acia) +{ + mc6850_set_CTS(acia, 1); +} + +void acia_ctsoff(ACIA *acia) +{ + mc6850_set_CTS(acia, 0); +} + + +/* TOHv4.1: option to deliberately fail the PBS protection + * (for proving the tests) */ +/*#define HACK_DELIBERATELY_BREAK_PRO_BOXING_SIMULATOR*/ + +#ifdef HACK_DELIBERATELY_BREAK_PRO_BOXING_SIMULATOR +static int hack_delay_tdre=0; +#endif + +/* TOHv4 */ +int acia_poll_2txc (ACIA *acia, + char *bit_or_zero_out, /* will be zero half the time */ + int32_t *tx_divider_out) { + + uint8_t half_bit; + uint8_t full_bit; + int e; + + e = TAPE_E_OK; + + /* polled at 2x baud speed */ + + half_bit = 0; + full_bit = 0; + *tx_divider_out = 64; + *bit_or_zero_out = 0; + + if (0 == (3 & acia->control_reg)) { + *tx_divider_out = 1; + } else if (1 == (3 & acia->control_reg) ) { + *tx_divider_out = 16; + } + + /* if the divider has been changed, txc_count may now + be out of bounds; scale it to the new divider, but + keep its entropy */ + while (acia->txc_count > (2 * *tx_divider_out)) { + acia->txc_count -= (2 * *tx_divider_out); + } + + if (acia->txc_count == (2 * *tx_divider_out)) { /* every 2 half-bits */ + full_bit = 1; + half_bit = 1; + acia->txc_count = 0; + } else if (acia->txc_count == *tx_divider_out) { /* every half-bit */ + half_bit = 1; + } + + /* wait for one half-bit before acknowledging TDRE */ + if (half_bit && acia->tx_waiting_to_set_tdre) { +#ifdef HACK_DELIBERATELY_BREAK_PRO_BOXING_SIMULATOR + /* introduce long, artificial TDRE delay (TXC here is clocked quite fast I think) */ + if (hack_delay_tdre < 25) { + hack_delay_tdre++; + } else { + acia->tx_waiting_to_set_tdre = 0; + acia->status_reg |= k_serial_acia_status_TDRE; + mc6850_update_irq_and_status_read(acia); + hack_delay_tdre=0; + } +#else + /* normal behaviour: one halfbit of TDRE delay */ + acia->tx_waiting_to_set_tdre = 0; + acia->status_reg |= k_serial_acia_status_TDRE; + mc6850_update_irq_and_status_read(acia); +#endif + } + + (acia->txc_count)++; + + if ( ! full_bit ) { + e = TAPE_E_ACIA_TX_BITCLK_NOT_READY; + } + + return e; + +} + +/* TOHv4-rc2 */ +void acia_update_irq_and_status_read(ACIA *acia) { + mc6850_update_irq_and_status_read(acia); +} + + + +/* ********** REMAINING CODE IS TAKEN FROM BEEBJIT ********** + (c) Chris Evans, under GPL + ********************************************************** */ + + +static void +mc6850_update_irq_and_status_read(ACIA *acia) { + + int do_check_send_int; + int do_check_receive_int; + int do_fire_int; + uint8_t acia_status_for_read; + int do_fire_send_int = 0; + int do_fire_receive_int = 0; + + do_check_send_int = ( (acia->control_reg & k_serial_acia_control_TCB_mask) + == k_serial_acia_TCB_RTS_and_TIE); + if (do_check_send_int) { + do_fire_send_int = !!(acia->status_reg & k_serial_acia_status_TDRE); + do_fire_send_int &= !(acia->status_reg & k_serial_acia_status_CTS); // if high, inhibit IRQ + } + + do_check_receive_int = !!(acia->control_reg & k_serial_acia_control_RIE); + if (do_check_receive_int) { + do_fire_receive_int = !!(acia->status_reg & k_serial_acia_status_RDRF); + do_fire_receive_int |= !!(acia->status_reg & k_serial_acia_status_DCD); + do_fire_receive_int |= !!(acia->status_reg & k_serial_acia_status_OVRN); + } + + do_fire_int = (do_fire_send_int | do_fire_receive_int); + + /* Bit 7 of the control register must be high if we're asserting IRQ. */ + acia->status_reg &= ~k_serial_acia_status_IRQ; + if (do_fire_int) { + acia->status_reg |= k_serial_acia_status_IRQ; + } + + state_6502_set_irq_level(do_fire_int); + + /* Update the raw read value for the status register. */ + acia_status_for_read = acia->status_reg; + + /* EMU MC6850: "A low CTS indicates that there is a Clear-to-Send from the + * modem. In the high state, the Transmit Data Register Empty bit is + * inhibited". + */ + if (acia->status_reg & k_serial_acia_status_CTS) { + acia_status_for_read &= ~k_serial_acia_status_TDRE; + } + + /* If the "DCD went high" bit isn't latched, it follows line level. */ + /* EMU MC6850, more verbosely: "It remains high after the DCD input is + * returned low until cleared by first reading the Status Register and then + * Data Register or until a master reset occurs. If the DCD input remains + * high after read status read data or master reset has occurred, the + * interrupt is cleared, the DCD status bit remains high and will follow + * the DCD input." + * Note that on real hardware, only a read of data register seems required + * to unlatch the DCD high condition. + */ + if (acia->line_dcd && !acia->line_cts) { + acia_status_for_read |= k_serial_acia_status_DCD; + } + + /* EMU TODO: MC6850: "Data Carrier Detect being high also causes RDRF to + * indicate empty." + * Unclear if that means the line level, latched DCD status, etc. + */ + + acia->status_reg_for_read = acia_status_for_read; +} + + +static void +mc6850_set_DCD(ACIA *acia, uint8_t is_DCD) { + /* The DCD low to high edge causes a bit latch, and potentially an IRQ. + * DCD going from high to low doesn't affect the status register until the + * bit latch is cleared by reading the data register. + */ + + if (is_DCD && !acia->line_dcd) { + acia->status_reg |= k_serial_acia_status_DCD; + mc6850_update_irq_and_status_read(acia); + } + + acia->line_dcd = is_DCD; + +} + +void +mc6850_set_CTS(ACIA *acia, uint8_t is_CTS) { + acia->status_reg &= ~k_serial_acia_status_CTS; + if (is_CTS) { + acia->status_reg |= k_serial_acia_status_CTS; + } + acia->line_cts = is_CTS; + if (!is_CTS) { + acia_dcdlow(acia); + } + mc6850_update_irq_and_status_read(acia); +} + +int +mc6850_get_RTS(ACIA *acia) { + if ((acia->control_reg & k_serial_acia_control_TCB_mask) == + k_serial_acia_TCB_no_RTS_no_TIE) { + return 0; + } + if (acia->status_reg & k_serial_acia_status_RDRF) { + return 0; + } + return 1; +} + +static int +mc6850_receive(ACIA *acia, uint8_t byte) { + + if (acia->status_reg & k_serial_acia_status_RDRF) { +/* log_do_log(k_log_serial, k_log_info, "receive buffer full"); */ + if ( ! acia->msg_printed_recv_buf_full ) { + log_warn("acia (%s): receive buffer full", acia->name); + acia->msg_printed_recv_buf_full = 1; + } + } else { + acia->msg_printed_recv_buf_full = 0; + } + acia->status_reg |= k_serial_acia_status_RDRF; + acia->status_reg &= ~k_serial_acia_status_FE; + acia->status_reg &= ~k_serial_acia_status_PE; + acia->rx_data_reg = byte; + + mc6850_update_irq_and_status_read(acia); + + if (acia->msg_printed_recv_buf_full) { return 0; } + + return 1; + +} + +static void +mc6850_clear_receive_state(ACIA *acia) { + acia->rx_shift_reg = 0; + acia->rx_shift_reg_count = 0; + acia->parity_accumulator = 0; + acia->rxc_count = 0; + acia->rx_sr_parity_error = 0; + acia->rx_sr_framing_error = 0; +} + +static void +mc6850_transfer_sr_to_receive(ACIA *acia) { + int ok = mc6850_receive(acia, acia->rx_shift_reg); + if (!ok) { + /* Overflow condition. Data wasn't read in time. Overflow condition will be + * raised once the existing data is read. + */ + acia->rx_sr_overflow = 1; + } else { + if (acia->rx_sr_parity_error) { + acia->status_reg |= k_serial_acia_status_PE; + } + if (acia->rx_sr_framing_error) { + acia->status_reg |= k_serial_acia_status_FE; + } + } + mc6850_clear_receive_state(acia); + + mc6850_update_irq_and_status_read(acia); +} + + +static int mc6850_receive_bit (ACIA *acia, uint8_t bit) { + + int e; + + e = TAPE_E_OK; + + /*bit = (('0'==code) ? 0 : 1);*/ + + switch (acia->state) { + case k_mc6850_state_need_start: + if (bit == 0) { + if (acia->rx_shift_reg != 0) { + log_warn("acia: BUG: start bit: rx_shift_reg != 0 (%x)", acia->rx_shift_reg); + e = TAPE_E_BUG; + } else if (acia->rx_shift_reg_count != 0) { + log_warn("acia: BUG: start bit: rx_shift_reg_count != 0 (%u)", acia->rx_shift_reg_count); + e = TAPE_E_BUG; + } else if (acia->parity_accumulator != 0) { + log_warn("acia: BUG: start bit: parity_accumulator != 0 (%d)", acia->parity_accumulator); + e = TAPE_E_BUG; + } else if (acia->rx_sr_parity_error != 0) { + log_warn("acia: BUG: start bit: rx_sr_parity_error != 0 (%d)", acia->rx_sr_parity_error); + e = TAPE_E_BUG; + } else if (acia->rx_sr_framing_error != 0) { + log_warn("acia: BUG: start bit: rx_sr_framing_error != 0 (%d)", acia->rx_sr_framing_error); + e = TAPE_E_BUG; + } + acia->state = k_mc6850_state_need_data; + } + break; + case k_mc6850_state_need_data: + if (bit) { + acia->rx_shift_reg |= (1 << acia->rx_shift_reg_count); + acia->parity_accumulator = ! acia->parity_accumulator; + } + acia->rx_shift_reg_count++; + if (acia->control_reg & k_serial_acia_control_8_bits) { + if (acia->rx_shift_reg_count == 8) { + if (acia->control_reg & 0x08) { + acia->state = k_mc6850_state_need_parity; + } else { + acia->state = k_mc6850_state_need_stop; + } + } + } else { + if (acia->rx_shift_reg_count == 7) { + acia->state = k_mc6850_state_need_parity; + } + } + break; + case k_mc6850_state_need_parity: + if (bit) { + acia->parity_accumulator = ! acia->parity_accumulator; + } + if (acia->parity_accumulator != ((acia->control_reg & 0x04) >> 2)) { +/* log_do_log(k_log_serial, k_log_warning, "incorrect parity bit"); */ + acia->rx_sr_parity_error = 1; + } + acia->state = k_mc6850_state_need_stop; + break; + case k_mc6850_state_need_stop: + if (bit != 1) { +/* log_do_log(k_log_serial, k_log_warning, "incorrect stop bit"); */ + acia->rx_sr_framing_error = 1; + } + mc6850_transfer_sr_to_receive(acia); + acia->state = k_mc6850_state_need_start; + break; + default: + /*assert(0);*/ + log_warn("acia: BUG: rx bad state (%d)", acia->state); + e = TAPE_E_BUG; + break; + } + + return e; + +} + +static void +mc6850_reset(ACIA *acia) { + + int is_CTS = !!(acia->status_reg & k_serial_acia_status_CTS); + + acia->rx_data_reg = 0; + acia->tx_data_reg = 0; + + acia->state = k_mc6850_state_need_start; + mc6850_clear_receive_state(acia); + acia->rx_sr_overflow = 0; + + /* Set TDRE (transmit data register empty). Clear everything else. */ + acia->status_reg = k_serial_acia_status_TDRE; + + acia->control_reg = 0; + + /* Reset of the ACIA cannot change external line levels. Make sure any status + * bits they affect are kept. + */ + /* These calls call mc6850_update_irq_and_status_read(). */ + mc6850_set_DCD(acia, acia->line_dcd); + mc6850_set_CTS(acia, is_CTS); + + reset_tx_shift_register(acia); + acia->tx_data_reg_loaded = 0; + +} + +void +mc6850_power_on_reset(ACIA *acia) { + acia_init(acia); /* TOHv4 */ +} + + + +#include "tape.h" +static uint8_t +mc6850_read(ACIA *acia, uint8_t reg) { + + if (reg == 0) { + /* Status register. */ + /* TOHv4: double mischief */ + if (acia->misframe_next_read) { + acia->misframe_next_read = 0; + acia->status_reg_for_read |= k_serial_acia_status_FE; + } + if (acia->gen_parity_error_next_read) { + acia->gen_parity_error_next_read = 0; + acia->status_reg_for_read |= k_serial_acia_status_PE; + } + return acia->status_reg_for_read; + } else { + /* Data register. */ + /* TOHv4: mischief */ + if (acia->corrupt_next_read) { + acia->corrupt_next_read = 0; + acia->rx_data_reg = ~(acia->rx_data_reg); + } + acia->status_reg &= ~k_serial_acia_status_DCD; + acia->status_reg &= ~k_serial_acia_status_OVRN; + if (acia->rx_sr_overflow) { + /*assert(acia->status_reg & k_serial_acia_status_RDRF);*/ + /* MC6850: "The Overrun does not occur in the Status Register until the + * valid character prior to Overrun has been read. + */ + acia->rx_sr_overflow = 0; + acia->status_reg |= k_serial_acia_status_OVRN; + /* RDRF remains asserted. */ + } else { + acia->status_reg &= ~k_serial_acia_status_RDRF; + } + + mc6850_update_irq_and_status_read(acia); + return acia->rx_data_reg; + } +} + + +static void +mc6850_write(ACIA *acia, uint8_t reg, uint8_t val) { + int e; + + if (reg == 0) { + /* Control register. */ + if ((val & 0x03) == 0x03) { + /* Master reset. */ + /* EMU NOTE: the data sheet says, "Master reset does not affect other + * Control Register bits.", however this does not seem to be fully + * correct. If there's an interrupt active at reset time, it is cleared + * after reset. This suggests it could be clearing CR, including the + * interrupt select bits. We clear CR, and this is sufficient to get the + * Frak tape to load. + * (After a master reset, the chip actually seems to be dormant until CR + * is written. One specific example is that TDRE is not indicated until + * CR is written -- a corner case we do not yet implement.) + */ + mc6850_reset(acia); + } else { + acia->control_reg = val; + } + + } else { + + if (acia->tx_data_reg_loaded) { + log_warn("acia: warning: data reg write: already loaded! (old %x, " + "new %x, pc %x, statusreg %x, _for_read %x)", + acia->tx_data_reg, val, pc,acia->status_reg, acia->status_reg_for_read); + } + + acia->tx_data_reg = val; + acia->tx_data_reg_loaded = 1; + + /* oof. */ + acia->status_reg &= ~k_serial_acia_status_TDRE; + +#ifdef BUILD_TAPE_BUGGY_LUMPY_TDRE_DISTRIBUTION + acia->txc_count = 0; /* synchronise, wreck TDRE delay distribution, introduce bug */ +#endif + + /* TOHv4: tx_hook() is attached to the act + of writing to the tx data register (rather than + the data register being copied to the shift register, + or any bits being shifted out of the shift register). + This enables polling-free operation for simple ACIA + use cases, like Music 2000, with acia->tx_no_polling set. */ + + if (acia->tx_hook != NULL) { + e = acia->tx_hook(acia, val); /* consume byte */ + if (0 != e) { + log_warn("acia: warning: acia[%s]->tx_hook() returns code %d", + acia->name, e); + } + } + + if (acia->tx_no_polling) { + /* TOHv4: Poll-free implementation for Music 2000. + Acknowledge TDRE immediately; consume bytes + immediately. Shift register is unused. + Transmission is conducted via the tx_hook + mechanism above. */ + acia->tx_data_reg_loaded = 0; + acia->tx_shift_reg_loaded = 0; + acia->tx_shift_reg_shift = 0; + acia->status_reg |= k_serial_acia_status_TDRE; + } + + } + + mc6850_update_irq_and_status_read(acia); + +} + +/* END BEEBJIT CODE */ diff -Nu b-em-40246d4-vanilla/src/acia.h b-em-40246d4-TOHv4.2/src/acia.h --- b-em-40246d4-vanilla/src/acia.h 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/acia.h 2025-07-06 14:17:40.693657342 +0100 @@ -1,33 +1,193 @@ #ifndef __INC_ACIA_H #define __INC_ACIA_H +/* TOHv3: export some of these */ +#define ACIA_STATREG_TXD_REG_EMP 0x02 +#define ACIA_STATREG_CTS 0x08 +#define ACIA_STATREG_FRAMING_ERR 0x10 +#define ACIA_STATREG_PARITY_ERR 0x40 + +#define ACIA_CTRLREG_TXCTRL_1 0x20 +#define ACIA_CTRLREG_TXCTRL_2 0x40 +#define ACIA_CTRLREG_DIVIDE_1 0x1 +#define ACIA_CTRLREG_DIVIDE_2 0x2 + +#define ACIA_CTRLREG_MASK_TXCTRL (ACIA_CTRLREG_TXCTRL_1 | ACIA_CTRLREG_TXCTRL_2) /* 0x60 */ +#define ACIA_CTRLREG_MASK_DIVIDE (ACIA_CTRLREG_DIVIDE_1 | ACIA_CTRLREG_DIVIDE_2) /* 0x3 */ + +#define ACIA_TX_PIPELINE_LEN_MAX 5 + +/* begin beebjit */ +/* (c) Chris Evans, under GPL */ +enum { + k_serial_acia_status_RDRF = 0x01, + k_serial_acia_status_TDRE = 0x02, + k_serial_acia_status_DCD = 0x04, + k_serial_acia_status_CTS = 0x08, + k_serial_acia_status_FE = 0x10, + k_serial_acia_status_OVRN = 0x20, + k_serial_acia_status_PE = 0x40, + k_serial_acia_status_IRQ = 0x80, +}; +/* end beebjit */ + + typedef struct acia ACIA; +#include "tape2.h" /* TOHv4: for BUILD_TAPE_INCOMPATIBLE_NEW_SAVE_STATES */ + + +/* TOHv3.3: formerly in tape2.h, then uef.h, now it's here */ +typedef struct serial_framing_s { + + char parity; /* 'N', 'O', or 'E' */ + + /* This is intended as a "nominal baud rate"; + it's approximate. So it holds e.g. 1200 baud, + not 1201.9: */ + int32_t nominal_baud; /* (1200 * 1024) / (ULA divider * ACIA divider) */ + + uint8_t num_data_bits; + uint8_t num_stop_bits; + +} serial_framing_t; + +#include "tape2.h" /* for BUILD_TAPE_DEV_MENU */ + struct acia { + + uint8_t line_cts; /* CTS status, e.g. from serial ULA */ + uint8_t line_dcd; /* TOHv2, TOHv3.3 */ + uint8_t control_reg; uint8_t status_reg; + uint8_t status_reg_for_read; /* TOHv3.3: from beebjit */ uint8_t rx_data_reg; uint8_t tx_data_reg; - void (*set_params)(ACIA *acia, uint8_t val); - void (*rx_hook)(ACIA *acia, uint8_t byte); - void (*tx_hook)(ACIA *acia, uint8_t byte); - void (*tx_end)(ACIA *acia); - void *udata; + + /* TOHv3.3: from beebjit: */ + // uint32_t rx_shift_reg_count; + uint8_t rx_shift_reg_count; /* TOHv4: use u8 instead of u32 for savestate */ + uint8_t rx_shift_reg; + + /* TOHv4: used int8 rather than int from beebjit + * (simplifies save states) */ + int8_t rx_sr_overflow; + int8_t rx_sr_parity_error; + int8_t rx_sr_framing_error; + int8_t state; + int8_t parity_accumulator; /* for RX */ + + /* TOHv4.1: parity TX support was missing! */ + /* This is bitflipped whenever there is a 1-bit, so it will be + * nonzero if an odd number of 1-bits have happened */ + uint8_t tx_odd_ones; + + int32_t rxc_count; /* TOHv4: for clock division */ + int32_t txc_count; /* TOHv4: for clock division */ + + /* TOHv3.3: Callbacks now return an error code */ + int (*set_params)(ACIA *acia, uint8_t val); +/* void (*rx_hook)(ACIA *acia, uint8_t byte); */ /* not needed */ + int (*tx_hook)(ACIA *acia, uint8_t byte); + int (*tx_end)(ACIA *acia); /* serial-to-file uses this */ + int (*reset_hook)(ACIA *acia); + void *udata; /* contains tape_state_t for tape system */ + + /* TOHv3: internal shift register state */ + uint8_t tx_shift_reg_value; + int8_t tx_shift_reg_shift; /* current amount of shift */ + uint8_t tx_shift_reg_loaded; /* is the shift reg loaded? */ + + uint8_t tx_data_reg_loaded; + + /* TOHv3.3: moved from tape vars; implement hoglet delay */ + uint8_t tx_waiting_to_set_tdre; + + uint8_t msg_printed_recv_buf_full; + +#ifdef BUILD_TAPE_DEV_MENU + uint8_t corrupt_next_read; + uint8_t misframe_next_read; + uint8_t gen_parity_error_next_read; +#endif + const char name[8]; unsigned intnum; + + /* TOHv4: for Music 2000, we simplify things by + introducing this setting that acknowledges TDRE + immediately on data write, obliviating the need + for any TX-side polling. + + If TX data is consumed bytewise from an ACIA's client + rather than bitwise, and you do not need a realistically + delayed TDRE, then set this flag; you avoid having to + call acia_poll_2txc() at all. */ + uint8_t tx_no_polling; + }; -uint8_t acia_read(ACIA *acia, uint16_t addr); -void acia_write(ACIA *acia, uint16_t addr, uint8_t val); -void acia_poll(ACIA *acia); -void acia_receive(ACIA *acia, uint8_t val); +void acia_init (ACIA *a); /* TOHv3.2 */ + +#ifdef BUILD_TAPE_INCOMPATIBLE_NEW_SAVE_STATES +int acia_savestate_BEMSNAP4(ACIA *acia, FILE *f); +int acia_loadstate_BEMSNAP4(ACIA *acia, FILE *f); +#endif void acia_savestate(ACIA *acia, FILE *f); void acia_loadstate(ACIA *acia, FILE *f); + +uint8_t acia_read (ACIA *acia, uint16_t addr); +void acia_write (ACIA *acia, uint16_t addr, uint8_t val) ; + + void acia_dcdhigh(ACIA *acia); void acia_dcdlow(ACIA *acia); + + +uint8_t acia_rx_awaiting_start (ACIA *a); /* TOHv3.3 */ +int acia_receive_bit_code (ACIA *acia, char code); +int acia_receive_bit (ACIA *acia, uint8_t bit); + +uint8_t acia_rx_frame_ready (ACIA *a) ; +uint8_t acia_transmit (ACIA *acia); + +/* TOHv3.3: from tape.c */ +void acia_get_framing (uint8_t ctrl_reg, serial_framing_t *f); +void acia_hack_tx_reset_shift_register (ACIA *a); + void acia_ctson(ACIA *acia); void acia_ctsoff(ACIA *acia); +/* TOHv4: */ +/* Call this at RXC rate and it will do the clock division + and set *fire_rxc_out=1 when a bit is required. + When this happens, client should call acia_receive_bit() + to supply the next bit. The status register may then be + examined to discover when the completed frame is ready. */ +int acia_poll_rxc (ACIA *acia, + uint8_t *fire_rxc_out, + int32_t *divider_out); + +/* Meanwhile call this at 2TXC; it will deal with TDRE and + operate the TX shift register. Bits that fall out of the + shift register become available here if bit_or_zero_out + is nonzero (which happens on every other call). + Note that bit_or_zero_out uses ASCII values '1' and '0' + rather than binary 1 and 0 (and '\0' if the bit is not + ready yet). */ +int acia_poll_2txc (ACIA *acia, + char *bit_or_zero_out, /* will be zero half the time */ + int32_t *tx_divider_out); + +int acia_run_tx_shift_register(ACIA *acia, serial_framing_t *f, char *bit_out); + +/* TOHv4, for no-poll serial-to-file */ +void acia_hack_consume_tx_byte_immediately (ACIA *acia); + +/* TOHv4-rc2: exposed */ +void acia_update_irq_and_status_read(ACIA *acia); + #endif Common subdirectories: b-em-40246d4-vanilla/src/ARMulator and b-em-40246d4-TOHv4.2/src/ARMulator diff -Nu b-em-40246d4-vanilla/src/config.c b-em-40246d4-TOHv4.2/src/config.c --- b-em-40246d4-vanilla/src/config.c 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/config.c 2025-07-06 14:17:40.693657342 +0100 @@ -147,7 +147,9 @@ return cdefault; } -void config_load(ALLEGRO_PATH *path) +/* TOHv4.2: add doing_testing argument to prevent certain config + * settings being loaded */ +void config_load(ALLEGRO_PATH *path, uint8_t doing_testing) { if (path || (path = find_cfg_file("b-em", ".cfg"))) { const char *cpath = al_path_cstr(path, ALLEGRO_NATIVE_PATH_SEP); @@ -172,8 +174,8 @@ mmb_fn = get_config_strdup("disc", "mmb"); if (!mmccard_fn) mmccard_fn = get_config_strdup("disc", "mmcard"); - if (!tape_fn) - tape_fn = get_config_path("tape", "tape"); + if ( ( ! doing_testing ) && !tape_vars.load_filename) /* TOHv4.2 merge */ + tape_vars.load_filename = get_config_path("tape", "tape"); al_remove_config_key(bem_cfg, "", "video_resize"); al_remove_config_key(bem_cfg, "", "tube6502speed"); @@ -229,8 +231,9 @@ mode7_fontfile = get_config_string("video", "mode7font", "saa5050"); - if (!fasttape && get_config_bool("tape", "fasttape", false)) - fasttape = true; + /* TOHv4.2 merge */ + if ( ( ! doing_testing ) && !tape_vars.overclock && get_config_bool("tape", "fasttape", false)) + tape_vars.overclock = true; scsi_enabled = get_config_bool("disc", "scsienable", 0); ide_enable = get_config_bool("disc", "ideenable", 0); @@ -338,9 +341,10 @@ set_config_string("disc", "mmccard", mmccard_fn); set_config_bool("disc", "defaultwriteprotect", defaultwriteprot); - if (tape_loaded) - al_set_config_value(bem_cfg, "tape", "tape", al_path_cstr(tape_fn, ALLEGRO_NATIVE_PATH_SEP)); - else + /* TOHv3, TOHv4.2 */ + if (TAPE_IS_LOADED(tape_state.filetype_bits)) { /* TOHv4: now a macro */ + al_set_config_value(bem_cfg, "tape", "tape", al_path_cstr(tape_vars.load_filename, ALLEGRO_NATIVE_PATH_SEP)); + } else al_remove_config_key(bem_cfg, "tape", "tape"); set_config_bool(NULL, "autopause", autopause); @@ -381,7 +385,7 @@ set_config_int("video", "ledvisibility", vid_ledvisibility); set_config_string("video", "mode7font", mode7_fontfile); - set_config_bool("tape", "fasttape", fasttape); + set_config_bool("tape", "fasttape", tape_vars.overclock); set_config_bool("disc", "scsienable", scsi_enabled); set_config_bool("disc", "ideenable", ide_enable); diff -Nu b-em-40246d4-vanilla/src/config.h b-em-40246d4-TOHv4.2/src/config.h --- b-em-40246d4-vanilla/src/config.h 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/config.h 2025-07-06 14:17:40.693657342 +0100 @@ -3,7 +3,7 @@ extern ALLEGRO_CONFIG *bem_cfg; -void config_load(ALLEGRO_PATH *path); +void config_load(ALLEGRO_PATH *path, uint8_t doing_testing); /* TOHv4.2 merge: add doing_testing */ void config_save(void); int get_config_int(const char *sect, const char *key, int idefault); diff -Nu b-em-40246d4-vanilla/src/csw.c b-em-40246d4-TOHv4.2/src/csw.c --- b-em-40246d4-vanilla/src/csw.c 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/csw.c 2025-07-06 14:17:40.693657342 +0100 @@ -1,5 +1,8 @@ /*B-em v2.2 by Tom Walker CSW cassette support*/ + +/* TOHv3: rewrite, 'Diminished' */ + #include #include #include @@ -8,310 +11,628 @@ #include "csw.h" #include "tape.h" -int csw_toneon=0; +/* #define CSW_DEBUG_PRINT */ -static int csw_intone = 1, csw_indat = 0, csw_datbits = 0, csw_enddat = 0; -static uint8_t *csw_dat = NULL; -static int csw_point; -static uint8_t csw_head[0x34]; -static int csw_skip = 0; -static int csw_loop = 1; -int csw_ena; +#define CSW_BODY_MAXLEN_RAW (8 * 1024 * 1024) +/*#define CSW_MAXLEN_OVERALL CSW_BODY_MAXLEN_RAW*/ -static void csw_read_failed(FILE *csw_f, const char *fn) -{ - if (ferror(csw_f)) - log_error("csw: read error on '%s': %s", fn, strerror(errno)); - else - log_error("csw: premature EOF on '%s'", fn); +#define CSW_HEADER_LEN 0x34 + +#define CSW_MAJOR_VERSION 2 + +#define CSW_RATE_MIN 8000 +#define CSW_RATE_MAX 192000 +#define CSW_RATE_DEFAULT 44100 /* might be compatibility problems if not 44.1K? */ + +#define CSW_MAX_PULSES (CSW_BODY_MAXLEN_RAW) /* good enough */ + +#define MAGIC "Compressed Square Wave\x1a" + +static void populate_pulsetimes_from_rate (csw_state_t *csw); +static char pulse_get_bit_value (uint32_t pulse, uint32_t thresh_smps, uint32_t max_smps); + +#ifdef CSW_DEBUG_PRINT +static void debug_csw_print (csw_state_t *csw); + +static void debug_csw_print (csw_state_t *csw) { + csw_header_t *hdr; + hdr = &(csw->header); + printf("csw:\n"); + printf(" version = %u.%u\n", hdr->version_maj, hdr->version_min); + printf(" rate = %u\n", hdr->rate); + printf(" compressed = %u\n", hdr->compressed); + printf(" flags = %u\n", hdr->flags); + printf(" ext_len = 0x%x\n", hdr->ext_len); } +#endif /*CSW_DEBUG_PRINT*/ + +/* TOHv3.2: bugfix, not enough brackets! */ +#define CSW_VALID_RATE(rate) (((rate) >= CSW_RATE_MIN) && ((rate) <= CSW_RATE_MAX)) -#define CSW_MAXLEN (8 * 1024 * 1024) -void csw_load(const char *fn) +int csw_load_file (const char *fn, csw_state_t *csw) { - FILE *csw_f; - int end,c; - unsigned long destlen = CSW_MAXLEN; - uint8_t *tempin; - - /*Allocate buffer*/ - if (!csw_dat) { - if (!(csw_dat = malloc(CSW_MAXLEN))) { - log_error("csw: out of memory reading '%s'", fn); - return; + int e; + uint8_t *buf; + uint32_t len; + uint8_t *body; + + buf = NULL; + body = NULL; + + memset(csw, 0, sizeof(csw_state_t)); + + e = tape_load_file (fn, 0, &buf, &len); + if (TAPE_E_OK != e) { return e; } + + do { /* try { */ + + uint32_t header_ext_len; + uint32_t body_len, raw_body_len; + uint32_t start_of_data; + int trunc; + uint32_t np, n; + uint32_t header_num_pulses; + + body = NULL; + body_len = 0; + raw_body_len = 0; + start_of_data = 0; + e = TAPE_E_OK; + + /* file needs to be at least HEADER_LEN long. + (used to be HEADER_LEN + one pulse, but we want a no-pulses + CSW to be valid, so the restriction has been loosened) */ + if (len < (/*1 +*/ CSW_HEADER_LEN)) { + log_warn("csw: header is truncated: '%s'", fn); + e = TAPE_E_CSW_HEADER_TRUNCATED; + break; + } + + if (0 != memcmp (MAGIC, buf, strlen(MAGIC))) { + log_warn("csw: bad magic: '%s'", fn); + e = TAPE_E_CSW_BAD_MAGIC; + break; + } + + csw->header.version_maj = buf[0x17]; + csw->header.version_min = buf[0x18]; + if (csw->header.version_maj != CSW_MAJOR_VERSION) { + log_warn("csw: unknown major version %u: '%s'", csw->header.version_maj, fn); + e = TAPE_E_CSW_BAD_VERSION; + break; + } + if ((csw->header.version_min != 0) && (csw->header.version_min != 1)) { + log_warn("csw: unknown minor version %u: '%s'", csw->header.version_min, fn); + e = TAPE_E_CSW_BAD_VERSION; + break; + } + if (1 == csw->header.version_min) { + log_warn("csw: minor version 1: CSW violates spec: '%s'", fn); } + + csw->header.rate = tape_read_u32 (buf + 0x19); + if ( ! CSW_VALID_RATE(csw->header.rate) ) { + log_warn("csw: bad sample rate %d: '%s'", csw->header.rate, fn); + e = TAPE_E_CSW_BAD_RATE; + break; + } + + /* read fill, and make alloc = fill */ + header_num_pulses = tape_read_u32(buf + 0x1d); + + /* blank (no-pulses) CSWs are now allowed; this restriction has been loosened */ + if ((header_num_pulses > CSW_MAX_PULSES)) { /* || (0 == header_num_pulses)) {*/ + log_warn("csw: bad number of pulses in header (%d): '%s'", header_num_pulses, fn); + e = TAPE_E_CSW_HEADER_NUM_PULSES; + break; + } + + if ((buf[0x21] != 1) && (buf[0x21] != 2)) { + log_warn("csw: bad compression value (%d): '%s'", buf[0x21], fn); + e = TAPE_E_CSW_COMP_VALUE; + break; + } + csw->header.compressed = (buf[0x21] == 2); + + if (buf[0x22] & 0xf8) { + log_warn("csw: bad flags value (0x%x): '%s'", buf[0x22], fn); + e = TAPE_E_CSW_BAD_FLAGS; + break; + } + /* Yuck. Someone out there has created non-standard CSWs with + an illegal version of 2.1, flags set to an illegal value, + and an illegal non-empty header extension. Permit these, but + you know who you are. */ + if ((buf[0x22] != 0) && (buf[0x22] != 1)) { + log_warn("csw: illegal flags (&%x): CSW violates spec: '%s'", buf[0x22], fn); + } + csw->header.flags = buf[0x22]; + + /* consume remaining header */ + csw->header.ext_len = buf[0x23]; + header_ext_len = csw->header.ext_len; /* 32-bit edition */ + if (header_ext_len != 0) { + log_warn("csw: hdr. ext. len. is nonzero (&%x); " + "may be an illegal CSW that abuses hdr. ext. for " + "purposes of graffiti", header_ext_len); + if ((CSW_HEADER_LEN + header_ext_len) > len) { /* TOHv3.2: again, 0 pulses is OK */ + log_warn("csw: file truncated during hdr. ext. (&%x > len (&%x)) : '%s'", + (CSW_HEADER_LEN + header_ext_len), len, fn); + e = TAPE_E_CSW_HEADER_TRUNCATED; + break; + } + } + + start_of_data = (CSW_HEADER_LEN + header_ext_len); + + raw_body_len = len - start_of_data; + +#ifdef CSW_DEBUG_PRINT + debug_csw_print(csw); +#endif + + /* compressed? */ + if (csw->header.compressed) { + + e = tape_decompress (&body, &body_len, buf + start_of_data, raw_body_len); + if (TAPE_E_OK != e) { break; } + + } else { + /* uncompressed; just copy */ + if (raw_body_len >= CSW_BODY_MAXLEN_RAW) { + log_warn("csw: raw body is too large (%u): '%s'", raw_body_len, fn); + e = TAPE_E_CSW_BODY_LARGE; + break; + } + body = buf + start_of_data; + body_len = raw_body_len; + } + + /* parse pulses */ + for (n=0, np=0, trunc=0; + (TAPE_E_OK == e) && (n < body_len); + n++, np++) { + uint32_t pulse; + if (0 == body[n]) { + if ((n + 4) >= body_len) { + log_warn("csw: truncated? '%s'", fn); + trunc = 1; + break; + } + pulse = tape_read_u32(body + n + 1); + /* TOHv3.2: we're going to be strict about this; if the pulse len + is < 256 then there's no need for the five-byte extended pulse + format -- it could have been done in one byte instead. I'm going to + reject five-byte pulses with values < 256, and codify that with + a unit test. This also prevents sneaking in a zero-duration + pulse this way. Those things are of-the-devil. */ + if (pulse < 256) { + log_warn("csw: 5-byte CSW pulse but duration < 256 (%u): '%s'\n", + pulse, fn); + e = TAPE_E_CSW_LONGPULSE_UNDER_256; + break; + } + n+=4; + } else { + pulse = body[n]; + } + e = csw_append_pulse(csw, pulse); + } + + if (TAPE_E_OK != e) { break; } + + if (csw->pulses_fill != header_num_pulses) { + log_warn("csw: pulses in body (%u) does not match value in header (%u): '%s'", + csw->pulses_fill, header_num_pulses, fn); + e = TAPE_E_CSW_PULSES_MISMATCH; + break; + } + + if (trunc) { break; } + + populate_pulsetimes_from_rate(csw); + + } while (0); /* catch { } */ + + free(buf); + /* TOHv3.2: fixed potential free(NULL) */ + if (csw->header.compressed && (body != NULL)) { free(body); } + + return e; + +} + + +static void populate_pulsetimes_from_rate (csw_state_t *csw) { + double d; + /* "three-halves" threshold: */ + d = ( (((double) csw->header.rate) / TAPE_1200_HZ) + + (((double) csw->header.rate) / (2.0 * TAPE_1200_HZ)) ) / 4.0; + csw->thresh_smps = (uint32_t) d; /* round down */ + d = (((double) csw->header.rate) / TAPE_1200_HZ); + csw->len_1200th_smps = (uint32_t) (d + 0.5); + csw->len_1200th_smps_perfect = d; +} + + +void csw_finish (csw_state_t *csw) { + if (NULL != csw->pulses) { + free(csw->pulses); } + memset(csw, 0, sizeof(csw_state_t)); +} + - /*Open file and get size*/ - if (!(csw_f = fopen(fn,"rb"))) { - log_warn("csw: unable to open CSW file '%s': %s", fn, strerror(errno)); - return; - } - fseek(csw_f, -1, SEEK_END); - end = ftell(csw_f); - fseek(csw_f, 0, SEEK_SET); - - /*Read header*/ - if (fread(csw_head, 0x34, 1, csw_f) == 1) { - for (c = 0; c < csw_head[0x23]; c++) - getc(csw_f); - /*Allocate temporary memory and read file into memory*/ - end -= ftell(csw_f); - if ((tempin = malloc(end))) { - if (fread(tempin, end, 1, csw_f) == 1) { - fclose(csw_f); - if (2 == csw_head[0x21]) { - /*Decompress*/ - uncompress(csw_dat, (unsigned long *)&destlen, tempin, end); - } else { - if (end >= CSW_MAXLEN) { - log_error("csw: out of memory reading '%s'", fn); - return; - } - memcpy(csw_dat, tempin, end); - destlen = end; + +uint8_t csw_peek_eof (csw_state_t *csw) { + return (csw->cur_pulse >= csw->pulses_fill); +} + + + +int csw_read_1200th (csw_state_t *csw, char *out_1200th) { + + char b; + + b = '?'; + + do { + + /* buffer for lookahead pulse values; + - 2 elements used for a 0-tone (1/1200s) + - 4 elements used for a 1-tone (1/1200s) */ + + char v[4] = { 'X','X','X','X' }; + + uint8_t n; + uint32_t k; + uint8_t lookahead; + char q; + int wins; + + if (csw->cur_pulse >= csw->pulses_fill) { + return TAPE_E_EOF; + } + + /* convert the next 4 pulses into + bit values that correspond to their length; so at 44.1K + ~9 becomes '1', and ~18 becomes '0': */ + for (n=0, k = csw->cur_pulse; + (n < 4) && (k < csw->pulses_fill); + n++, k++) { + v[n] = pulse_get_bit_value(csw->pulses[k], csw->thresh_smps, csw->len_1200th_smps); + } + + lookahead = 4; + + /* test for '1' first, and then '0' afterwards; + lookahead is halved after testing for '1': */ + for (k=0, q='1', wins=0; (k < 2); k++, lookahead >>= 1, q='0') { + if (q == v[0]) { /* first value matches the wanted value, proceed ... */ + for (n=0; (n < lookahead); n++) { + if (q == v[n]) { wins++; } } - free(tempin); - /*Reset data pointer*/ - csw_point = 0; - acia_dcdhigh(&sysacia); - tapellatch = (1000000 / (1200 / 10)) / 64; - tapelcount = 0; - tape_loaded = 1; - csw_ena = 1; - return; + if (wins < lookahead) { + break; /* insufficient correct-length pulses found in lookahead; failure */ + } + csw->cur_pulse += lookahead; + csw->sub_pulse = 0; + b = q; /* got it */ + break; } - else { - csw_read_failed(csw_f, fn); - free(tempin); + } + if (b==q) { break; } + + if ('S' == v[0]) { + if (csw->sub_pulse >= csw->pulses[csw->cur_pulse]) { + csw->sub_pulse = 0; + (csw->cur_pulse)++; + } else { + csw->sub_pulse += csw->len_1200th_smps; } + b = 'S'; + break; } - else - log_error("csw: out of memory reading '%s'", fn); + + /* give up; advance pulse and try again: */ + (csw->cur_pulse)++; + + } while (b == '?'); + + *out_1200th = b; + return TAPE_E_OK; + +} + + +static char pulse_get_bit_value (uint32_t pulse, uint32_t thresh_smps, uint32_t max_smps) { + if (pulse <= thresh_smps) { + return '1'; + } else if (pulse < max_smps) { + return '0'; } - else - csw_read_failed(csw_f, fn); - fclose(csw_f); + return 'S'; } -void csw_close() -{ - if (csw_dat) { - free(csw_dat); - csw_dat = NULL; + +int csw_clone (csw_state_t *out, csw_state_t *in) { + memcpy(out, in, sizeof(csw_state_t)); + out->pulses = malloc(sizeof(uint32_t) * in->pulses_alloc); /* TOHv3 */ + if (NULL == out->pulses) { + log_warn("csw: out of memory allocating CSW pulses clone"); + return TAPE_E_MALLOC; } + /* ensure unused portion is zero: */ + memset(out->pulses, 0, sizeof(uint32_t) * in->pulses_alloc); /* TOHv3 */ + /* clone pulses */ + memcpy(out->pulses, in->pulses, sizeof(uint32_t) * in->pulses_fill); + return TAPE_E_OK; } -static int ffound, fdat; -static int infilenames; -static void csw_receive(uint8_t val) -{ - csw_toneon--; - if (infilenames) - { - ffound = 1; - fdat = val; - } - else - acia_receive(&sysacia, val); + +void csw_rewind (csw_state_t *csw) { + csw->cur_pulse = 0; + csw->sub_pulse = 0; } -void csw_poll() -{ - int c; - uint8_t dat; - if (!csw_dat) return; - - for (c = 0; c < 10; c++) - { - dat = csw_dat[csw_point++]; - - if (csw_point >= (8 * 1024 * 1024)) - { - csw_point = 0; - csw_loop = 1; - } - if (csw_skip) - csw_skip--; - else if (csw_intone && dat > 0xD) /*Not in tone any more - data start bit*/ - { - csw_point++; /*Skip next half of wave*/ - if (sysacia_tapespeed) csw_skip = 6; - csw_intone = 0; - csw_indat = 1; - - csw_datbits = csw_enddat = 0; - acia_dcdlow(&sysacia); - return; - } - else if (csw_indat && csw_datbits != -1 && csw_datbits != -2) - { - csw_point++; /*Skip next half of wave*/ - if (sysacia_tapespeed) csw_skip = 6; - csw_enddat >>= 1; - - if (dat <= 0xD) - { - csw_point += 2; - if (sysacia_tapespeed) csw_skip += 6; - csw_enddat |= 0x80; - } - csw_datbits++; - if (csw_datbits == 8) - { - csw_receive(csw_enddat); - - csw_datbits = -2; - return; - } - } - else if (csw_indat && csw_datbits == -2) /*Deal with stop bit*/ - { - csw_point++; - if (sysacia_tapespeed) csw_skip = 6; - if (dat <= 0xD) - { - csw_point += 2; - if (sysacia_tapespeed) csw_skip += 6; - } - csw_datbits = -1; - } - else if (csw_indat && csw_datbits == -1) - { - if (dat <= 0xD) /*Back in tone again*/ - { - acia_dcdhigh(&sysacia); - csw_toneon = 2; - csw_indat = 0; - csw_intone = 1; - csw_datbits = 0; - return; - } - else /*Start bit*/ - { - csw_point++; /*Skip next half of wave*/ - if (sysacia_tapespeed) csw_skip += 6; - csw_datbits = 0; - csw_enddat = 0; - } - } + +#define TAPE_CSW_ALLOC_DELTA 100000 + +/* TOHv3: */ +int csw_append_pulse (csw_state_t *csw, + uint32_t pulse_len_smps) { + uint32_t z; + uint32_t *p; + int e; + /* sanity */ + if (0 == pulse_len_smps) { + /* nope */ + log_warn("csw: BUG: attempting to append zero-length pulse"); + return TAPE_E_CSW_WRITE_NULL_PULSE; + } + e = csw_init_blank_if_necessary(csw); + if (TAPE_E_OK != e) { return e; } + /* + for CSW, we must keep the value in the header consistent with the + actual buffer fill at all times, because the thing may be cloned at any time + for tape catalogue. + */ + if (csw->pulses_fill >= csw->pulses_alloc) { + z = csw->pulses_fill + TAPE_CSW_ALLOC_DELTA; + p = realloc(csw->pulses, z * sizeof(uint32_t)); + if (NULL == p) { + log_warn("csw: out of memory enlarging CSW"); + return TAPE_E_MALLOC; } + memset(p + csw->pulses_fill, + 0, + sizeof(uint32_t) * TAPE_CSW_ALLOC_DELTA); + csw->pulses = p; + csw->pulses_alloc = z; + } + csw->pulses[csw->pulses_fill] = pulse_len_smps; + (csw->pulses_fill)++; + return TAPE_E_OK; } -#define getcswbyte() ffound = 0; \ - while (!ffound && !csw_loop) \ - { \ - csw_poll(); \ - } \ - if (csw_loop) break; -static uint8_t ffilename[16]; -void csw_findfilenames() -{ - int temp, temps, tempd, tempi, tempsk, tempspd; - uint8_t tb; - int c; - int fsize = 0; - char s[256]; - uint32_t run, load; - uint8_t status; - int skip; - if (!csw_dat) return; - temp = csw_point; - temps = csw_indat; - tempi = csw_intone; - tempd = csw_datbits; - tempsk = csw_skip; - tempspd = sysacia_tapespeed; - csw_point = 0; - - csw_indat = csw_intone = csw_datbits = csw_skip = 0; - csw_intone = 1; - sysacia_tapespeed = 0; - -// gzseek(csw,12,SEEK_SET); - csw_loop = 0; - infilenames = 1; - while (!csw_loop) - { -// log_debug("Start\n"); - ffound = 0; - while (!ffound && !csw_loop) - { - csw_poll(); +/* TOHv3: */ +int csw_append_pulse_fractional_length (csw_state_t *csw, + double pulse_len_smps) { + + uint32_t pulse; + int e; + + e = csw_init_blank_if_necessary(csw); + if (TAPE_E_OK != e) { return e; } + + /* round down */ + pulse = (uint32_t) pulse_len_smps; + + /* jitter it, if accumulated error exceeds limit */ + if (csw->accumulated_error_smps > 0.5) { + pulse += 1; + } + + /* update the accumulated error */ + csw->accumulated_error_smps += (pulse_len_smps - ((double) pulse)); + + return csw_append_pulse(csw, pulse); + +} + + + +/* TOHv3: */ +int csw_init_blank_if_necessary (csw_state_t *csw) { + + csw_header_t *h; + + /* use the rate field in the header to determine + whether we have an existing valid CSW header or not */ + if ( CSW_VALID_RATE (csw->header.rate) ) { return TAPE_E_OK; } + + memset(csw, 0, sizeof(csw_state_t)); + h = &(csw->header); + + h->rate = CSW_RATE_DEFAULT; /* 44.1KHz */ + h->compressed = 1; + h->ext_len = 0; + h->version_maj = CSW_MAJOR_VERSION; + h->version_min = 0; + + populate_pulsetimes_from_rate(csw); + + return TAPE_E_OK; + +} + + +/* TOHv3: */ +int csw_append_leader (csw_state_t *csw, uint32_t num_1200ths) { + uint32_t i; + int e; + csw_init_blank_if_necessary(csw); + for (i=0, e=TAPE_E_OK; (TAPE_E_OK == e) && (i < (num_1200ths * 4)); i++) { + e = csw_append_pulse_fractional_length (csw, + csw->len_1200th_smps_perfect / 4.0); + } + return e; +} + + +/* TOHv3: */ +int csw_append_silence (csw_state_t *csw, float len_s) { + uint32_t smps; + float sane; + int e; + /* we must do this now, as we need a valid sample rate: */ + csw_init_blank_if_necessary(csw); + sane = (2.0f * TAPE_1200TH_IN_S_FLT); + if (len_s < sane) { + log_warn("csw: very short silence (%f s); using %f s instead", len_s, sane); + len_s = sane; + } + /* we'll use a pair of pulses, as Quadbike does, + in order to preserve the polarity. */ + smps = (uint32_t) ((len_s / 2.0f) * ((float) csw->header.rate)); + e = csw_append_pulse (csw, smps); /* 1 = also update header->num_pulses */ + if (TAPE_E_OK == e) { e = csw_append_pulse(csw, smps); } + return e; +} + +#include "b-em.h" /* for VERSION_STR */ + +/* TOHv3: */ +int csw_build_output (csw_state_t *csw, + uint8_t compress, + char **out, + size_t *len_out) { + + uint8_t pass; + size_t pos, zbuf_len; + char *buf, *zbuf; + int e; + uint8_t header[CSW_HEADER_LEN]; + size_t pos_compress_flag; + uint8_t verstr[16]; + size_t verlen; + + buf = NULL; + zbuf = NULL; + zbuf_len = 0; + + /* if generating a completely blank (no pulses) CSW, the CSW won't have a + header yet! */ + csw_init_blank_if_necessary(csw); + + pos=strlen(MAGIC); + memcpy(header, MAGIC, pos); + header[pos++] = csw->header.version_maj; + header[pos++] = csw->header.version_min; + tape_write_u32(header + pos, csw->header.rate); + pos+=4; + tape_write_u32(header + pos, csw->pulses_fill); + pos+=4; + pos_compress_flag = pos; /* TOHv4: defer compress value for now */ + pos++; + header[pos++] = csw->header.flags; + header[pos++] = 0; /* hdr ext len */ + /* needs to be null-terminated (apparently), although + frankly this is stupid, because readers should never + assume that the terminator may be relied upon */ + memcpy(verstr, " ", 16); + verlen = strlen(VERSION_STR); + if (verlen > 15) { verlen = 15; } + memcpy(verstr, VERSION_STR, verlen); /* do NOT copy null terminator from VERSION_STR */ + memcpy(header+pos, verstr, 16); + + for (pass=0, pos=0, *len_out=0, *out=NULL, e=TAPE_E_OK; + (pass < 2) && (csw->pulses_fill > 0); /* TOHv3.2: prevent catastrophe if no pulses */ + pass++) { + + uint32_t n; + + for (n=0, pos=0; n < csw->pulses_fill; n++) { + if (csw->pulses[n] <= 255) { + if (1 == pass) { + buf[pos] = 0xff & csw->pulses[n]; } - if (csw_loop) break; -// log_debug("FDAT %02X csw_toneon %i\n",fdat,csw_toneon); - if (fdat == 0x2A && csw_toneon == 1) - { - c = 0; - do - { - ffound = 0; - while (!ffound && !csw_loop) - { - csw_poll(); - } - if (csw_loop) break; - ffilename[c++] = fdat; - } while (fdat != 0x0 && c <= 10); - if (csw_loop) break; - c--; - while (c < 13) ffilename[c++] = 32; - ffilename[c] = 0; - - getcswbyte(); - tb = fdat; - getcswbyte(); - load = tb | (fdat << 8); - getcswbyte(); - tb = fdat; - getcswbyte(); - load |= (tb | (fdat << 8)) << 16; - - getcswbyte(); - tb = fdat; - getcswbyte(); - run = tb | (fdat << 8); - getcswbyte(); - tb = fdat; - getcswbyte(); - run |= (tb | (fdat << 8)) << 16; - - getcswbyte(); - getcswbyte(); - - getcswbyte(); - tb = fdat; - getcswbyte(); - skip = tb | (fdat << 8); - - fsize += skip; - - getcswbyte(); - status = fdat; - -//log_debug("Got block - %08X %08X %02X\n",load,run,status); - if (status & 0x80) - { - sprintf(s, "%s Size %04X Load %08X Run %08X", ffilename, fsize, load, run); - cataddname(s); - fsize = 0; - } - for (c = 0; c < skip + 8; c++) - { - getcswbyte(); - } + pos += 1; + } else { + if (1 == pass) { + buf[pos] = 0; + tape_write_u32(((uint8_t *) buf) + pos + 1, csw->pulses[n]); /* returns void */ } + pos += 5; + } + } + if (0 == pass) { + buf = malloc(pos); + if (NULL == buf) { + log_warn("csw: build CSW output: out of memory (1)"); + return TAPE_E_MALLOC; + } + } + } + + /* TOHv3.2: if the payload is empty, force type-1 CSW, + * rather than messing about trying to compress zero bytes */ + if ( (0 == pos) || ( ! compress ) ) { + if (pos > 0) { /* TOHv4: avoid malloc(0) */ + /* no compression or no data => just copy it */ + zbuf = malloc(pos); + if (NULL == zbuf) { + log_warn("csw: build CSW output: out of memory (2)"); + e = TAPE_E_MALLOC; + } else { + zbuf_len = pos; + memcpy(zbuf, buf, zbuf_len); + } } - infilenames = 0; - csw_indat = temps; - csw_intone = tempi; - csw_datbits = tempd; - csw_skip = tempsk; - sysacia_tapespeed = tempspd; - csw_point = temp; - csw_loop = 0; + header[pos_compress_flag] = 1; /* TOHv4: deferred compress flag */ + } else { + /* compress it */ + e = tape_zlib_compress (buf, + pos, + 0, /* use zlib encoding */ + &zbuf, + &zbuf_len); + header[pos_compress_flag] = 2; /* TOHv4: deferred compress flag */ + } + + + /* we're finished with uncompressed buffer now */ + free(buf); + + if (TAPE_E_OK != e) { + if (zbuf != NULL) { free(zbuf); } + return e; + } + + /* lame */ + *out = malloc(zbuf_len + CSW_HEADER_LEN); + if (NULL == *out) { + log_warn("csw: build CSW output: out of memory (3)"); + if (zbuf != NULL) { free(zbuf); } /* TOHv4: NULL check */ + return TAPE_E_MALLOC; + } + memcpy(*out, header, CSW_HEADER_LEN); + if (zbuf_len > 0) { /* TOHv4: zbuf_len > 0 check */ + memcpy(*out + CSW_HEADER_LEN, zbuf, zbuf_len); + } + *len_out = zbuf_len + CSW_HEADER_LEN; + + if (zbuf != NULL) { free(zbuf); } /* TOHv4: NULL check */ + + return e; + } +#ifdef BUILD_TAPE_READ_POS_TO_EOF_ON_WRITE +int csw_ffwd_to_end (csw_state_t *csw) { + csw->cur_pulse = csw->pulses_fill; + return TAPE_E_OK; +} +#endif diff -Nu b-em-40246d4-vanilla/src/csw.h b-em-40246d4-TOHv4.2/src/csw.h --- b-em-40246d4-vanilla/src/csw.h 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/csw.h 2025-07-06 14:17:40.693657342 +0100 @@ -1,12 +1,66 @@ #ifndef __INC__CSW_H #define __INC__CSW_H -void csw_load(const char *fn); +#include "tape2.h" /* for BUILD_TAPE_READ_POS_TO_EOF_ON_WRITE */ + +typedef struct csw_header_s { + uint8_t compressed; + uint8_t ext_len; + uint8_t version_maj; + uint8_t version_min; + uint32_t rate; + uint8_t flags; +} csw_header_t; + +typedef struct csw_state_s { + + csw_header_t header; + uint32_t *pulses; + + /* TOHv3: for write support: */ + uint32_t pulses_fill; + uint32_t pulses_alloc; + /* for jittering pulse lengths, to obtain correct output frequencies: */ + double accumulated_error_smps; + + /* comparison to this pulse length threshold must be + p <= thresh : short pulse + p > thresh : long pulse */ + uint32_t thresh_smps; + uint32_t len_1200th_smps; + double len_1200th_smps_perfect; + + /* reader state: */ + uint32_t cur_pulse; + uint32_t sub_pulse; + +} csw_state_t; + +int csw_load_file (const char *fn, csw_state_t *csw); void csw_close(void); void csw_poll(void); void csw_findfilenames(void); +int csw_clone (csw_state_t *out, csw_state_t *in); +void csw_rewind (csw_state_t *csw); +int csw_read_1200th (csw_state_t *csw, char *out_1200th); +uint8_t csw_peek_eof (csw_state_t *csw); +void csw_finish (csw_state_t *csw); -extern int csw_ena; -extern int csw_toneon; +/* TOHv3: */ +int csw_append_pulse (csw_state_t *csw, + uint32_t pulse_len_smps); +int csw_append_pulse_fractional_length (csw_state_t *csw, + double pulse_len_smps); +int csw_init_blank_if_necessary (csw_state_t *csw); +int csw_append_leader (csw_state_t *csw, uint32_t num_1200ths); +int csw_append_silence (csw_state_t *csw, float len_s); +/* TOHv3: */ +int csw_build_output (csw_state_t *csw, + uint8_t compress, + char **out, + size_t *len_out); +#ifdef BUILD_TAPE_READ_POS_TO_EOF_ON_WRITE +int csw_ffwd_to_end (csw_state_t *csw); +#endif #endif Common subdirectories: b-em-40246d4-vanilla/src/darm and b-em-40246d4-TOHv4.2/src/darm diff -Nu b-em-40246d4-vanilla/src/gui-allegro.c b-em-40246d4-TOHv4.2/src/gui-allegro.c --- b-em-40246d4-vanilla/src/gui-allegro.c 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/gui-allegro.c 2025-07-06 14:17:40.693657342 +0100 @@ -35,6 +35,8 @@ #include "video.h" #include "video_render.h" #include "vdfs.h" +#include "serial.h" +#include "tapenoise.h" /* TOHv4.1-rc9 */ #if defined(HAVE_JACK_JACK_H) || defined(HAVE_ALSA_ASOUNDLIB_H) #define HAVE_LINUX_MIDI @@ -57,6 +59,24 @@ static ALLEGRO_MENU *disc_menu; static ALLEGRO_MENU *rom_menu; +/* TOHv3.2, 3.3: */ +static ALLEGRO_MENU *tape_opts_save_menu; +/* TOHv3.3: */ +static ALLEGRO_MENU *tape_opts_load_menu; +/* TOHv3: */ +static ALLEGRO_MENU *tape_save_menu; +static ALLEGRO_MENU *tape_main_menu; +/* can define BUILD_TAPE_DEV_MENU, to get the "Mischief..." menu items + within the Tape menu: */ +#ifdef BUILD_TAPE_DEV_MENU +static ALLEGRO_MENU *tape_dev_menu; +#endif +static ALLEGRO_MENU *create_tape_save_menu(void); +#ifdef BUILD_TAPE_DEV_MENU +static ALLEGRO_MENU *create_tape_dev_menu(void); +#endif +static void tape_save_menu_nothing(ALLEGRO_EVENT *ev); + static inline int menu_id_num(menu_id_t id, int num) { return (num << 8) | id; @@ -202,25 +222,98 @@ al_set_menu_item_caption(disc_menu, menu_id_num(IDM_DISC_EJECT, drive), temp); } + + + static ALLEGRO_MENU *create_tape_menu(void) { - ALLEGRO_MENU *menu = al_create_menu(); ALLEGRO_MENU *speed = al_create_menu(); - int nflags, fflags; + ALLEGRO_MENU *menu = al_create_menu(); + ALLEGRO_MENU *m_opts = al_create_menu(); /* TOHv3.2 */ + tape_opts_save_menu = al_create_menu(); + tape_opts_load_menu = al_create_menu(); /* TOHv3.3 */ + int fflags; al_append_menu_item(menu, "Load tape...", IDM_TAPE_LOAD, 0, NULL, NULL); - al_append_menu_item(menu, "Rewind tape", IDM_TAPE_REWIND, 0, NULL, NULL); + al_append_menu_item(menu, "Save tape copy...", IDM_TAPE_SAVE, 0, NULL, create_tape_save_menu()); al_append_menu_item(menu, "Eject tape", IDM_TAPE_EJECT, 0, NULL, NULL); - al_append_menu_item(menu, "Catalogue tape", IDM_TAPE_CAT, 0, NULL, NULL); - if (fasttape) { - nflags = ALLEGRO_MENU_ITEM_CHECKBOX; + al_append_menu_item(menu, "Rewind tape playback", IDM_TAPE_REWIND, 0, NULL, NULL); + fflags = ALLEGRO_MENU_ITEM_CHECKBOX | (tape_is_record_activated(&tape_vars) ? ALLEGRO_MENU_ITEM_CHECKED : 0); + al_append_menu_item(menu, "Record and append to tape", IDM_TAPE_RECORD, fflags, NULL, NULL); + fflags = 0; +#ifdef BUILD_TAPE_MENU_GREYOUT_CAT + if ( ! tape_peek_for_data(&tape_state) ) { + fflags |= ALLEGRO_MENU_ITEM_DISABLED; + } +#endif + al_append_menu_item(menu, "Catalogue tape", IDM_TAPE_CAT, fflags, NULL, NULL); + + /* // feature postponed + fflags = 0; + al_append_menu_item(menu, "Examine chunks", IDM_TAPE_EXAMINE, fflags, NULL, NULL); + */ + + /* updated for TOHv3.2: */ + if (tape_vars.overclock) { + /*nflags = ALLEGRO_MENU_ITEM_CHECKBOX;*/ + fflags = ALLEGRO_MENU_ITEM_CHECKBOX|ALLEGRO_MENU_ITEM_CHECKED; + } else { + /*nflags = ALLEGRO_MENU_ITEM_CHECKBOX|ALLEGRO_MENU_ITEM_CHECKED;*/ + fflags = ALLEGRO_MENU_ITEM_CHECKBOX; + } + al_append_menu_item(speed, "Overclock ACIA + fast DCD (dubious)", IDM_TAPE_TURBO_OVERCLOCK, fflags, NULL, NULL); + /*al_append_menu_item(speed, "Fast (unreliable)", IDM_TAPE_TURBO_SKIP, fflags, NULL, NULL);*/ + if (tape_vars.strip_silence_and_leader) { + fflags = ALLEGRO_MENU_ITEM_CHECKBOX|ALLEGRO_MENU_ITEM_CHECKED; + } else { + fflags = ALLEGRO_MENU_ITEM_CHECKBOX; + } + al_append_menu_item(speed, "Skip leader/gaps + fast DCD", IDM_TAPE_TURBO_SKIP, fflags, NULL, NULL); + al_append_menu_item(menu, "Options", 0, 0, NULL, m_opts); + al_append_menu_item(menu, "Turbo load", 0, 0, NULL, speed); + + /* SAVE OPTIONS */ + if (tape_vars.save_always_117) { fflags = ALLEGRO_MENU_ITEM_CHECKBOX|ALLEGRO_MENU_ITEM_CHECKED; } else { - nflags = ALLEGRO_MENU_ITEM_CHECKBOX|ALLEGRO_MENU_ITEM_CHECKED; fflags = ALLEGRO_MENU_ITEM_CHECKBOX; } - al_append_menu_item(speed, "Normal", IDM_TAPE_SPEED_NORMAL, nflags, NULL, NULL); - al_append_menu_item(speed, "Fast", IDM_TAPE_SPEED_FAST, fflags, NULL, NULL); - al_append_menu_item(menu, "Tape speed", 0, 0, NULL, speed); + al_append_menu_item(tape_opts_save_menu, "UEF: Emit baud chunk (117) before each block", + IDM_TAPE_OPTS_SAVE_UEF_FORCE_117, fflags, NULL, NULL); + + if (tape_vars.save_prefer_112) { + fflags = ALLEGRO_MENU_ITEM_CHECKBOX|ALLEGRO_MENU_ITEM_CHECKED; + } else { + fflags = ALLEGRO_MENU_ITEM_CHECKBOX; + } + al_append_menu_item(tape_opts_save_menu, "UEF: Use chunk 112, not 116, for gaps", + IDM_TAPE_OPTS_SAVE_UEF_FORCE_112, fflags, NULL, NULL); + + if (tape_vars.save_do_not_generate_origin_on_append) { + fflags = ALLEGRO_MENU_ITEM_CHECKBOX|ALLEGRO_MENU_ITEM_CHECKED; + } else { + fflags = ALLEGRO_MENU_ITEM_CHECKBOX; + } + al_append_menu_item(tape_opts_save_menu, "UEF: Suppress origin chunk (0) on append", + IDM_TAPE_OPTS_SAVE_UEF_SUPPRESS_ORGN_ON_APPEND, fflags, NULL, NULL); + + /* TOHv4.2 */ + if ( tape_vars.wav_use_phase_shift ) { + fflags = ALLEGRO_MENU_ITEM_CHECKBOX|ALLEGRO_MENU_ITEM_CHECKED; + } else { + fflags = ALLEGRO_MENU_ITEM_CHECKBOX; + } + al_append_menu_item(tape_opts_save_menu, "WAV: Use cosine, not sine", + IDM_TAPE_OPTS_SAVE_WAV_PHASE_SHIFT, fflags, NULL, NULL); + + /* LOAD OPTIONS */ + al_append_menu_item(tape_opts_load_menu, "Filter out phantom blocks", + IDM_TAPE_OPTS_LOAD_FILTER_PHANTOMS, fflags, NULL, NULL); + al_append_menu_item(m_opts, "Loading", 0, 0, NULL, tape_opts_load_menu); + al_append_menu_item(m_opts, "Saving", 0, 0, NULL, tape_opts_save_menu); /*m_uef_save);*/ +#ifdef BUILD_TAPE_DEV_MENU + al_append_menu_item(menu, "Mischief", IDM_TAPE_DEV, 0, NULL, create_tape_dev_menu()); +#endif + tape_main_menu = menu; return menu; } @@ -462,7 +555,8 @@ add_checkbox_item(menu, "Paula", IDM_SOUND_PAULA, sound_paula); add_checkbox_item(menu, "Printer port DAC", IDM_SOUND_DAC, sound_dac); add_checkbox_item(menu, "Disc drive noise", IDM_SOUND_DDNOISE, sound_ddnoise); - add_checkbox_item(menu, "Tape noise", IDM_SOUND_TAPE, sound_tape); + add_checkbox_item(menu, "Tape signal", IDM_SOUND_TAPE, sound_tape); + add_checkbox_item(menu, "Tape relay clicks", IDM_SOUND_TAPE_RELAY, sound_tape_relay); /* TOHv2 */ add_checkbox_item(menu, "Internal sound filter", IDM_SOUND_FILTER, sound_filter); sub = al_create_menu(); add_radio_set(sub, wave_names, IDM_WAVE, curwave); @@ -638,6 +732,104 @@ al_register_event_source(queue, al_get_default_menu_event_source()); } +/* TOHv3.2 */ +void gui_set_record_mode (uint8_t activated) { + int flags; + flags = activated ? ALLEGRO_MENU_ITEM_CHECKED : 0; + al_set_menu_item_flags(tape_main_menu, IDM_TAPE_RECORD, flags); +} + +/* TOHv3 */ +void gui_alter_tape_menus (uint8_t filetype_bits) { + + int flags; + + if (NULL == tape_save_menu) { + log_warn("gui: alter tape submenu: tape_save_menu is NULL"); + return; + } + + if (NULL == tape_main_menu) { + log_warn("gui: alter tape submenu: tape_main_menu is NULL"); + return; + } + + flags = (TAPE_FILETYPE_BITS_UEF & filetype_bits) ? 0 : ALLEGRO_MENU_ITEM_DISABLED; + + al_set_menu_item_flags(tape_save_menu, IDM_TAPE_SAVE_UEF, flags); + al_set_menu_item_flags(tape_save_menu, IDM_TAPE_SAVE_UEF_UNCOMP, flags); /* TOHv3.2 */ + + flags = (filetype_bits & TAPE_FILETYPE_BITS_TIBET) ? 0 : ALLEGRO_MENU_ITEM_DISABLED; + al_set_menu_item_flags(tape_save_menu, IDM_TAPE_SAVE_TIBET, flags); + al_set_menu_item_flags(tape_save_menu, IDM_TAPE_SAVE_TIBETZ, flags); + + flags = (filetype_bits & TAPE_FILETYPE_BITS_CSW) ? 0 : ALLEGRO_MENU_ITEM_DISABLED; + al_set_menu_item_flags(tape_save_menu, IDM_TAPE_SAVE_CSW, flags); + al_set_menu_item_flags(tape_save_menu, IDM_TAPE_SAVE_CSW_UNCOMP, flags); /* TOHv3.2 */ + + /* WAV becomes available when at least one other format is also available */ + flags = (TAPE_FILETYPE_BITS_NONE == filetype_bits) ? ALLEGRO_MENU_ITEM_DISABLED : 0; + al_set_menu_item_flags(tape_save_menu, IDM_TAPE_SAVE_WAV, flags); + + gui_alter_tape_menus_2(); + +} + +void gui_alter_tape_menus_2(void) { + int flags; + /* this one is for greying out Catalogue Tape in the main tape menu */ + flags = 0; +#ifdef BUILD_TAPE_MENU_GREYOUT_CAT + if (!tape_peek_for_data(&tape_state)) { + flags = ALLEGRO_MENU_ITEM_DISABLED; + } +#endif + al_set_menu_item_flags (tape_main_menu, IDM_TAPE_CAT, flags); +} + +/* TOHv3 */ +void gui_alter_tape_eject (char *path) { + size_t z, len, pfxlen, sfxlen, sfx2len; + char *s; + const char *pfx, *sfx, *sfx2; + if (NULL == path) { + al_set_menu_item_caption (tape_main_menu, + IDM_TAPE_EJECT, + "Eject tape"); + } else { + if (path[0]=='\0') { return; } + for (z = strlen(path); + (z > 0) && ((path[z-1] != '/') && (path[z-1] != '\\')); + z--) ; + /* decided to go with the colon rather than the brackets; + brackets are slightly confusing given that they're also + used for "Rewind (playback only)" */ + pfx = "Eject tape: "; + sfx = ""; + sfx2 = "..."; + pfxlen = strlen(pfx); + sfxlen = strlen(sfx); + sfx2len = strlen(sfx2); + len = strlen(path+z); +#define GUI_MENU_TAPEFILE_MAXLEN 48 + if (len > GUI_MENU_TAPEFILE_MAXLEN) { + len = GUI_MENU_TAPEFILE_MAXLEN - 3; + sfx = sfx2; + sfxlen = sfx2len; + } + s = malloc(len + 1 + pfxlen + sfxlen); + if (NULL == s) { + log_warn("gui: alter tape eject menu entry: malloc failed for filename"); + return; + } + memcpy (s, pfx, pfxlen); + memcpy (s+pfxlen, path+z, len); + memcpy (s+pfxlen+len, sfx, sfxlen+1); + al_set_menu_item_caption (tape_main_menu, IDM_TAPE_EJECT, s); + free(s); + } +} + void gui_allegro_destroy(ALLEGRO_EVENT_QUEUE *queue, ALLEGRO_DISPLAY *display) { al_unregister_event_source(queue, al_get_default_menu_event_source()); @@ -996,52 +1188,273 @@ static void tape_load_ui(ALLEGRO_EVENT *event) { const char *fpath; - if (!tape_fn || !(fpath = al_path_cstr(tape_fn, ALLEGRO_NATIVE_PATH_SEP))) + if (!tape_vars.load_filename || !(fpath = al_path_cstr(tape_vars.load_filename, ALLEGRO_NATIVE_PATH_SEP))) fpath = "."; - ALLEGRO_FILECHOOSER *chooser = al_create_native_file_dialog(fpath, "Choose a tape to load", "*.uef;*.csw", ALLEGRO_FILECHOOSER_FILE_MUST_EXIST); + ALLEGRO_FILECHOOSER *chooser = al_create_native_file_dialog(fpath, "Choose a tape to load", "*.uef;*.csw;*.tibet;*.tibetz", ALLEGRO_FILECHOOSER_FILE_MUST_EXIST); if (chooser) { ALLEGRO_DISPLAY *display = (ALLEGRO_DISPLAY *)(event->user.data2); if (al_show_native_file_dialog(display, chooser)) { if (al_get_native_file_dialog_count(chooser) > 0) { - tape_close(); + /* TOHv3 */ + tape_state_finish(&tape_state, 1); /* alter menus */ ALLEGRO_PATH *path = al_create_path(al_get_native_file_dialog_path(chooser, 0)); tape_load(path); - tape_fn = path; - tape_loaded = 1; + tape_vars.load_filename = path; } } } } -static void tape_rewind(void) -{ - tape_close(); - tape_load(tape_fn); +/* TOHv3 */ +/* FIXME: pass in tape_state and tape_vars */ +static int tape_save (ALLEGRO_EVENT *event, const char *ext) { + ALLEGRO_PATH *apath = tape_vars.save_filename; /* tape file chooser path */ + ALLEGRO_FILECHOOSER *chooser; + const char *fpath; + char name[20], title[70]; + int e; /* TOHv4.2 */ + + e = TAPE_E_OK; + + snprintf(name, sizeof(name), "new%s", strchr(ext, '.')); /* e.g. name = "new.ssd" */ + if (NULL != apath) { + apath = al_clone_path(apath); + al_set_path_filename(apath, name); + fpath = al_path_cstr(apath, ALLEGRO_NATIVE_PATH_SEP); + } else { + fpath = name; + } + snprintf(title, sizeof(title), "Choose a tape file name to create"); + if ((chooser = al_create_native_file_dialog(fpath, title, ext, ALLEGRO_FILECHOOSER_SAVE))) { + ALLEGRO_DISPLAY *display = (ALLEGRO_DISPLAY *)(event->user.data2); + if (al_show_native_file_dialog(display, chooser)) { + if (al_get_native_file_dialog_count(chooser) > 0) { + ALLEGRO_PATH *path = al_create_path(al_get_native_file_dialog_path(chooser, 0)); + /*disc_close(drive);*/ + /* ^^ we don't do this because tape doesn't work this way. + we only have one tape loaded, and we can save + repeated snapshots of it. We can load a completely + new tape, but that's using the "load tape" dialogue, + not the "save tape" one. */ + + /* update tape_save_fn */ + if (NULL != tape_vars.save_filename) { + al_destroy_path(tape_vars.save_filename); + } + tape_vars.save_filename = path; + + switch(menu_get_id(event)) { + case IDM_TAPE_SAVE_UEF: + case IDM_TAPE_SAVE_UEF_UNCOMP: /* TOHv3.2 */ + e = tape_generate_and_save_output_file (&tape_state, + TAPE_FILETYPE_BITS_UEF, + tape_vars.save_prefer_112, /* TOHv3.2 */ + (char *) al_path_cstr(path, ALLEGRO_NATIVE_PATH_SEP), + menu_get_id(event)==IDM_TAPE_SAVE_UEF, /* compress? */ + &sysacia); /* might need resetting */ + break; + case IDM_TAPE_SAVE_TIBET: + e = tape_generate_and_save_output_file (&tape_state, + TAPE_FILETYPE_BITS_TIBET, + 0, + (char *) al_path_cstr(path, ALLEGRO_NATIVE_PATH_SEP), + 0, /* no Z => no compression */ + &sysacia); + break; + case IDM_TAPE_SAVE_TIBETZ: + e = tape_generate_and_save_output_file (&tape_state, + TAPE_FILETYPE_BITS_TIBET, + 0, + (char *) al_path_cstr(path, ALLEGRO_NATIVE_PATH_SEP), + 1, /* with Z => compress */ + &sysacia); + break; + case IDM_TAPE_SAVE_CSW: + case IDM_TAPE_SAVE_CSW_UNCOMP: /* TOHv3.2 */ + e = tape_generate_and_save_output_file (&tape_state, + TAPE_FILETYPE_BITS_CSW, + 0, + (char *) al_path_cstr(path, ALLEGRO_NATIVE_PATH_SEP), + menu_get_id(event)==IDM_TAPE_SAVE_CSW, /* compress? */ + &sysacia); + break; + case IDM_TAPE_SAVE_WAV: /* TOHv4.2 */ + /* any valid available file type data makes it possible + to save a WAV */ + if (TAPE_FILETYPE_BITS_NONE != tape_state.filetype_bits) { + e = tapenoise_write_wav(&tape_state, + (char *) al_path_cstr (path, ALLEGRO_NATIVE_PATH_SEP), + tape_vars.wav_use_phase_shift); + } + break; + default: + break; + } + } + } + al_destroy_native_file_dialog(chooser); + } + if (fpath != name) + al_destroy_path(apath); + return e; +} + +/* TOHv3 */ +static ALLEGRO_MENU *create_tape_save_menu(void) + { + ALLEGRO_MENU *menu = al_create_menu(); + al_append_menu_item (menu, "UEF...", IDM_TAPE_SAVE_UEF, 0, NULL, NULL); + al_append_menu_item (menu, "UEF (uncompressed)...", IDM_TAPE_SAVE_UEF_UNCOMP, 0, NULL, NULL); /* TOHv3.2 */ + al_append_menu_item (menu, "TIBETZ...", IDM_TAPE_SAVE_TIBETZ, 0, NULL, NULL); + al_append_menu_item (menu, "TIBET...", IDM_TAPE_SAVE_TIBET, 0, NULL, NULL); + al_append_menu_item (menu, "CSW...", IDM_TAPE_SAVE_CSW, 0, NULL, NULL); + al_append_menu_item (menu, "CSW (uncompressed)...", IDM_TAPE_SAVE_CSW_UNCOMP, 0, NULL, NULL); /* TOHv3.2 */ + al_append_menu_item (menu, "WAV...", IDM_TAPE_SAVE_WAV, 0, NULL, NULL); /* TOHv4.2 */ + tape_save_menu = menu; + return menu; +} +#ifdef BUILD_TAPE_DEV_MENU +static ALLEGRO_MENU *create_tape_dev_menu(void) { + + ALLEGRO_MENU *menu = al_create_menu(); + +#define TAPE_DEV_COMEDY_EMOTES "" +// text version +/*#define TAPE_DEV_COMEDY_EMOTES " >:-)"*/ +// alt. unicode emoji version +/*#define TAPE_DEV_COMEDY_EMOTES " \xF0\x9F\x98\x88"*/ + + al_append_menu_item(menu, "Corrupt next ACIA read" TAPE_DEV_COMEDY_EMOTES, IDM_TAPE_CORRUPT_READ, 0, NULL, NULL); + al_append_menu_item(menu, "Mis-frame next ACIA read" TAPE_DEV_COMEDY_EMOTES, IDM_TAPE_MISFRAME_READ, 0, NULL, NULL); + al_append_menu_item(menu, "Generate parity error on next ACIA read" TAPE_DEV_COMEDY_EMOTES, IDM_TAPE_GEN_PARITY_ERROR, 0, NULL, NULL); + + tape_dev_menu = menu; + + return menu; +} +#endif + + +static void tape_save_menu_nothing(ALLEGRO_EVENT *ev) { + } static void tape_eject(void) { - tape_close(); - tape_loaded = 0; + tape_set_record_activated(&tape_state, &tape_vars, &sysacia, 0); + gui_set_record_mode(0); + tape_state_finish(&tape_state, 1); /* 1 = update menus */ } -static void tape_normal(ALLEGRO_EVENT *event) +/* TOHv3.2: repurposed */ +static void tape_toggle_turbo_overclock(ALLEGRO_EVENT *event) { ALLEGRO_MENU *menu = (ALLEGRO_MENU *)(event->user.data3); - if (fasttape) { - fasttape = false; - al_set_menu_item_flags(menu, IDM_TAPE_SPEED_FAST, ALLEGRO_MENU_ITEM_CHECKBOX); + /* toggle */ + if (tape_vars.overclock) { + tape_vars.overclock = false; + al_set_menu_item_flags(menu, IDM_TAPE_TURBO_OVERCLOCK, ALLEGRO_MENU_ITEM_CHECKBOX); + } else { + tape_vars.overclock = true; + al_set_menu_item_flags(menu, IDM_TAPE_TURBO_OVERCLOCK, ALLEGRO_MENU_ITEM_CHECKBOX|ALLEGRO_MENU_ITEM_CHECKED); } + + serial_recompute_dividers_and_thresholds (tape_vars.overclock, + tape_state.ula_ctrl_reg, + &(tape_state.ula_rx_thresh_ns), + &(tape_state.ula_tx_thresh_ns), + &(tape_state.ula_rx_divider), + &(tape_state.ula_tx_divider)); + } -static void tape_fast(ALLEGRO_EVENT *event) +static void tape_toggle_filter_phantoms (ALLEGRO_EVENT *event) { + ALLEGRO_MENU *menu = (ALLEGRO_MENU *)(event->user.data3); + + /* toggle */ + if (tape_vars.disable_phantom_block_protection) { + tape_vars.disable_phantom_block_protection = 0; + al_set_menu_item_flags(menu, + IDM_TAPE_OPTS_LOAD_FILTER_PHANTOMS, + ALLEGRO_MENU_ITEM_CHECKBOX|ALLEGRO_MENU_ITEM_CHECKED); + } else { + tape_vars.disable_phantom_block_protection = 1; + al_set_menu_item_flags(menu, + IDM_TAPE_OPTS_LOAD_FILTER_PHANTOMS, + ALLEGRO_MENU_ITEM_CHECKBOX); + } +} + +static void tape_toggle_force_112 (ALLEGRO_EVENT *event) { + ALLEGRO_MENU *menu = (ALLEGRO_MENU *)(event->user.data3); + + /* toggle */ + if (tape_vars.save_prefer_112) { + tape_vars.save_prefer_112 = 0; + al_set_menu_item_flags(menu, IDM_TAPE_OPTS_SAVE_UEF_FORCE_112, ALLEGRO_MENU_ITEM_CHECKBOX); + } else { + tape_vars.save_prefer_112 = 1; + al_set_menu_item_flags(menu, + IDM_TAPE_OPTS_SAVE_UEF_FORCE_112, + ALLEGRO_MENU_ITEM_CHECKBOX|ALLEGRO_MENU_ITEM_CHECKED); + } +} + +static void tape_toggle_force_117 (ALLEGRO_EVENT *event) { + ALLEGRO_MENU *menu = (ALLEGRO_MENU *)(event->user.data3); + + /* toggle */ + if (tape_vars.save_always_117) { + tape_vars.save_always_117 = 0; + al_set_menu_item_flags(menu, IDM_TAPE_OPTS_SAVE_UEF_FORCE_117, ALLEGRO_MENU_ITEM_CHECKBOX); + } else { + tape_vars.save_always_117 = 1; + al_set_menu_item_flags(menu, + IDM_TAPE_OPTS_SAVE_UEF_FORCE_117, + ALLEGRO_MENU_ITEM_CHECKBOX|ALLEGRO_MENU_ITEM_CHECKED); + } +} + +static void tape_toggle_append_origin (ALLEGRO_EVENT *event) { + ALLEGRO_MENU *menu = (ALLEGRO_MENU *)(event->user.data3); + + /* toggle */ + if (tape_vars.save_do_not_generate_origin_on_append) { + tape_vars.save_do_not_generate_origin_on_append = 0; + al_set_menu_item_flags(menu, + IDM_TAPE_OPTS_SAVE_UEF_SUPPRESS_ORGN_ON_APPEND, + ALLEGRO_MENU_ITEM_CHECKBOX); + } else { + tape_vars.save_do_not_generate_origin_on_append = 1; + al_set_menu_item_flags(menu, + IDM_TAPE_OPTS_SAVE_UEF_SUPPRESS_ORGN_ON_APPEND, + ALLEGRO_MENU_ITEM_CHECKBOX|ALLEGRO_MENU_ITEM_CHECKED); + } +} + +/* TOHv3.2: repurposed */ +static void tape_toggle_turbo_skip(ALLEGRO_EVENT *event) { ALLEGRO_MENU *menu = (ALLEGRO_MENU *)(event->user.data3); - if (!fasttape) { - fasttape = true; - al_set_menu_item_flags(menu, IDM_TAPE_SPEED_NORMAL, ALLEGRO_MENU_ITEM_CHECKBOX); + if ( tape_vars.strip_silence_and_leader ) { + tape_vars.strip_silence_and_leader = 0; + al_set_menu_item_flags(menu, IDM_TAPE_TURBO_SKIP, ALLEGRO_MENU_ITEM_CHECKBOX); + } else { + tape_vars.strip_silence_and_leader = 1; + al_set_menu_item_flags(menu, IDM_TAPE_TURBO_SKIP, ALLEGRO_MENU_ITEM_CHECKBOX|ALLEGRO_MENU_ITEM_CHECKED); + } +} + +static void tape_toggle_wav_use_phase_shift(ALLEGRO_EVENT *event) { + ALLEGRO_MENU *menu = (ALLEGRO_MENU *)(event->user.data3); + if ( tape_vars.wav_use_phase_shift ) { + tape_vars.wav_use_phase_shift = 0; + al_set_menu_item_flags(menu, IDM_TAPE_OPTS_SAVE_WAV_PHASE_SHIFT, ALLEGRO_MENU_ITEM_CHECKBOX); + } else { + tape_vars.wav_use_phase_shift = 1; + al_set_menu_item_flags(menu, IDM_TAPE_OPTS_SAVE_WAV_PHASE_SHIFT, ALLEGRO_MENU_ITEM_CHECKBOX|ALLEGRO_MENU_ITEM_CHECKED); } } @@ -1289,21 +1702,85 @@ case IDM_TAPE_LOAD: tape_load_ui(event); break; + /* TOHv3 */ + case IDM_TAPE_SAVE: + tape_save_menu_nothing(event); + break; + case IDM_TAPE_SAVE_UEF: + case IDM_TAPE_SAVE_UEF_UNCOMP: /* TOHv3.2 */ + tape_save(event, "*.uef"); + break; + case IDM_TAPE_SAVE_TIBET: + tape_save(event, "*.tibet"); + break; + case IDM_TAPE_SAVE_TIBETZ: + tape_save(event, "*.tibetz"); + break; + case IDM_TAPE_SAVE_CSW: + case IDM_TAPE_SAVE_CSW_UNCOMP: /* TOHv3.2 */ + tape_save(event, "*.csw"); + break; + case IDM_TAPE_SAVE_WAV: /* TOHv4.2 */ + tape_save(event, "*.wav"); + break; + case IDM_TAPE_RECORD: + /* toggle */ + tape_set_record_activated (&tape_state, + &tape_vars, + &sysacia, + ! tape_is_record_activated (&tape_vars)); + break; +#ifdef BUILD_TAPE_DEV_MENU + case IDM_TAPE_DEV: + tape_save_menu_nothing(event); + break; + case IDM_TAPE_CORRUPT_READ: + sysacia.corrupt_next_read = 1; + break; + case IDM_TAPE_MISFRAME_READ: + /* can use Atic Atac's third-stage loader (after the "red rain") to test this one ... */ + sysacia.misframe_next_read = 1; + break; + case IDM_TAPE_GEN_PARITY_ERROR: + /* can use Atic Atac's third-stage loader (after the "red rain") to test this one? */ + sysacia.gen_parity_error_next_read = 1; + break; +#endif case IDM_TAPE_REWIND: - tape_rewind(); + tape_rewind_2(&tape_state); /* now lives on tape.c */ break; case IDM_TAPE_EJECT: tape_eject(); break; - case IDM_TAPE_SPEED_NORMAL: - tape_normal(event); + case IDM_TAPE_TURBO_OVERCLOCK: + tape_toggle_turbo_overclock(event); break; - case IDM_TAPE_SPEED_FAST: - tape_fast(event); + case IDM_TAPE_TURBO_SKIP: + tape_toggle_turbo_skip(event); break; case IDM_TAPE_CAT: gui_tapecat_start(); break; + /* // feature postponed + case IDM_TAPE_EXAMINE: + gui_tape_examine_start(&tape_vars, &tape_state); + break; + */ + case IDM_TAPE_OPTS_SAVE_UEF_FORCE_117: + tape_toggle_force_117(event); + break; + case IDM_TAPE_OPTS_SAVE_UEF_FORCE_112: + tape_toggle_force_112(event); + break; + case IDM_TAPE_OPTS_SAVE_UEF_SUPPRESS_ORGN_ON_APPEND: + tape_toggle_append_origin(event); + break; + case IDM_TAPE_OPTS_SAVE_WAV_PHASE_SHIFT: + tape_toggle_wav_use_phase_shift(event); + break; + case IDM_TAPE_OPTS_LOAD_FILTER_PHANTOMS: + tape_toggle_filter_phantoms(event); + break; case IDM_ROMS_LOAD: rom_load(event); break; @@ -1375,6 +1852,11 @@ break; case IDM_SOUND_TAPE: sound_tape = !sound_tape; + tapenoise_activated_hook(); + break; + /* TOHv2: */ + case IDM_SOUND_TAPE_RELAY: + sound_tape_relay = !sound_tape_relay; break; case IDM_SOUND_FILTER: sound_filter = !sound_filter; diff -Nu b-em-40246d4-vanilla/src/gui-allegro.h b-em-40246d4-TOHv4.2/src/gui-allegro.h --- b-em-40246d4-vanilla/src/gui-allegro.h 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/gui-allegro.h 2025-07-06 14:17:40.693657342 +0100 @@ -1,6 +1,8 @@ #ifndef __INC_GUI_ALLEGRO_H #define __INC_GUI_ALLEGRO_H +#include "tape2.h" /* some build options for tape */ + typedef enum { IDM_ZERO, IDM_FILE_RESET, @@ -44,11 +46,32 @@ IDM_DISC_VDFS_ENABLE, IDM_DISC_VDFS_ROOT, IDM_TAPE_LOAD, + IDM_TAPE_SAVE, /* TOHv3 */ + IDM_TAPE_SAVE_UEF, /* TOHv3 */ + IDM_TAPE_SAVE_UEF_UNCOMP, /* TOHv3.2 */ + IDM_TAPE_SAVE_TIBET, /* TOHv3 */ + IDM_TAPE_SAVE_TIBETZ, /* TOHv3 */ + IDM_TAPE_SAVE_CSW, /* TOHv3 */ + IDM_TAPE_SAVE_CSW_UNCOMP, /* TOHv3.2 */ + IDM_TAPE_SAVE_WAV, /* TOHv4.2: WAV mode */ + IDM_TAPE_RECORD, /* TOHv3 */ +#ifdef BUILD_TAPE_DEV_MENU + IDM_TAPE_CORRUPT_READ, /* TOHv3 */ + IDM_TAPE_MISFRAME_READ, /* TOHv3 */ + IDM_TAPE_GEN_PARITY_ERROR, /* TOHv3 */ + IDM_TAPE_DEV, /* TOHv3 */ +#endif IDM_TAPE_REWIND, IDM_TAPE_EJECT, IDM_TAPE_CAT, - IDM_TAPE_SPEED_NORMAL, - IDM_TAPE_SPEED_FAST, + /*IDM_TAPE_EXAMINE,*/ /* (feature postponed) */ + IDM_TAPE_TURBO_OVERCLOCK, /* TOHv3.2 */ + IDM_TAPE_TURBO_SKIP, /* TOHv3.2 */ + IDM_TAPE_OPTS_SAVE_UEF_FORCE_117, /* TOHv3.2 */ + IDM_TAPE_OPTS_SAVE_UEF_FORCE_112, /* TOHv3.2 */ + IDM_TAPE_OPTS_SAVE_UEF_SUPPRESS_ORGN_ON_APPEND, /* TOHv3.2 */ + IDM_TAPE_OPTS_SAVE_WAV_PHASE_SHIFT, /* TOHv4.2 */ + IDM_TAPE_OPTS_LOAD_FILTER_PHANTOMS, /* TOHv3.3, v4.2 */ IDM_ROMS_LOAD, IDM_ROMS_CLEAR, IDM_ROMS_RAM, @@ -73,6 +96,7 @@ IDM_SOUND_DAC, IDM_SOUND_DDNOISE, IDM_SOUND_TAPE, + IDM_SOUND_TAPE_RELAY, /* TOHv2 */ IDM_SOUND_FILTER, IDM_WAVE, IDM_SID_TYPE, @@ -120,5 +144,8 @@ extern void gui_allegro_event(ALLEGRO_EVENT *event); extern void gui_allegro_set_eject_text(int drive, ALLEGRO_PATH *path); extern void gui_set_disc_wprot(int drive, bool enabled); - +extern void gui_alter_tape_menus(uint8_t filetype_bits); +extern void gui_alter_tape_menus_2(void); +extern void gui_alter_tape_eject (char *path) ; +extern void gui_set_record_mode (uint8_t activated); /* TOHv3.2 */ #endif diff -Nu b-em-40246d4-vanilla/src/linux-gui.c b-em-40246d4-TOHv4.2/src/linux-gui.c --- b-em-40246d4-vanilla/src/linux-gui.c 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/linux-gui.c 2025-07-06 14:17:40.693657342 +0100 @@ -140,8 +140,8 @@ discmenu[6].flags = (writeprot[1]) ? D_SELECTED : 0; discmenu[7].flags = (defaultwriteprot) ? D_SELECTED : 0; discmenu[9].flags = (vdfs_enabled) ? D_SELECTED : 0; - tapespdmenu[0].flags = (!fasttape) ? D_SELECTED : 0; - tapespdmenu[1].flags = (fasttape) ? D_SELECTED : 0; + tapespdmenu[0].flags = (!tape_vars.overclock) ? D_SELECTED : 0; + tapespdmenu[1].flags = (tape_vars.overclock) ? D_SELECTED : 0; for (x = 0; x < NUM_MODELS; x++) modelmenu[x].flags = 0; for (x = 0; x < NUM_MODELS; x++) { if (curmodel == (intptr_t)modelmenu[x].dp) @@ -419,13 +419,13 @@ int gui_normal() { - fasttape = false; + tape_vars.overclock = false; gui_update(); return D_CLOSE; } int gui_fast() { - fasttape = true; + tape_vars.overclock = true; gui_update(); return D_CLOSE; } @@ -442,13 +442,13 @@ char tempname[260]; int ret; int xsize = windx - 32, ysize = windy - 16; - memcpy(tempname, al_path_cstr(tape_fn, ALLEGRO_NATIVE_PATH_SEP), 260); - ret=file_select_ex("Please choose a tape image", tempname, "UEF;CSW", 260, xsize, ysize); + memcpy(tempname, al_path_cstr(tape_vars.load_filename, ALLEGRO_NATIVE_PATH_SEP), 260); + ret=file_select_ex("Please choose a tape image", tempname, "UEF;CSW;TIBET;TIBETZ", 260, xsize, ysize); if (ret) { tape_close(); - tape_fn = al_create_path(tempname); - tape_load(tape_fn); + tape_vars.load_filename = al_create_path(tempname); + tape_load(tape_vars.load_filename); tape_loaded = 1; } return D_CLOSE; @@ -457,7 +457,7 @@ int gui_rewind() { tape_close(); - tape_load(tape_fn); + tape_load(tape_vars.load_filename); return D_CLOSE; } diff -Nu b-em-40246d4-vanilla/src/main.c b-em-40246d4-TOHv4.2/src/main.c --- b-em-40246d4-vanilla/src/main.c 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/main.c 2025-07-06 14:19:23.628774745 +0100 @@ -123,6 +123,11 @@ int num_emu_speeds = NUM_DEFAULT_SPEEDS; static int emu_speed_normal = 4; +/* TOHv4.2 merge */ +static int toh_cli_handle_tapesave (char *argv); +static int toh_cli_handle_tapetest (char *argv); +static int toh_cli_handle_expire (char *argv); + void main_reset() { m6502_reset(); @@ -154,8 +159,8 @@ "-disc disc.ssd - load disc.ssd into drives :0/:2\n" "-disc1 disc.ssd - load disc.ssd into drives :1/:3\n" "-autoboot - boot disc in drive :0\n" - "-tape tape.uef - load tape.uef\n" - "-fasttape - set tape speed to fast\n" + "-tape tape.uef - load tape.uef (or .csw/.tibet/.tibetz)\n" + "-fasttape - enable tape overclocking\n" "-Fx - set maximum video frames skipped\n" "-s - scanlines display mode\n" "-i - interlace display mode\n" @@ -173,7 +178,20 @@ "-printcmd c - printer output via command as text\n" "-printcmdbin c - printer output via command as binary\n" "-vroot host-dir - set the VDFS root\n" - "-vdir guest-dir - set the initial (boot) dir in VDFS\n\n"; + "-vdir guest-dir - set the initial (boot) dir in VDFS\n" + "-rs423file file - write RS423 output to file\n" + "-tapetest - bits: 1=quit @ EOF | 2=quit @ err | 4=cat @ boot |\n" /* TOHv3, v4 */ + " 8=cat @ end | 16=slow startup\n" /* TOHv4.1 */ + "-record - begin recording to tape at B-Em startup\n" /* TOHv3 */ + "-tapesave file - save tape copy on exit (format set by extension)\n" /* TOHv3 */ + "-tapeskip - skip silence and leader when loading tapes (faster)\n" /* TOHv3.2 */ + "-tape117 - when saving UEF, emit baud chunk &117 before every block\n" /* TOHv3.2 */ + "-tape112 - when saving UEF, use chunk &112 rather than &116 for silence\n" /* TOHv3.2 */ + "-tapeno0 - when appending to loaded UEF, do not emit origin chunk &0\n" /* TOHv3.2 */ + "-tapewavcosine - when saving WAV, use alternative phase (change tone at peak)\n" /* TOHv4.2 */ + "-expire - quit, with predictable exit code, after emulated seconds\n" /* TOHv3.3 */ + "-tapenopbp - do not filter out phantom blocks on tape load\n" + "\n"; static double main_calc_timer(int speed) { @@ -251,6 +269,11 @@ OPT_PASTE_KBD, OPT_PRINT, OPT_GROUND, + /* TOHv4.2 merge: */ + OPT_TAPETEST, + OPT_TAPESAVE, + OPT_RS423FILE, + OPT_EXPIRE } opt_state; void main_init(int argc, char *argv[]) @@ -265,8 +288,21 @@ ALLEGRO_PATH *cfg_fn = NULL; const char *ext, *exec_fn = NULL, *log_file = NULL; const char *vroot = NULL, *vdir = NULL; + int e; /* TOHv4.2 merge */ + char *rs423_fn; + + /* TOHv3.3, TOHv4, TOHv4.2 merge */ + shutdown_exit_code = SHUTDOWN_OK; + tape_vars.testing_mode = 0; + e = SHUTDOWN_OK; + rs423_fn = NULL; + + /* TOHv4.2 merge */ + tape_init(&tape_state, &tape_vars); while (--argc) { + //size_t len; /* TOHv3, TOHv4.2 merge */ +ALLEGRO_PATH *path; char *arg = *++argv; switch (state) { case OPT_GROUND: @@ -282,7 +318,31 @@ } else if (!strcasecmp(arg, "fullscreen")) fullscreen = 1; - else if (!strcasecmp(arg, "tape")) + /* begin TOHv4.2 merge ... */ + else if (!strcasecmp(arg, "rs423file")) { + state = OPT_RS423FILE; + } else if (!strcasecmp(arg, "tapesave")) { + state = OPT_TAPESAVE; + } else if (!strcasecmp(arg, "tapetest")) { /* TOHv3: must come before -tape */ + state = OPT_TAPETEST; + } else if (!strcasecmp(arg, "expire")) { /* TOHv3.3, TOHv4 */ + state = OPT_EXPIRE; + } else if (!strcasecmp(arg, "tapeskip")) { /* TOHv3.2: must come before -tape */ + tape_vars.strip_silence_and_leader = 1; + } else if (!strcasecmp(arg, "tape117")) { /* TOHv3.2: must come before -tape */ + tape_vars.save_always_117 = 1; + } else if (!strcasecmp(arg, "tape112")) { /* TOHv3.2: must come before -tape */ + tape_vars.save_prefer_112 = 1; + } else if (!strcasecmp(arg, "tapeno0")) { /* TOHv3.2: must come before -tape */ + tape_vars.save_do_not_generate_origin_on_append = 1; + } else if (!strcasecmp(arg, "tapenopbp")) { /* TOHv3.2: must come before -tape */ + tape_vars.disable_phantom_block_protection = 1; + } else if (!strcasecmp(arg, "tapewavcosine")) { /* TOHv4.2 */ + tape_vars.wav_use_phase_shift = 1; + } else if (!strcasecmp(arg, "record")) { /* TOHv3 */ + e = tape_set_record_activated(&tape_state, &tape_vars, NULL, 1); + /* end TOHv4.2 merge */ + } else if (!strcasecmp(arg, "tape")) state = OPT_TAPE; else if (!strcasecmp(arg, "disc") || !strcasecmp(arg, "-disk")) state = OPT_DISC1; @@ -293,7 +353,7 @@ else if (arg[0] == 't' || arg[0] == 'T') sscanf(&arg[1], "%i", &curtube); else if (!strcasecmp(arg, "fasttape")) - fasttape = true; + tape_vars.overclock = true; /* TOHv4.2 merge */ else if (!strcasecmp(arg, "autoboot")) autoboot = 150; else if (arg[0] == 'f' || arg[0]=='F') { @@ -340,8 +400,7 @@ else if (!strcasecmp(arg, "printcmdbin")) { print_dest = PDEST_PIPE_BIN; state = OPT_PRINT; - } - else { + } else { if (*arg != 'h' && *arg != '?') fprintf(stderr, "b-em: unrecognised option '-%s'\n", arg); fwrite(helptext, sizeof helptext-1, 1, stdout); @@ -349,17 +408,20 @@ } } else { - ALLEGRO_PATH *path = al_create_path(arg); + path = al_create_path(arg); ext = al_get_path_extension(path); if (ext && !strcasecmp(ext, ".snp")) { if (snap_fn) al_destroy_path(snap_fn); snap_fn = path; } - else if (ext && (!strcasecmp(ext, ".uef") || !strcasecmp(ext, ".csw"))) { - if (tape_fn) - al_destroy_path(tape_fn); - tape_fn = path; + else if ((ext != NULL) && ( ! strcasecmp(ext, ".uef") + || ! strcasecmp(ext, ".csw") + || ! strcasecmp(ext, ".tibet") /* TOHv3: +TIBET */ + || ! strcasecmp(ext, ".tibetz") ) ) { + if (tape_vars.load_filename) + al_destroy_path(tape_vars.load_filename); + tape_vars.load_filename = al_create_path(arg); } else { if (drives[0].discfn) @@ -383,11 +445,42 @@ case OPT_LOGFILE: log_file = arg; break; + /* begin TOHv4.2 merge ... */ + case OPT_EXPIRE: + e = toh_cli_handle_expire(arg); + break; + case OPT_TAPESAVE: + e = toh_cli_handle_tapesave(arg); + break; + case OPT_TAPETEST: + e = toh_cli_handle_tapetest(arg); + break; + case OPT_RS423FILE: + rs423_fn = arg; + break; case OPT_TAPE: - if (tape_fn) - al_destroy_path(tape_fn); - tape_fn = al_create_path(arg); + path = al_create_path(arg); + ext = al_get_path_extension(path); + if ((ext != NULL) && ( ! strcasecmp(ext, ".uef") + || ! strcasecmp(ext, ".csw") + || ! strcasecmp(ext, ".tibet") + || ! strcasecmp(ext, ".tibetz") ) ) { + if (tape_vars.load_filename) + al_destroy_path(tape_vars.load_filename); + tape_vars.load_filename = path; + } else { + fprintf(stderr, + "\"-tape\" file extension \"%s\" not recognised; tape ignored\n", + ext); + } + break; + /* end TOHv4.2 merge */ + /* + if (tape_vars.load_filename) + al_destroy_path(tape_vars.load_filename); + tape_vars.load_filename = path; break; + */ case OPT_EXEC: exec_fn = arg; break; @@ -406,14 +499,18 @@ case OPT_PRINT: print_filename = arg; print_filename_alloc = false; + break; } state = OPT_GROUND; + if (SHUTDOWN_OK != e) { break; } /* TOHv4.2 merge */ } if (state != OPT_GROUND) { fputs("b-em: missing argument\n", stderr); exit(1); } + if (SHUTDOWN_OK != e) { exit(e); } /* TOHv4.2 merge */ + al_init_native_dialog_addon(); al_set_new_window_title(VERSION_STR); al_init_primitives_addon(); @@ -422,13 +519,22 @@ exit(1); } key_init(); - config_load(cfg_fn); + /* TOHv4.2 merge: second argument forces certain config + * settings (tape related ones right now) not to be read. + * Specifically we don't want overclock getting switched + * on by default, nor do we want a previous UEF etc. file + * being loaded if none is provided on the command line; + * these will mess up the tests */ + config_load(cfg_fn, (tape_vars.testing_mode != 0) || (tape_vars.disable_debug_memview)); log_open(log_file); log_info("main: starting %s", VERSION_STR); main_load_speeds(); model_loadcfg(); + /* TOHv3, TOHv3.2, TOHv4.2 merge */ + acia_init(&sysacia); + ALLEGRO_DISPLAY *display = video_init(); mode7_makechars(); al_init_image_addon(); @@ -488,6 +594,7 @@ log_fatal("main: unable to create timer"); exit(1); } + al_register_event_source(queue, al_get_timer_event_source(timer)); al_init_user_event_source(&evsrc); al_register_event_source(queue, &evsrc); @@ -504,7 +611,10 @@ else disc_load(0, drives[0].discfn); disc_load(1, drives[1].discfn); - tape_load(tape_fn); + /* TOHv4.2 merge */ + if (tape_vars.load_filename != NULL) { + tape_load(tape_vars.load_filename); + } if (mmccard_fn) mmccard_load(mmccard_fn); if (defaultwriteprot) @@ -514,11 +624,124 @@ if (drives[1].discfn) gui_set_disc_wprot(1, drives[1].writeprot); main_setspeed(emuspeed); - debug_start(exec_fn, true); + debug_start(exec_fn, ! tape_vars.disable_debug_memview); // lovebug if (fullscreen) video_enterfullscreen(); // lovebug end + + /* TOHv4.1: mitigate Allegro crash problems under + * certain test environments (macOS) by offering a means + * of delaying startup */ + if (tape_vars.testing_mode & TAPE_TEST_SLOW_STARTUP) { +#ifdef WIN32 + Sleep(1000); +#else + sleep(1); +#endif + } + + /* TOHv3: if tape testing mode is enabled, do a + full catalogue of the tape on startup: */ + if (tape_vars.testing_mode & TAPE_TEST_CAT_ON_STARTUP) { + findfilenames_new(&tape_state, + 0, /* 0 = silent; no UI window: */ + ! tape_vars.disable_phantom_block_protection); + } + if (rs423_fn != NULL) { /* TOHv4: for -rs423file */ + sysacia_rec_start(rs423_fn); + } + +} + + +static int toh_cli_handle_expire (char *argv) { /* TOHv4.2 merge */ +#define OSP 15625 /* "otherstuff ticks" per second (see 6502.c) */ + tape_vars.testing_expire_ticks = OSP * ((int64_t)atoi(argv)); + if ( (tape_vars.testing_expire_ticks < (1 * ((int64_t)OSP))) + || (tape_vars.testing_expire_ticks > (2000 * ((int64_t)OSP)))) { + fprintf(stderr, + "\"-expire\" value was illegal: %s\n", + argv); + return SHUTDOWN_STARTUP_FAILURE; + } + return SHUTDOWN_OK; +} + +static int toh_cli_handle_tapetest (char *argv) { /* TOHv4.2 merge */ + tape_vars.testing_mode = 0x7f & atoi(argv); + tape_vars.disable_debug_memview = 1; /* TOHv4: prevent slew of race conditions on mem view window */ + /* TOHv4-rc4: now permit -tapetest 0 + * this suppresses mem view window but doesn't do anything else */ + if (TAPE_TEST_BAD_MASK & tape_vars.testing_mode) { + fprintf(stderr, + "\"-tapetest\" value was illegal: %u\n", + tape_vars.testing_mode); + return SHUTDOWN_STARTUP_FAILURE; + } + return SHUTDOWN_OK; +} + + +static int toh_cli_handle_tapesave (char *argv) { /* TOHv4.2 merge */ + + uint8_t fail; + size_t len; + uint8_t tapesave_filetype_bits, tapesave_do_compress; + + fail = 0; + tapesave_filetype_bits = 0; + tapesave_do_compress = 0; + + /* examine final N characters */ + + len = strlen(argv); + if (len < 5) { + fail = 1; + } else if (0 == strcasecmp(argv + (len - 4), ".uef")) { /* compress UEFs by default */ + tapesave_filetype_bits = TAPE_FILETYPE_BITS_UEF; + tapesave_do_compress = 1; + } else if (0 == strcasecmp(argv + (len - 4), ".csw")) { /* compress CSWs by default */ + tapesave_filetype_bits = TAPE_FILETYPE_BITS_CSW; + tapesave_do_compress = 1; + } else if (0 == strcasecmp(argv + (len - 4), ".wav")) { /* TOHv4.2: new WAV mode */ + tapesave_filetype_bits = TAPE_FILETYPE_BITS_WAV; + } + + if ( ( ! fail ) && (TAPE_FILETYPE_BITS_NONE == tapesave_filetype_bits ) ) { + if (len < 7) { + fail = 1; + } else if (0 == strcasecmp(argv + (len - 6), ".tibet")) { + tapesave_filetype_bits = TAPE_FILETYPE_BITS_TIBET; + } + } + + if ( ( ! fail ) && (TAPE_FILETYPE_BITS_NONE == tapesave_filetype_bits ) ) { + if (len < 8) { + fail = 1; + } else if (0 == strcasecmp(argv + (len - 7), ".unzuef")) { /* extension disables compression */ + tapesave_filetype_bits = TAPE_FILETYPE_BITS_UEF; + } else if (0 == strcasecmp(argv + (len - 7), ".unzcsw")) { /* extension disables compression */ + tapesave_filetype_bits = TAPE_FILETYPE_BITS_CSW; + } else if (0 == strcasecmp(argv + (len - 7), ".tibetz")) { + tapesave_filetype_bits = TAPE_FILETYPE_BITS_TIBET; + tapesave_do_compress = 1; + } + } + + if (fail) { + fprintf(stderr, "\"-tapesave\" filename was too short: %s\n", argv); + exit(1); + } else if (TAPE_FILETYPE_BITS_NONE == tapesave_filetype_bits) { + fprintf(stderr, "\"-tapesave\": unrecognised filetype, ignored: %s\n", argv); + } else { + tape_vars.quitsave_config.filename = argv; + tape_vars.quitsave_config.filetype_bits = tapesave_filetype_bits; + tape_vars.quitsave_config.do_compress = tapesave_do_compress; + } + + return SHUTDOWN_OK; + } void main_restart() @@ -640,7 +863,8 @@ ddnoise_headdown(); if (tapeledcount) { - if (--tapeledcount == 0 && !motor) { + /* TOHv3.3: state, not vars */ + if ( --tapeledcount == 0 && ! tape_state.ula_motor ) { log_debug("main: delayed cassette motor LED off"); led_update(LED_CASSETTE_MOTOR, 0, 0); } @@ -700,7 +924,12 @@ ALLEGRO_EVENT event; log_debug("main: about to start timer"); - al_start_timer(timer); + if (NULL == timer) { /* TOHv3.3: sanity check */ + log_warn("BUG: cannot start a NULL timer!\n"); + quitting = 1; + } else { + al_start_timer(timer); + } log_debug("main: entering main loop"); while (!quitting) { @@ -770,21 +999,20 @@ main_resume(); } } - log_debug("main: end loop"); -} - -static void tape_free(void) -{ - if (tape_fn) { - al_destroy_path(tape_fn); - tape_fn = NULL; + if (tape_vars.testing_mode & TAPE_TEST_CAT_ON_SHUTDOWN) { /* TOHv4 */ + findfilenames_new(&tape_state, + 0, /* 0 = silent; no UI window: */ + ! tape_vars.disable_phantom_block_protection); } + log_debug("main: end loop"); } void main_close() { gui_tapecat_close(); gui_keydefine_close(); + + sysacia_rec_stop(); /* TOHv4 */ debug_kill(); @@ -793,8 +1021,15 @@ midi_close(); mem_close(); - uef_close(); - csw_close(); + + /* TOHv3 */ + tape_save_on_shutdown(&(tape_state), + tape_vars.record_activated, + &(tape_state.filetype_bits), /* save using same type as we loaded (or any type if not loaded) */ + tape_vars.save_prefer_112, /* TOHv3.2 */ + tape_vars.wav_use_phase_shift, /* TOHv4.2 */ + &(tape_vars.quitsave_config)); + tape_state_finish(&tape_state, 0); /* 0 = don't alter menus */ tube_6502_close(); arm_close(); x86_close(); @@ -811,7 +1046,6 @@ music5000_close(); ddnoise_close(); tapenoise_close(); - tape_free(); al_destroy_timer(timer); al_destroy_event_queue(queue); led_close(); diff -Nu b-em-40246d4-vanilla/src/Makefile.am b-em-40246d4-TOHv4.2/src/Makefile.am --- b-em-40246d4-vanilla/src/Makefile.am 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/Makefile.am 2025-07-06 14:17:40.649656039 +0100 @@ -105,6 +105,7 @@ sysacia.c \ sysvia.c \ tape.c \ + tibet.c \ tapecat-allegro.c \ tapenoise.c \ pdp11/pdp11.c \ Common subdirectories: b-em-40246d4-vanilla/src/mc6809nc and b-em-40246d4-TOHv4.2/src/mc6809nc diff -Nu b-em-40246d4-vanilla/src/midi-linux.c b-em-40246d4-TOHv4.2/src/midi-linux.c --- b-em-40246d4-vanilla/src/midi-linux.c 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/midi-linux.c 2025-07-06 14:17:40.697657460 +0100 @@ -211,6 +211,8 @@ static void m2000_seq_init(midi_dev_t *midi, const char *pname) { int port; + midi->alsa_seq_port = -1; /* TOHv4 */ + if (midi->alsa_seq_enabled) { port = snd_seq_create_simple_port(midi_seq, pname, SND_SEQ_PORT_CAP_READ|SND_SEQ_PORT_CAP_SUBS_READ, SND_SEQ_PORT_TYPE_SOFTWARE); if (port < 0) @@ -256,6 +258,31 @@ } } + +#ifdef HAVE_ALSA_ASOUNDLIB_H +static void midi_alsa_seq_close(void) { + if (NULL == midi_seq) { return; } + if (midi_music2000_out1.alsa_seq_enabled) { + if (midi_music2000_out1.alsa_seq_port > -1) { + snd_seq_delete_simple_port(midi_seq, midi_music2000_out1.alsa_seq_port); + } + } + if (midi_music2000_out2.alsa_seq_enabled) { + if (midi_music2000_out2.alsa_seq_port > -1) { + snd_seq_delete_simple_port(midi_seq, midi_music2000_out2.alsa_seq_port); + } + } + if (midi_music2000_out3.alsa_seq_enabled) { + if (midi_music2000_out3.alsa_seq_port > -1) { + snd_seq_delete_simple_port(midi_seq, midi_music2000_out3.alsa_seq_port); + } + } + if (NULL != midi_seq) { snd_seq_close(midi_seq); } + midi_seq = NULL; +} +#endif + + static inline void alsa_seq_ctrl(snd_seq_event_t *ev, uint8_t *buffer) { ev->data.control.channel = buffer[0] & 0x0f; ev->data.control.param = buffer[1]; @@ -266,7 +293,7 @@ snd_seq_event_t ev; int res; - if (midi->alsa_seq_enabled && midi->alsa_seq_port) { + if (midi->alsa_seq_enabled && (midi->alsa_seq_port > -1)) { snd_seq_ev_clear(&ev); switch(buffer[0] >> 4) { @@ -498,6 +525,9 @@ void midi_close(void) { midi_jack_close(); +#ifdef HAVE_ALSA_ASOUNDLIB_H + midi_alsa_seq_close(); +#endif } void midi_load_config(void) { Common subdirectories: b-em-40246d4-vanilla/src/musahi and b-em-40246d4-TOHv4.2/src/musahi diff -Nu b-em-40246d4-vanilla/src/music2000.c b-em-40246d4-TOHv4.2/src/music2000.c --- b-em-40246d4-vanilla/src/music2000.c 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/music2000.c 2025-07-06 14:17:40.697657460 +0100 @@ -20,7 +20,7 @@ m2000_dev_t m2000_out2; m2000_dev_t m2000_out3; -static void acia_tx(ACIA *acia, uint8_t data) { +static int acia_tx(ACIA *acia, uint8_t data) { m2000_dev_t *m2000 = acia->udata; if (data & 0x80) { // status byte @@ -84,27 +84,31 @@ m2000->state = MS_ONE_OF_TWO; } } + return 0; /* TOHv3.3: ACIA callbacks return an error now */ } static ACIA music2000_acia1 = { - .tx_hook = acia_tx, - .udata = &m2000_out1, - .intnum = 0x100, - .name = "M2000:1" + .tx_hook = acia_tx, + .udata = &m2000_out1, + .intnum = 0x100, + .name = "M2000:1", + .tx_no_polling = 1 /* TOHv4: instantaneous writes */ }; static ACIA music2000_acia2 = { - .tx_hook = acia_tx, - .udata = &m2000_out2, - .intnum = 0x200, - .name = "M2000:2" + .tx_hook = acia_tx, + .udata = &m2000_out2, + .intnum = 0x200, + .name = "M2000:2", + .tx_no_polling = 1 /* TOHv4 */ }; static ACIA music2000_acia3 = { - .tx_hook = acia_tx, - .udata = &m2000_out3, - .intnum = 0x300, - .name = "M2000:3" + .tx_hook = acia_tx, + .udata = &m2000_out3, + .intnum = 0x300, + .name = "M2000:3", + .tx_no_polling = 1 /* TOHv4 */ }; uint8_t music2000_read(uint32_t addr) { @@ -134,12 +138,6 @@ } } -void music2000_poll(void) { - acia_poll(&music2000_acia1); - acia_poll(&music2000_acia2); - acia_poll(&music2000_acia3); -} - void music2000_init(midi_dev_t *out1, midi_dev_t *out2, midi_dev_t *out3) { m2000_out1.dev = out1; m2000_out2.dev = out2; Common subdirectories: b-em-40246d4-vanilla/src/NS32016 and b-em-40246d4-TOHv4.2/src/NS32016 Common subdirectories: b-em-40246d4-vanilla/src/pdp11 and b-em-40246d4-TOHv4.2/src/pdp11 Common subdirectories: b-em-40246d4-vanilla/src/resid-fp and b-em-40246d4-TOHv4.2/src/resid-fp diff -Nu b-em-40246d4-vanilla/src/savestate.c b-em-40246d4-TOHv4.2/src/savestate.c --- b-em-40246d4-vanilla/src/savestate.c 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/savestate.c 2025-07-06 14:17:40.697657460 +0100 @@ -75,7 +75,11 @@ unsigned char magic[8]; if (fread(magic, 8, 1, fp) == 1 && memcmp(magic, "BEMSNAP", 7) == 0) { int vers = magic[7]; +#ifdef BUILD_TAPE_INCOMPATIBLE_NEW_SAVE_STATES + if (vers >= '1' && vers <= '4') { /* TOHv4, v4-rc8 (bugfix) */ +#else if (vers >= '1' && vers <= '3') { +#endif char *name_copy = strdup(name); if (name_copy) { if (savestate_name) @@ -98,14 +102,24 @@ } } +#ifdef BUILD_TAPE_INCOMPATIBLE_NEW_SAVE_STATES +static void sysacia_savestate_TOHv4(FILE *f) { + acia_savestate_BEMSNAP4(&sysacia, f); +} +static void sysacia_loadstate_TOHv4(FILE *f) { + acia_loadstate_BEMSNAP4(&sysacia, f); +} +#else static void sysacia_savestate(FILE *f) { acia_savestate(&sysacia, f); } +#endif +/* this persists because we may need to load legacy BEMSNAP3 */ static void sysacia_loadstate(FILE *f) { + acia_init(&sysacia); acia_loadstate(&sysacia, f); } - static void save_tail(FILE *fp, int key, long start, long end, long size) { fseek(fp, start, SEEK_SET); @@ -193,7 +207,12 @@ void savestate_dosave(void) { FILE *fp = savestate_fp; + // fwrite("BEMSNAP3", 8,1, fp); +#ifdef BUILD_TAPE_INCOMPATIBLE_NEW_SAVE_STATES + fwrite("BEMSNAP4", 8,1, fp); +#else fwrite("BEMSNAP3", 8,1, fp); +#endif save_sect(fp, 'm', model_savestate); save_sect(fp, '6', m6502_savestate); save_zlib(fp, 'M', mem_savezlib); @@ -204,8 +223,13 @@ save_sect(fp, 'v', video_savestate); save_sect(fp, 's', sn_savestate); save_sect(fp, 'A', adc_savestate); - save_sect(fp, 'a', sysacia_savestate); - save_sect(fp, 'r', serial_savestate); +#ifdef BUILD_TAPE_INCOMPATIBLE_NEW_SAVE_STATES + save_sect(fp, '@', sysacia_savestate_TOHv4); /* BEMSNAP4 */ + save_sect(fp, '$', serial_savestate_BEMSNAP4); /* BEMSNAP4 */ +#else + save_sect(fp, 'a', sysacia_savestate); /* BEMSNAP3 */ + save_sect(fp, 'r', serial_savestate); /* BEMSNAP3 */ +#endif save_sect(fp, 'F', vdfs_savestate); save_sect(fp, '5', music5000_savestate); save_sect(fp, 'p', paula_savestate); @@ -324,11 +348,19 @@ case 'A': adc_loadstate(fp); break; - case 'a': + case 'a': /* BEMSNAP3 */ sysacia_loadstate(fp); break; +#ifdef BUILD_TAPE_INCOMPATIBLE_NEW_SAVE_STATES + case '@': /* BEMSNAP4 */ + sysacia_loadstate_TOHv4(fp); + break; + case '$': /* BEMSNAP4 */ + serial_loadstate_BEMSNAP4(fp); + break; +#endif case 'r': - serial_loadstate(fp); + serial_loadstate(fp); /* BEMSNAP3 */ break; case 'F': vdfs_loadstate(fp); @@ -390,6 +422,27 @@ } } +#ifdef BUILD_TAPE_INCOMPATIBLE_NEW_SAVE_STATES /* TOHv4 */ +static void load_state_four(FILE *fp) +{ + unsigned char hdr[3]; + + while (fread(hdr, sizeof hdr, 1, fp) == 1) { + int key = hdr[0]; + long size = hdr[1] | (hdr[2] << 8); + if (key & 0x80) { + if (fread(hdr, 2, 1, fp) != 1) { + log_error("savestate: unexpected EOF on file %s", savestate_name); + return; + } + size |= (hdr[0] << 16) | (hdr[1] << 24); + key &= 0x7f; + } + load_section(fp, key, size); + } +} +#endif + void savestate_doload(void) { FILE *fp = savestate_fp; @@ -403,6 +456,11 @@ case '3': load_state_three(fp); break; +#ifdef BUILD_TAPE_INCOMPATIBLE_NEW_SAVE_STATES /* TOHv4 */ + case '4': + load_state_four(fp); + break; +#endif } if (ferror(fp)) log_error("savestate: state not fully restored from V%c file '%s': %s", savestate_wantload, savestate_name, strerror(errno)); diff -Nu b-em-40246d4-vanilla/src/serial.c b-em-40246d4-TOHv4.2/src/serial.c --- b-em-40246d4-vanilla/src/serial.c 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/serial.c 2025-07-06 14:17:40.697657460 +0100 @@ -8,50 +8,234 @@ #include "sysacia.h" #include "tape.h" #include "tapenoise.h" +#include "acia.h" -int motor, acia_is_tape; - -static uint8_t serial_reg; -static uint8_t serial_transmit_rate, serial_recive_rate; +/* TOHv4 */ +const int16_t serial_divider_by_bits[8] = { 1, 16, 4, 128, 2, 64, 8, 256 }; void serial_reset() { - /*Dunno what happens to this on reset*/ - serial_reg = serial_transmit_rate = serial_recive_rate=0; - motor=0; - led_update(LED_CASSETTE_MOTOR, false, 0); + /*Dunno what happens to this on reset*/ + tape_state.ula_ctrl_reg =0; // serial_transmit_rate = serial_recive_rate=0; + + tape_stop_motor(&tape_state); /* TOHv3.2: move motor variable into tape.c */ + + tape_state.start_bit_wait_count_1200ths = 0; + tape_state.ula_dcd_tape = 0; /* TOHv3.3 */ + + serial_push_dcd_cts_lines_to_acia(&sysacia, &tape_state); /* TOHv3.3 (from beebjit) */ +// } -void serial_write(uint16_t addr, uint8_t val) + +void serial_write (uint16_t addr, uint8_t val) { - serial_reg = val; - serial_transmit_rate = val & 0x7; - serial_recive_rate = (val >> 3) & 0x7; + + tape_state.ula_ctrl_reg = val; + int new_motor = val & 0x80; - if (new_motor && !motor) { + if ( new_motor && ! tape_state.ula_motor ) { /* TOHv3.2 */ log_debug("serial: cassette motor on"); - tapenoise_motorchange(1); - led_update(LED_CASSETTE_MOTOR, 1, 0); - motor = tape_loaded; + tape_start_motor(&tape_state, tape_vars.strip_silence_and_leader); /* TOHv3.2 */ } - else if (!new_motor && motor) { + else if ( ( ! new_motor ) && tape_state.ula_motor ) { /* TOHv3.2 */ log_debug("serial: cassette motor off"); - tapenoise_motorchange(0); - motor = 0; - tapeledcount = 2; - } - if (val & 0x40) - { - /*RS423*/ - sysacia.status_reg &= ~12; /*Clear acia DCD and CTS*/ - acia_is_tape = 0; - } - else - { - /*Tape*/ - sysacia.status_reg &= ~8; /*Clear acia CTS*/ - acia_is_tape = 1; + tape_stop_motor(&tape_state); /* TOHv3.2 */ + } + + /* TOHv4: now precalculate dividers and thresholds on register writes, + to reduce workload in 6502.c */ + serial_recompute_dividers_and_thresholds (tape_vars.overclock, + tape_state.ula_ctrl_reg, + &(tape_state.ula_rx_thresh_ns), + &(tape_state.ula_tx_thresh_ns), + &(tape_state.ula_rx_divider), + &(tape_state.ula_tx_divider)); + + serial_push_dcd_cts_lines_to_acia(&sysacia, &tape_state); + +} + +/* TOHv4 */ +/* called by 6502.c on both edges of TXC to the ACIA */ +int serial_2txc_clock_for_tape (tape_state_t *ts, + tape_vars_t *tv, + ACIA *acia, + uint8_t emit_tapenoise) { + + int e; + uint8_t bitclk; + int32_t acia_tx_divider; + char bit_out; + int64_t ns_per_bit; + serial_framing_t f; + uint8_t silent; + + acia_tx_divider = 0; + bit_out = 0; + + /* FIXME? don't want to make an expensive cross-module call into acia.c? + We could make this more efficient by using three lookup arrays + for bits/parity/stops instead */ + acia_get_framing (acia->control_reg, &f); + + /* TDRE delay is handled in here */ + e = acia_poll_2txc (acia, &bit_out, &acia_tx_divider); + + if (0 == ts->ula_tx_divider) { return TAPE_E_OK; } + + bitclk = 1; + if (TAPE_E_ACIA_TX_BITCLK_NOT_READY == e) { /* no bit available yet */ + bitclk = 0; + e = TAPE_E_OK; /* not an error */ + } + + /* we will need to work out how many tones there are per bit */ + ns_per_bit = (13 * acia_tx_divider * ts->ula_tx_divider * 1000) / 16; + + if (bitclk) { + e = acia_run_tx_shift_register(acia, &f, &bit_out); + if (TAPE_E_OK != e) { return e; } + } + + if ( bitclk && ts->ula_motor ) { + + silent = ((acia->control_reg & ACIA_CTRLREG_MASK_TXCTRL) == ACIA_CTRLREG_TXCTRL_2); + + e = tape_write_bitclk (ts, + acia, + bit_out, + ns_per_bit, + silent, + emit_tapenoise && tv->record_activated, + tv->record_activated, + tv->save_always_117, + tv->save_prefer_112, + tv->save_do_not_generate_origin_on_append); + if (TAPE_E_OK != e) { return e; } + } + + return e; +} + +/* TOHv4 */ +/* called by 6502.c when RXC to the ACIA fires */ +int serial_rxc_clock_for_tape (tape_state_t *ts, + tape_vars_t *tv, + ACIA *acia, + uint8_t emit_tapenoise, + uint8_t *throw_eof_out) { + + int32_t acia_rx_divider; + uint8_t fire_acia_rxc; + int e; + + /* FIXME: the poll interface to the ACIA is stupid atm: */ + /* i) client calls acia_poll_rxc() at incident RXC frequency to see + if divided clock has fired; + ii) if so, then the ACIA needs to receive a bit, so go fetch a bit + from somewhere and call acia_receive_bit() with it + + a better approach might be to register a callback function, + e.g. acia_register_rxc_bit_source(myfunc), where + acia_poll_rxc() will maybe call "myfunc" to supply the next RX bit */ + + /* clock ACIA with divided clock from serial ULA; + is a bit needed from the tape? (also fetch divider) */ + acia_rx_divider = 0; /* compiler is stupid */ + e = acia_poll_rxc (acia, &fire_acia_rxc, &acia_rx_divider); + if (TAPE_E_OK != e) { return e; } + + if (fire_acia_rxc) { + + /* get bit from tape, place in ACIA; preserve it for DCD; handle tapenoise */ + e = tape_fire_acia_rxc (ts, tv, acia, acia_rx_divider, emit_tapenoise, throw_eof_out); + + } /* endif (ACIA RX clock fires) */ + + return e; + +} + + +/* TOHv4 */ +void serial_recompute_dividers_and_thresholds (uint8_t overclock_tape, /* OCing in ULA rather than ACIA */ + uint8_t ctrl_reg_value, + int32_t *rx_thresh_ns_out, + int32_t *tx_thresh_ns_out, + int32_t *rx_divider_out, + int32_t *tx_divider_out) { + + /* determine clock divider values for TXC and RXC */ + if ( ! ( ctrl_reg_value & 0x40) ) { + *rx_divider_out = overclock_tape ? 6 : 64; /* tape fixes divider at 64 */ + } else { + *rx_divider_out = serial_divider_by_bits[7 & (ctrl_reg_value>>3)]; + } + *tx_divider_out = serial_divider_by_bits[7 & ctrl_reg_value]; + + /* compute time thresholds for divided clocks */ + *rx_thresh_ns_out = (13 * *rx_divider_out * 1000) / 16; + /* this must be 2TXC because of the 0.5-bits of TDRE delay requirement */ + *tx_thresh_ns_out = (13 * (*tx_divider_out) * 1000) / 32; + +} + + +/* adapted from beebjit, (c) Chris Evans, under GPL: */ +void serial_push_dcd_cts_lines_to_acia (ACIA *acia, tape_state_t *ts) { + + int cts; + int dcd; + /*int chow; + static int cts_old=0; + struct mc6850_struct* p_serial = p_serial_ula->p_serial;*/ + + /* CTS. When tape is selected, CTS is always low (meaning active). For RS423, + * it is high (meaning inactive) unless we've connected a virtual device on + * the other end. + */ + /*chow=0;*/ + + dcd = 0; + cts = 1; + + /* if (ts->ula_rs423_mode) { */ + if (ts->ula_ctrl_reg & 0x40) { + if (ts->ula_have_serial_sink) { /*p_serial_ula->handle_output != -1) {*/ + cts = 0; + } else { + cts = 1; } + /* chow = 1;*/ + } else { + cts = 0; //0; + /* chow = 2;*/ + } + + /* DCD. In the tape case, it depends on a carrier tone on the tape. + * For the RS423 case, AUG clearly states: "It will always be low when the + * RS423 interface is selected". + */ + if (ts->ula_ctrl_reg & 0x40) { + dcd = 0; + } else { + dcd = ts->ula_dcd_tape; + } + + /* push DCD line value through to ACIA */ + if (dcd) { + acia_dcdhigh(acia); + } else { + acia_dcdlow(acia); + } + + if (cts) { + acia_ctson(acia); + } else { + acia_ctsoff(acia); + } + } @@ -62,12 +246,189 @@ return 0; } -void serial_savestate(FILE *f) +void serial_loadstate(FILE *f) { - putc(serial_reg, f); + serial_reset(); /* TOHv4 */ + serial_write(0, getc(f)); /* recomputes divider and threshold fields */ } -void serial_loadstate(FILE *f) +#define TAPE_BLIPTICKS_BEFORE_DCD_BLIP_FAST 217 +#define TAPE_BLIPTICKS_BEFORE_DCD_BLIP_SLOW 1083 + +#ifdef BUILD_TAPE_INCOMPATIBLE_NEW_SAVE_STATES +void serial_savestate_BEMSNAP4 (FILE *f) +{ + uint8_t bytes[24 + TAPE_TONES300_BUF_LEN]; /* i.e. [24 + 4] */ + uint8_t b4[4]; + uint8_t v; + tape_state_t *ts; + int e; + + v = TAPE_TONES300_BUF_LEN; /* 4 */ + ts = &(tape_state); + + bytes[0] = ts->ula_ctrl_reg; + bytes[1] = ts->ula_prevailing_rx_bit_value; + memcpy(2+bytes, ts->tones300, v); + bytes[2+v] = 0x7f & ts->tones300_fill; + tape_write_u32(b4, (uint32_t) ts->ula_rx_ns); + memcpy(3+v+bytes, b4, 4); + tape_write_u32(b4, (uint32_t) ts->ula_tx_ns); + memcpy(7+v+bytes, b4, 4); + bytes[11+v] = ts->ula_dcd_tape; + tape_write_u32(b4, (uint32_t) ts->ula_dcd_2mhz_counter); + memcpy(12+v+bytes, b4, 4); + tape_write_u32(b4, (uint32_t) ts->ula_dcd_blipticks); + memcpy(16+v+bytes, b4, 4); + tape_write_u32(b4, (uint32_t) ts->ula_rs423_taperoll_2mhz_counter); + memcpy(20+v+bytes, b4, 4); + + e = (1==fwrite(bytes, sizeof(bytes), 1, f)) ? TAPE_E_OK : TAPE_E_SAVESTATE; + + if (TAPE_E_OK != e) { + log_warn("serial: save state: fwrite failed"); + } + + /* return e; */ + +} +void serial_loadstate_BEMSNAP4(FILE *f) +{ + + uint8_t bytes[24 + TAPE_TONES300_BUF_LEN]; /* i.e. [24 + 4] */ + int32_t i; + uint8_t v; + tape_state_t *ts; + long q; + + v = TAPE_TONES300_BUF_LEN; /* 4 */ + ts = &(tape_state); + + q = ftell(f); + + if (1 != fread(bytes, sizeof(bytes), 1, f)) { + log_warn("serial: load state: read failed: %s", strerror(errno)); + return; /* TAPE_E_LOADSTATE; */ + } + + if ( ! TAPE_TONECODE_IS_LEGAL(bytes[1]) ) { + log_warn("serial: load state @ pos &%lx: error @ ula_prevailing_rx_bit_value", 1+q); + return; /* TAPE_E_LOADSTATE; */ + } + ts->ula_prevailing_rx_bit_value = bytes[1]; + + for (i=0; i < v; i++) { + if ( ('\0' != bytes[i+1]) && ! TAPE_TONECODE_IS_LEGAL(bytes[i+1]) ) { /* TOHv4-rc6, -rc7: fixes */ + log_warn("serial: load state @ pos &%lx: error @ tones300 (tonecode &%x)", 1+q+i, bytes[i+1]); + return; /* TAPE_E_LOADSTATE; */ + } + ts->tones300[i] = bytes[i+1]; + } + + if (bytes[2+v] >= v) { + log_warn("serial: load state: error @ tones300_fill"); + return; /* TAPE_E_LOADSTATE; */ + } + ts->tones300_fill = bytes[2+v]; + + /* (this will be validated in a moment) */ + ts->ula_rx_ns = 0x7fffffff & tape_read_u32(3+v+bytes); + ts->ula_tx_ns = 0x7fffffff & tape_read_u32(7+v+bytes); + + ts->ula_dcd_tape = (bytes[11+v] ? 1 : 0); + + ts->ula_dcd_2mhz_counter = 0x7fffffff & tape_read_u32(12+v+bytes); + if (ts->ula_dcd_2mhz_counter > TAPE_DCD_BLIP_TICKS_2MHZ) { + log_warn("serial: load state: error @ ula_dcd_2mhz_counter"); + return; /* TAPE_E_LOADSTATE; */ + } + + ts->ula_dcd_blipticks = 0x7fffffff & tape_read_u32(16+v+bytes); + if (ts->ula_dcd_blipticks > (1 + TAPE_BLIPTICKS_BEFORE_DCD_BLIP_SLOW)) { + log_warn("serial: load state: error @ ula_dcd_blipticks"); + return; /* TAPE_E_LOADSTATE; */ + } + + ts->ula_rs423_taperoll_2mhz_counter = 0x7fffffff & tape_read_u32(20+v+bytes); + if (ts->ula_rs423_taperoll_2mhz_counter > TAPE_1200TH_IN_2MHZ_INT) { + log_warn("serial: load state: error @ ula_rs423_taperoll_2mhz_counter"); + return; /* TAPE_E_LOADSTATE; */ + } + + /* this will recompute divider and threshold fields */ + serial_write(0, bytes[0]); + + /* NOW we can sanity-check the ula_Xx_ns fields against + their thresholds; we couldn't do this before we had + calculated thresholds. */ + if (ts->ula_rx_ns > ts->ula_rx_thresh_ns) { + log_warn("serial: load state: error ula_rx_ns(%d) > ula_rx_thresh_ns(%d)", + ts->ula_rx_ns, ts->ula_rx_thresh_ns); + return; /* TAPE_E_LOADSTATE; */ + } + if (ts->ula_tx_ns > ts->ula_tx_thresh_ns) { + log_warn("serial: load state: error @ ula_tx_ns"); + return; /* TAPE_E_LOADSTATE; */ + } + +} +#else +void serial_savestate(FILE *f) { - serial_write(0, getc(f)); + putc(tape_state.ula_ctrl_reg, f); +} +#endif + +void serial_handle_dcd_tick (tape_state_t *ts, + tape_vars_t *tv, + ACIA *acia) { + if (ts->ula_motor) { + serial_poll_dcd_blipticks (ts->ula_prevailing_rx_bit_value, + tv->overclock || tv->strip_silence_and_leader, /* fast DCD? */ + &(ts->ula_dcd_blipticks), /* DCD's blips counter UPDATED */ + &(ts->ula_dcd_tape)); /* incident line updated */ + } else { + /* HACK: this probably should be updated at 2 MHz + resolution, rather than at fire_dcd resolution, + but it is less CPU intensive this way. Essentially + means that when the motor is turned off, it will + take up to ~211uS for DCD to change. */ + ts->ula_dcd_tape = 0; /* incident line updated */ + } + serial_push_dcd_cts_lines_to_acia (acia, ts); +} + + + + +void serial_poll_dcd_blipticks (char bit_value_or_null, /* nulls are just ignored */ + uint8_t fast_dcd, + int32_t *dcd_bliptick_count_inout, /* blip-periods counter */ + uint8_t *dcd_line_inout) { /* DCD line to be manipulated */ + + int32_t ticks_until_blip; + int32_t fast, slow; + + fast = TAPE_BLIPTICKS_BEFORE_DCD_BLIP_FAST; + slow = TAPE_BLIPTICKS_BEFORE_DCD_BLIP_SLOW; + + ticks_until_blip = fast_dcd ? fast : slow; /* ? 217 : 1083; */ +/* ticks_until_blip = fast_dcd ? 92000 : 458000;*/ /* old 2 MHz numbers */ + + *dcd_line_inout = 0; /* may be changed later */ + + if (('0' == bit_value_or_null) || ('S' == bit_value_or_null)) { + *dcd_bliptick_count_inout = 0; + } + + if (*dcd_bliptick_count_inout < ticks_until_blip) { + (*dcd_bliptick_count_inout)++; + } else if (*dcd_bliptick_count_inout == ticks_until_blip) { + *dcd_line_inout = 1; + (*dcd_bliptick_count_inout)++; +/*printf("DCD blip\n");*/ + } + /* dcd_bliptick_count_inout final value is (ticks_until_blip+1), + where it will remain, until a '0' or 'S' resets it. */ + } diff -Nu b-em-40246d4-vanilla/src/serial.h b-em-40246d4-TOHv4.2/src/serial.h --- b-em-40246d4-vanilla/src/serial.h 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/serial.h 2025-07-06 14:17:40.697657460 +0100 @@ -1,13 +1,59 @@ #ifndef __INC_SERIAL_H #define __INC_SERIAL_H +/* TOHv4 */ +extern const int16_t serial_divider_by_bits[8]; + void serial_write(uint16_t addr, uint8_t val); uint8_t serial_read(uint16_t addr); void serial_reset(void); -void serial_savestate(FILE *f); void serial_loadstate(FILE *f); -extern int motor; -extern int acia_is_tape; +#include "tape.h" + +#ifdef BUILD_TAPE_INCOMPATIBLE_NEW_SAVE_STATES +/* TOHv4, for BEMSNAP4 */ +/* would like these to return error code, but + the save_sect() pointer-to-function isn't + currently compatible with that :( */ +void serial_savestate_BEMSNAP4(FILE *f); +void serial_loadstate_BEMSNAP4(FILE *f); +#else +void serial_savestate(FILE *f); +#endif + +#define TAPE_DCD_BLIP_TICKS_2MHZ 423 + +/* from beebjit: */ +void serial_push_dcd_cts_lines_to_acia (ACIA *acia, tape_state_t *ts); + +void serial_poll_dcd_blipticks (char bit_value_or_null, /* nulls are just ignored */ + uint8_t fast_dcd, + int32_t *dcd_bliptick_count_inout, /* blip periods counter */ + uint8_t *dcd_line_inout) ; + +/* TOHv4 */ +void serial_recompute_dividers_and_thresholds (uint8_t overclock_tape, /* OCing now in ULA not ACIA */ + uint8_t ctrl_reg_value, + int32_t *rx_thresh_ns_out, + int32_t *tx_thresh_ns_out, + int32_t *rx_divider_out, + int32_t *tx_divider_out); + +int serial_rxc_clock_for_tape (tape_state_t *ts, + tape_vars_t *tv, + ACIA *acia, + uint8_t emit_tapenoise, + uint8_t *throw_eof_out); + +int serial_2txc_clock_for_tape (tape_state_t *ts, + tape_vars_t *tv, + ACIA *acia, + uint8_t emit_tapenoise); + +void serial_handle_dcd_tick (tape_state_t *ts, + tape_vars_t *tv, + ACIA *acia); + #endif diff -Nu b-em-40246d4-vanilla/src/sound.c b-em-40246d4-TOHv4.2/src/sound.c --- b-em-40246d4-vanilla/src/sound.c 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/sound.c 2025-07-06 14:17:40.697657460 +0100 @@ -15,6 +15,7 @@ bool sound_ddnoise = false, sound_tape = false; bool sound_music5000 = false, sound_filter = false; bool sound_paula = false; +bool sound_tape_relay = false; /* TOHv2 */ static ALLEGRO_VOICE *voice; static ALLEGRO_MIXER *mixer; diff -Nu b-em-40246d4-vanilla/src/sound.h b-em-40246d4-TOHv4.2/src/sound.h --- b-em-40246d4-vanilla/src/sound.h 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/sound.h 2025-07-06 14:17:40.697657460 +0100 @@ -19,6 +19,7 @@ extern bool sound_internal, sound_beebsid, sound_dac; extern bool sound_ddnoise, sound_tape; extern bool sound_music5000, sound_filter, sound_paula; +extern bool sound_tape_relay; /* TOHv2 */ void sound_init(void); void sound_poll(int cycles); diff -Nu b-em-40246d4-vanilla/src/sysacia.c b-em-40246d4-TOHv4.2/src/sysacia.c --- b-em-40246d4-vanilla/src/sysacia.c 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/sysacia.c 2025-07-06 14:17:40.697657460 +0100 @@ -8,54 +8,98 @@ #include "serial.h" #include "tape.h" -int sysacia_tapespeed=0; FILE *sysacia_fp = NULL; -static void sysvia_set_params(ACIA *acia, uint8_t val) { - switch (val & 3) { - case 1: sysacia_tapespeed=0; break; - case 2: sysacia_tapespeed=1; break; - } +static int sysacia_reset(ACIA *acia); + +/* TOHv3.3 */ +static int sysacia_reset(ACIA *acia) { + tape_state_t *ts; + int e; + ts = (tape_state_t *) acia->udata; + e = tape_handle_acia_master_reset(ts); + /* + if (sysacia_fp != NULL) {*/ /* serial-to-file is active? */ + /*acia_ctsoff(acia); + } + */ + return e; } -static void sysacia_tx_hook(ACIA *acia, uint8_t data) +static int sysacia_tx_hook(ACIA *acia, uint8_t data) { - if (sysacia_fp) - putc(data, sysacia_fp); + + int e; + tape_state_t *ts; + + ts = (tape_state_t *) acia->udata; + + /* This callback is only used for RS423 with a TX sink. + Tape doesn't use it; it uses a full-fat shift-register + ACIA implementation instead, so return now if no sink or + if tape mode. */ + if ( ( ! ( ts->ula_ctrl_reg & 0x40 ) ) || ! ts->ula_have_serial_sink ) { + return 0; + } + + e = 0; + + if (NULL != sysacia_fp) { + e = fputc(data, sysacia_fp); + } + + /* TOHv4: hack: consume byte immediately; avoid polling for this + serial-to-file RS423 case. */ + acia_hack_consume_tx_byte_immediately(acia); + + return (e == EOF) ? -1 : 0; + } -static void sysacia_tx_end(ACIA *acia) +static int sysacia_tx_end(ACIA *acia) { - if (sysacia_fp) - fflush(sysacia_fp); + if (NULL != sysacia_fp) { + fflush(sysacia_fp); + } + return TAPE_E_OK; /* TOHv3.3: callbacks return error code */ } -ACIA sysacia = { - .set_params = sysvia_set_params, - .rx_hook = tape_receive, - .tx_hook = sysacia_tx_hook, - .tx_end = sysacia_tx_end, - .intnum = 0x04, - .name = "sysacia" -}; + ACIA sysacia = { + .set_params = NULL, //sysvia_set_params, +/* .rx_hook = tape_receive,*/ + .tx_hook = sysacia_tx_hook, + .tx_end = sysacia_tx_end, + .reset_hook = sysacia_reset, /* TOHv3.3 */ + .udata = &tape_state, + .name = "sysacia", /* TOHv4 */ + .tx_no_polling = 0 /* TOHv4 */ + }; void sysacia_rec_stop(void) { if (sysacia_fp) { fclose(sysacia_fp); sysacia_fp = NULL; - acia_ctsoff(&sysacia); +/* acia_ctson(&sysacia);*/ + tape_state.ula_have_serial_sink = 0; /* TOHv4: hack */ + serial_push_dcd_cts_lines_to_acia(&sysacia, &tape_state); } } -FILE *sysacia_rec_start(const char *filename) + + +FILE *sysacia_rec_start(/*ACIA *acia, tape_state_t *ts,*/ const char *filename) { +/*printf("sysacia_rec_start: %s\n", filename);*/ FILE *fp = fopen(filename, "wb"); if (fp) { sysacia_fp = fp; - acia_ctson(&sysacia); +/* acia_ctsoff(&sysacia);*/ + tape_state.ula_have_serial_sink = 1; /* TOHv4: hack */ + serial_push_dcd_cts_lines_to_acia(&sysacia, &tape_state); } else log_error("unable to open %s for writing: %s", filename, strerror(errno)); return fp; } + diff -Nu b-em-40246d4-vanilla/src/sysacia.h b-em-40246d4-TOHv4.2/src/sysacia.h --- b-em-40246d4-vanilla/src/sysacia.h 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/sysacia.h 2025-07-06 14:17:40.697657460 +0100 @@ -4,11 +4,11 @@ #include "acia.h" extern ACIA sysacia; -extern int sysacia_tapespeed; +/*extern int sysacia_tapespeed;*/ extern FILE *sysacia_fp; void sysacia_poll(void); void sysacia_rec_stop(void); -FILE *sysacia_rec_start(const char *filename); - +FILE *sysacia_rec_start(const char *filename); + #endif diff -Nu b-em-40246d4-vanilla/src/tape2.h b-em-40246d4-TOHv4.2/src/tape2.h --- b-em-40246d4-vanilla/src/tape2.h 1970-01-01 01:00:00.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/tape2.h 2025-07-06 14:17:40.697657460 +0100 @@ -0,0 +1,176 @@ +#ifndef __INC_TAPE2_H +#define __INC_TAPE2_H + + +/* BUILD OPTIONS */ +#define BUILD_TAPE_SANITY +#define BUILD_TAPE_DEV_MENU +#define BUILD_TAPE_MENU_GREYOUT_CAT +#define BUILD_TAPE_READ_POS_TO_EOF_ON_WRITE +#define BUILD_TAPE_INCOMPATIBLE_NEW_SAVE_STATES +/*#define BUILD_TAPE_CHECK_POLL_TIMING*/ +/*#define BUILD_TAPE_LOOP */ +#define BUILD_TAPE_NO_FASTTAPE_VIDEO_HACKS /* removes strange fasttape frameskip thing in video code */ + +/* include this line to introduce a bug which produces + a non-uniform TDRE delay distribution (rather than + the broadly flat one which actually occurs on h/w) + -- this is intended purely for testing the tests */ +/*#define BUILD_TAPE_BUGGY_LUMPY_TDRE_DISTRIBUTION*/ + +#define TAPE_E_OK 0 +#define TAPE_E_MALLOC 1 +#define TAPE_E_EOF 2 +#define TAPE_E_BUG 3 +#define TAPE_E_FOPEN 4 +#define TAPE_E_FTELL 5 +#define TAPE_E_FREAD 6 +#define TAPE_E_FILE_TOO_LARGE 7 +#define TAPE_E_ZLIB_INIT 8 +#define TAPE_E_ZLIB_DECOMPRESS 9 +#define TAPE_E_DECOMPRESSED_TOO_LARGE 10 +#define TAPE_E_SAVESTATE 11 +#define TAPE_E_LOADSTATE 12 + +#define TAPE_E_CSW_BAD_MAGIC 101 +#define TAPE_E_CSW_BAD_VERSION 102 +#define TAPE_E_CSW_BAD_RATE 103 +#define TAPE_E_CSW_HEADER_NUM_PULSES 104 +#define TAPE_E_CSW_BODY_LARGE 106 +#define TAPE_E_CSW_COMP_VALUE 107 +#define TAPE_E_CSW_BAD_FLAGS 108 +#define TAPE_E_CSW_PULSES_MISMATCH 109 +#define TAPE_E_CSW_HEADER_TRUNCATED 110 +#define TAPE_E_CSW_WRITE_NULL_PULSE 111 /* TOHv3 */ +#define TAPE_E_CSW_LONGPULSE_UNDER_256 112 /* TOHv3.2 */ + +#define TAPE_E_TIBET_BADCHAR 200 +#define TAPE_E_TIBET_VERSION 201 +#define TAPE_E_TIBET_VERSION_LINE 202 +#define TAPE_E_TIBET_VERSION_LINE_NOSPC 203 +#define TAPE_E_TIBET_UNK_WORD 204 +#define TAPE_E_TIBET_VERSION_MAJOR 205 +#define TAPE_E_TIBET_TOO_MANY_SPANS 206 /* not probed by tests */ +#define TAPE_E_TIBET_FIELD_INCOMPAT 207 +#define TAPE_E_TIBET_MULTI_DECIMAL_POINT 208 +#define TAPE_E_TIBET_POINT_ENDS_DECIMAL 209 +#define TAPE_E_TIBET_DECIMAL_BAD_CHAR 210 +#define TAPE_E_TIBET_DECIMAL_TOO_LONG 211 +#define TAPE_E_TIBET_DECIMAL_PARSE 212 /* not probed by tests */ +/*#define TAPE_E_TIBET_SHORT_SILENCE 213 */ /* (relaxed: no lower bound on silence duration) */ +#define TAPE_E_TIBET_LONG_SILENCE 214 +#define TAPE_E_TIBET_INT_TOO_LONG 215 +#define TAPE_E_TIBET_INT_PARSE 217 +#define TAPE_E_TIBET_INT_BAD_CHAR 218 +#define TAPE_E_TIBET_LONG_LEADER 219 +#define TAPE_E_TIBET_DUP_BAUD 220 +#define TAPE_E_TIBET_BAD_FRAMING 221 +#define TAPE_E_TIBET_DUP_FRAMING 222 +#define TAPE_E_TIBET_DUP_TIME 223 +#define TAPE_E_TIBET_TIME_HINT_TOOLARGE 224 +#define TAPE_E_TIBET_BAD_BAUD 225 +#define TAPE_E_TIBET_DUP_PHASE 226 +#define TAPE_E_TIBET_BAD_PHASE 227 +#define TAPE_E_TIBET_DUP_SPEED 228 +#define TAPE_E_TIBET_SPEED_HINT_HIGH 229 +#define TAPE_E_TIBET_SPEED_HINT_LOW 230 +#define TAPE_E_TIBET_DATA_JUNK_FOLLOWS_START 231 +#define TAPE_E_TIBET_DATA_JUNK_FOLLOWS_LINE 232 +#define TAPE_E_TIBET_DATA_ILLEGAL_CHAR 233 +#define TAPE_E_TIBET_DATA_DOUBLE_PULSE 234 +#define TAPE_E_TIBET_DATA_EXCESSIVE_TONES 235 /* not probed by tests */ +#define TAPE_E_TIBET_DANGLING_TIME 236 +#define TAPE_E_TIBET_DANGLING_PHASE 237 +#define TAPE_E_TIBET_DANGLING_SPEED 238 +#define TAPE_E_TIBET_DANGLING_BAUD 239 +#define TAPE_E_TIBET_DANGLING_FRAMING 240 +#define TAPE_E_TIBET_NO_DECODE 241 +#define TAPE_E_TIBET_EMPTY_LEADER 242 /* TOHv3 */ +#define TAPE_E_TIBET_OP_LEN 243 /* TOHv3: writing */ +#define TAPE_E_TIBET_ABSENT_VERSION 244 /* TOHv3: caught by unit test! */ +#define TAPE_E_TIBET_VERSION_NO_DP 245 /* TOHv3.2: version parsing */ +#define TAPE_E_TIBET_VERSION_NON_NUMERIC 246 /* TOHv3.2: version parsing */ +#define TAPE_E_TIBET_VERSION_BAD_LEN 247 /* TOHv3.2: version parsing */ +#define TAPE_E_TIBET_VERSION_MINOR 248 /* TOHv3.2: compatibility */ +#define TAPE_E_TIBET_CONCAT_VERSION_MISMATCH 249 /* TOHv3.2: force TIBET concat exact version match */ + +#define TAPE_E_UEF_BAD_MAGIC 301 +#define TAPE_E_UEF_BAD_HEADER 302 +#define TAPE_E_UEF_TRUNCATED 303 +#define TAPE_E_UEF_UNKNOWN_CHUNK 304 +#define TAPE_E_UEF_OVERSIZED_CHUNK 305 +#define TAPE_E_UEF_TOO_MANY_METADATA_CHUNKS 306 +#define TAPE_E_UEF_0104_NUM_BITS 307 +#define TAPE_E_UEF_0104_NUM_STOPS 308 +#define TAPE_E_UEF_CHUNKLEN_0000 309 +#define TAPE_E_UEF_CHUNKLEN_0001 310 +#define TAPE_E_UEF_CHUNKLEN_0003 311 +#define TAPE_E_UEF_CHUNKLEN_0005 312 +#define TAPE_E_UEF_CHUNKLEN_0006 313 +#define TAPE_E_UEF_CHUNKLEN_0007 314 +#define TAPE_E_UEF_CHUNKLEN_0008 315 +#define TAPE_E_UEF_CHUNKLEN_0009 316 +#define TAPE_E_UEF_CHUNKLEN_000A 317 +#define TAPE_E_UEF_CHUNKLEN_0100 318 +#define TAPE_E_UEF_CHUNKLEN_0102 319 +#define TAPE_E_UEF_CHUNKLEN_0104 320 +#define TAPE_E_UEF_CHUNKLEN_0110 321 +#define TAPE_E_UEF_CHUNKLEN_0111 322 +#define TAPE_E_UEF_CHUNKLEN_0112 323 +#define TAPE_E_UEF_CHUNKLEN_0115 324 +#define TAPE_E_UEF_CHUNKLEN_0116 325 +#define TAPE_E_UEF_CHUNKLEN_0113 326 +#define TAPE_E_UEF_CHUNKLEN_0114 327 +#define TAPE_E_UEF_CHUNKLEN_0117 328 +#define TAPE_E_UEF_CHUNKLEN_0120 329 +#define TAPE_E_UEF_CHUNKLEN_0130 330 +#define TAPE_E_UEF_CHUNKLEN_0131 331 +#define TAPE_E_UEF_CHUNKDAT_0005 332 +#define TAPE_E_UEF_CHUNKDAT_0006 333 +#define TAPE_E_UEF_0114_BAD_PULSEWAVE_1 334 +#define TAPE_E_UEF_0114_BAD_PULSEWAVE_2 335 +#define TAPE_E_UEF_0114_BAD_PULSEWAVE_COMBO 336 +#define TAPE_E_UEF_CHUNK_SPENT 337 +#define TAPE_E_UEF_0114_BAD_NUM_CYCS 338 +#define TAPE_E_UEF_0116_NEGATIVE_GAP 339 +#define TAPE_E_UEF_0116_HUGE_GAP 340 +#define TAPE_E_UEF_0102_WEIRD_DATA_0 341 +#define TAPE_E_UEF_EXCESS_0000 342 +#define TAPE_E_UEF_EXCESS_0001 343 +#define TAPE_E_UEF_EXCESS_0003 344 +#define TAPE_E_UEF_EXCESS_0005 345 +#define TAPE_E_UEF_EXCESS_0008 346 +#define TAPE_E_UEF_0130_VOCAB 347 +#define TAPE_E_UEF_0130_NUM_TAPES 348 +#define TAPE_E_UEF_0130_NUM_CHANNELS 349 +#define TAPE_E_UEF_0131_TAPE_ID 350 +#define TAPE_E_UEF_0131_CHANNEL_ID 351 +#define TAPE_E_UEF_0131_TAPE_ID_130_LIMIT 352 +#define TAPE_E_UEF_0131_CHANNEL_ID_130_LIMIT 353 +#define TAPE_E_UEF_0131_DESCRIPTION_LONG 354 +#define TAPE_E_UEF_LONG_CHUNK 355 /* exceeds generic length limit */ +#define TAPE_E_UEF_INLAY_SCAN_BPP 356 +#define TAPE_E_UEF_INLAY_SCAN_ZERO 357 +#define TAPE_E_UEF_0115_ILLEGAL 358 +#define TAPE_E_UEF_0117_BAD_RATE 359 +#define TAPE_E_UEF_UTF8_DEC_1 360 /* UTF-8 decoding errors */ +#define TAPE_E_UEF_UTF8_DEC_2 361 +#define TAPE_E_UEF_GLOBAL_CHUNK_SPAM 362 /* excessive number of "global" chunks (instructions, origin etc.) */ +#define TAPE_E_UEF_SAVE_NONSTD_BAUD 363 /* UEF 0.10 disallows non-300, non-1200 baud rates in chunk 117 */ + +#define TAPE_E_UNICODE_UNEXPECTED_NULL 370 +#define TAPE_E_UNICODE_MAX_LEN 371 +#define TAPE_E_ENC_UTF8 372 + +#define TAPE_E_SAVE_FOPEN 400 +#define TAPE_E_SAVE_FWRITE 401 +#define TAPE_E_SAVE_ZLIB_COMPRESS 402 +#define TAPE_E_SAVE_ZLIB_INIT 403 +#define TAPE_E_SAVE_WAV_BODY_TOO_LARGE 404 +#define TAPE_E_SAVE_FSEEK 405 + +#define TAPE_E_EXPIRY 500 /* TOHv4 */ + +#define TAPE_E_ACIA_TX_BITCLK_NOT_READY 600 /* not an error */ + +#endif /* __INC_TAPE2_H */ diff -Nu b-em-40246d4-vanilla/src/tape.c b-em-40246d4-TOHv4.2/src/tape.c --- b-em-40246d4-vanilla/src/tape.c 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/tape.c 2025-07-06 14:17:40.697657460 +0100 @@ -1,16 +1,84 @@ +/* - overhaul by Diminished */ + #include "b-em.h" +#include "acia.h" #include "led.h" #include "tape.h" #include "serial.h" -#include "tapenoise.h" +#include "tapenoise.h" /* for sound_tape flag */ #include "uef.h" #include "csw.h" +#include "sysacia.h" +#include "tibet.h" +#include "gui-allegro.h" /* TOHv3: for greying-out menu items */ +#include "main.h" /* for shutdown codes */ + +#include +#include + +/* TOHv3.2: moved globals onto this struct: */ +tape_vars_t tape_vars; +int tapelcount,tapeledcount; +char tape_dummy_for_unix_scprintf; + +static int check_pending_tibet_span_type (tape_state_t *t, uint8_t type); +static void tibet_load (const char *fn); +static void tibetz_load (const char *fn); +static int tape_write_end_data (tape_state_t *t); +static int tape_write_start_data (tape_state_t *t, uint8_t always_117, serial_framing_t *f); +static int tape_write_silence_2 (tape_state_t *t, double len_s, uint8_t silence112); +static int tape_write_1200th (tape_state_t *t, + serial_framing_t *f, + uint8_t tapenoise_active, + char value); +static int tape_write_leader (tape_state_t *t, uint32_t num_1200ths); +static void framing_to_string (char fs[4], serial_framing_t *f); +static int +wp_bitclk_start_data_if_needed (tape_state_t *ts, + /* need to provide baud rate + framing, + for TIBET hints, UEF &114 header, etc: */ + serial_framing_t *f, + uint8_t always_117, + uint8_t record_is_pressed); +static int wp_bitclk_accumulate_leader (tape_state_t *t, + uint8_t tapenoise_active, + int64_t ns_per_bit, + uint8_t record_is_pressed, + uint8_t *reset_shift_reg_out); +static int wp_end_data_section_if_ongoing (tape_state_t *t, + uint8_t record_is_pressed, + uint8_t *reset_shift_register_out); +static int +wp1_4k8_init_blank_tape_if_none_loaded (uint8_t *filetype_bits_inout); + /*uint8_t record_activated);*/ /* TOHv4.1: removed */ +static int wp_flush_accumulated_leader_maybe (tape_state_t *t, uint8_t record_is_pressed); +static int wp_bitclk_output_data (tape_state_t *ts, + ACIA *acia, + serial_framing_t *f, + uint8_t tapenoise_active, + int64_t ns_per_bit, + uint8_t record_is_pressed, + uint8_t always_117); -int tapelcount,tapellatch,tapeledcount; +#define SERIAL_STATE_AWAITING_START 0 +#define SERIAL_STATE_BITS 1 +#define SERIAL_STATE_PARITY 2 +#define SERIAL_STATE_AWAITING_STOP 3 -bool tape_loaded = false; -bool fasttape = false; -ALLEGRO_PATH *tape_fn = NULL; +#define TAPE_FILE_MAXLEN (16 * 1024 * 1024) +#define TAPE_MAX_DECOMPRESSED_LEN (32 * 1024 * 1024) + +static int tape_read_1200th (tape_state_t *ser, char *bit_out); +/*static uint8_t tape_peek_eof (tape_state_t *ser);*/ /* not used in TOHv4 */ +static int tibet_load_file (const char *fn, uint8_t decompress, tibet_t *t); +static void load_successful (char *filename); +static void csw_load (const char *fn); +static void tibet_load_2 (const char *fn, uint8_t decompress); +static void uef_load (const char *fn); +static int tape_uef_flush_incomplete_frame (uef_state_t *uef_inout, + uint8_t *serial_phase_inout, + uint8_t *serial_frame_inout, + uef_chunk_t *chunk_inout); /* TOHv3.2 */ static struct { @@ -20,18 +88,60 @@ } loaders[]= { - {"UEF", uef_load, uef_close}, - {"CSW", csw_load, csw_close}, - {0,0,0} +/* TOHv2: strange mixed-metaphor here + (terminated array & numeric limit) */ +/* TOHv3: individual close-functions are gone; + rely on universal tape_state_finish() now */ +#define TAPE_NUM_LOADERS 4 /* TOHv2 */ + {"UEF", uef_load, NULL}, + {"CSW", csw_load, NULL}, + {"TIBET", tibet_load, NULL}, /* TOH */ + {"TIBETZ", tibetz_load, NULL}, /* TOH */ + {NULL, NULL, NULL} /* TOHv2 */ }; -static int tape_loader; +/* all of our state lives here: */ +tape_state_t tape_state; /* exported */ + +/* used to determine whether "catalogue tape" should be greyed out */ +#ifdef BUILD_TAPE_MENU_GREYOUT_CAT +uint8_t tape_peek_for_data (tape_state_t *ts) { + + uint8_t r; + uint8_t have_tibet_data; + int e; + + r = 0; + have_tibet_data = 0; + + if (ts->disabled_due_to_error) { + return 0; + } + + if ( (ts->filetype_bits & TAPE_FILETYPE_BITS_CSW) + && (ts->csw.pulses_fill > 0)) { + r |= TAPE_FILETYPE_BITS_CSW; /* = 4 */ + } + if ( (ts->filetype_bits & TAPE_FILETYPE_BITS_UEF) + && (ts->uef.num_chunks > 0)) { + r |= TAPE_FILETYPE_BITS_UEF; /* = 1 */ + } + if (ts->filetype_bits & TAPE_FILETYPE_BITS_TIBET) { + e = tibet_have_any_data (&(ts->tibet), &have_tibet_data); + if ((TAPE_E_OK == e) && have_tibet_data) { + r |= TAPE_FILETYPE_BITS_TIBET; /* = 2 */ + } + } + + return r; + +} +#endif /* BUILD_TAPE_MENU_GREYOUT_CAT */ void tape_load(ALLEGRO_PATH *fn) { int c = 0; const char *p, *cpath; - if (!fn) return; p = al_get_path_extension(fn); if (!p || !*p) return; @@ -43,41 +153,2654 @@ { if (!strcasecmp(p, loaders[c].ext)) { - tape_loader = c; loaders[c].load(cpath); return; } c++; } - tape_loaded = 0; } -void tape_close() -{ - if (tape_loaded && tape_loader < 2) - loaders[tape_loader].close(); - tape_loaded = 0; +/* define this to get messages on poll timing */ +/* +#ifdef BUILD_TAPE_CHECK_POLL_TIMING +#include +static uint64_t poll_timing_us_prev = 0; +static uint32_t poll_timing_num_calls = 0; +#endif +*/ + +/* TOHv4.1: removed record_activated arg; this function should + * now only ever be called if record mode is activated */ +static int +wp1_4k8_init_blank_tape_if_none_loaded (uint8_t *filetype_bits_inout) { +/* uint8_t record_activated) {*/ + /* tape loaded? */ + if (TAPE_FILETYPE_BITS_NONE != *filetype_bits_inout) { + return TAPE_E_OK; /* yes */ + } + /* no tape loaded: initialise blank tape; all three/four + file types simultaneously, since we don't know + which one the user wants yet. */ + *filetype_bits_inout = TAPE_FILETYPES_ALL; + log_info("tape: initialised blank tape"); + gui_alter_tape_menus(*filetype_bits_inout); + /*gui_set_record_mode(1);*/ /*record_activated);*/ + return TAPE_E_OK; +} + +/* tape.c */ +static int wp_bitclk_handle_silence (tape_state_t *t, + uint8_t silent, + uint8_t tapenoise_active, + uint8_t record_is_pressed, + int64_t ns_per_bit, + uint8_t silence112, + uint8_t *reset_shift_register_out) { + + int e; + int64_t num_1200ths, i; + + e = TAPE_E_OK; + + /* handle silence logic: + 1. if silent, wipe the ACIA's tx data, send tape noise + 2. if not silent, but previously silent, end section, flush the silence to tape + */ + + if ( silent ) { + e = wp_end_data_section_if_ongoing(t, record_is_pressed, reset_shift_register_out); + if (record_is_pressed) { /* only accumulate silence if record is pressed */ + t->w_accumulate_silence_ns += ns_per_bit; + } + t->w_accumulate_bit_periods_ns += ns_per_bit; + num_1200ths = (t->w_accumulate_bit_periods_ns) / TAPE_1200TH_IN_NS_INT; + /* consume, if > 0; leave any remainder on variable for next time */ + t->w_accumulate_bit_periods_ns -= (num_1200ths * TAPE_1200TH_IN_NS_INT); + for (i=0; tapenoise_active && (i < num_1200ths); i++) { /* TOHv4: tones_per_bit loop */ + tapenoise_send_1200 ('S', &(t->tapenoise_no_emsgs)); + } + } else if (t->w_accumulate_silence_ns > 0) { + /* a prior silent section has ended, so we need to write it out */ + if (record_is_pressed) { + e = tape_write_silence_2 (t, + ((double) t->w_accumulate_silence_ns) / 1000000000.0, + silence112); + } + t->w_accumulate_silence_ns = 0; + } + + return e; + +} + + +/* TOHv3.2: rework; flush partial frames out to UEF properly */ +/* tape.c */ +static int wp_end_data_section_if_ongoing (tape_state_t *t, + uint8_t record_is_pressed, + uint8_t *reset_shift_register_out) { + + int e; + + e = TAPE_E_OK; + *reset_shift_register_out = 0; + + if (t->w_must_end_data) { + + /* Make sure there isn't a half-finished frame kicking about + on the UEF reservoir; this would end up being prepended to the start of + the next block, with disastrous consequences. This can happen + if the ACIA pipelining is wrong and the master reset that fires + at the end of the block lets half a byte escape or something. + Particular culprits: 300 baud, fast-tape writing. */ + + if (record_is_pressed && (t->w_uef_serial_phase != 0)) { + + log_warn("tape: WARNING: partial frame (phase=%u, expected 0)", + t->w_uef_serial_phase); + + e = tape_uef_flush_incomplete_frame (&(t->uef), + &(t->w_uef_serial_phase), + &(t->w_uef_serial_frame), + &(t->w_uef_tmpchunk)); + t->w_uef_serial_phase = 0; + + } + + *reset_shift_register_out = 1; + + if (record_is_pressed) { + e = tape_write_end_data(t); + } + t->w_must_end_data = 0; + + } + + return e; + +} + + + +/* tape.c */ +static int wp_bitclk_accumulate_leader (tape_state_t *t, + uint8_t tapenoise_active, + int64_t ns_per_bit, + uint8_t record_is_pressed, + uint8_t *reset_shift_reg_out) { + + int64_t num_1200ths, i; + /* if there is nothing to output in the shift register, then we have leader tone */ + /* end and write out any current data section */ + wp_end_data_section_if_ongoing (t, record_is_pressed, reset_shift_reg_out); + if (record_is_pressed) { /* only accumulate leader if record is pressed */ + t->w_accumulate_leader_ns += ns_per_bit; + } + t->w_accumulate_bit_periods_ns += ns_per_bit; + num_1200ths = t->w_accumulate_bit_periods_ns / TAPE_1200TH_IN_NS_INT; + /* consume it; leave remainder */ + t->w_accumulate_bit_periods_ns -= (num_1200ths * TAPE_1200TH_IN_NS_INT); + for (i=0; tapenoise_active && (i < num_1200ths); i++) { + tapenoise_send_1200 ('L', &(t->tapenoise_no_emsgs)); + } + + return TAPE_E_OK; + +} + + +/* tape.c */ +static int wp_flush_accumulated_leader_maybe (tape_state_t *t, uint8_t record_is_pressed) { + int e; + /* handle the end of a leader section */ + if ( t->w_accumulate_leader_ns > 0 ) { + if (record_is_pressed) { +/*printf("flushing %lf s of accumulated leader\n", t->w_accumulated_leader_s);*/ + e = tape_write_leader (t, t->w_accumulate_leader_ns / TAPE_1200TH_IN_NS_INT); + if (TAPE_E_OK != e) { return e; } + } + t->w_accumulate_leader_ns = 0; + } + return TAPE_E_OK; +} + +/* tape.c */ +static int +wp_bitclk_start_data_if_needed (tape_state_t *ts, + /* need to provide baud rate + framing, + for TIBET hints, UEF &114 header, etc: */ + serial_framing_t *f, + uint8_t always_117, + uint8_t record_is_pressed) { + + int e; + + e = TAPE_E_OK; + + /* Beware: record_is_pressed may be activated at any time, + including in the middle of a block. Additionally, there + is a further pathological case where record_is_pressed + gets toggled multiple times during the course of a + single block. We have to know when we're supposed to call + tape_write_start_data() under such circumstances. */ + if ( ! ts->w_must_end_data ) { + ts->w_must_end_data = 1; + if (record_is_pressed) { + e = tape_write_start_data (ts, always_117, f); + } + } + + return e; + +} + + + + +#ifdef BUILD_TAPE_READ_POS_TO_EOF_ON_WRITE +static int read_ffwd_to_end (tape_state_t *t) { + int e; + e = TAPE_E_OK; + if (TAPE_FILETYPE_BITS_UEF & t->filetype_bits) { + e = uef_ffwd_to_end (&(t->uef)); + } + if ((TAPE_E_OK == e) && (TAPE_FILETYPE_BITS_CSW & t->filetype_bits)) { + e = csw_ffwd_to_end (&(t->csw)); + } + if ((TAPE_E_OK == e) && (TAPE_FILETYPE_BITS_TIBET & t->filetype_bits)) { + e = tibet_ffwd_to_end (&(t->tibet)); + } + t->tape_finished_no_emsgs = 1; + return e; +} +#endif + + +/* calls gated by ( ( ! silent ) && ( ! have_leader ) ) */ +static int wp_bitclk_output_data (tape_state_t *ts, + ACIA *acia, + serial_framing_t *f, + uint8_t tapenoise_active, + int64_t ns_per_bit, + uint8_t record_is_pressed, + uint8_t always_117) { + + int e; + int64_t i, num_1200ths; + char p; + uint8_t stop_bit_position; /* TOHv4.1 */ + + e = TAPE_E_OK; + + /* + This is another hackish consequence of the fact that emulated + tape hardware is only really half-duplex. Scenario: + + - Record & append to tape is DISABLED (read mode) + - Tape noise generation is ENABLED + - a SAVE operation begins + + In this situation, rather than the SAVE operation producing + corresponding tape noise as might be expected, the tape noise + generator will instead PLAY BACK the loaded tape while + the SAVE occurs (remember, no actual writing to tape takes + place because record&append is OFF). In order to get tape + noise from the SAVE operation, record&append needs to be + enabled. + + In order to combat this counter-intuitive behaviour, + we can define BUILD_TAPE_READ_POS_TO_EOF_ON_WRITE in order + to ensure that the read pointers for any loaded tape formats + are advanced to EOF when a data write occurs ("tape finished"), + ensuring that no audio is played back. This is done regardless + of whether record&append is enabled or not. */ + + /* TOHv3.3: added extra call to read_ffwd_to_end() to stop receive overflows */ +#ifdef BUILD_TAPE_READ_POS_TO_EOF_ON_WRITE + e = read_ffwd_to_end(ts); + if (TAPE_E_OK != e) { return e; } +#endif + + /* setup: handle TIBET hints, UEF baud chunk &117, + UEF chunk &114 header information: */ + e = wp_bitclk_start_data_if_needed (ts, f, always_117, record_is_pressed); + if (TAPE_E_OK != e) { return e; } + + ts->w_accumulate_bit_periods_ns += ns_per_bit; + num_1200ths = ts->w_accumulate_bit_periods_ns / TAPE_1200TH_IN_NS_INT; + /* consume any whole 1200ths */ + ts->w_accumulate_bit_periods_ns -= (num_1200ths * TAPE_1200TH_IN_NS_INT); + + stop_bit_position = 1 + f->num_data_bits + ((f->parity != 'N') ? 1 : 0); /* TOHv4.1 */ + + /* FIXME: I don't think this 0-case should ever happen? + wp_bitclk_accumulate_leader() should have + been called instead of this one? */ + if (0 == acia->tx_shift_reg_loaded) { + + log_warn("tape: BUG: wp_bitclk_output_data called with shift reg not loaded"); + return TAPE_E_BUG; + + } else if (0 == acia->tx_shift_reg_shift) { /* TOHv4.1 */ + acia->tx_odd_ones = 0; + } else if (acia->tx_shift_reg_shift < (f->num_data_bits + 1)) { /* nom. 9 */ + + /* Start bit not sent yet? Send it now. Start bit is deferred, + in order to simulate the correct TX pipelining behaviour. + (This won't jeopardise tapenoise; double bit will just be + sequenced in the tapenoise ringbuffer.) */ + if (1 == acia->tx_shift_reg_shift) { + for (i=0; + record_is_pressed && (TAPE_E_OK == e) && (i < num_1200ths); + i++) { + e = tape_write_1200th (ts, f, tapenoise_active, '0'); + } + } + + /* data bit */ + for (i=0; + record_is_pressed && (TAPE_E_OK == e) && (i < num_1200ths); + i++) { + e = tape_write_1200th (ts, + f, + tapenoise_active, + (acia->tx_shift_reg_value & 1) ? '1' : '0'); + } + + if (acia->tx_shift_reg_value & 1) { + acia->tx_odd_ones = ! acia->tx_odd_ones; + } + + /* TOHv4.1: parity code was completely missing. LOL */ + } else if ((f->parity != 'N') && (acia->tx_shift_reg_shift == (f->num_data_bits + 1))) { + p = '0'; + if ( ( ('E' == f->parity) && ( acia->tx_odd_ones ) ) + || ( ('O' == f->parity) && ( ! acia->tx_odd_ones ) ) ) { + p = '1'; + } + /* send parity bit */ + for (i=0; + record_is_pressed && (TAPE_E_OK == e) && (i < num_1200ths); + i++) { + e = tape_write_1200th (ts, + f, + tapenoise_active, + p); + } + } else if (acia->tx_shift_reg_shift >= stop_bit_position) { /* TOHv4.1: dedicated stop_bit_position */ + + /* stop bit(s) */ + for (i=0; + record_is_pressed && (TAPE_E_OK == e) && (i < num_1200ths); + i++) { + e = tape_write_1200th (ts, f, tapenoise_active, '1'); + } + + } + /* else { + log_warn("tape: BUG: tx_shift_reg_shift insane value %u", acia->tx_shift_reg_shift); + return TAPE_E_BUG; + }*/ + + return e; + +} + +#include "acia.h" + + +int tape_flush_pending_piece (tape_state_t *ts, ACIA *acia_or_null, uint8_t silence112) { + + uint8_t reset_shift_reg; + int e; + + e = TAPE_E_OK; + +/*printf("tape_flush_pending_piece: w_accumulate_silence_ns = %lld\n", ts->w_accumulate_silence_ns);*/ + + if (ts->w_accumulate_leader_ns > 0) { + e = tape_write_leader (ts, ts->w_accumulate_leader_ns / TAPE_1200TH_IN_NS_INT); + ts->w_accumulate_leader_ns = 0; + } else if (ts->w_accumulate_silence_ns > TAPE_1200TH_IN_NS_INT) { + e = tape_write_silence_2 (ts, + ((double) ts->w_accumulate_silence_ns) / 1000000000.0, + silence112); + ts->w_accumulate_silence_ns = 0; + } else { + reset_shift_reg = 0; + wp_end_data_section_if_ongoing (ts, 1, &reset_shift_reg); + if (acia_or_null != NULL) { + acia_hack_tx_reset_shift_register(acia_or_null); + } else { + /* log_warn("tape: WARNING: ignoring reset_shift_reg=1 from wp_end_data_section_if_ongoing");*/ + } + } + + /* TOHv3.2: fixed memory leak: never do this; + it wipes the allocation on w_uef_tmpchunk! */ + /*memset(&(t->w_uef_tmpchunk), 0, sizeof(uef_chunk_t));*/ + + /* do this instead */ + ts->w_uef_tmpchunk.num_data_bytes_written = 0; + ts->w_uef_tmpchunk.type = 0xffff; + ts->w_uef_tmpchunk.len = 0; + + return e; + +} + + + +static int tape_write_prelude (tape_state_t *ts, + uint8_t record_is_pressed, + uint8_t no_origin_chunk_on_append) { + + int e; + + e = TAPE_E_OK; + +#ifdef BUILD_TAPE_READ_POS_TO_EOF_ON_WRITE + if (record_is_pressed) { + e = read_ffwd_to_end(ts); + if (TAPE_E_OK != e) { return e; } + } +#endif + + /* make sure that the first thing that gets written + to a UEF stream is an origin chunk. Note that we are + writing this directly to the UEF on the tape_state_t, + whereas data will be written to the staging chunk, + t->w_uef_tmpchunk (for now). */ + + if ( ( ts->filetype_bits & TAPE_FILETYPE_BITS_UEF ) + && record_is_pressed + && ! ts->w_uef_origin_written ) { + + if ( ! no_origin_chunk_on_append ) { + e = uef_store_chunk(&(ts->uef), + (uint8_t *) VERSION_STR, + 0, /* origin, chunk &0000 */ + strlen(VERSION_STR)+1, /* include the \0 */ + 0); /* TOHv4.2: dummy offset */ + if (TAPE_E_OK != e) { return e; } + } + + ts->w_uef_origin_written = 1; /* don't try again, mark it written even if disabled */ + + } + + /* TOHv4.1: now gated by record_is_pressed */ + if (record_is_pressed) { + e = wp1_4k8_init_blank_tape_if_none_loaded (&(ts->filetype_bits)); /*, record_is_pressed);*/ + if (TAPE_E_OK != e) { return e; } + } + + return e; + +} + + +/* TOHv4: now, we want to be called at the bit rate. + For each tick, we either have leader, silence, or data. + Leader and silence packets will have to be expanded out into + the appropriate number of 1200ths. */ +int tape_write_bitclk (tape_state_t *ts, + ACIA *acia, + char bit, + int64_t ns_per_bit, /* 1024 = 1200 baud; 4096 = 300; 8192 = 150 etc. */ + uint8_t silent, + uint8_t tapenoise_write_enabled, + uint8_t record_is_pressed, + uint8_t always_117, + uint8_t silence112, + uint8_t no_origin_chunk_on_append) { + + uint8_t have_leader; + int e; + serial_framing_t f; + uint8_t reset_shift_reg; + uint8_t had_data = 0; + + if (0 == ns_per_bit) { + log_warn("tape: BUG: ns_per_bit is zero!"); + return TAPE_E_BUG; + } + + had_data = tape_peek_for_data(ts); + + /* do some housekeeping: */ + e = tape_write_prelude (ts, record_is_pressed, no_origin_chunk_on_append); + if (TAPE_E_OK != e) { return e; } + + /* + handle silence: + - if silent: + - flush data section, if this silence interrupts one; + - generate silent tapenoise (potentially multiple 1200ths) + - or, if not silent: + - write silent section if one has just ended + */ + reset_shift_reg = 0; + e = wp_bitclk_handle_silence (ts, + silent, + tapenoise_write_enabled, + record_is_pressed, + ns_per_bit, + silence112, + &reset_shift_reg); + if (TAPE_E_OK != e) { return e; } + + /* Hack?? reset tx shiftreg if a partial frame needs flushing to the UEF. + Shouldn't be necessary?? */ + if (reset_shift_reg) { acia_hack_tx_reset_shift_register(acia); } + + /* handle leader: + - write data section if leader interrupts one; + - or, write leader section if one has just ended; + - if leader and tapenoise active, then send a leader tapenoise 1200th */ + have_leader = ! acia->tx_shift_reg_loaded; + + if ( have_leader && ! silent ) { + /* also sends potentially multiple 1200ths of tapenoise */ + e = wp_bitclk_accumulate_leader (ts, + record_is_pressed && tapenoise_write_enabled, + ns_per_bit, + record_is_pressed, + &reset_shift_reg); + if (TAPE_E_OK != e) { return e; } + } + + /* Hack?? reset tx shiftreg if a partial frame needs flushing to the UEF. + Shouldn't be necessary?? */ + if (reset_shift_reg) { acia_hack_tx_reset_shift_register(acia); } + + if ( silent || ! have_leader ) { + /* end of leader section? */ + e = wp_flush_accumulated_leader_maybe (ts, record_is_pressed); + if (TAPE_E_OK != e) { return e; } + } + + /* Silence and leader 1200ths & tapenoise are done; + data still remains: */ + if ( ( ! silent ) && ( ! have_leader ) ) { + +/* +#ifdef BUILD_TAPE_CHECK_POLL_TIMING +*/ +/* timing check thing */ +/* +struct timeval tv; +uint64_t us, elapsed; + +gettimeofday(&tv, NULL); +us = (tv.tv_sec * 1000000) + tv.tv_usec; +#define POLL_TIMING_NUM_CALLS 1000 +if (POLL_TIMING_NUM_CALLS == poll_timing_num_calls) { + elapsed = us - poll_timing_us_prev;*/ + /* overhaul v2: changed, moving average now */ + /*printf("tape poll timing: %llu microseconds\n", elapsed / POLL_TIMING_NUM_CALLS); + poll_timing_num_calls = 0; + poll_timing_us_prev = us; +} else { + poll_timing_num_calls++; +} +#endif */ + + acia_get_framing (acia->control_reg, &f); + f.nominal_baud = (int32_t) (0x7fffffff & ((TAPE_1200TH_IN_NS_INT * 1200) / ns_per_bit)); + + e = wp_bitclk_output_data (ts, + acia, + &f, + record_is_pressed && tapenoise_write_enabled, + ns_per_bit, + record_is_pressed, + always_117); + if (TAPE_E_OK != e) { return e; } + + } + + /* need to maybe un-grey Catalogue Tape in the Tape GUI menu? */ + if (tape_peek_for_data(ts) != had_data) { /* suddenly we have data, as it's just been written */ + gui_alter_tape_menus_2(); + } + + return e; + +} + +/* TOHv3.2 */ +static int tape_uef_flush_incomplete_frame (uef_state_t *uef_inout, + uint8_t *serial_phase_inout, + uint8_t *serial_frame_inout, + uef_chunk_t *chunk_inout) { + + uint8_t i; + int e; + + /* + printf ("tape_uef_flush_incomplete_frame (phase=%u, frame=0x%02x)\n", + *serial_phase_inout, *serial_frame_inout); + */ + + if ((*serial_phase_inout >= 2) && (*serial_phase_inout <= 8)) { + for (i=0; i < (9 - *serial_phase_inout) ; i++) { + *serial_frame_inout >>= 1; + } + } + e = uef_append_byte_to_chunk (chunk_inout, *serial_frame_inout); + if (TAPE_E_OK == e) { + e = uef_store_chunk(uef_inout, + chunk_inout->data, + chunk_inout->type, + chunk_inout->len, + 0); /* TOHv4.2: dummy offset */ + } + + /* we don't clean up the uef tmpchunk allocation any more; + just set the length to zero, and leave alloc and the data + buffer as they are, for future use */ + chunk_inout->len = 0; + chunk_inout->num_data_bytes_written = 0; + *serial_phase_inout = 0; + *serial_frame_inout = 0; + + return e; + +} + + +/* should be called by the reset callback */ +int tape_handle_acia_master_reset (tape_state_t *t) { + + int e; + + e = TAPE_E_OK; + + if (NULL == t) { + log_warn("BUG: tape_handle_acia_master_reset(NULL)\n"); + return TAPE_E_BUG; + } + +/*log_warn("tape_handle_acia_master_reset: phase is %u\n", t->w_uef_serial_phase);*/ + + /* if reset occurs during a frame */ + if ((t->filetype_bits & TAPE_FILETYPE_BITS_UEF) && (t->w_uef_serial_phase != 0)) { + /* if UEF, finish the frame */ + /* phase=1 : start bit sent + * phase=2 : bit 0 sent + * ... + * phase=9 : bit 7 sent + */ + /* TOHv3.2: actually push partial frames out to the UEF properly */ + e = tape_uef_flush_incomplete_frame (&(t->uef), + &(t->w_uef_serial_phase), + &(t->w_uef_serial_frame), + &(t->w_uef_tmpchunk)); + } + + return e; + +} + + +static int tape_write_1200th (tape_state_t *ts, + serial_framing_t *f, + uint8_t tapenoise_active, + char value) { + + int e; + uint8_t v; + + e = TAPE_E_OK; + + /* 1. TAPE NOISE */ + if (tapenoise_active) { + tapenoise_send_1200(value, &(ts->tapenoise_no_emsgs)); + } + + v = (value=='0') ? 0 : 1; + + do { /* try { */ + + double pulse; + uint8_t i; + uint8_t have_uef_bit, bit_value; + uint8_t total_num_bits; + uint16_t mask; + uint8_t len; + const char *payload117; + + /* 2. TIBET */ + if (ts->filetype_bits & TAPE_FILETYPE_BITS_TIBET) { /* TIBET enabled? */ + for (i=0; (TAPE_E_OK == e) && (i < 2); i++) { /* write two tonechars */ + e = tibet_append_tonechar (&(ts->tibet), v?'.':'-', &(ts->tibet_data_pending)); + } + if (TAPE_E_OK != e) { break; } + } + + /* 3. UEF */ + if ( ts->filetype_bits & TAPE_FILETYPE_BITS_UEF ) { + + have_uef_bit = 0; + + /* Have to decode incoming stream of 1200ths to make + chunks &100 and &104. Need to respect wacky baud rates + in order to do that. */ + + mask = 0; + if (1200 == f->nominal_baud) { + mask = 1; + len = 1; + } else if (600 == f->nominal_baud) { + mask = 3; + len = 2; + } else if (300 == f->nominal_baud) { + mask = 0xf; + len = 4; + } else if (150 == f->nominal_baud) { + mask = 0xff; + len = 8; + } else if (75 == f->nominal_baud) { + mask = 0xffff; + len = 16; + } else { + /* TOHv4.1: downgraded from bug to warning */ + log_warn("tape: WARNING: illegal tx baud rate %d", f->nominal_baud); + ts->w_uef_tmpchunk.len = 0; + //e = TAPE_E_BUG; + break; + } + + (ts->w_uef_enc100_shift_value) <<= 1; + (ts->w_uef_enc100_shift_value) |= v; + (ts->w_uef_enc100_shift_value) &= mask; + (ts->w_uef_enc100_shift_amount)++; + + /* check that the frame we have so far is uniform */ + for (i=0; i < ts->w_uef_enc100_shift_amount; i++) { + if ( (((ts->w_uef_enc100_shift_value) >> i) & 1) + != (ts->w_uef_enc100_shift_value & 1) ) { + log_warn("tape: BUG? UEF chunk &100/&104 bit-assembly shift register desync!"); + /* resynchronise; keep the new wayward bit and discard anything older */ + (ts->w_uef_enc100_shift_amount) = 1; + (ts->w_uef_enc100_shift_value) >>= i; + (ts->w_uef_enc100_shift_value) &= 1; + break; + } + } + + if (ts->w_uef_enc100_shift_amount == len) { + /* bit ready */ + have_uef_bit = 1; + bit_value = ts->w_uef_enc100_shift_value & 1; + (ts->w_uef_enc100_shift_amount) = 0; + } + + if (have_uef_bit) { + + total_num_bits = 1 + f->num_data_bits + + ((f->parity == 'N') ? 0 : 1) + + f->num_stop_bits; + + if (0 == ts->w_uef_serial_phase) { + /* expect start bit */ + if (0 == bit_value) { + ts->w_uef_serial_frame = 0; + (ts->w_uef_serial_phase)++; + } + /* don't advance phase if start bit not found */ + } else if (ts->w_uef_serial_phase <= f->num_data_bits) { + (ts->w_uef_serial_frame) >>= 1; + ts->w_uef_serial_frame |= (bit_value ? 0x80 : 0); + (ts->w_uef_serial_phase)++; + } else if (ts->w_uef_serial_phase == (total_num_bits - 1)) { + + /* (second) stop bit; frame is ready */ + + if (7 == f->num_data_bits) { + /* TOHv4.1: hack: fix UEF bug with 7-bit frames; + * one extra bitshift is required in this case. */ + (ts->w_uef_serial_frame) >>= 1; /* one ping only */ + } + + ts->w_uef_serial_phase = 0; /* expect next start bit */ + + if (ts->w_uef_prevailing_baud != f->nominal_baud) { + + ts->w_uef_prevailing_baud = f->nominal_baud; /* acknowledge the change */ + /*printf("new baud %u\n", f->baud300 ? 300 : 1200);*/ + + if (0 == ts->w_uef_tmpchunk.num_data_bytes_written) { + /* OK. Nothing written yet in this chunk, so we can insert a chunk 117 + with no ill effects. */ + /*printf("uef: new baud %u; OK to insert &117, no data written yet\n", f->baud300 ? 300 : 1200);*/ + payload117 = NULL; + e = uef_get_117_payload_for_nominal_baud (f->nominal_baud, &payload117); + if (TAPE_E_OK != e) { break; } + e = uef_store_chunk (&(ts->uef), + (uint8_t *) payload117, + 0x117, + 2, + 0); /* TOHv4.2: dummy offset */ + if (TAPE_E_OK != e) { break; } + } else { + /* TODO: a baud change occurred during a data chunk. + Not yet supported for UEF. + This needs the current data chunk to be flushed, a baud chunk &117 to be inserted, + and then another data chunk begun that contains the remaining data from + this point onwards. + ( Workaround: Use TIBET, insert "end","/baud 300","data" manually, then tibetuef.php. ) */ + log_warn("uef: BUG: baud change (%u) mid-chunk is not implemented!", f->nominal_baud); + } + } + + /* write byte into temporary chunk */ + e = uef_append_byte_to_chunk (&(ts->w_uef_tmpchunk), ts->w_uef_serial_frame); + if (TAPE_E_OK != e) { break; } + (ts->w_uef_tmpchunk.num_data_bytes_written)++; + } else { /* parity or first of two stop bits */ + (ts->w_uef_serial_phase)++; + } + + } /* endif (have_uef_bit) */ + + } /* endif UEF */ + + /* 4. CSW */ + if ( ts->filetype_bits & TAPE_FILETYPE_BITS_CSW ) { + + e = csw_init_blank_if_necessary(&(ts->csw)); + if (TAPE_E_OK != e) { return e; } + + pulse = ts->csw.len_1200th_smps_perfect / 2.0; + if (v) { pulse /= 2.0; } + + for (i=0; (TAPE_E_OK == e) && (i < (v?4:2)); i++) { + e = csw_append_pulse_fractional_length (&(ts->csw), pulse); + } + + if (TAPE_E_OK != e) { break; } + + } + + } while (0); + + if (TAPE_E_OK != e) { + ts->w_uef_tmpchunk.len = 0; + } + + return e; + +} + + +static int check_pending_tibet_span_type (tape_state_t *t, uint8_t type) { + if (t->tibet_data_pending.type != type) { + log_warn("tape: write: warning: TIBET: pending span has type %u, should be %u", + t->tibet_data_pending.type, type); + return TAPE_E_BUG; + } + return TAPE_E_OK; +} + + + +static int tape_write_end_data (tape_state_t *t) { + + int e; + + e = TAPE_E_OK; + + if (t->filetype_bits & TAPE_FILETYPE_BITS_TIBET) { + /* ensure we have a data span on the go */ + e = check_pending_tibet_span_type(t, TIBET_SPAN_DATA); /* an assert */ + /* TOHv4-rc2: Can get a non-data pending span type here if you + * hit Record and Append in the middle of a block write + * with an initially-blank tape. In this case such a data span is + * empty anyway so we just skip it. */ + if (TAPE_E_OK == e) { + e = tibet_append_data_span(&(t->tibet), &(t->tibet_data_pending)); + } else { + /* TOHv4-rc2 */ + log_warn("tape: write: warning: TIBET: discarding partial data span"); + } + if (TAPE_E_OK != e) { return e; } + memset(&(t->tibet_data_pending), 0, sizeof(tibet_span_t)); + } + + + if ((TAPE_E_OK == e) && (t->filetype_bits & TAPE_FILETYPE_BITS_UEF)) { + /* UEF: append tmpchunk to actual UEF struct */ + e = uef_store_chunk(&(t->uef), + t->w_uef_tmpchunk.data, + t->w_uef_tmpchunk.type, + t->w_uef_tmpchunk.len, + 0); /* TOHv4.2: dummy offset */ + } + + /* we don't clean up the uef tmpchunk allocation any more; + just set the length to zero, and leave alloc and the data + buffer as they are, for future use */ + t->w_uef_tmpchunk.len = 0; + t->w_uef_tmpchunk.num_data_bytes_written = 0; + + if (TAPE_E_OK != e) { return e; } + + return e; + +} + + +static int tape_write_start_data (tape_state_t *t, uint8_t always_117, serial_framing_t *f) { + + /* We don't insert hints for phase or speed any more. + * Those are qualities of a real-world tape, not + * one produced by an emulator. */ + + int e; + uint8_t i; + uint8_t uef_chunk_104_framing[3]; + tibet_span_hints_t *h; + const char *payload117; + + e = TAPE_E_OK; + + if (t->filetype_bits & TAPE_FILETYPE_BITS_TIBET) { + + e = check_pending_tibet_span_type(t, TIBET_SPAN_INVALID); /* an assert */ + if (e != TAPE_E_OK) { return e; } + + memset(&(t->tibet_data_pending), 0, sizeof(tibet_span_t)); + t->tibet_data_pending.type = TIBET_SPAN_DATA; + + h = &(t->tibet_data_pending.hints); + + h->have_baud = 1; + h->baud = f->nominal_baud; + h->have_framing = 1; + framing_to_string(h->framing, f); + if (e != TAPE_E_OK) { return e; } + } + + /* UEF */ + if (t->filetype_bits & TAPE_FILETYPE_BITS_UEF) { + + if (always_117) { + + payload117 = NULL; + + /* Well, UEF 0.10 states that the only values which are valid in + baud chunk &117 are 300 and 1200. Furthermore, it's only + a 16-bit field so theoretical baud rates > 38400 won't fit. + However, we can save perfectly valid tapes at e.g. 75 baud + (even though they can't be loaded back in). */ + + e = uef_get_117_payload_for_nominal_baud (f->nominal_baud, &payload117); + if (TAPE_E_OK != e) { return e; } + + /* If baud is valid for it, append baud chunk &117 */ + if (NULL != payload117) { + e = uef_store_chunk(&(t->uef), + (uint8_t *) payload117, + 0x117, + 2, + 0); /* TOHv4.2: dummy offset */ + if (e != TAPE_E_OK) { return e; } + } + + /* + * We set the last used baud rate here, so the rate change detection + * at byte level won't be triggered. + */ + + t->w_uef_prevailing_baud = f->nominal_baud; + + } + + /* clean up any prior tmpchunk */ + t->w_uef_tmpchunk.len = 0; + + /* work out which chunk type to use, based on framing */ + if ( ( 8 == f->num_data_bits) + && ( 1 == f->num_stop_bits) + && ('N' == f->parity) ) { + /* 8N1 => use chunk &100 */ + t->w_uef_tmpchunk.type = 0x100; + } else { + /* some other framing => use chunk &104 */ + t->w_uef_tmpchunk.type = 0x104; + } + + /* if chunk &104, then build the chunk sub-header w/framing information */ + if ( 0x104 == t->w_uef_tmpchunk.type ) { + + uef_chunk_104_framing[0] = f->num_data_bits; + uef_chunk_104_framing[1] = f->parity; + uef_chunk_104_framing[2] = f->num_stop_bits; + + for (i=0; (TAPE_E_OK == e) && (i < 3); i++) { + e = uef_append_byte_to_chunk(&(t->w_uef_tmpchunk), + uef_chunk_104_framing[i]); + } + + } + + /* on error, clean up tmpchunk */ + if (e != TAPE_E_OK) { + t->w_uef_tmpchunk.len = 0; + return e; + } + + } + + return e; + +} + + + +/* TODO: no support yet for (leader+&AA+leader), chunk &111 */ +static int tape_write_leader (tape_state_t *t, uint32_t num_1200ths) { + + int e; + uint8_t num_2400ths[2]; + tibet_span_hints_t hints; + + e = TAPE_E_OK; + memset(&hints, 0, sizeof(tibet_span_hints_t)); + + if (t->filetype_bits & TAPE_FILETYPE_BITS_TIBET) { + /* sanity: assert that no data is pending */ + e = check_pending_tibet_span_type (t, TIBET_SPAN_INVALID); + if (e != TAPE_E_OK) { return e; } + } + + /* this compromise may be necessary, because num_1200ths is computed + as (num_4800ths / 4): */ + if (0 == num_1200ths) { + num_1200ths = 1; + } + + if (t->filetype_bits & TAPE_FILETYPE_BITS_TIBET) { + e = tibet_append_leader_span (&(t->tibet), 2 * num_1200ths, &hints); + if (TAPE_E_OK != e) { return e; } + } + + /* chunk &110 */ + if (t->filetype_bits & TAPE_FILETYPE_BITS_UEF) { + tape_write_u16(num_2400ths, 2 * num_1200ths); + e = uef_store_chunk(&(t->uef), num_2400ths, 0x110, 2, 0); /* TOHv4.2: dummy offset */ + if (TAPE_E_OK != e) { return e; } + } + + if (t->filetype_bits & TAPE_FILETYPE_BITS_CSW) { + e = csw_append_leader(&(t->csw), num_1200ths); + } + + return e; + +} + + +/* TOHv3.2 (rc1 killer!): respect -tape112 switch (or GUI equivalent) */ +static int tape_write_silence_2 (tape_state_t *t, double len_s, uint8_t silence112) { + + int e; + tibet_span_hints_t hints; + int32_t num_112_cycs, rem; + uint8_t buf[2]; + + e = TAPE_E_OK; + memset(&hints, 0, sizeof(tibet_span_hints_t)); + + if (len_s < TAPE_1200TH_IN_S_FLT) { /*TAPE_832_US) {*/ + log_warn("tape: WARNING: zero-length silence on write, adjusting to one cycle :/"); + /* hack, to prevent silence w/duration of zero 1200ths */ + len_s = TAPE_1200TH_IN_S_FLT; /*TAPE_832_US;*/ + } + + /* FIXME: this is a little bit nasty, because the TIBET structs + only allow expressing silence in 2400ths, whereas the + text file itself allows silence with floating-point resolution */ + if (t->filetype_bits & TAPE_FILETYPE_BITS_TIBET) { + e = tibet_append_silent_span (&(t->tibet), len_s, &hints); + if (TAPE_E_OK != e) { return e; } + } + + /* silence in UEFs ... */ + if (t->filetype_bits & TAPE_FILETYPE_BITS_UEF) { + if (silence112) { + /* one or more chunk &112s */ + num_112_cycs = (int32_t) (0.5f + (len_s * TAPE_1200_HZ * 2.0)); // correct frequency value + if (0 == num_112_cycs) { + log_warn ("tape: WARNING: silence as &112: gap is tiny (%f s); round up to 1/2400", len_s); + num_112_cycs = 1; + } + /* multiple chunks needed */ + for ( rem = num_112_cycs; rem > 0; rem -= 65535 ) { + tape_write_u16(buf, (rem > 65535) ? 65535 : rem); + e = uef_store_chunk(&(t->uef), buf, 0x112, 2, 0); /* TOHv4.2: dummy offset */ + if (TAPE_E_OK != e) { return e; } + } + } else { + /* chunk &116 */ + /* FIXME: endianness? */ + union { float f; uint8_t b[4]; } u; + u.f = len_s; + e = uef_store_chunk(&(t->uef), u.b, 0x116, 4, 0); /* TOHv4.2: dummy offset */ + if (TAPE_E_OK != e) { return e; } + } + } + + if (t->filetype_bits & TAPE_FILETYPE_BITS_CSW) { + e = csw_append_silence(&(t->csw), len_s); + } + + return e; + +} + + +int tape_generate_and_save_output_file (tape_state_t *ts, /* TOHv4.2: pass this in */ + uint8_t filetype_bits, + uint8_t silence112, + char *path, + uint8_t compress, + ACIA *acia_or_null) { + + char *tape_op_buf, *bufz; + FILE *f; + int e; + size_t z, tape_op_buf_len, bufz_len; + uint8_t tibet, uef, csw; + + e = TAPE_E_OK; + tape_op_buf = NULL; + f = NULL; + tape_op_buf_len = 0; + bufz = NULL; + bufz_len = 0; + + /* make sure any pending pieces are appended before saving */ + e = tape_flush_pending_piece (ts, acia_or_null, silence112); + if (TAPE_E_OK != e) { return e; } + + tibet = (TAPE_FILETYPE_BITS_TIBET == filetype_bits); + uef = (TAPE_FILETYPE_BITS_UEF == filetype_bits); + csw = (TAPE_FILETYPE_BITS_CSW == filetype_bits); + + do { + + if (tibet) { + e = tibet_build_output (&(ts->tibet), + &tape_op_buf, + &tape_op_buf_len); + } else if (uef) { + e = uef_build_output (&(ts->uef), + &tape_op_buf, + &tape_op_buf_len); + } else if (csw) { + e = csw_build_output (&(ts->csw), + compress, + &tape_op_buf, + &tape_op_buf_len); + } else { + log_warn("tape: write: BUG: state failure"); + e = TAPE_E_BUG; + } + + if (TAPE_E_OK != e) { break; } + + if (NULL == tape_op_buf) { + log_warn("tape: write: BUG: generated tape output is NULL!"); + e = TAPE_E_BUG; + } else if (0 == tape_op_buf_len) { + log_warn("tape: write: BUG: generated tape output has zero length!"); + e = TAPE_E_BUG; + } + + if (TAPE_E_OK != e) { break; } + + if (compress && (tibet || uef)) { + /* TIBET and UEF are just gzipped */ + e = tape_zlib_compress (tape_op_buf, + tape_op_buf_len, + 1, /* use gzip encoding, boy howdy */ + &bufz, + &bufz_len); + if (TAPE_E_OK != e) { break; } + /* transparently replace original buffer */ + free(tape_op_buf); + tape_op_buf = bufz; + tape_op_buf_len = bufz_len; + } + + f = fopen(path, "wb"); + + if (NULL == f) { + log_warn("tape: could not open file for saving: %s", path); + e = TAPE_E_SAVE_FOPEN; + break; + } + + if ( (NULL != tape_op_buf) && (TAPE_E_OK == e) ) { + z = fwrite(tape_op_buf, tape_op_buf_len, 1, f); + if (z != 1) { + log_warn("tape: fwrite failed saving to file: %s", path); + e = TAPE_E_SAVE_FWRITE; + } else { + log_info("tape: saved: %s", path); + } + } + + } while (0); + + if (tape_op_buf != NULL) { + free(tape_op_buf); + tape_op_buf = NULL; + } + + if (NULL != f) { + fclose(f); + } + + if (TAPE_E_OK != e) { + log_warn("tape: failed to save output file (code %u)\n", e); + } + + return e; + +} + +//static int lol = 0; + +int +tape_tone_1200th_from_back_end (uint8_t strip_silence_and_leader, /* speedup */ + tape_state_t *ts, + uint8_t awaiting_start_bit, + uint8_t enable_phantom_block_protection, // TOHv3.3 + char *tone_1200th_out) { /* must know about leader */ + + int e; + uint8_t inhibit_start_bit_for_phantom_block_protection; /* TOHv3.2 */ + + *tone_1200th_out = '?'; + inhibit_start_bit_for_phantom_block_protection = 0; + +/* if (tape_peek_eof (ts)) { + *tone_1200th_out = 'S'; + return TAPE_E_EOF; + } */ + +//if (lol<600) { +// lol++; +// *tone_1200th_out = 'S'; +// return TAPE_E_OK; +//} + + + /* read 1/1200 seconds of tone from the selected tape back-end */ + + + do { /* TOHv3.2: only loops back if (strip_silence_and_leader) */ + + e = tape_read_1200th (ts, tone_1200th_out); + if (TAPE_E_EOF == e) { + *tone_1200th_out = 'S'; + ts->leader_skip_1200ths = 0; /* TOHv4.2 */ +//ts->silence_skip_1200ths = 0; /* TOHv4.2 */ + return TAPE_E_EOF; + } + if (e != TAPE_E_OK) { return e; } + + /* TOHv4.2 */ + /* Leader skip complication: + * If we have only seen a very short duration of leader 'L' so far, + * convert those tones back into '1's so they aren't leader-skipped. + * The aim is for a leader-skipped signal to contain a short run + * of '1's between blocks. This will allow enough of a breather + * that MOS can get its house in order with an overclocked ACIA. + * We go back to using 'L' once we've inserted a few 1200ths + * to make MOS happy. If enabled, the leader skip code can then do + * its thing with impunity. + * + * https://www.stardot.org.uk/forums/viewtopic.php?p=450605#p450605 + */ + if (('0' == *tone_1200th_out) || ('S' == *tone_1200th_out)) { + ts->leader_skip_1200ths = 0; + } else if (('L' == *tone_1200th_out) && (ts->leader_skip_1200ths < 10)) { + (ts->leader_skip_1200ths)++; + *tone_1200th_out = '1'; + } + +//if ('S' != *tone_1200th_out) { +// ts->silence_skip_1200ths = 0; +//} else { +// (ts->silence_skip_1200ths)++; +// if (ts->silence_skip_1200ths < 10) { +// break; +// } +//} + + /* handle some conditions concerning leader: */ + if ( awaiting_start_bit ) { + /* perform leader detection if not already done, + * to assist the tape noise generator: */ + if ( (ts->start_bit_wait_count_1200ths > 60) /* 15 bits of 300-baud ones */ + && ('1' == *tone_1200th_out)) { + *tone_1200th_out = 'L'; + } + /* If enabled, prevent "squawks" being mis-detected as + * the start of genuine MOS blocks. + * + * Real hardware tends to reject these phantom blocks, + * because smashing the analogue circuitry with a signal + * immediately following a long silence doesn't do wonders + * for its ability to detect a start bit correctly. It *is* + * possible to see these fake blocks on a Model B using a + * high quality digital tone source, and judicious volume + * settings. + * + * This check will make sure a certain duration of + * continuous tone has prevailed before a start bit may be + * recognised by the tape system. + * + * We don't do this if using the strip_silence_and_leader + * speed hack. + * + */ +#define SQUAWKPROT_1200THS_SINCE_SILENT 100 + if ( ! strip_silence_and_leader ) { + if ('S' == *tone_1200th_out) { + ts->num_1200ths_since_silence = 0; + /* avoid possible overflow when incrementing this: */ + } else if (ts->num_1200ths_since_silence < SQUAWKPROT_1200THS_SINCE_SILENT) { + (ts->num_1200ths_since_silence)++; + /* enforce >=~100 of 1200ths of leader tone after any silence */ + inhibit_start_bit_for_phantom_block_protection = 1; + } + } + /* otherwise, start bit is always eligible */ + (ts->start_bit_wait_count_1200ths)++; + } else { + ts->start_bit_wait_count_1200ths = 0; + } + + if (('0' == *tone_1200th_out) || ('S' == *tone_1200th_out)) { + ts->start_bit_wait_count_1200ths = 0; /* nix leader identification on '0' or 'S' */ + } + + } while ( (('L' == *tone_1200th_out) || ('S' == *tone_1200th_out)) + && strip_silence_and_leader + && (TAPE_E_OK == e)); /* TOHv3.2; loop to strip silence and leader */ + + if (e != TAPE_E_OK) { return e; } + + /* TOHv3.3: new run-time enable_phantom_block_protection variable */ + if (inhibit_start_bit_for_phantom_block_protection && enable_phantom_block_protection) { + *tone_1200th_out = 'L'; + } + + + + + return TAPE_E_OK; + } -/*Every 128 clocks, ie 15.625khz*/ -/*Div by 13 gives roughly 1200hz*/ -static uint16_t newdat; +static void framing_to_string (char fs[4], serial_framing_t *f) { + fs[0] = (f->num_data_bits == 7) ? '7' : '8'; + fs[1] = f->parity; + fs[2] = (f->num_stop_bits == 1) ? '1' : '2'; + fs[3] = '\0'; +} + + + +/* for tape catalogue */ + +/* WARNING: on error, caller is expected to clean up "out" */ +int tape_state_clone_and_rewind (tape_state_t *out, tape_state_t *in) { -void tape_poll(void) { - if (motor) { - if (csw_ena) csw_poll(); - else uef_poll(); + int e; + + e = TAPE_E_OK; + + memcpy(out, in, sizeof(tape_state_t)); + + /* wipe all of the structures on the target, and any pointers + they might have duplicated */ + memset(&(out->w_uef_tmpchunk), 0, sizeof(uef_chunk_t)); + memset(&(out->tibet), 0, sizeof(tibet_t) ); + memset(&(out->uef), 0, sizeof(uef_state_t)); + memset(&(out->csw), 0, sizeof(csw_state_t)); - if (newdat & 0x100) { - newdat&=0xFF; - tapenoise_adddat(newdat); + /* TOHv4.1: had horrible situation where this didn't get cloned, + * and so when findfilenames_new() did its thing, it would + * free the tibet_data_pending span on the live tape_state_t + * object instead. Erk */ + e = tibet_clone_span (&(out->tibet_data_pending), &(in->tibet_data_pending)); + + /* TOHv3: modified for simultaneous file types */ + if ((TAPE_FILETYPE_BITS_TIBET & in->filetype_bits) && (in->tibet.priv != NULL)) { + e = tibet_clone (&(out->tibet), &(in->tibet)); + if (TAPE_E_OK == e) { + e = tibet_rewind(&(out->tibet)); } - else if (csw_toneon || uef_toneon) - tapenoise_addhigh(); } + if ((TAPE_E_OK == e) && (TAPE_FILETYPE_BITS_CSW & in->filetype_bits)) { + e = csw_clone (&(out->csw), &(in->csw)); + if (TAPE_E_OK == e) { + csw_rewind (&(out->csw)); + } + } + if ((TAPE_E_OK == e) && (TAPE_FILETYPE_BITS_UEF & in->filetype_bits)) { + e = uef_clone (&(out->uef), &(in->uef)); + if (TAPE_E_OK == e) { + uef_rewind (&(out->uef)); + } + } + + if (TAPE_E_OK != e) { + log_warn("tape: clone-and-rewind: error, code %u\n", e); + /* caller must handle error, e.g. calling tape_state_finish() */ + } + + return e; + } -void tape_receive(ACIA *acia, uint8_t data) { - newdat = data | 0x100; + + +int findfilenames_new (tape_state_t *ts, + uint8_t show_ui_window, + uint8_t enable_phantom_block_protection) { + + int e; + int32_t n; + char fn[11]; + char s[256]; + uint32_t load, exec; + uint32_t file_len; + tape_state_t tclone; + ACIA acia_tmp; + + load = 0; + exec = 0; + file_len = 0; + + /* get a clone of the live tape state, + so we don't interfere with any ongoing cassette operation: */ + e = tape_state_clone_and_rewind (&tclone, ts); + + /* and make a suitable ACIA */ + memset(&acia_tmp, 0, sizeof(ACIA)); + acia_init(&acia_tmp); + acia_tmp.control_reg = 0x14; // 8N1 + + while ( /*TAPE_IS_LOADED(tclone.filetype_bits) &&*/ + (TAPE_E_OK == e) ) { + + int32_t limit; + uint16_t blknum; + uint16_t blklen; + uint8_t final; + uint8_t empty; + + memset(fn, 0, 11); + + limit = 29; /* initial byte limit, will be updated once block len is known */ + + blknum = 0; + blklen = 0; + final = 0; + empty = 0; + load = 0; + exec = 0; + memset (s, 0, 256); + + /* process one block: */ + for (n=0; (n < limit) && (TAPE_E_OK == e); n++) { + + uint8_t k; + uint8_t cat_1200th; + uint8_t value; + + /* assume 8N1/1200. + * + * MOS always uses 8N1; there is no concept of + * a "filename" without 8N1. However, the baud rate + * could be 300 rather than 1200. A job for another day + * will be to try both baud rates, and + * pick the one that produces the more meaningful outcome, + * although this gets tougher for mixed-baud tapes. + * + * For UEF chunks &100, &102 and &104 we could bypass + * tape_read_poll_...() and rifle through the UEF + * file itself; this would be baud-agnostic. It wouldn't + * help with chunk &114, nor with CSW or TIBET, though. */ + + e = tape_tone_1200th_from_back_end (0, /* don't strip silence and leader */ + &tclone, + acia_rx_awaiting_start(&acia_tmp), /* awaiting start bit? */ + enable_phantom_block_protection, // enable + (char *) &cat_1200th); + if (TAPE_E_OK != e) { break; } + + /* just assume 1200 baud for now, then + (i.e. 1/1200th = 1 ACIA RX bit): */ + e = acia_receive_bit_code (&acia_tmp, cat_1200th); + + if (TAPE_E_OK != e) { break; } + + if ( ! acia_rx_frame_ready(&acia_tmp) ) { + n--; + continue; /* frame not ready, go around again */ + } + + /* frame ready */ + + value = acia_read(&acia_tmp, 0); // status reg + value = acia_read(&acia_tmp, 1); // data reg + + if (0 == n) { /* 0 */ + /* need sync byte */ + if ('*' != value) { + n--; /* try again */ + } + } else if (n<12) { /* 1-11 */ + if ((11==n) && (value!=0)) { + /* no NULL terminator found; abort, resync */ + n=-1; + } else { + fn[n-1] = (char) value; + /* handle terminator */ + if (0 == value) { + memset(fn + n, 0, 11 - n); + n = 11; + /* sanitise non-printable characters */ + } else if ( ! isprint(value) ) { + fn[n-1] = '?'; + } + } + } else if (n<16) { /* 12-15 */ + load = (load >> 8) & 0xffffff; + load |= (((uint32_t) value) << 24); + } else if (n<20) { /* 16-19 */ + exec = (exec >> 8) & 0xffffff; + exec |= (((uint32_t) value) << 24); + } else if (n<22) { /* 20-21 */ + blknum = (blknum >> 8) & 0xff; + blknum |= (((uint16_t) value) << 8); + } else if (n<24) { /* 22-23 */ + blklen = (blklen >> 8) & 0xff; + blklen |= (((uint16_t) value) << 8); + } else if (n<25) { /* 24 */ + if (blklen < 0x100) { + final = 1; + } + if (0x80 & value) { + /* 'F' flag */ + final = 1; + } + if (0x40 & value) { + /* 'E' flag */ + empty = 1; + } + } else if (n<29) { /* 25-28 */ + /* As a sanity check, make sure that the next file address bytes + are all zero. If they're not, we'll skip this, because it's + likely not actually a file. Some protection schemes will + obviously break this. + (Ideally we'd check the header CRC instead) */ + if (value != 0) { + file_len = 0; + break; /* abort */ + } + if (28 == n) { + /* add block length to file total */ + file_len += (uint32_t) blklen; + if (final) { + for (k=0; k < 10; k++) { + /* for compatibility with what CSW does */ + if (fn[k] == '\0') { fn[k] = ' '; } + } + sprintf(s, "%s Size %04X Load %08X Run %08X", fn, file_len, load, exec); + /* TOHv3: For tape testing, don't bother to spawn the UI window. + It can lead to a race condition when shutdown happens + immediately on end-of-tape. Besides, the user will never + see it. */ + if (show_ui_window) { + cataddname(s); + } else { + /* TOHv3: for -tapetest: print to stdout; + wanted to use stderr, but difficulties + capturing output from it under Windows? */ + fprintf(stdout, "tapetest:%s %04x %08x %08x\n", fn, file_len, load, exec); + } + file_len = 0; + memset(fn, 0, 11); + } + /* skip remainder of block: HCRC2 + data + DCRC2 (but not if file empty) */ + limit += 2; + if ( ! empty ) { + limit += (blklen + 2); + } + } + } else { + /* actual block data here */ + /* ... skip byte ... */ + } + + } /* next byte in block */ + + } /* next block */ + + /* artificially force b-em shutdown, if TAPE_TEST_QUIT_ON_EOF; + this is for automated testing via -tapetest on CL */ + /* TOHv3.2: errors now take precedence over EOF condition */ + tape_handle_exception (&tclone, NULL, e, tape_vars.testing_mode & TAPE_TEST_QUIT_ON_EOF, 0, 0); + + /* free clone */ + tape_state_finish(&tclone, 0); /* 0 = DO NOT alter menus since this is a clone */ + + return e; + } + + + + +void tape_handle_exception (tape_state_t *ts, + tape_vars_t *tv, /* TOHv3.2: may be NULL */ + int error_code, + uint8_t eof_fatal, + uint8_t err_fatal, + uint8_t alter_menus) { /* 0 if called from findfilenames_new */ + int e, sx; + uint8_t tape_broken; + + if (TAPE_E_OK == error_code) { return; } + + sx = SHUTDOWN_OK; + + if (TAPE_E_EXPIRY == error_code) { + sx = SHUTDOWN_EXPIRED; /* TOHv4-rc1 */ + /* TOHv4.1: distinct file-not-found code */ + } else if (TAPE_E_FOPEN == error_code) { + /* TOHv4.1 */ + sx = SHUTDOWN_FOPEN; + log_warn("tape: tape load failure"); + } else if (TAPE_E_EOF == error_code) { + sx = SHUTDOWN_TAPE_EOF; /* TOHv4.1 */ + } else if (error_code != TAPE_E_OK) { /* generic error */ + log_warn("tape: code %d; disabling tape! (Eject tape to clear.)", error_code); + sx = SHUTDOWN_TAPE_ERROR; + // return; + } + + /* TOHv4.1: rework */ + tape_broken = (SHUTDOWN_OK != sx) && (SHUTDOWN_TAPE_EOF != sx); + + if ( tape_broken ) { + /* errors which are fatal to the tape system */ + tape_state_finish(ts, alter_menus); /* 1 = alter menus */ + if (tv != NULL) { + e = tape_set_record_activated(ts, tv, NULL, 0); + if (TAPE_E_OK == e) { + gui_set_record_mode(0); + /*tape_state_init(ts, tv, 0, 1);*/ /* alter menus */ + } + } + tape_state.disabled_due_to_error = 1; + } + + if ( err_fatal + && ( (SHUTDOWN_TAPE_ERROR == sx) || (SHUTDOWN_FOPEN == sx) )) { + quitting = true; + set_shutdown_exit_code(sx); + } else if (eof_fatal && (SHUTDOWN_TAPE_EOF == sx)) { + quitting = true; + set_shutdown_exit_code(sx); + } else if (SHUTDOWN_EXPIRED == sx) { /* ALWAYS fatal */ + quitting = true; + set_shutdown_exit_code(sx); + } + +} + +/* not currently used in TOHv4 */ +/* +static uint8_t tape_peek_eof (tape_state_t *t) { + + uint8_t r;*/ + + /* changed for simultaneous filetypes + + Things start to get potentially thorny here, because we + could have parallel representations that disagree on precisely + when the EOF is. We will operate on the principle that + if we have simultaneous filetypes (because we're writing + to output and user hasn't picked what kind of file we want + to save yet), just use UEF for reading back. */ + + /* + r = 1; + + if (TAPE_FILETYPE_BITS_UEF & t->filetype_bits) { + r = uef_peek_eof(&(t->uef)); + } else if (TAPE_FILETYPE_BITS_CSW & t->filetype_bits) { + r = csw_peek_eof(&(t->csw)); + } else if (TAPE_FILETYPE_BITS_TIBET & t->filetype_bits) { + r = 0xff & tibet_peek_eof(&(t->tibet)); + } + + return r; + +} */ + + +#include "6502.h" + +static int tape_read_1200th (tape_state_t *ser, char *value_out) { + + int e; + uef_meta_t meta[UEF_MAX_METADATA]; + uint32_t meta_len; + uint32_t m; + + e = TAPE_E_OK; + + /* TOHv3: Again the complication: + Saved data where the user hasn't picked an output format yet exists + as three parallel copies of itself. In this situation we use UEF + as the copy to read from, mainly because we might have written + UEF metadata into it which we would like to read back; we don't + have as rich metadata in the other file types. (TIBET allows timestamps + etc. but UEF's potential metadata is richer and more useful.) */ + + /* so, we check for UEF first. */ + if (TAPE_FILETYPE_BITS_UEF & ser->filetype_bits) { + meta_len = 0; + e = uef_read_1200th (&(ser->uef), + value_out, + meta, + &meta_len); + /* TODO: Any UEF metadata chunks that punctuate actual data + * chunks are accumulated on meta by the above call. Currently + * this includes chunks &115 (phase change), &120 (position marker), + * &130 (tape set info), and &131 (start of tape side). At + * present, nothing is done with these chunks and they are + * simply destroyed again here. For now we'll just log them */ + if ((TAPE_E_OK == e) || (TAPE_E_EOF == e)) { /* throws EOF */ + for (m=0; m < meta_len; m++) { + if (meta[m].type != 0x117) { /* only baud rate is currently used */ + log_info("uef: unused metadata, chunk &%x\n", meta[m].type); + } + } + uef_metadata_list_finish (meta, meta_len); + } + } else if (TAPE_FILETYPE_BITS_CSW & ser->filetype_bits) { + e = csw_read_1200th (&(ser->csw), value_out); + } else if (TAPE_FILETYPE_BITS_TIBET & ser->filetype_bits) { + /* The TIBET reference decoder has a built-in facility + * for decoding 300 baud; however, it is not used here, + * so zero is always passed to it. We always request + * 1/1200th tones from the TIBET back-end (and all other + * back-ends). 300 baud decoding is now done by b-em itself, + * regardless of the back-end in use. */ + e = tibet_read_bit (&(ser->tibet), + 0, /* always 1/1200th tones */ + value_out); + } else if (TAPE_FILETYPE_BITS_NONE == ser->filetype_bits) { + e = TAPE_E_EOF; + } else { + log_warn("tape: BUG: unknown internal filetype: &%x", ser->filetype_bits); + return TAPE_E_BUG; + } + + if (TAPE_E_EOF == e) { +#ifdef BUILD_TAPE_LOOP /* TOHv2 */ + log_warn("tape: tape finished; rewinding (TAPE_LOOP)"); + e = tape_rewind_2(ser); +#else + if ( ! tape_state.tape_finished_no_emsgs ) { + log_warn("tape: tape finished"); + } + tape_state.tape_finished_no_emsgs = 1; /* suppress further messages */ +#endif + } else { + tape_state.tape_finished_no_emsgs = 0; + } + + return e; + +} + +/* +void tape_rewind(void) { + tape_rewind_2 (&tape_state); +} +*/ + + +int tape_rewind_2 (tape_state_t *t) { + int e; + e = TAPE_E_OK; + if (TAPE_FILETYPE_BITS_CSW & t->filetype_bits) { + csw_rewind(&(t->csw)); + } + if (TAPE_FILETYPE_BITS_TIBET & t->filetype_bits) { + e = tibet_rewind(&(t->tibet)); + } + if (TAPE_FILETYPE_BITS_UEF & t->filetype_bits) { + uef_rewind(&(t->uef)); + } + t->tape_finished_no_emsgs = 0; /* TOHv2: un-inhibit "tape finished" msg */ + return e; +} + + +/* tv may be NULL: */ +void tape_state_init (tape_state_t *t, + tape_vars_t *tv, + uint8_t filetype_bits, + uint8_t alter_menus) { + + /* TOHv3.2: we don't touch Record Mode any more. If -record is + specified on the command line along with -tape (for auto-appending), + we don't want tape_state_init() or tape_state_finish() to cancel it. */ + + tape_state_finish(t, alter_menus); /* alter menus (grey-out Eject Tape, etc.) */ + t->filetype_bits = filetype_bits; + if (alter_menus) { + gui_alter_tape_menus(filetype_bits); + gui_set_record_mode(tape_is_record_activated(tv)); + } + t->w_uef_prevailing_baud = 1200; /* TOHv4 */ + + /* TOHv4 */ + /* This is a hack to make sure that the initial cached + values for dividers and thresholds that are stored on tape_state + are set to sane values, before MOS makes the first write to + the serial ULA's control register. */ + serial_recompute_dividers_and_thresholds (tape_vars.overclock, + 0x64, //tape_state.ula_ctrl_reg, + &(tape_state.ula_rx_thresh_ns), + &(tape_state.ula_tx_thresh_ns), + &(tape_state.ula_rx_divider), + &(tape_state.ula_tx_divider)); + +} + + + +void tape_state_finish (tape_state_t *t, uint8_t alter_menus) { + // uint8_t old_ula_ctrl_reg_value; + /* TOHv3: updated for simultaneous output formats */ + if (TAPE_FILETYPE_BITS_UEF & t->filetype_bits) { + uef_finish(&(t->uef)); + } + if (TAPE_FILETYPE_BITS_CSW & t->filetype_bits) { + csw_finish(&(t->csw)); + } + if (TAPE_FILETYPE_BITS_TIBET & t->filetype_bits) { + tibet_finish(&(t->tibet)); + } + if (t->w_uef_tmpchunk.data != NULL) { + free(t->w_uef_tmpchunk.data); + } + + /* TOHv4-rc2: no longer just zero out the whole state; + * turns out that is hoodlum behaviour. Be more selective */ + + /*memset(t, 0, sizeof(tape_state_t));*/ + if (t->tibet_data_pending.tones != NULL) { + free(t->tibet_data_pending.tones); + } + memset(&(t->tibet_data_pending), 0, sizeof(tibet_span_t)); + t->w_must_end_data = 0; + + if (alter_menus) { + /* set displayed Eject path to blank: */ + gui_alter_tape_eject(NULL); + /* deactivate Record Mode and update menus accordingly; + * simultaneously show all file types for saving since all + * are available on a blank tape: */ + gui_alter_tape_menus(TAPE_FILETYPES_ALL); + /*gui_alter_tape_menus_2();*/ /* grey out Catalogue Tape */ + } + /*tape_set_record_activated(t, &tape_vars, &sysacia, 0);*/ + t->filetype_bits = TAPE_FILETYPES_ALL; + t->w_accumulate_silence_ns = 0; + t->w_accumulate_leader_ns = 0; + memset(&(t->w_uef_tmpchunk), 0, sizeof(uef_chunk_t)); + t->w_uef_origin_written = 0; + t->w_uef_prevailing_baud = 1200; + t->w_uef_serial_frame = 0; + t->w_uef_serial_phase = 0; + t->w_uef_enc100_shift_value = 0; + t->w_uef_enc100_shift_amount = 0; + t->w_uef_was_loaded = 0; + t->disabled_due_to_error = 0; + t->tones300_fill = 0; + t->ula_prevailing_rx_bit_value = 'S'; + + /* TODO: pass in sysacia */ + /* + serial_push_dcd_cts_lines_to_acia(&sysacia, t); + acia_update_irq_and_status_read(&sysacia); + */ + +} + +uint8_t tape_is_record_activated(tape_vars_t *tv) { + return tv->record_activated; +} + +int tape_set_record_activated (tape_state_t *t, tape_vars_t *tv, ACIA *acia_or_null, uint8_t value) { + int32_t baud; + if (NULL == tv) { + log_warn("tape: BUG: tape_set_record_activated passed NULL tape_vars_t\n"); + return TAPE_E_BUG; + } else if (NULL == t) { + log_warn("tape: BUG: tape_set_record_activated passed NULL tape_state_t\n"); + return TAPE_E_BUG; + } + if ( (0 == value) && (0 != tv->record_activated) ) { + /* recording finished; flush any pending data */ + tape_flush_pending_piece(t, acia_or_null, tv->save_prefer_112); + } else { + /* work out current baud status when record is activated, + * so we know whether we need a chunk &117 for the first data + * chunk to be appended; set value on tape_state_t */ + baud = 0; + uef_scan_backwards_for_chunk_117 (&(t->uef), &baud); + if (0 == baud) { + baud = 1200; + } + tape_state.w_uef_prevailing_baud = baud; + } + tv->record_activated = value; + return TAPE_E_OK; +} + + +#define UNZIP_CHUNK 256 +#define DECOMP_DELTA (100 * 1024) + +int tape_decompress (uint8_t **out, uint32_t *len_out, uint8_t *in, uint32_t len_in) { + + /* if this returns E_OK, then 'in' will have been invalidated; + * otherwise, 'in' is still valid, and must be freed by the caller. */ + + uint8_t buf[UNZIP_CHUNK]; + size_t alloc; + z_stream strm; + uint32_t pos; + + *out = NULL; + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + strm.next_in = (unsigned char *) in; + strm.avail_in = len_in; + + /* OR 32 allows us to decompress both zlib (for CSW) + and gzip (for UEF and TIBETZ): */ + if ( inflateInit2 (&strm, 15 | 32) < 0 ) { + log_warn("tape: could not decompress data; zlib init failed."); + return TAPE_E_ZLIB_INIT; + } + + alloc = 0; + pos = 0; + + do { + + int zerr; + uint32_t piece_len; + uint32_t newsize; + uint8_t *p; + + strm.avail_out = UNZIP_CHUNK; + strm.next_out = buf; + + zerr = inflate (&strm, Z_NO_FLUSH); + + switch (zerr) { + case Z_OK: + case Z_STREAM_END: + case Z_BUF_ERROR: + break; + default: + inflateEnd (&strm); + log_warn ("tape: could not decompress data; zlib code %d", zerr); + if (NULL != *out) { free(*out); } + *out = NULL; + return TAPE_E_ZLIB_DECOMPRESS; + } + + piece_len = (UNZIP_CHUNK - strm.avail_out); + + p = NULL; + + if ((piece_len + pos) >= alloc) { + newsize = piece_len + pos + DECOMP_DELTA; + /* prevent shenanigans */ + if (newsize >= TAPE_MAX_DECOMPRESSED_LEN) { + log_warn ("tape: decompressed size is too large\n"); + inflateEnd (&strm); + if (*out != NULL) { free(*out); } + *out = NULL; + return TAPE_E_DECOMPRESSED_TOO_LARGE; + } + p = realloc (*out, newsize); + if (NULL == p) { + log_warn ("tape: could not decompress data; realloc failed\n"); + inflateEnd (&strm); + if (*out != NULL) { free(*out); } + *out = NULL; + return TAPE_E_MALLOC; + } + *out = p; + alloc = newsize; + } + + memcpy (*out + pos, buf, piece_len); + pos += piece_len; + + } while (strm.avail_out == 0); + + inflateEnd (&strm); + + *len_out = pos; + + return TAPE_E_OK; + +} + + +int tape_load_file (const char *fn, uint8_t decompress, uint8_t **buf_out, uint32_t *len_out) { + + FILE *f; + uint32_t pos, alloc; + uint8_t *buf; + int e; + uint8_t *buf2; + uint32_t buf2_len; + + e = TAPE_E_OK; + + buf = NULL; + buf2 = NULL; + buf2_len = 0; + + if (NULL == (f = fopen (fn, "rb"))) { + log_warn("tape: Unable to open file '%s': %s", fn, strerror(errno)); + return TAPE_E_FOPEN; + } + + pos = 0; + alloc = 0; + +#define TAPE_FILE_ALLOC_DELTA (1024 * 1024) + + while ( ! feof(f) && ! ferror(f) ) { + uint32_t chunk; + uint32_t newsize; + long num_read; + uint8_t *p; + chunk = 1024; + /* ask for 1024 bytes */ + if ((pos + chunk) >= TAPE_FILE_MAXLEN) { + log_warn("tape: File is too large: '%s' (max. %d)", fn, TAPE_FILE_MAXLEN); + e = TAPE_E_FILE_TOO_LARGE; + break; + } + if ((pos + chunk) >= alloc) { + newsize = pos + chunk + TAPE_FILE_ALLOC_DELTA; + p = realloc(buf, newsize); + if (NULL == p) { + log_warn("tape: Failed to grow file buffer: '%s'", fn); + e = TAPE_E_MALLOC; + break; + } + alloc = newsize; + buf = p; + } + num_read = fread (buf+pos, 1, chunk, f); + if (ferror(f) || (num_read < 0)) { + log_warn("tape: Stream error reading file '%s': %s", fn, strerror(errno)); + e = TAPE_E_FREAD; + break; + } + pos += (uint32_t) (0x7fffffff & num_read); + } + + fclose(f); + + if (TAPE_E_OK != e) { + free(buf); + return e; + } + + /* TOHv3.2: avoid attempting decompression, if uncompressed UEF is detected */ + e = uef_detect_magic (buf, pos, fn, 0); /* 0 = quiet, no errors */ + if (TAPE_E_OK == e) { decompress = 0; } + e = TAPE_E_OK; /* trap errors */ + + if (decompress) { + e = tape_decompress (&buf2, &buf2_len, buf, pos & 0x7fffffff); + free(buf); + if (TAPE_E_OK != e) { return e; } + log_info("tape_decompress: %u -> %u\n", (uint32_t) (pos & 0x7fffffff), buf2_len); + pos = buf2_len; + buf = buf2; + } + + *buf_out = buf; + *len_out = pos; + + return TAPE_E_OK; + +} + + +static void tibet_load (const char *fn) { + tibet_load_2(fn, 0); +} + +static void tibetz_load (const char *fn) { + tibet_load_2(fn, 1); +} + +static void uef_load (const char *fn) { + int e; + int32_t baud; + tape_state_init (&tape_state, &tape_vars, TAPE_FILETYPE_BITS_UEF, 1); + e = uef_load_file (fn, &(tape_state.uef)); + if (TAPE_E_OK != e) { + log_warn("tape: could not load UEF file (code %d): '%s'", e, fn); + tape_handle_exception(&tape_state, + &tape_vars, + e, + tape_vars.testing_mode & TAPE_TEST_QUIT_ON_EOF, + tape_vars.testing_mode & TAPE_TEST_QUIT_ON_ERR, + 1); /* alter menus on failure */ + } else { + load_successful((char *) fn); + tape_state.w_uef_was_loaded = 1; + tape_state.w_uef_origin_written = 0; + baud = 0; + uef_scan_backwards_for_chunk_117 (&(tape_state.uef), &baud); + if (0 == baud) { baud = 1200; } + tape_state.w_uef_prevailing_baud = baud; + } +} + + +static void tibet_load_2 (const char *fn, uint8_t decompress) { + int e; + tape_state_init (&tape_state, &tape_vars, TAPE_FILETYPE_BITS_TIBET, 1); /* alter menus */ + e = tibet_load_file (fn, decompress, &(tape_state.tibet)); + if (TAPE_E_OK != e) { + log_warn("tape: could not load TIBET file (code %d): '%s'", e, fn); + tape_handle_exception (&tape_state, + &tape_vars, + e, + tape_vars.testing_mode & TAPE_TEST_QUIT_ON_EOF, + tape_vars.testing_mode & TAPE_TEST_QUIT_ON_ERR, + 1); /* alter menus on failure */ + } else { + load_successful((char *)fn); + } +} + +static void csw_load (const char *fn) { + int e; + tape_state_init (&tape_state, &tape_vars, TAPE_FILETYPE_BITS_CSW, 1); /* alter menus */ + e = csw_load_file (fn, &(tape_state.csw)); + if (TAPE_E_OK != e) { + log_warn("tape: could not load CSW file (code %d): '%s'", e, fn); + tape_handle_exception(&tape_state, + &tape_vars, + e, + tape_vars.testing_mode & TAPE_TEST_QUIT_ON_EOF, + tape_vars.testing_mode & TAPE_TEST_QUIT_ON_ERR, + 1); /* alter menus on failure */ + } else { + load_successful((char *)fn); + } +} + +static void load_successful (char *path) { + + /* TOHv3: tapellatch removed */ + + tapelcount = 0; + + gui_alter_tape_eject (path); + gui_alter_tape_menus(tape_state.filetype_bits); + /*gui_set_record_mode(tape_vars.record_activated);*/ + +} + + +static int tibet_load_file (const char *fn, uint8_t decompress, tibet_t *t) { + + int e; + char *buf; + uint32_t len; + + len = 0; + buf = NULL; + + e = tape_load_file (fn, decompress, (uint8_t **) &buf, &len); + if (TAPE_E_OK != e) { return e; } + + e = tibet_decode (buf, len, t); + if (TAPE_E_OK != e) { + log_warn("tape: error (code %d) decoding TIBET file '%s'", e, fn); + free(buf); + return e; + } + + free(buf); + buf = NULL; + + return TAPE_E_OK; + +} + + +/* TOHv3 */ +void tape_write_u32 (uint8_t b[4], uint32_t v) { + b[0] = v & 0xff; + b[1] = (v >> 8) & 0xff; + b[2] = (v >> 16) & 0xff; + b[3] = (v >> 24) & 0xff; +} + +/* TOHv3 */ +void tape_write_u16 (uint8_t b[2], uint16_t v) { + b[0] = v & 0xff; + b[1] = (v >> 8) & 0xff; +} + +uint32_t tape_read_u32 (uint8_t *in) { + uint32_t u; + u = ( (uint32_t)in[0]) + | ((((uint32_t)in[1]) << 8) & 0xff00) + | ((((uint32_t)in[2]) << 16) & 0xff0000) + | ((((uint32_t)in[3]) << 24) & 0xff000000); + return u; +} + +uint32_t tape_read_u24 (uint8_t *in) { + uint32_t u; + u = ( (uint32_t)in[0]) + | ((((uint32_t)in[1]) << 8) & 0xff00) + | ((((uint32_t)in[2]) << 16) & 0xff0000); + return u; +} + +uint16_t tape_read_u16 (uint8_t *in) { + uint16_t u; + u = ( (uint16_t)in[0]) + | ((((uint16_t)in[1]) << 8) & 0xff00); + return u; +} + + + +int tape_zlib_compress (char *source_c, + size_t srclen, + uint8_t use_gzip_encoding, + char **dest, /* TOHv3.2: protect against NULL *dest */ + size_t *destlen) { + + int ret, flush; + z_stream strm; + size_t alloced=0; + uint8_t *source; + + /* allocate deflate state */ + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + + /* char -> uint8 */ + source = (uint8_t *) source_c; + + /* TOHv3.2: added refusal to compress empty buffer */ + if ((0 == srclen) || (srclen > 0x7fffffff)) { + log_warn("tape: write: compress: BUG: srclen is insane (%zu). Aborting.", srclen); + return TAPE_E_BUG; + } + + if (use_gzip_encoding) { + ret = deflateInit2 (&strm, + Z_BEST_COMPRESSION, /* Z_DEFAULT_COMPRESSION */ + Z_DEFLATED, + 15 + 16, /* windowbits 15, +16 for gzip encoding */ + 8, + Z_DEFAULT_STRATEGY); + } else { + ret = deflateInit (&strm, Z_BEST_COMPRESSION); + } + if (ret != Z_OK) { + return TAPE_E_SAVE_ZLIB_INIT; + } + + /* compress until end of file */ + strm.avail_in = 0x7fffffff & srclen; + flush = Z_FINISH; + + strm.next_in = source; + + /* run deflate() on input until output buffer not full, finish + compression if all of source has been read in */ + do { + + char *dest2; + + if (alloced <= *destlen) { /* realloc if no space left */ + dest2 = realloc(*dest, alloced = (*destlen ? (*destlen * 2) : srclen)); + if (NULL == dest2) { + log_warn("tape: write: compress: Failed to grow zlib buf (newsize %zu).", *destlen); + deflateEnd(&strm); + /* TOHv3.2: avoid free(NULL) */ + if (*dest != NULL) { + free(*dest); + } + *dest = NULL; + return TAPE_E_MALLOC; + } + *dest = dest2; + } + strm.avail_out = 0x7fffffff & (alloced - *destlen); /* bytes available in output buffer */ + strm.next_out = (uint8_t *) (*dest + *destlen); /* current offset in output buffer */ + ret = deflate(&strm, flush); /* no bad return value */ + *destlen += (alloced - *destlen) - strm.avail_out; + + } while (strm.avail_out == 0); + + /* clean up */ + deflateEnd(&strm); + + if ( ( strm.avail_in != 0 ) || ( ret != Z_STREAM_END ) ) { /* all input will be used */ + if (strm.avail_in != 0) { + log_warn("tape: write: compress: zlib compression failed: strm.avail_in != 0."); + } else { + log_warn("tape: write: compress: zlib compression failed (code %u).", ret); + } + free(*dest); + *dest = NULL; + return TAPE_E_SAVE_ZLIB_COMPRESS; + } + + return TAPE_E_OK; + +} + + + + +/* TOHv3 */ +void tape_init (tape_state_t *ts, tape_vars_t *tv) { + /*tape_state_init (ts, tv, 0, 0);*/ /* TOHv4.2 */ + tape_set_record_activated(ts, tv, NULL, 0); /* TOHv3.2, v4 */ + tv->record_activated = 0; + tapelcount = 0; + tapeledcount = 0; + tv->overclock = false; + tv->save_filename = NULL; + tv->load_filename = NULL; + tv->strip_silence_and_leader = 0; +} + +int tape_save_on_shutdown (tape_state_t *ts, /* TOHv4.2: now passed in */ + uint8_t record_is_pressed, + uint8_t *our_filetype_bits_inout, /* the MULTIPLE types we have available */ + uint8_t silence112, + uint8_t wav_use_phase_shift, + tape_shutdown_save_t *c) { /* c->filetype_bits is the SINGLE type requested by the user */ + + int e; + + if (NULL == c->filename) { return TAPE_E_OK; } + + e = wp1_4k8_init_blank_tape_if_none_loaded (our_filetype_bits_inout); /*, record_is_pressed);*/ + if (TAPE_E_OK != e) { return e; } + + /* TOHv4-rc2: add an origin chunk if there isn't one */ + if ((c->filetype_bits & TAPE_FILETYPE_BITS_UEF) && (0 == ts->uef.num_chunks) && record_is_pressed) { + e = uef_store_chunk(&(ts->uef), + (uint8_t *) VERSION_STR, + 0, /* origin, chunk &0000 */ + strlen(VERSION_STR)+1, /* include the \0 */ + 0); /* TOHv4.2: dummy offset */ + if (TAPE_E_OK != e) { return e; } + } + +//printf("*our_filetype_bits_inout = %x, c->filetype_bits = %x\n", +// *our_filetype_bits_inout, c->filetype_bits); + + if ( ((TAPE_FILETYPE_BITS_WAV==c->filetype_bits) && (*our_filetype_bits_inout != TAPE_FILETYPE_BITS_NONE)) + || TAPE_FILETYPE_BITS_NONE != (*our_filetype_bits_inout & c->filetype_bits)) { + if (c->filetype_bits & TAPE_FILETYPE_BITS_WAV) { /* TOHv4.2 */ + e = tapenoise_write_wav(ts, c->filename, wav_use_phase_shift); + } else { + e = tape_generate_and_save_output_file (ts, /* TOHv4.2: now passed in */ + c->filetype_bits, + silence112, + c->filename, + c->do_compress, + NULL); + } + if (TAPE_E_OK == e) { + log_info("tape: -tapesave: saved file: %s", c->filename); + } else { + log_warn("tape: -tapesave: error saving file: %s", c->filename); + } + } else { + log_warn ("tape: -tapesave error: data does not exist in desired format (have &%x, want &%x)", + *our_filetype_bits_inout, c->filetype_bits); + } + return TAPE_E_OK; +} + + +/* TOHv3.2: added explicit tape_start_motor(), tape_stop_motor(), tape_is_motor_running() + * made motor variable private to tape.c/.h + * TOHv3.3: moved some stuff here from serial.c */ +void tape_start_motor (tape_state_t *ts, uint8_t also_force_dcd_high) { + if (ts->ula_motor) { return; } + ts->ula_motor = 1; + tapenoise_motorchange(1); + led_update(LED_CASSETTE_MOTOR, 1, 0); + if (also_force_dcd_high) { + acia_dcdhigh(&sysacia); + } + /* TOHv4.2: reset this to ensure another half a second + of unskipped silence/leader plays out when the motor + starts up. */ + ts->strip_silence_and_leader_holdoff_1200ths=0; +} + +void tape_stop_motor(tape_state_t *ts) { + if ( ! ts->ula_motor ) { return; } + ts->ula_motor = 0; + tapeledcount = 2; /* FIXME: ???? (formerly from serial.c) */ + tapenoise_motorchange(0); + /* TOHv4.2: if motor is off, DCD counter is reset. See + * https://www.stardot.org.uk/forums/viewtopic.php?p=456479#p456479 + * and + * https://www.stardot.org.uk/forums/viewtopic.php?p=457271#p457271 + */ + ts->ula_dcd_blipticks = 0; +} + + + +int tape_rs423_eat_1200th (tape_state_t *ts, + tape_vars_t *tv, + ACIA *acia, + uint8_t emit_tapenoise, + uint8_t *throw_eof_out) { + + int e; + char tone; + + /* consume tape 1200th */ + tone = 'S'; + + /* continue to advance through the tape even if RS423 selected */ + e = tape_tone_1200th_from_back_end (0, ts, 0, 0, &tone); + if (TAPE_E_EOF == e) { + *throw_eof_out = 1; + e = TAPE_E_OK; + } + if (TAPE_E_OK != e) { return e; } + + /* TOHv4.1-rc9: bugfix: tape noise wasn't being sent in RS423+MOTOR1 case. + * This caused problems with Ultron, because the tapenoise was being + * played back out of the ringbuffer but it wasn't being replenished during + * block "motor off" breaks, so eventually you would get dropouts. */ + tapenoise_send_1200 (tone, &(ts->tapenoise_no_emsgs)); + + /* we also need to send s/1200 of silence to the TX back end */ + e = tape_write_bitclk (ts, + acia, + 'S', + TAPE_1200TH_IN_NS_INT, + 1, /* silent */ + emit_tapenoise && tv->record_activated, + tv->record_activated, + tv->save_always_117, + tv->save_prefer_112, + tv->save_do_not_generate_origin_on_append); + if (TAPE_E_OK != e) { return e; } + + if ('\0' != tone) { ts->ula_prevailing_rx_bit_value = tone; } /* for DCD */ + + return e; + +} + + +int tape_fire_acia_rxc (tape_state_t *ts, + tape_vars_t *tv, + ACIA *acia, + int32_t acia_rx_divider, + uint8_t emit_tapenoise, + uint8_t *throw_eof_out) { + + int64_t ns_per_bit; + uint8_t bit_ready; + char bit_in; + int e, m; + uint8_t strip; /* TOHv4.2 */ + + e = TAPE_E_OK; + + /* TOHv4-rc6: now gated by ula_motor; Atic-Atac loader + BREAK tapenoise bug */ + /*if ( ! ts->ula_motor ) { return TAPE_E_OK; } */ + /* TOHv4.1: this gate is instead applied to serial_rxc_clock_for_tape call in 6502.c */ + + /* both dividers have fired, so the ACIA now NEEDS A BIT from the appropriate tape back end; + however, the back ends emit 1200ths of TONE, not bits, so we need to convert + for 300 baud, and possibly also for 19200 baud if we want to try to support + whatever happens if you try to feed 1200/2400 tone to ~19.2 KHz decoding; + (I really need to test this on hardware) */ + + /* 1200 baud is 832016ns; if double-divided ACIA clock tick is faster than this, then + behaviour is "undefined" (specifically, for 19.2 KHz); nevertheless, the actual + hardware must do *something*; ideally, an emulator would duplicate the hardware's + exact sequence of e.g. framing errors and partial bytes; also I am wondering if + it is possible to craft a custom stream that decodes successfully at 19.2 KHz */ + +#define EIGHT_THREE_TWO (13 * 1000 * 64) + ns_per_bit = (EIGHT_THREE_TWO * acia_rx_divider) / 16; + bit_in = '1'; + bit_ready = 0; + + /* TOHv4.2: Don't activate "strip silence and leader" mode until the motor + has been running for ~0.6s. With overclock + strip modes + enabled, MOS can easily miss the first block under common circumstances + if we don't do this. */ +#define HOLDOFF_1200THS 800 + strip = (ts->strip_silence_and_leader_holdoff_1200ths >= HOLDOFF_1200THS) ? tv->strip_silence_and_leader : 0; + +strip = tv->strip_silence_and_leader; + + /* Increment strip_silence_and_leader_holdoff by number of 1200ths. */ + if (ts->strip_silence_and_leader_holdoff_1200ths < HOLDOFF_1200THS) { + (ts->strip_silence_and_leader_holdoff_1200ths) += (ns_per_bit / EIGHT_THREE_TWO); + } + + if (832000 == ns_per_bit) { + + /* 1201.92 baud */ + e = tape_tone_1200th_from_back_end (strip, + ts, + acia_rx_awaiting_start(acia), /* from shift reg state */ + ! tv->disable_phantom_block_protection, + &bit_in); + if (TAPE_E_EOF == e) { + *throw_eof_out = 1; + e = TAPE_E_OK; + } +/* +putchar(bit_in); +fflush(stdout); +*/ + if ( TAPE_E_OK != e ) { return e; } + /* TOHv4-rc6: now gated by ula_motor; tackle Atic-Atac loader tapenoise after BREAK bug */ + if ( ( ! tv->record_activated ) && emit_tapenoise && ts->ula_motor) { + tapenoise_send_1200 (bit_in, &(ts->tapenoise_no_emsgs)); + } + bit_ready = 1; + ts->tones300_fill = 0; + ts->ula_prevailing_rx_bit_value = bit_in; + + } else if (3328000 == ns_per_bit) { + + /* 300.48 baud, so we must consume four 1200ths; + note that we always do this regardless of whether + it makes a valid bit or not ... */ + + for (m=0; m < TAPE_TONES300_BUF_LEN; m++) { + + char tone, a, b; + + e = tape_tone_1200th_from_back_end (tv->strip_silence_and_leader, + ts, + acia_rx_awaiting_start(acia), // from shift reg state + ! tv->disable_phantom_block_protection, + &tone); + if (TAPE_E_EOF == e) { + *throw_eof_out = 1; + e = TAPE_E_OK; + } + if (TAPE_E_OK != e) { return e; } + + ts->ula_prevailing_rx_bit_value = tone; + + ts->tones300[ts->tones300_fill] = tone; + + b = ('L' == tone) ? '1' : tone; + a = ('L' == ts->tones300[0]) ? '1' : ts->tones300[0]; + + /* does the new 1200th match tones[0] ? */ + if (a != b) { + log_info("tape: warning: fuzzy 300-baud bit: [ %c %c %c %c ]; resynchronising", + ts->tones300[0], + (ts->tones300[1]!=0)?ts->tones300[1]:' ', + (ts->tones300[2]!=0)?ts->tones300[2]:' ', + (ts->tones300[3]!=0)?ts->tones300[3]:' '); + /* shift down and resync */ + ts->tones300[0] = tone; + ts->tones300[1] = 0; + ts->tones300[2] = 0; + ts->tones300[3] = 0; + ts->tones300_fill = 1; + } else if ( (TAPE_TONES300_BUF_LEN - 1) == ts->tones300_fill ) { + /* bit ready */ + bit_ready = 1; + bit_in = ts->tones300[0]; + ts->tones300_fill = 0; + if ( ( ! tv->record_activated ) && emit_tapenoise ) { + tapenoise_send_1200 (bit_in, &(ts->tapenoise_no_emsgs)); + tapenoise_send_1200 (bit_in, &(ts->tapenoise_no_emsgs)); + tapenoise_send_1200 (bit_in, &(ts->tapenoise_no_emsgs)); + tapenoise_send_1200 (bit_in, &(ts->tapenoise_no_emsgs)); + } + } else { + (ts->tones300_fill)++; + } + + } + + } else if (52000 == ns_per_bit) { + /* 19230.77 baud */ + /* log_warn("tape: warning: RX @ 19231 baud (I dispute that this is unusable; assume high tone for now)"); */ + ts->tones300_fill = 0; + } else { + log_warn("tape: BUG: RX: bad ns_per_bit (%"PRId64")", ns_per_bit); + e = TAPE_E_BUG; + } + + if (TAPE_E_OK != e) { return e; } + + if (bit_ready) { + e = acia_receive_bit_code (acia, bit_in); + } + + return e; + +} + diff -Nu b-em-40246d4-vanilla/src/tapecat-allegro.c b-em-40246d4-TOHv4.2/src/tapecat-allegro.c --- b-em-40246d4-vanilla/src/tapecat-allegro.c 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/tapecat-allegro.c 2025-07-06 14:17:40.697657460 +0100 @@ -3,6 +3,7 @@ #include "tapecat-allegro.h" #include "csw.h" #include "uef.h" +#include "tape.h" static ALLEGRO_TEXTLOG *textlog; ALLEGRO_EVENT_SOURCE uevsrc; @@ -22,36 +23,35 @@ al_init_user_event_source(&uevsrc); al_register_event_source(queue, &uevsrc); al_register_event_source(queue, al_get_native_text_log_event_source(ltxtlog)); - do + do { al_wait_for_event(queue, &event); - while (event.type != ALLEGRO_EVENT_NATIVE_DIALOG_CLOSE); + } while (event.type != ALLEGRO_EVENT_NATIVE_DIALOG_CLOSE); textlog = NULL; // set the global that cataddname will see to NULL - al_close_native_text_log(ltxtlog); // before destorying the textlog with our local copy. + al_close_native_text_log(ltxtlog); // before destroying the textlog with our local copy. al_destroy_event_queue(queue); } return NULL; } -static void start_cat(void) +static void start_cat(uint8_t enable_phantom_block_protection) /* TOHv3.3: parameter */ { - if (csw_ena) - csw_findfilenames(); - else - uef_findfilenames(); + findfilenames_new(&tape_state, + 1, /* TOHv3: "1" here means show UI */ + enable_phantom_block_protection); } void gui_tapecat_start(void) { - if (textlog) - start_cat(); - else { + if (textlog) { + start_cat( ! tape_vars.disable_phantom_block_protection ); + } else { ALLEGRO_TEXTLOG *ltxtlog = al_open_native_text_log("B-Em Tape Catalogue", ALLEGRO_TEXTLOG_MONOSPACE); if (ltxtlog) { ALLEGRO_THREAD *thread = al_create_thread(tapecat_thread, ltxtlog); if (thread) { al_start_thread(thread); textlog = ltxtlog; // open to writes from cataddname. - start_cat(); + start_cat( ! tape_vars.disable_phantom_block_protection ); return; } al_close_native_text_log(ltxtlog); diff -Nu b-em-40246d4-vanilla/src/tape.h b-em-40246d4-TOHv4.2/src/tape.h --- b-em-40246d4-vanilla/src/tape.h 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/tape.h 2025-07-06 14:17:40.697657460 +0100 @@ -1,17 +1,350 @@ #ifndef __INC_TAPE_H #define __INC_TAPE_H +#include + +#include "b-em.h" +#include "tibet.h" +#include "csw.h" +#include "uef.h" + +/* TOHv3: switch to bitfield for filetypes */ +#define TAPE_FILETYPE_BITS_NONE 0 +#define TAPE_FILETYPE_BITS_UEF 1 +#define TAPE_FILETYPE_BITS_TIBET 2 +#define TAPE_FILETYPE_BITS_CSW 4 +#define TAPE_FILETYPE_BITS_WAV 8 /* TOHv4.2: add WAV */ +#define TAPE_FILETYPES_ALL (1|2|4|8) + +/* for -tapetest CLI switch */ +#define TAPE_TEST_QUIT_ON_EOF 1 +#define TAPE_TEST_QUIT_ON_ERR 2 +#define TAPE_TEST_CAT_ON_STARTUP 4 +#define TAPE_TEST_CAT_ON_SHUTDOWN 8 /* TOHv4 */ +#define TAPE_TEST_SLOW_STARTUP 16 /* TOHv4.1 */ +#define TAPE_TEST_BAD_MASK 0xffffffe0 + +#ifdef _WIN32 +#define TAPE_SNPRINTF(BUF,BUFSIZE,MAXCHARS,FMTSTG,...) _snprintf_s((BUF),(BUFSIZE),(MAXCHARS),(FMTSTG),__VA_ARGS__) +#define TAPE_SCPRINTF(FMTSTG,...) _scprintf((FMTSTG),__VA_ARGS__) +#define TAPE_SSCANF sscanf_s +#define TAPE_STRNCPY(DEST,BUFSIZE,SRC,LEN) _mbsncpy_s((DEST),(BUFSIZE),(SRC),(LEN)) +#else +#define TAPE_SNPRINTF(BUF,BUFSIZE,MAXCHARS,FMTSTG...) snprintf((BUF),(BUFSIZE),FMTSTG) +#define TAPE_SCPRINTF(FMTSTG...) snprintf(&tape_dummy_for_unix_scprintf, 0, FMTSTG) +#define TAPE_SSCANF sscanf +#define TAPE_STRNCPY(DEST,BUFSIZE,SRC,LEN) strncpy((DEST),(SRC),(LEN)) +#endif /* end ndef _WIN32 */ + +/* tape is loaded if only *one* of TIBET, CSW or UEF is selected. + All three means a tape-from-scratch; none at all means no tape at all. */ +/* TOHv4: now a macro */ +#define TAPE_IS_LOADED(filetype_bits) \ + ((0 != (filetype_bits)) && (TAPE_FILETYPES_ALL != (filetype_bits))) + +#define TAPE_TONECODE_IS_LEGAL(c) (('0'==(c))||('1'==(c))||('S'==(c))||('L'==(c))) + +typedef struct tape_shutdown_save_s { + /* for -tapesave command-line option */ + char *filename; /* this memory belongs to argv[] */ + uint8_t filetype_bits; + uint8_t do_compress; +} tape_shutdown_save_t; + + + +#include "tibet.h" + +typedef struct tape_state_s { + + /* STATE VARIABLES */ + + /* TOHv3: filetype is now a bitfield */ + uint8_t filetype_bits; + + /* format-specific state: */ + tibet_t tibet; + uef_state_t uef; + csw_state_t csw; + + /* TIBET data spans are built here, + before being sent to the TIBET interface + to be included in the spans list: */ + tibet_span_t tibet_data_pending; + + // vars for local byte decoding: + uint32_t num_1200ths_since_silence; + uint32_t start_bit_wait_count_1200ths; + uint8_t bits_count; + + uint8_t tapenoise_no_emsgs; /* prevent "ring buffer full" spam */ + uint8_t tape_finished_no_emsgs; /* overhaul v2: prevent "tape finished" spam */ + + /* WRITING (w_...) : */ + uint8_t w_must_end_data; /* data pending, e.g. on w_uef_tmpchunk */ + + /* used to accumulate bit periods, until we have some 1200ths to send; + bit periods will be converted into tones (1200ths) */ + int64_t w_accumulate_bit_periods_ns; + + /* meanwhile this is used to accumulate all the silence and leader + in the current span (note that w_accumulate_bit_periods_ns + is included in these values) */ + int64_t w_accumulate_silence_ns; + int64_t w_accumulate_leader_ns; + + uef_chunk_t w_uef_tmpchunk; + uint8_t w_uef_origin_written; /* TOHv3.2 */ + + /* we scan backwards for &117s and update this value according + * to the first chunk &117 we find, whenever + * i) a new UEF is loaded; or + * ii) Record Mode is activated. + * This allows the save code to know whether it needs to + * insert &117 to change the prevailing baud or not. + */ + int32_t w_uef_prevailing_baud; /* TOHv4 */ + + /* for decoding the ACIA's serial stream, to convert back into bytes, + for writing UEF */ + uint8_t w_uef_serial_frame; + uint8_t w_uef_serial_phase; + + uint16_t w_uef_enc100_shift_value; + uint8_t w_uef_enc100_shift_amount; /* max 16 */ + + /* needed so we know whether we're appending UEF (and therefore + whether we need to grey out "don't emit origin" in the UEF + save options submenu) */ + uint8_t w_uef_was_loaded; /* TOHv3.2 */ + + uint8_t disabled_due_to_error; + + uint8_t ula_motor; /* TOHv3.3: moved here from tape_vars_t above */ + int32_t ula_rx_ns; /* TOHv4 */ + int32_t ula_tx_ns; /* TOHv4 */ + uint8_t ula_ctrl_reg; /* TOHv4 (moved from serial.c) */ + + /* whether or not this value makes it to the ACIA + depends on whether we're in tape or RS423 mode: */ + uint8_t ula_dcd_tape; /* TOHv3.3 */ + + int32_t ula_dcd_2mhz_counter; /* TOHv4 */ + int32_t ula_dcd_blipticks; /* TOHv4 */ + int32_t ula_rs423_taperoll_2mhz_counter; /* TOHv4 */ + + int32_t tape_noise_counter; /* TOHv4 */ + +#define TAPE_TONES300_BUF_LEN 4 + + /* for decoding 300 baud from its component 1200ths */ + char tones300[TAPE_TONES300_BUF_LEN]; + int8_t tones300_fill; + + /* In the interests of efficiency, the DCD part of the + tape interface is clocked at a different rate to the + RX part. This means that we cannot necessarily feed + a "fresh" bit from the tape into the DCD state machine. + So, we have to persist the most recently obtained bit + value, and supply that to the DCD logic. */ + char ula_prevailing_rx_bit_value; + + /* TOHv4: pre-computed when control register is written; + see serial_recompute_dividers_and_thresholds() */ + int32_t ula_rx_thresh_ns; + int32_t ula_tx_thresh_ns; + int32_t ula_rx_divider; + int32_t ula_tx_divider; + + /* TOHv4: hack for serial-to-file mode. + The correct architecture here would be a (linked) list + of pointers to functions that should be called + one after the other to sink a byte of TX serial data. + For now we just have this flag */ + uint8_t ula_have_serial_sink; + + /* TOHv4.2: + Don't engage "strip mode" until half a second of tape + has played since the last MOTOR ON. MOS can miss + the start of a block under common conditions + if overclock + strip are both enabled. This should + mitigate that. */ + uint32_t strip_silence_and_leader_holdoff_1200ths; + + uint32_t leader_skip_1200ths; + +} tape_state_t; + +/* +typedef struct tape_examine_s { + ALLEGRO_DISPLAY *win; + ALLEGRO_THREAD *thread; + tape_state_t clone; +} tape_examine_t; +*/ + +/* TOHv3.2: introduced this struct + * + * Gets rid of the messy tape global variables. + * + * You should be able to swap to a different tape without + * needing to touch any of the data on this struct. If you + * are adding a variable that will need altering or clearing + * when the loaded tape is changed or ejected, then it + * belongs on tape_state_t rather than tape_vars_t. This is + * for configuration stuff. + */ +typedef struct tape_vars_s { + + /* SETTINGS VARIABLES */ + + /* TOHv4: mechanism for -expiry option: */ + uint8_t expired_quit; + int64_t testing_expire_ticks; /* measured in "otherstuff" ticks */ + + uint8_t overclock; /* a.k.a. fasttape */ + + uint8_t strip_silence_and_leader; /* TOHv3.2: another speed hack */ + + uint8_t record_activated; /* TOHv3 */ + + ALLEGRO_PATH *save_filename; /* TOHv3 */ + ALLEGRO_PATH *load_filename; + + /* TOHv3: TAPE_TEST_xxx bitfields: */ + uint8_t testing_mode; + + /* TOHv3.2 */ + uint8_t save_always_117; + uint8_t save_prefer_112; + + /* TOHv3.2, for knowing when to emit &117 */ + uint8_t cur_baud300; + + tape_shutdown_save_t quitsave_config; /* TOHv3 */ + + uint8_t save_do_not_generate_origin_on_append; /* TOHv3.2 */ + + uint8_t disable_phantom_block_protection; + uint8_t disable_debug_memview; + + uint8_t wav_use_phase_shift; /* TOHv4.2 */ + + /* TOHv4.2: */ + /*tape_examine_t examine; */ + +} tape_vars_t; + + +/* TOHv3.2 */ +extern tape_vars_t tape_vars; +extern tape_state_t tape_state; + +/* TODO: ? should these be moved onto tape_vars too? */ +extern int /*tapelcount,*/ tapeledcount; + +extern char tape_dummy_for_unix_scprintf; + +uint32_t tape_read_u32 (uint8_t *in); +uint16_t tape_read_u16 (uint8_t *in); +uint32_t tape_read_u24 (uint8_t *in); +void tape_write_u32 (uint8_t b[4], uint32_t v); /* TOHv3 */ +void tape_write_u16 (uint8_t b[2], uint16_t v); /* TOHv3 */ + #include "acia.h" -extern ALLEGRO_PATH *tape_fn; -extern bool tape_loaded; +int tape_rewind_2 (tape_state_t *ser); +void tape_load (ALLEGRO_PATH *fn); +void tape_close (void); +void tape_receive (ACIA *acia, uint8_t data); +int findfilenames_new (tape_state_t *ts, + uint8_t show_ui_window, + uint8_t enable_phantom_block_protection); /* TOHv3: P.B.P. */ +void tape_state_init (tape_state_t *t, + tape_vars_t *tv, + uint8_t filetype_bits, + uint8_t alter_menus); +void tape_state_finish (tape_state_t *t, uint8_t alter_menus); +int tape_state_clone_and_rewind (tape_state_t *out, tape_state_t *in); +int tape_load_file (const char *fn, + uint8_t decompress, + uint8_t **buf_out, + uint32_t *len_out); +int tape_decompress (uint8_t **out, uint32_t *len_out, uint8_t *in, uint32_t len_in); +int tape_generate_and_save_output_file (tape_state_t *ts, /* TOHv4.2: pass this in */ + uint8_t filetype_bits, + uint8_t silence112, + char *path, + uint8_t compress, + ACIA *acia_or_null); +int tape_flush_pending_piece (tape_state_t *ts, ACIA *acia_or_null, uint8_t silence112); +int tape_zlib_compress (char *source_c, + size_t srclen, + uint8_t use_gzip_encoding, + char **dest, + size_t *destlen); +int tape_handle_acia_master_reset (tape_state_t *t); +void tape_init (tape_state_t *t, tape_vars_t *tv); +uint8_t tape_peek_for_data (tape_state_t *ts); +int tape_save_on_shutdown (tape_state_t *ts, /* TOHv4.2: now passed in */ + uint8_t record_is_pressed, + uint8_t *our_filetype_bits_inout, /* the MULTIPLE types we have available */ + uint8_t silence112, + uint8_t wav_use_phase_shift, + tape_shutdown_save_t *c); +void tape_stop_motor (tape_state_t *ts); +void tape_start_motor (tape_state_t *ts, uint8_t also_force_dcd_high); +int tape_set_record_activated (tape_state_t *t, tape_vars_t *tv, ACIA *acia_or_null, uint8_t value); +uint8_t tape_is_record_activated (tape_vars_t *tv); + +#define TAPE_1200TH_IN_1MHZ_INT (64 * 13) /* the famous 832us */ +#define TAPE_1200TH_IN_S_FLT (((double)TAPE_1200TH_IN_1MHZ_INT) / 1000000.0) +#define TAPE_1200TH_IN_NS_INT (1000 * TAPE_1200TH_IN_1MHZ_INT) +#define TAPE_1200TH_IN_2MHZ_INT (2 * TAPE_1200TH_IN_1MHZ_INT) + +#define TAPE_1200_HZ (1.0 / TAPE_1200TH_IN_S_FLT) /* 1201.9 Hz */ +#define TAPE_FLOAT_ZERO 0.00001 + +/* exported in TOHv4 */ +void tape_handle_exception (tape_state_t *ts, + tape_vars_t *tv, /* TOHv3.2: may be NULL */ + int error_code, + uint8_t eof_fatal, + uint8_t err_fatal, + uint8_t alter_menus); + +/* TOHv4 */ +int tape_write_bitclk (tape_state_t *ts, + ACIA *acia, + char bit, + int64_t ns_per_bit, /* 1024 = 1200 baud; 4096 = 300; 8192 = 150 etc. */ + uint8_t silent, + uint8_t tapenoise_write_enabled, + uint8_t record_is_pressed, + uint8_t always_117, + uint8_t silence112, + uint8_t no_origin_chunk_on_append); + +/* TOHv4 */ +int tape_rs423_eat_1200th (tape_state_t *ts, + tape_vars_t *tv, + ACIA *acia, + uint8_t emit_tapenoise, + uint8_t *throw_eof_out); -void tape_load(ALLEGRO_PATH *fn); -void tape_close(void); -void tape_poll(void); -void tape_receive(ACIA *acia, uint8_t data); +/* TOHv4 */ +int tape_fire_acia_rxc (tape_state_t *ts, + tape_vars_t *tv, + ACIA *acia, + int32_t acia_rx_divider, + uint8_t emit_tapenoise, + uint8_t *throw_eof_out); -extern int tapelcount,tapellatch,tapeledcount; -extern bool fasttape; +/* TOHv4.2: exported, for use by tapenoise_write_wav() in tapenoise.c */ +int tape_tone_1200th_from_back_end (uint8_t strip_silence_and_leader, /* speedup */ + tape_state_t *ts, + uint8_t awaiting_start_bit, + uint8_t enable_phantom_block_protection, // TOHv3.3 + char *tone_1200th_out); #endif diff -Nu b-em-40246d4-vanilla/src/tapenoise.c b-em-40246d4-TOHv4.2/src/tapenoise.c --- b-em-40246d4-vanilla/src/tapenoise.c 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/tapenoise.c 2025-07-06 14:17:40.697657460 +0100 @@ -1,5 +1,7 @@ /*B-em v2.2 by Tom Walker Tape noise (not very good)*/ + +/* - TOHv3 overhaul by 'Diminished' */ #include "b-em.h" #include @@ -7,36 +9,141 @@ #include "tapenoise.h" #include "sound.h" +#define TAPENOISE_VOLUME (0.20) +#define WAV_VOLUME (0.75) + +/* This is essential if you want it to feel like a real Beeb, + but it might cause dropouts; YMMV */ +#define LOW_LATENCY + +/* remember, BUFLEN_TN is the Allegro output buffer length; + AUDIO_RINGBUF_LEN is the internal ring buffer length. */ +#ifdef LOW_LATENCY +#define BUFLEN_TN 1280 /*1024*/ /*4096*/ /*(768)*/ +#define FREQ_TN (FREQ_DD) +#define AUDIO_RINGBUF_LEN ((FREQ_TN * 50) / 100) /*(((FREQ_TN * 15) / 100)*/ +#else +/* conservative values, large buffers */ +#define BUFLEN_TN 4096 /*(BUFLEN_DD)*/ +#define FREQ_TN (FREQ_DD) +#define AUDIO_RINGBUF_LEN ((FREQ_TN * 50) / 100) +#endif + static ALLEGRO_VOICE *voice; static ALLEGRO_MIXER *mixer; static ALLEGRO_AUDIO_STREAM *stream; -static int tpnoisep = 0; -static int tmcount = 0; -static int16_t tapenoise[BUFLEN_DD]; - -static float swavepos = 0; - -static int sinewave[32]; +#define PI 3.14159 +/* Hmm. We might need to allocate this on the heap; + it might be about 26K ? */ +static int16_t audio_ringbuf[AUDIO_RINGBUF_LEN]; + +static uint32_t audio_ringbuf_start = 0; +static uint32_t audio_ringbuf_fill = 0; + +/* New plan: have two precomputed sine waves, one slightly + too fast, one slightly too slow; choose one depending on + whether our audio ring buffer is getting too full or + too empty. + + pick 1208 Hz, because it places us nicely between + two sine wave lengths (at e.g. 44100 Hz output sampling rate): + (44100 / 1208) = 36.5 samples per cycle, which gives us + 36 smps/cyc for a "fast" sine wave (1225 Hz), and 37 smps/cyc for + a "slow" sine wave (1192 Hz). + + We only do this during data sections; during leader we just throw + away cycles if we're too fast. +*/ + +#define TAPENOISE_BASEFREQ 1208 + +#define FAST_SMPS (FREQ_TN / TAPENOISE_BASEFREQ) +#define SLOW_SMPS (FAST_SMPS + 1) + +/* each of these represents one or two cycles at the output sample rate, + so we can just copy it to the output buffer when Allegro wants audio. */ +static int16_t sinewave_2400_slow[SLOW_SMPS]; /* a pair of 2400 cycles */ +static int16_t sinewave_2400_fast[FAST_SMPS]; +static int16_t sinewave_1200_slow[SLOW_SMPS]; /* one single 1200 cycle */ +static int16_t sinewave_1200_fast[FAST_SMPS]; + +/* TOHv4.2: versions for saving WAVs: + (a) louder + (b) provide phase shift option + (c) "fast" version not needed */ +static int16_t wav_sin_2400[SLOW_SMPS]; /* a pair of 2400 cycles */ +static int16_t wav_sin_1200[SLOW_SMPS]; /* one single 1200 cycle */ +static int16_t wav_cos_2400[SLOW_SMPS]; /* a pair of 2400 cycles */ +static int16_t wav_cos_1200[SLOW_SMPS]; /* one single 1200 cycle */ + +static void init_sine_waves(void); +static void ringbuf_play (int16_t *buf); +static void ringbuf_append (int16_t *src, uint32_t len, uint8_t *no_emsgs_inout); + +static void init_sine_waves(void) { + uint32_t n; + double d; + double radians_per_sample; + radians_per_sample = (4.0 * PI) / (double) SLOW_SMPS; + for (n=0, d=0.0; n < SLOW_SMPS; n++) { + double sin_d; + sin_d = sin(d); + sinewave_2400_slow[n] = (int) round(sin_d * TAPENOISE_VOLUME * 32767.0); + wav_sin_2400[n] = (int) round(sin_d * WAV_VOLUME * 32767.0); + wav_cos_2400[n] = (int) round(cos(d) * WAV_VOLUME * 32767.0); + d += radians_per_sample; + } + radians_per_sample = (4.0 * PI) / (double) FAST_SMPS; + for (n=0, d=0.0; n < FAST_SMPS; n++) { + sinewave_2400_fast[n] = (int) round(sin(d) * TAPENOISE_VOLUME * 32767.0); + d += radians_per_sample; + } + radians_per_sample = (2.0 * PI) / (double) SLOW_SMPS; + for (n=0, d=0.0; n < SLOW_SMPS; n++) { + double sin_d; + sin_d = sin(d); + sinewave_1200_slow[n] = (int) round(sin_d * TAPENOISE_VOLUME * 32767.0); + wav_sin_1200[n] = (int) round(sin_d * WAV_VOLUME * 32767.0); + wav_cos_1200[n] = (int) round(cos(d) * WAV_VOLUME * 32767.0); + d += radians_per_sample; + } + radians_per_sample = (2.0 * PI) / (double) FAST_SMPS; + for (n=0, d=0.0; n < FAST_SMPS; n++) { + sinewave_1200_fast[n] = (int) round(sin(d) * TAPENOISE_VOLUME * 32767.0); + d += radians_per_sample; + } +} -#define PI 3.142 +/* TOHv4.1-rc9: when tapenoise is enabled, start in a sane state */ +void tapenoise_activated_hook(void) { + audio_ringbuf_fill = (AUDIO_RINGBUF_LEN / 2); /* ringbuf half-full */ + memset(audio_ringbuf, 0, sizeof(int16_t) * AUDIO_RINGBUF_LEN); /* of silence */ +} static ALLEGRO_SAMPLE *tsamples[2]; void tapenoise_init(ALLEGRO_EVENT_QUEUE *queue) { log_debug("tapenoise: tapenoise_init"); - if ((voice = al_create_voice(FREQ_DD, ALLEGRO_AUDIO_DEPTH_INT16, ALLEGRO_CHANNEL_CONF_1))) { - if ((mixer = al_create_mixer(FREQ_DD, ALLEGRO_AUDIO_DEPTH_INT16, ALLEGRO_CHANNEL_CONF_1))) { + if ((voice = al_create_voice(FREQ_TN, + ALLEGRO_AUDIO_DEPTH_INT16, + ALLEGRO_CHANNEL_CONF_1))) { + if ((mixer = al_create_mixer(FREQ_TN, + ALLEGRO_AUDIO_DEPTH_INT16, + ALLEGRO_CHANNEL_CONF_1))) { if (al_attach_mixer_to_voice(mixer, voice)) { - if ((stream = al_create_audio_stream(4, BUFLEN_DD, FREQ_DD, ALLEGRO_AUDIO_DEPTH_INT16, ALLEGRO_CHANNEL_CONF_1))) { + if ((stream = al_create_audio_stream(4, + BUFLEN_TN, + FREQ_TN, + ALLEGRO_AUDIO_DEPTH_INT16, + ALLEGRO_CHANNEL_CONF_1))) { if (al_attach_audio_stream_to_mixer(stream, mixer)) { ALLEGRO_PATH *dir = al_create_path_for_directory("ddnoise"); tsamples[0] = find_load_wav(dir, "motoron"); tsamples[1] = find_load_wav(dir, "motoroff"); al_destroy_path(dir); - for (int c = 0; c < 32; c++) - sinewave[c] = (int)(sin((float)c * ((2.0 * PI) / 32.0)) * 128.0); + init_sine_waves(); } else log_error("sound: unable to attach stream to mixer for tape noise"); } else @@ -46,7 +153,7 @@ } else log_error("sound: unable to create mixer for tape noise"); } else - log_error("sound: unable to create voice for for tape noise"); + log_error("sound: unable to create voice for tape noise"); } void tapenoise_close() @@ -66,95 +173,312 @@ al_destroy_voice(voice); } -static void send_buffer(void) -{ - int16_t *tapebuffer; - int c; +static uint8_t currently_fast = 0; - tpnoisep = 0; - if ((tapebuffer = al_get_audio_stream_fragment(stream))) { +static uint8_t is_ringbuf_filling (uint8_t now_fast, + uint8_t hysteresis, + uint32_t fill) { + if (hysteresis) { + /* "gentle" speed compensation */ + if ( ( ! now_fast ) && (fill >= ((3 * AUDIO_RINGBUF_LEN) / 4))) { + /* buffer is filling up; switch to fast mode */ + return 1; + } else if (now_fast && (fill <= ((1 * AUDIO_RINGBUF_LEN) / 4))) { + /* buffer is emptying; switch to slow mode */ + return 0; + } + } else { + /* "twitchy" speed compensation */ + if (fill >= (AUDIO_RINGBUF_LEN / 2)) { + return 1; + } else { + return 0; + } + } + return now_fast; /* if no change */ +} - for (c = 0; c < BUFLEN_DD; c++) { - tapebuffer[c] = tapenoise[c]; - tapenoise[c] = 0; +/* receive 1/1200 seconds' worth of data; + (this is usually one bit, but not if 300 baud) */ +void tapenoise_send_1200 (char tone_1200th, uint8_t *no_emsgs_inout) { + + int16_t *sine1200, *sine2400; + uint32_t num_smps; + uint8_t hysteresis; + +#ifdef SPEED_COMPENSATION_HYSTERESIS + hysteresis = 1; +#else + hysteresis = 0; +#endif + + currently_fast = is_ringbuf_filling (currently_fast, + hysteresis, + audio_ringbuf_fill); + + /* During leader sections, we can do speed compensation + just by throwing away cycles. This sounds less + distracting, hence this condition for adding the + cycle to the ring buffer. During data blocks, we + can't do this and we have to pick a fast/slow sine wave + and suffer the wobble of speed changes instead, but + the 1200/2400 switching will mask the audible effect. */ + + if (('L' != tone_1200th) || ! currently_fast) { + + if (currently_fast) { + num_smps = FAST_SMPS; + sine1200 = sinewave_1200_fast; + sine2400 = sinewave_2400_fast; + } else { + num_smps = SLOW_SMPS; + sine1200 = sinewave_1200_slow; + sine2400 = sinewave_2400_slow; } - al_set_audio_stream_fragment(stream, tapebuffer); - } else - log_debug("tapenoise: overrun"); + + if ('0' == tone_1200th) { + ringbuf_append(sine1200, num_smps, no_emsgs_inout); + } else if (('1' == tone_1200th) || ('L' == tone_1200th)) { + ringbuf_append(sine2400, num_smps, no_emsgs_inout); + } else { + ringbuf_append(NULL, num_smps, no_emsgs_inout); + } + + } + } -static void add_high(void) -{ - int c; - float wavediv = (32.0f * 2400.0f) / (float) FREQ_DD; - tmcount++; - for (c = 0; c < 368; c++) { - if (tpnoisep >= BUFLEN_DD) - send_buffer(); - tapenoise[tpnoisep++] = sinewave[((int)swavepos) & 0x1F] * 64; - swavepos += wavediv; +void tapenoise_play_ringbuf (void) { + int16_t *tapebuffer; + if (0 == audio_ringbuf_fill) { + return; + } + /* now output an audio chunk from the start of the ring buffer */ + if (NULL != (tapebuffer = al_get_audio_stream_fragment(stream))) { + ringbuf_play(tapebuffer); + al_set_audio_stream_fragment(stream, tapebuffer); } } -void tapenoise_addhigh(void) -{ - if (sound_tape) - add_high(); + +void tapenoise_flush_ringbuf(void) { + audio_ringbuf_fill = 0; } -static void add_dat(uint8_t dat) -{ - int c, d, e = 0; - float wavediv = (32.0f * 2400.0f) / (float) FREQ_DD; - for (c = 0; c < 30; c++) { /*Start bit*/ - if (tpnoisep >= BUFLEN_DD) - send_buffer(); - tapenoise[tpnoisep++] = sinewave[((int)swavepos) & 0x1F] * 64; - e++; - swavepos += (wavediv / 2); - } - swavepos = fmod(swavepos, 32.0); - while (swavepos < 32.0) { - if (tpnoisep >= BUFLEN_DD) - send_buffer(); - tapenoise[tpnoisep++] = sinewave[((int)swavepos) & 0x1F] * 64; - swavepos += (wavediv / 2); - e++; - } - for (d = 0; d < 8; d++) { - swavepos = fmod(swavepos, 32.0); - while (swavepos < 32.0) { - if (tpnoisep >= BUFLEN_DD) - send_buffer(); - tapenoise[tpnoisep++] = sinewave[((int)swavepos) & 0x1F] * ((dat & 1) ? 50 : 64); - if (dat & 1) swavepos += wavediv; - else swavepos += (wavediv / 2); - e++; - } - dat >>= 1; - } - for ( ;e < 368; e++) { /*Stop bit*/ - if (tpnoisep >= BUFLEN_DD) - send_buffer(); - tapenoise[tpnoisep++] = sinewave[((int)swavepos) & 0x1F] * 64; - swavepos += (wavediv / 2); + +/* #define TAPE_NOISE_RINGBUF_EMSGS */ +/* #define TAPE_NOISE_RINGBUF_EMSGS_2 */ + +static void ringbuf_append (int16_t *src, uint32_t len, uint8_t *no_emsgs_inout) { + uint32_t n, pos; + if ( ! sound_tape ) { + audio_ringbuf_fill = 0; + return; } - add_high(); + if ((audio_ringbuf_fill + len) >= AUDIO_RINGBUF_LEN) { + if ( ! *no_emsgs_inout ) { +#ifdef TAPE_NOISE_RINGBUF_EMSGS + log_warn("tapenoise: audio ring buffer is full\n"); +#endif + *no_emsgs_inout = 1; /* suppress further error message spam */ + } + return; + } + *no_emsgs_inout = 0; /* re-enable future error messages */ + pos = audio_ringbuf_start + audio_ringbuf_fill; + for (n=0; n < len; n++, pos++) { + int16_t i; + if (pos >= AUDIO_RINGBUF_LEN) { + pos -= AUDIO_RINGBUF_LEN; + } + if (src != NULL) { + i = src[n]; + } else { + i = 0; + } + audio_ringbuf[pos] = i; + } + audio_ringbuf_fill += len; } -void tapenoise_adddat(uint8_t dat) -{ - if (sound_tape) - add_dat(dat); + + +static void ringbuf_play (int16_t *buf) { + uint32_t n; + + /* remember, BUFLEN_TN is the Allegro output buffer length; + AUDIO_RINGBUF_LEN is the internal ring buffer length. */ + + for (n=0; (n < BUFLEN_TN) && (audio_ringbuf_fill > 0); n++) { + /* implement ring buffer wrappery */ + if (audio_ringbuf_start >= AUDIO_RINGBUF_LEN) { + audio_ringbuf_start = 0; + } + buf[n] = audio_ringbuf[audio_ringbuf_start]; + audio_ringbuf_start++; + audio_ringbuf_fill--; + } + + /* if fill was not complete, finish the job with zeros */ + if (n < BUFLEN_TN) { + memset(buf + n, 0, sizeof(int16_t) * (BUFLEN_TN - n)); + /* only warn if buffer isn't completely empty + (avoids endless error message spam) */ + if (n != 0) { +#ifdef TAPE_NOISE_RINGBUF_EMSGS + log_warn("tapenoise: audio ring buffer is empty\n"); +#endif + return; + } + } + +#ifdef TAPE_NOISE_RINGBUF_EMSGS_2 + printf("["); + for (n=0; n < 78; n++) { + if (n<(audio_ringbuf_fill * 78) / AUDIO_RINGBUF_LEN) { + printf("#"); + } else { + printf(" "); + } + } + printf("]\n"); +#endif + + } + void tapenoise_motorchange(int stat) { ALLEGRO_SAMPLE *smp; log_debug("tapenoise: motorchange, stat=%d", stat); - if ((stat < 2) && (smp = tsamples[stat])) + /* TOHv2: added sound_tape_relay gate */ + if (sound_tape_relay && (stat < 2) && (smp = tsamples[stat])) al_play_sample(smp, 1.0, 0.0, 1.0, ALLEGRO_PLAYMODE_ONCE, NULL); } + +/* TOHv4.2 + * this is a separate tape-to-tone mechanism, + * independent of the "live" tape-to-tone mechanism, + * for writing out WAVs. */ +int tapenoise_write_wav (tape_state_t *tape_state_live, char *fn, uint8_t use_phase_shift) { + + tape_state_t clone; + int e; +#define WAV_HEADER_LEN 44 + uint8_t header[WAV_HEADER_LEN]; + size_t num_1200ths; + char tone; + size_t z; + FILE *f; + int16_t silence[SLOW_SMPS]; + size_t file_len; + + e = tape_state_clone_and_rewind(&clone, tape_state_live); + if (TAPE_E_OK != e) { return e; } + + if (TAPE_FILETYPE_BITS_NONE == clone.filetype_bits) { + log_warn("tapenoise: save WAV: BUG: no viable tone source!"); + return TAPE_E_BUG; + } + + num_1200ths = 0; + + do { + e = tape_tone_1200th_from_back_end (0, &clone, 0, 0, &tone); + if (TAPE_E_OK == e) { num_1200ths++; } + } while (TAPE_E_OK == e); + + if ((TAPE_E_EOF != e) && (TAPE_E_OK != e)) { + log_warn("tapenoise: save WAV: pass 1, error fetching tone (code %d)", e); + return e; + } + +#define WAV_BODY_LIMIT_BYTES 1000000000 + if (num_1200ths > (WAV_BODY_LIMIT_BYTES / (SLOW_SMPS * 2))) { + log_warn("tapenoise: save WAV: WAV would exceed body size limit: %zu", (size_t) WAV_BODY_LIMIT_BYTES); + tape_state_finish(&clone, 0); + return TAPE_E_SAVE_WAV_BODY_TOO_LARGE; + } + + e = TAPE_E_OK; + + tape_rewind_2(&clone); + + log_info("tapenoise: save WAV: %zu tonepackets\n", num_1200ths); + + memcpy(header, "RIFF" + "\xFF\xFF\xFF\xFF" /* fill in later: (file_len - 8) */ + "WAVEfmt " + "\x10\x00\x00\x00" + "\x01\x00" + "\x01\x00" + "\x44\xAC\x00\x00" + "\x88\x58\x01\x00" + "\x02\x00" + "\x10\x00" + "data" + "\xFF\xFF\xFF\xFF", 44); /* fill in later: (file_len - WAV_HEADER_LEN) */ + + /* complete the fields in the WAV header */ + file_len = WAV_HEADER_LEN + (SLOW_SMPS * 2 * num_1200ths); + + tape_write_u32(header + 4, (file_len - 8) ); + tape_write_u32(header + 40, (file_len - WAV_HEADER_LEN)); + + f = fopen(fn, "wb"); + + if (NULL == f) { + log_warn("tapenoise: save WAV: failed to open file for saving: %s", fn); + tape_state_finish(&clone, 0); + return TAPE_E_SAVE_FOPEN; + } + + if (1 != fwrite (header, WAV_HEADER_LEN, 1, f)) { + log_warn("tapenoise: save WAV: header write failure"); + fclose(f); + tape_state_finish(&clone, 0); + return TAPE_E_SAVE_FWRITE; + } + + memset(silence, 0, 2 * SLOW_SMPS); + + for (z=0; z < num_1200ths; z++) { + +// int i; + int16_t *tone_packet; + + e = tape_tone_1200th_from_back_end (0, &clone, 0, 0, &tone); + if (TAPE_E_OK != e) { + log_warn("tapenoise: save WAV: pass 2, error fetching tone (code %d)", e); + fclose(f); + tape_state_finish(&clone, 0); + return TAPE_E_SAVE_FWRITE; + } + + tone_packet = use_phase_shift ? wav_cos_2400 : wav_sin_2400; + if ('0' == tone) { + tone_packet = use_phase_shift ? wav_cos_1200 : wav_sin_1200; + } else if ('S' == tone) { + tone_packet = silence; + } + + /* FIXME: endianness */ + if (1 != fwrite (tone_packet, SLOW_SMPS * 2, 1, f)) { + log_warn("tapenoise: save WAV: write failure"); + fclose(f); + tape_state_finish(&clone, 0); + return TAPE_E_SAVE_FWRITE; + } + + } + + tape_state_finish(&clone, 0); + fclose(f); + + return e; + +} diff -Nu b-em-40246d4-vanilla/src/tapenoise.h b-em-40246d4-TOHv4.2/src/tapenoise.h --- b-em-40246d4-vanilla/src/tapenoise.h 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/tapenoise.h 2025-07-06 14:17:40.697657460 +0100 @@ -1,11 +1,18 @@ #ifndef __INC_TAPENOISE_H #define __INC_TAPENOISE_H +#include "sound.h" +#include "tape.h" + void tapenoise_init(ALLEGRO_EVENT_QUEUE *queue); void tapenoise_close(void); -void tapenoise_addhigh(void); -void tapenoise_adddat(uint8_t dat); void tapenoise_motorchange(int stat); void tapenoise_streamfrag(void); +void tapenoise_send_1200 (char tone_1200th, uint8_t *no_emsgs_inout); +void tapenoise_play_ringbuf (void); +void tapenoise_activated_hook(void); /* TOHv4.1-rc9 */ +int tapenoise_write_wav (tape_state_t *tape_state_live, + char *fn, + uint8_t use_phase_shift); /* TOHv4.2 */ #endif diff -Nu b-em-40246d4-vanilla/src/tibet.c b-em-40246d4-TOHv4.2/src/tibet.c --- b-em-40246d4-vanilla/src/tibet.c 1970-01-01 01:00:00.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/tibet.c 2025-07-06 14:17:40.697657460 +0100 @@ -0,0 +1,2093 @@ +/* TIBET reference decoder, v2.1 + 'Diminished', June 2024 + Public Domain */ + +#include +#include +#include +#include + +#include "tibet.h" +#include "tape.h" + +#include + +#define TIBET_ALLOW_WEIRD_BAUD_RATES /* TOHv4: 75, 150, 600 baud */ + +#define TIBET_OUTPUT_MAX_LEN (100 * 1024 * 1024) + +#define TIBET_STATE_METADATA 0 +#define TIBET_STATE_DATA 1 + +#define TIBET_K_TIBET "tibet" +#define TIBET_K_SILENCE "silence" +#define TIBET_K_LEADER "leader" +#define TIBET_K_BAUD "/baud" +#define TIBET_K_FRAMING "/framing" +#define TIBET_K_TIME "/time" +#define TIBET_K_PHASE "/phase" +#define TIBET_K_SPEED "/speed" +#define TIBET_K_DATA "data" +#define TIBET_K_SQUAWK "squawk" +#define TIBET_K_END "end" + +// private parsing functions + +static int parse_line (tibet_priv_t *tp, + char *line, + size_t len, + tibet_span_t *pending_span, + tibet_span_hints_t *active_hints); +static int metadata_line (tibet_priv_t *tp, + char *word1, + char *word2, + tibet_span_t *pending_data_span, + tibet_span_hints_t *hints_inout); +static int data_line (tibet_priv_t *tp, char *word1, char *word2, tibet_span_t *pending_span); +static int sub_line (tibet_priv_t *tp, char *line, tibet_span_t *pending_data_span, tibet_span_hints_t *active_hints); +static uint8_t line_tok_and_trim (char *line, char **word2); +static int version (uint32_t linenum, char *word1, char *word2, uint32_t *major_out, uint32_t *minor_out); // TOHv3.2 +static int verify_dup_version_and_reset_baud_and_framing (tibet_priv_t *tp, + char *v, + tibet_span_hints_t *hints); +static int +enforce_hint_compat_for_silent_spans (tibet_span_hints_t *active_hints, + uint32_t linenum); +static int +enforce_hint_compat_for_leader_spans (tibet_span_hints_t *active_hints, + uint32_t linenum); +static int append_data_span (tibet_priv_t *priv, tibet_span_t *pending); +static int append_silent_span (tibet_priv_t *tp, float len_s, tibet_span_hints_t *hints); +static int append_leader_span (tibet_priv_t *tp, uint32_t num_2400ths, tibet_span_hints_t *hints); + +static int parse_float (int linenum, char *v, float *f); +static int parse_int (int linenum, char* v, uint32_t* i); +static int parse_silence (tibet_priv_t *tp, char *v, tibet_span_hints_t *active_hints); +static int parse_leader (tibet_priv_t *tp, char *v, tibet_span_hints_t *active_hints); +static int parse_baud (tibet_priv_t *tp, char *v, tibet_span_hints_t *hints); +static int parse_framing (tibet_priv_t *tp, char *v, tibet_span_hints_t *hints); +static int parse_time (tibet_priv_t *tp, char *v, tibet_span_hints_t *hints); +static int parse_phase (tibet_priv_t *tp, char *v, tibet_span_hints_t *hints); +static int parse_speed (tibet_priv_t *tp, char *v, tibet_span_hints_t *hints); +static int begin_data (tibet_priv_t *tp, char is_squawk, tibet_span_t *pending_span, tibet_span_hints_t *hints); + + +static int append_tonechar (tibet_priv_t *tp, char c, tibet_span_t *span_pending) ; + +// private client-facing functions + +static int c_next_tonechar (tibet_priv_t *tp, char *tc); +static int c_read_1200 (tibet_priv_t *t, uint8_t *out, uint32_t *cur_span_out); +static int c_get_priv (tibet_t *t, tibet_priv_t **priv, uint8_t enforce_decoded); +static int c_get_span (tibet_priv_t *tp, tibet_span_t **span); +static tibet_span_t *c_current_span (tibet_priv_t *tp); +static int c_new_span (tibet_priv_t *tp); +static int c_alloc_priv_if_needed(tibet_t *t); +static int c_set_version_if_missing (tibet_t *t); + + + +static int parse_line (tibet_priv_t *tp, char *line, size_t len, tibet_span_t *pending_span, tibet_span_hints_t *active_hints) { + + size_t eol; + char *sub; + int e; + + e = TAPE_E_OK; + + // remove comments + for (eol=0; (eol < len) && ('#' != line[eol]); eol++) + ; + + if (eol>0) { // skip blank lines + // we'll need proper null-terminated buffers for the sub-lines, + // because we'll want to printf() them etc., so we'll have to + // malloc each one + sub = TIBET_MALLOC (1 + eol); + if (NULL == sub) { + TIBET_ERR("TIBET: line %u: Could not malloc sub-line buffer.", tp->ds_linenum+1); + return TAPE_E_MALLOC; + } + sub[eol] = '\0'; + memcpy(sub, line, eol); + e = sub_line (tp, sub, pending_span, active_hints); + /* ?? */ + TIBET_FREE(sub); /* from cornfield valgrind testing */ + } + + (tp->ds_linenum)++; + return e; + +} + + + +static int sub_line (tibet_priv_t *tp, char *line, tibet_span_t *pending_data_span, tibet_span_hints_t *active_hints) { + + int e; + char *word1, *word2; + uint32_t major, minor; + + line_tok_and_trim (line, &word2); + word1 = line; + +//printf("LN %d: word1=\"%s\", word2=\"%s\"\n", tp->ds_linenum + 1, word1, word2); + + // got version line yet? + // TOHv3.2: now use separate tracking variable, ds_have_version; + // version is integer major/minor now + if ( ! tp->ds_have_version ) { + major = 0; + minor = 0; + e = version (tp->ds_linenum, word1, word2, &major, &minor); + if (TAPE_E_OK != e) { return e; } + if (major != TIBET_VERSION_MAJOR) { + TIBET_ERR("TIBET: line %u: file major version (%u) incompatible with decoder (%u)", + tp->ds_linenum+1, major, TIBET_VERSION_MAJOR); + return TAPE_E_TIBET_VERSION_MAJOR; + } + if (minor > TIBET_VERSION_MINOR) { + TIBET_ERR("TIBET: line %u: file minor version (%u) is newer than this decoder's (%u).", + tp->ds_linenum+1, minor, TIBET_VERSION_MINOR); + return TAPE_E_TIBET_VERSION_MINOR; + } + + tp->ds_have_version = 1; + tp->dd_version_major = major; + tp->dd_version_minor = minor; + + return e; + + } + + if (TIBET_STATE_METADATA == tp->ds_state) { + e = metadata_line (tp, word1, word2, pending_data_span, active_hints); + } else { + e = data_line (tp, word1, word2, pending_data_span); + /* TOHv4-rc3: memory leak found by valgrind */ + if ( (TAPE_E_OK != e) && (NULL != pending_data_span->tones) ) { + TIBET_FREE(pending_data_span->tones); + pending_data_span->tones = NULL; + pending_data_span->num_tones = 0; + pending_data_span->num_tones_alloc = 0; + } + } + + return e; +} + + + +//static int version (tibet_priv_t *tp, char *word1, char *word2) { +static int version (uint32_t linenum, char *word1, char *word2, uint32_t *major_out, uint32_t *minor_out) { + + int e; + size_t vlen, i, num_dps, dpix; + long major, minor; + + e = TAPE_E_OK; + + if (NULL == word2) { + TIBET_ERR("TIBET: line %u: no space in version line: \"%s\"", linenum+1, word1); + return TAPE_E_TIBET_VERSION_LINE_NOSPC; + } + if ( strcmp (word1, TIBET_K_TIBET) ) { + TIBET_ERR("TIBET: line %u: bad version word: \"%s\"", linenum+1, word1); + return TAPE_E_TIBET_VERSION_LINE; + } + // TOHv3.2: new, more flexible version logic + vlen = strlen(word2); + if ((vlen > TIBET_VERSION_MAX_LEN) || (vlen < 3)) { + TIBET_ERR("TIBET: line %u: version has bad length: \"%s\"", linenum+1, word2); + return TAPE_E_TIBET_VERSION_BAD_LEN; + } + for (i=0, num_dps=0, dpix=0; i=1 digit on each side + if ((i>0) && (i<(vlen-1)) && (word2[i]=='.')) { + dpix = i; + num_dps++; + } else if ((word2[i] < '0') || (word2[i] > '9')) { + TIBET_ERR("TIBET: line %u: version non-numeric: \"%s\"", linenum+1, word2); + return TAPE_E_TIBET_VERSION_NON_NUMERIC; + } + } + if (num_dps != 1) { + TIBET_ERR("TIBET: line %u: version has bad decimal point: \"%s\"", linenum+1, word2); + return TAPE_E_TIBET_VERSION_NO_DP; + } +#define TIBET_VERSION_PORTION_MAXLEN 5 + if (dpix > TIBET_VERSION_PORTION_MAXLEN) { + TIBET_ERR("TIBET: line %u: major version is too long: \"%s\"", linenum+1, word2); + return TAPE_E_TIBET_VERSION_BAD_LEN; + } + word2[dpix] = '\0'; // re-terminate + major = strtol(word2, NULL, 10); + word2[dpix] = '.'; // replace this + if ((vlen - (1+dpix)) > TIBET_VERSION_PORTION_MAXLEN) { + TIBET_ERR("TIBET: line %u: minor version is too long: \"%s\"", linenum+1, word2); + return TAPE_E_TIBET_VERSION_BAD_LEN; + } + minor = strtol(word2 + dpix + 1, NULL, 10); + + // TOHv3.2: eliminated dd_version string and replace with + // dd_version_major and dd_version_minor integers. + + *major_out = (uint32_t) (0x7fffffff & major); + *minor_out = (uint32_t) (0x7fffffff & minor); + + return e; + +} + + +/* returns TRUE if there is a second word */ +static uint8_t line_tok_and_trim (char *line, char **word2) { + size_t n, len; + len = strlen(line); + *word2 = NULL; + // start by eliminating trailing spaces + while ((len>0) && (' '==line[len-1])) { + len--; + } + line[len] = '\0'; + for (n=0; n < len; n++) { + if (' ' == line[n]) { + *word2 = line + n + 1; + line[n] = '\0'; + break; + } + } + return (*word2 != NULL); +} + + + +static int metadata_line (tibet_priv_t *tp, + char *word1, + char *word2, + tibet_span_t *pending_data_span, + tibet_span_hints_t *hints_inout) { + + int e; + + e = TAPE_E_OK; + +//printf("metadata_line: (%s, %s)\n", word1, word2); + + if ( ! strcmp (word1, TIBET_K_TIBET) ) { + e = verify_dup_version_and_reset_baud_and_framing (tp, word2, hints_inout); + } else if ( ! strcmp (word1, TIBET_K_SILENCE) ) { + e = parse_silence (tp, word2, hints_inout); + } else if ( ! strcmp (word1, TIBET_K_LEADER ) ) { + e = parse_leader (tp, word2, hints_inout); // hints may be zeroed + } else if ( ! strcmp (word1, TIBET_K_BAUD ) ) { + e = parse_baud (tp, word2, hints_inout); // hints may be zeroed + } else if ( ! strcmp (word1, TIBET_K_FRAMING) ) { + e = parse_framing (tp, word2, hints_inout); + } else if ( ! strcmp (word1, TIBET_K_TIME ) ) { + e = parse_time (tp, word2, hints_inout); + } else if ( ! strcmp (word1, TIBET_K_PHASE ) ) { + e = parse_phase (tp, word2, hints_inout); + } else if ( ! strcmp (word1, TIBET_K_SPEED ) ) { + e = parse_speed (tp, word2, hints_inout); + } else if ( ! strcmp (word1, TIBET_K_DATA ) ) { + if (word2 != NULL) { + TIBET_ERR("TIBET: line %u: junk follows data keyword: \"%s\"", tp->ds_linenum+1, word2); + e = TAPE_E_TIBET_DATA_JUNK_FOLLOWS_START; + } else { + e = begin_data (tp, 0, pending_data_span, hints_inout); // hints will be cloned onto data span, then wiped + } + } else if ( ! strcmp (word1, TIBET_K_SQUAWK ) ) { + if (word2 != NULL) { + TIBET_ERR("TIBET: line %u: junk follows squawk keyword: \"%s\"", tp->ds_linenum+1, word2); + e = TAPE_E_TIBET_DATA_JUNK_FOLLOWS_START; + } else { + e = begin_data (tp, 1, pending_data_span, hints_inout); // hints will be cloned onto data span, then wiped + } + } else { + TIBET_ERR("TIBET: line %u: unrecognised: \"%s\"", tp->ds_linenum+1, word1); + e = TAPE_E_TIBET_UNK_WORD; + } + + return e; + +} + + +static int begin_data (tibet_priv_t *tp, char is_squawk, tibet_span_t *pending_span, tibet_span_hints_t *hints) { + /* TIBET 0.5, TOHv3.2 + check removed -- we may need to have two adjacent data spans, + if we need to e.g. switch framings during a block, so this restriction has been relaxed + */ + /* + if (t->parsed.num_spans > 1) { + if (TIBET_SPAN_DATA == (current_span(t)-1)->type) { + TIBET_ERR("TIBET: line %u: two adjacent data spans\n", + state->linenum+1); + return TAPE_E_TIBET_ADJACENT_SPANS_SAME_TYPE; + } + } + */ + tp->ds_state = TIBET_STATE_DATA; + pending_span->type = TIBET_SPAN_DATA; + pending_span->is_squawk = is_squawk; + pending_span->hints = *hints; + memset(hints, 0, sizeof(tibet_span_hints_t)); + return TAPE_E_OK; +} + + +#define TIBET_SPEED_HINT_MIN 0.5 +#define TIBET_SPEED_HINT_MAX 1.5 + +static int parse_speed (tibet_priv_t *tp, char *v, tibet_span_hints_t *hints) { + int e; + float f; + //tibet_span_t *span; + //span = pending_span; //c_current_span(tp); + if (hints->have_speed) { + TIBET_ERR("TIBET: line %u: %s specified twice for same span", + tp->ds_linenum+1, TIBET_K_SPEED); + return TAPE_E_TIBET_DUP_SPEED; + } + e = parse_float (tp->ds_linenum+1, v, &f); + if (TAPE_E_OK != e) { return e; } + if (f >= TIBET_SPEED_HINT_MAX) { + TIBET_ERR("TIBET: line %u: %s is too large: \"%s\"", + tp->ds_linenum+1, TIBET_K_SPEED, v); + return TAPE_E_TIBET_SPEED_HINT_HIGH; + } + if (f <= TIBET_SPEED_HINT_MIN) { + TIBET_ERR("TIBET: line %u: %s is too small: \"%s\"", + tp->ds_linenum+1, TIBET_K_SPEED, v); + return TAPE_E_TIBET_SPEED_HINT_LOW; + } + hints->speed = f; + hints->have_speed = 1; + return TAPE_E_OK; +} + + + +static int parse_phase (tibet_priv_t *tp, char *v, tibet_span_hints_t *hints) { + int e; + uint32_t p; + if (hints->have_phase) { + TIBET_ERR("TIBET: line %u: %s specified twice for same span", + tp->ds_linenum+1, TIBET_K_PHASE); + return TAPE_E_TIBET_DUP_PHASE; + } + e = parse_int (tp->ds_linenum+1, v, &p); + if (TAPE_E_OK != e) { return e; } + if ((p != 0) && (p != 90) && (p != 180) && (p != 270)) { + TIBET_ERR("TIBET: line %u: illegal %s: \"%s\"", + tp->ds_linenum+1, TIBET_K_PHASE, v); + return TAPE_E_TIBET_BAD_PHASE; + } + hints->phase = p; + hints->have_phase = 1; + return TAPE_E_OK; +} + + +#define TIBET_TIME_HINT_MAX 36000.0f + +static int parse_time (tibet_priv_t *tp, char *v, tibet_span_hints_t *hints) { + int e; + float f; + if (hints->have_time) { + TIBET_ERR("TIBET: line %u: %s specified twice for same span", + tp->ds_linenum+1, TIBET_K_TIME); + return TAPE_E_TIBET_DUP_TIME; + } + e = parse_float (tp->ds_linenum+1, v, &f); + if (TAPE_E_OK != e) { return e; } + if (f > TIBET_TIME_HINT_MAX) { + TIBET_ERR("TIBET: line %u: %s is too large: \"%s\"", + tp->ds_linenum+1, TIBET_K_TIME, v); + return TAPE_E_TIBET_TIME_HINT_TOOLARGE; + } + hints->time = f; + hints->have_time = 1; + return TAPE_E_OK; +} + + +static int parse_framing (tibet_priv_t *tp, char *v, tibet_span_hints_t *hints) { + //tibet_span_t *span; + //span = pending_span; //c_current_span(tp); + if (hints->have_framing) { + TIBET_ERR("TIBET: line %u: %s specified twice for same span", + tp->ds_linenum+1, TIBET_K_FRAMING); + return TAPE_E_TIBET_DUP_FRAMING; + } + if ( strcmp ("7E2", v) + && strcmp ("7O2", v) + && strcmp ("7E1", v) + && strcmp ("7O1", v) + && strcmp ("8N2", v) + && strcmp ("8N1", v) + && strcmp ("8E1", v) + && strcmp ("8O1", v) ) { + TIBET_ERR("TIBET: line %u: illegal %s: \"%s\"", + tp->ds_linenum+1, TIBET_K_FRAMING, v); + return TAPE_E_TIBET_BAD_FRAMING; + } + memcpy(hints->framing, v, 3); + hints->framing[3] = '\0'; + hints->have_framing = 1; + return TAPE_E_OK; +} + + +static int parse_baud (tibet_priv_t *tp, char *v, tibet_span_hints_t *hints) { + int e; + uint32_t b; + if (hints->have_baud) { + TIBET_ERR("TIBET: line %u: %s specified twice for same span\n", + tp->ds_linenum+1, TIBET_K_BAUD); + return TAPE_E_TIBET_DUP_BAUD; + } + e = parse_int (tp->ds_linenum+1, v, &b); + if (TAPE_E_OK != e) { return e; } + /* TOHv4: support intermediate rates */ + if ( 1 +#ifdef TIBET_ALLOW_WEIRD_BAUD_RATES + && (b != 75) && (b != 150) && (b != 600) +#endif + && (b != 300) && (b != 1200)) { + TIBET_ERR("TIBET: line %u: illegal %s: \"%s\"", + tp->ds_linenum+1, TIBET_K_BAUD, v); + return TAPE_E_TIBET_BAD_BAUD; + } + hints->baud = b; + hints->have_baud = 1; + return TAPE_E_OK; +} + + +#define LEADER_MAX_CYCLES 100000000 + +static int parse_leader (tibet_priv_t *tp, char *v, tibet_span_hints_t *active_hints) { //}, tibet_span_t *pending_span) { + int e; + uint32_t i; + /* overhaul v3: add check for missing field */ + if (NULL == v) { + TIBET_ERR("TIBET: line %u: %s has missing value field", + tp->ds_linenum+1, TIBET_K_LEADER); + return TAPE_E_TIBET_EMPTY_LEADER; + } + e = enforce_hint_compat_for_leader_spans (active_hints, tp->ds_linenum+1); + if (TAPE_E_OK != e) { return e; } + e = parse_int (tp->ds_linenum+1, v, &i); + if (TAPE_E_OK != e) { return e; } + if (i > LEADER_MAX_CYCLES) { + TIBET_ERR("TIBET: line %u: illegal %s length: \"%s\"", + tp->ds_linenum+1, TIBET_K_LEADER, v); + return TAPE_E_TIBET_LONG_LEADER; + } + e = append_leader_span(tp, i, active_hints); + return e; +} + + + +static int parse_int (int linenum, char *v, uint32_t *i) { + size_t n, len; + char *p; + unsigned long lv; + /* overhaul v3: sanity check to catch bug */ + if (NULL == v) { + TIBET_ERR("TIBET: BUG: line %u: parse_int: NULL string!", linenum); + return TAPE_E_BUG; + } + len = strlen(v); + if (0 == len) { + TIBET_ERR("TIBET: BUG: line %u: integer is void!", linenum); + return TAPE_E_BUG; + } + if (len > 10) { + TIBET_ERR("TIBET: line %u: integer value is too long: \"%s\"", + linenum, v); + return TAPE_E_TIBET_INT_TOO_LONG; + } + for (n=0; n < len; n++) { + char c; + c = v[n]; + if ((c<'0') || (c>'9')) { + TIBET_ERR("TIBET: line %u: integer value has illegal char: \"%s\"", + linenum, v); + return TAPE_E_TIBET_INT_BAD_CHAR; + } + } + errno = 0; + lv = strtoul(v, &p, 10); + if ((p != (v + len)) || (0 != errno) || (lv > ((unsigned long) 0xffffffff))) { + TIBET_ERR("TIBET: line %u: bad integer: \"%s\"", + linenum, v); + return TAPE_E_TIBET_INT_PARSE; + } + *i = 0xffffffff & lv; + return TAPE_E_OK; +} + + + +#define TIBET_SILENCE_LEN_MAX 36000.0 +#define TIBET_SILENCE_LEN_MIN 0.0004 // relaxed +#define TIBET_FREQ_HI 2403.8f + +static int parse_silence (tibet_priv_t *tp, char *v, tibet_span_hints_t *active_hints) { + int e; + float f; + //tibet_span_t *span; + //span = pending_span; // c_current_span(tp); + /* + if (tp->dd_num_spans > 1) { + if (TIBET_SPAN_SILENT == (span-1)->type) { + TIBET_ERR("TIBET: line %u: two adjacent silent spans\n", + tp->ds_linenum+1); + return TAPE_E_TIBET_ADJACENT_SPANS_SAME_TYPE; + } + } + */ + e = enforce_hint_compat_for_silent_spans (active_hints, tp->ds_linenum+1); + if (TAPE_E_OK != e) { return e; } + e = parse_float (tp->ds_linenum+1, v, &f); + if (TAPE_E_OK != e) { return e; } + if (f < TIBET_SILENCE_LEN_MIN) { + /* + TIBET_ERR("TIBET: line %u: %s is too short: \"%s\"\n", + tp->ds_linenum+1, TIBET_K_SILENCE, v); + return TAPE_E_TIBET_SHORT_SILENCE; + */ + /* changed: very short periods of silence are now permitted, + but the decoder must skip them. */ + return TAPE_E_OK; + } + if (f > TIBET_SILENCE_LEN_MAX) { + TIBET_ERR("TIBET: line %u: %s has excessive length: \"%s\"", + tp->ds_linenum+1, TIBET_K_SILENCE, v); + return TAPE_E_TIBET_LONG_SILENCE; + } + //span->type = TIBET_SPAN_SILENT; + // work out a number of atoms equivalent to this silent length + // we're going to ignore the speed hint and just assume a speed of 1.0 + //span->num_tones = (uint32_t) roundf(f * TIBET_FREQ_HI); + e = append_silent_span (tp, f, active_hints); //(uint32_t) roundf(f * TIBET_FREQ_HI)); + return e; +} + + +#define TIBET_DECIMAL_MAX_CHARS 50 + +static int parse_float (int linenum, char *v, float *f) { + + size_t n, len; + char have_dp; + char *p; + + len = strlen(v); + have_dp=0; + + *f = 0.0f; + + if (len > TIBET_DECIMAL_MAX_CHARS) { + TIBET_ERR("TIBET: line %u: decimal is too long (max. %d chars): \"%s\"", + linenum, TIBET_DECIMAL_MAX_CHARS, v); + return TAPE_E_TIBET_DECIMAL_TOO_LONG; + } + + for (n=0; n < len; n++) { + char c; + c = v[n]; + if ('.' == c) { + if (have_dp) { + TIBET_ERR("TIBET: line %u: multiple decimal points in decimal: \"%s\"", + linenum, v); + return TAPE_E_TIBET_MULTI_DECIMAL_POINT; + } + have_dp = 1; + if (n==(len-1)) { + TIBET_ERR("TIBET: line %u: decimal point at end of decimal: \"%s\"", + linenum, v); + return TAPE_E_TIBET_POINT_ENDS_DECIMAL; + } + } else if ((c<'0') || (c>'9')) { + TIBET_ERR("TIBET: line %u: illegal character in decimal: \"%s\"", + linenum, v); + return TAPE_E_TIBET_DECIMAL_BAD_CHAR; + } + } // next char + + p = NULL; + errno = 0; + *f = strtof(v, &p); + + if ((0 != errno) || (p != (v + len))) { + TIBET_ERR ("TIBET: line %u: error parsing decimal: \"%s\"", linenum, v); + return TAPE_E_TIBET_DECIMAL_PARSE; + } + + //if (*f < TIBET_FLOAT_ZERO_MIN) { + // TIBET_ERR("TIBET: line %u: decimal is too small: \"%s\"\n", + // linenum, v); + // return TAPE_E_TIBET_SMALL_DECIMAL; + //} + + return TAPE_E_OK; + +} + + +static int +enforce_hint_compat_for_silent_spans (tibet_span_hints_t *active_hints, + uint32_t linenum) { + + char *fields_msg; + fields_msg = NULL; + + // (/time) is always permitted + // silent spans: reject (baud, framing, /speed, /phase) + + if (active_hints->have_baud) { + fields_msg = TIBET_K_BAUD; + } else if (active_hints->have_framing) { + fields_msg = TIBET_K_FRAMING; + } else if (active_hints->have_speed) { + fields_msg = TIBET_K_SPEED; + } else if (active_hints->have_phase) { + fields_msg = TIBET_K_PHASE; + } + + if (fields_msg != NULL) { + TIBET_ERR("TIBET: line %u: %s hint illegally supplied for silent span", + linenum, fields_msg); + return TAPE_E_TIBET_FIELD_INCOMPAT; + } + + return TAPE_E_OK; + +} + + +static int +enforce_hint_compat_for_leader_spans (tibet_span_hints_t *active_hints, + uint32_t linenum) { + + char *fields_msg; + fields_msg = NULL; + + // (/time) is always permitted + // leader spans: reject (framing, /phase, /baud); permit (/speed) + + if (active_hints->have_baud) { + fields_msg = TIBET_K_BAUD; + } else if (active_hints->have_framing) { + fields_msg = TIBET_K_FRAMING; + } else if (active_hints->have_phase) { + fields_msg = TIBET_K_PHASE; + } + + if (fields_msg != NULL) { + TIBET_ERR("TIBET: line %u: %s hint illegally supplied for leader span", + linenum, fields_msg); + return TAPE_E_TIBET_FIELD_INCOMPAT; + } + + return TAPE_E_OK; + +} + + + +// TOHv3.2 rework for version changes +static int verify_dup_version_and_reset_baud_and_framing (tibet_priv_t *tp, + char *v, + tibet_span_hints_t *hints) { + + int e; + uint32_t major, minor; + + major = 0; + minor = 0; + e = version (tp->ds_linenum, TIBET_K_TIBET, v, &major, &minor); + if (e != TAPE_E_OK) { return e; } + + // for concatenation, enforce identical version numbers + if ((tp->dd_version_major != major) || (tp->dd_version_minor != minor)) { + TIBET_ERR("TIBET: line %u: %s mismatch: %u.%u vs. %u.%u", // TOHv3.2 + tp->ds_linenum+1, + TIBET_K_TIBET, + major, + minor, + tp->dd_version_major, + tp->dd_version_minor); + return TAPE_E_TIBET_CONCAT_VERSION_MISMATCH; + } + /* duplicate version line needs to reset to 8N1/1200, + * since it represents the start of a concatenated file */ + hints->baud = 1200; + memcpy(hints->framing, "8N1", 4); + return TAPE_E_OK; +} + + + +#define SPAN_TONES_ALLOC_DELTA 1024 +#define SPAN_MAX_TONES 2000000 + +// just buffer all the tonechars; we'll just do some +// basic checking now, and then decode them on request later +static int data_line (tibet_priv_t *tp, char *word1, char *word2, tibet_span_t *pending_span) { + + int e; + size_t n, m, len1, len2; + char *line; + e = TAPE_E_OK; + + if ( ! strcmp (word1, TIBET_K_END) ) { +//printf("tibet: data_line(): end found, going back to STATE_METADATA, wiping pending_span\n"); + tp->ds_state = TIBET_STATE_METADATA; + e = append_data_span(tp, pending_span); + memset(pending_span, 0, sizeof(tibet_span_t)); + return e; + } + + // datastate + + //span = c_current_span(tp); + + // TOHv4-rc2: + // reconstruct line from word1 and word2 + // TIBET 0.5: permit a) spaces in data lines and b) comments after data lines + + len1 = strlen(word1); + len2 = (NULL == word2) ? 0 : strlen(word2); + line = TIBET_MALLOC(len1+len2+1); + if (NULL == line) { + TIBET_ERR("TIBET: line %u: malloc failure", tp->ds_linenum+1); + return TAPE_E_MALLOC; + } + + memcpy(line, word1, len1); + + for (n=0, m=0; (NULL != word2) && (n < len2); n++) { + char c; + c = word2[n]; + if (' ' == c) { continue; } // skip any further spaces + // if ('#' == c) { break; } + if ((c != '.') && (c != '-') && (c != 'P')) { + TIBET_ERR ("TIBET: line %u: junk follows data; illegal tone character: \"%c\"", + tp->ds_linenum+1, c); + e = TAPE_E_TIBET_DATA_JUNK_FOLLOWS_LINE; + break; + } + line[len1+m] = word2[n]; // otherwise copy + m++; + } + line[len1+m]='\0'; + + for (n=0; (TAPE_E_OK == e) && (n < m+len1); n++) { + char c; + c = line[n]; + e = append_tonechar(tp, c, pending_span); + } + + TIBET_FREE(line); + line = NULL; + + /* + if (TAPE_E_OK != e) { + TIBET_FREE(span->tones); + span->tones = NULL; + } + */ + + return e; + +} + + +static int append_tonechar (tibet_priv_t *tp, char c, tibet_span_t *span_pending) { + //int e; + tibet_span_t *span; + char *p; + uint32_t newlen; + + // our span is the pending span ... + + span = span_pending; + + if ((c != '.') && (c != '-') && (c != 'P')) { + TIBET_ERR ("TIBET: line %u: illegal tone character: \"%c\"", + tp->ds_linenum+1, c); + return TAPE_E_TIBET_DATA_ILLEGAL_CHAR; + } + if (span->num_tones >= SPAN_MAX_TONES) { + TIBET_ERR ("TIBET: line %u: too many tone characters in span", tp->ds_linenum + 1); + return TAPE_E_TIBET_DATA_EXCESSIVE_TONES; + } + if ((span->num_tones > 0) && ('P' == c) && ('P' == span->tones[span->num_tones - 1])) { + TIBET_ERR ("TIBET: line %u: illegal double pulse PP", + tp->ds_linenum+1); + return TAPE_E_TIBET_DATA_DOUBLE_PULSE; + } +//printf("num_tones = %u, num_tones_alloc = %u\n", span->num_tones, span->num_tones_alloc); + if (span->num_tones >= span->num_tones_alloc) { + newlen = span->num_tones + SPAN_TONES_ALLOC_DELTA; + p = TIBET_REALLOC(span->tones, newlen); + if (NULL == p) { + TIBET_ERR ("TIBET: line %u: could not realloc tones", tp->ds_linenum + 1); + return TAPE_E_MALLOC; + } + span->tones = p; + span->num_tones_alloc = newlen; + } +//printf("span %u->tones[%u] = %c\n", span->id, span->num_tones, c); + span->tones[span->num_tones] = c; + (span->num_tones)++; + return TAPE_E_OK; +} + +/* + + +static int append_tonechar (tibet_priv_t *tp, char c) { + //int e; + tibet_span_t *span; + char *p; + uint32_t newlen; + //e = c_get_span(tp, &span); + span = c_current_span(tp); + if ((c != '.') && (c != '-') && (c != 'P')) { + TIBET_ERR ("TIBET: line %u: illegal tone character: \"%c\"\n", + tp->ds_linenum+1, c); + return TAPE_E_TIBET_DATA_ILLEGAL_CHAR; + } + if (span->num_tones >= SPAN_MAX_TONES) { + TIBET_ERR ("TIBET: line %u: too many tone characters in span\n", tp->ds_linenum + 1); + return TAPE_E_TIBET_DATA_EXCESSIVE_TONES; + } + if ((span->num_tones > 0) && ('P' == c) && ('P' == span->tones[span->num_tones - 1])) { + TIBET_ERR ("TIBET: line %u: illegal double pulse PP\n", + tp->ds_linenum+1); + return TAPE_E_TIBET_DATA_DOUBLE_PULSE; + } + if (span->num_tones >= span->num_tones_alloc) { + newlen = span->num_tones_alloc + SPAN_TONES_ALLOC_DELTA; + p = TIBET_REALLOC(span->tones, newlen); + if (NULL == p) { + TIBET_ERR ("TIBET: line %u: could not realloc tones\n", tp->ds_linenum + 1); + return TAPE_E_MALLOC; + } + span->tones = p; + span->num_tones_alloc = newlen; + } +//printf("span %u->tones[%u] = %c\n", span->id, span->num_tones, c); + span->tones[span->num_tones] = c; + (span->num_tones)++; + return TAPE_E_OK; +} +*/ + + +// 2400ths: +static int c_next_tonechar (tibet_priv_t *tp, char *tc) { + tibet_span_t *span; + int e; + e = c_get_span(tp, &span); + if (TAPE_E_OK != e) { return e; } + if (TIBET_SPAN_DATA == span->type) { + if (span->tones[tp->c_tone_pos] == 'P') { + // we just skip pulses + (tp->c_tone_pos)++; + return c_next_tonechar (tp, tc); // and recurse + } + *tc = (span->tones[tp->c_tone_pos] == '.') ? '1' : '0'; + (tp->c_tone_pos)++; + } else if (TIBET_SPAN_SILENT == span->type) { + *tc = 'S'; + // TOHv3.2: playback silence fix + // note double-precision accumulator + // FIXME: probably should have just overloaded c_tone_pos + // for this instead of needlessly introducing a pointless + // ancillary floating point field, but never mind + tp->c_silence_pos_s += (832.0/2000000.0); + } else if (TIBET_SPAN_LEADER == span->type) { + *tc = 'L'; + (tp->c_tone_pos)++; + } + + // call this again, so that the span gets incremented if need be: + e = c_get_span(tp, &span); + return e; +} + + +int tibet_peek_eof (tibet_t *t) { + tibet_priv_t *tp; + int e; + e = c_get_priv (t, &tp, 1); + if (TAPE_E_OK != e) { return 1; } + return (tp->c_cur_span >= tp->dd_new_num_spans); +} + + +static int c_get_span (tibet_priv_t *tp, tibet_span_t **span) { + + tibet_span_t *_span; + uint8_t advance; // TOHv3.2 + + *span = NULL; + advance = 0; // TOHv3.2 + + if (tp->c_cur_span >= tp->dd_new_num_spans) { + //TIBET_ERR("TIBETdec: attempt to read beyond final span\n"); + return TAPE_E_EOF; + } + _span = tp->dd_new_spans + tp->c_cur_span; + + // TOHv3.2: fix silence not working on playback + if (TIBET_SPAN_SILENT == _span->type) { + if (tp->c_silence_pos_s >= _span->opo_silence_duration_secs) { + advance = 1; + } + } else { + if (tp->c_tone_pos >= _span->num_tones) { + // out of tones; attempt to advance span + advance = 1; + } + } + + // TOHv3.2 + if (advance) { + if (tp->c_cur_span >= tp->dd_new_num_spans) { + //TIBET_ERR("TIBETdec: ran out of spans\n"); + return TAPE_E_EOF; + } + (tp->c_cur_span)++; + tp->c_tone_pos = 0; + tp->c_silence_pos_s = 0.0f; + } + + // and again: + _span = tp->dd_new_spans + tp->c_cur_span; + +//printf("c_get_span: _span = %u, type %u\n", _span->id, _span->type); + + *span = _span; + + return TAPE_E_OK; + +} + + +// value will be '0', '1', 'L', or 'S' +// -- a pair of atoms (a.k.a. tonechars): one bit at 1200 baud +static int c_read_1200 (tibet_priv_t *tp, uint8_t *out, uint32_t *cur_span_out) { + + tibet_span_t *span; + int e; + char tc1, tc2; + + *cur_span_out = 0; + tc1 = 0; + tc2 = 0; + + e = c_get_span(tp, &span); + if (TAPE_E_OK != e) { return e; } + + e = c_next_tonechar (tp, &tc1); + if (TAPE_E_OK != e) { return e; } + e = c_next_tonechar (tp, &tc2); + if (TAPE_E_OK != e) { return e; } + + do { + // need a pair of tonechars + *cur_span_out = tp->c_cur_span; + if (tc1 == tc2) { + // OK, tonechars match + *out = tc1; + return TAPE_E_OK; + } + // skip first tonechar, resync and try again + tc1 = tc2; + e = c_next_tonechar (tp, &tc2); + } while (TAPE_E_OK == e); + + return e; + +} + + + + +static int c_get_priv (tibet_t *t, tibet_priv_t **priv, uint8_t enforce_decoded) { + tibet_priv_t *_priv; + /*int e;*/ + _priv = NULL; +//printf("c_get_priv: t->priv = %p, enforce_decoded = %u\n", t->priv, enforce_decoded); + if (NULL == t->priv) { + // check this before malloc, so we don't malloc if error + if ( enforce_decoded ) { + TIBET_ERR("TIBET: attempt to use client functions before decode [priv]"); + return TAPE_E_TIBET_NO_DECODE; + } + c_alloc_priv_if_needed(t); + + /* note that if this is a brand new TIBET instance, a blank + span will be created by default. The decoder needs this + so that it has a span available to attach hint lines to, before a + main data span actually begins. But it is a nuisance for + creating a TIBET file from scratch. */ + /* REVERT ME */ + /*e = c_new_span(t->priv); + if (TAPE_E_OK != e) { + TIBET_FREE(t->priv); + t->priv = NULL; + return e; + }*/ + + } +//printf("lol\n"); + _priv = (tibet_priv_t *) t->priv; + if (enforce_decoded && ! _priv->ds_decoded ) { + TIBET_ERR("TIBET: attempt to use client functions before decode [ds_decoded]"); + return TAPE_E_TIBET_NO_DECODE; + } + *priv = _priv; +//printf("ret OK\n"); + return TAPE_E_OK; +} + + +/* +static int c_check_parity (uint8_t frame, + uint8_t parity_bit, + char parity_mode, // 'E' or 'O' + uint8_t *success_out) { + uint8_t num_ones, i; + num_ones = 0; + // include bit 7 even if in 7-bit mode, it'll be zero in 7-bit mode so it doesn't matter + for (i=0; i < 8; i++) { + num_ones += (frame & 1); + frame >>= 1; + } + num_ones += (parity_bit ? 1 : 0); + *success_out = (parity_mode == 'E') ^ (1 == (num_ones & 1)); + return TAPE_E_OK; +} +*/ + +static tibet_span_t *c_current_span (tibet_priv_t *tp) { + if (0 == tp->dd_new_num_spans) { + return NULL; + } +//printf("c_current_span: %p\n", tp->dd_new_spans + (tp->dd_new_num_spans - 1)); + return tp->dd_new_spans + (tp->dd_new_num_spans - 1); +} + + +// ********************************** +// ***** BEGIN PUBLIC FUNCTIONS ***** +// ********************************** + +/* +int tibet_set_pending_hints (tibet_t *t, tibet_span_hints_t *h) { + c_get_priv(t, &tp, 0); + tp->ds_hints_pending = *h; +} +*/ + +/* +int tibet_get_pending_data_span (tibet_t *t, tibet_span_t *span_out) { + tibet_priv_t *tp; + int e; + *span_out = NULL; + e = c_get_priv (t, &tp, 1); + if (TAPE_E_OK != e) { return e; } + *span_out = &(tp->ds_data_span_pending); + return TAPE_E_OK; +} +*/ + +/* +int tibet_set_pending_data_span_hints (tibet_t *t, tibet_span_hints_t *h) { + tibet_priv_t *tp; + int e; + e = c_get_priv (t, &tp, 1); + if (TAPE_E_OK != e) { return e; } + tp->ds_data_span_pending.hints = *h; + return TAPE_E_OK; +} +*/ + +static uint8_t packet_len_for_framing_code (char fc[4]) { + uint8_t q; + q=1; /* start bit */ + q+=('7'==fc[0])?7:8; /* 7-8 data bits */ + if ('N'!=fc[1]) { q++; } /* 0-1 parity bits */ + q+=('1'==fc[2])?1:2; /* 1-2 stop bits */ + return q; +} + +int tibet_build_output (tibet_t *t, char **opf, size_t *opf_len) { + + uint32_t sn; + uint8_t pass; + size_t pos, len; + int e; + char *buf; + size_t z; + //char *version; + tibet_priv_t *tp; + uint32_t major, minor; + + tp = NULL; + + *opf_len = 0; + c_get_priv(t, &tp, 0); + + e = TAPE_E_OK; + + /* if version is undefined (file empty), use TIBET_VERSION */ + //version = (NULL == tp->dd_version) ? TIBET_VERSION : (tp->dd_version); + //version = tp->ds_have_version ? tp->dd_version : TIBET_VERSION; + + if (tp->ds_have_version) { + major = tp->dd_version_major; + minor = tp->dd_version_minor; + } else { + major = TIBET_VERSION_MAJOR; + minor = TIBET_VERSION_MINOR; + } + + for (pass=0, buf=NULL, len=0; pass < 2; pass++) { + uint8_t packet_len = 10; /* default: 8N1 nominal */ + pos = 0; +/* "tibet 0.4\n\n" */ +#define TIBET_OP_FMT_HEADER "%s %u.%u\n\n" + z = TAPE_SCPRINTF(TIBET_OP_FMT_HEADER, + TIBET_K_TIBET, + major, + minor); + if ( 1 == pass ) { + TAPE_SNPRINTF (buf+pos, + len+1-pos, + z, + TIBET_OP_FMT_HEADER, + TIBET_K_TIBET, + major, + minor); + } + pos += z; + /*for (sn=0; (TAPE_E_OK == e) && (sn < (tp->dd_num_spans - 1)); sn++) {*/ + for (sn=0; (TAPE_E_OK == e) && (sn < tp->dd_new_num_spans); sn++) { + tibet_span_t *span; + float f; + const char *s; + uint32_t tn; + span = tp->dd_new_spans + sn; +/* "leader 1234\n\n" */ +#define TIBET_OP_FMT_LEADER "%s %d\n\n" + if (TIBET_SPAN_LEADER == span->type) { + z = TAPE_SCPRINTF(TIBET_OP_FMT_LEADER, TIBET_K_LEADER, span->num_tones); + if ( 1 == pass ) { + TAPE_SNPRINTF (buf+pos, len+1-pos, z, TIBET_OP_FMT_LEADER, TIBET_K_LEADER, span->num_tones); + } + pos += z; +/* "silence 1.2345\n\n" */ +#define TIBET_OP_FMT_SILENCE "%s %f\n\n" + } else if (TIBET_SPAN_SILENT == span->type) { + f = span->opo_silence_duration_secs; + z = TAPE_SCPRINTF(TIBET_OP_FMT_SILENCE, TIBET_K_SILENCE, f); + if ( 1 == pass ) { + TAPE_SNPRINTF (buf+pos, len+1-pos, z, TIBET_OP_FMT_SILENCE, TIBET_K_SILENCE, f); + } + pos += z; +/* "/baud 1200\n" */ +#define TIBET_OP_FMT_BAUD "%s %d\n" + } else if (TIBET_SPAN_DATA == span->type) { + if (span->hints.have_baud) { + z = TAPE_SCPRINTF(TIBET_OP_FMT_BAUD, TIBET_K_BAUD, span->hints.baud); + if ( 1 == pass ) { + TAPE_SNPRINTF (buf+pos, + len+1-pos, + z, + TIBET_OP_FMT_BAUD, + TIBET_K_BAUD, + span->hints.baud); + } + pos += z; + } +/* "/framing 8N1\n" */ +#define TIBET_OP_FMT_FRAMING "%s %s\n" + if (span->hints.have_framing) { + z = TAPE_SCPRINTF(TIBET_OP_FMT_FRAMING, TIBET_K_FRAMING, span->hints.framing); + if ( 1 == pass ) { + TAPE_SNPRINTF (buf+pos, + len+1-pos, + z, + TIBET_OP_FMT_FRAMING, + TIBET_K_FRAMING, + span->hints.framing); + } + pos += z; + /* TOHv4.1, for superior line formatting */ + packet_len = packet_len_for_framing_code(span->hints.framing); + } +/* "/speed 1.04\n" */ +#define TIBET_OP_FMT_SPEED "%s %f\n" + if (span->hints.have_speed) { + z = TAPE_SCPRINTF(TIBET_OP_FMT_SPEED, TIBET_K_SPEED, span->hints.speed); + if ( 1 == pass ) { + TAPE_SNPRINTF (buf+pos, + len+1-pos, + z, + TIBET_OP_FMT_SPEED, + TIBET_K_SPEED, + span->hints.speed); + } + pos += z; + } +/* "/time 25.3\n" */ +#define TIBET_OP_FMT_TIME "%s %f\n" + if (span->hints.have_time) { + z = TAPE_SCPRINTF(TIBET_OP_FMT_TIME, TIBET_K_TIME, span->hints.time); + if ( 1 == pass ) { + TAPE_SNPRINTF (buf+pos, + len+1-pos, + z, + TIBET_OP_FMT_TIME, + TIBET_K_TIME, + span->hints.time); + } + pos += z; + } +/* "/phase 180\n" */ +#define TIBET_OP_FMT_PHASE "%s %u\n" + if (span->hints.have_phase) { + z = TAPE_SCPRINTF(TIBET_OP_FMT_PHASE, TIBET_K_PHASE, span->hints.phase); + if ( 1 == pass ) { + TAPE_SNPRINTF (buf+pos, + len+1-pos, + z, + TIBET_OP_FMT_PHASE, + TIBET_K_PHASE, + span->hints.phase); + } + pos += z; + } + s = span->is_squawk ? TIBET_K_SQUAWK : TIBET_K_DATA; +/* "data\n" or "squawk\n" */ +#define TIBET_OP_FMT_DATA_PFX "%s\n" + z = TAPE_SCPRINTF(TIBET_OP_FMT_DATA_PFX, s); + if ( 1 == pass ) { + TAPE_SNPRINTF (buf+pos, len+1-pos, z, TIBET_OP_FMT_DATA_PFX, s); + } + pos += z; + for (tn = 0; tn < span->num_tones; tn++) { + /* cosmetic hack: newline before first start bit */ + if (1 == pass) { + buf[pos] = span->tones[tn]; + } + pos++; + /* TOHv4.1: better line breaks */ + if (((tn % (packet_len * 2)) == ((packet_len * 2) - 1)) && (tn < (span->num_tones - 1))) { + if (1 == pass) { buf[pos] = '\n'; } + pos++; + } + } +/* "end\n\n" */ +#define TIBET_OP_FMT_DATA_SFX "\n%s\n\n" + z = TAPE_SCPRINTF(TIBET_OP_FMT_DATA_SFX, TIBET_K_END); + if ( 1 == pass ) { + TAPE_SNPRINTF (buf+pos, len+1-pos, z, TIBET_OP_FMT_DATA_SFX, TIBET_K_END); + } + pos += z; + } else { + log_warn("TIBET: BUG: span to write (#%u) has illegal type %u", sn, span->type); + e = TAPE_E_BUG; + break; + } + } /* next span */ + if (TAPE_E_OK != e) { break; } + len = pos; + if (len >= TIBET_OUTPUT_MAX_LEN) { + log_warn("TIBET: maximum output length exceeded"); + e = TAPE_E_TIBET_OP_LEN; + break; + } + if ( 0 == pass ) { + buf = TIBET_MALLOC(len + 1); + if (NULL == buf) { + log_warn("TIBET: build output, pass one: malloc failed"); + e = TAPE_E_BUG; + break; + } + } + } /* next pass */ + if ((TAPE_E_OK != e) && (buf != NULL)) { + /* shouldn't happen, but just to be sure */ + TIBET_FREE(buf); + buf = NULL; + } + *opf = buf; + *opf_len = len; + return e; +} + + +// fetch current span +tibet_span_t *tibet_current_final_span (tibet_t *t) { + tibet_priv_t *tp; + /*tp = (tibet_priv_t *) t->priv;*/ + tp = NULL; + c_get_priv(t, &tp, 0); + return c_current_span(tp); +} + +#define SPANS_ALLOC_DELTA 5 + +// advance span +static int c_new_span (tibet_priv_t *tp) { + + tibet_span_t *spans, *span; + uint32_t newsize; + + if (tp->dd_new_num_spans >= tp->ds_new_spans_alloc) { + // realloc + newsize = tp->ds_new_spans_alloc + SPANS_ALLOC_DELTA; + if (newsize > TIBET_MAX_SPANS) { + TIBET_ERR("TIBET: line %u: too many spans\n", tp->ds_linenum+1); + return TAPE_E_TIBET_TOO_MANY_SPANS; + } + spans = TIBET_REALLOC (tp->dd_new_spans, sizeof(tibet_span_t) * newsize); + if (NULL == spans) { + TIBET_ERR("TIBET: line %u: could not realloc spans (newsize %u)\n", + tp->ds_linenum+1, newsize); + // don't free + return TAPE_E_MALLOC; + } + memset(spans + tp->dd_new_num_spans, + 0, + (sizeof(tibet_span_t) * (newsize - tp->dd_new_num_spans))); + tp->ds_new_spans_alloc = newsize; + tp->dd_new_spans = spans; + } + + span = tp->dd_new_spans + tp->dd_new_num_spans; + + memset(span, 0, sizeof(tibet_span_t)); + + span->id = tp->dd_new_num_spans; + +//printf("c_new_span: id %u\n", span->id); + + (tp->dd_new_num_spans)++; + + return TAPE_E_OK; + +} + + + +int tibet_read_tonechar (tibet_t *t, char *value_out) { + + tibet_span_t *span; + int e; + char tc; + tibet_priv_t *tp; + + tc = '\0'; + + /*e = c_get_priv (t, &tp, 1);*/ // decode required + e = c_get_priv (t, &tp, 0); + if (TAPE_E_OK != e) { return e; } + + e = c_get_span (tp, &span); + if (TAPE_E_OK != e) { return e; } + + e = c_next_tonechar (tp, &tc); + if (TAPE_E_OK != e) { return e; } + + *value_out = tc; + + return TAPE_E_OK; + +} + + +// value will be '0', '1', 'L', or 'S' +int tibet_read_bit (tibet_t *t, uint8_t baud300, char *bit_out) { + + uint8_t tone0, tval0; + uint32_t cur_span; + uint8_t pairs_to_read; + uint8_t n; + int e; + tibet_priv_t *tp; + + /*e = c_get_priv (t, &tp, 1);*/ // decode required + e = c_get_priv (t, &tp, 0); + if (TAPE_E_OK != e) { return e; } + + cur_span = 0; + pairs_to_read = baud300 ? 4 : 1; + tval0 = '!'; + tone0 = '!'; + + // at 1200 baud, this just reads one tonepair + + // at 300 baud it will read four tonepairs, and check them for consistency + // if they are inconsistent it will reset the count to 0 and read another + // four tonepairs, starting at the first mismatching tonepair, until it wins + for (n=0; n < pairs_to_read; n++) { + uint8_t tone, tval; + e = c_read_1200 (tp, &tone, &cur_span); + if (TAPE_E_OK != e) { return e; } + if (tone == 'L') { + tval = '1'; // for comparison purposes, 'L' becomes '1' + } else { + tval = tone; + } + if (0==n) { + tval0 = tval; + tone0 = tone; + } else if (tval != tval0) { // 300 baud only + tval0 = tval; + tone0 = tone; // begin again + n=0; + } + } + + // we specifically return the first value received, + // in case we had to make up the end using leader + + *bit_out = tone0; + + return TAPE_E_OK; + +} + + + + +int tibet_rewind (tibet_t *t) { + + //tibet_span_t *dummy; + int e; + tibet_priv_t *tp; + + /*e = c_get_priv (t, &tp, 1);*/ // decode required + e = c_get_priv (t, &tp, 0); + if (TAPE_E_OK != e) { return e; } + + tp->c_tone_pos = 0; + tp->c_silence_pos_s = 0.0f; + tp->c_cur_span = 0; + + return TAPE_E_OK; + +} + + +//int tibet_get_version (tibet_t *t, char **version) { +// TOHv3.2: major/minor now +int tibet_get_version (tibet_t *t, uint32_t *major_out, uint32_t *minor_out) { + tibet_priv_t *priv; + int e; + e = c_get_priv (t, &priv, 0); + if (TAPE_E_OK != e) { return e; } + *major_out = priv->dd_version_major; + *minor_out = priv->dd_version_minor; + return TAPE_E_OK; +} + + + +int tibet_decode (char *buf, size_t len, tibet_t *t) { + + int e; + size_t n; + size_t line_start; + tibet_priv_t *tp; + tibet_span_hints_t active_hints; + tibet_span_t pending_data_span; + + line_start = 0; + + memset (t, 0, sizeof(tibet_t)); + memset (&pending_data_span, 0, sizeof(tibet_span_t)); + memset (&active_hints, 0, sizeof(tibet_span_hints_t)); + + tp = NULL; + + e = c_get_priv(t, &tp, 0); + if (TAPE_E_OK != e) { return e; } + + for (n=0; n < len; n++) { + + unsigned char c; + char final, nl; + size_t linelen; + + c = (unsigned char) buf[n]; + + if (((c < 0x20) || (c > 0x7e)) && (c != '\n')) { + TIBET_ERR("TIBET: Illegal character 0x%x\n", 0xff&c); + e = TAPE_E_TIBET_BADCHAR; + break; + } + + final = (n == (len-1)); + nl = ('\n' == c); + + if (nl || final) { + // end of line + linelen = n - line_start; + if (final && ! nl) { + linelen++; // deal with final line stickiness + } + e = parse_line (tp, buf + line_start, linelen, &pending_data_span, &active_hints); + line_start = n + 1; // skip newline character + } + + if (TAPE_E_OK != e) { break; } + + } + + if (TAPE_E_OK == e) { + + // ensure we found a version line (unit test: "TIBET:Zero-byte file") + //if (NULL == tp->dd_version) { + if ((0==tp->dd_version_major) && (0==tp->dd_version_minor)) { // TOHv3.2: major/minor now + TIBET_ERR("TIBET: version line not found; this is not a valid TIBET file.\n"); + e = TAPE_E_TIBET_ABSENT_VERSION; + } else if (active_hints.have_time) { // ensure no "dangling" properties + TIBET_ERR("TIBET: %s following final span\n", TIBET_K_TIME); + e = TAPE_E_TIBET_DANGLING_TIME; + } else if (active_hints.have_phase) { + TIBET_ERR("TIBET: %s following final span\n", TIBET_K_PHASE); + e = TAPE_E_TIBET_DANGLING_PHASE; + } else if (active_hints.have_speed) { + TIBET_ERR("TIBET: %s following final span\n", TIBET_K_SPEED); + e = TAPE_E_TIBET_DANGLING_SPEED; + } else if (active_hints.have_baud) { + TIBET_ERR("TIBET: %s following final span\n", TIBET_K_BAUD); + e = TAPE_E_TIBET_DANGLING_BAUD; + } else if (active_hints.have_framing) { + TIBET_ERR("TIBET: %s following final span\n", TIBET_K_FRAMING); + e = TAPE_E_TIBET_DANGLING_FRAMING; + } + + } + + if (TAPE_E_OK != e) { + tibet_finish(t); + } else { + tp->ds_decoded = 1; + } + + return e; + +} + + + +void tibet_finish (tibet_t *t) { + uint32_t n; + int e; + tibet_priv_t *tp; + e = c_get_priv(t, &tp, 0); + if (TAPE_E_OK != e) { return; } + for (n=0; n < tp->dd_new_num_spans; n++) { + if (NULL != tp->dd_new_spans[n].tones) { + TIBET_FREE(tp->dd_new_spans[n].tones); + } + } + if (NULL != tp->dd_new_spans) { + TIBET_FREE(tp->dd_new_spans); + } + // TOHv3.2: removed + //if (NULL != tp->dd_version) { + // TIBET_FREE(tp->dd_version); + //} + if (NULL != t->priv) { + TIBET_FREE(t->priv); + } + memset(t, 0, sizeof(tibet_t)); +} + + +static int c_alloc_priv_if_needed(tibet_t *t) { + if (NULL == t->priv) { + t->priv = malloc(sizeof(tibet_priv_t)); + if (NULL == t->priv) { + TIBET_ERR("TIBET: cannot alloc NULL in->priv\n"); + return TAPE_E_MALLOC; + } + memset(t->priv, 0, sizeof(tibet_priv_t)); + } + return TAPE_E_OK; +} + + + +int tibet_clone (tibet_t *out, tibet_t *in) { + + tibet_priv_t *tp_in, *tp_out; + int e; + uint32_t n; + + out->priv = NULL; + /* TOHv3: sanity */ + if (NULL == in) { + TIBET_ERR("TIBET: BUG: attempt to clone NULL instance!\n"); + return TAPE_E_BUG; + } + if (NULL == in->priv) { + TIBET_ERR("TIBET: BUG: attempt to clone instance w/NULL priv!\n"); + return TAPE_E_BUG; + } + e = c_get_priv(in, &tp_in, 0); + if (TAPE_E_OK != e) { return e; } + /* TOHv3: make sure input TIBET has a version set */ + e = c_set_version_if_missing(in); + if (TAPE_E_OK != e) { return e; } + if (NULL == tp_in) { + TIBET_ERR("TIBET: attempt to clone a NULL instance\n"); + return TAPE_E_BUG; + } + e = c_get_priv(out, &tp_out, 0); + if (TAPE_E_OK != e) { return e; } + //tp_out = out->priv; + memcpy(tp_out, tp_in, sizeof(tibet_priv_t)); + tp_out->dd_new_spans = NULL; + + /* TOHv3: make sure tp_in alloced size record is sane */ + if (tp_in->ds_new_spans_alloc < tp_in->dd_new_num_spans) { + log_warn("TIBET: clone: BUG: ds_new_spans_alloc < dd_num_spans somehow\n"); + tibet_finish(out); + return TAPE_E_BUG; + } + /* TOHv3: bugfix: alloc value was not accurate + * on the destination TIBET. We were allocing just dd_num_spans, + * but leaving tp_out->ds_new_spans_alloc having the same value as it was + * on tp_in; clearly an inconsistency, and a problem if anything + * else gets appended to tp_out later. We'll just alloc the full + * tp_in->ds_new_spans_alloc here, then tp_out->ds_new_spans_alloc is also + * accurate. */ + tp_out->dd_new_spans = malloc(tp_in->ds_new_spans_alloc * sizeof(tibet_span_t)); + if (NULL == tp_out->dd_new_spans) { + TIBET_ERR("TIBET: could not malloc clone spans\n"); + tibet_finish(out); + return TAPE_E_MALLOC; + } + /* TOHv3: insurance: make sure the whole allocated tp_out->dd_new_spans + * is zeroed (else only tp_in->dd_num_spans worth will be meaningfully + * populated, leaving an uninitialised disaster area at the end of + * the buffer) */ + memset(tp_out->dd_new_spans, + 0, + tp_in->ds_new_spans_alloc * sizeof(tibet_span_t)); + /* now copy just dd_num_spans across */ + // memcpy(tp_out->dd_new_spans, + // tp_in->dd_new_spans, + // tp_in->dd_new_num_spans * sizeof(tibet_span_t)); + // for (n=0; n < tp_in->dd_new_num_spans; n++) { + // tibet_span_t *span; + // tp_out->dd_new_spans[n] = + // span = tp_out->dd_new_spans + n; + // span->tones = NULL; + // } + for (n=0; n < tp_in->dd_new_num_spans; n++) { + e = tibet_clone_span (tp_out->dd_new_spans + n, tp_in->dd_new_spans + n); // TOHv4.1 + if (TAPE_E_OK != e) { + tibet_finish(out); + return e; + } + } + return TAPE_E_OK; +} + + +// TOHv4.1 +int tibet_clone_span (tibet_span_t *out, tibet_span_t *in) { + *out = *in; + out->tones = NULL; + if (in->tones != NULL) { + out->tones = malloc(in->num_tones_alloc); + if (NULL == out->tones) { + TIBET_ERR("TIBET: could not malloc clone span tones\n"); + return TAPE_E_MALLOC; + } + memcpy(out->tones, in->tones, in->num_tones_alloc); + } + return TAPE_E_OK; +} + + +int tibet_append_tonechar (tibet_t *t, char c, tibet_span_t *pending_span) { + tibet_priv_t *p; + int e; + /* make sure we have a version */ + /* TOHv3: add dedicated function for this */ + e = c_set_version_if_missing(t); + if (TAPE_E_OK != e) { return e; } + p = (tibet_priv_t *) t->priv; + return append_tonechar(p, c, pending_span); +} + +/* TOHv3 */ +static int c_set_version_if_missing (tibet_t *t) { + tibet_priv_t *p; + //size_t vl; + int e; + e = c_get_priv(t, &p, 0); + if (TAPE_E_OK != e) { return e; } + /* make sure we have a version */ + if ((0==p->dd_version_major)&&(0==p->dd_version_minor)) { // TOHv3.2: major/minor + p->dd_version_major = TIBET_VERSION_MAJOR; + p->dd_version_minor = TIBET_VERSION_MINOR; + } + return TAPE_E_OK; +} + + +int tibet_append_leader_span (tibet_t *t, uint32_t num_2400ths, tibet_span_hints_t *hints) { + tibet_priv_t *tp; + int e; + e = c_get_priv(t, &tp, 0); + if (TAPE_E_OK != e) { return e; } + return append_leader_span(tp, num_2400ths, hints); +} + +// hints will be copied, and zeroed on return +static int append_leader_span (tibet_priv_t *tp, uint32_t num_2400ths, tibet_span_hints_t *hints) { + int e; + tibet_span_t *span; + +//printf("tibet_append_leader_span\n"); /*, ");*/ + + e = c_new_span(tp); + if (TAPE_E_OK != e) { return e; } + span = tp->dd_new_spans + (tp->dd_new_num_spans - 1); +/*printf("id %u\n", span->id);*/ + //span->hints.have_speed = 1; + //span->hints.speed = speed; + span->hints = *hints; + span->num_tones = num_2400ths; + span->type = TIBET_SPAN_LEADER; + span->hints = *hints; + memset(hints, 0, sizeof(tibet_span_hints_t)); + return TAPE_E_OK; +} + + +int tibet_append_silent_span (tibet_t *t, float len_s, tibet_span_hints_t *hints) { + int e; + tibet_priv_t *tp; + e = c_get_priv(t, &tp, 0); + if (TAPE_E_OK != e) { return e; } + return append_silent_span (tp, len_s, hints); +} + + +// hints will be copied, and zeroed on return +static int append_silent_span (tibet_priv_t *tp, float len_s, tibet_span_hints_t *hints) { + int e; + tibet_span_t *span; + e = c_new_span(tp); + if (TAPE_E_OK != e) { return e; } + span = tp->dd_new_spans + (tp->dd_new_num_spans - 1); + span->type = TIBET_SPAN_SILENT; + span->opo_silence_duration_secs = len_s; + span->hints = *hints; + memset(hints, 0, sizeof(tibet_span_hints_t)); + return TAPE_E_OK; +} + + +/* caution! heap memory on pending->tones will be stolen, + and pending itself will never be the same again! */ +int tibet_append_data_span (tibet_t *t, tibet_span_t *pending) { + int e; + tibet_priv_t *tp; + e = c_get_priv(t, &tp, 0); + if (TAPE_E_OK != e) { return e; } + return append_data_span(tp, pending); +} + + +static int append_data_span (tibet_priv_t *priv, tibet_span_t *pending) { + int e; + tibet_span_t *span; + + if (pending->type != TIBET_SPAN_DATA) { + log_warn("TIBET: BUG: pending span has bad type (%u)\n", pending->type); + return TAPE_E_BUG; + } +//printf("tibet_append_data_span\n"); + + e = c_new_span(priv); + if (TAPE_E_OK != e) { return e; } + /* + e = c_get_span(tp, &span); + if (TAPE_E_OK != e) { return e; } + */ + span = priv->dd_new_spans + (priv->dd_new_num_spans - 1); + memcpy(span, pending, sizeof(tibet_span_t)); + /* let's just make sure there are no little misunderstandings */ + memset(pending, 0, sizeof(tibet_span_t)); + return TAPE_E_OK; +} + + + +int tibet_have_any_data (tibet_t *t, uint8_t *have_data_out) { + int e; + uint32_t i; + tibet_priv_t *tp; + *have_data_out = 0; + /* we don't want a call to c_get_priv() to cause a priv instance + * to be created, if we don't need one, so this is a rare case of + * checking t->priv first */ + if (NULL == t->priv) { return TAPE_E_OK; } + e = c_get_priv(t, &tp, 0); + if (TAPE_E_OK != e) { return e; } + for (i=0; i < tp->dd_new_num_spans; i++) { + if (TIBET_SPAN_DATA == tp->dd_new_spans[i].type) { + *have_data_out = 1; + return TAPE_E_OK; + } + } + return e; +} + + +#ifdef BUILD_TAPE_READ_POS_TO_EOF_ON_WRITE +int tibet_ffwd_to_end (tibet_t *t) { + + int e; + tibet_priv_t *tp; + + e = c_get_priv(t, &tp, 0); + if (TAPE_E_OK != e) { return e; } + + /* force read pointer past end of file, EOF */ + tp->c_cur_span = tp->dd_new_num_spans; + return TAPE_E_OK; + +} +#endif + + +/* **************** */ + + +/* the following functions are unused by B-Em, so they + are uncompiled for this application ... */ + +#if 0 + + +int tibet_peek_span_type (tibet_t *t, uint8_t *span_type_out) { + int e; + tibet_span_t *span; + tibet_priv_t *tp; + + *span_type_out = TIBET_SPAN_INVALID; + + /*e = c_get_priv (t, &tp, 1);*/ // decode required + e = c_get_priv (t, &tp, 0); + if (TAPE_E_OK != e) { return e; } + e = c_get_span (tp, &span); + if (TAPE_E_OK != e) { return e; } + *span_type_out = span->type; + return TAPE_E_OK; +} + + +int tibet_peek_span_hint_framing_data_bits (tibet_t *t, uint8_t *num_data_bits_out) { + + int e; + tibet_span_t *span; + tibet_priv_t *tp; + + *num_data_bits_out = 8; + + /*e = c_get_priv (t, &tp, 1);*/ // decode required + e = c_get_priv (t, &tp, 0); + if (TAPE_E_OK != e) { return e; } + e = c_get_span (tp, &span); + if (TAPE_E_OK != e) { return e; } + + if ( span->have_hint_framing ) { + *num_data_bits_out = (span->hint_framing[0] == '8') ? 8 : 7; + } + return TAPE_E_OK; +} + + +int tibet_peek_span_hint_framing_parity (tibet_t *t, char *parity_out) { + + int e; + tibet_span_t *span; + tibet_priv_t *tp; + + *parity_out = 'N'; + + /*e = c_get_priv (t, &tp, 1);*/ // decode required + e = c_get_priv (t, &tp, 0); + if (TAPE_E_OK != e) { return e; } + e = c_get_span (tp, &span); + if (TAPE_E_OK != e) { return e; } + + if ( span->have_hint_framing ) { + *parity_out = span->hint_framing[1]; + } + return TAPE_E_OK; +} + + +int tibet_peek_span_hint_framing_stop_bits (tibet_t *t, uint8_t *num_stop_bits_out) { + + int e; + tibet_span_t *span; + tibet_priv_t *tp; + + *num_stop_bits_out = 1; + + /*e = c_get_priv (t, &tp, 1);*/ // decode required + e = c_get_priv (t, &tp, 0); + if (TAPE_E_OK != e) { return e; } + e = c_get_span (tp, &span); + if (TAPE_E_OK != e) { return e; } + + if ( span->have_hint_framing ) { + *num_stop_bits_out = (span->hint_framing[2] == '1') ? 1 : 2; + } + return TAPE_E_OK; + +} + + +int tibet_peek_span_hint_baud (tibet_t *t, uint32_t *baud_out) { + + int e; + tibet_span_t *span; + tibet_priv_t *tp; + + *baud_out = 1200; + + /*e = c_get_priv (t, &tp, 1);*/ // decode required + e = c_get_priv (t, &tp, 0); + if (TAPE_E_OK != e) { return e; } + e = c_get_span (tp, &span); + if (TAPE_E_OK != e) { return e; } + + if ( span->have_hint_baud ) { + *baud_out = span->hint_baud; + } + return TAPE_E_OK; + +} + + + +int tibet_peek_span_id (tibet_t *t, uint32_t *span_id_out) { + + int e; + tibet_span_t *span; + tibet_priv_t *tp; + + *span_id_out = 0xffffffff; + + /*e = c_get_priv (t, &tp, 1);*/ // decode required + e = c_get_priv (t, &tp, 0); + if (TAPE_E_OK != e) { return e; } + e = c_get_span (tp, &span); + if (TAPE_E_OK != e) { return e; } + + *span_id_out = span->id; + return TAPE_E_OK; +} +#endif /* 0 */ + +/* +int tibet_read_frame (tibet_t *t, + uint8_t num_data_bits, + char parity, // 'N', 'O' or 'E' + uint8_t num_stop_bits, + uint8_t baud300, + uint8_t ignore_stop2_as_acia_does, + uint8_t *frame_out, + uint8_t *framing_err_out, + uint8_t *parity_err_out) { + + int e; + uint8_t n, parity_ok; + uint8_t bit, frame; + tibet_priv_t *tp; + + e = c_get_priv (t, &tp, 1); // decode required + if (TAPE_E_OK != e) { return e; } + + *parity_err_out = 0; + *framing_err_out = 0; + *frame_out = 0; + + // await start bit + bit = 0; + do { + e = tibet_read_bit (t, baud300, &bit); + if (TAPE_E_OK != e) { return e; } + } while (bit != '0'); + + frame = 0; + for (n=0; n < num_data_bits; n++) { + e = tibet_read_bit (t, baud300, &bit); + if (TAPE_E_OK != e) { return e; } // might be EOF + frame = (frame >> 1) & 0x7f; + frame |= ('0'==bit) ? 0 : 0x80; + } + if (num_data_bits == 7) { + frame = (frame >> 1) & 0x7f; + } + + *frame_out = frame; + + if (parity != 'N') { + e = tibet_read_bit (t, baud300, &bit); + if (TAPE_E_OK != e) { return e; } // might be EOF + e = c_check_parity (frame, bit, parity, &parity_ok); + if (TAPE_E_OK != e) { return e; } + *parity_err_out = parity_ok ? 0 : 1; + } + + // ACIA seems just to ignore the second stop bit. + // if the second stop bit is incorrectly a 1, then the ACIA + + + // just treats it as a new start bit. + if (ignore_stop2_as_acia_does) { + num_stop_bits = 1; + } + + // note that MOS doesn't care about framing errors! + for (n=0; n < num_stop_bits; n++) { + e = tibet_read_bit (t, baud300, &bit); + if (TAPE_E_OK != e) { return e; } // might be EOF + if ((bit != '1') && (bit != 'L')) { + *framing_err_out = 1; + break; // ? quit if first stop is wrong? + } + } + + return TAPE_E_OK; + +} +*/ + diff -Nu b-em-40246d4-vanilla/src/tibet.h b-em-40246d4-TOHv4.2/src/tibet.h --- b-em-40246d4-vanilla/src/tibet.h 1970-01-01 01:00:00.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/tibet.h 2025-07-06 14:17:40.701657578 +0100 @@ -0,0 +1,151 @@ +#ifndef __INC_TIBET_H +#define __INC_TIBET_H + +#define TIBET_SPAN_INVALID 0 +#define TIBET_SPAN_SILENT 1 +#define TIBET_SPAN_LEADER 2 +#define TIBET_SPAN_DATA 3 // also squawk + +#define TIBET_ERR log_warn +#define TIBET_FREE free +#define TIBET_MALLOC malloc +#define TIBET_REALLOC realloc + +// TOHv3.2: version number is now expressed as integer pair +#define TIBET_VERSION_MAJOR 0 +#define TIBET_VERSION_MINOR 5 +//#define TIBET_VERSION "0.4" + +#define TIBET_VERSION_MAX_LEN 11 // one decimal point + ten digits + +#define TIBET_MAX_SPANS 1000000 + +#include +#include + +#include "tape2.h" /* for BUILD_TAPE_READ_POS_TO_EOF_ON_WRITE */ + +typedef struct tibet_span_hints_s { + // these will be checked when the + // span type arrives, and if any are incompatible + // then an error will be generated; also allow us + // to reject duplicates, and client must check them + // to know which fields are valid + char have_baud; + char have_framing; + char have_time; + char have_phase; + char have_speed; + + // hints: + uint32_t baud; + char framing[4]; // null-terminated + float time; + uint32_t phase; + float speed; +} tibet_span_hints_t; + + +// attributes will be populated on here as we go, +// then if the span type arrives and they're not +// compatible, we reject the file at that point: +typedef struct tibet_span_s { + + uint32_t id; + + // type: + char type; // TIBET_SPAN_... + char is_squawk; + + // 1200ths (for data/squawk): + char *tones; + uint32_t num_tones; + uint32_t num_tones_alloc; + + /* for silent spans (OUTPUT ONLY [opo]) */ + float opo_silence_duration_secs; + + tibet_span_hints_t hints; + +} tibet_span_t; + + +typedef struct tibet_priv_s { + + // dd_ = decoded data: + tibet_span_t *dd_new_spans; + uint32_t dd_new_num_spans; + + // TOHv3.2: string dd_version removed, now parse version to a pair of integers + uint32_t dd_version_major; + uint32_t dd_version_minor; + //char *dd_version; + + // ds_ = decoding state: + char ds_state; + uint32_t ds_linenum; + uint32_t ds_new_spans_alloc; + uint8_t ds_decoded; + uint8_t ds_have_version; + + // client state: + uint32_t c_cur_span; + uint32_t c_tone_pos; // playback pointer for data and leader spans + // TOH-3.2: playback pointer for silent spans + // use double precision, as this is an accumulator + double c_silence_pos_s; + char c_prev_tonechar; + +} tibet_priv_t; + +typedef struct tibet_s { + void *priv; +} tibet_t; + +int tibet_decode (char *buf, size_t len, tibet_t *tibet); +void tibet_finish (tibet_t *tibet); + +/* +int tibet_peek_span_type (tibet_t *t, uint8_t *span_type_out); +int tibet_peek_span_hint_framing_data_bits (tibet_t *t, uint8_t *num_data_bits_out); +int tibet_peek_span_hint_framing_parity (tibet_t *t, char *parity_out); +int tibet_peek_span_hint_framing_stop_bits (tibet_t *t, uint8_t *num_stop_bits_out); +int tibet_peek_span_hint_baud (tibet_t *t, uint32_t *baud_out); +int tibet_peek_span_id (tibet_t *d, uint32_t *span_id_out); +*/ +int tibet_peek_eof (tibet_t *tp); + +int tibet_have_any_data (tibet_t *t, uint8_t *have_data_out); + +/* these modify client state and call stuff like c_get_span(), + * so they're effectively also c_...() functions? */ +int tibet_read_tonechar (tibet_t *t, char *value_out); +int tibet_read_bit (tibet_t *t, uint8_t baud300, char *bit_out); +int tibet_rewind (tibet_t *t); +int tibet_get_version (tibet_t *t, uint32_t *major_out, uint32_t *minor_out); +int tibet_clone (tibet_t *in, tibet_t *out); + +/* made public in TOHv3 */ +tibet_span_t *tibet_current_final_span (tibet_t *t); +/*int tibet_new_span (tibet_t *t);*/ /*, int type);*/ + +/* added in TOHv3 */ +int tibet_build_output (tibet_t *t, char **opf, size_t *opf_len); +//int tibet_append_tonechar (tibet_t *t, char c); +int tibet_append_tonechar (tibet_t *t, char c, tibet_span_t *pending_span); +/* +int tibet_set_pending_data_span_hints (tibet_t *t, tibet_span_hints_t *h); +*/ + +/* new interface ... */ +int tibet_append_leader_span (tibet_t *t, uint32_t num_2400ths, tibet_span_hints_t *hints); +int tibet_append_silent_span (tibet_t *t, float len_s, tibet_span_hints_t *hints); +int tibet_append_data_span (tibet_t *t, tibet_span_t *pending) ; /* pending = data+hints */ + +#ifdef BUILD_TAPE_READ_POS_TO_EOF_ON_WRITE +int tibet_ffwd_to_end (tibet_t *t); +#endif + +int tibet_clone_span (tibet_span_t *out, tibet_span_t *in); // TOHv4.1 + +#endif /* __INC_TIBET_H */ diff -Nu b-em-40246d4-vanilla/src/uef.c b-em-40246d4-TOHv4.2/src/uef.c --- b-em-40246d4-vanilla/src/uef.c 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/uef.c 2025-07-06 14:17:40.701657578 +0100 @@ -1,398 +1,2188 @@ /*B-em v2.2 by Tom Walker UEF/HQ-UEF tape support*/ + +/* - TOHv3, TOHv4 overhaul by 'Diminished' */ + +#ifndef TAPE_H_UEF +#define TAPE_H_UEF #include #include +#include #include "b-em.h" #include "sysacia.h" #include "csw.h" #include "uef.h" #include "tape.h" -int pps; -gzFile uef_f = NULL; +#define UEF_REJECT_IF_TRUNCATED +#define UEF_REJECT_IF_UNKNOWN_CHUNK + +//#define UEF_WRITE_REJECT_NONSTD_BAUD + + +static int chunks_decode (uint8_t *buf, + uint32_t len, + uef_state_t *uef, + uint8_t reject_if_truncated, + uint8_t reject_if_unknown_chunk); +static int uef_parse_global_chunks (uef_state_t *u) ; +static int chunk_verify_length (uef_chunk_t *c) ; +static int chunks_verify_lengths (uef_chunk_t *chunks, int32_t num_chunks); +static int compute_chunk_102_data_len (uint32_t chunk_len, + uint8_t data0, + uint32_t *len_bytes_out, + uint32_t *len_bits_out); +static uint8_t compute_parity_bit (uint8_t data, uint8_t num_data_bits, char parity); +static uint8_t valid_chunktype (uint16_t t); +static int chunk_114_next_cyc (uef_bitsource_t *src, + uint32_t chunk_len, + uint8_t *data, + uint8_t *cyc_out); +static int reload_reservoir (uef_state_t *u, uint8_t baud300); +static float uef_read_float (uint8_t b[4]); +static uint64_t reservoir_make_value (uint16_t v, uint8_t baud300); +static void init_bitsource (uef_bitsource_t *src); +static int +pre_parse_metadata_chunk (uef_chunk_t *chunk, + /* zero means no prior chunk 130: */ + uint8_t num_tapes_from_prior_chunk_130, + /* likewise: */ + uint8_t num_channels_from_prior_chunk_130, + uef_meta_t *meta_out); +static int uef_verify_meta_chunks (uef_state_t *u); +static uint8_t is_chunk_spent (uef_chunk_t *c, uef_bitsource_t *src); +static void uef_metadata_item_finish (uef_meta_t *m); +static int verify_utf8 (char *utf8, size_t utf8_len); +static int encode_utf8 (uint32_t c, uint8_t *utf8_out, uint8_t *len_bytes_out); /* TOHv4.2 */ int uef_toneon = 0; +int uef_intone = 0; -static int uef_inchunk = 0, uef_chunkid = 0, uef_chunklen = 0; -static int uef_chunkpos = 0, uef_chunkdatabits = 8; -static int uef_startchunk; -static float uef_chunkf; -static int uef_intone = 0; - -void uef_load(const char *fn) -{ - int c; -// printf("OpenUEF %s %08X\n",fn,uef); - if (uef_f) - gzclose(uef_f); - uef_f = gzopen(fn, "rb"); - if (!uef_f) { /*printf("Fail!\n");*/ return; } - for (c = 0; c < 12; c++) - gzgetc(uef_f); - uef_inchunk = uef_chunklen = uef_chunkid = 0; - tapellatch = (1000000 / (1200 / 10)) / 64; - tapelcount = 0; - pps = 120; - csw_ena = 0; -// printf("Tapellatch %i\n",tapellatch); - tape_loaded = 1; -// gzseek(uef,27535,SEEK_SET); -} - -void uef_close() -{ -//printf("CloseUEF\n"); - if (uef_f) - { - gzclose(uef_f); - uef_f = NULL; - } -} - -static int infilenames = 0; -static int uefloop = 0; -static uint8_t fdat; -int ffound; -static void uef_receive(uint8_t val) -{ - uef_toneon--; - if (infilenames) - { - ffound = 1; - fdat = val; -// log_debug("Dat %02X %c\n",val,(val<33)?'.':val); - } - else - { - acia_receive(&sysacia, val); -// log_debug("Dat %02X\n",val); - } -} - -void uef_poll() -{ - int c; - uint32_t templ; - float *tempf; - uint8_t temp; - if (!uef_f) - return; - if (!uef_inchunk) - { - uef_startchunk = 1; -// printf("%i ",gztell(uef)); - gzread(uef_f, &uef_chunkid, 2); - gzread(uef_f, &uef_chunklen, 4); - if (gzeof(uef_f)) - { - gzseek(uef_f, 12, SEEK_SET); - gzread(uef_f, &uef_chunkid, 2); - gzread(uef_f, &uef_chunklen, 4); - uefloop = 1; - } - uef_inchunk = 1; - uef_chunkpos = 0; -// printf("Chunk ID %04X len %i\n",uef_chunkid,uef_chunklen); - } -// else -// printf("Chunk %04X\n",uef_chunkid); - switch (uef_chunkid) - { - case 0x000: /*Origin*/ - for (c = 0; c < uef_chunklen; c++) - gzgetc(uef_f); - uef_inchunk = 0; - return; - - case 0x005: /*Target platform*/ - for (c = 0; c < uef_chunklen; c++) - gzgetc(uef_f); - uef_inchunk = 0; - return; - - case 0x100: /*Raw data*/ - if (uef_startchunk) - { - acia_dcdlow(&sysacia); - uef_startchunk = 0; - } - uef_chunklen--; - if (!uef_chunklen) - { - uef_inchunk = 0; - } - uef_receive(gzgetc(uef_f)); - return; +#define UEF_MAX_FLOAT_GAP 36000.0f /* ten hours */ +#define UEF_BASE_FREQ (TAPE_1200_HZ) - case 0x104: /*Defined data*/ - if (!uef_chunkpos) - { - uef_chunkdatabits = gzgetc(uef_f); - gzgetc(uef_f); - gzgetc(uef_f); - uef_chunklen -= 3; - uef_chunkpos = 1; - acia_dcdlow(&sysacia); - } - else - { - uef_chunklen--; - if (uef_chunklen <= 0) - uef_inchunk = 0; - temp = gzgetc(uef_f); -// printf("%i : %i %02X\n",gztell(uef),uef_chunklen,temp); - if (uef_chunkdatabits == 7) uef_receive(temp & 0x7F); - else uef_receive(temp); - } - return; - case 0x110: /*High tone*/ - uef_toneon = 2; - if (!uef_intone) - { - acia_dcdhigh(&sysacia); - uef_intone = gzgetc(uef_f); - uef_intone |= (gzgetc(uef_f) << 8); - uef_intone /= 20; - if (!uef_intone) uef_intone = 1; -// printf("uef_intone %i\n",uef_intone); - } - else - { - uef_intone--; - if (uef_intone == 0) - { - uef_inchunk = 0; - } - } - return; +/* TOHv4 */ +int uef_get_117_payload_for_nominal_baud (int32_t nominal_baud, + const char **payload_out) { + *payload_out = NULL; //"\x00\x00"; /* warning: violates UEF 0.10 */ + if (1200 == nominal_baud) { + *payload_out = "\xb0\x04"; + } else if (600 == nominal_baud) { + *payload_out = "\x58\x02"; /* warning: violates UEF 0.10 */ + } else if (300 == nominal_baud) { + *payload_out = "\x2c\x01"; + } else if (150 == nominal_baud) { + *payload_out = "\x96\x00"; /* warning: violates UEF 0.10 */ + } else if (75 == nominal_baud) { + *payload_out = "\x4b\x00"; /* warning: violates UEF 0.10 */ + } + if ( ( nominal_baud != 300 ) && ( nominal_baud != 1200 ) ) { +#ifdef UEF_WRITE_REJECT_NONSTD_BAUD + log_warn("tape: uef: WARNING: rejecting nonstandard baud rate %d\n", + nominal_baud); + return TAPE_E_UEF_SAVE_NONSTD_BAUD; +#else + log_warn("tape: uef: WARNING: nonstandard baud rate %d breaks UEF 0.10 spec\n", + nominal_baud); +#endif + } + return TAPE_E_OK; +} + +static void init_bitsource (uef_bitsource_t *src) { + memset(src, 0, sizeof(uef_bitsource_t)); + src->framing.num_data_bits = 8; + src->framing.parity = 'N'; + src->framing.num_stop_bits = 1; + src->chunk_114_pulsewaves[0] = '?'; + src->chunk_114_pulsewaves[1] = '?'; +} + +static void reset_bitsource (uef_bitsource_t *src) { + int32_t b; + /* IMPORTANT: this is the ONLY difference between calling + * init_bitsource() and calling reset_bitsource(); + * reset_bitsource will preserve the baud setting. This is + * the ONLY bitsource field which needs to persist in-between + * chunks. Everything else is reset. */ + b = src->framing.nominal_baud; + init_bitsource(src); + src->framing.nominal_baud = b; +} + +static int consider_chunk (uef_state_t *u, + uint8_t *contains_1200ths_out) { + + uint8_t cb; + uef_chunk_t *c; + uint16_t type; + float f; + uef_bitsource_t *src; + + c = u->chunks + u->cur_chunk; + type = c->type; + src = &(u->bitsrc); + + /* New chunk, so reset the bit source. IMPORTANT: This preserves + * the 300 baud setting; everything else is reset. */ + reset_bitsource(src); + + /* data chunks: len must be > 0: */ + cb = (((0x100==type)||(0x102==type)||(0x104==type)) && (c->len > 0)) + /* carrier tone: num cycles must be > 0: */ + || ((0x110==type) && (tape_read_u16(c->data) != 0)) + /* dummy byte in &111 means there is always data, so: */ + || (0x111==type) + /* integer gap: must have length > 0 */ + || ((0x112==type) && (tape_read_u16(c->data) != 0)) + /* overhaul v2: enforce (gap > 1/1200) */ + || ((0x116==type) && (uef_read_float (c->data) > (1.0/1200.0))) + || ((0x114==type) && (tape_read_u24(c->data) != 0)); + + /* chunk lengths were validated by chunk_verify_length() earlier, + * so we should be fine to go ahead and access the data without + * further length checks. (Yes, this may be stupid design.) */ + + if (0x102 == type) { + u->bitsrc.src_byte_pos = 1; /* skip first byte */ + } else if (0x104 == type) { + /* This is a data chunk with arbitrary framing, so establish + * the framing parameters before we start on the data; + * reminder once again that these are parameters for + * _decoding_the_UEF_. We do NOT program the ACIA with them. + * Only an idiot would do that. + */ + /* validate the framing spec */ + if ((c->data[0] != 8) && (c->data[0] != 7)) { + log_warn("uef: chunk &104: illegal number of data bits (%u, should be 7 or 8)", c->data[0]); + return TAPE_E_UEF_0104_NUM_BITS; + } else if ((c->data[1] != 'N') && (c->data[1] != 'O') && (c->data[1] != 'E')) { + log_warn("uef: chunk &104: illegal parity (&%x, should be &45, &4e or &4f)", c->data[1]); + return TAPE_E_UEF_0104_NUM_BITS; + } else if ((c->data[2] != 1) && (c->data[2] != 2)) { + log_warn("uef: chunk &104: illegal number of stop bits (%u, should be 1 or 2)", c->data[2]); + return TAPE_E_UEF_0104_NUM_STOPS; + } + /* use this framing for this chunk: */ + u->bitsrc.framing.num_data_bits = c->data[0]; + u->bitsrc.framing.parity = c->data[1]; + /* MakeUEF < 2.4 has E and O mixed up */ + if (u->reverse_even_and_odd_parity) { + if ('O'==u->bitsrc.framing.parity) { + u->bitsrc.framing.parity = 'E'; + } else if ('E' == u->bitsrc.framing.parity) { + u->bitsrc.framing.parity = 'O'; + } + } + u->bitsrc.framing.num_stop_bits = c->data[2]; + u->bitsrc.src_byte_pos = 3; /* skip header */ + } else if (0x114 == type) { + if (('P' != c->data[3]) && ('W' != c->data[3])) { + log_warn("uef: chunk &114: illegal pulse/wave char (%x): wanted P or W", c->data[3]); + return TAPE_E_UEF_0114_BAD_PULSEWAVE_1; + } + if (('P' != c->data[4]) && ('W' != c->data[4])) { + log_warn("uef: chunk &114: illegal pulse/wave char (%x): wanted P or W", c->data[4]); + return TAPE_E_UEF_0114_BAD_PULSEWAVE_2; + } + if (('P' == c->data[3]) && ('P' == c->data[4])) { + log_warn("uef: chunk &114: illegal pulse/wave char combination %c, %c", c->data[3], c->data[4]); + + /* TOHv3.2: this restriction has been relaxed. Permit sequence, + * even though it appears to violate the 0.10 UEF specification, as it + * has been observed in the wild: + * + * https://www.stardot.org.uk/forums/viewtopic.php?p=425576#p425576 + */ + + /* return TAPE_E_UEF_0114_BAD_PULSEWAVE_COMBO; */ + + } + u->bitsrc.chunk_114_total_cycs = tape_read_u24(c->data); + u->bitsrc.chunk_114_pulsewaves[0] = c->data[3]; + u->bitsrc.chunk_114_pulsewaves[1] = c->data[4]; + u->bitsrc.src_byte_pos = 5; /* skip header */ + } else if (0x110 == type) { + /* carrier tone */ + u->bitsrc.nodata_total_pre_cycs = tape_read_u16(c->data); + } else if (0x111==type) { + /* carrier tone + &AA + carrier tone */ + u->bitsrc.nodata_total_pre_cycs = tape_read_u16(c->data); + u->bitsrc.nodata_total_post_cycs = tape_read_u16(c->data + 2); + } else if (0x112==type) { + /* integer gap; UEF spec is wrong */ + u->bitsrc.nodata_total_pre_cycs = tape_read_u16(c->data); + } else if (0x116 == type) { + /* float gap; make sure it isn't negative! */ + f = uef_read_float (c->data); + if (f < 0.0f) { + log_warn("uef: chunk &116 contains negative float gap!"); + return TAPE_E_UEF_0116_NEGATIVE_GAP; + } + if (f > UEF_MAX_FLOAT_GAP) { + log_warn("uef: chunk &116 contains excessive float gap!"); + return TAPE_E_UEF_0116_HUGE_GAP; + } + u->bitsrc.nodata_total_pre_cycs = (uint32_t) (0.5f + (f * UEF_BASE_FREQ * 2.0f)); + } + + if (cb && is_chunk_spent(c, src)) { + /* sanity check: we also call is_chunk_spent() + on the new chunk. It is possible to end up + with a discrepancy where this function claims that + a chunk can provide some tapetime, but the call to + is_chunk_spent() in reload_reservoir() returns TRUE. + One situation where this arose was where a chunk + 116 (float gap) had a gap length of zero, so did + not actually resolve to any number of 1200ths. + This condition is checked separately now (above) + but even so this sanity check has been added + in case it happens some other way. + */ + log_warn("uef: chunk &%x that should have contained tapetime " + "is somehow empty; skipping", type); + cb = 0; /* as you were */ + } + + *contains_1200ths_out = cb; + + return TAPE_E_OK; + +} + +static float uef_read_float (uint8_t b[4]) { + /* FIXME: implement FLOAT-reading properly + (i.e. platform-independently) */ + /* avoid type-punning */ + union { uint8_t b[4]; float f; } u; + memcpy(u.b, b, 4); + return u.f; +} + + +static void uef_metadata_item_finish (uef_meta_t *m) { + if ((0x120 == m->type) && (m->data.position_marker != NULL)) { + free(m->data.position_marker); + } else if ((0x131 == m->type) && (m->data.start_of_tape_side.description != NULL)) { + free(m->data.start_of_tape_side.description); + } +} + + +void uef_metadata_list_finish (uef_meta_t metadata_list[UEF_MAX_METADATA], + uint32_t fill) { + uint32_t i; + for (i=0; i < fill; i++) { + uef_metadata_item_finish(metadata_list + i); + } + memset(metadata_list, 0, sizeof(uef_meta_t) * UEF_MAX_METADATA); +} + +/* called from uef_load_file(), to make an initial + * verification pass across all chunks and find out + * if it's worth persisting with this UEF file */ +static int uef_verify_meta_chunks (uef_state_t *u) { + uint32_t i; + uef_tape_set_info_t tsi; + memset (&tsi, 0, sizeof(uef_tape_set_info_t)); + for (i=0; i < u->num_chunks; i++) { + uef_chunk_t *c; + uef_meta_t m; + int e; + c = u->chunks + i; + memset(&m, 0, sizeof(uef_meta_t)); + e = pre_parse_metadata_chunk (c, tsi.num_tapes, tsi.num_channels, &m); + if (TAPE_E_OK != e) { return e; } + if (m.is_valid && (0x130 == m.type)) { + /* keep current working chunk 130 specification + * (note that this chunk type doesn't contain any heap memory) */ + tsi = m.data.tape_set_info; + } + uef_metadata_item_finish(&m); + } + return TAPE_E_OK; +} + + +/* we will let the caller clean up metadata_list on error; + * it's a tiny bit neater; + * + * if chunk was not a metadata-type chunk, then meta_out->is_valid + * will be 0; otherwise 1 */ +static int +pre_parse_metadata_chunk (uef_chunk_t *chunk, + /* zero means no prior chunk 130: */ + uint8_t num_tapes_from_prior_chunk_130, + /* likewise: */ + uint8_t num_channels_from_prior_chunk_130, + uef_meta_t *meta_out) { + + size_t desclen; + + memset(meta_out, 0, sizeof(uef_meta_t)); + + /* once again, chunk lengths should have been verified by + * chunk_verify_length() earlier */ + if (0x115 == chunk->type) { /* phase change */ + meta_out->data.phase = tape_read_u16(chunk->data); + if (meta_out->data.phase > 360) { + log_warn("uef: phase change: illegal value %u", meta_out->data.phase); + return TAPE_E_UEF_0115_ILLEGAL; + } + meta_out->is_valid = 1; + } else if (0x120 == chunk->type) { /* position marker text */ + meta_out->data.position_marker = malloc(1 + chunk->len); + if (NULL == meta_out->data.position_marker) { + log_warn("uef: could not allocate position marker metadata"); + //~ metadata_finish(metadata_list, *fill_inout); + return TAPE_E_MALLOC; + } + /* null-terminate: */ + meta_out->data.position_marker[chunk->len] = '\0'; + memcpy(meta_out->data.position_marker, + chunk->data, + chunk->len); + meta_out->is_valid = 1; + } else if (0x130 == chunk->type) { /* tape set info */ + if (chunk->data[0] > 4) { + log_warn("uef: tape set info: illegal vocabulary (max. 4): %u", chunk->data[0]); + return TAPE_E_UEF_0130_VOCAB; + } + meta_out->data.tape_set_info.vocabulary = chunk->data[0]; + if ((chunk->data[1] > 127) || (0 == chunk->data[1])) { + log_warn("uef: tape set info: illegal number of tapes (1<=nt<=127): %u", chunk->data[1]); + return TAPE_E_UEF_0130_NUM_TAPES; + } + /* this acts as a limit for future chunk &131s (will be + * passed back into this function on future calls to it): */ + meta_out->data.tape_set_info.num_tapes = chunk->data[1]; + if (0 == chunk->data[2]) { + log_warn("uef: tape set info: illegal (zero) number of channels"); + return TAPE_E_UEF_0130_NUM_CHANNELS; + } + /* this acts as a limit for future chunk &131s (will be + * passed back into this function on future calls to it): */ + meta_out->data.tape_set_info.num_channels = chunk->data[2]; + meta_out->is_valid = 1; + } else if (0x131 == chunk->type) { /* start of tape side */ + if (127 == (chunk->data[0] & 0x7f)) { + log_warn("uef: tape set info: bad tape ID %u", (chunk->data[0] & 0x7f)); + return TAPE_E_UEF_0131_TAPE_ID; + } else if ( (num_tapes_from_prior_chunk_130 > 0) + && ((chunk->data[0] & 0x7f) >= num_tapes_from_prior_chunk_130)) { + log_warn("uef: tape set info: tape ID exceeds prior max.: %u vs. %u", + (chunk->data[0] & 0x7f), num_tapes_from_prior_chunk_130 - 1); + return TAPE_E_UEF_0131_TAPE_ID_130_LIMIT; + } + meta_out->data.start_of_tape_side.tape_id = chunk->data[0] & 0x7f; + meta_out->data.start_of_tape_side.is_side_B = (chunk->data[0] & 0x80) ? 1 : 0; + if (0xff == chunk->data[1]) { + log_warn("uef: tape set info: bad channel ID 255"); + return TAPE_E_UEF_0131_CHANNEL_ID; + } else if ( (num_channels_from_prior_chunk_130 > 0) + && (chunk->data[1] >= num_channels_from_prior_chunk_130)) { + log_warn("uef: tape set info: tape ID exceeds prior max.: %u vs. %u", + chunk->data[1], num_channels_from_prior_chunk_130 - 1); + return TAPE_E_UEF_0131_CHANNEL_ID_130_LIMIT; + } + meta_out->data.start_of_tape_side.channel_id = chunk->data[1]; + meta_out->data.start_of_tape_side.description = malloc(1 + (chunk->len-2)); + if (NULL == meta_out->data.start_of_tape_side.description) { + return TAPE_E_MALLOC; + } + meta_out->data.start_of_tape_side.description[chunk->len-2] = '\0'; + memcpy (meta_out->data.start_of_tape_side.description, + chunk->data + 2, + chunk->len - 2); + desclen = strlen(meta_out->data.start_of_tape_side.description); + /* UEF spec constrains len to 255 chars max */ + if (desclen > 255) { + log_warn("uef: tape set info: description exceeds 255 chars (%zu)", + desclen); + return TAPE_E_UEF_0131_DESCRIPTION_LONG; + } + meta_out->is_valid = 1; + } else if (0x117 == chunk->type) { /* NEW: baud rate */ + meta_out->data.baud = tape_read_u16(chunk->data); + if ((meta_out->data.baud != 300) && (meta_out->data.baud != 1200)) { + log_warn("uef: data encoding format change: bad baud %u", + meta_out->data.baud); + return TAPE_E_UEF_0117_BAD_RATE; + } + meta_out->is_valid = 1; + } + + if (meta_out->is_valid) { + meta_out->type = chunk->type; + } + return TAPE_E_OK; +} + + + +static uint8_t compute_parity_bit (uint8_t data, uint8_t num_data_bits, char parity) { - case 0x111: /*High tone with dummy byte*/ - uef_toneon = 2; - if (!uef_intone) - { - acia_dcdhigh(&sysacia); - uef_intone = gzgetc(uef_f); - uef_intone |= (gzgetc(uef_f)<<8); - uef_intone /= 20; - if (!uef_intone) uef_intone = 1; + uint8_t n, num_ones; + + for (n=0, num_ones = 0; n < num_data_bits; n++) { + num_ones += (data & 1); + data = (data >> 1) & 0x7f; + } + + if (num_ones & 1) { + /* have odd */ + if ('E' == parity) { /* want even */ + return 1; + } + } else { + /* have even */ + if ('O' == parity) { /* want odd */ + return 1; + } + } + + return 0; + +} + + +static int chunk_114_next_cyc (uef_bitsource_t *src, + uint32_t chunk_len, + uint8_t *data, + uint8_t *cyc_out) { + + uint8_t v, b; + + /* Chunk is finished when all the cycles in the 24-bit value + * from bytes 0-2 have been consumed. */ + if (src->chunk_114_consumed_cycs >= src->chunk_114_total_cycs) { + return TAPE_E_UEF_CHUNK_SPENT; + } + + /* This shouldn't happen, unless the 24-bit value was full of + * lies and sin. */ + if (src->src_byte_pos >= chunk_len) { + log_warn("uef: Chunk &114 number-of-cycles field is wrong (&%x)", + src->chunk_114_total_cycs); + return TAPE_E_UEF_0114_BAD_NUM_CYCS; + } + + v = data[src->src_byte_pos]; + + /* get next cycle-bit; cycles are MSB first */ + b = (v >> (7 - src->chunk_114_src_bit_pos)) & 1; + + /* cycle is consumed: */ + (src->chunk_114_consumed_cycs)++; + (src->chunk_114_src_bit_pos)++; + + *cyc_out = b; + + if (src->chunk_114_src_bit_pos >= 8) { + (src->src_byte_pos)++; + src->chunk_114_src_bit_pos = 0; + } + + return TAPE_E_OK; + +} + + +static uint8_t is_chunk_spent (uef_chunk_t *c, uef_bitsource_t *src) { + uint16_t type; + uint32_t bits102, bytes102; + int e; + type = c->type; + if ((0x100==type)||(0x104==type)) { + return (src->src_byte_pos >= c->len); + } else if (0x102==type) { + e = compute_chunk_102_data_len (c->len, c->data[0], &bytes102, &bits102); + if (TAPE_E_OK != e) { return 1; } + return (src->src_byte_pos - 1) >= bytes102; + } else if (0x114==type) { + return (src->chunk_114_consumed_cycs >= src->chunk_114_total_cycs); + } else if (0x111==type) { + return (src->nodata_consumed_post_cycs >= src->nodata_total_post_cycs); + } else if ((0x110==type)||(0x112==type)||(0x116==type)) { + return (src->nodata_consumed_pre_cycs >= src->nodata_total_pre_cycs); + } + /* other chunk types don't contain any bits */ + return 1; +} + + +/* Take a set of bits and turn them into a set of 1/1200s tones. + * At 1200 baud, these will be the same thing, but at 300 baud, + * one bit becomes four 1/1200s tones, represented by four + * bits in the output uint64_t. */ +static uint64_t reservoir_make_value (uint16_t v, uint8_t baud300) { + uint64_t x, four; + uint8_t n; + if ( ! baud300 ) { return v; } + for (n=0, x=0; n < 16; n++) { + four = (((uint64_t) 15) << (n*4)); + x |= ((v>>n)&1) ? four : 0; + } + return x; +} + + +static int reload_reservoir (uef_state_t *u, uint8_t baud300) { + + int e; + uef_bitsource_t *src; + uint8_t v; + uef_chunk_t *chunk; + uint16_t pb; + uint8_t cycs[8]; + uint32_t saved_consumed_cycs; + uint32_t saved_src_bit_pos; + uint32_t saved_src_byte_pos; + uint16_t type; + uint16_t nbits; + uint32_t bits102, bytes102; + + /* cur_chunk starts at -1; force consideration of chunk 0 */ + if (u->cur_chunk < 0) { + return TAPE_E_UEF_CHUNK_SPENT; + } + + e = TAPE_E_OK; + src = &(u->bitsrc); + + chunk = u->chunks + u->cur_chunk; + + if (u->cur_chunk >= u->num_chunks) { + return TAPE_E_EOF; + } + + /* Empty the reservoir. */ + src->reservoir_pos = 0; + src->reservoir_len = 0; + + if (is_chunk_spent (chunk, src)) { + return TAPE_E_UEF_CHUNK_SPENT; + } + + type = chunk->type; + + /* Chunks &100, &102 and &104 are easy -- we'll just move one + * more byte through the source data. This nets us one frame + * for &100 and &104, and eight bits for chunk &102 (for 102, these + * are explicit bits, so not a frame -- it's just a fragment of + * the bitstream, which may include start, stop and parity + * bits). */ + if ( (0x100 == type) || (0x104 == type) ) { + /* get byte from input chunk and frame it */ + v = chunk->data[src->src_byte_pos]; + nbits = 1; /* start bit */ + src->reservoir_1200ths = (((uint16_t) v) << 1); + nbits += src->framing.num_data_bits; /* data bits */ + /* parity */ + if (src->framing.parity != 'N') { + pb = compute_parity_bit (v, + src->framing.num_data_bits, + src->framing.parity); + pb <<= nbits; + src->reservoir_1200ths |= pb; /* install parity bit */ + nbits++; + } + /* stop 1 */ + src->reservoir_1200ths |= (((uint64_t)1) << nbits); + nbits++; + if (2 == src->framing.num_stop_bits) { + nbits++; + src->reservoir_1200ths |= (((uint64_t) 1) << nbits); + } + src->reservoir_len = nbits; + (src->src_byte_pos)++; + + if (baud300) { + /* quadruple up the reservoir */ + src->reservoir_1200ths = reservoir_make_value (src->reservoir_1200ths, 1); + src->reservoir_len *= 4; + } + + } else if (0x102==type) { + /* explicit bits */ + /* get byte from input chunk and don't frame it */ + e = compute_chunk_102_data_len (chunk->len, chunk->data[0], &bytes102, &bits102); + if (TAPE_E_OK != e) { return e; } + if ((src->src_byte_pos - 1) == (bytes102 - 1)) { + /* final byte; may be incomplete */ + bits102 -= ((src->src_byte_pos - 1) * 8); + src->reservoir_len = bits102; + } else { + src->reservoir_len = 8; + } + v = chunk->data[src->src_byte_pos]; + src->reservoir_1200ths = v & 0xff; + (src->src_byte_pos)++; + + if (baud300) { + /* quadruple up the reservoir */ + src->reservoir_1200ths = reservoir_make_value (src->reservoir_1200ths, 1); + src->reservoir_len *= 4; + } + + } else if (0x114==type) { + + /* Squawk, or maybe actually data. */ + + do { + + /* Get the first cycle. It determines what we do next. + * (src is updated with advanced bit/byte positions) */ + e = chunk_114_next_cyc (src, chunk->len, chunk->data, cycs + 0); + if (TAPE_E_OK != e) { return e; } + + /* Look at the cycle and figure out how many more bits + * we need to pull from the chunk to complete one output + * bit. Remember, a 1200-baud 0-bit means one 1200 Hz cycle; + * a 1200-baud 1-bit means two 2400 Hz cycles. + * + * first cycle baud rate extra cycs needed total cycs + * 0 1200 0 1 + * 1 1200 1 2 + * 0 300 3 4 + * 1 300 7 8 + */ + + /* Before pulling any extra cycle, back up the bitsource + * state. We might need to revert it, if the output bit + * turns out to be ambiguous and we need to re-synchronise. + */ + + saved_consumed_cycs = src->chunk_114_consumed_cycs; + saved_src_bit_pos = src->chunk_114_src_bit_pos; + saved_src_byte_pos = src->src_byte_pos; + + /* Pull any cycle from the chunk that makes up the rest of + * this output bit. If cycs[0] is a 2400 Hz cycle, then + * we need to pull another cycle to make up the full + * 1/1200. */ + if (cycs[0]) { + e = chunk_114_next_cyc (src, chunk->len, chunk->data, cycs + 1); + if (TAPE_E_OK != e) { return e; } + } + + /* Now we have 1-2 cycles in cycs[]. Examine this data + * and see how fidelitous the bit is. */ + if ((cycs[0]) && ( ! cycs[1])) { + /* Bit is ambiguous. Restore the source state, throw out + * this cycle, and resynchronise on the next cycle. */ + src->chunk_114_consumed_cycs = saved_consumed_cycs; + src->chunk_114_src_bit_pos = saved_src_bit_pos; + src->src_byte_pos = saved_src_byte_pos; + } else { + break; + } + + } while (1); + + /* For &114, we only do one 1200th at a time; place a single 1200th + * into the reservoir. */ + src->reservoir_1200ths = cycs[0] ? 1 : 0; + src->reservoir_len = 1; + + /* 114 is cycle-explicit; so we don't do the quadrupling with 300 baud. */ + + } else if (0x110 == type) { + + /* leader -- send one 1200th only */ + + src->reservoir_1200ths = 0x1; + src->reservoir_len = 1; + (src->nodata_consumed_pre_cycs) += 2; + + /* leader chunks count cycles, not bits or anything, so + * they are unaffected by 300 baud. */ + + } else if (0x111 == type) { + + /* leader + &AA + leader */ + if (0 == src->chunk_111_state) { + /* pre-&AA leader */ + if (src->nodata_consumed_pre_cycs >= src->nodata_total_pre_cycs) { + src->chunk_111_state = 1; + } else { + src->reservoir_1200ths = 0x1; + src->reservoir_len = 1; + (src->nodata_consumed_pre_cycs)+=2; /* 2 cycs, 1 bit */ + } + } else if (1 == src->chunk_111_state) { + /* dummy byte; framed as 8N1 */ + src->reservoir_1200ths = 0x354; + src->reservoir_len = 10; + src->chunk_111_state = 2; + } else { + /* post-&AA leader */ + src->reservoir_1200ths = 0x1; + src->reservoir_len = 1; + (src->nodata_consumed_post_cycs) += 2; /* 2 cycs, 1 bit */ + } + + /* As above, leader chunks count cycles, not bits; + * they are unaffected by 300 baud. The dummy byte is always + * 1200 baud, because MOS always writes it that way. */ + + } else if ((0x112==type)||(0x116==type)) { + src->silence = 1; /* overrides reservoir */ + src->reservoir_len = 1; + /* TOHv3.2: fixed doubled silent duration; now consume silence twice as fast */ + (src->nodata_consumed_pre_cycs)+=2; + } + + return TAPE_E_OK; + +} + +uint8_t uef_peek_eof (uef_state_t *uef) { + return (uef->cur_chunk >= uef->num_chunks); +} + + +/* Non-data (metadata) chunks that are encountered in the course of + * finding the next actual data chunk are returned in metadata_list. + * Caller must offload all this metadata every time this function is + * called; the next call to this function that advances the chunk will + * wipe the contents of metadata_list. + * Caller must also call metadata_finish() on metadata_list, + * regardless of whether this function succeeds or fails. + */ +int uef_read_1200th (uef_state_t *u, + char *out_1200th, + uef_meta_t metadata_list[UEF_MAX_METADATA], /* caller must call uef_metadata_list_finish() */ + uint32_t *metadata_fill_out) { + + /* TODO: this function really should output 'L' to denote + * leader tone, rather than '1'. Currently the CSW code is + * being used to convert the output of this function from + * '1' to 'L' if leader is detected, but there's no reason + * why 'L' shouldn't be outputted directly from here if it + * came from a leader UEF chunk. */ + + int e; + uint8_t chunk_contains_bits; + uef_bitsource_t *src; + uint16_t shift; + + *out_1200th = '?'; + *metadata_fill_out = 0; + + if (u->cur_chunk >= u->num_chunks) { + /*log_info("uef: EOF");*/ + return TAPE_E_EOF; /* on EOF, metadata is NOT freed */ + } + + src = &(u->bitsrc); + + if (src->reservoir_pos >= src->reservoir_len) { + /* reservoir empty, refill it */ + e = reload_reservoir (u, + /* NOT the baud setting in the ACIA! + * It's the 300 baud setting for the UEF! + * Not the same thing at all! */ + (300 == u->bitsrc.framing.nominal_baud)); + if ((TAPE_E_OK != e) && (TAPE_E_UEF_CHUNK_SPENT != e)) { return e; } + /* reload_reservoir() may fail with "chunk spent", + * in which case we need to get a new chunk and try again: */ + if (TAPE_E_UEF_CHUNK_SPENT == e) { + chunk_contains_bits = 0; + e = TAPE_E_OK; + /* chunk spent! free any metadata from previous chunk */ + uef_metadata_list_finish (metadata_list, *metadata_fill_out); + /* loop to find a chunk that actually contains some bits, + collecting up any intervening metadata chunks in the process */ + while ( (TAPE_E_OK == e) && ! chunk_contains_bits ) { + uef_meta_t meta; + (u->cur_chunk)++; /* initially goes -1 to 0 */ +/*printf("uef: chunk #%d (type &%x, len &%x)\n", + u->cur_chunk, u->chunks[u->cur_chunk].type, + u->chunks[u->cur_chunk].len); */ + if (u->cur_chunk >= u->num_chunks) { + log_info("uef: EOF"); /* on EOF, metadata is NOT freed */ + return TAPE_E_EOF; } - else - { - uef_intone--; - if (uef_intone == 0 && uef_inchunk == 2) - { - uef_inchunk = 0; - } - else if (!uef_intone) - { - uef_inchunk = 2; - uef_intone = gzgetc(uef_f); - uef_intone |= (gzgetc(uef_f) << 8); - uef_intone /= 20; - if (!uef_intone) uef_intone = 1; - uef_receive(0xAA); - } + chunk_contains_bits = 0; + /* this ought to receive the tape set info from the previous + * chunk &130 for validation -- but it ought to have been + * checked already on load by uef_verify_meta_chunks(), + * so I don't think it's really necessary to check it again; + * => just pass 0,0 for the tape set info */ + e = pre_parse_metadata_chunk (u->chunks + u->cur_chunk, 0, 0, &meta); + if (TAPE_E_OK != e) { return e; } + if (meta.is_valid) { /* chunk is a metadata chunk, => no bits */ + /* we handle baud rate changes here and now */ + if (0x117 == meta.type) { + u->bitsrc.framing.nominal_baud = meta.data.baud; + log_info("uef: &117: baud change on tape: %u\n", meta.data.baud); + } + /* there is some interesting metadata on this chunk, + * so bag it up and return it */ + if ( *metadata_fill_out >= UEF_MAX_METADATA) { + log_warn ("uef: too many metadata chunks between data chunks"); + return TAPE_E_UEF_TOO_MANY_METADATA_CHUNKS; + } + metadata_list[*metadata_fill_out] = meta; + (*metadata_fill_out)++; + } else { + e = consider_chunk (u, &chunk_contains_bits); + if (TAPE_E_OK != e) { return e; } } - return; + } /* keep trying chunks until we get some bits */ + if (TAPE_E_OK != e) { return e; } + /* OK, new chunk with more bits, so try refilling the + * reservoir again: */ + e = reload_reservoir (u, + /* NOT the 300 baud setting in the ACIA! + * It's the 300 baud setting for the UEF loader! + * Not the same thing at all! */ + 300 == u->bitsrc.framing.nominal_baud); + if (TAPE_E_OK != e) { return e; } + } + } - case 0x112: /*Gap*/ - uef_toneon = 0; - if (!uef_intone) - { -// acia_dcdhigh(&sysacia); - uef_intone = gzgetc(uef_f); - uef_intone |= (gzgetc(uef_f) << 8); - uef_intone /= 20; -// printf("gap uef_intone %i\n",uef_intone); - if (!uef_intone) uef_intone = 1; + /* Reservoir contains something; emit 1/1200th of a second + * from the reservoir */ + + /* src->silence overrides the reservoir contents */ + if ( ! src->silence ) { + shift = src->reservoir_pos; + *out_1200th = (0x1 & (src->reservoir_1200ths >> shift)) ? '1' : '0'; + } else { + *out_1200th = 'S'; + } + + (src->reservoir_pos)++; + + return TAPE_E_OK; + +} + + +int uef_detect_magic (uint8_t *buf, + uint32_t buflen, + const char *filename, + uint8_t show_errors) { + uint32_t magic_len; + magic_len = 0xff & (1 + strlen(TAPE_UEF_MAGIC)); /* size_t -> uint32_t */ + if (buflen < (magic_len + 2)) { + if (show_errors) { + log_warn("uef: detect magic: file is too short: '%s'", filename); + } + return TAPE_E_UEF_BAD_HEADER; + } + if ( 0 != memcmp (TAPE_UEF_MAGIC, buf, magic_len) ) { + if (show_errors) { + log_warn("uef: detect magic: bad magic for file: '%s'", filename); + } + return TAPE_E_UEF_BAD_MAGIC; + } + return TAPE_E_OK; +} + + +int uef_load_file (const char *fn, uef_state_t *uef) { + + size_t magic_len; + uint32_t len; + uint8_t *buf; + int e; + uint8_t reject_if_truncated; + uint8_t reject_if_unknown_chunk; + uint32_t n; + +#ifdef UEF_REJECT_IF_TRUNCATED + reject_if_truncated = 1; +#else + reject_if_truncated = 0; +#endif + +#ifdef UEF_REJECT_IF_UNKNOWN_CHUNK + reject_if_unknown_chunk = 1; +#else + reject_if_unknown_chunk = 0; +#endif + + uef_finish(uef); + + /* TOHv3.2: now, if tape_load_file() detects UEF magic at start of file, + it will silently ignore the decompression flag supplied by the caller + and skip decompression. This eliminates the stupid "load the whole + file twice" bodge present in prior versions. */ + e = tape_load_file(fn, 1, &buf, &len); + if (TAPE_E_OK != e) { + log_warn("uef: could not load file: '%s'", fn); + return e; + } + + magic_len = 1 + strlen(TAPE_UEF_MAGIC); + /*if (len < (magic_len + 2)) { + log_warn("uef: file is too short: '%s'", fn); + free(buf); + return TAPE_E_UEF_BAD_HEADER; + } + if ( 0 != memcmp (MAGIC, buf, magic_len) ) { */ + + if (uef_detect_magic (buf, len, fn, 1)) { /* 1 = show errors */ + free(buf); + return TAPE_E_UEF_BAD_MAGIC; + } + + uef->version_minor = buf[0 + magic_len]; + uef->version_major = buf[1 + magic_len]; + log_info("uef: header OK: version %u.%u: '%s'", + uef->version_major, uef->version_minor, fn); + + do { + + e = chunks_decode(buf + magic_len + 2, + len - (magic_len + 2), + uef, + reject_if_truncated, + reject_if_unknown_chunk); + free(buf); + if (TAPE_E_OK != e) { break; } + + e = chunks_verify_lengths (uef->chunks, uef->num_chunks); + if (TAPE_E_OK != e) { break; } + + e = uef_parse_global_chunks(uef); + if (TAPE_E_OK != e) { break; } + + e = uef_verify_meta_chunks(uef); + if (TAPE_E_OK != e) { break; } + + } while (0); + + if (TAPE_E_OK != e) { + uef_finish(uef); + return e; + } + + /* + * Today's haiku: + * + * Log origin chunks + * Text may lack termination + * We'll need a copy. + */ + for (n=0; n < uef->globals.num_origins; n++) { + + char *min, *maj; + char *s; + uef_origin_t *o; + size_t z, dp, a; + int maj_i, min_i; + + o = uef->globals.origins + n; + +/* memcpy(o->data, "MakeUEF V10.10.", o->len = 15); */ + + s = malloc(1 + o->len); + if (NULL == s) { + uef_finish(uef); + return TAPE_E_MALLOC; + } + + s[o->len] = '\0'; + memcpy(s, o->utf8, o->len); + + len = 0x7fffffff & strlen(s); + + /* dispel curses */ + for (z=0; z < len; z++) { + if (!isprint(s[z])) { + s[z]='?'; + } + } + + log_info("uef: origins[%u]: \"%s\"", n, s); + + /* hunt for MakeUEF < 2.4, which has even and odd + * parity mixed up for chunk &104. NOTE: we only record + * the FIRST instance of any MakeUEF version origin chunk; if + * multiples show up, later ones are ignored. + * TODO: add some unit tests to probe this behaviour. */ + if ( (len >= 12) && (0 == strncmp (s, "MakeUEF V", 9) ) ) { + + if (uef->globals.have_makeuef_version) { + log_warn("uef: multiple MakeUEF version origin chunks; ignoring later ones"); + } else { + /* found MakeUEF version origin chunk */ + for (z=9, dp=0; z < len; z++) { + if ('.'==s[z]) { + dp = z; + break; + } } - else - { - uef_intone--; - if (uef_intone == 0) - { - uef_inchunk = 0; - } + if ((dp > 9) && (dp < (len-1))) { /* found decimal point */ + /* null terminate DP */ + s[dp] = '\0'; + maj = s + 9; + min = s + dp + 1; + for (a=0; (a < strlen(maj)) && isdigit(maj[a]); a++); + if (a!=strlen(maj)) { continue; } + for (a=0; (a < strlen(min)) && isdigit(min[a]); a++); + if (0==a) { continue; } + maj_i = atoi(maj); + min_i = atoi(min); + uef->globals.have_makeuef_version = 1; + uef->globals.makeuef_version_major = maj_i; + uef->globals.makeuef_version_minor = min_i; + log_info("uef: MakeUEF detected: version %d.%d", maj_i, min_i); + uef->reverse_even_and_odd_parity = ((maj_i < 2) || ((2==maj_i)&&(min_i<4))); + if (uef->reverse_even_and_odd_parity) { + log_warn("uef: Avoid MakeUEF < 2.4 (%d.%d) parity bug: swap chunk &104's E & O", maj_i, min_i); + } } - return; + } /* endif first makeuef origin chunk */ + } /* endif makeuef origin chunk*/ + + free(s); + + } /* next origin chunk */ + + return e; + +} - case 0x113: /*Float baud rate*/ - templ = gzgetc(uef_f); - templ |= (gzgetc(uef_f) << 8); - templ |= (gzgetc(uef_f) << 16); - templ |= (gzgetc(uef_f) << 24); - tempf = (float *)&templ; - tapellatch = (1000000 / ((*tempf) / 10)) / 64; - pps = (*tempf) / 10; - uef_inchunk = 0; - return; - - case 0x116: /*Float gap*/ - uef_toneon = 0; - if (!uef_chunkpos) - { - templ = gzgetc(uef_f); - templ |= (gzgetc(uef_f) << 8); - templ |= (gzgetc(uef_f) << 16); - templ |= (gzgetc(uef_f) << 24); - tempf = (float *)&templ; - uef_chunkf = *tempf; - //printf("Gap %f %i\n",uef_chunkf,pps); - uef_chunkpos = 1; -// uef_chunkf=4; - } - else - { -// printf("Gap now %f\n",uef_chunkf); - uef_chunkf -= ((float)1 / (float)pps); - if (uef_chunkf <= 0) uef_inchunk = 0; +#define UEF_CHUNK_DELTA 10 + +static int chunks_decode (uint8_t *buf, + uint32_t len, + uef_state_t *uef, + uint8_t reject_if_truncated, + uint8_t reject_if_unknown_chunk) { + + uint32_t pos; + uint32_t chunklen; + + chunklen = 0; + + if (NULL != uef->chunks) { + free(uef->chunks); + uef->chunks = NULL; + } + + for (pos=0, uef->num_chunks=0, uef->num_chunks_alloc=0; + pos < len; + pos += (6 + chunklen)) { + + uint16_t type; + int e; + + /* ensure type and chunklen are in-bounds */ + if ((pos + 6) >= len) { + log_warn("uef: chunk #%d: truncated chunk header\n", uef->num_chunks+1); + if (reject_if_truncated) { + uef_finish(uef); + return TAPE_E_UEF_TRUNCATED; + } else { + break; + } + } + + type = tape_read_u16(buf + pos); + chunklen = tape_read_u32(buf + pos + 2); + + if ( ! valid_chunktype(type) ) { + log_warn("uef: unknown chunk type &%x", type); + if (reject_if_unknown_chunk) { + uef_finish(uef); + return TAPE_E_UEF_UNKNOWN_CHUNK; + } else { + /* prevent this chunk from making it into the list */ + continue; + } + } + + /* ensure data is in-bounds */ + if ((pos + 6 + chunklen) > len) { + log_warn("uef: chunk #%d: truncated chunk body (pos + 6 = &%x, chunklen = &%x, buflen = &%x)\n", + uef->num_chunks+1, pos + 6, chunklen, len); + if (reject_if_truncated) { + uef_finish(uef); + return TAPE_E_UEF_TRUNCATED; + } else { + break; + } + } + + e = uef_store_chunk (uef, buf + pos + 6, type, chunklen, pos); /* TOHv4.2: add offset */ + if (TAPE_E_OK != e) { return e; } + + } + + /* + int x; + for (x=0; x < uef->num_chunks; x++) { + int z; + uef_chunk_t *c; + c = uef->chunks + x; + printf("\n\ntype &%x len &%x\n\n", c->type, c->len); + for (z=0; z < c->len; z++) { + printf("%02x", c->data[z]); + } + } + */ + + return TAPE_E_OK; + +} + + + +static uint8_t valid_chunktype (uint16_t t) { + return (0==t)||(1==t)||(3==t)||(5==t)||(6==t)||(7==t)||(8==t)||(9==t)||(10==t) + || (0x100==t)||(0x101==t)||(0x102==t)||(0x104==t)||(0x110==t)||(0x111==t) + || (0x112==t)||(0x116==t)||(0x113==t)||(0x114==t)||(0x115==t)||(0x117==t) + || (0x120==t)||(0x130==t)||(0x131==t); +} + +#define UEF_CHUNKLEN_MAX 0xffffff /* shrug */ + +/* TOHv3: exported now */ +int uef_store_chunk (uef_state_t *uef, uint8_t *buf, uint16_t type, uint32_t len, uint32_t offset) { /* TOHv4.2: add offset */ + + uef_chunk_t *chunk; + int32_t newsize; + uef_chunk_t *p; + +/*printf("uef_store_chunk(%d/%d): type &%x, len %u\n", + uef->num_chunks, uef->num_chunks_alloc, type, len); */ + + if (0 == len) { return TAPE_E_OK; } + + /* enforce some arbitrary maximum chunk length */ + if (len > UEF_CHUNKLEN_MAX) { + log_warn("uef: oversized chunk #%d, at &%x bytes", uef->num_chunks, len); + return TAPE_E_UEF_OVERSIZED_CHUNK; + } + + if (uef->num_chunks >= uef->num_chunks_alloc) { + newsize = uef->num_chunks_alloc + UEF_CHUNK_DELTA; + p = realloc(uef->chunks, newsize * sizeof(uef_chunk_t)); + if (NULL == p) { + log_warn("uef: could not reallocate chunks\n"); + /*uef_finish(uef);*/ /* ? */ + return TAPE_E_MALLOC; + } + uef->chunks = p; + uef->num_chunks_alloc = newsize; + } + + chunk = uef->chunks + uef->num_chunks; + + chunk->type = type; + chunk->len = len; + chunk->offset = offset; + chunk->data = malloc (len + 1); /* get an extra byte, for strings */ + chunk->alloc = len; /* TOHv3 */ + chunk->data[len] = '\0'; + + if (NULL == chunk->data) { + log_warn("uef: could not allocate chunk data for chunk #%d", uef->num_chunks); + return TAPE_E_MALLOC; + } + + memcpy(chunk->data, buf, len); + + (uef->num_chunks)++; + + return TAPE_E_OK; + +} + + +#define UEF_LOG_GLOBAL_CHUNKS + +static int uef_parse_global_chunks (uef_state_t *u) { + + uint32_t nrh, no, ni, nis, ntm; + int32_t n; + int e; + uef_globals_t *g; + + e = TAPE_E_OK; + + /* deal with &00xx chunks, which apply to the entire file, + * and copy properties onto the uef_state_t struct. + * + * UEF doesn't always specify what to do if several instances of + * these chunks are present. + * + * We allow chunks 0, 1, 3, 5, 8 in multiplicity. + * 6, 7, 9 and &A will be singletons. + */ + + g = &(u->globals); /* save some typing */ + memset (g, 0, sizeof(uef_globals_t)); + + /* perform an initial pass, so we know how many of each + * multichunk exists. */ + for (n=0; n < u->num_chunks; n++) { + if (0x0 == u->chunks[n].type) { (g->num_origins)++; } + if (0x1 == u->chunks[n].type) { (g->num_instructions)++; } + if (0x3 == u->chunks[n].type) { (g->num_inlay_scans)++; } + if (0x5 == u->chunks[n].type) { (g->num_target_machines)++; } + if (0x8 == u->chunks[n].type) { (g->num_rom_hints)++; } + } + + /* arbitrary sanity limit TAPE_UEF_MAX_GLOBAL_CHUNKS for all lists */ + if (g->num_origins > TAPE_UEF_MAX_GLOBAL_CHUNKS) { + log_warn("uef: too many origin chunks; found %u, limit is %u.", + g->num_origins, TAPE_UEF_MAX_GLOBAL_CHUNKS); + return TAPE_E_UEF_GLOBAL_CHUNK_SPAM; + } + + if (g->num_instructions > TAPE_UEF_MAX_GLOBAL_CHUNKS) { + log_warn("uef: too many instructions chunks; found %u, limit is %u.", + g->num_origins, TAPE_UEF_MAX_GLOBAL_CHUNKS); + return TAPE_E_UEF_GLOBAL_CHUNK_SPAM; + } + + if (g->num_inlay_scans > TAPE_UEF_MAX_GLOBAL_CHUNKS) { + log_warn("uef: too many inlay scan chunks; found %u, limit is %u.", + g->num_origins, TAPE_UEF_MAX_GLOBAL_CHUNKS); + return TAPE_E_UEF_GLOBAL_CHUNK_SPAM; + } + + if (g->num_target_machines > TAPE_UEF_MAX_GLOBAL_CHUNKS) { + log_warn("uef: too many target machine chunks; found %u, limit is %u.", + g->num_origins, TAPE_UEF_MAX_GLOBAL_CHUNKS); + return TAPE_E_UEF_GLOBAL_CHUNK_SPAM; + } + + if (g->num_rom_hints > TAPE_UEF_MAX_GLOBAL_CHUNKS) { + log_warn("uef: too many ROM hint chunks; found %u, limit is %u.", + g->num_origins, TAPE_UEF_MAX_GLOBAL_CHUNKS); + return TAPE_E_UEF_GLOBAL_CHUNK_SPAM; + } + + if ((TAPE_E_OK == e) && (g->num_origins > 0)) { + g->origins = malloc(sizeof(uef_origin_t) * g->num_origins); + if (NULL == g->origins) { + log_warn("uef: could not allocate origins list"); + e = TAPE_E_MALLOC; + } + } + if ((TAPE_E_OK == e) && (g->num_instructions > 0)) { + g->instructions = malloc(sizeof(uef_instructions_t) * g->num_instructions); + if (NULL == g->instructions) { + log_warn("uef: could not allocate instructions list"); + e = TAPE_E_MALLOC; + } + } + if ((TAPE_E_OK == e) && (g->num_inlay_scans > 0)) { + g->inlay_scans = malloc(sizeof(uef_inlay_scan_t) * g->num_inlay_scans); + if (NULL == g->inlay_scans) { + log_warn("uef: could not allocate inlay_scans list"); + e = TAPE_E_MALLOC; + } + } + if ((TAPE_E_OK == e) && (g->num_target_machines > 0)) { + g->target_machines = malloc(g->num_target_machines); /* one byte each */ + if (NULL == g->target_machines) { + log_warn("uef: could not allocate target_machines list"); + e = TAPE_E_MALLOC; + } + } + if ((TAPE_E_OK == e) && (g->num_rom_hints > 0)) { + g->rom_hints = malloc(sizeof(uef_rom_hint_t) * g->num_rom_hints); + if (NULL == g->rom_hints) { + log_warn("uef: could not allocate rom hints"); + e = TAPE_E_MALLOC; + } + } + + if (TAPE_E_OK == e) { + log_info("uef: multichunk counts: &0/%u; &1/%u; &3/%u; &5/%u; &8/%u\n", + g->num_origins, g->num_instructions, g->num_inlay_scans, + g->num_target_machines, g->num_rom_hints); + } + + for (n=0, no=0, ni=0, nis=0, ntm=0, nrh=0; + (TAPE_E_OK == e) && (n < u->num_chunks); + n++) { + uef_chunk_t *c; + uint8_t log_it; + uint8_t nyb_hi, nyb_lo; + c = u->chunks + n; + log_it = 0; + if ( 0x0 == c->type ) { + e = verify_utf8((char *) c->data, c->len); + if (TAPE_E_OK != e) { + log_warn("uef: bad UTF-8 in origin chunk!\n"); + break; + } + g->origins[no].utf8 = (char *) c->data; + g->origins[no].len = c->len; + no++; + log_it = 1; + } else if ( 0x1 == c->type ) { + e = verify_utf8((char *) c->data, c->len); + if (TAPE_E_OK != e) { + log_warn("uef: bad UTF-8 in instructions chunk!\n"); + break; + } + g->instructions[ni].utf8 = (char *) c->data; + g->instructions[ni].len = c->len; + ni++; + log_it = 1; + } else if ( 0x3 == c->type ) { + /* no parsing or validation is done on this yet + * NOTE: some header parsing is done independently + * by chunk_verify_length(), can leverage that code */ + g->inlay_scans[nis].data = (char *) c->data; + g->inlay_scans[nis].len = c->len; + nis++; + log_it = 1; + } else if ( 0x5 == c->type ) { + /* validate this */ + if (c->len != 1) { + log_warn("uef: target machine chunk is wrong length %u", c->len); + e = TAPE_E_UEF_CHUNKLEN_0005; + break; + } + nyb_hi = (c->data[0]>>4)&0xf; + nyb_lo = c->data[0]&0xf; + if ((nyb_hi>0x4) || (nyb_lo>0x2)) { + log_warn("uef: invalid target machine chunk"); + e = TAPE_E_UEF_CHUNKDAT_0005; + break; + } + g->target_machines[ntm] = c->data[0]; /* value not pointer */ + ntm++; + log_it = 1; + } else if ( 0x6 == c->type ) { + if (g->have_bit_mux) { + log_warn("uef: multiple bit multiplexing information chunks; ignoring later ones"); + } else { + /* not sure quite how to validate this, but it + * probably shouldn't be larger than 4, or smaller than 1 */ + if ((c->data[0] > 4) || (c->data[0] < 1)) { + log_warn("uef: invalid bit multiplexing information chunk"); + e = TAPE_E_UEF_CHUNKDAT_0006; + break; } - return; + g->bit_mux_info = c->data[0]; + g->have_bit_mux = 1; + log_it = 1; + } + } else if ( 0x7 == c->type ) { + if (g->have_extra_palette) { + log_warn("uef: multiple extra palette chunks; ignoring later ones"); + } else { + /* again, not parsed yet */ + g->extra_palette = c->data; + g->extra_palette_len = c->len; + g->have_extra_palette = 1; + log_it = 1; + } + } else if ( 0x8 == c->type ) { + g->rom_hints[nrh].data = c->data; + g->rom_hints[nrh].len = c->len; + log_it = 1; + nrh++; + } else if ( 0x9 == c->type ) { + if (g->have_short_title) { + log_warn("uef: multiple short title chunks; ignoring later ones"); + } else { + g->short_title = (char *) c->data; + g->short_title_len = c->len; + g->have_short_title = 1; + log_it = 1; + } + } else if ( 0xa == c->type ) { + if (g->have_visible_area) { + log_warn("uef: multiple visible area chunks; ignoring later ones"); + } else { + g->visible_area = c->data; + g->have_visible_area = 1; + log_it = 1; + } + } +#ifdef UEF_LOG_GLOBAL_CHUNKS + if (log_it) { + log_info("uef: \"global\" chunk type &%x, len %u", c->type, c->len); + } +#endif + +/* arbitrary hard sanity limit on multi chunk repeats */ +#define MAX_METADATA_MULTICHUNKS 10000 + + if (no >= MAX_METADATA_MULTICHUNKS) { + log_warn("uef: excessive number of origin chunks; aborting"); + e = TAPE_E_UEF_EXCESS_0000; + } else if (ni >= MAX_METADATA_MULTICHUNKS) { + log_warn("uef: excessive number of instructions chunks; aborting"); + e = TAPE_E_UEF_EXCESS_0001; + } else if (nis >= MAX_METADATA_MULTICHUNKS) { + log_warn("uef: excessive number of inlay scan chunks; aborting"); + e = TAPE_E_UEF_EXCESS_0003; + } else if (ntm >= MAX_METADATA_MULTICHUNKS) { + log_warn("uef: excessive number of target machine chunks; aborting"); + e = TAPE_E_UEF_EXCESS_0005; + } else if (nrh >= MAX_METADATA_MULTICHUNKS) { + log_warn("uef: excessive number of rom hint chunks; aborting"); + e = TAPE_E_UEF_EXCESS_0008; + } + } /* next chunk */ + + if (TAPE_E_OK != e) { + if (NULL != g->origins) { free(g->origins); } + if (NULL != g->instructions) { free(g->instructions); } + if (NULL != g->inlay_scans) { free(g->inlay_scans); } + if (NULL != g->target_machines) { free(g->target_machines); } + if (NULL != g->rom_hints) { free(g->rom_hints); } + g->origins = NULL; + g->instructions = NULL; + g->inlay_scans = NULL; + g->target_machines = NULL; + g->rom_hints = NULL; + } + + return e; + +} + + + +static int chunk_verify_length (uef_chunk_t *c) { + + uint32_t i; /* j;*/ + uint32_t len_bytes, len_bits; + uint8_t bpp, grey; /* chunk 3 */ + int e; + + /* let's set a catch-all sanity-based 5 MB upper limit on + * all chunk types for now */ + if (c->len > 5000000) { + log_warn("uef: chunk &%x length exceeds universal upper limit: %u bytes", + c->type, c->len); + return TAPE_E_UEF_LONG_CHUNK; + } + + if (0x0 == c->type) { /* origin information chunk */ + if (c->len < 1) { + log_warn("uef: chunk type &0 has bad length (%u, want >=1)", c->len); + return TAPE_E_UEF_CHUNKLEN_0000; + } + } else if (0x1 == c->type) { /* game instructions / manual or URL */ + if (c->len < 1) { + log_warn("uef: chunk type &1 has bad length (%u, want >=1)", c->len); + return TAPE_E_UEF_CHUNKLEN_0001; + } + } else if (0x3 == c->type) { /* inlay scan */ + if (c->len < 5) { + log_warn("uef: chunk type &3 has bad length (%u, want >=5)", c->len); + return TAPE_E_UEF_CHUNKLEN_0003; + } + /* do some parsing of the header to predict the length + * TODO: abstract this out into a separate function, + * so it can be called separately if someone writes a + * proper parser for chunk 3 */ + bpp = 0x7f & c->data[4]; /* BPP */ + grey = 0x80 & c->data[4]; /* greyscale? */ + /* keep this simple for now and enforce BPP = 8, 16, 24 or 32 */ + if ((bpp!=8)&&(bpp!=16)&&(bpp!=24)&&(bpp!=32)) { + log_warn("uef: chunk type &3 has bizarre BPP value &%u", bpp); + return TAPE_E_UEF_INLAY_SCAN_BPP; + } + /* compute predicted size */ + i = ((uint32_t)tape_read_u16(c->data+0)) + * ((uint32_t)tape_read_u16(c->data+2)) + * (bpp/8); + if (0==i) { + log_warn("uef: chunk type &3 has a pixel size of 0"); + return TAPE_E_UEF_INLAY_SCAN_ZERO; + } + i += 5; + if ((8==bpp) && ! grey) { + i += 768; /* palette */ + } + if (c->len != i) { + log_warn("uef: chunk type &3 has bad length (%u, want %u)", c->len, i); + return TAPE_E_UEF_CHUNKLEN_0003; + } + } else if (0x5 == c->type) { /* target machine chunk */ + if (c->len != 1) { + log_warn("uef: chunk type &5 has bad length (%u, want 1)", c->len); + return TAPE_E_UEF_CHUNKLEN_0005; + } + } else if (0x6 == c->type) { /* bit multiplexing information */ + if (c->len != 1) { + log_warn("uef: chunk type &6 has bad length (%u, want 1)", c->len); + return TAPE_E_UEF_CHUNKLEN_0006; + } + } else if (0x7 == c->type) { /* extra palette */ + if (c->len < 3) { + log_warn("uef: chunk type &7 has bad length (%u, want >=3)", c->len); + return TAPE_E_UEF_CHUNKLEN_0007; + } + } else if (0x8 == c->type) { /* ROM hint */ + if (c->len < 3) { + log_warn("uef: chunk type &8 has bad length (%u, want >=3)", c->len); + return TAPE_E_UEF_CHUNKLEN_0008; + } + } else if (0x9 == c->type) { /* short title */ + if (c->len < 1) { + log_warn("uef: chunk type &9 has bad length (%u, want >=1)", c->len); + return TAPE_E_UEF_CHUNKLEN_0009; + } +#define UEF_SHORT_TITLE_MAX_LEN 255 /* shrug. Would be nice if such numbers were in the UEF spec */ + if (c->len > UEF_SHORT_TITLE_MAX_LEN) { + log_warn("uef: short title chunk &9 is too long (%u bytes)", + c->len); + return TAPE_E_UEF_CHUNKLEN_0009; + } + } else if (0xa == c->type) { /* visible area */ + if (c->len != 8) { + log_warn("uef: chunk type &a has bad length (%u, want 8)", c->len); + return TAPE_E_UEF_CHUNKLEN_000A; + } + } else if (0x100 == c->type) { /* 8N1 chunk */ + if (c->len < 1) { + log_warn("uef: chunk type &100 has bad length (%u, want >=1)", c->len); + return TAPE_E_UEF_CHUNKLEN_0100; + } + /* TODO: multiplexed nonsense, chunk &101 */ + } else if (0x102 == c->type) { /* raw bits */ + if (c->len < 1) { + log_warn("uef: chunk type &102 has bad length (%u, want >=1)", c->len); + return TAPE_E_UEF_CHUNKLEN_0102; + } + e = compute_chunk_102_data_len (c->len, c->data[0], &len_bytes, &len_bits); + if (TAPE_E_OK != e) { return e; } + /* chunk len is data len plus one: */ + if (c->len != (len_bytes + 1)) { + log_warn("uef: chunk type &102 has bad length (%u, expect %u)", + c->len, len_bytes + 1); + return TAPE_E_UEF_CHUNKLEN_0102; + } + } else if (0x104 == c->type) { /* programmable framing */ + if (c->len < 3) { + log_warn("uef: chunk type &104 has bad length (%u, want >=3)", c->len); + return TAPE_E_UEF_CHUNKLEN_0104; + } + } else if (0x110 == c->type) { /* leader */ + if (c->len != 2) { + log_warn("uef: chunk type &110 has bad length (%u, want 2)", c->len); + return TAPE_E_UEF_CHUNKLEN_0110; + } + } else if (0x111 == c->type) { /* leader + &AA + leader */ + if (c->len != 4) { + log_warn("uef: chunk type &111 has bad length (%u, want 4)", c->len); + return TAPE_E_UEF_CHUNKLEN_0111; + } + } else if (0x112 == c->type) { /* integer gap */ + if (c->len != 2) { + log_warn("uef: chunk type &112 has bad length (%u, want 2)", c->len); + return TAPE_E_UEF_CHUNKLEN_0112; + } + } else if (0x116 == c->type) { /* float gap */ + if (c->len != 4) { + log_warn("uef: chunk type &116 has bad length (%u, want 4)", c->len); + return TAPE_E_UEF_CHUNKLEN_0116; + } + } else if (0x113 == c->type) { /* baud (float) */ + if (c->len != 4) { + log_warn("uef: chunk type &113 has bad length (%u, want 4)", c->len); + return TAPE_E_UEF_CHUNKLEN_0113; + } + } else if (0x114 == c->type) { /* arbitrary cycles */ + if (c->len < 6) { + log_warn("uef: chunk type &114 has bad length (%u, want >=6)", c->len); + return TAPE_E_UEF_CHUNKLEN_0114; + } + } else if (0x115 == c->type) { /* phase change */ + if (c->len != 2) { + log_warn("uef: chunk type &115 has bad length (%u, want 2)", c->len); + return TAPE_E_UEF_CHUNKLEN_0115; + } + } else if (0x117 == c->type) { /* baud */ + if (c->len != 2) { + log_warn("uef: chunk type &117 has bad length (%u, want 2)", c->len); + return TAPE_E_UEF_CHUNKLEN_0117; + } + } else if (0x120 == c->type) { /* position marker text */ + if (c->len < 1) { + log_warn("uef: chunk type &120 has bad length (%u, want >=1)", c->len); + return TAPE_E_UEF_CHUNKLEN_0120; + } + } else if (0x130 == c->type) { + if (c->len != 3) { + log_warn("uef: chunk type &130 has bad length (%u, want 3)", c->len); + return TAPE_E_UEF_CHUNKLEN_0130; + } + } else if (0x131 == c->type) { /* start of tape side */ + if ((c->len < 3) || (c->len > 258)) { + log_warn("uef: chunk type &131 has bad length (%u, want 3-258)", c->len); + return TAPE_E_UEF_CHUNKLEN_0131; + } + } + + return TAPE_E_OK; + +} + + + + +int uef_clone (uef_state_t *out, uef_state_t *in) { + /* globals poses a problem here, because it's mostly a list of + * pointers into in->chunks[]->data; we need globals fields to point + * into out->chunks[]->data instead. The cleanest way to fix this is + * probably just to zero out->globals, then run uef_parse_global_chunks() + * on out, to rebuild the list. + * rom_hints will also be allocated and populated separately for out. + */ + int32_t n; + int e; + memcpy(out, in, sizeof(uef_state_t)); + memset(&(out->globals), 0, sizeof(uef_globals_t)); + out->chunks = malloc(sizeof(uef_chunk_t) * in->num_chunks); + if (NULL == out->chunks) { + log_warn("uef: could not allocate clone UEF chunks\n"); + return TAPE_E_MALLOC; + } + for (n=0; n < out->num_chunks; n++) { + memcpy(out->chunks + n, in->chunks + n, sizeof(uef_chunk_t)); + out->chunks[n].data = NULL; /* no, that doesn't belong to you */ + } + for (n=0; n < out->num_chunks; n++) { + /* remember, we allocated one extra in case of strings */ + out->chunks[n].data = malloc(out->chunks[n].len + 1); + if (NULL == out->chunks[n].data) { + log_warn("uef: could not allocate clone UEF chunk data\n"); + uef_finish(out); + return TAPE_E_MALLOC; + } + /* again, one extra */ + memcpy(out->chunks[n].data, in->chunks[n].data, out->chunks[n].len + 1); + } + e = uef_parse_global_chunks(out); + if (TAPE_E_OK != e) { + uef_finish(out); + } + return e; +} - case 0x114: /*Security waves*/ - case 0x115: /*Polarity change*/ -// default: - for (c = 0; c < uef_chunklen; c++) - gzgetc(uef_f); - uef_inchunk = 0; - return; - - default: - for (c = 0; c < uef_chunklen; c++) - gzgetc(uef_f); - uef_inchunk = 0; - return; -//116 : float gap -//113 : float baud rate - - } -// allegro_exit(); -// printf("Bad chunk ID %04X length %i\n",uef_chunkid,uef_chunklen); -// exit(-1); -} - -uint8_t fbuffer[4]; - -#define getuefbyte() ffound = 0; \ - while (!ffound && !uefloop) \ - { \ - uef_poll(); \ - } \ - if (uefloop) break; - -uint8_t ffilename[16]; -void uef_findfilenames() -{ - int temp; - uint8_t tb; - int c; - int fsize = 0; - char s[256]; - uint32_t run, load; - uint8_t status; - int skip; - int binchunk = uef_inchunk, bchunkid = uef_chunkid, bchunklen = uef_chunklen; - int bchunkpos = uef_chunkpos, bchunkdatabits = uef_chunkdatabits; - int bintone = uef_intone, bffound = ffound; - float bchunkf = uef_chunkf; - uint8_t bdat = fdat; - if (!uef_f) return; - - uef_inchunk = 0; uef_chunkid = 0; uef_chunklen = 0; - uef_chunkpos = 0; uef_chunkdatabits = 8; uef_intone = 0; - uef_chunkf = 0; - - temp=gztell(uef_f); - gzseek(uef_f, 12, SEEK_SET); - uefloop = 0; - infilenames = 1; - while (!uefloop) - { - ffound = 0; - while (!ffound && !uefloop) - { - uef_poll(); +void uef_rewind (uef_state_t *u) { + u->cur_chunk = -1; + init_bitsource(&(u->bitsrc)); +} + +void uef_finish (uef_state_t *u) { + int32_t n; + if (NULL == u) { return; } + /* The fields on u->globals just point into u->chunks[].data, + * but these ones are multi and require lists: */ + if (NULL != u->globals.origins) { + free(u->globals.origins); + } + if (NULL != u->globals.instructions) { + free(u->globals.instructions); + } + if (NULL != u->globals.inlay_scans) { + free(u->globals.inlay_scans); + } + if (NULL != u->globals.target_machines) { + free(u->globals.target_machines); + } + if (NULL != u->globals.rom_hints) { + free(u->globals.rom_hints); + } + for (n=0; n < u->num_chunks; n++) { + if (NULL != u->chunks[n].data) { + free(u->chunks[n].data); + } + } + free(u->chunks); + memset(u, 0, sizeof(uef_state_t)); +} + + +static int chunks_verify_lengths (uef_chunk_t *chunks, int32_t num_chunks) { + int32_t n; + int e; + e = TAPE_E_OK; + for (n=0; (TAPE_E_OK == e) && (n < num_chunks); n++) { + e = chunk_verify_length(chunks + n); + } + return e; +} + + +#define REJECT_WEIRD_CHUNK_102_FIRST_BYTE + +static int compute_chunk_102_data_len (uint32_t chunk_len, + uint8_t data0, + uint32_t *len_bytes_out, + uint32_t *len_bits_out) { + uint32_t i; + *len_bits_out = 0; + *len_bytes_out = 0; + + /* You had your chance! Don't say I didn't ask! + https://stardot.org.uk/forums/viewtopic.php?p=391567 */ + + if (data0 < 8) { + /*log_info ("uef: chunk &102: interpretation A");*/ + data0 += 8; + } else if (data0 < 16) { + /*log_info ("uef: chunk &102: interpretation B");*/ + } else { + log_warn ("uef: chunk &102: data[0] is weird (&%x, expect < &10)", data0); +#ifdef REJECT_WEIRD_CHUNK_102_FIRST_BYTE + return TAPE_E_UEF_0102_WEIRD_DATA_0; +#endif + } + /* length of data in bits: */ + i = (chunk_len * 8) - data0; + *len_bits_out = i; + /* length of data in bytes: */ + if ((i % 8) == 0) { + i = (i / 8); + } else { + i = (i / 8) + 1; + } + *len_bytes_out = i; + return TAPE_E_OK; +} + + +/* TOHv3 */ +int uef_append_byte_to_chunk (uef_chunk_t *c, uint8_t b) { + uint32_t newsize; + uint8_t *p; + if ((NULL == c->data) && (c->alloc > 0)) { + log_warn("uef: BUG: chunk alloc > 0 (%u) but buffer is NULL!", c->alloc); + c->alloc = 0; + return TAPE_E_BUG; + } + if (c->len >= c->alloc) { +#define TAPE_UEF_CHUNK_DATA_DELTA 300 + newsize = c->len + TAPE_UEF_CHUNK_DATA_DELTA; + p = realloc(c->data, newsize+1); /* +1 for string usefulness */ + if (NULL == p) { + log_warn("uef: write: could not realloc UEF tmpchunk data buffer"); + return TAPE_E_MALLOC; + } + c->data = p; + c->alloc = newsize; + } + c->data[c->len] = b; + (c->len)++; + return TAPE_E_OK; +} + + +/* TOHv3 */ +int uef_build_output (uef_state_t *u, char **out, size_t *len_out) { + + uint32_t cn; + uint8_t pass; + int e; + size_t hdrlen; + + e = TAPE_E_OK; + + *out = NULL; + *len_out = 0; + + hdrlen = strlen(TAPE_UEF_MAGIC) + 3; /* +1 terminator, +2 version */ + + for (pass=0; (TAPE_E_OK == e) && (pass < 2); pass++) { + + size_t pos; + + if (1 == pass) { + *out = malloc(*len_out); + if (NULL == *out) { + e = TAPE_E_MALLOC; + break; + } + memcpy(*out, TAPE_UEF_MAGIC"\x00\x0a\x00", hdrlen); + } + + for (cn=0, pos=hdrlen; cn < u->num_chunks; cn++) { + uef_chunk_t *c; + uint8_t hdr[6]; + c = u->chunks + cn; + /* sanity */ + if (c->len > UEF_CHUNKLEN_MAX) { + log_warn("uef: write: chunk #%u has illegal length %u\n", cn, c->len); + e = TAPE_E_BUG; + break; + } + if (1 == pass) { + tape_write_u16(hdr, c->type); + tape_write_u32(hdr+2, c->len); + memcpy((*out)+pos, hdr, 6); + memcpy((*out)+pos+6, c->data, c->len); + } + pos += (6 + c->len); + } + *len_out = pos; + } + + if ((TAPE_E_OK != e) && (*out != NULL)) { + free(*out); + *out = NULL; + *len_out = 0; + } + + return e; + +} + +#ifdef BUILD_TAPE_READ_POS_TO_EOF_ON_WRITE +int uef_ffwd_to_end (uef_state_t *u) { + /* force read pointer past end of file, EOF */ + u->cur_chunk = u->num_chunks; + return TAPE_E_OK; +} +#endif + + + +/* NOTE for anyone implementing a save system that allows saving + * from an arbitrary position (perhaps overwriting, perhaps inserting). + * + * Currently, saving is always appending to the very end of the tape. + * We search for chunk &117 backwards from the end of the tape, in order + * to work out what the prevailing baud rate is, and therefore whether + * we need to change it with a chunk &117. (&117 breaks Elkulator, and + * so it's better to produce as few of them as possible). + * + * However, a general system that allows saving at an arbitrary point + * in the tape will need to modify this function in order to determine + * the prevailing baud rate at the current write pointer (which may or + * may not be distinct from the read pointer). You'll need to scan + * backwards from your write pointer, rather than from the end of the + * tape. */ +void uef_scan_backwards_for_chunk_117 (uef_state_t *u, + int32_t *prevailing_nominal_baud_out) { + + int32_t i; + int32_t start_chunk_num; + + *prevailing_nominal_baud_out = 0; + + /* must change this if implementing non-append writes + * (i.e. overwrite from point, insert from point) */ + start_chunk_num = u->num_chunks-1; + + for (i = start_chunk_num; (i >= 0) && (0x117 != u->chunks[i].type); i--) { +/*printf("scan for &117: uef->chunks[%d].type=&%x\n", i, u->chunks[i].type);*/ + } + + if (i >= 0) { +/*printf("scan for &117: uef->chunks[%d].type=&%x ", i, u->chunks[i].type);*/ + *prevailing_nominal_baud_out = (int32_t) *((uint16_t *) u->chunks[i].data); +/*printf("baud=%u\n", *prevailing_300_baud_out ? 300 : 1200);*/ + } + +} + + + + +#define UTF8_MODE_ASCII 1 +#define UTF8_MODE_110 2 +#define UTF8_MODE_1110 3 +#define UTF8_MODE_11110 4 +/* TOHv4.2: now decodes rather than just checking (for examine view) */ +static int decode_utf8 (char *buf, size_t buf_len, uint8_t *bytes_decoded_out, int *err_out) { + + /* decodes one Unicode character + (just one, any more in the buffer will be ignored) */ + + /* returns 0 on failure; ecode is available in err_out */ + + uint8_t i; + size_t j; + uint32_t m = UTF8_MODE_ASCII; + uint32_t bc=0; // byte count + uint8_t buffer[3]; + uint32_t tc=0; + int r; + + r = TAPE_E_OK; + if (NULL != err_out) { *err_out = 0; } + + for (j=0; j < buf_len; j++) { + + i = buf[j]; + + if (!bc) { + memset(buffer, 0, 3); + if (!(i & 0x80)) { + m = UTF8_MODE_ASCII; /* one byte */ + *bytes_decoded_out = 1; + } else if ((i & 0xe0)==0xc0) { + m = UTF8_MODE_110; /* two bytes 1010 1100 */ + *bytes_decoded_out = 2; + } else if ((i & 0xf0)==0xe0) { + m = UTF8_MODE_1110; /* three bytes */ + *bytes_decoded_out = 3; + } else if ((i & 0xf8)==0xf0) { + m = UTF8_MODE_11110; /* four bytes */ + *bytes_decoded_out = 4; + } else { + log_warn("uef: UTF-8: Decoding error [1] at offset 0x%x, byte 0x%x\n", tc, (uint32_t) i); + r = TAPE_E_UEF_UTF8_DEC_1; + break; + } + } else if ((i & 0xc0) != 0x80) { + log_warn("uef: UTF-8: Decoding error [2] at offset 0x%x: " + "byte is 0x%x, mode is %u\n", tc, (uint32_t) (i&0xff), m); + r = TAPE_E_UEF_UTF8_DEC_2; + break; + } + + switch (m) { + case UTF8_MODE_ASCII: /* one byte total */ + return i; + case UTF8_MODE_110: /* two bytes total */ + switch (bc) { + case 0: + buffer[0]=((char)(i & 0x1c))>>2; + buffer[1]=((char)(i & 0x03))<<6; + bc++; + break; + case 1: + buffer[1]|=((char)(i & 0x3f)); + return ((buffer[0]<<8) & 0xff00) | buffer[1]; + break; + default: + log_warn("uef: UTF-8: console_decode_utf8: Bug [3]\n"); + r = TAPE_E_BUG; + break; } - if (uefloop) break; - fbuffer[0] = fbuffer[1]; - fbuffer[1] = fbuffer[2]; - fbuffer[2] = fbuffer[3]; - fbuffer[3] = fdat; - if (fdat == 0x2A && uef_toneon == 1) - { - fbuffer[3] = 0; - c = 0; - do - { - ffound = 0; - while (!ffound && !uefloop) - { - uef_poll(); - } - if (uefloop) break; - ffilename[c++] = fdat; - } while (fdat != 0x0 && c < 10); - if (uefloop) break; - c--; - while (c < 13) ffilename[c++] = 32; - ffilename[c] = 0; - - getuefbyte(); - tb = fdat; - getuefbyte(); - load = tb | (fdat << 8); - getuefbyte(); - tb = fdat; - getuefbyte(); - load |= (tb | (fdat << 8)) << 16; - - getuefbyte(); - tb = fdat; - getuefbyte(); - run = tb | (fdat << 8); - getuefbyte(); - tb = fdat; - getuefbyte(); - run |= (tb | (fdat << 8)) << 16; - - getuefbyte(); - getuefbyte(); - - getuefbyte(); - tb = fdat; - getuefbyte(); - skip = tb | (fdat << 8); - - fsize += skip; - - getuefbyte(); - status = fdat; - - if (status & 0x80) - { - sprintf(s, "%s Size %04X Load %08X Run %08X", ffilename, fsize, load, run); - cataddname(s); - fsize = 0; - } - for (c = 0; c >2; + buffer[1]=((char)(i & 0x03))<<6; + bc++; + break; + case 2: + buffer[1]|=((char)(i & 0x3f)); + return ((buffer[0]<<8) & 0xff00) | buffer[1]; + default: + log_warn("uef: UTF-8: console_decode_utf8: Bug [4]\n"); + r = TAPE_E_BUG; + break; } + break; + case UTF8_MODE_11110: /* four bytes total */ + switch (bc) { + case 0: + buffer[0]=((char)(i & 0x07))<<2; + bc++; + break; + case 1: + buffer[0]|=((char)(i & 0x30))>>4; + buffer[1]=((char)(i & 0x0f))<<4; + bc++; + break; + case 2: + buffer[1]|=((char)(i & 0x3c))>>2; + buffer[2]=((char)(i & 0x03))<<6; + bc++; + break; + case 3: + buffer[2]|=((char)(i & 0x3f)); + return ((buffer[0]<<16) & 0xff0000) | ((buffer[1]<<8) & 0xff00) | buffer[2]; + default: + log_warn("uef: UTF-8: console_decode_utf8: Bug [5]\n"); + r = TAPE_E_BUG; + break; + } + break; + } + tc++; + } + + /* errors are routed here so debugger can be easily attached on error */ + *err_out = r; + return 0; + +} + +/* TOHv4.2 */ +void unicode_string_finish (uef_unicode_string_t *u) { + if (NULL == u) { return; } + if (NULL == u->text) { return; } + free(u->text); + memset(u, 0, sizeof(uef_unicode_string_t)); +} + +/* TOHv4.2: now leverages unicode_string_append() */ +int verify_utf8 (char *buf, size_t len) { + int e; + uef_unicode_string_t u; + memset(&u, 0, sizeof(uef_unicode_string_t)); + e = unicode_string_append(&u, buf); + unicode_string_finish(&u); + return e; +} + +/* TOHv4.2 */ +int unicode_string_to_utf8 (uef_unicode_string_t *u, char **utf8_out, size_t *chars_out, size_t *bytes_out, uint8_t line_break) { + size_t i; + uint8_t pass; + size_t j; // output position + *utf8_out = NULL; + j=0; i=0; // clang + if (NULL == u) { + log_warn("bug: unicode_string_to_utf8: NULL unicode string passed"); + return TAPE_E_BUG; + } + if (NULL == u->text) { + log_warn("bug: unicode_string_to_utf8: NULL u->text"); + return TAPE_E_BUG; + } + if (NULL != chars_out) { *chars_out = 0; } + if (NULL != bytes_out) { *bytes_out = 0; } + for (pass = 0; pass < 2 ; pass++) { + for (i=0, j=0; i < u->len; i++) { + uint8_t len8; + int e; + char utf8[5] = {0,0,0,0,0}; + len8 = 0; + e = encode_utf8 (u->text[i], (uint8_t *) utf8, &len8); + if (TAPE_E_OK != e) { return e; } + if (pass == 1) { memcpy(*utf8_out + j, utf8, len8); } + j += len8; } - infilenames = 0; - gzseek(uef_f, temp, SEEK_SET); - uefloop = 0; - uef_inchunk = binchunk; - uef_chunkid = bchunkid; - uef_chunklen = bchunklen; - uef_chunkpos = bchunkpos; - uef_chunkdatabits = bchunkdatabits; - uef_chunkf = bchunkf; - fdat = bdat; - ffound = bffound; - uef_intone = bintone; + if (pass == 0) { + *utf8_out = malloc(2 + j); /* +1 for '\0', +1 for \n */ + } + } + if (NULL != bytes_out) { *bytes_out = j; } + if (NULL != chars_out) { *chars_out = i; } + if (line_break) { + (*utf8_out)[j] = '\n'; + j++; + } + (*utf8_out)[j] = '\0'; + return TAPE_E_OK; +} + +#define FMT_SZT "zu" +#define UNICODE_MAXLEN 1000000 +#define UNICODE_TERMINATOR 0 + +/* TOHv4.2 */ +int unicode_string_append (uef_unicode_string_t *u, char *buf) { + + size_t i; + size_t len; + uint8_t pass; + size_t j; + int e; + + i=0; j=0; // clang + + if (NULL == u) { + printf("BUG: unicode_string_append: NULL input\n"); + return TAPE_E_BUG; + } + + if (NULL == buf) { return TAPE_E_OK; } + + len = strlen(buf); + e = TAPE_E_OK; + + for (pass = 0; pass < 2; pass++) { + + uint8_t utf8len; + + // i = srcpos, j = dstpos + // j begins at the prior len. scribbling prior terminator + for (i=0, j = u->len; i < len; j++, i += utf8len) { + + uint32_t x; + + utf8len = 0; + e = TAPE_E_OK; + x = decode_utf8 (buf + i, len - i, &utf8len, &e); + + if (TAPE_E_OK != e) { + return e; + } else if (0 == x) { + // shouldn't happen + log_warn("BUG: unexpected null byte encountered, src pos %" FMT_SZT ", dest pos %" FMT_SZT "\n", + i, j); + return TAPE_E_UNICODE_UNEXPECTED_NULL; + } + if ((utf8len + j) >= UNICODE_MAXLEN) { + printf("BUG: exceeded maximum length (%" FMT_SZT ")\n", (size_t) UNICODE_MAXLEN); + e = TAPE_E_UNICODE_MAX_LEN; + break; + } + if (1 == pass) { u->text[j] = x; } + + } + + if (TAPE_E_OK != e) { break; } + + if (0 == pass) { + // allocate + u->text = realloc(u->text, (sizeof(uint32_t) * (1 + j))); + } + + } + + if (TAPE_E_OK == e) { + u->text[j] = UNICODE_TERMINATOR; + u->len = j; + u->alloc = j; + } + + return e; + +} + +/* TOHv4.2 */ +static int encode_utf8 (uint32_t c, uint8_t *utf8_out, uint8_t *len_bytes_out) { + *len_bytes_out = 0; + if (c < 0x80) { + utf8_out[0] = c & 0x7f; + *len_bytes_out = 1; + return TAPE_E_OK; + } else if (c < 0x800) { + utf8_out[0] = 0xc0 | ((c >> 6) & 0x1f); + utf8_out[1] = 0x80 | (c & 0x3f); + *len_bytes_out = 2; + return TAPE_E_OK; + } else if (c < 0x10000) { + utf8_out[0] = 0xe0 | ((c >> 12) & 0xf); + utf8_out[1] = 0x80 | ((c >> 6) & 0x3f); + utf8_out[2] = 0x80 | (c & 0x3f); + *len_bytes_out = 3; + return TAPE_E_OK; + } else { + utf8_out[0] = 0xf0 | ((c >> 18) & 0x7); + utf8_out[1] = 0x80 | ((c >> 12) & 0x3f); + utf8_out[2] = 0x80 | ((c >> 6) & 0x3f); + utf8_out[3] = 0x80 | (c & 0x3f); + *len_bytes_out = 4; + return TAPE_E_OK; + } + return TAPE_E_ENC_UTF8; } +#endif diff -Nu b-em-40246d4-vanilla/src/uef.h b-em-40246d4-TOHv4.2/src/uef.h --- b-em-40246d4-vanilla/src/uef.h 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/uef.h 2025-07-06 14:17:40.701657578 +0100 @@ -1,11 +1,347 @@ #ifndef __INC_UEF_H #define __INC_UEF_H -void uef_load(const char *fn); +#include "tape2.h" +#include "acia.h" /* for serial_framing stuff */ + +/* maximum number of metadata chunks that will be stored in between + * actual bit-containing chunks */ +#define UEF_MAX_METADATA 128 + +#define TAPE_UEF_MAGIC "UEF File!" + +/* Impose arbitrary limit. In fact each list of global chunks + * (origins, instructions, inlay scans etc.) is dynamically + * allocated, so can be any size. See uef_parse_global_chunks(). */ +#define TAPE_UEF_MAX_GLOBAL_CHUNKS 128 + void uef_close(void); void uef_poll(void); void uef_findfilenames(void); extern int uef_toneon; +typedef struct uef_chunk_s { + + uint16_t type; + uint8_t *data; + uint32_t len; + uint32_t offset; /* TOHv4.2 */ + + /* for bureaucratic use */ + uint32_t alloc; + uint32_t num_data_bytes_written; + +} uef_chunk_t; + +typedef struct uef_rom_hint_s { + uint8_t *data; + uint32_t len; +} uef_rom_hint_t; + +typedef struct uef_origin_s { + /* careful: may not be null-terminated */ + char *utf8; + uint32_t len; +} uef_origin_t; + +typedef struct uef_tape_set_info_s { + /* chunk &130 */ + uint8_t vocabulary; /* 0-4 */ + uint8_t num_tapes; /* 1-127 */ + uint8_t num_channels; /* 1-255 */ +} uef_tape_set_info_t; + +typedef struct uef_start_of_tape_side_s { + /* chunk &131; a prior chunk &130 sets the limits for this */ + /* 0-126 */ + uint8_t tape_id; + uint8_t is_side_B; + /* 0-254, although expect 0 and 1 to be L and R: */ + uint8_t channel_id; + /* NOTE: properly malloced and null-terminated; + * doesn't just point into uef data, so it MUST BE FREED + * once done with: */ + char *description; +} uef_start_of_tape_side_t; + +/* UNION */ +typedef union uef_meta_u { + uint16_t phase; /* chunk &115 */ + /* NOTE: properly malloced and null-terminated; + * doesn't just point into uef data, so it MUST BE FREED + * once done with: */ + char *position_marker; /* chunk &120 */ + uef_tape_set_info_t tape_set_info; /* chunk &130 */ + /* similarly, this must be destroyed once it's done with: */ + uef_start_of_tape_side_t start_of_tape_side; /* chunk &131 */ + uint16_t baud; /* NEW: chunk &117 */ +} uef_meta_u_t; + +typedef struct uef_meta_s { + /* the generating chunk type. + * currently supported: + * &115 -- phase change + * &117 -- baud rate + * &120 -- position marker + * &130 -- tape set info + * &131 -- start of tape side */ + uint16_t type; + uef_meta_u_t data; + uint8_t is_valid; +} uef_meta_t; + +typedef struct uef_instructions_s { + /* careful: may not be null-terminated: */ + char *utf8; + uint32_t len; +} uef_instructions_t; + +typedef struct uef_inlay_scan_s { + char *data; + uint32_t len; +} uef_inlay_scan_t; + +typedef struct uef_globals_s { + + /* Note: Fields on globals just point into uef->data. Some + * chunk types are only allowed once, in which case they have + * a "have_..." flag to indicate that they have been found; + * there is then a pointer for that chunk type that points into + * uef->data. If these chunks occur multiple times, our strategy + * will just be to ignore later ones. + * + * Many global chunk types, though, may validly occur multiple + * times. In this case, there will be a malloced list whose + * elements point into multiple points in uef->data. + * + * These lists of multiple entries will need to be freed when + * the UEF is done. (None of the data that the lists or fields + * pointed to should be freed.) + */ + + /* careful with these: they may not be null-terminated: */ + uef_origin_t *origins; /* &0 */ + uint32_t num_origins; + + /* careful with this: it may not be null-terminated: */ + uint32_t num_instructions; /* &1 */ + uef_instructions_t *instructions; + + /* raw chunk 3, no parsing done on it yet: */ + uint32_t num_inlay_scans; /* &3 */ + uef_inlay_scan_t *inlay_scans; + + uint32_t num_target_machines; /* &5 */ + /* values; one byte; not pointers into UEF data; + * note two independent nybbles (see UEF spec) */ + uint8_t *target_machines; + + uint8_t have_bit_mux; /* &6 */ + uint8_t bit_mux_info; /* value; one byte; not a pointer */ + + /* raw chunks, no parsing done yet */ + uint8_t have_extra_palette; /* &7 */ + uint8_t *extra_palette; + uint32_t extra_palette_len; + + /* raw chunk, no parsing done yet */ + uef_rom_hint_t *rom_hints; /* &8 */ + uint32_t num_rom_hints; + + /* careful with this: it may not be null-terminated: */ + uint8_t have_short_title; + char *short_title; /* &9 */ + uint32_t short_title_len; + + uint8_t have_visible_area; + uint8_t *visible_area; /* &A */ + + /* pulled out of origin chunk: */ + uint8_t have_makeuef_version; + int makeuef_version_major; + int makeuef_version_minor; + +} uef_globals_t; + + +typedef struct uef_bitsource_s { + + /* This is going to work by having a local, pre-framed buffer which + * contains some 1/1200s tones. When a tone is needed, we'll shift + * a bit out of the buffer and send it to the ACIA. + * + * When the buffer is empty, we get more data from the source chunk + * and re-fill the pre-framed buffer. + * + * For chunks &100 and &104, we take 7 or 8 bits out of the source + * chunk, frame them with start, stop and parity bits, and place + * either 10 or 11 1200th-tones into the pre-framed buffer (a.k.a. + * "reservoir"). If 300 baud is selected with chunk &117, this + * becomes 40 or 44 1200th-tones. Note that the 300 baud setting + * of the ACIA has absolutely no effect on this; it is purely + * decided by UEF chunks. + * + * For chunk &102, the framing is explicit in the chunk, so we can + * just take 8 bits out of the chunk and place them directly into + * the reservoir (or quadruple those up to 32 bits, if at 300 baud). + * + * All the above chunk types can take data from the source chunk + * one byte at a time -- no sub-byte resolution is needed at source. + * + * This is not true of chunk &114. Chunk &114 contains cycles, not + * bits. At 1200 baud, an output 1-bit is two source bits. An + * output 0-bit is one source bit. At 300 baud, an output 1-bit + * is eight source bits, and a 0-bit is four source bits. For this + * chunk type, we will take 1 to 8 bits from the source and place + * just a single 1200th-tone into the reservoir. This will + * necessitate a source sub-byte position, which we don't need for + * the other chunk types. Chunk &114 uniquely does not care about + * the current selected UEF baud rate. + * + * We also have the other chunk types that contain either leader, + * gap, or . These (&110, &111, &112, &116) + * don't have any source data. We will mostly just be supplying + * a single bit (i.e. two 2400 Hz cycles) to the reservoir in these + * cases. Chunk &111 (leader + dummy byte) will send a single + * 1200th-tone to the reservoir during the pre-leader and + * post-leader sections; the actual dummy byte will be placed as + * 8N1 into the reservoir, so that part only will load 10 bits. + * Since MOS always writes the dummy byte at 1200 baud, we ignore + * the current baud setting for the dummy byte. */ + + /* Complicated, yes? This is why you should use TIBET instead. */ + + uint8_t silence; /* for gaps; one bit's worth of silence */ + + uint64_t reservoir_1200ths; /* atom pairs on their way to the ACIA */ + int8_t reservoir_len; /* total atom pairs in value */ + int8_t reservoir_pos; /* num atom pairs already sent to ACIA */ + + uint32_t src_byte_pos; /* chunk 100, 102, 104, 114 */ + + /* lengths of current leader or gap: */ + uint32_t nodata_total_pre_cycs; /* chunk 110, 111, 112, 116 */ + uint32_t nodata_consumed_pre_cycs; /* chunk 110, 111, 112, 116 */ + uint32_t nodata_total_post_cycs; /* chunk 111 only */ + uint32_t nodata_consumed_post_cycs; /* chunk 111 only */ + + uint8_t chunk_111_state; /* 0 = pre-leader, 1 = &AA, 2 = post-leader */ + + uint32_t chunk_114_src_bit_pos; /* chunk 114 only, 0 to 7 */ + uint32_t chunk_114_total_cycs; /* 24-bit value, &114's first 3 bytes */ + uint32_t chunk_114_consumed_cycs; /* count of total consumed cycles */ + char chunk_114_pulsewaves[2]; /* &114 bytes 4 & 5, 'P' / 'W' */ + + /* + * IMPORTANT: here follows the framing for the *UEF decoder*. + * + * This is *not* the same thing as the framing that is currently + * programmed into the ACIA. We *do not* allow metadata in a UEF + * file to go around merrily reprogramming the ACIA. That's just + * nonsense. + * + * 8N1-type framing values here are derived from the start of &104 + * chunks, and used to form the correct bitstream to send to the + * ACIA for that chunk type only. There is also the baud300 flag, + * which will be set by an instance of chunk &117. This one is used + * for all data chunks. + * + * Chunk &102 encodes bits, not 1/1200-second pairs of atoms. At + * 1200 baud, these are the same thing, but at 300 baud, they are + * not. + * + * This means that even if there were widespread implementation of + * UEF chunk &102, there is still no guaranteed way to take an + * arbitrary stream from a tape and encode it into a UEF file + * without first having to perform some reverse-engineering of its + * various framings. Viper's Ultron is an example; it has four + * blocks at 8N1/300. The agent that builds the UEF needs to be + * aware that these blocks are at 300 baud, or it will incorrectly + * populate the &102 chunk with quadrupled bits, i.e. "1111" + * instead of "1", and "0000" instead of "0". The UEF file would + * also need to contain some &117 chunks to inform the UEF decoder + * that 300 baud needs to be selected for the decoding of that &102 + * chunk. + * + * It is stupid: The tape contains "1111"; chunk &102 contains "1"; + * chunk &117 tells the UEF decoder that 300 baud is selected; + * the UEF decoder sends "1111" to the ACIA, and we are finally + * back where we started! + * + * Only chunk &114 permits + * a direct representation of *cycles* on the tape, not *bits*, and + * it (very generously) uses a 24-bit number to denote the number + * of cycles in the squawk, making it easily expansive enough to + * contain any sensible amount of data. You can get about 800K of + * Beeb data into a single &114 chunk: + * + * 2^24 cycles / 2 cycs/bit (worst case) = 2^23 bits; + * 2^23 bits / 10 bits/frame = 839K. + */ + serial_framing_t framing; + +} uef_bitsource_t; + + +typedef struct uef_state_s { + + /* global data */ + uint8_t version_major; + uint8_t version_minor; + + /* origin chunks, etc., which apply to entire file: */ + uef_globals_t globals; + + int32_t cur_chunk; + uef_bitsource_t bitsrc; + + uef_chunk_t *chunks; + int32_t num_chunks; + int32_t num_chunks_alloc; + + uint8_t reverse_even_and_odd_parity; + +} uef_state_t; + +/* TOHv4.2 */ +typedef struct uef_unicode_string { + uint32_t *text; + size_t len; + size_t alloc; +} uef_unicode_string_t; + +int uef_read_1200th (uef_state_t *u, + char *out_1200th, + uef_meta_t metadata_list[UEF_MAX_METADATA], /* caller must call metadata_finish() */ + uint32_t *metadata_fill_out); +void uef_finish (uef_state_t *u); +int uef_clone (uef_state_t *out, uef_state_t *in); +void uef_rewind (uef_state_t *u); +int uef_load_file (const char *fn, uef_state_t *uef); +uint8_t uef_peek_eof (uef_state_t *uef); +void uef_metadata_list_finish (uef_meta_t metadata_list[UEF_MAX_METADATA], + uint32_t fill); + +int uef_store_chunk (uef_state_t *uef, uint8_t *buf, uint16_t type, uint32_t len, uint32_t offset); +int uef_append_byte_to_chunk (uef_chunk_t *c, uint8_t b); +int uef_build_output (uef_state_t *u, char **out, size_t *len_out); +int uef_detect_magic (uint8_t *buf, + uint32_t buflen, + const char *filename, + uint8_t show_errors); +#ifdef BUILD_TAPE_READ_POS_TO_EOF_ON_WRITE +int uef_ffwd_to_end (uef_state_t *u); #endif +void uef_scan_backwards_for_chunk_117 (uef_state_t *u, int32_t *prevailing_nominal_baud_out); +int uef_get_117_payload_for_nominal_baud (int32_t nominal_baud, + const char **payload_out); +int unicode_string_append (uef_unicode_string_t *u, char *buf); /* TOHv4.2 */ +void unicode_string_finish (uef_unicode_string_t *u); /* TOHv4.2 */ +int unicode_string_to_utf8 (uef_unicode_string_t *u, + char **utf8_out, + size_t *chars_out, + size_t *bytes_out, + uint8_t line_break); /* TOHv4.2 */ + +#endif /* __INC_UEF_H */ diff -Nu b-em-40246d4-vanilla/src/vidalleg.c b-em-40246d4-TOHv4.2/src/vidalleg.c --- b-em-40246d4-vanilla/src/vidalleg.c 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/vidalleg.c 2025-07-06 14:17:40.701657578 +0100 @@ -593,7 +593,12 @@ save_screenshot(); ++framesrun; - if (++fskipcount >= ((motor && fasttape) ? 5 : vid_fskipmax)) { +#ifdef BUILD_TAPE_NO_FASTTAPE_VIDEO_HACKS + if (++fskipcount >= vid_fskipmax) { /* TOHv3.2 */ +#else + /* (this was the original code) */ + if (++fskipcount >= ((tape_state.ula_motor && tape_vars.overclock) ? 5 : vid_fskipmax)) { +#endif if (fullscreen_pending) { ALLEGRO_DISPLAY *display = al_get_current_display(); int newsizex = al_get_display_width(display); diff -Nu b-em-40246d4-vanilla/src/video.c b-em-40246d4-TOHv4.2/src/video.c --- b-em-40246d4-vanilla/src/video.c 2025-06-03 06:22:28.000000000 +0100 +++ b-em-40246d4-TOHv4.2/src/video.c 2025-07-06 14:17:40.701657578 +0100 @@ -1270,8 +1270,13 @@ video_doblit(crtc_mode, crtc[4]); } ccount++; - if (ccount == 10 || ((!motor || !fasttape) && !is_free_run())) - ccount = 0; +#ifdef BUILD_TAPE_NO_FASTTAPE_VIDEO_HACKS + /* TOHv3.2 */ + if ( (10 == ccount) || ! is_free_run() ) { ccount = 0; } +#else + /* original code */ + if (ccount == 10 || (((tape_state.ula_motor) || !tape_vars.overclock) && !is_free_run())) { ccount = 0; } +#endif scry = 0; if (timer_enable) { stopwatch_vblank = stopwatch;