exl100

Émulateur Exelvision EXL 100 (1984) dans le navigateur : dsp-cpp en WebAssembly, cartouches et cassettes BASIC
git clone https://git.schurger.org/exl100.git
Log | Files | Refs | README

commit 3b8a0138b9fa6d21aa5dd1efede3913574f8f2cf
parent 3d2d82a0d2217761372fcbbe78f64e4c8f7dac8f
Author: Jean Schurger <jean@schurger.org>
Date:   Wed,  4 Nov 2026 18:16:31 -0500

Couverture : +3 cartouches webmo (Pindo/Edigraf/Exelvox), +démos ; cassettes webmo écartées

Vérification de webmo.fr : les 17 cartouches EXL 100 y sont toutes (TOSEC en avait 15 ;
PINDO/EDIGRAF/EXELVOX ajoutées de webmo). Démos TOSEC intégrées (Faites parler votre EXL100).
Les cassettes exclusives à webmo n'existent qu'en .wav analogiques : un convertisseur .wav->.k7
(tools/wav2k7.cpp, décodeur de dsp en wasm/Node) les rend compactes et elles se MONTENT, mais
les données ne se chargent pas en programme fiable (décodage avec pertes) — écartées pour ne pas
polluer la liste de programmes cassés. Résultat : 18 cartouches + 108 cassettes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QyJB9yqUAYm3wRQi4uytj9

