ボイチャを音声として取得できるようになった

This commit is contained in:
ntki72 2024-09-08 22:13:51 +09:00
commit 6abc475db9
7 changed files with 1501 additions and 0 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text eol=lf

144
.gitignore vendored Normal file
View File

@ -0,0 +1,144 @@
# Created by https://www.toptal.com/developers/gitignore/api/node
# Edit at https://www.toptal.com/developers/gitignore?templates=node
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
# End of https://www.toptal.com/developers/gitignore/api/node

18
Dockerfile Normal file
View File

@ -0,0 +1,18 @@
# ベースイメージ
FROM node:18
# ffmpegをインストール
RUN apt-get update && apt-get install -y ffmpeg
# 作業ディレクトリの設定
WORKDIR /usr/src/app
# package.json と package-lock.json をコピーしてインストール
COPY package*.json ./
RUN npm install
# bot.jsと他の必要なファイルをコンテナにコピー
COPY . .
# Botを実行
CMD [ "node", "bot.js" ]

158
bot.js Normal file
View File

@ -0,0 +1,158 @@
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);

9
docker-compose.yml Normal file
View File

@ -0,0 +1,9 @@
version: '3'
services:
discord-bot:
build: .
container_name: discord-voice-bot
environment:
- TOKEN=MTI0MDY1MDc3NTE2MDY4NDU1NA.Gl_Z2X.SBWCjk3OzIViNbSXocoPBt_817BZQ12fONLVSY
volumes:
- ./recordings:/usr/src/app/recordings

1152
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

19
package.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "discord-voice-bot",
"version": "1.0.0",
"description": "Discord bot to record voice and convert to wav",
"main": "bot.js",
"dependencies": {
"@discordjs/opus": "^0.5.3",
"@discordjs/voice": "^0.14.0",
"discord.js": "^14.16.1",
"ffmpeg-static": "^4.4.1",
"libsodium-wrappers": "^0.7.10"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}