Laufzeit-Code vom Dev-Werkzeug getrennt: - game/: das eigentliche Spiel, startbar per `python -m game [--once]`. - formats.py (war tools/kellogg_formats.py) -- Asset-Parser, Kern-Bibliothek - audio.py (war tools/tfmx_audio.py) -- TFMX-Musik-Wrapper - intro.py (war tools/intro_sequence.py) -- Boot-/Intro-Sequenz - tfmx_player/(war tools/tfmx_player/) -- MIT-C-Renderer - __init__.py / __main__.py fuer `python -m game` - tools/: nur noch Dev-Werkzeuge (dat_extract, pcc_to_png, render_assets), importieren jetzt aus game.formats. - README.md: Ueberblick, Start, Struktur, Voraussetzungen, Rechtliches. - .gitignore: tfmx_player-Binary-Pfad auf game/ nachgezogen. Intro laeuft unveraendert (headless-Smoke-Test `python -m game --once` exit 0). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
62 lines
2.6 KiB
C
62 lines
2.6 KiB
C
// Copyright (c) 2026 Peter Fors
|
|
// SPDX-License-Identifier: MIT
|
|
//
|
|
// Common interface every ported replayer exports. Each player's header declares
|
|
// a `struct player_api <name>_api` global that the test player (or any host)
|
|
// can iterate to auto-detect the right replayer for a file.
|
|
//
|
|
// Audio output is interleaved float stereo, nominal range [-1.0, 1.0]. Players
|
|
// ACCUMULATE into the caller's buffer (caller must pre-clear). They do NOT
|
|
// clip; the host is responsible for any final saturation, dithering, or
|
|
// conversion to the audio backend's native sample format.
|
|
|
|
#pragma once
|
|
|
|
#include <stdint.h>
|
|
|
|
// Optional file-loader callback used by players that read companion files
|
|
// (IFF SMUS instrument files, Face The Music external samples, etc.). Hosts
|
|
// that pass a loader implement `fetch` to resolve a logical name (e.g.
|
|
// "Instruments/Bass6.instr") to a heap-allocated byte buffer + length. The
|
|
// player calls free() on the returned pointer when done.
|
|
struct player_loader {
|
|
void *ctx;
|
|
uint8_t *(*fetch)(void *ctx, const char *name, uint32_t *out_len);
|
|
};
|
|
|
|
struct player_api {
|
|
const char *name;
|
|
const char **extensions; /* null-terminated list of lowercase extensions, no dot */
|
|
void *(*init)(void *data, uint32_t len, int32_t sample_rate);
|
|
void (*free)(void *state);
|
|
void (*get_audio)(void *state, float *output, int32_t frames);
|
|
// Optional: when non-null and the host has a loader for sibling files,
|
|
// the host should prefer this entry point. Players that don't need
|
|
// external files leave this null and the host falls back to init().
|
|
void *(*init_ex)(void *data, uint32_t len, int32_t sample_rate, struct player_loader *loader);
|
|
};
|
|
|
|
// [=]===^=[ player_get_audio_s16 ]===============================================================[=]
|
|
// Convenience wrapper for hosts that want signed-16 PCM out. Drives the
|
|
// player's float get_audio into the caller-supplied scratch buffer (must hold
|
|
// at least frames * 2 floats), clears it first, then converts with hard
|
|
// saturation into `output` (frames * 2 int16 stereo samples). The scratch is
|
|
// caller-owned so the hot path never allocates; reuse the same buffer across
|
|
// calls.
|
|
#include <string.h>
|
|
static void player_get_audio_s16(struct player_api *api, void *state, int16_t *output, float *scratch, int32_t frames) {
|
|
int32_t samples = frames * 2;
|
|
memset(scratch, 0, (size_t)samples * sizeof(float));
|
|
api->get_audio(state, scratch, frames);
|
|
for(int32_t i = 0; i < samples; ++i) {
|
|
float v = scratch[i] * 32767.0f;
|
|
if(v > 32767.0f) {
|
|
v = 32767.0f;
|
|
}
|
|
if(v < -32768.0f) {
|
|
v = -32768.0f;
|
|
}
|
|
output[i] = (int16_t)v;
|
|
}
|
|
}
|