feat: C# Fur Elise player using Windows Beep API

This commit is contained in:
2026-09-21 10:58:32 +08:00
commit 7ece384453
5 changed files with 221 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
bin/
obj/
*.user
*.dll
*.exe
*.pdb
.vs/
.idea/
+10
View File
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>FurElise</AssemblyName>
<RootNamespace>FurElise</RootNamespace>
</PropertyGroup>
</Project>
+9
View File
@@ -0,0 +1,9 @@
{
"runtimeOptions": {
"tfm": "net10.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "10.0.12"
}
}
}
+132
View File
@@ -0,0 +1,132 @@
// Für Elise (致爱丽丝) — C# console player
// Windows: kernel32 Beep; --dry-run prints score only.
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
class FurElisePlayer
{
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool Beep(uint dwFreq, uint dwDuration);
static readonly Dictionary<string, int> Freq = new(StringComparer.OrdinalIgnoreCase)
{
["C4"] = 262, ["D4"] = 294, ["Ds4"] = 311, ["E4"] = 330,
["F4"] = 349, ["G4"] = 392, ["Gs4"] = 415, ["A4"] = 440,
["As4"] = 466, ["B4"] = 494,
["C5"] = 523, ["D5"] = 587, ["Ds5"] = 622, ["E5"] = 659,
["F5"] = 698, ["G5"] = 784, ["Gs5"] = 831, ["A5"] = 880,
["B5"] = 988, ["C6"] = 1047,
["A3"] = 220, ["B3"] = 247, ["E3"] = 165,
};
// note name, duration in 16th-note units (4 = quarter), rest = "R"
static readonly (string Note, int Units)[] Score =
{
// Theme A (famous opening)
("E5", 2), ("Ds5", 2), ("E5", 2), ("Ds5", 2), ("E5", 2),
("B4", 2), ("D5", 2), ("C5", 2),
("A4", 6), ("R", 2),
("C4", 2), ("E4", 2), ("A4", 2),
("B4", 6), ("R", 2),
("E4", 2), ("Gs4", 2), ("B4", 2),
("C5", 6), ("R", 2),
("E4", 2), ("E5", 2), ("Ds5", 2), ("E5", 2), ("Ds5", 2), ("E5", 2),
("B4", 2), ("D5", 2), ("C5", 2),
("A4", 6), ("R", 2),
("C4", 2), ("E4", 2), ("A4", 2),
("B4", 6), ("R", 2),
("E4", 2), ("C5", 2), ("B4", 2),
("A4", 12),
// Theme B (middle section, simplified)
("E4", 2), ("C5", 2), ("B4", 2),
("A4", 4), ("E4", 2), ("F4", 2),
("E4", 2), ("A4", 2), ("B4", 2), ("C5", 2),
("B4", 4), ("E4", 2), ("Gs4", 2),
("A4", 4), ("E4", 2), ("B4", 2),
("C5", 2), ("A4", 2), ("B4", 2), ("A4", 2),
("E5", 2), ("Ds5", 2), ("E5", 2), ("Ds5", 2), ("E5", 2),
("B4", 2), ("D5", 2), ("C5", 2),
("A4", 6), ("R", 2),
("C4", 2), ("E4", 2), ("A4", 2),
("B4", 6), ("R", 2),
("E4", 2), ("Gs4", 2), ("B4", 2),
("C5", 6), ("R", 2),
("E4", 2), ("E5", 2), ("Ds5", 2), ("E5", 2), ("Ds5", 2), ("E5", 2),
("B4", 2), ("D5", 2), ("C5", 2),
("A4", 6), ("R", 2),
("C4", 2), ("E4", 2), ("A4", 2),
("B4", 6), ("R", 2),
("E4", 2), ("C5", 2), ("B4", 2),
("A4", 12),
};
static int Main(string[] args)
{
var dryRun = false;
var bpm = 72; // Für Elise ~ 72 eighth-ish feel; we treat unit = 16th
var loops = 1;
for (var i = 0; i < args.Length; i++)
{
var a = args[i];
if (a == "--dry-run") dryRun = true;
else if (a == "--bpm" && i + 1 < args.Length) bpm = int.Parse(args[++i]);
else if (a == "--loops" && i + 1 < args.Length) loops = int.Parse(args[++i]);
else if (a == "--help" || a == "-h")
{
Console.WriteLine("FurElisePlayer [--dry-run] [--bpm 72] [--loops 1]");
Console.WriteLine("Play Beethoven - Für Elise via Windows Beep.");
return 0;
}
}
if (bpm < 40) bpm = 40;
if (bpm > 200) bpm = 200;
if (loops < 1) loops = 1;
// 16th-note duration in ms from BPM (quarter note = 60000/bpm)
var unitMs = (int)Math.Round(60000.0 / bpm / 4.0);
if (unitMs < 20) unitMs = 20;
Console.WriteLine("Beethoven - Für Elise (致爱丽丝)");
Console.WriteLine($"BPM={bpm}, unit={unitMs}ms, notes={Score.Length}, loops={loops}");
Console.WriteLine(dryRun ? "Mode: dry-run (no sound)" : "Mode: play");
Console.WriteLine(new string('-', 48));
for (var loop = 1; loop <= loops; loop++)
{
if (loops > 1) Console.WriteLine($"--- loop {loop}/{loops} ---");
foreach (var (note, units) in Score)
{
var ms = units * unitMs;
// slight articulation: leave a tiny gap
var soundMs = Math.Max(15, ms - 8);
if (note == "R" || note == "-" || note.Length == 0)
{
if (dryRun) Console.WriteLine($" REST {ms,4} ms");
else Thread.Sleep(ms);
continue;
}
if (!Freq.TryGetValue(note, out var hz))
{
Console.Error.WriteLine($"Unknown note: {note}");
continue;
}
if (dryRun)
Console.WriteLine($" {note,-5} {hz,4} Hz {ms,4} ms");
else if (!Beep((uint)hz, (uint)soundMs))
Console.Beep(hz, soundMs);
}
}
Console.WriteLine("Done.");
return 0;
}
}
+62
View File
@@ -0,0 +1,62 @@
# FurElise C#
用 C# 控制台程序在 Windows 上演奏贝多芬《致爱丽丝》(Für Elise)。
## 原理
- 使用 Windows 内核 API `kernel32!Beep(frequency, duration)` 发声
- 乐谱数据用 `(音名, 时值)` 数组描述,`R` 表示休止
- 音名映射到平均律频率(如 `A4=440``E5=659``Ds5=622`
## 运行
### 方式 A:已编译程序
```powershell
dotnet FurElise.dll
# 或
.\FurElise.exe
```
### 方式 B:源码直接编译(.NET 10 csc
```powershell
$refDir = "C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\10.0.12\ref\net10.0"
$refs = (Get-ChildItem "$refDir\*.dll" | ForEach-Object { "-r:`"$($_.FullName)`"" }) -join ' '
dotnet exec "C:\Program Files\dotnet\sdk\10.0.401\Roslyn\bincore\csc.dll" `
-nologo -t:exe -out:FurElise.dll $refs Program.cs
# 复制 runtimeconfig.json 后
dotnet FurElise.dll
```
### 方式 Cdotnet run(需可用的 SDK/NuGet
```powershell
dotnet run --project FurElise.csproj
```
## 参数
| 参数 | 说明 | 默认 |
|------|------|------|
| `--dry-run` | 只打印音符与频率,不发声 | 关 |
| `--bpm <n>` | 速度(四分音符 BPM | 72 |
| `--loops <n>` | 重复次数 | 1 |
| `--help` | 帮助 | |
示例:
```powershell
.\FurElise.exe --dry-run
.\FurElise.exe --bpm 90 --loops 2
```
## 说明
- 仅 Windows(依赖 `Beep` API);无声卡驱动策略时部分环境可能无声音
- 旋律为主题段落的简化编配,适合教学演示
- 请勿在文章/仓库中提交站点账号密码
## License
MIT