Compare commits

..

12 Commits

Author SHA1 Message Date
bryanthaboi cf45cbbf92 Merge pull request #1498 from bryanthaboi/dev
more stuff
2026-08-17 20:41:58 -04:00
github-actions 7d1ddf9b7c chore(ios): update app-repo.json [skip ci] 2026-08-17 20:09:51 -04:00
bryanthaboi faf82c2cec Merge pull request #1491 from AverageConsumer/codex/gen2-move-grid-hook
fix(gen2): honor mod move-grid navigation
2026-08-17 19:58:35 -04:00
AverageConsumer e0e030003b fix(gen2): honor mod move-grid navigation 2026-08-17 20:38:48 +02:00
github-actions ce2afb83f1 chore(ios): update app-repo.json [skip ci] 2026-08-17 14:13:48 -04:00
bryanthaboi 28f741f72f Merge pull request #1489 from bryanthaboi/dev
Update LauncherView.lua
2026-08-17 14:04:35 -04:00
github-actions ea28f886f3 chore(ios): update app-repo.json [skip ci] 2026-08-17 13:20:07 -04:00
bryanthaboi 6cd8f0ddea Merge pull request #1487 from bryanthaboi/dev
[release 0.2.0]
2026-08-17 13:10:08 -04:00
github-actions fb4eaeda10 chore(ios): update app-repo.json [skip ci] 2026-08-16 22:37:21 -04:00
bryanthaboi 69100301a1 Merge pull request #1460 from bryanthaboi/dev 2026-08-16 22:28:20 -04:00
github-actions 114352b75f chore(ios): update app-repo.json [skip ci] 2026-08-16 10:23:08 -04:00
bryanthaboi 0e40a7a1f4 Merge pull request #1408 from bryanthaboi/dev 2026-08-16 10:14:26 -04:00
1461 changed files with 149072 additions and 283623 deletions
-1
View File
@@ -1 +0,0 @@
* @bryanthaboi
+9 -15
View File
@@ -5,10 +5,6 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
**Turn off all mods before filing.** Disable everything in the launcher's MODS
tab, confirm the bug still happens, then open this. Bugs that only show up with
mods on belong with the mod author, not here.
A screenshot is worth more than any description. If you can grab one, grab one. A screenshot is worth more than any description. If you can grab one, grab one.
If you genuinely can't, that's fine, but then the details below need to be thorough If you genuinely can't, that's fine, but then the details below need to be thorough
enough that someone can find the bug without ever seeing your screen. enough that someone can find the bug without ever seeing your screen.
@@ -55,24 +51,22 @@ body:
validations: validations:
required: true required: true
- type: checkboxes - type: dropdown
id: mods_off id: mods_enabled
attributes: attributes:
label: Mods off label: Were any mods on
description: > description: Check the MODS tab in the launcher if you're not sure.
Turn off every mod in the launcher's MODS tab and reproduce the bug
before submitting. Do not file vanilla bugs with mods still enabled.
options: options:
- label: I turned off all mods and can still reproduce this - "No"
- "Yes"
validations:
required: true required: true
- type: input - type: input
id: mods_which id: mods_which
attributes: attributes:
label: Which mods (if you first noticed this with any on) label: Which mods (if any were on)
description: > description: List the enabled mods. Leave blank if none were on.
Optional. If you originally hit this with mods enabled, list them —
but only after you've confirmed it still happens with all of them off.
placeholder: nuzlocke 1.0.0, running-shoes 0.3 placeholder: nuzlocke 1.0.0, running-shoes 0.3
validations: validations:
required: false required: false
+118
View File
@@ -0,0 +1,118 @@
name: Feature request
description: Ask for something new in the engine, launcher, or platform — not a content/gameplay mod.
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Use this for **engine / launcher / platform** work (ports, video options, save
tooling, networking, mod API seams, docs).
If what you want is a gameplay, cosmetic, audio, or QoL change that a Lua mod
could ship — running shoes, alternate sprites, day/night, shiny indicators,
Gen 2-like battle toggles, soundtrack packs — open a
**[Mod request](https://github.com/bryanthaboi/gen1recomp/issues/new?template=mod_request.yml)**
instead.
"Can we add X" on its own is hard to act on. Say what you want, why you want it,
and how you picture it working.
- type: input
id: summary
attributes:
label: One line summary
description: What you want, in a sentence.
placeholder: Add Linux AppImage releases next to the macOS and Windows builds
validations:
required: true
- type: dropdown
id: game
attributes:
label: Which game is this about
description: Pick every version it applies to. Use N/A if it isn't game-specific.
multiple: true
options:
- Red
- Blue
- Yellow
- Gold
- N/A
validations:
required: true
- type: input
id: discord
attributes:
label: Discord username (optional)
description: >
So maintainers can ping you on Discord if they need a quick follow-up.
Leave blank if you'd rather keep everything on GitHub.
placeholder: yourname
validations:
required: false
- type: textarea
id: what
attributes:
label: What do you want
description: >
Describe it properly. What is it, where does it live (launcher, options,
engine), what does the player see or do. If it changes something that already
exists, say what it does today and what it should do instead.
placeholder: |
Ship a Linux AppImage on each release, same version as the macOS/Windows builds,
with the same save folder layout and mod discovery path.
validations:
required: true
- type: textarea
id: why
attributes:
label: Why is this worth doing
description: >
What's annoying or missing right now. What does this fix. If it's just because you
think it would be fun, say that, it's a real answer.
placeholder: |
LÖVE already runs on Linux; without a packaged build, players have to assemble
it themselves and miss release notes / update checks.
validations:
required: true
- type: textarea
id: how
attributes:
label: How should it work
description: >
The specifics. Which menu, what happens in the edge cases. If you don't
know, say what you'd expect as a player and leave the rest open.
placeholder: |
- GitHub Releases asset next to the .dmg / .exe
- Same options.lua / mods/ layout as desktop
- Documented in the README install section
validations:
required: true
- type: dropdown
id: scope
attributes:
label: Does this change how the original game plays
description: >
Some requests are quality of life, some change the actual game. Both are fine,
it just helps to know which one you're asking for.
options:
- Quality of life, original game is untouched
- Changes how the game plays
- Not sure
validations:
required: true
- type: textarea
id: extra
attributes:
label: Anything else
description: >
Reference screenshots, how another game does it, related issues. Leave blank
if nothing comes to mind.
validations:
required: false
+130
View File
@@ -0,0 +1,130 @@
name: Mod request
description: Ask for a gameplay, cosmetic, audio, or QoL change that belongs as a Lua mod.
labels: ["mod request"]
body:
- type: markdown
attributes:
value: |
This tracker is for ideas that should ship as **mods**, not as core engine
features — alternate sprites, running shoes, day/night, shiny indicators,
soundtrack packs, Gen 2-like battle toggles, map cosmetics, bag QoL, etc.
The engine already exposes a lot of this through registries and hooks
([modding wiki](https://github.com/bryanthaboi/gen1recomp/wiki)).
Use a **Feature request** instead for launcher / ports / video options /
networking / save tooling / new API seams.
- type: input
id: summary
attributes:
label: One line summary
description: What the mod should do, in a sentence.
placeholder: Hold B to run at bike speed on the overworld
validations:
required: true
- type: dropdown
id: game
attributes:
label: Which game is this for
description: Pick every version the mod should cover. Use N/A if it isn't game-specific.
multiple: true
options:
- Red
- Blue
- Yellow
- Gold
- N/A
validations:
required: true
- type: input
id: discord
attributes:
label: Discord username (optional)
description: >
So maintainers or mod authors can ping you on Discord if they pick this up.
Leave blank if you'd rather keep everything on GitHub.
placeholder: yourname
validations:
required: false
- type: textarea
id: what
attributes:
label: What should the mod do
description: >
Describe the player-facing behavior. What changes, where, what does the
player see or press. If it toggles from Options or a START-menu entry, say so.
placeholder: |
Hold B while walking outdoors to move at bike speed. Release to walk again.
Same places the bike is allowed; no effect in battles or menus.
validations:
required: true
- type: textarea
id: why
attributes:
label: Why is this worth doing as a mod
description: >
Why optional/modded rather than a core option. Who wants it on, who wants
vanilla left alone.
placeholder: |
Great for replaying and backtracking, but some people want a strict Gen 1
pace. A mod (or an opt-in mod option) keeps both camps happy.
validations:
required: true
- type: textarea
id: how
attributes:
label: How should it work
description: >
Buttons, menus, edge cases, whether it needs new art/audio. If you know a
hook or registry that fits (movement.speed, pokemon.sprite, rulesets, …),
mention it — otherwise leave it open.
placeholder: |
- Hold B on the overworld
- Same step timing as the bike
- Disabled where the bike is disabled
- Prefer hooks:wrap("movement.speed") if that still fits
validations:
required: true
- type: dropdown
id: vanilla
attributes:
label: With the mod off, is vanilla unchanged
options:
- Yes — parity when disabled
- No — it would replace something always-on
- Not sure
validations:
required: true
- type: dropdown
id: category
attributes:
label: Best-fit mod category
description: Same taxonomy as example mods (BALANCE, GRAPHICS, AUDIO, …).
options:
- GAMEPLAY / QoL
- GRAPHICS
- AUDIO
- BALANCE / ruleset
- CONTENT (maps, encounters, trainers)
- UI / TOOL
- TOTAL_CONVERSION-ish
- Not sure
validations:
required: true
- type: textarea
id: extra
attributes:
label: Anything else
description: >
Reference screenshots, other games/hacks that do it, related issues, or
"I'd like to try writing this myself." Leave blank if nothing comes to mind.
validations:
required: false
+1 -62
View File
@@ -26,64 +26,6 @@ permissions:
contents: read contents: read
jobs: jobs:
macos-changes:
name: detect macOS changes
runs-on: ubuntu-latest
outputs:
changed: ${{ steps.paths.outputs.changed }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- id: paths
env:
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
HEAD_SHA: ${{ github.sha }}
run: |
if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then
echo "changed=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(conf\.lua$|scripts/(build\.sh|build_love_macos\.sh|run\.sh|setup\.sh)$|Play-Mac\.command$|mobile/macos/|src/core/SaveData\.lua$|\.github/workflows/(ci|release)\.yml$)'; then
echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
fi
macos-build:
name: macOS LÖVE 12 build
needs: macos-changes
if: needs.macos-changes.outputs.changed == 'true'
runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }}
steps:
- uses: actions/checkout@v7
- name: Build LÖVE 12 macOS runtime
run: scripts/build_love_macos.sh --fetch
- name: Build and verify macOS app
env:
LOVE_APP: ${{ github.workspace }}/.bazinga/love12/love.app
MAC_STAGE_DIR: ${{ runner.temp }}/gen1recomp-mac-stage
run: |
set -euo pipefail
scripts/build.sh mac --no-notarize --identity - --version 0.0.0
unzip -tqq 'dist/mac/gen1recomp++-macos.zip'
app="$MAC_STAGE_DIR/gen1recomp++.app"
[ -d "$app" ]
[ -x "$app/Contents/MacOS/gen1recomp++" ]
[ "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$app/Contents/Info.plist")" = 'gen1recomp++' ]
[ "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleDisplayName' "$app/Contents/Info.plist")" = 'gen1recomp++' ]
[ "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$app/Contents/Info.plist")" = 'com.theboisclub.gen1recompplusplus' ]
[ "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$app/Contents/Info.plist")" = 0.0.0 ]
codesign --verify --deep --strict "$app"
otool -L "$app/Contents/Frameworks/love.framework/love" | grep -q '/Metal.framework/'
- name: Upload macOS build
uses: actions/upload-artifact@v7
with:
name: gen1recomp++-macos
path: dist/mac/gen1recomp++-macos.zip
if-no-files-found: error
retention-days: 7
ios-changes: ios-changes:
name: detect iOS changes name: detect iOS changes
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -178,7 +120,7 @@ jobs:
echo "changed=true" >> "$GITHUB_OUTPUT" echo "changed=true" >> "$GITHUB_OUTPUT"
exit 0 exit 0
fi fi
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(scripts/build_switch\.sh$|scripts/switch/|docs/switch-.*\.md$|tests/switch_ci_workflows_test\.lua$|tests/switch_transfer_docs_test\.lua$|\.github/workflows/(ci|release|platform-artifact-comment)\.yml$|src/core/(NxAssetOverlay|Platform|GameVersion)\.lua$|src/import/CacheFs\.lua$|tests/engine/(assets_version_fallback|nx_generated_guard|nx_yellow_boot|switch_diagnostics|cache_fs_gold_nx_load)_test\.lua$|tests/engine/platform_nx)'; then if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(scripts/build_switch\.sh$|scripts/switch/|docs/switch-.*\.md$|tests/switch_ci_workflows_test\.lua$|tests/switch_transfer_docs_test\.lua$|\.github/workflows/(ci|release|switch-artifact-comment)\.yml$|src/core/(NxAssetOverlay|Platform|GameVersion)\.lua$|src/import/CacheFs\.lua$|tests/engine/(assets_version_fallback|nx_generated_guard|nx_yellow_boot|switch_diagnostics)_test\.lua$|tests/engine/platform_nx)'; then
echo "changed=true" >> "$GITHUB_OUTPUT" echo "changed=true" >> "$GITHUB_OUTPUT"
else else
echo "changed=false" >> "$GITHUB_OUTPUT" echo "changed=false" >> "$GITHUB_OUTPUT"
@@ -209,9 +151,6 @@ jobs:
luajit tests/engine/assets_version_fallback_test.lua luajit tests/engine/assets_version_fallback_test.lua
luajit tests/engine/nx_generated_guard_test.lua luajit tests/engine/nx_generated_guard_test.lua
luajit tests/engine/nx_yellow_boot_test.lua luajit tests/engine/nx_yellow_boot_test.lua
luajit tests/engine/cache_fs_gold_nx_load_test.lua
luajit tests/engine/cache_fs_blue_mount_test.lua
luajit tests/engine/switch_diagnostics_test.lua
switch-build: switch-build:
name: Switch fused build name: Switch fused build
@@ -0,0 +1,55 @@
name: iOS artifact comment
on:
workflow_run:
workflows: [ci]
types: [completed]
permissions:
contents: read
issues: write
pull-requests: write
jobs:
comment:
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- id: artifact
env:
GH_TOKEN: ${{ github.token }}
RUN_ID: ${{ github.event.workflow_run.id }}
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
HEAD_REPOSITORY: ${{ github.event.workflow_run.head_repository.full_name }}
run: |
artifact_id="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/artifacts" --jq '.artifacts[] | select(.name == "gen1recomp++-ios-ipa") | .id')"
[ -n "$artifact_id" ] || exit 0
head_owner="${HEAD_REPOSITORY%%/*}"
pr_number="$(gh api "repos/$GITHUB_REPOSITORY/pulls?state=open&head=$head_owner:$HEAD_BRANCH" --jq '.[0].number // empty')"
[ -n "$pr_number" ] || exit 0
echo "artifact_url=https://github.com/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/artifacts/$artifact_id" >> "$GITHUB_OUTPUT"
echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT"
# Upsert via comment-tag only — do not delete-all bot comments (clobbers Switch).
- name: Get build info
id: build-info
env:
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
commit_hash="$(printf '%s' "$HEAD_SHA" | cut -c1-7)"
build_time="$(date "+%Y-%m-%d %H:%M:%S")"
echo "hash=$commit_hash" >> "$GITHUB_OUTPUT"
echo "time=$build_time" >> "$GITHUB_OUTPUT"
- name: comment iOS artifact
if: steps.artifact.outputs.pr_number != ''
uses: thollander/actions-comment-pull-request@v3
with:
message: |
[gen1recomp++.ipa](${{ steps.artifact.outputs.artifact_url }})
**Commit**: [#${{ steps.build-info.outputs.hash }}](https://github.com/${{ github.event.workflow_run.head_repository.full_name }}/commit/${{ github.event.workflow_run.head_sha }})
**Build Time**: `${{ steps.build-info.outputs.time }}`
<sub>This comment was automatically generated. [View workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }})</sub>
pr-number: ${{ steps.artifact.outputs.pr_number }}
comment-tag: ios-build-result
github-token: ${{ github.token }}
@@ -1,68 +0,0 @@
name: platform artifact comment
on:
workflow_run:
workflows: [ci]
types: [completed]
permissions:
contents: read
issues: write
pull-requests: write
jobs:
comment:
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- id: artifacts
env:
GH_TOKEN: ${{ github.token }}
RUN_ID: ${{ github.event.workflow_run.id }}
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
HEAD_REPOSITORY: ${{ github.event.workflow_run.head_repository.full_name }}
run: |
set -euo pipefail
artifacts="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/artifacts")"
head_owner="${HEAD_REPOSITORY%%/*}"
pr_number="$(gh api "repos/$GITHUB_REPOSITORY/pulls?state=open&head=$head_owner:$HEAD_BRANCH" --jq '.[0].number // empty')"
[ -n "$pr_number" ] || exit 0
message="## CI build artifacts"
found=0
while IFS='|' read -r platform artifact_name artifact_file; do
artifact_id="$(printf '%s' "$artifacts" | jq -r --arg name "$artifact_name" '[.artifacts[] | select(.name == $name) | .id][0] // empty')"
if [ -n "$artifact_id" ] && [ "$artifact_id" != null ]; then
artifact_url="https://github.com/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/artifacts/$artifact_id"
message+=$'\n'"- **$platform**: [$artifact_file]($artifact_url)"
found=1
fi
done <<'ARTIFACTS'
macOS|gen1recomp++-macos|gen1recomp++-macos.zip
iOS|gen1recomp++-ios-ipa|gen1recomp++.ipa
Nintendo Switch|gen1recomp-switch-nro|gen1recomp-switch.nro
Xbox UWP|gen1recomp-xbox-uwp|gen1recomp-xbox-uwp.zip
Linux arm64|gen1recomp-linux-arm64|gen1recomp-linux-arm64.AppImage
ARTIFACTS
[ "$found" -eq 1 ] || exit 0
commit_hash="$(printf '%s' "${{ github.event.workflow_run.head_sha }}" | cut -c1-7)"
build_time="$(date -u "+%Y-%m-%d %H:%M:%S UTC")"
message+=$'\n\n'"**Commit**: [#$commit_hash](https://github.com/$HEAD_REPOSITORY/commit/${{ github.event.workflow_run.head_sha }})"
message+=$'\n'"**Build Time**: \`$build_time\`"
message+=$'\n\n'"<sub>This comment was automatically generated. [View workflow run](https://github.com/$GITHUB_REPOSITORY/actions/runs/$RUN_ID)</sub>"
echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT"
{
echo 'message<<BUILD_MESSAGE'
printf '%s\n' "$message"
echo 'BUILD_MESSAGE'
} >> "$GITHUB_OUTPUT"
- name: comment platform artifacts
if: steps.artifacts.outputs.pr_number != ''
uses: thollander/actions-comment-pull-request@v3
with:
message: ${{ steps.artifacts.outputs.message }}
pr-number: ${{ steps.artifacts.outputs.pr_number }}
comment-tag: platform-build-result
github-token: ${{ github.token }}
+3 -10
View File
@@ -344,16 +344,9 @@ jobs:
echo "Identities available to codesign:" echo "Identities available to codesign:"
security find-identity -v -p codesigning "$KEYCHAIN_PATH" security find-identity -v -p codesigning "$KEYCHAIN_PATH"
- name: Build LÖVE 12 macOS runtime
run: |
set -euo pipefail
scripts/build_love_macos.sh --fetch
- name: Build macOS + Windows + Linux - name: Build macOS + Windows + Linux
env: env:
GEN1TLS_DLL: ${{ github.workspace }}/dist/native/win-x64/gen1tls.dll GEN1TLS_DLL: ${{ github.workspace }}/dist/native/win-x64/gen1tls.dll
LOVE_APP: ${{ github.workspace }}/.bazinga/love12/love.app
MAC_STAGE_DIR: ${{ runner.temp }}/gen1recomp-mac-stage
run: | run: |
set -euo pipefail set -euo pipefail
# Sign in-build (identity auto-detected from the temp keychain); # Sign in-build (identity auto-detected from the temp keychain);
@@ -435,8 +428,8 @@ jobs:
set -a; . "$ci_dir/notary.env"; set +a set -a; . "$ci_dir/notary.env"; set +a
echo "::add-mask::$APPLE_APP_PASSWORD" echo "::add-mask::$APPLE_APP_PASSWORD"
app="$RUNNER_TEMP/gen1recomp-mac-stage/gen1recomp++.app" app=".bazinga/work/gen1recomp.app"
zip="dist/mac/gen1recomp++-macos.zip" zip="dist/mac/gen1recomp-macos.zip"
[ -d "$app" ] || { echo "::error::signed app not found at $app"; exit 1; } [ -d "$app" ] || { echo "::error::signed app not found at $app"; exit 1; }
if [ -z "${APPLE_ID:-}" ] || [ -z "${APPLE_APP_PASSWORD:-}" ] || [ -z "${APPLE_TEAM_ID:-}" ]; then if [ -z "${APPLE_ID:-}" ] || [ -z "${APPLE_APP_PASSWORD:-}" ] || [ -z "${APPLE_TEAM_ID:-}" ]; then
echo "::error::notary.env is missing APPLE_ID / APPLE_APP_PASSWORD / APPLE_TEAM_ID." echo "::error::notary.env is missing APPLE_ID / APPLE_APP_PASSWORD / APPLE_TEAM_ID."
@@ -481,7 +474,7 @@ jobs:
outdir="dist/release" outdir="dist/release"
rm -rf "$outdir" rm -rf "$outdir"
mkdir -p "$outdir" mkdir -p "$outdir"
cp "dist/mac/gen1recomp++-macos.zip" "$outdir/gen1recomp++-${v}-macos.zip" cp "dist/mac/gen1recomp-macos.zip" "$outdir/gen1recomp-${v}-macos.zip"
cp "dist/win/gen1recomp-win64.zip" "$outdir/gen1recomp-${v}-windows.zip" cp "dist/win/gen1recomp-win64.zip" "$outdir/gen1recomp-${v}-windows.zip"
cp "dist/linux/gen1recomp-linux.zip" "$outdir/gen1recomp-${v}-linux.zip" cp "dist/linux/gen1recomp-linux.zip" "$outdir/gen1recomp-${v}-linux.zip"
@@ -0,0 +1,55 @@
name: Switch artifact comment
on:
workflow_run:
workflows: [ci]
types: [completed]
permissions:
contents: read
issues: write
pull-requests: write
jobs:
comment:
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- id: artifact
env:
GH_TOKEN: ${{ github.token }}
RUN_ID: ${{ github.event.workflow_run.id }}
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
HEAD_REPOSITORY: ${{ github.event.workflow_run.head_repository.full_name }}
run: |
artifact_id="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/artifacts" --jq '.artifacts[] | select(.name == "gen1recomp-switch-nro") | .id')"
[ -n "$artifact_id" ] || exit 0
head_owner="${HEAD_REPOSITORY%%/*}"
pr_number="$(gh api "repos/$GITHUB_REPOSITORY/pulls?state=open&head=$head_owner:$HEAD_BRANCH" --jq '.[0].number // empty')"
[ -n "$pr_number" ] || exit 0
echo "artifact_url=https://github.com/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/artifacts/$artifact_id" >> "$GITHUB_OUTPUT"
echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT"
# Upsert via comment-tag only — do not delete-all bot comments (clobbers iOS).
- name: Get build info
id: build-info
env:
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
commit_hash="$(printf '%s' "$HEAD_SHA" | cut -c1-7)"
build_time="$(date "+%Y-%m-%d %H:%M:%S")"
echo "hash=$commit_hash" >> "$GITHUB_OUTPUT"
echo "time=$build_time" >> "$GITHUB_OUTPUT"
- name: comment Switch artifact
if: steps.artifact.outputs.pr_number != ''
uses: thollander/actions-comment-pull-request@v3
with:
message: |
[gen1recomp-switch.nro](${{ steps.artifact.outputs.artifact_url }})
**Commit**: [#${{ steps.build-info.outputs.hash }}](https://github.com/${{ github.event.workflow_run.head_repository.full_name }}/commit/${{ github.event.workflow_run.head_sha }})
**Build Time**: `${{ steps.build-info.outputs.time }}`
<sub>This comment was automatically generated. [View workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }})</sub>
pr-number: ${{ steps.artifact.outputs.pr_number }}
comment-tag: switch-build-result
github-token: ${{ github.token }}
+17 -12
View File
@@ -27,15 +27,9 @@ ask() { # ask "question" -> yes by default
printf '\n \033[1mPokémon Red - LÖVE2D port\033[0m\n\n' printf '\n \033[1mPokémon Red - LÖVE2D port\033[0m\n\n'
have_love() { have_love() {
local app version command -v love >/dev/null 2>&1 && return 0
for app in ".bazinga/love12/love.app" "/Applications/love12.app" "$HOME/Applications/love12.app" \ [ -x "/Applications/love.app/Contents/MacOS/love" ] && return 0
"/Applications/love.app" "$HOME/Applications/love.app"; do [ -x "$HOME/Applications/love.app/Contents/MacOS/love" ] && return 0
[ -x "$app/Contents/MacOS/love" ] || continue
version="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$app/Contents/Info.plist" 2>/dev/null || true)"
printf '%s' "$version" | grep -Eq '^12(\.|$)' || continue
otool -L "$app/Contents/Frameworks/love.framework/love" 2>/dev/null \
| grep -q '/Metal.framework/' && return 0
done
return 1 return 1
} }
@@ -63,11 +57,22 @@ if ! command -v python3 >/dev/null 2>&1; then
fi fi
fi fi
if ! have_love && ! command -v xcodebuild >/dev/null 2>&1; then # ----------------------------------------------------------------- Homebrew
warn "LÖVE 12 is not installed and Xcode is required to build it for macOS." if ! have_love && ! command -v brew >/dev/null 2>&1 \
warn "Install Xcode from the App Store, launch it once, then run this again." && [ ! -x /opt/homebrew/bin/brew ] && [ ! -x /usr/local/bin/brew ]; then
warn "LÖVE (the game engine) is not installed; the easiest installer is Homebrew"
if ask "Install Homebrew now? (asks for your macOS password)"; then
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" \
|| { err "Homebrew install failed"; pause_exit 1; }
else
warn "OK, download LÖVE 11.x yourself from https://love2d.org,"
warn "drop love.app into /Applications, then run this again."
pause_exit 1 pause_exit 1
fi
fi fi
# make brew visible in THIS shell (fresh installs aren't on PATH yet)
[ -x /opt/homebrew/bin/brew ] && eval "$(/opt/homebrew/bin/brew shellenv)"
[ -x /usr/local/bin/brew ] && eval "$(/usr/local/bin/brew shellenv)"
# ------------------------------------------------------------------- build # ------------------------------------------------------------------- build
echo echo
+12 -8
View File
@@ -9,7 +9,7 @@ And before you say, "that's not a recomp", you're wrong. Recomp is an acronym. *
[Click Here for the AI Use Disclosure!](AIDisclosure.md) [Click Here for the AI Use Disclosure!](AIDisclosure.md)
> [!CAUTION] > [!CAUTION]
> **We are NOT affiliated with the website `gen1recomp[.]com`** That website is not run by this project, was not authorized by us, and we have no idea who operates it. It is impersonating this project; do not download anything from it, and treat anything it hosts or claims as untrustworthy. Even if the site currently links back to this repository, the people behind it can change its content at any time, so nothing on it should ever be trusted. This GitHub repository and the Discord linked below are the only official sources for this project. Also, as I assumed would eventually happen, the idiot that made that website now pumped it full of adware. Please stay away from that website. > **We are NOT affiliated with the website `gen1recomp[.]com`** That website is not run by this project, was not authorized by us, and we have no idea who operates it. It is impersonating this project; do not download anything from it, and treat anything it hosts or claims as untrustworthy. Even if the site currently links back to this repository, the people behind it can change its content at any time, so nothing on it should ever be trusted. This GitHub repository and the Discord linked below are the only official sources for this project.
<p align="center"><img src="https://raw.githubusercontent.com/bryanthaboi/gen1recomp/refs/heads/dev/assets/logo/logo.png"></p> <p align="center"><img src="https://raw.githubusercontent.com/bryanthaboi/gen1recomp/refs/heads/dev/assets/logo/logo.png"></p>
@@ -53,7 +53,7 @@ And before you say, "that's not a recomp", you're wrong. Recomp is an acronym. *
### Watch the latest update video ### Watch the latest update video
[![Watch the latest update video](https://img.youtube.com/vi/yi7LkWQPKKM/maxresdefault.jpg)](https://youtu.be/yi7LkWQPKKM) [![Watch the latest update video](https://img.youtube.com/vi/8IOgqbe4YvA/maxresdefault.jpg)](https://www.youtube.com/watch?v=8IOgqbe4YvA)
This project does not include a ROM, emulate the Game Boy, transpile assembly, This project does not include a ROM, emulate the Game Boy, transpile assembly,
@@ -194,9 +194,8 @@ By default the game keeps your save, options, and the private ROM-derived
data cache in your OS's normal per-user app data folder. To keep everything data cache in your OS's normal per-user app data folder. To keep everything
next to the game instead (handy for a USB stick or portable drive you carry next to the game instead (handy for a USB stick or portable drive you carry
between computers), drop an empty file named `portable.txt` next to the app between computers), drop an empty file named `portable.txt` next to the app
(next to `gen1recomp++.app` on macOS or `gen1recomp.exe` on Windows, or next (next to `gen1recomp.app`/`.exe`, or next to `main.lua`/`conf.lua` when
to `main.lua`/`conf.lua` when running from source), then launch the game. running from source), then launch the game. Portable mode is desktop-only
Portable mode is desktop-only
(Windows, Linux, macOS); it has no effect on Android or iOS, where the app (Windows, Linux, macOS); it has no effect on Android or iOS, where the app
runs from a read-only package. runs from a read-only package.
@@ -333,7 +332,7 @@ Maps can be edited in our own build of [Tiled](https://www.mapeditor.org),
and exported back out as a mod; see and exported back out as a mod; see
[docs/tiled-map-editing.md](docs/tiled-map-editing.md). [docs/tiled-map-editing.md](docs/tiled-map-editing.md).
## Bugs ## Bugs and Ideas
Found a bug? A warp dropping you somewhere it shouldn't, a battle doing math Found a bug? A warp dropping you somewhere it shouldn't, a battle doing math
that looks wrong, text in the wrong box, anything that does not match the that looks wrong, text in the wrong box, anything that does not match the
@@ -342,6 +341,12 @@ original game.
Attach a screenshot if you can. It saves a lot of back and forth, and if you Attach a screenshot if you can. It saves a lot of back and forth, and if you
can't get one, the form asks you to describe what you saw instead. can't get one, the form asks you to describe what you saw instead.
Thought of a feature that could be good, or a way to improve one that already
exists?
[Open a feature request](https://github.com/bryanthaboi/gen1recomp/issues/new?template=feature_request.yml).
Say what you want, why it is worth doing, and how you picture it working. A
request with real detail is one that can actually get built.
## More ## More
- [Link play](https://github.com/bryanthaboi/gen1recomp/wiki/Guide-Link-Play) - [Link play](https://github.com/bryanthaboi/gen1recomp/wiki/Guide-Link-Play)
@@ -349,8 +354,7 @@ can't get one, the form asks you to describe what you saw instead.
- [Save editor](https://github.com/bryanthaboi/gen1recomp/wiki/Guide-Save-Editor) - [Save editor](https://github.com/bryanthaboi/gen1recomp/wiki/Guide-Save-Editor)
— edit party, boxes, items, events, and Pokédex flags outside the game. — edit party, boxes, items, events, and Pokédex flags outside the game.
- `docs/architecture.md` — runtime details; - `docs/architecture.md` — runtime details;
`docs/behavior-porting-notes.md` — formula provenance; `docs/behavior-porting-notes.md` — formula provenance.
`docs/link-security.md` — what link play defends against, and what it doesn't.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

+1 -1
View File
@@ -61,7 +61,7 @@ function love.conf(t)
t.window.minwidth = 480 t.window.minwidth = 480
t.window.minheight = 360 t.window.minheight = 360
end end
t.version = (love._os == "iOS" or love._os == "OS X") and "12.0" or "11.5" t.version = love._os == "iOS" and "12.0" or "11.5"
t.window.vsync = 1 t.window.vsync = 1
t.modules.audio = not companion t.modules.audio = not companion
t.modules.joystick = not companion t.modules.joystick = not companion
-19
View File
@@ -5,27 +5,8 @@
-- voucher exchange and the BICYCLE/CANCEL price window need more than -- voucher exchange and the BICYCLE/CANCEL price window need more than
-- command rows (#568). -- command rows (#568).
local TextBox = require("src.render.TextBox")
-- data/events/hidden_events.asm:542
local BIKE_DISPLAYS = {
{ 1, 0 }, { 2, 1 }, { 1, 2 }, { 3, 2 }, { 0, 4 }, { 1, 5 },
}
return { return {
BIKE_SHOP = { BIKE_SHOP = {
-- engine/events/hidden_events/new_bike.asm:1
onInteract = function(game, ow, fx, fy)
for _, c in ipairs(BIKE_DISPLAYS) do
if c[1] == fx and c[2] == fy then
game.stack:push(TextBox.new(game,
(game.data.text or {})._NewBicycleText or "A shiny new\nBICYCLE!"))
return true
end
end
return false
end,
talk = { talk = {
-- BikeShopMiddleAgedWomanText (pokered/scripts/BikeShop.asm): -- BikeShopMiddleAgedWomanText (pokered/scripts/BikeShop.asm):
-- always shows the same flavor line, no branching. -- always shows the same flavor line, no branching.
-1
View File
@@ -168,7 +168,6 @@ return {
{ "jump_if_true", "come_see" }, { "jump_if_true", "come_see" },
{ "set_flag", "EVENT_GOT_POKEBALLS_FROM_OAK" }, { "set_flag", "EVENT_GOT_POKEBALLS_FROM_OAK" },
{ "give_item", "POKE_BALL", 5, false }, { "give_item", "POKE_BALL", 5, false },
{ "text_sound", "Get_Key_Item" }, -- OaksLab.asm:1060
{ "show_text", "_OaksLabOak1ReceivedPokeballsText" }, { "show_text", "_OaksLabOak1ReceivedPokeballsText" },
{ "show_text", "_OaksLabGivePokeballsExplanationText" }, { "show_text", "_OaksLabGivePokeballsExplanationText" },
{ "jump", "end" }, { "jump", "end" },
+1 -11
View File
@@ -516,17 +516,7 @@ M.ROUTE_24 = {
push(game, text(game)._Route24CooltrainerM1YouCouldBecomeATopLeaderText, push(game, text(game)._Route24CooltrainerM1YouCouldBecomeATopLeaderText,
done) done)
else else
-- scripts/Route24.asm:125 ow:engageTrainer(npc, done)
ow:engageTrainer(npc, function()
if ow:trainerDefeated(npc) then
-- scripts/Route24.asm:62
push(game,
text(game)._Route24CooltrainerM1YouCouldBecomeATopLeaderText,
done)
else
done()
end
end, text(game)._Route24CooltrainerM1DefeatedText, true)
end end
end end
if not flags.EVENT_GOT_NUGGET then if not flags.EVENT_GOT_NUGGET then
+4 -5
View File
@@ -7,9 +7,9 @@ local M = {}
local function text(game) return game.data.text end local function text(game) return game.data.text end
local function push(game, s, done, opts) local function push(game, s, done)
local TextBox = require("src.render.TextBox") local TextBox = require("src.render.TextBox")
game.stack:push(TextBox.new(game, s, done, opts)) game.stack:push(TextBox.new(game, s, done))
end end
-- PrintText on a text_end string returns with the box still drawn and -- PrintText on a text_end string returns with the box still drawn and
@@ -236,6 +236,7 @@ M.CINNABAR_GYM = {
if yes == machine.yes then if yes == machine.yes then
-- CinnabarGymQuizCorrectText: item jingle, then the gate -- CinnabarGymQuizCorrectText: item jingle, then the gate
-- slides open (SFX_GO_INSIDE) if it was still locked -- slides open (SFX_GO_INSIDE) if it was still locked
Sound.play(game.data, "Get_Item1")
push(game, t._CinnabarGymQuizCorrectText push(game, t._CinnabarGymQuizCorrectText
or "You're absolutely\ncorrect!\fGo on through!", function() or "You're absolutely\ncorrect!\fGo on through!", function()
if not game.save.flags[gymGateFlag(index)] then if not game.save.flags[gymGateFlag(index)] then
@@ -243,9 +244,7 @@ M.CINNABAR_GYM = {
Sound.play(game.data, "Go_Inside") Sound.play(game.data, "Go_Inside")
end end
applyGymGates(game, ow) applyGymGates(game, ow)
end, { preSound = function() end)
return Sound.play(game.data, "Get_Item1")
end })
return return
end end
Sound.play(game.data, "Denied") Sound.play(game.data, "Denied")
+6 -6
View File
@@ -17,9 +17,9 @@ local function surfingPikachu(game)
return nil return nil
end end
local function push(game, text, done, opts) local function push(game, text, done)
local TextBox = require("src.render.TextBox") local TextBox = require("src.render.TextBox")
game.stack:push(TextBox.new(game, text, done, opts)) game.stack:push(TextBox.new(game, text, done))
end end
-- the two-variant posters: the surf-capable line once a surfing -- the two-variant posters: the surf-capable line once a surfing
@@ -69,11 +69,11 @@ return {
TEXT_SUMMERBEACHHOUSE_PIKACHU = function(game, ow, npc, done) TEXT_SUMMERBEACHHOUSE_PIKACHU = function(game, ow, npc, done)
local t = game.data.text local t = game.data.text
-- scripts/SummerBeachHouse.asm:68
push(game, t._SummerBeachHousePikachuText or "PIKACHU: Pikaa!", push(game, t._SummerBeachHousePikachuText or "PIKACHU: Pikaa!",
done, { auto = { wait = true, delay = 0, sound = function() function()
return require("src.core.Sound").playCry(game.data, "PIKACHU") require("src.core.Sound").playCry(game.data, "PIKACHU")
end } }) done()
end)
end, end,
TEXT_SUMMERBEACHHOUSE_POSTER1 = poster(1), TEXT_SUMMERBEACHHOUSE_POSTER1 = poster(1),
-120
View File
@@ -1,120 +0,0 @@
# Link play: threat model and what the code actually guarantees
Link play is the only part of this game that reads bytes written by
somebody else. This is what it defends against, what it does not, and
where each guarantee lives.
## The boundary
Everything a peer or the relay sends arrives as one JSON object per line.
There is exactly one place it becomes a message:
src/link/Net.lua reads bytes, frames lines, decodes JSON
src/link/Wire.lua rebuilds each line as a typed message
src/link/Session.lua the only path from a transport into a mode
`Session:update` runs `Wire.sanitize` on every message before anything
else sees it. A schema returns a **new** table holding only the fields it
names, at the Lua types it names, so the rest of `src/link/` can read
`msg.slot`, `msg.parts.actives` or `msg.mons[i].dvs.hp` directly and be
right by construction. A message with no schema (a mod's, or a future
build's) keeps a bounded, scalar-only copy of its payload instead of
being dropped.
A message that fails its schema is **dropped and logged**, never fatal.
Latching a terminal failure would hand a hostile peer a cheaper
disconnect than sending nothing at all.
### Why the bounds are loose
Wire's numeric bounds are deliberately wider than the game's own clamps in
`Protocol.unpackMon`. Both peers run identical clamps over identical
packets; a bound that bit an honest value would change one side's copy of
a mon and desync the lockstep. Wire's job is types and sizes. Rules are
`Protocol`'s job, and it keeps its own clamps for the callers that reach
it without a Session (the mod API, `tests/`).
### Containment behind it
Assume something still gets through:
- `Game:step` pcalls the link pump, and pcalls `stack:update` **only
while a link session is active**. On a throw, `Game:breakLink` closes
the connection, unwinds to the overworld and says "The link was
broken." Outside link play the stack is unguarded on purpose: a blanket
pcall would swallow real engine bugs and leave the game silently wrong
instead of loudly broken.
- `Net` caps `rxBuf` at 256KB and its per-frame read at 512KB, so a peer
that never sends a newline ends as a clean disconnect.
- `Json.decode` refuses documents nested past 64 levels, and takes an
optional length cap that the link path passes and the mod-manifest path
does not.
## The relay (`../pokeserver`)
- A line that is not a JSON **object** with a string `type` is dropped
before any handler runs, and `onLine` is wrapped in try/catch.
`server.js` installs `uncaughtException`/`unhandledRejection` handlers:
one bad packet must never take every live match down with the process.
- Line buffers are capped, lines per second are capped, connections per
IP and in total are capped, and an unbound connection that never hosts
or joins is swept after 30s.
- `SERVER_ONLY` is the set of message types the server is the only
legitimate author of (`peer_gone`, `bracket_update`, `match_start`,
`tournament_over`, `spectate`, ...). A peer that sends one has them
dropped rather than forwarded, so a bracket opponent cannot forge a
tournament result or fake "your opponent left".
- Trainer names are reduced to a printable subset and capped at the same
10 characters the game enforces, on the way in, because they are
rendered by the dashboard and broadcast to every participant.
`pokeserver/test/hostile.js` is the regression net for all of that.
## What is NOT defended
**Party legality is trust-the-client.** Online play meets strangers, and
`Handshake.onlineAllowed` is a Lua function in the same VM the mods load
into. It cannot be made tamper-proof in-process, and pretending otherwise
would only cost honest mod authors. What lockstep and
`Protocol.unpackMon`'s recompute-from-species-data *do* guarantee is that
a cheater cannot invent stats, moves, or a shiny: every derived value is
rebuilt locally from real species data. They can send a legal party they
farmed or edited. That is the honest boundary.
What the relay does instead is **observe and record**. It already sees
every `hello`, so it keeps each connection's self-reported
`engineVersion`, `fingerprint` and `linkModified`, compares the two sides
of a room or a live tournament match, and logs and surfaces a
`modded` / `fingerprint_mismatch` / `version_skew` flag on the dashboard.
A patched client can still lie; what it cannot do is lie without the
tournament organizer having a record of it.
Client-side attestation is deliberately not built. This is an
open-source Lua game: it would be theater, and it would break honest
mods.
**The relay has no TLS.** Port 7778 is plaintext, so party contents,
trades and trainer names are visible to anyone on the network path. There
is nothing secret in a Pokemon party, but it is a real property of the
system and not an oversight. Fixing it means a TLS terminator in front of
the relay and a client that speaks it, which is a version break for every
shipped build.
**The dashboard has no default password.** `DASHBOARD_PASSWORD` is
required; with it unset the relay runs and the dashboard simply does not
start. It is still Basic Auth over plain HTTP, so it belongs behind an
IP restriction or an SSH tunnel (`pokeserver/DEPLOY.md`).
## Tests
luajit tests/link_hostile.lua every message type x every wrong type
luajit tests/link_desync_fuzz.lua lockstep fuzz, plus a mutation mode
luajit tests/run_link_tests.lua both of the above, plus the rest
cd ../pokeserver && npm test relay smoke, 16-player bracket, hostile
`tests/link_hostile.lua` builds its corpus from a template per message
type, replaces each field (and several nested ones) with every wrong Lua
type, and drives the survivors through the real trade session, a real
lockstep battle, a real spectator battle, and the tournament screen
**including its draw** -- because the two nastiest payloads are
delayed-fuse ones that crash on render rather than on receipt.
+5 -5
View File
@@ -512,9 +512,8 @@ gains a field instead of the name gaining a prefix.
id under Gen 1's `name` key, which is the one payload difference the id under Gen 1's `name` key, which is the one payload difference the
numeric flag space forces. numeric flag space forces.
- *Menus (`src/ui/gen2/`):* `ui.start_menu.items`, `ui.title_menu.items`, - *Menus (`src/ui/gen2/`):* `ui.start_menu.items`, `ui.title_menu.items`,
`ui.options.rows`, `ui.party.submenu`, `ui.party.grid_navigation`, `ui.options.rows`, `ui.party.submenu`, `ui.naming.grid`, `ui.pc.items`,
`ui.naming.grid`, `ui.pc.items`, `ui.list_menu`, `transition.style`. `ui.list_menu`, `transition.style`. `ui.list_menu` covers Gold's script
`ui.list_menu` covers Gold's script
menus (`ScriptMenu.lua`); the `Chrome.List` widget the START and title menus (`ScriptMenu.lua`); the `Chrome.List` widget the START and title
menus draw with does not raise it yet, so those two are composed through menus draw with does not raise it yet, so those two are composed through
their own hooks only. their own hooks only.
@@ -541,8 +540,9 @@ gains a field instead of the name gaining a prefix.
`battle.crit`, `battle.accuracy`, `battle.turn_order`, `battle.crit`, `battle.accuracy`, `battle.turn_order`,
`battle.enemy_action`, `battle.run`, `battle.exp_award`, `exp.gain`, `battle.enemy_action`, `battle.run`, `battle.exp_award`, `exp.gain`,
`catch.rate`, `trainer.party`, `battle.overlay`, `battle.low_health_alarm`, `catch.rate`, `trainer.party`, `battle.overlay`, `battle.low_health_alarm`,
`battle.catch_exp`, `battle.bottom_ui_visible` and `battle.catch_exp`, `battle.bottom_ui_visible`,
`battle.status_hud_visible`. One payload difference: Gen 1's vanilla `battle.status_hud_visible` and `battle.move_grid_navigation`. One payload
difference: Gen 1's vanilla
`battle.low_health_alarm` link reads `ctx.battle.data`, and Gold's battle `battle.low_health_alarm` link reads `ctx.battle.data`, and Gold's battle
screen has no `.data` field, so the Gen 2 site **adds** `ctx.data` beside the screen has no `.data` field, so the Gen 2 site **adds** `ctx.data` beside the
Gen 1 keys. A mod that calls `nextFn` is unaffected; one that reaches through Gen 1 keys. A mod that calls `nextFn` is unaffected; one that reaches through
+1 -1
View File
@@ -11,7 +11,7 @@ Features intentionally added beyond the original Pokémon Red, Blue, and Yellow
* **Persistent custom options** stored separately from game saves * **Persistent custom options** stored separately from game saves
* **Optional widescreen battle layout** * **Optional widescreen battle layout**
* **Mobile touch controls** with editable layouts, vibration, and orientation settings * **Mobile touch controls** with editable layouts, vibration, and orientation settings
* **Touch skins** in RetroArch overlay format and Delta `.deltaskin` (including PDF-wrapped bezel art), with per-button press states and Super Game Boy borders * **Touch skins** in RetroArch overlay format, with bezel art, per-button press states, and Super Game Boy borders
* **Pokédex diploma and printer image exports** * **Pokédex diploma and printer image exports**
## Gen 2 Specifics ## Gen 2 Specifics
+17 -109
View File
@@ -4,23 +4,17 @@ A **skin** replaces the on-screen controls wholesale: a bezel image, a
control layout, and the rectangle the Game Boy screen is drawn into. Engine: control layout, and the rectangle the Game Boy screen is drawn into. Engine:
`src/core/TouchSkin.lua` (model, parsers, zip export), `src/core/TouchControls.lua` `src/core/TouchSkin.lua` (model, parsers, zip export), `src/core/TouchControls.lua`
(draw and input), `src/render/Renderer.lua` (the screen viewport), (draw and input), `src/render/Renderer.lua` (the screen viewport),
`src/core/DeltaSkin.lua` (Delta `.deltaskin` import and export),
`src/ui/SkinStudio.lua` (the desktop editor). Tests: `src/ui/SkinStudio.lua` (the desktop editor). Tests:
`tests/engine/touch_skin_test.lua`, `tests/engine/skin_studio_test.lua`, `tests/engine/touch_skin_test.lua`, `tests/engine/skin_studio_test.lua`,
`tests/engine/skin_studio_ux.lua`,
`tests/engine/skin_studio_image_import.lua`, `tests/engine/skin_studio_image_import.lua`,
`tests/engine/skin_format_import_test.lua`, `tests/engine/launcher_skins_tab.lua`.
`tests/engine/launcher_skins_tab.lua`,
`tests/engine/launcher_skins_ux.lua`.
Skins are picked in the launcher's **Skins** tab, which also imports them and Skins are picked in the launcher's **Skins** tab, which also imports them and
opens the studio. `options.touchControls.skin` holds the folder name. opens the studio. `options.touchControls.skin` holds the folder name.
## Formats ## Formats
Three load: the native `skin.lua`, a RetroArch overlay `.cfg`, and a Delta Two load. `skin.lua` wins when a folder has both.
`.deltaskin`. `skin.lua` wins when a folder has more than one. The launcher
badges each installed skin with the format it was read from.
**RetroArch overlay `.cfg`.** The libretro `common-overlays` collection loads **RetroArch overlay `.cfg`.** The libretro `common-overlays` collection loads
as-is. Supported keys: as-is. Supported keys:
@@ -32,7 +26,7 @@ as-is. Supported keys:
| `overlayN_overlay` | bezel image | | `overlayN_overlay` | bezel image |
| `overlayN_full_screen` | stretch the page to the window | | `overlayN_full_screen` | stretch the page to the window |
| `overlayN_rect` | page placement, default `0,0,1,1` | | `overlayN_rect` | page placement, default `0,0,1,1` |
| `overlayN_aspect_ratio` | design aspect; the overlay letterboxes to it even when full screen | | `overlayN_aspect_ratio` | fallback aspect when not full screen |
| `overlayN_range_mod`, `overlayN_alpha_mod` | desc defaults | | `overlayN_range_mod`, `overlayN_alpha_mod` | desc defaults |
| `overlayN_viewport` | `x,y,w,h`, the screen cutout | | `overlayN_viewport` | `x,y,w,h`, the screen cutout |
| `overlayN_viewport_fill` | parsed; the engine always fits, see below | | `overlayN_viewport_fill` | parsed; the engine always fits, see below |
@@ -47,15 +41,6 @@ Hitboxes are `radial` or `rect`. Pipe-separated binds (`left|down`) are one
control that holds both. A `nul` desc is decoration: it draws and never control that holds both. A `nul` desc is decoration: it draws and never
captures a touch. captures a touch.
The area desc types are expanded rather than ignored: `dpad_area`,
`abxy_area`, `analog_left` and `analog_right` each become eight hitboxes over
the same area, one per 45 degree sector measured from its centre, the way
RetroArch resolves them: there is no neutral middle, and the four corner
sectors fire two inputs. Any `_up` / `_down` / `_left` / `_right` override and
the per-side reach are honoured, and the desc's own art is kept as decoration
over the top. Exporting a cfg folds the eight back into the one area desc they
came from. `retrok_<key>` is a keyboard bind.
Alpha follows RetroArch (`input_driver.c`, `input_overlay_post_poll`): every Alpha follows RetroArch (`input_driver.c`, `input_overlay_post_poll`): every
image sits at the overlay opacity, and a pressed control's image swaps to image sits at the overlay opacity, and a pressed control's image swaps to
`opacity * alpha_mod`. So `alpha_mod` above 1 lights a control up and below 1 `opacity * alpha_mod`. So `alpha_mod` above 1 lights a control up and below 1
@@ -86,32 +71,6 @@ return {
} }
``` ```
**Delta `.deltaskin`.** A zip (any wrapping folder is stripped) holding an
`info.json` plus its art. The `representations` tree is walked
device / display type / orientation, and every orientation that exists becomes
a page; `page.orient` is the orientation key, so a portrait/landscape pair
auto-rotates like a RetroArch one. Item `frame` rects are top-left plus size in
`mappingSize` points and are converted to the native centre plus half extent;
`extendedEdges` merge per key into the reach fields; `mask: "circle"` becomes a
radial hitbox. A `dpad` or `thumbstick` item expands into the 3x3 grid, so the
corners fire two directions. `screens[1].outputFrame` (or the legacy
`gameScreenFrame`) becomes the screen cutout. A portrait page with neither
keeps `mappingSize` as the overlay aspect, sits at the bottom of the
window, and puts the Game Boy picture in the leftover space above -- the
usual GBA4iOS controller-deck layout. Pages that name a screen rect still
stretch to the window the way Delta does. Host functions map to
engine hotkeys: `menu` to `menu_toggle`, `fastForward` to
`hold_fast_forward`, `toggleFastForward` to `toggle_fast_forward`;
`quickSave` and `quickLoad` have nothing to bind to and drop to decoration.
Both `com.rileytestut.delta.game.*` and Manic's `public.aoshuang.game.*`
identifiers are accepted, and a non Game Boy system warns instead of failing.
PDF artwork is usually a JPEG wrapped so iOS can scale it (Delta's
Image-to-PDF skins, Preview exports, and the like). Import extracts that
JPEG and draws it; a true vector PDF with no embedded image is still refused,
with a message asking for a PNG version. GBA4iOS `.gbcskin` / `.gbaskin` files
are an older, incompatible schema and are refused by name.
## Bindable actions ## Bindable actions
The eight Game Boy buttons: `a`, `b`, `start`, `select`, `up`, `down`, The eight Game Boy buttons: `a`, `b`, `start`, `select`, `up`, `down`,
@@ -160,22 +119,10 @@ them. Anything that binds a button still follows the usual mobile /
## Installing ## Installing
Four roads, all of them landing in `skins/` in the save directory: Drop a folder or a `.zip` into `skins/` in the save directory, or drop a zip on
the launcher window while the Skins tab is open. A zip is mounted in place, so
* **Import** on the Skins tab opens the host file picker for a `.zip` or a there is nothing to unpack. The folder needs one `skin.lua` or `.cfg`
`.deltaskin`. (`overlay.cfg` is preferred when there are several) and the images it names.
* **Paste a skin link** in the tab's URL row, then **Add**. The download runs
on the fetch pool (`src/net/Fetch.lua`), so the launcher stays live, and the
row shows a spinner until it lands. A link to a bare `overlay.cfg` is wrapped
into an archive on the way in. This is the road that works on a phone, where
there is no file picker to speak of.
* Drop a `.zip` or `.deltaskin` on the launcher window while the Skins tab is
open.
* Copy a folder or archive into `skins/` by hand.
An archive is mounted in place, so there is nothing to unpack. It needs one
`skin.lua`, `.cfg` (`overlay.cfg` is preferred when there are several) or
`info.json`, plus the images it names.
Two ship bundled, both from libretro's `common-overlays` under CC-BY-4.0: Two ship bundled, both from libretro's `common-overlays` under CC-BY-4.0:
@@ -210,40 +157,14 @@ The Super Game Boy preset locks the viewport to the real screen window,
160x144 at (48,40), so an SGB border cannot be drawn out of register. 160x144 at (48,40), so an SGB border cannot be drawn out of register.
**Editing.** Click a control to select it, drag to move, eight handles to **Editing.** Click a control to select it, drag to move, eight handles to
resize. Arrow keys nudge the selection one canvas pixel, shift-arrow ten. While resize. X / Y / W / H are in canvas pixels, so a control can be typed to the
a control is dragged it snaps to the centres and edges of the other controls coordinate its art was drawn at. Bind, hitbox shape, hit reach and idle and
and of the page itself when it comes within a few pixels, and the guide it
snapped to is drawn. X / Y / W / H are in canvas pixels, so a control can be
typed to the coordinate its art was drawn at. **Back** and **Front** move the
selection through the draw order. Bind, hitbox shape, hit reach and idle and
pressed images are per control; the bezel, the pages and the screen cutout are pressed images are per control; the bezel, the pages and the screen cutout are
per page. The cutout is itself a draggable element with a 10:9 lock. per page. The cutout is itself a draggable element with a 10:9 lock.
**Bind** opens a grid of every bind the engine understands: the eight Game Boy **Art.** The **Bezel**, **Idle art** and **Pressed art** rows cycle through the
buttons, the diagonal pairs, every hotkey, a few `key:` entries, and images already in the skin folder; the **Import** button beside each one opens
decoration. The COMBINE chips at the top toggle one part at a time, which is the host file picker (`src/core/FilePicker.lua`: osascript, PowerShell,
how a pipe bind like `left|down` is built without typing it.
**Undo** and **Redo** in the top bar cover every edit (ctrl+Z / ctrl+Y, or
`u` / shift+`u` without a keyboard modifier). The stack holds the last 50
actions. `L` toggles the bind captions drawn on the canvas.
Each page can **Lock** to portrait or landscape. With **Match canvas** on
(the default), the page list picks a matching mock device and the canvas preset
picks a matching page. Turn Match canvas off to look at a portrait page on a
landscape device. **Pages** opens the page list, where a page is selected,
renamed or deleted.
Starting a new skin, opening another one or closing the studio with unsaved
edits prompts first, with Save first / Discard / Cancel.
A RetroArch overlay whose pages are already named portrait / landscape
(the auto-rotate convention) locks those pages and turns Match canvas on
when you open it. You do not have to click Lock first.
**Art.** The **Bezel**, **Idle art** and **Pressed art** rows open a
thumbnail grid of the images already in the skin folder, with `(none)` first;
the **Import** button there and beside each row opens the host file picker (`src/core/FilePicker.lua`: osascript, PowerShell,
zenity/kdialog) and copies the chosen PNG or JPG into `img/` under the name in zenity/kdialog) and copies the chosen PNG or JPG into `img/` under the name in
the SKIN field, then assigns it to that slot. Dropping a PNG or JPG on the the SKIN field, then assigns it to that slot. Dropping a PNG or JPG on the
window does the same for whichever slot was last touched. A new bezel does not window does the same for whichever slot was last touched. A new bezel does not
@@ -255,24 +176,11 @@ buttons and the footer reports what is held. **Play** saves the skin, selects
it, and boots the game with it. it, and boots the game with it.
**Saving.** **Save** writes `skins/<name>/skin.lua` and copies every image the **Saving.** **Save** writes `skins/<name>/skin.lua` and copies every image the
skin names, so the folder stands alone. **Export** offers three formats, and skin names, so the folder stands alone. **Export** packs it as one zip
the Skins tab's gear offers the same three for any installed skin: (`src/core/SkinZip.lua`, store-only) carrying the native `skin.lua`, the
images, and the original `.cfg` when it came from one. An exported skin drops
| Export | Contents | straight back into `skins/` and still opens in RetroArch.
| --- | --- |
| gen1recomp `.zip` | the native `skin.lua`, the images, and the original `.cfg` when it came from one |
| RetroArch `.zip` | an `overlay.cfg` generated from the model, plus the images |
| Delta `.deltaskin` | an `info.json` generated from the model, plus the images |
All three are written store-only (`src/core/SkinZip.lua`) into `skins/_export/`
in the save directory, which is outside the folder the skin list scans, so an
export can never shadow the skin it came from. The notice names the full path
so a phone can find the file in its own file manager. On desktop **Show the
exported file** opens that folder.
## Not implemented ## Not implemented
True vector Delta skins (PDF artwork with no embedded JPEG). Those still need RetroArch's `analog_*`, `dpad_area`, `abxy_area` and `retrok_*` desc types.
a PDF renderer this engine does not carry, so they are refused with a message
rather than imported half-drawn. PDF files that wrap a JPEG, the usual Delta
skin case, extract on import.
+3 -5
View File
@@ -169,7 +169,6 @@ the NX runtime modules `src/core/NxAssetOverlay.lua`, `src/core/Platform.lua`,
`tests/engine/assets_version_fallback_test.lua`, `tests/engine/assets_version_fallback_test.lua`,
`tests/engine/nx_generated_guard_test.lua`, `tests/engine/nx_generated_guard_test.lua`,
`tests/engine/nx_yellow_boot_test.lua`, `tests/engine/nx_yellow_boot_test.lua`,
`tests/engine/cache_fs_gold_nx_load_test.lua`,
`tests/engine/switch_diagnostics_test.lua`, `tests/engine/platform_nx_*`, `tests/engine/switch_diagnostics_test.lua`, `tests/engine/platform_nx_*`,
or the Switch-related workflow YAML), CI runs: or the Switch-related workflow YAML), CI runs:
@@ -180,16 +179,15 @@ or the Switch-related workflow YAML), CI runs:
`luajit tests/switch_transfer_docs_test.lua`, and the NX engine suites `luajit tests/switch_transfer_docs_test.lua`, and the NX engine suites
headlessly (`luajit tests/engine/assets_version_fallback_test.lua`, headlessly (`luajit tests/engine/assets_version_fallback_test.lua`,
`luajit tests/engine/nx_generated_guard_test.lua`, `luajit tests/engine/nx_generated_guard_test.lua`,
`luajit tests/engine/nx_yellow_boot_test.lua`, `luajit tests/engine/nx_yellow_boot_test.lua`).
`luajit tests/engine/cache_fs_gold_nx_load_test.lua`).
2. **Fused NRO build** only on the **main** repository 2. **Fused NRO build** only on the **main** repository
(`bryanthaboi/gen1recomp`), on the self-hosted Mac runner (`bryanthaboi/gen1recomp`), on the self-hosted Mac runner
(`scripts/build_switch.sh --fetch --fused`), and only when the workflow (`scripts/build_switch.sh --fetch --fused`), and only when the workflow
head is that repo (same-repo push/PR). Fork CI never runs fused. Fork PRs into the main repo also skip Switch fused (offline selftest still runs) so untrusted head code is not executed on the self-hosted Mac; iOS device build eligibility is unchanged. Fused also waits for a successful offline selftest before starting on the Mac runner. head is that repo (same-repo push/PR). Fork CI never runs fused. Fork PRs into the main repo also skip Switch fused (offline selftest still runs) so untrusted head code is not executed on the self-hosted Mac; iOS device build eligibility is unchanged. Fused also waits for a successful offline selftest before starting on the Mac runner.
3. On successful PR fused builds, a follow-up workflow posts a PR comment 3. On successful PR fused builds, a follow-up workflow posts a PR comment
linking the Actions artifact named `gen1recomp-switch-nro` linking the Actions artifact named `gen1recomp-switch-nro`
(comment tag `platform-build-result`; see (comment tag `switch-build-result`; see
`.github/workflows/platform-artifact-comment.yml`). `.github/workflows/switch-artifact-comment.yml`).
Unrelated PRs do not burn the self-hosted Mac on Switch packaging. Unrelated PRs do not burn the self-hosted Mac on Switch packaging.
+9 -13
View File
@@ -3,7 +3,7 @@
Every GitHub Release that includes Switch support ships an SD-ready zip: Every GitHub Release that includes Switch support ships an SD-ready zip:
`gen1recomp-*-switch.zip`. Extract it at the root of your microSD (install `gen1recomp-*-switch.zip`. Extract it at the root of your microSD (install
or update, same steps), launch with **title override**, then import your or update, same steps), launch with **title override**, then import your
own legal `.gb` / `.gbc` ROM. own legal `.gb` ROM.
> You need a console that can run Switch homebrew (custom firmware / hbmenu). > You need a console that can run Switch homebrew (custom firmware / hbmenu).
> This project does not help you set that up. > This project does not help you set that up.
@@ -91,14 +91,13 @@ Do **not** launch from the Album applet path for normal play.
This project ships **no** game data. On first launch: This project ships **no** game data. On first launch:
1. Put your own legally obtained Pokémon Red, Blue (`.gb`), Yellow, or 1. Put your own legally obtained Pokémon Red, Blue (`.gb`), or Yellow
Gold (`.gbc`) dump into `switch/gen1recomp/pokemon-love2d/imports/` (the (`.gbc`) dump into `switch/gen1recomp/pokemon-love2d/imports/` (the
launcher also shows the live save-dir path). All four can sit in the launcher also shows the live save-dir path). All three can sit in the
same folder. same folder.
2. Use **Scan again** on that game's tab (Red / Blue / Yellow / Gold). 2. Use **Scan again** on that game's tab (Red / Blue / Yellow). Rescan
Rescan matches by ROM SHA-1 for the open tab only. A Red dump never matches by ROM SHA-1 for the open tab only. A Red dump never imports
imports from the Yellow tab (and vice versa). Gold is Beta in the from the Yellow tab (and vice versa).
launcher; a clean US Gold dump is enough to Play.
## 5. Import / Export a raw `.sav` ## 5. Import / Export a raw `.sav`
@@ -110,13 +109,10 @@ SD / FTP, same transfer methods as ROMs. Paths are **per game**:
| Red | `imports/saves/red/` | `exports/red/` | | Red | `imports/saves/red/` | `exports/red/` |
| Blue | `imports/saves/blue/` | `exports/blue/` | | Blue | `imports/saves/blue/` | `exports/blue/` |
| Yellow | `imports/saves/yellow/` | `exports/yellow/` | | Yellow | `imports/saves/yellow/` | `exports/yellow/` |
| Gold | `imports/saves/gold/` | `exports/gold/` |
(Under the save dir `pokemon-love2d/`. The zip already creates these folders. (Under the save dir `pokemon-love2d/`. The zip already creates these folders.)
Gold cart `.sav` import/export is not supported yet -- the folders exist so
MTP browsing matches the other games. Gold progress still saves in-engine.)
1. Copy a Gen 1 `.sav` (32 KB) into that game's inbox under the save dir 1. Copy a Gen1 `.sav` (32 KB) into that game's inbox under the save dir
([switch-transfer.md](switch-transfer.md)). ([switch-transfer.md](switch-transfer.md)).
2. With the game's ROM already imported, open **that game's tab** 2. With the game's ROM already imported, open **that game's tab**
**SAVE FILES****Import save**. Only that folder is scanned. **SAVE FILES****Import save**. Only that folder is scanned.
+4 -4
View File
@@ -22,8 +22,8 @@ Player install (what to download, title override) stays in
| Loose iteration pair | `sdmc:/switch/gen1recomp/gen1recomp.nro` **and** `game.love` beside it | | Loose iteration pair | `sdmc:/switch/gen1recomp/gen1recomp.nro` **and** `game.love` beside it |
| ROM inbox | LÖVE save dir → `imports/` (launcher shows the live `getSaveDirectory()` path; under MTP often `1: SD Card/<save identity>/imports/`) | | ROM inbox | LÖVE save dir → `imports/` (launcher shows the live `getSaveDirectory()` path; under MTP often `1: SD Card/<save identity>/imports/`) |
| Mod zip inbox | Same save dir → `imports/mods/` then MODS → **Scan again** | | Mod zip inbox | Same save dir → `imports/mods/` then MODS → **Scan again** |
| Save `.sav` inbox | Same save dir → `imports/saves/red\|blue\|yellow\|gold/` then that game's SAVE FILES → **Import save** (Gold cart `.sav` not supported yet) | | Save `.sav` inbox | Same save dir → `imports/saves/red\|blue\|yellow/` then that game's SAVE FILES → **Import save** |
| Save exports | Same save dir → `exports/red\|blue\|yellow\|gold/` (pull after **Export save**; Gold cart `.sav` not supported yet) | | Save exports | Same save dir → `exports/red\|blue\|yellow/` (pull after **Export save**; MTP / SD / FTP) |
| Opt-in diagnostics | Empty `switch-debug.txt` in the save dir → `switch.log` | | Opt-in diagnostics | Empty `switch-debug.txt` in the save dir → `switch.log` |
| Lua error log | `lua-error.log` in the save dir | | Lua error log | `lua-error.log` in the save dir |
@@ -54,8 +54,8 @@ macOS, not a Mac-only requirement.
3. Create `switch/gen1recomp/` if needed; extract the release zip at SD root 3. Create `switch/gen1recomp/` if needed; extract the release zip at SD root
(or copy NRO / `game.love` for loose). (or copy NRO / `game.love` for loose).
4. For ROMs/mods/saves, open the save-dir `imports/`, `imports/mods/`, 4. For ROMs/mods/saves, open the save-dir `imports/`, `imports/mods/`,
`imports/saves/<red|blue|yellow|gold>/`, or `exports/<red|blue|yellow|gold>/` `imports/saves/<red|blue|yellow>/`, or `exports/<red|blue|yellow>/` path the
path the launcher prints. launcher prints.
5. Wait for the queue; refresh; exit MTP responder; title-override launch. 5. Wait for the queue; refresh; exit MTP responder; title-override launch.
macOS clients often create AppleDouble sidecars (`._Something.zip`, macOS clients often create AppleDouble sidecars (`._Something.zip`,
-65
View File
@@ -1,65 +0,0 @@
# Tiled map editing (mod authoring)
`tools/tiled_export.py` turns the imported ROM cache into a
[Tiled](https://www.mapeditor.org) workspace, so maps can be edited in a
real map editor and exported back out as a mod. The original had no map
editor at all; the port's own map data is plain Lua, which is what makes
this a data path rather than an asset path.
Editing is done in our own Tiled build,
[bryanthaboi/tiled_gen1recomp](https://github.com/bryanthaboi/tiled_gen1recomp/releases),
which ships the `gen1-mod-export` extension the workspace relies on. Grab it
from that repo's releases; upstream Tiled opens the workspace but cannot
export a mod out of it.
```sh
python3 tools/tiled_export.py # -> build/tiled/ (gitignored)
```
Then open `build/tiled/gen1.tiled-project` in that build of Tiled.
- **The overworld is one surface.** All 222 maps become `maps/*.tmj`, and
`kanto.world` places the 36 connected overworld maps at their real
connection offsets. That world is pre-loaded (seeded into the workspace's
Tiled session), so opening any one overworld map draws its neighbors around
it and you scroll and edit straight across the seams. Everything else is a
double-click away in Tiled's project panel.
- **Extending Kanto wires both ends.** A connection lives on both maps, so
hooking a new map onto a base map also emits the return connection as a
patch on that base map, keeping its other directions intact. The return
offset is derived, not guessed: all 78 vanilla reciprocal pairs satisfy
`back.offset == -offset`.
- **A Tiled tile is a gen1 block.** Each of the 24 tilesets becomes a Tiled
tileset whose tiles are its 32x32 blocks, composited from the 8x8 sheet,
so a tile layer *is* the map's `blocks` array. Warps, signs and objects
sit on the 16px cell grid in object layers, which is the grid the engine
addresses them on.
- **Collision is visible.** View > Show Tile Collision Shapes draws the real
walkability: a rectangle covers each cell whose feet tile is not in the
tileset's `walkable` list, which is the rule `src/world/Map.lua` applies.
- **Maps are shown in their real colors.** Each map is atlased in the SGB
palette it renders with, so Cerulean is blue and Lavender is purple in the
editor exactly as in game. Vanilla resolves that through a cascade with
interiors inheriting the last outdoor map, so the workspace mirrors the
cascade and walks the warp graph to colour interiors. Changing a map's
`palette` exports `palette = "..."` on the record, which beats the cascade,
and the editor offers the real palette names as a dropdown.
- **New blocks and new tilesets.** `blocksets/*.tmj` show a tileset's blocks
as raw 8x8 tiles, four by four, so new blocks can be composed there;
per-tile flags on `tilesets/tiles_*.tsj` become `walkable`, `waterTiles`,
`doorTiles` and the rest.
- **Export is a diff, not a fork of the data.** The `gen1-mod-export`
extension (shipped in `tiled_gen1recomp`) writes either one map file or a whole
loadable mod folder. An edited vanilla map diffs against the imported data
and emits `mod.content.maps:patch` carrying *only* the fields that moved, so
a mod covers the parts it changes and leaves the rest to the base game; a
new map gets `:register` at an index of 1000 or above. An unchanged map
exports nothing at all. Exports pass `tools/modkit.py validate` and `lint`.
- **Or the whole record, on request.** Ticking `exactExport` on a map switches
it to `mod.content.maps:override`, pinning the map to exactly what the
editor shows. It is off by default because an override wins outright over
any other mod patching that map, where a patch composes.
No ROM-derived art travels into an exported mod: a tileset still drawing on
the player's own imported sheet references that path rather than shipping the
pixels, and only a sheet the author supplied is copied in.
+15 -122
View File
@@ -291,78 +291,6 @@ function closeSkinStudio()
end end
end end
local function makeLauncher()
local RomImporter = require("src.import.RomImporter")
local forceImport = os.getenv("POKEPORT_FORCE_IMPORT") == "1"
return RomImporter.new(function(version)
Importer = nil
bootGame(version)
end, {
launcher = true,
forceImport = forceImport,
onEditSave = openEditor,
onEditTouchControls = openTouchControlsEditor,
onOpenSkinStudio = require("src.ui.SkinStudio").available_desktop()
and openSkinStudio or nil,
})
end
local function returnToLauncher()
if not Game then return end
pcall(function() require("src.core.Music").stop() end)
pcall(function() require("src.core.Sound").stop() end)
if package.loaded["src.core.ChipAudio"] then
pcall(package.loaded["src.core.ChipAudio"].shutdown)
end
if package.loaded["src.core.DiscordPresence"] then
pcall(package.loaded["src.core.DiscordPresence"].shutdown)
end
if package.loaded["src.core.gen2.Clock"] then
pcall(package.loaded["src.core.gen2.Clock"].shutdown)
end
if package.loaded["src.net.Gen1Tls"] then
pcall(package.loaded["src.net.Gen1Tls"].shutdown)
end
if love.audio and love.audio.stop then
pcall(love.audio.stop)
end
local GameVersion = require("src.core.GameVersion")
local currentVersion = GameVersion.get()
if currentVersion then
require("src.import.CacheFs").unmountVersion(currentVersion)
end
require("src.core.Data"):unloadGenerated()
local Runtime = require("src.mods.Runtime")
if Runtime.reset then
Runtime.reset()
end
Game = nil
autopilot = nil
driverCo = nil
local Input = require("src.core.Input")
local TouchControls = require("src.core.TouchControls")
Input:reset()
TouchControls:reset()
require("src.core.Orientation").applyOptions(
require("src.core.SaveData").loadOptions())
local preload = require("src.mods.LauncherMods").translationStrings()
if preload then require("src.core.Strings").load({ strings = preload }) end
if love.window and love.window.setTitle then
local Version = require("src.core.Version")
love.window.setTitle(Version.title("Gen 1 Recompilation Project"))
end
Importer = makeLauncher()
end
function bootGame(version) function bootGame(version)
-- The launcher hands us the chosen game (Red / Blue / Yellow / Gold); -- The launcher hands us the chosen game (Red / Blue / Yellow / Gold);
-- scripted and headless runs fall back to POKEPORT_VERSION, then Red. -- scripted and headless runs fall back to POKEPORT_VERSION, then Red.
@@ -454,7 +382,7 @@ function love.load(args)
-- Apply the persisted Android orientation lock (#592) before the launcher -- Apply the persisted Android orientation lock (#592) before the launcher
-- shows: SDL created the window with no orientation hint, so without this -- shows: SDL created the window with no orientation hint, so without this
-- the launcher would rotate freely until options are applied at boot. -- the launcher would rotate freely until Game:applyOptions runs at boot.
-- No-op on desktop / iOS / when options.lua does not exist yet. -- No-op on desktop / iOS / when options.lua does not exist yet.
require("src.core.Orientation").applyOptions( require("src.core.Orientation").applyOptions(
require("src.core.SaveData").loadOptions()) require("src.core.SaveData").loadOptions())
@@ -514,8 +442,8 @@ function love.load(args)
-- (#767) only pays off if something fills that catalog this early, and no -- (#767) only pays off if something fills that catalog this early, and no
-- restart could: the ordering is the same on every launch. Read the -- restart could: the ordering is the same on every launch. Read the
-- enabled mods' string catalogs -- data only, no entry chunk -- so a -- enabled mods' string catalogs -- data only, no entry chunk -- so a
-- translation reaches the launcher too. The active game's loader replaces -- translation reaches the launcher too. Game:load replaces this with the
-- this with the real merged catalog once a version boots. -- real merged catalog once a version boots.
do do
local preload = require("src.mods.LauncherMods").translationStrings() local preload = require("src.mods.LauncherMods").translationStrings()
if preload then require("src.core.Strings").load({ strings = preload }) end if preload then require("src.core.Strings").load({ strings = preload }) end
@@ -556,7 +484,17 @@ function love.load(args)
-- by its SHA-1 (GameVersion.forSha1); pressing Play boots that game (Gold -- by its SHA-1 (GameVersion.forSha1); pressing Play boots that game (Gold
-- goes to its own service owner, src/core/Game2.lua -- docs/gold-phase1.md). -- goes to its own service owner, src/core/Game2.lua -- docs/gold-phase1.md).
-- Edit on a save row opens the bundled editor on that slot (openEditor). -- Edit on a save row opens the bundled editor on that slot (openEditor).
Importer = makeLauncher() Importer = RomImporter.new(function(version)
Importer = nil
bootGame(version)
end, {
launcher = true,
forceImport = forceImport,
onEditSave = openEditor,
onEditTouchControls = openTouchControlsEditor,
onOpenSkinStudio = require("src.ui.SkinStudio").available_desktop()
and openSkinStudio or nil,
})
end end
function love.update(dt) function love.update(dt)
@@ -870,46 +808,6 @@ function love.lowmemory()
if Game then Game:onResume() end if Game then Game:onResume() end
end end
love.handlers = love.handlers or {}
function love.handlers.audiosuspend()
local ChipAudio = package.loaded["src.core.ChipAudio"]
if ChipAudio then pcall(ChipAudio.setSuspended, true) end
end
function love.handlers.audioreset()
local ChipAudio = package.loaded["src.core.ChipAudio"]
if ChipAudio then
pcall(ChipAudio.setSuspended, false)
pcall(ChipAudio.rebuildPlayback)
end
local Music = package.loaded["src.core.Music"]
if Music then pcall(Music.onDeviceReset) end
local Sound = package.loaded["src.core.Sound"]
if Sound then pcall(Sound.onDeviceReset) end
end
function love.handlers.intent_game(version)
if type(version) ~= "string" or version == "" then return end
version = version:lower():gsub("^%s+", ""):gsub("%s+$", "")
local GameVersion = require("src.core.GameVersion")
if GameVersion.VERSIONS and not GameVersion.VERSIONS[version] then return end
local RomImporter = require("src.import.RomImporter")
if not RomImporter.isReady(version) then return end
local currentVersion = GameVersion.get()
if Game and currentVersion == version then
return
end
if Game then
returnToLauncher()
end
Importer = nil
bootGame(version)
end
function love.touchpressed(id, x, y, dx, dy, pressure) function love.touchpressed(id, x, y, dx, dy, pressure)
if editorMode then if editorMode then
-- iOS synthesizes mousepressed for the primary touch; forwarding here -- iOS synthesizes mousepressed for the primary touch; forwarding here
@@ -1115,16 +1013,11 @@ function love.quit()
-- docs/modding.md's core.quit_to_launcher entry) may veto returning to -- docs/modding.md's core.quit_to_launcher entry) may veto returning to
-- this Lua launcher via that hook. Vanilla behavior (used when no mod -- this Lua launcher via that hook. Vanilla behavior (used when no mod
-- claims the hook) is exactly the condition below. -- claims the hook) is exactly the condition below.
local isAndroid = (love.system and love.system.getOS and love.system.getOS() == "Android")
local wouldReturnToLauncher = PlatformHooks.quitToLauncher(function() local wouldReturnToLauncher = PlatformHooks.quitToLauncher(function()
return Game and not Importer and not quitToLauncher and not scripted return Game and not Importer and not quitToLauncher and not scripted
and (isAndroid or not launchedIntoGame) and not launchedIntoGame
end) end)
if wouldReturnToLauncher then if wouldReturnToLauncher then
if isAndroid then
returnToLauncher()
return true -- abort this quit; the restart lands back in the launcher
end
quitToLauncher = true quitToLauncher = true
-- Tell the fresh boot to ignore any boot-straight-into-a-game option this -- Tell the fresh boot to ignore any boot-straight-into-a-game option this
-- once, so the restart really does land in the launcher (#887). A failed -- once, so the restart really does land in the launcher (#887). A failed
@@ -29,8 +29,7 @@
<application <application
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@drawable/love"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="${NAME}" > android:label="${NAME}" >
<meta-data <meta-data
android:name="android.allow_multiple_resumed_activities" android:name="android.allow_multiple_resumed_activities"
@@ -40,7 +39,7 @@
android:exported="true" android:exported="true"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
android:label="${NAME}" android:label="${NAME}"
android:launchMode="singleTask" android:launchMode="singleInstance"
android:screenOrientation="${ORIENTATION}" android:screenOrientation="${ORIENTATION}"
android:resizeableActivity="false" android:resizeableActivity="false"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" > android:theme="@android:style/Theme.NoTitleBar.Fullscreen" >
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -3,9 +3,4 @@
<color name="colorPrimary">#3F51B5</color> <color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color> <color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color> <color name="colorAccent">#FF4081</color>
<color name="ic_launcher_background">#FFFFFF</color>
<color name="shortcut_red">#E53935</color>
<color name="shortcut_blue">#1E88E5</color>
<color name="shortcut_yellow">#FDD835</color>
<color name="shortcut_gold">#D4AF37</color>
</resources> </resources>
@@ -45,11 +45,6 @@
// own, which can name a different volume on merged / adopted-SD storage. // own, which can name a different volume on merged / adopted-SD storage.
#include "filesystem/Filesystem.h" #include "filesystem/Filesystem.h"
#include "common/Module.h"
#include "audio/Audio.h"
#include "audio/openal/Audio.h"
#include "event/Event.h"
namespace love namespace love
{ {
namespace android namespace android
@@ -283,70 +278,6 @@ bool restartApp()
return result; return result;
} }
bool updateAppShortcuts(const std::vector<std::string> &versions)
{
JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
jclass activity = env->FindClass("org/love2d/android/GameActivity");
if (activity == nullptr)
return false;
jmethodID method = env->GetStaticMethodID(activity, "updateAppShortcuts", "([Ljava/lang/String;)Z");
if (method == nullptr)
{
env->ExceptionClear();
env->DeleteLocalRef(activity);
return false;
}
jclass stringClass = env->FindClass("java/lang/String");
jobjectArray array = env->NewObjectArray((jsize) versions.size(), stringClass, nullptr);
for (size_t i = 0; i < versions.size(); ++i)
{
jstring jstr = env->NewStringUTF(versions[i].c_str());
env->SetObjectArrayElement(array, (jsize) i, jstr);
env->DeleteLocalRef(jstr);
}
jboolean result = env->CallStaticBooleanMethod(activity, method, array);
env->DeleteLocalRef(array);
env->DeleteLocalRef(stringClass);
env->DeleteLocalRef(activity);
return result;
}
std::string getLaunchGame()
{
JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
jclass activity = env->FindClass("org/love2d/android/GameActivity");
if (activity == nullptr)
return "";
jmethodID method = env->GetStaticMethodID(activity, "getLaunchGame", "()Ljava/lang/String;");
if (method == nullptr)
{
env->ExceptionClear();
env->DeleteLocalRef(activity);
return "";
}
jstring jgame = (jstring) env->CallStaticObjectMethod(activity, method);
if (jgame == nullptr)
{
env->DeleteLocalRef(activity);
return "";
}
const char *str = env->GetStringUTFChars(jgame, nullptr);
std::string result = (str != nullptr) ? str : "";
if (str != nullptr)
env->ReleaseStringUTFChars(jgame, str);
env->DeleteLocalRef(jgame);
env->DeleteLocalRef(activity);
return result;
}
bool httpDownload(const char *url, const char *destPath, const char *userAgent, const char *accept) bool httpDownload(const char *url, const char *destPath, const char *userAgent, const char *accept)
{ {
if (url == nullptr || destPath == nullptr) if (url == nullptr || destPath == nullptr)
@@ -443,104 +374,6 @@ bool httpPost(const char *url, const char *body, int bodyLen, const char *conten
return result; return result;
} }
bool httpRequest(const char *url, const char *method,
const char *const *headerPairs, int headerPairCount,
const char *body, int bodyLen, const char *userAgent, std::string &out)
{
out.clear();
if (url == nullptr)
return false;
if (headerPairCount < 0 || (headerPairCount > 0 && headerPairs == nullptr))
return false;
JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
// Same resolution rule as httpDownload: the activity's own class via
// SDL_AndroidGetActivity, never FindClass for an app class -- save sync
// runs on a love.thread worker, whose class loader cannot see them.
jobject activityObj = (jobject) SDL_AndroidGetActivity();
if (activityObj == nullptr)
return false;
jclass activity = env->GetObjectClass(activityObj);
env->DeleteLocalRef(activityObj);
// Old APK / new liblove skew: report "no transport" instead of aborting
// on a missing method (#597).
jmethodID method_id = env->GetStaticMethodID(activity, "httpRequest",
"(Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;[BLjava/lang/String;)[B");
if (method_id == nullptr)
{
env->ExceptionClear();
env->DeleteLocalRef(activity);
return false;
}
jobjectArray jheaders = nullptr;
if (headerPairCount > 0)
{
// java/lang/String, unlike an app class, resolves from any thread.
jclass stringClass = env->FindClass("java/lang/String");
if (stringClass == nullptr)
{
env->ExceptionClear();
env->DeleteLocalRef(activity);
return false;
}
jheaders = env->NewObjectArray((jsize) headerPairCount, stringClass, nullptr);
env->DeleteLocalRef(stringClass);
if (jheaders == nullptr)
{
env->ExceptionClear();
env->DeleteLocalRef(activity);
return false;
}
for (int i = 0; i < headerPairCount; i++)
{
jstring field = env->NewStringUTF(headerPairs[i] != nullptr ? headerPairs[i] : "");
env->SetObjectArrayElement(jheaders, (jsize) i, field);
if (field != nullptr)
env->DeleteLocalRef(field);
}
}
jstring jurl = env->NewStringUTF(url);
jstring jmethod = env->NewStringUTF(method != nullptr ? method : "GET");
// raw bytes across the bridge, as httpPost does: a request body is JSON
// carrying a base64 save, and a jstring would run it through modified UTF-8
jbyteArray jbody = nullptr;
if (body != nullptr && bodyLen >= 0)
{
jbody = env->NewByteArray((jsize) bodyLen);
if (jbody != nullptr && bodyLen > 0)
env->SetByteArrayRegion(jbody, 0, (jsize) bodyLen, (const jbyte*) body);
}
jstring jua = env->NewStringUTF(userAgent != nullptr ? userAgent : "gen1recomp");
jobject result = env->CallStaticObjectMethod(activity, method_id, jurl, jmethod,
jheaders, jbody, jua);
env->DeleteLocalRef(jurl);
env->DeleteLocalRef(jmethod);
if (jheaders != nullptr)
env->DeleteLocalRef(jheaders);
if (jbody != nullptr)
env->DeleteLocalRef(jbody);
env->DeleteLocalRef(jua);
env->DeleteLocalRef(activity);
if (result == nullptr)
return false;
jbyteArray bytes = (jbyteArray) result;
jsize length = env->GetArrayLength(bytes);
if (length > 0)
{
out.resize((size_t) length);
env->GetByteArrayRegion(bytes, 0, length, (jbyte*) &out[0]);
}
env->DeleteLocalRef(result);
return true;
}
/* /*
* TLS sockets. Same resolution rule as httpDownload above -- the activity's * TLS sockets. Same resolution rule as httpDownload above -- the activity's
* own class, never FindClass -- and the same tolerance for an old APK: a * own class, never FindClass -- and the same tolerance for an old APK: a
@@ -1487,98 +1320,4 @@ const char *love_android_poll_secondary_touch()
return event.empty() ? nullptr : event.c_str(); return event.empty() ? nullptr : event.c_str();
} }
static love::audio::openal::Audio *love_android_openal_audio()
{
love::audio::Audio *audio = love::Module::getInstance<love::audio::Audio>(love::Module::M_AUDIO);
if (audio == nullptr)
return nullptr;
const char *name = audio->getName();
if (name == nullptr || strcmp(name, "love.audio.openal") != 0)
return nullptr;
return (love::audio::openal::Audio *) audio;
}
extern "C" JNIEXPORT void JNICALL
Java_org_love2d_android_GameActivity_nativeAudioFocusLost(JNIEnv *env, jclass cls)
{
(void) env;
(void) cls;
love::audio::openal::pushAudioSuspendEvent();
love::audio::openal::Audio *audio = love_android_openal_audio();
if (audio != nullptr)
audio->pauseContext();
}
extern "C" JNIEXPORT void JNICALL
Java_org_love2d_android_GameActivity_nativeAudioFocusGained(JNIEnv *env, jclass cls)
{
(void) env;
(void) cls;
love::audio::openal::Audio *audio = love_android_openal_audio();
if (audio == nullptr)
return;
audio->resumeContext();
if (!audio->isDeviceConnected())
audio->reopenDevice();
love::audio::openal::pushAudioResetEvent();
}
extern "C" JNIEXPORT void JNICALL
Java_org_love2d_android_GameActivity_nativeAudioDeviceChanged(JNIEnv *env, jclass cls)
{
(void) env;
(void) cls;
love::audio::openal::Audio *audio = love_android_openal_audio();
if (audio == nullptr)
return;
audio->pauseContext();
audio->reopenDevice();
audio->resumeContext();
love::audio::openal::pushAudioResetEvent();
}
static void pushGameIntentEvent(const char *game)
{
auto eventmodule = love::Module::getInstance<love::event::Event>(love::Module::M_EVENT);
if (eventmodule == nullptr || game == nullptr)
return;
std::vector<love::Variant> args;
args.push_back(love::Variant(std::string(game)));
love::event::Message *msg = new love::event::Message("intent_game", args);
eventmodule->push(msg);
msg->release();
}
extern "C" JNIEXPORT void JNICALL
Java_org_love2d_android_GameActivity_nativeOnGameIntent(JNIEnv *env, jclass cls, jstring game)
{
(void) cls;
if (game == nullptr)
return;
const char *str = env->GetStringUTFChars(game, nullptr);
if (str != nullptr)
{
pushGameIntentEvent(str);
env->ReleaseStringUTFChars(game, str);
}
}
#endif // LOVE_ANDROID #endif // LOVE_ANDROID
@@ -90,16 +90,6 @@ bool syncHealthSteps();
**/ **/
bool restartApp(); bool restartApp();
/**
* Dynamic App Shortcuts: updates Android ShortcutManager with ready game versions.
**/
bool updateAppShortcuts(const std::vector<std::string> &versions);
/**
* Returns the game version requested via initial launch Intent (if any).
**/
std::string getLaunchGame();
/** /**
* Blocking HTTPS GET into destPath (GameActivity.httpDownload). Android has * Blocking HTTPS GET into destPath (GameActivity.httpDownload). Android has
* no curl binary, so this is the transport src/core/HostShell.lua uses there * no curl binary, so this is the transport src/core/HostShell.lua uses there
@@ -116,21 +106,6 @@ bool httpDownload(const char *url, const char *destPath, const char *userAgent,
**/ **/
bool httpPost(const char *url, const char *body, int bodyLen, const char *contentType, const char *userAgent); bool httpPost(const char *url, const char *body, int bodyLen, const char *contentType, const char *userAgent);
/**
* Blocking HTTPS request with a method, headers and a byte body
* (GameActivity.httpRequest). What save sync needs and neither of the two
* above can give it: PUT, per-request auth headers, and the response body of
* a 4xx as well as a 2xx. headerPairs is a flat name, value array of
* headerPairCount entries; body/userAgent may be null. `out` receives the
* Java side's envelope -- a head line of "STATUS <code>" or "ERROR <text>",
* a newline, then the raw response bytes. False means the platform has no
* such bridge at all (an old APK under a newer liblove), which the Lua side
* reports as "update the app" rather than as a failed request.
**/
bool httpRequest(const char *url, const char *method,
const char *const *headerPairs, int headerPairCount,
const char *body, int bodyLen, const char *userAgent, std::string &out);
/** /**
* TLS client sockets (GameActivity.tls*, implemented by TlsSocket.java). * TLS client sockets (GameActivity.tls*, implemented by TlsSocket.java).
* LuaSocket, which is what LOVE ships, does TCP only, so wss:// is otherwise * LuaSocket, which is what LOVE ships, does TCP only, so wss:// is otherwise
@@ -22,7 +22,6 @@
#include "common/delay.h" #include "common/delay.h"
#include "RecordingDevice.h" #include "RecordingDevice.h"
#include "sound/Decoder.h" #include "sound/Decoder.h"
#include "event/Event.h"
#include <cstdlib> #include <cstdlib>
#include <iostream> #include <iostream>
@@ -31,10 +30,6 @@
#include "common/ios.h" #include "common/ios.h"
#endif #endif
#ifndef ALC_CONNECTED
#define ALC_CONNECTED 0x313
#endif
namespace love namespace love
{ {
namespace audio namespace audio
@@ -42,35 +37,9 @@ namespace audio
namespace openal namespace openal
{ {
static const int DISCONNECT_CHECK_INTERVAL = 200; Audio::PoolThread::PoolThread(Pool *pool)
: pool(pool)
static void pushAudioEvent(const char *name)
{
auto eventmodule = Module::getInstance<event::Event>(Module::M_EVENT);
if (eventmodule == nullptr)
return;
event::Message *msg = new event::Message(name);
eventmodule->push(msg);
msg->release();
}
void pushAudioSuspendEvent()
{
pushAudioEvent("audiosuspend");
}
void pushAudioResetEvent()
{
pushAudioEvent("audioreset");
}
Audio::PoolThread::PoolThread(Audio *audio, Pool *pool)
: audio(audio)
, pool(pool)
, finish(false) , finish(false)
, paused(false)
{ {
threadName = "AudioPool"; threadName = "AudioPool";
} }
@@ -82,8 +51,6 @@ Audio::PoolThread::~PoolThread()
void Audio::PoolThread::threadFunction() void Audio::PoolThread::threadFunction()
{ {
int disconnectCheck = 0;
while (true) while (true)
{ {
{ {
@@ -94,23 +61,7 @@ void Audio::PoolThread::threadFunction()
} }
} }
if (paused.load())
{
disconnectCheck = 0;
sleep(5);
continue;
}
pool->update(); pool->update();
if (audio != nullptr && ++disconnectCheck >= DISCONNECT_CHECK_INTERVAL)
{
disconnectCheck = 0;
if (!audio->isDeviceConnected() && audio->reopenDevice())
pushAudioResetEvent();
}
sleep(5); sleep(5);
} }
} }
@@ -121,11 +72,6 @@ void Audio::PoolThread::setFinish()
finish = true; finish = true;
} }
void Audio::PoolThread::setPaused(bool paused)
{
this->paused.store(paused);
}
ALenum Audio::getFormat(int bitDepth, int channels) ALenum Audio::getFormat(int bitDepth, int channels)
{ {
if (bitDepth != 8 && bitDepth != 16) if (bitDepth != 8 && bitDepth != 16)
@@ -153,8 +99,6 @@ Audio::Audio()
, pool(nullptr) , pool(nullptr)
, poolThread(nullptr) , poolThread(nullptr)
, distanceModel(DISTANCE_INVERSE_CLAMPED) , distanceModel(DISTANCE_INVERSE_CLAMPED)
, alcReopenDeviceSOFT(nullptr)
, reopenChecked(false)
{ {
// Before opening new device, check if recording // Before opening new device, check if recording
// is requested. // is requested.
@@ -245,6 +189,13 @@ Audio::Audio()
throw; throw;
} }
poolThread = new PoolThread(pool);
poolThread->start();
#ifdef LOVE_IOS
love::ios::initAudioSessionInterruptionHandler();
#endif
#ifdef LOVE_ANDROID #ifdef LOVE_ANDROID
bool hasPauseDeviceExt = alcIsExtensionPresent(device, "ALC_SOFT_pause_device") == ALC_TRUE; bool hasPauseDeviceExt = alcIsExtensionPresent(device, "ALC_SOFT_pause_device") == ALC_TRUE;
alcDevicePauseSOFT = hasPauseDeviceExt alcDevicePauseSOFT = hasPauseDeviceExt
@@ -254,13 +205,6 @@ Audio::Audio()
? (LPALCDEVICERESUMESOFT) alcGetProcAddress(device, "alcDeviceResumeSOFT") ? (LPALCDEVICERESUMESOFT) alcGetProcAddress(device, "alcDeviceResumeSOFT")
: nullptr; : nullptr;
#endif #endif
poolThread = new PoolThread(this, pool);
poolThread->start();
#ifdef LOVE_IOS
love::ios::initAudioSessionInterruptionHandler();
#endif
} }
Audio::~Audio() Audio::~Audio()
@@ -370,9 +314,6 @@ std::vector<love::audio::Source*> Audio::pause()
void Audio::pauseContext() void Audio::pauseContext()
{ {
if (poolThread != nullptr)
poolThread->setPaused(true);
#ifdef LOVE_ANDROID #ifdef LOVE_ANDROID
if (alcDevicePauseSOFT) if (alcDevicePauseSOFT)
alcDevicePauseSOFT(device); alcDevicePauseSOFT(device);
@@ -409,52 +350,6 @@ void Audio::resumeContext()
if (context && alcGetCurrentContext() != context) if (context && alcGetCurrentContext() != context)
alcMakeContextCurrent(context); alcMakeContextCurrent(context);
#endif #endif
if (poolThread != nullptr)
poolThread->setPaused(false);
}
bool Audio::reopenDevice()
{
if (device == nullptr)
return false;
thread::Lock lock(deviceMutex);
if (!reopenChecked)
{
reopenChecked = true;
if (alcIsExtensionPresent(device, "ALC_SOFT_reopen_device") == ALC_TRUE)
alcReopenDeviceSOFT = (LPALCREOPENDEVICESOFT) alcGetProcAddress(device, "alcReopenDeviceSOFT");
}
if (alcReopenDeviceSOFT == nullptr)
return false;
alcGetError(device);
return alcReopenDeviceSOFT(device, nullptr, nullptr) == ALC_TRUE;
}
bool Audio::isDeviceConnected()
{
if (device == nullptr)
return false;
thread::Lock lock(deviceMutex);
if (alcIsExtensionPresent(device, "ALC_EXT_disconnect") != ALC_TRUE)
return true;
ALCint connected = 1;
alcGetError(device);
alcGetIntegerv(device, ALC_CONNECTED, 1, &connected);
if (alcGetError(device) != ALC_NO_ERROR)
return true;
return connected != 0;
} }
void Audio::setVolume(float volume) void Audio::setVolume(float volume)
@@ -22,7 +22,6 @@
#define LOVE_AUDIO_OPENAL_AUDIO_H #define LOVE_AUDIO_OPENAL_AUDIO_H
// STD // STD
#include <atomic>
#include <queue> #include <queue>
#include <map> #include <map>
#include <vector> #include <vector>
@@ -98,8 +97,6 @@ public:
std::vector<love::audio::Source*> pause(); std::vector<love::audio::Source*> pause();
void pauseContext(); void pauseContext();
void resumeContext(); void resumeContext();
bool reopenDevice();
bool isDeviceConnected();
void setVolume(float volume); void setVolume(float volume);
float getVolume() const; float getVolume() const;
@@ -158,7 +155,6 @@ private:
class PoolThread: public thread::Threadable class PoolThread: public thread::Threadable
{ {
protected: protected:
Audio *audio;
Pool *pool; Pool *pool;
// Set this to true when the thread should finish. // Set this to true when the thread should finish.
@@ -166,16 +162,13 @@ private:
// will read from it. // will read from it.
volatile bool finish; volatile bool finish;
std::atomic<bool> paused;
// finish lock // finish lock
love::thread::MutexRef mutex; love::thread::MutexRef mutex;
public: public:
PoolThread(Audio *audio, Pool *pool); PoolThread(Pool *pool);
virtual ~PoolThread(); virtual ~PoolThread();
void setFinish(); void setFinish();
void setPaused(bool paused);
void threadFunction(); void threadFunction();
}; };
@@ -184,13 +177,6 @@ private:
DistanceModel distanceModel; DistanceModel distanceModel;
//float metersPerUnit = 1.0; //float metersPerUnit = 1.0;
#ifndef ALC_SOFT_reopen_device
typedef ALCboolean (ALC_APIENTRY*LPALCREOPENDEVICESOFT)(ALCdevice *device, const ALCchar *deviceName, const ALCint *attribs);
#endif
LPALCREOPENDEVICESOFT alcReopenDeviceSOFT;
bool reopenChecked;
love::thread::MutexRef deviceMutex;
#ifdef LOVE_ANDROID #ifdef LOVE_ANDROID
# ifndef ALC_SOFT_pause_device # ifndef ALC_SOFT_pause_device
typedef void (ALC_APIENTRY*LPALCDEVICEPAUSESOFT)(ALCdevice *device); typedef void (ALC_APIENTRY*LPALCDEVICEPAUSESOFT)(ALCdevice *device);
@@ -202,9 +188,6 @@ private:
#endif #endif
}; // Audio }; // Audio
void pushAudioSuspendEvent();
void pushAudioResetEvent();
#ifdef ALC_EXT_EFX #ifdef ALC_EXT_EFX
// Effect objects // Effect objects
extern LPALGENEFFECTS alGenEffects; extern LPALGENEFFECTS alGenEffects;
@@ -245,25 +245,6 @@ bool System::restartApp() const
#endif #endif
} }
bool System::updateShortcuts(const std::vector<std::string> &versions) const
{
#ifdef LOVE_ANDROID
return love::android::updateAppShortcuts(versions);
#else
LOVE_UNUSED(versions);
return false;
#endif
}
std::string System::getLaunchGame() const
{
#ifdef LOVE_ANDROID
return love::android::getLaunchGame();
#else
return "";
#endif
}
bool System::httpDownload(const char *url, const char *destPath, bool System::httpDownload(const char *url, const char *destPath,
const char *userAgent, const char *accept) const const char *userAgent, const char *accept) const
{ {
@@ -293,27 +274,6 @@ bool System::httpPost(const char *url, const char *body, int bodyLen,
#endif #endif
} }
bool System::httpRequest(const char *url, const char *method,
const char *const *headerPairs, int headerPairCount,
const char *body, int bodyLen, const char *userAgent,
std::string &out) const
{
#ifdef LOVE_ANDROID
return love::android::httpRequest(url, method, headerPairs, headerPairCount,
body, bodyLen, userAgent, out);
#else
LOVE_UNUSED(url);
LOVE_UNUSED(method);
LOVE_UNUSED(headerPairs);
LOVE_UNUSED(headerPairCount);
LOVE_UNUSED(body);
LOVE_UNUSED(bodyLen);
LOVE_UNUSED(userAgent);
out.clear();
return false;
#endif
}
int System::tlsOpen(const char *host, int port) const int System::tlsOpen(const char *host, int port) const
{ {
#ifdef LOVE_ANDROID #ifdef LOVE_ANDROID
@@ -143,9 +143,6 @@ public:
**/ **/
virtual bool restartApp() const; virtual bool restartApp() const;
virtual bool updateShortcuts(const std::vector<std::string> &versions) const;
virtual std::string getLaunchGame() const;
/** /**
* Blocking HTTPS GET into an absolute host path (Android only; false * Blocking HTTPS GET into an absolute host path (Android only; false
* elsewhere). Android has no curl, which is what every other platform * elsewhere). Android has no curl, which is what every other platform
@@ -162,18 +159,6 @@ public:
virtual bool httpPost(const char *url, const char *body, int bodyLen, virtual bool httpPost(const char *url, const char *body, int bodyLen,
const char *contentType = nullptr, const char *userAgent = nullptr) const; const char *contentType = nullptr, const char *userAgent = nullptr) const;
/**
* Blocking HTTPS request with a method, headers and a byte body (Android
* only; false elsewhere). Save sync needs PUT, auth headers and the body
* of a 4xx, none of which the two bridges above can express. headerPairs
* is a flat name, value array; `out` receives the response envelope
* ("STATUS <code>" or "ERROR <text>", a newline, then the raw body).
**/
virtual bool httpRequest(const char *url, const char *method,
const char *const *headerPairs, int headerPairCount,
const char *body, int bodyLen, const char *userAgent,
std::string &out) const;
/** /**
* TLS client sockets (Android only; every call fails elsewhere, where * TLS client sockets (Android only; every call fails elsewhere, where
* LuaSec or another provider is the answer). Non-blocking by contract: * LuaSec or another provider is the answer). Non-blocking by contract:
@@ -22,9 +22,6 @@
#include "wrap_System.h" #include "wrap_System.h"
#include "sdl/System.h" #include "sdl/System.h"
#include <string>
#include <vector>
namespace love namespace love
{ {
namespace system namespace system
@@ -153,57 +150,6 @@ int w_httpPost(lua_State *L)
return 1; return 1;
} }
/*
* love.system.httpRequest(url, method, headers, body, userAgent) -> envelope
*
* `headers` is a flat array of alternating header name and value strings, so
* it maps straight onto the Java bridge's String[] without any parsing here.
* The single return is the response envelope -- a head line of
* "STATUS <code>" or "ERROR <text>", a newline, then the raw body -- or nil
* where the build has no bridge, which src/core/HostShell.lua turns into an
* "update the app" notice rather than a failed request.
*/
int w_httpRequest(lua_State *L)
{
const char *url = luaL_checkstring(L, 1);
const char *method = luaL_optstring(L, 2, "GET");
std::vector<std::string> fields;
if (!lua_isnoneornil(L, 3))
{
luaL_checktype(L, 3, LUA_TTABLE);
size_t count = luax_objlen(L, 3);
for (size_t i = 1; i <= count; i++)
{
lua_rawgeti(L, 3, (int) i);
const char *field = lua_tostring(L, -1);
fields.push_back(field != nullptr ? field : "");
lua_pop(L, 1);
}
}
std::vector<const char *> pairs;
for (size_t i = 0; i < fields.size(); i++)
pairs.push_back(fields[i].c_str());
size_t bodyLen = 0;
const char *body = nullptr;
if (!lua_isnoneornil(L, 4))
body = luaL_checklstring(L, 4, &bodyLen);
const char *ua = luaL_optstring(L, 5, nullptr);
std::string out;
bool ok = instance()->httpRequest(url, method,
pairs.empty() ? nullptr : &pairs[0], (int) pairs.size(),
body, (int) bodyLen, ua, out);
if (!ok)
{
lua_pushnil(L);
return 1;
}
lua_pushlstring(L, out.data(), out.size());
return 1;
}
int w_hasBackgroundMusic(lua_State *L) int w_hasBackgroundMusic(lua_State *L)
{ {
lua_pushboolean(L, instance()->hasBackgroundMusic()); lua_pushboolean(L, instance()->hasBackgroundMusic());
@@ -283,34 +229,6 @@ int w_tlsClose(lua_State *L)
return 0; return 0;
} }
int w_updateShortcuts(lua_State *L)
{
if (!lua_istable(L, 1))
return luaL_error(L, "Expected table of game version strings");
std::vector<std::string> versions;
int len = (int) luax_objlen(L, 1);
for (int i = 1; i <= len; ++i)
{
lua_rawgeti(L, 1, i);
if (lua_isstring(L, -1))
versions.push_back(lua_tostring(L, -1));
lua_pop(L, 1);
}
luax_pushboolean(L, instance()->updateShortcuts(versions));
return 1;
}
int w_getLaunchGame(lua_State *L)
{
std::string game = instance()->getLaunchGame();
if (game.empty())
lua_pushnil(L);
else
luax_pushstring(L, game);
return 1;
}
static const luaL_Reg functions[] = static const luaL_Reg functions[] =
{ {
{ "getOS", w_getOS }, { "getOS", w_getOS },
@@ -325,11 +243,8 @@ static const luaL_Reg functions[] =
{ "createFile", w_createFile }, { "createFile", w_createFile },
{ "syncHealthSteps", w_syncHealthSteps }, { "syncHealthSteps", w_syncHealthSteps },
{ "restartApp", w_restartApp }, { "restartApp", w_restartApp },
{ "updateShortcuts", w_updateShortcuts },
{ "getLaunchGame", w_getLaunchGame },
{ "httpDownload", w_httpDownload }, { "httpDownload", w_httpDownload },
{ "httpPost", w_httpPost }, { "httpPost", w_httpPost },
{ "httpRequest", w_httpRequest },
{ "tlsOpen", w_tlsOpen }, { "tlsOpen", w_tlsOpen },
{ "tlsStatus", w_tlsStatus }, { "tlsStatus", w_tlsStatus },
{ "tlsSend", w_tlsSend }, { "tlsSend", w_tlsSend },
@@ -1,40 +0,0 @@
---
name: Bug report
about: Create a report to help us improve Oboe
title: ''
labels: bug
assignees: ''
---
Android version(s):
Android device(s):
Oboe version:
App name used for testing:
(Please try to reproduce the issue using the OboeTester or an Oboe sample.)
**Short description**
(Please only report one bug per Issue. Do not combine multiple bugs.)
**Steps to reproduce**
**Expected behavior**
**Actual behavior**
**Device**
Please list which devices have this bug.
If device specific, and you are on Linux or a Macintosh, connect the device and please share the result for the following script. This gets properties of the device.
```
for p in \
ro.product.brand ro.product.manufacturer ro.product.model \
ro.product.device ro.product.cpu.abi ro.build.description \
ro.hardware ro.hardware.chipname ro.arch "| grep aaudio";
do echo "$p = $(adb shell getprop $p)"; done
```
**Any additional context**
If applicable, please attach a few seconds of an uncompressed recording of the sound in a WAV or AIFF file.
-36
View File
@@ -1,36 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
#Workaround for https://github.com/dependabot/dependabot-core/issues/6888#issuecomment-1539501116
registries:
maven-google:
type: maven-repository
url: "https://dl.google.com/dl/android/maven2/"
updates:
#Check for updates to Github Actions
- package-ecosystem: "github-actions"
directory: "/" #Location of package manifests
target-branch: "main"
open-pull-requests-limit: 5
labels:
- "dependencies"
- "dependencies/github-actions"
schedule:
interval: "daily"
#Check updates for Gradle dependencies
- package-ecosystem: "gradle"
registries:
- maven-google
directory: "/" #Location of package manifests
target-branch: "main"
open-pull-requests-limit: 10
labels:
- "dependencies"
- "dependencies/gradle"
schedule:
interval: "daily"
@@ -1,38 +0,0 @@
name: Build CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 17
- name: build samples and apps
uses: github/codeql-action/init@v3
with:
languages: cpp
- run: |
pushd samples
chmod +x gradlew
./gradlew -q clean bundleDebug
popd
pushd apps/OboeTester
chmod +x gradlew
./gradlew -q clean bundleDebug
popd
pushd apps/fxlab
chmod +x gradlew
./gradlew -q clean bundleDebug
popd
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
@@ -1,24 +0,0 @@
name: Update Docs
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Doxygen Action
uses: mattnotmitt/doxygen-action@v1.9.8
with:
doxyfile-path: "./Doxyfile"
working-directory: "."
- name: Deploy
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs/reference
@@ -4,5 +4,3 @@
.cxx/ .cxx/
.idea .idea
build build
.logpile
+17 -14
View File
@@ -1,21 +1,32 @@
LOCAL_PATH:= $(call my-dir) LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS) include $(CLEAR_VARS)
#
# Module
#
LOCAL_MODULE := oboe LOCAL_MODULE := oboe
LOCAL_ARM_NEON := true LOCAL_ARM_NEON := true
#
# Flags
#
LOCAL_CFLAGS := -Wall -Wextra-semi -Wshadow -Wshadow-field LOCAL_CFLAGS := -Wall -Wextra-semi -Wshadow -Wshadow-field
LOCAL_CPPFLAGS := -std=c++17 LOCAL_CPPFLAGS := -std=c++14
#
# Include paths
#
LOCAL_C_INCLUDES := \ LOCAL_C_INCLUDES := \
$(LOCAL_PATH)/include \ $(LOCAL_PATH)/include \
$(LOCAL_PATH)/src $(LOCAL_PATH)/src
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
#
# Source files
#
LOCAL_SRC_FILES := \ LOCAL_SRC_FILES := \
src/aaudio/AAudioLoader.cpp \ src/aaudio/AAudioLoader.cpp \
src/aaudio/AudioStreamAAudio.cpp \ src/aaudio/AudioStreamAAudio.cpp \
src/common/AdpfWrapper.cpp \
src/common/AudioSourceCaller.cpp \ src/common/AudioSourceCaller.cpp \
src/common/AudioStream.cpp \ src/common/AudioStream.cpp \
src/common/AudioStreamBuilder.cpp \ src/common/AudioStreamBuilder.cpp \
@@ -25,11 +36,8 @@ LOCAL_SRC_FILES := \
src/common/FixedBlockReader.cpp \ src/common/FixedBlockReader.cpp \
src/common/FixedBlockWriter.cpp \ src/common/FixedBlockWriter.cpp \
src/common/LatencyTuner.cpp \ src/common/LatencyTuner.cpp \
src/common/OboeExtensions.cpp \
src/common/SourceFloatCaller.cpp \ src/common/SourceFloatCaller.cpp \
src/common/SourceI16Caller.cpp \ src/common/SourceI16Caller.cpp \
src/common/SourceI24Caller.cpp \
src/common/SourceI32Caller.cpp \
src/common/Utilities.cpp \ src/common/Utilities.cpp \
src/common/QuirksManager.cpp \ src/common/QuirksManager.cpp \
src/fifo/FifoBuffer.cpp \ src/fifo/FifoBuffer.cpp \
@@ -37,26 +45,17 @@ LOCAL_SRC_FILES := \
src/fifo/FifoControllerBase.cpp \ src/fifo/FifoControllerBase.cpp \
src/fifo/FifoControllerIndirect.cpp \ src/fifo/FifoControllerIndirect.cpp \
src/flowgraph/FlowGraphNode.cpp \ src/flowgraph/FlowGraphNode.cpp \
src/flowgraph/ChannelCountConverter.cpp \
src/flowgraph/ClipToRange.cpp \ src/flowgraph/ClipToRange.cpp \
src/flowgraph/Limiter.cpp \
src/flowgraph/ManyToMultiConverter.cpp \ src/flowgraph/ManyToMultiConverter.cpp \
src/flowgraph/MonoBlend.cpp \
src/flowgraph/MonoToMultiConverter.cpp \ src/flowgraph/MonoToMultiConverter.cpp \
src/flowgraph/MultiToManyConverter.cpp \
src/flowgraph/MultiToMonoConverter.cpp \
src/flowgraph/RampLinear.cpp \ src/flowgraph/RampLinear.cpp \
src/flowgraph/SampleRateConverter.cpp \ src/flowgraph/SampleRateConverter.cpp \
src/flowgraph/SinkFloat.cpp \ src/flowgraph/SinkFloat.cpp \
src/flowgraph/SinkI16.cpp \ src/flowgraph/SinkI16.cpp \
src/flowgraph/SinkI24.cpp \ src/flowgraph/SinkI24.cpp \
src/flowgraph/SinkI32.cpp \
src/flowgraph/SinkI8_24.cpp \
src/flowgraph/SourceFloat.cpp \ src/flowgraph/SourceFloat.cpp \
src/flowgraph/SourceI16.cpp \ src/flowgraph/SourceI16.cpp \
src/flowgraph/SourceI24.cpp \ src/flowgraph/SourceI24.cpp \
src/flowgraph/SourceI32.cpp \
src/flowgraph/SourceI8_24.cpp \
src/flowgraph/resampler/IntegerRatio.cpp \ src/flowgraph/resampler/IntegerRatio.cpp \
src/flowgraph/resampler/LinearResampler.cpp \ src/flowgraph/resampler/LinearResampler.cpp \
src/flowgraph/resampler/MultiChannelResampler.cpp \ src/flowgraph/resampler/MultiChannelResampler.cpp \
@@ -76,6 +75,10 @@ LOCAL_SRC_FILES := \
src/common/Trace.cpp \ src/common/Trace.cpp \
src/common/Version.cpp src/common/Version.cpp
#
# Libraries related
#
LOCAL_LDLIBS := -llog LOCAL_LDLIBS := -llog
# Build
include $(BUILD_STATIC_LIBRARY) include $(BUILD_STATIC_LIBRARY)
@@ -9,7 +9,6 @@ project(oboe)
set (oboe_sources set (oboe_sources
src/aaudio/AAudioLoader.cpp src/aaudio/AAudioLoader.cpp
src/aaudio/AudioStreamAAudio.cpp src/aaudio/AudioStreamAAudio.cpp
src/common/AdpfWrapper.cpp
src/common/AudioSourceCaller.cpp src/common/AudioSourceCaller.cpp
src/common/AudioStream.cpp src/common/AudioStream.cpp
src/common/AudioStreamBuilder.cpp src/common/AudioStreamBuilder.cpp
@@ -19,11 +18,8 @@ set (oboe_sources
src/common/FixedBlockReader.cpp src/common/FixedBlockReader.cpp
src/common/FixedBlockWriter.cpp src/common/FixedBlockWriter.cpp
src/common/LatencyTuner.cpp src/common/LatencyTuner.cpp
src/common/OboeExtensions.cpp
src/common/SourceFloatCaller.cpp src/common/SourceFloatCaller.cpp
src/common/SourceI16Caller.cpp src/common/SourceI16Caller.cpp
src/common/SourceI24Caller.cpp
src/common/SourceI32Caller.cpp
src/common/Utilities.cpp src/common/Utilities.cpp
src/common/QuirksManager.cpp src/common/QuirksManager.cpp
src/fifo/FifoBuffer.cpp src/fifo/FifoBuffer.cpp
@@ -31,26 +27,17 @@ set (oboe_sources
src/fifo/FifoControllerBase.cpp src/fifo/FifoControllerBase.cpp
src/fifo/FifoControllerIndirect.cpp src/fifo/FifoControllerIndirect.cpp
src/flowgraph/FlowGraphNode.cpp src/flowgraph/FlowGraphNode.cpp
src/flowgraph/ChannelCountConverter.cpp
src/flowgraph/ClipToRange.cpp src/flowgraph/ClipToRange.cpp
src/flowgraph/Limiter.cpp
src/flowgraph/ManyToMultiConverter.cpp src/flowgraph/ManyToMultiConverter.cpp
src/flowgraph/MonoBlend.cpp
src/flowgraph/MonoToMultiConverter.cpp src/flowgraph/MonoToMultiConverter.cpp
src/flowgraph/MultiToManyConverter.cpp
src/flowgraph/MultiToMonoConverter.cpp
src/flowgraph/RampLinear.cpp src/flowgraph/RampLinear.cpp
src/flowgraph/SampleRateConverter.cpp src/flowgraph/SampleRateConverter.cpp
src/flowgraph/SinkFloat.cpp src/flowgraph/SinkFloat.cpp
src/flowgraph/SinkI16.cpp src/flowgraph/SinkI16.cpp
src/flowgraph/SinkI24.cpp src/flowgraph/SinkI24.cpp
src/flowgraph/SinkI32.cpp
src/flowgraph/SinkI8_24.cpp
src/flowgraph/SourceFloat.cpp src/flowgraph/SourceFloat.cpp
src/flowgraph/SourceI16.cpp src/flowgraph/SourceI16.cpp
src/flowgraph/SourceI24.cpp src/flowgraph/SourceI24.cpp
src/flowgraph/SourceI32.cpp
src/flowgraph/SourceI8_24.cpp
src/flowgraph/resampler/IntegerRatio.cpp src/flowgraph/resampler/IntegerRatio.cpp
src/flowgraph/resampler/LinearResampler.cpp src/flowgraph/resampler/LinearResampler.cpp
src/flowgraph/resampler/MultiChannelResampler.cpp src/flowgraph/resampler/MultiChannelResampler.cpp
@@ -83,23 +70,18 @@ target_include_directories(oboe
# Enable -Ofast # Enable -Ofast
target_compile_options(oboe target_compile_options(oboe
PRIVATE PRIVATE
-std=c++17 -std=c++14
-Wall -Wall
-Wextra-semi -Wextra-semi
-Wshadow -Wshadow
-Wshadow-field -Wshadow-field
"$<$<CONFIG:RELEASE>:-Ofast>" -Ofast
"$<$<CONFIG:DEBUG>:-O3>"
"$<$<CONFIG:DEBUG>:-Werror>") "$<$<CONFIG:DEBUG>:-Werror>")
# Enable logging of D,V for debug builds # Enable logging of D,V for debug builds
target_compile_definitions(oboe PUBLIC $<$<CONFIG:DEBUG>:OBOE_ENABLE_LOGGING=1>) target_compile_definitions(oboe PUBLIC $<$<CONFIG:DEBUG>:OBOE_ENABLE_LOGGING=1>)
option(OBOE_DO_NOT_DEFINE_OPENSL_ES_CONSTANTS "Do not define OpenSLES constants" OFF)
target_compile_definitions(oboe PRIVATE $<$<BOOL:${OBOE_DO_NOT_DEFINE_OPENSL_ES_CONSTANTS}>:DO_NOT_DEFINE_OPENSL_ES_CONSTANTS=1>)
target_link_libraries(oboe PRIVATE log OpenSLES) target_link_libraries(oboe PRIVATE log OpenSLES)
target_link_options(oboe PRIVATE "-Wl,-z,max-page-size=16384")
# When installing oboe put the libraries in the lib/<ABI> folder e.g. lib/arm64-v8a # When installing oboe put the libraries in the lib/<ABI> folder e.g. lib/arm64-v8a
install(TARGETS oboe install(TARGETS oboe
@@ -0,0 +1 @@
Please see the CONTRIBUTING.md file for more information.
+2 -2
View File
@@ -38,7 +38,7 @@ PROJECT_NAME = "Oboe"
# could be handy for archiving the generated documentation or if some version # could be handy for archiving the generated documentation or if some version
# control system is used. # control system is used.
PROJECT_NUMBER = PROJECT_NUMBER = 1.2
# Using the PROJECT_BRIEF tag one can provide an optional one line description # Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a # for a project that appears at the top of each page and should give viewer a
@@ -58,7 +58,7 @@ PROJECT_LOGO =
# entered, it will be relative to the location where doxygen was started. If # entered, it will be relative to the location where doxygen was started. If
# left blank the current directory will be used. # left blank the current directory will be used.
OUTPUT_DIRECTORY = ./docs OUTPUT_DIRECTORY = docs
# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub- # If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub-
# directories (in 2 levels) under the output directory of each output format and # directories (in 2 levels) under the output directory of each output format and
+1
View File
@@ -0,0 +1 @@
Please see the README.md file for more information.
+13 -14
View File
@@ -1,4 +1,4 @@
# Oboe [![Build CI](https://github.com/google/oboe/workflows/Build%20CI/badge.svg)](https://github.com/google/oboe/actions) # Oboe [![Build Status](https://travis-ci.org/google/oboe.svg?branch=master)](https://travis-ci.org/google/oboe)
[![Introduction to Oboe video](docs/images/getting-started-video.jpg)](https://www.youtube.com/watch?v=csfHAbr5ilI&list=PLWz5rJ2EKKc_duWv9IPNvx9YBudNMmLSa) [![Introduction to Oboe video](docs/images/getting-started-video.jpg)](https://www.youtube.com/watch?v=csfHAbr5ilI&list=PLWz5rJ2EKKc_duWv9IPNvx9YBudNMmLSa)
@@ -9,36 +9,35 @@ Oboe is a C++ library which makes it easy to build high-performance audio apps o
- Chooses the audio API (OpenSL ES on API 16+ or AAudio on API 27+) which will give the best audio performance on the target Android device - Chooses the audio API (OpenSL ES on API 16+ or AAudio on API 27+) which will give the best audio performance on the target Android device
- Automatic latency tuning - Automatic latency tuning
- Modern C++ allowing you to write clean, elegant code - Modern C++ allowing you to write clean, elegant code
- Workarounds for some known issues - [Used by popular apps and frameworks](docs/AppsUsingOboe.md)
- [Used by popular apps and frameworks](https://github.com/google/oboe/wiki/AppsUsingOboe)
## Documentation ## Requirements
To build Oboe you'll need a compiler which supports C++14 and the Android header files. The easiest way to obtain these is by downloading the Android NDK r17 or above. It can be installed using Android Studio's SDK manager, or via [direct download](https://developer.android.com/ndk/downloads/).
## API Documentation
- [Getting Started Guide](docs/GettingStarted.md) - [Getting Started Guide](docs/GettingStarted.md)
- [Full Guide to Oboe](docs/FullGuide.md) - [Full Guide to Oboe](docs/FullGuide.md)
- [API reference](https://google.github.io/oboe) - [API reference](https://google.github.io/oboe/reference)
- [Tech Notes](docs/notes/)
- [History of Audio features/bugs by Android version](docs/AndroidAudioHistory.md) - [History of Audio features/bugs by Android version](docs/AndroidAudioHistory.md)
- [Migration guide for apps using OpenSL ES](docs/OpenSLESMigration.md)
- [Frequently Asked Questions](docs/FAQ.md) (FAQ) - [Frequently Asked Questions](docs/FAQ.md) (FAQ)
- [Wiki](https://github.com/google/oboe/wiki)
- [Our roadmap](https://github.com/google/oboe/milestones) - Vote on a feature/issue by adding a thumbs up to the first comment. - [Our roadmap](https://github.com/google/oboe/milestones) - Vote on a feature/issue by adding a thumbs up to the first comment.
### Community
- Reddit: [r/androidaudiodev](https://www.reddit.com/r/androidaudiodev/)
- StackOverflow: [#oboe](https://stackoverflow.com/questions/tagged/oboe)
## Testing ## Testing
- [**OboeTester** app for measuring latency, glitches, etc.](apps/OboeTester/docs) - [**OboeTester** app for measuring latency, glitches, etc.](https://github.com/google/oboe/tree/master/apps/OboeTester/docs)
- [Oboe unit tests](tests) - [Oboe unit tests](https://github.com/google/oboe/tree/master/tests)
## Videos ## Videos
- [Getting started with Oboe](https://www.youtube.com/playlist?list=PLWz5rJ2EKKc_duWv9IPNvx9YBudNMmLSa) - [Getting started with Oboe](https://www.youtube.com/playlist?list=PLWz5rJ2EKKc_duWv9IPNvx9YBudNMmLSa)
- [Low Latency Audio - Because Your Ears Are Worth It](https://www.youtube.com/watch?v=8vOf_fDtur4) (Android Dev Summit '18) - [Low Latency Audio - Because Your Ears Are Worth It](https://www.youtube.com/watch?v=8vOf_fDtur4) (Android Dev Summit '18)
- [Real-time audio with the 100 oscillator synthesizer](https://www.youtube.com/watch?v=J04iPJBkAKs) (DroidCon Berlin '18)
- [Winning on Android](https://www.youtube.com/watch?v=tWBojmBpS74) - How to optimize an Android audio app. (ADC '18) - [Winning on Android](https://www.youtube.com/watch?v=tWBojmBpS74) - How to optimize an Android audio app. (ADC '18)
- [Real-Time Processing on Android](https://youtu.be/hY9BrS2uX-c) (ADC '19)
## Sample code and apps ## Sample code and apps
- Sample apps can be found in the [samples directory](samples). - Sample apps can be found in the [samples directory](samples).
- A complete "effects processor" app called FXLab can be found in the [apps/fxlab folder](apps/fxlab). - A complete "effects processor" app called FXLab can be found in the [apps/fxlab folder](apps/fxlab).
- Also check out the [Rhythm Game codelab](https://developer.android.com/codelabs/musicalgame-using-oboe?hl=en#0). - Also check out the [Rhythm Game codelab](https://codelabs.developers.google.com/codelabs/musicalgame-using-oboe/index.html#0).
### Third party sample code ### Third party sample code
- [Ableton Link integration demo](https://github.com/jbloit/AndroidLinkAudio) (author: jbloit) - [Ableton Link integration demo](https://github.com/jbloit/AndroidLinkAudio) (author: jbloit)
@@ -6,8 +6,6 @@
/build/ /build/
.idea/ .idea/
/app/build/ /app/build/
/app/release/
/app/debug/
/app/app.iml /app/app.iml
*.iml *.iml
/app/externalNativeBuild/ /app/externalNativeBuild/
@@ -1,14 +1,11 @@
cmake_minimum_required(VERSION 3.4.1) cmake_minimum_required(VERSION 3.4.1)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror -Wall -std=c++17 -fvisibility=hidden") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror -Wall -std=c++14")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O2") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O2")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3") set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3")
link_directories(${CMAKE_CURRENT_LIST_DIR}/..) link_directories(${CMAKE_CURRENT_LIST_DIR}/..)
# Increment this number when adding files to OboeTester => 105
# The change in this file will help Android Studio resync
# and generate new build files that reference the new code.
file(GLOB_RECURSE app_native_sources src/main/cpp/*) file(GLOB_RECURSE app_native_sources src/main/cpp/*)
### Name must match loadLibrary() call in MainActivity.java ### Name must match loadLibrary() call in MainActivity.java
@@ -33,4 +30,5 @@ include_directories(
# link to oboe # link to oboe
target_link_libraries(oboetester log oboe atomic) target_link_libraries(oboetester log oboe atomic)
target_link_options(oboetester PRIVATE "-Wl,-z,max-page-size=16384")
# bump 2 to resync CMake
@@ -1,17 +1,18 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
android { android {
compileSdkVersion 34 compileSdkVersion 28
defaultConfig { defaultConfig {
applicationId = "com.mobileer.oboetester" applicationId = "com.google.sample.oboe.manualtest"
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 34 targetSdkVersion 28
versionCode 91 // Also update the version in the AndroidManifest.xml file.
versionName "2.7.2" versionCode 32
versionName "1.5.24"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild { externalNativeBuild {
cmake { cmake {
cppFlags "-std=c++17" cppFlags "-std=c++14"
abiFilters "x86", "x86_64", "armeabi-v7a", "arm64-v8a" abiFilters "x86", "x86_64", "armeabi-v7a", "arm64-v8a"
} }
} }
@@ -30,15 +31,14 @@ android {
path "CMakeLists.txt" path "CMakeLists.txt"
} }
} }
namespace 'com.mobileer.oboetester'
} }
dependencies { dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs') implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "androidx.core:core-ktx:1.9.0" implementation 'com.android.support.constraint:constraint-layout:2.0.0-beta4'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.appcompat:appcompat:1.6.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.5' testImplementation 'junit:junit:4.13-beta-3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' implementation 'com.android.support:appcompat-v7:28.0.0'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
} }
@@ -1,138 +1,100 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
<uses-feature package="com.google.sample.oboe.manualtest"
android:name="android.hardware.microphone" android:versionCode="32"
android:required="false" /> android:versionName="1.5.24">
<uses-feature <!-- versionCode and versionName also have to be updated in build.gradle -->
android:name="android.hardware.audio.output"
android:required="true" /> <uses-feature android:name="android.hardware.microphone" android:required="true" />
<uses-feature <uses-feature android:name="android.hardware.audio.output" android:required="true" />
android:name="android.hardware.touchscreen" <uses-feature android:name="android.software.midi" android:required="true" />
android:required="false" />
<uses-feature
android:name="android.software.midi"
android:required="false" />
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<!-- debug-writing file need external storage writing -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<application <application
android:allowBackup="false"
android:fullBackupContent="false"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme" android:theme="@style/AppTheme">
android:requestLegacyExternalStorage="true"
android:banner="@mipmap/ic_launcher">
<activity <activity
android:name=".MainActivity" android:name="com.google.sample.oboe.manualtest.MainActivity"
android:launchMode="singleTask" android:launchMode="singleTask"
android:screenOrientation="portrait" android:label="@string/app_name"
android:exported="true"> android:screenOrientation="portrait">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".TestOutputActivity"
android:label="@string/title_activity_test_output"
android:screenOrientation="portrait" />
<activity
android:name=".TestInputActivity"
android:label="@string/title_activity_test_input"
android:screenOrientation="portrait" />
<activity
android:name=".TapToToneActivity"
android:label="@string/title_activity_output_latency"
android:screenOrientation="portrait" />
<activity
android:name=".RecorderActivity"
android:label="@string/title_activity_recorder"
android:screenOrientation="portrait" />
<activity
android:name=".EchoActivity"
android:label="@string/title_activity_echo"
android:screenOrientation="portrait" />
<activity
android:name=".RoundTripLatencyActivity"
android:label="@string/title_activity_rt_latency"
android:screenOrientation="portrait" />
<activity
android:name=".ManualGlitchActivity"
android:label="@string/title_activity_glitches"
android:screenOrientation="portrait" />
<activity
android:name=".AutomatedGlitchActivity"
android:label="@string/title_activity_auto_glitches"
android:screenOrientation="portrait" />
<activity
android:name=".TestDisconnectActivity"
android:label="@string/title_test_disconnect"
android:screenOrientation="portrait" />
<activity
android:name=".DeviceReportActivity"
android:label="@string/title_report_devices"
android:screenOrientation="portrait" />
<activity
android:name=".TestDataPathsActivity"
android:label="@string/title_data_paths"
android:screenOrientation="portrait" />
<activity
android:name=".ExtraTestsActivity"
android:exported="true"
android:label="@string/title_extra_tests"
android:screenOrientation="portrait" />
<activity <activity
android:name=".ExternalTapToToneActivity" android:name="com.google.sample.oboe.manualtest.TestOutputActivity"
android:label="@string/title_external_tap" android:label="@string/title_activity_test_output"
android:exported="true" android:screenOrientation="portrait">
android:screenOrientation="portrait" /> </activity>
<activity <activity
android:name=".TestPlugLatencyActivity" android:name="com.google.sample.oboe.manualtest.TestInputActivity"
android:label="@string/title_plug_latency" android:label="@string/title_activity_test_input"
android:exported="true" android:screenOrientation="portrait">
android:screenOrientation="portrait" /> </activity>
<activity <activity
android:name=".TestErrorCallbackActivity" android:name="com.google.sample.oboe.manualtest.TapToToneActivity"
android:label="@string/title_error_callback" android:label="@string/title_activity_output_latency"
android:exported="true" android:screenOrientation="portrait">
android:screenOrientation="portrait" /> </activity>
<activity <activity
android:name=".TestRouteDuringCallbackActivity" android:name="com.google.sample.oboe.manualtest.RecorderActivity"
android:label="@string/title_route_during_callback" android:label="@string/title_activity_recorder"
android:exported="true" android:screenOrientation="portrait">
android:screenOrientation="portrait" /> </activity>
<activity <activity
android:name=".DynamicWorkloadActivity" android:name="com.google.sample.oboe.manualtest.EchoActivity"
android:label="@string/title_dynamic_load" android:label="@string/title_activity_echo"
android:exported="true" android:screenOrientation="portrait">
android:screenOrientation="portrait" /> </activity>
<activity <activity
android:name=".TestColdStartLatencyActivity" android:name="com.google.sample.oboe.manualtest.RoundTripLatencyActivity"
android:label="@string/title_cold_start_latency" android:label="@string/title_activity_rt_latency"
android:exported="true" android:screenOrientation="portrait">
android:screenOrientation="portrait" /> </activity>
<activity <activity
android:name=".TestRapidCycleActivity" android:name="com.google.sample.oboe.manualtest.ManualGlitchActivity"
android:label="@string/title_rapid_cycle" android:label="@string/title_activity_glitches"
android:exported="true" android:screenOrientation="portrait">
android:screenOrientation="portrait" /> </activity>
<activity
android:name="com.google.sample.oboe.manualtest.AutoGlitchActivity"
android:label="@string/title_activity_glitches"
android:screenOrientation="portrait">
</activity>
<activity
android:name="com.google.sample.oboe.manualtest.TestDisconnectActivity"
android:label="@string/title_test_disconnect"
android:screenOrientation="portrait">
</activity>
<service <service
android:name=".MidiTapTester" android:name="com.google.sample.oboe.manualtest.AudioMidiTester"
android:permission="android.permission.BIND_MIDI_DEVICE_SERVICE" android:permission="android.permission.BIND_MIDI_DEVICE_SERVICE">
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.media.midi.MidiDeviceService" /> <action android:name="android.media.midi.MidiDeviceService" />
</intent-filter> </intent-filter>
@@ -142,21 +104,16 @@
android:resource="@xml/service_device_info" /> android:resource="@xml/service_device_info" />
</service> </service>
<service
android:name=".AudioForegroundService"
android:foregroundServiceType="mediaPlayback|microphone"
android:exported="false">
</service>
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.provider" android:authorities="${applicationId}.provider"
android:exported="false" android:exported="false"
android:grantUriPermissions="true"> android:grantUriPermissions="true">
<meta-data <meta-data
android:name="android.support.FILE_PROVIDER_PATHS" android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" /> android:resource="@xml/provider_paths"/>
</provider> </provider>
</application> </application>
</manifest> </manifest>
@@ -17,24 +17,29 @@
#include <cstring> #include <cstring>
#include <sched.h> #include <sched.h>
#include "common/OboeDebug.h"
#include "oboe/Oboe.h" #include "oboe/Oboe.h"
#include "AudioStreamGateway.h" #include "AudioStreamGateway.h"
using namespace oboe::flowgraph; using namespace flowgraph;
oboe::DataCallbackResult AudioStreamGateway::onAudioReady( oboe::DataCallbackResult AudioStreamGateway::onAudioReady(
oboe::AudioStream *audioStream, oboe::AudioStream *audioStream,
void *audioData, void *audioData,
int numFrames) { int numFrames) {
maybeHang(getNanoseconds()); if (!mSchedulerChecked) {
printScheduler(); mScheduler = sched_getscheduler(gettid());
mSchedulerChecked = true;
}
if (mAudioSink != nullptr) { if (mAudioSink != nullptr) {
mAudioSink->read(audioData, numFrames); mAudioSink->read(mFramePosition, audioData, numFrames);
mFramePosition += numFrames;
} }
return oboe::DataCallbackResult::Continue; return oboe::DataCallbackResult::Continue;
} }
int AudioStreamGateway::getScheduler() {
return mScheduler;
}
@@ -21,21 +21,24 @@
#include "flowgraph/FlowGraphNode.h" #include "flowgraph/FlowGraphNode.h"
#include "oboe/Oboe.h" #include "oboe/Oboe.h"
#include "OboeTesterStreamCallback.h"
using namespace oboe::flowgraph; using namespace flowgraph;
/** /**
* Bridge between an audio flowgraph and an audio device. * Bridge between an audio flowgraph and an audio device.
* Pass in an AudioSink and then pass * Pass in an AudioSink and then pass
* this object to the AudioStreamBuilder as a callback. * this object to the AudioStreamBuilder as a callback.
*/ */
class AudioStreamGateway : public OboeTesterStreamCallback { class AudioStreamGateway : public oboe::AudioStreamCallback {
public: public:
// AudioStreamGateway(int samplesPerFrame);
virtual ~AudioStreamGateway() = default; virtual ~AudioStreamGateway() = default;
void setAudioSink(std::shared_ptr<oboe::flowgraph::FlowGraphSink> sink) { void setAudioSink(std::shared_ptr<flowgraph::FlowGraphSink> sink) {
mAudioSink = sink; mAudioSink = sink;
if (sink) {
mFramePosition = sink->getLastFramePosition();
}
} }
/** /**
@@ -46,9 +49,13 @@ public:
void *audioData, void *audioData,
int numFrames) override; int numFrames) override;
private: int getScheduler();
std::shared_ptr<oboe::flowgraph::FlowGraphSink> mAudioSink; private:
int64_t mFramePosition = 0;
bool mSchedulerChecked = false;
int mScheduler;
std::shared_ptr<flowgraph::FlowGraphSink> mAudioSink;
}; };
@@ -1,91 +0,0 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "FormatConverterBox.h"
FormatConverterBox::FormatConverterBox(int32_t maxSamples,
oboe::AudioFormat inputFormat,
oboe::AudioFormat outputFormat) {
mInputFormat = inputFormat;
mOutputFormat = outputFormat;
mMaxSamples = maxSamples;
mInputBuffer = std::make_unique<uint8_t[]>(maxSamples * sizeof(int32_t));
mOutputBuffer = std::make_unique<uint8_t[]>(maxSamples * sizeof(int32_t));
mSource.reset();
switch (mInputFormat) {
case oboe::AudioFormat::I16:
case oboe::AudioFormat::IEC61937:
mSource = std::make_unique<oboe::flowgraph::SourceI16>(1);
break;
case oboe::AudioFormat::I24:
mSource = std::make_unique<oboe::flowgraph::SourceI24>(1);
break;
case oboe::AudioFormat::I32:
mSource = std::make_unique<oboe::flowgraph::SourceI32>(1);
break;
case oboe::AudioFormat::Float:
case oboe::AudioFormat::Invalid:
case oboe::AudioFormat::Unspecified:
mSource = std::make_unique<oboe::flowgraph::SourceFloat>(1);
break;
}
mSink.reset();
switch (mOutputFormat) {
case oboe::AudioFormat::I16:
case oboe::AudioFormat::IEC61937:
mSink = std::make_unique<oboe::flowgraph::SinkI16>(1);
break;
case oboe::AudioFormat::I24:
mSink = std::make_unique<oboe::flowgraph::SinkI24>(1);
break;
case oboe::AudioFormat::I32:
mSink = std::make_unique<oboe::flowgraph::SinkI32>(1);
break;
case oboe::AudioFormat::Float:
case oboe::AudioFormat::Invalid:
case oboe::AudioFormat::Unspecified:
mSink = std::make_unique<oboe::flowgraph::SinkFloat>(1);
break;
}
if (mSource && mSink) {
mSource->output.connect(&mSink->input);
mSink->pullReset();
}
}
int32_t FormatConverterBox::convertInternalBuffers(int32_t numSamples) {
assert(numSamples <= mMaxSamples);
return convert(getOutputBuffer(), numSamples, getInputBuffer());
}
int32_t FormatConverterBox::convertToInternalOutput(int32_t numSamples, const void *inputBuffer) {
assert(numSamples <= mMaxSamples);
return convert(getOutputBuffer(), numSamples, inputBuffer);
}
int32_t FormatConverterBox::convertFromInternalInput(void *outputBuffer, int32_t numSamples) {
assert(numSamples <= mMaxSamples);
return convert(outputBuffer, numSamples, getInputBuffer());
}
int32_t FormatConverterBox::convert(void *outputBuffer, int32_t numSamples, const void *inputBuffer) {
mSource->setData(inputBuffer, numSamples);
return mSink->read(outputBuffer, numSamples);
}
@@ -1,102 +0,0 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef OBOETESTER_FORMAT_CONVERTER_BOX_H
#define OBOETESTER_FORMAT_CONVERTER_BOX_H
#include <unistd.h>
#include <sys/types.h>
#include "oboe/Oboe.h"
#include "flowgraph/SinkFloat.h"
#include "flowgraph/SinkI16.h"
#include "flowgraph/SinkI24.h"
#include "flowgraph/SinkI32.h"
#include "flowgraph/SourceFloat.h"
#include "flowgraph/SourceI16.h"
#include "flowgraph/SourceI24.h"
#include "flowgraph/SourceI32.h"
/**
* Use flowgraph modules to convert between the various data formats.
*
* Note that this does not do channel conversions.
*/
class FormatConverterBox {
public:
FormatConverterBox(int32_t maxSamples,
oboe::AudioFormat inputFormat,
oboe::AudioFormat outputFormat);
/**
* @return internal buffer used to store input data
*/
void *getOutputBuffer() {
return (void *) mOutputBuffer.get();
};
/**
* @return internal buffer used to store output data
*/
void *getInputBuffer() {
return (void *) mInputBuffer.get();
};
/** Convert the data from inputFormat to outputFormat
* using both internal buffers.
*/
int32_t convertInternalBuffers(int32_t numSamples);
/**
* Convert data from external buffer into internal output buffer.
* @param numSamples
* @param inputBuffer
* @return
*/
int32_t convertToInternalOutput(int32_t numSamples, const void *inputBuffer);
/**
*
* Convert data from internal input buffer into external output buffer.
* @param outputBuffer
* @param numSamples
* @return
*/
int32_t convertFromInternalInput(void *outputBuffer, int32_t numSamples);
/**
* Convert data formats between the specified external buffers.
* @param outputBuffer
* @param numSamples
* @param inputBuffer
* @return
*/
int32_t convert(void *outputBuffer, int32_t numSamples, const void *inputBuffer);
private:
oboe::AudioFormat mInputFormat{oboe::AudioFormat::Invalid};
oboe::AudioFormat mOutputFormat{oboe::AudioFormat::Invalid};
int32_t mMaxSamples = 0;
std::unique_ptr<uint8_t[]> mInputBuffer;
std::unique_ptr<uint8_t[]> mOutputBuffer;
std::unique_ptr<oboe::flowgraph::FlowGraphSourceBuffered> mSource;
std::unique_ptr<oboe::flowgraph::FlowGraphSink> mSink;
};
#endif //OBOETESTER_FORMAT_CONVERTER_BOX_H
@@ -19,39 +19,28 @@
oboe::Result FullDuplexAnalyzer::start() { oboe::Result FullDuplexAnalyzer::start() {
getLoopbackProcessor()->setSampleRate(getOutputStream()->getSampleRate()); getLoopbackProcessor()->setSampleRate(getOutputStream()->getSampleRate());
getLoopbackProcessor()->prepareToTest(); getLoopbackProcessor()->onStartTest();
mWriteReadDeltaValid = false; return FullDuplexStream::start();
return FullDuplexStreamWithConversion::start();
} }
oboe::DataCallbackResult FullDuplexAnalyzer::onBothStreamsReadyFloat( oboe::DataCallbackResult FullDuplexAnalyzer::onBothStreamsReady(
const float *inputData, const void *inputData,
int numInputFrames, int numInputFrames,
float *outputData, void *outputData,
int numOutputFrames) { int numOutputFrames) {
int32_t inputStride = getInputStream()->getChannelCount(); int32_t inputStride = getInputStream()->getChannelCount();
int32_t outputStride = getOutputStream()->getChannelCount(); int32_t outputStride = getOutputStream()->getChannelCount();
auto *inputFloat = static_cast<const float *>(inputData); float *inputFloat = (float *) inputData;
float *outputFloat = outputData; float *outputFloat = (float *) outputData;
// Get atomic snapshot of the relative frame positions so they
// can be used to calculate timestamp latency.
int64_t framesRead = getInputStream()->getFramesRead();
int64_t framesWritten = getOutputStream()->getFramesWritten();
mWriteReadDelta = framesWritten - framesRead;
mWriteReadDeltaValid = true;
(void) getLoopbackProcessor()->process(inputFloat, inputStride, numInputFrames, (void) getLoopbackProcessor()->process(inputFloat, inputStride, numInputFrames,
outputFloat, outputStride, numOutputFrames); outputFloat, outputStride, numOutputFrames);
// Save data for later analysis or for writing to a WAVE file. // write the first channel of output and input to the stereo recorder
if (mRecording != nullptr) { if (mRecording != nullptr) {
float buffer[2]; float buffer[2];
int numBoth = std::min(numInputFrames, numOutputFrames); int numBoth = std::min(numInputFrames, numOutputFrames);
// Offset to the selected channels that we are analyzing.
inputFloat += getLoopbackProcessor()->getInputChannel();
outputFloat += getLoopbackProcessor()->getOutputChannel();
for (int i = 0; i < numBoth; i++) { for (int i = 0; i < numBoth; i++) {
buffer[0] = *outputFloat; buffer[0] = *outputFloat;
outputFloat += outputStride; outputFloat += outputStride;
@@ -59,15 +48,14 @@ oboe::DataCallbackResult FullDuplexAnalyzer::onBothStreamsReadyFloat(
inputFloat += inputStride; inputFloat += inputStride;
mRecording->write(buffer, 1); mRecording->write(buffer, 1);
} }
// Handle mismatch in numFrames. // Handle mismatch in in numFrames.
const float gapMarker = -0.9f; // Recognizable value so we can tell underruns from DSP gaps. buffer[0] = 0.0f; // gap in output
buffer[0] = gapMarker; // gap in output
for (int i = numBoth; i < numInputFrames; i++) { for (int i = numBoth; i < numInputFrames; i++) {
buffer[1] = *inputFloat; buffer[1] = *inputFloat;
inputFloat += inputStride; inputFloat += inputStride;
mRecording->write(buffer, 1); mRecording->write(buffer, 1);
} }
buffer[1] = gapMarker; // gap in input buffer[1] = 0.0f; // gap in input
for (int i = numBoth; i < numOutputFrames; i++) { for (int i = numBoth; i < numOutputFrames; i++) {
buffer[0] = *outputFloat; buffer[0] = *outputFloat;
outputFloat += outputStride; outputFloat += outputStride;
@@ -21,52 +21,40 @@
#include <sys/types.h> #include <sys/types.h>
#include "oboe/Oboe.h" #include "oboe/Oboe.h"
#include "FullDuplexStream.h"
#include "analyzer/LatencyAnalyzer.h" #include "analyzer/LatencyAnalyzer.h"
#include "FullDuplexStreamWithConversion.h"
#include "MultiChannelRecording.h" #include "MultiChannelRecording.h"
class FullDuplexAnalyzer : public FullDuplexStreamWithConversion { class FullDuplexAnalyzer : public FullDuplexStream {
public: public:
FullDuplexAnalyzer(LoopbackProcessor *processor) FullDuplexAnalyzer() {}
: mLoopbackProcessor(processor) {
}
/** /**
* Called when data is available on both streams. * Called when data is available on both streams.
* Caller should override this method. * Caller should override this method.
*/ */
oboe::DataCallbackResult onBothStreamsReadyFloat( oboe::DataCallbackResult onBothStreamsReady(
const float *inputData, const void *inputData,
int numInputFrames, int numInputFrames,
float *outputData, void *outputData,
int numOutputFrames int numOutputFrames
) override; ) override;
oboe::Result start() override; oboe::Result start() override;
LoopbackProcessor *getLoopbackProcessor() { bool isDone() {
return mLoopbackProcessor; return false;
} }
virtual LoopbackProcessor *getLoopbackProcessor() = 0;
void setRecording(MultiChannelRecording *recording) { void setRecording(MultiChannelRecording *recording) {
mRecording = recording; mRecording = recording;
} }
bool isWriteReadDeltaValid() {
return mWriteReadDeltaValid;
}
int64_t getWriteReadDelta() {
return mWriteReadDelta;
}
private: private:
MultiChannelRecording *mRecording = nullptr; MultiChannelRecording *mRecording = nullptr;
LoopbackProcessor * const mLoopbackProcessor;
std::atomic<bool> mWriteReadDeltaValid{false};
std::atomic<int64_t> mWriteReadDelta{0};
}; };
@@ -20,46 +20,28 @@
oboe::Result FullDuplexEcho::start() { oboe::Result FullDuplexEcho::start() {
int32_t delayFrames = (int32_t) (kMaxDelayTimeSeconds * getOutputStream()->getSampleRate()); int32_t delayFrames = (int32_t) (kMaxDelayTimeSeconds * getOutputStream()->getSampleRate());
mDelayLine = std::make_unique<InterpolatingDelayLine>(delayFrames); mDelayLine = std::make_unique<InterpolatingDelayLine>(delayFrames);
// Use peak detector for input streams return FullDuplexStream::start();
mNumChannels = getInputStream()->getChannelCount();
mPeakDetectors = std::make_unique<PeakDetector[]>(mNumChannels);
return FullDuplexStreamWithConversion::start();
} }
double FullDuplexEcho::getPeakLevel(int index) { oboe::DataCallbackResult FullDuplexEcho::onBothStreamsReady(
if (mPeakDetectors == nullptr) { const void *inputData,
LOGE("%s() called before setup()", __func__);
return -1.0;
} else if (index < 0 || index >= mNumChannels) {
LOGE("%s(), index out of range, 0 <= %d < %d", __func__, index, mNumChannels.load());
return -2.0;
}
return mPeakDetectors[index].getLevel();
}
oboe::DataCallbackResult FullDuplexEcho::onBothStreamsReadyFloat(
const float *inputData,
int numInputFrames, int numInputFrames,
float *outputData, void *outputData,
int numOutputFrames) { int numOutputFrames) {
// FIXME only handles matching stream formats.
// TODO Add delay node
// TODO use flowgraph to handle format conversion
int32_t framesToEcho = std::min(numInputFrames, numOutputFrames); int32_t framesToEcho = std::min(numInputFrames, numOutputFrames);
auto *inputFloat = const_cast<float *>(inputData); float *inputFloat = (float *)inputData;
float *outputFloat = outputData; float *outputFloat = (float *)outputData;
// zero out entire output array // zero out entire output array
memset(outputFloat, 0, static_cast<size_t>(numOutputFrames) memset(outputFloat, 0, numOutputFrames * getOutputStream()->getBytesPerFrame());
* static_cast<size_t>(getOutputStream()->getBytesPerFrame()));
int32_t inputStride = getInputStream()->getChannelCount(); int32_t inputStride = getInputStream()->getChannelCount();
int32_t outputStride = getOutputStream()->getChannelCount(); int32_t outputStride = getOutputStream()->getChannelCount();
float delayFrames = mDelayTimeSeconds * getOutputStream()->getSampleRate(); float delayFrames = mDelayTimeSeconds * getOutputStream()->getSampleRate();
while (framesToEcho-- > 0) { while (framesToEcho-- > 0) {
*outputFloat = mDelayLine->process(delayFrames, *inputFloat); // mono delay *outputFloat = mDelayLine->process(delayFrames, *inputFloat); // mono delay
for (int iChannel = 0; iChannel < inputStride; iChannel++) {
float sample = * (inputFloat + iChannel);
mPeakDetectors[iChannel].process(sample);
}
inputFloat += inputStride; inputFloat += inputStride;
outputFloat += outputStride; outputFloat += outputStride;
} }
@@ -21,31 +21,28 @@
#include <sys/types.h> #include <sys/types.h>
#include "oboe/Oboe.h" #include "oboe/Oboe.h"
#include "analyzer/LatencyAnalyzer.h" #include "FullDuplexStream.h"
#include "FullDuplexStreamWithConversion.h"
#include "InterpolatingDelayLine.h" #include "InterpolatingDelayLine.h"
class FullDuplexEcho : public FullDuplexStreamWithConversion { class FullDuplexEcho : public FullDuplexStream {
public: public:
FullDuplexEcho() { FullDuplexEcho() {
setNumInputBurstsCushion(0); setMNumInputBurstsCushion(0);
} }
/** /**
* Called when data is available on both streams. * Called when data is available on both streams.
* Caller should override this method. * Caller should override this method.
*/ */
oboe::DataCallbackResult onBothStreamsReadyFloat( oboe::DataCallbackResult onBothStreamsReady(
const float *inputData, const void *inputData,
int numInputFrames, int numInputFrames,
float *outputData, void *outputData,
int numOutputFrames int numOutputFrames
) override; ) override;
oboe::Result start() override; oboe::Result start() override;
double getPeakLevel(int index);
void setDelayTime(double delayTimeSeconds) { void setDelayTime(double delayTimeSeconds) {
mDelayTimeSeconds = delayTimeSeconds; mDelayTimeSeconds = delayTimeSeconds;
} }
@@ -54,9 +51,6 @@ private:
std::unique_ptr<InterpolatingDelayLine> mDelayLine; std::unique_ptr<InterpolatingDelayLine> mDelayLine;
static constexpr double kMaxDelayTimeSeconds = 4.0; static constexpr double kMaxDelayTimeSeconds = 4.0;
double mDelayTimeSeconds = kMaxDelayTimeSeconds; double mDelayTimeSeconds = kMaxDelayTimeSeconds;
std::atomic<int32_t> mNumChannels{0};
std::unique_ptr<PeakDetector[]> mPeakDetectors;
}; };
@@ -1,5 +1,5 @@
/* /*
* Copyright 2023 The Android Open Source Project * Copyright 2019 The Android Open Source Project
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -14,12 +14,5 @@
* limitations under the License. * limitations under the License.
*/ */
#ifndef OBOETESTER_OBOETOOLS_H #include "common/OboeDebug.h"
#define OBOETESTER_OBOETOOLS_H #include "FullDuplexGlitches.h"
#define NANOS_PER_MICROSECOND ((int64_t) 1000)
#define NANOS_PER_MILLISECOND (1000 * NANOS_PER_MICROSECOND)
#define NANOS_PER_SECOND (1000 * NANOS_PER_MILLISECOND)
#define MILLISECONDS_PER_SECOND 1000
#endif //OBOETESTER_OBOETOOLS_H
@@ -0,0 +1,53 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef OBOETESTER_FULL_DUPLEX_GLITCHES_H
#define OBOETESTER_FULL_DUPLEX_GLITCHES_H
#include <unistd.h>
#include <sys/types.h>
#include "oboe/Oboe.h"
#include "FullDuplexAnalyzer.h"
#include "analyzer/GlitchAnalyzer.h"
class FullDuplexGlitches : public FullDuplexAnalyzer {
public:
FullDuplexGlitches() {
setMNumInputBurstsCushion(1);
}
bool isDone() {
return false;
}
GlitchAnalyzer *getGlitchAnalyzer() {
return &mGlitchAnalyzer;
}
LoopbackProcessor *getLoopbackProcessor() override {
return (LoopbackProcessor *) &mGlitchAnalyzer;
}
private:
GlitchAnalyzer mGlitchAnalyzer;
};
#endif //OBOETESTER_FULL_DUPLEX_GLITCHES_H
@@ -0,0 +1,44 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <thread>
#include "common/OboeDebug.h"
#include "FullDuplexLatency.h"
static void analyze_data(FullDuplexLatency *fullDuplexLatency) {
fullDuplexLatency->analyzeData();
}
oboe::DataCallbackResult FullDuplexLatency::onBothStreamsReady(
const void *inputData,
int numInputFrames,
void *outputData,
int numOutputFrames) {
oboe::DataCallbackResult callbackResult = FullDuplexAnalyzer::onBothStreamsReady(
inputData, numInputFrames, outputData, numOutputFrames);
// Are we done?
if (mEchoAnalyzer.hasEnoughData()) {
// Crunch the numbers on a separate thread.
std::thread t(analyze_data, this);
t.detach();
callbackResult = oboe::DataCallbackResult::Stop;
}
return callbackResult;
};
@@ -0,0 +1,65 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef OBOETESTER_FULL_DUPLEX_LATENCY_H
#define OBOETESTER_FULL_DUPLEX_LATENCY_H
#include <unistd.h>
#include <sys/types.h>
#include "oboe/Oboe.h"
#include "FullDuplexAnalyzer.h"
class FullDuplexLatency : public FullDuplexAnalyzer {
public:
FullDuplexLatency() {}
/**
* Called when data is available on both streams.
* Caller should override this method.
*/
oboe::DataCallbackResult onBothStreamsReady(
const void *inputData,
int numInputFrames,
void *outputData,
int numOutputFrames
) override;
bool isDone() {
return mEchoAnalyzer.isDone();
}
void analyzeData() {
mEchoAnalyzer.analyze();
}
LatencyAnalyzer *getLatencyAnalyzer() {
return &mEchoAnalyzer;
}
LoopbackProcessor *getLoopbackProcessor() override {
return (LoopbackProcessor *) &mEchoAnalyzer;
}
private:
PulseLatencyAnalyzer mEchoAnalyzer;
};
#endif //OBOETESTER_FULL_DUPLEX_LATENCY_H
@@ -0,0 +1,137 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "common/OboeDebug.h"
#include "FullDuplexStream.h"
oboe::DataCallbackResult FullDuplexStream::onAudioReady(
oboe::AudioStream *outputStream,
void *audioData,
int numFrames) {
oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Continue;
int32_t actualFramesRead = 0;
// Silence the output.
int32_t numBytes = numFrames * outputStream->getBytesPerFrame();
memset(audioData, 0 /* value */, numBytes);
if (mCountCallbacksToDrain > 0) {
// Drain the input.
int32_t totalFramesRead = 0;
do {
oboe::ResultWithValue<int32_t> result = getInputStream()->read(mInputBuffer.get(),
numFrames,
0 /* timeout */);
if (!result) {
// Ignore errors because input stream may not be started yet.
break;
}
actualFramesRead = result.value();
totalFramesRead += actualFramesRead;
} while (actualFramesRead > 0);
// Only counts if we actually got some data.
if (totalFramesRead > 0) {
mCountCallbacksToDrain--;
}
} else if (mCountInputBurstsCushion > 0) {
// Let the input fill up a bit so we are not so close to the write pointer.
mCountInputBurstsCushion--;
} else if (mCountCallbacksToDiscard > 0) {
mCountCallbacksToDiscard--;
// Ignore. Allow the input to reach to equilibrium with the output.
oboe::ResultWithValue<int32_t> resultAvailable = getInputStream()->getAvailableFrames();
if (!resultAvailable) {
LOGE("%s() getAvailableFrames() returned %s\n",
__func__, convertToText(resultAvailable.error()));
callbackResult = oboe::DataCallbackResult::Stop;
} else {
int32_t framesAvailable = resultAvailable.value();
if (framesAvailable >= mMinimumFramesBeforeRead) {
oboe::ResultWithValue<int32_t> resultRead = getInputStream()->read(mInputBuffer.get(), numFrames, 0 /* timeout */);
if (!resultRead) {
LOGE("%s() read() returned %s\n", __func__, convertToText(resultRead.error()));
callbackResult = oboe::DataCallbackResult::Stop;
}
}
}
} else {
int32_t framesRead = 0;
oboe::ResultWithValue<int32_t> resultAvailable = getInputStream()->getAvailableFrames();
if (!resultAvailable) {
LOGE("%s() getAvailableFrames() returned %s\n", __func__, convertToText(resultAvailable.error()));
callbackResult = oboe::DataCallbackResult::Stop;
} else {
int32_t framesAvailable = resultAvailable.value();
if (framesAvailable >= mMinimumFramesBeforeRead) {
// Read data into input buffer.
oboe::ResultWithValue<int32_t> resultRead = getInputStream()->read(mInputBuffer.get(), numFrames, 0 /* timeout */);
if (!resultRead) {
LOGE("%s() read() returned %s\n", __func__, convertToText(resultRead.error()));
callbackResult = oboe::DataCallbackResult::Stop;
} else {
framesRead = resultRead.value();
}
}
}
if (callbackResult == oboe::DataCallbackResult::Continue) {
callbackResult = onBothStreamsReady(
mInputBuffer.get(), framesRead,
audioData, numFrames);
}
}
if (callbackResult == oboe::DataCallbackResult::Stop) {
getInputStream()->requestStop();
}
return callbackResult;
}
oboe::Result FullDuplexStream::start() {
mCountCallbacksToDrain = kNumCallbacksToDrain;
mCountInputBurstsCushion = mNumInputBurstsCushion;
mCountCallbacksToDiscard = kNumCallbacksToDiscard;
// Determine maximum size that could possibly be called.
int32_t bufferSize = getOutputStream()->getBufferCapacityInFrames()
* getOutputStream()->getChannelCount();
if (bufferSize > mBufferSize) {
mInputBuffer = std::make_unique<float[]>(bufferSize);
mBufferSize = bufferSize;
}
oboe::Result result = getInputStream()->requestStart();
if (result != oboe::Result::OK) {
return result;
}
return getOutputStream()->requestStart();
}
oboe::Result FullDuplexStream::stop() {
getOutputStream()->requestStop(); // TODO result?
return getInputStream()->requestStop();
}
int32_t FullDuplexStream::getMNumInputBurstsCushion() const {
return mNumInputBurstsCushion;
}
void FullDuplexStream::setMNumInputBurstsCushion(int32_t numBursts) {
FullDuplexStream::mNumInputBurstsCushion = numBursts;
}
@@ -0,0 +1,115 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef OBOETESTER_FULL_DUPLEX_STREAM_H
#define OBOETESTER_FULL_DUPLEX_STREAM_H
#include <unistd.h>
#include <sys/types.h>
#include "oboe/Oboe.h"
class FullDuplexStream : public oboe::AudioStreamCallback {
public:
FullDuplexStream() {}
virtual ~FullDuplexStream() = default;
void setInputStream(oboe::AudioStream *stream) {
mInputStream = stream;
}
oboe::AudioStream *getInputStream() {
return mInputStream;
}
void setOutputStream(oboe::AudioStream *stream) {
mOutputStream = stream;
}
oboe::AudioStream *getOutputStream() {
return mOutputStream;
}
virtual oboe::Result start();
virtual oboe::Result stop();
/**
* Called when data is available on both streams.
* Caller should override this method.
*/
virtual oboe::DataCallbackResult onBothStreamsReady(
const void *inputData,
int numInputFrames,
void *outputData,
int numOutputFrames
) = 0;
/**
* Called by Oboe when the stream is ready to process audio.
*/
oboe::DataCallbackResult onAudioReady(
oboe::AudioStream *audioStream,
void *audioData,
int numFrames) override;
int32_t getMNumInputBurstsCushion() const;
/**
* Number of bursts to leave in the input buffer as a cushion.
* Typically 0 for latency measurements
* or 1 for glitch tests.
*
* @param mNumInputBurstsCushion
*/
void setMNumInputBurstsCushion(int32_t mNumInputBurstsCushion);
void setMinimumFramesBeforeRead(int32_t numFrames) {
mMinimumFramesBeforeRead = numFrames;
}
int32_t getMinimumFramesBeforeRead() const {
return mMinimumFramesBeforeRead;
}
private:
// TODO add getters and setters
static constexpr int32_t kNumCallbacksToDrain = 20;
static constexpr int32_t kNumCallbacksToDiscard = 30;
// let input fill back up, usually 0 or 1
int32_t mNumInputBurstsCushion = 0;
int32_t mMinimumFramesBeforeRead = 0;
// We want to reach a state where the input buffer is empty and
// the output buffer is full.
// These are used in order.
// Drain several callback so that input is empty.
int32_t mCountCallbacksToDrain = kNumCallbacksToDrain;
// Let the input fill back up slightly so we don't run dry.
int32_t mCountInputBurstsCushion = mNumInputBurstsCushion;
// Discard some callbacks so the input and output reach equilibrium.
int32_t mCountCallbacksToDiscard = kNumCallbacksToDiscard;
oboe::AudioStream *mInputStream = nullptr;
oboe::AudioStream *mOutputStream = nullptr;
int32_t mBufferSize = 0;
std::unique_ptr<float[]> mInputBuffer;
};
#endif //OBOETESTER_FULL_DUPLEX_STREAM_H
@@ -1,61 +0,0 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "common/OboeDebug.h"
#include "FullDuplexStreamWithConversion.h"
oboe::Result FullDuplexStreamWithConversion::start() {
// Determine maximum size that could possibly be called.
int32_t maxFrames = getOutputStream()->getBufferCapacityInFrames();
int32_t inputBufferSize = maxFrames * getInputStream()->getChannelCount();
int32_t outputBufferSize = maxFrames * getOutputStream()->getChannelCount();
mInputConverter = std::make_unique<FormatConverterBox>(inputBufferSize,
getInputStream()->getFormat(),
oboe::AudioFormat::Float);
mOutputConverter = std::make_unique<FormatConverterBox>(outputBufferSize,
oboe::AudioFormat::Float,
getOutputStream()->getFormat());
return FullDuplexStream::start();
}
oboe::ResultWithValue<int32_t> FullDuplexStreamWithConversion::readInput(int32_t numFrames) {
oboe::ResultWithValue<int32_t> result = getInputStream()->read(
mInputConverter->getInputBuffer(),
numFrames,
0 /* timeout */);
if (result == oboe::Result::OK) {
int32_t numSamples = result.value() * getInputStream()->getChannelCount();
mInputConverter->convertInternalBuffers(numSamples);
}
return result;
}
oboe::DataCallbackResult FullDuplexStreamWithConversion::onBothStreamsReady(
const void *inputData,
int numInputFrames,
void *outputData,
int numOutputFrames
) {
oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Continue;
callbackResult = onBothStreamsReadyFloat(
static_cast<const float *>(mInputConverter->getOutputBuffer()),
numInputFrames,
static_cast<float *>(mOutputConverter->getInputBuffer()),
numOutputFrames);
mOutputConverter->convertFromInternalInput( outputData,
numOutputFrames * getOutputStream()->getChannelCount());
return callbackResult;
}
@@ -1,61 +0,0 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef OBOETESTER_FULL_DUPLEX_STREAM_WITH_CONVERSION_H
#define OBOETESTER_FULL_DUPLEX_STREAM_WITH_CONVERSION_H
#include <unistd.h>
#include <sys/types.h>
#include "oboe/Oboe.h"
#include "FormatConverterBox.h"
class FullDuplexStreamWithConversion : public oboe::FullDuplexStream {
public:
/**
* Called when data is available on both streams.
* Caller must override this method.
*/
virtual oboe::DataCallbackResult onBothStreamsReadyFloat(
const float *inputData,
int numInputFrames,
float *outputData,
int numOutputFrames
) = 0;
/**
* Overrides the default onBothStreamsReady by converting to floats and then calling
* onBothStreamsReadyFloat().
*/
oboe::DataCallbackResult onBothStreamsReady(
const void *inputData,
int numInputFrames,
void *outputData,
int numOutputFrames
) override;
oboe::ResultWithValue<int32_t> readInput(int32_t numFrames) override;
virtual oboe::Result start() override;
private:
std::unique_ptr<FormatConverterBox> mInputConverter;
std::unique_ptr<FormatConverterBox> mOutputConverter;
};
#endif //OBOETESTER_FULL_DUPLEX_STREAM_WITH_CONVERSION_H
@@ -17,36 +17,38 @@
#include "common/OboeDebug.h" #include "common/OboeDebug.h"
#include "InputStreamCallbackAnalyzer.h" #include "InputStreamCallbackAnalyzer.h"
double InputStreamCallbackAnalyzer::getPeakLevel(int index) {
if (mPeakDetectors == nullptr) {
LOGE("%s() called before setup()", __func__);
return -1.0;
} else if (index < 0 || index >= mNumChannels) {
LOGE("%s(), index out of range, 0 <= %d < %d", __func__, index, mNumChannels);
return -2.0;
}
return mPeakDetectors[index].getLevel();
}
oboe::DataCallbackResult InputStreamCallbackAnalyzer::onAudioReady( oboe::DataCallbackResult InputStreamCallbackAnalyzer::onAudioReady(
oboe::AudioStream *audioStream, oboe::AudioStream *audioStream,
void *audioData, void *audioData,
int numFrames) { int numFrames) {
int32_t channelCount = audioStream->getChannelCount(); int32_t channelCount = audioStream->getChannelCount();
maybeHang(getNanoseconds()); if (audioStream->getFormat() == oboe::AudioFormat::I16) {
printScheduler(); int16_t *shortData = (int16_t *) audioData;
mInputConverter->convertToInternalOutput(numFrames * channelCount, audioData); if (mRecording != nullptr) {
float *floatData = (float *) mInputConverter->getOutputBuffer(); mRecording->write(shortData, numFrames);
}
int16_t *frameData = shortData;
for (int iFrame = 0; iFrame < numFrames; iFrame++) {
for (int iChannel = 0; iChannel < channelCount; iChannel++) {
float sample = frameData[iChannel] / 32768.0f;
mPeakDetectors[iChannel].process(sample);
}
frameData += channelCount;
}
} else if (audioStream->getFormat() == oboe::AudioFormat::Float) {
float *floatData = (float *) audioData;
if (mRecording != nullptr) { if (mRecording != nullptr) {
mRecording->write(floatData, numFrames); mRecording->write(floatData, numFrames);
} }
int32_t sampleIndex = 0; float *frameData = floatData;
for (int iFrame = 0; iFrame < numFrames; iFrame++) { for (int iFrame = 0; iFrame < numFrames; iFrame++) {
for (int iChannel = 0; iChannel < channelCount; iChannel++) { for (int iChannel = 0; iChannel < channelCount; iChannel++) {
float sample = floatData[sampleIndex++]; float sample = frameData[iChannel];
mPeakDetectors[iChannel].process(sample); mPeakDetectors[iChannel].process(sample);
} }
frameData += channelCount;
}
} }
audioStream->waitForAvailableFrames(mMinimumFramesBeforeRead, oboe::kNanosPerSecond); audioStream->waitForAvailableFrames(mMinimumFramesBeforeRead, oboe::kNanosPerSecond);
@@ -23,31 +23,18 @@
// TODO #include "flowgraph/FlowGraph.h" // TODO #include "flowgraph/FlowGraph.h"
#include "oboe/Oboe.h" #include "oboe/Oboe.h"
#include "analyzer/PeakDetector.h"
#include "FormatConverterBox.h"
#include "MultiChannelRecording.h" #include "MultiChannelRecording.h"
#include "OboeTesterStreamCallback.h" #include "analyzer/PeakDetector.h"
class InputStreamCallbackAnalyzer : public OboeTesterStreamCallback { constexpr int kMaxInputChannels = 8;
class InputStreamCallbackAnalyzer : public oboe::AudioStreamCallback {
public: public:
void reset() { void reset() {
for (int iChannel = 0; iChannel < mNumChannels; iChannel++) { for (auto detector : mPeakDetectors) {
mPeakDetectors[iChannel].reset(); detector.reset();
} }
OboeTesterStreamCallback::reset();
}
void setup(int32_t maxFramesPerCallback,
int32_t channelCount,
oboe::AudioFormat inputFormat) {
mNumChannels = channelCount;
mPeakDetectors = std::make_unique<PeakDetector[]>(channelCount);
int32_t bufferSize = maxFramesPerCallback * channelCount;
mInputConverter = std::make_unique<FormatConverterBox>(bufferSize,
inputFormat,
oboe::AudioFormat::Float);
} }
/** /**
@@ -62,7 +49,9 @@ public:
mRecording = recording; mRecording = recording;
} }
double getPeakLevel(int index); double getPeakLevel(int index) {
return mPeakDetectors[index].getLevel();
}
void setMinimumFramesBeforeRead(int32_t numFrames) { void setMinimumFramesBeforeRead(int32_t numFrames) {
mMinimumFramesBeforeRead = numFrames; mMinimumFramesBeforeRead = numFrames;
@@ -73,12 +62,10 @@ public:
} }
public: public:
int32_t mNumChannels = 0; PeakDetector mPeakDetectors[kMaxInputChannels];
std::unique_ptr<PeakDetector[]> mPeakDetectors;
MultiChannelRecording *mRecording = nullptr; MultiChannelRecording *mRecording = nullptr;
private: private:
std::unique_ptr<FormatConverterBox> mInputConverter;
int32_t mMinimumFramesBeforeRead = 0; int32_t mMinimumFramesBeforeRead = 0;
}; };
@@ -14,8 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
#include <algorithm> #include "common/OboeDebug.h"
#include "InterpolatingDelayLine.h" #include "InterpolatingDelayLine.h"
InterpolatingDelayLine::InterpolatingDelayLine(int32_t delaySize) { InterpolatingDelayLine::InterpolatingDelayLine(int32_t delaySize) {
@@ -21,6 +21,9 @@
#include <unistd.h> #include <unistd.h>
#include <sys/types.h> #include <sys/types.h>
#include "oboe/Oboe.h"
#include "FullDuplexStream.h"
/** /**
* Monophonic delay line. * Monophonic delay line.
*/ */
@@ -14,20 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
// Set to 1 for debugging race condition #1180 with mAAudioStream.
// See also AudioStreamAAudio.cpp in Oboe.
// This was left in the code so that we could test the fix again easily in the future.
// We could not trigger the race condition without adding these get calls and the sleeps.
#define DEBUG_CLOSE_RACE 0
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
#if DEBUG_CLOSE_RACE
#include <thread>
#endif // DEBUG_CLOSE_RACE
#include <vector> #include <vector>
#include "oboe/AudioClock.h"
#include "util/WaveFileWriter.h" #include "util/WaveFileWriter.h"
#include "NativeAudioContext.h" #include "NativeAudioContext.h"
@@ -67,9 +57,9 @@ private:
bool ActivityContext::mUseCallback = true; bool ActivityContext::mUseCallback = true;
int ActivityContext::callbackSize = 0; int ActivityContext::callbackSize = 0;
std::shared_ptr<oboe::AudioStream> ActivityContext::getOutputStream() { oboe::AudioStream * ActivityContext::getOutputStream() {
for (auto entry : mOboeStreams) { for (auto entry : mOboeStreams) {
std::shared_ptr<oboe::AudioStream> oboeStream = entry.second; oboe::AudioStream *oboeStream = entry.second.get();
if (oboeStream->getDirection() == oboe::Direction::Output) { if (oboeStream->getDirection() == oboe::Direction::Output) {
return oboeStream; return oboeStream;
} }
@@ -77,9 +67,9 @@ std::shared_ptr<oboe::AudioStream> ActivityContext::getOutputStream() {
return nullptr; return nullptr;
} }
std::shared_ptr<oboe::AudioStream> ActivityContext::getInputStream() { oboe::AudioStream * ActivityContext::getInputStream() {
for (auto entry : mOboeStreams) { for (auto entry : mOboeStreams) {
std::shared_ptr<oboe::AudioStream> oboeStream = entry.second; oboe::AudioStream *oboeStream = entry.second.get();
if (oboeStream != nullptr) { if (oboeStream != nullptr) {
if (oboeStream->getDirection() == oboe::Direction::Input) { if (oboeStream->getDirection() == oboe::Direction::Input) {
return oboeStream; return oboeStream;
@@ -98,19 +88,9 @@ int32_t ActivityContext::allocateStreamIndex() {
return mNextStreamHandle++; return mNextStreamHandle++;
} }
oboe::Result ActivityContext::release() {
oboe::Result result = oboe::Result::OK;
stopBlockingIOThread();
for (auto entry : mOboeStreams) {
std::shared_ptr<oboe::AudioStream> oboeStream = entry.second;
result = oboeStream->release();
}
return result;
}
void ActivityContext::close(int32_t streamIndex) { void ActivityContext::close(int32_t streamIndex) {
stopBlockingIOThread(); stopBlockingIOThread();
std::shared_ptr<oboe::AudioStream> oboeStream = getStream(streamIndex); oboe::AudioStream *oboeStream = getStream(streamIndex);
if (oboeStream != nullptr) { if (oboeStream != nullptr) {
oboeStream->close(); oboeStream->close();
LOGD("ActivityContext::%s() delete stream %d ", __func__, streamIndex); LOGD("ActivityContext::%s() delete stream %d ", __func__, streamIndex);
@@ -119,18 +99,19 @@ void ActivityContext::close(int32_t streamIndex) {
} }
bool ActivityContext::isMMapUsed(int32_t streamIndex) { bool ActivityContext::isMMapUsed(int32_t streamIndex) {
std::shared_ptr<oboe::AudioStream> oboeStream = getStream(streamIndex); oboe::AudioStream *oboeStream = getStream(streamIndex);
if (oboeStream == nullptr) return false; if (oboeStream == nullptr) return false;
if (oboeStream->getAudioApi() != AudioApi::AAudio) return false; if (oboeStream->getAudioApi() != AudioApi::AAudio) return false;
return AAudioExtensions::getInstance().isMMapUsed(oboeStream.get()); return AAudioExtensions::getInstance().isMMapUsed(oboeStream);
} }
oboe::Result ActivityContext::pause() { oboe::Result ActivityContext::pause() {
oboe::Result result = oboe::Result::OK; oboe::Result result = oboe::Result::OK;
stopBlockingIOThread(); stopBlockingIOThread();
for (auto entry : mOboeStreams) { for (auto entry : mOboeStreams) {
std::shared_ptr<oboe::AudioStream> oboeStream = entry.second; oboe::AudioStream *oboeStream = entry.second.get();
result = oboeStream->requestPause(); result = oboeStream->requestPause();
printScheduler();
} }
return result; return result;
} }
@@ -139,8 +120,9 @@ oboe::Result ActivityContext::stopAllStreams() {
oboe::Result result = oboe::Result::OK; oboe::Result result = oboe::Result::OK;
stopBlockingIOThread(); stopBlockingIOThread();
for (auto entry : mOboeStreams) { for (auto entry : mOboeStreams) {
std::shared_ptr<oboe::AudioStream> oboeStream = entry.second; oboe::AudioStream *oboeStream = entry.second.get();
result = oboeStream->requestStop(); result = oboeStream->requestStop();
printScheduler();
} }
return result; return result;
} }
@@ -148,23 +130,23 @@ oboe::Result ActivityContext::stopAllStreams() {
void ActivityContext::configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) { void ActivityContext::configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) {
// We needed the proxy because we did not know the channelCount when we setup the Builder. // We needed the proxy because we did not know the channelCount when we setup the Builder.
if (mUseCallback) { if (mUseCallback) {
builder.setDataCallback(&oboeCallbackProxy); LOGD("ActivityContext::open() set callback to use oboeCallbackProxy, callback size = %d",
callbackSize);
builder.setCallback(&oboeCallbackProxy);
builder.setFramesPerCallback(callbackSize);
} }
} }
int ActivityContext::open(jint nativeApi, int ActivityContext::open(jint nativeApi,
jint sampleRate, jint sampleRate,
jint channelCount, jint channelCount,
jint channelMask,
jint format, jint format,
jint sharingMode, jint sharingMode,
jint performanceMode, jint performanceMode,
jint inputPreset, jint inputPreset,
jint usage,
jint contentType,
jint bufferCapacityInFrames,
jint deviceId, jint deviceId,
jint sessionId, jint sessionId,
jint framesPerBurst,
jboolean channelConversionAllowed, jboolean channelConversionAllowed,
jboolean formatConversionAllowed, jboolean formatConversionAllowed,
jint rateConversionQuality, jint rateConversionQuality,
@@ -200,9 +182,6 @@ int ActivityContext::open(jint nativeApi,
->setSharingMode((oboe::SharingMode) sharingMode) ->setSharingMode((oboe::SharingMode) sharingMode)
->setPerformanceMode((oboe::PerformanceMode) performanceMode) ->setPerformanceMode((oboe::PerformanceMode) performanceMode)
->setInputPreset((oboe::InputPreset)inputPreset) ->setInputPreset((oboe::InputPreset)inputPreset)
->setUsage((oboe::Usage)usage)
->setContentType((oboe::ContentType)contentType)
->setBufferCapacityInFrames(bufferCapacityInFrames)
->setDeviceId(deviceId) ->setDeviceId(deviceId)
->setSessionId((oboe::SessionId) sessionId) ->setSessionId((oboe::SessionId) sessionId)
->setSampleRate(sampleRate) ->setSampleRate(sampleRate)
@@ -211,13 +190,7 @@ int ActivityContext::open(jint nativeApi,
->setFormatConversionAllowed(formatConversionAllowed) ->setFormatConversionAllowed(formatConversionAllowed)
->setSampleRateConversionQuality((oboe::SampleRateConversionQuality) rateConversionQuality) ->setSampleRateConversionQuality((oboe::SampleRateConversionQuality) rateConversionQuality)
; ;
if (channelMask != (jint) oboe::ChannelMask::Unspecified) {
// Set channel mask when it is specified.
builder.setChannelMask((oboe::ChannelMask) channelMask);
}
if (mUseCallback) {
builder.setFramesPerCallback(callbackSize);
}
configureBuilder(isInput, builder); configureBuilder(isInput, builder);
builder.setAudioApi(audioApi); builder.setAudioApi(audioApi);
@@ -226,12 +199,6 @@ int ActivityContext::open(jint nativeApi,
bool oldMMapEnabled = AAudioExtensions::getInstance().isMMapEnabled(); bool oldMMapEnabled = AAudioExtensions::getInstance().isMMapEnabled();
AAudioExtensions::getInstance().setMMapEnabled(isMMap); AAudioExtensions::getInstance().setMMapEnabled(isMMap);
// Record time for opening.
if (isInput) {
mInputOpenedAt = oboe::AudioClock::getNanoseconds();
} else {
mOutputOpenedAt = oboe::AudioClock::getNanoseconds();
}
// Open a stream based on the builder settings. // Open a stream based on the builder settings.
std::shared_ptr<oboe::AudioStream> oboeStream; std::shared_ptr<oboe::AudioStream> oboeStream;
Result result = builder.openStream(oboeStream); Result result = builder.openStream(oboeStream);
@@ -248,7 +215,7 @@ int ActivityContext::open(jint nativeApi,
createRecording(); createRecording();
finishOpen(isInput, oboeStream); finishOpen(isInput, oboeStream.get());
} }
if (!mUseCallback) { if (!mUseCallback) {
@@ -256,24 +223,20 @@ int ActivityContext::open(jint nativeApi,
dataBuffer = std::make_unique<float[]>(numSamples); dataBuffer = std::make_unique<float[]>(numSamples);
} }
if (result != Result::OK) { return (result != Result::OK) ? (int)result : streamIndex;
return (int) result;
} else {
configureAfterOpen();
return streamIndex;
}
} }
oboe::Result ActivityContext::start() { oboe::Result ActivityContext::start() {
oboe::Result result = oboe::Result::OK; oboe::Result result = oboe::Result::OK;
std::shared_ptr<oboe::AudioStream> inputStream = getInputStream(); oboe::AudioStream *inputStream = getInputStream();
std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream(); oboe::AudioStream *outputStream = getOutputStream();
if (inputStream == nullptr && outputStream == nullptr) { if (inputStream == nullptr && outputStream == nullptr) {
LOGD("%s() - no streams defined", __func__); LOGD("%s() - no streams defined", __func__);
return oboe::Result::ErrorInvalidState; // not open return oboe::Result::ErrorInvalidState; // not open
} }
audioStreamGateway.reset(); configureForStart();
result = startStreams(); result = startStreams();
if (!mUseCallback && result == oboe::Result::OK) { if (!mUseCallback && result == oboe::Result::OK) {
@@ -282,29 +245,6 @@ oboe::Result ActivityContext::start() {
dataThread = new std::thread(threadCallback, this); dataThread = new std::thread(threadCallback, this);
} }
#if DEBUG_CLOSE_RACE
// Also put a sleep for 400 msec in AudioStreamAAudio::updateFramesRead().
if (outputStream != nullptr) {
std::thread raceDebugger([outputStream]() {
while (outputStream->getState() != StreamState::Closed) {
int64_t framesRead = outputStream->getFramesRead();
LOGD("raceDebugger, framesRead = %d, state = %d",
(int) framesRead, (int) outputStream->getState());
}
});
raceDebugger.detach();
}
#endif // DEBUG_CLOSE_RACE
return result;
}
oboe::Result ActivityContext::flush() {
oboe::Result result = oboe::Result::OK;
for (auto entry : mOboeStreams) {
std::shared_ptr<oboe::AudioStream> oboeStream = entry.second;
result = oboeStream->requestFlush();
}
return result; return result;
} }
@@ -319,11 +259,10 @@ int32_t ActivityContext::saveWaveFile(const char *filename) {
} }
MyOboeOutputStream outStream; MyOboeOutputStream outStream;
WaveFileWriter writer(&outStream); WaveFileWriter writer(&outStream);
// You must setup the format before the first write().
writer.setFrameRate(mSampleRate); writer.setFrameRate(mSampleRate);
writer.setSamplesPerFrame(mRecording->getChannelCount()); writer.setSamplesPerFrame(mRecording->getChannelCount());
writer.setBitsPerSample(24); writer.setBitsPerSample(24);
writer.setFrameCount(mRecording->getSizeInFrames());
float buffer[mRecording->getChannelCount()]; float buffer[mRecording->getChannelCount()];
// Read samples from start to finish. // Read samples from start to finish.
mRecording->rewind(); mRecording->rewind();
@@ -344,25 +283,13 @@ int32_t ActivityContext::saveWaveFile(const char *filename) {
return outStream.length(); return outStream.length();
} }
double ActivityContext::getTimestampLatency(int32_t streamIndex) {
std::shared_ptr<oboe::AudioStream> oboeStream = getStream(streamIndex);
if (oboeStream != nullptr) {
auto result = oboeStream->calculateLatencyMillis();
return (!result) ? -1.0 : result.value();
}
return -1.0;
}
// =================================================================== ActivityTestOutput // =================================================================== ActivityTestOutput
void ActivityTestOutput::close(int32_t streamIndex) { void ActivityTestOutput::close(int32_t streamIndex) {
ActivityContext::close(streamIndex); ActivityContext::close(streamIndex);
manyToMulti.reset(nullptr); manyToMulti.reset(nullptr);
monoToMulti.reset(nullptr); monoToMulti.reset(nullptr);
mVolumeRamp.reset();
mSinkFloat.reset(); mSinkFloat.reset();
mSinkI16.reset(); mSinkI16.reset();
mSinkI24.reset();
mSinkI32.reset();
} }
void ActivityTestOutput::setChannelEnabled(int channelIndex, bool enabled) { void ActivityTestOutput::setChannelEnabled(int channelIndex, bool enabled) {
@@ -386,9 +313,6 @@ void ActivityTestOutput::setChannelEnabled(int channelIndex, bool enabled) {
mExponentialShape.output.connect(&sineOscillators[channelIndex].frequency); mExponentialShape.output.connect(&sineOscillators[channelIndex].frequency);
sineOscillators[channelIndex].output.connect(manyToMulti->inputs[channelIndex].get()); sineOscillators[channelIndex].output.connect(manyToMulti->inputs[channelIndex].get());
break; break;
case SignalType::WhiteNoise:
mWhiteNoise.output.connect(manyToMulti->inputs[channelIndex].get());
break;
default: default:
break; break;
} }
@@ -397,20 +321,13 @@ void ActivityTestOutput::setChannelEnabled(int channelIndex, bool enabled) {
} }
} }
void ActivityTestOutput::configureAfterOpen() { void ActivityTestOutput::configureForStart() {
manyToMulti = std::make_unique<ManyToMultiConverter>(mChannelCount); manyToMulti = std::make_unique<ManyToMultiConverter>(mChannelCount);
std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream(); mSinkFloat = std::make_unique<SinkFloat>(mChannelCount);
mSinkI16 = std::make_unique<SinkI16>(mChannelCount);
mVolumeRamp = std::make_shared<RampLinear>(mChannelCount); oboe::AudioStream *outputStream = getOutputStream();
mVolumeRamp->setLengthInFrames(kRampMSec * outputStream->getSampleRate() /
MILLISECONDS_PER_SECOND);
mVolumeRamp->setTarget(mAmplitude);
mSinkFloat = std::make_shared<SinkFloat>(mChannelCount);
mSinkI16 = std::make_shared<SinkI16>(mChannelCount);
mSinkI24 = std::make_shared<SinkI24>(mChannelCount);
mSinkI32 = std::make_shared<SinkI32>(mChannelCount);
mTriangleOscillator.setSampleRate(outputStream->getSampleRate()); mTriangleOscillator.setSampleRate(outputStream->getSampleRate());
mTriangleOscillator.frequency.setValue(1.0/kSweepPeriod); mTriangleOscillator.frequency.setValue(1.0/kSweepPeriod);
@@ -427,52 +344,35 @@ void ActivityTestOutput::configureAfterOpen() {
mTriangleOscillator.output.connect(&(mExponentialShape.input)); mTriangleOscillator.output.connect(&(mExponentialShape.input));
{ {
double frequency = 330.0; double frequency = 330.0;
// Go up by a minor third or a perfect fourth just intoned interval.
const float interval = (mChannelCount > 8) ? (6.0f / 5.0f) : (4.0f / 3.0f);
for (int i = 0; i < mChannelCount; i++) { for (int i = 0; i < mChannelCount; i++) {
sineOscillators[i].setSampleRate(outputStream->getSampleRate()); sineOscillators[i].setSampleRate(outputStream->getSampleRate());
sineOscillators[i].frequency.setValue(frequency); sineOscillators[i].frequency.setValue(frequency);
frequency *= 4.0 / 3.0; // each sine is at a higher frequency
sineOscillators[i].amplitude.setValue(AMPLITUDE_SINE); sineOscillators[i].amplitude.setValue(AMPLITUDE_SINE);
sawtoothOscillators[i].setSampleRate(outputStream->getSampleRate());
sawtoothOscillators[i].frequency.setValue(frequency);
sawtoothOscillators[i].amplitude.setValue(AMPLITUDE_SAWTOOTH);
frequency *= interval; // each wave is at a higher frequency
setChannelEnabled(i, true); setChannelEnabled(i, true);
} }
} }
mWhiteNoise.amplitude.setValue(0.5); manyToMulti->output.connect(&(mSinkFloat.get()->input));
manyToMulti->output.connect(&(mSinkI16.get()->input));
manyToMulti->output.connect(&(mVolumeRamp.get()->input));
mVolumeRamp->output.connect(&(mSinkFloat.get()->input));
mVolumeRamp->output.connect(&(mSinkI16.get()->input));
mVolumeRamp->output.connect(&(mSinkI24.get()->input));
mVolumeRamp->output.connect(&(mSinkI32.get()->input));
// Clear framePosition in sine oscillators.
mSinkFloat->pullReset(); mSinkFloat->pullReset();
mSinkI16->pullReset(); mSinkI16->pullReset();
mSinkI24->pullReset();
mSinkI32->pullReset();
configureStreamGateway(); configureStreamGateway();
} }
void ActivityTestOutput::configureStreamGateway() { void ActivityTestOutput::configureStreamGateway() {
std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream(); oboe::AudioStream *outputStream = getOutputStream();
if (outputStream->getFormat() == oboe::AudioFormat::I16) { if (outputStream->getFormat() == oboe::AudioFormat::I16) {
audioStreamGateway.setAudioSink(mSinkI16); audioStreamGateway.setAudioSink(mSinkI16);
} else if (outputStream->getFormat() == oboe::AudioFormat::I24) {
audioStreamGateway.setAudioSink(mSinkI24);
} else if (outputStream->getFormat() == oboe::AudioFormat::I32) {
audioStreamGateway.setAudioSink(mSinkI32);
} else if (outputStream->getFormat() == oboe::AudioFormat::Float) { } else if (outputStream->getFormat() == oboe::AudioFormat::Float) {
audioStreamGateway.setAudioSink(mSinkFloat); audioStreamGateway.setAudioSink(mSinkFloat);
} }
if (mUseCallback) { if (mUseCallback) {
oboeCallbackProxy.setDataCallback(&audioStreamGateway); oboeCallbackProxy.setCallback(&audioStreamGateway);
} }
} }
@@ -480,7 +380,7 @@ void ActivityTestOutput::runBlockingIO() {
int32_t framesPerBlock = getFramesPerBlock(); int32_t framesPerBlock = getFramesPerBlock();
oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Continue; oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Continue;
std::shared_ptr<oboe::AudioStream> oboeStream = getOutputStream(); oboe::AudioStream *oboeStream = getOutputStream();
if (oboeStream == nullptr) { if (oboeStream == nullptr) {
LOGE("%s() : no stream found\n", __func__); LOGE("%s() : no stream found\n", __func__);
return; return;
@@ -489,7 +389,7 @@ void ActivityTestOutput::runBlockingIO() {
while (threadEnabled.load() while (threadEnabled.load()
&& callbackResult == oboe::DataCallbackResult::Continue) { && callbackResult == oboe::DataCallbackResult::Continue) {
// generate output by calling the callback // generate output by calling the callback
callbackResult = audioStreamGateway.onAudioReady(oboeStream.get(), callbackResult = audioStreamGateway.onAudioReady(oboeStream,
dataBuffer.get(), dataBuffer.get(),
framesPerBlock); framesPerBlock);
@@ -509,22 +409,11 @@ void ActivityTestOutput::runBlockingIO() {
} }
} }
oboe::Result ActivityTestOutput::startStreams() {
mSinkFloat->pullReset();
mSinkI16->pullReset();
mSinkI24->pullReset();
mSinkI32->pullReset();
if (mVolumeRamp != nullptr) {
mVolumeRamp->setTarget(mAmplitude);
}
return getOutputStream()->start();
}
// ======================================================================= ActivityTestInput // ======================================================================= ActivityTestInput
void ActivityTestInput::configureAfterOpen() { void ActivityTestInput::configureForStart() {
mInputAnalyzer.reset(); mInputAnalyzer.reset();
if (mUseCallback) { if (mUseCallback) {
oboeCallbackProxy.setDataCallback(&mInputAnalyzer); oboeCallbackProxy.setCallback(&mInputAnalyzer);
} }
mInputAnalyzer.setRecording(mRecording.get()); mInputAnalyzer.setRecording(mRecording.get());
} }
@@ -533,7 +422,7 @@ void ActivityTestInput::runBlockingIO() {
int32_t framesPerBlock = getFramesPerBlock(); int32_t framesPerBlock = getFramesPerBlock();
oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Continue; oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Continue;
std::shared_ptr<oboe::AudioStream> oboeStream = getInputStream(); oboe::AudioStream *oboeStream = getInputStream();
if (oboeStream == nullptr) { if (oboeStream == nullptr) {
LOGE("%s() : no stream found\n", __func__); LOGE("%s() : no stream found\n", __func__);
return; return;
@@ -561,7 +450,7 @@ void ActivityTestInput::runBlockingIO() {
} }
// analyze input // analyze input
callbackResult = mInputAnalyzer.onAudioReady(oboeStream.get(), callbackResult = mInputAnalyzer.onAudioReady(oboeStream,
dataBuffer.get(), dataBuffer.get(),
framesRead); framesRead);
} }
@@ -585,7 +474,8 @@ oboe::Result ActivityRecording::startPlayback() {
builder.setChannelCount(mChannelCount) builder.setChannelCount(mChannelCount)
->setSampleRate(mSampleRate) ->setSampleRate(mSampleRate)
->setFormat(oboe::AudioFormat::Float) ->setFormat(oboe::AudioFormat::Float)
->setCallback(&mPlayRecordingCallback); ->setCallback(&mPlayRecordingCallback)
->setAudioApi(oboe::AudioApi::OpenSLES);
oboe::Result result = builder.openStream(&playbackStream); oboe::Result result = builder.openStream(&playbackStream);
if (result != oboe::Result::OK) { if (result != oboe::Result::OK) {
delete playbackStream; delete playbackStream;
@@ -601,15 +491,13 @@ oboe::Result ActivityRecording::startPlayback() {
} }
// ======================================================================= ActivityTapToTone // ======================================================================= ActivityTapToTone
void ActivityTapToTone::configureAfterOpen() { void ActivityTapToTone::configureForStart() {
monoToMulti = std::make_unique<MonoToMultiConverter>(mChannelCount); monoToMulti = std::make_unique<MonoToMultiConverter>(mChannelCount);
mSinkFloat = std::make_shared<SinkFloat>(mChannelCount); mSinkFloat = std::make_unique<SinkFloat>(mChannelCount);
mSinkI16 = std::make_shared<SinkI16>(mChannelCount); mSinkI16 = std::make_unique<SinkI16>(mChannelCount);
mSinkI24 = std::make_shared<SinkI24>(mChannelCount);
mSinkI32 = std::make_shared<SinkI32>(mChannelCount);
std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream(); oboe::AudioStream *outputStream = getOutputStream();
sawPingGenerator.setSampleRate(outputStream->getSampleRate()); sawPingGenerator.setSampleRate(outputStream->getSampleRate());
sawPingGenerator.frequency.setValue(FREQUENCY_SAW_PING); sawPingGenerator.frequency.setValue(FREQUENCY_SAW_PING);
sawPingGenerator.amplitude.setValue(AMPLITUDE_SAW_PING); sawPingGenerator.amplitude.setValue(AMPLITUDE_SAW_PING);
@@ -617,26 +505,20 @@ void ActivityTapToTone::configureAfterOpen() {
sawPingGenerator.output.connect(&(monoToMulti->input)); sawPingGenerator.output.connect(&(monoToMulti->input));
monoToMulti->output.connect(&(mSinkFloat.get()->input)); monoToMulti->output.connect(&(mSinkFloat.get()->input));
monoToMulti->output.connect(&(mSinkI16.get()->input)); monoToMulti->output.connect(&(mSinkI16.get()->input));
monoToMulti->output.connect(&(mSinkI24.get()->input));
monoToMulti->output.connect(&(mSinkI32.get()->input));
mSinkFloat->pullReset();
mSinkI16->pullReset();
mSinkI24->pullReset();
mSinkI32->pullReset();
sawPingGenerator.setEnabled(false);
configureStreamGateway(); configureStreamGateway();
} }
// ======================================================================= ActivityFullDuplex // ======================================================================= ActivityRoundTripLatency
void ActivityFullDuplex::configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) { void ActivityFullDuplex::configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) {
if (isInput) { if (isInput) {
// Ideally the output streams should be opened first. // Ideally the output streams should be opened first.
std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream(); oboe::AudioStream *outputStream = getOutputStream();
if (outputStream != nullptr) { if (outputStream != nullptr) {
// The input and output buffers will run in sync with input empty // Make sure the capacity is bigger than two bursts.
// and output full. So set the input capacity to match the output. int32_t burst = outputStream->getFramesPerBurst();
builder.setBufferCapacityInFrames(outputStream->getBufferCapacityInFrames()); builder.setBufferCapacityInFrames(2 * burst);
} }
} }
} }
@@ -650,16 +532,15 @@ void ActivityEcho::configureBuilder(bool isInput, oboe::AudioStreamBuilder &buil
} }
// only output uses a callback, input is polled // only output uses a callback, input is polled
if (!isInput) { if (!isInput) {
builder.setCallback((oboe::AudioStreamCallback *) &oboeCallbackProxy); builder.setCallback(mFullDuplexEcho.get());
oboeCallbackProxy.setDataCallback(mFullDuplexEcho.get());
} }
} }
void ActivityEcho::finishOpen(bool isInput, std::shared_ptr<oboe::AudioStream> &oboeStream) { void ActivityEcho::finishOpen(bool isInput, oboe::AudioStream *oboeStream) {
if (isInput) { if (isInput) {
mFullDuplexEcho->setSharedInputStream(oboeStream); mFullDuplexEcho->setInputStream(oboeStream);
} else { } else {
mFullDuplexEcho->setSharedOutputStream(oboeStream); mFullDuplexEcho->setOutputStream(oboeStream);
} }
} }
@@ -668,102 +549,45 @@ void ActivityRoundTripLatency::configureBuilder(bool isInput, oboe::AudioStreamB
ActivityFullDuplex::configureBuilder(isInput, builder); ActivityFullDuplex::configureBuilder(isInput, builder);
if (mFullDuplexLatency.get() == nullptr) { if (mFullDuplexLatency.get() == nullptr) {
mFullDuplexLatency = std::make_unique<FullDuplexAnalyzer>(mLatencyAnalyzer.get()); mFullDuplexLatency = std::make_unique<FullDuplexLatency>();
} }
if (!isInput) { if (!isInput) {
// only output uses a callback, input is polled // only output uses a callback, input is polled
builder.setCallback((oboe::AudioStreamCallback *) &oboeCallbackProxy); builder.setCallback(mFullDuplexLatency.get());
oboeCallbackProxy.setDataCallback(mFullDuplexLatency.get());
} }
} }
void ActivityRoundTripLatency::finishOpen(bool isInput, std::shared_ptr<oboe::AudioStream> void ActivityRoundTripLatency::finishOpen(bool isInput, oboe::AudioStream *oboeStream) {
&oboeStream) {
if (isInput) { if (isInput) {
mFullDuplexLatency->setSharedInputStream(oboeStream); mFullDuplexLatency->setInputStream(oboeStream);
mFullDuplexLatency->setRecording(mRecording.get()); mFullDuplexLatency->setRecording(mRecording.get());
} else { } else {
mFullDuplexLatency->setSharedOutputStream(oboeStream); mFullDuplexLatency->setOutputStream(oboeStream);
} }
} }
// The timestamp latency is the difference between the input
// and output times for a specific frame.
// Start with the position and time from an input timestamp.
// Map the input position to the corresponding position in output
// and calculate its time.
// Use the difference between framesWritten and framesRead to
// convert input positions to output positions.
jdouble ActivityRoundTripLatency::measureTimestampLatency() {
if (!mFullDuplexLatency->isWriteReadDeltaValid()) return -1.0;
int64_t writeReadDelta = mFullDuplexLatency->getWriteReadDelta();
auto inputTimestampResult = mFullDuplexLatency->getInputStream()->getTimestamp(CLOCK_MONOTONIC);
if (!inputTimestampResult) return -1.0;
auto outputTimestampResult = mFullDuplexLatency->getOutputStream()->getTimestamp(CLOCK_MONOTONIC);
if (!outputTimestampResult) return -1.0;
int64_t inputPosition = inputTimestampResult.value().position;
int64_t inputTimeNanos = inputTimestampResult.value().timestamp;
int64_t ouputPosition = outputTimestampResult.value().position;
int64_t outputTimeNanos = outputTimestampResult.value().timestamp;
// Map input frame position to the corresponding output frame.
int64_t mappedPosition = inputPosition + writeReadDelta;
// Calculate when that frame will play.
int32_t sampleRate = mFullDuplexLatency->getOutputStream()->getSampleRate();
int64_t mappedTimeNanos = outputTimeNanos + ((mappedPosition - ouputPosition) * 1e9) / sampleRate;
// Latency is the difference in time between when a frame was recorded and
// when its corresponding echo was played.
return (mappedTimeNanos - inputTimeNanos) * 1.0e-6; // convert nanos to millis
}
// ======================================================================= ActivityGlitches // ======================================================================= ActivityGlitches
void ActivityGlitches::configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) { void ActivityGlitches::configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) {
ActivityFullDuplex::configureBuilder(isInput, builder); ActivityFullDuplex::configureBuilder(isInput, builder);
if (mFullDuplexGlitches.get() == nullptr) { if (mFullDuplexGlitches.get() == nullptr) {
mFullDuplexGlitches = std::make_unique<FullDuplexAnalyzer>(&mGlitchAnalyzer); mFullDuplexGlitches = std::make_unique<FullDuplexGlitches>();
} }
if (!isInput) { if (!isInput) {
// only output uses a callback, input is polled // only output uses a callback, input is polled
builder.setCallback((oboe::AudioStreamCallback *) &oboeCallbackProxy); builder.setCallback(mFullDuplexGlitches.get());
oboeCallbackProxy.setDataCallback(mFullDuplexGlitches.get());
} }
} }
void ActivityGlitches::finishOpen(bool isInput, std::shared_ptr<oboe::AudioStream> &oboeStream) { void ActivityGlitches::finishOpen(bool isInput, oboe::AudioStream *oboeStream) {
if (isInput) { if (isInput) {
mFullDuplexGlitches->setSharedInputStream(oboeStream); mFullDuplexGlitches->setInputStream(oboeStream);
mFullDuplexGlitches->setRecording(mRecording.get()); mFullDuplexGlitches->setRecording(mRecording.get());
} else { } else {
mFullDuplexGlitches->setSharedOutputStream(oboeStream); mFullDuplexGlitches->setOutputStream(oboeStream);
} }
} }
// ======================================================================= ActivityDataPath
void ActivityDataPath::configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) {
ActivityFullDuplex::configureBuilder(isInput, builder);
if (mFullDuplexDataPath.get() == nullptr) {
mFullDuplexDataPath = std::make_unique<FullDuplexAnalyzer>(&mDataPathAnalyzer);
}
if (!isInput) {
// only output uses a callback, input is polled
builder.setCallback((oboe::AudioStreamCallback *) &oboeCallbackProxy);
oboeCallbackProxy.setDataCallback(mFullDuplexDataPath.get());
}
}
void ActivityDataPath::finishOpen(bool isInput, std::shared_ptr<oboe::AudioStream> &oboeStream) {
if (isInput) {
mFullDuplexDataPath->setSharedInputStream(oboeStream);
mFullDuplexDataPath->setRecording(mRecording.get());
} else {
mFullDuplexDataPath->setSharedOutputStream(oboeStream);
}
}
// =================================================================== ActivityTestDisconnect // =================================================================== ActivityTestDisconnect
void ActivityTestDisconnect::close(int32_t streamIndex) { void ActivityTestDisconnect::close(int32_t streamIndex) {
@@ -771,9 +595,9 @@ void ActivityTestDisconnect::close(int32_t streamIndex) {
mSinkFloat.reset(); mSinkFloat.reset();
} }
void ActivityTestDisconnect::configureAfterOpen() { void ActivityTestDisconnect::configureForStart() {
std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream(); oboe::AudioStream *outputStream = getOutputStream();
std::shared_ptr<oboe::AudioStream> inputStream = getInputStream(); oboe::AudioStream *inputStream = getInputStream();
if (outputStream) { if (outputStream) {
mSinkFloat = std::make_unique<SinkFloat>(mChannelCount); mSinkFloat = std::make_unique<SinkFloat>(mChannelCount);
sineOscillator = std::make_unique<SineOscillator>(); sineOscillator = std::make_unique<SineOscillator>();
@@ -783,13 +607,13 @@ void ActivityTestDisconnect::configureAfterOpen() {
sineOscillator->frequency.setValue(440.0); sineOscillator->frequency.setValue(440.0);
sineOscillator->amplitude.setValue(AMPLITUDE_SINE); sineOscillator->amplitude.setValue(AMPLITUDE_SINE);
sineOscillator->output.connect(&(monoToMulti->input)); sineOscillator->output.connect(&(monoToMulti->input));
monoToMulti->output.connect(&(mSinkFloat->input)); monoToMulti->output.connect(&(mSinkFloat->input));
// Clear framePosition in sine oscillators.
mSinkFloat->pullReset(); mSinkFloat->pullReset();
audioStreamGateway.setAudioSink(mSinkFloat); audioStreamGateway.setAudioSink(mSinkFloat);
} else if (inputStream) { } else if (inputStream) {
audioStreamGateway.setAudioSink(nullptr); audioStreamGateway.setAudioSink(nullptr);
} }
oboeCallbackProxy.setDataCallback(&audioStreamGateway); oboeCallbackProxy.setCallback(&audioStreamGateway);
} }

Some files were not shown because too many files have changed in this diff Show More