first commit

This commit is contained in:
sim1222 2023-07-25 01:16:16 +09:00
commit b6abc9f526
25 changed files with 1030 additions and 0 deletions

25
.dockerignore Normal file
View File

@ -0,0 +1,25 @@
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/.idea
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/

View File

@ -0,0 +1,130 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoGeneratedRunConfigurationManager">
<projectFile>DiscordAudioBot/DiscordAudioBot.csproj</projectFile>
</component>
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="f4ab407f-7a01-4e71-a0f0-7d63ad2fa6c3" name="変更" comment="">
<change afterPath="$PROJECT_DIR$/.dockerignore" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.idea/.idea.DiscordAudioBot/.idea/workspace.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Elementary.sln" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Elementary/Config/Configuration.cs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Elementary/Dockerfile" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Elementary/Elementary.csproj" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Elementary/Program.cs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Elementary/appsettings.json" afterDir="false" />
<change afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="JSON File" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="HighlightingSettingsPerFile">
<setting file="file://$PROJECT_DIR$/Elementary/Config/Configuration.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/Elementary/Program.cs" root0="FORCE_HIGHLIGHTING" />
</component>
<component name="MarkdownSettingsMigration">
<option name="stateVersion" value="1" />
</component>
<component name="ProjectId" id="2Sdv0B6dZS0v9XvLddiZrHzEXEw" />
<component name="ProjectLevelVcsManager" settingsEditedManually="true" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"WebServerToolWindowFactoryState": "false",
"git-widget-placeholder": "master",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"settings.editor.selected.configurable": "preferences.lookFeel",
"vue.rearranger.settings.migration": "true"
},
"keyToStringList": {
"rider.external.source.directories": [
"C:\\Users\\user\\AppData\\Roaming\\JetBrains\\Rider2023.1\\resharper-host\\DecompilerCache",
"C:\\Users\\user\\AppData\\Roaming\\JetBrains\\Rider2023.1\\resharper-host\\SourcesCache",
"C:\\Users\\user\\AppData\\Local\\Symbols\\src"
]
}
}]]></component>
<component name="RunManager" selected=".NET プロジェクト.DiscordAudioBot">
<configuration name="DiscordAudioBot" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="" />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="" />
<option name="PASS_PARENT_ENVS" value="1" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Elementary/Elementary.csproj" />
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
<option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="" />
<method v="2">
<option name="Build" />
</method>
</configuration>
<configuration name="DiscordAudioBot/Dockerfile" type="docker-deploy" factoryName="dockerfile">
<deployment type="dockerfile">
<settings>
<option name="imageTag" value="discordaudiobot" />
<option name="containerName" value="discordaudiobot" />
<option name="contextFolderPath" value="G:\git\DiscordAudioBot" />
<option name="sourceFilePath" value="DiscordAudioBot/Dockerfile" />
</settings>
</deployment>
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="true" isSslEnabled="true" />
<method v="2" />
</configuration>
<configuration default="true" type="docker-deploy" factoryName="dockerfile" temporary="true">
<deployment type="dockerfile">
<settings />
</deployment>
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="true" isSslEnabled="false" />
<method v="2" />
</configuration>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="アプリケーションレベル" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="デフォルトタスク">
<changelist id="f4ab407f-7a01-4e71-a0f0-7d63ad2fa6c3" name="変更" comment="" />
<created>1689489047458</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1689489047458</updated>
<workItem from="1689489050574" duration="4153000" />
<workItem from="1689494655108" duration="2977000" />
<workItem from="1689502313585" duration="1275000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="VcsManagerConfiguration">
<option name="CLEAR_INITIAL_COMMIT_MESSAGE" value="true" />
</component>
</project>

31
Elementary.sln Normal file
View File

