diff -uN b-em-0b6f1d2-vanilla/src/6502.c b-em-0b6f1d2-TOH/src/6502.c --- b-em-0b6f1d2-vanilla/src/6502.c 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/6502.c 2025-11-01 18:06:01.786333347 +0000 @@ -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,55 @@ 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 * const ts, + tape_vars_t * const tv, + ACIA * const acia, + int const num_cycles, + bool const my_sound_tape, + bool const record_activated); + +static void tape_poll_2mhz (tape_state_t *ts, + tape_vars_t *tv, + ACIA *acia, + int num_cycles, + bool const emit_tapenoise, + bool const record_activated); + +static int tape_2mhz_handle_tape (tape_state_t * const ts, + tape_vars_t * const tv, + ACIA * const acia, + int const num_cycles, + bool const my_sound_tape, + bool * const throw_eof_out); + +static int tape_2mhz_handle_rs423_motor1 (tape_state_t * const ts, + tape_vars_t * const tv, + ACIA * const acia, + int const num_cycles, + bool const emit_tapenoise, + bool const record_activated, + bool * const throw_eof_out); + +static void tape_2mhz_handle_rs423_motor0 (tape_state_t * const ts, + int const 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 const ula_rx_thresh_ns, + int32_t const ula_tx_thresh_ns, + int32_t const ula_rx_divider, + int32_t const ula_tx_divider, + bool * const fire_acia_rxc_out, + bool * const fire_acia_2txc_out, + bool * const fire_dcd_out, + int32_t * const rx_ns_inout, + int32_t * const tx_ns_inout, + int32_t * const dcd_2mhz_counter_inout); + + static void polltime(int c) { cycles -= c; @@ -240,6 +290,12 @@ music5000_poll(c); stopwatch += c; otherstuffcount -= c; + tape_poll_2mhz (&tape_state, + &tape_vars, + &sysacia, + c, + sound_tape, + tape_vars.record_activated); if (motoron) { if (fdc_time) { fdc_time -= c; @@ -1076,16 +1132,334 @@ return temp; } + +#include + +static void tape_poll_2mhz (tape_state_t *ts, + tape_vars_t *tv, + ACIA *acia, + int num_cycles, + bool const my_sound_tape, + bool const record_activated) { + int e; + e = tape_2mhz_main (ts, tv, acia, num_cycles, my_sound_tape, record_activated); + /* FIXME? Should ( TAPE_E_OK == ts->prior_exception ) the old 'disabled_due_to_error' check + * be moved inside tape_handle_exception()? */ + if ( (TAPE_E_OK != e) && ( TAPE_E_OK == ts->prior_exception ) ) { + 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, + true); /* 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 * const tapenoise_counter_inout, + bool * const 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 const ula_rx_thresh_ns, + int32_t const ula_tx_thresh_ns, + int32_t const ula_rx_divider, + int32_t const ula_tx_divider, + bool * const fire_acia_rxc_out, + bool * const fire_acia_2txc_out, + bool * const fire_dcd_out, + int32_t * const rx_ns_inout, + int32_t * const tx_ns_inout, + int32_t * const dcd_2mhz_counter_inout) { + + *fire_acia_rxc_out = 0; + *fire_acia_2txc_out = 0; + *fire_dcd_out = 0; + + /* 2 MHz tick */ + (*rx_ns_inout) += 500; + (*tx_ns_inout) += 500; + (*dcd_2mhz_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; + } + } + + 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 * const ts, + tape_vars_t * const tv, + ACIA * const acia, + int const num_cycles, + bool const my_sound_tape, + bool const record_activated) { + + int e; + bool throw_tape_eof; + bool fire_tapenoise; + int i; + + if (tv->expired_quit) { return TAPE_E_EXPIRY; } +#ifdef BUILD_TAPE_TAPECTRL + if (tv->tapectrl.inhibited_by_gui) { return TAPE_E_OK; } /* TOHv4.3 */ +#endif + + throw_tape_eof = false; + e = TAPE_E_OK; + + if (ts->ula_ctrl_reg & 0x40) { /* RS423 mode */ + if (ts->ula_motor) { + /* RS423 with *MOTOR 1 */ + e = tape_2mhz_handle_rs423_motor1 (ts, tv, acia, num_cycles, my_sound_tape, tv->record_activated, &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; (i < num_cycles) && (TAPE_E_OK == e); i++) { + /* TOHv4.1-rc8: tapenoise ringbuf playback moved out of _handle_tape() above */ + fire_tapenoise = false; + ula_2mhz_clock_for_tapenoise(&(ts->tape_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(); + } + } + + if (TAPE_E_OK != e) { return e; } /* TOHv4.3 */ + + /* TOHv4.1: suppress EOF in record mode */ + return ((throw_tape_eof && ! tv->record_activated) ? TAPE_E_EOF : e); + +} + +/* new in TOHv4.1-rc9 */ +static void tape_2mhz_handle_rs423_motor0 (tape_state_t * const ts, int const 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: As long as tape mode is selected in the ULA, this + * function is polled regardless of motor state. */ +static int tape_2mhz_handle_tape (tape_state_t * const ts, + tape_vars_t * const tv, + ACIA * const acia, + int const num_cycles, + bool const my_sound_tape, + bool * const throw_eof_out) { + + int e,j; + + e = TAPE_E_OK; + + /* if (ts->disabled_due_to_error) { */ /* TOHv4.3 */ + if ( TAPE_E_OK != ts->prior_exception ) { /* TOHv4.3 */ + return TAPE_E_OK; + } + + for (j=0; (TAPE_E_OK == e) && (j < num_cycles); j++) { + + bool fire_ula_rxc, fire_ula_2txc, fire_dcd; + + /* 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 */ + &(ts->ula_rx_ns), /* counter updated */ + &(ts->ula_tx_ns), /* counter updated */ + &(ts->ula_dcd_2mhz_counter)); /* counter updated */ + + /* 2. handle RX */ + if ((TAPE_E_OK == e) && 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, + 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) { + /* if fire_ula_2txc was true then this shouldn't have happened */ + log_warn("6502: BUG: e == TAPE_E_ACIA_TX_BITCLK_NOT_READY"); + return TAPE_E_BUG; + } else if (TAPE_E_EOF == e) { + /* TOHv4.3-a4: There should never have been EOFs generated write-side. + * Ban this sick filth. */ + log_warn("6502: BUG: TX-side tape EOF is not allowed"); + 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); } + + } /* next 2 MHz cycle */ + + return e; + +} + + + + +#include "taperead.h" + +/* ONLY called while tape is rolling */ +static int tape_2mhz_handle_rs423_motor1 (tape_state_t * const ts, + tape_vars_t * const tv, + ACIA * const acia, + int const num_cycles, + bool const emit_tapenoise, + bool const record_activated, + bool * const 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, + update playback elapsed */ + if ( ts->ula_rs423_taperoll_2mhz_counter >= TAPE_1200TH_IN_2MHZ_INT ) { /*(128*13) ) {*/ + e = tape_rs423_eat_1200th (ts, tv, acia, record_activated, emit_tapenoise, throw_eof_out); + if (TAPE_E_OK != e) { return e; } + } + + /* Internal tape DCD logic and run-in detection etc. should continue + to function while RS423 mode is being used. (MOTOR 0 inhibits DCD logic + but RS423 mode doesn't.) + + 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 (motorspin) { motorspin--; if (!motorspin) @@ -1106,6 +1480,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 -uN b-em-0b6f1d2-vanilla/src/acia.c b-em-0b6f1d2-TOH/src/acia.c --- b-em-0b6f1d2-vanilla/src/acia.c 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/acia.c 2025-10-27 05:22:52.813610464 +0000 @@ -1,126 +1,351 @@ /*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 * const acia, uint8_t const 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 * const acia, + bool * const fire_rxc_out, + int32_t * const divider_out) { + + /* TOHv4.3: now in a function */ + *divider_out = acia_get_divider_from_ctrl_reg_bits(3 & acia->control_reg); + if (0 == *divider_out) { + 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; +/* call with (3 & acia->control_reg), or similar for TX */ +int32_t acia_get_divider_from_ctrl_reg_bits (uint8_t const cr3) { + + if (0x0 == cr3) { + return 1; /* if tape read (ULA /64) then 19200 baud */ + } else if (0x1 == cr3) { + return 16; /* if tape read (ULA /64) then 1200 baud */ + } else if (0x2 == cr3) { + return 64; /* if tape read (ULA /64) then 300 baud */ } + return 0; /* have fun dividing by this */ + } -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; +void acia_get_framing (uint8_t const ctrl_reg, serial_framing_t * const 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'; + } + + } + + +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; } + + 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 sn; + + 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; + } + sn = (int8_t) bytes[11]; + if ( (sn!=k_mc6850_state_null) + && (sn!=k_mc6850_state_need_start) + && (sn!=k_mc6850_state_need_data) + && (sn!=k_mc6850_state_need_parity) + && (sn!=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 +353,7 @@ bytes[1] = acia->status_reg; fwrite(bytes, sizeof(bytes), 1, f); } +#endif void acia_loadstate(ACIA *acia, FILE *f) { @@ -135,4 +361,617 @@ 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 *acia) { + /*log_warn("WARNING! acia_hack_tx_reset_shift_register()");*/ + reset_tx_shift_register(acia); +} + +static void reset_tx_shift_register(ACIA *acia) { + acia->tx_shift_reg_shift = 0; + acia->tx_shift_reg_loaded = 0; + acia->tx_shift_reg_value = 0; /* TOHv3.2 */ +} + +int acia_receive_bit (ACIA * const acia, uint8_t const bit) { + return mc6850_receive_bit (acia, bit); } + +/* code must be '1', '0', 'L' or 'S' */ +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 *acia) { + return (k_mc6850_state_need_start == acia->state); +} + +uint8_t acia_rx_frame_ready (ACIA *acia) { + return (k_mc6850_state_need_stop == acia->state); +} + + +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 * const acia, uint8_t const 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; + + 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 -uN b-em-0b6f1d2-vanilla/src/acia.h b-em-0b6f1d2-TOH/src/acia.h --- b-em-0b6f1d2-vanilla/src/acia.h 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/acia.h 2025-10-27 05:16:58.227796543 +0000 @@ -1,33 +1,196 @@ #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, BUILD_TAPE_DEV_MENU */ + + +/* 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; + 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 */ + +#include + +#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); /* code must be '1', '0', 'L' or 'S' */ +int acia_receive_bit (ACIA * const acia, uint8_t const 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 const ctrl_reg, serial_framing_t * const 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 * const acia, + bool * const fire_rxc_out, + int32_t * const 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); + +/* TOHv4.3: for baud indicator */ +int32_t acia_get_divider_from_ctrl_reg_bits (uint8_t const cr3); + #endif Common subdirectories: b-em-0b6f1d2-vanilla/src/ARMulator and b-em-0b6f1d2-TOH/src/ARMulator diff -uN b-em-0b6f1d2-vanilla/src/config.c b-em-0b6f1d2-TOH/src/config.c --- b-em-0b6f1d2-vanilla/src/config.c 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/config.c 2025-10-15 13:59:06.708900586 +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"); @@ -232,8 +234,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); @@ -341,9 +344,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); @@ -385,7 +389,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 -uN b-em-0b6f1d2-vanilla/src/config.h b-em-0b6f1d2-TOH/src/config.h --- b-em-0b6f1d2-vanilla/src/config.h 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/config.h 2025-10-15 13:59:06.709176742 +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); #define BOOL_USE_CONFIG 0x02 diff -uN b-em-0b6f1d2-vanilla/src/csw.c b-em-0b6f1d2-TOH/src/csw.c --- b-em-0b6f1d2-vanilla/src/csw.c 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/csw.c 2025-10-27 11:37:38.736764315 +0000 @@ -1,5 +1,22 @@ -/*B-em v2.2 by Tom Walker - CSW cassette support*/ +/* + * B-Em + * This file (C) 2025 'Diminished' + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + #include #include #include @@ -8,310 +25,827 @@ #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, double thresh_smps, double 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*/ -#define CSW_MAXLEN (8 * 1024 * 1024) +/* TOHv3.2: bugfix, not enough brackets! */ +#define CSW_VALID_RATE(rate) (((rate) >= CSW_RATE_MIN) && ((rate) <= CSW_RATE_MAX)) -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; - /*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; +#ifdef CSW_DEBUG_PRINT + debug_csw_print(csw); +#endif + + if (0 == raw_body_len) { + + log_warn("csw: WARNING: CSW body is empty!"); + + } else 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; /*, pulse_acc_16m_smps=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, as they 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; } - 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; - } - else { - csw_read_failed(csw_f, fn); - free(tempin); - } - } - else - log_error("csw: out of memory reading '%s'", fn); - } - else - csw_read_failed(csw_f, fn); - fclose(csw_f); + n+=4; + } else { + pulse = body[n]; + } + + /* insert the pulse */ + e = csw_append_pulse (csw, pulse, NULL); + + } + + 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; + } -void csw_close() -{ - if (csw_dat) { - free(csw_dat); - csw_dat = NULL; + +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_perfect = d; + d = (((double) csw->header.rate) / TAPE_1200_HZ); + 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)); } -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); + + +bool csw_peek_eof (const csw_state_t * const csw) { + return (csw->cur_pulse >= csw->pulses_fill); } -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; + +int csw_read_1200th (csw_state_t * const csw, + char * const out_1200th_or_null, + bool initial_scan, + int32_t * const elapsed_out_or_null) { /* TOHv4.3: return elapsed time if available (or -1) */ + + char b; + tape_interval_t *i_prev, *i_cur; + + b = '?'; + + if (NULL != elapsed_out_or_null) { + *elapsed_out_or_null = 0; + } + + /* TOHv4.3 */ + if (NULL == csw->pulses) { return TAPE_E_EOF; } + if (csw->pulses_fill < 1) { return TAPE_E_EOF; } + + i_prev = NULL; + i_cur = &(csw->pulses[csw->cur_pulse].timespan); + + if (csw->cur_pulse>0) { + i_prev = &(csw->pulses[csw->cur_pulse-1].timespan); + } + + /* currently cOnsOOming silence? */ + if (csw->num_silent_1200ths > 0) { + + /* in the process of cOnsOOming silence + * first of all, set up the elapsed 1200ths value to return */ + if (NULL != elapsed_out_or_null) { + *elapsed_out_or_null = i_cur->start_1200ths + csw->cur_silence_1200th; + } + + /* now decide whether it's finished yet */ + if (csw->cur_silence_1200th < (csw->num_silent_1200ths - 1)) { + + /* silence is not finished yet */ + + (csw->cur_silence_1200th)++; + + } else { + + if (initial_scan) { + i_cur->pos_1200ths = csw->num_silent_1200ths; + } + + (csw->cur_pulse)++; + + /* discard any remainder; end silent period */ + csw->cur_silence_1200th = 0; + csw->num_silent_1200ths = 0; + + } + + *out_1200th_or_null = 'S'; + return TAPE_E_OK; + } + + /* TOHv4.3: we are not currently cOnsOOming silence */ + if (initial_scan && (NULL != i_prev)) { + i_cur->start_1200ths = i_prev->start_1200ths + i_prev->pos_1200ths; + } + if (NULL != elapsed_out_or_null) { + *elapsed_out_or_null = i_cur->start_1200ths; // + i_cur->pos_1200ths; + } + + 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; + tape_interval_t *i; + csw_pulse_t *pulse_p; + + i_cur = &(csw->pulses[csw->cur_pulse].timespan); + + 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].len_smps, csw->thresh_smps_perfect, csw->len_1200th_smps_perfect); + + /* TOHv4.3: Also, at the same time, populate timestamp information on these 4 chunks */ + i_prev = (k > 0) ? &(csw->pulses[k-1].timespan) : NULL; + i = &(csw->pulses[k].timespan); + + /* pulse [k] takes its start time from pulse[k-1]. + * + * Pulses for '1' and '0' will be shorter than one 1200th, + * so most of these will have pos_1200ths=0; the final pulse + * will get the duration of the whole bit when the bit is + * committed. */ + if (initial_scan) { + i->start_1200ths = (NULL == i_prev) ? 0 : (i_prev->start_1200ths + i_prev->pos_1200ths); + i->pos_1200ths = 0; + } + + } /* next of 4 pulses */ + + /* see if enough pulses are in concordance (4 for '0', 2 for '1'); + test for '1' first, and then '0' afterwards; + lookahead is halved after testing for '1': */ + + pulse_p = csw->pulses + csw->cur_pulse; + + for (k=0, q='1', wins=0, lookahead=4; (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++; } } - 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; - } + if (wins < lookahead) { + break; /* insufficient correct-length pulses found in lookahead; failure */ } + csw->cur_pulse += lookahead; + b = q; /* got it */ + break; + } + } /* if '1' failed, try '0' */ + + /* it was '1' or '0', so break now */ + if (b==q) { + /* update the timespan information for the FINAL pulse of the 1200th ONLY */ + if (initial_scan) { + pulse_p[('0'==q)?1:3].timespan.pos_1200ths = 1; + + } + + break; + } + + /* neither { '1', '1', X, X } nor { '0', '0', '0', '0' } + * so, check for { 'S', X, X, X } */ + + if ('S' == v[0]) { + + /* Beginning of silence. Set up ongoing silence processing. */ + + csw->num_silent_1200ths = (int32_t) /*round*/((((double)pulse_p->len_smps) * TAPE_1200_HZ) / (double) (csw->header.rate)); /* round down */ + + if (0 == csw->num_silent_1200ths) { + // csw->num_silent_1200ths = 1; + log_warn("csw: WARNING: very short silence (%u smps); skipping!", pulse_p->len_smps); + b = '?'; + csw->num_silent_1200ths=0; + csw->cur_silence_1200th = 0; + (csw->cur_pulse)++; + continue; + } + + csw->cur_silence_1200th = 1; /* cOnsOOm */ + + *out_1200th_or_null = 'S'; + + if (NULL != elapsed_out_or_null) { + *elapsed_out_or_null = i_cur->start_1200ths + i_cur->pos_1200ths; + } + + if (initial_scan) { //} && (i_cur != NULL)) { + // (csw->pulses[csw->cur_pulse].timespan.pos_1200ths)++; + (i_cur->pos_1200ths)++; + } + + if (1==csw->num_silent_1200ths) { + (csw->cur_pulse)++; + csw->num_silent_1200ths=0; + csw->cur_silence_1200th = 0; + } + + return TAPE_E_OK; + + } + + /* give up; advance pulse and try again: */ + (csw->cur_pulse)++; + if (csw->cur_pulse >= csw->pulses_fill) { + if (out_1200th_or_null != NULL) { + *out_1200th_or_null='\0'; + } + return TAPE_E_EOF; } + + } while ('?' == b); /* Ambiguous pulse sequence, or short silence. Advance by one pulse. Try again. */ + + /* this code is always for '0' or '1' */ + + if (NULL != out_1200th_or_null) { + *out_1200th_or_null = b; + } + if (csw->cur_pulse >= csw->pulses_fill) { + return TAPE_E_EOF; + } + 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(); +static char pulse_get_bit_value (uint32_t pulse, double thresh_smps, double max_smps) { + double pulse_f; + pulse_f = (double) pulse; + if (pulse_f <= thresh_smps) { + return '1'; + } else if (pulse_f < max_smps) { + return '0'; + } + return 'S'; +} + + +int csw_clone (csw_state_t *out, csw_state_t *in) { + memcpy(out, in, sizeof(csw_state_t)); + out->pulses = malloc(sizeof(csw_pulse_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(csw_pulse_t) * in->pulses_alloc); /* TOHv3 */ + /* clone pulses */ + memcpy(out->pulses, in->pulses, sizeof(csw_pulse_t) * in->pulses_fill); + return TAPE_E_OK; +} + + +void csw_rewind (csw_state_t *csw) { + csw->cur_pulse = 0; + csw->cur_silence_1200th = 0; + csw->num_silent_1200ths = 0; +} + + +#define TAPE_CSW_ALLOC_DELTA 100000 + +/* TOHv3: */ +int csw_append_pulse (csw_state_t * const csw, + uint32_t const pulse_len_smps, + const tape_interval_t * const interval_or_null) { /* TOHv4.3: interval arg */ + uint32_t z; + csw_pulse_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(csw_pulse_t)); + if (NULL == p) { + log_warn("csw: out of memory enlarging CSW"); + return TAPE_E_MALLOC; + } + memset(p + csw->pulses_fill, /* wipe new portion */ + 0, + sizeof(csw_pulse_t) * TAPE_CSW_ALLOC_DELTA); + csw->pulses = p; + csw->pulses_alloc = z; + } + csw->pulses[csw->pulses_fill].len_smps = pulse_len_smps; + if (NULL == interval_or_null) { + memset(&(csw->pulses[csw->pulses_fill].timespan), + 0, + sizeof(tape_interval_t)); + } else { + csw->pulses[csw->pulses_fill].timespan = *interval_or_null; + } + (csw->pulses_fill)++; + return TAPE_E_OK; +} + + +/* TOHv3: */ +int csw_append_pulse_fractional_length (csw_state_t * const csw, + double const pulse_len_smps, + const tape_interval_t * const interval_or_null) { /* TOHv4.3: interval arg */ + + 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, interval_or_null); + +} + + + +/* 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 * const csw, + uint32_t const num_1200ths, + int32_t start_1200ths) { + uint32_t j; + int e; + tape_interval_t i; + csw_init_blank_if_necessary(csw); + i.start_1200ths = start_1200ths; + for (j=0, e=TAPE_E_OK; (TAPE_E_OK == e) && (j < (num_1200ths * 4)); j++) { + if (3==(j&3)) { /* TOHv4.3: update interval as we go */ + i.pos_1200ths = 1; + } else { + i.pos_1200ths = 0; + } + e = csw_append_pulse_fractional_length (csw, + csw->len_1200th_smps_perfect / 4.0, + &i); /* TOHv4.3: timespan */ + if (3==(j&3)) { /* TOHv4.3: update interval as we go */ + i.start_1200ths++; + } + } + return e; +} + + +/* TOHv3: */ +int csw_append_silence (csw_state_t * const csw, + float len_s, + const tape_interval_t * const interval_or_null) { + uint32_t smps; + float sane; + int e; + tape_interval_t i1, i2; + /* 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. + (TOHv4.3: We'll have to split the supplied interval.) */ + memset(&i1, 0, sizeof(tape_interval_t)); + memset(&i2, 0, sizeof(tape_interval_t)); + if (NULL != interval_or_null) { + i1 = *interval_or_null; + i1.pos_1200ths /= 2; + i2.start_1200ths = i1.start_1200ths + i1.pos_1200ths; + i2.pos_1200ths = interval_or_null->pos_1200ths - i1.pos_1200ths; /* remainder to interval 2 */ + } + smps = (uint32_t) ((len_s / 2.0f) * (float) csw->header.rate); + e = csw_append_pulse (csw, smps, &i1); + if (TAPE_E_OK == e) { e = csw_append_pulse (csw, smps, &i2); } + 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 + this is pointless, 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].len_smps <= 255) { + if (1 == pass) { + buf[pos] = 0xff & csw->pulses[n].len_smps; } - 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].len_smps); /* 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; + } } - infilenames = 0; - csw_indat = temps; - csw_intone = tempi; - csw_datbits = tempd; - csw_skip = tempsk; - sysacia_tapespeed = tempspd; - csw_point = temp; - csw_loop = 0; + } + + /* 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); + } + } + header[pos_compress_flag] = 1; /* TOHv4: deferred compress flag */ + } else { + /* compress it */ + e = tape_zlib_compress (buf, + pos, + false, /* 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; + +} + +int csw_ffwd_to_end (csw_state_t *csw) { + csw->cur_pulse = csw->pulses_fill; + return TAPE_E_OK; } +/* TOHv4.3 */ +void csw_get_duration_1200ths (const csw_state_t * const csw, + int32_t * const dur_1200ths_out) { + tape_interval_t *interval; + *dur_1200ths_out = 0; + if (csw->pulses_fill <= 0) { return; } + interval = &(csw->pulses[csw->pulses_fill-1].timespan); + *dur_1200ths_out = interval->start_1200ths + interval->pos_1200ths; + +} + +/* TOHv4.3, for seeking */ +int csw_change_current_pulse (csw_state_t * const csw, uint32_t const pulse_ix) { + if (pulse_ix >= csw->pulses_fill) { + log_warn("csw: BUG: seek: pulse_ix (%u) >= csw->pulses_fill (%u)", pulse_ix, csw->pulses_fill); + return TAPE_E_BUG; + } + csw->cur_pulse = pulse_ix; + csw->cur_silence_1200th = 0; + csw->num_silent_1200ths = 0; + return TAPE_E_OK; +} diff -uN b-em-0b6f1d2-vanilla/src/csw.h b-em-0b6f1d2-TOH/src/csw.h --- b-em-0b6f1d2-vanilla/src/csw.h 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/csw.h 2025-10-27 05:59:55.627824190 +0000 @@ -1,12 +1,103 @@ +/* + * B-Em + * This file (C) 2025 'Diminished' + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + #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 */ +#include "tapeseek.h" + +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; + +/* TOHv4.3 */ +typedef struct csw_pulse_s { + uint32_t len_smps; + tape_interval_t timespan; +} csw_pulse_t; + +typedef struct csw_state_s { + + csw_header_t header; + csw_pulse_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 */ + double thresh_smps_perfect; + double len_1200th_smps_perfect; + + /* reader state: */ + uint32_t cur_pulse; + + int32_t num_silent_1200ths; + int32_t cur_silence_1200th; + +} 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 * const csw, + char * const out_1200th_or_null, + bool initial_scan, + int32_t * const elapsed_out_or_null); /* TOHv4.3: return elapsed time if available (or -1) */ +bool csw_peek_eof (const csw_state_t * const csw); +void csw_force_eof (csw_state_t * const csw); +void csw_finish (csw_state_t *csw); -extern int csw_ena; -extern int csw_toneon; - +/* TOHv3: */ +int csw_append_pulse (csw_state_t * const csw, + uint32_t const pulse_len_smps, + const tape_interval_t * const interval_or_null); /* TOHv4.3: interval arg */ +int csw_append_pulse_fractional_length (csw_state_t *csw, + double pulse_len_smps, + const tape_interval_t * const interval_or_null) ; /* TOHv4.3: interval arg */ +int csw_init_blank_if_necessary (csw_state_t *csw); +int csw_append_leader (csw_state_t * const csw, + uint32_t const num_1200ths, + int32_t start_1200ths); /* TOHv4.3 */ +int csw_append_silence (csw_state_t * const csw, + float len_s, + const tape_interval_t * const interval_or_null); +/* TOHv3: */ +int csw_build_output (csw_state_t *csw, + uint8_t compress, + char **out, + size_t *len_out); +int csw_ffwd_to_end (csw_state_t *csw); +void csw_get_duration_1200ths (const csw_state_t * const csw, int32_t * const dur_1200ths_out); /* TOHv4.3 */ +int csw_change_current_pulse (csw_state_t * const csw, uint32_t const pulse_ix); /* TOHv4.3 */ #endif + Common subdirectories: b-em-0b6f1d2-vanilla/src/darm and b-em-0b6f1d2-TOH/src/darm diff -uN b-em-0b6f1d2-vanilla/src/debugger.c b-em-0b6f1d2-TOH/src/debugger.c --- b-em-0b6f1d2-vanilla/src/debugger.c 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/debugger.c 2025-10-31 15:36:03.607527764 +0000 @@ -17,6 +17,8 @@ #include "6502.h" #include "keyboard.h" #include "debugger_symbols.h" +#include "sysacia.h" +#include "tapeseek.h" #include @@ -92,6 +94,21 @@ char *arg, breakpoint **out_found, breakpoint **out_prev); +/* TOHv4.3 */ +static int debug_tapeseek_time (char * const iptr, + int const len, + tape_state_t * const ts, + tape_vars_t * const tv); +static int debug_tapeseek_percent (const char * const iptr, + tape_state_t * const ts, + tape_vars_t * const tv); +static int debug_tapeseek (const char * const iptr, + tape_state_t * const ts, + tape_vars_t * const tv); + +static int debug_taperec (const char * const iptr); +static void debug_tape_ls_uef (const tape_state_t * const ts); /* TOHv4.3-a4 */ +static void debug_tape_ls_tibet (tape_state_t * const ts); /* TOHv4.3-a4 */ static void close_trace(const char *why) { @@ -497,6 +514,11 @@ " symlist - list all symbols\n" " swiftsym f - load symbols in swift format from file f\n" " simplesym f - load symbols in name=value format from file f\n" + " taperec m - switch tape record-and-append mode 'on' or 'off'\n" /* TOH */ + " tapeseek t - seek to tape time ([h:][mm:]ss.sss, or xx.x%)\n" /* TOHv4.3 */ + " tapeeject - eject currently-loaded tape\n" /* TOHv4.3 */ + " tapelsuef - if UEF loaded, list its chunk information\n" /* TOHv4.3-a4 */ + " tapelstbt - if TIBET loaded, list its span information\n" /* TOHv4.3-a4 */ " trace fn - trace disassembly/registers to file, close file if no fn\n" " trange s e - trace the range s to e (replaces tracing everything)\n" " vrefresh t - extra video refresh on entering debugger. t=on or off\n" @@ -607,11 +629,10 @@ } static uint32_t parse_address_or_symbol(cpu_debug_t *cpu, const char *arg, const char **endret) { - - uint32_t a; - //first see if there is a symbol - if (symbol_find_by_name(cpu->symbols, arg, &a, endret)) - return a; + uint32_t addr; + // first see if there is a symbol + if (symbol_find_by_name(cpu->symbols, arg, &addr, endret)) + return addr; return cpu->parse_addr(cpu, arg, endret); } @@ -1382,6 +1403,8 @@ debug_outf("missing address"); } +#include "tape.h" + void debugger_do(cpu_debug_t *cpu, uint32_t addr) { uint32_t next_addr; @@ -1681,6 +1704,16 @@ debug_tracecmd(cpu, iptr); else if (!strncmp(cmd, "trange", cmdlen)) debug_trange(cpu, iptr); + else if (!strncmp(cmd, "tapeseek", cmdlen)) /* TOHv4.3 */ + debug_tapeseek(iptr, &tape_state, &tape_vars); + else if (!strncmp(cmd, "tapeeject", cmdlen)) /* TOHv4.3 */ + tape_ejected_by_user(&tape_state, &tape_vars, &sysacia); + else if (!strncmp(cmd, "tapelsuef", cmdlen)) /* TOHv4.3-a4 */ + debug_tape_ls_uef(&tape_state); + else if (!strncmp(cmd, "tapelstbt",cmdlen)) /* TOHv4.3-a4 */ + debug_tape_ls_tibet(&tape_state); + else if (!strncmp(cmd, "taperec", cmdlen)) /* TOHv4.3 */ + debug_taperec(iptr); else badcmd = true; break; @@ -1744,6 +1777,261 @@ } } +#include "tape2.h" +#include "tapectrl.h" +#include "tape.h" +#include "gui-allegro.h" + +/* TOHv4.3-a4 */ +static void debug_tape_ls_uef (const tape_state_t * const ts) { + int32_t i; + const char *hdr; + if (TAPE_FILETYPE_BITS_NONE == ts->filetype_bits) { + debug_outf("No tape is loaded.\n"); + return; + } else if ( ! (ts->filetype_bits & TAPE_FILETYPE_BITS_UEF) ) { + debug_outf("Tape has no UEF representation!\n"); + return; + } + hdr = "\n # | Type| Len | Start | Dur. |\n" + " | | |~s/1202|~s/1202|\n\n"; + for (i=0; i < ts->uef.num_chunks; i++) { + uef_chunk_t *uc; + uc = ts->uef.chunks + i; + if (!(i%100)) { debug_outf(hdr); } + debug_outf("%4d|&%4x|%5u|%7d|%7d|\n", + i, + uc->type, + uc->len, + uc->elapsed.start_1200ths, + uc->elapsed.pos_1200ths); + } + +} + + +static void debug_tape_ls_tibet (tape_state_t * const ts) { + int e; + uint32_t u, num_spans; + const char *hdr; + tibet_t *t; + if (TAPE_FILETYPE_BITS_NONE == ts->filetype_bits) { + debug_outf("No tape is loaded.\n"); + return; + } else if ( ! (ts->filetype_bits & TAPE_FILETYPE_BITS_TIBET) ) { + debug_outf("Tape has no TIBET representation!\n"); + return; + } + t = &(ts->tibet); + e = tibet_get_num_spans(t, &num_spans); + if (TAPE_E_OK != e) { return; } + hdr = "\n # | Type | Start | Dur. |\n" + " | | s/1202| s/1202|\n\n"; + for (u=0; u < num_spans; u++) { + char type; + const char *type_s; + tape_interval_t iv; + type=0; + type_s = NULL; + if (!(u%100)) { debug_outf(hdr); } + e = tibet_get_type_for_span(t, u, &type); + if (TAPE_E_OK != e) { return; } + e = tibet_get_time_interval_for_span(t, u, &iv); + if (TAPE_E_OK != e) { return; } + if (TIBET_SPAN_SILENT==type) { + type_s = "silent"; + } else if (TIBET_SPAN_LEADER==type) { + type_s = "leader"; + } else if (TIBET_SPAN_DATA==type) { + type_s = " data "; + } + debug_outf("%4u|%6s|%7d|%7d|\n", + u, + type_s, + iv.start_1200ths, + iv.pos_1200ths); + } +} + +static int debug_taperec (const char * const iptr) { + bool b; + bool tapectrl_opened; +#ifdef BUILD_TAPE_TAPECTRL + int e; + int32_t pos; +#endif + if ((strcmp("on",iptr)) && (strcmp("off",iptr))) { + debug_out("Mode must be on or off\n",23); + return TAPE_E_DEBUG_BAD_TAPEREC; + } + b = ('f'==iptr[1])?false:true; + tapectrl_opened = false; +#ifdef BUILD_TAPE_TAPECTRL + pos=0; + if (tape_vars.tapectrl_opened) { /* triage, mainthread-only flag */ + e = tape_get_duration_1200ths(&tape_state, &pos); + if (TAPE_E_OK != e) { return e; } + /* actual tapectrl.display will be checked under lock: */ + tapectrl_set_record (&(tape_vars.tapectrl), b, pos); + } +#endif + gui_set_record_mode(b); + return tape_set_record_activated (&tape_state, + &tape_vars, + &sysacia, + b, + tapectrl_opened); +} + + +static int debug_tapeseek (const char * const iptr, + tape_state_t * const ts, + tape_vars_t * const tv) { + + int e; + size_t len; + int leni; + char *p; + + e = TAPE_E_OK; + + len = strlen(iptr); + if ((0==len)||(len>256)) { + debug_out("Seek time was bad!\n", 19); + return TAPE_E_DEBUG_BAD_TAPESEEK; + } + + leni = 0x7fffffff & len; + + p = malloc(leni+1); + if (NULL==p) { + debug_out("Malloc failure\n", 15); + return TAPE_E_MALLOC; + } + memcpy(p,iptr,leni+1); + + if ('%'==p[leni-1]) { + p[leni-1] = '\0'; + e = debug_tapeseek_percent(p, ts, tv); + p[leni-1] = '%'; + } else { + e = debug_tapeseek_time(p, leni, ts, tv); + } + free(p); + + return e; +} + +#include "tapectrl.h" /* TOHv4.3 */ + +/* TOHv4.3 */ +static int debug_tapeseek_percent (const char * const iptr, + tape_state_t * const ts, + tape_vars_t * const tv) { + double d; + char *p; + d = strtod(iptr, &p); + if ((NULL == p) || (p == iptr)) { + debug_out("Seek fraction was bad\n",22); + return TAPE_E_DEBUG_BAD_TAPESEEK; + } + if (d>100.0) { d=100.0; } + if (d<0.0) { d=0.0; } + /* OK. Push out a pair of synthetic seek messages to both the main thread + and the tapectrl GUI thread. */ + return tape_seek_for_debug (ts, tv, d / 100.0); +} + +#include "gui-allegro.h" + + + + +static int debug_tapeseek_time (char * const iptr, + int const len, + tape_state_t * const ts, + tape_vars_t * const tv) { + + double secs; + int i,h,m,e,seps; + int32_t duration_1200ths; + + for (i=len-1, secs=-1.0, m=-1, h=-1, e=TAPE_E_OK, seps=0; + (i>-2) && (seps<4); + i--) { + + char c,*next; + + c = '\0'; + if (i>=0) { c = iptr[i]; } + + if ((-1==i)||(':'==c)) { + seps++; /* start of segment, or start of string */ + } else { + continue; /* pass over digits */ + } + + if ( (-1 != i) && ( (!isdigit(c)) && (c!=':') && (c!='.') ) ) { + seps = 8; + break; + } + + next = iptr+i+1; + + if (1==seps) { + if (i==(len-1)) { + seps = 5; /* empty segment, throw error */ + } else { + secs = atof(next); + if (secs < 0.0) { secs = 0.0; } /* should be impossible */ + if (i>-1) { iptr[i]='\0'; } /* null-terminate the next segment (minutes) */ + } + } else if (2==seps) { + if ((secs > 60.0) || (((*next)=='\0')&&(i<(len-1)))) { + seps = 6; /* throw error */ + } else { + m = atoi(next); + if (m < 0) { m=0; } /* should be impossible */ + if (i>-1) { iptr[i]='\0'; } /* null-terminate the next segment (hours) */ + } + } else if (3==seps) { + if ((m > 60) || ((*next)=='\0')) { + seps = 7; /* throw error */ + } else { + h = atoi(next); + if (h < 0) { h=0; } /* should be impossible */ + if (h > 10) { seps = 8; } /* throw error */ + } + } + } + + if (seps>3) { + debug_out("Bad time\n",9); + return TAPE_E_DEBUG_BAD_TAPESEEK; + } + + if (m>0) { secs+=(m*60); } + if (h>0) { secs+=(h*60*60); } + + /* hack: make sure time displayed rounds up */ + secs += 0.001; + + secs *= TAPE_1200_HZ; + + duration_1200ths = 0; + e = tape_get_duration_1200ths (ts, &duration_1200ths); + if (TAPE_E_OK != e) { return e; } + + if (secs >= (double) duration_1200ths) { + debug_out("Time exceeds duration\n",22); + return TAPE_E_DEBUG_BAD_TAPESEEK; + } + + return tape_seek_for_debug (ts, tv, (secs / (double) duration_1200ths)); + +} + + static void hit_point(cpu_debug_t *cpu, uint32_t addr, uint32_t value, const char *enter, const char *desc) { char addr_str[20 + SYM_MAX], iaddr_str[20 + SYM_MAX]; Common subdirectories: b-em-0b6f1d2-vanilla/src/.deps and b-em-0b6f1d2-TOH/src/.deps diff -uN b-em-0b6f1d2-vanilla/src/gui-allegro.c b-em-0b6f1d2-TOH/src/gui-allegro.c --- b-em-0b6f1d2-vanilla/src/gui-allegro.c 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/gui-allegro.c 2025-11-01 15:10:58.822186226 +0000 @@ -1,5 +1,6 @@ #include "b-em.h" #include +#include /* TOHv4.3 */ #include "gui-allegro.h" #include "6502.h" @@ -35,6 +36,9 @@ #include "video.h" #include "video_render.h" #include "vdfs.h" +#include "serial.h" +#include "tapenoise.h" /* TOHv4.1-rc9 */ +#include "tapectrl.h" /* TOHv4.3 */ #if defined(HAVE_JACK_JACK_H) || defined(HAVE_ALSA_ASOUNDLIB_H) #define HAVE_LINUX_MIDI @@ -57,6 +61,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 +224,103 @@ al_set_menu_item_caption(disc_menu, menu_id_num(IDM_DISC_EJECT, drive), temp); } + +#include "taperead.h" + 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); + +#ifdef BUILD_TAPE_TAPECTRL + fflags = 0; + al_append_menu_item(menu, "Control tape...", IDM_TAPE_TAPECTRL, fflags, NULL, NULL); +#endif + + /* 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 { + fflags = ALLEGRO_MENU_ITEM_CHECKBOX; + } + 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 { - 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, "WAV: Use cosine, not sine", + IDM_TAPE_OPTS_SAVE_WAV_PHASE_SHIFT, fflags, NULL, NULL); + + /* LOAD OPTIONS */ + if ( tape_vars.permit_phantoms ) { /* TOHv4.3 */ + fflags = ALLEGRO_MENU_ITEM_CHECKBOX; + } else { + fflags = ALLEGRO_MENU_ITEM_CHECKBOX|ALLEGRO_MENU_ITEM_CHECKED; + } + al_append_menu_item(tape_opts_load_menu, "Strip squawks and 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); +#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; } @@ -463,7 +563,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); @@ -639,6 +740,102 @@ al_register_event_source(queue, al_get_default_menu_event_source()); } +/* TOHv3.2 */ +void gui_set_record_mode (bool const 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 const 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 (const char * const 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 63 + 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()); @@ -997,52 +1194,276 @@ static void tape_load_ui(ALLEGRO_EVENT *event) { const char *fpath; - if (!tape_fn || !(fpath = al_path_cstr(tape_fn, ALLEGRO_NATIVE_PATH_SEP))) + bool tapectrl_opened; /* TOHv4.3-a3 */ + /*int e;*/ /* TOHv4.3 */ + 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 */ + tapectrl_opened = false; +#ifdef BUILD_TAPE_TAPECTRL + tapectrl_opened = tape_vars.tapectrl_opened; +#endif + tape_set_record_activated(&tape_state, &tape_vars, &sysacia, false, tapectrl_opened); ALLEGRO_PATH *path = al_create_path(al_get_native_file_dialog_path(chooser, 0)); - tape_load(path); - tape_fn = path; - tape_loaded = 1; + /*e =*/ tape_load(&tape_state, &tape_vars, path); /* TOHv4.3: tape_handle_exception() should deal with errors, so ignore return code */ + 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. */ + 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; } -static void tape_eject(void) +/* 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 gui_eject_tape(void) { - tape_close(); - tape_loaded = 0; + tape_ejected_by_user(&tape_state, &tape_vars, &sysacia); } -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_toggle_filter_phantoms (ALLEGRO_EVENT *event) { + ALLEGRO_MENU *menu = (ALLEGRO_MENU *)(event->user.data3); + + /* toggle */ + if (tape_vars.permit_phantoms) { + tape_vars.permit_phantoms = false; + al_set_menu_item_flags(menu, + IDM_TAPE_OPTS_LOAD_FILTER_PHANTOMS, + ALLEGRO_MENU_ITEM_CHECKBOX|ALLEGRO_MENU_ITEM_CHECKED); + } else { + tape_vars.permit_phantoms = true; + 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); } } -static void tape_fast(ALLEGRO_EVENT *event) +/* 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); } } @@ -1183,8 +1604,25 @@ static const char all_dext[] = "*.ssd;*.dsd;*.img;*.adf;*.ads;*.adm;*.adl;*.sdd;*.ddd;*.fdi;*.imd;*.hfe;" "*.SSD;*.DSD;*.IMG;*.ADF;*.ADS;*.ADM;*.ADL;*.SDD;*.DDD;*.FDI;*.IMD;*.HFE"; +#include "tapeseek.h" + void gui_allegro_event(ALLEGRO_EVENT *event) { + /* TOHv4.3 */ + bool tape_rec, tcw_opened; + tape_ctrl_window_t *tcw; +#ifdef BUILD_TAPE_TAPECTRL + int e; + int32_t tape_pos_1200ths; +#endif + + tcw = NULL; + tcw_opened = false; +#ifdef BUILD_TAPE_TAPECTRL + tcw = &(tape_vars.tapectrl); + tcw_opened = tape_vars.tapectrl_opened; +#endif + switch(menu_get_id(event)) { case IDM_ZERO: break; @@ -1301,21 +1739,116 @@ 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_rec = ! tape_is_record_activated (&tape_vars); +#ifdef BUILD_TAPE_TAPECTRL + if (tcw_opened) { + tape_pos_1200ths = 0; + if (tape_rec) { + tape_get_duration_1200ths(&tape_state, &tape_pos_1200ths); + } + tapectrl_set_record(&(tape_vars.tapectrl), tape_rec, tape_pos_1200ths); + } +#endif + tape_set_record_activated (&tape_state, + &tape_vars, + &sysacia, + tape_rec, + tcw_opened); + 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, + tcw, + tape_vars.record_activated, + tcw_opened); break; case IDM_TAPE_EJECT: - tape_eject(); + gui_eject_tape(); 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; +#ifdef BUILD_TAPE_TAPECTRL + case IDM_TAPE_TAPECTRL: /* TOHv4.3 */ + if ( ! tape_vars.tapectrl_opened ) { + e = tapectrl_start_gui_thread (&tape_state, + &tape_vars, + tape_vars.tapectrl_allow_resize, + tape_vars.tapectrl_ui_scale * ( hiresdisplay ? 2.0f : 1.0f)); + if (TAPE_E_TAPECTRL_THREAD_EXISTS == e) { + e = TAPE_E_OK; /* trap this */ + } else { + /* It is not really necessary to shut down the entire tape system just because + * the tape control window failed to open. Do it anyway. */ + 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, + true); + } + } +#endif + 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; @@ -1390,6 +1923,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; @@ -1508,3 +2046,4 @@ break; } } + diff -uN b-em-0b6f1d2-vanilla/src/gui-allegro.h b-em-0b6f1d2-TOH/src/gui-allegro.h --- b-em-0b6f1d2-vanilla/src/gui-allegro.h 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/gui-allegro.h 2025-10-15 13:59:06.711191016 +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,34 @@ 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, +#ifdef BUILD_TAPE_TAPECTRL + IDM_TAPE_TAPECTRL, /* TOHv4.3 */ +#endif + 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, @@ -74,6 +99,7 @@ IDM_SOUND_DAC, IDM_SOUND_DDNOISE, IDM_SOUND_TAPE, + IDM_SOUND_TAPE_RELAY, /* TOHv2 */ IDM_SOUND_FILTER, IDM_WAVE, IDM_SID_TYPE, @@ -121,5 +147,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 const filetype_bits); +extern void gui_alter_tape_menus_2(void); +void gui_alter_tape_eject (const char * const path); +extern void gui_set_record_mode (bool const activated); /* TOHv3.2 */ #endif diff -uN b-em-0b6f1d2-vanilla/src/linux-gui.c b-em-0b6f1d2-TOH/src/linux-gui.c --- b-em-0b6f1d2-vanilla/src/linux-gui.c 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/linux-gui.c 2025-10-15 13:59:06.711561576 +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 -uN b-em-0b6f1d2-vanilla/src/main.c b-em-0b6f1d2-TOH/src/main.c --- b-em-0b6f1d2-vanilla/src/main.c 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/main.c 2025-10-27 06:17:48.724603623 +0000 @@ -47,6 +47,7 @@ #include "sysacia.h" #include "tape.h" #include "tapecat-allegro.h" +#include "tape-io.h" #include "tapenoise.h" #include "tube.h" #include "via.h" @@ -125,6 +126,14 @@ 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); +#ifdef BUILD_TAPE_TAPECTRL +static int toh_cli_handle_tapeuiscale (const char * const argv); +#endif + void main_reset() { m6502_reset(); @@ -156,8 +165,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" @@ -177,7 +186,24 @@ "-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 | 32=tapectrl\n" /* TOHv4.1, v4.3 */ + "-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 */ +#ifdef BUILD_TAPE_TAPECTRL + "-tapeuiscale n - set scale at which to display tape control window (1.0 - 4.0)\n" /* TOHv4.3 */ + "-tapeuiresize - allow free resize of tape control window (may cause crashes)\n" /* TOHv4.3 */ +#endif + "-expire n - quit, with predictable exit code, after n emulated seconds\n" /* TOHv3.3 */ + "-tapenopbp - do not filter out phantom blocks on tape load\n" + "\n"; static double main_calc_timer(int speed) { @@ -255,8 +281,18 @@ OPT_PASTE_KBD, OPT_PRINT, OPT_GROUND, + /* TOHv4.2 merge: */ + OPT_TAPETEST, + OPT_TAPESAVE, +#ifdef BUILD_TAPE_TAPECTRL + OPT_TAPEUISCALE, /* TOHv4.3 */ +#endif + OPT_RS423FILE, + OPT_EXPIRE } opt_state; +#include "tapeseek.h" + void main_init(int argc, char *argv[]) { if (!al_init()) { @@ -269,8 +305,33 @@ 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; + int32_t tape_duration_1200ths; + bool tcw_opened; /* TOHv4.3 */ + + /* TOHv3.3, TOHv4, TOHv4.2 merge */ + shutdown_exit_code = SHUTDOWN_OK; + tape_vars.testing_mode = 0; + e = SHUTDOWN_OK; + rs423_fn = NULL; + tcw_opened = false; + + /* TOHv4.2 merge */ + tape_init(&tape_state, &tape_vars); + + /* TOHv4.3 */ +#ifdef BUILD_TAPE_TAPECTRL +#ifdef BUILD_TAPE_TAPECTRL_ALLOW_RESIZE + tape_vars.tapectrl_allow_resize = true; +#endif +#ifdef BUILD_TAPE_TAPECTRL_DOUBLE_SIZE + tape_vars.tapectrl_ui_scale = 2.0f; +#endif +#endif while (--argc) { + ALLEGRO_PATH *path; char *arg = *++argv; switch (state) { case OPT_GROUND: @@ -286,7 +347,48 @@ } 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 = true; + } else if (!strcasecmp(arg, "tape117")) { /* TOHv3.2: must come before -tape */ + tape_vars.save_always_117 = true; + } else if (!strcasecmp(arg, "tape112")) { /* TOHv3.2: must come before -tape */ + tape_vars.save_prefer_112 = true; + } else if (!strcasecmp(arg, "tapeno0")) { /* TOHv3.2: must come before -tape */ + tape_vars.save_do_not_generate_origin_on_append = true; + } else if (!strcasecmp(arg, "tapenopbp")) { /* TOHv3.2: must come before -tape */ + tape_vars.permit_phantoms = true; + } else if (!strcasecmp(arg, "tapewavcosine")) { /* TOHv4.2 */ + tape_vars.wav_use_phase_shift = true; +#ifdef BUILD_TAPE_TAPECTRL /* TOHv4.3 */ + } else if (!strcasecmp(arg, "tapeuiscale")) { + state = OPT_TAPEUISCALE; + } else if (!strcasecmp(arg, "tapeuiresize")) { + tape_vars.tapectrl_allow_resize = true; +#endif + } else if (!strcasecmp(arg, "record")) { /* TOHv3 */ + // tape_state.display + tape_get_duration_1200ths(&tape_state, &tape_duration_1200ths); +#ifdef BUILD_TAPE_TAPECTRL + tapectrl_set_record(&(tape_vars.tapectrl), false, tape_duration_1200ths); + tcw_opened = true; +#endif + tape_set_record_activated(&tape_state, + &tape_vars, + &sysacia, + true, + tcw_opened); + //gui_set_record_mode(false); + /* end TOHv4.2 merge */ + } else if (!strcasecmp(arg, "tape")) state = OPT_TAPE; else if (!strcasecmp(arg, "disc") || !strcasecmp(arg, "-disk")) state = OPT_DISC1; @@ -297,7 +399,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') { @@ -357,17 +459,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) @@ -391,11 +496,41 @@ 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; +#ifdef BUILD_TAPE_TAPECTRL + case OPT_TAPEUISCALE: /* TOHv4.3 */ + e = toh_cli_handle_tapeuiscale(arg); + break; +#endif + 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 */ case OPT_EXEC: exec_fn = arg; break; @@ -414,14 +549,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(); @@ -430,13 +569,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(); @@ -512,7 +660,48 @@ else disc_load(0, drives[0].discfn); disc_load(1, drives[1].discfn); - tape_load(tape_fn); + + /* TOHv4.3 */ + /* need the mutex for tapectrl to exist for all time, so set that up now */ +#ifdef BUILD_TAPE_TAPECTRL + tape_vars.tapectrl.mutex = al_create_mutex(); + if (NULL == tape_vars.tapectrl.mutex) { + log_warn("ERROR: Failed to create tapectrl mutex"); + exit(SHUTDOWN_STARTUP_FAILURE /* 1 */); + } +#endif + + /* TOHv4.2 merge */ + if (tape_vars.load_filename != NULL) { + e = tape_load(&tape_state, &tape_vars, tape_vars.load_filename); /* TOHv4.3: must load before tapectrl spawn */ + } + if (TAPE_E_OK != e) { + log_warn("ERROR: Failed to load tape (code %d)", e); + /* not fatal, but shutdown_exit_code was set by tape_handle_exception(), + so the tests can work their magic */ + } + +#ifdef BUILD_TAPE_TAPECTRL /* TOHv4.3 */ + if ( ( TAPE_E_OK == e ) + && ( TAPE_E_OK == tape_state.prior_exception ) + && ( tape_vars.testing_mode & 32 ) + // && ! tape_state.disabled_due_to_error + && ! tape_vars.tapectrl_opened ) { + + e = tapectrl_start_gui_thread (&tape_state, + &tape_vars, + tape_vars.tapectrl_allow_resize, + tape_vars.tapectrl_ui_scale * ( hiresdisplay ? 2.0f : 1.0f)); + if (TAPE_E_OK != e) { + log_warn("ERROR: Failed to start tapectrl window (code %d)", e); + exit(SHUTDOWN_STARTUP_FAILURE /* 1 */); + } + + } +#endif + + e = TAPE_E_OK; + if (mmccard_fn) mmccard_load(mmccard_fn); if (defaultwriteprot) @@ -522,11 +711,147 @@ 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 retarding startup */ + if (tape_vars.testing_mode & TAPE_TEST_SLOW_STARTUP) { +#ifdef WIN32 + Sleep(1000); +#else + sleep(1); +#endif + } + + /* TOHv3: if tape testing mode _CAT_ON_STARTUP 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.permit_phantoms); + } + if (rs423_fn != NULL) { /* TOHv4: for -rs423file */ + sysacia_rec_start(rs423_fn); + } + +} + + +#define TAPEUISCALE_MIN 0.5 +#define TAPEUISCALE_MAX 1.5 +#define TAPEUISCALE_EPSILON 0.0001 + +#ifdef BUILD_TAPE_TAPECTRL +static int toh_cli_handle_tapeuiscale (const char * const argv) { + float scale; + scale = (float) atof(argv); + if ( (scale < (TAPEUISCALE_MIN - TAPEUISCALE_EPSILON)) + || (scale > (TAPEUISCALE_MAX + TAPEUISCALE_EPSILON))) { + fprintf(stderr, + "\"-tapeuiscale\" value was illegal (legal range %.1f - %.1f): %s\n", + TAPEUISCALE_MIN, TAPEUISCALE_MAX, argv); + return SHUTDOWN_STARTUP_FAILURE; + } + tape_vars.tapectrl_ui_scale = scale; + return TAPE_E_OK; +} +#endif + +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 */ + // if (tape_vars.testing_mode & 32) { + // tape_vars.open_tapectrl_at_start = true; + // } + /* 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() @@ -648,7 +973,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); } @@ -690,6 +1016,15 @@ else slow_count = 0; } + +#ifdef BUILD_TAPE_TAPECTRL + /* TOHv4.3: handle messages from tape control window */ + if ((now - prev_time) > 0.1) { + /* this calls tape_handle_exception() itself, so will probably always return _OK: */ + /*e =*/ tapectrl_handle_messages(&tape_vars, &tape_state, &sysacia); + } +#endif + execs = 0; prev_time = now; } @@ -699,6 +1034,8 @@ event.type = ALLEGRO_EVENT_TIMER; al_emit_user_event(&evsrc, &event, NULL); } + + } static double last_switch_in = 0.0; @@ -708,7 +1045,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) { @@ -778,21 +1120,35 @@ 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.permit_phantoms); } + log_debug("main: end loop"); } void main_close() { + + /* TOHv4.3 */ + tape_vars_t *tv; + tape_state_t *ts; + tv = &tape_vars; + ts = &tape_state; + + /* TOHv4.3-a4: with tapectrl, allegro becomes crashy here on Windows + * under conditions of rapid startup/shutdown. Investigating + * reveals that the display bitmap contains an invalid pointer + * at the point of the al_destroy_display() call in video_close(). + * Disposing of the display bitmap ASAP early seems to mitigate this + * crash. */ + al_set_target_bitmap(NULL); + gui_tapecat_close(); gui_keydefine_close(); + + sysacia_rec_stop(); /* TOHv4 */ debug_kill(); @@ -801,8 +1157,9 @@ midi_close(); mem_close(); - uef_close(); - csw_close(); + + /* TOHv3 */ + tube_6502_close(); arm_close(); x86_close(); @@ -819,10 +1176,19 @@ music5000_close(); ddnoise_close(); tapenoise_close(); - tape_free(); al_destroy_timer(timer); al_destroy_event_queue(queue); led_close(); +#ifdef BUILD_TAPE_TAPECTRL + tapectrl_finish(&(tv->tapectrl), &(tv->tapectrl_opened)); +#endif + tape_save_on_shutdown(ts, + tv->record_activated, + &(ts->filetype_bits), /* save using same type as we loaded (or any type if not loaded) */ + tv->save_prefer_112, /* TOHv3.2 */ + tv->wav_use_phase_shift, /* TOHv4.2 */ + &(tv->quitsave_config)); + tape_state_finish(ts, 0); /* 0 = don't alter menus */ video_close(); model_close(); log_close(); diff -uN b-em-0b6f1d2-vanilla/src/Makefile.am b-em-0b6f1d2-TOH/src/Makefile.am --- b-em-0b6f1d2-vanilla/src/Makefile.am 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/Makefile.am 2025-10-27 06:33:20.499660837 +0000 @@ -105,8 +105,15 @@ sysacia.c \ sysvia.c \ tape.c \ + tapectrl.c \ + tapeseek.c \ + taperead.c \ + tapewrite.c \ + tape-interval.c \ + tape-io.c \ tapecat-allegro.c \ tapenoise.c \ + tibet.c \ pdp11/pdp11.c \ pdp11/pdp11_debug.c \ textsave.c \ Common subdirectories: b-em-0b6f1d2-vanilla/src/mc6809nc and b-em-0b6f1d2-TOH/src/mc6809nc diff -uN b-em-0b6f1d2-vanilla/src/midi-linux.c b-em-0b6f1d2-TOH/src/midi-linux.c --- b-em-0b6f1d2-vanilla/src/midi-linux.c 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/midi-linux.c 2025-10-15 13:59:06.712575125 +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) { diff -uN b-em-0b6f1d2-vanilla/src/model.c b-em-0b6f1d2-TOH/src/model.c --- b-em-0b6f1d2-vanilla/src/model.c 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/model.c 2025-10-15 13:59:06.712825672 +0100 @@ -258,7 +258,12 @@ } void model_check(void) { - const int defmodel = 3; + int defmodel = 3; + + // BUGFIX: defend against model 3 not being defined in cfgfile + if (defmodel >= model_count) { + defmodel = model_count-1; + } if (curmodel < 0 || curmodel >= model_count) { log_warn("No model #%d, using #%d (%s) instead", curmodel, defmodel, models[defmodel].name); Common subdirectories: b-em-0b6f1d2-vanilla/src/musahi and b-em-0b6f1d2-TOH/src/musahi diff -uN b-em-0b6f1d2-vanilla/src/music2000.c b-em-0b6f1d2-TOH/src/music2000.c --- b-em-0b6f1d2-vanilla/src/music2000.c 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/music2000.c 2025-10-15 13:59:06.713032497 +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-0b6f1d2-vanilla/src/NS32016 and b-em-0b6f1d2-TOH/src/NS32016 Common subdirectories: b-em-0b6f1d2-vanilla/src/pdp11 and b-em-0b6f1d2-TOH/src/pdp11 Common subdirectories: b-em-0b6f1d2-vanilla/src/resid-fp and b-em-0b6f1d2-TOH/src/resid-fp diff -uN b-em-0b6f1d2-vanilla/src/savestate.c b-em-0b6f1d2-TOH/src/savestate.c --- b-em-0b6f1d2-vanilla/src/savestate.c 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/savestate.c 2025-10-18 05:00:16.358205679 +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_BEMSNAP4(FILE *f) { + acia_savestate_BEMSNAP4(&sysacia, f); +} +static void sysacia_loadstate_BEMSNAP4(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_BEMSNAP4); /* 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,12 +348,20 @@ case 'A': adc_loadstate(fp); break; - case 'a': + case 'a': /* BEMSNAP3 */ sysacia_loadstate(fp); break; case 'r': - serial_loadstate(fp); + serial_loadstate(fp); /* BEMSNAP3 */ break; +#ifdef BUILD_TAPE_INCOMPATIBLE_NEW_SAVE_STATES + case '@': /* BEMSNAP4 */ + sysacia_loadstate_BEMSNAP4(fp); + break; + case '$': /* BEMSNAP4 */ + serial_loadstate_BEMSNAP4(fp); + break; +#endif case 'F': vdfs_loadstate(fp); break; @@ -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 -uN b-em-0b6f1d2-vanilla/src/serial.c b-em-0b6f1d2-TOH/src/serial.c --- b-em-0b6f1d2-vanilla/src/serial.c 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/serial.c 2025-10-31 15:01:44.885806169 +0000 @@ -1,6 +1,10 @@ /*B-em v2.2 by Tom Walker Serial ULA emulation*/ +/* TOH overhaul by 'Diminished', 2025 */ + +/* Some code is from beebjit, (c) Chris Evans, under GPL */ + #include #include "b-em.h" #include "led.h" @@ -8,50 +12,312 @@ #include "sysacia.h" #include "tape.h" #include "tapenoise.h" +#include "acia.h" +#include "tapectrl.h" +#include "tapewrite.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); -} - -void serial_write(uint16_t addr, uint8_t val) -{ - serial_reg = val; - serial_transmit_rate = val & 0x7; - serial_recive_rate = (val >> 3) & 0x7; - int new_motor = val & 0x80; - if (new_motor && !motor) { - log_debug("serial: cassette motor on"); - tapenoise_motorchange(1); - led_update(LED_CASSETTE_MOTOR, 1, 0); - motor = tape_loaded; - } - else if (!new_motor && motor) { - 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; + + /* TOHv4.3 */ + tape_ctrl_window_t *tcw; + bool tcw_opened; + + /*Dunno what happens to this on reset*/ + tape_state.ula_ctrl_reg =0; + + tcw = NULL; + tcw_opened = false; +#ifdef BUILD_TAPE_TAPECTRL + tcw = &(tape_vars.tapectrl); + tcw_opened = tape_vars.tapectrl_opened; +#endif + + tape_stop_motor(&tape_state, + tcw, + /*tape_vars.save_prefer_112,*/ + /* this is just to update the display: */ + tape_vars.record_activated, /* TOHv3.2, 4.3 */ + tcw_opened); /* TOHv4.3 */ + + tape_state.start_bit_wait_count_1200ths = 0; + tape_state.ula_dcd_tape = false; /* 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) +{ + + /* TOHv4.3 */ + tape_ctrl_window_t *tcw; + bool tcw_opened; + + tape_state.ula_ctrl_reg = val; + + tcw = NULL; + tcw_opened = false; +#ifdef BUILD_TAPE_TAPECTRL + tcw = &(tape_vars.tapectrl); + tcw_opened = tape_vars.tapectrl_opened; +#endif + + int new_motor = val & 0x80; + if ( new_motor && ! tape_state.ula_motor ) { /* TOHv3.2 */ + log_debug("serial: cassette motor on"); + tape_start_motor (&tape_state, + tcw, /* TOHv4.3 */ + tape_vars.strip_silence_and_leader, /* TOHv3.2 */ + tcw_opened); /* TOHv4.3 */ + } + else if ( ( ! new_motor ) && tape_state.ula_motor ) { /* TOHv3.2 */ + log_debug("serial: cassette motor off"); + tape_stop_motor(&tape_state, + tcw, + /*tape_vars.save_prefer_112,*/ + /* this is just to update the display: */ + tape_vars.record_activated, /* TOHv3.2, 4.3 */ + tcw_opened); /* TOHv4.3 */ + } + + /* 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 */ + +/* have this return "write was silent" flag? use it to gate read tapenoise? + * so that the write tapenoise always takes priority regardless of the REC + * mode setting? */ +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_from_acia; + int64_t ns_per_bit; + serial_framing_t f; + uint8_t silent; + tape_ctrl_window_t *tcw; + tape_interval_list_t *iv_list; + bool tcw_opened; + uint32_t *since_last_tone; + + acia_tx_divider = 0; + bit_from_acia = 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_from_acia, &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_from_acia); + if (TAPE_E_OK != e) { return e; } + + if ( ts->ula_motor ) { + + /* ACIA's generated RTS line */ + silent = ((acia->control_reg & ACIA_CTRLREG_MASK_TXCTRL) == ACIA_CTRLREG_TXCTRL_2); + + if (silent) { + bit_from_acia = 'S'; /* ignore whatever came from the ACIA */ + } + + tcw = NULL; + tcw_opened = false; + iv_list = NULL; + since_last_tone = NULL; + +#ifdef BUILD_TAPE_TAPECTRL + tcw = &(tv->tapectrl); + tcw_opened = tv->tapectrl_opened; + iv_list = &(tv->interval_list); + since_last_tone = &(tv->since_last_tone_sent_to_gui); +#endif + + e = tape_write_bitclk (ts, + tcw, /* TOHv4.3 */ + iv_list, /* TOHv4.3 */ + acia, + bit_from_acia, + ns_per_bit, + emit_tapenoise && tv->record_activated, + tv->record_activated, + tv->save_always_117, + tv->save_prefer_112, + tv->save_do_not_generate_origin_on_append, + tcw_opened, + since_last_tone); + if (TAPE_E_OK != e) { return e; } + } + + } /* endif (bitclk) */ + + return e; +} + +#include "taperead.h" + +/* TOHv4 */ +/* called by 6502.c when RXC to the ACIA fires */ +int serial_rxc_clock_for_tape (tape_state_t * const ts, + tape_vars_t * const tv, + ACIA * const acia, + bool const emit_tapenoise, + bool const record_activated, + bool * const throw_eof_out) { + + int32_t acia_rx_divider; + bool fire_acia_rxc; + int e; + + /* TOHv4.3: now record_activated just disables the entire RX side. + * Probably the way forward. + * (This variable used to be passed down into tape_fire_acia_rxc) */ + if (record_activated) { return TAPE_E_OK; } + + /* 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 */ + fire_acia_rxc = false; + 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, /*record_activated,*/ throw_eof_out); + + } /* endif (ACIA RX clock fires) */ + + return e; + +} + + +/* TOHv4 */ +void serial_recompute_dividers_and_thresholds (bool overclock_tape, /* OCing is done 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 +328,197 @@ 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) { - serial_write(0, getc(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 ? 1 : 0; + 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) ? true : false; + + 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) +{ + putc(tape_state.ula_ctrl_reg, f); +} +#endif + +#ifdef BUILD_TAPE_TAPECTRL +#include "tapectrl.h" +#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 = false; /* incident line updated */ + } +// printf("dcd = %u\n", ts->ula_dcd_tape); +#ifdef BUILD_TAPE_TAPECTRL + if ((ts->ula_dcd_tape) && tv->tapectrl_opened) { + tapectrl_to_gui_msg_dcd (&(tv->tapectrl), true, true); /* TOHv4.3 */ + } +#endif + serial_push_dcd_cts_lines_to_acia (acia, ts); +} + + + + +void serial_poll_dcd_blipticks (char const bit_value_or_nil, /* '\0's are just ignored */ + bool const fast_dcd, /* used for both turbo read modes */ + int32_t * const dcd_bliptick_count_inout, /* blip-periods counter */ + bool * const 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 = false; /* may be changed later */ + + if (('0' == bit_value_or_nil) || ('S' == bit_value_or_nil)) { + *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 = true; + (*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 -uN b-em-0b6f1d2-vanilla/src/serial.h b-em-0b6f1d2-TOH/src/serial.h --- b-em-0b6f1d2-vanilla/src/serial.h 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/serial.h 2025-10-28 09:15:10.938060227 +0000 @@ -1,13 +1,64 @@ #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); +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 "tape2.h" /* for BUILD_TAPE_INCOMPATIBLE_NEW_SAVE_STATES */ + +#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 + +/* fwdrefs */ +typedef struct tape_state_s tape_state_t; +typedef struct acia ACIA; /* fwdref */ + +#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 const bit_value_or_nil, /* '\0's are just ignored */ + bool const fast_dcd, /* used for both turbo read modes */ + int32_t * const dcd_bliptick_count_inout, /* blip-periods counter */ + bool * const dcd_line_inout); /* DCD line to be manipulated */ + +/* TOHv4 */ +void serial_recompute_dividers_and_thresholds (bool 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 * const ts, + tape_vars_t * const tv, + ACIA * const acia, + bool const emit_tapenoise, + bool const record_activated, + bool * const 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 -uN b-em-0b6f1d2-vanilla/src/sound.c b-em-0b6f1d2-TOH/src/sound.c --- b-em-0b6f1d2-vanilla/src/sound.c 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/sound.c 2025-10-15 13:59:06.714079891 +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 -uN b-em-0b6f1d2-vanilla/src/sound.h b-em-0b6f1d2-TOH/src/sound.h --- b-em-0b6f1d2-vanilla/src/sound.h 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/sound.h 2025-10-15 13:59:06.714249394 +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 -uN b-em-0b6f1d2-vanilla/src/sysacia.c b-em-0b6f1d2-TOH/src/sysacia.c --- b-em-0b6f1d2-vanilla/src/sysacia.c 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/sysacia.c 2025-10-27 06:56:14.455068560 +0000 @@ -8,54 +8,93 @@ #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); + 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 -uN b-em-0b6f1d2-vanilla/src/sysacia.h b-em-0b6f1d2-TOH/src/sysacia.h --- b-em-0b6f1d2-vanilla/src/sysacia.h 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/sysacia.h 2025-10-15 13:59:06.714573386 +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 -uN b-em-0b6f1d2-vanilla/src/tape2.h b-em-0b6f1d2-TOH/src/tape2.h --- b-em-0b6f1d2-vanilla/src/tape2.h 1970-01-01 01:00:00.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/tape2.h 2025-11-01 20:20:55.636845094 +0000 @@ -0,0 +1,234 @@ +/* + * B-Em + * This file (C) 2025 'Diminished' + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef __INC_TAPE2_H +#define __INC_TAPE2_H + +/* BUILD OPTIONS */ +#define BUILD_TAPE_SANITY /* extra run-time checks */ +#define BUILD_TAPE_DEV_MENU +#define BUILD_TAPE_MENU_GREYOUT_CAT +#define BUILD_TAPE_INCOMPATIBLE_NEW_SAVE_STATES +#define BUILD_TAPE_NO_FASTTAPE_VIDEO_HACKS /* remove strange fasttape frameskip thing in video code */ +#define BUILD_TAPE_CAT_TIMESTAMPS /* new-style tape catalogue format containing timestamps */ +//#define BUILD_TAPE_CAT_NEED_NEXT_ADDR_ZERO /* exclude tapecat files w/nonzero "next file addr" (break Citadel) */ +#define BUILD_TAPE_TAPECTRL +#define BUILD_TAPE_TAPECTRL_LAMP_OUTLINES +// #define BUILD_TAPE_TAPECTRL_ALLOW_RESIZE /* allow free resize. Broken on macOS, so disabled by default */ +// #define BUILD_TAPE_TAPECTRL_DOUBLE_SIZE /* double-size tape window by default */ +// #define BUILD_TAPE_TAPECTRL_PRINT_MUTEX_LOCKS /* for debugging */ +// #define BUILD_TAPE_TAPECTRL_LOG_MUTEX_LOCKS /* for debugging */ +#define BUILD_TAPE_TAPECTRL_WIDEN_THIN_STRIPES /* make thin stripes thicker (to the point where they may now become the New Thick Stripes) */ + /* Do not sleep in tape_ctrl_guithread_main() if no events. + * This increases responsiveness, but will triple the CPU usage of + * the tapectrl thread. */ +// #define BUILD_TAPE_TAPECTRL_DELUXE_RESPONSE + +/* 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_UNK_EXT 13 /* TOHv4.3: no loader exists for this file extension */ +/*#define TAPE_E_BLANK_FILENAME 14*/ +#define TAPE_E_BLANK_EXTENSION 15 + +#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 (wrongly, IMO) */ +#define TAPE_E_UEF_CHUNKDAT_0003 364 /* TOHv4.3 */ + +#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 */ + +/* tapectrl stuff (TOHv4.3) */ +#define TAPE_E_DEBUG_BAD_TAPESEEK 600 +#define TAPE_E_TAPECTRL_OUTSIDE_ZONE 601 /* not an error */ +#define TAPE_E_TAPECTRL_CREATE_THREAD 602 /* al_create_thread() failure */ +#define TAPE_E_TAPECTRL_THREAD_EXISTS 603 /* tape_ctrl_start_gui_thread() called but thread already running */ +#define TAPE_E_TAPECTRL_THREAD_SHUTTING_DOWN 604 /* al_create_thread() failure */ + +#define TAPE_E_ACIA_TX_BITCLK_NOT_READY 700 /* not an error */ +#define TAPE_E_DEBUG_BAD_TAPEREC 701 + +/* Unicode */ +#define TAPE_E_UNICODE_UNEXPECTED_NULL 800 +#define TAPE_E_UNICODE_MAX_LEN 801 +#define TAPE_E_ENC_UTF8 802 + +/* Allegro */ +#define TAPE_E_ALLEGRO_CREATE_BITMAP 900 /* TOHv4.3 */ +#define TAPE_E_ALLEGRO_LOCK_BITMAP 901 /* TOHv4.3 */ + +/* universal fwdrefs */ +typedef struct tape_vars_s tape_vars_t; + +#define TAPE_TONECODE_IS_LEGAL(c) (('0'==(c))||('1'==(c))||('S'==(c))||('L'==(c))) +#define TAPE_CRUDE_LEADER_DETECT_1200THS 170 + +/* +#define TAPE_EOF_TRISTATE_NO 2 +#define TAPE_EOF_TRISTATE_YES 3 +#define TAPE_EOF_TRISTATE_UNCHANGED 4 +*/ + +#endif /* __INC_TAPE2_H */ diff -uN b-em-0b6f1d2-vanilla/src/tape.c b-em-0b6f1d2-TOH/src/tape.c --- b-em-0b6f1d2-vanilla/src/tape.c 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/tape.c 2025-11-02 07:34:13.061400267 +0000 @@ -1,83 +1,1131 @@ +/* + * B-Em + * This file (C) 2025 'Diminished' + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + #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 "tapewrite.h" +#include "taperead.h" +#include + +/* VARIABLES */ -int tapelcount,tapellatch,tapeledcount; +/* saveable state lives here: */ +tape_state_t tape_state; +/* other tape variables go here (e.g. config settings): */ +tape_vars_t tape_vars; +/* legacy tape variables */ +int tapeledcount; /*,tapelcount;*/ +/* dummy variable, so a valid char * pointer may be passed to snprintf() on Unix + * when emulating windows's scprintf() */ +char tape_dummy_for_unix_scprintf; -bool tape_loaded = false; -bool fasttape = false; -ALLEGRO_PATH *tape_fn = NULL; +/* 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; + } -static struct -{ - char *ext; - void (*load)(const char *fn); - void (*close)(); +/*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->tallied_1200ths, + &(t->w_uef_serial_phase), + &(t->w_uef_serial_frame), + &(t->w_uef_tmpchunk)); + } + + return e; + } -loaders[]= -{ - {"UEF", uef_load, uef_close}, - {"CSW", csw_load, csw_close}, - {0,0,0} -}; -static int tape_loader; +/* 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_load(ALLEGRO_PATH *fn) -{ - int c = 0; - const char *p, *cpath; + 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 (!fn) return; - p = al_get_path_extension(fn); - if (!p || !*p) return; - if (*p == '.') - p++; - cpath = al_path_cstr(fn, ALLEGRO_NATIVE_PATH_SEP); - log_info("tape: Loading %s %s", cpath, p); - while (loaders[c].ext) - { - if (!strcasecmp(p, loaders[c].ext)) - { - tape_loader = c; - loaders[c].load(cpath); - return; + /* 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)); + } + } + 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; + +} + + + +int findfilenames_new (tape_state_t * const ts, + bool const show_ui_window, + bool const filter_phantoms) { + + 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; +#ifdef BUILD_TAPE_CAT_TIMESTAMPS + int32_t last_time_1200ths; + int32_t file_start_time_1200ths; +#endif + + load = 0; + exec = 0; + file_len = 0; +#ifdef BUILD_TAPE_CAT_TIMESTAMPS + last_time_1200ths = -1; + file_start_time_1200ths = -1; +#endif + + /* 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_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; + char cat_1200th; + uint8_t value; + int32_t time_1200ths_or_minus_one; /* TOHv4.3 */ +#ifdef BUILD_TAPE_CAT_TIMESTAMPS + int hrs, mins, secs; +#endif + + /* 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_1200th_from_back_end (false, /* don't strip silence and leader */ + &tclone, + acia_rx_awaiting_start(&acia_tmp), /* awaiting start bit? */ + filter_phantoms, /* enable */ + &cat_1200th, + &time_1200ths_or_minus_one); + if (TAPE_E_OK != e) { break; } + +#ifdef BUILD_TAPE_CAT_TIMESTAMPS + if (time_1200ths_or_minus_one > -1) { + last_time_1200ths = time_1200ths_or_minus_one; + } else { + last_time_1200ths++; /* no updated time from back-end, so just increment */ + } +#endif + + /* we don't need tclone.tallied_1200ths, so don't call update_playback_elapsed on it */ + + /* 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; /* filename complete */ + /* 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); +#ifdef BUILD_TAPE_CAT_TIMESTAMPS + if (0 == blknum) { + /* record file start time */ + file_start_time_1200ths = last_time_1200ths; } - c++; +#endif + } 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 */ +#ifdef BUILD_TAPE_CAT_NEED_NEXT_ADDR_ZERO + /* 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 */ + } +#endif + 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] = ' '; } + } +#ifdef BUILD_TAPE_CAT_TIMESTAMPS + to_hours_minutes_seconds (file_start_time_1200ths, &hrs, &mins, &secs); + sprintf(s, "%d:%02d:%02d %s Size %04X Load %08X Run %08X", hrs, mins, secs, fn, file_len, load, exec); +#else + sprintf(s, "%s Size %04X Load %08X Run %08X", fn, file_len, load, exec); +#endif + /* 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? */ +#ifdef BUILD_TAPE_CAT_TIMESTAMPS + fprintf(stdout, "tapetest:%d:%02d:%02d %s %04x %08x %08x\n", hrs, mins, secs, fn, file_len, load, exec); +#else + fprintf(stdout, "tapetest:%s %04x %08x %08x\n", fn, file_len, load, exec); +#endif + } + file_len = 0; + memset(fn, 0, 11); + } + /* skip remainder of block: H.CRC2 + data + D.CRC2 (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, + false, + false); /* don't alter menus */ + /* free clone */ + tape_state_finish(&tclone, 0); /* 0 = DO NOT alter menus since this is a clone */ + + return e; + +} + +void to_hours_minutes_seconds (int32_t const elapsed_1200ths, + int32_t * const h_out, + int32_t * const m_out, + int32_t * const s_out) { + int32_t secs; + secs = (int32_t) (elapsed_1200ths * TAPE_1200TH_IN_S_FLT); + *s_out = secs % 60; + *m_out = secs / 60; + *h_out = *m_out / 60; + *m_out %= 60; +} + + +void tape_handle_exception (tape_state_t * const ts, + tape_vars_t * const tv, /* TOHv3.2: may be NULL */ + int const error_code, + bool const eof_fatal, + bool const err_fatal, + bool const not_tapecat_mode) { /* false if called from findfilenames_new */ + + int sx; + bool tape_broken; + bool tcw_opened; + + if (TAPE_E_OK == error_code) { return; } + + /* if (ts->disabled_due_to_error && not_tapecat_mode) { */ + if ((TAPE_E_OK != ts->prior_exception) && not_tapecat_mode) { + log_warn("tape: BUG: handling exception when tape is already disabled!"); + return; + } + + sx = SHUTDOWN_OK; + tcw_opened = false; + + 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; + } + + /* TOHv4.1: rework */ + tape_broken = (SHUTDOWN_OK != sx) && (SHUTDOWN_TAPE_EOF != sx); + +#ifdef BUILD_TAPE_TAPECTRL + if (NULL != tv) { /* tv will be null if we're doing tape cat */ + tcw_opened = tv->tapectrl_opened; + } +#endif + + if ( tape_broken ) { + /* errors which are fatal to the tape system */ + tape_state_finish(ts, not_tapecat_mode); /* 1 = alter menus */ + if (tv != NULL) { + tape_set_record_activated(ts, tv, NULL, false, tcw_opened); /* ignore errors */ + gui_set_record_mode(false); +#ifdef BUILD_TAPE_TAPECTRL + if (tcw_opened) { + tapectrl_set_record(&(tv->tapectrl), false, 0); + } +#endif + } + ts->prior_exception = error_code; +#ifdef BUILD_TAPE_TAPECTRL + if (tcw_opened) { + tapectrl_to_gui_msg_error (&(tv->tapectrl), true, true, error_code); } - tape_loaded = 0; +#endif + } + + if ( err_fatal + && ( (SHUTDOWN_TAPE_ERROR == sx) || (SHUTDOWN_FOPEN == sx) ) ) { + quitting = true; /* (global) */ + set_shutdown_exit_code(sx); + } else if (eof_fatal && (SHUTDOWN_TAPE_EOF == sx)) { + quitting = true; /* (global) */ + set_shutdown_exit_code(sx); + } else if (SHUTDOWN_EXPIRED == sx) { /* ALWAYS fatal */ + quitting = true; /* (global) */ + set_shutdown_exit_code(sx); + } + + if (error_code != TAPE_E_EOF) { + ts->prior_exception = error_code; /* TOHv4.3: keep this so it can be sent to tapectrl */ + } + } -void tape_close() -{ - if (tape_loaded && tape_loader < 2) - loaders[tape_loader].close(); - tape_loaded = 0; + + +int tape_state_init_alter_menus (tape_state_t * const ts, /* not const, because of tibet priv horribleness */ + tape_vars_t * const tv) { + + int32_t pos_1200ths; + bool rec; + int e; + + e = TAPE_E_OK; + pos_1200ths = 0; + + rec = tape_is_record_activated(tv); + +#ifdef BUILD_TAPE_TAPECTRL + if (rec) { + tape_get_duration_1200ths(ts, &pos_1200ths); + } + if (tv->tapectrl_opened) { + /* tapectrl_set_record() will eventually send a TIME message + * to the tapectrl. Before this occurs, we need to make sure + * that the inlay scans are nullified, so that the duration + * isn't suddenly zero but with an active stripe set. */ + e = tapectrl_to_gui_msg_stripes(&(tv->tapectrl), true, true, NULL); + tapectrl_set_record(&(tv->tapectrl), rec, pos_1200ths); + } +#endif + gui_alter_tape_menus(ts->filetype_bits); + gui_set_record_mode(rec); + return e; +} + + +/* tv may be NULL: */ +void tape_state_init (tape_state_t *t, + tape_vars_t *tv, + uint8_t filetype_bits, + bool 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) { + tape_state_init_alter_menus(t, 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 (tv->overclock, + 0x64, //tape_state.ula_ctrl_reg, + &(t->ula_rx_thresh_ns), + &(t->ula_tx_thresh_ns), + &(t->ula_rx_divider), + &(t->ula_tx_divider)); + +} + + +void tape_ejected_by_user (tape_state_t * const ts, + tape_vars_t * const tv, + ACIA * const acia) { + + bool tcw_opened; + + tcw_opened = false; +#ifdef BUILD_TAPE_TAPECTRL + tcw_opened = tv->tapectrl_opened; +#endif + + /* cancel record mode */ + tape_set_record_activated(ts, tv, acia, 0, tcw_opened); + gui_set_record_mode(false); + +#ifdef BUILD_TAPE_TAPECTRL + if (tcw_opened) { + /* FIXME: there is some needless locking and unlocking in this sequence */ + tapectrl_set_record(&(tv->tapectrl), false, false); + tapectrl_eject(&(tv->tapectrl)); /* TOHv4.3 */ + tape_interval_list_finish(&(tv->interval_list)); /* TOHv4.3 */ + } +#endif + + tape_state_finish(ts, true); /* true = update menus */ + +} + + +void tape_state_finish (tape_state_t *t, bool alter_menus) { + + /* 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); + /* simultaneously show all file types for saving since all + * are available on a blank tape: */ + gui_alter_tape_menus(TAPE_FILETYPES_ALL); + } + 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->tones300_fill = 0; + t->tallied_1200ths = 0; /* TOHv4.3 */ + t->ula_prevailing_rx_bit_value = 'S'; + t->prior_exception = TAPE_E_OK; /* TOHv4.3-a3 */ + +} + +bool tape_is_record_activated (const tape_vars_t * const tv) { + return tv->record_activated; +} + +int tape_set_record_activated (tape_state_t * const t, + tape_vars_t * const tv, + ACIA * const acia_or_null, + bool const value, + bool const tapectrl_opened) { + + int32_t baud; + int e; + /* TOHv4.3 */ + tape_ctrl_window_t *tcw; + bool tcw_opened; + int32_t dur; + + e = TAPE_E_OK; + +/*printf("\n\ntape_set_record_activated: %x\n", value);*/ + + 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; + } + + tcw = NULL; + tcw_opened = false; +#ifdef BUILD_TAPE_TAPECTRL + tcw = &(tv->tapectrl); + tcw_opened = tv->tapectrl_opened; +#endif + + if ( (false == value) && (false != tv->record_activated) ) { + /* recording finished; flush any pending data */ + e = tape_flush_pending_piece (t, + acia_or_null, + tv->save_prefer_112, + true); /* populate timestamp information */ + if (TAPE_E_OK != e) { return e; } + e = tape_rewind_2(t, tcw, false, tcw_opened); + } 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 = 1200; + if ( (t->filetype_bits & TAPE_FILETYPE_BITS_UEF) && (t->uef.num_chunks>0) ) { + uef_scan_backwards_for_chunk_117 (&(t->uef), t->uef.num_chunks-1, &baud); + if (0 == baud) { baud = 1200; } + } + t->w_uef_prevailing_baud = baud; + } + + tv->record_activated = value; + + /* TOHv4.3-a2 */ + dur = 0; + + // if ( ! t->disabled_due_to_error ) { + if ( TAPE_E_OK == t->prior_exception ) { + e = tape_get_duration_1200ths(t, &dur); + if (TAPE_E_OK != e) { return e; } + } + +#ifdef BUILD_TAPE_TAPECTRL + /* TOHv4.3-a2: bugfix + * TOHv4.3-a3: getting a lot of test crashes here, had to add the mutex check too */ + if (tapectrl_opened && (tcw->mutex != NULL)) { + tapectrl_set_gui_rapid_value_time (tcw, false, true, t->tallied_1200ths); //, dur); + } +#endif + + /* TOHv4.3: set up initial value of tallied_1200ths from prior duration */ + /* this is needed for (rec enabled -> *tape -> *cat) correct functioning */ + if (value) { + t->tallied_1200ths = dur; + } else { + t->tallied_1200ths = 0; + /* Also, this is the FINALO PIECE. */ +#ifdef BUILD_TAPE_TAPECTRL +/* printf("FINALO PIECE: tv->interval_list.decstate.wip.type = %u\n", tv->interval_list.decstate.wip.type); */ + if (tv->interval_list.decstate.wip.type != TAPE_INTERVAL_TYPE_PENDING) { /* make sure the WIP is real */ + +/*printf("tape_set_record_activated(): appending interval wip (%d + %d), interval type \"%s\"\n", + tv->interval_list.decstate.wip.start_1200ths, + tv->interval_list.decstate.wip.pos_1200ths, + tape_interval_name_from_type(tv->interval_list.decstate.wip.type));*/ + + e = tape_interval_list_append (&(tv->interval_list), + &(tv->interval_list.decstate.wip), + true); /* derive wip start time from previous list entry */ + if (TAPE_E_OK != e) { return e; } + } + memset(&(tv->interval_list.decstate.wip), 0, sizeof(tape_interval_t)); + /* bugfix: forgot to send the updated intervals list over to the tapectrl window. */ + if (tcw_opened) { + e = tapectrl_to_gui_msg_stripes (tcw, true, true, &(tv->interval_list)); + } + if (TAPE_E_OK != e) { return e; } + +#ifdef BUILD_TAPE_SANITY + /* various timestamp integrity checks ... */ + e = tape_interval_list_integrity_check(&tv->interval_list); + if (TAPE_E_OK != e) { return e; } + if (t->filetype_bits & TAPE_FILETYPE_BITS_UEF) { + e = uef_verify_timestamps(&(t->uef)); + } + if (TAPE_E_OK != e) { return e; } +#endif + + +#endif + } + +// tape_interval_list_t *ivl; +// tibet_priv_t *priv; +// tibet_span_t *span; +// tape_interval_t *iv; + + +// ivl = &(tv->interval_list); +// if (ivl->fill>0) { +// iv = ivl->list + ivl->fill - 1; +// priv = (tibet_priv_t *) t->tibet.priv; +// span = priv->dd_new_spans + priv->dd_new_num_spans - 1; +// printf("durations: per ivl (%d + %d = %d), per timestamp (%d + %d = %d)\n", +// iv->start_1200ths, +// iv->pos_1200ths, +// iv->start_1200ths + iv->pos_1200ths, +// span->timespan.start_1200ths, +// span->timespan.pos_1200ths, +// span->timespan.start_1200ths + span->timespan.pos_1200ths); +// } + + return e; + } -/*Every 128 clocks, ie 15.625khz*/ -/*Div by 13 gives roughly 1200hz*/ -static uint16_t newdat; -void tape_poll(void) { - if (motor) { - if (csw_ena) csw_poll(); - else uef_poll(); +int tape_load_successful (tape_state_t * const ts, + tape_vars_t * const tv, + const char * const path) { + + int32_t total_1200ths; /* TOHv4.3 */ + int e; + tape_ctrl_window_t *tcw; + bool tcw_opened; + tape_interval_list_t *iv_list; +#ifdef BUILD_TAPE_TAPECTRL + tape_ctrl_msg_from_gui_t from_gui; +#endif + + /* TOHv4.3: check removed: record mode on load is a valid configuration if -tape and -record provided on CL */ + /* + if (tv->record_activated) { + log_warn("tape: load_successful: BUG: record mode is activated"); + return TAPE_E_BUG; + }*/ - if (newdat & 0x100) { - newdat&=0xFF; - tapenoise_adddat(newdat); + /* tape_load_ui() in gui-allegro.c should already have called + * tape_state_finish() which will reset this itself, but it is + * done again here anyway */ + ts->prior_exception = TAPE_E_OK; + + gui_alter_tape_eject (path); + gui_alter_tape_menus(ts->filetype_bits); + + e = TAPE_E_OK; + +#ifdef BUILD_TAPE_TAPECTRL + tcw = &(tv->tapectrl); + tcw_opened = tv->tapectrl_opened; + iv_list = &(tv->interval_list); +#else + tcw = NULL; + tcw_opened = false; + iv_list = NULL; +#endif + + /* TOHv4.3: now have to build the time map for the back-ends, + which entails reading the entire tape on load :( */ + + /* scan the tape */ + e = tapeseek_run_initial_scan (ts, iv_list); + + if (TAPE_E_OK != e) { + log_warn("tape: ERROR: initial scan failed\n"); + tape_handle_exception (ts, + &tape_vars, + e, + tape_vars.testing_mode & TAPE_TEST_QUIT_ON_EOF, + tape_vars.testing_mode & TAPE_TEST_QUIT_ON_ERR, + true); /* alter menus */ + return TAPE_E_OK; /* exception handled */ + } + if (TAPE_E_OK != e) { return e; } + total_1200ths = 0; + e = tape_get_duration_1200ths (ts, &total_1200ths); + if (total_1200ths > 0) { + log_info("Tape scan complete (%.1lf minutes, %d intervals); rewinding ...", + total_1200ths / (60.0 * TAPE_1200_HZ), +#ifdef BUILD_TAPE_TAPECTRL + tv->interval_list.fill); +#else + 0); +#endif + } else { + log_warn("We ain't brought no hose."); + } + + tape_rewind_2(ts, tcw, tv->record_activated, tcw_opened); + +#ifdef BUILD_TAPE_TAPECTRL + /* hack: self-insert a THREAD_STARTED message into the from_gui queue. + * This will send a full update sequence to the tapectrl. */ + if (tcw_opened) { + from_gui.type = TAPECTRL_FROM_GUI_THREAD_STARTED; + from_gui.ready = true; + TAPECTRL_LOCK_MUTEX(tcw->mutex); + if (tcw->display != NULL) { + tapectrl_queue_from_gui_msg (tcw, &from_gui); } - else if (csw_toneon || uef_toneon) - tapenoise_addhigh(); + TAPECTRL_UNLOCK_MUTEX(tcw->mutex); } +#endif + + /* trap error or EOF */ + + return e; + } -void tape_receive(ACIA *acia, uint8_t data) { - newdat = data | 0x100; + + + +/* 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 (const uint8_t * const in) { + uint16_t u; + u = ( (uint16_t)in[0]) + | ((((uint16_t)in[1]) << 8) & 0xff00); + return u; +} + + +/* TOHv3 */ +void tape_init (tape_state_t *ts, tape_vars_t *tv) { + 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; +#ifdef BUILD_TAPE_TAPECTRL +#ifdef BUILD_TAPE_TAPECTRL_DOUBLE_SIZE + tv->tapectrl_ui_scale = 2.0f; +#else + tv->tapectrl_ui_scale = 1.0f; +#endif +#endif +} + + + +/* TOHv3.2: added explicit tape_start_motor(), tape_stop_motor(), tape_is_motor_running() + * TOHv3.3: moved some stuff here from serial.c */ +int tape_start_motor (tape_state_t * const ts, + tape_ctrl_window_t * const tcw, + bool const also_force_dcd_high, + bool const tapectrl_opened) { + + if (ts->ula_motor) { return TAPE_E_OK; } + + 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 ~0.6s + of unskipped silence/leader plays out when the motor + starts up. */ + ts->strip_silence_and_leader_holdoff_1200ths=0; + + /* TOHv4.3 */ +#ifdef BUILD_TAPE_TAPECTRL + if ( tapectrl_opened ) { + tapectrl_to_gui_msg_motor(tcw, true, true, true); + } +#endif + + return TAPE_E_OK; + +} + + + +int tape_stop_motor (tape_state_t * const ts, + tape_ctrl_window_t * const tcw, + /*bool const silence112,*/ + bool const record_activated, + bool const tapectrl_opened) { + + int e; + + e = TAPE_E_OK; + + if ( ! ts->ula_motor ) { return TAPE_E_OK; } /* not running */ + ts->ula_motor = false; + tapeledcount = 2; /* (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; + + /* TOHv4.3-a3: There used to be a call to tape_flush_pending_piece() here, + * so that MOTOR OFF would flush data to the TX back-end (UEF, TIBET, CSW). + * This is no longer done, meaning you can now accumulate several sessions + * of e.g. silence into one single UEF chunk or TIBET span. Flushing only + * occurs when REC mode is deactivated by the user. */ + +#ifdef BUILD_TAPE_TAPECTRL + if (tapectrl_opened) { + if (TAPE_E_OK != e) { return e; } + e = tapectrl_set_gui_rapid_value_signal(tcw, true, false, 'S'); + if (TAPE_E_OK != e) { return e; } + /* TOHv4.3: was having problems sometimes with motor off + * from MOS extinguishing the "record mode" lamp. Don't + * know why but maybe this will fix it. */ + tapectrl_to_gui_msg_record(tcw, false, false, record_activated); + /* TOHv4.3 */ + tapectrl_to_gui_msg_motor(tcw, false, true, false); + } +#endif + return e; +} + +/* This stays on tape.c rather than taperead or tapewrite, + * because it deals with both record and playback situations. */ + +/* NOTE that this is called if record mode is enabled, as well as on playback! */ +/* (tape is guaranteed to be rolling) */ + +/* FIXME: this sends about three different msg types to the GUI + * and locks the mutex independently each time. See if it's possible to amalgamate + * these into a single mutex session. */ +int tape_rs423_eat_1200th (tape_state_t * const ts, + tape_vars_t * const tv, + ACIA * const acia, + bool const record_activated, /* TOHv4.3 */ + bool const emit_tapenoise, + bool * const throw_eof_out) { + + int e; + char tone; + int32_t total_1200ths; /* TOHv4.3 */ + int32_t elapsed_or_minus_one; + tape_ctrl_window_t *tcw; + bool tcw_opened; + tape_interval_list_t *iv_list; + uint32_t *since_last_tone_p; + + /* FIXME: should this not be a bug? */ + /* if ( ts->disabled_due_to_error ) { return TAPE_E_OK; } */ + if ( TAPE_E_OK != ts->prior_exception ) { return TAPE_E_OK; } + + /* consume tape 1200th */ + tone = '\0'; + + /* TOHv4.3 */ + e = tape_get_duration_1200ths(ts, &total_1200ths); + if (TAPE_E_OK != e) { return e; } + + elapsed_or_minus_one = -1; + + /* continue to advance through the tape even if RS423 selected */ + if ( ! record_activated ) { + e = tape_1200th_from_back_end (false, ts, false, false, &tone, &elapsed_or_minus_one); + if (TAPE_E_EOF == e) { + *throw_eof_out = true; + e = TAPE_E_OK; /* trap EOF */ + } + if (TAPE_E_OK != e) { return e; } + } + +#ifdef BUILD_TAPE_TAPECTRL + if ( record_activated ) { + /* record mode; show tallied time */ + total_1200ths = ts->tallied_1200ths; + } else if ( ! *throw_eof_out ) { /* don't update elapsed time if tape has ended */ + /* playback mode */ + /* increments ts->tallied_1200ths, if passed -1 */ + e = tape_update_playback_elapsed(ts, elapsed_or_minus_one); + } + if (TAPE_E_OK != e) { return e; } +#endif + + if ('\0' != tone) { ts->ula_prevailing_rx_bit_value = tone; } /* for DCD */ + + /* TOHv4.3: add crude leader detection for RS423 side; + * needed for tapectrl lamps, and also to fix tapenoise warble */ + if ('1' == tone) { + if (ts->rs423_leader_detect > TAPE_CRUDE_LEADER_DETECT_1200THS /* =100 */) { + tone = 'L'; + } else { + (ts->rs423_leader_detect)++; + } + } else { + ts->rs423_leader_detect = 0; + } + + /* TOHv4.3: don't play RS423's RX audio when record mode is activated */ + if ( ! record_activated ) { +#ifdef BUILD_TAPE_TAPECTRL + e = send_tone_to_tapectrl_maybe (&(tv->tapectrl), + ts->tallied_1200ths, + total_1200ths, + tone, + &(tv->since_last_tone_sent_to_gui)); + if (TAPE_E_OK != e) { return e; } +#endif + /* 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 drop-outs. */ + tapenoise_send_1200 (tone, &(ts->tapenoise_no_emsgs)); + } + + tcw = NULL; + iv_list = NULL; + tcw_opened = false; + since_last_tone_p = NULL; +#ifdef BUILD_TAPE_TAPECTRL + tcw = &(tv->tapectrl); + iv_list = &(tv->interval_list); + tcw_opened = tv->tapectrl_opened; + since_last_tone_p = &(tv->since_last_tone_sent_to_gui); +#endif + + /* we also need to send s/1200 of silence to the TX back end */ + e = tape_write_bitclk (ts, + tcw, + iv_list, + acia, + 'S', + TAPE_1200TH_IN_NS_INT, + emit_tapenoise && record_activated, + record_activated, + tv->save_always_117, + tv->save_prefer_112, + tv->save_do_not_generate_origin_on_append, + tcw_opened, + since_last_tone_p); + if (TAPE_E_OK != e) { return e; } + + if ( (total_1200ths>=0) + && (ts->tallied_1200ths > total_1200ths) + && ! record_activated) { + log_warn("tape: rs423_eat_1200th: BUG: tallied_1200ths (%d) > total_1200ths (%d)", + ts->tallied_1200ths, total_1200ths); + return TAPE_E_BUG; + } + +#ifdef BUILD_TAPE_TAPECTRL + if (tcw_opened) { + /* this might lock and unlock the mutex */ + if ((TAPE_E_OK == e) && (*throw_eof_out != tv->previous_eof_value)) { +/*printf("tapectrl_to_gui_msg_eof(%u): %s:%s(%d)\n", true, __FILE__, __func__, __LINE__);*/ + tapectrl_to_gui_msg_eof(&(tv->tapectrl), true, true, true); + } + } + tv->previous_eof_value = *throw_eof_out; +#endif + + return e; + } diff -uN b-em-0b6f1d2-vanilla/src/tapecat-allegro.c b-em-0b6f1d2-TOH/src/tapecat-allegro.c --- b-em-0b6f1d2-vanilla/src/tapecat-allegro.c 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/tapecat-allegro.c 2025-10-27 07:25:03.372389704 +0000 @@ -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 (bool const enable_phantom_block_protection) /* TOHv3.3: parameter */ { - if (csw_ena) - csw_findfilenames(); - else - uef_findfilenames(); + findfilenames_new(&tape_state, + true, /* TOHv3: show UI */ + enable_phantom_block_protection); } void gui_tapecat_start(void) { - if (textlog) - start_cat(); - else { + if (textlog) { + start_cat( ! tape_vars.permit_phantoms ); + } 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.permit_phantoms ); return; } al_close_native_text_log(ltxtlog); diff -uN b-em-0b6f1d2-vanilla/src/tapectrl.c b-em-0b6f1d2-TOH/src/tapectrl.c --- b-em-0b6f1d2-vanilla/src/tapectrl.c 1970-01-01 01:00:00.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/tapectrl.c 2025-11-01 20:01:18.581216640 +0000 @@ -0,0 +1,2311 @@ +/* + * B-Em + * This file (C) 2025 'Diminished' + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +/* this file new in TOHv4.3 */ + +#include "tape2.h" + +#ifdef BUILD_TAPE_TAPECTRL + +#include "tapectrl.h" + +/* reduce this value to increase responsiveness, at cost of CPU */ +#define TAPECTRL_PAINT_SLEEP_MS 10 + +#define TAPECTRL_W 720 +#define TAPECTRL_H 286 + +#define RECT_OUTLINE_W_PX_FLT (1.0f) + +#define SEEKER_MARGINS_PX 45 +#define SEEKER_H 14 + +#define SEEKER_Y (236) +#define SEEKER_TRACK_WIDTH 9 +#define SEEKER_CLICKZONE_Y 45 +#define KNOB_H (18.0f) +#define KNOB_W (16.0f) + +#define IN_SCRUBZONE_Y(my,margin_y,scale) ( (((float)(my)) >= (((float)margin_y) + (scale*(SEEKER_Y - (SEEKER_CLICKZONE_Y / 2))))) \ + && (((float)(my)) <= (((float)margin_y) + (scale*(SEEKER_Y + (SEEKER_CLICKZONE_Y / 2)))))) + +#define SEVENSEG_X 350 +#define SEVENSEG_Y 30 + + /* +RECT_OUTLINE_W_PX_FLT to line up outside edge with seeker: */ +#define INLAY_X (SEEKER_MARGINS_PX + ((int)RECT_OUTLINE_W_PX_FLT) - (KNOB_W / 2)) +#define INLAY_Y SEVENSEG_Y +#define INLAY_SQUARE_SIZE 160 + +#define LAMP_PLAY_X (SEVENSEG_X - 60) +#define LAMP_PLAY_Y (SEVENSEG_Y + (SEVENSEG_BAR_LEN/2)) +#define LAMP_RECORD_X (LAMP_PLAY_X - 35) +#define LAMP_RECORD_Y (SEVENSEG_Y + SEVENSEG_BAR_LEN) + +#define SEVENSEG_BAR_TIP_GAP 2.0 +#define SEVENSEG_BAR_WIDTH 3.0 +#define SEVENSEG_BAR_LEN 25 +#define SEVENSEG_BAR_TILT_X 7 +#define SEVENSEG_BAR_TOP_ROWS_TILT_FUDGE_X 1 /* x-offset for segs 1, 2, 4 */ +#define SEVENSEG_SMALL_GAP 12 +#define SEVENSEG_SEPARATOR_WIDTH (SEVENSEG_BAR_WIDTH + SEVENSEG_SMALL_GAP) +#define SEVENSEG_DIGIT_WIDTH (SEVENSEG_BAR_LEN + SEVENSEG_SMALL_GAP) + +#define TAPECTRL_KEY_REPEAT_ONSET_S 0.5 +#define TAPECTRL_KEY_REPEAT_PERIOD_S 0.05 + +#define TAPECTRL_KEY_PRESS_SEEK_1200THS 6010 /* 5s */ +#define TAPECTRL_KEY_HOLD_TICK_SEEK_1200THS 20000 + +#define TAPECTRL_LAMP_TOP_ROW_Y (SEVENSEG_Y + 4) +#define TAPECTRL_LAMP_ROW_H 36 +#define TAPECTRL_LAMP_BULB_RADIUS 6 +#define TAPECTRL_LAMPS_X (TAPECTRL_W - 90) /*(SEVENSEG_X + 240)*/ +#define TAPECTRL_LAMP_LABELS_X (TAPECTRL_LAMPS_X + TAPECTRL_LAMP_BULB_RADIUS + 10) + +//#define TAPECTRL_REC_IS_UP +#define TAPECTRL_REC_X 222 +#ifdef TAPECTRL_REC_IS_UP +#define TAPECTRL_REC_Y 90 +#else +#define TAPECTRL_REC_Y 150 +#endif + +#define TAPECTRL_REC_W 60 +#define TAPECTRL_REC_H 40 +#define TAPECTRL_REC_LBL_OFF_X 15 +#define TAPECTRL_REC_LBL_OFF_Y 12 + +#include "tapectrl-labels.h" + +#define COLOUR_BG 24,24,24 +#define COLOUR_LAMP_A 255,255,0 +#define COLOUR_LAMP_OUTLINE 96,96,0 +#define COLOUR_LAMP_PLAY 0,210,0 +#define COLOUR_LAMP_PLAY_OUTLINE 0,96,0 +#define COLOUR_LAMP_REC 210,0,0 +#define COLOUR_LAMP_REC_OUTLINE 96,0,0 +#define COLOUR_7SEG COLOUR_LAMP_A +#define COLOUR_SEEKER_TRACK 25,0,120 +#define COLOUR_SEEKER_KNOB 0xff,0xef,0x90 +#define COLOUR_ERROR 255,0,0 + +#define COLOUR_RECT_OUTLINE 200,200,200 + +/* pink skin */ +/*#define COLOUR_INTERVAL_SILENCE 0x1a,0x23,0x60*/ /* navy */ +/*#define COLOUR_INTERVAL_LEADER 0xc0,0x50,0xc0*/ /* purple */ +/*#define COLOUR_INTERVAL_DATA 0xff,0x90,0xff*/ /* pink */ +/* LED skin */ +/*#define COLOUR_INTERVAL_SILENCE 0,255,0*/ +/*#define COLOUR_INTERVAL_LEADER 255,255,0*/ +/*#define COLOUR_INTERVAL_DATA 255,0,0*/ +/* fiery skin */ +#define COLOUR_INTERVAL_SILENCE 64,0,0 /* scarlet */ +#define COLOUR_INTERVAL_LEADER 0xb0,0x78,0x0 /* orange */ +#define COLOUR_INTERVAL_DATA 0xff,0xef,0 /* yellow */ + +/* these must match WIDTH and HEIGHT in the mklabels.sh script and the value in mksource.c */ +#define TAPECTRL_LABEL_WIDTH 40 /* was the -size parameter to imagemagick */ +#define TAPECTRL_LABEL_HEIGHT 14 + +/* +#define TAPECTRL_IX_TONE 0 +#define TAPECTRL_IX_DATA 1 +#define TAPECTRL_IX_EOF 2 +#define TAPECTRL_IX_300 3 +#define TAPECTRL_IX_DCD 4 +*/ +#define TAPECTRL_IX_REC 5 +static const char *tapectrl_labels[TAPECTRL_NUM_LABELS] = { + TAPECTRL_LABEL_TONE, + TAPECTRL_LABEL_DATA, + TAPECTRL_LABEL_DCD, + TAPECTRL_LABEL_300, + TAPECTRL_LABEL_EOF, + TAPECTRL_LABEL_REC +}; + +#include "tape.h" + +static void finish_labels (tape_ctrl_window_t * const tcw); + +static void queue_to_gui_msg (tape_ctrl_window_t * const tcw, + const tape_ctrl_msg_to_gui_t * const msg) ; + +static int guithread_rec_button_pressed (tape_ctrl_window_t * const tcw, + tape_ctrl_msg_from_gui_t * const from_gui_out); + +static void *tape_ctrl_guithread_main (ALLEGRO_THREAD * const thread, void *arg); + +static int guithread_update_time (int32_t const elapsed_1200ths, + int32_t const duration_1200ths, /* TODO: not currently used */ + float const scale, + float const margin_xin, + float const margin_yin, + int const reported_error); + +static int draw_7seg (ALLEGRO_COLOR const c, + int const hrs, + int const mins, + int const secs, + float const scale, + float const margin_xin, + float const margin_yin, + bool const hours_and_separators); + +static int tape_ctrl_mainthread_handle_messages_2 (tape_vars_t * const tv, + tape_state_t * const ts, + ACIA * const acia); + +static int guithread_main_paint (tape_ctrl_window_t * const tcw, + int const black_bar_w, + bool const letterbox, + float const scale); + +static int draw_digit (uint8_t const d, + float const x, /* x, y already include margin_x or margin_y */ + float const y, + float scale, + ALLEGRO_COLOR const c, + bool const colon); + +static int seek_frac_from_mouse_xy (int const x, + int const y, + float const margin_x, + float const margin_y, + float const scale, + float * const f_out); + +static int plot_label_to_bitmap (ALLEGRO_BITMAP * const ab, const char * const label); + +static void finish_inlays (tape_ctrl_window_t * const tcw); + +static int handle_mouse_button_down (int const mx, + int const my, + float const scale, + float const margin_x, + float const margin_y, + bool const record_activated, + /* outputs: */ + uint32_t * const current_inlay_inout, + bool * const held_out, + bool * const rec_button_pressed_out); + +static int +guithread_init_intervals (tape_ctrl_window_t * const tcw, + /* This is the copy on the to_gui message. + * We will take it. */ + tape_interval_list_t * const interval_list); + +static int guithread_init_inlays (tape_ctrl_window_t * const tcw, + uint32_t const num_scans, + uef_inlay_scan_t * const scans); + +static int guithread_paint_seeker_stripes (tape_ctrl_window_t * const tcw, + double margin_x, + double margin_y, + float scale); + +/* TOHv4.3-a3 */ +static int32_t guithread_duration_from_intervals (const tape_interval_list_t * const iv_list); + + +#define LAMP_PLAY_TRIANGLE_NUM_VERTICES 3 +static const float lamp_play_triangle_vertices[2 * LAMP_PLAY_TRIANGLE_NUM_VERTICES] = { + LAMP_PLAY_X, LAMP_PLAY_Y, + LAMP_PLAY_X, LAMP_PLAY_Y + SEVENSEG_BAR_LEN, + LAMP_PLAY_X + SEVENSEG_BAR_LEN, LAMP_PLAY_Y + (SEVENSEG_BAR_LEN / 2) +}; + +#define SEEKER_KNOB_NUM_VERTICES 5 +/* + 1 5 + 2 4 + 3 +*/ +static const float seeker_knob_vertices[2 * SEEKER_KNOB_NUM_VERTICES] = { + -(KNOB_W/2.0f), -KNOB_H, + -(KNOB_W/2.0f), -(KNOB_H*0.3f), + 0.0f, 0.0f, + (KNOB_W/2.0f), -(KNOB_H*0.3f), + (KNOB_W/2.0f), -KNOB_H +}; + +extern int shutdown_exit_code; +/* this is the top-level mainthread-polled function; it is responsible + for executing the outcomes of events arriving from the GUI */ +int tapectrl_handle_messages (tape_vars_t * const tv, tape_state_t * const ts, ACIA * const acia) { /* not const */ + + int e; + e = TAPE_E_OK; + + /* if the window has already gone away, we lock the + * mutex and recover the final error code from tapectrl. */ + + /* remember, tapectrl_opened is only changed by the main thread, so + * there isn't a race here. If it's false then that means the main thread + * hasn't tried to start the gui thread (or the window has closed again). */ + if (/*ts->disabled_due_to_error ||*/ ! tv->tapectrl_opened ) { + e = tv->tapectrl.tapectrl_error; + } else { + /* otherwise call this like normal (locks mutex, checks display != NULL) */ + e = tape_ctrl_mainthread_handle_messages_2(tv, ts, acia); + } + if (TAPE_E_OK != ts->prior_exception) { + return TAPE_E_OK; + } /* TOHv4.3-a3 */ + /* exceptions for the tapectrl window are processed here */ + /* printf("tapectrl main thread: expect error from tcw to arrive here: e = %d\n", e); */ + tape_handle_exception (ts, + tv, + e, + tv->testing_mode & TAPE_TEST_QUIT_ON_EOF, + tv->testing_mode & TAPE_TEST_QUIT_ON_ERR, + true); /* alter menus */ + + return TAPE_E_OK; + +} + + +static int seek_frac_from_mouse_xy (int const x, + int const y, + float const margin_x, + float const margin_y, + float const scale, + float * const f_out) { + float seeker_w; + seeker_w = scale * (float) (TAPECTRL_W - (2*SEEKER_MARGINS_PX)); + if ((y<0) || IN_SCRUBZONE_Y(y,margin_y,scale)) { + *f_out = (x - (margin_x + (scale * SEEKER_MARGINS_PX))) / seeker_w; + if (*f_out > 1.0f) { *f_out = 1.0f; } + if (*f_out < 0.0f) { *f_out = 0.0f; } + return TAPE_E_OK; + } + return TAPE_E_TAPECTRL_OUTSIDE_ZONE; +} + +#include "gui-allegro.h" +#include "tapeseek.h" +#include "taperead.h" + +/* this is called by the MAIN THREAD to process messages from the tape control window + * called from main_timer() in main.c + * This function is NOT called at all while tape_vars.tapectrl_opened is false. + * This is a main thread-only variable. */ +static int tape_ctrl_mainthread_handle_messages_2 (tape_vars_t * const tv, + tape_state_t * const ts, + ACIA * const acia) { /* modified, not const */ + + int32_t elapsed_1200ths_actual; + int32_t duration_1200ths; + int e; + tape_ctrl_window_t *tcw; + double t; + bool key_held_seek; + bool my_eof; + int32_t div; + int msg_i, msg_n; + tape_ctrl_msg_from_gui_t from_gui_copy[TAPECTRL_MSG_QUEUE_SIZE]; + + tcw = &(tv->tapectrl); + + e = TAPE_E_OK; + key_held_seek = false; + my_eof = false; + + TAPECTRL_LOCK_MUTEX(tcw->mutex); + + /* confirm this */ + if (NULL == tv->tapectrl.display) { + TAPECTRL_UNLOCK_MUTEX(tcw->mutex); + return TAPE_E_OK; + } + + /* handle error from the tapectrl thread */ + if (tcw->tapectrl_error != TAPE_E_OK) { + e = tcw->tapectrl_error; + tcw->tapectrl_error = TAPE_E_OK; + /* echo it back to the tapectrl thread */ + tapectrl_to_gui_msg_error (tcw, false, false, e); /* TOHv4.3-a3 */ + TAPECTRL_UNLOCK_MUTEX(tcw->mutex); + return e; + } + + /* decide whether we are interested in the RX bits or the TX bits */ + div = acia_get_divider_from_ctrl_reg_bits(3 & acia->control_reg); + + tapectrl_to_gui_msg_baud (tcw, false, false, (64==div) && ts->ula_motor); /* send msg to GUI */ + + /* with the mutex locked, copy the messages */ + memcpy(from_gui_copy, + (const void *) tcw->from_gui, + sizeof(tape_ctrl_msg_from_gui_t) * tcw->from_gui_fill); + msg_n = tcw->from_gui_fill; + tcw->from_gui_fill = 0; /* empty the queue */ + + TAPECTRL_UNLOCK_MUTEX(tcw->mutex); + + t = al_get_time(); + + duration_1200ths = 0; + /* if ( ! ts->disabled_due_to_error ) { */ + if ( TAPE_E_OK == ts->prior_exception ) { /* duration forced to 0 if tape is disabled */ + e = tape_get_duration_1200ths (ts, &duration_1200ths); + if (TAPE_E_OK != e) { return e; } + } + + /* now actually process the messages from the copy */ + for (msg_i=0; msg_i < msg_n; msg_i++) { + + tape_ctrl_msg_from_gui_t *from_gui; + int32_t tallied; + + from_gui = from_gui_copy + msg_i; + + if ( ! from_gui->ready ) { + log_warn("tapectrl: BUG: from_gui msg from queue does not have ready=true!"); + return TAPE_E_BUG; + } + + /* FIXME: inhibited_by_gui is global when there should be three independent types + * for mouse, leftarrow, rightarrow -- otherwise can use combination of these + * controls to desync the state */ + + /* record activation needs to work even if no tapetime exists yet */ + if ( (TAPECTRL_FROM_GUI_SEEK_AND_SET_REC == from_gui->type) + // && ! ts->disabled_due_to_error) { /* TOHv4.3-a3: disabled gate */ + && (TAPE_E_OK == ts->prior_exception) ) { /* TOHv4.3-a3: disabled gate */ + gui_set_record_mode (from_gui->data.seek.record_activated); + e = tape_set_record_activated (ts, + tv, + acia, + from_gui->data.seek.record_activated, + tv->tapectrl_opened); + if (TAPE_E_OK != e) { break; } + } + + if (duration_1200ths > 0) { + if ( (TAPECTRL_FROM_GUI_SEEK == from_gui->type) + || (TAPECTRL_FROM_GUI_SEEK_AND_SET_REC == from_gui->type)) { + /* keep a copy of current seek position on the mainthread side */ + tcw->seeking_last_position_1200ths = duration_1200ths * from_gui->data.seek.fraction; + e = tape_seek_absolute (ts, + tcw->seeking_last_position_1200ths, + duration_1200ths, + &elapsed_1200ths_actual, + &my_eof, + &(tv->desync_message_printed)); + + if ( ! tcw->inhibited_by_gui ) { /* latch this */ + tcw->inhibited_by_gui = from_gui->data.seek.held + || from_gui->data.seek.left_held + || from_gui->data.seek.right_held; + } + + if (TAPE_E_OK == e) { + tapectrl_set_gui_rapid_value_time(tcw, true, false, elapsed_1200ths_actual); /*, duration_1200ths); */ + tapectrl_to_gui_msg_eof(tcw, false, false, my_eof); /* send msg to tapectrl GUI */ +/*printf("tapectrl_to_gui_msg_eof(%u): %s:%s(%d)\n", my_eof, __FILE__, __func__, __LINE__);*/ + } + /* do this manually */ + TAPECTRL_UNLOCK_MUTEX(tcw->mutex); + if (TAPE_E_OK != e) { return e; } + + tv->previous_eof_value = my_eof; + ts->tallied_1200ths = elapsed_1200ths_actual; + + if (from_gui->data.seek.left_held) { + tcw->seeking_left_held = true; + tcw->seeking_autorepeat_start_time = t; + } + if (from_gui->data.seek.right_held) { + tcw->seeking_right_held = true; + tcw->seeking_autorepeat_start_time = t; + } + + } else if ( (TAPECTRL_FROM_GUI_SEEK_RELEASED == from_gui->type)) { + tcw->inhibited_by_gui = false; + } else if (TAPECTRL_FROM_GUI_LEFT_RELEASED == from_gui->type) { + tcw->inhibited_by_gui = false; + tcw->seeking_left_held = false; + } else if (TAPECTRL_FROM_GUI_RIGHT_RELEASED == from_gui->type) { + tcw->inhibited_by_gui = false; + tcw->seeking_right_held = false; + } + if (TAPE_E_OK != e) { return e; } + } /* endif (duration_1200ths > 0) */ + + if (TAPECTRL_FROM_GUI_THREAD_STARTED == from_gui->type) { + /* GUI thread has started up -- need to send any + * lazy init-time messages across from here, + * e.g. current motor status */ + /* note that this still should happen even if the tape is disabled due to error */ + tallied = tv->record_activated ? duration_1200ths : ts->tallied_1200ths; + + /* send about twenty messages to the tapectrl thread, + * under a single mutex lock */ + tapectrl_to_gui_msg_motor (tcw, true, false, ts->ula_motor); + tapectrl_set_gui_rapid_value_time (tcw, false, false, tallied); + tapectrl_to_gui_msg_eof (tcw, false, false, my_eof); /* send msg to tapectrl GUI */ +/*printf("tapectrl_to_gui_msg_eof(%u): %s:%s(%d)\n", my_eof, __FILE__, __func__, __LINE__);*/ + tapectrl_to_gui_msg_error (tcw, false, false, ts->prior_exception); /* TOHv4.3-a3 */ + tapectrl_to_gui_msg_record (tcw, false, false, tv->record_activated); + e = tapectrl_to_gui_msg_stripes (tcw, false, false, &(tv->interval_list)); + if (TAPE_E_OK == e) { + e = tapectrl_to_gui_msg_inlays_2 (tcw, + false, + false, /* unlock */ + ts->uef.globals.num_inlay_scans, + ts->uef.globals.inlay_scans); + } + TAPECTRL_UNLOCK_MUTEX(tcw->mutex); + } + } /* next msg_i */ + + if (duration_1200ths > 0) { /* again ! */ + + if (tcw->seeking_left_held) { + t = al_get_time(); + if (t > (tcw->seeking_autorepeat_start_time + TAPECTRL_KEY_REPEAT_ONSET_S)) { + tcw->seeking_last_position_1200ths -= TAPECTRL_KEY_HOLD_TICK_SEEK_1200THS; + key_held_seek = true; + } + } else if (tcw->seeking_right_held) { + t = al_get_time(); + if (t > (tcw->seeking_autorepeat_start_time + TAPECTRL_KEY_REPEAT_ONSET_S)) { + tcw->seeking_last_position_1200ths += TAPECTRL_KEY_HOLD_TICK_SEEK_1200THS; + key_held_seek = true; + } + } + } + + // if (key_held_seek && ! ts->disabled_due_to_error) { + if (key_held_seek && (TAPE_E_OK == ts->prior_exception)) { + + if (tcw->seeking_last_position_1200ths >= duration_1200ths) { + tcw->seeking_last_position_1200ths = duration_1200ths - TAPE_SEEK_CLAMP_BACK_OFF_1200THS; + } + if (tcw->seeking_last_position_1200ths < 0) { + tcw->seeking_last_position_1200ths = 0; + } + + e = tape_seek_absolute (ts, + tcw->seeking_last_position_1200ths, + duration_1200ths, + &elapsed_1200ths_actual, + &my_eof, + &(tv->desync_message_printed)); + if (TAPE_E_OK != e) { return e; } + + ts->tallied_1200ths = elapsed_1200ths_actual; + + /* complete the loop; echo an acknowledgement back to the tapectrl + * thread, and get it to set the seeker position */ + tapectrl_set_gui_rapid_value_time(tcw, true, false, elapsed_1200ths_actual); /* call locks mutex */ + if (my_eof != tv->previous_eof_value) { + /* maybe update EOF state too */ + tapectrl_to_gui_msg_eof(tcw, false, false, my_eof); /* send msg to tapectrl GUI */ +/*printf("tapectrl_to_gui_msg_eof(%u): %s:%s(%d)\n", my_eof, __FILE__, __func__, __LINE__);*/ + } + tv->previous_eof_value = my_eof; + /* do this manually */ + TAPECTRL_UNLOCK_MUTEX(tcw->mutex); + + } /* endif (key_held_seek) */ + + return e; + +} + + +// main thread +void tapectrl_finish (tape_ctrl_window_t * const tcw, bool * const tapectrl_opened_inout) { + tapectrl_close (tcw, tapectrl_opened_inout); + al_destroy_mutex(tcw->mutex); + memset(tcw, 0, sizeof(tape_ctrl_window_t)); +} + +// main thread +void tapectrl_close (tape_ctrl_window_t * const tcw, bool * const tapectrl_opened_inout) { + + ALLEGRO_MUTEX *m; + int ret, *ret_p; + bool join; + + join = false; + + TAPECTRL_LOCK_MUTEX(tcw->mutex); + join = (NULL != tcw->thread); + tcw->shut_tapectrl_down = true; /* instruct tapectrl thread to shut down */ + TAPECTRL_UNLOCK_MUTEX(tcw->mutex); + + if (join) { + ret_p = &ret; + al_join_thread(tcw->thread, (void **) &ret_p); + } + + TAPECTRL_LOCK_MUTEX(tcw->mutex); + + /* We don't destroy the mutex. Back up its pointer. */ + m = tcw->mutex; + + if (tcw->thread != NULL) { + al_destroy_thread(tcw->thread); + } + tcw->thread = NULL; + + /* destroy bitmaps */ + finish_labels(tcw); + finish_inlays(tcw); + tape_interval_list_finish(&(tcw->interval_list)); /* the TCW copy of the intervals, not the one on tape_vars_t */ + + memset(tcw, 0, sizeof(tape_ctrl_window_t)); + + *tapectrl_opened_inout = false; + + tcw->mutex = m; /* Restore mutex. */ + + TAPECTRL_UNLOCK_MUTEX(tcw->mutex); + +} + +static void finish_inlays (tape_ctrl_window_t * const tcw) { + uint32_t i; + if (NULL == tcw->inlays) { return; } + for (i=0; i < tcw->num_inlays; i++) { + if (NULL != tcw->inlays[i]) { + al_destroy_bitmap(tcw->inlays[i]); + tcw->inlays[i] = NULL; + } + } + free(tcw->inlays); + tcw->inlays = NULL; + tcw->num_inlays = 0; + tcw->current_inlay = 0; +} + +static void finish_labels (tape_ctrl_window_t * const tcw) { + int i; + for (i=0; i < TAPECTRL_NUM_LABELS; i++) { + if (NULL != tcw->labels[i]) { + al_destroy_bitmap(tcw->labels[i]); + tcw->labels[i] = NULL; + } + } +} + +static void recompute_margins(tape_ctrl_window_t * const tcw, int const disp_w, int const disp_h) { + float aspect_ratio, wanted_ratio; + aspect_ratio = ((float)disp_w) / (float) disp_h; + wanted_ratio = ((float)TAPECTRL_W) / (float) TAPECTRL_H; + /* compute margins */ + if (aspect_ratio > wanted_ratio) { + /* window is wide */ + tcw->scale = disp_h / (float) TAPECTRL_H; + tcw->margin_x = 0.5f * (disp_w - (TAPECTRL_W * tcw->scale)); + tcw->margin_y = 0.0f; + } else { + /* window is tall */ + tcw->scale = disp_w / (float) TAPECTRL_W; + tcw->margin_x = 0.0f; + tcw->margin_y = 0.5f * (disp_h - (TAPECTRL_H * tcw->scale)); + } +} + + +static int32_t guithread_duration_from_intervals (const tape_interval_list_t * const iv_list) { + tape_interval_t *ivl; + if ((NULL==iv_list) || (NULL==iv_list->list) || (iv_list->fill<1)) { + return 0; + } + ivl = iv_list->list + iv_list->fill - 1; + return ivl->start_1200ths + ivl->pos_1200ths; +} + + +static void *tape_ctrl_guithread_main (ALLEGRO_THREAD * const thread, void *arg) { + + tape_vars_t *tv; /* NO tape_state_t; communication must occur through tape_vars */ + tape_ctrl_window_t *tcw; + ALLEGRO_EVENT_QUEUE *eq; + ALLEGRO_EVENT ev; + bool quit, just_started; + int mx, my; /* mouse */ + int e; + ALLEGRO_MUTEX *mutex_p; + ALLEGRO_THREAD *thread_p; + ALLEGRO_DISPLAY *tmpdisp; + bool held; + int flagz; + bool can_resize; + int win_w, win_h; + + memset(&ev, 0, sizeof(ALLEGRO_EVENT)); /* TOHv4.3-a1 */ + + tv = (tape_vars_t *) arg; + tcw = &(tv->tapectrl); + + eq = NULL; + e = TAPE_E_OK; + + if (NULL == tcw->mutex) { + log_warn("tapectrl: BUG: mutex is NULL"); + /* testing with valgrind revealed that + * memory was being leaked in this case, + * so go ahead and try to destroy these now */ + tape_interval_list_finish(&(tcw->interval_list)); + finish_inlays(tcw); + finish_labels(tcw); + return NULL; + } + + can_resize = tcw->can_resize; + + al_set_new_window_title("B-Em Tape Control"); + + TAPECTRL_LOCK_MUTEX(tcw->mutex); + + /* window already open? */ + if ((NULL != tcw->display) || (tcw->shut_tapectrl_down)) { + TAPECTRL_UNLOCK_MUTEX(tcw->mutex); + return NULL; + } + + flagz = ALLEGRO_WINDOWED | (can_resize ? ALLEGRO_RESIZABLE : 0); + al_set_new_display_flags (flagz); + win_w = (int)(tcw->scale * TAPECTRL_W); + win_h = (int)(tcw->scale * TAPECTRL_H); + tmpdisp = al_create_display (win_w, win_h); + if (NULL == tmpdisp) { + log_warn("tapectrl: cannot create tape control display!"); + TAPECTRL_UNLOCK_MUTEX(tcw->mutex); + return NULL; + } + + /* OK. Commit to it */ + tcw->display = tmpdisp; + + TAPECTRL_UNLOCK_MUTEX(tcw->mutex); + + eq = al_create_event_queue(); + al_register_event_source(eq, al_get_display_event_source((ALLEGRO_DISPLAY *) tcw->display)); + al_register_event_source(eq, al_get_keyboard_event_source()); + al_register_event_source(eq, al_get_mouse_event_source()); + + recompute_margins(tcw, win_w, win_h); + + quit = 0; + mx = 0; + my = 0; + held = false; + just_started = true; + + /* Let's rap about errors. Note that there are two error codes in play. + * There is the normal error code 'e' here, which handles errors in + * this loop related to the display of the tapectrl window. The + * assumption is that tapectrl errors are fatal to the tapectrl window, + * so the window should shut down if they occur. + * + * Meanwhile there is a second source of error, tcw->reported_error, + * which is used for reporting errors from the main tape system. This + * is the error type that is displayed on the 7-segment in red. + * + * The point here is that if an error occurs in the following loop, + * you have an option -- report it on the 7-seg by setting + * tcw->reported_error, or shut down the tapectrl by setting 'e' and + * then optionally breaking out of the loop. + * + * tcw->reported_error is only accessed from the tapectrl thread, + * so you can set it in here. */ + + while ( ! quit && ! al_get_thread_should_stop (thread) && (TAPE_E_OK == e) ) { + + tape_ctrl_msg_to_gui_t to_gui_copy[TAPECTRL_MSG_QUEUE_SIZE]; + tape_ctrl_msg_from_gui_t from_gui; + tape_ctrl_gui_rapid_values_t rapid_vals; + float f; + int msg_n,msg_i; + int32_t dur; /* TOHv4.3-a3 */ + + memset(&(from_gui.data.seek), 0, sizeof(from_gui.data.seek)); + + from_gui.ready = false; + + if (just_started) { + /* request initial state from main thread */ + from_gui.type = TAPECTRL_FROM_GUI_THREAD_STARTED; + from_gui.ready = true; + } + + /* 1. PREPARE OUTGOING EVENTS (from_gui) */ + // e.g. guithread_main_prepare_msgs_from_gui() + + if ( ! quit && ! al_is_event_queue_empty (eq) && ! just_started ) { + + int kc; + bool rec_pressed; + + al_get_next_event(eq, &ev); + + if (ALLEGRO_EVENT_MOUSE_AXES == ev.type) { continue; } /* ignore */ + + dur = guithread_duration_from_intervals(&(tcw->interval_list)); + + if (ALLEGRO_EVENT_KEY_DOWN == ev.type) { + kc = ev.keyboard.keycode; + if ((ALLEGRO_KEY_LEFT == kc) && (ev.keyboard.display == tcw->display)) { + tcw->time_key_pressed = al_get_time(); + tcw->elapsed_1200ths -= TAPECTRL_KEY_PRESS_SEEK_1200THS; + if (tcw->elapsed_1200ths < 0) { tcw->elapsed_1200ths = 0; } + from_gui.type = TAPECTRL_FROM_GUI_SEEK; + if (0==dur) { + from_gui.data.seek.fraction = 0.0f; + } else { + from_gui.data.seek.fraction = tcw->elapsed_1200ths / (double) dur; + } + from_gui.data.seek.left_held = true; + from_gui.ready = true; + } else if ((ALLEGRO_KEY_RIGHT == kc) && (ev.keyboard.display == tcw->display)) { + tcw->time_key_pressed = al_get_time(); + tcw->elapsed_1200ths += TAPECTRL_KEY_PRESS_SEEK_1200THS; + if (tcw->elapsed_1200ths >= dur) { tcw->elapsed_1200ths = dur; } + from_gui.type = TAPECTRL_FROM_GUI_SEEK; + if (0==dur) { + from_gui.data.seek.fraction = 0.0f; + } else { + from_gui.data.seek.fraction = tcw->elapsed_1200ths / (double) dur; + } + from_gui.data.seek.right_held = true; + from_gui.ready = true; + } + } else if (ALLEGRO_EVENT_KEY_UP == ev.type) { + kc = ev.keyboard.keycode; + if ((ALLEGRO_KEY_LEFT == kc) && (ev.keyboard.display == tcw->display)) { + // leftarrow_held = false; + from_gui.type = TAPECTRL_FROM_GUI_LEFT_RELEASED; + from_gui.data.seek.left_held = false; + from_gui.ready = true; + } else if ((ALLEGRO_KEY_RIGHT == kc) && (ev.keyboard.display == tcw->display)) { + // rightarrow_held = false; + from_gui.type = TAPECTRL_FROM_GUI_RIGHT_RELEASED; + from_gui.data.seek.right_held = false; + from_gui.ready = true; + } + } else if (ALLEGRO_EVENT_DISPLAY_CLOSE == ev.type) { + quit = true; + } else if (ALLEGRO_EVENT_MOUSE_AXES == ev.type) { + mx = ev.mouse.x; + my = ev.mouse.y; + } else if ((ALLEGRO_EVENT_MOUSE_BUTTON_DOWN == ev.type) && (ev.mouse.display == tcw->display)) { + mx = ev.mouse.x; + my = ev.mouse.y; + rec_pressed = false; + e = handle_mouse_button_down (mx, + my, + tcw->scale, + tcw->margin_x, + tcw->margin_y, + tcw->record_activated, + /* sets these variables: */ + &(tcw->current_inlay), + &held, + &rec_pressed); + if (TAPE_E_OK != e) { + quit = true; + break; + } + if (rec_pressed) { + e = guithread_rec_button_pressed(tcw, &from_gui); + } + } else if ((ALLEGRO_EVENT_MOUSE_BUTTON_UP == ev.type) && (ev.mouse.display == tcw->display)) { + from_gui.type = TAPECTRL_FROM_GUI_SEEK_RELEASED; + from_gui.ready = true; + held = false; + } else if ((ALLEGRO_EVENT_MOUSE_ENTER_DISPLAY == ev.type) && (ev.mouse.display == tcw->display)) { + + } else if ((ALLEGRO_EVENT_MOUSE_LEAVE_DISPLAY == ev.type) && (ev.mouse.display == tcw->display)) { + + } else if ((ALLEGRO_EVENT_DISPLAY_RESIZE == ev.type) && (ev.display.source == tcw->display)) { + recompute_margins(tcw, ev.display.width, ev.display.height); + al_acknowledge_resize((ALLEGRO_DISPLAY *) tcw->display); + } + + } + + if (quit) { break; } + + from_gui.timestamp = ev.any.timestamp; + + just_started = false; + + /* 2. SEND AND RECEIVE EVENTS */ + + if (NULL == tcw->mutex) { + log_warn("tapectrl thread: BUG: tapectrl mutex is suddenly NULL"); + return NULL; + } + + TAPECTRL_LOCK_MUTEX (tcw->mutex); + + /* check this again, while we have the mutex locked */ + if (tcw->shut_tapectrl_down) { + TAPECTRL_UNLOCK_MUTEX(tcw->mutex); + quit = 1; + break; + } + + /* send message(s) to main thread */ + if (from_gui.ready) { + tapectrl_queue_from_gui_msg (tcw, &from_gui); + } + + /* with the mutex locked, copy the messages, and the rapid-values */ + memcpy(to_gui_copy, + (const void *) tcw->to_gui, + sizeof(tape_ctrl_msg_to_gui_t) * tcw->to_gui_fill); + msg_n = tcw->to_gui_fill; + tcw->to_gui_fill = 0; /* empty the queue */ + + rapid_vals = tcw->gui_rapid_values; + tcw->gui_rapid_values.time_ready = false; + // tcw->gui_rapid_values.eof_ready = false; + tcw->gui_rapid_values.tone_ready = false; + + TAPECTRL_UNLOCK_MUTEX (tcw->mutex); + + /* 3. HANDLE INCOMING EVENTS (to_gui) + * Working from a copy, so can now proceed without locks. */ + + for (msg_i = 0; (TAPE_E_OK==e) && (msg_i < msg_n); msg_i++) { + tape_ctrl_msg_to_gui_t *msg; + msg = to_gui_copy + msg_i; + switch (msg->type) { + case TAPECTRL_TO_GUI_EOF: + tcw->end_of_tape = msg->eof; + break; + case TAPECTRL_TO_GUI_ERROR: /* TOHv4.3-a3 */ + tcw->reported_error = msg->error; + break; + case TAPECTRL_TO_GUI_MOTOR: + tcw->motor = msg->motor; + break; + case TAPECTRL_TO_GUI_RECORD: + tcw->record_activated = msg->rec; + break; + case TAPECTRL_TO_GUI_BAUD: + tcw->baud300 = msg->baud300; + break; + case TAPECTRL_TO_GUI_DCD: + tcw->dcd = true; //msg->dcd; + tcw->dcd_on_start_time = al_get_time(); + break; + case TAPECTRL_TO_GUI_INLAYS: + /* make errors in inlay scan rendering fatal to tapectrl? + * Could arguably just set reported_error instead for a red light */ + e = guithread_init_inlays (tcw, msg->inlays.fill, msg->inlays.scans); + + msg->inlays.fill = 0; + msg->inlays.scans = NULL; + break; + case TAPECTRL_TO_GUI_STRIPES: + /* guithread_init_intervals() steals the provided intervals list + * which was malloced in the main thread ... */ + e = guithread_init_intervals(tcw, &(msg->stripes)); + /* again, treat these errors as fatal to tapectrl */ + break; + default: + break; + + } /* end switch (msg type) */ + } /* next msg, from to_gui queue */ + + if (TAPE_E_OK != e) { break; } + + // 3b. RAPID VALUES + + if (rapid_vals.time_ready) { + tcw->elapsed_1200ths = rapid_vals.elapsed_1200ths; + } + if (rapid_vals.tone_ready) { + tcw->have_signal = (rapid_vals.tone != 'S'); + tcw->have_data = (tcw->have_signal && rapid_vals.tone != 'L'); + msg_n++; + } + + /* Hack: Clear the DCD indicator after a brief pulse. */ + if (tcw->dcd && (al_get_time() > (tcw->dcd_on_start_time + 0.06f))) { + tcw->dcd = false; + msg_n++; + } + + /* 4. HANDLE 'HELD' CONDITION */ + + /* Mouse button was pressed or is currently held */ + + dur = guithread_duration_from_intervals(&(tcw->interval_list)); + + if (held) { + + mx = ev.mouse.x; + my = ev.mouse.y; + f = NAN; + e = seek_frac_from_mouse_xy (mx, -1, tcw->margin_x, tcw->margin_y, tcw->scale, &f); + if (TAPE_E_TAPECTRL_OUTSIDE_ZONE == e) { /* trap this */ + e = TAPE_E_OK; + } else if (TAPE_E_OK != e) { + /* break; */ + } + if (TAPE_E_OK == e) { + from_gui.type = TAPECTRL_FROM_GUI_SEEK; + from_gui.data.seek.fraction = f; + from_gui.ready = true; + /* send another, new, separate event to the main thread */ + TAPECTRL_LOCK_MUTEX (tcw->mutex); + tapectrl_queue_from_gui_msg(tcw, &from_gui); + tcw->gui_rapid_values.suppress_next_rapid_time_value = true; /* TOHv4.3-a4: suppress bounce glitch */ + TAPECTRL_UNLOCK_MUTEX(tcw->mutex); + + /* this is the line of code that implements a snappy seek response, + * while we wait for the messages to go to the main thread and then + * the response to come back */ + tcw->elapsed_1200ths = (int32_t) (f * dur); + + if (tcw->shut_tapectrl_down) { /* (again, check this while we have the opportunity) */ + quit = true; + break; + } + + } + + } /* endif (held) */ + + // 5. PAINT + + e = guithread_main_paint(tcw, + (tcw->margin_y > 0) ? tcw->margin_y : tcw->margin_x, + tcw->margin_y > 0, + tcw->scale); + if (TAPE_E_OK != e) { break; } + + /* again, paint errors will be fatal to tapectrl */ + +#ifndef BUILD_TAPE_TAPECTRL_DELUXE_RESPONSE + if (0 == msg_n) { + /* no messages needed to be dealt with, so sleep for a bit */ +#ifdef WIN32 + Sleep(20); +#else + usleep(20000); +#endif + } +#endif + + } /* end main loop */ + + TAPECTRL_LOCK_MUTEX(tcw->mutex); + + tcw->tapectrl_error = e; + + finish_labels(tcw); + finish_inlays(tcw); + tape_interval_list_finish(&(tcw->interval_list)); /* the TCW copy of the intervals, not the one on tape_vars_t */ + + al_destroy_display((ALLEGRO_DISPLAY *) tcw->display); + tcw->display = NULL; + + /* back up the mutex and the thread */ + mutex_p = tcw->mutex; + thread_p = tcw->thread; + + tcw->mutex = mutex_p; /* don't destroy the mutex */ + tcw->thread = thread_p; /* or the thread */ + + /* This is supposed to be a mainthread-only variable. + * However, Allegro seems to make this impossible. >:| + * This variable should be cleared from the main thread, + * but there seems to be no way of detecting from the + * main thread that the tapectrl thread has shut down + * (without repeatedly locking/unlocking a mutex)?? + */ + tv->tapectrl_opened = false; + + TAPECTRL_UNLOCK_MUTEX (tcw->mutex); + + return NULL; + +} + +/* MUTEX MUST BE LOCKED WHEN CALLED */ +static void queue_to_gui_msg (tape_ctrl_window_t * const tcw, + const tape_ctrl_msg_to_gui_t * const msg) { + if ( tcw->to_gui_fill >= TAPECTRL_MSG_QUEUE_SIZE ) { + log_warn("tapectrl: to_gui_msgs queue is full; flushing"); + tcw->to_gui_fill = 0; + return; + } + tcw->to_gui[tcw->to_gui_fill] = *msg; + tcw->to_gui_fill++; +} + + +/* MUTEX MUST BE LOCKED WHEN CALLED */ +void tapectrl_queue_from_gui_msg (tape_ctrl_window_t * const tcw, + const tape_ctrl_msg_from_gui_t * const msg) { + int i; + if ( ! msg->ready ) { + log_warn("tapectrl: BUG: queue from_gui msg: ready=0"); + return; + } + if ( tcw->from_gui_fill >= TAPECTRL_MSG_QUEUE_SIZE ) { + log_warn("tapectrl: WARNING: from_gui overflow -- flushing queue"); + tcw->from_gui_fill = 0; + return; + } + /* hack: if this is a _SEEK message, preferentially overwrite any + * existing _SEEK message already on the queue */ + if (TAPECTRL_FROM_GUI_SEEK == msg->type) { + for (i=0; i < tcw->from_gui_fill; i++) { + if (TAPECTRL_FROM_GUI_SEEK == tcw->from_gui[i].type) { + tcw->from_gui[i].data.seek.fraction = msg->data.seek.fraction; /* lol */ + return; /* lmao, even */ + } + } + } + tcw->from_gui[tcw->from_gui_fill] = *msg; + tcw->from_gui_fill++; +} + +static int guithread_rec_button_pressed (tape_ctrl_window_t * const tcw, + tape_ctrl_msg_from_gui_t * const from_gui_out) { + /* 1. set main flag + 2. set gui-allegro menu + 3. set tapectrl-side flag + 4. set tapectrl-side GUI state + */ + /* shouldn't need to check tcw->display as we shouldn't be here if there's no window */ + + tcw->record_activated = tcw->record_activated ? false : true; /* toggle tapectrl-side flag */ + + /* build SEEK+REC message to main thread */ + from_gui_out->type = TAPECTRL_FROM_GUI_SEEK_AND_SET_REC; + from_gui_out->data.seek.record_activated = tcw->record_activated; + from_gui_out->data.seek.fraction = (tcw->record_activated) ? 1.0f : 0.0f; + from_gui_out->ready = true; + + return TAPE_E_OK; + +} + +static int handle_mouse_button_down (int const mx, + int const my, + float const scale, + float const margin_x, + float const margin_y, + bool const record_activated, + uint32_t * const current_inlay_inout, + bool * const held_out, + bool * const rec_button_pressed_out) { + + float f; + int e; + + f = NAN; + /* only seek if record is not activated */ + e = TAPE_E_OK; + + /* 1. SEEKER */ + + if ( ! record_activated ) { + e = seek_frac_from_mouse_xy (mx, my, margin_x, margin_y, scale, &f); + if (TAPE_E_OK == e) { /* seek_frac_from_mouse_xy returns error if outside scrubzone */ + *held_out = true; + } + } + + /* 2. INLAY SCAN */ + + if ( (mx > (margin_x + (scale * (float) INLAY_X))) + && (mx < (margin_x + (scale * (float) (INLAY_X + INLAY_SQUARE_SIZE)))) + && (my > (margin_y + (scale * (float) INLAY_Y))) + && (my < (margin_y + (scale * (float) (INLAY_Y + INLAY_SQUARE_SIZE))))) { + /* if clicked, advance inlay scan (value validation is done later) */ + (*current_inlay_inout)++; + } + + /* 3. RECORD BUTTON */ + + if ( (mx > (margin_x + (scale * (float) TAPECTRL_REC_X))) + && (mx < (margin_x + (scale * (float) (TAPECTRL_REC_X + TAPECTRL_REC_W)))) + && (my > (margin_y + (scale * (float) TAPECTRL_REC_Y))) + && (my < (margin_y + (scale * (float) (TAPECTRL_REC_Y + TAPECTRL_REC_H))))) { + *rec_button_pressed_out = true; + } + + return TAPE_E_OK; +} + + + +static ALLEGRO_COLOR interval_type_to_colour (uint8_t type) { + if (TAPE_INTERVAL_TYPE_SILENCE == type) { + return al_map_rgb(COLOUR_INTERVAL_SILENCE); /* navy */ + } else if (TAPE_INTERVAL_TYPE_LEADER == type) { + return al_map_rgb(COLOUR_INTERVAL_LEADER); /* purple */ + } + return al_map_rgb(COLOUR_INTERVAL_DATA); /* pink */ +} + +/* In testing on macOS, this redraw is incredibly crashy when run + * from the tapectrl thread, and a resize happens. + * + * On Linux, it's better, but even here it may throw a video error + * and hang the emulator. The 'nouveau' driver may be flaky. The + * Official Nvidia Driver seems a bit better. + * + * Windows looks OK I think although it may depend on hardware. + * + * Resizing behaviour is therefore now a boolean value at the + * time of GUI thread creation. */ + +static int guithread_main_paint (tape_ctrl_window_t * const tcw, + int const black_bar_w, + bool const letterbox, + float const scale) { + + int lbix, v; + float bm_w, bm_h, lb_w, lb_h; + ALLEGRO_BITMAP *bm; + double margin_x, margin_y, seeker_w, scrub_x; + float vertices[LAMP_PLAY_TRIANGLE_NUM_VERTICES * 2]; + float knob_vx[SEEKER_KNOB_NUM_VERTICES * 2]; + float lamp_x, lamp_y, square_f; + float inlay_w, inlay_h, max_dim; + bool large; + bool draw_lamp_outlines; + float row; + ALLEGRO_COLOR ca, cb; + bool ok; + int e; + int32_t dur; /* TOHv4.3-a3 */ + + margin_x = (float) (letterbox ? 0 : black_bar_w); + margin_y = (float) (letterbox ? black_bar_w : 0); + +#ifdef WIN32 + Sleep(TAPECTRL_PAINT_SLEEP_MS); +#else + usleep(1000 * TAPECTRL_PAINT_SLEEP_MS); +#endif + + ok = (TAPE_E_OK == tcw->reported_error); + + al_clear_to_color (al_map_rgba (COLOUR_BG, 0)); + + /* 1. SEEKER */ + + dur = guithread_duration_from_intervals(&(tcw->interval_list)); + + if ( ok && ! tcw->record_activated ) { /* do not offer seeking in record mode */ + + scrub_x = 0.0f; + seeker_w = scale * (((float) TAPECTRL_W) - (2.0f * ((float) SEEKER_MARGINS_PX))); + + /* FIXME? old code to deal with elapsed_1200ths=-1 ... is this still needed or not? */ + if ( tcw->elapsed_1200ths < 0 ) { + tcw->elapsed_1200ths = dur; + } + if (dur > 0) { + if (tcw->elapsed_1200ths > dur) { + log_warn("tapectrl: seek: WARNING: elapsed_1200ths (%d) > duration (%d), clamping", + tcw->elapsed_1200ths, dur); + tcw->elapsed_1200ths = dur; + } + scrub_x = (tcw->elapsed_1200ths * seeker_w) / (float) dur; + } + + e = guithread_paint_seeker_stripes(tcw, margin_x, margin_y, scale); + if (TAPE_E_OK != e) { return e; } + + scrub_x += (scale * (float) SEEKER_MARGINS_PX); + + for (v=0; v < (SEEKER_KNOB_NUM_VERTICES * 2); v+=2) { + knob_vx[v] = margin_x + scrub_x + (scale * (float) seeker_knob_vertices[v] ); + knob_vx[v+1] = margin_y + (scale * (((float) seeker_knob_vertices[v+1]) + (float) (SEEKER_Y - (SEEKER_H / 2)))); + } + + al_draw_filled_polygon (knob_vx, + SEEKER_KNOB_NUM_VERTICES, + al_map_rgb(COLOUR_SEEKER_KNOB)); + } + + + /* 2. TIME */ + + guithread_update_time (tcw->elapsed_1200ths, + dur, //tcw->duration_1200ths, + scale, + margin_x, + margin_y, + tcw->reported_error); + + lamp_x = margin_x + (scale * (float) TAPECTRL_LAMPS_X); + lamp_y = margin_y + (scale * (float) TAPECTRL_LAMP_TOP_ROW_Y); + + + /* 3. LAMPARRAY */ + + if (ok) { + + draw_lamp_outlines = false; +#ifdef BUILD_TAPE_TAPECTRL_LAMP_OUTLINES + draw_lamp_outlines = true; +#define OUTLINE_THICKNESS (1.0f) +#endif + + ca = al_map_rgb(COLOUR_LAMP_A); + cb = al_map_rgb(COLOUR_LAMP_OUTLINE); + + row = 0.0f; + if (tcw->have_signal) { + al_draw_filled_circle (lamp_x, + lamp_y + (scale * row * (float) TAPECTRL_LAMP_ROW_H), + scale * (float) TAPECTRL_LAMP_BULB_RADIUS, + ca); + } else if (draw_lamp_outlines) { + al_draw_circle (lamp_x, + lamp_y + (scale * row * (float) TAPECTRL_LAMP_ROW_H), + scale * (float) TAPECTRL_LAMP_BULB_RADIUS, + cb, + OUTLINE_THICKNESS * scale); + } + + row += 1.0f; + if (tcw->have_data) { + al_draw_filled_circle (lamp_x, + lamp_y + (scale * row * (float) TAPECTRL_LAMP_ROW_H), + scale * (float) TAPECTRL_LAMP_BULB_RADIUS, + ca); + } else if (draw_lamp_outlines) { + al_draw_circle (lamp_x, + lamp_y + (scale * row * (float) TAPECTRL_LAMP_ROW_H), + scale * (float) TAPECTRL_LAMP_BULB_RADIUS, + cb, + OUTLINE_THICKNESS * scale); + } + + row += 1.0f; + if (tcw->dcd) { + al_draw_filled_circle (lamp_x, + lamp_y + (scale * row * (float) TAPECTRL_LAMP_ROW_H), + scale * (float) TAPECTRL_LAMP_BULB_RADIUS, + ca); + } else if (draw_lamp_outlines) { + al_draw_circle (lamp_x, + lamp_y + (scale * row * (float) TAPECTRL_LAMP_ROW_H), + scale * (float) TAPECTRL_LAMP_BULB_RADIUS, + cb, + OUTLINE_THICKNESS * scale); + } + + row += 1.0f; + if (tcw->baud300) { + al_draw_filled_circle (lamp_x, + lamp_y + (scale * row * (float) TAPECTRL_LAMP_ROW_H), + scale * (float) TAPECTRL_LAMP_BULB_RADIUS, + ca); + } else if (draw_lamp_outlines) { + al_draw_circle (lamp_x, + lamp_y + (scale * row * (float) TAPECTRL_LAMP_ROW_H), + scale * (float) TAPECTRL_LAMP_BULB_RADIUS, + cb, + OUTLINE_THICKNESS * scale); + } + + row += 1.0f; + if (tcw->end_of_tape && ! tcw->record_activated) { /* record inhibits EOF */ + al_draw_filled_circle (lamp_x, + lamp_y + (scale * row * (float) TAPECTRL_LAMP_ROW_H), + scale * (float) TAPECTRL_LAMP_BULB_RADIUS, + ca); + } else if (draw_lamp_outlines) { + al_draw_circle (lamp_x, + lamp_y + (scale * row * (float) TAPECTRL_LAMP_ROW_H), + scale * (float) TAPECTRL_LAMP_BULB_RADIUS, + cb, + OUTLINE_THICKNESS * scale); + } + } /* endif (ok) */ + + /* 4. PLAY TRIANGLE */ + + if (ok) { + + for (v=0; v < (LAMP_PLAY_TRIANGLE_NUM_VERTICES * 2); v+=2) { + vertices[v] = margin_x + (scale * (float) lamp_play_triangle_vertices[v] ); + vertices[v+1] = margin_y + (scale * (float) lamp_play_triangle_vertices[v+1]); + } + + if (tcw->motor) { + al_draw_filled_polygon (vertices, + LAMP_PLAY_TRIANGLE_NUM_VERTICES, /* 3, natch */ + al_map_rgb(COLOUR_LAMP_PLAY)); + } else if (draw_lamp_outlines) { + al_draw_polygon(vertices, + LAMP_PLAY_TRIANGLE_NUM_VERTICES, + ALLEGRO_LINE_JOIN_ROUND, + al_map_rgb(COLOUR_LAMP_PLAY_OUTLINE), + OUTLINE_THICKNESS * scale, + 1.0f); + } + } /* endif (ok) */ + + /* 5. RECORD CIRCLE */ + + if (ok) { + if (tcw->record_activated) { + al_draw_filled_circle (margin_x + (scale * (float) LAMP_RECORD_X), + margin_y + (scale * (float) LAMP_RECORD_Y), + (scale * (float) SEVENSEG_BAR_LEN) / 2.0f, + al_map_rgb(COLOUR_LAMP_REC)); + } else if (draw_lamp_outlines) { + al_draw_circle (margin_x + (scale * (float) LAMP_RECORD_X), + margin_y + (scale * (float) LAMP_RECORD_Y), + (scale * (float) SEVENSEG_BAR_LEN) / 2.0f, + al_map_rgb(COLOUR_LAMP_REC_OUTLINE), + OUTLINE_THICKNESS * scale); + } + } + + /* 6. LABELS */ + + if (ok) { + + lb_w = (float) TAPECTRL_LABEL_WIDTH; + lb_h = (float) TAPECTRL_LABEL_HEIGHT; + + for (lbix=0; lbix < (TAPECTRL_NUM_LABELS - 1); lbix++) { + float row_offset; + float fudge; + /* fudge: move labels in capitals down one pixel */ + fudge = ((2==lbix)||(3==lbix)) ? 2.0f : 3.0f; + row_offset = TAPECTRL_LAMP_TOP_ROW_Y + + (lbix * TAPECTRL_LAMP_ROW_H) + - (fudge + TAPECTRL_LAMP_BULB_RADIUS); /* adjust the 3 */ + al_draw_scaled_bitmap (tcw->labels[lbix], + 0.0f, 0.0f, + lb_w, lb_h, + margin_x + (scale * (float) TAPECTRL_LAMP_LABELS_X), + margin_y + (scale * row_offset), + lb_w * scale, lb_h * scale, + 0); + } + + } + + /* 7. INLAY SCAN BORDER */ + + square_f = (float) INLAY_SQUARE_SIZE; + + if (ok) { + al_draw_rectangle (margin_x + (scale * ((-RECT_OUTLINE_W_PX_FLT) + (float)(INLAY_X))), + margin_y + (scale * ((-RECT_OUTLINE_W_PX_FLT) + (float)(INLAY_Y))), + margin_x + (scale * ((square_f) + (float)(INLAY_X))), + margin_y + (scale * ((square_f) + (float)(INLAY_Y))), + al_map_rgb(COLOUR_RECT_OUTLINE), + scale * RECT_OUTLINE_W_PX_FLT); + } + + /* 8. INLAY SCAN IMAGE */ + + if (ok && (tcw->num_inlays > 0)) { + + if (tcw->current_inlay >= tcw->num_inlays) { + tcw->current_inlay = 0; + } + + bm = tcw->inlays[tcw->current_inlay]; + + bm_w = (float) al_get_bitmap_width (bm); + bm_h = (float) al_get_bitmap_height (bm); + + max_dim = (bm_w > bm_h) ? bm_w : bm_h; + + large = (max_dim > square_f); + + if (large) { + if (bm_w > bm_h) { + inlay_w = scale * square_f; + inlay_h = scale * ((bm_h * square_f) / bm_w); + } else { + inlay_w = scale * ((bm_w * square_f) / bm_h); + inlay_h = scale * square_f; + } + } else { + /* both dimensions are smaller than the frame + image appears 1:1, centred in both X and Y */ + inlay_w = scale * bm_w; + inlay_h = scale * bm_h; + } + + al_draw_scaled_bitmap(bm, + 0, + 0, + bm_w, + bm_h, + margin_x + (scale * ( ((float)INLAY_X) + ((square_f-bm_w)/2.0f))), + margin_y + (scale * ( ((float)INLAY_Y) + ((square_f-bm_h)/2.0f))), + inlay_w, + inlay_h, + 0); + } /* endif (ok) */ + + /* 9. RECORD BUTTON */ + if (ok) { + al_draw_rectangle (margin_x + (scale * (float) TAPECTRL_REC_X), + margin_y + (scale * (float) TAPECTRL_REC_Y), + margin_x + (scale * (float) (TAPECTRL_REC_X + TAPECTRL_REC_W)), + margin_y + (scale * (float) (TAPECTRL_REC_Y + TAPECTRL_REC_H)), + al_map_rgb(COLOUR_RECT_OUTLINE), + RECT_OUTLINE_W_PX_FLT * scale); + + al_draw_scaled_bitmap (tcw->labels[TAPECTRL_IX_REC], + 0.0f, 0.0f, + lb_w, lb_h, + margin_x + (scale * (float) (TAPECTRL_REC_X + TAPECTRL_REC_LBL_OFF_X)), + margin_y + (scale * (float) (TAPECTRL_REC_Y + TAPECTRL_REC_LBL_OFF_Y)), + lb_w * scale, lb_h * scale, + 0); + } + + al_flip_display(); + + return TAPE_E_OK; + +} + + +void tapectrl_set_record (tape_ctrl_window_t * const tcw, + bool const activated, + int32_t const duration_1200ths) { + /* mutex will be NULL if 'set record activated' is arriving from the command-line, + * so test for that */ + if (tcw->mutex != NULL) { + tapectrl_to_gui_msg_record (tcw, true, false, activated); + tapectrl_set_gui_rapid_value_time(tcw, false, true, duration_1200ths); + } +} + + +/* TOHv4.3-a3: signal, EOF and time messages have been replaced with + * just setting some variables on tcw. */ +int tapectrl_set_gui_rapid_value_signal (tape_ctrl_window_t * const tcw, + bool const need_lock, + bool const need_unlock, + char const tonecode) { + if ('\0' == tonecode) { + log_warn("tapectrl: BUG: set signal rapid value: tonecode is nil\n"); + return TAPE_E_BUG; + } + if (need_lock) { TAPECTRL_LOCK_MUTEX(tcw->mutex); } + if (tcw->display != NULL) { + tcw->gui_rapid_values.tone = tonecode; + tcw->gui_rapid_values.tone_ready = true; + } + if (need_unlock) { TAPECTRL_UNLOCK_MUTEX(tcw->mutex); } + return TAPE_E_OK; +} + +void tapectrl_to_gui_msg_eof (tape_ctrl_window_t * const tcw, + bool const need_lock, + bool const need_unlock, + bool const end) { + tape_ctrl_msg_to_gui_t msg; + msg.type = TAPECTRL_TO_GUI_EOF; + msg.eof = end; + if (need_lock) { TAPECTRL_LOCK_MUTEX(tcw->mutex); } + if (tcw->display != NULL) { queue_to_gui_msg(tcw, &msg); } + if (need_unlock) { TAPECTRL_UNLOCK_MUTEX(tcw->mutex); } +} + +void tapectrl_set_gui_rapid_value_time (tape_ctrl_window_t * const tcw, + bool const need_lock, + bool const need_unlock, + int32_t const elapsed_1200ths) { + if (need_lock) { TAPECTRL_LOCK_MUTEX(tcw->mutex); } + if (NULL != tcw->display) { + /* When user performs a seek, the seeker marker is updated immediately + * by the tapectrl thread. It also sends a from_gui _SEEK message. + * However, before the main thread processes the seek message, it may + * update the time on the tapectrl by sending it a to_gui message. + * This will contain the old time rather than the new one requested + * by the user, leading to a harmless but annoying visual glitch. + * + * Combat this by allowing the "do seek click" routine on tapectrl + * to suppress the next time update being sent by the main thread: + */ + if (tcw->gui_rapid_values.suppress_next_rapid_time_value) { /* TOHv4.3-a4 */ + tcw->gui_rapid_values.suppress_next_rapid_time_value = false; /* cancel this */ + } else { /* otherwise, actually send the update */ + tcw->gui_rapid_values.elapsed_1200ths = elapsed_1200ths; + tcw->gui_rapid_values.time_ready = true; + } + } + if (need_unlock) { TAPECTRL_UNLOCK_MUTEX(tcw->mutex); } +} + +void tapectrl_to_gui_msg_dcd (tape_ctrl_window_t * const tcw, + bool const need_lock, + bool const need_unlock) { + tape_ctrl_msg_to_gui_t msg; + msg.type = TAPECTRL_TO_GUI_DCD; + if (need_lock) { + TAPECTRL_LOCK_MUTEX(tcw->mutex); + } + if (NULL != tcw->display) { /* check it again */ + queue_to_gui_msg(tcw, &msg); + } + if (need_unlock) { + TAPECTRL_UNLOCK_MUTEX(tcw->mutex); + } +} + + +/* send record state to tapectrl thread */ +void tapectrl_to_gui_msg_record (tape_ctrl_window_t * const tcw, + bool const need_lock, + bool const need_unlock, + bool const rec) { + tape_ctrl_msg_to_gui_t msg; + msg.type = TAPECTRL_TO_GUI_RECORD; + msg.rec = rec; + if (need_lock ) { TAPECTRL_LOCK_MUTEX(tcw->mutex); } + if (tcw->display != NULL) { queue_to_gui_msg(tcw, &msg); } + if (need_unlock) { TAPECTRL_UNLOCK_MUTEX(tcw->mutex); } +} + +/* send motor state to tapectrl thread */ + +void tapectrl_to_gui_msg_motor (tape_ctrl_window_t * const tcw, + bool const need_lock, + bool const need_unlock, + bool const motor) { + tape_ctrl_msg_to_gui_t msg; + msg.type = TAPECTRL_TO_GUI_MOTOR; + msg.motor = motor; + if (need_lock) { TAPECTRL_LOCK_MUTEX(tcw->mutex); } + if (tcw->display != NULL) { + queue_to_gui_msg(tcw, &msg); + } + if (need_unlock) { TAPECTRL_UNLOCK_MUTEX(tcw->mutex); } +} + + + +/* TOHv4.3-a3 */ +int tapectrl_to_gui_msg_error (tape_ctrl_window_t * const tcw, + bool const need_lock, + bool const need_unlock, + int const error) { + tape_ctrl_msg_to_gui_t msg; + if (TAPE_E_EOF == error) { + log_warn("tapectrl: BUG: Attempt to send to_gui error message of type _EOF"); + return TAPE_E_BUG; + } + msg.type = TAPECTRL_TO_GUI_ERROR; + msg.error = error; + if (need_lock) { TAPECTRL_LOCK_MUTEX(tcw->mutex); } + if (tcw->display != NULL) { queue_to_gui_msg(tcw, &msg); } + if (need_unlock) { TAPECTRL_UNLOCK_MUTEX(tcw->mutex); } + return TAPE_E_OK; +} + +int tapectrl_to_gui_msg_inlays_2 (tape_ctrl_window_t * const tcw, + bool const need_lock, + bool const need_unlock, + int32_t const num_scans, + uef_inlay_scan_t * const scans) { + tape_ctrl_msg_to_gui_t msg; + msg.type = TAPECTRL_TO_GUI_INLAYS; + msg.inlays.fill = num_scans; + msg.inlays.scans = scans; /* permanently steal the allocation */ + if (need_lock) { TAPECTRL_LOCK_MUTEX(tcw->mutex); } + if (tcw->display != NULL) { queue_to_gui_msg(tcw, &msg); } + if (need_unlock) { TAPECTRL_UNLOCK_MUTEX(tcw->mutex); } + return TAPE_E_OK; +} + +int tapectrl_to_gui_msg_stripes (tape_ctrl_window_t * const tcw, + bool const need_lock, + bool const need_unlock, + const tape_interval_list_t * const intervals) { /* this will be from tape_vars */ + + tape_ctrl_msg_to_gui_t msg; + int e; + + msg.type = TAPECTRL_TO_GUI_STRIPES; + + if (NULL == intervals) { + memset(&(msg.stripes), 0, sizeof(tape_interval_list_t)); + } else { +/*printf("tapectrl_to_gui_msg_stripes(): %d intervals.\n", intervals->fill);*/ + /* Allocate a fresh copy of the master intervals list from tape_vars. + * A pointer to this copy will be passed in the to_gui message, and + * the allocation will be managed henceforth by the tapectrl thread. */ + e = tape_interval_list_clone (&(msg.stripes), intervals); + if (TAPE_E_OK != e) { return e; } + } + + if (need_lock) { TAPECTRL_LOCK_MUTEX(tcw->mutex); } + if (tcw->display != NULL) { queue_to_gui_msg(tcw, &msg); } + if (need_unlock) { TAPECTRL_UNLOCK_MUTEX(tcw->mutex); } + + return TAPE_E_OK; + +} + +void tapectrl_to_gui_msg_baud (tape_ctrl_window_t * const tcw, + bool const need_lock, + bool const need_unlock, + bool const baud300) { + tape_ctrl_msg_to_gui_t msg; + msg.type = TAPECTRL_TO_GUI_BAUD; + msg.baud300 = baud300; + if (need_lock) { TAPECTRL_LOCK_MUTEX(tcw->mutex); } + if (tcw->display != NULL) { queue_to_gui_msg(tcw, &msg); } + if (need_unlock) { TAPECTRL_UNLOCK_MUTEX(tcw->mutex); } +} + +static int guithread_update_time (int32_t const elapsed_1200ths, + int32_t const duration_1200ths, /* TODO: not currently used */ + float const scale, + float const margin_xin, + float const margin_yin, + int const reported_error) { + ALLEGRO_COLOR ca; + int ok; + int hrs,mins,secs; + ok = (reported_error == TAPE_E_OK); + ca = ok ? al_map_rgb(COLOUR_7SEG) : al_map_rgb(COLOUR_ERROR); + + hrs = mins = secs = 0; + if (ok) { + to_hours_minutes_seconds(elapsed_1200ths, &hrs, &mins, &secs); + } else { + secs = reported_error % 100; + mins = (reported_error / 100) % 60; + } + draw_7seg(ca, hrs, mins, secs, scale, margin_xin, margin_yin, reported_error==TAPE_E_OK); /* draw new */ + return TAPE_E_OK; +} + +/* don't bother protecting this w/paint_mutex; only runs on tapectrl init */ +static int init_labels (tape_ctrl_window_t * const tcw) { + int e,i; + e = TAPE_E_OK; + finish_labels(tcw); + /* needs to be MEMORY_BITMAP on macOS, or bad things occur */ + al_set_new_bitmap_flags(ALLEGRO_MEMORY_BITMAP); //ALLEGRO_VIDEO_BITMAP); //); + for (i=0; i < TAPECTRL_NUM_LABELS; i++) { + ALLEGRO_BITMAP *ab; + ab = al_create_bitmap(TAPECTRL_LABEL_WIDTH, TAPECTRL_LABEL_HEIGHT); + if (NULL == ab) { + log_warn("tapectrl init: al_create_bitmap for label %d failed", i); + e = TAPE_E_ALLEGRO_CREATE_BITMAP; + break; + } + e = plot_label_to_bitmap (ab, tapectrl_labels[i]); /* convert 2bpp -> native */ + if (TAPE_E_OK != e) { break; } + tcw->labels[i] = ab; + } + if (TAPE_E_OK != e) { + /* clean up */ + finish_labels(tcw); + } + return e; +} + + + +static int +guithread_init_intervals (tape_ctrl_window_t * const tcw, + /* This is the copy on the to_gui message. + * We will take it. */ + tape_interval_list_t * const interval_list_or_null) { + + int e; + + e = TAPE_E_OK; + + tape_interval_list_finish(&(tcw->interval_list)); + + if (NULL == interval_list_or_null) { + memset(&(tcw->interval_list), 0, sizeof(tape_interval_list_t)); + } else { + tcw->interval_list = *interval_list_or_null; /* take the copy from the to_gui msg */ + } + + return e; +} + + + +static int guithread_init_inlays (tape_ctrl_window_t * const tcw, + uint32_t const num_scans, + uef_inlay_scan_t * const scans) { + + int e; + uint32_t u; + + /* found by valgrind testing: malloc(0) */ + if ( 0 == num_scans ) { + finish_inlays(tcw); + return TAPE_E_OK; + } + + e = TAPE_E_OK; + + do { + + finish_inlays(tcw); + + /* rely on bound on num global chunks to make this sane */ + tcw->inlays = malloc(num_scans * sizeof(ALLEGRO_BITMAP *)); + if (NULL == tcw->inlays) { + log_warn("tapectrl init: out of memory allocating tapectrl inlays"); + e = TAPE_E_MALLOC; + break; + } + + al_set_new_bitmap_flags(ALLEGRO_MEMORY_BITMAP); /* hopefully avoid macOS chaos */ + + for (u=0; u < num_scans; u++) { + + uef_inlay_scan_t *scan; + uint32_t x, y, k; + ALLEGRO_BITMAP *bitmap; + ALLEGRO_LOCKED_REGION *region; + + scan = scans + u; + + bitmap = al_create_bitmap(scan->w, scan->h); + + if (NULL == bitmap) { + log_warn("tapectrl init: al_create_bitmap for inlay %u failed", u); + e = TAPE_E_ALLEGRO_CREATE_BITMAP; + break; + } + + region = al_lock_bitmap(bitmap, ALLEGRO_PIXEL_FORMAT_RGBA_8888, ALLEGRO_LOCK_WRITEONLY); + if (NULL == region) { + log_warn("tapectrl: al_lock_bitmap failed"); + e = TAPE_E_ALLEGRO_LOCK_BITMAP; + break; + } + + al_set_target_bitmap(bitmap); + al_reset_clipping_rectangle(); + + if (scan->bpp != 8) { + log_warn("tapectrl: WARNING: skipping inlay scan #%u having unsupported bpp (%u)\n", + u, scan->bpp); + al_draw_filled_rectangle (0.0f,0.0f,INLAY_SQUARE_SIZE,INLAY_SQUARE_SIZE,al_map_rgb(255,255,0)); + al_draw_line (0.0f,0.0f,INLAY_SQUARE_SIZE,INLAY_SQUARE_SIZE,al_map_rgb(255,0,255),INLAY_SQUARE_SIZE/10.0f); + al_draw_line (INLAY_SQUARE_SIZE,0.0f,0.0f,INLAY_SQUARE_SIZE,al_map_rgb(255,0,255),INLAY_SQUARE_SIZE/10.0f); + } else { + for (y=0, k=0; y < scan->h; y++) { + for (x=0; x < scan->w; x++, k+=(scan->bpp/8)) { + uint8_t b,g,r; + uint32_t offset; + ALLEGRO_COLOR col; + b=g=r=0; + offset = (0xff&((uint32_t)scan->body[k])) * 3; + if (scan->palette != NULL) { + b = scan->palette[0 + offset]; + g = scan->palette[1 + offset]; + r = scan->palette[2 + offset]; + } else if (scan->grey) { + b = g = r = scan->body[k]; + } + col = al_map_rgb(r,g,b); + al_put_pixel(x,y,col); + } + } + } + al_unlock_bitmap(bitmap); + al_set_target_backbuffer(al_get_current_display()); + tcw->inlays[u] = bitmap; + } + if (TAPE_E_OK != e) { + finish_inlays(tcw); + break; + } + tcw->num_inlays = num_scans; + + } while (0); + + return e; +} + +/* main thread spawns tapectrl thread */ +int tapectrl_start_gui_thread (tape_state_t * const ts, + tape_vars_t * const tv, + bool const can_resize, + float const scale) { + + int e; + int32_t d; + tape_ctrl_window_t *tcw; + e = TAPE_E_OK; + d = 0; + + tcw = &(tv->tapectrl); + + if (tv->tapectrl_opened) { + log_warn("tapectrl: BUG: start thread: tapectrl_opened already set!"); + return TAPE_E_BUG; + } + + if (NULL == tcw->mutex) { + log_warn("tapectrl: BUG: start thread: mutex is NULL"); + return TAPE_E_BUG; + } + + /* if (!ts->disabled_due_to_error) { */ /* TOHv4.3-a2: gate this */ + if ( TAPE_E_OK == ts->prior_exception ) { /* TOHv4.3-a2: gate this */ + e = tape_get_duration_1200ths(ts, &d); + if (TAPE_E_OK != e) { return e; } + } + + e = init_labels(tcw); + if (TAPE_E_OK != e) { return e; } + + /* render inlay scans to native surfaces */ + tcw->num_inlays = 0; + + /* we can call this from the main thread, since the GUI thread isn't running yet */ + if ( (ts->filetype_bits & TAPE_FILETYPE_BITS_UEF) + && (ts->uef.globals.num_inlay_scans > 0)) { /* bugfix: only if we have scans */ + + e = guithread_init_inlays(tcw, + ts->uef.globals.num_inlay_scans, + ts->uef.globals.inlay_scans); + if (TAPE_E_OK != e) { return e; } + + } + + /* we can call this from the main thread, since the GUI thread isn't running yet */ + e = guithread_init_intervals(tcw, NULL); //&(tv->interval_list)); + if (TAPE_E_OK != e) { + finish_inlays(tcw); + return e; + } + + /* before starting the thread, get the tcw state right */ + tcw->record_activated = tv->record_activated; + + // tcw->duration_1200ths = d; + if (tv->record_activated) { + tcw->elapsed_1200ths = d; + ts->tallied_1200ths = d; + } + + /* copy these flags onto tcw for access by the GUI thread. */ + tcw->can_resize = can_resize; + tcw->scale = scale; + + /* make it so that mainthread will now have to lock the mutex + * any time it wants to access protected variables: */ + tv->tapectrl_opened = true; + + TAPECTRL_LOCK_MUTEX(tcw->mutex); + + /* Start the thread. */ + if ( tcw->shut_tapectrl_down ) { + e = TAPE_E_TAPECTRL_THREAD_SHUTTING_DOWN; + } else if ( NULL == tcw->display ) { + tcw->thread = al_create_thread (tape_ctrl_guithread_main, tv); + if (NULL == tcw->thread) { + e = TAPE_E_TAPECTRL_CREATE_THREAD; + tv->tapectrl_opened = false; + } else { + al_start_thread(tcw->thread); + } + } else { + e = TAPE_E_TAPECTRL_THREAD_EXISTS; + } + + if ((TAPE_E_OK != e) && (TAPE_E_TAPECTRL_THREAD_EXISTS != e)) { + finish_inlays(tcw); + tape_interval_list_finish(&(tv->interval_list)); + } + + TAPECTRL_UNLOCK_MUTEX(tcw->mutex); + + return e; + +} + +static int plot_label_to_bitmap (ALLEGRO_BITMAP * const ab, const char * const label) { + + int p,y; + ALLEGRO_LOCKED_REGION *region; + + if (NULL == ab) { + log_warn("tapectrl: BUG: plot_label_to_bitmap called w/NULL bitmap"); + return TAPE_E_BUG; + } + + al_set_target_bitmap(ab); + + region = al_lock_bitmap(ab, ALLEGRO_PIXEL_FORMAT_RGBA_8888, ALLEGRO_LOCK_WRITEONLY); + if (NULL == region) { + log_warn("tapectrl: al_lock_bitmap failed"); + return TAPE_E_ALLEGRO_LOCK_BITMAP; + } + + for (y=0, p=0; ymutex); + e = tapectrl_set_gui_rapid_value_signal (tcw, false, false, 'S'); + if (TAPE_E_OK != e) { + TAPECTRL_UNLOCK_MUTEX(tcw->mutex); + return e; + } +/*printf("tapectrl_to_gui_msg_eof(%u): %s:%s(%d)\n", false, __FILE__, __func__, __LINE__);*/ + tapectrl_to_gui_msg_eof(tcw, false, false, true); /* send msg to tapectrl GUI */ + e = tapectrl_to_gui_msg_error (tcw, false, false, TAPE_E_OK); + if (TAPE_E_OK == e) { + e = tapectrl_to_gui_msg_inlays_2 (tcw, false, false, 0, NULL); + } + if (TAPE_E_OK == e) { + e = tapectrl_to_gui_msg_stripes (tcw, false, false, NULL); + } + finish_inlays(tcw); + tape_interval_list_finish(&(tcw->interval_list)); /* the TCW copy of the intervals, not the one on tape_vars_t */ + TAPECTRL_UNLOCK_MUTEX(tcw->mutex); + return e; +} + + +void _tapectrl_lock_mutex (ALLEGRO_MUTEX * const m) { + al_lock_mutex(m); +} + +void _tapectrl_unlock_mutex (ALLEGRO_MUTEX * const m) { + al_unlock_mutex(m); +} + +/* caution: this will lock+unlock the mutex */ +int send_tone_to_tapectrl_maybe (tape_ctrl_window_t * const tcw, + int32_t const elapsed, + int32_t const duration, + char const tone, + uint32_t * const since_last_tone_sent_to_gui_inout) { + int e; + e = TAPE_E_OK; + /* limit rate of TIME and SIGNAL messages */ + if (*since_last_tone_sent_to_gui_inout > 100) { + *since_last_tone_sent_to_gui_inout = 0; + tapectrl_set_gui_rapid_value_time(tcw, true, false, elapsed); + if (TAPE_E_OK == e) { + e = tapectrl_set_gui_rapid_value_signal(tcw, false, true, tone); + } + } else { + (*since_last_tone_sent_to_gui_inout)++; + } + return e; +} + +static int guithread_paint_seeker_stripes (tape_ctrl_window_t * const tcw, + double margin_x, + double margin_y, + float scale) { + + tape_interval_list_t *ivl; + int32_t ivn; + double next_x_off; + int32_t dur; + + ivl = &(tcw->interval_list); + + dur = guithread_duration_from_intervals(&(tcw->interval_list)); + + for (ivn=0, next_x_off=0.0f; ivn < ivl->fill; ivn++) { + + float x_px, w_px, frac, extra_w; + tape_interval_t *prev, *cur, *next; + + cur = ivl->list + ivn; + +#ifdef BUILD_TAPE_SANITY + if (cur->start_1200ths > (dur)) { + log_warn("tapectrl: paint: BUG: interval %d start (%d) > duration (%d); skip painting", + ivn, cur->start_1200ths, dur); //tcw->duration_1200ths); + return TAPE_E_BUG; + } +#endif + + extra_w = 0.0f; + + /* if both adjacent stripes are wider, widen the current stripe by 1 pixel. + * This emphasises the thinner intervals, so that they should be discernible + * in the seeker bar even if very short (at scale 1.0, at least). */ +#ifdef BUILD_TAPE_TAPECTRL_WIDEN_THIN_STRIPES + if ( (ivn>0) && (ivn < (ivl->fill - 1))) { + prev = cur - 1; + next = cur + 1; + if ((prev->pos_1200ths > cur->pos_1200ths) && (next->pos_1200ths > cur->pos_1200ths)) { + extra_w = 0.5f * scale; + } + } +#endif + + if (0==dur) { + x_px=0; + w_px=0; + } else { + frac = cur->start_1200ths / (float) dur; + x_px = margin_x + next_x_off + (scale * (SEEKER_MARGINS_PX + (frac * (float) (TAPECTRL_W - (2*SEEKER_MARGINS_PX))))); + + frac = cur->pos_1200ths / (float) dur; + w_px = extra_w + (scale * frac * (float) (TAPECTRL_W - (2*SEEKER_MARGINS_PX))) - next_x_off; + } + + al_draw_line(x_px, + margin_y + (scale * (float) SEEKER_Y), + x_px + w_px + extra_w + 1, /* +1 ensures no hairline cracks and will be probably painted over anyway */ + margin_y + (scale * (float) SEEKER_Y), + interval_type_to_colour(ivl->list[ivn].type), + scale * (float) SEEKER_TRACK_WIDTH); + + if (next_x_off > 0.001f) { + next_x_off = 0.0f; + } + + if (extra_w > 0.001f) { + next_x_off = 1.0f * scale; + } + + } + + return TAPE_E_OK; + +} + +#endif /* BUILD_TAPE_TAPECTRL */ diff -uN b-em-0b6f1d2-vanilla/src/tapectrl.h b-em-0b6f1d2-TOH/src/tapectrl.h --- b-em-0b6f1d2-vanilla/src/tapectrl.h 1970-01-01 01:00:00.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/tapectrl.h 2025-11-01 20:16:40.698760378 +0000 @@ -0,0 +1,290 @@ +/* + * B-Em + * This file (C) 2025 'Diminished' + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef __INC_TAPECTRL_H +#define __INC_TAPECTRL_H + +/* this file new in TOHv4.3 */ + +#include +#include + +#include "tape2.h" + +#define TAPECTRL_NUM_LAMPS_NEW 5 +#define TAPECTRL_NUM_LABELS (TAPECTRL_NUM_LAMPS_NEW + 1) + +#define TAPECTRL_FROM_GUI_NONE 0 +#define TAPECTRL_FROM_GUI_SEEK 1 +#define TAPECTRL_FROM_GUI_SEEK_AND_SET_REC 2 +#define TAPECTRL_FROM_GUI_SEEK_RELEASED 3 +#define TAPECTRL_FROM_GUI_LEFT_RELEASED 4 +#define TAPECTRL_FROM_GUI_RIGHT_RELEASED 5 +#define TAPECTRL_FROM_GUI_THREAD_STARTED 6 + +#define TAPECTRL_TO_GUI_NULL 0 +/* #define TAPECTRL_TO_GUI_TIME 1 */ +#define TAPECTRL_TO_GUI_EOF 2 +#define TAPECTRL_TO_GUI_MOTOR 3 +#define TAPECTRL_TO_GUI_RECORD 4 +#define TAPECTRL_TO_GUI_BAUD 5 +/* #define TAPECTRL_TO_GUI_SIGNAL 6 */ +#define TAPECTRL_TO_GUI_DCD 7 +#define TAPECTRL_TO_GUI_INLAYS 8 +#define TAPECTRL_TO_GUI_STRIPES 9 +#define TAPECTRL_TO_GUI_ERROR 10 /* error in main thread, inform the GUI of this (or cancel previous error) */ + +#define TAPE_SEEK_CLAMP_BACK_OFF_1200THS 120 /* 0.1 s */ + +#define TAPECTRL_MSG_QUEUE_SIZE 16 + +typedef struct tape_ctrl_msg_from_gui_s { + + /* messages from tapectrl GUI thread to main thread: */ + + int type; /* TAPECTRL_FROM_GUI_... */ + + union { + struct { /* for TAPECTRL_FROM_GUI_SEEK_... */ + float fraction; + bool held; + bool left_held; + bool right_held; + bool record_activated; /* only for TAPECTRL_FROM_GUI_SEEK_AND_SET_REC variant */ + } seek; + /* other event fields would go here */ + } data; + + bool ready; + + double timestamp; /* from allegro event */ + +} tape_ctrl_msg_from_gui_t; + +typedef struct uef_inlay_scan_s uef_inlay_scan_t; /* fwdref */ + +#include "tape-interval.h" + +typedef struct tape_ctrl_msg_to_gui_s { + + /* messages from main thread to tapectrl thread: */ + + uint32_t type; /* TAPECTRL_TO_GUI_... */ + + union { + bool motor; /* TAPECTRL_TO_GUI_MOTOR */ + bool rec; /* TAPECTRL_TO_GUI_RECORD */ + bool baud300; /* TAPECTRL_TO_GUI_BAUD */ + bool eof; /* TAPECTRL_TO_GUI_EOF */ + struct { + int32_t fill; + uef_inlay_scan_t *scans; + } inlays; /* TAPECTRL_TO_GUI_INLAYS */ + tape_interval_list_t stripes; /* TAPECTRL_TO_GUI_STRIPES */ + int error; /* TAPECTRL_TO_GUI_ERROR */ + }; + +} tape_ctrl_msg_to_gui_t; + + +typedef struct tape_ctrl_gui_rapid_values_s { + volatile int32_t elapsed_1200ths; + volatile bool time_ready; + volatile bool suppress_next_rapid_time_value; + volatile char tone; + volatile bool tone_ready; +} tape_ctrl_gui_rapid_values_t; + + +typedef struct tape_ctrl_window_s { + + /* tapectrl GUI thread ONLY ... */ + ALLEGRO_THREAD *thread; + int32_t elapsed_1200ths; /* displayed time */ + bool have_signal; /* lamps */ + bool have_data; + bool dcd; + bool record_activated; + double time_key_pressed; + double time_last_key_repeat; + bool motor; + bool end_of_tape; + bool baud300; + ALLEGRO_BITMAP *labels[TAPECTRL_NUM_LABELS]; + float margin_x; + float margin_y; + float scale; + bool can_resize; + float dcd_on_start_time; /* apply some persistence to the DCD lamp */ + int reported_error; /* current main thread error; now also handles EOF conditions */ + ALLEGRO_BITMAP **inlays; + uint32_t num_inlays; /* copied from UEF globals */ + uint32_t current_inlay; + tape_interval_list_t interval_list; + + /* EITHER thread may access these (via mutex) */ + ALLEGRO_MUTEX *mutex; + volatile ALLEGRO_DISPLAY *display; /* doubles as the "window is open" flag */ + volatile bool shut_tapectrl_down; + volatile int from_gui_fill; + volatile tape_ctrl_msg_from_gui_t from_gui[TAPECTRL_MSG_QUEUE_SIZE]; + volatile int to_gui_fill; + volatile tape_ctrl_msg_to_gui_t to_gui[TAPECTRL_MSG_QUEUE_SIZE]; + volatile int tapectrl_error; /* GUI-side error (thread shuts down?) */ + /* TOHv4.3-a3: Time and signal messages have had to be replaced + * by just locking the mutex and overwriting a value, as they were + * saturating the traditional message queue mechanism. So there are + * effectively two messaging mechanisms now, an infrequent one + * and this frequent one: */ + volatile tape_ctrl_gui_rapid_values_t gui_rapid_values; + + /* main thread ONLY */ + bool seeking_left_held; + bool seeking_right_held; + double seeking_autorepeat_start_time; + int32_t seeking_last_position_1200ths; + /* If seeking using the GUI, then playback is inhibited until the + mouse button or left/rightarrow is released again. */ + bool inhibited_by_gui; + +} tape_ctrl_window_t; + +#include +#include "acia.h" +#include "uef.h" + +/* fwdref */ +typedef struct tape_state_s tape_state_t; + +#ifdef BUILD_TAPE_TAPECTRL /* if not compiled in, keep the structures, but not the functions */ + +int tapectrl_handle_messages (tape_vars_t * const tv, + tape_state_t * const ts, + ACIA * const acia); + +int tapectrl_start_gui_thread (tape_state_t * const ts, + tape_vars_t * const tv, + bool const can_resize, + float const initial_scale); + +void tapectrl_finish (tape_ctrl_window_t * const tcw, bool * const tapectrl_opened_inout); + +void tapectrl_close (tape_ctrl_window_t * const tcw, bool * const tapectrl_opened_inout); + +void tapectrl_set_record (tape_ctrl_window_t * const tcw, + bool const activated, + int32_t const duration_1200ths); + +/* functions for sending messages from main thread to GUI thread */ +void tapectrl_to_gui_msg_record (tape_ctrl_window_t * const tcw, + bool const need_lock, + bool const need_unlock, + bool const rec); + +void tapectrl_to_gui_msg_motor (tape_ctrl_window_t * const tcw, + bool const need_lock, + bool const need_unlock, + bool const motor); + +void tapectrl_to_gui_msg_dcd (tape_ctrl_window_t * const tcw, + bool const need_lock, + bool const need_unlock); + // bool const dcd); + +int tapectrl_to_gui_msg_stripes (tape_ctrl_window_t * const tcw, + bool const need_lock, + bool const need_unlock, + const tape_interval_list_t * const intervals); /* from tape_vars.interval_list */ + +int tapectrl_to_gui_msg_inlays_2 (tape_ctrl_window_t * const tcw, + bool const need_lock, + bool const need_unlock, + int32_t const num_scans, + uef_inlay_scan_t * const scans); /* this will be stolen */ + +void tapectrl_to_gui_msg_baud (tape_ctrl_window_t * const tcw, + bool const need_lock, + bool const need_unlock, + bool const baud300) ; + +int tapectrl_to_gui_msg_error (tape_ctrl_window_t * const tcw, + bool const need_lock, + bool const need_unlock, + int const error); + +void tapectrl_to_gui_msg_eof (tape_ctrl_window_t * const tcw, + bool const need_lock, + bool const need_unlock, + bool const end); + + +/* TOHv4.3-a3: signal and time messages no longer use the queue. + * They now use a separate "one-shot" mechanism, as they were + * completely saturating the message queues previously. */ +int tapectrl_set_gui_rapid_value_signal (tape_ctrl_window_t * const tcw, + bool const need_lock, + bool const need_unlock, + char const tonecode); + +void tapectrl_set_gui_rapid_value_time (tape_ctrl_window_t * const tcw, + bool const need_lock, + bool const need_unlock, + int32_t const elapsed_1200ths); + +int tapectrl_eject (tape_ctrl_window_t * const tcw); + +int send_eof_to_tapectrl_if_changed (tape_ctrl_window_t * const tcw, + int32_t const elapsed, + int32_t const duration, + char const tone, + uint32_t * const since_last_tone_sent_to_gui_inout); + +int send_tone_to_tapectrl_maybe (tape_ctrl_window_t * const tcw, + int32_t const elapsed, + int32_t const duration, + char const tone, + uint32_t * const since_last_tone_sent_to_gui_inout); + +/* exported in TOHv4.3-a3 so that load_successful() can send _THREAD_STARTED to itself */ +void tapectrl_queue_from_gui_msg (tape_ctrl_window_t * const tcw, + const tape_ctrl_msg_from_gui_t * const msg); + +#ifdef BUILD_TAPE_TAPECTRL_PRINT_MUTEX_LOCKS +#define TAPECTRL_LOCK_MUTEX(m) _tapectrl_lock_mutex(m); printf(" lock: %s:%s(%d)\n", __FILE__, __func__, __LINE__) +#define TAPECTRL_UNLOCK_MUTEX(m) _tapectrl_unlock_mutex(m); printf("unlock: %s:%s(%d)\n", __FILE__, __func__, __LINE__) +#elif defined BUILD_TAPE_TAPECTRL_LOG_MUTEX_LOCKS +#define TAPECTRL_LOCK_MUTEX(m) _tapectrl_lock_mutex(m); log_warn(" lock: %s:%s(%d)", __FILE__, __func__, __LINE__) +#define TAPECTRL_UNLOCK_MUTEX(m) _tapectrl_unlock_mutex(m); log_warn("unlock: %s:%s(%d)", __FILE__, __func__, __LINE__) +#else +#define TAPECTRL_LOCK_MUTEX(m) _tapectrl_lock_mutex(m) +#define TAPECTRL_UNLOCK_MUTEX(m) _tapectrl_unlock_mutex(m) +#endif + +/* +#define TAPECTRL_LOCK_MUTEX(m) +#define TAPECTRL_UNLOCK_MUTEX(m) +*/ + +void _tapectrl_lock_mutex (ALLEGRO_MUTEX * const m); +void _tapectrl_unlock_mutex (ALLEGRO_MUTEX * const m); + + +#endif /* BUILD_TAPE_TAPECTRL */ + +#endif /* __INC_TAPECTRL_H */ diff -uN b-em-0b6f1d2-vanilla/src/tapectrl-labels.h b-em-0b6f1d2-TOH/src/tapectrl-labels.h --- b-em-0b6f1d2-vanilla/src/tapectrl-labels.h 1970-01-01 01:00:00.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/tapectrl-labels.h 2025-11-01 20:10:05.495892398 +0000 @@ -0,0 +1,264 @@ +/* + * B-Em + * This file (C) 2025 'Diminished' + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifdef TAPECTRL_LABELS_H +#error tapectrl_labels.h included twice +#endif + +#define TAPECTRL_LABELS_H + +/* + * ------------------------------- + * HOW TO MAKE A NEW SET OF LABELS + * ------------------------------- + * + * The font was Google's Roboto Regular. + * + * These labels are manufactured using the following procedure. It + * is rather crude but this is how it is done. Summarising: + * + * - Use imagemagick on a Linux machine to generate 2-bit greyscale + * bitmaps (.gray files) from text based on a font; + * + * - Crudely convert these .gray files into source code with a + * bad C program. + * + * The bad C program is called mksource.c. It will output crude ASCII + * representation of each label on stderr for examination, while sending + * C source code to stdout, for inclusion within this B-Em file + * (tapectrl-labels.h). + * + * mksource.c is here: + * + */ + +/* + #include + + // pipe each N.gray file to this program + // ASCII visible output will be on stderr + // C source code will be on stdout + // peace + + #define WIDTH 40 // MUST MATCH THE .gray FILES! + + #define CODE_INDENT " " + + int main(void) { + int x,y,i; + unsigned char b; + printf("%s",CODE_INDENT); + for (y=0,i=0; (EOF!=i); y++) { + int x; + char *slash; + printf("\""); + // each byte is 4 pixels + for (x=0; x "${N}"-src.txt + echo '' + + N=$[$N+1] + done +*/ + +/* + * The contents of the generated *-src.txt files then need to + * be pasted crudely into this here .h file. This final part of the + * procedure is not very good and would benefit from improvement. + */ + +/* + * + * Labels are 2-bit greyscale (4 shades). + * + */ + +#define TAPECTRL_LABEL_TONE \ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + "\x1d\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + "\x1d\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + "\xff\xc0\xbe\x40\xa7\xe4\x02\xf9\x00\x00" \ + "\x1d\x03\xd6\xd0\xb9\x6c\x0b\x5b\x00\x00" \ + "\x1d\x07\x40\xb0\xb0\x1d\x1d\x03\x80\x00" \ + "\x1d\x0a\x00\x74\xa0\x1d\x2c\x02\x80\x00" \ + "\x1d\x0e\x00\x74\xa0\x1d\x2f\xff\xc0\x00" \ + "\x1d\x0a\x00\x74\xa0\x1d\x2d\x55\x40\x00" \ + "\x1d\x07\x40\xb0\xa0\x1d\x1d\x00\x00\x00" \ + "\x0e\x43\xd6\xd0\xa0\x1d\x0b\x57\x40\x00" \ + "\x0b\xc0\xbe\x40\xa0\x1d\x02\xfd\x00\x00" \ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + +#define TAPECTRL_LABEL_DATA \ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + "\x00\x07\x40\x00\x00\x00\x00\x00\x00\x00" \ + "\x00\x07\x40\x00\x00\x70\x00\x00\x00\x00" \ + "\x00\x07\x40\x00\x00\x70\x00\x00\x00\x00" \ + "\x07\xe7\x40\xbe\x43\xfe\x06\xf9\x00\x00" \ + "\x1e\x5f\x43\x96\xd0\xb4\x1e\x5b\x00\x00" \ + "\x38\x07\x41\x00\xe0\x70\x04\x03\x40\x00" \ + "\x34\x07\x40\xaf\xe0\x70\x06\xff\x40\x00" \ + "\x74\x07\x43\x90\xe0\x70\x1e\x47\x40\x00" \ + "\x34\x07\x47\x00\xe0\x70\x2c\x03\x40\x00" \ + "\x38\x07\x47\x01\xe0\x70\x2c\x07\x40\x00" \ + "\x1e\x5f\x43\x97\xe0\x79\x1e\x6f\x40\x00" \ + "\x07\xe7\x41\xbd\xa0\x2e\x07\xe6\x80\x00" \ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + +#define TAPECTRL_LABEL_EOF \ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + "\x00\x00\x00\x00\x00\x00\x28\x00\x00\x00" \ + "\x00\x00\x00\x00\x00\x00\x28\x00\x00\x00" \ + "\x00\x00\x00\x00\x00\x00\x28\x00\x00\x00" \ + "\x06\xf8\x0a\xbe\x40\x6f\xa8\x00\x00\x00" \ + "\x1e\x5e\x0b\x96\xc0\xf5\xb8\x00\x00\x00" \ + "\x38\x07\x0a\x01\xc1\xd0\x38\x00\x00\x00" \ + "\x34\x07\x4a\x01\xd2\xc0\x28\x00\x00\x00" \ + "\x7f\xff\x4a\x01\xd2\x80\x28\x00\x00\x00" \ + "\x79\x55\x0a\x01\xd2\xc0\x28\x00\x00\x00" \ + "\x38\x00\x0a\x01\xd1\xd0\x38\x00\x00\x00" \ + "\x1e\x5b\x0a\x01\xd0\xf5\xb8\x00\x00\x00" \ + "\x06\xf9\x0a\x01\xd0\x6f\xa8\x00\x00\x00" \ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + +#define TAPECTRL_LABEL_300 \ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + "\x06\xf8\x00\xbe\x40\x1b\xe0\x00\x00\x00" \ + "\x2d\x5e\x02\xd6\xd0\x79\x7c\x00\x00\x00" \ + "\x38\x07\x47\x40\xa0\xb0\x1d\x00\x00\x00" \ + "\x00\x07\x47\x00\xb0\xe0\x0e\x00\x00\x00" \ + "\x00\x1e\x07\x00\x70\xe0\x0e\x00\x00\x00" \ + "\x02\xf8\x0b\x00\x70\xe0\x0e\x00\x00\x00" \ + "\x00\x5e\x07\x00\x70\xe0\x0e\x00\x00\x00" \ + "\x00\x07\x47\x00\x70\xe0\x0e\x00\x00\x00" \ + "\x10\x03\x47\x00\xb0\xe0\x0e\x00\x00\x00" \ + "\x38\x07\x43\x40\xa0\xb0\x1d\x00\x00\x00" \ + "\x2e\x5e\x02\xd6\xd0\x79\x7c\x00\x00\x00" \ + "\x06\xf8\x00\xbe\x40\x1b\xe0\x00\x00\x00" \ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + +#define TAPECTRL_LABEL_DCD \ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + "\x2f\xf9\x00\x06\xf9\x01\xff\x90\x00\x00" \ + "\x2d\x5b\x80\x2e\x5b\x41\xd5\xb8\x00\x00" \ + "\x2c\x01\xd0\x78\x02\xc1\xd0\x1e\x00\x00" \ + "\x2c\x00\xe0\xb0\x01\xd1\xd0\x0b\x00\x00" \ + "\x2c\x00\xb0\xb0\x00\x01\xd0\x07\x40\x00" \ + "\x2c\x00\x70\xe0\x00\x01\xd0\x07\x40\x00" \ + "\x2c\x00\x70\xe0\x00\x01\xd0\x07\x40\x00" \ + "\x2c\x00\xb0\xb0\x00\x01\xd0\x07\x40\x00" \ + "\x2c\x00\xe0\xb0\x01\xd1\xd0\x0b\x00\x00" \ + "\x2c\x01\xd0\x78\x02\xc1\xd0\x1e\x00\x00" \ + "\x2d\x5b\x80\x2e\x5b\x41\xd5\xb8\x00\x00" \ + "\x2f\xf9\x00\x06\xf9\x01\xff\x90\x00\x00" \ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + +#define TAPECTRL_LABEL_REC \ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + "\x2f\xfe\x00\xff\xfd\x01\xbe\x40\x00\x00" \ + "\x2d\x57\xc0\xe5\x54\x0b\x96\xd0\x00\x00" \ + "\x2c\x01\xd0\xd0\x00\x1e\x00\xb0\x00\x00" \ + "\x2c\x00\xe0\xd0\x00\x2c\x00\x74\x00\x00" \ + "\x2c\x00\xe0\xd0\x00\x28\x00\x00\x00\x00" \ + "\x2c\x06\xc0\xff\xf8\x38\x00\x00\x00\x00" \ + "\x2f\xfe\x40\xe5\x50\x38\x00\x00\x00\x00" \ + "\x2d\x5e\x00\xd0\x00\x28\x00\x00\x00\x00" \ + "\x2c\x07\x40\xd0\x00\x2c\x00\x74\x00\x00" \ + "\x2c\x03\x80\xd0\x00\x1e\x00\xb0\x00\x00" \ + "\x2c\x01\xd0\xe5\x54\x0b\x96\xd0\x00\x00" \ + "\x2c\x00\xb0\xff\xfe\x01\xbe\x40\x00\x00" \ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" diff -uN b-em-0b6f1d2-vanilla/src/tape.h b-em-0b6f1d2-TOH/src/tape.h --- b-em-0b6f1d2-vanilla/src/tape.h 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/tape.h 2025-10-31 15:00:34.973968420 +0000 @@ -1,17 +1,369 @@ +/* + * B-Em + * This file (C) 2025 'Diminished' + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + #ifndef __INC_TAPE_H #define __INC_TAPE_H -#include "acia.h" +#include + +#include "tape2.h" + +#include "b-em.h" +#include "tibet.h" +#include "csw.h" +#include "uef.h" +#include "tapectrl.h" +#include "tape-io.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 0xffffffc0 + +#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 + * TOHv4.3: still used in 6502.c! */ +#define TAPE_IS_LOADED(filetype_bits) \ + ((TAPE_FILETYPE_BITS_NONE != (filetype_bits)) && (TAPE_FILETYPES_ALL != (filetype_bits))) + +#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 + + +typedef struct tape_state_s { + + /* reminder! anything on this struct which is a pointer to heap memory + * needs to be deep-copied in tape_state_clone_and_rewind(). */ + + /* 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; /* TOHv2: 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. ALL UEF data gets passed through this mechanism! */ + 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 */ + + bool 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: */ + bool 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 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 0.6s 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; + + /* not sure if this should be on tape_vars_t rather than tape_state_t, + * but tape_state_t already has an internal stream pointer, so another + * time value shouldn't do any additional harm. + * This is also used for recording */ + + /* FIXME: rename to tallied_1200ths */ + int32_t tallied_1200ths; /* TOHv4.3 */ + + /* tally count of subsequent '1' tones in this variable, + * crude RS423 leader detect */ + int rs423_leader_detect; /* TOHv4.3 */ -extern ALLEGRO_PATH *tape_fn; -extern bool tape_loaded; + /* TOHv4.3: hack, 19.2 kbps read */ + char hack_19200ths_last_value; + int hack_19200ths_consumed; -void tape_load(ALLEGRO_PATH *fn); -void tape_close(void); -void tape_poll(void); -void tape_receive(ACIA *acia, uint8_t data); + /* TOHv4.3: allow communicating failure code to tapectrl + * if tapectrl window is opened after the error */ + int prior_exception; -extern int tapelcount,tapellatch,tapeledcount; -extern bool fasttape; +} tape_state_t; + +#include "tape-io.h" + +/* 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: */ + bool expired_quit; + int64_t testing_expire_ticks; /* measured in "otherstuff" ticks */ + + bool overclock; /* a.k.a. fasttape */ + + bool strip_silence_and_leader; /* TOHv3.2: another speed hack */ + + bool record_activated; /* TOHv3 */ + + ALLEGRO_PATH *save_filename; /* TOHv3 */ + ALLEGRO_PATH *load_filename; + + /* TOHv3: TAPE_TEST_xxx bitfields: */ + uint8_t testing_mode; + + /* TOHv3.2 */ + bool save_always_117; + bool save_prefer_112; + + /* TOHv3.2, for knowing when to emit &117 */ + bool cur_baud300; + + tape_shutdown_save_t quitsave_config; /* TOHv3 */ + + bool save_do_not_generate_origin_on_append; /* TOHv3.2 */ + + bool permit_phantoms; + bool disable_debug_memview; + + bool wav_use_phase_shift; /* TOHv4.2 */ + + /* all new in TOHv4.3 */ +#ifdef BUILD_TAPE_TAPECTRL + tape_ctrl_window_t tapectrl; + bool tapectrl_allow_resize; + float tapectrl_ui_scale; + /* allows mainthread to skip locking mutex to check to see whether display exists or not */ + bool tapectrl_opened; + uint32_t since_last_tone_sent_to_gui; /* reduce msg rate to GUI */ + uint32_t since_last_eof_sent_to_gui; /* same */ + /* suppress seek desync error message spam */ + bool desync_message_printed; + /* used to detect change in EOF status, so msg can be sent to tapectrl GUI */ + bool previous_eof_value; + /* support seeker bar striping feature */ + tape_interval_list_t interval_list; #endif + +} tape_vars_t; + +extern int tapeledcount; +extern char tape_dummy_for_unix_scprintf; + +/* saveable state lives here: */ +extern tape_state_t tape_state; + +/* other tape variables go here (e.g. config settings): */ +extern tape_vars_t tape_vars; + +uint32_t tape_read_u32 (uint8_t *in); +uint16_t tape_read_u16 (const uint8_t * const 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" + +int findfilenames_new (tape_state_t * const ts, + bool const show_ui_window, + bool const filter_phantoms); + +void tape_state_init (tape_state_t *t, + tape_vars_t *tv, + uint8_t filetype_bits, + bool alter_menus); + +void tape_state_finish (tape_state_t *t, bool alter_menus); + +int tape_state_clone_and_rewind (tape_state_t *out, tape_state_t *in); + +int tape_handle_acia_master_reset (tape_state_t *t); + +void tape_init (tape_state_t *t, tape_vars_t *tv); + +int tape_stop_motor (tape_state_t * const ts, + tape_ctrl_window_t * const tcw, + /*bool const silence112,*/ + bool const record_activated, + bool const tapectrl_opened); /* TOHv4.3 */ + +int tape_start_motor (tape_state_t * const ts, + tape_ctrl_window_t * const tcw, + bool const also_force_dcd_high, + bool const tapectrl_opened); /* TOHv4.3 */ + +int tape_set_record_activated (tape_state_t * const t, + tape_vars_t * const tv, + ACIA * const acia_or_null, + bool const value, + bool const tapectrl_opened); + +bool tape_is_record_activated (const tape_vars_t * const tv); + +/* exported in TOHv4 */ +void tape_handle_exception (tape_state_t * const ts, + tape_vars_t * const tv, /* TOHv3.2: may be NULL */ + int const error_code, + bool const eof_fatal, + bool const err_fatal, + bool const not_tapecat_mode); + +int tape_state_init_alter_menus (tape_state_t * const ts, /* not const, because of tibet priv horribleness */ + tape_vars_t * const tv); + +void tape_ejected_by_user(tape_state_t * const ts, tape_vars_t * const tv, ACIA * const acia) ; + +void to_hours_minutes_seconds (int32_t const elapsed_1200ths, + int32_t * const h_out, + int32_t * const m_out, + int32_t * const s_out); + +int tape_load_successful (tape_state_t * const ts, + tape_vars_t * const tv, + const char * const path) ; + +int tape_rs423_eat_1200th (tape_state_t * const ts, + tape_vars_t * const tv, + ACIA * const acia, + bool const record_activated, /* TOHv4.3 */ + bool const emit_tapenoise, + bool * const throw_eof_out); + +#endif /* __INC_TAPE_H */ diff -uN b-em-0b6f1d2-vanilla/src/tape-interval.c b-em-0b6f1d2-TOH/src/tape-interval.c --- b-em-0b6f1d2-vanilla/src/tape-interval.c 1970-01-01 01:00:00.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/tape-interval.c 2025-10-31 16:55:05.548674664 +0000 @@ -0,0 +1,212 @@ +/* + * B-Em + * This file (C) 2025 'Diminished' + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include +#include +#include + +#include "tape2.h" +#include "tape-interval.h" + +#include "logging.h" + +void tape_interval_list_finish (tape_interval_list_t * const iv_list_inout) { + if (NULL == iv_list_inout) { return; } + if (NULL != iv_list_inout->list) { + free(iv_list_inout->list); + } + memset(iv_list_inout, 0, sizeof(tape_interval_list_t)); +} + +int tape_interval_list_clone (tape_interval_list_t * const out, + const tape_interval_list_t * const in) { + tape_interval_t *tmp; + /*finish_interval_list(out);*/ + tmp = NULL; + if (in->alloc > 0) { + if (in->fill > in->alloc) { /* TOHv4.3-a3 sanity */ + log_warn("tape-interval: clone interval list: BUG: in->fill(%d) > in->alloc(%d)\n", in->fill, in->alloc); + return TAPE_E_MALLOC; + } + tmp = malloc(in->alloc * sizeof(tape_interval_t)); + if (NULL == tmp) { + log_warn("tapectrl: ERROR: out of memory cloning intervals list"); + return TAPE_E_MALLOC; + } + memset (tmp, 0, in->alloc * sizeof(tape_interval_t)); + memcpy (tmp, in->list, in->fill * sizeof(tape_interval_t)); + } else if (in->fill > 0) { + log_warn("tape-interval: clone interval list: in->fill > 0 but in->alloc == 0"); + return TAPE_E_BUG; + } + out->alloc = in->alloc; + out->fill = in->fill; + out->list = tmp; + return TAPE_E_OK; +} + + +int tape_interval_list_append (tape_interval_list_t * const iv_list_inout, + const tape_interval_t * const to_append, + bool const deduce_start_time) { + + int32_t newsize; + tape_interval_t *tmp, *prev, *cur; + + if (NULL == iv_list_inout) { + log_warn("tape: BUG: append to intervals list: iv_list_inout is NULL"); + return TAPE_E_BUG; + } + + if (iv_list_inout->alloc <= iv_list_inout->fill) { + newsize = iv_list_inout->alloc + 100; + tmp = realloc(iv_list_inout->list, sizeof(tape_interval_t) * newsize); + if (NULL == tmp) { + log_warn("tape: initial scan: appending to interval list: out of memory"); + return TAPE_E_MALLOC; + } + /* zero out the next interval */ + memset((tmp + iv_list_inout->fill), + 0, + sizeof(tape_interval_t) * (newsize - iv_list_inout->fill)); + iv_list_inout->alloc = newsize; + iv_list_inout->list = tmp; + } + + /* +printf("[%d]: %s; (%d + %d)\n", + *num_intervals_inout, + interval_get_name_from_type(to_append->type), + to_append->start_1200ths, + to_append->pos_1200ths); + */ + + iv_list_inout->list[iv_list_inout->fill] = *to_append; + + if (deduce_start_time) { + cur = iv_list_inout->list + iv_list_inout->fill; + if (iv_list_inout->fill > 0) { + prev = cur - 1; + cur->start_1200ths = prev->start_1200ths + prev->pos_1200ths; + } else { + cur->start_1200ths = 0; + } + } + + iv_list_inout->fill++; + + return TAPE_E_OK; + +} + +/* will be used both for recording and for playback (i.e. initial scan) */ + +/* CAUTION: remember to call append_to_interval_list() with iv_tmp_inout + * after this function returns, in order to add the last remaining + * piece to the interval list! */ +int tape_interval_list_send_1200th (tape_interval_list_t * const iv_list_inout, + char tone, /* not const! */ + bool const free_list_on_error) { + int e; + tape_interval_list_decstate_t *dec_p; + + e = TAPE_E_OK; + +#ifdef BUILD_TAPE_SANITY + if ( ! TAPE_TONECODE_IS_LEGAL(tone) ) { + log_warn("tape: BUG: send_1200th_to_interval_list: bad tone (&%x)", tone); + return TAPE_E_BUG; + } +#endif + + dec_p = &(iv_list_inout->decstate); + + if ('1'==tone) { + if (dec_p->leader_detect_1200ths >= TAPE_CRUDE_LEADER_DETECT_1200THS) { + tone = 'L'; + } else { + dec_p->leader_detect_1200ths++; + } + } else { + dec_p->leader_detect_1200ths = 0; + } + + /* if interval type has not been decided yet, commit to one now */ + if (TAPE_INTERVAL_TYPE_PENDING == dec_p->wip.type) { + dec_p->wip.type = tape_interval_type_from_tone(tone); + } + + /* end of this interval? */ + if ( ('\0' != dec_p->prev_tone) + && ( tape_interval_type_from_tone(dec_p->prev_tone) + != tape_interval_type_from_tone(tone) )) { + + e = tape_interval_list_append (iv_list_inout, &(dec_p->wip), true); + if (TAPE_E_OK != e) { + if (free_list_on_error) { + tape_interval_list_finish(iv_list_inout); + } + return e; + } + /* wipe WIP to prepare for the next interval */ + memset(&(dec_p->wip), 0, sizeof(tape_interval_t)); + dec_p->wip.type = TAPE_INTERVAL_TYPE_PENDING; /* (=0, so not strictly needed) */ + } + + iv_list_inout->decstate.wip.pos_1200ths++; + iv_list_inout->decstate.prev_tone = tone; + + return TAPE_E_OK; + +} + + +const char * tape_interval_name_from_type (uint8_t const type) { + if (TAPE_INTERVAL_TYPE_SILENCE == type) { + return "SILENT"; + } else if (TAPE_INTERVAL_TYPE_DATA == type) { + return " data"; + } + return "leader"; +} + +uint8_t tape_interval_type_from_tone (char const tone) { + if ('S' == tone) { + return TAPE_INTERVAL_TYPE_SILENCE; + } else if ('L' == tone) { + return TAPE_INTERVAL_TYPE_LEADER; + } + return TAPE_INTERVAL_TYPE_DATA; +} + +bool tape_interval_list_integrity_check (const tape_interval_list_t * const list) { + int32_t i; + for (i=0; i < (list->fill - 1); i++) { + tape_interval_t *iv_a, *iv_b; + iv_a = list->list + i; + iv_b = iv_a + 1; + if ((iv_a->start_1200ths + iv_a->pos_1200ths) != iv_b->start_1200ths) { + log_warn("tape-interval: BUG: list integrity failure: list[%d]=(%d + %d), but [%d]'s start is %d", + i, iv_a->start_1200ths, iv_a->pos_1200ths, i+1, iv_b->start_1200ths); + return TAPE_E_BUG; + } + } +/*printf("tape_interval_list_integrity_check: %d intervals, OK.\n", i);*/ + return TAPE_E_OK; +} diff -uN b-em-0b6f1d2-vanilla/src/tape-interval.h b-em-0b6f1d2-TOH/src/tape-interval.h --- b-em-0b6f1d2-vanilla/src/tape-interval.h 1970-01-01 01:00:00.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/tape-interval.h 2025-10-29 03:05:39.266448452 +0000 @@ -0,0 +1,82 @@ +/* + * B-Em + * This file (C) 2025 'Diminished' + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef __INC_TAPE_INTERVAL +#define __INC_TAPE_INTERVAL + +#include +#include + +#define TAPE_INTERVAL_TYPE_PENDING 0 /* TOHv4.3-a3 */ +#define TAPE_INTERVAL_TYPE_SILENCE 1 +#define TAPE_INTERVAL_TYPE_LEADER 2 +#define TAPE_INTERVAL_TYPE_DATA 3 + +/* TOHv4.3: attach to UEF chunks, TIBET spans, CSW pulses ... + * - ALSO used for the seeker bar stripes */ +typedef struct tape_interval_s { + uint8_t type; /* TAPE_INTERVAL_TYPE_..., used for bar stripes only */ + int32_t start_1200ths; + int32_t pos_1200ths; + /* "private", for counting 4800ths + * (TIBET: 'P' pulse is s/4800; normal tonechar is s/2400): */ + uint8_t sub_pos_4800ths; +} tape_interval_t; + +/* decoder state */ +/* FIXME: rename to ..._list_decoder_s */ +typedef struct tape_interval_list_decstate_s { + + int32_t leader_detect_1200ths; + char prev_tone; + tape_interval_t wip; + +} tape_interval_list_decstate_t; + +typedef struct tape_interval_list_s { + + tape_interval_t *list; + int32_t alloc; + int32_t fill; + + /* current decoder state */ + tape_interval_list_decstate_t decstate; + +} tape_interval_list_t; + +int tape_interval_list_append (tape_interval_list_t * const iv_list_inout, + const tape_interval_t * const to_append, + bool const deduce_start_time); + +int tape_interval_list_send_1200th (tape_interval_list_t * const iv_list_inout, + char tone, /* not const! */ + bool const free_list_on_error); + +uint8_t tape_interval_type_from_tone (char const tone); + +const char * tape_interval_name_from_type (uint8_t const type); + +void tape_interval_list_finish (tape_interval_list_t * const iv_list_inout); /* TOHv4.3 */ + +int tape_interval_list_clone (tape_interval_list_t * const out, + const tape_interval_list_t * const in); + +bool tape_interval_list_integrity_check (const tape_interval_list_t * const list); /* TOHv4.3-a4 */ + +#endif diff -uN b-em-0b6f1d2-vanilla/src/tape-io.c b-em-0b6f1d2-TOH/src/tape-io.c --- b-em-0b6f1d2-vanilla/src/tape-io.c 1970-01-01 01:00:00.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/tape-io.c 2025-11-02 07:24:42.256428763 +0000 @@ -0,0 +1,690 @@ +/* + * B-Em + * This file (C) 2025 'Diminished' + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "tapeseek.h" +#include "tape-io.h" + +#include + +#define TAPE_FILE_MAXLEN (32 * 1024 * 1024) /* TOHv4.3: doubled for psycho 114 chunk test */ +#define TAPE_MAX_DECOMPRESSED_LEN (64 * 1024 * 1024) + +static int tibet_load_file (tape_state_t * const ts, + tape_vars_t * const tv, + const char * const fn, + bool const decompress, + tibet_t * const t); + +static int csw_load (tape_state_t * const ts, + tape_vars_t * const tv, + const char * const fn); + +static int tibet_load_2 (tape_state_t * const ts, + tape_vars_t * const tv, + const char * const fn, + bool decompress); + +static int uef_load (tape_state_t * const ts, + tape_vars_t * const tv, + const char * const fn); + +static int tibet_load (tape_state_t * const ts, + tape_vars_t * const tv, + const char * const fn) ; + +static int tibetz_load (tape_state_t * const ts, + tape_vars_t * const tv, + const char * const fn); + +static struct +{ + char *ext; + /* TOHv4.3: changed prototype, return int error code, pass ts and tv */ + int (*load)(tape_state_t * const ts, tape_vars_t * const tv, const char * const fn); + void (*close)(); +} +loaders[]= +{ + /* 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 tibet_load_file (tape_state_t * const ts, + tape_vars_t * const tv, + const char * const fn, + bool const decompress, + tibet_t * const 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; + +} + +static int csw_load (tape_state_t * const ts, + tape_vars_t * const tv, + const char * const fn) { + + int e; + + /* TOHv4.3: do not touch ACIA or ULA */ + ts->filetype_bits = TAPE_FILETYPE_BITS_CSW; + tape_state_init_alter_menus(ts, tv); + + e = csw_load_file (fn, &(ts->csw)); + if (TAPE_E_OK != e) { + log_warn("tape: could not load CSW file (code %d): '%s'", e, fn); + } else { + e = tape_load_successful (ts, tv, (char *)fn); + } + return e; +} + + + +/* this may already have set the shutdown exit code; + * so don't propagate errors from this */ +int tape_load(tape_state_t * const ts, + tape_vars_t * const tv, + const ALLEGRO_PATH * const fn) +{ + int e; /* TOHv4.3 */ + int c = 0; + const char *p, *cpath; + if (NULL == fn) { /* TOHv4.3 */ + log_warn("tape: BUG: load: NULL filename supplied"); + return TAPE_E_BUG; + } + cpath = al_path_cstr(fn, ALLEGRO_NATIVE_PATH_SEP); + if ('\0' == cpath[0]) { return TAPE_E_OK; } + /* ignore blanks */ + p = al_get_path_extension(fn); + if ((NULL==p) || ('\0'==*p)) { + log_warn("tape: load: filename has no extension"); + return TAPE_E_BLANK_EXTENSION; + } + if (*p == '.') { p++; } + log_info("tape: Loading %s %s", cpath, p); + while (loaders[c].ext) + { + if (!strcasecmp(p, loaders[c].ext)) + { + /* TOHv4.3: returns a code now */ + e = loaders[c].load(ts, tv, cpath); + /* TOHv4.3: now here rather than duplicated in uef_load, csw_load etc. */ + tape_handle_exception(ts, + tv, + e, + tv->testing_mode & TAPE_TEST_QUIT_ON_EOF, + tv->testing_mode & TAPE_TEST_QUIT_ON_ERR, + true); /* alter menus on failure */ + return TAPE_E_OK; + } + c++; + } + log_warn("tape: load: no loader found for file extension: %s\n", p); + return TAPE_E_UNK_EXT; +} + +static int tibet_load (tape_state_t * const ts, + tape_vars_t * const tv, + const char * const fn) { + return tibet_load_2(ts, tv, fn, false); +} + +static int tibetz_load (tape_state_t * const ts, + tape_vars_t * const tv, + const char * const fn) { + return tibet_load_2(ts, tv, fn, true); +} + + + +/* FIXME: need to change the function pointer for the file type + * loader prototype. needs to return int, and take ts and tv as args. + * all three loaders will then need to have their prototypes changed. */ +static int uef_load (tape_state_t * const ts, + tape_vars_t * const tv, + const char * const fn) { + + int e; + int32_t baud; + + /* TOHv4.3: removed; if tape state is reset on tape replacement, + bad things occur, so we can't do this any more: */ + /*tape_state_init (ts, tv, TAPE_FILETYPE_BITS_UEF, 1);*/ + + /* instead we have to init some things, but not touch the ACIA or ULA */ + ts->filetype_bits = TAPE_FILETYPE_BITS_UEF; + tape_state_init_alter_menus(ts, tv); + + e = uef_load_file (fn, &(ts->uef)); + if (TAPE_E_OK != e) { + log_warn("tape: could not load UEF file (code %d): '%s'", e, fn); + } else { + e = tape_load_successful (ts, tv, (char *) fn); /* TOHv4.3: handle errors properly */ + if (TAPE_E_OK != e) { return e; } + ts->w_uef_was_loaded = 1; + ts->w_uef_origin_written = 0; + baud = 1200; + if (ts->uef.num_chunks>0) { + uef_scan_backwards_for_chunk_117 (&(ts->uef), ts->uef.num_chunks-1, &baud); + if (0 == baud) { baud = 1200; } + ts->w_uef_prevailing_baud = baud; + } + } + return e; +} + +/* FIXME: need to change the function pointer for the file type + * loader prototype. needs to return int, and take ts and tv as args. + * all three loaders will then need to have their prototypes changed. */ +static int tibet_load_2 (tape_state_t * const ts, + tape_vars_t * const tv, + const char *fn, + bool decompress) { + int e; + + /* TOHv4.3: do not touch ACIA or ULA */ + ts->filetype_bits = TAPE_FILETYPE_BITS_TIBET; + tape_state_init_alter_menus(ts, tv); + + e = tibet_load_file (ts, tv, fn, decompress, &(ts->tibet)); + if (TAPE_E_OK != e) { + log_warn("tape-io: could not load TIBET file (code %d): '%s'", e, fn); + } else { + e = tape_load_successful(ts, tv, (char *)fn); /* TOHv4.3: handle errors prop'ly */ + } + return e; +} + + + +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 *ptr; + 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; + ptr = realloc(buf, newsize); + if (NULL == ptr) { + log_warn("tape: Failed to grow file buffer: '%s'", fn); + e = TAPE_E_MALLOC; + break; + } + alloc = newsize; + buf = ptr; + } + 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; + +} + +#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 *ptr; + + 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); + + ptr = 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; + } + ptr = realloc (*out, newsize); + if (NULL == ptr) { + log_warn ("tape: could not decompress data; realloc failed\n"); + inflateEnd (&strm); + if (*out != NULL) { free(*out); } + *out = NULL; + return TAPE_E_MALLOC; + } + *out = ptr; + 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; + +} + + +#include "tapewrite.h" + + +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; + int32_t duration; /* TOHv4.3 */ + + e = TAPE_E_OK; + tape_op_buf = NULL; + f = NULL; + tape_op_buf_len = 0; + bufz = NULL; + bufz_len = 0; + + e = tape_get_duration_1200ths(ts, &duration); /* TOHv4.3 */ + if (TAPE_E_OK != e) { return e; } + + /* make sure any pending pieces are appended before saving */ + e = tape_flush_pending_piece (ts, acia_or_null, silence112, true); + 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, + true, /* 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; + +} + + +int tape_zlib_compress (char * const source_c, + size_t const srclen, + bool const use_gzip_encoding, + char ** const dest_out, + size_t * const destlen_out) { + + int ret, flush; + z_stream strm; + size_t alloced; + uint8_t *source; + + /* allocate deflate state */ + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + + alloced = 0; + + 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_out) { /* realloc if no space left */ + dest2 = realloc(*dest_out, alloced = (*destlen_out ? (*destlen_out * 2) : srclen)); + if (NULL == dest2) { + log_warn("tape: write: compress: Failed to grow zlib buf (newsize %zu).", *destlen_out); + deflateEnd(&strm); + /* TOHv3.2: avoid free(NULL) */ + if (*dest_out != NULL) { free(*dest_out); } + *dest_out = NULL; + return TAPE_E_MALLOC; + } + *dest_out = dest2; + } + strm.avail_out = 0x7fffffff & (alloced - *destlen_out); /* bytes available in output buffer */ + strm.next_out = (uint8_t *) (*dest_out + *destlen_out); /* current offset in output buffer */ + ret = deflate(&strm, flush); /* no bad return value */ + *destlen_out += (alloced - *destlen_out) - 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_out); + *dest_out = NULL; + return TAPE_E_SAVE_ZLIB_COMPRESS; + } + + return TAPE_E_OK; + +} + +#include "tapenoise.h" + +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; + tape_interval_t elapsed; + + if (NULL == c->filename) { return TAPE_E_OK; } + + e = tape_wp_init_blank_tape_if_needed (our_filetype_bits_inout); + if (TAPE_E_OK != e) { return e; } + + /* TOHv4-rc2: add an origin chunk if there isn't one */ + /* FIXME: this code should be on uef.c */ + if ((c->filetype_bits & TAPE_FILETYPE_BITS_UEF) && (0 == ts->uef.num_chunks) && record_is_pressed) { + elapsed.start_1200ths = 0; + elapsed.pos_1200ths = 0; + 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 */ + 0, + 0, + 0, + &elapsed); + if (TAPE_E_OK != e) { return e; } + } + + 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; +} diff -uN b-em-0b6f1d2-vanilla/src/tape-io.h b-em-0b6f1d2-TOH/src/tape-io.h --- b-em-0b6f1d2-vanilla/src/tape-io.h 1970-01-01 01:00:00.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/tape-io.h 2025-10-27 11:37:55.689435233 +0000 @@ -0,0 +1,65 @@ +/* + * B-Em + * This file (C) 2025 'Diminished' + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef __INC_TAPE_IO +#define __INC_TAPE_IO + +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 "tape.h" + +int tape_load(tape_state_t * const ts, + tape_vars_t * const tv, + const ALLEGRO_PATH * const fn); + +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_zlib_compress (char * const source_c, + size_t const srclen, + bool const use_gzip_encoding, + char ** const dest_out, + size_t * const destlen_out); + +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); + + +#endif /* __INC_TAPE_IO */ diff -uN b-em-0b6f1d2-vanilla/src/tapenoise.c b-em-0b6f1d2-TOH/src/tapenoise.c --- b-em-0b6f1d2-vanilla/src/tapenoise.c 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/tapenoise.c 2025-10-27 09:38:49.562851999 +0000 @@ -1,5 +1,23 @@ -/*B-em v2.2 by Tom Walker - Tape noise (not very good)*/ +/* + * B-Em + * This file (C) 2025 'Diminished' + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "tape2.h" #include "b-em.h" #include @@ -7,36 +25,130 @@ #include "tapenoise.h" #include "sound.h" +#define TAPENOISE_VOLUME (0.20) +#define WAV_VOLUME (0.75) + +/* remember, BUFLEN_TN is the Allegro output buffer length; + AUDIO_RINGBUF_LEN is the internal ring buffer length. */ +#define BUFLEN_TN 1280 +#define FREQ_TN (FREQ_DD) +#define AUDIO_RINGBUF_LEN ((FREQ_TN * 50) / 100) /*(((FREQ_TN * 15) / 100)*/ + 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 +/* FIXME? We might need to allocate this on the heap instead; + 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 +158,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 +178,318 @@ al_destroy_voice(voice); } -static void send_buffer(void) -{ - int16_t *tapebuffer; - int c; +static uint8_t currently_fast = 0; + +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 */ +} - tpnoisep = 0; - if ((tapebuffer = al_get_audio_stream_fragment(stream))) { +/* 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 + +/* printf("%c", tone_1200th); fflush(stdout); */ + + 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; + } - for (c = 0; c < BUFLEN_DD; c++) { - tapebuffer[c] = tapenoise[c]; - tapenoise[c] = 0; + 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); } - al_set_audio_stream_fragment(stream, tapebuffer); - } else - log_debug("tapenoise: overrun"); + + } + } -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; + } + 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; } - add_high(); + 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); } + +#include "tapeseek.h" +#include "taperead.h" + +/* 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; + + /* get a copy of the live tape state */ + 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!"); + tape_state_finish(&clone, false); /* BUGFIX: TOHv4.3 */ + return TAPE_E_BUG; + } + + num_1200ths = 0; + + do { + e = tape_1200th_from_back_end (false, &clone, false, false, &tone, NULL); + 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, false); + return TAPE_E_SAVE_WAV_BODY_TOO_LARGE; + } + + e = TAPE_E_OK; + + tape_rewind_2(&clone, NULL, false, false); + + 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, false); + 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, false); + return TAPE_E_SAVE_FWRITE; + } + + memset(silence, 0, 2 * SLOW_SMPS); + + for (z=0; z < num_1200ths; z++) { + + int16_t *tone_packet; + + e = tape_1200th_from_back_end (false, &clone, false, false, &tone, NULL); //, NULL); + 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, false); + return TAPE_E_SAVE_FWRITE; + } + + } + + tape_state_finish(&clone, false); + fclose(f); + + return e; + +} diff -uN b-em-0b6f1d2-vanilla/src/tapenoise.h b-em-0b6f1d2-TOH/src/tapenoise.h --- b-em-0b6f1d2-vanilla/src/tapenoise.h 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/tapenoise.h 2025-10-26 06:10:32.443132710 +0000 @@ -1,11 +1,37 @@ +/* + * B-Em + * This file (C) 2025 'Diminished' + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + #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 -uN b-em-0b6f1d2-vanilla/src/taperead.c b-em-0b6f1d2-TOH/src/taperead.c --- b-em-0b6f1d2-vanilla/src/taperead.c 1970-01-01 01:00:00.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/taperead.c 2025-11-01 06:46:13.103527122 +0000 @@ -0,0 +1,538 @@ +/* + * B-Em + * This file (C) 2025 'Diminished' + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "taperead.h" + +/* 'elapsed' value comes from the initial scan data */ +int tape_update_playback_elapsed (tape_state_t * const ts, int32_t const elapsed_or_minus_one) { + /* for PLAYBACK, not RECORDING */ + if (elapsed_or_minus_one != -1) { + /* Don't use the predicted, new value. Stick with the old one. */ + ts->tallied_1200ths = elapsed_or_minus_one; + } else { + /* -1 means we don't have an exact value from the tape back end, so + * we update the tallied value */ + (ts->tallied_1200ths)++; + } + return TAPE_E_OK; +} + +#include "tapenoise.h" + + +int tape_fire_acia_rxc (tape_state_t * const ts, + tape_vars_t * const tv, + ACIA * const acia, + int32_t const acia_rx_divider, + bool const emit_tapenoise, + bool * const throw_eof_out) { + + int64_t ns_per_bit; + bool bit_ready; + int32_t time_1200ths_or_minus_one, total_1200ths; + char bit_in; + int e, m; + bool fire; /* TOHv4.3 */ + uint8_t strip; /* TOHv4.2 */ + + e = TAPE_E_OK; + + /* 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 need to convert + for 300 baud, and possibly also for 19200 baud if 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 = false; + + /* 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 this is not done. */ +#define HOLDOFF_1200THS 800 + strip = (ts->strip_silence_and_leader_holdoff_1200ths >= HOLDOFF_1200THS) ? tv->strip_silence_and_leader : 0; + + /* 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); + } + + time_1200ths_or_minus_one = -1; + total_1200ths = -1; + fire = false; + + /* TOHv4.3: support 19200 baud tape consumption as well as 1200 */ + if ( (832000 == ns_per_bit) || (52000 == ns_per_bit) ) { + + /* Note that when SAVEing using MOS, the RX side of things + * will (apropos of nothing) be switched into 19200 baud mode. + * So ns_per_bit will be 52000 while MOS is doing tape TX. + * 15 out of 16 ticks here can not generate an EOF. */ + + fire = (832000 == ns_per_bit) || (ts->hack_19200ths_consumed >= 15); + + if (52000 == ns_per_bit) { /* 19200 baud */ + ts->hack_19200ths_consumed++; + bit_in = ts->hack_19200ths_last_value; + if ('\0' == bit_in) { + bit_in = 'S'; /* hack */ + } + } + + if (fire) { /* every 1200th, for 1200 and 19200 baud modes */ + + ts->hack_19200ths_consumed = 0; + + /* 1201.92 baud (or sixteen ~19200ths) */ + e = tape_1200th_from_back_end (strip, + ts, + acia_rx_awaiting_start(acia), /* from shift reg state */ + ! tv->permit_phantoms, + &bit_in, + &time_1200ths_or_minus_one); /* TOHv4.3 */ + if (TAPE_E_EOF == e) { + *throw_eof_out = true; + e = TAPE_E_OK; + } + + if ( TAPE_E_OK != e ) { return e; } + + /* don't update elapsed time if tape has ended + * [OLD: bugfix: also exclude record mode, which uses + * tallied_1200ths for its own purpose] */ + if ( ! *throw_eof_out ) { + e = tape_update_playback_elapsed(ts, time_1200ths_or_minus_one); /* TOHv4.3 */ + } + if (TAPE_E_OK != e) { return e; } + + /* TOHv4-rc6: now gated by ula_motor; tackle Atic-Atac loader tapenoise after BREAK bug */ + if ( emit_tapenoise && ts->ula_motor) { + tapenoise_send_1200 (bit_in, &(ts->tapenoise_no_emsgs)); + } + ts->tones300_fill = 0; + + ts->hack_19200ths_last_value = bit_in; /* TOHv4.3 */ + + } + + ts->ula_prevailing_rx_bit_value = bit_in; + bit_ready = true; + + } else if (3328000 == ns_per_bit) { + + fire = true; /* actually fires four times, but who's counting */ + + /* 300.48 baud, so must consume four 1200ths; + note that this is always done regardless of whether + it makes a valid bit or not ... */ + + for (m=0; m < TAPE_TONES300_BUF_LEN; m++) { + + char tone, ta, tb; + + e = tape_1200th_from_back_end (tv->strip_silence_and_leader, + ts, + acia_rx_awaiting_start(acia), // from shift reg state + ! tv->permit_phantoms, + &tone, + &time_1200ths_or_minus_one); /* TOHv4.3 */ + if (TAPE_E_EOF == e) { + *throw_eof_out = true; + e = TAPE_E_OK; + } + if (TAPE_E_OK != e) { return e; } + + /* TOHv4.3: bugfix for *TAPE3 playing 1200 baud section tapenoise at 300 baud */ + if (emit_tapenoise && ts->ula_motor) { + tapenoise_send_1200 (tone, &(ts->tapenoise_no_emsgs)); + } + + /* don't update elapsed time if tape has ended */ + if ( ! *throw_eof_out ) { + tape_update_playback_elapsed(ts, time_1200ths_or_minus_one); /* TOHv4.3 */ + } + if (TAPE_E_OK != e) { return e; } + + ts->ula_prevailing_rx_bit_value = tone; + + ts->tones300[ts->tones300_fill] = tone; + + tb = ('L' == tone) ? '1' : tone; + ta = ('L' == ts->tones300[0]) ? '1' : ts->tones300[0]; + + /* does the new 1200th match tones[0] ? */ + if (ta != tb) { + /* logging removed -- too spammy for some tapes */ + /* + 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 = true; + bit_in = ts->tones300[0]; + ts->tones300_fill = 0; + } else { + (ts->tones300_fill)++; + } + + + } + + } 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; } + + /* TOHv4.3 */ + e = tape_get_duration_1200ths (ts, &total_1200ths); + if ((TAPE_E_OK == e) && bit_ready) { + e = acia_receive_bit_code (acia, bit_in); + } + +#ifdef BUILD_TAPE_TAPECTRL + if ((TAPE_E_OK == e) && tv->tapectrl_opened && fire) { /* TOHv4.3-a4: now gated by 'fire' */ + + /* TOHv4.3-a4: this code will execute either every 1/1200th or 1/300th of a second. + * It will go every 1/1200th even when RX 19200 baud is selected. */ + + /* will lock+unlock mutex */ + e = send_tone_to_tapectrl_maybe (&(tv->tapectrl), + ts->tallied_1200ths, + total_1200ths, + bit_in, + &(tv->since_last_tone_sent_to_gui)); + + /* will also lock+unlock mutex */ + if ((TAPE_E_OK == e) && (*throw_eof_out != tv->previous_eof_value) ) {/*&& ! tv->record_activated) {*/ +/* +printf("tapectrl_to_gui_msg_eof(%u): %s:%s(%d) [ns_per_bit = %"PRId64", ts->hack_19200ths_consumed=%d, t=%lf]\n", +*throw_eof_out, __FILE__, __func__, __LINE__, ns_per_bit, ts->hack_19200ths_consumed, al_get_time()); +*/ + tapectrl_to_gui_msg_eof(&(tv->tapectrl), true, true, *throw_eof_out); + } + + } + + if (fire) { + tv->previous_eof_value = *throw_eof_out; + } +#endif /* BUILD_TAPE_TAPECTRL */ + + if (TAPE_E_OK != e) { return e; } + return e; +} + + + +int tape_1200th_from_back_end ( bool const strip_silence_and_leader, /* turbo */ + tape_state_t * const ts, + bool const awaiting_start_bit, + bool const enable_phantom_block_protection, /* TOHv3.3 */ + char * const tone_1200th_out, /* must know about leader */ + int32_t * const updated_elapsed_1200ths_out_or_null) { /* usually returns -1, but sometimes a timestamp */ + + int e; + bool inhibit_start_bit_for_phantom_block_protection; /* TOHv3.2 */ + + *tone_1200th_out = '\0'; + inhibit_start_bit_for_phantom_block_protection = false; + if (updated_elapsed_1200ths_out_or_null != NULL) { + *updated_elapsed_1200ths_out_or_null = -1; + } + + /* 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, 0, tone_1200th_out, updated_elapsed_1200ths_out_or_null); + if (TAPE_E_EOF == e) { + *tone_1200th_out = 'S'; + ts->leader_skip_1200ths = 0; /* TOHv4.2 */ + return TAPE_E_EOF; + } + if (e != TAPE_E_OK) { return e; } + + /* TOHv4.2 */ + /* Leader skip complication: + * If 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. + * Go back to using 'L' after inserting 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'; + } + + /* 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 > TAPE_CRUDE_LEADER_DETECT_1200THS) + && ('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 = true; + } + } + /* 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; + +} + + +/* FIXME: t's data is not const because of TIBET's stupid priv nonsense */ +bool tape_peek_eof (tape_state_t * const t) { + + bool 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 = true; + + 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" + +int tape_read_1200th (tape_state_t * const ser, + bool const initial_scan_mode, + char * const value_out_or_null, + int32_t * const updated_elapsed_1200ths_out_or_null) { /* only valid when -1 not returned */ + + int e; + uef_meta_t meta[UEF_MAX_METADATA]; + uint32_t meta_len; + uint32_t m; + int32_t d; + int32_t hour, min, sec; /* TOHv4.3 */ + + hour = 0; /* TOHv4.3 */ + min = 0; + sec = 0; + + e = TAPE_E_OK; + + if (NULL != updated_elapsed_1200ths_out_or_null) { + *updated_elapsed_1200ths_out_or_null = -1; + } + + /* 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 useful metadata in the other file types. (TIBET allows timestamps + etc., but UEF's potential metadata is significantly richer.) */ + + /* 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_or_null, + meta, + initial_scan_mode, /* TOHv4.3 */ + &meta_len, + updated_elapsed_1200ths_out_or_null); /* only valid if -1 not returned */ + /* 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); + // UEF_METADATA_LIST_FINISH (meta, meta_len); /* TOHv4.3: now a macro */ + } + } else if (TAPE_FILETYPE_BITS_CSW & ser->filetype_bits) { + e = csw_read_1200th (&(ser->csw), + value_out_or_null, + initial_scan_mode, + updated_elapsed_1200ths_out_or_null); + } 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 */ + initial_scan_mode, /* TOHv4.3 */ + value_out_or_null); + } 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) { + if ( ! ser->tape_finished_no_emsgs ) { + if (initial_scan_mode) { /* TOHv4.3: */ + tape_get_duration_1200ths(ser, &d); + to_hours_minutes_seconds(d, &hour, &min, &sec); + log_info("tape: duration %d (%d:%02d:%02d)", d, hour, min, sec); + } else { + log_warn("tape: tape finished"); + } + } + ser->tape_finished_no_emsgs = 1; /* suppress further messages */ + } else { + ser->tape_finished_no_emsgs = 0; + } + + return e; + +} + +/* used to determine whether "catalogue tape" should be greyed out */ +#ifdef BUILD_TAPE_MENU_GREYOUT_CAT +bool tape_peek_for_data (tape_state_t * const ts) { + + uint8_t r; + uint8_t have_tibet_data; /* don't use bool in tibet.c/.h */ + int e; + + r = 0; + have_tibet_data = 0; + + /* if (ts->disabled_due_to_error) { */ + if (TAPE_E_OK != ts->prior_exception) { + 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 */ + + diff -uN b-em-0b6f1d2-vanilla/src/taperead.h b-em-0b6f1d2-TOH/src/taperead.h --- b-em-0b6f1d2-vanilla/src/taperead.h 1970-01-01 01:00:00.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/taperead.h 2025-10-28 09:14:25.433418806 +0000 @@ -0,0 +1,54 @@ +/* + * B-Em + * This file (C) 2025 'Diminished' + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef __INC_TAPEREAD +#define __INC_TAPEREAD + +#include "tape.h" + +bool tape_peek_for_data (tape_state_t * const ts); + +/* TOHv4 */ +int tape_fire_acia_rxc (tape_state_t * const ts, + tape_vars_t * const tv, + ACIA * const acia, + int32_t const acia_rx_divider, + bool const emit_tapenoise, + bool * const throw_eof_out); + +int tape_1200th_from_back_end ( bool const strip_silence_and_leader, /* turbo */ + tape_state_t * const ts, + bool const awaiting_start_bit, + bool const enable_phantom_block_protection, /* TOHv3.3 */ + char * const tone_1200th_out, /* must know about leader */ + int32_t * const updated_elapsed_1200ths_out_or_null); + +bool tape_peek_eof (tape_state_t * const t); + +int tape_read_1200th (tape_state_t * const ser, + bool const initial_scan_mode, + char * const value_out_or_null, + int32_t * const updated_elapsed_1200ths_out_or_null) ; + +/* 'elapsed' value comes from the initial scan data */ +int tape_update_playback_elapsed (tape_state_t * const ts, int32_t const elapsed_or_minus_one); + +int tape_wp_init_blank_tape_if_needed (uint8_t *filetype_bits_inout); + +#endif /* __INC_TAPEREAD */ diff -uN b-em-0b6f1d2-vanilla/src/tapeseek.c b-em-0b6f1d2-TOH/src/tapeseek.c --- b-em-0b6f1d2-vanilla/src/tapeseek.c 1970-01-01 01:00:00.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/tapeseek.c 2025-11-01 18:11:48.640535125 +0000 @@ -0,0 +1,628 @@ +/* + * B-Em + * This file (C) 2025 'Diminished' + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "tapeseek.h" +#include "tape-interval.h" + +/* TOHv4.3 */ +static int tape_seek_main (tape_state_t * const ts, + int32_t const time_1200ths_wanted, + int32_t * const got_1200ths_out, + bool * const throw_eof_out, + bool * const desynced_inout); + +static int change_current_piece (tape_state_t * const ts, + int64_t const piece_ix, + bool * const seek_desynced_out); + +static int get_num_pieces (tape_state_t * const ts, int64_t * const out); + +/* TOHv4.3 */ +static int get_time_interval_for_piece (tape_state_t * const ts, /* not const -- TIBET might modify it */ + int64_t const piece_ix, /* UEF chunk ix, TIBET span ix, CSW pulse ix */ + tape_interval_t * const interval_out); + +#include "tape.h" + +/* TOHv4.3 */ +/* danger: value will not be valid until initial tape scan */ +/* TODO: add a duration_is_valid flag to tape_state_t based on an EOF having been encountered at least once */ +int tape_get_duration_1200ths (tape_state_t * const ts, + int32_t * const duration_1200ths_out) { + int e; + e = TAPE_E_OK; + *duration_1200ths_out = -1; + if ( TAPE_E_OK != ts->prior_exception ) { + log_warn("tape: BUG: tape_get_duration_1200ths is called even though tape is disabled!"); + return TAPE_E_BUG; + } else if (ts->filetype_bits & TAPE_FILETYPE_BITS_UEF) { + e = uef_get_duration_1200ths(&(ts->uef), duration_1200ths_out); + } else if (ts->filetype_bits & TAPE_FILETYPE_BITS_TIBET) { + e = tibet_get_duration(&(ts->tibet), duration_1200ths_out); + } else if (ts->filetype_bits & TAPE_FILETYPE_BITS_CSW) { + csw_get_duration_1200ths(&(ts->csw), duration_1200ths_out); /* void return */ + } else { + } + return e; +} + + +/* TOHv4.3 */ +int tape_seek_absolute (tape_state_t * const ts, /* modified */ + int32_t time_1200ths_wanted, + int32_t const duration_1200ths, + int32_t * const time_1200ths_actual_out, + bool * const eof_out, + bool * const desynced_inout) { + + int e; + int32_t d; + + e = TAPE_E_OK; + d = 0; + + if (eof_out != NULL) { + *eof_out = false; + } + +/* printf("tape_seek_absolute: %d/%d\n", time_1200ths_wanted, duration_1200ths); */ + +#ifdef BUILD_TAPE_SANITY + if (time_1200ths_wanted < 0) { + log_warn("tape: seek: BUG: time_1200ths_wanted is duff (%d)", time_1200ths_wanted); + return TAPE_E_BUG; + } +#endif + d = duration_1200ths; + if (time_1200ths_wanted >= d) { + time_1200ths_wanted = d - TAPE_SEEK_CLAMP_BACK_OFF_1200THS; + } /* clamp */ + if (time_1200ths_wanted < 0) { + time_1200ths_wanted = 0; + } + if (time_1200ths_actual_out != NULL) { + *time_1200ths_actual_out = -1; + } + e = tape_seek_main (ts, + time_1200ths_wanted, + time_1200ths_actual_out, + eof_out, + desynced_inout); + return e; +} + +#include "taperead.h" + + +/* TOHv4.3 */ +static int tape_seek_main (tape_state_t * const ts, + int32_t const time_1200ths_wanted, + int32_t * const got_1200ths_out, + bool * const throw_eof_out, + bool * const desynced_inout) { + + int32_t d; + tape_interval_t interval, interval2; + int e; + int32_t t; + bool my_eof; + + /* UEF uses int32_t for chunk index; + * both CSW and TIBET use uint32_t for span or pulse index. + * + * We hence need a "position" variable which is capable of containing + * both uint32_t and int32_t. Use int64_t. */ + int64_t len, orig_len, piece; + + d=0; + my_eof = false; + if (throw_eof_out != NULL) { + *throw_eof_out = false; + } + + e = tape_get_duration_1200ths(ts, &d); + if (TAPE_E_OK != e) { return e; } + + if (time_1200ths_wanted > d) { + log_warn("tape: BUG: requested seek time (%d) exceeds duration (%d)!\n", time_1200ths_wanted, d); + return TAPE_E_BUG; + } + + orig_len = -1; + piece = -1; + + e = get_num_pieces(ts, &orig_len); /* if multiple filetypes, this will use UEF */ + if (TAPE_E_OK != e) { return e; } + + memset(&interval, 0, sizeof(tape_interval_t)); + + if (orig_len > 1) { + + /* Do we actually need to seek on all three back-ends simultaneously? + * We'll only ever read from one of them (UEF), and writes + * are always appended to the end of the stream. + * + */ + + /* coarse binary chop */ +/*printf("seek: coarse : ");*/ + for (len = orig_len / 4, piece = orig_len / 2; + len > 0; + len /= 2) { + + e = get_time_interval_for_piece (ts, piece, &interval); /* UEF, if multi */ + if (TAPE_E_OK != e) { return e; } + + if (interval.start_1200ths > time_1200ths_wanted) { + piece -= len; + } else { + piece += len; + } +/*printf("%"PRId64" ", piece);*/ + } +/*printf("\n");*/ + + /* fine (part one): advance pieces until we find the wanted time */ +/*printf(" fine, fwd: ");*/ + do { + e = get_time_interval_for_piece(ts, piece, &interval); /* UEF, if multi */ + if (TAPE_E_OK != e) { return e; } + interval2.start_1200ths = -1; + if ((piece+1) < orig_len) { + e = get_time_interval_for_piece(ts, piece+1, &interval2); /* UEF, if multi */ + // t1 = chks[start_chunk+1].elapsed.start_1200ths; + if (TAPE_E_OK != e) { return e; } + } + if ( (interval2.start_1200ths > interval.start_1200ths) + && (interval2.start_1200ths <= time_1200ths_wanted) ) { + /* + log_warn("tape: seek: fixup: want time %d, piece #%"PRId64"/%"PRId64" has %d, #%"PRId64"/%"PRId64" has %d", + time_1200ths_wanted, + piece, + orig_len, + interval.start_1200ths, + piece+1, + orig_len, + interval2.start_1200ths); */ + piece++; +/*printf("%"PRId64" ", piece);*/ + } else { + break; + } + } while (interval2.start_1200ths>0); +/*printf("\n");*/ + + /* fine (part two): rewind pieces until we find wanted time + * (rarely needed but issues near start of files) */ +/*printf(" fine, rev: ");*/ + e = get_time_interval_for_piece(ts, piece, &interval); /* UEF, if multi */ + if (TAPE_E_OK != e) { return e; } + while (interval.start_1200ths > time_1200ths_wanted) { + piece--; +/*printf("%"PRId64" ", piece); */ + e = get_time_interval_for_piece(ts, piece, &interval); /* UEF, if multi */ + if (TAPE_E_OK != e) { return e; } + } +/*printf("\n");*/ + + } + + /* fudge: CSW, TIBET won't let you seek back to t=0 for some reason. + * if requested time was 0, override the seek process and just force piece=0 */ + if ((0 == time_1200ths_wanted) || (piece < 0) /*TOHv4.3-a4*/) { + piece = 0; + } + + e = change_current_piece (ts, piece, desynced_inout); + if (TAPE_E_OK != e) { return e; } + + e = get_time_interval_for_piece (ts, piece, &interval); + if (TAPE_E_OK != e) { return e; } + + if (interval.start_1200ths < 0) { + log_warn("tape: seek: BUG: get_time_interval_for_piece (%"PRId64"/%"PRId64") gave bad interval start %d", + piece, orig_len, interval.start_1200ths); + return TAPE_E_BUG; + } + +/* printf("SEEK: wanted %d, hit %d\n", time_1200ths_wanted, interval.start_1200ths); */ + + for (t = interval.start_1200ths ; + (TAPE_E_OK == e) && (t < time_1200ths_wanted); + t++) { + + char tc; + + e = tape_1200th_from_back_end (false, ts, false, false, &tc, NULL); + + if (TAPE_E_OK != e) { + // this shouldn't happen + log_warn("tape: seek: time not found in piece #%"PRId64"/%"PRId64" (code %d); " + "sought %d 1200ths and read %d, interval (%d + %d), duration is %d\n", + piece, /* partition piece (chunk, span, pulse) */ + orig_len, /* total num. pieces (chunk, span, pulse) */ + e, + time_1200ths_wanted, + t, + interval.start_1200ths, + interval.pos_1200ths, + d); + } + } /* next 1200th in interval */ + + if (throw_eof_out != NULL) { + *throw_eof_out = my_eof; + } + if (got_1200ths_out != NULL) { + *got_1200ths_out = t; + } + + return e; + +} + +/* TOHv4.3 */ +/* beware: if multiple filetypes are available, only the UEF gets the seek. + * This hopefully won't matter, as we read exclusively from the UEF on playback, + * and always advance to the end of the tape when recording, so desync + * shouldn't matter. + */ +static int change_current_piece (tape_state_t * const ts, + int64_t const piece_ix, + bool * const desynced_inout) { + + int e; + uint8_t types, bits; + + e = TAPE_E_OK; + + if (piece_ix < 0) { /* TOHv4.3-a4 */ + log_warn("tapeseek: BUG: changing piece: piece_ix is illegal (%"PRId64")", piece_ix); + return TAPE_E_BUG; + } + + for (bits=ts->filetype_bits, types=0; bits; bits>>=1, types+=(1&bits)) + { } +/* printf("change_current_piece: %lld\n", piece_ix); */ + if (TAPE_FILETYPE_BITS_UEF & ts->filetype_bits) { + /* Reset bitsource for arbitrary new chunk. + Baud rate is reset to 1200. */ + e = uef_change_current_chunk(&(ts->uef), 0x7fffffff & piece_ix); + } else if (TAPE_FILETYPE_BITS_CSW & ts->filetype_bits) { + e = csw_change_current_pulse(&(ts->csw), 0xffffffff & piece_ix); + } else if (TAPE_FILETYPE_BITS_TIBET & ts->filetype_bits) { + e = tibet_change_current_span(&(ts->tibet), 0xffffffff & piece_ix); + } + /* + * What we will do is to rewind both CSW and TIBET so that + * they don't EOF if we happen to read from them. + */ + if (types>2) { /* do we have more than just WAV + UEF? */ + if (TAPE_FILETYPE_BITS_CSW & ts->filetype_bits) { + csw_rewind(&(ts->csw)); + if ( (desynced_inout != NULL) && ! *desynced_inout ) { + log_warn("tape: change_current_piece: WARNING? seek desyncs across parallel copies"); + } + } + if (TAPE_FILETYPE_BITS_TIBET & ts->filetype_bits) { + tibet_rewind(&(ts->tibet)); + if ( (desynced_inout != NULL) && ! *desynced_inout ) { + log_warn("tape: change_current_piece: WARNING? seek desyncs across parallel copies"); + } + } + if (desynced_inout != NULL) { + *desynced_inout = true; /* prevent further messages */ + } + } + + return e; +} + +/* TOHv4.3 */ +static int get_num_pieces (tape_state_t * const ts, int64_t * const out) { + uint32_t n; + int e; + if (TAPE_FILETYPE_BITS_UEF & ts->filetype_bits) { + *out = ts->uef.num_chunks; /* int32_t -> int64_t */ + } else if (TAPE_FILETYPE_BITS_CSW & ts->filetype_bits) { + *out = ts->csw.pulses_fill; /* uint32_t -> int64_t */ + } else if (TAPE_FILETYPE_BITS_TIBET & ts->filetype_bits) { + n=0; + e = tibet_get_num_spans(&(ts->tibet), &n); + if (TAPE_E_OK != e) { return e; } + *out = n; /* uint32_t -> int64_t */ + } + return TAPE_E_OK; +} + +/* TOHv4.3 */ +static int get_time_interval_for_piece (tape_state_t * const ts, /* not const -- TIBET might modify it */ + int64_t const piece_ix, /* UEF chunk ix, TIBET span ix, CSW pulse ix */ + tape_interval_t * const interval_out) { + int e; + interval_out->start_1200ths = 0; + interval_out->pos_1200ths = 0; + if (piece_ix < 0) { /* TOHv4.3 */ + log_warn("tape: seek: BUG: bad piece_ix %"PRId64"\n", piece_ix); + return TAPE_E_BUG; + } + if (TAPE_FILETYPE_BITS_UEF & ts->filetype_bits) { + *interval_out = ts->uef.chunks[0x7fffffff & piece_ix].elapsed; + } else if ((TAPE_FILETYPE_BITS_CSW & ts->filetype_bits) && (ts->csw.pulses != NULL)) { + *interval_out = ts->csw.pulses[0xffffffff & piece_ix].timespan; + } else if (TAPE_FILETYPE_BITS_TIBET & ts->filetype_bits) { + e = tibet_get_time_interval_for_span(&(ts->tibet), 0xffffffff & piece_ix, interval_out); + if (TAPE_E_OK != e) { return e; } + } + return TAPE_E_OK; +} + + +/* TOHv4.3 */ +/* FIXME: why do we have both read_ffwd_to_end() and tape_ffwd_to_end()? */ +/* +int tape_ffwd_to_end (tape_state_t * const ts) { + int e; + if (TAPE_FILETYPE_BITS_UEF & ts->filetype_bits) { + e = uef_ffwd_to_end(&(ts->uef)); + if (TAPE_E_OK != e) { return e; } + } + if (TAPE_FILETYPE_BITS_CSW & ts->filetype_bits) { + e = csw_ffwd_to_end(&(ts->csw)); + if (TAPE_E_OK != e) { return e; } + } + if (TAPE_FILETYPE_BITS_TIBET & ts->filetype_bits) { + e = tibet_ffwd_to_end(&(ts->tibet)); + if (TAPE_E_OK != e) { return e; } + } + return TAPE_E_OK; +} +*/ + +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; +} + + +int tapeseek_run_initial_scan (tape_state_t * const ts, + tape_interval_list_t * const iv_list_inout) { + + int e; + char tone; + tape_interval_t iv; + bool at_eof; +#ifdef BUILD_TAPE_TAPECTRL + int64_t np; +#endif + int32_t leader_detect_1200ths; + + memset(&iv, 0, sizeof(tape_interval_t)); + + if (NULL != iv_list_inout) { + tape_interval_list_finish(iv_list_inout); + } + + ts->csw.cur_silence_1200th = 0; + ts->csw.num_silent_1200ths = 0; + + tone = 'S'; + e = TAPE_E_OK; + at_eof = false; + + leader_detect_1200ths = 0; + + /* this process builds two (2) different interval lists; + * - one goes on CSW pulse, UEF chunk, or TIBET span; + * - the other goes on tape_vars (and is returned in the arguments of this function) */ + + do { + tone = '\0'; + e = tape_read_1200th (ts, + true, /* initial-scan mode! build time map on ts */ + // &initial_scan_csw_pulse_leftovers_smps, + &tone, + NULL); + + if ('L' == tone) { + if (leader_detect_1200ths > TAPE_CRUDE_LEADER_DETECT_1200THS) { + tone = 'L'; + } else { + leader_detect_1200ths++; + } + } else { + leader_detect_1200ths = 0; + } + + if ( TAPE_E_EOF == e ) { + at_eof = true; + e = TAPE_E_OK; + } + +#ifdef BUILD_TAPE_TAPECTRL + if ( ('\0' != tone) && ( TAPE_E_OK == e ) && ! at_eof && (NULL != iv_list_inout) ) { + e = tape_interval_list_send_1200th (iv_list_inout, tone, true); /* free on error */ + } +#endif + + } while ( ( TAPE_E_OK == e ) && ! at_eof ); + +#ifdef BUILD_TAPE_TAPECTRL + /* FINALO PIECE */ + if ((TAPE_E_OK == e) && (NULL != iv_list_inout)) { + e = tape_interval_list_append (iv_list_inout, + &(iv_list_inout->decstate.wip), + true); + memset(&(iv_list_inout->decstate.wip), 0, sizeof(tape_interval_t)); + } + + if (TAPE_E_OK != e) { return e; } + + if ((iv_list_inout != NULL) && (iv_list_inout->fill > 0)) { + + tape_interval_t foo, *bar; + int32_t t_foo, t_bar, delta; + + memset(&foo, 0, sizeof(foo)); + + np=0; + get_num_pieces(ts, &np); + if (np>0) { + get_time_interval_for_piece(ts, np-1, &foo); + } + + bar = iv_list_inout->list + iv_list_inout->fill - 1; + t_foo = foo.start_1200ths + foo.pos_1200ths; + t_bar = bar->start_1200ths + bar->pos_1200ths; + delta = t_bar - t_foo; + + /* + printf("durations; per stripes: %d; per pieces: %d; delta %d (%f s)\n", + t_bar, + t_foo, + delta, + delta / TAPE_1200_HZ); //1201.92307692); + */ + + if (delta < 0) { delta *= -1; } + + /* Allow an off-by-one between these two ways of measuring duration. + * At 300 baud, this off-by-one becomes an off-by-four. + * Ignore discrepancies of 4 x 1200ths or shorter. Worse errors + * are flagged as a bug. */ + if ((delta>0) && (delta<5)) { + log_warn("tape: initial scan: WARNING: minor duration mismatch (%d)", delta); + } else if (delta>0) { + log_warn("tape: initial scan: BUG: severe duration mismatch (%d)", delta); + return TAPE_E_BUG; + } + + } +#endif /* BUILD_TAPE_TAPECTRL */ + + ts->csw.cur_pulse = 0; + + return e; + +} + + + +int tape_rewind_2 (tape_state_t * const t, + tape_ctrl_window_t * const tcw, + bool const record_activated, + bool const tapectrl_opened) { + int e; + e = TAPE_E_OK; + if (record_activated) { return e; } /* TOHv4.3 */ + 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 */ + t->tallied_1200ths = 0; /* TOHv4.3 */ +#ifdef BUILD_TAPE_TAPECTRL + /* will lock mutex */ + if (tapectrl_opened && (tcw != NULL)) { + tapectrl_to_gui_msg_error (tcw, true, false, TAPE_E_OK); + tapectrl_set_gui_rapid_value_time (tcw, false, true, 0); //, tcw->duration_1200ths); + } +#endif + return e; +} + + +/* -tapeseek command in debugger + * (runs in main thread) */ +int tape_seek_for_debug (tape_state_t * const ts, + tape_vars_t * const tv, + double const seek_fraction) { + + int32_t duration; + int e; +#ifdef BUILD_TAPE_TAPECTRL + tape_ctrl_msg_from_gui_t from_gui; + tape_ctrl_msg_to_gui_t to_gui; + tape_ctrl_window_t *tcw; +#endif + + e = TAPE_E_OK; + + tape_get_duration_1200ths(ts, &duration); + ts->tallied_1200ths = (duration * seek_fraction); + +#ifdef BUILD_TAPE_TAPECTRL + if (tv->tapectrl_opened) { + tcw = &(tv->tapectrl); + TAPECTRL_LOCK_MUTEX(tcw->mutex); + if (NULL != tcw->display) { + /* If building w/tapectrl, send seek messages both ways; + * to the main thread, to the GUI. */ + memset(&from_gui, 0, sizeof(tape_ctrl_msg_from_gui_t)); + from_gui.type = TAPECTRL_FROM_GUI_SEEK; + from_gui.data.seek.fraction = seek_fraction; + from_gui.ready = true; + memset(&to_gui, 0, sizeof(tape_ctrl_msg_to_gui_t)); + tapectrl_queue_from_gui_msg(tcw, &from_gui); + tapectrl_set_gui_rapid_value_time (tcw, false, false, ts->tallied_1200ths); //, duration); + /* fall through! */ + } else { + /* if we don't have tapectrl, don't use the msg systems; just call this directly. */ + e = tape_seek_absolute (ts, + duration * seek_fraction, + duration, + NULL, + NULL, + NULL); + } + /* is this code actually needed? */ + TAPECTRL_UNLOCK_MUTEX(tcw->mutex); + /* FUNCTION RETURNS HERE */ + return e; + } +#endif + + /* Tapectrl not compiled in, or tapectrl window is closed. + * Mutex not locked. + * If we don't have tapectrl, don't use the msg queues; just call this directly. */ + e = tape_seek_absolute (ts, + duration * seek_fraction, + duration, + NULL, + NULL, + NULL); + return e; + +} diff -uN b-em-0b6f1d2-vanilla/src/tapeseek.h b-em-0b6f1d2-TOH/src/tapeseek.h --- b-em-0b6f1d2-vanilla/src/tapeseek.h 1970-01-01 01:00:00.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/tapeseek.h 2025-11-01 18:12:01.813423681 +0000 @@ -0,0 +1,57 @@ +/* + * B-Em + * This file (C) 2025 'Diminished' + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef __INC_TAPESEEK_H +#define __INC_TAPESEEK_H + +#include "tape2.h" +#include "tapectrl.h" + +typedef struct tape_state_s tape_state_t; + +int tape_seek_absolute (tape_state_t * const ts, /* modified */ + int32_t time_1200ths_wanted, + int32_t const duration_1200ths, + int32_t * const time_1200ths_actual_out, + bool * const eof_out, + bool * const desynced_inout); + +/* TOHv4.3: tape control */ +int tape_get_duration_1200ths (tape_state_t * const ts, + int32_t * const duration_1200ths_out); /* invalid until initial scan */ + +int tape_rewind_2 (tape_state_t * const t, + tape_ctrl_window_t * const tcw, + bool const record_activated, + bool const tapectrl_opened); + +/* -tapeseek command in debugger + * (runs in main thread) */ +int tape_seek_for_debug (tape_state_t * const ts, + tape_vars_t * const tv, + double const seek_fraction); + +/*int tape_ffwd_to_end (tape_state_t * const ts);*/ /* TOHv4.3 */ + +int tapeseek_run_initial_scan (tape_state_t * const ts, + tape_interval_list_t * const iv_list_inout); + +int read_ffwd_to_end (tape_state_t *t); + +#endif /* __INC_TAPESEEK_H */ diff -uN b-em-0b6f1d2-vanilla/src/tapewrite.c b-em-0b6f1d2-TOH/src/tapewrite.c --- b-em-0b6f1d2-vanilla/src/tapewrite.c 1970-01-01 01:00:00.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/tapewrite.c 2025-11-01 14:49:42.907560963 +0000 @@ -0,0 +1,1471 @@ +/* + * B-Em + * This file (C) 2025 'Diminished' + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "tapewrite.h" +#include "acia.h" +#include "gui-allegro.h" + +#define SERIAL_STATE_AWAITING_START 0 +#define SERIAL_STATE_BITS 1 +#define SERIAL_STATE_PARITY 2 +#define SERIAL_STATE_AWAITING_STOP 3 + +static int check_pending_tibet_span_type (tape_state_t *t, uint8_t type); + +static void framing_to_string (char fs[4], const serial_framing_t * const f); + +static int tape_write_end_data (tape_state_t *t); + +static int tape_write_start_data (tape_state_t * const t, + bool const always_117, + const serial_framing_t * const f); + +static int tape_write_silence_2 (tape_state_t * const t, + double len_s, + bool const silence112, + int32_t const silence_start_elapsed_1200ths); + +static int tape_write_data_1200th (tape_state_t * const ts, + tape_ctrl_window_t * const tcw, + tape_interval_list_t * const iv_list_inout, + const serial_framing_t * const f, + bool const tapenoise_active, + char const value, + uint32_t * const since_last_tone_sent_to_gui_inout, + bool const tapectrl_opened); + +static int tape_write_leader (tape_state_t * const t, uint32_t num_1200ths, bool const populate_elapsed); + +static int +wp_bitclk_start_data_if_needed (tape_state_t * const ts, + /* need to provide baud rate + framing, + f or TIBET hints, UEF &114 header, etc: */ + const serial_framing_t * const f, + bool const always_117, + bool const record_is_pressed); + +static int wp_bitclk_accumulate_leader (tape_state_t * const ts, + tape_ctrl_window_t * const tcw, + tape_interval_list_t * const iv_list_inout, + bool const tapenoise_active, + int64_t const ns_per_bit, + bool const record_is_pressed, + bool * const reset_shift_reg_out, + uint32_t * const since_last_tone_sent_to_gui_inout, + bool const tapectrl_opened); + +static int wp_end_data_section_if_ongoing (tape_state_t * const t, + bool const record_is_pressed, + bool * const reset_shift_register_out); + +static int tape_write_prelude (tape_state_t * const ts, + bool const record_is_pressed, + bool const no_origin_chunk_on_append); + +static int wp_flush_accumulated_leader_maybe (tape_state_t * const t, + bool const record_is_pressed, + bool const populate_elapsed); + +static int wp_bitclk_output_data (tape_state_t *ts, + tape_ctrl_window_t *tcw, /* TOHv4.3 */ + tape_interval_list_t * const iv_list_inout, + ACIA *acia, + const serial_framing_t * const f, + uint32_t *since_last_tone_sent_to_gui_inout, /* limit to_gui message rate */ + uint8_t tapenoise_active, + int64_t ns_per_bit, + uint8_t record_is_pressed, + uint8_t always_117, + bool const tapectrl_opened); + +static int wp_bitclk_handle_silence (tape_state_t * const ts, + tape_ctrl_window_t * const tcw, /* TOHv4.3 */ + bool const silent, + bool const tapenoise_active, + bool const record_is_pressed, + int64_t const ns_per_bit, + bool const silence112, + bool const tapectrl_opened, + tape_interval_list_t * const iv_list_inout, + bool * const reset_shift_register_out, + uint32_t * const since_last_tone_sent_to_gui_inout); + +// + +int tape_flush_pending_piece (tape_state_t * const ts, + ACIA * const acia_or_null, + bool const silence112, + bool const populate_elapsed) { + + bool reset_shift_reg; + int e; + int32_t duration; + + e = TAPE_E_OK; + + /* if (ts->disabled_due_to_error) { return TAPE_E_OK; } */ /* TOHv4.3-a3 */ + if (TAPE_E_OK != ts->prior_exception) { return TAPE_E_OK; } /* TOHv4.3-a3 */ + + e = tape_get_duration_1200ths (ts, &duration); + if (TAPE_E_OK != e) { return e; } + +/* printf("tape_flush_pending_piece: leader_ns = %"PRId64", silence_ns = %"PRId64"\n", + ts->w_accumulate_leader_ns, ts->w_accumulate_silence_ns); +*/ + + if (ts->w_accumulate_leader_ns > 0) { + e = wp_flush_accumulated_leader_maybe (ts, true, populate_elapsed); + 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, + duration); /* TOHv4.3 */ + ts->w_accumulate_silence_ns = 0; + /* TOHv4.3: bugfix? kludge? force EOF + * (REC is/was pressed, so forcing EOF is correct behaviour here?) */ + ts->uef.cur_chunk = ts->uef.num_chunks; + } else { + reset_shift_reg = 0; + e = wp_end_data_section_if_ongoing (ts, true, &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");*/ + } + } + + if (ts->filetype_bits & TAPE_FILETYPE_BITS_UEF) { + uef_clear_chunk(&(ts->w_uef_tmpchunk)); + } + + return e; + +} + +#include "taperead.h" + +/* TOHv4: Want to be called at the bit rate. + For each tick, either have leader, silence, or data. + Leader and silence packets will have to be expanded out into + the appropriate number of 1200ths. */ + +/* NOTE: motor is guaranteed to be running */ +int tape_write_bitclk ( tape_state_t * const ts, + tape_ctrl_window_t * const tcw, /* TOHv4.3 */ + tape_interval_list_t * const iv_list_inout, /* TOHv4.3 */ + ACIA * const acia, + char const bit, + int64_t const ns_per_bit, + bool const tapenoise_write_enabled, + bool const record_is_pressed, + bool const always_117, + bool const silence112, + bool const no_origin_chunk_on_append, + bool const tapectrl_opened, /* TOHv4.3 */ + uint32_t * const since_last_tone_sent_to_gui_inout) { + + bool have_leader; + int e; + serial_framing_t f; + bool reset_shift_reg; + bool had_data = 0; + bool silent; + + 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); + silent = ('S' == bit); + + /* 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, + tcw, /* TOHv4.3 */ + silent, + tapenoise_write_enabled, + record_is_pressed, + ns_per_bit, + silence112, + tapectrl_opened, /* TOHv4.3 */ + iv_list_inout, + &reset_shift_reg, + since_last_tone_sent_to_gui_inout); + 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, + tcw, /* TOHv4.3 */ + iv_list_inout, + record_is_pressed && tapenoise_write_enabled, + ns_per_bit, + record_is_pressed, + &reset_shift_reg, + since_last_tone_sent_to_gui_inout, + tapectrl_opened); + 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, true); + if (TAPE_E_OK != e) { return e; } + } + + /* Silence and leader 1200ths & tapenoise are done; + data still remains: */ + if ( ( ! silent ) && ( ! have_leader ) ) { + + 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, + tcw, + iv_list_inout, + acia, + &f, + since_last_tone_sent_to_gui_inout, /* limit to_gui message rate */ + record_is_pressed && tapenoise_write_enabled, + ns_per_bit, + record_is_pressed, + always_117, + tapectrl_opened); + 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 */ +/* This situation (an incomplete TX frame) is quite easy to provoke + * if you issue fast CRs to the "RECORD then RETURN" prompt. The + * dummy byte will be interrupted and this function will be called + * to clean up the mess. + * + * Another way would be to start writing some data blocks + * and then spam the REC button. */ + +int tape_uef_flush_incomplete_frame (uef_state_t * const uef_inout, + int32_t const tallied_1200ths, + uint8_t * const serial_phase_inout, + uint8_t * const serial_frame_inout, + uef_chunk_t * const chunk_inout) { /* in practice this is ts->w_uef_tmpchunk */ + + 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; + } + } + + /* Bafflingly, the needed value here is 16 to make the + * duration per the stripes equal to the duration per pieces + * (UEF chunks in this case). I don't know why it's 16. + * This code never runs for 300 baud saving. */ + + /* chunk_inout->elapsed.pos_1200ths += 16; */ + + e = uef_append_byte_to_chunk (chunk_inout, *serial_frame_inout); + if (TAPE_E_OK == e) { + chunk_inout->elapsed.pos_1200ths = tallied_1200ths - chunk_inout->elapsed.start_1200ths; + e = uef_store_chunk(uef_inout, + chunk_inout->data, + chunk_inout->type, + chunk_inout->len, + 0, /* TOHv4.2: dummy offset */ + 0, + 0, + 0, /* although this is a data chunk, we never write UEF &114, so this field is always 0 */ + &(chunk_inout->elapsed)); + } + + uef_clear_chunk(chunk_inout); + + *serial_phase_inout = 0; + *serial_frame_inout = 0; + + return e; + +} + +static int wp_flush_accumulated_leader_maybe (tape_state_t * const ts, + bool const record_is_pressed, + bool const populate_elapsed) { + int e; + /* handle the end of a leader section */ + if ( ts->w_accumulate_leader_ns > 0 ) { + if (record_is_pressed) { +/* printf("flushing %"PRId64" ms accumulated leader\n", ts->w_accumulate_leader_ns / 1000000); */ + e = tape_write_leader (ts, ts->w_accumulate_leader_ns / TAPE_1200TH_IN_NS_INT, populate_elapsed); + if (TAPE_E_OK != e) { return e; } + } + ts->w_accumulate_leader_ns = 0; + } + return TAPE_E_OK; +} + +static int +wp_bitclk_start_data_if_needed (tape_state_t * const ts, + /* need to provide baud rate + framing, + f or TIBET hints, UEF &114 header, etc: */ + const serial_framing_t * const f, + bool const always_117, + bool const record_is_pressed) { + + int e; + + e = TAPE_E_OK; + +#ifdef BUILD_TAPE_SANITY + /* TOHv4.3-a4: UEF: w_must_end_data implies there is some Beeb data + * on the tmpchunk. If the tmpchunk type is 0 (the origin chunk), + * this is clearly not the case and something has gone wrong. */ + if ( (ts->filetype_bits & TAPE_FILETYPE_BITS_UEF) + && (0 == ts->w_uef_tmpchunk.type) + && ts->w_must_end_data) { + /* shouldn't happen */ + log_warn("tapewrite: BUG: UEF tmpchunk type 0 (origin) but ts->w_must_end_data is set!"); + return TAPE_E_BUG; + } + + if ( ! ts->w_must_end_data ) { + /* this means that nothing should contain any data -- + * enforce this */ + if ( (ts->filetype_bits & TAPE_FILETYPE_BITS_UEF) && (ts->w_uef_tmpchunk.type != 0) ) { + log_warn("tapewrite: BUG: w_must_end_data is 0 but w_uef_tmpchunk.type is nonzero (&%x)", + ts->w_uef_tmpchunk.type); + return TAPE_E_BUG; + } + if (ts->filetype_bits & TAPE_FILETYPE_BITS_CSW) { + /* nothing to do? */ + } + if ( (ts->filetype_bits & TAPE_FILETYPE_BITS_TIBET) && (ts->tibet_data_pending.type != TIBET_SPAN_INVALID) ) { + log_warn("tapewrite: BUG: w_must_end_data is 0 but tibet_data_pending.type is nonzero (&%x)", + ts->tibet_data_pending.type); + return TAPE_E_BUG; + } + } +#endif + + /* 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. */ +/*printf("wp_bitclk_start_data_if_needed: w_must_end_data = %u, tmpchunk type = &%x\n", ts->w_must_end_data, ts->w_uef_tmpchunk.type);*/ + if ( record_is_pressed && ! ts->w_must_end_data ) { + ts->w_must_end_data = 1; + e = tape_write_start_data (ts, always_117, f); + if (TAPE_E_OK != e) { return e; } + } + + return e; + +} + + + +/* calls gated by ( ( ! silent ) && ( ! have_leader ) ) */ +static int wp_bitclk_output_data (tape_state_t *ts, + tape_ctrl_window_t *tcw, /* TOHv4.3 */ + tape_interval_list_t * const iv_list_inout, + ACIA *acia, + const serial_framing_t * const f, + uint32_t *since_last_tone_sent_to_gui_inout, /* limit to_gui message rate */ + uint8_t tapenoise_active, + int64_t ns_per_bit, + uint8_t record_is_pressed, + uint8_t always_117, + bool const tapectrl_opened) { + + 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 REC mode is OFF -- in order to get tape + noise from the SAVE operation, REC mode needs to be + enabled). */ + + /* TOHv3.3: added extra call to read_ffwd_to_end() to stop receive overflows */ + /*e = read_ffwd_to_end(ts);*/ + + /* TOHv4.3-a3: cancel any EOF */ + /*if (TAPE_E_EOF == e) { e = TAPE_E_OK; }*/ + if (TAPE_E_OK != e) { return e; } + + /* 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_data_1200th (ts, + tcw, + iv_list_inout, + f, + tapenoise_active, + '0', + since_last_tone_sent_to_gui_inout, + tapectrl_opened); + } + } + + /* data bit */ + for (i=0; + record_is_pressed && (TAPE_E_OK == e) && (i < num_1200ths); + i++) { + e = tape_write_data_1200th (ts, + tcw, + iv_list_inout, + f, + tapenoise_active, + (acia->tx_shift_reg_value & 1) ? '1' : '0', + since_last_tone_sent_to_gui_inout, + tapectrl_opened); + } + + 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_data_1200th (ts, + tcw, + iv_list_inout, + f, + tapenoise_active, + p, + since_last_tone_sent_to_gui_inout, + tapectrl_opened); + } + } 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_data_1200th (ts, + tcw, + iv_list_inout, + f, + tapenoise_active, + '1', + since_last_tone_sent_to_gui_inout, + tapectrl_opened); + } + + } + /* else { + log_warn("tape: BUG: tx_shift_reg_shift insane value %u", acia->tx_shift_reg_shift); + return TAPE_E_BUG; + }*/ + + return e; + +} + + +static int tape_write_prelude (tape_state_t * const ts, + bool const record_is_pressed, + bool const no_origin_chunk_on_append) { + + int e; + tape_interval_t elapsed; + int32_t duration; + + e = TAPE_E_OK; + + if (record_is_pressed) { + e = read_ffwd_to_end(ts); + if (TAPE_E_OK != e) { return e; } + } + + /* 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 ) { + + e = uef_get_duration_1200ths(&(ts->uef), &duration); + if (TAPE_E_OK != e) { return e; } + + if (-1 == duration) { duration = 0; } /* TOHv4.3-a4 */ + + /* TOHv4.3 */ + elapsed.pos_1200ths = 0; /* use the start time from tmpchunk, but cancel any tone */ + elapsed.start_1200ths = duration; +/*printf("tape_write_prelude: UEF: origin: timestamp (%d + %d)\n", elapsed.start_1200ths, elapsed.pos_1200ths);*/ + + 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 */ + 0, /* TOHv4.3: no cycle information ... */ + 0, + 0, + &elapsed); /* TOHv4.3 */ + 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 = tape_wp_init_blank_tape_if_needed (&(ts->filetype_bits)); /*, record_is_pressed);*/ + if (TAPE_E_OK != e) { return e; } + } + + return e; + +} + +#include "tapenoise.h" +#include "tapeseek.h" + +/* NOT used for silence or leader! + * You probably wanted tape_write_bitclk(). + * + * REC is guaranteed to be pressed ... */ +static int tape_write_data_1200th (tape_state_t * const ts, + tape_ctrl_window_t * const tcw, + tape_interval_list_t * const iv_list_inout, + const serial_framing_t * const f, + bool const tapenoise_active, + char const value, + uint32_t * const since_last_tone_sent_to_gui_inout, + bool const tapectrl_opened) { + + int e; + uint8_t v; + int32_t duration; + + e = tape_get_duration_1200ths(ts, &duration); + if (TAPE_E_OK != e) { return e; } + + /* 1. TAPE NOISE */ + if (tapenoise_active) { + tapenoise_send_1200(value, &(ts->tapenoise_no_emsgs)); + } + +#ifdef BUILD_TAPE_TAPECTRL + /* note: this will lock+unlock mutex */ + if (tapectrl_opened) { + e = send_tone_to_tapectrl_maybe (tcw, + ts->tallied_1200ths, + ts->tallied_1200ths, + value, + since_last_tone_sent_to_gui_inout); + } + + /* TOHv4.3 */ + /* add this to the stripes ... */ + if (iv_list_inout != NULL) { + e = tape_interval_list_send_1200th (iv_list_inout, value, false); /* false = no free on error */ + if (TAPE_E_OK != e) { return e; } + } +#endif + + v = (value=='0') ? 0 : 1; + + do { /* try { */ + + double pulse; + uint8_t i, num_pulses; + bool have_uef_bit; + uint8_t bit_value; + uint8_t total_num_bits; + uint16_t mask; + uint8_t len; + const char *payload117; + tape_interval_t elapsed, interval; + + /* 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 = false; + + /* 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; + 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 = true; + 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 tmpchunk, so we can ram a chunk 117 + directly into the UEF stream with no ill effects. Ram */ + /*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; } + /* dovetail against previous chunk */ + elapsed.start_1200ths = duration; + elapsed.pos_1200ths = 0; + e = uef_store_chunk (&(ts->uef), + (uint8_t *) payload117, + 0x117, + 2, + 0, /* TOHv4.2: dummy offset */ + 0, + 0, + 0, /* TOHv4.3: chunk &114 never used for writing, so 0 here */ + &elapsed); /* TOHv4.3 */ + if (TAPE_E_OK != e) { break; } + /* update both tmpchunk and display tallied time with duration value (i.e. last saved UEF chunk) */ + ts->w_uef_tmpchunk.elapsed.start_1200ths = elapsed.start_1200ths; + ts->tallied_1200ths = elapsed.start_1200ths; + } 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; } + + /* memset(&interval, 0, sizeof(tape_interval_t)); */ + interval.pos_1200ths = 0; + interval.start_1200ths = duration; + num_pulses = v?4:2; + + /* At 1200 Hz, we will make 2 pulses of durations 0 and 1 1200th. + * At 2400 Hz, we will make 4 pulses with durations 0, 0, 0 and 1 1200th. */ + for (i=0; (TAPE_E_OK == e) && (i < num_pulses); i++) { + if (i==(num_pulses-1)) { /* TOHv4.3 */ + interval.pos_1200ths = 1; + } + e = csw_append_pulse_fractional_length (&(ts->csw), pulse, &interval); + } + + if (TAPE_E_OK != e) { break; } + + } + + } while (0); /* catch */ + + /* finally */ + + if (TAPE_E_OK != e) { + ts->w_uef_tmpchunk.len = 0; + } else { + (ts->tallied_1200ths)++; /* TOHv4.3 */ + } + + 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; + tape_interval_t *elapsed; + + e = TAPE_E_OK; + + elapsed = NULL; + + if (t->filetype_bits & TAPE_FILETYPE_BITS_TIBET) { + + elapsed = &(t->tibet_data_pending.timespan); + + if (-1 == elapsed->start_1200ths) { elapsed->start_1200ths = 0; } /* TOHv4.3-a4 */ + + // tape_get_duration_1200ths(t, &(elapsed->start_1200ths)); + elapsed->pos_1200ths = t->tallied_1200ths - elapsed->start_1200ths; +/* +printf("tape_write_end_data: TIBET: setting timespan=(%d + %d)=%d\n", + elapsed->start_1200ths, elapsed->pos_1200ths, elapsed->start_1200ths+elapsed->pos_1200ths); +*/ + /* 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. */ + /* TOHv4.3-a3: There is a test for this now. */ + 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"); + memset(&(t->tibet_data_pending), 0, sizeof(tibet_span_t)); + return TAPE_E_OK; /* TOHv4.3-a3: trap this error */ + } + if (TAPE_E_OK != e) { return e; } + memset(&(t->tibet_data_pending), 0, sizeof(tibet_span_t)); + } + +/*printf("len=%u, tallied %u, elapsed start %d\n", t->w_uef_tmpchunk.len, t->tallied_1200ths, elapsed->start_1200ths);*/ + + /* this is where the tmpchunk actually gets stored, + so we need accurate elapsed data now */ + if ( (TAPE_E_OK == e) + && (t->filetype_bits & TAPE_FILETYPE_BITS_UEF) + && (t->w_uef_tmpchunk.len > 0) /* TOHv4.3-a3 */ ) { + + elapsed = &(t->w_uef_tmpchunk.elapsed); + /* UEF: append tmpchunk to actual UEF struct */ + elapsed->pos_1200ths = t->tallied_1200ths - elapsed->start_1200ths; + +/*printf("\n*********** tallied %d, start %d\n", t->tallied_1200ths, elapsed->start_1200ths);*/ + 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 */ + 0, + 0, + 0, /* TOHv4.3: type &114 never written so cycs_114 is 0 */ + elapsed); /* TOHv4.3 */ + + } + + uef_clear_chunk (&(t->w_uef_tmpchunk)); + + return e; + +} + + +static int tape_write_start_data (tape_state_t * const t, + bool const always_117, + const serial_framing_t * const f) { + + /* We don't insert TIBET 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; + tape_interval_t elapsed; /* TOHv4.3 */ + int32_t dur; /* TOHv4.3-a4 */ + + 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; /* avoid bool on tibet.c/.h */ + h->baud = f->nominal_baud; + h->have_framing = 1; + framing_to_string(h->framing, f); + + dur = -1; + tape_get_duration_1200ths(t, &dur); + t->tibet_data_pending.timespan.pos_1200ths = 0; + tape_state.tallied_1200ths = dur; /* TOHv4.3 */ + + t->tibet_data_pending.timespan.start_1200ths = dur; + + } + + /* UEF */ + if (t->filetype_bits & TAPE_FILETYPE_BITS_UEF) { + + /* TOHv4.3 */ + elapsed.start_1200ths = 0; + tape_get_duration_1200ths(t, &(elapsed.start_1200ths)); + elapsed.pos_1200ths = 0; + + 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 */ + 0, + 0, + 0, + &elapsed); + 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; + t->w_uef_tmpchunk.elapsed = elapsed; /* TOHv4.3 */ + tape_state.tallied_1200ths = elapsed.start_1200ths; /* TOHv4.3 */ + + /* 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 * const t, uint32_t num_1200ths, bool const populate_elapsed) { + + int e; + uint8_t num_2400ths_enc[2]; + tibet_span_hints_t hints; + tape_interval_t elapsed; + int32_t d; + + 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; + } + + d=-1; + // num_2400ths = 0x7fffffff; + if (populate_elapsed) { + tape_get_duration_1200ths(t, &d); + } + + + if (t->filetype_bits & TAPE_FILETYPE_BITS_TIBET) { + e = tibet_append_leader_span (&(t->tibet), d, 2 * num_1200ths, &hints); + if (TAPE_E_OK != e) { return e; } + } + + /* chunk &110 */ + if (t->filetype_bits & TAPE_FILETYPE_BITS_UEF) { + + /* TOHv4.3: elapsed */ + elapsed.start_1200ths = d; /* chunk must dovetail to previous */ + elapsed.pos_1200ths = num_1200ths; + + tape_write_u16(num_2400ths_enc, 2 * num_1200ths); + e = uef_store_chunk(&(t->uef), num_2400ths_enc, 0x110, 2, 0, num_1200ths * 2, 0, 0, &elapsed); /* 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, d); + } + + return e; + +} + + +/* TOHv3.2 (rc1 killer!): respect -tape112 switch (or GUI equivalent) */ +static int tape_write_silence_2 (tape_state_t * const t, + double len_s, + bool const silence112, + int32_t const silence_start_elapsed_1200ths) { /* TOHv4.3 */ + + int e; + tibet_span_hints_t hints; + int32_t rem, num_2400ths; + uint8_t buf[2]; + tape_interval_t elapsed; + + e = TAPE_E_OK; + memset(&hints, 0, sizeof(tibet_span_hints_t)); + +/* printf("tape_write_silence_2: %lf\n", len_s); */ + + if (len_s < TAPE_1200TH_IN_S_FLT) { + 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; + } + + /* 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, silence_start_elapsed_1200ths, &hints); + if (TAPE_E_OK != e) { return e; } + } + + /* silence in UEFs ... */ + if (t->filetype_bits & TAPE_FILETYPE_BITS_UEF) { + elapsed.start_1200ths = silence_start_elapsed_1200ths; /* TOHv4.3 */ + if (silence112) { + /* one or more chunk &112s */ + num_2400ths = (int32_t) (0.5f + (len_s * TAPE_1200_HZ * 2.0)); + if (0 == num_2400ths) { + log_warn ("tape: WARNING: silence as &112: gap is tiny (%f s); round up to 1/2400", len_s); + num_2400ths = 1; + } + /* multiple chunks needed */ + for ( rem = num_2400ths; rem > 0; rem -= 65535 ) { + tape_write_u16(buf, (rem > 65535) ? 65535 : rem); + elapsed.pos_1200ths = (rem / 2); + e = uef_store_chunk (&(t->uef), + buf, + 0x112, + 2, + 0, /* TOHv4.2: dummy offset */ + num_2400ths, /* TOHv4.3: pre_cycs for silence (for seeking) */ + 0, + 0, + &elapsed); + if (TAPE_E_OK != e) { return e; } + elapsed.start_1200ths += elapsed.pos_1200ths; + } + } else { + /* chunk &116 */ + /* FIXME: endianness? */ + union { float f; uint8_t b[4]; } u; + u.f = len_s; + num_2400ths = (int32_t) (0.5f + (len_s * TAPE_1200_HZ * 2.0)); + elapsed.pos_1200ths = (num_2400ths / 2); + e = uef_store_chunk(&(t->uef), + u.b, + 0x116, + 4, + 0, /* TOHv4.2: dummy offset */ + num_2400ths, /* TOHv4.3: pre_cycs for silence (for seeking) */ + 0, + 0, + &elapsed); + if (TAPE_E_OK != e) { return e; } + } + } + + if (t->filetype_bits & TAPE_FILETYPE_BITS_CSW) { + elapsed.start_1200ths = silence_start_elapsed_1200ths; /* TOHv4.3-a4 */ + num_2400ths = (int32_t) (0.5f + (len_s * TAPE_1200_HZ * 2.0)); + elapsed.pos_1200ths = (num_2400ths / 2); + e = csw_append_silence(&(t->csw), len_s, &elapsed); + } + + return e; + +} + +/* TOHv4.1: removed record_activated arg; this function should + * now only ever be called if record mode is activated */ +int tape_wp_init_blank_tape_if_needed (uint8_t * const filetype_bits_inout) { + /* 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); + return TAPE_E_OK; +} + +#include "tapeseek.h" + +/* tape.c */ +static int wp_bitclk_handle_silence (tape_state_t * const t, + tape_ctrl_window_t * const tcw, /* TOHv4.3 */ + bool const silent, + bool const tapenoise_active, + bool const record_is_pressed, + int64_t const ns_per_bit, + bool const silence112, + bool const tapectrl_opened, + tape_interval_list_t * const iv_list_inout, /* the master copy on tape_vars->interval_list, not its tapectrl clone */ + bool * const reset_shift_register_out, + uint32_t * const since_last_tone_sent_to_gui_inout) { + + int e; + int64_t num_1200ths, i; + int32_t duration; /* TOHv4.3 */ + + e = TAPE_E_OK; + + /* handle TX silence logic: + 1. if silent, wipe the ACIA's tx data, send 'S' to tape noise + 2. if not silent, but previously silent, end section, flush the silence to tape + */ + + if ( silent ) { + /* TOHv4.3 */ + /* silent section BEGINS? record current elapsed time */ + if (0 == t->w_accumulate_bit_periods_ns) { + /* borrow this field, even though we don't use tmpchunk to make a silent chunk */ + t->w_uef_tmpchunk.elapsed.start_1200ths = t->tallied_1200ths; + } + e = wp_end_data_section_if_ongoing(t, record_is_pressed, reset_shift_register_out); + if (TAPE_E_OK != e) { return e; } + 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; (i < num_1200ths); i++) { /* TOHv4: tones_per_bit loop */ + if (tapenoise_active) { + tapenoise_send_1200 ('S', &(t->tapenoise_no_emsgs)); + } + /* TOHv4.3 */ + if (record_is_pressed) { + (t->tallied_1200ths)++; +#ifdef BUILD_TAPE_TAPECTRL + e = tape_interval_list_send_1200th(iv_list_inout, 'S', false); /* no free on error */ +#endif + } + } + /* TOHv4.3 */ + if (record_is_pressed && tapectrl_opened) { +#ifdef BUILD_TAPE_TAPECTRL + /* will lock+unlock mutex */ + send_tone_to_tapectrl_maybe (tcw, + t->tallied_1200ths, + t->tallied_1200ths, + 'S', + //true, + //true, + since_last_tone_sent_to_gui_inout); +#endif + + if (TAPE_E_OK != e) { return e; } + } + } else if (t->w_accumulate_silence_ns > 0) { + /* a prior silent section has ended, so we need to write it out */ + /* TOHv4.3: */ + e = tape_get_duration_1200ths(t, &duration); + if (TAPE_E_OK != e) { return e; } + if (record_is_pressed) { + e = tape_write_silence_2 (t, + ((double) t->w_accumulate_silence_ns) / 1000000000.0, + silence112, + duration); + if (TAPE_E_OK != e) { return e; } + } + t->tallied_1200ths += (t->w_accumulate_silence_ns / TAPE_1200TH_IN_NS_INT); /* TOHv4.3 */ + t->w_accumulate_silence_ns = 0; + } + + return e; + +} + + +/* TOHv3.2: rework; flush partial frames out to UEF properly */ +static int wp_end_data_section_if_ongoing (tape_state_t * const t, + bool const record_is_pressed, + bool * const reset_shift_register_out) { + + int e; + + if ( ! t->w_must_end_data ) { return TAPE_E_OK; } + + e = TAPE_E_OK; + *reset_shift_register_out = 0; + + /* 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 ((t->filetype_bits & TAPE_FILETYPE_BITS_UEF) && 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->tallied_1200ths, + &(t->w_uef_serial_phase), + &(t->w_uef_serial_frame), + &(t->w_uef_tmpchunk)); + if (TAPE_E_OK != e) { return e; } /* TOHv4.3-a4: errors were being missed */ + 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; + +} + + +static int wp_bitclk_accumulate_leader (tape_state_t * const t, + tape_ctrl_window_t * const tcw, + tape_interval_list_t * const iv_list_inout, /* this will be the one from tape_vars.interval_list */ + bool const tapenoise_active, + int64_t const ns_per_bit, + bool const record_is_pressed, + bool * const reset_shift_reg_out, + uint32_t * const since_last_tone_sent_to_gui_inout, + bool const tapectrl_opened) { + + int32_t num_1200ths, i; + int e; + + /* if there is nothing to output in the shift register, + * then we have leader tone */ + + /* end and write out any current data section */ + e = wp_end_data_section_if_ongoing (t, record_is_pressed, reset_shift_reg_out); + if (TAPE_E_OK != e) { return e; } /* TOHv4.3: bugfix */ + 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; i < num_1200ths; i++) { + if (tapenoise_active) { + tapenoise_send_1200 ('L', &(t->tapenoise_no_emsgs)); + } + /* TOHv4.3 */ +#ifdef BUILD_TAPE_TAPECTRL + /* No -- don't gate this with tapectrl_opened. The intervals list + * on tape_vars must be updated regardless of the status of + * the tapectrl. */ + if (record_is_pressed) { /* && tapectrl_opened) {*/ + e = tape_interval_list_send_1200th(iv_list_inout, 'L', false); /* no free on error */ + } + if (TAPE_E_OK != e) { return e; } +#endif + } + /* TOHv4.3: update tapectrl window: + * - set elapsed to duration + * - leader to lamps*/ + if (record_is_pressed) { /* fix: needed this check or it would send + spurious TX 'L' to tapectrl during RX */ + t->tallied_1200ths += num_1200ths; + if ((num_1200ths > 0) && tapectrl_opened) { + /* will lock+unlock mutex */ +#ifdef BUILD_TAPE_TAPECTRL + send_tone_to_tapectrl_maybe (tcw, + t->tallied_1200ths, + t->tallied_1200ths, + 'L', + since_last_tone_sent_to_gui_inout); +#endif + } + } + return e; +} + + +static void framing_to_string (char fs[4], const serial_framing_t * const 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'; +} + + + diff -uN b-em-0b6f1d2-vanilla/src/tapewrite.h b-em-0b6f1d2-TOH/src/tapewrite.h --- b-em-0b6f1d2-vanilla/src/tapewrite.h 1970-01-01 01:00:00.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/tapewrite.h 2025-11-01 14:49:30.099220896 +0000 @@ -0,0 +1,52 @@ +/* + * B-Em + * This file (C) 2025 'Diminished' + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef __INC_TAPEWRITE_H +#define __INC_TAPEWRITE_H + +#include "tape.h" + +int tape_flush_pending_piece (tape_state_t * const ts, + ACIA * const acia_or_null, + bool const silence112, + bool const populate_elapsed); + +int tape_write_bitclk (tape_state_t * const ts, + tape_ctrl_window_t * const tcw, /* TOHv4.3 */ + tape_interval_list_t * const iv_list_inout, /* TOHv4.3 */ + ACIA * const acia, + char const bit, + int64_t const ns_per_bit, + bool const tapenoise_write_enabled, + bool const record_is_pressed, + bool const always_117, + bool const silence112, + bool const no_origin_chunk_on_append, + bool const tapectrl_opened, /* TOHv4.3 */ + uint32_t * const since_last_tone_sent_to_gui_inout); + +int tape_uef_flush_incomplete_frame (uef_state_t * const uef_inout, + int32_t const tallied_1200ths, + uint8_t * const serial_phase_inout, + uint8_t * const serial_frame_inout, + uef_chunk_t * const chunk_inout); /* in practice this is ts->w_uef_tmpchunk */ + +int tape_wp_init_blank_tape_if_needed (uint8_t * const filetype_bits_inout); + +#endif /* __INC_TAPEWRITE_H */ diff -uN b-em-0b6f1d2-vanilla/src/tibet.c b-em-0b6f1d2-TOH/src/tibet.c --- b-em-0b6f1d2-vanilla/src/tibet.c 1970-01-01 01:00:00.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/tibet.c 2025-11-02 07:55:41.224228491 +0000 @@ -0,0 +1,2254 @@ +/* + * B-Em + * This file (C) 2025 'Diminished' + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#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, int32_t start_1200ths, tibet_span_hints_t *hints); +static int append_leader_span (tibet_priv_t *tp, + int32_t start_1200ths, /* or -1, before initial scan */ + 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, uint8_t populate_elapsed, char *tc); +static int c_read_1200 (tibet_priv_t *t, uint8_t populate_elapsed, uint8_t *out_or_null, 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, uint8_t populate_elapsed, 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); + +/* TOHv4.3 */ +static void reset_client_state(tibet_priv_t * const tp); +static void next_tonechar_do_elapsed (bool const populate_elapsed, tibet_span_t * const span); + +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) { + int e; + uint32_t i; + /* TOHv3: 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; + } + /* don't write any timespan information yet */ + e = append_leader_span(tp, -1, 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; + } + /* do not write any timespan information yet ... */ + e = append_silent_span (tp, f, -1, active_hints); + 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 +#define SPAN_MAX_TONES 17000000 /* thinking of the 24-bit chunk &114 field here */ + +// 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; + } + 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; + } + span->tones[span->num_tones] = c; + (span->num_tones)++; /* 2400ths */ + return TAPE_E_OK; +} + + +// 2400ths: +static int c_next_tonechar (tibet_priv_t *tp, uint8_t populate_elapsed, char *tc) { + + tibet_span_t *span; + int e; + + e = c_get_span(tp, populate_elapsed, &span); + if (TAPE_E_OK != e) { return e; } + + if (TIBET_SPAN_DATA == span->type) { + if ('P' == span->tones[tp->c_tone_pos]) { + // we just skip pulses + (tp->c_tone_pos)++; + next_tonechar_do_elapsed(populate_elapsed, span); + return c_next_tonechar (tp, populate_elapsed, tc); // and recurse + } +#ifdef BUILD_TAPE_SANITY + if ( (span->tones[tp->c_tone_pos]!='.') + && (span->tones[tp->c_tone_pos]!='-')) { + TIBET_ERR("TIBET: BUG: illegal tone in data span->tones: &%x ('%c')\n", + span->tones[tp->c_tone_pos],span->tones[tp->c_tone_pos]); + return TAPE_E_BUG; + } +#endif + *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) { +// printf("\n\n"); + *tc = 'L'; + (tp->c_tone_pos)++; + } + + next_tonechar_do_elapsed(populate_elapsed, span); + + // call this again, so that the span gets incremented if need be: + e = c_get_span(tp, populate_elapsed, &span); + + return e; + +} + + +static void next_tonechar_do_elapsed (bool const populate_elapsed, tibet_span_t * const span) { + if (populate_elapsed) { /* TOHv4.3 */ + (span->timespan.sub_pos_4800ths) += 2; + while (span->timespan.sub_pos_4800ths >= 4) { + (span->timespan.sub_pos_4800ths) -= 4; + (span->timespan.pos_1200ths)++; + } + } +} + + +bool tibet_peek_eof (tibet_t * const 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, uint8_t populate_elapsed, tibet_span_t **span) { + + tibet_span_t *_span; + uint8_t advance; // TOHv3.2 + tape_interval_t *t0, *t1; /* TOHv4.3 */ + int32_t t0v; + + *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; + } + // TOHv4.3 + t0 = &(_span->timespan); + if ((tp->c_cur_span+1) < tp->dd_new_num_spans) { + t1 = &(tp->dd_new_spans[tp->c_cur_span+1].timespan); + t0v = (t0->start_1200ths + t0->pos_1200ths); /* compute predicted elapsed */ + // printf("finished span %d, type %u, interval = %d + %d\n", tp->c_cur_span, tp->dd_new_spans[tp->c_cur_span].type, t0->start_1200ths, t0->pos_1200ths); + if (! populate_elapsed) { + if ((t1->start_1200ths > 0) && (t1->start_1200ths != t0v)) { + TIBET_ERR("WARNING: TIBET: c_get_span(): expected %d 1200ths on new span but found %d existing (span ix %d)", + t0v, t1->start_1200ths, tp->c_cur_span+1); + } + } + if (populate_elapsed) { /* TOHv4.3 */ + // printf("dd_new_spans[%d+1].start_1200ths = %d\n", tp->c_cur_span, t0v); + t1->start_1200ths = t0v; + t1->pos_1200ths = 0; + t1->sub_pos_4800ths = 0; + } + } + (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 populate_elapsed, + uint8_t *out_or_null, + 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, populate_elapsed, &span); + if (TAPE_E_OK != e) { return e; } + + e = c_next_tonechar (tp, populate_elapsed, &tc1); + if (TAPE_E_OK != e) { return e; } + e = c_next_tonechar (tp, populate_elapsed, &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 + if (NULL != out_or_null) { + *out_or_null = tc1; + } + return TAPE_E_OK; + } + // skip first tonechar, resync and try again + tc1 = tc2; + e = c_next_tonechar (tp, populate_elapsed, &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 100 /* TOHv4.3: was previously 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, uint8_t populate_elapsed, 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, populate_elapsed, &span); + if (TAPE_E_OK != e) { return e; } + + e = c_next_tonechar (tp, populate_elapsed, &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, + uint8_t populate_elapsed, /* TOHv4.3 */ + char *bit_out_or_null) { + + 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, populate_elapsed, &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 + + if (bit_out_or_null != NULL) { + *bit_out_or_null = 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; + + /* TOHv4.3-a6 + * fix memleak: destroy any existing allocation before wiping the TIBET */ + tibet_finish(t); + 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; + if (NULL == t) { return; } /* fix: TOHv4.3-a6 */ + 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, int32_t start_1200ths, 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, start_1200ths, num_2400ths, hints); +} + + +// hints will be copied, and zeroed on return +static int append_leader_span (tibet_priv_t *tp, + int32_t start_1200ths, /* or -1, before initial scan */ + uint32_t num_2400ths, + tibet_span_hints_t *hints) { + + int e; + tibet_span_t *span; + +//printf("tibet_append_leader_span\n"); /*, ");*/ + + if (num_2400ths < 2) { + TIBET_ERR("TIBET: WARNING: append leader span: clamping num_2400ths: %u -> 2", + num_2400ths); + num_2400ths = 2; + } + +// printf("append_leader_span: start 1200ths %d, num 2400ths %u\n", start_1200ths, num_2400ths); + + if (start_1200ths < 0) { /* TOHv4.3: bugfix */ + start_1200ths = 0; + } + + e = c_new_span(tp); + if (TAPE_E_OK != e) { return e; } + span = tp->dd_new_spans + (tp->dd_new_num_spans - 1); + span->hints = *hints; + span->num_tones = num_2400ths; + span->type = TIBET_SPAN_LEADER; + memset(hints, 0, sizeof(tibet_span_hints_t)); + /* TOHv4.3 */ + if (start_1200ths > 0) { + span->timespan.start_1200ths = start_1200ths; + span->timespan.pos_1200ths = (num_2400ths / 2); + } + return TAPE_E_OK; +} + + +int tibet_append_silent_span (tibet_t *t, float len_s, int32_t start_1200ths, 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, start_1200ths, hints); +} + + +// hints will be copied, and zeroed on return +static int append_silent_span (tibet_priv_t *tp, float len_s, int32_t start_1200ths_or_minus_one, 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); +// printf("append_silent_span (%u): len %f\n", (tp->dd_new_num_spans - 1), len_s); + span->type = TIBET_SPAN_SILENT; + span->opo_silence_duration_secs = len_s; + span->hints = *hints; + memset(hints, 0, sizeof(tibet_span_hints_t)); + /* TOHv4.3 */ + memset(&(span->timespan), 0, sizeof(tape_interval_t)); +// printf("append_silent_span @ %d: start_1200ths = %d, len %f\n", tp->dd_new_num_spans-1, start_1200ths_or_minus_one, len_s); + if (start_1200ths_or_minus_one >= 0) { + span->timespan.start_1200ths = start_1200ths_or_minus_one; + span->timespan.pos_1200ths = (int32_t) (0.5 + (len_s * (1.0/TAPE_1200TH_IN_S_FLT) /* 1201.9 */)); + } + 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) { + TIBET_ERR("TIBET: BUG: pending span has bad type (%u)", 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); + +//printf("tibet_append_data_span: %d + %d = %d\n", span->timespan.start_1200ths, span->timespan.pos_1200ths, span->timespan.start_1200ths+span->timespan.pos_1200ths); + + 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 * const 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; +} + + +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; + +} + +int tibet_get_duration (tibet_t *t, int32_t *out_1200ths) { + + int e; + tibet_priv_t *tp; + tibet_span_t *span; + + *out_1200ths = -1; + + e = c_get_priv(t, &tp, 0); + if (TAPE_E_OK != e) { return e; } + + if (0 == tp->dd_new_num_spans) { + *out_1200ths = 0; + return TAPE_E_OK; + } + + span = tp->dd_new_spans + tp->dd_new_num_spans - 1; +/*printf("tibet_get_duration: %d spans: final is (%d + %d) type %u or %u\n", tp->dd_new_num_spans, span->timespan.start_1200ths, span->timespan.pos_1200ths, span->type, span->timespan.type);*/ + *out_1200ths = span->timespan.start_1200ths + span->timespan.pos_1200ths; + + return TAPE_E_OK; + +} + +/* TOHv4.3 */ +int tibet_get_num_spans (tibet_t *t, uint32_t *out) { + int e; + tibet_priv_t *tp; + *out = 0; + e = c_get_priv(t, &tp, 0); + if (TAPE_E_OK != e) { return e; } + *out = tp->dd_new_num_spans; + return e; +} + +/* TOHv4.3 */ +int tibet_get_time_interval_for_span (tibet_t * const t, + uint32_t const span_ix, + tape_interval_t * const interval_out) { + int e; + tibet_priv_t *tp; + e = c_get_priv(t, &tp, 0); + if (TAPE_E_OK != e) { return e; } + if (span_ix >= tp->dd_new_num_spans) { + TIBET_ERR("TIBET: BUG: tibet_get_time_interval_for_span: bad span ix (%u, have %u spans)", + span_ix, tp->dd_new_num_spans); + return TAPE_E_BUG; + } + *interval_out = tp->dd_new_spans[span_ix].timespan; + return e; +} + +int tibet_get_type_for_span (tibet_t * const t, uint32_t const span_ix, char * const type_out) { + int e; + tibet_priv_t *tp; + e = c_get_priv(t, &tp, 0); + if (TAPE_E_OK != e) { return e; } + if (span_ix >= tp->dd_new_num_spans) { + TIBET_ERR("TIBET: BUG: tibet_get_type_for_span: bad span ix (%u, have %u spans)", + span_ix, tp->dd_new_num_spans); + return TAPE_E_BUG; + } + *type_out = tp->dd_new_spans[span_ix].type; + return e; +} + +/* TOHv4.3 */ +int tibet_change_current_span(tibet_t * const t, uint32_t const span_ix) { + tibet_priv_t *tp; + int e; + e = c_get_priv(t, &tp, 0); + if (TAPE_E_OK != e) { return e; } + if (span_ix >= tp->dd_new_num_spans) { + TIBET_ERR("TIBET: BUG: tibet_change_current_span: bad span ix (%u, total %u)", + span_ix, tp->dd_new_num_spans); + return TAPE_E_BUG; + } + tp->c_cur_span = span_ix; + reset_client_state(tp); + return e; +} + +/* TOHv4.3 */ +static void reset_client_state(tibet_priv_t * const tp) { + tp->c_tone_pos = 0; + tp->c_silence_pos_s = 0.0; + tp->c_prev_tonechar = '\0'; +} + + +/* **************** */ + + +/* 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 -uN b-em-0b6f1d2-vanilla/src/tibet.h b-em-0b6f1d2-TOH/src/tibet.h --- b-em-0b6f1d2-vanilla/src/tibet.h 1970-01-01 01:00:00.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/tibet.h 2025-10-31 08:40:55.771881883 +0000 @@ -0,0 +1,178 @@ +/* + * B-Em + * This file (C) 2025 'Diminished' + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#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 + +#include "tape2.h" /* for BUILD_TAPE_READ_POS_TO_EOF_ON_WRITE */ +#include "tapeseek.h" + +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; + + char *tones; + uint32_t num_tones; /* 2400ths */ + uint32_t num_tones_alloc; + + /* for silent spans (OUTPUT ONLY [opo]) */ + float opo_silence_duration_secs; + + tibet_span_hints_t hints; + + tape_interval_t timespan; /* TOHv4.3 */ + +} 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); +*/ +bool tibet_peek_eof (tibet_t * const t); + +int tibet_have_any_data (tibet_t *t, uint8_t * const 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, uint8_t populate_elapsed, char *value_out); +int tibet_read_bit (tibet_t *t, uint8_t baud300, uint8_t populate_elapsed, char *bit_out_or_null); +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, 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, int32_t start_1200ths, uint32_t num_2400ths, tibet_span_hints_t *hints); +int tibet_append_silent_span (tibet_t *t, float len_s, int32_t start_1200ths, tibet_span_hints_t *hints); +int tibet_append_data_span (tibet_t *t, tibet_span_t *pending) ; /* pending = data+hints */ + +int tibet_ffwd_to_end (tibet_t *t); + +int tibet_clone_span (tibet_span_t *out, tibet_span_t *in); // TOHv4.1 + +/* TOHv4.3 */ +int tibet_seek (tibet_t *t, uint8_t wanted_1200ths, int32_t *got_1200ths_out); +int tibet_get_duration (tibet_t *t, int32_t *out_1200ths); +int tibet_get_num_spans (tibet_t *t, uint32_t *out); +int tibet_get_time_interval_for_span (tibet_t *t, uint32_t span_ix, tape_interval_t *interval_out); +int tibet_get_type_for_span (tibet_t * const t, uint32_t const span_ix, char * const type_out) ; /* -a4 */ +int tibet_change_current_span(tibet_t * const t, uint32_t const span_ix) ; + +#endif /* __INC_TIBET_H */ diff -uN b-em-0b6f1d2-vanilla/src/uef.c b-em-0b6f1d2-TOH/src/uef.c --- b-em-0b6f1d2-vanilla/src/uef.c 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/uef.c 2025-11-01 18:10:37.991204026 +0000 @@ -1,398 +1,2522 @@ /*B-em v2.2 by Tom Walker UEF/HQ-UEF tape support*/ + +/* - TOHv3, TOHv4 overhaul by 'Diminished' */ + +#ifndef TAPE_H_UEF +/* + * B-Em + * This file (C) 2025 'Diminished' + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#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 -int uef_toneon = 0; +/*#define UEF_WRITE_REJECT_NONSTD_BAUD*/ -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; - 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; +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 (const uef_chunk_t * const 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 bool valid_chunktype (uint16_t const t); +static int chunk_114_next_cyc (uef_bitsource_t *src, + uint32_t chunk_len, + uint32_t total_cycs, + 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 * const src, int32_t const nominal_baud); +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 */ +static int chunk_3_parse_header (const uef_chunk_t * const c, + uint16_t * const w_out, + uint16_t * const h_out, + uint8_t * const bpp_out, + bool * const grey_out, + uint32_t * const palette_pos_out); + +#define UEF_MAX_FLOAT_GAP 36000.0f /* ten hours */ +#define UEF_BASE_FREQ (TAPE_1200_HZ) + + +/* TOHv4 */ +int uef_get_117_payload_for_nominal_baud (int32_t const nominal_baud, + const char ** const payload_out) { + *payload_out = NULL; + 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; +} - 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; +static void init_bitsource (uef_bitsource_t * const src, int32_t const nominal_baud) { + memset(src, 0, sizeof(uef_bitsource_t)); + src->framing.num_data_bits = 8; + src->framing.parity = 'N'; + src->framing.num_stop_bits = 1; + src->framing.nominal_baud = nominal_baud; + src->chunk_114_pulsewaves[0] = '?'; + src->chunk_114_pulsewaves[1] = '?'; +} + +static int consider_chunk (uef_state_t * const u, + bool * const 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); + +/*printf("consider_chunk, type &%x\n", type);*/ + + /* New chunk, so reset the bit source. IMPORTANT: This preserves + * the 300 baud setting; everything else is reset. */ + init_bitsource(src, src->framing.nominal_baud); /* TOHv4.3: reset_bitsource() is gone */ + + /* 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; */ + + } + c->chunk_114_total_cycs = tape_read_u24(c->data); /* TOHv4.3 */ + 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 */ + c->nodata_total_pre_2400ths = tape_read_u16(c->data); /* TOHv4.3: now on chunk, not bitsrc */ + } else if (0x111==type) { + /* carrier tone + &AA + carrier tone */ + c->nodata_total_pre_2400ths = tape_read_u16(c->data); /* TOHv4.3: now on chunk, not bitsrc */ + c->nodata_total_post_2400ths = tape_read_u16(c->data + 2); /* TOHv4.3: now on chunk, not bitsrc */ + } else if (0x112==type) { + /* integer gap; UEF spec is wrong */ + c->nodata_total_pre_2400ths = tape_read_u16(c->data); /* TOHv4.3: now on chunk, not bitsrc */ + } 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; + } + c->nodata_total_pre_2400ths = (uint32_t) (0.5f + (f * UEF_BASE_FREQ * 2.0f)); /* TOHv4.3: now on chunk, not bitsrc */ + } + + 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) { + + 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, + uint32_t total_cycs, + 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 >= 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)", 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 >= c->chunk_114_total_cycs); /* TOHv4.3: totals now on chunk */ + } else if (0x111==type) { + return (src->nodata_consumed_post_2400ths >= c->nodata_total_post_2400ths); /* TOHv4.3: totals now on chunk */ + } else if ((0x110==type)||(0x112==type)||(0x116==type)) { + return (src->nodata_consumed_pre_2400ths >= c->nodata_total_pre_2400ths); /* TOHv4.3: totals now on chunk */ + } + /* other chunk types don't contain any bits */ + return 1; +} - 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; + +/* 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; +} + +/* TOHv4.3 */ +void uef_clear_chunk (uef_chunk_t * const c) { + uint8_t *data; + uint32_t alloc; + /* preserve these */ + data = c->data; + alloc = c->alloc; + /* memset */ + memset(c, 0, sizeof(uef_chunk_t)); + /* restore them */ + c->data = data; + c->alloc = alloc; +} + + +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->chunk_114_total_cycs, 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->chunk_114_total_cycs, 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_2400ths) += 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_2400ths >= chunk->nodata_total_pre_2400ths) { + src->chunk_111_state = 1; /* advance state */ + } else { + src->reservoir_1200ths = 0x1; + src->reservoir_len = 1; + (src->nodata_consumed_pre_2400ths)+=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; /* advance state */ + } else { + /* post-&AA leader */ + src->reservoir_1200ths = 0x1; + src->reservoir_len = 1; + (src->nodata_consumed_post_2400ths) += 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_2400ths)+=2; + } + + return TAPE_E_OK; + +} + +bool uef_peek_eof (const uef_state_t * const uef) { + return (uef->cur_chunk >= uef->num_chunks); +} + +/* TOHv4.3 */ +void uef_force_eof (uef_state_t * const uef) { + uef->cur_chunk = uef->num_chunks; +} + +static int accrue_metadata_til_tapetime (uef_state_t * const u, + bool const initial_scan, + int32_t * const updated_elapsed_1200ths_out_or_null, + uef_meta_t *metadata_list, + uint32_t * const metadata_fill_out) { + + int e; + bool chunk_contains_bits; + + chunk_contains_bits = false; + e = TAPE_E_OK; + + /* 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; + tape_interval_t *t, *t_prev; + int32_t now; + + (u->cur_chunk)++; /* initially goes -1 to 0 */ + + if (u->cur_chunk >= u->num_chunks) { + /*log_info("uef: EOF");*/ /* on EOF, metadata is NOT freed */ + return TAPE_E_EOF; + } + + t = &(u->chunks[u->cur_chunk].elapsed); + + if (initial_scan) { + t->pos_1200ths = 0; + t->sub_pos_4800ths = 0; + } + /* TOHv4.3: */ + if (u->cur_chunk < 1) { + if (initial_scan) { + t->start_1200ths = 0; + } + if (updated_elapsed_1200ths_out_or_null != NULL) { + *updated_elapsed_1200ths_out_or_null = 0; + } + } else { + t_prev = &(u->chunks[u->cur_chunk-1].elapsed); + now = t_prev->start_1200ths + t_prev->pos_1200ths; + /*printf("now = %d + %d\n", t_prev->start_1200ths, t_prev->pos_1200ths);*/ +#ifdef BUILD_TAPE_SANITY + /* has the chunk's start time previously been written? + if so, make sure that our recalculated start time matches it */ + if ((t->start_1200ths != 0) && (t->start_1200ths != now)) { + log_warn("tape: UEF: BUG: read 1200th: chunk #%d (type &%x); old chunk start time (%d) does not match new (%d)\n", + u->cur_chunk, u->chunks[u->cur_chunk].type, t->start_1200ths, now); + return TAPE_E_BUG; + } +#endif + if (initial_scan) { + t->start_1200ths = now; + } + if (updated_elapsed_1200ths_out_or_null != NULL) { + *updated_elapsed_1200ths_out_or_null = now; + } + } + chunk_contains_bits = false; + /* 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; + if ( ! initial_scan ) { /* suppress initial scan logspam */ + log_info("uef: &117: baud change on tape: %u\n", meta.data.baud); } - 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); - } + } + if (metadata_list != NULL) { + /* 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; } - return; + metadata_list[*metadata_fill_out] = meta; + (*metadata_fill_out)++; + } + } else { + e = consider_chunk (u, &chunk_contains_bits); + if (TAPE_E_OK != e) { return e; } + } + } /* keep trying chunks until we get some bits */ + return TAPE_E_OK; +} + + +/* 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 * const u, + char * const out_1200th_or_null, + uef_meta_t * const metadata_list, /* caller must call uef_metadata_list_finish() */ + bool const initial_scan, + uint32_t * const metadata_fill_out, + /* TOHv4.3: usually returns -1, but may return accurate timestamp: */ + int32_t * const updated_elapsed_1200ths_out_or_null) { + + /* 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 we + * know it came from a leader UEF chunk. */ + + int e; + uef_bitsource_t *src; + uint16_t shift; + + if (NULL != metadata_fill_out) { + *metadata_fill_out = 0; + } + + if (NULL != out_1200th_or_null ) { *out_1200th_or_null = '\0'; } + if (NULL != updated_elapsed_1200ths_out_or_null) { *updated_elapsed_1200ths_out_or_null = -1; } + + 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) { + e = TAPE_E_OK; + /* chunk spent! free any metadata from previous chunk */ + if (NULL != metadata_list) { + uef_metadata_list_finish (metadata_list, *metadata_fill_out); + } + e = accrue_metadata_til_tapetime (u, + initial_scan, + updated_elapsed_1200ths_out_or_null, + metadata_list, + metadata_fill_out); + 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 it fails again then give up */ + if (TAPE_E_OK != e) { return e; } + + } /* endif (new chunk needed) */ + + } /* endif (reservoir empty) */ + + /* Reservoir contains something; emit 1/1200th of a second + * from the reservoir */ + + /* src->silence (maybe set by above reload_reservoir call) + * overrides the reservoir contents: */ + if (out_1200th_or_null != NULL) { + if ( ! src->silence ) { + shift = src->reservoir_pos; + *out_1200th_or_null = (0x1 & (src->reservoir_1200ths >> shift)) ? '1' : '0'; + } else { + *out_1200th_or_null = 'S'; + } + } - 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; + (src->reservoir_pos)++; + + /* TOHv4.3, for initial scan */ + if (initial_scan) { + u->chunks[u->cur_chunk].elapsed.pos_1200ths++; + } + + return TAPE_E_OK; + +} + +/* TOHv4.3 */ +int uef_get_elapsed_1200ths (const uef_state_t * const u, + int32_t * const time_1200ths_out) { + tape_interval_t elapsed; + int e; + *time_1200ths_out = -1; + if (0==u->num_chunks) { + return TAPE_E_OK; + } + if (u->cur_chunk < 0) { /* cur_chunk = -1 is legal */ + *time_1200ths_out = 0; + return TAPE_E_OK; + } + if (u->cur_chunk >= u->num_chunks) { + /* beyond end of tape -- just return the duration */ + e = uef_get_duration_1200ths(u, time_1200ths_out); + return e; + } + elapsed = u->chunks[u->cur_chunk].elapsed; + *time_1200ths_out = elapsed.start_1200ths + elapsed.pos_1200ths; + return TAPE_E_OK; +} + + +/* TOHv4.3 */ +int uef_get_duration_1200ths (const uef_state_t * const uef, + int32_t * const duration_1200ths_out) { + + tape_interval_t *et; + + *duration_1200ths_out = -1; + + if (0 == uef->num_chunks) { return TAPE_E_OK; } + + et = &(uef->chunks[uef->num_chunks - 1].elapsed); + *duration_1200ths_out = et->start_1200ths + et->pos_1200ths; + + return TAPE_E_OK; + +} + + +int uef_detect_magic ( uint8_t * const buf, + uint32_t const buflen, + const char * const filename, + bool const 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; +} + +static bool my_isdigit (char c) { + return (c >= '0') && (c <= '9'); +} + +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); + buf = NULL; + 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 */ + /* FIXME: log message assumes non-UTF8 */ + for (z=0; z < len; z++) { + //if (!isprint(s[z])) { + /* TOHv4.3-a2: bugfix for test "Chunk &0, origin: legal UTF-8:" + * Don't use isprint() any more. */ + if ((s[z]&0x80) || (((unsigned char)s[z])<0x20) || ((unsigned char)s[z])>0x7e) { + 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)) && my_isdigit(maj[a]); a++); + if (a!=strlen(maj)) { continue; } + for (a=0; (a < strlen(min)) && my_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: Work around MakeUEF < 2.4 (%d.%d) parity bug: swap chunk &104 even and odd", 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 1000 /* TOHv4.3: increased from 10 to 1000 */ + +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; + tape_interval_t elapsed; + + chunklen = 0; + + elapsed.start_1200ths = 0; + elapsed.pos_1200ths = 0; + elapsed.sub_pos_4800ths = 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 */ + /* TOHv4.3-a3: relaxed from >= to >, allow zero-length chunks in principle */ + 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; + } + } + + /* TOHv4.2: add offset */ + /* TOHv4.3: add elapsed fields (both 0 until initial scan runs) + new 3x cycs fields will be filled in later, on parse, so they're 0 here */ + e = uef_store_chunk (uef, buf + pos + 6, type, chunklen, pos, 0, 0, 0, &elapsed); + if (TAPE_E_OK != e) { return e; } + + } + + // if (0==uef->num_chunks) { + // return TAPE_E_BUG; + // } + + /* + 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 bool valid_chunktype (uint16_t const 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); +} + +/* integrity check */ +int uef_verify_timestamps (const uef_state_t * const uef) { + int32_t i; + for (i=0; i < (uef->num_chunks - 1); i++) { + uef_chunk_t *c1, *c2; + tape_interval_t *iv1, *iv2; + int32_t end; + c1 = uef->chunks + i; + c2 = c1 + 1; + iv1 = &(c1->elapsed); + iv2 = &(c2->elapsed); + end = (iv1->start_1200ths + iv1->pos_1200ths); + if (end != iv2->start_1200ths) { + log_warn("uef: BUG: timestamp integrity failure: chunks[%d]/t&%x = (%d + %d) = %d but [%d]/t&%x's start is %d\n", + i, c1->type, iv1->start_1200ths, iv1->pos_1200ths, end, i+1, c2->type, iv2->start_1200ths); + return TAPE_E_BUG; + } + } + /*printf("verify_timestamps: %d chunks OK\n", i);*/ + return TAPE_E_OK; +} + +#define UEF_CHUNKLEN_MAX 0xffffff /* shrug */ + +/* TOHv3: exported now */ +int uef_store_chunk ( uef_state_t * const uef, + const uint8_t * const buf, + uint16_t const type, + uint32_t const len, + uint32_t const offset, /* TOHv4.2: add offset */ + uint32_t const nodata_pre_2400ths, /* TOHv4.3: needed for chunks &110, 111, 112, 116 */ + uint32_t const nodata_post_2400ths, /* TOHv4.3: needed for chunk &111 only */ + uint32_t const cycs_114, /* TOHv4.3: chunk &114's 24-bit length field */ + const tape_interval_t * const elapsed) { + + uef_chunk_t *chunk; + int32_t newsize; + uef_chunk_t *p; + +#ifdef BUILD_TAPE_SANITY + if (elapsed->start_1200ths < -1) { + log_warn("uef: BUG: store chunk (type &%x): elapsed start_1200ths is duff (%d)", type, elapsed->start_1200ths); + return TAPE_E_BUG; + } + if (elapsed->pos_1200ths < 0) { + log_warn("uef: BUG: store chunk (type &%x): elapsed pos_1200ths is duff (%d)", type, elapsed->pos_1200ths); + return TAPE_E_BUG; + } +#endif + + /* TOHv4.3 now permits zero-length chunks in principle. */ + + /* 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"); + /*uef_finish(uef);*/ /* ? */ + return TAPE_E_MALLOC; + } + uef->chunks = p; + uef->num_chunks_alloc = newsize; + } + + chunk = uef->chunks + uef->num_chunks; + + memset(chunk, 0, sizeof(uef_chunk_t)); /* TOHv4.3 */ + chunk->type = type; + chunk->len = len; + chunk->offset = offset; + chunk->data = malloc (len + 1); /* alloc an extra byte, for strings */ + chunk->alloc = len; /* TOHv3 */ + chunk->data[len] = '\0'; + chunk->elapsed = *elapsed; /* TOHv4.3 */ + if (chunk->elapsed.start_1200ths < 0) { + chunk->elapsed.start_1200ths = 0; + } + /* TOHv4.3: the caller must provide these fields, + * in order for seeking to work on generated UEF files. */ + chunk->nodata_total_pre_2400ths = nodata_pre_2400ths; + chunk->nodata_total_post_2400ths = nodata_post_2400ths; + chunk->chunk_114_total_cycs = cycs_114; + + 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)++; + +#ifdef BUILD_TAPE_SANITY + /*e = verify_timestamps(uef);*/ /* TOHv4.3-a4 */ + /* + if (TAPE_E_OK != e) { + debug_tape_ls_uef(&tape_state); + } + */ + /*if (TAPE_E_OK != e) { return e; }*/ +#endif + + 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_instructions, 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_inlay_scans, 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_target_machines, 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_rom_hints, 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; + /* TOHv4.3 */ + uef_inlay_scan_t *scan; + uint32_t palette_pos, expected; + 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!"); + 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!"); + break; + } + g->instructions[ni].utf8 = (char *) c->data; + g->instructions[ni].len = c->len; + ni++; + log_it = 1; + } else if ( 0x3 == c->type ) { + scan = g->inlay_scans + nis; + e = chunk_3_parse_header(c, + &(scan->w), + &(scan->h), + &(scan->bpp), + &(scan->grey), + &palette_pos); /* 0 or 5 */ + if (TAPE_E_UEF_INLAY_SCAN_BPP == e) { + /* permit inlay scans with unsupported formats */ + e = TAPE_E_OK; + } + if (TAPE_E_OK != e) { break; } + + expected = (scan->w * scan->h * (scan->bpp/8)); + + if (5 == palette_pos) { + expected += 773; + } else { + expected += 5; + } + + /* compute expected chunk len */ + if (expected > c->len) { + log_warn("uef: chunk 3 expected len (%u) mismatches actual (%u)", + expected, c->len); + e = TAPE_E_UEF_CHUNKDAT_0003; + } + scan->body = (char *) (c->data + ((5 == palette_pos) ? 773 : 5)); + scan->palette = NULL; + if (palette_pos > 0) { + scan->palette = (char *) (c->data + palette_pos); + } + 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_3_parse_header (const uef_chunk_t * const c, + uint16_t * const w_out, + uint16_t * const h_out, + uint8_t * const bpp_out, + bool * const grey_out, + uint32_t * const palette_pos_out) { + uint32_t len_body; + uint32_t i; + *palette_pos_out = 0; + *w_out = -1; + *h_out = -1; + *bpp_out = 0xff; + *grey_out = false; + if (c->len < 5) { + log_warn("uef: chunk type &3 has bad length (%u, need >=5)", c->len); + return TAPE_E_UEF_CHUNKLEN_0003; + } + /* compute predicted size */ + /* bugfix: the caller needs these values even if this function fails, so */ + *w_out = tape_read_u16(c->data+0); + *h_out = tape_read_u16(c->data+2); + *bpp_out = 0x7f & c->data[4]; /* BPP */ + *grey_out = 0x80 & c->data[4]; /* greyscale? */ + /* keep this simple for now and enforce BPP = 8, 16, 24 or 32 */ + if ((*bpp_out!=8)&&(*bpp_out!=16)&&(*bpp_out!=24)&&(*bpp_out!=32)) { + log_warn("uef: chunk type &3 has bizarre BPP value &%u", *bpp_out); + return TAPE_E_UEF_INLAY_SCAN_BPP; + } + /* TOHv4.3: reject non-8bpp for now */ + if (*bpp_out != 8) { + log_warn("uef: inlay scan: Only 8 bpp supported for now, found %u. Ignoring this scan.", *bpp_out); + return TAPE_E_UEF_INLAY_SCAN_BPP; + } + len_body = (*w_out) * (*h_out) * ((*bpp_out) / 8); + if (0 == len_body) { + log_warn("uef: chunk type &3 has a pixel size of 0"); + return TAPE_E_UEF_INLAY_SCAN_ZERO; + } + i = 5; + if ((8==(*bpp_out)) && ! (*grey_out)) { + *palette_pos_out = i; + i += 768; /* palette */ + } + if (c->len != (i + len_body)) { + log_warn("uef: chunk type &3 has bad length (%u, expected %u)", c->len, (i + len_body)); + return TAPE_E_UEF_CHUNKLEN_0003; + } + return TAPE_E_OK; +} + + +static int chunk_verify_length (const uef_chunk_t * const c) { + + uint32_t len_bytes, len_bits; + uint32_t palette_pos; /* chunk 3 */ + uint16_t w,h; /* chunk 3 */ + uint8_t bpp; /* chunk 3 */ + bool 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 */ + /* TOHv4.3-a3: an empty instructions chunk is probably OK. + * Permit len=0. + * Can envisage a situation where some graphical UEF chunk + * editor exists, and the "instructions" field is just left blank. + */ + /* + 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 */ + e = chunk_3_parse_header (c, &w, &h, &bpp, &grey, &palette_pos); + if (TAPE_E_UEF_INLAY_SCAN_BPP == e) { + e = TAPE_E_OK; /* ignore unsupported inlay scan formats */ + } + if (TAPE_E_OK != e) { return e; } + } 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; + +} + + - 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(); + +int uef_clone (uef_state_t * const out, const uef_state_t * const 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; +} + +void uef_rewind (uef_state_t * const u) { + u->cur_chunk = -1; + /* reset baud to 1200 */ + init_bitsource(&(u->bitsrc), 1200); +} + +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 * const c, uint8_t const 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 1000 + 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: BUG: chunk #%u has illegal length %u", cn, c->len); + e = TAPE_E_BUG; + break; + } + if ((0==c->type) && (0==c->len)) { + log_warn("uef: write: BUG: Refusing to save chunk w/ix %d type &0 w/length 0", cn); + 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; + +} + +int uef_ffwd_to_end (uef_state_t * const u) { + + uef_chunk_t *uc; + int e,i; + char dummy; + + /* force read pointer past end of file, EOF */ + + /* TOHv4.3 edition: don't cheat */ + if (u->num_chunks < 1) { return TAPE_E_OK; } + u->cur_chunk = u->num_chunks - 1; + uc = u->chunks + u->cur_chunk; + if ( (0x100 == uc->type) + || (0x102 == uc->type) + || (0x104 == uc->type) + || (0x114 == uc->type) ) { + u->bitsrc.src_byte_pos = uc->len; + /* chunk 110, 112, 116 */ + } else if ( (0x116 == uc->type) + || (0x112 == uc->type) + || (0x110 == uc->type) ) { + u->bitsrc.nodata_consumed_pre_2400ths = uc->nodata_total_pre_2400ths; + /* chunk 111 */ + } else if (0x111 == uc->type) { + u->bitsrc.nodata_consumed_pre_2400ths = uc->nodata_total_pre_2400ths; + u->bitsrc.nodata_consumed_post_2400ths = uc->nodata_total_post_2400ths; + } + /* ensure EOF is provoked */ + for (i=0,e=0;(i<20)&&(TAPE_E_EOF != e);i++) { + e = uef_read_1200th (u, &dummy, false, NULL, NULL, NULL); + } + if (TAPE_E_EOF != e) { + log_warn("UEF: ffwd to end: BUG: eof has not successfully been provoked (code %d)", e); + return TAPE_E_BUG; + } + return TAPE_E_OK; +} + + + +/* + * 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. + * + * 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. But frankly that will be the least of your worries if you + * are serious about inserting audio at an arbitrary point in the tape. */ +void uef_scan_backwards_for_chunk_117 (uef_state_t *u, + int32_t start_chunk_num, /* now passed in */ + int32_t *prevailing_nominal_baud_out) { + + int32_t i; + + *prevailing_nominal_baud_out = 1200; + + for (i = start_chunk_num; (i >= 0) && (0x117 != u->chunks[i].type); i--) + { } + + if (i >= 0) { + *prevailing_nominal_baud_out = (int32_t) *((uint16_t *) u->chunks[i].data); + } + +} + + +#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; + } + break; + case UTF8_MODE_1110: /* three bytes total */ + switch (bc) { + case 0: + buffer[0]=((char)(i & 0x0f))<<4; + bc++; + break; + case 1: + buffer[0]|=((char)(i & 0x3c))>>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; } - 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 >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 at end of first pass */ + u->text = realloc(u->text, (sizeof(uint32_t) * (1 + j))); + } + + } /* next pass */ + + 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; +} + +int uef_change_current_chunk (uef_state_t * const uef, int32_t const chunk_ix) { + int32_t baud; + if ((chunk_ix >= uef->num_chunks) || (chunk_ix < 0)) { + log_warn("uef: BUG: uef_change_current_chunk to illegal chunk %d (%d available)", + chunk_ix, uef->num_chunks); + return TAPE_E_BUG; + } + /* scan for most recent baud chunk */ + uef_scan_backwards_for_chunk_117(uef, chunk_ix, &baud); + uef->cur_chunk = chunk_ix; + init_bitsource (&(uef->bitsrc), baud); + return TAPE_E_OK; } +#endif diff -uN b-em-0b6f1d2-vanilla/src/uef.h b-em-0b6f1d2-TOH/src/uef.h --- b-em-0b6f1d2-vanilla/src/uef.h 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/uef.h 2025-10-31 16:50:22.330929784 +0000 @@ -1,11 +1,421 @@ +/* + * B-Em + * This file (C) 2025 'Diminished' + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + #ifndef __INC_UEF_H #define __INC_UEF_H -void uef_load(const char *fn); +#include "tape2.h" +#include "acia.h" /* for serial_framing stuff */ +#include "tapeseek.h" + +/* 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 + + + +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; + tape_interval_t elapsed; /* TOHv4.3 */ + /* former bitsrc fields moved onto chunk in TOHv4.3; + express leader/silence durations, and chunk 114's num. cycles */ + uint32_t nodata_total_pre_2400ths; /* chunk 110, 111, 112, 116 */ + uint32_t nodata_total_post_2400ths; /* chunk 111 only */ + uint32_t chunk_114_total_cycs; /* 24-bit value, &114's first 3 bytes */ + +} 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 { + uint16_t w; + uint16_t h; + uint8_t bpp; + bool grey; + uint32_t body_len; + char *body; /* points into UEF chunk */ + char *palette; /* points into UEF chunk */ +} 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 currently-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. */ + + /* 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_consumed_pre_2400ths; /* chunk 110, 111, 112, 116: */ + uint32_t nodata_consumed_post_2400ths; /* 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_consumed_cycs; /* count of total consumed cycles */ + char chunk_114_pulsewaves[2]; /* &114 bytes 4 & 5, 'P' / 'W' */ + + /* + * 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 tones. 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; + void uef_close(void); -void uef_poll(void); + +/* void uef_poll(void); */ + void uef_findfilenames(void); -extern int uef_toneon; +void uef_clear_chunk (uef_chunk_t * const c); /* TOHv4.3 */ + +int uef_get_elapsed_1200ths (const uef_state_t * const u, + int32_t * const time_1200ths_out); + +int uef_read_1200th (uef_state_t * const u, + char * const out_1200th_or_null, + uef_meta_t * const metadata_list, /* caller must call uef_metadata_list_finish() */ + bool const initial_scan, + uint32_t * const metadata_fill_out, + /* TOHv4.3: usually returns -1, but may return accurate timestamp: */ + int32_t * const updated_elapsed_1200ths_out_or_null); + +void uef_finish (uef_state_t *u); + +int uef_clone (uef_state_t * const out, const uef_state_t * const in); + +void uef_rewind (uef_state_t * const u); + +int uef_load_file (const char *fn, uef_state_t *uef); + +bool uef_peek_eof (const uef_state_t * const uef); + +void uef_force_eof (uef_state_t * const uef); /* TOHv4.3 */ + +void uef_metadata_list_finish (uef_meta_t metadata_list[UEF_MAX_METADATA], + uint32_t fill); + +int uef_store_chunk ( uef_state_t * const uef, + const uint8_t * const buf, + uint16_t const type, + uint32_t const len, + uint32_t const offset, /* TOHv4.2: add offset */ + /* extra metadata: */ + uint32_t const nodata_pre_2400ths, /* TOHv4.3: needed for chunks &110, 111, 112, 116 */ + uint32_t const nodata_post_2400ths, /* TOHv4.3: needed for chunk &111 only */ + uint32_t const cycs_114, /* TOHv4.3: chunk &114's 24-bit length field */ + const tape_interval_t * const elapsed); + +int uef_append_byte_to_chunk (uef_chunk_t * const c, uint8_t const b); + +int uef_build_output (uef_state_t *u, char **out, size_t *len_out); + +int uef_detect_magic ( uint8_t * const buf, + uint32_t const buflen, + const char * const filename, + bool const show_errors); + +int uef_ffwd_to_end (uef_state_t * const u); + +void uef_scan_backwards_for_chunk_117 (uef_state_t *u, + int32_t start_chunk_num, /* now passed in */ + int32_t *prevailing_nominal_baud_out); + +int uef_get_117_payload_for_nominal_baud (int32_t const nominal_baud, + const char ** const 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 */ + +int uef_seek (uef_state_t *u, + int32_t time_1200ths_wanted, + int32_t *time_1200ths_actual_out) ; /* TOHv4.3 */ + +int uef_get_duration_1200ths (const uef_state_t * const uef, + int32_t * const duration_1200ths_out); /* TOHv4.3 */ + +int uef_change_current_chunk (uef_state_t * const uef, int32_t const chunk_ix); + +int uef_verify_timestamps (const uef_state_t * const uef); /* TOHv4.3-a4 */ -#endif +#endif /* __INC_UEF_H */ diff -uN b-em-0b6f1d2-vanilla/src/vidalleg.c b-em-0b6f1d2-TOH/src/vidalleg.c --- b-em-0b6f1d2-vanilla/src/vidalleg.c 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/vidalleg.c 2025-10-20 10:58:16.294564348 +0100 @@ -600,7 +600,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 -uN b-em-0b6f1d2-vanilla/src/video.c b-em-0b6f1d2-TOH/src/video.c --- b-em-0b6f1d2-vanilla/src/video.c 2025-10-10 11:22:34.000000000 +0100 +++ b-em-0b6f1d2-TOH/src/video.c 2025-10-27 03:54:36.494808536 +0000 @@ -1277,8 +1277,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;