first commit
This commit is contained in:
commit
b6abc9f526
25
.dockerignore
Normal file
25
.dockerignore
Normal 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
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
bin/
|
||||
obj/
|
||||
/packages/
|
||||
riderModule.iml
|
||||
/_ReSharper.Caches/
|
130
.idea/.idea.DiscordAudioBot/.idea/workspace.xml
generated
Normal file
130
.idea/.idea.DiscordAudioBot/.idea/workspace.xml
generated
Normal 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
31
Elementary.sln
Normal 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
|
36
Elementary/Audio/AudioConverter.cs
Normal file
36
Elementary/Audio/AudioConverter.cs
Normal 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));
|
||||
}
|
||||
}
|
93
Elementary/Audio/AudioManager.cs
Normal file
93
Elementary/Audio/AudioManager.cs
Normal 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();
|
||||
}
|
||||
}
|
80
Elementary/Audio/AudioMixer.cs
Normal file
80
Elementary/Audio/AudioMixer.cs
Normal 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();
|
||||
}
|
||||
}
|
10
Elementary/Audio/IAudioConverter.cs
Normal file
10
Elementary/Audio/IAudioConverter.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Elementary.Audio;
|
||||
|
||||
public interface IAudioConverter
|
||||
{
|
||||
public Process CreateStreamFromFilePath(string path);
|
||||
|
||||
public Process CreateStreamFromStream(Stream stream);
|
||||
}
|
25
Elementary/Audio/MessageHandler.cs
Normal file
25
Elementary/Audio/MessageHandler.cs
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
94
Elementary/Audio/PlaybackQueue.cs
Normal file
94
Elementary/Audio/PlaybackQueue.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
52
Elementary/Audio/SozaiAPI.cs
Normal file
52
Elementary/Audio/SozaiAPI.cs
Normal 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;
|
||||
}
|
||||
}
|
58
Elementary/Audio/VoicevoxAPI.cs
Normal file
58
Elementary/Audio/VoicevoxAPI.cs
Normal 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;
|
||||
}
|
||||
}
|
52
Elementary/Audio/VolumeAdjustedWaveStream.cs
Normal file
52
Elementary/Audio/VolumeAdjustedWaveStream.cs
Normal 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);
|
||||
}
|
||||
}
|
59
Elementary/Commands/Handler.cs
Normal file
59
Elementary/Commands/Handler.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
80
Elementary/Commands/MessageCommands.cs
Normal file
80
Elementary/Commands/MessageCommands.cs
Normal 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();
|
||||
}
|
||||
}
|
31
Elementary/Config/Configuration.cs
Normal file
31
Elementary/Config/Configuration.cs
Normal 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
18
Elementary/Dockerfile
Normal 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"]
|
52
Elementary/Elementary.csproj
Normal file
52
Elementary/Elementary.csproj
Normal 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
76
Elementary/Program.cs
Normal 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;
|
||||
}
|
||||
}
|
14
Elementary/appsettings.json
Normal file
14
Elementary/appsettings.json
Normal 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
BIN
Elementary/bass.dll
Normal file
Binary file not shown.
BIN
Elementary/bassmix.dll
Normal file
BIN
Elementary/bassmix.dll
Normal file
Binary file not shown.
BIN
Elementary/libsodium.dll
Normal file
BIN
Elementary/libsodium.dll
Normal file
Binary file not shown.
BIN
Elementary/opus.dll
Normal file
BIN
Elementary/opus.dll
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user