@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.6.33815.320
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elementary", "Elementary\Elementary.csproj", "{E853B381-09A7-46CF-88CC-62C44B5ACA89}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{E853B381-09A7-46CF-88CC-62C44B5ACA89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E853B381-09A7-46CF-88CC-62C44B5ACA89}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E853B381-09A7-46CF-88CC-62C44B5ACA89}.Debug|x64.ActiveCfg = Debug|x64
{E853B381-09A7-46CF-88CC-62C44B5ACA89}.Debug|x64.Build.0 = Debug|x64
{E853B381-09A7-46CF-88CC-62C44B5ACA89}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E853B381-09A7-46CF-88CC-62C44B5ACA89}.Release|Any CPU.Build.0 = Release|Any CPU
{E853B381-09A7-46CF-88CC-62C44B5ACA89}.Release|x64.ActiveCfg = Release|x64
{E853B381-09A7-46CF-88CC-62C44B5ACA89}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A79BA4-F57B-4B08-A752-906D0AF3800F}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,36 @@
using System.Diagnostics;
using NAudio;
using NAudio.Wave;
// using ManagedBass;
// using ManagedBass.Mix;
namespace Elementary.Audio;
public class AudioConverter
{
public readonly WaveFormat WaveFormat = new(48000, 16, 2);
public WaveFormatConversionStream CreateStreamFromFilePath(string path, float volume = 1.0f)
=> new(WaveFormat, new VolumeAdjustedWaveStream(new MediaFoundationReader(path), volume));
/// <summary>
/// Return PCM stream from any codec of audio stream
/// </summary>
/// <param name="stream"></param>
/// <returns></returns>
public WaveFormatConversionStream CreateStreamFromStream(Stream stream)
{
return new(WaveFormat, new StreamMediaFoundationReader(stream));
}
/// <summary>
/// Return PCM stream from any codec of audio stream
/// </summary>
/// <param name="stream"></param>
/// <param name="volume"></param>
/// <returns></returns>
public WaveFormatConversionStream CreateStreamFromStream(Stream stream, float volume = 1.0f)
{
return new(WaveFormat, new VolumeAdjustedWaveStream(new StreamMediaFoundationReader(stream), volume));
}
}

View File

@ -0,0 +1,93 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text.RegularExpressions;
using Discord;
using Discord.Audio;
using Discord.WebSocket;
using ManagedBass;
using Microsoft.Extensions.DependencyInjection;
using NAudio.Wave;
namespace Elementary.Audio;
public class AudioManager
{
private DiscordSocketClient _client;
private IServiceProvider _services;
private IAudioClient _audioClient;
private AudioOutStream _audioStream;
private SozaiAPI _sozaiAPI;
private VoicevoxAPI _voicevoxAPI;
private AudioConverter _audioConverter;
private PlaybackQueue _playbackQueue;
// private AudioMixer _audioMixer;
public bool isConnected;
public AudioManager(IServiceProvider services, DiscordSocketClient client, SozaiAPI sozaiApi,
VoicevoxAPI voicevoxApi)
{
_services = services;
_client = client;
_sozaiAPI = sozaiApi;
_voicevoxAPI = voicevoxApi;
_audioConverter = new();
}
public async Task JoinChannel(IVoiceChannel channel)
{
_audioClient = await channel.ConnectAsync(true);
_audioStream = _audioClient.CreatePCMStream(AudioApplication.Music, 128 * 1024);
_playbackQueue = _services.GetRequiredService<PlaybackQueue>();
// _audioMixer = new AudioMixer(_audioStream);
isConnected = true;
}
public async Task LeaveChannel()
{
await _audioClient.StopAsync();
}
public async Task PlayAudio(string path)
{
await using var wave = _audioConverter.CreateStreamFromFilePath(path, 0.15f);
// _audioMixer.AddStream(wave);
await wave.CopyToAsync(_audioStream);
}
public async Task PlayText(string text)
{
if (text.Contains("```")) text = "コードブロック";
if (text.StartsWith("!") || text.StartsWith(".")) return;
text = Regex.Replace(text, @"https?://[\w/:%#\$&\?\(\)~\.=\+\-]+", "URL");
float volume = 0.12f;
Stream? stream = await _sozaiAPI.GetAudioStream(text);
if (stream == null)
{
stream = await _voicevoxAPI.Speak(text);
volume = 0.8f;
if (stream == null) return;
}
await using var wave = _audioConverter.CreateStreamFromStream(stream, volume);
// _audioMixer.AddStream(wave);
await wave.CopyToAsync(_audioStream);
}
public async Task StopAudio()
{
// _audioMixer.Stop();
_playbackQueue.Flush();
await _audioStream.FlushAsync();
}
}

View File

@ -0,0 +1,80 @@
using ManagedBass;
using ManagedBass.Mix;
namespace Elementary.Audio;
public class AudioMixer
{
private readonly int _mixerStream;
private MemoryStream?[] _inputStreams;
private Stream _outStream;
private object _lock = new();
public AudioMixer(Stream outStream)
{
_outStream = outStream;
Bass.Init(-1, 44100, DeviceInitFlags.NoSpeakerAssignment, IntPtr.Zero);
_mixerStream = BassMix.CreateMixerStream(44100, 2, BassFlags.Float | BassFlags.MixerNonStop);
_inputStreams = new MemoryStream[10];
Bass.ChannelPlay(_mixerStream);
}
/// <summary>
/// Add a stream to the mixer.
/// wait for the stream to finish playing
/// </summary>
/// <param name="stream"></param>
/// <param name="volume"></param>
public void AddStream(Stream stream, float volume = 1.0f)
{
Console.WriteLine("Adding stream to mixer");
byte[] pcmBytes;
using (MemoryStream ms = new())
{
stream.CopyTo(ms);
pcmBytes = ms.ToArray();
}
lock (_lock)
{
for (var i = 0; i < _inputStreams.Length; i++)
{
if (_inputStreams[i] == null)
{
_inputStreams[i] = new MemoryStream(pcmBytes);
break;
}
}
}
var channel = Bass.CreateStream(pcmBytes, 0, pcmBytes.Length, BassFlags.Float);
Bass.ChannelSetAttribute(channel, ChannelAttribute.Volume, volume);
BassMix.MixerAddChannel(_mixerStream, channel, BassFlags.Default);
int length = Bass.ChannelGetData(_mixerStream, new byte[4096], 4096);
byte[] buffer = new byte[length];
length = Bass.ChannelGetData(_mixerStream, buffer, length);
_outStream.Write(buffer, 0, length);
}
public void Stop()
{
Bass.ChannelStop(_mixerStream);
}
public void Dispose()
{
lock (_lock)
{
foreach (var stream in _inputStreams)
{
stream?.Dispose();
}
}
_outStream.Flush();
Bass.StreamFree(_mixerStream);
Bass.Free();
}
}

View File

@ -0,0 +1,10 @@
using System.Diagnostics;
namespace Elementary.Audio;
public interface IAudioConverter
{
public Process CreateStreamFromFilePath(string path);
public Process CreateStreamFromStream(Stream stream);
}

View File

@ -0,0 +1,25 @@
using Discord.Audio;
using Discord.WebSocket;
namespace Elementary.Audio;
public class MessageHandler
{
// private IAudioClient _audioClient;
private PlaybackQueue _playbackQueue;
public MessageHandler(PlaybackQueue playbackQueue)
{
_playbackQueue = playbackQueue;
}
public async Task HandleMessage(SocketMessage message)
{
await _playbackQueue.Enqueue(new PlaybackJob()
{
Type = JobType.Text,
Text = message.Content
});
}
}

View File

@ -0,0 +1,94 @@
using System.Collections.Concurrent;
using Discord.Audio;
using NAudio.Wave;
namespace Elementary.Audio;
public enum JobType
{
Audio,
Text,
// Sozai
}
public class PlaybackJob
{
public JobType Type;
public string Text;
public float Volume = 1.0f;
}
public class PlaybackQueue
{
// any type of job ConcurrentQueue
private ConcurrentQueue<PlaybackJob> _queue;
private AudioManager _audioManager;
private object _lock;
private bool _isPlaying;
public PlaybackQueue(AudioManager audioManager)
{
_queue = new();
_isPlaying = false;
_lock = new();
_audioManager = audioManager;
}
/// <summary>
/// Enqueue audio stream and play it
/// </summary>
/// <param name="job"></param>
public async Task Enqueue(PlaybackJob job)
{
_queue.Enqueue(job);
Console.WriteLine("Enqueued");
if (!_isPlaying)
{
lock (_lock)
{
if (!_isPlaying)
{
Console.WriteLine("Start Playing due to empty queue");
PlayNext();
}
}
}
await Task.CompletedTask;
}
public void Flush()
{
_queue.Clear();
Console.WriteLine("Queue Flushed");
}
private async void PlayNext()
{
if (_queue.TryDequeue(out var currentStream))
{
Console.WriteLine("Start Playing");
_isPlaying = true;
await (currentStream.Type switch
{
JobType.Audio => _audioManager.PlayAudio(currentStream.Text),
JobType.Text => _audioManager.PlayText(currentStream.Text),
_ => throw new ArgumentOutOfRangeException()
});
Console.WriteLine("Finished Playing");
await Task.Delay(200);
_isPlaying = false;
PlayNext();
}
else if (_queue.IsEmpty)
{
Console.WriteLine("Queue is empty");
_isPlaying = false;
}
}
}

