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:
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;
+}