From 96e3f37b7609d4a237922e14de6737e4bfa7d0ff Mon Sep 17 00:00:00 2001 From: Andrea Odetti Date: Fri, 28 May 2021 18:10:09 +0100 Subject: [PATCH] Add support for Cassette Tapes. Signed-off-by: Andrea Odetti --- linux.md | 6 +- source/CMakeLists.txt | 3 +- source/frontends/sdl/imgui/sdlsettings.cpp | 33 ++++++ source/frontends/sdl/processfile.cpp | 40 ++++++++ source/linux/tape.cpp | 112 +++++++++++++++++++++ source/linux/tape.h | 10 ++ 6 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 source/linux/tape.cpp create mode 100644 source/linux/tape.h diff --git a/linux.md b/linux.md index c1416611..9b5b7377 100644 --- a/linux.md +++ b/linux.md @@ -41,11 +41,15 @@ Uthernet I is supported via `libpcap`, but it requires elevated capabilities: Unfortunately, this must be reapplied after every build. +Most of the debugger now works (in the ImGui version). + +## New features + Uthernet II is supported too and by default uses `libslirp` which does *not* require elevated capabilities. Use the ImGui settings to enable it. `libslirp` is not packaged on Raspberry Pi OS. `libpcap` will be used instead, unless the user manually compiles and installs [libslirp](https://gitlab.freedesktop.org/slirp/libslirp). -Most of the debugger now works (in the ImGui version). +Audio files can be read via the cassette interface (SDL Version). Just drop a `wav` file into the emulator. Tested with all the formats from [asciiexpress](https://asciiexpress.net/). ## Executables diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index faefae91..76cb1622 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -52,7 +52,6 @@ set(SOURCE_FILES AY8910.cpp Mockingboard.cpp Pravets.cpp - Tape.cpp YamlHelper.cpp Log.cpp Disk.cpp @@ -105,6 +104,7 @@ set(SOURCE_FILES linux/keyboard.cpp linux/linuxframe.cpp linux/context.cpp + linux/tape.cpp linux/network/uthernet2.cpp linux/network/tfe2.cpp linux/network/slirp2.cpp @@ -208,6 +208,7 @@ set(HEADER_FILES linux/registry.h linux/keyboard.h linux/linuxframe.h + linux/tape.h linux/network/uthernet2.h linux/network/tfe2.h linux/network/slirp2.h diff --git a/source/frontends/sdl/imgui/sdlsettings.cpp b/source/frontends/sdl/imgui/sdlsettings.cpp index 7f4191e6..abe6b44b 100644 --- a/source/frontends/sdl/imgui/sdlsettings.cpp +++ b/source/frontends/sdl/imgui/sdlsettings.cpp @@ -4,6 +4,7 @@ #include "frontends/sdl/sdlframe.h" #include "linux/registry.h" #include "linux/version.h" +#include "linux/tape.h" #include "Interface.h" #include "CardManager.h" @@ -612,6 +613,38 @@ namespace sa2 ImGui::EndTabItem(); } + if (ImGui::BeginTabItem("Tape")) + { + size_t size; + size_t pos; + int frequency; + uint8_t bit; + getTapeInfo(size, pos, frequency, bit); + + if (pos < size) + { + const float remaining = float(size - pos) / float(frequency); + const float fraction = float(pos) / float(size); + char buf[32]; + sprintf(buf, "-%.1f s", remaining); + const ImU32 color = bit ? IM_COL32(200, 0, 0, 100) : IM_COL32(0, 200, 0, 100); + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, color); + ImGui::ProgressBar(fraction, ImVec2(-FLT_MIN, 0), buf); + ImGui::LabelText("Frequency", "%d Hz", frequency); + ImGui::PopStyleColor(); + if (ImGui::Button("Eject tape")) + { + ejectTape(); + } + } + else + { + ImGui::TextUnformatted("Drop a .wav file."); + } + + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Debugger")) { if (ImGui::RadioButton("Color", g_iColorScheme == SCHEME_COLOR)) { g_iColorScheme = SCHEME_COLOR; } ImGui::SameLine(); diff --git a/source/frontends/sdl/processfile.cpp b/source/frontends/sdl/processfile.cpp index 4ec738cf..51933b93 100644 --- a/source/frontends/sdl/processfile.cpp +++ b/source/frontends/sdl/processfile.cpp @@ -3,6 +3,8 @@ #include "frontends/sdl/sdlframe.h" #include "frontends/common2/utils.h" +#include "linux/tape.h" + #include "CardManager.h" #include "Disk.h" #include "Core.h" @@ -41,6 +43,39 @@ namespace } } } + + void insertTape(sa2::SDLFrame * frame, const char * filename) + { + SDL_AudioSpec wavSpec; + Uint32 wavLength; + Uint8 *wavBuffer; + if (!SDL_LoadWAV(filename, &wavSpec, &wavBuffer, &wavLength)) + { + frame->FrameMessageBox("Could not open wav file", "ERROR", MB_OK); + return; + } + + SDL_AudioCVT cvt; + // tested with all formats from https://asciiexpress.net/ + // 8 bit mono is just enough + // TAPEIN will interpolate so we do not need to resample at a higher frequency + const SDL_AudioFormat format = sizeof(tape_data_t) == 1 ? AUDIO_S8 : AUDIO_S16SYS; + const int res = SDL_BuildAudioCVT(&cvt, wavSpec.format, wavSpec.channels, wavSpec.freq, format, 1, wavSpec.freq); + cvt.len = wavLength; + std::vector output(cvt.len_mult * cvt.len / sizeof(tape_data_t)); + std::memcpy(output.data(), wavBuffer, cvt.len); + SDL_FreeWAV(wavBuffer); + + if (res) + { + cvt.buf = reinterpret_cast(output.data()); + SDL_ConvertAudio(&cvt); + output.resize(cvt.len_cvt / sizeof(tape_data_t)); + } + + setCassetteTape(output, wavSpec.freq); + } + } @@ -50,10 +85,15 @@ namespace sa2 void processFile(SDLFrame * frame, const char * filename, const size_t dragAndDropSlot, const size_t dragAndDropDrive) { const char * yaml = ".yaml"; + const char * wav = ".wav"; if (strlen(filename) > strlen(yaml) && !strcmp(filename + strlen(filename) - strlen(yaml), yaml)) { common2::setSnapshotFilename(filename, true); } + else if (strlen(filename) > strlen(wav) && !strcmp(filename + strlen(filename) - strlen(wav), wav)) + { + insertTape(frame, filename); + } else { insertDisk(frame, filename, dragAndDropSlot, dragAndDropDrive); diff --git a/source/linux/tape.cpp b/source/linux/tape.cpp new file mode 100644 index 00000000..6bf0ca2a --- /dev/null +++ b/source/linux/tape.cpp @@ -0,0 +1,112 @@ +#include "StdAfx.h" + +#include "linux/tape.h" + +#include "Core.h" +#include "Tape.h" +#include "Memory.h" +#include "Pravets.h" +#include "CPU.h" + +#include + +namespace +{ + std::vector cassetteTape; + uint64_t baseCycles; + int tapeFrequency; + bool playing = false; + BYTE lastBit = 1; + // threshold not needed for https://asciiexpress.net + // but probably necessary for a real audio file + constexpr tape_data_t threshold = 5; + + BYTE getBitValue(const tape_data_t val) + { + // this has been tested on all formats from https://asciiexpress.net + // + // we are extracting the sign bit (set for negative numbers) + // this is really important for the asymmetric wave in https://asciiexpress.net/diskserver/ + // not so much for the other cases + if (val > threshold) + { + lastBit = 0; + } + else if (val < -threshold) + { + lastBit = 1; + } + // else leave it unchanged to the previous value + return lastBit; + } + + tape_data_t getCurrentWave(size_t & pos) + { + if (playing && !cassetteTape.empty()) + { + const double delta = g_nCumulativeCycles - baseCycles; + const double position = delta / g_fCurrentCLK6502 * tapeFrequency; + pos = static_cast(position); + + if (pos < cassetteTape.size() - 1) + { + // linear interpolation + const double reminder = position - pos; + const double value = (1.0 - reminder) * cassetteTape[pos] + reminder * cassetteTape[pos + 1]; + return lround(value); + } + + cassetteTape.clear(); + } + pos = 0; + return std::numeric_limits::min(); + } +} + +//--------------------------------------------------------------------------- + +BYTE __stdcall TapeRead(WORD pc, WORD address, BYTE, BYTE, ULONG nExecutedCycles) // $C060 TAPEIN +{ + if (g_Apple2Type == A2TYPE_PRAVETS8A) + return GetPravets().GetKeycode( MemReadFloatingBus(nExecutedCycles) ); + + CpuCalcCycles(nExecutedCycles); + + if (!playing) + { + // start play as soon as TAPEIN is read + playing = true; + baseCycles = g_nCumulativeCycles; + } + + size_t pos; + const tape_data_t val = getCurrentWave(pos); // this has side effects + const BYTE highBit = getBitValue(val); + + return MemReadFloatingBus(highBit, nExecutedCycles); +} + +BYTE __stdcall TapeWrite(WORD, WORD address, BYTE, BYTE, ULONG nExecutedCycles) // $C020 TAPEOUT +{ + return 0; +} + +void setCassetteTape(const std::vector & data, const int frequency) +{ + playing = false; + cassetteTape = data; + tapeFrequency = frequency; +} + +void getTapeInfo(size_t & size, size_t & pos, int & frequency, uint8_t & bit) +{ + const tape_data_t val = getCurrentWave(pos); // this has side effects + bit = lastBit; + size = cassetteTape.size(); + frequency = tapeFrequency; +} + +void ejectTape() +{ + cassetteTape.clear(); +} diff --git a/source/linux/tape.h b/source/linux/tape.h new file mode 100644 index 00000000..52a8a84c --- /dev/null +++ b/source/linux/tape.h @@ -0,0 +1,10 @@ +#pragma once + +#include +#include + +typedef int8_t tape_data_t; + +void setCassetteTape(const std::vector & data, const int frequency); +void getTapeInfo(size_t & size, size_t & pos, int & frequency, uint8_t & bit); +void ejectTape();