Add C64 SID audio output via OpenSL ES with sound toggle
This commit is contained in:
@@ -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)
|
||||
|
||||
+3
@@ -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") }
|
||||
|
||||
|
||||
+29
-42
@@ -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 1A–4A), row B (parts 1B–4B) -->
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user