Skip to content

LibreShockwave

Bankn8II©$A edited this page Sep 14, 2026 · 1 revision

Welcome to the LibreShockwave wiki!

lokalt: G:\EXTENSIONS_L_ATOM\emo_macromedia_shockwave

lokalt: E:\WEBB_OFFLINE\virtual-oscilloscope

image

https://uoxyc.github.io/LibreShockwave/web/index.html

image

https://uoxyc.github.io/LibreShockwave/web/test-harness.html

image

https://uoxyc.github.io/LibreShockwave/debugger/debug-harness.html

image

LibreShockwave

Website

LibreShockwave is a C++20 project for parsing Macromedia/Adobe Director and Shockwave files (.dir, .dxr, .dcr, .cct, .cst).

It won't just be an emulator: the goal is to eventually become a full software suite and ecosystem, with a Director player, alongside a replacement for Director MX (2004, 11.5, etc.) as an open source replacement for Macromedia/Adobe Shockwave.

Requirements

  • CMake 3.20 or newer

  • A C++20 compiler

  • zlib development headers, or a zlib-compatible zlib-ng package

  • Optional: Ninja for faster incremental builds

  • Optional: Qt6 Widgets (or Qt5 Widgets) for the desktop debugger

  • Optional: Emscripten for the browser/WASM target

  • Optional: Node.js and npm for browser/WASM verification

Linux Setup

Use the build script to print the dependency commands for your Linux distribution:

./build.sh --deps

The script knows the native package names for Debian, Ubuntu, Linux Mint, Pop!_OS, Fedora, RHEL, Rocky Linux, AlmaLinux, CentOS Stream, Arch Linux, Manjaro, openSUSE, and Alpine Linux.

To let the script install native dependencies for the detected distribution:

./build.sh --install-deps

Automatic installation is optional; by default the script only builds.

Build

./build.sh

By default this configures a Debug build, builds the C++ tests and native probe tools, and runs ctest.

Useful variants:

./build.sh --release
./build.sh --generator Ninja
./build.sh --jobs 8
./build.sh --target libreshockwave_probe --no-tests
./build.sh --clean

The main library target is LibreShockwave::libreshockwave.

Asset catalogue extractor

Extract a directory of Director/Shockwave files into the same asset layout used by the v31 asset catalogue:

./cmake-build-debug/cpp/libreshockwave_asset_extractor path/to/dcr-files path/to/output

The output contains one directory per input file with manifest.tsv, file_info.tsv, and decoded bitmaps/, text/, sounds/, palettes/, and raw_chunks/, and scripts/ subdirectories. The output directory is optional; if omitted, the generated catalogue is written into the input directory.

The scripts/ directory contains native decompiled Lingo source (.ls) and resolved bytecode listings (.lsasm). Script assets are also added to the per-file manifest.tsv when they can be associated with a cast member.

Formats

  • RIFX, XFIR, RIFF, and FFIR containers
  • Big-endian and little-endian Director files
  • Afterburner-compressed files (.dcr, .cct)
  • Director versions 4 through 12

Capabilities

Reading

  • Cast members: bitmaps, text, scripts, sounds, shapes, palettes, fonts, and Shockwave3D metadata
  • Lingo bytecode with symbol, global, property, and handler resolution
  • Score and timeline data, including frames, channels, labels, palettes, tempos, and behavior intervals
  • File metadata, including stage dimensions, tempo, version, movie type, endian mode, and external cast paths

Extraction

  • Bitmaps: 1/2/4/8/16/32-bit depths, palette support, native alpha, matte data, and ARGB output buffers
  • Text: field and rich text cast members through STXT and XMED data
  • Sound: MP3 extraction, PCM WAV conversion, and IMA ADPCM decoding
  • Palettes: built-in Director palettes and custom CLUT chunks
  • Fonts: PFR1 font parsing and conversion to TrueType (.ttf) bytes

Debugger

LibreShockwave includes a Qt desktop debugger and a browser/WASM debugger harness. Both expose movie playback, Lingo bytecode and decompiled code, breakpoints, stepping, call-stack and variable inspection, and watch expressions.

Desktop debugger

The desktop debugger is built only when Qt6 Widgets or Qt5 Widgets is available. Build its target explicitly:

./build.sh --target libreshockwave_debugger --no-tests
./cmake-build-debug/cpp/libreshockwave_debugger_app/libreshockwave_debugger path/to/movie.dcr
Screenshot_20260808_153804

Use --release and cmake-build-release for a Release build. The executable is placed under <build-dir>/cpp/libreshockwave_debugger_app/ when a custom build directory is used.

The command-line argument is optional. Pass a movie or recording as the positional argument; --play starts ordinary playback or recording replay after loading (including asynchronous HTTP(S) movies):

libreshockwave_debugger --play path/to/movie.dcr
libreshockwave_debugger --play path/to/session.lswdebug

You can also open local .dir, .dcr, .dxr, .cct, and .cst files from File → Open Movie, or enter an HTTP(S) movie URL with File → Open URL. Network-dependent movies can receive their key=value external parameters from Parameters → Edit Parameters; parameter changes take effect after reloading the movie. The window remembers recent movies, parameters, layout, and breakpoints per movie.

The debugger workflow is:

  1. Load a movie and press Play.
  2. Expand a cast in Movies & Scripts, select a script and handler, then use the Bytecode or Decompiled code view.
  3. Click a code gutter line to toggle a breakpoint, or press F9 while paused to toggle one at the current instruction.
  4. When execution pauses, inspect the call stack, locals, properties, globals, and watches. Enter an expression in the Watches panel to evaluate it in the paused context.

Playback and stepping shortcuts are:

Key Action
F5 Continue
Esc Pause
F9 Toggle breakpoint
F10 Step over
F11 Step into
Shift+F11 Step out

Play & Record saves stage mouse and keyboard input as a .lswdebug file. Open that file with File → Open Debug Recording to reload its movie and external parameters and replay the captured input.

C++ API Examples

When vendoring the repository inside another CMake project:

add_subdirectory(path/to/LibreShockwave)
target_link_libraries(my_tool PRIVATE LibreShockwave::libreshockwave)
target_compile_features(my_tool PRIVATE cxx_std_20)

Load File Example

The C++ API currently loads from memory. Read the file bytes, then call DirectorFile::load.

#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <iterator>
#include <stdexcept>
#include <vector>

#include "libreshockwave/DirectorFile.hpp"

std::vector<std::uint8_t> readFile(const std::filesystem::path& path) {
    std::ifstream input(path, std::ios::binary);
    if (!input) {
        throw std::runtime_error("Unable to open file");
    }
    return {
        std::istreambuf_iterator<char>(input),
        std::istreambuf_iterator<char>()
    };
}

int main(int argc, char** argv) {
    const std::filesystem::path moviePath = argc > 1 ? argv[1] : "movie.dcr";
    auto file = libreshockwave::DirectorFile::load(readFile(moviePath));
    file->setBasePath(moviePath.parent_path().string());

    std::cout << file->stageWidth() << "x" << file->stageHeight()
              << " tempo=" << file->tempo()
              << " cast members=" << file->castMembers().size()
              << '\n';
}

Cast Example

for (const auto& member : file->castMembers()) {
    if (member->isBitmap()) {
        // Decode with file->decodeBitmap(member).
    }
    if (member->isScript()) {
        // Resolve with file->getScriptForCastMember(member).
    }
    if (member->isSound()) {
        // Read linked snd chunks.
    }
    if (member->isText()) {
        // Read linked STXT/XMED text.
    }
}

Bitmap Example

#include <cstdint>
#include <vector>
// #define STB_IMAGE_WRITE_IMPLEMENTATION
// #include "stb_image_write.h"

