ボイチャを音声として取得できるようになった
This commit is contained in:
commit
6abc475db9
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
* text eol=lf
|
144
.gitignore
vendored
Normal file
144
.gitignore
vendored
Normal 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
18
Dockerfile
Normal 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
158
bot.js
Normal 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
9
docker-compose.yml
Normal 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
1152
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
package.json
Normal file
19
package.json
Normal 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"
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user