Add support for Cassette Tapes.
Signed-off-by: Andrea Odetti <mariofutire@gmail.com>
This commit is contained in:
parent
a059fad539
commit
96e3f37b76
6 changed files with 202 additions and 2 deletions
6
linux.md
6
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.
|
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.
|
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).
|
`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
|
## Executables
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,6 @@ set(SOURCE_FILES
|
||||||
AY8910.cpp
|
AY8910.cpp
|
||||||
Mockingboard.cpp
|
Mockingboard.cpp
|
||||||
Pravets.cpp
|
Pravets.cpp
|
||||||
Tape.cpp
|
|
||||||
YamlHelper.cpp
|
YamlHelper.cpp
|
||||||
Log.cpp
|
Log.cpp
|
||||||
Disk.cpp
|
Disk.cpp
|
||||||
|
@ -105,6 +104,7 @@ set(SOURCE_FILES
|
||||||
linux/keyboard.cpp
|
linux/keyboard.cpp
|
||||||
linux/linuxframe.cpp
|
linux/linuxframe.cpp
|
||||||
linux/context.cpp
|
linux/context.cpp
|
||||||
|
linux/tape.cpp
|
||||||
linux/network/uthernet2.cpp
|
linux/network/uthernet2.cpp
|
||||||
linux/network/tfe2.cpp
|
linux/network/tfe2.cpp
|
||||||
linux/network/slirp2.cpp
|
linux/network/slirp2.cpp
|
||||||
|
@ -208,6 +208,7 @@ set(HEADER_FILES
|
||||||
linux/registry.h
|
linux/registry.h
|
||||||
linux/keyboard.h
|
linux/keyboard.h
|
||||||
linux/linuxframe.h
|
linux/linuxframe.h
|
||||||
|
linux/tape.h
|
||||||
linux/network/uthernet2.h
|
linux/network/uthernet2.h
|
||||||
linux/network/tfe2.h
|
linux/network/tfe2.h
|
||||||
linux/network/slirp2.h
|
linux/network/slirp2.h
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
#include "frontends/sdl/sdlframe.h"
|
#include "frontends/sdl/sdlframe.h"
|
||||||
#include "linux/registry.h"
|
#include "linux/registry.h"
|
||||||
#include "linux/version.h"
|
#include "linux/version.h"
|
||||||
|
#include "linux/tape.h"
|
||||||
|
|
||||||
#include "Interface.h"
|
#include "Interface.h"
|
||||||
#include "CardManager.h"
|
#include "CardManager.h"
|
||||||
|
@ -612,6 +613,38 @@ namespace sa2
|
||||||
ImGui::EndTabItem();
|
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::BeginTabItem("Debugger"))
|
||||||
{
|
{
|
||||||
if (ImGui::RadioButton("Color", g_iColorScheme == SCHEME_COLOR)) { g_iColorScheme = SCHEME_COLOR; } ImGui::SameLine();
|
if (ImGui::RadioButton("Color", g_iColorScheme == SCHEME_COLOR)) { g_iColorScheme = SCHEME_COLOR; } ImGui::SameLine();
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
#include "frontends/sdl/sdlframe.h"
|
#include "frontends/sdl/sdlframe.h"
|
||||||
#include "frontends/common2/utils.h"
|
#include "frontends/common2/utils.h"
|
||||||
|
|
||||||
|
#include "linux/tape.h"
|
||||||
|
|
||||||
#include "CardManager.h"
|
#include "CardManager.h"
|
||||||
#include "Disk.h"
|
#include "Disk.h"
|
||||||
#include "Core.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<tape_data_t> 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<Uint8 *>(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)
|
void processFile(SDLFrame * frame, const char * filename, const size_t dragAndDropSlot, const size_t dragAndDropDrive)
|
||||||
{
|
{
|
||||||
const char * yaml = ".yaml";
|
const char * yaml = ".yaml";
|
||||||
|
const char * wav = ".wav";
|
||||||
if (strlen(filename) > strlen(yaml) && !strcmp(filename + strlen(filename) - strlen(yaml), yaml))
|
if (strlen(filename) > strlen(yaml) && !strcmp(filename + strlen(filename) - strlen(yaml), yaml))
|
||||||
{
|
{
|
||||||
common2::setSnapshotFilename(filename, true);
|
common2::setSnapshotFilename(filename, true);
|
||||||
}
|
}
|
||||||
|
else if (strlen(filename) > strlen(wav) && !strcmp(filename + strlen(filename) - strlen(wav), wav))
|
||||||
|
{
|
||||||
|
insertTape(frame, filename);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
insertDisk(frame, filename, dragAndDropSlot, dragAndDropDrive);
|
insertDisk(frame, filename, dragAndDropSlot, dragAndDropDrive);
|
||||||
|
|
112
source/linux/tape.cpp
Normal file
112
source/linux/tape.cpp
Normal file
|
@ -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 <limits>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::vector<tape_data_t> 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<size_t>(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<tape_data_t>::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<tape_data_t> & 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();
|
||||||
|
}
|
10
source/linux/tape.h
Normal file
10
source/linux/tape.h
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
typedef int8_t tape_data_t;
|
||||||
|
|
||||||
|
void setCassetteTape(const std::vector<tape_data_t> & data, const int frequency);
|
||||||
|
void getTapeInfo(size_t & size, size_t & pos, int & frequency, uint8_t & bit);
|
||||||
|
void ejectTape();
|
Loading…
Add table
Reference in a new issue