Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,14 @@ tecs/
│ ├── platform/ # Window, events, audio and gamepad backends
│ ├── physics/ # Rapier contract and binding
│ ├── input.nupp # Gameplay input over the platform event stream
│ ├── audio.nupp # Clips, voices, groups, limits, the Sound component
│ ├── audio/ # Clips, voices, groups, limits, the Sound component, General MIDI, Yamaha FM, music
│ ├── sequence.nupp # Sequencer, with the tween runtime inside it
│ ├── assets.nupp # Asset orchestration
│ ├── mcp.nupp # Debug server: transport, tools, sandbox
│ └── events.nupp, files.nupp, watch.nupp
├── native/rust/
│ ├── winit-host/ # The application loop, the wgpu renderer, the host ABI
│ ├── tecs-audio/ # cpal output and capture behind a batched contract
│ ├── tecs-audio/ # cpal output and capture behind a batched contract; General MIDI and Yamaha FM voices
│ ├── tecs-gamepad/ # gilrs enumeration and observations
│ ├── physics/ # Rapier storage and stepping
│ └── build-support/ # Native packaging and binary inspection
Expand Down
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ rayon = "1.12"
reqwest = { version = "0.13.4", default-features = false, features = ["deflate", "gzip", "rustls-no-provider", "socks", "stream"] }
resvg = { version = "0.47", default-features = false, features = ["text"] }
regex = "1.12"
rustysynth = "1.3.7"
rmcp = { version = "3.0.0", default-features = false, features = ["client", "server", "transport-io", "transport-streamable-http-client-reqwest", "transport-streamable-http-server"] }
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
serde = { version = "1", features = ["derive"] }
Expand All @@ -70,6 +71,7 @@ urlencoding = "2.1.3"
walkdir = "2.5"
wgpu = "30.0.1"
winit = "0.30.13"
ymfm-oxide = { git = "https://github.com/albanmgicquel/ymfm-oxide", rev = "4cc9e106c7dbf28c848a9a463bf3ef74709a5ed0" }
axum = { version = "0.8", default-features = false, features = ["http1", "tokio"] }

[profile.release]
Expand Down
244 changes: 242 additions & 2 deletions README.md

Large diffs are not rendered by default.

54 changes: 54 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,60 @@ Open Font License 1.1. The tests and retained UI example read it. Packages ship
`share/tecs/assets/fonts` with `JetBrainsMono-OFL.txt` and the versioned
`JetBrainsMono-NOTICE.md`.

## SoundFonts

Tecs ships no SoundFont. `tecs.audio.synth` reads one a game supplies, and a
General MIDI set is game content under its own licence, so a game that ships
one ships that licence beside it. FluidR3 GM, for example, is MIT.

## Linked BSD-licensed code

### ymfm-oxide

`tecs.audio.fm` emulates every chip it offers with ymfm-oxide
(<https://github.com/albanmgicquel/ymfm-oxide>), a pure-Rust port by Alban
Gicquel of ymfm, Aaron Giles's emulator of the Yamaha FM chips
(<https://github.com/aaronsgiles/ymfm>). It is a Cargo dependency pinned to one
commit, compiled into `tecsaudio`, so every package carries it and
`cargo-licenses.txt` records it as BSD-3-Clause. The licence asks for its
notice to travel with a binary, which is this one; Tecs keeps none of its
source. The OPLL family plays ymfm's own copyright-free stand-ins for the
instrument ROMs, made by David Viens and Hubert Lamontagne and carried in the
crate; no Yamaha ROM ships with Tecs.

```text
BSD 3-Clause License

Copyright (c) 2021, Aaron Giles
Copyright (c) 2026, Alban Gicquel
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
```

## Scope of the checks

The generated inventory and source manifest make shipped dependency names,
Expand Down
2 changes: 1 addition & 1 deletion native/rust/build-support/src/nupp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use anyhow::Result;
use std::path::{Path, PathBuf};
use std::process::Command;
pub const OUTPUT: &str = "out/nupp";
const HOST_FEATURES: &str = "base,native-files,native-net";
const HOST_FEATURES: &str = "base,native-files,native-net,native-compression";

pub fn compiler(root: &Path) -> Result<PathBuf> {
if let Some(root) = std::env::var_os("NUPP_COMPILER_ROOT") {
Expand Down
2 changes: 2 additions & 0 deletions native/rust/tecs-audio/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ crate-type = ["cdylib", "rlib"]

[dependencies]
cpal.workspace = true
rustysynth.workspace = true
symphonia.workspace = true
ymfm-oxide.workspace = true
72 changes: 72 additions & 0 deletions native/rust/tecs-audio/examples/synthscale.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//! Plays a C major scale on the default output through the synthesizer.
//!
//! A manual check for a real machine, kept out of the test suite because it
//! opens the output device, which on macOS can block without bound when the
//! audio daemon is unreachable. It takes the SoundFont as its argument or from
//! `TECS_TEST_SOUNDFONT`, and says it skipped when given neither.
//!
//! ```text
//! cargo run -p tecs-audio --example synthscale -- path/to/FluidR3_GM.sf2
//! ```

use std::time::Duration;

use tecsaudio::command::{self, Command};
use tecsaudio::engine::Engine;
use tecsaudio::synth::Settings;

fn main() {
let path = std::env::args()
.nth(1)
.or_else(|| std::env::var("TECS_TEST_SOUNDFONT").ok());
let Some(path) = path else {
eprintln!("skipped: name a SoundFont as the argument or in TECS_TEST_SOUNDFONT");
return;
};

let engine = Engine::open(48_000, 2, 8);
if !engine.available() {
eprintln!("no output device: {}", engine.last_error());
}
let font = match engine.load_sound_font(&path) {
Ok(font) => font,
Err(reason) => {
eprintln!("{reason}");
std::process::exit(1);
}
};
let handle = 1;
let mut port = engine
.open_synth(font, handle, &Settings::default())
.expect("the synthesizer opens");
engine.submit(&[Command {
kind: command::PLAY,
handle,
clip: 0,
flags: command::FLAG_SYNTH,
gain: 1.0,
pitch: 1.0,
start: 0.0,
fade: 0.0,
loop_start: 0.0,
x: 0.0,
y: 0.0,
z: 0.0,
}]);

// Scheduled ahead in one go, a quarter of a second apart, against the
// synthesizer clock rather than against when this thread wakes up.
let start = port.now() + 0.1;
let keys = [60u8, 62, 64, 65, 67, 69, 71, 72];
for (step, key) in keys.iter().enumerate() {
let at = start + step as f64 * 0.25;
port.send(0x90, *key, 100, at);
port.send(0x80, *key, 0, at + 0.22);
}
std::thread::sleep(Duration::from_millis(3000));
eprintln!(
"played {} notes; the synthesizer clock reads {:.3} s",
keys.len(),
port.now()
);
}
4 changes: 4 additions & 0 deletions native/rust/tecs-audio/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ pub const FLAG_STEREO: u32 = 4;
/// Marks a `PLAY` reading its input from the file rather than held samples.
pub const FLAG_STREAM: u32 = 8;

/// Marks a `PLAY` whose input is the synthesizer `tecs_audio_synth_open` or
/// `tecs_audio_fm_open` parked under the command's handle, rather than a clip.
pub const FLAG_SYNTH: u32 = 16;

/// Requests one operation.
///
/// One struct covers every operation, because the array crossing the seam has
Expand Down
Loading
Loading