Last active
December 23, 2020 22:51
-
-
Save OverlappingElvis/09a6d402c2715c12489be5d3808533c8 to your computer and use it in GitHub Desktop.
midi-to-blobs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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() |
I'm confused, how do you get the blobs to sing with the test.json
file?
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)
This must now be called with a midi file argument: node index.js <song.mid>
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
I think this might be getting out of hand