// license:BSD-3-Clause // copyright-holders:Nathan Woods /**************************************************************************** opresolv.cpp Extensible ranged option resolution handling ****************************************************************************/ #include "opresolv.h" #include "strformat.h" #include #include #include #include #include namespace util { /*************************************************************************** option_resolution ***************************************************************************/ // ------------------------------------------------- // ctor // ------------------------------------------------- option_resolution::option_resolution(const option_guide &guide) { // reserve space for entries m_entries.reserve(guide.entries().size()); // initialize each of the entries; can't use foreach because we need to scan the // ENUM_VALUE entries for (auto iter = guide.entries().begin(); iter != guide.entries().end(); iter++) { // create the entry m_entries.emplace_back(*iter); entry &entry(m_entries.back()); // if this is an enumeration, identify the values if (iter->type() == option_guide::entry::option_type::ENUM_BEGIN) { // enum values begin after the ENUM_BEGIN auto enum_value_begin = iter + 1; auto enum_value_end = enum_value_begin; // and identify all entries of type ENUM_VALUE while (enum_value_end != guide.entries().end() && enum_value_end->type() == option_guide::entry::option_type::ENUM_VALUE) { iter++; enum_value_end++; } // set the range entry.set_enum_value_range(enum_value_begin, enum_value_end); } } } // ------------------------------------------------- // dtor // ------------------------------------------------- option_resolution::~option_resolution() { } // ------------------------------------------------- // lookup_in_specification // ------------------------------------------------- const char *option_resolution::lookup_in_specification(const char *specification, const option_guide::entry &option) { const char *s; s = strchr(specification, option.parameter()); return s ? s + 1 : nullptr; } // ------------------------------------------------- // set_specification - sets the option specification // and mutates values accordingly // ------------------------------------------------- void option_resolution::set_specification(const std::string &specification) { for (auto &entry : m_entries) { // find this entry's info in the specification auto entry_specification = lookup_in_specification(specification.c_str(), entry.m_guide_entry); // parse this entry's specification (e.g. - set up ranges and defaults) entry.parse_specification(entry_specification); // is this a range typed that needs to be defaulted for the first time? if (entry.is_pertinent() && entry.is_ranged() && entry.value().empty()) { entry.set_value(entry.default_value()); } } } // ------------------------------------------------- // find // ------------------------------------------------- option_resolution::entry *option_resolution::find(int parameter) { auto iter = std::find_if( m_entries.begin(), m_entries.end(), [parameter](const entry &e) { return e.parameter() == parameter; }); return iter != m_entries.end() ? &*iter : nullptr; } // ------------------------------------------------- // find // ------------------------------------------------- option_resolution::entry *option_resolution::find(const std::string &identifier) { auto iter = std::find_if( m_entries.begin(), m_entries.end(), [&](const entry &e) { return !strcmp(e.identifier(), identifier.c_str()); }); return iter != m_entries.end() ? &*iter : nullptr; } // ------------------------------------------------- // lookup_int // ------------------------------------------------- int option_resolution::lookup_int(int parameter) { auto entry = find(parameter); assert(entry != nullptr); return entry->value_int(); } // ------------------------------------------------- // lookup_string // ------------------------------------------------- const std::string &option_resolution::lookup_string(int parameter) { auto entry = find(parameter); assert(entry != nullptr); return entry->value(); } // ------------------------------------------------- // error_string // ------------------------------------------------- option_resolution::error option_resolution::get_default(const char *specification, int option_char, int *val) { // NYI return error::INTERNAL; } // ------------------------------------------------- // error_string // ------------------------------------------------- const char *option_resolution::error_string(option_resolution::error err) { switch (err) { case error::SUCCESS: return "The operation completed successfully"; case error::OUTOFMEMORY: return "Out of memory"; case error::PARAMOUTOFRANGE: return "Parameter out of range"; case error::PARAMNOTSPECIFIED: return "Parameter not specified"; case error::PARAMNOTFOUND: return "Unknown parameter"; case error::PARAMALREADYSPECIFIED: return "Parameter specified multiple times"; case error::BADPARAM: return "Invalid parameter"; case error::SYNTAX: return "Syntax error"; case error::INTERNAL: return "Internal error"; } return nullptr; } // ------------------------------------------------- // entry::ctor // ------------------------------------------------- option_resolution::entry::entry(const option_guide::entry &guide_entry) : m_guide_entry(guide_entry), m_is_pertinent(false) { } // ------------------------------------------------- // entry::set_enum_value_range // ------------------------------------------------- void option_resolution::entry::set_enum_value_range(const option_guide::entry *begin, const option_guide::entry *end) { m_enum_value_begin = begin; m_enum_value_end = end; } // ------------------------------------------------- // entry::parse_specification // ------------------------------------------------- void option_resolution::entry::parse_specification(const char *specification) { // clear some items out m_ranges.clear(); m_default_value.clear(); // is this even pertinent? m_is_pertinent = (specification != nullptr) && (specification[0] != '\0'); if (m_is_pertinent) { int value = 0; bool in_range = false; bool in_default = false; bool default_specified = false; bool half_range = false; size_t pos = 0; m_ranges.emplace_back(); while (specification[pos] && !isalpha(specification[pos])) { if (specification[pos] == '-') { // range specifier assert(!in_range && !in_default && "Syntax error in specification"); in_range = true; pos++; m_ranges.back().max = -1; if (!half_range) { m_ranges.back().min = -1; half_range = true; } } else if (specification[pos] == '[') { // begin default value assert(!in_default && !default_specified && "Syntax error in specification"); in_default = true; pos++; } else if (specification[pos] == ']') { // end default value assert(in_default && "Syntax error in specification"); in_default = false; default_specified = true; pos++; m_default_value = numeric_value(value); } else if (specification[pos] == '/') { // value separator assert(!in_default && !in_range && "Syntax error in specification"); pos++; // if we are spitting out ranges, complete the range if (half_range) { m_ranges.emplace_back(); half_range = false; } } else if (specification[pos] == ';') { // basic separator pos++; } else if (isdigit(specification[pos])) { // numeric value */ value = 0; do { value *= 10; value += specification[pos++] - '0'; } while (isdigit(specification[pos])); if (!half_range) { m_ranges.back().min = value; half_range = true; } m_ranges.back().max = value; in_range = false; } else { // invalid character - abort because we cannot recover from this syntax error throw false; } } // we can't have zero length guidelines strings assert(pos > 0); // appease compiler for scenarios where assert() has been preprocessed out (void)(in_range && in_default && default_specified); } } // ------------------------------------------------- // entry::numeric_value // ------------------------------------------------- std::string option_resolution::entry::numeric_value(int value) { return string_format("%d", value); } // ------------------------------------------------- // entry::value // ------------------------------------------------- const std::string &option_resolution::entry::value() const { static std::string empty_string; return is_pertinent() ? m_value : empty_string; } // ------------------------------------------------- // entry::value_int // ------------------------------------------------- int option_resolution::entry::value_int() const { return is_pertinent() ? atoi(m_value.c_str()) : -1; } // ------------------------------------------------- // entry::set_value // ------------------------------------------------- bool option_resolution::entry::set_value(const std::string &value) { // reject the value if this isn't pertinent if (!is_pertinent()) return false; // if this is ranged, check against the ranges if (is_ranged() && find_in_ranges(std::atoi(value.c_str())) == m_ranges.cend()) return false; // looks good! change the value m_value = value; return true; } // ------------------------------------------------- // entry::can_bump_lower // ------------------------------------------------- bool option_resolution::entry::can_bump_lower() const { return !m_ranges.empty() && value_int() > m_ranges.front().min; } // ------------------------------------------------- // entry::can_bump_higher // ------------------------------------------------- bool option_resolution::entry::can_bump_higher() const { return !m_ranges.empty() && value_int() < m_ranges.back().max; } // ------------------------------------------------- // entry::bump_lower // ------------------------------------------------- bool option_resolution::entry::bump_lower() { auto old_value = value_int(); auto current_range = find_in_ranges(old_value); assert(current_range != m_ranges.end()); int new_value; if (old_value > current_range->min) { // decrement within current range new_value = old_value - 1; } else if (current_range != m_ranges.begin()) { // go to the top of the previous range new_value = (current_range - 1)->max; } else { // at the minimum; don't bump new_value = old_value; } set_value(new_value); return new_value != old_value; } // ------------------------------------------------- // entry::bump_higher // ------------------------------------------------- bool option_resolution::entry::bump_higher() { auto old_value = value_int(); auto current_range = find_in_ranges(old_value); assert(current_range != m_ranges.end()); int new_value; if (old_value < current_range->max) { // increment within current range new_value = old_value + 1; } else if (current_range != (m_ranges.end() - 1)) { // go to the bottom of the next range new_value = (current_range + 1)->min; } else { // at the minimum; don't bump new_value = old_value; } set_value(new_value); return new_value != old_value; } // ------------------------------------------------- // entry::find_in_ranges // ------------------------------------------------- option_resolution::entry::rangelist::const_iterator option_resolution::entry::find_in_ranges(int value) const { return std::find_if( m_ranges.begin(), m_ranges.end(), [&](const auto &r) { return r.min <= value && value <= r.max; }); } } // namespace util 80 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391
// license:BSD-3-Clause
// copyright-holders:Robbbert
/********************************************************************

Support for SOL-20 cassette images


SOL20 tapes consist of these sections:
1. A high tone whenever idle
2. A header
3. The data, in blocks of 256 bytes plus a CRC byte
4. The last block may be shorter, depending on the number of bytes
   left to save.

Each byte has 1 start bit, 8 data bits (0-7), 2 stop bits.

The default speed is 1200 baud, which is what we emulate here.
A high bit is 1 cycle of 1200 Hz, while a low bit is half a cycle
of 600 Hz.

Formats:
SVT - The full explanation may be found on the Solace web site,
      however this is a summary of what we support.
      C (carrier) time in decaseconds
      D (data bytes) in ascii text
      H (header) tape header info
      Multiple programs
      Unsupported:
      B (set baud rate) B 300 or B 1200
      F load ENT file
      S (silence) time in decaseconds
      bad-byte symbols
      escaped characters

********************************************************************/
#include "sol_cas.h"

