rewrite cassette handling: wave files, separate in/out ports

This commit is contained in:
Christopher Mosher 2019-01-21 00:12:47 -05:00
parent e296b6a6f9
commit c787dbb80c
17 changed files with 771 additions and 304 deletions

View File

@ -11,7 +11,8 @@ epple2_LDFLAGS=$(all_libraries)
epple2_CPPFLAGS = $(AM_CPPFLAGS) -DETCDIR=\"$(sysconfdir)\"
epple2_SOURCES = a2colorsobserved.cpp addressbus.cpp analogtv.cpp apple2.cpp \
applentsc.cpp card.cpp cassette.cpp clipboardhandler.cpp clockcard.cpp \
applentsc.cpp card.cpp cassette.cpp cassettein.cpp cassetteout.cpp \
clipboardhandler.cpp clockcard.cpp \
configep2.cpp cpu.cpp diskbytes.cpp diskcontroller.cpp drive.cpp drivemotor.cpp \
emptyslot.cpp emulator.cpp firmwarecard.cpp gui.cpp hypermode.cpp \
keyboard.cpp keyboardbuffermode.cpp languagecard.cpp lowpass_1_5_mhz.cpp \
@ -26,7 +27,8 @@ StateCalculator.cpp Trace.cpp TransCache.cpp TransNetwork.cpp \
tinyfiledialogs.cpp
noinst_HEADERS = a2colorsobserved.h addressbus.h analogtv.h apple2.h applentsc.h \
card.h cassette.h clipboardhandler.h clockcard.h configep2.h cpu.h diskbytes.h \
card.h cassette.h cassettein.h cassetteout.h \
clipboardhandler.h clockcard.h configep2.h cpu.h diskbytes.h \
diskcontroller.h drive.h drivemotor.h e2const.h emptyslot.h emulator.h firmwarecard.h font3x5.h gui.h \
hypermode.h keyboardbuffermode.h keyboard.h languagecard.h lowpass_1_5_mhz.h \
lowpass_3_58_mhz.h lss.h memory.h paddlebuttonstates.h paddles.h picturegenerator.h \

View File

