Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 872d6b4516 | |||
| def270f7c7 | |||
| 93e336b7cb | |||
| 4c8c1cf36b | |||
| 7d9e99ea18 | |||
| b27e5ab017 | |||
| fd9f3da91a | |||
| a7c19be88f | |||
| 9ab80adaca | |||
| 518d61e039 | |||
| 4349a1142f | |||
| 9713977755 | |||
| 63448ca640 | |||
| b36d38815f | |||
| e24f812475 | |||
| fddf619ed2 | |||
| bf83509ef2 | |||
| 6c05b854c4 | |||
| 2baafab027 | |||
| 813f9d959b | |||
| fba87f028c | |||
| cb4647daf0 | |||
| 93374fbbbb | |||
| abe176b26c | |||
| 085180992d | |||
| 9984958193 | |||
| 5871469002 | |||
| 302b2c9591 | |||
| 67a170fd6e | |||
| 66079686fc | |||
| cc5ff987ac | |||
| f8ba51636b | |||
| fb97318e87 | |||
| 580449b8df | |||
| fd73ab2a11 | |||
| 48f710c3d6 | |||
| 794a5fc6cb | |||
| 286988a1e3 | |||
| 48a3140a28 | |||
| f7bdaa81f8 | |||
| a1ab5e2cff | |||
| 99806ead25 | |||
| def967a8f8 | |||
| 7583ba8729 | |||
| fc83ecd52f |
@@ -0,0 +1 @@
|
||||
* @bryanthaboi
|
||||
@@ -5,6 +5,10 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
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.
|
||||
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.
|
||||
@@ -51,22 +55,24 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: mods_enabled
|
||||
- type: checkboxes
|
||||
id: mods_off
|
||||
attributes:
|
||||
label: Were any mods on
|
||||
description: Check the MODS tab in the launcher if you're not sure.
|
||||
label: Mods off
|
||||
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:
|
||||
- "No"
|
||||
- "Yes"
|
||||
validations:
|
||||
required: true
|
||||
- label: I turned off all mods and can still reproduce this
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: mods_which
|
||||
attributes:
|
||||
label: Which mods (if any were on)
|
||||
description: List the enabled mods. Leave blank if none were on.
|
||||
label: Which mods (if you first noticed this with any 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
|
||||
validations:
|
||||
required: false
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
[](https://www.youtube.com/watch?v=8IOgqbe4YvA)
|
||||
[](https://youtu.be/yi7LkWQPKKM)
|
||||
|
||||
|
||||
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
|
||||
[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
|
||||
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
|
||||
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
|
||||
|
||||
- [Link play](https://github.com/bryanthaboi/gen1recomp/wiki/Guide-Link-Play)
|
||||
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -5,8 +5,27 @@
|
||||
-- voucher exchange and the BICYCLE/CANCEL price window need more than
|
||||
-- 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 {
|
||||
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 = {
|
||||
-- BikeShopMiddleAgedWomanText (pokered/scripts/BikeShop.asm):
|
||||
-- always shows the same flavor line, no branching.
|
||||
|
||||
@@ -168,6 +168,7 @@ return {
|
||||
{ "jump_if_true", "come_see" },
|
||||
{ "set_flag", "EVENT_GOT_POKEBALLS_FROM_OAK" },
|
||||
{ "give_item", "POKE_BALL", 5, false },
|
||||
{ "text_sound", "Get_Key_Item" }, -- OaksLab.asm:1060
|
||||
{ "show_text", "_OaksLabOak1ReceivedPokeballsText" },
|
||||
{ "show_text", "_OaksLabGivePokeballsExplanationText" },
|
||||
{ "jump", "end" },
|
||||
|
||||
@@ -516,7 +516,17 @@ M.ROUTE_24 = {
|
||||
push(game, text(game)._Route24CooltrainerM1YouCouldBecomeATopLeaderText,
|
||||
done)
|
||||
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
|
||||
if not flags.EVENT_GOT_NUGGET then
|
||||
|
||||
@@ -7,9 +7,9 @@ local M = {}
|
||||
|
||||
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")
|
||||
game.stack:push(TextBox.new(game, s, done))
|
||||
game.stack:push(TextBox.new(game, s, done, opts))
|
||||
end
|
||||
|
||||
-- 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
|
||||
-- CinnabarGymQuizCorrectText: item jingle, then the gate
|
||||
-- slides open (SFX_GO_INSIDE) if it was still locked
|
||||
Sound.play(game.data, "Get_Item1")
|
||||
push(game, t._CinnabarGymQuizCorrectText
|
||||
or "You're absolutely\ncorrect!\fGo on through!", function()
|
||||
if not game.save.flags[gymGateFlag(index)] then
|
||||
@@ -244,7 +243,9 @@ M.CINNABAR_GYM = {
|
||||
Sound.play(game.data, "Go_Inside")
|
||||
end
|
||||
applyGymGates(game, ow)
|
||||
end)
|
||||
end, { preSound = function()
|
||||
return Sound.play(game.data, "Get_Item1")
|
||||
end })
|
||||
return
|
||||
end
|
||||
Sound.play(game.data, "Denied")
|
||||
|
||||
@@ -17,9 +17,9 @@ local function surfingPikachu(game)
|
||||
return nil
|
||||
end
|
||||
|
||||
local function push(game, text, done)
|
||||
local function push(game, text, done, opts)
|
||||
local TextBox = require("src.render.TextBox")
|
||||
game.stack:push(TextBox.new(game, text, done))
|
||||
game.stack:push(TextBox.new(game, text, done, opts))
|
||||
end
|
||||
|
||||
-- the two-variant posters: the surf-capable line once a surfing
|
||||
@@ -69,11 +69,11 @@ return {
|
||||
|
||||
TEXT_SUMMERBEACHHOUSE_PIKACHU = function(game, ow, npc, done)
|
||||
local t = game.data.text
|
||||
-- scripts/SummerBeachHouse.asm:68
|
||||
push(game, t._SummerBeachHousePikachuText or "PIKACHU: Pikaa!",
|
||||
function()
|
||||
require("src.core.Sound").playCry(game.data, "PIKACHU")
|
||||
done()
|
||||
end)
|
||||
done, { auto = { wait = true, delay = 0, sound = function()
|
||||
return require("src.core.Sound").playCry(game.data, "PIKACHU")
|
||||
end } })
|
||||
end,
|
||||
|
||||
TEXT_SUMMERBEACHHOUSE_POSTER1 = poster(1),
|
||||
|
||||
@@ -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
|
||||
numeric flag space forces.
|
||||
- *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.list_menu`, `transition.style`. `ui.list_menu` covers Gold's script
|
||||
`ui.options.rows`, `ui.party.submenu`, `ui.party.grid_navigation`,
|
||||
`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 draw with does not raise it yet, so those two are composed through
|
||||
their own hooks only.
|
||||
|
||||
@@ -11,9 +11,8 @@ Features intentionally added beyond the original Pokémon Red, Blue, and Yellow
|
||||
* **Persistent custom options** stored separately from game saves
|
||||
* **Optional widescreen battle layout**
|
||||
* **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**
|
||||
* **Mod download counts** from the index feed, with Most-downloaded and Trending sorts
|
||||
|
||||
## Gen 2 Specifics
|
||||
|
||||
|
||||
@@ -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:
|
||||
`src/core/TouchSkin.lua` (model, parsers, zip export), `src/core/TouchControls.lua`
|
||||
(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:
|
||||
`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/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
|
||||
opens the studio. `options.touchControls.skin` holds the folder name.
|
||||
|
||||
## 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
|
||||
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
|
||||
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
|
||||
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
|
||||
@@ -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
|
||||
|
||||
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
|
||||
|
||||
Drop a folder or a `.zip` into `skins/` in the save directory, or drop a zip on
|
||||
the launcher window while the Skins tab is open. A zip is mounted in place, so
|
||||
there is nothing to unpack. The folder needs one `skin.lua` or `.cfg`
|
||||
(`overlay.cfg` is preferred when there are several) and the images it names.
|
||||
Four roads, all of them landing in `skins/` in the save directory:
|
||||
|
||||
* **Import** on the Skins tab opens the host file picker for a `.zip` or a
|
||||
`.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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
**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
|
||||
coordinate its art was drawn at. Bind, hitbox shape, hit reach and idle and
|
||||
resize. Arrow keys nudge the selection one canvas pixel, shift-arrow ten. While
|
||||
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
|
||||
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
|
||||
(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
|
||||
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
|
||||
(the auto-rotate convention) locks those pages and turns Match canvas on
|
||||
when you open it. You do not have to click Lock first.
|
||||
|
||||
**Art.** The **Bezel**, **Idle art** and **Pressed art** rows cycle through the
|
||||
images already in the skin folder; the **Import** button beside each one opens
|
||||
the host file picker (`src/core/FilePicker.lua`: osascript, PowerShell,
|
||||
**Art.** The **Bezel**, **Idle art** and **Pressed art** rows open a
|
||||
thumbnail grid of the images already in the skin folder, with `(none)` first;
|
||||
the **Import** button there and beside each row opens the host file picker (`src/core/FilePicker.lua`: osascript, PowerShell,
|
||||
zenity/kdialog) and copies the chosen PNG or JPG into `img/` under the name in
|
||||
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
|
||||
@@ -185,11 +255,24 @@ buttons and the footer reports what is held. **Play** saves the skin, selects
|
||||
it, and boots the game with it.
|
||||
|
||||
**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
|
||||
(`src/core/SkinZip.lua`, store-only) carrying the native `skin.lua`, the
|
||||
images, and the original `.cfg` when it came from one. An exported skin drops
|
||||
straight back into `skins/` and still opens in RetroArch.
|
||||
skin names, so the folder stands alone. **Export** offers three formats, and
|
||||
the Skins tab's gear offers the same three for any installed skin:
|
||||
|
||||
| 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
|
||||
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
@@ -291,6 +291,78 @@ function closeSkinStudio()
|
||||
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)
|
||||
-- The launcher hands us the chosen game (Red / Blue / Yellow / Gold);
|
||||
-- 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
|
||||
-- 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.
|
||||
require("src.core.Orientation").applyOptions(
|
||||
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
|
||||
-- restart could: the ordering is the same on every launch. Read the
|
||||
-- enabled mods' string catalogs -- data only, no entry chunk -- so a
|
||||
-- translation reaches the launcher too. Game:load replaces this with the
|
||||
-- real merged catalog once a version boots.
|
||||
-- translation reaches the launcher too. The active game's loader replaces
|
||||
-- this with the real merged catalog once a version boots.
|
||||
do
|
||||
local preload = require("src.mods.LauncherMods").translationStrings()
|
||||
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
|
||||
-- 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).
|
||||
Importer = RomImporter.new(function(version)
|
||||
Importer = nil
|
||||
bootGame(version)
|
||||
end, {
|
||||
launcher = true,
|
||||
forceImport = forceImport,
|
||||
onEditSave = openEditor,
|
||||
onEditTouchControls = openTouchControlsEditor,
|
||||
onOpenSkinStudio = require("src.ui.SkinStudio").available_desktop()
|
||||
and openSkinStudio or nil,
|
||||
})
|
||||
Importer = makeLauncher()
|
||||
end
|
||||
|
||||
function love.update(dt)
|
||||
@@ -808,6 +870,46 @@ function love.lowmemory()
|
||||
if Game then Game:onResume() 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)
|
||||
if editorMode then
|
||||
-- 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
|
||||
-- this Lua launcher via that hook. Vanilla behavior (used when no mod
|
||||
-- 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()
|
||||
return Game and not Importer and not quitToLauncher and not scripted
|
||||
and not launchedIntoGame
|
||||
and (isAndroid or not launchedIntoGame)
|
||||
end)
|
||||
if wouldReturnToLauncher then
|
||||
if isAndroid then
|
||||
returnToLauncher()
|
||||
return true -- abort this quit; the restart lands back in the launcher
|
||||
end
|
||||
quitToLauncher = true
|
||||
-- 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
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/love"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:label="${NAME}" >
|
||||
<meta-data
|
||||
android:name="android.allow_multiple_resumed_activities"
|
||||
@@ -39,7 +40,7 @@
|
||||
android:exported="true"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
|
||||
android:label="${NAME}"
|
||||
android:launchMode="singleInstance"
|
||||
android:launchMode="singleTask"
|
||||
android:screenOrientation="${ORIENTATION}"
|
||||
android:resizeableActivity="false"
|
||||
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" >
|
||||
|
||||
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
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="colorPrimaryDark">#303F9F</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>
|
||||
|
||||
@@ -45,6 +45,11 @@
|
||||
// own, which can name a different volume on merged / adopted-SD storage.
|
||||
#include "filesystem/Filesystem.h"
|
||||
|
||||
#include "common/Module.h"
|
||||
#include "audio/Audio.h"
|
||||
#include "audio/openal/Audio.h"
|
||||
#include "event/Event.h"
|
||||
|
||||
namespace love
|
||||
{
|
||||
namespace android
|
||||
@@ -278,6 +283,70 @@ bool restartApp()
|
||||
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)
|
||||
{
|
||||
if (url == nullptr || destPath == nullptr)
|
||||
@@ -374,6 +443,104 @@ bool httpPost(const char *url, const char *body, int bodyLen, const char *conten
|
||||
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
|
||||
* 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();
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -90,6 +90,16 @@ bool syncHealthSteps();
|
||||
**/
|
||||
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
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* 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).
|
||||
* LuaSocket, which is what LOVE ships, does TCP only, so wss:// is otherwise
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "common/delay.h"
|
||||
#include "RecordingDevice.h"
|
||||
#include "sound/Decoder.h"
|
||||
#include "event/Event.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
@@ -30,6 +31,10 @@
|
||||
#include "common/ios.h"
|
||||
#endif
|
||||
|
||||
#ifndef ALC_CONNECTED
|
||||
#define ALC_CONNECTED 0x313
|
||||
#endif
|
||||
|
||||
namespace love
|
||||
{
|
||||
namespace audio
|
||||
@@ -37,9 +42,35 @@ namespace audio
|
||||
namespace openal
|
||||
{
|
||||
|
||||
Audio::PoolThread::PoolThread(Pool *pool)
|
||||
: pool(pool)
|
||||
static const int DISCONNECT_CHECK_INTERVAL = 200;
|
||||
|
||||
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)
|
||||
, paused(false)
|
||||
{
|
||||
threadName = "AudioPool";
|
||||
}
|
||||
@@ -51,6 +82,8 @@ Audio::PoolThread::~PoolThread()
|
||||
|
||||
void Audio::PoolThread::threadFunction()
|
||||
{
|
||||
int disconnectCheck = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
{
|
||||
@@ -61,7 +94,23 @@ void Audio::PoolThread::threadFunction()
|
||||
}
|
||||
}
|
||||
|
||||
if (paused.load())
|
||||
{
|
||||
disconnectCheck = 0;
|
||||
sleep(5);
|
||||
continue;
|
||||
}
|
||||
|
||||
pool->update();
|
||||
|
||||
if (audio != nullptr && ++disconnectCheck >= DISCONNECT_CHECK_INTERVAL)
|
||||
{
|
||||
disconnectCheck = 0;
|
||||
|
||||
if (!audio->isDeviceConnected() && audio->reopenDevice())
|
||||
pushAudioResetEvent();
|
||||
}
|
||||
|
||||
sleep(5);
|
||||
}
|
||||
}
|
||||
@@ -72,6 +121,11 @@ void Audio::PoolThread::setFinish()
|
||||
finish = true;
|
||||
}
|
||||
|
||||
void Audio::PoolThread::setPaused(bool paused)
|
||||
{
|
||||
this->paused.store(paused);
|
||||
}
|
||||
|
||||
ALenum Audio::getFormat(int bitDepth, int channels)
|
||||
{
|
||||
if (bitDepth != 8 && bitDepth != 16)
|
||||
@@ -99,6 +153,8 @@ Audio::Audio()
|
||||
, pool(nullptr)
|
||||
, poolThread(nullptr)
|
||||
, distanceModel(DISTANCE_INVERSE_CLAMPED)
|
||||
, alcReopenDeviceSOFT(nullptr)
|
||||
, reopenChecked(false)
|
||||
{
|
||||
// Before opening new device, check if recording
|
||||
// is requested.
|
||||
@@ -189,13 +245,6 @@ Audio::Audio()
|
||||
throw;
|
||||
}
|
||||
|
||||
poolThread = new PoolThread(pool);
|
||||
poolThread->start();
|
||||
|
||||
#ifdef LOVE_IOS
|
||||
love::ios::initAudioSessionInterruptionHandler();
|
||||
#endif
|
||||
|
||||
#ifdef LOVE_ANDROID
|
||||
bool hasPauseDeviceExt = alcIsExtensionPresent(device, "ALC_SOFT_pause_device") == ALC_TRUE;
|
||||
alcDevicePauseSOFT = hasPauseDeviceExt
|
||||
@@ -205,6 +254,13 @@ Audio::Audio()
|
||||
? (LPALCDEVICERESUMESOFT) alcGetProcAddress(device, "alcDeviceResumeSOFT")
|
||||
: nullptr;
|
||||
#endif
|
||||
|
||||
poolThread = new PoolThread(this, pool);
|
||||
poolThread->start();
|
||||
|
||||
#ifdef LOVE_IOS
|
||||
love::ios::initAudioSessionInterruptionHandler();
|
||||
#endif
|
||||
}
|
||||
|
||||
Audio::~Audio()
|
||||
@@ -314,6 +370,9 @@ std::vector<love::audio::Source*> Audio::pause()
|
||||
|
||||
void Audio::pauseContext()
|
||||
{
|
||||
if (poolThread != nullptr)
|
||||
poolThread->setPaused(true);
|
||||
|
||||
#ifdef LOVE_ANDROID
|
||||
if (alcDevicePauseSOFT)
|
||||
alcDevicePauseSOFT(device);
|
||||
@@ -350,6 +409,52 @@ void Audio::resumeContext()
|
||||
if (context && alcGetCurrentContext() != context)
|
||||
alcMakeContextCurrent(context);
|
||||
#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)
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#define LOVE_AUDIO_OPENAL_AUDIO_H
|
||||
|
||||
// STD
|
||||
#include <atomic>
|
||||
#include <queue>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
@@ -97,6 +98,8 @@ public:
|
||||
std::vector<love::audio::Source*> pause();
|
||||
void pauseContext();
|
||||
void resumeContext();
|
||||
bool reopenDevice();
|
||||
bool isDeviceConnected();
|
||||
void setVolume(float volume);
|
||||
float getVolume() const;
|
||||
|
||||
@@ -155,6 +158,7 @@ private:
|
||||
class PoolThread: public thread::Threadable
|
||||
{
|
||||
protected:
|
||||
Audio *audio;
|
||||
Pool *pool;
|
||||
|
||||
// Set this to true when the thread should finish.
|
||||
@@ -162,13 +166,16 @@ private:
|
||||
// will read from it.
|
||||
volatile bool finish;
|
||||
|
||||
std::atomic<bool> paused;
|
||||
|
||||
// finish lock
|
||||
love::thread::MutexRef mutex;
|
||||
|
||||
public:
|
||||
PoolThread(Pool *pool);
|
||||
PoolThread(Audio *audio, Pool *pool);
|
||||
virtual ~PoolThread();
|
||||
void setFinish();
|
||||
void setPaused(bool paused);
|
||||
void threadFunction();
|
||||
};
|
||||
|
||||
@@ -177,6 +184,13 @@ private:
|
||||
DistanceModel distanceModel;
|
||||
//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
|
||||
# ifndef ALC_SOFT_pause_device
|
||||
typedef void (ALC_APIENTRY*LPALCDEVICEPAUSESOFT)(ALCdevice *device);
|
||||
@@ -188,6 +202,9 @@ private:
|
||||
#endif
|
||||
}; // Audio
|
||||
|
||||
void pushAudioSuspendEvent();
|
||||
void pushAudioResetEvent();
|
||||
|
||||
#ifdef ALC_EXT_EFX
|
||||
// Effect objects
|
||||
extern LPALGENEFFECTS alGenEffects;
|
||||
|
||||
@@ -245,6 +245,25 @@ bool System::restartApp() const
|
||||
#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,
|
||||
const char *userAgent, const char *accept) const
|
||||
{
|
||||
@@ -274,6 +293,27 @@ bool System::httpPost(const char *url, const char *body, int bodyLen,
|
||||
#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
|
||||
{
|
||||
#ifdef LOVE_ANDROID
|
||||
|
||||
@@ -143,6 +143,9 @@ public:
|
||||
**/
|
||||
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
|
||||
* 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,
|
||||
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
|
||||
* LuaSec or another provider is the answer). Non-blocking by contract:
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
#include "wrap_System.h"
|
||||
#include "sdl/System.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace love
|
||||
{
|
||||
namespace system
|
||||
@@ -150,6 +153,57 @@ int w_httpPost(lua_State *L)
|
||||
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)
|
||||
{
|
||||
lua_pushboolean(L, instance()->hasBackgroundMusic());
|
||||
@@ -229,6 +283,34 @@ int w_tlsClose(lua_State *L)
|
||||
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[] =
|
||||
{
|
||||
{ "getOS", w_getOS },
|
||||
@@ -243,8 +325,11 @@ static const luaL_Reg functions[] =
|
||||
{ "createFile", w_createFile },
|
||||
{ "syncHealthSteps", w_syncHealthSteps },
|
||||
{ "restartApp", w_restartApp },
|
||||
{ "updateShortcuts", w_updateShortcuts },
|
||||
{ "getLaunchGame", w_getLaunchGame },
|
||||
{ "httpDownload", w_httpDownload },
|
||||
{ "httpPost", w_httpPost },
|
||||
{ "httpRequest", w_httpRequest },
|
||||
{ "tlsOpen", w_tlsOpen },
|
||||
{ "tlsStatus", w_tlsStatus },
|
||||
{ "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.
|
||||
@@ -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/
|
||||
.idea
|
||||
build
|
||||
.logpile
|
||||
|
||||
|
||||
@@ -1,32 +1,21 @@
|
||||
LOCAL_PATH:= $(call my-dir)
|
||||
include $(CLEAR_VARS)
|
||||
|
||||
#
|
||||
# Module
|
||||
#
|
||||
LOCAL_MODULE := oboe
|
||||
LOCAL_ARM_NEON := true
|
||||
|
||||
#
|
||||
# Flags
|
||||
#
|
||||
LOCAL_CFLAGS := -Wall -Wextra-semi -Wshadow -Wshadow-field
|
||||
LOCAL_CPPFLAGS := -std=c++14
|
||||
LOCAL_CPPFLAGS := -std=c++17
|
||||
|
||||
#
|
||||
# Include paths
|
||||
#
|
||||
LOCAL_C_INCLUDES := \
|
||||
$(LOCAL_PATH)/include \
|
||||
$(LOCAL_PATH)/src
|
||||
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
||||
|
||||
#
|
||||
# Source files
|
||||
#
|
||||
LOCAL_SRC_FILES := \
|
||||
src/aaudio/AAudioLoader.cpp \
|
||||
src/aaudio/AudioStreamAAudio.cpp \
|
||||
src/common/AdpfWrapper.cpp \
|
||||
src/common/AudioSourceCaller.cpp \
|
||||
src/common/AudioStream.cpp \
|
||||
src/common/AudioStreamBuilder.cpp \
|
||||
@@ -36,8 +25,11 @@ LOCAL_SRC_FILES := \
|
||||
src/common/FixedBlockReader.cpp \
|
||||
src/common/FixedBlockWriter.cpp \
|
||||
src/common/LatencyTuner.cpp \
|
||||
src/common/OboeExtensions.cpp \
|
||||
src/common/SourceFloatCaller.cpp \
|
||||
src/common/SourceI16Caller.cpp \
|
||||
src/common/SourceI24Caller.cpp \
|
||||
src/common/SourceI32Caller.cpp \
|
||||
src/common/Utilities.cpp \
|
||||
src/common/QuirksManager.cpp \
|
||||
src/fifo/FifoBuffer.cpp \
|
||||
@@ -45,17 +37,26 @@ LOCAL_SRC_FILES := \
|
||||
src/fifo/FifoControllerBase.cpp \
|
||||
src/fifo/FifoControllerIndirect.cpp \
|
||||
src/flowgraph/FlowGraphNode.cpp \
|
||||
src/flowgraph/ChannelCountConverter.cpp \
|
||||
src/flowgraph/ClipToRange.cpp \
|
||||
src/flowgraph/Limiter.cpp \
|
||||
src/flowgraph/ManyToMultiConverter.cpp \
|
||||
src/flowgraph/MonoBlend.cpp \
|
||||
src/flowgraph/MonoToMultiConverter.cpp \
|
||||
src/flowgraph/MultiToManyConverter.cpp \
|
||||
src/flowgraph/MultiToMonoConverter.cpp \
|
||||
src/flowgraph/RampLinear.cpp \
|
||||
src/flowgraph/SampleRateConverter.cpp \
|
||||
src/flowgraph/SinkFloat.cpp \
|
||||
src/flowgraph/SinkI16.cpp \
|
||||
src/flowgraph/SinkI24.cpp \
|
||||
src/flowgraph/SinkI32.cpp \
|
||||
src/flowgraph/SinkI8_24.cpp \
|
||||
src/flowgraph/SourceFloat.cpp \
|
||||
src/flowgraph/SourceI16.cpp \
|
||||
src/flowgraph/SourceI24.cpp \
|
||||
src/flowgraph/SourceI32.cpp \
|
||||
src/flowgraph/SourceI8_24.cpp \
|
||||
src/flowgraph/resampler/IntegerRatio.cpp \
|
||||
src/flowgraph/resampler/LinearResampler.cpp \
|
||||
src/flowgraph/resampler/MultiChannelResampler.cpp \
|
||||
@@ -75,10 +76,6 @@ LOCAL_SRC_FILES := \
|
||||
src/common/Trace.cpp \
|
||||
src/common/Version.cpp
|
||||
|
||||
#
|
||||
# Libraries related
|
||||
#
|
||||
LOCAL_LDLIBS := -llog
|
||||
|
||||
# Build
|
||||
include $(BUILD_STATIC_LIBRARY)
|
||||
|
||||
@@ -9,6 +9,7 @@ project(oboe)
|
||||
set (oboe_sources
|
||||
src/aaudio/AAudioLoader.cpp
|
||||
src/aaudio/AudioStreamAAudio.cpp
|
||||
src/common/AdpfWrapper.cpp
|
||||
src/common/AudioSourceCaller.cpp
|
||||
src/common/AudioStream.cpp
|
||||
src/common/AudioStreamBuilder.cpp
|
||||
@@ -18,26 +19,38 @@ set (oboe_sources
|
||||
src/common/FixedBlockReader.cpp
|
||||
src/common/FixedBlockWriter.cpp
|
||||
src/common/LatencyTuner.cpp
|
||||
src/common/OboeExtensions.cpp
|
||||
src/common/SourceFloatCaller.cpp
|
||||
src/common/SourceI16Caller.cpp
|
||||
src/common/SourceI24Caller.cpp
|
||||
src/common/SourceI32Caller.cpp
|
||||
src/common/Utilities.cpp
|
||||
src/common/QuirksManager.cpp
|
||||
src/fifo/FifoBuffer.cpp
|
||||
src/fifo/FifoController.cpp
|
||||
src/fifo/FifoControllerBase.cpp
|
||||
src/fifo/FifoControllerIndirect.cpp
|
||||
src/flowgraph/FlowGraphNode.cpp
|
||||
src/flowgraph/FlowGraphNode.cpp
|
||||
src/flowgraph/ChannelCountConverter.cpp
|
||||
src/flowgraph/ClipToRange.cpp
|
||||
src/flowgraph/Limiter.cpp
|
||||
src/flowgraph/ManyToMultiConverter.cpp
|
||||
src/flowgraph/MonoBlend.cpp
|
||||
src/flowgraph/MonoToMultiConverter.cpp
|
||||
src/flowgraph/MultiToManyConverter.cpp
|
||||
src/flowgraph/MultiToMonoConverter.cpp
|
||||
src/flowgraph/RampLinear.cpp
|
||||
src/flowgraph/SampleRateConverter.cpp
|
||||
src/flowgraph/SinkFloat.cpp
|
||||
src/flowgraph/SinkI16.cpp
|
||||
src/flowgraph/SinkI24.cpp
|
||||
src/flowgraph/SinkI32.cpp
|
||||
src/flowgraph/SinkI8_24.cpp
|
||||
src/flowgraph/SourceFloat.cpp
|
||||
src/flowgraph/SourceI16.cpp
|
||||
src/flowgraph/SourceI24.cpp
|
||||
src/flowgraph/SourceI32.cpp
|
||||
src/flowgraph/SourceI8_24.cpp
|
||||
src/flowgraph/resampler/IntegerRatio.cpp
|
||||
src/flowgraph/resampler/LinearResampler.cpp
|
||||
src/flowgraph/resampler/MultiChannelResampler.cpp
|
||||
@@ -70,18 +83,23 @@ target_include_directories(oboe
|
||||
# Enable -Ofast
|
||||
target_compile_options(oboe
|
||||
PRIVATE
|
||||
-std=c++14
|
||||
-std=c++17
|
||||
-Wall
|
||||
-Wextra-semi
|
||||
-Wshadow
|
||||
-Wshadow-field
|
||||
-Ofast
|
||||
"$<$<CONFIG:RELEASE>:-Ofast>"
|
||||
"$<$<CONFIG:DEBUG>:-O3>"
|
||||
"$<$<CONFIG:DEBUG>:-Werror>")
|
||||
|
||||
# Enable logging of D,V for debug builds
|
||||
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_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
|
||||
install(TARGETS oboe
|
||||
@@ -89,4 +107,4 @@ install(TARGETS oboe
|
||||
ARCHIVE DESTINATION lib/${ANDROID_ABI})
|
||||
|
||||
# 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.
|
||||
@@ -38,7 +38,7 @@ PROJECT_NAME = "Oboe"
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 1.2
|
||||
PROJECT_NUMBER =
|
||||
|
||||
# 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
|
||||
@@ -58,7 +58,7 @@ PROJECT_LOGO =
|
||||
# entered, it will be relative to the location where doxygen was started. If
|
||||
# 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-
|
||||
# directories (in 2 levels) under the output directory of each output format and
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Please see the README.md file for more information.
|
||||
@@ -1,4 +1,4 @@
|
||||
# Oboe [](https://travis-ci.org/google/oboe)
|
||||
# Oboe [](https://github.com/google/oboe/actions)
|
||||
|
||||
[](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
|
||||
- Automatic latency tuning
|
||||
- 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
|
||||
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
|
||||
## Documentation
|
||||
- [Getting Started Guide](docs/GettingStarted.md)
|
||||
- [Full Guide to Oboe](docs/FullGuide.md)
|
||||
- [API reference](https://google.github.io/oboe/reference)
|
||||
- [Tech Notes](docs/notes/)
|
||||
- [API reference](https://google.github.io/oboe)
|
||||
- [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)
|
||||
- [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.
|
||||
|
||||
### Community
|
||||
- Reddit: [r/androidaudiodev](https://www.reddit.com/r/androidaudiodev/)
|
||||
- StackOverflow: [#oboe](https://stackoverflow.com/questions/tagged/oboe)
|
||||
|
||||
## Testing
|
||||
- [**OboeTester** app for measuring latency, glitches, etc.](https://github.com/google/oboe/tree/master/apps/OboeTester/docs)
|
||||
- [Oboe unit tests](https://github.com/google/oboe/tree/master/tests)
|
||||
- [**OboeTester** app for measuring latency, glitches, etc.](apps/OboeTester/docs)
|
||||
- [Oboe unit tests](tests)
|
||||
|
||||
## Videos
|
||||
- [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)
|
||||
- [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)
|
||||
- [Real-Time Processing on Android](https://youtu.be/hY9BrS2uX-c) (ADC '19)
|
||||
|
||||
## Sample code and apps
|
||||
- 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).
|
||||
- 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
|
||||
- [Ableton Link integration demo](https://github.com/jbloit/AndroidLinkAudio) (author: jbloit)
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
/build/
|
||||
.idea/
|
||||
/app/build/
|
||||
/app/release/
|
||||
/app/debug/
|
||||
/app/app.iml
|
||||
*.iml
|
||||
/app/externalNativeBuild/
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
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_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3")
|
||||
|
||||
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/*)
|
||||
|
||||
### Name must match loadLibrary() call in MainActivity.java
|
||||
@@ -30,5 +33,4 @@ include_directories(
|
||||
|
||||
# link to oboe
|
||||
target_link_libraries(oboetester log oboe atomic)
|
||||
|
||||
# bump 2 to resync CMake
|
||||
target_link_options(oboetester PRIVATE "-Wl,-z,max-page-size=16384")
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
compileSdkVersion 34
|
||||
defaultConfig {
|
||||
applicationId = "com.google.sample.oboe.manualtest"
|
||||
applicationId = "com.mobileer.oboetester"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 28
|
||||
// Also update the version in the AndroidManifest.xml file.
|
||||
versionCode 32
|
||||
versionName "1.5.24"
|
||||
targetSdkVersion 34
|
||||
versionCode 91
|
||||
versionName "2.7.2"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
cppFlags "-std=c++14"
|
||||
cppFlags "-std=c++17"
|
||||
abiFilters "x86", "x86_64", "armeabi-v7a", "arm64-v8a"
|
||||
}
|
||||
}
|
||||
@@ -31,14 +30,15 @@ android {
|
||||
path "CMakeLists.txt"
|
||||
}
|
||||
}
|
||||
namespace 'com.mobileer.oboetester'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
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'
|
||||
implementation 'com.android.support:appcompat-v7:28.0.0'
|
||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||
}
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
}
|
||||
|
||||
@@ -1,100 +1,138 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.sample.oboe.manualtest"
|
||||
android:versionCode="32"
|
||||
android:versionName="1.5.24">
|
||||
<!-- versionCode and versionName also have to be updated in build.gradle -->
|
||||
|
||||
<uses-feature android:name="android.hardware.microphone" android:required="true" />
|
||||
<uses-feature android:name="android.hardware.audio.output" android:required="true" />
|
||||
<uses-feature android:name="android.software.midi" android:required="true" />
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-feature
|
||||
android:name="android.hardware.microphone"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.audio.output"
|
||||
android:required="true" />
|
||||
<uses-feature
|
||||
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.MODIFY_AUDIO_SETTINGS" />
|
||||
<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_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
|
||||
android:allowBackup="false"
|
||||
android:fullBackupContent="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
android:theme="@style/AppTheme"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:banner="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name="com.google.sample.oboe.manualtest.MainActivity"
|
||||
android:name=".MainActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/app_name"
|
||||
android:screenOrientation="portrait">
|
||||
android:screenOrientation="portrait"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="com.google.sample.oboe.manualtest.TestOutputActivity"
|
||||
android:name=".TestOutputActivity"
|
||||
android:label="@string/title_activity_test_output"
|
||||
android:screenOrientation="portrait">
|
||||
</activity>
|
||||
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="com.google.sample.oboe.manualtest.TestInputActivity"
|
||||
android:name=".TestInputActivity"
|
||||
android:label="@string/title_activity_test_input"
|
||||
android:screenOrientation="portrait">
|
||||
</activity>
|
||||
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="com.google.sample.oboe.manualtest.TapToToneActivity"
|
||||
android:name=".TapToToneActivity"
|
||||
android:label="@string/title_activity_output_latency"
|
||||
android:screenOrientation="portrait">
|
||||
</activity>
|
||||
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="com.google.sample.oboe.manualtest.RecorderActivity"
|
||||
android:name=".RecorderActivity"
|
||||
android:label="@string/title_activity_recorder"
|
||||
android:screenOrientation="portrait">
|
||||
</activity>
|
||||
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="com.google.sample.oboe.manualtest.EchoActivity"
|
||||
android:name=".EchoActivity"
|
||||
android:label="@string/title_activity_echo"
|
||||
android:screenOrientation="portrait">
|
||||
</activity>
|
||||
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="com.google.sample.oboe.manualtest.RoundTripLatencyActivity"
|
||||
android:name=".RoundTripLatencyActivity"
|
||||
android:label="@string/title_activity_rt_latency"
|
||||
android:screenOrientation="portrait">
|
||||
</activity>
|
||||
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="com.google.sample.oboe.manualtest.ManualGlitchActivity"
|
||||
android:name=".ManualGlitchActivity"
|
||||
android:label="@string/title_activity_glitches"
|
||||
android:screenOrientation="portrait">
|
||||
</activity>
|
||||
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="com.google.sample.oboe.manualtest.AutoGlitchActivity"
|
||||
android:label="@string/title_activity_glitches"
|
||||
android:screenOrientation="portrait">
|
||||
</activity>
|
||||
|
||||
android:name=".AutomatedGlitchActivity"
|
||||
android:label="@string/title_activity_auto_glitches"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="com.google.sample.oboe.manualtest.TestDisconnectActivity"
|
||||
android:name=".TestDisconnectActivity"
|
||||
android:label="@string/title_test_disconnect"
|
||||
android:screenOrientation="portrait">
|
||||
</activity>
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name=".DeviceReportActivity"
|
||||
android:label="@string/title_report_devices"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name=".TestDataPathsActivity"
|
||||
android:label="@string/title_data_paths"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name=".ExtraTestsActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/title_extra_tests"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
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
|
||||
android:name="com.google.sample.oboe.manualtest.AudioMidiTester"
|
||||
android:permission="android.permission.BIND_MIDI_DEVICE_SERVICE">
|
||||
android:name=".MidiTapTester"
|
||||
android:permission="android.permission.BIND_MIDI_DEVICE_SERVICE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.media.midi.MidiDeviceService" />
|
||||
</intent-filter>
|
||||
@@ -104,16 +142,21 @@
|
||||
android:resource="@xml/service_device_info" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".AudioForegroundService"
|
||||
android:foregroundServiceType="mediaPlayback|microphone"
|
||||
android:exported="false">
|
||||
</service>
|
||||
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/provider_paths"/>
|
||||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -17,29 +17,24 @@
|
||||
#include <cstring>
|
||||
#include <sched.h>
|
||||
|
||||
#include "common/OboeDebug.h"
|
||||
#include "oboe/Oboe.h"
|
||||
#include "AudioStreamGateway.h"
|
||||
|
||||
using namespace flowgraph;
|
||||
using namespace oboe::flowgraph;
|
||||
|
||||
oboe::DataCallbackResult AudioStreamGateway::onAudioReady(
|
||||
oboe::AudioStream *audioStream,
|
||||
void *audioData,
|
||||
int numFrames) {
|
||||
|
||||
if (!mSchedulerChecked) {
|
||||
mScheduler = sched_getscheduler(gettid());
|
||||
mSchedulerChecked = true;
|
||||
}
|
||||
maybeHang(getNanoseconds());
|
||||
printScheduler();
|
||||
|
||||
if (mAudioSink != nullptr) {
|
||||
mAudioSink->read(mFramePosition, audioData, numFrames);
|
||||
mFramePosition += numFrames;
|
||||
mAudioSink->read(audioData, numFrames);
|
||||
}
|
||||
|
||||
return oboe::DataCallbackResult::Continue;
|
||||
}
|
||||
|
||||
int AudioStreamGateway::getScheduler() {
|
||||
return mScheduler;
|
||||
}
|
||||
|
||||
@@ -21,24 +21,21 @@
|
||||
|
||||
#include "flowgraph/FlowGraphNode.h"
|
||||
#include "oboe/Oboe.h"
|
||||
#include "OboeTesterStreamCallback.h"
|
||||
|
||||
using namespace flowgraph;
|
||||
using namespace oboe::flowgraph;
|
||||
|
||||
/**
|
||||
* Bridge between an audio flowgraph and an audio device.
|
||||
* Pass in an AudioSink and then pass
|
||||
* this object to the AudioStreamBuilder as a callback.
|
||||
*/
|
||||
class AudioStreamGateway : public oboe::AudioStreamCallback {
|
||||
class AudioStreamGateway : public OboeTesterStreamCallback {
|
||||
public:
|
||||
// AudioStreamGateway(int samplesPerFrame);
|
||||
virtual ~AudioStreamGateway() = default;
|
||||
|
||||
void setAudioSink(std::shared_ptr<flowgraph::FlowGraphSink> sink) {
|
||||
void setAudioSink(std::shared_ptr<oboe::flowgraph::FlowGraphSink> sink) {
|
||||
mAudioSink = sink;
|
||||
if (sink) {
|
||||
mFramePosition = sink->getLastFramePosition();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,13 +46,9 @@ public:
|
||||
void *audioData,
|
||||
int numFrames) override;
|
||||
|
||||
int getScheduler();
|
||||
|
||||
private:
|
||||
int64_t mFramePosition = 0;
|
||||
bool mSchedulerChecked = false;
|
||||
int mScheduler;
|
||||
std::shared_ptr<flowgraph::FlowGraphSink> mAudioSink;
|
||||
|
||||
std::shared_ptr<oboe::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() {
|
||||
getLoopbackProcessor()->setSampleRate(getOutputStream()->getSampleRate());
|
||||
getLoopbackProcessor()->onStartTest();
|
||||
return FullDuplexStream::start();
|
||||
getLoopbackProcessor()->prepareToTest();
|
||||
mWriteReadDeltaValid = false;
|
||||
return FullDuplexStreamWithConversion::start();
|
||||
}
|
||||
|
||||
oboe::DataCallbackResult FullDuplexAnalyzer::onBothStreamsReady(
|
||||
const void *inputData,
|
||||
oboe::DataCallbackResult FullDuplexAnalyzer::onBothStreamsReadyFloat(
|
||||
const float *inputData,
|
||||
int numInputFrames,
|
||||
void *outputData,
|
||||
float *outputData,
|
||||
int numOutputFrames) {
|
||||
|
||||
int32_t inputStride = getInputStream()->getChannelCount();
|
||||
int32_t outputStride = getOutputStream()->getChannelCount();
|
||||
float *inputFloat = (float *) inputData;
|
||||
float *outputFloat = (float *) outputData;
|
||||
auto *inputFloat = static_cast<const float *>(inputData);
|
||||
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,
|
||||
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) {
|
||||
float buffer[2];
|
||||
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++) {
|
||||
buffer[0] = *outputFloat;
|
||||
outputFloat += outputStride;
|
||||
@@ -48,14 +59,15 @@ oboe::DataCallbackResult FullDuplexAnalyzer::onBothStreamsReady(
|
||||
inputFloat += inputStride;
|
||||
mRecording->write(buffer, 1);
|
||||
}
|
||||
// Handle mismatch in in numFrames.
|
||||
buffer[0] = 0.0f; // gap in output
|
||||
// Handle mismatch in numFrames.
|
||||
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++) {
|
||||
buffer[1] = *inputFloat;
|
||||
inputFloat += inputStride;
|
||||
mRecording->write(buffer, 1);
|
||||
}
|
||||
buffer[1] = 0.0f; // gap in input
|
||||
buffer[1] = gapMarker; // gap in input
|
||||
for (int i = numBoth; i < numOutputFrames; i++) {
|
||||
buffer[0] = *outputFloat;
|
||||
outputFloat += outputStride;
|
||||
|
||||
@@ -21,40 +21,52 @@
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "oboe/Oboe.h"
|
||||
#include "FullDuplexStream.h"
|
||||
#include "analyzer/LatencyAnalyzer.h"
|
||||
#include "FullDuplexStreamWithConversion.h"
|
||||
#include "MultiChannelRecording.h"
|
||||
|
||||
class FullDuplexAnalyzer : public FullDuplexStream {
|
||||
class FullDuplexAnalyzer : public FullDuplexStreamWithConversion {
|
||||
public:
|
||||
FullDuplexAnalyzer() {}
|
||||
FullDuplexAnalyzer(LoopbackProcessor *processor)
|
||||
: mLoopbackProcessor(processor) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when data is available on both streams.
|
||||
* Caller should override this method.
|
||||
*/
|
||||
oboe::DataCallbackResult onBothStreamsReady(
|
||||
const void *inputData,
|
||||
oboe::DataCallbackResult onBothStreamsReadyFloat(
|
||||
const float *inputData,
|
||||
int numInputFrames,
|
||||
void *outputData,
|
||||
float *outputData,
|
||||
int numOutputFrames
|
||||
) override;
|
||||
|
||||
oboe::Result start() override;
|
||||
|
||||
bool isDone() {
|
||||
return false;
|
||||
LoopbackProcessor *getLoopbackProcessor() {
|
||||
return mLoopbackProcessor;
|
||||
}
|
||||
|
||||
virtual LoopbackProcessor *getLoopbackProcessor() = 0;
|
||||
|
||||
void setRecording(MultiChannelRecording *recording) {
|
||||
mRecording = recording;
|
||||
}
|
||||
|
||||
bool isWriteReadDeltaValid() {
|
||||
return mWriteReadDeltaValid;
|
||||
}
|
||||
|
||||
int64_t getWriteReadDelta() {
|
||||
return mWriteReadDelta;
|
||||
}
|
||||
|
||||
private:
|
||||
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() {
|
||||
int32_t delayFrames = (int32_t) (kMaxDelayTimeSeconds * getOutputStream()->getSampleRate());
|
||||
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(
|
||||
const void *inputData,
|
||||
double FullDuplexEcho::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.load());
|
||||
return -2.0;
|
||||
}
|
||||
return mPeakDetectors[index].getLevel();
|
||||
}
|
||||
|
||||
oboe::DataCallbackResult FullDuplexEcho::onBothStreamsReadyFloat(
|
||||
const float *inputData,
|
||||
int numInputFrames,
|
||||
void *outputData,
|
||||
float *outputData,
|
||||
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);
|
||||
float *inputFloat = (float *)inputData;
|
||||
float *outputFloat = (float *)outputData;
|
||||
auto *inputFloat = const_cast<float *>(inputData);
|
||||
float *outputFloat = outputData;
|
||||
// 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 outputStride = getOutputStream()->getChannelCount();
|
||||
float delayFrames = mDelayTimeSeconds * getOutputStream()->getSampleRate();
|
||||
while (framesToEcho-- > 0) {
|
||||
*outputFloat = mDelayLine->process(delayFrames, *inputFloat); // mono delay
|
||||
|
||||
for (int iChannel = 0; iChannel < inputStride; iChannel++) {
|
||||
float sample = * (inputFloat + iChannel);
|
||||
mPeakDetectors[iChannel].process(sample);
|
||||
}
|
||||
|
||||
inputFloat += inputStride;
|
||||
outputFloat += outputStride;
|
||||
}
|
||||
|
||||
@@ -21,28 +21,31 @@
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "oboe/Oboe.h"
|
||||
#include "FullDuplexStream.h"
|
||||
#include "analyzer/LatencyAnalyzer.h"
|
||||
#include "FullDuplexStreamWithConversion.h"
|
||||
#include "InterpolatingDelayLine.h"
|
||||
|
||||
class FullDuplexEcho : public FullDuplexStream {
|
||||
class FullDuplexEcho : public FullDuplexStreamWithConversion {
|
||||
public:
|
||||
FullDuplexEcho() {
|
||||
setMNumInputBurstsCushion(0);
|
||||
setNumInputBurstsCushion(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when data is available on both streams.
|
||||
* Caller should override this method.
|
||||
*/
|
||||
oboe::DataCallbackResult onBothStreamsReady(
|
||||
const void *inputData,
|
||||
oboe::DataCallbackResult onBothStreamsReadyFloat(
|
||||
const float *inputData,
|
||||
int numInputFrames,
|
||||
void *outputData,
|
||||
float *outputData,
|
||||
int numOutputFrames
|
||||
) override;
|
||||
|
||||
oboe::Result start() override;
|
||||
|
||||
double getPeakLevel(int index);
|
||||
|
||||
void setDelayTime(double delayTimeSeconds) {
|
||||
mDelayTimeSeconds = delayTimeSeconds;
|
||||
}
|
||||
@@ -51,6 +54,9 @@ private:
|
||||
std::unique_ptr<InterpolatingDelayLine> mDelayLine;
|
||||
static constexpr double kMaxDelayTimeSeconds = 4.0;
|
||||
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 "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::AudioStream *audioStream,
|
||||
void *audioData,
|
||||
int numFrames) {
|
||||
int32_t channelCount = audioStream->getChannelCount();
|
||||
|
||||
if (audioStream->getFormat() == oboe::AudioFormat::I16) {
|
||||
int16_t *shortData = (int16_t *) audioData;
|
||||
if (mRecording != nullptr) {
|
||||
mRecording->write(shortData, numFrames);
|
||||
}
|
||||
int16_t *frameData = shortData;
|
||||
for (int iFrame = 0; iFrame < numFrames; iFrame++) {
|
||||
for (int iChannel = 0; iChannel < channelCount; iChannel++) {
|
||||
float sample = frameData[iChannel] / 32768.0f;
|
||||
mPeakDetectors[iChannel].process(sample);
|
||||
}
|
||||
frameData += channelCount;
|
||||
}
|
||||
} else if (audioStream->getFormat() == oboe::AudioFormat::Float) {
|
||||
float *floatData = (float *) audioData;
|
||||
if (mRecording != nullptr) {
|
||||
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;
|
||||
maybeHang(getNanoseconds());
|
||||
printScheduler();
|
||||
mInputConverter->convertToInternalOutput(numFrames * channelCount, audioData);
|
||||
float *floatData = (float *) mInputConverter->getOutputBuffer();
|
||||
if (mRecording != nullptr) {
|
||||
mRecording->write(floatData, numFrames);
|
||||
}
|
||||
int32_t sampleIndex = 0;
|
||||
for (int iFrame = 0; iFrame < numFrames; iFrame++) {
|
||||
for (int iChannel = 0; iChannel < channelCount; iChannel++) {
|
||||
float sample = floatData[sampleIndex++];
|
||||
mPeakDetectors[iChannel].process(sample);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,18 +23,31 @@
|
||||
|
||||
// TODO #include "flowgraph/FlowGraph.h"
|
||||
#include "oboe/Oboe.h"
|
||||
#include "MultiChannelRecording.h"
|
||||
|
||||
#include "analyzer/PeakDetector.h"
|
||||
#include "FormatConverterBox.h"
|
||||
#include "MultiChannelRecording.h"
|
||||
#include "OboeTesterStreamCallback.h"
|
||||
|
||||
constexpr int kMaxInputChannels = 8;
|
||||
|
||||
class InputStreamCallbackAnalyzer : public oboe::AudioStreamCallback {
|
||||
class InputStreamCallbackAnalyzer : public OboeTesterStreamCallback {
|
||||
public:
|
||||
|
||||
void reset() {
|
||||
for (auto detector : mPeakDetectors) {
|
||||
detector.reset();
|
||||
for (int iChannel = 0; iChannel < mNumChannels; iChannel++) {
|
||||
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;
|
||||
}
|
||||
|
||||
double getPeakLevel(int index) {
|
||||
return mPeakDetectors[index].getLevel();
|
||||
}
|
||||
double getPeakLevel(int index);
|
||||
|
||||
void setMinimumFramesBeforeRead(int32_t numFrames) {
|
||||
mMinimumFramesBeforeRead = numFrames;
|
||||
@@ -62,11 +73,13 @@ public:
|
||||
}
|
||||
|
||||
public:
|
||||
PeakDetector mPeakDetectors[kMaxInputChannels];
|
||||
MultiChannelRecording *mRecording = nullptr;
|
||||
int32_t mNumChannels = 0;
|
||||
std::unique_ptr<PeakDetector[]> mPeakDetectors;
|
||||
MultiChannelRecording *mRecording = nullptr;
|
||||
|
||||
private:
|
||||
int32_t mMinimumFramesBeforeRead = 0;
|
||||
std::unique_ptr<FormatConverterBox> mInputConverter;
|
||||
int32_t mMinimumFramesBeforeRead = 0;
|
||||
};
|
||||
|
||||
#endif //NATIVEOBOE_INPUTSTREAMCALLBACKANALYZER_H
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "common/OboeDebug.h"
|
||||
#include <algorithm>
|
||||
|
||||
#include "InterpolatingDelayLine.h"
|
||||
|
||||
InterpolatingDelayLine::InterpolatingDelayLine(int32_t delaySize) {
|
||||
|
||||
@@ -21,9 +21,6 @@
|
||||
#include <unistd.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "oboe/Oboe.h"
|
||||
#include "FullDuplexStream.h"
|
||||
|
||||
/**
|
||||
* Monophonic delay line.
|
||||
*/
|
||||
|
||||
@@ -14,10 +14,20 @@
|
||||
* 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 <iostream>
|
||||
#if DEBUG_CLOSE_RACE
|
||||
#include <thread>
|
||||
#endif // DEBUG_CLOSE_RACE
|
||||
#include <vector>
|
||||
|
||||
#include "oboe/AudioClock.h"
|
||||
#include "util/WaveFileWriter.h"
|
||||
|
||||
#include "NativeAudioContext.h"
|
||||
@@ -57,9 +67,9 @@ private:
|
||||
bool ActivityContext::mUseCallback = true;
|
||||
int ActivityContext::callbackSize = 0;
|
||||
|
||||
oboe::AudioStream * ActivityContext::getOutputStream() {
|
||||
std::shared_ptr<oboe::AudioStream> ActivityContext::getOutputStream() {
|
||||
for (auto entry : mOboeStreams) {
|
||||
oboe::AudioStream *oboeStream = entry.second.get();
|
||||
std::shared_ptr<oboe::AudioStream> oboeStream = entry.second;
|
||||
if (oboeStream->getDirection() == oboe::Direction::Output) {
|
||||
return oboeStream;
|
||||
}
|
||||
@@ -67,9 +77,9 @@ oboe::AudioStream * ActivityContext::getOutputStream() {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
oboe::AudioStream * ActivityContext::getInputStream() {
|
||||
std::shared_ptr<oboe::AudioStream> ActivityContext::getInputStream() {
|
||||
for (auto entry : mOboeStreams) {
|
||||
oboe::AudioStream *oboeStream = entry.second.get();
|
||||
std::shared_ptr<oboe::AudioStream> oboeStream = entry.second;
|
||||
if (oboeStream != nullptr) {
|
||||
if (oboeStream->getDirection() == oboe::Direction::Input) {
|
||||
return oboeStream;
|
||||
@@ -88,9 +98,19 @@ int32_t ActivityContext::allocateStreamIndex() {
|
||||
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) {
|
||||
stopBlockingIOThread();
|
||||
oboe::AudioStream *oboeStream = getStream(streamIndex);
|
||||
std::shared_ptr<oboe::AudioStream> oboeStream = getStream(streamIndex);
|
||||
if (oboeStream != nullptr) {
|
||||
oboeStream->close();
|
||||
LOGD("ActivityContext::%s() delete stream %d ", __func__, streamIndex);
|
||||
@@ -99,19 +119,18 @@ void ActivityContext::close(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->getAudioApi() != AudioApi::AAudio) return false;
|
||||
return AAudioExtensions::getInstance().isMMapUsed(oboeStream);
|
||||
return AAudioExtensions::getInstance().isMMapUsed(oboeStream.get());
|
||||
}
|
||||
|
||||
oboe::Result ActivityContext::pause() {
|
||||
oboe::Result result = oboe::Result::OK;
|
||||
stopBlockingIOThread();
|
||||
for (auto entry : mOboeStreams) {
|
||||
oboe::AudioStream *oboeStream = entry.second.get();
|
||||
std::shared_ptr<oboe::AudioStream> oboeStream = entry.second;
|
||||
result = oboeStream->requestPause();
|
||||
printScheduler();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -120,9 +139,8 @@ oboe::Result ActivityContext::stopAllStreams() {
|
||||
oboe::Result result = oboe::Result::OK;
|
||||
stopBlockingIOThread();
|
||||
for (auto entry : mOboeStreams) {
|
||||
oboe::AudioStream *oboeStream = entry.second.get();
|
||||
std::shared_ptr<oboe::AudioStream> oboeStream = entry.second;
|
||||
result = oboeStream->requestStop();
|
||||
printScheduler();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -130,23 +148,23 @@ oboe::Result ActivityContext::stopAllStreams() {
|
||||
void ActivityContext::configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) {
|
||||
// We needed the proxy because we did not know the channelCount when we setup the Builder.
|
||||
if (mUseCallback) {
|
||||
LOGD("ActivityContext::open() set callback to use oboeCallbackProxy, callback size = %d",
|
||||
callbackSize);
|
||||
builder.setCallback(&oboeCallbackProxy);
|
||||
builder.setFramesPerCallback(callbackSize);
|
||||
builder.setDataCallback(&oboeCallbackProxy);
|
||||
}
|
||||
}
|
||||
|
||||
int ActivityContext::open(jint nativeApi,
|
||||
jint sampleRate,
|
||||
jint channelCount,
|
||||
jint channelMask,
|
||||
jint format,
|
||||
jint sharingMode,
|
||||
jint performanceMode,
|
||||
jint inputPreset,
|
||||
jint usage,
|
||||
jint contentType,
|
||||
jint bufferCapacityInFrames,
|
||||
jint deviceId,
|
||||
jint sessionId,
|
||||
jint framesPerBurst,
|
||||
jboolean channelConversionAllowed,
|
||||
jboolean formatConversionAllowed,
|
||||
jint rateConversionQuality,
|
||||
@@ -182,6 +200,9 @@ int ActivityContext::open(jint nativeApi,
|
||||
->setSharingMode((oboe::SharingMode) sharingMode)
|
||||
->setPerformanceMode((oboe::PerformanceMode) performanceMode)
|
||||
->setInputPreset((oboe::InputPreset)inputPreset)
|
||||
->setUsage((oboe::Usage)usage)
|
||||
->setContentType((oboe::ContentType)contentType)
|
||||
->setBufferCapacityInFrames(bufferCapacityInFrames)
|
||||
->setDeviceId(deviceId)
|
||||
->setSessionId((oboe::SessionId) sessionId)
|
||||
->setSampleRate(sampleRate)
|
||||
@@ -190,7 +211,13 @@ int ActivityContext::open(jint nativeApi,
|
||||
->setFormatConversionAllowed(formatConversionAllowed)
|
||||
->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);
|
||||
|
||||
builder.setAudioApi(audioApi);
|
||||
@@ -199,6 +226,12 @@ int ActivityContext::open(jint nativeApi,
|
||||
bool oldMMapEnabled = AAudioExtensions::getInstance().isMMapEnabled();
|
||||
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.
|
||||
std::shared_ptr<oboe::AudioStream> oboeStream;
|
||||
Result result = builder.openStream(oboeStream);
|
||||
@@ -215,7 +248,7 @@ int ActivityContext::open(jint nativeApi,
|
||||
|
||||
createRecording();
|
||||
|
||||
finishOpen(isInput, oboeStream.get());
|
||||
finishOpen(isInput, oboeStream);
|
||||
}
|
||||
|
||||
if (!mUseCallback) {
|
||||
@@ -223,20 +256,24 @@ int ActivityContext::open(jint nativeApi,
|
||||
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 result = oboe::Result::OK;
|
||||
oboe::AudioStream *inputStream = getInputStream();
|
||||
oboe::AudioStream *outputStream = getOutputStream();
|
||||
std::shared_ptr<oboe::AudioStream> inputStream = getInputStream();
|
||||
std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
|
||||
if (inputStream == nullptr && outputStream == nullptr) {
|
||||
LOGD("%s() - no streams defined", __func__);
|
||||
return oboe::Result::ErrorInvalidState; // not open
|
||||
}
|
||||
|
||||
configureForStart();
|
||||
|
||||
audioStreamGateway.reset();
|
||||
result = startStreams();
|
||||
|
||||
if (!mUseCallback && result == oboe::Result::OK) {
|
||||
@@ -245,6 +282,29 @@ oboe::Result ActivityContext::start() {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -259,10 +319,11 @@ int32_t ActivityContext::saveWaveFile(const char *filename) {
|
||||
}
|
||||
MyOboeOutputStream outStream;
|
||||
WaveFileWriter writer(&outStream);
|
||||
|
||||
// You must setup the format before the first write().
|
||||
writer.setFrameRate(mSampleRate);
|
||||
writer.setSamplesPerFrame(mRecording->getChannelCount());
|
||||
writer.setBitsPerSample(24);
|
||||
writer.setFrameCount(mRecording->getSizeInFrames());
|
||||
float buffer[mRecording->getChannelCount()];
|
||||
// Read samples from start to finish.
|
||||
mRecording->rewind();
|
||||
@@ -283,13 +344,25 @@ int32_t ActivityContext::saveWaveFile(const char *filename) {
|
||||
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
|
||||
void ActivityTestOutput::close(int32_t streamIndex) {
|
||||
ActivityContext::close(streamIndex);
|
||||
manyToMulti.reset(nullptr);
|
||||
monoToMulti.reset(nullptr);
|
||||
mVolumeRamp.reset();
|
||||
mSinkFloat.reset();
|
||||
mSinkI16.reset();
|
||||
mSinkI24.reset();
|
||||
mSinkI32.reset();
|
||||
}
|
||||
|
||||
void ActivityTestOutput::setChannelEnabled(int channelIndex, bool enabled) {
|
||||
@@ -313,6 +386,9 @@ void ActivityTestOutput::setChannelEnabled(int channelIndex, bool enabled) {
|
||||
mExponentialShape.output.connect(&sineOscillators[channelIndex].frequency);
|
||||
sineOscillators[channelIndex].output.connect(manyToMulti->inputs[channelIndex].get());
|
||||
break;
|
||||
case SignalType::WhiteNoise:
|
||||
mWhiteNoise.output.connect(manyToMulti->inputs[channelIndex].get());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -321,13 +397,20 @@ void ActivityTestOutput::setChannelEnabled(int channelIndex, bool enabled) {
|
||||
}
|
||||
}
|
||||
|
||||
void ActivityTestOutput::configureForStart() {
|
||||
void ActivityTestOutput::configureAfterOpen() {
|
||||
manyToMulti = std::make_unique<ManyToMultiConverter>(mChannelCount);
|
||||
|
||||
mSinkFloat = std::make_unique<SinkFloat>(mChannelCount);
|
||||
mSinkI16 = std::make_unique<SinkI16>(mChannelCount);
|
||||
std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
|
||||
|
||||
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.frequency.setValue(1.0/kSweepPeriod);
|
||||
@@ -344,35 +427,52 @@ void ActivityTestOutput::configureForStart() {
|
||||
mTriangleOscillator.output.connect(&(mExponentialShape.input));
|
||||
{
|
||||
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++) {
|
||||
sineOscillators[i].setSampleRate(outputStream->getSampleRate());
|
||||
sineOscillators[i].frequency.setValue(frequency);
|
||||
frequency *= 4.0 / 3.0; // each sine is at a higher frequency
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
manyToMulti->output.connect(&(mSinkFloat.get()->input));
|
||||
manyToMulti->output.connect(&(mSinkI16.get()->input));
|
||||
mWhiteNoise.amplitude.setValue(0.5);
|
||||
|
||||
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();
|
||||
mSinkI16->pullReset();
|
||||
mSinkI24->pullReset();
|
||||
mSinkI32->pullReset();
|
||||
|
||||
configureStreamGateway();
|
||||
}
|
||||
|
||||
void ActivityTestOutput::configureStreamGateway() {
|
||||
oboe::AudioStream *outputStream = getOutputStream();
|
||||
std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
|
||||
if (outputStream->getFormat() == oboe::AudioFormat::I16) {
|
||||
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) {
|
||||
audioStreamGateway.setAudioSink(mSinkFloat);
|
||||
}
|
||||
|
||||
if (mUseCallback) {
|
||||
oboeCallbackProxy.setCallback(&audioStreamGateway);
|
||||
oboeCallbackProxy.setDataCallback(&audioStreamGateway);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,7 +480,7 @@ void ActivityTestOutput::runBlockingIO() {
|
||||
int32_t framesPerBlock = getFramesPerBlock();
|
||||
oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Continue;
|
||||
|
||||
oboe::AudioStream *oboeStream = getOutputStream();
|
||||
std::shared_ptr<oboe::AudioStream> oboeStream = getOutputStream();
|
||||
if (oboeStream == nullptr) {
|
||||
LOGE("%s() : no stream found\n", __func__);
|
||||
return;
|
||||
@@ -389,7 +489,7 @@ void ActivityTestOutput::runBlockingIO() {
|
||||
while (threadEnabled.load()
|
||||
&& callbackResult == oboe::DataCallbackResult::Continue) {
|
||||
// generate output by calling the callback
|
||||
callbackResult = audioStreamGateway.onAudioReady(oboeStream,
|
||||
callbackResult = audioStreamGateway.onAudioReady(oboeStream.get(),
|
||||
dataBuffer.get(),
|
||||
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
|
||||
void ActivityTestInput::configureForStart() {
|
||||
void ActivityTestInput::configureAfterOpen() {
|
||||
mInputAnalyzer.reset();
|
||||
if (mUseCallback) {
|
||||
oboeCallbackProxy.setCallback(&mInputAnalyzer);
|
||||
oboeCallbackProxy.setDataCallback(&mInputAnalyzer);
|
||||
}
|
||||
mInputAnalyzer.setRecording(mRecording.get());
|
||||
}
|
||||
@@ -422,7 +533,7 @@ void ActivityTestInput::runBlockingIO() {
|
||||
int32_t framesPerBlock = getFramesPerBlock();
|
||||
oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Continue;
|
||||
|
||||
oboe::AudioStream *oboeStream = getInputStream();
|
||||
std::shared_ptr<oboe::AudioStream> oboeStream = getInputStream();
|
||||
if (oboeStream == nullptr) {
|
||||
LOGE("%s() : no stream found\n", __func__);
|
||||
return;
|
||||
@@ -450,7 +561,7 @@ void ActivityTestInput::runBlockingIO() {
|
||||
}
|
||||
|
||||
// analyze input
|
||||
callbackResult = mInputAnalyzer.onAudioReady(oboeStream,
|
||||
callbackResult = mInputAnalyzer.onAudioReady(oboeStream.get(),
|
||||
dataBuffer.get(),
|
||||
framesRead);
|
||||
}
|
||||
@@ -474,8 +585,7 @@ oboe::Result ActivityRecording::startPlayback() {
|
||||
builder.setChannelCount(mChannelCount)
|
||||
->setSampleRate(mSampleRate)
|
||||
->setFormat(oboe::AudioFormat::Float)
|
||||
->setCallback(&mPlayRecordingCallback)
|
||||
->setAudioApi(oboe::AudioApi::OpenSLES);
|
||||
->setCallback(&mPlayRecordingCallback);
|
||||
oboe::Result result = builder.openStream(&playbackStream);
|
||||
if (result != oboe::Result::OK) {
|
||||
delete playbackStream;
|
||||
@@ -491,13 +601,15 @@ oboe::Result ActivityRecording::startPlayback() {
|
||||
}
|
||||
|
||||
// ======================================================================= ActivityTapToTone
|
||||
void ActivityTapToTone::configureForStart() {
|
||||
void ActivityTapToTone::configureAfterOpen() {
|
||||
monoToMulti = std::make_unique<MonoToMultiConverter>(mChannelCount);
|
||||
|
||||
mSinkFloat = std::make_unique<SinkFloat>(mChannelCount);
|
||||
mSinkI16 = std::make_unique<SinkI16>(mChannelCount);
|
||||
mSinkFloat = std::make_shared<SinkFloat>(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.frequency.setValue(FREQUENCY_SAW_PING);
|
||||
sawPingGenerator.amplitude.setValue(AMPLITUDE_SAW_PING);
|
||||
@@ -505,20 +617,26 @@ void ActivityTapToTone::configureForStart() {
|
||||
sawPingGenerator.output.connect(&(monoToMulti->input));
|
||||
monoToMulti->output.connect(&(mSinkFloat.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();
|
||||
}
|
||||
|
||||
// ======================================================================= ActivityRoundTripLatency
|
||||
// ======================================================================= ActivityFullDuplex
|
||||
void ActivityFullDuplex::configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) {
|
||||
if (isInput) {
|
||||
// Ideally the output streams should be opened first.
|
||||
oboe::AudioStream *outputStream = getOutputStream();
|
||||
std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
|
||||
if (outputStream != nullptr) {
|
||||
// Make sure the capacity is bigger than two bursts.
|
||||
int32_t burst = outputStream->getFramesPerBurst();
|
||||
builder.setBufferCapacityInFrames(2 * burst);
|
||||
// The input and output buffers will run in sync with input empty
|
||||
// and output full. So set the input capacity to match the output.
|
||||
builder.setBufferCapacityInFrames(outputStream->getBufferCapacityInFrames());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -532,15 +650,16 @@ void ActivityEcho::configureBuilder(bool isInput, oboe::AudioStreamBuilder &buil
|
||||
}
|
||||
// only output uses a callback, input is polled
|
||||
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) {
|
||||
mFullDuplexEcho->setInputStream(oboeStream);
|
||||
mFullDuplexEcho->setSharedInputStream(oboeStream);
|
||||
} else {
|
||||
mFullDuplexEcho->setOutputStream(oboeStream);
|
||||
mFullDuplexEcho->setSharedOutputStream(oboeStream);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,45 +668,102 @@ void ActivityRoundTripLatency::configureBuilder(bool isInput, oboe::AudioStreamB
|
||||
ActivityFullDuplex::configureBuilder(isInput, builder);
|
||||
|
||||
if (mFullDuplexLatency.get() == nullptr) {
|
||||
mFullDuplexLatency = std::make_unique<FullDuplexLatency>();
|
||||
mFullDuplexLatency = std::make_unique<FullDuplexAnalyzer>(mLatencyAnalyzer.get());
|
||||
}
|
||||
if (!isInput) {
|
||||
// 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) {
|
||||
mFullDuplexLatency->setInputStream(oboeStream);
|
||||
mFullDuplexLatency->setSharedInputStream(oboeStream);
|
||||
mFullDuplexLatency->setRecording(mRecording.get());
|
||||
} 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
|
||||
void ActivityGlitches::configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) {
|
||||
ActivityFullDuplex::configureBuilder(isInput, builder);
|
||||
|
||||
if (mFullDuplexGlitches.get() == nullptr) {
|
||||
mFullDuplexGlitches = std::make_unique<FullDuplexGlitches>();
|
||||
mFullDuplexGlitches = std::make_unique<FullDuplexAnalyzer>(&mGlitchAnalyzer);
|
||||
}
|
||||
if (!isInput) {
|
||||
// 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) {
|
||||
mFullDuplexGlitches->setInputStream(oboeStream);
|
||||
mFullDuplexGlitches->setSharedInputStream(oboeStream);
|
||||
mFullDuplexGlitches->setRecording(mRecording.get());
|
||||
} 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
|
||||
void ActivityTestDisconnect::close(int32_t streamIndex) {
|
||||
@@ -595,9 +771,9 @@ void ActivityTestDisconnect::close(int32_t streamIndex) {
|
||||
mSinkFloat.reset();
|
||||
}
|
||||
|
||||
void ActivityTestDisconnect::configureForStart() {
|
||||
oboe::AudioStream *outputStream = getOutputStream();
|
||||
oboe::AudioStream *inputStream = getInputStream();
|
||||
void ActivityTestDisconnect::configureAfterOpen() {
|
||||
std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
|
||||
std::shared_ptr<oboe::AudioStream> inputStream = getInputStream();
|
||||
if (outputStream) {
|
||||
mSinkFloat = std::make_unique<SinkFloat>(mChannelCount);
|
||||
sineOscillator = std::make_unique<SineOscillator>();
|
||||
@@ -607,13 +783,13 @@ void ActivityTestDisconnect::configureForStart() {
|
||||
sineOscillator->frequency.setValue(440.0);
|
||||
sineOscillator->amplitude.setValue(AMPLITUDE_SINE);
|
||||
sineOscillator->output.connect(&(monoToMulti->input));
|
||||
|
||||
monoToMulti->output.connect(&(mSinkFloat->input));
|
||||
// Clear framePosition in sine oscillators.
|
||||
mSinkFloat->pullReset();
|
||||
audioStreamGateway.setAudioSink(mSinkFloat);
|
||||
} else if (inputStream) {
|
||||
audioStreamGateway.setAudioSink(nullptr);
|
||||
}
|
||||
oboeCallbackProxy.setCallback(&audioStreamGateway);
|
||||
oboeCallbackProxy.setDataCallback(&audioStreamGateway);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
#ifndef NATIVEOBOE_NATIVEAUDIOCONTEXT_H
|
||||
#define NATIVEOBOE_NATIVEAUDIOCONTEXT_H
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <jni.h>
|
||||
#include <sys/system_properties.h>
|
||||
#include <thread>
|
||||
@@ -27,161 +26,50 @@
|
||||
#include "common/OboeDebug.h"
|
||||
#include "oboe/Oboe.h"
|
||||
|
||||
#include "aaudio/AAudioExtensions.h"
|
||||
#include "AudioStreamGateway.h"
|
||||
|
||||
#include "flowunits/ImpulseOscillator.h"
|
||||
#include "flowgraph/ManyToMultiConverter.h"
|
||||
#include "flowgraph/MonoToMultiConverter.h"
|
||||
#include "flowgraph/RampLinear.h"
|
||||
#include "flowgraph/SinkFloat.h"
|
||||
#include "flowgraph/SinkI16.h"
|
||||
#include "flowgraph/SinkI24.h"
|
||||
#include "flowgraph/SinkI32.h"
|
||||
#include "flowunits/ExponentialShape.h"
|
||||
#include "flowunits/LinearShape.h"
|
||||
#include "flowunits/SineOscillator.h"
|
||||
#include "flowunits/SawtoothOscillator.h"
|
||||
#include "flowunits/TriangleOscillator.h"
|
||||
#include "flowunits/WhiteNoise.h"
|
||||
|
||||
#include "FullDuplexAnalyzer.h"
|
||||
#include "FullDuplexEcho.h"
|
||||
#include "FullDuplexGlitches.h"
|
||||
#include "FullDuplexLatency.h"
|
||||
#include "FullDuplexStream.h"
|
||||
#include "analyzer/GlitchAnalyzer.h"
|
||||
#include "analyzer/DataPathAnalyzer.h"
|
||||
#include "InputStreamCallbackAnalyzer.h"
|
||||
#include "MultiChannelRecording.h"
|
||||
#include "OboeStreamCallbackProxy.h"
|
||||
#include "OboeTools.h"
|
||||
#include "PlayRecordingCallback.h"
|
||||
#include "SawPingGenerator.h"
|
||||
#include "flowunits/TriangleOscillator.h"
|
||||
|
||||
// These must match order in strings.xml and in StreamConfiguration.java
|
||||
#define NATIVE_MODE_UNSPECIFIED 0
|
||||
#define NATIVE_MODE_OPENSLES 1
|
||||
#define NATIVE_MODE_AAUDIO 2
|
||||
|
||||
#define MAX_SINE_OSCILLATORS 8
|
||||
#define MAX_SINE_OSCILLATORS 16
|
||||
#define AMPLITUDE_SINE 1.0
|
||||
#define AMPLITUDE_SAWTOOTH 0.5
|
||||
#define FREQUENCY_SAW_PING 800.0
|
||||
#define AMPLITUDE_SAW_PING 0.8
|
||||
#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
|
||||
|
||||
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.
|
||||
*/
|
||||
@@ -189,12 +77,13 @@ class ActivityContext {
|
||||
public:
|
||||
|
||||
ActivityContext() {}
|
||||
|
||||
virtual ~ActivityContext() = default;
|
||||
|
||||
oboe::AudioStream *getStream(int32_t streamIndex) {
|
||||
std::shared_ptr<oboe::AudioStream> getStream(int32_t streamIndex) {
|
||||
auto it = mOboeStreams.find(streamIndex);
|
||||
if (it != mOboeStreams.end()) {
|
||||
return it->second.get();
|
||||
return it->second;
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -202,52 +91,87 @@ public:
|
||||
|
||||
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,
|
||||
jint sampleRate,
|
||||
jint channelCount,
|
||||
jint channelMask,
|
||||
jint format,
|
||||
jint sharingMode,
|
||||
jint performanceMode,
|
||||
jint inputPreset,
|
||||
jint usage,
|
||||
jint contentType,
|
||||
jint bufferCapacityInFrames,
|
||||
jint deviceId,
|
||||
jint sessionId,
|
||||
jint framesPerBurst,
|
||||
jboolean channelConversionAllowed,
|
||||
jboolean formatConversionAllowed,
|
||||
jint rateConversionQuality,
|
||||
jboolean isMMap,
|
||||
jboolean isInput);
|
||||
|
||||
oboe::Result release();
|
||||
|
||||
virtual void close(int32_t streamIndex);
|
||||
|
||||
void printScheduler() {
|
||||
#if OBOE_ENABLE_LOGGING
|
||||
int scheduler = audioStreamGateway.getScheduler();
|
||||
#endif
|
||||
LOGI("scheduler = 0x%08x, SCHED_FIFO = 0x%08X\n", scheduler, SCHED_FIFO);
|
||||
}
|
||||
|
||||
virtual void configureForStart() {}
|
||||
virtual void configureAfterOpen() {}
|
||||
|
||||
oboe::Result start();
|
||||
|
||||
oboe::Result pause();
|
||||
|
||||
oboe::Result flush();
|
||||
|
||||
oboe::Result stopAllStreams();
|
||||
|
||||
virtual oboe::Result stop() {
|
||||
return stopAllStreams();
|
||||
}
|
||||
|
||||
double getCpuLoad() {
|
||||
float 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);
|
||||
}
|
||||
|
||||
void setHearWorkload(bool enabled) {
|
||||
oboeCallbackProxy.setHearWorkload(enabled);
|
||||
}
|
||||
|
||||
virtual oboe::Result startPlayback() {
|
||||
return oboe::Result::OK;
|
||||
}
|
||||
@@ -275,9 +199,66 @@ public:
|
||||
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);
|
||||
|
||||
int32_t getFramesPerBlock() {
|
||||
@@ -288,6 +269,14 @@ public:
|
||||
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() {
|
||||
return oboeCallbackProxy.getFramesPerCallback();
|
||||
}
|
||||
@@ -296,6 +285,8 @@ public:
|
||||
|
||||
virtual void setSignalType(int signalType) {}
|
||||
|
||||
virtual void setAmplitude(float amplitude) {}
|
||||
|
||||
virtual int32_t saveWaveFile(const char *filename);
|
||||
|
||||
virtual void setMinimumFramesBeforeRead(int32_t numFrames) {}
|
||||
@@ -303,9 +294,19 @@ public:
|
||||
static bool mUseCallback;
|
||||
static int callbackSize;
|
||||
|
||||
double getTimestampLatency(int32_t streamIndex);
|
||||
|
||||
void setCpuAffinityMask(uint32_t mask) {
|
||||
oboeCallbackProxy.setCpuAffinityMask(mask);
|
||||
}
|
||||
|
||||
void setWorkloadReportingEnabled(bool enabled) {
|
||||
oboeCallbackProxy.setWorkloadReportingEnabled(enabled);
|
||||
}
|
||||
|
||||
protected:
|
||||
oboe::AudioStream *getInputStream();
|
||||
oboe::AudioStream *getOutputStream();
|
||||
std::shared_ptr<oboe::AudioStream> getInputStream();
|
||||
std::shared_ptr<oboe::AudioStream> getOutputStream();
|
||||
int32_t allocateStreamIndex();
|
||||
void freeStreamIndex(int32_t streamIndex);
|
||||
|
||||
@@ -314,7 +315,7 @@ protected:
|
||||
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;
|
||||
|
||||
@@ -332,9 +333,11 @@ protected:
|
||||
int32_t mSampleRate = 0; // TODO per stream
|
||||
|
||||
std::atomic<bool> threadEnabled{false};
|
||||
std::thread *dataThread = nullptr;
|
||||
std::thread *dataThread = nullptr; // FIXME never gets deleted
|
||||
|
||||
private:
|
||||
int64_t mInputOpenedAt = 0;
|
||||
int64_t mOutputOpenedAt = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -346,7 +349,7 @@ public:
|
||||
ActivityTestInput() {}
|
||||
virtual ~ActivityTestInput() = default;
|
||||
|
||||
void configureForStart() override;
|
||||
void configureAfterOpen() override;
|
||||
|
||||
double getPeakLevel(int index) override {
|
||||
return mInputAnalyzer.getPeakLevel(index);
|
||||
@@ -354,8 +357,6 @@ public:
|
||||
|
||||
void runBlockingIO() override;
|
||||
|
||||
InputStreamCallbackAnalyzer mInputAnalyzer;
|
||||
|
||||
void setMinimumFramesBeforeRead(int32_t numFrames) override {
|
||||
mInputAnalyzer.setMinimumFramesBeforeRead(numFrames);
|
||||
mMinimumFramesBeforeRead = numFrames;
|
||||
@@ -369,9 +370,13 @@ protected:
|
||||
|
||||
oboe::Result startStreams() override {
|
||||
mInputAnalyzer.reset();
|
||||
mInputAnalyzer.setup(std::max(getInputStream()->getFramesPerBurst(), callbackSize),
|
||||
getInputStream()->getChannelCount(),
|
||||
getInputStream()->getFormat());
|
||||
return getInputStream()->requestStart();
|
||||
}
|
||||
|
||||
InputStreamCallbackAnalyzer mInputAnalyzer;
|
||||
int32_t mMinimumFramesBeforeRead = 0;
|
||||
};
|
||||
|
||||
@@ -414,11 +419,9 @@ public:
|
||||
|
||||
void close(int32_t streamIndex) override;
|
||||
|
||||
oboe::Result startStreams() override {
|
||||
return getOutputStream()->start();
|
||||
}
|
||||
oboe::Result startStreams() override;
|
||||
|
||||
void configureForStart() override;
|
||||
void configureAfterOpen() override;
|
||||
|
||||
virtual void configureStreamGateway();
|
||||
|
||||
@@ -439,6 +442,13 @@ public:
|
||||
mSignalType = (SignalType) signalType;
|
||||
}
|
||||
|
||||
void setAmplitude(float amplitude) override {
|
||||
mAmplitude = amplitude;
|
||||
if (mVolumeRamp) {
|
||||
mVolumeRamp->setTarget(mAmplitude);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
SignalType mSignalType = SignalType::Sine;
|
||||
|
||||
@@ -446,15 +456,22 @@ protected:
|
||||
std::vector<SawtoothOscillator> sawtoothOscillators;
|
||||
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;
|
||||
LinearShape mLinearShape;
|
||||
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<MonoToMultiConverter> monoToMulti;
|
||||
std::shared_ptr<flowgraph::SinkFloat> mSinkFloat;
|
||||
std::shared_ptr<flowgraph::SinkI16> mSinkI16;
|
||||
std::shared_ptr<oboe::flowgraph::SinkFloat> mSinkFloat;
|
||||
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() {}
|
||||
virtual ~ActivityTapToTone() = default;
|
||||
|
||||
void configureForStart() override;
|
||||
void configureAfterOpen() override;
|
||||
|
||||
virtual void setEnabled(bool enabled) override {
|
||||
sawPingGenerator.setEnabled(enabled);
|
||||
virtual void trigger() override {
|
||||
sawPingGenerator.trigger();
|
||||
}
|
||||
|
||||
SawPingGenerator sawPingGenerator;
|
||||
@@ -522,12 +539,16 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
double getPeakLevel(int index) override {
|
||||
return mFullDuplexEcho->getPeakLevel(index);
|
||||
}
|
||||
|
||||
FullDuplexAnalyzer *getFullDuplexAnalyzer() override {
|
||||
return (FullDuplexAnalyzer *) mFullDuplexEcho.get();
|
||||
}
|
||||
|
||||
protected:
|
||||
void finishOpen(bool isInput, oboe::AudioStream *oboeStream) override;
|
||||
void finishOpen(bool isInput, std::shared_ptr<oboe::AudioStream> &oboeStream) override;
|
||||
|
||||
private:
|
||||
std::unique_ptr<FullDuplexEcho> mFullDuplexEcho{};
|
||||
@@ -538,36 +559,74 @@ private:
|
||||
*/
|
||||
class ActivityRoundTripLatency : public ActivityFullDuplex {
|
||||
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 {
|
||||
mAnalyzerLaunched = false;
|
||||
return mFullDuplexLatency->start();
|
||||
}
|
||||
|
||||
void configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) override;
|
||||
|
||||
LatencyAnalyzer *getLatencyAnalyzer() {
|
||||
return mFullDuplexLatency->getLatencyAnalyzer();
|
||||
return mLatencyAnalyzer.get();
|
||||
}
|
||||
|
||||
int32_t getState() override {
|
||||
return getLatencyAnalyzer()->getState();
|
||||
}
|
||||
|
||||
int32_t getResult() override {
|
||||
return getLatencyAnalyzer()->getState();
|
||||
return getLatencyAnalyzer()->getState(); // TODO This does not look right.
|
||||
}
|
||||
|
||||
bool isAnalyzerDone() override {
|
||||
return mFullDuplexLatency->isDone();
|
||||
if (!mAnalyzerLaunched) {
|
||||
mAnalyzerLaunched = launchAnalysisIfReady();
|
||||
}
|
||||
return mLatencyAnalyzer->isDone();
|
||||
}
|
||||
|
||||
FullDuplexAnalyzer *getFullDuplexAnalyzer() override {
|
||||
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:
|
||||
void finishOpen(bool isInput, oboe::AudioStream *oboeStream) override;
|
||||
void finishOpen(bool isInput, std::shared_ptr<oboe::AudioStream> &oboeStream) override;
|
||||
|
||||
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;
|
||||
|
||||
GlitchAnalyzer *getGlitchAnalyzer() {
|
||||
if (!mFullDuplexGlitches) return nullptr;
|
||||
return mFullDuplexGlitches->getGlitchAnalyzer();
|
||||
return &mGlitchAnalyzer;
|
||||
}
|
||||
|
||||
int32_t getState() override {
|
||||
return getGlitchAnalyzer()->getState();
|
||||
}
|
||||
|
||||
int32_t getResult() override {
|
||||
return getGlitchAnalyzer()->getResult();
|
||||
}
|
||||
|
||||
bool isAnalyzerDone() override {
|
||||
return mFullDuplexGlitches->isDone();
|
||||
return mGlitchAnalyzer.isDone();
|
||||
}
|
||||
|
||||
FullDuplexAnalyzer *getFullDuplexAnalyzer() override {
|
||||
@@ -602,10 +662,54 @@ public:
|
||||
}
|
||||
|
||||
protected:
|
||||
void finishOpen(bool isInput, oboe::AudioStream *oboeStream) override;
|
||||
void finishOpen(bool isInput, std::shared_ptr<oboe::AudioStream> &oboeStream) override;
|
||||
|
||||
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;
|
||||
|
||||
oboe::Result startStreams() override {
|
||||
oboe::AudioStream *outputStream = getOutputStream();
|
||||
std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
|
||||
if (outputStream) {
|
||||
return outputStream->start();
|
||||
}
|
||||
|
||||
oboe::AudioStream *inputStream = getInputStream();
|
||||
std::shared_ptr<oboe::AudioStream> inputStream = getInputStream();
|
||||
if (inputStream) {
|
||||
return inputStream->start();
|
||||
}
|
||||
return oboe::Result::ErrorNull;
|
||||
}
|
||||
|
||||
void configureForStart() override;
|
||||
void configureAfterOpen() override;
|
||||
|
||||
private:
|
||||
std::unique_ptr<SineOscillator> sineOscillator;
|
||||
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 {
|
||||
public:
|
||||
@@ -679,6 +784,9 @@ public:
|
||||
case ActivityType::TestDisconnect:
|
||||
currentActivity = &mActivityTestDisconnect;
|
||||
break;
|
||||
case ActivityType::DataPath:
|
||||
currentActivity = &mActivityDataPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -693,6 +801,7 @@ public:
|
||||
ActivityEcho mActivityEcho;
|
||||
ActivityRoundTripLatency mActivityRoundTripLatency;
|
||||
ActivityGlitches mActivityGlitches;
|
||||
ActivityDataPath mActivityDataPath;
|
||||
ActivityTestDisconnect mActivityTestDisconnect;
|
||||
|
||||
private:
|
||||
@@ -708,11 +817,11 @@ private:
|
||||
RoundTripLatency = 5,
|
||||
Glitches = 6,
|
||||
TestDisconnect = 7,
|
||||
DataPath = 8,
|
||||
};
|
||||
|
||||
ActivityType mActivityType = ActivityType::Undefined;
|
||||
ActivityContext *currentActivity = &mActivityTestOutput;
|
||||
|
||||
};
|
||||
|
||||
#endif //NATIVEOBOE_NATIVEAUDIOCONTEXT_H
|
||||
|
||||
@@ -17,40 +17,8 @@
|
||||
#include "common/OboeDebug.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;
|
||||
|
||||
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::AudioStream *audioStream,
|
||||
void *audioData,
|
||||
@@ -58,6 +26,23 @@ oboe::DataCallbackResult OboeStreamCallbackProxy::onAudioReady(
|
||||
oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Stop;
|
||||
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++;
|
||||
mFramesPerCallback = numFrames;
|
||||
|
||||
@@ -65,31 +50,67 @@ oboe::DataCallbackResult OboeStreamCallbackProxy::onAudioReady(
|
||||
return oboe::DataCallbackResult::Stop;
|
||||
}
|
||||
|
||||
s_burnCPU((int32_t)(mWorkload * kWorkloadScaler * numFrames));
|
||||
|
||||
if (mCallback != nullptr) {
|
||||
callbackResult = mCallback->onAudioReady(audioStream, audioData, numFrames);
|
||||
}
|
||||
|
||||
// Update CPU load
|
||||
double calculationTime = (double)(getNanoseconds() - startTimeNanos);
|
||||
double inverseRealTime = audioStream->getSampleRate() / (1.0e9 * numFrames);
|
||||
double currentCpuLoad = calculationTime * inverseRealTime; // avoid a divide
|
||||
mCpuLoad = (mCpuLoad * 0.95) + (currentCpuLoad * 0.05); // simple low pass filter
|
||||
mSynthWorkload.onCallback(mNumWorkloadVoices);
|
||||
if (mNumWorkloadVoices > 0) {
|
||||
// Render into the buffer or discard the synth voices.
|
||||
float *buffer = (audioStream->getChannelCount() == 2 && mHearWorkload)
|
||||
? 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;
|
||||
}
|
||||
|
||||
void OboeStreamCallbackProxy::onErrorBeforeClose(oboe::AudioStream *audioStream, oboe::Result error) {
|
||||
LOGD("OboeStreamCallbackProxy::%s(%p, %d) called", __func__, audioStream, error);
|
||||
if (mCallback != nullptr) {
|
||||
mCallback->onErrorBeforeClose(audioStream, error);
|
||||
int OboeStreamCallbackProxy::applyCpuAffinityMask(uint32_t mask) {
|
||||
int err = 0;
|
||||
// Capture original CPU set so we can restore it.
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
void OboeStreamCallbackProxy::onErrorAfterClose(oboe::AudioStream *audioStream, oboe::Result error) {
|
||||
LOGD("OboeStreamCallbackProxy::%s(%p, %d) called", __func__, audioStream, error);
|
||||
if (mCallback != nullptr) {
|
||||
mCallback->onErrorAfterClose(audioStream, error);
|
||||
if (mask) {
|
||||
cpu_set_t cpu_set;
|
||||
CPU_ZERO(&cpu_set);
|
||||
int cpuCount = sysconf(_SC_NPROCESSORS_CONF);
|
||||
for (int cpuIndex = 0; cpuIndex < cpuCount; cpuIndex++) {
|
||||
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 <sys/types.h>
|
||||
#include <sys/sysinfo.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:
|
||||
|
||||
void setCallback(oboe::AudioStreamCallback *callback) {
|
||||
void setDataCallback(oboe::AudioStreamDataCallback *callback) {
|
||||
mCallback = callback;
|
||||
setCallbackCount(0);
|
||||
mStatistics.clear();
|
||||
mPreviousMask = 0;
|
||||
}
|
||||
|
||||
static void setCallbackReturnStop(bool b) {
|
||||
@@ -54,39 +168,106 @@ public:
|
||||
void *audioData,
|
||||
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
|
||||
* 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) {
|
||||
mWorkload = std::max(0.0, workload);
|
||||
void setWorkload(int32_t workload) {
|
||||
mNumWorkloadVoices = std::max(0, workload);
|
||||
}
|
||||
|
||||
double getWorkload() const {
|
||||
return mWorkload;
|
||||
int32_t getWorkload() const {
|
||||
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;
|
||||
}
|
||||
|
||||
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:
|
||||
static constexpr int32_t kWorkloadScaler = 500;
|
||||
double mWorkload = 0.0;
|
||||
std::atomic<double> mCpuLoad{0};
|
||||
static constexpr double kNsToMsScaler = 0.000001;
|
||||
std::atomic<float> mCpuLoad{0.0f};
|
||||
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;
|
||||
|
||||
int64_t mCallbackCount = 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
|
||||
|
||||
@@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -14,5 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "common/OboeDebug.h"
|
||||
#include "FullDuplexGlitches.h"
|
||||
#ifndef OBOETESTER_OBOETOOLS_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 "SawPingGenerator.h"
|
||||
|
||||
using namespace flowgraph;
|
||||
using namespace oboe::flowgraph;
|
||||
|
||||
SawPingGenerator::SawPingGenerator()
|
||||
: OscillatorBase()
|
||||
@@ -30,6 +30,11 @@ SawPingGenerator::SawPingGenerator()
|
||||
|
||||
SawPingGenerator::~SawPingGenerator() { }
|
||||
|
||||
void SawPingGenerator::reset() {
|
||||
FlowGraphNode::reset();
|
||||
mAcknowledgeCount.store(mRequestCount.load());
|
||||
}
|
||||
|
||||
int32_t SawPingGenerator::onProcess(int numFrames) {
|
||||
|
||||
const float *frequencies = frequency.getBuffer();
|
||||
@@ -58,11 +63,7 @@ int32_t SawPingGenerator::onProcess(int numFrames) {
|
||||
return numFrames;
|
||||
}
|
||||
|
||||
void SawPingGenerator::setEnabled(bool enabled) {
|
||||
if (enabled) {
|
||||
mRequestCount++;
|
||||
} else {
|
||||
mAcknowledgeCount.store(mRequestCount.load());
|
||||
}
|
||||
void SawPingGenerator::trigger() {
|
||||
mRequestCount++;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,9 @@ public:
|
||||
|
||||
int32_t onProcess(int numFrames) override;
|
||||
|
||||
void setEnabled(bool enabled);
|
||||
void trigger();
|
||||
|
||||
void reset() override;
|
||||
|
||||
private:
|
||||
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
|
||||