Compare commits

...

45 Commits

Author SHA1 Message Date
bryanthaboi 872d6b4516 Merge pull request #1572 from bryanthaboi/dev
bugs and bug reporting
2026-08-19 14:03:47 -04:00
bryanthaboi def270f7c7 Merge pull request #1560 from castdrian/device-report-bug-tab
feat(launcher): add bug tab and native device reporting
2026-08-19 13:54:38 -04:00
bryanthaboi 93e336b7cb Update video link and thumbnail in README 2026-08-19 13:36:35 -04:00
bryanthaboi 4c8c1cf36b CLOSES #998, CLOSES #1472, CLOSES #1526, CLOSES #1529, CLOSES #1530, CLOSES #1532, CLOSES #1534, CLOSES #1547, CLOSES #1549, CLOSES #1550, CLOSES #1551 2026-08-19 11:19:54 -04:00
Adrian Castro 7d9e99ea18 chore(repo): add code owners 2026-08-19 15:59:42 +02:00
Adrian Castro b27e5ab017 fix(launcher): simplify bug report card title 2026-08-19 15:51:23 +02:00
Adrian Castro fd9f3da91a fix(launcher): use rounded bug report icon 2026-08-19 15:51:23 +02:00
Adrian Castro a7c19be88f fix(launcher): use standard bug report icon 2026-08-19 15:51:22 +02:00
Adrian Castro 9ab80adaca feat(launcher): add bug tab and native device reporting 2026-08-19 15:50:50 +02:00
github-actions 518d61e039 chore(ios): update app-repo.json [skip ci] 2026-08-19 06:53:26 -04:00
bryanthaboi 4349a1142f Merge pull request #1554 from bryanthaboi/dev
adrian if ur reading this im ....
2026-08-19 06:44:22 -04:00
bryanthaboi 9713977755 Merge branch 'dev' of https://github.com/bryanthaboi/gen1recomp into dev 2026-08-19 06:41:12 -04:00
bryanthaboi 63448ca640 shamona 2026-08-19 06:41:10 -04:00
github-actions b36d38815f chore(ios): update app-repo.json [skip ci] 2026-08-19 06:21:12 -04:00
bryanthaboi e24f812475 Merge pull request #1553 from bryanthaboi/dev
fix stuff baby
2026-08-19 06:10:44 -04:00
bryanthaboi fddf619ed2 Merge pull request #1527 from thibautbus/fix/status-abbreviation-translation
Translate the status abbreviations shown outside battle
2026-08-19 06:01:14 -04:00
bryanthaboi bf83509ef2 Merge pull request #1542 from AverageConsumer/codex/gen2-ball-cache-invalidation
fix(gen2): refresh caches missing trainer HUD balls
2026-08-19 06:00:48 -04:00
bryanthaboi 6c05b854c4 Merge pull request #1543 from AverageConsumer/codex/gen2-party-grid-navigation
fix(gen2): honor battle party grid navigation
2026-08-19 06:00:21 -04:00
bryanthaboi 2baafab027 Merge pull request #1544 from castdrian/safe-mode-report-issue
feat(launcher): add safe mode and issue reporting
2026-08-19 06:00:00 -04:00
bryanthaboi 813f9d959b Merge pull request #1546 from 1Jamie/feat/android-exit-game-to-launcher
feat(android): add adaptive icons, dynamic shortcuts, in-process hot-swap, and exit-to-launcher
2026-08-19 05:59:34 -04:00
bryanthaboi fba87f028c Merge pull request #1552 from thibautbus/fix/pikachu-unhappy-gsub-crash
Fix a crash releasing your own caught Pikachu in Yellow
2026-08-19 05:59:06 -04:00
bryanthaboi cb4647daf0 Merge branch 'dev' of https://github.com/bryanthaboi/gen1recomp into dev 2026-08-19 05:57:46 -04:00
bryanthaboi 93374fbbbb skin studio updates, save sync CLOSES #1533 2026-08-19 05:57:44 -04:00
thibautbus abe176b26c Fix a crash releasing your own caught Pikachu in Yellow
BoxMenu.lua's release() pushes both its "Once released...OK?" prompt
and its Yellow-only "Pikachu looks unhappy" message through
TextBox.new(game, (t._X or Strings(...)):gsub(...)) -- gsub returns
two values (the text and a substitution count), and since the gsub
call is the last argument in the TextBox.new(...) call with nothing
after it, Lua expands both into the call: the count lands in
TextBox.new's third parameter, onDone. TextBox.lua later calls
onDone() once the box is dismissed; a number is not callable, so
every release of your own caught Pikachu in Yellow crashed --
regardless of its nickname (unlike the separate %-escape gsub bug,
this one needs no special save content, ordinary play reaches it
every time).

Fixed by wrapping the gsub call in an extra pair of parens, which
truncates it to its first return value only -- the same fix already
applied to the neighboring _OnceReleasedText/_MonWasReleasedText
lines on the (separate, unmerged) fix/route-more-messages-through-romtext
branch, where this exact bug shape was first noticed while adding a
third callsite with the same pattern.

tests/engine/pikachu_unhappy_release_crash.lua: registers a fake
Data.pokemon.PIKACHU cloned from the fixture species (ROM-free) so
the species == "PIKACHU" check can be exercised, drives the real
interactive release flow in Yellow on a mon owned by the player, and
confirms the crash. Verified failing pre-fix (exact same
"attempt to call field 'onDone' (a number value)" error) and passing
post-fix.
2026-08-19 11:34:20 +02:00
thibautbus 085180992d Cover the status abbreviation translation fix with a targeted test
Neither tests/parity_status_true_color.lua (SGB recolor rectangle) nor
tests/parity_party_icon_mirror.lua (icon mirroring) check the drawn
status text, so this fix had no coverage. Drive SummaryMenu:draw() and
PartyMenu:draw() with a mod-patched statuses registry and check the
patched label reaches Font.draw instead of the raw status id, plus a
vanilla case confirming the no-mod fallback is unchanged.

