diff --git a/Readme.md b/Readme.md index 402d96e..05c2ed2 100644 --- a/Readme.md +++ b/Readme.md @@ -1,356 +1,130 @@ -# Mono + GTK# + Glade — WSL Setup Guide - -## Table of Contents -- [1. Prerequisites](#1-prerequisites) -- [2. Install Mono](#2-install-mono) -- [3. Install GTK# (GTK# 3.0, for Ubuntu 24.04)](#3-install-gtk-gtk-30-for-ubuntu-2404) -- [4. Install Glade (visual UI designer)](#4-install-glade-visual-ui-designer) -- [5. Optional: libgdiplus (only if using System.Drawing)](#5-optional-libgdiplus-only-if-using-systemdrawing) -- [6. Compiling & Running](#6-compiling--running) -- [7. Minimal Working Examples](#7-minimal-working-examples) -- [8. Custom Output Names, Targets & Exporting DLLs](#8-custom-output-names-targets--exporting-dlls) -- [9. Common Errors & Fixes](#9-common-errors--fixes) -- [10. Notes on Alternatives](#10-notes-on-alternatives) - ---- - -## 1. Prerequisites -- Windows 11 (or updated Windows 10) with WSL2 and **WSLg** enabled for GUI support -- Ubuntu 24.04 (noble) or similar WSL distro - -Check WSLg is active: -```bash -echo $DISPLAY -``` -Should return something like `:0`. If empty, GUI apps won't display. - ---- - -## 2. Install Mono - -```bash -sudo apt update -sudo apt install mono-complete -``` - -Verify: -```bash -mono --version -mcs --version -``` - ---- - -## 3. Install GTK# (GTK# 3.0, for Ubuntu 24.04) - -> Note: `gtk-sharp2` is **not available** on Ubuntu 24.04 (noble). Use `gtk-sharp3` instead. - -```bash -sudo add-apt-repository universe -sudo apt update -sudo apt install gtk-sharp3 -``` - -Verify the package is registered: -```bash -pkg-config --list-all | grep gtk-sharp -``` -Expected output: -gtk-sharp-3.0 Gtk - Gtk ---- - -## 4. Install Glade (visual UI designer) - -```bash -sudo apt install glade -``` - -Launch it: -```bash -glade -``` -Design your UI visually, then save as `yourfile.glade` (XML format). - ---- - -## 5. Optional: libgdiplus (only if using System.Drawing) - -Only needed if your code uses `System.Drawing.Bitmap`, `Graphics`, etc. (not required for plain GTK#/Glade apps): - -```bash -sudo apt install libgdiplus -``` - ---- - -## 6. Compiling & Running - -### Plain console C# program -```bash -mcs myprogram.cs -mono myprogram.exe -``` - -### GTK# 3.0 program (no Glade) -```bash -mcs -pkg:gtk-sharp-3.0 myapp.cs -mono myapp.exe -``` - -### GTK# 3.0 program using a Glade file -```bash -mcs -pkg:gtk-sharp-3.0 -pkg:glade-sharp-3.0 myapp.cs -mono myapp.exe -``` -Make sure the `.glade` file referenced in code sits in the same folder (or update the path in `new Builder("...")`). - ---- - -## 7. Minimal Working Examples - -### Hello World (console) -```csharp -using System; - -class Hello { - static void Main() { - Console.WriteLine("Hello from Mono in WSL!"); - } -} -``` -```bash -mcs hello.cs -mono hello.exe -``` - -### Hello World (GTK# window, no Glade) -```csharp -using System; -using Gtk; - -class GtkCheck { - static void Main() { - Application.Init(); - var win = new Window("Hello GTK#3"); - win.SetDefaultSize(300, 200); - win.DeleteEvent += (o, args) => Application.Quit(); - - var button = new Button("Click me"); - button.Clicked += (o, args) => Console.WriteLine("Clicked!"); - win.Add(button); - - win.ShowAll(); - Application.Run(); - } -} -``` -```bash -mcs -pkg:gtk-sharp-3.0 gtkcheck.cs -mono gtkcheck.exe -``` - -### Loading a UI built in Glade -```csharp -using System; -using Gtk; - -class Program { - static void Main() { - Application.Init(); - var builder = new Builder("myui.glade"); - builder.Autoconnect(new Program()); - - var win = (Window)builder.GetObject("window1"); - win.DeleteEvent += (o, args) => Application.Quit(); - win.ShowAll(); - - Application.Run(); - } -} -``` -```bash -mcs -pkg:gtk-sharp-3.0 -pkg:glade-sharp-3.0 program.cs -mono program.exe -``` - ---- - -## 8. Custom Output Names, Targets & Exporting DLLs - -By default, `mcs file.cs` names the output after the source file (`file.exe`). You can control this with `-out:` and change what kind of binary is produced with `-target:`. - -### 8.1 Custom output name -```bash -mcs -pkg:gtk-sharp-3.0 Program.cs -out:MyCustomApp.exe -``` -Run it the same way: -```bash -mono MyCustomApp.exe -``` - -### 8.2 `-target` options -| Target | Produces | Notes | -|---|---|---| -| `exe` (default) | Console executable | Shows a console window when run on Windows | -| `winexe` | Windows GUI executable | No console window — correct choice for GTK#/Glade GUI apps distributed on Windows | -| `library` | `.dll` class library | No `Main` entry point required to run directly; used by other programs | -| `module` | `.netmodule` | Rarely used; for combining into a single assembly later | - -Your example, building a windowed GUI executable with a custom name: -```bash -mcs -target:winexe -pkg:gtk-sharp-3.0 Program.cs -out:programwin.exe -``` -This produces `programwin.exe`, which on Windows will run without popping up a console window alongside your GTK window. - -### 8.3 Exporting a DLL - -If you want to package reusable code (helper classes, business logic, etc.) as a library instead of a standalone app: - -```bash -mcs -target:library Program.cs -out:MyLibrary.dll -``` - -- No `Main()` method is required in a `library` target (though it's fine if one class in your project has one — it just won't be used as an entry point for the DLL itself). -- This creates `MyLibrary.dll`, a Mono/.NET assembly that other C# programs can reference. - -### 8.4 Using a DLL in another program - -Suppose `MyLibrary.dll` contains: -```csharp -// MyLibrary.cs -namespace MyLib { - public class Greeter { - public string Greet(string name) => $"Hello, {name}!"; - } -} -``` -Compiled with: -```bash -mcs -target:library MyLibrary.cs -out:MyLibrary.dll -``` - -Then in a separate consumer program: -```csharp -// ConsumerApp.cs -using System; -using MyLib; - -class ConsumerApp { - static void Main() { - var greeter = new Greeter(); - Console.WriteLine(greeter.Greet("World")); - } -} -``` - -Compile it while referencing the DLL with `-r:`: -```bash -mcs ConsumerApp.cs -r:MyLibrary.dll -out:ConsumerApp.exe -``` - -Run it (make sure `MyLibrary.dll` is in the same folder as `ConsumerApp.exe`, or on Mono's library search path): -```bash -mono ConsumerApp.exe -``` - -### 8.5 Combining `-target:library` with GTK# packages -If your DLL itself uses GTK# types (e.g., a shared custom widget), include the package flag when building the library too: -```bash -mcs -target:library -pkg:gtk-sharp-3.0 MyGtkWidgets.cs -out:MyGtkWidgets.dll -``` -And reference both the package and the DLL when compiling the consumer: -```bash -mcs -pkg:gtk-sharp-3.0 ConsumerApp.cs -r:MyGtkWidgets.dll -out:ConsumerApp.exe -``` - -### 8.6 Quick reference -```bash -# Console exe, custom name -mcs Program.cs -out:myapp.exe - -# Windowed (no console) exe, custom name -mcs -target:winexe -pkg:gtk-sharp-3.0 Program.cs -out:programwin.exe - -# Class library (DLL) -mcs -target:library Library.cs -out:MyLibrary.dll - -# Program referencing that DLL -mcs Consumer.cs -r:MyLibrary.dll -out:Consumer.exe -``` - -### 8.7 Running the compiled .exe on Windows (outside WSL) - -Compiling in WSL produces a `.exe` that targets the .NET/Mono runtime — it is **not** a native Windows binary, and it will not run on Windows by itself. Two things are needed: - -**1. Install a runtime on the Windows machine** - -Pick one: -- **Mono runtime for Windows** — install the [Mono for Windows](https://www.mono-project.com/download/stable/) package so `.exe` files built with `mcs` can run via the Mono runtime. After install, you can either double-click the `.exe` (Mono associates itself with `.exe` files) or run it explicitly: -```powershell - mono programwin.exe -``` -- **.NET Framework / .NET runtime** — for simple GTK#-free console apps, the .exe may also run under an installed .NET runtime, but GTK# apps specifically require Mono's GTK# libraries, so the Mono for Windows install is the reliable option. - -**2. Copy the required DLLs alongside the .exe** - -Your compiled `.exe` references managed DLLs at runtime (not just at compile time). Anything you referenced with `-r:` or `-pkg:` must physically travel with the `.exe`: - -- Any custom library you built, e.g. `MyLibrary.dll` or `MyGtkWidgets.dll` -- GTK# binding assemblies your app depends on (typically already provided by installing Mono for Windows, since it bundles the GTK# runtime — but if you used a nonstandard package, copy its DLL too) -- Your `.glade` file, if the app loads one via `Builder("myui.glade")` — this isn't a DLL but is just as required; it must sit next to the `.exe` at the same relative path your code expects - -Minimum folder layout to copy over to Windows: -programwin.exe -MyLibrary.dll (any custom DLLs you referenced with -r:) -myui.glade (if using Glade) - -**Common pitfall:** a program that runs fine in WSL but fails on Windows with `FileNotFoundException` or `Could not load file or assembly` almost always means a referenced DLL (or the `.glade` file) wasn't copied over, or Mono for Windows isn't installed. - ---- - -## 9. Common Errors & Fixes - -| Error | Cause | Fix | -|---|---|---| -| `File does not contain a valid CIL image` | Ran `mono` on `.cs` source instead of compiled `.exe` | Compile first: `mcs file.cs`, then `mono file.exe` | -| `CS0246: type or namespace 'Gtk' could not be found` | Missing package reference during compile | Add `-pkg:gtk-sharp-3.0` to `mcs` command | -| `CS0103: name 'Console' does not exist` | Missing `using System;` | Add `using System;` at top of file | -| `Unable to locate package gtk-sharp2` | Package dropped in Ubuntu 24.04 | Use `gtk-sharp3` instead | -| `DllNotFoundException: libgdiplus` | Code uses `System.Drawing` without the lib installed | `sudo apt install libgdiplus` | -| `Could not load file or assembly 'MyLibrary.dll'` | DLL not in same folder / not found | Place DLL next to the `.exe`, or set `MONO_PATH` | -| `FileNotFoundException` / `Could not load file or assembly` (on Windows) | Runtime not installed, or referenced DLL/.glade not copied alongside the .exe | Install Mono for Windows; copy all `-r:` DLLs and `.glade` files into the same folder as the `.exe` | - ---- - -## 10. Notes on Alternatives - -GTK# is a legacy, lightly-maintained binding. For new projects, consider: -- **Avalonia UI** — modern, XAML-based, cross-platform, actively maintained -```bash - sudo apt install dotnet-sdk-8.0 - dotnet new install Avalonia.Templates - dotnet new avalonia.app -o MyApp -``` - -Use GTK#/Mono/Glade for legacy projects or learning; use Avalonia + `dotnet` for anything new and long-term. - - -Personal note: - -Compile note: - to compile the cs file - mcs NAME_OF_THE_FILE.cs - for GTK appilcation - mcs -pkg:gtk-sharp-3.0 NAME_OF_THE_FILE.cs - custom output name / windowed exe - mcs -target:winexe -pkg:gtk-sharp-3.0 NAME_OF_THE_FILE.cs -out:CUSTOM_NAME.exe - export as DLL - mcs -target:library NAME_OF_THE_FILE.cs -out:CUSTOM_NAME.dll - use a DLL in another program - mcs Consumer.cs -r:CUSTOM_NAME.dll -out:Consumer.exe - - - -to run note: - same for all cases - mono NAME_OF_THE_FILE.exe \ No newline at end of file +# Audio Test Bundle + +Standalone harness for verifying the OpenAL playback path used by the EARS +Linux build. Plays a WAV twice — once through `paplay` (reference), once +through OpenAL via OpenTK — so a bad file, a bad parser and a bad audio +backend can be told apart. + +## Contents + +| File | Purpose | +|---|---| +| `altest.exe` | the test program | +| `OpenTK.dll` | managed OpenAL binding | +| `OpenTK.dll.config` | dllmap: `openal32.dll` → `libopenal.so.1` | +| `alsoft.conf` | OpenAL Soft settings (backend, resampler, buffers) | +| `sound/*.wav` | test assets | +| `src/altest.cs`, `src/WavFile.cs` | sources, only needed to rebuild | + +`OpenTK.dll` and `OpenTK.dll.config` must sit **next to `altest.exe`**. Mono +resolves assemblies relative to the executable, not the working directory. +A missing `.config` produces `DllNotFoundException: openal32.dll` on Linux, +which reads like nonsense until you know what it means. + +## Install + +```bash +sudo apt install mono-runtime libmono-system-drawing4.0-cil \ + libopenal1 openal-info pulseaudio-utils sox +``` + +`libmono-system-drawing4.0-cil` is easy to miss. OpenTK's net45 build +references System.Drawing even though the audio path never touches it. + +Add `mono-mcs` only if rebuilding on this machine. + +## Run + +```bash +cd audiotest +export ALSOFT_CONF=$PWD/alsoft.conf + +soxi sound/SND200.wav # what the file actually is +paplay sound/SND200.wav # reference playback +mono ./altest.exe sound/SND200.wav +mono ./altest.exe sound/SND200.wav 0.5 # optional gain, 0..1 +``` + +`ALSOFT_CONF` is required. Without it OpenAL only reads `~/.alsoftrc`, which +won't exist on a fresh machine, and the bundled settings are ignored silently. + +Verbose mixer log: + +```bash +ALSOFT_LOGLEVEL=3 mono ./altest.exe sound/SND200.wav 2>&1 | \ + grep -E "backend|Post-start|underrun|Error|resampler|Created device" +``` + +## Reading the result + +| Symptom | Meaning | +|---|---| +| Clean in both | audio path is good | +| Clean in `paplay`, bad in `altest` | backend or resampler config — check the log | +| Bad in both | source file, or the sound device itself | +| Bad at gain 1.0, clean at 0.5 | clipping; cap gain at 0.85 in `AudioEngine.SetGain` | +| `DllNotFoundException` | `OpenTK.dll.config` missing next to the exe | + +Key log lines: + +- `Initialized backend "pulse"` — which backend won +- `Post-start: Stereo, Float32, 44100hz, 882 / 2646 buffer` — negotiated rate + and buffer. Small buffers on a slow transport cause periodic ticks. +- `Created device ... "RDP Sink"` — under WSL this is the WSLg bridge, not + real hardware. Jitter here is usually the transport and disappears on the + target device. Don't tune against it. + +Assets are 11025 Hz mono. Against a 44100 or 48000 Hz device everything is +resampled; `bsinc24` in `alsoft.conf` handles this well. If a future device +negotiates a rate that isn't an integer multiple, pre-converting the assets +removes the question entirely: + +```bash +sox in.wav -r 48000 out.wav +``` + +## Sound comes out the wrong device + +Common on the tablet, where HDMI wins the default. + +```bash +pactl list short sinks # what exists right now +wpctl status # default is marked with * +``` + +Set the default **by name**, not by node ID: + +```bash +pactl set-default-sink alsa_output.platform-cht-bsw-rt5645.HiFi__Speaker__sink +``` + +Node IDs are not stable. UCM destroys and recreates the built-in sink every +time the headphone jack is plugged or unplugged, and the new node gets a new +ID. An ID captured a minute ago is probably already dead — this looks like a +sink "disappearing" but is normal behaviour. HDMI keeps a stable ID precisely +because nothing switches it, which is why it tends to win by default. + +The durable fix for a kiosk is to remove HDMI from the running: `pavucontrol` +→ Configuration → HDMI card → **Off**. This persists across reboots +(WirePlumber stores card profiles in `~/.local/state/wireplumber/`). With +HDMI gone, whichever built-in sink exists is the only candidate and jack +switching just works. + +Note: under PipeWire, `/etc/pulse/default.pa` and `~/.config/pulse/default.pa` +are **ignored**. An old config there will look like it should work while doing +nothing. The live state is `~/.local/state/wireplumber/default-nodes`. + +If a sink seems missing, restart the stack before assuming hardware failure: + +```bash +systemctl --user restart wireplumber pipewire pipewire-pulse +``` + +## Rebuilding + +```bash +mcs src/altest.cs src/WavFile.cs -r:OpenTK.dll -out:altest.exe +``` + +Keep `altest.cs` out of the main program's `*.cs` glob — two `Main` methods +in one build is a duplicate-entry-point error. \ No newline at end of file diff --git a/ReadmeQuick.md b/ReadmeQuick.md index f227bb0..76071ef 100644 --- a/ReadmeQuick.md +++ b/ReadmeQuick.md @@ -1,17 +1,17 @@ -### 8.6 Quick reference -```bash -# Console exe, custom name -mcs Program.cs -out:myapp.exe - -# Console exe, custom name -mcs -pkg:gtk-sharp-3.0 Program.cs -out:programwin.exe - -# Windowed (no console) exe, custom name -mcs -target:winexe -pkg:gtk-sharp-3.0 Program.cs -out:programwin.exe - -# Class library (DLL) -mcs -target:library Library.cs -out:MyLibrary.dll - -# Program referencing that DLL -mcs Consumer.cs -r:MyLibrary.dll -out:Consumer.exe -``` +### 8.6 Quick reference +```bash +# Console exe, custom name +mcs Program.cs -out:myapp.exe + +# Console exe, custom name +mcs -pkg:gtk-sharp-3.0 Program.cs -out:programwin.exe + +# Windowed (no console) exe, custom name +mcs -target:winexe -pkg:gtk-sharp-3.0 Program.cs -out:programwin.exe + +# Class library (DLL) +mcs -target:library Library.cs -out:MyLibrary.dll + +# Program referencing that DLL +mcs Consumer.cs -r:MyLibrary.dll -out:Consumer.exe +``` diff --git a/audiotest/OpenTK.dll b/audiotest/OpenTK.dll new file mode 100644 index 0000000..c335751 --- /dev/null +++ b/audiotest/OpenTK.dll Binary files differ diff --git a/audiotest/OpenTK.dll.config b/audiotest/OpenTK.dll.config new file mode 100644 index 0000000..7098d39 --- /dev/null +++ b/audiotest/OpenTK.dll.config @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/audiotest/WavFile.cs b/audiotest/WavFile.cs new file mode 100644 index 0000000..0ac207b --- /dev/null +++ b/audiotest/WavFile.cs @@ -0,0 +1,64 @@ +using System; +using System.IO; +using System.Text; +using OpenTK.Audio.OpenAL; + +public class WavFile +{ + public byte[] Data; + public ALFormat Format; // Al.FORMAT_* + public int SampleRate; + + public static WavFile Load(string path) + { + using (var fs = File.OpenRead(path)) + using (var br = new BinaryReader(fs)) + { + if (new string(br.ReadChars(4)) != "RIFF") throw new Exception("Not RIFF: " + path); + br.ReadInt32(); + if (new string(br.ReadChars(4)) != "WAVE") throw new Exception("Not WAVE: " + path); + + short channels = 0, bits = 0; + int rate = 0; + byte[] data = null; + + while (fs.Position < fs.Length - 8) + { + string id = new string(br.ReadChars(4)); + int len = br.ReadInt32(); + + if (id == "fmt ") + { + br.ReadInt16(); // audio format (1 = PCM) + channels = br.ReadInt16(); + rate = br.ReadInt32(); + br.ReadInt32(); // byte rate + br.ReadInt16(); // block align + bits = br.ReadInt16(); + if (len > 16) br.ReadBytes(len - 16); + } + else if (id == "data") + { + data = br.ReadBytes(len); + } + else + { + br.ReadBytes(len); // skip unknown chunk + } + + if ((len & 1) == 1) br.ReadByte(); // chunks are word-aligned + } + + if (data == null) throw new Exception("No data chunk: " + path); + + ALFormat fmt; + if (channels == 1 && bits == 8) fmt = ALFormat.Mono8; + else if (channels == 1 && bits == 16) fmt = ALFormat.Mono16; + else if (channels == 2 && bits == 8) fmt = ALFormat.Stereo8; + else if (channels == 2 && bits == 16) fmt = ALFormat.Stereo16; + else throw new Exception("Unsupported WAV: " + channels + "ch " + bits + "bit"); + + return new WavFile { Data = data, Format = fmt, SampleRate = rate }; + } + } +} \ No newline at end of file diff --git a/audiotest/alsoft.conf b/audiotest/alsoft.conf new file mode 100644 index 0000000..fdbb03b --- /dev/null +++ b/audiotest/alsoft.conf @@ -0,0 +1,6 @@ +[general] +drivers = pulse,alsa +resampler = bsinc24 +period_size = 1024 +periods = 4 +output-limiter = true \ No newline at end of file diff --git a/audiotest/altest.cs b/audiotest/altest.cs new file mode 100644 index 0000000..8b812af --- /dev/null +++ b/audiotest/altest.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading; +using OpenTK.Audio; +using OpenTK.Audio.OpenAL; + +class AlTest +{ + static void Main(string[] args) + { + if (args.Length < 1) + { + Console.WriteLine("usage: altest [gain 0..1]"); + return; + } + float gain = args.Length > 1 ? float.Parse(args[1]) : 1.0f; + + var ctx = new AudioContext(); + ctx.MakeCurrent(); + Console.WriteLine("device: " + AudioContext.CurrentContext.CurrentDevice); + + WavFile w = WavFile.Load(args[0]); + Console.WriteLine("{0} {1} Hz {2} bytes", w.Format, w.SampleRate, w.Data.Length); + + int buf = AL.GenBuffer(); + AL.BufferData(buf, w.Format, w.Data, w.Data.Length, w.SampleRate); + + int src = AL.GenSource(); + AL.Source(src, ALSourcei.Buffer, buf); + AL.Source(src, ALSourcef.Gain, gain); + AL.SourcePlay(src); + + ALError e = AL.GetError(); + if (e != ALError.NoError) Console.WriteLine("AL error: " + AL.GetErrorString(e)); + + while (AL.GetSourceState(src) == ALSourceState.Playing) + Thread.Sleep(50); + + AL.DeleteSource(src); + AL.DeleteBuffer(buf); + ctx.Dispose(); + } +} \ No newline at end of file diff --git a/audiotest/altest.exe b/audiotest/altest.exe new file mode 100644 index 0000000..283c6d2 --- /dev/null +++ b/audiotest/altest.exe Binary files differ diff --git a/audiotest/build.sh b/audiotest/build.sh new file mode 100644 index 0000000..e734470 --- /dev/null +++ b/audiotest/build.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -e +cd "$(dirname "$0")/.." + +OUT=audiotest +rm -rf "$OUT" +mkdir -p "$OUT/src" "$OUT/sound" + +cp altest.cs WavFile.cs "$OUT/src/" +cp OpenTK.dll OpenTK.dll.config "$OUT/" +cp bin/linux/sound/SND200.wav "$OUT/sound/" +cp audiotest_files/{install.sh,run.sh,alsoft.conf} "$OUT/" + +mcs "$OUT/src/altest.cs" "$OUT/src/WavFile.cs" \ + -r:"$OUT/OpenTK.dll" -out:"$OUT/altest.exe" + +chmod +x "$OUT"/*.sh +tar czf audiotest.tar.gz "$OUT" +echo "built audiotest.tar.gz" \ No newline at end of file diff --git a/audiotest/readme.md b/audiotest/readme.md new file mode 100644 index 0000000..923cf84 --- /dev/null +++ b/audiotest/readme.md @@ -0,0 +1,3 @@ +sudo apt install mono-runtime libmono-system-drawing4.0-cil libopenal1 pulseaudio-utils sox +paplay sound/SND200.wav +mono ./altest.exe sound/SND200.wav \ No newline at end of file