Protocol Specification
Technical specification of the BeatBlocks protocol
Data Model
The BeatBlocks protocol defines a structured data model for representing generative music compositions. This model consists of several key components that work together to create dynamic, procedurally generated music.
BeatBlockInputData
The core data structure that defines a BeatBlock composition:
interface BeatBlockInputData {
details: Details;
layers: Layer[];
dynamics?: any;
generationConfig: GenerationConfig;
template: TemplateSection[];
arrangement?: Section[];
}
Details
Metadata about the composition
interface Details {
title: string;
author: string;
bpm: number;
imgId?: string;
visId?: string;
}
Layer
Individual audio components
interface Layer {
id: string;
loopLength: number;
path: string;
volume: number;
groups: string[];
mutex: string[];
loop: boolean;
offset?: number;
alignment?: 'start' | 'end' | 'center';
weight?: number;
}
GenerationConfig
Configuration for the procedural generation algorithm:
interface GenerationConfig {
seed: number;
groups: string[];
mutexes: string[];
}
seed: Deterministic random seed for reproducible generation
groups: Categories for organizing layers (e.g., "aa", "bb", "cc")
mutexes: Mutually exclusive layer types (e.g., "drums", "bass", "vox")
TemplateSection
Template for generating song sections:
interface TemplateSection {
length: number;
layerCount: number;
inclusions: string[];
exclusions: string[];
}
length: Duration of the section in bars
layerCount: Number of layers to include
inclusions: Layer types that must be included
exclusions: Layer types that must be excluded
Section
A generated section of the composition:
interface Section {
length: number;
layers: SectionLayer[];
}
Generation Algorithm
The BeatBlocks protocol uses a deterministic algorithm to generate music arrangements from templates. This ensures that the same seed always produces the same musical result.
Pseudorandom Number Generation
BeatBlocks uses the Mulberry32 algorithm for deterministic random number generation:
function mulberry32(seed) {
let a = seed >>> 0;
return function() {
a = (a + 0x6D2B79F5) >>> 0;
let t = Math.imul(a ^ (a >>> 15), a | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
}
}
Layer Selection Process
The generation algorithm follows these steps:
- If an arrangement already exists, use it directly
- Initialize random number generator with the provided seed
- Process mutex constraints to lock specific layer types
- For each template section:
- Filter available layers based on group constraints
- Apply mutex constraints to prevent conflicting layers
- Include required layers specified in inclusions
- Exclude forbidden layers specified in exclusions
- Randomly select remaining layers based on weights until layerCount is reached
- Return the complete arrangement with all selected layers
Audio Processing
Master Processing Chain
BeatBlocks implements a professional-grade audio processing chain:
// Default dynamics settings
dynamics: {
compressor: {
threshold: -12.0,
knee: 12.0,
ratio: 2.0,
attack: 0.003,
release: 0.25
},
limiter: {
threshold: -3.0,
knee: 0.0,
ratio: 20.0,
attack: 0.003,
release: 0.25
}
}
The processing chain consists of:
- Compressor: Balances dynamic range
- Master Gain: Controls overall volume
- Stereo Panner: Manages stereo field
- Limiter: Prevents clipping
Audio Encoding
BeatBlocks uses Opus audio encoding for efficient on-chain storage of audio layers:
Opus Encoding
Opus provides high-quality audio at extremely low bitrates, making it ideal for blockchain storage:
// Convert audio buffer to Opus format
async function encodeToOpus(audioBuffer, options = {}) {
const { channels = 1, sampleRate = 48000, bitRate = 64000 } = options;
// Create encoder
const encoder = new OpusEncoder(sampleRate, channels, 'audio');
encoder.setBitrate(bitRate);
// Get audio data
const audioData = audioBuffer.getChannelData(0);
// Convert to Int16 format required by Opus
const int16Data = new Int16Array(audioData.length);
for (let i = 0; i < audioData.length; i++) {
int16Data[i] = Math.max(-1, Math.min(1, audioData[i])) * 0x7FFF;
}
// Encode to Opus
const opusData = encoder.encode(int16Data);
// Create Ogg container
const oggData = new Uint8Array(opusData.length + 28); // 28 bytes for Ogg header
// Write Ogg header
// ... (Ogg header writing code) ...
return oggData;
}
Opus Decoding
Decoding Opus files for playback:
// Decode Opus audio for playback
async function decodeOpusFile(opusData) {
// Create decoder
const decoder = new OpusDecoder();
await decoder.ready;
// Initialize decoder with Opus data
decoder.decode(opusData);
// Get decoded PCM data
const decodedData = decoder.decode_float();
// Create audio buffer
const audioBuffer = audioContext.createBuffer(1, decodedData.length, 48000);
const channelData = audioBuffer.getChannelData(0);
// Copy decoded data to audio buffer
channelData.set(decodedData);
return audioBuffer;
}
External Audio Libraries
BeatBlocks uses the following external libraries for cross-browser audio support:
ogg-opus-decoder
Used for decoding Opus audio in browsers that don't natively support it. This library is loaded from an Ordinals inscription:
<script src="https://fractal-static.unisat.io/content/65f6f2660882264d37035dea68818887ccb02afb8a59788fce886d072af4d200i0"></script>
The decoder is used to convert Opus-encoded audio to PCM format that can be played by the Web Audio API.
lame.min.js
Used for MP3 encoding when exporting compositions. This library is also loaded from an Ordinals inscription:
<script src="https://fractal-static.unisat.io/content/ec91336c8c4edeedae5c1d35dbe3c2551270de17a7261351f45665f211881789i0"></script>
The LAME encoder is used when downloading compositions as MP3 files, providing better compatibility across devices compared to WAV exports.
Note: These libraries are loaded dynamically from the blockchain, ensuring that the entire BeatBlocks system remains fully on-chain without external dependencies.
Audio Export
BeatBlocks can export compositions as WAV files:
// WAV encoding
encodeWAV(samples, numChannels, sampleRate, bitDepth) {
const bytesPerSample = bitDepth / 8;
const blockAlign = numChannels * bytesPerSample;
const buffer = new ArrayBuffer(44 + samples.length * bytesPerSample);
const view = new DataView(buffer);
// Write WAV header
this.writeString(view, 0, 'RIFF');
view.setUint32(4, 36 + samples.length * bytesPerSample, true);
this.writeString(view, 8, 'WAVE');
// ... additional header data ...
// Write audio samples
this.floatTo16BitPCM(view, 44, samples);
return new Blob([view], { type: 'audio/wav' });
}
Example Implementation
Below is a simplified example of how to use the BeatBlocks protocol:
// Initialize BeatBlock
const audioContext = new AudioContext();
const beatBlock = new BeatBlock(audioContext);
// Load song data
const songData = {
details: {
title: "Example Song",
author: "BeatBlocks Developer",
bpm: 120
},
generationConfig: {
seed: 12345,
groups: ["main", "alt"],
mutexes: ["drums", "bass"]
},
layers: [
{
id: "drums-1",
name: "Basic Beat",
loopLength: 4,
path: "/content/example-drums-1.opus",
volume: 1,
loop: true,
alignment: "start",
groups: ["main"],
mutex: ["drums"],
weight: 1
},
// Additional layers...
],
template: [
{
length: 8,
layerCount: 3,
inclusions: ["drums"],
exclusions: []
},
// Additional template sections...
]
};
// Initialize and play
async function setupAndPlay() {
await beatBlock.initialize(songData);
await beatBlock.play();
}
setupAndPlay();
For a complete implementation example, see the Creating BeatBlocks guide.
Ordinals Integration
BeatBlocks are designed to be inscribed on the Bitcoin blockchain using Ordinals:
Layer Storage
Each audio layer is stored as an individual inscription with a unique content ID. Layers are typically encoded as Opus audio files for efficient storage.
Example path format:/content/[inscription_id]
Composition Storage
The complete BeatBlock composition is stored as a JSON inscription that references the individual layer inscriptions.
The composition can be either a template (for generative music) or a fully arranged piece.
Ordinals API Integration
BeatBlocks can query child inscriptions using the Ordinals API:
export type OrdChildrenResponse = {
children: Array<{
charms: Array<string>
fee: number
height: number
id: string
number: number
output: string
sat: any
satpoint: string
timestamp: number
}>
more: boolean
page: number
}
The protocol includes utilities for loading remote scripts and resources from Ordinals:
async function loadRecentChildScript(parentInscriptionId, baseUrl) {
try {
const response = await fetch(
`${baseUrl}/api/inscription/${parentInscriptionId}/children`
);
const data = await response.json();
if (data.children && data.children.length > 0) {
// Sort by timestamp (newest first)
const sortedChildren = data.children.sort(
(a, b) => b.timestamp - a.timestamp
);
// Load the most recent child script
const mostRecentChild = sortedChildren[0];
await loadRemoteScript(
`${baseUrl}/content/${mostRecentChild.id}`
);
return mostRecentChild.id;
}
} catch (error) {
console.error("Failed to load child script:", error);
throw error;
}
}