#include <cassert>


#define WAVEENTRY_LOW  -32768
#define WAVEENTRY_HIGH  32767

#define SOL20_WAV_FREQUENCY   4800

// image size
static uint32_t sol20_image_size; // FIXME: global variable prevent multiple instances
static bool level;
static uint8_t sol20_cksm_byte;
static uint32_t sol20_byte_num;
static uint8_t sol20_header[16];

static int sol20_put_samples(int16_t *buffer, int sample_pos, int count)
{
	if (buffer)
	{
		for (int i=0; i<count; i++)
			buffer[sample_pos + i] = level ? WAVEENTRY_LOW : WAVEENTRY_HIGH;

		level ^= 1;
	}

	return count;
}

static int sol20_output_bit(int16_t *buffer, int sample_pos, bool bit)
{
	int samples = 0;

	if (bit)
	{
		samples += sol20_put_samples(buffer, sample_pos + samples, 2);
		samples += sol20_put_samples(buffer, sample_pos + samples, 2);
	}
	else
	{
		samples += sol20_put_samples(buffer, sample_pos + samples, 4);
	}

	return samples;
}

static int sol20_output_byte(int16_t *buffer, int sample_pos, uint8_t byte)
{
	int samples = 0;
	uint8_t i;

	/* start */
	samples += sol20_output_bit (buffer, sample_pos + samples, 0);

	/* data */
	for (i = 0; i<8; i++)
		samples += sol20_output_bit (buffer, sample_pos + samples, (byte >> i) & 1);

	/* stop */
	for (i = 0; i<2; i++)
		samples += sol20_output_bit (buffer, sample_pos + samples, 1);

	return samples;
}