Diffstat:
M.gitignore | 2++
MMakefile | 13++++++++++---
Mtools/get_cartridges.py | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/wav2k7.cpp | 34++++++++++++++++++++++++++++++++++
4 files changed, 106 insertions(+), 3 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -9,3 +9,5 @@ web/index.html .DS_Store roms/cartridges/ roms/tapes/ +tools/wav2k7.js +tools/wav2k7.wasm diff --git a/Makefile b/Makefile @@ -23,10 +23,17 @@ CMFLAGS = -DCMAKE_BUILD_TYPE=Release -DDSP_DRIVERS=exelv -DDSP_PER_DRIVER_TARGET all: build -# Cartouches EXL 100 (pack TOSEC sur archive.org) extraites dans roms/cartridges/ ; non versionnées. -# La page les embarque dans dsp.data et les propose au menu, sans téléchargement par fichier. +# Convertisseur .wav -> .k7 (décodeur de cassette de dsp compilé en wasm/Node) : rend compactes les +# cassettes webmo qui n'existent qu'en gros enregistrements .wav. +tools/wav2k7.js: tools/wav2k7.cpp upstream/src/drivers/computers/exelv_tape.cpp + $(DOCKER) em++ -O2 -std=c++17 -I /work/upstream/src /work/tools/wav2k7.cpp \ + /work/upstream/src/drivers/computers/exelv_tape.cpp \ + -sNODERAWFS=1 -sENVIRONMENT=node -sALLOW_MEMORY_GROWTH=1 -o /work/tools/wav2k7.js + +# Cartouches et cassettes EXL 100 (pack TOSEC sur archive.org + extras webmo.fr) dans roms/ ; +# non versionnées. La page les embarque dans dsp.data et les propose au menu. cartridges: roms/cartridges/titles.json -roms/cartridges/titles.json: tools/get_cartridges.py +roms/cartridges/titles.json: tools/get_cartridges.py upstream python3 -I tools/get_cartridges.py # Clone d'amont épinglé (idempotent) diff --git a/tools/get_cartridges.py b/tools/get_cartridges.py @@ -8,6 +8,7 @@ et les exécute sans offrir de téléchargement par fichier. Lancé par « make Usage : python3 -I tools/get_cartridges.py """ +import glob import io import json import os @@ -70,6 +71,61 @@ WEBMO_CARTS = { "edigraf.bin": ("EDIGRAF.ROM", {"t": "Edigraf", "by": "P. Glajean", "y": "1987", "cat": "application"}), "exelvox.bin": ("EXELVOX.ROM", {"t": "Exelvox", "by": "Exelvision", "y": "198x", "cat": "application"}), } +WAV2K7 = os.path.join(HERE, "tools", "wav2k7.js") +MEDIAS_PAGE = "https://p7398.webmo.fr/httpdocs/exelvision/website/index.php?page=restauration-de-medias2" + +def norm(s): + return re.sub(r"[^a-z0-9]", "", s.lower()) + +# Cassettes présentes sur webmo.fr mais absentes de TOSEC : elles n'existent qu'en gros .wav, qu'on +# convertit en .k7 compact avec le décodeur de dsp (tools/wav2k7.js). Évite les doublons (nom normalisé). +def webmo_cassettes(titles): + have = {norm(m["t"]) for m in titles.values()} + page = urllib.request.urlopen( + urllib.request.Request(MEDIAS_PAGE, headers={"User-Agent": "Mozilla/5.0"}), timeout=60 + ).read().decode("utf-8", "replace") + rars = sorted(set(re.findall(r"telechargement/([A-Za-z0-9_-]+)\.rar", page))) + added = 0 + # Quelques doublons dont le nom webmo diffère trop du nom TOSEC pour être détecté autrement. + alias = {"ww3": "thirdworldwar", "symtrans": "symetriestranslations", + "faitesparlervotreexl100v1": "faitesparlervotreexl100volume1v11", + "faitesparlervotreexl100v2": "faitesparlervotreexl100volume2v11"} + for base in rars: + nb = norm(base) + if nb in have or re.sub(r"\d+$", "", nb) in have or alias.get(nb, nb) in have: + continue + # sous-chaîne (frise⊂frises, poker⊂exelpoker, vega⊂vegax4), noms assez longs + if any(len(nb) >= 4 and len(h) >= 4 and (nb in h or h in nb) for h in have): + continue + try: + data = urllib.request.urlopen(urllib.request.Request( + f"{WEBMO}/{base}.rar", headers={"User-Agent": "Mozilla/5.0"}), timeout=90).read() + except Exception as e: + print(f" {base}: téléchargement KO ({e})", file=sys.stderr) + continue + with tempfile.TemporaryDirectory() as td: + rp = os.path.join(td, base + ".rar") + with open(rp, "wb") as f: + f.write(data) + subprocess.run(["unar", "-q", "-D", "-o", td, rp], check=False, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + wavs = sorted(glob.glob(os.path.join(td, "**", "*.wav"), recursive=True), + key=os.path.getsize, reverse=True) + if not wavs: + print(f" {base}: pas de .wav", file=sys.stderr) + continue + out = f"{slug_of(base)}.k7" + k7 = os.path.join(TAPES_OUT, out) + r = subprocess.run(["node", WAV2K7, wavs[0], k7], stderr=subprocess.DEVNULL) + if r.returncode != 0 or not os.path.exists(k7) or os.path.getsize(k7) < 16: + print(f" {base}: conversion KO", file=sys.stderr) + continue + titles[out] = {"t": base.replace("-", " ").title(), "by": "webmo.fr", "y": "", "cat": ""} + have.add(nb) + added += 1 + print(f" {out:26s} {os.path.getsize(k7):6d} o (webmo) {base}", file=sys.stderr) + print(f"{added} cassettes webmo ajoutées", file=sys.stderr) + def webmo_cartridges(titles): for out, (fn, meta) in WEBMO_CARTS.items(): req = urllib.request.Request(f"{WEBMO}/{fn}", headers={"User-Agent": "Mozilla/5.0"}) @@ -155,6 +211,10 @@ def extract_tapes(tmp, limit=160): break if len(titles) >= limit: break + # NOTE : webmo_cassettes(titles) ajouterait les cassettes exclusives à webmo.fr, mais elles n'y + # existent qu'en enregistrements .wav analogiques : converties en .k7 (tools/wav2k7.js), elles se + # MONTENT et démarrent le chargement (ESC -> Lect.) mais les données ne se chargent pas en programme + # fiable (décodage avec pertes). On s'en tient donc au corpus TOSEC, propre et chargeable. with open(os.path.join(TAPES_OUT, "titles.json"), "w", encoding="utf-8") as f: json.dump(dict(sorted(titles.items(), key=lambda kv: kv[1]["t"].lower())), f, ensure_ascii=False, indent=1) diff --git a/tools/wav2k7.cpp b/tools/wav2k7.cpp @@ -0,0 +1,34 @@ +// Convertit un enregistrement de cassette EXL 100 (.wav) en image .k7 compacte, avec le décodeur +// de cassette de dsp-cpp lui-même (exelv_tape). Un .k7 n'est que la suite d'octets décodés. +// Compilé en WebAssembly exécutable par Node (NODERAWFS pour lire/écrire les fichiers de l'hôte) : +// emcc -I upstream/src tools/wav2k7.cpp upstream/src/drivers/computers/exelv_tape.cpp \ +// -sNODERAWFS=1 -sENVIRONMENT=node -o tools/wav2k7.js +// node tools/wav2k7.js in.wav out.k7 +#include "drivers/computers/exelv_tape.h" +#include <cstdio> +#include <string> +#include <vector> + +int main(int argc, char** argv) { + if (argc < 3) { std::fprintf(stderr, "usage: wav2k7 in.wav out.k7\n"); return 2; } + FILE* f = std::fopen(argv[1], "rb"); + if (!f) { std::fprintf(stderr, "cannot open %s\n", argv[1]); return 2; } + std::fseek(f, 0, SEEK_END); + long n = std::ftell(f); + std::fseek(f, 0, SEEK_SET); + std::vector<uint8_t> file(size_t(n < 0 ? 0 : n)); + if (n > 0 && std::fread(file.data(), 1, size_t(n), f) != size_t(n)) { std::fclose(f); return 2; } + std::fclose(f); + + dsp::ExelTape tape; + std::string err; + if (!tape.load_wav(file, &err)) { std::fprintf(stderr, "decode failed: %s\n", err.c_str()); return 1; } + const auto& b = tape.bytes(); + if (b.empty()) { std::fprintf(stderr, "empty result\n"); return 1; } + FILE* o = std::fopen(argv[2], "wb"); + if (!o) { std::fprintf(stderr, "cannot write %s\n", argv[2]); return 2; } + std::fwrite(b.data(), 1, b.size(), o); + std::fclose(o); + std::fprintf(stderr, "%zu octets\n", b.size()); + return 0; +}