Functioning audio bot

This commit is contained in:
Logan Cusano
2022-12-11 20:10:36 -05:00
parent 9ce846f1d9
commit ca9463856b
10 changed files with 142 additions and 122 deletions

View File

@@ -1,11 +1,12 @@
//Config
import { getTOKEN, getGuildID, getApplicationID } from './utilities/configHandler.js';
// Commands
import ping from './controllers/ping.js';
import { join, leave } from './controllers/voiceController.js';
import ping from './commands/ping.js';
import join from './commands/join.js';
import leave from './commands/leave.js';
// Debug
import Debug from 'debug';
const debug = Debug("bot:app");
import debugBuilder from "./utilities/debugBuilder.js";
const log = new debugBuilder("bot", "app");
// Modules
import { Client, GatewayIntentBits } from 'discord.js';
// Utilities
@@ -23,15 +24,13 @@ const client = new Client({
// When the client is connected and ready
client.on('ready', () =>{
debug(`${client.user.tag} is ready`)
console.log(`${client.user.tag} is ready`)
log.INFO(`${client.user.tag} is ready`)
});
/*
* Saved For later
client.on('messageCreate', (message) => {
debug(`Message Sent by: ${message.author.tag}\n\t'${message.content}'`);
console.log(`Message Sent by: ${message.author.tag}\n\t'${message.content}'`);
log.DEBUG(`Message Sent by: ${message.author.tag}\n\t'${message.content}'`);
});
*/
@@ -50,8 +49,8 @@ client.on('interactionCreate', (interaction) => {
break;
default:
interaction.reply({ content: 'Command not found, try one that exists', fetchReply: true })
.then((message) => console.log(`Reply sent with content ${message.content}`))
.catch(console.error);
.then((message) => log.DEBUG(`Reply sent with content ${message.content}`))
.catch((err) => log.ERROR(err));
}
}
})

View File

@@ -0,0 +1,35 @@
import {joinVoiceChannel} from "@discordjs/voice";
import {replyToInteraction} from "../utilities/messageHandler.js";
import {createAudioInstance} from "../controllers/audioController.js";
import OpusEncoderPkg from "@discordjs/opus";
// Declare the encoder (module is incompatible modern import method)
const { OpusEncoder } = OpusEncoderPkg;
const encoder = new OpusEncoder(48000, 2);
/**
* Join the specified voice channel
*
* @param interaction Message interaction from discord
*/
export default async function join(interaction){
const voiceChannel = interaction.options.getChannel('voicechannel');
const voiceConnection = joinVoiceChannel({
channelId: voiceChannel.id,
guildId: interaction.guildId,
adapterCreator: interaction.guild.voiceAdapterCreator,
selfMute: false,
selfDeaf: false,
});
replyToInteraction(interaction, `Ok, Joining ${voiceChannel.name}`);
const audioInstance = createAudioInstance();
audioInstance.on('data', buffer => {
buffer = Buffer.from(buffer);
const encoded = encoder.encode(buffer);
voiceConnection.playOpusPacket(encoded);
})
audioInstance.start();
}

View File

@@ -0,0 +1,18 @@
import {getVoiceConnection} from "@discordjs/voice";
import {replyToInteraction} from "../utilities/messageHandler.js";
// Debug
//import debugBuilder from "../utilities/debugBuilder.js";
//const log = new debugBuilder("bot", "leave");
/**
* If in a voice channel for the specified guild, leave
*
* @param interaction Message interaction from discord
*/
export default async function leave(interaction){
const guildId = interaction.guild.id;
const voiceConnection = getVoiceConnection(guildId);
if (!voiceConnection) return replyToInteraction(interaction, "Not in a voice channel.");
voiceConnection.destroy();
return replyToInteraction(interaction, `Goodbye`);
}

View File

