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.
- 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
- 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
- Ghost blog embedded HTML JavaScript for drag n drop .OSB file to dump properties to screen.
- 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')
- 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");
// }
}
- 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');
- 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;
// }