// Calculate checksum
static uint8_t sol20_calc_cksm(uint8_t cksm, uint8_t data)
{
	data -= cksm;
	cksm = data;
	data ^= cksm;
	data ^= 0xff;
	data -= cksm;
	return data;
}

// Ignore remainder of line
static void sol20_scan_to_eol(const uint8_t *bytes)
{
	bool t = 1;
	while (t)
	{
		if (sol20_byte_num >= sol20_image_size)
		{
			sol20_byte_num = 0;
			t = 0;
		}
		else
		if (bytes[sol20_byte_num] == 0x0d)
			t = 0;
		else
			sol20_byte_num++;
	}
}

// skip spaces and symbols looking for a hex digit
static void sol20_scan_to_hex(const uint8_t *bytes)
{
	bool t = 1;
	while (t)
	{
		if (sol20_byte_num >= sol20_image_size)
		{
			sol20_byte_num = 0;
			t = 0;
		}
		else
		{
			uint8_t chr = bytes[sol20_byte_num];
			if (chr == 0x0d)
				t = 0;
			else
			if (((chr >= '0') && (chr <= '9')) || ((chr >= 'A') && (chr <= 'F')))
				t = 0;
			else
				sol20_byte_num++;
		}
	}
}

// Turn n digits into hex
static int sol20_read_hex(const uint8_t *bytes, uint8_t numdigits)
{
	int data = 0;
	uint8_t i,chr;

	for (i = 0; i < numdigits; i++)
	{
		chr = bytes[sol20_byte_num];
		if ((chr >= '0') && (chr <= '9'))
		{
			data = (data << 4) | (chr-48);
			sol20_byte_num++;
		}
		else
		if ((chr >= 'A') && (chr <= 'F'))
		{
			data = (data << 4) | (chr-55);
			sol20_byte_num++;
		}
		else
			i = numdigits;
	}
	return data;
}

// Turn digits into decimal
static int sol20_read_dec(const uint8_t *bytes)
{
	int data = 0;

	while ((bytes[sol20_byte_num] >= '0') && (bytes[sol20_byte_num] <= '9'))
	{
		data = data*10 + bytes[sol20_byte_num] - 48;
		sol20_byte_num++;
	}

	return data;
}

static int sol20_handle_cassette(int16_t *buffer, const uint8_t *bytes)
{
	uint32_t sample_count = 0;
	uint32_t i = 0,t = 0;
	uint16_t cc = 0;
	sol20_byte_num = 1;
	bool process_d = 0;
	uint16_t length = 0;

	// 1st line of file must say SVT
	if ((bytes[0] == 'S') && (bytes[1] == 'V') && (bytes[2] == 'T'))
	{ }
	else
		return sample_count;

	// ignore remainder of line
	sol20_scan_to_eol(bytes);

	// process the commands
	while (sol20_byte_num)
	{
		sol20_byte_num+=2; // bump to start of next line
		uint8_t chr = bytes[sol20_byte_num];  // Get command
		if (sol20_byte_num >= sol20_image_size)
			sol20_byte_num = 0;
		else
		{
			switch (chr)
			{
				case 0x0d:
					break;
				case 'C': // carrier
					{
						if (cc) // if this is the next file, clean up after the previous one
						{
							sample_count += sol20_output_byte(buffer, sample_count, sol20_cksm_byte); // final checksum if needed
							cc = 0;
						}

						sol20_byte_num+=2; // bump to parameter
						t = sol20_read_dec(bytes) * 140; // convert 10th of seconds to number of ones
						for (i = 0; i < t; i++)
							sample_count += sol20_output_bit(buffer, sample_count, 1);
						sol20_scan_to_eol(bytes);
						break;
					}
				case 'H': // header
					{
						if (cc) // if this is the next file, clean up after the previous one
						{
							sample_count += sol20_output_byte(buffer, sample_count, sol20_cksm_byte); // final checksum if needed
							cc = 0;
						}

						sol20_byte_num+=2; // bump to file name
						for (i = 0; i < 5; i++)
							sol20_header[i] = 0x20;
						for (i = 0; i < 5; i++)
						{
							sol20_header[i] = bytes[sol20_byte_num++];
							if (sol20_header[i] == 0x20)
								break;
						}
						sol20_header[5] = 0;
						sol20_scan_to_hex(bytes); // bump to file type
						sol20_header[6] = sol20_read_hex(bytes, 2);
						sol20_scan_to_hex(bytes); // bump to length
						length = sol20_read_hex(bytes, 4);
						sol20_header[7] = length;
						sol20_header[8] = length >> 8;
						sol20_scan_to_hex(bytes); // bump to load-address
						i = sol20_read_hex(bytes, 4);
						sol20_header[9] = i;
						sol20_header[10] = i >> 8;
						sol20_scan_to_hex(bytes); // bump to exec-address
						i = sol20_read_hex(bytes, 4);
						sol20_header[11] = i;
						sol20_header[12] = i >> 8;
						sol20_header[13] = 0;
						sol20_header[14] = 0;
						sol20_header[15] = 0;
						sol20_cksm_byte = 0;
						for (i = 0; i < 16; i++)
							sol20_cksm_byte = sol20_calc_cksm(sol20_cksm_byte, sol20_header[i]);
						// write leader
						for (i = 0; i < 100; i++)
							sample_count += sol20_output_byte(buffer, sample_count, 0);
						// write SOH
						sample_count += sol20_output_byte(buffer, sample_count, 1);
						// write Header
						for (i = 0; i < 16; i++)
							sample_count += sol20_output_byte(buffer, sample_count, sol20_header[i]);
						// write checksum
						sample_count += sol20_output_byte(buffer, sample_count, sol20_cksm_byte);

						sol20_cksm_byte = 0;
						process_d = 1;
						sol20_scan_to_eol(bytes);
						break;
					}
				case 'D':  // data
					{
						sol20_byte_num+=2; // bump to first byte
						while ((bytes[sol20_byte_num] != 0x0d) && sol20_byte_num && process_d)
						{
							t = sol20_read_hex(bytes, 2);
							sample_count += sol20_output_byte(buffer, sample_count, t);
							cc++;
							// if it's a data byte reduce remaining length and calculate checksum;
							// tape supplies checksums except last one
							if (cc < 257)
							{
								length--;
								sol20_cksm_byte = sol20_calc_cksm(sol20_cksm_byte, t);
							}
							else
							// didnt need it, throw away
							{
								cc = 0;
								sol20_cksm_byte = 0;
							}
							// see if finished tape
							if (!length)
								process_d = 0;
							// bump to next byte
							sol20_scan_to_hex(bytes);
						}
					}
					[[fallthrough]];
				default:  // everything else is ignored
					sol20_scan_to_eol(bytes);
					break;
			}
		}
	}

	if (cc)  // reached the end of the svt file
		sample_count += sol20_output_byte(buffer, sample_count, sol20_cksm_byte); // final checksum if needed

	return sample_count;
}


