Skip to content

hanhee-song/marble_machine

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Marble Machine

NB: best run on Chrome with headphones connected via 3.5mm. Otherwise, check out the video demo.

Live Site

Sample Song 1

Sample Song 2

Sample Song A B C

About Marble Machine

The core mechanics of this client-side JavaScript app was built in 24 hours using JavaScript and React. The React component rendering process is tightly controlled and 100% optimized for minimal rerendering and reconciliation.

Marble Machine has been inspired by Wintergatan. As a musician myself (violin, piano, orchestra conducting, guitar, and bass) with prior experience in creating looped electronic/acoustic music, I set out to combine my passion for programming and my love for music in one app.

Known Compatibility Issues
  • Does not work in Firefox due to the way Firefox handles (or rather, is unable to handle) a large number of concurrent audio files
  • Does not work with most Bluetooth audio devices for the same reason given above

Instrument API

The Instrument class has the following methods:

constructor(mm)

Initialize the instrument by passing in a number of measures. This can be changed later.

setMm(mm)

Changes the instrument's mm.

getName()

Returns the instrument's name.

Render methods

allNotes()

Returns an array of all of its tones

getNotes(beat)

Returns all of the notes that it will play at a given beat

getLine(note)

Returns an array of booleans of length mm representing the presence of the given note. This is for the React line component.

setUpdateComponentCallback(callback, note)

The Instrument will remember the given callback for the specific note. This is to allow components to force a rerender of the component that stores the instrument's notes in its state.

Example:

updateNotes() {
  this.setState({
    muted: this.props.instrument.isMuted(this.props.note),
    line: this.props.instrument.getLine(this.props.note),
  });
}

this.props.instrument.setUpdateComponentCallback(this.updateNotes, this.props.note);
updateComponents([note])

The instrument will invoke all of its callbacks. If a note is passed in, the Instrument will invoke all the callbacks for the given note.

Example

handleAddNote() {
  this.props.instrument.addNote();
  this.props.instrument.updateComponents(this.props.note)
}

Interface methods

addNote(note, beat)

Adds the note to the given beat.

removeNote(note, beat)

Removes the note from the given beat.

playNote(note)

Plays the note. This method is only for when the user is clicking on the GUI and expects audio feedback after placing a note.

playAtBeat(beat)

Plays all the notes at the given beat.

clearAllNotes()

Wipes the board clean.

History

getMostRecentHistory()

Returns the timestamp of the most recent change.

historyPop()

Undo the most recent change to the given instrument

Example of history handling:

handleUndo(e) {
  // Hash of { timestamps: [inst, inst] }
  const instrumentUndos = {};
  
  // I use an array because a simultaneous change (namely, hitting
  // that undo button) may result in the same timestamp for multiple instruments
  this.state.instruments.forEach((instrument) => {
    const mostRecent = instrument.getMostRecentHistory();
    if (mostRecent) {
      if (instrumentUndos[mostRecent]) {
        instrumentUndos[mostRecent].push(instrument);
      } else {
        instrumentUndos[mostRecent] = [instrument];
      }
    }
  });
  
  // We're going to put in here the most recent timestamp's instruments
  // plus we'll check the other timestamps to see if they're within
  // about 10 miliseconds
  let instrumentsToReverse = [];
  
  // Get the most recent timestamp
  const keys = Object.keys(instrumentUndos).map(time => Number(time));
  const max = Math.max.apply(null, keys);
  // instrumentsToReverse = instrumentsToReverse.concat(instrumentUndos[max]);
  
  // Iterate over all keys, put anything within 10 miliseconds in the arr
  keys.forEach(timestamp => {
    if (max - timestamp < 10) {
      let instruments = instrumentUndos[timestamp];
      instrumentsToReverse = instrumentsToReverse.concat(instruments);
    }
  });
  
  instrumentsToReverse = [...new Set(instrumentsToReverse)];
  
  if (instrumentsToReverse.length > 0) {
    instrumentsToReverse.forEach(instrument => {
      instrument.historyPop();
      instrument.updateComponents();
    });
  }
}

Mute

mute([note])

Mutes all notes. If a note is given, mutes only the given note.

unmute([note])

Unmutes all notes. If a note is given, unmutes only the given note.

isMuted([note])

Returns a boolean - true if all notes are muted and false if there is at least one unmuted note. If a note is given, returns a boolean for whether or not the given note is muted.

Import/Export

exportJSON()

Exports its internal states as a stringified JSON.

importJSON(json)

Takes in either a JSON or a stringified JSON and updates itself to the new data. Will break horribly if an invalid JSON is given.

Setting up a subclass of the instrument

Here is an example of setting up a subclass. Each instrument must have its own setup method in which it sets its this.notes array and builds up the this.sounds hash with audio elements. In the constructor, this.name must be set. Preloading the audio is optional.

import Instrument from './instrument';

class Vibraphone extends Instrument {
  constructor(props) {
    super(props);
    // this.notes
    this.setup();
    this.name = "Vibraphone";
    this._preloadAudio();
  }
  
  setup() {
    this.notes = [
      "0e", "0fs",
      "1g", "1a", "1b", "1c", "1d", "1e", "1fs",
      "2g", "2a", "2b", "2c", "2d", "2e", "2fs",
      "3g", "3a", "3b", "3c", "3d", "3e", "3fs",
      "4g", "4a", "4b", "4c"
    ].reverse();
    this.notes.forEach((note) => {
      this.sounds[note] = new Audio(`audio/vib_${note}.wav`);
      this.sounds[note].url = `public/audio/vib_${note}.wav`;
      this.sounds[note].volume = 0.16;
    });
  }
}

export default Vibraphone;

About

It's super cool. You have to listen to it. You really do.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published