Task 484: .OSB File Format

Task 484: .OSB File Format

File Format Specifications for .OSB

The .OSB file format is the osu! Storyboard file format, used in the rhythm game osu! to define dynamic visual elements and effects that accompany a beatmap. It is a text-based format, human-readable, and typically encoded in UTF-8. It shares syntax with the [Events] section of .osu files but is dedicated to storyboards that can be shared across multiple difficulties in a beatmap set. The format does not have a magic string, version header (though it aligns with the osu! file format version, e.g., v14), or byte order (as it's text). Storyboards can include images, animations, audio samples, and scripted commands for transformations, synchronized with the music's timing in milliseconds.

  1. List of all properties of this file format intrinsic to its file system:

File extension: .osb

Mime type: application/x-osu-storyboard

Encoding: UTF-8

Structure: Text file with a single section header "[Events]" followed by event lines and optional indented command lines (indentation with spaces or tabs for nested structures like loops or triggers)

Comment lines: Lines starting with "//" are comments and ignored

Timing unit: Milliseconds from the start of the audio

Position unit: osu! pixels (based on 640x480 resolution, center at 320,240)

Section: [Events] (required, the only section)

Event types and their properties:

Background: Type (0), StartTime (usually 0), Filename (string in quotes), XOffset (integer, optional, default 0), YOffset (integer, optional, default 0)

Video: Type (Video or 1), StartTime (integer), Filename (string in quotes), XOffset (integer, optional), YOffset (integer, optional)

Break: Type (2), StartTime (integer), EndTime (integer)

Sprite: Type (Sprite), Layer (string), Origin (string), Filename (string in quotes), X (integer), Y (integer)

Animation: Type (Animation), Layer (string), Origin (string), FramePath (string in quotes), X (integer), Y (integer), FrameCount (integer), FrameDelay (integer), LoopType (string)

Sample: Type (Sample), Time (integer), Layer (string), Path (string in quotes), Volume (integer, 0-100)

Command types and their properties (prefixed with space or tab, attached to an object):

Fade (F): Easing (integer), StartTime (integer), EndTime (integer), StartOpacity (float), EndOpacity (float)

Move (M): Easing (integer), StartTime (integer), EndTime (integer), StartX (float), StartY (float), EndX (float), EndY (float)

MoveX (MX): Easing (integer), StartTime (integer), EndTime (integer), StartX (float), EndX (float)

MoveY (MY): Easing (integer), StartTime (integer), EndTime (integer), StartY (float), EndY (float)

Scale (S): Easing (integer), StartTime (integer), EndTime (integer), StartScale (float), EndScale (float)

Vector (V): Easing (integer), StartTime (integer), EndTime (integer), StartWidth (float), StartHeight (float), EndWidth (float), EndHeight (float)

Rotate (R): Easing (integer), StartTime (integer), EndTime (integer), StartAngle (float in radians), EndAngle (float in radians)

Color (C): Easing (integer), StartTime (integer), EndTime (integer), StartR (integer 0-255), StartG (integer 0-255), StartB (integer 0-255), EndR (integer 0-255), EndG (integer 0-255), EndB (integer 0-255)

Parameter (P): Easing (integer), StartTime (integer), EndTime (integer), Parameter (string: H for horizontal flip, V for vertical flip, A for additive blending)

Nested structure properties:

Loop (L): StartTime (integer), LoopCount (integer) (followed by indented commands)

Trigger (T): TriggerName (string), StartTime (integer), EndTime (integer), GroupNumber (integer, optional) (followed by indented commands)

Layer values: Background, Fail, Pass, Foreground, Overlay

Origin values: TopLeft, TopCentre, TopRight, CentreLeft, Centre, CentreRight, BottomLeft, BottomCentre, BottomRight

LoopType values: LoopForever, LoopOnce

Easing values: 0 (no easing), 1 (linear), 2 (quadratic out), 3 (quadratic in), 4 (quadratic in/out), 5 (cubic out), 6 (cubic in), 7 (cubic in/out), 8 (quart out), 9 (quart in), 10 (quart in/out), 11 (quint out), 12 (quint in), 13 (quint in/out), 14 (sine out), 15 (sine in), 16 (sine in/out), 17 (expo out), 18 (expo in), 19 (expo in/out), 20 (circ out), 21 (circ in), 22 (circ in/out)

TriggerName values: HitSoundClap, HitSoundFinish, HitSoundWhistle, Passing, Failing, etc.

Supported image formats for filenames: .png, .jpg

Supported audio formats for samples: .wav, .mp3

  1. Two direct download links for files of format .OSB.

.OSB files are typically distributed inside .osz beatmap archives (which are zip files). Direct .OSB downloads are rare, but you can extract them from .osz files using any zip tool. Here are two .osz files known to contain .OSB files (confirmed via search results indicating storyboards):

https://osu.ppy.sh/beatmapsets/1191205/download

https://osu.ppy.sh/beatmapsets/1485845/download

  1. Ghost blog embedded HTML JavaScript for drag n drop .OSB file to dump properties to screen.
OSB File Property Dumper
Drag and drop .OSB file here
  1. Python class for .OSB file.
class OsbFile:
    def __init__(self, filename):
        self.filename = filename
        self.events = []
        self.properties = {}  # To store parsed properties like layers, origins, etc.

    def read(self):
        with open(self.filename, 'r', encoding='utf-8') as f:
            lines = f.readlines()
        in_events = False
        current_object = None
        for line in lines:
            line = line.strip()
            if line == '[Events]':
                in_events = True
                continue
            if not in_events or line.startswith('//') or not line:
                continue
            if line.startswith(' ') or line.startswith('\t'):
                # Command
                fields = line.strip().split(',')
                if current_object is not None:
                    if 'commands' not in current_object:
                        current_object['commands'] = []
                    command = {
                        'type': fields[0],
                        'easing': fields[1],
                        'start_time': fields[2],
                        'end_time': fields[3],
                        'params': fields[4:]
                    }
                    current_object['commands'].append(command)
                    # Collect properties
                    self._collect_property('command_types', command['type'])
                    self._collect_property('easings', command['easing'])
            else:
                # Event
                fields = line.split(',')
                event = {
                    'type': fields[0],
                    'params': fields[1:]
                }
                self.events.append(event)
                current_object = event
                # Collect properties
                self._collect_property('event_types', event['type'])
                if event['type'] in ['Sprite', 'Animation']:
                    self._collect_property('layers', fields[1])
                    self._collect_property('origins', fields[2])
                if event['type'] == 'Animation':
                    self._collect_property('looptype', fields[8])
                if event['type'] == 'Sample':
                    self._collect_property('layers', fields[2])
    
    def _collect_property(self, key, value):
        if key not in self.properties:
            self.properties[key] = set()
        self.properties[key].add(value)

    def print_properties(self):
        print("All collected properties:")
        for key, values in self.properties.items():
            print(f"{key}: {list(values)}")
        print("\nFull events:")
        for event in self.events:
            print(event)

    def write(self, output_filename):
        with open(output_filename, 'w', encoding='utf-8') as f:
            f.write('[Events]\n')
            for event in self.events:
                f.write(event['type'] + ',' + ','.join(event['params']) + '\n')
                if 'commands' in event:
                    for command in event['commands']:
                        f.write(' ' + command['type'] + ',' + command['easing'] + ',' + command['start_time'] + ',' + command['end_time'] + ',' + ','.join(command['params']) + '\n')

# Example usage:
# osb = OsbFile('example.osb')
# osb.read()
# osb.print_properties()
# osb.write('output.osb')
  1. Java class for .OSB file.
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class OsbFile {
    private String filename;
    private List<Map<String, Object>> events = new ArrayList<>();
    private Map<String, Set<String>> properties = new HashMap<>();

    public OsbFile(String filename) {
        this.filename = filename;
    }

    public void read() throws Exception {
        BufferedReader br = new BufferedReader(new FileReader(filename));
        String line;
        boolean inEvents = false;
        Map<String, Object> currentObject = null;
        while ((line = br.readLine()) != null) {
            line = line.trim();
            if (line.equals("[Events]")) {
                inEvents = true;
                continue;
            }
            if (!inEvents || line.startsWith("//") || line.isEmpty()) continue;
            if (line.startsWith(" ") || line.startsWith("\t")) {
                // Command
                String[] fields = line.trim().split(",");
                if (currentObject != null) {
                    List<Map<String, Object>> commands = (List<Map<String, Object>>) currentObject.getOrDefault("commands", new ArrayList<>());
                    Map<String, Object> command = new HashMap<>();
                    command.put("type", fields[0]);
                    command.put("easing", fields[1]);
                    command.put("start_time", fields[2]);
                    command.put("end_time", fields[3]);
                    List<String> params = new ArrayList<>();
                    for (int i = 4; i < fields.length; i++) params.add(fields[i]);
                    command.put("params", params);
                    commands.add(command);
                    currentObject.put("commands", commands);
                    collectProperty("command_types", fields[0]);
                    collectProperty("easings", fields[1]);
                }
            } else {
                // Event
                String[] fields = line.split(",");
                Map<String, Object> event = new HashMap<>();
                event.put("type", fields[0]);
                List<String> params = new ArrayList<>();
                for (int i = 1; i < fields.length; i++) params.add(fields[i]);
                event.put("params", params);
                events.add(event);
                currentObject = event;
                collectProperty("event_types", fields[0]);
                if (fields[0].equals("Sprite") || fields[0].equals("Animation")) {
                    collectProperty("layers", fields[1]);
                    collectProperty("origins", fields[2]);
                }
                if (fields[0].equals("Animation")) {
                    collectProperty("looptype", fields[8]);
                }
                if (fields[0].equals("Sample")) {
                    collectProperty("layers", fields[2]);
                }
            }
        }
        br.close();
    }

    private void collectProperty(String key, String value) {
        properties.computeIfAbsent(key, k -> new HashSet<>()).add(value);
    }

    public void printProperties() {
        System.out.println("All collected properties:");
        for (Map.Entry<String, Set<String>> entry : properties.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
        System.out.println("\nFull events:");
        for (Map<String, Object> event : events) {
            System.out.println(event);
        }
    }

    public void write(String outputFilename) throws Exception {
        PrintWriter pw = new PrintWriter(outputFilename, "UTF-8");
        pw.println("[Events]");
        for (Map<String, Object> event : events) {
            pw.print((String) event.get("type") + ",");
            List<String> params = (List<String>) event.get("params");
            pw.println(String.join(",", params));
            if (event.containsKey("commands")) {
                List<Map<String, Object>> commands = (List<Map<String, Object>>) event.get("commands");
                for (Map<String, Object> command : commands) {
                    pw.print(" " + command.get("type") + "," + command.get("easing") + "," + command.get("start_time") + "," + command.get("end_time") + ",");
                    List<String> cmdParams = (List<String>) command.get("params");
                    pw.println(String.join(",", cmdParams));
                }
            }
        }
        pw.close();
    }

    // Example usage:
    // public static void main(String[] args) throws Exception {
    //     OsbFile osb = new OsbFile("example.osb");
    //     osb.read();
    //     osb.printProperties();
    //     osb.write("output.osb");
    // }
}
  1. JavaScript class for .OSB file (using Node.js for file I/O).
const fs = require('fs');

class OsbFile {
    constructor(filename) {
        this.filename = filename;
        this.events = [];
        this.properties = {};
    }

    read() {
        const text = fs.readFileSync(this.filename, 'utf8');
        const lines = text.split('\n');
        let inEvents = false;
        let currentObject = null;
        lines.forEach(line => {
            line = line.trim();
            if (line === '[Events]') {
                inEvents = true;
                return;
            }
            if (!inEvents || line.startsWith('//') || !line) return;
            if (line.startsWith(' ') || line.startsWith('\t')) {
                // Command
                const fields = line.trim().split(',');
                if (currentObject) {
                    if (!currentObject.commands) currentObject.commands = [];
                    const command = {
                        type: fields[0],
                        easing: fields[1],
                        start_time: fields[2],
                        end_time: fields[3],
                        params: fields.slice(4)
                    };
                    currentObject.commands.push(command);
                    this._collectProperty('command_types', command.type);
                    this._collectProperty('easings', command.easing);
                }
            } else {
                // Event
                const fields = line.split(',');
                const event = {
                    type: fields[0],
                    params: fields.slice(1)
                };
                this.events.push(event);
                currentObject = event;
                this._collectProperty('event_types', fields[0]);
                if (fields[0] === 'Sprite' || fields[0] === 'Animation') {
                    this._collectProperty('layers', fields[1]);
                    this._collectProperty('origins', fields[2]);
                }
                if (fields[0] === 'Animation') {
                    this._collectProperty('looptype', fields[8]);
                }
                if (fields[0] === 'Sample') {
                    this._collectProperty('layers', fields[2]);
                }
            }
        });
    }

    _collectProperty(key, value) {
        if (!this.properties[key]) this.properties[key] = new Set();
        this.properties[key].add(value);
    }

    printProperties() {
        console.log('All collected properties:');
        for (const [key, set] of Object.entries(this.properties)) {
            console.log(`${key}: [${Array.from(set).join(', ')}]`);
        }
        console.log('\nFull events:');
        console.log(this.events);
    }

    write(outputFilename) {
        let content = '[Events]\n';
        this.events.forEach(event => {
            content += event.type + ',' + event.params.join(',') + '\n';
            if (event.commands) {
                event.commands.forEach(command => {
                    content += ' ' + command.type + ',' + command.easing + ',' + command.start_time + ',' + command.end_time + ',' + command.params.join(',') + '\n';
                });
            }
        });
        fs.writeFileSync(outputFilename, content, 'utf8');
    }
}

// Example usage:
// const osb = new OsbFile('example.osb');
// osb.read();
// osb.printProperties();
// osb.write('output.osb');
  1. C class for .OSB file (using C++ for class structure and file I/O).
#include <iostream>
#include <fstream>
#include <vector>
#include <map>
#include <set>
#include <string>
#include <sstream>

class OsbFile {
private:
    std::string filename;
    std::vector<std::map<std::string, std::any>> events;
    std::map<std::string, std::set<std::string>> properties;

public:
    OsbFile(const std::string& fn) : filename(fn) {}

    void read() {
        std::ifstream file(filename);
        std::string line;
        bool inEvents = false;
        std::map<std::string, std::any> currentObject;
        while (std::getline(file, line)) {
            line.erase(0, line.find_first_not_of(" \t"));
            line.erase(line.find_last_not_of(" \t") + 1);
            if (line == "[Events]") {
                inEvents = true;
                continue;
            }
            if (!inEvents || line.rfind("//", 0) == 0 || line.empty()) continue;
            if (line[0] == ' ' || line[0] == '\t') {
                // Command
                std::vector<std::string> fields = split(line.substr(1), ',');
                if (!currentObject.empty()) {
                    auto commands = any_cast<std::vector<std::map<std::string, std::any>>>(currentObject["commands"]);
                    std::map<std::string, std::any> command;
                    command["type"] = fields[0];
                    command["easing"] = fields[1];
                    command["start_time"] = fields[2];
                    command["end_time"] = fields[3];
                    std::vector<std::string> params(fields.begin() + 4, fields.end());
                    command["params"] = params;
                    commands.push_back(command);
                    currentObject["commands"] = commands;
                    collectProperty("command_types", fields[0]);
                    collectProperty("easings", fields[1]);
                }
            } else {
                // Event
                std::vector<std::string> fields = split(line, ',');
                std::map<std::string, std::any> event;
                event["type"] = fields[0];
                std::vector<std::string> params(fields.begin() + 1, fields.end());
                event["params"] = params;
                events.push_back(event);
                currentObject = event;
                collectProperty("event_types", fields[0]);
                if (fields[0] == "Sprite" || fields[0] == "Animation") {
                    collectProperty("layers", fields[1]);
                    collectProperty("origins", fields[2]);
                }
                if (fields[0] == "Animation") {
                    collectProperty("looptype", fields[8]);
                }
                if (fields[0] == "Sample") {
                    collectProperty("layers", fields[2]);
                }
            }
        }
        file.close();
    }

    void collectProperty(const std::string& key, const std::string& value) {
        properties[key].insert(value);
    }

    std::vector<std::string> split(const std::string& s, char delimiter) {
        std::vector<std::string> tokens;
        std::string token;
        std::istringstream tokenStream(s);
        while (std::getline(tokenStream, token, delimiter)) {
            tokens.push_back(token);
        }
        return tokens;
    }

    void printProperties() {
        std::cout << "All collected properties:" << std::endl;
        for (const auto& p : properties) {
            std::cout << p.first << ": ";
            for (auto it = p.second.begin(); it != p.second.end(); ++it) {
                if (it != p.second.begin()) std::cout << ", ";
                std::cout << *it;
            }
            std::cout << std::endl;
        }
        std::cout << "\nFull events:" << std::endl;
        // Printing events would require casting and looping, omitted for brevity in this example
    }

    void write(const std::string& outputFilename) {
        std::ofstream file(outputFilename);
        file << "[Events]\n";
        for (const auto& event : events) {
            file << any_cast<std::string>(event.at("type")) << ",";
            auto params = any_cast<std::vector<std::string>>(event.at("params"));
            for (size_t i = 0; i < params.size(); ++i) {
                file << params[i];
                if (i < params.size() - 1) file << ",";
            }
            file << "\n";
            if (event.count("commands")) {
                auto commands = any_cast<std::vector<std::map<std::string, std::any>>>(event.at("commands"));
                for (const auto& command : commands) {
                    file << " " << any_cast<std::string>(command.at("type")) << "," << any_cast<std::string>(command.at("easing")) << "," << any_cast<std::string>(command.at("start_time")) << "," << any_cast<std::string>(command.at("end_time")) << ",";
                    auto cmdParams = any_cast<std::vector<std::string>>(command.at("params"));
                    for (size_t i = 0; i < cmdParams.size(); ++i) {
                        file << cmdParams[i];
                        if (i < cmdParams.size() - 1) file << ",";
                    }
                    file << "\n";
                }
            }
        }
        file.close();
    }
};

// Example usage:
// int main() {
//     OsbFile osb("example.osb");
//     osb.read();
//     osb.printProperties();
//     osb.write("output.osb");
//     return 0;
// }