View File

@ -0,0 +1,52 @@
using System.Text.Json;
namespace Elementary.Audio;
public class SozaiAPI
{
private Uri AssetsInfoUrl = new("https://synchthia-sounds.storage.googleapis.com/index.json");
private Asset[] Assets;
private class Asset
{
/// <summary>
/// SHA1 hash of the file
/// </summary>
public string hash { get; set; }
public string id { get; set; }
public string[] names { get; set; }
public string[] namespaces { get; set; }
public string path { get; set; }
public string url { get; set; }
}
private HttpClient _client;
public SozaiAPI()
{
_client = new HttpClient();
}
public async Task Setup()
{
var response = await _client.GetAsync(AssetsInfoUrl);
var stream = await response.Content.ReadAsStreamAsync();
Assets = await JsonSerializer.DeserializeAsync<Asset[]>(stream);
Console.WriteLine($"Loaded {Assets.Length} assets");
}
public async Task<Stream?> GetAudioStream(string name)
{
var asset = Assets.FirstOrDefault(asset => asset.names.Contains(name));
if (asset == null) return null;
Console.WriteLine($"Requested {asset.names[0]}");
var response = await _client.GetAsync(asset.url);
Console.WriteLine($"Got response {response.StatusCode}");
var stream = await response.Content.ReadAsStreamAsync();
return stream;
}
}

View File

@ -0,0 +1,58 @@
using System.Net.Http.Json;
using System.Text;
namespace Elementary.Audio;
public class VoicevoxAPI
{
private UriBuilder _APIRootUrl;
private HttpClient _client;
public async Task Setup(string url)
{
Uri _url = new(url);
_APIRootUrl = new UriBuilder($"{_url.Scheme}://{_url.Host}:{_url.Port}");
_client = new HttpClient();
}
/// <summary>
/// Returns a stream of the 24000Hz wave audio generated from the text
/// </summary>
/// <param name="text"></param>
/// <param name="speaker"></param>
/// <returns></returns>
public async Task<Stream?> Speak(string text, string speaker = "47")
{
Console.WriteLine($"Requested TTS {text}");
var query = await GetAudioQuery(text, speaker);
if (query == null) return null;
var stream = await GetAudioStream(query, speaker);
if (stream == null) return null;
return stream;
}
internal async Task<string?> GetAudioQuery(string text, string speaker = "1")
{
UriBuilder builder = new(_APIRootUrl.Uri);
builder.Path = "/audio_query";
builder.Query = $"text={text}&speaker={speaker}";
var response = await _client.PostAsync(builder.Uri, new StringContent(string.Empty));
var stream = await response.Content.ReadAsStreamAsync();
return await new StreamReader(stream).ReadToEndAsync();
}
internal async Task<Stream?> GetAudioStream(string query, string speaker = "1")
{
UriBuilder builder = new(_APIRootUrl.Uri);
builder.Path = "/synthesis";
builder.Query = $"speaker={speaker}";
var response =
await _client.PostAsync(builder.Uri, new StringContent(query, Encoding.UTF8, "application/json"));
var stream = await response.Content.ReadAsStreamAsync();
return stream;
}
}

View File

@ -0,0 +1,52 @@
using NAudio.Wave;
namespace Elementary.Audio;
public class VolumeAdjustedWaveStream : WaveStream
{
private readonly WaveStream _waveStream;
private readonly float _volume;
public VolumeAdjustedWaveStream(WaveStream waveStream, float volume)
{
_waveStream = waveStream;
_volume = volume;
}
public override WaveFormat WaveFormat => _waveStream.WaveFormat;
public override long Length => _waveStream.Length;
public override long Position
{
get => _waveStream.Position;
set => _waveStream.Position = value;
}
public override int Read(byte[] buffer, int offset, int count)
{
var read = _waveStream.Read(buffer, offset, count);
try
{
for (var i = 0; i < read; i += 2)
{
var sample = (short) ((buffer[offset + i + 1] << 8) | buffer[offset + i]);
sample = (short) (sample * _volume);
buffer[offset + i] = (byte) (sample & 0xFF);
buffer[offset + i + 1] = (byte) (sample >> 8);
}
}
catch (Exception e)
{
Console.WriteLine(e);
}
return read;
}
protected override void Dispose(bool disposing)
{
// _waveStream.Dispose();
// base.Dispose(disposing);
}
}

