diff --git a/src/Makefile.am b/src/Makefile.am index e859248..a138122 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -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 \ diff --git a/src/addressbus.cpp b/src/addressbus.cpp index 9a08141..04e197e 100644 --- a/src/addressbus.cpp +++ b/src/addressbus.cpp @@ -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) { diff --git a/src/addressbus.h b/src/addressbus.h index 899077f..6b245cc 100644 --- a/src/addressbus.h +++ b/src/addressbus.h @@ -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); diff --git a/src/apple2.cpp b/src/apple2.cpp index 8cf75ba..16ac093 100644 --- a/src/apple2.cpp +++ b/src/apple2.cpp @@ -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(); diff --git a/src/apple2.h b/src/apple2.h index df572a9..ce5569d 100644 --- a/src/apple2.h +++ b/src/apple2.h @@ -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 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; diff --git a/src/cassette.cpp b/src/cassette.cpp index 8444f1b..698e830 100644 --- a/src/cassette.cpp +++ b/src/cassette.cpp @@ -1,6 +1,7 @@ /* epple2 - Copyright (C) 2008 by Christopher A. Mosher + + Copyright © 2008, 2019, Christopher Alan Mosher, Shelton, CT, USA. 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 . */ - - - -/* -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 #include +#include +#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; } diff --git a/src/cassette.h b/src/cassette.h index 55019fc..6a468cf 100644 --- a/src/cassette.h +++ b/src/cassette.h @@ -1,83 +1,57 @@ /* - epple2 - Copyright (C) 2008 by Christopher A. Mosher +epple2 +Copyright (C) 2008 by Christopher A. Mosher - 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 . +You should have received a copy of the GNU General Public License +along with this program. If not, see . */ #ifndef CASSETTE_H #define CASSETTE_H #include #include +#include #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 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 diff --git a/src/cassettein.cpp b/src/cassettein.cpp new file mode 100644 index 0000000..55c870c --- /dev/null +++ b/src/cassettein.cpp @@ -0,0 +1,259 @@ +/* + epple2 + + Copyright © 2008, 2019, Christopher Alan Mosher, Shelton, CT, USA. + + 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 . +*/ +#include +#include +#include +#include +#include +#include "tinyfiledialogs.h" +#include "cassettein.h" +#include "e2const.h" +#include +#include + +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::malloc(cvt.len_mult * cvt.len)); + memcpy(cvt.buf, wav_buffer, cvt.len); + SDL_FreeWAV(wav_buffer); + + SDL_ConvertAudio(&cvt); + this->samp = reinterpret_cast(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; +} diff --git a/src/cassettein.h b/src/cassettein.h new file mode 100644 index 0000000..f00ed12 --- /dev/null +++ b/src/cassettein.h @@ -0,0 +1,48 @@ +/* +epple2 +Copyright (C) 2008 by Christopher A. Mosher + +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 . +*/ +#ifndef CASSETTEIN_H +#define CASSETTEIN_H + +#include +#include +#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 diff --git a/src/cassetteout.cpp b/src/cassetteout.cpp new file mode 100644 index 0000000..f2f258b --- /dev/null +++ b/src/cassetteout.cpp @@ -0,0 +1,178 @@ +/* + epple2 + + Copyright © 2008, 2019, Christopher Alan Mosher, Shelton, CT, USA. + + 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 . +*/ +#include +#include +#include +#include +#include +#include "tinyfiledialogs.h" +#include "cassetteout.h" +#include "e2const.h" +#include +#include + +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; +} diff --git a/src/cassetteout.h b/src/cassetteout.h new file mode 100644 index 0000000..fd528ce --- /dev/null +++ b/src/cassetteout.h @@ -0,0 +1,44 @@ +/* +epple2 +Copyright (C) 2008 by Christopher A. Mosher + +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 . +*/ +#ifndef CASSETTEOUT_H +#define CASSETTEOUT_H + +#include +#include +#include +#include "cassette.h" + +class CassetteOut : public Cassette { + private: + std::vector 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 diff --git a/src/configep2.cpp b/src/configep2.cpp index 17b8f89..0c0f290 100644 --- a/src/configep2.cpp +++ b/src/configep2.cpp @@ -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 @@ -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 { diff --git a/src/configep2.h b/src/configep2.h index 29d7d61..cd8c4dd 100644 --- a/src/configep2.h +++ b/src/configep2.h @@ -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 diff --git a/src/emptyslot.h b/src/emptyslot.h index 055e63c..3af1d69 100644 --- a/src/emptyslot.h +++ b/src/emptyslot.h @@ -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; } diff --git a/src/emulator.cpp b/src/emulator.cpp index d3cb83e..7d9429f 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -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()); } diff --git a/src/screenimage.cpp b/src/screenimage.cpp index 492225f..e503a05 100644 --- a/src/screenimage.cpp +++ b/src/screenimage.cpp @@ -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' : ' '; diff --git a/src/screenimage.h b/src/screenimage.h index de88ddd..bfd1145 100644 --- a/src/screenimage.h +++ b/src/screenimage.h @@ -42,7 +42,8 @@ private: unsigned int cmdpos; void createScreen(); std::vector 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);