159 lines
5.6 KiB
JavaScript
159 lines
5.6 KiB
JavaScript
|
const sodium = require('libsodium-wrappers');
|
||
|
const { Client, GatewayIntentBits } = require('discord.js');
|
||
|
const { joinVoiceChannel, getVoiceConnection, VoiceConnectionStatus, EndBehaviorType, createAudioPlayer, createAudioResource, VoiceReceiver } = require('@discordjs/voice');
|
||
|
const fs = require('fs');
|
||
|
const { pipeline } = require('stream');
|
||
|
const { OpusEncoder } = require('@discordjs/opus'); // Opusデコーダーをインポート
|
||
|
const { exec } = require('child_process');
|
||
|
|
||
|
sodium.ready.then(() => {
|
||
|
console.log('Sodium library loaded!');
|
||
|
});
|
||
|
|
||
|
const client = new Client({
|
||
|
intents: [
|
||
|
GatewayIntentBits.Guilds,
|
||
|
GatewayIntentBits.GuildVoiceStates,
|
||
|
GatewayIntentBits.GuildMessages,
|
||
|
GatewayIntentBits.MessageContent,
|
||
|
],
|
||
|
});
|
||
|
|
||
|
client.once('ready', () => {
|
||
|
console.log('Bot is ready!');
|
||
|
});
|
||
|
|
||
|
client.on('messageCreate', async message => {
|
||
|
if (message.content === '!join') {
|
||
|
if (message.member.voice.channel) {
|
||
|
const connection = joinVoiceChannel({
|
||
|
channelId: message.member.voice.channel.id,
|
||
|
guildId: message.guild.id,
|
||
|
adapterCreator: message.guild.voiceAdapterCreator,
|
||
|
selfDeaf: false,
|
||
|
selfMute: false,
|
||
|
});
|
||
|
|
||
|
connection.on(VoiceConnectionStatus.Ready, () => {
|
||
|
console.log('Bot has connected to the channel!');
|
||
|
startRecording(connection, message.member.voice.channel);
|
||
|
});
|
||
|
} else {
|
||
|
message.reply('ボイスチャンネルに接続してください。');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (message.content === '!leave') {
|
||
|
const connection = getVoiceConnection(message.guild.id);
|
||
|
if (connection) {
|
||
|
connection.destroy();
|
||
|
message.reply('ボイスチャンネルを離れました。');
|
||
|
} else {
|
||
|
message.reply('ボイスチャンネルに接続していません。');
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
function startRecording(connection, voiceChannel) {
|
||
|
const receiver = connection.receiver;
|
||
|
const activeStreams = new Map(); // ユーザーごとの音声ストリーム管理
|
||
|
|
||
|
receiver.speaking.on('start', (userId) => {
|
||
|
const user = voiceChannel.members.get(userId);
|
||
|
if (!user) {
|
||
|
console.error('User is undefined');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
console.log(`Started receiving audio from user: ${user.id}`);
|
||
|
|
||
|
// すでに音声ストリームが存在する場合はスキップ
|
||
|
if (activeStreams.has(user.id)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const audioStream = receiver.subscribe(user.id, {
|
||
|
end: EndBehaviorType.Manual
|
||
|
});
|
||
|
|
||
|
const filePath = `./recordings/${user.id}-${Date.now()}.pcm`;
|
||
|
const writableStream = fs.createWriteStream(filePath);
|
||
|
activeStreams.set(user.id, { audioStream, writableStream, filePath });
|
||
|
|
||
|
// OpusデータをデコードしてPCMデータとして書き込む
|
||
|
const opusDecoder = new OpusEncoder(48000, 2); // Opusデコーダーを使用
|
||
|
audioStream.on('data', (chunk) => {
|
||
|
try {
|
||
|
const pcmData = opusDecoder.decode(chunk);
|
||
|
writableStream.write(pcmData);
|
||
|
console.log(`Received ${chunk.length} bytes of audio data from user ${user.id}`);
|
||
|
} catch (err) {
|
||
|
console.error('Error decoding audio chunk:', err);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// エラーハンドリング
|
||
|
audioStream.on('error', (err) => {
|
||
|
console.error('Error in the audio stream:', err);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
receiver.speaking.on('end', (userId) => {
|
||
|
const streamData = activeStreams.get(userId);
|
||
|
if (!streamData) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const { writableStream, filePath } = streamData;
|
||
|
|
||
|
// ストリームを終了し、ファイルを書き込み完了後に変換
|
||
|
writableStream.end(() => {
|
||
|
console.log(`Recording saved to ${filePath}`);
|
||
|
activeStreams.delete(userId);
|
||
|
|
||
|
// ファイルが空でないことを確認してから変換
|
||
|
fs.stat(filePath, (err, stats) => {
|
||
|
if (err) {
|
||
|
console.error('Error checking file:', err);
|
||
|
return;
|
||
|
}
|
||
|
if (stats.size > 0) {
|
||
|
convertToWavInBackground(filePath); // バックグラウンドで変換を実行
|
||
|
} else {
|
||
|
console.log(`File ${filePath} is empty, skipping conversion.`);
|
||
|
fs.unlinkSync(filePath); // 空のファイルを削除
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function convertToWavInBackground(pcmPath) {
|
||
|
const wavPath = pcmPath.replace('.pcm', '.wav');
|
||
|
console.log(`Converting ${pcmPath} to ${wavPath} in the background`);
|
||
|
|
||
|
// バックグラウンドで変換を実行
|
||
|
exec(`ffmpeg -f s16le -ar 48000 -ac 2 -i ${pcmPath} ${wavPath}`, (error, stdout, stderr) => {
|
||
|
if (error) {
|
||
|
console.error(`Error during conversion: ${error.message}`);
|
||
|
return;
|
||
|
}
|
||
|
if (stderr) {
|
||
|
console.error(`FFmpeg stderr: ${stderr}`);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// 変換が成功したらPCMファイルを削除
|
||
|
console.log(`Successfully converted ${pcmPath} to ${wavPath}`);
|
||
|
fs.unlink(pcmPath, (unlinkError) => {
|
||
|
if (unlinkError) {
|
||
|
console.error(`Error deleting PCM file: ${unlinkError.message}`);
|
||
|
} else {
|
||
|
console.log(`Deleted PCM file: ${pcmPath}`);
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
|
||
|
client.login(process.env.TOKEN);
|