View File

@ -0,0 +1,59 @@
using System.Reflection;
using Discord.Audio;
using Discord.Commands;
using Discord.WebSocket;
using Elementary.Audio;
namespace Elementary.Commands;
public class Handler
{
private readonly DiscordSocketClient _client;
private readonly CommandService _commands;
private readonly IServiceProvider _services;
private readonly AudioManager _audioManager;
private readonly MessageHandler _messageHandler;
// Retrieve client and CommandService instance via ctor
public Handler(DiscordSocketClient client, CommandService commands, IServiceProvider services,
AudioManager audioManager, MessageHandler messageHandler)
{
_commands = commands;
_client = client;
_services = services;
_audioManager = audioManager;
_messageHandler = messageHandler;
}
public async Task Setup()
{
await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services);
_client.MessageReceived += HandleMessage;
}
private async Task HandleMessage(SocketMessage messageParam)
{
var message = messageParam as SocketUserMessage;
if (message == null || message.Author.IsBot) return;
var argPos = 0;
if (message.HasCharPrefix('!', ref argPos) ||
message.HasMentionPrefix(_client.CurrentUser, ref argPos))
{
SocketCommandContext context = new(_client, message);
await _commands.ExecuteAsync(
context,
argPos,
_services);
}
if (_audioManager.isConnected)
{
Console.WriteLine("Handling message");
await _messageHandler.HandleMessage(message);
}
}
}

View File

@ -0,0 +1,80 @@
using Discord;
using Discord.Commands;
using Elementary.Audio;
namespace Elementary.Commands;
public class MessageCommands : ModuleBase<SocketCommandContext>
{
private AudioManager _audioManager;
private readonly IServiceProvider _serviceProvider;
private readonly CommandService _commandService;
public MessageCommands(AudioManager audioManager, CommandService commandService, IServiceProvider serviceProvider)
{
_audioManager = audioManager;
_commandService = commandService;
_serviceProvider = serviceProvider;
}
[Command("help")]
[Summary("Displays a list of commands.")]
public async Task HelpAsync()
{
EmbedBuilder embedBuilder = new()
{
Title = "Elementary Commands",
Description = "A list of commands for Elementary.",
Color = Color.Blue
};
foreach (var module in _commandService.Modules)
{
string description = null;
foreach (var command in module.Commands)
{
var result = await command.CheckPreconditionsAsync(Context, _serviceProvider);
if (result.IsSuccess) description += $"!{command.Name} - {command.Summary}\n";
}
if (!string.IsNullOrWhiteSpace(description)) embedBuilder.AddField(module.Name, description);
}
await ReplyAsync(embed: embedBuilder.Build());
}
[Command("say")]
[Summary("Echoes a message.")]
public Task SayAsync([Remainder] [Summary("The text to echo")] string echo)
{
return ReplyAsync(echo);
}
[Command("join", RunMode = RunMode.Async)]
[Summary("Joins a voice channel.")]
public async Task JoinAsync(IVoiceChannel channel = null)
{
await _audioManager.JoinChannel(channel ?? (Context.User as IGuildUser)?.VoiceChannel);
}
[Command("leave", RunMode = RunMode.Async)]
[Summary("Leaves a voice channel.")]
public async Task LeaveAsync()
{
await _audioManager.LeaveChannel();
}
[Command("play", RunMode = RunMode.Async)]
[Summary("Plays audio from a file.")]
public async Task PlayAsync([Remainder] [Summary("The path to the file")] string path)
{
await _audioManager.PlayAudio(path);
}
[Command("stop", RunMode = RunMode.Async)]
[Summary("Stops playing audio.")]
public async Task StopAsync()
{
await _audioManager.StopAudio();
}
}

View File

@ -0,0 +1,31 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration.Json;
namespace Elementary.Config;
public class AppSettings
{
public DiscordSettings DiscordSettings { get; set; }
}
public class DiscordSettings
{
public string Token { get; set; }
}
public class Configuration
{
public AppSettings AppSettings { get; set; }
public static Configuration LoadFromJson()
{
var builder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", false, true);
var configuration = builder.Build();
var config = new Configuration();
configuration.Bind(config);
return config;
}
}

