Created
June 9, 2026 11:18
-
-
Save haruki7049/04777cd2888615a5f0f5b1b6c74c44ad to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| diff --git a/.deps.nix b/.deps.nix | |
| new file mode 100644 | |
| index 0000000..942885e | |
| --- /dev/null | |
| +++ b/.deps.nix | |
| @@ -0,0 +1,38 @@ | |
| +# generated by zon2nix (https://github.com/nix-community/zon2nix) | |
| + | |
| +{ | |
| + linkFarm, | |
| + fetchzip, | |
| + fetchgit, | |
| +}: | |
| + | |
| +linkFarm "zig-packages" [ | |
| + { | |
| + name = "riff_zig-1.0.0-J7H0A7E6TAAFStkejtsMRsIhfgLMLzf5iYxWgeSFBLJu"; | |
| + path = fetchzip { | |
| + url = "https://github.com/haruki7049/RIFF.zig/archive/refs/tags/1.0.0.tar.gz"; | |
| + hash = "sha256-ELlLnVXNOfI44m9EbnKAsaWMjDKEAP1i0VnIdv1Tq14="; | |
| + }; | |
| + } | |
| + { | |
| + name = "system_sdk-0.3.0-dev-alwUNnYaaAJAtIdE2fg4NQfDqEKs7QCXy_qYukAOBfmF"; | |
| + path = fetchzip { | |
| + url = "https://github.com/zig-gamedev/system_sdk/archive/c0dbf11cdc17da5904ea8a17eadc54dee26567ec.tar.gz"; | |
| + hash = "sha256-O3+Z4F58yESes3AMR3yfgqzJjM35BjwxGULgjKgJ6cE="; | |
| + }; | |
| + } | |
| + { | |
| + name = "zaudio-0.11.0-dev-_M-91kHvPwAlW8MCRI4XiTbTeihqB8Zspgiuw-6Gqgdz"; | |
| + path = fetchzip { | |
| + url = "https://github.com/zig-gamedev/zaudio/archive/bb93ad665b89e302d4515a36b44cb8e73eaf6766.tar.gz"; | |
| + hash = "sha256-iMGK3w/SQ6IxD2vW1x0nwKshhdzNLAsbngx0Vo1Svgc="; | |
| + }; | |
| + } | |
| + { | |
| + name = "zigggwavvv-1.0.0-iR5xWiDEAAD6QVEQLTO2j1OfcisaJgUfwo0hLAJnb6hS"; | |
| + path = fetchzip { | |
| + url = "https://github.com/haruki7049/zigggwavvv/archive/refs/tags/1.0.0.tar.gz"; | |
| + hash = "sha256-XBhbXfS7KJlIuchKeTPYoMZrZvaCXuWNX+eWLxkdV0s="; | |
| + }; | |
| + } | |
| +] | |
| diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml | |
| index dd174e0..3b8f7fe 100644 | |
| --- a/.github/workflows/ci.yml | |
| +++ b/.github/workflows/ci.yml | |
| @@ -14,5 +14,5 @@ jobs: | |
| - uses: actions/checkout@v5.0.0 | |
| - uses: mlugg/setup-zig@v2.0.5 | |
| with: | |
| - version: 0.15.1 | |
| + version: 0.15.2 | |
| - run: zig build test | |
| diff --git a/.github/workflows/deploy-api-docs.yml b/.github/workflows/deploy-api-docs.yml | |
| new file mode 100644 | |
| index 0000000..ffe802a | |
| --- /dev/null | |
| +++ b/.github/workflows/deploy-api-docs.yml | |
| @@ -0,0 +1,45 @@ | |
| +on: | |
| + push: | |
| + branches: ["main"] | |
| + workflow_dispatch: | |
| + | |
| +permissions: | |
| + pages: write | |
| + contents: read | |
| + id-token: write | |
| + | |
| +concurrency: | |
| + group: pages | |
| + cancel-in-progress: false | |
| + | |
| +jobs: | |
| + build: | |
| + runs-on: ubuntu-latest | |
| + | |
| + steps: | |
| + - name: Check out repository | |
| + uses: actions/checkout@v6.0.1 | |
| + - name: Setup Ziglang | |
| + uses: mlugg/setup-zig@v2.0.5 | |
| + with: | |
| + version: 0.15.2 | |
| + - name: Generate docs | |
| + run: | | |
| + zig build docs | |
| + - name: Setup Pages | |
| + uses: actions/configure-pages@v5.0.0 | |
| + - name: Upload artifact | |
| + uses: actions/upload-pages-artifact@v4.0.0 | |
| + with: | |
| + path: zig-out/share/lightmix/docs/ | |
| + | |
| + deploy: | |
| + needs: build | |
| + runs-on: ubuntu-latest | |
| + environment: | |
| + name: github-pages | |
| + url: ${{ steps.deployment.outputs.page_url }} | |
| + steps: | |
| + - name: Deploy to GitHub Pages | |
| + id: deployment | |
| + uses: actions/deploy-pages@v4.0.5 | |
| diff --git a/.github/workflows/nix-checker.yml b/.github/workflows/nix-checker.yml | |
| new file mode 100644 | |
| index 0000000..76aa493 | |
| --- /dev/null | |
| +++ b/.github/workflows/nix-checker.yml | |
| @@ -0,0 +1,37 @@ | |
| +name: nix-checker | |
| + | |
| +on: | |
| + push: | |
| + | |
| +permissions: {} | |
| + | |
| +jobs: | |
| + run-nix-check: | |
| + strategy: | |
| + matrix: | |
| + os: [ubuntu-latest, macos-latest] | |
| + runs-on: ${{ matrix.os }} | |
| + steps: | |
| + - name: Checkout | |
| + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 | |
| + - name: Install Nix | |
| + uses: cachix/install-nix-action@v31.6.0 | |
| + with: | |
| + nix_path: nixpkgs=channel:nixos-unstable | |
| + - name: Run nix flake check | |
| + run: nix flake check --all-systems | |
| + | |
| + run-nix-build: | |
| + strategy: | |
| + matrix: | |
| + os: [ubuntu-latest, macos-latest] | |
| + runs-on: ${{ matrix.os }} | |
| + steps: | |
| + - name: Checkout | |
| + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 | |
| + - name: Install Nix | |
| + uses: cachix/install-nix-action@v31.6.0 | |
| + with: | |
| + nix_path: nixpkgs=channel:nixos-unstable | |
| + - name: Run nix-build with flakes | |
| + run: nix build .#default | |
| diff --git a/.github/workflows/pr-conventional-commits-validation.yml b/.github/workflows/pr-conventional-commits-validation.yml | |
| new file mode 100644 | |
| index 0000000..3239625 | |
| --- /dev/null | |
| +++ b/.github/workflows/pr-conventional-commits-validation.yml | |
| @@ -0,0 +1,15 @@ | |
| +name: PR Conventional Commit Validation | |
| + | |
| +on: | |
| + pull_request: | |
| + types: [opened, synchronize, reopened, edited] | |
| + | |
| +jobs: | |
| + validate-pr-title: | |
| + runs-on: ubuntu-latest | |
| + steps: | |
| + - name: PR Conventional Commit Validation | |
| + uses: ytanikin/pr-conventional-commits@1.4.0 | |
| + with: | |
| + task_types: '["feat","fix","docs","test","ci","refactor","perf","chore","revert"]' | |
| + add_scope_label: "true" | |
| diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md | |
| new file mode 100644 | |
| index 0000000..da1beda | |
| --- /dev/null | |
| +++ b/CODE_OF_CONDUCT.md | |
| @@ -0,0 +1,128 @@ | |
| +# Contributor Covenant Code of Conduct | |
| + | |
| +## Our Pledge | |
| + | |
| +We as members, contributors, and leaders pledge to make participation in our | |
| +community a harassment-free experience for everyone, regardless of age, body | |
| +size, visible or invisible disability, ethnicity, sex characteristics, gender | |
| +identity and expression, level of experience, education, socio-economic status, | |
| +nationality, personal appearance, race, religion, or sexual identity | |
| +and orientation. | |
| + | |
| +We pledge to act and interact in ways that contribute to an open, welcoming, | |
| +diverse, inclusive, and healthy community. | |
| + | |
| +## Our Standards | |
| + | |
| +Examples of behavior that contributes to a positive environment for our | |
| +community include: | |
| + | |
| +- Demonstrating empathy and kindness toward other people | |
| +- Being respectful of differing opinions, viewpoints, and experiences | |
| +- Giving and gracefully accepting constructive feedback | |
| +- Accepting responsibility and apologizing to those affected by our mistakes, | |
| + and learning from the experience | |
| +- Focusing on what is best not just for us as individuals, but for the | |
| + overall community | |
| + | |
| +Examples of unacceptable behavior include: | |
| + | |
| +- The use of sexualized language or imagery, and sexual attention or | |
| + advances of any kind | |
| +- Trolling, insulting or derogatory comments, and personal or political attacks | |
| +- Public or private harassment | |
| +- Publishing others' private information, such as a physical or email | |
| + address, without their explicit permission | |
| +- Other conduct which could reasonably be considered inappropriate in a | |
| + professional setting | |
| + | |
| +## Enforcement Responsibilities | |
| + | |
| +Community leaders are responsible for clarifying and enforcing our standards of | |
| +acceptable behavior and will take appropriate and fair corrective action in | |
| +response to any behavior that they deem inappropriate, threatening, offensive, | |
| +or harmful. | |
| + | |
| +Community leaders have the right and responsibility to remove, edit, or reject | |
| +comments, commits, code, wiki edits, issues, and other contributions that are | |
| +not aligned to this Code of Conduct, and will communicate reasons for moderation | |
| +decisions when appropriate. | |
| + | |
| +## Scope | |
| + | |
| +This Code of Conduct applies within all community spaces, and also applies when | |
| +an individual is officially representing the community in public spaces. | |
| +Examples of representing our community include using an official e-mail address, | |
| +posting via an official social media account, or acting as an appointed | |
| +representative at an online or offline event. | |
| + | |
| +## Enforcement | |
| + | |
| +Instances of abusive, harassing, or otherwise unacceptable behavior may be | |
| +reported to the community leaders responsible for enforcement at | |
| +tontonkirikiri@gmail.com. | |
| +All complaints will be reviewed and investigated promptly and fairly. | |
| + | |
| +All community leaders are obligated to respect the privacy and security of the | |
| +reporter of any incident. | |
| + | |
| +## Enforcement Guidelines | |
| + | |
| +Community leaders will follow these Community Impact Guidelines in determining | |
| +the consequences for any action they deem in violation of this Code of Conduct: | |
| + | |
| +### 1. Correction | |
| + | |
| +**Community Impact**: Use of inappropriate language or other behavior deemed | |
| +unprofessional or unwelcome in the community. | |
| + | |
| +**Consequence**: A private, written warning from community leaders, providing | |
| +clarity around the nature of the violation and an explanation of why the | |
| +behavior was inappropriate. A public apology may be requested. | |
| + | |
| +### 2. Warning | |
| + | |
| +**Community Impact**: A violation through a single incident or series | |
| +of actions. | |
| + | |
| +**Consequence**: A warning with consequences for continued behavior. No | |
| +interaction with the people involved, including unsolicited interaction with | |
| +those enforcing the Code of Conduct, for a specified period of time. This | |
| +includes avoiding interactions in community spaces as well as external channels | |
| +like social media. Violating these terms may lead to a temporary or | |
| +permanent ban. | |
| + | |
| +### 3. Temporary Ban | |
| + | |
| +**Community Impact**: A serious violation of community standards, including | |
| +sustained inappropriate behavior. | |
| + | |
| +**Consequence**: A temporary ban from any sort of interaction or public | |
| +communication with the community for a specified period of time. No public or | |
| +private interaction with the people involved, including unsolicited interaction | |
| +with those enforcing the Code of Conduct, is allowed during this period. | |
| +Violating these terms may lead to a permanent ban. | |
| + | |
| +### 4. Permanent Ban | |
| + | |
| +**Community Impact**: Demonstrating a pattern of violation of community | |
| +standards, including sustained inappropriate behavior, harassment of an | |
| +individual, or aggression toward or disparagement of classes of individuals. | |
| + | |
| +**Consequence**: A permanent ban from any sort of public interaction within | |
| +the community. | |
| + | |
| +## Attribution | |
| + | |
| +This Code of Conduct is adapted from the [Contributor Covenant][homepage], | |
| +version 2.0, available at | |
| +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. | |
| + | |
| +Community Impact Guidelines were inspired by [Mozilla's code of conduct | |
| +enforcement ladder](https://github.com/mozilla/diversity). | |
| + | |
| +For answers to common questions about this code of conduct, see the FAQ at | |
| +https://www.contributor-covenant.org/faq. Translations are available at | |
| +https://www.contributor-covenant.org/translations. | |
| + | |
| +[homepage]: https://www.contributor-covenant.org | |
| diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md | |
| new file mode 100644 | |
| index 0000000..e9dc3ee | |
| --- /dev/null | |
| +++ b/CONTRIBUTING.md | |
| @@ -0,0 +1,377 @@ | |
| +# Contributing to lightmix | |
| + | |
| +Thank you for your interest in contributing to lightmix! This document provides guidelines and information for contributors. | |
| + | |
| +## Table of Contents | |
| + | |
| +- [Code of Conduct](#code-of-conduct) | |
| +- [Getting Started](#getting-started) | |
| +- [Development Environment](#development-environment) | |
| +- [How to Contribute](#how-to-contribute) | |
| +- [Coding Guidelines](#coding-guidelines) | |
| +- [Testing](#testing) | |
| +- [Submitting Changes](#submitting-changes) | |
| +- [Documentation](#documentation) | |
| +- [Audio-Specific Guidelines](#audio-specific-guidelines) | |
| + | |
| +## Code of Conduct | |
| + | |
| +See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md). We use [Contributor Covenant](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html). | |
| + | |
| +## Getting Started | |
| + | |
| +1. **Fork the repository** on GitHub | |
| +1. **Clone your fork** locally: | |
| + ```bash | |
| + git clone https://github.com/YOUR_USERNAME/lightmix.git | |
| + cd lightmix | |
| + ``` | |
| +1. **Set up the development environment** (see below) | |
| +1. **Create a new branch** for your work: | |
| + ```bash | |
| + git checkout -b feature/your-feature-name | |
| + ``` | |
| + | |
| +## Development Environment | |
| + | |
| +### Required Tools | |
| + | |
| +- **Zig 0.15.2** - This project tracks Zig's minor version | |
| +- **Git** for version control | |
| + | |
| +### Optional but Recommended | |
| + | |
| +- **Nix** (with flakes enabled) for reproducible development environment | |
| +- **direnv** for automatic environment loading | |
| +- **Audio player** (`sox`, VLC, or similar) for testing audio output | |
| + | |
| +### Setup with Nix | |
| + | |
| +If you have Nix with flakes: | |
| + | |
| +```bash | |
| +# Automatic setup with direnv | |
| +echo "use flake" > .envrc | |
| +direnv allow | |
| + | |
| +# Or manually enter the dev shell | |
| +nix develop | |
| +``` | |
| + | |
| +### Setup without Nix | |
| + | |
| +1. Install Zig 0.15.2 from [ziglang.org](https://ziglang.org/download/) | |
| +1. Verify installation: | |
| + ```bash | |
| + zig version # Should show 0.15.2 | |
| + ``` | |
| + | |
| +### Building and Testing | |
| + | |
| +```bash | |
| +# Run all tests | |
| +zig build test | |
| + | |
| +# Build the library | |
| +zig build | |
| + | |
| +# Generate documentation | |
| +zig build docs | |
| + | |
| +# Run examples | |
| +cd examples/01-getting-started/hello-wave | |
| +zig build run | |
| +``` | |
| + | |
| +## How to Contribute | |
| + | |
| +### Types of Contributions | |
| + | |
| +We welcome: | |
| + | |
| +- **Bug fixes** - Fix issues in the core library or examples | |
| +- **New features** - Add new audio processing capabilities | |
| +- **Examples** - Create educational examples demonstrating library features | |
| +- **Documentation** - Improve docs, comments, or README files | |
| +- **Tests** - Add test coverage for existing functionality | |
| +- **Performance improvements** - Optimize audio processing algorithms | |
| + | |
| +### Finding Something to Work On | |
| + | |
| +- Check the [issue tracker](https://github.com/haruki7049/lightmix/issues) for open issues | |
| +- Look for issues labeled `good first issue` or `help wanted` | |
| +- Review the examples directory for areas that need more coverage | |
| +- Propose new features by opening an issue first | |
| + | |
| +## Coding Guidelines | |
| + | |
| +### General Principles | |
| + | |
| +1. **Clarity over cleverness** - Write code that's easy to understand | |
| +1. **Type safety** - Leverage Zig's type system for compile-time safety | |
| +1. **Memory safety** - Properly manage allocations and prevent leaks | |
| +1. **Documentation** - Document public APIs and complex algorithms | |
| + | |
| +### Code Style | |
| + | |
| +- **Comments**: Write comments in **English** | |
| + - Documentation comments (`///` and `//!`) must be in English | |
| + - Regular inline comments (`//`) should also be in English | |
| +- **Formatting**: Use `zig fmt` before committing | |
| + - Run `zig fmt .` in the project root | |
| +- **Naming**: | |
| + - `camelCase` for functions and variables | |
| + - `PascalCase` for types | |
| + - `SCREAMING_SNAKE_CASE` for constants | |
| +- **Indentation**: 4 spaces (handled by `zig fmt`) | |
| + | |
| +### Documentation Comments | |
| + | |
| +Use documentation comments for all public APIs: | |
| + | |
| +```zig | |
| +/// Creates a new Wave instance from sample data. | |
| +/// | |
| +/// The function creates a deep copy of the sample data, so the caller | |
| +/// retains ownership of the original samples slice. | |
| +/// | |
| +/// ## Parameters | |
| +/// - `samples`: Slice of sample data to copy | |
| +/// - `allocator`: Memory allocator for internal allocations | |
| +/// - `options`: Initialization options (sample rate and channel count) | |
| +/// | |
| +/// ## Returns | |
| +/// A new Wave instance containing a copy of the sample data | |
| +pub fn init( | |
| + samples: []const T, | |
| + allocator: std.mem.Allocator, | |
| + options: InitOptions, | |
| +) Self { | |
| + // Implementation | |
| +} | |
| +``` | |
| + | |
| +### Generic Programming | |
| + | |
| +When creating generic functions: | |
| + | |
| +- Use `comptime` parameters for type flexibility | |
| +- Document type constraints clearly | |
| +- Provide usage examples | |
| + | |
| +````zig | |
| +/// Wave type function: Creates a Wave type for the specified sample type. | |
| +/// | |
| +/// ## Type Parameter | |
| +/// - `T`: The sample data type (typically f64, f80, or f128) | |
| +/// | |
| +/// ## Usage | |
| +/// ```zig | |
| +/// const Wave = lightmix.Wave; | |
| +/// const wave = Wave(f64).init(samples, allocator, .{ | |
| +/// .sample_rate = 44100, | |
| +/// .channels = 1, | |
| +/// }); | |
| +/// defer wave.deinit(); | |
| +/// ``` | |
| +pub fn inner(comptime T: type) type { | |
| + return struct { | |
| + // Implementation | |
| + }; | |
| +} | |
| +```` | |
| + | |
| +### Memory Management | |
| + | |
| +- **Always provide `deinit` functions** for types that allocate memory | |
| +- **Document ownership** clearly in function comments | |
| +- **Use `defer` appropriately** in examples and tests | |
| +- **Test for memory leaks** using `std.testing.allocator` | |
| + | |
| +```zig | |
| +test "no memory leaks" { | |
| + const allocator = std.testing.allocator; | |
| + const wave = Wave(f64).init(samples, allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + defer wave.deinit(); | |
| + // Test code | |
| +} | |
| +``` | |
| + | |
| +## Testing | |
| + | |
| +### Writing Tests | |
| + | |
| +- **Unit tests**: Place in the same file as the code being tested | |
| +- **Integration tests**: Place in `tests/` directory | |
| +- **Example tests**: Each example should run without errors | |
| + | |
| +### Test Organization | |
| + | |
| +```zig | |
| +// In src/wave.zig | |
| +test "init creates deep copy of samples" { | |
| + const allocator = testing.allocator; | |
| + var original_samples = [_]T{ 1.0, 2.0, 3.0 }; | |
| + const wave = Self.init(&original_samples, allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + defer wave.deinit(); | |
| + | |
| + // Modify original samples | |
| + original_samples[0] = 999.0; | |
| + | |
| + // Wave samples should be unchanged | |
| + try testing.expectEqual(wave.samples[0], 1.0); | |
| +} | |
| +``` | |
| + | |
| +### Running Tests | |
| + | |
| +```bash | |
| +# Run all tests | |
| +zig build test | |
| + | |
| +# Run specific test file | |
| +zig test src/wave.zig | |
| + | |
| +# Run with memory leak detection | |
| +zig test src/wave.zig --test-filter "no memory leaks" | |
| +``` | |
| + | |
| +## Submitting Changes | |
| + | |
| +### Before Submitting | |
| + | |
| +1. **Run tests**: `zig build test` | |
| +1. **Format code**: `zig fmt .` | |
| +1. **Update documentation** if needed | |
| +1. **Add tests** for new functionality | |
| +1. **Test examples** if they're affected | |
| + | |
| +### Commit Messages | |
| + | |
| +Write clear, descriptive commit messages: | |
| + | |
| +``` | |
| +Add filter chaining example | |
| + | |
| +- Create new example in examples/03-wave-operations/ | |
| +- Demonstrate using multiple filters in sequence | |
| +- Include documentation about filter composition | |
| +``` | |
| + | |
| +### Pull Request Process | |
| + | |
| +1. **Push your branch** to your fork | |
| +1. **Open a Pull Request** against `main` | |
| +1. **Describe your changes**: | |
| + - What problem does this solve? | |
| + - How did you test it? | |
| + - Any breaking changes? | |
| +1. **Respond to review feedback** | |
| +1. **Keep your PR up to date** with main | |
| + | |
| +### PR Title Format | |
| + | |
| +- `feat: Add new feature` | |
| +- `fix: Fix bug description` | |
| +- `docs: Update documentation` | |
| +- `test: Add tests for X` | |
| +- `refactor: Improve code structure` | |
| +- `perf: Optimize performance of X` | |
| + | |
| +## Documentation | |
| + | |
| +### Types of Documentation | |
| + | |
| +1. **API Documentation** - Document all public functions, types, and constants | |
| +1. **Examples** - Create runnable examples with detailed comments | |
| +1. **README** - Keep README.md up to date with new features | |
| +1. **Build-time Docs** - Run `zig build docs` to generate API docs | |
| + | |
| +### Example Documentation | |
| + | |
| +When creating examples: | |
| + | |
| +- **Add top-level documentation** (`//!`) explaining the concept | |
| +- **Include inline comments** for complex code sections | |
| +- **Provide context** about what the example demonstrates | |
| +- **Show expected output** or results | |
| + | |
| +```zig | |
| +//! # Filter Chaining Example | |
| +//! | |
| +//! This example demonstrates how to chain multiple filters together. | |
| +//! We apply decay and volume reduction to create a fade-out effect. | |
| +//! | |
| +//! ## What you'll learn: | |
| +//! - Chaining filter functions | |
| +//! - Creating composite effects | |
| +//! - Managing filter ownership | |
| + | |
| +const std = @import("std"); | |
| +const lightmix = @import("lightmix"); | |
| + | |
| +pub fn main() !void { | |
| + // Example implementation with comments | |
| +} | |
| +``` | |
| + | |
| +## Audio-Specific Guidelines | |
| + | |
| +### Sample Rates | |
| + | |
| +- **Default to 44100 Hz** in examples (CD quality) | |
| +- **Support common rates**: 22050, 44100, 48000, 96000 Hz | |
| +- **Document rate requirements** clearly | |
| + | |
| +### Audio Formats | |
| + | |
| +- **Prefer PCM formats** for examples | |
| +- **Support standard bit depths**: 16, 24, 32 bits | |
| +- **Document format limitations** | |
| + | |
| +### Testing Audio Output | |
| + | |
| +When contributing audio generation code: | |
| + | |
| +1. **Listen to the output** - Does it sound correct? | |
| +1. **Check for clipping** - Ensure samples stay within [-1.0, 1.0] | |
| +1. **Verify silence** - Silent sections should be exactly 0.0 | |
| +1. **Test edge cases** - Empty waves, single samples, etc. | |
| + | |
| +### Performance Considerations | |
| + | |
| +- **Avoid unnecessary allocations** in tight loops | |
| +- **Use `comptime` for static computations** | |
| +- **Profile performance-critical code** | |
| +- **Document time complexity** for algorithms | |
| + | |
| +### Example Audio Files | |
| + | |
| +When adding test audio files: | |
| + | |
| +- **Keep files small** (< 1 MB preferred) | |
| +- **Use standard formats** (WAV, 16-bit PCM) | |
| +- **Document file contents** in comments | |
| +- **Add to `.gitignore` if generated** | |
| + | |
| +## Questions? | |
| + | |
| +If you have questions: | |
| + | |
| +- **Open an issue** for discussion | |
| +- **Check existing issues** and PRs | |
| +- **Review examples** for patterns and conventions | |
| + | |
| +## License | |
| + | |
| +By contributing to lightmix, you agree that your contributions will be licensed under the [MIT License](./LICENSE). | |
| + | |
| +______________________________________________________________________ | |
| + | |
| +Thank you for contributing to lightmix! 🎵 | |
| diff --git a/README.md b/README.md | |
| index f92397e..d3f34e4 100644 | |
| --- a/README.md | |
| +++ b/README.md | |
| @@ -2,6 +2,17 @@ | |
| `lightmix` is an audio processing library written by Zig-lang. | |
| +## Why I create this | |
| + | |
| +I created this project because I felt a disconnect between existing audio synthesis environments and the standard software development workflow I use every day. | |
| + | |
| +- **From "Recording" to "Building"**: | |
| + In many existing tools, exporting audio feels like a manual task. I often had to click a record button or write specific code to manage recording buffers, essentially capturing the output in real-time. I wanted a workflow where audio is treated as a build artifact—where running `zig build run` (or `zig build`) instantly produces a WAV file, just as it would a binary executable. | |
| +- **Integration with the Modern Toolchain**: | |
| + I found it cumbersome to set up dedicated runtimes or specialized IDEs just to generate sound. I wanted to use my preferred editor and the standard Zig toolchain without any external dependencies or complex server setups. | |
| + | |
| +**lightmix** is my attempt to bridge these two worlds. It allows me to "build" sound with the same precision, automation, and simplicity that I expect from any other software project. | |
| + | |
| ## How to use | |
| In `build.zig`, import lightmix from `build.zig.zon` using `b.dependency()`: | |
| @@ -17,65 +28,175 @@ const lib_mod = b.createModule(.{ | |
| lib_mod.addImport("lightmix", lightmix.module("lightmix")); // Add lightmix to your library or executable module. | |
| ``` | |
| -You can find some examples in [./examples](./examples) directory. If you want to copy a example, edit `.lightmix = .{ .path = "../../.." }` on it's `build.zig.zon`. | |
| +You can find some examples in [./examples](./examples) directory. If you want to copy an example, edit `.lightmix = .{ .path = "../../.." }` in its `build.zig.zon`. | |
| + | |
| +## Build-time Wave file generation | |
| + | |
| +lightmix provides a helper function `addWave` in `build.zig` that allows you to generate and install Wave files during the build process. | |
| + | |
| +### `addWave` function in `build.zig` | |
| + | |
| +The `addWave` function is a build-time helper that allows you to generate Wave files as part of your build process. This means you can write a Zig function that generates audio, and the build system will automatically create the WAV file when you run `zig build`. | |
| + | |
| +#### How to use `addWave` | |
| + | |
| +1. First, create a module with a function that generates a Wave: | |
| + | |
| +```zig | |
| +// In your src/root.zig or similar file | |
| +const std = @import("std"); | |
| +const lightmix = @import("lightmix"); | |
| + | |
| +pub fn generate(allocator: std.mem.Allocator) !lightmix.Wave(f64) { | |
| + const allocator = std.heap.page_allocator; | |
| + | |
| + // Generate your audio data (example: 1 second of silence) | |
| + const data: [44100]f64 = [_]f64{0.0} ** 44100; | |
| + | |
| + // Wave.init() creates a deep copy of the data | |
| + // The original data array can be safely discarded after this call | |
| + const wave: lightmix.Wave(f64) = try lightmix.Wave(f64).init(data[0..], allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + | |
| + return wave; | |
| +} | |
| +``` | |
| + | |
| +2. In your `build.zig`, use the `addWave` function: | |
| + | |
| +```zig | |
| +const std = @import("std"); | |
| +const l = @import("lightmix"); | |
| + | |
| +pub fn build(b: *std.Build) !void { | |
| + const target = b.standardTargetOptions(.{}); | |
| + const optimize = b.standardOptimizeOption(.{}); | |
| + | |
| + const lightmix = b.dependency("lightmix", .{}); | |
| + | |
| + // Create your module that contains the wave generation function | |
| + const mod = b.createModule(.{ | |
| + .root_source_file = b.path("src/root.zig"), | |
| + .target = target, | |
| + .optimize = optimize, | |
| + .imports = &.{ | |
| + .{ .name = "lightmix", .module = lightmix.module("lightmix") }, | |
| + }, | |
| + }); | |
| + | |
| + // Use addWave to generate the wave file during build | |
| + const wave = try l.addWave(b, mod, .{ | |
| + .func_name = "generate", // Name of your wave generation function (optional, defaults to "gen") | |
| + .format = .{ .wav = .{ | |
| + .name = "result.wav", // Output filename (optional, defaults to "result.wav") | |
| + .format_code = .pcm, // Wave format code (e.g., .pcm, .ieee_float) | |
| + .bits = 16, // The bits depth for this wave (e.g., 8, 16, 24, 32) | |
| + } }, | |
| + .path = .{ .custom = "share" }, // Install directory (optional, defaults to "share") | |
| + }); | |
| + | |
| + // Add to the install step so it runs during `zig build` | |
| + // You can use installWave helper or access wave.step directly | |
| + l.installWave(b, wave); | |
| + // Or: b.getInstallStep().dependOn(wave.step); | |
| +} | |
| +``` | |
| + | |
| +3. Run `zig build` to generate the wave file. The file will be created in `zig-out/share/result.wav` (or your configured path). | |
| + | |
| +#### `addWave` Options | |
| + | |
| +- **`func_name`**: The name of the function in your module that generates the Wave. The function must have the signature `pub fn name() !lightmix.Wave(T)` where T is your chosen sample type (e.g., f64) (default: `"gen"`) | |
| +- **`path`**: The installation directory relative to the install prefix (default: `.{ .custom = "share" }`) | |
| +- **`wave.name`**: The output filename for the wave file (default: `"result.wav"`) | |
| +- **`wave.bits`**: The bit depth for the wave file, which is typed u16 | |
| +- **`wave.format_code`**: Audio encoding format (e.g., .pcm, .ieee_float) | |
| + | |
| +You can find a complete example in [./examples/06-advanced/build-time-generation](./examples/06-advanced/build-time-generation). | |
| ## lightmix's types | |
| ### `Wave` | |
| -Contains a PCM audio source. | |
| +`Wave` is a generic type function that accepts a sample type parameter. It contains PCM audio source with samples of the specified floating-point type. | |
| + | |
| +Both waves must have identical `sample_rate`, `channels`, and sample length, or the program will panic. | |
| + | |
| +Supported sample types: `f64`, `f80`, `f128`. | |
| ```zig | |
| const allocator = std.heap.page_allocator; // Use your allocator | |
| -const data: []const f32 = &[_]{ 0.0, 0.0, 0.0 }; // This array contains 3 float number, then this wave will make from 3 samples. | |
| +const data: []const f64 = &[_]f64{ 0.0, 0.0, 0.0 }; // This array contains 3 float numbers, then this wave will be made from 3 samples. | |
| -const wave: lightmix.Wave = Wave.init(data, allocator, .{ | |
| +const wave: lightmix.Wave(f64) = try lightmix.Wave(f64).init(data, allocator, .{ | |
| .sample_rate = 44100, // Samples per second. | |
| - .channels = 1, // Channels for this Wave. If this wave have two channels, it means this wave is stereo. | |
| - .bits = 16, // Bits for this wave. | |
| + .channels = 1, // Channels for this Wave. If this wave has two channels, it means this wave is stereo. | |
| }); | |
| -defer wave.deinit(); // Wave.data is owned data by passed allocator, then you must `free` this wave. | |
| +defer wave.deinit(); // Wave samples are owned by the passed allocator, so you must free this wave. | |
| ``` | |
| -You can write your `Wave` to your wave file, such as `result.wav`. | |
| +You can write your `Wave` to a wave file, such as `result.wav`. | |
| ```zig | |
| -// First, create your `Wave`. | |
| -const wave: lightmix.Wave = generate_wave(); | |
| +// First, create your Wave with a specific sample type | |
| +const wave = generate_wave(); // Returns a Wave(f64) | |
| // Second, you must create a file, typed as `std.fs.File`. | |
| -var file = try std.fs.cwd().createFile("result.wav", .{}); | |
| +const file = try std.fs.cwd().createFile("result.wav", .{}); | |
| defer file.close(); | |
| +// Wav file size calculation | |
| +const bits = 16; | |
| +const bytes_per_sample = (bits + 7) / 8; | |
| +const header_size = 44; | |
| +const total_size = header_size + (wave.samples.len * wave.channels * bytes_per_sample); | |
| + | |
| +// Create a buffer for file writer | |
| +const buf = try allocator.alloc(u8, total_size); | |
| +defer allocator.free(buf); | |
| + | |
| +// Create a std.fs.File.Writer variable from the file. | |
| +// It has an `interface` variable typed `std.Io.Writer`. | |
| +var writer = file.writer(buf); | |
| + | |
| // Then, write down your wave!! | |
| -try wave.write(file); | |
| +try wave.write(.wav, &writer.interface, .{ | |
| + .allocator = allocator, | |
| + .bits = bits, // Bit depth for the output file | |
| + .format_code = .pcm, // Format code (e.g., .pcm or .ieee_float) | |
| +}); | |
| ``` | |
| ### `Composer` | |
| -Contains a `Composer.WaveInfo` array, which contains a `Wave` and the timing when it plays. | |
| +`Composer` is a generic type function that accepts a sample type parameter (same as Wave). It contains a `Composer(T).WaveInfo` array, which contains a `Wave(T)` and the timing when it plays. | |
| ```zig | |
| const allocator = std.heap.page_allocator; // Use your allocator | |
| -const wave: lightmix.Wave = generate_wave(); | |
| +const wave = generate_wave(); // Returns a Wave(f64) | |
| -const info: []const lightmix.Composer.WaveInfo = &[_]WaveInfo{ | |
| +const info: []const lightmix.Composer(f64).WaveInfo = &.{ | |
| .{ .wave = wave, .start_point = 0 }, | |
| .{ .wave = wave, .start_point = 44100 }, | |
| }; | |
| -const composer: lightmix.Composer = Composer.init_with(info, allocator, . | |
| +const composer: lightmix.Composer(f64) = try lightmix.Composer(f64).init_with(info, allocator, .{ | |
| .sample_rate = 44100, // Samples per second. | |
| - .channels = 1, // Channels for the Wave. If this composer have two channels, it means the wave is stereo. | |
| - .bits = 16, // Bits for the wave. | |
| + .channels = 1, // Channels for the Wave. If this composer has two channels, it means the wave is stereo. | |
| }); | |
| -defer composer.deinit(); // Composer.info is also owned data by passed allocator, then you must `free` this wave. | |
| +defer composer.deinit(); // Composer.info is also owned by the passed allocator, so you must free this composer. | |
| -const wave: lightmix.Wave = composer.finalize(); // Let's finalize to create a `Wave`!! | |
| -defer wave.deinit(); // Don't forget to free the `Wave` data. | |
| +const result: lightmix.Wave(f64) = composer.finalize(.{}); // Let's finalize to create a Wave(f64)!! | |
| +defer result.deinit(); // Don't forget to free the Wave data. | |
| ``` | |
| ## Zig version | |
| -0.15.1 | |
| +0.15.2 | |
| This project will follows Ziglang's minor version. | |
| + | |
| +## API Documentations | |
| + | |
| +https://haruki7049.github.io/lightmix | |
| diff --git a/build.zig b/build.zig | |
| index 011d4b1..0dfb07b 100644 | |
| --- a/build.zig | |
| +++ b/build.zig | |
| @@ -1,28 +1,49 @@ | |
| const std = @import("std"); | |
| +const z_wav = @import("zigggwavvv"); | |
| -pub fn build(b: *std.Build) void { | |
| +pub const Wave = @import("./src/wave.zig"); | |
| +pub const Composer = @import("./src/composer.zig"); | |
| + | |
| +pub fn build(b: *std.Build) !void { | |
| const target = b.standardTargetOptions(.{}); | |
| const optimize = b.standardOptimizeOption(.{}); | |
| - const with_debug_features = b.option(bool, "with_debug_features", "Enable debug features implemented by PortAudio") orelse false; | |
| - | |
| - const options = b.addOptions(); | |
| - options.addOption(bool, "with_debug_features", with_debug_features); | |
| - | |
| // Dependencies | |
| - const lightmix_wav = b.dependency("lightmix_wav", .{}); | |
| - const known_folders = b.dependency("known_folders", .{}); | |
| + const zigggwavvv = b.dependency("zigggwavvv", .{}); | |
| + const zaudio = b.dependency("zaudio", .{}); | |
| // Library module declaration | |
| const lib_mod = b.addModule("lightmix", .{ | |
| .root_source_file = b.path("src/root.zig"), | |
| .target = target, | |
| .optimize = optimize, | |
| + .imports = &.{ | |
| + .{ .name = "zigggwavvv", .module = zigggwavvv.module("zigggwavvv") }, | |
| + .{ .name = "zaudio", .module = zaudio.module("root") }, | |
| + }, | |
| }); | |
| - lib_mod.addImport("lightmix_wav", lightmix_wav.module("lightmix_wav")); | |
| - lib_mod.addImport("known-folders", known_folders.module("known-folders")); | |
| - lib_mod.addOptions("build_options", options); | |
| - lib_mod.addOptions("with_debug_features", options); | |
| + | |
| + // miniaudio linking | |
| + lib_mod.linkLibrary(zaudio.artifact("miniaudio")); | |
| + | |
| + // # macOS | |
| + // apple-sdk framework linking is needed if your machine runs macOS. | |
| + // This needs SDKROOT environment variable. | |
| + // Your SDKROOT should be a string as "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk" (If you don't set SDKROOT, lightmix uses "xcrun --show-sdk-path" command to get SDKROOT). | |
| + // You can use pkgs.apple-sdk on nixpkgs with "pkgs.mkShell". You should have SDKROOT environment variable by pkgs.apple-sdk's hook when you use "pkgs.mkShell". | |
| + // | |
| + // I must write below programs, because "miniaudio" linking needs macOS SDK on macOS. | |
| + if (target.result.os.tag == .macos) { | |
| + const sdkroot_envvar: []const u8 = b.graph.env_map.get("SDKROOT") orelse inner: { | |
| + // These processes need "xcrun" command | |
| + const argv = &.{ "xcrun", "--show-sdk-path" }; | |
| + const result = b.run(argv); // The stdout of "xcrun --show-sdk-path" | |
| + | |
| + break :inner result; | |
| + }; | |
| + const sdkroot: []const u8 = try std.mem.concat(b.allocator, u8, &.{ sdkroot_envvar, "/System/Library/Frameworks" }); | |
| + lib_mod.addFrameworkPath(.{ .cwd_relative = sdkroot }); | |
| + } | |
| // Library installation | |
| const lib = b.addLibrary(.{ | |
| @@ -30,10 +51,6 @@ pub fn build(b: *std.Build) void { | |
| .name = "lightmix", | |
| .root_module = lib_mod, | |
| }); | |
| - | |
| - if (with_debug_features) | |
| - lib.linkLibC(); | |
| - | |
| b.installArtifact(lib); | |
| // Library unit tests | |
| @@ -46,6 +63,35 @@ pub fn build(b: *std.Build) void { | |
| const test_step = b.step("test", "Run unit tests"); | |
| test_step.dependOn(&run_lib_unit_tests.step); | |
| + // Integration tests | |
| + // Wave | |
| + const wave_integration_test = b.addTest(.{ | |
| + .root_module = b.createModule(.{ | |
| + .root_source_file = b.path("tests/wave.zig"), | |
| + .target = target, | |
| + .optimize = optimize, | |
| + .imports = &.{ | |
| + .{ .name = "lightmix", .module = lib_mod }, | |
| + }, | |
| + }), | |
| + }); | |
| + const run_wave_integration_tests = b.addRunArtifact(wave_integration_test); | |
| + test_step.dependOn(&run_wave_integration_tests.step); | |
| + | |
| + // Composer | |
| + const composer_integration_test = b.addTest(.{ | |
| + .root_module = b.createModule(.{ | |
| + .root_source_file = b.path("tests/composer.zig"), | |
| + .target = target, | |
| + .optimize = optimize, | |
| + .imports = &.{ | |
| + .{ .name = "lightmix", .module = lib_mod }, | |
| + }, | |
| + }), | |
| + }); | |
| + const run_composer_integration_tests = b.addRunArtifact(composer_integration_test); | |
| + test_step.dependOn(&run_composer_integration_tests.step); | |
| + | |
| // Docs | |
| const docs_step = b.step("docs", "Emit docs"); | |
| const docs_install = b.addInstallDirectory(.{ | |
| @@ -55,3 +101,348 @@ pub fn build(b: *std.Build) void { | |
| }); | |
| docs_step.dependOn(&docs_install.step); | |
| } | |
| + | |
| +/// Creates a build step that generates a WAV file at compile time. | |
| +/// | |
| +/// This function enables compile-time audio generation by calling a user-defined | |
| +/// function that returns a Wave instance, then writing it to a WAV file during | |
| +/// the build process. | |
| +/// | |
| +/// ## Parameters | |
| +/// - `b`: The build context | |
| +/// - `mod`: The module containing the wave generation function | |
| +/// - `options`: Configuration options for wave generation | |
| +/// | |
| +/// ## Returns | |
| +/// A pointer to a value typed CompileWave | |
| +/// | |
| +/// ## Errors | |
| +/// Returns errors from: | |
| +/// - File system operations (creating cache directory, writing files) | |
| +/// - Memory allocation failures | |
| +/// - The user-provided wave generation function (if it returns an error) | |
| +/// | |
| +/// ## Usage | |
| +/// ```zig | |
| +/// const std = @import("std"); | |
| +/// const l = @import("lightmix"); | |
| +/// | |
| +/// pub fn build(b: *std.Build) !void { | |
| +/// const target = b.standardTargetOptions(.{}); | |
| +/// const optimize = b.standardOptimizeOption(.{}); | |
| +/// | |
| +/// // Dependencies | |
| +/// const lightmix = b.dependency("lightmix", .{}); | |
| +/// | |
| +/// // Module | |
| +/// const mod = b.createModule(.{ | |
| +/// .root_source_file = b.path("src/main.zig"), | |
| +/// .target = target, | |
| +/// .optimize = optimize, | |
| +/// .imports = &.{ | |
| +/// .{ .name = "lightmix", .module = lightmix.module("lightmix") }, | |
| +/// }, | |
| +/// }); | |
| +/// | |
| +/// // Install Wave file into `zig-out` as `result.wav` (default Wave name) | |
| +/// const wave = try l.addWave(b, mod, .{ | |
| +/// .func_name = "gen", | |
| +/// .format = .{ .wav = .{ .bits = 16, .format_code = .pcm } }, | |
| +/// }); | |
| +/// b.getInstallStep().dependOn(wave.step); | |
| +/// } | |
| +/// ``` | |
| +/// | |
| +/// The user module must export a function matching the signature specified in | |
| +/// `options.func_name` (default: "gen") that returns `!lightmix.Wave(T)`, and receives an argument `std.mem.Allocator`. | |
| +pub fn addWave( | |
| + b: *std.Build, | |
| + mod: *std.Build.Module, | |
| + options: CreateWaveOptions, | |
| +) anyerror!*CompileWave { | |
| + return switch (options.format) { | |
| + .wav => Generator.Wav.gen(b, mod, options), | |
| + }; | |
| +} | |
| + | |
| +const Generator = struct { | |
| + const Wav = struct { | |
| + fn gen( | |
| + b: *std.Build, | |
| + mod: *std.Build.Module, | |
| + options: CreateWaveOptions, | |
| + ) anyerror!*CompileWave { | |
| + // Create .zig-cache/lightmix directory | |
| + b.cache_root.handle.access("lightmix", .{}) catch { | |
| + try b.cache_root.handle.makeDir("lightmix"); | |
| + }; | |
| + | |
| + // Create a wave file in .zig-cache/lightmix | |
| + const tmp_path: []const u8 = try std.fs.path.join(b.allocator, &[_][]const u8{ | |
| + try b.build_root.handle.realpathAlloc(b.allocator, "."), | |
| + ".zig-cache", | |
| + "lightmix", | |
| + options.format.wav.name, | |
| + }); | |
| + const tmp_path_in_zig: []const u8 = try std.mem.replaceOwned( | |
| + u8, | |
| + b.allocator, | |
| + tmp_path, | |
| + "\\", | |
| + "/", | |
| + ); | |
| + // Generate temporary Zig code that calls the user's function | |
| + const gen_source = try std.fmt.allocPrint(b.allocator, | |
| + \\const std = @import("std"); | |
| + \\const user_module = @import("user_module"); | |
| + \\ | |
| + \\var gpa = std.heap.GeneralPurposeAllocator(.{{}}){{}}; | |
| + \\const allocator = gpa.allocator(); | |
| + \\ | |
| + \\pub fn main() !void {{ | |
| + \\ defer {{ | |
| + \\ const leaked = gpa.deinit(); | |
| + \\ if (leaked == .leak) | |
| + \\ @panic("Memory leak happened"); | |
| + \\ }} | |
| + \\ | |
| + \\ const wave = try user_module.{s}(allocator); | |
| + \\ defer wave.deinit(); | |
| + \\ | |
| + \\ const bits = {d}; | |
| + \\ const bytes_per_sample = (bits + 7) / 8; | |
| + \\ | |
| + \\ const header_size = 44; | |
| + \\ const total_size = header_size + (wave.samples.len * wave.channels * bytes_per_sample); | |
| + \\ | |
| + \\ const file = try std.fs.cwd().createFile("{s}", .{{}}); | |
| + \\ defer file.close(); | |
| + \\ const buf = try allocator.alloc(u8, total_size); | |
| + \\ defer allocator.free(buf); | |
| + \\ var writer = file.writer(buf); | |
| + \\ | |
| + \\ try wave.write(.wav, &writer.interface, .{{ | |
| + \\ .format_code = .{s}, | |
| + \\ .bits = bits, | |
| + \\ }}); | |
| + \\ | |
| + \\ try writer.interface.flush(); | |
| + \\}} | |
| + , .{ | |
| + options.func_name, | |
| + options.format.wav.bits, | |
| + tmp_path_in_zig, | |
| + @tagName(options.format.wav.format_code), | |
| + }); | |
| + | |
| + // Create a write files step to generate the temporary source | |
| + const write_files = b.addWriteFiles(); | |
| + const gen_file = write_files.add("wave_gen.zig", gen_source); | |
| + | |
| + // Create executable that generates the wave | |
| + const gen_exe = b.addExecutable(.{ | |
| + .name = "wave_generator", | |
| + .root_module = b.createModule(.{ | |
| + .root_source_file = gen_file, | |
| + .target = b.graph.host, | |
| + .optimize = .Debug, | |
| + .imports = &.{ | |
| + .{ .name = "user_module", .module = mod }, | |
| + }, | |
| + }), | |
| + }); | |
| + | |
| + // Run the generator during build | |
| + const run_gen = b.addRunArtifact(gen_exe); | |
| + | |
| + const src_path: []const u8 = try std.fs.path.join(b.allocator, &[_][]const u8{ ".zig-cache", "lightmix", options.format.wav.name }); | |
| + // Install the generated wave file | |
| + const install_wave = b.addInstallFileWithDir( | |
| + b.path(src_path), | |
| + options.path, | |
| + options.format.wav.name, | |
| + ); | |
| + install_wave.step.dependOn(&run_gen.step); | |
| + | |
| + const result = try b.allocator.create(CompileWave); | |
| + result.* = CompileWave{ | |
| + .step = &install_wave.step, | |
| + .root_module = mod, | |
| + .name = options.format.wav.name, | |
| + .create_wave_options = options, | |
| + }; | |
| + return result; | |
| + } | |
| + }; | |
| +}; | |
| + | |
| +/// A return type for addWave function. | |
| +pub const CompileWave = struct { | |
| + step: *std.Build.Step, | |
| + root_module: *std.Build.Module, | |
| + name: []const u8, | |
| + create_wave_options: CreateWaveOptions, | |
| +}; | |
| + | |
| +/// Options for configuring compile-time wave generation. | |
| +/// | |
| +/// These options control how the wave generation function is called and | |
| +/// where the resulting WAV file is installed. | |
| +pub const CreateWaveOptions = struct { | |
| + /// Name of the function in the module that generates the Wave. | |
| + /// The function must have signature: `pub fn name() !lightmix.Wave(T)` | |
| + /// where T is typically f64, f80, or f128. | |
| + func_name: []const u8 = "gen", | |
| + | |
| + /// Destination path relative to the install prefix where the WAV file will be installed. | |
| + /// Defaults to the "share" directory. | |
| + path: std.Build.InstallDir = .{ .custom = "share" }, | |
| + | |
| + /// Output format and codec-specific options for the wave file to generate. | |
| + format: FormatOptions, | |
| +}; | |
| + | |
| +/// Tagged union that selects the output format and carries its format-specific options. | |
| +pub const FormatOptions = union(enum) { | |
| + wav: WavOptions, | |
| +}; | |
| + | |
| +/// Options for configuring a WAV file's output properties. | |
| +/// | |
| +/// This struct specifies the output filename, bit depth, and audio format | |
| +/// for the generated WAV file. | |
| +pub const WavOptions = struct { | |
| + /// The output filename for the wave file (e.g., "result.wav", "audio.wav"). | |
| + name: []const u8 = "result.wav", | |
| + | |
| + /// The bit depth for the wave file (e.g., 16, 24, or 32 bits per sample). | |
| + bits: u16, | |
| + | |
| + /// Audio encoding format such as .pcm (PCM integer) or .ieee_float (floating-point). | |
| + format_code: z_wav.FormatCode, | |
| +}; | |
| + | |
| +/// A helper function to install Wave file from a pointer of a value typed CompileWave. | |
| +/// | |
| +/// This function does as the following: | |
| +/// | |
| +/// ``` | |
| +/// b.getInstallStep().dependOn(wave.step); | |
| +/// ``` | |
| +/// | |
| +/// ## Usage | |
| +/// ``` | |
| +/// // Install Wave file into `zig-out` as `result.wav` (default Wave name) | |
| +/// const wave = try l.addWave(b, mod, .{ | |
| +/// .func_name = "gen", | |
| +/// .format = .{ .wav = .{ .bits = 16, .format_code = .pcm } }, | |
| +/// }); | |
| +/// l.installWave(b, wave); | |
| +/// ``` | |
| +pub fn installWave(b: *std.Build, wave: *CompileWave) void { | |
| + b.getInstallStep().dependOn(wave.step); | |
| +} | |
| + | |
| +/// Create a run step that plays the wave generated by CompileWave. | |
| +/// | |
| +/// This function creates a build step that: | |
| +/// 1. Generates an executable that calls the wave generation function | |
| +/// 2. Calls the play() method on the resulting Wave | |
| +/// 3. Returns a Run step that can be added as a dependency | |
| +/// | |
| +/// ## Parameters | |
| +/// - `b`: Build context | |
| +/// - `wave`: Pointer to CompileWave containing the wave generation function info | |
| +/// - `options`: Options for creating the play executable | |
| +/// | |
| +/// ## Returns | |
| +/// A Run step that executes the play functionality | |
| +/// | |
| +/// ## Usage | |
| +/// ```zig | |
| +/// const wave = try l.addWave(b, mod, .{ | |
| +/// .format = .{ .wav = .{ .bits = 16, .format_code = .pcm } }, | |
| +/// }); | |
| +/// const play = try l.addPlay(b, wave, .{}); | |
| +/// l.installPlay(b, play); | |
| +/// ``` | |
| +pub fn addPlay( | |
| + b: *std.Build, | |
| + wave: *CompileWave, | |
| + options: PlayOptions, | |
| +) anyerror!*std.Build.Step.Run { | |
| + // Generate temporary Zig code that calls the user's function and plays it | |
| + const play_source = try std.fmt.allocPrint(b.allocator, | |
| + \\const std = @import("std"); | |
| + \\const user_module = @import("user_module"); | |
| + \\ | |
| + \\var gpa = std.heap.GeneralPurposeAllocator(.{{}}){{}}; | |
| + \\const allocator = gpa.allocator(); | |
| + \\ | |
| + \\pub fn main() !void {{ | |
| + \\ defer {{ | |
| + \\ const leaked = gpa.deinit(); | |
| + \\ if (leaked == .leak) | |
| + \\ @panic("Memory leak happened"); | |
| + \\ }} | |
| + \\ | |
| + \\ const wave = try user_module.{s}(allocator); | |
| + \\ defer wave.deinit(); | |
| + \\ try wave.play(); | |
| + \\}} | |
| + \\ | |
| + , .{wave.create_wave_options.func_name}); | |
| + | |
| + // Create a write files step to generate the temporary source | |
| + const write_files = b.addWriteFiles(); | |
| + const play_source_file = write_files.add("play_wave.zig", play_source); | |
| + | |
| + // Create executable that plays the wave | |
| + const play_exe = b.addExecutable(.{ | |
| + .name = options.exe_name, | |
| + .root_module = b.createModule(.{ | |
| + .root_source_file = play_source_file, | |
| + .target = b.graph.host, | |
| + .optimize = options.optimize, | |
| + .imports = &.{ | |
| + .{ .name = "user_module", .module = wave.root_module }, | |
| + }, | |
| + }), | |
| + }); | |
| + | |
| + // Create and return a run step | |
| + const run_play = b.addRunArtifact(play_exe); | |
| + | |
| + return run_play; | |
| +} | |
| + | |
| +/// A helper function to play Wave at build time from a Run step. | |
| +/// | |
| +/// This function does as the following: | |
| +/// | |
| +/// ``` | |
| +/// b.getInstallStep().dependOn(&play.step); | |
| +/// ``` | |
| +/// | |
| +/// ## Usage | |
| +/// ```zig | |
| +/// // Play Wave at build time | |
| +/// const wave = try l.addWave(b, mod, .{ | |
| +/// .func_name = "gen", | |
| +/// .format = .{ .wav = .{ .bits = 16, .format_code = .pcm } }, | |
| +/// }); | |
| +/// const play = try l.addPlay(b, wave, .{}); | |
| +/// l.installPlay(b, play); | |
| +/// ``` | |
| +pub fn installPlay(b: *std.Build, play: *std.Build.Step.Run) void { | |
| + b.getInstallStep().dependOn(&play.step); | |
| +} | |
| + | |
| +/// Options for creating a play executable | |
| +pub const PlayOptions = struct { | |
| + /// Name of the generated executable | |
| + exe_name: []const u8 = "play_wave", | |
| + | |
| + /// Optimization mode for the executable | |
| + optimize: std.builtin.OptimizeMode = .Debug, | |
| +}; | |
| diff --git a/build.zig.zon b/build.zig.zon | |
| index 8664401..4edcf9f 100644 | |
| --- a/build.zig.zon | |
| +++ b/build.zig.zon | |
| @@ -1,17 +1,17 @@ | |
| .{ | |
| .name = .lightmix, | |
| - .version = "0.5.0", | |
| + .version = "0.20.4", | |
| .fingerprint = 0xe48269dbca8904a, | |
| - .minimum_zig_version = "0.15.1", | |
| + .minimum_zig_version = "0.15.2", | |
| .dependencies = .{ | |
| - .lightmix_wav = .{ | |
| - .url = "https://github.com/haruki7049/lightmix_wav/archive/refs/tags/0.2.2.tar.gz", | |
| - .hash = "lightmix_wav-0.1.0-IIrulkoMVQDAWgmrgqWgHvtyMFKpinx5YmrvxjA0QUqu", | |
| + .zigggwavvv = .{ | |
| + .url = "https://github.com/haruki7049/zigggwavvv/archive/refs/tags/1.0.0.tar.gz", | |
| + .hash = "zigggwavvv-1.0.0-iR5xWiDEAAD6QVEQLTO2j1OfcisaJgUfwo0hLAJnb6hS", | |
| }, | |
| - .known_folders = .{ | |
| - .url = "https://github.com/ziglibs/known-folders/archive/ab5cf5feb936fa3b72c95d3ad0c0c67791937ba1.tar.gz", | |
| - .hash = "known_folders-0.0.0-Fy-PJtTTAADUOhGKM0sxzG4eMkNQxRvx9e5dfHVyaeA3", | |
| + .zaudio = .{ | |
| + .url = "https://github.com/zig-gamedev/zaudio/archive/bb93ad665b89e302d4515a36b44cb8e73eaf6766.tar.gz", | |
| + .hash = "zaudio-0.11.0-dev-_M-91kHvPwAlW8MCRI4XiTbTeihqB8Zspgiuw-6Gqgdz", | |
| }, | |
| }, | |
| diff --git a/examples/.gitignore b/examples/.gitignore | |
| new file mode 100644 | |
| index 0000000..e69de29 | |
| diff --git a/examples/Composer/compose_multipul_sinewave/.gitignore b/examples/01-getting-started/.gitignore | |
| similarity index 100% | |
| rename from examples/Composer/compose_multipul_sinewave/.gitignore | |
| rename to examples/01-getting-started/.gitignore | |
| diff --git a/examples/Composer/compose_multipul_sinewave/build.zig b/examples/01-getting-started/hello-wave/build.zig | |
| similarity index 87% | |
| rename from examples/Composer/compose_multipul_sinewave/build.zig | |
| rename to examples/01-getting-started/hello-wave/build.zig | |
| index 735a448..81ce6fe 100644 | |
| --- a/examples/Composer/compose_multipul_sinewave/build.zig | |
| +++ b/examples/01-getting-started/hello-wave/build.zig | |
| @@ -12,17 +12,19 @@ pub fn build(b: *std.Build) void { | |
| .root_source_file = b.path("src/main.zig"), | |
| .target = target, | |
| .optimize = optimize, | |
| + .imports = &.{ | |
| + .{ .name = "lightmix", .module = lightmix.module("lightmix") }, | |
| + }, | |
| }); | |
| - exe_mod.addImport("lightmix", lightmix.module("lightmix")); | |
| // Executable | |
| const exe = b.addExecutable(.{ | |
| - .name = "compose_multipul_soundless", | |
| + .name = "hello-wave", | |
| .root_module = exe_mod, | |
| }); | |
| b.installArtifact(exe); | |
| - // Rum cmd | |
| + // Run cmd | |
| const run_cmd = b.addRunArtifact(exe); | |
| run_cmd.step.dependOn(b.getInstallStep()); | |
| if (b.args) |args| { | |
| diff --git a/examples/01-getting-started/hello-wave/build.zig.zon b/examples/01-getting-started/hello-wave/build.zig.zon | |
| new file mode 100644 | |
| index 0000000..79c2910 | |
| --- /dev/null | |
| +++ b/examples/01-getting-started/hello-wave/build.zig.zon | |
| @@ -0,0 +1,18 @@ | |
| +.{ | |
| + .name = .hello_wave, | |
| + .version = "0.1.0", | |
| + .fingerprint = 0x44bdec7cce7d03ec, | |
| + .minimum_zig_version = "0.15.2", | |
| + | |
| + .dependencies = .{ | |
| + .lightmix = .{ | |
| + .path = "../../..", | |
| + }, | |
| + }, | |
| + | |
| + .paths = .{ | |
| + "build.zig", | |
| + "build.zig.zon", | |
| + "src", | |
| + }, | |
| +} | |
| diff --git a/examples/01-getting-started/hello-wave/src/main.zig b/examples/01-getting-started/hello-wave/src/main.zig | |
| new file mode 100644 | |
| index 0000000..0ee97ea | |
| --- /dev/null | |
| +++ b/examples/01-getting-started/hello-wave/src/main.zig | |
| @@ -0,0 +1,66 @@ | |
| +//! # Hello Wave - Your First Audio with lightmix | |
| +//! | |
| +//! This is the simplest possible example using lightmix. | |
| +//! It creates a 1-second sine wave at 440Hz (musical note A4) and saves it to a WAV file. | |
| +//! | |
| +//! ## What you'll learn: | |
| +//! - How to generate audio samples | |
| +//! - How to create a Wave from samples | |
| +//! - How to save audio to a WAV file | |
| +//! | |
| +//! ## Run this example: | |
| +//! ``` | |
| +//! zig build run | |
| +//! ``` | |
| +//! This will create `result.wav` in the current directory. | |
| + | |
| +const std = @import("std"); | |
| +const lightmix = @import("lightmix"); | |
| +const Wave = lightmix.Wave; | |
| + | |
| +pub fn main() !void { | |
| + // Use page allocator for simplicity | |
| + // In production, consider using a more sophisticated allocator | |
| + const allocator = std.heap.page_allocator; | |
| + | |
| + // Generate a 440Hz sine wave (A4 note) | |
| + const frequency: f64 = 440.0; | |
| + const sample_rate: f64 = 44100.0; | |
| + | |
| + // Calculate radians per second for sine wave generation | |
| + const radians_per_sec: f64 = frequency * 2.0 * std.math.pi; | |
| + | |
| + // Generate the audio samples | |
| + var samples: [44100]f64 = undefined; | |
| + for (0..samples.len) |i| { | |
| + const t = @as(f64, @floatFromInt(i)) / sample_rate; | |
| + samples[i] = 0.5 * @sin(radians_per_sec * t); // 0.5 = volume control | |
| + } | |
| + | |
| + // Create a Wave object from our samples | |
| + const wave = try Wave(f64).init(samples[0..], allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, // Mono audio | |
| + }); | |
| + defer wave.deinit(); | |
| + | |
| + // Save the wave to a WAV file | |
| + const file = try std.fs.cwd().createFile("result.wav", .{}); | |
| + defer file.close(); | |
| + const buf = try allocator.alloc(u8, 10 * 1024 * 1024); | |
| + defer allocator.free(buf); | |
| + var writer = file.writer(buf); | |
| + | |
| + // Write as 16-bit integer PCM (most common format) | |
| + try wave.write(.wav, &writer.interface, .{ | |
| + .format_code = .pcm, | |
| + .bits = 16, | |
| + }); | |
| + | |
| + try writer.interface.flush(); | |
| + | |
| + std.debug.print("✓ Created result.wav - A 440Hz sine wave!\n", .{}); | |
| + std.debug.print(" Duration: 1 second\n", .{}); | |
| + std.debug.print(" Sample rate: 44100 Hz\n", .{}); | |
| + std.debug.print(" Format: 16-bit mono\n", .{}); | |
| +} | |
| diff --git a/examples/guitar/acoustic_guitar/build.zig b/examples/01-getting-started/using-filters/build.zig | |
| similarity index 81% | |
| rename from examples/guitar/acoustic_guitar/build.zig | |
| rename to examples/01-getting-started/using-filters/build.zig | |
| index eab9e81..c24971f 100644 | |
| --- a/examples/guitar/acoustic_guitar/build.zig | |
| +++ b/examples/01-getting-started/using-filters/build.zig | |
| @@ -4,42 +4,37 @@ pub fn build(b: *std.Build) void { | |
| const target = b.standardTargetOptions(.{}); | |
| const optimize = b.standardOptimizeOption(.{}); | |
| - // Dependencies | |
| const lightmix = b.dependency("lightmix", .{}); | |
| - // Executable Module | |
| const exe_mod = b.createModule(.{ | |
| .root_source_file = b.path("src/main.zig"), | |
| .target = target, | |
| .optimize = optimize, | |
| + .imports = &.{ | |
| + .{ .name = "lightmix", .module = lightmix.module("lightmix") }, | |
| + }, | |
| }); | |
| - exe_mod.addImport("lightmix", lightmix.module("lightmix")); | |
| - // Executable | |
| const exe = b.addExecutable(.{ | |
| - .name = "acoustic_guitar", | |
| + .name = "using-filters", | |
| .root_module = exe_mod, | |
| }); | |
| b.installArtifact(exe); | |
| - // Rum cmd | |
| const run_cmd = b.addRunArtifact(exe); | |
| run_cmd.step.dependOn(b.getInstallStep()); | |
| if (b.args) |args| { | |
| run_cmd.addArgs(args); | |
| } | |
| - // Run step | |
| const run_step = b.step("run", "Run the app"); | |
| run_step.dependOn(&run_cmd.step); | |
| - // Unit tests | |
| const exe_unit_tests = b.addTest(.{ | |
| .root_module = exe_mod, | |
| }); | |
| const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); | |
| - // Test step | |
| const test_step = b.step("test", "Run unit tests"); | |
| test_step.dependOn(&run_exe_unit_tests.step); | |
| } | |
| diff --git a/examples/01-getting-started/using-filters/build.zig.zon b/examples/01-getting-started/using-filters/build.zig.zon | |
| new file mode 100644 | |
| index 0000000..baf0591 | |
| --- /dev/null | |
| +++ b/examples/01-getting-started/using-filters/build.zig.zon | |
| @@ -0,0 +1,18 @@ | |
| +.{ | |
| + .name = .using_filters, | |
| + .version = "0.1.0", | |
| + .fingerprint = 0x4ed3f39bf2fcbf80, | |
| + .minimum_zig_version = "0.15.2", | |
| + | |
| + .dependencies = .{ | |
| + .lightmix = .{ | |
| + .path = "../../..", | |
| + }, | |
| + }, | |
| + | |
| + .paths = .{ | |
| + "build.zig", | |
| + "build.zig.zon", | |
| + "src", | |
| + }, | |
| +} | |
| diff --git a/examples/01-getting-started/using-filters/src/main.zig b/examples/01-getting-started/using-filters/src/main.zig | |
| new file mode 100644 | |
| index 0000000..b82a67a | |
| --- /dev/null | |
| +++ b/examples/01-getting-started/using-filters/src/main.zig | |
| @@ -0,0 +1,84 @@ | |
| +//! # Using Filters - Transform Your Audio | |
| +//! | |
| +//! This example demonstrates how to use filters to transform audio waves. | |
| +//! We'll create a sine wave and apply a decay filter to create a fade-out effect. | |
| +//! | |
| +//! ## What you'll learn: | |
| +//! - How to create custom filter functions | |
| +//! - How to apply filters to waves | |
| +//! - How filter chaining works | |
| +//! | |
| +//! ## Run this example: | |
| +//! ``` | |
| +//! zig build run | |
| +//! ``` | |
| + | |
| +const std = @import("std"); | |
| +const lightmix = @import("lightmix"); | |
| +const Wave = lightmix.Wave; | |
| + | |
| +pub fn main() !void { | |
| + const allocator = std.heap.page_allocator; | |
| + | |
| + // Generate a 440Hz sine wave | |
| + const frequency: f64 = 440.0; | |
| + const sample_rate: f64 = 44100.0; | |
| + const radians_per_sec: f64 = frequency * 2.0 * std.math.pi; | |
| + | |
| + var samples: [44100]f64 = undefined; | |
| + for (0..samples.len) |i| { | |
| + const t = @as(f64, @floatFromInt(i)) / sample_rate; | |
| + samples[i] = 0.5 * @sin(radians_per_sec * t); | |
| + } | |
| + | |
| + var wave = try Wave(f64).init(samples[0..], allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + try wave.filter(decayFilter); | |
| + defer wave.deinit(); | |
| + | |
| + // Save the result | |
| + const file = try std.fs.cwd().createFile("result.wav", .{}); | |
| + defer file.close(); | |
| + const buf = try allocator.alloc(u8, 10 * 1024 * 1024); | |
| + defer allocator.free(buf); | |
| + var writer = file.writer(buf); | |
| + | |
| + try wave.write(.wav, &writer.interface, .{ | |
| + .format_code = .pcm, | |
| + .bits = 16, | |
| + }); | |
| + | |
| + try writer.interface.flush(); | |
| + | |
| + std.debug.print("✓ Created result.wav with fade-out effect!\n", .{}); | |
| +} | |
| + | |
| +/// Decay filter: Creates a linear fade-out effect | |
| +/// The volume decreases from 100% to 0% over the duration of the wave | |
| +fn decayFilter(comptime T: type, original_wave: Wave(T)) !Wave(T) { | |
| + var result_list: std.array_list.Aligned(T, null) = .empty; | |
| + | |
| + // Process each sample, applying a decay factor | |
| + for (original_wave.samples, 0..) |sample, n| { | |
| + // Calculate how far from the end we are | |
| + const remaining_samples = original_wave.samples.len - n; | |
| + | |
| + // Decay factor: 1.0 at start, 0.0 at end | |
| + const decay_factor = @as(T, @floatFromInt(remaining_samples)) / | |
| + @as(T, @floatFromInt(original_wave.samples.len)); | |
| + | |
| + // Apply the decay to the sample | |
| + const decayed_sample = sample * decay_factor; | |
| + try result_list.append(original_wave.allocator, decayed_sample); | |
| + } | |
| + | |
| + // Return a new Wave with the filtered samples | |
| + return Wave(T){ | |
| + .samples = try result_list.toOwnedSlice(original_wave.allocator), | |
| + .allocator = original_wave.allocator, | |
| + .sample_rate = original_wave.sample_rate, | |
| + .channels = original_wave.channels, | |
| + }; | |
| +} | |
| diff --git a/examples/02-wave-basics/.gitignore b/examples/02-wave-basics/.gitignore | |
| new file mode 100644 | |
| index 0000000..3fa78f0 | |
| --- /dev/null | |
| +++ b/examples/02-wave-basics/.gitignore | |
| @@ -0,0 +1,4 @@ | |
| +brown_noise.wav | |
| +pink_noise.wav | |
| +white_noise.wav | |
| +result.wav | |
| diff --git a/examples/Wave/generate_sinewave/build.zig b/examples/02-wave-basics/noise/build.zig | |
| similarity index 80% | |
| rename from examples/Wave/generate_sinewave/build.zig | |
| rename to examples/02-wave-basics/noise/build.zig | |
| index 88023bd..dc81f22 100644 | |
| --- a/examples/Wave/generate_sinewave/build.zig | |
| +++ b/examples/02-wave-basics/noise/build.zig | |
| @@ -4,42 +4,37 @@ pub fn build(b: *std.Build) void { | |
| const target = b.standardTargetOptions(.{}); | |
| const optimize = b.standardOptimizeOption(.{}); | |
| - // Dependencies | |
| const lightmix = b.dependency("lightmix", .{}); | |
| - // Executable Module | |
| const exe_mod = b.createModule(.{ | |
| .root_source_file = b.path("src/main.zig"), | |
| .target = target, | |
| .optimize = optimize, | |
| + .imports = &.{ | |
| + .{ .name = "lightmix", .module = lightmix.module("lightmix") }, | |
| + }, | |
| }); | |
| - exe_mod.addImport("lightmix", lightmix.module("lightmix")); | |
| - // Executable | |
| const exe = b.addExecutable(.{ | |
| - .name = "generate_sinewave", | |
| + .name = "noise", | |
| .root_module = exe_mod, | |
| }); | |
| b.installArtifact(exe); | |
| - // Rum cmd | |
| const run_cmd = b.addRunArtifact(exe); | |
| run_cmd.step.dependOn(b.getInstallStep()); | |
| if (b.args) |args| { | |
| run_cmd.addArgs(args); | |
| } | |
| - // Run step | |
| const run_step = b.step("run", "Run the app"); | |
| run_step.dependOn(&run_cmd.step); | |
| - // Unit tests | |
| const exe_unit_tests = b.addTest(.{ | |
| .root_module = exe_mod, | |
| }); | |
| const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); | |
| - // Test step | |
| const test_step = b.step("test", "Run unit tests"); | |
| test_step.dependOn(&run_exe_unit_tests.step); | |
| } | |
| diff --git a/examples/02-wave-basics/noise/build.zig.zon b/examples/02-wave-basics/noise/build.zig.zon | |
| new file mode 100644 | |
| index 0000000..abe3465 | |
| --- /dev/null | |
| +++ b/examples/02-wave-basics/noise/build.zig.zon | |
| @@ -0,0 +1,17 @@ | |
| +.{ | |
| + .name = .noise, | |
| + .version = "0.1.0", | |
| + .fingerprint = 0xfda99629c4965348, | |
| + .minimum_zig_version = "0.15.2", | |
| + | |
| + .dependencies = .{ | |
| + .lightmix = .{ | |
| + .path = "../../..", | |
| + }, | |
| + }, | |
| + .paths = .{ | |
| + "build.zig", | |
| + "build.zig.zon", | |
| + "src", | |
| + }, | |
| +} | |
| diff --git a/examples/02-wave-basics/noise/src/main.zig b/examples/02-wave-basics/noise/src/main.zig | |
| new file mode 100644 | |
| index 0000000..5b6f2a5 | |
| --- /dev/null | |
| +++ b/examples/02-wave-basics/noise/src/main.zig | |
| @@ -0,0 +1,124 @@ | |
| +//! # Noise - Random Audio Signals | |
| +//! | |
| +//! This example demonstrates three types of noise commonly used in audio: | |
| +//! - White Noise: Equal energy across all frequencies (like TV static) | |
| +//! - Pink Noise: Equal energy per octave (more natural, like rain) | |
| +//! - Brown Noise: Even more bass-heavy (like ocean waves) | |
| +//! | |
| +//! ## What you'll learn: | |
| +//! - Using random number generation for audio | |
| +//! - Different types of noise and their characteristics | |
| +//! - Filtering techniques for colored noise | |
| + | |
| +const std = @import("std"); | |
| +const lightmix = @import("lightmix"); | |
| +const Wave = lightmix.Wave; | |
| + | |
| +pub fn main() !void { | |
| + const allocator = std.heap.page_allocator; | |
| + | |
| + // Generate different types of noise | |
| + const white_noise = try generateWhiteNoise(allocator); | |
| + defer white_noise.deinit(); | |
| + | |
| + const pink_noise = try generatePinkNoise(allocator); | |
| + defer pink_noise.deinit(); | |
| + | |
| + const brown_noise = try generateBrownNoise(allocator); | |
| + defer brown_noise.deinit(); | |
| + | |
| + // Save each to a file | |
| + try saveWave(white_noise, "white_noise.wav", allocator); | |
| + try saveWave(pink_noise, "pink_noise.wav", allocator); | |
| + try saveWave(brown_noise, "brown_noise.wav", allocator); | |
| + | |
| + std.debug.print("✓ Generated noise samples:\n", .{}); | |
| + std.debug.print(" white_noise.wav - Equal energy across frequencies\n", .{}); | |
| + std.debug.print(" pink_noise.wav - Equal energy per octave\n", .{}); | |
| + std.debug.print(" brown_noise.wav - Bass-heavy noise\n", .{}); | |
| +} | |
| + | |
| +/// White noise: completely random values | |
| +fn generateWhiteNoise(allocator: std.mem.Allocator) !Wave(f64) { | |
| + var prng = std.Random.DefaultPrng.init(0); | |
| + const rand = prng.random(); | |
| + | |
| + var samples: [44100]f64 = undefined; | |
| + for (0..samples.len) |i| { | |
| + // Random value between -0.5 and +0.5 | |
| + samples[i] = (rand.float(f64) * 2.0 - 1.0) * 0.5; | |
| + } | |
| + | |
| + return try Wave(f64).init(samples[0..], allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| +} | |
| + | |
| +/// Pink noise: filtered white noise with 1/f spectrum | |
| +fn generatePinkNoise(allocator: std.mem.Allocator) !Wave(f64) { | |
| + var prng = std.Random.DefaultPrng.init(0); | |
| + const rand = prng.random(); | |
| + | |
| + // Paul Kellett's refined method for pink noise | |
| + var b0: f64 = 0.0; | |
| + var b1: f64 = 0.0; | |
| + var b2: f64 = 0.0; | |
| + | |
| + var samples: [44100]f64 = undefined; | |
| + for (0..samples.len) |i| { | |
| + const white = rand.float(f64) * 2.0 - 1.0; | |
| + | |
| + b0 = 0.99765 * b0 + white * 0.0990460; | |
| + b1 = 0.96300 * b1 + white * 0.2965164; | |
| + b2 = 0.57000 * b2 + white * 1.0526913; | |
| + | |
| + samples[i] = (b0 + b1 + b2 + white * 0.1848) * 0.15; | |
| + } | |
| + | |
| + return try Wave(f64).init(samples[0..], allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| +} | |
| + | |
| +/// Brown noise: integrated white noise (random walk) | |
| +fn generateBrownNoise(allocator: std.mem.Allocator) !Wave(f64) { | |
| + var prng = std.Random.DefaultPrng.init(0); | |
| + const rand = prng.random(); | |
| + | |
| + var last_value: f64 = 0.0; | |
| + var samples: [44100]f64 = undefined; | |
| + | |
| + for (0..samples.len) |i| { | |
| + // Add small random step to previous value | |
| + const step = (rand.float(f64) * 2.0 - 1.0) * 0.02; | |
| + last_value += step; | |
| + | |
| + // Clamp to prevent drift | |
| + last_value = @max(-1.0, @min(1.0, last_value)); | |
| + | |
| + samples[i] = last_value * 0.5; | |
| + } | |
| + | |
| + return try Wave(f64).init(samples[0..], allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| +} | |
| + | |
| +fn saveWave(wave: Wave(f64), filename: []const u8, allocator: std.mem.Allocator) !void { | |
| + const file = try std.fs.cwd().createFile(filename, .{}); | |
| + defer file.close(); | |
| + const buf = try allocator.alloc(u8, 10 * 1024 * 1024); | |
| + defer allocator.free(buf); | |
| + var writer = file.writer(buf); | |
| + | |
| + // Write as 16-bit integer PCM (most common format) | |
| + try wave.write(.wav, &writer.interface, .{ | |
| + .format_code = .pcm, | |
| + .bits = 16, | |
| + }); | |
| + | |
| + try writer.interface.flush(); | |
| +} | |
| diff --git a/examples/Wave/generate_soundless/build.zig b/examples/02-wave-basics/sawtooth-wave/build.zig | |
| similarity index 80% | |
| rename from examples/Wave/generate_soundless/build.zig | |
| rename to examples/02-wave-basics/sawtooth-wave/build.zig | |
| index 88023bd..6dc2a16 100644 | |
| --- a/examples/Wave/generate_soundless/build.zig | |
| +++ b/examples/02-wave-basics/sawtooth-wave/build.zig | |
| @@ -4,42 +4,37 @@ pub fn build(b: *std.Build) void { | |
| const target = b.standardTargetOptions(.{}); | |
| const optimize = b.standardOptimizeOption(.{}); | |
| - // Dependencies | |
| const lightmix = b.dependency("lightmix", .{}); | |
| - // Executable Module | |
| const exe_mod = b.createModule(.{ | |
| .root_source_file = b.path("src/main.zig"), | |
| .target = target, | |
| .optimize = optimize, | |
| + .imports = &.{ | |
| + .{ .name = "lightmix", .module = lightmix.module("lightmix") }, | |
| + }, | |
| }); | |
| - exe_mod.addImport("lightmix", lightmix.module("lightmix")); | |
| - // Executable | |
| const exe = b.addExecutable(.{ | |
| - .name = "generate_sinewave", | |
| + .name = "sawtooth-wave", | |
| .root_module = exe_mod, | |
| }); | |
| b.installArtifact(exe); | |
| - // Rum cmd | |
| const run_cmd = b.addRunArtifact(exe); | |
| run_cmd.step.dependOn(b.getInstallStep()); | |
| if (b.args) |args| { | |
| run_cmd.addArgs(args); | |
| } | |
| - // Run step | |
| const run_step = b.step("run", "Run the app"); | |
| run_step.dependOn(&run_cmd.step); | |
| - // Unit tests | |
| const exe_unit_tests = b.addTest(.{ | |
| .root_module = exe_mod, | |
| }); | |
| const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); | |
| - // Test step | |
| const test_step = b.step("test", "Run unit tests"); | |
| test_step.dependOn(&run_exe_unit_tests.step); | |
| } | |
| diff --git a/examples/02-wave-basics/sawtooth-wave/build.zig.zon b/examples/02-wave-basics/sawtooth-wave/build.zig.zon | |
| new file mode 100644 | |
| index 0000000..e4cdfb9 | |
| --- /dev/null | |
| +++ b/examples/02-wave-basics/sawtooth-wave/build.zig.zon | |
| @@ -0,0 +1,18 @@ | |
| +.{ | |
| + .name = .sawtooth_wave, | |
| + .version = "0.1.0", | |
| + .fingerprint = 0xa494bbdff0765ae5, | |
| + .minimum_zig_version = "0.15.2", | |
| + | |
| + .dependencies = .{ | |
| + .lightmix = .{ | |
| + .path = "../../..", | |
| + }, | |
| + }, | |
| + | |
| + .paths = .{ | |
| + "build.zig", | |
| + "build.zig.zon", | |
| + "src", | |
| + }, | |
| +} | |
| diff --git a/examples/02-wave-basics/sawtooth-wave/src/main.zig b/examples/02-wave-basics/sawtooth-wave/src/main.zig | |
| new file mode 100644 | |
| index 0000000..36fa34e | |
| --- /dev/null | |
| +++ b/examples/02-wave-basics/sawtooth-wave/src/main.zig | |
| @@ -0,0 +1,51 @@ | |
| +//! # Sawtooth Wave - The Bright Sound | |
| +//! | |
| +//! A sawtooth wave has a linear rise and sharp fall, creating a bright, harsh sound. | |
| +//! It's rich in both odd and even harmonics, making it useful for brass and string synthesis. | |
| +//! | |
| +//! ## What you'll learn: | |
| +//! - How to generate a sawtooth wave using modulo operation | |
| +//! - Understanding harmonic richness | |
| +//! - Applications in synthesis | |
| + | |
| +const std = @import("std"); | |
| +const lightmix = @import("lightmix"); | |
| +const Wave = lightmix.Wave; | |
| + | |
| +pub fn main() !void { | |
| + const allocator = std.heap.page_allocator; | |
| + | |
| + // Generate a 440Hz sawtooth wave | |
| + const frequency: f64 = 440.0; | |
| + const sample_rate: f64 = 44100.0; | |
| + const period: f64 = sample_rate / frequency; | |
| + | |
| + var samples: [44100]f64 = undefined; | |
| + for (0..samples.len) |i| { | |
| + // Calculate position within the current period (0.0 to 1.0) | |
| + const phase = @mod(@as(f64, @floatFromInt(i)), period) / period; | |
| + | |
| + // Sawtooth wave: Linear ramp from -1 to +1 | |
| + samples[i] = (phase * 2.0) - 1.0; | |
| + } | |
| + | |
| + const wave = try Wave(f64).init(samples[0..], allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + defer wave.deinit(); | |
| + | |
| + const file = try std.fs.cwd().createFile("result.wav", .{}); | |
| + defer file.close(); | |
| + const buf = try allocator.alloc(u8, 10 * 1024 * 1024); | |
| + var writer = file.writer(buf); | |
| + | |
| + try wave.write(.wav, &writer.interface, .{ | |
| + .bits = 16, | |
| + .format_code = .pcm, | |
| + }); | |
| + | |
| + try writer.interface.flush(); | |
| + | |
| + std.debug.print("✓ Generated sawtooth wave at 440 Hz\n", .{}); | |
| +} | |
| diff --git a/examples/02-wave-basics/sine-wave/build.zig b/examples/02-wave-basics/sine-wave/build.zig | |
| new file mode 100644 | |
| index 0000000..0814bc0 | |
| --- /dev/null | |
| +++ b/examples/02-wave-basics/sine-wave/build.zig | |
| @@ -0,0 +1,40 @@ | |
| +const std = @import("std"); | |
| + | |
| +pub fn build(b: *std.Build) void { | |
| + const target = b.standardTargetOptions(.{}); | |
| + const optimize = b.standardOptimizeOption(.{}); | |
| + | |
| + const lightmix = b.dependency("lightmix", .{}); | |
| + | |
| + const exe_mod = b.createModule(.{ | |
| + .root_source_file = b.path("src/main.zig"), | |
| + .target = target, | |
| + .optimize = optimize, | |
| + .imports = &.{ | |
| + .{ .name = "lightmix", .module = lightmix.module("lightmix") }, | |
| + }, | |
| + }); | |
| + | |
| + const exe = b.addExecutable(.{ | |
| + .name = "sine-wave", | |
| + .root_module = exe_mod, | |
| + }); | |
| + b.installArtifact(exe); | |
| + | |
| + const run_cmd = b.addRunArtifact(exe); | |
| + run_cmd.step.dependOn(b.getInstallStep()); | |
| + if (b.args) |args| { | |
| + run_cmd.addArgs(args); | |
| + } | |
| + | |
| + const run_step = b.step("run", "Run the app"); | |
| + run_step.dependOn(&run_cmd.step); | |
| + | |
| + const exe_unit_tests = b.addTest(.{ | |
| + .root_module = exe_mod, | |
| + }); | |
| + const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); | |
| + | |
| + const test_step = b.step("test", "Run unit tests"); | |
| + test_step.dependOn(&run_exe_unit_tests.step); | |
| +} | |
| diff --git a/examples/02-wave-basics/sine-wave/build.zig.zon b/examples/02-wave-basics/sine-wave/build.zig.zon | |
| new file mode 100644 | |
| index 0000000..a91b3ce | |
| --- /dev/null | |
| +++ b/examples/02-wave-basics/sine-wave/build.zig.zon | |
| @@ -0,0 +1,18 @@ | |
| +.{ | |
| + .name = .sine_wave, | |
| + .version = "0.1.0", | |
| + .fingerprint = 0x2230d3f0bce50a14, | |
| + .minimum_zig_version = "0.15.2", | |
| + | |
| + .dependencies = .{ | |
| + .lightmix = .{ | |
| + .path = "../../..", | |
| + }, | |
| + }, | |
| + | |
| + .paths = .{ | |
| + "build.zig", | |
| + "build.zig.zon", | |
| + "src", | |
| + }, | |
| +} | |
| diff --git a/examples/02-wave-basics/sine-wave/src/main.zig b/examples/02-wave-basics/sine-wave/src/main.zig | |
| new file mode 100644 | |
| index 0000000..07fe272 | |
| --- /dev/null | |
| +++ b/examples/02-wave-basics/sine-wave/src/main.zig | |
| @@ -0,0 +1,55 @@ | |
| +//! # Sine Wave - The Pure Tone | |
| +//! | |
| +//! A sine wave is the most fundamental waveform in audio synthesis. | |
| +//! It produces a pure tone with no harmonics, sounding very smooth and "electronic". | |
| +//! | |
| +//! ## What you'll learn: | |
| +//! - Mathematical formula for sine wave generation | |
| +//! - Relationship between frequency and pitch | |
| +//! - How sample rate affects audio quality | |
| +//! | |
| +//! ## Musical note in this example: | |
| +//! - A4: 440.0 Hz (standard tuning reference) | |
| + | |
| +const std = @import("std"); | |
| +const lightmix = @import("lightmix"); | |
| +const Wave = lightmix.Wave; | |
| + | |
| +pub fn main() !void { | |
| + const allocator = std.heap.page_allocator; | |
| + | |
| + // Generate a 440Hz sine wave (A4 note) | |
| + const frequency: f64 = 440.0; | |
| + const sample_rate: f64 = 44100.0; | |
| + const radians_per_sec: f64 = frequency * 2.0 * std.math.pi; | |
| + const volume: f64 = 0.5; | |
| + | |
| + var samples: [44100]f64 = undefined; | |
| + for (0..samples.len) |i| { | |
| + const t = @as(f64, @floatFromInt(i)) / sample_rate; | |
| + // Sine wave formula: amplitude * sin(2π * frequency * time) | |
| + samples[i] = volume * @sin(radians_per_sec * t); | |
| + } | |
| + | |
| + const wave = try Wave(f64).init(samples[0..], allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + defer wave.deinit(); | |
| + | |
| + // Save to file | |
| + const file = try std.fs.cwd().createFile("result.wav", .{}); | |
| + defer file.close(); | |
| + const buf = try allocator.alloc(u8, 10 * 1024 * 1024); | |
| + defer allocator.free(buf); | |
| + var writer = file.writer(buf); | |
| + | |
| + try wave.write(.wav, &writer.interface, .{ | |
| + .format_code = .pcm, | |
| + .bits = 16, | |
| + }); | |
| + | |
| + try writer.interface.flush(); | |
| + | |
| + std.debug.print("✓ Generated sine wave at 440 Hz\n", .{}); | |
| +} | |
| diff --git a/examples/02-wave-basics/square-wave/build.zig b/examples/02-wave-basics/square-wave/build.zig | |
| new file mode 100644 | |
| index 0000000..2d6c65d | |
| --- /dev/null | |
| +++ b/examples/02-wave-basics/square-wave/build.zig | |
| @@ -0,0 +1,40 @@ | |
| +const std = @import("std"); | |
| + | |
| +pub fn build(b: *std.Build) void { | |
| + const target = b.standardTargetOptions(.{}); | |
| + const optimize = b.standardOptimizeOption(.{}); | |
| + | |
| + const lightmix = b.dependency("lightmix", .{}); | |
| + | |
| + const exe_mod = b.createModule(.{ | |
| + .root_source_file = b.path("src/main.zig"), | |
| + .target = target, | |
| + .optimize = optimize, | |
| + .imports = &.{ | |
| + .{ .name = "lightmix", .module = lightmix.module("lightmix") }, | |
| + }, | |
| + }); | |
| + | |
| + const exe = b.addExecutable(.{ | |
| + .name = "square-wave", | |
| + .root_module = exe_mod, | |
| + }); | |
| + b.installArtifact(exe); | |
| + | |
| + const run_cmd = b.addRunArtifact(exe); | |
| + run_cmd.step.dependOn(b.getInstallStep()); | |
| + if (b.args) |args| { | |
| + run_cmd.addArgs(args); | |
| + } | |
| + | |
| + const run_step = b.step("run", "Run the app"); | |
| + run_step.dependOn(&run_cmd.step); | |
| + | |
| + const exe_unit_tests = b.addTest(.{ | |
| + .root_module = exe_mod, | |
| + }); | |
| + const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); | |
| + | |
| + const test_step = b.step("test", "Run unit tests"); | |
| + test_step.dependOn(&run_exe_unit_tests.step); | |
| +} | |
| diff --git a/examples/02-wave-basics/square-wave/build.zig.zon b/examples/02-wave-basics/square-wave/build.zig.zon | |
| new file mode 100644 | |
| index 0000000..3b5ec63 | |
| --- /dev/null | |
| +++ b/examples/02-wave-basics/square-wave/build.zig.zon | |
| @@ -0,0 +1,18 @@ | |
| +.{ | |
| + .name = .square_wave, | |
| + .version = "0.1.0", | |
| + .fingerprint = 0xd1743863eb9f5d29, | |
| + .minimum_zig_version = "0.15.2", | |
| + | |
| + .dependencies = .{ | |
| + .lightmix = .{ | |
| + .path = "../../..", | |
| + }, | |
| + }, | |
| + | |
| + .paths = .{ | |
| + "build.zig", | |
| + "build.zig.zon", | |
| + "src", | |
| + }, | |
| +} | |
| diff --git a/examples/02-wave-basics/square-wave/src/main.zig b/examples/02-wave-basics/square-wave/src/main.zig | |
| new file mode 100644 | |
| index 0000000..c634cb7 | |
| --- /dev/null | |
| +++ b/examples/02-wave-basics/square-wave/src/main.zig | |
| @@ -0,0 +1,56 @@ | |
| +//! # Square Wave - The Digital Sound | |
| +//! | |
| +//! A square wave alternates between two values (high and low) with sharp transitions. | |
| +//! It produces a rich, buzzy sound with many odd harmonics, commonly used in retro video game music. | |
| +//! | |
| +//! ## What you'll learn: | |
| +//! - How to generate a square wave | |
| +//! - Understanding duty cycle (50% in this example) | |
| +//! - The characteristic "buzzy" sound of square waves | |
| +//! | |
| +//! ## Sound characteristics: | |
| +//! - Contains fundamental frequency + odd harmonics (3rd, 5th, 7th, etc.) | |
| +//! - Sharp, electronic, "8-bit" sound | |
| + | |
| +const std = @import("std"); | |
| +const lightmix = @import("lightmix"); | |
| +const Wave = lightmix.Wave; | |
| + | |
| +pub fn main() !void { | |
| + const allocator = std.heap.page_allocator; | |
| + | |
| + // Generate a 440Hz square wave | |
| + const frequency: f64 = 440.0; | |
| + const sample_rate: f64 = 44100.0; | |
| + const volume: f64 = 0.5; | |
| + | |
| + var samples: [44100]f64 = undefined; | |
| + for (0..samples.len) |i| { | |
| + const t = @as(f64, @floatFromInt(i)) / sample_rate; | |
| + const phase = t * frequency; | |
| + | |
| + // Square wave: +1 for first half of cycle, -1 for second half | |
| + // We use @mod to get the fractional part of the phase | |
| + samples[i] = if (@mod(phase, 1.0) < 0.5) volume else -volume; | |
| + } | |
| + | |
| + const wave = try Wave(f64).init(samples[0..], allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + defer wave.deinit(); | |
| + | |
| + const file = try std.fs.cwd().createFile("result.wav", .{}); | |
| + defer file.close(); | |
| + const buf = try allocator.alloc(u8, 10 * 1024 * 1024); | |
| + var writer = file.writer(buf); | |
| + | |
| + try wave.write(.wav, &writer.interface, .{ | |
| + .bits = 16, | |
| + .format_code = .pcm, | |
| + }); | |
| + | |
| + try writer.interface.flush(); | |
| + std.debug.print("✓ Generated square wave at 440 Hz\n", .{}); | |
| + std.debug.print(" Duty cycle: 50%\n", .{}); | |
| +} | |
| diff --git a/examples/02-wave-basics/triangle-wave/build.zig b/examples/02-wave-basics/triangle-wave/build.zig | |
| new file mode 100644 | |
| index 0000000..c6837be | |
| --- /dev/null | |
| +++ b/examples/02-wave-basics/triangle-wave/build.zig | |
| @@ -0,0 +1,40 @@ | |
| +const std = @import("std"); | |
| + | |
| +pub fn build(b: *std.Build) void { | |
| + const target = b.standardTargetOptions(.{}); | |
| + const optimize = b.standardOptimizeOption(.{}); | |
| + | |
| + const lightmix = b.dependency("lightmix", .{}); | |
| + | |
| + const exe_mod = b.createModule(.{ | |
| + .root_source_file = b.path("src/main.zig"), | |
| + .target = target, | |
| + .optimize = optimize, | |
| + .imports = &.{ | |
| + .{ .name = "lightmix", .module = lightmix.module("lightmix") }, | |
| + }, | |
| + }); | |
| + | |
| + const exe = b.addExecutable(.{ | |
| + .name = "triangle-wave", | |
| + .root_module = exe_mod, | |
| + }); | |
| + b.installArtifact(exe); | |
| + | |
| + const run_cmd = b.addRunArtifact(exe); | |
| + run_cmd.step.dependOn(b.getInstallStep()); | |
| + if (b.args) |args| { | |
| + run_cmd.addArgs(args); | |
| + } | |
| + | |
| + const run_step = b.step("run", "Run the app"); | |
| + run_step.dependOn(&run_cmd.step); | |
| + | |
| + const exe_unit_tests = b.addTest(.{ | |
| + .root_module = exe_mod, | |
| + }); | |
| + const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); | |
| + | |
| + const test_step = b.step("test", "Run unit tests"); | |
| + test_step.dependOn(&run_exe_unit_tests.step); | |
| +} | |
| diff --git a/examples/02-wave-basics/triangle-wave/build.zig.zon b/examples/02-wave-basics/triangle-wave/build.zig.zon | |
| new file mode 100644 | |
| index 0000000..0da389e | |
| --- /dev/null | |
| +++ b/examples/02-wave-basics/triangle-wave/build.zig.zon | |
| @@ -0,0 +1,18 @@ | |
| +.{ | |
| + .name = .triangle_wave, | |
| + .version = "0.1.0", | |
| + .fingerprint = 0xb357e96b64517423, | |
| + .minimum_zig_version = "0.15.2", | |
| + | |
| + .dependencies = .{ | |
| + .lightmix = .{ | |
| + .path = "../../..", | |
| + }, | |
| + }, | |
| + | |
| + .paths = .{ | |
| + "build.zig", | |
| + "build.zig.zon", | |
| + "src", | |
| + }, | |
| +} | |
| diff --git a/examples/02-wave-basics/triangle-wave/src/main.zig b/examples/02-wave-basics/triangle-wave/src/main.zig | |
| new file mode 100644 | |
| index 0000000..76b920c | |
| --- /dev/null | |
| +++ b/examples/02-wave-basics/triangle-wave/src/main.zig | |
| @@ -0,0 +1,56 @@ | |
| +//! # Triangle Wave - The Mellow Sound | |
| +//! | |
| +//! A triangle wave rises and falls linearly, producing a mellow sound. | |
| +//! It contains only odd harmonics (like a square wave), but they fall off faster, | |
| +//! making it softer and rounder than a square wave. | |
| +//! | |
| +//! ## What you'll learn: | |
| +//! - How to generate a triangle wave | |
| +//! - Piecewise linear function implementation | |
| +//! - Balancing between sine and square wave characteristics | |
| + | |
| +const std = @import("std"); | |
| +const lightmix = @import("lightmix"); | |
| +const Wave = lightmix.Wave; | |
| + | |
| +pub fn main() !void { | |
| + const allocator = std.heap.page_allocator; | |
| + | |
| + // Generate a 440Hz triangle wave | |
| + const frequency: f64 = 440.0; | |
| + const sample_rate: f64 = 44100.0; | |
| + | |
| + var samples: [44100]f64 = undefined; | |
| + for (0..samples.len) |i| { | |
| + const t = @as(f64, @floatFromInt(i)) / sample_rate; | |
| + const phase = @mod(t * frequency, 1.0); | |
| + | |
| + // Triangle wave: rises from 0 to 1 in first half, falls from 1 to 0 in second half | |
| + // Transform to -1 to +1 range | |
| + samples[i] = if (phase < 0.5) | |
| + 4.0 * phase - 1.0 // Rising: -1 to +1 | |
| + else | |
| + 3.0 - 4.0 * phase; // Falling: +1 to -1 | |
| + } | |
| + | |
| + const wave = try Wave(f64).init(samples[0..], allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + defer wave.deinit(); | |
| + | |
| + const file = try std.fs.cwd().createFile("result.wav", .{}); | |
| + defer file.close(); | |
| + const buf = try allocator.alloc(u8, 10 * 1024 * 1024); | |
| + defer allocator.free(buf); | |
| + var writer = file.writer(buf); | |
| + | |
| + try wave.write(.wav, &writer.interface, .{ | |
| + .format_code = .pcm, | |
| + .bits = 16, | |
| + }); | |
| + | |
| + try writer.interface.flush(); | |
| + | |
| + std.debug.print("✓ Generated triangle wave at 440 Hz\n", .{}); | |
| +} | |
| diff --git a/examples/03-wave-operations/.gitignore b/examples/03-wave-operations/.gitignore | |
| new file mode 100644 | |
| index 0000000..a4beede | |
| --- /dev/null | |
| +++ b/examples/03-wave-operations/.gitignore | |
| @@ -0,0 +1,4 @@ | |
| +result.wav | |
| +a3_octave_down.wav | |
| +a4_original.wav | |
| +a5_octave_up.wav | |
| diff --git a/examples/03-wave-operations/filtering/build.zig b/examples/03-wave-operations/filtering/build.zig | |
| new file mode 100644 | |
| index 0000000..8019df1 | |
| --- /dev/null | |
| +++ b/examples/03-wave-operations/filtering/build.zig | |
| @@ -0,0 +1,40 @@ | |
| +const std = @import("std"); | |
| + | |
| +pub fn build(b: *std.Build) void { | |
| + const target = b.standardTargetOptions(.{}); | |
| + const optimize = b.standardOptimizeOption(.{}); | |
| + | |
| + const lightmix = b.dependency("lightmix", .{}); | |
| + | |
| + const exe_mod = b.createModule(.{ | |
| + .root_source_file = b.path("src/main.zig"), | |
| + .target = target, | |
| + .optimize = optimize, | |
| + .imports = &.{ | |
| + .{ .name = "lightmix", .module = lightmix.module("lightmix") }, | |
| + }, | |
| + }); | |
| + | |
| + const exe = b.addExecutable(.{ | |
| + .name = "filtering", | |
| + .root_module = exe_mod, | |
| + }); | |
| + b.installArtifact(exe); | |
| + | |
| + const run_cmd = b.addRunArtifact(exe); | |
| + run_cmd.step.dependOn(b.getInstallStep()); | |
| + if (b.args) |args| { | |
| + run_cmd.addArgs(args); | |
| + } | |
| + | |
| + const run_step = b.step("run", "Run the app"); | |
| + run_step.dependOn(&run_cmd.step); | |
| + | |
| + const exe_unit_tests = b.addTest(.{ | |
| + .root_module = exe_mod, | |
| + }); | |
| + const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); | |
| + | |
| + const test_step = b.step("test", "Run unit tests"); | |
| + test_step.dependOn(&run_exe_unit_tests.step); | |
| +} | |
| diff --git a/examples/03-wave-operations/filtering/build.zig.zon b/examples/03-wave-operations/filtering/build.zig.zon | |
| new file mode 100644 | |
| index 0000000..df03954 | |
| --- /dev/null | |
| +++ b/examples/03-wave-operations/filtering/build.zig.zon | |
| @@ -0,0 +1,18 @@ | |
| +.{ | |
| + .name = .filtering, | |
| + .version = "0.1.0", | |
| + .fingerprint = 0x253bec01838b0c59, | |
| + .minimum_zig_version = "0.15.2", | |
| + | |
| + .dependencies = .{ | |
| + .lightmix = .{ | |
| + .path = "../../..", | |
| + }, | |
| + }, | |
| + | |
| + .paths = .{ | |
| + "build.zig", | |
| + "build.zig.zon", | |
| + "src", | |
| + }, | |
| +} | |
| diff --git a/examples/03-wave-operations/filtering/src/main.zig b/examples/03-wave-operations/filtering/src/main.zig | |
| new file mode 100644 | |
| index 0000000..2668377 | |
| --- /dev/null | |
| +++ b/examples/03-wave-operations/filtering/src/main.zig | |
| @@ -0,0 +1,88 @@ | |
| +//! # Filtering - Apply Multiple Transformations | |
| +//! | |
| +//! This example shows how to chain multiple filters together to transform audio. | |
| +//! We'll create a sine wave and apply decay, volume reduction, and distortion filters. | |
| +//! | |
| +//! ## What you'll learn: | |
| +//! - How to chain multiple filters | |
| +//! - Creating different types of audio effects | |
| +//! - Understanding filter composition | |
| + | |
| +const std = @import("std"); | |
| +const lightmix = @import("lightmix"); | |
| +const Wave = lightmix.Wave; | |
| + | |
| +pub fn main() !void { | |
| + const allocator = std.heap.page_allocator; | |
| + | |
| + // Generate a sine wave | |
| + const frequency: f64 = 440.0; | |
| + const sample_rate: f64 = 44100.0; | |
| + const radians_per_sec: f64 = frequency * 2.0 * std.math.pi; | |
| + | |
| + var samples: [44100]f64 = undefined; | |
| + for (0..samples.len) |i| { | |
| + const t = @as(f64, @floatFromInt(i)) / sample_rate; | |
| + samples[i] = 0.8 * @sin(radians_per_sec * t); | |
| + } | |
| + | |
| + var wave = try Wave(f64).init(samples[0..], allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + try wave.filter(decayFilter); // Apply fade-out | |
| + try wave.filter(halveSampleValuesFilter); // Reduce volume | |
| + defer wave.deinit(); | |
| + | |
| + const file = try std.fs.cwd().createFile("result.wav", .{}); | |
| + defer file.close(); | |
| + const buf = try allocator.alloc(u8, 10 * 1024 * 1024); | |
| + defer allocator.free(buf); | |
| + var writer = file.writer(buf); | |
| + | |
| + try wave.write(.wav, &writer.interface, .{ | |
| + .format_code = .pcm, | |
| + .bits = 16, | |
| + }); | |
| + | |
| + try writer.interface.flush(); | |
| + | |
| + std.debug.print("✓ Applied multiple filters:\n", .{}); | |
| + std.debug.print(" 1. Decay (fade-out)\n", .{}); | |
| + std.debug.print(" 2. Volume reduction (50%)\n", .{}); | |
| +} | |
| + | |
| +/// Decay filter: Linear fade-out effect | |
| +fn decayFilter(comptime T: type, original_wave: Wave(T)) !Wave(T) { | |
| + var result_list: std.array_list.Aligned(T, null) = .empty; | |
| + | |
| + for (original_wave.samples, 0..) |sample, n| { | |
| + const remaining = original_wave.samples.len - n; | |
| + const decay_factor = @as(T, @floatFromInt(remaining)) / | |
| + @as(T, @floatFromInt(original_wave.samples.len)); | |
| + try result_list.append(original_wave.allocator, sample * decay_factor); | |
| + } | |
| + | |
| + return Wave(T){ | |
| + .samples = try result_list.toOwnedSlice(original_wave.allocator), | |
| + .allocator = original_wave.allocator, | |
| + .sample_rate = original_wave.sample_rate, | |
| + .channels = original_wave.channels, | |
| + }; | |
| +} | |
| + | |
| +/// Volume reduction filter: Halve all sample values | |
| +fn halveSampleValuesFilter(comptime T: type, original_wave: Wave(T)) !Wave(T) { | |
| + var result_list: std.array_list.Aligned(T, null) = .empty; | |
| + | |
| + for (original_wave.samples) |sample| { | |
| + try result_list.append(original_wave.allocator, sample * 0.5); | |
| + } | |
| + | |
| + return Wave(T){ | |
| + .samples = try result_list.toOwnedSlice(original_wave.allocator), | |
| + .allocator = original_wave.allocator, | |
| + .sample_rate = original_wave.sample_rate, | |
| + .channels = original_wave.channels, | |
| + }; | |
| +} | |
| diff --git a/examples/03-wave-operations/frequency-changes/build.zig b/examples/03-wave-operations/frequency-changes/build.zig | |
| new file mode 100644 | |
| index 0000000..d1b538d | |
| --- /dev/null | |
| +++ b/examples/03-wave-operations/frequency-changes/build.zig | |
| @@ -0,0 +1,40 @@ | |
| +const std = @import("std"); | |
| + | |
| +pub fn build(b: *std.Build) void { | |
| + const target = b.standardTargetOptions(.{}); | |
| + const optimize = b.standardOptimizeOption(.{}); | |
| + | |
| + const lightmix = b.dependency("lightmix", .{}); | |
| + | |
| + const exe_mod = b.createModule(.{ | |
| + .root_source_file = b.path("src/main.zig"), | |
| + .target = target, | |
| + .optimize = optimize, | |
| + .imports = &.{ | |
| + .{ .name = "lightmix", .module = lightmix.module("lightmix") }, | |
| + }, | |
| + }); | |
| + | |
| + const exe = b.addExecutable(.{ | |
| + .name = "frequency-changes", | |
| + .root_module = exe_mod, | |
| + }); | |
| + b.installArtifact(exe); | |
| + | |
| + const run_cmd = b.addRunArtifact(exe); | |
| + run_cmd.step.dependOn(b.getInstallStep()); | |
| + if (b.args) |args| { | |
| + run_cmd.addArgs(args); | |
| + } | |
| + | |
| + const run_step = b.step("run", "Run the app"); | |
| + run_step.dependOn(&run_cmd.step); | |
| + | |
| + const exe_unit_tests = b.addTest(.{ | |
| + .root_module = exe_mod, | |
| + }); | |
| + const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); | |
| + | |
| + const test_step = b.step("test", "Run unit tests"); | |
| + test_step.dependOn(&run_exe_unit_tests.step); | |
| +} | |
| diff --git a/examples/03-wave-operations/frequency-changes/build.zig.zon b/examples/03-wave-operations/frequency-changes/build.zig.zon | |
| new file mode 100644 | |
| index 0000000..165fb55 | |
| --- /dev/null | |
| +++ b/examples/03-wave-operations/frequency-changes/build.zig.zon | |
| @@ -0,0 +1,18 @@ | |
| +.{ | |
| + .name = .frequency_changes, | |
| + .version = "0.1.0", | |
| + .fingerprint = 0x1330bff611602aa, | |
| + .minimum_zig_version = "0.15.2", | |
| + | |
| + .dependencies = .{ | |
| + .lightmix = .{ | |
| + .path = "../../..", | |
| + }, | |
| + }, | |
| + | |
| + .paths = .{ | |
| + "build.zig", | |
| + "build.zig.zon", | |
| + "src", | |
| + }, | |
| +} | |
| diff --git a/examples/03-wave-operations/frequency-changes/src/main.zig b/examples/03-wave-operations/frequency-changes/src/main.zig | |
| new file mode 100644 | |
| index 0000000..a2fa561 | |
| --- /dev/null | |
| +++ b/examples/03-wave-operations/frequency-changes/src/main.zig | |
| @@ -0,0 +1,70 @@ | |
| +//! # Frequency Changes - Pitch Shifting | |
| +//! | |
| +//! This example demonstrates techniques for changing the pitch of audio: | |
| +//! - Doubling frequency (one octave up) | |
| +//! - Halving frequency (one octave down) | |
| +//! | |
| +//! ## What you'll learn: | |
| +//! - How frequency relates to pitch | |
| +//! - Simple pitch shifting techniques | |
| +//! - Octave relationships in music | |
| + | |
| +const std = @import("std"); | |
| +const lightmix = @import("lightmix"); | |
| +const Wave = lightmix.Wave; | |
| + | |
| +pub fn main() !void { | |
| + const allocator = std.heap.page_allocator; | |
| + | |
| + // Generate original 440Hz sine wave (A4) | |
| + const original = try generateSineWave(440.0, allocator); | |
| + defer original.deinit(); | |
| + | |
| + // One octave higher (880Hz - A5) | |
| + const octave_up = try generateSineWave(880.0, allocator); | |
| + defer octave_up.deinit(); | |
| + | |
| + // One octave lower (220Hz - A3) | |
| + const octave_down = try generateSineWave(220.0, allocator); | |
| + defer octave_down.deinit(); | |
| + | |
| + try saveWave(original, "a4_original.wav", allocator); | |
| + try saveWave(octave_up, "a5_octave_up.wav", allocator); | |
| + try saveWave(octave_down, "a3_octave_down.wav", allocator); | |
| + | |
| + std.debug.print("✓ Generated frequency variations:\n", .{}); | |
| + std.debug.print(" a4_original.wav - 440 Hz (A4)\n", .{}); | |
| + std.debug.print(" a5_octave_up.wav - 880 Hz (A5, +1 octave)\n", .{}); | |
| + std.debug.print(" a3_octave_down.wav - 220 Hz (A3, -1 octave)\n", .{}); | |
| +} | |
| + | |
| +fn generateSineWave(frequency: f64, allocator: std.mem.Allocator) !Wave(f64) { | |
| + const sample_rate: f64 = 44100.0; | |
| + const radians_per_sec: f64 = frequency * 2.0 * std.math.pi; | |
| + | |
| + var samples: [44100]f64 = undefined; | |
| + for (0..samples.len) |i| { | |
| + const t = @as(f64, @floatFromInt(i)) / sample_rate; | |
| + samples[i] = 0.5 * @sin(radians_per_sec * t); | |
| + } | |
| + | |
| + return try Wave(f64).init(samples[0..], allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| +} | |
| + | |
| +fn saveWave(wave: Wave(f64), filename: []const u8, allocator: std.mem.Allocator) !void { | |
| + const file = try std.fs.cwd().createFile(filename, .{}); | |
| + defer file.close(); | |
| + const buf = try allocator.alloc(u8, 10 * 1024 * 1024); | |
| + defer allocator.free(buf); | |
| + var writer = file.writer(buf); | |
| + | |
| + try wave.write(.wav, &writer.interface, .{ | |
| + .format_code = .pcm, | |
| + .bits = 16, | |
| + }); | |
| + | |
| + try writer.interface.flush(); | |
| +} | |
| diff --git a/examples/03-wave-operations/mixing-waves/build.zig b/examples/03-wave-operations/mixing-waves/build.zig | |
| new file mode 100644 | |
| index 0000000..4d29c8a | |
| --- /dev/null | |
| +++ b/examples/03-wave-operations/mixing-waves/build.zig | |
| @@ -0,0 +1,40 @@ | |
| +const std = @import("std"); | |
| + | |
| +pub fn build(b: *std.Build) void { | |
| + const target = b.standardTargetOptions(.{}); | |
| + const optimize = b.standardOptimizeOption(.{}); | |
| + | |
| + const lightmix = b.dependency("lightmix", .{}); | |
| + | |
| + const exe_mod = b.createModule(.{ | |
| + .root_source_file = b.path("src/main.zig"), | |
| + .target = target, | |
| + .optimize = optimize, | |
| + .imports = &.{ | |
| + .{ .name = "lightmix", .module = lightmix.module("lightmix") }, | |
| + }, | |
| + }); | |
| + | |
| + const exe = b.addExecutable(.{ | |
| + .name = "mixing-waves", | |
| + .root_module = exe_mod, | |
| + }); | |
| + b.installArtifact(exe); | |
| + | |
| + const run_cmd = b.addRunArtifact(exe); | |
| + run_cmd.step.dependOn(b.getInstallStep()); | |
| + if (b.args) |args| { | |
| + run_cmd.addArgs(args); | |
| + } | |
| + | |
| + const run_step = b.step("run", "Run the app"); | |
| + run_step.dependOn(&run_cmd.step); | |
| + | |
| + const exe_unit_tests = b.addTest(.{ | |
| + .root_module = exe_mod, | |
| + }); | |
| + const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); | |
| + | |
| + const test_step = b.step("test", "Run unit tests"); | |
| + test_step.dependOn(&run_exe_unit_tests.step); | |
| +} | |
| diff --git a/examples/03-wave-operations/mixing-waves/build.zig.zon b/examples/03-wave-operations/mixing-waves/build.zig.zon | |
| new file mode 100644 | |
| index 0000000..719460c | |
| --- /dev/null | |
| +++ b/examples/03-wave-operations/mixing-waves/build.zig.zon | |
| @@ -0,0 +1,18 @@ | |
| +.{ | |
| + .name = .mixing_waves, | |
| + .version = "0.1.0", | |
| + .fingerprint = 0xccd58c58e459b785, | |
| + .minimum_zig_version = "0.15.2", | |
| + | |
| + .dependencies = .{ | |
| + .lightmix = .{ | |
| + .path = "../../..", | |
| + }, | |
| + }, | |
| + | |
| + .paths = .{ | |
| + "build.zig", | |
| + "build.zig.zon", | |
| + "src", | |
| + }, | |
| +} | |
| diff --git a/examples/03-wave-operations/mixing-waves/src/main.zig b/examples/03-wave-operations/mixing-waves/src/main.zig | |
| new file mode 100644 | |
| index 0000000..74a2892 | |
| --- /dev/null | |
| +++ b/examples/03-wave-operations/mixing-waves/src/main.zig | |
| @@ -0,0 +1,74 @@ | |
| +//! # Mixing Waves - Combining Multiple Audio Sources | |
| +//! | |
| +//! This example demonstrates how to mix (combine) multiple waves together. | |
| +//! We'll create a simple musical chord by mixing three sine waves at different frequencies. | |
| +//! | |
| +//! ## What you'll learn: | |
| +//! - How to use Wave.mix() to combine waves | |
| +//! - Creating musical chords from multiple frequencies | |
| +//! - Understanding additive synthesis | |
| +//! | |
| +//! ## Musical theory: | |
| +//! We'll create a C major chord (C4-E4-G4): | |
| +//! - C4: 261.63 Hz (root) | |
| +//! - E4: 329.63 Hz (major third) | |
| +//! - G4: 392.00 Hz (perfect fifth) | |
| + | |
| +const std = @import("std"); | |
| +const lightmix = @import("lightmix"); | |
| +const Wave = lightmix.Wave; | |
| + | |
| +pub fn main() !void { | |
| + const allocator = std.heap.page_allocator; | |
| + | |
| + // Generate three notes of a C major chord | |
| + const c4 = try generateSineWave(261.63, 0.3, allocator); // C4 - root | |
| + defer c4.deinit(); | |
| + const e4 = try generateSineWave(329.63, 0.3, allocator); // E4 - major third | |
| + defer e4.deinit(); | |
| + const g4 = try generateSineWave(392.00, 0.3, allocator); // G4 - perfect fifth | |
| + defer g4.deinit(); | |
| + | |
| + // Mix the first two notes | |
| + const c_e_mix = try c4.mix(e4, .{}); | |
| + defer c_e_mix.deinit(); | |
| + | |
| + // Mix in the third note to complete the chord | |
| + const chord = try c_e_mix.mix(g4, .{}); | |
| + defer chord.deinit(); | |
| + | |
| + // Save the result | |
| + const file = try std.fs.cwd().createFile("result.wav", .{}); | |
| + defer file.close(); | |
| + const buf = try allocator.alloc(u8, 10 * 1024 * 1024); | |
| + defer allocator.free(buf); | |
| + var writer = file.writer(buf); | |
| + | |
| + try chord.write(.wav, &writer.interface, .{ | |
| + .format_code = .pcm, | |
| + .bits = 16, | |
| + }); | |
| + | |
| + try writer.interface.flush(); | |
| + | |
| + std.debug.print("✓ Created C major chord (C4-E4-G4)\n", .{}); | |
| + std.debug.print(" C4: 261.63 Hz\n", .{}); | |
| + std.debug.print(" E4: 329.63 Hz\n", .{}); | |
| + std.debug.print(" G4: 392.00 Hz\n", .{}); | |
| +} | |
| + | |
| +fn generateSineWave(frequency: f64, volume: f64, allocator: std.mem.Allocator) !Wave(f64) { | |
| + const sample_rate: f64 = 44100.0; | |
| + const radians_per_sec: f64 = frequency * 2.0 * std.math.pi; | |
| + | |
| + var samples: [44100]f64 = undefined; | |
| + for (0..samples.len) |i| { | |
| + const t = @as(f64, @floatFromInt(i)) / sample_rate; | |
| + samples[i] = volume * @sin(radians_per_sec * t); | |
| + } | |
| + | |
| + return try Wave(f64).init(samples[0..], allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| +} | |
| diff --git a/examples/Composer/compose_multipul_soundless/.gitignore b/examples/04-composer/.gitignore | |
| similarity index 100% | |
| rename from examples/Composer/compose_multipul_soundless/.gitignore | |
| rename to examples/04-composer/.gitignore | |
| diff --git a/examples/04-composer/overlapping-sounds/build.zig b/examples/04-composer/overlapping-sounds/build.zig | |
| new file mode 100644 | |
| index 0000000..9bd713d | |
| --- /dev/null | |
| +++ b/examples/04-composer/overlapping-sounds/build.zig | |
| @@ -0,0 +1,40 @@ | |
| +const std = @import("std"); | |
| + | |
| +pub fn build(b: *std.Build) void { | |
| + const target = b.standardTargetOptions(.{}); | |
| + const optimize = b.standardOptimizeOption(.{}); | |
| + | |
| + const lightmix = b.dependency("lightmix", .{}); | |
| + | |
| + const exe_mod = b.createModule(.{ | |
| + .root_source_file = b.path("src/main.zig"), | |
| + .target = target, | |
| + .optimize = optimize, | |
| + .imports = &.{ | |
| + .{ .name = "lightmix", .module = lightmix.module("lightmix") }, | |
| + }, | |
| + }); | |
| + | |
| + const exe = b.addExecutable(.{ | |
| + .name = "overlapping-sounds", | |
| + .root_module = exe_mod, | |
| + }); | |
| + b.installArtifact(exe); | |
| + | |
| + const run_cmd = b.addRunArtifact(exe); | |
| + run_cmd.step.dependOn(b.getInstallStep()); | |
| + if (b.args) |args| { | |
| + run_cmd.addArgs(args); | |
| + } | |
| + | |
| + const run_step = b.step("run", "Run the app"); | |
| + run_step.dependOn(&run_cmd.step); | |
| + | |
| + const exe_unit_tests = b.addTest(.{ | |
| + .root_module = exe_mod, | |
| + }); | |
| + const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); | |
| + | |
| + const test_step = b.step("test", "Run unit tests"); | |
| + test_step.dependOn(&run_exe_unit_tests.step); | |
| +} | |
| diff --git a/examples/04-composer/overlapping-sounds/build.zig.zon b/examples/04-composer/overlapping-sounds/build.zig.zon | |
| new file mode 100644 | |
| index 0000000..3a10065 | |
| --- /dev/null | |
| +++ b/examples/04-composer/overlapping-sounds/build.zig.zon | |
| @@ -0,0 +1,18 @@ | |
| +.{ | |
| + .name = .overlapping_sounds, | |
| + .version = "0.1.0", | |
| + .fingerprint = 0x1c9cd58a6b23253, | |
| + .minimum_zig_version = "0.15.2", | |
| + | |
| + .dependencies = .{ | |
| + .lightmix = .{ | |
| + .path = "../../..", | |
| + }, | |
| + }, | |
| + | |
| + .paths = .{ | |
| + "build.zig", | |
| + "build.zig.zon", | |
| + "src", | |
| + }, | |
| +} | |
| diff --git a/examples/04-composer/overlapping-sounds/src/main.zig b/examples/04-composer/overlapping-sounds/src/main.zig | |
| new file mode 100644 | |
| index 0000000..108fa98 | |
| --- /dev/null | |
| +++ b/examples/04-composer/overlapping-sounds/src/main.zig | |
| @@ -0,0 +1,98 @@ | |
| +//! # Overlapping Sounds - Polyphony with Composer | |
| +//! | |
| +//! This example shows how to use Composer to create overlapping sounds, | |
| +//! simulating polyphonic music where multiple notes play simultaneously. | |
| +//! | |
| +//! ## What you'll learn: | |
| +//! - Creating overlapping audio with Composer | |
| +//! - Understanding polyphony vs monophony | |
| +//! - Building more complex arrangements | |
| + | |
| +const std = @import("std"); | |
| +const lightmix = @import("lightmix"); | |
| +const Wave = lightmix.Wave; | |
| +const Composer = lightmix.Composer; | |
| + | |
| +pub fn main() !void { | |
| + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; | |
| + defer _ = gpa.deinit(); | |
| + const allocator = gpa.allocator(); | |
| + | |
| + var composer = Composer(f64).init(allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + defer composer.deinit(); | |
| + | |
| + // Create a longer sustaining note | |
| + const long_c = try generateLongNote(261.63, allocator); | |
| + defer long_c.deinit(); | |
| + | |
| + // Create shorter melody notes | |
| + const e4 = try generateNote(329.63, allocator); | |
| + defer e4.deinit(); | |
| + const g4 = try generateNote(392.00, allocator); | |
| + defer g4.deinit(); | |
| + | |
| + // Arrange with overlaps: bass note holds while melody plays | |
| + var arrangement: std.array_list.Aligned(Composer(f64).WaveInfo, null) = .empty; | |
| + defer arrangement.deinit(allocator); | |
| + | |
| + try arrangement.append(allocator, .{ .wave = long_c, .start_point = 0 }); // Bass starts at 0 | |
| + try arrangement.append(allocator, .{ .wave = e4, .start_point = 0 }); // Melody starts with bass | |
| + try arrangement.append(allocator, .{ .wave = g4, .start_point = 22050 }); // Second melody note at 0.5s | |
| + | |
| + try composer.appendSlice(arrangement.items); | |
| + | |
| + const result = try composer.finalize(.{}); | |
| + defer result.deinit(); | |
| + | |
| + const file = try std.fs.cwd().createFile("result.wav", .{}); | |
| + defer file.close(); | |
| + const buf = try allocator.alloc(u8, 10 * 1024 * 1024); | |
| + defer allocator.free(buf); | |
| + var writer = file.writer(buf); | |
| + | |
| + try result.write(.wav, &writer.interface, .{ | |
| + .format_code = .pcm, | |
| + .bits = 16, | |
| + }); | |
| + | |
| + try writer.interface.flush(); | |
| + | |
| + std.debug.print("✓ Created overlapping arrangement\n", .{}); | |
| +} | |
| + | |
| +fn generateNote(frequency: f64, allocator: std.mem.Allocator) !Wave(f64) { | |
| + const sample_rate: f64 = 44100.0; | |
| + const radians_per_sec: f64 = frequency * 2.0 * std.math.pi; | |
| + | |
| + var samples: [22050]f64 = undefined; | |
| + for (0..samples.len) |i| { | |
| + const t = @as(f64, @floatFromInt(i)) / sample_rate; | |
| + const envelope = 1.0 - (t / 0.5); | |
| + samples[i] = 0.2 * @sin(radians_per_sec * t) * envelope; | |
| + } | |
| + | |
| + return try Wave(f64).init(samples[0..], allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| +} | |
| + | |
| +fn generateLongNote(frequency: f64, allocator: std.mem.Allocator) !Wave(f64) { | |
| + const sample_rate: f64 = 44100.0; | |
| + const radians_per_sec: f64 = frequency * 2.0 * std.math.pi; | |
| + | |
| + var samples: [66150]f64 = undefined; | |
| + for (0..samples.len) |i| { | |
| + const t = @as(f64, @floatFromInt(i)) / sample_rate; | |
| + const envelope = 1.0 - (t / 1.5); | |
| + samples[i] = 0.15 * @sin(radians_per_sec * t) * envelope; | |
| + } | |
| + | |
| + return try Wave(f64).init(samples[0..], allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| +} | |
| diff --git a/examples/04-composer/simple-sequence/build.zig b/examples/04-composer/simple-sequence/build.zig | |
| new file mode 100644 | |
| index 0000000..2b97d4c | |
| --- /dev/null | |
| +++ b/examples/04-composer/simple-sequence/build.zig | |
| @@ -0,0 +1,40 @@ | |
| +const std = @import("std"); | |
| + | |
| +pub fn build(b: *std.Build) void { | |
| + const target = b.standardTargetOptions(.{}); | |
| + const optimize = b.standardOptimizeOption(.{}); | |
| + | |
| + const lightmix = b.dependency("lightmix", .{}); | |
| + | |
| + const exe_mod = b.createModule(.{ | |
| + .root_source_file = b.path("src/main.zig"), | |
| + .target = target, | |
| + .optimize = optimize, | |
| + .imports = &.{ | |
| + .{ .name = "lightmix", .module = lightmix.module("lightmix") }, | |
| + }, | |
| + }); | |
| + | |
| + const exe = b.addExecutable(.{ | |
| + .name = "simple-sequence", | |
| + .root_module = exe_mod, | |
| + }); | |
| + b.installArtifact(exe); | |
| + | |
| + const run_cmd = b.addRunArtifact(exe); | |
| + run_cmd.step.dependOn(b.getInstallStep()); | |
| + if (b.args) |args| { | |
| + run_cmd.addArgs(args); | |
| + } | |
| + | |
| + const run_step = b.step("run", "Run the app"); | |
| + run_step.dependOn(&run_cmd.step); | |
| + | |
| + const exe_unit_tests = b.addTest(.{ | |
| + .root_module = exe_mod, | |
| + }); | |
| + const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); | |
| + | |
| + const test_step = b.step("test", "Run unit tests"); | |
| + test_step.dependOn(&run_exe_unit_tests.step); | |
| +} | |
| diff --git a/examples/04-composer/simple-sequence/build.zig.zon b/examples/04-composer/simple-sequence/build.zig.zon | |
| new file mode 100644 | |
| index 0000000..0e8c253 | |
| --- /dev/null | |
| +++ b/examples/04-composer/simple-sequence/build.zig.zon | |
| @@ -0,0 +1,18 @@ | |
| +.{ | |
| + .name = .simple_sequence, | |
| + .version = "0.1.0", | |
| + .fingerprint = 0x78a3b5a1fd29c606, | |
| + .minimum_zig_version = "0.15.2", | |
| + | |
| + .dependencies = .{ | |
| + .lightmix = .{ | |
| + .path = "../../..", | |
| + }, | |
| + }, | |
| + | |
| + .paths = .{ | |
| + "build.zig", | |
| + "build.zig.zon", | |
| + "src", | |
| + }, | |
| +} | |
| diff --git a/examples/04-composer/simple-sequence/src/main.zig b/examples/04-composer/simple-sequence/src/main.zig | |
| new file mode 100644 | |
| index 0000000..bb38176 | |
| --- /dev/null | |
| +++ b/examples/04-composer/simple-sequence/src/main.zig | |
| @@ -0,0 +1,84 @@ | |
| +//! # Simple Sequence - Using Composer | |
| +//! | |
| +//! This example introduces the Composer, which allows you to arrange multiple | |
| +//! waves in time. We'll create a simple melody by sequencing sine waves. | |
| +//! | |
| +//! ## What you'll learn: | |
| +//! - How to use the Composer API | |
| +//! - Sequencing waves with start_point | |
| +//! - Creating simple melodies | |
| +//! | |
| +//! ## The melody: | |
| +//! We'll play C-D-E-C in sequence (first 4 notes of "Frère Jacques") | |
| + | |
| +const std = @import("std"); | |
| +const lightmix = @import("lightmix"); | |
| +const Wave = lightmix.Wave; | |
| +const Composer = lightmix.Composer; | |
| + | |
| +pub fn main() !void { | |
| + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; | |
| + defer _ = gpa.deinit(); | |
| + const allocator = gpa.allocator(); | |
| + | |
| + // Create composer | |
| + var composer = Composer(f64).init(allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + defer composer.deinit(); | |
| + | |
| + // Generate notes (quarter notes at 120 BPM = 0.5 seconds each) | |
| + const c4 = try generateNote(261.63, allocator); // C4 | |
| + defer c4.deinit(); | |
| + const d4 = try generateNote(293.66, allocator); // D4 | |
| + defer d4.deinit(); | |
| + const e4 = try generateNote(329.63, allocator); // E4 | |
| + defer e4.deinit(); | |
| + | |
| + // Arrange notes in sequence | |
| + var notes_list: std.array_list.Aligned(Composer(f64).WaveInfo, null) = .empty; | |
| + defer notes_list.deinit(allocator); | |
| + | |
| + try notes_list.append(allocator, .{ .wave = c4, .start_point = 0 }); // 0.0s | |
| + try notes_list.append(allocator, .{ .wave = d4, .start_point = 22050 }); // 0.5s | |
| + try notes_list.append(allocator, .{ .wave = e4, .start_point = 44100 }); // 1.0s | |
| + try notes_list.append(allocator, .{ .wave = c4, .start_point = 66150 }); // 1.5s | |
| + | |
| + try composer.appendSlice(notes_list.items); | |
| + | |
| + const result = try composer.finalize(.{}); | |
| + defer result.deinit(); | |
| + | |
| + const file = try std.fs.cwd().createFile("result.wav", .{}); | |
| + defer file.close(); | |
| + const buf = try allocator.alloc(u8, 10 * 1024 * 1024); | |
| + defer allocator.free(buf); | |
| + var writer = file.writer(buf); | |
| + | |
| + try result.write(.wav, &writer.interface, .{ | |
| + .format_code = .pcm, | |
| + .bits = 16, | |
| + }); | |
| + | |
| + try writer.interface.flush(); | |
| + | |
| + std.debug.print("✓ Created simple melody: C-D-E-C\n", .{}); | |
| +} | |
| + | |
| +fn generateNote(frequency: f64, allocator: std.mem.Allocator) !Wave(f64) { | |
| + const sample_rate: f64 = 44100.0; | |
| + const radians_per_sec: f64 = frequency * 2.0 * std.math.pi; | |
| + | |
| + var samples: [22050]f64 = undefined; | |
| + for (0..samples.len) |i| { | |
| + const t = @as(f64, @floatFromInt(i)) / sample_rate; | |
| + const envelope = 1.0 - (t / 0.5); // Simple decay | |
| + samples[i] = 0.3 * @sin(radians_per_sec * t) * envelope; | |
| + } | |
| + | |
| + return try Wave(f64).init(samples[0..], allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| +} | |
| diff --git a/examples/05-practical-examples/.gitignore b/examples/05-practical-examples/.gitignore | |
| new file mode 100644 | |
| index 0000000..6fddcdb | |
| --- /dev/null | |
| +++ b/examples/05-practical-examples/.gitignore | |
| @@ -0,0 +1,3 @@ | |
| +result.wav | |
| +guitar_a2.wav | |
| +guitar_e2.wav | |
| diff --git a/examples/05-practical-examples/drum/build.zig b/examples/05-practical-examples/drum/build.zig | |
| new file mode 100644 | |
| index 0000000..bf7ed42 | |
| --- /dev/null | |
| +++ b/examples/05-practical-examples/drum/build.zig | |
| @@ -0,0 +1,40 @@ | |
| +const std = @import("std"); | |
| + | |
| +pub fn build(b: *std.Build) void { | |
| + const target = b.standardTargetOptions(.{}); | |
| + const optimize = b.standardOptimizeOption(.{}); | |
| + | |
| + const lightmix = b.dependency("lightmix", .{}); | |
| + | |
| + const exe_mod = b.createModule(.{ | |
| + .root_source_file = b.path("src/main.zig"), | |
| + .target = target, | |
| + .optimize = optimize, | |
| + .imports = &.{ | |
| + .{ .name = "lightmix", .module = lightmix.module("lightmix") }, | |
| + }, | |
| + }); | |
| + | |
| + const exe = b.addExecutable(.{ | |
| + .name = "drum", | |
| + .root_module = exe_mod, | |
| + }); | |
| + b.installArtifact(exe); | |
| + | |
| + const run_cmd = b.addRunArtifact(exe); | |
| + run_cmd.step.dependOn(b.getInstallStep()); | |
| + if (b.args) |args| { | |
| + run_cmd.addArgs(args); | |
| + } | |
| + | |
| + const run_step = b.step("run", "Run the app"); | |
| + run_step.dependOn(&run_cmd.step); | |
| + | |
| + const exe_unit_tests = b.addTest(.{ | |
| + .root_module = exe_mod, | |
| + }); | |
| + const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); | |
| + | |
| + const test_step = b.step("test", "Run unit tests"); | |
| + test_step.dependOn(&run_exe_unit_tests.step); | |
| +} | |
| diff --git a/examples/05-practical-examples/drum/build.zig.zon b/examples/05-practical-examples/drum/build.zig.zon | |
| new file mode 100644 | |
| index 0000000..b84a698 | |
| --- /dev/null | |
| +++ b/examples/05-practical-examples/drum/build.zig.zon | |
| @@ -0,0 +1,18 @@ | |
| +.{ | |
| + .name = .drum, | |
| + .version = "0.1.0", | |
| + .fingerprint = 0xa33e93205174b58f, | |
| + .minimum_zig_version = "0.15.2", | |
| + | |
| + .dependencies = .{ | |
| + .lightmix = .{ | |
| + .path = "../../..", | |
| + }, | |
| + }, | |
| + | |
| + .paths = .{ | |
| + "build.zig", | |
| + "build.zig.zon", | |
| + "src", | |
| + }, | |
| +} | |
| diff --git a/examples/05-practical-examples/drum/src/main.zig b/examples/05-practical-examples/drum/src/main.zig | |
| new file mode 100644 | |
| index 0000000..6f354ea | |
| --- /dev/null | |
| +++ b/examples/05-practical-examples/drum/src/main.zig | |
| @@ -0,0 +1,130 @@ | |
| +//! # Drum Synthesis - Snare Drum | |
| +//! | |
| +//! This example creates a snare drum sound by combining: | |
| +//! - Pink noise (for the "snare" wire rattle) | |
| +//! - Sine wave (for the drum shell resonance) | |
| +//! - Decay envelopes (for the percussive attack) | |
| +//! | |
| +//! ## What you'll learn: | |
| +//! - Combining different sound sources for synthesis | |
| +//! - Using noise for percussion sounds | |
| +//! - Envelope shaping for realistic drums | |
| + | |
| +const std = @import("std"); | |
| +const lightmix = @import("lightmix"); | |
| +const Wave = lightmix.Wave; | |
| + | |
| +pub fn main() !void { | |
| + const allocator = std.heap.page_allocator; | |
| + | |
| + const snare_wave = try generateSnare(allocator); | |
| + defer snare_wave.deinit(); | |
| + | |
| + const file = try std.fs.cwd().createFile("result.wav", .{}); | |
| + defer file.close(); | |
| + const buf = try allocator.alloc(u8, 10 * 1024 * 1024); | |
| + defer allocator.free(buf); | |
| + var writer = file.writer(buf); | |
| + | |
| + try snare_wave.write(.wav, &writer.interface, .{ | |
| + .format_code = .pcm, | |
| + .bits = 16, | |
| + }); | |
| + | |
| + try writer.interface.flush(); | |
| + | |
| + std.debug.print("✓ Generated snare drum sound\n", .{}); | |
| +} | |
| + | |
| +fn generateSnare(allocator: std.mem.Allocator) !Wave(f64) { | |
| + // Generate pink noise for snare wires | |
| + var noise: Wave(f64) = try generatePinkNoise(allocator); | |
| + // Apply aggressive decay to noise | |
| + try noise.filter(fastDecayFilter); | |
| + try noise.filter(fastDecayFilter); | |
| + try noise.filter(fastDecayFilter); | |
| + defer noise.deinit(); | |
| + | |
| + // Generate low sine for drum body | |
| + var tone = try generateDrumTone(allocator); | |
| + // Apply decay to tone | |
| + try tone.filter(fastDecayFilter); | |
| + try tone.filter(halveSampleValuesFilter); | |
| + defer tone.deinit(); | |
| + | |
| + // Mix together | |
| + return noise.mix(tone, .{}); | |
| +} | |
| + | |
| +fn generatePinkNoise(allocator: std.mem.Allocator) !Wave(f64) { | |
| + var prng = std.Random.DefaultPrng.init(0); | |
| + const rand = prng.random(); | |
| + | |
| + var b0: f64 = 0.0; | |
| + var b1: f64 = 0.0; | |
| + var b2: f64 = 0.0; | |
| + | |
| + var samples: [22050]f64 = undefined; // 0.5 seconds | |
| + for (0..samples.len) |i| { | |
| + const white = rand.float(f64) * 2.0 - 1.0; | |
| + b0 = 0.99765 * b0 + white * 0.0990460; | |
| + b1 = 0.96300 * b1 + white * 0.2965164; | |
| + b2 = 0.57000 * b2 + white * 1.0526913; | |
| + samples[i] = (b0 + b1 + b2 + white * 0.1848) * 0.3; | |
| + } | |
| + | |
| + return try Wave(f64).init(samples[0..], allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| +} | |
| + | |
| +fn generateDrumTone(allocator: std.mem.Allocator) !Wave(f64) { | |
| + const frequency: f64 = 200.0; // Low frequency for drum body | |
| + const sample_rate: f64 = 44100.0; | |
| + const radians_per_sec: f64 = frequency * 2.0 * std.math.pi; | |
| + | |
| + var samples: [22050]f64 = undefined; | |
| + for (0..samples.len) |i| { | |
| + const t = @as(f64, @floatFromInt(i)) / sample_rate; | |
| + samples[i] = 0.4 * @sin(radians_per_sec * t); | |
| + } | |
| + | |
| + return try Wave(f64).init(samples[0..], allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| +} | |
| + | |
| +fn fastDecayFilter(comptime T: type, original_wave: Wave(T)) !Wave(T) { | |
| + var result_list: std.array_list.Aligned(T, null) = .empty; | |
| + | |
| + for (original_wave.samples, 0..) |sample, n| { | |
| + const remaining = original_wave.samples.len - n; | |
| + const decay_factor = @as(T, @floatFromInt(remaining)) / | |
| + @as(T, @floatFromInt(original_wave.samples.len)); | |
| + try result_list.append(original_wave.allocator, sample * decay_factor); | |
| + } | |
| + | |
| + return Wave(T){ | |
| + .samples = try result_list.toOwnedSlice(original_wave.allocator), | |
| + .allocator = original_wave.allocator, | |
| + .sample_rate = original_wave.sample_rate, | |
| + .channels = original_wave.channels, | |
| + }; | |
| +} | |
| + | |
| +fn halveSampleValuesFilter(comptime T: type, original_wave: Wave(T)) !Wave(T) { | |
| + var result_list: std.array_list.Aligned(T, null) = .empty; | |
| + | |
| + for (original_wave.samples) |sample| { | |
| + try result_list.append(original_wave.allocator, sample * 0.5); | |
| + } | |
| + | |
| + return Wave(T){ | |
| + .samples = try result_list.toOwnedSlice(original_wave.allocator), | |
| + .allocator = original_wave.allocator, | |
| + .sample_rate = original_wave.sample_rate, | |
| + .channels = original_wave.channels, | |
| + }; | |
| +} | |
| diff --git a/examples/drum/snare_drum/build.zig b/examples/05-practical-examples/guitar/build.zig | |
| similarity index 86% | |
| rename from examples/drum/snare_drum/build.zig | |
| rename to examples/05-practical-examples/guitar/build.zig | |
| index e6fbabc..36ae2bb 100644 | |
| --- a/examples/drum/snare_drum/build.zig | |
| +++ b/examples/05-practical-examples/guitar/build.zig | |
| @@ -4,10 +4,8 @@ pub fn build(b: *std.Build) void { | |
| const target = b.standardTargetOptions(.{}); | |
| const optimize = b.standardOptimizeOption(.{}); | |
| - // Dependencies | |
| const lightmix = b.dependency("lightmix", .{}); | |
| - // Executable Module | |
| const exe_mod = b.createModule(.{ | |
| .root_source_file = b.path("src/main.zig"), | |
| .target = target, | |
| @@ -15,31 +13,26 @@ pub fn build(b: *std.Build) void { | |
| }); | |
| exe_mod.addImport("lightmix", lightmix.module("lightmix")); | |
| - // Executable | |
| const exe = b.addExecutable(.{ | |
| - .name = "snare_drum", | |
| + .name = "guitar", | |
| .root_module = exe_mod, | |
| }); | |
| b.installArtifact(exe); | |
| - // Rum cmd | |
| const run_cmd = b.addRunArtifact(exe); | |
| run_cmd.step.dependOn(b.getInstallStep()); | |
| if (b.args) |args| { | |
| run_cmd.addArgs(args); | |
| } | |
| - // Run step | |
| const run_step = b.step("run", "Run the app"); | |
| run_step.dependOn(&run_cmd.step); | |
| - // Unit tests | |
| const exe_unit_tests = b.addTest(.{ | |
| .root_module = exe_mod, | |
| }); | |
| const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); | |
| - // Test step | |
| const test_step = b.step("test", "Run unit tests"); | |
| test_step.dependOn(&run_exe_unit_tests.step); | |
| } | |
| diff --git a/examples/05-practical-examples/guitar/build.zig.zon b/examples/05-practical-examples/guitar/build.zig.zon | |
| new file mode 100644 | |
| index 0000000..c0d8782 | |
| --- /dev/null | |
| +++ b/examples/05-practical-examples/guitar/build.zig.zon | |
| @@ -0,0 +1,18 @@ | |
| +.{ | |
| + .name = .guitar, | |
| + .version = "0.1.0", | |
| + .fingerprint = 0x423ac30d2a62da7e, | |
| + .minimum_zig_version = "0.15.2", | |
| + | |
| + .dependencies = .{ | |
| + .lightmix = .{ | |
| + .path = "../../..", | |
| + }, | |
| + }, | |
| + | |
| + .paths = .{ | |
| + "build.zig", | |
| + "build.zig.zon", | |
| + "src", | |
| + }, | |
| +} | |
| diff --git a/examples/05-practical-examples/guitar/src/main.zig b/examples/05-practical-examples/guitar/src/main.zig | |
| new file mode 100644 | |
| index 0000000..c24d3d0 | |
| --- /dev/null | |
| +++ b/examples/05-practical-examples/guitar/src/main.zig | |
| @@ -0,0 +1,87 @@ | |
| +//! # Guitar Synthesis - Karplus-Strong Algorithm | |
| +//! | |
| +//! This example demonstrates the Karplus-Strong algorithm for plucked string synthesis. | |
| +//! It simulates a guitar-like sound by using noise excitation and feedback delay. | |
| +//! | |
| +//! ## What you'll learn: | |
| +//! - Physical modeling synthesis | |
| +//! - The Karplus-Strong algorithm | |
| +//! - Creating realistic instrument sounds | |
| +//! | |
| +//! ## How it works: | |
| +//! 1. Start with noise in a delay buffer | |
| +//! 2. Average adjacent samples and apply decay | |
| +//! 3. Feed back into the buffer | |
| +//! This creates a pitched, decaying sound like a plucked string! | |
| + | |
| +const std = @import("std"); | |
| +const lightmix = @import("lightmix"); | |
| +const Wave = lightmix.Wave; | |
| + | |
| +pub fn main() !void { | |
| + const allocator = std.heap.page_allocator; | |
| + | |
| + // Generate guitar notes at different frequencies | |
| + const e2 = try generateGuitarNote(82.41, allocator); // Low E string | |
| + defer e2.deinit(); | |
| + | |
| + const a2 = try generateGuitarNote(110.00, allocator); // A string | |
| + defer a2.deinit(); | |
| + | |
| + try saveWave(e2, "guitar_e2.wav", allocator); | |
| + try saveWave(a2, "guitar_a2.wav", allocator); | |
| + | |
| + std.debug.print("✓ Generated guitar sounds:\n", .{}); | |
| + std.debug.print(" guitar_e2.wav - Low E string (82.41 Hz)\n", .{}); | |
| + std.debug.print(" guitar_a2.wav - A string (110.00 Hz)\n", .{}); | |
| +} | |
| + | |
| +/// Karplus-Strong guitar synthesis | |
| +fn generateGuitarNote(frequency: f64, allocator: std.mem.Allocator) !Wave(f64) { | |
| + const sample_rate: f64 = 44100.0; | |
| + const decay: f64 = 0.996; // Decay factor (close to 1 = longer sustain) | |
| + | |
| + var result: [88200]f64 = undefined; | |
| + | |
| + // Calculate period in samples | |
| + const period = @as(usize, @intFromFloat(sample_rate / frequency)); | |
| + | |
| + // Initialize delay buffer with noise | |
| + var buffer: [2000]f64 = undefined; | |
| + var prng = std.Random.DefaultPrng.init(0); | |
| + const rand = prng.random(); | |
| + | |
| + for (buffer[0..period]) |*val| { | |
| + val.* = rand.float(f64) * 2.0 - 1.0; | |
| + } | |
| + | |
| + // Karplus-Strong loop: average adjacent samples with decay | |
| + var idx: usize = 0; | |
| + for (0..result.len) |i| { | |
| + const next_idx = (idx + 1) % period; | |
| + const avg = (buffer[idx] + buffer[next_idx]) * 0.5 * decay; | |
| + buffer[idx] = avg; | |
| + result[i] = avg * 0.8; // Volume adjustment | |
| + idx = next_idx; | |
| + } | |
| + | |
| + return try Wave(f64).init(result[0..], allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| +} | |
| + | |
| +fn saveWave(wave: Wave(f64), filename: []const u8, allocator: std.mem.Allocator) !void { | |
| + const file = try std.fs.cwd().createFile(filename, .{}); | |
| + defer file.close(); | |
| + const buf = try allocator.alloc(u8, 10 * 1024 * 1024); | |
| + defer allocator.free(buf); | |
| + var writer = file.writer(buf); | |
| + | |
| + try wave.write(.wav, &writer.interface, .{ | |
| + .format_code = .pcm, | |
| + .bits = 16, | |
| + }); | |
| + | |
| + try writer.interface.flush(); | |
| +} | |
| diff --git a/examples/06-advanced/build-time-generation/build.zig b/examples/06-advanced/build-time-generation/build.zig | |
| new file mode 100644 | |
| index 0000000..3b725a8 | |
| --- /dev/null | |
| +++ b/examples/06-advanced/build-time-generation/build.zig | |
| @@ -0,0 +1,38 @@ | |
| +const std = @import("std"); | |
| +const l = @import("lightmix"); | |
| + | |
| +pub fn build(b: *std.Build) !void { | |
| + const target = b.standardTargetOptions(.{}); | |
| + const optimize = b.standardOptimizeOption(.{}); | |
| + | |
| + const lightmix = b.dependency("lightmix", .{}); | |
| + | |
| + const mod = b.createModule(.{ | |
| + .root_source_file = b.path("src/main.zig"), | |
| + .target = target, | |
| + .optimize = optimize, | |
| + .imports = &.{ | |
| + .{ .name = "lightmix", .module = lightmix.module("lightmix") }, | |
| + }, | |
| + }); | |
| + | |
| + // Wave Installation | |
| + const wave = try l.addWave(b, mod, .{ | |
| + // .func_name = "gen", // The default value of func_name is "gen" | |
| + .format = .{ .wav = .{ | |
| + .bits = 16, | |
| + .format_code = .pcm, | |
| + } }, | |
| + }); | |
| + l.installWave(b, wave); | |
| + | |
| + // Unit tests | |
| + const unit_tests = b.addTest(.{ | |
| + .root_module = mod, | |
| + }); | |
| + const run_unit_tests = b.addRunArtifact(unit_tests); | |
| + | |
| + // Test step | |
| + const test_step = b.step("test", "Run unit tests"); | |
| + test_step.dependOn(&run_unit_tests.step); | |
| +} | |
| diff --git a/examples/06-advanced/build-time-generation/build.zig.zon b/examples/06-advanced/build-time-generation/build.zig.zon | |
| new file mode 100644 | |
| index 0000000..40a40aa | |
| --- /dev/null | |
| +++ b/examples/06-advanced/build-time-generation/build.zig.zon | |
| @@ -0,0 +1,18 @@ | |
| +.{ | |
| + .name = .build_time_generation, | |
| + .version = "0.1.0", | |
| + .fingerprint = 0xa66d0dedc977e4d5, | |
| + .minimum_zig_version = "0.15.2", | |
| + | |
| + .dependencies = .{ | |
| + .lightmix = .{ | |
| + .path = "../../..", | |
| + }, | |
| + }, | |
| + | |
| + .paths = .{ | |
| + "build.zig", | |
| + "build.zig.zon", | |
| + "src", | |
| + }, | |
| +} | |
| diff --git a/examples/06-advanced/build-time-generation/src/main.zig b/examples/06-advanced/build-time-generation/src/main.zig | |
| new file mode 100644 | |
| index 0000000..215911e | |
| --- /dev/null | |
| +++ b/examples/06-advanced/build-time-generation/src/main.zig | |
| @@ -0,0 +1,37 @@ | |
| +const std = @import("std"); | |
| +const lightmix = @import("lightmix"); | |
| +const Wave = lightmix.Wave; | |
| + | |
| +pub fn gen(allocator: std.mem.Allocator) !Wave(f64) { | |
| + // Generate a chord using const-compatible patterns | |
| + const c5 = try generateSineWave(523.25, allocator); // C5 | |
| + defer c5.deinit(); | |
| + const e5 = try generateSineWave(659.25, allocator); // E5 | |
| + defer e5.deinit(); | |
| + const g5 = try generateSineWave(783.99, allocator); // G5 | |
| + defer g5.deinit(); | |
| + | |
| + // Mix the three notes | |
| + const ce_mix = try c5.mix(e5, .{}); | |
| + defer ce_mix.deinit(); | |
| + | |
| + const chord = try ce_mix.mix(g5, .{}); | |
| + | |
| + return chord; | |
| +} | |
| + | |
| +fn generateSineWave(frequency: f64, allocator: std.mem.Allocator) !Wave(f64) { | |
| + const sample_rate: f64 = 44100.0; | |
| + const radians_per_sec: f64 = frequency * 2.0 * std.math.pi; | |
| + | |
| + var samples: [44100]f64 = undefined; | |
| + for (0..samples.len) |i| { | |
| + const t = @as(f64, @floatFromInt(i)) / sample_rate; | |
| + samples[i] = 0.25 * @sin(radians_per_sec * t); | |
| + } | |
| + | |
| + return try Wave(f64).init(samples[0..], allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| +} | |
| diff --git a/examples/06-advanced/build-time-play/build.zig b/examples/06-advanced/build-time-play/build.zig | |
| new file mode 100644 | |
| index 0000000..3c5e1f5 | |
| --- /dev/null | |
| +++ b/examples/06-advanced/build-time-play/build.zig | |
| @@ -0,0 +1,47 @@ | |
| +const std = @import("std"); | |
| +const l = @import("lightmix"); | |
| + | |
| +pub fn build(b: *std.Build) !void { | |
| + const target = b.standardTargetOptions(.{}); | |
| + const optimize = b.standardOptimizeOption(.{}); | |
| + | |
| + const lightmix = b.dependency("lightmix", .{}); | |
| + | |
| + const mod = b.createModule(.{ | |
| + .root_source_file = b.path("src/main.zig"), | |
| + .target = target, | |
| + .optimize = optimize, | |
| + .imports = &.{ | |
| + .{ .name = "lightmix", .module = lightmix.module("lightmix") }, | |
| + }, | |
| + }); | |
| + | |
| + if (target.result.os.tag == .linux) { | |
| + mod.linkSystemLibrary("alsa", .{}); | |
| + mod.linkSystemLibrary("libpulse", .{}); | |
| + mod.linkSystemLibrary("libpipewire-0.3", .{}); | |
| + } | |
| + | |
| + const wave = try l.addWave(b, mod, .{ | |
| + // .func_name = "gen", // The default value of func_name is "gen" | |
| + .format = .{ .wav = .{ | |
| + .bits = 16, | |
| + .format_code = .pcm, | |
| + } }, | |
| + }); | |
| + l.installWave(b, wave); | |
| + | |
| + const play = try l.addPlay(b, wave, .{}); | |
| + const play_step = b.step("play", "Play produced Wave file"); | |
| + play_step.dependOn(&play.step); | |
| + | |
| + // Unit tests | |
| + const unit_tests = b.addTest(.{ | |
| + .root_module = mod, | |
| + }); | |
| + const run_unit_tests = b.addRunArtifact(unit_tests); | |
| + | |
| + // Test step | |
| + const test_step = b.step("test", "Run unit tests"); | |
| + test_step.dependOn(&run_unit_tests.step); | |
| +} | |
| diff --git a/examples/06-advanced/build-time-play/build.zig.zon b/examples/06-advanced/build-time-play/build.zig.zon | |
| new file mode 100644 | |
| index 0000000..ec82c9a | |
| --- /dev/null | |
| +++ b/examples/06-advanced/build-time-play/build.zig.zon | |
| @@ -0,0 +1,18 @@ | |
| +.{ | |
| + .name = .build_time_play, | |
| + .version = "0.1.0", | |
| + .fingerprint = 0x4ab620c4c90e0f63, | |
| + .minimum_zig_version = "0.15.2", | |
| + | |
| + .dependencies = .{ | |
| + .lightmix = .{ | |
| + .path = "../../..", | |
| + }, | |
| + }, | |
| + | |
| + .paths = .{ | |
| + "build.zig", | |
| + "build.zig.zon", | |
| + "src", | |
| + }, | |
| +} | |
| diff --git a/examples/06-advanced/build-time-play/src/main.zig b/examples/06-advanced/build-time-play/src/main.zig | |
| new file mode 100644 | |
| index 0000000..215911e | |
| --- /dev/null | |
| +++ b/examples/06-advanced/build-time-play/src/main.zig | |
| @@ -0,0 +1,37 @@ | |
| +const std = @import("std"); | |
| +const lightmix = @import("lightmix"); | |
| +const Wave = lightmix.Wave; | |
| + | |
| +pub fn gen(allocator: std.mem.Allocator) !Wave(f64) { | |
| + // Generate a chord using const-compatible patterns | |
| + const c5 = try generateSineWave(523.25, allocator); // C5 | |
| + defer c5.deinit(); | |
| + const e5 = try generateSineWave(659.25, allocator); // E5 | |
| + defer e5.deinit(); | |
| + const g5 = try generateSineWave(783.99, allocator); // G5 | |
| + defer g5.deinit(); | |
| + | |
| + // Mix the three notes | |
| + const ce_mix = try c5.mix(e5, .{}); | |
| + defer ce_mix.deinit(); | |
| + | |
| + const chord = try ce_mix.mix(g5, .{}); | |
| + | |
| + return chord; | |
| +} | |
| + | |
| +fn generateSineWave(frequency: f64, allocator: std.mem.Allocator) !Wave(f64) { | |
| + const sample_rate: f64 = 44100.0; | |
| + const radians_per_sec: f64 = frequency * 2.0 * std.math.pi; | |
| + | |
| + var samples: [44100]f64 = undefined; | |
| + for (0..samples.len) |i| { | |
| + const t = @as(f64, @floatFromInt(i)) / sample_rate; | |
| + samples[i] = 0.25 * @sin(radians_per_sec * t); | |
| + } | |
| + | |
| + return try Wave(f64).init(samples[0..], allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| +} | |
| diff --git a/examples/06-advanced/modular-composing/README.md b/examples/06-advanced/modular-composing/README.md | |
| new file mode 100644 | |
| index 0000000..b5c2ef9 | |
| --- /dev/null | |
| +++ b/examples/06-advanced/modular-composing/README.md | |
| @@ -0,0 +1,91 @@ | |
| +# Modular Composing Example | |
| + | |
| +This example demonstrates how to generate audio using a modular architecture with lightmix. | |
| + | |
| +## Overview | |
| + | |
| +This project shows how to separate music theory and synthesis concepts into independent packages and combine them to generate audio. | |
| + | |
| +- **temperaments**: Package defining tuning systems | |
| +- **synths**: Package providing audio synthesis engines | |
| + | |
| +By combining these packages, you can build a flexible and maintainable audio generation system. | |
| + | |
| +## Project Structure | |
| + | |
| +``` | |
| +modular-composing/ | |
| +├── build.zig # Build configuration | |
| +├── build.zig.zon # Dependency definitions | |
| +├── src/ | |
| +│ └── root.zig # Main entry point | |
| +└── packages/ | |
| + ├── temperaments/ # Tuning system package | |
| + │ ├── src/ | |
| + │ │ ├── root.zig | |
| + │ │ └── twelve_equal_temperament.zig | |
| + │ ├── build.zig | |
| + │ └── build.zig.zon | |
| + └── synths/ # Synthesizer package | |
| + ├── src/ | |
| + │ ├── synths.zig | |
| + │ └── synths/ | |
| + │ └── sine.zig | |
| + ├── build.zig | |
| + └── build.zig.zon | |
| +``` | |
| + | |
| +## Package Descriptions | |
| + | |
| +### temperaments | |
| + | |
| +Provides tuning systems (scale tuning systems). Currently implements Twelve Equal Temperament. | |
| + | |
| +Key features: | |
| + | |
| +- Convert MIDI numbers to frequencies | |
| +- Manipulate pitches by semitones | |
| +- Manage note names and octaves | |
| + | |
| +### synths | |
| + | |
| +Provides synthesizer engines. Currently implements a sine wave oscillator. | |
| + | |
| +Key features: | |
| + | |
| +- Generate sine waves at specified pitches | |
| +- Customize sample rate and channel count | |
| + | |
| +## Usage | |
| + | |
| +Run the following command in this directory to generate a `result.wav` file: | |
| + | |
| +```bash | |
| +zig build | |
| +``` | |
| + | |
| +The generated file is an audio file containing a sine wave playing C4 (middle C) for 1 second. | |
| + | |
| +## Extension Methods | |
| + | |
| +This example can be extended in the following ways: | |
| + | |
| +1. **Add new tuning systems**: Add new tuning systems to the `temperaments` package (e.g., just intonation, Pythagorean tuning) | |
| +1. **Add new synthesizers**: Add new waveforms to the `synths` package (e.g., square wave, sawtooth wave, triangle wave) | |
| +1. **Create complex music**: Combine multiple notes to create chords and melodies | |
| + | |
| +## Technical Features | |
| + | |
| +- **Modular design**: Each feature is implemented as an independent package | |
| +- **Leverage Zig build system**: Manage package dependencies with `build.zig` | |
| +- **Type safety**: Safe implementation leveraging Zig's powerful type system | |
| +- **Build-time generation**: Generate WAV files at compile time | |
| + | |
| +## Requirements | |
| + | |
| +- Zig 0.15.2 or later | |
| +- lightmix library | |
| + | |
| +## License | |
| + | |
| +This project is part of the lightmix project and is subject to the same license. | |
| diff --git a/examples/06-advanced/modular-composing/build.zig b/examples/06-advanced/modular-composing/build.zig | |
| new file mode 100644 | |
| index 0000000..17fe2bb | |
| --- /dev/null | |
| +++ b/examples/06-advanced/modular-composing/build.zig | |
| @@ -0,0 +1,50 @@ | |
| +const std = @import("std"); | |
| +const l = @import("lightmix"); | |
| + | |
| +pub fn build(b: *std.Build) !void { | |
| + const target = b.standardTargetOptions(.{}); | |
| + const optimize = b.standardOptimizeOption(.{}); | |
| + | |
| + const lightmix = b.dependency("lightmix", .{}); | |
| + const synths = b.dependency("synths", .{}); | |
| + const temperaments = b.dependency("temperaments", .{}); | |
| + | |
| + const mod = b.addModule("modular-composing", .{ | |
| + .root_source_file = b.path("src/root.zig"), | |
| + .target = target, | |
| + .optimize = optimize, | |
| + .imports = &.{ | |
| + .{ .name = "lightmix", .module = lightmix.module("lightmix") }, | |
| + .{ .name = "synths", .module = synths.module("synths") }, | |
| + .{ .name = "temperaments", .module = temperaments.module("temperaments") }, | |
| + }, | |
| + }); | |
| + | |
| + // Library linking on Linux | |
| + if (target.result.os.tag == .linux) { | |
| + mod.linkSystemLibrary("alsa", .{}); | |
| + mod.linkSystemLibrary("libpulse", .{}); | |
| + mod.linkSystemLibrary("libpipewire-0.3", .{}); | |
| + } | |
| + | |
| + const wave = try l.addWave(b, mod, .{ | |
| + .func_name = "gen", | |
| + .format = .{ .wav = .{ | |
| + .bits = 16, | |
| + .format_code = .pcm, | |
| + } }, | |
| + }); | |
| + l.installWave(b, wave); | |
| + | |
| + const play_step = b.step("play", "Play the produced Wave file"); | |
| + const play = try l.addPlay(b, wave, .{}); | |
| + play_step.dependOn(&play.step); | |
| + | |
| + const mod_tests = b.addTest(.{ | |
| + .root_module = mod, | |
| + }); | |
| + const run_mod_tests = b.addRunArtifact(mod_tests); | |
| + | |
| + const test_step = b.step("test", "Run tests"); | |
| + test_step.dependOn(&run_mod_tests.step); | |
| +} | |
| diff --git a/examples/06-advanced/modular-composing/build.zig.zon b/examples/06-advanced/modular-composing/build.zig.zon | |
| new file mode 100644 | |
| index 0000000..35dc993 | |
| --- /dev/null | |
| +++ b/examples/06-advanced/modular-composing/build.zig.zon | |
| @@ -0,0 +1,18 @@ | |
| +.{ | |
| + .name = .modular_composing, | |
| + .version = "0.0.0", | |
| + .fingerprint = 0xf05ee580c12b42d4, | |
| + .minimum_zig_version = "0.15.2", | |
| + | |
| + .dependencies = .{ | |
| + .lightmix = .{ .path = "../../.." }, | |
| + .synths = .{ .path = "packages/synths" }, | |
| + .temperaments = .{ .path = "packages/temperaments" }, | |
| + }, | |
| + | |
| + .paths = .{ | |
| + "build.zig", | |
| + "build.zig.zon", | |
| + "src", | |
| + }, | |
| +} | |
| diff --git a/examples/06-advanced/modular-composing/packages/synths/build.zig b/examples/06-advanced/modular-composing/packages/synths/build.zig | |
| new file mode 100644 | |
| index 0000000..5d041dd | |
| --- /dev/null | |
| +++ b/examples/06-advanced/modular-composing/packages/synths/build.zig | |
| @@ -0,0 +1,35 @@ | |
| +const std = @import("std"); | |
| +const l = @import("lightmix"); | |
| + | |
| +pub fn build(b: *std.Build) !void { | |
| + const target = b.standardTargetOptions(.{}); | |
| + const optimize = b.standardOptimizeOption(.{}); | |
| + | |
| + const lightmix = b.dependency("lightmix", .{}); | |
| + const temperaments = b.dependency("temperaments", .{}); | |
| + | |
| + const mod = b.addModule("synths", .{ | |
| + .root_source_file = b.path("src/synths.zig"), | |
| + .target = target, | |
| + .optimize = optimize, | |
| + .imports = &.{ | |
| + .{ .name = "lightmix", .module = lightmix.module("lightmix") }, | |
| + .{ .name = "temperaments", .module = temperaments.module("temperaments") }, | |
| + }, | |
| + }); | |
| + | |
| + // Install lib as a static library | |
| + const lib = b.addLibrary(.{ | |
| + .name = "synths", | |
| + .root_module = mod, | |
| + }); | |
| + b.installArtifact(lib); | |
| + | |
| + const mod_tests = b.addTest(.{ | |
| + .root_module = mod, | |
| + }); | |
| + const run_mod_tests = b.addRunArtifact(mod_tests); | |
| + | |
| + const test_step = b.step("test", "Run tests"); | |
| + test_step.dependOn(&run_mod_tests.step); | |
| +} | |
| diff --git a/examples/06-advanced/modular-composing/packages/synths/build.zig.zon b/examples/06-advanced/modular-composing/packages/synths/build.zig.zon | |
| new file mode 100644 | |
| index 0000000..262ad24 | |
| --- /dev/null | |
| +++ b/examples/06-advanced/modular-composing/packages/synths/build.zig.zon | |
| @@ -0,0 +1,17 @@ | |
| +.{ | |
| + .name = .synths, | |
| + .version = "0.0.0", | |
| + .fingerprint = 0x249fdde76d58985d, | |
| + .minimum_zig_version = "0.15.2", | |
| + | |
| + .dependencies = .{ | |
| + .lightmix = .{ .path = "../../../../.." }, | |
| + .temperaments = .{ .path = "../temperaments" }, | |
| + }, | |
| + | |
| + .paths = .{ | |
| + "build.zig", | |
| + "build.zig.zon", | |
| + "src", | |
| + }, | |
| +} | |
| diff --git a/examples/06-advanced/modular-composing/packages/synths/src/synths.zig b/examples/06-advanced/modular-composing/packages/synths/src/synths.zig | |
| new file mode 100644 | |
| index 0000000..7e40dfd | |
| --- /dev/null | |
| +++ b/examples/06-advanced/modular-composing/packages/synths/src/synths.zig | |
| @@ -0,0 +1,13 @@ | |
| +//! Synths Package | |
| +//! | |
| +//! This package provides audio synthesis engines (synthesizers). | |
| +//! Each synthesizer generates audio waveforms at specified pitches and durations. | |
| +//! | |
| +//! Currently, only a sine wave oscillator is provided, but other waveforms | |
| +//! such as square wave, sawtooth wave, and triangle wave can be added in the future. | |
| + | |
| +/// Sine Wave Synthesizer | |
| +/// | |
| +/// Generates pure sine waves at specified pitches. | |
| +/// A sine wave is the most basic waveform and is a pure tone without harmonics. | |
| +pub const Sine = @import("./synths/sine.zig"); | |
| diff --git a/examples/06-advanced/modular-composing/packages/synths/src/synths/sine.zig b/examples/06-advanced/modular-composing/packages/synths/src/synths/sine.zig | |
| new file mode 100644 | |
| index 0000000..e910ae8 | |
| --- /dev/null | |
| +++ b/examples/06-advanced/modular-composing/packages/synths/src/synths/sine.zig | |
| @@ -0,0 +1,37 @@ | |
| +const std = @import("std"); | |
| +const lightmix = @import("lightmix"); | |
| +const temperaments = @import("temperaments"); | |
| + | |
| +const Wave = lightmix.Wave; | |
| +const Scale = temperaments.TwelveEqualTemperament; | |
| + | |
| +pub fn gen( | |
| + allocator: std.mem.Allocator, | |
| + length: usize, | |
| + sample_rate: u32, | |
| + channels: u16, | |
| + scale: Scale, | |
| +) !Wave(f64) { | |
| + // Allocate sample data for the specified length | |
| + var samples = try allocator.alloc(f64, length); | |
| + | |
| + // Calculate sine wave values at each sample point | |
| + for (0..samples.len) |i| { | |
| + // Calculate time t (in seconds) | |
| + // t = sample index / sampling rate | |
| + const t = @as(f64, @floatFromInt(i)) / @as(f64, @floatFromInt(sample_rate)); | |
| + | |
| + // Calculate sine wave: sin(2πft) | |
| + // scale.gen() gets the frequency f | |
| + // 2πft is the phase in radians | |
| + samples[i] = @sin(t * scale.gen() * 2.0 * std.math.pi); | |
| + } | |
| + | |
| + // Initialize and return the Wave object | |
| + return Wave(f64){ | |
| + .samples = samples, | |
| + .allocator = allocator, | |
| + .sample_rate = sample_rate, | |
| + .channels = channels, | |
| + }; | |
| +} | |
| diff --git a/examples/06-advanced/modular-composing/packages/temperaments/build.zig b/examples/06-advanced/modular-composing/packages/temperaments/build.zig | |
| new file mode 100644 | |
| index 0000000..53692ab | |
| --- /dev/null | |
| +++ b/examples/06-advanced/modular-composing/packages/temperaments/build.zig | |
| @@ -0,0 +1,29 @@ | |
| +const std = @import("std"); | |
| + | |
| +pub fn build(b: *std.Build) void { | |
| + const target = b.standardTargetOptions(.{}); | |
| + const optimize = b.standardOptimizeOption(.{}); | |
| + | |
| + // Declare a temperaments module | |
| + const mod = b.addModule("temperaments", .{ | |
| + .root_source_file = b.path("src/root.zig"), | |
| + .target = target, | |
| + .optimize = optimize, | |
| + }); | |
| + | |
| + // Install lib as a static library | |
| + const lib = b.addLibrary(.{ | |
| + .name = "temperaments", | |
| + .root_module = mod, | |
| + }); | |
| + b.installArtifact(lib); | |
| + | |
| + // Tests | |
| + const mod_tests = b.addTest(.{ | |
| + .root_module = mod, | |
| + }); | |
| + const run_mod_tests = b.addRunArtifact(mod_tests); | |
| + | |
| + const test_step = b.step("test", "Run tests"); | |
| + test_step.dependOn(&run_mod_tests.step); | |
| +} | |
| diff --git a/examples/06-advanced/modular-composing/packages/temperaments/build.zig.zon b/examples/06-advanced/modular-composing/packages/temperaments/build.zig.zon | |
| new file mode 100644 | |
| index 0000000..f7f305a | |
| --- /dev/null | |
| +++ b/examples/06-advanced/modular-composing/packages/temperaments/build.zig.zon | |
| @@ -0,0 +1,14 @@ | |
| +.{ | |
| + .name = .temperaments, | |
| + .version = "0.0.0", | |
| + .fingerprint = 0x24b8773a7d72da8c, | |
| + .minimum_zig_version = "0.15.2", | |
| + | |
| + .dependencies = .{}, | |
| + | |
| + .paths = .{ | |
| + "build.zig", | |
| + "build.zig.zon", | |
| + "src", | |
| + }, | |
| +} | |
| diff --git a/examples/06-advanced/modular-composing/packages/temperaments/src/root.zig b/examples/06-advanced/modular-composing/packages/temperaments/src/root.zig | |
| new file mode 100644 | |
| index 0000000..6cd3522 | |
| --- /dev/null | |
| +++ b/examples/06-advanced/modular-composing/packages/temperaments/src/root.zig | |
| @@ -0,0 +1,14 @@ | |
| +//! Temperaments Package | |
| +//! | |
| +//! This package provides tuning systems (scale tuning systems). | |
| +//! A tuning system is a system that defines the relative frequency relationships | |
| +//! of pitches used in music. | |
| +//! | |
| +//! Currently, only Twelve Equal Temperament is provided, but other tuning systems | |
| +//! such as just intonation and Pythagorean tuning can be added in the future. | |
| + | |
| +/// Twelve Equal Temperament | |
| +/// | |
| +/// A tuning system that divides one octave into 12 equal semitones. | |
| +/// This is the most commonly used tuning system in Western music. | |
| +pub const TwelveEqualTemperament = @import("./twelve_equal_temperament.zig"); | |
| diff --git a/examples/06-advanced/modular-composing/packages/temperaments/src/twelve_equal_temperament.zig b/examples/06-advanced/modular-composing/packages/temperaments/src/twelve_equal_temperament.zig | |
| new file mode 100644 | |
| index 0000000..821916f | |
| --- /dev/null | |
| +++ b/examples/06-advanced/modular-composing/packages/temperaments/src/twelve_equal_temperament.zig | |
| @@ -0,0 +1,120 @@ | |
| +//! Twelve Equal Temperament | |
| +//! | |
| +//! This module implements twelve equal temperament. | |
| +//! Twelve equal temperament is a tuning system that divides one octave into 12 equal semitones, | |
| +//! with each semitone having a frequency ratio of 2^(1/12). This allows the same pitch relationships | |
| +//! to be maintained in any key (allowing free transposition). | |
| +//! | |
| +//! ## Usage Example | |
| +//! ```zig | |
| +//! const scale = TwelveEqualTemperament{ .code = .c, .octave = 4 }; | |
| +//! const freq = scale.gen(); // Get the frequency of C4 (approximately 261.63 Hz) | |
| +//! | |
| +//! const d4 = scale.add(2); // Move to D4 (2 semitones up) | |
| +//! ``` | |
| + | |
| +const std = @import("std"); | |
| +const testing = std.testing; | |
| + | |
| +const Self = @This(); | |
| + | |
| +/// Note code (C, C#, D, etc.) | |
| +code: Code, | |
| + | |
| +/// Octave number (4 for C4) | |
| +octave: usize, | |
| + | |
| +/// Move the pitch by the specified number of semitones | |
| +/// | |
| +/// ## Arguments | |
| +/// - `self`: Current pitch | |
| +/// - `semitones`: Number of semitones to move (positive for up, negative for down) | |
| +/// | |
| +/// ## Returns | |
| +/// A new `Self` instance representing the moved pitch | |
| +/// | |
| +/// ## Example | |
| +/// ```zig | |
| +/// const c4 = TwelveEqualTemperament{ .code = .c, .octave = 4 }; | |
| +/// const e4 = c4.add(4); // E4, 4 semitones up from C4 | |
| +/// const a3 = c4.add(-3); // A3, 3 semitones down from C4 | |
| +/// ``` | |
| +pub fn add(self: Self, semitones: isize) Self { | |
| + // Convert current pitch to MIDI number (C-1 = 0, A4 = 69) | |
| + const self_midi_number: isize = @intCast(12 * (self.octave + 1) + @intFromEnum(self.code)); | |
| + | |
| + // Add semitones | |
| + const result_midi_number: isize = self_midi_number + semitones; | |
| + | |
| + // Decompose MIDI number into note code and octave | |
| + // Remainder of division by 12 is the note, quotient gives the octave | |
| + const result_code: Code = @enumFromInt(@as(u8, @intCast(@mod(result_midi_number, 12)))); | |
| + const result_octave: usize = @intCast(@divTrunc(result_midi_number, 12) - 1); | |
| + | |
| + return Self{ | |
| + .code = result_code, | |
| + .octave = result_octave, | |
| + }; | |
| +} | |
| + | |
| +/// Calculate frequency (Hz) from pitch | |
| +/// | |
| +/// Uses the frequency calculation formula for twelve equal temperament: | |
| +/// f = 440 * 2^((midi_number-69)/12) | |
| +/// where midi_number is the MIDI number, and 440Hz is the reference frequency for A4 (MIDI number 69). | |
| +/// | |
| +/// ## Arguments | |
| +/// - `scale`: Pitch to calculate frequency for | |
| +/// | |
| +/// ## Returns | |
| +/// Frequency of the pitch (Hz) | |
| +/// | |
| +/// ## Example | |
| +/// ```zig | |
| +/// const a4 = TwelveEqualTemperament{ .code = .a, .octave = 4 }; | |
| +/// const freq = a4.gen(); // 440.0 Hz | |
| +/// ``` | |
| +pub fn gen(scale: Self) f32 { | |
| + // Convert pitch to MIDI number | |
| + const midi_number: isize = @intCast(12 * (scale.octave + 1) + @intFromEnum(scale.code)); | |
| + | |
| + // Calculate semitone difference from A4 (MIDI number 69, 440Hz) | |
| + const exp: f32 = @floatFromInt(midi_number - 69); | |
| + | |
| + // Twelve equal temperament formula: f = 440 * 2^((midi_number-69)/12) | |
| + const result: f32 = 440.0 * std.math.pow(f32, 2.0, exp / 12.0); | |
| + return result; | |
| +} | |
| + | |
| +/// Note code | |
| +/// | |
| +/// Codes with `~s` represent sharp (#). | |
| +/// Example: `cs` is C# (C sharp) | |
| +/// | |
| +/// ## Note names and MIDI number mapping | |
| +/// - c (0): C | |
| +/// - cs (1): C♯ | |
| +/// - d (2): D | |
| +/// - ds (3): D♯ | |
| +/// - e (4): E | |
| +/// - f (5): F | |
| +/// - fs (6): F♯ | |
| +/// - g (7): G | |
| +/// - gs (8): G♯ | |
| +/// - a (9): A | |
| +/// - as (10): A♯ | |
| +/// - b (11): B | |
| +pub const Code = enum(u8) { | |
| + c = 0, | |
| + cs = 1, | |
| + d = 2, | |
| + ds = 3, | |
| + e = 4, | |
| + f = 5, | |
| + fs = 6, | |
| + g = 7, | |
| + gs = 8, | |
| + a = 9, | |
| + as = 10, | |
| + b = 11, | |
| +}; | |
| diff --git a/examples/06-advanced/modular-composing/src/root.zig b/examples/06-advanced/modular-composing/src/root.zig | |
| new file mode 100644 | |
| index 0000000..b2d3b92 | |
| --- /dev/null | |
| +++ b/examples/06-advanced/modular-composing/src/root.zig | |
| @@ -0,0 +1,9 @@ | |
| +const std = @import("std"); | |
| +const lightmix = @import("lightmix"); | |
| +const synths = @import("synths"); | |
| + | |
| +const Wave = lightmix.Wave; | |
| + | |
| +pub fn gen(allocator: std.mem.Allocator) !Wave(f64) { | |
| + return synths.Sine.gen(allocator, 44100, 44100, 1, .{ .code = .c, .octave = 4 }); | |
| +} | |
| diff --git a/examples/Wave/generate_double_frequency_wave/build.zig b/examples/06-advanced/runtime-play/build.zig | |
| similarity index 65% | |
| rename from examples/Wave/generate_double_frequency_wave/build.zig | |
| rename to examples/06-advanced/runtime-play/build.zig | |
| index 8eccc1c..d6b2b49 100644 | |
| --- a/examples/Wave/generate_double_frequency_wave/build.zig | |
| +++ b/examples/06-advanced/runtime-play/build.zig | |
| @@ -1,45 +1,47 @@ | |
| const std = @import("std"); | |
| +const l = @import("lightmix"); | |
| -pub fn build(b: *std.Build) void { | |
| +pub fn build(b: *std.Build) !void { | |
| const target = b.standardTargetOptions(.{}); | |
| const optimize = b.standardOptimizeOption(.{}); | |
| - // Dependencies | |
| const lightmix = b.dependency("lightmix", .{}); | |
| - // Executable Module | |
| const exe_mod = b.createModule(.{ | |
| .root_source_file = b.path("src/main.zig"), | |
| .target = target, | |
| .optimize = optimize, | |
| + .imports = &.{ | |
| + .{ .name = "lightmix", .module = lightmix.module("lightmix") }, | |
| + }, | |
| }); | |
| - exe_mod.addImport("lightmix", lightmix.module("lightmix")); | |
| - // Executable | |
| + if (target.result.os.tag == .linux) { | |
| + exe_mod.linkSystemLibrary("alsa", .{}); | |
| + exe_mod.linkSystemLibrary("libpulse", .{}); | |
| + exe_mod.linkSystemLibrary("libpipewire-0.3", .{}); | |
| + } | |
| + | |
| const exe = b.addExecutable(.{ | |
| - .name = "generate_double_frequency_wave", | |
| + .name = "noise", | |
| .root_module = exe_mod, | |
| }); | |
| b.installArtifact(exe); | |
| - // Rum cmd | |
| const run_cmd = b.addRunArtifact(exe); | |
| run_cmd.step.dependOn(b.getInstallStep()); | |
| if (b.args) |args| { | |
| run_cmd.addArgs(args); | |
| } | |
| - // Run step | |
| const run_step = b.step("run", "Run the app"); | |
| run_step.dependOn(&run_cmd.step); | |
| - // Unit tests | |
| const exe_unit_tests = b.addTest(.{ | |
| .root_module = exe_mod, | |
| }); | |
| const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); | |
| - // Test step | |
| - const test_step = b.step("test", "Run unit tests"); | |
| + const test_step = b.step("test", "Run tests"); | |
| test_step.dependOn(&run_exe_unit_tests.step); | |
| } | |
| diff --git a/examples/Composer/compose_multipul_soundless/build.zig.zon b/examples/06-advanced/runtime-play/build.zig.zon | |
| similarity index 54% | |
| rename from examples/Composer/compose_multipul_soundless/build.zig.zon | |
| rename to examples/06-advanced/runtime-play/build.zig.zon | |
| index df58b43..fdc9c03 100644 | |
| --- a/examples/Composer/compose_multipul_soundless/build.zig.zon | |
| +++ b/examples/06-advanced/runtime-play/build.zig.zon | |
| @@ -1,8 +1,8 @@ | |
| .{ | |
| - .name = .compose_multipul_soundless, | |
| + .name = .runtime_play, | |
| .version = "0.0.0", | |
| - .fingerprint = 0xbd23ff1b97235887, | |
| - .minimum_zig_version = "0.15.1", | |
| + .fingerprint = 0xe6c2d04f8c10927a, // Changing this has security and trust implications. | |
| + .minimum_zig_version = "0.15.2", | |
| .dependencies = .{ | |
| .lightmix = .{ .path = "../../.." }, | |
| diff --git a/examples/06-advanced/runtime-play/src/main.zig b/examples/06-advanced/runtime-play/src/main.zig | |
| new file mode 100644 | |
| index 0000000..1901ba7 | |
| --- /dev/null | |
| +++ b/examples/06-advanced/runtime-play/src/main.zig | |
| @@ -0,0 +1,27 @@ | |
| +const std = @import("std"); | |
| +const lightmix = @import("lightmix"); | |
| +const Wave = lightmix.Wave; | |
| + | |
| +pub fn main() !void { | |
| + const allocator = std.heap.page_allocator; | |
| + | |
| + // Generate a 440Hz sine wave (A4 note) | |
| + const frequency: f128 = 440.0; | |
| + const sample_rate: f128 = 44100.0; | |
| + const radians_per_sec: f128 = frequency * 2.0 * std.math.pi; | |
| + const volume: f128 = 0.5; | |
| + | |
| + var samples: [44100]f128 = undefined; | |
| + for (0..samples.len) |i| { | |
| + const t = @as(f128, @floatFromInt(i)) / sample_rate; | |
| + // Sine wave formula: amplitude * sin(2π * frequency * time) | |
| + samples[i] = volume * @sin(radians_per_sec * t); | |
| + } | |
| + | |
| + const wave = try Wave(f128).init(samples[0..], allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + | |
| + try wave.play(); | |
| +} | |
| diff --git a/examples/Composer/compose_multipul_sinewave/build.zig.zon b/examples/Composer/compose_multipul_sinewave/build.zig.zon | |
| deleted file mode 100644 | |
| index df58b43..0000000 | |
| --- a/examples/Composer/compose_multipul_sinewave/build.zig.zon | |
| +++ /dev/null | |
| @@ -1,16 +0,0 @@ | |
| -.{ | |
| - .name = .compose_multipul_soundless, | |
| - .version = "0.0.0", | |
| - .fingerprint = 0xbd23ff1b97235887, | |
| - .minimum_zig_version = "0.15.1", | |
| - | |
| - .dependencies = .{ | |
| - .lightmix = .{ .path = "../../.." }, | |
| - }, | |
| - | |
| - .paths = .{ | |
| - "build.zig", | |
| - "build.zig.zon", | |
| - "src", | |
| - }, | |
| -} | |
| diff --git a/examples/Composer/compose_multipul_sinewave/src/main.zig b/examples/Composer/compose_multipul_sinewave/src/main.zig | |
| deleted file mode 100644 | |
| index 04218fb..0000000 | |
| --- a/examples/Composer/compose_multipul_sinewave/src/main.zig | |
| +++ /dev/null | |
| @@ -1,80 +0,0 @@ | |
| -const std = @import("std"); | |
| -const lightmix = @import("lightmix"); | |
| -const Wave = lightmix.Wave; | |
| -const Composer = lightmix.Composer; | |
| -const WaveInfo = Composer.WaveInfo; | |
| - | |
| -var gpa = std.heap.GeneralPurposeAllocator(.{}){}; | |
| -const allocator = gpa.allocator(); | |
| - | |
| -pub fn main() !void { | |
| - defer _ = gpa.detectLeaks(); | |
| - | |
| - const composer = Composer.init(allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }); | |
| - defer composer.deinit(); | |
| - | |
| - const data: [44100]f32 = generate_sinewave_data(); | |
| - const wave = Wave.init(data[0..], allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }); | |
| - | |
| - const decayed_wave: Wave = wave.filter(decay).filter(decay).filter(decay); | |
| - defer decayed_wave.deinit(); | |
| - | |
| - var append_list: std.array_list.Aligned(WaveInfo, null) = .empty; | |
| - defer append_list.deinit(allocator); | |
| - try append_list.append(allocator, .{ .wave = decayed_wave, .start_point = 0 }); | |
| - try append_list.append(allocator, .{ .wave = decayed_wave, .start_point = 44100 }); | |
| - | |
| - const appended_composer = composer.appendSlice(append_list.items); | |
| - defer appended_composer.deinit(); | |
| - | |
| - const result: Wave = appended_composer.finalize(); | |
| - defer result.deinit(); | |
| - | |
| - var file = try std.fs.cwd().createFile("result.wav", .{}); | |
| - defer file.close(); | |
| - | |
| - try result.write(file); | |
| -} | |
| - | |
| -fn generate_sinewave_data() [44100]f32 { | |
| - const sample_rate: f32 = 44100.0; | |
| - const radins_per_sec: f32 = 440.0 * 2.0 * std.math.pi; | |
| - | |
| - var result: [44100]f32 = undefined; | |
| - var i: usize = 0; | |
| - | |
| - while (i < result.len) : (i += 1) { | |
| - result[i] = 0.5 * std.math.sin(@as(f32, @floatFromInt(i)) * radins_per_sec / sample_rate); | |
| - } | |
| - | |
| - return result; | |
| -} | |
| - | |
| -fn decay(original_wave: Wave) !Wave { | |
| - var result: std.array_list.Aligned(f32, null) = .empty; | |
| - | |
| - for (original_wave.data, 0..) |data, n| { | |
| - const i = original_wave.data.len - n; | |
| - const volume: f32 = @as(f32, @floatFromInt(i)) * (1.0 / @as(f32, @floatFromInt(original_wave.data.len))); | |
| - | |
| - const new_data = data * volume; | |
| - try result.append(allocator, new_data); | |
| - } | |
| - | |
| - return Wave{ | |
| - .data = try result.toOwnedSlice(original_wave.allocator), | |
| - .allocator = original_wave.allocator, | |
| - | |
| - .sample_rate = original_wave.sample_rate, | |
| - .channels = original_wave.channels, | |
| - .bits = original_wave.bits, | |
| - }; | |
| -} | |
| diff --git a/examples/Composer/compose_multipul_soundless/build.zig b/examples/Composer/compose_multipul_soundless/build.zig | |
| deleted file mode 100644 | |
| index 735a448..0000000 | |
| --- a/examples/Composer/compose_multipul_soundless/build.zig | |
| +++ /dev/null | |
| @@ -1,45 +0,0 @@ | |
| -const std = @import("std"); | |
| - | |
| -pub fn build(b: *std.Build) void { | |
| - const target = b.standardTargetOptions(.{}); | |
| - const optimize = b.standardOptimizeOption(.{}); | |
| - | |
| - // Dependencies | |
| - const lightmix = b.dependency("lightmix", .{}); | |
| - | |
| - // Executable Module | |
| - const exe_mod = b.createModule(.{ | |
| - .root_source_file = b.path("src/main.zig"), | |
| - .target = target, | |
| - .optimize = optimize, | |
| - }); | |
| - exe_mod.addImport("lightmix", lightmix.module("lightmix")); | |
| - | |
| - // Executable | |
| - const exe = b.addExecutable(.{ | |
| - .name = "compose_multipul_soundless", | |
| - .root_module = exe_mod, | |
| - }); | |
| - b.installArtifact(exe); | |
| - | |
| - // Rum cmd | |
| - const run_cmd = b.addRunArtifact(exe); | |
| - run_cmd.step.dependOn(b.getInstallStep()); | |
| - if (b.args) |args| { | |
| - run_cmd.addArgs(args); | |
| - } | |
| - | |
| - // Run step | |
| - const run_step = b.step("run", "Run the app"); | |
| - run_step.dependOn(&run_cmd.step); | |
| - | |
| - // Unit tests | |
| - const exe_unit_tests = b.addTest(.{ | |
| - .root_module = exe_mod, | |
| - }); | |
| - const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); | |
| - | |
| - // Test step | |
| - const test_step = b.step("test", "Run unit tests"); | |
| - test_step.dependOn(&run_exe_unit_tests.step); | |
| -} | |
| diff --git a/examples/Composer/compose_multipul_soundless/src/main.zig b/examples/Composer/compose_multipul_soundless/src/main.zig | |
| deleted file mode 100644 | |
| index 2bee249..0000000 | |
| --- a/examples/Composer/compose_multipul_soundless/src/main.zig | |
| +++ /dev/null | |
| @@ -1,53 +0,0 @@ | |
| -const std = @import("std"); | |
| -const lightmix = @import("lightmix"); | |
| -const Wave = lightmix.Wave; | |
| -const Composer = lightmix.Composer; | |
| -const WaveInfo = Composer.WaveInfo; | |
| - | |
| -pub fn main() !void { | |
| - const allocator = std.heap.page_allocator; | |
| - const composer: Composer = Composer.init(allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }); | |
| - defer composer.deinit(); | |
| - | |
| - const data: []const f32 = generate_soundless_data(44100, allocator); | |
| - defer allocator.free(data); | |
| - | |
| - const wave = Wave.init(data, allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }); | |
| - defer wave.deinit(); | |
| - | |
| - var append_list: std.array_list.Aligned(WaveInfo, null) = .empty; | |
| - defer append_list.deinit(allocator); | |
| - try append_list.append(allocator, .{ .wave = wave, .start_point = 0 }); | |
| - try append_list.append(allocator, .{ .wave = wave, .start_point = 0 }); | |
| - | |
| - const appended_composer = composer.appendSlice(append_list.items); | |
| - defer appended_composer.deinit(); | |
| - | |
| - const result: Wave = appended_composer.finalize(); | |
| - defer result.deinit(); | |
| - | |
| - var file = try std.fs.cwd().createFile("result.wav", .{}); | |
| - defer file.close(); | |
| - | |
| - try result.write(file); | |
| -} | |
| - | |
| -fn generate_soundless_data(length: usize, allocator: std.mem.Allocator) []const f32 { | |
| - var list: std.array_list.Aligned(f32, null) = .empty; | |
| - | |
| - // Append empty wave | |
| - for (0..length) |_| | |
| - list.append(allocator, 0.0) catch @panic("Out of memory"); | |
| - | |
| - const result: []const f32 = list.toOwnedSlice(allocator) catch @panic("Out of memory"); | |
| - | |
| - return result; | |
| -} | |
| diff --git a/examples/QUICKSTART.md b/examples/QUICKSTART.md | |
| new file mode 100644 | |
| index 0000000..2b08834 | |
| --- /dev/null | |
| +++ b/examples/QUICKSTART.md | |
| @@ -0,0 +1,100 @@ | |
| +# Examples Directory - Quick Start Guide | |
| + | |
| +## 🚀 Quick Start | |
| + | |
| +**Complete beginners? Start here:** | |
| + | |
| +```bash | |
| +cd examples/01-getting-started/hello-wave | |
| +zig build run | |
| +# Play the generated result.wav file | |
| +``` | |
| + | |
| +## 📚 Learning Path | |
| + | |
| +Follow this recommended order: | |
| + | |
| +1. **01-getting-started/** - Your first sounds (2 examples) | |
| + | |
| + - Start with `hello-wave` - creates a simple sine wave | |
| + - Then try `using-filters` - learn audio transformations | |
| + | |
| +1. **02-wave-basics/** - Understanding different sounds (5 examples) | |
| + | |
| + - Try all five: sine, square, sawtooth, triangle, noise | |
| + - Listen to how each one sounds different! | |
| + | |
| +1. **03-wave-operations/** - Combining sounds (3 examples) | |
| + | |
| + - `mixing-waves` - create a musical chord | |
| + - `filtering` - chain effects | |
| + - `frequency-changes` - change pitch | |
| + | |
| +1. **04-composer/** - Making music (2 examples) | |
| + | |
| + - `simple-sequence` - create a melody | |
| + - `overlapping-sounds` - layer sounds | |
| + | |
| +1. **05-practical-examples/** - Real instruments (2 examples) | |
| + | |
| + - `guitar` - realistic string sound | |
| + - `drum` - percussion synthesis | |
| + | |
| +1. **06-advanced/** - Advanced techniques (4 examples) | |
| + | |
| + - `build-time-generation` - compile-time audio | |
| + - `build-time-play` | |
| + - `modular-composing` - organize complex projects | |
| + - `runtime-play` | |
| + | |
| +## 🎵 Example Categories | |
| + | |
| +``` | |
| +📦 examples/ | |
| +│ | |
| +├── 🌱 01-getting-started - Absolute beginner friendly | |
| +├── 🎼 02-wave-basics - Basic waveforms & sounds | |
| +├── 🔧 03-wave-operations - Transform & combine audio | |
| +├── 🎹 04-composer - Sequence & layer sounds | |
| +├── 🎸 05-practical-examples - Real synthesis techniques | |
| +└── 🚀 06-advanced - Advanced features | |
| + | |
| +📚 Legacy examples (Wave/, Composer/, etc.) preserved for reference | |
| +``` | |
| + | |
| +## ⚠️ Important | |
| + | |
| +- **CHECK YOUR VOLUME** before playing generated audio files! | |
| +- All examples create `result.wav` in their directory | |
| +- Format: 16-bit mono PCM @ 44.1kHz | |
| +- Each example focuses on ONE concept | |
| + | |
| +## 🎧 Playing Audio | |
| + | |
| +After running an example: | |
| + | |
| +- **Linux:** `aplay result.wav` | |
| +- **macOS:** `afplay result.wav` | |
| +- **Windows:** `start result.wav` | |
| +- **All:** VLC, Audacity, or any audio player | |
| + | |
| +## 📖 Full Documentation | |
| + | |
| +See `examples/README.md` for complete details. | |
| + | |
| +## 🐛 Issues? | |
| + | |
| +If an example doesn't work: | |
| + | |
| +1. Check you're in the example's directory | |
| +1. Try `zig build clean` then `zig build run` | |
| +1. Make sure you have a compatible Zig version | |
| + | |
| +## 🎓 What Each Example Teaches | |
| + | |
| +Every example includes: | |
| + | |
| +- ✅ Detailed comments explaining the code | |
| +- ✅ Top-level documentation describing the concept | |
| +- ✅ Clear, readable variable names | |
| +- ✅ Complete, runnable code | |
| diff --git a/examples/README.md b/examples/README.md | |
| index 65413e2..b1bc0e1 100644 | |
| --- a/examples/README.md | |
| +++ b/examples/README.md | |
| @@ -1,12 +1,169 @@ | |
| -# Examples | |
| +# lightmix Examples | |
| -- Composer | |
| -- Wave | |
| -- drum | |
| -- guitar | |
| +Welcome to the lightmix examples! This directory contains a comprehensive set of examples organized by difficulty and concept, designed to help you learn audio programming with lightmix step by step. | |
| -## How to run examples | |
| +## 📚 Example Categories | |
| -Go to the example's directory you want to check. Then, run this command, `zig build run`. You will see the `result.wav` file in current directory. | |
| +### 01-getting-started/ | |
| -**Please check the volume!! The wave files' volume are not checked!!** | |
| +**Perfect for first-time users** | |
| + | |
| +Start here if you're new to lightmix! These examples introduce the basics with clear, simple code. | |
| + | |
| +- **hello-wave** - Create your first sine wave and save it to a WAV file | |
| +- **using-filters** - Learn how to apply filters to transform audio | |
| + | |
| +**What you'll learn:** Basic Wave creation, saving audio files, applying simple filters | |
| + | |
| +### 02-wave-basics/ | |
| + | |
| +**Understanding fundamental waveforms** | |
| + | |
| +Learn about the building blocks of sound synthesis. Each example generates a different basic waveform. | |
| + | |
| +- **sine-wave** - Pure tone with no harmonics (smooth, clean sound) | |
| +- **square-wave** - Rich in odd harmonics (buzzy, 8-bit sound) | |
| +- **sawtooth-wave** - Bright with all harmonics (brass, string-like) | |
| +- **triangle-wave** - Mellow with odd harmonics (softer than square) | |
| +- **noise** - White, pink, and brown noise generation | |
| + | |
| +**What you'll learn:** Wave generation algorithms, harmonics, audio characteristics of different waveforms | |
| + | |
| +### 03-wave-operations/ | |
| + | |
| +**Manipulating and combining waves** | |
| + | |
| +Once you can generate waves, learn how to transform and combine them. | |
| + | |
| +- **mixing-waves** - Combine multiple waves to create chords and complex sounds | |
| +- **filtering** - Chain multiple filters for audio effects | |
| +- **frequency-changes** - Change pitch by altering frequency | |
| + | |
| +**What you'll learn:** Wave mixing, filter chaining, additive synthesis, pitch relationships | |
| + | |
| +### 04-composer/ | |
| + | |
| +**Arranging sounds in time** | |
| + | |
| +The Composer lets you sequence and layer multiple audio sources. | |
| + | |
| +- **simple-sequence** - Create a simple melody by sequencing notes | |
| +- **overlapping-sounds** - Layer sounds on top of each other (polyphony) | |
| + | |
| +**What you'll learn:** Using the Composer API, timing control, creating musical arrangements | |
| + | |
| +### 05-practical-examples/ | |
| + | |
| +**Real-world instrument synthesis** | |
| + | |
| +See how to combine techniques to create realistic instrument sounds. | |
| + | |
| +- **guitar** - Karplus-Strong plucked string synthesis | |
| +- **drum** - Snare drum using noise + tone synthesis | |
| + | |
| +**What you'll learn:** Physical modeling, combining noise and tones, percussion synthesis | |
| + | |
| +### 06-advanced/ | |
| + | |
| +**Advanced techniques** | |
| + | |
| +Ready for more? These examples show advanced lightmix features. | |
| + | |
| +- **build-time-generation** - Generate audio files at compile-time | |
| +- **build-time-play** | |
| +- **runtime-play** | |
| +- **modular-composing** - Organize complex audio projects with modules | |
| + | |
| +**What you'll learn:** Build system integration, code organization, reusable components | |
| + | |
| +## 🚀 How to Run Examples | |
| + | |
| +Each example is a complete Zig project with its own `build.zig`. To run any example: | |
| + | |
| +```bash | |
| +cd examples/01-getting-started/hello-wave | |
| +zig build run | |
| +``` | |
| + | |
| +This will create a `result.wav` file in the example directory. | |
| + | |
| +### Build-Time Generation | |
| + | |
| +The `06-advanced/build-time-generation` example is special - it generates audio during the build: | |
| + | |
| +```bash | |
| +cd examples/06-advanced/build-time-generation | |
| +zig build # Note: just 'build', not 'build run' | |
| +``` | |
| + | |
| +## ⚠️ Important Notes | |
| + | |
| +### Volume Warning | |
| + | |
| +**Please check your volume before playing the generated WAV files!** The examples do not normalize or limit audio levels. Start with low volume and adjust as needed. | |
| + | |
| +### Sample Rate | |
| + | |
| +Most examples use 44100 Hz (CD quality), which is standard for audio files. | |
| + | |
| +### File Formats | |
| + | |
| +Examples typically output 16-bit PCM WAV files (`.i16`), which is widely compatible. Some examples demonstrate other formats like `.f32`. | |
| + | |
| +## 📖 Learning Path | |
| + | |
| +We recommend following this order: | |
| + | |
| +1. **Start with 01-getting-started/** - Get comfortable with the basics | |
| +1. **Explore 02-wave-basics/** - Understand different waveforms and their sounds | |
| +1. **Try 03-wave-operations/** - Learn to combine and transform waves | |
| +1. **Move to 04-composer/** - Start creating musical sequences | |
| +1. **Study 05-practical-examples/** - See real synthesis techniques in action | |
| +1. **Experiment with 06-advanced/** - Explore advanced features | |
| + | |
| +## 🎵 Understanding the Code | |
| + | |
| +Each example includes: | |
| + | |
| +- **Detailed comments** explaining what the code does | |
| +- **Top-level documentation** (`//!`) describing the concept | |
| +- **Clear variable names** to make the code readable | |
| +- **Complete, runnable code** - no dependencies on other examples | |
| + | |
| +## 🔊 Listening to Results | |
| + | |
| +After generating WAV files, you can play them with: | |
| + | |
| +- **Linux:** `aplay result.wav` or `ffplay result.wav` | |
| +- **macOS:** `afplay result.wav` | |
| +- **Windows:** `start result.wav` or use Windows Media Player | |
| +- **All platforms:** VLC, Audacity, or any audio player | |
| + | |
| +## 🐛 Troubleshooting | |
| + | |
| +**"Out of memory" errors:** | |
| + | |
| +- The examples use `page_allocator` for simplicity | |
| +- For production code, consider using `GeneralPurposeAllocator` or an arena | |
| + | |
| +**"File not found" errors:** | |
| + | |
| +- Make sure you're in the example's directory when running `zig build run` | |
| +- The `result.wav` is created in the current working directory | |
| + | |
| +**Build errors:** | |
| + | |
| +- Ensure you're using a compatible Zig version (check the main README.md) | |
| +- Try `zig build clean` and then `zig build run` again | |
| + | |
| +## 📚 Further Learning | |
| + | |
| +After working through these examples, check out: | |
| + | |
| +- The main lightmix documentation in `src/root.zig` | |
| +- API documentation for Wave and Composer modules | |
| +- The test files in `tests/` for more usage patterns | |
| + | |
| +## 🤝 Contributing | |
| + | |
| +Found a bug in an example? Have an idea for a new example? Contributions are welcome! Please open an issue or pull request on the GitHub repository. | |
| diff --git a/examples/Wave/generate_brown_noise/.gitignore b/examples/Wave/generate_brown_noise/.gitignore | |
| deleted file mode 100644 | |
| index 374ca18..0000000 | |
| --- a/examples/Wave/generate_brown_noise/.gitignore | |
| +++ /dev/null | |
| @@ -1 +0,0 @@ | |
| -result.wav | |
| diff --git a/examples/Wave/generate_brown_noise/build.zig b/examples/Wave/generate_brown_noise/build.zig | |
| deleted file mode 100644 | |
| index bea5676..0000000 | |
| --- a/examples/Wave/generate_brown_noise/build.zig | |
| +++ /dev/null | |
| @@ -1,45 +0,0 @@ | |
| -const std = @import("std"); | |
| - | |
| -pub fn build(b: *std.Build) void { | |
| - const target = b.standardTargetOptions(.{}); | |
| - const optimize = b.standardOptimizeOption(.{}); | |
| - | |
| - // Dependencies | |
| - const lightmix = b.dependency("lightmix", .{}); | |
| - | |
| - // Executable Module | |
| - const exe_mod = b.createModule(.{ | |
| - .root_source_file = b.path("src/main.zig"), | |
| - .target = target, | |
| - .optimize = optimize, | |
| - }); | |
| - exe_mod.addImport("lightmix", lightmix.module("lightmix")); | |
| - | |
| - // Executable | |
| - const exe = b.addExecutable(.{ | |
| - .name = "generate_brown_noise", | |
| - .root_module = exe_mod, | |
| - }); | |
| - b.installArtifact(exe); | |
| - | |
| - // Rum cmd | |
| - const run_cmd = b.addRunArtifact(exe); | |
| - run_cmd.step.dependOn(b.getInstallStep()); | |
| - if (b.args) |args| { | |
| - run_cmd.addArgs(args); | |
| - } | |
| - | |
| - // Run step | |
| - const run_step = b.step("run", "Run the app"); | |
| - run_step.dependOn(&run_cmd.step); | |
| - | |
| - // Unit tests | |
| - const exe_unit_tests = b.addTest(.{ | |
| - .root_module = exe_mod, | |
| - }); | |
| - const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); | |
| - | |
| - // Test step | |
| - const test_step = b.step("test", "Run unit tests"); | |
| - test_step.dependOn(&run_exe_unit_tests.step); | |
| -} | |
| diff --git a/examples/Wave/generate_brown_noise/build.zig.zon b/examples/Wave/generate_brown_noise/build.zig.zon | |
| deleted file mode 100644 | |
| index f5a2738..0000000 | |
| --- a/examples/Wave/generate_brown_noise/build.zig.zon | |
| +++ /dev/null | |
| @@ -1,14 +0,0 @@ | |
| -.{ | |
| - .name = .generate_brown_noise, | |
| - .version = "0.0.0", | |
| - .fingerprint = 0x225c63cf08b1f45a, | |
| - .minimum_zig_version = "0.15.1", | |
| - | |
| - .dependencies = .{ .lightmix = .{ .path = "../../.." } }, | |
| - | |
| - .paths = .{ | |
| - "build.zig", | |
| - "build.zig.zon", | |
| - "src", | |
| - }, | |
| -} | |
| diff --git a/examples/Wave/generate_brown_noise/src/main.zig b/examples/Wave/generate_brown_noise/src/main.zig | |
| deleted file mode 100644 | |
| index 4ce1484..0000000 | |
| --- a/examples/Wave/generate_brown_noise/src/main.zig | |
| +++ /dev/null | |
| @@ -1,66 +0,0 @@ | |
| -const std = @import("std"); | |
| -const lightmix = @import("lightmix"); | |
| -const Wave = lightmix.Wave; | |
| -const allocator = std.heap.page_allocator; | |
| - | |
| -pub fn main() !void { | |
| - const data: [44100]f32 = generate_brown_noise(); | |
| - const brown_noise: Wave = Wave.init(data[0..], allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }); | |
| - | |
| - const decayed_brown_noise: Wave = brown_noise.filter(decay).filter(decay).filter(decay); | |
| - defer decayed_brown_noise.deinit(); | |
| - | |
| - var file = try std.fs.cwd().createFile("result.wav", .{}); | |
| - defer file.close(); | |
| - | |
| - try decayed_brown_noise.write(file); | |
| -} | |
| - | |
| -fn generate_brown_noise() [44100]f32 { | |
| - var result: [44100]f32 = undefined; | |
| - | |
| - var prng = std.Random.DefaultPrng.init(0); | |
| - const rand = prng.random(); | |
| - | |
| - var last: f32 = 0.0; | |
| - var i: usize = 0; | |
| - while (i < result.len) : (i += 1) { | |
| - const white: f32 = rand.float(f32) * 2.0 - 1.0; | |
| - last += white; | |
| - | |
| - if (last > 1.0) | |
| - last = 1.0; | |
| - | |
| - if (last < -1.0) | |
| - last = -1.0; | |
| - | |
| - result[i] = last; | |
| - } | |
| - | |
| - return result; | |
| -} | |
| - | |
| -fn decay(original_wave: Wave) !Wave { | |
| - var result: std.array_list.Aligned(f32, null) = .empty; | |
| - | |
| - for (original_wave.data, 0..) |data, n| { | |
| - const i = original_wave.data.len - n; | |
| - const volume: f32 = @as(f32, @floatFromInt(i)) * (1.0 / @as(f32, @floatFromInt(original_wave.data.len))); | |
| - | |
| - const new_data = data * volume; | |
| - try result.append(original_wave.allocator, new_data); | |
| - } | |
| - | |
| - return Wave{ | |
| - .data = try result.toOwnedSlice(original_wave.allocator), | |
| - .allocator = original_wave.allocator, | |
| - | |
| - .sample_rate = original_wave.sample_rate, | |
| - .channels = original_wave.channels, | |
| - .bits = original_wave.bits, | |
| - }; | |
| -} | |
| diff --git a/examples/Wave/generate_double_frequency_wave/.gitignore b/examples/Wave/generate_double_frequency_wave/.gitignore | |
| deleted file mode 100644 | |
| index 374ca18..0000000 | |
| --- a/examples/Wave/generate_double_frequency_wave/.gitignore | |
| +++ /dev/null | |
| @@ -1 +0,0 @@ | |
| -result.wav | |
| diff --git a/examples/Wave/generate_double_frequency_wave/build.zig.zon b/examples/Wave/generate_double_frequency_wave/build.zig.zon | |
| deleted file mode 100644 | |
| index cc7a6a5..0000000 | |
| --- a/examples/Wave/generate_double_frequency_wave/build.zig.zon | |
| +++ /dev/null | |
| @@ -1,14 +0,0 @@ | |
| -.{ | |
| - .name = .generate_double_frequency_wave, | |
| - .version = "0.0.0", | |
| - .fingerprint = 0xe103efd538644454, | |
| - .minimum_zig_version = "0.15.1", | |
| - | |
| - .dependencies = .{ .lightmix = .{ .path = "../../.." } }, | |
| - | |
| - .paths = .{ | |
| - "build.zig", | |
| - "build.zig.zon", | |
| - "src", | |
| - }, | |
| -} | |
| diff --git a/examples/Wave/generate_double_frequency_wave/src/main.zig b/examples/Wave/generate_double_frequency_wave/src/main.zig | |
| deleted file mode 100644 | |
| index cbbd083..0000000 | |
| --- a/examples/Wave/generate_double_frequency_wave/src/main.zig | |
| +++ /dev/null | |
| @@ -1,76 +0,0 @@ | |
| -const std = @import("std"); | |
| -const lightmix = @import("lightmix"); | |
| -const Wave = lightmix.Wave; | |
| -const allocator = std.heap.page_allocator; | |
| - | |
| -pub fn main() !void { | |
| - const data: [44100]f32 = generate_sinewave_data(); | |
| - const sinewave: Wave = Wave.init(data[0..], allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }); | |
| - | |
| - const double_freq_wave: Wave = sinewave.filter(to_double_freq).filter(decay); | |
| - defer double_freq_wave.deinit(); | |
| - | |
| - var file = try std.fs.cwd().createFile("result.wav", .{}); | |
| - defer file.close(); | |
| - | |
| - try double_freq_wave.write(file); | |
| -} | |
| - | |
| -fn decay(original_wave: Wave) !Wave { | |
| - var result: std.array_list.Aligned(f32, null) = .empty; | |
| - | |
| - for (original_wave.data, 0..) |data, n| { | |
| - const i = original_wave.data.len - n; | |
| - const volume: f32 = @as(f32, @floatFromInt(i)) * (1.0 / @as(f32, @floatFromInt(original_wave.data.len))); | |
| - | |
| - const new_data = data * volume; | |
| - try result.append(original_wave.allocator, new_data); | |
| - } | |
| - | |
| - return Wave{ | |
| - .data = try result.toOwnedSlice(original_wave.allocator), | |
| - .allocator = original_wave.allocator, | |
| - | |
| - .sample_rate = original_wave.sample_rate, | |
| - .channels = original_wave.channels, | |
| - .bits = original_wave.bits, | |
| - }; | |
| -} | |
| - | |
| -fn to_double_freq(original_wave: Wave) !Wave { | |
| - var result: std.array_list.Aligned(f32, null) = .empty; | |
| - | |
| - for (original_wave.data, 0..) |data, i| { | |
| - if (i % 2 == 0) | |
| - try result.append(original_wave.allocator, data); | |
| - } | |
| - | |
| - std.debug.print("{d}\n", .{result.items.len}); | |
| - | |
| - return Wave{ | |
| - .data = try result.toOwnedSlice(original_wave.allocator), | |
| - .allocator = original_wave.allocator, | |
| - | |
| - .sample_rate = original_wave.sample_rate, | |
| - .channels = original_wave.channels, | |
| - .bits = original_wave.bits, | |
| - }; | |
| -} | |
| - | |
| -fn generate_sinewave_data() [44100]f32 { | |
| - const sample_rate: f32 = 44100.0; | |
| - const radins_per_sec: f32 = 440.0 * 4.0 * std.math.pi; | |
| - | |
| - var result: [44100]f32 = undefined; | |
| - var i: usize = 0; | |
| - | |
| - while (i < result.len) : (i += 1) { | |
| - result[i] = 0.5 * std.math.sin(@as(f32, @floatFromInt(i)) * radins_per_sec / sample_rate); | |
| - } | |
| - | |
| - return result; | |
| -} | |
| diff --git a/examples/Wave/generate_function/.gitignore b/examples/Wave/generate_function/.gitignore | |
| deleted file mode 100644 | |
| index 374ca18..0000000 | |
| --- a/examples/Wave/generate_function/.gitignore | |
| +++ /dev/null | |
| @@ -1 +0,0 @@ | |
| -result.wav | |
| diff --git a/examples/Wave/generate_function/build.zig b/examples/Wave/generate_function/build.zig | |
| deleted file mode 100644 | |
| index 3553b8f..0000000 | |
| --- a/examples/Wave/generate_function/build.zig | |
| +++ /dev/null | |
| @@ -1,45 +0,0 @@ | |
| -const std = @import("std"); | |
| - | |
| -pub fn build(b: *std.Build) void { | |
| - const target = b.standardTargetOptions(.{}); | |
| - const optimize = b.standardOptimizeOption(.{}); | |
| - | |
| - // Dependencies | |
| - const lightmix = b.dependency("lightmix", .{}); | |
| - | |
| - // Executable Module | |
| - const exe_mod = b.createModule(.{ | |
| - .root_source_file = b.path("src/main.zig"), | |
| - .target = target, | |
| - .optimize = optimize, | |
| - }); | |
| - exe_mod.addImport("lightmix", lightmix.module("lightmix")); | |
| - | |
| - // Executable | |
| - const exe = b.addExecutable(.{ | |
| - .name = "generate_function", | |
| - .root_module = exe_mod, | |
| - }); | |
| - b.installArtifact(exe); | |
| - | |
| - // Rum cmd | |
| - const run_cmd = b.addRunArtifact(exe); | |
| - run_cmd.step.dependOn(b.getInstallStep()); | |
| - if (b.args) |args| { | |
| - run_cmd.addArgs(args); | |
| - } | |
| - | |
| - // Run step | |
| - const run_step = b.step("run", "Run the app"); | |
| - run_step.dependOn(&run_cmd.step); | |
| - | |
| - // Unit tests | |
| - const exe_unit_tests = b.addTest(.{ | |
| - .root_module = exe_mod, | |
| - }); | |
| - const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); | |
| - | |
| - // Test step | |
| - const test_step = b.step("test", "Run unit tests"); | |
| - test_step.dependOn(&run_exe_unit_tests.step); | |
| -} | |
| diff --git a/examples/Wave/generate_function/build.zig.zon b/examples/Wave/generate_function/build.zig.zon | |
| deleted file mode 100644 | |
| index 3341b6d..0000000 | |
| --- a/examples/Wave/generate_function/build.zig.zon | |
| +++ /dev/null | |
| @@ -1,14 +0,0 @@ | |
| -.{ | |
| - .name = .generate_function, | |
| - .version = "0.0.0", | |
| - .fingerprint = 0x6cbe76a58f61502c, | |
| - .minimum_zig_version = "0.15.1", | |
| - | |
| - .dependencies = .{ .lightmix = .{ .path = "../../.." } }, | |
| - | |
| - .paths = .{ | |
| - "build.zig", | |
| - "build.zig.zon", | |
| - "src", | |
| - }, | |
| -} | |
| diff --git a/examples/Wave/generate_function/src/main.zig b/examples/Wave/generate_function/src/main.zig | |
| deleted file mode 100644 | |
| index 0bce3d9..0000000 | |
| --- a/examples/Wave/generate_function/src/main.zig | |
| +++ /dev/null | |
| @@ -1,34 +0,0 @@ | |
| -const std = @import("std"); | |
| -const lightmix = @import("lightmix"); | |
| -const Wave = lightmix.Wave; | |
| -const allocator = std.heap.page_allocator; | |
| - | |
| -pub fn main() !void { | |
| - const sinewave: Wave = Wave.from_file_content(@embedFile("./assets/sine.wav"), allocator); | |
| - | |
| - const overtone_wave: Wave = sinewave.filter(generate_function).filter(generate_function).filter(generate_function); | |
| - defer overtone_wave.deinit(); | |
| - | |
| - var file = try std.fs.cwd().createFile("result.wav", .{}); | |
| - defer file.close(); | |
| - | |
| - try overtone_wave.write(file); | |
| -} | |
| - | |
| -fn generate_function(original_wave: Wave) !Wave { | |
| - var result: std.array_list.Aligned(f32, null) = .empty; | |
| - | |
| - for (original_wave.data) |data| { | |
| - const new_data = data * 2; | |
| - try result.append(original_wave.allocator, new_data); | |
| - } | |
| - | |
| - return Wave{ | |
| - .data = try result.toOwnedSlice(original_wave.allocator), | |
| - .allocator = original_wave.allocator, | |
| - | |
| - .sample_rate = original_wave.sample_rate, | |
| - .channels = original_wave.channels, | |
| - .bits = original_wave.bits, | |
| - }; | |
| -} | |
| diff --git a/examples/Wave/generate_half_frequency_wave/.gitignore b/examples/Wave/generate_half_frequency_wave/.gitignore | |
| deleted file mode 100644 | |
| index 374ca18..0000000 | |
| --- a/examples/Wave/generate_half_frequency_wave/.gitignore | |
| +++ /dev/null | |
| @@ -1 +0,0 @@ | |
| -result.wav | |
| diff --git a/examples/Wave/generate_half_frequency_wave/build.zig b/examples/Wave/generate_half_frequency_wave/build.zig | |
| deleted file mode 100644 | |
| index 44a4341..0000000 | |
| --- a/examples/Wave/generate_half_frequency_wave/build.zig | |
| +++ /dev/null | |
| @@ -1,45 +0,0 @@ | |
| -const std = @import("std"); | |
| - | |
| -pub fn build(b: *std.Build) void { | |
| - const target = b.standardTargetOptions(.{}); | |
| - const optimize = b.standardOptimizeOption(.{}); | |
| - | |
| - // Dependencies | |
| - const lightmix = b.dependency("lightmix", .{}); | |
| - | |
| - // Executable Module | |
| - const exe_mod = b.createModule(.{ | |
| - .root_source_file = b.path("src/main.zig"), | |
| - .target = target, | |
| - .optimize = optimize, | |
| - }); | |
| - exe_mod.addImport("lightmix", lightmix.module("lightmix")); | |
| - | |
| - // Executable | |
| - const exe = b.addExecutable(.{ | |
| - .name = "generate_half_frequency_wave", | |
| - .root_module = exe_mod, | |
| - }); | |
| - b.installArtifact(exe); | |
| - | |
| - // Rum cmd | |
| - const run_cmd = b.addRunArtifact(exe); | |
| - run_cmd.step.dependOn(b.getInstallStep()); | |
| - if (b.args) |args| { | |
| - run_cmd.addArgs(args); | |
| - } | |
| - | |
| - // Run step | |
| - const run_step = b.step("run", "Run the app"); | |
| - run_step.dependOn(&run_cmd.step); | |
| - | |
| - // Unit tests | |
| - const exe_unit_tests = b.addTest(.{ | |
| - .root_module = exe_mod, | |
| - }); | |
| - const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); | |
| - | |
| - // Test step | |
| - const test_step = b.step("test", "Run unit tests"); | |
| - test_step.dependOn(&run_exe_unit_tests.step); | |
| -} | |
| diff --git a/examples/Wave/generate_half_frequency_wave/build.zig.zon b/examples/Wave/generate_half_frequency_wave/build.zig.zon | |
| deleted file mode 100644 | |
| index baa3851..0000000 | |
| --- a/examples/Wave/generate_half_frequency_wave/build.zig.zon | |
| +++ /dev/null | |
| @@ -1,14 +0,0 @@ | |
| -.{ | |
| - .name = .generate_half_frequency_wave, | |
| - .version = "0.0.0", | |
| - .fingerprint = 0xa13ef7d4c5c0e072, | |
| - .minimum_zig_version = "0.15.1", | |
| - | |
| - .dependencies = .{ .lightmix = .{ .path = "../../.." } }, | |
| - | |
| - .paths = .{ | |
| - "build.zig", | |
| - "build.zig.zon", | |
| - "src", | |
| - }, | |
| -} | |
| diff --git a/examples/Wave/generate_half_frequency_wave/src/main.zig b/examples/Wave/generate_half_frequency_wave/src/main.zig | |
| deleted file mode 100644 | |
| index 1bb465c..0000000 | |
| --- a/examples/Wave/generate_half_frequency_wave/src/main.zig | |
| +++ /dev/null | |
| @@ -1,53 +0,0 @@ | |
| -const std = @import("std"); | |
| -const lightmix = @import("lightmix"); | |
| -const Wave = lightmix.Wave; | |
| -const allocator = std.heap.page_allocator; | |
| - | |
| -pub fn main() !void { | |
| - const data: [44100]f32 = generate_sinewave_data(); | |
| - const sinewave: Wave = Wave.init(data[0..], allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }); | |
| - | |
| - const half_freq_wave: Wave = sinewave.filter(to_half_freq); | |
| - defer half_freq_wave.deinit(); | |
| - | |
| - var file = try std.fs.cwd().createFile("result.wav", .{}); | |
| - defer file.close(); | |
| - | |
| - try half_freq_wave.write(file); | |
| -} | |
| - | |
| -fn to_half_freq(original_wave: Wave) !Wave { | |
| - var result: std.array_list.Aligned(f32, null) = .empty; | |
| - | |
| - for (original_wave.data) |data| { | |
| - try result.append(original_wave.allocator, data); | |
| - try result.append(original_wave.allocator, data); | |
| - } | |
| - | |
| - return Wave{ | |
| - .data = try result.toOwnedSlice(original_wave.allocator), | |
| - .allocator = original_wave.allocator, | |
| - | |
| - .sample_rate = original_wave.sample_rate, | |
| - .channels = original_wave.channels, | |
| - .bits = original_wave.bits, | |
| - }; | |
| -} | |
| - | |
| -fn generate_sinewave_data() [44100]f32 { | |
| - const sample_rate: f32 = 44100.0; | |
| - const radins_per_sec: f32 = 440.0 * 4.0 * std.math.pi; | |
| - | |
| - var result: [44100]f32 = undefined; | |
| - var i: usize = 0; | |
| - | |
| - while (i < result.len) : (i += 1) { | |
| - result[i] = 0.5 * std.math.sin(@as(f32, @floatFromInt(i)) * radins_per_sec / sample_rate); | |
| - } | |
| - | |
| - return result; | |
| -} | |
| diff --git a/examples/Wave/generate_mix_wave/.gitignore b/examples/Wave/generate_mix_wave/.gitignore | |
| deleted file mode 100644 | |
| index 374ca18..0000000 | |
| --- a/examples/Wave/generate_mix_wave/.gitignore | |
| +++ /dev/null | |
| @@ -1 +0,0 @@ | |
| -result.wav | |
| diff --git a/examples/Wave/generate_mix_wave/build.zig b/examples/Wave/generate_mix_wave/build.zig | |
| deleted file mode 100644 | |
| index dfebd8e..0000000 | |
| --- a/examples/Wave/generate_mix_wave/build.zig | |
| +++ /dev/null | |
| @@ -1,45 +0,0 @@ | |
| -const std = @import("std"); | |
| - | |
| -pub fn build(b: *std.Build) void { | |
| - const target = b.standardTargetOptions(.{}); | |
| - const optimize = b.standardOptimizeOption(.{}); | |
| - | |
| - // Dependencies | |
| - const lightmix = b.dependency("lightmix", .{}); | |
| - | |
| - // Executable Module | |
| - const exe_mod = b.createModule(.{ | |
| - .root_source_file = b.path("src/main.zig"), | |
| - .target = target, | |
| - .optimize = optimize, | |
| - }); | |
| - exe_mod.addImport("lightmix", lightmix.module("lightmix")); | |
| - | |
| - // Executable | |
| - const exe = b.addExecutable(.{ | |
| - .name = "generate_mix_wave", | |
| - .root_module = exe_mod, | |
| - }); | |
| - b.installArtifact(exe); | |
| - | |
| - // Rum cmd | |
| - const run_cmd = b.addRunArtifact(exe); | |
| - run_cmd.step.dependOn(b.getInstallStep()); | |
| - if (b.args) |args| { | |
| - run_cmd.addArgs(args); | |
| - } | |
| - | |
| - // Run step | |
| - const run_step = b.step("run", "Run the app"); | |
| - run_step.dependOn(&run_cmd.step); | |
| - | |
| - // Unit tests | |
| - const exe_unit_tests = b.addTest(.{ | |
| - .root_module = exe_mod, | |
| - }); | |
| - const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); | |
| - | |
| - // Test step | |
| - const test_step = b.step("test", "Run unit tests"); | |
| - test_step.dependOn(&run_exe_unit_tests.step); | |
| -} | |
| diff --git a/examples/Wave/generate_mix_wave/build.zig.zon b/examples/Wave/generate_mix_wave/build.zig.zon | |
| deleted file mode 100644 | |
| index b5f9fbd..0000000 | |
| --- a/examples/Wave/generate_mix_wave/build.zig.zon | |
| +++ /dev/null | |
| @@ -1,14 +0,0 @@ | |
| -.{ | |
| - .name = .generate_mix_wave, | |
| - .version = "0.0.0", | |
| - .fingerprint = 0x3080a68e671504f0, | |
| - .minimum_zig_version = "0.15.1", | |
| - | |
| - .dependencies = .{ .lightmix = .{ .path = "../../.." } }, | |
| - | |
| - .paths = .{ | |
| - "build.zig", | |
| - "build.zig.zon", | |
| - "src", | |
| - }, | |
| -} | |
| diff --git a/examples/Wave/generate_mix_wave/src/main.zig b/examples/Wave/generate_mix_wave/src/main.zig | |
| deleted file mode 100644 | |
| index abbd034..0000000 | |
| --- a/examples/Wave/generate_mix_wave/src/main.zig | |
| +++ /dev/null | |
| @@ -1,86 +0,0 @@ | |
| -const std = @import("std"); | |
| -const lightmix = @import("lightmix"); | |
| -const Wave = lightmix.Wave; | |
| -const allocator = std.heap.page_allocator; | |
| - | |
| -pub fn main() !void { | |
| - const sine_data: [44100]f32 = generate_sinewave_data(); | |
| - const sinewave: Wave = Wave.init(sine_data[0..], allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }); | |
| - | |
| - const sawtooth_data: [44100]f32 = generate_sawtooth_wave_data(); | |
| - const sawtooth_wave: Wave = Wave.init(sawtooth_data[0..], allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }); | |
| - | |
| - const decayed_sawtooth_wave: Wave = sawtooth_wave | |
| - .filter(decay) | |
| - .filter(decay) | |
| - .filter(decay) | |
| - .filter(decay) | |
| - .filter(decay); | |
| - | |
| - const mixed_wave: Wave = sinewave.mix(decayed_sawtooth_wave); | |
| - defer mixed_wave.deinit(); | |
| - | |
| - var file = try std.fs.cwd().createFile("result.wav", .{}); | |
| - defer file.close(); | |
| - | |
| - try mixed_wave.write(file); | |
| -} | |
| - | |
| -fn generate_sawtooth_wave_data() [44100]f32 { | |
| - const sample_rate: f32 = 44100.0; | |
| - const freq: f32 = 440.0; | |
| - const period: f32 = sample_rate / freq; | |
| - | |
| - var result: [44100]f32 = undefined; | |
| - var i: usize = 0; | |
| - | |
| - while (i < result.len) : (i += 1) { | |
| - const phase = @as(f32, @floatFromInt(i % @as(usize, @intFromFloat(period)))) / period; | |
| - result[i] = (phase * 2.0) - 1.0; | |
| - } | |
| - | |
| - return result; | |
| -} | |
| - | |
| -fn generate_sinewave_data() [44100]f32 { | |
| - const sample_rate: f32 = 44100.0; | |
| - const radins_per_sec: f32 = 440.0 * 2.0 * std.math.pi; | |
| - | |
| - var result: [44100]f32 = undefined; | |
| - var i: usize = 0; | |
| - | |
| - while (i < result.len) : (i += 1) { | |
| - result[i] = 0.5 * std.math.sin(@as(f32, @floatFromInt(i)) * radins_per_sec / sample_rate); | |
| - } | |
| - | |
| - return result; | |
| -} | |
| - | |
| -fn decay(original_wave: Wave) !Wave { | |
| - var result: std.array_list.Aligned(f32, null) = .empty; | |
| - | |
| - for (original_wave.data, 0..) |data, n| { | |
| - const i = original_wave.data.len - n; | |
| - const volume: f32 = @as(f32, @floatFromInt(i)) * (1.0 / @as(f32, @floatFromInt(original_wave.data.len))); | |
| - | |
| - const new_data = data * volume; | |
| - try result.append(original_wave.allocator, new_data); | |
| - } | |
| - | |
| - return Wave{ | |
| - .data = try result.toOwnedSlice(original_wave.allocator), | |
| - .allocator = original_wave.allocator, | |
| - | |
| - .sample_rate = original_wave.sample_rate, | |
| - .channels = original_wave.channels, | |
| - .bits = original_wave.bits, | |
| - }; | |
| -} | |
| diff --git a/examples/Wave/generate_pinknoise/.gitignore b/examples/Wave/generate_pinknoise/.gitignore | |
| deleted file mode 100644 | |
| index 374ca18..0000000 | |
| --- a/examples/Wave/generate_pinknoise/.gitignore | |
| +++ /dev/null | |
| @@ -1 +0,0 @@ | |
| -result.wav | |
| diff --git a/examples/Wave/generate_pinknoise/build.zig b/examples/Wave/generate_pinknoise/build.zig | |
| deleted file mode 100644 | |
| index 2db7c6e..0000000 | |
| --- a/examples/Wave/generate_pinknoise/build.zig | |
| +++ /dev/null | |
| @@ -1,45 +0,0 @@ | |
| -const std = @import("std"); | |
| - | |
| -pub fn build(b: *std.Build) void { | |
| - const target = b.standardTargetOptions(.{}); | |
| - const optimize = b.standardOptimizeOption(.{}); | |
| - | |
| - // Dependencies | |
| - const lightmix = b.dependency("lightmix", .{}); | |
| - | |
| - // Executable Module | |
| - const exe_mod = b.createModule(.{ | |
| - .root_source_file = b.path("src/main.zig"), | |
| - .target = target, | |
| - .optimize = optimize, | |
| - }); | |
| - exe_mod.addImport("lightmix", lightmix.module("lightmix")); | |
| - | |
| - // Executable | |
| - const exe = b.addExecutable(.{ | |
| - .name = "generate_pinknoise", | |
| - .root_module = exe_mod, | |
| - }); | |
| - b.installArtifact(exe); | |
| - | |
| - // Rum cmd | |
| - const run_cmd = b.addRunArtifact(exe); | |
| - run_cmd.step.dependOn(b.getInstallStep()); | |
| - if (b.args) |args| { | |
| - run_cmd.addArgs(args); | |
| - } | |
| - | |
| - // Run step | |
| - const run_step = b.step("run", "Run the app"); | |
| - run_step.dependOn(&run_cmd.step); | |
| - | |
| - // Unit tests | |
| - const exe_unit_tests = b.addTest(.{ | |
| - .root_module = exe_mod, | |
| - }); | |
| - const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); | |
| - | |
| - // Test step | |
| - const test_step = b.step("test", "Run unit tests"); | |
| - test_step.dependOn(&run_exe_unit_tests.step); | |
| -} | |
| diff --git a/examples/Wave/generate_pinknoise/build.zig.zon b/examples/Wave/generate_pinknoise/build.zig.zon | |
| deleted file mode 100644 | |
| index 9a594ec..0000000 | |
| --- a/examples/Wave/generate_pinknoise/build.zig.zon | |
| +++ /dev/null | |
| @@ -1,14 +0,0 @@ | |
| -.{ | |
| - .name = .generate_pinknoise, | |
| - .version = "0.0.0", | |
| - .fingerprint = 0xa54dcee860679070, | |
| - .minimum_zig_version = "0.15.1", | |
| - | |
| - .dependencies = .{ .lightmix = .{ .path = "../../.." } }, | |
| - | |
| - .paths = .{ | |
| - "build.zig", | |
| - "build.zig.zon", | |
| - "src", | |
| - }, | |
| -} | |
| diff --git a/examples/Wave/generate_pinknoise/src/main.zig b/examples/Wave/generate_pinknoise/src/main.zig | |
| deleted file mode 100644 | |
| index eb98195..0000000 | |
| --- a/examples/Wave/generate_pinknoise/src/main.zig | |
| +++ /dev/null | |
| @@ -1,68 +0,0 @@ | |
| -const std = @import("std"); | |
| -const lightmix = @import("lightmix"); | |
| -const Wave = lightmix.Wave; | |
| -const allocator = std.heap.page_allocator; | |
| - | |
| -pub fn main() !void { | |
| - const data: [44100]f32 = generate_pink_noise(); | |
| - const pinknoise: Wave = Wave.init(data[0..], allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }); | |
| - | |
| - const decayed_pinknoise: Wave = pinknoise.filter(decay).filter(decay).filter(decay); | |
| - defer decayed_pinknoise.deinit(); | |
| - | |
| - var file = try std.fs.cwd().createFile("result.wav", .{}); | |
| - defer file.close(); | |
| - | |
| - try decayed_pinknoise.write(file); | |
| -} | |
| - | |
| -fn generate_pink_noise() [44100]f32 { | |
| - var result: [44100]f32 = undefined; | |
| - | |
| - var prng = std.Random.DefaultPrng.init(0); | |
| - const rand = prng.random(); | |
| - | |
| - var b0: f32 = 0.0; | |
| - var b1: f32 = 0.0; | |
| - var b2: f32 = 0.0; | |
| - | |
| - var i: usize = 0; | |
| - while (i < result.len) : (i += 1) { | |
| - const white: f32 = rand.float(f32) * 2.0 - 1.0; | |
| - | |
| - b0 = 0.99765 * b0 + white * 0.0990460; | |
| - b1 = 0.96300 * b1 + white * 0.2965164; | |
| - b2 = 0.57000 * b2 + white * 1.0526913; | |
| - | |
| - const pink: f32 = b0 + b1 + b2 + white * 0.1848; | |
| - | |
| - result[i] = pink; | |
| - } | |
| - | |
| - return result; | |
| -} | |
| - | |
| -fn decay(original_wave: Wave) !Wave { | |
| - var result: std.array_list.Aligned(f32, null) = .empty; | |
| - | |
| - for (original_wave.data, 0..) |data, n| { | |
| - const i = original_wave.data.len - n; | |
| - const volume: f32 = @as(f32, @floatFromInt(i)) * (1.0 / @as(f32, @floatFromInt(original_wave.data.len))); | |
| - | |
| - const new_data = data * volume; | |
| - try result.append(original_wave.allocator, new_data); | |
| - } | |
| - | |
| - return Wave{ | |
| - .data = try result.toOwnedSlice(original_wave.allocator), | |
| - .allocator = original_wave.allocator, | |
| - | |
| - .sample_rate = original_wave.sample_rate, | |
| - .channels = original_wave.channels, | |
| - .bits = original_wave.bits, | |
| - }; | |
| -} | |
| diff --git a/examples/Wave/generate_sawtooth_wave/.gitignore b/examples/Wave/generate_sawtooth_wave/.gitignore | |
| deleted file mode 100644 | |
| index 374ca18..0000000 | |
| --- a/examples/Wave/generate_sawtooth_wave/.gitignore | |
| +++ /dev/null | |
| @@ -1 +0,0 @@ | |
| -result.wav | |
| diff --git a/examples/Wave/generate_sawtooth_wave/build.zig b/examples/Wave/generate_sawtooth_wave/build.zig | |
| deleted file mode 100644 | |
| index ace9741..0000000 | |
| --- a/examples/Wave/generate_sawtooth_wave/build.zig | |
| +++ /dev/null | |
| @@ -1,45 +0,0 @@ | |
| -const std = @import("std"); | |
| - | |
| -pub fn build(b: *std.Build) void { | |
| - const target = b.standardTargetOptions(.{}); | |
| - const optimize = b.standardOptimizeOption(.{}); | |
| - | |
| - // Dependencies | |
| - const lightmix = b.dependency("lightmix", .{}); | |
| - | |
| - // Executable Module | |
| - const exe_mod = b.createModule(.{ | |
| - .root_source_file = b.path("src/main.zig"), | |
| - .target = target, | |
| - .optimize = optimize, | |
| - }); | |
| - exe_mod.addImport("lightmix", lightmix.module("lightmix")); | |
| - | |
| - // Executable | |
| - const exe = b.addExecutable(.{ | |
| - .name = "generate_sawtooth_wave", | |
| - .root_module = exe_mod, | |
| - }); | |
| - b.installArtifact(exe); | |
| - | |
| - // Rum cmd | |
| - const run_cmd = b.addRunArtifact(exe); | |
| - run_cmd.step.dependOn(b.getInstallStep()); | |
| - if (b.args) |args| { | |
| - run_cmd.addArgs(args); | |
| - } | |
| - | |
| - // Run step | |
| - const run_step = b.step("run", "Run the app"); | |
| - run_step.dependOn(&run_cmd.step); | |
| - | |
| - // Unit tests | |
| - const exe_unit_tests = b.addTest(.{ | |
| - .root_module = exe_mod, | |
| - }); | |
| - const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); | |
| - | |
| - // Test step | |
| - const test_step = b.step("test", "Run unit tests"); | |
| - test_step.dependOn(&run_exe_unit_tests.step); | |
| -} | |
| diff --git a/examples/Wave/generate_sawtooth_wave/build.zig.zon b/examples/Wave/generate_sawtooth_wave/build.zig.zon | |
| deleted file mode 100644 | |
| index b14879f..0000000 | |
| --- a/examples/Wave/generate_sawtooth_wave/build.zig.zon | |
| +++ /dev/null | |
| @@ -1,14 +0,0 @@ | |
| -.{ | |
| - .name = .generate_sawtooth_wave, | |
| - .version = "0.0.0", | |
| - .fingerprint = 0x9abf981241297e1a, | |
| - .minimum_zig_version = "0.15.1", | |
| - | |
| - .dependencies = .{ .lightmix = .{ .path = "../../.." } }, | |
| - | |
| - .paths = .{ | |
| - "build.zig", | |
| - "build.zig.zon", | |
| - "src", | |
| - }, | |
| -} | |
| diff --git a/examples/Wave/generate_sawtooth_wave/src/main.zig b/examples/Wave/generate_sawtooth_wave/src/main.zig | |
| deleted file mode 100644 | |
| index c481fc0..0000000 | |
| --- a/examples/Wave/generate_sawtooth_wave/src/main.zig | |
| +++ /dev/null | |
| @@ -1,58 +0,0 @@ | |
| -const std = @import("std"); | |
| -const lightmix = @import("lightmix"); | |
| -const Wave = lightmix.Wave; | |
| -const allocator = std.heap.page_allocator; | |
| - | |
| -pub fn main() !void { | |
| - const data: [44100]f32 = generate_sawtooth_wave_data(); | |
| - const sawtooth_wave: Wave = Wave.init(data[0..], allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }); | |
| - | |
| - const decayed_wave: Wave = sawtooth_wave.filter(decay); | |
| - defer decayed_wave.deinit(); | |
| - | |
| - var file = try std.fs.cwd().createFile("result.wav", .{}); | |
| - defer file.close(); | |
| - | |
| - try decayed_wave.write(file); | |
| -} | |
| - | |
| -fn generate_sawtooth_wave_data() [44100]f32 { | |
| - const sample_rate: f32 = 44100.0; | |
| - const freq: f32 = 440.0; | |
| - const period: f32 = sample_rate / freq; | |
| - | |
| - var result: [44100]f32 = undefined; | |
| - var i: usize = 0; | |
| - | |
| - while (i < result.len) : (i += 1) { | |
| - const phase = @as(f32, @floatFromInt(i % @as(usize, @intFromFloat(period)))) / period; | |
| - result[i] = (phase * 2.0) - 1.0; | |
| - } | |
| - | |
| - return result; | |
| -} | |
| - | |
| -fn decay(original_wave: Wave) !Wave { | |
| - var result: std.array_list.Aligned(f32, null) = .empty; | |
| - | |
| - for (original_wave.data, 0..) |data, n| { | |
| - const i = original_wave.data.len - n; | |
| - const volume: f32 = @as(f32, @floatFromInt(i)) * (1.0 / @as(f32, @floatFromInt(original_wave.data.len))); | |
| - | |
| - const new_data = data * volume; | |
| - try result.append(original_wave.allocator, new_data); | |
| - } | |
| - | |
| - return Wave{ | |
| - .data = try result.toOwnedSlice(original_wave.allocator), | |
| - .allocator = original_wave.allocator, | |
| - | |
| - .sample_rate = original_wave.sample_rate, | |
| - .channels = original_wave.channels, | |
| - .bits = original_wave.bits, | |
| - }; | |
| -} | |
| diff --git a/examples/Wave/generate_sinewave/.gitignore b/examples/Wave/generate_sinewave/.gitignore | |
| deleted file mode 100644 | |
| index 374ca18..0000000 | |
| --- a/examples/Wave/generate_sinewave/.gitignore | |
| +++ /dev/null | |
| @@ -1 +0,0 @@ | |
| -result.wav | |
| diff --git a/examples/Wave/generate_sinewave/build.zig.zon b/examples/Wave/generate_sinewave/build.zig.zon | |
| deleted file mode 100644 | |
| index f079a1e..0000000 | |
| --- a/examples/Wave/generate_sinewave/build.zig.zon | |
| +++ /dev/null | |
| @@ -1,14 +0,0 @@ | |
| -.{ | |
| - .name = .generate_sinewave, | |
| - .version = "0.0.0", | |
| - .fingerprint = 0x67afc43683928479, | |
| - .minimum_zig_version = "0.15.1", | |
| - | |
| - .dependencies = .{ .lightmix = .{ .path = "../../.." } }, | |
| - | |
| - .paths = .{ | |
| - "build.zig", | |
| - "build.zig.zon", | |
| - "src", | |
| - }, | |
| -} | |
| diff --git a/examples/Wave/generate_sinewave/src/main.zig b/examples/Wave/generate_sinewave/src/main.zig | |
| deleted file mode 100644 | |
| index abddaa2..0000000 | |
| --- a/examples/Wave/generate_sinewave/src/main.zig | |
| +++ /dev/null | |
| @@ -1,36 +0,0 @@ | |
| -const std = @import("std"); | |
| -const lightmix = @import("lightmix"); | |
| -const Wave = lightmix.Wave; | |
| -const allocator = std.heap.page_allocator; | |
| - | |
| -pub fn main() !void { | |
| - const data: [44100]f32 = generate_sinewave_data(); | |
| - const sinewave: Wave = Wave.init(data[0..], allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }); | |
| - defer sinewave.deinit(); | |
| - | |
| - var file = try std.fs.cwd().createFile("result.wav", .{}); | |
| - defer file.close(); | |
| - | |
| - try sinewave.write(file); | |
| -} | |
| - | |
| -const c_5: f32 = 523.251; | |
| -const volume: f32 = 1.0; | |
| - | |
| -fn generate_sinewave_data() [44100]f32 { | |
| - const sample_rate: f32 = 44100.0; | |
| - const radins_per_sec: f32 = c_5 * 2.0 * std.math.pi; | |
| - | |
| - var result: [44100]f32 = undefined; | |
| - var i: usize = 0; | |
| - | |
| - while (i < result.len) : (i += 1) { | |
| - result[i] = std.math.sin(@as(f32, @floatFromInt(i)) * radins_per_sec / sample_rate) * volume; | |
| - } | |
| - | |
| - return result; | |
| -} | |
| diff --git a/examples/Wave/generate_soundless/.gitignore b/examples/Wave/generate_soundless/.gitignore | |
| deleted file mode 100644 | |
| index 374ca18..0000000 | |
| --- a/examples/Wave/generate_soundless/.gitignore | |
| +++ /dev/null | |
| @@ -1 +0,0 @@ | |
| -result.wav | |
| diff --git a/examples/Wave/generate_soundless/build.zig.zon b/examples/Wave/generate_soundless/build.zig.zon | |
| deleted file mode 100644 | |
| index f079a1e..0000000 | |
| --- a/examples/Wave/generate_soundless/build.zig.zon | |
| +++ /dev/null | |
| @@ -1,14 +0,0 @@ | |
| -.{ | |
| - .name = .generate_sinewave, | |
| - .version = "0.0.0", | |
| - .fingerprint = 0x67afc43683928479, | |
| - .minimum_zig_version = "0.15.1", | |
| - | |
| - .dependencies = .{ .lightmix = .{ .path = "../../.." } }, | |
| - | |
| - .paths = .{ | |
| - "build.zig", | |
| - "build.zig.zon", | |
| - "src", | |
| - }, | |
| -} | |
| diff --git a/examples/Wave/generate_soundless/src/main.zig b/examples/Wave/generate_soundless/src/main.zig | |
| deleted file mode 100644 | |
| index 8d584aa..0000000 | |
| --- a/examples/Wave/generate_soundless/src/main.zig | |
| +++ /dev/null | |
| @@ -1,30 +0,0 @@ | |
| -const std = @import("std"); | |
| -const lightmix = @import("lightmix"); | |
| -const Wave = lightmix.Wave; | |
| - | |
| -pub fn main() !void { | |
| - const allocator = std.heap.page_allocator; | |
| - const data: []const f32 = generate_soundless_data(44100, allocator); | |
| - defer allocator.free(data); | |
| - | |
| - const soundless_wave: Wave = Wave.init(data, allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }); | |
| - defer soundless_wave.deinit(); | |
| - | |
| - var file = try std.fs.cwd().createFile("result.wav", .{}); | |
| - defer file.close(); | |
| - | |
| - try soundless_wave.write(file); | |
| -} | |
| - | |
| -fn generate_soundless_data(length: usize, allocator: std.mem.Allocator) []const f32 { | |
| - var result: []f32 = allocator.alloc(f32, length) catch @panic("Out of memory"); | |
| - | |
| - for (0..result.len) |i| | |
| - result[i] = 0.0; | |
| - | |
| - return result; | |
| -} | |
| diff --git a/examples/Wave/generate_square_wave/.gitignore b/examples/Wave/generate_square_wave/.gitignore | |
| deleted file mode 100644 | |
| index 374ca18..0000000 | |
| --- a/examples/Wave/generate_square_wave/.gitignore | |
| +++ /dev/null | |
| @@ -1 +0,0 @@ | |
| -result.wav | |
| diff --git a/examples/Wave/generate_square_wave/build.zig b/examples/Wave/generate_square_wave/build.zig | |
| deleted file mode 100644 | |
| index b868e22..0000000 | |
| --- a/examples/Wave/generate_square_wave/build.zig | |
| +++ /dev/null | |
| @@ -1,45 +0,0 @@ | |
| -const std = @import("std"); | |
| - | |
| -pub fn build(b: *std.Build) void { | |
| - const target = b.standardTargetOptions(.{}); | |
| - const optimize = b.standardOptimizeOption(.{}); | |
| - | |
| - // Dependencies | |
| - const lightmix = b.dependency("lightmix", .{}); | |
| - | |
| - // Executable Module | |
| - const exe_mod = b.createModule(.{ | |
| - .root_source_file = b.path("src/main.zig"), | |
| - .target = target, | |
| - .optimize = optimize, | |
| - }); | |
| - exe_mod.addImport("lightmix", lightmix.module("lightmix")); | |
| - | |
| - // Executable | |
| - const exe = b.addExecutable(.{ | |
| - .name = "generate_square_wave", | |
| - .root_module = exe_mod, | |
| - }); | |
| - b.installArtifact(exe); | |
| - | |
| - // Rum cmd | |
| - const run_cmd = b.addRunArtifact(exe); | |
| - run_cmd.step.dependOn(b.getInstallStep()); | |
| - if (b.args) |args| { | |
| - run_cmd.addArgs(args); | |
| - } | |
| - | |
| - // Run step | |
| - const run_step = b.step("run", "Run the app"); | |
| - run_step.dependOn(&run_cmd.step); | |
| - | |
| - // Unit tests | |
| - const exe_unit_tests = b.addTest(.{ | |
| - .root_module = exe_mod, | |
| - }); | |
| - const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); | |
| - | |
| - // Test step | |
| - const test_step = b.step("test", "Run unit tests"); | |
| - test_step.dependOn(&run_exe_unit_tests.step); | |
| -} | |
| diff --git a/examples/Wave/generate_square_wave/build.zig.zon b/examples/Wave/generate_square_wave/build.zig.zon | |
| deleted file mode 100644 | |
| index 04b4565..0000000 | |
| --- a/examples/Wave/generate_square_wave/build.zig.zon | |
| +++ /dev/null | |
| @@ -1,14 +0,0 @@ | |
| -.{ | |
| - .name = .generate_square_wave, | |
| - .version = "0.0.0", | |
| - .fingerprint = 0x5a3eaa4ef07db985, | |
| - .minimum_zig_version = "0.15.1", | |
| - | |
| - .dependencies = .{ .lightmix = .{ .path = "../../.." } }, | |
| - | |
| - .paths = .{ | |
| - "build.zig", | |
| - "build.zig.zon", | |
| - "src", | |
| - }, | |
| -} | |
| diff --git a/examples/Wave/generate_square_wave/src/main.zig b/examples/Wave/generate_square_wave/src/main.zig | |
| deleted file mode 100644 | |
| index 0003365..0000000 | |
| --- a/examples/Wave/generate_square_wave/src/main.zig | |
| +++ /dev/null | |
| @@ -1,59 +0,0 @@ | |
| -const std = @import("std"); | |
| -const lightmix = @import("lightmix"); | |
| -const Wave = lightmix.Wave; | |
| -const allocator = std.heap.page_allocator; | |
| - | |
| -pub fn main() !void { | |
| - const data: [44100]f32 = generate_square_wave_data(); | |
| - const square_wave: Wave = Wave.init(data[0..], allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }); | |
| - | |
| - const decayed_wave: Wave = square_wave.filter(decay); | |
| - defer decayed_wave.deinit(); | |
| - | |
| - var file = try std.fs.cwd().createFile("result.wav", .{}); | |
| - defer file.close(); | |
| - | |
| - try decayed_wave.write(file); | |
| -} | |
| - | |
| -fn generate_square_wave_data() [44100]f32 { | |
| - const sample_rate: f32 = 44100.0; | |
| - const radins_per_sec: f32 = 440.0 * 2.0 * std.math.pi; | |
| - const sharpness: f32 = 7.0; | |
| - | |
| - var result: [44100]f32 = undefined; | |
| - var i: usize = 0; | |
| - | |
| - while (i < result.len) : (i += 1) { | |
| - const sine_wave = std.math.sin(@as(f32, @floatFromInt(i)) * radins_per_sec / sample_rate); | |
| - const square_wave = std.math.tanh(sharpness * sine_wave); | |
| - result[i] = square_wave; | |
| - } | |
| - | |
| - return result; | |
| -} | |
| - | |
| -fn decay(original_wave: Wave) !Wave { | |
| - var result: std.array_list.Aligned(f32, null) = .empty; | |
| - | |
| - for (original_wave.data, 0..) |data, n| { | |
| - const i = original_wave.data.len - n; | |
| - const volume: f32 = @as(f32, @floatFromInt(i)) * (1.0 / @as(f32, @floatFromInt(original_wave.data.len))); | |
| - | |
| - const new_data = data * volume; | |
| - try result.append(original_wave.allocator, new_data); | |
| - } | |
| - | |
| - return Wave{ | |
| - .data = try result.toOwnedSlice(original_wave.allocator), | |
| - .allocator = original_wave.allocator, | |
| - | |
| - .sample_rate = original_wave.sample_rate, | |
| - .channels = original_wave.channels, | |
| - .bits = original_wave.bits, | |
| - }; | |
| -} | |
| diff --git a/examples/Wave/generate_triangle_wave/.gitignore b/examples/Wave/generate_triangle_wave/.gitignore | |
| deleted file mode 100644 | |
| index 374ca18..0000000 | |
| --- a/examples/Wave/generate_triangle_wave/.gitignore | |
| +++ /dev/null | |
| @@ -1 +0,0 @@ | |
| -result.wav | |
| diff --git a/examples/Wave/generate_triangle_wave/build.zig b/examples/Wave/generate_triangle_wave/build.zig | |
| deleted file mode 100644 | |
| index 965736c..0000000 | |
| --- a/examples/Wave/generate_triangle_wave/build.zig | |
| +++ /dev/null | |
| @@ -1,45 +0,0 @@ | |
| -const std = @import("std"); | |
| - | |
| -pub fn build(b: *std.Build) void { | |
| - const target = b.standardTargetOptions(.{}); | |
| - const optimize = b.standardOptimizeOption(.{}); | |
| - | |
| - // Dependencies | |
| - const lightmix = b.dependency("lightmix", .{}); | |
| - | |
| - // Executable Module | |
| - const exe_mod = b.createModule(.{ | |
| - .root_source_file = b.path("src/main.zig"), | |
| - .target = target, | |
| - .optimize = optimize, | |
| - }); | |
| - exe_mod.addImport("lightmix", lightmix.module("lightmix")); | |
| - | |
| - // Executable | |
| - const exe = b.addExecutable(.{ | |
| - .name = "generate_triangle_wave", | |
| - .root_module = exe_mod, | |
| - }); | |
| - b.installArtifact(exe); | |
| - | |
| - // Rum cmd | |
| - const run_cmd = b.addRunArtifact(exe); | |
| - run_cmd.step.dependOn(b.getInstallStep()); | |
| - if (b.args) |args| { | |
| - run_cmd.addArgs(args); | |
| - } | |
| - | |
| - // Run step | |
| - const run_step = b.step("run", "Run the app"); | |
| - run_step.dependOn(&run_cmd.step); | |
| - | |
| - // Unit tests | |
| - const exe_unit_tests = b.addTest(.{ | |
| - .root_module = exe_mod, | |
| - }); | |
| - const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); | |
| - | |
| - // Test step | |
| - const test_step = b.step("test", "Run unit tests"); | |
| - test_step.dependOn(&run_exe_unit_tests.step); | |
| -} | |
| diff --git a/examples/Wave/generate_triangle_wave/build.zig.zon b/examples/Wave/generate_triangle_wave/build.zig.zon | |
| deleted file mode 100644 | |
| index ab49995..0000000 | |
| --- a/examples/Wave/generate_triangle_wave/build.zig.zon | |
| +++ /dev/null | |
| @@ -1,14 +0,0 @@ | |
| -.{ | |
| - .name = .generate_triangle_wave, | |
| - .version = "0.0.0", | |
| - .fingerprint = 0x8d7ccaa6407e2b94, | |
| - .minimum_zig_version = "0.15.1", | |
| - | |
| - .dependencies = .{ .lightmix = .{ .path = "../../.." } }, | |
| - | |
| - .paths = .{ | |
| - "build.zig", | |
| - "build.zig.zon", | |
| - "src", | |
| - }, | |
| -} | |
| diff --git a/examples/Wave/generate_triangle_wave/src/main.zig b/examples/Wave/generate_triangle_wave/src/main.zig | |
| deleted file mode 100644 | |
| index 6c9f050..0000000 | |
| --- a/examples/Wave/generate_triangle_wave/src/main.zig | |
| +++ /dev/null | |
| @@ -1,63 +0,0 @@ | |
| -const std = @import("std"); | |
| -const lightmix = @import("lightmix"); | |
| -const Wave = lightmix.Wave; | |
| -const allocator = std.heap.page_allocator; | |
| - | |
| -pub fn main() !void { | |
| - const data: [44100]f32 = generate_triangle_wave_data(); | |
| - const triangle_wave: Wave = Wave.init(data[0..], allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }); | |
| - | |
| - const decayed_wave: Wave = triangle_wave.filter(decay); | |
| - defer decayed_wave.deinit(); | |
| - | |
| - var file = try std.fs.cwd().createFile("result.wav", .{}); | |
| - defer file.close(); | |
| - | |
| - try decayed_wave.write(file); | |
| -} | |
| - | |
| -fn generate_triangle_wave_data() [44100]f32 { | |
| - const sample_rate: f32 = 44100.0; | |
| - const freq: f32 = 440.0; | |
| - const period: f32 = sample_rate / freq; | |
| - | |
| - var result: [44100]f32 = undefined; | |
| - var i: usize = 0; | |
| - | |
| - while (i < result.len) : (i += 1) { | |
| - const phase = @as(f32, @floatFromInt(i % @as(usize, @intFromFloat(period)))) / period; | |
| - const triangle_value = if (phase < 0.5) | |
| - (phase * 4.0) - 1.0 // 前半 | |
| - else | |
| - 3.0 - (phase * 4.0); // 後半 | |
| - | |
| - result[i] = triangle_value; | |
| - } | |
| - | |
| - return result; | |
| -} | |
| - | |
| -fn decay(original_wave: Wave) !Wave { | |
| - var result: std.array_list.Aligned(f32, null) = .empty; | |
| - | |
| - for (original_wave.data, 0..) |data, n| { | |
| - const i = original_wave.data.len - n; | |
| - const volume: f32 = @as(f32, @floatFromInt(i)) * (1.0 / @as(f32, @floatFromInt(original_wave.data.len))); | |
| - | |
| - const new_data = data * volume; | |
| - try result.append(original_wave.allocator, new_data); | |
| - } | |
| - | |
| - return Wave{ | |
| - .data = try result.toOwnedSlice(original_wave.allocator), | |
| - .allocator = original_wave.allocator, | |
| - | |
| - .sample_rate = original_wave.sample_rate, | |
| - .channels = original_wave.channels, | |
| - .bits = original_wave.bits, | |
| - }; | |
| -} | |
| diff --git a/examples/Wave/generate_whitenoise/.gitignore b/examples/Wave/generate_whitenoise/.gitignore | |
| deleted file mode 100644 | |
| index 374ca18..0000000 | |
| --- a/examples/Wave/generate_whitenoise/.gitignore | |
| +++ /dev/null | |
| @@ -1 +0,0 @@ | |
| -result.wav | |
| diff --git a/examples/Wave/generate_whitenoise/build.zig b/examples/Wave/generate_whitenoise/build.zig | |
| deleted file mode 100644 | |
| index 9ebad25..0000000 | |
| --- a/examples/Wave/generate_whitenoise/build.zig | |
| +++ /dev/null | |
| @@ -1,45 +0,0 @@ | |
| -const std = @import("std"); | |
| - | |
| -pub fn build(b: *std.Build) void { | |
| - const target = b.standardTargetOptions(.{}); | |
| - const optimize = b.standardOptimizeOption(.{}); | |
| - | |
| - // Dependencies | |
| - const lightmix = b.dependency("lightmix", .{}); | |
| - | |
| - // Executable Module | |
| - const exe_mod = b.createModule(.{ | |
| - .root_source_file = b.path("src/main.zig"), | |
| - .target = target, | |
| - .optimize = optimize, | |
| - }); | |
| - exe_mod.addImport("lightmix", lightmix.module("lightmix")); | |
| - | |
| - // Executable | |
| - const exe = b.addExecutable(.{ | |
| - .name = "generate_whitenoise", | |
| - .root_module = exe_mod, | |
| - }); | |
| - b.installArtifact(exe); | |
| - | |
| - // Rum cmd | |
| - const run_cmd = b.addRunArtifact(exe); | |
| - run_cmd.step.dependOn(b.getInstallStep()); | |
| - if (b.args) |args| { | |
| - run_cmd.addArgs(args); | |
| - } | |
| - | |
| - // Run step | |
| - const run_step = b.step("run", "Run the app"); | |
| - run_step.dependOn(&run_cmd.step); | |
| - | |
| - // Unit tests | |
| - const exe_unit_tests = b.addTest(.{ | |
| - .root_module = exe_mod, | |
| - }); | |
| - const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); | |
| - | |
| - // Test step | |
| - const test_step = b.step("test", "Run unit tests"); | |
| - test_step.dependOn(&run_exe_unit_tests.step); | |
| -} | |
| diff --git a/examples/Wave/generate_whitenoise/build.zig.zon b/examples/Wave/generate_whitenoise/build.zig.zon | |
| deleted file mode 100644 | |
| index c53d461..0000000 | |
| --- a/examples/Wave/generate_whitenoise/build.zig.zon | |
| +++ /dev/null | |
| @@ -1,14 +0,0 @@ | |
| -.{ | |
| - .name = .generate_whitenoise, | |
| - .version = "0.0.0", | |
| - .fingerprint = 0x50bfa204b59a6196, | |
| - .minimum_zig_version = "0.15.1", | |
| - | |
| - .dependencies = .{ .lightmix = .{ .path = "../../.." } }, | |
| - | |
| - .paths = .{ | |
| - "build.zig", | |
| - "build.zig.zon", | |
| - "src", | |
| - }, | |
| -} | |
| diff --git a/examples/Wave/generate_whitenoise/src/main.zig b/examples/Wave/generate_whitenoise/src/main.zig | |
| deleted file mode 100644 | |
| index f7d69f4..0000000 | |
| --- a/examples/Wave/generate_whitenoise/src/main.zig | |
| +++ /dev/null | |
| @@ -1,57 +0,0 @@ | |
| -const std = @import("std"); | |
| -const lightmix = @import("lightmix"); | |
| -const Wave = lightmix.Wave; | |
| -const allocator = std.heap.page_allocator; | |
| - | |
| -pub fn main() !void { | |
| - const data: [44100]f32 = generate_white_noise(); | |
| - const whitenoise: Wave = Wave.init(data[0..], allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }); | |
| - | |
| - const decayed_whitenoise: Wave = whitenoise.filter(decay).filter(decay).filter(decay); | |
| - defer decayed_whitenoise.deinit(); | |
| - | |
| - var file = try std.fs.cwd().createFile("result.wav", .{}); | |
| - defer file.close(); | |
| - | |
| - try decayed_whitenoise.write(file); | |
| -} | |
| - | |
| -fn generate_white_noise() [44100]f32 { | |
| - var result: [44100]f32 = undefined; | |
| - | |
| - var prng = std.Random.DefaultPrng.init(0); | |
| - const rand = prng.random(); | |
| - | |
| - var i: usize = 0; | |
| - while (i < result.len) : (i += 1) { | |
| - const r: f32 = rand.float(f32) * 2.0 - 1.0; | |
| - result[i] = r; | |
| - } | |
| - | |
| - return result; | |
| -} | |
| - | |
| -fn decay(original_wave: Wave) !Wave { | |
| - var result: std.array_list.Aligned(f32, null) = .empty; | |
| - | |
| - for (original_wave.data, 0..) |data, n| { | |
| - const i = original_wave.data.len - n; | |
| - const volume: f32 = @as(f32, @floatFromInt(i)) * (1.0 / @as(f32, @floatFromInt(original_wave.data.len))); | |
| - | |
| - const new_data = data * volume; | |
| - try result.append(original_wave.allocator, new_data); | |
| - } | |
| - | |
| - return Wave{ | |
| - .data = try result.toOwnedSlice(original_wave.allocator), | |
| - .allocator = original_wave.allocator, | |
| - | |
| - .sample_rate = original_wave.sample_rate, | |
| - .channels = original_wave.channels, | |
| - .bits = original_wave.bits, | |
| - }; | |
| -} | |
| diff --git a/examples/Wave/generate_with_debug_play/.gitignore b/examples/Wave/generate_with_debug_play/.gitignore | |
| deleted file mode 100644 | |
| index 374ca18..0000000 | |
| --- a/examples/Wave/generate_with_debug_play/.gitignore | |
| +++ /dev/null | |
| @@ -1 +0,0 @@ | |
| -result.wav | |
| diff --git a/examples/Wave/generate_with_debug_play/build.zig b/examples/Wave/generate_with_debug_play/build.zig | |
| deleted file mode 100644 | |
| index 3b4fe70..0000000 | |
| --- a/examples/Wave/generate_with_debug_play/build.zig | |
| +++ /dev/null | |
| @@ -1,49 +0,0 @@ | |
| -const std = @import("std"); | |
| - | |
| -pub fn build(b: *std.Build) void { | |
| - const target = b.standardTargetOptions(.{}); | |
| - const optimize = b.standardOptimizeOption(.{}); | |
| - | |
| - // Dependencies | |
| - const lightmix = b.dependency("lightmix", .{ | |
| - .with_debug_features = true, | |
| - }); | |
| - | |
| - // Executable Module | |
| - const exe_mod = b.createModule(.{ | |
| - .root_source_file = b.path("src/main.zig"), | |
| - .target = target, | |
| - .optimize = optimize, | |
| - }); | |
| - exe_mod.addImport("lightmix", lightmix.module("lightmix")); | |
| - | |
| - // Executable | |
| - const exe = b.addExecutable(.{ | |
| - .name = "generate_with_debug_play", | |
| - .root_module = exe_mod, | |
| - }); | |
| - exe.linkSystemLibrary2("portaudio-2.0", .{}); | |
| - exe.linkSystemLibrary2("sndfile", .{}); | |
| - b.installArtifact(exe); | |
| - | |
| - // Rum cmd | |
| - const run_cmd = b.addRunArtifact(exe); | |
| - run_cmd.step.dependOn(b.getInstallStep()); | |
| - if (b.args) |args| { | |
| - run_cmd.addArgs(args); | |
| - } | |
| - | |
| - // Run step | |
| - const run_step = b.step("run", "Run the app"); | |
| - run_step.dependOn(&run_cmd.step); | |
| - | |
| - // Unit tests | |
| - const exe_unit_tests = b.addTest(.{ | |
| - .root_module = exe_mod, | |
| - }); | |
| - const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); | |
| - | |
| - // Test step | |
| - const test_step = b.step("test", "Run unit tests"); | |
| - test_step.dependOn(&run_exe_unit_tests.step); | |
| -} | |
| diff --git a/examples/Wave/generate_with_debug_play/build.zig.zon b/examples/Wave/generate_with_debug_play/build.zig.zon | |
| deleted file mode 100644 | |
| index 1066a26..0000000 | |
| --- a/examples/Wave/generate_with_debug_play/build.zig.zon | |
| +++ /dev/null | |
| @@ -1,14 +0,0 @@ | |
| -.{ | |
| - .name = .generate_with_debug_play, | |
| - .version = "0.0.0", | |
| - .fingerprint = 0x397a3b91015a3229, | |
| - .minimum_zig_version = "0.15.1", | |
| - | |
| - .dependencies = .{ .lightmix = .{ .path = "../../.." } }, | |
| - | |
| - .paths = .{ | |
| - "build.zig", | |
| - "build.zig.zon", | |
| - "src", | |
| - }, | |
| -} | |
| diff --git a/examples/Wave/generate_with_debug_play/src/assets/sine.wav b/examples/Wave/generate_with_debug_play/src/assets/sine.wav | |
| deleted file mode 100644 | |
| index 13cb2fb..0000000 | |
| Binary files a/examples/Wave/generate_with_debug_play/src/assets/sine.wav and /dev/null differ | |
| diff --git a/examples/Wave/generate_with_debug_play/src/main.zig b/examples/Wave/generate_with_debug_play/src/main.zig | |
| deleted file mode 100644 | |
| index f8e7322..0000000 | |
| --- a/examples/Wave/generate_with_debug_play/src/main.zig | |
| +++ /dev/null | |
| @@ -1,59 +0,0 @@ | |
| -const std = @import("std"); | |
| -const lightmix = @import("lightmix"); | |
| -const Wave = lightmix.Wave; | |
| -const allocator = std.heap.page_allocator; | |
| - | |
| -pub fn main() !void { | |
| - const sinewave: Wave = Wave.from_file_content(@embedFile("./assets/sine.wav"), allocator); | |
| - | |
| - const decayed_wave: Wave = sinewave.filter(decay).filter(amp); | |
| - defer decayed_wave.deinit(); | |
| - | |
| - var file = try std.fs.cwd().createFile("result.wav", .{}); | |
| - defer file.close(); | |
| - | |
| - try decayed_wave.write(file); | |
| - | |
| - try decayed_wave.debug_play(); | |
| -} | |
| - | |
| -fn decay(original_wave: Wave) !Wave { | |
| - var result: std.array_list.Aligned(f32, null) = .empty; | |
| - | |
| - for (original_wave.data, 0..) |data, n| { | |
| - const i = original_wave.data.len - n; | |
| - const volume: f32 = @as(f32, @floatFromInt(i)) * (1.0 / @as(f32, @floatFromInt(original_wave.data.len))); | |
| - | |
| - const new_data = data * volume; | |
| - try result.append(original_wave.allocator, new_data); | |
| - } | |
| - | |
| - return Wave{ | |
| - .data = try result.toOwnedSlice(original_wave.allocator), | |
| - .allocator = original_wave.allocator, | |
| - | |
| - .sample_rate = original_wave.sample_rate, | |
| - .channels = original_wave.channels, | |
| - .bits = original_wave.bits, | |
| - }; | |
| -} | |
| - | |
| -fn amp(original_wave: Wave) !Wave { | |
| - var result: std.array_list.Aligned(f32, null) = .empty; | |
| - | |
| - for (original_wave.data, 0..) |data, i| { | |
| - const volume: f32 = @as(f32, @floatFromInt(i)) * (1.0 / @as(f32, @floatFromInt(original_wave.data.len))); | |
| - | |
| - const new_data = data * volume; | |
| - try result.append(original_wave.allocator, new_data); | |
| - } | |
| - | |
| - return Wave{ | |
| - .data = try result.toOwnedSlice(original_wave.allocator), | |
| - .allocator = original_wave.allocator, | |
| - | |
| - .sample_rate = original_wave.sample_rate, | |
| - .channels = original_wave.channels, | |
| - .bits = original_wave.bits, | |
| - }; | |
| -} | |
| diff --git a/examples/Wave/generate_with_decay/.gitignore b/examples/Wave/generate_with_decay/.gitignore | |
| deleted file mode 100644 | |
| index 374ca18..0000000 | |
| --- a/examples/Wave/generate_with_decay/.gitignore | |
| +++ /dev/null | |
| @@ -1 +0,0 @@ | |
| -result.wav | |
| diff --git a/examples/Wave/generate_with_decay/build.zig b/examples/Wave/generate_with_decay/build.zig | |
| deleted file mode 100644 | |
| index 3553b8f..0000000 | |
| --- a/examples/Wave/generate_with_decay/build.zig | |
| +++ /dev/null | |
| @@ -1,45 +0,0 @@ | |
| -const std = @import("std"); | |
| - | |
| -pub fn build(b: *std.Build) void { | |
| - const target = b.standardTargetOptions(.{}); | |
| - const optimize = b.standardOptimizeOption(.{}); | |
| - | |
| - // Dependencies | |
| - const lightmix = b.dependency("lightmix", .{}); | |
| - | |
| - // Executable Module | |
| - const exe_mod = b.createModule(.{ | |
| - .root_source_file = b.path("src/main.zig"), | |
| - .target = target, | |
| - .optimize = optimize, | |
| - }); | |
| - exe_mod.addImport("lightmix", lightmix.module("lightmix")); | |
| - | |
| - // Executable | |
| - const exe = b.addExecutable(.{ | |
| - .name = "generate_function", | |
| - .root_module = exe_mod, | |
| - }); | |
| - b.installArtifact(exe); | |
| - | |
| - // Rum cmd | |
| - const run_cmd = b.addRunArtifact(exe); | |
| - run_cmd.step.dependOn(b.getInstallStep()); | |
| - if (b.args) |args| { | |
| - run_cmd.addArgs(args); | |
| - } | |
| - | |
| - // Run step | |
| - const run_step = b.step("run", "Run the app"); | |
| - run_step.dependOn(&run_cmd.step); | |
| - | |
| - // Unit tests | |
| - const exe_unit_tests = b.addTest(.{ | |
| - .root_module = exe_mod, | |
| - }); | |
| - const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); | |
| - | |
| - // Test step | |
| - const test_step = b.step("test", "Run unit tests"); | |
| - test_step.dependOn(&run_exe_unit_tests.step); | |
| -} | |
| diff --git a/examples/Wave/generate_with_decay/build.zig.zon b/examples/Wave/generate_with_decay/build.zig.zon | |
| deleted file mode 100644 | |
| index 3341b6d..0000000 | |
| --- a/examples/Wave/generate_with_decay/build.zig.zon | |
| +++ /dev/null | |
| @@ -1,14 +0,0 @@ | |
| -.{ | |
| - .name = .generate_function, | |
| - .version = "0.0.0", | |
| - .fingerprint = 0x6cbe76a58f61502c, | |
| - .minimum_zig_version = "0.15.1", | |
| - | |
| - .dependencies = .{ .lightmix = .{ .path = "../../.." } }, | |
| - | |
| - .paths = .{ | |
| - "build.zig", | |
| - "build.zig.zon", | |
| - "src", | |
| - }, | |
| -} | |
| diff --git a/examples/Wave/generate_with_decay/src/assets/sine.wav b/examples/Wave/generate_with_decay/src/assets/sine.wav | |
| deleted file mode 100644 | |
| index 13cb2fb..0000000 | |
| Binary files a/examples/Wave/generate_with_decay/src/assets/sine.wav and /dev/null differ | |
| diff --git a/examples/Wave/generate_with_decay/src/main.zig b/examples/Wave/generate_with_decay/src/main.zig | |
| deleted file mode 100644 | |
| index d6bfaf6..0000000 | |
| --- a/examples/Wave/generate_with_decay/src/main.zig | |
| +++ /dev/null | |
| @@ -1,60 +0,0 @@ | |
| -const std = @import("std"); | |
| -const lightmix = @import("lightmix"); | |
| -const Wave = lightmix.Wave; | |
| - | |
| -var gpa = std.heap.GeneralPurposeAllocator(.{}){}; | |
| -const allocator = gpa.allocator(); | |
| - | |
| -pub fn main() !void { | |
| - defer _ = gpa.detectLeaks(); | |
| - | |
| - const sinewave: Wave = Wave.from_file_content(@embedFile("./assets/sine.wav"), allocator); | |
| - const decayed_wave: Wave = sinewave.filter(decay).filter(amp); | |
| - defer decayed_wave.deinit(); | |
| - | |
| - var file = try std.fs.cwd().createFile("result.wav", .{}); | |
| - defer file.close(); | |
| - | |
| - try decayed_wave.write(file); | |
| -} | |
| - | |
| -fn decay(original_wave: Wave) !Wave { | |
| - var result: std.array_list.Aligned(f32, null) = .empty; | |
| - | |
| - for (original_wave.data, 0..) |data, n| { | |
| - const i = original_wave.data.len - n; | |
| - const volume: f32 = @as(f32, @floatFromInt(i)) * (1.0 / @as(f32, @floatFromInt(original_wave.data.len))); | |
| - | |
| - const new_data = data * volume; | |
| - try result.append(original_wave.allocator, new_data); | |
| - } | |
| - | |
| - return Wave{ | |
| - .data = try result.toOwnedSlice(original_wave.allocator), | |
| - .allocator = original_wave.allocator, | |
| - | |
| - .sample_rate = original_wave.sample_rate, | |
| - .channels = original_wave.channels, | |
| - .bits = original_wave.bits, | |
| - }; | |
| -} | |
| - | |
| -fn amp(original_wave: Wave) !Wave { | |
| - var result: std.array_list.Aligned(f32, null) = .empty; | |
| - | |
| - for (original_wave.data, 0..) |data, i| { | |
| - const volume: f32 = @as(f32, @floatFromInt(i)) * (1.0 / @as(f32, @floatFromInt(original_wave.data.len))); | |
| - | |
| - const new_data = data * volume; | |
| - try result.append(original_wave.allocator, new_data); | |
| - } | |
| - | |
| - return Wave{ | |
| - .data = try result.toOwnedSlice(original_wave.allocator), | |
| - .allocator = original_wave.allocator, | |
| - | |
| - .sample_rate = original_wave.sample_rate, | |
| - .channels = original_wave.channels, | |
| - .bits = original_wave.bits, | |
| - }; | |
| -} | |
| diff --git a/examples/drum/snare_drum/.gitignore b/examples/drum/snare_drum/.gitignore | |
| deleted file mode 100644 | |
| index 374ca18..0000000 | |
| --- a/examples/drum/snare_drum/.gitignore | |
| +++ /dev/null | |
| @@ -1 +0,0 @@ | |
| -result.wav | |
| diff --git a/examples/drum/snare_drum/build.zig.zon b/examples/drum/snare_drum/build.zig.zon | |
| deleted file mode 100644 | |
| index e3c335e..0000000 | |
| --- a/examples/drum/snare_drum/build.zig.zon | |
| +++ /dev/null | |
| @@ -1,14 +0,0 @@ | |
| -.{ | |
| - .name = .generate_snare_drum, | |
| - .version = "0.0.0", | |
| - .fingerprint = 0x99c92069736f5bdb, | |
| - .minimum_zig_version = "0.15.1", | |
| - | |
| - .dependencies = .{ .lightmix = .{ .path = "../../.." } }, | |
| - | |
| - .paths = .{ | |
| - "build.zig", | |
| - "build.zig.zon", | |
| - "src", | |
| - }, | |
| -} | |
| diff --git a/examples/drum/snare_drum/src/main.zig b/examples/drum/snare_drum/src/main.zig | |
| deleted file mode 100644 | |
| index 77bd773..0000000 | |
| --- a/examples/drum/snare_drum/src/main.zig | |
| +++ /dev/null | |
| @@ -1,127 +0,0 @@ | |
| -const std = @import("std"); | |
| -const lightmix = @import("lightmix"); | |
| -const Wave = lightmix.Wave; | |
| -const allocator = std.heap.page_allocator; | |
| - | |
| -pub fn main() !void { | |
| - const snare_wave = generate_snare_wave(); | |
| - defer snare_wave.deinit(); | |
| - | |
| - var file = try std.fs.cwd().createFile("result.wav", .{}); | |
| - defer file.close(); | |
| - | |
| - try snare_wave.write(file); | |
| -} | |
| - | |
| -fn generate_snare_wave() Wave { | |
| - const pinknoise_data: [44100]f32 = generate_pink_noise(); | |
| - const pinknoise: Wave = Wave.init(pinknoise_data[0..], allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }); | |
| - | |
| - const sine_data: [44100]f32 = generate_sinewave_data(); | |
| - const sinewave: Wave = Wave.init(sine_data[0..], allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }); | |
| - | |
| - const decayed_pinknoise = pinknoise | |
| - .filter(decay) | |
| - .filter(decay) | |
| - .filter(decay) | |
| - .filter(decay) | |
| - .filter(decay) | |
| - .filter(decay); | |
| - defer decayed_pinknoise.deinit(); | |
| - | |
| - const decayed_sinewave = sinewave | |
| - .filter(decay) | |
| - .filter(decay) | |
| - .filter(half_volume) | |
| - .filter(half_volume); | |
| - defer decayed_sinewave.deinit(); | |
| - | |
| - const result = decayed_pinknoise.mix(decayed_sinewave); | |
| - return result; | |
| -} | |
| - | |
| -fn generate_pink_noise() [44100]f32 { | |
| - var result: [44100]f32 = undefined; | |
| - | |
| - var prng = std.Random.DefaultPrng.init(0); | |
| - const rand = prng.random(); | |
| - | |
| - var b0: f32 = 0.0; | |
| - var b1: f32 = 0.0; | |
| - var b2: f32 = 0.0; | |
| - | |
| - var i: usize = 0; | |
| - while (i < result.len) : (i += 1) { | |
| - const white: f32 = rand.float(f32) * 2.0 - 1.0; | |
| - | |
| - b0 = 0.99765 * b0 + white * 0.0990460; | |
| - b1 = 0.96300 * b1 + white * 0.2965164; | |
| - b2 = 0.57000 * b2 + white * 1.0526913; | |
| - | |
| - const pink: f32 = b0 + b1 + b2 + white * 0.1848; | |
| - | |
| - result[i] = pink; | |
| - } | |
| - | |
| - return result; | |
| -} | |
| - | |
| -fn generate_sinewave_data() [44100]f32 { | |
| - const sample_rate: f32 = 44100.0; | |
| - const radins_per_sec: f32 = 440.0 * 2.0 * std.math.pi; | |
| - | |
| - var result: [44100]f32 = undefined; | |
| - var i: usize = 0; | |
| - | |
| - while (i < result.len) : (i += 1) { | |
| - result[i] = 0.5 * std.math.sin(@as(f32, @floatFromInt(i)) * radins_per_sec / sample_rate); | |
| - } | |
| - | |
| - return result; | |
| -} | |
| - | |
| -fn decay(original_wave: Wave) !Wave { | |
| - var result: std.array_list.Aligned(f32, null) = .empty; | |
| - | |
| - for (original_wave.data, 0..) |data, n| { | |
| - const i = original_wave.data.len - n; | |
| - const volume: f32 = @as(f32, @floatFromInt(i)) * (1.0 / @as(f32, @floatFromInt(original_wave.data.len))); | |
| - | |
| - const new_data = data * volume; | |
| - try result.append(original_wave.allocator, new_data); | |
| - } | |
| - | |
| - return Wave{ | |
| - .data = try result.toOwnedSlice(original_wave.allocator), | |
| - .allocator = original_wave.allocator, | |
| - | |
| - .sample_rate = original_wave.sample_rate, | |
| - .channels = original_wave.channels, | |
| - .bits = original_wave.bits, | |
| - }; | |
| -} | |
| - | |
| -fn half_volume(original_wave: Wave) !Wave { | |
| - var result: std.array_list.Aligned(f32, null) = .empty; | |
| - | |
| - for (original_wave.data) |data| { | |
| - try result.append(original_wave.allocator, data / 2.0); | |
| - } | |
| - | |
| - return Wave{ | |
| - .data = try result.toOwnedSlice(original_wave.allocator), | |
| - .allocator = original_wave.allocator, | |
| - | |
| - .sample_rate = original_wave.sample_rate, | |
| - .channels = original_wave.channels, | |
| - .bits = original_wave.bits, | |
| - }; | |
| -} | |
| diff --git a/examples/guitar/acoustic_guitar/.gitignore b/examples/guitar/acoustic_guitar/.gitignore | |
| deleted file mode 100644 | |
| index 374ca18..0000000 | |
| --- a/examples/guitar/acoustic_guitar/.gitignore | |
| +++ /dev/null | |
| @@ -1 +0,0 @@ | |
| -result.wav | |
| diff --git a/examples/guitar/acoustic_guitar/build.zig.zon b/examples/guitar/acoustic_guitar/build.zig.zon | |
| deleted file mode 100644 | |
| index de2156d..0000000 | |
| --- a/examples/guitar/acoustic_guitar/build.zig.zon | |
| +++ /dev/null | |
| @@ -1,14 +0,0 @@ | |
| -.{ | |
| - .name = .generate_acoustic_guitar, | |
| - .version = "0.0.0", | |
| - .fingerprint = 0x8550528c13906134, | |
| - .minimum_zig_version = "0.15.1", | |
| - | |
| - .dependencies = .{ .lightmix = .{ .path = "../../.." } }, | |
| - | |
| - .paths = .{ | |
| - "build.zig", | |
| - "build.zig.zon", | |
| - "src", | |
| - }, | |
| -} | |
| diff --git a/examples/guitar/acoustic_guitar/src/main.zig b/examples/guitar/acoustic_guitar/src/main.zig | |
| deleted file mode 100644 | |
| index 0d80cc7..0000000 | |
| --- a/examples/guitar/acoustic_guitar/src/main.zig | |
| +++ /dev/null | |
| @@ -1,51 +0,0 @@ | |
| -const std = @import("std"); | |
| -const lightmix = @import("lightmix"); | |
| -const Wave = lightmix.Wave; | |
| -const allocator = std.heap.page_allocator; | |
| - | |
| -pub fn main() !void { | |
| - const data: [44100]f32 = generate_guitar_note(); | |
| - const guitar: Wave = Wave.init(data[0..], allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }); | |
| - defer guitar.deinit(); | |
| - | |
| - var file = try std.fs.cwd().createFile("result.wav", .{}); | |
| - defer file.close(); | |
| - | |
| - try guitar.write(file); | |
| -} | |
| - | |
| -// By ChatGPT... | |
| -// This function will create Guitar data | |
| -fn generate_guitar_note() [44100]f32 { | |
| - const freq: f32 = 440.0; | |
| - const sample_rate: f32 = 44100.0; | |
| - const decay: f32 = 0.996; | |
| - | |
| - var result: [44100]f32 = undefined; | |
| - | |
| - const period = @as(usize, @intFromFloat(sample_rate / freq)); | |
| - var buffer: [2000]f32 = undefined; | |
| - var prng = std.Random.DefaultPrng.init(0); | |
| - const rand = prng.random(); | |
| - | |
| - for (buffer[0..period]) |*val| { | |
| - val.* = rand.float(f32) * 2.0 - 1.0; | |
| - } | |
| - | |
| - // Karplus–Strong loop | |
| - var idx: usize = 0; | |
| - var i: usize = 0; | |
| - while (i < result.len) : (i += 1) { | |
| - const next_idx = (idx + 1) % period; | |
| - const avg = (buffer[idx] + buffer[next_idx]) * 0.5 * decay; | |
| - buffer[idx] = avg; | |
| - result[i] = avg; | |
| - idx = next_idx; | |
| - } | |
| - | |
| - return result; | |
| -} | |
| diff --git a/flake.lock b/flake.lock | |
| index 31a3766..99c6deb 100644 | |
| --- a/flake.lock | |
| +++ b/flake.lock | |
| @@ -2,11 +2,11 @@ | |
| "nodes": { | |
| "flake-compat": { | |
| "locked": { | |
| - "lastModified": 1747046372, | |
| - "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=", | |
| + "lastModified": 1766929376, | |
| + "narHash": "sha256-Eyg78VjLzKNG2XkqLqylSvHGAwsP8gsvmUHtZxQwPq4=", | |
| "owner": "edolstra", | |
| "repo": "flake-compat", | |
| - "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", | |
| + "rev": "236f248441a986331cda53b039e7f9fd96e03635", | |
| "type": "github" | |
| }, | |
| "original": { | |
| @@ -22,11 +22,11 @@ | |
| ] | |
| }, | |
| "locked": { | |
| - "lastModified": 1754487366, | |
| - "narHash": "sha256-pHYj8gUBapuUzKV/kN/tR3Zvqc7o6gdFB9XKXIp1SQ8=", | |
| + "lastModified": 1765835352, | |
| + "narHash": "sha256-XswHlK/Qtjasvhd1nOa1e8MgZ8GS//jBoTqWtrS1Giw=", | |
| "owner": "hercules-ci", | |
| "repo": "flake-parts", | |
| - "rev": "af66ad14b28a127c5c0f3bbb298218fc63528a18", | |
| + "rev": "a34fae9c08a15ad73f295041fec82323541400a9", | |
| "type": "github" | |
| }, | |
| "original": { | |
| @@ -37,11 +37,11 @@ | |
| }, | |
| "nixpkgs": { | |
| "locked": { | |
| - "lastModified": 1756636162, | |
| - "narHash": "sha256-mBecwgUTWRgClJYqcF+y4O1bY8PQHqeDpB+zsAn+/zA=", | |
| + "lastModified": 1766870016, | |
| + "narHash": "sha256-fHmxAesa6XNqnIkcS6+nIHuEmgd/iZSP/VXxweiEuQw=", | |
| "owner": "nixos", | |
| "repo": "nixpkgs", | |
| - "rev": "37ff64b7108517f8b6ba5705ee5085eac636a249", | |
| + "rev": "5c2bc52fb9f8c264ed6c93bd20afa2ff5e763dce", | |
| "type": "github" | |
| }, | |
| "original": { | |
| @@ -82,11 +82,11 @@ | |
| ] | |
| }, | |
| "locked": { | |
| - "lastModified": 1756662192, | |
| - "narHash": "sha256-F1oFfV51AE259I85av+MAia221XwMHCOtZCMcZLK2Jk=", | |
| + "lastModified": 1766000401, | |
| + "narHash": "sha256-+cqN4PJz9y0JQXfAK5J1drd0U05D5fcAGhzhfVrDlsI=", | |
| "owner": "numtide", | |
| "repo": "treefmt-nix", | |
| - "rev": "1aabc6c05ccbcbf4a635fb7a90400e44282f61c4", | |
| + "rev": "42d96e75aa56a3f70cab7e7dc4a32868db28e8fd", | |
| "type": "github" | |
| }, | |
| "original": { | |
| diff --git a/flake.nix b/flake.nix | |
| index aaaafb7..823ab86 100644 | |
| --- a/flake.nix | |
| +++ b/flake.nix | |
| @@ -23,6 +23,24 @@ | |
| perSystem = | |
| { pkgs, lib, ... }: | |
| + let | |
| + lightmix = pkgs.stdenv.mkDerivation { | |
| + name = "lightmix"; | |
| + src = lib.cleanSource ./.; | |
| + doCheck = true; | |
| + | |
| + nativeBuildInputs = [ | |
| + pkgs.zig_0_15.hook | |
| + ]; | |
| + | |
| + postPatch = '' | |
| + ln -s ${pkgs.callPackage ./.deps.nix { }} $ZIG_GLOBAL_CACHE_DIR/p | |
| + | |
| + # Remove NIX_CFLAGS_COMPILE because zig cannot understand it | |
| + unset NIX_CFLAGS_COMPILE | |
| + ''; | |
| + }; | |
| + in | |
| { | |
| treefmt = { | |
| projectRootFile = ".git/config"; | |
| @@ -39,14 +57,22 @@ | |
| # Markdown | |
| programs.mdformat.enable = true; | |
| + settings.formatter.mdformat.excludes = [ "CODE_OF_CONDUCT.md" ]; | |
| + }; | |
| + | |
| + packages = { | |
| + inherit lightmix; | |
| + default = lightmix; | |
| + }; | |
| + | |
| + checks = { | |
| + inherit lightmix; | |
| }; | |
| devShells.default = pkgs.mkShell { | |
| nativeBuildInputs = [ | |
| # Compiler | |
| pkgs.zig_0_15 | |
| - | |
| - # C library manager | |
| pkgs.pkg-config | |
| # LSP | |
| @@ -55,12 +81,21 @@ | |
| # Music Player | |
| pkgs.sox # Use this command as: `play result.wav` | |
| + | |
| + # zon2nix | |
| + pkgs.zon2nix | |
| ]; | |
| - buildInputs = [ | |
| - pkgs.portaudio | |
| - pkgs.libsndfile | |
| + buildInputs = lib.optionals pkgs.stdenv.isLinux [ | |
| + pkgs.alsa-lib | |
| + pkgs.pulseaudio | |
| + pkgs.pipewire | |
| ]; | |
| + | |
| + shellHook = '' | |
| + # Remove NIX_CFLAGS_COMPILE because zig cannot understand it | |
| + unset NIX_CFLAGS_COMPILE | |
| + ''; | |
| }; | |
| }; | |
| }; | |
| diff --git a/src/composer.zig b/src/composer.zig | |
| index efaf6dd..1e8cc39 100644 | |
| --- a/src/composer.zig | |
| +++ b/src/composer.zig | |
| @@ -1,340 +1,437 @@ | |
| -//! # Composer | |
| -//! | |
| -//! Composer is used to combine multipul `Wave`. | |
| - | |
| const std = @import("std"); | |
| const testing = std.testing; | |
| const Wave = @import("./root.zig").Wave; | |
| -const Self = @This(); | |
| - | |
| -pub const WaveInfo = struct { | |
| - wave: Wave, | |
| - start_point: usize, | |
| +/// Composer type function: Creates a Composer type for the specified sample type. | |
| +/// | |
| +/// Composer allows sequencing and overlaying multiple Wave instances in time to create | |
| +/// complex audio arrangements. | |
| +/// | |
| +/// ## Type Parameter | |
| +/// - `T`: The sample data type (typically f64, f80, or f128 for floating-point audio) | |
| +/// | |
| +/// ## Usage | |
| +/// ```zig | |
| +/// const Composer = lightmix.Composer; | |
| +/// const composer = Composer(f64).init(allocator, .{ | |
| +/// .sample_rate = 44100, | |
| +/// .channels = 1, | |
| +/// }); | |
| +/// defer composer.deinit(); | |
| +/// | |
| +/// // Append waves at specific time points | |
| +/// const composed = composer | |
| +/// .append(.{ .wave = wave1, .start_point = 0 }) | |
| +/// .append(.{ .wave = wave2, .start_point = 22050 }); | |
| +/// defer composed.deinit(); | |
| +/// | |
| +/// // Finalize to create the mixed result | |
| +/// const result = composed.finalize(.{}); | |
| +/// defer result.deinit(); | |
| +/// ``` | |
| +pub fn inner(comptime T: type) type { | |
| + return struct { | |
| + info: []const WaveInfo, | |
| + allocator: std.mem.Allocator, | |
| + sample_rate: u32, | |
| + channels: u16, | |
| + | |
| + const Self = @This(); | |
| + | |
| + /// Information about a wave to be placed at a specific time point. | |
| + pub const WaveInfo = struct { | |
| + wave: Wave(T), | |
| + start_point: usize, | |
| + | |
| + fn to_wave(self: WaveInfo, allocator: std.mem.Allocator) std.mem.Allocator.Error!Wave(T) { | |
| + var padding_samples: []T = try allocator.alloc(T, self.start_point); | |
| + | |
| + for (0..padding_samples.len) |i| { | |
| + padding_samples[i] = 0.0; | |
| + } | |
| + | |
| + const slices: []const []const T = &[_][]const T{ padding_samples, self.wave.samples }; | |
| + const samples = std.mem.concat(allocator, T, slices); | |
| + | |
| + const result: Wave(T) = try Wave(T).init(samples, allocator, .{ | |
| + .sample_rate = self.wave.sample_rate, | |
| + .channels = self.wave.channels, | |
| + }); | |
| + | |
| + return result; | |
| + } | |
| + }; | |
| - fn to_wave(self: WaveInfo, allocator: std.mem.Allocator) Wave { | |
| - var padding_data: []f32 = allocator.alloc(f32, self.start_point) catch @panic("Out of memory"); | |
| + /// Options for initializing a Composer instance. | |
| + pub const InitOptions = struct { | |
| + sample_rate: u32, | |
| + channels: u16, | |
| + }; | |
| - for (0..padding_data.len) |i| { | |
| - padding_data[i] = 0.0; | |
| + /// Creates a new empty Composer instance. | |
| + /// | |
| + /// ## Parameters | |
| + /// - `allocator`: Memory allocator for internal allocations | |
| + /// - `options`: Initialization options (sample rate and channel count) | |
| + /// | |
| + /// ## Returns | |
| + /// A new Composer instance with no waves | |
| + pub fn init( | |
| + allocator: std.mem.Allocator, | |
| + options: InitOptions, | |
| + ) Self { | |
| + return Self{ | |
| + .allocator = allocator, | |
| + .info = &[_]WaveInfo{}, | |
| + | |
| + .sample_rate = options.sample_rate, | |
| + .channels = options.channels, | |
| + }; | |
| } | |
| - const slices: []const []const f32 = &[_][]const f32{ padding_data, self.wave.data }; | |
| - const data = std.mem.concat(allocator, f32, slices); | |
| - | |
| - const result: Wave = Wave.init(data, allocator, .{ | |
| - .sample_rate = self.wave.sample_rate, | |
| - .channels = self.wave.channels, | |
| - .bits = self.wave.bits, | |
| - }); | |
| - | |
| - return result; | |
| - } | |
| -}; | |
| - | |
| -info: []const WaveInfo, | |
| -allocator: std.mem.Allocator, | |
| - | |
| -sample_rate: usize, | |
| -channels: usize, | |
| -bits: usize, | |
| - | |
| -pub const initOptions = struct { | |
| - sample_rate: usize, | |
| - channels: usize, | |
| - bits: usize, | |
| -}; | |
| - | |
| -pub fn init(allocator: std.mem.Allocator, options: initOptions) Self { | |
| - return Self{ | |
| - .allocator = allocator, | |
| - .info = &[_]WaveInfo{}, | |
| - | |
| - .sample_rate = options.sample_rate, | |
| - .channels = options.channels, | |
| - .bits = options.bits, | |
| - }; | |
| -} | |
| - | |
| -pub fn deinit(self: Self) void { | |
| - self.allocator.free(self.info); | |
| -} | |
| - | |
| -pub fn init_with(info: []const WaveInfo, allocator: std.mem.Allocator, options: initOptions) Self { | |
| - var list: std.array_list.Aligned(WaveInfo, null) = .empty; | |
| - list.appendSlice(allocator, info) catch @panic("Out of memory"); | |
| - | |
| - return Self{ | |
| - .allocator = allocator, | |
| - .info = list.toOwnedSlice(allocator) catch @panic("Out of memory"), | |
| - | |
| - .sample_rate = options.sample_rate, | |
| - .channels = options.channels, | |
| - .bits = options.bits, | |
| - }; | |
| -} | |
| - | |
| -pub fn append(self: Self, waveinfo: WaveInfo) Self { | |
| - var d: std.array_list.Aligned(WaveInfo, null) = .empty; | |
| - d.appendSlice(self.allocator, self.info) catch @panic("Out of memory"); | |
| - d.append(self.allocator, waveinfo) catch @panic("Out of memory"); | |
| - | |
| - const result: []const WaveInfo = d.toOwnedSlice(self.allocator) catch @panic("Out of memory"); | |
| - | |
| - return Self{ | |
| - .allocator = self.allocator, | |
| - .info = result, | |
| - | |
| - .sample_rate = self.sample_rate, | |
| - .channels = self.channels, | |
| - .bits = self.bits, | |
| - }; | |
| -} | |
| - | |
| -pub fn appendSlice(self: Self, append_list: []const WaveInfo) Self { | |
| - var d: std.array_list.Aligned(WaveInfo, null) = .empty; | |
| - d.appendSlice(self.allocator, self.info) catch @panic("Out of memory"); | |
| - d.appendSlice(self.allocator, append_list) catch @panic("Out of memory"); | |
| - | |
| - const result: []const WaveInfo = d.toOwnedSlice(self.allocator) catch @panic("Out of memory"); | |
| - | |
| - return Self{ | |
| - .allocator = self.allocator, | |
| - .info = result, | |
| - | |
| - .sample_rate = self.sample_rate, | |
| - .channels = self.channels, | |
| - .bits = self.bits, | |
| - }; | |
| -} | |
| - | |
| -pub fn finalize(self: Self) Wave { | |
| - var end_point: usize = 0; | |
| - | |
| - // Calculate the length for emitted wave | |
| - for (self.info) |waveinfo| { | |
| - const ep = waveinfo.start_point + waveinfo.wave.data.len; | |
| + /// Frees the memory allocated for the composer's internal data. | |
| + /// | |
| + /// Note: This does not free the individual Wave instances stored in WaveInfo. | |
| + /// Those must be freed separately by the caller. | |
| + pub fn deinit(self: Self) void { | |
| + self.allocator.free(self.info); | |
| + } | |
| - if (end_point < ep) | |
| - end_point = ep; | |
| - } | |
| + /// Creates a new Composer instance initialized with the provided wave information. | |
| + /// | |
| + /// ## Parameters | |
| + /// - `info`: Slice of WaveInfo structures describing waves and their start points | |
| + /// - `allocator`: Memory allocator for internal allocations | |
| + /// - `options`: Initialization options (sample rate and channel count) | |
| + /// | |
| + /// ## Returns | |
| + /// A new Composer instance containing the provided waves | |
| + pub fn init_with( | |
| + info: []const WaveInfo, | |
| + allocator: std.mem.Allocator, | |
| + options: InitOptions, | |
| + ) std.mem.Allocator.Error!Self { | |
| + var list: std.array_list.Aligned(WaveInfo, null) = .empty; | |
| + try list.appendSlice(allocator, info); | |
| + | |
| + return Self{ | |
| + .allocator = allocator, | |
| + .info = try list.toOwnedSlice(allocator), | |
| + | |
| + .sample_rate = options.sample_rate, | |
| + .channels = options.channels, | |
| + }; | |
| + } | |
| - var padded_waveinfo_list: std.array_list.Aligned(WaveInfo, null) = .empty; | |
| - defer padded_waveinfo_list.deinit(self.allocator); | |
| + /// Appends a single wave to the composition. This method modifies the composer in-place. | |
| + /// | |
| + /// ## Parameters | |
| + /// - `self`: Pointer to the composer to modify (will be updated in-place) | |
| + /// - `waveinfo`: Information about the wave and when it should start | |
| + /// | |
| + /// ## Memory Management | |
| + /// The old internal array is freed, and a new one is allocated with the | |
| + /// appended wave. The composer pointer is updated to reference the new data. | |
| + /// | |
| + /// ## Example | |
| + /// ``` | |
| + /// var composer: Composer(f64) = Composer(f64).init(allocator, .{ | |
| + /// .sample_rate = 44100, | |
| + /// .channels = 1, | |
| + /// }); | |
| + /// defer composer.deinit(); | |
| + /// | |
| + /// // Append modifies composer in-place | |
| + /// try composer.append(.{ .wave = wave1, .start_point = 0 }); | |
| + /// try composer.append(.{ .wave = wave2, .start_point = 44100 }); | |
| + /// ``` | |
| + pub fn append(self: *Self, waveinfo: WaveInfo) std.mem.Allocator.Error!void { | |
| + var d: std.array_list.Aligned(WaveInfo, null) = .empty; | |
| + try d.appendSlice(self.allocator, self.info); | |
| + try d.append(self.allocator, waveinfo); | |
| + | |
| + const result: Self = Self{ | |
| + .allocator = self.allocator, | |
| + .info = try d.toOwnedSlice(self.allocator), | |
| + | |
| + .sample_rate = self.sample_rate, | |
| + .channels = self.channels, | |
| + }; | |
| + | |
| + self.deinit(); // Free the old one now | |
| + self.* = result; // Then copy the new one (result variable) | |
| + } | |
| - // Filter each WaveInfo to append padding both of start and last | |
| - for (self.info) |waveinfo| { | |
| - const padded_at_start: []const f32 = padding_for_start(waveinfo.wave.data, waveinfo.start_point, self.allocator); | |
| - defer self.allocator.free(padded_at_start); | |
| + /// Appends multiple waves to the composition. This method modifies the composer in-place. | |
| + /// | |
| + /// ## Parameters | |
| + /// - `self`: Pointer to the composer to modify (will be updated in-place) | |
| + /// - `append_list`: Slice of WaveInfo structures to append | |
| + /// | |
| + /// ## Memory Management | |
| + /// The old internal array is freed, and a new one is allocated with the | |
| + /// appended wave. The composer pointer is updated to reference the new data. | |
| + pub fn appendSlice(self: *Self, append_list: []const WaveInfo) std.mem.Allocator.Error!void { | |
| + var d: std.array_list.Aligned(WaveInfo, null) = .empty; | |
| + try d.appendSlice(self.allocator, self.info); | |
| + try d.appendSlice(self.allocator, append_list); | |
| + | |
| + const result: Self = Self{ | |
| + .allocator = self.allocator, | |
| + .info = try d.toOwnedSlice(self.allocator), | |
| + | |
| + .sample_rate = self.sample_rate, | |
| + .channels = self.channels, | |
| + }; | |
| + | |
| + self.deinit(); | |
| + self.* = result; | |
| + } | |
| - const padded_at_start_and_last: []const f32 = padding_for_last(padded_at_start, end_point, self.allocator); | |
| - defer self.allocator.free(padded_at_start_and_last); | |
| + /// Finalizes the composition by mixing all waves together. | |
| + /// | |
| + /// This creates a single Wave by: | |
| + /// 1. Calculating the total length needed | |
| + /// 2. Padding each wave to align with its start_point | |
| + /// 3. Mixing all waves together using the provided mixer function | |
| + /// | |
| + /// ## Parameters | |
| + /// - `self`: The composer containing all the waves to mix | |
| + /// - `options`: Mixing options (includes the mixer function) | |
| + /// | |
| + /// ## Returns | |
| + /// A new Wave containing the final mixed composition | |
| + /// | |
| + /// ## Performance Notes | |
| + /// Memory usage is proportional to: `number_of_waves × total_length × sample_size` | |
| + /// Each wave is temporarily padded to the full composition length before mixing. | |
| + /// Consider using this for up to ~100 overlapping waves on typical systems. | |
| + pub fn finalize(self: Self, options: Wave(T).mixOptions) std.mem.Allocator.Error!Wave(T) { | |
| + var end_point: usize = 0; | |
| + | |
| + // Calculate the length for emitted wave | |
| + for (self.info) |waveinfo| { | |
| + const ep = waveinfo.start_point + waveinfo.wave.samples.len; | |
| + | |
| + if (end_point < ep) | |
| + end_point = ep; | |
| + } | |
| + | |
| + var padded_waveinfo_list: std.array_list.Aligned(WaveInfo, null) = .empty; | |
| + defer padded_waveinfo_list.deinit(self.allocator); | |
| + | |
| + // Filter each WaveInfo to append padding both of start and last | |
| + for (self.info) |waveinfo| { | |
| + const padded_at_start: []const T = try padding_for_start(waveinfo.wave.samples, waveinfo.start_point, self.allocator); | |
| + defer self.allocator.free(padded_at_start); | |
| + | |
| + const padded_at_start_and_last: []const T = try padding_for_last(padded_at_start, end_point, self.allocator); | |
| + defer self.allocator.free(padded_at_start_and_last); | |
| + | |
| + const wave = try Wave(T).init(padded_at_start_and_last, self.allocator, .{ | |
| + .sample_rate = self.sample_rate, | |
| + .channels = self.channels, | |
| + }); | |
| + | |
| + const wi: WaveInfo = WaveInfo{ | |
| + .wave = wave, | |
| + .start_point = waveinfo.start_point, | |
| + }; | |
| + | |
| + try padded_waveinfo_list.append(self.allocator, wi); | |
| + } | |
| + | |
| + const padded_waveinfo_slice: []const WaveInfo = try padded_waveinfo_list.toOwnedSlice(self.allocator); | |
| + defer self.allocator.free(padded_waveinfo_slice); | |
| + | |
| + const empty_samples: []const T = try generate_soundless_samples(end_point, self.allocator); | |
| + defer self.allocator.free(empty_samples); | |
| + | |
| + var result = try Wave(T).init(empty_samples, self.allocator, .{ | |
| + .sample_rate = self.sample_rate, | |
| + .channels = self.channels, | |
| + }); | |
| + | |
| + for (padded_waveinfo_slice) |waveinfo| { | |
| + const wave = try result.mix(waveinfo.wave, options); | |
| + result.deinit(); | |
| + waveinfo.wave.deinit(); | |
| + result = wave; | |
| + } | |
| + | |
| + return result; | |
| + } | |
| - const wave: Wave = Wave.init(padded_at_start_and_last, self.allocator, .{ | |
| - .sample_rate = self.sample_rate, | |
| - .channels = self.channels, | |
| - .bits = self.bits, | |
| - }); | |
| + fn padding_for_start(samples: []const T, start_point: usize, allocator: std.mem.Allocator) std.mem.Allocator.Error![]const T { | |
| + const padding_length: usize = start_point; | |
| + var padding: std.array_list.Aligned(T, null) = .empty; | |
| + defer padding.deinit(allocator); | |
| - const wi: WaveInfo = WaveInfo{ | |
| - .wave = wave, | |
| - .start_point = waveinfo.start_point, | |
| - }; | |
| + // Append padding | |
| + for (0..padding_length) |_| | |
| + try padding.append(allocator, 0.0); | |
| - padded_waveinfo_list.append(self.allocator, wi) catch @panic("Out of memory"); | |
| - } | |
| + // Append samples slice | |
| + try padding.appendSlice(allocator, samples); | |
| - const padded_waveinfo_slice: []const WaveInfo = padded_waveinfo_list.toOwnedSlice(self.allocator) catch @panic("Out of memory"); | |
| - defer self.allocator.free(padded_waveinfo_slice); | |
| + const result: []const T = try padding.toOwnedSlice(allocator); | |
| - const empty_data: []const f32 = generate_soundless_data(end_point, self.allocator); | |
| - defer self.allocator.free(empty_data); | |
| + return result; | |
| + } | |
| - var result: Wave = Wave.init(empty_data, self.allocator, .{ | |
| - .sample_rate = self.sample_rate, | |
| - .channels = self.channels, | |
| - .bits = self.bits, | |
| - }); | |
| + fn padding_for_last(samples: []const T, end_point: usize, allocator: std.mem.Allocator) std.mem.Allocator.Error![]const T { | |
| + std.debug.assert(samples.len <= end_point); | |
| - for (padded_waveinfo_slice) |waveinfo| { | |
| - const wave = result.mix(waveinfo.wave); | |
| - result.deinit(); | |
| - waveinfo.wave.deinit(); | |
| - result = wave; | |
| - } | |
| + const padding_length: usize = end_point - samples.len; | |
| + var padding: std.array_list.Aligned(T, null) = .empty; | |
| + defer padding.deinit(allocator); | |
| - return result; | |
| -} | |
| + // Append samples slice | |
| + try padding.appendSlice(allocator, samples); | |
| -fn padding_for_start(data: []const f32, start_point: usize, allocator: std.mem.Allocator) []const f32 { | |
| - const padding_length: usize = start_point; | |
| - var padding: std.array_list.Aligned(f32, null) = .empty; | |
| - defer padding.deinit(allocator); | |
| + // Append padding | |
| + for (0..padding_length) |_| | |
| + try padding.append(allocator, 0.0); | |
| - // Append padding | |
| - for (0..padding_length) |_| | |
| - padding.append(allocator, 0.0) catch @panic("Out of memory"); | |
| + const result: []const T = try padding.toOwnedSlice(allocator); | |
| - // Append data slice | |
| - padding.appendSlice(allocator, data) catch @panic("Out of memory"); | |
| + return result; | |
| + } | |
| - const result: []const f32 = padding.toOwnedSlice(allocator) catch @panic("Out of memory"); | |
| + fn generate_soundless_samples(length: usize, allocator: std.mem.Allocator) std.mem.Allocator.Error![]const T { | |
| + var list: std.array_list.Aligned(T, null) = .empty; | |
| + defer list.deinit(allocator); | |
| - return result; | |
| -} | |
| + // Append empty wave | |
| + for (0..length) |_| | |
| + try list.append(allocator, 0.0); | |
| -fn padding_for_last(data: []const f32, end_point: usize, allocator: std.mem.Allocator) []const f32 { | |
| - std.debug.assert(data.len <= end_point); | |
| + const result: []const T = try list.toOwnedSlice(allocator); | |
| - const padding_length: usize = end_point - data.len; | |
| - var padding: std.array_list.Aligned(f32, null) = .empty; | |
| - defer padding.deinit(allocator); | |
| + return result; | |
| + } | |
| - // Append data slice | |
| - padding.appendSlice(allocator, data) catch @panic("Out of memory"); | |
| + test "padding_for_start" { | |
| + const allocator = testing.allocator; | |
| + const samples: []const T = &[_]T{ 1.0, 1.0 }; | |
| + const start_point: usize = 10; | |
| - // Append padding | |
| - for (0..padding_length) |_| | |
| - padding.append(allocator, 0.0) catch @panic("Out of memory"); | |
| + const result: []const T = try padding_for_start(samples, start_point, allocator); | |
| + defer allocator.free(result); | |
| - const result: []const f32 = padding.toOwnedSlice(allocator) catch @panic("Out of memory"); | |
| + try testing.expectEqual(samples.len + start_point, result.len); | |
| - return result; | |
| -} | |
| + const expected: []const T = &[_]T{ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0 }; | |
| + for (0..result.len) |i| { | |
| + try testing.expectApproxEqAbs(expected[i], result[i], 0.001); | |
| + } | |
| + } | |
| -fn generate_soundless_data(length: usize, allocator: std.mem.Allocator) []const f32 { | |
| - var list: std.array_list.Aligned(f32, null) = .empty; | |
| - defer list.deinit(allocator); | |
| + test "init & deinit" { | |
| + const allocator = testing.allocator; | |
| + const composer = Self.init(allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + defer composer.deinit(); | |
| + } | |
| - // Append empty wave | |
| - for (0..length) |_| | |
| - list.append(allocator, 0.0) catch @panic("Out of memory"); | |
| + test "init_with & deinit" { | |
| + const allocator = testing.allocator; | |
| + var reader = std.Io.Reader.fixed(@embedFile("./assets/sine.wav")); | |
| - const result: []const f32 = list.toOwnedSlice(allocator) catch @panic("Out of memory"); | |
| + const wave = try Wave(T).read(.wav, allocator, &reader); | |
| + defer wave.deinit(); | |
| - return result; | |
| -} | |
| + const info: []const WaveInfo = &[_]WaveInfo{ .{ .wave = wave, .start_point = 0 }, .{ .wave = wave, .start_point = 0 } }; | |
| -test "padding_for_start" { | |
| - const allocator = testing.allocator; | |
| - const data: []const f32 = &[_]f32{ 1.0, 1.0 }; | |
| - const start_point: usize = 10; | |
| + const composer = try Self.init_with(info, allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + defer composer.deinit(); | |
| + } | |
| - const result: []const f32 = padding_for_start(data, start_point, allocator); | |
| - defer allocator.free(result); | |
| + test "append" { | |
| + const allocator = testing.allocator; | |
| + var reader = std.Io.Reader.fixed(@embedFile("./assets/sine.wav")); | |
| + var composer = Self.init(allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + defer composer.deinit(); | |
| - try testing.expectEqual(data.len + start_point, result.len); | |
| + const wave = try Wave(T).read(.wav, allocator, &reader); | |
| + defer wave.deinit(); | |
| - const expected: []const f32 = &[_]f32{ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0 }; | |
| - for (0..result.len) |i| { | |
| - try testing.expectApproxEqAbs(expected[i], result[i], 0.001); | |
| - } | |
| -} | |
| + try composer.append(.{ .wave = wave, .start_point = 0 }); | |
| -test "init & deinit" { | |
| - const allocator = testing.allocator; | |
| - const composer = Self.init(allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }); | |
| - defer composer.deinit(); | |
| -} | |
| + try testing.expectEqualSlices(WaveInfo, composer.info, &[_]WaveInfo{.{ .wave = wave, .start_point = 0 }}); | |
| + } | |
| -test "init_with & deinit" { | |
| - const allocator = testing.allocator; | |
| + test "appendSlice" { | |
| + const allocator = testing.allocator; | |
| + var composer = Self.init(allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + defer composer.deinit(); | |
| - const wave = Wave.from_file_content(@embedFile("./assets/sine.wav"), allocator); | |
| - defer wave.deinit(); | |
| + var reader = std.Io.Reader.fixed(@embedFile("./assets/sine.wav")); | |
| - const info: []const WaveInfo = &[_]WaveInfo{ .{ .wave = wave, .start_point = 0 }, .{ .wave = wave, .start_point = 0 } }; | |
| + const wave = try Wave(T).read(.wav, allocator, &reader); | |
| + defer wave.deinit(); | |
| - const composer = Self.init_with(info, allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }); | |
| - defer composer.deinit(); | |
| -} | |
| + var append_list: std.array_list.Aligned(WaveInfo, null) = .empty; | |
| + defer append_list.deinit(allocator); | |
| + try append_list.append(allocator, .{ .wave = wave, .start_point = 0 }); | |
| + try append_list.append(allocator, .{ .wave = wave, .start_point = 0 }); | |
| + try composer.appendSlice(append_list.items); | |
| -test "append" { | |
| - const allocator = testing.allocator; | |
| - const composer = Self.init(allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }); | |
| - defer composer.deinit(); | |
| + try testing.expectEqualSlices(WaveInfo, composer.info, &[_]WaveInfo{ .{ .wave = wave, .start_point = 0 }, .{ .wave = wave, .start_point = 0 } }); | |
| + } | |
| - const wave = Wave.from_file_content(@embedFile("./assets/sine.wav"), allocator); | |
| - defer wave.deinit(); | |
| + test "finalize" { | |
| + const allocator = testing.allocator; | |
| + var composer = Self.init(allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + defer composer.deinit(); | |
| - const appended_composer = composer.append(.{ .wave = wave, .start_point = 0 }); | |
| - defer appended_composer.deinit(); | |
| + var samples: []T = try allocator.alloc(T, 44100); | |
| + defer allocator.free(samples); | |
| - try testing.expectEqualSlices(WaveInfo, appended_composer.info, &[_]WaveInfo{.{ .wave = wave, .start_point = 0 }}); | |
| -} | |
| + for (0..samples.len) |i| { | |
| + samples[i] = 1.0; | |
| + } | |
| -test "appendSlice" { | |
| - const allocator = testing.allocator; | |
| - const composer = Self.init(allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }); | |
| - defer composer.deinit(); | |
| + const wave = try Wave(T).init(samples, allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + defer wave.deinit(); | |
| - const wave = Wave.from_file_content(@embedFile("./assets/sine.wav"), allocator); | |
| - defer wave.deinit(); | |
| + var append_list: std.array_list.Aligned(WaveInfo, null) = .empty; | |
| + defer append_list.deinit(allocator); | |
| + try append_list.append(allocator, .{ .wave = wave, .start_point = 0 }); | |
| + try append_list.append(allocator, .{ .wave = wave, .start_point = 44100 }); | |
| + try composer.appendSlice(append_list.items); | |
| - var append_list: std.array_list.Aligned(WaveInfo, null) = .empty; | |
| - defer append_list.deinit(allocator); | |
| - try append_list.append(allocator, .{ .wave = wave, .start_point = 0 }); | |
| - try append_list.append(allocator, .{ .wave = wave, .start_point = 0 }); | |
| + const result = try composer.finalize(.{}); | |
| + defer result.deinit(); | |
| - const appended_composer = composer.appendSlice(append_list.items); | |
| - defer appended_composer.deinit(); | |
| + try testing.expectEqual(result.samples.len, 88200); | |
| - try testing.expectEqualSlices(WaveInfo, appended_composer.info, &[_]WaveInfo{ .{ .wave = wave, .start_point = 0 }, .{ .wave = wave, .start_point = 0 } }); | |
| + try testing.expectEqual(result.sample_rate, 44100); | |
| + try testing.expectEqual(result.channels, 1); | |
| + } | |
| + }; | |
| } | |
| -test "finalize" { | |
| - const allocator = testing.allocator; | |
| - const composer = Self.init(allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }); | |
| - defer composer.deinit(); | |
| - | |
| - var data: []f32 = try allocator.alloc(f32, 44100); | |
| - defer allocator.free(data); | |
| - | |
| - for (0..data.len) |i| { | |
| - data[i] = 1.0; | |
| - } | |
| - | |
| - const wave = Wave.init(data, allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }); | |
| - defer wave.deinit(); | |
| - | |
| - var append_list: std.array_list.Aligned(WaveInfo, null) = .empty; | |
| - defer append_list.deinit(allocator); | |
| - try append_list.append(allocator, .{ .wave = wave, .start_point = 0 }); | |
| - try append_list.append(allocator, .{ .wave = wave, .start_point = 44100 }); | |
| - | |
| - const appended_composer = composer.appendSlice(append_list.items); | |
| - defer appended_composer.deinit(); | |
| - | |
| - const result: Wave = appended_composer.finalize(); | |
| - defer result.deinit(); | |
| - | |
| - try testing.expectEqual(result.data.len, 88200); | |
| - | |
| - try testing.expectEqual(result.sample_rate, 44100); | |
| - try testing.expectEqual(result.channels, 1); | |
| - try testing.expectEqual(result.bits, 16); | |
| +test "Run tests for each samples' type" { | |
| + _ = inner(f128); | |
| + _ = inner(f80); | |
| + _ = inner(f64); | |
| + // _ = inner(f32); zigggwavvv 0.2.1 cannot use f32 as samples' type | |
| } | |
| diff --git a/src/root.zig b/src/root.zig | |
| index d6d5ee2..71aabd6 100644 | |
| --- a/src/root.zig | |
| +++ b/src/root.zig | |
| @@ -1,9 +1,60 @@ | |
| -//! # lightmix | |
| +//! # lightmix - Audio Synthesis and Manipulation Library | |
| //! | |
| -//! `lightmix` is an audio processing library written by Zig-lang. | |
| +//! lightmix is a Zig library for audio waveform generation, manipulation, and composition. | |
| +//! It provides type-safe, generic interfaces for working with audio data. | |
| +//! | |
| +//! ## Core Types | |
| +//! | |
| +//! ### Wave | |
| +//! The `Wave` type function creates audio waveform types for different sample formats. | |
| +//! It supports operations like mixing, filtering, and reading/writing WAV files. | |
| +//! | |
| +//! ### Composer | |
| +//! The `Composer` type function creates types for sequencing and overlaying multiple | |
| +//! Wave instances in time to create complex audio arrangements. | |
| +//! | |
| +//! ## Example Usage | |
| +//! | |
| +//! ```zig | |
| +//! const std = @import("std"); | |
| +//! const lightmix = @import("lightmix"); | |
| +//! const Wave = lightmix.Wave; | |
| +//! const Composer = lightmix.Composer; | |
| +//! | |
| +//! pub fn main() !void { | |
| +//! const allocator = std.heap.page_allocator; | |
| +//! | |
| +//! // Create a simple sine wave | |
| +//! var samples: [44100]f64 = undefined; | |
| +//! for (0..samples.len) |i| { | |
| +//! const t = @as(f64, @floatFromInt(i)) / 44100.0; | |
| +//! samples[i] = @sin(t * 440.0 * 2.0 * std.math.pi); | |
| +//! } | |
| +//! | |
| +//! // Wave(T).init() creates a deep copy of samples | |
| +//! const wave: Wave(f64) = Wave(f64).init(&samples, allocator, .{ | |
| +//! .sample_rate = 44100, | |
| +//! .channels = 1, | |
| +//! }); | |
| +//! defer wave.deinit(); | |
| +//! | |
| +//! // Create a composition with multiple waves | |
| +//! var composer: Composer(f64) = Composer(f64).init(allocator, .{ | |
| +//! .sample_rate = 44100, | |
| +//! .channels = 1, | |
| +//! }); | |
| +//! defer composer.deinit(); | |
| +//! | |
| +//! // Composer(T).append() modifies the Composer in-place | |
| +//! composer.append(.{ .wave = wave, .start_point = 0 }); | |
| +//! | |
| +//! const result: Wave(f64) = try composer.finalize(.{}); | |
| +//! defer result.deinit(); | |
| +//! } | |
| +//! ``` | |
| -pub const Wave = @import("./wave.zig"); | |
| -pub const Composer = @import("./composer.zig"); | |
| +pub const Wave = @import("./wave.zig").inner; | |
| +pub const Composer = @import("./composer.zig").inner; | |
| test "Import tests" { | |
| _ = @import("./wave.zig"); | |
| diff --git a/src/wave.zig b/src/wave.zig | |
| index 31d3521..4dca70e 100644 | |
| --- a/src/wave.zig | |
| +++ b/src/wave.zig | |
| @@ -1,537 +1,954 @@ | |
| -//! # Wave | |
| -//! | |
| -//! Contains a wave data. | |
| - | |
| const std = @import("std"); | |
| -const build_options = @import("build_options"); | |
| -const lightmix_wav = @import("lightmix_wav"); | |
| -const known_folders = @import("known-folders"); | |
| +const zigggwavvv = @import("zigggwavvv"); | |
| +const zaudio = @import("zaudio"); | |
| const testing = std.testing; | |
| -const Self = @This(); | |
| - | |
| -/// A wave data, expressed by array contains f32. | |
| -data: []const f32, | |
| - | |
| -/// An allocator | |
| -allocator: std.mem.Allocator, | |
| - | |
| -/// This wave's sample rate. | |
| -sample_rate: usize, | |
| - | |
| -/// This wave's channels. | |
| -channels: usize, | |
| - | |
| -/// This wave's bits. | |
| -bits: usize, | |
| - | |
| -pub const initOptions = struct { | |
| - sample_rate: usize, | |
| - channels: usize, | |
| - bits: usize, | |
| -}; | |
| +/// Wave type function: Creates a Wave type for the specified sample type. | |
| +/// | |
| +/// Wave represents audio waveform data with methods for manipulation, mixing, and I/O. | |
| +/// | |
| +/// ## Type Parameter | |
| +/// - `T`: The sample data type (typically f64, f80, or f128 for floating-point audio) | |
| +/// | |
| +/// ## Usage | |
| +/// ```zig | |
| +/// const Wave = lightmix.Wave; | |
| +/// const wave = Wave(f64).init(samples, allocator, .{ | |
| +/// .sample_rate = 44100, | |
| +/// .channels = 1, | |
| +/// }); | |
| +/// defer wave.deinit(); | |
| +/// ``` | |
| +pub fn inner(comptime T: type) type { | |
| + return struct { | |
| + const Self = @This(); | |
| + | |
| + samples: []const T, | |
| + allocator: std.mem.Allocator, | |
| + sample_rate: u32, | |
| + channels: u16, | |
| + | |
| + /// Supported audio file formats for reading wave data. | |
| + pub const LowLevelInterfaces = enum { | |
| + wav, | |
| + | |
| + /// Reads wave data using the specified file format. | |
| + /// | |
| + /// ## Parameters | |
| + /// - `self`: The file format to use for decoding | |
| + /// - `allocator`: Memory allocator for sample data | |
| + /// - `reader`: A reader interface providing the raw file bytes | |
| + /// | |
| + /// ## Returns | |
| + /// A `LowLevelWave` containing the decoded samples, sample rate, and channel count | |
| + /// | |
| + /// ## Errors | |
| + /// Returns errors from the underlying format decoder or allocation failures | |
| + pub fn read(self: LowLevelInterfaces, allocator: std.mem.Allocator, reader: anytype) anyerror!LowLevelWave { | |
| + return switch (self) { | |
| + .wav => { | |
| + const v = try zigggwavvv.Wave(T).read(allocator, reader); | |
| + | |
| + return .{ | |
| + .samples = v.samples, | |
| + .sample_rate = v.sample_rate, | |
| + .channels = v.channels, | |
| + }; | |
| + }, | |
| + }; | |
| + } | |
| -/// Initialize a Wave with wave data (`[]const f32`). | |
| -pub fn init(data: []const f32, allocator: std.mem.Allocator, options: initOptions) Self { | |
| - const owned_data = allocator.alloc(f32, data.len) catch @panic("Out of memory"); | |
| - @memcpy(owned_data, data); | |
| + /// Writes wave data using the specified file format. | |
| + /// | |
| + /// ## Parameters | |
| + /// - `self`: The file format to use for encoding | |
| + /// - `wave`: The wave instance to write | |
| + /// - `writer`: A writer interface for the output bytes | |
| + /// - `options`: Format-specific write options (see `writeOptions`) | |
| + /// | |
| + /// ## Errors | |
| + /// Returns errors from the underlying format encoder or I/O failures | |
| + pub fn write(self: LowLevelInterfaces, wave: Self, writer: anytype, options: writeOptions(self)) anyerror!void { | |
| + switch (self) { | |
| + .wav => { | |
| + const zigggwavvv_wave = zigggwavvv.Wave(T).init(.{ | |
| + .format_code = options.format_code, | |
| + .sample_rate = wave.sample_rate, | |
| + .channels = wave.channels, | |
| + .bits = options.bits, | |
| + .samples = try wave.allocator.dupe(T, wave.samples), | |
| + }); | |
| + defer zigggwavvv_wave.deinit(wave.allocator); | |
| + | |
| + try zigggwavvv_wave.write(writer, .{ | |
| + .allocator = wave.allocator, | |
| + .use_fact = options.use_fact, | |
| + .use_peak = options.use_peak, | |
| + .peak_timestamp = options.peak_timestamp, | |
| + }); | |
| + }, | |
| + } | |
| + } | |
| - return Self{ | |
| - .data = owned_data, | |
| - .allocator = allocator, | |
| + /// Returns the format-specific options type for `write`. | |
| + /// | |
| + /// ## Parameters | |
| + /// - `interface`: The file format whose options type to return | |
| + /// | |
| + /// ## Returns | |
| + /// The options struct type corresponding to the given format | |
| + pub fn writeOptions(interface: LowLevelInterfaces) type { | |
| + return switch (interface) { | |
| + .wav => writeWavOptions, | |
| + }; | |
| + } | |
| - .sample_rate = options.sample_rate, | |
| - .channels = options.channels, | |
| - .bits = options.bits, | |
| - }; | |
| -} | |
| + /// Options for writing wave data to a WAV file. | |
| + pub const writeWavOptions = struct { | |
| + /// Whether to include a `fact` chunk in the output file | |
| + use_fact: bool = false, | |
| + /// Whether to include a `PEAK` chunk in the output file | |
| + use_peak: bool = false, | |
| + /// Timestamp value written into the `PEAK` chunk (only used when `use_peak` is true) | |
| + peak_timestamp: u32 = 0, | |
| + | |
| + /// Bits per sample (e.g. 16 or 24) | |
| + bits: u16, | |
| + /// Audio format code (e.g. PCM or IEEE float) | |
| + format_code: zigggwavvv.FormatCode, | |
| + }; | |
| + | |
| + /// Raw wave data returned by low-level format decoders. | |
| + pub const LowLevelWave = struct { | |
| + samples: []const T, | |
| + sample_rate: u32, | |
| + channels: u16, | |
| + }; | |
| + }; | |
| -/// Mix a wave and the other wave. | |
| -/// The each wave data's length, sample_rate, channels, and bits must be same. | |
| -/// That's because we cannot ajust the timing for every users which the each wave should be played. | |
| -pub fn mix(self: Self, other: Self) Self { | |
| - std.debug.assert(self.data.len == other.data.len); | |
| - std.debug.assert(self.sample_rate == other.sample_rate); | |
| - std.debug.assert(self.channels == other.channels); | |
| - std.debug.assert(self.bits == other.bits); | |
| - | |
| - if (self.data.len == 0) | |
| - return Self{ | |
| - .data = &[_]f32{}, | |
| - .allocator = self.allocator, | |
| - | |
| - .sample_rate = self.sample_rate, | |
| - .channels = self.channels, | |
| - .bits = self.bits, | |
| + /// Options for initializing a Wave instance. | |
| + pub const InitOptions = struct { | |
| + sample_rate: u32, | |
| + channels: u16, | |
| }; | |
| - var data: std.array_list.Aligned(f32, null) = .empty; | |
| + /// Creates a new Wave instance from the provided sample data. | |
| + /// | |
| + /// The function creates a deep copy of the sample data, so the caller | |
| + /// retains ownership of the original samples slice. | |
| + /// | |
| + /// ## Parameters | |
| + /// - `samples`: Slice of sample data to copy | |
| + /// - `allocator`: Memory allocator for internal allocations | |
| + /// - `options`: Initialization options (sample rate and channel count) | |
| + /// | |
| + /// ## Returns | |
| + /// A new Wave instance containing a copy of the sample data | |
| + /// | |
| + /// ## Errors | |
| + /// - Allocator error (errors.OutOfMemory) | |
| + pub fn init( | |
| + samples: []const T, | |
| + allocator: std.mem.Allocator, | |
| + options: InitOptions, | |
| + ) std.mem.Allocator.Error!Self { | |
| + const owned_samples = try allocator.alloc(T, samples.len); | |
| + @memcpy(owned_samples, samples); | |
| + | |
| + return Self{ | |
| + .samples = owned_samples, | |
| + .allocator = allocator, | |
| + | |
| + .sample_rate = options.sample_rate, | |
| + .channels = options.channels, | |
| + }; | |
| + } | |
| - for (0..self.data.len) |i| { | |
| - data.append(self.allocator, self.data[i] + other.data[i]) catch @panic("Out of memory"); | |
| - } | |
| + /// Options for mixing two waves together. | |
| + pub const mixOptions = struct { | |
| + mixer: fn (T, T) T = default_mixing_expression, | |
| + }; | |
| - const result: []const f32 = data.toOwnedSlice(self.allocator) catch @panic("Out of memory"); | |
| + /// Default mixing function that adds two samples together. | |
| + /// | |
| + /// ## Parameters | |
| + /// - `left`: Sample value from the first wave | |
| + /// - `right`: Sample value from the second wave | |
| + /// | |
| + /// ## Returns | |
| + /// The sum of the two sample values | |
| + pub fn default_mixing_expression(left: T, right: T) T { | |
| + const result: T = left + right; | |
| + return result; | |
| + } | |
| - return Self{ | |
| - .data = result, | |
| - .allocator = self.allocator, | |
| + /// Mixes this wave with another wave, combining their samples. | |
| + /// | |
| + /// Both waves must have the same length, sample rate, and channel count. | |
| + /// The mixing is performed sample-by-sample using the provided mixer function. | |
| + /// | |
| + /// ## Parameters | |
| + /// - `self`: The first wave to mix | |
| + /// - `other`: The second wave to mix | |
| + /// - `options`: Mixing options (includes the mixer function) | |
| + /// | |
| + /// ## Returns | |
| + /// A new Wave containing the mixed result | |
| + /// | |
| + /// ## Errors | |
| + /// - Allocator error (errors.OutOfMemory) | |
| + pub fn mix(self: Self, other: Self, options: mixOptions) std.mem.Allocator.Error!Self { | |
| + std.debug.assert(self.samples.len == other.samples.len); | |
| + std.debug.assert(self.sample_rate == other.sample_rate); | |
| + std.debug.assert(self.channels == other.channels); | |
| + | |
| + if (self.samples.len == 0) | |
| + return Self{ | |
| + .samples = &[_]T{}, | |
| + .allocator = self.allocator, | |
| + | |
| + .sample_rate = self.sample_rate, | |
| + .channels = self.channels, | |
| + }; | |
| + | |
| + var samples: std.array_list.Aligned(T, null) = .empty; | |
| + | |
| + for (0..self.samples.len) |i| { | |
| + const left: T = self.samples[i]; | |
| + const right: T = other.samples[i]; | |
| + const result: T = options.mixer(left, right); | |
| + | |
| + try samples.append(self.allocator, result); | |
| + } | |
| - .sample_rate = self.sample_rate, | |
| - .channels = self.channels, | |
| - .bits = self.bits, | |
| - }; | |
| -} | |
| + const result: []const T = try samples.toOwnedSlice(self.allocator); | |
| -pub fn fill_zero_to_end(self: Self, start: usize, end: usize) !Self { | |
| - // Initialization | |
| - var result: std.array_list.Aligned(f32, null) = .empty; | |
| - try result.appendSlice(self.allocator, self.data); | |
| + return Self{ | |
| + .samples = result, | |
| + .allocator = self.allocator, | |
| - const delete_count: usize = result.items.len - start; | |
| + .sample_rate = self.sample_rate, | |
| + .channels = self.channels, | |
| + }; | |
| + } | |
| - for (0..delete_count) |_| { | |
| - _ = result.pop(); | |
| - } | |
| + /// Separates the wave into two parts at a specified point. | |
| + /// | |
| + /// This function splits the wave's samples into two new Wave instances: | |
| + /// one containing samples from the start to the separation point, | |
| + /// and another containing samples from the separation point to the end. | |
| + /// | |
| + /// ## Parameters | |
| + /// - `self`: The wave to separate | |
| + /// - `options`: Separation options (allocator and separation point) | |
| + /// | |
| + /// ## Returns | |
| + /// A SeparateResult containing two Wave instances (initial and terminal) | |
| + /// | |
| + /// ## Errors | |
| + /// - `SeparatingZeroLengthWave`: If the wave has no samples | |
| + /// - `TooBigSeparatePoint`: If the separation point exceeds the wave length | |
| + /// - `OutOfMemory`: Allocator error when memory allocation fails | |
| + pub fn separate( | |
| + self: Self, | |
| + options: SeparateOptions, | |
| + ) (SeparateErrors || std.mem.Allocator.Error)!SeparateResult { | |
| + if (self.samples.len == 0) | |
| + return error.SeparatingZeroLengthWave; | |
| + | |
| + if (self.samples.len < options.separate_point) | |
| + return error.TooBigSeparatePoint; | |
| + | |
| + const initial_len = options.separate_point; | |
| + const terminal_len = self.samples.len - options.separate_point; | |
| + var initial: []T = try options.allocator.alloc(T, initial_len); | |
| + var terminal: []T = try options.allocator.alloc(T, terminal_len); | |
| + | |
| + for (0..initial.len) |i| { | |
| + initial[i] = self.samples[i]; | |
| + } | |
| + for (0..terminal.len) |i| { | |
| + terminal[i] = self.samples[initial_len + i]; | |
| + } | |
| - std.debug.assert(start == result.items.len); | |
| + const result = SeparateResult{ | |
| + .initial = Self{ | |
| + .allocator = options.allocator, | |
| + .samples = initial, | |
| + .sample_rate = self.sample_rate, | |
| + .channels = self.channels, | |
| + }, | |
| + .terminal = Self{ | |
| + .allocator = options.allocator, | |
| + .samples = terminal, | |
| + .sample_rate = self.sample_rate, | |
| + .channels = self.channels, | |
| + }, | |
| + }; | |
| - for (delete_count..end) |_| { | |
| - try result.append(self.allocator, 0.0); | |
| - } | |
| + return result; | |
| + } | |
| - std.debug.assert(result.items.len == end); | |
| + /// Options for separating a wave into two parts. | |
| + pub const SeparateOptions = struct { | |
| + /// Memory allocator for the new wave instances | |
| + allocator: std.mem.Allocator, | |
| + /// Sample index at which to split the wave (exclusive for initial, inclusive for terminal) | |
| + separate_point: usize, | |
| + }; | |
| - return Self{ | |
| - .data = try result.toOwnedSlice(self.allocator), | |
| - .allocator = self.allocator, | |
| + /// Result of separating a wave into two parts. | |
| + pub const SeparateResult = struct { | |
| + /// The first part of the wave (from start to separation point) | |
| + initial: Self, | |
| + /// The second part of the wave (from separation point to end) | |
| + terminal: Self, | |
| + }; | |
| - .sample_rate = self.sample_rate, | |
| - .channels = self.channels, | |
| - .bits = self.bits, | |
| - }; | |
| -} | |
| + /// Errors that can occur when separating a wave. | |
| + pub const SeparateErrors = error{ | |
| + /// Attempted to separate a wave with zero samples | |
| + SeparatingZeroLengthWave, | |
| + /// The separation point exceeds the wave's sample length | |
| + TooBigSeparatePoint, | |
| + }; | |
| -/// Free the Wave struct | |
| -pub fn deinit(self: Self) void { | |
| - self.allocator.free(self.data); | |
| -} | |
| + /// Truncates the wave at a start point and fills with zeros to the end point. | |
| + /// | |
| + /// This is useful for creating silence or padding at the end of a wave. | |
| + /// | |
| + /// ## Parameters | |
| + /// - `self`: The wave to modify | |
| + /// - `start`: Sample index where truncation begins | |
| + /// - `end`: Sample index where the new wave ends (filled with zeros) | |
| + /// | |
| + /// ## Returns | |
| + /// A new Wave with samples from 0 to `start`, then zeros from `start` to `end` | |
| + /// | |
| + /// ## Errors | |
| + /// - Allocator error (errors.OutOfMemory) | |
| + pub fn fill_zero_to_end(self: Self, start: usize, end: usize) std.mem.Allocator.Error!Self { | |
| + // Initialization | |
| + var result: std.array_list.Aligned(T, null) = .empty; | |
| + try result.appendSlice(self.allocator, self.samples); | |
| + | |
| + const delete_count: usize = result.items.len - start; | |
| + | |
| + for (0..delete_count) |_| { | |
| + _ = result.pop(); | |
| + } | |
| -/// Create Wave from binary data | |
| -/// The data argument can receive a binary data, as @embedFile("./assets/sine.wav") | |
| -/// Therefore you can use this function as: | |
| -/// const wave = Wave.from_file_content(@embedFile("./asset/sine.wav"), allocator); | |
| -pub fn from_file_content(content: []const u8, allocator: std.mem.Allocator) Self { | |
| - var stream = std.io.fixedBufferStream(content); | |
| - var decoder = lightmix_wav.decoder(stream.reader()) catch |err| { | |
| - std.debug.print("In lightmix_wav\n", .{}); | |
| - std.debug.print("{any}\n", .{err}); | |
| - @panic("Failed to create decoder"); | |
| - }; | |
| + std.debug.assert(start == result.items.len); | |
| - var buf: [64]f32 = undefined; | |
| - var arraylist: std.array_list.Aligned(f32, null) = .empty; | |
| + for (delete_count..end) |_| { | |
| + try result.append(self.allocator, 0.0); | |
| + } | |
| - while (true) { | |
| - // Read samples as f32. Channels are interleaved. | |
| - const samples_read = decoder.read(f32, &buf) catch |err| { | |
| - std.debug.print("In lightmix_wav\n", .{}); | |
| - std.debug.print("{any}\n", .{err}); | |
| - @panic("Failed to read samples from decoder"); | |
| - }; | |
| + std.debug.assert(result.items.len == end); | |
| - // < ------ Do something with samples in buf. ------ > | |
| - arraylist.appendSlice(allocator, &buf) catch @panic("Out of memory"); | |
| + return Self{ | |
| + .samples = try result.toOwnedSlice(self.allocator), | |
| + .allocator = self.allocator, | |
| - if (samples_read < buf.len) { | |
| - break; | |
| + .sample_rate = self.sample_rate, | |
| + .channels = self.channels, | |
| + }; | |
| + } | |
| + /// Frees the memory allocated for the wave's sample data. | |
| + /// | |
| + /// This must be called when you're done with a Wave instance to avoid memory leaks. | |
| + pub fn deinit(self: Self) void { | |
| + self.allocator.free(self.samples); | |
| } | |
| - } | |
| - | |
| - const result: []const f32 = arraylist.toOwnedSlice(allocator) catch @panic("Out of memory"); | |
| - | |
| - const sample_rate: usize = decoder.sampleRate(); | |
| - const channels: usize = decoder.channels(); | |
| - const bits: usize = decoder.bits(); | |
| - | |
| - return Self{ | |
| - .data = result, | |
| - .allocator = allocator, | |
| - .sample_rate = sample_rate, | |
| - .channels = channels, | |
| - .bits = bits, | |
| - }; | |
| -} | |
| + /// Reads wave data from a file using the specified format. | |
| + /// | |
| + /// ## Parameters | |
| + /// - `file_extension`: The file format to use for decoding (e.g. `.wav`) | |
| + /// - `allocator`: Memory allocator for sample data | |
| + /// - `reader`: A reader interface for reading the audio file data | |
| + /// | |
| + /// ## Returns | |
| + /// A new Wave instance containing the audio data from the file | |
| + /// | |
| + /// ## Errors | |
| + /// Returns errors from the underlying format decoder or allocation failures | |
| + pub fn read( | |
| + file_extension: LowLevelInterfaces, | |
| + allocator: std.mem.Allocator, | |
| + reader: anytype, | |
| + ) anyerror!Self { | |
| + const lowlevel_wave = try file_extension.read(allocator, reader); | |
| + | |
| + return Self{ | |
| + .samples = lowlevel_wave.samples, | |
| + .allocator = allocator, | |
| + .sample_rate = lowlevel_wave.sample_rate, | |
| + .channels = lowlevel_wave.channels, | |
| + }; | |
| + } | |
| + /// Writes wave data to a file writer using the specified format. | |
| + /// | |
| + /// ## Parameters | |
| + /// - `self`: The wave to write | |
| + /// - `file_extension`: The file format to use for encoding (e.g. `.wav`) | |
| + /// - `writer`: A writer interface for writing the audio file data | |
| + /// - `options`: Format-specific write options (e.g. bit depth, format code) | |
| + /// | |
| + /// ## Errors | |
| + /// Returns errors from the underlying format encoder or I/O failures | |
| + pub fn write(self: Self, file_extension: LowLevelInterfaces, writer: anytype, options: LowLevelInterfaces.writeOptions(file_extension)) anyerror!void { | |
| + try file_extension.write(self, writer, options); | |
| + } | |
| -/// Writes down the wave data to `std.fs.File`. | |
| -pub fn write(self: Self, file: std.fs.File) !void { | |
| - var encoder = try lightmix_wav.encoder(i16, file, self.sample_rate, self.channels); | |
| - try encoder.write(f32, self.data); | |
| - try encoder.finalize(); | |
| -} | |
| + /// Applies a filter function with custom arguments to the wave. | |
| + /// | |
| + /// The original wave is automatically freed after the filter is applied. | |
| + /// This enables chaining multiple filters together efficiently. | |
| + /// | |
| + /// ## Parameters | |
| + /// - `self`: The wave to filter (will be freed after filtering) | |
| + /// - `args_type`: The type of the arguments to pass to the filter function | |
| + /// - `filter_fn`: The filter function to apply | |
| + /// - `args`: Arguments to pass to the filter function | |
| + /// | |
| + /// ## Errors | |
| + /// Returns any error produced by `filter_fn` or allocation failures | |
| + /// | |
| + /// ## Example Usage | |
| + /// ```zig | |
| + /// const DecayWithDebugPrintArgs = struct { | |
| + /// string: []const u8, | |
| + /// }; | |
| + /// | |
| + /// /// Decay filter: Creates a linear fade-out effect | |
| + /// /// The volume decreases from 100% to 0% over the duration of the wave | |
| + /// fn decay_with_debug_print(comptime T: type, original_wave: Wave(T), args: DecayWithDebugPrintArgs) !Wave(T) { | |
| + /// var result_list: std.array_list.Aligned(T, null) = .empty; | |
| + /// defer result_list.deinit(original_wave.allocator); | |
| + /// | |
| + /// // Process each sample, applying a decay factor | |
| + /// for (original_wave.samples, 0..) |sample, n| { | |
| + /// // Calculate how far from the end we are | |
| + /// const remaining_samples = original_wave.samples.len - n; | |
| + /// | |
| + /// // Decay factor: 1.0 at start, 0.0 at end | |
| + /// const decay_factor = @as(T, @floatFromInt(remaining_samples)) / | |
| + /// @as(T, @floatFromInt(original_wave.samples.len)); | |
| + /// | |
| + /// // Apply the decay to the sample | |
| + /// const decayed_sample = sample * decay_factor; | |
| + /// try result_list.append(original_wave.allocator, decayed_sample); | |
| + /// } | |
| + /// | |
| + /// // A example usage of args | |
| + /// // This means that you can accept any arguments to change this filter's effects | |
| + /// std.debug.print("A message from args: {s}\n", .{args.string}); | |
| + /// | |
| + /// // Return a new Wave with the filtered samples | |
| + /// return Wave(T).init(result_list.items, original_wave.allocator, .{ | |
| + /// .sample_rate = original_wave.sample_rate, | |
| + /// .channels = original_wave.channels, | |
| + /// }); | |
| + /// } | |
| + /// | |
| + /// // Sine wave generation | |
| + /// var samples: [44100]f64 = undefined; | |
| + /// for (0..samples.len) |i| { | |
| + /// const t = @as(f64, @floatFromInt(i)) / sample_rate; | |
| + /// samples[i] = 0.5 * @sin(radians_per_sec * t); | |
| + /// } | |
| + /// | |
| + /// const wave: Wave(f64) = Wave(f64).init(samples[0..], allocator, .{ | |
| + /// .sample_rate = 44100, | |
| + /// .channels = 1, | |
| + /// }); | |
| + /// | |
| + /// const decayed_wave: Wave(f64) = wave.filter(decay); | |
| + /// defer decayed_wave.deinit(); | |
| + /// ``` | |
| + pub fn filter_with( | |
| + self: *Self, | |
| + comptime args_type: type, | |
| + comptime filter_fn: anytype, | |
| + args: args_type, | |
| + ) anyerror!void { | |
| + const result: Self = try filter_fn(T, self.*, args); | |
| + | |
| + // To destroy original samples array | |
| + // If we don't do this, we may catch some memory leaks by not to free original samples array | |
| + self.deinit(); | |
| + | |
| + // Assign the result into self | |
| + self.* = result; | |
| + } | |
| -/// Filters a zig function. | |
| -/// Use this function as `wave.filter_with(Args, your_filter, .{ args = 0 });` | |
| -/// This function uses self.deinit() to avoid the memory leaks by not free the data arrays | |
| -pub fn filter_with( | |
| - self: Self, | |
| - comptime args_type: type, | |
| - filter_fn: fn (self: Self, args: args_type) anyerror!Self, | |
| - args: args_type, | |
| -) Self { | |
| - // To destroy original data array | |
| - // If we don't do this, we may catch some memory leaks by not to free original data array | |
| - defer self.deinit(); | |
| - | |
| - const result: Self = filter_fn(self, args) catch |err| { | |
| - std.debug.print("{any}\n", .{err}); | |
| - @panic("Error happened in filter_with function..."); | |
| - }; | |
| + /// Applies a filter function to the wave. | |
| + /// | |
| + /// The original wave is automatically freed after the filter is applied. | |
| + /// This enables chaining multiple filters together efficiently. | |
| + /// | |
| + /// ## Parameters | |
| + /// - `self`: The wave to filter (will be freed after filtering) | |
| + /// - `filter_fn`: The filter function to apply | |
| + /// | |
| + /// ## Errors | |
| + /// Returns any error produced by `filter_fn` or allocation failures | |
| + /// | |
| + /// ## Example Usage | |
| + /// ```zig | |
| + /// /// Decay filter: Creates a linear fade-out effect | |
| + /// /// The volume decreases from 100% to 0% over the duration of the wave | |
| + /// fn decay(comptime T: type, original_wave: Wave(T)) !Wave(T) { | |
| + /// var result_list: std.array_list.Aligned(T, null) = .empty; | |
| + /// defer result_list.deinit(original_wave.allocator); | |
| + /// | |
| + /// // Process each sample, applying a decay factor | |
| + /// for (original_wave.samples, 0..) |sample, n| { | |
| + /// // Calculate how far from the end we are | |
| + /// const remaining_samples = original_wave.samples.len - n; | |
| + /// | |
| + /// // Decay factor: 1.0 at start, 0.0 at end | |
| + /// const decay_factor = @as(T, @floatFromInt(remaining_samples)) / | |
| + /// @as(T, @floatFromInt(original_wave.samples.len)); | |
| + /// | |
| + /// // Apply the decay to the sample | |
| + /// const decayed_sample = sample * decay_factor; | |
| + /// try result_list.append(original_wave.allocator, decayed_sample); | |
| + /// } | |
| + /// | |
| + /// // Return a new Wave with the filtered samples | |
| + /// return Wave(T).init(result_list.items, original_wave.allocator, .{ | |
| + /// .sample_rate = original_wave.sample_rate, | |
| + /// .channels = original_wave.channels, | |
| + /// }); | |
| + /// } | |
| + /// | |
| + /// // Sine wave generation | |
| + /// var samples: [44100]f64 = undefined; | |
| + /// for (0..samples.len) |i| { | |
| + /// const t = @as(f64, @floatFromInt(i)) / sample_rate; | |
| + /// samples[i] = 0.5 * @sin(radians_per_sec * t); | |
| + /// } | |
| + /// | |
| + /// const wave: Wave(f64) = Wave(f64).init(samples[0..], allocator, .{ | |
| + /// .sample_rate = 44100, | |
| + /// .channels = 1, | |
| + /// }); | |
| + /// | |
| + /// const decayed_wave: Wave(f64) = wave.filter(decay); | |
| + /// defer decayed_wave.deinit(); | |
| + /// ``` | |
| + pub fn filter( | |
| + self: *Self, | |
| + comptime filter_fn: anytype, | |
| + ) anyerror!void { | |
| + const result: Self = try filter_fn(T, self.*); | |
| + | |
| + // To destroy original samples array | |
| + // If we don't do this, we may catch some memory leaks by not to free original samples array | |
| + self.deinit(); | |
| + | |
| + // Assign the result into self | |
| + self.* = result; | |
| + } | |
| - return result; | |
| -} | |
| + /// Plays the wave audio through the system audio output. | |
| + /// | |
| + /// Initializes the audio engine, converts samples to f32, and blocks until | |
| + /// playback completes. | |
| + /// | |
| + /// ## Parameters | |
| + /// - `self`: The wave to play | |
| + /// | |
| + /// ## Errors | |
| + /// Returns errors from the audio engine initialization or playback | |
| + pub fn play(self: Self) anyerror!void { | |
| + const allocator = self.allocator; | |
| + | |
| + zaudio.init(allocator); | |
| + defer zaudio.deinit(); | |
| + | |
| + var engine: *zaudio.Engine = try zaudio.Engine.create(null); | |
| + defer engine.destroy(); | |
| + | |
| + const samples = try allocator.alloc(f32, self.samples.len); | |
| + defer allocator.free(samples); | |
| + | |
| + for (self.samples, 0..) |orig_sample, i| { | |
| + samples[i] = @as(f32, @floatCast(orig_sample)); | |
| + } | |
| -/// Filters a zig function. | |
| -/// Use this function as `wave.filter(your_filter);` | |
| -/// This function uses self.deinit() to avoid the memory leaks by not free the data arrays | |
| -pub fn filter( | |
| - self: Self, | |
| - filter_fn: fn (self: Self) anyerror!Self, | |
| -) Self { | |
| - // To destroy original data array | |
| - // If we don't do this, we may catch some memory leaks by not to free original data array | |
| - defer self.deinit(); | |
| - | |
| - const result: Self = filter_fn(self) catch |err| { | |
| - std.debug.print("{any}\n", .{err}); | |
| - @panic("Error happened in filter function..."); | |
| - }; | |
| + var buffer_config = zaudio.AudioBuffer.Config.init(.float32, self.channels, samples.len / self.channels, samples.ptr); | |
| + buffer_config.sample_rate = self.sample_rate; | |
| + const buffer = try zaudio.AudioBuffer.create(buffer_config); | |
| + defer buffer.destroy(); | |
| + const sound = try engine.createSoundFromDataSource(buffer.asDataSourceMut(), .{}, null); | |
| + defer sound.destroy(); | |
| - return result; | |
| -} | |
| + try sound.start(); | |
| -/// Plays the wave instantly. | |
| -/// You must enable `with_debug_features` in `build.zig`. | |
| -pub fn debug_play(self: Self) !void { | |
| - if (!build_options.with_debug_features) | |
| - @panic("Wave.debug_play called without 'with_debug_features' flag. Please turn on the flag."); | |
| + while (!sound.isAtEnd()) { | |
| + std.Thread.sleep(10 * std.time.ns_per_ms); | |
| + } | |
| + } | |
| - const cache_dir: std.fs.Dir = try known_folders.open(self.allocator, .cache, .{}) orelse @panic("XDG cache dir is null"); | |
| - cache_dir.access("lightmix", .{}) catch { | |
| - try cache_dir.makeDir("lightmix"); | |
| - }; | |
| + fn tmp_file(allocator: std.mem.Allocator) !struct { std.fs.File, std.testing.TmpDir, []const u8 } { | |
| + const timestamp: i64 = std.time.timestamp(); | |
| + const timestamp_str: []const u8 = try std.fmt.allocPrint(allocator, "{s}", .{timestamp}); | |
| - const path: []const u8 = blk: { | |
| - const cache_dir_path: []const u8 = try known_folders.getPath(self.allocator, .cache) orelse @panic("XDG cache dir is null"); | |
| - const now: std.time.Instant = try std.time.Instant.now(); | |
| + const tmp = std.testing.tmpDir(.{}); | |
| + const file = try tmp.dir.createFile(timestamp_str ++ ".wav", .{}); | |
| - const result: []const u8 = try std.fmt.allocPrint(self.allocator, "{s}/lightmix/result-{d}.{d}.wav", .{ cache_dir_path, now.timestamp.sec, now.timestamp.nsec }); | |
| - break :blk result; | |
| - }; | |
| - defer self.allocator.free(path); | |
| + return .{ file, tmp, timestamp_str }; | |
| + } | |
| - const file = try cache_dir.createFile(path, .{}); | |
| + /// Options for the `play` function (reserved for future use). | |
| + pub const PlayOptions = struct { | |
| + do_cleanup: bool, | |
| + }; | |
| - try self.write(file); | |
| - std.debug.print("Wave file saved to: {s}\n", .{path}); | |
| + test "read & deinit" { | |
| + const allocator = testing.allocator; | |
| + var reader = std.Io.Reader.fixed(@embedFile("./assets/sine.wav")); | |
| + const wave = try Self.read(.wav, allocator, &reader); | |
| + defer wave.deinit(); | |
| - // Debug-play | |
| + try testing.expectApproxEqAbs(wave.samples[0], 0.0, 0.00001); | |
| + try testing.expectApproxEqAbs(wave.samples[1], 0.05011139255958739, 0.00001); | |
| + try testing.expectApproxEqAbs(wave.samples[2], 0.1000396740623188, 0.00001); | |
| - std.debug.print("Debug-playing...\n", .{}); | |
| + try testing.expectEqual(wave.sample_rate, 44100); | |
| + try testing.expectEqual(wave.channels, 1); | |
| + } | |
| - const c_headers = @cImport({ | |
| - @cInclude("portaudio.h"); | |
| - @cInclude("sndfile.h"); | |
| - }); | |
| + test "init & deinit" { | |
| + const allocator = testing.allocator; | |
| - const c_path: [*c]const u8 = try self.allocator.dupeZ(u8, path); | |
| + const generator = struct { | |
| + fn sinewave() [44100]T { | |
| + const sample_rate: T = 44100.0; | |
| + const radins_per_sec: T = 440.0 * 2.0 * std.math.pi; | |
| - var sfInfo: c_headers.SF_INFO = undefined; | |
| - const sndFile: ?*c_headers.SNDFILE = c_headers.sf_open(c_path, c_headers.SFM_READ, &sfInfo); | |
| - defer _ = c_headers.sf_close(sndFile); | |
| + var result: [44100]T = undefined; | |
| + var i: usize = 0; | |
| - if (c_headers.Pa_Initialize() != c_headers.paNoError) | |
| - return error.PortaudioInitFailed; | |
| + while (i < result.len) : (i += 1) { | |
| + result[i] = 0.5 * std.math.sin(@as(T, @floatFromInt(i)) * radins_per_sec / sample_rate); | |
| + } | |
| - defer _ = c_headers.Pa_Terminate(); | |
| + return result; | |
| + } | |
| + }; | |
| - var stream: ?*c_headers.PaStream = undefined; | |
| + const samples: [44100]T = generator.sinewave(); | |
| + const wave = try Self.init(samples[0..], allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + defer wave.deinit(); | |
| - if (self.bits != 16) | |
| - @panic("Debug-playing the others of 16 bits' Wave is not implemented yet."); | |
| + try testing.expectEqual(wave.sample_rate, 44100); | |
| + try testing.expectEqual(wave.channels, 1); | |
| + } | |
| - if (c_headers.Pa_OpenDefaultStream( | |
| - &stream, | |
| - 0, | |
| - @intCast(self.channels), | |
| - c_headers.paFloat32, | |
| - @as(f64, @floatFromInt(self.sample_rate)), | |
| - 256, | |
| - null, | |
| - null, | |
| - ) != c_headers.paNoError) | |
| - return error.PortaudioFailedToOpenStream; | |
| + test "mix" { | |
| + const allocator = testing.allocator; | |
| + const generator = struct { | |
| + fn sinewave() [44100]T { | |
| + const sample_rate: T = 44100.0; | |
| + const radins_per_sec: T = 440.0 * 2.0 * std.math.pi; | |
| - defer _ = c_headers.Pa_CloseStream(stream.?); | |
| + var result: [44100]T = undefined; | |
| + var i: usize = 0; | |
| - if (c_headers.Pa_StartStream(stream) != c_headers.paNoError) | |
| - return error.PortaudioStartStreamFailed; | |
| + while (i < result.len) : (i += 1) { | |
| + result[i] = 0.5 * std.math.sin(@as(T, @floatFromInt(i)) * radins_per_sec / sample_rate); | |
| + } | |
| - defer _ = c_headers.Pa_StopStream(stream); | |
| + return result; | |
| + } | |
| + }; | |
| - var buffer: [256 * 2]f32 = undefined; // stereo | |
| - var framesRead: i64 = undefined; | |
| + const samples: [44100]T = generator.sinewave(); | |
| + const wave = try Self.init(samples[0..], allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + defer wave.deinit(); | |
| - while (true) { | |
| - framesRead = c_headers.sf_readf_float(sndFile, &buffer, 256); | |
| + const result: Self = try wave.mix(wave, .{}); | |
| + defer result.deinit(); | |
| - if (c_headers.Pa_WriteStream(stream, &buffer, @intCast(framesRead)) != c_headers.paNoError) | |
| - break; | |
| + try testing.expectEqual(wave.sample_rate, 44100); | |
| + try testing.expectEqual(wave.channels, 1); | |
| - if (framesRead <= 0) | |
| - break; | |
| - } | |
| + try testing.expectApproxEqAbs(result.samples[0], 0.0, 0.00001); | |
| + try testing.expectApproxEqAbs(result.samples[1], 0.06264832417874369, 0.00001); | |
| + try testing.expectApproxEqAbs(result.samples[2], 0.1250505236945281, 0.00001); | |
| + } | |
| - std.debug.print("Debug-playing finished.\n", .{}); | |
| -} | |
| + test "fill_zero_to_end" { | |
| + const allocator = testing.allocator; | |
| + const generator = struct { | |
| + fn sinewave() [44100]T { | |
| + const sample_rate: T = 44100.0; | |
| + const radins_per_sec: T = 440.0 * 2.0 * std.math.pi; | |
| + | |
| + var result: [44100]T = undefined; | |
| + var i: usize = 0; | |
| + | |
| + while (i < result.len) : (i += 1) { | |
| + result[i] = 0.5 * std.math.sin(@as(T, @floatFromInt(i)) * radins_per_sec / sample_rate); | |
| + } | |
| + | |
| + return result; | |
| + } | |
| + }; | |
| + | |
| + const samples: [44100]T = generator.sinewave(); | |
| + const wave = try Self.init(samples[0..], allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + defer wave.deinit(); | |
| + | |
| + const filled_wave: Self = try wave.fill_zero_to_end(22050, 44100); | |
| + defer filled_wave.deinit(); | |
| + | |
| + try testing.expectEqual(filled_wave.sample_rate, 44100); | |
| + try testing.expectEqual(filled_wave.channels, 1); | |
| + | |
| + try testing.expectApproxEqAbs(filled_wave.samples[0], 0.0, 0.00001); | |
| + try testing.expectApproxEqAbs(filled_wave.samples[1], 0.031324162089371846, 0.00001); | |
| + try testing.expectApproxEqAbs(filled_wave.samples[2], 0.06252526184726405, 0.00001); | |
| + | |
| + try testing.expectApproxEqAbs(filled_wave.samples[22049], -0.03132416208941618, 0.00001); | |
| + try testing.expectApproxEqAbs(filled_wave.samples[22050], 0.0, 0.00001); | |
| + try testing.expectApproxEqAbs(filled_wave.samples[22051], 0.0, 0.00001); | |
| + try testing.expectApproxEqAbs(filled_wave.samples[44099], 0.0, 0.00001); | |
| + } | |
| -const DebugPlayErrors = error{ | |
| - PortaudioInitFailed, | |
| - PortaudioFailedToOpenStream, | |
| - PortaudioStartStreamFailed, | |
| -}; | |
| + test "filter_with" { | |
| + const allocator = testing.allocator; | |
| + const samples: []const T = &[_]T{}; | |
| -test "from_file_content & deinit" { | |
| - const allocator = testing.allocator; | |
| - const wave = Self.from_file_content(@embedFile("./assets/sine.wav"), allocator); | |
| - defer wave.deinit(); | |
| + var wave = try Self.init(samples[0..], allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + try wave.filter_with(ArgsForTesting, test_filter_with_args, .{ .samples = 3 }); | |
| + defer wave.deinit(); | |
| - try testing.expectEqual(wave.data[0], 0.0); | |
| - try testing.expectEqual(wave.data[1], 5.0109863e-2); | |
| - try testing.expectEqual(wave.data[2], 1.0003662e-1); | |
| + try testing.expectEqual(wave.sample_rate, 44100); | |
| + try testing.expectEqual(wave.channels, 1); | |
| - try testing.expectEqual(wave.sample_rate, 44100); | |
| - try testing.expectEqual(wave.channels, 1); | |
| - try testing.expectEqual(wave.bits, 16); | |
| -} | |
| + try testing.expectEqual(wave.samples.len, 3); | |
| + try testing.expectEqual(wave.samples[0], 0.0); | |
| + try testing.expectEqual(wave.samples[1], 0.0); | |
| + try testing.expectEqual(wave.samples[2], 0.0); | |
| + } | |
| -test "init & deinit" { | |
| - const allocator = testing.allocator; | |
| + test "filter" { | |
| + const allocator = testing.allocator; | |
| + const samples: []const T = &[_]T{}; | |
| + | |
| + var wave = try Self.init(samples, allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + try wave.filter(test_filter_without_args); | |
| + defer wave.deinit(); | |
| + | |
| + try testing.expectEqual(wave.sample_rate, 44100); | |
| + try testing.expectEqual(wave.channels, 1); | |
| + | |
| + try testing.expectEqual(wave.samples.len, 5); | |
| + try testing.expectEqual(wave.samples[0], 0.0); | |
| + try testing.expectEqual(wave.samples[1], 0.0); | |
| + try testing.expectEqual(wave.samples[2], 0.0); | |
| + try testing.expectEqual(wave.samples[3], 0.0); | |
| + try testing.expectEqual(wave.samples[4], 0.0); | |
| + } | |
| - const generator = struct { | |
| - fn sinewave() [44100]f32 { | |
| - const sample_rate: f32 = 44100.0; | |
| - const radins_per_sec: f32 = 440.0 * 2.0 * std.math.pi; | |
| + test "filter memory leaks' check" { | |
| + const allocator = testing.allocator; | |
| + const samples: []const T = &[_]T{}; | |
| + | |
| + var wave = try Self.init(samples, allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + try wave.filter(test_filter_without_args); | |
| + try wave.filter(test_filter_without_args); | |
| + try wave.filter(test_filter_without_args); | |
| + try wave.filter(test_filter_without_args); | |
| + defer wave.deinit(); | |
| + | |
| + try testing.expectEqual(wave.sample_rate, 44100); | |
| + try testing.expectEqual(wave.channels, 1); | |
| + | |
| + try testing.expectEqual(wave.samples.len, 5); | |
| + try testing.expectEqual(wave.samples[0], 0.0); | |
| + try testing.expectEqual(wave.samples[1], 0.0); | |
| + try testing.expectEqual(wave.samples[2], 0.0); | |
| + try testing.expectEqual(wave.samples[3], 0.0); | |
| + try testing.expectEqual(wave.samples[4], 0.0); | |
| + } | |
| - var result: [44100]f32 = undefined; | |
| - var i: usize = 0; | |
| + test "init with empty samples" { | |
| + const allocator = testing.allocator; | |
| + const samples: []const T = &[_]T{}; | |
| - while (i < result.len) : (i += 1) { | |
| - result[i] = 0.5 * std.math.sin(@as(f32, @floatFromInt(i)) * radins_per_sec / sample_rate); | |
| - } | |
| + const wave = try Self.init(samples, allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + defer wave.deinit(); | |
| - return result; | |
| + try testing.expectEqual(wave.samples.len, 0); | |
| + try testing.expectEqual(wave.sample_rate, 44100); | |
| + try testing.expectEqual(wave.channels, 1); | |
| } | |
| - }; | |
| - const data: [44100]f32 = generator.sinewave(); | |
| - const wave = Self.init(data[0..], allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }); | |
| - defer wave.deinit(); | |
| + test "init creates deep copy of samples" { | |
| + const allocator = testing.allocator; | |
| + var original_samples = [_]T{ 1.0, 2.0, 3.0 }; | |
| + const wave = try Self.init(&original_samples, allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + defer wave.deinit(); | |
| + | |
| + // Modify original samples | |
| + original_samples[0] = 999.0; | |
| + | |
| + // Wave samples should be unchanged (deep copy was made) | |
| + try testing.expectEqual(wave.samples[0], 1.0); | |
| + try testing.expectEqual(wave.samples[1], 2.0); | |
| + try testing.expectEqual(wave.samples[2], 3.0); | |
| + } | |
| - try testing.expectEqual(wave.sample_rate, 44100); | |
| - try testing.expectEqual(wave.channels, 1); | |
| - try testing.expectEqual(wave.bits, 16); | |
| -} | |
| + test "init with different channels" { | |
| + const allocator = testing.allocator; | |
| + const samples: []const T = &[_]T{ 1.0, 2.0, 3.0, 4.0 }; | |
| + | |
| + // Mono | |
| + const wave_mono = try Self.init(samples, allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + defer wave_mono.deinit(); | |
| + try testing.expectEqual(wave_mono.channels, 1); | |
| + | |
| + // Stereo | |
| + const wave_stereo = try Self.init(samples, allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 2, | |
| + }); | |
| + defer wave_stereo.deinit(); | |
| + try testing.expectEqual(wave_stereo.channels, 2); | |
| + } | |
| -test "mix" { | |
| - const allocator = testing.allocator; | |
| - const generator = struct { | |
| - fn sinewave() [44100]f32 { | |
| - const sample_rate: f32 = 44100.0; | |
| - const radins_per_sec: f32 = 440.0 * 2.0 * std.math.pi; | |
| + test "mix preserves wave properties" { | |
| + const allocator = testing.allocator; | |
| + const samples1: []const T = &[_]T{ 1.0, 2.0, 3.0 }; | |
| + const samples2: []const T = &[_]T{ 0.5, 1.0, 1.5 }; | |
| + | |
| + const wave1 = try Self.init(samples1, allocator, .{ | |
| + .sample_rate = 48000, | |
| + .channels = 2, | |
| + }); | |
| + defer wave1.deinit(); | |
| + | |
| + const wave2 = try Self.init(samples2, allocator, .{ | |
| + .sample_rate = 48000, | |
| + .channels = 2, | |
| + }); | |
| + defer wave2.deinit(); | |
| + | |
| + const result: Self = try wave1.mix(wave2, .{}); | |
| + defer result.deinit(); | |
| + | |
| + try testing.expectEqual(result.sample_rate, 48000); | |
| + try testing.expectEqual(result.channels, 2); | |
| + try testing.expectEqual(result.samples.len, 3); | |
| + try testing.expectEqual(result.samples[0], 1.5); | |
| + try testing.expectEqual(result.samples[1], 3.0); | |
| + try testing.expectEqual(result.samples[2], 4.5); | |
| + } | |
| - var result: [44100]f32 = undefined; | |
| - var i: usize = 0; | |
| + test "read with different sample rates" { | |
| + const allocator = testing.allocator; | |
| + var reader = std.Io.Reader.fixed(@embedFile("./assets/sine.wav")); | |
| + const wave = try Self.read(.wav, allocator, &reader); | |
| + defer wave.deinit(); | |
| - while (i < result.len) : (i += 1) { | |
| - result[i] = 0.5 * std.math.sin(@as(f32, @floatFromInt(i)) * radins_per_sec / sample_rate); | |
| - } | |
| + // Verify the wave has valid properties | |
| + try testing.expect(wave.sample_rate > 0); | |
| + try testing.expect(wave.channels > 0); | |
| + try testing.expect(wave.samples.len > 0); | |
| + } | |
| - return result; | |
| + test "separate func separates a Wave" { | |
| + const allocator = testing.allocator; | |
| + const samples: []const T = &[_]T{ 1.0, 2.0, 3.0, 4.0, 5.0 }; | |
| + const original = try Self.init(samples, allocator, .{ | |
| + .sample_rate = 41000, | |
| + .channels = 1, | |
| + }); | |
| + defer original.deinit(); | |
| + | |
| + const result = try original.separate(.{ | |
| + .allocator = allocator, | |
| + .separate_point = 3, | |
| + }); | |
| + defer result.initial.deinit(); | |
| + defer result.terminal.deinit(); | |
| + | |
| + try testing.expectEqualSlices(T, result.initial.samples, &.{ 1.0, 2.0, 3.0 }); | |
| + try testing.expectEqualSlices(T, result.terminal.samples, &.{ 4.0, 5.0 }); | |
| } | |
| }; | |
| - | |
| - const data: [44100]f32 = generator.sinewave(); | |
| - const wave = Self.init(data[0..], allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }); | |
| - defer wave.deinit(); | |
| - | |
| - const result: Self = wave.mix(wave); | |
| - defer result.deinit(); | |
| - | |
| - try testing.expectEqual(wave.sample_rate, 44100); | |
| - try testing.expectEqual(wave.channels, 1); | |
| - try testing.expectEqual(wave.bits, 16); | |
| - | |
| - try testing.expectEqual(result.data[0], 0.0); | |
| - try testing.expectEqual(result.data[1], 6.2648326e-2); | |
| - try testing.expectEqual(result.data[2], 1.2505053e-1); | |
| } | |
| -test "fill_zero_to_end" { | |
| - const allocator = testing.allocator; | |
| - const generator = struct { | |
| - fn sinewave() [44100]f32 { | |
| - const sample_rate: f32 = 44100.0; | |
| - const radins_per_sec: f32 = 440.0 * 2.0 * std.math.pi; | |
| - | |
| - var result: [44100]f32 = undefined; | |
| - var i: usize = 0; | |
| +test "Run tests for each samples' type" { | |
| + _ = inner(f128); | |
| + _ = inner(f80); | |
| + _ = inner(f64); | |
| + // _ = inner(f32); zigggwavvv 0.2.1 cannot use f32 as samples' type | |
| +} | |
| - while (i < result.len) : (i += 1) { | |
| - result[i] = 0.5 * std.math.sin(@as(f32, @floatFromInt(i)) * radins_per_sec / sample_rate); | |
| - } | |
| +fn test_filter_without_args(comptime SampleType: type, original: inner(SampleType)) !inner(SampleType) { | |
| + var result: std.array_list.Aligned(SampleType, null) = .empty; | |
| + defer result.deinit(original.allocator); | |
| - return result; | |
| - } | |
| - }; | |
| + for (0..5) |_| | |
| + try result.append(original.allocator, 0.0); | |
| - const data: [44100]f32 = generator.sinewave(); | |
| - const wave = Self.init(data[0..], allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| + return inner(SampleType).init(result.items, original.allocator, .{ | |
| + .sample_rate = original.sample_rate, | |
| + .channels = original.channels, | |
| }); | |
| - defer wave.deinit(); | |
| - | |
| - const filled_wave: Self = try wave.fill_zero_to_end(22050, 44100); | |
| - defer filled_wave.deinit(); | |
| - | |
| - try testing.expectEqual(filled_wave.sample_rate, 44100); | |
| - try testing.expectEqual(filled_wave.channels, 1); | |
| - try testing.expectEqual(filled_wave.bits, 16); | |
| - | |
| - try testing.expectEqual(filled_wave.data[0], 0.0); | |
| - try testing.expectEqual(filled_wave.data[1], 3.1324163e-2); | |
| - try testing.expectEqual(filled_wave.data[2], 6.2525265e-2); | |
| - | |
| - try testing.expectEqual(filled_wave.data[22049], -3.1344667e-2); | |
| - try testing.expectEqual(filled_wave.data[22050], 0.0); | |
| - try testing.expectEqual(filled_wave.data[22051], 0.0); | |
| - try testing.expectEqual(filled_wave.data[44099], 0.0); | |
| -} | |
| - | |
| -test "filter_with" { | |
| - const allocator = testing.allocator; | |
| - const data: []const f32 = &[_]f32{}; | |
| - const wave: Self = Self.init(data, allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }).filter_with(ArgsForTesting, test_filter_with_args, .{ .samples = 3 }); | |
| - defer wave.deinit(); | |
| - | |
| - try testing.expectEqual(wave.sample_rate, 44100); | |
| - try testing.expectEqual(wave.channels, 1); | |
| - try testing.expectEqual(wave.bits, 16); | |
| - | |
| - try testing.expectEqual(wave.data.len, 3); | |
| - try testing.expectEqual(wave.data[0], 0.0); | |
| - try testing.expectEqual(wave.data[1], 0.0); | |
| - try testing.expectEqual(wave.data[2], 0.0); | |
| } | |
| fn test_filter_with_args( | |
| - original_wave: Self, | |
| + comptime SampleType: type, | |
| + original: inner(SampleType), | |
| args: ArgsForTesting, | |
| -) !Self { | |
| - var result: std.array_list.Aligned(f32, null) = .empty; | |
| +) !inner(SampleType) { | |
| + var result: std.array_list.Aligned(SampleType, null) = .empty; | |
| + defer result.deinit(original.allocator); | |
| for (0..args.samples) |_| | |
| - try result.append(original_wave.allocator, 0.0); | |
| + try result.append(original.allocator, 0.0); | |
| - return Self{ | |
| - .data = try result.toOwnedSlice(original_wave.allocator), | |
| - .allocator = original_wave.allocator, | |
| - | |
| - .sample_rate = original_wave.sample_rate, | |
| - .channels = original_wave.channels, | |
| - .bits = original_wave.bits, | |
| - }; | |
| + return try inner(SampleType).init(result.items, original.allocator, .{ | |
| + .sample_rate = original.sample_rate, | |
| + .channels = original.channels, | |
| + }); | |
| } | |
| const ArgsForTesting = struct { | |
| samples: usize, | |
| }; | |
| - | |
| -test "filter" { | |
| - const allocator = testing.allocator; | |
| - const data: []const f32 = &[_]f32{}; | |
| - const wave: Self = Self.init(data, allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }).filter(test_filter_without_args); | |
| - defer wave.deinit(); | |
| - | |
| - try testing.expectEqual(wave.sample_rate, 44100); | |
| - try testing.expectEqual(wave.channels, 1); | |
| - try testing.expectEqual(wave.bits, 16); | |
| - | |
| - try testing.expectEqual(wave.data.len, 5); | |
| - try testing.expectEqual(wave.data[0], 0.0); | |
| - try testing.expectEqual(wave.data[1], 0.0); | |
| - try testing.expectEqual(wave.data[2], 0.0); | |
| - try testing.expectEqual(wave.data[3], 0.0); | |
| - try testing.expectEqual(wave.data[4], 0.0); | |
| -} | |
| - | |
| -fn test_filter_without_args(original_wave: Self) !Self { | |
| - var result: std.array_list.Aligned(f32, null) = .empty; | |
| - | |
| - for (0..5) |_| | |
| - try result.append(original_wave.allocator, 0.0); | |
| - | |
| - return Self{ | |
| - .data = try result.toOwnedSlice(original_wave.allocator), | |
| - .allocator = original_wave.allocator, | |
| - | |
| - .sample_rate = original_wave.sample_rate, | |
| - .channels = original_wave.channels, | |
| - .bits = original_wave.bits, | |
| - }; | |
| -} | |
| - | |
| -test "filter memory leaks' check" { | |
| - const allocator = testing.allocator; | |
| - const data: []const f32 = &[_]f32{}; | |
| - const wave: Self = Self.init(data, allocator, .{ | |
| - .sample_rate = 44100, | |
| - .channels = 1, | |
| - .bits = 16, | |
| - }) | |
| - .filter(test_filter_without_args) | |
| - .filter(test_filter_without_args) | |
| - .filter(test_filter_without_args) | |
| - .filter(test_filter_without_args); | |
| - defer wave.deinit(); | |
| - | |
| - try testing.expectEqual(wave.sample_rate, 44100); | |
| - try testing.expectEqual(wave.channels, 1); | |
| - try testing.expectEqual(wave.bits, 16); | |
| - | |
| - try testing.expectEqual(wave.data.len, 5); | |
| - try testing.expectEqual(wave.data[0], 0.0); | |
| - try testing.expectEqual(wave.data[1], 0.0); | |
| - try testing.expectEqual(wave.data[2], 0.0); | |
| - try testing.expectEqual(wave.data[3], 0.0); | |
| - try testing.expectEqual(wave.data[4], 0.0); | |
| -} | |
| diff --git a/examples/Wave/generate_function/src/assets/sine.wav b/tests/assets/sine.wav | |
| similarity index 100% | |
| rename from examples/Wave/generate_function/src/assets/sine.wav | |
| rename to tests/assets/sine.wav | |
| diff --git a/tests/assets/soundless.wav b/tests/assets/soundless.wav | |
| new file mode 100644 | |
| index 0000000..24d262a | |
| Binary files /dev/null and b/tests/assets/soundless.wav differ | |
| diff --git a/tests/composer.zig b/tests/composer.zig | |
| new file mode 100644 | |
| index 0000000..e0675b5 | |
| --- /dev/null | |
| +++ b/tests/composer.zig | |
| @@ -0,0 +1,72 @@ | |
| +const std = @import("std"); | |
| +const lightmix = @import("lightmix"); | |
| +const Wave = lightmix.Wave; | |
| +const Composer = lightmix.Composer; | |
| + | |
| +test "Compose multiple soundless Wave" { | |
| + const allocator = std.testing.allocator; | |
| + var composer = Composer(f64).init(allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + defer composer.deinit(); | |
| + | |
| + const data: []const f64 = try generate_soundless_data(44100, allocator); | |
| + defer allocator.free(data); | |
| + | |
| + const wave: Wave(f64) = try Wave(f64).init(data, allocator, .{ | |
| + .sample_rate = 44100, | |
| + .channels = 1, | |
| + }); | |
| + defer wave.deinit(); | |
| + | |
| + var append_list: std.array_list.Aligned(Composer(f64).WaveInfo, null) = .empty; | |
| + defer append_list.deinit(allocator); | |
| + try append_list.append(allocator, .{ .wave = wave, .start_point = 0 }); | |
| + try append_list.append(allocator, .{ .wave = wave, .start_point = 0 }); | |
| + | |
| + try composer.appendSlice(append_list.items); | |
| + | |
| + const result = try composer.finalize(.{}); | |
| + defer result.deinit(); | |
| + | |
| + // Create TmpDir | |
| + var tmpDir = std.testing.tmpDir(.{}); | |
| + defer tmpDir.cleanup(); | |
| + | |
| + var file = try tmpDir.dir.createFile("result.wav", .{}); | |
| + defer file.close(); | |
| + const buf = try allocator.alloc(u8, 10 * 1024 * 1024); | |
| + defer allocator.free(buf); | |
| + var writer = file.writer(buf); | |
| + | |
| + // Write Wave into the file | |
| + try result.write(.wav, &writer.interface, .{ | |
| + .format_code = .pcm, | |
| + .bits = 16, | |
| + }); | |
| + | |
| + try writer.interface.flush(); | |
| + | |
| + // Read the written wave file | |
| + const result_bytes = try tmpDir.dir.readFileAlloc(allocator, "result.wav", 10 * 1024 * 1024); | |
| + defer allocator.free(result_bytes); | |
| + | |
| + // Read the actual file | |
| + const expected_bytes = try std.fs.cwd().readFileAlloc(allocator, "tests/assets/soundless.wav", 100 * 1024 * 1024); | |
| + defer allocator.free(expected_bytes); | |
| + | |
| + try std.testing.expectEqualSlices(u8, expected_bytes, result_bytes); | |
| +} | |
| + | |
| +fn generate_soundless_data(length: usize, allocator: std.mem.Allocator) ![]const f64 { | |
| + var list: std.array_list.Aligned(f64, null) = .empty; | |
| + | |
| + // Append empty wave | |
| + for (0..length) |_| | |
| + try list.append(allocator, 0.0); | |
| + | |
| + const result: []const f64 = try list.toOwnedSlice(allocator); | |
| + | |
| + return result; | |
| +} | |
| diff --git a/tests/wave.zig b/tests/wave.zig | |
| new file mode 100644 | |
| index 0000000..842bea2 | |
| --- /dev/null | |
| +++ b/tests/wave.zig | |
| @@ -0,0 +1,31 @@ | |
| +const std = @import("std"); | |
| +const lightmix = @import("lightmix"); | |
| +const Wave = lightmix.Wave; | |
| + | |
| +test "read sine.wav" { | |
| + const allocator = std.testing.allocator; | |
| + var reader = std.Io.Reader.fixed(@embedFile("./assets/sine.wav")); | |
| + | |
| + const sine = try Wave(f64).read(.wav, allocator, &reader); | |
| + defer sine.deinit(); | |
| + | |
| + const expected_samples: []const f64 = &[_]f64{ | |
| + 0, | |
| + 0.05011139255958739, | |
| + 0.1000396740623188, | |
| + 0.14960173345133823, | |
| + 0.19849238563188573, | |
| + 0.24665059358500932, | |
| + 0.293954283272805, | |
| + 0.33979308450575274, | |
| + 0.3847468489638966, | |
| + 0.42771691030610065, | |
| + 0.4693441572313608, | |
| + 0.5090182195501571, | |
| + 0.5465865047151097, | |
| + 0.5822626422925504, | |
| + 0.6153447065645314, | |
| + 0.6462599566637165, | |
| + }; | |
| + try std.testing.expectEqualSlices(f64, expected_samples, sine.samples[0..16]); | |
| +} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment