Initial release: Orca VM, TUI, MIDI/OSC/UDP output, and release pipeline
This commit is contained in:
@@ -0,0 +1,137 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- arch: x86_64
|
||||||
|
target: x86_64-unknown-linux-gnu
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
- arch: aarch64
|
||||||
|
target: aarch64-unknown-linux-gnu
|
||||||
|
runs-on: aarch64
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.runs-on }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install system dependencies
|
||||||
|
if: matrix.arch == 'x86_64'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libasound2-dev pkg-config
|
||||||
|
|
||||||
|
- name: Install Rust stable
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Cache cargo registry
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.cargo/registry
|
||||||
|
key: ${{ runner.os }}-${{ matrix.arch }}-cargo-${{ hashFiles('Cargo.lock') }}
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: cargo test --target ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --release --target ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Package
|
||||||
|
run: |
|
||||||
|
TAG=${{ gitea.ref_name }}
|
||||||
|
TARBALL="orca-rs-${TAG}-${{ matrix.arch }}.tar.gz"
|
||||||
|
mkdir pkg
|
||||||
|
cp target/${{ matrix.target }}/release/orca-rs pkg/
|
||||||
|
cp target/${{ matrix.target }}/release/orca-cli pkg/
|
||||||
|
cp LICENSE pkg/
|
||||||
|
tar -czf "${TARBALL}" -C pkg .
|
||||||
|
sha256sum "${TARBALL}" > "${TARBALL}.sha256"
|
||||||
|
echo "TARBALL=${TARBALL}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Create release
|
||||||
|
run: |
|
||||||
|
curl -sX POST \
|
||||||
|
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases" \
|
||||||
|
-d "{\"tag_name\":\"${{ gitea.ref_name }}\",\"name\":\"${{ gitea.ref_name }}\"}" \
|
||||||
|
--fail-with-body || true
|
||||||
|
|
||||||
|
- name: Upload assets
|
||||||
|
run: |
|
||||||
|
RELEASE_ID=$(curl -s \
|
||||||
|
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
||||||
|
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases/tags/${{ gitea.ref_name }}" \
|
||||||
|
| jq -r '.id')
|
||||||
|
|
||||||
|
for FILE in "${{ env.TARBALL }}" "${{ env.TARBALL }}.sha256"; do
|
||||||
|
# Remove any existing asset with the same name so re-runs stay clean
|
||||||
|
EXISTING=$(curl -s \
|
||||||
|
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
||||||
|
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases/${RELEASE_ID}/assets" \
|
||||||
|
| jq -r ".[] | select(.name == \"${FILE}\") | .id")
|
||||||
|
for AID in $EXISTING; do
|
||||||
|
curl -sX DELETE \
|
||||||
|
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
||||||
|
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases/${RELEASE_ID}/assets/${AID}"
|
||||||
|
done
|
||||||
|
|
||||||
|
curl -sX POST \
|
||||||
|
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases/${RELEASE_ID}/assets?name=${FILE}" \
|
||||||
|
--data-binary "@${FILE}" --fail-with-body
|
||||||
|
done
|
||||||
|
|
||||||
|
update-aur:
|
||||||
|
needs: build
|
||||||
|
runs-on: aarch64
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Compute checksums and update PKGBUILD
|
||||||
|
run: |
|
||||||
|
TAG=${{ gitea.ref_name }}
|
||||||
|
BASE="${{ gitea.server_url }}/${{ gitea.repository }}/releases/download/${TAG}"
|
||||||
|
|
||||||
|
SUM_X86=$(curl -sL "${BASE}/orca-rs-${TAG}-x86_64.tar.gz" | sha256sum | cut -d' ' -f1)
|
||||||
|
SUM_AARCH=$(curl -sL "${BASE}/orca-rs-${TAG}-aarch64.tar.gz" | sha256sum | cut -d' ' -f1)
|
||||||
|
|
||||||
|
sed -i "s/sha256sums_x86_64=('.*')/sha256sums_x86_64=('${SUM_X86}')/" aur/PKGBUILD
|
||||||
|
sed -i "s/sha256sums_aarch64=('.*')/sha256sums_aarch64=('${SUM_AARCH}')/" aur/PKGBUILD
|
||||||
|
|
||||||
|
- name: Push to AUR
|
||||||
|
env:
|
||||||
|
AUR_SSH_KEY: ${{ secrets.AUR_SSH_KEY }}
|
||||||
|
run: |
|
||||||
|
install -dm700 ~/.ssh
|
||||||
|
echo "$AUR_SSH_KEY" > ~/.ssh/aur
|
||||||
|
chmod 600 ~/.ssh/aur
|
||||||
|
echo "Host aur.archlinux.org" >> ~/.ssh/config
|
||||||
|
echo " IdentityFile ~/.ssh/aur" >> ~/.ssh/config
|
||||||
|
echo " User aur" >> ~/.ssh/config
|
||||||
|
ssh-keyscan aur.archlinux.org >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
|
|
||||||
|
rm -rf /tmp/aur-orca-rs
|
||||||
|
git clone ssh://aur@aur.archlinux.org/orca-rs.git /tmp/aur-orca-rs
|
||||||
|
cp aur/PKGBUILD /tmp/aur-orca-rs/
|
||||||
|
cd /tmp/aur-orca-rs
|
||||||
|
makepkg --printsrcinfo > .SRCINFO
|
||||||
|
git config user.name "Bendik Aagaard Lynghaug"
|
||||||
|
git config user.email "bendik.lynghaug@gmail.com"
|
||||||
|
git add PKGBUILD .SRCINFO
|
||||||
|
git commit -m "Update to ${{ gitea.ref_name }}"
|
||||||
|
git push origin master
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+621
@@ -0,0 +1,621 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "alsa"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43"
|
||||||
|
dependencies = [
|
||||||
|
"alsa-sys",
|
||||||
|
"bitflags 2.13.0",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "alsa-sys"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "1.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block2"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
|
||||||
|
dependencies = [
|
||||||
|
"objc2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.20.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "coremidi"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a1fa14fb8c3ca83d0d7f22f4afc9ecfe5f40947f01ce639a638a9377c2662dde"
|
||||||
|
dependencies = [
|
||||||
|
"block2",
|
||||||
|
"core-foundation",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"coremidi-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "coremidi-sys"
|
||||||
|
version = "3.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cc9504310988d938e49fff1b5f1e56e3dafe39bb1bae580c19660b58b83a191e"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossterm"
|
||||||
|
version = "0.28.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.0",
|
||||||
|
"crossterm_winapi",
|
||||||
|
"mio",
|
||||||
|
"parking_lot",
|
||||||
|
"rustix",
|
||||||
|
"signal-hook",
|
||||||
|
"signal-hook-mio",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossterm_winapi"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-core"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-task"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-util"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"pin-project-lite",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.103"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"futures-util",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.186"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.4.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lock_api"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||||
|
dependencies = [
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.33"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "midir"
|
||||||
|
version = "0.10.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56542e359bb7e4bd1a77cb79042be32d4af0713a9ce58160355eaf72df9db87c"
|
||||||
|
dependencies = [
|
||||||
|
"alsa",
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"coremidi",
|
||||||
|
"js-sys",
|
||||||
|
"libc",
|
||||||
|
"parking_lot",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
"windows",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mio"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"wasi",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
|
||||||
|
dependencies = [
|
||||||
|
"objc2-encode",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-encode"
|
||||||
|
version = "4.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "orca-rs"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"crossterm",
|
||||||
|
"midir",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot"
|
||||||
|
version = "0.12.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||||
|
dependencies = [
|
||||||
|
"lock_api",
|
||||||
|
"parking_lot_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot_core"
|
||||||
|
version = "0.9.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
|
"smallvec",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkg-config"
|
||||||
|
version = "0.3.33"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.46"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.5.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "0.38.44"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.0",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scopeguard"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook"
|
||||||
|
version = "0.3.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"signal-hook-registry",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-mio"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"signal-hook",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-registry"
|
||||||
|
version = "1.4.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||||
|
dependencies = [
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slab"
|
||||||
|
version = "0.4.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.15.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.118"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.126"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.126"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.126"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.126"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-sys"
|
||||||
|
version = "0.3.103"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-i686-pc-windows-gnu",
|
||||||
|
"winapi-x86_64-pc-windows-gnu",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-i686-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows"
|
||||||
|
version = "0.56.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core",
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.56.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement",
|
||||||
|
"windows-interface",
|
||||||
|
"windows-result",
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.56.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.56.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.59.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||||
|
dependencies = [
|
||||||
|
"windows_aarch64_gnullvm",
|
||||||
|
"windows_aarch64_msvc",
|
||||||
|
"windows_i686_gnu",
|
||||||
|
"windows_i686_gnullvm",
|
||||||
|
"windows_i686_msvc",
|
||||||
|
"windows_x86_64_gnu",
|
||||||
|
"windows_x86_64_gnullvm",
|
||||||
|
"windows_x86_64_msvc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "orca-rs"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Orca live coding environment, ported to Rust"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://prosjekt.klingenbergbygg.no/bl/orca-rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
crossterm = "0.28"
|
||||||
|
midir = "0.10"
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Bendik Aagaard Lynghaug
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# orca-rs
|
||||||
|
|
||||||
|
A Rust port of [Orca-c](https://github.com/hundredrabbits/Orca-c), the livecoding
|
||||||
|
environment for procedural sequencing.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
On Arch Linux:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yay -S orca-rs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires ALSA headers on Linux (`alsa-lib`) for MIDI support.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```sh
|
||||||
|
orca-rs [options] [file.orca]
|
||||||
|
```
|
||||||
|
|
||||||
|
With no file, edits save to `untitled.orca`. A named file that doesn't exist
|
||||||
|
yet is created on first save.
|
||||||
|
|
||||||
|
| Option | Effect |
|
||||||
|
|---|---|
|
||||||
|
| `--bpm <n>` | Tempo (default 120; one tick = one 16th note) |
|
||||||
|
| `--seed <n>` | Random seed for the `R` operator |
|
||||||
|
| `--udp-port <n>` / `--udp-host <h>` | Send `;` strings over UDP (host defaults to 127.0.0.1) |
|
||||||
|
| `--osc-port <n>` / `--osc-host <h>` | Send `=` messages over OSC |
|
||||||
|
| `--no-midi` | Disable MIDI output (on by default) |
|
||||||
|
| `--midi-name <s>` | Connect to an existing MIDI port whose name contains `<s>` |
|
||||||
|
| `--list-midi` | List MIDI output ports and exit |
|
||||||
|
|
||||||
|
MIDI is on by default: orca-rs creates a virtual ALSA sequencer port named
|
||||||
|
`orca-rs`, which PipeWire exposes through its Midi-Bridge — connect it to a
|
||||||
|
synth with qpwgraph/Helvum, or find it under `wpctl status`.
|
||||||
|
|
||||||
|
There is also a headless runner, equivalent to orca-c's `cli`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
orca-cli -t 60 file.orca # simulate 60 ticks, print the grid
|
||||||
|
```
|
||||||
|
|
||||||
|
## Keys
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
|---|---|
|
||||||
|
| Arrow keys | Move cursor (Alt+arrows: leap by 8) |
|
||||||
|
| Shift+arrows | Grow/shrink selection |
|
||||||
|
| Ctrl+arrows | Resize grid |
|
||||||
|
| `0-9 a-z A-Z ! : ; = # * . ?` | Place glyph (fills selection) |
|
||||||
|
| Backspace / Delete | Erase selection |
|
||||||
|
| Space | Play / pause |
|
||||||
|
| Ctrl+F | Step one frame (while paused) |
|
||||||
|
| Ctrl+R | Reset frame counter |
|
||||||
|
| `<` / `>` | BPM −1 / +1 |
|
||||||
|
| `(` / `)` | BPM −10 / +10 |
|
||||||
|
| Ctrl+Z or Ctrl+U | Undo |
|
||||||
|
| Ctrl+X / Ctrl+C / Ctrl+V | Cut / copy / paste selection |
|
||||||
|
| Ctrl+S | Save |
|
||||||
|
| Esc | Collapse selection |
|
||||||
|
| Ctrl+Q | Quit |
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
- `src/sim/` — the VM: all operators, ported from orca-c's `sim.c`
|
||||||
|
- `src/tui.rs` — terminal UI (crossterm), counterpart of `tui_main.c`
|
||||||
|
- `src/io.rs` — UDP / OSC / MIDI event output
|
||||||
|
- `src/bin/orca-cli.rs` — headless runner, counterpart of `cli_main.c`
|
||||||
|
|
||||||
|
## Releasing
|
||||||
|
|
||||||
|
Releases are cut with [cargo-release](https://github.com/crate-ci/cargo-release)
|
||||||
|
(`cargo release patch|minor`), which bumps `Cargo.toml` and `aur/PKGBUILD` and
|
||||||
|
pushes a `v*` tag. The Gitea Actions workflow then builds x86_64 and aarch64
|
||||||
|
tarballs, attaches them to the Gitea release, and pushes the updated PKGBUILD
|
||||||
|
to the AUR. Required repo secrets: `GITEA_TOKEN`, `AUR_SSH_KEY`.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
pkgbase = orca-rs
|
||||||
|
pkgdesc = Orca live coding environment, ported to Rust (terminal UI with MIDI/OSC/UDP output)
|
||||||
|
pkgver = 0.1.0
|
||||||
|
pkgrel = 1
|
||||||
|
url = https://prosjekt.klingenbergbygg.no/bl/orca-rs
|
||||||
|
arch = x86_64
|
||||||
|
arch = aarch64
|
||||||
|
license = MIT
|
||||||
|
depends = alsa-lib
|
||||||
|
optdepends = pipewire-alsa: expose the MIDI port through PipeWire
|
||||||
|
provides = orca-rs
|
||||||
|
conflicts = orca-rs-git
|
||||||
|
conflicts = orca-rs-bin
|
||||||
|
source_x86_64 = orca-rs-v0.1.0-x86_64.tar.gz::https://prosjekt.klingenbergbygg.no/bl/orca-rs/releases/download/v0.1.0/orca-rs-v0.1.0-x86_64.tar.gz
|
||||||
|
sha256sums_x86_64 = SKIP
|
||||||
|
source_aarch64 = orca-rs-v0.1.0-aarch64.tar.gz::https://prosjekt.klingenbergbygg.no/bl/orca-rs/releases/download/v0.1.0/orca-rs-v0.1.0-aarch64.tar.gz
|
||||||
|
sha256sums_aarch64 = SKIP
|
||||||
|
|
||||||
|
pkgname = orca-rs
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Maintainer: Bendik Aagaard Lynghaug <bendik.lynghaug@gmail.com>
|
||||||
|
pkgname=orca-rs
|
||||||
|
pkgver=0.1.0
|
||||||
|
pkgrel=1
|
||||||
|
pkgdesc="Orca live coding environment, ported to Rust (terminal UI with MIDI/OSC/UDP output)"
|
||||||
|
arch=('x86_64' 'aarch64')
|
||||||
|
url="https://prosjekt.klingenbergbygg.no/bl/orca-rs"
|
||||||
|
license=('MIT')
|
||||||
|
depends=('alsa-lib')
|
||||||
|
optdepends=(
|
||||||
|
'pipewire-alsa: expose the MIDI port through PipeWire'
|
||||||
|
)
|
||||||
|
provides=('orca-rs')
|
||||||
|
conflicts=('orca-rs-git' 'orca-rs-bin')
|
||||||
|
source_x86_64=("orca-rs-v${pkgver}-x86_64.tar.gz::https://prosjekt.klingenbergbygg.no/bl/orca-rs/releases/download/v${pkgver}/orca-rs-v${pkgver}-x86_64.tar.gz")
|
||||||
|
source_aarch64=("orca-rs-v${pkgver}-aarch64.tar.gz::https://prosjekt.klingenbergbygg.no/bl/orca-rs/releases/download/v${pkgver}/orca-rs-v${pkgver}-aarch64.tar.gz")
|
||||||
|
sha256sums_x86_64=('SKIP')
|
||||||
|
sha256sums_aarch64=('SKIP')
|
||||||
|
|
||||||
|
package() {
|
||||||
|
install -Dm755 orca-rs "$pkgdir/usr/bin/orca-rs"
|
||||||
|
install -Dm755 orca-cli "$pkgdir/usr/bin/orca-cli"
|
||||||
|
|
||||||
|
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
publish = false
|
||||||
|
allow-branch = ["main"]
|
||||||
|
|
||||||
|
[[pre-release-replacements]]
|
||||||
|
file = "aur/PKGBUILD"
|
||||||
|
search = "pkgver=.*"
|
||||||
|
replace = "pkgver={{version}}"
|
||||||
|
|
||||||
|
[[pre-release-replacements]]
|
||||||
|
file = "aur/PKGBUILD"
|
||||||
|
search = "pkgrel=.*"
|
||||||
|
replace = "pkgrel=1"
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
//! Headless runner, equivalent to orca-c's cli_main.c:
|
||||||
|
//! load a grid file, simulate N ticks, print the resulting grid to stdout.
|
||||||
|
|
||||||
|
use std::io::Write;
|
||||||
|
use std::process::exit;
|
||||||
|
|
||||||
|
use orca_rs::sim::{orca_run, Field, MarkBuffer, Oevent};
|
||||||
|
|
||||||
|
const USAGE: &str = "\
|
||||||
|
Usage: orca-cli [options] infile
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-t <n> Number of timesteps to simulate (default: 1)
|
||||||
|
-h, --help Print this message and exit
|
||||||
|
";
|
||||||
|
|
||||||
|
fn die(msg: &str) -> ! {
|
||||||
|
eprintln!("{msg}");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut ticks: usize = 1;
|
||||||
|
let mut path: Option<String> = None;
|
||||||
|
|
||||||
|
let mut args = std::env::args().skip(1);
|
||||||
|
while let Some(arg) = args.next() {
|
||||||
|
match arg.as_str() {
|
||||||
|
"-t" => {
|
||||||
|
ticks = args
|
||||||
|
.next()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or_else(|| die("-t requires a non-negative integer"));
|
||||||
|
}
|
||||||
|
"-h" | "--help" => {
|
||||||
|
print!("{USAGE}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ if path.is_none() && !arg.starts_with('-') => path = Some(arg),
|
||||||
|
_ => die(USAGE),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = path.unwrap_or_else(|| die(USAGE));
|
||||||
|
let mut field = match Field::load_file(&path) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => die(&format!("File load error: {}", e.as_str())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let height = field.height as usize;
|
||||||
|
let width = field.width as usize;
|
||||||
|
let mut mbuf = MarkBuffer::new();
|
||||||
|
mbuf.ensure_size(height, width);
|
||||||
|
let mut events: Vec<Oevent> = Vec::new();
|
||||||
|
|
||||||
|
for tick in 0..ticks {
|
||||||
|
mbuf.clear(height, width);
|
||||||
|
orca_run(
|
||||||
|
&mut field.buffer,
|
||||||
|
&mut mbuf.buffer[..height * width],
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
tick,
|
||||||
|
&mut events,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
events.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = std::io::stdout();
|
||||||
|
let mut out = stdout.lock();
|
||||||
|
if field.write_to(&mut out).and_then(|_| out.flush()).is_err() {
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
//! Event output backends: UDP strings (`;`), OSC integer messages (`=`),
|
||||||
|
//! and MIDI notes/CC/pitchbend (`:` `%` `!` `?`) — the counterpart of the
|
||||||
|
//! PortMidi/oosc code in orca-c's tui_main.c.
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
use std::net::{SocketAddr, ToSocketAddrs, UdpSocket};
|
||||||
|
|
||||||
|
use midir::{MidiOutput, MidiOutputConnection};
|
||||||
|
|
||||||
|
use crate::sim::Oevent;
|
||||||
|
|
||||||
|
fn resolve(host: &str, port: u16) -> io::Result<(UdpSocket, SocketAddr)> {
|
||||||
|
let dest = (host, port)
|
||||||
|
.to_socket_addrs()?
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "cannot resolve host"))?;
|
||||||
|
let bind_addr = if dest.is_ipv4() { "0.0.0.0:0" } else { "[::]:0" };
|
||||||
|
let socket = UdpSocket::bind(bind_addr)?;
|
||||||
|
Ok((socket, dest))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UdpOut {
|
||||||
|
socket: UdpSocket,
|
||||||
|
dest: SocketAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UdpOut {
|
||||||
|
pub fn new(host: &str, port: u16) -> io::Result<Self> {
|
||||||
|
let (socket, dest) = resolve(host, port)?;
|
||||||
|
Ok(Self { socket, dest })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send(&self, payload: &[u8]) {
|
||||||
|
let _ = self.socket.send_to(payload, self.dest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OscOut {
|
||||||
|
socket: UdpSocket,
|
||||||
|
dest: SocketAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OscOut {
|
||||||
|
pub fn new(host: &str, port: u16) -> io::Result<Self> {
|
||||||
|
let (socket, dest) = resolve(host, port)?;
|
||||||
|
Ok(Self { socket, dest })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_ints(&self, path: &str, ints: &[i32]) {
|
||||||
|
let mut msg = Vec::with_capacity(path.len() + ints.len() * 5 + 8);
|
||||||
|
msg.extend_from_slice(path.as_bytes());
|
||||||
|
msg.push(0);
|
||||||
|
while msg.len() % 4 != 0 {
|
||||||
|
msg.push(0);
|
||||||
|
}
|
||||||
|
msg.push(b',');
|
||||||
|
msg.extend(std::iter::repeat_n(b'i', ints.len()));
|
||||||
|
msg.push(0);
|
||||||
|
while msg.len() % 4 != 0 {
|
||||||
|
msg.push(0);
|
||||||
|
}
|
||||||
|
for i in ints {
|
||||||
|
msg.extend_from_slice(&i.to_be_bytes());
|
||||||
|
}
|
||||||
|
let _ = self.socket.send_to(&msg, self.dest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A sounding note; `remaining` counts down one per tick, note-off at zero.
|
||||||
|
struct SusNote {
|
||||||
|
channel: u8,
|
||||||
|
pitch: u8,
|
||||||
|
remaining: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MidiOut {
|
||||||
|
conn: MidiOutputConnection,
|
||||||
|
sustained: Vec<SusNote>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MidiOut {
|
||||||
|
pub fn list_ports() -> Vec<String> {
|
||||||
|
match MidiOutput::new("orca-rs") {
|
||||||
|
Ok(out) => out
|
||||||
|
.ports()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|p| out.port_name(p).ok())
|
||||||
|
.collect(),
|
||||||
|
Err(_) => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// With a name hint, connect to the first real port whose name contains it
|
||||||
|
/// (case-insensitive). Without one, create a virtual ALSA port named
|
||||||
|
/// "orca-rs" that synths can connect to.
|
||||||
|
pub fn open(name_hint: Option<&str>) -> Result<Self, String> {
|
||||||
|
let out = MidiOutput::new("orca-rs").map_err(|e| e.to_string())?;
|
||||||
|
let conn = match name_hint {
|
||||||
|
Some(hint) => {
|
||||||
|
let needle = hint.to_lowercase();
|
||||||
|
let port = out
|
||||||
|
.ports()
|
||||||
|
.into_iter()
|
||||||
|
.find(|p| {
|
||||||
|
out.port_name(p)
|
||||||
|
.map(|n| n.to_lowercase().contains(&needle))
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
.ok_or_else(|| format!("no MIDI output port matching '{hint}'"))?;
|
||||||
|
out.connect(&port, "orca-rs").map_err(|e| e.to_string())?
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use midir::os::unix::VirtualOutput;
|
||||||
|
out.create_virtual("orca-rs").map_err(|e| e.to_string())?
|
||||||
|
}
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
{
|
||||||
|
let ports = out.ports();
|
||||||
|
let port = ports.first().ok_or("no MIDI output ports available")?;
|
||||||
|
out.connect(port, "orca-rs").map_err(|e| e.to_string())?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(Self { conn, sustained: Vec::new() })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send(&mut self, msg: &[u8]) {
|
||||||
|
let _ = self.conn.send(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Advance one tick: expire sustained notes and emit their note-offs.
|
||||||
|
pub fn advance_tick(&mut self) {
|
||||||
|
let mut offs: Vec<(u8, u8)> = Vec::new();
|
||||||
|
self.sustained.retain_mut(|n| {
|
||||||
|
n.remaining -= 1;
|
||||||
|
if n.remaining == 0 {
|
||||||
|
offs.push((n.channel, n.pitch));
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (channel, pitch) in offs {
|
||||||
|
self.send(&[0x80 | channel, pitch, 0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn note_on(&mut self, channel: u8, pitch: u8, velocity: u8, duration: u8, mono: bool) {
|
||||||
|
// Retrigger rules from orca-c: mono kills the whole channel, and a
|
||||||
|
// repeated pitch is released before sounding again.
|
||||||
|
let mut offs: Vec<(u8, u8)> = Vec::new();
|
||||||
|
self.sustained.retain(|n| {
|
||||||
|
if n.channel == channel && (mono || n.pitch == pitch) {
|
||||||
|
offs.push((n.channel, n.pitch));
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (ch, p) in offs {
|
||||||
|
self.send(&[0x80 | ch, p, 0]);
|
||||||
|
}
|
||||||
|
self.send(&[0x90 | channel, pitch, velocity]);
|
||||||
|
self.sustained.push(SusNote {
|
||||||
|
channel,
|
||||||
|
pitch,
|
||||||
|
remaining: duration.max(1),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn control_change(&mut self, channel: u8, control: u8, value: u8) {
|
||||||
|
self.send(&[0xB0 | channel, control, value]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pitch_bend(&mut self, channel: u8, lsb: u8, msb: u8) {
|
||||||
|
self.send(&[0xE0 | channel, lsb, msb]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Release everything immediately (used on pause and on exit).
|
||||||
|
pub fn all_notes_off(&mut self) {
|
||||||
|
let notes: Vec<(u8, u8)> = self
|
||||||
|
.sustained
|
||||||
|
.drain(..)
|
||||||
|
.map(|n| (n.channel, n.pitch))
|
||||||
|
.collect();
|
||||||
|
for (channel, pitch) in notes {
|
||||||
|
self.send(&[0x80 | channel, pitch, 0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct EventOutputs {
|
||||||
|
pub udp: Option<UdpOut>,
|
||||||
|
pub osc: Option<OscOut>,
|
||||||
|
pub midi: Option<MidiOut>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventOutputs {
|
||||||
|
/// Call once at the start of every tick, before running the sim.
|
||||||
|
pub fn begin_tick(&mut self) {
|
||||||
|
if let Some(midi) = &mut self.midi {
|
||||||
|
midi.advance_tick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deliver the events produced by one sim tick.
|
||||||
|
pub fn dispatch(&mut self, events: &mut Vec<Oevent>) {
|
||||||
|
for ev in events.drain(..) {
|
||||||
|
match ev {
|
||||||
|
Oevent::MidiNote { channel, octave, note, velocity, duration, mono } => {
|
||||||
|
if let Some(midi) = &mut self.midi {
|
||||||
|
let pitch = (octave as u16 * 12 + note as u16).min(127) as u8;
|
||||||
|
midi.note_on(channel & 15, pitch, velocity.min(127), duration, mono);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Oevent::MidiCc { channel, control, value } => {
|
||||||
|
if let Some(midi) = &mut self.midi {
|
||||||
|
midi.control_change(channel & 15, control & 0x7f, value & 0x7f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Oevent::MidiPb { channel, lsb, msb } => {
|
||||||
|
if let Some(midi) = &mut self.midi {
|
||||||
|
midi.pitch_bend(channel & 15, lsb & 0x7f, msb & 0x7f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Oevent::OscInts { glyph, numbers } => {
|
||||||
|
if let Some(osc) = &self.osc {
|
||||||
|
let path = format!("/{}", glyph as char);
|
||||||
|
let ints: Vec<i32> = numbers.iter().map(|&n| n as i32).collect();
|
||||||
|
osc.send_ints(&path, &ints);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Oevent::UdpString { chars } => {
|
||||||
|
if let Some(udp) = &self.udp {
|
||||||
|
udp.send(&chars);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Silence all sustained MIDI notes (pause / quit).
|
||||||
|
pub fn silence(&mut self) {
|
||||||
|
if let Some(midi) = &mut self.midi {
|
||||||
|
midi.all_notes_off();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod io;
|
||||||
|
pub mod sim;
|
||||||
|
pub mod tui;
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
use std::process::exit;
|
||||||
|
|
||||||
|
use orca_rs::io::{EventOutputs, MidiOut, OscOut, UdpOut};
|
||||||
|
use orca_rs::tui::{self, TuiConfig};
|
||||||
|
|
||||||
|
const USAGE: &str = "\
|
||||||
|
Usage: orca-rs [options] [file.orca]
|
||||||
|
|
||||||
|
Open the Orca live-coding environment. If the file does not exist it will
|
||||||
|
be created on first save; with no file, edits save to untitled.orca.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--bpm <n> Tempo (default: 120)
|
||||||
|
--seed <n> Random seed for the R operator (default: 1)
|
||||||
|
--udp-port <n> Send ';' operator strings over UDP to this port
|
||||||
|
--udp-host <h> UDP destination host (default: 127.0.0.1)
|
||||||
|
--osc-port <n> Send '=' operator messages over OSC to this port
|
||||||
|
--osc-host <h> OSC destination host (default: 127.0.0.1)
|
||||||
|
--no-midi Disable MIDI output (on by default: creates a virtual
|
||||||
|
ALSA port named 'orca-rs', visible in PipeWire)
|
||||||
|
--midi-name <s> Connect to the MIDI output port whose name contains <s>
|
||||||
|
instead of creating a virtual port
|
||||||
|
--list-midi List available MIDI output ports and exit
|
||||||
|
-h, --help Print this message and exit
|
||||||
|
";
|
||||||
|
|
||||||
|
fn die(msg: &str) -> ! {
|
||||||
|
eprintln!("{msg}");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_num(args: &mut impl Iterator<Item = String>, flag: &str) -> usize {
|
||||||
|
args.next()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or_else(|| die(&format!("{flag} requires a number")))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut path: Option<String> = None;
|
||||||
|
let mut bpm: usize = 120;
|
||||||
|
let mut seed: usize = 1;
|
||||||
|
let mut udp_host = "127.0.0.1".to_string();
|
||||||
|
let mut udp_port: Option<u16> = None;
|
||||||
|
let mut osc_host = "127.0.0.1".to_string();
|
||||||
|
let mut osc_port: Option<u16> = None;
|
||||||
|
let mut midi = true;
|
||||||
|
let mut midi_name: Option<String> = None;
|
||||||
|
|
||||||
|
let mut args = std::env::args().skip(1);
|
||||||
|
while let Some(arg) = args.next() {
|
||||||
|
match arg.as_str() {
|
||||||
|
"--bpm" => bpm = parse_num(&mut args, "--bpm").clamp(10, 999),
|
||||||
|
"--seed" => seed = parse_num(&mut args, "--seed"),
|
||||||
|
"--udp-port" => udp_port = Some(parse_num(&mut args, "--udp-port") as u16),
|
||||||
|
"--udp-host" => udp_host = args.next().unwrap_or_else(|| die("--udp-host requires a value")),
|
||||||
|
"--osc-port" => osc_port = Some(parse_num(&mut args, "--osc-port") as u16),
|
||||||
|
"--osc-host" => osc_host = args.next().unwrap_or_else(|| die("--osc-host requires a value")),
|
||||||
|
"--midi" => midi = true,
|
||||||
|
"--no-midi" => midi = false,
|
||||||
|
"--midi-name" => {
|
||||||
|
midi = true;
|
||||||
|
midi_name = Some(args.next().unwrap_or_else(|| die("--midi-name requires a value")));
|
||||||
|
}
|
||||||
|
"--list-midi" => {
|
||||||
|
let ports = MidiOut::list_ports();
|
||||||
|
if ports.is_empty() {
|
||||||
|
println!("no MIDI output ports found");
|
||||||
|
} else {
|
||||||
|
for p in ports {
|
||||||
|
println!("{p}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
"-h" | "--help" => {
|
||||||
|
print!("{USAGE}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ if path.is_none() && !arg.starts_with('-') => path = Some(arg),
|
||||||
|
_ => die(USAGE),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut outputs = EventOutputs::default();
|
||||||
|
if let Some(port) = udp_port {
|
||||||
|
outputs.udp =
|
||||||
|
Some(UdpOut::new(&udp_host, port).unwrap_or_else(|e| die(&format!("UDP setup failed: {e}"))));
|
||||||
|
}
|
||||||
|
if let Some(port) = osc_port {
|
||||||
|
outputs.osc =
|
||||||
|
Some(OscOut::new(&osc_host, port).unwrap_or_else(|e| die(&format!("OSC setup failed: {e}"))));
|
||||||
|
}
|
||||||
|
if midi {
|
||||||
|
outputs.midi = Some(MidiOut::open(midi_name.as_deref()).unwrap_or_else(|e| {
|
||||||
|
die(&format!("MIDI setup failed: {e} (use --no-midi to run without MIDI)"))
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = tui::run(TuiConfig { path, bpm, seed, outputs }) {
|
||||||
|
die(&e);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
use std::io::{self, BufRead, Write};
|
||||||
|
use std::fs::File;
|
||||||
|
use super::{Glyph, Mark};
|
||||||
|
|
||||||
|
pub struct Field {
|
||||||
|
pub buffer: Vec<Glyph>,
|
||||||
|
pub width: u16,
|
||||||
|
pub height: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MarkBuffer {
|
||||||
|
pub buffer: Vec<Mark>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum FieldLoadError {
|
||||||
|
CantOpen,
|
||||||
|
TooManyColumns,
|
||||||
|
TooManyRows,
|
||||||
|
NoRowsRead,
|
||||||
|
NotARectangle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FieldLoadError {
|
||||||
|
pub fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::CantOpen => "cannot open file",
|
||||||
|
Self::TooManyColumns => "too many columns",
|
||||||
|
Self::TooManyRows => "too many rows",
|
||||||
|
Self::NoRowsRead => "no rows read",
|
||||||
|
Self::NotARectangle => "not a rectangle",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Field {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { buffer: Vec::new(), width: 0, height: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_fill(height: usize, width: usize, fill: Glyph) -> Self {
|
||||||
|
Self {
|
||||||
|
buffer: vec![fill; height * width],
|
||||||
|
width: width as u16,
|
||||||
|
height: height as u16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resize(&mut self, height: usize, width: usize) {
|
||||||
|
let mut new_buffer = vec![b'.'; height * width];
|
||||||
|
super::copy_subrect(
|
||||||
|
&self.buffer,
|
||||||
|
&mut new_buffer,
|
||||||
|
self.height as usize,
|
||||||
|
self.width as usize,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
(self.height as usize).min(height),
|
||||||
|
(self.width as usize).min(width),
|
||||||
|
);
|
||||||
|
self.buffer = new_buffer;
|
||||||
|
self.width = width as u16;
|
||||||
|
self.height = height as u16;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_file(path: &str) -> Result<Self, FieldLoadError> {
|
||||||
|
let file = File::open(path).map_err(|_| FieldLoadError::CantOpen)?;
|
||||||
|
let reader = io::BufReader::new(file);
|
||||||
|
let mut rows: Vec<Vec<u8>> = Vec::new();
|
||||||
|
let mut row_width: Option<usize> = None;
|
||||||
|
|
||||||
|
for line in reader.lines() {
|
||||||
|
let line = line.map_err(|_| FieldLoadError::CantOpen)?;
|
||||||
|
let row: Vec<u8> = line.trim_end_matches('\r').bytes().collect();
|
||||||
|
|
||||||
|
if rows.len() >= u16::MAX as usize {
|
||||||
|
return Err(FieldLoadError::TooManyRows);
|
||||||
|
}
|
||||||
|
if row.len() > u16::MAX as usize {
|
||||||
|
return Err(FieldLoadError::TooManyColumns);
|
||||||
|
}
|
||||||
|
match row_width {
|
||||||
|
None => row_width = Some(row.len()),
|
||||||
|
Some(w) if w != row.len() => return Err(FieldLoadError::NotARectangle),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
rows.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows.is_empty() {
|
||||||
|
return Err(FieldLoadError::NoRowsRead);
|
||||||
|
}
|
||||||
|
|
||||||
|
let width = row_width.unwrap_or(0);
|
||||||
|
let height = rows.len();
|
||||||
|
let buffer = rows.into_iter().flatten().collect();
|
||||||
|
Ok(Self { buffer, width: width as u16, height: height as u16 })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_to<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
|
let h = self.height as usize;
|
||||||
|
let wd = self.width as usize;
|
||||||
|
for iy in 0..h {
|
||||||
|
w.write_all(&self.buffer[iy * wd..(iy + 1) * wd])?;
|
||||||
|
writeln!(w)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Field {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MarkBuffer {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { buffer: Vec::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ensure_size(&mut self, height: usize, width: usize) {
|
||||||
|
let needed = height * width;
|
||||||
|
if self.buffer.len() < needed {
|
||||||
|
self.buffer.resize(needed, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self, height: usize, width: usize) {
|
||||||
|
let n = height * width;
|
||||||
|
if self.buffer.len() >= n {
|
||||||
|
self.buffer[..n].fill(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MarkBuffer {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
// Coordinate-heavy signatures deliberately mirror the C gbuffer API.
|
||||||
|
#![allow(clippy::too_many_arguments)]
|
||||||
|
|
||||||
|
use super::{Glyph, Mark};
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn peek_rel(
|
||||||
|
gbuf: &[Glyph],
|
||||||
|
height: usize,
|
||||||
|
width: usize,
|
||||||
|
y: usize,
|
||||||
|
x: usize,
|
||||||
|
dy: isize,
|
||||||
|
dx: isize,
|
||||||
|
) -> Glyph {
|
||||||
|
let y0 = y as isize + dy;
|
||||||
|
let x0 = x as isize + dx;
|
||||||
|
if y0 < 0 || x0 < 0 || y0 as usize >= height || x0 as usize >= width {
|
||||||
|
return b'.';
|
||||||
|
}
|
||||||
|
gbuf[y0 as usize * width + x0 as usize]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn poke(gbuf: &mut [Glyph], height: usize, width: usize, y: usize, x: usize, g: Glyph) {
|
||||||
|
debug_assert!(y < height && x < width);
|
||||||
|
let _ = height;
|
||||||
|
gbuf[y * width + x] = g;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn poke_rel(
|
||||||
|
gbuf: &mut [Glyph],
|
||||||
|
height: usize,
|
||||||
|
width: usize,
|
||||||
|
y: usize,
|
||||||
|
x: usize,
|
||||||
|
dy: isize,
|
||||||
|
dx: isize,
|
||||||
|
g: Glyph,
|
||||||
|
) {
|
||||||
|
let y0 = y as isize + dy;
|
||||||
|
let x0 = x as isize + dx;
|
||||||
|
if y0 < 0 || x0 < 0 || y0 as usize >= height || x0 as usize >= width {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gbuf[y0 as usize * width + x0 as usize] = g;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn mbuf_poke_rel_or(
|
||||||
|
mbuf: &mut [Mark],
|
||||||
|
height: usize,
|
||||||
|
width: usize,
|
||||||
|
y: usize,
|
||||||
|
x: usize,
|
||||||
|
dy: isize,
|
||||||
|
dx: isize,
|
||||||
|
flags: Mark,
|
||||||
|
) {
|
||||||
|
let y0 = y as isize + dy;
|
||||||
|
let x0 = x as isize + dx;
|
||||||
|
if y0 < 0 || x0 < 0 || y0 as usize >= height || x0 as usize >= width {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mbuf[y0 as usize * width + x0 as usize] |= flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy_subrect(
|
||||||
|
src: &[Glyph],
|
||||||
|
dest: &mut [Glyph],
|
||||||
|
src_h: usize,
|
||||||
|
src_w: usize,
|
||||||
|
dest_h: usize,
|
||||||
|
dest_w: usize,
|
||||||
|
src_y: usize,
|
||||||
|
src_x: usize,
|
||||||
|
dest_y: usize,
|
||||||
|
dest_x: usize,
|
||||||
|
height: usize,
|
||||||
|
width: usize,
|
||||||
|
) {
|
||||||
|
if width == 0 || height == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for row in 0..height {
|
||||||
|
let sy = src_y + row;
|
||||||
|
let dy = dest_y + row;
|
||||||
|
if sy >= src_h || dy >= dest_h {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for col in 0..width {
|
||||||
|
let sx = src_x + col;
|
||||||
|
let dx = dest_x + col;
|
||||||
|
if sx >= src_w || dx >= dest_w {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
dest[dy * dest_w + dx] = src[sy * src_w + sx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fill_subrect(
|
||||||
|
gbuf: &mut [Glyph],
|
||||||
|
grid_h: usize,
|
||||||
|
grid_w: usize,
|
||||||
|
y: usize,
|
||||||
|
x: usize,
|
||||||
|
height: usize,
|
||||||
|
width: usize,
|
||||||
|
fill: Glyph,
|
||||||
|
) {
|
||||||
|
for row in 0..height {
|
||||||
|
let gy = y + row;
|
||||||
|
if gy >= grid_h {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for col in 0..width {
|
||||||
|
let gx = x + col;
|
||||||
|
if gx >= grid_w {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
gbuf[gy * grid_w + gx] = fill;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
mod field;
|
||||||
|
mod gbuffer;
|
||||||
|
mod vmio;
|
||||||
|
#[allow(clippy::module_inception)]
|
||||||
|
mod sim;
|
||||||
|
|
||||||
|
pub use field::{Field, MarkBuffer, FieldLoadError};
|
||||||
|
pub use gbuffer::{copy_subrect, fill_subrect};
|
||||||
|
pub use vmio::Oevent;
|
||||||
|
pub use sim::orca_run;
|
||||||
|
|
||||||
|
pub type Glyph = u8;
|
||||||
|
pub type Mark = u8;
|
||||||
|
|
||||||
|
pub const MARK_INPUT: Mark = 1 << 0;
|
||||||
|
pub const MARK_OUTPUT: Mark = 1 << 1;
|
||||||
|
pub const MARK_HASTE: Mark = 1 << 2;
|
||||||
|
pub const MARK_LOCK: Mark = 1 << 3;
|
||||||
|
pub const MARK_SLEEP: Mark = 1 << 4;
|
||||||
+832
@@ -0,0 +1,832 @@
|
|||||||
|
use super::vmio::Oevent;
|
||||||
|
use super::{MARK_HASTE, MARK_INPUT, MARK_LOCK, MARK_OUTPUT, MARK_SLEEP};
|
||||||
|
use super::gbuffer::{mbuf_poke_rel_or, peek_rel, poke, poke_rel};
|
||||||
|
|
||||||
|
// Port flag aliases matching the C macros
|
||||||
|
const IN: u8 = MARK_INPUT;
|
||||||
|
const OUT: u8 = MARK_OUTPUT;
|
||||||
|
const PARAM: u8 = MARK_HASTE;
|
||||||
|
const NONLOCKING: u8 = MARK_LOCK;
|
||||||
|
|
||||||
|
// glyph_table[i] = the glyph for numeric index i (0-35)
|
||||||
|
const GLYPH_TABLE: [u8; 36] = *b"0123456789abcdefghijklmnopqrstuvwxyz";
|
||||||
|
|
||||||
|
// index_table[ascii] = numeric index of that glyph (0-35); 0 for invalid
|
||||||
|
#[rustfmt::skip]
|
||||||
|
const INDEX_TABLE: [u8; 128] = [
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-15
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16-31
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 32-47
|
||||||
|
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, // 48-63
|
||||||
|
0, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, // 64-79
|
||||||
|
25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 0, 0, 0, 0, 0, // 80-95
|
||||||
|
0, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, // 96-111
|
||||||
|
25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 0, 0, 0, 0, 0, // 112-127
|
||||||
|
];
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn index_of(g: u8) -> usize {
|
||||||
|
INDEX_TABLE[(g & 0x7f) as usize] as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn glyph_of(idx: usize) -> u8 {
|
||||||
|
GLYPH_TABLE[idx % 36]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preserve the alphabetic case of `caser` in `g`; digits are returned unchanged.
|
||||||
|
// Mirrors the C bit-manipulation formula in sim.c.
|
||||||
|
#[inline]
|
||||||
|
fn glyph_with_case(g: u8, caser: u8) -> u8 {
|
||||||
|
const CASE_BIT: u8 = 1 << 5;
|
||||||
|
const ALPHA_BIT: u8 = 1 << 6;
|
||||||
|
(g & !CASE_BIT) | ((!g & ALPHA_BIT) >> 1) | (caser & CASE_BIT)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns MIDI pitch class (0-11 + optional sharp) for letter glyphs A-Z/a-z.
|
||||||
|
// Uppercase = natural, lowercase = sharp. A=H, B=I (i.e. C is the base note).
|
||||||
|
fn midi_note_number_of(g: u8) -> Option<u8> {
|
||||||
|
let sharp = (g >> 5) & 1;
|
||||||
|
let g = g & !0x20u8; // make uppercase
|
||||||
|
if !g.is_ascii_uppercase() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let deg = if g <= b'B' {
|
||||||
|
(b'G' - b'B' + g - b'A') as usize
|
||||||
|
} else {
|
||||||
|
(g - b'C') as usize
|
||||||
|
};
|
||||||
|
let semitones: [u8; 7] = [0, 2, 4, 5, 7, 9, 11];
|
||||||
|
Some(deg as u8 / 7 * 12 + semitones[deg % 7] + sharp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Operator context ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
struct Ctx<'a> {
|
||||||
|
gbuf: &'a mut [u8],
|
||||||
|
mbuf: &'a mut [u8],
|
||||||
|
height: usize,
|
||||||
|
width: usize,
|
||||||
|
y: usize,
|
||||||
|
x: usize,
|
||||||
|
tick: usize,
|
||||||
|
vars: &'a mut [u8; 36],
|
||||||
|
events: &'a mut Vec<Oevent>,
|
||||||
|
random_seed: usize,
|
||||||
|
this_char: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Ctx<'a> {
|
||||||
|
#[inline]
|
||||||
|
fn peek(&self, dy: isize, dx: isize) -> u8 {
|
||||||
|
peek_rel(self.gbuf, self.height, self.width, self.y, self.x, dy, dx)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn poke(&mut self, dy: isize, dx: isize, g: u8) {
|
||||||
|
poke_rel(self.gbuf, self.height, self.width, self.y, self.x, dy, dx, g);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn stun(&mut self, dy: isize, dx: isize) {
|
||||||
|
mbuf_poke_rel_or(self.mbuf, self.height, self.width, self.y, self.x, dy, dx, MARK_SLEEP);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write glyph and immediately mark it as sleeping (won't execute this tick).
|
||||||
|
#[inline]
|
||||||
|
fn poke_stunned(&mut self, dy: isize, dx: isize, g: u8) {
|
||||||
|
let y0 = self.y as isize + dy;
|
||||||
|
let x0 = self.x as isize + dx;
|
||||||
|
if y0 < 0 || x0 < 0 || y0 as usize >= self.height || x0 as usize >= self.width {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let offs = y0 as usize * self.width + x0 as usize;
|
||||||
|
self.gbuf[offs] = g;
|
||||||
|
self.mbuf[offs] |= MARK_SLEEP;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark a neighboring cell's port flags. XOR with LOCK matches the C PORT macro:
|
||||||
|
// INPUT/OUTPUT ports are automatically locked; NONLOCKING cancels that lock.
|
||||||
|
#[inline]
|
||||||
|
fn port(&mut self, dy: isize, dx: isize, flags: u8) {
|
||||||
|
mbuf_poke_rel_or(self.mbuf, self.height, self.width, self.y, self.x, dy, dx, flags ^ MARK_LOCK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn lock(&mut self, dy: isize, dx: isize) {
|
||||||
|
mbuf_poke_rel_or(self.mbuf, self.height, self.width, self.y, self.x, dy, dx, MARK_LOCK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn has_neighboring_bang(&self) -> bool {
|
||||||
|
let base = self.y * self.width + self.x;
|
||||||
|
let gbuf = &self.gbuf;
|
||||||
|
if self.x + 1 < self.width && gbuf[base + 1] == b'*' {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if self.x > 0 && gbuf[base - 1] == b'*' {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if self.y + 1 < self.height && gbuf[base + self.width] == b'*' {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if self.y > 0 && gbuf[base - self.width] == b'*' {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true when the operator should bail out early (lowercase but not banged).
|
||||||
|
#[inline]
|
||||||
|
fn lowercase_requires_bang(&self) -> bool {
|
||||||
|
(self.this_char & (1 << 5) != 0) && !self.has_neighboring_bang()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true when the operator should bail out (not banged at all).
|
||||||
|
#[inline]
|
||||||
|
fn stop_if_not_banged(&self) -> bool {
|
||||||
|
!self.has_neighboring_bang()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Operator implementations ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn oper_movement(ctx: &mut Ctx) {
|
||||||
|
if (ctx.this_char & (1 << 5) != 0) && !ctx.has_neighboring_bang() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (dy, dx): (isize, isize) = match ctx.this_char | 0x20 {
|
||||||
|
b'n' => (-1, 0),
|
||||||
|
b'e' => (0, 1),
|
||||||
|
b's' => (1, 0),
|
||||||
|
b'w' => (0, -1),
|
||||||
|
_ => (0, 0),
|
||||||
|
};
|
||||||
|
let y0 = ctx.y as isize + dy;
|
||||||
|
let x0 = ctx.x as isize + dx;
|
||||||
|
let cur = ctx.y * ctx.width + ctx.x;
|
||||||
|
if y0 < 0 || x0 < 0 || y0 as usize >= ctx.height || x0 as usize >= ctx.width {
|
||||||
|
ctx.gbuf[cur] = b'*';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let dest = y0 as usize * ctx.width + x0 as usize;
|
||||||
|
if ctx.gbuf[dest] == b'.' {
|
||||||
|
ctx.gbuf[dest] = ctx.this_char;
|
||||||
|
ctx.gbuf[cur] = b'.';
|
||||||
|
ctx.mbuf[dest] |= MARK_SLEEP;
|
||||||
|
} else {
|
||||||
|
ctx.gbuf[cur] = b'*';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_midicc(ctx: &mut Ctx) {
|
||||||
|
ctx.port(0, 1, IN);
|
||||||
|
ctx.port(0, 2, IN);
|
||||||
|
ctx.port(0, 3, IN);
|
||||||
|
if ctx.stop_if_not_banged() { return; }
|
||||||
|
let channel_g = ctx.peek(0, 1);
|
||||||
|
let control_g = ctx.peek(0, 2);
|
||||||
|
let value_g = ctx.peek(0, 3);
|
||||||
|
if channel_g == b'.' || control_g == b'.' { return; }
|
||||||
|
let channel = index_of(channel_g);
|
||||||
|
if channel > 15 { return; }
|
||||||
|
ctx.port(0, 0, OUT);
|
||||||
|
ctx.events.push(Oevent::MidiCc {
|
||||||
|
channel: channel as u8,
|
||||||
|
control: index_of(control_g) as u8,
|
||||||
|
value: (index_of(value_g) * 127 / 35) as u8,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_comment(ctx: &mut Ctx) {
|
||||||
|
let max_x = (ctx.x + 255).min(ctx.width);
|
||||||
|
for x0 in (ctx.x + 1)..max_x {
|
||||||
|
ctx.mbuf[ctx.y * ctx.width + x0] |= MARK_LOCK;
|
||||||
|
if ctx.gbuf[ctx.y * ctx.width + x0] == b'#' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_bang(ctx: &mut Ctx) {
|
||||||
|
poke(ctx.gbuf, ctx.height, ctx.width, ctx.y, ctx.x, b'.');
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_midi(ctx: &mut Ctx) {
|
||||||
|
for i in 1isize..=5 {
|
||||||
|
ctx.port(0, i, IN);
|
||||||
|
}
|
||||||
|
if ctx.stop_if_not_banged() { return; }
|
||||||
|
let channel_g = ctx.peek(0, 1);
|
||||||
|
let octave_g = ctx.peek(0, 2);
|
||||||
|
let note_g = ctx.peek(0, 3);
|
||||||
|
let velocity_g = ctx.peek(0, 4);
|
||||||
|
let length_g = ctx.peek(0, 5);
|
||||||
|
if octave_g == b'.' { return; }
|
||||||
|
let mut octave_num = index_of(octave_g) as u8;
|
||||||
|
if octave_num > 9 { octave_num = 9; }
|
||||||
|
let note_num = match midi_note_number_of(note_g) {
|
||||||
|
Some(n) => n,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let channel_num = {
|
||||||
|
let c = index_of(channel_g);
|
||||||
|
if c > 15 { 15 } else { c as u8 }
|
||||||
|
};
|
||||||
|
let vel_num: u8 = if velocity_g == b'.' {
|
||||||
|
127
|
||||||
|
} else {
|
||||||
|
let v = index_of(velocity_g);
|
||||||
|
if v == 0 { return; }
|
||||||
|
let v = v * 8 - 1;
|
||||||
|
if v > 127 { 127 } else { v as u8 }
|
||||||
|
};
|
||||||
|
ctx.port(0, 0, OUT);
|
||||||
|
ctx.events.push(Oevent::MidiNote {
|
||||||
|
channel: channel_num,
|
||||||
|
octave: octave_num,
|
||||||
|
note: note_num,
|
||||||
|
velocity: vel_num,
|
||||||
|
duration: (index_of(length_g) & 0x7f) as u8,
|
||||||
|
mono: ctx.this_char == b'%',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_udp(ctx: &mut Ctx) {
|
||||||
|
let n = (ctx.width - ctx.x - 1).min(super::vmio::UDP_STR_COUNT);
|
||||||
|
let row_base = ctx.y * ctx.width + ctx.x + 1;
|
||||||
|
let mut chars: Vec<u8> = Vec::new();
|
||||||
|
for i in 0..n {
|
||||||
|
let g = ctx.gbuf[row_base + i];
|
||||||
|
if g == b'.' { break; }
|
||||||
|
chars.push(g);
|
||||||
|
ctx.mbuf[row_base + i] |= MARK_LOCK;
|
||||||
|
}
|
||||||
|
if ctx.stop_if_not_banged() { return; }
|
||||||
|
ctx.port(0, 0, OUT);
|
||||||
|
ctx.events.push(Oevent::UdpString { chars });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_osc(ctx: &mut Ctx) {
|
||||||
|
ctx.port(0, 1, IN | PARAM);
|
||||||
|
ctx.port(0, 2, IN | PARAM);
|
||||||
|
let len = index_of(ctx.peek(0, 2)).min(super::vmio::OSC_INT_COUNT);
|
||||||
|
for i in 0..len {
|
||||||
|
ctx.port(0, i as isize + 3, IN);
|
||||||
|
}
|
||||||
|
if ctx.stop_if_not_banged() { return; }
|
||||||
|
let g = ctx.peek(0, 1);
|
||||||
|
if g == b'.' { return; }
|
||||||
|
ctx.port(0, 0, OUT);
|
||||||
|
let numbers: Vec<u8> = (0..len)
|
||||||
|
.map(|i| index_of(ctx.peek(0, i as isize + 3)) as u8)
|
||||||
|
.collect();
|
||||||
|
ctx.events.push(Oevent::OscInts { glyph: g, numbers });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_midipb(ctx: &mut Ctx) {
|
||||||
|
ctx.port(0, 1, IN);
|
||||||
|
ctx.port(0, 2, IN);
|
||||||
|
ctx.port(0, 3, IN);
|
||||||
|
if ctx.stop_if_not_banged() { return; }
|
||||||
|
let channel_g = ctx.peek(0, 1);
|
||||||
|
let msb_g = ctx.peek(0, 2);
|
||||||
|
let lsb_g = ctx.peek(0, 3);
|
||||||
|
if channel_g == b'.' { return; }
|
||||||
|
let channel = index_of(channel_g);
|
||||||
|
if channel > 15 { return; }
|
||||||
|
ctx.port(0, 0, OUT);
|
||||||
|
ctx.events.push(Oevent::MidiPb {
|
||||||
|
channel: channel as u8,
|
||||||
|
msb: (index_of(msb_g) * 127 / 35) as u8,
|
||||||
|
lsb: (index_of(lsb_g) * 127 / 35) as u8,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_add(ctx: &mut Ctx) {
|
||||||
|
if ctx.lowercase_requires_bang() { return; }
|
||||||
|
ctx.port(0, -1, IN | PARAM);
|
||||||
|
ctx.port(0, 1, IN);
|
||||||
|
ctx.port(1, 0, OUT);
|
||||||
|
let a = ctx.peek(0, -1);
|
||||||
|
let b = ctx.peek(0, 1);
|
||||||
|
let g = glyph_of((index_of(a) + index_of(b)) % 36);
|
||||||
|
ctx.poke(1, 0, glyph_with_case(g, b));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_subtract(ctx: &mut Ctx) {
|
||||||
|
if ctx.lowercase_requires_bang() { return; }
|
||||||
|
ctx.port(0, -1, IN | PARAM);
|
||||||
|
ctx.port(0, 1, IN);
|
||||||
|
ctx.port(1, 0, OUT);
|
||||||
|
let a = ctx.peek(0, -1);
|
||||||
|
let b = ctx.peek(0, 1);
|
||||||
|
let val = (index_of(b) as isize - index_of(a) as isize).unsigned_abs();
|
||||||
|
ctx.poke(1, 0, glyph_with_case(glyph_of(val), b));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_clock(ctx: &mut Ctx) {
|
||||||
|
if ctx.lowercase_requires_bang() { return; }
|
||||||
|
ctx.port(0, -1, IN | PARAM);
|
||||||
|
ctx.port(0, 1, IN);
|
||||||
|
ctx.port(1, 0, OUT);
|
||||||
|
let b = ctx.peek(0, 1);
|
||||||
|
let rate = {
|
||||||
|
let r = index_of(ctx.peek(0, -1));
|
||||||
|
if r == 0 { 1 } else { r }
|
||||||
|
};
|
||||||
|
let mod_num = {
|
||||||
|
let m = index_of(b);
|
||||||
|
if m == 0 { 8 } else { m }
|
||||||
|
};
|
||||||
|
let g = glyph_of(ctx.tick / rate % mod_num);
|
||||||
|
ctx.poke(1, 0, glyph_with_case(g, b));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_delay(ctx: &mut Ctx) {
|
||||||
|
if ctx.lowercase_requires_bang() { return; }
|
||||||
|
ctx.port(0, -1, IN | PARAM);
|
||||||
|
ctx.port(0, 1, IN);
|
||||||
|
ctx.port(1, 0, OUT);
|
||||||
|
let rate = {
|
||||||
|
let r = index_of(ctx.peek(0, -1));
|
||||||
|
if r == 0 { 1 } else { r }
|
||||||
|
};
|
||||||
|
let mod_num = {
|
||||||
|
let m = index_of(ctx.peek(0, 1));
|
||||||
|
if m == 0 { 8 } else { m }
|
||||||
|
};
|
||||||
|
let g = if ctx.tick.is_multiple_of(rate * mod_num) { b'*' } else { b'.' };
|
||||||
|
ctx.poke(1, 0, g);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_if(ctx: &mut Ctx) {
|
||||||
|
if ctx.lowercase_requires_bang() { return; }
|
||||||
|
ctx.port(0, -1, IN | PARAM);
|
||||||
|
ctx.port(0, 1, IN);
|
||||||
|
ctx.port(1, 0, OUT);
|
||||||
|
let g0 = ctx.peek(0, -1);
|
||||||
|
let g1 = ctx.peek(0, 1);
|
||||||
|
ctx.poke(1, 0, if g0 == g1 { b'*' } else { b'.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_generator(ctx: &mut Ctx) {
|
||||||
|
if ctx.lowercase_requires_bang() { return; }
|
||||||
|
let out_x = index_of(ctx.peek(0, -3)) as isize;
|
||||||
|
let out_y = index_of(ctx.peek(0, -2)) as isize + 1;
|
||||||
|
let len = index_of(ctx.peek(0, -1)) as isize;
|
||||||
|
ctx.port(0, -3, IN | PARAM);
|
||||||
|
ctx.port(0, -2, IN | PARAM);
|
||||||
|
ctx.port(0, -1, IN | PARAM);
|
||||||
|
for i in 0..len {
|
||||||
|
ctx.port(0, i + 1, IN);
|
||||||
|
ctx.port(out_y, out_x + i, OUT | NONLOCKING);
|
||||||
|
let g = ctx.peek(0, i + 1);
|
||||||
|
ctx.poke_stunned(out_y, out_x + i, g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_halt(ctx: &mut Ctx) {
|
||||||
|
if ctx.lowercase_requires_bang() { return; }
|
||||||
|
ctx.port(1, 0, IN | PARAM);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_increment(ctx: &mut Ctx) {
|
||||||
|
if ctx.lowercase_requires_bang() { return; }
|
||||||
|
ctx.port(0, -1, IN | PARAM);
|
||||||
|
ctx.port(0, 1, IN);
|
||||||
|
ctx.port(1, 0, IN | OUT);
|
||||||
|
let ga = ctx.peek(0, -1);
|
||||||
|
let gb = ctx.peek(0, 1);
|
||||||
|
let rate = if ga == b'.' || ga == b'*' { 1 } else { index_of(ga) };
|
||||||
|
let max = { let m = index_of(gb); if m == 0 { 36 } else { m } };
|
||||||
|
let val = (index_of(ctx.peek(1, 0)) + rate) % max;
|
||||||
|
ctx.poke(1, 0, glyph_with_case(glyph_of(val), gb));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_jump(ctx: &mut Ctx) {
|
||||||
|
if ctx.lowercase_requires_bang() { return; }
|
||||||
|
let g = ctx.peek(-1, 0);
|
||||||
|
if g == b'J' { return; }
|
||||||
|
ctx.port(-1, 0, IN);
|
||||||
|
for i in 1isize..=256 {
|
||||||
|
if ctx.peek(i, 0) != ctx.this_char {
|
||||||
|
ctx.port(i, 0, OUT);
|
||||||
|
ctx.poke(i, 0, g);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ctx.stun(i, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_konkat(ctx: &mut Ctx) {
|
||||||
|
if ctx.lowercase_requires_bang() { return; }
|
||||||
|
let len = {
|
||||||
|
let l = index_of(ctx.peek(0, -1)) as isize;
|
||||||
|
if l == 0 { 1 } else { l }
|
||||||
|
};
|
||||||
|
ctx.port(0, -1, IN | PARAM);
|
||||||
|
for i in 0..len {
|
||||||
|
ctx.port(0, i + 1, IN);
|
||||||
|
let var = ctx.peek(0, i + 1);
|
||||||
|
if var != b'.' {
|
||||||
|
let result = ctx.vars[index_of(var)];
|
||||||
|
ctx.port(1, i + 1, OUT);
|
||||||
|
ctx.poke(1, i + 1, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_lesser(ctx: &mut Ctx) {
|
||||||
|
if ctx.lowercase_requires_bang() { return; }
|
||||||
|
ctx.port(0, -1, IN | PARAM);
|
||||||
|
ctx.port(0, 1, IN);
|
||||||
|
ctx.port(1, 0, OUT);
|
||||||
|
let ga = ctx.peek(0, -1);
|
||||||
|
let gb = ctx.peek(0, 1);
|
||||||
|
if ga == b'.' || gb == b'.' {
|
||||||
|
ctx.poke(1, 0, b'.');
|
||||||
|
} else {
|
||||||
|
let ia = index_of(ga);
|
||||||
|
let ib = index_of(gb);
|
||||||
|
let out = ia.min(ib);
|
||||||
|
ctx.poke(1, 0, glyph_with_case(glyph_of(out), gb));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_multiply(ctx: &mut Ctx) {
|
||||||
|
if ctx.lowercase_requires_bang() { return; }
|
||||||
|
ctx.port(0, -1, IN | PARAM);
|
||||||
|
ctx.port(0, 1, IN);
|
||||||
|
ctx.port(1, 0, OUT);
|
||||||
|
let a = ctx.peek(0, -1);
|
||||||
|
let b = ctx.peek(0, 1);
|
||||||
|
let g = glyph_of((index_of(a) * index_of(b)) % 36);
|
||||||
|
ctx.poke(1, 0, glyph_with_case(g, b));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_offset(ctx: &mut Ctx) {
|
||||||
|
if ctx.lowercase_requires_bang() { return; }
|
||||||
|
let in_x = index_of(ctx.peek(0, -2)) as isize + 1;
|
||||||
|
let in_y = index_of(ctx.peek(0, -1)) as isize;
|
||||||
|
ctx.port(0, -1, IN | PARAM);
|
||||||
|
ctx.port(0, -2, IN | PARAM);
|
||||||
|
ctx.port(in_y, in_x, IN);
|
||||||
|
ctx.port(1, 0, OUT);
|
||||||
|
let g = ctx.peek(in_y, in_x);
|
||||||
|
ctx.poke(1, 0, g);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_push(ctx: &mut Ctx) {
|
||||||
|
if ctx.lowercase_requires_bang() { return; }
|
||||||
|
let key = index_of(ctx.peek(0, -2));
|
||||||
|
let len = index_of(ctx.peek(0, -1));
|
||||||
|
ctx.port(0, -1, IN | PARAM);
|
||||||
|
ctx.port(0, -2, IN | PARAM);
|
||||||
|
ctx.port(0, 1, IN);
|
||||||
|
if len == 0 { return; }
|
||||||
|
let out_x = (key % len) as isize;
|
||||||
|
for i in 0..len as isize {
|
||||||
|
ctx.lock(1, i);
|
||||||
|
}
|
||||||
|
ctx.port(1, out_x, OUT);
|
||||||
|
let g = ctx.peek(0, 1);
|
||||||
|
ctx.poke(1, out_x, g);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_query(ctx: &mut Ctx) {
|
||||||
|
if ctx.lowercase_requires_bang() { return; }
|
||||||
|
let in_x = index_of(ctx.peek(0, -3)) as isize + 1;
|
||||||
|
let in_y = index_of(ctx.peek(0, -2)) as isize;
|
||||||
|
let len = index_of(ctx.peek(0, -1)) as isize;
|
||||||
|
let out_x = 1 - len;
|
||||||
|
ctx.port(0, -3, IN | PARAM);
|
||||||
|
ctx.port(0, -2, IN | PARAM);
|
||||||
|
ctx.port(0, -1, IN | PARAM);
|
||||||
|
for i in 0..len {
|
||||||
|
ctx.port(in_y, in_x + i, IN);
|
||||||
|
ctx.port(1, out_x + i, OUT);
|
||||||
|
let g = ctx.peek(in_y, in_x + i);
|
||||||
|
ctx.poke(1, out_x + i, g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_random(ctx: &mut Ctx) {
|
||||||
|
if ctx.lowercase_requires_bang() { return; }
|
||||||
|
ctx.port(0, -1, IN | PARAM);
|
||||||
|
ctx.port(0, 1, IN);
|
||||||
|
ctx.port(1, 0, OUT);
|
||||||
|
let gb = ctx.peek(0, 1);
|
||||||
|
let a = index_of(ctx.peek(0, -1));
|
||||||
|
let b = { let b = index_of(gb); if b == 0 { 36 } else { b } };
|
||||||
|
let (min, max) = if a == b {
|
||||||
|
ctx.poke(1, 0, glyph_of(a));
|
||||||
|
return;
|
||||||
|
} else if a < b {
|
||||||
|
(a, b)
|
||||||
|
} else {
|
||||||
|
(b, a)
|
||||||
|
};
|
||||||
|
// 32-bit shift-multiply hash — same algorithm as C sim.c
|
||||||
|
let mut key = (ctx.random_seed + ctx.y * ctx.width + ctx.x)
|
||||||
|
^ (ctx.tick << 16);
|
||||||
|
key = (key ^ 61) ^ (key >> 16);
|
||||||
|
key = key.wrapping_add(key << 3);
|
||||||
|
key ^= key >> 4;
|
||||||
|
key = key.wrapping_mul(0x27d4eb2d);
|
||||||
|
key ^= key >> 15;
|
||||||
|
let val = key % (max - min) + min;
|
||||||
|
ctx.poke(1, 0, glyph_with_case(glyph_of(val), gb));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_track(ctx: &mut Ctx) {
|
||||||
|
if ctx.lowercase_requires_bang() { return; }
|
||||||
|
let key = index_of(ctx.peek(0, -2));
|
||||||
|
let len = index_of(ctx.peek(0, -1));
|
||||||
|
ctx.port(0, -2, IN | PARAM);
|
||||||
|
ctx.port(0, -1, IN | PARAM);
|
||||||
|
if len == 0 { return; }
|
||||||
|
let read_x = (key % len) as isize + 1;
|
||||||
|
for i in 0..len as isize {
|
||||||
|
ctx.lock(0, i + 1);
|
||||||
|
}
|
||||||
|
ctx.port(0, read_x, IN);
|
||||||
|
ctx.port(1, 0, OUT);
|
||||||
|
let g = ctx.peek(0, read_x);
|
||||||
|
ctx.poke(1, 0, g);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_uclid(ctx: &mut Ctx) {
|
||||||
|
if ctx.lowercase_requires_bang() { return; }
|
||||||
|
ctx.port(0, -1, IN | PARAM);
|
||||||
|
ctx.port(0, 1, IN);
|
||||||
|
ctx.port(1, 0, OUT);
|
||||||
|
let left = ctx.peek(0, -1);
|
||||||
|
let steps = if left == b'.' || left == b'*' { 1 } else { index_of(left) };
|
||||||
|
let max = { let m = index_of(ctx.peek(0, 1)); if m == 0 { 8 } else { m } };
|
||||||
|
let bucket = (steps * (ctx.tick + max - 1)) % max + steps;
|
||||||
|
ctx.poke(1, 0, if bucket >= max { b'*' } else { b'.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_variable(ctx: &mut Ctx) {
|
||||||
|
if ctx.lowercase_requires_bang() { return; }
|
||||||
|
ctx.port(0, -1, IN | PARAM);
|
||||||
|
ctx.port(0, 1, IN);
|
||||||
|
let left = ctx.peek(0, -1);
|
||||||
|
let right = ctx.peek(0, 1);
|
||||||
|
if left != b'.' {
|
||||||
|
ctx.vars[index_of(left)] = right;
|
||||||
|
} else if right != b'.' {
|
||||||
|
ctx.port(1, 0, OUT);
|
||||||
|
let result = ctx.vars[index_of(right)];
|
||||||
|
ctx.poke(1, 0, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_teleport(ctx: &mut Ctx) {
|
||||||
|
if ctx.lowercase_requires_bang() { return; }
|
||||||
|
let out_x = index_of(ctx.peek(0, -2)) as isize;
|
||||||
|
let out_y = index_of(ctx.peek(0, -1)) as isize + 1;
|
||||||
|
ctx.port(0, -2, IN | PARAM);
|
||||||
|
ctx.port(0, -1, IN | PARAM);
|
||||||
|
ctx.port(0, 1, IN);
|
||||||
|
ctx.port(out_y, out_x, OUT | NONLOCKING);
|
||||||
|
let g = ctx.peek(0, 1);
|
||||||
|
ctx.poke_stunned(out_y, out_x, g);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_yump(ctx: &mut Ctx) {
|
||||||
|
if ctx.lowercase_requires_bang() { return; }
|
||||||
|
let g = ctx.peek(0, -1);
|
||||||
|
if g == b'Y' { return; }
|
||||||
|
ctx.port(0, -1, IN);
|
||||||
|
for i in 1isize..=256 {
|
||||||
|
if ctx.peek(0, i) != ctx.this_char {
|
||||||
|
ctx.port(0, i, OUT);
|
||||||
|
ctx.poke(0, i, g);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ctx.stun(0, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oper_lerp(ctx: &mut Ctx) {
|
||||||
|
if ctx.lowercase_requires_bang() { return; }
|
||||||
|
ctx.port(0, -1, IN | PARAM);
|
||||||
|
ctx.port(0, 1, IN);
|
||||||
|
ctx.port(1, 0, IN | OUT);
|
||||||
|
let g = ctx.peek(0, -1);
|
||||||
|
let b = ctx.peek(0, 1);
|
||||||
|
let rate: isize = if g == b'.' || g == b'*' { 1 } else { index_of(g) as isize };
|
||||||
|
let goal = index_of(b) as isize;
|
||||||
|
let val = index_of(ctx.peek(1, 0)) as isize;
|
||||||
|
let modv = if val <= goal - rate {
|
||||||
|
rate
|
||||||
|
} else if val >= goal + rate {
|
||||||
|
-rate
|
||||||
|
} else {
|
||||||
|
goal - val
|
||||||
|
};
|
||||||
|
ctx.poke(1, 0, glyph_with_case(glyph_of((val + modv) as usize), b));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Dispatch ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn dispatch(ctx: &mut Ctx) {
|
||||||
|
match ctx.this_char {
|
||||||
|
b'!' => oper_midicc(ctx),
|
||||||
|
b'#' => oper_comment(ctx),
|
||||||
|
b'%' | b':' => oper_midi(ctx),
|
||||||
|
b'*' => oper_bang(ctx),
|
||||||
|
b';' => oper_udp(ctx),
|
||||||
|
b'=' => oper_osc(ctx),
|
||||||
|
b'?' => oper_midipb(ctx),
|
||||||
|
b'A' | b'a' => oper_add(ctx),
|
||||||
|
b'B' | b'b' => oper_subtract(ctx),
|
||||||
|
b'C' | b'c' => oper_clock(ctx),
|
||||||
|
b'D' | b'd' => oper_delay(ctx),
|
||||||
|
b'E' | b'e' => oper_movement(ctx),
|
||||||
|
b'F' | b'f' => oper_if(ctx),
|
||||||
|
b'G' | b'g' => oper_generator(ctx),
|
||||||
|
b'H' | b'h' => oper_halt(ctx),
|
||||||
|
b'I' | b'i' => oper_increment(ctx),
|
||||||
|
b'J' | b'j' => oper_jump(ctx),
|
||||||
|
b'K' | b'k' => oper_konkat(ctx),
|
||||||
|
b'L' | b'l' => oper_lesser(ctx),
|
||||||
|
b'M' | b'm' => oper_multiply(ctx),
|
||||||
|
b'N' | b'n' => oper_movement(ctx),
|
||||||
|
b'O' | b'o' => oper_offset(ctx),
|
||||||
|
b'P' | b'p' => oper_push(ctx),
|
||||||
|
b'Q' | b'q' => oper_query(ctx),
|
||||||
|
b'R' | b'r' => oper_random(ctx),
|
||||||
|
b'S' | b's' => oper_movement(ctx),
|
||||||
|
b'T' | b't' => oper_track(ctx),
|
||||||
|
b'U' | b'u' => oper_uclid(ctx),
|
||||||
|
b'V' | b'v' => oper_variable(ctx),
|
||||||
|
b'W' | b'w' => oper_movement(ctx),
|
||||||
|
b'X' | b'x' => oper_teleport(ctx),
|
||||||
|
b'Y' | b'y' => oper_yump(ctx),
|
||||||
|
b'Z' | b'z' => oper_lerp(ctx),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Public entry point ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Run one tick of the Orca VM.
|
||||||
|
///
|
||||||
|
/// `gbuf` and `mbuf` must both have length `height * width`.
|
||||||
|
/// `mbuf` should be zeroed before each call (mark flags are per-tick).
|
||||||
|
/// Events generated this tick are appended to `events`.
|
||||||
|
pub fn orca_run(
|
||||||
|
gbuf: &mut [u8],
|
||||||
|
mbuf: &mut [u8],
|
||||||
|
height: usize,
|
||||||
|
width: usize,
|
||||||
|
tick: usize,
|
||||||
|
events: &mut Vec<Oevent>,
|
||||||
|
random_seed: usize,
|
||||||
|
) {
|
||||||
|
debug_assert_eq!(gbuf.len(), height * width);
|
||||||
|
debug_assert_eq!(mbuf.len(), height * width);
|
||||||
|
let mut vars = [b'.'; 36];
|
||||||
|
for iy in 0..height {
|
||||||
|
for ix in 0..width {
|
||||||
|
let glyph_char = gbuf[iy * width + ix];
|
||||||
|
if glyph_char == b'.' {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let cell_flags = mbuf[iy * width + ix] & (MARK_LOCK | MARK_SLEEP);
|
||||||
|
if cell_flags != 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut ctx = Ctx {
|
||||||
|
gbuf: &mut *gbuf,
|
||||||
|
mbuf: &mut *mbuf,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
y: iy,
|
||||||
|
x: ix,
|
||||||
|
tick,
|
||||||
|
vars: &mut vars,
|
||||||
|
events,
|
||||||
|
random_seed,
|
||||||
|
this_char: glyph_char,
|
||||||
|
};
|
||||||
|
dispatch(&mut ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn run_grid(src: &str, ticks: usize) -> (Vec<u8>, Vec<Oevent>) {
|
||||||
|
let lines: Vec<&str> = src.lines().collect();
|
||||||
|
let height = lines.len();
|
||||||
|
let width = lines.iter().map(|l| l.len()).max().unwrap_or(0);
|
||||||
|
let mut gbuf: Vec<u8> = vec![b'.'; height * width];
|
||||||
|
for (iy, line) in lines.iter().enumerate() {
|
||||||
|
for (ix, b) in line.bytes().enumerate() {
|
||||||
|
gbuf[iy * width + ix] = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut mbuf = vec![0u8; height * width];
|
||||||
|
let mut events = Vec::new();
|
||||||
|
for tick in 0..ticks {
|
||||||
|
mbuf.fill(0);
|
||||||
|
orca_run(&mut gbuf, &mut mbuf, height, width, tick, &mut events, 1);
|
||||||
|
}
|
||||||
|
(gbuf, events)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bang_clears_itself() {
|
||||||
|
let (grid, _) = run_grid("*", 1);
|
||||||
|
assert_eq!(grid[0], b'.');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn movement_north_hits_wall() {
|
||||||
|
// N at top row should turn into *
|
||||||
|
let (grid, _) = run_grid("N..", 1);
|
||||||
|
assert_eq!(grid[0], b'*');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn movement_south_moves_down() {
|
||||||
|
let (grid, _) = run_grid("S\n.\n.", 1);
|
||||||
|
// 3×1 grid; S moves to row 1 (index 1)
|
||||||
|
assert_eq!(grid[0], b'.');
|
||||||
|
assert_eq!(grid[1], b'S');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn movement_east_moves_right() {
|
||||||
|
let (grid, _) = run_grid("E.", 1);
|
||||||
|
assert_eq!(grid[0], b'.');
|
||||||
|
assert_eq!(grid[1], b'E');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clock_basic() {
|
||||||
|
// .2C. — rate=2, mod=default(8), tick=0 → output=glyph_of(0/2%8)=glyph_of(0)='0'
|
||||||
|
// C is at x=2, output at (1,2) so the grid needs 2 rows:
|
||||||
|
let (grid, _) = run_grid(".2C.\n....", 1);
|
||||||
|
// output cell is row1, col2 = index 1*4+2 = 6
|
||||||
|
assert_eq!(grid[6], b'0');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_operator() {
|
||||||
|
// .A3. where left param='.', right='3' → (0+3)%36 = '3', case of '3' = '3'
|
||||||
|
// 3A3. — left='3'(=3), right='3'(=3) → (3+3)%36=6='6'
|
||||||
|
let (grid, _) = run_grid("3A3.\n....", 1);
|
||||||
|
// output at row1,col1 = index 4+1 = 5
|
||||||
|
assert_eq!(grid[5], b'6');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delay_fires_at_zero() {
|
||||||
|
// .D. with rate=1, mod=8 → fires at tick%8==0
|
||||||
|
// .1D8 — but we need output below
|
||||||
|
let (grid, _) = run_grid(".1D8\n....", 1);
|
||||||
|
// tick=0: 0 % (1*8) == 0 → output '*'
|
||||||
|
assert_eq!(grid[4 + 2], b'*');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn midi_note_event() {
|
||||||
|
// ':' at (0,0) with '*' directly below at (1,0). The '*' is processed
|
||||||
|
// after row 0, so ':' still sees it as a neighbor during its own tick.
|
||||||
|
// Params: channel='0'→0, octave='5'→5, note='C'→C natural, vel='.', len='.'
|
||||||
|
let (_, events) = run_grid(":05C..\n*.....", 1);
|
||||||
|
assert!(events.iter().any(|e| matches!(e, Oevent::MidiNote { channel: 0, octave: 5, note: 0, .. })));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn if_equal_outputs_bang() {
|
||||||
|
// aFa. — a==a → '*'
|
||||||
|
let (grid, _) = run_grid("aFa.\n....", 1);
|
||||||
|
assert_eq!(grid[4 + 1], b'*'); // output at row1,col1
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn if_unequal_outputs_dot() {
|
||||||
|
// aFb. — a!=b → '.'
|
||||||
|
let (grid, _) = run_grid("aFb.\n....", 1);
|
||||||
|
assert_eq!(grid[4 + 1], b'.');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn increment_wraps() {
|
||||||
|
// .I. with value at row1='9'(=9), no max → max=36
|
||||||
|
// rate=1, so 9+1=10 → 'a'
|
||||||
|
let (grid, _) = run_grid(".I.\n.9.", 1);
|
||||||
|
// output/input at row1,col1 = index 4
|
||||||
|
assert_eq!(grid[4], b'a');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
pub const OSC_INT_COUNT: usize = 35;
|
||||||
|
pub const UDP_STR_COUNT: usize = 16;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum Oevent {
|
||||||
|
MidiNote {
|
||||||
|
channel: u8,
|
||||||
|
octave: u8,
|
||||||
|
note: u8,
|
||||||
|
velocity: u8,
|
||||||
|
duration: u8,
|
||||||
|
mono: bool,
|
||||||
|
},
|
||||||
|
MidiCc {
|
||||||
|
channel: u8,
|
||||||
|
control: u8,
|
||||||
|
value: u8,
|
||||||
|
},
|
||||||
|
MidiPb {
|
||||||
|
channel: u8,
|
||||||
|
lsb: u8,
|
||||||
|
msb: u8,
|
||||||
|
},
|
||||||
|
OscInts {
|
||||||
|
glyph: u8,
|
||||||
|
numbers: Vec<u8>,
|
||||||
|
},
|
||||||
|
UdpString {
|
||||||
|
chars: Vec<u8>,
|
||||||
|
},
|
||||||
|
}
|
||||||
+494
@@ -0,0 +1,494 @@
|
|||||||
|
//! Interactive terminal UI — the crossterm counterpart of orca-c's
|
||||||
|
//! ncurses tui_main.c: grid editor, playback clock, selection, undo,
|
||||||
|
//! and event delivery to the configured outputs.
|
||||||
|
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{self, Write};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use crossterm::cursor::{Hide, MoveTo, Show};
|
||||||
|
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||||||
|
use crossterm::style::{Attribute, Print, SetAttribute};
|
||||||
|
use crossterm::terminal::{
|
||||||
|
self, disable_raw_mode, enable_raw_mode, BeginSynchronizedUpdate, Clear, ClearType,
|
||||||
|
EndSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen,
|
||||||
|
};
|
||||||
|
use crossterm::{execute, queue};
|
||||||
|
|
||||||
|
use crate::io::EventOutputs;
|
||||||
|
use crate::sim::{
|
||||||
|
copy_subrect, fill_subrect, orca_run, Field, MarkBuffer, Oevent, MARK_HASTE, MARK_INPUT,
|
||||||
|
MARK_LOCK, MARK_OUTPUT, MARK_SLEEP,
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_UNDO: usize = 256;
|
||||||
|
const MAX_DIM: usize = 512;
|
||||||
|
const HELP_LINE: &str =
|
||||||
|
"space play ^F step ^S save ^Z undo ^X/^C/^V clip shift+arrows select ^arrows resize < > ( ) bpm ^Q quit";
|
||||||
|
|
||||||
|
pub struct TuiConfig {
|
||||||
|
pub path: Option<String>,
|
||||||
|
pub bpm: usize,
|
||||||
|
pub seed: usize,
|
||||||
|
pub outputs: EventOutputs,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Clip {
|
||||||
|
data: Vec<u8>,
|
||||||
|
h: usize,
|
||||||
|
w: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Snapshot {
|
||||||
|
data: Vec<u8>,
|
||||||
|
h: usize,
|
||||||
|
w: usize,
|
||||||
|
tick: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct App {
|
||||||
|
field: Field,
|
||||||
|
mbuf: MarkBuffer,
|
||||||
|
scratch: Vec<u8>,
|
||||||
|
tick: usize,
|
||||||
|
bpm: usize,
|
||||||
|
playing: bool,
|
||||||
|
cur_y: usize,
|
||||||
|
cur_x: usize,
|
||||||
|
sel_h: usize,
|
||||||
|
sel_w: usize,
|
||||||
|
clipboard: Option<Clip>,
|
||||||
|
undo: Vec<Snapshot>,
|
||||||
|
outputs: EventOutputs,
|
||||||
|
events: Vec<Oevent>,
|
||||||
|
seed: usize,
|
||||||
|
path: String,
|
||||||
|
msg: String,
|
||||||
|
needs_preview: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_valid_glyph(c: char) -> bool {
|
||||||
|
c.is_ascii_alphanumeric() || matches!(c, '!' | '#' | '%' | '*' | '.' | ':' | ';' | '=' | '?')
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tick_period(bpm: usize) -> Duration {
|
||||||
|
// One Orca frame is a 16th note: 60000 ms / bpm / 4.
|
||||||
|
Duration::from_millis((60_000 / bpm.max(1) / 4).max(1) as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
fn dims(&self) -> (usize, usize) {
|
||||||
|
(self.field.height as usize, self.field.width as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clamp_cursor(&mut self) {
|
||||||
|
let (h, w) = self.dims();
|
||||||
|
self.cur_y = self.cur_y.min(h - 1);
|
||||||
|
self.cur_x = self.cur_x.min(w - 1);
|
||||||
|
self.sel_h = self.sel_h.clamp(1, h - self.cur_y);
|
||||||
|
self.sel_w = self.sel_w.clamp(1, w - self.cur_x);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_tick(&mut self) {
|
||||||
|
let (h, w) = self.dims();
|
||||||
|
self.mbuf.ensure_size(h, w);
|
||||||
|
self.mbuf.clear(h, w);
|
||||||
|
self.outputs.begin_tick();
|
||||||
|
orca_run(
|
||||||
|
&mut self.field.buffer,
|
||||||
|
&mut self.mbuf.buffer[..h * w],
|
||||||
|
h,
|
||||||
|
w,
|
||||||
|
self.tick,
|
||||||
|
&mut self.events,
|
||||||
|
self.seed,
|
||||||
|
);
|
||||||
|
self.outputs.dispatch(&mut self.events);
|
||||||
|
self.tick += 1;
|
||||||
|
self.needs_preview = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the sim on a scratch copy so port highlighting stays live while
|
||||||
|
// paused, without mutating the grid or emitting events.
|
||||||
|
fn preview(&mut self) {
|
||||||
|
let (h, w) = self.dims();
|
||||||
|
self.mbuf.ensure_size(h, w);
|
||||||
|
self.mbuf.clear(h, w);
|
||||||
|
self.scratch.clear();
|
||||||
|
self.scratch.extend_from_slice(&self.field.buffer);
|
||||||
|
let mut discarded: Vec<Oevent> = Vec::new();
|
||||||
|
orca_run(
|
||||||
|
&mut self.scratch,
|
||||||
|
&mut self.mbuf.buffer[..h * w],
|
||||||
|
h,
|
||||||
|
w,
|
||||||
|
self.tick,
|
||||||
|
&mut discarded,
|
||||||
|
self.seed,
|
||||||
|
);
|
||||||
|
self.needs_preview = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_undo(&mut self) {
|
||||||
|
if self.undo.len() >= MAX_UNDO {
|
||||||
|
self.undo.remove(0);
|
||||||
|
}
|
||||||
|
let (h, w) = self.dims();
|
||||||
|
self.undo.push(Snapshot {
|
||||||
|
data: self.field.buffer.clone(),
|
||||||
|
h,
|
||||||
|
w,
|
||||||
|
tick: self.tick,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn undo(&mut self) {
|
||||||
|
if let Some(s) = self.undo.pop() {
|
||||||
|
self.field.buffer = s.data;
|
||||||
|
self.field.height = s.h as u16;
|
||||||
|
self.field.width = s.w as u16;
|
||||||
|
self.tick = s.tick;
|
||||||
|
self.clamp_cursor();
|
||||||
|
self.needs_preview = true;
|
||||||
|
self.msg = "undid".into();
|
||||||
|
} else {
|
||||||
|
self.msg = "nothing to undo".into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fill_selection(&mut self, g: u8) {
|
||||||
|
self.push_undo();
|
||||||
|
let (h, w) = self.dims();
|
||||||
|
fill_subrect(
|
||||||
|
&mut self.field.buffer,
|
||||||
|
h,
|
||||||
|
w,
|
||||||
|
self.cur_y,
|
||||||
|
self.cur_x,
|
||||||
|
self.sel_h,
|
||||||
|
self.sel_w,
|
||||||
|
g,
|
||||||
|
);
|
||||||
|
self.needs_preview = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_selection(&mut self) {
|
||||||
|
let (h, w) = self.dims();
|
||||||
|
let mut data = vec![b'.'; self.sel_h * self.sel_w];
|
||||||
|
copy_subrect(
|
||||||
|
&self.field.buffer,
|
||||||
|
&mut data,
|
||||||
|
h,
|
||||||
|
w,
|
||||||
|
self.sel_h,
|
||||||
|
self.sel_w,
|
||||||
|
self.cur_y,
|
||||||
|
self.cur_x,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
self.sel_h,
|
||||||
|
self.sel_w,
|
||||||
|
);
|
||||||
|
self.clipboard = Some(Clip {
|
||||||
|
data,
|
||||||
|
h: self.sel_h,
|
||||||
|
w: self.sel_w,
|
||||||
|
});
|
||||||
|
self.msg = "copied".into();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paste(&mut self) {
|
||||||
|
let Some(clip) = self.clipboard.take() else {
|
||||||
|
self.msg = "clipboard empty".into();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.push_undo();
|
||||||
|
let (h, w) = self.dims();
|
||||||
|
copy_subrect(
|
||||||
|
&clip.data,
|
||||||
|
&mut self.field.buffer,
|
||||||
|
clip.h,
|
||||||
|
clip.w,
|
||||||
|
h,
|
||||||
|
w,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
self.cur_y,
|
||||||
|
self.cur_x,
|
||||||
|
clip.h,
|
||||||
|
clip.w,
|
||||||
|
);
|
||||||
|
self.msg = "pasted".into();
|
||||||
|
self.clipboard = Some(clip);
|
||||||
|
self.needs_preview = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resize_grid(&mut self, dy: isize, dx: isize) {
|
||||||
|
let (h, w) = self.dims();
|
||||||
|
let nh = (h as isize + dy).clamp(1, MAX_DIM as isize) as usize;
|
||||||
|
let nw = (w as isize + dx).clamp(1, MAX_DIM as isize) as usize;
|
||||||
|
if (nh, nw) == (h, w) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.push_undo();
|
||||||
|
self.field.resize(nh, nw);
|
||||||
|
self.mbuf.ensure_size(nh, nw);
|
||||||
|
self.clamp_cursor();
|
||||||
|
self.needs_preview = true;
|
||||||
|
self.msg = format!("grid {nw}x{nh}");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(&mut self) {
|
||||||
|
let result = File::create(&self.path).and_then(|mut f| self.field.write_to(&mut f));
|
||||||
|
self.msg = match result {
|
||||||
|
Ok(()) => format!("saved {}", self.path),
|
||||||
|
Err(e) => format!("save failed: {e}"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore the terminal even if we exit via error or panic.
|
||||||
|
struct TermGuard;
|
||||||
|
|
||||||
|
impl Drop for TermGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = disable_raw_mode();
|
||||||
|
let _ = execute!(io::stdout(), LeaveAlternateScreen, Show);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(cfg: TuiConfig) -> Result<(), String> {
|
||||||
|
let (path, field) = match cfg.path {
|
||||||
|
Some(p) => {
|
||||||
|
if std::path::Path::new(&p).exists() {
|
||||||
|
let f = Field::load_file(&p)
|
||||||
|
.map_err(|e| format!("cannot load {p}: {}", e.as_str()))?;
|
||||||
|
(p, f)
|
||||||
|
} else {
|
||||||
|
(p, default_field()?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => ("untitled.orca".to_string(), default_field()?),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut app = App {
|
||||||
|
mbuf: MarkBuffer::new(),
|
||||||
|
scratch: Vec::new(),
|
||||||
|
tick: 0,
|
||||||
|
bpm: cfg.bpm,
|
||||||
|
playing: false,
|
||||||
|
cur_y: 0,
|
||||||
|
cur_x: 0,
|
||||||
|
sel_h: 1,
|
||||||
|
sel_w: 1,
|
||||||
|
clipboard: None,
|
||||||
|
undo: Vec::new(),
|
||||||
|
outputs: cfg.outputs,
|
||||||
|
events: Vec::new(),
|
||||||
|
seed: cfg.seed,
|
||||||
|
path,
|
||||||
|
msg: String::new(),
|
||||||
|
needs_preview: true,
|
||||||
|
field,
|
||||||
|
};
|
||||||
|
|
||||||
|
enable_raw_mode().map_err(|e| e.to_string())?;
|
||||||
|
let _guard = TermGuard;
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
execute!(stdout, EnterAlternateScreen, Hide).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
main_loop(&mut app, &mut stdout).map_err(|e| e.to_string())?;
|
||||||
|
app.outputs.silence();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_field() -> Result<Field, String> {
|
||||||
|
let (cols, rows) = terminal::size().unwrap_or((80, 25));
|
||||||
|
let w = (cols as usize).clamp(20, 200);
|
||||||
|
let h = (rows.saturating_sub(3) as usize).clamp(10, 200);
|
||||||
|
Ok(Field::with_fill(h, w, b'.'))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main_loop(app: &mut App, stdout: &mut io::Stdout) -> io::Result<()> {
|
||||||
|
let mut next_tick = Instant::now();
|
||||||
|
loop {
|
||||||
|
if app.needs_preview && !app.playing {
|
||||||
|
app.preview();
|
||||||
|
}
|
||||||
|
draw(stdout, app)?;
|
||||||
|
|
||||||
|
let timeout = if app.playing {
|
||||||
|
let now = Instant::now();
|
||||||
|
if now >= next_tick {
|
||||||
|
app.do_tick();
|
||||||
|
next_tick += tick_period(app.bpm);
|
||||||
|
if next_tick < now {
|
||||||
|
next_tick = now + tick_period(app.bpm);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
next_tick - now
|
||||||
|
} else {
|
||||||
|
Duration::from_millis(200)
|
||||||
|
};
|
||||||
|
|
||||||
|
if event::poll(timeout)? {
|
||||||
|
match event::read()? {
|
||||||
|
Event::Key(key)
|
||||||
|
if matches!(key.kind, KeyEventKind::Press | KeyEventKind::Repeat)
|
||||||
|
&& !handle_key(app, key, &mut next_tick) =>
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns false when the app should quit.
|
||||||
|
fn handle_key(app: &mut App, key: KeyEvent, next_tick: &mut Instant) -> bool {
|
||||||
|
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||||
|
let shift = key.modifiers.contains(KeyModifiers::SHIFT);
|
||||||
|
let alt = key.modifiers.contains(KeyModifiers::ALT);
|
||||||
|
let (h, w) = app.dims();
|
||||||
|
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char('q') if ctrl => return false,
|
||||||
|
KeyCode::Char('s') if ctrl => app.save(),
|
||||||
|
KeyCode::Char('z') | KeyCode::Char('u') if ctrl => app.undo(),
|
||||||
|
KeyCode::Char('c') if ctrl => app.copy_selection(),
|
||||||
|
KeyCode::Char('x') if ctrl => {
|
||||||
|
app.copy_selection();
|
||||||
|
app.fill_selection(b'.');
|
||||||
|
app.msg = "cut".into();
|
||||||
|
}
|
||||||
|
KeyCode::Char('v') if ctrl => app.paste(),
|
||||||
|
KeyCode::Char('f') if ctrl
|
||||||
|
&& !app.playing => {
|
||||||
|
app.do_tick();
|
||||||
|
app.needs_preview = true;
|
||||||
|
}
|
||||||
|
KeyCode::Char('r') if ctrl => {
|
||||||
|
app.tick = 0;
|
||||||
|
app.needs_preview = true;
|
||||||
|
app.msg = "frame reset".into();
|
||||||
|
}
|
||||||
|
KeyCode::Char(' ') => {
|
||||||
|
app.playing = !app.playing;
|
||||||
|
if app.playing {
|
||||||
|
*next_tick = Instant::now();
|
||||||
|
} else {
|
||||||
|
app.outputs.silence();
|
||||||
|
app.needs_preview = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Char('<') => app.bpm = (app.bpm.saturating_sub(1)).max(10),
|
||||||
|
KeyCode::Char('>') => app.bpm = (app.bpm + 1).min(999),
|
||||||
|
KeyCode::Char('(') => app.bpm = (app.bpm.saturating_sub(10)).max(10),
|
||||||
|
KeyCode::Char(')') => app.bpm = (app.bpm + 10).min(999),
|
||||||
|
KeyCode::Esc => {
|
||||||
|
app.sel_h = 1;
|
||||||
|
app.sel_w = 1;
|
||||||
|
app.msg.clear();
|
||||||
|
}
|
||||||
|
KeyCode::Backspace | KeyCode::Delete => app.fill_selection(b'.'),
|
||||||
|
KeyCode::Up | KeyCode::Down | KeyCode::Left | KeyCode::Right => {
|
||||||
|
let (dy, dx): (isize, isize) = match key.code {
|
||||||
|
KeyCode::Up => (-1, 0),
|
||||||
|
KeyCode::Down => (1, 0),
|
||||||
|
KeyCode::Left => (0, -1),
|
||||||
|
_ => (0, 1),
|
||||||
|
};
|
||||||
|
if ctrl {
|
||||||
|
app.resize_grid(dy, dx);
|
||||||
|
} else if shift {
|
||||||
|
app.sel_h = (app.sel_h as isize + dy).clamp(1, (h - app.cur_y) as isize) as usize;
|
||||||
|
app.sel_w = (app.sel_w as isize + dx).clamp(1, (w - app.cur_x) as isize) as usize;
|
||||||
|
} else {
|
||||||
|
let step: isize = if alt { 8 } else { 1 };
|
||||||
|
app.cur_y = (app.cur_y as isize + dy * step).clamp(0, h as isize - 1) as usize;
|
||||||
|
app.cur_x = (app.cur_x as isize + dx * step).clamp(0, w as isize - 1) as usize;
|
||||||
|
app.clamp_cursor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Char(c) if !ctrl && !alt && is_valid_glyph(c) => {
|
||||||
|
app.fill_selection(c as u8);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(out: &mut io::Stdout, app: &App) -> io::Result<()> {
|
||||||
|
let (tw, th) = terminal::size()?;
|
||||||
|
let (gh, gw) = app.dims();
|
||||||
|
let grid_rows = th.saturating_sub(2) as usize;
|
||||||
|
let vis_h = gh.min(grid_rows);
|
||||||
|
let vis_w = gw.min(tw as usize);
|
||||||
|
|
||||||
|
queue!(out, BeginSynchronizedUpdate, Hide, Clear(ClearType::All))?;
|
||||||
|
|
||||||
|
for y in 0..vis_h {
|
||||||
|
queue!(out, MoveTo(0, y as u16))?;
|
||||||
|
for x in 0..vis_w {
|
||||||
|
let g = app.field.buffer[y * gw + x];
|
||||||
|
let m = app.mbuf.buffer.get(y * gw + x).copied().unwrap_or(0);
|
||||||
|
let is_cursor = y == app.cur_y && x == app.cur_x;
|
||||||
|
let in_sel = y >= app.cur_y
|
||||||
|
&& y < app.cur_y + app.sel_h
|
||||||
|
&& x >= app.cur_x
|
||||||
|
&& x < app.cur_x + app.sel_w;
|
||||||
|
|
||||||
|
let ch = if g == b'.' {
|
||||||
|
if is_cursor {
|
||||||
|
'@'
|
||||||
|
} else if y % 8 == 0 && x % 8 == 0 {
|
||||||
|
'+'
|
||||||
|
} else {
|
||||||
|
'.'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
g as char
|
||||||
|
};
|
||||||
|
|
||||||
|
queue!(out, SetAttribute(Attribute::Reset))?;
|
||||||
|
if is_cursor {
|
||||||
|
queue!(out, SetAttribute(Attribute::Reverse), SetAttribute(Attribute::Bold))?;
|
||||||
|
} else if in_sel {
|
||||||
|
queue!(out, SetAttribute(Attribute::Reverse))?;
|
||||||
|
} else if m & MARK_OUTPUT != 0 {
|
||||||
|
queue!(out, SetAttribute(Attribute::Reverse))?;
|
||||||
|
} else if m & (MARK_INPUT | MARK_HASTE) != 0 {
|
||||||
|
queue!(out, SetAttribute(Attribute::Bold))?;
|
||||||
|
} else if g == b'.' || m & (MARK_LOCK | MARK_SLEEP) != 0 {
|
||||||
|
queue!(out, SetAttribute(Attribute::Dim))?;
|
||||||
|
}
|
||||||
|
queue!(out, Print(ch))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = if app.playing { "playing" } else { "paused" };
|
||||||
|
let status = format!(
|
||||||
|
"{} {}f {}bpm {} {},{} {}x{} {}",
|
||||||
|
app.path, app.tick, app.bpm, state, app.cur_x, app.cur_y, gw, gh, app.msg
|
||||||
|
);
|
||||||
|
let mut line1: String = status.chars().take(tw as usize).collect();
|
||||||
|
line1 = format!("{:<width$}", line1, width = tw as usize);
|
||||||
|
let help: String = HELP_LINE.chars().take(tw as usize).collect();
|
||||||
|
|
||||||
|
queue!(
|
||||||
|
out,
|
||||||
|
SetAttribute(Attribute::Reset),
|
||||||
|
MoveTo(0, th.saturating_sub(2)),
|
||||||
|
SetAttribute(Attribute::Reverse),
|
||||||
|
Print(line1),
|
||||||
|
SetAttribute(Attribute::Reset),
|
||||||
|
MoveTo(0, th.saturating_sub(1)),
|
||||||
|
SetAttribute(Attribute::Dim),
|
||||||
|
Print(help),
|
||||||
|
SetAttribute(Attribute::Reset),
|
||||||
|
EndSynchronizedUpdate
|
||||||
|
)?;
|
||||||
|
out.flush()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user