mirror of
https://github.com/alfishe/amiga-bootcamp.git
synced 2026-06-13 00:26:28 +00:00
More content added
This commit is contained in:
parent
5fac29ccd5
commit
8133b3a6cb
90 changed files with 7794 additions and 705 deletions
|
|
@ -10,12 +10,15 @@ Shared libraries beyond the core exec/dos/graphics/intuition subsystems. These p
|
|||
|---|---|
|
||||
| [utility.md](utility.md) | TagItem lists with chaining (TAG_MORE), callback hooks (register convention), date/time utilities, tag iteration patterns |
|
||||
| [expansion.md](expansion.md) | Zorro II/III bus architecture, AutoConfig ROM layout, board enumeration, FPGA implementation notes |
|
||||
| [icon.md](icon.md) | Workbench icons (.info): DiskObject structure, ToolType parsing, icon types, OS 3.5+ true-colour icons |
|
||||
| [icon.md](icon.md) | Workbench icons (.info): DiskObject structure, ToolType parsing, icon types, OS 3.5+ true-color icons |
|
||||
| [workbench.md](workbench.md) | Workbench integration: WBStartup handling, AppWindow drag-and-drop, AppIcon, AppMenuItem |
|
||||
| [iffparse.md](iffparse.md) | IFF file parsing: ILBM/8SVX/ANIM, BitMapHeader, ByteRun1 compression, clipboard integration |
|
||||
| [locale.md](locale.md) | Internationalisation: catalogue system (.cd/.ct files), locale-aware date/number formatting, character classification |
|
||||
| [iffparse.md](iffparse.md) | IFF file parsing: ILBM/8SVX/ANIM, nested chunk hierarchy, ByteRun1 compression, PBM planar deinterleaving, clipboard integration, decision guide vs DataTypes |
|
||||
| [locale.md](locale.md) | Internationalization: catalog system (.cd/.ct files), locale-aware date/number formatting, character classification |
|
||||
| [keymap.md](keymap.md) | Keyboard mapping: raw-to-ASCII translation, KeyMap structure, dead keys, rawkey codes, national layouts |
|
||||
| [rexxsyslib.md](rexxsyslib.md) | ARexx scripting: hosting ARexx ports, command parsing, sending commands, return codes |
|
||||
| [mathffp.md](mathffp.md) | Motorola FFP and IEEE 754 floating point |
|
||||
| [layers.md](layers.md) | Window clipping: ClipRect engine, Simple/Smart/Super refresh, damage repair, backfill hooks, layer locking |
|
||||
| [diskfont.md](diskfont.md) | Disk-based fonts: FONTS: directory structure, AvailFonts enumeration, colour fonts (OS 3.0+) |
|
||||
| [diskfont.md](diskfont.md) | Disk-based fonts: FONTS: directory structure, AvailFonts enumeration, color fonts (OS 3.0+) |
|
||||
| [datatypes.md](datatypes.md) | DataTypes system: object-oriented file loading for images, sound, text, animation via BOOPSI classes |
|
||||
| [amigaguide.md](amigaguide.md) | AmigaGuide hypertext help system: database format, @commands, API, ARexx integration, cross-database linking |
|
||||
| [translator.md](translator.md) | translator.library: English-to-phonetic translation for speech synthesis, narrator.device integration, ARPABET phonemes |
|
||||
|
|
|
|||
760
11_libraries/amigaguide.md
Normal file
760
11_libraries/amigaguide.md
Normal file
|
|
@ -0,0 +1,760 @@
|
|||
[← Home](../README.md) · [Libraries](README.md)
|
||||
|
||||
# AmigaGuide — Hypertext Help System and Database Format
|
||||
|
||||
## Overview
|
||||
|
||||
AmigaGuide is AmigaOS's native hypertext document format, introduced in 1991 and elevated to a system component with OS 3.0. An AmigaGuide database is a plain ASCII file annotated with `@` commands that define nodes, links, text attributes, and executable actions.
|
||||
|
||||
The `amigaguide.library` provides both a standalone viewer and a programmatic API, allowing applications to open help databases synchronously or asynchronously, send navigation commands via an ARexx port, and even generate content dynamically at runtime through **dynamic nodes**. Because AmigaGuide is also implemented as a DataType class (`amigaguideclass`), any application using the DataTypes framework — including MultiView — can display `.guide` files without additional code. For developers, this means a single help file format serves both standalone documentation and in-application context-sensitive help, with cross-database linking, embedded images (via DataTypes), and ARexx-driven interactivity built in.
|
||||
|
||||
> **Key constraint**: AmigaGuide is text-centric. While it can launch images, animations, and audio through DataType links, the document itself is linear text — there is no concept of a 2D page layout, embedded widgets, or cascading stylesheets.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### System Components
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Application"
|
||||
APP["Application\nHelp menu / F1 key"]
|
||||
SYNC["OpenAmigaGuide()\nSynchronous viewer"]
|
||||
ASYNC["OpenAmigaGuideAsync()\nAsync + ARexx control"]
|
||||
end
|
||||
|
||||
subgraph "amigaguide.library"
|
||||
LIB["amigaguide.library\nParser + Window manager"]
|
||||
DNODE["Dynamic Node Host\n(optional callback)"]
|
||||
end
|
||||
|
||||
subgraph "DataTypes"
|
||||
DT["amigaguideclass\nRenders .guide in MultiView"]
|
||||
PIC["pictureclass\nEmbedded images"]
|
||||
ANI["animationclass\nEmbedded animations"]
|
||||
end
|
||||
|
||||
subgraph "External"
|
||||
AREXX["ARexx port\nRemote control / callbacks"]
|
||||
SHELL["Shell\nSYSTEM links"]
|
||||
end
|
||||
|
||||
APP --> SYNC --> LIB
|
||||
APP --> ASYNC --> LIB
|
||||
LIB --> DT
|
||||
LIB --> DNODE
|
||||
DT --> PIC
|
||||
DT --> ANI
|
||||
LIB --> AREXX
|
||||
LIB --> SHELL
|
||||
|
||||
style LIB fill:#e8f4fd,stroke:#2196f3,color:#333
|
||||
style DT fill:#e8f5e9,stroke:#4caf50,color:#333
|
||||
```
|
||||
|
||||
### File → Nodes → Links
|
||||
|
||||
An AmigaGuide database is a flat file divided into **nodes**. Each node is a self-contained document fragment. Navigation between nodes happens through **links**, which are rendered as clickable buttons in the viewer.
|
||||
|
||||
```
|
||||
@DATABASE MyApp.guide
|
||||
@INDEX Main
|
||||
|
||||
@NODE Main "MyApp Help"
|
||||
Welcome to MyApp.
|
||||
|
||||
@{"Getting Started" LINK GettingStarted}
|
||||
@{"Reference" LINK Reference}
|
||||
@EndNode
|
||||
|
||||
@NODE GettingStarted "Getting Started"
|
||||
1. Install the software.
|
||||
2. Run from Workbench.
|
||||
|
||||
@{"Back to Main" LINK Main}
|
||||
@EndNode
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Format Reference
|
||||
|
||||
### Database-Level Commands (Global)
|
||||
|
||||
These commands apply to the entire database. Most must appear at the start of the file, before the first `@NODE`.
|
||||
|
||||
| Command | Description | Required |
|
||||
|---|---|---|
|
||||
| `@DATABASE <name>` | Identifies this file as an AmigaGuide database. Must be the **first line**. | Yes |
|
||||
| `@INDEX <node>` | Specifies the node used for the **Index** button. Defaults to MAIN if omitted. | No |
|
||||
| `@HELP <node>` | Specifies the node used for the **Help** button. | No |
|
||||
| `@WIDTH <chars>` | Declares the maximum width of any node (viewer hint). | No |
|
||||
| `@HEIGHT <rows>` | Declares the maximum height of any node. | No |
|
||||
| `@FONT <name> <size>` | Default font for the database. | No |
|
||||
| `@WORDWRAP` | Enables automatic word wrapping (V39). | No |
|
||||
| `@SMARTWRAP` | Smarter wrapping: paragraphs separated by blank lines (V40). | No |
|
||||
| `@TAB <spaces>` | Tab stop width (default 8) (V40). | No |
|
||||
| `@AUTHOR <name>` | Author metadata. | No |
|
||||
| `@(C) <text>` | Copyright notice. | No |
|
||||
| `$VER: <version>` | Standard AmigaDOS version string. | No |
|
||||
| `@ONOPEN <rexx>` | ARexx command to execute when the database opens (V40). | No |
|
||||
| `@ONCLOSE <rexx>` | ARexx command to execute when the database closes (V40). | No |
|
||||
|
||||
### Node-Level Commands
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `@NODE <name> "<title>"` | Starts a new node. `<name>` must be unique, no spaces. `<title>` appears in the window title bar. |
|
||||
| `@ENDNODE` | Ends the current node. |
|
||||
| `@TITLE "<title>"` | Overrides the window title for this node (alternative to `@NODE`'s title argument). |
|
||||
| `@TOC <node>` | Sets the **Contents** button target for this node. Defaults to MAIN. |
|
||||
| `@PREV <node>` | Sets the **Browse <** target. Defaults to previous node in file. |
|
||||
| `@NEXT <node>` | Sets the **Browse >** target. Defaults to next node in file. |
|
||||
| `@REMARK <text>` | Comment — not displayed. |
|
||||
|
||||
### Link Syntax
|
||||
|
||||
Links are the core hypertext mechanism. They can appear anywhere on a line and are rendered as buttons.
|
||||
|
||||
```
|
||||
@{"<label>" <action> <arguments>}
|
||||
```
|
||||
|
||||
#### Action Commands
|
||||
|
||||
| Action | Syntax | Description |
|
||||
|---|---|---|
|
||||
| `LINK` | `@{"Label" LINK TargetNode}` | Jump to another node in this database. |
|
||||
| `LINK` (cross-db) | `@{"Label" LINK Other.guide/NodeName}` | Jump to a node in another database. |
|
||||
| `RX` | `@{"Label" RX 'ADDRESS MYPORT MyCommand'}` | Execute an ARexx command. |
|
||||
| `RXS` | `@{"Label" RXS MyScript.rexx}` | Run an ARexx script file. |
|
||||
| `SYSTEM` | `@{"Label" SYSTEM 'dir >ram:output'}` | Execute a Shell command. |
|
||||
| `BEEP` | `@{"Label" BEEP}` | Play the system beep. |
|
||||
| `QUIT` | `@{"Label" QUIT}` | Close the AmigaGuide viewer. |
|
||||
|
||||
> **Cross-database paths**: The path can be any AmigaDOS path, including assigns. If no path is given, AmigaGuide searches `ENV:AmigaGuide/Path`. As of OS 3.0, you can also link to any DataTypes-supported file: `@{"Picture" LINK image.iff/Main}`.
|
||||
|
||||
### Text Attributes
|
||||
|
||||
Attributes wrap text in `@{name ...}` and must be terminated with `@{uname}` (undo attribute).
|
||||
|
||||
| Attribute | Description | Example |
|
||||
|---|---|---|
|
||||
| `@{b}` / `@{ub}` | Bold on / off | `@{b}bold text@{ub}` |
|
||||
| `@{i}` / `@{ui}` | Italic on / off | `@{i}italic@{ui}` |
|
||||
| `@{u}` / `@{uu}` | Underline on / off | `@{u}underlined@{uu}` |
|
||||
| `@{fg <n>}` / `@{ufg}` | Foreground pen color | `@{fg 2}red text@{ufg}` |
|
||||
| `@{bg <n>}` / `@{ubg}` | Background pen color | `@{bg 1}highlighted@{ubg}` |
|
||||
| `@{jleft}` | Left justify (default) | |
|
||||
| `@{jright}` | Right justify | |
|
||||
| `@{jcenter}` | Center justify | |
|
||||
| `@{amigaguide}` | Embed an AmigaGuide glyph | |
|
||||
| `@{clear}` | Clear to end of line | |
|
||||
|
||||
### Macros (V40)
|
||||
|
||||
Macros let you define reusable attribute sequences:
|
||||
|
||||
```
|
||||
@MACRO warning "@{b}WARNING:@{ub} $1"
|
||||
|
||||
Later in text:
|
||||
This is a @{"critical issue" warning "Do not power off during format."}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Structures
|
||||
|
||||
### NewAmigaGuide
|
||||
|
||||
```c
|
||||
/* libraries/amigaguide.h — NDK 3.9 */
|
||||
struct NewAmigaGuide {
|
||||
BPTR nag_Lock; /* Lock on database directory */
|
||||
STRPTR nag_Name; /* Database file name */
|
||||
struct Screen * nag_Screen; /* Screen to open on, or NULL */
|
||||
STRPTR nag_PubScreen; /* Name of public screen */
|
||||
STRPTR nag_HostPort; /* App's ARexx port (unused) */
|
||||
STRPTR nag_ClientPort; /* Base name for DB's ARexx port */
|
||||
ULONG nag_Flags; /* NAGF_* flags */
|
||||
STRPTR * nag_Context; /* NULL-terminated context array */
|
||||
STRPTR nag_Node; /* Starting node name */
|
||||
LONG nag_Line; /* Starting line number */
|
||||
struct TagItem * nag_Extens; /* Additional tags (V37+) */
|
||||
APTR nag_Client; /* Private — must be NULL */
|
||||
};
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Value | Meaning |
|
||||
|---|---|---|
|
||||
| `NAGF_LOCK` | `0x0001` | `nag_Lock` is valid |
|
||||
| `NAGF_CLOSE` | `0x0002` | Close the database file when done |
|
||||
| `NAGF_NOTIFY` | `0x0004` | Enable ARexx notification |
|
||||
| `NAGF_HOSTPORT` | `0x0008` | `nag_HostPort` is valid |
|
||||
| `NAGF_CONTEXT` | `0x0010` | `nag_Context` is valid |
|
||||
| `NAGF_UNIQUE` | `0x0020` | Create unique ARexx port name |
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### Synchronous Viewer
|
||||
|
||||
```c
|
||||
#include <libraries/amigaguide.h>
|
||||
#include <proto/amigaguide.h>
|
||||
|
||||
/* Open a modal AmigaGuide viewer. Blocks until user closes all windows. */
|
||||
AMIGAGUIDECONTEXT OpenAmigaGuideA(struct NewAmigaGuide *nag,
|
||||
struct TagItem *attrs);
|
||||
AMIGAGUIDECONTEXT OpenAmigaGuide(struct NewAmigaGuide *nag,
|
||||
Tag tag1, ...);
|
||||
|
||||
/* Close the viewer and free resources */
|
||||
void CloseAmigaGuide(AMIGAGUIDECONTEXT handle);
|
||||
```
|
||||
|
||||
**Common tags for `OpenAmigaGuide()`:**
|
||||
|
||||
| Tag | Description |
|
||||
|---|---|
|
||||
| `AGA_HelpGroup` | Unique ID for help window grouping (V39) |
|
||||
|
||||
### Asynchronous Viewer
|
||||
|
||||
```c
|
||||
/* Open a non-modal viewer. Returns immediately. */
|
||||
AMIGAGUIDECONTEXT OpenAmigaGuideAsyncA(struct NewAmigaGuide *nag,
|
||||
struct TagItem *attrs);
|
||||
|
||||
/* Send a command to an async AmigaGuide instance */
|
||||
LONG SendAmigaGuideCmdA(AMIGAGUIDECONTEXT handle,
|
||||
STRPTR cmd,
|
||||
struct TagItem *attrs);
|
||||
|
||||
/* Send a context-sensitive help request */
|
||||
LONG SendAmigaGuideContextA(AMIGAGUIDECONTEXT handle,
|
||||
struct TagItem *attrs);
|
||||
```
|
||||
|
||||
### Navigation Commands (SendAmigaGuideCmd)
|
||||
|
||||
Commands are sent as strings to the async viewer:
|
||||
|
||||
| Command | Effect |
|
||||
|---|---|
|
||||
| `"BUTTON Contents"` | Click the Contents button |
|
||||
| `"BUTTON Index"` | Click the Index button |
|
||||
| `"BUTTON Retrace"` | Click Retrace |
|
||||
| `"BUTTON Browse <"` | Browse previous |
|
||||
| `"BUTTON Browse >"` | Browse next |
|
||||
| `"BUTTON Help"` | Click Help |
|
||||
| `"NODE <name>"` | Jump to a specific node |
|
||||
| `"QUIT"` | Close the viewer |
|
||||
|
||||
---
|
||||
|
||||
## Practical Examples
|
||||
|
||||
### Example 1: Minimal Help Database
|
||||
|
||||
```
|
||||
@database MyApp.guide
|
||||
|
||||
@NODE Main "MyApp Help"
|
||||
@{b}MyApp v1.0 Help@{ub}
|
||||
|
||||
Welcome to MyApp. Choose a topic:
|
||||
|
||||
@{"Quick Start" LINK QuickStart}
|
||||
@{"Menu Reference" LINK MenuRef}
|
||||
@{"Troubleshooting" LINK Troubleshoot}
|
||||
@EndNode
|
||||
|
||||
@NODE QuickStart "Quick Start"
|
||||
1. Double-click the MyApp icon.
|
||||
2. Select @{"New Project" LINK NewProject} from the Project menu.
|
||||
3. Save often with @{"Save" SYSTEM "echo Save reminder >CON:0/0/400/50/MyApp"}.
|
||||
|
||||
@{"Back to Main" LINK Main}
|
||||
@EndNode
|
||||
|
||||
@NODE MenuRef "Menu Reference"
|
||||
| Menu | Item | Action |
|
||||
| Project | New | Creates a new document |
|
||||
| Project | Open | Opens an existing document |
|
||||
| Project | Save | Saves the current document |
|
||||
|
||||
@{"Back to Main" LINK Main}
|
||||
@EndNode
|
||||
|
||||
@NODE Troubleshoot "Troubleshooting"
|
||||
@{b}Common Problems@{ub}
|
||||
|
||||
@{b}Problem:@{ub} App crashes on startup.
|
||||
@{b}Solution:@{ub} Ensure @{"MYAPP: assign" SYSTEM "assign >NIL:"} exists.
|
||||
|
||||
@{"Back to Main" LINK Main}
|
||||
@EndNode
|
||||
```
|
||||
|
||||
### Example 2: Open Help from an Application
|
||||
|
||||
```c
|
||||
#include <exec/types.h>
|
||||
#include <libraries/amigaguide.h>
|
||||
#include <proto/amigaguide.h>
|
||||
#include <proto/dos.h>
|
||||
|
||||
void ShowHelp(CONST_STRPTR dbPath, CONST_STRPTR nodeName)
|
||||
{
|
||||
struct NewAmigaGuide nag = {0};
|
||||
|
||||
nag.nag_Name = (STRPTR)dbPath;
|
||||
nag.nag_Node = (STRPTR)nodeName;
|
||||
nag.nag_Flags = NAGF_CLOSE;
|
||||
|
||||
AMIGAGUIDECONTEXT ctx = OpenAmigaGuide(&nag, TAG_DONE);
|
||||
if (ctx)
|
||||
{
|
||||
/* Synchronous: blocks here until user closes viewer */
|
||||
CloseAmigaGuide(ctx);
|
||||
}
|
||||
else
|
||||
{
|
||||
LONG err = IoErr();
|
||||
Printf("Failed to open help: %ld\n", err);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: Async Help with ARexx Control
|
||||
|
||||
```c
|
||||
#include <libraries/amigaguide.h>
|
||||
#include <proto/amigaguide.h>
|
||||
|
||||
struct NewAmigaGuide nag = {0};
|
||||
nag.nag_Name = "MyApp.guide";
|
||||
nag.nag_Node = "Main";
|
||||
nag.nag_Flags = NAGF_CLOSE | NAGF_NOTIFY;
|
||||
|
||||
AMIGAGUIDECONTEXT ctx = OpenAmigaGuideAsyncA(&nag, NULL);
|
||||
if (ctx)
|
||||
{
|
||||
/* Application continues running... */
|
||||
|
||||
/* Later: programmatically navigate to a node */
|
||||
SendAmigaGuideCmdA(ctx, "NODE Troubleshoot", NULL);
|
||||
|
||||
/* Later: close help from code */
|
||||
SendAmigaGuideCmdA(ctx, "QUIT", NULL);
|
||||
|
||||
CloseAmigaGuide(ctx);
|
||||
}
|
||||
```
|
||||
|
||||
### Example 4: Context-Sensitive Help (F1 Key)
|
||||
|
||||
```c
|
||||
/* In your IDCMP event loop: */
|
||||
case IDCMP_RAWKEY:
|
||||
if (code == 0x5B) /* Help key scancode */
|
||||
{
|
||||
/* Determine which gadget is under the mouse */
|
||||
UWORD gadID = GetGadgetIDUnderMouse(win);
|
||||
|
||||
CONST_STRPTR node = "Main";
|
||||
switch (gadID)
|
||||
{
|
||||
case GAD_OPEN: node = "FileOpen"; break;
|
||||
case GAD_SAVE: node = "FileSave"; break;
|
||||
case GAD_PREFS: node = "Preferences"; break;
|
||||
}
|
||||
|
||||
ShowHelp("PROGDIR:MyApp.guide", node);
|
||||
}
|
||||
break;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Decision Guide
|
||||
|
||||
| Criterion | AmigaGuide | Raw Text + More | IFF-FTXT |
|
||||
|---|---|---|---|
|
||||
| **When to use** | Application help; cross-linked docs; need buttons and ARexx | Simple one-page docs; minimal dependencies | Structured text with formatting but no interactivity |
|
||||
| **Interactivity** | Links, ARexx, Shell commands | None | None |
|
||||
| **Images/audio** | Via DataType links (V39+) | None | None |
|
||||
| **Cross-database links** | Yes — `@{"Label" LINK other.guide/node}` | No | No |
|
||||
| **Viewer required** | amigaguide.library or MultiView | Any text viewer | Any IFF text viewer |
|
||||
| **OS version** | OS 2.0+ (full features: 3.0+) | Works everywhere | OS 1.3+ |
|
||||
| **Embedding in app UI** | Can open standalone only; not embeddable as gadget | Display in custom read-only string gadget | Display in custom gadget |
|
||||
|
||||
---
|
||||
|
||||
## Historical Context & Modern Analogies
|
||||
|
||||
### The Elegance of Executable Documentation
|
||||
|
||||
AmigaGuide was not merely a hypertext format — it was a **programmable application extension mechanism** disguised as a help file. A developer could ship a `.guide` file that, when the user clicked a link, sent an ARexx command back to the running application, executed a shell script to repair a configuration, or opened an IFF image through the DataTypes system. The help file was not static documentation; it was an interactive partner to the application.
|
||||
|
||||
This design philosophy — **documentation as code** — would not resurface in mainstream computing until the rise of literate programming tools, Jupyter notebooks, and interactive web documentation in the 2010s. In 1991, it was genuinely unique.
|
||||
|
||||
Consider what a single AmigaGuide database could do without the host application knowing anything about its internal structure:
|
||||
|
||||
1. **Navigate internally** — `@NODE` and `@LINK` provide structured hypertext
|
||||
2. **Control the host application** — `RX` links send ARexx commands to the app's port
|
||||
3. **Modify the system** — `SYSTEM` links execute shell commands
|
||||
4. **Display rich media** — cross-links to any DataType-supported image, sound, or animation
|
||||
5. **Form a documentation graph** — cross-database `@{"Other" LINK lib.guide/Main}` creates a web of help files
|
||||
|
||||
The host application only needed to call `OpenAmigaGuideAsyncA()` with a file path. Everything else — rendering, navigation, interactivity, media embedding — was handled by `amigaguide.library` and the DataTypes framework.
|
||||
|
||||
### The 1991 Competitive Landscape
|
||||
|
||||
| Platform (1991) | System | Hypertext | Scripting | External Linking | App Control | System Integration |
|
||||
|---|---|---|---|---|---|---|
|
||||
| **AmigaOS** | **AmigaGuide** | Nodes | ARexx (RX/RXS) | Cross-db `.guide` | Yes — ARexx ports | DataTypes, Shell |
|
||||
| **Mac OS** | **HyperCard** (1987) | Visual stacks | HyperTalk | Stack-to-stack | Limited — AppleEvents | Clipboard, File |
|
||||
| **Windows 3.0** | **WinHelp** (`.hlp`) | Popups, macros | Limited macro lang | No | No | ShellExecute |
|
||||
| **NeXTSTEP** | **Help** + **Digital Librarian** | Rich text | No | No | No | Search index |
|
||||
| **UNIX** | **Texinfo** / **man -k`** | Cross-references | No | Info nodes only | No | man path |
|
||||
| **VMS** | **Bookreader** | Structured docs | No | No | No | DEC-specific |
|
||||
|
||||
The critical differentiator is the **ARexx bidirectional control**. HyperCard stacks could script the Macintosh (via AppleEvents, later), but the help file could not easily send a command to a *specific running application* and expect a response. WinHelp macros were confined to the help viewer itself. AmigaGuide's `RX` links could address any ARexx port on the system — including the application that opened the help file — creating a true conversational relationship between documentation and software.
|
||||
|
||||
### How It Worked in Practice: The MUI Developer Reference
|
||||
|
||||
The **MUI (Magic User Interface)** developer reference — `MUIdev.guide` — is perhaps the finest example of AmigaGuide's power. It was not a manual; it was an ecosystem:
|
||||
|
||||
| Feature | Traditional Manual | MUIdev.guide |
|
||||
|---|---|---|
|
||||
| Class reference | Static tables | `@NODE` per class with `@TOC` navigation |
|
||||
| Example code | Embedded in text | `SYSTEM` links to run example scripts |
|
||||
| Cross-references | Page numbers | `@LINK` to other `.guide` databases |
|
||||
| Method parameters | Text description | ARexx-driven parameter exploration (in advanced setups) |
|
||||
| Updates | Re-print or new edition | Drop-in replacement `.guide` file |
|
||||
|
||||
A developer reading about `Listview.mui` could click a link to jump to the `List` class reference in the same file, click another to open the `Popobject.mui` reference in a separate database, and click a third to execute an ARexx script that created a live example window — all without leaving the help viewer.
|
||||
|
||||
### Modern Analogies
|
||||
|
||||
No single modern system replicates AmigaGuide's exact combination of properties, but several come close in different dimensions:
|
||||
|
||||
| AmigaGuide Concept | Modern Equivalent | Why It Maps (and Where It Diverges) |
|
||||
|---|---|---|
|
||||
| `@NODE` / modular docs | **DITA topics / DocBook `<section>`** | Modular, reusable document fragments. DITA goes further with conditional profiling (`@audience`, `@platform`); AmigaGuide has no conditional text. |
|
||||
| `@{"Label" LINK Node}` | **HTML `<a href>` + anchor** | Direct hypertext navigation. HTML is richer (CSS styling, embedded media); AmigaGuide links are constrained to button-like widgets. |
|
||||
| `RX` / `RXS` — app control from docs | **Jupyter Notebook cells** | Documentation that executes code and returns results. Jupyter is far more powerful (Python, visualization, stateful kernels), but requires a heavy runtime. AmigaGuide's ARexx was lightweight and system-wide. |
|
||||
| `SYSTEM` — shell from docs | **Markdown code fences with `bash` execution** (e.g., R Markdown, Quarto) | Document-embedded command execution. Modern tools sandbox or prompt; AmigaGuide executed with the user's full privileges. |
|
||||
| Cross-database `@LINK other.guide` | **HTML `<a href>` across files** | The web itself is the analogy — a distributed documentation graph. AmigaGuide's `ENV:AmigaGuide/Path` is a primitive `PATH` for docs. |
|
||||
| `amigaguide.library` viewer | **Qt Assistant / Apple Help Viewer / DevDocs** | Dedicated offline help viewers. Modern viewers support full-text search, indexing, and CSS; AmigaGuide had none of these but loaded instantly on 68000-class hardware. |
|
||||
| `.guide` as DataType | **HTML rendered in any WebKit view** | Universal rendering via shared framework. The key difference: a DataType object is an OS-native BOOPSI object; a WebKit view is an application-embedded widget. |
|
||||
| `@ONOPEN` / `@ONCLOSE` | **HTML `<body onload>` / JavaScript `DOMContentLoaded`** | Event-driven document lifecycle. AmigaGuide's events are limited to ARexx commands; modern web docs have full Turing-complete scripting. |
|
||||
|
||||
### What Made AmigaGuide Unique (and Unreplicated)
|
||||
|
||||
Despite the partial modern analogies above, no contemporary system in 1991 — and few today — offered this specific combination:
|
||||
|
||||
1. **Zero-compilation hypertext with system-wide reach**: HyperCard required the HyperCard runtime (a separate application). WinHelp required a compiler (`hc.exe`) to build `.hlp` from `.rtf` source. AmigaGuide files are plain ASCII — editable in any text editor, viewable in any AmigaGuide-aware application, with no build step.
|
||||
|
||||
2. **Documentation as remote control**: The `RX` link is not merely "run a script" — it is "send a message to a named port." This means the help file can query application state (`ADDRESS MyApp GETVERSION`), trigger actions (`ADDRESS MyApp DOACTION`), or chain multiple applications together. It is RPC embedded in documentation.
|
||||
|
||||
3. **Late-bound media embedding**: Because AmigaGuide links to DataTypes, a `.guide` file written in 1991 could embed a PNG image in 1996, an MP3 in 2000, or a video in 2026 — without the help file or the viewer knowing what PNG, MP3, or video formats are. The DataTypes system (see [datatypes.md](datatypes.md)) handles the indirection.
|
||||
|
||||
4. **No host application dependency**: A `.guide` file is self-sufficient. It carries its own navigation structure (`@INDEX`, `@TOC`), styling (`@MACRO`, attributes), and actions. The host application does not need to parse, render, or understand the file format — it merely hands the path to `amigaguide.library`.
|
||||
|
||||
### Where the Analogies Break Down
|
||||
|
||||
- **No compilation step**: Unlike WinHelp (`.hlp`) or modern CHM, `.guide` files are raw text — no indexing, compression, or encryption. This is simpler but slower for large documents.
|
||||
- **No full-text search**: The viewer does not index content. Navigation is entirely through pre-defined links or external tools.
|
||||
- **No conditional text**: There is no `#if`, audience profiling, or platform-specific filtering. Every user sees every node.
|
||||
- **Linear rendering**: Nodes are displayed as scrolling text — no pagination, no columns, no responsive layout, no CSS.
|
||||
- **No security model**: `SYSTEM` and `RX` links execute with the user's full privileges. There is no sandbox, no confirmation dialog, and no capability system. A malicious `.guide` file can delete files or send harmful ARexx commands.
|
||||
- **ARexx dependency**: The full power of AmigaGuide requires a functional `rexxsyslib.library` and ARexx port infrastructure. On systems without ARexx, `RX` links fail silently and async mode loses much of its utility.
|
||||
|
||||
---
|
||||
|
||||
## When to Use / When NOT to Use
|
||||
|
||||
### When to Use AmigaGuide
|
||||
|
||||
| Scenario | Why AmigaGuide Works |
|
||||
|---|---|
|
||||
| **Application help files** | Native OS support; opens from Help key; context-sensitive via `nag_Node` |
|
||||
| **Cross-referenced documentation** | `LINK other.guide/node` creates a documentation ecosystem |
|
||||
| **Interactive tutorials** | `SYSTEM` and `RX` links let users execute commands from within help |
|
||||
| **Small-to-medium docs (< 200 KB)** | Raw text is efficient; no compilation overhead |
|
||||
| **Integration with ARexx-enabled apps** | Help can send commands back to the application |
|
||||
|
||||
### When NOT to Use AmigaGuide
|
||||
|
||||
| Scenario | Problem | Better Alternative |
|
||||
|---|---|---|
|
||||
| **Large manuals (> 500 KB)** | No search; linear loading; slow navigation | Split into multiple `.guide` files or use external viewer |
|
||||
| **Print-quality documentation** | No page layout, margins, or typography control | Texinfo → PostScript, or DTP tools |
|
||||
| **Secure/restricted content** | No access control; plain text is trivially editable | Compiled help (WinHelp-style) or PDF |
|
||||
| **Embedded in-app help pane** | Cannot embed AmigaGuide viewer as a sub-window gadget | Custom read-only BOOPSI gadget with formatted text |
|
||||
| **Modern cross-platform docs** | `.guide` is Amiga-only | HTML, Markdown, or plain text |
|
||||
|
||||
---
|
||||
|
||||
## Best Practices & Antipatterns
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Always name the first node `MAIN`** — viewers default to it if no starting node is specified.
|
||||
2. **Set `@INDEX` and `@TOC`** — gives users predictable navigation buttons.
|
||||
3. **Use `@SMARTWRAP` (V40) instead of `@WORDWRAP`** — produces cleaner output on all viewer versions.
|
||||
4. **Provide a `@{"Back" LINK ...}` link on every non-MAIN node** — users expect a way back.
|
||||
5. **Use `PROGDIR:` or assigned paths for cross-database links** — avoids breakage when the user moves the application.
|
||||
6. **Set `$VER:`** — allows the `Version` command to report the help file version.
|
||||
7. **Keep nodes focused** — one topic per node; deep nesting via links is better than long scrolling.
|
||||
8. **Test in both MultiView and standalone AmigaGuide** — rendering differs slightly between viewers.
|
||||
9. **Use `@MACRO` for consistent styling** (V40) — reduces markup repetition.
|
||||
10. **Set `@ONCLOSE` to clean up** if your `@{RX}` links create temporary files or ports.
|
||||
|
||||
### Antipatterns
|
||||
|
||||
#### 1. The Missing EndNode
|
||||
|
||||
```
|
||||
/* ANTIPATTERN — @ENDNODE omitted */
|
||||
@NODE Main "Help"
|
||||
Welcome to the app.
|
||||
@NODE Setup "Setup"
|
||||
Install instructions.
|
||||
@EndNode
|
||||
|
||||
/* RESULT: "Setup" node includes all text from MAIN onwards
|
||||
because MAIN was never properly closed. */
|
||||
|
||||
/* CORRECT — always close every node */
|
||||
@NODE Main "Help"
|
||||
Welcome to the app.
|
||||
@EndNode
|
||||
|
||||
@NODE Setup "Setup"
|
||||
Install instructions.
|
||||
@EndNode
|
||||
```
|
||||
|
||||
#### 2. The Broken Cross-Database Link
|
||||
|
||||
```
|
||||
/* ANTIPATTERN — relative path assumes current directory */
|
||||
@{"See Also" LINK OtherApp.guide/Main}
|
||||
|
||||
/* If the user opens help from a different directory, this fails. */
|
||||
|
||||
/* CORRECT — use an assign or absolute path */
|
||||
@{"See Also" LINK MYAPP:Docs/OtherApp.guide/Main}
|
||||
```
|
||||
|
||||
#### 3. The Unclean RX Link
|
||||
|
||||
```
|
||||
/* ANTIPATTERN — RX command with unquoted special characters */
|
||||
@{"Run" RX 'ADDRESS MYPORT Run script with spaces'}
|
||||
|
||||
/* Parsing ambiguity — may truncate at first space in argument. */
|
||||
|
||||
/* CORRECT — quote the argument or use RXS for complex scripts */
|
||||
@{"Run" RX 'ADDRESS MYPORT "Run script with spaces"'}
|
||||
```
|
||||
|
||||
#### 4. The Invisible Link
|
||||
|
||||
```
|
||||
/* ANTIPATTERN — link text that looks like body text */
|
||||
For more information see the advanced topics section.
|
||||
|
||||
/* No clickable link — users won't know it's interactive. */
|
||||
|
||||
/* CORRECT — use explicit button-like labels */
|
||||
For more information, @{"click here for advanced topics" LINK Advanced}.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pitfalls & Common Mistakes
|
||||
|
||||
### 1. Node Name Collisions
|
||||
|
||||
```
|
||||
/* PITFALL — node names are case-insensitive but must be unique */
|
||||
@NODE Setup "Setup"
|
||||
@NODE setup "Setup Details" /* COLLISION: "setup" == "Setup" */
|
||||
```
|
||||
|
||||
### 2. AmigaGuide Path Not Set
|
||||
|
||||
```
|
||||
/* PITFALL — cross-database links fail if ENV:AmigaGuide/Path is missing */
|
||||
@{"External" LINK SomeLib.guide/Main}
|
||||
|
||||
/* If SomeLib.guide is not in the current directory or the path,
|
||||
the link silently fails. Set the path at install time:
|
||||
SetEnv AmigaGuide/Path "MYAPP:Docs" */
|
||||
```
|
||||
|
||||
### 3. Forgetting Synchronous Blocks
|
||||
|
||||
```c
|
||||
/* PITFALL — calling OpenAmigaGuide() from the main task freezes UI */
|
||||
void OnHelpClick(void)
|
||||
{
|
||||
/* This blocks until the user closes the help window! */
|
||||
OpenAmigaGuide(&nag, TAG_DONE);
|
||||
/* App is frozen — no IDCMP processing, no timer events */
|
||||
}
|
||||
|
||||
/* CORRECT — use async mode for multi-window apps */
|
||||
void OnHelpClick(void)
|
||||
{
|
||||
ctx = OpenAmigaGuideAsyncA(&nag, NULL);
|
||||
/* App continues; handle help closure via ARexx or ignore */
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Dynamic Node Confusion
|
||||
|
||||
Dynamic nodes are generated on-the-fly by an application hosting `amigaguide.library`. If you are not implementing a dynamic node host, do not use `@DNODE` — it is obsolete and ignored by modern viewers.
|
||||
|
||||
### 5. Attribute Nesting Errors
|
||||
|
||||
```
|
||||
/* PITFALL — incorrect nesting of attributes */
|
||||
@{b}bold @{i}bold-italic@{ub} still italic@{ui}
|
||||
|
||||
/* @{ub} undoes bold, but italic is still active.
|
||||
Viewer rendering is undefined. */
|
||||
|
||||
/* CORRECT — close in reverse order of opening (LIFO) */
|
||||
@{b}bold @{i}bold-italic@{ui} bold again@{ub} normal
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Real-World Software Using AmigaGuide
|
||||
|
||||
| Software | AmigaGuide Usage |
|
||||
|---|---|
|
||||
| **MultiView** (OS 3.0+) | Displays any `.guide` file via `amigaguideclass` — the default viewer |
|
||||
| **SAS/C** | Compiler error explanations linked via AmigaGuide |
|
||||
| **Directory Opus** | Configuration help and button reference |
|
||||
| **MUI** | `MUIdev.guide` — the entire MUI developer reference is an AmigaGuide database |
|
||||
| **AmigaOS installer scripts** | Post-install help often launched as AmigaGuide |
|
||||
| **Aminet** | Package documentation frequently shipped as `.guide` files |
|
||||
|
||||
### Common Integration Patterns
|
||||
|
||||
**Pattern A: Context-Sensitive Help**
|
||||
```
|
||||
Application maintains a mapping:
|
||||
Window A + Gadget X → "NodeRef/GadgetX"
|
||||
Window B + Menu Y → "NodeRef/MenuY"
|
||||
|
||||
On Help key:
|
||||
nag.nag_Node = mappedNode;
|
||||
OpenAmigaGuideAsyncA(&nag, NULL);
|
||||
```
|
||||
|
||||
**Pattern B: ARexx-Driven Navigation**
|
||||
```
|
||||
Application exposes ARexx port "MYAPP.1"
|
||||
AmigaGuide links use:
|
||||
@{"Do Action" RX 'ADDRESS MYAPP.1 DOACTION'}
|
||||
|
||||
Result: User clicks help → ARexx command sent → app performs action
|
||||
```
|
||||
|
||||
**Pattern C: Documentation Suite**
|
||||
```
|
||||
MyApp.guide (main help)
|
||||
MyApp_API.guide (function reference)
|
||||
MyApp_Tools.guide (utility reference)
|
||||
|
||||
Cross-links:
|
||||
MyApp.guide: @{"API Reference" LINK MyApp_API.guide/Main}
|
||||
MyApp_API.guide: @{"Back to User Guide" LINK MyApp.guide/Main}
|
||||
```
|
||||
|
||||
### AmigaOS Developer Documentation as AmigaGuide
|
||||
|
||||
Commodore itself structured the entire AmigaOS developer documentation corpus as AmigaGuide databases. The **Amiga Developer CD 2.1 (ADCD 2.1)** — the definitive official SDK — shipped the following manuals as `.guide` files:
|
||||
|
||||
| ADCD 2.1 Path | Content | Approximate Scope |
|
||||
|---|---|---|
|
||||
| `Libraries_Manual_guide/` | *ROM Kernel Reference Manual: Libraries* | Every system library: Exec, DOS, Intuition, Graphics, etc. |
|
||||
| `Devices_Manual_guide/` | *ROM Kernel Reference Manual: Devices* | TrackDisk, Audio, Serial, Parallel, Timer, etc. |
|
||||
| `Hardware_Manual_guide/` | *ROM Kernel Reference Manual: Hardware* | Custom chips, DMA, CIA, chipset registers |
|
||||
| `Includes_and_Autodocs_3._guide/` | NDK 3.1 headers + autodocs | All struct definitions and function-by-function API docs |
|
||||
|
||||
This was not merely a packaging choice — it was an architectural statement. The same AmigaGuide viewer that displayed a game's help file could display the complete technical reference for `graphics.library` or the Blitter's minterm logic. A developer could:
|
||||
|
||||
1. **Browse autodocs interactively** — click `@LINK` cross-references to jump from `AllocMem` to `MemHeader` to `FreeMem`
|
||||
2. **Keep docs open while coding** — the async viewer sat alongside the editor, navigable without leaving the Workbench
|
||||
3. **Search with external tools** — because `.guide` files are plain ASCII, `grep` and `Search` could find text inside them; no proprietary indexing format required
|
||||
4. **Ship custom subsets** — a developer could copy just the relevant autodoc nodes into a project's `Docs/` drawer
|
||||
|
||||
The autodoc format — a structured comment convention in NDK header files — was also converted to AmigaGuide by community tools. **New Style Autodocs** (Aminet `dev/misc/NSA_amigaguide.lha`, 2002) repackaged library autodocs as hyperlinked `.guide` files with `@NODE` per function and `@TOC` navigation, making the raw API reference far more browsable than scrolling through flat text files.
|
||||
|
||||
### Authoring Tools and Converters
|
||||
|
||||
Because AmigaGuide is plain ASCII, any text editor suffices for authoring. However, several specialized tools streamlined creation and conversion:
|
||||
|
||||
| Tool | Aminet Path | Purpose |
|
||||
|---|---|---|
|
||||
| **AGWriter** | `text/hyper/AGWriter103.lha` | GUI editor for creating, editing, and validating AmigaGuide files. Supports WYSIWYG-style node management, link insertion (`LINK`, `RX`, `RXS`, `SYSTEM`), and round-trip conversion to plain text. |
|
||||
| **GuideML** | `text/hyper/guideml.lha` | AmigaGuide-to-HTML converter (C + GUI). Converts `@NODE` to HTML pages, `@LINK` to `<a href>`, and attributes to inline styles. Includes source code. |
|
||||
| **ag2html** | `text/hyper/ag2html.lha` | Perl script that converts a `.guide` file into a directory of interlinked HTML files suitable for web serving. |
|
||||
| **HTML2Guide** | `util/conv/HTML2Guide-1.2.lha` | Reverse converter: batch-converts `.html`/`.htm` files (including subdirectories) into a single AmigaGuide database with preserved relative links. |
|
||||
| **New Style Autodocs** | `dev/misc/NSA_amigaguide.lha` | Reformats NDK autodoc text files into hyperlinked AmigaGuide databases with per-function `@NODE` entries. |
|
||||
|
||||
> [!NOTE]
|
||||
> The ADCD 2.1 online mirror (`http://amigadev.elowar.com/read/ADCD_2.1/`) renders the original AmigaGuide `.guide` files as HTML using modern server-side conversion. The URL structure directly maps to the original ADCD directory layout, preserving the node hierarchy that Commodore established in 1994.
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: Can I embed an image directly in an AmigaGuide node?**
|
||||
> Not inline. You link to it via `@{"Image" LINK picture.iff/Main}` (V39+). The DataTypes viewer opens the image in a separate window or replaces the current view depending on the viewer.
|
||||
|
||||
**Q: Why does my link to another database fail?**
|
||||
> Either the path is wrong, or `ENV:AmigaGuide/Path` does not include the target directory. Always use assigns (e.g., `MYAPP:Docs/Other.guide/Main`) for reliability.
|
||||
|
||||
**Q: What is the difference between `RX` and `RXS`?**
|
||||
> `RX` executes an inline ARexx command string. `RXS` executes an ARexx script file from disk. Use `RXS` for complex multi-line scripts.
|
||||
|
||||
**Q: Can I use AmigaGuide on OS 1.3?**
|
||||
> No. AmigaGuide requires OS 2.0+ for the viewer, and OS 3.0+ for DataType integration (cross-linking to images/audio). On 1.3, use plain text or IFF-FTXT.
|
||||
|
||||
**Q: How do I make my `.guide` file open from Workbench?**
|
||||
> Set the icon's default tool to `SYS:Utilities/MultiView` (OS 3.0+) or `SYS:Utilities/AmigaGuide`. Ensure `amigaguide.library` is in `LIBS:`.
|
||||
|
||||
**Q: Is there a size limit for AmigaGuide files?**
|
||||
> No hard limit, but files over ~500 KB become unwieldy due to the lack of full-text search. Split large documentation into multiple linked databases.
|
||||
|
||||
**Q: Can I convert AmigaGuide to HTML?**
|
||||
> Not natively. Third-party tools exist (e.g., `guide2html` on Aminet) that parse the `@` commands and emit HTML. The mapping is straightforward since the concepts are similar.
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- NDK 3.9: `libraries/amigaguide.h`, `datatypes/amigaguideclass.h`
|
||||
- ADCD 2.1: `amigaguide.library` autodocs (`OpenAmigaGuide`, `OpenAmigaGuideAsync`, `SendAmigaGuideCmd`)
|
||||
- *Amiga ROM Kernel Reference Manual: Libraries* — Appendix C: AmigaGuide
|
||||
- See also: [datatypes.md](datatypes.md) — DataTypes framework that powers `amigaguideclass` and media embedding
|
||||
- See also: [rexxsyslib.md](rexxsyslib.md) — ARexx scripting and port communication
|
||||
- See also: [input_events.md](../09_intuition/input_events.md) — Handling Help key and IDCMP for context-sensitive help
|
||||
- See also: [boopsi.md](../09_intuition/boopsi.md) — BOOPSI foundation underlying the AmigaGuide DataType
|
||||
839
11_libraries/datatypes.md
Normal file
839
11_libraries/datatypes.md
Normal file
|
|
@ -0,0 +1,839 @@
|
|||
[← Home](../README.md) · [Libraries](README.md)
|
||||
|
||||
# Datatypes System — Object-Oriented File Loading for Images, Sound, Text, and Animation
|
||||
|
||||
## Overview
|
||||
|
||||
The DataTypes system is AmigaOS's extensible, object-oriented framework for loading, displaying, and manipulating structured data without hard-coding format-specific parsers. Built on Intuition's BOOPSI object model and introduced in OS 2.0, it allows a single API — `NewDTObject()` — to instantiate an object from an ILBM image, an 8SVX sample, an ASCII text file, or any third-party format installed on the system. The framework delegates parsing to loadable **datatype classes** (subclasses of pictureclass, soundclass, textclass, animationclass, or amigaguideclass) that live in `SYS:Classes/DataTypes/`. Each class registers itself via a descriptor in `DEVS:DataTypes/` so that `datatypes.library` can identify files by magic bytes or extension, route them to the correct parser, and present a uniform interface to the application. For the developer, this means no per-format loader code, no manual IFF chunk walking for basic tasks, and automatic clipboard integration. The trade-off is memory overhead (BOOPSI objects are larger than raw buffers) and reduced control over the parsing pipeline.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### The BOOPSI Class Hierarchy
|
||||
|
||||
The DataTypes framework is a layered extension of BOOPSI. Every datatype object is a BOOPSI object, and every datatype class is a BOOPSI `IClass` registered with Intuition.
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Application"
|
||||
APP["NewDTObject(path)"]
|
||||
end
|
||||
|
||||
subgraph "datatypes.library"
|
||||
NDTO["NewDTObject()\nIdentify file → Find class"]
|
||||
DTA["DataType superclass\n(common attributes)"]
|
||||
end
|
||||
|
||||
subgraph "Intuition — BOOPSI"
|
||||
ROOT["rootclass"]
|
||||
GAD["gadgetclass"]
|
||||
end
|
||||
|
||||
subgraph "Concrete Datatype Classes"
|
||||
PIC["pictureclass\n(ILBM, PNG, GIF, JPEG)"]
|
||||
SND["soundclass\n(8SVX, WAV, AIFF)"]
|
||||
TXT["textclass\n(ASCII, IFF-FTXT)"]
|
||||
ANI["animationclass\n(ANIM, GIF anim)"]
|
||||
AGD["amigaguideclass\n(.guide hypertext)"]
|
||||
end
|
||||
|
||||
APP --> NDTO
|
||||
NDTO --> DTA
|
||||
DTA --> ROOT
|
||||
DTA --> GAD
|
||||
PIC --> DTA
|
||||
SND --> DTA
|
||||
TXT --> DTA
|
||||
ANI --> DTA
|
||||
AGD --> DTA
|
||||
|
||||
style NDTO fill:#e8f4fd,stroke:#2196f3,color:#333
|
||||
style PIC fill:#e8f5e9,stroke:#4caf50,color:#333
|
||||
style SND fill:#fff3e0,stroke:#ff9800,color:#333
|
||||
```
|
||||
|
||||
### Data Flow: From File to Renderable Object
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant App as Application
|
||||
participant DT as datatypes.library
|
||||
participant Desc as DEVS:DataTypes/ descriptor
|
||||
participant Class as Concrete class
|
||||
participant Super as pictureclass/soundclass
|
||||
|
||||
App->>DT: NewDTObject(path, DTA_SourceType, DTST_FILE, ...)
|
||||
DT->>Desc: Scan descriptors for matching signature
|
||||
Desc-->>DT: Match: "ilbm.datatype"
|
||||
DT->>Class: Create object (OM_NEW)
|
||||
Class->>Class: Parse file, build BitMap / VoiceHeader
|
||||
Class->>Super: Set superclass attributes (PDTA_BitMap, etc.)
|
||||
Super-->>Class: Object ready
|
||||
Class-->>DT: Return DataType object
|
||||
DT-->>App: Return Object *
|
||||
|
||||
App->>DT: GetDTAttrs(obj, PDTA_BitMap, &bm, ...)
|
||||
DT-->>App: BitMap * populated
|
||||
|
||||
App->>DT: DisposeDTObject(obj)
|
||||
DT->>Class: OM_DISPOSE
|
||||
Class->>Class: Free file-specific resources
|
||||
```
|
||||
|
||||
### Key Design Insight
|
||||
|
||||
The framework splits responsibility into three layers:
|
||||
|
||||
1. **datatypes.library** — file identification, class routing, common attribute management
|
||||
2. **Superclass** (pictureclass, soundclass, etc.) — domain-specific rendering, clipboard serialization, standard data format
|
||||
3. **Subclass** (ilbm.datatype, gif.datatype, etc.) — file parsing, conversion to superclass format
|
||||
|
||||
This means a picture subclass only needs to parse its format and fill a `BitMapHeader`, allocate a `BitMap`, and supply a `ColorMap`. The picture superclass handles everything else: rendering into a window, copy-to-clipboard, save-back-to-file, and color remapping.
|
||||
|
||||
---
|
||||
|
||||
## Data Structures
|
||||
|
||||
### DataType Header
|
||||
|
||||
```c
|
||||
/* datatypes/datatypes.h — NDK 3.9 */
|
||||
struct DataType {
|
||||
struct Node dtn_Node; /* ln_Name = human-readable name */
|
||||
struct DataType * dtn_Next; /* next in chain */
|
||||
struct DataType * dtn_Previous; /* previous in chain */
|
||||
ULONG dtn_Flags; /* DTF_* flags */
|
||||
ULONG dtn_Labels; /* label bitfield (for ASL requester) */
|
||||
UWORD dtn_GroupID; /* GID_* (picture, sound, text, etc.) */
|
||||
UWORD dtn_ID; /* class-specific ID */
|
||||
ULONG dtn_CodePage; /* text codepage, if applicable */
|
||||
STRPTR dtn_TextAttr; /* text attribute string */
|
||||
ULONG dtn_Reserved[4]; /* must be zero */
|
||||
};
|
||||
```
|
||||
|
||||
### BitMapHeader (Picture Class)
|
||||
|
||||
```c
|
||||
/* datatypes/pictureclass.h — NDK 3.9 */
|
||||
struct BitMapHeader {
|
||||
UWORD bmh_Width; /* image width in pixels */
|
||||
UWORD bmh_Height; /* image height in pixels */
|
||||
WORD bmh_Left; /* x offset (usually 0) */
|
||||
WORD bmh_Top; /* y offset (usually 0) */
|
||||
UBYTE bmh_Depth; /* number of bitplanes (1–8 typical) */
|
||||
UBYTE bmh_Masking; /* 0=none, 1=has mask, 2=transparent color */
|
||||
UBYTE bmh_Compression; /* 0=none, 1=ByteRun1 */
|
||||
UBYTE bmh_Pad;
|
||||
UWORD bmh_Transparent; /* transparent color index */
|
||||
UBYTE bmh_XAspect; /* pixel aspect ratio X */
|
||||
UBYTE bmh_YAspect; /* pixel aspect ratio Y */
|
||||
WORD bmh_PageWidth; /* source page width */
|
||||
WORD bmh_PageHeight; /* source page height */
|
||||
};
|
||||
```
|
||||
|
||||
### VoiceHeader (Sound Class)
|
||||
|
||||
```c
|
||||
/* datatypes/soundclass.h — NDK 3.9 */
|
||||
struct VoiceHeader {
|
||||
ULONG vh_OneShotHiSamples; /* one-shot part length */
|
||||
ULONG vh_RepeatHiSamples; /* repeat part length */
|
||||
ULONG vh_SamplesPerHiCycle; /* samples per cycle */
|
||||
UWORD vh_SamplesPerSec; /* sample rate (Hz) */
|
||||
UBYTE vh_Octaves; /* number of octaves */
|
||||
UBYTE vh_Compression; /* 0=none */
|
||||
ULONG vh_Volume; /* 0–64 (or 0–65535 for 16-bit) */
|
||||
};
|
||||
```
|
||||
|
||||
### SourceType Constants
|
||||
|
||||
| Constant | Value | Meaning |
|
||||
|---|---|---|
|
||||
| `DTST_FILE` | 1 | Source is an AmigaDOS file path |
|
||||
| `DTST_CLIPBOARD` | 2 | Source is the clipboard device |
|
||||
| `DTST_RAM` | 3 | Source is a memory buffer |
|
||||
| `DTST_HOTLINK` | 4 | Source is a hypertext link (AmigaGuide) |
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### Object Lifecycle
|
||||
|
||||
```c
|
||||
#include <datatypes/datatypes.h>
|
||||
#include <proto/datatypes.h>
|
||||
|
||||
/* Create a DataType object from a source */
|
||||
Object *NewDTObject(APTR name, ULONG tag1, ...);
|
||||
|
||||
/* Destroy a DataType object and all associated resources */
|
||||
void DisposeDTObject(Object *o);
|
||||
```
|
||||
|
||||
**Key tags for `NewDTObject()`:**
|
||||
|
||||
| Tag | Type | I/S/G | Description |
|
||||
|---|---|---|---|
|
||||
| `DTA_SourceType` | `ULONG` | I | `DTST_FILE`, `DTST_CLIPBOARD`, `DTST_RAM` |
|
||||
| `DTA_Handle` | `BPTR` | I | File handle (if `DTST_FILE` and already open) |
|
||||
| `DTA_DataType` | `struct DataType *` | G | Returns the matched DataType descriptor |
|
||||
| `DTA_NominalHoriz` | `ULONG` | G | Native width in pixels (picture/animation) |
|
||||
| `DTA_NominalVert` | `ULONG` | G | Native height in pixels |
|
||||
| `DTA_ObjName` | `STRPTR` | G | Title/name metadata from file |
|
||||
| `DTA_ObjAuthor` | `STRPTR` | G | Author metadata |
|
||||
| `DTA_ObjAnnotation` | `STRPTR` | G | Annotation/comment metadata |
|
||||
| `DTA_ObjCopyright` | `STRPTR` | G | Copyright string |
|
||||
| `DTA_ObjVersion` | `STRPTR` | G | Version string |
|
||||
| `DTA_Domain` | `STRPTR` | I | Request a specific datatype by name |
|
||||
| `DTA_GroupID` | `ULONG` | I | Restrict to group (e.g., `GID_PICTURE`) |
|
||||
|
||||
### Attribute Access
|
||||
|
||||
```c
|
||||
/* Get one or more attributes */
|
||||
ULONG GetDTAttrs(Object *o, ULONG tag1, ...);
|
||||
|
||||
/* Set one or more attributes */
|
||||
ULONG SetDTAttrs(Object *o, struct Window *win,
|
||||
struct Requester *req, ULONG tag1, ...);
|
||||
```
|
||||
|
||||
> **Note**: `SetDTAttrs()` requires a window pointer when the object is embedded in a gadget context — it triggers visual refresh through Intuition.
|
||||
|
||||
### Window Embedding
|
||||
|
||||
Because DataType objects are BOOPSI gadgets, they can be added directly to Intuition windows:
|
||||
|
||||
```c
|
||||
/* Add a DataType object to a window as a gadget */
|
||||
LONG AddDTObject(struct Window *win, struct Requester *req,
|
||||
Object *o, LONG pos);
|
||||
|
||||
/* Remove from window */
|
||||
void RemoveDTObject(struct Window *win, Object *o);
|
||||
|
||||
/* Trigger redraw (e.g., after SetDTAttrs changes visual state) */
|
||||
void RefreshDTObjectA(Object *o, struct Window *win,
|
||||
struct Requester *req, struct TagItem *attrs);
|
||||
```
|
||||
|
||||
### Methods
|
||||
|
||||
```c
|
||||
/* Perform a method on the object */
|
||||
ULONG DoDTMethodA(Object *o, struct Window *win,
|
||||
struct Requester *req, Msg msg);
|
||||
|
||||
/* Get supported method mask */
|
||||
ULONG GetDTMethods(Object *obj);
|
||||
|
||||
/* Get supported trigger method mask */
|
||||
ULONG GetDTTriggerMethods(Object *obj);
|
||||
```
|
||||
|
||||
Common methods:
|
||||
|
||||
| Method | Description |
|
||||
|---|---|
|
||||
| `DTM_WRITE` | Save object back to file or clipboard |
|
||||
| `DTM_COPY` | Copy to clipboard |
|
||||
| `DTM_PRINT` | Print the object |
|
||||
| `DTM_TRIGGER` | Execute a trigger (e.g., `STM_PLAY` for sound) |
|
||||
|
||||
### Trigger Methods
|
||||
|
||||
| Trigger | Description |
|
||||
|---|---|
|
||||
| `STM_PLAY` | Play sound / start animation |
|
||||
| `STM_STOP` | Stop playback |
|
||||
| `STM_PAUSE` | Pause playback |
|
||||
| `STM_RESUME` | Resume playback |
|
||||
| `STM_REWIND` | Rewind to start |
|
||||
| `STM_FASTFORWARD` | Fast forward |
|
||||
|
||||
### Picture Class Attributes
|
||||
|
||||
| Tag | Type | Description |
|
||||
|---|---|---|
|
||||
| `PDTA_BitMapHeader` | `struct BitMapHeader **` | Pointer to header struct |
|
||||
| `PDTA_BitMap` | `struct BitMap **` | The actual bitmap (Chip RAM!) |
|
||||
| `PDTA_ColorRegisters` | `struct ColorRegister **` | Palette entries (RGB) |
|
||||
| `PDTA_CRegs` | `LONG **` | Color registers for remapping |
|
||||
| `PDTA_NumColors` | `ULONG` | Number of colors in palette |
|
||||
| `PDTA_ModeID` | `ULONG` | Display ModeID for this image |
|
||||
|
||||
### Sound Class Attributes
|
||||
|
||||
| Tag | Type | Description |
|
||||
|---|---|---|
|
||||
| `SDTA_Sample` | `UBYTE **` | 8-bit sample data pointer |
|
||||
| `SDTA_SampleLength` | `ULONG` | Length in bytes |
|
||||
| `SDTA_Period` | `UWORD` | Paula period value |
|
||||
| `SDTA_Volume` | `UWORD` | Volume 0–64 |
|
||||
| `SDTA_Cycles` | `UWORD` | Loop count (0 = infinite) |
|
||||
| `SDTA_VoiceHeader` | `struct VoiceHeader **` | Voice header struct |
|
||||
|
||||
---
|
||||
|
||||
## Decision Guide: DataTypes vs. Alternatives
|
||||
|
||||
| Criterion | DataTypes | iffparse.library | Direct File I/O |
|
||||
|---|---|---|---|
|
||||
| **When to use** | Quick loading of standard formats; GUI display; clipboard integration | Full control over IFF structure; need custom chunk handling | Non-standard formats; maximum performance; minimal memory |
|
||||
| **Code size** | Small — one API for all formats | Medium — must handle chunks per format | Large — custom parser per format |
|
||||
| **Format coverage** | Extensible via installed classes | IFF only (ILBM, 8SVX, ANIM, etc.) | Whatever you implement |
|
||||
| **Memory overhead** | Higher (BOOPSI objects, BitMapHeader, ColorMap) | Low — you control allocations | Lowest — raw buffers |
|
||||
| **Display integration** | Automatic — embed as gadget | Manual — parse then render yourself | Manual |
|
||||
| **Save/write support** | Yes — `DTM_WRITE` delegates to class | Yes — manual chunk writing | Yes — custom writer |
|
||||
| **OS version** | Requires OS 2.0+ (V37+) | Requires OS 1.3+ (iffparse) | Always works |
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
Q1{"Need to display data<br/>in a window?"} -->|"Yes"| Q2{"Format is standard<br/>ILBM/8SVX/ANIM/text?"}
|
||||
Q1 -->|"No / custom engine"| DIRECT["Direct I/O or<br/>custom parser"]
|
||||
Q2 -->|"Yes"| Q3{"Need fine-grained<br/>chunk control?"}
|
||||
Q2 -->|"No / third-party format"| DT["DataTypes<br/>(if class exists)"]
|
||||
Q3 -->|"No"| DT
|
||||
Q3 -->|"Yes"| IFF["iffparse.library<br/>(manual chunk walk)"]
|
||||
|
||||
style DT fill:#e8f5e9,stroke:#4caf50,color:#333
|
||||
style IFF fill:#fff3e0,stroke:#ff9800,color:#333
|
||||
style DIRECT fill:#ffebee,stroke:#f44336,color:#333
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Historical Context & Modern Analogies
|
||||
|
||||
### The Elegance of Zero-Recompile Extensibility
|
||||
|
||||
The DataTypes system embodies a design philosophy that remains radical even today: **applications should not know what file formats exist**. A developer writes a paint program that calls `NewDTObject()` on any file. Ten years later, a user installs a JPEG datatype class that did not exist when the paint program was written. The paint program — never recompiled, never patched, never reconfigured — can now load JPEG files as if the format had been built in from the start.
|
||||
|
||||
This is not plugin architecture in the limited sense of "my application loads plugins for itself." This is **system-wide late binding**: the OS itself brokers between applications and formats. Two files copied to disk constitute a complete installation:
|
||||
|
||||
1. `SYS:Classes/DataTypes/picture/jpeg.datatype` — the class binary
|
||||
2. `DEVS:DataTypes/JPEG` — the descriptor (magic bytes, extension, precedence)
|
||||
|
||||
No registry editing. No config file parsing. No daemon restart. The next `NewDTObject()` call automatically discovers the new class through `datatypes.library`'s internal scan. Every DataTypes-aware application on the system gains the new capability simultaneously.
|
||||
|
||||
In 1990, this was unprecedented. Applications on every other platform carried format support inside their own binaries:
|
||||
|
||||
| Platform (1990) | Extensible? | Recompile Required? | Mechanism |
|
||||
|---|---|---|---|
|
||||
| **AmigaOS 2.0+** | **Yes** | **No** | System-wide BOOPSI classes with descriptors |
|
||||
| **Atari ST / TT** | No | Yes | Each app bundles its own loaders |
|
||||
| **Mac OS (System 7)** | Partial | Yes | Apps link against specific graphics libraries |
|
||||
| **Windows 3.0** | No | Yes | OLE 1.0 is COM-based and app-specific, not file-centric |
|
||||
| **UNIX (X11)** | No | Yes | Statically linked image libraries per application |
|
||||
| **NeXTSTEP** | Partial | Partial | NXImage uses typed streams but apps must bundle readers |
|
||||
|
||||
The "no recompile" property is the critical differentiator. NeXTSTEP had typed streams, but an application shipped with the readers it knew about. Windows would not get system-wide file-type extensibility until the Windows 95 shell and COM IDataObject (1995). macOS would not get equivalent functionality until QuickTime importer components (1991) and later UTType (2007). Linux distributions would not standardize on MIME-type associations with shared handler plugins until the mid-2000s.
|
||||
|
||||
### How It Worked in Practice
|
||||
|
||||
The release cycle of PNG in the mid-1990s illustrates the benefit perfectly:
|
||||
|
||||
| Step | Traditional Platform | AmigaOS with DataTypes |
|
||||
|---|---|---|
|
||||
| New format emerges (PNG) | User must wait for every app vendor to add support | User waits for one developer to write a PNG datatype |
|
||||
| Support arrives | Download new versions of paint program, image viewer, thumbnailer | Copy `png.datatype` and `DEVS:DataTypes/PNG` to disk |
|
||||
| Integration | Each app has its own PNG decoder, color management, metadata handling | All apps share one canonical PNG decoder |
|
||||
| Consistency | Colors render differently in each app | One color remap through pictureclass |
|
||||
| Clipboard | Apps must add PNG import/export to their private clipboard logic | `DTST_CLIPBOARD` works automatically via the superclass |
|
||||
|
||||
### Modern Analogies
|
||||
|
||||
The closest modern equivalents to DataTypes' system-wide, zero-recompile extensibility are **media framework plugin systems** and **OS-level content-type registries**:
|
||||
|
||||
| DataTypes Concept | Modern Equivalent | Why It Maps |
|
||||
|---|---|---|
|
||||
| `NewDTObject()` → auto-routed to class | **GStreamer pipeline auto-plug** | Install an AV1 decoder plugin; ALL GStreamer applications (Totem, Cheese, Rhythmbox) can play AV1 without recompilation. They do not know AV1 exists — they ask GStreamer to "play this" and the framework negotiates the correct element. |
|
||||
| Descriptor in `DEVS:DataTypes/` | **Linux MIME `.desktop` associations + shared-mime-info** | Install one MIME type definition and one application handler; all file managers, web browsers, and email clients can open that format. But: apps must still understand the data, unlike DataTypes where the OS returns a usable object. |
|
||||
| pictureclass / soundclass | **ImageMagick codec delegates / GEGL operations** | Add a HEIC delegate to ImageMagick; all ImageMagick-based tools (and GIMP via plugin) gain HEIC support. The superclass (`Image` / `GeglBuffer`) abstracts format specifics. |
|
||||
| `DTA_SourceType` + auto-detection | **macOS `UTType` + `QLPreviewController`** | The system identifies a file by its content (not just extension) and routes to the correct preview provider. QuickLook plugins work system-wide without host application recompilation. |
|
||||
| `DTM_WRITE` serialization | **Microsoft COM `IPersistFile`** | The OS asks the format handler to serialize itself; the host application delegates save logic. But COM requires explicit interface negotiation; DataTypes uses attribute tags. |
|
||||
| `DTST_CLIPBOARD` uniform transfer | **HTML5 Clipboard API `ClipboardItem`** | Applications paste "an image" without caring whether it arrived as PNG, JPEG, or BMP. The platform handles format conversion. |
|
||||
| BOOPSI gadget embedding | **WPF `Image` / Qt `QLabel` with `QPixmap`** | The view widget renders from an abstract source. But modern widgets are framework-specific; a DataTypes object is an Intuition-native BOOPSI gadget addable to any window. |
|
||||
|
||||
### Where the Analogies Break Down
|
||||
|
||||
Despite the parallels, no modern system fully replicates DataTypes' combination of properties:
|
||||
|
||||
1. **True OS-native object model**: DataTypes objects are BOOPSI objects — they can be added directly to Intuition windows, receive input events, and participate in the layout system. Modern equivalents require wrapper widgets (Qt `QImageReader` → `QLabel`), adapter layers (GStreamer → GTK video sink), or explicit host application support (QuickLook plugins need a host that calls the API).
|
||||
|
||||
2. **Single-file install, no registration step**: GStreamer plugins need `gst-plugin-scanner` cache updates; ImageMagick delegates need `policy.xml` edits; macOS UTTypes need `Info.plist` declarations and app bundle restarts. Amiga DataTypes required only a file copy — `datatypes.library` rescans on demand.
|
||||
|
||||
3. **Write-time ignorance**: A developer in 1992 could write `NewDTObject(path, DTA_GroupID, GID_PICTURE, TAG_DONE)` and be guaranteed that every image format invented through 2026 would work, provided a datatype class existed. Modern frameworks generally require the application to at least specify a MIME type or content category.
|
||||
|
||||
4. **No async loading**: `NewDTObject()` is synchronous and blocking. Modern frameworks (GStreamer's async state changes, Android's `ContentResolver` async queries, web `createImageBitmap()`) use callbacks, promises, or coroutines to avoid blocking the UI thread.
|
||||
|
||||
5. **No streaming or progressive decode**: The entire file is parsed into a complete BOOPSI object before control returns. There is no equivalent to modern progressive JPEG decode, video frame streaming, or memory-mapped access.
|
||||
|
||||
6. **Planar graphics assumptions**: Color remapping, BitMap allocation, and palette management are designed for Amiga's planar display system. A modern RGBA framebuffer datatype would need a fundamentally different `pictureclass` implementation.
|
||||
|
||||
---
|
||||
|
||||
## Practical Examples
|
||||
|
||||
### Example 1: Load and Display an Image
|
||||
|
||||
```c
|
||||
#include <exec/types.h>
|
||||
#include <intuition/intuition.h>
|
||||
#include <datatypes/datatypes.h>
|
||||
#include <datatypes/pictureclass.h>
|
||||
#include <proto/datatypes.h>
|
||||
#include <proto/exec.h>
|
||||
#include <proto/intuition.h>
|
||||
#include <proto/graphics.h>
|
||||
|
||||
/* Load an image file and extract its BitMap */
|
||||
struct BitMap *LoadPicture(CONST_STRPTR path, struct ColorMap **cmOut)
|
||||
{
|
||||
struct BitMap *bm = NULL;
|
||||
struct ColorMap *cm = NULL;
|
||||
|
||||
Object *dto = NewDTObject((APTR)path,
|
||||
DTA_SourceType, DTST_FILE,
|
||||
DTA_GroupID, GID_PICTURE,
|
||||
PDTA_Remap, FALSE, /* keep original colors */
|
||||
TAG_DONE);
|
||||
|
||||
if (!dto)
|
||||
{
|
||||
/* File not found, or no matching picture class installed */
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Retrieve the BitMap and ColorMap from the picture superclass */
|
||||
GetDTAttrs(dto,
|
||||
PDTA_BitMap, (ULONG)&bm,
|
||||
PDTA_ColorMap, (ULONG)&cm,
|
||||
TAG_DONE);
|
||||
|
||||
/* Note: the BitMap belongs to the object; if you need it after
|
||||
DisposeDTObject(), you must copy or detach it. */
|
||||
|
||||
if (cmOut) *cmOut = cm;
|
||||
|
||||
/* DisposeDTObject() will free bm and cm unless you detached them.
|
||||
For a quick blit-and-forget, keep the object alive until done. */
|
||||
return bm; /* still valid while dto lives */
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: Play a Sound Sample
|
||||
|
||||
```c
|
||||
#include <datatypes/datatypes.h>
|
||||
#include <datatypes/soundclass.h>
|
||||
#include <proto/datatypes.h>
|
||||
|
||||
BOOL PlaySoundFile(CONST_STRPTR path)
|
||||
{
|
||||
Object *dto = NewDTObject((APTR)path,
|
||||
DTA_SourceType, DTST_FILE,
|
||||
DTA_GroupID, GID_SOUND,
|
||||
TAG_DONE);
|
||||
|
||||
if (!dto) return FALSE;
|
||||
|
||||
/* Trigger playback */
|
||||
struct dtTrigger dtt;
|
||||
dtt.dtt_Method = DTM_TRIGGER;
|
||||
dtt.dtt_GInfo = NULL;
|
||||
dtt.dtt_Function = STM_PLAY;
|
||||
dtt.dtt_Data = NULL;
|
||||
|
||||
DoDTMethodA(dto, NULL, NULL, (Msg)&dtt);
|
||||
|
||||
/* In a real app, wait for completion or provide UI to stop */
|
||||
/* For now, just let it play and clean up after a delay */
|
||||
Delay(300); /* ~6 seconds at 50 Hz */
|
||||
|
||||
DisposeDTObject(dto);
|
||||
return TRUE;
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: Embed a DataType Object in a Window
|
||||
|
||||
```c
|
||||
#include <intuition/intuition.h>
|
||||
#include <datatypes/datatypes.h>
|
||||
#include <proto/datatypes.h>
|
||||
#include <proto/intuition.h>
|
||||
|
||||
struct Window *win;
|
||||
Object *dtObj;
|
||||
|
||||
BOOL OpenImageWindow(CONST_STRPTR path)
|
||||
{
|
||||
win = OpenWindowTags(NULL,
|
||||
WA_Title, "DataTypes Viewer",
|
||||
WA_Width, 640,
|
||||
WA_Height, 480,
|
||||
WA_Flags, WFLG_CLOSEGADGET | WFLG_DRAGBAR |
|
||||
WFLG_DEPTHGADGET | WFLG_ACTIVATE,
|
||||
TAG_DONE);
|
||||
|
||||
if (!win) return FALSE;
|
||||
|
||||
/* Create the DataType object; it acts as a BOOPSI gadget */
|
||||
dtObj = NewDTObject((APTR)path,
|
||||
DTA_SourceType, DTST_FILE,
|
||||
GA_Left, 0,
|
||||
GA_Top, 0,
|
||||
GA_RelWidth, TRUE,
|
||||
GA_RelHeight, TRUE,
|
||||
TAG_DONE);
|
||||
|
||||
if (!dtObj)
|
||||
{
|
||||
CloseWindow(win);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Add to window — the object renders itself */
|
||||
AddDTObject(win, NULL, dtObj, -1);
|
||||
RefreshDTObjectA(dtObj, win, NULL, NULL);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void CloseImageWindow(void)
|
||||
{
|
||||
if (dtObj)
|
||||
{
|
||||
RemoveDTObject(win, dtObj);
|
||||
DisposeDTObject(dtObj);
|
||||
dtObj = NULL;
|
||||
}
|
||||
if (win)
|
||||
{
|
||||
CloseWindow(win);
|
||||
win = NULL;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example 4: Load from Clipboard
|
||||
|
||||
```c
|
||||
#include <datatypes/datatypes.h>
|
||||
#include <proto/datatypes.h>
|
||||
|
||||
Object *LoadFromClipboard(void)
|
||||
{
|
||||
Object *dto = NewDTObject(NULL,
|
||||
DTA_SourceType, DTST_CLIPBOARD,
|
||||
DTA_GroupID, GID_PICTURE, /* or GID_TEXT, GID_SOUND */
|
||||
TAG_DONE);
|
||||
|
||||
/* The library reads the clipboard unit 0 and identifies the
|
||||
format automatically via the descriptor signatures. */
|
||||
|
||||
return dto;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When to Use / When NOT to Use
|
||||
|
||||
### When to Use DataTypes
|
||||
|
||||
| Scenario | Why DataTypes Excels |
|
||||
|---|---|
|
||||
| **System-friendly applications** | Cooperative with Intuition; objects are valid BOOPSI gadgets |
|
||||
| **Multi-format viewers** | One codebase handles ILBM, PNG, GIF, JPEG, etc. automatically |
|
||||
| **Clipboard integration** | `DTST_CLIPBOARD` gives uniform read/write for free |
|
||||
| **Rapid prototyping** | `NewDTObject()` + `AddDTObject()` displays an image in ~10 lines |
|
||||
| **Workbench tools** | Metadata tags (`DTA_ObjName`, `DTA_ObjAuthor`) expose file info |
|
||||
| **Save/export support** | `DTM_WRITE` delegates to the class; no custom serializer needed |
|
||||
|
||||
### When NOT to Use DataTypes
|
||||
|
||||
| Scenario | Problem | Better Alternative |
|
||||
|---|---|---|
|
||||
| **Game engine texture loading** | BOOPSI overhead, planar bitmap conversion, no async | Direct `AllocBitMap()` + custom loader; use [iffparse.library](iffparse.md) for IFF |
|
||||
| **Real-time audio streaming** | Entire sample loaded synchronously; no streaming API | Direct `audio.device` with double-buffered [IORequest](../06_exec_os/io_requests.md) |
|
||||
| **Memory-constrained tools** | Object overhead + full bitmap + colormap can be large | iffparse.library with on-demand decode |
|
||||
| **Custom or proprietary formats** | No class exists; writing a class is more work than a parser | Direct file I/O or custom parser |
|
||||
| **Batch conversion pipelines** | `NewDTObject()` → `DTM_WRITE` is convenient but slower than dedicated tools | Command-line tools (ImageMagick port, etc.) |
|
||||
| **Need pixel-level access during decode** | Framework abstracts away the parsing pipeline | iffparse.library or direct loader |
|
||||
|
||||
### Applicability Ranges
|
||||
|
||||
- **Image sizes up to ~1 MB** (Chip RAM permitting): DataTypes handles comfortably
|
||||
- **Audio samples > 500 KB**: Consider direct audio.device I/O to avoid duplication
|
||||
- **Animation**: Use DataTypes for short clips; long sequences need custom frame management
|
||||
- **Object counts**: Embedding >20 DataType gadgets in one window stresses Intuition's gadget list
|
||||
|
||||
---
|
||||
|
||||
## Best Practices & Antipatterns
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Always check `NewDTObject()` return value** — a missing class or corrupted file returns `NULL`
|
||||
2. **Set `DTA_GroupID` when the type is known** — reduces descriptor scan time and prevents misidentification
|
||||
3. **Use `PDTA_Remap, FALSE` for off-screen processing** — avoids unnecessary color remapping
|
||||
4. **Keep the object alive while using its BitMap/Sample** — disposing frees the underlying buffers
|
||||
5. **Call `RefreshDTObjectA()` after `SetDTAttrs()` on an embedded object** — Intuition does not auto-redraw
|
||||
6. **Use `RemoveDTObject()` before `DisposeDTObject()`** on window-embedded objects — prevents dangling gadget pointers
|
||||
7. **Query `GetDTMethods()` before calling `DTM_WRITE`** — not all subclasses implement save
|
||||
8. **Free the file handle yourself if you passed `DTA_Handle`** — the library does not close it
|
||||
9. **Call `SetIoErr()` before returning `NULL` from a custom class** — proper DOS error propagation
|
||||
10. **Test with `DTA_Domain` to force a specific class** — useful for debugging format detection
|
||||
|
||||
### Antipatterns
|
||||
|
||||
#### 1. The Orphaned BitMap
|
||||
|
||||
```c
|
||||
/* ANTIPATTERN — extracting pointer then disposing the owner */
|
||||
struct BitMap *bm;
|
||||
GetDTAttrs(dto, PDTA_BitMap, (ULONG)&bm, TAG_DONE);
|
||||
DisposeDTObject(dto);
|
||||
/* bm is now a dangling pointer — the BitMap was freed */
|
||||
BltBitMap(bm, ...); /* CRASH */
|
||||
|
||||
/* CORRECT — either keep dto alive, or copy the BitMap */
|
||||
struct BitMap *myCopy = AllocBitMap(width, height, depth,
|
||||
BMF_CLEAR, screen->RastPort.BitMap);
|
||||
BltBitMap(bm, 0, 0, myCopy, 0, 0, width, height, 0xC0, 0x01, NULL);
|
||||
DisposeDTObject(dto);
|
||||
/* myCopy is now safe to use independently */
|
||||
```
|
||||
|
||||
#### 2. The Naked Dispose
|
||||
|
||||
```c
|
||||
/* ANTIPATTERN — disposing while still attached to window */
|
||||
DisposeDTObject(dtObj); /* Gadget list still references this object */
|
||||
CloseWindow(win); /* Intuition walks gadget list → CRASH */
|
||||
|
||||
/* CORRECT — remove first, then dispose */
|
||||
RemoveDTObject(win, dtObj);
|
||||
DisposeDTObject(dtObj);
|
||||
CloseWindow(win);
|
||||
```
|
||||
|
||||
#### 3. The Silent Failure
|
||||
|
||||
```c
|
||||
/* ANTIPATTERN — no error checking, no diagnostics */
|
||||
Object *dto = NewDTObject((APTR)path, TAG_DONE);
|
||||
AddDTObject(win, NULL, dto, -1); /* dto could be NULL */
|
||||
|
||||
/* CORRECT — check and report */
|
||||
Object *dto = NewDTObject((APTR)path,
|
||||
DTA_SourceType, DTST_FILE,
|
||||
TAG_DONE);
|
||||
if (!dto)
|
||||
{
|
||||
LONG err = IoErr();
|
||||
Printf("Failed to load '%s': %ld\n", path, err);
|
||||
return FALSE;
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. The Assuming Saver
|
||||
|
||||
```c
|
||||
/* ANTIPATTERN — calling DTM_WRITE on a read-only subclass */
|
||||
struct dtWrite dtw;
|
||||
dtw.dtw_Method = DTM_WRITE;
|
||||
dtw.dtw_GInfo = NULL;
|
||||
dtw.dtw_FileHandle = fh;
|
||||
dtw.dtw_Mode = DTWM_RAW;
|
||||
DoDTMethodA(dto, NULL, NULL, (Msg)&dtw); /* May fail silently */
|
||||
|
||||
/* CORRECT — check capabilities first */
|
||||
if (GetDTMethods(dto) & (1L << DTM_WRITE))
|
||||
{
|
||||
/* Safe to attempt write */
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pitfalls & Common Mistakes
|
||||
|
||||
### 1. BitMap in Chip RAM for AGA/Display
|
||||
|
||||
```c
|
||||
/* PITFALL — assuming BitMap is displayable on custom screens */
|
||||
struct BitMap *bm;
|
||||
GetDTAttrs(dto, PDTA_BitMap, (ULONG)&bm, TAG_DONE);
|
||||
|
||||
/* On systems without graphics card, BitMap MUST be in Chip RAM
|
||||
for blitter or display DMA. DataTypes usually allocates correctly,
|
||||
but if you replace the BitMap, verify: */
|
||||
if (TypeOfMem(bm) & MEMF_CHIP)
|
||||
{
|
||||
/* Safe for display DMA */
|
||||
}
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> **Requires Chip RAM**: The `PDTA_BitMap` returned by pictureclass must reside in Chip RAM if you intend to use it with the Blitter or display it via custom chip DMA. Fast RAM bitmaps are fine for CPU-only processing but will crash if passed to `BltBitMap()` or attached to a `ViewPort` on OCS/ECS/AGA.
|
||||
|
||||
### 2. ColorMap Lifecycle
|
||||
|
||||
```c
|
||||
/* PITFALL — ColorMap freed with object, but ViewPort still references it */
|
||||
struct ColorMap *cm;
|
||||
GetDTAttrs(dto, PDTA_ColorMap, (ULONG)&cm, TAG_DONE);
|
||||
viewport->ColorMap = cm; /* ViewPort now references object's memory */
|
||||
DisposeDTObject(dto); /* cm is freed — ViewPort now has dangling pointer */
|
||||
|
||||
/* CORRECT — copy the ColorMap if the ViewPort outlives the object */
|
||||
struct ColorMap *cmCopy = CopyColorMap(cm);
|
||||
viewport->ColorMap = cmCopy;
|
||||
DisposeDTObject(dto);
|
||||
/* Free cmCopy later when ViewPort is torn down */
|
||||
```
|
||||
|
||||
### 3. Forgetting to Refresh After Resize
|
||||
|
||||
```c
|
||||
/* PITFALL — changing size attributes without refresh */
|
||||
SetDTAttrs(dtObj, win, NULL,
|
||||
DTA_NominalHoriz, 320,
|
||||
DTA_NominalVert, 200,
|
||||
TAG_DONE);
|
||||
/* Window still shows old size; visual garbage ensues */
|
||||
|
||||
/* CORRECT — trigger redraw */
|
||||
SetDTAttrs(dtObj, win, NULL,
|
||||
DTA_NominalHoriz, 320,
|
||||
DTA_NominalVert, 200,
|
||||
TAG_DONE);
|
||||
RefreshDTObjectA(dtObj, win, NULL, NULL);
|
||||
```
|
||||
|
||||
### 4. Mixing DataTypes with Custom Blitter Code
|
||||
|
||||
```c
|
||||
/* PITFALL — DataTypes BitMap may not match your RastPort's format */
|
||||
struct BitMap *bm;
|
||||
GetDTAttrs(dto, PDTA_BitMap, (ULONG)&bm, TAG_DONE);
|
||||
|
||||
/* If your RastPort is 3-plane and the image is 5-plane (HAM),
|
||||
direct BltBitMap() produces garbage. Always check depth: */
|
||||
struct BitMapHeader *bmh;
|
||||
GetDTAttrs(dto, PDTA_BitMapHeader, (ULONG)&bmh, TAG_DONE);
|
||||
if (bmh && bmh->bmh_Depth <= myRastPort->BitMap->Depth)
|
||||
{
|
||||
BltBitMap(bm, ...); /* Safe */
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Assuming DTA_Handle Ownership
|
||||
|
||||
```c
|
||||
/* PITFALL — library closes handle if it opened it, but not if you passed it */
|
||||
BPTR fh = Open(path, MODE_OLDFILE);
|
||||
Object *dto = NewDTObject(NULL,
|
||||
DTA_SourceType, DTST_FILE,
|
||||
DTA_Handle, fh,
|
||||
TAG_DONE);
|
||||
DisposeDTObject(dto);
|
||||
/* fh may or may not still be valid — behavior is inconsistent
|
||||
across subclasses. Always open/close yourself. */
|
||||
|
||||
/* CORRECT — let DataTypes open the file, or manage the handle yourself */
|
||||
Object *dto = NewDTObject((APTR)path,
|
||||
DTA_SourceType, DTST_FILE,
|
||||
TAG_DONE);
|
||||
DisposeDTObject(dto);
|
||||
/* File was opened and closed by the framework */
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Real-World Software That Relied on DataTypes
|
||||
|
||||
| Software | How It Used DataTypes |
|
||||
|---|---|
|
||||
| **MultiView** (OS 3.1+) | The canonical DataTypes viewer — opens any file DataTypes can identify; essentially a thin wrapper around `NewDTObject()` + `AddDTObject()` |
|
||||
| **Directory Opus** | File viewer pane uses DataTypes for image/sound preview |
|
||||
| **Personal Paint** | Uses pictureclass for import of non-native formats (GIF, JPEG via third-party classes) |
|
||||
| **AmigaGuide** | The hypertext system is implemented as a DataType class; `amigaguideclass` renders pages and handles links |
|
||||
| **Web browsers (AWeb, IBrowse)** | Image decoding delegated to DataTypes; new image formats installable without browser updates |
|
||||
| **Audio players** | `STM_PLAY` trigger used for preview in file managers |
|
||||
|
||||
### Integration Patterns
|
||||
|
||||
**Pattern A: Thumbnail Generator**
|
||||
```
|
||||
For each file in directory:
|
||||
dto = NewDTObject(path, DTA_GroupID, GID_PICTURE)
|
||||
if dto:
|
||||
GetDTAttrs(dto, PDTA_BitMap, &bm, ...)
|
||||
ScaleBitMap(bm, thumbSize) /* custom downscale */
|
||||
DisposeDTObject(dto)
|
||||
```
|
||||
|
||||
**Pattern B: Format Validator**
|
||||
```
|
||||
dto = NewDTObject(path, TAG_DONE)
|
||||
if dto:
|
||||
GetDTAttrs(dto, DTA_DataType, &dtn, ...)
|
||||
printf("Type: %s Group: %s\n", dtn->dtn_Node.ln_Name, groupName[dtn->dtn_GroupID])
|
||||
DisposeDTObject(dto)
|
||||
else:
|
||||
printf("Unknown or corrupted file\n")
|
||||
```
|
||||
|
||||
**Pattern C: Clipboard Image Paste**
|
||||
```
|
||||
dto = NewDTObject(NULL, DTA_SourceType, DTST_CLIPBOARD, DTA_GroupID, GID_PICTURE)
|
||||
if dto:
|
||||
AddDTObject(win, NULL, dto, -1)
|
||||
RefreshDTObjectA(dto, win, NULL, NULL)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: Can I write my own DataType class?**
|
||||
> Yes. You create a BOOPSI subclass of the appropriate superclass (e.g., pictureclass), implement `OM_NEW` to parse your format and fill the superclass structures, and install a descriptor in `DEVS:DataTypes/`. The OS will automatically route matching files to your class.
|
||||
|
||||
**Q: Why does `NewDTObject()` return NULL for a valid JPEG?**
|
||||
> Either `jpeg.datatype` is not installed, or the file's magic bytes do not match the descriptor's signature. Verify the class exists in `SYS:Classes/DataTypes/` and its descriptor is in `DEVS:DataTypes/`. Also check `IoErr()` immediately after failure.
|
||||
|
||||
**Q: How do I know which methods an object supports?**
|
||||
> Call `GetDTMethods(obj)` — it returns a bitmask of supported methods. Similarly, `GetDTTriggerMethods(obj)` returns supported triggers like `STM_PLAY`.
|
||||
|
||||
**Q: Is DataTypes available on OS 1.3?**
|
||||
> No. DataTypes requires OS 2.0 (V37) or later, as it depends on BOOPSI (introduced in 2.0). On 1.3, use iffparse.library or direct file I/O.
|
||||
|
||||
**Q: Can I use DataTypes without opening an Intuition window?**
|
||||
> Yes. `NewDTObject()` with `DTST_FILE` creates an off-screen object. Use `GetDTAttrs()` to extract `PDTA_BitMap` or `SDTA_Sample` and process the data directly. You only need a window if you call `AddDTObject()`.
|
||||
|
||||
**Q: Why is my embedded DataType object not redrawn after `SetDTAttrs()`?**
|
||||
> `SetDTAttrs()` updates internal state but does not send a gadget refresh message. You must explicitly call `RefreshDTObjectA()` or use `SetGadgetAttrs()` if the object is in a window context.
|
||||
|
||||
**Q: Do DataType objects work with MUI?**
|
||||
> Indirectly. MUI provides the `Boopsi` class to embed native BOOPSI gadgets. Create the DataType object, then embed it via `MUIA_Boopsi_Gadget` or `MUIA_Boopsi_Object` in a `BoopsiObject`.
|
||||
|
||||
## References
|
||||
|
||||
- NDK 3.9: `datatypes/datatypes.h`, `datatypes/datatypesclass.h`, `datatypes/pictureclass.h`, `datatypes/soundclass.h`, `datatypes/textclass.h`, `datatypes/animationclass.h`
|
||||
- ADCD 2.1: `datatypes.library` autodocs (`datatypes.doc`, `picture_dtc.doc`, `sound_dtc.doc`)
|
||||
- *Amiga ROM Kernel Reference Manual: Libraries* — Chapter 23: DataTypes
|
||||
- See also: [iffparse.md](iffparse.md) — Low-level IFF parsing when DataTypes abstraction is insufficient
|
||||
- See also: [boopsi.md](../09_intuition/boopsi.md) — BOOPSI object system foundation
|
||||
- See also: [bitmap.md](../08_graphics/bitmap.md) — Planar BitMap structure and Chip RAM requirements
|
||||
- See also: [memory_management.md](../06_exec_os/memory_management.md) — `AllocMem()` flags and Chip vs Fast RAM
|
||||
|
|
@ -123,23 +123,23 @@ FreeMem(buf, bufSize);
|
|||
| `AFF_DISK` | Available on `FONTS:` | Requires disk access to load |
|
||||
| `AFF_SCALED` | Algorithmically scaled from another size | Lower quality; avoid when native size exists |
|
||||
| `AFF_BITMAP` | Bitmap (pixel) font | Standard Amiga font format |
|
||||
| `AFF_TAGGED` | Tagged (OS 3.0+ extended) font | Supports colour fonts, outlined fonts |
|
||||
| `AFF_TAGGED` | Tagged (OS 3.0+ extended) font | Supports color fonts, outlined fonts |
|
||||
|
||||
---
|
||||
|
||||
## Colour Fonts (OS 3.0+)
|
||||
## Color Fonts (OS 3.0+)
|
||||
|
||||
OS 3.0 introduced **colour bitmap fonts** — each glyph can have multiple bitplanes:
|
||||
OS 3.0 introduced **color bitmap fonts** — each glyph can have multiple bitplanes:
|
||||
|
||||
```c
|
||||
/* Colour fonts use ColorTextFont — an extension of TextFont: */
|
||||
/* Color fonts use ColorTextFont — an extension of TextFont: */
|
||||
struct ColorTextFont {
|
||||
struct TextFont ctf_TF; /* standard TextFont */
|
||||
UWORD ctf_Flags; /* CT_COLORFONT etc. */
|
||||
UBYTE ctf_Depth; /* number of bitplanes */
|
||||
UBYTE ctf_FgColor; /* default foreground pen */
|
||||
UBYTE ctf_Low; /* lowest colour used */
|
||||
UBYTE ctf_High; /* highest colour used */
|
||||
UBYTE ctf_Low; /* lowest color used */
|
||||
UBYTE ctf_High; /* highest color used */
|
||||
APTR ctf_PlanePick; /* plane selection */
|
||||
APTR ctf_PlaneOnOff; /* plane on/off defaults */
|
||||
struct ColorFontColors *ctf_ColorTable;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
Every Workbench-visible file has a companion `.info` file containing its icon imagery, tool types (key=value metadata), default tool, stack size, and position. `icon.library` provides reading, writing, and manipulating these structures.
|
||||
|
||||
The `.info` file format is binary, not text. icon.library handles all serialisation.
|
||||
The `.info` file format is binary, not text. icon.library handles all serialization.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
|
@ -52,7 +52,7 @@ struct DiskObject {
|
|||
|
||||
## Icon Types
|
||||
|
||||
| Constant | Value | Description | Workbench Behaviour |
|
||||
| Constant | Value | Description | Workbench Behavior |
|
||||
|---|---|---|---|
|
||||
| `WBDISK` | 1 | Disk/volume icon | Opens drawer showing disk contents |
|
||||
| `WBDRAWER` | 2 | Drawer (directory) | Opens drawer window |
|
||||
|
|
@ -169,12 +169,12 @@ FreeDiskObject(newIcon);
|
|||
|
||||
## OS 3.5+ New-Style Icons
|
||||
|
||||
AmigaOS 3.5 introduced **true-colour icons** (PNG-based) alongside the legacy planar format. The `icon.library` v46+ handles both transparently — `GetDiskObject` returns the best available format.
|
||||
AmigaOS 3.5 introduced **true-color icons** (PNG-based) alongside the legacy planar format. The `icon.library` v46+ handles both transparently — `GetDiskObject` returns the best available format.
|
||||
|
||||
| Feature | Legacy (OS 1.x–3.1) | New-Style (OS 3.5+) |
|
||||
|---|---|---|
|
||||
| Format | Planar bitplane imagery | PNG/true-colour embedded |
|
||||
| Colours | 4–16 (Workbench palette) | 24-bit true colour |
|
||||
| Format | Planar bitplane imagery | PNG/true-color embedded |
|
||||
| Colors | 4–16 (Workbench palette) | 24-bit true color |
|
||||
| Size | Fixed (standard sizes) | Scalable |
|
||||
| Transparency | 1-bit mask | 8-bit alpha channel |
|
||||
| Storage | `do_Gadget.GadgetRender` | Extended chunks in `.info` |
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ Common IFF types:
|
|||
flowchart TD
|
||||
subgraph "IFF ILBM File"
|
||||
FORM["FORM ILBM"] --> BMHD["BMHD<br/>(bitmap header)"]
|
||||
FORM --> CMAP["CMAP<br/>(colour palette)"]
|
||||
FORM --> CMAP["CMAP<br/>(color palette)"]
|
||||
FORM --> CAMG["CAMG<br/>(Amiga view mode)"]
|
||||
FORM --> BODY["BODY<br/>(pixel data)"]
|
||||
end
|
||||
|
|
@ -60,6 +60,344 @@ Nested FORMs:
|
|||
| `CAT ` | Unordered concatenation of FORMs |
|
||||
| `PROP` | Default property block (within LIST) |
|
||||
|
||||
### Chunk Wire Format
|
||||
|
||||
Every IFF chunk begins with an 8-byte header — a 4-character ASCII ID followed by a 4-byte big-endian size:
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────┐
|
||||
│ IFF Chunk Header (8 bytes) │
|
||||
├──────────┬──────────┬──────────┬──────────┬───────────┤
|
||||
│ Offset │ +$00 │ +$01 │ +$02 │ +$03 │
|
||||
├──────────┼──────────┴──────────┴──────────┴───────────┤
|
||||
│ ID │ 4-char ASCII FOURCC (e.g. 'B' 'M' 'H' 'D') │
|
||||
│ │ → ID_BMHD = 0x424D4844 │
|
||||
├──────────┼──────────┬──────────┬──────────┬───────────┤
|
||||
│ Offset │ +$04 │ +$05 │ +$06 │ +$07 │
|
||||
├──────────┼──────────┴──────────┴──────────┴───────────┤
|
||||
│ Size │ 32-bit BIG-ENDIAN unsigned LONG │
|
||||
│ │ (data bytes ONLY, excludes header & pad) │
|
||||
└──────────┴────────────────────────────────────────────┘
|
||||
|
||||
┌── Data bytes (0..Size-1) ──┐
|
||||
│ │
|
||||
│ [pad byte if Size is odd] │
|
||||
└────────────────────────────┘
|
||||
```
|
||||
|
||||
```c
|
||||
/* The raw chunk header — 8 bytes on disk */
|
||||
struct IFFChunkHeader {
|
||||
ULONG ck_ID; /* 4-char FOURCC, e.g. 'BMHD' = 0x424D4844 */
|
||||
LONG ck_Size; /* data length in bytes, BIG-ENDIAN */
|
||||
};
|
||||
```
|
||||
|
||||
| Field | Bytes | Type | Description |
|
||||
|---|---|---|---|
|
||||
| `ck_ID` | 0–3 | `ULONG` (big-endian) | FOURCC identifier. `0x424D4844` = `'B'<<24 \| 'M'<<16 \| 'H'<<8 \| 'D'` = "BMHD" |
|
||||
| `ck_Size` | 4–7 | `LONG` (big-endian) | Number of data bytes following the header. **Does not include** the 8-byte header or the optional pad byte. May be 0 for empty chunks |
|
||||
|
||||
**Pad byte rule**: If `ck_Size` is odd, a single zero-pad byte follows the data to restore even alignment. The pad byte is **not counted** in `ck_Size`. This ensures the next chunk header always starts at an even offset from the file beginning.
|
||||
|
||||
### Container Headers — FORM, LIST, CAT
|
||||
|
||||
Container chunks (FORM, LIST, CAT) have a **12-byte header** — the standard 8-byte chunk header plus a 4-byte form type:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ Container Chunk Header (12 bytes minimum) │
|
||||
├──────────┬──────────┬──────────┬──────────┬──────────────────────┤
|
||||
│ Offset │ +$00–$03 │ +$04–$07 │ +$08–$0B │ +$0C... │
|
||||
├──────────┼──────────┼──────────┼──────────┼──────────────────────┤
|
||||
│ Field │ ck_ID │ ck_Size │ FormType │ Sub-chunks... │
|
||||
│ │ "FORM" │ (includes│ "ILBM" │ │
|
||||
│ │ "LIST" │ FormType│ "FTXT" │ │
|
||||
│ │ "CAT " │ + subs) │ "PBM " │ │
|
||||
└──────────┴──────────┴──────────┴──────────┴──────────────────────┘
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> **Critical detail**: `ck_Size` in a container header **includes the 4-byte FormType plus all nested sub-chunks**. For example, a FORM ILBM with BMHD (20 bytes) + CMAP (48 bytes) + BODY (32,000 bytes) has `ck_Size = 4 + 20 + 48 + 32000 = 32072`. The 4-byte FormType is part of the counted size.
|
||||
|
||||
### Computing Offsets — Walking a File Manually
|
||||
|
||||
When reading an IFF file without iffparse.library, you walk chunks by reading the header and advancing:
|
||||
|
||||
```
|
||||
next_chunk_offset = current_offset + 8 + ck_Size + (ck_Size & 1)
|
||||
│ │ │ │
|
||||
│ │ │ └─ pad byte if odd
|
||||
│ │ └─────────── data bytes
|
||||
│ └────────────────── chunk header (8 bytes)
|
||||
└───────────────────────────────── where we are now
|
||||
```
|
||||
|
||||
```c
|
||||
/* Manual IFF chunk walker — for reading without iffparse.library */
|
||||
LONG WalkIFFChunks(BPTR fh)
|
||||
{
|
||||
LONG offset = 0;
|
||||
struct IFFChunkHeader hdr;
|
||||
|
||||
while (Read(fh, &hdr, sizeof(hdr)) == sizeof(hdr))
|
||||
{
|
||||
Printf("Offset $%08lx: chunk '%c%c%c%c', size=%ld\n",
|
||||
offset,
|
||||
(char)(hdr.ck_ID >> 24), (char)(hdr.ck_ID >> 16),
|
||||
(char)(hdr.ck_ID >> 8), (char)(hdr.ck_ID),
|
||||
hdr.ck_Size);
|
||||
|
||||
if (hdr.ck_ID == ID_FORM || hdr.ck_ID == ID_LIST ||
|
||||
hdr.ck_ID == ID_CAT)
|
||||
{
|
||||
/* Read the 4-char FormType after the header */
|
||||
ULONG formType;
|
||||
Read(fh, &formType, 4);
|
||||
Printf(" → Container type: '%c%c%c%c'\n",
|
||||
(char)(formType >> 24), (char)(formType >> 16),
|
||||
(char)(formType >> 8), (char)(formType));
|
||||
/* formType is included in ck_Size — adjust remaining data */
|
||||
LONG dataAfterType = hdr.ck_Size - 4;
|
||||
/* Skip the nested contents */
|
||||
Seek(fh, dataAfterType, OFFSET_CURRENT);
|
||||
offset += 8 + 4 + dataAfterType;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Regular chunk — skip its data + optional pad */
|
||||
Seek(fh, hdr.ck_Size, OFFSET_CURRENT);
|
||||
offset += 8 + hdr.ck_Size;
|
||||
}
|
||||
|
||||
/* Skip pad byte if data was odd-sized */
|
||||
if (hdr.ck_Size & 1)
|
||||
{
|
||||
Seek(fh, 1, OFFSET_CURRENT);
|
||||
offset += 1;
|
||||
}
|
||||
|
||||
offset += 8; /* header already read, accounted in next loop */
|
||||
}
|
||||
return offset; /* total file size walked */
|
||||
}
|
||||
```
|
||||
|
||||
### Identifying Content Types
|
||||
|
||||
To determine what kind of data an IFF file contains, read the **top-level FormType** (the 4 bytes following the `FORM` header):
|
||||
|
||||
| Top-Level Signature | Content Type | Sub-chunks to Expect |
|
||||
|---|---|---|
|
||||
| `FORM .... ILBM` | Interleaved Bitmap image | `BMHD`, `CMAP`, `CAMG`(opt), `BODY`, `CRNG`(opt) |
|
||||
| `FORM .... 8SVX` | 8-bit Sampled Voice (audio) | `VHDR`, `BODY`, `CHAN`(opt), `NAME`(opt) |
|
||||
| `FORM .... ANIM` | Animation (ILBM frames with timing) | `ANHD`, sub-`FORM ILBM` frames, `DLTA`(opt) |
|
||||
| `FORM .... FTXT` | Formatted Text | `CHRS` — character data with style markers |
|
||||
| `FORM .... SMUS` | Simple Musical Score | `SHDR`, `TRAK` — tracker-style music data |
|
||||
| `FORM .... PBM ` | Planar BitMap (non-interleaved ILBM) | Same chunks as ILBM, but BODY is plane-contiguous |
|
||||
| `FORM .... AIFF` | Audio Interchange File Format | `COMM`, `SSND`, `MARK`(opt), `INST`(opt) |
|
||||
| `LIST .... FTXT` | AmigaGuide hypertext document | `PROP ILBM`, nested `FORM FTXT` + `FORM ILBM` |
|
||||
| `CAT .... ILBM` | Concatenated bitmap collection | Multiple `FORM ILBM` sub-chunks (sprite sheet) |
|
||||
|
||||
> [!NOTE]
|
||||
> The spaces after `CAT ` and `PBM ` are intentional — FOURCCs are always exactly 4 characters. `CAT ` = `0x43415420`, `PBM ` = `0x50424D20`.
|
||||
|
||||
**Quick identification in a hex editor**:
|
||||
```
|
||||
Offset 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F ASCII
|
||||
------ ------------------------------------------------- -----
|
||||
$0000 46 4F 52 4D 00 00 7D 18 49 4C 42 4D 42 4D 48 44 FORM..}.ILBMBMHD
|
||||
└─"FORM"──┘ └─size──┘ └─"ILBM"─┘ └─"BMHD"──────┘
|
||||
ck_ID ck_Size FormType first sub-chunk
|
||||
= 32024 bytes total starts here
|
||||
```
|
||||
|
||||
### FTXT — Formatted Text
|
||||
|
||||
Unlike raw ASCII where every character is a single byte of glyph data, FTXT encodes **styled text** with inline formatting commands. The `CHRS` chunk contains a stream of characters interspersed with control codes that change font, color, alignment, and spacing:
|
||||
|
||||
| FTXT Command | Code | Effect |
|
||||
|---|---|---|
|
||||
| `\n` | `$0A` | Line break (same as ASCII LF) |
|
||||
| `\t` | `$09` | Tab (same as ASCII HT) |
|
||||
| **SetFont** | `$80 nn` | Switch to font number `nn` (pre-loaded via `FONS` chunk) |
|
||||
| **SetColor** | `$81 rr gg bb` | Set text color to 24-bit RGB |
|
||||
| **SetJustification** | `$82 mm` | `0`=left, `1`=center, `2`=right, `3`=full justify |
|
||||
| **SetLeftMargin** | `$83 wwww` | Set left margin in pixels (big-endian WORD) |
|
||||
| **SetRightMargin** | `$84 wwww` | Set right margin in pixels |
|
||||
| **Indent** | `$85 wwww` | Indent first line of paragraph |
|
||||
| **SetLeading** | `$86 wwww` | Line spacing in pixels |
|
||||
| **SetKerning** | `$87 nn` | `0`=off, `1`=on |
|
||||
| **PageBreak** | `$8C` | Force new page |
|
||||
| **CenterText** | `$8D` | Center current line |
|
||||
| **SetTabs** | `$8E nn wwww...` | Define `nn` tab stops (list of big-endian WORDs) |
|
||||
|
||||
```c
|
||||
/* A CHRS stream might look like this in memory: */
|
||||
UBYTE chrs[] = {
|
||||
0x82, 0x01, /* SetJustification → center */
|
||||
0x81, 0xFF, 0x00, 0x00, /* SetColor → red */
|
||||
'H', 'e', 'a', 'd', 'i', 'n', 'g', 0x0A,
|
||||
0x81, 0x00, 0x00, 0x00, /* SetColor → black */
|
||||
0x82, 0x00, /* SetJustification → left */
|
||||
'B', 'o', 'd', 'y', ' ', 't', 'e', 'x', 't', '.', 0x0A
|
||||
};
|
||||
```
|
||||
|
||||
**How FTXT differs from raw ASCII:**
|
||||
- Raw text is just bytes → glyphs. FTXT is a **serialized stream of layout instructions** that a renderer executes sequentially.
|
||||
- Font changes are explicit (`SetFont nn`) — you can mix proportional and monospace fonts in the same document.
|
||||
- Color is per-span, not per-character — `SetColor` applies to all subsequent text until the next `SetColor`.
|
||||
- The `FONS` chunk (optional) pre-declares which fonts the document needs, so the renderer can load them before parsing `CHRS`.
|
||||
|
||||
FTXT is the text layer underlying **AmigaGuide** hypertext. An AmigaGuide file (`LIST FTXT+ILBM`) is a collection of FTXT pages (each a `FORM FTXT`) with inline image references (`FORM ILBM`), linked by `@{"linkname"}` cross-reference markers embedded in the character stream.
|
||||
|
||||
### ANIM — Animation Format
|
||||
|
||||
An IFF ANIM file is physically a **sequence of ILBM frames** packaged in a single FORM, with an animation header (`ANHD`) that describes timing and playback parameters:
|
||||
|
||||
```
|
||||
FORM ANIM
|
||||
├── ANHD ← animation header (frame count, timing, mode)
|
||||
├── FORM ILBM ← frame 0 (full image — mandatory keyframe)
|
||||
├── DLTA ← frame 1 delta data (only changed pixels from frame 0)
|
||||
├── DLTA ← frame 2 delta data (only changed pixels from frame 1)
|
||||
├── ...
|
||||
└── FORM ILBM ← optional refresh keyframe every N frames
|
||||
```
|
||||
|
||||
| ANHD Field | Size | Description |
|
||||
|---|---|---|
|
||||
| `ah_Operation` | UBYTE | `0`=standard (DLTA are XOR deltas), `2`=long-delta mode (individual longword patches), `7`=set-delta (replace, not XOR) |
|
||||
| `ah_Mask` | UBYTE | Interleaving mask bits (which planes the delta covers) |
|
||||
| `ah_Width/ah_Height` | UWORD | Frame dimensions |
|
||||
| `ah_Left/ah_Top` | WORD | Frame offset within display |
|
||||
| `ah_AbsTime` | ULONG | Frame display time in **jiffies** (1/60s NTSC, 1/50s PAL) |
|
||||
| `ah_RelTime` | ULONG | Inter-frame delay in jiffies |
|
||||
| `ah_Interleave` | UBYTE | `0`=no interleaving, `1`=interleaved storage |
|
||||
| `ah_Pad0/ah_Pad1` | UBYTE | Padding for alignment |
|
||||
| `ah_Flags` | ULONG | `1`=interleaved, `2`=half-brite mode |
|
||||
|
||||
**How ANIM works physically:**
|
||||
|
||||
1. **Frame 0 is always a full ILBM** — it serves as the reference image and defines the palette (CMAP) for the entire animation.
|
||||
2. **Subsequent frames are DLTA chunks** — they contain only the pixels that *changed* from the previous frame, encoded as (offset, data) pairs. This is essentially a binary diff format at the pixel level.
|
||||
3. **DLTA data is XOR-applied** — the decoder reads the previous frame's pixel data, XORs the delta bytes on top, and produces the new frame. XOR means you can animate both directions (a pixel that was set can be cleared).
|
||||
4. **Refresh frames** (full ILBMs) appear periodically to prevent accumulated delta errors and to allow seeking.
|
||||
|
||||
> [!WARNING]
|
||||
> ANIM frames share a single palette (CMAP from frame 0). If your frames have different palettes, you cannot use IFF ANIM — use a custom multi-ILBM format or ANIM5/ANIM7 extensions (rare).
|
||||
|
||||
**Performance characteristics:** On a stock A500 (68000), ANIM playback can achieve ~6–10 FPS at 320×200×5 bitplanes using standard DLTA mode. Long-delta mode (`ah_Operation=2`) is faster on 68020+ because it copies aligned longwords rather than byte-by-byte XOR. The ANIM format was used by Deluxe Paint's "Anim" feature, SCALA multimedia presentations, and countless Amiga game cutscenes.
|
||||
|
||||
> See also: [animation.md](../08_graphics/animation.md) — the GEL system (BOBs, VSprites, AnimObs) for real-time sprite animation, which is a completely different mechanism from IFF ANIM file playback.
|
||||
|
||||
### 8SVX — 8-bit Sampled Voice
|
||||
|
||||
8SVX is the Amiga's native digital audio format — equivalent to what WAV became on Windows, but simpler and with chunk-based extensibility built in from the start.
|
||||
|
||||
```
|
||||
FORM 8SVX
|
||||
├── VHDR ← voice header (sample rate, volume, compression)
|
||||
├── NAME ← optional sample name string
|
||||
├── CHAN ← optional panning/volume per channel
|
||||
└── BODY ← raw sample data (signed 8-bit PCM, or compressed)
|
||||
```
|
||||
|
||||
| VHDR Field | Size | Description |
|
||||
|---|---|---|
|
||||
| `vh_OneShotHiSamples` | ULONG | Number of samples (big-endian) — hi word of 32-bit count |
|
||||
| `vh_RepeatHiSamples` | ULONG | Repeat offset for looping instruments |
|
||||
| `vh_SamplesPerCycle` | ULONG | Playback rate in **Hz** (e.g., 22050, 11025, 8363) |
|
||||
| `vh_Octaves` | UWORD | Number of frequency octaves (usually 1) |
|
||||
| `vh_Compression` | ULONG | `0`=signed 8-bit PCM, `1`=Fibonacci delta, `2`=Exponential delta |
|
||||
| `vh_Volume` | ULONG | Playback volume (0–65535, linear scale) |
|
||||
|
||||
**8SVX vs WAV analogies:**
|
||||
|
||||
| Aspect | IFF 8SVX (1985) | RIFF WAV (1991) |
|
||||
|---|---|---|
|
||||
| **Container** | IFF FORM (big-endian) | RIFF chunk (little-endian) |
|
||||
| **Sample data chunk** | `BODY` | `data` |
|
||||
| **Header chunk** | `VHDR` (20 bytes, fixed layout) | `fmt ` (variable size, extensible) |
|
||||
| **Sample format** | Signed 8-bit PCM only (in practice) | 8/16/24/32-bit, PCM or float |
|
||||
| **Compression** | Fibonacci delta (lossy), Exponential delta (lossy) | μ-law, ADPCM, MP3 (in theory) |
|
||||
| **Loop points** | `vh_RepeatHiSamples` + `vh_OneShotHiSamples` define a sustain loop | `smpl` chunk with loop points |
|
||||
| **Multi-channel** | One sample per 8SVX; stereo = two 8SVX files | Multi-channel interleaved in single file |
|
||||
| **Legacy** | Died with the Amiga; converted to WAV via SoX/ffmpeg | Still the universal PCM container |
|
||||
|
||||
> [!WARNING]
|
||||
> 8SVX samples are **signed 8-bit** (range -128 to +127). If you read them as unsigned (0–255), every sample shifts by 128 — silence becomes a loud DC offset. This is the #1 cause of "8SVX sounds like static" bug reports.
|
||||
|
||||
**Fibonacci delta compression** (`vh_Compression=1`) encodes the difference between consecutive samples using Fibonacci-encoded values. It achieves ~4:1 compression on typical speech, but is lossy — repeated encode/decode cycles degrade quality. Most modern tools convert 8SVX to WAV on import rather than handling Fibonacci delta natively.
|
||||
|
||||
> See also: [audio.md](../10_devices/audio.md) — audio.device DMA channel programming for real-time sample playback on Paula.
|
||||
|
||||
### Nested Containers in Practice
|
||||
|
||||
An ILBM file is a single FORM, but IFF supports deep nesting for multi-object documents:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph TOP["LIST FTXT+ILBM (hypertext document)"]
|
||||
direction LR
|
||||
P["PROP ILBM<br/>(shared palette, mode)"]
|
||||
F1["FORM FTXT<br/>Heading"]
|
||||
F2["FORM ILBM<br/>Inline image 1"]
|
||||
F3["FORM FTXT<br/>Body text"]
|
||||
F4["FORM ILBM<br/>Inline image 2"]
|
||||
end
|
||||
|
||||
subgraph INSIDE["Inside each FORM ILBM"]
|
||||
direction TB
|
||||
BM["BMHD"] --> CM["CMAP"]
|
||||
CM --> BO["BODY"]
|
||||
end
|
||||
|
||||
TOP -.-> INSIDE
|
||||
|
||||
style P fill:#fff9c4,stroke:#f9a825,color:#333
|
||||
style TOP fill:#e8f4fd,stroke:#2196f3,color:#333
|
||||
```
|
||||
|
||||
| Container | Use Case | iffparse Behavior |
|
||||
|---|---|---|
|
||||
| `FORM ILBM` | Single image | `StopChunk(iff, ID_ILBM, ID_BMHD)` stops at BMHD within this FORM only |
|
||||
| `LIST FTXT+ILBM` | AmigaGuide article with inline images | `ParseIFF` descends into LIST, then into each FORM — context node stack tracks nesting |
|
||||
| `CAT ILBM` | Sprite sheet (unrelated images concatenated) | Same API as LIST; `CurrentChunk()` returns the innermost context |
|
||||
|
||||
iffparse.library maintains an internal **context stack**. Each `PushChunk`/entry into a FORM pushes a new `ContextNode`, and each `PopChunk`/exit restores the parent. The `cn_Type` field tells you whether you're inside a FORM (`IFF_FORMTYPE`), LIST (`IFF_LISTTYPE`), or CAT (`IFF_CATTYPE`).
|
||||
|
||||
### IFF Design Philosophy
|
||||
|
||||
IFF was not designed as an image format — it was designed as a **universal data interchange container**. Jerry Morrison of Electronic Arts authored the "EA IFF 85" specification (January 14, 1985) with a single goal: files that could move between applications, operating systems, and even different computers without losing structure. Every IFF file self-describes its contents via FOURCC chunk IDs (4-character ASCII codes like `BMHD`, `CMAP`, `BODY`). An application that encounters an unknown chunk simply **skips it** by reading its size and advancing the file pointer — no error, no corruption, just graceful ignorance.
|
||||
|
||||
This "skip what you don't understand" contract is the single most important design decision in IFF. It means a file created by Deluxe Paint in 1986 with a custom `DPAN` chunk (DPaint animation settings) can be opened by a modern image viewer — the viewer skips `DPAN` and renders the image from `BMHD` + `CMAP` + `BODY`. This is exactly how DataTypes descriptors work a decade later, and it's the same philosophy behind PNG's ancillary chunks and XML namespaces.
|
||||
|
||||
### IFF vs Contemporary Formats (1984–1990)
|
||||
|
||||
| Format | Year | Platform | Type | Extensible? | Compression | Palette | Multi-Image | Byte Order |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| **IFF ILBM** | 1985 | Amiga | Nested container | ✅ Yes — unknown chunks skipped safely | ByteRun1 RLE (optional) | 1–256 colors (CMAP) | ✅ Yes (LIST, CAT) | Big-Endian |
|
||||
| **MacPaint** | 1984 | Macintosh | Fixed bitmap | ❌ No — 576×720 only, no header version | None | 1-bit only | ❌ No | Big-Endian |
|
||||
| **PCX** | 1984 | PC DOS (ZSoft) | Single image | ❌ No — fixed header layout | RLE (mandatory) | 1–256 colors (EGA/VGA) | ❌ No | Little-Endian |
|
||||
| **BMP (DIB)** | 1985 | Windows 1.0 | Single image | ⚠️ Partial — later versions added header fields but no chunk skip | RLE (rarely used) | 1–256 colors (palette) | ❌ No | Little-Endian |
|
||||
| **GIF 87a** | 1987 | CompuServe | Multi-image (non-animated) | ⚠️ Partial — extension blocks in GIF89a | LZW (mandatory) | 1–256 colors (global + local palette) | ✅ Yes (multiple images) | Little-Endian |
|
||||
| **TIFF** | 1986 | Aldus (Mac/PC) | Tag-based container | ✅ Yes — IFD tags with skip rule | Multiple codecs (RLE, LZW, CCITT, JPEG later) | 1–24-bit (tag-defined) | ✅ Yes (multiple IFDs) | Both (magic: `II`/`MM`) |
|
||||
| **IMG (GEM)** | 1985 | Atari ST (Digital Research) | Single image | ❌ No — rigid header | None (later versions: PackBits) | 1–16 colors | ❌ No | Big-Endian |
|
||||
|
||||
**Key takeaways from the era:**
|
||||
|
||||
1. **IFF was the only format that was a true container from day one.** BMP, PCX, and MacPaint were rigid: one file = one image with a fixed header. IFF could hold multiple FORMs in a LIST, nest metadata inside PROP chunks, and intermix text (FTXT) with images (ILBM) in the same file — essential for AmigaGuide hypertext.
|
||||
|
||||
2. **IFF and TIFF were the only extensible formats.** TIFF's tag-based approach (IFD entries with numeric tags) was more flexible than IFF's FOURCC chunks, but also more complex — a TIFF reader must parse the IFD chain before it can skip unknown tags. An IFF reader skips unknown chunks with zero metadata lookup. Both, however, shared the same core insight: **a format that can't grow is already dead.**
|
||||
|
||||
3. **PCX and GIF owned the PC world for different reasons.** PCX was dead-simple — a 128-byte header followed by RLE data — and every DOS paint program supported it. GIF owned online services (CompuServe) and the early web because LZW compression produced smaller files than RLE on synthetic graphics, and the 87a spec included multi-image support before any other format except IFF.
|
||||
|
||||
4. **BMP won by platform monopoly, not technical merit.** BMP (Device Independent Bitmap) was Windows' native image format. It had no compression in practice (RLE was specified but almost never used), no extensibility, and no multi-image support. It survived because every Windows application was forced to support it. IFF was technically superior in every dimension — but Microsoft's platform dominance made BMP the default, just as it made WAV (RIFF) the default over IFF 8SVX.
|
||||
|
||||
5. **Byte order was a religious war.** IFF and MacPaint used big-endian (Motorola 68000 native). PCX, BMP, and GIF used little-endian (Intel x86 native). TIFF supported both with a magic-number flag. IFF's big-endian choice was natural for the Amiga but created an eternal annoyance for cross-platform tools — reading an IFF file on a PC required byte-swapping every LONG and WORD.
|
||||
|
||||
---
|
||||
|
||||
## Reading an IFF File
|
||||
|
|
@ -102,8 +440,8 @@ while ((error = ParseIFF(iff, IFFPARSE_SCAN)) == 0)
|
|||
{
|
||||
UBYTE palette[256 * 3];
|
||||
LONG palSize = ReadChunkBytes(iff, palette, cn->cn_Size);
|
||||
LONG numColours = palSize / 3;
|
||||
Printf("Palette: %ld colours\n", numColours);
|
||||
LONG numColors = palSize / 3;
|
||||
Printf("Palette: %ld colors\n", numColors);
|
||||
break;
|
||||
}
|
||||
case ID_CAMG:
|
||||
|
|
@ -149,8 +487,8 @@ WriteChunkBytes(iff, &bmhd, sizeof(bmhd));
|
|||
PopChunk(iff);
|
||||
|
||||
/* Write CMAP chunk: */
|
||||
PushChunk(iff, 0, ID_CMAP, numColours * 3);
|
||||
WriteChunkBytes(iff, palette, numColours * 3);
|
||||
PushChunk(iff, 0, ID_CMAP, numColors * 3);
|
||||
WriteChunkBytes(iff, palette, numColors * 3);
|
||||
PopChunk(iff);
|
||||
|
||||
/* Write BODY chunk: */
|
||||
|
|
@ -180,7 +518,7 @@ struct BitMapHeader {
|
|||
UBYTE bmh_Masking; /* 0=none, 1=hasMask, 2=hasTransparentColor */
|
||||
UBYTE bmh_Compression; /* 0=none, 1=ByteRun1 */
|
||||
UBYTE bmh_Pad;
|
||||
UWORD bmh_Transparent; /* transparent colour index */
|
||||
UWORD bmh_Transparent; /* transparent color index */
|
||||
UBYTE bmh_XAspect; /* pixel aspect ratio */
|
||||
UBYTE bmh_YAspect;
|
||||
WORD bmh_PageWidth; /* source page width */
|
||||
|
|
@ -228,15 +566,67 @@ void DecompressByteRun1(UBYTE *src, UBYTE *dst, LONG dstSize)
|
|||
|
||||
---
|
||||
|
||||
## Common Chunk IDs
|
||||
## Reading Planar Bitmap Data from BODY
|
||||
|
||||
After decompressing the BODY chunk, you have **interleaved bitplanes** — row 0 of plane 0, row 0 of plane 1, ..., row 0 of plane n, then row 1 of plane 0, ... This is the raw ILBM layout. To build a usable `BitMap` structure, you must deinterleave and convert to Amiga bitplane order.
|
||||
|
||||
### Deinterleaving the BODY
|
||||
|
||||
```c
|
||||
/* Given a decompressed BODY buffer and a BMHD header,
|
||||
* fill a struct BitMap with per-plane pointers: */
|
||||
struct BitMap *BuildBitMap(UBYTE *bodyData, struct BitMapHeader *bmhd)
|
||||
{
|
||||
struct BitMap *bm = AllocMem(sizeof(struct BitMap), MEMF_CLEAR);
|
||||
ULONG widthBytes = (bmhd->bmh_Width + 15) / 16 * 2; /* word-aligned row width */
|
||||
ULONG planeSize = widthBytes * bmhd->bmh_Height;
|
||||
UBYTE depth = bmhd->bmh_Depth;
|
||||
|
||||
InitBitMap(bm, depth, widthBytes * 8, bmhd->bmh_Height);
|
||||
|
||||
/* Allocate each plane separately (Chip RAM if DMA needed): */
|
||||
for (UBYTE p = 0; p < depth; p++)
|
||||
{
|
||||
bm->Planes[p] = AllocMem(planeSize, MEMF_CHIP | MEMF_CLEAR);
|
||||
if (!bm->Planes[p]) { /* OOM — free prior planes */ }
|
||||
}
|
||||
|
||||
/* Deinterleave: ILBM stores row-interleaved, Amiga wants per-plane contiguous */
|
||||
for (ULONG y = 0; y < bmhd->bmh_Height; y++)
|
||||
{
|
||||
for (UBYTE p = 0; p < depth; p++)
|
||||
{
|
||||
/* Source: p-th plane of this row in interleaved BODY */
|
||||
UBYTE *src = bodyData + (y * depth + p) * widthBytes;
|
||||
/* Dest: row y of plane p */
|
||||
UBYTE *dst = bm->Planes[p] + y * widthBytes;
|
||||
memcpy(dst, src, widthBytes);
|
||||
}
|
||||
}
|
||||
|
||||
return bm;
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> The ILBM BODY interleaves by **(row × depth)**, not by depth-first. If you get the deinterleaving wrong, you'll see "striped" images where each group of depth rows came from the wrong plane.
|
||||
|
||||
### PBM (Planar BitMap) vs ILBM
|
||||
|
||||
| Aspect | ILBM (Interleaved) | PBM (Planar) |
|
||||
|---|---|---|
|
||||
| **Channel order** | Row 0 → all planes → Row 1 → all planes... | Plane 0 → all rows → Plane 1 → all rows... |
|
||||
| **Faster for...** | Rendering one row at a time (line-by-line drawing) | Blitter copy (`BltBitMap`) — each plane is contiguous |
|
||||
| **BODY layout** | Default ILBMalen format | `BODY` chunk identical, but "PBM " FORM type signals planar intent |
|
||||
| **iffparse support** | Standard `ID_ILBM` + `ID_BMHD` + `ID_BODY` | Same parsing; `ID_PBM` chunk ID (`0x50424D20`) if present |
|
||||
|
||||
| FORM Type | Chunk | Size | Description |
|
||||
|---|---|---|---|
|
||||
| `ILBM` | `BMHD` | 20 | Bitmap header (width, height, depth, compression) |
|
||||
| `ILBM` | `CMAP` | n×3 | Colour map (R,G,B triples, 8-bit each) |
|
||||
| `ILBM` | `CMAP` | n×3 | Color map (R,G,B triples, 8-bit each) |
|
||||
| `ILBM` | `CAMG` | 4 | Amiga display mode (ModeID for ViewPort) |
|
||||
| `ILBM` | `BODY` | varies | Pixel data (interleaved bitplanes) |
|
||||
| `ILBM` | `CRNG` | 8 | Colour cycling range (DPaint) |
|
||||
| `ILBM` | `CRNG` | 8 | Color cycling range (DPaint) |
|
||||
| `ILBM` | `GRAB` | 4 | Hotspot (cursor/brush grab point) |
|
||||
| `8SVX` | `VHDR` | 20 | Voice header (rate, volume, octaves) |
|
||||
| `8SVX` | `BODY` | varies | Audio sample data (signed 8-bit) |
|
||||
|
|
@ -263,9 +653,379 @@ FreeIFF(iff);
|
|||
|
||||
---
|
||||
|
||||
## When to Use IFFParse vs DataTypes
|
||||
|
||||
Both `iffparse.library` and the [DataTypes](datatypes.md) system can load IFF ILBM images, but they serve different needs:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
START["Need to read an IFF file?"]
|
||||
|
||||
Q1{"Do you know the exact<br/>internal structure?"}
|
||||
Q2{"Need clipboard/<br/>drag-and-drop?"}
|
||||
Q3{"Loading non-IFF<br/>formats (PNG, GIF)?"}
|
||||
Q4{"Memory budget<br/>under 64 KB?"}
|
||||
|
||||
IFF["✅ iffparse.library<br/>Fine-grained control<br/>Lower overhead"]
|
||||
DT["✅ datatypes.library<br/>Format-agnostic<br/>Auto-detection"]
|
||||
|
||||
START --> Q1
|
||||
Q1 -->|Yes| Q4
|
||||
Q1 -->|No| DT
|
||||
Q4 -->|Yes| IFF
|
||||
Q4 -->|No| Q2
|
||||
Q2 -->|Yes| DT
|
||||
Q2 -->|No| Q3
|
||||
Q3 -->|Yes| DT
|
||||
Q3 -->|No| IFF
|
||||
|
||||
style IFF fill:#e8f5e9,stroke:#4caf50,color:#333
|
||||
style DT fill:#e8f4fd,stroke:#2196f3,color:#333
|
||||
```
|
||||
|
||||
| Criterion | iffparse.library | datatypes.library |
|
||||
|---|---|---|
|
||||
| **Parsing granularity** | Per-chunk — you decide what to read and when | Per-object — library reads everything |
|
||||
| **Format support** | Any IFF file (ILBM, 8SVX, ANIM, FTXT, custom) | IFF + PNG, GIF, JPEG, WAV, AIFF (via subclass) |
|
||||
| **Stream source** | DOS file, clipboard, memory (custom hook) | DOS file, clipboard |
|
||||
| **Writing** | Full control via `PushChunk`/`PopChunk` | Not supported — DataTypes is read-only |
|
||||
| **Memory overhead** | ~2 KB for IFFHandle + context stack | ~20–50 KB for BOOPSI object + decoded bitmap |
|
||||
| **API complexity** | 10 functions, manual state management | 3 functions, Booleans for auto-everything |
|
||||
| **Best for** | IFF-specific tools, crunchers, conversion utilities | Application file loading, thumbnails, format-agnostic viewers |
|
||||
|
||||
> [!NOTE]
|
||||
> If you need to **write** files in any format, use iffparse.library. DataTypes cannot serialize objects back to disk — it's strictly a reader.
|
||||
|
||||
---
|
||||
|
||||
## Pitfalls & Common Mistakes
|
||||
|
||||
### 1. Forgetting the Odd-Byte Padding
|
||||
|
||||
Every chunk's data must be padded to an even byte boundary. When writing, iffparse handles this automatically. When reading raw bytes outside iffparse (manual parsing), you must account for the pad byte yourself:
|
||||
|
||||
```c
|
||||
/* BAD: Hard-coding chunk position without pad accounting */
|
||||
LONG chunkSize = ReadLong(fh); /* e.g., 21 bytes */
|
||||
Read(fh, buffer, chunkSize); /* reads 21 bytes */
|
||||
LONG nextChunkID = ReadLong(fh); /* WRONG: if 21 is odd, this reads the pad byte as ID! */
|
||||
|
||||
/* CORRECT: Skip pad byte if chunk size is odd */
|
||||
LONG chunkSize = ReadLong(fh);
|
||||
Read(fh, buffer, chunkSize);
|
||||
if (chunkSize & 1) Read(fh, &pad, 1); /* consume pad byte */
|
||||
LONG nextChunkID = ReadLong(fh); /* now at correct position */
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> iffparse.library's `ReadChunkBytes` handles padding internally, but only if you use the library's own `InitIFFasDOS` hook. If you provide custom stream hooks, you are responsible for pad-byte alignment.
|
||||
|
||||
### 2. Reading Chunk Size as Little-Endian
|
||||
|
||||
The 68000 is **big-endian**. IFF sizes are also big-endian (network byte order). If you cast chunk bytes to a LONG without byte-swapping on a little-endian system (cross-compiled test harness, PC-hosted IFF validator), you'll read garbage:
|
||||
|
||||
```c
|
||||
/* DANGEROUS on x86 test rigs: */
|
||||
LONG size = *(LONG *)chunkHeader; /* bytes are 00 00 00 14 → x86 reads 0x14000000! */
|
||||
|
||||
/* SAFE: always use explicit big-endian parse */
|
||||
LONG size = (chunk[0] << 24) | (chunk[1] << 16) | (chunk[2] << 8) | chunk[3];
|
||||
```
|
||||
|
||||
### 3. Missing StopChunk Registration
|
||||
|
||||
If you don't call `StopChunk()` for a chunk type, `ParseIFF(IFFPARSE_SCAN)` skips right over it. This is by design, but it's a common surprise:
|
||||
|
||||
```c
|
||||
/* Only BMHD and BODY are registered — CMAP is silently skipped! */
|
||||
StopChunk(iff, ID_ILBM, ID_BMHD);
|
||||
StopChunk(iff, ID_ILBM, ID_BODY);
|
||||
/* Missing: StopChunk(iff, ID_ILBM, ID_CMAP); */
|
||||
|
||||
while (ParseIFF(iff, IFFPARSE_SCAN) == 0) {
|
||||
/* Will see BMHD → BODY, never CMAP */
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Closing the DOS Filehandle Before CloseIFF
|
||||
|
||||
`CloseIFF` may need to seek or read trailer data. If you close the underlying file handle first, `CloseIFF` operates on a stale/garbage `iff_Stream`:
|
||||
|
||||
```c
|
||||
/* BAD: Stream closed before library */
|
||||
Close((BPTR)iff->iff_Stream); /* gone! */
|
||||
CloseIFF(iff); /* operates on freed handle → undefined behavior */
|
||||
|
||||
/* CORRECT: Library first, then stream */
|
||||
CloseIFF(iff);
|
||||
Close((BPTR)iff->iff_Stream);
|
||||
FreeIFF(iff);
|
||||
```
|
||||
|
||||
### 5. EHB / Extra-HalfBrite Pitfall
|
||||
|
||||
EHB mode uses 64 colors where the upper 32 are half-brightness versions of the lower 32. If `CAMG` indicates EHB (`$80`), the CMAP chunk still has only 64 entries — but `bmh_Depth` is 6, not 5. Relying on `bmh_Depth` to calculate palette size will give wrong results:
|
||||
|
||||
```c
|
||||
/* BAD: bmh_Depth==6 for EHB suggests 2^6=64 colors, but only 64 are stored */
|
||||
LONG expectedColors = 1 << bmh_Depth; /* 64 — correct by coincidence */
|
||||
|
||||
/* CORRECT: CMAP chunk size determines real color count */
|
||||
LONG numColors = cn->cn_Size / 3; /* always accurate */
|
||||
|
||||
/* Extra check: EHB flag in CAMG */
|
||||
if (viewMode & EXTRA_HALFBRITE)
|
||||
Printf("EHB mode: 32 base colors + 32 half-bright variants\n");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Named Antipatterns
|
||||
|
||||
### 6. "The Blind Write": Forgetting IFFSIZE_UNKNOWN
|
||||
|
||||
When writing an IFF file where the body size isn't known until after encoding, you must use `IFFSIZE_UNKNOWN`. Pushing with a hard-coded size that turns out wrong produces a corrupt file:
|
||||
|
||||
```c
|
||||
/* BAD: Guessing size before encoding */
|
||||
PushChunk(iff, 0, ID_BODY, 32000); /* hope this is enough */
|
||||
WriteChunkBytes(iff, data, actualSize); /* actualSize might be 48000! */
|
||||
/* iffparse writes only min(32000, actualSize) — data loss */
|
||||
|
||||
/* CORRECT: Let iffparse handle unknown sizes */
|
||||
PushChunk(iff, 0, ID_BODY, IFFSIZE_UNKNOWN);
|
||||
WriteChunkBytes(iff, data, actualSize);
|
||||
PopChunk(iff); /* iffparse seeks back and patches the correct size */
|
||||
```
|
||||
|
||||
### 7. "The Leaky Parser": Skipping FreeIFF
|
||||
|
||||
`AllocIFF` allocates an `IFFHandle` plus internal buffers. Forgetting to call `FreeIFF` leaks memory even if the file was closed. This is particularly insidious in image batch converters:
|
||||
|
||||
```c
|
||||
/* BAD: Loop that leaks an IFFHandle per iteration */
|
||||
for (int i = 0; i < numFiles; i++) {
|
||||
struct IFFHandle *iff = AllocIFF();
|
||||
/* ... parse one file ... */
|
||||
CloseIFF(iff);
|
||||
Close((BPTR)iff->iff_Stream);
|
||||
/* Missing FreeIFF(iff) — leaks ~2 KB per file! */
|
||||
}
|
||||
|
||||
/* CORRECT: Always free the handle */
|
||||
for (int i = 0; i < numFiles; i++) {
|
||||
struct IFFHandle *iff = AllocIFF();
|
||||
/* ... parse ... */
|
||||
CloseIFF(iff);
|
||||
Close((BPTR)iff->iff_Stream);
|
||||
FreeIFF(iff); /* return handle to system */
|
||||
}
|
||||
```
|
||||
|
||||
### 8. "The Chunk Swallower": Reading More Than cn_Size
|
||||
|
||||
`ReadChunkBytes` returns the actual number of bytes read, which may be less than requested if you asked for more than the chunk contains. The `ContextNode->cn_Size` is authoritative:
|
||||
|
||||
```c
|
||||
/* BAD: Blind ReadChunkBytes for fixed-size struct without checking */
|
||||
struct BitMapHeader bmhd;
|
||||
ReadChunkBytes(iff, &bmhd, sizeof(bmhd)); /* what if chunk is smaller? */
|
||||
|
||||
/* CORRECT: Respect cn_Size */
|
||||
struct BitMapHeader bmhd;
|
||||
LONG toRead = min(cn->cn_Size, sizeof(bmhd));
|
||||
LONG actuallyRead = ReadChunkBytes(iff, &bmhd, toRead);
|
||||
if (actuallyRead < toRead)
|
||||
Printf("Warning: truncated BMHD chunk\n");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: Can I parse two IFF files concurrently?**
|
||||
|
||||
Yes. Each `AllocIFF()` returns an independent `IFFHandle`. You can interleave `ParseIFF` calls on two handles without interference — useful for comparing or merging IFF files.
|
||||
|
||||
**Q: What happens if I don't register StopChunk for BODY?**
|
||||
|
||||
`ParseIFF` skips the pixel data entirely and proceeds to the next chunk (or end of FORM). This is actually useful for extracting only metadata (BMHD, CMAP) from large images without allocating memory for the BODY.
|
||||
|
||||
**Q: Can I use iffparse to validate an IFF file structure?**
|
||||
|
||||
Yes. Run `ParseIFF(iff, IFFPARSE_SCAN)` until it returns `IFFERR_EOC` (end of context). If it returns any other error before that, the file is malformed. Use `IFFPARSE_STEP` instead of `IFFPARSE_SCAN` to walk one level at a time for detailed error reporting.
|
||||
|
||||
**Q: Does iffparse handle AmigaGuide files?**
|
||||
|
||||
Yes — AmigaGuide `.guide` files are IFF `FORM FTXT+ILBM` under the hood. `iffparse.library` can parse them at the chunk level, but [amigaguide.library](amigaguide.md) provides a higher-level API for displaying and navigating AmigaGuide documents.
|
||||
|
||||
**Q: Why does my 8SVX file sound like static after parsing with iffparse?**
|
||||
|
||||
8SVX samples are **signed 8-bit** PCM. If you treat them as unsigned (common on PC sound APIs), the zero-crossing shifts polarity. The `VHDR` chunk's `vh_Compression` field tells you the encoding: 0 = signed 8-bit PCM, 1 = Fibonacci delta, 2 = Exponential delta.
|
||||
|
||||
---
|
||||
|
||||
## Use-Case Cookbook
|
||||
|
||||
### 1. Metadata Extractor — Read Only BMHD and CMAP
|
||||
|
||||
For thumbnail generation or file cataloging, skip the BODY entirely:
|
||||
|
||||
```c
|
||||
struct IFFHandle *iff = AllocIFF();
|
||||
iff->iff_Stream = (ULONG)Open(file, MODE_OLDFILE);
|
||||
InitIFFasDOS(iff);
|
||||
OpenIFF(iff, IFFF_READ);
|
||||
|
||||
/* Register only metadata chunks — BODY is skipped automatically */
|
||||
StopChunk(iff, ID_ILBM, ID_BMHD);
|
||||
StopChunk(iff, ID_ILBM, ID_CMAP);
|
||||
|
||||
LONG error;
|
||||
while ((error = ParseIFF(iff, IFFPARSE_SCAN)) == 0)
|
||||
{
|
||||
struct ContextNode *cn = CurrentChunk(iff);
|
||||
if (cn->cn_ID == ID_BMHD) {
|
||||
ReadChunkBytes(iff, &meta.bmhd, sizeof(meta.bmhd));
|
||||
} else if (cn->cn_ID == ID_CMAP) {
|
||||
meta.palSize = ReadChunkBytes(iff, meta.palette, cn->cn_Size);
|
||||
}
|
||||
}
|
||||
|
||||
CloseIFF(iff); Close((BPTR)iff->iff_Stream); FreeIFF(iff);
|
||||
```
|
||||
|
||||
### 2. Batch IFF-to-Raw Converter
|
||||
|
||||
```c
|
||||
void ConvertILBMtoRaw(STRPTR input, STRPTR output)
|
||||
{
|
||||
struct IFFHandle *iff = AllocIFF();
|
||||
iff->iff_Stream = (ULONG)Open(input, MODE_OLDFILE);
|
||||
InitIFFasDOS(iff); OpenIFF(iff, IFFF_READ);
|
||||
|
||||
StopChunk(iff, ID_ILBM, ID_BMHD);
|
||||
StopChunk(iff, ID_ILBM, ID_BODY);
|
||||
|
||||
struct BitMapHeader bmhd;
|
||||
UBYTE *bodyData = NULL;
|
||||
|
||||
while (ParseIFF(iff, IFFPARSE_SCAN) == 0)
|
||||
{
|
||||
struct ContextNode *cn = CurrentChunk(iff);
|
||||
if (cn->cn_ID == ID_BMHD)
|
||||
ReadChunkBytes(iff, &bmhd, sizeof(bmhd));
|
||||
else if (cn->cn_ID == ID_BODY)
|
||||
{
|
||||
bodyData = AllocMem(cn->cn_Size, MEMF_ANY);
|
||||
ReadChunkBytes(iff, bodyData, cn->cn_Size);
|
||||
|
||||
/* Decompress if needed */
|
||||
if (bmhd.bmh_Compression == 1)
|
||||
{
|
||||
ULONG rawSize = ((bmhd.bmh_Width + 15) / 16 * 2)
|
||||
* bmhd.bmh_Height * bmhd.bmh_Depth;
|
||||
UBYTE *raw = AllocMem(rawSize, MEMF_ANY);
|
||||
DecompressByteRun1(bodyData, raw, rawSize);
|
||||
FreeMem(bodyData, cn->cn_Size);
|
||||
bodyData = raw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Write raw planar data to output file */
|
||||
if (bodyData)
|
||||
{
|
||||
BPTR fh = Open(output, MODE_NEWFILE);
|
||||
ULONG bodySize = ((bmhd.bmh_Width + 15) / 16 * 2)
|
||||
* bmhd.bmh_Height * bmhd.bmh_Depth;
|
||||
Write(fh, bodyData, bodySize);
|
||||
Close(fh);
|
||||
FreeMem(bodyData, bodySize);
|
||||
}
|
||||
|
||||
CloseIFF(iff); Close((BPTR)iff->iff_Stream); FreeIFF(iff);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Building a Composite IFF (Sprite Sheet from Multiple ILBMs)
|
||||
|
||||
```c
|
||||
/* Create a CAT ILBM containing multiple sub-images: */
|
||||
struct IFFHandle *iff = AllocIFF();
|
||||
iff->iff_Stream = (ULONG)Open("sprites.iff", MODE_NEWFILE);
|
||||
InitIFFasDOS(iff);
|
||||
OpenIFF(iff, IFFF_WRITE);
|
||||
|
||||
PushChunk(iff, ID_ILBM, ID_CAT, IFFSIZE_UNKNOWN);
|
||||
|
||||
for (int i = 0; i < numFrames; i++)
|
||||
{
|
||||
/* Each frame is a FORM ILBM */
|
||||
PushChunk(iff, ID_ILBM, ID_FORM, IFFSIZE_UNKNOWN);
|
||||
/* Write BMHD, CMAP, BODY for this frame... */
|
||||
PopChunk(iff); /* close this FORM */
|
||||
}
|
||||
|
||||
PopChunk(iff); /* close the CAT */
|
||||
CloseIFF(iff); Close((BPTR)iff->iff_Stream); FreeIFF(iff);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## IFF as a Container — Comparison Across Eras
|
||||
|
||||
IFF was not just another image format — it was a **general-purpose binary container** for any kind of structured data. The FOURCC + size + data + pad pattern is a design template that recurred in dozens of later formats. This section compares IFF against both historical contemporaries and modern containers on container-design dimensions.
|
||||
|
||||
### Container Feature Matrix
|
||||
|
||||
| Dimension | IFF (1985) | RIFF (1991) | TIFF (1986) | PNG (1996) | ISOBMFF / MP4 (2001) | Matroska / EBML (2002) | ZIP (1989) |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| **Identification** | `FORM`/`LIST`/`CAT ` FOURCC at file start | `RIFF`/`RIFX` + form-type FOURCC | `II` or `MM` magic + offset to first IFD | `\x89PNG\r\n\x1a\n` 8-byte signature | `ftyp` box with brand code | `\x1a\x45\xdf\xa3` EBML header + DocType | `PK\x03\x04` local file header |
|
||||
| **Chunk ID scheme** | 4-char ASCII FOURCC (`BMHD`, `CMAP`) | 4-char ASCII FOURCC (`fmt `, `data`) | 16-bit numeric tag IDs (256, 257...) | 4-char ASCII (`IHDR`, `IDAT`, `IEND`) | 4-char ASCII (`moov`, `mdat`, `trak`) | Variable-length integer Element ID | Filename string (directory-based) |
|
||||
| **Unknown-chunk skip** | ✅ Safe — reader skips by `size` field | ✅ Same as IFF | ⚠️ IFD chain must be parsed first | ✅ Same as IFF — fundamental design goal | ✅ Box size in header enables skip | ✅ EBML variable-length size | ❌ Not applicable — files are opaque entries |
|
||||
| **Nesting depth** | Arbitrary (FORM in LIST in FORM) | Same as IFF | 2 levels (IFD → sub-IFD) | Single-level (chunks within file only) | Arbitrary (boxes contain boxes) | Arbitrary (elements contain elements) | Flat (file list, no nesting) |
|
||||
| **Size-at-write-unknown** | ✅ `IFFSIZE_UNKNOWN` + seek-and-patch | ✅ Same mechanism | ❌ Must know sizes before writing IFD offsets | ❌ `IDAT` size must be determined at write time | ✅ `mdat` can be large + `moov` offsets patched | ✅ EBML Unknown-Size marker | ✅ Central directory written after file data |
|
||||
| **Multi-stream** | ✅ Multiple FORMs in LIST/CAT | ✅ Multiple LIST chunks | ✅ Multiple IFDs (multi-page TIFF) | ❌ Single image only (APNG extends later) | ✅ Multiple tracks (video + audio + subtitles) | ✅ Multiple tracks + attachments + chapters | ✅ Multiple files in archive |
|
||||
| **Endianness** | Big-Endian (Motorola) | Little-Endian (RIFF) or Big (RIFX) | Both (`II`=LE, `MM`=BE) | Big-Endian (network byte order) | Big-Endian (network byte order) | Big-Endian | Not applicable (per-file) |
|
||||
| **Compression model** | Per-chunk optional (BODY may be ByteRun1, others raw) | Per-chunk optional (codec in format chunk) | Per-image codec tag | Mandatory: all IDAT is zlib-deflated | Per-track codec | Per-track codec | Per-file (deflate, store, bzip2...) |
|
||||
| **Extensibility mindset** | Add new chunks — old readers skip them | Add new chunks — old readers skip them | Add new tags — old readers skip unknown | Add ancillary chunks — critical chunks can't be skipped | Add new boxes — old readers skip unknown | Add new elements — old readers skip unknown | Add new files to archive |
|
||||
| **Primary use case** | Any structured data (images, audio, text, hypertext) | Multimedia (WAV audio, AVI video, ANI cursors) | Document imaging, DTP, scanning | Lossless compressed images | MP4 video, M4A audio, 3GP mobile | MKV/MKA/MKS — video, audio, subtitles | Generic file bundling |
|
||||
|
||||
### What Each Container Got Right (and Wrong)
|
||||
|
||||
**IFF (1985)** — The original template. Its single greatest contribution was the **skip-unknown-chunk contract**: a reader that encounters an unrecognized FOURCC simply reads the 4-byte size and advances past the data. This means any IFF file created in 1986 can be opened by any IFF reader written in 2026, regardless of what chunks were added in between. The limitation was that IFF required chunks to be strictly nested (no overlapping), which turned out to be too rigid for some streaming scenarios.
|
||||
|
||||
**RIFF (1991)** — Microsoft and IBM essentially copied IFF and flipped the byte order to little-endian. This was a pragmatic decision (x86 native), but it broke interoperability with IFF files. RIFF added the `RIFX` variant for big-endian, but almost nothing used it. RIFF's dominance came from WAV (1991) and AVI (1992) — formats that still underpin audio and video on Windows. The RIFF container is a direct IFF descendant wearing an Intel shirt.
|
||||
|
||||
**TIFF (1986)** — Aldus took a different approach: instead of FOURCC chunk IDs, TIFF uses numeric tags in an Image File Directory (IFD). This is more compact and machine-friendly, but harder to inspect with a hex editor. TIFF's key innovation was **tag typing** (BYTE, ASCII, SHORT, LONG, RATIONAL) — a type system for metadata that IFF lacked. The downside: a TIFF reader must parse the IFD chain to locate image data, while an IFF reader can scan linearly. TIFF won desktop publishing; IFF won multimedia interchange.
|
||||
|
||||
**PNG (1996)** — PNG is IFF's most successful spiritual descendant. The 4-character chunk IDs (`IHDR`, `IDAT`, `IEND`, `tEXt`, `zTXt`), the skip-unknown-chunk rule, and the big-endian byte order are all direct IFF heritage. PNG added two innovations: **critical vs ancillary chunks** (unknown ancillary chunks are skipped; unknown critical chunks cause error), and a **mandatory compression layer** (zlib). The critical/ancillary distinction solved IFF's unresolved question: what if a chunk is essential to rendering? In IFF, the answer was always "skip it anyway" — in PNG, you can declare that a chunk must be understood or the file is invalid.
|
||||
|
||||
**ISOBMFF / MP4 (2001)** — The ISO Base Media File Format (used by MP4, 3GP, MOV) is a box-structured container that descends conceptually from IFF. Each box has a 4-byte size, 4-char type, and optional 8-byte extended size — nearly identical to IFF chunks. The key difference: ISOBMFF boxes can reference each other via file offsets (e.g., `moov` box has a table pointing into `mdat`), creating a **non-linear structure** that IFF cannot express. This enables streaming (metadata at the end is still usable) but requires a two-pass read for some operations.
|
||||
|
||||
**Matroska / EBML (2002)** — Matroska uses Extensible Binary Meta Language (EBML), which generalizes IFF's chunk concept to **variable-length element IDs and sizes**. A 1-byte element ID means "this is a common field" and a 4-byte ID means "this is a rare extension". This variable-length encoding is more space-efficient than IFF's fixed 4-byte FOURCCs, but it requires a schema (the `DocType`) to interpret element meaning — you can't just read the bytes and know what you're looking at. IFF's FOURCCs are self-documenting (`CMAP` in a hex dump is obviously "color map"); EBML's numeric IDs are not.
|
||||
|
||||
**ZIP (1989)** — A different paradigm: ZIP is a **directory-indexed archive**, not a structured container. Files are stored sequentially with local headers, and a central directory at the end maps filenames to offsets. This is the same "size unknown at write time" problem solved differently: IFF seeks back and patches the chunk size; ZIP appends a separate index. For bundling unrelated files, ZIP wins (one file = many independent entries). For structured data with internal relationships, IFF wins (one file = one logical object with nested parts).
|
||||
|
||||
### IFF's Container Design — Pros and Cons
|
||||
|
||||
| ✅ Advantage | ❌ Limitation |
|
||||
|---|---|
|
||||
| **Self-describing**: FOURCCs are human-readable ASCII — `BMHD` is obviously "bitmap header" in a hex dump | **Fixed 4-byte IDs**: wastes space on common chunks that could use 1-byte IDs (EBML solved this) |
|
||||
| **Linear scannable**: reader can walk file start-to-end without back-references | **No random access**: cannot jump directly to a specific chunk without scanning from the start (TIFF IFD offsets solved this) |
|
||||
| **Pad-byte rule**: simple and deterministic — always align to word boundary | **No compression at container level**: compression is per-chunk, not per-file (ZIP/zlib solved this) |
|
||||
| **Unknown-chunk skip**: forward-compatible forever — the defining feature | **No critical vs ancillary distinction**: IFF cannot declare a chunk as mandatory (PNG solved this) |
|
||||
| **Write-friendly**: `IFFSIZE_UNKNOWN` enables streaming writes with retroactive size patching | **Seek required**: `PopChunk` must seek back to patch the size — breaks on non-seekable streams |
|
||||
| **Rich nesting**: LIST/PROP/CAT cover structured documents, sprite sheets, and hypertext | **No cross-references**: chunks cannot point to data in other chunks by offset (ISOBMFF solved this) |
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- NDK39: `libraries/iffparse.h`, `datatypes/pictureclass.h`
|
||||
- EA IFF-85 specification: the original format definition
|
||||
- ADCD 2.1: iffparse.library autodocs
|
||||
- See also: [datatypes.md](datatypes.md) — higher-level data loading system that supersedes iffparse for most application use cases
|
||||
- See also: [ham_ehb_modes.md](../08_graphics/ham_ehb_modes.md) — HAM-encoded ILBM files
|
||||
- See also: [amigaguide.md](amigaguide.md) — AmigaGuide hypertext system (IFF FTXT+ILBM under the hood)
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
[← Home](../README.md) · [Libraries](README.md)
|
||||
|
||||
# locale.library — Internationalisation
|
||||
# locale.library — Internationalization
|
||||
|
||||
## Overview
|
||||
|
||||
`locale.library` (OS 2.1+) provides the Amiga's internationalisation (i18n) framework: language-aware string lookup via catalogues, locale-sensitive date/number/currency formatting, and character classification. Applications that use locale.library display in the user's preferred language automatically.
|
||||
`locale.library` (OS 2.1+) provides the Amiga's internationalization (i18n) framework: language-aware string lookup via catalogs, locale-sensitive date/number/currency formatting, and character classification. Applications that use locale.library display in the user's preferred language automatically.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
APP["Application"] -->|"GetCatalogStr(cat, ID, fallback)"| LOC["locale.library"]
|
||||
LOC -->|"Looks up string ID"| CAT["myapp.catalog<br/>(LOCALE:Catalogs/deutsch/)"]
|
||||
CAT -->|"Found"| DE["'Datei öffnen'"]
|
||||
LOC -->|"Not found"| FB["Fallback:<br/>'Open File'"]
|
||||
LOC -->|"Not found"| FB["Fallback:<br/>'Open File'<br/><i>(built into executable)</i>"]
|
||||
|
||||
style LOC fill:#e8f4fd,stroke:#2196f3,color:#333
|
||||
style CAT fill:#c8e6c9,stroke:#2e7d32,color:#333
|
||||
|
|
@ -19,7 +19,7 @@ flowchart TD
|
|||
|
||||
---
|
||||
|
||||
## Catalogue System
|
||||
## Catalog System
|
||||
|
||||
### Creating String IDs
|
||||
|
||||
|
|
@ -39,12 +39,12 @@ static const char *builtinStrings[] = {
|
|||
};
|
||||
```
|
||||
|
||||
### Using Catalogues
|
||||
### Using Catalogs
|
||||
|
||||
```c
|
||||
struct Library *LocaleBase = OpenLibrary("locale.library", 38);
|
||||
|
||||
/* Open the application's catalogue: */
|
||||
/* Open the application's catalog: */
|
||||
struct Catalog *cat = OpenCatalog(NULL, "myapp.catalog",
|
||||
OC_BuiltInLanguage, (ULONG)"english",
|
||||
TAG_DONE);
|
||||
|
|
@ -52,7 +52,7 @@ struct Catalog *cat = OpenCatalog(NULL, "myapp.catalog",
|
|||
|
||||
/* Get localised string (with English fallback): */
|
||||
STRPTR openStr = GetCatalogStr(cat, MSG_OPEN_FILE, "Open File");
|
||||
/* Returns German "Datei öffnen" if German catalogue exists,
|
||||
/* Returns German "Datei öffnen" if German catalog exists,
|
||||
otherwise the fallback "Open File" */
|
||||
|
||||
/* Use throughout the application: */
|
||||
|
|
@ -63,7 +63,7 @@ CloseCatalog(cat);
|
|||
CloseLibrary(LocaleBase);
|
||||
```
|
||||
|
||||
### Catalogue File Structure
|
||||
### Catalog File Structure
|
||||
|
||||
```
|
||||
LOCALE:Catalogs/deutsch/myapp.catalog ← German
|
||||
|
|
@ -71,10 +71,10 @@ LOCALE:Catalogs/français/myapp.catalog ← French
|
|||
LOCALE:Catalogs/italiano/myapp.catalog ← Italian
|
||||
```
|
||||
|
||||
Catalogues are compiled from `.cd` (catalogue description) and `.ct` (catalogue translation) files using **CatComp** or **FlexCat**:
|
||||
Catalogs are compiled from `.cd` (catalog description) and `.ct` (catalog translation) files using **CatComp** or **FlexCat**:
|
||||
|
||||
```
|
||||
; myapp.cd — catalogue description
|
||||
; myapp.cd — catalog description
|
||||
MSG_OPEN_FILE (1//)
|
||||
Open File
|
||||
;
|
||||
|
|
@ -182,8 +182,84 @@ LONG result = StrnCmp(loc, str1, str2, -1, SC_COLLATE2);
|
|||
|
||||
---
|
||||
|
||||
## Historical Perspective — i18n in the Early 1990s
|
||||
|
||||
locale.library shipped with Amiga OS 2.1 in 1992, at a time when internationalization was barely on the radar of most operating systems. The landscape was fragmented and primitive:
|
||||
|
||||
### The State of Internationalization (1990–1992)
|
||||
|
||||
| Platform | i18n Mechanism | String Lookup | Recompilation Required? | Number/Date Locale | Character Classification |
|
||||
|---|---|---|---|---|---|
|
||||
| **Amiga OS 2.1** | locale.library + external catalogs | Numeric ID → catalog file | ❌ No — drop a new `.catalog` file in the locale directory | ✅ FormatDate, FormatString, decimal/group separators | ✅ IsAlpha, IsUpper, collation (SC_COLLATE1/2) |
|
||||
| **Classic Mac OS (System 7)** | STR# resources + `GetIndString` | Integer ID → resource fork | ❌ No — ResEdit the resource fork | ❌ No — TextUtils had minimal formatting | ❌ No — ASCII-only on 68k Macs |
|
||||
| **Windows 3.1** | String table resources (`.rc` → `.res`) | Integer ID → resource DLL | ✅ Yes — re-link the resource DLL if separate; otherwise recompile | ❌ No — `GetNumberFormat`/`GetDateFormat` arrived in Win32 (NT 3.1, 1993) | ❌ No — `IsCharAlpha` was ASCII-only |
|
||||
| **Unix / X11 (Motif)** | `catgets()` (XPG3) or manual `#ifdef LANG_DE` | Numeric set+message ID → `.cat` file | ❌ No — message catalogs external | ⚠️ Partial — `setlocale()` + `nl_langinfo()`, but implementation was spotty across vendors | ⚠️ Partial — `isalpha()` was locale-aware only if the C library supported it |
|
||||
| **Atari ST (TOS)** | Nothing | — | — | — | — |
|
||||
| **NeXTSTEP 3.x** | `.strings` files + `NSLocalizedString()` | Natural-language key → `.strings` file | ❌ No — `.strings` files loaded at runtime | ✅ NSNumberFormatter, NSDateFormatter | ✅ NSString character methods |
|
||||
|
||||
### What Made locale.library Innovative
|
||||
|
||||
**1. External catalog files with zero-recompile extensibility.** This was the single most important design decision. An Amiga application ships with English strings compiled in. A translator in Germany creates `myapp.catalog` using CatComp/FlexCat, drops it into `LOCALE:Catalogs/deutsch/`, and the application immediately speaks German — no patches, no resource editing, no recompilation. Windows 3.1 could do this with separate resource DLLs, but only if the developer shipped them separately from the start — retrofitting an existing English-only app required recompilation. Mac OS System 7 required editing the resource fork with ResEdit. locale.library made localization a deployment decision, not a build decision.
|
||||
|
||||
**2. Numeric string IDs decouple code from language.** The application never hard-codes a German string — it hard-codes `MSG_OPEN_FILE = 1`. The catalog maps `1 → "Datei öffnen"`. This means translators never touch source code, developers never touch translations, and the same binary works in any language. Compare with early Unix practices where developers sprinkled `#ifdef LANG_DE ... #elif LANG_FR ...` throughout the source.
|
||||
|
||||
**3. Graceful fallback is built-in, not bolted-on.** `GetCatalogStr(cat, ID, "Open File")` always returns *something* — either the translated string from the catalog, or the English fallback. If the catalog file is missing, corrupted, or doesn't contain the requested ID, the application still works. This is the same principle that makes IFF safe (skip unknown chunks) — the system degrades gracefully under incomplete data.
|
||||
|
||||
**4. Locale-aware formatting is separate from string translation.** This was unusual for 1992. `FormatDate()` uses the user's locale preferences for date ordering (`DD/MM/YY` vs `MM/DD/YY`), weekday/month names, and decimal separators — but it doesn't require the application to be translated. A German user running an English application still sees `"Mittwoch, 23. April 1992"` if their locale is set to German, even though menu strings remain in English. This separation of concerns — content translation vs presentation formatting — wouldn't become standard until the late 1990s.
|
||||
|
||||
**5. Hook-based formatting for extensibility.** `FormatDate()` takes a `struct Hook *` callback rather than writing to a fixed buffer. The callback receives one character at a time, giving the application complete control over output — write to a window, send to a printer, stream to a file, or concatenate into a growing buffer. Qt's `QLocale` and macOS's `NSDateFormatter` return fixed `QString`/`NSString` objects; the Amiga's hook approach was both more primitive and more flexible.
|
||||
|
||||
> [!NOTE]
|
||||
> **NeXTSTEP deserves special mention.** It had `NSLocalizedString()` and `.strings` files in 1991, predating locale.library. NeXT's approach used **natural-language keys** (`"Open File" = "Datei öffnen";`) rather than numeric IDs — simpler for humans but fragile: if the developer tweaked the English string, the key changed and every translation broke. locale.library's numeric IDs are stable across English string edits.
|
||||
|
||||
---
|
||||
|
||||
## Modern Analogies — locale.library vs Today's Frameworks
|
||||
|
||||
Amiga's i18n architecture maps surprisingly well to modern frameworks — many of its design patterns became industry standards:
|
||||
|
||||
### Framework Comparison
|
||||
|
||||
| Concept | Amiga locale.library (1992) | Qt (1995–) | POSIX gettext (1990s–) | macOS / iOS (2000s–) | Android (2008–) |
|
||||
|---|---|---|---|---|---|
|
||||
| **String lookup function** | `GetCatalogStr(cat, ID, fallback)` | `QObject::tr("source")` or `QT_TR_NOOP()` | `gettext("original")` | `NSLocalizedString(@"key", @"comment")` | `getString(R.string.id)` |
|
||||
| **Translation key type** | Numeric integer ID (stable) | Source string as key (fragile) | Source string as key (fragile) | Natural-language key string (stable if chosen carefully) | Auto-generated integer ID from XML |
|
||||
| **Translation storage** | Binary `.catalog` (compiled from `.cd` + `.ct`) | `.qm` (compiled from `.ts` XML) | `.mo` (compiled from `.po`) | `.strings` plist/dictionary | `strings.xml` in `res/values-<locale>/` |
|
||||
| **External file** | ✅ `LOCALE:Catalogs/<lang>/` | ✅ loaded from filesystem | ✅ `/usr/share/locale/<lang>/LC_MESSAGES/` | ✅ `.lproj` bundles | ✅ APK resources, no post-install modification |
|
||||
| **Fallback chain** | Built-in English string passed as parameter | Source string in code → untranslated | Source string in code → untranslated | Key string → untranslated | Default `strings.xml` → key shown |
|
||||
| **Number/date formatting** | `FormatDate()`, `FormatString()`, `loc_DecimalPoint` | `QLocale::toString()`, `QDate::toString()` | `strftime()`, `nl_langinfo()` | `NSDateFormatter`, `NSNumberFormatter` | `DateFormat`, `NumberFormat` |
|
||||
| **Character classification** | `IsAlpha()`, `IsUpper()`, `ConvToUpper()`, `StrnCmp()` with collation | `QChar::isLetter()`, `QString::localeAwareCompare()` | `iswalpha()`, `wcscoll()` (C95) | `NSCharacterSet`, `localizedCompare:` | `Character.isLetter()`, `Collator` |
|
||||
| **Post-install translation** | ✅ Drop `.catalog` file — no app restart needed | ⚠️ `.qm` loaded at startup; need `QTranslator::load()` signal | ⚠️ `.mo` cached; need `bindtextdomain` reload | ❌ `.strings` in bundle — app resigning required | ❌ Resources baked into APK |
|
||||
| **Translator tooling** | CatComp, FlexCat | `lupdate`/`lrelease` + Qt Linguist | `xgettext`/`msgfmt` + Poedit | `genstrings` + Xcode | Android Studio translation editor |
|
||||
|
||||
### Architecture Patterns That Survived
|
||||
|
||||
**External resource files.** Every modern i18n system separates translations from code. locale.library was among the first consumer-OS frameworks to make this the *default* rather than a bolt-on. The directory convention `LOCALE:Catalogs/<language>/` prefigured `/usr/share/locale/` and `res/values-de/` by years.
|
||||
|
||||
**String ID indirection.** Whether it's `MSG_OPEN_FILE = 1` (Amiga), `R.string.open_file` (Android), or a `.strings` key (macOS), the pattern is identical: code references an abstract identifier, and the runtime resolves it to a language-specific string. locale.library's numeric IDs are arguable the most robust form — they can't accidentally change when someone rewrites the source string.
|
||||
|
||||
**Fallback chains.** `GetCatalogStr(cat, ID, "Open File")` prefigures Qt's `tr("Open File")` and Android's resource fallback. The key difference: locale.library requires the fallback as an explicit parameter, which means it's always in the executable. Qt and gettext use the source string as both key and fallback — simpler API but couples translation to the exact English phrasing.
|
||||
|
||||
**Formatter/translator separation.** The fact that `FormatDate()` works independently of catalog files — formatting weekday names from the active locale even when the app is untranslated — is a design subtlety that took other platforms years to replicate. Windows didn't separate locale formatting from UI language until Vista (2006).
|
||||
|
||||
### locale.library Design — Pros and Cons
|
||||
|
||||
| Advantage | Limitation |
|
||||
|---|---|
|
||||
| **Zero-recompile localization** — the killer feature. A translator never touches source code | **Binary catalog format** — proprietary and unversioned. No way to inspect a `.catalog` file without CatComp tools |
|
||||
| **Stable numeric IDs** — changing an English string doesn't invalidate translations | **Numeric IDs require tooling** — you need CatComp or FlexCat to generate the `#define` header. Manual ID assignment invites collisions |
|
||||
| **Graceful fallback** — missing catalog = English, not crash | **Zero-recompile also means zero-deploy tooling** — there's no package manager, so `.catalog` files are manually copied. AmigaOS never solved distribution |
|
||||
| **Formatting independent of translation** — German dates even in English apps | **Single-byte character set only** — locale.library predates Unicode. Languages with >256 characters (Japanese, Chinese, Korean) are impossible without hacks |
|
||||
| **Hook-based formatting** — application controls output destination | **No plural rules** — `GetCatalogStr` returns one string. Modern gettext has `ngettext()` for plural forms; Qt has `tr("%n files", "", n)` |
|
||||
| **Built into the OS** — no third-party library needed, consistent across all apps | **Limited adoption outside Amiga** — Commodore's bankruptcy meant the approach never influenced broader industry standards |
|
||||
|
||||
> [!NOTE]
|
||||
> **The Qt connection is not accidental.** Qt's `tr()` system — source strings as keys, compiled `.qm` files, `lupdate`/`lrelease` toolchain, and `QLocale` for formatting — is the closest spiritual successor to locale.library in the modern world. Both share the same core insight: i18n must be a **runtime deployment decision**, not a compile-time `#ifdef`. The key architectural difference: Qt uses source strings as translation keys (simpler for developers, fragile on rewrites); locale.library uses numeric IDs (stable, but requires tooling to generate).
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- NDK39: `libraries/locale.h`
|
||||
- ADCD 2.1: locale.library autodocs
|
||||
- CatComp / FlexCat documentation for catalogue compilation
|
||||
- CatComp / FlexCat documentation for catalog compilation
|
||||
|
|
|
|||
|
|
@ -201,11 +201,11 @@ graph TB
|
|||
| `68060.library` 40.1 | — | Phase5 | For Blizzard/CyberStorm accelerators |
|
||||
| `68060.library` 46.x | — | Motorola FPSP reference | Best precision and compatibility |
|
||||
| `Mu68040.library` | — | Thomas Richter (MMULib) | Enhanced, with MMU support |
|
||||
| `Mu68060.library` | — | Thomas Richter (MMULib) | Enhanced, with MMU + optimisations |
|
||||
| `Mu68060.library` | — | Thomas Richter (MMULib) | Enhanced, with MMU + optimizations |
|
||||
|
||||
### Three CPU Flavours
|
||||
|
||||
| CPU | FPU | Math Behaviour |
|
||||
| CPU | FPU | Math Behavior |
|
||||
|---|---|---|
|
||||
| **68040** (full) | ✅ Built-in | Basic FPU ops in hardware. Transcendentals trapped → 68040.library |
|
||||
| **68LC040** | ❌ No FPU | ALL FPU ops trap → needs 68040.library + SoftIEEE or full emulation |
|
||||
|
|
@ -232,7 +232,7 @@ Performance-critical code should use lookup tables, CORDIC algorithms, or polyno
|
|||
|
||||
## Third-Party Replacement Libraries
|
||||
|
||||
The Amiga's library system allows **drop-in replacements** — you simply copy an optimised `.library` to `LIBS:` and it supersedes the ROM version. Several third-party packages exploit this for dramatic speedups.
|
||||
The Amiga's library system allows **drop-in replacements** — you simply copy an optimized `.library` to `LIBS:` and it supersedes the ROM version. Several third-party packages exploit this for dramatic speedups.
|
||||
|
||||
### HSMathLibs (Matthias Henze)
|
||||
|
||||
|
|
@ -261,7 +261,7 @@ The most comprehensive third-party math library replacement. Written entirely in
|
|||
|
||||
### Mu680x0Libs / MMULib (Thomas Richter)
|
||||
|
||||
Part of the MMULib package, these provide optimised CPU-specific libraries including math support:
|
||||
Part of the MMULib package, these provide optimized CPU-specific libraries including math support:
|
||||
|
||||
| Package | Aminet |
|
||||
|---|---|
|
||||
|
|
@ -312,8 +312,8 @@ graph TB
|
|||
LIBS -->|"LIBS: copy wins"| HS
|
||||
LIBS -->|"No LIBS: copy"| ROM
|
||||
ROM --> STD
|
||||
HS -->|"040 version"| OPT040["68040-optimised code"]
|
||||
HS -->|"060 version"| OPT060["68060-optimised code"]
|
||||
HS -->|"040 version"| OPT040["68040-optimized code"]
|
||||
HS -->|"060 version"| OPT060["68060-optimized code"]
|
||||
MU --> FPSP["FPSP: handles<br/>unimplemented FPU ops"]
|
||||
```
|
||||
|
||||
|
|
@ -453,7 +453,7 @@ vc +aos68k prog.c -lmieee # software IEEE
|
|||
|
||||
4. **68LC040/060 crashes** — Code that works on full 68040 Gurus on LC variants. Install `SoftIEEE` or equivalent FPSP to trap Line-F exceptions from unimplemented FPU instructions.
|
||||
|
||||
5. **HSMathLibs CPU mismatch** — Installing the 040-optimised libraries on a 060 (or vice versa) can cause incorrect results or reduced performance. The pipeline timings are architecturally different.
|
||||
5. **HSMathLibs CPU mismatch** — Installing the 040-optimized libraries on a 060 (or vice versa) can cause incorrect results or reduced performance. The pipeline timings are architecturally different.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
536
11_libraries/translator.md
Normal file
536
11_libraries/translator.md
Normal file
|
|
@ -0,0 +1,536 @@
|
|||
[← Home](../README.md) · [Libraries](README.md)
|
||||
|
||||
# translator.library — English-to-Phonetic Translation for Speech Synthesis
|
||||
|
||||
## Overview
|
||||
|
||||
`translator.library` is the front half of the Amiga's built-in text-to-speech pipeline: a single-function library that converts unrestricted English text into **phonetic strings** — the expanded ARPABET phoneme codes used by `narrator.device` to generate human-like speech through the Amiga's audio hardware. Introduced with AmigaOS 1.2 and distributed as a disk-based library in `LIBS:`, it encapsulates over 450 context-sensitive pronunciation rules, an exception dictionary for irregular words (through, though, cough), abbreviation expansion (Dr., Prof., lb.), and automatic content-word accentuation — all in a single call: `Translate()`. The output is a string of space-delimited phoneme codes with stress markers that can be passed directly to `narrator.device` via `CMD_WRITE`, stored for later playback, or analyzed for phonetic research. While hand-coded phonetics always produce higher-quality speech, `Translate()` is the only practical option when the input is arbitrary user text at runtime.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### The Amiga Speech Pipeline
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph INPUT["Input Layer"]
|
||||
ENG["English Text<br/>(ASCII)"]
|
||||
PHON["Hand-Coded<br/>Phonetic String"]
|
||||
end
|
||||
|
||||
subgraph TRANSLATOR["translator.library"]
|
||||
TR["Translate()<br/>English → Phonetic"]
|
||||
RULES["450+ Context Rules<br/>Exception Dictionary<br/>Abbreviation Expansion"]
|
||||
end
|
||||
|
||||
subgraph NARRATOR["narrator.device"]
|
||||
SYNTH["Speech Synthesizer<br/>Formant Model"]
|
||||
MOUTH["Mouth Shape<br/>Generator"]
|
||||
end
|
||||
|
||||
subgraph OUTPUT["Output Layer"]
|
||||
AUDIO["audio.device<br/>DMA Audio Channels"]
|
||||
MOUTHDATA["mouth_rb<br/>Width/Height"]
|
||||
end
|
||||
|
||||
ENG --> TR
|
||||
TR --> RULES
|
||||
RULES --> TR
|
||||
TR -->|"Phonetic String"| SYNTH
|
||||
PHON --> SYNTH
|
||||
SYNTH --> AUDIO
|
||||
SYNTH --> MOUTH
|
||||
MOUTH --> MOUTHDATA
|
||||
|
||||
style TR fill:#e8f4fd,stroke:#2196f3,color:#333
|
||||
style SYNTH fill:#fff3e0,stroke:#ff9800,color:#333
|
||||
```
|
||||
|
||||
### Library Base
|
||||
|
||||
| Name | Type | Description |
|
||||
|---|---|---|
|
||||
| `TranslatorBase` | `struct Library *` | Library base pointer returned by `OpenLibrary()` |
|
||||
| `ITranslator` | Interface pointer (OS 4.x+) | Interface-based access for AmigaOS 4+ |
|
||||
|
||||
`translator.library` is a **disk-based** library — it lives in `LIBS:translator.library`, not in ROM. This means `OpenLibrary()` can fail if the file is missing, and the library can be expunged from memory under low-memory conditions.
|
||||
|
||||
### Key Design Decisions
|
||||
|
||||
| Decision | Rationale |
|
||||
|---|---|
|
||||
| **Single-function API** | Translation is inherently stateless — input text, output phonetics. No session, no configuration |
|
||||
| **Disk-based, not ROM** | Phonetic dictionary is large (~20+ KB of rules); keeping it out of ROM saves Kickstart space |
|
||||
| **Negative return codes for overflow** | Allows progressive translation of long texts without pre-allocating a huge buffer |
|
||||
| **Rule-based, not neural** | 1985 technology couldn't run a neural TTS; the 450 context-sensitive rules were state-of-the-art for the era |
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### Opening and Closing
|
||||
|
||||
```c
|
||||
/* Classic AmigaOS (1.x–3.x) — LVO -30 */
|
||||
struct Library *TranslatorBase;
|
||||
|
||||
TranslatorBase = OpenLibrary("translator.library", 0);
|
||||
if (!TranslatorBase) { /* LIBS:translator.library not found */ }
|
||||
|
||||
/* ... use Translate() ... */
|
||||
|
||||
CloseLibrary(TranslatorBase);
|
||||
```
|
||||
|
||||
```c
|
||||
/* AmigaOS 4.x — Interface-based */
|
||||
struct Library *TranslatorBase;
|
||||
struct TranslatorIFace *ITranslator;
|
||||
|
||||
TranslatorBase = IExec->OpenLibrary("translator.library", 0);
|
||||
if (TranslatorBase)
|
||||
{
|
||||
ITranslator = (struct TranslatorIFace *)
|
||||
IExec->GetInterface(TranslatorBase, "main", 1, NULL);
|
||||
if (ITranslator)
|
||||
{
|
||||
/* ... use ITranslator->Translate() ... */
|
||||
}
|
||||
IExec->DropInterface((struct Interface *)ITranslator);
|
||||
}
|
||||
IExec->CloseLibrary(TranslatorBase);
|
||||
```
|
||||
|
||||
### Translate()
|
||||
|
||||
```c
|
||||
/* LVO -36 — Converts English text to phonetic string */
|
||||
LONG Translate(STRPTR input, /* a0: English input string */
|
||||
LONG inputLen, /* d0: length of input */
|
||||
STRPTR output, /* a1: output buffer for phonetics */
|
||||
LONG outputSize /* d0: size of output buffer */);
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
|---|---|
|
||||
| `input` | Null-terminated or length-delimited English ASCII string. Case-insensitive; punctuation is preserved where it affects pronunciation |
|
||||
| `inputLen` | Number of characters to translate from `input`. Use `strlen(input)` for the full string |
|
||||
| `output` | Pre-allocated buffer to receive the phonetic string. **Must be large enough** — phonetics are typically 2–4× the input length |
|
||||
| `outputSize` | Size of the output buffer in bytes |
|
||||
|
||||
**Return value:**
|
||||
|
||||
| Return | Meaning |
|
||||
|---|---|
|
||||
| `0` | Full translation succeeded; output buffer was large enough |
|
||||
| **Negative** value | Buffer overflow — translation stopped at a word boundary. `-(rtnCode)` is the character offset in the input string where translation ended. Resume by calling `Translate(input + offset, inputLen - offset, output, outputSize)` |
|
||||
| Other non-zero | Translation error (unlikely — the library tries to translate literally if rules fail) |
|
||||
|
||||
> [!NOTE]
|
||||
> The negative return value always stops at a **word boundary** (space or punctuation), not mid-word. This prevents split phonemes and makes resumption seamless.
|
||||
|
||||
### Output Format
|
||||
|
||||
The output is a space-delimited string of **ARPABET phoneme codes** with **stress markers** appended to vowels:
|
||||
|
||||
```
|
||||
Input: "This is Amiga speaking."
|
||||
Output: "DH IH1 Z IH1 Z AE1 M IH0 G AH0 S P IY1 K IH0 NG ."
|
||||
└─ "This" ─┘ └"is"─┘ └─── "Amiga" ───┘ └─── "speaking" ───┘
|
||||
```
|
||||
|
||||
| Marker | Meaning | Example |
|
||||
|---|---|---|
|
||||
| `0` | No stress (unstressed vowel) | `IH0` = unstressed "i" (as in "rabbit") |
|
||||
| `1` | Primary stress | `IY1` = stressed "ee" (as in "speak") |
|
||||
| `2` | Secondary stress | `OW2` = secondary "oh" (as in "overflow") |
|
||||
| `3` | Emphatic stress (rare) | Used for contrastive emphasis |
|
||||
|
||||
---
|
||||
|
||||
## Phonetic Output Examples
|
||||
|
||||
| English Input | Phonetic Output (approx.) |
|
||||
|---|---|
|
||||
| `Hello world.` | `HH EH0 L OW1 W ER1 L D .` |
|
||||
| `The quick brown fox.` | `DH AH0 K W IH1 K B R AW1 N F AA1 K S .` |
|
||||
| `Amiga` | `AE1 M IH0 G AH0` or `AH0 M IY1 G AH0` (both valid) |
|
||||
| `Commodore` | `K AA1 M AH0 D AO1 R` |
|
||||
| `Guru Meditation` | `G UH1 R UW0 M EH2 D IH0 T EY1 SH AH0 N` |
|
||||
|
||||
> [!WARNING]
|
||||
> The translator library was designed for **American English** pronunciation. British spellings (colour, centre) and non-English words will be translated using American phonetic rules and may sound odd.
|
||||
|
||||
---
|
||||
|
||||
## Integration with narrator.device
|
||||
|
||||
The standard workflow:
|
||||
|
||||
```c
|
||||
#include <devices/narrator.h>
|
||||
#include <clib/translator_protos.h>
|
||||
|
||||
/* 1. Open translator */
|
||||
struct Library *TranslatorBase = OpenLibrary("translator.library", 0);
|
||||
|
||||
/* 2. Open narrator device */
|
||||
struct MsgPort *mp = CreatePort(NULL, 0);
|
||||
struct narrator_rb *voiceIO = (struct narrator_rb *)
|
||||
CreateExtIO(mp, sizeof(struct narrator_rb));
|
||||
OpenDevice("narrator.device", 0, (struct IORequest *)voiceIO, 0);
|
||||
|
||||
/* 3. Translate English → phonetic */
|
||||
#define PHONBUF_SIZE 2048
|
||||
STRPTR english = "Welcome to the Amiga speech system.";
|
||||
UBYTE phonBuffer[PHONBUF_SIZE];
|
||||
LONG result = Translate(english, strlen(english),
|
||||
(STRPTR)phonBuffer, PHONBUF_SIZE);
|
||||
|
||||
if (result == 0)
|
||||
{
|
||||
/* 4. Configure voice parameters */
|
||||
voiceIO->rate = 150; /* words per minute */
|
||||
voiceIO->pitch = 110; /* Hz baseline */
|
||||
voiceIO->sex = 0; /* 0=male, 1=female */
|
||||
voiceIO->volume = 64; /* 0–64 */
|
||||
voiceIO->sampfreq = 22200; /* Hz (Amiga native rate) */
|
||||
|
||||
/* 5. Send to narrator */
|
||||
voiceIO->message.io_Command = CMD_WRITE;
|
||||
voiceIO->message.io_Data = phonBuffer;
|
||||
voiceIO->message.io_Length = strlen((STRPTR)phonBuffer);
|
||||
DoIO((struct IORequest *)voiceIO);
|
||||
}
|
||||
|
||||
/* 6. Cleanup */
|
||||
CloseDevice((struct IORequest *)voiceIO);
|
||||
DeleteExtIO((struct IORequest *)voiceIO);
|
||||
DeletePort(mp);
|
||||
CloseLibrary(TranslatorBase);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When to Use / When NOT to Use
|
||||
|
||||
| Scenario | Use `Translate()`? | Rationale |
|
||||
|---|---|---|
|
||||
| **Unrestricted user input** (text editor, terminal, chat) | ✅ Yes | Only practical option — you can't pre-code phonetics for arbitrary text |
|
||||
| **Fixed application strings** (game dialog, error messages) | ❌ No | Hand-code phonetics once; ship the phonetic strings. Much better quality |
|
||||
| **Accessibility screen reader** | ✅ Yes | Essential — must speak whatever is on screen |
|
||||
| **Demo/game with iconic lines** | ❌ No | Hand-tune phonetics, stress, and timing for maximum impact |
|
||||
| **Multi-language support** | ❌ No | translator.library is English-only; use a third-party TTS or pre-recorded samples |
|
||||
| **Phonetic research/analysis** | ⚠️ Maybe | Output is useful for analysis but not linguistically rigorous — use as a starting point |
|
||||
| **Speaking numbers/dates** | ⚠️ Maybe | Library handles some abbreviations but not all; pre-process complex formats into spelled-out words |
|
||||
|
||||
---
|
||||
|
||||
## Pitfalls & Common Mistakes
|
||||
|
||||
### 1. Underestimating Phonetic Buffer Size
|
||||
|
||||
The phonetic representation is **always longer** than the input English. A 100-character sentence typically produces 300–500 bytes of phonetics:
|
||||
|
||||
```c
|
||||
/* BAD: Same-sized buffer — will overflow on first long word */
|
||||
UBYTE phonBuf[256];
|
||||
STRPTR english = "The extraordinarily complicated implementation...";
|
||||
LONG result = Translate(english, strlen(english), (STRPTR)phonBuf, 256);
|
||||
/* result will be negative — phonetic for "extraordinarily" alone is ~40 chars */
|
||||
|
||||
/* CORRECT: Allocate 4× input length, minimum 512 bytes */
|
||||
#define PHONBUF_SIZE(maxInput) (((maxInput) * 4) + 512)
|
||||
UBYTE *phonBuf = AllocMem(PHONBUF_SIZE(strlen(english)), MEMF_ANY);
|
||||
```
|
||||
|
||||
### 2. Ignoring Negative Return Code
|
||||
|
||||
A negative return from `Translate()` is a **resumption offset**, not a fatal error:
|
||||
|
||||
```c
|
||||
/* BAD: Treats partial translation as failure */
|
||||
LONG rtn = Translate(text, len, buf, size);
|
||||
if (rtn != 0) { /* panic — but text was partially translated! */ }
|
||||
|
||||
/* CORRECT: Resume from offset on negative return */
|
||||
LONG offset = 0;
|
||||
while (offset < len)
|
||||
{
|
||||
LONG rtn = Translate(text + offset, len - offset, buf, BUF_SIZE);
|
||||
if (rtn == 0) break; /* done */
|
||||
if (rtn < 0) offset += (-rtn); /* resume from word boundary */
|
||||
else { /* unexpected error */ break; }
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Passing Non-Null-Terminated Input with Wrong Length
|
||||
|
||||
If `inputLen` doesn't match the actual string, `Translate()` reads garbage or stops early:
|
||||
|
||||
```c
|
||||
/* BAD: strlen() on a buffer that may not be null-terminated */
|
||||
UBYTE buf[256];
|
||||
Read(fh, buf, 256); /* may fill entire buffer — no terminator */
|
||||
Translate((STRPTR)buf, strlen((STRPTR)buf), out, 1024);
|
||||
/* strlen() may read past the buffer! */
|
||||
|
||||
/* CORRECT: Use the explicit read count */
|
||||
LONG actual = Read(fh, buf, 256);
|
||||
Translate((STRPTR)buf, actual, out, 1024);
|
||||
```
|
||||
|
||||
### 4. Not Checking for Missing Disk-Based Library
|
||||
|
||||
Unlike ROM libraries, `translator.library` may not be present:
|
||||
|
||||
```c
|
||||
/* BAD: Assumes library is always available */
|
||||
struct Library *TranslatorBase = OpenLibrary("translator.library", 0);
|
||||
Translate("Hello", 5, buf, 512); /* crash if TranslatorBase == NULL! */
|
||||
|
||||
/* CORRECT: Always check the return */
|
||||
struct Library *TranslatorBase = OpenLibrary("translator.library", 0);
|
||||
if (TranslatorBase)
|
||||
{
|
||||
Translate("Hello", 5, buf, 512);
|
||||
CloseLibrary(TranslatorBase);
|
||||
}
|
||||
else
|
||||
{
|
||||
Printf("Speech not available — translator.library missing\n");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Named Antipatterns
|
||||
|
||||
### "The Mumbler" — Unrealistic Rate/Pitch
|
||||
|
||||
Setting `rate` extremely high makes speech unintelligible, but the translator itself has nothing to do with it — the problem is feeding valid phonetics to a misconfigured narrator:
|
||||
|
||||
```c
|
||||
/* BAD: Chipmunk speech */
|
||||
voiceIO->rate = 400; /* 400 words/min — unintelligible */
|
||||
voiceIO->pitch = 255; /* extremely high pitch */
|
||||
|
||||
/* Sensible defaults: */
|
||||
voiceIO->rate = 150; /* natural conversational speed */
|
||||
voiceIO->pitch = 110; /* male baseline (85–110 for male, 160–220 for female) */
|
||||
voiceIO->sex = 0; /* 0=male, 1=female */
|
||||
```
|
||||
|
||||
### "The Silent Speaker" — Mismatched Audio Allocation
|
||||
|
||||
The narrator device must allocate audio channels. If another application holds all four channels, `OpenDevice("narrator.device", ...)` succeeds but speech may not be audible:
|
||||
|
||||
```c
|
||||
/* BAD: No check on audio channel availability */
|
||||
OpenDevice("narrator.device", 0, (struct IORequest *)voiceIO, 0);
|
||||
/* Speech may be silent if audio channels are all in use */
|
||||
|
||||
/* CORRECT: Set channel mask to request specific channels */
|
||||
UBYTE chanMasks[] = { 0x03, 0x0C, 0x30, 0xC0 }; /* try channels 0-1, 2-3, 4-5, 6-7 */
|
||||
voiceIO->ch_masks = chanMasks;
|
||||
voiceIO->nm_masks = 4;
|
||||
```
|
||||
|
||||
### "The Echo" — Forgetting io_Data Nesting
|
||||
|
||||
When you send a `CMD_WRITE` to the narrator device, the `io_Data` pointer must remain valid until the I/O completes. Using a stack buffer with `DoIO()` is fine (blocking); using `SendIO()` (asynchronous) with a stack buffer is not:
|
||||
|
||||
```c
|
||||
/* BAD: Stack buffer with async I/O */
|
||||
void SpeakAsync(STRPTR text)
|
||||
{
|
||||
UBYTE phonBuf[512]; /* stack — disappears on return! */
|
||||
Translate(text, strlen(text), (STRPTR)phonBuf, 512);
|
||||
voiceIO->message.io_Data = phonBuf;
|
||||
SendIO((struct IORequest *)voiceIO); /* async — phonBuf gone when this returns */
|
||||
}
|
||||
|
||||
/* CORRECT: Allocate or use static buffer for async */
|
||||
UBYTE phonBuf[2048]; /* static — stays valid */
|
||||
void SpeakAsync(STRPTR text)
|
||||
{
|
||||
Translate(text, strlen(text), (STRPTR)phonBuf, sizeof(phonBuf));
|
||||
voiceIO->message.io_Data = phonBuf;
|
||||
SendIO((struct IORequest *)voiceIO);
|
||||
/* phonBuf lives until AbortIO or CMD_FLUSH */
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: Can I use translator.library without narrator.device?**
|
||||
|
||||
Yes. The phonetic output is a plain ASCII string — you can save it, analyze it, send it over a network, or use it as input to a custom speech synthesizer. The translator and narrator are independent.
|
||||
|
||||
**Q: Why does the same word sometimes translate differently?**
|
||||
|
||||
The translator uses **context-sensitive** rules. The pronunciation of "read" depends on surrounding tense markers; "record" as a noun vs. verb gets different stress. The same word in different sentences may produce different phonetics — this is correct behavior.
|
||||
|
||||
**Q: How do I make the narrator sound female?**
|
||||
|
||||
Set `voiceIO->sex = 1` (female). This adjusts formant frequencies and baseline pitch. For manual fine-tuning, adjust `voiceIO->pitch` (160–220 Hz for female) and `voiceIO->F1adj` through `F3adj` (formant shifts).
|
||||
|
||||
**Q: Can translator.library handle multiple languages?**
|
||||
|
||||
No. The rule set and exception dictionary are English-only. German, French, or other languages will be treated as misspelled English and produce garbled phonetics. Use locale-specific TTS solutions for non-English speech.
|
||||
|
||||
**Q: How big is the output buffer really needed?**
|
||||
|
||||
Empirically, 4× the input length plus a 512-byte safety margin. The longest single English word phonetics (like "supercalifragilisticexpialidocious") is roughly 80 characters from 34 input characters. A typical sentence expands 2.5–3×.
|
||||
|
||||
**Q: Does Translate() handle punctuation?**
|
||||
|
||||
Yes. Punctuation marks (`.`, `,`, `?`, `!`, `;`, `:`) are passed through to the phonetic output. The narrator device interprets them as prosody cues: `.` = falling intonation, `?` = rising intonation.
|
||||
|
||||
---
|
||||
|
||||
## Use-Case Cookbook
|
||||
|
||||
### 1. Simple One-Shot Speech
|
||||
|
||||
The blocking pattern — suitable for alert messages, game notifications, short announcements:
|
||||
|
||||
```c
|
||||
void Say(STRPTR english)
|
||||
{
|
||||
struct Library *TranslatorBase = OpenLibrary("translator.library", 0);
|
||||
if (!TranslatorBase) return;
|
||||
|
||||
UBYTE phonBuf[2048];
|
||||
LONG rtn = Translate(english, strlen(english),
|
||||
(STRPTR)phonBuf, sizeof(phonBuf));
|
||||
if (rtn == 0)
|
||||
{
|
||||
struct MsgPort *mp = CreatePort(NULL, 0);
|
||||
struct narrator_rb *vio = (struct narrator_rb *)
|
||||
CreateExtIO(mp, sizeof(struct narrator_rb));
|
||||
|
||||
if (OpenDevice("narrator.device", 0, (struct IORequest *)vio, 0) == 0)
|
||||
{
|
||||
vio->rate = 150;
|
||||
vio->pitch = 110;
|
||||
vio->volume = 64;
|
||||
vio->sampfreq = 22200;
|
||||
|
||||
vio->message.io_Command = CMD_WRITE;
|
||||
vio->message.io_Data = phonBuf;
|
||||
vio->message.io_Length = strlen((STRPTR)phonBuf);
|
||||
DoIO((struct IORequest *)vio);
|
||||
|
||||
CloseDevice((struct IORequest *)vio);
|
||||
}
|
||||
DeleteExtIO((struct IORequest *)vio);
|
||||
DeletePort(mp);
|
||||
}
|
||||
CloseLibrary(TranslatorBase);
|
||||
}
|
||||
|
||||
/* Usage: */
|
||||
Say("Game over. Insert coin to continue.");
|
||||
```
|
||||
|
||||
### 2. Animated Talking Head (with Mouth Shapes)
|
||||
|
||||
The narrator can generate mouth width/height data while speaking:
|
||||
|
||||
```c
|
||||
/* Open two I/O requests — one for speech, one for mouth data */
|
||||
struct narrator_rb *voiceIO = /* ... */;
|
||||
struct mouth_rb *mouthIO = (struct mouth_rb *)
|
||||
CreateExtIO(mp, sizeof(struct mouth_rb));
|
||||
|
||||
/* Enable mouth shape generation */
|
||||
voiceIO->mouths = 1; /* non-zero = generate mouth data */
|
||||
|
||||
/* Send speech command */
|
||||
voiceIO->message.io_Command = CMD_WRITE;
|
||||
voiceIO->message.io_Data = phonBuf;
|
||||
voiceIO->message.io_Length = strlen((STRPTR)phonBuf);
|
||||
SendIO((struct IORequest *)voiceIO);
|
||||
|
||||
/* While speaking, read mouth shapes */
|
||||
while (!CheckIO((struct IORequest *)voiceIO))
|
||||
{
|
||||
mouthIO->voice.message.io_Command = CMD_READ;
|
||||
mouthIO->voice.message.io_Data = phonBuf; /* same buffer — narrator correlates */
|
||||
mouthIO->voice.message.io_Length = strlen((STRPTR)phonBuf);
|
||||
DoIO((struct IORequest *)mouthIO);
|
||||
|
||||
/* mouthIO->width = 0..255 (closed → wide open) */
|
||||
/* mouthIO->height = 0..255 (closed → wide open) */
|
||||
AnimateMouth(mouthIO->width, mouthIO->height);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Progressive Translation of Long Text
|
||||
|
||||
For documents or long-form text where a single 2 KB buffer won't suffice:
|
||||
|
||||
```c
|
||||
LONG TranslateLongText(STRPTR text, LONG totalLen, BPTR outputFH)
|
||||
{
|
||||
UBYTE phonBuf[2048];
|
||||
LONG offset = 0;
|
||||
|
||||
while (offset < totalLen)
|
||||
{
|
||||
LONG bytesAvail = totalLen - offset;
|
||||
LONG rtn = Translate(text + offset, bytesAvail,
|
||||
(STRPTR)phonBuf, sizeof(phonBuf));
|
||||
|
||||
if (rtn == 0)
|
||||
{
|
||||
/* Final chunk — write and done */
|
||||
LONG phonLen = strlen((STRPTR)phonBuf);
|
||||
Write(outputFH, phonBuf, phonLen);
|
||||
break;
|
||||
}
|
||||
else if (rtn < 0)
|
||||
{
|
||||
/* Write completed portion, resume at word boundary */
|
||||
LONG phonLen = strlen((STRPTR)phonBuf);
|
||||
Write(outputFH, phonBuf, phonLen);
|
||||
offset += (-rtn);
|
||||
}
|
||||
else
|
||||
{
|
||||
/* unexpected error */
|
||||
return rtn;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Modern Analogies
|
||||
|
||||
| Amiga Concept | Modern Equivalent | Why It Maps | Where It Diverges |
|
||||
|---|---|---|---|
|
||||
| **translator.library** | macOS `NSSpeechSynthesizer` / Windows SAPI Text-to-Speech | Both accept English text and produce speech. The API philosophy — text in, audio out — is identical | Modern APIs bundle translation and synthesis; Amiga splits them into library (translate) and device (speak) |
|
||||
| **ARPABET phonemes** | IPA (International Phonetic Alphabet) | Both encode pronunciation as discrete symbols. ARPABET is a machine-readable subset of IPA | ARPABET is English-only; IPA is universal. ARPABET uses ASCII, IPA uses Unicode |
|
||||
| **450 context-sensitive rules** | Modern TTS neural networks (Tacotron, FastSpeech) | Both learn pronunciation from data — rules are a 1985 hand-crafted "model" | Neural TTS requires gigabytes of training data; rule-based works with zero training |
|
||||
| **narrator.device formant synthesis** | Vocaloid / singing synthesis | Both use formant models (F0, F1, F2...) to generate vocal sounds | Narrator.device is a 1985-era 8-bit formant synth; Vocaloid uses concatenative sampling + ML |
|
||||
| **`Say` command / `speak:` handler** | `say` command on macOS / `espeak` on Linux | Both provide command-line text-to-speech | Amiga `Say` feeds translator.library → narrator.device; macOS `say` uses a system-wide speech server |
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- ADCD 2.1: *ROM Kernel Reference Manual: Libraries* — Chapter 36: Translator Library
|
||||
- ADCD 2.1: *ROM Kernel Reference Manual: Devices* — Chapter 8: Narrator Device
|
||||
- NDK 3.9: `devices/narrator.h` — `narrator_rb` and `mouth_rb` structures
|
||||
- NDK 3.9: `clib/translator_protos.h` — `Translate()` prototype
|
||||
- AmigaOS Documentation Wiki: [Narrator Device](https://wiki.amigaos.net/wiki/Narrator_Device) — complete phoneme table and phonetic writing guide
|
||||
- AmigaOS Documentation Wiki: [Translator Library](https://wiki.amigaos.net/wiki/Translator_Library) — OS 4.x interface reference
|
||||
- See also: [audio.md](../10_devices/audio.md) — audio.device DMA channel allocation used by narrator
|
||||
- See also: [iffparse.md](iffparse.md) — IFF FTXT parsing (the AmigaGuide format sometimes wraps speech metadata in IFF chunks)
|
||||
|
|
@ -142,7 +142,7 @@ ULONG __saveds __asm MyHookFunc(
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* Initialise the hook: */
|
||||
/* Initialize the hook: */
|
||||
struct Hook myHook;
|
||||
myHook.h_Entry = (HOOKFUNC)HookEntry; /* utility.library glue */
|
||||
myHook.h_SubEntry = (HOOKFUNC)MyHookFunc;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue