聊天栏音频可视化

结合聊天栏与字符串表现音频可视化
源码来自工程『Sincerely

源码

主体思路是在一个由字符串组成的平面上通过读取TimeLine中的信息生成动画,最后将这个平面映射到聊天栏,并生成tellraw命令。 (注释已比较详细了,不作太多解释。)

using System;
using Audio2Minecraft;
using Newtonsoft.Json;

namespace WpfApplication1
{
    /// 
    /// MainWindow.xaml 的交互逻辑
    /// 
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void button_Click(object sender, RoutedEventArgs e)
        {
            InheritExpression.SetCompareLists(System.Windows.Forms.Application.StartupPath + "\\test");
            //生成TimeLine(时间序列)
            var a = new TimeLine().Serialize(MidiPath.Text, WavePath.Text, 72);
            //var a = new AudioStreamMidi().Serialize(MidiPath.Text, new TimeLine()); Console.Write(new List()[0]);

            //所有音轨的发生发声延长3tick
            a.Sound_ExtraDelay(3);
            a.Sound_StopSound(false);   //禁用/stopsound
            a.EnableWave(false);    //禁用Wave
			a.Sound_InheritExpression("%p");    //设置子表达式
            //设定音轨及其音色
            for (var i = 1; i <= 8; i++) { a.Sound_SoundName("1", i.ToString());} //钢琴
            a.Sound_SoundName("74c", "74"); a.Sound_StopSound(true, "74"); a.Sound_PercVolume(90, "45");
            a.Sound_SoundName("45c", "45"); a.Sound_StopSound(true, "45"); a.Sound_PercVolume(175, "45"); a.Sound_ExtraDelay(4);
            a.Sound_SoundName("96c", "96"); a.Sound_StopSound(true, "96"); a.Sound_PercVolume(160, "45"); a.Sound_ExtraDelay(4);
            a.Sound_SoundName("52c", "52"); a.Sound_StopSound(true, "52"); a.Sound_PercVolume(160, "45"); a.Sound_ExtraDelay(4); //弦乐
            a.Sound_SoundName("0.86", "beat", "Drum 86");
            a.Sound_SoundName("0.40", "beat", "Electric Snare");
            a.Sound_SoundName("0.43", "beat", "High Floor Tom");
            a.Sound_SoundName("0.41", "beat", "Low Floor Tom");
            a.Sound_SoundName("0.69", "beat", "Cabasa");
            a.Sound_SoundName("0.44", "beat", "Pedal Hi-Hat");
            a.Sound_SoundName("0.67", "beat", "High Agogo");
            a.Sound_SoundName("0.45", "beat", "Low Tom");
            a.Sound_SoundName("0.57", "beat", "Crash Cymbal 2"); //设定音色

            a.EnableMidi(false); //禁用Midi
            a.EnableMidi(true, "", "", -1, "PlaySound"); //只启用Midi的/playsound

            //生成CommandLine(命令序列)
            var b = new CommandLine().Serialize(a);
            b.Start.Clear(); b.End.Clear();

            //生成聊天栏文本(命令序列)
            var text = textShow(a);
            for (int i = 0; i < 51; i++) b.Keyframe.Insert(0, new Command()); //插入18个tick用于'琴键'的下落
            b = b.Combine(b, text); //读取Text嵌入命令序列

            //每一刻都增加"tp @p ~0.2 ~ ~"的命令
            for (int i = 0; i < b.Keyframe.Count; i ++) b.Keyframe[i].Commands.Add("tp @p ~0.2 ~ ~");
            //生成schematic文件
            new Schematic().ExportSchematic(b, new ExportSetting() { AlwaysActive = true, AlwaysLoadEntities = false, Direction = 0, Width = 5 }, "E:\\time.schematic");
        }

        private void MidiSelect(object sender, MouseButtonEventArgs e)
        {
            OpenFileDialog fileDialog = new OpenFileDialog();
            fileDialog.Filter = "Midi|*.mid";
            fileDialog.FilterIndex = 1;
            if (fileDialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) { MidiPath.Text = fileDialog.FileName; }
        }

