mirror of
https://github.com/Leonmmcoset/CMLeonOS.git
synced 2026-04-21 19:24:00 +00:00
增加音乐播放器
This commit is contained in:
@@ -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 = (_, _) =>
|
||||
|
||||
498
Gui/Apps/MusicEditor.cs
Normal file
498
Gui/Apps/MusicEditor.cs
Normal file
@@ -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<WindowManager>();
|
||||
|
||||
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<NoteEvent> notes = new List<NoteEvent>();
|
||||
private readonly List<NoteEvent> playback = new List<NoteEvent>();
|
||||
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<string> lines = new List<string>();
|
||||
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<NoteEvent> list)
|
||||
{
|
||||
// Avoid List<T>.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user