add nbs and txt support for music

This commit is contained in:
ChomeNS 2022-12-30 15:40:17 +07:00
parent e78cda6373
commit 94517b0a63
7 changed files with 273 additions and 9 deletions

View file

@ -3,5 +3,6 @@
"1.19 Support",
"*music play file autocomplete",
"Improved URL loading in *draw and *music playurl",
"You can now do *clearchat [player] without \"specific\""
"You can now do *clearchat [player] without \"specific\"",
"Added .nbs and .txt support for music command"
]

View file

@ -3,6 +3,7 @@
const fs = require('fs/promises')
const { EmbedBuilder } = require('discord.js')
const path = require('path')
const getFilenameFromUrl = require('../util/getFilenameFromUrl')
const fileExists = require('../util/file-exists')
const fileList = require('../util/file-list')
const axios = require('axios')
@ -31,7 +32,7 @@ async function play (bot, values, discord, channeldc, selector, config) {
filepath !== '') absolutePath = await resolve(file)
else absolutePath = await resolve(filepath)
song = bot.music.load(await fs.readFile(absolutePath), path.basename(absolutePath))
song = await bot.music.load(await fs.readFile(absolutePath), path.basename(absolutePath))
bot.music.queue.push(song)
bot.music.play(song)
if (discord) {
@ -44,6 +45,7 @@ async function play (bot, values, discord, channeldc, selector, config) {
bot.tellraw(selector, [{ text: 'Added ', color: 'white' }, { text: song.name, color: 'gold' }, { text: ' to the song queue', color: 'white' }])
}
} catch (e) {
console.log(e)
if (discord) {
const Embed = new EmbedBuilder()
.setColor(config.discord.embedsColors.error)
@ -66,7 +68,7 @@ async function playUrl (bot, values, discord, channeldc, selector, config) {
},
responseType: 'arraybuffer'
})
song = bot.music.load(response.data, url)
song = await bot.music.load(response.data, getFilenameFromUrl(url))
bot.music.queue.push(song)
bot.music.play(song)
if (discord) {

View file

@ -1,6 +1,8 @@
const path = require('path')
const { Midi } = require('@tonejs/midi')
const { convertMidi } = require('../util/midi_converter')
const convertNBS = require('../util/nbs_converter')
const parseTXTSong = require('../util/txt_song_parser')
const soundNames = {
harp: 'minecraft:block.note_block.harp',
@ -79,11 +81,22 @@ function inject (bot) {
clearInterval(interval)
})
bot.music.load = function (buffer, fallbackName = '[unknown]') {
// TODO: NBS Support
const midi = new Midi(buffer)
const song = convertMidi(midi)
if (song.name === '') song.name = fallbackName
bot.music.load = async function (buffer, fallbackName = '[unknown]') {
let song
switch (path.extname(fallbackName)) {
case '.nbs':
song = convertNBS(buffer)
break
case '.txt':
song = parseTXTSong(buffer.toString())
break
default:
// eslint-disable-next-line no-case-declarations
const midi = new Midi(buffer)
song = convertMidi(midi)
if (song.name === '') song.name = fallbackName
break
}
return song
}

View file

@ -0,0 +1,14 @@
const path = require('path')
/**
* get filename from url
* @param {string} urlStr the url
* @return {string} filename
* @example
* getFilenameFromUrl('https://sus.red/amogus.mid?verysus=true') // returns 'amogus.mid'
*/
function getFilenameFromUrl (urlStr) {
const url = new URL(urlStr)
return path.basename(url.pathname)
}
module.exports = getFilenameFromUrl

62
util/nbs_converter.js Normal file
View file

@ -0,0 +1,62 @@
const nbs = require('./nbs_file')
const instrumentNames = [
'harp',
'bass',
'basedrum',
'snare',
'hat',
'guitar',
'flute',
'bell',
'chime',
'xylophone',
'iron_xylophone',
'cow_bell',
'didgeridoo',
'bit',
'banjo',
'pling'
]
function convertNBS (buf) {
const parsed = nbs.parse(buf)
const song = {
name: parsed.songName,
notes: [],
loop: false,
loopPosition: 0,
length: 0
}
if (parsed.loop > 0) {
song.loop = true
song.loopPosition = parsed.loopStartTick
}
for (const note of parsed.nbsNotes) {
let instrument = note.instrument
if (note.instrument < instrumentNames.length) {
instrument = instrumentNames[note.instrument]
} else continue
if (note.key < 33 || note.key > 55) continue
const layerVolume = 100
// will add layer volume later
const time = tickToMs(note.tick, parsed.tempo)
song.length = Math.max(song.length, time)
song.notes.push({
instrument,
pitch: note.key - 33,
volume: note.velocity * layerVolume / 10000,
time
})
}
return song
}
function tickToMs (tick = 1, tempo) {
return Math.floor(1000 * tick * 100 / tempo)
}
module.exports = convertNBS

157
util/nbs_file.js Normal file
View file

@ -0,0 +1,157 @@
function parse (buffer) {
let i = 0
let songLength = 0
let format = 0
let vanillaInstrumentCount = 0
songLength = readShort()
if (songLength === 0) {
format = readByte()
}
if (format >= 1) {
vanillaInstrumentCount = readByte()
}
if (format >= 3) {
songLength = readShort()
}
const layerCount = readShort()
const songName = readString()
const songAuthor = readString()
const songOriginalAuthor = readString()
const songDescription = readString()
const tempo = readShort()
const autoSaving = readByte()
const autoSavingDuration = readByte()
const timeSignature = readByte()
const minutesSpent = readInt()
const leftClicks = readInt()
const rightClicks = readInt()
const blocksAdded = readInt()
const blocksRemoved = readInt()
const origFileName = readString()
let loop = 0
let maxLoopCount = 0
let loopStartTick = 0
if (format >= 4) {
loop = readByte()
maxLoopCount = readByte()
loopStartTick = readShort()
}
const nbsNotes = []
let tick = -1
while (true) {
const tickJumps = readShort()
if (tickJumps === 0) break
tick += tickJumps
let layer = -1
while (true) {
const layerJumps = readShort()
if (layerJumps === 0) break
layer += layerJumps
const note = nbsNote()
note.tick = tick
note.layer = layer
note.instrument = readByte()
note.key = readByte()
if (format >= 4) {
note.velocity = readByte()
note.panning = readByte()
note.pitch = readShort()
}
nbsNotes.push(note)
}
}
const nbsLayers = []
if (i <= buffer.length) {
for (let j = 0; j < layerCount; j++) {
const layer = nbsLayer()
layer.name = readString()
if (format >= 4) {
layer.lock = readByte()
}
layer.volume = readByte()
if (format >= 2) {
layer.stereo = readByte()
}
nbsLayers.push(layer)
}
}
return {
songLength,
format,
vanillaInstrumentCount,
layerCount,
songName,
songAuthor,
songOriginalAuthor,
songDescription,
tempo,
autoSaving,
autoSavingDuration,
timeSignature,
minutesSpent,
leftClicks,
rightClicks,
blocksAdded,
blocksRemoved,
origFileName,
loop,
maxLoopCount,
loopStartTick,
nbsNotes,
nbsLayers
}
function readByte () {
return buffer.readInt8(i++)
}
function readShort () {
const short = buffer.readInt16LE(i)
i += 2
return short
}
function readInt () {
const int = buffer.readInt32LE(i)
i += 4
return int
}
function readString () {
let length = readInt()
let string = ''
for (; length > 0; length--) string += String.fromCharCode(readByte())
return string
}
}
function nbsNote () {
return {
tick: null,
layer: null,
instrument: null,
key: null,
velocity: 100,
panning: 100,
pitch: 0
}
}
function nbsLayer () {
return {
name: null,
lock: 0,
volume: null,
stereo: 100
}
}
module.exports = { parse, nbsNote, nbsLayer }

15
util/txt_song_parser.js Normal file
View file

@ -0,0 +1,15 @@
const { instrumentsArray } = require('minecraft-data')('1.15.2') // chip hardcoding moment
function parseTXTSong (data) {
let length = 0
const notes = String(data).split(/\r\n|\r|\n/).map(line => {
const [tick, pitch, instrument] = line.split(':').map(Number)
if (tick === undefined || pitch === undefined || instrument === undefined) return undefined
const time = tick * 50
length = Math.max(length, time)
return { time, pitch, instrument: instrumentsArray[instrument].name, volume: 1 }
}).filter(note => note !== undefined)
return { name: '', notes, loop: false, loopPosition: 0, length }
}
module.exports = parseTXTSong