18
Elementary/Dockerfile Normal file
View File

@ -0,0 +1,18 @@
FROM mcr.microsoft.com/dotnet/runtime:7.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /src
COPY ["DiscordAudioBot/DiscordAudioBot.csproj", "DiscordAudioBot/"]
RUN dotnet restore "DiscordAudioBot/DiscordAudioBot.csproj"
COPY . .
WORKDIR "/src/DiscordAudioBot"
RUN dotnet build "DiscordAudioBot.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "DiscordAudioBot.csproj" -c Release -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "DiscordAudioBot.dll"]

View File

@ -0,0 +1,52 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<Configurations>Debug;Release</Configurations>
<Platforms>AnyCPU;x64</Platforms>
</PropertyGroup>
<ItemGroup>
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="CSCore" Version="1.2.1.2" />
<PackageReference Include="Discord.Net" Version="3.11.0" />
<PackageReference Include="Discord.Net.Core" Version="3.11.0" />
<PackageReference Include="ManagedBass" Version="3.1.1" />
<PackageReference Include="ManagedBass.Mix" Version="3.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="NAudio" Version="2.1.0" />
<PackageReference Include="System.Speech" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="libsodium.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="opus.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="bass.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="bassmix.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

76
Elementary/Program.cs Normal file
View File

@ -0,0 +1,76 @@
using System.Collections.Concurrent;
using System.Reflection;
using Discord;
using Discord.Audio;
using Discord.Commands;
using Discord.WebSocket;
using Elementary.Audio;
using Elementary.Commands;
using ManagedBass;
using Microsoft.Extensions.DependencyInjection;
using NAudio.Wave;
using Configuration = Elementary.Config.Configuration;
namespace Elementary;
public class Program
{
private DiscordSocketClient _client;
private IServiceProvider _services;
private CommandService _commands;
private Handler _handler;
private SozaiAPI _sozaiAPI;
private VoicevoxAPI _voicevoxAPI;
public static void Main(string[] args)
{
new Program().Setup().GetAwaiter().GetResult();
}
private async Task Setup()
{
var configuration = Configuration.LoadFromJson();
_client = new DiscordSocketClient(new DiscordSocketConfig
{
GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent
});
_commands = new CommandService();
_services = new ServiceCollection()
.AddSingleton(_client)
.AddSingleton(_commands)
.AddSingleton<Handler>()
.AddSingleton<AudioManager>()
.AddSingleton<VoicevoxAPI>()
.AddSingleton<SozaiAPI>()
.AddSingleton<PlaybackQueue>()
.AddTransient<AudioConverter>()
.AddSingleton<MessageHandler>()
.BuildServiceProvider();
_sozaiAPI = _services.GetRequiredService<SozaiAPI>();
_voicevoxAPI = _services.GetRequiredService<VoicevoxAPI>();
_handler = new Handler(_client, _commands, _services, _services.GetRequiredService<AudioManager>(),
_services.GetRequiredService<MessageHandler>());
_client.Log += Log;
_commands.Log += Log;
// Bass.Init(-1, 44100, DeviceInitFlags.NoSpeakerAssignment, IntPtr.Zero);
await _handler.Setup();
await _sozaiAPI.Setup();
await _voicevoxAPI.Setup("http://localhost:50021");
await _client.LoginAsync(TokenType.Bot, configuration.AppSettings.DiscordSettings.Token);
await _client.StartAsync();
await Task.Delay(-1); //keep the program running
}
private static Task Log(LogMessage msg)
{
Console.WriteLine(msg.ToString());
return Task.CompletedTask;
}
}

View File

@ -0,0 +1,14 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"AppSettings": {
"DiscordSettings": {
"Token": "MTEzMDAzMjkxMTQzMzg1OTE5Mw.G8yVqt.HrC65f0t4dvZQIZ7iER4CV70pLvhAl8PIyknSs"
}
}
}

BIN
Elementary/bass.dll Normal file

Binary file not shown.

BIN
Elementary/bassmix.dll Normal file

Binary file not shown.

BIN
Elementary/libsodium.dll Normal file

Binary file not shown.

BIN
Elementary/opus.dll Normal file

Binary file not shown.

9
README.md Normal file
View File

@ -0,0 +1,9 @@
# Elementary
## Elementary is a simple, lightweight, and easy to use Discord bot written in C#
### Features
- TTS
- Music
- Talking