Also cover the hudLabel-shadowing bug directly through the real
Registry:patch (not a hand-built table): a label-only patch, the exact
shape a translation mod would send, must reach Status.hudLabelFor for
all five vanilla ids. Confirmed both regressions: reverting
src/ui/*.lua and src/battle/*.lua to dev's pre-fix content fails 2 of
the draw-site checks; reverting only the vanilla hudLabel removal in
Status.lua fails the 3 checks whose French label differs from English
(FRZ/BRN/SLP).
2026-08-19 08:08:20 +02:00
thibautbus 9984958193 Translate the status abbreviations shown outside battle
src/ui/SummaryMenu.lua:148 and src/ui/PartyMenu.lua:824 drew mon.status
(PSN/PAR/BRN/FRZ/SLP) as a bare literal, bypassing translation. Unlike
plain text, a mod translates status labels through the statuses content
registry (mod.content.statuses:patch(id, { label = value }), the same
registry src/battle/BattleState.lua:statusLabel already reads in battle.
Route both screens through the same lookup, extracted as
Status.hudLabelFor(statuses, id) and shared with BattleState:statusLabel
so the hudLabel-or-label fallback rule lives in one place, with the raw
status id kept as the fallback when no record overrides it.

Found along the way: Status.RECORDS' five vanilla entries duplicated
hudLabel = label ("FRZ", hudLabel = "FRZ", ...) for no functional
reason. Since hudLabelFor reads hudLabel before label, and
Registry:patch only overrides fields a mod actually passes, a
translation mod's label-only patch (the natural shape for a status
catalog carrying one string per id, with no separate hudLabel data to
patch) was silently shadowed by the untouched vanilla hudLabel -- the
translation was stored but never displayed, in or out of battle. This
affected BattleState:statusLabel too, before this change and
independently of it. Dropped the redundant hudLabel field from all
five vanilla records: it's declared optional in the schema, and
nothing in this codebase ever gives it a value different from label --
setting it here only recreated the shadowing trap for no observed
benefit. Left a comment above Status.RECORDS warning against
re-adding it.
2026-08-19 08:08:20 +02:00
1jamie 5871469002 fix(tests): avoid false positive Game: pattern match in skin_studio test 2026-08-18 21:16:32 -05:00
1jamie 302b2c9591 feat(android): add adaptive icons, dynamic shortcuts, in-process hot-swap, and exit-to-launcher 2026-08-18 20:37:25 -05:00
Adrian Castro 67a170fd6e feat(launcher): add safe mode and issue reporting 2026-08-19 00:44:30 +02:00
AverageConsumer 66079686fc fix(gen2): honor battle party grid navigation 2026-08-19 00:35:39 +02:00
AverageConsumer cc5ff987ac fix(gen2): refresh caches missing trainer HUD balls 2026-08-18 23:52:25 +02:00
github-actions f8ba51636b chore(ios): update app-repo.json [skip ci] 2026-08-18 17:18:36 -04:00
bryanthaboi fb97318e87 Merge pull request #1541 from bryanthaboi/dev
tuesday afternoon squashing
2026-08-18 17:08:46 -04:00
bryanthaboi 580449b8df Merge pull request #1538 from castdrian/ios-audio-stuff
fix(ios): recover audio after route changes
2026-08-18 17:02:10 -04:00
bryanthaboi fd73ab2a11 Merge branch 'dev' of https://github.com/bryanthaboi/gen1recomp into dev 2026-08-18 16:58:12 -04:00
bryanthaboi 48f710c3d6 Update bug_report.yml 2026-08-18 16:58:04 -04:00
bryanthaboi 794a5fc6cb Merge pull request #1540 from 1Jamie/fix/grass-standing-overdraw
fixes #1537 #1528  also fixes feet layering of gold with grass. Unifies gen1/gen2 grass layering into a single system
2026-08-18 16:55:39 -04:00
1jamie 286988a1e3 fixes #1537 #1528
also fixes feet layering of gold with grass. Unifies gen1/gen2 grass layering into a single system
2026-08-18 15:37:27 -05:00
Adrian Castro 48a3140a28 fix(ios): recover audio after route changes 2026-08-18 22:36:11 +02:00
bryanthaboi f7bdaa81f8 true color sprites shouldnt show in non color modes 2026-08-18 16:06:15 -04:00
bryanthaboi a1ab5e2cff title screen on gold going the wrong dir 2026-08-18 15:54:48 -04:00
bryanthaboi 99806ead25 Potentially CLOSES #1310, CLOSES #1005, CLOSES #1291 2026-08-18 15:49:47 -04:00
1jamie def967a8f8 fix(render): replace per-entity grass overdraw with full-screen cell pass
The previous drawCellBottom calls fired only for cells containing a
tracked entity.  While walking this worked acceptably because the
sprite's sub-pixel tween kept the visual overlap plausible, but while
standing still the sprite is pixel-aligned with the cell and the opaque
leaf-edge pixels in the grass bottom row paint over the player's feet.

This change removes the per-entity isGrassCell checks and replaces them
with a single post-sprite pass that overdraws every visible grass cell.

TileRenderer:
- ensureWindow now builds grassCells (all paths) and grassBatch (DMG/SGB
  shader path) alongside winBatch during the existing tile scan loop.
  A grassSeen table deduplicates cells so each cx/cy pair is only
  recorded once despite having two bottom-row tiles.
- drawGrassOverdraw: DMG/SGB draws the grassBatch under color0KeyShader
  in one call; GBC iterates grassCells and calls drawCellBottomRaw per
  cell (pre-keyed images can't share a SpriteBatch).
- markGrassOverdrawRedraw: iterates grassCells and calls
  markCellBottomRedraw for the post-zone OBP-replay pass (GBC only).
- releaseBatches cleans up grassBatch and grassCells.

OverworldController (flat path):
- Entity loop draws sprites only; grass overdraw fires once after the
  loop via drawGrassOverdraw + markGrassOverdrawRedraw.

OverworldController (tilt path):
- Grass cells are injected into the billboard sort queue keyed on the
  world-pixel foot of each cell's bottom tile row (cy*16+16), so they
  depth-sort correctly against entities at different y positions.  Each
  grass cell billboards via drawCellBottomRaw inside the upright pass.

Fixes: standing-in-tall-grass feet overdraw (Gen 2 confirmed, Gen 1
improved); NPCs and Pikachu follower in grass benefit automatically.
Parity test: tests/parity_grass_seam.lua 10/10, engine 228/228.
2026-08-18 13:23:10 -05:00
bryanthaboi 7583ba8729 CLOSES #1471 2026-08-18 14:19:42 -04:00
github-actions fc83ecd52f chore(ios): update app-repo.json [skip ci] 2026-08-18 12:05:11 -04:00
1392 changed files with 279802 additions and 148377 deletions
+1
View File
@@ -0,0 +1 @@
* @bryanthaboi
+16 -10
View File
@@ -5,6 +5,10 @@ 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.
@@ -51,22 +55,24 @@ body:
validations: validations:
required: true required: true
- type: dropdown - type: checkboxes
id: mods_enabled id: mods_off
attributes: attributes:
label: Were any mods on label: Mods off
description: Check the MODS tab in the launcher if you're not sure. description: >
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:
- "No" - label: I turned off all mods and can still reproduce this
- "Yes" required: true
validations:
required: true
- type: input - type: input
id: mods_which id: mods_which
attributes: attributes:
label: Which mods (if any were on) label: Which mods (if you first noticed this with any on)
description: List the enabled mods. Leave blank if none were on. description: >
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
@@ -1,118 +0,0 @@
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
@@ -1,130 +0,0 @@
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
+2 -8
View File
@@ -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/8IOgqbe4YvA/maxresdefault.jpg)](https://www.youtube.com/watch?v=8IOgqbe4YvA) [![Watch the latest update video](https://img.youtube.com/vi/yi7LkWQPKKM/maxresdefault.jpg)](https://youtu.be/yi7LkWQPKKM)
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,
@@ -332,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 and Ideas ## Bugs
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
@@ -341,12 +341,6 @@ 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)
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

+19
View File
@@ -5,8 +5,27 @@
-- 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,6 +168,7 @@ 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" },
+11 -1
View File
@@ -516,7 +516,17 @@ M.ROUTE_24 = {
push(game, text(game)._Route24CooltrainerM1YouCouldBecomeATopLeaderText, push(game, text(game)._Route24CooltrainerM1YouCouldBecomeATopLeaderText,
done) done)
else else
ow:engageTrainer(npc, done) -- scripts/Route24.asm:125
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
+5 -4
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) local function push(game, s, done, opts)
local TextBox = require("src.render.TextBox") local TextBox = require("src.render.TextBox")
game.stack:push(TextBox.new(game, s, done)) game.stack:push(TextBox.new(game, s, done, opts))
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,7 +236,6 @@ 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
@@ -244,7 +243,9 @@ M.CINNABAR_GYM = {
Sound.play(game.data, "Go_Inside") Sound.play(game.data, "Go_Inside")
end end
applyGymGates(game, ow) applyGymGates(game, ow)
end) end, { preSound = function()
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) local function push(game, text, done, opts)
local TextBox = require("src.render.TextBox") local TextBox = require("src.render.TextBox")
game.stack:push(TextBox.new(game, text, done)) game.stack:push(TextBox.new(game, text, done, opts))
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!",
function() done, { auto = { wait = true, delay = 0, sound = function()
require("src.core.Sound").playCry(game.data, "PIKACHU") return require("src.core.Sound").playCry(game.data, "PIKACHU")
done() end } })
end)
end, end,
TEXT_SUMMERBEACHHOUSE_POSTER1 = poster(1), TEXT_SUMMERBEACHHOUSE_POSTER1 = poster(1),
+3 -2
View File
@@ -512,8 +512,9 @@ 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.naming.grid`, `ui.pc.items`, `ui.options.rows`, `ui.party.submenu`, `ui.party.grid_navigation`,
`ui.list_menu`, `transition.style`. `ui.list_menu` covers Gold's script `ui.naming.grid`, `ui.pc.items`, `ui.list_menu`, `transition.style`.
`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.
+1 -2
View File
@@ -11,9 +11,8 @@ 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, with bezel art, per-button press states, and Super Game Boy borders * **Touch skins** in RetroArch overlay format and Delta `.deltaskin` (including PDF-wrapped bezel art), with per-button press states and Super Game Boy borders
* **Pokédex diploma and printer image exports** * **Pokédex diploma and printer image exports**
* **Mod download counts** from the index feed, with Most-downloaded and Trending sorts
## Gen 2 Specifics ## Gen 2 Specifics
+101 -18
View File
@@ -4,17 +4,23 @@ 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/launcher_skins_tab.lua`. `tests/engine/skin_format_import_test.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
Two load. `skin.lua` wins when a folder has both. Three load: the native `skin.lua`, a RetroArch overlay `.cfg`, and a Delta
`.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:
@@ -41,6 +47,15 @@ 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
@@ -71,6 +86,32 @@ 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`,
@@ -119,10 +160,22 @@ them. Anything that binds a button still follows the usual mobile /
## Installing ## Installing
Drop a folder or a `.zip` into `skins/` in the save directory, or drop a zip on Four roads, all of them landing in `skins/` in the save directory:
the launcher window while the Skins tab is open. A zip is mounted in place, so
there is nothing to unpack. The folder needs one `skin.lua` or `.cfg` * **Import** on the Skins tab opens the host file picker for a `.zip` or a
(`overlay.cfg` is preferred when there are several) and the images it names. `.deltaskin`.
* **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:
@@ -157,23 +210,40 @@ 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. X / Y / W / H are in canvas pixels, so a control can be typed to the resize. Arrow keys nudge the selection one canvas pixel, shift-arrow ten. While
coordinate its art was drawn at. Bind, hitbox shape, hit reach and idle and a control is dragged it snaps to the centres and edges of the other controls
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
buttons, the diagonal pairs, every hotkey, a few `key:` entries, and
decoration. The COMBINE chips at the top toggle one part at a time, which is
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 Each page can **Lock** to portrait or landscape. With **Match canvas** on
(the default), Next page picks a matching mock device and the canvas preset (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 picks a matching page. Turn Match canvas off to look at a portrait page on a
landscape device. 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 A RetroArch overlay whose pages are already named portrait / landscape
(the auto-rotate convention) locks those pages and turns Match canvas on (the auto-rotate convention) locks those pages and turns Match canvas on
when you open it. You do not have to click Lock first. when you open it. You do not have to click Lock first.
**Art.** The **Bezel**, **Idle art** and **Pressed art** rows cycle through the **Art.** The **Bezel**, **Idle art** and **Pressed art** rows open a
images already in the skin folder; the **Import** button beside each one opens thumbnail grid of the images already in the skin folder, with `(none)` first;
the host file picker (`src/core/FilePicker.lua`: osascript, PowerShell, 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
@@ -185,11 +255,24 @@ 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** packs it as one zip skin names, so the folder stands alone. **Export** offers three formats, and
(`src/core/SkinZip.lua`, store-only) carrying the native `skin.lua`, the the Skins tab's gear offers the same three for any installed skin:
images, and the original `.cfg` when it came from one. An exported skin drops
straight back into `skins/` and still opens in RetroArch. | Export | Contents |
| --- | --- |
| 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
RetroArch's `analog_*`, `dpad_area`, `abxy_area` and `retrok_*` desc types. True vector Delta skins (PDF artwork with no embedded JPEG). Those still need
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.
+65
View File
@@ -0,0 +1,65 @@
# 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.
+122 -15
View File
@@ -291,6 +291,78 @@ 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.
@@ -382,7 +454,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 Game:applyOptions runs at boot. -- the launcher would rotate freely until options are applied 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())
@@ -442,8 +514,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. Game:load replaces this with the -- translation reaches the launcher too. The active game's loader replaces
-- real merged catalog once a version boots. -- this with the 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
@@ -484,17 +556,7 @@ 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 = RomImporter.new(function(version) Importer = makeLauncher()
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)
@@ -808,6 +870,46 @@ 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
@@ -1013,11 +1115,16 @@ 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 not launchedIntoGame and (isAndroid or 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,7 +29,8 @@
<application <application
android:allowBackup="true" android:allowBackup="true"
android:icon="@drawable/love" android:icon="@mipmap/ic_launcher"
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"
@@ -39,7 +40,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="singleInstance" android:launchMode="singleTask"
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.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

@@ -0,0 +1,5 @@
<?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>
@@ -0,0 +1,5 @@
<?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,4 +3,9 @@
<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,6 +45,11 @@
// 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
@@ -278,6 +283,70 @@ 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)
@@ -374,6 +443,104 @@ 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
@@ -1320,4 +1487,98 @@ 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,6 +90,16 @@ 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
@@ -106,6 +116,21 @@ 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,6 +22,7 @@
#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>
@@ -30,6 +31,10 @@
#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
@@ -37,9 +42,35 @@ namespace audio
namespace openal namespace openal
{ {
Audio::PoolThread::PoolThread(Pool *pool) static const int DISCONNECT_CHECK_INTERVAL = 200;
: 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";
} }
@@ -51,6 +82,8 @@ Audio::PoolThread::~PoolThread()
void Audio::PoolThread::threadFunction() void Audio::PoolThread::threadFunction()
{ {
int disconnectCheck = 0;
while (true) while (true)
{ {
{ {
@@ -61,7 +94,23 @@ 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);
} }
} }
@@ -72,6 +121,11 @@ 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)
@@ -99,6 +153,8 @@ 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.
@@ -189,13 +245,6 @@ 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
@@ -205,6 +254,13 @@ 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()
@@ -314,6 +370,9 @@ 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);
@@ -350,6 +409,52 @@ 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,6 +22,7 @@
#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>
@@ -97,6 +98,8 @@ 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;
@@ -155,6 +158,7 @@ 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.
@@ -162,13 +166,16 @@ 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(Pool *pool); PoolThread(Audio *audio, Pool *pool);
virtual ~PoolThread(); virtual ~PoolThread();
void setFinish(); void setFinish();
void setPaused(bool paused);
void threadFunction(); void threadFunction();
}; };
@@ -177,6 +184,13 @@ 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);
@@ -188,6 +202,9 @@ 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,6 +245,25 @@ 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
{ {
@@ -274,6 +293,27 @@ 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,6 +143,9 @@ 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
@@ -159,6 +162,18 @@ 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,6 +22,9 @@
#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
@@ -150,6 +153,57 @@ 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());
@@ -229,6 +283,34 @@ 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 },
@@ -243,8 +325,11 @@ 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 },
@@ -0,0 +1,40 @@
---
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
@@ -0,0 +1,36 @@
# 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"
@@ -0,0 +1,38 @@
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
@@ -0,0 +1,24 @@
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,3 +4,5 @@
.cxx/ .cxx/
.idea .idea
build build
.logpile
+14 -17
View File
@@ -1,32 +1,21 @@
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++14 LOCAL_CPPFLAGS := -std=c++17
#
# 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 \
@@ -36,8 +25,11 @@ 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 \
@@ -45,17 +37,26 @@ 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 \
@@ -75,10 +76,6 @@ 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,6 +9,7 @@ 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
@@ -18,26 +19,38 @@ 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
src/fifo/FifoController.cpp src/fifo/FifoController.cpp
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
@@ -70,18 +83,23 @@ target_include_directories(oboe
# Enable -Ofast # Enable -Ofast
target_compile_options(oboe target_compile_options(oboe
PRIVATE PRIVATE
-std=c++14 -std=c++17
-Wall -Wall
-Wextra-semi -Wextra-semi
-Wshadow -Wshadow
-Wshadow-field -Wshadow-field
-Ofast "$<$<CONFIG:RELEASE>:-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
@@ -89,4 +107,4 @@ install(TARGETS oboe
ARCHIVE DESTINATION lib/${ANDROID_ABI}) ARCHIVE DESTINATION lib/${ANDROID_ABI})
# Also install the headers # Also install the headers
install(DIRECTORY include/oboe DESTINATION include) install(DIRECTORY include/oboe DESTINATION include)
@@ -1 +0,0 @@
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 = 1.2 PROJECT_NUMBER =
# 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
@@ -1 +0,0 @@
Please see the README.md file for more information.
+14 -13
View File
@@ -1,4 +1,4 @@
# Oboe [![Build Status](https://travis-ci.org/google/oboe.svg?branch=master)](https://travis-ci.org/google/oboe) # Oboe [![Build CI](https://github.com/google/oboe/workflows/Build%20CI/badge.svg)](https://github.com/google/oboe/actions)
[![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,35 +9,36 @@ 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
- [Used by popular apps and frameworks](docs/AppsUsingOboe.md) - Workarounds for some known issues
- [Used by popular apps and frameworks](https://github.com/google/oboe/wiki/AppsUsingOboe)
## Requirements ## Documentation
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/reference) - [API reference](https://google.github.io/oboe)
- [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.](https://github.com/google/oboe/tree/master/apps/OboeTester/docs) - [**OboeTester** app for measuring latency, glitches, etc.](apps/OboeTester/docs)
- [Oboe unit tests](https://github.com/google/oboe/tree/master/tests) - [Oboe unit tests](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://codelabs.developers.google.com/codelabs/musicalgame-using-oboe/index.html#0). - Also check out the [Rhythm Game codelab](https://developer.android.com/codelabs/musicalgame-using-oboe?hl=en#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,6 +6,8 @@
/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,11 +1,14 @@
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++14") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror -Wall -std=c++17 -fvisibility=hidden")
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
@@ -30,5 +33,4 @@ 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,18 +1,17 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
android { android {
compileSdkVersion 28 compileSdkVersion 34
defaultConfig { defaultConfig {
applicationId = "com.google.sample.oboe.manualtest" applicationId = "com.mobileer.oboetester"
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 28 targetSdkVersion 34
// Also update the version in the AndroidManifest.xml file. versionCode 91
versionCode 32 versionName "2.7.2"
versionName "1.5.24"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild { externalNativeBuild {
cmake { cmake {
cppFlags "-std=c++14" cppFlags "-std=c++17"
abiFilters "x86", "x86_64", "armeabi-v7a", "arm64-v8a" abiFilters "x86", "x86_64", "armeabi-v7a", "arm64-v8a"
} }
} }
@@ -31,14 +30,15 @@ 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 'com.android.support.constraint:constraint-layout:2.0.0-beta4' implementation "androidx.core:core-ktx:1.9.0"
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.appcompat:appcompat:1.6.1'
testImplementation 'junit:junit:4.13-beta-3' androidTestImplementation 'androidx.test.ext:junit:1.1.5'
implementation 'com.android.support:appcompat-v7:28.0.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'com.android.support.test:runner:1.0.2' }
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
@@ -1,100 +1,138 @@
<?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">
package="com.google.sample.oboe.manualtest" <uses-feature
android:versionCode="32" android:name="android.hardware.microphone"
android:versionName="1.5.24"> android:required="false" />
<!-- versionCode and versionName also have to be updated in build.gradle --> <uses-feature
android:name="android.hardware.audio.output"
<uses-feature android:name="android.hardware.microphone" android:required="true" /> android:required="true" />
<uses-feature android:name="android.hardware.audio.output" android:required="true" /> <uses-feature
<uses-feature android:name="android.software.midi" android:required="true" /> android:name="android.hardware.touchscreen"
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.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<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="com.google.sample.oboe.manualtest.MainActivity" android:name=".MainActivity"
android:launchMode="singleTask" android:launchMode="singleTask"
android:label="@string/app_name" android:screenOrientation="portrait"
android:screenOrientation="portrait"> android:exported="true">
<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 <activity
android:name="com.google.sample.oboe.manualtest.TestOutputActivity" android:name=".TestOutputActivity"
android:label="@string/title_activity_test_output" android:label="@string/title_activity_test_output"
android:screenOrientation="portrait"> android:screenOrientation="portrait" />
</activity>
<activity <activity
android:name="com.google.sample.oboe.manualtest.TestInputActivity" android:name=".TestInputActivity"
android:label="@string/title_activity_test_input" android:label="@string/title_activity_test_input"
android:screenOrientation="portrait"> android:screenOrientation="portrait" />
</activity>
<activity <activity
android:name="com.google.sample.oboe.manualtest.TapToToneActivity" android:name=".TapToToneActivity"
android:label="@string/title_activity_output_latency" android:label="@string/title_activity_output_latency"
android:screenOrientation="portrait"> android:screenOrientation="portrait" />
</activity>
<activity <activity
android:name="com.google.sample.oboe.manualtest.RecorderActivity" android:name=".RecorderActivity"
android:label="@string/title_activity_recorder" android:label="@string/title_activity_recorder"
android:screenOrientation="portrait"> android:screenOrientation="portrait" />
</activity>
<activity <activity
android:name="com.google.sample.oboe.manualtest.EchoActivity" android:name=".EchoActivity"
android:label="@string/title_activity_echo" android:label="@string/title_activity_echo"
android:screenOrientation="portrait"> android:screenOrientation="portrait" />
</activity>
<activity <activity
android:name="com.google.sample.oboe.manualtest.RoundTripLatencyActivity" android:name=".RoundTripLatencyActivity"
android:label="@string/title_activity_rt_latency" android:label="@string/title_activity_rt_latency"
android:screenOrientation="portrait"> android:screenOrientation="portrait" />
</activity>
<activity <activity
android:name="com.google.sample.oboe.manualtest.ManualGlitchActivity" android:name=".ManualGlitchActivity"
android:label="@string/title_activity_glitches" android:label="@string/title_activity_glitches"
android:screenOrientation="portrait"> android:screenOrientation="portrait" />
</activity>
<activity <activity
android:name="com.google.sample.oboe.manualtest.AutoGlitchActivity" android:name=".AutomatedGlitchActivity"
android:label="@string/title_activity_glitches" android:label="@string/title_activity_auto_glitches"
android:screenOrientation="portrait"> android:screenOrientation="portrait" />
</activity>
<activity <activity
android:name="com.google.sample.oboe.manualtest.TestDisconnectActivity" android:name=".TestDisconnectActivity"
android:label="@string/title_test_disconnect" android:label="@string/title_test_disconnect"
android:screenOrientation="portrait"> android:screenOrientation="portrait" />
</activity> <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
android:name=".ExternalTapToToneActivity"
android:label="@string/title_external_tap"
android:exported="true"
android:screenOrientation="portrait" />
<activity
android:name=".TestPlugLatencyActivity"
android:label="@string/title_plug_latency"
android:exported="true"
android:screenOrientation="portrait" />
<activity
android:name=".TestErrorCallbackActivity"
android:label="@string/title_error_callback"
android:exported="true"
android:screenOrientation="portrait" />
<activity
android:name=".TestRouteDuringCallbackActivity"
android:label="@string/title_route_during_callback"
android:exported="true"
android:screenOrientation="portrait" />
<activity
android:name=".DynamicWorkloadActivity"
android:label="@string/title_dynamic_load"
android:exported="true"
android:screenOrientation="portrait" />
<activity
android:name=".TestColdStartLatencyActivity"
android:label="@string/title_cold_start_latency"
android:exported="true"
android:screenOrientation="portrait" />
<activity
android:name=".TestRapidCycleActivity"
android:label="@string/title_rapid_cycle"
android:exported="true"
android:screenOrientation="portrait" />
<service <service
android:name="com.google.sample.oboe.manualtest.AudioMidiTester" android:name=".MidiTapTester"
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>
@@ -104,16 +142,21 @@
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="android.support.v4.content.FileProvider" android:name="androidx.core.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,29 +17,24 @@
#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 flowgraph; using namespace oboe::flowgraph;
oboe::DataCallbackResult AudioStreamGateway::onAudioReady( oboe::DataCallbackResult AudioStreamGateway::onAudioReady(
oboe::AudioStream *audioStream, oboe::AudioStream *audioStream,
void *audioData, void *audioData,
int numFrames) { int numFrames) {
if (!mSchedulerChecked) { maybeHang(getNanoseconds());
mScheduler = sched_getscheduler(gettid()); printScheduler();
mSchedulerChecked = true;
}
if (mAudioSink != nullptr) { if (mAudioSink != nullptr) {
mAudioSink->read(mFramePosition, audioData, numFrames); mAudioSink->read(audioData, numFrames);
mFramePosition += numFrames;
} }
return oboe::DataCallbackResult::Continue; return oboe::DataCallbackResult::Continue;
} }
int AudioStreamGateway::getScheduler() {
return mScheduler;
}
@@ -21,24 +21,21 @@
#include "flowgraph/FlowGraphNode.h" #include "flowgraph/FlowGraphNode.h"
#include "oboe/Oboe.h" #include "oboe/Oboe.h"
#include "OboeTesterStreamCallback.h"
using namespace flowgraph; using namespace oboe::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 oboe::AudioStreamCallback { class AudioStreamGateway : public OboeTesterStreamCallback {
public: public:
// AudioStreamGateway(int samplesPerFrame);
virtual ~AudioStreamGateway() = default; virtual ~AudioStreamGateway() = default;
void setAudioSink(std::shared_ptr<flowgraph::FlowGraphSink> sink) { void setAudioSink(std::shared_ptr<oboe::flowgraph::FlowGraphSink> sink) {
mAudioSink = sink; mAudioSink = sink;
if (sink) {
mFramePosition = sink->getLastFramePosition();
}
} }
/** /**
@@ -49,13 +46,9 @@ public:
void *audioData, void *audioData,
int numFrames) override; int numFrames) override;
int getScheduler();
private: private:
int64_t mFramePosition = 0;
bool mSchedulerChecked = false; std::shared_ptr<oboe::flowgraph::FlowGraphSink> mAudioSink;
int mScheduler;
std::shared_ptr<flowgraph::FlowGraphSink> mAudioSink;
}; };
@@ -0,0 +1,91 @@
/*
* 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);
}
@@ -0,0 +1,102 @@
/*
* 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,28 +19,39 @@
oboe::Result FullDuplexAnalyzer::start() { oboe::Result FullDuplexAnalyzer::start() {
getLoopbackProcessor()->setSampleRate(getOutputStream()->getSampleRate()); getLoopbackProcessor()->setSampleRate(getOutputStream()->getSampleRate());
getLoopbackProcessor()->onStartTest(); getLoopbackProcessor()->prepareToTest();
return FullDuplexStream::start(); mWriteReadDeltaValid = false;
return FullDuplexStreamWithConversion::start();
} }
oboe::DataCallbackResult FullDuplexAnalyzer::onBothStreamsReady( oboe::DataCallbackResult FullDuplexAnalyzer::onBothStreamsReadyFloat(
const void *inputData, const float *inputData,
int numInputFrames, int numInputFrames,
void *outputData, float *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();
float *inputFloat = (float *) inputData; auto *inputFloat = static_cast<const float *>(inputData);
float *outputFloat = (float *) outputData; float *outputFloat = 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);
// write the first channel of output and input to the stereo recorder // Save data for later analysis or for writing to a WAVE file.
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;
@@ -48,14 +59,15 @@ oboe::DataCallbackResult FullDuplexAnalyzer::onBothStreamsReady(
inputFloat += inputStride; inputFloat += inputStride;
mRecording->write(buffer, 1); mRecording->write(buffer, 1);
} }
// Handle mismatch in in numFrames. // Handle mismatch in numFrames.
buffer[0] = 0.0f; // gap in output const float gapMarker = -0.9f; // Recognizable value so we can tell underruns from DSP gaps.
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] = 0.0f; // gap in input buffer[1] = gapMarker; // 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,40 +21,52 @@
#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 FullDuplexStream { class FullDuplexAnalyzer : public FullDuplexStreamWithConversion {
public: public:
FullDuplexAnalyzer() {} FullDuplexAnalyzer(LoopbackProcessor *processor)
: 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 onBothStreamsReady( oboe::DataCallbackResult onBothStreamsReadyFloat(
const void *inputData, const float *inputData,
int numInputFrames, int numInputFrames,
void *outputData, float *outputData,
int numOutputFrames int numOutputFrames
) override; ) override;
oboe::Result start() override; oboe::Result start() override;
bool isDone() { LoopbackProcessor *getLoopbackProcessor() {
return false; return mLoopbackProcessor;
} }
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,28 +20,46 @@
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);
return FullDuplexStream::start(); // Use peak detector for input streams
mNumChannels = getInputStream()->getChannelCount();
mPeakDetectors = std::make_unique<PeakDetector[]>(mNumChannels);
return FullDuplexStreamWithConversion::start();
} }
oboe::DataCallbackResult FullDuplexEcho::onBothStreamsReady( double FullDuplexEcho::getPeakLevel(int index) {
const void *inputData, 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.load());
return -2.0;
}
return mPeakDetectors[index].getLevel();
}
oboe::DataCallbackResult FullDuplexEcho::onBothStreamsReadyFloat(
const float *inputData,
int numInputFrames, int numInputFrames,
void *outputData, float *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);
float *inputFloat = (float *)inputData; auto *inputFloat = const_cast<float *>(inputData);
float *outputFloat = (float *)outputData; float *outputFloat = outputData;
// zero out entire output array // zero out entire output array
memset(outputFloat, 0, numOutputFrames * getOutputStream()->getBytesPerFrame()); memset(outputFloat, 0, static_cast<size_t>(numOutputFrames)
* 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,28 +21,31 @@
#include <sys/types.h> #include <sys/types.h>
#include "oboe/Oboe.h" #include "oboe/Oboe.h"
#include "FullDuplexStream.h" #include "analyzer/LatencyAnalyzer.h"
#include "FullDuplexStreamWithConversion.h"
#include "InterpolatingDelayLine.h" #include "InterpolatingDelayLine.h"
class FullDuplexEcho : public FullDuplexStream { class FullDuplexEcho : public FullDuplexStreamWithConversion {
public: public:
FullDuplexEcho() { FullDuplexEcho() {
setMNumInputBurstsCushion(0); setNumInputBurstsCushion(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 onBothStreamsReady( oboe::DataCallbackResult onBothStreamsReadyFloat(
const void *inputData, const float *inputData,
int numInputFrames, int numInputFrames,
void *outputData, float *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;
} }
@@ -51,6 +54,9 @@ 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,53 +0,0 @@
/*
* 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
@@ -1,44 +0,0 @@
/*
* 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;
};
@@ -1,65 +0,0 @@
/*
* 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
@@ -1,137 +0,0 @@
/*
* 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;
}
@@ -1,115 +0,0 @@
/*
* 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
@@ -0,0 +1,61 @@
/*
* 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;
}
@@ -0,0 +1,61 @@
/*
* 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,37 +17,35 @@
#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();
if (audioStream->getFormat() == oboe::AudioFormat::I16) { maybeHang(getNanoseconds());
int16_t *shortData = (int16_t *) audioData; printScheduler();
if (mRecording != nullptr) { mInputConverter->convertToInternalOutput(numFrames * channelCount, audioData);
mRecording->write(shortData, numFrames); float *floatData = (float *) mInputConverter->getOutputBuffer();
} if (mRecording != nullptr) {
int16_t *frameData = shortData; mRecording->write(floatData, numFrames);
for (int iFrame = 0; iFrame < numFrames; iFrame++) { }
for (int iChannel = 0; iChannel < channelCount; iChannel++) { int32_t sampleIndex = 0;
float sample = frameData[iChannel] / 32768.0f; for (int iFrame = 0; iFrame < numFrames; iFrame++) {
mPeakDetectors[iChannel].process(sample); for (int iChannel = 0; iChannel < channelCount; iChannel++) {
} float sample = floatData[sampleIndex++];
frameData += channelCount; mPeakDetectors[iChannel].process(sample);
}
} else if (audioStream->getFormat() == oboe::AudioFormat::Float) {
float *floatData = (float *) audioData;
if (mRecording != nullptr) {
mRecording->write(floatData, numFrames);
}
float *frameData = floatData;
for (int iFrame = 0; iFrame < numFrames; iFrame++) {
for (int iChannel = 0; iChannel < channelCount; iChannel++) {
float sample = frameData[iChannel];
mPeakDetectors[iChannel].process(sample);
}
frameData += channelCount;
} }
} }
@@ -23,18 +23,31 @@
// TODO #include "flowgraph/FlowGraph.h" // TODO #include "flowgraph/FlowGraph.h"
#include "oboe/Oboe.h" #include "oboe/Oboe.h"
#include "MultiChannelRecording.h"
#include "analyzer/PeakDetector.h" #include "analyzer/PeakDetector.h"
#include "FormatConverterBox.h"
#include "MultiChannelRecording.h"
#include "OboeTesterStreamCallback.h"
constexpr int kMaxInputChannels = 8; class InputStreamCallbackAnalyzer : public OboeTesterStreamCallback {
class InputStreamCallbackAnalyzer : public oboe::AudioStreamCallback {
public: public:
void reset() { void reset() {
for (auto detector : mPeakDetectors) { for (int iChannel = 0; iChannel < mNumChannels; iChannel++) {
detector.reset(); mPeakDetectors[iChannel].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);
} }
/** /**
@@ -49,9 +62,7 @@ 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;
@@ -62,11 +73,13 @@ public:
} }
public: public:
PeakDetector mPeakDetectors[kMaxInputChannels]; int32_t mNumChannels = 0;
MultiChannelRecording *mRecording = nullptr; std::unique_ptr<PeakDetector[]> mPeakDetectors;
MultiChannelRecording *mRecording = nullptr;
private: private:
int32_t mMinimumFramesBeforeRead = 0; std::unique_ptr<FormatConverterBox> mInputConverter;
int32_t mMinimumFramesBeforeRead = 0;
}; };
#endif //NATIVEOBOE_INPUTSTREAMCALLBACKANALYZER_H #endif //NATIVEOBOE_INPUTSTREAMCALLBACKANALYZER_H
@@ -14,7 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
#include "common/OboeDebug.h" #include <algorithm>
#include "InterpolatingDelayLine.h" #include "InterpolatingDelayLine.h"
InterpolatingDelayLine::InterpolatingDelayLine(int32_t delaySize) { InterpolatingDelayLine::InterpolatingDelayLine(int32_t delaySize) {
@@ -21,9 +21,6 @@
#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,10 +14,20 @@
* 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"
@@ -57,9 +67,9 @@ private:
bool ActivityContext::mUseCallback = true; bool ActivityContext::mUseCallback = true;
int ActivityContext::callbackSize = 0; int ActivityContext::callbackSize = 0;
oboe::AudioStream * ActivityContext::getOutputStream() { std::shared_ptr<oboe::AudioStream> ActivityContext::getOutputStream() {
for (auto entry : mOboeStreams) { for (auto entry : mOboeStreams) {
oboe::AudioStream *oboeStream = entry.second.get(); std::shared_ptr<oboe::AudioStream> oboeStream = entry.second;
if (oboeStream->getDirection() == oboe::Direction::Output) { if (oboeStream->getDirection() == oboe::Direction::Output) {
return oboeStream; return oboeStream;
} }
@@ -67,9 +77,9 @@ oboe::AudioStream * ActivityContext::getOutputStream() {
return nullptr; return nullptr;
} }
oboe::AudioStream * ActivityContext::getInputStream() { std::shared_ptr<oboe::AudioStream> ActivityContext::getInputStream() {
for (auto entry : mOboeStreams) { for (auto entry : mOboeStreams) {
oboe::AudioStream *oboeStream = entry.second.get(); std::shared_ptr<oboe::AudioStream> oboeStream = entry.second;
if (oboeStream != nullptr) { if (oboeStream != nullptr) {
if (oboeStream->getDirection() == oboe::Direction::Input) { if (oboeStream->getDirection() == oboe::Direction::Input) {
return oboeStream; return oboeStream;
@@ -88,9 +98,19 @@ 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();
oboe::AudioStream *oboeStream = getStream(streamIndex); std::shared_ptr<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);
@@ -99,19 +119,18 @@ void ActivityContext::close(int32_t streamIndex) {
} }
bool ActivityContext::isMMapUsed(int32_t streamIndex) { bool ActivityContext::isMMapUsed(int32_t streamIndex) {
oboe::AudioStream *oboeStream = getStream(streamIndex); std::shared_ptr<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); return AAudioExtensions::getInstance().isMMapUsed(oboeStream.get());
} }
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) {
oboe::AudioStream *oboeStream = entry.second.get(); std::shared_ptr<oboe::AudioStream> oboeStream = entry.second;
result = oboeStream->requestPause(); result = oboeStream->requestPause();
printScheduler();
} }
return result; return result;
} }
@@ -120,9 +139,8 @@ 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) {
oboe::AudioStream *oboeStream = entry.second.get(); std::shared_ptr<oboe::AudioStream> oboeStream = entry.second;
result = oboeStream->requestStop(); result = oboeStream->requestStop();
printScheduler();
} }
return result; return result;
} }
@@ -130,23 +148,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) {
LOGD("ActivityContext::open() set callback to use oboeCallbackProxy, callback size = %d", builder.setDataCallback(&oboeCallbackProxy);
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,
@@ -182,6 +200,9 @@ 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)
@@ -190,7 +211,13 @@ 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);
@@ -199,6 +226,12 @@ 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);
@@ -215,7 +248,7 @@ int ActivityContext::open(jint nativeApi,
createRecording(); createRecording();
finishOpen(isInput, oboeStream.get()); finishOpen(isInput, oboeStream);
} }
if (!mUseCallback) { if (!mUseCallback) {
@@ -223,20 +256,24 @@ int ActivityContext::open(jint nativeApi,
dataBuffer = std::make_unique<float[]>(numSamples); dataBuffer = std::make_unique<float[]>(numSamples);
} }
return (result != Result::OK) ? (int)result : streamIndex; if (result != Result::OK) {
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;
oboe::AudioStream *inputStream = getInputStream(); std::shared_ptr<oboe::AudioStream> inputStream = getInputStream();
oboe::AudioStream *outputStream = getOutputStream(); std::shared_ptr<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
} }
configureForStart(); audioStreamGateway.reset();
result = startStreams(); result = startStreams();
if (!mUseCallback && result == oboe::Result::OK) { if (!mUseCallback && result == oboe::Result::OK) {
@@ -245,6 +282,29 @@ 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;
} }
@@ -259,10 +319,11 @@ 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();
@@ -283,13 +344,25 @@ 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) {
@@ -313,6 +386,9 @@ 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;
} }
@@ -321,13 +397,20 @@ void ActivityTestOutput::setChannelEnabled(int channelIndex, bool enabled) {
} }
} }
void ActivityTestOutput::configureForStart() { void ActivityTestOutput::configureAfterOpen() {
manyToMulti = std::make_unique<ManyToMultiConverter>(mChannelCount); manyToMulti = std::make_unique<ManyToMultiConverter>(mChannelCount);
mSinkFloat = std::make_unique<SinkFloat>(mChannelCount); std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
mSinkI16 = std::make_unique<SinkI16>(mChannelCount);
oboe::AudioStream *outputStream = getOutputStream(); mVolumeRamp = std::make_shared<RampLinear>(mChannelCount);
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);
@@ -344,35 +427,52 @@ void ActivityTestOutput::configureForStart() {
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);
} }
} }
manyToMulti->output.connect(&(mSinkFloat.get()->input)); mWhiteNoise.amplitude.setValue(0.5);
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() {
oboe::AudioStream *outputStream = getOutputStream(); std::shared_ptr<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.setCallback(&audioStreamGateway); oboeCallbackProxy.setDataCallback(&audioStreamGateway);
} }
} }
@@ -380,7 +480,7 @@ void ActivityTestOutput::runBlockingIO() {
int32_t framesPerBlock = getFramesPerBlock(); int32_t framesPerBlock = getFramesPerBlock();
oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Continue; oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Continue;
oboe::AudioStream *oboeStream = getOutputStream(); std::shared_ptr<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;
@@ -389,7 +489,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, callbackResult = audioStreamGateway.onAudioReady(oboeStream.get(),
dataBuffer.get(), dataBuffer.get(),
framesPerBlock); framesPerBlock);
@@ -409,11 +509,22 @@ 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::configureForStart() { void ActivityTestInput::configureAfterOpen() {
mInputAnalyzer.reset(); mInputAnalyzer.reset();
if (mUseCallback) { if (mUseCallback) {
oboeCallbackProxy.setCallback(&mInputAnalyzer); oboeCallbackProxy.setDataCallback(&mInputAnalyzer);
} }
mInputAnalyzer.setRecording(mRecording.get()); mInputAnalyzer.setRecording(mRecording.get());
} }
@@ -422,7 +533,7 @@ void ActivityTestInput::runBlockingIO() {
int32_t framesPerBlock = getFramesPerBlock(); int32_t framesPerBlock = getFramesPerBlock();
oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Continue; oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Continue;
oboe::AudioStream *oboeStream = getInputStream(); std::shared_ptr<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;
@@ -450,7 +561,7 @@ void ActivityTestInput::runBlockingIO() {
} }
// analyze input // analyze input
callbackResult = mInputAnalyzer.onAudioReady(oboeStream, callbackResult = mInputAnalyzer.onAudioReady(oboeStream.get(),
dataBuffer.get(), dataBuffer.get(),
framesRead); framesRead);
} }
@@ -474,8 +585,7 @@ 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;
@@ -491,13 +601,15 @@ oboe::Result ActivityRecording::startPlayback() {
} }
// ======================================================================= ActivityTapToTone // ======================================================================= ActivityTapToTone
void ActivityTapToTone::configureForStart() { void ActivityTapToTone::configureAfterOpen() {
monoToMulti = std::make_unique<MonoToMultiConverter>(mChannelCount); monoToMulti = std::make_unique<MonoToMultiConverter>(mChannelCount);
mSinkFloat = std::make_unique<SinkFloat>(mChannelCount); mSinkFloat = std::make_shared<SinkFloat>(mChannelCount);
mSinkI16 = std::make_unique<SinkI16>(mChannelCount); mSinkI16 = std::make_shared<SinkI16>(mChannelCount);
mSinkI24 = std::make_shared<SinkI24>(mChannelCount);
mSinkI32 = std::make_shared<SinkI32>(mChannelCount);
oboe::AudioStream *outputStream = getOutputStream(); std::shared_ptr<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);
@@ -505,20 +617,26 @@ void ActivityTapToTone::configureForStart() {
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();
} }
// ======================================================================= ActivityRoundTripLatency // ======================================================================= ActivityFullDuplex
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.
oboe::AudioStream *outputStream = getOutputStream(); std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
if (outputStream != nullptr) { if (outputStream != nullptr) {
// Make sure the capacity is bigger than two bursts. // The input and output buffers will run in sync with input empty
int32_t burst = outputStream->getFramesPerBurst(); // and output full. So set the input capacity to match the output.
builder.setBufferCapacityInFrames(2 * burst); builder.setBufferCapacityInFrames(outputStream->getBufferCapacityInFrames());
} }
} }
} }
@@ -532,15 +650,16 @@ 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(mFullDuplexEcho.get()); builder.setCallback((oboe::AudioStreamCallback *) &oboeCallbackProxy);
oboeCallbackProxy.setDataCallback(mFullDuplexEcho.get());
} }
} }
void ActivityEcho::finishOpen(bool isInput, oboe::AudioStream *oboeStream) { void ActivityEcho::finishOpen(bool isInput, std::shared_ptr<oboe::AudioStream> &oboeStream) {
if (isInput) { if (isInput) {
mFullDuplexEcho->setInputStream(oboeStream); mFullDuplexEcho->setSharedInputStream(oboeStream);
} else { } else {
mFullDuplexEcho->setOutputStream(oboeStream); mFullDuplexEcho->setSharedOutputStream(oboeStream);
} }
} }
@@ -549,45 +668,102 @@ 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<FullDuplexLatency>(); mFullDuplexLatency = std::make_unique<FullDuplexAnalyzer>(mLatencyAnalyzer.get());
} }
if (!isInput) { if (!isInput) {
// only output uses a callback, input is polled // only output uses a callback, input is polled
builder.setCallback(mFullDuplexLatency.get()); builder.setCallback((oboe::AudioStreamCallback *) &oboeCallbackProxy);
oboeCallbackProxy.setDataCallback(mFullDuplexLatency.get());
} }
} }
void ActivityRoundTripLatency::finishOpen(bool isInput, oboe::AudioStream *oboeStream) { void ActivityRoundTripLatency::finishOpen(bool isInput, std::shared_ptr<oboe::AudioStream>
&oboeStream) {
if (isInput) { if (isInput) {
mFullDuplexLatency->setInputStream(oboeStream); mFullDuplexLatency->setSharedInputStream(oboeStream);
mFullDuplexLatency->setRecording(mRecording.get()); mFullDuplexLatency->setRecording(mRecording.get());
} else { } else {
mFullDuplexLatency->setOutputStream(oboeStream); mFullDuplexLatency->setSharedOutputStream(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<FullDuplexGlitches>(); mFullDuplexGlitches = std::make_unique<FullDuplexAnalyzer>(&mGlitchAnalyzer);
} }
if (!isInput) { if (!isInput) {
// only output uses a callback, input is polled // only output uses a callback, input is polled
builder.setCallback(mFullDuplexGlitches.get()); builder.setCallback((oboe::AudioStreamCallback *) &oboeCallbackProxy);
oboeCallbackProxy.setDataCallback(mFullDuplexGlitches.get());
} }
} }
void ActivityGlitches::finishOpen(bool isInput, oboe::AudioStream *oboeStream) { void ActivityGlitches::finishOpen(bool isInput, std::shared_ptr<oboe::AudioStream> &oboeStream) {
if (isInput) { if (isInput) {
mFullDuplexGlitches->setInputStream(oboeStream); mFullDuplexGlitches->setSharedInputStream(oboeStream);
mFullDuplexGlitches->setRecording(mRecording.get()); mFullDuplexGlitches->setRecording(mRecording.get());
} else { } else {
mFullDuplexGlitches->setOutputStream(oboeStream); mFullDuplexGlitches->setSharedOutputStream(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) {
@@ -595,9 +771,9 @@ void ActivityTestDisconnect::close(int32_t streamIndex) {
mSinkFloat.reset(); mSinkFloat.reset();
} }
void ActivityTestDisconnect::configureForStart() { void ActivityTestDisconnect::configureAfterOpen() {
oboe::AudioStream *outputStream = getOutputStream(); std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
oboe::AudioStream *inputStream = getInputStream(); std::shared_ptr<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>();
@@ -607,13 +783,13 @@ void ActivityTestDisconnect::configureForStart() {
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.setCallback(&audioStreamGateway); oboeCallbackProxy.setDataCallback(&audioStreamGateway);
} }
@@ -17,7 +17,6 @@
#ifndef NATIVEOBOE_NATIVEAUDIOCONTEXT_H #ifndef NATIVEOBOE_NATIVEAUDIOCONTEXT_H
#define NATIVEOBOE_NATIVEAUDIOCONTEXT_H #define NATIVEOBOE_NATIVEAUDIOCONTEXT_H
#include <dlfcn.h>
#include <jni.h> #include <jni.h>
#include <sys/system_properties.h> #include <sys/system_properties.h>
#include <thread> #include <thread>
@@ -27,161 +26,50 @@
#include "common/OboeDebug.h" #include "common/OboeDebug.h"
#include "oboe/Oboe.h" #include "oboe/Oboe.h"
#include "aaudio/AAudioExtensions.h"
#include "AudioStreamGateway.h" #include "AudioStreamGateway.h"
#include "flowunits/ImpulseOscillator.h" #include "flowunits/ImpulseOscillator.h"
#include "flowgraph/ManyToMultiConverter.h" #include "flowgraph/ManyToMultiConverter.h"
#include "flowgraph/MonoToMultiConverter.h" #include "flowgraph/MonoToMultiConverter.h"
#include "flowgraph/RampLinear.h"
#include "flowgraph/SinkFloat.h" #include "flowgraph/SinkFloat.h"
#include "flowgraph/SinkI16.h" #include "flowgraph/SinkI16.h"
#include "flowgraph/SinkI24.h"
#include "flowgraph/SinkI32.h"
#include "flowunits/ExponentialShape.h" #include "flowunits/ExponentialShape.h"
#include "flowunits/LinearShape.h" #include "flowunits/LinearShape.h"
#include "flowunits/SineOscillator.h" #include "flowunits/SineOscillator.h"
#include "flowunits/SawtoothOscillator.h" #include "flowunits/SawtoothOscillator.h"
#include "flowunits/TriangleOscillator.h"
#include "flowunits/WhiteNoise.h"
#include "FullDuplexAnalyzer.h"
#include "FullDuplexEcho.h" #include "FullDuplexEcho.h"
#include "FullDuplexGlitches.h" #include "analyzer/GlitchAnalyzer.h"
#include "FullDuplexLatency.h" #include "analyzer/DataPathAnalyzer.h"
#include "FullDuplexStream.h"
#include "InputStreamCallbackAnalyzer.h" #include "InputStreamCallbackAnalyzer.h"
#include "MultiChannelRecording.h" #include "MultiChannelRecording.h"
#include "OboeStreamCallbackProxy.h" #include "OboeStreamCallbackProxy.h"
#include "OboeTools.h"
#include "PlayRecordingCallback.h" #include "PlayRecordingCallback.h"
#include "SawPingGenerator.h" #include "SawPingGenerator.h"
#include "flowunits/TriangleOscillator.h"
// These must match order in strings.xml and in StreamConfiguration.java // These must match order in strings.xml and in StreamConfiguration.java
#define NATIVE_MODE_UNSPECIFIED 0 #define NATIVE_MODE_UNSPECIFIED 0
#define NATIVE_MODE_OPENSLES 1 #define NATIVE_MODE_OPENSLES 1
#define NATIVE_MODE_AAUDIO 2 #define NATIVE_MODE_AAUDIO 2
#define MAX_SINE_OSCILLATORS 8 #define MAX_SINE_OSCILLATORS 16
#define AMPLITUDE_SINE 1.0 #define AMPLITUDE_SINE 1.0
#define AMPLITUDE_SAWTOOTH 0.5 #define AMPLITUDE_SAWTOOTH 0.5
#define FREQUENCY_SAW_PING 800.0 #define FREQUENCY_SAW_PING 800.0
#define AMPLITUDE_SAW_PING 0.8 #define AMPLITUDE_SAW_PING 0.8
#define AMPLITUDE_IMPULSE 0.7 #define AMPLITUDE_IMPULSE 0.7
#define NANOS_PER_MICROSECOND ((int64_t) 1000)
#define NANOS_PER_MILLISECOND (1000 * NANOS_PER_MICROSECOND)
#define NANOS_PER_SECOND (1000 * NANOS_PER_MILLISECOND)
#define LIB_AAUDIO_NAME "libaaudio.so"
#define FUNCTION_IS_MMAP "AAudioStream_isMMapUsed"
#define FUNCTION_SET_MMAP_POLICY "AAudio_setMMapPolicy"
#define FUNCTION_GET_MMAP_POLICY "AAudio_getMMapPolicy"
#define SECONDS_TO_RECORD 10 #define SECONDS_TO_RECORD 10
typedef struct AAudioStreamStruct AAudioStream;
/**
* Call some AAudio test routines that are not part of the normal API.
*/
class AAudioExtensions {
public:
AAudioExtensions() {
int32_t policy = getIntegerProperty("aaudio.mmap_policy", 0);
mMMapSupported = isPolicyEnabled(policy);
policy = getIntegerProperty("aaudio.mmap_exclusive_policy", 0);
mMMapExclusiveSupported = isPolicyEnabled(policy);
}
static bool isPolicyEnabled(int32_t policy) {
return (policy == AAUDIO_POLICY_AUTO || policy == AAUDIO_POLICY_ALWAYS);
}
static AAudioExtensions &getInstance() {
static AAudioExtensions instance;
return instance;
}
bool isMMapUsed(oboe::AudioStream *oboeStream) {
if (!loadLibrary()) return false;
AAudioStream *aaudioStream = (AAudioStream *) oboeStream->getUnderlyingStream();
return mAAudioStream_isMMap(aaudioStream);
}
bool setMMapEnabled(bool enabled) {
if (!loadLibrary()) return false;
return mAAudio_setMMapPolicy(enabled ? AAUDIO_POLICY_AUTO : AAUDIO_POLICY_NEVER);
}
bool isMMapEnabled() {
if (!loadLibrary()) return false;
int32_t policy = mAAudio_getMMapPolicy();
return isPolicyEnabled(policy);
}
bool isMMapSupported() {
return mMMapSupported;
}
bool isMMapExclusiveSupported() {
return mMMapExclusiveSupported;
}
private:
enum {
AAUDIO_POLICY_NEVER = 1,
AAUDIO_POLICY_AUTO,
AAUDIO_POLICY_ALWAYS
};
typedef int32_t aaudio_policy_t;
int getIntegerProperty(const char *name, int defaultValue) {
int result = defaultValue;
char valueText[PROP_VALUE_MAX] = {0};
if (__system_property_get(name, valueText) != 0) {
result = atoi(valueText);
}
return result;
}
// return true if it succeeds
bool loadLibrary() {
if (mFirstTime) {
mFirstTime = false;
mLibHandle = dlopen(LIB_AAUDIO_NAME, 0);
if (mLibHandle == nullptr) {
LOGI("%s() could not find " LIB_AAUDIO_NAME, __func__);
return false;
}
mAAudioStream_isMMap = (bool (*)(AAudioStream *stream))
dlsym(mLibHandle, FUNCTION_IS_MMAP);
if (mAAudioStream_isMMap == nullptr) {
LOGI("%s() could not find " FUNCTION_IS_MMAP, __func__);
return false;
}
mAAudio_setMMapPolicy = (int32_t (*)(aaudio_policy_t policy))
dlsym(mLibHandle, FUNCTION_SET_MMAP_POLICY);
if (mAAudio_setMMapPolicy == nullptr) {
LOGI("%s() could not find " FUNCTION_SET_MMAP_POLICY, __func__);
return false;
}
mAAudio_getMMapPolicy = (aaudio_policy_t (*)())
dlsym(mLibHandle, FUNCTION_GET_MMAP_POLICY);
if (mAAudio_getMMapPolicy == nullptr) {
LOGI("%s() could not find " FUNCTION_GET_MMAP_POLICY, __func__);
return false;
}
}
return true;
}
bool mFirstTime = true;
void *mLibHandle = nullptr;
bool (*mAAudioStream_isMMap)(AAudioStream *stream) = nullptr;
int32_t (*mAAudio_setMMapPolicy)(aaudio_policy_t policy) = nullptr;
aaudio_policy_t (*mAAudio_getMMapPolicy)() = nullptr;
bool mMMapSupported = false;
bool mMMapExclusiveSupported = false;
};
/** /**
* Abstract base class that corresponds to a test at the Java level. * Abstract base class that corresponds to a test at the Java level.
*/ */
@@ -189,12 +77,13 @@ class ActivityContext {
public: public:
ActivityContext() {} ActivityContext() {}
virtual ~ActivityContext() = default; virtual ~ActivityContext() = default;
oboe::AudioStream *getStream(int32_t streamIndex) { std::shared_ptr<oboe::AudioStream> getStream(int32_t streamIndex) {
auto it = mOboeStreams.find(streamIndex); auto it = mOboeStreams.find(streamIndex);
if (it != mOboeStreams.end()) { if (it != mOboeStreams.end()) {
return it->second.get(); return it->second;
} else { } else {
return nullptr; return nullptr;
} }
@@ -202,52 +91,87 @@ public:
virtual void configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder); virtual void configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder);
/**
* Open a stream with the given parameters.
* @param nativeApi
* @param sampleRate
* @param channelCount
* @param channelMask
* @param format
* @param sharingMode
* @param performanceMode
* @param inputPreset
* @param deviceId
* @param sessionId
* @param framesPerBurst
* @param channelConversionAllowed
* @param formatConversionAllowed
* @param rateConversionQuality
* @param isMMap
* @param isInput
* @return stream ID
*/
int open(jint nativeApi, int 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,
jboolean isMMap, jboolean isMMap,
jboolean isInput); jboolean isInput);
oboe::Result release();
virtual void close(int32_t streamIndex); virtual void close(int32_t streamIndex);
void printScheduler() { virtual void configureAfterOpen() {}
#if OBOE_ENABLE_LOGGING
int scheduler = audioStreamGateway.getScheduler();
#endif
LOGI("scheduler = 0x%08x, SCHED_FIFO = 0x%08X\n", scheduler, SCHED_FIFO);
}
virtual void configureForStart() {}
oboe::Result start(); oboe::Result start();
oboe::Result pause(); oboe::Result pause();
oboe::Result flush();
oboe::Result stopAllStreams(); oboe::Result stopAllStreams();
virtual oboe::Result stop() { virtual oboe::Result stop() {
return stopAllStreams(); return stopAllStreams();
} }
double getCpuLoad() { float getCpuLoad() {
return oboeCallbackProxy.getCpuLoad(); return oboeCallbackProxy.getCpuLoad();
} }
void setWorkload(double workload) { float getAndResetMaxCpuLoad() {
return oboeCallbackProxy.getAndResetMaxCpuLoad();
}
uint32_t getAndResetCpuMask() {
return oboeCallbackProxy.getAndResetCpuMask();
}
std::string getCallbackTimeString() {
return oboeCallbackProxy.getCallbackTimeString();
}
void setWorkload(int32_t workload) {
oboeCallbackProxy.setWorkload(workload); oboeCallbackProxy.setWorkload(workload);
} }
void setHearWorkload(bool enabled) {
oboeCallbackProxy.setHearWorkload(enabled);
}
virtual oboe::Result startPlayback() { virtual oboe::Result startPlayback() {
return oboe::Result::OK; return oboe::Result::OK;
} }
@@ -275,9 +199,66 @@ public:
return 0.0; return 0.0;
} }
virtual void setEnabled(bool enabled) { static int64_t getNanoseconds(clockid_t clockId = CLOCK_MONOTONIC) {
struct timespec time;
int result = clock_gettime(clockId, &time);
if (result < 0) {
return result;
}
return (time.tv_sec * NANOS_PER_SECOND) + time.tv_nsec;
} }
// Calculate time between beginning and when frame[0] occurred.
int32_t calculateColdStartLatencyMillis(int32_t sampleRate,
int64_t beginTimeNanos,
int64_t timeStampPosition,
int64_t timestampNanos) const {
int64_t elapsedNanos = NANOS_PER_SECOND * (timeStampPosition / (double) sampleRate);
int64_t timeOfFrameZero = timestampNanos - elapsedNanos;
int64_t coldStartLatencyNanos = timeOfFrameZero - beginTimeNanos;
return coldStartLatencyNanos / NANOS_PER_MILLISECOND;
}
int32_t getColdStartInputMillis() {
std::shared_ptr<oboe::AudioStream> oboeStream = getInputStream();
if (oboeStream != nullptr) {
int64_t framesRead = oboeStream->getFramesRead();
if (framesRead > 0) {
// Base latency on the time that frame[0] would have been received by the app.
int64_t nowNanos = getNanoseconds();
return calculateColdStartLatencyMillis(oboeStream->getSampleRate(),
mInputOpenedAt,
framesRead,
nowNanos);
}
}
return -1;
}
int32_t getColdStartOutputMillis() {
std::shared_ptr<oboe::AudioStream> oboeStream = getOutputStream();
if (oboeStream != nullptr) {
auto result = oboeStream->getTimestamp(CLOCK_MONOTONIC);
if (result) {
auto frameTimestamp = result.value();
// Calculate the time that frame[0] would have been played by the speaker.
int64_t position = frameTimestamp.position;
int64_t timestampNanos = frameTimestamp.timestamp;
return calculateColdStartLatencyMillis(oboeStream->getSampleRate(),
mOutputOpenedAt,
position,
timestampNanos);
}
}
return -1;
}
/**
* Trigger a sound or impulse.
* @param enabled
*/
virtual void trigger() {}
bool isMMapUsed(int32_t streamIndex); bool isMMapUsed(int32_t streamIndex);
int32_t getFramesPerBlock() { int32_t getFramesPerBlock() {
@@ -288,6 +269,14 @@ public:
return oboeCallbackProxy.getCallbackCount(); return oboeCallbackProxy.getCallbackCount();
} }
oboe::Result getLastErrorCallbackResult() {
std::shared_ptr<oboe::AudioStream> stream = getOutputStream();
if (stream == nullptr) {
stream = getInputStream();
}
return stream ? oboe::Result::ErrorNull : stream->getLastErrorCallbackResult();
}
int32_t getFramesPerCallback() { int32_t getFramesPerCallback() {
return oboeCallbackProxy.getFramesPerCallback(); return oboeCallbackProxy.getFramesPerCallback();
} }
@@ -296,6 +285,8 @@ public:
virtual void setSignalType(int signalType) {} virtual void setSignalType(int signalType) {}
virtual void setAmplitude(float amplitude) {}
virtual int32_t saveWaveFile(const char *filename); virtual int32_t saveWaveFile(const char *filename);
virtual void setMinimumFramesBeforeRead(int32_t numFrames) {} virtual void setMinimumFramesBeforeRead(int32_t numFrames) {}
@@ -303,9 +294,19 @@ public:
static bool mUseCallback; static bool mUseCallback;
static int callbackSize; static int callbackSize;
double getTimestampLatency(int32_t streamIndex);
void setCpuAffinityMask(uint32_t mask) {
oboeCallbackProxy.setCpuAffinityMask(mask);
}
void setWorkloadReportingEnabled(bool enabled) {
oboeCallbackProxy.setWorkloadReportingEnabled(enabled);
}
protected: protected:
oboe::AudioStream *getInputStream(); std::shared_ptr<oboe::AudioStream> getInputStream();
oboe::AudioStream *getOutputStream(); std::shared_ptr<oboe::AudioStream> getOutputStream();
int32_t allocateStreamIndex(); int32_t allocateStreamIndex();
void freeStreamIndex(int32_t streamIndex); void freeStreamIndex(int32_t streamIndex);
@@ -314,7 +315,7 @@ protected:
SECONDS_TO_RECORD * mSampleRate); SECONDS_TO_RECORD * mSampleRate);
} }
virtual void finishOpen(bool isInput, oboe::AudioStream *oboeStream) {} virtual void finishOpen(bool isInput, std::shared_ptr<oboe::AudioStream> &oboeStream) {}
virtual oboe::Result startStreams() = 0; virtual oboe::Result startStreams() = 0;
@@ -332,9 +333,11 @@ protected:
int32_t mSampleRate = 0; // TODO per stream int32_t mSampleRate = 0; // TODO per stream
std::atomic<bool> threadEnabled{false}; std::atomic<bool> threadEnabled{false};
std::thread *dataThread = nullptr; std::thread *dataThread = nullptr; // FIXME never gets deleted
private: private:
int64_t mInputOpenedAt = 0;
int64_t mOutputOpenedAt = 0;
}; };
/** /**
@@ -346,7 +349,7 @@ public:
ActivityTestInput() {} ActivityTestInput() {}
virtual ~ActivityTestInput() = default; virtual ~ActivityTestInput() = default;
void configureForStart() override; void configureAfterOpen() override;
double getPeakLevel(int index) override { double getPeakLevel(int index) override {
return mInputAnalyzer.getPeakLevel(index); return mInputAnalyzer.getPeakLevel(index);
@@ -354,8 +357,6 @@ public:
void runBlockingIO() override; void runBlockingIO() override;
InputStreamCallbackAnalyzer mInputAnalyzer;
void setMinimumFramesBeforeRead(int32_t numFrames) override { void setMinimumFramesBeforeRead(int32_t numFrames) override {
mInputAnalyzer.setMinimumFramesBeforeRead(numFrames); mInputAnalyzer.setMinimumFramesBeforeRead(numFrames);
mMinimumFramesBeforeRead = numFrames; mMinimumFramesBeforeRead = numFrames;
@@ -369,9 +370,13 @@ protected:
oboe::Result startStreams() override { oboe::Result startStreams() override {
mInputAnalyzer.reset(); mInputAnalyzer.reset();
mInputAnalyzer.setup(std::max(getInputStream()->getFramesPerBurst(), callbackSize),
getInputStream()->getChannelCount(),
getInputStream()->getFormat());
return getInputStream()->requestStart(); return getInputStream()->requestStart();
} }
InputStreamCallbackAnalyzer mInputAnalyzer;
int32_t mMinimumFramesBeforeRead = 0; int32_t mMinimumFramesBeforeRead = 0;
}; };
@@ -414,11 +419,9 @@ public:
void close(int32_t streamIndex) override; void close(int32_t streamIndex) override;
oboe::Result startStreams() override { oboe::Result startStreams() override;
return getOutputStream()->start();
}
void configureForStart() override; void configureAfterOpen() override;
virtual void configureStreamGateway(); virtual void configureStreamGateway();
@@ -439,6 +442,13 @@ public:
mSignalType = (SignalType) signalType; mSignalType = (SignalType) signalType;
} }
void setAmplitude(float amplitude) override {
mAmplitude = amplitude;
if (mVolumeRamp) {
mVolumeRamp->setTarget(mAmplitude);
}
}
protected: protected:
SignalType mSignalType = SignalType::Sine; SignalType mSignalType = SignalType::Sine;
@@ -446,15 +456,22 @@ protected:
std::vector<SawtoothOscillator> sawtoothOscillators; std::vector<SawtoothOscillator> sawtoothOscillators;
static constexpr float kSweepPeriod = 10.0; // for triangle up and down static constexpr float kSweepPeriod = 10.0; // for triangle up and down
// A triangle LFO is shaped into either a linear or an exponential range. // A triangle LFO is shaped into either a linear or an exponential range for sweep.
TriangleOscillator mTriangleOscillator; TriangleOscillator mTriangleOscillator;
LinearShape mLinearShape; LinearShape mLinearShape;
ExponentialShape mExponentialShape; ExponentialShape mExponentialShape;
class WhiteNoise mWhiteNoise;
static constexpr int kRampMSec = 10; // for volume control
float mAmplitude = 1.0f;
std::shared_ptr<RampLinear> mVolumeRamp;
std::unique_ptr<ManyToMultiConverter> manyToMulti; std::unique_ptr<ManyToMultiConverter> manyToMulti;
std::unique_ptr<MonoToMultiConverter> monoToMulti; std::unique_ptr<MonoToMultiConverter> monoToMulti;
std::shared_ptr<flowgraph::SinkFloat> mSinkFloat; std::shared_ptr<oboe::flowgraph::SinkFloat> mSinkFloat;
std::shared_ptr<flowgraph::SinkI16> mSinkI16; std::shared_ptr<oboe::flowgraph::SinkI16> mSinkI16;
std::shared_ptr<oboe::flowgraph::SinkI24> mSinkI24;
std::shared_ptr<oboe::flowgraph::SinkI32> mSinkI32;
}; };
/** /**
@@ -466,10 +483,10 @@ public:
ActivityTapToTone() {} ActivityTapToTone() {}
virtual ~ActivityTapToTone() = default; virtual ~ActivityTapToTone() = default;
void configureForStart() override; void configureAfterOpen() override;
virtual void setEnabled(bool enabled) override { virtual void trigger() override {
sawPingGenerator.setEnabled(enabled); sawPingGenerator.trigger();
} }
SawPingGenerator sawPingGenerator; SawPingGenerator sawPingGenerator;
@@ -522,12 +539,16 @@ public:
} }
} }
double getPeakLevel(int index) override {
return mFullDuplexEcho->getPeakLevel(index);
}
FullDuplexAnalyzer *getFullDuplexAnalyzer() override { FullDuplexAnalyzer *getFullDuplexAnalyzer() override {
return (FullDuplexAnalyzer *) mFullDuplexEcho.get(); return (FullDuplexAnalyzer *) mFullDuplexEcho.get();
} }
protected: protected:
void finishOpen(bool isInput, oboe::AudioStream *oboeStream) override; void finishOpen(bool isInput, std::shared_ptr<oboe::AudioStream> &oboeStream) override;
private: private:
std::unique_ptr<FullDuplexEcho> mFullDuplexEcho{}; std::unique_ptr<FullDuplexEcho> mFullDuplexEcho{};
@@ -538,36 +559,74 @@ private:
*/ */
class ActivityRoundTripLatency : public ActivityFullDuplex { class ActivityRoundTripLatency : public ActivityFullDuplex {
public: public:
ActivityRoundTripLatency() {
#define USE_WHITE_NOISE_ANALYZER 1
#if USE_WHITE_NOISE_ANALYZER
// New analyzer that uses a short pattern of white noise bursts.
mLatencyAnalyzer = std::make_unique<WhiteNoiseLatencyAnalyzer>();
#else
// Old analyzer based on encoded random bits.
mLatencyAnalyzer = std::make_unique<EncodedRandomLatencyAnalyzer>();
#endif
mLatencyAnalyzer->setup();
}
virtual ~ActivityRoundTripLatency() = default;
oboe::Result startStreams() override { oboe::Result startStreams() override {
mAnalyzerLaunched = false;
return mFullDuplexLatency->start(); return mFullDuplexLatency->start();
} }
void configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) override; void configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) override;
LatencyAnalyzer *getLatencyAnalyzer() { LatencyAnalyzer *getLatencyAnalyzer() {
return mFullDuplexLatency->getLatencyAnalyzer(); return mLatencyAnalyzer.get();
} }
int32_t getState() override { int32_t getState() override {
return getLatencyAnalyzer()->getState(); return getLatencyAnalyzer()->getState();
} }
int32_t getResult() override { int32_t getResult() override {
return getLatencyAnalyzer()->getState(); return getLatencyAnalyzer()->getState(); // TODO This does not look right.
} }
bool isAnalyzerDone() override { bool isAnalyzerDone() override {
return mFullDuplexLatency->isDone(); if (!mAnalyzerLaunched) {
mAnalyzerLaunched = launchAnalysisIfReady();
}
return mLatencyAnalyzer->isDone();
} }
FullDuplexAnalyzer *getFullDuplexAnalyzer() override { FullDuplexAnalyzer *getFullDuplexAnalyzer() override {
return (FullDuplexAnalyzer *) mFullDuplexLatency.get(); return (FullDuplexAnalyzer *) mFullDuplexLatency.get();
} }
static void analyzeData(LatencyAnalyzer *analyzer) {
analyzer->analyze();
}
bool launchAnalysisIfReady() {
// Are we ready to do the analysis?
if (mLatencyAnalyzer->hasEnoughData()) {
// Crunch the numbers on a separate thread.
std::thread t(analyzeData, mLatencyAnalyzer.get());
t.detach();
return true;
}
return false;
}
jdouble measureTimestampLatency();
protected: protected:
void finishOpen(bool isInput, oboe::AudioStream *oboeStream) override; void finishOpen(bool isInput, std::shared_ptr<oboe::AudioStream> &oboeStream) override;
private: private:
std::unique_ptr<FullDuplexLatency> mFullDuplexLatency{}; std::unique_ptr<FullDuplexAnalyzer> mFullDuplexLatency{};
std::unique_ptr<LatencyAnalyzer> mLatencyAnalyzer;
bool mAnalyzerLaunched = false;
}; };
/** /**
@@ -583,18 +642,19 @@ public:
void configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) override; void configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) override;
GlitchAnalyzer *getGlitchAnalyzer() { GlitchAnalyzer *getGlitchAnalyzer() {
if (!mFullDuplexGlitches) return nullptr; return &mGlitchAnalyzer;
return mFullDuplexGlitches->getGlitchAnalyzer();
} }
int32_t getState() override { int32_t getState() override {
return getGlitchAnalyzer()->getState(); return getGlitchAnalyzer()->getState();
} }
int32_t getResult() override { int32_t getResult() override {
return getGlitchAnalyzer()->getResult(); return getGlitchAnalyzer()->getResult();
} }
bool isAnalyzerDone() override { bool isAnalyzerDone() override {
return mFullDuplexGlitches->isDone(); return mGlitchAnalyzer.isDone();
} }
FullDuplexAnalyzer *getFullDuplexAnalyzer() override { FullDuplexAnalyzer *getFullDuplexAnalyzer() override {
@@ -602,10 +662,54 @@ public:
} }
protected: protected:
void finishOpen(bool isInput, oboe::AudioStream *oboeStream) override; void finishOpen(bool isInput, std::shared_ptr<oboe::AudioStream> &oboeStream) override;
private: private:
std::unique_ptr<FullDuplexGlitches> mFullDuplexGlitches{}; std::unique_ptr<FullDuplexAnalyzer> mFullDuplexGlitches{};
GlitchAnalyzer mGlitchAnalyzer;
};
/**
* Measure Data Path
*/
class ActivityDataPath : public ActivityFullDuplex {
public:
oboe::Result startStreams() override {
return mFullDuplexDataPath->start();
}
void configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) override;
void configureAfterOpen() override {
// set buffer size
std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
int32_t capacityInFrames = outputStream->getBufferCapacityInFrames();
int32_t burstInFrames = outputStream->getFramesPerBurst();
int32_t capacityInBursts = capacityInFrames / burstInFrames;
int32_t sizeInBursts = std::max(2, capacityInBursts / 2);
// Set size of buffer to minimize underruns.
auto result = outputStream->setBufferSizeInFrames(sizeInBursts * burstInFrames);
static_cast<void>(result); // Avoid unused variable.
LOGD("ActivityDataPath: %s() capacity = %d, burst = %d, size = %d",
__func__, capacityInFrames, burstInFrames, result.value());
}
DataPathAnalyzer *getDataPathAnalyzer() {
return &mDataPathAnalyzer;
}
FullDuplexAnalyzer *getFullDuplexAnalyzer() override {
return (FullDuplexAnalyzer *) mFullDuplexDataPath.get();
}
protected:
void finishOpen(bool isInput, std::shared_ptr<oboe::AudioStream> &oboeStream) override;
private:
std::unique_ptr<FullDuplexAnalyzer> mFullDuplexDataPath{};
DataPathAnalyzer mDataPathAnalyzer;
}; };
/** /**
@@ -620,28 +724,29 @@ public:
void close(int32_t streamIndex) override; void close(int32_t streamIndex) override;
oboe::Result startStreams() override { oboe::Result startStreams() override {
oboe::AudioStream *outputStream = getOutputStream(); std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
if (outputStream) { if (outputStream) {
return outputStream->start(); return outputStream->start();
} }
oboe::AudioStream *inputStream = getInputStream(); std::shared_ptr<oboe::AudioStream> inputStream = getInputStream();
if (inputStream) { if (inputStream) {
return inputStream->start(); return inputStream->start();
} }
return oboe::Result::ErrorNull; return oboe::Result::ErrorNull;
} }
void configureForStart() override; void configureAfterOpen() override;
private: private:
std::unique_ptr<SineOscillator> sineOscillator; std::unique_ptr<SineOscillator> sineOscillator;
std::unique_ptr<MonoToMultiConverter> monoToMulti; std::unique_ptr<MonoToMultiConverter> monoToMulti;
std::shared_ptr<flowgraph::SinkFloat> mSinkFloat; std::shared_ptr<oboe::flowgraph::SinkFloat> mSinkFloat;
}; };
/** /**
* Switch between various * Global context for native tests.
* Switch between various ActivityContexts.
*/ */
class NativeAudioContext { class NativeAudioContext {
public: public:
@@ -679,6 +784,9 @@ public:
case ActivityType::TestDisconnect: case ActivityType::TestDisconnect:
currentActivity = &mActivityTestDisconnect; currentActivity = &mActivityTestDisconnect;
break; break;
case ActivityType::DataPath:
currentActivity = &mActivityDataPath;
break;
} }
} }
@@ -693,6 +801,7 @@ public:
ActivityEcho mActivityEcho; ActivityEcho mActivityEcho;
ActivityRoundTripLatency mActivityRoundTripLatency; ActivityRoundTripLatency mActivityRoundTripLatency;
ActivityGlitches mActivityGlitches; ActivityGlitches mActivityGlitches;
ActivityDataPath mActivityDataPath;
ActivityTestDisconnect mActivityTestDisconnect; ActivityTestDisconnect mActivityTestDisconnect;
private: private:
@@ -708,11 +817,11 @@ private:
RoundTripLatency = 5, RoundTripLatency = 5,
Glitches = 6, Glitches = 6,
TestDisconnect = 7, TestDisconnect = 7,
DataPath = 8,
}; };
ActivityType mActivityType = ActivityType::Undefined; ActivityType mActivityType = ActivityType::Undefined;
ActivityContext *currentActivity = &mActivityTestOutput; ActivityContext *currentActivity = &mActivityTestOutput;
}; };
#endif //NATIVEOBOE_NATIVEAUDIOCONTEXT_H #endif //NATIVEOBOE_NATIVEAUDIOCONTEXT_H
@@ -17,40 +17,8 @@
#include "common/OboeDebug.h" #include "common/OboeDebug.h"
#include "OboeStreamCallbackProxy.h" #include "OboeStreamCallbackProxy.h"
// Linear congruential random number generator.
static uint32_t s_random16() {
static uint32_t seed = 1234;
seed = ((seed * 31421) + 6927) & 0x0FFFF;
return seed;
}
/**
* The random number generator is good for burning CPU because the compiler cannot
* easily optimize away the computation.
* @param workload number of times to execute the loop
* @return a white noise value between -1.0 and +1.0
*/
static float s_burnCPU(int32_t workload) {
uint32_t random = 0;
for (int32_t i = 0; i < workload; i++) {
for (int32_t j = 0; j < 10; j++) {
random = random ^ s_random16();
}
}
return (random - 32768) * (1.0 / 32768);
}
bool OboeStreamCallbackProxy::mCallbackReturnStop = false; bool OboeStreamCallbackProxy::mCallbackReturnStop = false;
int64_t OboeStreamCallbackProxy::getNanoseconds(clockid_t clockId) {
struct timespec time;
int result = clock_gettime(clockId, &time);
if (result < 0) {
return result;
}
return (time.tv_sec * 1e9) + time.tv_nsec;
}
oboe::DataCallbackResult OboeStreamCallbackProxy::onAudioReady( oboe::DataCallbackResult OboeStreamCallbackProxy::onAudioReady(
oboe::AudioStream *audioStream, oboe::AudioStream *audioStream,
void *audioData, void *audioData,
@@ -58,6 +26,23 @@ oboe::DataCallbackResult OboeStreamCallbackProxy::onAudioReady(
oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Stop; oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Stop;
int64_t startTimeNanos = getNanoseconds(); int64_t startTimeNanos = getNanoseconds();
// Record which CPU this is running on.
orCurrentCpuMask(sched_getcpu());
// Tell ADPF in advance what our workload will be.
if (mWorkloadReportingEnabled) {
audioStream->reportWorkload(mNumWorkloadVoices);
}
// Change affinity if app requested a change.
uint32_t mask = mCpuAffinityMask;
if (mask != mPreviousMask) {
int err = applyCpuAffinityMask(mask);
if (err != 0) {
}
mPreviousMask = mask;
}
mCallbackCount++; mCallbackCount++;
mFramesPerCallback = numFrames; mFramesPerCallback = numFrames;
@@ -65,31 +50,67 @@ oboe::DataCallbackResult OboeStreamCallbackProxy::onAudioReady(
return oboe::DataCallbackResult::Stop; return oboe::DataCallbackResult::Stop;
} }
s_burnCPU((int32_t)(mWorkload * kWorkloadScaler * numFrames));
if (mCallback != nullptr) { if (mCallback != nullptr) {
callbackResult = mCallback->onAudioReady(audioStream, audioData, numFrames); callbackResult = mCallback->onAudioReady(audioStream, audioData, numFrames);
} }
// Update CPU load mSynthWorkload.onCallback(mNumWorkloadVoices);
double calculationTime = (double)(getNanoseconds() - startTimeNanos); if (mNumWorkloadVoices > 0) {
double inverseRealTime = audioStream->getSampleRate() / (1.0e9 * numFrames); // Render into the buffer or discard the synth voices.
double currentCpuLoad = calculationTime * inverseRealTime; // avoid a divide float *buffer = (audioStream->getChannelCount() == 2 && mHearWorkload)
mCpuLoad = (mCpuLoad * 0.95) + (currentCpuLoad * 0.05); // simple low pass filter ? static_cast<float *>(audioData) : nullptr;
mSynthWorkload.renderStereo(buffer, numFrames);
}
// Measure CPU load.
int64_t currentTimeNanos = getNanoseconds();
// Sometimes we get a short callback when doing sample rate conversion.
// Just ignore those to avoid noise.
if (numFrames > (getFramesPerCallback() / 2)) {
int64_t calculationTime = currentTimeNanos - startTimeNanos;
float currentCpuLoad = calculationTime * 0.000000001f * audioStream->getSampleRate() / numFrames;
mCpuLoad = (mCpuLoad * 0.95f) + (currentCpuLoad * 0.05f); // simple low pass filter
mMaxCpuLoad = std::max(currentCpuLoad, mMaxCpuLoad.load());
}
if (mPreviousCallbackTimeNs != 0) {
mStatistics.add((currentTimeNanos - mPreviousCallbackTimeNs) * kNsToMsScaler);
}
mPreviousCallbackTimeNs = currentTimeNanos;
return callbackResult; return callbackResult;
} }
void OboeStreamCallbackProxy::onErrorBeforeClose(oboe::AudioStream *audioStream, oboe::Result error) { int OboeStreamCallbackProxy::applyCpuAffinityMask(uint32_t mask) {
LOGD("OboeStreamCallbackProxy::%s(%p, %d) called", __func__, audioStream, error); int err = 0;
if (mCallback != nullptr) { // Capture original CPU set so we can restore it.
mCallback->onErrorBeforeClose(audioStream, error); if (!mIsOriginalCpuSetValid) {
err = sched_getaffinity((pid_t) 0,
sizeof(mOriginalCpuSet),
&mOriginalCpuSet);
if (err) {
LOGE("%s(0x%02X) - sched_getaffinity(), errno = %d\n", __func__, mask, errno);
return -errno;
}
mIsOriginalCpuSetValid = true;
} }
} if (mask) {
cpu_set_t cpu_set;
void OboeStreamCallbackProxy::onErrorAfterClose(oboe::AudioStream *audioStream, oboe::Result error) { CPU_ZERO(&cpu_set);
LOGD("OboeStreamCallbackProxy::%s(%p, %d) called", __func__, audioStream, error); int cpuCount = sysconf(_SC_NPROCESSORS_CONF);
if (mCallback != nullptr) { for (int cpuIndex = 0; cpuIndex < cpuCount; cpuIndex++) {
mCallback->onErrorAfterClose(audioStream, error); if (mask & (1 << cpuIndex)) {
CPU_SET(cpuIndex, &cpu_set);
}
}
err = sched_setaffinity((pid_t) 0, sizeof(cpu_set_t), &cpu_set);
} else {
// Restore original mask.
err = sched_setaffinity((pid_t) 0, sizeof(mOriginalCpuSet), &mOriginalCpuSet);
} }
if (err) {
LOGE("%s(0x%02X) - sched_setaffinity(), errno = %d\n", __func__, mask, errno);
return -errno;
}
return 0;
} }
@@ -19,15 +19,129 @@
#include <unistd.h> #include <unistd.h>
#include <sys/types.h> #include <sys/types.h>
#include <sys/sysinfo.h>
#include "oboe/Oboe.h" #include "oboe/Oboe.h"
#include "synth/Synthesizer.h"
#include "synth/SynthTools.h"
#include "OboeTesterStreamCallback.h"
class OboeStreamCallbackProxy : public oboe::AudioStreamCallback { class DoubleStatistics {
public:
void add(double statistic) {
if (skipCount < kNumberStatisticsToSkip) {
skipCount++;
} else {
if (statistic <= 0.0) return;
sum = statistic + sum;
count++;
minimum = std::min(statistic, minimum.load());
maximum = std::max(statistic, maximum.load());
}
}
double getAverage() const {
return sum / count;
}
std::string dump() const {
if (count == 0) return "?";
char buff[100];
snprintf(buff, sizeof(buff), "%3.1f/%3.1f/%3.1f ms", minimum.load(), getAverage(), maximum.load());
std::string buffAsStr = buff;
return buffAsStr;
}
void clear() {
skipCount = 0;
sum = 0;
count = 0;
minimum = DBL_MAX;
maximum = 0;
}
private:
static constexpr double kNumberStatisticsToSkip = 5; // Skip the first 5 frames
std::atomic<int> skipCount { 0 };
std::atomic<double> sum { 0 };
std::atomic<int> count { 0 };
std::atomic<double> minimum { DBL_MAX };
std::atomic<double> maximum { 0 };
};
/**
* Manage the synthesizer workload that burdens the CPU.
* Adjust the number of voices according to the requested workload.
* Trigger noteOn and noteOff messages.
*/
class SynthWorkload {
public:
SynthWorkload() {
mSynth.setup(marksynth::kSynthmarkSampleRate, marksynth::kSynthmarkMaxVoices);
}
void onCallback(double workload) {
// If workload changes then restart notes.
if (workload != mPreviousWorkload) {
mSynth.allNotesOff();
mAreNotesOn = false;
mCountdown = 0; // trigger notes on
mPreviousWorkload = workload;
}
if (mCountdown <= 0) {
if (mAreNotesOn) {
mSynth.allNotesOff();
mAreNotesOn = false;
mCountdown = mOffFrames;
} else {
mSynth.notesOn((int)mPreviousWorkload);
mAreNotesOn = true;
mCountdown = mOnFrames;
}
}
}
/**
* Render the notes into a stereo buffer.
* Passing a nullptr will cause the calculated results to be discarded.
* The workload should be the same.
* @param buffer a real stereo buffer or nullptr
* @param numFrames
*/
void renderStereo(float *buffer, int numFrames) {
if (buffer == nullptr) {
int framesLeft = numFrames;
while (framesLeft > 0) {
int framesThisTime = std::min(kDummyBufferSizeInFrames, framesLeft);
// Do the work then throw it away.
mSynth.renderStereo(&mDummyStereoBuffer[0], framesThisTime);
framesLeft -= framesThisTime;
}
} else {
mSynth.renderStereo(buffer, numFrames);
}
mCountdown -= numFrames;
}
private:
marksynth::Synthesizer mSynth;
static constexpr int kDummyBufferSizeInFrames = 32;
float mDummyStereoBuffer[kDummyBufferSizeInFrames * 2];
double mPreviousWorkload = 1.0;
bool mAreNotesOn = false;
int mCountdown = 0;
int mOnFrames = (int) (0.2 * 48000);
int mOffFrames = (int) (0.3 * 48000);
};
class OboeStreamCallbackProxy : public OboeTesterStreamCallback {
public: public:
void setCallback(oboe::AudioStreamCallback *callback) { void setDataCallback(oboe::AudioStreamDataCallback *callback) {
mCallback = callback; mCallback = callback;
setCallbackCount(0); setCallbackCount(0);
mStatistics.clear();
mPreviousMask = 0;
} }
static void setCallbackReturnStop(bool b) { static void setCallbackReturnStop(bool b) {
@@ -54,39 +168,106 @@ public:
void *audioData, void *audioData,
int numFrames) override; int numFrames) override;
void onErrorBeforeClose(oboe::AudioStream *audioStream, oboe::Result error) override;
void onErrorAfterClose(oboe::AudioStream *audioStream, oboe::Result error) override;
/** /**
* Specify the amount of artificial workload that will waste CPU cycles * Specify the amount of artificial workload that will waste CPU cycles
* and increase the CPU load. * and increase the CPU load.
* @param workload typically ranges from 0.0 to 100.0 * @param workload typically ranges from 0 to 400
*/ */
void setWorkload(double workload) { void setWorkload(int32_t workload) {
mWorkload = std::max(0.0, workload); mNumWorkloadVoices = std::max(0, workload);
} }
double getWorkload() const { int32_t getWorkload() const {
return mWorkload; return mNumWorkloadVoices;
} }
double getCpuLoad() const { void setHearWorkload(bool enabled) {
mHearWorkload = enabled;
}
/**
* This is the callback duration relative to the real-time equivalent.
* So it may be higher than 1.0.
* @return low pass filtered value for the fractional CPU load
*/
float getCpuLoad() const {
return mCpuLoad; return mCpuLoad;
} }
static int64_t getNanoseconds(clockid_t clockId = CLOCK_MONOTONIC); /**
* Calling this will atomically reset the max to zero so only call
* this from one client.
*
* @return last value of the maximum unfiltered CPU load.
*/
float getAndResetMaxCpuLoad() {
return mMaxCpuLoad.exchange(0.0f);
}
std::string getCallbackTimeString() const {
return mStatistics.dump();
}
/**
* @return mask of the CPUs used since the last reset
*/
uint32_t getAndResetCpuMask() {
return mCpuMask.exchange(0);
}
void orCurrentCpuMask(int cpuIndex) {
mCpuMask |= (1 << cpuIndex);
}
/**
* @param cpuIndex
* @return 0 on success or a negative errno
*/
int setCpuAffinity(int cpuIndex) {
cpu_set_t cpu_set;
CPU_ZERO(&cpu_set);
CPU_SET(cpuIndex, &cpu_set);
int err = sched_setaffinity((pid_t) 0, sizeof(cpu_set_t), &cpu_set);
return err == 0 ? 0 : -errno;
}
/**
*
* @param mask bits for each CPU or zero for all
* @return
*/
int applyCpuAffinityMask(uint32_t mask);
void setCpuAffinityMask(uint32_t mask) {
mCpuAffinityMask = mask;
}
void setWorkloadReportingEnabled(bool enabled) {
mWorkloadReportingEnabled = enabled;
}
private: private:
static constexpr int32_t kWorkloadScaler = 500; static constexpr double kNsToMsScaler = 0.000001;
double mWorkload = 0.0; std::atomic<float> mCpuLoad{0.0f};
std::atomic<double> mCpuLoad{0}; std::atomic<float> mMaxCpuLoad{0.0f};
int64_t mPreviousCallbackTimeNs = 0;
DoubleStatistics mStatistics;
int32_t mNumWorkloadVoices = 0;
SynthWorkload mSynthWorkload;
bool mHearWorkload = false;
bool mWorkloadReportingEnabled = false;
oboe::AudioStreamCallback *mCallback = nullptr; oboe::AudioStreamDataCallback *mCallback = nullptr;
static bool mCallbackReturnStop; static bool mCallbackReturnStop;
int64_t mCallbackCount = 0; int64_t mCallbackCount = 0;
std::atomic<int32_t> mFramesPerCallback{0}; std::atomic<int32_t> mFramesPerCallback{0};
std::atomic<uint32_t> mCpuAffinityMask{0};
std::atomic<uint32_t> mPreviousMask{0};
std::atomic<uint32_t> mCpuMask{0};
cpu_set_t mOriginalCpuSet;
bool mIsOriginalCpuSetValid = false;
}; };
#endif //NATIVEOBOE_OBOESTREAMCALLBACKPROXY_H #endif //NATIVEOBOE_OBOESTREAMCALLBACKPROXY_H
@@ -0,0 +1,86 @@
/*
* Copyright 2020 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 <sched.h>
#include <cstring>
#include "AudioStreamGateway.h"
#include "common/OboeDebug.h"
#include "oboe/Oboe.h"
#include "OboeStreamCallbackProxy.h"
#include "OboeTesterStreamCallback.h"
#include "OboeTools.h"
#include "synth/IncludeMeOnce.h"
int32_t OboeTesterStreamCallback::mHangTimeMillis = 0;
// Print if scheduler changes.
void OboeTesterStreamCallback::printScheduler() {
#if OBOE_ENABLE_LOGGING
int scheduler = sched_getscheduler(gettid());
if (scheduler != mPreviousScheduler) {
int schedulerType = scheduler & 0xFFFF; // mask off high flags
LOGD("callback CPU scheduler = 0x%08x = %s",
scheduler,
((schedulerType == SCHED_FIFO) ? "SCHED_FIFO" :
((schedulerType == SCHED_OTHER) ? "SCHED_OTHER" :
((schedulerType == SCHED_RR) ? "SCHED_RR" : "UNKNOWN")))
);
mPreviousScheduler = scheduler;
}
#endif
}
// Sleep to cause an XRun. Then reschedule.
void OboeTesterStreamCallback::maybeHang(const int64_t startNanos) {
if (mHangTimeMillis == 0) return;
if (startNanos > mNextTimeToHang) {
LOGD("%s() start sleeping", __func__);
// Take short naps until it is time to wake up.
int64_t nowNanos = startNanos;
int64_t wakeupNanos = startNanos + (mHangTimeMillis * NANOS_PER_MILLISECOND);
while (nowNanos < wakeupNanos && mHangTimeMillis > 0) {
int32_t sleepTimeMicros = (int32_t) ((wakeupNanos - nowNanos) / 1000);
if (sleepTimeMicros == 0) break;
// The usleep() function can fail if it sleeps for more than one second.
// So sleep for several small intervals.
// This also allows us to exit the loop if mHangTimeMillis gets set to zero.
const int32_t maxSleepTimeMicros = 100 * 1000;
sleepTimeMicros = std::min(maxSleepTimeMicros, sleepTimeMicros);
usleep(sleepTimeMicros);
nowNanos = getNanoseconds();
}
// Calculate when we hang again.
const int32_t minDurationMillis = 500;
const int32_t maxDurationMillis = std::max(10000, mHangTimeMillis * 2);
int32_t durationMillis = mHangTimeMillis * 10;
durationMillis = std::max(minDurationMillis, std::min(maxDurationMillis, durationMillis));
mNextTimeToHang = startNanos + (durationMillis * NANOS_PER_MILLISECOND);
LOGD("%s() slept for %d msec, durationMillis = %d", __func__,
(int)((nowNanos - startNanos) / 1e6L),
durationMillis);
}
}
int64_t OboeTesterStreamCallback::getNanoseconds(clockid_t clockId) {
struct timespec time;
int result = clock_gettime(clockId, &time);
if (result < 0) {
return result;
}
return (time.tv_sec * 1e9) + time.tv_nsec;
}
@@ -0,0 +1,58 @@
/*
* Copyright 2020 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_STREAM_CALLBACK_H
#define OBOETESTER_STREAM_CALLBACK_H
#include <unistd.h>
#include <sys/types.h>
#include <sys/sysinfo.h>
#include "flowgraph/FlowGraphNode.h"
#include "oboe/Oboe.h"
#include "synth/Synthesizer.h"
#include "synth/SynthTools.h"
class OboeTesterStreamCallback : public oboe::AudioStreamCallback {
public:
virtual ~OboeTesterStreamCallback() = default;
// Call this before starting.
void reset() {
mPreviousScheduler = -1;
}
static int64_t getNanoseconds(clockid_t clockId = CLOCK_MONOTONIC);
/**
* Specify a sleep time that will hang the audio periodically.
*
* @param hangTimeMillis
*/
static void setHangTimeMillis(int hangTimeMillis) {
mHangTimeMillis = hangTimeMillis;
}
protected:
void printScheduler();
void maybeHang(int64_t nowNanos);
int mPreviousScheduler = -1;
static int mHangTimeMillis;
int64_t mNextTimeToHang = 0;
};
#endif //OBOETESTER_STREAM_CALLBACK_H
@@ -1,5 +1,5 @@
/* /*
* Copyright 2019 The Android Open Source Project * Copyright 2023 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,5 +14,12 @@
* limitations under the License. * limitations under the License.
*/ */
#include "common/OboeDebug.h" #ifndef OBOETESTER_OBOETOOLS_H
#include "FullDuplexGlitches.h" #define OBOETESTER_OBOETOOLS_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
@@ -19,7 +19,7 @@
#include "oboe/Definitions.h" #include "oboe/Definitions.h"
#include "SawPingGenerator.h" #include "SawPingGenerator.h"
using namespace flowgraph; using namespace oboe::flowgraph;
SawPingGenerator::SawPingGenerator() SawPingGenerator::SawPingGenerator()
: OscillatorBase() : OscillatorBase()
@@ -30,6 +30,11 @@ SawPingGenerator::SawPingGenerator()
SawPingGenerator::~SawPingGenerator() { } SawPingGenerator::~SawPingGenerator() { }
void SawPingGenerator::reset() {
FlowGraphNode::reset();
mAcknowledgeCount.store(mRequestCount.load());
}
int32_t SawPingGenerator::onProcess(int numFrames) { int32_t SawPingGenerator::onProcess(int numFrames) {
const float *frequencies = frequency.getBuffer(); const float *frequencies = frequency.getBuffer();
@@ -58,11 +63,7 @@ int32_t SawPingGenerator::onProcess(int numFrames) {
return numFrames; return numFrames;
} }
void SawPingGenerator::setEnabled(bool enabled) { void SawPingGenerator::trigger() {
if (enabled) { mRequestCount++;
mRequestCount++;
} else {
mAcknowledgeCount.store(mRequestCount.load());
}
} }
@@ -32,7 +32,9 @@ public:
int32_t onProcess(int numFrames) override; int32_t onProcess(int numFrames) override;
void setEnabled(bool enabled); void trigger();
void reset() override;
private: private:
std::atomic<int> mRequestCount; // external thread increments this to request a beep std::atomic<int> mRequestCount; // external thread increments this to request a beep
@@ -0,0 +1,118 @@
/*
* 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 <stdlib.h>
#include <aaudio/AAudioExtensions.h>
#include "common/OboeDebug.h"
#include "oboe/AudioClock.h"
#include "TestColdStartLatency.h"
#include "OboeTools.h"
using namespace oboe;
int32_t TestColdStartLatency::open(bool useInput, bool useLowLatency, bool useMmap, bool
useExclusive) {
mDataCallback = std::make_shared<MyDataCallback>();
// Enable MMAP if needed
bool wasMMapEnabled = AAudioExtensions::getInstance().isMMapEnabled();
AAudioExtensions::getInstance().setMMapEnabled(useMmap);
int64_t beginOpenNanos = AudioClock::getNanoseconds();
AudioStreamBuilder builder;
Result result = builder.setFormat(AudioFormat::Float)
->setPerformanceMode(useLowLatency ? PerformanceMode::LowLatency :
PerformanceMode::None)
->setDirection(useInput ? Direction::Input : Direction::Output)
->setChannelCount(kChannelCount)
->setDataCallback(mDataCallback)
->setSharingMode(useExclusive ? SharingMode::Exclusive : SharingMode::Shared)
->openStream(mStream);
int64_t endOpenNanos = AudioClock::getNanoseconds();
int64_t actualDurationNanos = endOpenNanos - beginOpenNanos;
mOpenTimeMicros = actualDurationNanos / NANOS_PER_MICROSECOND;
// Revert MMAP back to its previous state
AAudioExtensions::getInstance().setMMapEnabled(wasMMapEnabled);
mDeviceId = mStream->getDeviceId();
return (int32_t) result;
}
int32_t TestColdStartLatency::start() {
mBeginStartNanos = AudioClock::getNanoseconds();
Result result = mStream->requestStart();
int64_t endStartNanos = AudioClock::getNanoseconds();
int64_t actualDurationNanos = endStartNanos - mBeginStartNanos;
mStartTimeMicros = actualDurationNanos / NANOS_PER_MICROSECOND;
return (int32_t) result;
}
int32_t TestColdStartLatency::close() {
Result result1 = mStream->requestStop();
Result result2 = mStream->close();
return (int32_t)((result1 != Result::OK) ? result1 : result2);
}
int32_t TestColdStartLatency::getColdStartTimeMicros() {
int64_t position;
int64_t timestampNanos;
if (mStream->getDirection() == Direction::Output) {
auto result = mStream->getTimestamp(CLOCK_MONOTONIC);
if (!result) {
return -1; // ERROR
}
auto frameTimestamp = result.value();
// Calculate the time that frame[0] would have been played by the speaker.
position = frameTimestamp.position;
timestampNanos = frameTimestamp.timestamp;
} else {
position = mStream->getFramesRead();
timestampNanos = AudioClock::getNanoseconds();
}
double sampleRate = (double) mStream->getSampleRate();
int64_t elapsedNanos = NANOS_PER_SECOND * (position / sampleRate);
int64_t timeOfFrameZero = timestampNanos - elapsedNanos;
int64_t coldStartLatencyNanos = timeOfFrameZero - mBeginStartNanos;
return coldStartLatencyNanos / NANOS_PER_MICROSECOND;
}
// Callback that sleeps then touches the audio buffer.
DataCallbackResult TestColdStartLatency::MyDataCallback::onAudioReady(
AudioStream *audioStream,
void *audioData,
int32_t numFrames) {
float *floatData = (float *) audioData;
const int numSamples = numFrames * kChannelCount;
if (audioStream->getDirection() == Direction::Output) {
// Fill mono buffer with a sine wave.
for (int i = 0; i < numSamples; i++) {
*floatData++ = sinf(mPhase) * 0.2f;
if ((i % kChannelCount) == (kChannelCount - 1)) {
mPhase += kPhaseIncrement;
// Wrap the phase around in a circle.
if (mPhase >= M_PI) mPhase -= 2 * M_PI;
}
}
}
return DataCallbackResult::Continue;
}
@@ -0,0 +1,76 @@
/*
* 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_TEST_COLD_START_LATENCY_H
#define OBOETESTER_TEST_COLD_START_LATENCY_H
#include "oboe/Oboe.h"
#include <thread>
/**
* Test for getting the cold start latency
*/
class TestColdStartLatency {
public:
int32_t open(bool useInput, bool useLowLatency, bool useMmap, bool useExclusive);
int32_t start();
int32_t close();
int32_t getColdStartTimeMicros();
int32_t getOpenTimeMicros() {
return (int32_t) (mOpenTimeMicros.load());
}
int32_t getStartTimeMicros() {
return (int32_t) (mStartTimeMicros.load());
}
int32_t getDeviceId() {
return mDeviceId;
}
protected:
std::atomic<int64_t> mBeginStartNanos{0};
std::atomic<double> mOpenTimeMicros{0};
std::atomic<double> mStartTimeMicros{0};
std::atomic<double> mColdStartTimeMicros{0};
std::atomic<int32_t> mDeviceId{0};
private:
class MyDataCallback : public oboe::AudioStreamDataCallback { public:
MyDataCallback() {}
oboe::DataCallbackResult onAudioReady(
oboe::AudioStream *audioStream,
void *audioData,
int32_t numFrames) override;
private:
// For sine generator.
float mPhase = 0.0f;
static constexpr float kPhaseIncrement = 2.0f * (float) M_PI * 440.0f / 48000.0f;
};
std::shared_ptr<oboe::AudioStream> mStream;
std::shared_ptr<MyDataCallback> mDataCallback;
static constexpr int kChannelCount = 1;
};
#endif //OBOETESTER_TEST_COLD_START_LATENCY_H
@@ -0,0 +1,75 @@
/*
* Copyright 2022 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 <stdlib.h>
#include "common/OboeDebug.h"
#include "TestErrorCallback.h"
using namespace oboe;
oboe::Result TestErrorCallback::open() {
mCallbackMagic = 0;
mDataCallback = std::make_shared<MyDataCallback>();
mErrorCallback = std::make_shared<MyErrorCallback>(this);
AudioStreamBuilder builder;
oboe::Result result = builder.setSharingMode(oboe::SharingMode::Exclusive)
->setPerformanceMode(oboe::PerformanceMode::LowLatency)
->setFormat(oboe::AudioFormat::Float)
->setChannelCount(kChannelCount)
#if 0
->setDataCallback(mDataCallback.get())
->setErrorCallback(mErrorCallback.get()) // This can lead to a crash or FAIL.
#else
->setDataCallback(mDataCallback)
->setErrorCallback(mErrorCallback) // shared_ptr avoids a crash
#endif
->openStream(mStream);
return result;
}
oboe::Result TestErrorCallback::start() {
return mStream->requestStart();
}
oboe::Result TestErrorCallback::stop() {
return mStream->requestStop();
}
oboe::Result TestErrorCallback::close() {
return mStream->close();
}
int TestErrorCallback::test() {
oboe::Result result = open();
if (result != oboe::Result::OK) {
return (int) result;
}
return (int) start();
}
DataCallbackResult TestErrorCallback::MyDataCallback::onAudioReady(
AudioStream *audioStream,
void *audioData,
int32_t numFrames) {
float *output = (float *) audioData;
// Fill buffer with random numbers to create "white noise".
int numSamples = numFrames * kChannelCount;
for (int i = 0; i < numSamples; i++) {
*output++ = (float)((drand48() - 0.5) * 0.2);
}
return oboe::DataCallbackResult::Continue;
}
@@ -0,0 +1,113 @@
/*
* Copyright 2022 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_TEST_ERROR_CALLBACK_H
#define OBOETESTER_TEST_ERROR_CALLBACK_H
#include "oboe/Oboe.h"
#include <thread>
/**
* This code is an experiment to see if we can cause a crash from the ErrorCallback.
*/
class TestErrorCallback {
public:
oboe::Result open();
oboe::Result start();
oboe::Result stop();
oboe::Result close();
int test();
int32_t getCallbackMagic() {
return mCallbackMagic.load();
}
protected:
std::atomic<int32_t> mCallbackMagic{0};
private:
void cleanup() {
mDataCallback.reset();
mErrorCallback.reset();
mStream.reset();
}
class MyDataCallback : public oboe::AudioStreamDataCallback { public:
oboe::DataCallbackResult onAudioReady(
oboe::AudioStream *audioStream,
void *audioData,
int32_t numFrames) override;
};
class MyErrorCallback : public oboe::AudioStreamErrorCallback {
public:
MyErrorCallback(TestErrorCallback *parent): mParent(parent) {}
virtual ~MyErrorCallback() {
// If the delete occurs before onErrorAfterClose() then this bad magic
// value will be seen by the Java test code, causing a failure.
// It is also possible that this code will just cause OboeTester to crash!
mMagic = 0xdeadbeef;
LOGE("%s() called", __func__);
}
void onErrorBeforeClose(oboe::AudioStream *oboeStream, oboe::Result error) override {
LOGE("%s() - error = %s, parent = %p",
__func__, oboe::convertToText(error), &mParent);
// Trigger a crash by "deleting" this callback object while in use!
// Do not try this at home. We are just trying to reproduce the crash
// reported in #1603.
std::thread t([this]() {
this->mParent->cleanup(); // Possibly delete stream and callback objects.
LOGE("onErrorBeforeClose called cleanup!");
});
t.detach();
// There is a race condition between the deleting thread and this thread.
// We do not want to add synchronization because the object is getting deleted
// and cannot be relied on.
// So we sleep here to give the deleting thread a chance to win the race.
usleep(10 * 1000);
}
void onErrorAfterClose(oboe::AudioStream *oboeStream, oboe::Result error) override {
// The callback was probably deleted by now.
LOGE("%s() - error = %s, mMagic = 0x%08X",
__func__, oboe::convertToText(error), mMagic.load());
mParent->mCallbackMagic = mMagic.load();
}
private:
TestErrorCallback *mParent;
// This must match the value in TestErrorCallbackActivity.java
static constexpr int32_t kMagicGood = 0x600DCAFE;
std::atomic<int32_t> mMagic{kMagicGood};
};
std::shared_ptr<oboe::AudioStream> mStream;
std::shared_ptr<MyDataCallback> mDataCallback;
std::shared_ptr<MyErrorCallback> mErrorCallback;
static constexpr int kChannelCount = 2;
};
#endif //OBOETESTER_TEST_ERROR_CALLBACK_H

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