@@ -1,16 +1,31 @@
// Config
import { getDeviceID, getDeviceName } from '../utilities/configHandler.js'
import {getDeviceID} from '../utilities/configHandler.js'
// Modules
import portAudio from 'naudiodon';
import {createAudioResource} from "@discordjs/voice";
// Debug
import debugBuilder from "../utilities/debugBuilder.js";
const log = new debugBuilder("bot", "audioController");
export function getAudioDevice({deviceName = undefined, deviceId = undefined}){
/**
* Checks to make sure the selected audio device is available and returns the device object (PortAudio Device Info)
* At least one option must be supplied, it will prefer ID to device name
*
* @param deviceName The name of the device being queried
* @param deviceId The ID of the device being queried
* @returns {unknown}
*/
export function confirmAudioDevice({deviceName = undefined, deviceId = undefined}){
const deviceList = getAudioDevices();
if (!deviceName && !deviceId) throw new Error("No device given");
if (deviceName) return deviceList.find(device => device.name === deviceName);
if (deviceId) return deviceList.find(device => device.id === deviceId);
if (deviceName) return deviceList.find(device => device.name === deviceName);
}
/**
* Return a list of the audio devices connected with input channels
*
* @returns {unknown[]}
*/
export function getAudioDevices(){
const deviceList = portAudio.getDevices().map((device) => {
if (device.defaultSampleRate === 48000 || device.maxInputChannels > 0) {
@@ -20,16 +35,21 @@ export function getAudioDevices(){
return null;
}
}).filter(Boolean);
console.log("Devices:", deviceList);
log.DEBUG("Device List: ", deviceList);
return deviceList;
}
/**
* Create and return the audio instance from the saved settings
* TODO Allow the client to save and load these settings dynamically
*
* @returns {IoStreamRead}
*/
export function createAudioInstance() {
//const resource = createAudioResource();
const selectedDevice = getAudioDevice({deviceId: getDeviceID()});//{deviceName: "VoiceMeeter VAIO3 Output (VB-Au"});
console.log(selectedDevice);
const selectedDevice = confirmAudioDevice({deviceId: getDeviceID()});//{deviceName: "VoiceMeeter VAIO3 Output (VB-Au"});
log.DEBUG("Device selected from config: ", selectedDevice);
// Create an instance of AudioIO with outOptions (defaults are as below), which will return a WritableStream
const audioInstance = new portAudio.AudioIO({
return new portAudio.AudioIO({
inOptions: {
channelCount: 2,
sampleFormat: portAudio.SampleFormat16Bit,
@@ -40,6 +60,4 @@ export function createAudioInstance() {
highwaterMark: 3840
},
});
//audioInstance.start();
return audioInstance;
}

View File

@@ -1,87 +0,0 @@
// Modules
import {
joinVoiceChannel,
getVoiceConnection,
createAudioResource,
createAudioPlayer,
NoSubscriberBehavior, StreamType
} from '@discordjs/voice';
import OpusEncoderPkg from "@discordjs/opus";
const { OpusEncoder } = OpusEncoderPkg;
// Utilities
import { replyToInteraction } from '../utilities/messageHandler.js';
import { createAudioInstance } from "./audioController.js";
/**
* Join the specified voice channel
*
* @param interaction Message interaction from discord
*/
export function join(interaction){
const voiceChannel = interaction.options.getChannel('voicechannel');
const voiceConnection = joinVoiceChannel({
channelId: voiceChannel.id,
guildId: interaction.guildId,
adapterCreator: interaction.guild.voiceAdapterCreator,
selfMute: false,
selfDeaf: false,
});
replyToInteraction(interaction, `Ok, Joining ${voiceChannel.name}`);
// Declare the encoder
const encoder = new OpusEncoder(48000, 2);
const player = createAudioPlayer({
behaviors: {
noSubscriber: NoSubscriberBehavior.Play,
},
});
const audioInstance = createAudioInstance();
audioInstance.on('data', buffer => {
buffer = Buffer.from(buffer);
const encoded = encoder.encode(buffer);
voiceConnection.playOpusPacket(encoded);
})
audioInstance.start();
}
/**
* If in a voice channel for the specified guild, leave
*
* @param interaction Message interaction from discord
*/
export function leave(interaction){
const guildId = interaction.guild.id;
const voiceConnection = getVoiceConnection(guildId);
if (!voiceConnection) return replyToInteraction(interaction, "Not in a voice channel.");
voiceConnection.destroy();
return replyToInteraction(interaction, `Goodbye`);
}
/*
* Brute forcing the required buffer size, 16 bit, 48KHz, 2ch = 480 Bytes (need to get frames still)
let notWorking = true;
let splitIndexInverse = 0
while (notWorking) {
try {
const newBuffer = buffer.slice(buffer.length - splitIndexInverse);
console.log(timestamp ,newBuffer, "end")
const encoded = encoder.encode(newBuffer);
console.log("Working Buffer Length" ,newBuffer.length)
console.log("Working Buffer" ,newBuffer)
notWorking = false
break;
} catch (err){
console.log(err)
if (splitIndexInverse >= buffer.length) {
notWorking = false
throw new Error("Shit fucked")
}
splitIndexInverse += 1
}
}
*/

View File

@@ -1,4 +1,7 @@
import { readFileSync } from 'fs';
// Debug
import debugBuilder from "./debugBuilder.js";
const log = new debugBuilder("bot", "configHandler");
export function getConfig() {
return JSON.parse(readFileSync("./config/botConfig.json"));
@@ -6,31 +9,40 @@ export function getConfig() {
export function getTOKEN() {
const parsedJSON = getConfig();
return parsedJSON.TOKEN;
const token = parsedJSON.TOKEN;
log.DEBUG("Discord API Token: ", token)
return token;
}
export function getGuildID() {
const parsedJSON = getConfig();
parsedJSON.GuildID = BigInt(parsedJSON.GuildID);
//console.log("Guild ID: ", parsedJSON.GuildID);
return parsedJSON.GuildID;
const guildID = BigInt(parsedJSON.GuildID);
log.DEBUG("Guild ID: ", guildID);
return guildID;
}
export function getApplicationID() {
const parsedJSON = getConfig();
parsedJSON.ApplicationID = BigInt(parsedJSON.ApplicationID);
//console.log("Application ID: ", parsedJSON.ApplicationID);
return parsedJSON.ApplicationID;
const appID = BigInt(parsedJSON.ApplicationID);
log.DEBUG("Application ID: ", appID);
return appID;
}
export function getDeviceID(){
const parsedJSON = getConfig();
//console.log("Device ID: ", parseInt(parsedJSON.DeviceID));
return parseInt(parsedJSON.DeviceID);
const deviceID = parseInt(parsedJSON.DeviceID);
log.DEBUG("Device ID: ", deviceID);
return deviceID;
}
export function getDeviceName(){
const parsedJSON = getConfig();
//console.log("Device Name: ", parseInt(parsedJSON.DeviceName));
return parsedJSON.DeviceName;
const deviceName = parsedJSON.DeviceName;
log.DEBUG("Device Name: ", deviceName);
return deviceName;
}

View File

@@ -0,0 +1,17 @@
// Debug
import Debug from 'debug';
export default class debugBuilder {
/**
* Create the different logging methods for a function
* Namespace template = ("[app]:[fileName]:['INFO', 'WARNING', 'DEBUG', 'ERROR']")
* @param {string} appName The name of the app to be used in the 'app' portion of the namespace
* @param {string} fileName The name of the file calling the builder to be used in the 'fileName' portion of the namespace
*/
constructor(appName, fileName) {
this.INFO = Debug(`${appName}:${fileName}:INFO`);
this.DEBUG = Debug(`${appName}:${fileName}:DEBUG`);
this.WARN = Debug(`${appName}:${fileName}:WARNING`);
this.ERROR = Debug(`${appName}:${fileName}::ERROR`);
}
}

View File

@@ -1,5 +1,9 @@
// Debug
import debugBuilder from "./debugBuilder.js";
const log = new debugBuilder("bot", "messageHandler");
export function replyToInteraction(interaction, message){
interaction.reply({ content: message, fetchReply: true })
.then((message) => console.log(`Reply sent with content ${message.content}`))
.catch(console.error);
.then((message) => log.DEBUG(`Reply sent with content ${message.content}`))
.catch((err) => log.ERROR(err));
}

View File

@@ -2,6 +2,9 @@ import {SlashCommandBuilder} from "@discordjs/builders";
import {REST} from "@discordjs/rest";
import {getApplicationID, getGuildID, getTOKEN} from "./configHandler.js";
import { Routes, ChannelType } from "discord.js";
// Debug
import debugBuilder from "./debugBuilder.js";
const log = new debugBuilder("bot", "registerCommands");
const pingCommand = new SlashCommandBuilder()
.setName("ping")
@@ -38,8 +41,9 @@ export default async function registerCommands(callback){
await rest.put(Routes.applicationGuildCommands(clientID, guildID), {
body: commands,
});
log.DEBUG("Successfully registered the following commands: ", commands)
callback();
} catch (err) {
console.log(err);
log.ERROR(err);
}
}