for (const auto& member : file->castMembers()) {
    if (!member->isBitmap()) {
        continue;
    }

    if (auto bitmap = file->decodeBitmap(member)) {
        const int width = bitmap->width();
        const int height = bitmap->height();
        const auto& argbPixels = bitmap->pixels();
        // Example uses stb_image_write.h (https://github.com/nothings/stb).
        std::vector<std::uint8_t> rgbaPixels;
        rgbaPixels.reserve(static_cast<size_t>(width) * height * 4);
        for (const auto argb : argbPixels) {
            rgbaPixels.push_back((argb >> 16) & 0xFF);
            rgbaPixels.push_back((argb >> 8) & 0xFF);
            rgbaPixels.push_back(argb & 0xFF);
            rgbaPixels.push_back((argb >> 24) & 0xFF);
        }

        stbi_write_png(
            (member->name() + ".png").c_str(),
            width,
            height,
            4,
            rgbaPixels.data(),
            width * 4);
    }
}

Text Example

for (const auto& member : file->castMembers()) {
    auto text = file->getTextForMember(member);
    if (text && !text->text().empty()) {
        std::cout << member->name() << ": " << text->text() << '\n';
    }
}

Sound Example

#include <filesystem>
#include <fstream>
#include <memory>

#include "libreshockwave/audio/SoundConverter.hpp"
#include "libreshockwave/chunks/SoundChunk.hpp"
#include "libreshockwave/format/ChunkType.hpp"

for (const auto& member : file->castMembers()) {
    if (!member->isSound()) {
        continue;
    }

    for (const auto& chunk : file->getLinkedChunksForMember(
             member,
             libreshockwave::format::fourCC(libreshockwave::format::ChunkType::snd_))) {
        auto sound = std::dynamic_pointer_cast<libreshockwave::chunks::SoundChunk>(chunk);
        if (!sound) {
            continue;
        }

        const auto bytes = sound->isMp3()
            ? libreshockwave::audio::SoundConverter::extractMp3(*sound).value_or(std::vector<std::uint8_t>{})
            : libreshockwave::audio::SoundConverter::toWav(*sound);

        std::ofstream output(member->name() + (sound->isMp3() ? ".mp3" : ".wav"), std::ios::binary);
        output.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
    }
}

Font Example

Director files can embed PFR1 font data inside XMED chunks. LibreShockwave can parse PFR1 data and convert it to standard TrueType bytes.

#include <filesystem>
#include <fstream>
#include <memory>

#include "libreshockwave/chunks/RawChunk.hpp"
#include "libreshockwave/font/Pfr1Font.hpp"
#include "libreshockwave/font/Pfr1TtfConverter.hpp"
#include "libreshockwave/format/ChunkType.hpp"

for (const auto& member : file->castMembers()) {
    for (const auto& chunk : file->getLinkedChunksForMember(
             member,
             libreshockwave::format::fourCC(libreshockwave::format::ChunkType::XMED))) {
        auto raw = std::dynamic_pointer_cast<libreshockwave::chunks::RawChunk>(chunk);
        if (!raw || raw->data().size() < 4) {
            continue;
        }

        const auto& data = raw->data();
        if (data[0] != 'P' || data[1] != 'F' || data[2] != 'R' || data[3] != '1') {
            continue;
        }

        auto font = libreshockwave::font::Pfr1Font::parse(data);
        auto ttf = libreshockwave::font::Pfr1TtfConverter::convert(*font, font->fontName);

        std::ofstream output(member->name() + ".ttf", std::ios::binary);
        output.write(reinterpret_cast<const char*>(ttf.data()), static_cast<std::streamsize>(ttf.size()));
    }
}

Script Example

for (const auto& script : file->scripts()) {
    auto names = file->getScriptNamesForScript(script);

    for (const auto& global : script->getGlobalNames(names.get())) {
        std::cout << "global " << global << '\n';
    }

    for (const auto& handler : script->handlers()) {
        std::cout << "handler " << script->getHandlerName(handler, names.get()) << '\n';

        for (const auto& instruction : handler.instructions) {
            std::cout << "  " << instruction.offset << ": "
                      << instruction.toString() << '\n';
        }
    }
}

Score Example

if (auto score = file->scoreChunk()) {
    std::cout << "frames=" << score->getFrameCount()
              << " channels=" << score->getChannelCount()
              << '\n';

    for (const auto& interval : score->frameIntervals()) {
        std::cout << "interval "
                  << interval.primary.startFrame
                  << "-"
                  << interval.primary.endFrame
                  << '\n';
    }
}

if (auto labels = file->frameLabelsChunk()) {
    for (const auto& label : labels->labels()) {
        std::cout << label.frameNum.value() << ": " << label.label << '\n';
    }
}

Frame Example

#include "libreshockwave/player/Player.hpp"

libreshockwave::player::Player player(file);
player.setExternalParams({
    {"sw1", "external.variables.txt=http://example.com/vars.txt"},
    {"sw2", "connection.info.host=127.0.0.1"}
});
player.play();

for (int frame = 0; frame < 10 && player.tick(); ++frame) {
    auto rendered = player.frameSnapshot().renderFrame();
    const auto& argbPixels = rendered.pixels();
    // rendered.width(), rendered.height(), and argbPixels describe the frame.
}

player.shutdown();

Debug Example

player.setDebugEnabled(true);
player.setErrorListener([](std::string_view message, std::string_view detail) {
    std::cerr << "Lingo error: " << message << '\n' << detail << '\n';
});

auto callStack = player.formatLingoCallStack();

For bytecode-level debugging, attach a libreshockwave::player::debug::DebugControllerApi implementation with player.setDebugController(...).

Browser Player

Build the Emscripten target from an Emscripten-enabled shell:

./build.sh --wasm --release

The generated browser player is assembled under:

cmake-build-wasm/cpp/wasm-dist/

When testing in a browser, serve that output directory over HTTP instead of opening files directly from disk:

cd cmake-build-wasm/cpp/wasm-dist
python3 -m http.server 8098 --bind 127.0.0.1
http://127.0.0.1:8098/

If you use another web server, serve the files from the same directory. The current WASM build is single-threaded and does not use Emscripten pthreads or shared memory, so SharedArrayBuffer cross-origin isolation headers are not required.

Direct remote movie and asset URLs still use normal browser fetch() behavior, so those servers must either allow CORS or be served through the same origin.

Embed Example

The browser target exposes LibreShockwavePlayer from libreshockwave-cpp-player.js.

<canvas id="stage" width="640" height="480"></canvas>
<script src="libreshockwave-cpp-player.js"></script>
<script>
  const player = LibreShockwavePlayer.create("stage", {
    params: {
      sw1: "external.variables.txt=http://example.com/vars.txt"
    },
    debugPlayback: true,
    onLoad(info) {
      console.log(info.width + "x" + info.height);
    },
    onError(message) {
      console.error(message);
    }
  });

  player.load("movie.dcr");
</script>

The following files must be served from the same directory unless basePath is supplied:

File Purpose
index.html Ready-made browser player page
libreshockwave-cpp-player.js Main browser player API
libreshockwave-cpp-worker.js Worker wrapper for the WASM runtime
libreshockwave-cpp-wasm.js Emscripten module loader
libreshockwave-cpp-wasm.wasm Compiled runtime

Layout

cpp/
  CMakeLists.txt
  include/libreshockwave/       Public C++ headers
  src/                          Runtime, SDK, VM, and player sources
  apps/
    tools/                      Native probes and browser fixture checker
    wasm/                       WASM bridge entry points
  resources/fonts/              Bundled runtime font assets
  tests/                        C++ regression and contract tests
web/
  ...                          Browser player and worker assets
docs/
  rendering-rules.md            Renderer behavior notes
  inks.txt                      Director ink behavior reference

Verify

node --check web/libreshockwave-cpp-player.js
node --check web/libreshockwave-cpp-worker.js
node --check cpp/apps/tools/browser_fixture_check.js
node --check cpp/apps/tools/browser_index_check.js
./build.sh
git diff --check

Run fixture probes that match the risk of the change before saving a C++ port slice.

Acknowledgements

LibreShockwave could not have been done without these projects:

License

See LICENCE.

Clone this wiki locally