Cover photo

Making Music from Ethereum Blocks

KnowVember day 2 - Controlling Supercollider with external events

Its only day two and I'm already regretting this decision to write on a new topic every day. Thats not to say I'm going to stop, but I am utterly exhausted from being that guy who helps their friends move apartments. Friends who live on third floors, nonetheless.

But I cant let exhaustion stop me on day two of a month-long challenge! How sad would that be? I will push through and learn something new and teach it in the process.

Today's challenge

In today's post I'll once again make the computer sing with code. This time, the score will be set by the Ethereum blockchain.

How? To put it simply, write a node script which keeps checking for the latest block and whenever it gets a new one, it sends data about it to supercollider. Supercollider will have to have some musical routine that takes this data into account and adjusts the music accordingly.

Lets get started!

Node.js

Getting started, I make a new project directory and npm init -y. Then some dependencies: npm install dotenv ethers node-osc. For getting Ethereum data from Infura, and sending to SC with Open Sound Control (OSC) messages.

Heres the code, broken into three files:

// index.js
require('dotenv').config();
const ethers = require("ethers");
const { processBlockData } = require('./processBlockData');
const { sendOSCMessage } = require('./sendOSCMessage');

const url = `https://mainnet.infura.io/v3/${process.env.INFURA_KEY}`;
const provider = new ethers.JsonRpcProvider(url);

let lastBlockData = {
  number: -1,
  time: 0, 
  gasUsed: '0'
};

function checkForNewBlock() {
  provider.getBlockNumber().then((currentBlockNumber) => {
    if (currentBlockNumber > lastBlockData.number) {
      console.log(`New block found: ${currentBlockNumber}`);
      provider.getBlock(currentBlockNumber).then((blockData) => {
        const timeSinceLastBlock = blockData.timestamp - (lastBlockData.time || blockData.timestamp);
        const gasUsedInGwei = ethers.formatUnits(blockData.gasUsed, 'gwei');

        lastBlockData = {
          number: currentBlockNumber,
          time: blockData.timestamp,
          gasUsed: gasUsedInGwei
        };

        const processedData = processBlockData(timeSinceLastBlock, gasUsedInGwei);
        sendOSCMessage(processedData);

      }).catch((error) => {
        console.error("Error fetching block data:", error);
      });
    } else {
      console.log(`No new block yet. Current block number: ${currentBlockNumber}`);
    }
  }).catch((error) => {
    console.error("Error fetching block number:", error);
  });
}

setInterval(checkForNewBlock, 2000);
// processBlockData.js
function processBlockData(timeSinceLastBlock, gasUsedInGwei) {
  console.log(`Time since last block: ${timeSinceLastBlock}s, Gas used: ${gasUsedInGwei} ETH`);
  return {
    timeSinceLastBlock,
    gasUsedInGwei
  };
}

module.exports = { processBlockData };
const { Client } = require("node-osc");

const oscClient = new Client('127.0.0.1', 57120);

function sendOSCMessage(data) {
  const message = {
    address: '/ethereum/block',
    args: [
      {
        type: 'f',
        value: data.timeSinceLastBlock
      },
      {
        type: 'f',
        value: parseFloat(data.gasUsedInGwei)
      }
    ]
  };

  console.log(`Sending OSC message:`, message);

  oscClient.send(message.address, ...message.args.map(arg => arg.value), (err) => {
    if (err) console.error('Error sending OSC message:', err);
    else console.log('OSC message sent');
  });
}

module.exports = { sendOSCMessage };

A quick breakdown of the code:

  • It checks every 2 seconds to see if a new block has been mined

  • If it finds a new block number, it fetches the block, stores the timestamp and gasUsed, and calculates the amount of time since the previous block was mined.

  • It then sends gasUsedInGwei and timeSinceLastBlock as an osc message defined as /ethereum/block to port 57120 which is the default port that sclang listens for messages on.

Supercollider

We can receive these messages in supercollider with an OSCdef.

(
OSCdef(\ethBlock, {|msg, time, addr, recvPort|

    var timeSinceLastBlock = msg[1].asFloat; // First argument
    var gasUsedInGwei = msg[2].asFloat;     // Second argument

    "Time since last block: %, Gas used: % Gwei".format(timeSinceLastBlock, gasUsedInGwei).postln;
    
}, '/ethereum/block');

)

This block is a lot easier to read. The argument msg is an array with the address pattern '/ethereum/block' in index 0 and arguments in index 1 and 2. The arguments are parsed and printed.

Both these scripts are ready to run and show communication between node and supercollider.

run node index.js in our node project directory, and in the SuperCollider IDE, put the cursor anywhere within the outermost parentheses and press Cmd+Enter (or Ctrl+Enter on a pc.)

Here is the output of my node script and of my SuperCollider script:

You know, maybe this is a Proof of Stake thing, but I didnt know that Eth blocks were so evenly spaced now. I guess when its not difficulty-driven, the block times can be more predictably even. Theres also the chance that its a problem with my code, but I'm not trying to write all night, so I guess we'll only use the second argument for our example this time!

Now that we have our communication between Node and SuperCollider, lets make some music with the only value of the two here that actually changes block to block - gas used. Gas changes a LOT based on gas prices, and I'm writing this at night during a bear market. So its incredibly low.

Our music should reflect the state of the market. If a lot of gas is being spent, the music should be more energetic. When its low like this, it should be calm. I'll keep it simple so as not to stay up too late doing this. Like I mentioned, I'm already exhausted, so it will be about as simple as it gets:

Assuming our node script is running, we can then start our SuperCollider code.

Start server by running line 1.

Then run the block between 4 and 22, which defines our Tick routine. The comments explain it clearly - this is a simple tick sound, the rate of which is set by the speed argument. The depth of the reverb is the reciprocal of the speed argument so that, for example, if the speed was 0.1 (one tick every ten seconds), the reverb would be ten seconds long.

Then we can define our OSCdef starting on line 25. On line 28 we convert our linear range of 0.001 ETH to 1000 ETH of gas used per block to an exponential range of 0.2 seconds to 200 seconds

Finally run the tick by executing the final line.

The thrilling conclusion is you will hear a reverbed ticking sound. Since its late at night in a bear market, it ticks every few seconds. But if this were run during Yuga Labs' Otherside sale, it would sound like a steady buzzing sound.

Conclusion

Using OSC messages and ProxySpaces, its possible to control changes in audio routines in realtime. This example may have been quite boring sounding, but it is the foundation for something much more interesting. I will likely explore this theme further in the next few days! I hope this has gotten you excited for the possibilities of using external sources to control music in SuperCollider. If not, thats okay, I'll be excited enough for both of us. Tomorrow. For now, its time to sleep.

Loading...
highlight
Collect this post to permanently own it.
mxjxn musings logo
Subscribe to mxjxn musings and never miss a post.