Skip to content

Instantly share code, notes, and snippets.

@OverlappingElvis
Last active December 23, 2020 22:51
Show Gist options
  • Save OverlappingElvis/09a6d402c2715c12489be5d3808533c8 to your computer and use it in GitHub Desktop.
Save OverlappingElvis/09a6d402c2715c12489be5d3808533c8 to your computer and use it in GitHub Desktop.
midi-to-blobs
// This script is deprecated, use https://github.com/OverlappingElvis/blob-opera-midi instead!
const fs = require(`fs`)
const _ = require(`lodash`)
const { Player } = require(`midi-player-js`)
const blessed = require(`blessed`)
const contrib = require(`blessed-contrib`)
const Alea = require(`alea`)
const VOWELS = _.range(4)
const CONSONANTS = _.range(5, 29)
const VOICES = [`Soprano`, `Mezzo-Soprano`, `Tenor`, `Bass`]
const getCurrentPhoneme = (tick, collection) => {
const prng = new Alea(tick)
const tickValue = Math.round(prng() * 10)
return collection[tickValue % collection.length]
}
const player = new Player()
const inputFile = process.argv[2]
if (_.isEmpty(inputFile) || _.last(inputFile.split(`.`)) !== `mid`) {
throw new Error(`Must provide a midi file.`)
}
player.loadFile(`./${inputFile}`)
const songTime = player.getSongTime()
const allEvents = player.getEvents()
const MAX_TICKS = _.last(_.maxBy(allEvents, (events) => _.last(events).tick)).tick
const noteEventsOnly = allEvents.map(track => track.filter(event => event.name === `Note on`)).filter(track => !_.isEmpty(track))
const timelines = noteEventsOnly.map((track, index) => {
return {
title: `Track ${index}`,
x: track.map(event => event.tick),
y: track.map(event => event.noteNumber),
style: {
line: _.times(3, () => Math.random() * 255)
}
}
})
const trackAssignments = [0, 1, 2, 3]
const screen = blessed.screen()
const grid = new contrib.grid({
rows: 8,
cols: 2,
screen: screen
})
const trackList = grid.set(0, 0, 3, 1, contrib.table, {
keys: true,
columnWidth: [14, 8]
})
const line = grid.set(0, 1, 3, 1, contrib.line, {
showLegend: true
})
const log = grid.set(4, 0, 1, 1, contrib.log)
const save = grid.set(4, 1, 1, 1, blessed.button, {
mouse: true,
content: `Export`
})
save.on(`press`, () => {
const parsedEvents = trackAssignments.map((trackIndex) => {
const track = noteEventsOnly[parseInt(trackIndex, 10)]
return track.reduce((memo, event) => {
if (!event.velocity) {
return memo
}
const timeSeconds = Math.abs((event.tick / MAX_TICKS) * songTime + (Math.random() * 0.025 * _.sample([1, -1])))
memo.push({
timeSeconds: timeSeconds,
midiPitch: event.noteNumber,
librettoChunk: {
vowel: {
name: getCurrentPhoneme(event.tick, VOWELS),
duration: 0.20000000298023224
},
suffix: [
{
name: getCurrentPhoneme(event.tick, CONSONANTS),
duration: 0.10000000149011612
}
]
}
})
return memo
}, [])
}).map((track) => {
return {
notes: track,
startSuffix: [
{
name: _.sample(CONSONANTS),
duration: 0.10000000149011612
}
]
}
})
const song = {
theme: 1,
parts: parsedEvents
}
fs.writeFile(`${inputFile}.json`, JSON.stringify(song), () => {
log.log(`Wrote song to ${inputFile}.json`)
screen.render()
})
})
line.setData(timelines)
screen.key(['escape', 'q', 'C-c'], function(ch, key) {
return process.exit(0);
})
trackList.focus()
const setTracklistData = () => {
trackList.setData({
headers: [`Part`, `Track`],
data: VOICES.map((val, index) => [val, trackAssignments[index]])
})
}
setTracklistData()
trackList.rows.on(`select`, function(event) {
let prompt = blessed.prompt({
left: `center`,
top: `center`,
height: `shrink`,
width: `shrink`,
border: `line`
})
screen.append(prompt)
prompt.input(`Set track number`, ``, (err, value) => {
log.log(`Assigning track ${value} to ${VOICES[event.index - 2]}`)
trackAssignments[event.index - 2] = value
prompt = null
setTracklistData()
screen.render()
})
})
log.log(`Started MIDI to Blob Opera.`)
screen.render()
@OverlappingElvis
Copy link
Author

I think this might be getting out of hand
image

@The-King-of-Toasters
Copy link

I'm confused, how do you get the blobs to sing with the test.json file?

@OverlappingElvis
Copy link
Author

I'm confused, how do you get the blobs to sing with the test.json file?

If you set a breakpoint in the blob opera script where sample songs are loaded, you can sideload your custom json (see https://twitter.com/Overlapping/status/1338979945792466944)

@OverlappingElvis
Copy link
Author

This must now be called with a midi file argument: node index.js <song.mid>

@OverlappingElvis
Copy link
Author

Check out https://github.com/OverlappingElvis/blob-opera-midi, now a standalone package

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment