Add C64 SID audio output via OpenSL ES with sound toggle

This commit is contained in:
ml
2026-06-13 13:40:24 +02:00
parent 7a8cd492d5
commit 5a390aa733
12 changed files with 435 additions and 88 deletions
+3 -15
View File
@@ -101,21 +101,9 @@ The NDK must be installed: **Android Studio → SDK Manager → SDK Tools → ND
After the first successful build, subsequent builds skip the VICE compilation step entirely (outputs are up-to-date).
**C64 ROM files** must be copied to the device separately (not redistributable).
VICE expects files named exactly `kernal`, `basic`, `chargen` (no extensions).
**Via USB file transfer (recommended):** connect the phone, open it in Windows Explorer or
Android File Transfer (Mac), and copy the files to:
```
Android/data/de.ladkau.schwertundmagieonpebblecompanionapp/files/
```
**Via adb (alternative):**
```bash
adb push kernal /sdcard/Android/data/de.ladkau.schwertundmagieonpebblecompanionapp/files/kernal
adb push basic /sdcard/Android/data/de.ladkau.schwertundmagieonpebblecompanionapp/files/basic
adb push chargen /sdcard/Android/data/de.ladkau.schwertundmagieonpebblecompanionapp/files/chargen
```
**C64 ROM files** (`kernal`, `basic`, `chargen`, `1541`) all ship inside the VICE 3.8
tarball at `res/vice-3.8.tar.gz` and are bundled in the APK under `app/src/main/assets/`.
They are copied automatically to the external files dir on first launch — no user action needed.
### Key Kotlin/C files
@@ -13,6 +13,39 @@
</DropdownSelection>
<DialogSelection />
</SelectionState>
<SelectionState runConfigName="main">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-06-10T16:34:31.851340592Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=adf68432" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
<SelectionState runConfigName="androidTest">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-06-10T16:34:31.851340592Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=adf68432" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
<SelectionState runConfigName="unitTest">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-06-10T16:34:31.851340592Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=adf68432" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>
</project>
@@ -1 +1,7 @@
/build
/build
# ROM files extracted from the VICE tarball at build time (see extractViceRoms task)
/src/main/assets/kernal
/src/main/assets/basic
/src/main/assets/chargen
/src/main/assets/1541
@@ -130,6 +130,48 @@ tasks.whenTaskAdded {
}
}
// ---------------------------------------------------------------------------
// ROM extraction task
//
// Extracts C64 and drive ROMs from the bundled VICE tarball into the Android
// assets directory so they are packaged into the APK automatically.
// Output files are gitignored — the tarball is the single source of truth.
// ---------------------------------------------------------------------------
val assetsDir = layout.projectDirectory.dir("src/main/assets")
tasks.register<Copy>("extractViceRoms") {
group = "build"
description = "Extract C64 and 1541 ROMs from the VICE tarball into assets/"
from(tarTree(viceTarball.asFile)) {
include("vice-3.8/data/C64/kernal-901227-03.bin")
include("vice-3.8/data/C64/basic-901226-01.bin")
include("vice-3.8/data/C64/chargen-901225-01.bin")
include("vice-3.8/data/DRIVES/dos1541-325302-01+901229-05.bin")
eachFile {
// Flatten the tarball directory structure: place each ROM directly
// in assets/ with its short name rather than the versioned filename.
relativePath = RelativePath(true, when (name) {
"kernal-901227-03.bin" -> "kernal"
"basic-901226-01.bin" -> "basic"
"chargen-901225-01.bin" -> "chargen"
"dos1541-325302-01+901229-05.bin" -> "1541"
else -> name
})
}
includeEmptyDirs = false
}
into(assetsDir)
}
// Run extractViceRoms before assets are merged into the APK.
tasks.whenTaskAdded {
if (name.startsWith("merge") && name.endsWith("Assets")) {
dependsOn("extractViceRoms")
}
}
dependencies {
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.appcompat)
@@ -37,6 +37,9 @@ class C64Engine {
*/
external fun injectKey(keyCode: Int, pressed: Boolean)
/** Enable or disable audio output. Disabled by default. */
external fun setSoundEnabled(enabled: Boolean)
companion object {
init { System.loadLibrary("vice_jni") }
@@ -11,11 +11,9 @@ import android.util.Log
import android.widget.Button
import android.widget.ScrollView
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.documentfile.provider.DocumentFile
import fi.iki.elonen.NanoHTTPD
import java.io.File
import java.text.SimpleDateFormat
@@ -45,6 +43,7 @@ class MainActivity : AppCompatActivity() {
private val engine = C64Engine()
private var emuThread: Thread? = null
@Volatile private var emuRunning = false
private var soundEnabled = false
// ---- disk slots ---------------------------------------------------------
@@ -56,41 +55,6 @@ class MainActivity : AppCompatActivity() {
)
private val episodeButtons = mutableMapOf<DiskSlot, Button>()
// ---- ROM import (pick folder — kernal/basic/chargen have no extension) --
private val romNames = listOf("kernal", "basic", "chargen")
private val romDirPickerLauncher: ActivityResultLauncher<Intent> =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let { treeUri -> importRomDir(treeUri) }
}
}
private fun importRomDir(treeUri: Uri) {
val dir = DocumentFile.fromTreeUri(this, treeUri) ?: run {
appendLog("Cannot access selected folder")
return
}
val files = dir.listFiles()
var imported = 0
for (name in romNames) {
val file = files.firstOrNull { it.name?.lowercase() == name } ?: run {
appendLog("Not found: $name")
continue
}
importFileToAssets(file.uri, name)
appendLog("Imported: $name")
imported++
}
if (imported == romNames.size) {
appendLog("All ROMs imported — restarting emulator")
mainHandler.postDelayed({ recreate() }, 400)
} else {
appendLog("Only $imported/${romNames.size} ROMs found — check folder contents")
}
}
// ---- Disk import (multi-select) -----------------------------------------
private val diskPickerLauncher: ActivityResultLauncher<Intent> =
@@ -126,13 +90,17 @@ class MainActivity : AppCompatActivity() {
display = findViewById(R.id.c64_display)
keyboard = findViewById(R.id.c64_keyboard)
findViewById<Button>(R.id.btn_import_roms).setOnClickListener {
romDirPickerLauncher.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE))
}
findViewById<Button>(R.id.btn_import_disks).setOnClickListener {
launchPicker(diskPickerLauncher, "Select SCHWUM disk images", multiSelect = true)
}
val btnSound = findViewById<Button>(R.id.btn_sound)
btnSound.setOnClickListener {
soundEnabled = !soundEnabled
engine.setSoundEnabled(soundEnabled)
btnSound.text = if (soundEnabled) "Sound: ON" else "Sound: OFF"
}
for (slot in diskSlots) {
val btn = findViewById<Button>(slot.btnId)
episodeButtons[slot] = btn
@@ -150,10 +118,10 @@ class MainActivity : AppCompatActivity() {
private fun initEmulator() {
val assetDir = getExternalFilesDir(null) ?: filesDir
assetDir.mkdirs()
copyBundledRoms(assetDir)
val romDir = assetDir.absolutePath
val ok = engine.initEmulator(romDir)
tvStatus.text = if (ok) "Emulator ready"
else "Copy ROMs + .d64 to:\n$romDir"
tvStatus.text = if (ok) "Emulator ready" else "Emulator init failed"
Log.d(TAG, "initEmulator=$ok romDir=$romDir")
emuRunning = true
@@ -168,6 +136,25 @@ class MainActivity : AppCompatActivity() {
}.also { it.name = "emu-loop"; it.isDaemon = true; it.start() }
}
// ---- bundled ROM extraction ---------------------------------------------
private val bundledRoms = listOf("kernal", "basic", "chargen", "1541")
private fun copyBundledRoms(destDir: File) {
for (name in bundledRoms) {
val dest = File(destDir, name)
if (dest.exists()) continue
try {
assets.open(name).use { input ->
dest.outputStream().use { output -> input.copyTo(output) }
}
Log.d(TAG, "Copied bundled ROM: $name")
} catch (e: Exception) {
Log.e(TAG, "Failed to copy bundled ROM $name", e)
}
}
}
// ---- virtual keyboard ---------------------------------------------------
private fun wireKeyboard() {
@@ -17,7 +17,7 @@ endif()
# ---- JNI wrapper library -------------------------------------------------
add_library(vice_jni SHARED vice_jni.c)
target_link_libraries(vice_jni android log c++_shared z)
target_link_libraries(vice_jni android log c++_shared z OpenSLES)
if(HAVE_VICE)
target_compile_definitions(vice_jni PRIVATE HAVE_VICE_SRC=1)
@@ -23,6 +23,8 @@
#include <unistd.h>
#include <sys/stat.h>
#include <android/log.h>
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "ViceJNI", __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "ViceJNI", __VA_ARGS__)
@@ -34,6 +36,12 @@ static uint32_t g_framebuf[FRAME_W * FRAME_H];
static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
static int g_ready = 0;
/* Pending disk path — written by loadDisk (Android thread), consumed by
* video_canvas_refresh (VICE thread). autostart_disk must be called from
* the VICE thread; calling it cross-thread corrupts VICE internal state. */
static pthread_mutex_t g_pending_lock = PTHREAD_MUTEX_INITIALIZER;
static char g_pending_disk[512];
/* =========================================================================
* VICE integration (compiled only when libvice.a is linked in)
* ========================================================================= */
@@ -52,7 +60,170 @@ extern int __real_console_init(void);
#include "video.h"
#include "palette.h"
#include "keyboard.h"
#include "attach.h"
#include "autostart.h"
/* =========================================================================
* Android OpenSL ES sound driver — registered with VICE before main_program()
* so VICE can select it via -sounddev android.
* ========================================================================= */
#include "sound.h"
/* Mute flag — 0 = silent (default), 1 = audio passes through. */
static volatile int g_sound_enabled = 0;
/* Lock-free-ish ring buffer (mutex-protected).
* AUDIO_RING_SAMPLES must be a power of 2 for the masking trick.
* 32768 × 2 bytes = 64 KB, about 0.34 s at 48 kHz mono. */
#define AUDIO_RING_SAMPLES (1u << 15)
static int16_t g_audio_ring[AUDIO_RING_SAMPLES];
static unsigned int g_ring_write = 0;
static unsigned int g_ring_read = 0;
static pthread_mutex_t g_ring_mutex = PTHREAD_MUTEX_INITIALIZER;
/* OpenSL ES state */
static SLObjectItf g_sl_engine_obj = NULL;
static SLEngineItf g_sl_engine = NULL;
static SLObjectItf g_sl_out_mix_obj = NULL;
static SLObjectItf g_sl_player_obj = NULL;
static SLPlayItf g_sl_play = NULL;
static SLAndroidSimpleBufferQueueItf g_sl_bq = NULL;
/* Four output buffers, each 2048 int16_t samples (~42 ms at 48 kHz) */
#define AUDIO_N_BUFS 4
#define AUDIO_BUF_SAMPS 2048
static int16_t g_sl_bufs[AUDIO_N_BUFS][AUDIO_BUF_SAMPS];
static int g_sl_next_buf = 0;
static unsigned int ring_avail_locked(void) {
return (g_ring_write - g_ring_read) & (AUDIO_RING_SAMPLES - 1);
}
/* OpenSL ES callback — called when a buffer finishes playing. */
static void sl_bufq_callback(SLAndroidSimpleBufferQueueItf bq, void *ctx) {
(void)ctx;
int buf = g_sl_next_buf;
g_sl_next_buf = (g_sl_next_buf + 1) % AUDIO_N_BUFS;
pthread_mutex_lock(&g_ring_mutex);
unsigned int avail = ring_avail_locked();
unsigned int fill = (avail > AUDIO_BUF_SAMPS) ? AUDIO_BUF_SAMPS : avail;
for (unsigned int i = 0; i < fill; i++)
g_sl_bufs[buf][i] = g_audio_ring[(g_ring_read + i) & (AUDIO_RING_SAMPLES - 1)];
g_ring_read = (g_ring_read + fill) & (AUDIO_RING_SAMPLES - 1);
pthread_mutex_unlock(&g_ring_mutex);
/* Silence for underrun */
for (unsigned int i = fill; i < AUDIO_BUF_SAMPS; i++)
g_sl_bufs[buf][i] = 0;
(*bq)->Enqueue(bq, g_sl_bufs[buf], AUDIO_BUF_SAMPS * sizeof(int16_t));
}
static int android_sound_init(const char *param, int *speed,
int *fragsize, int *fragnr, int *channels) {
(void)param;
LOGI("android_sound_init: speed=%d frag=%d nr=%d ch=%d",
*speed, *fragsize, *fragnr, *channels);
if (*channels > 2) *channels = 2;
if (*channels < 1) *channels = 1;
SLresult r;
r = slCreateEngine(&g_sl_engine_obj, 0, NULL, 0, NULL, NULL);
if (r != SL_RESULT_SUCCESS) { LOGE("slCreateEngine: %d", (int)r); return -1; }
(*g_sl_engine_obj)->Realize(g_sl_engine_obj, SL_BOOLEAN_FALSE);
(*g_sl_engine_obj)->GetInterface(g_sl_engine_obj, SL_IID_ENGINE, &g_sl_engine);
(*g_sl_engine)->CreateOutputMix(g_sl_engine, &g_sl_out_mix_obj, 0, NULL, NULL);
(*g_sl_out_mix_obj)->Realize(g_sl_out_mix_obj, SL_BOOLEAN_FALSE);
SLDataLocator_AndroidSimpleBufferQueue loc_bq = {
SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, AUDIO_N_BUFS };
SLDataFormat_PCM fmt = {
SL_DATAFORMAT_PCM,
(SLuint32)*channels,
(SLuint32)*speed * 1000, /* OpenSL ES uses milliHz */
SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16,
(*channels == 2) ? (SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT)
: SL_SPEAKER_FRONT_CENTER,
SL_BYTEORDER_LITTLEENDIAN };
SLDataSource src = { &loc_bq, &fmt };
SLDataLocator_OutputMix loc_out = { SL_DATALOCATOR_OUTPUTMIX, g_sl_out_mix_obj };
SLDataSink sink = { &loc_out, NULL };
const SLInterfaceID iid[] = { SL_IID_ANDROIDSIMPLEBUFFERQUEUE };
const SLboolean req[] = { SL_BOOLEAN_TRUE };
r = (*g_sl_engine)->CreateAudioPlayer(g_sl_engine, &g_sl_player_obj,
&src, &sink, 1, iid, req);
if (r != SL_RESULT_SUCCESS) { LOGE("CreateAudioPlayer: %d", (int)r); return -1; }
(*g_sl_player_obj)->Realize(g_sl_player_obj, SL_BOOLEAN_FALSE);
(*g_sl_player_obj)->GetInterface(g_sl_player_obj, SL_IID_PLAY, &g_sl_play);
(*g_sl_player_obj)->GetInterface(g_sl_player_obj,
SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &g_sl_bq);
(*g_sl_bq)->RegisterCallback(g_sl_bq, sl_bufq_callback, NULL);
(*g_sl_play)->SetPlayState(g_sl_play, SL_PLAYSTATE_PLAYING);
/* Prime the callback loop with AUDIO_N_BUFS silent buffers */
for (int i = 0; i < AUDIO_N_BUFS; i++) {
memset(g_sl_bufs[i], 0, sizeof(g_sl_bufs[i]));
(*g_sl_bq)->Enqueue(g_sl_bq, g_sl_bufs[i], AUDIO_BUF_SAMPS * sizeof(int16_t));
}
g_sl_next_buf = 0;
LOGI("android_sound_init: OpenSL ES ready");
return 0;
}
static int android_sound_write(int16_t *pbuf, size_t nr) {
if (!g_sound_enabled) return 0;
pthread_mutex_lock(&g_ring_mutex);
unsigned int free_space = (AUDIO_RING_SAMPLES - 1) - ring_avail_locked();
unsigned int to_write = ((unsigned int)nr < free_space) ? (unsigned int)nr : free_space;
for (unsigned int i = 0; i < to_write; i++) {
g_audio_ring[g_ring_write] = pbuf[i];
g_ring_write = (g_ring_write + 1) & (AUDIO_RING_SAMPLES - 1);
}
pthread_mutex_unlock(&g_ring_mutex);
return 0;
}
static int android_sound_bufferspace(void) {
pthread_mutex_lock(&g_ring_mutex);
int free_space = (int)((AUDIO_RING_SAMPLES - 1) - ring_avail_locked());
pthread_mutex_unlock(&g_ring_mutex);
return free_space;
}
static void android_sound_close(void) {
if (g_sl_play)
(*g_sl_play)->SetPlayState(g_sl_play, SL_PLAYSTATE_STOPPED);
if (g_sl_player_obj) {
(*g_sl_player_obj)->Destroy(g_sl_player_obj);
g_sl_player_obj = NULL; g_sl_play = NULL; g_sl_bq = NULL;
}
if (g_sl_out_mix_obj) {
(*g_sl_out_mix_obj)->Destroy(g_sl_out_mix_obj);
g_sl_out_mix_obj = NULL;
}
if (g_sl_engine_obj) {
(*g_sl_engine_obj)->Destroy(g_sl_engine_obj);
g_sl_engine_obj = NULL; g_sl_engine = NULL;
}
LOGI("android_sound_close: done");
}
static const sound_device_t g_android_sound_device = {
"android",
android_sound_init,
android_sound_write,
NULL, /* dump */
NULL, /* flush */
android_sound_bufferspace,
android_sound_close,
NULL, /* suspend */
NULL, /* resume */
1, /* need_attenuation */
2, /* max_channels */
false /* is_timing_source */
};
/* Stubs for symbols that live in arch-specific or excluded files. */
void main_exit(void) { LOGI("main_exit called — ignoring in JNI context"); }
@@ -124,6 +295,22 @@ void video_canvas_refresh(video_canvas_t *canvas,
unsigned int xs, unsigned int ys,
unsigned int xi, unsigned int yi,
unsigned int w, unsigned int h) {
/* Process any pending disk load on the VICE thread so autostart_disk is
* never called cross-thread (it modifies complex internal VICE state). */
pthread_mutex_lock(&g_pending_lock);
if (g_pending_disk[0] != '\0') {
char path[512];
strncpy(path, g_pending_disk, sizeof(path) - 1);
path[sizeof(path) - 1] = '\0';
g_pending_disk[0] = '\0';
pthread_mutex_unlock(&g_pending_lock);
LOGI("autostart_disk: %s", path);
int asd_ret = autostart_disk(8, 0, path, NULL, 0, AUTOSTART_MODE_RUN);
LOGI("autostart_disk returned %d", asd_ret);
} else {
pthread_mutex_unlock(&g_pending_lock);
}
/* Clamp destination to our framebuffer bounds. */
if (xi >= FRAME_W || yi >= FRAME_H) return;
if (xi + w > FRAME_W) w = FRAME_W - xi;
@@ -140,9 +327,30 @@ void video_canvas_refresh(video_canvas_t *canvas,
}
/* VICE main loop runs in this thread. */
static char *g_vice_argv[12];
static char *g_vice_argv[20];
static int g_vice_argc;
/* Read from fd line-by-line and forward each line to Android logcat.
* Used to capture VICE stderr output which would otherwise be invisible. */
static void *stderr_reader_thread(void *arg) {
int fd = *(int *)arg;
free(arg);
char line[512];
int pos = 0;
char ch;
while (read(fd, &ch, 1) == 1) {
if (ch == '\n' || pos >= (int)sizeof(line) - 1) {
line[pos] = '\0';
if (pos > 0) LOGI("VICE: %s", line);
pos = 0;
} else {
line[pos++] = ch;
}
}
close(fd);
return NULL;
}
static void *vice_thread(void *arg) {
(void)arg;
LOGI("vice_thread: starting main_program");
@@ -194,31 +402,66 @@ JNI_FN(jboolean, initEmulator)(JNIEnv *env, jobject obj, jstring romDir) {
* VICE's compiled-in defaults are "kernal-901227-03.bin" etc., so we
* also pass -kernal/-basic/-chargen to match the actual file names.
* Android FUSE rejects symlinks, so we copy the files into romDir/C64/. */
/* Copy C64 ROMs into romDir/C64/ (sysfile_load searches <dir>/<machine>/). */
char vice_c64_dir[512];
snprintf(vice_c64_dir, sizeof(vice_c64_dir), "%s/C64", dir_copy);
mkdir(vice_c64_dir, 0755);
LOGI("ROM target dir: %s", vice_c64_dir);
const char *roms[] = {"kernal", "basic", "chargen", NULL};
for (int i = 0; roms[i]; i++) {
const char *c64_roms[] = {"kernal", "basic", "chargen", NULL};
for (int i = 0; c64_roms[i]; i++) {
char src[512], dst[512];
snprintf(src, sizeof(src), "%s/%s", dir_copy, roms[i]);
snprintf(dst, sizeof(dst), "%s/%s", vice_c64_dir, roms[i]);
snprintf(src, sizeof(src), "%s/%s", dir_copy, c64_roms[i]);
snprintf(dst, sizeof(dst), "%s/%s", vice_c64_dir, c64_roms[i]);
FILE *fsrc = fopen(src, "rb");
if (!fsrc) { LOGI("ROM not found: %s", src); continue; }
FILE *fdst = fopen(dst, "wb");
if (!fdst) { fclose(fsrc); LOGI("ROM copy open failed: %s errno=%d", dst, errno); continue; }
if (!fdst) { fclose(fsrc); LOGI("ROM copy failed: %s", dst); continue; }
char buf[4096]; size_t n;
while ((n = fread(buf, 1, sizeof(buf), fsrc)) > 0) fwrite(buf, 1, n, fdst);
fclose(fsrc); fclose(fdst);
LOGI("ROM copied: %s", roms[i]);
LOGI("ROM copied: %s", c64_roms[i]);
}
/* Copy 1541 drive ROM into romDir/DRIVES/1541 (sysfile_load path for drives).
* Required for true 1541 emulation so the game's custom fast loader works.
* User places the file named "1541" alongside kernal/basic/chargen. */
char vice_drives_dir[512];
snprintf(vice_drives_dir, sizeof(vice_drives_dir), "%s/DRIVES", dir_copy);
mkdir(vice_drives_dir, 0755);
{
char src[512], dst[512];
snprintf(src, sizeof(src), "%s/1541", dir_copy);
snprintf(dst, sizeof(dst), "%s/DRIVES/1541", dir_copy);
FILE *fsrc = fopen(src, "rb");
if (fsrc) {
FILE *fdst = fopen(dst, "wb");
if (fdst) {
char buf[4096]; size_t n;
while ((n = fread(buf, 1, sizeof(buf), fsrc)) > 0) fwrite(buf, 1, n, fdst);
fclose(fdst);
LOGI("ROM copied: 1541");
}
fclose(fsrc);
} else {
LOGI("1541 ROM not found — drive may not work with fast loaders");
}
}
static char dir_arg[512];
snprintf(dir_arg, sizeof(dir_arg), "%s", dir_copy);
/* -VICIIborders none → border mode 3 → no borders → 320×200 canvas.
* Default (mode 0 = normal) gives 384×272 which doesn't match our framebuffer. */
g_vice_argc = 11;
/* -VICIIborders none → 320×200 canvas, no borders.
* -dos1541 1541 → drive ROM filename (overrides VICE's long default).
* -iecdevice8 → enable IEC device emulation on unit 8 (sets the ID
* bit in iecbus_device_index). This is the flag that
* actually routes IEC bus traffic for unit 8 to the
* virtual device layer. Without it the index resolves
* to IECBUS_DEVICE_NONE → "DEVICE NOT PRESENT".
* -virtualdev8 → enable KERNAL trap-based emulation on unit 8 (VD
* bit). Together with -iecdevice8 this gives index 3
* → IECBUS_DEVICE_IECDEVICE and fast KERNAL traps.
* -sounddev android → select our custom OpenSL ES driver registered above
* instead of VICE's built-in dummy (silent) driver. */
g_vice_argc = 17;
g_vice_argv[0] = "x64";
g_vice_argv[1] = "-directory";
g_vice_argv[2] = dir_arg;
@@ -230,7 +473,42 @@ JNI_FN(jboolean, initEmulator)(JNIEnv *env, jobject obj, jstring romDir) {
g_vice_argv[8] = "chargen";
g_vice_argv[9] = "-VICIIborders";
g_vice_argv[10] = "none";
g_vice_argv[11] = NULL;
g_vice_argv[11] = "-dos1541";
g_vice_argv[12] = "1541";
g_vice_argv[13] = "-iecdevice8";
g_vice_argv[14] = "-virtualdev8";
g_vice_argv[15] = "-sounddev";
g_vice_argv[16] = "android";
g_vice_argv[17] = NULL;
/* Redirect stdout and stderr to Android logcat.
* VICE's archdep_open_default_log_file() detects a FIFO on stdout and
* writes all log output there instead of opening a log file, so piping
* stdout captures trap installation messages, autostart progress, etc.
* Stderr captures VICE command-line parse errors. */
int stdout_fds[2], stderr_fds[2];
if (pipe(stdout_fds) == 0) {
dup2(stdout_fds[1], STDOUT_FILENO);
close(stdout_fds[1]);
int *rfd = malloc(sizeof(int));
*rfd = stdout_fds[0];
pthread_t stdout_tid;
pthread_create(&stdout_tid, NULL, stderr_reader_thread, rfd);
pthread_detach(stdout_tid);
}
if (pipe(stderr_fds) == 0) {
dup2(stderr_fds[1], STDERR_FILENO);
close(stderr_fds[1]);
int *rfd = malloc(sizeof(int));
*rfd = stderr_fds[0];
pthread_t stderr_tid;
pthread_create(&stderr_tid, NULL, stderr_reader_thread, rfd);
pthread_detach(stderr_tid);
}
/* Register the android sound driver before VICE's sound_open() runs.
* pthread_create provides a memory barrier so the VICE thread sees our write. */
sound_register_device(&g_android_sound_device);
pthread_t tid;
int rc = pthread_create(&tid, NULL, vice_thread, NULL);
@@ -251,9 +529,15 @@ JNI_FN(jboolean, loadDisk)(JNIEnv *env, jobject obj, jstring path) {
(void)obj;
#ifdef HAVE_VICE_SRC
const char *p = (*env)->GetStringUTFChars(env, path, NULL);
int rc = file_system_attach_disk(8, 0, p);
/* Queue the disk path for autostart on the VICE thread.
* autostart_disk must not be called cross-thread — video_canvas_refresh
* picks this up on the next frame and calls it from the VICE thread. */
pthread_mutex_lock(&g_pending_lock);
strncpy(g_pending_disk, p, sizeof(g_pending_disk) - 1);
g_pending_disk[sizeof(g_pending_disk) - 1] = '\0';
pthread_mutex_unlock(&g_pending_lock);
(*env)->ReleaseStringUTFChars(env, path, p);
return (jboolean)(rc == 0);
return JNI_TRUE;
#else
(void)env; (void)path;
return JNI_FALSE;
@@ -284,3 +568,8 @@ JNI_FN(void, injectKey)(JNIEnv *env, jobject obj, jint keyCode, jboolean pressed
(void)keyCode; (void)pressed;
#endif
}
JNI_FN(void, setSoundEnabled)(JNIEnv *env, jobject obj, jboolean enabled) {
(void)env; (void)obj;
g_sound_enabled = enabled ? 1 : 0;
}
@@ -30,33 +30,32 @@
android:textSize="12sp" />
</LinearLayout>
<!-- Import buttons -->
<!-- Import disk images + sound toggle -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="4dp">
<Button
android:id="@+id/btn_import_roms"
android:layout_width="0dp"
android:layout_height="36dp"
android:layout_weight="1"
android:layout_marginEnd="2dp"
android:text="Import ROMs"
android:textSize="11sp"
style="?attr/materialButtonOutlinedStyle" />
android:layout_marginTop="4dp"
android:orientation="horizontal">
<Button
android:id="@+id/btn_import_disks"
android:layout_width="0dp"
android:layout_height="36dp"
android:layout_weight="1"
android:layout_marginStart="2dp"
android:layout_marginEnd="2dp"
android:text="Import Disks"
android:textSize="11sp"
style="?attr/materialButtonOutlinedStyle" />
<Button
android:id="@+id/btn_sound"
android:layout_width="0dp"
android:layout_height="36dp"
android:layout_weight="1"
android:layout_marginStart="2dp"
android:text="Sound: OFF"
android:textSize="11sp"
style="?attr/materialButtonOutlinedStyle" />
</LinearLayout>
<!-- Episode selector — row A (parts 1A4A), row B (parts 1B4B) -->
Binary file not shown.
Binary file not shown.
Binary file not shown.