        private void WaveSelect(object sender, MouseButtonEventArgs e)
        {
            OpenFileDialog fileDialog = new OpenFileDialog();
            fileDialog.Filter = "Wave|*.wav";
            fileDialog.FilterIndex = 1;
            if (fileDialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) { WavePath.Text = fileDialog.FileName; }
        }
        private void LrcSelect(object sender, MouseButtonEventArgs e)
        {
            OpenFileDialog fileDialog = new OpenFileDialog();
            fileDialog.Filter = "Lrc|*.lrc;*.amlrc;*.txt";
            fileDialog.FilterIndex = 1;
            if (fileDialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) { LrcPath.Text = fileDialog.FileName; }
        }


        private CommandLine textShow(TimeLine a) //根据时间序列生成聊天栏文本
        {
            var text = new CommandLine(); //实例化聊天栏文本(命令序列)
            var wav = new AudioStreamWave().Serialize(WavePath.Text, new TimeLine(), 60, 60); //实例化Wav(时间序列)

            var timeD = new List(); //实例化文本的二维平面

            //获取频率&音色的特征信息(平均值&最大值)
            /*var highest_fre = wav.TickNodes.Max(v => v.WaveNodesLeft.Max(h => h.Param["FrequencyPerTick"].Max(t => t.Value)));
            var highest_vol = wav.TickNodes.Max(v => v.WaveNodesLeft.Max(h => h.Param["VolumePerTick"].Max(t => t.Value)));
            var av_highest_fre = wav.TickNodes.Average(v => v.WaveNodesLeft.Max(h => h.Param["FrequencyPerTick"].Max(t => t.Value)));
            var av_highest_vol = wav.TickNodes.Average(v => v.WaveNodesLeft.Max(h => h.Param["VolumePerTick"].Max(t => t.Value)));
            var av_fre = (double)wav.TickNodes.Sum(v => v.WaveNodesLeft.Sum(h => h.Param["FrequencyPerTick"].Sum(t => t.Value))) / wav.TickNodes.Count / 160;
            var av_vol = (double)wav.TickNodes.Sum(v => v.WaveNodesLeft.Sum(h => h.Param["VolumePerTick"].Sum(t => t.Value))) / wav.TickNodes.Count / 160;
             => average: 77 - level: 6*/

            for (int i = 0; i < wav.TickNodes.Count; i++) //创建文本平面的像素点(64*18)
            {
                timeD.Add(new Pixel[64, 18]);
                for (int x = 0; x < 63; x++)
                    for (int y = 0; y < 18; y++)
                    {
                        timeD[i][x, y] = new Pixel();
                    }
            }
            for (int i = 0; i < wav.TickNodes.Count; i++) //节奏可视化
            {
                if (a.TickNodes[i].MidiTracks.Count > 0)
                {
                    if (a.TickNodes[i].MidiTracks.ContainsKey("beat")) //遍历beat音轨下的乐器
                    {
                        if (a.TickNodes[i].MidiTracks["beat"].ContainsKey("Crash Cymbal 2")) //单面钹
                        {
                            for (int j = 0; j < 64; j++)
                            {
                                var delT = Math.Sqrt(64 * 64 - (j + 0.5 - 32) * (j + 0.5 - 32)); //延时
                                timeD = crash(timeD, i + (int)delT - 64, j, "dark_gray"); //生成动画
                            }
                        }
                        if (a.TickNodes[i].MidiTracks["beat"].ContainsKey("Low Tom") || a.TickNodes[i].MidiTracks["beat"].ContainsKey("Low Floor Tom") || a.TickNodes[i].MidiTracks["beat"].ContainsKey("High Floor Tom")) //低音鼓&低音落地鼓&高音落地鼓
                        {
                            timeD = beat(timeD, i, 0, "dark_aqua", "aqua"); //在x=0和x=63生成动画
                            timeD = beat(timeD, i, 63, "dark_aqua", "aqua");
                        }
                        if (a.TickNodes[i].MidiTracks["beat"].ContainsKey("Electric Snare")) //电子鼓
                        {
                            timeD = beat(timeD, i, 1, "dark_green", "green"); //在x=1和x=62生成动画
                            timeD = beat(timeD, i, 62, "dark_green", "green");
                        }
                    }
                }
            }

            for (int i = 0; i < wav.TickNodes.Count; i++) //波形可视化
            {
                var display = timeD[i];
                foreach (var node in wav.TickNodes[i].WaveNodesLeft)
                {
                    for (int j = 0; j < 64; j++)
                    {
                        if (node.Param["VolumePerTick"].Count == 60 && j > 1 && j < 62) //遍历x=2到x=61
                        {
                            var h = node.Param["VolumePerTick"][j - 2].Value / 18; //获取音量
                            if (h > 18) h = 18;

                            if (i == 0) h = h / 2;
                            else
                            {
                                var mh = 0;
                                for (int d = 0; d < 18; d++) if (timeD[i - 1][j, d].Color != "black") mh++;
                                h = (h + mh) / 2;
                            }  //获取上一刻该处的波形高度

                            var p = (double)(60 - Math.Abs(j - 30)) / 60 * 100;
                            h = (int)(h * p / 100); //按照x坐标得到该处的波形高度(中间高两边低)

                            for (int k = 0; k < h; k++)
                            {
                                var color = "dark_purple";
                                if (k == h - 1)
                                    color = "blue"; //最上层为蓝色
                                display[j, k] = new Pixel() { Color = color }; //设置像素
                            }
                        }
                    }
                }
            }

            for (int i = 0; i < 54; i++) timeD.Insert(0, new Pixel[64, 18]); //插入18个tick用于'琴键'的下落
            //最高音: 97 , 最低音: 25
            for (int i = 0; i < a.TickNodes.Count; i++) //琴键可视化
            {
                if (a.TickNodes[i].MidiTracks.Count > 0)
                {
                    foreach (var track in a.TickNodes[i].MidiTracks.Keys)
                    {
                        var color = "white";
                        if (track == "2") color = "yellow";
                        if (track == "3") color = "gray";
                        if (track == "4") color = "yellow";
                        if (track == "5") color = "yellow";
                        if (track == "6") color = "red";
                        if (track == "7") color = "red";
                        if (track == "8") color = "red"; //设置各音轨的琴键颜色

                        foreach (string instrument in a.TickNodes[i].MidiTracks[track].Keys)
                        {
                            if (instrument == "Acoustic Grand") //遍历所有Acoustic Grand
                            {
                                var nodes = a.TickNodes[i].MidiTracks[track][instrument];
                                foreach (var node in nodes)
                                {
                                    var pitch = node.Param["Pitch"].Value; //获取音高
                                    var x = pitch - 31; if (x > 63 || x < 0) continue; //删除出现在平面外的琴键
                                    for (int s = 0; s < 3; s++) //琴键高度=3
                                    {
                                        for (int y = 0; y < 60; y++) //遍历x
                                        {
                                            var h = y / 3 + s - 3;
                                            if (h == 0 && s > 0)
                                                timeD[i + 60 - y][x, h] = new Pixel() { Char = "◙", Color = color }; //表示正在消失的琴键样式
                                            else if (h == 0 && s == 0)
                                                timeD[i + 60 - y][x, h] = new Pixel() { Char = "◘", Color = color }; //表示刚刚敲击的琴键样式
                                            else if (h < 18 && h >= 0)
                                                timeD[i + 60 - y][x, h] = new Pixel() { Color = color };
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }

            var lrc = new Lrc().Serialize(LrcPath.Text); //获取.lrc的内容
            var Lrcs = textLrc(lrc, timeD.Count, 54); //.lrc -> Tellraw

            for (int i = 0; i < timeD.Count; i++) //像素点 -> Tellraw
            {
                var display = timeD[i];
                text.Keyframe.Add(new Command());
                var tlw = new Tellraw();
                for (int y = 17; y > -1; y--)
                {
                    var sumT = new Tellraw.Text();
                    var lastT = new Tellraw.Text();
                    for (int x = 0; x < 64; x++)
                    {
                        var t = new Tellraw.Text();
                        if (display[x, y] == null) t = new Tellraw.Text() { text = "▌", color = "black" };
                        else t = new Tellraw.Text() { text = display[x, y].Char, color = display[x, y].Color };

                        if (x == 0) lastT = t;

                        if (lastT.text == t.text && lastT.color == t.color)
                        {
                            sumT.text += t.text;
                            sumT.color = t.color;
                            if (x == 63) tlw.texts.Add(sumT);
                        }
                        else
                        {
                            tlw.texts.Add(sumT);
                            lastT = t;
                            sumT = t;
                            if (x == 63) tlw.texts.Add(t);
                        }
                    }
                    tlw.texts[tlw.texts.Count - 1].text += "\n";
                }
                if (Lrcs[i] != null) tlw.texts.AddRange(Lrcs[i].texts);
                else tlw.texts.Add(new Tellraw.Text() { text = "\n" });

                //播放进度
                var proBar = new Tellraw.Text[3];
                proBar[0] = new Tellraw.Text() { color = "dark_purple" };
                proBar[1] = new Tellraw.Text() { text = "▪", color = "gold" };
                proBar[2] = new Tellraw.Text() { color = "white" };
                var did = (int)((double)i / timeD.Count * 94) + 1;
                for (int t = 0; t < did - 1; t++) proBar[0].text += "▪";
                for (int t = did; t < 94; t++) proBar[2].text += "▫";
                tlw.texts.Add(new Tellraw.Text() { text = " ▎▎ ", color = "white" });
                tlw.texts.AddRange(proBar);
                var m = (i / 1200 > 9) ? (i / 1200).ToString() : "0" + (i / 1200).ToString();
                var s = (i % 1200 / 20 > 9) ? (i % 1200 / 20).ToString() : "0" + (i % 1200 / 20).ToString();
                tlw.texts.Add(new Tellraw.Text() { text = " " + m + ":" + s, color = "white" });

                text.Keyframe[i].Commands.Add("tellraw @p " + JsonConvert.SerializeObject(tlw.texts)); //生成tellraw命令
            }
            return text;
        }

        private List beat(List timeD, int i, int j, string c1, string c2)
        {
            for (int m = 0; m < 1; m++) timeD[i - 3][j, m] = new Pixel() { Color = c1 };
            for (int m = 0; m < 6; m++) timeD[i - 2][j, m] = new Pixel() { Color = c1 };
            for (int m = 0; m < 11; m++) timeD[i - 1][j, m] = new Pixel() { Color = c2 };
            for (int m = 0; m < 16; m++) timeD[i][j, m] = new Pixel() { Color = c2 };
            for (int m = 0; m < 13; m++) timeD[i + 1][j, m] = new Pixel() { Color = c2 };
            for (int m = 0; m < 11; m++) timeD[i + 2][j, m] = new Pixel() { Color = c2 };
            for (int m = 0; m < 10; m++) timeD[i + 3][j, m] = new Pixel() { Color = c2 };
            for (int m = 0; m < 9; m++) timeD[i + 4][j, m] = new Pixel() { Color = c2 };
            for (int m = 0; m < 8; m++) timeD[i + 5][j, m] = new Pixel() { Color = c1 };
            for (int m = 0; m < 7; m++) timeD[i + 6][j, m] = new Pixel() { Color = c1 };
            for (int m = 0; m < 6; m++) timeD[i + 7][j, m] = new Pixel() { Color = c1 };
            for (int m = 0; m < 5; m++) timeD[i + 8][j, m] = new Pixel() { Color = c1 };
            for (int m = 0; m < 4; m++) timeD[i + 9][j, m] = new Pixel() { Color = c1 };
            for (int m = 0; m < 3; m++) timeD[i + 10][j, m] = new Pixel() { Color = c1 };
            for (int m = 0; m < 2; m++) timeD[i + 11][j, m] = new Pixel() { Color = c1 };
            for (int m = 0; m < 1; m++) timeD[i + 12][j, m] = new Pixel() { Color = c1 };
            return timeD;
        } //打击乐动画
        private List crash(List timeD, int i, int j, string c1)
        {
            for (int m = 0; m < 5; m++) timeD[i - 3][j, m] = new Pixel() { Color = c1 };
            for (int m = 0; m < 10; m++) timeD[i - 2][j, m] = new Pixel() { Color = c1 };
            for (int m = 0; m < 18; m++) timeD[i - 1][j, m] = new Pixel() { Color = c1 };
            for (int m = 0; m < 18; m++) timeD[i][j, m] = new Pixel() { Color = c1 };
            for (int m = 0; m < 18; m++) timeD[i + 1][j, m] = new Pixel() { Color = c1 };
            for (int m = 0; m < 16; m++) timeD[i + 2][j, m] = new Pixel() { Color = c1 };
            for (int m = 0; m < 14; m++) timeD[i + 3][j, m] = new Pixel() { Color = c1 };
            for (int m = 0; m < 13; m++) timeD[i + 4][j, m] = new Pixel() { Color = c1 };
            for (int m = 0; m < 12; m++) timeD[i + 5][j, m] = new Pixel() { Color = c1 };
            for (int m = 0; m < 11; m++) timeD[i + 6][j, m] = new Pixel() { Color = c1 };
            for (int m = 0; m < 10; m++) timeD[i + 7][j, m] = new Pixel() { Color = c1 };
            for (int m = 0; m < 9; m++) timeD[i + 8][j, m] = new Pixel() { Color = c1 };
            for (int m = 0; m < 8; m++) timeD[i + 9][j, m] = new Pixel() { Color = c1 };
            for (int m = 0; m < 7; m++) timeD[i + 10][j, m] = new Pixel() { Color = c1 };
            for (int m = 0; m < 6; m++) timeD[i + 11][j, m] = new Pixel() { Color = c1 };
            for (int m = 0; m < 5; m++) timeD[i + 12][j, m] = new Pixel() { Color = c1 };
            for (int m = 0; m < 4; m++) timeD[i + 13][j, m] = new Pixel() { Color = c1 };
            for (int m = 0; m < 3; m++) timeD[i + 14][j, m] = new Pixel() { Color = c1 };
            for (int m = 0; m < 2; m++) timeD[i + 15][j, m] = new Pixel() { Color = c1 };
            for (int m = 0; m < 1; m++) timeD[i + 16][j, m] = new Pixel() { Color = c1 };
            return timeD;
        } //单面钹动画

        private Tellraw[] textLrc(Lrc lrc, int sum = 0, int delay = 0) //.lrc -> Tellraw
        {
            var textLrc = new Tellraw[sum];
            foreach(var l in lrc.Lrcs)
            {
                var start = l.Start;
                var duration = l.Duration;
                var length = l.Content.Length; if (length == 0) continue;
                var pTick = (double)duration / length;
                for (int i = 0; i < duration; i++)
                {
                    var index = start + i + delay;
                    var playedChar = (int)((i + 1) / pTick) + 1;

                    if (playedChar >= length) playedChar = length;
                    var hltext = l.Content.Substring(0, playedChar);
                    var ntext = l.Content.Substring(playedChar);

                    var tlw = new Tellraw();
                    tlw.texts = new List()
                    {
                        new Tellraw.Text() { text = " --  「", color = "dark_gray"},
                        new Tellraw.Text() { text = hltext.Replace("\u3000",@"  "), color = "yellow"},
                        new Tellraw.Text() { text = ntext.Replace("\u3000",@"  "), color = "gray"},
                        new Tellraw.Text() { text = "」\n", color = "dark_gray"}
                    };

                    textLrc[index] = tlw;
                }
            }
            return textLrc;
        }
    }

    public class Pixel //像素点
    {
        public string Char = "▌";
        public string Color = "black";
    }

    public class Tellraw //Tellraw
    {
        public List texts = new List();
        public class Text
        {
            public string text = "";
            public string color = "white";
        }
    }
}

因为不需要在游戏中调用元素,所以这项工程没有结合计分板,

工程效果

av21704817