From 3a9ff9c70f49d6db80cc0e13f12cb9b379e7275a Mon Sep 17 00:00:00 2001 From: Leonmmcoset Date: Sun, 29 Mar 2026 19:18:33 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=9F=B3=E4=B9=90=E6=92=AD?= =?UTF-8?q?=E6=94=BE=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BuildTime.txt | 2 +- GitCommit.txt | 2 +- Gui/AppManager.cs | 1 + Gui/Apps/Files.cs | 16 ++ Gui/Apps/MusicEditor.cs | 498 ++++++++++++++++++++++++++++++++++++++++ System/BootMenu.cs | 7 +- 6 files changed, 521 insertions(+), 5 deletions(-) create mode 100644 Gui/Apps/MusicEditor.cs diff --git a/BuildTime.txt b/BuildTime.txt index a23bfa9..6f96d89 100644 --- a/BuildTime.txt +++ b/BuildTime.txt @@ -1 +1 @@ -2026-03-28 22:22:00 \ No newline at end of file +2026-03-29 19:08:01 \ No newline at end of file diff --git a/GitCommit.txt b/GitCommit.txt index a00c459..e2f888f 100644 --- a/GitCommit.txt +++ b/GitCommit.txt @@ -1 +1 @@ -e9d4ae3 \ No newline at end of file +78bda7c \ No newline at end of file diff --git a/Gui/AppManager.cs b/Gui/AppManager.cs index 438ade3..1c5652f 100644 --- a/Gui/AppManager.cs +++ b/Gui/AppManager.cs @@ -141,6 +141,7 @@ namespace CMLeonOS.Gui RegisterApp(new AppMetadata("MarkIt Viewer", () => { return new MarkItViewer(); }, Icons.Icon_Default, Color.FromArgb(25, 25, 25))); RegisterApp(new AppMetadata("Environment Variables", () => { return new EnvironmentVariables(); }, Icons.Icon_Default, Color.FromArgb(25, 25, 25))); RegisterApp(new AppMetadata("UILib Gallery", () => { return new UILibGallery(); }, Icons.Icon_Default, Color.FromArgb(25, 25, 25))); + RegisterApp(new AppMetadata("Music Editor", () => { return new MusicEditor(); }, Icons.Icon_Default, Color.FromArgb(25, 25, 25))); Logger.Logger.Instance.Info("AppManager", $"{AppMetadatas.Count} apps were registered."); diff --git a/Gui/Apps/Files.cs b/Gui/Apps/Files.cs index dda4b46..218897f 100644 --- a/Gui/Apps/Files.cs +++ b/Gui/Apps/Files.cs @@ -250,6 +250,10 @@ namespace CMLeonOS.Gui.Apps { ProcessManager.AddProcess(this, new MarkItViewer(path)).Start(); } + else if (extension == ".mus") + { + ProcessManager.AddProcess(this, new MusicEditor(path)).Start(); + } else { ShowOpenWithPrompt(path); @@ -279,6 +283,9 @@ namespace CMLeonOS.Gui.Apps case "MarkIt Viewer": ProcessManager.AddProcess(this, new MarkItViewer(path)).Start(); break; + case "Music Editor": + ProcessManager.AddProcess(this, new MusicEditor(path)).Start(); + break; default: Logger.Logger.Instance.Warning("Files", $"Unsupported open-with app: {appName}"); break; @@ -333,6 +340,15 @@ namespace CMLeonOS.Gui.Apps }; wm.AddWindow(markItButton); + Button musicButton = new Button(openWithWindow, 108, 98, 84, 24); + musicButton.Text = "Music"; + musicButton.OnClick = (_, _) => + { + wm.RemoveWindow(openWithWindow); + OpenWithApp(path, "Music Editor"); + }; + wm.AddWindow(musicButton); + Button cancelButton = new Button(openWithWindow, openWithWindow.Width - 80 - 12, openWithWindow.Height - 20 - 12, 80, 20); cancelButton.Text = "Cancel"; cancelButton.OnClick = (_, _) => diff --git a/Gui/Apps/MusicEditor.cs b/Gui/Apps/MusicEditor.cs new file mode 100644 index 0000000..7973e78 --- /dev/null +++ b/Gui/Apps/MusicEditor.cs @@ -0,0 +1,498 @@ +using CMLeonOS; +using CMLeonOS.Gui.UILib; +using System; +using System.Collections.Generic; +using System.IO; + +namespace CMLeonOS.Gui.Apps +{ + internal class MusicEditor : Process + { + internal MusicEditor() : base("Music Editor", ProcessType.Application) { } + internal MusicEditor(string openPath) : base("Music Editor", ProcessType.Application) + { + path = openPath; + } + + private class NoteEvent + { + public int Tick; + public string Note = "C4"; + public int DurationMs = 200; + } + + private const string FileMagic = "MUS1"; + private const int DefaultTickMs = 200; + + private AppWindow window; + private readonly WindowManager wm = ProcessManager.GetProcess(); + + private ShortcutBar shortcutBar; + private Table notesTable; + private NumericUpDown tickInput; + private NumericUpDown durationInput; + private Dropdown noteDropdown; + private TextBox statusText; + + private FileBrowser fileBrowser; + private string path; + private bool modified; + + private readonly List notes = new List(); + private readonly List playback = new List(); + private bool playing; + private DateTime playbackStart; + private int playIndex; + + private readonly string[] noteNames = new[] + { + "C3","D3","E3","F3","G3","A3","B3", + "C4","D4","E4","F4","G4","A4","B4", + "C5","D5","E5","F5","G5","A5","B5" + }; + + private void UpdateTitle() + { + string name = string.IsNullOrWhiteSpace(path) ? "Untitled.mus" : Path.GetFileName(path); + window.Title = modified ? $"{name}* - Music Editor" : $"{name} - Music Editor"; + } + + private void UpdateStatus(string text) + { + statusText.Text = text; + statusText.Render(); + } + + private void RefreshTable() + { + notesTable.Cells.Clear(); + for (int i = 0; i < notes.Count; i++) + { + NoteEvent n = notes[i]; + notesTable.Cells.Add(new TableCell($"{i:D2} | t={n.Tick} | {n.Note} | {n.DurationMs}ms", i)); + } + notesTable.Render(); + wm.Update(notesTable); + } + + private void MarkModified() + { + modified = true; + UpdateTitle(); + } + + private void NewProject() + { + StopPlayback(); + notes.Clear(); + path = null; + modified = false; + RefreshTable(); + UpdateStatus("New project."); + UpdateTitle(); + } + + private void AddNote() + { + NoteEvent n = new NoteEvent + { + Tick = tickInput.Value, + DurationMs = durationInput.Value, + Note = noteDropdown.SelectedIndex >= 0 ? noteDropdown.SelectedText : "C4" + }; + + notes.Add(n); + SortNotesByTick(notes); + RefreshTable(); + MarkModified(); + UpdateStatus($"Added {n.Note} at tick {n.Tick}."); + } + + private void RemoveSelected() + { + int idx = notesTable.SelectedCellIndex; + if (idx < 0 || idx >= notes.Count) + { + UpdateStatus("No note selected."); + return; + } + + notes.RemoveAt(idx); + RefreshTable(); + MarkModified(); + UpdateStatus("Removed selected note."); + } + + private void SaveToPath(string targetPath) + { + if (string.IsNullOrWhiteSpace(targetPath)) + { + UpdateStatus("Invalid path."); + return; + } + + List lines = new List(); + lines.Add(FileMagic); + lines.Add($"TICK_MS={DefaultTickMs}"); + for (int i = 0; i < notes.Count; i++) + { + NoteEvent n = notes[i]; + lines.Add($"{n.Tick}|{n.Note}|{n.DurationMs}"); + } + + File.WriteAllLines(targetPath, lines.ToArray()); + path = targetPath; + modified = false; + UpdateTitle(); + UpdateStatus($"Saved: {Path.GetFileName(targetPath)}"); + } + + private void Save() + { + if (string.IsNullOrWhiteSpace(path)) + { + SaveAs(); + return; + } + SaveToPath(path); + } + + private void SaveAs() + { + fileBrowser = new FileBrowser(this, wm, (selectedPath) => + { + if (string.IsNullOrWhiteSpace(selectedPath)) + { + return; + } + + string finalPath = selectedPath; + if (!finalPath.EndsWith(".mus")) + { + finalPath += ".mus"; + } + SaveToPath(finalPath); + }, selectDirectoryOnly: true); + fileBrowser.Show(); + } + + private void OpenFromPath(string openPath) + { + if (!File.Exists(openPath)) + { + new MessageBox(this, "Music Editor", "File does not exist.").Show(); + return; + } + + string[] lines = File.ReadAllLines(openPath); + if (lines.Length == 0 || lines[0].Trim() != FileMagic) + { + new MessageBox(this, "Music Editor", "Invalid .mus format.").Show(); + return; + } + + StopPlayback(); + notes.Clear(); + + for (int i = 1; i < lines.Length; i++) + { + string line = lines[i].Trim(); + if (string.IsNullOrWhiteSpace(line) || line.StartsWith("TICK_MS=")) + { + continue; + } + + string[] parts = line.Split('|'); + if (parts.Length < 3) + { + continue; + } + + if (!int.TryParse(parts[0], out int tick)) + { + continue; + } + if (!int.TryParse(parts[2], out int duration)) + { + continue; + } + + if (duration < 10) + { + duration = 10; + } + + notes.Add(new NoteEvent + { + Tick = tick, + Note = parts[1], + DurationMs = duration + }); + } + + SortNotesByTick(notes); + path = openPath; + modified = false; + RefreshTable(); + UpdateTitle(); + UpdateStatus($"Opened: {Path.GetFileName(openPath)}"); + } + + private void Open() + { + fileBrowser = new FileBrowser(this, wm, (selectedPath) => + { + if (string.IsNullOrWhiteSpace(selectedPath)) + { + return; + } + + OpenFromPath(selectedPath); + }); + fileBrowser.Show(); + } + + private int GetNoteFrequency(string note) + { + switch ((note ?? string.Empty).Trim().ToUpper()) + { + case "C3": return 131; + case "D3": return 147; + case "E3": return 165; + case "F3": return 175; + case "G3": return 196; + case "A3": return 220; + case "B3": return 247; + case "C4": return 262; + case "D4": return 294; + case "E4": return 330; + case "F4": return 349; + case "G4": return 392; + case "A4": return 440; + case "B4": return 494; + case "C5": return 523; + case "D5": return 587; + case "E5": return 659; + case "F5": return 698; + case "G5": return 784; + case "A5": return 880; + case "B5": return 988; + default: return 440; + } + } + + private void Play() + { + if (notes.Count == 0) + { + UpdateStatus("No notes to play."); + return; + } + + playback.Clear(); + for (int i = 0; i < notes.Count; i++) + { + playback.Add(new NoteEvent + { + Tick = notes[i].Tick, + Note = notes[i].Note, + DurationMs = notes[i].DurationMs + }); + } + SortNotesByTick(playback); + + playIndex = 0; + playbackStart = DateTime.Now; + playing = true; + UpdateStatus("Playing..."); + } + + private void StopPlayback() + { + if (!playing) + { + return; + } + + playing = false; + playIndex = 0; + playback.Clear(); + UpdateStatus("Stopped."); + } + + private void Layout() + { + int topBarHeight = 24; + int controlY = topBarHeight + 8; + int rowHeight = 26; + + shortcutBar.MoveAndResize(0, 0, window.Width, topBarHeight); + shortcutBar.Render(); + + tickInput.MoveAndResize(8, controlY, 100, rowHeight); + noteDropdown.MoveAndResize(116, controlY, 100, rowHeight); + durationInput.MoveAndResize(224, controlY, 110, rowHeight); + + int tableY = controlY + rowHeight + 8; + int statusHeight = 24; + notesTable.MoveAndResize(8, tableY, window.Width - 16, window.Height - tableY - statusHeight - 10); + notesTable.Render(); + + statusText.MoveAndResize(8, window.Height - statusHeight - 6, window.Width - 16, statusHeight); + statusText.Render(); + } + + private void SortNotesByTick(List list) + { + // Avoid List.Sort for IL2CPU compatibility. + for (int i = 1; i < list.Count; i++) + { + NoteEvent key = list[i]; + int j = i - 1; + while (j >= 0 && list[j].Tick > key.Tick) + { + list[j + 1] = list[j]; + j--; + } + list[j + 1] = key; + } + } + + public override void Start() + { + base.Start(); + + window = new AppWindow(this, 180, 100, 760, 460); + window.Title = "Untitled.mus - Music Editor"; + window.Icon = AppManager.DefaultAppIcon; + window.CanResize = true; + window.UserResized = Layout; + window.Closing = TryStop; + wm.AddWindow(window); + + shortcutBar = new ShortcutBar(window, 0, 0, window.Width, 24); + shortcutBar.Cells.Add(new ShortcutBarCell("New", NewProject)); + shortcutBar.Cells.Add(new ShortcutBarCell("Open", Open)); + shortcutBar.Cells.Add(new ShortcutBarCell("Save", Save)); + shortcutBar.Cells.Add(new ShortcutBarCell("Save As", SaveAs)); + shortcutBar.Cells.Add(new ShortcutBarCell("Add Note", AddNote)); + shortcutBar.Cells.Add(new ShortcutBarCell("Remove", RemoveSelected)); + shortcutBar.Cells.Add(new ShortcutBarCell("Play", Play)); + shortcutBar.Cells.Add(new ShortcutBarCell("Stop", StopPlayback)); + shortcutBar.Render(); + wm.AddWindow(shortcutBar); + + tickInput = new NumericUpDown(window, 8, 32, 100, 26); + tickInput.Minimum = 0; + tickInput.Maximum = 4096; + tickInput.Value = 0; + tickInput.Step = 1; + wm.AddWindow(tickInput); + + noteDropdown = new Dropdown(window, 116, 32, 100, 26); + noteDropdown.PlaceholderText = "Note"; + for (int i = 0; i < noteNames.Length; i++) + { + noteDropdown.Items.Add(noteNames[i]); + } + noteDropdown.RefreshItems(); + noteDropdown.SelectedIndex = 7; // C4 + wm.AddWindow(noteDropdown); + + durationInput = new NumericUpDown(window, 224, 32, 110, 26); + durationInput.Minimum = 10; + durationInput.Maximum = 2000; + durationInput.Step = 10; + durationInput.Value = 200; + wm.AddWindow(durationInput); + + notesTable = new Table(window, 8, 68, window.Width - 16, window.Height - 100); + notesTable.CellHeight = 22; + notesTable.AllowDeselection = false; + notesTable.TableCellSelected = (index) => + { + if (index < 0 || index >= notes.Count) + { + return; + } + NoteEvent n = notes[index]; + tickInput.Value = n.Tick; + durationInput.Value = n.DurationMs; + for (int i = 0; i < noteDropdown.Items.Count; i++) + { + if (noteDropdown.Items[i] == n.Note) + { + noteDropdown.SelectedIndex = i; + break; + } + } + UpdateStatus($"Selected {n.Note} at tick {n.Tick}."); + }; + wm.AddWindow(notesTable); + + statusText = new TextBox(window, 8, window.Height - 30, window.Width - 16, 24); + statusText.ReadOnly = true; + statusText.Text = "Ready. Add notes and press Play."; + wm.AddWindow(statusText); + + Layout(); + RefreshTable(); + UpdateTitle(); + + if (!string.IsNullOrWhiteSpace(path) && File.Exists(path)) + { + OpenFromPath(path); + } + + wm.Update(window); + } + + public override void Run() + { + if (!playing) + { + return; + } + + if (playIndex >= playback.Count) + { + playing = false; + UpdateStatus("Playback complete."); + return; + } + + int elapsedMs = (int)(DateTime.Now - playbackStart).TotalMilliseconds; + NoteEvent next = playback[playIndex]; + int targetMs = next.Tick * DefaultTickMs; + if (elapsedMs < targetMs) + { + return; + } + + int frequency = GetNoteFrequency(next.Note); + int duration = next.DurationMs; + if (duration < 20) duration = 20; + if (duration > 500) duration = 500; + + // Cosmos beep is mono beeper output; use short pulse per note. + Console.Beep(); + + playIndex++; + if (playIndex >= playback.Count) + { + playing = false; + UpdateStatus("Playback complete."); + } + else + { + UpdateStatus($"Playing: {next.Note} ({frequency}Hz, {duration}ms)"); + } + } + + public override void Stop() + { + StopPlayback(); + base.Stop(); + } + } +} diff --git a/System/BootMenu.cs b/System/BootMenu.cs index 6530d8d..791c5d3 100644 --- a/System/BootMenu.cs +++ b/System/BootMenu.cs @@ -105,8 +105,9 @@ namespace CMLeonOS int height = Console.WindowHeight; int panelWidth = Math.Min(MaxPanelWidth, Math.Max(MinPanelWidth, width - 8)); int panelLeft = Math.Max(0, (width - panelWidth) / 2); - int contentLines = 9 + options.Length; - int panelTop = Math.Max(0, (height - contentLines) / 2); + int contentLineCount = 8 + options.Length; + int panelHeight = contentLineCount + 2; + int panelTop = Math.Max(0, (height - panelHeight) / 2); DrawHorizontalBorder(panelLeft, panelTop, panelWidth, true, ConsoleColor.Cyan, ConsoleColor.Black); DrawPanelLine(panelLeft, panelTop, panelWidth, 1, " CMLeonOS Boot Manager ", ConsoleColor.Black, ConsoleColor.Cyan); @@ -136,7 +137,7 @@ namespace CMLeonOS ); } - DrawHorizontalBorder(panelLeft, panelTop + contentLines, panelWidth, false, ConsoleColor.Cyan, ConsoleColor.Black); + DrawHorizontalBorder(panelLeft, panelTop + panelHeight - 1, panelWidth, false, ConsoleColor.Cyan, ConsoleColor.Black); Console.ResetColor(); }