/*******************************************************************
   Generate samples for the tape image
********************************************************************/

static int sol20_cassette_fill_wave(int16_t *buffer, int length, uint8_t *bytes)
{
	return sol20_handle_cassette(buffer, bytes);
}

/*******************************************************************
   Calculate the number of samples needed for this tape image
********************************************************************/

static int sol20_cassette_calculate_size_in_samples(const uint8_t *bytes, int length)
{
	sol20_image_size = length;

	return sol20_handle_cassette(nullptr, bytes);
}

static const cassette_image::LegacyWaveFiller sol20_legacy_fill_wave =
{
	sol20_cassette_fill_wave,                 /* fill_wave */
	-1,                                     /* chunk_size */
	0,                                      /* chunk_samples */
	sol20_cassette_calculate_size_in_samples, /* chunk_sample_calc */
	SOL20_WAV_FREQUENCY,                      /* sample_frequency */
	0,                                      /* header_samples */
	0                                       /* trailer_samples */
};

static cassette_image::error sol20_cassette_identify(cassette_image *cassette, cassette_image::Options *opts)
{
	return cassette->legacy_identify(opts, &sol20_legacy_fill_wave);
}

static cassette_image::error sol20_cassette_load(cassette_image *cassette)
{
	return cassette->legacy_construct(&sol20_legacy_fill_wave);
}

static const cassette_image::Format sol20_cassette_image_format =
{
	"svt",
	sol20_cassette_identify,
	sol20_cassette_load,
	nullptr
};

CASSETTE_FORMATLIST_START(sol20_cassette_formats)
	CASSETTE_FORMAT(sol20_cassette_image_format)
CASSETTE_FORMATLIST_END