diff --git a/README.md b/README.md index 4f114eed..74cfe5ba 100644 --- a/README.md +++ b/README.md @@ -650,9 +650,13 @@ print("run[CQ:image,file="+j["img"]+"]") - [x] 团队听音练习 - - [x] *.mid (解析上传的mid文件) + - [x] *.mid (midi 转 txt) - - [x] 注: 该插件需要安装timidity,安装脚本可参考https://gitcode.net/anto_july/midi/-/raw/master/timidity.sh + - [x] midi制作*.txt (txt 转 midi) + + - [x] 设置音色40 (0~127) + + - [x] 注: 该插件需要安装timidity,linux安装脚本可参考https://gitcode.net/anto_july/midi/-/raw/master/timidity.sh,,windows安装脚本可参考https://gitcode.net/anto_july/midi/-/raw/master/timidity.bat,windows需要管理员模式运行 - [x] 符号说明: C5是中央C,后面不写数字,默认接5,Cb6<1,b代表降调,#代表升调,6比5高八度,<1代表音长×2,<3代表音长×8,<-1代表音长×0.5,<-3代表音长×0.125,R是休止符 diff --git a/go.mod b/go.mod index 0093d015..5bda82e2 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,6 @@ require ( github.com/tidwall/gjson v1.14.1 github.com/wcharczuk/go-chart/v2 v2.1.0 github.com/wdvxdr1123/ZeroBot v1.5.2-0.20220610070647-9eeffcb277ee - gitlab.com/gomidi/midi v1.23.7 gitlab.com/gomidi/midi/v2 v2.0.17 golang.org/x/image v0.0.0-20220601225756-64ec528b34cd ) diff --git a/go.sum b/go.sum index fea79edb..395a0e3a 100644 --- a/go.sum +++ b/go.sum @@ -259,8 +259,6 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -gitlab.com/gomidi/midi v1.23.7 h1:I6qKoIk9s9dcX+pNf0jC+tziCzJFn82bMpuntRkLeik= -gitlab.com/gomidi/midi v1.23.7/go.mod h1:3ohtNOhqoSakkuLG/Li1OI6I3J1c2LErnJF5o/VBq1c= gitlab.com/gomidi/midi/v2 v2.0.17 h1:kf16wNwFFOskl0trvarOwMuZUQICdIGn37LP9QqIRuo= gitlab.com/gomidi/midi/v2 v2.0.17/go.mod h1:quTyMKSQ4Klevxu6gY4gy2USbeZra0fV5SalndmPfsY= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= diff --git a/plugin/midicreate/midicreate.go b/plugin/midicreate/midicreate.go index 455b4f5a..16a1c9ab 100644 --- a/plugin/midicreate/midicreate.go +++ b/plugin/midicreate/midicreate.go @@ -15,6 +15,7 @@ import ( "time" ctrl "github.com/FloatTech/zbpctrl" + "github.com/FloatTech/zbputils/binary" "github.com/FloatTech/zbputils/control" "github.com/FloatTech/zbputils/ctxext" "github.com/FloatTech/zbputils/file" @@ -22,7 +23,6 @@ import ( "github.com/pkg/errors" zero "github.com/wdvxdr1123/ZeroBot" "github.com/wdvxdr1123/ZeroBot/message" - "gitlab.com/gomidi/midi/gm" "gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2/smf" ) @@ -30,11 +30,13 @@ import ( func init() { engine := control.Register("midicreate", &ctrl.Options[*zero.Ctx]{ DisableOnDefault: false, - Help: "midi音乐制作,该插件需要安装timidity,安装脚本可参考https://gitcode.net/anto_july/midi/-/raw/master/timidity.sh\n" + + Help: "midi音乐制作,该插件需要安装timidity,linux安装脚本可参考https://gitcode.net/anto_july/midi/-/raw/master/timidity.sh,windows安装脚本可参考https://gitcode.net/anto_july/midi/-/raw/master/timidity.bat,windows需要管理员模式运行\n" + "- midi制作 CCGGAAGR FFEEDDCR GGFFEEDR GGFFEEDR CCGGAAGR FFEEDDCR\n" + "- 个人听音练习\n" + "- 团队听音练习\n" + - "- *.mid (解析上传的mid文件)", + "- *.mid (midi 转 txt)\n" + + "- midi制作*.txt (txt 转 midi)\n" + + "- 设置音色40 (0~127)", PrivateDataFolder: "midicreate", }) cachePath := engine.DataFolder() + "cache/" @@ -48,7 +50,7 @@ func init() { uid := ctx.Event.UserID input := ctx.State["args"].(string) midiFile := cachePath + strconv.FormatInt(uid, 10) + time.Now().Format("20060102150405") + "_midicreate.mid" - cmidiFile, err := str2music(input, midiFile) + cmidiFile, err := str2music(ctx, input, midiFile) if err != nil { if file.IsExist(midiFile) { ctx.UploadThisGroupFile(file.BOTPATH+"/"+midiFile, filepath.Base(midiFile), "") @@ -87,7 +89,7 @@ func init() { target := uint8(55 + rand.Intn(34)) answer := name(target) + strconv.Itoa(int(target/12)) midiFile := cachePath + strconv.FormatInt(uid, 10) + time.Now().Format("20060102150405") + "_midicreate.mid" - cmidiFile, err := str2music(answer, midiFile) + cmidiFile, err := str2music(ctx, answer, midiFile) if err != nil { ctx.SendChain(message.Text("ERROR:听音练习结束, 无法转换midi文件, ", err)) return @@ -137,7 +139,7 @@ func init() { ), ) midiFile = cachePath + strconv.FormatInt(uid, 10) + time.Now().Format("20060102150405") + "_midicreate.mid" - cmidiFile, err = str2music(c.Event.Message.String(), midiFile) + cmidiFile, err = str2music(ctx, c.Event.Message.String(), midiFile) if err != nil { ctx.SendChain(message.Text("ERROR: can't convert midi file,", err)) return @@ -172,7 +174,7 @@ func init() { target = uint8(55 + rand.Intn(34)) answer = name(target) + strconv.Itoa(int(target/12)) midiFile = cachePath + strconv.FormatInt(uid, 10) + time.Now().Format("20060102150405") + "_midicreate.mid" - cmidiFile, err = str2music(answer, midiFile) + cmidiFile, err = str2music(ctx, answer, midiFile) if err != nil { ctx.SendChain(message.Text("ERROR:听音练习结束, 无法转换midi文件, ", err)) return @@ -193,7 +195,7 @@ func init() { ) time.Sleep(time.Millisecond * 500) midiFile = cachePath + strconv.FormatInt(uid, 10) + time.Now().Format("20060102150405") + "_midicreate.mid" - cmidiFile, err = str2music(c.Event.Message.String(), midiFile) + cmidiFile, err = str2music(ctx, c.Event.Message.String(), midiFile) if err != nil { ctx.SendChain(message.Text("ERROR: can't convert midi file,", err)) return @@ -231,8 +233,54 @@ func init() { ctx.SendChain(message.Text("ERROR:", err)) return } - midStr := mid2txt(data) - ctx.SendChain(message.Text("文件名:", ctx.Event.File.Name, "\n转化的midi字符:", midStr)) + s, err := smf.ReadFrom(bytes.NewReader(data)) + if err != nil { + ctx.SendChain(message.Text("ERROR:", err)) + return + } + for i := 0; i < int(s.NumTracks()); i++ { + midStr := mid2txt(data, i) + if err != nil { + ctx.SendChain(message.Text("ERROR:", err)) + return + } + fileName := strings.ReplaceAll(cachePath+"/"+ctx.Event.File.Name, ".mid", fmt.Sprintf("-%d.txt", i)) + _ = os.WriteFile(fileName, binary.StringToBytes(midStr), 0666) + ctx.UploadThisGroupFile(file.BOTPATH+"/"+fileName, filepath.Base(fileName), "") + } + }) + engine.On("notice/group_upload", func(ctx *zero.Ctx) bool { + return path.Ext(ctx.Event.File.Name) == ".txt" && strings.Contains(ctx.Event.File.Name, "midi制作") + }).SetBlock(false).Limit(ctxext.LimitByGroup). + Handle(func(ctx *zero.Ctx) { + fileURL := ctx.GetThisGroupFileUrl(ctx.Event.File.BusID, ctx.Event.File.ID) + data, err := web.GetData(fileURL) + if err != nil { + ctx.SendChain(message.Text("ERROR:", err)) + return + } + uid := ctx.Event.UserID + midiFile := cachePath + strconv.FormatInt(uid, 10) + time.Now().Format("20060102150405") + "_midicreate.mid" + cmidiFile, err := str2music(ctx, binary.BytesToString(data), midiFile) + if err != nil { + ctx.SendChain(message.Text("ERROR:无法转换midi文件,", err)) + return + } + ctx.SendChain(message.Record("file:///" + file.BOTPATH + "/" + cmidiFile)) + }) + engine.OnPrefix("设置音色").SetBlock(true). + Handle(func(ctx *zero.Ctx) { + param := ctx.State["args"].(string) + timbre, err := strconv.Atoi(param) + if err != nil { + ctx.SendChain(message.Text("ERROR:", err)) + } + err = setTimbreMode(ctx, int64(timbre)) + if err != nil { + ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text(err)) + return + } + ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text("成功")) }) } @@ -253,8 +301,8 @@ var ( } ) -func str2music(input, midiFile string) (cmidiFile string, err error) { - err = mkMidi(midiFile, input) +func str2music(ctx *zero.Ctx, input, midiFile string) (cmidiFile string, err error) { + err = mkMidi(ctx, midiFile, input) if err != nil { return } @@ -264,7 +312,7 @@ func str2music(input, midiFile string) (cmidiFile string, err error) { return } -func mkMidi(filePath, input string) error { +func mkMidi(ctx *zero.Ctx, filePath, input string) error { if file.IsExist(filePath) { return nil } @@ -276,7 +324,8 @@ func mkMidi(filePath, input string) error { tr.Add(0, smf.MetaMeter(4, 4)) tr.Add(0, smf.MetaTempo(72)) tr.Add(0, smf.MetaInstrument("Violin")) - tr.Add(0, midi.ProgramChange(0, gm.Instr_Violin.Value())) + timbre := getTimbreMode(ctx) + tr.Add(0, midi.ProgramChange(0, uint8(timbre))) k := strings.ReplaceAll(input, " ", "") @@ -410,46 +459,43 @@ func processOne(note string) uint8 { return o(base, level) } -func mid2txt(midBytes []byte) (midStr string) { +func mid2txt(midBytes []byte, trackNo int) (midStr string) { var ( - absTicksStart float64 - absTicksEnd float64 - startNote byte - endNote byte - defaultMetric = 960.0 - defaultTrackNo = 0 + absTicksStart float64 + absTicksEnd float64 + startNote byte + endNote byte + defaultMetric = 960.0 ) - _ = smf.ReadTracksFrom(bytes.NewReader(midBytes)). + _ = smf.ReadTracksFrom(bytes.NewReader(midBytes), trackNo). Do( func(te smf.TrackEvent) { - if !te.Message.IsMeta() && te.TrackNo == defaultTrackNo { + if !te.Message.IsMeta() { b := te.Message.Bytes() - if len(b) == 3 { - if b[0] == 0x90 && b[2] > 0 { - absTicksStart = float64(te.AbsTicks) - startNote = b[1] - } - if b[0] == 0x80 || (b[0] == 0x90 && b[2] == 0x00) { - absTicksEnd = float64(te.AbsTicks) - endNote = b[1] + if te.Message.Is(midi.NoteOnMsg) && b[2] > 0 { + absTicksStart = float64(te.AbsTicks) + startNote = b[1] + } + if te.Message.Is(midi.NoteOffMsg) || (te.Message.Is(midi.NoteOnMsg) && b[2] == 0x00) { + absTicksEnd = float64(te.AbsTicks) + endNote = b[1] + if startNote == endNote { + sign := name(b[1]) + level := b[1] / 12 + length := (absTicksEnd - absTicksStart) / defaultMetric + midStr += sign + if level != 5 { + midStr += strconv.Itoa(int(level)) + } + pow := int(math.Round(math.Log2(length))) + if pow >= -4 && pow != 0 { + midStr += "<" + strconv.Itoa(pow) + } + startNote = 0 + endNote = 0 } } - if (b[0] == 0x80 || (b[0] == 0x90 && b[2] == 0x00)) && startNote == endNote { - sign := name(b[1]) - level := b[1] / 12 - length := (absTicksEnd - absTicksStart) / defaultMetric - midStr += sign - if level != 5 { - midStr += strconv.Itoa(int(level)) - } - pow := int(math.Round(math.Log2(length))) - if pow >= -4 && pow != 0 { - midStr += "<" + strconv.Itoa(pow) - } - startNote = 0 - endNote = 0 - } - if (b[0] == 0x90 && b[2] > 0) && absTicksStart > absTicksEnd { + if (te.Message.Is(midi.NoteOnMsg) && b[2] > 0) && absTicksStart > absTicksEnd { length := (absTicksStart - absTicksEnd) / defaultMetric pow := int(math.Round(math.Log2(length))) if pow == 0 { @@ -463,3 +509,31 @@ func mid2txt(midBytes []byte) (midStr string) { ) return } + +func setTimbreMode(ctx *zero.Ctx, timbre int64) error { + gid := ctx.Event.GroupID + if gid == 0 { + gid = -ctx.Event.UserID + } + if timbre < 0 || timbre > 127 { + return errors.New("音色应该在0~127之间") + } + m, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]) + if !ok { + return errors.New("no such plugin") + } + return m.SetData(gid, timbre) +} + +func getTimbreMode(ctx *zero.Ctx) (index int64) { + gid := ctx.Event.GroupID + if gid == 0 { + gid = -ctx.Event.UserID + } + m, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]) + if ok { + index := m.GetData(gid) + return index + } + return 40 +}