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);