@ -22,11 +22,12 @@
#include "paddles.h"
#include "paddlebuttonstates.h"
#include "speakerclicker.h"
#include "cassette.h"
#include "cassettein.h"
#include "cassetteout.h"
#include "slots.h"
AddressBus::AddressBus(Memory& ram, Memory& rom, Keyboard& kbd, VideoMode& vid, Paddles& paddles, PaddleButtonStates& paddleButtonStates, SpeakerClicker& speaker, Cassette& cassette, Slots& slts):
ram(ram), rom(rom), kbd(kbd), vid(vid), paddles(paddles), paddleButtonStates(paddleButtonStates), speaker(speaker), cassette(cassette), slts(slts)
AddressBus::AddressBus(Memory& ram, Memory& rom, Keyboard& kbd, VideoMode& vid, Paddles& paddles, PaddleButtonStates& paddleButtonStates, SpeakerClicker& speaker, CassetteIn& cassetteIn, CassetteOut& cassetteOut, Slots& slts):
ram(ram), rom(rom), kbd(kbd), vid(vid), paddles(paddles), paddleButtonStates(paddleButtonStates), speaker(speaker), cassetteIn(cassetteIn), cassetteOut(cassetteOut), slts(slts)
{
}
@ -132,7 +133,7 @@ unsigned char AddressBus::readSwitch(unsigned short address)
}
else if (islot == 0x2)
{
this->cassette.output();
this->cassetteOut.output();
}
else if (islot == 0x3)
{
@ -158,7 +159,7 @@ unsigned char AddressBus::readSwitch(unsigned short address)
int sw2 = iswch & 0x7;
if (sw2 == 0)
{
setD7(this->cassette.input());
setD7(this->cassetteIn.input());
}
else if (sw2 < 4)
{

View File

@ -24,7 +24,8 @@ class VideoMode;
class Paddles;
class PaddleButtonStates;
class SpeakerClicker;
class Cassette;
class CassetteIn;
class CassetteOut;
class Slots;
class AddressBus
@ -37,13 +38,14 @@ private:
Paddles& paddles;
PaddleButtonStates& paddleButtonStates;
SpeakerClicker& speaker;
Cassette& cassette;
Slots& slts;
CassetteIn& cassetteIn;
CassetteOut& cassetteOut;
Slots& slts;
unsigned char data; // this emulates the (floating) data bus
public:
AddressBus(Memory& ram, Memory& rom, Keyboard& kbd, VideoMode& vid, Paddles& paddles, PaddleButtonStates& paddleButtonStates, SpeakerClicker& speaker, Cassette& cassette, Slots& slts);
AddressBus(Memory& ram, Memory& rom, Keyboard& kbd, VideoMode& vid, Paddles& paddles, PaddleButtonStates& paddleButtonStates, SpeakerClicker& speaker, CassetteIn& cassetteIn, CassetteOut& cassetteOut, Slots& slts);
~AddressBus();
unsigned char read(const unsigned short address);

View File

@ -43,8 +43,9 @@ Apple2::Apple2(KeypressQueue& keypresses, PaddleButtonStates& paddleButtonStates
kbd(keypresses,fhyper,buffered),
rom(AddressBus::MOTHERBOARD_ROM_SIZ),
ram(AddressBus::MOTHERBOARD_RAM_SIZ),
cassette(gui),
addressBus(ram,rom,kbd,videoMode,paddles,paddleButtonStates,speaker,cassette,slts),
cassetteIn(gui),
cassetteOut(gui),
addressBus(ram,rom,kbd,videoMode,paddles,paddleButtonStates,speaker,cassetteIn,cassetteOut,slts),
picgen(tv,videoMode,this->revision),
video(videoMode,addressBus,picgen,textRows),
#ifdef USE_EMU
@ -69,7 +70,8 @@ void Apple2::tick() {
this->video.tick();
this->paddles.tick();
this->speaker.tick();
this->cassette.tick();
this->cassetteIn.tick();
this->cassetteOut.tick();
if (this->revision > 0) {
this->powerUpReset.tick();

View File

@ -33,7 +33,8 @@
#include "speakerclicker.h"
#include "analogtv.h"
#include "powerupreset.h"
#include "cassette.h"
#include "cassettein.h"
#include "cassetteout.h"
#include "Emu6502.h"
#include <fstream>
class Emulator;
@ -51,8 +52,9 @@ class Apple2 : public Timable
SpeakerClicker speaker;
Memory rom;
Memory ram;
Cassette cassette;
AddressBus addressBus;
CassetteIn cassetteIn;
CassetteOut cassetteOut;
AddressBus addressBus;
PictureGenerator picgen;
TextCharacters textRows;
Video video;

View File

@ -1,6 +1,7 @@
/*
epple2
Copyright (C) 2008 by Christopher A. Mosher <cmosher01@gmail.com>
Copyright © 2008, 2019, Christopher Alan Mosher, Shelton, CT, USA. <cmosher01@gmail.com>
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
@ -15,195 +16,97 @@
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
cassette tape image file format
-------------------------------
Each byte represents one half cycle, in 10-microsoecond units.
For example, consider the following values in the file (decimal):
65 65 65 65 65 20 25 50 50 25 25 25 25 50 50
This represents the following half-cycles (in microseconds)
650 650 650 650 650 200 250 500 500 250 250 250 250 500 500
which has the following meaning:
|--------HEADER-----|--sync-|-1-bit-|-0-bit-|-0-bit-|-1-bit-|
| | | | | | |
|650 650 650 650 650|200 250|500 500|250 250|250 250|500 500|
*/
#include <fstream>
#include <iostream>
#include <cstdlib>
#include "tinyfiledialogs.h"
#include "cassette.h"
Cassette::Cassette(ScreenImage& gui):
gui(gui), t(0), prevT(0), markT(0), positive(false)
{
unload();
gui(gui),
t(0),
t_active(0),
playing(false),
modified(false) {
}
Cassette::~Cassette() {
}
void Cassette::note(const char *n) {
std::cout << "cassette (" << port() << "): " << n << std::endl;
}
void Cassette::tick() {
if (this->playing) {
++this->t;
/*
* Automatically stop the tape if the Apple doesn't use
* it within the given number of cycles.
*/
if (this->t_active+100000 <= this->t) {
note("STOP");
std::cout << "cassette: t=" << this->t << std::endl;
this->playing = false;
}
}
}
Cassette::~Cassette()
{
// 0=cancel(abort), 1=yes(save), 2=no(discard)
static int askSave() {
return tinyfd_messageBox(
"Save changes",
"You have unsaved changes to your tape image.\nDo you want to SAVE them?",
"yesnocancel",
"warning",
0);
}
void Cassette::tick()
{
++this->t;
// TODO: check for roll-over of tick-count in Cassette???
// if (this->t == 0)
bool Cassette::eject() {
if (isLoaded()) {
if (isModified()) {
const int resp = askSave();
if (resp == 0) { // cancel
return false;
}
if (resp == 1) { // yes (save)
if (!write()) {
return false;
}
}
this->modified = false;
this->gui.setCassetteDirty(false);
}
this->file.clear();
this->playing = false;
this->t = 0;
this->t_active = 0;
}
return true;
}
void Cassette::output()
{
if (isWriteProtected() || !isLoaded())
{
return;
}
if (this->half_cycles.size() <= this->pos)
{
this->half_cycles.push_back(getHalfCycleTime());
this->pos = this->half_cycles.size();
this->gui.setCassettePos(this->pos,this->half_cycles.size());
this->modified = true;
this->gui.setCassetteDirty(true);
}
else
{
// TODO should we allow overwriting of cassestte tape data?
// If so, need to watch out because while reading Apple will
// be calling Cassette::output, too.
}
this->prevT = this->t;
void Cassette::save() {
if (isLoaded()) {
if (isModified()) {
if (write()) {
this->modified = false;
this->gui.setCassetteDirty(false);
}
}
}
}
unsigned char Cassette::getHalfCycleTime() // in 10-microsecond units
{
const unsigned int delta_cycles(this->t-this->prevT);
if (delta_cycles < 225)
return 20;
if (delta_cycles < 375)
return 25;
if (delta_cycles < 575)
return 50;
return 65;
bool Cassette::isLoaded() {
return !this->file.empty();
}
bool Cassette::input()
{
if (!isLoaded())
{
// no tape loaded
return true;
}
if (this->markT <= this->t) // we've hit our mark
{
this->positive = !this->positive;
if (this->pos < this->half_cycles.size())
{
// set our next mark to be at the end of next half-cycle read from tape
this->markT = this->t+this->half_cycles[this->pos++]*10;
this->gui.setCassettePos(this->pos,this->half_cycles.size());
}
}
return this->positive;
}
void Cassette::rewind()
{
this->pos = 0;
this->gui.setCassettePos(this->pos,this->half_cycles.size());
}
bool Cassette::newFile(const std::string& filePath)
{
std::ifstream in(filePath.c_str(),std::ios::binary|std::ios::in);
if (in.is_open())
{
in.close();
std::cerr << "Error creating file; file already exists: " << filePath << std::endl;
return false;
}
std::ofstream out(filePath.c_str(),std::ios::binary|std::ios::out);
out.close();
return load(filePath);
}
bool Cassette::load(const std::string& filePath)
{
// TODO better I/O error handling during cassette loading and saving
std::ifstream in(filePath.c_str(),std::ios::binary|std::ios::in|std::ios::ate);
if (!in.is_open())
{
std::cerr << "Error loading " << filePath << std::endl;
return false;
}
if (isLoaded())
{
unload();
}
const std::ifstream::pos_type size = in.tellg();
if (size > 0)
{
this->half_cycles.resize(size);
in.seekg(0,std::ios::beg);
in.read((char*)&this->half_cycles[0],size);
// std::cerr << "Read " << size << " bytes from " << filePath << std::endl;
}
in.close();
this->filePath = filePath;
checkForWriteProtection();
this->loaded = true;
this->modified = false;
this->gui.setCassetteFile(filePath);
this->gui.setCassetteDirty(false);
this->gui.setCassettePos(this->pos,size);
return true;
}
void Cassette::checkForWriteProtection()
{
std::ofstream outf(filePath.c_str(),std::ios::binary|std::ios::app);
this->writable = outf.is_open();
outf.close();
}
void Cassette::save()
{
if (isWriteProtected() || !isLoaded())
{
return;
}
std::ofstream out(filePath.c_str(),std::ios::binary);
out.write((char*)&this->half_cycles[0],this->half_cycles.size());
out.flush();
out.close();
this->modified = false;
this->gui.setCassetteDirty(false);
}
void Cassette::unload()
{
this->pos = 0;
this->writable = true;
this->loaded = false;
this->filePath = "";
this->modified = false;
this->half_cycles.clear();
this->gui.setCassetteFile("");
this->gui.setCassetteDirty(false);
this->gui.setCassettePos(this->pos,this->half_cycles.size());
bool Cassette::isModified() {
return this->modified;
}

View File

@ -1,83 +1,57 @@
/*
epple2
Copyright (C) 2008 by Christopher A. Mosher <cmosher01@gmail.com>
epple2
Copyright (C) 2008 by Christopher A. Mosher <cmosher01@gmail.com>
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 3 of the License, or
(at your option) any later version.
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 3 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.
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, see <http://www.gnu.org/licenses/>.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef CASSETTE_H
#define CASSETTE_H
#include <vector>
#include <string>
#include <cstdint>
#include "screenimage.h"
class Cassette
{
private:
ScreenImage& gui;
class Cassette {
protected:
ScreenImage& gui;
unsigned int t;
unsigned int prevT;
std::uint_fast32_t t;
std::uint_fast32_t t_active;
unsigned int markT;
bool positive;
bool playing; // tape is moving
std::vector<unsigned char> half_cycles;
bool modified;
std::string file;
std::string fileName;
std::string filePath;
bool writable;
bool loaded;
unsigned int pos;
bool modified;
// save all data to file, return true if it worked
virtual bool write() { return true; }
virtual std::string port() { return ""; }
void note(const char *n);
void checkForWriteProtection();
public:
Cassette(ScreenImage& gui);
virtual ~Cassette();
unsigned char getHalfCycleTime(); // in 10-microsecond units
virtual bool eject(); // returns false if user cancels operation
void save();
public:
Cassette(ScreenImage& gui);
~Cassette();
bool isLoaded();
bool isModified();
void tick();
void output();
bool input();
void rewind();
bool newFile(const std::string& filePath);
bool load(const std::string& filePath);
std::string getFileName()
{
return this->fileName;
}
bool isLoaded()
{
return this->loaded;
}
void save();
void unload();
bool isWriteProtected()
{
return !this->writable;
}
bool isModified()
{
return this->modified;
}
virtual void tick();
};
#endif

259
src/cassettein.cpp Normal file
View File

@ -0,0 +1,259 @@
/*
epple2
Copyright © 2008, 2019, Christopher Alan Mosher, Shelton, CT, USA. <cmosher01@gmail.com>
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 3 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, see <http://www.gnu.org/licenses/>.
*/
#include <fstream>
#include <iostream>
#include <cstdlib>
#include <SDL2/SDL_audio.h>
#include <SDL2/SDL_log.h>
#include "tinyfiledialogs.h"
#include "cassettein.h"
#include "e2const.h"
#include <vector>
#include <cmath>
CassetteIn::CassetteIn(ScreenImage& gui):
Cassette(gui),
samp(nullptr),
samp_siz(0),
positive(false),
slope_was(0) {
this->gui.setCassetteInFile("[empty]");
}
CassetteIn::~CassetteIn() {
}
std::string CassetteIn::port() {
return "IN";
}
void CassetteIn::note_pos() {
std::cout << "cassette: ";
std::cout << "t=" << this->t;
std::cout << ", ";
std::cout << "t_act=" << this->t_active;
if (this->playing) {
std::cout << "+";
}
std::cout << std::endl;
}
static const float MOVEMENT_THRESHOLD = 0.001f;
/* prev --> curr: rising=+1, falling=-1, flat=0 */
static std::int_fast8_t slope(const float prev, const float curr) {
if (abs(curr-prev) < MOVEMENT_THRESHOLD) {
return 0;
}
return prev < curr ? +1 : -1;
}
//static std::uint_fast32_t prev_p = 0;
//static std::uint_fast32_t ccc = 0;
void CassetteIn::tick() {
Cassette::tick();
if (this->t % 10) {
return;
}
if (this->playing) {
const std::uint_fast32_t p = this->t/10 > 0 ? this->t/10 : 1;
if (this->samp_siz <= p) {
note("END OF TAPE");
this->t = this->samp_siz*10;
this->t_active = this->t;
this->playing = false;
this->gui.setCassettePos(this->samp_siz,this->samp_siz);
note_pos();
}
this->gui.setCassettePos(p,this->samp_siz);
/*
* Reconstruct original square wave signal (from the Apple cassette output port)
* from the cassette tape's (lagging) sine wave. Assume the cassette's recorded voltage level
* lagged from its input voltage. Therefore, here we check for the sine wave BEGINNING
* to react to what we assume is the input voltage's (square wave) swing, and we use
* that as the signal to send up to the emulator.
*
* ---+ +-----
* .\ /
* . \ /
* . \ /
* . +-----+
* . .
* . .
* ---+ +---------
* | |
* | |
* | |
* +---------+
*/
std::int_fast8_t slope_is = slope(this->samp[p-1], this->samp[p]);
if (slope_is != 0 && slope_is != this->slope_was) {
this->positive = !this->positive;
// std::printf("%d ", (p-prev_p));
// if (!(++ccc % 40)) {
// std::printf("\n");
// }
// prev_p = p;
this->slope_was = slope_is;
}
}
}
bool CassetteIn::input() {
if (!isLoaded()) {
return false;
}
if (!this->playing) {
if (this->t/10 < this->samp_siz) {
note("PLAY");
this->playing = true;
note_pos();
}
}
if (this->playing) {
this->t_active = this->t;
}
return this->positive;
}
void CassetteIn::rewind() {
if (!isLoaded()) {
return;
}
note_pos();
note("REWIND");
this->t = 0;
this->t_active = 0;
this->slope_was = 0;
this->positive = false;
this->playing = false;
note_pos();
note("FAST FORWARD TO TONE");
const unsigned int HEAD_SAMPLES = 17;
std::int_fast8_t slope_was = 0;
uint i_was = 0;
uint c_head = 0;
for (std::uint_fast32_t i = 1; i < this->samp_siz; ++i) {
std::int_fast8_t slope_is = slope(this->samp[i-1], this->samp[i]);
if (slope_is) {
if (slope_is != slope_was) {
// std::printf("%d ", (i-prev_p));
// if (!(++ccc % 40)) {
// std::printf("\n");
// }
// prev_p = i;
this->positive = !this->positive;
this->slope_was = slope_is;
const std::uint_fast32_t d = i-i_was;
if (59u <= d && d <= 71u) {
++c_head;
// std::printf("=====[%d]====", c_head);
// fast-forward to the header tone
if (HEAD_SAMPLES <= c_head) {
// prev_p = i;
this->t = i*10-1;
this->t_active = this->t;
// ccc = 0;
break;
}
} else {
c_head = 0;
}
i_was = i;
slope_was = slope_is;
}
}
}
note_pos();
this->gui.setCassettePos(this->t/10,this->samp_siz);
}
bool CassetteIn::load(const std::string& filePath) {
SDL_AudioSpec wav_spec;
std::uint8_t *wav_buffer;
std::uint32_t wav_length;
if (SDL_LoadWAV(filePath.c_str(), &wav_spec, &wav_buffer, &wav_length) == nullptr) {
SDL_Log("Error: %s ; file: %s\n", SDL_GetError(), filePath.c_str());
return false;
}
// std::printf("opened WAVE file %s\n", filePath.c_str());
// std::printf(" buffer size: %d bytes\n", wav_length);
// std::printf(" sample rate: %dHz\n", wav_spec.freq);
// std::printf(" sample datatype: %04X\n", wav_spec.format);
// std::printf(" channels: %d\n", wav_spec.channels);
// std::printf(" silence value: %d\n", wav_spec.silence);
// std::printf(" sample count: %d\n", wav_spec.samples);
if (isLoaded()) {
if (!eject()) {
return false;
}
}
/* convert input sample to floating-point, at rate of 10 CPU cycles per sample, for easy calculation */
SDL_AudioCVT cvt;
SDL_BuildAudioCVT(&cvt, wav_spec.format, wav_spec.channels, wav_spec.freq, AUDIO_F32, 1, E2Const::AVG_CPU_HZ/10);
cvt.len = wav_length;
cvt.buf = reinterpret_cast<std::uint8_t*>(std::malloc(cvt.len_mult * cvt.len));
memcpy(cvt.buf, wav_buffer, cvt.len);
SDL_FreeWAV(wav_buffer);
SDL_ConvertAudio(&cvt);
this->samp = reinterpret_cast<float*>(cvt.buf);
this->samp_siz = cvt.len_cvt/4;
note("LOAD");
note_pos();
this->file = filePath;
this->gui.setCassetteInFile(filePath);
rewind();
return true;
}
bool CassetteIn::eject() {
const bool ok = Cassette::eject();
if (ok) {
this->gui.setCassetteInFile("(no tape)");
this->gui.setCassettePos(0,0);
std::free(this->samp);
this->samp_siz = 0;
}
return ok;
}

48
src/cassettein.h Normal file
View File

@ -0,0 +1,48 @@
/*
epple2
Copyright (C) 2008 by Christopher A. Mosher <cmosher01@gmail.com>
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 3 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, see <http://www.gnu.org/licenses/>.
*/
#ifndef CASSETTEIN_H
#define CASSETTEIN_H
#include <string>
#include <cstdint>
#include "cassette.h"
class CassetteIn : public Cassette {
private:
float *samp;
std::uint32_t samp_siz;
bool positive;
std::int_fast8_t slope_was;
virtual std::string port();
void note_pos();
public:
CassetteIn(ScreenImage& gui);
virtual ~CassetteIn();
virtual void tick();
bool input();
bool load(const std::string& filePath);
void rewind();
virtual bool eject();
};
#endif

178
src/cassetteout.cpp Normal file
View File

@ -0,0 +1,178 @@
/*
epple2
Copyright © 2008, 2019, Christopher Alan Mosher, Shelton, CT, USA. <cmosher01@gmail.com>
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 3 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, see <http://www.gnu.org/licenses/>.
*/
#include <fstream>
#include <iostream>
#include <cstdlib>
#include <SDL2/SDL_audio.h>
#include <SDL2/SDL_log.h>
#include "tinyfiledialogs.h"
#include "cassetteout.h"
#include "e2const.h"
#include <vector>
#include <cmath>
CassetteOut::CassetteOut(ScreenImage& gui):
Cassette(gui) {
this->gui.setCassetteOutFile("[empty]");
}
CassetteOut::~CassetteOut() {
}
std::string CassetteOut::port() {
return "OUT";
}
//void CassetteOut::note_pos() {
// std::cout << "cassette: ";
// std::cout << "t=" << this->t;
// std::cout << ", ";
// std::cout << "t(in )=" << this->playing_t;
// if (this->playing) {
// std::cout << "*";
// }
// std::cout << ", ";
// std::cout << "t(out)=" << this->recording_t;
// if (this->recording) {
// std::cout << "*";
// }
// if (this->samp_out.size()) {
// std::cout << "[" << this->samp_out.size() << "]";
// }
// std::cout << std::endl;
//}
void CassetteOut::tick() {
Cassette::tick();
}
void CassetteOut::output() {
if (isLoaded()) {
if (!this->playing) {
this->playing = true;
note("PLAY+RECORD");
// note_pos();
} else {
const std::uint32_t d = this->t-this->t_active;
this->samp_out.push_back(d);
this->modified = true;
this->gui.setCassetteDirty(true);
}
this->t_active = this->t;
}
}
bool CassetteOut::blank(const std::string& filePath) {
if (!eject()) {
return false;
}
std::ifstream in(filePath.c_str(),std::ios::binary|std::ios::in);
if (in.is_open()) {
in.close();
std::cerr << "Error creating file; file already exists: " << filePath << std::endl;
return false;
}
std::ofstream out(filePath.c_str(),std::ios::binary|std::ios::out);
out.close();
if (out) {
this->file = filePath;
this->gui.setCassetteOutFile(this->file);
this->samp_out.clear();
} else {
std::cerr << "Error creating file: " << filePath << std::endl;
}
return (bool)out;
}
bool CassetteOut::eject() {
const bool ok = Cassette::eject();
if (ok) {
this->gui.setCassetteOutFile("(no tape)");
this->samp_out.clear();
}
return ok;
}
bool CassetteOut::write() {
std::ofstream out(this->file.c_str(), std::ios::binary);
std::uint32_t c_sample = 0;
for (std::uint32_t i = 0; i < this->samp_out.size(); ++i) {
if (this->samp_out[i] < 3000) {
c_sample += this->samp_out[i]/50;
}
}
std::uint32_t longVal;
std::uint16_t wordVal;
out.write("RIFF", 4);
out.write((char*)&(longVal = 36+c_sample), 4);
out.write("WAVE", 4);
out.write("fmt ", 4);
out.write((char*)&(longVal = 16), 4);
out.write((char*)&(wordVal = 1), 2); // PCM
out.write((char*)&(wordVal = 1), 2); // mono, one channel
out.write((char*)&(longVal = E2Const::AVG_CPU_HZ/50), 4); // samples per second
out.write((char*)&(longVal = E2Const::AVG_CPU_HZ/50), 4); // byte rate (same)
out.write((char*)&(wordVal = 1), 2); // alignment
out.write((char*)&(wordVal = 8), 2); // bits per sample
out.write("data", 4);
out.write((char*)&(longVal = c_sample), 4);
const float pi = acos(-1);
bool positive = false;
for (std::uint32_t i = 0; i < this->samp_out.size(); ++i) {
if (this->samp_out[i] < 3000) {
for (std::uint_fast8_t s = 0; s < this->samp_out[i]/50; ++s) {
float x = sin(pi/2 + 50*pi*s/this->samp_out[i]);
if (!positive) {
x = -1.0f*x;
}
x = round(128+128*x);
if (x > 255.0f) {
x = 255.0f;
}
if (x < 0.0f) {
x = 0.0f;
}
std::uint8_t bx = x;
out.write((char*)&bx, 1);
}
}
positive = !positive;
}
out.flush();
out.close();
return (bool)out;
}

44
src/cassetteout.h Normal file
View File

@ -0,0 +1,44 @@
/*
epple2
Copyright (C) 2008 by Christopher A. Mosher <cmosher01@gmail.com>
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 3 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, see <http://www.gnu.org/licenses/>.
*/
#ifndef CASSETTEOUT_H
#define CASSETTEOUT_H
#include <vector>
#include <string>
#include <cstdint>
#include "cassette.h"
class CassetteOut : public Cassette {
private:
std::vector<std::uint32_t> samp_out;
virtual std::string port();
public:
CassetteOut(ScreenImage& gui);
virtual ~CassetteOut();
virtual void tick();
void output();
bool blank(const std::string& filePath);
virtual bool write();
virtual bool eject();
};
#endif

View File

@ -25,7 +25,8 @@
#include "standardout.h"
#include "standardin.h"
#include "clockcard.h"
#include "cassette.h"
#include "cassettein.h"
#include "cassetteout.h"
#include "tinyfiledialogs.h"
#include <iostream>
@ -74,7 +75,7 @@ static void trim(std::string& str)
}
}
void Config::parse(Memory& ram, Memory& rom, Slots& slts, int& revision, ScreenImage& gui, Cassette& cassette)
void Config::parse(Memory& ram, Memory& rom, Slots& slts, int& revision, ScreenImage& gui, CassetteIn& cassetteIn, CassetteOut& cassetteOut)
{
std::ifstream* pConfig;
@ -144,7 +145,7 @@ void Config::parse(Memory& ram, Memory& rom, Slots& slts, int& revision, ScreenI
trim(line);
if (!line.empty())
{
parseLine(line,ram,rom,slts,revision,gui,cassette);
parseLine(line,ram,rom,slts,revision,gui,cassetteIn,cassetteOut);
}
std::getline(*pConfig,line);
}
@ -153,11 +154,11 @@ void Config::parse(Memory& ram, Memory& rom, Slots& slts, int& revision, ScreenI
// TODO: make sure there is no more than ONE stdin and/or ONE stdout card
}
void Config::parseLine(const std::string& line, Memory& ram, Memory& rom, Slots& slts, int& revision, ScreenImage& gui, Cassette& cassette)
void Config::parseLine(const std::string& line, Memory& ram, Memory& rom, Slots& slts, int& revision, ScreenImage& gui, CassetteIn& cassetteIn, CassetteOut& cassetteOut)
{
try
{
tryParseLine(line,ram,rom,slts,revision,gui,cassette);
tryParseLine(line,ram,rom,slts,revision,gui,cassetteIn,cassetteOut);
}
catch (const ConfigException& err)
{
@ -165,7 +166,7 @@ void Config::parseLine(const std::string& line, Memory& ram, Memory& rom, Slots&
}
}
void Config::tryParseLine(const std::string& line, Memory& ram, Memory& rom, Slots& slts, int& revision, ScreenImage& gui, Cassette& cassette)
void Config::tryParseLine(const std::string& line, Memory& ram, Memory& rom, Slots& slts, int& revision, ScreenImage& gui, CassetteIn& cassetteIn, CassetteOut& cassetteOut)
{
std::istringstream tok(line);
@ -301,29 +302,49 @@ void Config::tryParseLine(const std::string& line, Memory& ram, Memory& rom, Slo
if (cas == "rewind")
{
cassette.rewind();
cassetteIn.rewind();
}
else if (cas == "new")
else if (cas == "blank")
{
std::string fcas;
std::getline(tok,fcas);
trim(fcas);
cassette.newFile(fcas);
if (!fcas.empty()) {
cassetteOut.blank(fcas);
}
}
else if (cas == "load")
{
std::string fcas;
std::getline(tok,fcas);
trim(fcas);
cassette.load(fcas);
std::string fn_optional;
std::getline(tok,fn_optional);
trim(fn_optional);
if (fn_optional.length() == 0) {
gui.exitFullScreen();
char const *ft[1] = { "*.wav" };
char const *fn = tinyfd_openFileDialog("Load cassette audio", "", 1, ft, "WAVE cassette images", 0);
if (fn) {
fn_optional = std::string(fn);
}
}
if (fn_optional.length() > 0) {
cassetteIn.load(fn_optional);
}
}
else if (cas == "unload")
else if (cas == "eject")
{
cassette.unload();
}
std::string eject;
tok >> eject;
if (eject == "in") {
cassetteIn.eject();
} else if (eject == "out") {
cassetteOut.eject();
} else {
throw ConfigException("error: unknown cassette to eject: "+eject);
}
}
else if (cas == "save")
{
cassette.save();
cassetteOut.save();
}
else
{

View File

@ -22,7 +22,8 @@
class Memory;
class Slots;
class ScreenImage;
class Cassette;
class CassetteIn;
class CassetteOut;
class ConfigException
{
@ -41,14 +42,14 @@ private:
static void unloadDisk(Slots& slts, int slot, int drive);
static void saveDisk(Slots& slts, int slot, int drive);
static void insertCard(const std::string& cardType, int slot, Slots& slts, ScreenImage& gui);
static void tryParseLine(const std::string& line, Memory& ram, Memory& rom, Slots& slts, int& revision, ScreenImage& gui, Cassette& cassette);
static void tryParseLine(const std::string& line, Memory& ram, Memory& rom, Slots& slts, int& revision, ScreenImage& gui, CassetteIn& cassetteIn, CassetteOut& cassetteOut);
public:
Config(const std::string& file_path);
~Config();
void parse(Memory& ram, Memory& rom, Slots& slts, int& revision, ScreenImage& gui, Cassette& cassette);
static void parseLine(const std::string& line, Memory& ram, Memory& rom, Slots& slts, int& revision, ScreenImage& gui, Cassette& cassette);
void parse(Memory& ram, Memory& rom, Slots& slts, int& revision, ScreenImage& gui, CassetteIn& cassetteIn, CassetteOut& cassetteOut);
static void parseLine(const std::string& line, Memory& ram, Memory& rom, Slots& slts, int& revision, ScreenImage& gui, CassetteIn& cassetteIn, CassetteOut& cassetteOut);
};
#endif

View File

@ -26,7 +26,7 @@ public:
EmptySlot() {}
virtual ~EmptySlot() {}
virtual std::string getName() { return "empty"; }
virtual std::string getName() { return "[empty]"; }
// empty slots have no ROMs, so just return data (for floating bus emulation)
virtual unsigned char readRom(const unsigned short address, const unsigned char data) { return data; }

View File

@ -67,7 +67,7 @@ void Emulator::powerOffComputer() {
}
void Emulator::config(Config& cfg) {
cfg.parse(this->apple2.ram, this->apple2.rom, this->apple2.slts, this->apple2.revision, this->screenImage, this->apple2.cassette);
cfg.parse(this->apple2.ram, this->apple2.rom, this->apple2.slts, this->apple2.revision, this->screenImage, this->apple2.cassetteIn, this->apple2.cassetteOut);
}
void Emulator::init() {
@ -432,13 +432,14 @@ void Emulator::cmdKey(const SDL_KeyboardEvent& keyEvent) {
void Emulator::processCommand() {
this->screenImage.exitCommandMode();
this->screenImage.drawPower(this->timable == &this->apple2);
this->pendingCommandExit = true;
if (cmdline.empty()) {
return;
}
Config::parseLine(cmdline, this->apple2.ram, this->apple2.rom, this->apple2.slts, this->apple2.revision, this->screenImage, this->apple2.cassette);
Config::parseLine(cmdline, this->apple2.ram, this->apple2.rom, this->apple2.slts, this->apple2.revision, this->screenImage, this->apple2.cassetteIn, this->apple2.cassetteOut);
cmdline.erase(cmdline.begin(), cmdline.end());
}

View File

@ -43,6 +43,7 @@ static const char* power =
#define SCRW 640
#define SCRH 480
#define ASPECT_RATIO 1.191 /* UA2, p. 8-28 */
#define R_SLOT 67
static const int HEIGHT = E2Const::VISIBLE_ROWS_PER_FIELD * 2;
static const int WIDTH = AppleNTSC::H - AppleNTSC::PIC_START - 2;
@ -59,7 +60,8 @@ buffer(true),
fillLines(true),
display(AnalogTV::MONITOR_COLOR),
slotnames(8),
cassettename(32, ' ') {
cassInName(32, ' '),
cassOutName(32, ' ') {
createScreen();
}
@ -111,7 +113,7 @@ void ScreenImage::drawLabels() {
}
void ScreenImage::drawSlots() {
int r(65);
int r(R_SLOT-1);
int c(17);
drawText("SLOTS:", r++, c);
for (int slot(0); slot < 8; ++slot) {
@ -132,11 +134,20 @@ void ScreenImage::drawSlot(int slot, int r, int c) {
void ScreenImage::drawCassette() {
int r(65);
int c(85);
drawText("CASSETTE:", r, c);
c += 9;
drawText(this->cassettename, r, c);
const int len = this->cassettename.length();
int c(80);
drawText("CASSETTE: IN<-", r, c);
c += 15;
drawText(this->cassInName, r, c);
int len = this->cassInName.length();
if (len < 40) {
drawText(std::string(40 - len, ' '), r, c + len);
}
++r;
c = 81+9;
drawText("OUT->", r, c);
c += 5;
drawText(this->cassOutName, r, c);
len = this->cassOutName.length();
if (len < 40) {
drawText(std::string(40 - len, ' '), r, c + len);
}
@ -156,7 +167,7 @@ void ScreenImage::drawFnKeys() {
drawText(
" XXXXXXXXXXXXXX WINDOW FILL-LINES CMD RESET PASTE SAVE BMP QUIT! REPT HYPER BUFFER ", r++, c);
drawText(
" F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12 ", r++, c);
" F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12 ", r++, c);
if (this->fullscreen)
invertText(76, 32, 42); // FULLSCRN
@ -251,7 +262,7 @@ void ScreenImage::displayHz(int hz) {
void ScreenImage::drawPower(bool on) {
unsigned int* pn = this->pixels;
pn += (HEIGHT + 5)*SCRW + 5;
pn += (HEIGHT + 35)*SCRW + 5;
for (int r = 0; r < POWERD/ASPECT_RATIO; ++r) {
if (r < LABEL_Y || LABEL_Y + 7 <= r) {
for (int c = 0; c < POWERD; ++c) {
@ -337,7 +348,7 @@ void ScreenImage::backspaceCommand() {
}
void ScreenImage::updateSlotName(const int slot, Card* card) {
int r(66 + slot);
int r(R_SLOT + slot);
int c(20);
const std::string& name = card->getName();
this->slotnames[slot] = name;
@ -356,7 +367,7 @@ void ScreenImage::removeCard(const int slot, Card* card /* empty */) {
*/
void ScreenImage::setDiskFile(int slot, int drive, const std::string& filepath) {
std::string f = truncateFilePath(filepath);
int r(66 + slot);
int r(R_SLOT + slot);
int c(37 + 32 * drive);
drawText(f, r, c);
@ -383,7 +394,7 @@ std::string ScreenImage::truncateFilePath(const std::string& filepath) {
}
void ScreenImage::clearCurrentDrive(int slot, int drive) {
int r(66 + slot);
int r(R_SLOT + slot);
int c(35 + 32 * drive);
drawChar(' ', r, c);
this->slotnames[slot][c - 20] = ' ';
@ -393,7 +404,7 @@ void ScreenImage::clearCurrentDrive(int slot, int drive) {
}
void ScreenImage::setCurrentDrive(int slot, int drive, int track, bool on) {
int r(66 + slot);
int r(R_SLOT + slot);
int c(35 + 32 * drive);
drawChar(' ', r, c, 0xFFFFFF, on ? 0xFF0000 : 0);
c += 15;
@ -413,7 +424,7 @@ void ScreenImage::setCurrentDrive(int slot, int drive, int track, bool on) {
}
void ScreenImage::setTrack(int slot, int drive, int track) {
int r(66 + slot);
int r(R_SLOT + slot);
int c(52 + 32 * drive);
char nibh = Util::hexDigit((((unsigned char) track) >> 4) & 0xF);
drawChar(nibh, r, c);
@ -425,19 +436,19 @@ void ScreenImage::setTrack(int slot, int drive, int track) {
}
void ScreenImage::setIO(int slot, int drive, bool on) {
int r(66 + slot);
int r(R_SLOT + slot);
int c(35 + 32 * drive);
drawChar(' ', r, c, 0xFFFFFF, on ? 0xFF0000 : 0);
}
void ScreenImage::setDirty(int slot, int drive, bool dirty) {
int r(66 + slot);
int r(R_SLOT + slot);
int c(36 + 32 * drive);
drawChar(dirty ? '*' : ' ', r, c);
this->slotnames[slot][c - 20] = dirty ? '*' : ' ';
}
void ScreenImage::setCassetteFile(const std::string& filepath) {
void ScreenImage::setCassetteInFile(const std::string& filepath) {
std::string f = truncateFilePath(filepath);
int r(65);
int c(85 + 11);
@ -449,18 +460,34 @@ void ScreenImage::setCassetteFile(const std::string& filepath) {
drawText(d, r, c + f.length());
}
this->cassettename.replace(c - 94, 12, 12, ' ');
this->cassettename.replace(c - 94, f.length(), f);
this->cassInName.replace(c - 94, 12, 12, ' ');
this->cassInName.replace(c - 94, f.length(), f);
}
void ScreenImage::setCassetteOutFile(const std::string& filepath) {
std::string f = truncateFilePath(filepath);
int r(66);
int c(85 + 11);
drawText(f, r, c);
const int dlen = 12 - f.length();
if (dlen > 0) {
std::string d(dlen, ' ');
drawText(d, r, c + f.length());
}
this->cassOutName.replace(c - 94, 12, 12, ' ');
this->cassOutName.replace(c - 94, f.length(), f);
}
void ScreenImage::setCassetteDirty(bool dirty) {
int r(65);
int r(66);
int c(85 + 10);
drawChar(dirty ? '*' : ' ', r, c);
this->cassettename[c - 94] = dirty ? '*' : ' ';
this->cassOutName[c - 94] = dirty ? '*' : ' ';
}
void ScreenImage::setCassettePos(int pos, int siz) {
void ScreenImage::setCassettePos(unsigned int pos, unsigned int siz) {
int r(65);
int c(110);
std::ostringstream os;
@ -474,7 +501,7 @@ void ScreenImage::setCassettePos(int pos, int siz) {
0: language RW B2
*/
void ScreenImage::setLangCard(int slot, bool readEnable, bool writeEnable, int bank) {
int r(66 + slot);
int r(R_SLOT + slot);
int c(29);
drawChar(readEnable ? 'R' : ' ', r, c);
this->slotnames[slot][c - 20] = readEnable ? 'R' : ' ';
@ -496,7 +523,7 @@ void ScreenImage::setLangCard(int slot, bool readEnable, bool writeEnable, int b
0: firmware D F8
*/
void ScreenImage::setFirmCard(int slot, bool bank, bool F8) {
int r(66 + slot);
int r(R_SLOT + slot);
int c(29);
drawChar(bank ? 'D' : ' ', r, c);
this->slotnames[slot][c - 20] = bank ? 'D' : ' ';

View File

@ -42,7 +42,8 @@ private:
unsigned int cmdpos;
void createScreen();
std::vector<std::string> slotnames;
std::string cassettename;
std::string cassInName;
std::string cassOutName;
static std::string truncateFilePath(const std::string& filepath);
@ -87,9 +88,10 @@ public:
void setIO(int slot, int drive, bool on);
void setDirty(int slot, int drive, bool dirty);
void setCassetteFile(const std::string& filepath);
void setCassetteDirty(bool dirty);
void setCassettePos(int pos, int siz);
void setCassetteInFile(const std::string& filepath);
void setCassetteOutFile(const std::string& filepath);
void setCassetteDirty(bool dirty); // cassette out only
void setCassettePos(unsigned int pos, unsigned int siz); // cassette in only
void setLangCard(int slot, bool readEnable, bool writeEnable, int bank);
void setFirmCard(int slot, bool bank, bool F8);