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:

  1. If an arrangement already exists, use it directly
  2. Initialize random number generator with the provided seed
  3. Process mutex constraints to lock specific layer types
  4. 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
  5. 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:

  1. Compressor: Balances dynamic range
  2. Master Gain: Controls overall volume
  3. Stereo Panner: Manages stereo field
  4. 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;
  }
}